sw/inc/crsrsh.hxx                                       |    1 
 sw/inc/ndtxt.hxx                                        |    8 ++++
 sw/qa/core/crsr/crsr.cxx                                |   32 ++++++++++++++++
 sw/qa/core/doc/doc.cxx                                  |   30 +++++++++++++++
 sw/qa/core/unocore/unocore.cxx                          |   27 +++++++++++++
 sw/qa/extras/ww8export/ww8export2.cxx                   |   28 ++++++++++++++
 sw/source/core/crsr/crstrvl.cxx                         |   21 ++++++++++
 sw/source/core/doc/DocumentContentOperationsManager.cxx |   12 +++++-
 sw/source/core/txtnode/ndtxt.cxx                        |   22 +++++++++++
 sw/source/core/unocore/unocrsrhelper.cxx                |    6 +++
 sw/source/filter/ww8/wrtw8nds.cxx                       |    7 +++
 sw/source/uibase/docvw/edtwin.cxx                       |    3 +
 sw/source/uibase/shells/textsh.cxx                      |    2 -
 13 files changed, 196 insertions(+), 3 deletions(-)

New commits:
commit 32dab3228cd315437efe0c5b850d116235eaa797
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Thu May 12 16:31:53 2022 +0200
Commit:     Miklos Vajna <vmik...@collabora.com>
CommitDate: Thu May 12 18:29:07 2022 +0200

    sw content controls: fixes for the ending dummy char
    
    - make sure the DOC/RTF export doesn't write the dummy char as-is
    
    - let "enter" only insert a linebreak while inside a content control, to
      ensure that the starting and ending dummy char stays inside the same
      text node
    
    - let deletion of the dummy character at the end behave the same as the
      start dummy character: if trying to delete that single character, then
      just move the cursor, don't delete it
    
    - reject document insertion in the middle of a content control, similar
      to input fields
    
    Change-Id: I9b54ef50261e6b17f38eadadacfe1e1111199e96
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/134239
    Tested-by: Jenkins
    Reviewed-by: Miklos Vajna <vmik...@collabora.com>

diff --git a/sw/inc/crsrsh.hxx b/sw/inc/crsrsh.hxx
index a34e02d45ead..2dd27529810a 100644
--- a/sw/inc/crsrsh.hxx
+++ b/sw/inc/crsrsh.hxx
@@ -717,6 +717,7 @@ public:
         const bool bIncludeInputFieldAtStart );
     SwField* GetCurField( const bool bIncludeInputFieldAtStart = false ) const;
     bool CursorInsideInputField() const;
+    bool CursorInsideContentControl() const;
     static bool PosInsideInputField( const SwPosition& rPos );
     bool DocPtInsideInputField( const Point& rDocPt ) const;
     static sal_Int32 StartOfInputFieldAtPos( const SwPosition& rPos );
diff --git a/sw/inc/ndtxt.hxx b/sw/inc/ndtxt.hxx
index 1320cb23b9bc..a2ca71ea197c 100644
--- a/sw/inc/ndtxt.hxx
+++ b/sw/inc/ndtxt.hxx
@@ -403,6 +403,14 @@ public:
         const sal_Int32 nIndex,
         const sal_uInt16 nWhich = RES_TXTATR_END ) const;
 
+    /**
+     * Get the text attribute of an end dummy character at nIndex. Return the 
attribute only in
+     * case its which id is nWhich.
+     *
+     * Note that the position of the end dummy character is one less than the 
end of the attribute.
+     */
+    SwTextAttr* GetTextAttrForEndCharAt(sal_Int32 nIndex, sal_uInt16 nWhich) 
const;
+
     SwTextField* GetFieldTextAttrAt(
         const sal_Int32 nIndex,
         const bool bIncludeInputFieldAtStart = false ) const;
diff --git a/sw/qa/core/crsr/crsr.cxx b/sw/qa/core/crsr/crsr.cxx
index e95d0d541c12..882f9b6bcbab 100644
--- a/sw/qa/core/crsr/crsr.cxx
+++ b/sw/qa/core/crsr/crsr.cxx
@@ -23,6 +23,7 @@
 #include <docsh.hxx>
 #include <unotxdoc.hxx>
 #include <wrtsh.hxx>
+#include <ndtxt.hxx>
 
 constexpr OUStringLiteral DATA_DIRECTORY = u"/sw/qa/core/crsr/data/";
 
@@ -104,6 +105,37 @@ CPPUNIT_TEST_FIXTURE(SwCoreCrsrTest, 
testSelAllStartsWithTable)
     CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(0), 
pDoc->GetTableFrameFormatCount(/*bUsed=*/true));
 }
 
+CPPUNIT_TEST_FIXTURE(SwCoreCrsrTest, testContentControlLineBreak)
+{
+    // Given a document with a (rich text) content control:
+    SwDoc* pDoc = createSwDoc();
+    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);
+
+    // When pressing "enter" in the middle of that content control:
+    SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell();
+    pWrtShell->SttEndDoc(/*bStt=*/true);
+    // Go after "t".
+    pWrtShell->Right(CRSR_SKIP_CHARS, /*bSelect=*/false, 2, 
/*bBasicCall=*/false);
+    dispatchCommand(mxComponent, ".uno:InsertPara", {});
+
+    // Then make sure that we only insert a line break, not a new paragraph:
+    SwTextNode* pTextNode = 
pWrtShell->GetCursor()->GetMark()->nNode.GetNode().GetTextNode();
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: t\nest
+    // - Actual  : est
+    // i.e. a new paragraph was inserted, which is not allowed for inline 
content controls.
+    CPPUNIT_ASSERT_EQUAL(OUString("t\nest"), 
pTextNode->GetExpandText(pWrtShell->GetLayout()));
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/qa/core/doc/doc.cxx b/sw/qa/core/doc/doc.cxx
index d370d89baefc..520041f44562 100644
--- a/sw/qa/core/doc/doc.cxx
+++ b/sw/qa/core/doc/doc.cxx
@@ -225,6 +225,36 @@ CPPUNIT_TEST_FIXTURE(SwCoreDocTest, 
testImageHyperlinkStyle)
     CPPUNIT_ASSERT_EQUAL(aExpected, aActual);
 }
 
+CPPUNIT_TEST_FIXTURE(SwCoreDocTest, testContentControlDelete)
+{
+    // Given a document with a content control:
+    SwDoc* pDoc = createSwDoc();
+    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);
+
+    // When deleting the dummy character at the end of the content control:
+    SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell();
+    pWrtShell->SttEndDoc(/*bStt=*/false);
+    pWrtShell->DelLeft();
+
+    // Then make sure that we only enter the content control, to be consistent 
with the start dummy
+    // character:
+    SwTextNode* pTextNode = 
pWrtShell->GetCursor()->GetMark()->nNode.GetNode().GetTextNode();
+    // Without the accompanying fix in place, this test would have failed with:
+    // - Expected: ^Atest^A
+    // - Actual  : ^Atest
+    // i.e. the end dummy character got deleted, but not the first one, which 
is inconsistent.
+    CPPUNIT_ASSERT_EQUAL(OUString("\x0001test\x0001"), pTextNode->GetText());
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/qa/core/unocore/unocore.cxx b/sw/qa/core/unocore/unocore.cxx
index ee058645c951..166a24ae9128 100644
--- a/sw/qa/core/unocore/unocore.cxx
+++ b/sw/qa/core/unocore/unocore.cxx
@@ -14,6 +14,7 @@
 #include <com/sun/star/text/XTextFrame.hpp>
 #include <com/sun/star/text/XTextViewCursorSupplier.hpp>
 #include <com/sun/star/text/XDependentTextField.hpp>
+#include <com/sun/star/document/XDocumentInsertable.hpp>
 
 #include <comphelper/propertyvalue.hxx>
 #include <comphelper/sequenceashashmap.hxx>
@@ -516,6 +517,32 @@ CPPUNIT_TEST_FIXTURE(SwCoreUnocoreTest, 
testContentControlDropdown)
     CPPUNIT_ASSERT_EQUAL(OUString("R"), aListItems[0].m_aValue);
 }
 
+CPPUNIT_TEST_FIXTURE(SwCoreUnocoreTest, 
testInsertFileInContentControlException)
+{
+    // Given a document with a content control:
+    createSwDoc();
+    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);
+
+    // Reject inserting a document inside the content control:
+    xCursor->goLeft(1, false);
+    OUString aURL(m_directories.getURLFromSrc(DATA_DIRECTORY) + 
"tdf119081.odt");
+    uno::Reference<document::XDocumentInsertable> xInsertable(xCursor, 
uno::UNO_QUERY);
+    CPPUNIT_ASSERT_THROW(xInsertable->insertDocumentFromURL(aURL, {}), 
uno::RuntimeException);
+
+    // Accept inserting a document outside the content control:
+    xCursor->goRight(1, false);
+    xInsertable->insertDocumentFromURL(aURL, {});
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/qa/extras/ww8export/ww8export2.cxx 
b/sw/qa/extras/ww8export/ww8export2.cxx
index d6523f172f57..40311d9e5298 100644
--- a/sw/qa/extras/ww8export/ww8export2.cxx
+++ b/sw/qa/extras/ww8export/ww8export2.cxx
@@ -1071,6 +1071,34 @@ DECLARE_WW8EXPORT_TEST(testTdf118412, "tdf118412.doc")
     CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(1251), nBottomMargin);
 }
 
+CPPUNIT_TEST_FIXTURE(Test, testContentControlExport)
+{
+    // Given a document with a (rich text) content control:
+    mxComponent = loadFromDesktop("private:factory/swriter");
+    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);
+
+    // When saving that document to DOC and loading it back:
+    reload("MS Word 97", "");
+
+    // Then make sure the dummy character at the end is filtered out:
+    OUString aBodyText = getBodyText();
+    // Without the accompanying fix in place, this test would have failed:
+    // - Expected: test
+    // - Actual  : test<space>
+    // i.e. the CH_TXTATR_BREAKWORD at the end was written, then the import 
replaced that with a
+    // space.
+    CPPUNIT_ASSERT_EQUAL(OUString("test"), aBodyText);
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/core/crsr/crstrvl.cxx b/sw/source/core/crsr/crstrvl.cxx
index ac48ef29b9c6..bbb20f94ea47 100644
--- a/sw/source/core/crsr/crstrvl.cxx
+++ b/sw/source/core/crsr/crstrvl.cxx
@@ -1002,6 +1002,27 @@ bool SwCursorShell::CursorInsideInputField() const
     return false;
 }
 
+bool SwCursorShell::CursorInsideContentControl() const
+{
+    for (SwPaM& rCursor : GetCursor()->GetRingContainer())
+    {
+        const SwPosition* pStart = rCursor.Start();
+        SwTextNode* pTextNode = pStart->nNode.GetNode().GetTextNode();
+        if (!pTextNode)
+        {
+            continue;
+        }
+
+        sal_Int32 nIndex = pStart->nContent.GetIndex();
+        if (pTextNode->GetTextAttrAt(nIndex, RES_TXTATR_CONTENTCONTROL, 
SwTextNode::PARENT))
+        {
+            return true;
+        }
+    }
+
+    return false;
+}
+
 bool SwCursorShell::PosInsideInputField( const SwPosition& rPos )
 {
     return dynamic_cast<const SwTextInputField*>(GetTextFieldAtPos( &rPos, 
false )) != nullptr;
diff --git a/sw/source/core/doc/DocumentContentOperationsManager.cxx 
b/sw/source/core/doc/DocumentContentOperationsManager.cxx
index a979ebddb951..8fc4525c8e2e 100644
--- a/sw/source/core/doc/DocumentContentOperationsManager.cxx
+++ b/sw/source/core/doc/DocumentContentOperationsManager.cxx
@@ -555,12 +555,22 @@ namespace sw
                             // at the end, so no need to check in nStartNode
                             if (n == nEndNode && !isOnlyFieldmarks)
                             {
-                                SwTextAttr const*const 
pAttr(rTextNode.GetTextAttrForCharAt(i));
+                                SwTextAttr const* 
pAttr(rTextNode.GetTextAttrForCharAt(i));
                                 if (pAttr && pAttr->End() && (nEnd  < 
*pAttr->End()))
                                 {
                                     assert(pAttr->HasDummyChar());
                                     rBreaks.emplace_back(n, i);
                                 }
+
+                                if (!pAttr)
+                                {
+                                    // See if this is an end dummy character 
for a content control.
+                                    pAttr = 
rTextNode.GetTextAttrForEndCharAt(i, RES_TXTATR_CONTENTCONTROL);
+                                    if (pAttr && (nStart > pAttr->GetStart()))
+                                    {
+                                        rBreaks.emplace_back(n, i);
+                                    }
+                                }
                             }
                             break;
                         }
diff --git a/sw/source/core/txtnode/ndtxt.cxx b/sw/source/core/txtnode/ndtxt.cxx
index 1a3d53c4b4aa..1932ed689276 100644
--- a/sw/source/core/txtnode/ndtxt.cxx
+++ b/sw/source/core/txtnode/ndtxt.cxx
@@ -3090,6 +3090,28 @@ SwTextAttr * SwTextNode::GetTextAttrForCharAt(
     return nullptr;
 }
 
+SwTextAttr* SwTextNode::GetTextAttrForEndCharAt(sal_Int32 nIndex, sal_uInt16 
nWhich) const
+{
+    SwTextAttr* pAttr = GetTextAttrAt(nIndex, nWhich, SwTextNode::EXPAND);
+    if (!pAttr)
+    {
+        return nullptr;
+    }
+
+    if (!pAttr->End())
+    {
+        return nullptr;
+    }
+
+    // The start-end range covers the end dummy character.
+    if (*pAttr->End() - 1 != nIndex)
+    {
+        return nullptr;
+    }
+
+    return pAttr;
+}
+
 namespace
 {
 
diff --git a/sw/source/core/unocore/unocrsrhelper.cxx 
b/sw/source/core/unocore/unocrsrhelper.cxx
index b97ee8910e07..04b93de0d003 100644
--- a/sw/source/core/unocore/unocrsrhelper.cxx
+++ b/sw/source/core/unocore/unocrsrhelper.cxx
@@ -1033,6 +1033,12 @@ void InsertFile(SwUnoCursor* pUnoCursor, const OUString& 
rURL,
         {
             throw uno::RuntimeException("cannot insert file inside input 
field");
         }
+
+        if 
(pTextNode->GetTextAttrAt(pUnoCursor->GetPoint()->nContent.GetIndex(),
+                                     RES_TXTATR_CONTENTCONTROL, 
SwTextNode::PARENT))
+        {
+            throw uno::RuntimeException("cannot insert file inside content 
controls");
+        }
     }
 
     std::unique_ptr<SfxMedium> pMed;
diff --git a/sw/source/filter/ww8/wrtw8nds.cxx 
b/sw/source/filter/ww8/wrtw8nds.cxx
index 63786a219948..3ff1326c24a7 100644
--- a/sw/source/filter/ww8/wrtw8nds.cxx
+++ b/sw/source/filter/ww8/wrtw8nds.cxx
@@ -85,6 +85,7 @@
 #include <oox/export/vmlexport.hxx>
 #include <sal/log.hxx>
 #include <comphelper/propertysequence.hxx>
+#include <comphelper/string.hxx>
 
 #include "sprmids.hxx"
 
@@ -1788,6 +1789,12 @@ OUString SwWW8AttrIter::GetSnippet(const OUString &rStr, 
sal_Int32 nCurrentPos,
     aSnippet = aSnippet.replace(0x0A, 0x0B);
     aSnippet = aSnippet.replace(CHAR_HARDHYPHEN, 0x1e);
     aSnippet = aSnippet.replace(CHAR_SOFTHYPHEN, 0x1f);
+    // Ignore the dummy character at the end of content controls.
+    static sal_Unicode const aForbidden[] = {
+        CH_TXTATR_BREAKWORD,
+        0
+    };
+    aSnippet = comphelper::string::removeAny(aSnippet, aForbidden);
 
     m_rExport.m_aCurrentCharPropStarts.push( nCurrentPos );
     const SfxPoolItem &rItem = GetItem(RES_CHRATR_CASEMAP);
diff --git a/sw/source/uibase/docvw/edtwin.cxx 
b/sw/source/uibase/docvw/edtwin.cxx
index eeb8554ccfcc..12ade08df6d6 100644
--- a/sw/source/uibase/docvw/edtwin.cxx
+++ b/sw/source/uibase/docvw/edtwin.cxx
@@ -1876,7 +1876,8 @@ KEYINPUT_CHECKTABLE_INSDEL:
                 case KEY_RETURN:
                 {
                     if ( !rSh.HasReadonlySel()
-                         && !rSh.CursorInsideInputField() )
+                         && !rSh.CursorInsideInputField()
+                         && !rSh.CursorInsideContentControl() )
                     {
                         const SelectionType nSelectionType = 
rSh.GetSelectionType();
                         if(nSelectionType & SelectionType::Ole)
diff --git a/sw/source/uibase/shells/textsh.cxx 
b/sw/source/uibase/shells/textsh.cxx
index 6902bcb25529..ef12a8a9908f 100644
--- a/sw/source/uibase/shells/textsh.cxx
+++ b/sw/source/uibase/shells/textsh.cxx
@@ -197,7 +197,7 @@ void SwTextShell::ExecInsert(SfxRequest &rReq)
 
     case FN_INSERT_BREAK:
         {
-            if( !rSh.CursorInsideInputField() )
+            if (!rSh.CursorInsideInputField() && 
!rSh.CursorInsideContentControl())
             {
                 rSh.SplitNode();
             }

Reply via email to