tdf#171959 sw: keep trailing blank decorations inside the text area
Commitb36f181bb7(tdf#168777: Trailing space must show all decorations, also for ODF, 2025-10-11) made the trailing blank of a line show its underline by default. In a justified paragraph that underline is drawn past the right margin. A line's trailing blank lives in a hole portion, which deliberately takes no part in the layout (see853e13f914, tdf#57187: make sure to put trailing blanks to hole portion in narrow lines, 2023-12-30), so it hangs outside the line. When the blank is formatted, its decorated part is limited to the room then left on the line - but adjustment happens afterwards, and justification hands exactly that room to the glue between the words. The blank ends up beyond the text area with its decorations still on. Right-aligned text has the same problem, and centred text has it on lines that nearly fill the width. Reconsider the decorated part once the line is adjusted. The room still available is the width of the trailing margin portion(s), which is where adjustment parks whatever is left of the line, so no coordinates are needed. Lines that need no adjustment at all - left-aligned ones - get no margin portion, and keep the room they were formatted against. Where the room no longer fits the whole blank, the hole is split: the first part is decorated and as wide as the room, a plain hole carries the rest. A hole holds nothing but blanks, so the characters divide between the two in proportion to the width, and the cursor still travels over the whole blank. The blank also took a share of the justification stretch while being painted, which made the overshoot about twice the width of a space. It is not part of the line's content, so exclude it. Signed-off-by: Mike Kaganski <mike.kaganski@collabora.com> Change-Id: I82dfe55a10f7b15969bc4561f8f2f04eb7eeee52 Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/7834 Reviewed-by: Caolán McNamara <caolan.mcnamara@collabora.com> Tested-by: Jenkins CPCI <releng@collaboraoffice.com> (cherry picked from commit 6ebdd28de79e412501152305b48a64d25b3609c5) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/208741 Tested-by: Xisco Fauli <xiscofauli@libreoffice.org> Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
committed by
Xisco Fauli
parent
0aeb164f0f
commit
9e11a7494b
Binary file not shown.
Binary file not shown.
@@ -11,7 +11,9 @@
|
||||
#include <comphelper/propertysequence.hxx>
|
||||
#include <com/sun/star/linguistic2/XHyphenator.hpp>
|
||||
#include <com/sun/star/text/XTextSectionsSupplier.hpp>
|
||||
#include <vcl/gdimtf.hxx>
|
||||
#include <vcl/scheduler.hxx>
|
||||
#include <editeng/adjustitem.hxx>
|
||||
#include <editeng/unolingu.hxx>
|
||||
#include <editeng/editobj.hxx>
|
||||
#include <comphelper/propertyvalue.hxx>
|
||||
@@ -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<GDIMetaFile> 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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
* the License at http://www.apache.org/licenses/LICENSE-2.0 .
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
|
||||
#include <o3tl/safeint.hxx>
|
||||
|
||||
#include <ndtxt.hxx>
|
||||
#include <doc.hxx>
|
||||
#include <paratr.hxx>
|
||||
@@ -40,6 +45,7 @@
|
||||
#include "pordrop.hxx"
|
||||
#include <crstate.hxx>
|
||||
#include "pormulti.hxx"
|
||||
#include "portxt.hxx"
|
||||
#include <numrule.hxx>
|
||||
#include <com/sun/star/i18n/ScriptType.hpp>
|
||||
|
||||
@@ -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<SwTwips> calcTrailingRoom(const SwLinePortion* p)
|
||||
{
|
||||
std::optional<SwTwips> 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<SwHolePortion&>(rPor);
|
||||
if (!rHole.ShowUnderline())
|
||||
return;
|
||||
|
||||
const std::optional<SwTwips> 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<sal_Int32>((nCount * *oRoom + nWidth / 2) / nWidth);
|
||||
const TextFrameIndex nKeep(std::max<sal_Int32>(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<SwHolePortion*>(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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user