include/oox/export/drawingml.hxx         |    5 +
 oox/qa/unit/data/outliner-list-style.odp |binary
 oox/qa/unit/export.cxx                   |   25 ++++++
 oox/source/export/drawingml.cxx          |  129 +++++++++++++++++++++++++++++--
 4 files changed, 151 insertions(+), 8 deletions(-)

New commits:
commit 6a8b96ddd47af2be3f06e299ee7058438083ba5b
Author:     Miklos Vajna <[email protected]>
AuthorDate: Fri Sep 26 11:52:23 2025 +0200
Commit:     Caolán McNamara <[email protected]>
CommitDate: Fri Sep 26 14:49:36 2025 +0200

    PPTX export: fix missing non-first level list style for outline shapes
    
    Export the bugdoc to PPTX, open in PowerPoint, go to the first slide, go
    to the end of the first bullet, enter, tab, indent grows to a large
    value, while it should only grow to match the existing other already
    indented bullet.
    
    It seems this happens because these indents are defined in the "list
    style" of the shape, where we wrote the paragraph properties only for
    the first level, see commit 0f9dc676eefce79ea63218edd910af486a09a52b
    (tdf#59323: pptx export: add initial support for lstStyles in textboxes,
    2021-06-16).
    
    Fix the problem by writing paragraph properties for all list levels that
    the outliner shape stores: Impress has 7 levels and PowerPoint has
    markup for 9 levels, so in practice this can be mapped without problems.
    
    A further improvement would be to make sure the outline shape on the
    normal page and the master page is associated correctly, and then it
    would not be necessary to repeat the formatting for shapes on the normal
    slide. Similarly, the PPTX import still needs to parse this list style
    of the shape, which is not yet done here, so the editing in PowerPoint
    is now correct, but not in Impress.
    
    Change-Id: I1aecb3c062ccbe5014d322d0561f0d2ff50ac698
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/191536
    Tested-by: Caolán McNamara <[email protected]>
    Tested-by: Jenkins CollaboraOffice <[email protected]>
    Reviewed-by: Caolán McNamara <[email protected]>

diff --git a/include/oox/export/drawingml.hxx b/include/oox/export/drawingml.hxx
index 810fc4ff492e..e5eaf6f60363 100644
--- a/include/oox/export/drawingml.hxx
+++ b/include/oox/export/drawingml.hxx
@@ -451,6 +451,11 @@ public:
 
     void WriteText( const css::uno::Reference< css::uno::XInterface >& 
rXIface, bool bBodyPr, bool bText = true, sal_Int32 nXmlNamespace = 0, bool 
bWritePropertiesAsLstStyles = false);
 
+    /// Writes one list level inside the list styles container.
+    void WriteLstStyle(const css::uno::Reference<css::text::XTextContent>& 
rParagraph,
+                       bool& rbOverridingCharHeight, sal_Int32& rnCharHeight,
+                       const css::uno::Reference<css::beans::XPropertySet>& 
rXShapePropSet,
+                       sal_Int32 nElement);
     /** Populates the lstStyle with the shape's text run and paragraph 
properties */
     void WriteLstStyles(const css::uno::Reference<css::text::XTextContent>& 
rParagraph,
                        bool& rbOverridingCharHeight, sal_Int32& rnCharHeight,
diff --git a/oox/qa/unit/data/outliner-list-style.odp 
b/oox/qa/unit/data/outliner-list-style.odp
new file mode 100644
index 000000000000..dc86caf053bf
Binary files /dev/null and b/oox/qa/unit/data/outliner-list-style.odp differ
diff --git a/oox/qa/unit/export.cxx b/oox/qa/unit/export.cxx
index 63351ae79e4b..30273cd2acf2 100644
--- a/oox/qa/unit/export.cxx
+++ b/oox/qa/unit/export.cxx
@@ -1432,6 +1432,31 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf163803_ImageFill)
     assertXPath(pXmlDoc, "//p:pic/p:spPr/a:solidFill");
     assertXPath(pXmlDoc, "//p:pic/p:spPr/a:solidFill/a:srgbClr", "val", 
u"000000");
 }
+
+CPPUNIT_TEST_FIXTURE(Test, testPPTXExportOutlinerListStyle)
+{
+    // Given a slide with an outliner shape and a matching master slide with 
its own outliner shape:
+    loadFromFile(u"outliner-list-style.odp");
+
+    // When saving to PPTX:
+    save(u"Impress Office Open XML"_ustr);
+
+    // Then make sure that the list style of the outliner shape on the master 
page is written:
+    xmlDocUniquePtr pXmlDoc = 
parseExport(u"ppt/slideMasters/slideMaster1.xml"_ustr);
+    assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl1pPr", 1);
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: 1
+    // - Actual  : 0
+    // - XPath '//p:sp[2]/p:txBody/a:lstStyle/a:lvl2pPr' number of nodes is 
incorrect
+    // i.e. only the first list level was written, the UI couldn't format a 
new 2nd level paragraph
+    // correctly.
+    assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl2pPr", 1);
+    assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl3pPr", 1);
+    assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl4pPr", 1);
+    assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl5pPr", 1);
+    assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl6pPr", 1);
+    assertXPath(pXmlDoc, "//p:sp[2]/p:txBody/a:lstStyle/a:lvl7pPr", 1);
+}
 }
 
 CPPUNIT_PLUGIN_IMPLEMENT();
diff --git a/oox/source/export/drawingml.cxx b/oox/source/export/drawingml.cxx
index 3efaac66122d..85aa932240f6 100644
--- a/oox/source/export/drawingml.cxx
+++ b/oox/source/export/drawingml.cxx
@@ -137,6 +137,7 @@
 #include <svx/EnhancedCustomShape2d.hxx>
 #include <drawingml/presetgeometrynames.hxx>
 #include <docmodel/uno/UnoGradientTools.hxx>
+#include <svx/svdpage.hxx>
 
 using namespace ::css;
 using namespace ::css::beans;
@@ -3648,15 +3649,31 @@ bool DrawingML::WriteParagraphProperties(const 
Reference<XTextContent>& rParagra
     WriteParagraphTabStops( rXPropSet );
 
     // do not end element for lstStyles since, defRPr should be stacked inside 
it
-    if( nElement != XML_lvl1pPr )
+    bool bLstStyle = false;
+    switch (nElement)
+    {
+        case XML_lvl1pPr:
+        case XML_lvl2pPr:
+        case XML_lvl3pPr:
+        case XML_lvl4pPr:
+        case XML_lvl5pPr:
+        case XML_lvl6pPr:
+        case XML_lvl7pPr:
+        case XML_lvl8pPr:
+        case XML_lvl9pPr:
+            bLstStyle = true;
+            break;
+    }
+    if( !bLstStyle )
         mpFS->endElementNS( XML_a, nElement );
 
     return true;
 }
 
-void DrawingML::WriteLstStyles(const 
css::uno::Reference<css::text::XTextContent>& rParagraph,
+void DrawingML::WriteLstStyle(const 
css::uno::Reference<css::text::XTextContent>& rParagraph,
                                bool& rbOverridingCharHeight, sal_Int32& 
rnCharHeight,
-                               const 
css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet)
+                               const 
css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet,
+                               sal_Int32 nElement)
 {
     Reference<XEnumerationAccess> xAccess(rParagraph, UNO_QUERY);
     if (!xAccess.is())
@@ -3683,14 +3700,110 @@ void DrawingML::WriteLstStyles(const 
css::uno::Reference<css::text::XTextContent
         if (xFirstRunPropSetInfo->hasPropertyByName(u"CharHeight"_ustr))
             fFirstCharHeight = 
xFirstRunPropSet->getPropertyValue(u"CharHeight"_ustr).get<float>();
 
-        mpFS->startElementNS(XML_a, XML_lstStyle);
-        if( !WriteParagraphProperties(rParagraph, fFirstCharHeight, 
XML_lvl1pPr) )
-            mpFS->startElementNS(XML_a, XML_lvl1pPr);
+        if( !WriteParagraphProperties(rParagraph, fFirstCharHeight, nElement) )
+            mpFS->startElementNS(XML_a, nElement);
         WriteRunProperties(xFirstRunPropSet, false, XML_defRPr, true, 
rbOverridingCharHeight,
                            rnCharHeight, GetScriptType(rRun->getString()), 
rXShapePropSet);
-        mpFS->endElementNS(XML_a, XML_lvl1pPr);
-        mpFS->endElementNS(XML_a, XML_lstStyle);
+        mpFS->endElementNS(XML_a, nElement);
+    }
+}
+
+namespace
+{
+/// In case xShapeProps is an outliner shape, return a paragraph enumeration 
describing the outline
+/// text format.
+uno::Reference<container::XEnumeration> GetOutlinerTextFormatParaEnum(const 
uno::Reference<beans::XPropertySet>& xShapeProps)
+{
+    SdrObject* pShape = SdrObject::getSdrObjectFromXShape(xShapeProps);
+    if (pShape->GetObjIdentifier() != SdrObjKind::OutlineText)
+    {
+        // Not an outliner shape.
+        return {};
+    }
+
+    SdrPage* pPage = pShape->getSdrPageFromSdrObject();
+    if (pPage->TRG_HasMasterPage())
+    {
+        // Not a master page, the matching master page contains the shape that 
provides the outline
+        // text format.
+        SdrPage& rMasterPage = pPage->TRG_GetMasterPage();
+        for (const rtl::Reference<SdrObject>& pObject : rMasterPage)
+        {
+            if (pObject->GetObjIdentifier() == SdrObjKind::OutlineText)
+            {
+                pShape = pObject.get();
+                break;
+            }
+        }
     }
+
+    uno::Reference<text::XTextRange> xOutliner(pShape->getUnoShape(), 
uno::UNO_QUERY);
+    uno::Reference<container::XEnumerationAccess> xText(xOutliner->getText(), 
uno::UNO_QUERY);
+    return xText->createEnumeration();
+}
+}
+
+void DrawingML::WriteLstStyles(const 
css::uno::Reference<css::text::XTextContent>& rParagraph,
+                               bool& rbOverridingCharHeight, sal_Int32& 
rnCharHeight,
+                               const 
css::uno::Reference<css::beans::XPropertySet>& rXShapePropSet)
+{
+    mpFS->startElementNS(XML_a, XML_lstStyle);
+
+    uno::Reference<container::XEnumeration> xMasterParagraphEnum = 
GetOutlinerTextFormatParaEnum(rXShapePropSet);
+    if (xMasterParagraphEnum.is())
+    {
+        // Outliner shape, write the outline text format as multiple list 
levels, each with its
+        // paragraph properties.
+        sal_Int32 nLevel = 0;
+        while (xMasterParagraphEnum->hasMoreElements())
+        {
+            uno::Reference<css::text::XTextContent> 
xParagraph(xMasterParagraphEnum->nextElement(), uno::UNO_QUERY);
+            sal_Int32 nElement = 0;
+            switch (nLevel)
+            {
+                case 0:
+                    nElement = XML_lvl1pPr;
+                    break;
+                case 1:
+                    nElement = XML_lvl2pPr;
+                    break;
+                case 2:
+                    nElement = XML_lvl3pPr;
+                    break;
+                case 3:
+                    nElement = XML_lvl4pPr;
+                    break;
+                case 4:
+                    nElement = XML_lvl5pPr;
+                    break;
+                case 5:
+                    nElement = XML_lvl6pPr;
+                    break;
+                case 6:
+                    nElement = XML_lvl7pPr;
+                    break;
+                case 7:
+                    nElement = XML_lvl8pPr;
+                    break;
+                case 8:
+                    nElement = XML_lvl9pPr;
+                    break;
+            }
+            if (!nElement)
+            {
+                break;
+            }
+
+            WriteLstStyle(xParagraph, rbOverridingCharHeight, rnCharHeight, 
rXShapePropSet, nElement);
+            ++nLevel;
+        }
+    }
+    else
+    {
+        WriteLstStyle(rParagraph, rbOverridingCharHeight, rnCharHeight, 
rXShapePropSet, XML_lvl1pPr);
+    }
+
+    mpFS->endElementNS(XML_a, XML_lstStyle);
 }
 
 void DrawingML::WriteParagraph( const Reference< XTextContent >& rParagraph,

Reply via email to