tdf#118947 sw tablestyle: manually scan parents for ::SET

...since we do not want to find DocDefaults or pool defaults.

This patch mainly does two things.
1.) Since DocDefaults get merged into root-level styles,
this patch identifies these DocDefaults as values that
can be overwritten by table style properties.

2.) Some properties (like left/right margins) are paired
together, so if one is SET, the other is also initialized
and reported as SET. These are identified, and
followed to see if they actually inherit from pool defaults.

Change-Id: I4291c77073858ae23360a9f34d3650bfd5df86ca
Reviewed-on: https://gerrit.libreoffice.org/81413
Tested-by: Jenkins
Reviewed-by: Justin Luth <justin_luth@sil.org>
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
diff --git a/sw/qa/extras/ooxmlexport/data/tdf118947_tableStyle.docx b/sw/qa/extras/ooxmlexport/data/tdf118947_tableStyle.docx
new file mode 100644
index 0000000..0b40aab
--- /dev/null
+++ b/sw/qa/extras/ooxmlexport/data/tdf118947_tableStyle.docx
Binary files differ
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx
index 64c1fc6..0d6cdb8 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport13.cxx
@@ -10,6 +10,7 @@
#include <swmodeltestbase.hxx>

#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/style/LineSpacing.hpp>
#include <com/sun/star/text/WritingMode2.hpp>
#include <com/sun/star/text/XTextFrame.hpp>
#include <com/sun/star/drawing/XControlShape.hpp>
@@ -311,6 +312,34 @@ DECLARE_OOXMLEXPORT_TEST(testTdf123636_newlinePageBreak4, "tdf123636_newlinePage
    assertXPath(pDump, "/root/page[2]/body/txt[1]/Text", 0);
}

DECLARE_OOXMLEXPORT_TEST(testTdf118947_tableStyle, "tdf118947_tableStyle.docx")
{
    uno::Reference<text::XTextTable> xTable(getParagraphOrTable(1), uno::UNO_QUERY);
    uno::Reference<text::XTextRange> xCell(xTable->getCellByName("A1"), uno::UNO_QUERY);
    uno::Reference<container::XEnumerationAccess> xParaEnumAccess(xCell->getText(), uno::UNO_QUERY);
    uno::Reference<container::XEnumeration> xParaEnum = xParaEnumAccess->createEnumeration();
    uno::Reference<text::XTextRange> xPara(xParaEnum->nextElement(), uno::UNO_QUERY);
    CPPUNIT_ASSERT_EQUAL(OUString("Table grid settings set line-spacing to 250% instead of single-spacing, which is set as a document default."), xPara->getString());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("TextBody has 10pt font size", 10.f, getProperty<float>(xPara, "CharHeight"));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("TextBody has 1pt space below paragraph", sal_Int32(35), getProperty<sal_Int32>(xPara, "ParaBottomMargin"));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Table has 10pt space above paragraph", sal_Int32(353), getProperty<sal_Int32>(xPara, "ParaTopMargin"));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Table style sets 0 right margin", sal_Int32(0), getProperty<sal_Int32>(xPara, "ParaRightMargin"));
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("TextBody has 1.07 line-spacing", sal_Int16(107), getProperty<style::LineSpacing>(xPara, "ParaLineSpacing").Height, 1);

    xCell.set(xTable->getCellByName("A2"), uno::UNO_QUERY);
    xParaEnumAccess.set(xCell->getText(), uno::UNO_QUERY);
    xParaEnum = xParaEnumAccess->createEnumeration();
    xPara.set(xParaEnum->nextElement(), uno::UNO_QUERY);
    CPPUNIT_ASSERT_EQUAL(OUString("Notice that this is 8pt font in compatibility mode."), xPara->getString());
    // Even though not specified, Table-Style distributes the DocDefault font/justification unless overrideTableStyleFontSizeAndJustification.
    // DocDefault is 8pt.
    //CPPUNIT_ASSERT_EQUAL_MESSAGE("Compat mode has 8pt font size", 8.f, getProperty<float>(xPara, "CharHeight"));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Normal has 0pt space below paragraph", sal_Int32(0), getProperty<sal_Int32>(xPara, "ParaBottomMargin"));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Table sets 10pt space above paragraph", sal_Int32(353), getProperty<sal_Int32>(xPara, "ParaTopMargin"));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Table style sets 0 right margin", sal_Int32(0), getProperty<sal_Int32>(xPara, "ParaRightMargin"));
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("Table sets 2.5 line-spacing", sal_Int16(250), getProperty<style::LineSpacing>(xPara, "ParaLineSpacing").Height, 1);
}

DECLARE_OOXMLEXPORT_TEST(tdf123912_protectedForm, "tdf123912_protectedForm.odt")
{
    SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument *>(mxComponent.get());
diff --git a/sw/source/core/unocore/unotbl.cxx b/sw/source/core/unocore/unotbl.cxx
index 7733037..34c2b1b 100644
--- a/sw/source/core/unocore/unotbl.cxx
+++ b/sw/source/core/unocore/unotbl.cxx
@@ -982,6 +982,67 @@ uno::Reference< beans::XPropertySetInfo >  SwXCell::getPropertySetInfo()
    return xRef;
}

// If the current property matches the previous parent's property (i.e. no reason for it to be set),
// then it may be a ::DEFAULT value, even if it is marked as ::SET
static bool lcl_mayBeDefault( const sal_uInt16 nWhich, sal_uInt8 nMemberId,
                       const SfxPoolItem* pPrevItem, const SfxPoolItem& rCurrItem,
                       const bool bDirect )
{
    bool bMayBeDefault = false;
    // These are the paragraph/character pairs that I found running unit tests.
    // UNFORTUNATELY there is no way to see if a property has multiple members.
    // Since valid members can be nMemberId == 0, we can't do something like "if (nMemberId & ~CONVERT_TWIPS) != 0"
    // Perhaps the full list can be found in editeng/memberids.h???
    switch ( nWhich )
    {
        case RES_BOX:
        case RES_UL_SPACE:
        case RES_LR_SPACE:
        case RES_CHRATR_ESCAPEMENT:
        case RES_CHRATR_FONT:
        case RES_CHRATR_CJK_FONT:
        case RES_CHRATR_CTL_FONT:
        case RES_CHRATR_FONTSIZE:
        case RES_CHRATR_CJK_FONTSIZE:
        case RES_CHRATR_CTL_FONTSIZE:
        case RES_CHRATR_WEIGHT:
        case RES_CHRATR_CJK_WEIGHT:
        case RES_CHRATR_CTL_WEIGHT:
        case RES_CHRATR_LANGUAGE:
        case RES_CHRATR_CJK_LANGUAGE:
        case RES_CHRATR_CTL_LANGUAGE:
        case RES_CHRATR_POSTURE:
        case RES_CHRATR_CJK_POSTURE:
        case RES_CHRATR_CTL_POSTURE:
        case RES_PARATR_ADJUST:
        {
            // These properties are paired up, containing multiple properties in one nWhich.
            // If one is ::SET, they all report ::SET, even if only initialized with the default value.
            // Assume created automatically by another MemberId.
            bMayBeDefault = true;
            if ( pPrevItem )
            {
                uno::Any aPrev;
                uno::Any aCurr;
                (*pPrevItem).QueryValue(aPrev, nMemberId);
                rCurrItem.QueryValue(aCurr, nMemberId);
                // If different, it overrides a parent value, so can't be considered a default.
                bMayBeDefault = aPrev == aCurr;
            }
            break;
        }
        default:
        {
            // Since DocDefaults are copied into root-level stylesheets (tdf#103961),
            // identify the duplicated properties as DocDefault values.
            // Assume any style information could have been inherited/copied.
            if ( !bDirect )
                bMayBeDefault = !pPrevItem || *pPrevItem == rCurrItem;
        }
    }
    return bMayBeDefault;
}

void SwXCell::setPropertyValue(const OUString& rPropertyName, const uno::Any& aValue)
{
    SolarMutexGuard aGuard;
@@ -1024,26 +1085,61 @@ void SwXCell::setPropertyValue(const OUString& rPropertyName, const uno::Any& aV
                while ( &aIdx.GetNode() != pEndNd )
                {
                    const SwTextNode* pNd = aIdx.GetNode().GetTextNode();
                    if ( pNd )
                    {
                        //point and mark selecting the whole paragraph
                        SwPaM aPaM(*pNd, 0, *pNd, pNd->GetText().getLength());
                        const bool bHasAttrSet = pNd->HasSwAttrSet();
                        const SfxItemSet& aSet = pNd->GetSwAttrSet();
                        // isPARATR: replace DEFAULT_VALUE properties only
                        // Require that the property is default in the paragraph style as well,
                        // unless the style is the default style.
                        // isCHRATR: change the base/auto SwAttr property, but don't remove the DIRECT hints
                        bool bCustomParent = false;
                        if (const SwFormatColl* pFormatColl = pNd->GetFormatColl())
                        {
                            bCustomParent = pFormatColl->GetPoolFormatId() != RES_POOLCOLL_STANDARD;
                        }
                        bool bSearchInParent = bCustomParent && !pNd->GetNumRule();
                        if ( !bHasAttrSet || SfxItemState::DEFAULT == aSet.GetItemState(pEntry->nWID, bSearchInParent) )
                            SwUnoCursorHelper::SetPropertyValue(aPaM, rParaPropSet, rPropertyName, aValue, SetAttrMode::DONTREPLACE);
                    }
                    ++aIdx;
                    if ( !pNd )
                        continue;

                    const SfxPoolItem* pPrevItem = nullptr;
                    const SfxPoolItem* pCurrItem = nullptr;
                    // Table-styles don't override direct formatting
                    if ( pNd->HasSwAttrSet() && SfxItemState::SET == pNd->GetSwAttrSet().GetItemState(pEntry->nWID, false, &pCurrItem) )
                    {
                        // Some WIDs have several MIDs, so perhaps ::SET refers to another MID and this property was copied from parents?
                        if ( lcl_mayBeDefault(pEntry->nWID, pEntry->nMemberId, pPrevItem, *pCurrItem, /*bDirect=*/true) )
                            pPrevItem = pCurrItem;
                        else
                            continue; //don't override direct formatting
                    }

                    bool bSet = false;
                    // HACK: don't check styles if numbering/bullets are turned on. Table-styles don't override numbering formatting
                    SwFormat* pFormatColl = pNd->GetNumRule() ? nullptr : pNd->GetFormatColl();
                    // Manually walk through the parent properties in order to avoid the default properties.
                    // Table-styles don't override paragraph-style formatting.
                    //    TODO: ?except for fontsize/justification if compat:overrideTableStyleFontSizeAndJustification?
                    while ( pFormatColl )
                    {
                        if ( SfxItemState::SET == pFormatColl->GetItemState(pEntry->nWID, /*bSrchInParent=*/false, &pCurrItem) )
                        {
                            if ( lcl_mayBeDefault(pEntry->nWID, pEntry->nMemberId, pPrevItem, *pCurrItem, false) )
                            {
                                // if the property matches DocDefaults, then table-style needs to override it
                                pPrevItem = pFormatColl->IsDefault() ? nullptr : pCurrItem;
                            }
                            else
                            {
                                bSet = true; //don't override style formatting
                                break;
                            }
                        }
                        pFormatColl = pFormatColl->DerivedFrom();
                    }
                    if ( bSet )
                        continue;

                    // Check if previous ::SET came from the pool defaults.
                    if ( pPrevItem && pNd->GetSwAttrSet().GetPool() )
                    {
                        pCurrItem = &pNd->GetSwAttrSet().GetPool()->GetDefaultItem(pEntry->nWID);
                        if ( !lcl_mayBeDefault(pEntry->nWID, pEntry->nMemberId, pPrevItem, *pCurrItem, false) )
                            continue;
                    }

                    // Apply table-style property
                    // point and mark selecting the whole paragraph
                    SwPaM aPaM(*pNd, 0, *pNd, pNd->GetText().getLength());
                    // for isCHRATR: change the base/auto SwAttr property, but don't remove the DIRECT hints
                    SwUnoCursorHelper::SetPropertyValue(aPaM, rParaPropSet, rPropertyName, aValue, SetAttrMode::DONTREPLACE);
                }
                return;
            }