desktop/qa/data/three-changes.fodt | 40 ++++++ desktop/qa/desktop_lib/test_desktop_lib.cxx | 162 +++++++++++++++++++++++++++- desktop/source/lib/init.cxx | 13 ++ include/LibreOfficeKit/LibreOfficeKit.h | 3 include/LibreOfficeKit/LibreOfficeKit.hxx | 10 + include/sfx2/lokhelper.hxx | 2 include/sfx2/viewsh.hxx | 7 - sfx2/source/control/dispatch.cxx | 16 ++ sfx2/source/view/lokhelper.cxx | 9 + 9 files changed, 259 insertions(+), 3 deletions(-)
New commits: commit c4f3d75567d5664259e01ff14a75e6f3e952ac9a Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Wed Jun 18 14:22:17 2025 +0500 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Fri Jun 20 08:57:41 2025 +0200 LOK: Introduce "read-only mode with tracked changes management" When it is enabled, most of the editing commands in a read-only view are disabled, as usual; but the commands to manage tracked changes are enabled, and allow user to accept / reject / comment on changes. Saving is enabled, too. Change-Id: Ic8bafc335c47ecd1b897c428a804cea2bc677cb3 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/186686 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Miklos Vajna <vmik...@collabora.com> diff --git a/desktop/qa/data/three-changes.fodt b/desktop/qa/data/three-changes.fodt new file mode 100644 index 000000000000..0c7072b244a0 --- /dev/null +++ b/desktop/qa/data/three-changes.fodt @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.4" office:mimetype="application/vnd.oasis.opendocument.text"> + <office:automatic-styles> + <style:style style:name="T1" style:family="text"> + <style:text-properties fo:font-weight="bold"/> + </style:style> + </office:automatic-styles> + <office:body> + <office:text> + <text:tracked-changes text:track-changes="false"> + <text:changed-region xml:id="ct2412392131792" text:id="ct2412392131792"> + <text:deletion> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-06-16T14:08:27</dc:date> + </office:change-info> + </text:deletion> + </text:changed-region> + <text:changed-region xml:id="ct2412428113776" text:id="ct2412428113776"> + <text:format-change> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-06-17T12:41:00</dc:date> + </office:change-info> + </text:format-change> + </text:changed-region> + <text:changed-region xml:id="ct2412428117232" text:id="ct2412428117232"> + <text:insertion> + <office:change-info> + <dc:creator>Mike</dc:creator> + <dc:date>2025-06-17T12:41:19</dc:date> + </office:change-info> + </text:insertion> + </text:changed-region> + </text:tracked-changes> + <text:p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum consequat mi quis pretium semper. Proin luctus orci ac neque venenatis, quis commodo dolor posuere. Curabitur dignissim sapien quis cursus egestas. <text:change-start text:change-id="ct2412392131792"/>Donec<text:change-end text:change-id="ct2412392131792"/> blandit auctor arcu, nec <text:change-start text:change-id="ct2412428113776"/><text:span text:style-name="T1">pellentesque</text:span><text:change-end text:change-id="ct2412428113776"/> eros molestie eget. In consectetur aliquam hendrerit. Sed cursus mauris vitae ligula pellentesque, non pellentesque urna aliquet. Fusce placerat mauris enim, nec rutrum purus semper vel. Praesent tincidunt neque eu pellentesque pharetra. Fusce pellentesque est orci.<text:change-start text:change-id="ct2412428117232"/> Sapienti sat.<text:change-end text:change-id="ct2412428117232"/></text:p> + </office:text> + </office:body> +</office:document> \ No newline at end of file diff --git a/desktop/qa/desktop_lib/test_desktop_lib.cxx b/desktop/qa/desktop_lib/test_desktop_lib.cxx index 903b99cb17f1..49fa3e5a4a5d 100644 --- a/desktop/qa/desktop_lib/test_desktop_lib.cxx +++ b/desktop/qa/desktop_lib/test_desktop_lib.cxx @@ -196,6 +196,7 @@ public: void testCommentsCallbacksWriter(); void testCommentsAddEditDeleteDraw(); void testCommentsInReadOnlyMode(); + void testRedlinesInReadOnlyMode(); void testCalcValidityDropdown(); void testCalcValidityDropdownInReadonlyMode(); void testRunMacro(); @@ -272,6 +273,7 @@ public: CPPUNIT_TEST(testCommentsCallbacksWriter); CPPUNIT_TEST(testCommentsAddEditDeleteDraw); CPPUNIT_TEST(testCommentsInReadOnlyMode); + CPPUNIT_TEST(testRedlinesInReadOnlyMode); CPPUNIT_TEST(testCalcValidityDropdown); CPPUNIT_TEST(testCalcValidityDropdownInReadonlyMode); CPPUNIT_TEST(testRunMacro); @@ -2207,6 +2209,49 @@ void DesktopLOKTest::testRedlineCalc() namespace { +struct RedlineInfo +{ + std::string action; + std::string index; + std::string author; + std::string type; + std::string comment; + std::string description; + std::string dateTime; +}; + +std::vector<RedlineInfo> getRedlineInfo(const boost::property_tree::ptree& redlineNode) +{ + std::vector<RedlineInfo> result; + result.reserve(redlineNode.size()); + for (const auto& redline : redlineNode) + { + result.emplace_back(); + result.back().index = redline.second.get<std::string>("index"); + result.back().author = redline.second.get<std::string>("author"); + result.back().type = redline.second.get<std::string>("type"); + result.back().comment = redline.second.get<std::string>("comment"); + result.back().description = redline.second.get<std::string>("description"); + result.back().dateTime = redline.second.get<std::string>("dateTime"); + if (auto oAction = redline.second.get_optional<std::string>("action")) + result.back().action = *oAction; + } + + return result; +} + +std::vector<RedlineInfo> getRedlineInfo(LibLODocument_Impl* pDocument) +{ + char* json + = pDocument->m_pDocumentClass->getCommandValues(pDocument, ".uno:AcceptTrackedChanges"); + std::stringstream stream(json); + free(json); + CPPUNIT_ASSERT(!stream.str().empty()); + boost::property_tree::ptree tree; + boost::property_tree::read_json(stream, tree); + return getRedlineInfo(tree.get_child("redlines")); +} + class ViewCallback { LibLODocument_Impl* mpDocument; @@ -2222,6 +2267,7 @@ public: tools::Rectangle m_aOwnCursor; boost::property_tree::ptree m_aCommentCallbackResult; boost::property_tree::ptree m_aColorPaletteCallbackResult; + RedlineInfo m_aLastRedlineInfo; ViewCallback(LibLODocument_Impl* pDocument) : mpDocument(pDocument), @@ -2305,6 +2351,17 @@ public: boost::property_tree::read_json(aStream, m_JSONDialog); } break; + case LOK_CALLBACK_REDLINE_TABLE_SIZE_CHANGED: + case LOK_CALLBACK_REDLINE_TABLE_ENTRY_MODIFIED: + { + std::stringstream aStream(pPayload); + boost::property_tree::ptree tree; + boost::property_tree::read_json(aStream, tree); + auto redlines = getRedlineInfo(tree); + CPPUNIT_ASSERT_EQUAL(size_t(1), redlines.size()); + m_aLastRedlineInfo = redlines[0]; + } + break; } } }; @@ -2902,6 +2959,108 @@ void DesktopLOKTest::testCommentsInReadOnlyMode() //CPPUNIT_ASSERT_EQUAL(nCommentId, aView.m_aCommentCallbackResult.get<int>("id")); } +void DesktopLOKTest::testRedlinesInReadOnlyMode() +{ + // In AllowManageRedlines mode, it must be possible to perform redline editing commands, + // even in read-only mode. + + using namespace std::string_literals; + + LibLODocument_Impl* pDocument = loadDoc("three-changes.fodt"); + + int viewId = pDocument->m_pDocumentClass->createView(pDocument); + pDocument->m_pDocumentClass->setView(pDocument, viewId); + pDocument->m_pDocumentClass->initializeForRendering(pDocument, "{}"); + ViewCallback aCallback(pDocument); + Scheduler::ProcessEventsToIdle(); + + CPPUNIT_ASSERT_EQUAL(size_t(3), getRedlineInfo(pDocument).size()); + + // Activate read-only mode + SfxLokHelper::setViewReadOnly(viewId, true); + + // Go to the 1st tracked change: "Delete “Donec”" + pDocument->pClass->postUnoCommand(pDocument, ".uno:NextTrackedChange", {}, false); + Scheduler::ProcessEventsToIdle(); + + // Check that redline management commands don't work in pure read-only + // Try to reject current redline + pDocument->pClass->postUnoCommand(pDocument, ".uno:RejectTrackedChange", {}, false); + Scheduler::ProcessEventsToIdle(); + // Nothing happened + CPPUNIT_ASSERT_EQUAL(size_t(3), getRedlineInfo(pDocument).size()); + CPPUNIT_ASSERT_EQUAL(""s, aCallback.m_aLastRedlineInfo.action); + CPPUNIT_ASSERT_EQUAL(""s, aCallback.m_aLastRedlineInfo.author); + CPPUNIT_ASSERT_EQUAL(""s, aCallback.m_aLastRedlineInfo.type); + CPPUNIT_ASSERT_EQUAL(""s, aCallback.m_aLastRedlineInfo.comment); + CPPUNIT_ASSERT_EQUAL(""s, aCallback.m_aLastRedlineInfo.description); + CPPUNIT_ASSERT_EQUAL(""s, aCallback.m_aLastRedlineInfo.dateTime); + + // Activate the AllowManageRedlines mode + SfxLokHelper::setAllowManageRedlines(viewId, true); + + // Try to reject current redline + pDocument->pClass->postUnoCommand(pDocument, ".uno:RejectTrackedChange", {}, false); + Scheduler::ProcessEventsToIdle(); + // One change gone; it is recorded "Remove"d in aCallback.m_aLastRedlineInfo + CPPUNIT_ASSERT_EQUAL(size_t(2), getRedlineInfo(pDocument).size()); + CPPUNIT_ASSERT_EQUAL("Remove"s, aCallback.m_aLastRedlineInfo.action); + CPPUNIT_ASSERT_EQUAL("Mike"s, aCallback.m_aLastRedlineInfo.author); + CPPUNIT_ASSERT_EQUAL("Delete"s, aCallback.m_aLastRedlineInfo.type); + CPPUNIT_ASSERT_EQUAL(""s, aCallback.m_aLastRedlineInfo.comment); + CPPUNIT_ASSERT_EQUAL("Delete “Donec”"s, aCallback.m_aLastRedlineInfo.description); + CPPUNIT_ASSERT_EQUAL("2025-06-16T14:08:27"s, aCallback.m_aLastRedlineInfo.dateTime); + + // Go to the 2nd tracked change: "Attributes changed" + pDocument->pClass->postUnoCommand(pDocument, ".uno:NextTrackedChange", {}, false); + Scheduler::ProcessEventsToIdle(); + + // Comment on it + pDocument->pClass->postUnoCommand(pDocument, ".uno:CommentChangeTracking", + R"({"Text":{"type":"string","value":"Some comment"}})", + false); + Scheduler::ProcessEventsToIdle(); + // One change got a comment; it is recorded "Modify"ed in aCallback.m_aLastRedlineInfo + CPPUNIT_ASSERT_EQUAL(size_t(2), getRedlineInfo(pDocument).size()); + CPPUNIT_ASSERT_EQUAL("Modify"s, aCallback.m_aLastRedlineInfo.action); + CPPUNIT_ASSERT_EQUAL("Mike"s, aCallback.m_aLastRedlineInfo.author); + CPPUNIT_ASSERT_EQUAL("Format"s, aCallback.m_aLastRedlineInfo.type); + CPPUNIT_ASSERT_EQUAL("Some comment"s, aCallback.m_aLastRedlineInfo.comment); + CPPUNIT_ASSERT_EQUAL("Attributes changed"s, aCallback.m_aLastRedlineInfo.description); + CPPUNIT_ASSERT_EQUAL("2025-06-17T12:41:00"s, aCallback.m_aLastRedlineInfo.dateTime); + + // Go to the 3rd tracked change: "Insert “ Sapienti sat.”" + pDocument->pClass->postUnoCommand(pDocument, ".uno:NextTrackedChange", {}, false); + Scheduler::ProcessEventsToIdle(); + + // Accept it + pDocument->pClass->postUnoCommand(pDocument, ".uno:AcceptTrackedChange", {}, false); + Scheduler::ProcessEventsToIdle(); + // One change gone; it is recorded "Remove"d in aCallback.m_aLastRedlineInfo + CPPUNIT_ASSERT_EQUAL(size_t(1), getRedlineInfo(pDocument).size()); + CPPUNIT_ASSERT_EQUAL("Remove"s, aCallback.m_aLastRedlineInfo.action); + CPPUNIT_ASSERT_EQUAL("Mike"s, aCallback.m_aLastRedlineInfo.author); + CPPUNIT_ASSERT_EQUAL("Insert"s, aCallback.m_aLastRedlineInfo.type); + CPPUNIT_ASSERT_EQUAL(""s, aCallback.m_aLastRedlineInfo.comment); + CPPUNIT_ASSERT_EQUAL("Insert “ Sapienti sat.”"s, aCallback.m_aLastRedlineInfo.description); + CPPUNIT_ASSERT_EQUAL("2025-06-17T12:41:19"s, aCallback.m_aLastRedlineInfo.dateTime); + + // Make sure that another (unrelated to redline management) editing command is not working + pDocument->pClass->postUnoCommand(pDocument, ".uno:InsertAnnotation", + R"({"Text":{"type":"string","value":"Comment"}})", + false); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT(aCallback.m_aCommentCallbackResult.empty()); + + // Check that the same command would succeed in AllowChangeComments mode + SfxLokHelper::setAllowChangeComments(viewId, true); + pDocument->pClass->postUnoCommand(pDocument, ".uno:InsertAnnotation", + R"({"Text":{"type":"string","value":"Comment"}})", + false); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT(!aCallback.m_aCommentCallbackResult.empty()); +} + void DesktopLOKTest::testCalcValidityDropdown() { LibLODocument_Impl* pDocument = loadDoc("validity.ods"); @@ -3982,9 +4141,10 @@ void DesktopLOKTest::testABI() CPPUNIT_ASSERT_EQUAL(documentClassOffset(77), offsetof(struct _LibreOfficeKitDocumentClass, renderNextSlideLayer)); CPPUNIT_ASSERT_EQUAL(documentClassOffset(78), offsetof(struct _LibreOfficeKitDocumentClass, setViewOption)); CPPUNIT_ASSERT_EQUAL(documentClassOffset(79), offsetof(struct _LibreOfficeKitDocumentClass, setColorPreviewState)); + CPPUNIT_ASSERT_EQUAL(documentClassOffset(80), offsetof(struct _LibreOfficeKitDocumentClass, setAllowManageRedlines)); // As above - CPPUNIT_ASSERT_EQUAL(documentClassOffset(80), sizeof(struct _LibreOfficeKitDocumentClass)); + CPPUNIT_ASSERT_EQUAL(documentClassOffset(81), sizeof(struct _LibreOfficeKitDocumentClass)); } CPPUNIT_TEST_SUITE_REGISTRATION(DesktopLOKTest); diff --git a/desktop/source/lib/init.cxx b/desktop/source/lib/init.cxx index da3ca5e06504..89b8b8287205 100644 --- a/desktop/source/lib/init.cxx +++ b/desktop/source/lib/init.cxx @@ -1328,6 +1328,8 @@ static void doc_setViewReadOnly(LibreOfficeKitDocument* pThis, int nId, const bo static void doc_setAllowChangeComments(LibreOfficeKitDocument* pThis, int nId, const bool allow); +static void doc_setAllowManageRedlines(LibreOfficeKitDocument* pThis, int nId, bool allow); + static void doc_setAccessibilityState(LibreOfficeKitDocument* pThis, int nId, bool bEnabled); static char* doc_getA11yFocusedParagraph(LibreOfficeKitDocument* pThis); @@ -1547,6 +1549,7 @@ LibLODocument_Impl::LibLODocument_Impl(uno::Reference <css::lang::XComponent> xC m_pDocumentClass->setViewReadOnly = doc_setViewReadOnly; m_pDocumentClass->setAllowChangeComments = doc_setAllowChangeComments; + m_pDocumentClass->setAllowManageRedlines = doc_setAllowManageRedlines; m_pDocumentClass->getPresentationInfo = doc_getPresentationInfo; m_pDocumentClass->createSlideRenderer = doc_createSlideRenderer; @@ -7611,6 +7614,16 @@ static void doc_setAllowChangeComments(SAL_UNUSED_PARAMETER LibreOfficeKitDocume SfxLokHelper::setAllowChangeComments(nId, allow); } +static void doc_setAllowManageRedlines(SAL_UNUSED_PARAMETER LibreOfficeKitDocument* /*pThis*/, int nId, bool allow) +{ + comphelper::ProfileZone aZone("doc_setAllowManageRedlines"); + + SolarMutexGuard aGuard; + SetLastExceptionMsg(); + + SfxLokHelper::setAllowManageRedlines(nId, allow); +} + static void doc_setAccessibilityState(SAL_UNUSED_PARAMETER LibreOfficeKitDocument* pThis, int nId, bool nEnabled) { SolarMutexGuard aGuard; diff --git a/include/LibreOfficeKit/LibreOfficeKit.h b/include/LibreOfficeKit/LibreOfficeKit.h index 7f933aa29a4b..c242d00e0bde 100644 --- a/include/LibreOfficeKit/LibreOfficeKit.h +++ b/include/LibreOfficeKit/LibreOfficeKit.h @@ -562,6 +562,9 @@ struct _LibreOfficeKitDocumentClass /// @see lok::Document::setColorPreviewState(). void (*setColorPreviewState) (LibreOfficeKitDocument* pThis, int nId, bool nEnabled); + /// @see lok::Document::setAllowManageRedlines(). + void (*setAllowManageRedlines)(LibreOfficeKitDocument* pThis, int nId, bool allow); + #endif // defined LOK_USE_UNSTABLE_API || defined LIBO_INTERNAL_ONLY }; diff --git a/include/LibreOfficeKit/LibreOfficeKit.hxx b/include/LibreOfficeKit/LibreOfficeKit.hxx index 8de0898817fb..c4dac79c412c 100644 --- a/include/LibreOfficeKit/LibreOfficeKit.hxx +++ b/include/LibreOfficeKit/LibreOfficeKit.hxx @@ -879,6 +879,16 @@ public: mpDoc->pClass->setAllowChangeComments(mpDoc, nId, allow); } + /** Set if the view can manage redlines in readonly mode or not. + * + * @param nId view ID + * @param allow + */ + void setAllowManageRedlines(int nId, bool allow) + { + mpDoc->pClass->setAllowManageRedlines(mpDoc, nId, allow); + } + /** * Enable/Disable accessibility support for the window with the specified nId. * diff --git a/include/sfx2/lokhelper.hxx b/include/sfx2/lokhelper.hxx index d8cc5af06214..a3db5bada7d6 100644 --- a/include/sfx2/lokhelper.hxx +++ b/include/sfx2/lokhelper.hxx @@ -125,6 +125,8 @@ public: static void setViewReadOnly(int nId, bool readOnly); // In readonly view, can user add / modify comments or not. static void setAllowChangeComments(int nId, bool allow); + // In readonly view, can user accept / reject tracked changes or not. + static void setAllowManageRedlines(int nId, bool allow); /// Get the language used by the loading view (used for all save operations). static const LanguageTag & getLoadLanguage(); /// Set the language used by the loading view (used for all save operations). diff --git a/include/sfx2/viewsh.hxx b/include/sfx2/viewsh.hxx index dcd6619d4e0f..1e55b1594493 100644 --- a/include/sfx2/viewsh.hxx +++ b/include/sfx2/viewsh.hxx @@ -225,8 +225,9 @@ private: LOKDocumentFocusListener& GetLOKDocumentFocusListener(); const LOKDocumentFocusListener& GetLOKDocumentFocusListener() const; - bool lokReadOnlyView = false; // When true, this is a LOK readonly view. - bool allowChangeComments = false; // When true, user can edit comments in readonly view mode. + bool lokReadOnlyView : 1 = false; // When true, this is a LOK readonly view + bool allowChangeComments : 1 = false; // Allow editing comments in readonly view mode + bool allowManageRedlines : 1 = false; // Allow accepting/rejecting changes in readonly view mode public: @@ -254,6 +255,8 @@ public: bool IsLokReadOnlyView() const { return lokReadOnlyView; }; void SetAllowChangeComments(bool allow) { allowChangeComments = allow; } bool IsAllowChangeComments() const { return allowChangeComments; } + void SetAllowManageRedlines(bool allow) { allowManageRedlines = allow; } + bool IsAllowManageRedlines() const { return allowManageRedlines; } // Misc diff --git a/sfx2/source/control/dispatch.cxx b/sfx2/source/control/dispatch.cxx index 40910ff6e949..11614d9ef688 100644 --- a/sfx2/source/control/dispatch.cxx +++ b/sfx2/source/control/dispatch.cxx @@ -1554,6 +1554,22 @@ static bool IsCommandAllowedInLokReadOnlyViewMode(std::u16string_view commandNam if (std::find(std::begin(allowed), std::end(allowed), commandName) != std::end(allowed)) return true; } + if (viewShell.IsAllowManageRedlines()) + { + static constexpr std::u16string_view allowed[] = { + u".uno:AcceptTrackedChange", + u".uno:RejectTrackedChange", + u".uno:AcceptAllTrackedChanges", + u".uno:RejectAllTrackedChanges", + u".uno:AcceptTrackedChangeToNext", + u".uno:RejectTrackedChangeToNext", + u".uno:CommentChangeTracking", + u".uno:Save", + }; + + if (std::find(std::begin(allowed), std::end(allowed), commandName) != std::end(allowed)) + return true; + } return false; } diff --git a/sfx2/source/view/lokhelper.cxx b/sfx2/source/view/lokhelper.cxx index b16b7c7ff292..d9184e33e5bb 100644 --- a/sfx2/source/view/lokhelper.cxx +++ b/sfx2/source/view/lokhelper.cxx @@ -382,6 +382,15 @@ void SfxLokHelper::setAllowChangeComments(int nId, bool allow) } } +void SfxLokHelper::setAllowManageRedlines(int nId, bool allow) +{ + if (SfxViewShell* pViewShell = getViewOfId(nId)) + { + LOK_INFO("lok.readonlyview", "SfxLokHelper::setAllowManageRedlines: view id: " << nId << ", allow: " << allow); + pViewShell->SetAllowManageRedlines(allow); + } +} + void SfxLokHelper::setAccessibilityState(int nId, bool nEnabled) { std::vector<SfxViewShell*>& rViewArr = SfxGetpApp()->GetViewShells_Impl();