desktop/source/lib/init.cxx | 66 ++++++++++++++------------ sw/CppunitTest_sw_core_text.mk | 1 sw/inc/unotxdoc.hxx | 5 ++ sw/inc/view.hxx | 2 sw/inc/viewopt.hxx | 12 ++++ sw/qa/core/text/data/redline.docx |binary sw/qa/core/text/itrpaint.cxx | 90 ++++++++++++++++++++++++++++++++++++ sw/source/core/text/itrpaint.cxx | 31 ++++++++++++ sw/source/uibase/config/viewopt.cxx | 9 +++ sw/source/uibase/uiview/view.cxx | 11 ++++ sw/source/uibase/uno/loktxdoc.cxx | 29 +++++++++++ 11 files changed, 226 insertions(+), 30 deletions(-)
New commits: commit 358108ee1e8d7cc790cb37c3591c65ea71983f28 Author: Miklos Vajna <[email protected]> AuthorDate: Wed Nov 26 09:39:11 2025 +0100 Commit: Caolán McNamara <[email protected]> CommitDate: Wed Nov 26 17:19:21 2025 +0100 cool#13574 sw lok: introduce a redline render mode view option Redlines (when shown) are decorated with an underline for inserts and with a strikethrough for deletions. There is no way to render just the old or the new version, which would be handy for a 2-up render result (old on the left hand side, new on the right hand side). Now fully hiding inserts or deletions would require not sharing the same layout and would be quite complex to implement, it's possible to simply not render the unwanted content, typically leaving white holes in the render result. The newly introduced SwRedlineRenderMode as a view option does exactly this: keep the default behavior unchanged, but allow not rendering inserts or deletions as a view option. This requires no change to the LOK API: ther was a paint "mode" parameter already (to handle normal slides, slide masters, slide notes in Impress), which can be reused here for Writer's redline needs. Change-Id: I21aee667913b916c030d067bbe8bb03a5f6b3ee2 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/194547 Reviewed-by: Caolán McNamara <[email protected]> Tested-by: Jenkins CollaboraOffice <[email protected]> Code-Style: Caolán McNamara <[email protected]> diff --git a/desktop/source/lib/init.cxx b/desktop/source/lib/init.cxx index d1650492743b..8278b98f550e 100644 --- a/desktop/source/lib/init.cxx +++ b/desktop/source/lib/init.cxx @@ -4501,34 +4501,37 @@ static void doc_paintPartTile(LibreOfficeKitDocument* pThis, SfxViewShell* pCurrentViewShell = SfxViewShell::Current(); const OString sCurrentViewRenderState = pDoc->getViewRenderState(pCurrentViewShell); - if (!isText) + // Check if just switching to another view is enough, that has less side-effects. + // Render state is sometimes empty, don't risk it. + if ((nPart != doc_getPart(pThis) || nMode != pDoc->getEditMode()) && !sCurrentViewRenderState.isEmpty()) { - // Check if just switching to another view is enough, that has less side-effects. - // Render state is sometimes empty, don't risk it. - if ((nPart != doc_getPart(pThis) || nMode != pDoc->getEditMode()) && !sCurrentViewRenderState.isEmpty()) - { - nViewId = getAlternativeViewForPaint(pThis, pDoc, pCurrentViewShell, sCurrentViewRenderState, nPart, nMode); + nViewId = getAlternativeViewForPaint(pThis, pDoc, pCurrentViewShell, sCurrentViewRenderState, nPart, nMode); - if (nViewId == -1) - nViewId = nOrigViewId; // Couldn't find an alternative view. - // else -> We found an alternative view and already switched to that. - } + if (nViewId == -1) + nViewId = nOrigViewId; // Couldn't find an alternative view. + // else -> We found an alternative view and already switched to that. + } - // Disable callbacks while we are painting - after setting the view - if (nViewId != nOrigViewId) - disableViewCallbacks(pDocument, nViewId); - else + // Disable callbacks while we are painting - after setting the view + if (nViewId != nOrigViewId) + disableViewCallbacks(pDocument, nViewId); + else + { + // If we are here, we couldn't find an alternative view. We need to check the part and mode. + if (!isText) { - // If we are here, we couldn't find an alternative view. We need to check the part and mode. nOrigPart = doc_getPart(pThis); if (nPart != nOrigPart) doc_setPartImpl(pThis, nPart, false); - - nOrigEditMode = pDoc->getEditMode(); - if (nOrigEditMode != nMode) - SfxLokHelper::setEditMode(nMode, pDoc); } + nOrigEditMode = pDoc->getEditMode(); + if (nOrigEditMode != nMode) + SfxLokHelper::setEditMode(nMode, pDoc); + } + + if (!isText) + { bPaintTextEdit = (nPart == nOrigPart && nMode == nOrigEditMode); pDoc->setPaintTextEdit(bPaintTextEdit); } @@ -4538,22 +4541,25 @@ static void doc_paintPartTile(LibreOfficeKitDocument* pThis, if (!isText) { pDoc->setPaintTextEdit(true); + } - if (nViewId == nOrigViewId) - { - // We didn't find an alternative view, set the part and mode back to their initial values if needed. - if (nMode != nOrigEditMode) - SfxLokHelper::setEditMode(nOrigEditMode, pDoc); + if (nViewId == nOrigViewId) + { + // We didn't find an alternative view, set the part and mode back to their initial values if needed. + if (nMode != nOrigEditMode) + SfxLokHelper::setEditMode(nOrigEditMode, pDoc); + if (!isText) + { if (nPart != nOrigPart) doc_setPartImpl(pThis, nOrigPart, false); } - else - { - // We found an alternative view and used it. Enable its callbacks again and turn back to our original view. - enableViewCallbacks(pDocument, nViewId); - doc_setView(pThis, nOrigViewId); - } + } + else + { + // We found an alternative view and used it. Enable its callbacks again and turn back to our original view. + enableViewCallbacks(pDocument, nViewId); + doc_setView(pThis, nOrigViewId); } } catch (const std::exception&) diff --git a/sw/CppunitTest_sw_core_text.mk b/sw/CppunitTest_sw_core_text.mk index 8c215dfab4e1..bb6fba294df4 100644 --- a/sw/CppunitTest_sw_core_text.mk +++ b/sw/CppunitTest_sw_core_text.mk @@ -16,6 +16,7 @@ $(eval $(call gb_CppunitTest_use_common_precompiled_header,sw_core_text)) $(eval $(call gb_CppunitTest_add_exception_objects,sw_core_text, \ sw/qa/core/text/frmform \ sw/qa/core/text/itratr \ + sw/qa/core/text/itrpaint \ sw/qa/core/text/itrform2 \ sw/qa/core/text/porlay \ sw/qa/core/text/porrst \ diff --git a/sw/inc/unotxdoc.hxx b/sw/inc/unotxdoc.hxx index 68ce93ff4875..67cd923ece0a 100644 --- a/sw/inc/unotxdoc.hxx +++ b/sw/inc/unotxdoc.hxx @@ -510,6 +510,11 @@ public: /// @see vcl::ITiledRenderable::supportsCommand(). bool supportsCommand(std::u16string_view rCommand) override; + /// @see vcl::ITiledRenderable::getEditMode(). + int getEditMode() override; + /// @see vcl::ITiledRenderable::setEditMode(). + void setEditMode(int nEditMode) override; + void Invalidate(); void Reactivate(SwDocShell* pNewDocShell); SwXDocumentPropertyHelper * GetPropertyHelper (); diff --git a/sw/inc/view.hxx b/sw/inc/view.hxx index 124fe66e29ee..f8fa6d4e2cd5 100644 --- a/sw/inc/view.hxx +++ b/sw/inc/view.hxx @@ -727,6 +727,8 @@ public: virtual tools::Rectangle getLOKVisibleArea() const override; virtual void flushPendingLOKInvalidateTiles() override; virtual std::optional<OString> getLOKPayload(int nType, int nViewId) const override; + /// See SfxViewShell::getEditMode(). + int getEditMode() const override; bool IsHighlightCharDF() const { return m_bIsHighlightCharDF; } bool IsSpotlightParaStyles() const { return m_bIsSpotlightParaStyles; } diff --git a/sw/inc/viewopt.hxx b/sw/inc/viewopt.hxx index feb63a4d03c7..35d39129a01f 100644 --- a/sw/inc/viewopt.hxx +++ b/sw/inc/viewopt.hxx @@ -262,6 +262,14 @@ struct SwViewColors ViewOptFlags m_nAppearanceFlags; }; +/// View option to determine if some of the redlines should be omitted during rendering or not. +enum class SwRedlineRenderMode +{ + Standard, + OmitInserts, + OmitDeletes, +}; + class SwViewOption { SwViewColors m_aColorConfig; @@ -282,6 +290,7 @@ class SwViewOption sal_uInt8 m_nPagePreviewRow; // Page Preview Row/Columns. sal_uInt8 m_nPagePreviewCol; // Page Preview Row/Columns. SwFillMode m_nShadowCursorFillMode; // FillMode for ShadowCursor. + SwRedlineRenderMode m_eRedlineRenderMode; bool m_bReadonly : 1; // Readonly-Doc. bool m_bStarOneSetting : 1; // Prevent from UI automatics (no scrollbars in readonly documents). bool m_bIsPagePreview : 1; // The preview mustn't print field/footnote/... shadings. @@ -864,6 +873,9 @@ public: SwFillMode GetShdwCursorFillMode() const { return m_nShadowCursorFillMode; } void SetShdwCursorFillMode( SwFillMode nMode ) { m_nShadowCursorFillMode = nMode; }; + SwRedlineRenderMode GetRedlineRenderMode() const { return m_eRedlineRenderMode; } + void SetRedlineRenderMode(SwRedlineRenderMode eMode) { m_eRedlineRenderMode = eMode; }; + bool IsShowPlaceHolderFields() const { return m_bShowPlaceHolderFields; } void SetShowPlaceHolderFields(bool bSet) { m_bShowPlaceHolderFields = bSet; } diff --git a/sw/qa/core/text/data/redline.docx b/sw/qa/core/text/data/redline.docx new file mode 100644 index 000000000000..09ab86ba16b6 Binary files /dev/null and b/sw/qa/core/text/data/redline.docx differ diff --git a/sw/qa/core/text/itrpaint.cxx b/sw/qa/core/text/itrpaint.cxx new file mode 100644 index 000000000000..6f08dfc82a89 --- /dev/null +++ b/sw/qa/core/text/itrpaint.cxx @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <swmodeltestbase.hxx> + +#include <memory> + +#include <docsh.hxx> +#include <wrtsh.hxx> +#include <ndtxt.hxx> + +namespace +{ +/// Covers sw/source/core/text/itrpaint.cxx fixes. +class Test : public SwModelTestBase +{ +public: + Test() + : SwModelTestBase(u"/sw/qa/core/text/data/"_ustr) + { + } +}; + +CPPUNIT_TEST_FIXTURE(Test, testRedlineRenderModeOmitInsertDelete) +{ + // Default rendering: + createSwDoc("redline.docx"); + + SwDocShell* pDocShell = getSwDocShell(); + std::shared_ptr<GDIMetaFile> xMetaFile = pDocShell->GetPreviewMetaFile(); + + MetafileXmlDump dumper; + xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile); + OUString aContent = getXPathContent(pXmlDoc, "(//textarray)[1]/text"); + assertXPath(pXmlDoc, "//textarray", 3); + sal_Int32 nIndex1 = getXPath(pXmlDoc, "(//textarray)[1]", "index").toInt32(); + sal_Int32 nLength1 = getXPath(pXmlDoc, "(//textarray)[1]", "length").toInt32(); + CPPUNIT_ASSERT_EQUAL(u"baseline "_ustr, aContent.copy(nIndex1, nLength1)); + sal_Int32 nIndex2 = getXPath(pXmlDoc, "(//textarray)[2]", "index").toInt32(); + sal_Int32 nLength2 = getXPath(pXmlDoc, "(//textarray)[2]", "length").toInt32(); + CPPUNIT_ASSERT_EQUAL(u"oldcontent"_ustr, aContent.copy(nIndex2, nLength2)); + sal_Int32 nIndex3 = getXPath(pXmlDoc, "(//textarray)[3]", "index").toInt32(); + sal_Int32 nLength3 = getXPath(pXmlDoc, "(//textarray)[3]", "length").toInt32(); + CPPUNIT_ASSERT_EQUAL(u"newcontent"_ustr, aContent.copy(nIndex3, nLength3)); + + // Omit inserts: + SwWrtShell* pWrtShell = pDocShell->GetWrtShell(); + SwViewOption aOpt(*pWrtShell->GetViewOptions()); + aOpt.SetRedlineRenderMode(SwRedlineRenderMode::OmitInserts); + pWrtShell->ApplyViewOptions(aOpt); + + xMetaFile = pDocShell->GetPreviewMetaFile(); + + pXmlDoc = dumpAndParse(dumper, *xMetaFile); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 2 + // - Actual : 3 + // i.e. the inserts were not omitted. + assertXPath(pXmlDoc, "//textarray", 2); + nIndex1 = getXPath(pXmlDoc, "(//textarray)[1]", "index").toInt32(); + nLength1 = getXPath(pXmlDoc, "(//textarray)[1]", "length").toInt32(); + CPPUNIT_ASSERT_EQUAL(u"baseline "_ustr, aContent.copy(nIndex1, nLength1)); + nIndex2 = getXPath(pXmlDoc, "(//textarray)[2]", "index").toInt32(); + nLength2 = getXPath(pXmlDoc, "(//textarray)[2]", "length").toInt32(); + CPPUNIT_ASSERT_EQUAL(u"oldcontent"_ustr, aContent.copy(nIndex2, nLength2)); + + // Omit deletes: + aOpt.SetRedlineRenderMode(SwRedlineRenderMode::OmitDeletes); + pWrtShell->ApplyViewOptions(aOpt); + + xMetaFile = pDocShell->GetPreviewMetaFile(); + + pXmlDoc = dumpAndParse(dumper, *xMetaFile); + assertXPath(pXmlDoc, "//textarray", 2); + nIndex1 = getXPath(pXmlDoc, "(//textarray)[1]", "index").toInt32(); + nLength1 = getXPath(pXmlDoc, "(//textarray)[1]", "length").toInt32(); + CPPUNIT_ASSERT_EQUAL(u"baseline "_ustr, aContent.copy(nIndex1, nLength1)); + nIndex2 = getXPath(pXmlDoc, "(//textarray)[2]", "index").toInt32(); + nLength2 = getXPath(pXmlDoc, "(//textarray)[2]", "length").toInt32(); + CPPUNIT_ASSERT_EQUAL(u"newcontent"_ustr, aContent.copy(nIndex2, nLength2)); +} +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itrpaint.cxx b/sw/source/core/text/itrpaint.cxx index 6e9d48a3a8f3..89b8b5ab605c 100644 --- a/sw/source/core/text/itrpaint.cxx +++ b/sw/source/core/text/itrpaint.cxx @@ -42,6 +42,8 @@ #include "pormulti.hxx" #include <doc.hxx> #include <fmturl.hxx> +#include <IDocumentRedlineAccess.hxx> +#include <redline.hxx> // Returns, if we have an underline breaking situation // Adding some more conditions here means you also have to change them @@ -302,6 +304,9 @@ void SwTextPainter::DrawTextLine( const SwRect &rPaint, SwSaveClip &rClip, // Reference portion for the paragraph end portion SwLinePortion* pEndTempl = m_pCurr->GetFirstPortion(); + const SwDoc& rDoc = GetInfo().GetTextFrame()->GetDoc(); + const IDocumentRedlineAccess& rIDRA = rDoc.getIDocumentRedlineAccess(); + const SwRedlineTable& rRedlineTable = rIDRA.GetRedlineTable(); while( pPor ) { bool bSeeked = true; @@ -419,6 +424,32 @@ void SwTextPainter::DrawTextLine( const SwRect &rPaint, SwSaveClip &rClip, roTaggedLabel.emplace(nullptr, nullptr, &aPorInfo, *pOut); } + // See if the redline render mode requires to omit the paint of the text portion. + SwRedlineTable::size_type nRedline = SwRedlineTable::npos; + SwRedlineRenderMode eRedlineRenderMode = SwRedlineRenderMode::Standard; + if (GetRedln() && GetRedln()->IsOn()) + { + nRedline = GetRedln()->GetAct(); + eRedlineRenderMode = GetInfo().GetOpt().GetRedlineRenderMode(); + } + bool bOmitPaint = false; + if (nRedline != SwRedlineTable::npos) + { + const SwRangeRedline* pRedline = rRedlineTable[nRedline]; + RedlineType eType = pRedline->GetType(); + if (eRedlineRenderMode == SwRedlineRenderMode::OmitInserts + && eType == RedlineType::Insert) + { + bOmitPaint = true; + } + else if (eRedlineRenderMode == SwRedlineRenderMode::OmitDeletes + && eType == RedlineType::Delete) + { + bOmitPaint = true; + } + } + + if (!bOmitPaint) { // #i16816# tagged pdf support Por_Info aPorInfo(*pPor, *this, 0); diff --git a/sw/source/uibase/config/viewopt.cxx b/sw/source/uibase/config/viewopt.cxx index e48fd44595b2..09d7b675ab41 100644 --- a/sw/source/uibase/config/viewopt.cxx +++ b/sw/source/uibase/config/viewopt.cxx @@ -146,6 +146,7 @@ bool SwViewOption::IsEqualFlags( const SwViewOption &rOpt ) const && m_bShowPlaceHolderFields == rOpt.m_bShowPlaceHolderFields && m_bIdle == rOpt.m_bIdle && m_nDefaultAnchor == rOpt.m_nDefaultAnchor + && m_eRedlineRenderMode == rOpt.m_eRedlineRenderMode #ifdef DBG_UTIL // correspond to the statements in ui/config/cfgvw.src && m_bTest1 == rOpt.IsTest1() @@ -230,6 +231,7 @@ SwViewOption::SwViewOption() : m_nPagePreviewRow( 1 ), m_nPagePreviewCol( 2 ), m_nShadowCursorFillMode( SwFillMode::Tab ), + m_eRedlineRenderMode(SwRedlineRenderMode::Standard), m_bReadonly(false), m_bStarOneSetting(false), m_bIsPagePreview(false), @@ -311,6 +313,7 @@ SwViewOption::SwViewOption(const SwViewOption& rVOpt) m_aRetouchColor = rVOpt.GetRetoucheColor(); m_sSymbolFont = rVOpt.m_sSymbolFont; m_nShadowCursorFillMode = rVOpt.m_nShadowCursorFillMode; + m_eRedlineRenderMode = rVOpt.m_eRedlineRenderMode; m_bStarOneSetting = rVOpt.m_bStarOneSetting; mbBookView = rVOpt.mbBookView; mbBrowseMode = rVOpt.mbBrowseMode; @@ -358,6 +361,7 @@ SwViewOption& SwViewOption::operator=( const SwViewOption &rVOpt ) m_aRetouchColor = rVOpt.GetRetoucheColor(); m_sSymbolFont = rVOpt.m_sSymbolFont; m_nShadowCursorFillMode = rVOpt.m_nShadowCursorFillMode; + m_eRedlineRenderMode = rVOpt.m_eRedlineRenderMode; m_bStarOneSetting = rVOpt.m_bStarOneSetting; mbBookView = rVOpt.mbBookView; mbBrowseMode = rVOpt.mbBrowseMode; @@ -622,6 +626,11 @@ void SwViewOption::dumpAsXml(xmlTextWriterPtr pWriter) const (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwViewOption")); (void)xmlTextWriterWriteFormatAttribute(pWriter, BAD_CAST("ptr"), "%p", this); m_nCoreOptions.dumpAsXml(pWriter); + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("redline-render-mode")); + (void)xmlTextWriterWriteAttribute( + pWriter, BAD_CAST("value"), + BAD_CAST(OString::number(static_cast<int>(m_eRedlineRenderMode)).getStr())); + (void)xmlTextWriterEndElement(pWriter); (void)xmlTextWriterEndElement(pWriter); } diff --git a/sw/source/uibase/uiview/view.cxx b/sw/source/uibase/uiview/view.cxx index 263c62221bd0..80972473c696 100644 --- a/sw/source/uibase/uiview/view.cxx +++ b/sw/source/uibase/uiview/view.cxx @@ -2030,6 +2030,17 @@ std::optional<OString> SwView::getLOKPayload(int nType, int nViewId) const return std::nullopt; } +int SwView::getEditMode() const +{ + SwWrtShell* pWrtShell = GetWrtShellPtr(); + if (!pWrtShell) + { + return 0; + } + + return static_cast<int>(pWrtShell->GetViewOptions()->GetRedlineRenderMode()); +} + OUString SwView::GetDataSourceName() const { uno::Reference<lang::XMultiServiceFactory> xFactory(GetDocShell()->GetModel(), uno::UNO_QUERY); diff --git a/sw/source/uibase/uno/loktxdoc.cxx b/sw/source/uibase/uno/loktxdoc.cxx index f5539e48b39b..c363e1f3ff42 100644 --- a/sw/source/uibase/uno/loktxdoc.cxx +++ b/sw/source/uibase/uno/loktxdoc.cxx @@ -1134,6 +1134,35 @@ bool SwXTextDocument::supportsCommand(std::u16string_view rCommand) return std::find(vForward.begin(), vForward.end(), rCommand) != vForward.end(); } +int SwXTextDocument::getEditMode() +{ + SwViewShell* pViewShell = m_pDocShell->GetWrtShell(); + if (!pViewShell) + { + return 0; + } + + SfxViewShell* pView = pViewShell->GetSfxViewShell(); + return pView->getEditMode(); +} + +void SwXTextDocument::setEditMode(int nEditMode) +{ + auto eMode = static_cast<SwRedlineRenderMode>(nEditMode); + SwViewShell* pViewShell = m_pDocShell->GetWrtShell(); + if (!pViewShell) + { + return; + } + + SwViewOption aOpt(*pViewShell->GetViewOptions()); + if (eMode != aOpt.GetRedlineRenderMode()) + { + aOpt.SetRedlineRenderMode(eMode); + pViewShell->ApplyViewOptions(aOpt); + } +} + void SwXTextDocument::getCommandValues(tools::JsonWriter& rJsonWriter, std::string_view rCommand) { using namespace std::string_view_literals;
