sw/source/uibase/inc/wrtsh.hxx                 |    2 
 sw/source/uibase/uiview/view2.cxx              |  111 ---------------------
 sw/source/uibase/utlui/content.cxx             |   52 ++++++---
 sw/source/uibase/wrtsh/wrtsh3.cxx              |  132 +++++++++++++++++++++++++
 sw/uiconfig/swriter/ui/navigatorcontextmenu.ui |   10 +
 5 files changed, 179 insertions(+), 128 deletions(-)

New commits:
commit 9be7dacd773ee32a3d50dba77115fbee92506cea
Author:     Jim Raykowski <[email protected]>
AuthorDate: Tue Jan 13 20:28:45 2026 -0900
Commit:     Jim Raykowski <[email protected]>
CommitDate: Wed Jan 14 23:03:54 2026 +0100

    tdf#169674 Sort headings and retain their contents while moving them
    
    Enhancement patch to do this from the Writer Navigator Headings
    (Outline) context menu. Currently experimental mode must be on to see
    the 'Sort Alphabetically in Document' menu item which is only shown for
    headings with sub headings.
    
    Change-Id: Iec0006d1309b3c96bc7019d009b9dc4f9146df62
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197230
    Tested-by: Jenkins
    Reviewed-by: Jim Raykowski <[email protected]>

diff --git a/sw/source/uibase/inc/wrtsh.hxx b/sw/source/uibase/inc/wrtsh.hxx
index 077d03aebfde..821a321f7a65 100644
--- a/sw/source/uibase/inc/wrtsh.hxx
+++ b/sw/source/uibase/inc/wrtsh.hxx
@@ -539,6 +539,8 @@ typedef bool (SwWrtShell::*FNSimpleMove)();
 
     std::optional<OString> getLOKPayload(int nType, int nViewId) const;
 
+    void SortChapters(const SwOutlineNodes::size_type nOutlineNodePos = 
SwOutlineNodes::npos);
+
 private:
 
     void  OpenMark();
diff --git a/sw/source/uibase/uiview/view2.cxx 
b/sw/source/uibase/uiview/view2.cxx
index 31a288b82136..08dae34969c7 100644
--- a/sw/source/uibase/uiview/view2.cxx
+++ b/sw/source/uibase/uiview/view2.cxx
@@ -165,8 +165,6 @@
 #include <svx/dialog/gotodlg.hxx>
 #include <unotools/tempfile.hxx>
 
-#include <set>
-
 const char sStatusDelim[] = " : ";
 
 using namespace sfx2;
@@ -1712,114 +1710,7 @@ void SwView::Execute(SfxRequest &rReq)
         }
         case FN_SORT_CHAPTERS:
         {
-            auto sort_chapters = [this](const SwNode* pParentNode, int 
nOutlineLevel)
-            {
-                const SwNode* pEndNode;
-
-                std::vector<const SwTextNode*> vLevelOutlineNodes;
-                auto GetLevelOutlineNodesAndEndNode = [&]()
-                {
-                    vLevelOutlineNodes.clear();
-                    bool bParentFound = false;
-                    pEndNode = &m_pWrtShell->GetNodes().GetEndOfContent();
-                    for (const SwNode* pNode : 
m_pWrtShell->GetNodes().GetOutLineNds())
-                    {
-                        if (!pNode->IsTextNode())
-                            continue;
-                        if (pParentNode && !bParentFound)
-                        {
-                            bParentFound = pNode == pParentNode;
-                            continue;
-                        }
-                        if (pNode->GetTextNode()->GetAttrOutlineLevel() < 
nOutlineLevel)
-                        {
-                            pEndNode = pNode;
-                            break;
-                        }
-                        if (pNode->GetTextNode()->GetAttrOutlineLevel() == 
nOutlineLevel)
-                            
vLevelOutlineNodes.emplace_back(pNode->GetTextNode());
-                    }
-                };
-
-                GetLevelOutlineNodesAndEndNode();
-
-                std::vector<const SwTextNode*> vSortedLevelOutlineNodes = 
vLevelOutlineNodes;
-                std::stable_sort(vSortedLevelOutlineNodes.begin(), 
vSortedLevelOutlineNodes.end(),
-                                 [](const SwTextNode* a, const SwTextNode* b)
-                                 {
-                                     const OUString& raText = a->GetText();
-                                     const OUString& rbText = b->GetText();
-                                     return raText < rbText;
-                                 });
-
-                for (size_t i = 0, nSize = vLevelOutlineNodes.size(); i < 
nSize; i++)
-                {
-                    // Find the position that the sorted node is at in the 
unsorted vector.
-                    // This is the position of the node in the unsorted vector 
that is used for the start of
-                    // the range of nodes to be moved in this iteration.
-                    size_t j = 0;
-                    for (; j < nSize; j++)
-                    {
-                        if (vSortedLevelOutlineNodes[i] == 
vLevelOutlineNodes[j])
-                            break;
-                    }
-
-                    // The end node in the range is the next entry in the 
unsorted vector or the pEndNode set
-                    // by GetLevelOutlineNodesAndEndNode or the end of the 
document.
-                    const SwNode* pEndRangeNode;
-                    if (j + 1 < nSize)
-                        pEndRangeNode = vLevelOutlineNodes[j + 1];
-                    else
-                        pEndRangeNode = pEndNode;
-
-                    SwNodeRange aNodeRange(*vLevelOutlineNodes[j], 
SwNodeOffset(0), *pEndRangeNode,
-                                           SwNodeOffset(0));
-
-                    // Move the range of nodes to before the node in the 
unsorted outline vector at the
-                    // current iteration index to match the position of the 
outline node in the sorted vector.
-                    m_pWrtShell->getIDocumentContentOperations().MoveNodeRange(
-                        aNodeRange, 
*const_cast<SwTextNode*>(vLevelOutlineNodes[i]),
-                        SwMoveFlags::DEFAULT | SwMoveFlags::CREATEUNDOOBJ);
-
-                    GetLevelOutlineNodesAndEndNode();
-                }
-            };
-
-            const SwOutlineNodes& rOutlineNodes = 
m_pWrtShell->GetNodes().GetOutLineNds();
-
-            if (rOutlineNodes.empty())
-                return;
-
-            m_pWrtShell->StartAction();
-            m_pWrtShell->StartUndo(SwUndoId::SORT_CHAPTERS);
-
-            // Create an ordered set of outline levels in the outline nodes 
for use to determine
-            // the lowest level to use for first sort and to only iterate over 
higher levels used.
-            std::set<int> aOutlineLevelSet;
-            for (const SwNode* pNode : rOutlineNodes)
-            {
-                int nOutlineLevel = 
pNode->GetTextNode()->GetAttrOutlineLevel();
-                aOutlineLevelSet.emplace(nOutlineLevel);
-            }
-
-            // No parent node for the lowest outline level nodes sort.
-            sort_chapters(nullptr /*pParentNode*/,
-                          
aOutlineLevelSet.extract(aOutlineLevelSet.begin()).value());
-
-            for (int nOutlineLevel : aOutlineLevelSet)
-                for (size_t i = 0, nSize = rOutlineNodes.size(); i < nSize; 
i++)
-                {
-                    const SwNode* pParentNode = rOutlineNodes[i];
-                    if (i + 1 < nSize
-                        && rOutlineNodes[i + 
1]->GetTextNode()->GetAttrOutlineLevel()
-                               == nOutlineLevel)
-                    {
-                        sort_chapters(pParentNode, nOutlineLevel);
-                    }
-                }
-
-            m_pWrtShell->EndUndo(SwUndoId::SORT_CHAPTERS);
-            m_pWrtShell->EndAction();
+            m_pWrtShell->SortChapters();
         }
         break;
         default:
diff --git a/sw/source/uibase/utlui/content.cxx 
b/sw/source/uibase/utlui/content.cxx
index 5ae78dc29249..0d729fc38625 100644
--- a/sw/source/uibase/utlui/content.cxx
+++ b/sw/source/uibase/utlui/content.cxx
@@ -135,6 +135,8 @@
 #include <sfx2/passwd.hxx>
 #include <svl/PasswordHelper.hxx>
 
+#include <officecfg/Office/Common.hxx>
+
 #define CTYPE_CNT   0
 #define CTYPE_CTT   1
 
@@ -1945,28 +1947,40 @@ IMPL_LINK(SwContentTree, CommandHdl, const 
CommandEvent&, rCEvt, bool)
     bool bRemoveFootnoteTracking = true;
     bool bRemoveEndnoteTracking = true;
 
+    // display the content type content in alphabetical order (does not change 
the document)
     bool bRemoveSortEntry = true;
+    // sort outline content in alphabetical order (changes the document)
+    bool bRemoveSortOutlineAlphabeticallyInDocument = true;
 
     bool bRemoveProtectSection = true;
     bool bRemoveHideSection = true;
 
     if (xEntry)
     {
+        bool bIsContentType;
         const SwContentType* pType;
-        if (lcl_IsContentType(*xEntry, *m_xTreeView))
+        if ((bIsContentType = lcl_IsContentType(*xEntry, *m_xTreeView)))
             pType = weld::fromId<SwContentType*>(m_xTreeView->get_id(*xEntry));
         else
             pType = weld::fromId<SwContent*>(
                         m_xTreeView->get_id(*xEntry))->GetParent();
         const ContentTypeId nContentType = pType->GetType();
 
-        if (nContentType != ContentTypeId::FOOTNOTE && nContentType != 
ContentTypeId::ENDNOTE
-            && nContentType != ContentTypeId::POSTIT && nContentType != 
ContentTypeId::UNKNOWN)
+        if (bIsContentType && nContentType != ContentTypeId::FOOTNOTE
+            && nContentType != ContentTypeId::ENDNOTE && nContentType != 
ContentTypeId::POSTIT
+            && nContentType != ContentTypeId::UNKNOWN)
         {
             bRemoveSortEntry = false;
             xPop->set_active(u"sort"_ustr, pType->IsAlphabeticSort());
         }
 
+        if (officecfg::Office::Common::Misc::ExperimentalMode::get()
+            && nContentType == ContentTypeId::OUTLINE)
+        {
+            if (bIsContentType || m_xTreeView->iter_has_child(*xEntry))
+                bRemoveSortOutlineAlphabeticallyInDocument = false;
+        }
+
         OUString aIdent;
         switch (nContentType)
         {
@@ -2406,19 +2420,11 @@ IMPL_LINK(SwContentTree, CommandHdl, const 
CommandEvent&, rCEvt, bool)
     if (bRemoveCopyEntry)
         xPop->remove(u"copy"_ustr);
 
-    if (bRemoveGotoEntry &&
-            bRemoveCopyEntry &&
-            bRemoveSelectEntry &&
-            bRemoveDeleteEntry &&
-            bRemoveMakeFootnotesEndnotesViceVersaEntry &&
-            bRemoveChapterEntries &&
-            bRemovePostItEntries &&
-            bRemoveRenameEntry &&
-            bRemoveIndexEntry &&
-            bRemoveUpdateIndexEntry &&
-            bRemoveReadonlyIndexEntry &&
-            bRemoveUnprotectEntry &&
-            bRemoveEditEntry)
+    if (bRemoveGotoEntry && bRemoveCopyEntry && bRemoveSelectEntry && 
bRemoveDeleteEntry
+        && bRemoveMakeFootnotesEndnotesViceVersaEntry && bRemoveChapterEntries
+        && bRemovePostItEntries && bRemoveRenameEntry && bRemoveIndexEntry
+        && bRemoveUpdateIndexEntry && bRemoveReadonlyIndexEntry && 
bRemoveUnprotectEntry
+        && bRemoveEditEntry && bRemoveSortOutlineAlphabeticallyInDocument)
         xPop->remove(u"separator2"_ustr);
 
     if (!bOutline)
@@ -2470,6 +2476,8 @@ IMPL_LINK(SwContentTree, CommandHdl, const CommandEvent&, 
rCEvt, bool)
         xPop->remove(u"endnotetracking"_ustr);
     if (bRemoveSortEntry)
         xPop->remove(u"sort"_ustr);
+    if (bRemoveSortOutlineAlphabeticallyInDocument)
+        xPop->remove(u"sortalphabeticallyindocument"_ustr);
     if (bRemoveProtectSection)
         xPop->remove(u"protectsection"_ustr);
     if (bRemoveHideSection)
@@ -6104,7 +6112,17 @@ void SwContentTree::ExecuteContextMenuAction(const 
OUString& rSelectedPopupEntry
         EditEntry(*xFirst, EditEntryMode::TEXT_ALTERNATIVE);
         return;
     }
-
+    else if (rSelectedPopupEntry == "sortalphabeticallyindocument")
+    {
+        SwOutlineNodes::size_type nOutlineNodePos = SwOutlineNodes::npos;
+        if (lcl_IsContent(*xFirst, *m_xTreeView))
+        {
+            nOutlineNodePos
+                = 
weld::fromId<SwOutlineContent*>(m_xTreeView->get_id(*xFirst))->GetOutlinePos();
+        }
+        m_pActiveShell->SortChapters(nOutlineNodePos);
+        return;
+    }
 
     auto nSelectedPopupEntry = rSelectedPopupEntry.toUInt32();
     switch (nSelectedPopupEntry)
diff --git a/sw/source/uibase/wrtsh/wrtsh3.cxx 
b/sw/source/uibase/wrtsh/wrtsh3.cxx
index 67ba8e22b6e7..324b3e7a3507 100644
--- a/sw/source/uibase/wrtsh/wrtsh3.cxx
+++ b/sw/source/uibase/wrtsh/wrtsh3.cxx
@@ -44,6 +44,11 @@
 #include <strings.hrc>
 #include <textcontentcontrol.hxx>
 
+#include <vector>
+#include <set>
+#include <IDocumentContentOperations.hxx>
+#include <ndtxt.hxx>
+
 using namespace ::com::sun::star;
 
 bool SwWrtShell::MoveBookMark( BookMarkMove eFuncId, const 
::sw::mark::MarkBase* const pMark)
@@ -395,4 +400,131 @@ SwPostItMgr* SwWrtShell::GetPostItMgr()
     return m_rView.GetPostItMgr();
 }
 
+void SwWrtShell::SortChapters(const SwOutlineNodes::size_type nOutlineNodePos)
+{
+    auto sort_chapters = [this](const SwNode* pParentNode, int nOutlineLevel)
+    {
+        const SwNode* pEndNode;
+
+        std::vector<const SwTextNode*> vLevelOutlineNodes;
+        auto GetLevelOutlineNodesAndEndNode = [&]()
+        {
+            vLevelOutlineNodes.clear();
+            bool bParentFound = false;
+            pEndNode = &GetNodes().GetEndOfContent();
+            for (const SwNode* pNode : GetNodes().GetOutLineNds())
+            {
+                if (!pNode->IsTextNode())
+                    continue;
+                if (pParentNode && !bParentFound)
+                {
+                    bParentFound = pNode == pParentNode;
+                    continue;
+                }
+                if (pNode->GetTextNode()->GetAttrOutlineLevel() < 
nOutlineLevel)
+                {
+                    pEndNode = pNode;
+                    break;
+                }
+                if (pNode->GetTextNode()->GetAttrOutlineLevel() == 
nOutlineLevel)
+                    vLevelOutlineNodes.emplace_back(pNode->GetTextNode());
+            }
+        };
+
+        GetLevelOutlineNodesAndEndNode();
+
+        std::vector<const SwTextNode*> vSortedLevelOutlineNodes = 
vLevelOutlineNodes;
+        std::stable_sort(vSortedLevelOutlineNodes.begin(), 
vSortedLevelOutlineNodes.end(),
+                         [](const SwTextNode* a, const SwTextNode* b)
+                         {
+                             const OUString& raText = a->GetText();
+                             const OUString& rbText = b->GetText();
+                             return raText < rbText;
+                         });
+
+        for (size_t i = 0, nSize = vLevelOutlineNodes.size(); i < nSize; i++)
+        {
+            // Find the position that the sorted node is at in the unsorted 
vector.
+            // This is the position of the node in the unsorted vector that is 
used for the start of
+            // the range of nodes to be moved in this iteration.
+            size_t j = 0;
+            for (; j < nSize; j++)
+            {
+                if (vSortedLevelOutlineNodes[i] == vLevelOutlineNodes[j])
+                    break;
+            }
+
+            // The end node in the range is the next entry in the unsorted 
vector or the pEndNode set
+            // by GetLevelOutlineNodesAndEndNode or the end of the document.
+            const SwNode* pEndRangeNode;
+            if (j + 1 < nSize)
+                pEndRangeNode = vLevelOutlineNodes[j + 1];
+            else
+                pEndRangeNode = pEndNode;
+
+            SwNodeRange aNodeRange(*vLevelOutlineNodes[j], SwNodeOffset(0), 
*pEndRangeNode,
+                                   SwNodeOffset(0));
+
+            // Move the range of nodes to before the node in the unsorted 
outline vector at the
+            // current iteration index to match the position of the outline 
node in the sorted vector.
+            getIDocumentContentOperations().MoveNodeRange(
+                aNodeRange, *const_cast<SwTextNode*>(vLevelOutlineNodes[i]),
+                SwMoveFlags::DEFAULT | SwMoveFlags::CREATEUNDOOBJ);
+
+            GetLevelOutlineNodesAndEndNode();
+        }
+    };
+
+    const SwOutlineNodes& rOutlineNodes = GetNodes().GetOutLineNds();
+
+    if (rOutlineNodes.empty())
+        return;
+
+    StartAction();
+    StartUndo(SwUndoId::SORT_CHAPTERS);
+
+    // Create an ordered set of outline levels in the outline nodes for use to 
determine
+    // the lowest level to use for first sort and to only iterate over higher 
levels used.
+    std::set<int> aOutlineLevelSet;
+    for (const SwNode* pNode : rOutlineNodes)
+    {
+        int nOutlineLevel = pNode->GetTextNode()->GetAttrOutlineLevel();
+        aOutlineLevelSet.emplace(nOutlineLevel);
+    }
+
+    // No parent node for the lowest outline level nodes sort.
+    if (nOutlineNodePos == SwOutlineNodes::npos)
+    {
+        sort_chapters(nullptr /*pParentNode*/,
+                      
aOutlineLevelSet.extract(aOutlineLevelSet.begin()).value());
+        for (int nOutlineLevel : aOutlineLevelSet)
+        {
+            for (size_t i = 0, nSize = rOutlineNodes.size(); i + 1 < nSize; 
i++)
+            {
+                if (rOutlineNodes[i]->GetTextNode()->GetAttrOutlineLevel() < 
nOutlineLevel
+                    && rOutlineNodes[i + 
1]->GetTextNode()->GetAttrOutlineLevel() == nOutlineLevel)
+                {
+                    const SwNode* pParentNode = rOutlineNodes[i];
+                    sort_chapters(pParentNode, nOutlineLevel);
+                }
+            }
+        }
+    }
+    else
+    {
+        for (int nOutlineLevel : aOutlineLevelSet)
+        {
+            if 
(rOutlineNodes[nOutlineNodePos]->GetTextNode()->GetAttrOutlineLevel()
+                < nOutlineLevel)
+            {
+                const SwNode* pParentNode = rOutlineNodes[nOutlineNodePos];
+                sort_chapters(pParentNode, nOutlineLevel);
+            }
+        }
+    }
+
+    EndUndo(SwUndoId::SORT_CHAPTERS);
+    EndAction();
+}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/uiconfig/swriter/ui/navigatorcontextmenu.ui 
b/sw/uiconfig/swriter/ui/navigatorcontextmenu.ui
index 596b03845523..30d3ba84b83f 100644
--- a/sw/uiconfig/swriter/ui/navigatorcontextmenu.ui
+++ b/sw/uiconfig/swriter/ui/navigatorcontextmenu.ui
@@ -98,6 +98,14 @@
         <accelerator key="Right" signal="activate" 
modifiers="GDK_CONTROL_MASK"/>
       </object>
     </child>
+    <child>
+      <object class="GtkMenuItem" id="sortalphabeticallyindocument">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="label" translatable="yes" 
context="navigatorcontextmenu|STR_SORT_ALPHABETICALLY_IN_DOCUMENT">Sort 
Alphabetically in Document</property>
+        <property name="use-underline">True</property>
+      </object>
+    </child>
     <child>
       <object class="GtkMenuItem" id="401">
         <property name="visible">True</property>
@@ -608,7 +616,7 @@
       <object class="GtkCheckMenuItem" id="sort">
         <property name="visible">True</property>
         <property name="can-focus">False</property>
-        <property name="label" translatable="yes" 
context="navigatorcontextmenu|STR_SORT_ALPHABETICALLY">Sort 
Alphabetically</property>
+        <property name="label" translatable="yes" 
context="navigatorcontextmenu|STR_SORT_ALPHABETICALLY">Display 
Alphabetically</property>
         <property name="use-underline">True</property>
       </object>
     </child>

Reply via email to