xmloff/qa/unit/style.cxx           |   19 +++++++++++++--
 xmloff/source/draw/shapeexport.cxx |   14 ++++++++++-
 xmloff/source/draw/ximpshap.cxx    |   46 +++++++++++++++++++++++++++++++++++++
 3 files changed, 76 insertions(+), 3 deletions(-)

New commits:
commit ae6ce55091b063eb8bd695ab25ea28fc053d9ece
Author:     Caolán McNamara <[email protected]>
AuthorDate: Sat Feb 28 20:37:52 2026 +0000
Commit:     Caolán McNamara <[email protected]>
CommitDate: Sun Mar 1 20:15:09 2026 +0100

    tdf#156707 always write fo:border for form control shapes on export
    
    On export, always write ControlBorder and ControlBorderColor to the
    auto style for control shapes, even when the property state is
    DEFAULT_VALUE.
    
    On import, when fo:border is absent, apply the control model's default
    border to the shape.
    
    a) New document, user sets flat (2) border:
        The constructor default is 2 (flat) but getPropertyDefaultByHandle
        returns 1 (3D). Property state is DIRECT_VALUE and the filter exports
        fo:border as before. On reimport fo:border is present and applied.
        No change in behavior.
    
    b) New document, user sets no (0) border:
        Same as flat, DIRECT_VALUE, exported by the filter. No change.
    
    c) New document, user sets 3D (1) border:
        1 matches getPropertyDefaultByHandle so property state is
        DEFAULT_VALUE and the export filter would previously skip it.
        Now export always adds adds ControlBorder explicitly, so fo:border
        is written. On reimport fo:border is present so applied.
    
    d) Old document (pre tdf#152974) with no fo:border:
        These documents never wrote fo:border for 3D because that was the
        real default. Now on import the importer detects the missing
        fo:border and queries getPropertyDefault("Border") for the control
        which returns 1 (3D) for text controls, restoring the original
        border. On reexport the exporter writes an explicit fo:border.
    
    e) Label controls (tdf#167358):
        Labels default to no (0) border. Their getPropertyDefault
        returns 0, so the importer applies no border when fo:border is
        absent, so this fix effort shouldn't cause that problem again.
    
    Change-Id: I365adf84e9d26ae3231331230178381d31bfa829
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/200685
    Tested-by: Jenkins
    Reviewed-by: Caolán McNamara <[email protected]>

diff --git a/xmloff/qa/unit/style.cxx b/xmloff/qa/unit/style.cxx
index 677022579a3c..b3fc52123e27 100644
--- a/xmloff/qa/unit/style.cxx
+++ b/xmloff/qa/unit/style.cxx
@@ -304,13 +304,28 @@ CPPUNIT_TEST_FIXTURE(XmloffStyleTest, testTdf156707)
     xShape = getShape(1);
     xShapeProperties.set(xShape, uno::UNO_QUERY_THROW);
     xShapeProperties->getPropertyValue(u"ControlBorder"_ustr) >>= nBorderStyle;
-    // since tdf#152974, this shape SHOULD ACTUALLY have a 3d border (1), NOT 
a flat one(2).
-    CPPUNIT_ASSERT_EQUAL_MESSAGE("DID YOU FIX ME?", sal_uInt16(2), 
nBorderStyle);
+    // since tdf#152974, this shape should have a 3d border (1), not a flat 
one(2).
+    CPPUNIT_ASSERT_EQUAL(sal_uInt16(1), nBorderStyle);
 
     xShape = getShape(2);
     xShapeProperties.set(xShape, uno::UNO_QUERY_THROW);
     xShapeProperties->getPropertyValue(u"ControlBorder"_ustr) >>= nBorderStyle;
     CPPUNIT_ASSERT_EQUAL(sal_uInt16(0), nBorderStyle);
+
+    // Verify export: fo:border must be written for all three control shapes
+    xmlDocUniquePtr pXmlDoc = parseExport(u"content.xml"_ustr);
+    static constexpr OString sStylePath = 
"/office:document-content/office:automatic-styles/"
+                                          "style:style[@style:name='"_ostr;
+    static constexpr OString sGfxProps = 
"']/style:graphic-properties[@fo:border]"_ostr;
+
+    // Get the auto style name from each draw:control element
+    OUString sStyle0 = getXPath(pXmlDoc, "(//draw:control)[1]", "style-name");
+    OUString sStyle1 = getXPath(pXmlDoc, "(//draw:control)[2]", "style-name");
+    OUString sStyle2 = getXPath(pXmlDoc, "(//draw:control)[3]", "style-name");
+
+    assertXPath(pXmlDoc, sStylePath + sStyle0.toUtf8() + sGfxProps, 1);
+    assertXPath(pXmlDoc, sStylePath + sStyle1.toUtf8() + sGfxProps, 1);
+    assertXPath(pXmlDoc, sStylePath + sStyle2.toUtf8() + sGfxProps, 1);
 }
 
 CPPUNIT_TEST_FIXTURE(XmloffStyleTest, testTdf167358)
diff --git a/xmloff/source/draw/shapeexport.cxx 
b/xmloff/source/draw/shapeexport.cxx
index 5a17e37b0d75..7ffd051464fc 100644
--- a/xmloff/source/draw/shapeexport.cxx
+++ b/xmloff/source/draw/shapeexport.cxx
@@ -445,11 +445,12 @@ void XMLShapeExport::collectShapeAutoStyles(const 
uno::Reference< drawing::XShap
                 {
                     uno::Reference< beans::XPropertySet > 
xControlModel(xControl->getControl(), uno::UNO_QUERY);
                     DBG_ASSERT(xControlModel.is(), 
"XMLShapeExport::collectShapeAutoStyles: no control model on the control 
shape!");
+                    const rtl::Reference<XMLPropertySetMapper>& rMapper = 
GetPropertySetMapper()->getPropertySetMapper();
 
                     OUString sNumberStyle = 
mrExport.GetFormExport()->getControlNumberStyle(xControlModel);
                     if (!sNumberStyle.isEmpty())
                     {
-                        sal_Int32 nIndex = 
GetPropertySetMapper()->getPropertySetMapper()->FindEntryIndex(CTF_SD_CONTROL_SHAPE_DATA_STYLE);
+                        sal_Int32 nIndex = 
rMapper->FindEntryIndex(CTF_SD_CONTROL_SHAPE_DATA_STYLE);
                             // TODO : this retrieval of the index could be 
moved into the ctor, holding the index
                             //          as member, thus saving time.
                         DBG_ASSERT(-1 != nIndex, 
"XMLShapeExport::collectShapeAutoStyles: could not obtain the index for our 
context id!");
@@ -457,6 +458,17 @@ void XMLShapeExport::collectShapeAutoStyles(const 
uno::Reference< drawing::XShap
                         XMLPropertyState aNewState(nIndex, 
uno::Any(sNumberStyle));
                         aPropStates.push_back(aNewState);
                     }
+
+                    // tdf#156707 always export ControlBorder for control 
shapes, even when property state is DEFAULT_VALUE
+                    sal_Int32 nBorderIndex = 
rMapper->FindEntryIndex("ControlBorder", XML_NAMESPACE_FO, 
GetXMLToken(XML_BORDER));
+                    if (nBorderIndex != -1)
+                    {
+                        if (!std::any_of(aPropStates.cbegin(), 
aPropStates.cend(),
+                            [nBorderIndex](const XMLPropertyState& rProp) { 
return rProp.mnIndex == nBorderIndex; }))
+                        {
+                            aPropStates.emplace_back(nBorderIndex, 
xPropSet->getPropertyValue(u"ControlBorder"_ustr));
+                        }
+                    }
                 }
             }
 
diff --git a/xmloff/source/draw/ximpshap.cxx b/xmloff/source/draw/ximpshap.cxx
index 40912115fbe2..a39c1d50aa76 100644
--- a/xmloff/source/draw/ximpshap.cxx
+++ b/xmloff/source/draw/ximpshap.cxx
@@ -49,6 +49,7 @@
 #include <com/sun/star/lang/XMultiServiceFactory.hpp>
 #include <com/sun/star/util/XCloneable.hpp>
 #include <com/sun/star/beans/XMultiPropertyStates.hpp>
+#include <com/sun/star/beans/XPropertyState.hpp>
 #include <xexptran.hxx>
 #include <com/sun/star/drawing/PolyPolygonBezierCoords.hpp>
 #include <com/sun/star/beans/XPropertySetInfo.hpp>
@@ -1725,6 +1726,51 @@ void SdXMLControlShapeContext::startFastElement 
(sal_Int32 nElement,
     }
 
     SetStyle();
+
+    // tdf#156707 if fo:border was not in the auto style, apply the control
+    // model's own default (which varies per control type) to the shape
+    if (const XMLPropStyleContext* pDocStyle = dynamic_cast<const 
XMLPropStyleContext*>(FindAutoStyle()))
+    {
+        SvXMLImportPropertyMapper* pImpMapper = 
GetImport().GetShapeImport()->GetPropertySetMapper();
+        const rtl::Reference<XMLPropertySetMapper>& rMapper = 
pImpMapper->getPropertySetMapper();
+
+        bool bHasBorder = false;
+        for (const auto& rProp : pDocStyle->GetProperties())
+        {
+            if (rProp.mnIndex < 0)
+                continue;
+            if (rMapper->GetEntryAPIName(rProp.mnIndex) == "ControlBorder")
+            {
+                bHasBorder = true;
+                break;
+            }
+        }
+
+        if (!bHasBorder)
+        {
+            uno::Reference<drawing::XControlShape> xControl(mxShape, 
uno::UNO_QUERY);
+            if (xControl.is())
+            {
+                uno::Reference<beans::XPropertySet> 
xControlModel(xControl->getControl(), uno::UNO_QUERY);
+                if (xControlModel.is())
+                {
+                    uno::Reference<beans::XPropertySetInfo> 
xInfo(xControlModel->getPropertySetInfo());
+                    if (xInfo.is() && xInfo->hasPropertyByName(u"Border"_ustr))
+                    {
+                        uno::Reference<beans::XPropertyState> 
xPropState(xControlModel, uno::UNO_QUERY);
+                        if (xPropState.is())
+                        {
+                            uno::Any aDefault = 
xPropState->getPropertyDefault(u"Border"_ustr);
+                            uno::Reference<beans::XPropertySet> 
xShapePropSet(mxShape, uno::UNO_QUERY);
+                            if (xShapePropSet.is())
+                                
xShapePropSet->setPropertyValue(u"ControlBorder"_ustr, aDefault);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     SetLayer();
 
     // set pos, size, shear and rotate

Reply via email to