desktop/source/lib/init.cxx | 44 ++---------- include/vcl/ITiledRenderable.hxx | 3 sw/inc/cmdid.h | 3 sw/inc/unotxdoc.hxx | 3 sw/qa/uibase/shells/shells.cxx | 125 ++++++++++++++++++++++++++++++++++++ sw/qa/uibase/uno/uno.cxx | 82 +++++++++++++++++++++-- sw/sdi/_textsh.sdi | 12 +++ sw/sdi/swriter.sdi | 28 ++++++++ sw/source/uibase/shells/textfld.cxx | 66 +++++++++++++++++++ sw/source/uibase/shells/textsh1.cxx | 93 ++++++++++++++++++++++++++ sw/source/uibase/uno/loktxdoc.cxx | 89 +++++++++++++++++++++++++ 11 files changed, 502 insertions(+), 46 deletions(-)
New commits: commit 06948b0792d898962d57bbcc2897c99f611f183e Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Fri Jan 6 10:08:00 2023 +0100 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Mon Jan 30 10:53:45 2023 +0100 sw: add a new .uno:UpdateTextFormField UNO command It is possible to update all fieldsmarks (of a certion type, of a certain field command prefix), but one can't update the fieldmark under the cursor, which is needed for Zotero citation clusters. To make this more complex, insertion inside an existing fieldmark is explicitly not wanted, see commit a178a2ac6df8dc63a7ab8d4a19b90ae8a17baca4 (sw UI: fix crash on inserting a fieldmark inside a fieldmark, 2023-01-02). Fix the problem by adding a new .uno:UpdateTextFormField UNO command that can update the (innermost) fieldmark under the current cursor. The uno command is intentionally hidden from the customize dialog since it only makes sense to invoke it from a macro / API with parameters, not interactively. (cherry picked from commit 337416dafb66ed8f930d2d69e83fae438fc85f3c) Conflicts: sw/qa/uibase/shells/shells.cxx Change-Id: I46fc4f701a20839945d765eb13aec7362ab83788 diff --git a/sw/inc/cmdid.h b/sw/inc/cmdid.h index dbb3a259b422..6f51a0e5e855 100644 --- a/sw/inc/cmdid.h +++ b/sw/inc/cmdid.h @@ -314,6 +314,7 @@ class SwUINumRuleItem; #define FN_PROTECT_BOOKMARKS (FN_INSERT2 + 27) #define FN_UPDATE_TEXT_FORMFIELDS (FN_INSERT2 + 28) +#define FN_UPDATE_TEXT_FORMFIELD (FN_INSERT2 + 29) // clipboard table content #define FN_PASTE_NESTED_TABLE (FN_INSERT2 + 30) /* instead of the cell-by-cell copy between source and target tables */ diff --git a/sw/qa/uibase/shells/shells.cxx b/sw/qa/uibase/shells/shells.cxx index 19ed03bbfa56..610890c15312 100644 --- a/sw/qa/uibase/shells/shells.cxx +++ b/sw/qa/uibase/shells/shells.cxx @@ -586,6 +586,68 @@ CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testUpdateBookmarks) CPPUNIT_ASSERT_EQUAL(OUString("Anew result 1Cnew result 2E"), aActual); } +CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testUpdateFieldmark) +{ + // Given a document with a fieldmark: + createSwDoc(); + uno::Sequence<css::beans::PropertyValue> aArgs = { + comphelper::makePropertyValue("FieldType", uno::Any(OUString(ODF_UNHANDLED))), + comphelper::makePropertyValue("FieldCommand", + uno::Any(OUString("ADDIN ZOTERO_ITEM old command 1"))), + comphelper::makePropertyValue("FieldResult", uno::Any(OUString("old result 1"))), + }; + dispatchCommand(mxComponent, ".uno:TextFormField", aArgs); + + // When updating that fieldmark to have new field command & result: + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + pWrtShell->SttEndDoc(/*bStt=*/false); + pWrtShell->Left(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + std::vector<beans::PropertyValue> aArgsVec = comphelper::JsonToPropertyValues(R"json( +{ + "FieldType": { + "type": "string", + "value": "vnd.oasis.opendocument.field.UNHANDLED" + }, + "FieldCommandPrefix": { + "type": "string", + "value": "ADDIN ZOTERO_ITEM" + }, + "Field": { + "type": "[]com.sun.star.beans.PropertyValue", + "value": { + "FieldType": { + "type": "string", + "value": "vnd.oasis.opendocument.field.UNHANDLED" + }, + "FieldCommand": { + "type": "string", + "value": "ADDIN ZOTERO_ITEM new command 1" + }, + "FieldResult": { + "type": "string", + "value": "new result 1" + } + } + } +} +)json"); + aArgs = comphelper::containerToSequence(aArgsVec); + dispatchCommand(mxComponent, ".uno:UpdateTextFormField", aArgs); + + // Then make sure that the document text is updated accordingly: + SwCursor* pCursor = pWrtShell->GetCursor(); + OUString aActual = pCursor->Start()->GetNode().GetTextNode()->GetText(); + static sal_Unicode const aForbidden[] + = { CH_TXT_ATR_FIELDSTART, CH_TXT_ATR_FIELDSEP, CH_TXT_ATR_FIELDEND, 0 }; + aActual = comphelper::string::removeAny(aActual, aForbidden); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: new result 1 + // - Actual : old result 1 + // i.e. the document text was not updated. + CPPUNIT_ASSERT_EQUAL(OUString("new result 1"), aActual); +} + CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/sdi/_textsh.sdi b/sw/sdi/_textsh.sdi index 63a99f3ea9f7..ca01f0afe7d1 100644 --- a/sw/sdi/_textsh.sdi +++ b/sw/sdi/_textsh.sdi @@ -1802,6 +1802,12 @@ interface BaseText StateMethod = StateField ; ] + FN_UPDATE_TEXT_FORMFIELD + [ + ExecMethod = ExecField ; + StateMethod = StateField ; + ] + FN_PROTECT_FIELDS [ ExecMethod = Execute ; diff --git a/sw/sdi/swriter.sdi b/sw/sdi/swriter.sdi index a0380a2d1944..72330e643039 100644 --- a/sw/sdi/swriter.sdi +++ b/sw/sdi/swriter.sdi @@ -8316,6 +8316,20 @@ SfxVoidItem TextFormFields FN_UPDATE_TEXT_FORMFIELDS GroupId = SfxGroupId::Controls; ] +SfxVoidItem UpdateTextFormField FN_UPDATE_TEXT_FORMFIELD +(SfxStringItem FieldType FN_PARAM_1, SfxStringItem FieldCommandPrefix FN_PARAM_2, SfxUnoAnyItem Field FN_PARAM_3) +[ + AutoUpdate = TRUE, + FastCall = FALSE, + ReadOnlyDoc = FALSE, + Toggle = FALSE, + Container = FALSE, + RecordAbsolute = FALSE, + RecordPerSet; + + GroupId = SfxGroupId::Controls; +] + SfxVoidItem CheckBoxFormField FN_INSERT_CHECKBOX_FORMFIELD [ diff --git a/sw/source/uibase/shells/textfld.cxx b/sw/source/uibase/shells/textfld.cxx index b5decbd8b13a..dffea8e924ee 100644 --- a/sw/source/uibase/shells/textfld.cxx +++ b/sw/source/uibase/shells/textfld.cxx @@ -1010,6 +1010,72 @@ FIELD_INSERT: }); rReq.Done(); } + break; + case FN_UPDATE_TEXT_FORMFIELD: + { + OUString aFieldType; + const SfxStringItem* pFieldType = rReq.GetArg<SfxStringItem>(FN_PARAM_1); + if (pFieldType) + { + aFieldType = pFieldType->GetValue(); + } + OUString aFieldCommandPrefix; + const SfxStringItem* pFieldCommandPrefix = rReq.GetArg<SfxStringItem>(FN_PARAM_2); + if (pFieldCommandPrefix) + { + aFieldCommandPrefix = pFieldCommandPrefix->GetValue(); + } + uno::Sequence<beans::PropertyValue> aField; + const SfxUnoAnyItem* pFields = rReq.GetArg<SfxUnoAnyItem>(FN_PARAM_3); + if (pFields) + { + pFields->GetValue() >>= aField; + } + + IDocumentMarkAccess& rIDMA = *rSh.getIDocumentMarkAccess(); + SwPosition& rCursor = *rSh.GetCursor()->GetPoint(); + sw::mark::IFieldmark* pFieldmark = rIDMA.getFieldmarkFor(rCursor); + if (!pFieldmark) + { + break; + } + + if (pFieldmark->GetFieldname() != aFieldType) + { + break; + } + + auto itParam = pFieldmark->GetParameters()->find(ODF_CODE_PARAM); + if (itParam == pFieldmark->GetParameters()->end()) + { + break; + } + + OUString aCommand; + itParam->second >>= aCommand; + if (!aCommand.startsWith(aFieldCommandPrefix)) + { + break; + } + + rSh.GetDoc()->GetIDocumentUndoRedo().StartUndo(SwUndoId::INSERT_FORM_FIELD, nullptr); + rSh.StartAction(); + comphelper::SequenceAsHashMap aMap(aField); + itParam->second = aMap["FieldCommand"]; + SwPaM aPaM(pFieldmark->GetMarkPos(), pFieldmark->GetOtherMarkPos()); + aPaM.Normalize(); + // Skip field start & separator. + aPaM.GetPoint()->AdjustContent(2); + // Skip field end. + aPaM.GetMark()->AdjustContent(-1); + rSh.GetDoc()->getIDocumentContentOperations().DeleteAndJoin(aPaM); + OUString aFieldResult; + aMap["FieldResult"] >>= aFieldResult; + SwTranslateHelper::PasteHTMLToPaM(rSh, &aPaM, aFieldResult.toUtf8(), true); + + rSh.EndAction(); + rSh.GetDoc()->GetIDocumentUndoRedo().EndUndo(SwUndoId::INSERT_FORM_FIELD, nullptr); + } break; default: OSL_FAIL("wrong dispatcher"); commit 8534ad5782908cebdb710dbefc2e84c85997bdc2 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Thu Jan 5 08:13:21 2023 +0100 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Mon Jan 30 10:45:39 2023 +0100 sw lok: expose field type & command of fieldmark under cursor It was possible to get the properties of all fieldmarks, but you could not get the properties of the fieldmark under the current cursor. Getting the properties of the current fieldmark is useful for Zotero: if we already have a citation and want to insert one more, then we should turn the current citation into a citation cluster. Fix the problem by adding API similar to what commit 24219cc1e9829f82a533667aef0f51b6a7df6fc2 (sw lok, .uno:TextFormFields: expose field code of fieldmarks, 2022-11-25), did: but here we return properties of the innermost fieldmark, not all fieldmarks. Also introduce a ITiledRenderable::supportsCommandValues(), this way next time a command value getter is added in sw/sc/sd, no need to touch desktop/ anymore. (cherry picked from commit bb20dee2ef1b0804065e1cda2c834d257fdd90ed) Conflicts: sw/source/uibase/uno/loktxdoc.cxx Change-Id: I7f1a7064307034a18527ab5e985d2eac56807cb7 diff --git a/desktop/source/lib/init.cxx b/desktop/source/lib/init.cxx index 13fe2c779890..5d67ac352926 100644 --- a/desktop/source/lib/init.cxx +++ b/desktop/source/lib/init.cxx @@ -5752,12 +5752,13 @@ static char* doc_getCommandValues(LibreOfficeKitDocument* pThis, const char* pCo static constexpr OStringLiteral aSheetGeometryData(".uno:SheetGeometryData"); static constexpr OStringLiteral aCellCursor(".uno:CellCursor"); static constexpr OStringLiteral aFontSubset(".uno:FontSubset&name="); - static const std::initializer_list<std::u16string_view> vForward = { - u"TextFormFields", - u"SetDocumentProperties", - u"Bookmarks", - u"Fields" - }; + + ITiledRenderable* pDoc = getTiledRenderable(pThis); + if (!pDoc) + { + SetLastExceptionMsg("Document doesn't support tiled rendering"); + return nullptr; + } if (!strcmp(pCommand, ".uno:LanguageStatus")) { @@ -5801,13 +5802,6 @@ static char* doc_getCommandValues(LibreOfficeKitDocument* pThis, const char* pCo } else if (o3tl::starts_with(aCommand, aViewRowColumnHeaders)) { - ITiledRenderable* pDoc = getTiledRenderable(pThis); - if (!pDoc) - { - SetLastExceptionMsg("Document doesn't support tiled rendering"); - return nullptr; - } - tools::Rectangle aRectangle; if (aCommand.size() > o3tl::make_unsigned(aViewRowColumnHeaders.getLength())) { @@ -5853,13 +5847,6 @@ static char* doc_getCommandValues(LibreOfficeKitDocument* pThis, const char* pCo } else if (o3tl::starts_with(aCommand, aSheetGeometryData)) { - ITiledRenderable* pDoc = getTiledRenderable(pThis); - if (!pDoc) - { - SetLastExceptionMsg("Document doesn't support tiled rendering"); - return nullptr; - } - bool bColumns = true; bool bRows = true; bool bSizes = true; @@ -5919,12 +5906,6 @@ static char* doc_getCommandValues(LibreOfficeKitDocument* pThis, const char* pCo } else if (o3tl::starts_with(aCommand, aCellCursor)) { - ITiledRenderable* pDoc = getTiledRenderable(pThis); - if (!pDoc) - { - SetLastExceptionMsg("Document doesn't support tiled rendering"); - return nullptr; - } // Ignore command's deprecated parameters. tools::JsonWriter aJsonWriter; pDoc->getCellCursor(aJsonWriter); @@ -5934,17 +5915,8 @@ static char* doc_getCommandValues(LibreOfficeKitDocument* pThis, const char* pCo { return getFontSubset(aCommand.substr(aFontSubset.getLength())); } - else if (std::find(vForward.begin(), vForward.end(), - INetURLObject(OUString::fromUtf8(aCommand)).GetURLPath()) - != vForward.end()) + else if (pDoc->supportsCommandValues(INetURLObject(OUString::fromUtf8(aCommand)).GetURLPath())) { - ITiledRenderable* pDoc = getTiledRenderable(pThis); - if (!pDoc) - { - SetLastExceptionMsg("Document doesn't support tiled rendering"); - return nullptr; - } - tools::JsonWriter aJsonWriter; pDoc->getCommandValues(aJsonWriter, aCommand); return aJsonWriter.extractData(); diff --git a/include/vcl/ITiledRenderable.hxx b/include/vcl/ITiledRenderable.hxx index 10d0ea87752c..7d3f4b7aaf33 100644 --- a/include/vcl/ITiledRenderable.hxx +++ b/include/vcl/ITiledRenderable.hxx @@ -369,6 +369,9 @@ public: */ virtual void setPaintTextEdit(bool) {} + /// Decides if it's OK to call getCommandValues(rCommand). + virtual bool supportsCommandValues(std::u16string_view /*rCommand*/) { return false; } + /// Returns a json mapping of the possible values for the given command. virtual void getCommandValues(tools::JsonWriter& /*rJsonWriter*/, std::string_view /*rCommand*/) { diff --git a/sw/inc/unotxdoc.hxx b/sw/inc/unotxdoc.hxx index 150ea15af498..a7bbcb776f44 100644 --- a/sw/inc/unotxdoc.hxx +++ b/sw/inc/unotxdoc.hxx @@ -462,6 +462,9 @@ public: /// @see vcl::ITiledRenderable::getCommandValues(). void getCommandValues(tools::JsonWriter& rJsonWriter, std::string_view rCommand) override; + /// @see vcl::ITiledRenderable::supportsCommandValues(). + bool supportsCommandValues(std::u16string_view rCommand) override; + void Invalidate(); void Reactivate(SwDocShell* pNewDocShell); SwXDocumentPropertyHelper * GetPropertyHelper (); diff --git a/sw/qa/uibase/uno/uno.cxx b/sw/qa/uibase/uno/uno.cxx index 8b9f1f7aa568..3eb830ec8c55 100644 --- a/sw/qa/uibase/uno/uno.cxx +++ b/sw/qa/uibase/uno/uno.cxx @@ -375,6 +375,44 @@ CPPUNIT_TEST_FIXTURE(SwUibaseUnoTest, testGetFields) aRef.get<std::string>("name")); } +CPPUNIT_TEST_FIXTURE(SwUibaseUnoTest, testGetTextFormField) +{ + // Given a document with a fieldmark: + createSwDoc(); + uno::Sequence<css::beans::PropertyValue> aArgs = { + comphelper::makePropertyValue("FieldType", uno::Any(OUString(ODF_UNHANDLED))), + comphelper::makePropertyValue("FieldCommand", + uno::Any(OUString("ADDIN ZOTERO_ITEM foo bar"))), + comphelper::makePropertyValue("FieldResult", uno::Any(OUString("result"))), + }; + dispatchCommand(mxComponent, ".uno:TextFormField", aArgs); + + // When stepping into the fieldmark with the cursor and getting the command value for + // uno:TextFormField: + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + pWrtShell->SttEndDoc(/*bStt=*/false); + pWrtShell->Left(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + tools::JsonWriter aJsonWriter; + std::string_view aCommand(".uno:TextFormField?type=vnd.oasis.opendocument.field.UNHANDLED&" + "commandPrefix=ADDIN%20ZOTERO_ITEM"); + auto pXTextDocument = dynamic_cast<SwXTextDocument*>(mxComponent.get()); + pXTextDocument->getCommandValues(aJsonWriter, aCommand); + + // Then make sure we find the inserted fieldmark: + std::unique_ptr<char[], o3tl::free_delete> pJSON(aJsonWriter.extractData()); + std::stringstream aStream(pJSON.get()); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + // Without the accompanying fix in place, this test would have failed with: + // - No such node (type) + // i.e. the returned JSON was just an empty object. + CPPUNIT_ASSERT_EQUAL(std::string("vnd.oasis.opendocument.field.UNHANDLED"), + aTree.get<std::string>("type")); + CPPUNIT_ASSERT_EQUAL(std::string("ADDIN ZOTERO_ITEM foo bar"), + aTree.get<std::string>("command")); +} + CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/uibase/uno/loktxdoc.cxx b/sw/source/uibase/uno/loktxdoc.cxx index fd6dbfd153c6..62e6244bd4a4 100644 --- a/sw/source/uibase/uno/loktxdoc.cxx +++ b/sw/source/uibase/uno/loktxdoc.cxx @@ -37,6 +37,7 @@ #include <fmtrfmrk.hxx> #include <txtrfmrk.hxx> #include <ndtxt.hxx> +#include <wrtsh.hxx> using namespace ::com::sun::star; @@ -47,7 +48,7 @@ namespace /// Parameters: /// /// - type: e.g. ODF_UNHANDLED -/// - commandPrefix: field comment prefix not not return all fieldmarks +/// - commandPrefix: field command prefix to not return all fieldmarks void GetTextFormFields(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell, const std::map<OUString, OUString>& rArguments) { @@ -98,6 +99,60 @@ void GetTextFormFields(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell, } } +/// Implements getCommandValues(".uno:TextFormField"). +/// +/// Parameters: +/// +/// - type: e.g. ODF_UNHANDLED +/// - commandPrefix: field command prefix to not return all fieldmarks +void GetTextFormField(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell, + const std::map<OUString, OUString>& rArguments) +{ + OUString aType; + OUString aCommandPrefix; + auto it = rArguments.find("type"); + if (it != rArguments.end()) + { + aType = it->second; + } + + it = rArguments.find("commandPrefix"); + if (it != rArguments.end()) + { + aCommandPrefix = it->second; + } + + IDocumentMarkAccess& rIDMA = *pDocShell->GetDoc()->getIDocumentMarkAccess(); + SwWrtShell* pWrtShell = pDocShell->GetWrtShell(); + SwPosition& rCursor = *pWrtShell->GetCursor()->GetPoint(); + sw::mark::IFieldmark* pFieldmark = rIDMA.getFieldmarkFor(rCursor); + if (!pFieldmark) + { + return; + } + + if (pFieldmark->GetFieldname() != aType) + { + return; + } + + auto itParam = pFieldmark->GetParameters()->find(ODF_CODE_PARAM); + if (itParam == pFieldmark->GetParameters()->end()) + { + return; + } + + OUString aCommand; + itParam->second >>= aCommand; + if (!aCommand.startsWith(aCommandPrefix)) + { + return; + } + + rJsonWriter.put("type", aType); + rJsonWriter.put("command", aCommand); +} + /// Implements getCommandValues(".uno:SetDocumentProperties"). /// /// Parameters: @@ -236,11 +291,21 @@ void GetFields(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell, } } +bool SwXTextDocument::supportsCommandValues(std::u16string_view rCommand) +{ + static const std::initializer_list<std::u16string_view> vForward + = { u"TextFormFields", u"TextFormField", u"SetDocumentProperties", u"Bookmarks", + u"Fields" }; + + return std::find(vForward.begin(), vForward.end(), rCommand) != vForward.end(); +} + void SwXTextDocument::getCommandValues(tools::JsonWriter& rJsonWriter, std::string_view rCommand) { std::map<OUString, OUString> aMap; static constexpr OStringLiteral aTextFormFields(".uno:TextFormFields"); + static constexpr OStringLiteral aTextFormField(".uno:TextFormField"); static constexpr OStringLiteral aSetDocumentProperties(".uno:SetDocumentProperties"); static constexpr OStringLiteral aBookmarks(".uno:Bookmarks"); static constexpr OStringLiteral aFields(".uno:Fields"); @@ -271,6 +336,10 @@ void SwXTextDocument::getCommandValues(tools::JsonWriter& rJsonWriter, std::stri { GetTextFormFields(rJsonWriter, m_pDocShell, aMap); } + if (o3tl::starts_with(rCommand, aTextFormField)) + { + GetTextFormField(rJsonWriter, m_pDocShell, aMap); + } else if (o3tl::starts_with(rCommand, aSetDocumentProperties)) { GetDocumentProperties(rJsonWriter, m_pDocShell, aMap); commit dd8a5613f2b19e514a028464ce95e8022d0798a4 Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Fri Jan 6 13:35:21 2023 +0100 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Mon Jan 30 10:44:03 2023 +0100 sw lok: get all refmarks: sort the refmarks array This was using the order used by GetItemSurrogates(), which is mostly by pointer address. We know that these pool items have a text attribute, which have a node and a content index, sort by that. In theory two refmark may start at the same doc model position, but that's never the case when using Zotero, so don't worry about that for now. For a document with 5 refmarks, the original order I got was 2-3-5-4-1, now it's property 1-2-3-4-5. (cherry picked from commit 5a2ee5ba893b6b8f4e7fd6623b7f10faf0bda509) Conflicts: sw/source/uibase/uno/loktxdoc.cxx Change-Id: I2768845414bd36afca91ec02a0f3364c246ddfd9 diff --git a/sw/qa/uibase/uno/uno.cxx b/sw/qa/uibase/uno/uno.cxx index 5a54d5843ff2..8b9f1f7aa568 100644 --- a/sw/qa/uibase/uno/uno.cxx +++ b/sw/qa/uibase/uno/uno.cxx @@ -322,13 +322,21 @@ CPPUNIT_TEST_FIXTURE(SwUibaseUnoTest, testGetFields) { // Given a document with a refmark: createSwDoc(); - uno::Sequence<css::beans::PropertyValue> aArgs = { - comphelper::makePropertyValue("TypeName", uno::Any(OUString("SetRef"))), - comphelper::makePropertyValue( - "Name", uno::Any(OUString("ZOTERO_ITEM CSL_CITATION {} RNDpyJknp173F"))), - comphelper::makePropertyValue("Content", uno::Any(OUString("mycontent"))), - }; - dispatchCommand(mxComponent, ".uno:InsertField", aArgs); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + OUString aName("ZOTERO_ITEM CSL_CITATION {} "); + for (int i = 0; i < 5; ++i) + { + uno::Sequence<css::beans::PropertyValue> aArgs = { + comphelper::makePropertyValue("TypeName", uno::Any(OUString("SetRef"))), + comphelper::makePropertyValue("Name", uno::Any(aName + OUString::number(i + 1))), + comphelper::makePropertyValue("Content", uno::Any(OUString("mycontent"))), + }; + dispatchCommand(mxComponent, ".uno:InsertField", aArgs); + pWrtShell->SttEndDoc(/*bStt=*/false); + pWrtShell->SplitNode(); + pWrtShell->SttEndDoc(/*bStt=*/false); + } // When getting the refmarks: tools::JsonWriter aJsonWriter; @@ -344,7 +352,27 @@ CPPUNIT_TEST_FIXTURE(SwUibaseUnoTest, testGetFields) // Without the accompanying fix in place, this test would have failed with: // - No such node (setRefs) // i.e. the returned JSON was just empty. - CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aTree.get_child("setRefs").count("")); + CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(5), aTree.get_child("setRefs").count("")); + auto it = aTree.get_child("setRefs").begin(); + boost::property_tree::ptree aRef = (it++)->second; + CPPUNIT_ASSERT_EQUAL(std::string("ZOTERO_ITEM CSL_CITATION {} 1"), + aRef.get<std::string>("name")); + aRef = (it++)->second; + CPPUNIT_ASSERT_EQUAL(std::string("ZOTERO_ITEM CSL_CITATION {} 2"), + aRef.get<std::string>("name")); + aRef = (it++)->second; + // Without the accompanying fix in place, this test would have failed with: + // - Expected: ZOTERO_ITEM CSL_CITATION {} 3 + // - Actual : ZOTERO_ITEM CSL_CITATION {} 4 + // i.e. the output was unsorted. + CPPUNIT_ASSERT_EQUAL(std::string("ZOTERO_ITEM CSL_CITATION {} 3"), + aRef.get<std::string>("name")); + aRef = (it++)->second; + CPPUNIT_ASSERT_EQUAL(std::string("ZOTERO_ITEM CSL_CITATION {} 4"), + aRef.get<std::string>("name")); + aRef = (it++)->second; + CPPUNIT_ASSERT_EQUAL(std::string("ZOTERO_ITEM CSL_CITATION {} 5"), + aRef.get<std::string>("name")); } CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/sw/source/uibase/uno/loktxdoc.cxx b/sw/source/uibase/uno/loktxdoc.cxx index d82fc8388775..fd6dbfd153c6 100644 --- a/sw/source/uibase/uno/loktxdoc.cxx +++ b/sw/source/uibase/uno/loktxdoc.cxx @@ -35,6 +35,8 @@ #include <doc.hxx> #include <docsh.hxx> #include <fmtrfmrk.hxx> +#include <txtrfmrk.hxx> +#include <ndtxt.hxx> using namespace ::com::sun::star; @@ -206,9 +208,23 @@ void GetFields(tools::JsonWriter& rJsonWriter, SwDocShell* pDocShell, SwDoc* pDoc = pDocShell->GetDoc(); tools::ScopedJsonWriterArray aBookmarks = rJsonWriter.startArray("setRefs"); + std::vector<const SwFormatRefMark*> aRefMarks; for (sal_uInt16 i = 0; i < pDoc->GetRefMarks(); ++i) { - const SwFormatRefMark* pRefMark = pDoc->GetRefMark(i); + aRefMarks.push_back(pDoc->GetRefMark(i)); + } + // Sort the refmarks based on their start position. + std::sort(aRefMarks.begin(), aRefMarks.end(), + [](const SwFormatRefMark* pMark1, const SwFormatRefMark* pMark2) -> bool { + const SwTextRefMark* pTextRefMark1 = pMark1->GetTextRefMark(); + const SwTextRefMark* pTextRefMark2 = pMark2->GetTextRefMark(); + SwPosition aPos1(pTextRefMark1->GetTextNode(), pTextRefMark1->GetStart()); + SwPosition aPos2(pTextRefMark2->GetTextNode(), pTextRefMark2->GetStart()); + return aPos1 < aPos2; + }); + + for (const auto& pRefMark : aRefMarks) + { if (!pRefMark->GetRefName().startsWith(aNamePrefix)) { continue; commit 8e8c650a90487e93e42c70dd7ff736a446d38e9a Author: Miklos Vajna <vmik...@collabora.com> AuthorDate: Wed Dec 14 16:06:37 2022 +0100 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Mon Jan 30 10:41:07 2023 +0100 sw: add a new .uno:UpdateBookmarks UNO command Current the .uno:InsertBookmark command allows inserting a bookmark with a provided content into the document, but an existing bookmark can't be updated similarly. This is a problem in case Zotero citations are to be modeled with bookmarks. Another trouble is that bookmarks don't have dummy characters, so we need to be careful to replace the content in a way that maintains these bookmarks, a naive delete + insert will collapse them. Fix the problem by introducing a new .uno:UpdateBookmarks command, somewhat similar to what commit 7765b442e13048f857fd7ee49ced1731caee297e (sw: add a new .uno:TextFormFields UNO command, 2022-11-28) did. As usual, the provided new text is meant to be HTML, which allows multi-paragraph, formatted content. (cherry picked from commit 724180ec495a696c79332653cb6fb52ecfbccc29) Conflicts: sw/qa/uibase/shells/shells.cxx Change-Id: Id5bb3fd2fd9e147d98709b5a7e0c322e73c6d283 diff --git a/sw/inc/cmdid.h b/sw/inc/cmdid.h index 3839e396d448..dbb3a259b422 100644 --- a/sw/inc/cmdid.h +++ b/sw/inc/cmdid.h @@ -203,6 +203,7 @@ class SwUINumRuleItem; // Region: Insert #define FN_INSERT_BOOKMARK (FN_INSERT + 2 ) /* Bookmark */ +// FN_INSERT + 3 is FN_INSERT_BREAK #define FN_INSERT_BREAK_DLG (FN_INSERT + 4 ) /* Break */ #define FN_INSERT_COLUMN_BREAK (FN_INSERT + 5 ) /* Column break */ @@ -320,6 +321,7 @@ class SwUINumRuleItem; #define FN_TABLE_PASTE_COL_BEFORE (FN_INSERT2 + 32) /* paste table as new table columns */ #define FN_EDIT_BOOKMARK (FN_INSERT2 + 33 ) /* Bookmark */ +#define FN_UPDATE_BOOKMARKS (FN_INSERT2 + 34) // Region: Format #define FN_AUTOFORMAT_APPLY (FN_FORMAT + 1 ) /* apply autoformat options */ diff --git a/sw/qa/uibase/shells/shells.cxx b/sw/qa/uibase/shells/shells.cxx index 1222a53fe52d..19ed03bbfa56 100644 --- a/sw/qa/uibase/shells/shells.cxx +++ b/sw/qa/uibase/shells/shells.cxx @@ -523,6 +523,69 @@ CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testUpdateRefmarks) CPPUNIT_ASSERT_EQUAL(OUString("new content"), pTextNode->GetText()); } +CPPUNIT_TEST_FIXTURE(SwUibaseShellsTest, testUpdateBookmarks) +{ + // Given a document with 2 bookmarks, first covering "B" and second covering "D": + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + pWrtShell->Insert("ABCDE"); + pWrtShell->SttEndDoc(/*bStt=*/true); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/true, 1, /*bBasicCall=*/false); + pWrtShell->SetBookmark(vcl::KeyCode(), "ZOTERO_BREF_GiQ7DAWQYWLy"); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/true, 1, /*bBasicCall=*/false); + pWrtShell->SetBookmark(vcl::KeyCode(), "ZOTERO_BREF_PRxDGUb4SWXF"); + + // When updating the content of bookmarks: + pWrtShell->SttEndDoc(/*bStt=*/true); + std::vector<beans::PropertyValue> aArgsVec = comphelper::JsonToPropertyValues(R"json( +{ + "BookmarkNamePrefix": { + "type": "string", + "value": "ZOTERO_BREF_" + }, + "Bookmarks": { + "type": "[][]com.sun.star.beans.PropertyValue", + "value": [ + { + "Bookmark": { + "type": "string", + "value": "ZOTERO_BREF_GiQ7DAWQYWLy" + }, + "BookmarkText": { + "type": "string", + "value": "new result 1" + } + }, + { + "Bookmark": { + "type": "string", + "value": "ZOTERO_BREF_PRxDGUb4SWXF" + }, + "BookmarkText": { + "type": "string", + "value": "new result 2" + } + } + ] + } +} +)json"); + uno::Sequence<beans::PropertyValue> aArgs = comphelper::containerToSequence(aArgsVec); + dispatchCommand(mxComponent, ".uno:UpdateBookmarks", aArgs); + + // Then make sure that the only paragraph is updated correctly: + SwCursor* pCursor = pWrtShell->GetCursor(); + OUString aActual = pCursor->GetPointNode().GetTextNode()->GetText(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: Anew result 1Cnew result 2E + // - Actual : ABCDE + // i.e. the content was not updated. + CPPUNIT_ASSERT_EQUAL(OUString("Anew result 1Cnew result 2E"), aActual); +} + CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/sdi/_textsh.sdi b/sw/sdi/_textsh.sdi index 102e6495e7a2..63a99f3ea9f7 100644 --- a/sw/sdi/_textsh.sdi +++ b/sw/sdi/_textsh.sdi @@ -146,6 +146,12 @@ interface BaseText StateMethod = GetState ; DisableFlags="SfxDisableFlags::SwOnProtectedCursor"; ] + FN_UPDATE_BOOKMARKS + [ + ExecMethod = Execute ; + StateMethod = GetState ; + DisableFlags="SfxDisableFlags::SwOnProtectedCursor"; + ] FN_SET_REMINDER [ ExecMethod = Execute ; diff --git a/sw/sdi/swriter.sdi b/sw/sdi/swriter.sdi index b9414ea2bfe5..a0380a2d1944 100644 --- a/sw/sdi/swriter.sdi +++ b/sw/sdi/swriter.sdi @@ -2586,6 +2586,20 @@ SfxVoidItem EditBookmark FN_EDIT_BOOKMARK GroupId = SfxGroupId::Insert; ] +SfxVoidItem UpdateBookmarks FN_UPDATE_BOOKMARKS +(SfxStringItem BookmarkNamePrefix FN_PARAM_1, SfxUnoAnyItem Bookmarks FN_PARAM_2) +[ + AutoUpdate = FALSE, + FastCall = FALSE, + ReadOnlyDoc = FALSE, + Toggle = FALSE, + Container = FALSE, + RecordAbsolute = FALSE, + RecordPerSet; + + GroupId = SfxGroupId::Insert; +] + SfxVoidItem SetReminder FN_SET_REMINDER [ diff --git a/sw/source/uibase/shells/textsh1.cxx b/sw/source/uibase/shells/textsh1.cxx index 832f6d7820c1..d136c80d2d07 100644 --- a/sw/source/uibase/shells/textsh1.cxx +++ b/sw/source/uibase/shells/textsh1.cxx @@ -102,6 +102,7 @@ #include <xmloff/odffields.hxx> #include <bookmark.hxx> #include <linguistic/misc.hxx> +#include <comphelper/sequenceashashmap.hxx> #include <authfld.hxx> #include <config_wasm_strip.h> #if !ENABLE_WASM_STRIP_EXTRA @@ -112,6 +113,7 @@ #endif // ENABLE_WASM_STRIP_EXTRA #include <translatehelper.hxx> #include <IDocumentContentOperations.hxx> +#include <IDocumentUndoRedo.hxx> using namespace ::com::sun::star; using namespace com::sun::star::beans; @@ -379,6 +381,92 @@ OUString GetLocalURL(const SwWrtShell& rSh) return rLocalURL; } +void UpdateBookmarks(SfxRequest& rReq, SwWrtShell& rWrtSh) +{ + if (rWrtSh.getIDocumentSettingAccess().get(DocumentSettingId::PROTECT_BOOKMARKS)) + { + return; + } + + OUString aBookmarkNamePrefix; + const SfxStringItem* pBookmarkNamePrefix = rReq.GetArg<SfxStringItem>(FN_PARAM_1); + if (pBookmarkNamePrefix) + { + aBookmarkNamePrefix = pBookmarkNamePrefix->GetValue(); + } + + uno::Sequence<beans::PropertyValues> aBookmarks; + const SfxUnoAnyItem* pBookmarks = rReq.GetArg<SfxUnoAnyItem>(FN_PARAM_2); + if (pBookmarks) + { + pBookmarks->GetValue() >>= aBookmarks; + } + + rWrtSh.GetDoc()->GetIDocumentUndoRedo().StartUndo(SwUndoId::INSBOOKMARK, nullptr); + rWrtSh.StartAction(); + + IDocumentMarkAccess& rIDMA = *rWrtSh.GetDoc()->getIDocumentMarkAccess(); + sal_Int32 nBookmarkIndex = 0; + bool bSortMarks = false; + for (auto it = rIDMA.getBookmarksBegin(); it != rIDMA.getBookmarksEnd(); ++it) + { + auto pMark = dynamic_cast<sw::mark::Bookmark*>(*it); + assert(pMark); + if (!pMark->GetName().startsWith(aBookmarkNamePrefix)) + { + continue; + } + + if (aBookmarks.getLength() <= nBookmarkIndex) + { + continue; + } + + comphelper::SequenceAsHashMap aMap(aBookmarks[nBookmarkIndex++]); + if (aMap["Bookmark"].get<OUString>() != pMark->GetName()) + { + continue; + } + + OUString aBookmarkText = aMap["BookmarkText"].get<OUString>(); + + // Insert markers to remember where the paste positions are. + SwPaM aMarkers(pMark->GetMarkEnd()); + IDocumentContentOperations& rIDCO = rWrtSh.GetDoc()->getIDocumentContentOperations(); + bool bSuccess = rIDCO.InsertString(aMarkers, "XY"); + if (bSuccess) + { + SwPaM aPasteEnd(pMark->GetMarkEnd()); + aPasteEnd.Move(fnMoveForward, GoInContent); + + // Paste HTML content. + SwPaM* pCursorPos = rWrtSh.GetCursor(); + *pCursorPos = aPasteEnd; + SwTranslateHelper::PasteHTMLToPaM(rWrtSh, pCursorPos, aBookmarkText.toUtf8(), true); + + // Update the bookmark to point to the new content. + SwPaM aPasteStart(pMark->GetMarkEnd()); + aPasteStart.Move(fnMoveForward, GoInContent); + SwPaM aStartMarker(pMark->GetMarkStart(), *aPasteStart.GetPoint()); + SwPaM aEndMarker(*aPasteEnd.GetPoint(), *aPasteEnd.GetPoint()); + aEndMarker.GetMark()->AdjustContent(1); + pMark->SetMarkPos(*aPasteStart.GetPoint()); + pMark->SetOtherMarkPos(*aPasteEnd.GetPoint()); + bSortMarks = true; + + // Remove markers. the start marker includes the old content as well. + rIDCO.DeleteAndJoin(aStartMarker); + rIDCO.DeleteAndJoin(aEndMarker); + } + } + if (bSortMarks) + { + rIDMA.assureSortedMarkContainers(); + } + + rWrtSh.EndAction(); + rWrtSh.GetDoc()->GetIDocumentUndoRedo().EndUndo(SwUndoId::INSBOOKMARK, nullptr); +} } void SwTextShell::Execute(SfxRequest &rReq) @@ -758,6 +846,11 @@ void SwTextShell::Execute(SfxRequest &rReq) break; } + case FN_UPDATE_BOOKMARKS: + { + UpdateBookmarks(rReq, rWrtSh); + break; + } case FN_DELETE_BOOKMARK: { if (pItem && !rWrtSh.getIDocumentSettingAccess().get(DocumentSettingId::PROTECT_BOOKMARKS))