tdf#168559 PPTX exp: 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 0f9dc676ee
(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/+/191544
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins
This commit is contained in:
Miklos Vajna
2025-09-26 16:14:54 +02:00
parent e03dbd2e30
commit b4d45c41ce
4 changed files with 151 additions and 8 deletions
+5
View File
@@ -458,6 +458,11 @@ public:
sal_Int32 nXmlNamespace, bool bIsFontworkShape,
sal_Int32 nTop, sal_Int32 nBottom, sal_Int32 nLeft, sal_Int32 nRight);
/// 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,
Binary file not shown.
+25
View File
@@ -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();
+121 -8
View File
@@ -138,6 +138,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;
@@ -3649,15 +3650,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())
@@ -3684,16 +3701,112 @@ 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,
bool& rbOverridingCharHeight, sal_Int32& rnCharHeight,
const css::uno::Reference< css::beans::XPropertySet >& rXShapePropSet)