editeng/inc/editdoc.hxx                      |    2 
 editeng/source/editeng/editdoc.cxx           |  139 +++++++++++++--------
 editeng/source/editeng/editview.cxx          |   45 +++++++
 editeng/source/editeng/impedit.hxx           |   11 +
 editeng/source/editeng/impedit2.cxx          |  171 ++++++++++++++++++++++++++-
 editeng/source/editeng/impedit4.cxx          |    4 
 include/editeng/editview.hxx                 |    7 +
 svx/source/dialog/weldeditview.cxx           |    2 
 sw/inc/AnnotationWin.hxx                     |    3 
 sw/source/core/doc/DocumentStateManager.cxx  |  115 ++++++++++++++----
 sw/source/uibase/docvw/AnnotationWin2.cxx    |   16 ++
 sw/source/uibase/docvw/SidebarTxtControl.cxx |   29 ++++
 sw/source/uibase/docvw/SidebarTxtControl.hxx |    5 
 13 files changed, 464 insertions(+), 85 deletions(-)

New commits:
commit 0ff4c6619031582b6c520db90a17ed285973e1be
Author:     Michael Stahl <[email protected]>
AuthorDate: Fri Feb 14 18:03:16 2025 +0100
Commit:     Michael Stahl <[email protected]>
CommitDate: Mon May 12 11:33:51 2025 +0200

    LOCRDT editeng,sw: yrs peer cursors for editengine
    
    The cursors are represented as integers in yrs as a first step, which
    isn't ideal.
    
    Surprisingly EditView doesn't really have functions to correct the
    cursor, only ImpEditEngine::UpdateSelections() for the case when nodes
    are deleted, apparently there's only one active EditView at a time
    typically so it wasn't needed?  Typically the active EditView's cursor
    is assigned from a return value of ImpEditEngine.
    
    Add some new functions to correct peer cursors and call them all over
    ImpEditEngine; unfortunately UpdateSelectionsDelete() isn't easy to get
    working on EditView's actual EditSelection cursor so leave that for the
    future...
    
    To fix EE paint of non-active comment, add SwAnnotationWin::Paint()
    
    To get things to paint with "gen", it's enough to invalidate the
    SwAnnotationWin, but for gtk3 a queue_draw() must be called, else it's
    only painted when clicking on the window.
    
    Cursors are painted in color in SidebarTextControl::Paint()
    
    Change-Id: Id8696238290405554ece1b9961676972a559b58e
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/181679
    Tested-by: Jenkins
    Reviewed-by: Michael Stahl <[email protected]>

diff --git a/editeng/inc/editdoc.hxx b/editeng/inc/editdoc.hxx
index eb0d79da7e6e..cb252ee0c877 100644
--- a/editeng/inc/editdoc.hxx
+++ b/editeng/inc/editdoc.hxx
@@ -134,6 +134,8 @@ public:
     void YrsWriteEEState();
     void YrsReadEEState(YTransaction *, ImpEditEngine & rIEE);
     void YrsApplyEEDelta(YTransaction *, YTextEvent const* pEvent, 
ImpEditEngine & rIEE);
+    ::std::optional<EditSelection> YrsReadEECursor(::std::pair<int64_t, 
int64_t> point,
+            ::std::optional<::std::pair<int64_t, int64_t>> oMark);
     void YrsSetStyle(sal_Int32 nPara, ::std::u16string_view rStyle);
     void YrsSetParaAttr(sal_Int32 nPara, SfxPoolItem const& rItem);
     OString GetYrsCommentId() const;
diff --git a/editeng/source/editeng/editdoc.cxx 
b/editeng/source/editeng/editdoc.cxx
index 505ee1e118ac..45c89ef0dcfd 100644
--- a/editeng/source/editeng/editdoc.cxx
+++ b/editeng/source/editeng/editdoc.cxx
@@ -2712,75 +2712,105 @@ void EditDoc::YrsReadEEState(YTransaction *const pTxn, 
ImpEditEngine & rIEE)
     rIEE.RemoveParagraph(nodes); // remove pre-existing one from InitDoc()
 }
 
-static void YrsAdjustCursors(ImpEditEngine & rIEE, EditDoc & rDoc,
-    sal_Int32 const node, sal_Int32 const pos, ContentNode *const pNewNode, 
sal_Int32 const delta)
+::std::optional<EditSelection> EditDoc::YrsReadEECursor(
+    ::std::pair<int64_t, int64_t> const i_point,
+    ::std::optional<::std::pair<int64_t, int64_t>> const i_oMark)
 {
-    for (EditView *const pView : rIEE.GetEditViews())
+    // TODO should not use ints here?
+    ::std::optional<EditPaM> oMark;
+    if (i_oMark)
     {
-        bool bSet{false};
-        EditSelection sel{pView->getImpl().GetEditSelection()};
-        ContentNode const*const pNode{rDoc.GetObject(node)};
-        if (sel.Min().GetNode() == pNode
-            && pos <= sel.Min().GetIndex())
+//        yvalidate(i_oMark->first < Count());
+        if (Count() <= i_oMark->first)
         {
-            sel.Min().SetNode(pNewNode);
-            sel.Min().SetIndex(sel.Min().GetIndex() + delta);
-            bSet = true;
+            return {};
         }
-        if (sel.Max().GetNode() == pNode
-            && pos <= sel.Max().GetIndex())
+        ContentNode & rNode{*GetObject(i_oMark->first)};
+//        yvalidate(i_oMark->second <= o3tl::make_unsigned(rNode.Len()));
+        if (o3tl::make_unsigned(rNode.Len()) < i_oMark->second)
         {
-            sel.Max().SetNode(pNewNode);
-            sel.Max().SetIndex(sel.Max().GetIndex() + delta);
-            bSet = true;
+            return {};
         }
-        if (bSet)
+        oMark.emplace(&rNode, static_cast<sal_Int32>(i_oMark->second));
+    }
+
+    // the problem with using ints here is that multiple local edits may
+    // occur before the peer sends the updated cursor for the first edit,
+    // and then its position may be invalid; work around this here by
+    // ignoring obviously invalid cursors.
+//    yvalidate(i_point.first < Count());
+    if (Count() <= i_point.first)
+    {
+        return {};
+    }
+    ContentNode & rNode{*GetObject(i_point.first)};
+//    yvalidate(i_point.second <= o3tl::make_unsigned(rNode.Len()));
+    if (o3tl::make_unsigned(rNode.Len()) < i_point.second)
+    {
+        return {};
+    }
+    EditPaM point{&rNode, static_cast<sal_Int32>(i_point.second)};
+
+    return EditSelection{point, oMark ? *oMark : point};
+}
+
+static bool UpdatePosDelete(EditDoc & rDoc, EditPaM & rPos, ESelection const& 
rDeleted)
+{
+    auto const nPosNode{rDoc.GetPos(rPos.GetNode())};
+    if (rDeleted.start.nPara == nPosNode)
+    {
+        if (rDeleted.start.nIndex < rPos.GetIndex())
         {
-            pView->getImpl().SetEditSelection(sel);
+            if (rDeleted.start.nPara == rDeleted.end.nPara && 
rDeleted.end.nIndex < rPos.GetIndex())
+            {
+                rPos.SetIndex(rPos.GetIndex() - (rDeleted.end.nIndex - 
rDeleted.start.nIndex));
+            }
+            else
+            {
+                rPos.SetIndex(rDeleted.start.nIndex);
+            }
+            return true;
         }
     }
+    else if (rDeleted.start.nPara < nPosNode && nPosNode <= rDeleted.end.nPara)
+    {
+        if (nPosNode == rDeleted.end.nPara && rDeleted.end.nIndex < 
rPos.GetIndex())
+        {
+            rPos.SetIndex(rPos.GetIndex() - (rDeleted.end.nIndex - 
rDeleted.start.nIndex));
+        }
+        else
+        {
+            rPos.SetIndex(rDeleted.start.nIndex);
+        }
+        rPos.SetNode(rDoc.GetObject(rDeleted.start.nPara));
+        return true;
+    }
+#if 0
+    else if (rDeleted.end.nPara < nPosNode)
+    {
+        rPos.SetNode(rDoc.GetObject(nPosNode - (rDeleted.end.nPara - 
rDeleted.start.nPara)));
+        return true;
+    }
+#endif
+    return false;
+
 }
 
-// TODO test this
+// TODO this is now sort of duplicated as UpdateSelectionsDelete, but
+// consolidating that would probably require replacing EditSelection with
+// ESelection in EditView
 static void YrsAdjustCursorsDel(ImpEditEngine & rIEE, EditDoc & rDoc,
     sal_Int32 const startNode, sal_Int32 const startPos,
     sal_Int32 const endNode, sal_Int32 const endPos)
 {
+    ESelection const deleted{startNode, startPos, endNode, endPos};
     for (EditView *const pView : rIEE.GetEditViews())
     {
-        bool bSet{false};
         EditSelection sel{pView->getImpl().GetEditSelection()};
-        ContentNode *const pStartNode{rDoc.GetObject(startNode)};
-        ContentNode const*const pEndNode{rDoc.GetObject(endNode)};
-        if ((sel.Min().GetNode() == pStartNode && startPos < 
sel.Min().GetIndex())
-            || (startNode < rDoc.GetPos(sel.Min().GetNode()) && 
rDoc.GetPos(sel.Min().GetNode()) < endNode)
-            || (sel.Min().GetNode() == pEndNode && sel.Min().GetIndex() < 
endPos))
-        {
-            sel.Min().SetNode(pStartNode);
-            sel.Min().SetIndex(startPos);
-            bSet = true;
-        }
-        else if (sel.Min().GetNode() == pEndNode)
-        {
-            sel.Min().SetNode(pStartNode);
-            sel.Min().SetIndex(startPos + sel.Min().GetIndex() - endPos);
-            bSet = true;
-        }
-        if ((sel.Max().GetNode() == pStartNode && startPos < 
sel.Max().GetIndex())
-            || (startNode < rDoc.GetPos(sel.Max().GetNode()) && 
rDoc.GetPos(sel.Max().GetNode()) < endNode)
-            || (sel.Max().GetNode() == pEndNode && sel.Max().GetIndex() < 
endPos))
-        {
-            sel.Max().SetNode(pStartNode);
-            sel.Max().SetIndex(startPos);
-            bSet = true;
-        }
-        else if (sel.Max().GetNode() == pEndNode)
-        {
-            sel.Max().SetNode(pStartNode);
-            sel.Max().SetIndex(startPos + sel.Max().GetIndex() - endPos);
-            bSet = true;
-        }
-        if (bSet)
+        bool isChanged{false};
+        isChanged |= UpdatePosDelete(rDoc, sel.Min(), deleted);
+        isChanged |= UpdatePosDelete(rDoc, sel.Max(), deleted);
+        if (isChanged)
         {
             pView->getImpl().SetEditSelection(sel);
         }
@@ -2832,7 +2862,6 @@ void EditDoc::YrsApplyEEDelta(YTransaction *const 
/*pTxn*/, YTextEvent const*con
                                     { return {rEntry.key, rEntry.value}; };
                                 EditPaM const pam{maContents[node].get(), pos};
                                 YrsImplInsertFeature<YDeltaAttr>(rIEE, pam, 
pChange[i].attributes, pChange[i].attributes_len, GetAttr);
-                                YrsAdjustCursors(rIEE, *this, node, pos, 
maContents[node].get(), 1);
                                 ++pos;
                                 break;
                             }
@@ -2845,9 +2874,8 @@ void EditDoc::YrsApplyEEDelta(YTransaction *const 
/*pTxn*/, YTextEvent const*con
                                     EditSelection const 
sel{EditPaM{maContents[node].get(), pos}};
                                     rIEE.InsertText(sel, str.copy(index, 
iPara));
                                 }
-                                EditPaM const newPos{rIEE.SplitContent(node, 
pos + iPara)};
+                                rIEE.SplitContent(node, pos + iPara);
                                 rIEE.SetStyleSheet(node, pStyle);
-                                YrsAdjustCursors(rIEE, *this, node, pos, 
const_cast<ContentNode*>(newPos.GetNode()), -pos);
                                 index = iPara + 1;
                                 pos = 0;
                                 ++node;
@@ -2868,7 +2896,6 @@ void EditDoc::YrsApplyEEDelta(YTransaction *const 
/*pTxn*/, YTextEvent const*con
                             {
                                 EditSelection const 
sel{EditPaM{maContents[node].get(), pos}};
                                 rIEE.InsertText(sel, str.copy(index, 
str.getLength() - index));
-                                YrsAdjustCursors(rIEE, *this, node, pos, 
maContents[node].get(), str.getLength() - index);
                                 pos += str.getLength() - index;
                             }
                             break;
@@ -2936,6 +2963,8 @@ void EditDoc::YrsApplyEEDelta(YTransaction *const 
/*pTxn*/, YTextEvent const*con
                 if (pChange[i].tag == Y_EVENT_CHANGE_DELETE)
                 {
                     YrsAdjustCursorsDel(rIEE, *this, nodeStart, posStart, 
node, pos);
+                    ESelection const deleted{rIEE.CreateESel(*oSel)};
+                    rIEE.UpdateSelectionsDelete(deleted);
                     rIEE.DeleteSelected(*oSel);
                     node = nodeStart;
                     pos = posStart;
diff --git a/editeng/source/editeng/editview.cxx 
b/editeng/source/editeng/editview.cxx
index 29f0065ffeda..65152d487577 100644
--- a/editeng/source/editeng/editview.cxx
+++ b/editeng/source/editeng/editview.cxx
@@ -347,6 +347,51 @@ void 
EditView::GetSelectionRectangles(std::vector<tools::Rectangle>& rLogicRects
     return getImpl().GetSelectionRectangles(getImpl().GetEditSelection(), 
rLogicRects);
 }
 
+#if defined(YRS)
+void EditView::YrsGetSelectionRectangles(
+    ::std::vector<::std::pair<OUString, ::std::vector<tools::Rectangle>>>& 
rLogicRects) const
+{
+    for (auto const& it : getImpl().m_PeerCursors)
+    {
+        rLogicRects.push_back({it.second.first, {}});
+        EditSelection const 
sel{getEditEngine().getImpl().CreateSel(it.second.second)};
+        if (sel.HasRange())
+        {
+            getImpl().GetSelectionRectangles(sel, rLogicRects.back().second);
+        }
+        else
+        {
+            ParaPortion const& 
rParaPortion{getEditEngine().GetParaPortions().getRef(it.second.second.start.nPara)};
+            auto const 
oCursor{getImpl().ImplGetCursorRectAndMaybeScroll(sel.Min(), rParaPortion, 
false)};
+            if (oCursor)
+            {
+                auto const rect{std::get<0>(*oCursor)};
+                rLogicRects.back().second.push_back(rect);
+            }
+        }
+    }
+}
+
+void EditView::YrsApplyEECursor(OString const& rPeerId, OUString const& 
rAuthor,
+    ::std::pair<int64_t, int64_t> const point,
+    ::std::optional<::std::pair<int64_t, int64_t>> const oMark)
+{
+    ::std::optional<EditSelection> const 
oSel{getEditEngine().GetEditDoc().YrsReadEECursor(point, oMark)};
+    if (!oSel)
+    {
+        SAL_DEBUG("YRS ignoring invalid cursor position");
+        return;
+    }
+    ESelection const esel{getEditEngine().getImpl().CreateESel(*oSel)};
+    getImpl().m_PeerCursors[rPeerId] = {rAuthor, esel};
+}
+
+bool EditView::YrsDelEECursor(OString const& rId)
+{
+    return getImpl().m_PeerCursors.erase(rId) != 0;
+}
+#endif
+
 Point EditView::CalculateTextPaintStartPosition() const
 {
     return getImpEditEngine().CalculateTextPaintStartPosition(getImpl());
diff --git a/editeng/source/editeng/impedit.hxx 
b/editeng/source/editeng/impedit.hxx
index de80aa861af7..4353d6ba2068 100644
--- a/editeng/source/editeng/impedit.hxx
+++ b/editeng/source/editeng/impedit.hxx
@@ -287,6 +287,13 @@ private:
     EESelectionMode meSelectionMode;
     EditSelection maEditSelection;
     EEAnchorMode meAnchorMode;
+#if defined(YRS)
+public:
+    /// when SwAnnotationWin::UpdateData() is called, the EE is cleared
+    /// and recreated, so use ESelection not EditSelection to survive this!
+    ::std::unordered_map<OString, ::std::pair<OUString, ESelection>> 
m_PeerCursors;
+private:
+#endif
 
     /// mechanism to change from the classic refresh mode that simply
     // invalidates the area where text was changed. When set, the invalidate
@@ -702,7 +709,7 @@ private:
 
     void                ImpBreakLine(ParaPortion& rParaPortion, EditLine& 
rLine, TextPortion const * pPortion, sal_Int32 nPortionStart, tools::Long 
nRemainingWidth, bool bCanHyphenate);
     void                ImpAdjustBlocks(ParaPortion& rParaPortion, EditLine& 
rLine, tools::Long nRemainingSpace );
-    EditPaM             ImpConnectParagraphs( ContentNode* pLeft, ContentNode* 
pRight, bool bBackward = false );
+    EditPaM             ImpConnectParagraphs(ContentNode* pLeft, ContentNode* 
pRight, bool bBackward = false, bool isUpdateCursors = true);
     EditPaM             ImpDeleteSelection(const EditSelection& rCurSel);
     EditPaM             ImpInsertParaBreak( EditPaM& rPaM, bool 
bKeepEndingAttribs = true );
     EditPaM             ImpInsertParaBreak( const EditSelection& 
rEditSelection );
@@ -1013,6 +1020,8 @@ public:
 
     bool                    UpdateSelection(EditSelection &);
     void                    UpdateSelections();
+    void UpdateSelectionsDelete(ESelection const& rDeleted);
+    void UpdateSelectionsInsert(ESelection const& rInserted);
 
     void                EnableUndo( bool bEnable );
     bool IsUndoEnabled() const { return mbUndoEnabled; }
diff --git a/editeng/source/editeng/impedit2.cxx 
b/editeng/source/editeng/impedit2.cxx
index bc00e49613d0..1b022a323960 100644
--- a/editeng/source/editeng/impedit2.cxx
+++ b/editeng/source/editeng/impedit2.cxx
@@ -474,6 +474,7 @@ bool ImpEditEngine::Command( const CommandEvent& rCEvt, 
EditView* pView )
                 FormatAndLayout( pView );
             }
 
+            UpdateSelections(); // other views
             EditSelection aNewSel( EditPaM( mpIMEInfos->aPos.GetNode(), 
mpIMEInfos->aPos.GetIndex()+pData->GetCursorPos() ) );
             pView->SetSelection( CreateESel( aNewSel ) );
             pView->SetInsertMode( !pData->IsCursorOverwrite() );
@@ -2212,7 +2213,8 @@ EditSelection ImpEditEngine::ImpMoveParagraphs( Range 
aOldPositions, sal_Int32 n
 }
 
 
-EditPaM ImpEditEngine::ImpConnectParagraphs( ContentNode* pLeft, ContentNode* 
pRight, bool bBackward )
+EditPaM ImpEditEngine::ImpConnectParagraphs(ContentNode* pLeft, ContentNode* 
pRight,
+        bool bBackward, bool const isUpdateCursors)
 {
     OSL_ENSURE( pLeft != pRight, "Join together the same paragraph ?" );
     OSL_ENSURE(maEditDoc.GetPos(pLeft) != EE_PARA_MAX, "Inserted node not 
found (1)");
@@ -2230,6 +2232,7 @@ EditPaM ImpEditEngine::ImpConnectParagraphs( ContentNode* 
pLeft, ContentNode* pR
 
     sal_Int32 nParagraphTobeDeleted = maEditDoc.GetPos( pRight );
     maDeletedNodes.push_back(std::make_unique<DeletedNodeInfo>( pRight, 
nParagraphTobeDeleted ));
+    ESelection const deleted{nParagraphTobeDeleted - 1, pLeft->Len(), 
nParagraphTobeDeleted, 0};
 
     GetEditEnginePtr()->ParagraphConnected( maEditDoc.GetPos( pLeft ), 
maEditDoc.GetPos( pRight ) );
 
@@ -2297,6 +2300,11 @@ EditPaM ImpEditEngine::ImpConnectParagraphs( 
ContentNode* pLeft, ContentNode* pR
         }
     }
 
+    if (isUpdateCursors)
+    {
+        UpdateSelectionsDelete(deleted);
+    }
+
     TextModified();
 
     return aPaM;
@@ -2398,6 +2406,7 @@ EditPaM ImpEditEngine::ImpDeleteSelection(const 
EditSelection& rCurSel)
     if ( !rCurSel.HasRange() )
         return rCurSel.Min();
 
+    ESelection const deleted{CreateESel(rCurSel)};
     EditSelection aCurSel(rCurSel);
     aCurSel.Adjust( maEditDoc );
     EditPaM aStartPaM(aCurSel.Min());
@@ -2440,7 +2449,7 @@ EditPaM ImpEditEngine::ImpDeleteSelection(const 
EditSelection& rCurSel)
         assert(pPortion);
         pPortion->MarkSelectionInvalid( 0 );
         // Join together...
-        aStartPaM = ImpConnectParagraphs( aStartPaM.GetNode(), 
aEndPaM.GetNode() );
+        aStartPaM = ImpConnectParagraphs(aStartPaM.GetNode(), 
aEndPaM.GetNode(), false, false);
     }
     else
     {
@@ -2450,6 +2459,7 @@ EditPaM ImpEditEngine::ImpDeleteSelection(const 
EditSelection& rCurSel)
         pPortion->MarkInvalid( aEndPaM.GetIndex(), aStartPaM.GetIndex() - 
aEndPaM.GetIndex() );
     }
 
+    UpdateSelectionsDelete(deleted);
     UpdateSelections();
     TextModified();
     return aStartPaM;
@@ -2467,8 +2477,13 @@ void ImpEditEngine::RemoveParagraph( sal_Int32 nPara )
     if ( pNode && pPortion )
     {
         // No Undo encapsulation needed.
+        auto const len{pNode->Len()};
         ImpRemoveParagraph(nPara);
         InvalidateFromParagraph(nPara);
+        ESelection const deleted{nPara == 0 ? nPara : nPara - 1,
+            nPara == 0 ? 0 : maEditDoc.GetObject(nPara-1)->Len(),
+            nPara, len};
+        UpdateSelectionsDelete(deleted);
         UpdateSelections();
         if (IsUpdateLayout())
             FormatAndLayout();
@@ -2830,6 +2845,10 @@ EditPaM ImpEditEngine::ImpInsertText(const 
EditSelection& aCurSel, const OUStrin
 
     UndoActionEnd();
 
+    ESelection inserted{CreateEPaM(aCurPaM)};
+    inserted.end = CreateEPaM(aPaM);
+    UpdateSelectionsInsert(inserted);
+
     TextModified();
     return aPaM;
 }
@@ -2876,6 +2895,10 @@ EditPaM ImpEditEngine::ImpInsertFeature(const 
EditSelection& rCurSel, const SfxP
     assert(pPortion);
     pPortion->MarkInvalid( aPaM.GetIndex()-1, 1 );
 
+    ESelection inserted{CreateEPaM(rCurSel.Min())};
+    inserted.end = CreateEPaM(aPaM);
+    UpdateSelectionsInsert(inserted);
+
     TextModified();
 
     return aPaM;
@@ -2955,6 +2978,10 @@ EditPaM ImpEditEngine::ImpInsertParaBreak( EditPaM& 
rPaM, bool bKeepEndingAttrib
     if( nullptr != rPaM.GetNode() )
         rPaM.GetNode()->checkAndDeleteEmptyAttribs(); // if empty Attributes 
have emerged.
 
+    ESelection inserted{CreateEPaM(rPaM)};
+    inserted.end = CreateEPaM(aPaM);
+    UpdateSelectionsInsert(inserted);
+
     TextModified();
     return aPaM;
 }
@@ -3805,10 +3832,150 @@ void ImpEditEngine::UpdateSelections()
         {
             pView->getImpl().SetEditSelection(aCurSel);
         }
+#if defined(YRS)
+        for (auto & it : pView->getImpl().m_PeerCursors)
+        {
+            EditSelection sel(CreateSel(it.second.second));
+            if (UpdateSelection(sel))
+            {
+                assert(false); // should have called Insert/Delete?
+                it.second.second = CreateESel(sel);
+            }
+        }
+#endif
     }
     maDeletedNodes.clear();
 }
 
+#if defined(YRS)
+static bool UpdatePosDelete(EPaM & rPos, ESelection const& rDeleted)
+{
+    // correct towards start: RemoveParagraph(Count()) can't work otherwise
+    assert(rDeleted.IsAdjusted());
+    if (rDeleted.start.nPara == rPos.nPara)
+    {
+        if (rDeleted.start.nIndex < rPos.nIndex)
+        {
+            if (rDeleted.start.nPara == rDeleted.end.nPara && 
rDeleted.end.nIndex < rPos.nIndex)
+            {
+                rPos.nIndex -= (rDeleted.end.nIndex - rDeleted.start.nIndex);
+            }
+            else
+            {
+                rPos.nIndex = rDeleted.start.nIndex;
+            }
+            return true;
+        }
+    }
+    else if (rDeleted.start.nPara < rPos.nPara && rPos.nPara <= 
rDeleted.end.nPara)
+    {
+        if (rPos.nPara == rDeleted.end.nPara && rDeleted.end.nIndex < 
rPos.nIndex)
+        {
+            rPos.nIndex -= rDeleted.end.nIndex - rDeleted.start.nIndex;
+        }
+        else
+        {
+            rPos.nIndex = rDeleted.start.nIndex;
+        }
+        rPos.nPara = rDeleted.start.nPara;
+        return true;
+    }
+    else if (rDeleted.end.nPara < rPos.nPara)
+    {
+        rPos.nPara -= rDeleted.end.nPara - rDeleted.start.nPara;
+        return true;
+    }
+    return false;
+}
+#endif
+
+static bool UpdatePosInsert(EPaM & rPos, ESelection const& rInserted)
+{
+    assert(rInserted.IsAdjusted());
+    if (rInserted.start.nPara == rPos.nPara)
+    {
+        if (rInserted.start.nIndex <= rPos.nIndex)
+        {
+            rPos.nPara = rInserted.end.nPara;
+            rPos.nIndex = rInserted.end.nIndex + (rPos.nIndex - 
rInserted.start.nIndex);
+            return true;
+        }
+    }
+    else if (rInserted.start.nPara < rPos.nPara && rInserted.start.nPara != 
rInserted.end.nPara)
+    {
+        rPos.nPara += rInserted.end.nPara - rInserted.start.nPara;
+        return true;
+    }
+    return false;
+}
+
+void ImpEditEngine::UpdateSelectionsDelete(ESelection const& rDeleted)
+{
+    ESelection deleted(rDeleted);
+    deleted.Adjust();
+    for (EditView *const pView : maEditViews)
+    {
+        // FIXME fails because CreateESel needs the deleted nodes! but if it's
+        // called before deleting the nodes, it will assign wrong nodes...
+        // perhaps the cursors could be stored as ESelection in the first
+        // place, but that would require reviewing all code that inserts or
+        // deletes nodes if it does the update in the correct place...
+#if 0
+        EditSelection const sel{pView->getImpl().GetEditSelection()};
+        ESelection esel{CreateESel(sel)};
+        bool isChanged{false};
+        isChanged |= UpdatePosDelete(esel.start, deleted);
+        isChanged |= UpdatePosDelete(esel.end, deleted);
+        if (isChanged)
+        {
+            pView->getImpl().SetEditSelection(CreateSel(esel));
+        }
+#else
+        (void)pView;
+        (void)deleted;
+#endif
+#if defined(YRS)
+        for (auto & it : pView->getImpl().m_PeerCursors)
+        {
+            UpdatePosDelete(it.second.second.start, deleted);
+            UpdatePosDelete(it.second.second.end, deleted);
+        }
+#endif
+    }
+}
+
+void ImpEditEngine::UpdateSelectionsInsert(ESelection const& rInserted)
+{
+    for (EditView *const pView : maEditViews)
+    {
+        EditSelection const sel{pView->getImpl().GetEditSelection()};
+        ESelection esel{CreateESel(sel)};
+        // nodes have been inserted, but CreateESel counts them: adjust for 
this!
+        if (rInserted.start.nPara < esel.start.nPara)
+        {
+            esel.start.nPara -= rInserted.end.nPara - rInserted.start.nPara;
+        }
+        if (rInserted.start.nPara < esel.end.nPara)
+        {
+            esel.end.nPara -= rInserted.end.nPara - rInserted.start.nPara;
+        }
+        bool isChanged{false};
+        isChanged |= UpdatePosInsert(esel.start, rInserted);
+        isChanged |= UpdatePosInsert(esel.end, rInserted);
+        if (isChanged)
+        {
+            pView->getImpl().SetEditSelection(CreateSel(esel));
+        }
+#if defined(YRS)
+        for (auto & it : pView->getImpl().m_PeerCursors)
+        {
+            UpdatePosInsert(it.second.second.start, rInserted);
+            UpdatePosInsert(it.second.second.end, rInserted);
+        }
+#endif
+    }
+}
+
 EditSelection ImpEditEngine::ConvertSelection(
     sal_Int32 nStartPara, sal_Int32 nStartPos, sal_Int32 nEndPara, sal_Int32 
nEndPos )
 {
diff --git a/editeng/source/editeng/impedit4.cxx 
b/editeng/source/editeng/impedit4.cxx
index 3b7520a39658..54580a5439b4 100644
--- a/editeng/source/editeng/impedit4.cxx
+++ b/editeng/source/editeng/impedit4.cxx
@@ -3240,11 +3240,15 @@ short ImpEditEngine::ReplaceTextOnly(
 
             DBG_ASSERT( (nCurrentPos+1) < pNode->Len(), "TransliterateText - 
String smaller than expected!" );
             GetEditDoc().RemoveChars( EditPaM( pNode, nCurrentPos+1 ), -nDiff);
+            ESelection const deleted{maEditDoc.GetPos(pNode), nCurrentPos+1, 
maEditDoc.GetPos(pNode), nCurrentPos-nDiff};
+            UpdateSelectionsDelete(deleted);
         }
         else
         {
             DBG_ASSERT( nDiff == 1, "TransliterateText - Diff other than 
expected! But should work..." );
             GetEditDoc().InsertText( EditPaM( pNode, nCurrentPos ), 
OUStringChar(rNewText[n]) );
+            ESelection const inserted{maEditDoc.GetPos(pNode), nCurrentPos, 
maEditDoc.GetPos(pNode), nCurrentPos+nDiff};
+            UpdateSelectionsInsert(inserted);
 
         }
         nDiffs = sal::static_int_cast< short >(nDiffs + nDiff);
diff --git a/include/editeng/editview.hxx b/include/editeng/editview.hxx
index 750848eaee91..4e1ee56ef7ce 100644
--- a/include/editeng/editview.hxx
+++ b/include/editeng/editview.hxx
@@ -41,6 +41,7 @@
 class IYrsTransactionSupplier;
 typedef struct TransactionInner YTransaction;
 typedef struct YTextEvent YTextEvent;
+typedef struct Branch Branch;
 #endif
 
 class EditTextObject;
@@ -416,6 +417,12 @@ public:
     void YrsReadEEState(YTransaction *);
     void YrsApplyEEDelta(YTransaction *, YTextEvent const* pEvent);
     OString GetYrsCommentId() const;
+    void YrsApplyEECursor(OString const& rPeerId, OUString const& rAuthor,
+            ::std::pair<int64_t, int64_t> point,
+            ::std::optional<::std::pair<int64_t, int64_t>> oMark);
+    bool YrsDelEECursor(OString const& rPeerId);
+    void YrsGetSelectionRectangles(
+        ::std::vector<::std::pair<OUString, ::std::vector<tools::Rectangle>>>& 
rLogicRects) const;
 #endif
 };
 
diff --git a/svx/source/dialog/weldeditview.cxx 
b/svx/source/dialog/weldeditview.cxx
index 86d64454e0b4..399245547284 100644
--- a/svx/source/dialog/weldeditview.cxx
+++ b/svx/source/dialog/weldeditview.cxx
@@ -206,7 +206,6 @@ void WeldEditView::PaintSelection(vcl::RenderContext& 
rRenderContext, tools::Rec
         aLogicRanges.emplace_back(nLeft, nTop, nRight, nBottom);
     }
 
-    // get the system's highlight color
     sdr::overlay::OverlaySelection 
aCursorOverlay(sdr::overlay::OverlayType::Transparent, color,
                                                   std::move(aLogicRanges), 
true);
 
@@ -248,6 +247,7 @@ void WeldEditView::DoPaint(vcl::RenderContext& 
rRenderContext, const tools::Rect
     std::vector<tools::Rectangle> aLogicRects;
     pEditView->GetSelectionRectangles(aLogicRects);
 
+    // get the system's highlight color
     const Color aHighlight(SvtOptionsDrawinglayer::getHilightColor());
     PaintSelection(rRenderContext, rRect, aLogicRects, aHighlight);
 
diff --git a/sw/inc/AnnotationWin.hxx b/sw/inc/AnnotationWin.hxx
index c63d6d339d1d..63e7abeff956 100644
--- a/sw/inc/AnnotationWin.hxx
+++ b/sw/inc/AnnotationWin.hxx
@@ -211,11 +211,14 @@ class SAL_DLLPUBLIC_RTTI SwAnnotationWin final : public 
InterimItemWindow
         bool IsRootNote() const;
         void SetAsRoot();
 
+        void queue_draw();
+
     private:
 
         virtual void    LoseFocus() override;
         virtual void    GetFocus() override;
 
+        virtual void Paint(vcl::RenderContext& rRenderContext, const 
tools::Rectangle& rRect) override;
         void        SetSizePixel( const Size& rNewSize ) override;
 
         DECL_DLLPRIVATE_LINK(ModifyHdl, LinkParamNone*, void);
diff --git a/sw/source/core/doc/DocumentStateManager.cxx 
b/sw/source/core/doc/DocumentStateManager.cxx
index 91f39d7f7634..c0735e01de94 100644
--- a/sw/source/core/doc/DocumentStateManager.cxx
+++ b/sw/source/core/doc/DocumentStateManager.cxx
@@ -630,6 +630,7 @@ struct ObserveCursorState : public ObserveState
 {
     struct Update {
         OString const peerId;
+        ::std::optional<OString> const oCommentId;
         ::std::optional<OUString> const oAuthor;
         ::std::pair<int64_t, int64_t> const point;
         ::std::optional<::std::pair<int64_t, int64_t>> const oMark;
@@ -641,8 +642,25 @@ void YrsCursorUpdates(ObserveCursorState & rState)
 {
     for (auto const& it : rState.CursorUpdates)
     {
-#if defined(LOPLUGIN_BLOCK_BLOCK)
-#endif
+        if (it.oCommentId)
+        {
+            auto const 
it2{rState.rYrsSupplier.GetComments().find(*it.oCommentId)};
+            yvalidate(it2 != rState.rYrsSupplier.GetComments().end());
+            SwAnnotationWin & rWin{*it2->second.front()->mpPostIt};
+            // note: rState.pTxn is invalid at this point!
+            rWin.GetOutlinerView()->GetEditView().YrsApplyEECursor(it.peerId, 
*it.oAuthor, it.point, it.oMark);
+            if ((rWin.GetStyle() & WB_DIALOGCONTROL) == 0)
+            {
+                // note: Invalidate does work with gen but does not with gtk3
+                //rWin.Invalidate(); // not active window, force paint
+                rWin.queue_draw();
+            }
+            else
+            {   // apparently this repaints active window
+                rWin.GetOutlinerView()->GetEditView().Invalidate();
+            }
+        }
+        else
         {
             ::std::optional<SwPosition> oMark;
             if (it.oMark)
@@ -678,8 +696,43 @@ void YrsCursorUpdates(ObserveCursorState & rState)
     }
 }
 
+void YrsInvalidateEECursors(ObserveState const& rState,
+    OString const& rPeerId, OString const*const pCommentId)
+{
+    for (auto const& it : rState.rYrsSupplier.GetComments())
+    {
+        if (pCommentId == nullptr || *pCommentId != it.first)
+        {
+            SwAnnotationWin & rWin{*it.second.front()->mpPostIt};
+            if (rWin.GetOutlinerView()->GetEditView().YrsDelEECursor(rPeerId))
+            {
+                rWin.queue_draw(); // repaint
+            }
+        }
+    }
+}
+
+void YrsInvalidateSwCursors(ObserveState const& rState,
+    OString const& rPeerId, OUString const& rAuthor, bool const isAdd)
+{
+    for (SwViewShell & rShell : 
rState.rDoc.getIDocumentLayoutAccess().GetCurrentViewShell()->GetRingContainer())
+    {
+        if (auto const pShell{dynamic_cast<SwCursorShell *>(&rShell)})
+        {
+            if (isAdd)
+            {
+                pShell->YrsAddCursor(rPeerId, {}, {}, rAuthor);
+            }
+            else
+            {
+                pShell->YrsSetCursor(rPeerId, {}, {});
+            }
+        }
+    }
+}
+
 void YrsReadCursor(ObserveCursorState & rState, OString const& rPeerId,
-    YOutput const& rCursor, ::std::optional<OUString> const oAuthor)
+    YOutput const& rCursor, OUString const& rAuthor, bool const isAdd)
 {
     switch (rCursor.tag)
     {
@@ -689,10 +742,32 @@ void YrsReadCursor(ObserveCursorState & rState, OString 
const& rPeerId,
             auto const len{yarray_len(pArray)};
             if (len == 3 || len == 5)
             {
-                // TODO cursor in EE
+                ::std::unique_ptr<YOutput, YOutputDeleter> const 
pComment{yarray_get(pArray, rState.pTxn, 0)};
+                yvalidate(pComment->tag == Y_JSON_STR && pComment->len < 
SAL_MAX_INT32);
+                OString const commentId{pComment->value.str, 
static_cast<sal_Int32>(pComment->len)};
+                YrsInvalidateEECursors(rState, rPeerId, &commentId);
+                YrsInvalidateSwCursors(rState, rPeerId, rAuthor, false);
+                ::std::optional<::std::pair<int64_t, int64_t>> oMark;
+                if (len == 5)
+                {
+                    ::std::unique_ptr<YOutput, YOutputDeleter> const pNode{
+                        yarray_get(pArray, rState.pTxn, 3)};
+                    yvalidate(pNode->tag == Y_JSON_INT);
+                    ::std::unique_ptr<YOutput, YOutputDeleter> const pContent{
+                        yarray_get(pArray, rState.pTxn, 4)};
+                    yvalidate(pContent->tag == Y_JSON_INT);
+                    oMark.emplace(pNode->value.integer, 
pContent->value.integer);
+                }
+                ::std::unique_ptr<YOutput, YOutputDeleter> const 
pNode{yarray_get(pArray, rState.pTxn, 1)};
+                yvalidate(pNode->tag == Y_JSON_INT);
+                ::std::unique_ptr<YOutput, YOutputDeleter> const 
pContent{yarray_get(pArray, rState.pTxn, 2)};
+                yvalidate(pContent->tag == Y_JSON_INT);
+                ::std::pair<int64_t, int64_t> const pos{pNode->value.integer, 
pContent->value.integer};
+                rState.CursorUpdates.emplace_back(rPeerId, 
::std::optional<OString>{commentId}, ::std::optional<OUString>{rAuthor}, pos, 
oMark);
             }
             else if (len == 2 || len == 4)
             {
+                YrsInvalidateEECursors(rState, rPeerId, nullptr);
                 // the only reason this stuff has a hope of working with
                 // integers is that the SwNodes is read-only
                 ::std::optional<::std::pair<int64_t, int64_t>> oMark;
@@ -710,27 +785,17 @@ void YrsReadCursor(ObserveCursorState & rState, OString 
const& rPeerId,
                 ::std::unique_ptr<YOutput, YOutputDeleter> const 
pContent{yarray_get(pArray, rState.pTxn, 1)};
                 yvalidate(pContent->tag == Y_JSON_INT);
                 ::std::pair<int64_t, int64_t> const pos{pNode->value.integer, 
pContent->value.integer};
-                rState.CursorUpdates.emplace_back(rPeerId, oAuthor, pos, 
oMark);
+                rState.CursorUpdates.emplace_back(rPeerId, 
::std::optional<OString>{},
+                    isAdd ? ::std::optional<OUString>{rAuthor} : 
::std::optional<OUString>{},
+                    pos, oMark);
             }
             else
                 yvalidate(false);
             break;
         }
         case Y_JSON_NULL:
-            for (SwViewShell & rShell : 
rState.rDoc.getIDocumentLayoutAccess().GetCurrentViewShell()->GetRingContainer())
-            {
-                if (auto const pShell{dynamic_cast<SwCursorShell *>(&rShell)})
-                {
-                    if (oAuthor)
-                    {
-                        pShell->YrsAddCursor(rPeerId, {}, {}, *oAuthor);
-                    }
-                    else
-                    {
-                        pShell->YrsSetCursor(rPeerId, {}, {});
-                    }
-                }
-            }
+            YrsInvalidateEECursors(rState, rPeerId, nullptr);
+            YrsInvalidateSwCursors(rState, rPeerId, rAuthor, isAdd);
             break;
         default:
             yvalidate(false);
@@ -783,7 +848,7 @@ extern "C" void observe_cursors(void *const pState, 
uint32_t count, YEvent const
                                         static_cast<sal_Int32>(pAuthor->len), 
RTL_TEXTENCODING_UTF8};
                                     ::std::unique_ptr<YOutput, YOutputDeleter> 
const pCursor{
                                         yarray_get(pArray, rState.pTxn, 1)};
-                                    YrsReadCursor(rState, peerId, *pCursor, 
{author});
+                                    YrsReadCursor(rState, peerId, *pCursor, 
author, true);
                                     break;
                                 }
                                 default:
@@ -830,7 +895,10 @@ extern "C" void observe_cursors(void *const pState, 
uint32_t count, YEvent const
                 yvalidate(pChange[1].len == 1);
                 yvalidate(pChange[2].tag == Y_EVENT_CHANGE_ADD);
                 yvalidate(pChange[2].len == 1);
-                YrsReadCursor(rState, peerId, pChange[2].values[0], {});
+                Branch const*const pArray{yarray_event_target(pEvent)};
+                ::std::unique_ptr<YOutput, YOutputDeleter> const 
pAuthor{yarray_get(pArray, rState.pTxn, 0)};
+                OUString const author{pAuthor->value.str, 
static_cast<sal_Int32>(pAuthor->len), RTL_TEXTENCODING_UTF8};
+                YrsReadCursor(rState, peerId, pChange[2].values[0], author, 
false);
                 break;
             }
             default:
@@ -1168,12 +1236,12 @@ void DocumentStateManager::YrsRemoveComment(SwPosition 
const& rPos, OString cons
 void DocumentStateManager::YrsNotifyCursorUpdate()
 {
     SwWrtShell *const 
pShell{dynamic_cast<SwWrtShell*>(m_rDoc.getIDocumentLayoutAccess().GetCurrentViewShell())};
-    if (!m_pYrsSupplier || !pShell->GetView().GetPostItMgr())
+    YTransaction *const pTxn{m_pYrsSupplier ? 
m_pYrsSupplier->GetWriteTransaction() : nullptr};
+    if (!pTxn || !pShell->GetView().GetPostItMgr())
     {
         return;
     }
     SAL_DEBUG("YRS NotifyCursorUpdate");
-    YTransaction *const pTxn{m_pYrsSupplier->GetWriteTransaction()};
     YDoc *const pYDoc{m_pYrsSupplier->GetYDoc()};
     auto const id{ydoc_id(pYDoc)};
     ::std::unique_ptr<YOutput, YOutputDeleter> 
pEntry{ymap_get(m_pYrsSupplier->m_pCursors, pTxn, 
OString::number(id).getStr())};
@@ -1464,7 +1532,6 @@ void DocumentStateManager::SetModified()
 
 #if defined(YRS)
     SAL_DEBUG("YRS SetModified");
-    // FIXME: this is called only on LoseFocus! not while editing comment
     YrsCommitModified();
 #endif
 }
diff --git a/sw/source/uibase/docvw/AnnotationWin2.cxx 
b/sw/source/uibase/docvw/AnnotationWin2.cxx
index 59843da348d6..4560672bb304 100644
--- a/sw/source/uibase/docvw/AnnotationWin2.cxx
+++ b/sw/source/uibase/docvw/AnnotationWin2.cxx
@@ -937,6 +937,22 @@ void SwAnnotationWin::LoseFocus()
 {
 }
 
+void SwAnnotationWin::queue_draw()
+{
+    if (mxSidebarTextControlWin)
+    {
+        mxSidebarTextControlWin->queue_draw();
+    }
+}
+
+void SwAnnotationWin::Paint(vcl::RenderContext& rRenderContext, const 
tools::Rectangle& rRect)
+{
+    if (mxSidebarTextControl)
+    {
+        mxSidebarTextControl->Paint(rRenderContext, rRect);
+    }
+}
+
 void SwAnnotationWin::ShowNote()
 {
     SetPosAndSize();
diff --git a/sw/source/uibase/docvw/SidebarTxtControl.cxx 
b/sw/source/uibase/docvw/SidebarTxtControl.cxx
index e59dbe75c2de..ebc8cde72761 100644
--- a/sw/source/uibase/docvw/SidebarTxtControl.cxx
+++ b/sw/source/uibase/docvw/SidebarTxtControl.cxx
@@ -57,6 +57,7 @@
 #include <IDocumentDeviceAccess.hxx>
 #if defined(YRS)
 #include <IDocumentState.hxx>
+#include <swmodule.hxx>
 #endif
 #include <redline.hxx>
 #include <memory>
@@ -205,9 +206,18 @@ OUString SidebarTextControl::RequestHelp(tools::Rectangle& 
rHelpRect)
 #if defined(YRS)
 void SidebarTextControl::EditViewInvalidate(const tools::Rectangle& rRect)
 {
+    SAL_DEBUG("YRS EditViewInvalidate");
+    
mrDocView.GetDocShell()->GetDoc()->getIDocumentState().YrsNotifyCursorUpdate();
     mrDocView.GetDocShell()->GetDoc()->getIDocumentState().YrsCommitModified();
     return WeldEditView::EditViewInvalidate(rRect);
 }
+
+void SidebarTextControl::EditViewSelectionChange()
+{
+    SAL_DEBUG("YRS EditViewSelectionChange");
+    
mrDocView.GetDocShell()->GetDoc()->getIDocumentState().YrsNotifyCursorUpdate();
+    return WeldEditView::EditViewSelectionChange();
+}
 #endif
 
 void SidebarTextControl::EditViewScrollStateChange()
@@ -268,6 +278,25 @@ void SidebarTextControl::Paint(vcl::RenderContext& 
rRenderContext, const tools::
 
     DoPaint(rRenderContext, rRect);
 
+#if defined(YRS)
+    if (EditView *const pEditView{GetEditView()})
+    {
+        rRenderContext.Push(vcl::PushFlags::ALL);
+        rRenderContext.SetClipRegion();
+
+        ::std::vector<::std::pair<OUString, ::std::vector<tools::Rectangle>>> 
rects;
+        pEditView->YrsGetSelectionRectangles(rects);
+        for (auto const& it : rects)
+        {
+            ::std::size_t const 
authorId{SwModule::get()->InsertRedlineAuthor(it.first)};
+            Color const color{SwPostItMgr::GetColorAnchor(authorId)};
+            PaintSelection(rRenderContext, rRect, it.second, color);
+        }
+
+        rRenderContext.Pop();
+    }
+#endif
+
     if (mrSidebarWin.GetLayoutStatus() != SwPostItHelper::DELETED)
         return;
 
diff --git a/sw/source/uibase/docvw/SidebarTxtControl.hxx 
b/sw/source/uibase/docvw/SidebarTxtControl.hxx
index bbce1ac833e7..22b7a8d3fb32 100644
--- a/sw/source/uibase/docvw/SidebarTxtControl.hxx
+++ b/sw/source/uibase/docvw/SidebarTxtControl.hxx
@@ -40,8 +40,6 @@ class SidebarTextControl : public WeldEditView
         void MakeVisible();
 
     protected:
-        virtual void Paint(vcl::RenderContext& rRenderContext, const 
tools::Rectangle& rRect) override;
-
         virtual bool Command(const CommandEvent& rCEvt) override;
         virtual void GetFocus() override;
         virtual void LoseFocus() override;
@@ -53,12 +51,15 @@ class SidebarTextControl : public WeldEditView
                            SwView& rDocView,
                            SwPostItMgr& rPostItMgr);
 
+        virtual void Paint(vcl::RenderContext& rRenderContext, const 
tools::Rectangle& rRect) override;
+
         virtual EditView* GetEditView() const override;
 
         virtual EditEngine* GetEditEngine() const override;
 
 #if defined(YRS)
         virtual void EditViewInvalidate(const tools::Rectangle& rRect) 
override;
+        virtual void EditViewSelectionChange() override;
 #endif
         virtual void EditViewScrollStateChange() override;
 

Reply via email to