sw content controls: add initial DOCX import

- map inline/run SDTs with unknown type (i.e. rich text) to
  SwContentControl

- decouple block and run SDTs and leave block ones unchanged

- track start position of run SDTs similar to bookmarks, which needs
  different code to SDT at text start vs later

- fix DocxAttributeOutput::RunText() to please
  CppunitTest_sw_ooxmlexport2's testFdo67013, which had an inline SDT in
  footer, not at para end

Change-Id: I59b8b7f3170cf37f1547db07ae0992850e0e3aa8
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/133195
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport17.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport17.cxx
index 5d5bdb2..dcfede6 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport17.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport17.cxx
@@ -321,8 +321,9 @@ DECLARE_OOXMLEXPORT_TEST(testTdf148361, "tdf148361.docx")
    uno::Reference<text::XTextField> xTextField1(xFields->nextElement(), uno::UNO_QUERY);
    CPPUNIT_ASSERT_EQUAL(OUString("itadmin"), xTextField1->getPresentation(false));

    uno::Reference<text::XTextField> xTextField2(xFields->nextElement(), uno::UNO_QUERY);
    CPPUNIT_ASSERT_EQUAL(OUString("[Type text]"), xTextField2->getPresentation(false));
    OUString aActual = getParagraph(2)->getString();
    // This was "itadmin".
    CPPUNIT_ASSERT_EQUAL(OUString("[Type text]"), aActual);
}

DECLARE_OOXMLEXPORT_TEST(testTdf142407, "tdf142407.docx")
diff --git a/sw/source/filter/ww8/docxattributeoutput.cxx b/sw/source/filter/ww8/docxattributeoutput.cxx
index c80ea49..ca52bac 100644
--- a/sw/source/filter/ww8/docxattributeoutput.cxx
+++ b/sw/source/filter/ww8/docxattributeoutput.cxx
@@ -3273,6 +3273,7 @@ void DocxAttributeOutput::RunText( const OUString& rText, rtl_TextEncoding /*eCh
    if (m_nCloseContentControlInThisRun > 0)
    {
        ++m_nCloseContentControlInPreviousRun;
        --m_nCloseContentControlInThisRun;
    }
    m_bRunTextIsOn = true;
    // one text can be split into more <w:t>blah</w:t>'s by line breaks etc.
diff --git a/writerfilter/CppunitTest_writerfilter_dmapper.mk b/writerfilter/CppunitTest_writerfilter_dmapper.mk
index 48b4ee8..de1a8ce 100644
--- a/writerfilter/CppunitTest_writerfilter_dmapper.mk
+++ b/writerfilter/CppunitTest_writerfilter_dmapper.mk
@@ -23,6 +23,7 @@ $(eval $(call gb_CppunitTest_add_exception_objects,writerfilter_dmapper, \
    writerfilter/qa/cppunittests/dmapper/GraphicImport \
    writerfilter/qa/cppunittests/dmapper/TextEffectsHandler \
    writerfilter/qa/cppunittests/dmapper/PropertyMap \
    writerfilter/qa/cppunittests/dmapper/SdtHelper \
))

$(eval $(call gb_CppunitTest_use_libraries,writerfilter_dmapper, \
diff --git a/writerfilter/qa/cppunittests/dmapper/SdtHelper.cxx b/writerfilter/qa/cppunittests/dmapper/SdtHelper.cxx
new file mode 100644
index 0000000..da2663b
--- /dev/null
+++ b/writerfilter/qa/cppunittests/dmapper/SdtHelper.cxx
@@ -0,0 +1,89 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <test/bootstrapfixture.hxx>
#include <unotest/macros_test.hxx>

#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/frame/Desktop.hpp>
#include <com/sun/star/text/XTextDocument.hpp>

using namespace com::sun::star;

namespace
{
/// Tests for writerfilter/source/dmapper/SdtHelper.cxx.
class Test : public test::BootstrapFixture, public unotest::MacrosTest
{
private:
    uno::Reference<lang::XComponent> mxComponent;

public:
    void setUp() override;
    void tearDown() override;
    uno::Reference<lang::XComponent>& getComponent() { return mxComponent; }
};

void Test::setUp()
{
    test::BootstrapFixture::setUp();

    mxDesktop.set(frame::Desktop::create(mxComponentContext));
}

void Test::tearDown()
{
    if (mxComponent.is())
        mxComponent->dispose();

    test::BootstrapFixture::tearDown();
}

constexpr OUStringLiteral DATA_DIRECTORY = u"/writerfilter/qa/cppunittests/dmapper/data/";

CPPUNIT_TEST_FIXTURE(Test, testSdtRunRichText)
{
    // Given a document with a rich text inline/run SDT:
    OUString aURL = m_directories.getURLFromSrc(DATA_DIRECTORY) + "sdt-run-rich-text.docx";

    // When loading the document:
    getComponent() = loadFromDesktop(aURL);

    // Then make sure that formatting of the text inside the SDT is not lost:
    uno::Reference<text::XTextDocument> xTextDocument(getComponent(), uno::UNO_QUERY);
    uno::Reference<container::XEnumerationAccess> xParaEnumAccess(xTextDocument->getText(),
                                                                  uno::UNO_QUERY);
    uno::Reference<container::XEnumeration> xParaEnum = xParaEnumAccess->createEnumeration();
    uno::Reference<container::XEnumerationAccess> xPara(xParaEnum->nextElement(), uno::UNO_QUERY);
    uno::Reference<container::XEnumeration> xPortionEnum = xPara->createEnumeration();
    uno::Reference<beans::XPropertySet> xPortion(xPortionEnum->nextElement(), uno::UNO_QUERY);
    OUString aTextPortionType;
    xPortion->getPropertyValue("TextPortionType") >>= aTextPortionType;
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: ContentControl
    // - Actual  : TextField
    // i.e. the SDT was imported as a text field, and the whole SDT had 12pt font size.
    CPPUNIT_ASSERT_EQUAL(OUString("ContentControl"), aTextPortionType);
    uno::Reference<text::XTextContent> xContentControl;
    xPortion->getPropertyValue("ContentControl") >>= xContentControl;
    uno::Reference<text::XTextRange> xContentControlRange(xContentControl, uno::UNO_QUERY);
    uno::Reference<text::XText> xText = xContentControlRange->getText();
    uno::Reference<container::XEnumerationAccess> xContentEnumAccess(xText, uno::UNO_QUERY);
    uno::Reference<container::XEnumeration> xContentEnum = xContentEnumAccess->createEnumeration();
    uno::Reference<beans::XPropertySet> xContent(xContentEnum->nextElement(), uno::UNO_QUERY);
    float fCharheight{};
    xContent->getPropertyValue("CharHeight") >>= fCharheight;
    CPPUNIT_ASSERT_EQUAL(12.f, fCharheight);
    xContent.set(xContentEnum->nextElement(), uno::UNO_QUERY);
    xContent->getPropertyValue("CharHeight") >>= fCharheight;
    CPPUNIT_ASSERT_EQUAL(24.f, fCharheight);
}
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/writerfilter/qa/cppunittests/dmapper/data/sdt-run-rich-text.docx b/writerfilter/qa/cppunittests/dmapper/data/sdt-run-rich-text.docx
new file mode 100644
index 0000000..b945d0b
--- /dev/null
+++ b/writerfilter/qa/cppunittests/dmapper/data/sdt-run-rich-text.docx
Binary files differ
diff --git a/writerfilter/source/dmapper/DomainMapper.cxx b/writerfilter/source/dmapper/DomainMapper.cxx
index 7c2e932..61ff5a2 100644
--- a/writerfilter/source/dmapper/DomainMapper.cxx
+++ b/writerfilter/source/dmapper/DomainMapper.cxx
@@ -1074,14 +1074,33 @@ void DomainMapper::lcl_attribute(Id nName, Value & val)
        }
        break;
        case NS_ooxml::LN_CT_SdtBlock_sdtContent:
        case NS_ooxml::LN_CT_SdtRun_sdtContent:
            if (m_pImpl->m_pSdtHelper->getControlType() == SdtControlType::unknown)
            {
                // Still not determined content type? and it is even not unsupported? Then it is plain text field
                m_pImpl->m_pSdtHelper->setControlType(SdtControlType::plainText);
                if (nName == NS_ooxml::LN_CT_SdtRun_sdtContent)
                {
                    m_pImpl->m_pSdtHelper->setControlType(SdtControlType::richText);
                    m_pImpl->PushSdt();
                }
            }
            m_pImpl->SetSdt(true);
        break;
        case NS_ooxml::LN_CT_SdtBlock_sdtEndContent:
        case NS_ooxml::LN_CT_SdtRun_sdtEndContent:
            if (nName == NS_ooxml::LN_CT_SdtRun_sdtEndContent)
            {
                switch (m_pImpl->m_pSdtHelper->getControlType())
                {
                    case SdtControlType::richText:
                        m_pImpl->PopSdt();
                        break;
                    default:
                        break;
                }
            }

            m_pImpl->SetSdt(false);

            // It's not possible to insert the relevant property to the character context here:
@@ -2734,6 +2753,11 @@ void DomainMapper::sprmWithProps( Sprm& rSprm, const PropertyMapPtr& rContext )
        m_pImpl->disableInteropGrabBag();
    }
    break;
    case NS_ooxml::LN_CT_SdtPr_showingPlcHdr:
    {
        m_pImpl->m_pSdtHelper->SetShowingPlcHdr();
    }
    break;
    case NS_ooxml::LN_CT_SdtPr_dataBinding:
    case NS_ooxml::LN_CT_SdtPr_equation:
    case NS_ooxml::LN_CT_SdtPr_checkbox:
diff --git a/writerfilter/source/dmapper/DomainMapper_Impl.cxx b/writerfilter/source/dmapper/DomainMapper_Impl.cxx
index 6bf0651..2ce7081 100644
--- a/writerfilter/source/dmapper/DomainMapper_Impl.cxx
+++ b/writerfilter/source/dmapper/DomainMapper_Impl.cxx
@@ -832,6 +832,60 @@ void DomainMapper_Impl::SetSdt(bool bSdt)
    }
}

void DomainMapper_Impl::PushSdt()
{
    if (m_aTextAppendStack.empty())
    {
        return;
    }

    uno::Reference<text::XTextAppend> xTextAppend = m_aTextAppendStack.top().xTextAppend;
    uno::Reference<text::XTextCursor> xCursor
        = xTextAppend->getText()->createTextCursorByRange(xTextAppend->getEnd());
    // Offset so the cursor is not adjusted as we import the SDT's content.
    bool bStart = !xCursor->goLeft(1, /*bExpand=*/false);
    m_xSdtStarts.push({bStart, OUString(), xCursor->getStart()});
}

void DomainMapper_Impl::PopSdt()
{
    if (m_xSdtStarts.empty())
    {
        return;
    }

    BookmarkInsertPosition aPosition = m_xSdtStarts.top();
    m_xSdtStarts.pop();
    uno::Reference<text::XTextRange> xStart = aPosition.m_xTextRange;
    uno::Reference<text::XTextRange> xEnd = GetTopTextAppend()->getEnd();
    uno::Reference<text::XText> xText = xEnd->getText();
    uno::Reference<text::XTextCursor> xCursor = xText->createTextCursorByRange(xStart);
    if (!xCursor)
    {
        SAL_WARN("writerfilter.dmapper", "DomainMapper_Impl::PopSdt: no start position");
        return;
    }

    if (aPosition.m_bIsStartOfText)
    {
        xCursor->gotoStart(/*bExpand=*/false);
    }
    else
    {
        // Undo the goLeft() in DomainMapper_Impl::PushSdt();
        xCursor->goRight(1, /*bExpand=*/false);
    }
    xCursor->gotoRange(xEnd, /*bExpand=*/true);
    uno::Reference<text::XTextContent> xContentControl(
        m_xTextFactory->createInstance("com.sun.star.text.ContentControl"), uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xContentControlProps(xContentControl, uno::UNO_QUERY);
    if (m_pSdtHelper->GetShowingPlcHdr())
    {
        xContentControlProps->setPropertyValue("ShowingPlaceHolder",
                                               uno::makeAny(m_pSdtHelper->GetShowingPlcHdr()));
    }
    xText->insertTextContent(xCursor, xContentControl, /*bAbsorb=*/true);
}

void    DomainMapper_Impl::PushProperties(ContextType eId)
{
diff --git a/writerfilter/source/dmapper/DomainMapper_Impl.hxx b/writerfilter/source/dmapper/DomainMapper_Impl.hxx
index 9bef6fb..a8355e1 100644
--- a/writerfilter/source/dmapper/DomainMapper_Impl.hxx
+++ b/writerfilter/source/dmapper/DomainMapper_Impl.hxx
@@ -625,6 +625,7 @@ private:

    css::uno::Reference<css::text::XTextRange> m_xGlossaryEntryStart;
    css::uno::Reference<css::text::XTextRange> m_xSdtEntryStart;
    std::stack<BookmarkInsertPosition> m_xSdtStarts;

    std::queue< css::uno::Reference< css::text::XTextFrame > > m_xPendingTextBoxFrames;

@@ -725,6 +726,8 @@ public:

    /// Setter method for m_bSdt.
    void SetSdt(bool bSdt);
    void PushSdt();
    void PopSdt();
    /// Getter method for m_bSdt.
    bool GetSdt() const { return m_bSdt;}
    bool GetParaChanged() const { return m_bParaChanged;}
diff --git a/writerfilter/source/dmapper/SdtHelper.cxx b/writerfilter/source/dmapper/SdtHelper.cxx
index 0bda90d..924d702 100644
--- a/writerfilter/source/dmapper/SdtHelper.cxx
+++ b/writerfilter/source/dmapper/SdtHelper.cxx
@@ -419,6 +419,10 @@ bool SdtHelper::containedInInteropGrabBag(const OUString& rValueName)
        [&rValueName](const beans::PropertyValue& i) { return i.Name == rValueName; });
}

void SdtHelper::SetShowingPlcHdr() { m_bShowingPlcHdr = true; }

bool SdtHelper::GetShowingPlcHdr() const { return m_bShowingPlcHdr; }

void SdtHelper::clear()
{
    m_aDropDownItems.clear();
@@ -427,6 +431,7 @@ void SdtHelper::clear()
    m_sDataBindingXPath.clear();
    m_sDataBindingStoreItemID.clear();
    m_aGrabBag.clear();
    m_bShowingPlcHdr = false;
}

} // namespace writerfilter::dmapper
diff --git a/writerfilter/source/dmapper/SdtHelper.hxx b/writerfilter/source/dmapper/SdtHelper.hxx
index d898aee..f0515f9 100644
--- a/writerfilter/source/dmapper/SdtHelper.hxx
+++ b/writerfilter/source/dmapper/SdtHelper.hxx
@@ -42,6 +42,7 @@ enum class SdtControlType
    datePicker,
    dropDown,
    plainText,
    richText,
    unsupported, // Sdt block is defined, but we still do not support such type of field
    unknown
};
@@ -93,6 +94,9 @@ class SdtHelper final : public virtual SvRefBase
    /// empty sequence from not yet initialized)
    bool m_bPropertiesXMLsLoaded;

    /// Current contents are placeholder text.
    bool m_bShowingPlcHdr = false;

    /// Create and append the drawing::XControlShape, containing the various models.
    void createControlShape(css::awt::Size aSize,
                            css::uno::Reference<css::awt::XControlModel> const& xControlModel,
@@ -155,6 +159,9 @@ public:
    bool isInteropGrabBagEmpty() const;
    bool containedInInteropGrabBag(const OUString& rValueName);
    sal_Int32 getInteropGrabBagSize() const;

    void SetShowingPlcHdr();
    bool GetShowingPlcHdr() const;
};

} // namespace writerfilter::dmapper
diff --git a/writerfilter/source/ooxml/OOXMLFastContextHandler.cxx b/writerfilter/source/ooxml/OOXMLFastContextHandler.cxx
index e6aa152..9feaffa 100644
--- a/writerfilter/source/ooxml/OOXMLFastContextHandler.cxx
+++ b/writerfilter/source/ooxml/OOXMLFastContextHandler.cxx
@@ -468,6 +468,22 @@ void OOXMLFastContextHandler::endSdt()
    mpStream->props(pProps.get());
}

void OOXMLFastContextHandler::startSdtRun()
{
    OOXMLPropertySet::Pointer_t pProps(new OOXMLPropertySet);
    OOXMLValue::Pointer_t pVal = OOXMLIntegerValue::Create(1);
    pProps->add(NS_ooxml::LN_CT_SdtRun_sdtContent, pVal, OOXMLProperty::ATTRIBUTE);
    mpStream->props(pProps.get());
}

void OOXMLFastContextHandler::endSdtRun()
{
    OOXMLPropertySet::Pointer_t pProps(new OOXMLPropertySet);
    OOXMLValue::Pointer_t pVal = OOXMLIntegerValue::Create(1);
    pProps->add(NS_ooxml::LN_CT_SdtRun_sdtEndContent, pVal, OOXMLProperty::ATTRIBUTE);
    mpStream->props(pProps.get());
}

void OOXMLFastContextHandler::startSectionGroup()
{
    if (isForwardEvents())
diff --git a/writerfilter/source/ooxml/OOXMLFastContextHandler.hxx b/writerfilter/source/ooxml/OOXMLFastContextHandler.hxx
index b64a87e..30491f0 100644
--- a/writerfilter/source/ooxml/OOXMLFastContextHandler.hxx
+++ b/writerfilter/source/ooxml/OOXMLFastContextHandler.hxx
@@ -139,6 +139,8 @@ public:
    virtual void popBiDiEmbedLevel();
    void startSdt();
    void endSdt();
    void startSdtRun();
    void endSdtRun();

    void startField();
    void fieldSeparator();
diff --git a/writerfilter/source/ooxml/model.xml b/writerfilter/source/ooxml/model.xml
index 56767f5..04fb193 100644
--- a/writerfilter/source/ooxml/model.xml
+++ b/writerfilter/source/ooxml/model.xml
@@ -18315,8 +18315,9 @@
      <element name="sdtPr" tokenid="ooxml:CT_SdtRun_sdtPr"/>
      <element name="sdtEndPr" tokenid="ooxml:CT_SdtRun_sdtEndPr"/>
      <element name="sdtContent" tokenid="ooxml:CT_SdtRun_sdtContent"/>
      <action name="start" action="startSdt"/>
      <action name="end" action="endSdt"/>
      <element name="sdtEndContent" tokenid="ooxml:CT_SdtRun_sdtEndContent"/>
      <action name="start" action="startSdtRun"/>
      <action name="end" action="endSdtRun"/>
    </resource>
    <resource name="CT_SdtCell" resource="Stream">
      <element name="sdtPr" tokenid="ooxml:CT_SdtCell_sdtPr"/>