sw/qa/core/text/itrpaint.cxx | 21 ++++-- sw/source/core/inc/drawfont.hxx | 6 + sw/source/core/text/inftxt.cxx | 8 ++ sw/source/core/text/inftxt.hxx | 4 + sw/source/core/text/itrpaint.cxx | 122 +++++++++++++++++++++++++----------- sw/source/core/txtnode/fntcache.cxx | 25 ++++++- 6 files changed, 143 insertions(+), 43 deletions(-)
New commits: commit d9cc16bf57835cc3c41b4efe42791ddf2d7772d4 Author: Miklos Vajna <[email protected]> AuthorDate: Tue Jan 20 09:50:21 2026 +0100 Commit: Xisco Fauli <[email protected]> CommitDate: Thu Jan 22 19:53:10 2026 +0100 cool#13574 sw redline render mode: somewhat color ins/del as green/red So far the non-standard redline render mode focused on hiding inserts and deletes: typically hiding inserts on the left hand side, and hiding deletes on the right hand side, a bit similar to the side-by-side source code diff. What's missing is to do a color transform for the opposite cases, i.e. deletes on the left and inserts on the right. The intent is to color deletes with a red-like color on the left and inserts with a green-like color on the right. We still need to do this in SwFntObj::DrawText(), because automatic colors are not expanded before that point. To keep this readable in both light and dark mode, do the color transform in HSL space and set the "hue" of the color to red/green, and constrain saturation/lightness a bit to ensure readability. The images are not yet touched for this, they are still simply grayscale when hiding and unchanged otherwise. Change-Id: I21e0721d66a5b0d4e85476a0631f360610c92d8a Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197712 Reviewed-by: Miklos Vajna <[email protected]> Tested-by: Jenkins Signed-off-by: Xisco Fauli <[email protected]> Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197774 diff --git a/sw/qa/core/text/itrpaint.cxx b/sw/qa/core/text/itrpaint.cxx index a59960616d68..ffe3699d2c7a 100644 --- a/sw/qa/core/text/itrpaint.cxx +++ b/sw/qa/core/text/itrpaint.cxx @@ -44,13 +44,24 @@ sal_Int16 GetColorLightness(std::u16string_view rRGB) return nBrightness; } +int GetColorHue(std::u16string_view rRGB) +{ + Color aColor(o3tl::toInt32(rRGB.substr(1, 2), 16), o3tl::toInt32(rRGB.substr(3, 2), 16), + o3tl::toInt32(rRGB.substr(5, 2), 16)); + sal_uInt16 nHue; + sal_uInt16 nSaturation; + sal_uInt16 nBrightness; + aColor.RGBtoHSB(nHue, nSaturation, nBrightness); + return nHue; +} + CPPUNIT_TEST_FIXTURE(Test, testRedlineRenderModeOmitInsertDelete) { // Reset redline author IDs to a predictable default. SwGlobals::ensure(); SwModule::get()->ClearRedlineAuthors(); - // Default rendering: default, normal lightness, normal lightness. + // Default rendering: default, delete, insert. createSwDoc("redline.docx"); SwDocShell* pDocShell = getSwDocShell(); @@ -81,7 +92,7 @@ CPPUNIT_TEST_FIXTURE(Test, testRedlineRenderModeOmitInsertDelete) = getXPath(pXmlDoc, "(//textarray)[3]/preceding-sibling::textcolor[1]", "color"); CPPUNIT_ASSERT_EQUAL(aRedlineColorString, aColor3); - // Omit inserts: default, normal lightness, increased lightness. + // Omit inserts, color deletes: default, red-like, increased lightness. SwWrtShell* pWrtShell = pDocShell->GetWrtShell(); SwViewOption aOpt(*pWrtShell->GetViewOptions()); aOpt.SetRedlineRenderMode(SwRedlineRenderMode::OmitInserts); @@ -100,7 +111,7 @@ CPPUNIT_TEST_FIXTURE(Test, testRedlineRenderModeOmitInsertDelete) nLength2 = getXPath(pXmlDoc, "(//textarray)[2]", "length").toInt32(); CPPUNIT_ASSERT_EQUAL(u"oldcontent"_ustr, aContent.copy(nIndex2, nLength2)); aColor2 = getXPath(pXmlDoc, "(//textarray)[2]/preceding-sibling::textcolor[1]", "color"); - CPPUNIT_ASSERT_EQUAL(u"#000000"_ustr, aColor2); + CPPUNIT_ASSERT_EQUAL(0, GetColorHue(aColor2)); nIndex3 = getXPath(pXmlDoc, "(//textarray)[3]", "index").toInt32(); nLength3 = getXPath(pXmlDoc, "(//textarray)[3]", "length").toInt32(); CPPUNIT_ASSERT_EQUAL(u"newcontent"_ustr, aContent.copy(nIndex3, nLength3)); @@ -111,7 +122,7 @@ CPPUNIT_TEST_FIXTURE(Test, testRedlineRenderModeOmitInsertDelete) // i.e. the 3rd text portion had no increased lightness from black. CPPUNIT_ASSERT_GREATEREQUAL(static_cast<sal_Int16>(49), GetColorLightness(aColor3)); - // Omit deletes: default, de-saturated, normal saturation. + // Omit deletes, color inserts: default, increased lightness, green-like. aOpt.SetRedlineRenderMode(SwRedlineRenderMode::OmitDeletes); pWrtShell->ApplyViewOptions(aOpt); @@ -133,7 +144,7 @@ CPPUNIT_TEST_FIXTURE(Test, testRedlineRenderModeOmitInsertDelete) nLength3 = getXPath(pXmlDoc, "(//textarray)[3]", "length").toInt32(); CPPUNIT_ASSERT_EQUAL(u"newcontent"_ustr, aContent.copy(nIndex3, nLength3)); aColor3 = getXPath(pXmlDoc, "(//textarray)[3]/preceding-sibling::textcolor[1]", "color"); - CPPUNIT_ASSERT_EQUAL(u"#000000"_ustr, aColor3); + CPPUNIT_ASSERT_EQUAL(120, GetColorHue(aColor3)); } } diff --git a/sw/source/core/inc/drawfont.hxx b/sw/source/core/inc/drawfont.hxx index 18c7913abf00..1a15be30f316 100644 --- a/sw/source/core/inc/drawfont.hxx +++ b/sw/source/core/inc/drawfont.hxx @@ -92,6 +92,8 @@ class SW_DLLPUBLIC SwDrawTextInfo // inside second half of bound rect, used for Accessibility bool m_bPosMatchesBounds : 1 = false; bool m_bOmitPaint = false; + bool m_bInsertColorPaint = false; + bool m_bDeleteColorPaint = false; #ifdef DBG_UTIL // These flags should control that the appropriate Set-function has been @@ -662,6 +664,10 @@ public: void SetOmitPaint(bool bOmitPaint) { m_bOmitPaint = bOmitPaint; } bool GetOmitPaint() const { return m_bOmitPaint; } + void SetInsertColorPaint(bool bInsertColorPaint) { m_bInsertColorPaint = bInsertColorPaint; } + bool GetInsertColorPaint() const { return m_bInsertColorPaint; } + void SetDeleteColorPaint(bool bDeleteColorPaint) { m_bDeleteColorPaint = bDeleteColorPaint; } + bool GetDeleteColorPaint() const { return m_bDeleteColorPaint; } }; #endif diff --git a/sw/source/core/text/inftxt.cxx b/sw/source/core/text/inftxt.cxx index d63102bc4f07..da133a9c3dec 100644 --- a/sw/source/core/text/inftxt.cxx +++ b/sw/source/core/text/inftxt.cxx @@ -729,6 +729,14 @@ void SwTextPaintInfo::DrawText_( const OUString &rText, const SwLinePortion &rPo { aDrawInf.SetOmitPaint(m_bOmitPaint); } + else if (m_bInsertColorPaint) + { + aDrawInf.SetInsertColorPaint(m_bInsertColorPaint); + } + else if (m_bDeleteColorPaint) + { + aDrawInf.SetDeleteColorPaint(m_bDeleteColorPaint); + } // Draw text next to the left border Point aFontPos(m_aPos); diff --git a/sw/source/core/text/inftxt.hxx b/sw/source/core/text/inftxt.hxx index 1ff88448610d..b32d654cd35d 100644 --- a/sw/source/core/text/inftxt.hxx +++ b/sw/source/core/text/inftxt.hxx @@ -371,6 +371,8 @@ class SwTextPaintInfo : public SwTextSizeInfo sal_uInt16 m_nSpaceIdx; SwLineInfo const* m_pLineInfo{nullptr}; // hack: need this to get line props bool m_bOmitPaint = false; + bool m_bInsertColorPaint = false; + bool m_bDeleteColorPaint = false; void DrawText_(const OUString &rText, const SwLinePortion &rPor, const TextFrameIndex nIdx, const TextFrameIndex nLen, @@ -496,6 +498,8 @@ public: sw::WrongListIterator* GetSmartTags() const { return m_pSmartTags; } void SetLineInfo(SwLineInfo const*const pLineInfo) { m_pLineInfo = pLineInfo; } void SetOmitPaint(bool bOmitPaint) { m_bOmitPaint = bOmitPaint; } + void SetInsertColorPaint(bool bInsertColorPaint) { m_bInsertColorPaint = bInsertColorPaint; } + void SetDeleteColorPaint(bool bDeleteColorPaint) { m_bDeleteColorPaint = bDeleteColorPaint; } }; class SwTextFormatInfo : public SwTextPaintInfo diff --git a/sw/source/core/text/itrpaint.cxx b/sw/source/core/text/itrpaint.cxx index 972d2edfcf24..33af3776a82c 100644 --- a/sw/source/core/text/itrpaint.cxx +++ b/sw/source/core/text/itrpaint.cxx @@ -129,6 +129,8 @@ class SwTextPaintOmitter { SwTextPainter& m_rPainter; bool m_bOmitPaint; + bool m_bInsertColorPaint; + bool m_bDeleteColorPaint; public: SwTextPaintOmitter(SwTextPainter& rPainter, const SwRedlineTable& rRedlineTable); @@ -138,6 +140,8 @@ public: SwTextPaintOmitter::SwTextPaintOmitter(SwTextPainter& rPainter, const SwRedlineTable& rRedlineTable) : m_rPainter(rPainter) , m_bOmitPaint(false) + , m_bInsertColorPaint(false) + , m_bDeleteColorPaint(false) { if (!rPainter.GetRedln() || !rPainter.GetRedln()->IsOn()) { @@ -153,19 +157,39 @@ SwTextPaintOmitter::SwTextPaintOmitter(SwTextPainter& rPainter, const SwRedlineT SwRedlineRenderMode eRedlineRenderMode = rPainter.GetInfo().GetOpt().GetRedlineRenderMode(); const SwRangeRedline* pRedline = rRedlineTable[nRedline]; RedlineType eType = pRedline->GetType(); + // We have a matrix of redline render mode and redline types. The intent is to show the "omit + // inserts" mode on the left (inserts are semi-hidden, deletes are colored), and to show the + // "omit deletes" mode on the right (deletes are semi-hidden, inserts are colored). And do none + // of this in the standard (default) case. if (eRedlineRenderMode == SwRedlineRenderMode::OmitInserts && eType == RedlineType::Insert) { m_bOmitPaint = true; } + else if (eRedlineRenderMode == SwRedlineRenderMode::OmitInserts && eType == RedlineType::Delete) + { + m_bDeleteColorPaint = true; + } else if (eRedlineRenderMode == SwRedlineRenderMode::OmitDeletes && eType == RedlineType::Delete) { m_bOmitPaint = true; } + else if (eRedlineRenderMode == SwRedlineRenderMode::OmitDeletes && eType == RedlineType::Insert) + { + m_bInsertColorPaint = true; + } if (m_bOmitPaint) { rPainter.GetInfo().SetOmitPaint(true); } + else if (m_bInsertColorPaint) + { + rPainter.GetInfo().SetInsertColorPaint(true); + } + else if (m_bDeleteColorPaint) + { + rPainter.GetInfo().SetDeleteColorPaint(true); + } } SwTextPaintOmitter::~SwTextPaintOmitter() @@ -174,6 +198,14 @@ SwTextPaintOmitter::~SwTextPaintOmitter() { m_rPainter.GetInfo().SetOmitPaint(false); } + else if (m_bInsertColorPaint) + { + m_rPainter.GetInfo().SetInsertColorPaint(false); + } + else if (m_bDeleteColorPaint) + { + m_rPainter.GetInfo().SetDeleteColorPaint(false); + } } } diff --git a/sw/source/core/txtnode/fntcache.cxx b/sw/source/core/txtnode/fntcache.cxx index bc270a72d139..fcbce1563c9a 100644 --- a/sw/source/core/txtnode/fntcache.cxx +++ b/sw/source/core/txtnode/fntcache.cxx @@ -1049,16 +1049,33 @@ void SwFntObj::DrawText( SwDrawTextInfo &rInf ) Color aOldColor( pTmpFont->GetColor() ); bool bChgColor = rInf.ApplyAutoColor( pTmpFont ); - if (rInf.GetOmitPaint()) + if (rInf.GetOmitPaint() || rInf.GetInsertColorPaint() || rInf.GetDeleteColorPaint()) { Color aColor = pTmpFont->GetColor(); sal_uInt16 nHue; sal_uInt16 nSaturation; sal_uInt16 nBrightness; aColor.RGBtoHSB(nHue, nSaturation, nBrightness); - // 50% lightness: balance between completely omitting the paint and hard-to-notice small - // difference. - nBrightness = 50; + if (rInf.GetOmitPaint()) + { + // 50% lightness: balance between completely omitting the paint and hard-to-notice small + // difference. + nBrightness = 50; + } + else if (rInf.GetInsertColorPaint()) + { + // Insert: color transform to produce a green-like color. + nHue = 120; + nSaturation = std::max<sal_uInt16>(nSaturation, 75); + nBrightness = std::clamp<sal_uInt16>(nBrightness, 40, 60); + } + else if (rInf.GetDeleteColorPaint()) + { + // Delete: color transform to produce a red-like color. + nHue = 0; + nSaturation = std::max<sal_uInt16>(nSaturation, 75); + nBrightness = std::clamp<sal_uInt16>(nBrightness, 40, 60); + } aColor = Color::HSBtoRGB(nHue, nSaturation, nBrightness); if (aColor != pTmpFont->GetColor()) { commit 6fd7a154547fb71a86115617eb64909b76a8dc8e Author: Miklos Vajna <[email protected]> AuthorDate: Fri Jan 16 16:39:41 2026 +0100 Commit: Xisco Fauli <[email protected]> CommitDate: Thu Jan 22 19:53:02 2026 +0100 cool#13574 sw redline render mode: extract SwTextPaintOmitter from text painter SwTextPainter::DrawTextLine() is quite large, pull out part of that to a new class, see <https://gerrit.libreoffice.org/c/core/+/197320/comment/22f3c720_6f7c95fb/>. Change-Id: I89a0a49646481e174e3a2fa7508de6827d7683cd Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197558 Tested-by: Jenkins Reviewed-by: Miklos Vajna <[email protected]> Signed-off-by: Xisco Fauli <[email protected]> Reviewed-on: https://gerrit.libreoffice.org/c/core/+/197773 diff --git a/sw/source/core/text/itrpaint.cxx b/sw/source/core/text/itrpaint.cxx index cfbbcd5b62b8..972d2edfcf24 100644 --- a/sw/source/core/text/itrpaint.cxx +++ b/sw/source/core/text/itrpaint.cxx @@ -122,6 +122,61 @@ SwLinePortion *SwTextPainter::CalcPaintOfst(const SwRect &rPaint, bool& rbSkippe return pPor; } +namespace +{ +/// See if the redline render mode requires to omit the paint of the text portion. +class SwTextPaintOmitter +{ + SwTextPainter& m_rPainter; + bool m_bOmitPaint; + +public: + SwTextPaintOmitter(SwTextPainter& rPainter, const SwRedlineTable& rRedlineTable); + ~SwTextPaintOmitter(); +}; + +SwTextPaintOmitter::SwTextPaintOmitter(SwTextPainter& rPainter, const SwRedlineTable& rRedlineTable) + : m_rPainter(rPainter) + , m_bOmitPaint(false) +{ + if (!rPainter.GetRedln() || !rPainter.GetRedln()->IsOn()) + { + return; + } + + SwRedlineTable::size_type nRedline = rPainter.GetRedln()->GetAct(); + if (nRedline == SwRedlineTable::npos) + { + return; + } + + SwRedlineRenderMode eRedlineRenderMode = rPainter.GetInfo().GetOpt().GetRedlineRenderMode(); + const SwRangeRedline* pRedline = rRedlineTable[nRedline]; + RedlineType eType = pRedline->GetType(); + if (eRedlineRenderMode == SwRedlineRenderMode::OmitInserts && eType == RedlineType::Insert) + { + m_bOmitPaint = true; + } + else if (eRedlineRenderMode == SwRedlineRenderMode::OmitDeletes && eType == RedlineType::Delete) + { + m_bOmitPaint = true; + } + + if (m_bOmitPaint) + { + rPainter.GetInfo().SetOmitPaint(true); + } +} + +SwTextPaintOmitter::~SwTextPaintOmitter() +{ + if (m_bOmitPaint) + { + m_rPainter.GetInfo().SetOmitPaint(false); + } +} +} + // There are two possibilities to output transparent font: // 1) DrawRect on the whole line and DrawText afterwards // (objectively fast, subjectively slow) @@ -427,37 +482,9 @@ 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) - { - GetInfo().SetOmitPaint(true); - } + SwTextPaintOmitter aTextPaintOmitter(*this, rRedlineTable); - { // #i16816# tagged pdf support Por_Info aPorInfo(*pPor, *this, 0); SwTaggedPDFHelper aTaggedPDFHelper( nullptr, nullptr, &aPorInfo, *pOut ); @@ -479,11 +506,6 @@ void SwTextPainter::DrawTextLine( const SwRect &rPaint, SwSaveClip &rClip, } } - if (bOmitPaint) - { - GetInfo().SetOmitPaint(false); - } - // lazy open LBody and paragraph tag after num portions have been painted to Lbl if (pPor->InNumberGrp() // also footnote label // note: numbering portion may be split if it has multiple scripts
