diff --git a/sw/qa/extras/layout/data/tdf171959-center.odt b/sw/qa/extras/layout/data/tdf171959-center.odt new file mode 100644 index 000000000000..748e3420ab75 Binary files /dev/null and b/sw/qa/extras/layout/data/tdf171959-center.odt differ diff --git a/sw/qa/extras/layout/data/tdf171959.odt b/sw/qa/extras/layout/data/tdf171959.odt new file mode 100644 index 000000000000..5122a7eafc98 Binary files /dev/null and b/sw/qa/extras/layout/data/tdf171959.odt differ diff --git a/sw/qa/extras/layout/layout4.cxx b/sw/qa/extras/layout/layout4.cxx index dd7256d1c800..7d6a54040ab6 100644 --- a/sw/qa/extras/layout/layout4.cxx +++ b/sw/qa/extras/layout/layout4.cxx @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -1987,6 +1989,82 @@ CPPUNIT_TEST_FIXTURE(SwLayoutWriter4, TestTdf164098) pWrtShell->Insert(u"ـ"_ustr); } +CPPUNIT_TEST_FIXTURE(SwLayoutWriter4, testTdf171959) +{ + // Every line of this document ends in a blank, and the text is justified and underlined. + createSwDoc("tdf171959.odt"); + + // Whether the trailing blank is decorated can only be settled once the line has been adjusted, + // which happens on the way to painting. + std::shared_ptr xMetaFile = getSwDocShell()->GetPreviewMetaFile(); + + // Justification stretches each line to the right margin, leaving no room for the trailing + // blank, which hangs outside the text area. Without the fix the blank kept its underline, so + // the underline was drawn past the margin. + xmlDocUniquePtr pLayout = parseLayoutDump(); + assertXPath(pLayout, "//SwHolePortion", 7); + assertXPath(pLayout, "//SwHolePortion[@show-underline='true']", 0); + + // The blank is still painted (tdf#43244), only without decorations. + MetafileXmlDump dumper; + xmlDocUniquePtr pMetaXml = dumpAndParse(dumper, *xMetaFile); + assertXPath(pMetaXml, "//textarray[@length='1']", 7); + assertXPath(pMetaXml, "//textarray[@length='1'][preceding-sibling::font[1]/@underline!='0']", + 0); +} + +CPPUNIT_TEST_FIXTURE(SwLayoutWriter4, testTdf171959Centered) +{ + // Same document, but centred: here the trailing blank has the right half of the free space to + // live in, so its decorations may show as far as that reaches. + createSwDoc("tdf171959-center.odt"); + getSwDocShell()->GetPreviewMetaFile(); + + xmlDocUniquePtr pLayout = parseLayoutDump(); + + // A line with 365 twips to spare decorates the whole 60 twip blank. + assertXPath(pLayout, "//txt[1]/SwParaPortion/SwLineLayout[2]/SwHolePortion", "width", u"60"); + assertXPath(pLayout, "//txt[1]/SwParaPortion/SwLineLayout[2]/SwHolePortion", "show-underline", + u"true"); + + // A line with only 49 twips left decorates that much, and an undecorated hole carries the rest + // of the blank, so the cursor still travels over all of it. + assertXPath(pLayout, "//txt[1]/SwParaPortion/SwLineLayout[5]/SwHolePortion[1]", "width", u"49"); + assertXPath(pLayout, "//txt[1]/SwParaPortion/SwLineLayout[5]/SwHolePortion[1]", + "show-underline", u"true"); + assertXPath(pLayout, "//txt[1]/SwParaPortion/SwLineLayout[5]/SwHolePortion[2]", "width", u"11"); + assertXPath(pLayout, "//txt[1]/SwParaPortion/SwLineLayout[5]/SwHolePortion[2]", + "show-underline", u"false"); +} + +CPPUNIT_TEST_FIXTURE(SwLayoutWriter4, testTdf171959ManyBlanks) +{ + // A centred line ending in many blanks: only the right half of the free space is inside the + // text area, so roughly half of the blanks may keep the decoration. + createSwDoc(); + SwWrtShell* pWrtShell = getSwDocShell()->GetWrtShell(); + pWrtShell->SetAttrItem(SvxAdjustItem(SvxAdjust::Center, RES_PARATR_ADJUST)); + pWrtShell->Insert("foo" + RepeatedUChar(' ', 200)); + getSwDocShell()->GetPreviewMetaFile(); + + xmlDocUniquePtr pLayout = parseLayoutDump(); + + // The blank was already too long for the line, so it had a plain hole after it: what no longer + // fits the text area joins that one instead of making a third. + assertXPath(pLayout, "//SwLineLayout/SwHolePortion", 2); + assertXPath(pLayout, "//SwLineLayout/SwHolePortion[1]", "show-underline", u"true"); + assertXPath(pLayout, "//SwLineLayout/SwHolePortion[2]", "show-underline", u"false"); + + // The blanks are shared out, not squeezed into the decorated part, and none go missing. + const sal_Int32 nLen1 + = getXPath(pLayout, "//SwLineLayout/SwHolePortion[1]", "length").toInt32(); + const sal_Int32 nLen2 + = getXPath(pLayout, "//SwLineLayout/SwHolePortion[2]", "length").toInt32(); + CPPUNIT_ASSERT_GREATER(sal_Int32(1), nLen1); + CPPUNIT_ASSERT_GREATER(sal_Int32(0), nLen2); + CPPUNIT_ASSERT_EQUAL(sal_Int32(200), nLen1 + nLen2); +} + } // end of anonymous namespace CPPUNIT_PLUGIN_IMPLEMENT(); diff --git a/sw/source/core/text/inftxt.cxx b/sw/source/core/text/inftxt.cxx index af767839c260..3ada0c5b69bd 100644 --- a/sw/source/core/text/inftxt.cxx +++ b/sw/source/core/text/inftxt.cxx @@ -701,8 +701,11 @@ void SwTextPaintInfo::DrawText_( const OUString &rText, const SwLinePortion &rPo aDrawInf.SetUnderFnt( m_pUnderFnt ); + // tdf#171959: a hole portion carries the trailing blank that hangs outside the line, so it + // takes no part in the justification of the line's content const tools::Long nSpaceAdd = ( rPor.IsBlankPortion() || rPor.IsDropPortion() || - rPor.InNumberGrp() ) ? 0 : GetSpaceAdd(/*bShrink=*/true); + rPor.InNumberGrp() || rPor.IsHolePortion() ) + ? 0 : GetSpaceAdd(/*bShrink=*/true); if ( nSpaceAdd ) { TextFrameIndex nCharCnt(0); diff --git a/sw/source/core/text/itrcrsr.cxx b/sw/source/core/text/itrcrsr.cxx index 7b5f6ce4a363..36a6106597c7 100644 --- a/sw/source/core/text/itrcrsr.cxx +++ b/sw/source/core/text/itrcrsr.cxx @@ -17,6 +17,11 @@ * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ +#include +#include + +#include + #include #include #include @@ -40,6 +45,7 @@ #include "pordrop.hxx" #include #include "pormulti.hxx" +#include "portxt.hxx" #include #include @@ -437,6 +443,75 @@ static bool isTrailingDecoration(SwLinePortion* p) return true; // no more portions } +// How much of the text area is still free after this portion: an adjusted line parks whatever is +// left of it in the trailing margin portion(s). Lines that need no adjustment - left-aligned ones - +// get no margin portion at all, and keep the room they were formatted against; those answer none. +static std::optional calcTrailingRoom(const SwLinePortion* p) +{ + std::optional oRoom; + for (; p; p = p->GetNextPortion()) + if (p->IsMarginPortion()) + oRoom = oRoom.value_or(0) + p->Width(); + return oRoom; +} + +// tdf#171959: The trailing blank deliberately hangs outside the line (tdf#57187), so once the line +// is adjusted it can sit past the text area - and its decorations would then be drawn out there. +// Keep them inside by decorating only as much as still fits, and letting a plain hole carry the +// rest of the blank, so the cursor can still be moved over all of it. +static void limitTrailingDecoration(SwLinePortion& rPor, const SwLinePortion* pNext) +{ + if (!rPor.IsHolePortion()) + return; + + auto& rHole = static_cast(rPor); + if (!rHole.ShowUnderline()) + return; + + const std::optional oRoom = calcTrailingRoom(pNext); + if (!oRoom || *oRoom >= rHole.Width()) + return; + + if (*oRoom <= 0) + { + rHole.SetShowUnderline(false); + return; + } + + // A hole holds nothing but blanks, so its characters divide up in proportion to its width. + // Keeping one is the least that makes sense: the decorated part exists to be decorated. + const SwTwips nWidth = rHole.Width(); + const sal_Int32 nCount = sal_Int32(rHole.GetLen()); + // The room is short of the width, so the share of the blanks that fits cannot exceed them all + const sal_Int32 nFits + = o3tl::narrowing((nCount * *oRoom + nWidth / 2) / nWidth); + const TextFrameIndex nKeep(std::max(1, nFits)); + const TextFrameIndex nMoved(rHole.GetLen() - nKeep); + const SwTwips nExcess(nWidth - *oRoom); + + rHole.SetLen(nKeep); + rHole.Width(*oRoom); + + // A blank too long for the line already has a plain hole after it, holding what did not fit. + // The part that no longer fits the text area belongs there too; only make a hole of its own + // when there is none. Note the follower has not been given its width yet, so hand it over as + // extra blank width, the way formatting left it. + SwLinePortion* pFollow = rHole.GetNextPortion(); + if (pFollow && pFollow->IsHolePortion() + && !static_cast(pFollow)->ShowUnderline()) + { + pFollow->SetLen(pFollow->GetLen() + nMoved); + pFollow->ExtraBlankWidth(pFollow->ExtraBlankWidth() + nExcess); + return; + } + + SwHolePortion* pRest = new SwHolePortion(rHole); + pRest->SetShowUnderline(false); + pRest->SetLen(nMoved); + pRest->Width(nExcess); + rHole.Insert(pRest); +} + // tdf#120715 tdf#43100: Make width for some HolePortions, so cursor will be able to move into it. // It should not change the layout, so this should be called after the layout is calculated. void SwTextCursor::AddExtraBlankWidth() @@ -451,6 +526,7 @@ void SwTextCursor::AddExtraBlankWidth() { pPos->Width(pPos->Width() + pPos->ExtraBlankWidth()); pPos->ExtraBlankWidth(0); + limitTrailingDecoration(*pPos, pNextPos); } pPos = pNextPos; } diff --git a/sw/source/core/text/portxt.hxx b/sw/source/core/text/portxt.hxx index 579928994ad5..51b932b36234 100644 --- a/sw/source/core/text/portxt.hxx +++ b/sw/source/core/text/portxt.hxx @@ -79,6 +79,7 @@ public: virtual void Paint( const SwTextPaintInfo &rInf ) const override; bool ShowUnderline() const { return m_bShowUnderline; } + void SetShowUnderline(bool bNew) { m_bShowUnderline = bNew; } // Accessibility: pass information about this portion to the PortionHandler virtual void HandlePortion( SwPortionHandler& rPH ) const override;