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 bb8086aea02d867c6de4ef298808c47ba2bfe356 Author: Miklos Vajna <[email protected]> AuthorDate: Wed Nov 26 09:39:11 2025 +0100 Commit: Miklos Vajna <[email protected]> CommitDate: Thu Nov 27 10:41:32 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/+/194676 Reviewed-by: Miklos Vajna <[email protected]> Tested-by: Jenkins Code-Style: Miklos Vajna <[email protected]> diff --git a/desktop/source/lib/init.cxx b/desktop/source/lib/init.cxx index 4fefc4cf908b..2ef7c12af7b9 100644 --- a/desktop/source/lib/init.cxx +++ b/desktop/source/lib/init.cxx @@ -4495,34 +4495,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); } @@ -4532,22 +4535,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 addfc4ec8a2f..7462a448319f 100644 --- a/sw/inc/unotxdoc.hxx +++ b/sw/inc/unotxdoc.hxx @@ -499,6 +499,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 d20b0ed85866..4e8764b4f56b 100644 --- a/sw/inc/view.hxx +++ b/sw/inc/view.hxx @@ -735,6 +735,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 IsSpotlightCharDF() const { return m_bIsSpotlightCharDF; } bool IsSpotlightParaStyles() const { return m_bIsSpotlightParaStyles; } diff --git a/sw/inc/viewopt.hxx b/sw/inc/viewopt.hxx index ff93ba1540c7..f07cdb15c7ed 100644 --- a/sw/inc/viewopt.hxx +++ b/sw/inc/viewopt.hxx @@ -263,6 +263,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; @@ -281,6 +289,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. @@ -866,6 +875,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 14693858cda8..3a030dd8d232 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 @@ -305,6 +307,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; @@ -422,6 +427,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 5a2b5dcdbe90..0fc106ee026c 100644 --- a/sw/source/uibase/config/viewopt.cxx +++ b/sw/source/uibase/config/viewopt.cxx @@ -142,6 +142,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() @@ -226,6 +227,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), @@ -307,6 +309,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; @@ -354,6 +357,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; @@ -618,6 +622,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 f458607815d5..e890589231e5 100644 --- a/sw/source/uibase/uiview/view.cxx +++ b/sw/source/uibase/uiview/view.cxx @@ -2055,6 +2055,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 1e13e8846bc4..f6fe6222f0d1 100644 --- a/sw/source/uibase/uno/loktxdoc.cxx +++ b/sw/source/uibase/uno/loktxdoc.cxx @@ -1135,6 +1135,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;
