include/svx/rubydialog.hxx                  |    2 
 svx/source/dialog/rubydialog.cxx            |  150 ++++++++++++++++++++-
 svx/uiconfig/ui/asianphoneticguidedialog.ui |   14 ++
 sw/qa/core/uwriter.cxx                      |  195 ++++++++++++++++++++++++++--
 sw/source/core/doc/docruby.cxx              |  120 ++++++++---------
 5 files changed, 401 insertions(+), 80 deletions(-)

New commits:
commit 5a45f7925b8c88baeb23ee253491888bfa6233cc
Author:     Jonathan Clark <jonat...@libreoffice.org>
AuthorDate: Thu Sep 12 06:47:53 2024 -0600
Commit:     Jonathan Clark <jonat...@libreoffice.org>
CommitDate: Thu Sep 12 21:51:39 2024 +0200

    tdf#156543 sw: Added base text mono feature to Asian Phonetic Guide
    
    This change adds a new button, Mono, to the Asian Phonetic Guide.
    Clicking on this button will automatically separate each base text
    character into its own base text run.
    
    Change-Id: I973e2c3259918db59e46dc7b89cb7e8ee4f45469
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/173276
    Tested-by: Jenkins
    Reviewed-by: Jonathan Clark <jonat...@libreoffice.org>

diff --git a/include/svx/rubydialog.hxx b/include/svx/rubydialog.hxx
index 30b284ee0fbd..9b996527a391 100644
--- a/include/svx/rubydialog.hxx
+++ b/include/svx/rubydialog.hxx
@@ -84,6 +84,7 @@ class SvxRubyDialog final : public SfxModelessDialogController
     std::unique_ptr<weld::Button> m_xStylistPB;
 
     std::unique_ptr<weld::Button> m_xSelectionGroupPB;
+    std::unique_ptr<weld::Button> m_xSelectionMonoPB;
 
     std::unique_ptr<weld::Button> m_xApplyPB;
     std::unique_ptr<weld::Button> m_xClosePB;
@@ -95,6 +96,7 @@ class SvxRubyDialog final : public SfxModelessDialogController
     std::unique_ptr<weld::CustomWeld> m_xPreview;
 
     DECL_LINK(SelectionGroup_Impl, weld::Button&, void);
+    DECL_LINK(SelectionMono_Impl, weld::Button&, void);
     DECL_LINK(ApplyHdl_Impl, weld::Button&, void);
     DECL_LINK(CloseHdl_Impl, weld::Button&, void);
     DECL_LINK(StylistHdl_Impl, weld::Button&, void);
diff --git a/svx/source/dialog/rubydialog.cxx b/svx/source/dialog/rubydialog.cxx
index f85a395029b0..8f19b1598a6f 100644
--- a/svx/source/dialog/rubydialog.cxx
+++ b/svx/source/dialog/rubydialog.cxx
@@ -20,6 +20,7 @@
 #include <sal/config.h>
 #include <tools/debug.hxx>
 #include <comphelper/diagnose_ex.hxx>
+#include <comphelper/processfactory.hxx>
 
 #include <svx/rubydialog.hxx>
 #include <sfx2/dispatch.hxx>
@@ -38,6 +39,8 @@
 #include <com/sun/star/text/RubyAdjust.hpp>
 #include <com/sun/star/view/XSelectionChangeListener.hpp>
 #include <com/sun/star/view/XSelectionSupplier.hpp>
+#include <com/sun/star/i18n/BreakIterator.hpp>
+#include <com/sun/star/i18n/CharacterIteratorMode.hpp>
 #include <cppuhelper/implbase.hxx>
 #include <svtools/colorcfg.hxx>
 #include <vcl/event.hxx>
@@ -80,6 +83,7 @@ SfxChildWinInfo SvxRubyChildWindow::GetInfo() const { return 
SfxChildWindow::Get
 
 class SvxRubyData_Impl : public 
cppu::WeakImplHelper<css::view::XSelectionChangeListener>
 {
+    Reference<css::i18n::XBreakIterator> xBreak;
     Reference<XModel> xModel;
     Reference<XRubySelection> xSelection;
     Sequence<PropertyValues> aRubyValues;
@@ -132,14 +136,14 @@ public:
 
         OUString sBaseTmp;
         OUStringBuffer aBaseString;
-        for (const PropertyValues& pVals : aRubyValues)
+        for (const PropertyValues& rVals : aRubyValues)
         {
             sBaseTmp.clear();
-            for (const PropertyValue& pVal : pVals)
+            for (const PropertyValue& rVal : rVals)
             {
-                if (pVal.Name == cRubyBaseText)
+                if (rVal.Name == cRubyBaseText)
                 {
-                    pVal.Value >>= sBaseTmp;
+                    rVal.Value >>= sBaseTmp;
                 }
             }
 
@@ -151,16 +155,16 @@ public:
 
         // Copy some reasonable style values from the previous ruby array
         pNewRubyValues[0] = aRubyValues[0];
-        for (const PropertyValues& pVals : aRubyValues)
+        for (const PropertyValues& rVals : aRubyValues)
         {
-            for (const PropertyValue& pVal : pVals)
+            for (const PropertyValue& rVal : rVals)
             {
-                if (pVal.Name == cRubyText)
+                if (rVal.Name == cRubyText)
                 {
-                    pVal.Value >>= sBaseTmp;
+                    rVal.Value >>= sBaseTmp;
                     if (!sBaseTmp.isEmpty())
                     {
-                        pNewRubyValues[0] = pVals;
+                        pNewRubyValues[0] = rVals;
                         break;
                     }
                 }
@@ -184,12 +188,131 @@ public:
 
         aRubyValues = std::move(aNewRubyValues);
     }
+
+    bool IsSelectionMono()
+    {
+        if (!xBreak.is())
+        {
+            // Cannot continue if BreakIterator is not available
+            // Disable the button
+            return true;
+        }
+
+        // Locale does not matter in this case; default ICU BreakIterator is 
sufficient
+        Locale aLocale;
+
+        OUString sBaseTmp;
+        return std::all_of(
+            aRubyValues.begin(), aRubyValues.end(), [&](const PropertyValues& 
rVals) {
+                return !std::any_of(rVals.begin(), rVals.end(), [&](const 
PropertyValue& rVal) {
+                    if (rVal.Name == cRubyBaseText)
+                    {
+                        rVal.Value >>= sBaseTmp;
+                        sal_Int32 nDone = 0;
+                        auto nPos = xBreak->nextCharacters(
+                            sBaseTmp, 0, aLocale, 
css::i18n::CharacterIteratorMode::SKIPCELL, 1,
+                            nDone);
+                        return nPos < sBaseTmp.getLength();
+                    }
+
+                    return false;
+                });
+            });
+    }
+
+    void MakeSelectionMono()
+    {
+        if (!xBreak.is())
+        {
+            // Cannot continue if BreakIterator is not available
+            return;
+        }
+
+        // Locale does not matter in this case; default ICU BreakIterator is 
sufficient
+        Locale aLocale;
+
+        OUString sBaseTmp;
+
+        // Count the grapheme clusters
+        sal_Int32 nTotalGraphemeClusters = 0;
+        for (const PropertyValues& rVals : aRubyValues)
+        {
+            for (const PropertyValue& rVal : rVals)
+            {
+                if (rVal.Name == cRubyBaseText)
+                {
+                    rVal.Value >>= sBaseTmp;
+
+                    sal_Int32 nPos = 0;
+                    while (nPos < sBaseTmp.getLength())
+                    {
+                        sal_Int32 nDone = 0;
+                        nPos = xBreak->nextCharacters(sBaseTmp, nPos, aLocale,
+                                                      
css::i18n::CharacterIteratorMode::SKIPCELL, 1,
+                                                      nDone);
+                        ++nTotalGraphemeClusters;
+                    }
+                }
+            }
+        }
+
+        // Put each grapheme cluster in its own entry
+        Sequence<PropertyValues> aNewRubyValues{ nTotalGraphemeClusters };
+        PropertyValues* pNewRubyValues = aNewRubyValues.getArray();
+
+        sal_Int32 nCurrGraphemeCluster = 0;
+        for (const PropertyValues& rVals : aRubyValues)
+        {
+            for (const PropertyValue& rVal : rVals)
+            {
+                if (rVal.Name == cRubyBaseText)
+                {
+                    rVal.Value >>= sBaseTmp;
+
+                    sal_Int32 nPos = 0;
+                    while (nPos < sBaseTmp.getLength())
+                    {
+                        sal_Int32 nDone = 0;
+                        auto nNextPos = xBreak->nextCharacters(
+                            sBaseTmp, nPos, aLocale, 
css::i18n::CharacterIteratorMode::SKIPCELL, 1,
+                            nDone);
+
+                        PropertyValues& rNewVals = 
pNewRubyValues[nCurrGraphemeCluster++];
+
+                        // Initialize new property values with values from 
current run
+                        rNewVals = rVals;
+
+                        PropertyValue* aNewVals = rNewVals.getArray();
+                        for (sal_Int32 i = 0; i < rNewVals.getLength(); ++i)
+                        {
+                            PropertyValue& rNewVal = aNewVals[i];
+
+                            if (rNewVal.Name == cRubyText)
+                            {
+                                rNewVal.Value <<= OUString{};
+                            }
+                            else if (rNewVal.Name == cRubyBaseText)
+                            {
+                                rNewVal.Value <<= sBaseTmp.copy(nPos, nNextPos 
- nPos);
+                            }
+                        }
+
+                        nPos = nNextPos;
+                    }
+                }
+            }
+        }
+
+        aRubyValues = std::move(aNewRubyValues);
+    }
 };
 
 SvxRubyData_Impl::SvxRubyData_Impl()
     : bHasSelectionChanged(false)
     , bDisposing(false)
 {
+    Reference<XComponentContext> xContext = 
::comphelper::getProcessComponentContext();
+    xBreak = css::i18n::BreakIterator::create(xContext);
 }
 
 SvxRubyData_Impl::~SvxRubyData_Impl() {}
@@ -273,6 +396,7 @@ SvxRubyDialog::SvxRubyDialog(SfxBindings* pBind, 
SfxChildWindow* pCW, weld::Wind
     , m_xCharStyleLB(m_xBuilder->weld_combo_box(u"stylelb"_ustr))
     , m_xStylistPB(m_xBuilder->weld_button(u"styles"_ustr))
     , m_xSelectionGroupPB(m_xBuilder->weld_button(u"selection-group"_ustr))
+    , m_xSelectionMonoPB(m_xBuilder->weld_button(u"selection-mono"_ustr))
     , m_xApplyPB(m_xBuilder->weld_button(u"ok"_ustr))
     , m_xClosePB(m_xBuilder->weld_button(u"close"_ustr))
     , m_xContentArea(m_xDialog->weld_content_area())
@@ -295,6 +419,7 @@ SvxRubyDialog::SvxRubyDialog(SfxBindings* pBind, 
SfxChildWindow* pCW, weld::Wind
     aEditArr[7] = m_xRight4ED.get();
 
     m_xSelectionGroupPB->connect_clicked(LINK(this, SvxRubyDialog, 
SelectionGroup_Impl));
+    m_xSelectionMonoPB->connect_clicked(LINK(this, SvxRubyDialog, 
SelectionMono_Impl));
     m_xApplyPB->connect_clicked(LINK(this, SvxRubyDialog, ApplyHdl_Impl));
     m_xClosePB->connect_clicked(LINK(this, SvxRubyDialog, CloseHdl_Impl));
     m_xStylistPB->connect_clicked(LINK(this, SvxRubyDialog, StylistHdl_Impl));
@@ -474,6 +599,7 @@ void SvxRubyDialog::Update()
 {
     // Only enable selection grouping options when they can be applied
     m_xSelectionGroupPB->set_sensitive(!m_pImpl->IsSelectionGrouped());
+    m_xSelectionMonoPB->set_sensitive(!m_pImpl->IsSelectionMono());
 
     const Sequence<PropertyValues>& aRubyValues = m_pImpl->GetRubyValues();
     sal_Int32 nLen = aRubyValues.getLength();
@@ -581,6 +707,12 @@ IMPL_LINK_NOARG(SvxRubyDialog, SelectionGroup_Impl, 
weld::Button&, void)
     Update();
 }
 
+IMPL_LINK_NOARG(SvxRubyDialog, SelectionMono_Impl, weld::Button&, void)
+{
+    m_pImpl->MakeSelectionMono();
+    Update();
+}
+
 IMPL_LINK_NOARG(SvxRubyDialog, ApplyHdl_Impl, weld::Button&, void)
 {
     const Sequence<PropertyValues>& aRubyValues = m_pImpl->GetRubyValues();
diff --git a/svx/uiconfig/ui/asianphoneticguidedialog.ui 
b/svx/uiconfig/ui/asianphoneticguidedialog.ui
index 66a045d26ba7..a0fd8453f69e 100644
--- a/svx/uiconfig/ui/asianphoneticguidedialog.ui
+++ b/svx/uiconfig/ui/asianphoneticguidedialog.ui
@@ -520,6 +520,20 @@
                     <property name="position">0</property>
                   </packing>
                 </child>
+                <child>
+                  <object class="GtkButton" id="selection-mono">
+                    <property name="label" translatable="yes" 
context="asianphoneticguidedialog|selectionmono">_Mono</property>
+                    <property name="visible">True</property>
+                    <property name="can-focus">True</property>
+                    <property name="receives-default">True</property>
+                    <property name="use-underline">True</property>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
               </object>
               <packing>
                 <property name="left-attach">1</property>
diff --git a/sw/qa/core/uwriter.cxx b/sw/qa/core/uwriter.cxx
index 2e10ae43aa1c..abfce2412516 100644
--- a/sw/qa/core/uwriter.cxx
+++ b/sw/qa/core/uwriter.cxx
@@ -2023,10 +2023,10 @@ void SwDocTest::testFillRubyList()
         rList->push_back(std::move(pEnt));
     };
 
-    auto fnGetCombinedString = [&]
+    auto fnGetCombinedString = [](SwPaM& rPaM)
     {
         SwRubyList aRubies;
-        SwDoc::FillRubyList(aPaM, aRubies);
+        SwDoc::FillRubyList(rPaM, aRubies);
 
         OUStringBuffer aTemp;
 
@@ -2041,13 +2041,13 @@ void SwDocTest::testFillRubyList()
     // Single word without existing rubies
     {
         fnAppendJapanese(u"学校"_ustr);
-        CPPUNIT_ASSERT_EQUAL(u"学校[]"_ustr, fnGetCombinedString());
+        CPPUNIT_ASSERT_EQUAL(u"学校[]"_ustr, fnGetCombinedString(aPaM));
     }
 
     // Compound word without existing rubies
     {
         fnAppendJapanese(u"自動販売機"_ustr);
-        CPPUNIT_ASSERT_EQUAL(u"自動[]販売[]機[]"_ustr, fnGetCombinedString());
+        CPPUNIT_ASSERT_EQUAL(u"自動[]販売[]機[]"_ustr, fnGetCombinedString(aPaM));
     }
 
     // Single word with existing rubies
@@ -2059,7 +2059,7 @@ void SwDocTest::testFillRubyList()
 
         m_pDoc->SetRubyList(aPaM, rList);
 
-        CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]"_ustr, fnGetCombinedString());
+        CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]"_ustr, fnGetCombinedString(aPaM));
     }
 
     // Compound word with existing rubies
@@ -2073,7 +2073,7 @@ void SwDocTest::testFillRubyList()
 
         m_pDoc->SetRubyList(aPaM, rList);
 
-        CPPUNIT_ASSERT_EQUAL(u"自動[じどう]販売[はんばい]機[き]"_ustr, 
fnGetCombinedString());
+        CPPUNIT_ASSERT_EQUAL(u"自動[じどう]販売[はんばい]機[き]"_ustr, 
fnGetCombinedString(aPaM));
     }
 
     // Compound word with existing rubies treated as a single word
@@ -2087,7 +2087,7 @@ void SwDocTest::testFillRubyList()
 
         m_pDoc->SetRubyList(aPaM, rList);
 
-        CPPUNIT_ASSERT_EQUAL(u"自動販売機[じどうはんばいき]"_ustr, fnGetCombinedString());
+        CPPUNIT_ASSERT_EQUAL(u"自動販売機[じどうはんばいき]"_ustr, 
fnGetCombinedString(aPaM));
     }
 
     // tdf#141466: Characteristic test from bug
@@ -2103,7 +2103,8 @@ void SwDocTest::testFillRubyList()
 
         m_pDoc->SetRubyList(aPaM, rList);
 
-        CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]に[]行[い]き[]ます[]。[]"_ustr, 
fnGetCombinedString());
+        CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]に[]行[い]き[]ます[]。[]"_ustr,
+                             fnGetCombinedString(aPaM));
     }
 
     // tdf#107184: Characteristic test for ruby group mode editing
@@ -2115,7 +2116,26 @@ void SwDocTest::testFillRubyList()
 
         m_pDoc->SetRubyList(aPaM, rList);
 
-        CPPUNIT_ASSERT_EQUAL(u"学校に行きます[がっこうにいきます]"_ustr, 
fnGetCombinedString());
+        CPPUNIT_ASSERT_EQUAL(u"学校に行きます[がっこうにいきます]"_ustr, 
fnGetCombinedString(aPaM));
+    }
+
+    // tdf#156543: Characteristic test for ruby mono mode editing
+    {
+        fnAppendJapanese(u"学校に行きます"_ustr);
+
+        SwRubyList rList;
+        fnAppendRuby(&rList, u"学"_ustr, u"がっ"_ustr);
+        fnAppendRuby(&rList, u"校"_ustr, u"こう"_ustr);
+        fnAppendRuby(&rList, u"に"_ustr, u""_ustr);
+        fnAppendRuby(&rList, u"行"_ustr, u"い"_ustr);
+        fnAppendRuby(&rList, u"き"_ustr, u""_ustr);
+        fnAppendRuby(&rList, u"ま"_ustr, u""_ustr);
+        fnAppendRuby(&rList, u"す"_ustr, u""_ustr);
+
+        m_pDoc->SetRubyList(aPaM, rList);
+
+        CPPUNIT_ASSERT_EQUAL(u"学[がっ]校[こう]に[]行[い]き[]ます[]"_ustr,
+                             fnGetCombinedString(aPaM));
     }
 
     // tdf#156543: Characteristic test for ruby mono mode editing
@@ -2133,7 +2153,19 @@ void SwDocTest::testFillRubyList()
 
         m_pDoc->SetRubyList(aPaM, rList);
 
-        CPPUNIT_ASSERT_EQUAL(u"学[がっ]校[こう]に[]行[い]き[]ます[]"_ustr, 
fnGetCombinedString());
+        CPPUNIT_ASSERT_EQUAL(u"学[がっ]校[こう]に[]行[い]き[]ます[]"_ustr,
+                             fnGetCombinedString(aPaM));
+    }
+
+    // Partial PaM
+    {
+        fnAppendJapanese(u"学校に行こう。"_ustr);
+
+        SwPaM aAdjPaM{ *aPaM.GetPoint(), *aPaM.GetMark() };
+        aAdjPaM.Normalize();
+        aAdjPaM.GetMark()->AdjustContent(-1);
+
+        CPPUNIT_ASSERT_EQUAL(u"学校[]に[]行[]こう[]"_ustr, 
fnGetCombinedString(aAdjPaM));
     }
 
     // Empty PaM
@@ -2142,7 +2174,7 @@ void SwDocTest::testFillRubyList()
 
         aPaM.DeleteMark();
 
-        CPPUNIT_ASSERT_EQUAL(u"学校[]"_ustr, fnGetCombinedString());
+        CPPUNIT_ASSERT_EQUAL(u"学校[]"_ustr, fnGetCombinedString(aPaM));
     }
 }
 
@@ -2225,6 +2257,15 @@ void SwDocTest::testSetRubyList()
         CPPUNIT_ASSERT_EQUAL(sal_Int32(2), aPaM.GetMark()->GetContentIndex());
 
         CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]"_ustr, fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(2), aPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(2), aPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]"_ustr, fnGetCombinedString());
     }
 
     // tdf#141466: Characteristic test from bug
@@ -2245,6 +2286,48 @@ void SwDocTest::testSetRubyList()
         CPPUNIT_ASSERT_EQUAL(sal_Int32(8), aPaM.GetMark()->GetContentIndex());
 
         CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]に行[い]きます。"_ustr, fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(8), aPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(8), aPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]に行[い]きます。"_ustr, fnGetCombinedString());
+    }
+
+    // Base text merging/deletion at end of selection
+    {
+        fnAppendJapanese(u"学校に行こう。"_ustr);
+
+        SwPaM aAdjPaM{ *aPaM.GetPoint(), *aPaM.GetMark() };
+        aAdjPaM.GetPoint()->AdjustContent(-1);
+
+        SwRubyList rList;
+        fnAppendRuby(&rList, u"学校"_ustr, u"がっこう"_ustr);
+        fnAppendRuby(&rList, u"に"_ustr, u""_ustr);
+        fnAppendRuby(&rList, u"行こう"_ustr, u"いこう"_ustr);
+        fnAppendRuby(&rList, u""_ustr, u""_ustr);
+        fnAppendRuby(&rList, u""_ustr, u"this ruby should not appear"_ustr);
+        fnAppendRuby(&rList, u""_ustr, u""_ustr);
+
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), 
aAdjPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), 
aAdjPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aAdjPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), 
aAdjPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), 
aAdjPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]に行こう[いこう]。"_ustr, 
fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), 
aAdjPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), 
aAdjPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aAdjPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), 
aAdjPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), 
aAdjPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]に行こう[いこう]。"_ustr, 
fnGetCombinedString());
     }
 
     // Base text deletion
@@ -2281,6 +2364,15 @@ void SwDocTest::testSetRubyList()
         CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
 
         CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]に来[き]ます。"_ustr, fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aPaM, rList2);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学校[がっこう]に来[き]ます。"_ustr, fnGetCombinedString());
     }
 
     // tdf#107184: Characteristic test for ruby group mode editing
@@ -2297,6 +2389,15 @@ void SwDocTest::testSetRubyList()
         CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
 
         CPPUNIT_ASSERT_EQUAL(u"学校に行きます[がっこうにいきます]"_ustr, 
fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学校に行きます[がっこうにいきます]"_ustr, 
fnGetCombinedString());
     }
 
     // tdf#107184: Delete ruby in group mode after populating
@@ -2324,6 +2425,15 @@ void SwDocTest::testSetRubyList()
         CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
 
         CPPUNIT_ASSERT_EQUAL(u"学校に行きます"_ustr, fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aPaM, rList2);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学校に行きます"_ustr, fnGetCombinedString());
     }
 
     // tdf#156543: Characteristic test for ruby mono mode editing
@@ -2346,6 +2456,15 @@ void SwDocTest::testSetRubyList()
         CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
 
         CPPUNIT_ASSERT_EQUAL(u"学[がっ]校[こう]に行[い]きます[す]"_ustr, 
fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), aPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), aPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学[がっ]校[こう]に行[い]きます[す]"_ustr, 
fnGetCombinedString());
     }
 
     // Offset PaM - Combination of insert and replace
@@ -2371,10 +2490,54 @@ void SwDocTest::testSetRubyList()
         CPPUNIT_ASSERT_EQUAL(sal_Int32(1), 
aEmptyPaM.GetPoint()->GetContentIndex());
         CPPUNIT_ASSERT_EQUAL(sal_Int32(7), 
aEmptyPaM.GetMark()->GetContentIndex());
 
+        CPPUNIT_ASSERT_EQUAL(u"学森林[しんりん]海上[かいじょう]地面[じめん]員"_ustr,
+                             fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(1), 
aEmptyPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), 
aEmptyPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aEmptyPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(1), 
aEmptyPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), 
aEmptyPaM.GetMark()->GetContentIndex());
+
         CPPUNIT_ASSERT_EQUAL(u"学森林[しんりん]海上[かいじょう]地面[じめん]員"_ustr,
                              fnGetCombinedString());
     }
 
+    // Partial PaM with mono replacement
+    {
+        fnAppendJapanese(u"学校に行こう。"_ustr);
+
+        SwPaM aAdjPaM{ *aPaM.GetPoint(), *aPaM.GetMark() };
+        aAdjPaM.Normalize();
+        aAdjPaM.GetMark()->AdjustContent(-1);
+
+        SwRubyList rList;
+        fnAppendRuby(&rList, u"学"_ustr, u"がっ"_ustr);
+        fnAppendRuby(&rList, u"校"_ustr, u"こう"_ustr);
+        fnAppendRuby(&rList, u"に"_ustr, u""_ustr);
+        fnAppendRuby(&rList, u"行"_ustr, u"い"_ustr);
+        fnAppendRuby(&rList, u"こ"_ustr, u""_ustr);
+        fnAppendRuby(&rList, u"う"_ustr, u""_ustr);
+
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), 
aAdjPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), 
aAdjPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aAdjPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), 
aAdjPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), 
aAdjPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学[がっ]校[こう]に行[い]こう。"_ustr, 
fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), 
aAdjPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), 
aAdjPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aAdjPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(0), 
aAdjPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), 
aAdjPaM.GetMark()->GetContentIndex());
+
+        CPPUNIT_ASSERT_EQUAL(u"学[がっ]校[こう]に行[い]こう。"_ustr, 
fnGetCombinedString());
+    }
+
     // Empty PaM - Should insert
     {
         fnAppendJapanese(u"学校"_ustr);
@@ -2397,6 +2560,16 @@ void SwDocTest::testSetRubyList()
         CPPUNIT_ASSERT_EQUAL(sal_Int32(1), 
aEmptyPaM.GetPoint()->GetContentIndex());
         CPPUNIT_ASSERT_EQUAL(sal_Int32(7), 
aEmptyPaM.GetMark()->GetContentIndex());
 
+        CPPUNIT_ASSERT_EQUAL(u"学森林[しんりん]海上[かいじょう]地面[じめん]校"_ustr,
+                             fnGetCombinedString());
+
+        // Operation should be idempotent
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(1), 
aEmptyPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), 
aEmptyPaM.GetMark()->GetContentIndex());
+        m_pDoc->SetRubyList(aEmptyPaM, rList);
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(1), 
aEmptyPaM.GetPoint()->GetContentIndex());
+        CPPUNIT_ASSERT_EQUAL(sal_Int32(7), 
aEmptyPaM.GetMark()->GetContentIndex());
+
         CPPUNIT_ASSERT_EQUAL(u"学森林[しんりん]海上[かいじょう]地面[じめん]校"_ustr,
                              fnGetCombinedString());
     }
diff --git a/sw/source/core/doc/docruby.cxx b/sw/source/core/doc/docruby.cxx
index 15629c70325e..954870d56776 100644
--- a/sw/source/core/doc/docruby.cxx
+++ b/sw/source/core/doc/docruby.cxx
@@ -117,53 +117,57 @@ void SwDoc::SetRubyList(SwPaM& rPam, const SwRubyList& 
rList)
         }
 
         SwRubyListEntry aCheckEntry;
-        if (!SelectNextRubyChars(aPam, aCheckEntry))
+        auto bSelected = SelectNextRubyChars(aPam, aCheckEntry);
+
+        if (bSelected)
         {
-            // goto next paragraph
-            aPam.DeleteMark();
-            aPam.Move(fnMoveForward, GoInNode);
+            ++nCurrBaseTexts;
 
-            if (*aPam.GetPoint() >= *pEnd)
+            // Existing ruby text was located. Apply the new attributes.
+            const SwRubyListEntry* pEntry = rList[nListEntry++].get();
+            if (aCheckEntry.GetRubyAttr() != pEntry->GetRubyAttr())
             {
-                break;
+                // set/reset the attribute
+                if (!pEntry->GetRubyAttr().GetText().isEmpty())
+                {
+                    getIDocumentContentOperations().InsertPoolItem(aPam, 
pEntry->GetRubyAttr());
+                }
+                else
+                {
+                    ResetAttrs(aPam, true, aDelArr);
+                }
             }
 
-            continue;
-        }
+            if (aCheckEntry.GetText() != pEntry->GetText())
+            {
+                if (pEntry->GetText().isEmpty())
+                {
+                    ResetAttrs(aPam, true, aDelArr);
+                }
 
-        ++nCurrBaseTexts;
+                // text is changed, so replace the original
+                getIDocumentContentOperations().ReplaceRange(aPam, 
pEntry->GetText(), false);
+                aPam.Exchange();
+            }
 
-        const SwRubyListEntry* pEntry = rList[nListEntry++].get();
-        if (aCheckEntry.GetRubyAttr() != pEntry->GetRubyAttr())
+            aPam.DeleteMark();
+        }
+        else
         {
-            // set/reset the attribute
-            if (!pEntry->GetRubyAttr().GetText().isEmpty())
-            {
-                getIDocumentContentOperations().InsertPoolItem(aPam, 
pEntry->GetRubyAttr());
-            }
-            else
-            {
-                ResetAttrs(aPam, true, aDelArr);
-            }
+            // No existing ruby text located. Advance to next paragraph.
+            aPam.DeleteMark();
+            aPam.Move(fnMoveForward, GoInNode);
         }
 
-        if (aCheckEntry.GetText() != pEntry->GetText())
+        // Stop substituting when the cursor advances to the end of the 
selection.
+        if (*aPam.GetPoint() >= *pEnd)
         {
-            if (pEntry->GetText().isEmpty())
-            {
-                ResetAttrs(aPam, true, aDelArr);
-            }
-
-            // text is changed, so replace the original
-            getIDocumentContentOperations().ReplaceRange(aPam, 
pEntry->GetText(), false);
-            aPam.Exchange();
+            break;
         }
-
-        aPam.DeleteMark();
     }
 
     // Delete any spans past the end of the ruby list
-    while (nListEntry == rList.size() && nCurrBaseTexts < nMaxBaseTexts)
+    while (nListEntry == rList.size() && nCurrBaseTexts < nMaxBaseTexts && 
*aPam.GetPoint() < *pEnd)
     {
         if (pEnd != pStt)
         {
@@ -172,27 +176,24 @@ void SwDoc::SetRubyList(SwPaM& rPam, const SwRubyList& 
rList)
         }
 
         SwRubyListEntry aCheckEntry;
-        if (!SelectNextRubyChars(aPam, aCheckEntry))
+        auto bSelected = SelectNextRubyChars(aPam, aCheckEntry);
+
+        if (bSelected)
         {
-            // goto next paragraph
-            aPam.DeleteMark();
-            aPam.Move(fnMoveForward, GoInNode);
+            ++nCurrBaseTexts;
 
-            if (*aPam.GetPoint() >= *pEnd)
-            {
-                break;
-            }
+            ResetAttrs(aPam, true, aDelArr);
+            getIDocumentContentOperations().DeleteRange(aPam);
+            aPam.Exchange();
 
-            continue;
+            aPam.DeleteMark();
+        }
+        else
+        {
+            // No existing ruby text located. Advance to next paragraph.
+            aPam.DeleteMark();
+            aPam.Move(fnMoveForward, GoInNode);
         }
-
-        ++nCurrBaseTexts;
-
-        ResetAttrs(aPam, true, aDelArr);
-        getIDocumentContentOperations().DeleteRange(aPam);
-        aPam.Exchange();
-
-        aPam.DeleteMark();
     }
 
     // Insert any spans past the end of the base text list
@@ -201,22 +202,21 @@ void SwDoc::SetRubyList(SwPaM& rPam, const SwRubyList& 
rList)
     {
         const SwRubyListEntry* pEntry = rList[nListEntry++].get();
 
-        if (pEnd != pStt)
+        if (!pEntry->GetText().isEmpty())
         {
             aPam.SetMark();
-            *aPam.GetMark() = *pEnd;
-        }
+            getIDocumentContentOperations().InsertString(aPam, 
pEntry->GetText());
+            aPam.GetMark()->AdjustContent(-pEntry->GetText().getLength());
+
+            if (!pEntry->GetRubyAttr().GetText().isEmpty())
+            {
+                getIDocumentContentOperations().InsertPoolItem(aPam, 
pEntry->GetRubyAttr());
+            }
 
-        aPam.SetMark();
-        getIDocumentContentOperations().InsertString(aPam, pEntry->GetText());
-        nTotalContentGrowth += pEntry->GetText().getLength();
+            aPam.DeleteMark();
 
-        if (!pEntry->GetRubyAttr().GetText().isEmpty())
-        {
-            getIDocumentContentOperations().InsertPoolItem(aPam, 
pEntry->GetRubyAttr());
+            nTotalContentGrowth += pEntry->GetText().getLength();
         }
-
-        aPam.DeleteMark();
     }
 
     // Expand selection to account for insertion

Reply via email to