sw/Library_sw.mk                                        |    2 
 sw/inc/formatcontentcontrol.hxx                         |  104 ++
 sw/inc/hintids.hxx                                      |   17 
 sw/inc/textcontentcontrol.hxx                           |   41 
 sw/inc/unobaseclass.hxx                                 |    1 
 sw/inc/unocoll.hxx                                      |    1 
 sw/inc/unotextcursor.hxx                                |    1 
 sw/qa/core/unocore/unocore.cxx                          |   28 
 sw/source/core/bastyp/init.cxx                          |    5 
 sw/source/core/doc/DocumentContentOperationsManager.cxx |    2 
 sw/source/core/doc/dbgoutsw.cxx                         |    2 
 sw/source/core/inc/unocontentcontrol.hxx                |  139 +++
 sw/source/core/txtnode/attrcontentcontrol.cxx           |  237 +++++
 sw/source/core/txtnode/thints.cxx                       |   32 
 sw/source/core/txtnode/txatbase.cxx                     |    2 
 sw/source/core/unocore/unocoll.cxx                      |    7 
 sw/source/core/unocore/unocontentcontrol.cxx            |  686 ++++++++++++++++
 sw/source/core/unocore/unoobj.cxx                       |   37 
 sw/source/core/unocore/unotext.cxx                      |    4 
 sw/source/filter/html/css1atr.cxx                       |    2 
 sw/source/filter/html/htmlatr.cxx                       |    2 
 21 files changed, 1332 insertions(+), 20 deletions(-)

New commits:
commit 67da73d0cc0dfb2822b6bcffb05f71b452f72605
Author:     Miklos Vajna <[email protected]>
AuthorDate: Thu Mar 31 09:43:24 2022 +0200
Commit:     Miklos Vajna <[email protected]>
CommitDate: Thu Apr 21 08:07:30 2022 +0200

    sw content controls: add UNO API to insert this
    
    This only allows the default richText type, the others are not yet
    handled.
    
    (cherry picked from commit c4268efef25129b162884312b15527f1e5c3bcb0)
    
    Conflicts:
            sw/qa/core/unocore/unocore.cxx
    
    Change-Id: I39f73ccd9e2c1f0db5735cf97301b01482063d1d
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/132961
    Tested-by: Jenkins CollaboraOffice <[email protected]>
    Reviewed-by: Miklos Vajna <[email protected]>

diff --git a/sw/Library_sw.mk b/sw/Library_sw.mk
index be6fae7446a1..28db1cc7a94b 100644
--- a/sw/Library_sw.mk
+++ b/sw/Library_sw.mk
@@ -474,6 +474,7 @@ $(eval $(call gb_Library_add_exception_objects,sw,\
     sw/source/core/unocore/unobkm \
     sw/source/core/unocore/unochart \
     sw/source/core/unocore/unocoll \
+    sw/source/core/unocore/unocontentcontrol \
     sw/source/core/unocore/unocrsr \
     sw/source/core/unocore/unocrsrhelper \
     sw/source/core/unocore/unodraw \
diff --git a/sw/inc/unobaseclass.hxx b/sw/inc/unobaseclass.hxx
index 0dc56aad2886..6d76bf5dbbac 100644
--- a/sw/inc/unobaseclass.hxx
+++ b/sw/inc/unobaseclass.hxx
@@ -52,6 +52,7 @@ enum class CursorType
                         // a text range or cursor
     SelectionInTable,
     Meta,         // meta/meta-field
+    ContentControl,
 };
 
 /*
diff --git a/sw/inc/unocoll.hxx b/sw/inc/unocoll.hxx
index 821d50ab4a2c..8d883a8223cb 100644
--- a/sw/inc/unocoll.hxx
+++ b/sw/inc/unocoll.hxx
@@ -177,6 +177,7 @@ enum class SwServiceType {
     StyleTable                      = 114,
     StyleCell                       = 115,
     LineBreak                       = 116,
+    ContentControl                  = 117,
 
     Invalid                         = USHRT_MAX
 };
diff --git a/sw/inc/unotextcursor.hxx b/sw/inc/unotextcursor.hxx
index e6c33b2aad8f..b055f2d64504 100644
--- a/sw/inc/unotextcursor.hxx
+++ b/sw/inc/unotextcursor.hxx
@@ -100,6 +100,7 @@ public:
 
     SwUnoCursor& GetCursor();
     bool IsAtEndOfMeta() const;
+    bool IsAtEndOfContentControl() const;
 
     void DeleteAndInsert(OUString const& rText,
                 const bool bForceExpandHints);
diff --git a/sw/qa/core/unocore/unocore.cxx b/sw/qa/core/unocore/unocore.cxx
index d971e57d77e3..e98b8d2838b1 100644
--- a/sw/qa/core/unocore/unocore.cxx
+++ b/sw/qa/core/unocore/unocore.cxx
@@ -25,6 +25,7 @@
 #include <view.hxx>
 #include <ndtxt.hxx>
 #include <textlinebreak.hxx>
+#include <textcontentcontrol.hxx>
 
 using namespace ::com::sun::star;
 
@@ -281,6 +282,33 @@ CPPUNIT_TEST_FIXTURE(SwCoreUnocoreTest, 
testLineBreakTextPortionEnum)
     CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int16>(SwLineBreakClear::ALL), 
eClear);
 }
 
+CPPUNIT_TEST_FIXTURE(SwCoreUnocoreTest, testContentControlInsert)
+{
+    // Given an empty document:
+    SwDoc* pDoc = createSwDoc();
+
+    // When inserting a content control around one or more text portions:
+    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);
+    xText->insertTextContent(xCursor, xContentControl, /*bAbsorb=*/true);
+
+    // Then make sure that the text attribute is inserted:
+    SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell();
+    SwNodeOffset nIndex = pWrtShell->GetCursor()->GetNode().GetIndex();
+    SwTextNode* pTextNode = pDoc->GetNodes()[nIndex]->GetTextNode();
+    SwTextAttr* pAttr = pTextNode->GetTextAttrForCharAt(0, 
RES_TXTATR_CONTENTCONTROL);
+    // Without the accompanying fix in place, this test would have failed, as 
the
+    // SwXContentControl::attach() implementation was missing.
+    CPPUNIT_ASSERT(pAttr);
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/inc/unocontentcontrol.hxx 
b/sw/source/core/inc/unocontentcontrol.hxx
new file mode 100644
index 000000000000..7f90f2ec18e7
--- /dev/null
+++ b/sw/source/core/inc/unocontentcontrol.hxx
@@ -0,0 +1,139 @@
+/* -*- 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/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ *   Licensed to the Apache Software Foundation (ASF) under one or more
+ *   contributor license agreements. See the NOTICE file distributed
+ *   with this work for additional information regarding copyright
+ *   ownership. The ASF licenses this file to you under the Apache
+ *   License, Version 2.0 (the "License"); you may not use this file
+ *   except in compliance with the License. You may obtain a copy of
+ *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#pragma once
+
+#include <memory>
+#include <deque>
+
+#include <com/sun/star/lang/XServiceInfo.hpp>
+#include <com/sun/star/lang/XUnoTunnel.hpp>
+#include <com/sun/star/beans/XPropertySet.hpp>
+#include <com/sun/star/container/XChild.hpp>
+#include <com/sun/star/container/XEnumerationAccess.hpp>
+#include <com/sun/star/text/XTextContent.hpp>
+#include <com/sun/star/text/XTextField.hpp>
+
+#include <cppuhelper/implbase.hxx>
+
+#include <unobaseclass.hxx>
+
+typedef std::deque<css::uno::Reference<css::text::XTextRange>> TextRangeList_t;
+
+class SwPaM;
+class SwTextNode;
+class SwContentControl;
+
+/**
+ * UNO API wrapper around an SwContentControl, exposed as the 
com.sun.star.text.ContentControl
+ * service.
+ */
+class SwXContentControl
+    : public cppu::WeakImplHelper<css::lang::XUnoTunnel, 
css::lang::XServiceInfo,
+                                  css::container::XEnumerationAccess, 
css::text::XTextContent,
+                                  css::text::XText>
+{
+public:
+    class Impl;
+
+protected:
+    sw::UnoImplPtr<Impl> m_pImpl;
+
+    void AttachImpl(const css::uno::Reference<css::text::XTextRange>& 
xTextRange,
+                    sal_uInt16 nWhich);
+
+    ~SwXContentControl() override;
+
+    SwXContentControl(const SwXContentControl&) = delete;
+    SwXContentControl& operator=(const SwXContentControl&) = delete;
+
+    SwXContentControl(SwDoc* pDoc, SwContentControl* pContentControl,
+                      const css::uno::Reference<css::text::XText>& xParentText,
+                      std::unique_ptr<const TextRangeList_t> pPortions);
+
+    SwXContentControl(SwDoc* pDoc);
+
+public:
+    static css::uno::Reference<css::text::XTextContent>
+    CreateXContentControl(SwContentControl& rContentControl,
+                          const css::uno::Reference<css::text::XText>& 
xParentText = nullptr,
+                          std::unique_ptr<const TextRangeList_t>&& pPortions
+                          = std::unique_ptr<const TextRangeList_t>());
+
+    static css::uno::Reference<css::text::XTextContent> 
CreateXContentControl(SwDoc& rDoc);
+
+    /// Initializes params with position of the attribute content (without 
CH_TXTATR).
+    bool SetContentRange(SwTextNode*& rpNode, sal_Int32& rStart, sal_Int32& 
rEnd) const;
+    css::uno::Reference<css::text::XText> GetParentText() const;
+
+    bool CheckForOwnMemberContentControl(const SwPaM& rPam, const bool 
bAbsorb);
+
+    static const css::uno::Sequence<sal_Int8>& getUnoTunnelId();
+
+    // XUnoTunnel
+    sal_Int64 SAL_CALL getSomething(const css::uno::Sequence<sal_Int8>& 
Identifier) override;
+
+    // XServiceInfo
+    OUString SAL_CALL getImplementationName() override;
+    sal_Bool SAL_CALL supportsService(const OUString& rServiceName) override;
+    css::uno::Sequence<OUString> SAL_CALL getSupportedServiceNames() override;
+
+    // XComponent
+    void SAL_CALL dispose() override;
+    void SAL_CALL
+    addEventListener(const css::uno::Reference<css::lang::XEventListener>& 
xListener) override;
+    void SAL_CALL
+    removeEventListener(const css::uno::Reference<css::lang::XEventListener>& 
xListener) override;
+
+    // XElementAccess
+    css::uno::Type SAL_CALL getElementType() override;
+    sal_Bool SAL_CALL hasElements() override;
+
+    // XEnumerationAccess
+    css::uno::Reference<css::container::XEnumeration> SAL_CALL 
createEnumeration() override;
+
+    // XTextContent
+    void SAL_CALL attach(const css::uno::Reference<css::text::XTextRange>& 
xTextRange) override;
+    css::uno::Reference<css::text::XTextRange> SAL_CALL getAnchor() override;
+
+    // XTextRange
+    css::uno::Reference<css::text::XText> SAL_CALL getText() override;
+    css::uno::Reference<css::text::XTextRange> SAL_CALL getStart() override;
+    css::uno::Reference<css::text::XTextRange> SAL_CALL getEnd() override;
+    OUString SAL_CALL getString() override;
+    void SAL_CALL setString(const OUString& rString) override;
+
+    // XSimpleText
+    css::uno::Reference<css::text::XTextCursor> SAL_CALL createTextCursor() 
override;
+    css::uno::Reference<css::text::XTextCursor> SAL_CALL 
createTextCursorByRange(
+        const css::uno::Reference<css::text::XTextRange>& xTextPosition) 
override;
+    void SAL_CALL insertString(const 
css::uno::Reference<css::text::XTextRange>& xRange,
+                               const OUString& aString, sal_Bool bAbsorb) 
override;
+    void SAL_CALL insertControlCharacter(const 
css::uno::Reference<css::text::XTextRange>& xRange,
+                                         sal_Int16 nControlCharacter, sal_Bool 
bAbsorb) override;
+
+    // XText
+    void SAL_CALL insertTextContent(const 
css::uno::Reference<css::text::XTextRange>& xRange,
+                                    const 
css::uno::Reference<css::text::XTextContent>& xContent,
+                                    sal_Bool bAbsorb) override;
+    void SAL_CALL
+    removeTextContent(const css::uno::Reference<css::text::XTextContent>& 
xContent) override;
+};
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/unocore/unocoll.cxx 
b/sw/source/core/unocore/unocoll.cxx
index fd10e33424f2..044e5acc99cb 100644
--- a/sw/source/core/unocore/unocoll.cxx
+++ b/sw/source/core/unocore/unocoll.cxx
@@ -56,6 +56,7 @@
 #include <unobookmark.hxx>
 #include <unorefmark.hxx>
 #include <unometa.hxx>
+#include <unocontentcontrol.hxx>
 #include <docsh.hxx>
 #include <hints.hxx>
 #include <frameformats.hxx>
@@ -455,7 +456,8 @@ const ProvNamesId_Type aProvNamesId[] =
     { CSS_TEXT_FIELDMASTER_BIBLIOGRAPHY,              
SwServiceType::FieldMasterBibliography },
     { "com.sun.star.style.TableStyle",                
SwServiceType::StyleTable },
     { "com.sun.star.style.CellStyle",                 SwServiceType::StyleCell 
},
-    { "com.sun.star.text.LineBreak",                  SwServiceType::LineBreak 
}
+    { "com.sun.star.text.LineBreak",                  SwServiceType::LineBreak 
},
+    { "com.sun.star.text.ContentControl",             
SwServiceType::ContentControl }
 };
 
 const SvEventDescription* sw_GetSupportedMacroItems()
@@ -828,6 +830,9 @@ SwXServiceProvider::MakeInstance(SwServiceType nObjectType, 
SwDoc & rDoc)
         case SwServiceType::LineBreak:
             xRet = SwXLineBreak::CreateXLineBreak(nullptr);
             break;
+        case SwServiceType::ContentControl:
+            xRet = SwXContentControl::CreateXContentControl(rDoc);
+        break;
         default:
             throw uno::RuntimeException();
     }
diff --git a/sw/source/core/unocore/unocontentcontrol.cxx 
b/sw/source/core/unocore/unocontentcontrol.cxx
new file mode 100644
index 000000000000..535762feae58
--- /dev/null
+++ b/sw/source/core/unocore/unocontentcontrol.cxx
@@ -0,0 +1,686 @@
+/* -*- 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/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ *   Licensed to the Apache Software Foundation (ASF) under one or more
+ *   contributor license agreements. See the NOTICE file distributed
+ *   with this work for additional information regarding copyright
+ *   ownership. The ASF licenses this file to you under the Apache
+ *   License, Version 2.0 (the "License"); you may not use this file
+ *   except in compliance with the License. You may obtain a copy of
+ *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <unocontentcontrol.hxx>
+
+#include <mutex>
+
+#include <com/sun/star/text/XWordCursor.hpp>
+
+#include <comphelper/interfacecontainer4.hxx>
+#include <comphelper/servicehelper.hxx>
+#include <cppuhelper/supportsservice.hxx>
+
+#include <formatcontentcontrol.hxx>
+#include <ndtxt.hxx>
+#include <textcontentcontrol.hxx>
+#include <unotext.hxx>
+#include <unotextcursor.hxx>
+#include <unotextrange.hxx>
+#include <doc.hxx>
+#include <unoport.hxx>
+
+using namespace com::sun::star;
+
+namespace
+{
+/// UNO API wrapper around the text inside an SwXContentControl.
+class SwXContentControlText : public cppu::OWeakObject, public SwXText
+{
+private:
+    SwXContentControl& m_rContentControl;
+
+    void PrepareForAttach(uno::Reference<text::XTextRange>& xRange, const 
SwPaM& rPam) override;
+
+protected:
+    const SwStartNode* GetStartNode() const override;
+    uno::Reference<text::XTextCursor> CreateCursor() override;
+
+public:
+    SwXContentControlText(SwDoc& rDoc, SwXContentControl& rContentControl);
+
+    /// SwXText::Invalidate() is protected.
+    using SwXText::Invalidate;
+
+    // XInterface
+    void SAL_CALL acquire() noexcept override { cppu::OWeakObject::acquire(); }
+    void SAL_CALL release() noexcept override { cppu::OWeakObject::release(); }
+
+    // XTypeProvider
+    uno::Sequence<sal_Int8> SAL_CALL getImplementationId() override;
+
+    // XText
+    uno::Reference<text::XTextCursor> SAL_CALL createTextCursor() override;
+    uno::Reference<text::XTextCursor> SAL_CALL
+    createTextCursorByRange(const uno::Reference<text::XTextRange>& 
xTextPosition) override;
+};
+}
+
+SwXContentControlText::SwXContentControlText(SwDoc& rDoc, SwXContentControl& 
rContentControl)
+    : SwXText(&rDoc, CursorType::ContentControl)
+    , m_rContentControl(rContentControl)
+{
+}
+
+const SwStartNode* SwXContentControlText::GetStartNode() const
+{
+    auto pParent = 
dynamic_cast<SwXText*>(m_rContentControl.GetParentText().get());
+    return pParent ? pParent->GetStartNode() : nullptr;
+}
+
+void SwXContentControlText::PrepareForAttach(uno::Reference<text::XTextRange>& 
xRange,
+                                             const SwPaM& rPam)
+{
+    // Create a new cursor to prevent modifying SwXTextRange.
+    xRange = static_cast<text::XWordCursor*>(
+        new SwXTextCursor(*GetDoc(), &m_rContentControl, 
CursorType::ContentControl,
+                          *rPam.GetPoint(), (rPam.HasMark()) ? rPam.GetMark() 
: nullptr));
+}
+
+uno::Reference<text::XTextCursor> SwXContentControlText::CreateCursor()
+{
+    uno::Reference<text::XTextCursor> xRet;
+    if (IsValid())
+    {
+        SwTextNode* pTextNode;
+        sal_Int32 nContentControlStart;
+        sal_Int32 nContentControlEnd;
+        bool bSuccess = m_rContentControl.SetContentRange(pTextNode, 
nContentControlStart,
+                                                          nContentControlEnd);
+        if (bSuccess)
+        {
+            SwPosition aPos(*pTextNode, nContentControlStart);
+            xRet = static_cast<text::XWordCursor*>(
+                new SwXTextCursor(*GetDoc(), &m_rContentControl, 
CursorType::ContentControl, aPos));
+        }
+    }
+    return xRet;
+}
+
+uno::Sequence<sal_Int8> SAL_CALL SwXContentControlText::getImplementationId()
+{
+    return css::uno::Sequence<sal_Int8>();
+}
+
+// XText
+uno::Reference<text::XTextCursor> SAL_CALL 
SwXContentControlText::createTextCursor()
+{
+    return CreateCursor();
+}
+
+uno::Reference<text::XTextCursor> SAL_CALL 
SwXContentControlText::createTextCursorByRange(
+    const uno::Reference<text::XTextRange>& xTextPosition)
+{
+    const uno::Reference<text::XTextCursor> xCursor(CreateCursor());
+    xCursor->gotoRange(xTextPosition, false);
+    return xCursor;
+}
+
+/**
+ * The inner part SwXContentControl, which is deleted with a locked SolarMutex.
+ *
+ * The content control has a cached list of text portions for its contents.  
This list is created by
+ * SwXTextPortionEnumeration.  The content control listens at the SwTextNode 
and throws away the
+ * cache when the text node changes.
+ */
+class SwXContentControl::Impl : public SvtListener
+{
+public:
+    uno::WeakReference<uno::XInterface> m_wThis;
+    // Just for OInterfaceContainerHelper4.
+    std::mutex m_Mutex;
+    ::comphelper::OInterfaceContainerHelper4<css::lang::XEventListener> 
m_EventListeners;
+    std::unique_ptr<const TextRangeList_t> m_pTextPortions;
+    // 3 possible states: not attached, attached, disposed
+    bool m_bIsDisposed;
+    bool m_bIsDescriptor;
+    uno::Reference<text::XText> m_xParentText;
+    rtl::Reference<SwXContentControlText> m_xText;
+    SwContentControl* m_pContentControl;
+
+    Impl(SwXContentControl& rThis, SwDoc& rDoc, SwContentControl* 
pContentControl,
+         const uno::Reference<text::XText>& xParentText,
+         std::unique_ptr<const TextRangeList_t> pPortions)
+        : m_pTextPortions(std::move(pPortions))
+        , m_bIsDisposed(false)
+        , m_bIsDescriptor(pContentControl == nullptr)
+        , m_xParentText(xParentText)
+        , m_xText(new SwXContentControlText(rDoc, rThis))
+        , m_pContentControl(pContentControl)
+    {
+        if (m_pContentControl)
+        {
+            StartListening(m_pContentControl->GetNotifier());
+        }
+    }
+
+    const SwContentControl* GetContentControl() const;
+
+protected:
+    void Notify(const SfxHint& rHint) override;
+};
+
+const SwContentControl* SwXContentControl::Impl::GetContentControl() const
+{
+    return m_pContentControl;
+}
+
+// sw::BroadcastingModify
+void SwXContentControl::Impl::Notify(const SfxHint& rHint)
+{
+    // throw away cache (SwTextNode changed)
+    m_pTextPortions.reset();
+
+    if (rHint.GetId() != SfxHintId::Dying && rHint.GetId() != 
SfxHintId::Deinitializing)
+        return;
+
+    m_bIsDisposed = true;
+    m_pContentControl = nullptr;
+    m_xText->Invalidate();
+    uno::Reference<uno::XInterface> xThis(m_wThis);
+    if (!xThis.is())
+    {
+        // If UNO object is already dead, don't refer to it in an event.
+        return;
+    }
+    lang::EventObject aEvent(xThis);
+    std::unique_lock aGuard(m_Mutex);
+    m_EventListeners.disposeAndClear(aGuard, aEvent);
+}
+
+uno::Reference<text::XText> SwXContentControl::GetParentText() const
+{
+    return m_pImpl->m_xParentText;
+}
+
+SwXContentControl::SwXContentControl(SwDoc* pDoc, SwContentControl* 
pContentControl,
+                                     const uno::Reference<text::XText>& 
xParentText,
+                                     std::unique_ptr<const TextRangeList_t> 
pPortions)
+    : m_pImpl(new SwXContentControl::Impl(*this, *pDoc, pContentControl, 
xParentText,
+                                          std::move(pPortions)))
+{
+}
+
+SwXContentControl::SwXContentControl(SwDoc* pDoc)
+    : m_pImpl(new SwXContentControl::Impl(*this, *pDoc, nullptr, nullptr, 
nullptr))
+{
+}
+
+SwXContentControl::~SwXContentControl() {}
+
+uno::Reference<text::XTextContent> 
SwXContentControl::CreateXContentControl(SwDoc& rDoc)
+{
+    rtl::Reference<SwXContentControl> xContentControl(new 
SwXContentControl(&rDoc));
+    uno::Reference<text::XTextContent> xTextContent(xContentControl);
+    xContentControl->m_pImpl->m_wThis = xTextContent;
+    return xContentControl;
+}
+
+uno::Reference<text::XTextContent>
+SwXContentControl::CreateXContentControl(SwContentControl& rContentControl,
+                                         const uno::Reference<text::XText>& 
xParent,
+                                         std::unique_ptr<const 
TextRangeList_t>&& pPortions)
+{
+    // re-use existing SwXContentControl
+    uno::Reference<text::XTextContent> 
xContentControl(rContentControl.GetXContentControl());
+    if (xContentControl.is())
+    {
+        if (pPortions)
+        {
+            // Set the cache in the XContentControl to the given portions.
+            auto pXContentControl
+                = 
comphelper::getFromUnoTunnel<SwXContentControl>(xContentControl);
+            assert(pXContentControl);
+            // The content control must always be created with the complete 
content.  If
+            // SwXTextPortionEnumeration is created for a selection, it must 
be checked that the
+            // content control is contained in the selection.
+            pXContentControl->m_pImpl->m_pTextPortions = std::move(pPortions);
+            if (pXContentControl->m_pImpl->m_xParentText.get() != 
xParent.get())
+            {
+                SAL_WARN("sw.uno", "SwXContentControl with different parent");
+                pXContentControl->m_pImpl->m_xParentText.set(xParent);
+            }
+        }
+        return xContentControl;
+    }
+
+    // Create new SwXContentControl.
+    SwTextNode* pTextNode = rContentControl.GetTextNode();
+    if (!pTextNode)
+    {
+        SAL_WARN("sw.uno", "CreateXContentControl: no text node");
+        return nullptr;
+    }
+    uno::Reference<text::XText> xParentText(xParent);
+    if (!xParentText.is())
+    {
+        SwTextContentControl* pTextAttr = rContentControl.GetTextAttr();
+        if (!pTextAttr)
+        {
+            SAL_WARN("sw.uno", "CreateXContentControl: no text attr");
+            return nullptr;
+        }
+        SwPosition aPos(*pTextNode, pTextAttr->GetStart());
+        xParentText.set(sw::CreateParentXText(pTextNode->GetDoc(), aPos));
+    }
+    if (!xParentText.is())
+    {
+        return nullptr;
+    }
+    rtl::Reference<SwXContentControl> pXContentControl = new SwXContentControl(
+        &pTextNode->GetDoc(), &rContentControl, xParentText, 
std::move(pPortions));
+    xContentControl.set(pXContentControl);
+    rContentControl.SetXContentControl(xContentControl);
+    pXContentControl->m_pImpl->m_wThis = xContentControl;
+    return xContentControl;
+}
+
+bool SwXContentControl::SetContentRange(SwTextNode*& rpNode, sal_Int32& rStart,
+                                        sal_Int32& rEnd) const
+{
+    const SwContentControl* pContentControl = m_pImpl->GetContentControl();
+    if (pContentControl)
+    {
+        const SwTextContentControl* pTextAttr = pContentControl->GetTextAttr();
+        if (pTextAttr)
+        {
+            rpNode = pContentControl->GetTextNode();
+            if (rpNode)
+            {
+                // rStart points at the first position within the content 
control.
+                rStart = pTextAttr->GetStart() + 1;
+                rEnd = *pTextAttr->End();
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+bool SwXContentControl::CheckForOwnMemberContentControl(const SwPaM& rPam, 
bool bAbsorb)
+{
+    SwTextNode* pTextNode;
+    sal_Int32 nContentControlStart;
+    sal_Int32 nContentControlEnd;
+    bool bSuccess = SetContentRange(pTextNode, nContentControlStart, 
nContentControlEnd);
+    if (!bSuccess)
+    {
+        SAL_WARN("sw.core", 
"SwXContentControl::CheckForOwnMemberContentControl: no pam");
+        throw lang::DisposedException();
+    }
+
+    const SwPosition* pStartPos(rPam.Start());
+    if (&pStartPos->nNode.GetNode() != pTextNode)
+    {
+        throw lang::IllegalArgumentException(
+            "trying to insert into a nesting text content, but start "
+            "of text range not in same paragraph as text content",
+            nullptr, 0);
+    }
+    bool bForceExpandHints(false);
+    sal_Int32 nStartPos = pStartPos->nContent.GetIndex();
+    if ((nStartPos < nContentControlStart) || (nStartPos > nContentControlEnd))
+    {
+        throw lang::IllegalArgumentException(
+            "trying to insert into a nesting text content, but start "
+            "of text range not inside text content",
+            nullptr, 0);
+    }
+    else if (nStartPos == nContentControlEnd)
+    {
+        bForceExpandHints = true;
+    }
+    if (rPam.HasMark() && bAbsorb)
+    {
+        const SwPosition* pEndPos = rPam.End();
+        if (&pEndPos->nNode.GetNode() != pTextNode)
+        {
+            throw lang::IllegalArgumentException(
+                "trying to insert into a nesting text content, but end "
+                "of text range not in same paragraph as text content",
+                nullptr, 0);
+        }
+        sal_Int32 nEndPos = pEndPos->nContent.GetIndex();
+        if ((nEndPos < nContentControlStart) || (nEndPos > nContentControlEnd))
+        {
+            throw lang::IllegalArgumentException(
+                "trying to insert into a nesting text content, but end "
+                "of text range not inside text content",
+                nullptr, 0);
+        }
+        else if (nEndPos == nContentControlEnd)
+        {
+            bForceExpandHints = true;
+        }
+    }
+    return bForceExpandHints;
+}
+
+const uno::Sequence<sal_Int8>& SwXContentControl::getUnoTunnelId()
+{
+    static const comphelper::UnoIdInit theSwXContentControlUnoTunnelId;
+    return theSwXContentControlUnoTunnelId.getSeq();
+}
+
+// XUnoTunnel
+sal_Int64 SAL_CALL SwXContentControl::getSomething(const 
uno::Sequence<sal_Int8>& rId)
+{
+    return comphelper::getSomethingImpl<SwXContentControl>(rId, this);
+}
+
+// XServiceInfo
+OUString SAL_CALL SwXContentControl::getImplementationName() { return 
"SwXContentControl"; }
+
+sal_Bool SAL_CALL SwXContentControl::supportsService(const OUString& 
rServiceName)
+{
+    return cppu::supportsService(this, rServiceName);
+}
+
+uno::Sequence<OUString> SAL_CALL SwXContentControl::getSupportedServiceNames()
+{
+    return { "com.sun.star.text.TextContent", 
"com.sun.star.text.ContentControl" };
+}
+
+// XComponent
+void SAL_CALL
+SwXContentControl::addEventListener(const 
uno::Reference<lang::XEventListener>& xListener)
+{
+    m_pImpl->m_EventListeners.addInterface(xListener);
+}
+
+void SAL_CALL
+SwXContentControl::removeEventListener(const 
uno::Reference<lang::XEventListener>& xListener)
+{
+    m_pImpl->m_EventListeners.removeInterface(xListener);
+}
+
+void SAL_CALL SwXContentControl::dispose()
+{
+    SolarMutexGuard g;
+
+    if (m_pImpl->m_bIsDescriptor)
+    {
+        m_pImpl->m_pTextPortions.reset();
+        lang::EventObject aEvent(static_cast<::cppu::OWeakObject&>(*this));
+        std::unique_lock aGuard(m_pImpl->m_Mutex);
+        m_pImpl->m_EventListeners.disposeAndClear(aGuard, aEvent);
+        m_pImpl->m_bIsDisposed = true;
+        m_pImpl->m_xText->Invalidate();
+    }
+    else if (!m_pImpl->m_bIsDisposed)
+    {
+        SwTextNode* pTextNode;
+        sal_Int32 nContentControlStart;
+        sal_Int32 nContentControlEnd;
+        bool bSuccess = SetContentRange(pTextNode, nContentControlStart, 
nContentControlEnd);
+        if (!bSuccess)
+        {
+            SAL_WARN("sw.core", "SwXContentControl::dispose: no pam");
+        }
+        else
+        {
+            // -1 because of CH_TXTATR
+            SwPaM aPam(*pTextNode, nContentControlStart - 1, *pTextNode, 
nContentControlEnd);
+            SwDoc& rDoc(pTextNode->GetDoc());
+            rDoc.getIDocumentContentOperations().DeleteAndJoin(aPam);
+
+            // removal should call Modify and do the dispose
+            assert(m_pImpl->m_bIsDisposed);
+        }
+    }
+}
+
+void SwXContentControl::AttachImpl(const uno::Reference<text::XTextRange>& 
xTextRange,
+                                   sal_uInt16 nWhich)
+{
+    SolarMutexGuard aGuard;
+
+    if (m_pImpl->m_bIsDisposed)
+    {
+        throw lang::DisposedException();
+    }
+    if (!m_pImpl->m_bIsDescriptor)
+    {
+        throw uno::RuntimeException("SwXContentControl::AttachImpl(): already 
attached",
+                                    static_cast<::cppu::OWeakObject*>(this));
+    }
+
+    uno::Reference<lang::XUnoTunnel> xRangeTunnel(xTextRange, uno::UNO_QUERY);
+    if (!xRangeTunnel.is())
+    {
+        throw lang::IllegalArgumentException(
+            "SwXContentControl::AttachImpl(): argument is no XUnoTunnel",
+            static_cast<::cppu::OWeakObject*>(this), 0);
+    }
+    SwXTextRange* pRange = 
comphelper::getFromUnoTunnel<SwXTextRange>(xRangeTunnel);
+    OTextCursorHelper* pCursor
+        = pRange ? nullptr : 
comphelper::getFromUnoTunnel<OTextCursorHelper>(xRangeTunnel);
+    if (!pRange && !pCursor)
+    {
+        throw lang::IllegalArgumentException(
+            "SwXContentControl::AttachImpl(): argument not supported type",
+            static_cast<::cppu::OWeakObject*>(this), 0);
+    }
+
+    SwDoc* pDoc = pRange ? &pRange->GetDoc() : pCursor->GetDoc();
+    if (!pDoc)
+    {
+        throw lang::IllegalArgumentException(
+            "SwXContentControl::AttachImpl(): argument has no SwDoc",
+            static_cast<::cppu::OWeakObject*>(this), 0);
+    }
+
+    SwUnoInternalPaM aPam(*pDoc);
+    ::sw::XTextRangeToSwPaM(aPam, xTextRange);
+
+    UnoActionContext aContext(pDoc);
+
+    auto pTextCursor = dynamic_cast<SwXTextCursor*>(pCursor);
+    bool bForceExpandHints = pTextCursor && 
pTextCursor->IsAtEndOfContentControl();
+    SetAttrMode nInsertFlags = bForceExpandHints
+                                   ? (SetAttrMode::FORCEHINTEXPAND | 
SetAttrMode::DONTEXPAND)
+                                   : SetAttrMode::DONTEXPAND;
+
+    auto pContentControl = std::make_shared<SwContentControl>(nullptr);
+    SwFormatContentControl aContentControl(pContentControl, nWhich);
+    bool bSuccess
+        = pDoc->getIDocumentContentOperations().InsertPoolItem(aPam, 
aContentControl, nInsertFlags);
+    SwTextAttr* pTextAttr = pContentControl->GetTextAttr();
+    if (!bSuccess)
+    {
+        throw lang::IllegalArgumentException(
+            "SwXContentControl::AttachImpl(): cannot create meta: range 
invalid?",
+            static_cast<::cppu::OWeakObject*>(this), 1);
+    }
+    if (!pTextAttr)
+    {
+        SAL_WARN("sw.core", "content control inserted, but has no text 
attribute?");
+        throw uno::RuntimeException(
+            "SwXContentControl::AttachImpl(): cannot create content control",
+            static_cast<::cppu::OWeakObject*>(this));
+    }
+
+    m_pImpl->EndListeningAll();
+    m_pImpl->m_pContentControl = pContentControl.get();
+    m_pImpl->StartListening(pContentControl->GetNotifier());
+    
pContentControl->SetXContentControl(uno::Reference<text::XTextContent>(this));
+
+    m_pImpl->m_xParentText = sw::CreateParentXText(*pDoc, *aPam.GetPoint());
+
+    m_pImpl->m_bIsDescriptor = false;
+}
+
+// XTextContent
+void SAL_CALL SwXContentControl::attach(const 
uno::Reference<text::XTextRange>& xTextRange)
+{
+    return SwXContentControl::AttachImpl(xTextRange, 
RES_TXTATR_CONTENTCONTROL);
+}
+
+uno::Reference<text::XTextRange> SAL_CALL SwXContentControl::getAnchor()
+{
+    SolarMutexGuard g;
+
+    if (m_pImpl->m_bIsDisposed)
+    {
+        throw lang::DisposedException();
+    }
+    if (m_pImpl->m_bIsDescriptor)
+    {
+        throw uno::RuntimeException("SwXContentControl::getAnchor(): not 
inserted",
+                                    static_cast<::cppu::OWeakObject*>(this));
+    }
+
+    SwTextNode* pTextNode;
+    sal_Int32 nContentControlStart;
+    sal_Int32 nContentControlEnd;
+    bool bSuccess = SetContentRange(pTextNode, nContentControlStart, 
nContentControlEnd);
+    if (!bSuccess)
+    {
+        SAL_WARN("sw.core", "no pam");
+        throw lang::DisposedException("SwXContentControl::getAnchor(): not 
attached",
+                                      static_cast<::cppu::OWeakObject*>(this));
+    }
+
+    SwPosition aStart(*pTextNode, nContentControlStart - 1); // -1 due to 
CH_TXTATR
+    SwPosition aEnd(*pTextNode, nContentControlEnd);
+    return SwXTextRange::CreateXTextRange(pTextNode->GetDoc(), aStart, &aEnd);
+}
+
+// XTextRange
+uno::Reference<text::XText> SAL_CALL SwXContentControl::getText() { return 
this; }
+
+uno::Reference<text::XTextRange> SAL_CALL SwXContentControl::getStart()
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->getStart();
+}
+
+uno::Reference<text::XTextRange> SAL_CALL SwXContentControl::getEnd()
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->getEnd();
+}
+
+OUString SAL_CALL SwXContentControl::getString()
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->getString();
+}
+
+void SAL_CALL SwXContentControl::setString(const OUString& rString)
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->setString(rString);
+}
+
+// XSimpleText
+uno::Reference<text::XTextCursor> SAL_CALL 
SwXContentControl::createTextCursor()
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->createTextCursor();
+}
+
+uno::Reference<text::XTextCursor> SAL_CALL
+SwXContentControl::createTextCursorByRange(const 
uno::Reference<text::XTextRange>& xTextPosition)
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->createTextCursorByRange(xTextPosition);
+}
+
+void SAL_CALL SwXContentControl::insertString(const 
uno::Reference<text::XTextRange>& xRange,
+                                              const OUString& rString, 
sal_Bool bAbsorb)
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->insertString(xRange, rString, bAbsorb);
+}
+
+void SAL_CALL SwXContentControl::insertControlCharacter(
+    const uno::Reference<text::XTextRange>& xRange, sal_Int16 
nControlCharacter, sal_Bool bAbsorb)
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->insertControlCharacter(xRange, nControlCharacter, 
bAbsorb);
+}
+
+// XText
+void SAL_CALL SwXContentControl::insertTextContent(
+    const uno::Reference<text::XTextRange>& xRange,
+    const uno::Reference<text::XTextContent>& xContent, sal_Bool bAbsorb)
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->insertTextContent(xRange, xContent, bAbsorb);
+}
+
+void SAL_CALL
+SwXContentControl::removeTextContent(const uno::Reference<text::XTextContent>& 
xContent)
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_xText->removeTextContent(xContent);
+}
+
+// XElementAccess
+uno::Type SAL_CALL SwXContentControl::getElementType()
+{
+    return cppu::UnoType<text::XTextRange>::get();
+}
+
+sal_Bool SAL_CALL SwXContentControl::hasElements()
+{
+    SolarMutexGuard g;
+    return m_pImpl->m_pContentControl != nullptr;
+}
+
+// XEnumerationAccess
+uno::Reference<container::XEnumeration> SAL_CALL 
SwXContentControl::createEnumeration()
+{
+    SolarMutexGuard g;
+
+    if (m_pImpl->m_bIsDisposed)
+    {
+        throw lang::DisposedException();
+    }
+    if (m_pImpl->m_bIsDescriptor)
+    {
+        throw uno::RuntimeException("createEnumeration(): not inserted",
+                                    static_cast<::cppu::OWeakObject*>(this));
+    }
+
+    SwTextNode* pTextNode;
+    sal_Int32 nContentControlStart;
+    sal_Int32 nContentControlEnd;
+    bool bSuccess = SetContentRange(pTextNode, nContentControlStart, 
nContentControlEnd);
+    if (!bSuccess)
+    {
+        SAL_WARN("sw.core", "no pam");
+        throw lang::DisposedException();
+    }
+
+    SwPaM aPam(*pTextNode, nContentControlStart);
+
+    if (!m_pImpl->m_pTextPortions)
+    {
+        return new SwXTextPortionEnumeration(aPam, GetParentText(), 
nContentControlStart,
+                                             nContentControlEnd);
+    }
+    else
+    {
+        return new SwXTextPortionEnumeration(aPam, 
std::deque(*m_pImpl->m_pTextPortions));
+    }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/unocore/unoobj.cxx 
b/sw/source/core/unocore/unoobj.cxx
index 09136cf44c8c..90072ed52b78 100644
--- a/sw/source/core/unocore/unoobj.cxx
+++ b/sw/source/core/unocore/unoobj.cxx
@@ -54,6 +54,7 @@
 #include <unomap.hxx>
 #include <unoprnms.hxx>
 #include <unometa.hxx>
+#include <unocontentcontrol.hxx>
 #include <unotext.hxx>
 #include <com/sun/star/text/TextMarkupType.hpp>
 #include <vcl/svapp.hxx>
@@ -823,6 +824,42 @@ bool SwXTextCursor::IsAtEndOfMeta() const
     return false;
 }
 
+bool SwXTextCursor::IsAtEndOfContentControl() const
+{
+    if (CursorType::ContentControl == m_eType)
+    {
+        auto pCursor( m_pUnoCursor );
+        auto pXContentControl(
+                dynamic_cast<SwXContentControl*>(m_xParentText.get()) );
+        if (!pXContentControl)
+        {
+            SAL_WARN("sw.core", "SwXTextCursor::IsAtEndOfContentControl: no 
content control");
+        }
+        if (pCursor && pXContentControl)
+        {
+            SwTextNode * pTextNode;
+            sal_Int32 nStart;
+            sal_Int32 nEnd;
+            const bool bSuccess(
+                    pXContentControl->SetContentRange(pTextNode, nStart, nEnd) 
);
+            if (!bSuccess)
+            {
+                SAL_WARN("sw.core", "SwXTextCursor::IsAtEndOfContentControl: 
no pam");
+            }
+            else
+            {
+                const SwPosition end(*pTextNode, nEnd);
+                if (   (*pCursor->GetPoint() == end)
+                    || (*pCursor->GetMark()  == end))
+                {
+                    return true;
+                }
+            }
+        }
+    }
+    return false;
+}
+
 OUString SwXTextCursor::getImplementationName()
 {
     return "SwXTextCursor";
diff --git a/sw/source/core/unocore/unotext.cxx 
b/sw/source/core/unocore/unotext.cxx
index 2ad7fce679d8..8b0f605640e6 100644
--- a/sw/source/core/unocore/unotext.cxx
+++ b/sw/source/core/unocore/unotext.cxx
@@ -70,6 +70,7 @@
 #include <SwRewriter.hxx>
 #include <strings.hrc>
 #include <frameformats.hxx>
+#include <unocontentcontrol.hxx>
 
 using namespace ::com::sun::star;
 
@@ -593,13 +594,14 @@ SwXText::insertTextContent(
         comphelper::getFromUnoTunnel<SwXReferenceMark>(xContentTunnel);
     SwXMeta *const pMeta =
         comphelper::getFromUnoTunnel<SwXMeta>(xContentTunnel);
+    auto* pContentControl = 
comphelper::getFromUnoTunnel<SwXContentControl>(xContentTunnel);
     SwXTextField* pTextField =
         comphelper::getFromUnoTunnel<SwXTextField>(xContentTunnel);
     if (pTextField && pTextField->GetServiceId() != 
SwServiceType::FieldTypeAnnotation)
         pTextField = nullptr;
 
     const bool bAttribute = pBookmark || pDocumentIndexMark
-        || pSection || pReferenceMark || pMeta || pTextField;
+        || pSection || pReferenceMark || pMeta || pContentControl || 
pTextField;
 
     if (bAbsorb && !bAttribute)
     {
commit 0ef9ce463f6fe6bd68f01d6b075da3b5e4dbdd32
Author:     Miklos Vajna <[email protected]>
AuthorDate: Wed Mar 30 08:16:21 2022 +0200
Commit:     Miklos Vajna <[email protected]>
CommitDate: Thu Apr 21 08:07:14 2022 +0200

    sw content controls: add document model
    
    This is meant to represent inline structured document tags (<w:sdt>
    elements in DOCX) or content controls (as Word users know this).
    
    Don't confuse this with block-level, cell-level or table-level content
    controls, which are not covered here.
    
    You may wonder why fields or fieldmarks can't be used to represent this.
    The problems are:
    
    - a fieldmark can contain a paragraph break, inline content controls
      can't
    
    - content controls must be a well-formed XML element, while fieldmarks
      can start/end at random document model positions
    
    - fieldmarks are supposed to have a field command and a result result
      (with a separator between the two), but content controls don't have a
      field command
    
    - mapping to a field has the problem that Writer fields can only have a
      single text portion / run, but content controls can have multiple ones
    
    So model content controls with a meta-like text attribute which covers
    all these cases.
    
    SwContentControl is mostly empty as a start, it can track more data in
    follow-up commits. SwTextContentControl and SwFormatContentControl are
    the matching text attr and pool item subclasses.
    
    (cherry picked from commit 3dd4f3691458ea537bc1867386269694775cfbcb)
    
    Change-Id: I1e57f7756cf87041975371483ec4fc8abf875dfe
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/132960
    Tested-by: Jenkins CollaboraOffice <[email protected]>
    Reviewed-by: Miklos Vajna <[email protected]>

diff --git a/sw/Library_sw.mk b/sw/Library_sw.mk
index ef3a27b74458..be6fae7446a1 100644
--- a/sw/Library_sw.mk
+++ b/sw/Library_sw.mk
@@ -420,6 +420,7 @@ $(eval $(call gb_Library_add_exception_objects,sw,\
     sw/source/core/tox/ToxTextGenerator \
     sw/source/core/tox/ToxWhitespaceStripper \
     sw/source/core/txtnode/SwGrammarContact \
+    sw/source/core/txtnode/attrcontentcontrol \
     sw/source/core/txtnode/atrfld \
     sw/source/core/txtnode/atrflyin \
     sw/source/core/txtnode/atrftn \
diff --git a/sw/inc/formatcontentcontrol.hxx b/sw/inc/formatcontentcontrol.hxx
new file mode 100644
index 000000000000..0fa075bf7036
--- /dev/null
+++ b/sw/inc/formatcontentcontrol.hxx
@@ -0,0 +1,104 @@
+/* -*- 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/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ *   Licensed to the Apache Software Foundation (ASF) under one or more
+ *   contributor license agreements. See the NOTICE file distributed
+ *   with this work for additional information regarding copyright
+ *   ownership. The ASF licenses this file to you under the Apache
+ *   License, Version 2.0 (the "License"); you may not use this file
+ *   except in compliance with the License. You may obtain a copy of
+ *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#pragma once
+
+#include <com/sun/star/text/XTextContent.hpp>
+
+#include <cppuhelper/weakref.hxx>
+#include <svl/poolitem.hxx>
+
+#include "calbck.hxx"
+
+class SwContentControl;
+class SwTextContentControl;
+class SwTextNode;
+class SwXContentControl;
+
+/// SfxPoolItem subclass that wraps an SwContentControl.
+class SwFormatContentControl final : public SfxPoolItem
+{
+    std::shared_ptr<SwContentControl> m_pContentControl;
+    SwTextContentControl* m_pTextAttr;
+
+public:
+    SwTextContentControl* GetTextAttr() { return m_pTextAttr; }
+    void SetTextAttr(SwTextContentControl* pTextAttr);
+
+    /// This method must be called when the hint is actually copied.
+    void DoCopy(SwTextNode& rTargetTextNode);
+
+    explicit SwFormatContentControl(sal_uInt16 nWhich);
+
+    explicit SwFormatContentControl(const std::shared_ptr<SwContentControl>& 
pContentControl,
+                                    sal_uInt16 nWhich);
+    ~SwFormatContentControl() override;
+
+    /// SfxPoolItem
+    bool operator==(const SfxPoolItem&) const override;
+    SwFormatContentControl* Clone(SfxItemPool* pPool = nullptr) const override;
+
+    /**
+     * Notify clients registered at m_pContentControl that this content 
control is being
+     * (re-)moved.
+     */
+    void NotifyChangeTextNode(SwTextNode* pTextNode);
+    static SwFormatContentControl* CreatePoolDefault(sal_uInt16 nWhich);
+    SwContentControl* GetContentControl() { return m_pContentControl.get(); }
+};
+
+/// Stores the properties of a content control.
+class SwContentControl : public sw::BroadcastingModify
+{
+    css::uno::WeakReference<css::text::XTextContent> m_wXContentControl;
+
+    SwFormatContentControl* m_pFormat;
+
+    /// Can be nullptr if not in a document for undo purposes.
+    SwTextNode* m_pTextNode;
+
+public:
+    SwTextContentControl* GetTextAttr() const;
+
+    SwTextNode* GetTextNode() const { return m_pTextNode; }
+
+    SwFormatContentControl* GetFormatContentControl() const { return 
m_pFormat; }
+
+    void SetFormatContentControl(SwFormatContentControl* pFormat) { m_pFormat 
= pFormat; };
+
+    void NotifyChangeTextNode(SwTextNode* pTextNode);
+
+    css::uno::WeakReference<css::text::XTextContent> GetXContentControl() const
+    {
+        return m_wXContentControl;
+    }
+
+    void SetXContentControl(const 
css::uno::Reference<css::text::XTextContent>& xContentCnotrol)
+    {
+        m_wXContentControl = xContentCnotrol;
+    }
+
+    virtual void SwClientNotify(const SwModify&, const SfxHint&) override;
+
+    explicit SwContentControl(SwFormatContentControl* pFormat);
+
+    virtual ~SwContentControl() override;
+};
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/inc/hintids.hxx b/sw/inc/hintids.hxx
index f446d6ab306b..6f681de8998e 100644
--- a/sw/inc/hintids.hxx
+++ b/sw/inc/hintids.hxx
@@ -32,6 +32,7 @@ class SwUpdateAttr;
 class SwAttrSetChg;
 class SwDocPosUpdate;
 class SwFormatMeta;
+class SwFormatContentControl;
 class SvXMLAttrContainerItem;
 class SwMsgPoolItem;
 class SwPtrMsgPoolItem;
@@ -275,17 +276,17 @@ constexpr TypedWhichId<SwFormatCharFormat> 
RES_TXTATR_CHARFMT(52);
 constexpr TypedWhichId<SwFormatRuby> RES_TXTATR_CJK_RUBY(53);
 constexpr TypedWhichId<SvXMLAttrContainerItem> 
RES_TXTATR_UNKNOWN_CONTAINER(54);
 constexpr TypedWhichId<SwFormatField> RES_TXTATR_INPUTFIELD(55);
-constexpr sal_uInt16 RES_TXTATR_WITHEND_END(56);
+constexpr TypedWhichId<SwFormatContentControl> RES_TXTATR_CONTENTCONTROL(56);
+constexpr sal_uInt16 RES_TXTATR_WITHEND_END(57);
 
 // all TextAttributes without an end
 constexpr sal_uInt16 RES_TXTATR_NOEND_BEGIN(RES_TXTATR_WITHEND_END);
-constexpr TypedWhichId<SwFormatField> 
RES_TXTATR_FIELD(RES_TXTATR_NOEND_BEGIN); // 56
-constexpr TypedWhichId<SwFormatFlyCnt> RES_TXTATR_FLYCNT(57);
-constexpr TypedWhichId<SwFormatFootnote> RES_TXTATR_FTN(58);
-constexpr TypedWhichId<SwFormatField> RES_TXTATR_ANNOTATION(59);
-constexpr TypedWhichId<SwFormatLineBreak> RES_TXTATR_LINEBREAK(60);
-constexpr TypedWhichId<SfxBoolItem> RES_TXTATR_DUMMY1(61);
-constexpr TypedWhichId<SfxBoolItem> RES_TXTATR_DUMMY2(62);
+constexpr TypedWhichId<SwFormatField> 
RES_TXTATR_FIELD(RES_TXTATR_NOEND_BEGIN); // 57
+constexpr TypedWhichId<SwFormatFlyCnt> RES_TXTATR_FLYCNT(58);
+constexpr TypedWhichId<SwFormatFootnote> RES_TXTATR_FTN(59);
+constexpr TypedWhichId<SwFormatField> RES_TXTATR_ANNOTATION(60);
+constexpr TypedWhichId<SwFormatLineBreak> RES_TXTATR_LINEBREAK(61);
+constexpr TypedWhichId<SfxBoolItem> RES_TXTATR_DUMMY1(62);
 constexpr sal_uInt16 RES_TXTATR_NOEND_END(63);
 constexpr sal_uInt16 RES_TXTATR_END(RES_TXTATR_NOEND_END);
 
diff --git a/sw/inc/textcontentcontrol.hxx b/sw/inc/textcontentcontrol.hxx
new file mode 100644
index 000000000000..3410a6a35506
--- /dev/null
+++ b/sw/inc/textcontentcontrol.hxx
@@ -0,0 +1,41 @@
+/* -*- 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/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ *   Licensed to the Apache Software Foundation (ASF) under one or more
+ *   contributor license agreements. See the NOTICE file distributed
+ *   with this work for additional information regarding copyright
+ *   ownership. The ASF licenses this file to you under the Apache
+ *   License, Version 2.0 (the "License"); you may not use this file
+ *   except in compliance with the License. You may obtain a copy of
+ *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+#pragma once
+
+#include "txatbase.hxx"
+
+class SwFormatContentControl;
+
+/// SwTextAttr subclass that tracks the location of the wrapped 
SwFormatContentControl.
+class SW_DLLPUBLIC SwTextContentControl final : public SwTextAttrNesting
+{
+    SwTextContentControl(SwFormatContentControl& rAttr, sal_Int32 nStart, 
sal_Int32 nEnd);
+
+public:
+    static SwTextContentControl* CreateTextContentControl(SwTextNode* 
pTargetTextNode,
+                                                          
SwFormatContentControl& rAttr,
+                                                          sal_Int32 nStart, 
sal_Int32 nEnd,
+                                                          bool bIsCopy);
+
+    ~SwTextContentControl() override;
+
+    void ChgTextNode(SwTextNode* pNode);
+};
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/bastyp/init.cxx b/sw/source/core/bastyp/init.cxx
index 87cb7dfa3127..dcbe9856602b 100644
--- a/sw/source/core/bastyp/init.cxx
+++ b/sw/source/core/bastyp/init.cxx
@@ -99,6 +99,7 @@
 #include <fmtline.hxx>
 #include <fmtlsplt.hxx>
 #include <fmtmeta.hxx>
+#include <formatcontentcontrol.hxx>
 #include <fmtornt.hxx>
 #include <fmtpdsc.hxx>
 #include <fmtrfmrk.hxx>
@@ -314,6 +315,7 @@ SfxItemInfo aSlotTab[] =
     { SID_ATTR_CHAR_CJK_RUBY, false },          // RES_TXTATR_CJK_RUBY
     { 0, true },                           // RES_TXTATR_UNKNOWN_CONTAINER
     { 0, false },                               // RES_TXTATR_INPUTFIELD
+    { 0, false },                               // RES_TXTATR_CONTENTCONTROL
 
     { 0, false },                               // RES_TXTATR_FIELD
     { 0, false },                               // RES_TXTATR_FLYCNT
@@ -321,7 +323,6 @@ SfxItemInfo aSlotTab[] =
     { 0, false },                               // RES_TXTATR_ANNOTATION
     { 0, false },                               // RES_TXTATR_LINEBREAK
     { 0, true },                           // RES_TXTATR_DUMMY1
-    { 0, true },                           // RES_TXTATR_DUMMY2
 
     { SID_ATTR_PARA_LINESPACE, true },     // RES_PARATR_LINESPACING
     { SID_ATTR_PARA_ADJUST, true },        // RES_PARATR_ADJUST
@@ -512,6 +513,7 @@ void InitCore()
     aAttrTab[ RES_TXTATR_CJK_RUBY - POOLATTR_BEGIN ] =      new SwFormatRuby( 
OUString() );
     aAttrTab[ RES_TXTATR_UNKNOWN_CONTAINER - POOLATTR_BEGIN ] = new 
SvXMLAttrContainerItem( RES_TXTATR_UNKNOWN_CONTAINER );
     aAttrTab[ RES_TXTATR_INPUTFIELD - POOLATTR_BEGIN ] = new SwFormatField( 
RES_TXTATR_INPUTFIELD );
+    aAttrTab[ RES_TXTATR_CONTENTCONTROL - POOLATTR_BEGIN ] = new 
SwFormatContentControl( RES_TXTATR_CONTENTCONTROL );
 
     aAttrTab[ RES_TXTATR_FIELD- POOLATTR_BEGIN ] =          new SwFormatField( 
RES_TXTATR_FIELD );
     aAttrTab[ RES_TXTATR_FLYCNT - POOLATTR_BEGIN ] =        new 
SwFormatFlyCnt( nullptr );
@@ -521,7 +523,6 @@ void InitCore()
 
 // TextAttr - Dummies
     aAttrTab[ RES_TXTATR_DUMMY1 - POOLATTR_BEGIN ] =        new SfxBoolItem( 
RES_TXTATR_DUMMY1 );
-    aAttrTab[ RES_TXTATR_DUMMY2 - POOLATTR_BEGIN ] =        new SfxBoolItem( 
RES_TXTATR_DUMMY2 );
 
     aAttrTab[ RES_PARATR_LINESPACING- POOLATTR_BEGIN ] =    new 
SvxLineSpacingItem( LINE_SPACE_DEFAULT_HEIGHT, RES_PARATR_LINESPACING );
     aAttrTab[ RES_PARATR_ADJUST- POOLATTR_BEGIN ] =         new SvxAdjustItem( 
SvxAdjust::Left, RES_PARATR_ADJUST );
diff --git a/sw/source/core/doc/DocumentContentOperationsManager.cxx 
b/sw/source/core/doc/DocumentContentOperationsManager.cxx
index 11f91cfe38b5..b3f76c03a4c3 100644
--- a/sw/source/core/doc/DocumentContentOperationsManager.cxx
+++ b/sw/source/core/doc/DocumentContentOperationsManager.cxx
@@ -1563,7 +1563,7 @@ namespace //local functions originally from docfmt.cxx
                 SfxItemSetFixed<
                         RES_TXTATR_REFMARK, RES_TXTATR_METAFIELD,
                         RES_TXTATR_CJK_RUBY, RES_TXTATR_CJK_RUBY,
-                        RES_TXTATR_INPUTFIELD, RES_TXTATR_INPUTFIELD>
+                        RES_TXTATR_INPUTFIELD, RES_TXTATR_CONTENTCONTROL>
                      aTextSet(rDoc.GetAttrPool());
 
                 aTextSet.Put( rChgSet );
diff --git a/sw/source/core/doc/dbgoutsw.cxx b/sw/source/core/doc/dbgoutsw.cxx
index 0d294863dbed..73b0c47462eb 100644
--- a/sw/source/core/doc/dbgoutsw.cxx
+++ b/sw/source/core/doc/dbgoutsw.cxx
@@ -139,6 +139,7 @@ static std::map<sal_uInt16,OUString> & GetItemWhichMap()
         { RES_TXTATR_TOXMARK , "TXTATR_TOXMARK" },
         { RES_TXTATR_CHARFMT , "TXTATR_CHARFMT" },
         { RES_TXTATR_INPUTFIELD , "RES_TXTATR_INPUTFIELD" },
+        { RES_TXTATR_CONTENTCONTROL , "RES_TXTATR_CONTENTCONTROL" },
         { RES_TXTATR_CJK_RUBY , "TXTATR_CJK_RUBY" },
         { RES_TXTATR_UNKNOWN_CONTAINER , "TXTATR_UNKNOWN_CONTAINER" },
         { RES_TXTATR_META , "TXTATR_META" },
@@ -149,7 +150,6 @@ static std::map<sal_uInt16,OUString> & GetItemWhichMap()
         { RES_TXTATR_ANNOTATION , "TXTATR_ANNOTATION" },
         { RES_TXTATR_LINEBREAK , "RES_TXTATR_LINEBREAK" },
         { RES_TXTATR_DUMMY1 , "TXTATR_DUMMY1" },
-        { RES_TXTATR_DUMMY2 , "TXTATR_DUMMY2" },
         { RES_PARATR_LINESPACING , "PARATR_LINESPACING" },
         { RES_PARATR_ADJUST , "PARATR_ADJUST" },
         { RES_PARATR_SPLIT , "PARATR_SPLIT" },
diff --git a/sw/source/core/txtnode/attrcontentcontrol.cxx 
b/sw/source/core/txtnode/attrcontentcontrol.cxx
new file mode 100644
index 000000000000..eb127f1a1ba6
--- /dev/null
+++ b/sw/source/core/txtnode/attrcontentcontrol.cxx
@@ -0,0 +1,237 @@
+/* -*- 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/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ *   Licensed to the Apache Software Foundation (ASF) under one or more
+ *   contributor license agreements. See the NOTICE file distributed
+ *   with this work for additional information regarding copyright
+ *   ownership. The ASF licenses this file to you under the Apache
+ *   License, Version 2.0 (the "License"); you may not use this file
+ *   except in compliance with the License. You may obtain a copy of
+ *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <formatcontentcontrol.hxx>
+
+#include <sal/log.hxx>
+
+#include <ndtxt.hxx>
+#include <textcontentcontrol.hxx>
+
+using namespace com::sun::star;
+
+SwFormatContentControl* SwFormatContentControl::CreatePoolDefault(sal_uInt16 
nWhich)
+{
+    return new SwFormatContentControl(nWhich);
+}
+
+SwFormatContentControl::SwFormatContentControl(sal_uInt16 nWhich)
+    : SfxPoolItem(nWhich)
+    , m_pTextAttr(nullptr)
+{
+}
+
+SwFormatContentControl::SwFormatContentControl(
+    const std::shared_ptr<SwContentControl>& pContentControl, sal_uInt16 
nWhich)
+    : SfxPoolItem(nWhich)
+    , m_pContentControl(pContentControl)
+    , m_pTextAttr(nullptr)
+{
+    if (!pContentControl)
+    {
+        SAL_WARN("sw.core", "SwFormatContentControl ctor: no 
pContentControl?");
+    }
+    // Not calling m_pContentControl->SetFormatContentControl(this) here; only 
from SetTextAttr.
+}
+
+SwFormatContentControl::~SwFormatContentControl()
+{
+    if (m_pContentControl && (m_pContentControl->GetFormatContentControl() == 
this))
+    {
+        NotifyChangeTextNode(nullptr);
+        m_pContentControl->SetFormatContentControl(nullptr);
+    }
+}
+
+bool SwFormatContentControl::operator==(const SfxPoolItem& rOther) const
+{
+    return SfxPoolItem::operator==(rOther)
+           && m_pContentControl
+                  == static_cast<const 
SwFormatContentControl&>(rOther).m_pContentControl;
+}
+
+SwFormatContentControl* SwFormatContentControl::Clone(SfxItemPool* /*pPool*/) 
const
+{
+    // If this is indeed a copy, then DoCopy will be called later.
+    if (m_pContentControl)
+    {
+        return new SwFormatContentControl(m_pContentControl, Which());
+    }
+    else
+    {
+        return new SwFormatContentControl(Which());
+    }
+}
+
+void SwFormatContentControl::SetTextAttr(SwTextContentControl* pTextAttr)
+{
+    if (m_pTextAttr && pTextAttr)
+    {
+        SAL_WARN("sw.core", "SwFormatContentControl::SetTextAttr: already has 
a text attribute");
+    }
+    if (!m_pTextAttr && !pTextAttr)
+    {
+        SAL_WARN("sw.core", "SwFormatContentControl::SetTextAttr: no attribute 
to remove");
+    }
+    m_pTextAttr = pTextAttr;
+    if (!m_pContentControl)
+    {
+        SAL_WARN("sw.core", "inserted SwFormatContentControl has no 
SwContentControl");
+    }
+    // The SwContentControl should be able to find the current text attribute.
+    if (m_pContentControl)
+    {
+        if (pTextAttr)
+        {
+            m_pContentControl->SetFormatContentControl(this);
+        }
+        else if (m_pContentControl->GetFormatContentControl() == this)
+        {
+            // The text attribute is gone, so de-register from text node.
+            NotifyChangeTextNode(nullptr);
+            m_pContentControl->SetFormatContentControl(nullptr);
+        }
+    }
+}
+
+void SwFormatContentControl::NotifyChangeTextNode(SwTextNode* pTextNode)
+{
+    // Not deleting m_pTextAttr here, SwNodes::ChgNode() doesn't do that, 
either.
+    if (!m_pContentControl)
+    {
+        SAL_WARN("sw.core", "SwFormatContentControl::NotifyChangeTextNode: no 
content control?");
+    }
+    if (m_pContentControl && (m_pContentControl->GetFormatContentControl() == 
this))
+    {
+        // Not calling Modify, that would call 
SwXContentControl::SwClientNotify.
+        m_pContentControl->NotifyChangeTextNode(pTextNode);
+    }
+}
+
+// This SwFormatContentControl has been cloned and points at the same 
SwContentControl as the
+// source: this function copies the SwContentControl.
+void SwFormatContentControl::DoCopy(SwTextNode& rTargetTextNode)
+{
+    if (!m_pContentControl)
+    {
+        SAL_WARN("sw.core", "SwFormatContentControl::DoCopy: called for 
SwFormatContentControl "
+                            "with no SwContentControl.");
+        return;
+    }
+
+    m_pContentControl = std::make_shared<SwContentControl>(this);
+    m_pContentControl->NotifyChangeTextNode(&rTargetTextNode);
+}
+
+SwContentControl::SwContentControl(SwFormatContentControl* pFormat)
+    : sw::BroadcastingModify()
+    , m_pFormat(pFormat)
+    , m_pTextNode(nullptr)
+{
+}
+
+SwContentControl::~SwContentControl() {}
+
+SwTextContentControl* SwContentControl::GetTextAttr() const
+{
+    return m_pFormat ? m_pFormat->GetTextAttr() : nullptr;
+}
+
+void SwContentControl::NotifyChangeTextNode(SwTextNode* pTextNode)
+{
+    m_pTextNode = pTextNode;
+    if (m_pTextNode && (GetRegisteredIn() != m_pTextNode))
+    {
+        m_pTextNode->Add(this);
+    }
+    else if (!m_pTextNode)
+    {
+        EndListeningAll();
+    }
+    if (!pTextNode)
+    {
+        // If the text node is gone, then invalidate clients (e.g. UNO object).
+        GetNotifier().Broadcast(SfxHint(SfxHintId::Deinitializing));
+    }
+}
+
+void SwContentControl::SwClientNotify(const SwModify&, const SfxHint& rHint)
+{
+    if (rHint.GetId() != SfxHintId::SwLegacyModify)
+        return;
+
+    auto pLegacy = static_cast<const sw::LegacyModifyHint*>(&rHint);
+    CallSwClientNotify(rHint);
+    GetNotifier().Broadcast(SfxHint(SfxHintId::DataChanged));
+
+    if (pLegacy->GetWhich() == RES_REMOVE_UNO_OBJECT)
+    {
+        // Invalidate cached uno object.
+        SetXContentControl(uno::Reference<text::XTextContent>());
+        GetNotifier().Broadcast(SfxHint(SfxHintId::Deinitializing));
+    }
+}
+
+SwTextContentControl* 
SwTextContentControl::CreateTextContentControl(SwTextNode* pTargetTextNode,
+                                                                     
SwFormatContentControl& rAttr,
+                                                                     sal_Int32 
nStart,
+                                                                     sal_Int32 
nEnd, bool bIsCopy)
+{
+    if (bIsCopy)
+    {
+        // rAttr is already cloned, now call DoCopy to copy the 
SwContentControl
+        if (!pTargetTextNode)
+        {
+            SAL_WARN("sw.core",
+                     "SwTextContentControl ctor: cannot copy content control 
without target node");
+        }
+        rAttr.DoCopy(*pTargetTextNode);
+    }
+    auto pTextContentControl(new SwTextContentControl(rAttr, nStart, nEnd));
+    return pTextContentControl;
+}
+
+SwTextContentControl::SwTextContentControl(SwFormatContentControl& rAttr, 
sal_Int32 nStart,
+                                           sal_Int32 nEnd)
+    : SwTextAttr(rAttr, nStart)
+    , SwTextAttrNesting(rAttr, nStart, nEnd)
+{
+    rAttr.SetTextAttr(this);
+    SetHasDummyChar(true);
+}
+
+SwTextContentControl::~SwTextContentControl()
+{
+    auto& rFormatContentControl = 
static_cast<SwFormatContentControl&>(GetAttr());
+    if (rFormatContentControl.GetTextAttr() == this)
+    {
+        rFormatContentControl.SetTextAttr(nullptr);
+    }
+}
+
+void SwTextContentControl::ChgTextNode(SwTextNode* pNode)
+{
+    auto& rFormatContentControl = 
static_cast<SwFormatContentControl&>(GetAttr());
+    if (rFormatContentControl.GetTextAttr() == this)
+    {
+        rFormatContentControl.NotifyChangeTextNode(pNode);
+    }
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/txtnode/thints.cxx 
b/sw/source/core/txtnode/thints.cxx
index ec5ef9aa7679..f465b65a2e39 100644
--- a/sw/source/core/txtnode/thints.cxx
+++ b/sw/source/core/txtnode/thints.cxx
@@ -52,6 +52,8 @@
 #include <ftnidx.hxx>
 #include <fmtruby.hxx>
 #include <fmtmeta.hxx>
+#include <formatcontentcontrol.hxx>
+#include <textcontentcontrol.hxx>
 #include <breakit.hxx>
 #include <doc.hxx>
 #include <IDocumentUndoRedo.hxx>
@@ -105,7 +107,8 @@ SwpHints::SwpHints(const SwTextNode& rParent)
 static void TextAttrDelete( SwDoc & rDoc, SwTextAttr * const pAttr )
 {
     if (RES_TXTATR_META == pAttr->Which() ||
-        RES_TXTATR_METAFIELD == pAttr->Which())
+        RES_TXTATR_METAFIELD == pAttr->Which() ||
+        pAttr->Which() == RES_TXTATR_CONTENTCONTROL)
     {
         static_txtattr_cast<SwTextMeta *>(pAttr)->ChgTextNode(nullptr); // 
prevents ASSERT
     }
@@ -161,7 +164,8 @@ bool isSelfNestable(const sal_uInt16 nWhich)
         (RES_TXTATR_INPUTFIELD == nWhich))
         return false;
     assert((RES_TXTATR_META  == nWhich) ||
-           (RES_TXTATR_METAFIELD  == nWhich));
+           (RES_TXTATR_METAFIELD  == nWhich) ||
+           (RES_TXTATR_CONTENTCONTROL  == nWhich));
     return true;
 }
 
@@ -173,7 +177,8 @@ bool isSplittable(const sal_uInt16 nWhich)
         return true;
     assert((RES_TXTATR_META  == nWhich) ||
            (RES_TXTATR_METAFIELD  == nWhich) ||
-           (RES_TXTATR_INPUTFIELD  == nWhich));
+           (RES_TXTATR_INPUTFIELD  == nWhich) ||
+           (RES_TXTATR_CONTENTCONTROL  == nWhich));
     return false;
 }
 
@@ -300,7 +305,8 @@ void SwpHints::InsertNesting(SwTextAttrNesting & rNewHint)
 /**
 
 The following hints correspond to well-formed XML elements in ODF:
-RES_TXTATR_INETFMT, RES_TXTATR_CJK_RUBY, RES_TXTATR_META, RES_TXTATR_METAFIELD
+RES_TXTATR_INETFMT, RES_TXTATR_CJK_RUBY, RES_TXTATR_META, RES_TXTATR_METAFIELD,
+RES_TXTATR_CONTENTCONTROL
 
 The writer core must ensure that these do not overlap; if they did,
 the document would not be storable as ODF.
@@ -324,6 +330,8 @@ the result is properly nested.
 
 meta and meta-field must not be split, because they have xml:id.
 
+content controls should not split, either.
+
 These constraints result in the following design:
 
 RES_TXTATR_INETFMT:
@@ -377,6 +385,7 @@ SwpHints::TryInsertNesting( SwTextNode & rNode, 
SwTextAttrNesting & rNewHint )
             (RES_TXTATR_CJK_RUBY  == nNewWhich) ||
             (RES_TXTATR_META      == nNewWhich) ||
             (RES_TXTATR_METAFIELD == nNewWhich) ||
+            (RES_TXTATR_CONTENTCONTROL == nNewWhich) ||
             (RES_TXTATR_INPUTFIELD == nNewWhich));
 
     NestList_t OverlappingExisting; // existing hints to be split
@@ -1144,6 +1153,11 @@ SwTextAttr* MakeTextAttr(
     case RES_TXTATR_LINEBREAK:
         pNew = new SwTextLineBreak(static_cast<SwFormatLineBreak&>(rNew), 
nStt);
         break;
+    case RES_TXTATR_CONTENTCONTROL:
+        pNew = SwTextContentControl::CreateTextContentControl(
+            pTextNode, static_cast<SwFormatContentControl&>(rNew), nStt, nEnd,
+            bIsCopy == CopyOrNewType::Copy);
+        break;
     default:
         assert(RES_TXTATR_AUTOFMT == rNew.Which());
         pNew = new SwTextAttrEnd( rNew, nStt, nEnd );
@@ -1271,6 +1285,11 @@ void SwTextNode::DestroyAttr( SwTextAttr* pAttr )
         static_txtattr_cast<SwTextMeta*>(pAttr)->ChgTextNode(nullptr);
     }
         break;
+    case RES_TXTATR_CONTENTCONTROL:
+    {
+        
static_txtattr_cast<SwTextContentControl*>(pAttr)->ChgTextNode(nullptr);
+        break;
+    }
 
     default:
         break;
@@ -3172,6 +3191,10 @@ bool SwpHints::TryInsertHint(
         static_txtattr_cast<SwTextMeta *>(pHint)->ChgTextNode( &rNode );
         break;
 
+    case RES_TXTATR_CONTENTCONTROL:
+        static_txtattr_cast<SwTextContentControl*>(pHint)->ChgTextNode( &rNode 
);
+        break;
+
     case RES_CHRATR_HIDDEN:
         rNode.SetCalcHiddenCharFlags();
         break;
@@ -3477,6 +3500,7 @@ sal_Unicode GetCharOfTextAttr( const SwTextAttr& rAttr )
         case RES_TXTATR_FTN:
         case RES_TXTATR_META:
         case RES_TXTATR_METAFIELD:
+        case RES_TXTATR_CONTENTCONTROL:
         {
             cRet = CH_TXTATR_BREAKWORD;
         }
diff --git a/sw/source/core/txtnode/txatbase.cxx 
b/sw/source/core/txtnode/txatbase.cxx
index d4235a830358..cd18549c58d8 100644
--- a/sw/source/core/txtnode/txatbase.cxx
+++ b/sw/source/core/txtnode/txatbase.cxx
@@ -165,6 +165,8 @@ void SwTextAttr::dumpAsXml(xmlTextWriterPtr pWriter) const
             break;
         case RES_TXTATR_META:
             break;
+        case RES_TXTATR_CONTENTCONTROL:
+            break;
         default:
             SAL_WARN("sw.core", "Unhandled TXTATR");
             break;
diff --git a/sw/source/filter/html/css1atr.cxx 
b/sw/source/filter/html/css1atr.cxx
index 882d7678ef55..150bba91ed8b 100644
--- a/sw/source/filter/html/css1atr.cxx
+++ b/sw/source/filter/html/css1atr.cxx
@@ -3473,6 +3473,7 @@ SwAttrFnTab const aCSS1AttrFnTab = {
 /* RES_TXTATR_CJK_RUBY */           nullptr,
 /* RES_TXTATR_UNKNOWN_CONTAINER */  nullptr,
 /* RES_TXTATR_INPUTFIELD */         nullptr,
+/* RES_TXTATR_CONTENTCONTROL */     nullptr,
 
 /* RES_TXTATR_FIELD */              nullptr,
 /* RES_TXTATR_FLYCNT */             nullptr,
@@ -3480,7 +3481,6 @@ SwAttrFnTab const aCSS1AttrFnTab = {
 /* RES_TXTATR_ANNOTATION */         nullptr,
 /* RES_TXTATR_LINEBREAK */          nullptr,
 /* RES_TXTATR_DUMMY1 */             nullptr, // Dummy:
-/* RES_TXTATR_DUMMY2 */             nullptr, // Dummy:
 
 /* RES_PARATR_LINESPACING   */      OutCSS1_SvxLineSpacing,
 /* RES_PARATR_ADJUST    */          OutCSS1_SvxAdjust,
diff --git a/sw/source/filter/html/htmlatr.cxx 
b/sw/source/filter/html/htmlatr.cxx
index b34eed5bd41b..e50a5f06d6c1 100644
--- a/sw/source/filter/html/htmlatr.cxx
+++ b/sw/source/filter/html/htmlatr.cxx
@@ -3264,6 +3264,7 @@ SwAttrFnTab aHTMLAttrFnTab = {
 /* RES_TXTATR_CJK_RUBY */           nullptr,
 /* RES_TXTATR_UNKNOWN_CONTAINER */  nullptr,
 /* RES_TXTATR_INPUTFIELD */         OutHTML_SwFormatField,
+/* RES_TXTATR_CONTENTCONTROL */     nullptr,
 
 /* RES_TXTATR_FIELD */              OutHTML_SwFormatField,
 /* RES_TXTATR_FLYCNT */             OutHTML_SwFlyCnt,
@@ -3271,7 +3272,6 @@ SwAttrFnTab aHTMLAttrFnTab = {
 /* RES_TXTATR_ANNOTATION */         OutHTML_SwFormatField,
 /* RES_TXTATR_LINEBREAK */          OutHTML_SwFormatLineBreak,
 /* RES_TXTATR_DUMMY1 */             nullptr, // Dummy:
-/* RES_TXTATR_DUMMY2 */             nullptr, // Dummy:
 
 /* RES_PARATR_LINESPACING   */      nullptr,
 /* RES_PARATR_ADJUST    */          OutHTML_SvxAdjust,

Reply via email to