sw content controls, drop-down: add doc model & UNO API

Add a new property, which is a list of display-text / value pairs. If
the list is non-empty, that implies that the type is a dropdown.

This should be enough for the UI to be able to provide a list of choices
& update dropdown state on click.

Note that in contrast to dropdown field-marks, here each entry has a
user-readable string and a machine-readable value. Fieldmarks only had a
single value.

Change-Id: I22b9f554e2e1a9e84cc7eb7e17772ea1a5775316
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/133742
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins
diff --git a/sw/inc/formatcontentcontrol.hxx b/sw/inc/formatcontentcontrol.hxx
index 8419446..0cd65b6 100644
--- a/sw/inc/formatcontentcontrol.hxx
+++ b/sw/inc/formatcontentcontrol.hxx
@@ -20,6 +20,7 @@
#pragma once

#include <com/sun/star/text/XTextContent.hpp>
#include <com/sun/star/beans/PropertyValue.hpp>

#include <cppuhelper/weakref.hxx>
#include <sal/types.h>
@@ -36,6 +37,7 @@ enum class SwContentControlType
{
    RICH_TEXT,
    CHECKBOX,
    DROP_DOWN_LIST,
};

/// SfxPoolItem subclass that wraps an SwContentControl.
@@ -73,6 +75,21 @@ public:
    void dumpAsXml(xmlTextWriterPtr pWriter) const override;
};

/// Represents one list item in a content control dropdown list.
class SwContentControlListItem
{
public:
    OUString m_aDisplayText;
    OUString m_aValue;

    void dumpAsXml(xmlTextWriterPtr pWriter) const;

    static void ItemsToAny(const std::vector<SwContentControlListItem>& rItems,
                           css::uno::Any& rVal);

    static std::vector<SwContentControlListItem> ItemsFromAny(const css::uno::Any& rVal);
};

/// Stores the properties of a content control.
class SAL_DLLPUBLIC_RTTI SwContentControl : public sw::BroadcastingModify
{
@@ -98,6 +115,8 @@ class SAL_DLLPUBLIC_RTTI SwContentControl : public sw::BroadcastingModify
    /// If m_bCheckbox is true, the value of an unchecked checkbox.
    OUString m_aUncheckedState;

    std::vector<SwContentControlListItem> m_aListItems;

public:
    SwTextContentControl* GetTextAttr() const;

@@ -148,6 +167,13 @@ public:

    OUString GetUncheckedState() const { return m_aUncheckedState; }

    std::vector<SwContentControlListItem> GetListItems() const { return m_aListItems; }

    void SetListItems(const std::vector<SwContentControlListItem>& rListItems)
    {
        m_aListItems = rListItems;
    }

    virtual void dumpAsXml(xmlTextWriterPtr pWriter) const;
};

diff --git a/sw/inc/unoprnms.hxx b/sw/inc/unoprnms.hxx
index bfc3a28..1874919 100644
--- a/sw/inc/unoprnms.hxx
+++ b/sw/inc/unoprnms.hxx
@@ -876,6 +876,7 @@
#define UNO_NAME_CHECKED "Checked"
#define UNO_NAME_CHECKED_STATE "CheckedState"
#define UNO_NAME_UNCHECKED_STATE "UncheckedState"
#define UNO_NAME_LIST_ITEMS "ListItems"
#endif

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/qa/core/unocore/unocore.cxx b/sw/qa/core/unocore/unocore.cxx
index 0f1b6a5..f941c60 100644
--- a/sw/qa/core/unocore/unocore.cxx
+++ b/sw/qa/core/unocore/unocore.cxx
@@ -456,6 +456,57 @@ CPPUNIT_TEST_FIXTURE(SwCoreUnocoreTest, testContentControlCheckbox)
    CPPUNIT_ASSERT_EQUAL(OUString(u"☐"), pContentControl->GetUncheckedState());
}

CPPUNIT_TEST_FIXTURE(SwCoreUnocoreTest, testContentControlDropdown)
{
    // Given an empty document:
    SwDoc* pDoc = createSwDoc();

    // When inserting a dropdown content control:
    uno::Reference<lang::XMultiServiceFactory> xMSF(mxComponent, uno::UNO_QUERY);
    uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY);
    uno::Reference<text::XText> xText = xTextDocument->getText();
    uno::Reference<text::XTextCursor> xCursor = xText->createTextCursor();
    xText->insertString(xCursor, "test", /*bAbsorb=*/false);
    xCursor->gotoStart(/*bExpand=*/false);
    xCursor->gotoEnd(/*bExpand=*/true);
    uno::Reference<text::XTextContent> xContentControl(
        xMSF->createInstance("com.sun.star.text.ContentControl"), uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xContentControlProps(xContentControl, uno::UNO_QUERY);
    {
        uno::Sequence<beans::PropertyValues> aListItems = {
            {
                comphelper::makePropertyValue("DisplayText", uno::makeAny(OUString("red"))),
                comphelper::makePropertyValue("Value", uno::makeAny(OUString("R"))),
            },
            {
                comphelper::makePropertyValue("DisplayText", uno::makeAny(OUString("green"))),
                comphelper::makePropertyValue("Value", uno::makeAny(OUString("G"))),
            },
            {
                comphelper::makePropertyValue("DisplayText", uno::makeAny(OUString("blue"))),
                comphelper::makePropertyValue("Value", uno::makeAny(OUString("B"))),
            },
        };
        // Without the accompanying fix in place, this test would have failed with:
        // An uncaught exception of type com.sun.star.beans.UnknownPropertyException
        xContentControlProps->setPropertyValue("ListItems", uno::makeAny(aListItems));
    }
    xText->insertTextContent(xCursor, xContentControl, /*bAbsorb=*/true);

    // Then make sure that the specified properties are set:
    SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell();
    SwTextNode* pTextNode = pWrtShell->GetCursor()->GetNode().GetTextNode();
    SwTextAttr* pAttr = pTextNode->GetTextAttrForCharAt(0, RES_TXTATR_CONTENTCONTROL);
    auto pTextContentControl = static_txtattr_cast<SwTextContentControl*>(pAttr);
    auto& rFormatContentControl
        = static_cast<SwFormatContentControl&>(pTextContentControl->GetAttr());
    SwContentControl* pContentControl = rFormatContentControl.GetContentControl();
    std::vector<SwContentControlListItem> aListItems = pContentControl->GetListItems();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), aListItems.size());
    CPPUNIT_ASSERT_EQUAL(OUString("red"), aListItems[0].m_aDisplayText);
    CPPUNIT_ASSERT_EQUAL(OUString("R"), aListItems[0].m_aValue);
}

CPPUNIT_PLUGIN_IMPLEMENT();

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/txtnode/attrcontentcontrol.cxx b/sw/source/core/txtnode/attrcontentcontrol.cxx
index e469bab..c28e686 100644
--- a/sw/source/core/txtnode/attrcontentcontrol.cxx
+++ b/sw/source/core/txtnode/attrcontentcontrol.cxx
@@ -22,6 +22,8 @@
#include <libxml/xmlwriter.h>

#include <sal/log.hxx>
#include <comphelper/propertyvalue.hxx>
#include <comphelper/sequenceashashmap.hxx>

#include <ndtxt.hxx>
#include <textcontentcontrol.hxx>
@@ -220,9 +222,76 @@ void SwContentControl::dumpAsXml(xmlTextWriterPtr pWriter) const
                                            BAD_CAST(m_aCheckedState.toUtf8().getStr()));
    (void)xmlTextWriterWriteFormatAttribute(pWriter, BAD_CAST("unchecked-state"), "%s",
                                            BAD_CAST(m_aUncheckedState.toUtf8().getStr()));

    if (!m_aListItems.empty())
    {
        for (const auto& rListItem : m_aListItems)
        {
            rListItem.dumpAsXml(pWriter);
        }
    }

    (void)xmlTextWriterEndElement(pWriter);
}

void SwContentControlListItem::dumpAsXml(xmlTextWriterPtr pWriter) const
{
    (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwContentControlListItem"));
    (void)xmlTextWriterWriteFormatAttribute(pWriter, BAD_CAST("ptr"), "%p", this);
    (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("display-text"),
                                      BAD_CAST(m_aDisplayText.toUtf8().getStr()));
    (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("value"),
                                      BAD_CAST(m_aValue.toUtf8().getStr()));

    (void)xmlTextWriterEndElement(pWriter);
}

void SwContentControlListItem::ItemsToAny(const std::vector<SwContentControlListItem>& rItems,
                                          uno::Any& rVal)
{
    uno::Sequence<uno::Sequence<beans::PropertyValue>> aRet(rItems.size());

    uno::Sequence<beans::PropertyValue>* pRet = aRet.getArray();
    for (size_t i = 0; i < rItems.size(); ++i)
    {
        const SwContentControlListItem& rItem = rItems[i];
        uno::Sequence<beans::PropertyValue> aItem = {
            comphelper::makePropertyValue("DisplayText", rItem.m_aDisplayText),
            comphelper::makePropertyValue("Value", rItem.m_aValue),
        };
        pRet[i] = aItem;
    }

    rVal <<= aRet;
}

std::vector<SwContentControlListItem>
SwContentControlListItem::ItemsFromAny(const css::uno::Any& rVal)
{
    std::vector<SwContentControlListItem> aRet;

    uno::Sequence<uno::Sequence<beans::PropertyValue>> aSequence;
    rVal >>= aSequence;
    for (const auto& rItem : aSequence)
    {
        comphelper::SequenceAsHashMap aMap(rItem);
        SwContentControlListItem aItem;
        auto it = aMap.find("DisplayText");
        if (it != aMap.end())
        {
            it->second >>= aItem.m_aDisplayText;
        }
        it = aMap.find("Value");
        if (it != aMap.end())
        {
            it->second >>= aItem.m_aValue;
        }
        aRet.push_back(aItem);
    }

    return aRet;
}

SwTextContentControl* SwTextContentControl::CreateTextContentControl(SwTextNode* pTargetTextNode,
                                                                     SwFormatContentControl& rAttr,
                                                                     sal_Int32 nStart,
diff --git a/sw/source/core/unocore/unocontentcontrol.cxx b/sw/source/core/unocore/unocontentcontrol.cxx
index 81ccc9f..19e5e60 100644
--- a/sw/source/core/unocore/unocontentcontrol.cxx
+++ b/sw/source/core/unocore/unocontentcontrol.cxx
@@ -160,6 +160,7 @@ public:
    bool m_bChecked;
    OUString m_aCheckedState;
    OUString m_aUncheckedState;
    std::vector<SwContentControlListItem> m_aListItems;

    Impl(SwXContentControl& rThis, SwDoc& rDoc, SwContentControl* pContentControl,
         const uno::Reference<text::XText>& xParentText,
@@ -516,6 +517,7 @@ void SwXContentControl::AttachImpl(const uno::Reference<text::XTextRange>& xText
    pContentControl->SetChecked(m_pImpl->m_bChecked);
    pContentControl->SetCheckedState(m_pImpl->m_aCheckedState);
    pContentControl->SetUncheckedState(m_pImpl->m_aUncheckedState);
    pContentControl->SetListItems(m_pImpl->m_aListItems);

    SwFormatContentControl aContentControl(pContentControl, nWhich);
    bool bSuccess
@@ -524,7 +526,7 @@ void SwXContentControl::AttachImpl(const uno::Reference<text::XTextRange>& xText
    if (!bSuccess)
    {
        throw lang::IllegalArgumentException(
            "SwXContentControl::AttachImpl(): cannot create meta: range invalid?",
            "SwXContentControl::AttachImpl(): cannot create content control: invalid range",
            static_cast<::cppu::OWeakObject*>(this), 1);
    }
    if (!pTextAttr)
@@ -742,6 +744,19 @@ void SAL_CALL SwXContentControl::setPropertyValue(const OUString& rPropertyName,
            }
        }
    }
    else if (rPropertyName == UNO_NAME_LIST_ITEMS)
    {
        std::vector<SwContentControlListItem> aItems
            = SwContentControlListItem::ItemsFromAny(rValue);
        if (m_pImpl->m_bIsDescriptor)
        {
            m_pImpl->m_aListItems = aItems;
        }
        else
        {
            m_pImpl->m_pContentControl->SetListItems(aItems);
        }
    }
    else
    {
        throw beans::UnknownPropertyException();
@@ -808,6 +823,19 @@ uno::Any SAL_CALL SwXContentControl::getPropertyValue(const OUString& rPropertyN
            aRet <<= m_pImpl->m_pContentControl->GetUncheckedState();
        }
    }
    else if (rPropertyName == UNO_NAME_LIST_ITEMS)
    {
        std::vector<SwContentControlListItem> aItems;
        if (m_pImpl->m_bIsDescriptor)
        {
            aItems = m_pImpl->m_aListItems;
        }
        else
        {
            aItems = m_pImpl->m_pContentControl->GetListItems();
        }
        SwContentControlListItem::ItemsToAny(aItems, aRet);
    }
    else
    {
        throw beans::UnknownPropertyException();
diff --git a/sw/source/core/unocore/unomap1.cxx b/sw/source/core/unocore/unomap1.cxx
index 3da6c80..b580ed2 100644
--- a/sw/source/core/unocore/unomap1.cxx
+++ b/sw/source/core/unocore/unomap1.cxx
@@ -1027,6 +1027,7 @@ const SfxItemPropertyMapEntry* SwUnoPropertyMapProvider::GetContentControlProper
        { u"" UNO_NAME_CHECKED, 0, cppu::UnoType<bool>::get(), PROPERTY_NONE, 0 },
        { u"" UNO_NAME_CHECKED_STATE, 0, cppu::UnoType<OUString>::get(), PROPERTY_NONE, 0 },
        { u"" UNO_NAME_UNCHECKED_STATE, 0, cppu::UnoType<OUString>::get(), PROPERTY_NONE, 0 },
        { u"" UNO_NAME_LIST_ITEMS, 0, cppu::UnoType<uno::Sequence<uno::Sequence<beans::PropertyValue>>>::get(), PROPERTY_NONE, 0 },
        { u"", 0, css::uno::Type(), 0, 0 }
    };

diff --git a/sw/source/uibase/wrtsh/wrtsh1.cxx b/sw/source/uibase/wrtsh/wrtsh1.cxx
index a0cd400..32d515d 100644
--- a/sw/source/uibase/wrtsh/wrtsh1.cxx
+++ b/sw/source/uibase/wrtsh/wrtsh1.cxx
@@ -1030,6 +1030,7 @@ void SwWrtShell::InsertContentControl(SwContentControlType eType)
    switch (eType)
    {
        case SwContentControlType::RICH_TEXT:
        case SwContentControlType::DROP_DOWN_LIST:
        {
            pContentControl->SetShowingPlaceHolder(true);
            if (!HasSelection())