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(); }