diff --git a/README.yrs b/README.yrs new file mode 100644 index 000000000000..55a99e2a3de0 --- /dev/null +++ b/README.yrs @@ -0,0 +1,46 @@ + +## Experimental Writer comments editing collaboration with yrs + +### How to build + +First, build yrs C FFI bindings: + +``` + git clone https://github.com/y-crdt/y-crdt.git + cd y-crdt + git checkout v0.23.1 + cargo build -p yffi +``` + +Then, put the yrs build directory in autogen.input: + +`--with-yrs=/path/to/y-crdt` + +### How to run + +To prevent crashes at runtime, set the environment variable +EDIT_COMMENT_IN_READONLY_MODE=1 and open documents in read-only mode: only +inserting/deleting comments, and editing inside comments will be enabled. + +Currently, communication happens over a hard-coded pipe: + +* start an soffice with YRSACCEPT=1 load a Writer document and it will listen + and block until connect + (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 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. + +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. + diff --git a/RepositoryExternal.mk b/RepositoryExternal.mk index f16521c50d7d..9cacf77039d5 100644 --- a/RepositoryExternal.mk +++ b/RepositoryExternal.mk @@ -4449,4 +4449,21 @@ $(call gb_LinkTarget_set_include,$(1),\ endef endif +ifneq ($(WITH_YRS),) + +define gb_LinkTarget__use_yrs +$(call gb_LinkTarget_set_include,$(1),\ + $$(INCLUDE) \ + -I$(WITH_YRS)/tests-ffi/include \ +) +$(call gb_LinkTarget_add_defs,$(1),-DYRS) +$(call gb_LinkTarget_add_libs,$(1),$(WITH_YRS)/target/debug/libyrs.a) +endef + +else + +gb_LinkTarget__use_yrs := + +endif + # vim: set noet sw=4 ts=4: diff --git a/config_host.mk.in b/config_host.mk.in index 0a4d61f62875..70775a16782e 100644 --- a/config_host.mk.in +++ b/config_host.mk.in @@ -784,6 +784,7 @@ export WITH_LOCALES=@WITH_LOCALES@ export WITH_MYSPELL_DICTS=@WITH_MYSPELL_DICTS@ export WITH_THEMES=@WITH_THEMES@ export WITH_WEBDAV=@WITH_WEBDAV@ +WITH_YRS=@WITH_YRS@ export WORKDIR=@WORKDIR@ export WORKDIR_FOR_BUILD=@WORKDIR_FOR_BUILD@ export WPD_CFLAGS=$(gb_SPACE)@WPD_CFLAGS@ diff --git a/configure.ac b/configure.ac index 7819f4d4ec97..e7059bb611d8 100644 --- a/configure.ac +++ b/configure.ac @@ -2830,6 +2830,12 @@ AC_ARG_WITH(hamcrest, --without-junit disables those tests. Not relevant in the --without-java case.]), ,with_hamcrest=yes) +AC_ARG_WITH(yrs, + AS_HELP_STRING([--with-yrs=], + [Specifies the built yrs git repo for very experimental experiments.]), + WITH_YRS=$withval) +AC_SUBST(WITH_YRS) + AC_ARG_WITH(perl-home, AS_HELP_STRING([--with-perl-home=], [If you have installed Perl 5 Distribution, on your system, please diff --git a/editeng/CppunitTest_editeng_core.mk b/editeng/CppunitTest_editeng_core.mk index 46a232d97f38..64f17ae3ba4a 100644 --- a/editeng/CppunitTest_editeng_core.mk +++ b/editeng/CppunitTest_editeng_core.mk @@ -53,6 +53,7 @@ $(eval $(call gb_CppunitTest_use_externals,editeng_core,\ boost_headers \ icuuc \ libxml2 \ + yrs \ )) $(eval $(call gb_CppunitTest_set_include,editeng_core,\ diff --git a/editeng/CppunitTest_editeng_editeng.mk b/editeng/CppunitTest_editeng_editeng.mk index 38cbc9543db6..6c2d20917e86 100644 --- a/editeng/CppunitTest_editeng_editeng.mk +++ b/editeng/CppunitTest_editeng_editeng.mk @@ -47,6 +47,7 @@ $(eval $(call gb_CppunitTest_use_externals,editeng_editeng,\ boost_headers \ icuuc \ libxml2 \ + yrs \ )) $(eval $(call gb_CppunitTest_set_include,editeng_editeng,\ diff --git a/editeng/Library_editeng.mk b/editeng/Library_editeng.mk index 36893816b569..cfd4f357270c 100644 --- a/editeng/Library_editeng.mk +++ b/editeng/Library_editeng.mk @@ -166,6 +166,7 @@ $(eval $(call gb_Library_use_externals,editeng,\ icuuc \ icu_headers \ libxml2 \ + yrs \ )) # vim: set noet sw=4 ts=4: diff --git a/editeng/inc/editdoc.hxx b/editeng/inc/editdoc.hxx index dfb0adec96d2..eb0d79da7e6e 100644 --- a/editeng/inc/editdoc.hxx +++ b/editeng/inc/editdoc.hxx @@ -46,6 +46,13 @@ enum class TextRotation; +#if defined(YRS) +class ImpEditEngine; +class IYrsTransactionSupplier; +typedef struct TransactionInner YTransaction; +typedef struct YTextEvent YTextEvent; +#endif + #define CHARPOSGROW 16 #define DEFTAB 720 @@ -119,6 +126,19 @@ private: bool mbModified:1; bool mbDisableAttributeExpanding:1; +#if defined(YRS) + OString m_CommentId; + IYrsTransactionSupplier * m_pYrsSupplier{nullptr}; +public: + void SetYrsCommentId(IYrsTransactionSupplier *, OString const& rId); + void YrsWriteEEState(); + void YrsReadEEState(YTransaction *, ImpEditEngine & rIEE); + void YrsApplyEEDelta(YTransaction *, YTextEvent const* pEvent, ImpEditEngine & rIEE); + void YrsSetStyle(sal_Int32 nPara, ::std::u16string_view rStyle); + void YrsSetParaAttr(sal_Int32 nPara, SfxPoolItem const& rItem); + OString GetYrsCommentId() const; +#endif + public: EditDoc( SfxItemPool* pItemPool ); ~EditDoc(); @@ -139,21 +159,19 @@ public: void CreateDefFont( bool bUseStyles ); const SvxFont& GetDefFont() const { return maDefFont; } - void SetDefTab(sal_uInt16 nTab) - { - mnDefTab = nTab ? nTab : DEFTAB; - } + void SetDefTab(sal_uInt16 nTab); sal_uInt16 GetDefTab() const { return mnDefTab; } - void SetVertical( bool bVertical ) { mbIsVertical = bVertical; } + void SetVertical(bool bVertical); + bool IsEffectivelyVertical() const; bool IsTopToBottom() const; bool GetVertical() const; - void SetRotation( TextRotation nRotation ) { mnRotation = nRotation; } + void SetRotation(TextRotation nRotation); TextRotation GetRotation() const { return mnRotation; } void SetFixedCellHeight( bool bUseFixedCellHeight ) diff --git a/editeng/source/editeng/editdoc.cxx b/editeng/source/editeng/editdoc.cxx index a34ca78d810d..235708d6c93a 100644 --- a/editeng/source/editeng/editdoc.cxx +++ b/editeng/source/editeng/editdoc.cxx @@ -42,6 +42,16 @@ #include #include #include +#if defined(YRS) +#include +#include +#include +#include +#include +#include +#include +#include +#endif #include #include @@ -714,6 +724,2301 @@ void EditSelection::Adjust( const EditDoc& rNodes ) } } +#if defined(YRS) +#include + +namespace { + +struct YrsReplayGuard +{ + IYrsTransactionSupplier *const m_pYrsSupplier; + IYrsTransactionSupplier::Mode m_Mode; + + explicit YrsReplayGuard(IYrsTransactionSupplier *const pYrsSupplier) + : m_pYrsSupplier(pYrsSupplier) + { + if (m_pYrsSupplier) + { + m_Mode = m_pYrsSupplier->SetMode(IYrsTransactionSupplier::Mode::Replay); + } + } + ~YrsReplayGuard() + { + if (m_pYrsSupplier) + { + m_pYrsSupplier->SetMode(m_Mode); + } + } +}; + +struct YrsWrite +{ + YTransaction *const pTxn; + Branch *const pProps; + Branch *const pText; +}; + +constexpr char CH_PARA = 0x0d; + +YrsWrite GetYrsWrite(IYrsTransactionSupplier *const pYrsSupplier, + OString const& rId, YTransaction *const pInTxn = nullptr) +{ + if (!pYrsSupplier) + { + return { nullptr, nullptr, nullptr }; + } + YDoc *const pDoc{pYrsSupplier->GetYDoc()}; + YTransaction *const pTxn{pInTxn ? pInTxn : pYrsSupplier->GetWriteTransaction()}; + // write is disabled when receiving edits from peers + if (!pTxn) + { + return { nullptr, nullptr, nullptr }; + } + assert(pDoc); + Branch *const pComments{pYrsSupplier->GetCommentMap()}; + ::std::unique_ptr const pComment{ymap_get(pComments, pTxn, rId.getStr())}; + yvalidate(pComment->tag == Y_ARRAY); + yvalidate(pComment->len == 1); + Branch *const pCommentArray{pComment->value.y_type}; + ::std::unique_ptr const pProps{yarray_get(pCommentArray, pTxn, 1)}; + yvalidate(pProps->tag == Y_MAP); + yvalidate(pProps->len == 1); + ::std::unique_ptr const pText{yarray_get(pCommentArray, pTxn, 2)}; + yvalidate(pText->tag == Y_TEXT); + yvalidate(pText->len == 1); + return { pTxn, pProps->value.y_type, pText->value.y_type }; +} + +void YrsSetVertical(IYrsTransactionSupplier *const pYrsSupplier, + OString const& rCommentId, bool const isVertical) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + YInput const input{yinput_bool(isVertical ? Y_TRUE : Y_FALSE)}; + ymap_insert(yw.pProps, yw.pTxn, "is-vertical", &input); +} + +void YrsSetRotation(IYrsTransactionSupplier *const pYrsSupplier, + OString const& rCommentId, TextRotation const nRotation) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + YInput const input{yinput_long(static_cast(nRotation))}; + ymap_insert(yw.pProps, yw.pTxn, "rotation", &input); +} + +void YrsSetDefTab(IYrsTransactionSupplier *const pYrsSupplier, + OString const& rCommentId, sal_uInt16 const nDefTab) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + YInput const input{yinput_long(static_cast(nDefTab))}; + ymap_insert(yw.pProps, yw.pTxn, "def-tab", &input); +} + +void YrsInsertAttribImplImpl(YrsWrite const& yw, SfxPoolItem const& rItm, + uint32_t const nStart, uint32_t const nLen) +{ + ::std::vector tabStops; + ::std::vector<::std::vector> tabStopValues; + ::std::vector tempStrings; + ::std::vector itemArray; + ::std::vector itemNames; + YInput attr; + char const* attrName; + switch (rItm.Which()) + { + case EE_CHAR_COLOR: + case EE_CHAR_BKGCOLOR: + { + sal_uInt32 const nColor{static_cast(rItm).getColor()}; + attr = yinput_long(nColor); + attrName = rItm.Which() == EE_CHAR_COLOR ? "EE_CHAR_COLOR" : "EE_CHAR_BKGCOLOR"; + break; + } + case EE_CHAR_FONTINFO: + case EE_CHAR_FONTINFO_CJK: + case EE_CHAR_FONTINFO_CTL: + { + SvxFontItem const& rItem{static_cast(rItm)}; + tempStrings.reserve(2); // prevent realloc + tempStrings.emplace_back(OUStringToOString(rItem.GetFamilyName(), RTL_TEXTENCODING_UTF8)); + itemArray.emplace_back(yinput_string(tempStrings.back().getStr())); + itemNames.emplace_back("familyname"); + tempStrings.emplace_back(OUStringToOString(rItem.GetStyleName(), RTL_TEXTENCODING_UTF8)); + itemArray.emplace_back(yinput_string(tempStrings.back().getStr())); + itemNames.emplace_back("style"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetFamily()))); + itemNames.emplace_back("family"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetPitch()))); + itemNames.emplace_back("pitch"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetCharSet()))); + itemNames.emplace_back("charset"); + attr = yinput_json_map(const_cast(itemNames.data()), itemArray.data(), itemArray.size()); + attrName = rItm.Which() == EE_CHAR_FONTINFO + ? "EE_CHAR_FONTINFO" + : rItm.Which() == EE_CHAR_FONTINFO_CJK ? "EE_CHAR_FONTINFO_CJK" : "EE_CHAR_FONTINFO_CTL"; + break; + } + case EE_CHAR_FONTHEIGHT: + case EE_CHAR_FONTHEIGHT_CJK: + case EE_CHAR_FONTHEIGHT_CTL: + { + SvxFontHeightItem const& rItem{static_cast(rItm)}; + itemNames.emplace_back("height"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetHeight()))); + itemNames.emplace_back("prop"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetProp()))); + itemNames.emplace_back("propunit"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetPropUnit()))); + attr = yinput_json_map(const_cast(itemNames.data()), itemArray.data(), itemArray.size()); + attrName = rItm.Which() == EE_CHAR_FONTHEIGHT + ? "EE_CHAR_FONTHEIGHT" + : rItm.Which() == EE_CHAR_FONTHEIGHT_CJK ? "EE_CHAR_FONTHEIGHT_CJK" : "EE_CHAR_FONTHEIGHT_CTL"; + break; + } + case EE_CHAR_FONTWIDTH: + { + SvxCharScaleWidthItem const& rItem{static_cast(rItm)}; + attr = yinput_long(rItem.GetValue()); + attrName = "EE_CHAR_FONTWIDTH"; + break; + } + case EE_CHAR_WEIGHT: + case EE_CHAR_WEIGHT_CJK: + case EE_CHAR_WEIGHT_CTL: + { + SvxWeightItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetWeight())); + attrName = rItm.Which() == EE_CHAR_WEIGHT + ? "EE_CHAR_WEIGHT" + : rItm.Which() == EE_CHAR_WEIGHT_CJK ? "EE_CHAR_WEIGHT_CJK" : "EE_CHAR_WEIGHT_CTL"; + break; + } + case EE_CHAR_UNDERLINE: + case EE_CHAR_OVERLINE: + { + SvxTextLineItem const& rItem{static_cast(rItm)}; + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLineStyle()))); + itemNames.emplace_back("style"); + itemArray.emplace_back(yinput_long(uint64_t(sal_uInt32(rItem.GetColor())))); + itemNames.emplace_back("color"); + attr = yinput_json_map(const_cast(itemNames.data()), itemArray.data(), itemArray.size()); + attrName = rItm.Which() == EE_CHAR_UNDERLINE ? "EE_CHAR_UNDERLINE" : "EE_CHAR_OVERLINE"; + break; + } + case EE_CHAR_STRIKEOUT: + { + SvxCrossedOutItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetStrikeout())); + attrName = "EE_CHAR_STRIKEOUT"; + break; + } + case EE_CHAR_ITALIC: + case EE_CHAR_ITALIC_CJK: + case EE_CHAR_ITALIC_CTL: + { + SvxPostureItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetPosture())); + attrName = rItm.Which() == EE_CHAR_ITALIC + ? "EE_CHAR_ITALIC" + : rItm.Which() == EE_CHAR_ITALIC_CJK ? "EE_CHAR_ITALIC_CJK" : "EE_CHAR_ITALIC_CTL"; + break; + } + case EE_CHAR_OUTLINE: + { + SvxContourItem const& rItem{static_cast(rItm)}; + attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE); + attrName = "EE_CHAR_OUTLINE"; + break; + } + case EE_CHAR_SHADOW: + { + SvxShadowedItem const& rItem{static_cast(rItm)}; + attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE); + attrName = "EE_CHAR_SHADOW"; + break; + } + case EE_CHAR_ESCAPEMENT: + { + SvxEscapementItem const& rItem{static_cast(rItm)}; + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetEsc()))); + itemNames.emplace_back("esc"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetProportionalHeight()))); + itemNames.emplace_back("prop"); + attr = yinput_json_map(const_cast(itemNames.data()), itemArray.data(), itemArray.size()); + attrName = "EE_CHAR_ESCAPEMENT"; + break; + } + case EE_CHAR_PAIRKERNING: + { + SvxAutoKernItem const& rItem{static_cast(rItm)}; + attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE); + attrName = "EE_CHAR_PAIRKERNING"; + break; + } + case EE_CHAR_KERNING: + { + SvxKerningItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetValue())); + attrName = "EE_CHAR_KERNING"; + break; + } + case EE_CHAR_WLM: + { + SvxWordLineModeItem const& rItem{static_cast(rItm)}; + attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE); + attrName = "EE_CHAR_WLM"; + break; + } + case EE_CHAR_LANGUAGE: + case EE_CHAR_LANGUAGE_CJK: + case EE_CHAR_LANGUAGE_CTL: + { + SvxLanguageItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetValue().get())); + attrName = rItm.Which() == EE_CHAR_LANGUAGE + ? "EE_CHAR_LANGUAGE" + : rItm.Which() == EE_CHAR_LANGUAGE_CJK ? "EE_CHAR_LANGUAGE_CJK" : "EE_CHAR_LANGUAGE_CTL"; + break; + } + case EE_CHAR_EMPHASISMARK: + { + SvxEmphasisMarkItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetValue())); + attrName = "EE_CHAR_EMPHASISMARK"; + break; + } + case EE_CHAR_RELIEF: + { + SvxCharReliefItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetValue())); + attrName = "EE_CHAR_RELIEF"; + break; + } + case EE_CHAR_CASEMAP: + { + SvxCaseMapItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetValue())); + attrName = "EE_CHAR_CASEMAP"; + break; + } + case EE_PARA_WRITINGDIR: + { + SvxFrameDirectionItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetValue())); + attrName = "EE_PARA_WRITINGDIR"; + break; + } + case EE_PARA_HANGINGPUNCTUATION: + { + SvxHangingPunctuationItem const& rItem{static_cast(rItm)}; + attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE); + attrName = "EE_PARA_HANGINGPUNCTUATION"; + break; + } + case EE_PARA_FORBIDDENRULES: + { + SvxForbiddenRuleItem const& rItem{static_cast(rItm)}; + attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE); + attrName = "EE_PARA_FORBIDDENRULES"; + break; + } + case EE_PARA_ASIANCJKSPACING: + { + SvxScriptSpaceItem const& rItem{static_cast(rItm)}; + attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE); + attrName = "EE_PARA_ASIANCJKSPACING"; + break; + } +//TODO complex, but apparently no way to set this in comment? inline constexpr TypedWhichId EE_PARA_NUMBULLET (EE_PARA_START+5); + case EE_PARA_HYPHENATE: + case EE_PARA_HYPHENATE_NO_CAPS: + case EE_PARA_HYPHENATE_NO_LAST_WORD: + case EE_PARA_BULLETSTATE: + { + SfxBoolItem const& rItem{static_cast(rItm)}; + attr = yinput_bool(rItem.GetValue() ? Y_TRUE : Y_FALSE); + attrName = rItm.Which() == EE_PARA_HYPHENATE + ? "EE_PARA_HYPHENATE" + : rItm.Which() == EE_PARA_HYPHENATE_NO_CAPS + ? "EE_PARA_HYPHENATE_NO_CAPS" + : rItm.Which() == EE_PARA_HYPHENATE_NO_LAST_WORD + ? "EE_PARA_HYPHENATE_NO_LAST_WORD" + : "EE_PARA_BULLETSTATE"; + break; + } +//TODO no way to set this in comment? inline constexpr TypedWhichId EE_PARA_OUTLLRSPACE (EE_PARA_START+10); + case EE_PARA_OUTLLEVEL: + { + SfxInt16Item const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetValue())); + attrName = "EE_PARA_OUTLLEVEL"; + break; + } +//TODO complex, but apparently no way to set this in comment? inline constexpr TypedWhichId EE_PARA_BULLET (EE_PARA_START+12); + case EE_PARA_LRSPACE: + { + SvxLRSpaceItem const& rItem{static_cast(rItm)}; + itemArray.emplace_back(yinput_float(rItem.GetTextFirstLineOffset().m_dValue)); + itemNames.emplace_back("first-line-offset"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetTextFirstLineOffset().m_nUnit))); + itemNames.emplace_back("first-line-offset-unit"); + itemArray.emplace_back(yinput_float(rItem.GetLeft().m_dValue)); + itemNames.emplace_back("left-margin"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLeft().m_nUnit))); + itemNames.emplace_back("left-margin-unit"); + itemArray.emplace_back(yinput_float(rItem.GetRight().m_dValue)); + itemNames.emplace_back("right-margin"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetRight().m_nUnit))); + itemNames.emplace_back("right-margin-unit"); + itemArray.emplace_back(yinput_bool(rItem.IsAutoFirst() ? Y_TRUE : Y_FALSE)); + itemNames.emplace_back("auto-first"); + attr = yinput_json_map(const_cast(itemNames.data()), itemArray.data(), itemArray.size()); + attrName = "EE_PARA_LRSPACE"; + break; + } + case EE_PARA_ULSPACE: + { + SvxULSpaceItem const& rItem{static_cast(rItm)}; + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetUpper()))); + itemNames.emplace_back("upper-margin"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLower()))); + itemNames.emplace_back("lower-margin"); + // TODO what does EE support here? + attr = yinput_json_map(const_cast(itemNames.data()), itemArray.data(), itemArray.size()); + attrName = "EE_PARA_ULSPACE"; + break; + } + case EE_PARA_SBL: + { + SvxLineSpacingItem const& rItem{static_cast(rItm)}; + switch (rItem.GetLineSpaceRule()) + { + case SvxLineSpaceRule::Auto: + break; + case SvxLineSpaceRule::Fix: + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLineHeight()))); + itemNames.emplace_back("line-space-fix"); + break; + case SvxLineSpaceRule::Min: + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetLineHeight()))); + itemNames.emplace_back("line-space-min"); + break; + } + switch (rItem.GetInterLineSpaceRule()) + { + case SvxInterLineSpaceRule::Off: + break; + case SvxInterLineSpaceRule::Prop: + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetPropLineSpace()))); + itemNames.emplace_back("inter-line-space-prop"); + break; + case SvxInterLineSpaceRule::Fix: + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetInterLineSpace()))); + itemNames.emplace_back("inter-line-space-fix"); + break; + } + attr = yinput_json_map(const_cast(itemNames.data()), itemArray.data(), itemArray.size()); + attrName = "EE_PARA_SBL"; + break; + } + case EE_PARA_JUST: + { + SvxAdjustItem const& rItem{static_cast(rItm)}; + switch (rItem.GetAdjust()) + { + case SvxAdjust::Left: + case SvxAdjust::Right: + case SvxAdjust::Center: + attr = yinput_long(uint64_t(rItem.GetAdjust())); + break; + case SvxAdjust::Block: + switch (rItem.GetLastBlock()) + { + case SvxAdjust::Left: + attr = yinput_long(uint64_t(SvxAdjust::Block)); + break; + case SvxAdjust::Center: + attr = yinput_long(uint64_t(5)); + break; + case SvxAdjust::Block: + attr = yinput_long(uint64_t(rItem.GetOneWord() == SvxAdjust::Block ? 7 : 6)); + break; + default: + assert(false); + } + break; + default: + assert(false); + } + attrName = "EE_PARA_JUST"; + break; + } + case EE_PARA_TABS: + { + SvxTabStopItem const& rItem{static_cast(rItm)}; + itemNames.emplace_back("default-distance"); + itemArray.emplace_back(yinput_long(uint64_t(rItem.GetDefaultDistance()))); + tabStopValues.reserve(rItem.Count()); // prevent realloc + for (decltype(rItem.Count()) i{0}; i < rItem.Count(); ++i) + { + SvxTabStop const& rTab{rItem.At(i)}; + char const*const names[]{"pos", "adjustment", "decimal", "fill"}; + tabStopValues.emplace_back(); + tabStopValues.back().emplace_back(yinput_long(uint64_t(rTab.GetTabPos()))); + tabStopValues.back().emplace_back(yinput_long(uint64_t(rTab.GetAdjustment()))); + tabStopValues.back().emplace_back(yinput_long(uint64_t(rTab.GetDecimal()))); + tabStopValues.back().emplace_back(yinput_long(uint64_t(rTab.GetFill()))); + tabStops.emplace_back(yinput_json_map(const_cast(names), tabStopValues.back().data(), 4)); + } + itemNames.emplace_back("tab-stops"); + itemArray.emplace_back(yinput_json_array(tabStops.data(), tabStops.size())); + attr = yinput_json_map(const_cast(itemNames.data()), itemArray.data(), itemArray.size()); + attrName = "EE_PARA_TABS"; + break; + } + case EE_PARA_JUST_METHOD: + { + SvxJustifyMethodItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetValue())); + attrName = "EE_PARA_JUST_METHOD"; + break; + } + case EE_PARA_VER_JUST: + { + SvxVerJustifyItem const& rItem{static_cast(rItm)}; + attr = yinput_long(uint64_t(rItem.GetValue())); + attrName = "EE_PARA_VER_JUST"; + break; + } + // these aren't editable? +//constexpr TypedWhichId EE_CHAR_XMLATTRIBS (EE_CHAR_START+27); +//constexpr TypedWhichId EE_CHAR_GRABBAG (EE_CHAR_START+30); + + default: + assert(false); + } + assert(itemNames.size() == itemArray.size()); + YInput const attrs{yinput_json_map(const_cast(&attrName), &attr, 1)}; + ytext_format(yw.pText, yw.pTxn, nStart, nLen, &attrs); +} + +void YrsInsertAttribImpl(YrsWrite const& yw, uint32_t const offset, EditCharAttrib const*const pAttr) +{ + auto const start{offset + pAttr->GetStart()}; + auto const len{pAttr->GetEnd() - pAttr->GetStart()}; + YrsInsertAttribImplImpl(yw, *pAttr->GetItem(), start, len); +} + +void YrsInsertFeature(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, + EditDoc const& rDoc, uint32_t const index, EditCharAttrib const*const pAttr) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + uint32_t i{0}; + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + i += pAttr->GetStart(); + char const feature[]{ CH_FEATURE, '\0' }; + switch (pAttr->Which()) + { + case EE_FEATURE_TAB: + case EE_FEATURE_LINEBR: + { + YInput const type{yinput_string(pAttr->Which() == EE_FEATURE_TAB ? "tab" : "line")}; + YInput attrArray[]{ type }; + char const*const attrNames[]{ "feature" }; + YInput const attrs{yinput_json_map(const_cast(attrNames), attrArray, 1)}; + ytext_insert(yw.pText, yw.pTxn, i, feature, &attrs); + break; + } + case EE_FEATURE_FIELD: + { + SvxURLField const*const pURLField{dynamic_cast(dynamic_cast(pAttr->GetItem())->GetField())}; + assert(pURLField); + YInput const type{yinput_string("url")}; + // ??? somehow this comes out as Y_JSON_NUM at the other end? + YInput const format{yinput_long(static_cast(pURLField->GetFormat()))}; + OString const urlStr{OUStringToOString(pURLField->GetURL(), RTL_TEXTENCODING_UTF8)}; + YInput const url{yinput_string(urlStr.getStr())}; + OString const reprStr{OUStringToOString(pURLField->GetRepresentation(), RTL_TEXTENCODING_UTF8)}; + YInput const representation{yinput_string(reprStr.getStr())}; + OString const targetStr{OUStringToOString(pURLField->GetTargetFrame(), RTL_TEXTENCODING_UTF8)}; + YInput const targetframe{yinput_string(targetStr.getStr())}; + YInput attrArray[]{ type, format, url, representation, targetframe }; + char const*const attrNames[]{ "feature", "url-format", "url-url", "url-representation", "url-targetframe" }; + // don't use yinput_ymap for this! + YInput const attrs{yinput_json_map(const_cast(attrNames), attrArray, 5)}; + ytext_insert(yw.pText, yw.pTxn, i, feature, &attrs); + + break; + } + default: // EE_FEATURE_NOTCONV appears unused? + assert(false); + } +} + +void YrsAddPara(IYrsTransactionSupplier *const pYrsSupplier, + OString const& rCommentId, EditDoc const& rDoc, uint32_t const index) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + SAL_DEBUG("YRS YrsAddPara"); + // need to encode into 1 YText + char const para[]{ CH_PARA, '\0' }; + uint32_t i{0}; + // UTF-16 index should be equal to EditDoc one + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + ContentAttribs const& rParaAttribs{rDoc.GetObject(index)->GetContentAttribs()}; + auto const pStyle{rParaAttribs.GetStyleSheet()}; + if (pStyle) + { + OString const styleName{OUStringToOString(pStyle->GetName(), RTL_TEXTENCODING_UTF8)}; + YInput const style{yinput_string(styleName.getStr())}; + YInput attrArray[]{ style }; + char const*const attrNames[]{ "para-style" }; + YInput const attrs{yinput_json_map(const_cast(attrNames), attrArray, 1)}; + ytext_insert(yw.pText, yw.pTxn, i, para, &attrs); + } + else + { + ytext_insert(yw.pText, yw.pTxn, i, para, nullptr); + } + for (SfxItemIter it{rParaAttribs.GetItems()}; !it.IsAtEnd(); it.NextItem()) + { + YrsInsertAttribImplImpl(yw, *it.GetCurItem(), i, 1); + } +} + +void YrsRemovePara(IYrsTransactionSupplier *const pYrsSupplier, + OString const& rCommentId, EditDoc const& rDoc, uint32_t const index) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + uint32_t i{0}; + if (index != 0) + { + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + } + uint32_t const len(rDoc.GetObject(index)->Len() + 1); + ytext_remove_range(yw.pText, yw.pTxn, i, len); +} + +void YrsClear(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + auto const len{ytext_len(yw.pText, yw.pTxn)}; + ytext_remove_range(yw.pText, yw.pTxn, 0, len); +} + +void YrsInsertParaBreak(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, + EditDoc const& rDoc, uint32_t const index, uint32_t const content) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + // need to encode into 1 YText + char const para[]{ CH_PARA, '\0' }; + uint32_t i{0}; + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + i += content; + ContentAttribs const& rParaAttribs{rDoc.GetObject(index)->GetContentAttribs()}; + OString const styleName{OUStringToOString(rParaAttribs.GetStyleSheet()->GetName(), RTL_TEXTENCODING_UTF8)}; + YInput const style{yinput_string(styleName.getStr())}; + YInput attrArray[]{ style }; + char const*const attrNames[]{ "para-style" }; + YInput const attrs{yinput_json_map(const_cast(attrNames), attrArray, 1)}; + ytext_insert(yw.pText, yw.pTxn, i, para, &attrs); + for (SfxItemIter it{rParaAttribs.GetItems()}; !it.IsAtEnd(); it.NextItem()) + { + YrsInsertAttribImplImpl(yw, *it.GetCurItem(), i, 1); + } +} + +void YrsInsertText(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, + EditDoc const& rDoc, uint32_t const index, uint32_t const content, ::std::u16string_view const rText) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + uint32_t i{0}; + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + i += content; + OString const text{::rtl::OUStringToOString(rText, RTL_TEXTENCODING_UTF8)}; + ytext_insert(yw.pText, yw.pTxn, i, text.getStr(), nullptr); +} + +void YrsConnectPara(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, + EditDoc const& rDoc, uint32_t const index, uint32_t const pos) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + uint32_t i{0}; + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + i += pos; + ytext_remove_range(yw.pText, yw.pTxn, i, 1); +} + +void YrsRemoveChars(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, + EditDoc const& rDoc, uint32_t const index, uint32_t const content, uint32_t const length) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + uint32_t i{0}; + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + i += content; + ytext_remove_range(yw.pText, yw.pTxn, i, length); +} + +void YrsSetStyle(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, + EditDoc const& rDoc, uint32_t const index, ::std::u16string_view const rStyle) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + uint32_t i{0}; + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + i += rDoc.GetObject(index)->Len(); + OString const styleName{OUStringToOString(rStyle, RTL_TEXTENCODING_UTF8)}; + YInput const style{yinput_string(styleName.getStr())}; + YInput attrArray[]{ style }; + char const*const attrNames[]{ "para-style" }; + YInput const attrs{yinput_json_map(const_cast(attrNames), attrArray, 1)}; + ytext_format(yw.pText, yw.pTxn, i, 1, &attrs); +} + +void YrsSetParaAttr(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, + EditDoc const& rDoc, uint32_t const index, SfxPoolItem const& rItem) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + uint32_t i{0}; + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + i += rDoc.GetObject(index)->Len(); + YrsInsertAttribImplImpl(yw, rItem, i, 1); +} + +char const* YrsWhichToAttrName(sal_Int16 const nWhich) +{ + switch (nWhich) + { + case EE_CHAR_COLOR: + return "EE_CHAR_COLOR"; + case EE_CHAR_BKGCOLOR: + return "EE_CHAR_BKGCOLOR"; + case EE_CHAR_FONTINFO: + return "EE_CHAR_FONTINFO"; + case EE_CHAR_FONTINFO_CJK: + return "EE_CHAR_FONTINFO_CJK"; + case EE_CHAR_FONTINFO_CTL: + return "EE_CHAR_FONTINFO_CTL"; + case EE_CHAR_FONTHEIGHT: + return "EE_CHAR_FONTHEIGHT"; + case EE_CHAR_FONTHEIGHT_CJK: + return "EE_CHAR_FONTHEIGHT_CJK"; + case EE_CHAR_FONTHEIGHT_CTL: + return "EE_CHAR_FONTHEIGHT_CTL"; + case EE_CHAR_FONTWIDTH: + return "EE_CHAR_FONTWIDTH"; + case EE_CHAR_WEIGHT: + return "EE_CHAR_WEIGHT"; + case EE_CHAR_WEIGHT_CJK: + return "EE_CHAR_WEIGHT_CJK"; + case EE_CHAR_WEIGHT_CTL: + return "EE_CHAR_WEIGHT_CTL"; + case EE_CHAR_UNDERLINE: + return "EE_CHAR_UNDERLINE"; + case EE_CHAR_OVERLINE: + return "EE_CHAR_OVERLINE"; + case EE_CHAR_STRIKEOUT: + return "EE_CHAR_STRIKEOUT"; + case EE_CHAR_ITALIC: + return "EE_CHAR_ITALIC"; + case EE_CHAR_ITALIC_CJK: + return "EE_CHAR_ITALIC_CJK"; + case EE_CHAR_ITALIC_CTL: + return "EE_CHAR_ITALIC_CTL"; + case EE_CHAR_OUTLINE: + return "EE_CHAR_OUTLINE"; + case EE_CHAR_SHADOW: + return "EE_CHAR_SHADOW"; + case EE_CHAR_ESCAPEMENT: + return "EE_CHAR_ESCAPEMENT"; + case EE_CHAR_PAIRKERNING: + return "EE_CHAR_PAIRKERNING"; + case EE_CHAR_KERNING: + return "EE_CHAR_KERNING"; + case EE_CHAR_WLM: + return "EE_CHAR_WLM"; + case EE_CHAR_LANGUAGE: + return "EE_CHAR_LANGUAGE"; + case EE_CHAR_LANGUAGE_CJK: + return "EE_CHAR_LANGUAGE_CJK"; + case EE_CHAR_LANGUAGE_CTL: + return "EE_CHAR_LANGUAGE_CTL"; + case EE_CHAR_EMPHASISMARK: + return "EE_CHAR_EMPHASISMARK"; + case EE_CHAR_RELIEF: + return "EE_CHAR_RELIEF"; + case EE_CHAR_CASEMAP: + return "EE_CHAR_CASEMAP"; + case EE_PARA_WRITINGDIR: + return "EE_PARA_WRITINGDIR"; + case EE_PARA_HANGINGPUNCTUATION: + return "EE_PARA_HANGINGPUNCTUATION"; + case EE_PARA_FORBIDDENRULES: + return "EE_PARA_FORBIDDENRULES"; + case EE_PARA_ASIANCJKSPACING: + return "EE_PARA_ASIANCJKSPACING"; +//TODO complex, but apparently no way to set this in comment? inline constexpr TypedWhichId EE_PARA_NUMBULLET (EE_PARA_START+5); + case EE_PARA_HYPHENATE: + return "EE_PARA_HYPHENATE"; + case EE_PARA_HYPHENATE_NO_CAPS: + return "EE_PARA_HYPHENATE_NO_CAPS"; + case EE_PARA_HYPHENATE_NO_LAST_WORD: + return "EE_PARA_HYPHENATE_NO_LAST_WORD"; + case EE_PARA_BULLETSTATE: + return "EE_PARA_BULLETSTATE"; +//TODO no way to set this in comment? inline constexpr TypedWhichId EE_PARA_OUTLLRSPACE (EE_PARA_START+10); + case EE_PARA_OUTLLEVEL: + return "EE_PARA_OUTLLEVEL"; +//TODO complex, but apparently no way to set this in comment? inline constexpr TypedWhichId EE_PARA_BULLET (EE_PARA_START+12); + case EE_PARA_LRSPACE: + return "EE_PARA_LRSPACE"; + case EE_PARA_ULSPACE: + return "EE_PARA_ULSPACE"; + case EE_PARA_SBL: + return "EE_PARA_SBL"; + case EE_PARA_JUST: + return "EE_PARA_JUST"; + case EE_PARA_TABS: + return "EE_PARA_TABS"; + case EE_PARA_JUST_METHOD: + return "EE_PARA_JUST_METHOD"; + case EE_PARA_VER_JUST: + return "EE_PARA_VER_JUST"; + default: + assert(false); + } +} + +void YrsRemoveAttrib(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, + EditDoc const& rDoc, uint32_t const index, sal_uInt16 const nWhich, sal_Int32 const nStart, sal_Int32 const nEnd) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + uint32_t i{0}; + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + YInput const attr{yinput_null()}; + char const*const attrName{YrsWhichToAttrName(nWhich)}; + YInput const attrs{yinput_json_map(const_cast(&attrName), const_cast(&attr), 1)}; + ytext_format(yw.pText, yw.pTxn, i + nStart, nEnd - nStart, &attrs); +} + +void YrsInsertAttrib(IYrsTransactionSupplier *const pYrsSupplier, OString const& rCommentId, EditDoc const& rDoc, uint32_t const index, EditCharAttrib const*const pAttr) +{ + YrsWrite const yw{GetYrsWrite(pYrsSupplier, rCommentId)}; + if (yw.pTxn == nullptr) + { + return; + } + uint32_t i{0}; + for (auto paras{index}; paras != 0; --paras) + { + i += rDoc.GetObject(paras-1)->Len() + 1; + } + YrsInsertAttribImpl(yw, i, pAttr); +} + +uint64_t YrsReadInt(YOutput const& rValue) +{ + // with the v1 encoding, JSON is being sent apparently (like "family":2) , which has issues with integers being sometimes read as floats so workaround here + if (rValue.tag == Y_JSON_INT) + { + return rValue.value.integer; + } + else + { + yvalidate(rValue.tag == Y_JSON_NUM); + return ::std::lround(rValue.value.num); + } +} + +void YrsImplInsertAttr(SfxItemSet & rSet, ::std::vector *const pRemoved, + char const*const pKey, YOutput const& rValue) +{ + sal_uInt16 nWhich{0}; + if (strcmp(pKey, "EE_CHAR_COLOR") == 0) + { + nWhich = EE_CHAR_COLOR; + } + else if (strcmp(pKey, "EE_CHAR_BKGCOLOR") == 0) + { + nWhich = EE_CHAR_BKGCOLOR; + } + else if (strcmp(pKey, "EE_CHAR_FONTINFO") == 0) + { + nWhich = EE_CHAR_FONTINFO; + } + else if (strcmp(pKey, "EE_CHAR_FONTINFO_CJK") == 0) + { + nWhich = EE_CHAR_FONTINFO_CJK; + } + else if (strcmp(pKey, "EE_CHAR_FONTINFO_CTL") == 0) + { + nWhich = EE_CHAR_FONTINFO_CTL; + } + else if (strcmp(pKey, "EE_CHAR_FONTHEIGHT") == 0) + { + nWhich = EE_CHAR_FONTHEIGHT; + } + else if (strcmp(pKey, "EE_CHAR_FONTHEIGHT_CJK") == 0) + { + nWhich = EE_CHAR_FONTHEIGHT_CJK; + } + else if (strcmp(pKey, "EE_CHAR_FONTHEIGHT_CTL") == 0) + { + nWhich = EE_CHAR_FONTHEIGHT_CTL; + } + else if (strcmp(pKey, "EE_CHAR_FONTWIDTH") == 0) + { + nWhich = EE_CHAR_FONTWIDTH; + } + else if (strcmp(pKey, "EE_CHAR_WEIGHT") == 0) + { + nWhich = EE_CHAR_WEIGHT; + } + else if (strcmp(pKey, "EE_CHAR_WEIGHT_CJK") == 0) + { + nWhich = EE_CHAR_WEIGHT_CJK; + } + else if (strcmp(pKey, "EE_CHAR_WEIGHT_CTL") == 0) + { + nWhich = EE_CHAR_WEIGHT_CTL; + } + else if (strcmp(pKey, "EE_CHAR_UNDERLINE") == 0) + { + nWhich = EE_CHAR_UNDERLINE; + } + else if (strcmp(pKey, "EE_CHAR_OVERLINE") == 0) + { + nWhich = EE_CHAR_OVERLINE; + } + else if (strcmp(pKey, "EE_CHAR_STRIKEOUT") == 0) + { + nWhich = EE_CHAR_STRIKEOUT; + } + else if (strcmp(pKey, "EE_CHAR_ITALIC") == 0) + { + nWhich = EE_CHAR_ITALIC; + } + else if (strcmp(pKey, "EE_CHAR_ITALIC_CJK") == 0) + { + nWhich = EE_CHAR_ITALIC_CJK; + } + else if (strcmp(pKey, "EE_CHAR_ITALIC_CTL") == 0) + { + nWhich = EE_CHAR_ITALIC_CTL; + } + else if (strcmp(pKey, "EE_CHAR_OUTLINE") == 0) + { + nWhich = EE_CHAR_OUTLINE; + } + else if (strcmp(pKey, "EE_CHAR_SHADOW") == 0) + { + nWhich = EE_CHAR_SHADOW; + } + else if (strcmp(pKey, "EE_CHAR_ESCAPEMENT") == 0) + { + nWhich = EE_CHAR_ESCAPEMENT; + } + else if (strcmp(pKey, "EE_CHAR_PAIRKERNING") == 0) + { + nWhich = EE_CHAR_PAIRKERNING; + } + else if (strcmp(pKey, "EE_CHAR_KERNING") == 0) + { + nWhich = EE_CHAR_KERNING; + } + else if (strcmp(pKey, "EE_CHAR_WLM") == 0) + { + nWhich = EE_CHAR_WLM; + } + else if (strcmp(pKey, "EE_CHAR_LANGUAGE") == 0) + { + nWhich = EE_CHAR_LANGUAGE; + } + else if (strcmp(pKey, "EE_CHAR_LANGUAGE_CJK") == 0) + { + nWhich = EE_CHAR_LANGUAGE_CJK; + } + else if (strcmp(pKey, "EE_CHAR_LANGUAGE_CTL") == 0) + { + nWhich = EE_CHAR_LANGUAGE_CTL; + } + else if (strcmp(pKey, "EE_CHAR_EMPHASISMARK") == 0) + { + nWhich = EE_CHAR_EMPHASISMARK; + } + else if (strcmp(pKey, "EE_CHAR_RELIEF") == 0) + { + nWhich = EE_CHAR_RELIEF; + } + else if (strcmp(pKey, "EE_CHAR_CASEMAP") == 0) + { + nWhich = EE_CHAR_CASEMAP; + } + else if (strcmp(pKey, "EE_PARA_WRITINGDIR") == 0) + { + nWhich = EE_PARA_WRITINGDIR; + } + else if (strcmp(pKey, "EE_PARA_HANGINGPUNCTUATION") == 0) + { + nWhich = EE_PARA_HANGINGPUNCTUATION; + } + else if (strcmp(pKey, "EE_PARA_FORBIDDENRULES") == 0) + { + nWhich = EE_PARA_FORBIDDENRULES; + } + else if (strcmp(pKey, "EE_PARA_ASIANCJKSPACING") == 0) + { + nWhich = EE_PARA_ASIANCJKSPACING; + } + else if (strcmp(pKey, "EE_PARA_HYPHENATE") == 0) + { + nWhich = EE_PARA_HYPHENATE; + } + else if (strcmp(pKey, "EE_PARA_HYPHENATE_NO_CAPS") == 0) + { + nWhich = EE_PARA_HYPHENATE_NO_CAPS; + } + else if (strcmp(pKey, "EE_PARA_HYPHENATE_NO_LAST_WORD") == 0) + { + nWhich = EE_PARA_HYPHENATE_NO_LAST_WORD; + } + else if (strcmp(pKey, "EE_PARA_BULLETSTATE") == 0) + { + nWhich = EE_PARA_BULLETSTATE; + } + else if (strcmp(pKey, "EE_PARA_OUTLLEVEL") == 0) + { + nWhich = EE_PARA_OUTLLEVEL; + } + else if (strcmp(pKey, "EE_PARA_LRSPACE") == 0) + { + nWhich = EE_PARA_LRSPACE; + } + else if (strcmp(pKey, "EE_PARA_ULSPACE") == 0) + { + nWhich = EE_PARA_ULSPACE; + } + else if (strcmp(pKey, "EE_PARA_SBL") == 0) + { + nWhich = EE_PARA_SBL; + } + else if (strcmp(pKey, "EE_PARA_JUST") == 0) + { + nWhich = EE_PARA_JUST; + } + else if (strcmp(pKey, "EE_PARA_TABS") == 0) + { + nWhich = EE_PARA_TABS; + } + else if (strcmp(pKey, "EE_PARA_JUST_METHOD") == 0) + { + nWhich = EE_PARA_JUST_METHOD; + } + else if (strcmp(pKey, "EE_PARA_VER_JUST") == 0) + { + nWhich = EE_PARA_VER_JUST; + } + else if (pKey[0] == 'E' && pKey[1] == 'E' && pKey[2] == '_') + { + abort(); + } + else + { + return; + } + + if (rValue.tag == Y_JSON_NULL) + { + assert(pRemoved); + if (pRemoved) + { + pRemoved->emplace_back(nWhich); + } + return; + } + else switch (nWhich) + { + case EE_CHAR_COLOR: + case EE_CHAR_BKGCOLOR: + { + Color const c(ColorTransparency, YrsReadInt(rValue)); + SvxColorItem const item(c, nWhich); + rSet.Put(item); + break; + } + case EE_CHAR_FONTINFO: + case EE_CHAR_FONTINFO_CJK: + case EE_CHAR_FONTINFO_CTL: + { + yvalidate(rValue.tag == Y_JSON_MAP); + ::std::optional oFamilyName; + ::std::optional oStyle; + ::std::optional oFamily; + ::std::optional oPitch; + ::std::optional oCharset; + for (decltype(rValue.len) i = 0; i < rValue.len; ++i) + { + const char*const pEntry{rValue.value.map[i].key}; + if (strcmp(pEntry, "familyname") == 0) + { + yvalidate(rValue.value.map[i].value->tag == Y_JSON_STR); + oFamilyName.emplace(OStringToOUString(rValue.value.map[i].value->value.str, RTL_TEXTENCODING_UTF8)); + } + else if (strcmp(pEntry, "style") == 0) + { + yvalidate(rValue.value.map[i].value->tag == Y_JSON_STR); + oStyle.emplace(OStringToOUString(rValue.value.map[i].value->value.str, RTL_TEXTENCODING_UTF8)); + } + else if (strcmp(pEntry, "family") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(FAMILY_DONTKNOW <= value && value <= FAMILY_SYSTEM); + oFamily.emplace(FontFamily(value)); + } + else if (strcmp(pEntry, "pitch") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(PITCH_DONTKNOW <= value && value <= PITCH_VARIABLE); + oPitch.emplace(FontPitch(value)); + } + else if (strcmp(pEntry, "charset") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*RTL_TEXTENCODING_DONTKNOW <= value &&*/ value <= RTL_TEXTENCODING_UNICODE); + oCharset.emplace(rtl_TextEncoding(value)); + } + else yvalidate(false); + } + if (oFamilyName && oStyle && oFamily && oPitch && oCharset) + { + SvxFontItem const item{ + *oFamily, *oFamilyName, *oStyle, *oPitch, *oCharset, nWhich}; + rSet.Put(item); + } + else yvalidate(false); + break; + } + case EE_CHAR_FONTHEIGHT: + case EE_CHAR_FONTHEIGHT_CJK: + case EE_CHAR_FONTHEIGHT_CTL: + { + yvalidate(rValue.tag == Y_JSON_MAP); + ::std::optional oHeight; + ::std::optional oProp; + ::std::optional oMapUnit; + for (decltype(rValue.len) i = 0; i < rValue.len; ++i) + { + const char*const pEntry{rValue.value.map[i].key}; + if (strcmp(pEntry, "height") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_INT32); + oHeight.emplace(value); + } + else if (strcmp(pEntry, "prop") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_INT16); + oProp.emplace(value); + } + else if (strcmp(pEntry, "propunit") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= ::std::underlying_type_t(MapUnit::LAST)); + oMapUnit.emplace(MapUnit(value)); + } + else yvalidate(false); + } + if (oHeight && oProp && oMapUnit) + { + SvxFontHeightItem item{*oHeight, 100, nWhich}; + item.SetProp(*oProp, *oMapUnit); + rSet.Put(item); + } + else yvalidate(false); + break; + } + case EE_CHAR_FONTWIDTH: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_INT16); + SvxCharScaleWidthItem const item{sal_uInt16(value), TypedWhichId(nWhich)}; + rSet.Put(item); + break; + } + case EE_CHAR_WEIGHT: + case EE_CHAR_WEIGHT_CJK: + case EE_CHAR_WEIGHT_CTL: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(WEIGHT_DONTKNOW <= value && value <= WEIGHT_BLACK); + SvxWeightItem const item{FontWeight(value), nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_UNDERLINE: + case EE_CHAR_OVERLINE: + { + yvalidate(rValue.tag == Y_JSON_MAP); + ::std::optional oStyle; + ::std::optional oColor; + for (decltype(rValue.len) i = 0; i < rValue.len; ++i) + { + const char*const pEntry{rValue.value.map[i].key}; + if (strcmp(pEntry, "style") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(LINESTYLE_NONE <= value && value <= LINESTYLE_BOLDWAVE); + oStyle.emplace(FontLineStyle(value)); + } + else if (strcmp(pEntry, "color") == 0) + { + oColor.emplace(ColorTransparency, YrsReadInt(*rValue.value.map[i].value)); + } + else yvalidate(false); + } + if (oStyle && oColor) + { + if (nWhich == EE_CHAR_UNDERLINE) + { + SvxUnderlineItem item{*oStyle, EE_CHAR_UNDERLINE}; + item.SetColor(*oColor); + rSet.Put(item); + } + else + { + SvxOverlineItem item{*oStyle, EE_CHAR_OVERLINE}; + item.SetColor(*oColor); + rSet.Put(item); + } + } + else yvalidate(false); + break; + } + case EE_CHAR_STRIKEOUT: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(STRIKEOUT_NONE <= value && value <= STRIKEOUT_X); + SvxCrossedOutItem const item{FontStrikeout(value), nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_ITALIC: + case EE_CHAR_ITALIC_CJK: + case EE_CHAR_ITALIC_CTL: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(ITALIC_NONE <= value && value <= ITALIC_DONTKNOW); + SvxPostureItem const item{FontItalic(value), nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_OUTLINE: + { + yvalidate(rValue.tag == Y_JSON_BOOL); + SvxContourItem const item{rValue.value.flag == Y_TRUE, nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_SHADOW: + { + yvalidate(rValue.tag == Y_JSON_BOOL); + SvxShadowedItem const item{rValue.value.flag == Y_TRUE, nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_ESCAPEMENT: + { + yvalidate(rValue.tag == Y_JSON_MAP); + ::std::optional oEsc; + ::std::optional oProp; + for (decltype(rValue.len) i = 0; i < rValue.len; ++i) + { + const char*const pEntry{rValue.value.map[i].key}; + if (strcmp(pEntry, "esc") == 0) + { + int64_t const value{static_cast(YrsReadInt(*rValue.value.map[i].value))}; + yvalidate(SAL_MIN_INT16 <= value && value <= SAL_MAX_INT16); + oEsc.emplace(short(value)); + } + else if (strcmp(pEntry, "prop") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_UINT8); + oProp.emplace(sal_uInt8(value)); + } + else yvalidate(false); + } + if (oEsc && oProp) + { + SvxEscapementItem const item{*oEsc, *oProp, EE_CHAR_ESCAPEMENT}; + rSet.Put(item); + } + else yvalidate(false); + break; + } + case EE_CHAR_PAIRKERNING: + { + yvalidate(rValue.tag == Y_JSON_BOOL); + SvxAutoKernItem const item{rValue.value.flag == Y_TRUE, nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_KERNING: + { + int64_t const value{static_cast(YrsReadInt(rValue))}; + yvalidate(SAL_MIN_INT16 <= value && value <= SAL_MAX_INT16); + SvxKerningItem const item{short(value), nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_WLM: + { + yvalidate(rValue.tag == Y_JSON_BOOL); + SvxWordLineModeItem const item{rValue.value.flag == Y_TRUE, nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_LANGUAGE: + case EE_CHAR_LANGUAGE_CJK: + case EE_CHAR_LANGUAGE_CTL: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16); + SvxLanguageItem const item{LanguageType(value), nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_EMPHASISMARK: + { + auto const value{YrsReadInt(rValue)}; + yvalidate((value & 0x300f) == value); + SvxEmphasisMarkItem const item{FontEmphasisMark(value), TypedWhichId(nWhich)}; + rSet.Put(item); + break; + } + case EE_CHAR_RELIEF: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(::std::underlying_type_t(FontRelief::NONE) <= value && value <= ::std::underlying_type_t(FontRelief::Engraved)); + SvxCharReliefItem const item{FontRelief(value), nWhich}; + rSet.Put(item); + break; + } + case EE_CHAR_CASEMAP: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(::std::underlying_type_t(SvxCaseMap::NotMapped) <= value && value < ::std::underlying_type_t(SvxCaseMap::End)); + SvxCaseMapItem const item{SvxCaseMap(value), nWhich}; + rSet.Put(item); + break; + } + case EE_PARA_WRITINGDIR: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(::std::underlying_type_t(SvxFrameDirection::Horizontal_LR_TB) <= value && value < ::std::underlying_type_t(SvxFrameDirection::Stacked)); + SvxFrameDirectionItem const item{SvxFrameDirection(value), nWhich}; + rSet.Put(item); + break; + } + case EE_PARA_HANGINGPUNCTUATION: + { + yvalidate(rValue.tag == Y_JSON_BOOL); + SvxHangingPunctuationItem const item{rValue.value.flag == Y_TRUE, nWhich}; + rSet.Put(item); + break; + } + case EE_PARA_FORBIDDENRULES: + { + yvalidate(rValue.tag == Y_JSON_BOOL); + SvxForbiddenRuleItem const item{rValue.value.flag == Y_TRUE, nWhich}; + rSet.Put(item); + break; + } + case EE_PARA_ASIANCJKSPACING: + { + yvalidate(rValue.tag == Y_JSON_BOOL); + SvxScriptSpaceItem const item{rValue.value.flag == Y_TRUE, nWhich}; + rSet.Put(item); + break; + } + case EE_PARA_HYPHENATE: + case EE_PARA_HYPHENATE_NO_CAPS: + case EE_PARA_HYPHENATE_NO_LAST_WORD: + case EE_PARA_BULLETSTATE: + { + yvalidate(rValue.tag == Y_JSON_BOOL); + SfxBoolItem const item{nWhich, rValue.value.flag == Y_TRUE}; + rSet.Put(item); + break; + } + case EE_PARA_OUTLLEVEL: + { + int64_t const value{static_cast(YrsReadInt(rValue))}; + yvalidate(-1 <= value && value < SVX_MAX_NUM); + SfxInt16Item const item{nWhich, sal_Int16(value)}; + rSet.Put(item); + break; + } + case EE_PARA_LRSPACE: + { + yvalidate(rValue.tag == Y_JSON_MAP); + ::std::optional oFirstLineIndent; + ::std::optional oFirstLineIndentUnit; + ::std::optional oLeft; + ::std::optional oLeftUnit; + ::std::optional oRight; + ::std::optional oRightUnit; + ::std::optional oAutoFirst; + for (decltype(rValue.len) i = 0; i < rValue.len; ++i) + { + const char*const pEntry{rValue.value.map[i].key}; + if (strcmp(pEntry, "first-line-offset") == 0) + { + yvalidate(rValue.value.map[i].value->tag == Y_JSON_NUM); + oFirstLineIndent.emplace(rValue.value.map[i].value->value.num); + } + else if (strcmp(pEntry, "first-line-offset-unit") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(css::util::MeasureUnit::MM_100TH <= value && value <= css::util::MeasureUnit::FONT_CJK_ADVANCE); + oFirstLineIndentUnit.emplace(sal_Int16(value)); + } + else if (strcmp(pEntry, "left-margin") == 0) + { + yvalidate(rValue.value.map[i].value->tag == Y_JSON_NUM); + oLeft.emplace(rValue.value.map[i].value->value.num); + } + else if (strcmp(pEntry, "left-margin-unit") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(css::util::MeasureUnit::MM_100TH <= value && value <= css::util::MeasureUnit::FONT_CJK_ADVANCE); + oLeftUnit.emplace(sal_Int16(value)); + } + else if (strcmp(pEntry, "right-margin") == 0) + { + yvalidate(rValue.value.map[i].value->tag == Y_JSON_NUM); + oRight.emplace(rValue.value.map[i].value->value.num); + } + else if (strcmp(pEntry, "right-margin-unit") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(css::util::MeasureUnit::MM_100TH <= value && value <= css::util::MeasureUnit::FONT_CJK_ADVANCE); + oRightUnit.emplace(sal_Int16(value)); + } + else if (strcmp(pEntry, "auto-first") == 0) + { + yvalidate(rValue.value.map[i].value->tag == Y_JSON_BOOL); + oAutoFirst.emplace(rValue.value.map[i].value->value.flag == Y_TRUE); + } + else yvalidate(false); + } + if (oFirstLineIndent && oFirstLineIndentUnit && oAutoFirst.has_value() + && oLeft && oLeftUnit && oRight && oRightUnit) + { + SvxLRSpaceItem item{{*oLeft, *oLeftUnit}, {*oRight, *oRightUnit}, {*oFirstLineIndent, *oFirstLineIndentUnit}, EE_PARA_LRSPACE}; + item.SetAutoFirst(*oAutoFirst); + rSet.Put(item); + } + else yvalidate(false); + break; + } + case EE_PARA_ULSPACE: + { + yvalidate(rValue.tag == Y_JSON_MAP); + ::std::optional oUpper; + ::std::optional oLower; + for (decltype(rValue.len) i = 0; i < rValue.len; ++i) + { + const char*const pEntry{rValue.value.map[i].key}; + if (strcmp(pEntry, "upper-margin") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16); + oUpper.emplace(sal_uInt16(value)); + } + else if (strcmp(pEntry, "lower-margin") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16); + oLower.emplace(sal_uInt16(value)); + } + else yvalidate(false); + } + if (oUpper && oLower) + { + SvxULSpaceItem const item{*oUpper, *oLower, EE_PARA_ULSPACE}; + rSet.Put(item); + } + else yvalidate(false); + break; + } + case EE_PARA_SBL: + { + yvalidate(rValue.tag == Y_JSON_MAP); + ::std::optional oLineSpaceFix; + ::std::optional oLineSpaceMin; + ::std::optional oInterLineSpaceProp; + ::std::optional oInterLineSpaceFix; + for (decltype(rValue.len) i = 0; i < rValue.len; ++i) + { + const char*const pEntry{rValue.value.map[i].key}; + if (strcmp(pEntry, "line-space-fix") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16); + oLineSpaceFix.emplace(sal_uInt16(value)); + } + else if (strcmp(pEntry, "line-space-min") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16); + oLineSpaceMin.emplace(sal_uInt16(value)); + } + else if (strcmp(pEntry, "inter-line-space-prop") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16); + oInterLineSpaceProp.emplace(sal_uInt16(value)); + } + else if (strcmp(pEntry, "inter-line-space-fix") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_UINT16); + oInterLineSpaceFix.emplace(sal_Int16(value)); + } + else yvalidate(false); + } + SvxLineSpacingItem item{0, EE_PARA_SBL}; + if (oLineSpaceFix) + { + item.SetLineHeight(*oLineSpaceFix); + item.SetLineSpaceRule(SvxLineSpaceRule::Fix); + rSet.Put(item); + } + else if (oLineSpaceMin) + { + item.SetLineHeight(*oLineSpaceMin); + rSet.Put(item); + } + else if (oInterLineSpaceProp) + { + item.SetPropLineSpace(*oInterLineSpaceProp); + rSet.Put(item); + } + else if (oInterLineSpaceFix) + { + item.SetInterLineSpace(*oInterLineSpaceFix); + rSet.Put(item); + } + else yvalidate(false); + break; + } + case EE_PARA_JUST: + { + auto const value{YrsReadInt(rValue)}; + SvxAdjustItem item{SvxAdjust(), nWhich}; + switch (value) + { + case uint64_t(SvxAdjust::Left): + case uint64_t(SvxAdjust::Right): + case uint64_t(SvxAdjust::Center): + item.SetAdjust(SvxAdjust(value)); + break; + case uint64_t(SvxAdjust::Block): + item.SetAdjust(SvxAdjust(value)); + item.SetLastBlock(SvxAdjust::Left); + break; + case 5: + item.SetAdjust(SvxAdjust::Block); + item.SetLastBlock(SvxAdjust::Center); + break; + case 6: + item.SetAdjust(SvxAdjust::Block); + item.SetLastBlock(SvxAdjust::Block); + item.SetOneWord(SvxAdjust::Left); + break; + case 7: + item.SetAdjust(SvxAdjust::Block); + item.SetLastBlock(SvxAdjust::Block); + item.SetOneWord(SvxAdjust::Block); + break; + default: + abort(); + } + rSet.Put(item); + break; + } + case EE_PARA_TABS: + { + yvalidate(rValue.tag == Y_JSON_MAP); + ::std::optional oDefault; + ::std::optional<::std::vector> oTabs; + for (decltype(rValue.len) i = 0; i < rValue.len; ++i) + { + const char*const pEntry{rValue.value.map[i].key}; + if (strcmp(pEntry, "default-distance") == 0) + { + auto const value{YrsReadInt(*rValue.value.map[i].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_INT32); + oDefault.emplace(sal_Int32(value)); + } + else if (strcmp(pEntry, "tab-stops") == 0) + { + oTabs.emplace(); + yvalidate(rValue.value.map[i].value->tag == Y_JSON_ARR); + YOutput const& rArray{*rValue.value.map[i].value}; + for (decltype(rArray.len) j = 0; j < rArray.len; ++j) + { + YOutput const& rMap{rArray.value.array[j]}; + yvalidate(rMap.tag == Y_JSON_MAP); + ::std::optional oPos; + ::std::optional oAdjust; + ::std::optional oDecimal; + ::std::optional oFill; + for (decltype(rMap.len) k = 0; k < rMap.len; ++k) + { + const char*const pE{rMap.value.map[k].key}; + if (strcmp(pE, "pos") == 0) + { + auto const value{YrsReadInt(*rMap.value.map[k].value)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_INT32); + oPos.emplace(sal_Int32(value)); + } + else if (strcmp(pE, "adjustment") == 0) + { + auto const value{YrsReadInt(*rMap.value.map[k].value)}; + yvalidate(::std::underlying_type_t(SvxTabAdjust::Left) <= value && value < ::std::underlying_type_t(SvxTabAdjust::End)); + oAdjust.emplace(SvxTabAdjust(value)); + } + else if (strcmp(pE, "decimal") == 0) + { + auto const value{YrsReadInt(*rMap.value.map[k].value)}; + yvalidate(/*0 <= value && */value < SAL_MAX_UINT16); + oDecimal.emplace(sal_Unicode(value)); + } + else if (strcmp(pE, "fill") == 0) + { + auto const value{YrsReadInt(*rMap.value.map[k].value)}; + yvalidate(/*0 <= value && */value < SAL_MAX_UINT16); + oFill.emplace(sal_Unicode(value)); + } + else yvalidate(false); + } + if (oPos && oAdjust && oDecimal && oFill) + { + oTabs->emplace_back(*oPos, *oAdjust, *oDecimal, *oFill); + } + else yvalidate(false); + } + } + } + if (oDefault && oTabs) + { + SvxTabStopItem item{nWhich}; + item.SetDefaultDistance(*oDefault); + for (SvxTabStop const& it : *oTabs) + { + item.Insert(it); + } + rSet.Put(item); + } + else yvalidate(false); + break; + } + case EE_PARA_JUST_METHOD: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(::std::underlying_type_t(SvxCellJustifyMethod::Auto) <= value && value <= ::std::underlying_type_t(SvxCellJustifyMethod::Distribute)); + SvxJustifyMethodItem const item{SvxCellJustifyMethod(value), nWhich}; + rSet.Put(item); + break; + } + case EE_PARA_VER_JUST: + { + auto const value{YrsReadInt(rValue)}; + yvalidate(::std::underlying_type_t(SvxCellVerJustify::Standard) <= value && value <= ::std::underlying_type_t(SvxCellVerJustify::Block)); + SvxVerJustifyItem const item{SvxCellVerJustify(value), nWhich}; + rSet.Put(item); + break; + } + + default: + assert(false); + } +} + +// TODO: this could be a lot simpler if feature were a nested json-map like attr +template void +YrsImplInsertFeature(ImpEditEngine & rIEE, + EditPaM const& rPam, + T const*const elements, uint32_t const len, + ::std::function<::std::pair(T const&)> pGetAttr) +{ + ::std::optional oFeature; + ::std::optional oFormat; + ::std::optional oURL; + ::std::optional oRepresentation; + ::std::optional oTargetFrame; + for (::std::remove_const_t k = 0; k < len; ++k) + { + T const& rAttr{elements[k]}; + auto [pKey, rValue]{pGetAttr(rAttr)}; + if (strcmp(pKey, "feature") == 0) + { + yvalidate(rValue.tag == Y_JSON_STR); + oFeature.emplace(OStringToOUString(rValue.value.str, RTL_TEXTENCODING_UTF8)); + } + else if (strcmp(pKey, "url-format") == 0) + { + uint64_t const value{YrsReadInt(rValue)}; + switch (value) + { + case ::std::underlying_type_t(SvxURLFormat::AppDefault): + case ::std::underlying_type_t(SvxURLFormat::Url): + case ::std::underlying_type_t(SvxURLFormat::Repr): + oFormat.emplace(SvxURLFormat(value)); + break; + default: + yvalidate(false); + } + } + else if (strcmp(pKey, "url-url") == 0) + { + yvalidate(rValue.tag == Y_JSON_STR); + oURL.emplace(OStringToOUString(rValue.value.str, RTL_TEXTENCODING_UTF8)); + } + else if (strcmp(pKey, "url-representation") == 0) + { + yvalidate(rValue.tag == Y_JSON_STR); + oRepresentation.emplace(OStringToOUString(rValue.value.str, RTL_TEXTENCODING_UTF8)); + } + else if (strcmp(pKey, "url-targetframe") == 0) + { + yvalidate(rValue.tag == Y_JSON_STR); + oTargetFrame.emplace(OStringToOUString(rValue.value.str, RTL_TEXTENCODING_UTF8)); + } + } + if (oFeature && *oFeature == "tab") + { + EditSelection const sel{rPam}; + rIEE.InsertTab(sel); + } + else if (oFeature && *oFeature == "line") + { + EditSelection const sel{rPam}; + rIEE.InsertLineBreak(sel); + } + else if (oFeature && *oFeature == "url") + { + if (oFormat && oURL && oRepresentation) + { + SvxURLField field{*oURL, *oRepresentation, *oFormat}; + if (oTargetFrame) + { + field.SetTargetFrame(*oTargetFrame); + } + SvxFieldItem const item{field, EE_FEATURE_FIELD}; + EditSelection const sel{rPam}; + rIEE.InsertField(sel, item); + } + else yvalidate(false); + } + else + { + yvalidate(false); + } +} + +} // namespace + +void EditDoc::YrsSetStyle(sal_Int32 const index, ::std::u16string_view const rStyle) +{ + ::YrsSetStyle(m_pYrsSupplier, m_CommentId, *this, index, rStyle); +} + +void EditDoc::YrsSetParaAttr(sal_Int32 const index, SfxPoolItem const& rItem) +{ + ::YrsSetParaAttr(m_pYrsSupplier, m_CommentId, *this, index, rItem); +} + +void EditDoc::YrsWriteEEState() +{ + assert(m_pYrsSupplier); + + YrsWrite const yw{GetYrsWrite(m_pYrsSupplier, m_CommentId)}; + assert(yw.pTxn && yw.pProps && yw.pText); + + YInput const vertical{yinput_bool(mbIsVertical ? Y_TRUE : Y_FALSE)}; + ymap_insert(yw.pProps, yw.pTxn, "is-vertical", &vertical); + YInput const rotation{yinput_long(static_cast(mnRotation))}; + ymap_insert(yw.pProps, yw.pTxn, "rotation", &rotation); + YInput const deftab{yinput_long(static_cast(mnDefTab))}; + ymap_insert(yw.pProps, yw.pTxn, "def-tab", &deftab); + + for (::std::unique_ptr const& rpNode : maContents) + { + auto const start{ytext_len(yw.pText, yw.pTxn)}; + sal_Int32 pos{0}; + for (sal_Int32 i = rpNode->GetString().indexOf(CH_FEATURE, pos); + i != -1; i = rpNode->GetString().indexOf(CH_FEATURE, pos)) + { + if (i != pos) + { + OString const content{OUStringToOString(rpNode->GetString().subView(pos, i - pos), RTL_TEXTENCODING_UTF8)}; + ytext_insert(yw.pText, yw.pTxn, start + pos, content.getStr(), nullptr); + } + EditCharAttrib const*const pAttrib{rpNode->GetCharAttribs().FindFeature(i)}; + assert(pAttrib); + // this will insert the CH_FEATURE too + YrsInsertFeature(m_pYrsSupplier, m_CommentId, *this, GetPos(rpNode.get()), pAttrib); + pos = i+1; + } + if (pos != rpNode->GetString().getLength()) + { + OString const content{OUStringToOString(rpNode->GetString().subView(pos, rpNode->GetString().getLength() - pos), RTL_TEXTENCODING_UTF8)}; + ytext_insert(yw.pText, yw.pTxn, start + pos, content.getStr(), nullptr); + } + for (::std::unique_ptr const& rpAttr : rpNode->GetCharAttribs().GetAttribs()) + { + if (!rpAttr->IsFeature()) + { + YrsInsertAttribImpl(yw, start, rpAttr.get()); + } + } + char const para[]{ CH_PARA, '\0' }; + ContentAttribs const& rParaAttribs{rpNode->GetContentAttribs()}; + OString const styleName{OUStringToOString(rpNode->GetStyleSheet()->GetName(), RTL_TEXTENCODING_UTF8)}; + YInput const style{yinput_string(styleName.getStr())}; + YInput attrArray[]{ style }; + char const*const attrNames[]{ "para-style" }; + YInput const attrs{yinput_json_map(const_cast(attrNames), attrArray, 1)}; + auto const end{ytext_len(yw.pText, yw.pTxn)}; + ytext_insert(yw.pText, yw.pTxn, end, para, &attrs); + for (SfxItemIter it{rParaAttribs.GetItems()}; !it.IsAtEnd(); it.NextItem()) + { + YrsInsertAttribImplImpl(yw, *it.GetCurItem(), end, 1); + } + } +} + +void EditDoc::YrsReadEEState(YTransaction *const pTxn, ImpEditEngine & rIEE) +{ + assert(m_pYrsSupplier); + YrsWrite const yw{GetYrsWrite(m_pYrsSupplier, m_CommentId, pTxn)}; + assert(yw.pTxn && yw.pProps && yw.pText); + + ::std::unique_ptr const pVertical{ymap_get(yw.pProps, yw.pTxn, "is-vertical")}; + yvalidate(!pVertical || pVertical->tag == Y_JSON_BOOL); + if (pVertical && pVertical->tag == Y_JSON_BOOL) + { + SetVertical(pVertical->value.flag == Y_TRUE); + } + ::std::unique_ptr const pRotation{ymap_get(yw.pProps, yw.pTxn, "rotation")}; + yvalidate(pRotation); + { + auto const value{YrsReadInt(*pRotation)}; + switch (value) + { + case int64_t(TextRotation::NONE): + case int64_t(TextRotation::TOPTOBOTTOM): + case int64_t(TextRotation::BOTTOMTOTOP): + SetRotation(TextRotation(value)); + break; + default: + yvalidate(false); + } + } + ::std::unique_ptr const pDefTab{ymap_get(yw.pProps, yw.pTxn, "def-tab")}; + yvalidate(pDefTab); + { + auto const value{YrsReadInt(*pDefTab)}; + yvalidate(/*0 <= value && */value <= SAL_MAX_INT16); + SetDefTab(value); + } + + assert(maContents.size() == 1); +#if 0 + // there is a getImpl().GetEditSelection() too, who knows what else... try to reuse existing node? + rIEE.GetParaPortions().Reset(); + maContents.clear(); + // the last paragraph cannot be removed, as cursors and a11y need it + rIEE.RemoveParagraph(0); // remove pre-existing one from InitDoc() +#endif + uint32_t chunks{0}; + YChunk *const pChunks{ytext_chunks(yw.pText, yw.pTxn, &chunks)}; + sal_Int32 nodes{0}; + ContentNode * pNode{nullptr}; + for (decltype(chunks) i = 0; i < chunks; ++i) + { + decltype(nodes) const nodeStart{nodes}; + sal_Int32 const posStart{pNode ? pNode->Len() : 0}; + + yvalidate(pChunks[i].data.tag == Y_JSON_STR); + OString const str(pChunks[i].data.value.str); + sal_Int32 strStart{0}; + + for (sal_Int32 j = 0; j < str.getLength(); ++j) + { + if (!pNode) + { + //pNode = new ContentNode(GetItemPool()); + //rIEE.InsertContent(pNode, nodes); // does not set DefFont? + pNode = rIEE.ImpFastInsertParagraph(nodes).GetNode(); +// rIEE.GetParaPortions().Insert(nodes, ::std::make_unique(pNode)); +//does not call ParagraphInserted so no a11y event Insert(nodes, ::std::unique_ptr(pNode)); + // calling UpdateSelections() is pointless, it only handles deletes + } + if (str[j] == CH_PARA) + { + if (j != 0) + { + OString const portion{str.copy(strStart, j - strStart)}; + pNode->Append(OStringToOUString(portion, RTL_TEXTENCODING_UTF8)); + } + for (decltype(pChunks[i].fmt_len) k = 0; k < pChunks[i].fmt_len; ++k) + { + if (strcmp(pChunks[i].fmt[k].key, "para-style") == 0) + { + yvalidate(pChunks[i].fmt[k].value->tag == Y_JSON_STR); + OUString const style{OStringToOUString(pChunks[i].fmt[k].value->value.str, RTL_TEXTENCODING_UTF8)}; + SfxStyleSheet *const pStyle{dynamic_cast( + rIEE.GetStyleSheetPool()->Find(style, SfxStyleFamily::Para))}; + if (!pStyle) { abort(); } + pNode->SetStyleSheet(pStyle); + } + } + pNode = nullptr; + ++nodes; + strStart = j+1; + } + else if (str[j] == CH_FEATURE) + { + if (j != strStart) + { + OString const portion{str.copy(strStart, j - strStart)}; + pNode->Append(OStringToOUString(portion, RTL_TEXTENCODING_UTF8)); + } + auto GetAttr = [](YMapEntry const& rEntry) -> ::std::pair + { return {rEntry.key, *rEntry.value}; }; + EditPaM const pam{pNode, pNode->Len()}; + YrsImplInsertFeature(rIEE, pam, pChunks[i].fmt, pChunks[i].fmt_len, GetAttr); + strStart = j+1; + } + } + if (strStart != str.getLength()) + { + OString const portion{str.copy(strStart, str.getLength() - strStart)}; + pNode->Append(OStringToOUString(portion, RTL_TEXTENCODING_UTF8)); + } + assert((pNode == nullptr) == (str[str.getLength()-1] == CH_PARA)); + ContentNode *const pEndNode{pNode ? pNode : maContents[nodes-1].get()}; + EditSelection const sel{EditPaM(maContents[nodeStart].get(), posStart), EditPaM(pEndNode, pEndNode->Len())}; + SfxItemSet set{rIEE.GetEmptyItemSet()}; + for (decltype(pChunks[i].fmt_len) j = 0; j < pChunks[i].fmt_len; ++j) + { + YrsImplInsertAttr(set, nullptr, pChunks[i].fmt[j].key, *pChunks[i].fmt[j].value); + } + if (set.Count()) + { + rIEE.SetAttribs(sel, set); + } + } + ychunks_destroy(pChunks, chunks); + 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) +{ + for (EditView *const pView : rIEE.GetEditViews()) + { + bool bSet{false}; + EditSelection sel{pView->getImpl().GetEditSelection()}; + ContentNode const*const pNode{rDoc.GetObject(node)}; + if (sel.Min().GetNode() == pNode + && pos <= sel.Min().GetIndex()) + { + sel.Min().SetNode(pNewNode); + sel.Min().SetIndex(sel.Min().GetIndex() + delta); + bSet = true; + } + if (sel.Max().GetNode() == pNode + && pos <= sel.Max().GetIndex()) + { + sel.Max().SetNode(pNewNode); + sel.Max().SetIndex(sel.Max().GetIndex() + delta); + bSet = true; + } + if (bSet) + { + pView->getImpl().SetEditSelection(sel); + } + } +} + +// TODO test this +static void YrsAdjustCursorsDel(ImpEditEngine & rIEE, EditDoc & rDoc, + sal_Int32 const startNode, sal_Int32 const startPos, + sal_Int32 const endNode, sal_Int32 const 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) + { + pView->getImpl().SetEditSelection(sel); + } + } +} + +void EditDoc::YrsApplyEEDelta(YTransaction *const /*pTxn*/, YTextEvent const*const pEvent, ImpEditEngine & rIEE) +{ + uint32_t lenC{0}; + YDeltaOut *const pChange{ytext_event_delta(pEvent, &lenC)}; + + sal_Int32 node{0}; + sal_Int32 pos{0}; + + for (decltype(lenC) i = 0; i < lenC; ++i) + { + switch (pChange[i].tag) + { + case Y_EVENT_CHANGE_ADD: + { + decltype(node) const nodeStart{node}; + decltype(pos) const posStart{pos}; + + SfxStyleSheet * pStyle{nullptr}; + SfxItemSet set{rIEE.GetEmptyItemSet()}; + for (decltype(pChange[i].attributes_len) j = 0; j < pChange[i].attributes_len; ++j) + { + YrsImplInsertAttr(set, nullptr, pChange[i].attributes[j].key, pChange[i].attributes[j].value); + if (strcmp(pChange[i].attributes[j].key, "para-style") == 0) + { + yvalidate(pChange[i].attributes[j].value.tag == Y_JSON_STR); + OUString const style{OStringToOUString(pChange[i].attributes[j].value.value.str, RTL_TEXTENCODING_UTF8)}; + pStyle = dynamic_cast( + rIEE.GetStyleSheetPool()->Find(style, SfxStyleFamily::Para)); + if (!pStyle) { abort(); } + } + } + + for (decltype(pChange[i].len) j = 0; j < pChange[i].len; ++j) + { + switch (pChange[i].insert[j].tag) + { + case Y_JSON_STR: + { + OUString const str{OStringToOUString(::std::string_view(pChange[i].insert[j].value.str), RTL_TEXTENCODING_UTF8)}; + if (str.getLength() == 1 && str[0] == CH_FEATURE) + { + auto GetAttr = [](YDeltaAttr const& rEntry) -> ::std::pair + { return {rEntry.key, rEntry.value}; }; + EditPaM const pam{maContents[node].get(), pos}; + YrsImplInsertFeature(rIEE, pam, pChange[i].attributes, pChange[i].attributes_len, GetAttr); + YrsAdjustCursors(rIEE, *this, node, pos, maContents[node].get(), 1); + ++pos; + break; + } + sal_Int32 index{0}; + sal_Int32 iPara{str.indexOf(CH_PARA)}; + if (iPara != -1) + { + if (index != iPara) + { + EditSelection const sel{EditPaM{maContents[node].get(), pos}}; + rIEE.InsertText(sel, str.copy(index, iPara)); + } + EditPaM const newPos{rIEE.SplitContent(node, pos + iPara)}; + rIEE.SetStyleSheet(node, pStyle); + YrsAdjustCursors(rIEE, *this, node, pos, const_cast(newPos.GetNode()), -pos); + index = iPara + 1; + pos = 0; + ++node; + iPara = str.indexOf(CH_PARA, index); + } + while (iPara != -1) + { +// EditSelection const sel{EditPaM{maContents[node].get(), pos}}; +// rIEE.InsertText(sel, str); + rIEE.InsertParagraph(node, str.copy(index, iPara - index)); + rIEE.SetStyleSheet(node, pStyle); + index = iPara + 1; + ++node; + iPara = str.indexOf(CH_PARA, index); + } + assert(iPara == -1); + if (index != str.getLength()) + { + 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; + } + default: + assert(false); + } + } + + EditPaM const start{maContents[nodeStart].get(), posStart}; + EditPaM const end{maContents[node].get(), pos}; + EditSelection const sel{start, end}; + if (set.Count()) + { + rIEE.SetAttribs(sel, set); + } + } + break; + case Y_EVENT_CHANGE_DELETE: + case Y_EVENT_CHANGE_RETAIN: + { + decltype(node) const nodeStart{node}; + decltype(pos) const posStart{pos}; + + // len should be UTF16 via Y_OFFSET_UTF16 + sal_Int32 len{static_cast(pChange[i].len)}; + while (0 < len) + { + yvalidate(o3tl::make_unsigned(node) < maContents.size()); + ContentNode & rNode{*maContents[node]}; + if (pos + len <= rNode.Len()) + { + pos += len; + len = 0; + } + else + { + len -= rNode.Len() - pos + 1; + pos = 0; + ++node; + } + } + ::std::optional oSel; + // setting attribute on node special case - there may be + // EE_PARA_* obviously and possibly also EE_CHAR_* but + // unclear how those would be inserted - and a "para-style" + if (pos == 0 && pChange[i].len == 1 && pChange[i].tag == Y_EVENT_CHANGE_RETAIN) + { + assert(o3tl::make_unsigned(node) <= maContents.size()); + EditPaM const start{maContents[node - 1].get(), 0}; + oSel.emplace(start); + } + else + { + if (pos == 0 && static_cast(node) == maContents.size()) + { // adjust past-the-end positon (formatting change) + --node; + pos = maContents[node]->Len(); + } + assert(o3tl::make_unsigned(node) < maContents.size() && pos <= maContents[node]->Len()); + EditPaM const start{maContents[nodeStart].get(), posStart}; + EditPaM const end{maContents[node].get(), pos}; + oSel.emplace(start, end); + } + if (pChange[i].tag == Y_EVENT_CHANGE_DELETE) + { + YrsAdjustCursorsDel(rIEE, *this, nodeStart, posStart, node, pos); + rIEE.DeleteSelected(*oSel); + node = nodeStart; + pos = posStart; + } + else if (0 < pChange[i].attributes_len) + { + assert(pChange[i].tag == Y_EVENT_CHANGE_RETAIN); + SfxItemSet set{rIEE.GetEmptyItemSet()}; + ::std::vector removed; + for (decltype(pChange[i].attributes_len) j = 0; j < pChange[i].attributes_len; ++j) + { + if (pos == 0 && pChange[i].len == 1 + && strcmp(pChange[i].attributes[j].key, "para-style") == 0) + { + yvalidate(pChange[i].attributes[j].value.tag == Y_JSON_STR); + OUString const style{OStringToOUString(pChange[i].attributes[j].value.value.str, RTL_TEXTENCODING_UTF8)}; + SfxStyleSheet *const pStyle{dynamic_cast( + rIEE.GetStyleSheetPool()->Find(style, SfxStyleFamily::Para))}; + if (!pStyle) { abort(); } + rIEE.SetStyleSheet(node - 1, pStyle); + } + else + { + YrsImplInsertAttr(set, &removed, pChange[i].attributes[j].key, pChange[i].attributes[j].value); + } + } + if (set.Count()) + { + rIEE.SetAttribs(*oSel, set); + } + for (auto const nWhich : removed) + { + rIEE.RemoveCharAttribs(*oSel, EERemoveParaAttribsMode::RemoveAll, nWhich); + } + } + } + break; + default: + assert(false); + } + } + + ytext_delta_destroy(pChange, lenC); +} + +void EditDoc::SetYrsCommentId(IYrsTransactionSupplier *const pYrsSupplier, OString const& rId) +{ + assert(!m_pYrsSupplier); + m_pYrsSupplier = pYrsSupplier; + m_CommentId = rId; +} + +OString EditDoc::GetYrsCommentId() const +{ + return m_CommentId; +} + +void EditView::SetYrsCommentId(IYrsTransactionSupplier *const pYrsSupplier, OString const& rId) +{ + getEditEngine().GetEditDoc().SetYrsCommentId(pYrsSupplier, rId); +} + +OString EditView::GetYrsCommentId() const +{ + return getEditEngine().GetEditDoc().GetYrsCommentId(); +} + +void EditView::YrsWriteEEState() +{ + return getEditEngine().GetEditDoc().YrsWriteEEState(); +} + +void EditView::YrsReadEEState(YTransaction *const pTxn) +{ + return getEditEngine().GetEditDoc().YrsReadEEState(pTxn, getImpEditEngine()); +} + +void EditView::YrsApplyEEDelta(YTransaction *const pTxn, YTextEvent const*const pEvent) +{ + return getEditEngine().GetEditDoc().YrsApplyEEDelta(pTxn, pEvent, getImpEditEngine()); +} +#endif + EditDoc::EditDoc( SfxItemPool* pPool ) : mnLastCache(0), mpItemPool(pPool ? pPool : new EditEngineItemPool()), @@ -724,14 +3029,46 @@ EditDoc::EditDoc( SfxItemPool* pPool ) : mbModified(false), mbDisableAttributeExpanding(false) { +#if defined(YRS) + SAL_DEBUG("YRS +EditDoc"); +#endif // Don't create an empty node, Clear() will be called in EditEngine-CTOR }; EditDoc::~EditDoc() { +#if defined(YRS) + SAL_DEBUG("YRS -EditDoc"); +#endif maContents.clear(); } +// not sure which of the members make sense to sync - if its only a cache for +// some value elsewhere in the model then probably not? +void EditDoc::SetVertical(bool const bVertical) +{ + mbIsVertical = bVertical; +#if defined(YRS) + YrsSetVertical(m_pYrsSupplier, m_CommentId, mbIsVertical); +#endif +} + +void EditDoc::SetRotation(TextRotation const nRotation) +{ + mnRotation = nRotation; +#if defined(YRS) + YrsSetRotation(m_pYrsSupplier, m_CommentId, mnRotation); +#endif +} + +void EditDoc::SetDefTab(sal_uInt16 const nTab) +{ + mnDefTab = nTab ? nTab : DEFTAB; +#if defined(YRS) + YrsSetDefTab(m_pYrsSupplier, m_CommentId, mnDefTab); +#endif +} + void CreateFont( SvxFont& rFont, const SfxItemSet& rSet, bool bSearchInParent, SvtScriptType nScriptType ) { vcl::Font aPrevFont( rFont ); @@ -813,6 +3150,7 @@ void CreateFont( SvxFont& rFont, const SfxItemSet& rSet, bool bSearchInParent, S void EditDoc::CreateDefFont( bool bUseStyles ) { SfxItemSet aTmpSet(SfxItemSet::makeFixedSfxItemSet(GetItemPool())); + // maDefFont depends only on items and flags, no need to sync CreateFont(maDefFont, aTmpSet); maDefFont.SetVertical( IsEffectivelyVertical() ); maDefFont.SetOrientation( Degree10(IsEffectivelyVertical() ? (IsTopToBottom() ? 2700 : 900) : 0) ); @@ -865,6 +3203,9 @@ void EditDoc::Insert(sal_Int32 nPos, std::unique_ptr pNode) return; } maContents.insert(maContents.begin()+nPos, std::move(pNode)); +#if defined(YRS) + YrsAddPara(m_pYrsSupplier, m_CommentId, *this, nPos); +#endif } void EditDoc::Remove(sal_Int32 nPos) @@ -874,6 +3215,9 @@ void EditDoc::Remove(sal_Int32 nPos) SAL_WARN( "editeng", "EditDoc::Remove - out of bounds pos " << nPos); return; } +#if defined(YRS) + YrsRemovePara(m_pYrsSupplier, m_CommentId, *this, nPos); +#endif maContents.erase(maContents.begin() + nPos); } @@ -885,6 +3229,9 @@ std::unique_ptr EditDoc::Release(sal_Int32 nPos) return nullptr; } +#if defined(YRS) + YrsRemovePara(m_pYrsSupplier, m_CommentId, *this, nPos); +#endif std::unique_ptr pNode = std::move(maContents[nPos]); maContents.erase(maContents.begin() + nPos); return pNode; @@ -971,6 +3318,11 @@ EditPaM EditDoc::Clear() { maContents.clear(); +#if defined(YRS) + // Insert will call YrsAddPara() + YrsClear(m_pYrsSupplier, m_CommentId); +#endif + ContentNode* pNode = new ContentNode(GetItemPool()); Insert(0, std::unique_ptr(pNode)); @@ -1014,6 +3366,11 @@ EditPaM EditDoc::RemoveText() maContents.clear(); +#if defined(YRS) + // Insert will call YrsAddPara() + YrsClear(m_pYrsSupplier, m_CommentId); +#endif + ContentNode* pNode = new ContentNode(GetItemPool()); Insert(0, std::unique_ptr(pNode)); @@ -1040,6 +3397,10 @@ EditPaM EditDoc::InsertText( EditPaM aPaM, const OUString& rStr ) SetModified( true ); +#if defined(YRS) + YrsInsertText(m_pYrsSupplier, m_CommentId, *this, GetPos(aPaM.GetNode()), aPaM.GetIndex() - rStr.getLength(), rStr); +#endif + return aPaM; } @@ -1077,10 +3438,22 @@ EditPaM EditDoc::InsertParaBreak( EditPaM aPaM, bool bKeepEndingAttribs ) // Character attributes may need to be copied or trimmed: pNode->CopyAndCutAttribs( aPaM.GetNode(), GetItemPool(), bKeepEndingAttribs ); +#if defined(YRS) + { + // skip the YrsAddPara in Insert + YrsReplayGuard const g{m_pYrsSupplier}; +#endif Insert(nPos+1, std::unique_ptr(pNode)); +#if defined(YRS) + } +#endif SetModified(true); +#if defined(YRS) + YrsInsertParaBreak(m_pYrsSupplier, m_CommentId, *this, nPos, aPaM.GetIndex()); +#endif + aPaM.SetNode( pNode ); aPaM.SetIndex( 0 ); return aPaM; @@ -1100,6 +3473,10 @@ EditPaM EditDoc::InsertFeature( EditPaM aPaM, const SfxPoolItem& rItem ) SetModified( true ); +#if defined(YRS) + YrsInsertFeature(m_pYrsSupplier, m_CommentId, *this, GetPos(aPaM.GetNode()), pAttrib); +#endif + aPaM.SetIndex( aPaM.GetIndex() + 1 ); return aPaM; } @@ -1115,10 +3492,23 @@ EditPaM EditDoc::ConnectParagraphs( ContentNode* pLeft, ContentNode* pRight ) // the one to the right disappears. sal_Int32 nRight = GetPos( pRight ); +#if defined(YRS) + { + // skip the YrsRemovePara in Remove, the node even still has the text... + YrsReplayGuard const g{m_pYrsSupplier}; +#endif Remove( nRight ); +#if defined(YRS) + } +#endif SetModified(true); +#if defined(YRS) + assert(nRight != 0); + YrsConnectPara(m_pYrsSupplier, m_CommentId, *this, nRight - 1, aPaM.GetIndex()); +#endif + return aPaM; } @@ -1129,6 +3519,10 @@ void EditDoc::RemoveChars( EditPaM aPaM, sal_Int32 nChars ) aPaM.GetNode()->CollapseAttribs( aPaM.GetIndex(), nChars ); SetModified( true ); + +#if defined(YRS) + YrsRemoveChars(m_pYrsSupplier, m_CommentId, *this, GetPos(aPaM.GetNode()), aPaM.GetIndex(), nChars); +#endif } void EditDoc::InsertAttribInSelection( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEnd, const SfxPoolItem& rPoolItem ) @@ -1162,11 +3556,24 @@ void EditDoc::InsertAttribInSelection( ContentNode* pNode, sal_Int32 nStart, sal // Will become a large Attribute. pEndingAttrib->GetEnd() = pStartingAttrib->GetEnd(); pNode->GetCharAttribs().Remove(pStartingAttrib); +#if defined(YRS) + YrsInsertAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pEndingAttrib); +#endif } else if ( pStartingAttrib && ( *(pStartingAttrib->GetItem()) == rPoolItem ) ) + { pStartingAttrib->GetStart() = nStart; +#if defined(YRS) + YrsInsertAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pStartingAttrib); +#endif + } else if ( pEndingAttrib && ( *(pEndingAttrib->GetItem()) == rPoolItem ) ) + { pEndingAttrib->GetEnd() = nEnd; +#if defined(YRS) + YrsInsertAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pEndingAttrib); +#endif + } else InsertAttrib( rPoolItem, pNode, nStart, nEnd ); @@ -1220,6 +3627,9 @@ bool EditDoc::RemoveAttribs( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEn if ( pAttr->GetEnd() > nEnd ) { bNeedsSorting = true; +#if defined(YRS) + YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), pAttr->GetStart(), nEnd); +#endif pAttr->GetStart() = nEnd; // then it starts after this rpStarting = pAttr; if ( nWhich ) @@ -1238,6 +3648,9 @@ bool EditDoc::RemoveAttribs( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEn bChanged = true; if ( ( pAttr->GetStart() < nStart ) && !pAttr->IsFeature() ) { +#if defined(YRS) + YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), nStart, pAttr->GetEnd()); +#endif pAttr->GetEnd() = nStart; // then it ends here rpEnding = pAttr; } @@ -1255,6 +3668,9 @@ bool EditDoc::RemoveAttribs( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEn { bNeedsSorting = true; pAttr->GetStart() = nEnd; +#if defined(YRS) + YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), nStart, nEnd); +#endif rpStarting = pAttr; if ( nWhich ) break; // There can be further attributes! @@ -1262,6 +3678,9 @@ bool EditDoc::RemoveAttribs( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEn else if ( pAttr->GetEnd() == nEnd ) { pAttr->GetEnd() = nStart; +#if defined(YRS) + YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), nStart, nEnd); +#endif rpEnding = pAttr; if ( nWhich ) break; // There can be further attributes! @@ -1272,7 +3691,15 @@ bool EditDoc::RemoveAttribs( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEn sal_Int32 nOldEnd = pAttr->GetEnd(); pAttr->GetEnd() = nStart; rpEnding = pAttr; +#if defined(YRS) + YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), nStart, nEnd); + { + YrsReplayGuard const g{m_pYrsSupplier}; +#endif InsertAttrib( *pAttr->GetItem(), pNode, nEnd, nOldEnd ); +#if defined(YRS) + } +#endif if ( nWhich ) break; // There can be further attributes! } @@ -1280,6 +3707,9 @@ bool EditDoc::RemoveAttribs( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEn } if ( bRemoveAttrib ) { +#if defined(YRS) + YrsRemoveAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttr->Which(), pAttr->GetStart(), pAttr->GetEnd()); +#endif DBG_ASSERT( ( pAttr != rpStarting ) && ( pAttr != rpEnding ), "Delete and retain the same attribute?" ); DBG_ASSERT( !pAttr->IsFeature(), "RemoveAttribs: Remove a feature?!" ); rAttribs.erase(rAttribs.begin()+nAttr); @@ -1315,6 +3745,10 @@ void EditDoc::InsertAttrib( const SfxPoolItem& rPoolItem, ContentNode* pNode, sa pNode->GetCharAttribs().InsertAttrib( pAttrib ); SetModified( true ); + +#if defined(YRS) + YrsInsertAttrib(m_pYrsSupplier, m_CommentId, *this, GetPos(pNode), pAttrib); +#endif } void EditDoc::InsertAttrib( ContentNode* pNode, sal_Int32 nStart, sal_Int32 nEnd, const SfxPoolItem& rPoolItem ) diff --git a/editeng/source/editeng/impedit.hxx b/editeng/source/editeng/impedit.hxx index f725ffbb170b..de80aa861af7 100644 --- a/editeng/source/editeng/impedit.hxx +++ b/editeng/source/editeng/impedit.hxx @@ -524,6 +524,9 @@ public: class ImpEditEngine : public SfxListener, public svl::StyleSheetUser { friend class EditEngine; +#if defined(YRS) + friend class EditDoc; +#endif typedef EditEngine::ViewsType ViewsType; diff --git a/editeng/source/editeng/impedit5.cxx b/editeng/source/editeng/impedit5.cxx index cbbd830d23e4..31683f08825d 100644 --- a/editeng/source/editeng/impedit5.cxx +++ b/editeng/source/editeng/impedit5.cxx @@ -90,6 +90,9 @@ void ImpEditEngine::SetStyleSheet( sal_Int32 nPara, SfxStyleSheet* pStyle ) if ( pCurStyle ) EndListening( *pCurStyle ); pNode->SetStyleSheet( pStyle, maStatus.UseCharAttribs() ); +#if defined(YRS) + maEditDoc.YrsSetStyle(nPara, pStyle ? pStyle->GetName() : OUString()); +#endif if ( pStyle ) StartListening(*pStyle, DuplicateHandling::Allow); @@ -550,6 +553,9 @@ void ImpEditEngine::SetAttribs( EditSelection aSel, const SfxItemSet& rSet, SetA { pNode->GetContentAttribs().GetItems().Put( rItem ); bParaAttribFound = true; +#if defined(YRS) + maEditDoc.YrsSetParaAttr(nNode, rItem); +#endif } else { diff --git a/include/editeng/editview.hxx b/include/editeng/editview.hxx index 1ac391151a02..750848eaee91 100644 --- a/include/editeng/editview.hxx +++ b/include/editeng/editview.hxx @@ -37,6 +37,12 @@ #include +#if defined(YRS) +class IYrsTransactionSupplier; +typedef struct TransactionInner YTransaction; +typedef struct YTextEvent YTextEvent; +#endif + class EditTextObject; class EditEngine; class ImpEditEngine; @@ -403,6 +409,14 @@ public: /// To inform editeng that negated x document coordinates are in use. void SetNegativeX(bool bSet); bool IsNegativeX() const; + +#if defined(YRS) + void SetYrsCommentId(IYrsTransactionSupplier *, OString const& rId); + void YrsWriteEEState(); + void YrsReadEEState(YTransaction *); + void YrsApplyEEDelta(YTransaction *, YTextEvent const* pEvent); + OString GetYrsCommentId() const; +#endif }; #endif // INCLUDED_EDITENG_EDITVIEW_HXX diff --git a/include/editeng/yrs.hxx b/include/editeng/yrs.hxx new file mode 100644 index 000000000000..4842d47540c4 --- /dev/null +++ b/include/editeng/yrs.hxx @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * 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/. + */ + +#pragma once + +extern "C" { +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wextern-c-compat" +#endif +#include +#if defined(__clang__) +#pragma clang diagnostic pop +#endif +} + +#include + +// check input is valid values to find encoding bugs early +#define yvalidate(cond) \ + if (!(cond)) \ + { \ + std::abort(); \ + } + +struct YOutputDeleter +{ + void operator()(YOutput* const p) const { youtput_destroy(p); } +}; + +class IYrsTransactionSupplier +{ +public: + enum class Mode + { + Edit, + Replay + }; + +protected: + Mode m_Mode{ Mode::Edit }; + +public: + IYrsTransactionSupplier() = default; + virtual ~IYrsTransactionSupplier() = default; + + Mode SetMode(Mode const mode) + { + Mode ret = mode; + std::swap(ret, m_Mode); + return ret; + } + + virtual YDoc* GetYDoc() = 0; + virtual Branch* GetCommentMap() = 0; + virtual Branch* GetCursorMap() = 0; + virtual YTransaction* GetReadTransaction() = 0; + virtual YTransaction* GetWriteTransaction() = 0; + virtual bool CommitTransaction(bool isForce = false) = 0; + virtual OString GenNewCommentId() = 0; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/sfx2/Library_sfx.mk b/sfx2/Library_sfx.mk index 66e51be7f87e..f1b57b85f9ef 100644 --- a/sfx2/Library_sfx.mk +++ b/sfx2/Library_sfx.mk @@ -44,6 +44,10 @@ $(eval $(call gb_Library_set_include,sfx,\ $(eval $(call gb_Library_add_defs,sfx,-DSFX2_DLLIMPLEMENTATION)) +ifneq ($(WITH_YRS),) +$(eval $(call gb_Library_add_defs,sfx,-DYRS)) +endif + $(eval $(call gb_Library_use_libraries,sfx,\ basegfx \ comphelper \ diff --git a/sfx2/source/view/frmload.cxx b/sfx2/source/view/frmload.cxx index 39de317cb4fd..80615abe165f 100644 --- a/sfx2/source/view/frmload.cxx +++ b/sfx2/source/view/frmload.cxx @@ -46,6 +46,10 @@ #include #include #include +#if defined(YRS) +#include +#include +#endif #include #include @@ -627,6 +631,45 @@ sal_Bool SAL_CALL SfxFrameLoader_Impl::load( const Sequence< PropertyValue >& rA Reference< XModel2 > xModel = aDescriptor.getOrDefault( u"Model"_ustr, Reference< XModel2 >() ); const bool bExternalModel = xModel.is(); +#if defined(YRS) + uno::Reference xConnection; + if (!xModel.is() && aDescriptor.getOrDefault(u"URL"_ustr, OUString()) == "private:factory/swriter" && !getenv("YRSACCEPT")) + { + SAL_DEBUG("YRS connect sfx2"); + + // must read this SYNC + auto const conn = u"pipe,name=ytest"_ustr; + auto const xConnector = css::connection::Connector::create(m_aContext); + xConnection = xConnector->connect(conn); + uno::Sequence buf; + if (xConnection->read(buf, 4) != 4) + { + abort(); + } + sal_Int32 const size{static_cast(buf[0]) + | static_cast(buf[1]) << 8 + | static_cast(buf[2]) << 16 + | static_cast(buf[3]) << 24}; + if (size != 0) + { + SAL_DEBUG("YRS connect reading file of size " << size); + uno::Sequence buff(size); + if (xConnection->read(buff, size) != size) + { + abort(); + } + uno::Reference const xInStream{ + io::SequenceInputStream::createStreamFromSequence(m_aContext, buff)}; + assert(xInStream.is()); + + aDescriptor.put(u"URL"_ustr, u"private:stream"_ustr); + aDescriptor.put(u"InputStream"_ustr, uno::Any(xInStream)); + } + aDescriptor.put(u"ReadOnly"_ustr, uno::Any(true)); + aDescriptor.put(u"YrsConnect"_ustr, uno::Any(xConnection)); + } +#endif + // check for factory URLs to create a new doc, instead of loading one const OUString sURL = aDescriptor.getOrDefault( u"URL"_ustr, OUString() ); const bool bIsFactoryURL = sURL.startsWith( "private:factory/" ); diff --git a/sw/Library_sw.mk b/sw/Library_sw.mk index fe08797c4830..b007b90de126 100644 --- a/sw/Library_sw.mk +++ b/sw/Library_sw.mk @@ -94,6 +94,7 @@ $(eval $(call gb_Library_use_externals,sw,\ icuuc \ icu_headers \ libxml2 \ + yrs \ )) ifneq ($(ENABLE_WASM_STRIP_ACCESSIBILITY),TRUE) diff --git a/sw/inc/IDocumentState.hxx b/sw/inc/IDocumentState.hxx index 6653c4a04b07..bfacc071df9f 100644 --- a/sw/inc/IDocumentState.hxx +++ b/sw/inc/IDocumentState.hxx @@ -19,6 +19,14 @@ #pragma once +#if defined(YRS) +#include +#include +#include +struct SwPosition; +class SwPostItField; +#endif + /** Get information about the current document state */ class IDocumentState @@ -47,6 +55,21 @@ public: virtual bool IsEnableSetModified() const = 0; virtual void SetEnableSetModified(bool bEnableSetModified) = 0; +#if defined(YRS) + 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 YrsNotifySetResolved(OString const& rCommentId, SwPostItField const& rField) = 0; + virtual void YrsAddCommentImpl(SwPosition const& rPos, OString const& rCommentId) = 0; + virtual void YrsAddComment(SwPosition const& rPos, ::std::optional oAnchorStart, + SwPostItField const& rField, bool isInsert) + = 0; + virtual void YrsRemoveCommentImpl(rtl::OString const& rCommentId) = 0; + virtual void YrsRemoveComment(SwPosition const& rPos, rtl::OString const& rCommentId) = 0; +#endif + protected: virtual ~IDocumentState(){}; }; diff --git a/sw/inc/PostItMgr.hxx b/sw/inc/PostItMgr.hxx index 3ee4ed00eec6..bcd44a7e9dab 100644 --- a/sw/inc/PostItMgr.hxx +++ b/sw/inc/PostItMgr.hxx @@ -131,6 +131,9 @@ class SAL_DLLPUBLIC_RTTI SwPostItMgr final : public SfxListener, SwAnnotationItem* InsertItem( SfxBroadcaster* pItem, bool bCheckExistence, bool bFocus); void RemoveItem( SfxBroadcaster* pBroadcast ); +#if defined(YRS) + public: +#endif VclPtr GetOrCreateAnnotationWindow(SwAnnotationItem& rItem, bool& rCreated); public: diff --git a/sw/source/core/doc/DocumentStateManager.cxx b/sw/source/core/doc/DocumentStateManager.cxx index bf965d54ba4d..da9e2e74ccd1 100644 --- a/sw/source/core/doc/DocumentStateManager.cxx +++ b/sw/source/core/doc/DocumentStateManager.cxx @@ -23,9 +23,1069 @@ #include #include +#if defined(YRS) +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#include + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#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 YrsMakeYDoc() +{ + YOptions o{yoptions()}; + o.encoding = Y_OFFSET_UTF16; + YDoc *const pYDoc{ydoc_new_with_options(o)}; + return ::std::unique_ptr{pYDoc}; +} + +} // namespace + +class YrsTransactionSupplier : public IYrsTransactionSupplier +{ +private: + friend class DocumentStateManager; + ::std::unique_ptr m_pYDoc; + Branch * m_pComments; + Branch * m_pCursors; + YTransaction * m_pCurrentReadTransaction { nullptr }; + YTransaction * m_pCurrentWriteTransaction { nullptr }; + int m_nLocalComments { 0 }; + ::std::map> 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 m_xConnection; + DocumentStateManager * m_pDSM; + +public: + YrsThread(uno::Reference const& xConnection, + DocumentStateManager & rDSM) + : ::salhelper::Thread("yrs reader") + , m_xConnection(xConnection) + , m_pDSM(&rDSM) + { + } + + void execute() override + { + uno::Sequence buf; + while (m_xConnection->read(buf, 4) == 4) + { + sal_Int32 const size{static_cast(buf[0]) + | static_cast(buf[1]) << 8 + | static_cast(buf[2]) << 16 + | static_cast(buf[3]) << 24}; + if (size == 0) + { + SAL_DEBUG("YRS 0"); + break; + } + SAL_DEBUG("YRS receive " << size); + ::std::unique_ptr> pBuf{new uno::Sequence(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(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> posUpdates; + ::std::vector<::std::tuple> startUpdates; + ::std::map<::std::pair, std::pair> 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 const pPos{yarray_get(pArray, pTxn, 0)}; + yvalidate(pPos->tag == Y_ARRAY); + ::std::unique_ptr const pNode{yarray_get(pPos->value.y_type, pTxn, 0)}; + yvalidate(pNode->tag == Y_JSON_INT); + ::std::unique_ptr 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(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 const pPos{yarray_get(pArray, pTxn, 0)}; + yvalidate(pPos->tag == Y_ARRAY); + ::std::unique_ptr const pProps{yarray_get(pArray, pTxn, 1)}; + yvalidate(pProps->tag == Y_MAP); + ::std::unique_ptr 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 oStartPos; + if (yarray_len(pPos->value.y_type) == 4) + { + ::std::unique_ptr const pStartNode{ + yarray_get(pPos->value.y_type, pTxn, 2)}; + ::std::unique_ptr 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(content)); + } + + OUString author; + { + ::std::unique_ptr 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 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 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 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().GetPostItId())> oParentId; + { + ::std::unique_ptr 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(it.first.second)}; + SAL_DEBUG("YRS " << anchorPos); + SwWrtShell *const pShell{dynamic_cast(rState.rDoc.getIDocumentLayoutAccess().GetCurrentViewShell())}; + SwPostItFieldType* pType = static_cast(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( + 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> const pBuf{static_cast*>(pVoid)}; + uint32_t const length{pBuf->size()}; + assert(length != 0); + switch ((*pBuf)[0]) + { + case ::std::underlying_type_t(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 buf(5 + len); + sal_Int8 * it{buf.getArray()}; + writeLength(it, len+1); + *it = ::std::underlying_type_t(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::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(pBuf->begin()) + 1, length - 1, &len); + uno::Sequence buf(5 + len); + sal_Int8 * it{buf.getArray()}; + writeLength(it, len+1); + *it = ::std::underlying_type_t(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::SendStateDiff): + { + SAL_DEBUG("apply update: " << yupdate_debug_v1(reinterpret_cast(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(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 const pComment{ymap_get(m_pYrsSupplier->m_pComments, pTxn, rCommentId.getStr())}; + assert(pComment); + ::std::unique_ptr 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 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 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 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 const pComment{ + ymap_get(m_pYrsSupplier->m_pComments, pTxn, it.first.getStr())}; + assert(pComment); + ::std::unique_ptr const pPos{yarray_get(pComment->value.y_type, pTxn, 0)}; + assert(pPos); + ::std::unique_ptr const pPosN{yarray_get(pPos->value.y_type, pTxn, 0)}; + ::std::unique_ptr 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( + 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 const pComment{ + ymap_get(m_pYrsSupplier->m_pComments, pTxn, it.first.getStr())}; + assert(pComment); + ::std::unique_ptr const pPos{yarray_get(pComment->value.y_type, pTxn, 0)}; + assert(pPos); + ::std::unique_ptr const pPosN{yarray_get(pPos->value.y_type, pTxn, 2)}; + ::std::unique_ptr 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(propsNames), propsArray, parentId.getLength() ? 5 : 4)}; + YInput const text{yinput_ytext(const_cast(""))}; + 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 const pComment{ + ymap_get(m_pYrsSupplier->m_pComments, pTxn, it.first.getStr())}; + assert(pComment); + ::std::unique_ptr const pPos{yarray_get(pComment->value.y_type, pTxn, 0)}; + assert(pPos); + ::std::unique_ptr const pPosN{yarray_get(pPos->value.y_type, pTxn, 0)}; + ::std::unique_ptr 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( + 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 const pComment{ + ymap_get(m_pYrsSupplier->m_pComments, pTxn, it.first.getStr())}; + assert(pComment); + ::std::unique_ptr const pPos{yarray_get(pComment->value.y_type, pTxn, 0)}; + assert(pPos); + ::std::unique_ptr const pPosN{yarray_get(pPos->value.y_type, pTxn, 2)}; + ::std::unique_ptr 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 xConnection = m_xAcceptor->accept(conn); + uno::Sequence buf(4); + uno::Sequence 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 const xInStream{temp.openStreamNoLock()}; + uno::Reference 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> 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 fields; + rType.GatherFields(fields); + for (SwFormatField const*const pField : fields) + { + SwPostItField const& rField{*dynamic_cast(pField->GetField())}; + SwTextAnnotationField const& rHint{*static_cast(pField->GetTextField())}; + SwPosition const pos{rHint.GetTextNode(), rHint.GetStart()}; + ::std::optional 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 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 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), @@ -34,6 +1094,21 @@ DocumentStateManager::DocumentStateManager( SwDoc& i_rSwdoc ) : 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() @@ -53,8 +1128,36 @@ void DocumentStateManager::SetModified() 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 buf(5); + sal_Int8 * it{buf.getArray()}; + writeLength(it, 1); + *it = ::std::underlying_type_t(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 diff --git a/sw/source/core/inc/DocumentStateManager.hxx b/sw/source/core/inc/DocumentStateManager.hxx index 60c8c4923cdc..0457e7920f70 100644 --- a/sw/source/core/inc/DocumentStateManager.hxx +++ b/sw/source/core/inc/DocumentStateManager.hxx @@ -21,16 +21,29 @@ #include +#if defined(YRS) +#include +#include +#include +struct SwPosition; +#endif + class SwDoc; namespace sw { +#if defined(YRS) +class YrsThread; +class YrsTransactionSupplier; +#endif + class DocumentStateManager final : public IDocumentState { public: DocumentStateManager( SwDoc& i_rSwdoc ); + ~DocumentStateManager(); void SetModified() override; void ResetModified() override; @@ -55,6 +68,26 @@ private: bool mbUpdateExpField; //< TRUE: Update expression fields. bool mbNewDoc ; //< TRUE: new Doc. bool mbInCallModified; //< TRUE: in Set/Reset-Modified link. + +#if defined(YRS) + friend class YrsThread; + ::rtl::Reference m_pYrsReader; + css::uno::Reference m_xAcceptor; + ::std::unique_ptr m_pYrsSupplier; + +public: + void YrsInitAcceptor() override; + void YrsInitConnector(css::uno::Any const& raConnector) override; + IYrsTransactionSupplier::Mode SetYrsMode(IYrsTransactionSupplier::Mode mode) override; + void YrsCommitModified() override; + + void YrsNotifySetResolved(OString const& rCommentId, SwPostItField const& rField) override; + void YrsAddCommentImpl(SwPosition const& rPos, OString const& rCommentId) override; + void YrsAddComment(SwPosition const& rPos, ::std::optional oAnchorStart, + SwPostItField const& rField, bool isInsert) override; + void YrsRemoveCommentImpl(OString const& rCommentId) override; + void YrsRemoveComment(SwPosition const& rPos, OString const& rCommentId) override; +#endif }; } diff --git a/sw/source/uibase/app/docsh.cxx b/sw/source/uibase/app/docsh.cxx index 6fc966e19bdd..697125870804 100644 --- a/sw/source/uibase/app/docsh.cxx +++ b/sw/source/uibase/app/docsh.cxx @@ -1209,6 +1209,22 @@ void SwDocShell::LoadingFinished() // before is called. const bool bHasDocToStayModified( m_xDoc->getIDocumentState().IsModified() && m_xDoc->getIDocumentLinksAdministration().LinksUpdated() ); +#if defined(YRS) +#if 0 + // this doesn't even filter as advertised! + auto const args{GetBaseModel()->getArgs2({u"YrsConnect"_ustr})}; +#endif + // when loading, it is only available from SfxMedium, not SfxBaseModel + for (auto const& rArg : GetMedium()->GetArgs()) + { + if (rArg.Name == "YrsConnect") + { + m_xDoc->getIDocumentState().YrsInitConnector(rArg.Value); + break; + } + } +#endif + FinishedLoading(); SfxViewFrame* pVFrame = SfxViewFrame::GetFirst(this); if(pVFrame) diff --git a/sw/source/uibase/docvw/AnnotationWin.cxx b/sw/source/uibase/docvw/AnnotationWin.cxx index 474c0757eab9..7a40f1fea909 100644 --- a/sw/source/uibase/docvw/AnnotationWin.cxx +++ b/sw/source/uibase/docvw/AnnotationWin.cxx @@ -61,6 +61,9 @@ #include #include #include +#if defined(YRS) +#include +#endif #include #include #include "ShadowOverlayObject.hxx" @@ -206,6 +209,9 @@ void SwAnnotationWin::SetPostItText() // get text from SwPostItField and insert into our textview mpOutliner->SetModifyHdl( Link() ); mpOutliner->EnableUndo( false ); +#if defined(YRS) + auto const mode = mrView.GetDocShell()->GetDoc()->getIDocumentState().SetYrsMode(IYrsTransactionSupplier::Mode::Replay); +#endif if( mpField->GetTextObject() ) mpOutliner->SetText( *mpField->GetTextObject() ); else @@ -214,6 +220,9 @@ void SwAnnotationWin::SetPostItText() GetOutlinerView()->SetStyleSheet(SwResId(STR_POOLCOLL_COMMENT)); GetOutlinerView()->InsertText(sNewText); } +#if defined(YRS) + mrView.GetDocShell()->GetDoc()->getIDocumentState().SetYrsMode(mode); +#endif mpOutliner->ClearModifyFlag(); mpOutliner->GetUndoManager().Clear(); @@ -256,6 +265,11 @@ void SwAnnotationWin::SetResolved(bool resolved) UpdateData(); Invalidate(); collectUIInformation(u"SETRESOLVED"_ustr,get_id()); +#if defined(YRS) + mrView.GetDocShell()->GetDoc()->getIDocumentState().YrsNotifySetResolved( + GetOutlinerView()->GetEditView().GetYrsCommentId(), + *static_cast(mpFormatField->GetField())); +#endif } void SwAnnotationWin::ToggleResolved() diff --git a/sw/source/uibase/docvw/PostItMgr.cxx b/sw/source/uibase/docvw/PostItMgr.cxx index 79ff948e33f9..70c30cf1e6e4 100644 --- a/sw/source/uibase/docvw/PostItMgr.cxx +++ b/sw/source/uibase/docvw/PostItMgr.cxx @@ -45,6 +45,9 @@ #include #include #include +#if defined(YRS) +#include +#endif #include #include #include @@ -517,6 +520,11 @@ void SwPostItMgr::RemoveItem( SfxBroadcaster* pBroadcast ) [&pBroadcast](const std::unique_ptr& pField) { return pField->GetBroadcaster() == pBroadcast; }); if (i != mvPostItFields.end()) { +#if defined(YRS) + mpView->GetDocShell()->GetDoc()->getIDocumentState().YrsRemoveComment( + (*i)->GetAnchorPosition(), + (*i)->mpPostIt->GetOutlinerView()->GetEditView().GetYrsCommentId()); +#endif std::unique_ptr p = std::move(*i); // tdf#120487 remove from list before dispose, so comment window // won't be recreated due to the entry still in the list if focus @@ -900,6 +908,9 @@ VclPtr SwPostItMgr::GetOrCreateAnnotationWindow(SwAnnotationIte pPostIt->InitControls(); pPostIt->SetReadonly(mbReadOnly); rItem.mpPostIt = pPostIt; +#if defined(YRS) + SAL_DEBUG("YRS GetOrCreateAnnotationWindow " << rItem.mpPostIt); +#endif if (mpAnswer) { if (pPostIt->GetPostItField()->GetParentPostItId() != 0) //do we really have another note in front of this one diff --git a/sw/source/uibase/docvw/SidebarTxtControl.cxx b/sw/source/uibase/docvw/SidebarTxtControl.cxx index f2bcddb8ac2e..e59dbe75c2de 100644 --- a/sw/source/uibase/docvw/SidebarTxtControl.cxx +++ b/sw/source/uibase/docvw/SidebarTxtControl.cxx @@ -55,6 +55,9 @@ #include #include #include +#if defined(YRS) +#include +#endif #include #include @@ -199,6 +202,14 @@ OUString SidebarTextControl::RequestHelp(tools::Rectangle& rHelpRect) return OUString(); } +#if defined(YRS) +void SidebarTextControl::EditViewInvalidate(const tools::Rectangle& rRect) +{ + mrDocView.GetDocShell()->GetDoc()->getIDocumentState().YrsCommitModified(); + return WeldEditView::EditViewInvalidate(rRect); +} +#endif + void SidebarTextControl::EditViewScrollStateChange() { mrSidebarWin.SetScrollbar(); diff --git a/sw/source/uibase/docvw/SidebarTxtControl.hxx b/sw/source/uibase/docvw/SidebarTxtControl.hxx index 891d3200380e..bbce1ac833e7 100644 --- a/sw/source/uibase/docvw/SidebarTxtControl.hxx +++ b/sw/source/uibase/docvw/SidebarTxtControl.hxx @@ -57,6 +57,9 @@ class SidebarTextControl : public WeldEditView virtual EditEngine* GetEditEngine() const override; +#if defined(YRS) + virtual void EditViewInvalidate(const tools::Rectangle& rRect) override; +#endif virtual void EditViewScrollStateChange() override; void SetDrawingArea(weld::DrawingArea* pDrawingArea) override; diff --git a/sw/source/uibase/fldui/fldmgr.cxx b/sw/source/uibase/fldui/fldmgr.cxx index 68d67b7f7c20..09e9bffd52c1 100644 --- a/sw/source/uibase/fldui/fldmgr.cxx +++ b/sw/source/uibase/fldui/fldmgr.cxx @@ -75,6 +75,10 @@ #include #include #include +#if defined(YRS) +#include +#include +#endif #include using namespace com::sun::star::uno; @@ -1524,7 +1528,8 @@ bool SwFieldMgr::InsertField( // insert pCurShell->StartAllAction(); - bool const isSuccess = pCurShell->InsertField2(*pField, rData.m_oAnnotationRange ? &*rData.m_oAnnotationRange : nullptr); + ::std::optional oAnchorStart; + bool const isSuccess = pCurShell->InsertField2(*pField, rData.m_oAnnotationRange ? &*rData.m_oAnnotationRange : nullptr, &oAnchorStart); if (isSuccess) { @@ -1566,6 +1571,21 @@ bool SwFieldMgr::InsertField( pField.reset(); pCurShell->EndAllAction(); + +#if defined(YRS) + if (isSuccess) + { + // now the SwAnnotationWin are created + // shell cursor is behind field + SwPosition const pos{pCurShell->GetCursor()->GetPoint()->nContent, -1}; + pCurShell->GetDoc()->getIDocumentState().YrsAddComment( + pos, oAnchorStart, + static_cast(*SwCursorShell::GetTextFieldAtPos(&pos, ::sw::GetTextAttrMode::Default)->GetFormatField().GetField()), + true); + pCurShell->GetDoc()->getIDocumentState().YrsCommitModified(); + } +#endif + return isSuccess; } diff --git a/sw/source/uibase/inc/wrtsh.hxx b/sw/source/uibase/inc/wrtsh.hxx index 86c0f49af1a8..5a760b1eca65 100644 --- a/sw/source/uibase/inc/wrtsh.hxx +++ b/sw/source/uibase/inc/wrtsh.hxx @@ -317,7 +317,10 @@ typedef bool (SwWrtShell::*FNSimpleMove)(); int IntelligentCut(SelectionType nSelectionType, bool bCut = true); // edit - SW_DLLPUBLIC bool InsertField2(SwField const &, SwPaM* pAnnotationRange = nullptr); + bool InsertField2Impl(SwField const &, SwPaM* pAnnotationRange, + ::std::optional *const poAnchorStart); + SW_DLLPUBLIC bool InsertField2(SwField const &, SwPaM* pAnnotationRange = nullptr, + ::std::optional *const poAnchorStart = nullptr); SW_DLLPUBLIC void Insert(const OUString &); // graphic void InsertGraphic( const OUString &rPath, const OUString &rFilter, diff --git a/sw/source/uibase/uiview/view.cxx b/sw/source/uibase/uiview/view.cxx index 1af36d3bdf3a..f817fe575df2 100644 --- a/sw/source/uibase/uiview/view.cxx +++ b/sw/source/uibase/uiview/view.cxx @@ -1027,6 +1027,9 @@ SwView::SwView(SfxViewFrame& _rFrame, SfxViewShell* pOldSh) // Set DocShell m_xGlueDocShell.reset(new SwViewGlueDocShell(*this, rDocSh)); m_pPostItMgr.reset(new SwPostItMgr(this)); +#if defined(YRS) + m_pWrtShell->GetDoc()->getIDocumentState().YrsInitAcceptor(); +#endif // Check and process the DocSize. Via the handler, the shell could not // be found, because the shell is not known in the SFX management diff --git a/sw/source/uibase/uno/unotxdoc.cxx b/sw/source/uibase/uno/unotxdoc.cxx index b3524a9917a0..4ab02ba063e5 100644 --- a/sw/source/uibase/uno/unotxdoc.cxx +++ b/sw/source/uibase/uno/unotxdoc.cxx @@ -523,6 +523,17 @@ Reference< XInterface > SwXTextDocument::getCurrentSelection() sal_Bool SwXTextDocument::attachResource(const OUString& aURL, const Sequence< beans::PropertyValue >& aArgs) { +#if defined(YRS) + // this is for new document + for (auto const& rArg : aArgs) + { + if (rArg.Name == "YrsConnect") + { + m_pDocShell->GetDoc()->getIDocumentState().YrsInitConnector(rArg.Value); + break; + } + } +#endif return SfxBaseModel::attachResource(aURL, aArgs); } diff --git a/sw/source/uibase/wrtsh/wrtsh2.cxx b/sw/source/uibase/wrtsh/wrtsh2.cxx index 2350e7292bde..adf9c21669f1 100644 --- a/sw/source/uibase/wrtsh/wrtsh2.cxx +++ b/sw/source/uibase/wrtsh/wrtsh2.cxx @@ -66,11 +66,18 @@ #include #include -bool SwWrtShell::InsertField2(SwField const& rField, SwPaM* pAnnotationRange) +bool SwWrtShell::InsertField2(SwField const& rField, + SwPaM* pAnnotationRange, ::std::optional *const poAnchorStart) { ResetCursorStack(); if(!CanInsert()) return false; + return InsertField2Impl(rField, pAnnotationRange, poAnchorStart); +} + +bool SwWrtShell::InsertField2Impl(SwField const& rField, + SwPaM* pAnnotationRange, ::std::optional *const poAnchorStart) +{ StartAllAction(); SwRewriter aRewriter; @@ -135,7 +142,11 @@ bool SwWrtShell::InsertField2(SwField const& rField, SwPaM* pAnnotationRange) pAnnotationTextRange->Start()->AdjustContent(-1); } IDocumentMarkAccess* pMarksAccess = GetDoc()->getIDocumentMarkAccess(); - pMarksAccess->makeAnnotationMark( *pAnnotationTextRange, SwMarkName() ); + auto pMark{pMarksAccess->makeAnnotationMark(*pAnnotationTextRange, SwMarkName())}; + if (poAnchorStart) + { + poAnchorStart->emplace(pMark->GetMarkStart()); + } } pAnnotationTextRange.reset(); }