tdf#169791 sw: Fix trampled ruby base text with adding selection

Handling for non-contiguous selections in the Asian Phonetic Guide was
broken by the change that introduced base text grouping. This change
restores the lost functionality.

Regression from
 commit 3d9b8701cb
 Author: Jonathan Clark <jonathan@libreoffice.org>
 Date:   Tue Sep 10 09:06:51 2024 -0600

Change-Id: Id3129f370fa72ab180f41a9816f9cd4d8718f847
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/195112
Tested-by: Jenkins
Reviewed-by: Jonathan Clark <jonathan@libreoffice.org>
(cherry picked from commit 1e275b2e0f)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/195259
Reviewed-by: Adolfo Jayme Barrientos <fitojb@ubuntu.com>
This commit is contained in:
Jonathan Clark
2025-12-09 14:02:46 +01:00
committed by Adolfo Jayme Barrientos
parent 12a6b103fd
commit 1cf5e10ad9
6 changed files with 298 additions and 139 deletions
+86 -34
View File
@@ -67,6 +67,7 @@ constexpr OUString cRubyText = u"RubyText"_ustr;
constexpr OUString cRubyAdjust = u"RubyAdjust"_ustr;
constexpr OUString cRubyPosition = u"RubyPosition"_ustr;
constexpr OUString cRubyCharStyleName = u"RubyCharStyleName"_ustr;
constexpr OUString cRubySequence = u"RubySequenceNumber"_ustr;
} // end anonymous namespace
@@ -125,66 +126,116 @@ public:
virtual void SAL_CALL selectionChanged(const css::lang::EventObject& aEvent) override;
virtual void SAL_CALL disposing(const css::lang::EventObject& Source) override;
bool IsSelectionGrouped() { return aRubyValues.getLength() < 2; }
bool IsSelectionGrouped()
{
auto nLastSequence = std::numeric_limits<sal_Int32>::lowest();
for (const PropertyValues& rVals : aRubyValues)
{
for (const PropertyValue& rVal : rVals)
{
sal_Int32 nValue;
if ((rVal.Name == cRubySequence) && (rVal.Value >>= nValue))
{
if (nValue <= nLastSequence)
{
return false;
}
nLastSequence = nValue;
}
}
}
return true;
}
void MakeSelectionGrouped()
{
if (aRubyValues.getLength() < 2)
if (IsSelectionGrouped())
{
return;
}
sal_Int32 nSequences = 0;
for (const PropertyValue& rVal : aRubyValues[aRubyValues.size() - 1])
{
if (rVal.Name == cRubySequence)
{
rVal.Value >>= nSequences;
}
}
++nSequences;
Sequence<PropertyValues> aNewRubyValues{ nSequences };
PropertyValues* pNewRubyValues = aNewRubyValues.getArray();
OUString sBaseTmp;
OUStringBuffer aBaseString;
for (const PropertyValues& rVals : aRubyValues)
{
sBaseTmp.clear();
for (const PropertyValue& rVal : rVals)
OUStringBuffer aRubyString;
sal_Int32 nCurrSequence = 0;
bool bAtStartOfSequence = true;
auto fnFinishSequence = [&] {
for (PropertyValue& rVal : asNonConstRange(pNewRubyValues[nCurrSequence]))
{
if (rVal.Name == cRubyBaseText)
{
rVal.Value >>= sBaseTmp;
sBaseTmp = aBaseString.makeStringAndClear();
rVal.Value <<= sBaseTmp;
}
else if (rVal.Name == cRubyText)
{
sBaseTmp = aRubyString.makeStringAndClear();
rVal.Value <<= sBaseTmp;
}
}
};
aBaseString.append(sBaseTmp);
}
Sequence<PropertyValues> aNewRubyValues{ 1 };
PropertyValues* pNewRubyValues = aNewRubyValues.getArray();
// Copy some reasonable style values from the previous ruby array
pNewRubyValues[0] = aRubyValues[0];
for (const PropertyValues& rVals : aRubyValues)
{
// Check for advance
for (const PropertyValue& rVal : rVals)
{
if (rVal.Name == cRubyText)
if (rVal.Name == cRubySequence)
{
rVal.Value >>= sBaseTmp;
if (!sBaseTmp.isEmpty())
sal_Int32 nSeq = nCurrSequence;
rVal.Value >>= nSeq;
if (nSeq > nCurrSequence)
{
pNewRubyValues[0] = rVals;
fnFinishSequence();
nCurrSequence = nSeq;
bAtStartOfSequence = true;
break;
}
}
}
if (bAtStartOfSequence)
{
// Copy some reasonable style values from the previous ruby array
pNewRubyValues[nCurrSequence] = rVals;
bAtStartOfSequence = false;
}
for (const PropertyValue& rVal : rVals)
{
if (rVal.Name == cRubyBaseText)
{
sBaseTmp.clear();
rVal.Value >>= sBaseTmp;
aBaseString.append(sBaseTmp);
}
else if (rVal.Name == cRubyText)
{
sBaseTmp.clear();
rVal.Value >>= sBaseTmp;
aRubyString.append(sBaseTmp);
}
}
}
PropertyValue* pNewValues = pNewRubyValues[0].getArray();
for (sal_Int32 i = 0; i < pNewRubyValues[0].getLength(); ++i)
{
if (pNewValues[i].Name == cRubyBaseText)
{
sBaseTmp = aBaseString;
pNewValues[i].Value <<= sBaseTmp;
}
else if (pNewValues[i].Name == cRubyText)
{
sBaseTmp.clear();
pNewValues[i].Value <<= sBaseTmp;
}
}
fnFinishSequence();
aRubyValues = std::move(aNewRubyValues);
}
@@ -363,13 +414,14 @@ void SvxRubyData_Impl::AssertOneEntry()
{
aRubyValues.realloc(1);
Sequence<PropertyValue>& rValues = aRubyValues.getArray()[0];
rValues.realloc(5);
rValues.realloc(6);
PropertyValue* pValues = rValues.getArray();
pValues[0].Name = cRubyBaseText;
pValues[1].Name = cRubyText;
pValues[2].Name = cRubyAdjust;
pValues[3].Name = cRubyPosition;
pValues[4].Name = cRubyCharStyleName;
pValues[5].Name = cRubySequence;
}
}
+5
View File
@@ -25,6 +25,8 @@ class SwRubyListEntry
{
OUString m_sText;
SwFormatRuby m_aRubyAttr;
sal_Int32 m_nSequence = 0;
public:
SwRubyListEntry() : m_aRubyAttr( OUString() ) {}
@@ -34,6 +36,9 @@ public:
const SwFormatRuby& GetRubyAttr() const { return m_aRubyAttr; }
SwFormatRuby& GetRubyAttr() { return m_aRubyAttr; }
void SetRubyAttr( const SwFormatRuby& rAttr ) { m_aRubyAttr = rAttr; }
void SetSequence(sal_Int32 nSequence) { m_nSequence = nSequence; }
sal_Int32 GetSequence() const { return m_nSequence; }
};
#endif //_ INCLUDED_SW_INC_RUBYLIST_HXX
+1
View File
@@ -624,6 +624,7 @@ inline constexpr OUString UNO_NAME_RUBY_ADJUST = u"RubyAdjust"_ustr;
inline constexpr OUString UNO_NAME_RUBY_CHAR_STYLE_NAME = u"RubyCharStyleName"_ustr;
inline constexpr OUString UNO_NAME_RUBY_IS_ABOVE = u"RubyIsAbove"_ustr;
inline constexpr OUString UNO_NAME_RUBY_POSITION = u"RubyPosition"_ustr;
inline constexpr OUString UNO_NAME_RUBY_SEQUENCE_NUMBER = u"RubySequenceNumber"_ustr;
inline constexpr OUString UNO_NAME_FOOTNOTE_HEIGHT = u"FootnoteHeight"_ustr;
inline constexpr OUString UNO_NAME_FOOTNOTE_LINE_WEIGHT = u"FootnoteLineWeight"_ustr;
inline constexpr OUString UNO_NAME_FOOTNOTE_LINE_COLOR = u"FootnoteLineColor"_ustr;
+77 -1
View File
@@ -2019,6 +2019,11 @@ void SwDocTest::testFillRubyList()
for (auto const& rRuby : aRubies)
{
aTemp.append(rRuby->GetText() + u"["_ustr + rRuby->GetRubyAttr().GetText() + u"]"_ustr);
if (rRuby->GetSequence())
{
aTemp.append(u"("_ustr + OUString::number(rRuby->GetSequence()) + u")"_ustr);
}
}
return aTemp.makeStringAndClear();
@@ -2162,6 +2167,34 @@ void SwDocTest::testFillRubyList()
CPPUNIT_ASSERT_EQUAL(u"学校[]"_ustr, fnGetCombinedString(aPaM));
}
// tdf#169791: Characteristic test for ruby with non-contiguous selection
{
fnAppendJapanese(u"学校に行こう"_ustr);
SwPaM aPaM0{ *aPaM.GetPoint(), *aPaM.GetPoint() };
aPaM0.GetMark()->AdjustContent(2);
SwPaM aPaM1{ *aPaM.GetPoint(), *aPaM.GetPoint(), &aPaM0 };
aPaM1.GetPoint()->AdjustContent(3);
aPaM1.GetMark()->AdjustContent(4);
CPPUNIT_ASSERT_EQUAL(u"学校[]行[](1)"_ustr, fnGetCombinedString(aPaM1));
}
// tdf#169791: Another non-contiguous selection
{
fnAppendJapanese(u"経済学者の経済学者"_ustr);
SwPaM aPaM0{ *aPaM.GetPoint(), *aPaM.GetPoint() };
aPaM0.GetMark()->AdjustContent(4);
SwPaM aPaM1{ *aPaM.GetPoint(), *aPaM.GetPoint(), &aPaM0 };
aPaM1.GetPoint()->AdjustContent(5);
aPaM1.GetMark()->AdjustContent(9);
CPPUNIT_ASSERT_EQUAL(u"経済[]学者[]経済[](1)学者[](1)"_ustr, fnGetCombinedString(aPaM1));
}
}
void SwDocTest::testSetRubyList()
@@ -2191,11 +2224,13 @@ void SwDocTest::testSetRubyList()
CPPUNIT_ASSERT_EQUAL(rText, aPaM.GetText());
};
auto fnAppendRuby = [](SwRubyList* rList, OUString aBase, OUString aRuby)
auto fnAppendRuby
= [](SwRubyList* rList, OUString aBase, OUString aRuby, sal_Int32 nSequence = 0)
{
auto pEnt = std::make_unique<SwRubyListEntry>();
pEnt->SetText(std::move(aBase));
pEnt->SetRubyAttr(SwFormatRuby{ std::move(aRuby) });
pEnt->SetSequence(nSequence);
rList->push_back(std::move(pEnt));
};
@@ -2559,6 +2594,47 @@ void SwDocTest::testSetRubyList()
CPPUNIT_ASSERT_EQUAL(u"学森林[しんりん]海上[かいじょう]地面[じめん]校"_ustr,
fnGetCombinedString());
}
// tdf#169791: Characteristic test for ruby with non-contiguous selection
{
fnAppendJapanese(u"学校に行こう"_ustr);
SwPaM aPaM0{ *aPaM.GetPoint(), *aPaM.GetPoint() };
aPaM0.GetMark()->AdjustContent(2);
SwPaM aPaM1{ *aPaM.GetPoint(), *aPaM.GetPoint(), &aPaM0 };
aPaM1.GetPoint()->AdjustContent(3);
aPaM1.GetMark()->AdjustContent(4);
SwRubyList rList;
fnAppendRuby(&rList, u"学校"_ustr, u"がっこう"_ustr, /*nSequence*/ 0);
fnAppendRuby(&rList, u"行"_ustr, u"い"_ustr, /*nSequence*/ 1);
m_pDoc->SetRubyList(aPaM1, rList);
CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]に行[い]こう"_ustr, fnGetCombinedString());
}
// tdf#169791: Test for ruby with non-contiguous selection and deleted inner subsequence
{
fnAppendJapanese(u"経済学者の経済学者"_ustr);
SwPaM aPaM0{ *aPaM.GetPoint(), *aPaM.GetPoint() };
aPaM0.GetMark()->AdjustContent(4);
SwPaM aPaM1{ *aPaM.GetPoint(), *aPaM.GetPoint(), &aPaM0 };
aPaM1.GetPoint()->AdjustContent(5);
aPaM1.GetMark()->AdjustContent(9);
SwRubyList rList;
fnAppendRuby(&rList, u"経済学者"_ustr, u"けいざいがくしゃ"_ustr, /*nSequence*/ 0);
fnAppendRuby(&rList, u"経済学者"_ustr, u"けいざいがくしゃ"_ustr, /*nSequence*/ 1);
m_pDoc->SetRubyList(aPaM1, rList);
CPPUNIT_ASSERT_EQUAL(u"経済学者[けいざいがくしゃ]の経済学者[けいざいがくしゃ]"_ustr,
fnGetCombinedString());
}
}
CPPUNIT_TEST_SUITE_REGISTRATION(SwDocTest);
+117 -103
View File
@@ -49,6 +49,7 @@ constexpr int nMaxBaseTexts = 30;
*/
sal_uInt16 SwDoc::FillRubyList( const SwPaM& rPam, SwRubyList& rList )
{
sal_Int32 nCurrSequence = 0;
const SwPaM *_pStartCursor = rPam.GetNext(),
*_pStartCursor2 = _pStartCursor;
bool bCheckEmpty = &rPam != _pStartCursor;
@@ -59,6 +60,7 @@ sal_uInt16 SwDoc::FillRubyList( const SwPaM& rPam, SwRubyList& rList )
SwPaM aPam( *pStart );
do {
std::unique_ptr<SwRubyListEntry> pNew(new SwRubyListEntry);
pNew->SetSequence(nCurrSequence);
if( pEnd != pStart )
{
aPam.SetMark();
@@ -85,6 +87,7 @@ sal_uInt16 SwDoc::FillRubyList( const SwPaM& rPam, SwRubyList& rList )
if (nMaxBaseTexts <= rList.size())
break;
_pStartCursor = _pStartCursor->GetNext();
++nCurrSequence;
} while( _pStartCursor != _pStartCursor2 );
return rList.size();
@@ -100,124 +103,135 @@ void SwDoc::SetRubyList(SwPaM& rPam, const SwRubyList& rList)
SwRubyList::size_type nListEntry = 0;
int nCurrBaseTexts = 0;
sal_Int32 nTotalContentGrowth = 0;
sal_Int32 nCurrSequence = 0;
const SwPaM* pStartCursor = rPam.GetNext();
auto [pStart, pEnd] = pStartCursor->StartEnd();
bool bCheckEmpty = (&rPam == pStartCursor) || (pStart != pEnd && *pStart != *pEnd);
// Sequentially replace as many spans as possible
SwPaM aPam(*pStart);
while (bCheckEmpty && nListEntry < rList.size() && nCurrBaseTexts < nMaxBaseTexts)
const SwPaM* pStartCursor2 = pStartCursor;
do
{
if (pEnd != pStart)
auto [pStart, pEnd] = pStartCursor->StartEnd();
bool bCheckEmpty = (&rPam == pStartCursor) || (pStart != pEnd && *pStart != *pEnd);
// Sequentially replace as many spans as possible
SwPaM aPam(*pStart);
while (bCheckEmpty && nListEntry < rList.size()
&& rList[nListEntry]->GetSequence() == nCurrSequence
&& nCurrBaseTexts < nMaxBaseTexts)
{
aPam.SetMark();
*aPam.GetMark() = *pEnd;
if (pEnd != pStart)
{
aPam.SetMark();
*aPam.GetMark() = *pEnd;
}
SwRubyListEntry aCheckEntry;
auto bSelected = SelectNextRubyChars(aPam, aCheckEntry);
if (bSelected)
{
++nCurrBaseTexts;
// Existing ruby text was located. Apply the new attributes.
const SwRubyListEntry* pEntry = rList[nListEntry++].get();
if (aCheckEntry.GetRubyAttr() != pEntry->GetRubyAttr())
{
// set/reset the attribute
if (!pEntry->GetRubyAttr().GetText().isEmpty())
{
getIDocumentContentOperations().InsertPoolItem(aPam, pEntry->GetRubyAttr());
}
else
{
ResetAttrs(aPam, true, aDelArr);
}
}
if (aCheckEntry.GetText() != pEntry->GetText())
{
if (pEntry->GetText().isEmpty())
{
ResetAttrs(aPam, true, aDelArr);
}
// text is changed, so replace the original
getIDocumentContentOperations().ReplaceRange(aPam, pEntry->GetText(), false);
aPam.Exchange();
}
aPam.DeleteMark();
}
else
{
// No existing ruby text located. Advance to next paragraph.
aPam.DeleteMark();
aPam.Move(fnMoveForward, GoInNode);
}
// Stop substituting when the cursor advances to the end of the selection.
if (*aPam.GetPoint() >= *pEnd)
{
break;
}
}
SwRubyListEntry aCheckEntry;
auto bSelected = SelectNextRubyChars(aPam, aCheckEntry);
if (bSelected)
// Delete any spans past the end of the ruby list
while ((nListEntry == rList.size() || rList[nListEntry]->GetSequence() != nCurrSequence)
&& nCurrBaseTexts < nMaxBaseTexts && *aPam.GetPoint() < *pEnd)
{
++nCurrBaseTexts;
// Existing ruby text was located. Apply the new attributes.
const SwRubyListEntry* pEntry = rList[nListEntry++].get();
if (aCheckEntry.GetRubyAttr() != pEntry->GetRubyAttr())
if (pEnd != pStart)
{
// set/reset the attribute
aPam.SetMark();
*aPam.GetMark() = *pEnd;
}
SwRubyListEntry aCheckEntry;
auto bSelected = SelectNextRubyChars(aPam, aCheckEntry);
if (bSelected)
{
++nCurrBaseTexts;
ResetAttrs(aPam, true, aDelArr);
getIDocumentContentOperations().DeleteRange(aPam);
aPam.Exchange();
aPam.DeleteMark();
}
else
{
// No existing ruby text located. Advance to next paragraph.
aPam.DeleteMark();
aPam.Move(fnMoveForward, GoInNode);
}
}
// Insert any spans past the end of the base text list
while (nListEntry < rList.size() && rList[nListEntry]->GetSequence() == nCurrSequence)
{
const SwRubyListEntry* pEntry = rList[nListEntry++].get();
if (!pEntry->GetText().isEmpty())
{
aPam.SetMark();
getIDocumentContentOperations().InsertString(aPam, pEntry->GetText());
aPam.GetMark()->AdjustContent(-pEntry->GetText().getLength());
if (!pEntry->GetRubyAttr().GetText().isEmpty())
{
getIDocumentContentOperations().InsertPoolItem(aPam, pEntry->GetRubyAttr());
}
else
{
ResetAttrs(aPam, true, aDelArr);
}
aPam.DeleteMark();
nTotalContentGrowth += pEntry->GetText().getLength();
}
if (aCheckEntry.GetText() != pEntry->GetText())
{
if (pEntry->GetText().isEmpty())
{
ResetAttrs(aPam, true, aDelArr);
}
// text is changed, so replace the original
getIDocumentContentOperations().ReplaceRange(aPam, pEntry->GetText(), false);
aPam.Exchange();
}
aPam.DeleteMark();
}
else
{
// No existing ruby text located. Advance to next paragraph.
aPam.DeleteMark();
aPam.Move(fnMoveForward, GoInNode);
}
// Stop substituting when the cursor advances to the end of the selection.
if (*aPam.GetPoint() >= *pEnd)
{
break;
}
}
// Delete any spans past the end of the ruby list
while (nListEntry == rList.size() && nCurrBaseTexts < nMaxBaseTexts && *aPam.GetPoint() < *pEnd)
{
if (pEnd != pStart)
{
aPam.SetMark();
*aPam.GetMark() = *pEnd;
}
SwRubyListEntry aCheckEntry;
auto bSelected = SelectNextRubyChars(aPam, aCheckEntry);
if (bSelected)
{
++nCurrBaseTexts;
ResetAttrs(aPam, true, aDelArr);
getIDocumentContentOperations().DeleteRange(aPam);
aPam.Exchange();
aPam.DeleteMark();
}
else
{
// No existing ruby text located. Advance to next paragraph.
aPam.DeleteMark();
aPam.Move(fnMoveForward, GoInNode);
}
}
// Insert any spans past the end of the base text list
sal_Int32 nTotalContentGrowth = 0;
while (nListEntry < rList.size())
{
const SwRubyListEntry* pEntry = rList[nListEntry++].get();
if (!pEntry->GetText().isEmpty())
{
aPam.SetMark();
getIDocumentContentOperations().InsertString(aPam, pEntry->GetText());
aPam.GetMark()->AdjustContent(-pEntry->GetText().getLength());
if (!pEntry->GetRubyAttr().GetText().isEmpty())
{
getIDocumentContentOperations().InsertPoolItem(aPam, pEntry->GetRubyAttr());
}
aPam.DeleteMark();
nTotalContentGrowth += pEntry->GetText().getLength();
}
}
pStartCursor = pStartCursor->GetNext();
++nCurrSequence;
} while (pStartCursor != pStartCursor2);
// Expand selection to account for insertion
rPam.Normalize();
+12 -1
View File
@@ -509,8 +509,9 @@ Sequence< Sequence< PropertyValue > > SwXTextView::getRubyList( sal_Bool /*bAuto
const OUString& rEntryText = pEntry->GetText();
const SwFormatRuby& rAttr = pEntry->GetRubyAttr();
sal_Int32 nSequence = pEntry->GetSequence();
pRet[n].realloc(6);
pRet[n].realloc(7);
PropertyValue* pValues = pRet[n].getArray();
pValues[0].Name = UNO_NAME_RUBY_BASE_TEXT;
pValues[0].Value <<= rEntryText;
@@ -525,6 +526,8 @@ Sequence< Sequence< PropertyValue > > SwXTextView::getRubyList( sal_Bool /*bAuto
pValues[4].Value <<= !rAttr.GetPosition();
pValues[5].Name = UNO_NAME_RUBY_POSITION;
pValues[5].Value <<= rAttr.GetPosition();
pValues[6].Name = UNO_NAME_RUBY_SEQUENCE_NUMBER;
pValues[6].Value <<= nSequence;
}
return aRet;
}
@@ -594,6 +597,14 @@ void SAL_CALL SwXTextView::setRubyList(
if(rProperty.Value >>= nTmp)
pEntry->GetRubyAttr().SetPosition( nTmp );
}
else if (rProperty.Name == UNO_NAME_RUBY_SEQUENCE_NUMBER)
{
sal_Int32 nTmp = 0;
if (rProperty.Value >>= nTmp)
{
pEntry->SetSequence(nTmp);
}
}
}
aList.push_back(std::move(pEntry));
}