README.yrs | 93 +++++- sfx2/source/control/dispatch.cxx | 2 sw/inc/IDocumentState.hxx | 7 sw/inc/docufld.hxx | 8 sw/source/core/doc/DocumentStateManager.cxx | 418 ++++++++++++++++++++------- sw/source/core/fields/docufld.cxx | 4 sw/source/core/inc/DocumentStateManager.hxx | 6 sw/source/core/undo/docundo.cxx | 19 + sw/source/core/undo/rolbck.cxx | 10 sw/source/core/undo/unattr.cxx | 15 sw/source/core/unocore/unofield.cxx | 3 sw/source/uibase/docvw/AnnotationWin.cxx | 19 - sw/source/uibase/docvw/SidebarTxtControl.cxx | 2 sw/source/uibase/fldui/fldmgr.cxx | 4 14 files changed, 493 insertions(+), 117 deletions(-)
New commits: commit 094480aefadf72e46c28c35f7b873abeac1e4ff3 Author: Michael Stahl <michael.st...@allotropia.de> AuthorDate: Mon May 26 11:36:04 2025 +0200 Commit: Michael Stahl <michael.st...@allotropia.de> CommitDate: Fri Jun 20 18:44:16 2025 +0200 LOCRDT sfx2,sw: Undo Finally, add the comment id to SwPostItField too, so that undo actions can recreate the comment with the same id. Ensure that all inserted comments have an id (including via UNO API) so that the Undo can record it (SwHistorySetTextField/SwUndoAttr). Also this requires a more careful management of committing the YTransaction, to distinguish between creating new Undo stack items and extending the existing one. Change-Id: I986264dee956ec4b219ab8c7148d44fee4bbfa2b Reviewed-on: https://gerrit.libreoffice.org/c/core/+/185866 Tested-by: Jenkins Reviewed-by: Michael Stahl <michael.st...@allotropia.de> diff --git a/README.yrs b/README.yrs index 080a40a41792..66fbe6a68d69 100644 --- a/README.yrs +++ b/README.yrs @@ -16,6 +16,8 @@ Then, put the yrs build directory in autogen.input: `--with-yrs=/path/to/y-crdt` +All the related code should be behind macros in `config_host/config_collab.h` + ### How to run To prevent crashes at runtime, set the environment variable @@ -29,19 +31,100 @@ Currently, communication happens over a hard-coded pipe: (you can also create a new Writer document but that will be boring if all you can do is insert comments into empty doc) -* start another soffice YRSCONNECT=1 with a different user profile, create - new Writer document, and it will connect and load the document from the - other side +* start another soffice with YRSCONNECT=1 with a different user profile, + create new Writer document, and it will connect and load the document from + the other side All sorts of paragraph and character formattings should work inside comments. +Peer cursors should be displayed both in the sw document body and inside +comments. + Inserting hyperlinks also works, although sadly i wasn't able to figure out how to enable the menu items in read-only mode, so it only works in editable mode. -Undo/Redo doesn't work at all, it's disabled in readonly mode anyway. - Switching to editable mode is also possible, but only comment-related editing is synced via yrs, so if other editing operations change the positions of comments, a crash will be inevitable. +### Implementation + +Most of it is in 2 classes: EditDoc and sw::DocumentStateManager (for now); +the latter gets a new member YrsTransactionSupplier. + +DocumentStateManager starts a thread to communicate, and this sends new +messages to the main thread via PostUserEvent(). + +The EditDoc models of the comments are duplicated in a yrs YDocument model +and this is then synced remotely by yrs. + +The structure of the yrs model is: + +* YMap of comments (key: CommentId created from peer id + counter) + - YArray + - anchor pos: 2 or 4 ints [manually updated when editing sw] + - YMap of comment properties + - YText containing mapped EditDoc state +* YMap of cursors (key: peer id) + - either sw cursor: 2 or 4 ints [manually updated when editing sw] + or EditDoc position: CommentId + 2 or 4 ints, or WeakRef + or Y_JSON_NULL (for sw non-text selections, effectively ignored) + +Some confusing object relationships: + +SwAnnotationWin -> Outliner -> OutlinerEditEng -> EditEngine -> ImpEditEngine -> EditDoc + -> OutlinerView -> EditView -> ImpEditView -> EditEngine + + -> SidebarTextControl + +### Undo + +There was no Undo for edits inside comments anyway, only when losing the +focus a SwUndoFieldFromDoc is created. + +There are 2 approaches how Undo could work: either let all the SwUndo +actions modify the yrs model, or use the yrs yundo_manager and ensure that +for every top-level SwUndo there is exactly one item in the yundo_manager's +stack, so that Undo/Redo will have the same effect in the sw model and +the yrs model. + +Let's try if the second approach can be made to work. + +The yundo_manager by default creates stack items based on a timer, so we +configure that to a 2^31 timeout and invoke yundo_manager_stop() to create +all the items manually. + +yundo_manager_undo()/redo() etc internally create a YTransaction and commit +it, which will of course fail if one already exists! + +The yundo_manager creates a new item also for things like cursor movements +(because we put the cursors in the same YDocument as the content); there is +a way to filter the changes by Branch, but that filtering happens when the +undo/redo is invoked, not when the stack item is created - so the +"temporary" stack item is always extended with yrs changes until a "real" +change that has a corresponding SwUndo happens and there is a corresponding +yundo_manager_stop() then. + +There are still some corner cases where the 2 undo stacks aren't synced so +there are various workarounds like DummyUndo action or m_nTempUndoOffset +counter for these. + +Also the SwUndoFieldFromDoc batch action is problematic: it is created when +the comment loses focus, but all editing operations in the comment are +inserted in the yrs model immediately as they happen - so if changes are +received from peers, the creation of the SwUndoFieldFromDoc must be forced +to happen before the peer changes are applied in the sw document model, to +keep the actions in order. + +The comment id is used as a key in yrs ymap so it should be the same when +the Undo or Redo inserts a comment again, hence it's added to SwPostItField. + +For edits that are received from peers, what happens is that all the +received changes must be grouped into one SwUndo (list) action because there +is going to be one yrs undo item; and invoking Undo will just send the +changes to peers and they will create a new undo stack item as the local +instance goes back in the undo stack, and it's not possible to do it +differently because the undo stack items may contain different changes on +each peer. + diff --git a/sfx2/source/control/dispatch.cxx b/sfx2/source/control/dispatch.cxx index 4dba2cd3b4fd..782e61fe7e2f 100644 --- a/sfx2/source/control/dispatch.cxx +++ b/sfx2/source/control/dispatch.cxx @@ -1704,6 +1704,8 @@ bool SfxDispatcher::FindServer_(sal_uInt16 nSlot, SfxSlotServer& rServer) { OUString sCommand = pSlot->GetCommand(); if (sCommand == u".uno:InsertAnnotation"_ustr + || sCommand == u".uno:Undo"_ustr + || sCommand == u".uno:Redo"_ustr || ((sCommand == u".uno:FontDialog"_ustr || sCommand == u".uno:ParagraphDialog"_ustr) && pIFace->GetClassName() == "SwAnnotationShell"_ostr)) diff --git a/sw/inc/IDocumentState.hxx b/sw/inc/IDocumentState.hxx index c05d895f11b1..c3423ce1924b 100644 --- a/sw/inc/IDocumentState.hxx +++ b/sw/inc/IDocumentState.hxx @@ -25,6 +25,7 @@ #include <com/sun/star/uno/Any.h> #include <editeng/yrstransactionsupplier.hxx> #include <optional> +class SfxUndoAction; struct SwPosition; class SwPostItField; #endif @@ -61,9 +62,10 @@ public: virtual void YrsInitAcceptor() = 0; virtual void YrsInitConnector(css::uno::Any const& raConnector) = 0; virtual IYrsTransactionSupplier::Mode SetYrsMode(IYrsTransactionSupplier::Mode mode) = 0; - virtual void YrsCommitModified() = 0; + virtual void YrsCommitModified(bool isUnfinishedUndo) = 0; virtual void YrsNotifySetResolved(OString const& rCommentId, SwPostItField const& rField) = 0; + virtual OString YrsGenNewCommentId() = 0; virtual void YrsAddCommentImpl(SwPosition const& rPos, OString const& rCommentId) = 0; virtual void YrsAddComment(SwPosition const& rPos, ::std::optional<SwPosition> oAnchorStart, SwPostItField const& rField, bool isInsert) @@ -71,6 +73,9 @@ public: virtual void YrsRemoveCommentImpl(rtl::OString const& rCommentId) = 0; virtual void YrsRemoveComment(SwPosition const& rPos) = 0; virtual void YrsNotifyCursorUpdate() = 0; + virtual void YrsEndUndo() = 0; + virtual void YrsDoUndo(SfxUndoAction const* pUndo) = 0; + virtual void YrsDoRedo(SfxUndoAction const* pUndo) = 0; #endif protected: diff --git a/sw/inc/docufld.hxx b/sw/inc/docufld.hxx index 2183f133bde6..6502c5962ee0 100644 --- a/sw/inc/docufld.hxx +++ b/sw/inc/docufld.hxx @@ -20,6 +20,8 @@ #include <sal/config.h> +#include <config_collab.h> + #include <string_view> #include <editeng/outlobj.hxx> @@ -464,6 +466,12 @@ class SW_DLLPUBLIC SwPostItField final : public SwField sal_uInt32 m_nParaId; sal_uInt32 m_nParentPostItId; SwMarkName m_sParentName; /// Parent comment's name. +#if ENABLE_YRS + OString m_CommentId; +public: + OString GetYrsCommentId() const { return m_CommentId; } + void SetYrsCommentId(OString const& rCommentId) { m_CommentId = rCommentId; } +#endif public: static sal_uInt32 s_nLastPostItId; diff --git a/sw/source/core/doc/DocumentStateManager.cxx b/sw/source/core/doc/DocumentStateManager.cxx index 7c02b968d809..9cdefcc8125e 100644 --- a/sw/source/core/doc/DocumentStateManager.cxx +++ b/sw/source/core/doc/DocumentStateManager.cxx @@ -33,6 +33,8 @@ #include <IDocumentFieldsAccess.hxx> #include <txtannotationfld.hxx> #include <ndtxt.hxx> +#include <undobj.hxx> +#include <rolbck.hxx> #include <editeng/yrs.hxx> #include <editeng/editview.hxx> @@ -63,6 +65,12 @@ namespace sw #if ENABLE_YRS using sw::annotation::SwAnnotationWin; +#define yvalidate_undo(rDSM, offset) { \ + SAL_INFO("sw.yrs", "UNDO sw: " << (rDSM).m_rDoc.GetIDocumentUndoRedo().GetUndoActionCount() << " yrs: " << yundo_manager_undo_stack_len((rDSM).m_pYrsSupplier->GetUndoManager()) << " offset: " << offset << " tempoffset: " << (rDSM).m_pYrsSupplier->m_nTempUndoOffset); \ + assert((rDSM).m_rDoc.GetIDocumentUndoRedo().GetUndoActionCount(false) + (offset) == yundo_manager_undo_stack_len((rDSM).m_pYrsSupplier->GetUndoManager()) + (rDSM).m_pYrsSupplier->m_nTempUndoOffset \ + || (rDSM).m_pYrsReader == nullptr); \ +} + namespace { enum class Message : sal_uInt8 @@ -83,19 +91,35 @@ struct YDocDeleter { void operator()(YDoc *const p) const { ydoc_destroy(p); } } return ::std::unique_ptr<YDoc, YDocDeleter>{pYDoc}; } +// max possible timeout => force combining everything until yundo_manager_stop() +const YUndoManagerOptions g_YUndoOptions{SAL_MAX_INT32}; + +class DummyUndo : public SwUndo +{ +public: + DummyUndo(SwDoc & rDoc) + : SwUndo(SwUndoId::COMPAREDOC, rDoc) + {} + virtual void UndoImpl(::sw::UndoRedoContext &) override {} + virtual void RedoImpl(::sw::UndoRedoContext &) override {} +}; + } // namespace class YrsTransactionSupplier : public IYrsTransactionSupplier { private: friend class DocumentStateManager; + friend class YrsThread; ::std::unique_ptr<YDoc, YDocDeleter> m_pYDoc; Branch * m_pComments; Branch * m_pCursors; + YUndoManager * m_pUndoManager; YTransaction * m_pCurrentReadTransaction { nullptr }; YTransaction * m_pCurrentWriteTransaction { nullptr }; int m_nLocalComments { 0 }; ::std::map<OString, ::std::vector<SwAnnotationItem *>> m_Comments; + int m_nTempUndoOffset { 0 }; ///< for "unfinished" yrs undo (cursor move or EE edit) public: YrsTransactionSupplier(); @@ -104,6 +128,7 @@ public: YDoc* GetYDoc() override { return m_pYDoc.get(); } Branch* GetCommentMap() override { return m_pComments; } Branch* GetCursorMap() override { return m_pCursors; } + YUndoManager * GetUndoManager() { return m_pUndoManager; } YTransaction * GetReadTransaction() override; YTransaction * GetWriteTransaction() override; bool CommitTransaction(bool isForce = false) override; @@ -116,11 +141,15 @@ YrsTransactionSupplier::YrsTransactionSupplier() // ymap implicitly calls transact_mut() , m_pComments(ymap(m_pYDoc.get(), "comments")) , m_pCursors(ymap(m_pYDoc.get(), "cursors")) + , m_pUndoManager(yundo_manager(m_pYDoc.get(), &g_YUndoOptions)) { + yundo_manager_add_scope(m_pUndoManager, m_pComments); } YrsTransactionSupplier::~YrsTransactionSupplier() { + YrsTransactionSupplier::CommitTransaction(true); // yundo_manager_destroy requires no txn exists + yundo_manager_destroy(m_pUndoManager); // with cursors there will always be a pending transaction, and there is no api to cancel it, so just ignore it... //assert(m_pCurrentWriteTransaction == nullptr); (void) m_pCurrentWriteTransaction; @@ -188,11 +217,17 @@ OString YrsTransactionSupplier::GenNewCommentId() return OString::number(id) + OString::number(counter); } +OString DocumentStateManager::YrsGenNewCommentId() +{ + return m_pYrsSupplier->GenNewCommentId(); +} + class YrsThread : public ::salhelper::Thread { public: uno::Reference<connection::XConnection> m_xConnection; DocumentStateManager * m_pDSM; + bool m_hasFirstState{false}; public: YrsThread(uno::Reference<connection::XConnection> const& xConnection, @@ -236,14 +271,23 @@ struct ObserveState YTransaction *const pTxn; }; +struct ObserveCommentState : public ObserveState +{ + bool isUndoAction = false; +}; + extern "C" void observe_comments(void *const pState, uint32_t count, YEvent const*const events) { SAL_INFO("sw.yrs", "YRS observe_comments"); - ObserveState & rState{*static_cast<ObserveState*>(pState)}; + ObserveCommentState & rState{*static_cast<ObserveCommentState*>(pState)}; // DO NOT call rState.rYrsSupplier.GetWriteTransaction()! YTransaction *const pTxn{rState.pTxn}; // ??? that is TransactionMut - there is no way to construct YTransaction from it??? YTransaction *const pTxn{pEvent->txn}; + SwWrtShell *const pShell{dynamic_cast<SwWrtShell*>(rState.rDoc.getIDocumentLayoutAccess().GetCurrentViewShell())}; + + pShell->StartUndo(SwUndoId::INSDOKUMENT, nullptr); + ::std::vector<::std::tuple<OString, uint64_t, uint64_t>> posUpdates; ::std::vector<::std::tuple<OString, uint64_t, uint64_t>> startUpdates; ::std::map<::std::pair<uint64_t, uint64_t>, std::pair<OString, Branch const*>> newComments; @@ -270,14 +314,7 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons SwAnnotationWin & rWin{*rState.rYrsSupplier.GetComments().find(commentId)->second.front()->mpPostIt}; rWin.GetOutlinerView()->GetEditView().YrsApplyEEDelta(rState.pTxn, pEvent); - if ((rWin.GetStyle() & WB_DIALOGCONTROL) == 0) - { - rWin.UpdateData(); // not active window, force update - } - else - { // apparently this repaints active window - rWin.GetOutlinerView()->GetEditView().Invalidate(); - } + rWin.UpdateData(); // invalidate, and create SwUndo } break; case Y_ARRAY: @@ -302,40 +339,83 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons switch (lenC) { case 2: - yvalidate(pChange[0].tag == Y_EVENT_CHANGE_DELETE); - yvalidate(pChange[0].len == 2); - yvalidate(pChange[1].tag == Y_EVENT_CHANGE_ADD); - yvalidate(pChange[1].len == 2); - yvalidate(pChange[1].values[0].tag == Y_JSON_INT); - yvalidate(pChange[1].values[1].tag == Y_JSON_INT); - posUpdates.emplace_back(commentId, pChange[1].values[0].value.integer, pChange[1].values[1].value.integer); + if (pChange[0].tag == Y_EVENT_CHANGE_DELETE) + { + yvalidate(pChange[0].len == 2); + yvalidate(pChange[1].tag == Y_EVENT_CHANGE_ADD); + yvalidate(pChange[1].len == 2); + yvalidate(pChange[1].values[0].tag == Y_JSON_INT); + yvalidate(pChange[1].values[1].tag == Y_JSON_INT); + posUpdates.emplace_back(commentId, pChange[1].values[0].value.integer, pChange[1].values[1].value.integer); + } + else // Undo can send with different order... + { + yvalidate(pChange[0].tag == Y_EVENT_CHANGE_ADD); + yvalidate(pChange[0].len == 2); + yvalidate(pChange[0].values[0].tag == Y_JSON_INT); + yvalidate(pChange[0].values[1].tag == Y_JSON_INT); + yvalidate(pChange[1].tag == Y_EVENT_CHANGE_DELETE); + yvalidate(pChange[1].len == 2); + posUpdates.emplace_back(commentId, pChange[0].values[0].value.integer, pChange[0].values[1].value.integer); + } break; case 3: yvalidate(pChange[0].tag == Y_EVENT_CHANGE_RETAIN); yvalidate(pChange[0].len == 2); - yvalidate(pChange[1].tag == Y_EVENT_CHANGE_DELETE); - yvalidate(pChange[1].len == 2); - yvalidate(pChange[2].tag == Y_EVENT_CHANGE_ADD); - yvalidate(pChange[2].len == 2); - yvalidate(pChange[2].values[0].tag == Y_JSON_INT); - yvalidate(pChange[2].values[1].tag == Y_JSON_INT); - startUpdates.emplace_back(commentId, pChange[2].values[0].value.integer, pChange[2].values[1].value.integer); + if (pChange[1].tag == Y_EVENT_CHANGE_DELETE) + { + yvalidate(pChange[1].len == 2); + yvalidate(pChange[2].tag == Y_EVENT_CHANGE_ADD); + yvalidate(pChange[2].len == 2); + yvalidate(pChange[2].values[0].tag == Y_JSON_INT); + yvalidate(pChange[2].values[1].tag == Y_JSON_INT); + startUpdates.emplace_back(commentId, pChange[2].values[0].value.integer, pChange[2].values[1].value.integer); + } + else // Undo can send with different order... + { + yvalidate(pChange[1].tag == Y_EVENT_CHANGE_ADD); + yvalidate(pChange[1].len == 2); + yvalidate(pChange[1].values[0].tag == Y_JSON_INT); + yvalidate(pChange[1].values[1].tag == Y_JSON_INT); + yvalidate(pChange[2].tag == Y_EVENT_CHANGE_DELETE); + yvalidate(pChange[2].len == 2); + startUpdates.emplace_back(commentId, pChange[1].values[0].value.integer, pChange[1].values[1].value.integer); + } break; case 4: - yvalidate(pChange[0].tag == Y_EVENT_CHANGE_DELETE); - yvalidate(pChange[0].len == 2); - yvalidate(pChange[1].tag == Y_EVENT_CHANGE_ADD); - yvalidate(pChange[1].len == 2); - yvalidate(pChange[1].values[0].tag == Y_JSON_INT); - yvalidate(pChange[1].values[1].tag == Y_JSON_INT); - yvalidate(pChange[2].tag == Y_EVENT_CHANGE_DELETE); - yvalidate(pChange[2].len == 2); - yvalidate(pChange[3].tag == Y_EVENT_CHANGE_ADD); - yvalidate(pChange[3].len == 2); - yvalidate(pChange[3].values[0].tag == Y_JSON_INT); - yvalidate(pChange[3].values[1].tag == Y_JSON_INT); - posUpdates.emplace_back(commentId, pChange[1].values[0].value.integer, pChange[1].values[1].value.integer); - startUpdates.emplace_back(commentId, pChange[3].values[0].value.integer, pChange[3].values[1].value.integer); + if (pChange[0].tag == Y_EVENT_CHANGE_DELETE) + { + yvalidate(pChange[0].len == 2); + yvalidate(pChange[1].tag == Y_EVENT_CHANGE_ADD); + yvalidate(pChange[1].len == 2); + yvalidate(pChange[1].values[0].tag == Y_JSON_INT); + yvalidate(pChange[1].values[1].tag == Y_JSON_INT); + yvalidate(pChange[2].tag == Y_EVENT_CHANGE_DELETE); + yvalidate(pChange[2].len == 2); + yvalidate(pChange[3].tag == Y_EVENT_CHANGE_ADD); + yvalidate(pChange[3].len == 2); + yvalidate(pChange[3].values[0].tag == Y_JSON_INT); + yvalidate(pChange[3].values[1].tag == Y_JSON_INT); + posUpdates.emplace_back(commentId, pChange[1].values[0].value.integer, pChange[1].values[1].value.integer); + startUpdates.emplace_back(commentId, pChange[3].values[0].value.integer, pChange[3].values[1].value.integer); + } + else // Undo can send with different order... + { + yvalidate(pChange[0].tag == Y_EVENT_CHANGE_ADD); + yvalidate(pChange[0].len == 2); + yvalidate(pChange[0].values[0].tag == Y_JSON_INT); + yvalidate(pChange[0].values[1].tag == Y_JSON_INT); + yvalidate(pChange[1].tag == Y_EVENT_CHANGE_DELETE); + yvalidate(pChange[1].len == 2); + yvalidate(pChange[2].tag == Y_EVENT_CHANGE_ADD); + yvalidate(pChange[2].len == 2); + yvalidate(pChange[2].values[0].tag == Y_JSON_INT); + yvalidate(pChange[2].values[1].tag == Y_JSON_INT); + yvalidate(pChange[3].tag == Y_EVENT_CHANGE_DELETE); + yvalidate(pChange[3].len == 2); + posUpdates.emplace_back(commentId, pChange[0].values[0].value.integer, pChange[0].values[1].value.integer); + startUpdates.emplace_back(commentId, pChange[2].values[0].value.integer, pChange[2].values[1].value.integer); + } break; default: yvalidate(false); @@ -351,6 +431,42 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons Branch const*const pMap{ymap_event_target(pEvent)}; uint32_t lenP{0}; YPathSegment *const pPath{ymap_event_path(pEvent, &lenP)}; + + auto const HandleDeleteComment = [=](OString const& rCommentId, + ::std::vector<SwAnnotationItem *> const& rItems) + { + pShell->Push(); + *pShell->GetCursor()->GetPoint() = rItems.front()->GetAnchorPosition(); + pShell->SetMark(); + pShell->Right(SwCursorSkipMode::Chars, true, 1, false, false); + pShell->DelRight(); + pShell->Pop(SwCursorShell::PopMode::DeleteStack); + rState.rDoc.getIDocumentState().YrsRemoveCommentImpl(rCommentId); + }; + + auto const HandleNewComment = [=, &newComments](auto const& rChange) + { + // new comment + yvalidate(pMap == rState.rYrsSupplier.GetCommentMap()); + yvalidate(lenP == 0); + Branch const*const pArray{rChange.new_value->value.y_type}; + yvalidate(yarray_len(pArray) == 3); + ::std::unique_ptr<YOutput, YOutputDeleter> const pPos{yarray_get(pArray, pTxn, 0)}; + yvalidate(pPos->tag == Y_ARRAY); + ::std::unique_ptr<YOutput, YOutputDeleter> const pNode{yarray_get(pPos->value.y_type, pTxn, 0)}; + yvalidate(pNode->tag == Y_JSON_INT); + ::std::unique_ptr<YOutput, YOutputDeleter> const pContent{yarray_get(pPos->value.y_type, pTxn, 1)}; + yvalidate(pContent->tag == Y_JSON_INT); + + if (!newComments.insert({ + {pNode->value.integer, pContent->value.integer}, + {rChange.key, pArray} + }).second) + { + abort(); + } + }; + uint32_t lenK{0}; YEventKeyChange *const pChange{ymap_event_keys(pEvent, &lenK)}; for (decltype(lenK) j = 0; j < lenK; ++j) @@ -370,25 +486,7 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons break; case Y_ARRAY: { - // new comment - yvalidate(pMap == rState.rYrsSupplier.GetCommentMap()); - yvalidate(lenP == 0); - Branch const*const pArray{pChange[j].new_value->value.y_type}; - yvalidate(yarray_len(pArray) == 3); - ::std::unique_ptr<YOutput, YOutputDeleter> const pPos{yarray_get(pArray, pTxn, 0)}; - yvalidate(pPos->tag == Y_ARRAY); - ::std::unique_ptr<YOutput, YOutputDeleter> const pNode{yarray_get(pPos->value.y_type, pTxn, 0)}; - yvalidate(pNode->tag == Y_JSON_INT); - ::std::unique_ptr<YOutput, YOutputDeleter> const pContent{yarray_get(pPos->value.y_type, pTxn, 1)}; - yvalidate(pContent->tag == Y_JSON_INT); - - if (!newComments.insert({ - {pNode->value.integer, pContent->value.integer}, - {pChange[j].key, pArray} - }).second) - { - abort(); - } + HandleNewComment(pChange[j]); } break; case Y_MAP: @@ -421,14 +519,7 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons OString const commentId{pChange[j].key}; auto const it{rState.rYrsSupplier.GetComments().find(commentId)}; yvalidate(it != rState.rYrsSupplier.GetComments().end()); - SwWrtShell *const pShell{dynamic_cast<SwWrtShell*>(rState.rDoc.getIDocumentLayoutAccess().GetCurrentViewShell())}; - pShell->Push(); - *pShell->GetCursor()->GetPoint() = it->second.front()->GetAnchorPosition(); - pShell->SetMark(); - pShell->Right(SwCursorSkipMode::Chars, true, 1, false, false); - pShell->DelRight(); - pShell->Pop(SwCursorShell::PopMode::DeleteStack); - rState.rDoc.getIDocumentState().YrsRemoveCommentImpl(commentId); + HandleDeleteComment(commentId, it->second); break; } default: @@ -437,7 +528,7 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons break; case Y_EVENT_KEY_CHANGE_UPDATE: { - OString const prop{pChange[j].key}; + OString const key{pChange[j].key}; switch (pChange[j].new_value->tag) { case Y_JSON_BOOL: @@ -449,15 +540,27 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons // in props map... yvalidate(pPath[1].tag == Y_EVENT_PATH_INDEX); yvalidate(pPath[1].value.index == 1); - yvalidate(prop == "resolved"); + yvalidate(key == "resolved"); auto const it{rState.rYrsSupplier.GetComments().find(commentId)}; yvalidate(it != rState.rYrsSupplier.GetComments().end()); + it->second.front()->mpPostIt->SetResolved( pChange[j].new_value->value.flag == Y_TRUE); break; } + case Y_ARRAY: + { + auto const it{rState.rYrsSupplier.GetComments().find(key)}; + if (it != rState.rYrsSupplier.GetComments().end()) + { // comment may or may not exist + // if it does, have to delete it + HandleDeleteComment(key, it->second); + } + HandleNewComment(pChange[j]); + break; + } default: assert(false); } @@ -567,7 +670,6 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons yvalidate(it.first.second <= o3tl::make_unsigned(rNode.GetTextNode()->Len())); SwPosition anchorPos{*rNode.GetTextNode(), static_cast<sal_Int32>(it.first.second)}; SAL_INFO("sw.yrs", "YRS " << anchorPos); - SwWrtShell *const pShell{dynamic_cast<SwWrtShell*>(rState.rDoc.getIDocumentLayoutAccess().GetCurrentViewShell())}; SwPostItFieldType* pType = static_cast<SwPostItFieldType*>(pShell->GetFieldType(0, SwFieldIds::Postit)); auto pField{ new SwPostItField{ @@ -582,6 +684,8 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons { pField->SetParentPostItId(*oParentId); } + OString const commentId{it.second.first}; + pField->SetYrsCommentId(commentId); pShell->Push(); *pShell->GetCursor()->GetPoint() = anchorPos; @@ -598,10 +702,11 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons assert(b); (void)b; pShell->Pop(SwCursorShell::PopMode::DeleteCurrent); --anchorPos.nContent; - OString const commentId{it.second.first}; rState.rDoc.getIDocumentState().YrsAddCommentImpl(anchorPos, commentId); - rState.rYrsSupplier.GetComments().find(commentId)->second.front()->mpPostIt->GetOutlinerView()->GetEditView().YrsReadEEState(rState.pTxn); + SwAnnotationWin & rWin{*rState.rYrsSupplier.GetComments().find(commentId)->second.front()->mpPostIt}; + rWin.GetOutlinerView()->GetEditView().YrsReadEEState(rState.pTxn); + rWin.UpdateData(); // force SwPostItField::mpText for Undo! } // comments inserted, now check position updates for consistency @@ -624,6 +729,11 @@ extern "C" void observe_comments(void *const pState, uint32_t count, YEvent cons yvalidate(o3tl::make_unsigned(pos.GetNodeIndex().get()) == ::std::get<1>(rUpdate)); yvalidate(o3tl::make_unsigned(pos.GetContentIndex()) == ::std::get<2>(rUpdate)); } + + if (pShell->EndUndo() != SwUndoId::EMPTY) + { + rState.isUndoAction = true; + } } struct ObserveCursorState : public ObserveState @@ -1017,7 +1127,32 @@ IMPL_LINK(YrsThread, HandleMessage, void*, pVoid, void) } case ::std::underlying_type_t<Message>(Message::SendStateDiff): { + if (m_pDSM->m_pYrsSupplier->m_nTempUndoOffset != 0) + { + SAL_INFO("sw.yrs", "pre apply update: finish EE undo"); + yvalidate_undo(*m_pDSM, 0); + // prevent our yrs undo from being combined with peer yrs undo + m_pDSM->m_pYrsSupplier->CommitTransaction(); + yundo_manager_stop(m_pDSM->m_pYrsSupplier->GetUndoManager()); + // reset it before creating the SwUndo - not ideal but there + // is currently nothing to check it before the SwUndo and + // YrsEndUndo() then checks it + m_pDSM->m_pYrsSupplier->m_nTempUndoOffset = 0; + SwWrtShell *const pShell{dynamic_cast<SwWrtShell*>(m_pDSM->m_rDoc.getIDocumentLayoutAccess().GetCurrentViewShell())}; + if (sw::annotation::SwAnnotationWin *const pWin{ + pShell->GetView().GetPostItMgr()->GetActiveSidebarWin()}) + { + // note: this does YrsEndUndo() and thus possibly + // YrsCommitModified() but that should be a no-op because + // we called it already (unconditionally!) + pWin->UpdateData(); + } + else + // this should not be possible? LoseFocus calls YrsEndUndo + assert(false); + } SAL_INFO("sw.yrs", "apply update: " << yupdate_debug_v1(reinterpret_cast<char const*>(pBuf->begin()) + 1, length - 1)); + yvalidate_undo(*m_pDSM, 0); YTransaction *const pTxn{m_pDSM->m_pYrsSupplier->GetWriteTransaction()}; m_pDSM->m_pYrsSupplier->SetMode(IYrsTransactionSupplier::Mode::Replay); auto const err = ytransaction_apply(pTxn, reinterpret_cast<char const*>(pBuf->begin()) + 1, length - 1); @@ -1028,16 +1163,38 @@ IMPL_LINK(YrsThread, HandleMessage, void*, pVoid, void) } // let's have one observe_deep instead of a observe on every // YText - need to be notified on new comments being created... - ObserveState state{*m_pDSM->m_pYrsSupplier, m_pDSM->m_rDoc, pTxn}; + ObserveCommentState state{*m_pDSM->m_pYrsSupplier, m_pDSM->m_rDoc, pTxn, false}; YSubscription *const pSubComments = yobserve_deep(m_pDSM->m_pYrsSupplier->GetCommentMap(), &state, observe_comments); ObserveCursorState cursorState{*m_pDSM->m_pYrsSupplier, m_pDSM->m_rDoc, pTxn, {}}; // not sure if yweak_observe would work for (weakref) cursors YSubscription *const pSubCursors = yobserve_deep(m_pDSM->m_pYrsSupplier->GetCursorMap(), &cursorState, observe_cursors); + auto const undos{yundo_manager_undo_stack_len(m_pDSM->m_pYrsSupplier->GetUndoManager())}; m_pDSM->m_pYrsSupplier->CommitTransaction(true); + if (state.isUndoAction) + { + yundo_manager_stop(m_pDSM->m_pYrsSupplier->GetUndoManager()); + } + else if (undos != yundo_manager_undo_stack_len(m_pDSM->m_pYrsSupplier->GetUndoManager())) + { + // argh, something changed but nothing changed! + // example: both peers undo a delete comment then sync. + // add a dummy SwUndo to match the yrs one. + yundo_manager_stop(m_pDSM->m_pYrsSupplier->GetUndoManager()); + m_pDSM->m_rDoc.GetIDocumentUndoRedo().AppendUndo( + ::std::make_unique<DummyUndo>(m_pDSM->m_rDoc)); + } YrsCursorUpdates(cursorState); + yvalidate_undo(*m_pDSM, 0); m_pDSM->m_pYrsSupplier->SetMode(IYrsTransactionSupplier::Mode::Edit); yunobserve(pSubComments); yunobserve(pSubCursors); + if (!m_hasFirstState) + { + // the first message contains the "initial state" - don't allow Undo before that + m_pDSM->m_rDoc.GetIDocumentUndoRedo().DelAllUndoObj(); + yundo_manager_clear(m_pDSM->m_pYrsSupplier->GetUndoManager()); + m_hasFirstState = true; + } break; } } @@ -1061,10 +1218,13 @@ void DocumentStateManager::YrsNotifySetResolved(OString const& rCommentId, SwPos ::std::unique_ptr<YOutput, YOutputDeleter> const pProps{yarray_get(pComment->value.y_type, pTxn, 1)}; YInput const resolved{yinput_bool(rField.GetResolved() ? Y_TRUE : Y_FALSE)}; ymap_insert(pProps->value.y_type, pTxn, "resolved", &resolved); - YrsCommitModified(); + YrsCommitModified(false); + yundo_manager_stop(m_pYrsSupplier->GetUndoManager()); + // UpdateData() is called later than this + yvalidate_undo(*this, 1); } -void DocumentStateManager::YrsAddCommentImpl(SwPosition const& rAnchorPos, OString const& commentId) +void DocumentStateManager::YrsAddCommentImpl(SwPosition const& rAnchorPos, OString const& rCommentId) { SAL_INFO("sw.yrs", "YRS AddCommentImpl"); ::std::vector<SwAnnotationItem *> items; @@ -1081,11 +1241,11 @@ void DocumentStateManager::YrsAddCommentImpl(SwPosition const& rAnchorPos, OStri SwAnnotationWin *const pWin{ rShell.GetPostItMgr()->GetOrCreateAnnotationWindow(*it, isNew)}; assert(pWin); - pWin->GetOutlinerView()->GetEditView().SetYrsCommentId(m_pYrsSupplier.get(), commentId); + pWin->GetOutlinerView()->GetEditView().SetYrsCommentId(m_pYrsSupplier.get(), rCommentId); } } } - m_pYrsSupplier->m_Comments.emplace(commentId, items); + m_pYrsSupplier->m_Comments.emplace(rCommentId, items); } void DocumentStateManager::YrsAddComment(SwPosition const& rPos, @@ -1093,7 +1253,8 @@ void DocumentStateManager::YrsAddComment(SwPosition const& rPos, bool const isInsert) { SAL_INFO("sw.yrs", "YRS AddComment " << rPos); - OString const commentId{m_pYrsSupplier->GenNewCommentId()}; + OString const commentId{rField.GetYrsCommentId()}; + assert(!commentId.isEmpty()); // this calls EditViewInvalidate so prevent destroying pTxn YrsAddCommentImpl(rPos, commentId); YTransaction *const pTxn{m_pYrsSupplier->GetWriteTransaction()}; @@ -1189,6 +1350,13 @@ void DocumentStateManager::YrsAddComment(SwPosition const& rPos, // either update the cursors here, or wait for round-trip? //do it in 1 caller so that load document can batch it? CommitModified(); // SetModified is called earlier + if (isInsert) + { + // stop causes commit to create a new undo stack item + YrsCommitModified(false); + yundo_manager_stop(m_pYrsSupplier->GetUndoManager()); + yvalidate_undo(*this, 0); + } } void DocumentStateManager::YrsRemoveCommentImpl(OString const& rCommentId) @@ -1210,7 +1378,7 @@ void DocumentStateManager::YrsRemoveComment(SwPosition const& rPos) OString const commentId{it2->first}; YrsRemoveCommentImpl(commentId); YTransaction *const pTxn{m_pYrsSupplier->GetWriteTransaction()}; - if (!pTxn) + if (!pTxn || !m_rDoc.GetIDocumentUndoRedo().DoesUndo()) { return; } @@ -1267,9 +1435,50 @@ void DocumentStateManager::YrsRemoveComment(SwPosition const& rPos) yarray_insert_range(pPos->value.y_type, pTxn, 2, posArray, 2); } } + YrsCommitModified(false); + yundo_manager_stop(m_pYrsSupplier->GetUndoManager()); + if (m_rDoc.GetIDocumentUndoRedo().DoesUndo()) + { // SwUndoDelete is being constructed on stack and not yet inserted! + yvalidate_undo(*this, 1); + } // either update the cursors here, or wait for round-trip? } +void DocumentStateManager::YrsEndUndo() +{ + if (m_pYrsSupplier->m_Mode == IYrsTransactionSupplier::Mode::Edit) + { + YrsCommitModified(false); + yundo_manager_stop(m_pYrsSupplier->GetUndoManager()); + yvalidate_undo(*this, 0); + } +} + +void DocumentStateManager::YrsDoUndo(SfxUndoAction const*const pUndo) +{ + YrsCommitModified(false); + // this creates a transaction and commits + yvalidate(yundo_manager_undo(m_pYrsSupplier->GetUndoManager()) == Y_TRUE + // ugly: yundo_manager_undo returns false both for nothing happened + // and for error so manually ignore the case where it's expected + // nothing happens + || dynamic_cast<DummyUndo const*>(pUndo) != nullptr + || !m_pYrsReader.is()); // CppunitTest_sw_core_docnode asserts +//implicit yundo_manager_stop(m_pYrsSupplier->GetUndoManager()); + yvalidate_undo(*this, 0); +} + +void DocumentStateManager::YrsDoRedo(SfxUndoAction const*const pUndo) +{ + YrsCommitModified(false); + // this creates a transaction and commits + yvalidate(yundo_manager_redo(m_pYrsSupplier->GetUndoManager()) == Y_TRUE + || dynamic_cast<DummyUndo const*>(pUndo) != nullptr + || !m_pYrsReader.is()); // CppunitTest_sw_core_docnode asserts +//implicit yundo_manager_stop(m_pYrsSupplier->GetUndoManager()); + yvalidate_undo(*this, 0); +} + void DocumentStateManager::YrsNotifyCursorUpdate() { SwWrtShell *const pShell{dynamic_cast<SwWrtShell*>(m_rDoc.getIDocumentLayoutAccess().GetCurrentViewShell())}; @@ -1303,7 +1512,7 @@ void DocumentStateManager::YrsNotifyCursorUpdate() #if ENABLE_YRS_WEAK if (pWin->GetOutlinerView()->GetEditView().YrsWriteEECursor(pTxn, *pEntry->value.y_type, pCurrent.get())) { - YrsCommitModified(); + YrsCommitModified(true); } #else ESelection const sel{pWin->GetOutlinerView()->GetSelection()}; @@ -1327,7 +1536,7 @@ void DocumentStateManager::YrsNotifyCursorUpdate() YInput const input{yinput_yarray(positions.data(), 5)}; yarray_remove_range(pEntry->value.y_type, pTxn, 1, 1); yarray_insert_range(pEntry->value.y_type, pTxn, 1, &input, 1); - YrsCommitModified(); + YrsCommitModified(true); } } else @@ -1341,7 +1550,7 @@ void DocumentStateManager::YrsNotifyCursorUpdate() YInput const input{yinput_yarray(positions.data(), 3)}; yarray_remove_range(pEntry->value.y_type, pTxn, 1, 1); yarray_insert_range(pEntry->value.y_type, pTxn, 1, &input, 1); - YrsCommitModified(); + YrsCommitModified(true); } } #endif @@ -1356,7 +1565,7 @@ void DocumentStateManager::YrsNotifyCursorUpdate() YInput const input{yinput_null()}; yarray_remove_range(pEntry->value.y_type, pTxn, 1, 1); yarray_insert_range(pEntry->value.y_type, pTxn, 1, &input, 1); - YrsCommitModified(); + YrsCommitModified(true); } } else @@ -1379,7 +1588,7 @@ void DocumentStateManager::YrsNotifyCursorUpdate() YInput const input{yinput_yarray(positions.data(), 4)}; yarray_remove_range(pEntry->value.y_type, pTxn, 1, 1); yarray_insert_range(pEntry->value.y_type, pTxn, 1, &input, 1); - YrsCommitModified(); + YrsCommitModified(true); } } else @@ -1395,7 +1604,7 @@ void DocumentStateManager::YrsNotifyCursorUpdate() YInput const input{yinput_yarray(positions.data(), 2)}; yarray_remove_range(pEntry->value.y_type, pTxn, 1, 1); yarray_insert_range(pEntry->value.y_type, pTxn, 1, &input, 1); - YrsCommitModified(); + YrsCommitModified(true); } } } @@ -1492,6 +1701,9 @@ void DocumentStateManager::YrsInitAcceptor() } YrsAddComment(pos, oAnchorStart, rField, false); } + YrsCommitModified(false); // no txn for undo! + m_pYrsReader->m_hasFirstState = true; + yundo_manager_clear(m_pYrsSupplier->GetUndoManager()); // initiate sync of comments to other side //AddComment would have done this m_pYrsSupplier->GetWriteTransaction(); YrsNotifyCursorUpdate(); @@ -1576,29 +1788,39 @@ void DocumentStateManager::SetModified() if( m_rDoc.GetAutoCorrExceptWord() && !m_rDoc.GetAutoCorrExceptWord()->IsDeleted() ) m_rDoc.DeleteAutoCorrExceptWord(); - -#if ENABLE_YRS - SAL_INFO("sw.yrs", "YRS SetModified"); - YrsCommitModified(); -#endif } #if ENABLE_YRS -void DocumentStateManager::YrsCommitModified() +void DocumentStateManager::YrsCommitModified(bool const isUnfinishedUndo) { - if (m_pYrsSupplier->CommitTransaction() && m_pYrsReader.is()) + auto const undos{yundo_manager_undo_stack_len(m_pYrsSupplier->GetUndoManager())}; + if (!isUnfinishedUndo) + { + m_pYrsSupplier->m_nTempUndoOffset = 0; + } + if (m_pYrsSupplier->CommitTransaction()) { - uno::Sequence<sal_Int8> buf(5); - sal_Int8 * it{buf.getArray()}; - writeLength(it, 1); - *it = ::std::underlying_type_t<Message>(Message::RequestStateVector); - try { - m_pYrsReader->m_xConnection->write(buf); + if (isUnfinishedUndo) + { + if (undos != yundo_manager_undo_stack_len(m_pYrsSupplier->GetUndoManager())) + { + m_pYrsSupplier->m_nTempUndoOffset = -1; + } } - catch (io::IOException const&) + if (m_pYrsReader.is()) { - TOOLS_WARN_EXCEPTION("sw", "YRS CommitTransaction"); - m_pYrsReader->m_xConnection->close(); + uno::Sequence<sal_Int8> buf(5); + sal_Int8 * it{buf.getArray()}; + writeLength(it, 1); + *it = ::std::underlying_type_t<Message>(Message::RequestStateVector); + try { + m_pYrsReader->m_xConnection->write(buf); + } + catch (io::IOException const&) + { + TOOLS_WARN_EXCEPTION("sw", "YRS CommitTransaction"); + m_pYrsReader->m_xConnection->close(); + } } } } diff --git a/sw/source/core/fields/docufld.cxx b/sw/source/core/fields/docufld.cxx index 67cffc40bad7..b1b32f188ebe 100644 --- a/sw/source/core/fields/docufld.cxx +++ b/sw/source/core/fields/docufld.cxx @@ -1858,6 +1858,10 @@ std::unique_ptr<SwField> SwPostItField::Copy() const if (mpText) pRet->SetTextObject( *mpText ); +#if ENABLE_YRS + pRet->SetYrsCommentId(m_CommentId); +#endif + // Note: member <m_xTextObject> not copied. return std::unique_ptr<SwField>(pRet.release()); diff --git a/sw/source/core/inc/DocumentStateManager.hxx b/sw/source/core/inc/DocumentStateManager.hxx index 8245796bac95..ffade6f23613 100644 --- a/sw/source/core/inc/DocumentStateManager.hxx +++ b/sw/source/core/inc/DocumentStateManager.hxx @@ -79,15 +79,19 @@ public: void YrsInitAcceptor() override; void YrsInitConnector(css::uno::Any const& raConnector) override; IYrsTransactionSupplier::Mode SetYrsMode(IYrsTransactionSupplier::Mode mode) override; - void YrsCommitModified() override; + void YrsCommitModified(bool isUnfinishedUndo) override; void YrsNotifySetResolved(OString const& rCommentId, SwPostItField const& rField) override; + OString YrsGenNewCommentId() override; void YrsAddCommentImpl(SwPosition const& rPos, OString const& rCommentId) override; void YrsAddComment(SwPosition const& rPos, ::std::optional<SwPosition> oAnchorStart, SwPostItField const& rField, bool isInsert) override; void YrsRemoveCommentImpl(OString const& rCommentId) override; void YrsRemoveComment(SwPosition const& rPos) override; void YrsNotifyCursorUpdate() override; + void YrsEndUndo() override; + void YrsDoUndo(SfxUndoAction const* pUndo) override; + void YrsDoRedo(SfxUndoAction const* pUndo) override; #endif }; diff --git a/sw/source/core/undo/docundo.cxx b/sw/source/core/undo/docundo.cxx index 56a068890239..8f9fc0e4e464 100644 --- a/sw/source/core/undo/docundo.cxx +++ b/sw/source/core/undo/docundo.cxx @@ -702,11 +702,30 @@ bool UndoManager::impl_DoUndoRedo(UndoOrRedoType undoOrRedo, size_t nUndoOffset) // N.B. these may throw! if (UndoOrRedoType::Undo == undoOrRedo) { +#if ENABLE_YRS + SfxUndoAction * pUndo{GetUndoActionCount() ? GetUndoAction() : nullptr}; + if (auto const*const pListAction = dynamic_cast<SfxListUndoAction*>(pUndo)) + { + if (pListAction->GetId() == sal_uInt16(SwUndoId::HEADER_FOOTER)) + { // urgh! argh! this one destroys itself! aieeeee! + pUndo = nullptr; + } + } +#endif bRet = SdrUndoManager::UndoWithContext(context); +#if ENABLE_YRS + m_rState.YrsDoUndo(pUndo); +#endif } else { +#if ENABLE_YRS + SfxUndoAction *const pUndo{GetRedoActionCount() ? GetRedoAction() : nullptr}; +#endif bRet = SdrUndoManager::RedoWithContext(context); +#if ENABLE_YRS + m_rState.YrsDoRedo(pUndo); +#endif } if (bRet) diff --git a/sw/source/core/undo/rolbck.cxx b/sw/source/core/undo/rolbck.cxx index 7906e9496fd4..c961e6d539b6 100644 --- a/sw/source/core/undo/rolbck.cxx +++ b/sw/source/core/undo/rolbck.cxx @@ -59,6 +59,7 @@ #include <strings.hrc> #include <bookmark.hxx> #include <frameformats.hxx> + #include <memory> #include <utility> @@ -360,11 +361,10 @@ void SwHistorySetTextField::SetInDoc( SwDoc& rDoc, bool ) if (m_nFieldWhich == SwFieldIds::Postit) { SwPosition const pos{*pTextNd, m_nPos}; - pTextNd->GetDoc().getIDocumentState().YrsAddComment( - pos, {}, // FIXME no way to get anchor start here? - static_cast<SwPostItField const&>(*pTextNd->GetFieldTextAttrAt(pos.GetContentIndex(), - ::sw::GetTextAttrMode::Default)->GetFormatField().GetField()), - true); + // do use the same comment id because it's a ymap key! + OString const commentId{static_cast<SwPostItField const*>(m_pField->GetField())->GetYrsCommentId()}; + assert(!commentId.isEmpty()); + pTextNd->GetDoc().getIDocumentState().YrsAddCommentImpl(pos, commentId); } #endif } diff --git a/sw/source/core/undo/unattr.cxx b/sw/source/core/undo/unattr.cxx index 0fb796de9a2c..d54b37e7c999 100644 --- a/sw/source/core/undo/unattr.cxx +++ b/sw/source/core/undo/unattr.cxx @@ -59,6 +59,11 @@ #include <frameformats.hxx> #include <editsh.hxx> +#if ENABLE_YRS +#include <docufld.hxx> +#endif + + SwUndoFormatAttrHelper::SwUndoFormatAttrHelper(SwFormat& rFormat, bool bSvDrwPt) : SwClient(&rFormat) , m_rFormat(rFormat) @@ -927,6 +932,16 @@ void SwUndoAttr::redoAttribute(SwPaM& rPam, const sw::UndoRedoContext & rContext rDoc.getIDocumentRedlineAccess().SetRedlineFlags_intern( eOld ); } else { rDoc.getIDocumentContentOperations().InsertItemSet( rPam, m_AttrSet, m_nInsertFlags ); +#if ENABLE_YRS + SwFormatField const*const pItem{m_AttrSet.GetItemIfSet(RES_TXTATR_ANNOTATION, false)}; + if (pItem != nullptr && pItem->GetField()->Which() == SwFieldIds::Postit) + { + SwPosition const pos{rPam.GetPoint()->nContent, -1}; + OString const commentId{static_cast<SwPostItField const*>(pItem->GetField())->GetYrsCommentId()}; + assert(!commentId.isEmpty()); + rDoc.getIDocumentState().YrsAddCommentImpl(pos, commentId); + } +#endif } } diff --git a/sw/source/core/unocore/unofield.cxx b/sw/source/core/unocore/unofield.cxx index 746d21b1509f..e9d70d74d236 100644 --- a/sw/source/core/unocore/unofield.cxx +++ b/sw/source/core/unocore/unofield.cxx @@ -1394,6 +1394,9 @@ void SAL_CALL SwXTextField::attach( pPostItField->SetTextObject( m_pImpl->m_xTextObject->CreateText() ); pPostItField->SetPar2(m_pImpl->m_xTextObject->GetText()); } +#if ENABLE_YRS + pPostItField->SetYrsCommentId(pDoc->getIDocumentState().YrsGenNewCommentId()); +#endif xField.reset(pPostItField); } break; diff --git a/sw/source/uibase/docvw/AnnotationWin.cxx b/sw/source/uibase/docvw/AnnotationWin.cxx index 25d3dfdecf8b..21a79b078755 100644 --- a/sw/source/uibase/docvw/AnnotationWin.cxx +++ b/sw/source/uibase/docvw/AnnotationWin.cxx @@ -261,15 +261,18 @@ void SwAnnotationWin::SetResolved(bool resolved) mxMetadataResolved->hide(); if(IsResolved() != oldState) + { mbResolvedStateUpdated = true; +#if ENABLE_YRS + // for undo, before UpdateData() + mrView.GetDocShell()->GetDoc()->getIDocumentState().YrsNotifySetResolved( + GetOutlinerView()->GetEditView().GetYrsCommentId(), + *static_cast<SwPostItField const*>(mpFormatField->GetField())); +#endif + } UpdateData(); Invalidate(); collectUIInformation(u"SETRESOLVED"_ustr,get_id()); -#if ENABLE_YRS - mrView.GetDocShell()->GetDoc()->getIDocumentState().YrsNotifySetResolved( - GetOutlinerView()->GetEditView().GetYrsCommentId(), - *static_cast<SwPostItField const*>(mpFormatField->GetField())); -#endif } void SwAnnotationWin::ToggleResolved() @@ -376,6 +379,9 @@ void SwAnnotationWin::UpdateData() SwPosition aPosition( pTextField->GetTextNode(), pTextField->GetStart() ); rUndoRedo.AppendUndo( std::make_unique<SwUndoFieldFromDoc>(aPosition, *pOldField, *mpField, true)); +#if ENABLE_YRS + mrView.GetDocShell()->GetDoc()->getIDocumentState().YrsEndUndo(); +#endif } // so we get a new layout of notes (anchor position is still the same and we would otherwise not get one) mrMgr.SetLayout(); @@ -503,6 +509,9 @@ void SwAnnotationWin::InitAnswer(OutlinerParaObject const & rText) SwPosition aPosition( pTextField->GetTextNode(), pTextField->GetStart() ); rUndoRedo.AppendUndo( std::make_unique<SwUndoFieldFromDoc>(aPosition, *pOldField, *mpField, true)); +#if ENABLE_YRS +// no! there is a StartUndo wrapping this mrView.GetDocShell()->GetDoc()->getIDocumentState().YrsEndUndo(); +#endif } mpOutliner->SetModifyHdl( LINK( this, SwAnnotationWin, ModifyHdl ) ); mpOutliner->ClearModifyFlag(); diff --git a/sw/source/uibase/docvw/SidebarTxtControl.cxx b/sw/source/uibase/docvw/SidebarTxtControl.cxx index 945bbecfd11a..56554988499c 100644 --- a/sw/source/uibase/docvw/SidebarTxtControl.cxx +++ b/sw/source/uibase/docvw/SidebarTxtControl.cxx @@ -208,7 +208,7 @@ void SidebarTextControl::EditViewInvalidate(const tools::Rectangle& rRect) { SAL_INFO("sw.yrs", "YRS EditViewInvalidate"); mrDocView.GetDocShell()->GetDoc()->getIDocumentState().YrsNotifyCursorUpdate(); - mrDocView.GetDocShell()->GetDoc()->getIDocumentState().YrsCommitModified(); + mrDocView.GetDocShell()->GetDoc()->getIDocumentState().YrsCommitModified(true); return WeldEditView::EditViewInvalidate(rRect); } diff --git a/sw/source/uibase/fldui/fldmgr.cxx b/sw/source/uibase/fldui/fldmgr.cxx index a150cb86b498..27ceba387d23 100644 --- a/sw/source/uibase/fldui/fldmgr.cxx +++ b/sw/source/uibase/fldui/fldmgr.cxx @@ -921,6 +921,9 @@ bool SwFieldMgr::InsertField( pPostItField->SetParentPostItId(std::get<1>(*rData.m_oParentId)); pPostItField->SetParentName(std::get<2>(*rData.m_oParentId)); } +#if ENABLE_YRS + pPostItField->SetYrsCommentId(pCurShell->GetDoc()->getIDocumentState().YrsGenNewCommentId()); +#endif pField.reset(pPostItField); } break; @@ -1591,7 +1594,6 @@ bool SwFieldMgr::InsertField( pos, oAnchorStart, static_cast<SwPostItField const&>(*SwCursorShell::GetTextFieldAtPos(&pos, ::sw::GetTextAttrMode::Default)->GetFormatField().GetField()), true); - pCurShell->GetDoc()->getIDocumentState().YrsCommitModified(); } #endif