This can be enabled in configure via --with-yrs=...; see also README.yrs Currently this is hardcoded to run over a local named pipe. Everything related to editengine is implemented in EditDoc and made accessible via its wrapper classes; everything related to communication and accessing sw's comment is implemented in sw::DocumentStateManager. The acceptor starts in SwView::SwView(), once SwPostItMgr exists. There is a hack in SfxFrameLoader_Impl::load() to fetch the document that the accepting soffice has loaded, and load it in the connecting soffice as well. Change-Id: I89476b5864b70f479bcf15989374c1c65b5da9ea Reviewed-on: https://gerrit.libreoffice.org/c/core/+/175652 Tested-by: Jenkins Reviewed-by: Michael Stahl <michael.stahl@allotropia.de>
1222 lines
51 KiB
C++
1222 lines
51 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
|
/*
|
|
* This file is part of the LibreOffice project.
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*
|
|
* This file incorporates work covered by the following license notice:
|
|
*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed
|
|
* with this work for additional information regarding copyright
|
|
* ownership. The ASF licenses this file to you under the Apache
|
|
* License, Version 2.0 (the "License"); you may not use this file
|
|
* except in compliance with the License. You may obtain a copy of
|
|
* the License at http://www.apache.org/licenses/LICENSE-2.0 .
|
|
*/
|
|
#include <DocumentStateManager.hxx>
|
|
#include <doc.hxx>
|
|
#include <DocumentStatisticsManager.hxx>
|
|
#include <IDocumentUndoRedo.hxx>
|
|
#include <DocumentLayoutManager.hxx>
|
|
#include <acorrect.hxx>
|
|
|
|
#if defined(YRS)
|
|
#include <AnnotationWin.hxx>
|
|
#include <wrtsh.hxx>
|
|
#include <view.hxx>
|
|
#include <docufld.hxx>
|
|
#include <PostItMgr.hxx>
|
|
#include <IDocumentFieldsAccess.hxx>
|
|
#include <txtannotationfld.hxx>
|
|
#include <ndtxt.hxx>
|
|
|
|
#include <editeng/yrs.hxx>
|
|
#include <editeng/editview.hxx>
|
|
#include <editeng/outliner.hxx>
|
|
|
|
#include <sfx2/docfile.hxx>
|
|
|
|
#include <vcl/svapp.hxx>
|
|
|
|
#include <ucbhelper/content.hxx>
|
|
|
|
#include <sax/tools/converter.hxx>
|
|
|
|
#include <comphelper/diagnose_ex.hxx>
|
|
#include <comphelper/processfactory.hxx>
|
|
|
|
#include <com/sun/star/connection/Acceptor.hpp>
|
|
#include <com/sun/star/connection/Connector.hpp>
|
|
#include <com/sun/star/io/IOException.hpp>
|
|
#include <com/sun/star/io/XSeekable.hpp>
|
|
|
|
#include <salhelper/thread.hxx>
|
|
#endif
|
|
|
|
namespace sw
|
|
{
|
|
|
|
#if defined(YRS)
|
|
using sw::annotation::SwAnnotationWin;
|
|
|
|
namespace {
|
|
|
|
enum class Message : sal_uInt8
|
|
{
|
|
//Init,
|
|
RequestStateVector,
|
|
SendStateVector,
|
|
SendStateDiff,
|
|
};
|
|
|
|
struct YDocDeleter { void operator()(YDoc *const p) const { ydoc_destroy(p); } };
|
|
|
|
::std::unique_ptr<YDoc, YDocDeleter> YrsMakeYDoc()
|
|
{
|
|
YOptions o{yoptions()};
|
|
o.encoding = Y_OFFSET_UTF16;
|
|
YDoc *const pYDoc{ydoc_new_with_options(o)};
|
|
return ::std::unique_ptr<YDoc, YDocDeleter>{pYDoc};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class YrsTransactionSupplier : public IYrsTransactionSupplier
|
|
{
|
|
private:
|
|
friend class DocumentStateManager;
|
|
::std::unique_ptr<YDoc, YDocDeleter> m_pYDoc;
|
|
Branch * m_pComments;
|
|
Branch * m_pCursors;
|
|
YTransaction * m_pCurrentReadTransaction { nullptr };
|
|
YTransaction * m_pCurrentWriteTransaction { nullptr };
|
|
int m_nLocalComments { 0 };
|
|
::std::map<OString, ::std::vector<SwAnnotationItem *>> m_Comments;
|
|
|
|
public:
|
|
YrsTransactionSupplier();
|
|
~YrsTransactionSupplier();
|
|
|
|
YDoc* GetYDoc() override { return m_pYDoc.get(); }
|
|
Branch* GetCommentMap() override { return m_pComments; }
|
|
Branch* GetCursorMap() override { return m_pCursors; }
|
|
YTransaction * GetReadTransaction() override;
|
|
YTransaction * GetWriteTransaction() override;
|
|
bool CommitTransaction(bool isForce = false) override;
|
|
OString GenNewCommentId() override;
|
|
decltype(m_Comments) const& GetComments() { return m_Comments; }
|
|
};
|
|
|
|
YrsTransactionSupplier::YrsTransactionSupplier()
|
|
: m_pYDoc(YrsMakeYDoc())
|
|
// ymap implicitly calls transact_mut()
|
|
, m_pComments(ymap(m_pYDoc.get(), "comments"))
|
|
, m_pCursors(ymap(m_pYDoc.get(), "cursors"))
|
|
{
|
|
}
|
|
|
|
YrsTransactionSupplier::~YrsTransactionSupplier()
|
|
{
|
|
assert(m_pCurrentWriteTransaction == nullptr);
|
|
}
|
|
|
|
YTransaction * YrsTransactionSupplier::GetReadTransaction()
|
|
{
|
|
if (m_pCurrentWriteTransaction)
|
|
{
|
|
return m_pCurrentWriteTransaction;
|
|
}
|
|
if (m_pCurrentReadTransaction)
|
|
{
|
|
return m_pCurrentReadTransaction;
|
|
}
|
|
m_pCurrentReadTransaction = ydoc_read_transaction(m_pYDoc.get());
|
|
return m_pCurrentReadTransaction;
|
|
}
|
|
|
|
YTransaction * YrsTransactionSupplier::GetWriteTransaction()
|
|
{
|
|
if (m_Mode != Mode::Edit)
|
|
{
|
|
return nullptr;
|
|
}
|
|
if (m_pCurrentWriteTransaction)
|
|
{
|
|
return m_pCurrentWriteTransaction;
|
|
}
|
|
if (m_pCurrentReadTransaction)
|
|
{
|
|
// commit it? or is it an error?
|
|
assert(false);
|
|
}
|
|
m_pCurrentWriteTransaction = ydoc_write_transaction(m_pYDoc.get(), 0, nullptr);
|
|
return m_pCurrentWriteTransaction;
|
|
}
|
|
|
|
bool YrsTransactionSupplier::CommitTransaction(bool const isForce)
|
|
{
|
|
bool ret{false};
|
|
if (!isForce && m_Mode == Mode::Replay)
|
|
{
|
|
return ret;
|
|
}
|
|
if (m_pCurrentWriteTransaction)
|
|
{
|
|
assert(m_pCurrentReadTransaction == nullptr);
|
|
ytransaction_commit(m_pCurrentWriteTransaction);
|
|
m_pCurrentWriteTransaction = nullptr;
|
|
ret = true;
|
|
}
|
|
if (m_pCurrentReadTransaction)
|
|
{
|
|
ytransaction_commit(m_pCurrentReadTransaction);
|
|
m_pCurrentReadTransaction = nullptr;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
OString YrsTransactionSupplier::GenNewCommentId()
|
|
{
|
|
auto const id{ydoc_id(m_pYDoc.get())};
|
|
auto const counter{++m_nLocalComments};
|
|
return OString::number(id) + OString::number(counter);
|
|
}
|
|
|
|
class YrsThread : public ::salhelper::Thread
|
|
{
|
|
public:
|
|
uno::Reference<connection::XConnection> m_xConnection;
|
|
DocumentStateManager * m_pDSM;
|
|
|
|
public:
|
|
YrsThread(uno::Reference<connection::XConnection> const& xConnection,
|
|
DocumentStateManager & rDSM)
|
|
: ::salhelper::Thread("yrs reader")
|
|
, m_xConnection(xConnection)
|
|
, m_pDSM(&rDSM)
|
|
{
|
|
}
|
|
|
|
void execute() override
|
|
{
|
|
uno::Sequence<sal_Int8> buf;
|
|
while (m_xConnection->read(buf, 4) == 4)
|
|
{
|
|
sal_Int32 const size{static_cast<sal_uInt8>(buf[0])
|
|
| static_cast<sal_uInt8>(buf[1]) << 8
|
|
| static_cast<sal_uInt8>(buf[2]) << 16
|
|
| static_cast<sal_uInt8>(buf[3]) << 24};
|
|
if (size == 0)
|
|
{
|
|
SAL_DEBUG("YRS 0");
|
|
break;
|
|
}
|
|
SAL_DEBUG("YRS receive " << size);
|
|
::std::unique_ptr<uno::Sequence<sal_Int8>> pBuf{new uno::Sequence<sal_Int8>(size)};
|
|
m_xConnection->read(*pBuf, size);
|
|
Application::PostUserEvent(LINK(this, YrsThread, HandleMessage), pBuf.release());
|
|
}
|
|
}
|
|
|
|
DECL_LINK(HandleMessage, void*, void);
|
|
};
|
|
|
|
namespace {
|
|
|
|
struct ObserveState
|
|
{
|
|
YrsTransactionSupplier & rYrsSupplier;
|
|
SwDoc & rDoc;
|
|
YTransaction *const pTxn;
|
|
};
|
|
|
|
extern "C" void observe_comments(void *const pState, uint32_t count, YEvent const*const events)
|
|
{
|
|
SAL_DEBUG("YRS observe_comments");
|
|
ObserveState & rState{*static_cast<ObserveState*>(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};
|
|
|
|
::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;
|
|
|
|
for (decltype(count) i = 0; i < count; ++i)
|
|
{
|
|
switch (events[i].tag)
|
|
{
|
|
case Y_TEXT:
|
|
{
|
|
YTextEvent const*const pEvent{&events[i].content.text};
|
|
Branch const*const pText{ytext_event_target(pEvent)};
|
|
(void)pText;
|
|
|
|
uint32_t lenP{0};
|
|
YPathSegment *const pPath{ytext_event_path(pEvent, &lenP)};
|
|
yvalidate(lenP == 2);
|
|
yvalidate(pPath[1].tag == Y_EVENT_PATH_INDEX);
|
|
yvalidate(pPath[1].value.index == 2);
|
|
yvalidate(pPath[0].tag == Y_EVENT_PATH_KEY);
|
|
OString const commentId{pPath[0].value.key};
|
|
|
|
ypath_destroy(pPath, lenP);
|
|
|
|
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();
|
|
}
|
|
}
|
|
break;
|
|
case Y_ARRAY:
|
|
{
|
|
YArrayEvent const*const pEvent{&events[i].content.array};
|
|
Branch const*const pArray{yarray_event_target(pEvent)};
|
|
(void)pArray;
|
|
uint32_t lenP{0};
|
|
YPathSegment *const pPath{yarray_event_path(pEvent, &lenP)};
|
|
yvalidate(lenP == 2);
|
|
yvalidate(pPath[0].tag == Y_EVENT_PATH_KEY);
|
|
yvalidate(pPath[1].tag == Y_EVENT_PATH_INDEX);
|
|
yvalidate(pPath[1].value.index == 0); // this means position update
|
|
// check that pArray is really that comment's position array
|
|
assert(pArray == yarray_get(
|
|
ymap_get(rState.rYrsSupplier.GetCommentMap(), pTxn, pPath[0].value.key)->value.y_type,
|
|
pTxn, pPath[1].value.index)->value.y_type);
|
|
OString const commentId{pPath[0].value.key};
|
|
uint32_t lenC{0};
|
|
YEventChange *const pChange{yarray_event_delta(pEvent, &lenC)};
|
|
// position update can be for end, start, or both
|
|
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);
|
|
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);
|
|
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);
|
|
break;
|
|
default:
|
|
yvalidate(false);
|
|
}
|
|
yevent_delta_destroy(pChange, lenC);
|
|
ypath_destroy(pPath, lenP);
|
|
}
|
|
break;
|
|
case Y_MAP:
|
|
{
|
|
// new comment: lenP = 0, lenK = 1, Y_EVENT_KEY_CHANGE_ADD, Y_ARRAY
|
|
YMapEvent const*const pEvent{&events[i].content.map};
|
|
Branch const*const pMap{ymap_event_target(pEvent)};
|
|
uint32_t lenP{0};
|
|
YPathSegment *const pPath{ymap_event_path(pEvent, &lenP)};
|
|
uint32_t lenK{0};
|
|
YEventKeyChange *const pChange{ymap_event_keys(pEvent, &lenK)};
|
|
for (decltype(lenK) j = 0; j < lenK; ++j)
|
|
{
|
|
switch (pChange[j].tag)
|
|
{
|
|
case Y_EVENT_KEY_CHANGE_ADD:
|
|
#if 1
|
|
switch (pChange[j].new_value->tag)
|
|
{
|
|
case Y_JSON_INT:
|
|
{
|
|
int64_t const number{pChange[j].new_value->value.integer};
|
|
(void)number;
|
|
assert(false);
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
break;
|
|
case Y_MAP:
|
|
{
|
|
Branch const*const pMap2{pChange[j].new_value->value.y_type};
|
|
(void)pMap2;
|
|
assert(false);
|
|
}
|
|
break;
|
|
case Y_TEXT:
|
|
{
|
|
Branch const*const pText{pChange[j].new_value->value.y_type};
|
|
(void)pText;
|
|
assert(false);
|
|
}
|
|
break;
|
|
default:
|
|
assert(false);
|
|
}
|
|
#endif
|
|
break;
|
|
case Y_EVENT_KEY_CHANGE_DELETE:
|
|
switch (pChange[j].old_value->tag)
|
|
{
|
|
case Y_ARRAY:
|
|
{
|
|
// delete comment
|
|
yvalidate(pMap == rState.rYrsSupplier.GetCommentMap());
|
|
yvalidate(lenP == 0);
|
|
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);
|
|
break;
|
|
}
|
|
default:
|
|
assert(false);
|
|
}
|
|
break;
|
|
case Y_EVENT_KEY_CHANGE_UPDATE:
|
|
{
|
|
OString const prop{pChange[j].key};
|
|
switch (pChange[j].new_value->tag)
|
|
{
|
|
case Y_JSON_BOOL:
|
|
{
|
|
yvalidate(pMap != rState.rYrsSupplier.GetCommentMap());
|
|
yvalidate(lenP == 2);
|
|
yvalidate(pPath[0].tag == Y_EVENT_PATH_KEY);
|
|
OString const commentId{pPath[0].value.key};
|
|
// in props map...
|
|
yvalidate(pPath[1].tag == Y_EVENT_PATH_INDEX);
|
|
yvalidate(pPath[1].value.index == 1);
|
|
yvalidate(prop == "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;
|
|
}
|
|
default:
|
|
assert(false);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
assert(false);
|
|
}
|
|
}
|
|
yevent_keys_destroy(pChange, lenK);
|
|
ypath_destroy(pPath, lenP);
|
|
}
|
|
break;
|
|
default:
|
|
assert(false);
|
|
}
|
|
}
|
|
|
|
// insert into each node in order of content index! else pos is wrong
|
|
for (auto const& it : newComments)
|
|
{
|
|
Branch const*const pArray{it.second.second};
|
|
// new comment
|
|
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 pProps{yarray_get(pArray, pTxn, 1)};
|
|
yvalidate(pProps->tag == Y_MAP);
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pText{yarray_get(pArray, pTxn, 2)};
|
|
yvalidate(pText->tag == Y_TEXT);
|
|
// pPos->value.y_type index 0/1 are node/content (key of newComments)
|
|
::std::optional<SwPosition> oStartPos;
|
|
if (yarray_len(pPos->value.y_type) == 4)
|
|
{
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pStartNode{
|
|
yarray_get(pPos->value.y_type, pTxn, 2)};
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pStartContent{
|
|
yarray_get(pPos->value.y_type, pTxn, 3)};
|
|
auto const node{pStartNode->value.integer};
|
|
yvalidate(SwNodeOffset{node} < rState.rDoc.GetNodes().Count());
|
|
SwNode & rStartNode{*rState.rDoc.GetNodes()[SwNodeOffset{node}]};
|
|
yvalidate(rStartNode.IsTextNode());
|
|
auto const content{pStartContent->value.integer};
|
|
yvalidate(content <= rStartNode.GetTextNode()->Len());
|
|
oStartPos.emplace(*rStartNode.GetTextNode(), static_cast<sal_Int32>(content));
|
|
}
|
|
|
|
OUString author;
|
|
{
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pAuthor{ymap_get(pProps->value.y_type, pTxn, "author")};
|
|
yvalidate(!pAuthor || pAuthor->tag == Y_JSON_STR);
|
|
if (pAuthor && pAuthor->tag == Y_JSON_STR)
|
|
{
|
|
author = OUString(pAuthor->value.str, pAuthor->len, RTL_TEXTENCODING_UTF8);
|
|
}
|
|
}
|
|
OUString initials;
|
|
{
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pInitials{ymap_get(pProps->value.y_type, pTxn, "initials")};
|
|
yvalidate(!pInitials || pInitials->tag == Y_JSON_STR);
|
|
if (pInitials && pInitials->tag == Y_JSON_STR)
|
|
{
|
|
initials = OUString(pInitials->value.str, pInitials->len, RTL_TEXTENCODING_UTF8);
|
|
}
|
|
}
|
|
DateTime date{DateTime::SYSTEM};
|
|
{
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pDate{ymap_get(pProps->value.y_type, pTxn, "date")};
|
|
yvalidate(!pDate || pDate->tag == Y_JSON_STR);
|
|
if (pDate && pDate->tag == Y_JSON_STR)
|
|
{
|
|
util::DateTime unoDate;
|
|
if (::sax::Converter::parseDateTime(unoDate, pDate->value.str))
|
|
{
|
|
date = unoDate;
|
|
}
|
|
}
|
|
}
|
|
bool isResolved{false};
|
|
{
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pResolved{ymap_get(pProps->value.y_type, pTxn, "resolved")};
|
|
yvalidate(!pResolved || pResolved->tag == Y_JSON_BOOL);
|
|
if (pResolved && pResolved->tag == Y_JSON_BOOL)
|
|
{
|
|
isResolved = pResolved->value.flag == Y_TRUE;
|
|
}
|
|
}
|
|
::std::optional<decltype(::std::declval<SwPostItField>().GetPostItId())> oParentId;
|
|
{
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pParent{ymap_get(pProps->value.y_type, pTxn, "parent")};
|
|
yvalidate(!pParent || pParent->tag == Y_JSON_STR);
|
|
if (pParent && pParent->tag == Y_JSON_STR)
|
|
{
|
|
OString const parentId{OString(pParent->value.str, pParent->len)};
|
|
auto const itP{rState.rYrsSupplier.GetComments().find(parentId)};
|
|
// note: newComments is sorted by position, and reply
|
|
// comments are always inserted *after* their parent
|
|
// comment, so it should never happen that the parent
|
|
// doesn't exist here
|
|
yvalidate(itP != rState.rYrsSupplier.GetComments().end());
|
|
oParentId.emplace(itP->second.front()->mpPostIt->GetPostItField()->GetPostItId());
|
|
}
|
|
}
|
|
yvalidate(SwNodeOffset{it.first.first} < rState.rDoc.GetNodes().Count());
|
|
SwNode & rNode{*rState.rDoc.GetNodes()[SwNodeOffset{it.first.first}]};
|
|
yvalidate(rNode.IsTextNode());
|
|
yvalidate(it.first.second <= o3tl::make_unsigned(rNode.GetTextNode()->Len()));
|
|
SwPosition anchorPos{*rNode.GetTextNode(), static_cast<sal_Int32>(it.first.second)};
|
|
SAL_DEBUG("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{
|
|
pType,
|
|
author,
|
|
"", // content
|
|
initials,
|
|
SwMarkName{}, // name
|
|
date}};
|
|
pField->SetResolved(isResolved);
|
|
if (oParentId)
|
|
{
|
|
pField->SetParentPostItId(*oParentId);
|
|
}
|
|
|
|
pShell->Push();
|
|
*pShell->GetCursor()->GetPoint() = anchorPos;
|
|
if (oStartPos)
|
|
{
|
|
pShell->SetMark();
|
|
*pShell->GetCursor()->GetMark() = *oStartPos;
|
|
}
|
|
else
|
|
{
|
|
pShell->ClearMark();
|
|
}
|
|
bool const b = pShell->InsertField2Impl(*pField, nullptr, nullptr);
|
|
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);
|
|
}
|
|
|
|
// comments inserted, now check position updates for consistency
|
|
for (auto & rUpdate : posUpdates)
|
|
{
|
|
auto const it{rState.rYrsSupplier.GetComments().find(::std::get<0>(rUpdate))};
|
|
yvalidate(it != rState.rYrsSupplier.GetComments().end());
|
|
SwPosition const pos{it->second.front()->GetAnchorPosition()};
|
|
yvalidate(o3tl::make_unsigned(pos.GetNodeIndex().get()) == ::std::get<1>(rUpdate));
|
|
yvalidate(o3tl::make_unsigned(pos.GetContentIndex()) == ::std::get<2>(rUpdate));
|
|
}
|
|
for (auto & rUpdate : startUpdates)
|
|
{
|
|
auto const it{rState.rYrsSupplier.GetComments().find(::std::get<0>(rUpdate))};
|
|
yvalidate(it != rState.rYrsSupplier.GetComments().end());
|
|
|
|
SwTextAnnotationField const& rHint{*static_cast<SwTextAnnotationField const*>(
|
|
it->second.front()->GetFormatField().GetTextField())};
|
|
SwPosition const pos{rHint.GetAnnotationMark()->GetMarkStart()};
|
|
yvalidate(o3tl::make_unsigned(pos.GetNodeIndex().get()) == ::std::get<1>(rUpdate));
|
|
yvalidate(o3tl::make_unsigned(pos.GetContentIndex()) == ::std::get<2>(rUpdate));
|
|
}
|
|
}
|
|
|
|
extern "C" void observe_cursors(void *const pState, uint32_t count, YEvent const*const events)
|
|
{
|
|
#if 1
|
|
(void) pState;
|
|
(void) count;
|
|
(void) events;
|
|
#endif
|
|
}
|
|
|
|
void writeLength(sal_Int8 *& rpBuf, sal_Int32 const len)
|
|
{
|
|
*rpBuf = (len >> 0) & 0xFF;
|
|
++rpBuf;
|
|
*rpBuf = (len >> 8) & 0xFF;
|
|
++rpBuf;
|
|
*rpBuf = (len >> 16) & 0xFF;
|
|
++rpBuf;
|
|
*rpBuf = (len >> 24) & 0xFF;
|
|
++rpBuf;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
IMPL_LINK(YrsThread, HandleMessage, void*, pVoid, void)
|
|
{
|
|
SAL_DEBUG("YRS HandleMessage");
|
|
DBG_TESTSOLARMUTEX();
|
|
if (!m_pDSM)
|
|
{
|
|
SAL_DEBUG("m_pDSM died");
|
|
return;
|
|
}
|
|
// wrap this, not strictly needed but can't hurt?
|
|
m_pDSM->m_rDoc.getIDocumentLayoutAccess().GetCurrentViewShell()->StartAllAction();
|
|
::std::unique_ptr<uno::Sequence<sal_Int8>> const pBuf{static_cast<uno::Sequence<sal_Int8>*>(pVoid)};
|
|
uint32_t const length{pBuf->size()};
|
|
assert(length != 0);
|
|
switch ((*pBuf)[0])
|
|
{
|
|
case ::std::underlying_type_t<Message>(Message::RequestStateVector):
|
|
{
|
|
SAL_DEBUG("sending state vector");
|
|
YTransaction *const pTxn{m_pDSM->m_pYrsSupplier->GetWriteTransaction()};
|
|
uint32_t len{0};
|
|
char * pSV = ytransaction_state_vector_v1(pTxn, &len);
|
|
uno::Sequence<sal_Int8> buf(5 + len);
|
|
sal_Int8 * it{buf.getArray()};
|
|
writeLength(it, len+1);
|
|
*it = ::std::underlying_type_t<Message>(Message::SendStateVector);
|
|
++it;
|
|
::std::copy(pSV, pSV+len, it);
|
|
try
|
|
{
|
|
m_xConnection->write(buf);
|
|
}
|
|
catch (io::IOException const&)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("sw", "YRS HandleMessage");
|
|
}
|
|
m_pDSM->m_pYrsSupplier->CommitTransaction();
|
|
ybinary_destroy(pSV, len);
|
|
break;
|
|
}
|
|
case ::std::underlying_type_t<Message>(Message::SendStateVector):
|
|
{
|
|
SAL_DEBUG("received state vector");
|
|
YTransaction *const pTxn{m_pDSM->m_pYrsSupplier->GetWriteTransaction()};
|
|
uint32_t len{0};
|
|
char * pUpdate = ytransaction_state_diff_v1(pTxn,
|
|
reinterpret_cast<char const*>(pBuf->begin()) + 1, length - 1, &len);
|
|
uno::Sequence<sal_Int8> buf(5 + len);
|
|
sal_Int8 * it{buf.getArray()};
|
|
writeLength(it, len+1);
|
|
*it = ::std::underlying_type_t<Message>(Message::SendStateDiff);
|
|
++it;
|
|
::std::copy(pUpdate, pUpdate+len, it);
|
|
try
|
|
{
|
|
m_xConnection->write(buf);
|
|
}
|
|
catch (io::IOException const&)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("sw", "YRS HandleMessage");
|
|
}
|
|
m_pDSM->m_pYrsSupplier->CommitTransaction();
|
|
ybinary_destroy(pUpdate, len);
|
|
break;
|
|
}
|
|
case ::std::underlying_type_t<Message>(Message::SendStateDiff):
|
|
{
|
|
SAL_DEBUG("apply update: " << yupdate_debug_v1(reinterpret_cast<char const*>(pBuf->begin()) + 1, length - 1));
|
|
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);
|
|
if (err != 0)
|
|
{
|
|
SAL_DEBUG("ytransaction_apply error " << err);
|
|
abort();
|
|
}
|
|
// 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};
|
|
YSubscription *const pSubComments = yobserve_deep(m_pDSM->m_pYrsSupplier->GetCommentMap(), &state, observe_comments);
|
|
// not sure if yweak_observe would work for (weakref) cursors
|
|
YSubscription *const pSubCursors = yobserve_deep(m_pDSM->m_pYrsSupplier->GetCursorMap(), &state, observe_cursors);
|
|
m_pDSM->m_pYrsSupplier->CommitTransaction(true);
|
|
m_pDSM->m_pYrsSupplier->SetMode(IYrsTransactionSupplier::Mode::Edit);
|
|
yunobserve(pSubComments);
|
|
yunobserve(pSubCursors);
|
|
break;
|
|
}
|
|
}
|
|
m_pDSM->m_rDoc.getIDocumentLayoutAccess().GetCurrentViewShell()->EndAllAction();
|
|
}
|
|
|
|
IYrsTransactionSupplier::Mode DocumentStateManager::SetYrsMode(IYrsTransactionSupplier::Mode const mode)
|
|
{
|
|
return m_pYrsSupplier->SetMode(mode);
|
|
}
|
|
|
|
void DocumentStateManager::YrsNotifySetResolved(OString const& rCommentId, SwPostItField const& rField)
|
|
{
|
|
YTransaction *const pTxn{m_pYrsSupplier->GetWriteTransaction()};
|
|
if (!pTxn)
|
|
{
|
|
return;
|
|
}
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pComment{ymap_get(m_pYrsSupplier->m_pComments, pTxn, rCommentId.getStr())};
|
|
assert(pComment);
|
|
::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();
|
|
}
|
|
|
|
void DocumentStateManager::YrsAddCommentImpl(SwPosition const& rAnchorPos, OString const& commentId)
|
|
{
|
|
SAL_DEBUG("YRS AddCommentImpl");
|
|
::std::vector<SwAnnotationItem *> items;
|
|
// ??? TODO how should this work for multiple viewshells? every shell has its own EditEngine? unclear.
|
|
for (SwViewShell & rShell : m_rDoc.getIDocumentLayoutAccess().GetCurrentViewShell()->GetRingContainer())
|
|
{
|
|
for (::std::unique_ptr<SwAnnotationItem> const& it : *rShell.GetPostItMgr())
|
|
{
|
|
if (it->GetAnchorPosition() == rAnchorPos)
|
|
{
|
|
items.emplace_back(it.get());
|
|
// for a loaded document, GetOrCreateAnnotationWindowForLatestPostItField() cannot be used
|
|
bool isNew{false};
|
|
SwAnnotationWin *const pWin{
|
|
rShell.GetPostItMgr()->GetOrCreateAnnotationWindow(*it, isNew)};
|
|
assert(pWin);
|
|
pWin->GetOutlinerView()->GetEditView().SetYrsCommentId(m_pYrsSupplier.get(), commentId);
|
|
}
|
|
}
|
|
}
|
|
m_pYrsSupplier->m_Comments.emplace(commentId, items);
|
|
}
|
|
|
|
void DocumentStateManager::YrsAddComment(SwPosition const& rPos,
|
|
::std::optional<SwPosition> const oAnchorStart, SwPostItField const& rField,
|
|
bool const isInsert)
|
|
{
|
|
SAL_DEBUG("YRS AddComment " << rPos);
|
|
OString const commentId{m_pYrsSupplier->GenNewCommentId()};
|
|
// this calls EditViewInvalidate so prevent destroying pTxn
|
|
YrsAddCommentImpl(rPos, commentId);
|
|
YTransaction *const pTxn{m_pYrsSupplier->GetWriteTransaction()};
|
|
// first, adjust position of all other comments in the paragraph
|
|
if (isInsert)
|
|
{
|
|
// it could be faster to get the SwPostItField from the node, but then can't get the commentId
|
|
for (auto const& it : m_pYrsSupplier->m_Comments)
|
|
{
|
|
SwAnnotationItem const*const pItem{it.second.front()};
|
|
SwPosition const& rItPos{pItem->GetAnchorPosition()};
|
|
if (rPos.nNode == rItPos.nNode && rPos.nContent <= rItPos.nContent
|
|
&& it.first != commentId)
|
|
{
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pComment{
|
|
ymap_get(m_pYrsSupplier->m_pComments, pTxn, it.first.getStr())};
|
|
assert(pComment);
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPos{yarray_get(pComment->value.y_type, pTxn, 0)};
|
|
assert(pPos);
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPosN{yarray_get(pPos->value.y_type, pTxn, 0)};
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPosC{yarray_get(pPos->value.y_type, pTxn, 1)};
|
|
// SwTextNode::Update already moved rItPos
|
|
assert(pPosN->value.integer == rItPos.GetNodeIndex().get());
|
|
assert(pPosC->value.integer + 1 == rItPos.GetContentIndex());
|
|
yarray_remove_range(pPos->value.y_type, pTxn, 0, 2);
|
|
YInput const anchorNode{yinput_long(rItPos.GetNodeIndex().get())};
|
|
YInput const anchorContent{yinput_long(rItPos.GetContentIndex())};
|
|
YInput posArray[]{anchorNode, anchorContent};
|
|
yarray_insert_range(pPos->value.y_type, pTxn, 0, posArray, 2);
|
|
}
|
|
// anchor start can be in a different node than the field!
|
|
SwTextAnnotationField const& rHint{*static_cast<SwTextAnnotationField const*>(
|
|
pItem->GetFormatField().GetTextField())};
|
|
::sw::mark::AnnotationMark const*const pMark{rHint.GetAnnotationMark()};
|
|
if (pMark != nullptr
|
|
&& rPos.nNode == pMark->GetMarkStart().nNode
|
|
&& rPos.nContent <= pMark->GetMarkStart().nContent)
|
|
{
|
|
assert(it.first != commentId); // start always before inserted char
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pComment{
|
|
ymap_get(m_pYrsSupplier->m_pComments, pTxn, it.first.getStr())};
|
|
assert(pComment);
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPos{yarray_get(pComment->value.y_type, pTxn, 0)};
|
|
assert(pPos);
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPosN{yarray_get(pPos->value.y_type, pTxn, 2)};
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPosC{yarray_get(pPos->value.y_type, pTxn, 3)};
|
|
// SwTextNode::Update already moved pMark
|
|
assert(pPosN->value.integer == pMark->GetMarkStart().GetNodeIndex().get());
|
|
assert(pPosC->value.integer + 1 == pMark->GetMarkStart().GetContentIndex());
|
|
yarray_remove_range(pPos->value.y_type, pTxn, 2, 2);
|
|
YInput const anchorStartNode{yinput_long(pMark->GetMarkStart().GetNodeIndex().get())};
|
|
YInput const anchorStartContent{yinput_long(pMark->GetMarkStart().GetContentIndex())};
|
|
YInput posArray[]{anchorStartNode, anchorStartContent};
|
|
yarray_insert_range(pPos->value.y_type, pTxn, 2, posArray, 2);
|
|
}
|
|
}
|
|
}
|
|
YInput const anchorNode{yinput_long(rPos.GetNodeIndex().get())};
|
|
YInput const anchorContent{yinput_long(rPos.GetContentIndex())};
|
|
YInput const anchorStartNode{oAnchorStart ? yinput_long(oAnchorStart->GetNodeIndex().get()) : yinput_undefined()};
|
|
YInput const anchorStartContent{oAnchorStart ? yinput_long(oAnchorStart->GetContentIndex()) : yinput_undefined()};
|
|
YInput posArray[]{anchorNode, anchorContent, anchorStartNode, anchorStartContent};
|
|
YInput const anchor{yinput_yarray(posArray, oAnchorStart ? 4 : 2)};
|
|
OString const authorString{OUStringToOString(rField.GetPar1(), RTL_TEXTENCODING_UTF8)};
|
|
OString const initialsString{OUStringToOString(rField.GetInitials(), RTL_TEXTENCODING_UTF8)};
|
|
OUStringBuffer dateBuf;
|
|
::sax::Converter::convertDateTime(dateBuf, rField.GetDateTime().GetUNODateTime(), nullptr, true);
|
|
OString const dateString{OUStringToOString(dateBuf.makeStringAndClear(), RTL_TEXTENCODING_UTF8)};
|
|
char const*const propsNames[]{ "author", "initials", "date", "resolved", "parent" };
|
|
YInput const author{yinput_string(authorString.getStr())};
|
|
YInput const initials{yinput_string(initialsString.getStr())};
|
|
YInput const date{yinput_string(dateString.getStr())};
|
|
OString parentId;
|
|
if (rField.GetParentPostItId() != 0)
|
|
{
|
|
sw::annotation::SwAnnotationWin const*const pWin{
|
|
m_rDoc.getIDocumentLayoutAccess().GetCurrentViewShell()->GetPostItMgr()->GetAnnotationWin(rField.GetParentPostItId())};
|
|
assert(pWin);
|
|
parentId = pWin->GetOutlinerView()->GetEditView().GetYrsCommentId();
|
|
}
|
|
YInput const parent{yinput_string(parentId.getStr())};
|
|
YInput const resolved{yinput_bool(rField.GetResolved() ? Y_TRUE : Y_FALSE)};
|
|
YInput propsArray[]{author, initials, date, resolved, parent};
|
|
YInput const properties{yinput_ymap(const_cast<char**>(propsNames), propsArray, parentId.getLength() ? 5 : 4)};
|
|
YInput const text{yinput_ytext(const_cast<char*>(""))};
|
|
YInput commentArray[]{anchor, properties, text};
|
|
YInput const comment{yinput_yarray(commentArray, 3)};
|
|
ymap_insert(m_pYrsSupplier->m_pComments, pTxn, commentId.getStr(), &comment);
|
|
// just use first one?
|
|
// or check which one is active = (GetStyle() & WB_DIALOGCONTROL)
|
|
m_pYrsSupplier->m_Comments.find(commentId)->second.front()->mpPostIt->GetOutlinerView()->GetEditView().YrsWriteEEState();
|
|
|
|
// 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
|
|
}
|
|
|
|
void DocumentStateManager::YrsRemoveCommentImpl(OString const& rCommentId)
|
|
{
|
|
assert(!rCommentId.isEmpty());
|
|
m_pYrsSupplier->m_Comments.erase(rCommentId);
|
|
}
|
|
|
|
void DocumentStateManager::YrsRemoveComment(SwPosition const& rPos, OString const& rCommentId)
|
|
{
|
|
SAL_DEBUG("YRS RemoveComment");
|
|
YrsRemoveCommentImpl(rCommentId);
|
|
YTransaction *const pTxn{m_pYrsSupplier->GetWriteTransaction()};
|
|
if (!pTxn)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ymap_remove(m_pYrsSupplier->m_pComments, pTxn, rCommentId.getStr());
|
|
|
|
// first, adjust position of all other comments in the paragraph
|
|
for (auto const& it : m_pYrsSupplier->m_Comments)
|
|
{
|
|
SwAnnotationItem const*const pItem{it.second.front()};
|
|
SwPosition const& rItPos{pItem->GetAnchorPosition()};
|
|
if (rPos.nNode == rItPos.nNode && rPos.nContent <= rItPos.nContent)
|
|
{
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pComment{
|
|
ymap_get(m_pYrsSupplier->m_pComments, pTxn, it.first.getStr())};
|
|
assert(pComment);
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPos{yarray_get(pComment->value.y_type, pTxn, 0)};
|
|
assert(pPos);
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPosN{yarray_get(pPos->value.y_type, pTxn, 0)};
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPosC{yarray_get(pPos->value.y_type, pTxn, 1)};
|
|
// SwTextNode::Update will move rItPos soon
|
|
assert(pPosN->value.integer == rItPos.GetNodeIndex().get());
|
|
assert(pPosC->value.integer == rItPos.GetContentIndex());
|
|
yarray_remove_range(pPos->value.y_type, pTxn, 0, 2);
|
|
YInput const anchorNode{yinput_long(rItPos.GetNodeIndex().get())};
|
|
YInput const anchorContent{yinput_long(rItPos.GetContentIndex() - 1)};
|
|
YInput posArray[]{anchorNode, anchorContent};
|
|
yarray_insert_range(pPos->value.y_type, pTxn, 0, posArray, 2);
|
|
}
|
|
// anchor start can be in a different node than the field!
|
|
SwTextAnnotationField const& rHint{*static_cast<SwTextAnnotationField const*>(
|
|
pItem->GetFormatField().GetTextField())};
|
|
::sw::mark::AnnotationMark const*const pMark{rHint.GetAnnotationMark()};
|
|
if (pMark != nullptr
|
|
&& rPos.nNode == pMark->GetMarkStart().nNode
|
|
&& rPos.nContent <= pMark->GetMarkStart().nContent)
|
|
{
|
|
assert(it.first != rCommentId); // start always before inserted char
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pComment{
|
|
ymap_get(m_pYrsSupplier->m_pComments, pTxn, it.first.getStr())};
|
|
assert(pComment);
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPos{yarray_get(pComment->value.y_type, pTxn, 0)};
|
|
assert(pPos);
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPosN{yarray_get(pPos->value.y_type, pTxn, 2)};
|
|
::std::unique_ptr<YOutput, YOutputDeleter> const pPosC{yarray_get(pPos->value.y_type, pTxn, 3)};
|
|
// SwTextNode::Update will move pMark soon
|
|
assert(pPosN->value.integer == pMark->GetMarkStart().GetNodeIndex().get());
|
|
assert(pPosC->value.integer == pMark->GetMarkStart().GetContentIndex());
|
|
YInput const anchorStartNode{yinput_long(pMark->GetMarkStart().GetNodeIndex().get())};
|
|
YInput const anchorStartContent{yinput_long(pMark->GetMarkStart().GetContentIndex() - 1)};
|
|
YInput posArray[]{anchorStartNode, anchorStartContent};
|
|
yarray_insert_range(pPos->value.y_type, pTxn, 2, posArray, 2);
|
|
}
|
|
}
|
|
// either update the cursors here, or wait for round-trip?
|
|
}
|
|
|
|
void DocumentStateManager::YrsInitAcceptor()
|
|
{
|
|
if (!getenv("YRSACCEPT") || m_pYrsReader.is())
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
auto const conn = u"pipe,name=ytest"_ustr;
|
|
auto const xContext{comphelper::getProcessComponentContext()};
|
|
SAL_DEBUG("YRS accept");
|
|
m_xAcceptor = css::connection::Acceptor::create(xContext);
|
|
// TODO move to thread?
|
|
uno::Reference<connection::XConnection> xConnection = m_xAcceptor->accept(conn);
|
|
uno::Sequence<sal_Int8> buf(4);
|
|
uno::Sequence<sal_Int8> data;
|
|
if (SfxMedium const*const pMedium{m_rDoc.GetDocShell()->GetMedium()})
|
|
{
|
|
OUString const url{pMedium->GetOrigURL()};
|
|
if (!url.isEmpty())
|
|
{
|
|
try
|
|
{
|
|
SAL_DEBUG("YRS send file: " << url);
|
|
::ucbhelper::Content temp{url, {}, xContext};
|
|
uno::Reference<io::XInputStream> const xInStream{temp.openStreamNoLock()};
|
|
uno::Reference<io::XSeekable> const xSeekable{xInStream, uno::UNO_QUERY};
|
|
if (xSeekable.is())
|
|
{
|
|
auto const len(xSeekable->getLength() - xSeekable->getPosition());
|
|
if (xInStream->readBytes(data, len) != len)
|
|
{
|
|
throw uno::RuntimeException(u"short readBytes"_ustr);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
::std::vector<uno::Sequence<sal_Int8>> bufs;
|
|
bool isDone{false};
|
|
do
|
|
{
|
|
bufs.emplace_back();
|
|
isDone = xInStream->readSomeBytes(bufs.back(), 65536) == 0;
|
|
} while (!isDone);
|
|
sal_Int32 nSize{0};
|
|
for (auto const& rBuf : bufs)
|
|
{
|
|
if (o3tl::checked_add(nSize, rBuf.getLength(), nSize))
|
|
{
|
|
throw std::bad_alloc(); // too large for Sequence
|
|
}
|
|
}
|
|
size_t nCopied{0};
|
|
data.realloc(nSize);
|
|
for (auto const& rBuf : bufs)
|
|
{
|
|
::std::memcpy(data.getArray() + nCopied, rBuf.getConstArray(), rBuf.getLength());
|
|
nCopied += rBuf.getLength(); // can't overflow
|
|
}
|
|
}
|
|
sal_Int8 * it{buf.getArray()};
|
|
writeLength(it, data.getLength());
|
|
}
|
|
catch (...)
|
|
{
|
|
abort();
|
|
}
|
|
}
|
|
}
|
|
xConnection->write(buf);
|
|
xConnection->write(data);
|
|
m_pYrsReader = new YrsThread(xConnection, *this);
|
|
//m_xAcceptor->stopAccepting();
|
|
m_pYrsReader->launch();
|
|
SAL_DEBUG("YRS started");
|
|
// inserting comments needs sidebar wins so needs a view shell first
|
|
SwFieldType & rType{*m_rDoc.getIDocumentFieldsAccess().GetFieldType(SwFieldIds::Postit, OUString(), false)};
|
|
std::vector<SwFormatField*> fields;
|
|
rType.GatherFields(fields);
|
|
for (SwFormatField const*const pField : fields)
|
|
{
|
|
SwPostItField const& rField{*dynamic_cast<SwPostItField const*>(pField->GetField())};
|
|
SwTextAnnotationField const& rHint{*static_cast<SwTextAnnotationField const*>(pField->GetTextField())};
|
|
SwPosition const pos{rHint.GetTextNode(), rHint.GetStart()};
|
|
::std::optional<SwPosition> oAnchorStart;
|
|
if (::sw::mark::AnnotationMark const*const pMark{rHint.GetAnnotationMark()})
|
|
{
|
|
oAnchorStart.emplace(pMark->GetMarkStart());
|
|
}
|
|
YrsAddComment(pos, oAnchorStart, rField, false);
|
|
}
|
|
// initiate sync of comments to other side
|
|
//AddComment would have done this m_pYrsSupplier->GetWriteTransaction();
|
|
YrsCommitModified();
|
|
}
|
|
catch (uno::Exception const&) // exception here will cause UAF from SwView later
|
|
{
|
|
DBG_UNHANDLED_EXCEPTION("sw");
|
|
abort();
|
|
}
|
|
}
|
|
|
|
void DocumentStateManager::YrsInitConnector(uno::Any const& raConnection)
|
|
{
|
|
assert(!m_pYrsReader);
|
|
uno::Reference<connection::XConnection> xConnection;
|
|
raConnection >>= xConnection;
|
|
assert(xConnection.is());
|
|
|
|
// delete all fields that were loaded from a document - they must be
|
|
// inserted via yrs
|
|
// cannot call SwPostItMgr::Delete() because no view shell yet
|
|
SwFieldType & rType{*m_rDoc.getIDocumentFieldsAccess().GetFieldType(SwFieldIds::Postit, OUString(), false)};
|
|
std::vector<SwFormatField*> fields;
|
|
rType.GatherFields(fields);
|
|
for (SwFormatField *const pField : fields)
|
|
{
|
|
SwTextField const*const pHint{pField->GetTextField()};
|
|
if (pHint)
|
|
{
|
|
SwTextField::DeleteTextField(*pHint);
|
|
}
|
|
}
|
|
// don't request comments to be sent here - let the other side just commit, that will send them
|
|
// TODO ... but we get an undo action per comment, which is unfortunate
|
|
|
|
m_pYrsReader = new YrsThread(xConnection, *this);
|
|
m_pYrsReader->launch();
|
|
SAL_DEBUG("YRS started (InitConnector)");
|
|
}
|
|
|
|
#endif
|
|
|
|
DocumentStateManager::DocumentStateManager( SwDoc& i_rSwdoc ) :
|
|
m_rDoc( i_rSwdoc ),
|
|
mbEnableSetModified(true),
|
|
mbModified(false),
|
|
mbUpdateExpField(false),
|
|
mbNewDoc(false),
|
|
mbInCallModified(false)
|
|
{
|
|
#if defined(YRS)
|
|
m_pYrsSupplier.reset(new YrsTransactionSupplier);
|
|
#endif
|
|
}
|
|
|
|
DocumentStateManager::~DocumentStateManager()
|
|
{
|
|
#if defined(YRS)
|
|
if (m_pYrsReader) // not in e.g. AutoText docs
|
|
{
|
|
m_pYrsReader->m_xConnection->close();
|
|
m_pYrsReader->m_pDSM = nullptr;
|
|
m_pYrsReader->join();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void DocumentStateManager::SetModified()
|
|
{
|
|
if (!IsEnableSetModified())
|
|
return;
|
|
|
|
m_rDoc.GetDocumentLayoutManager().ClearSwLayouterEntries();
|
|
mbModified = true;
|
|
m_rDoc.GetDocumentStatisticsManager().SetDocStatModified( true );
|
|
if( m_rDoc.GetOle2Link().IsSet() )
|
|
{
|
|
mbInCallModified = true;
|
|
m_rDoc.GetOle2Link().Call( true );
|
|
mbInCallModified = false;
|
|
}
|
|
|
|
if( m_rDoc.GetAutoCorrExceptWord() && !m_rDoc.GetAutoCorrExceptWord()->IsDeleted() )
|
|
m_rDoc.DeleteAutoCorrExceptWord();
|
|
|
|
#if defined(YRS)
|
|
SAL_DEBUG("YRS SetModified");
|
|
// FIXME: this is called only on LoseFocus! not while editing comment
|
|
YrsCommitModified();
|
|
#endif
|
|
}
|
|
|
|
#if defined(YRS)
|
|
void DocumentStateManager::YrsCommitModified()
|
|
{
|
|
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);
|
|
}
|
|
catch (io::IOException const&)
|
|
{
|
|
TOOLS_WARN_EXCEPTION("sw", "YRS CommitTransaction");
|
|
m_pYrsReader->m_xConnection->close();
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
void DocumentStateManager::ResetModified()
|
|
{
|
|
// give the old and new modified state to the link
|
|
// Bit 0: -> old state
|
|
// Bit 1: -> new state
|
|
bool bOldModified = mbModified;
|
|
mbModified = false;
|
|
m_rDoc.GetDocumentStatisticsManager().SetDocStatModified( false );
|
|
m_rDoc.GetIDocumentUndoRedo().SetUndoNoModifiedPosition();
|
|
if( bOldModified && m_rDoc.GetOle2Link().IsSet() )
|
|
{
|
|
mbInCallModified = true;
|
|
m_rDoc.GetOle2Link().Call( false );
|
|
mbInCallModified = false;
|
|
}
|
|
}
|
|
|
|
bool DocumentStateManager::IsModified() const
|
|
{
|
|
return mbModified;
|
|
}
|
|
|
|
bool DocumentStateManager::IsEnableSetModified() const
|
|
{
|
|
return mbEnableSetModified;
|
|
}
|
|
|
|
void DocumentStateManager::SetEnableSetModified(bool bEnableSetModified)
|
|
{
|
|
mbEnableSetModified = bEnableSetModified;
|
|
}
|
|
|
|
bool DocumentStateManager::IsInCallModified() const
|
|
{
|
|
return mbInCallModified;
|
|
}
|
|
|
|
bool DocumentStateManager::IsUpdateExpField() const
|
|
{
|
|
return mbUpdateExpField;
|
|
}
|
|
|
|
bool DocumentStateManager::IsNewDoc() const
|
|
{
|
|
return mbNewDoc;
|
|
}
|
|
|
|
void DocumentStateManager::SetNewDoc(bool b)
|
|
{
|
|
mbNewDoc = b;
|
|
}
|
|
|
|
void DocumentStateManager::SetUpdateExpFieldStat(bool b)
|
|
{
|
|
mbUpdateExpField = b;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|