diff options
author | Mike Kaganski <mike.kaganski@collabora.com> | 2021-05-06 14:14:56 +0300 |
---|---|---|
committer | Mike Kaganski <mike.kaganski@collabora.com> | 2021-06-08 14:51:29 +0200 |
commit | d0a1616ccad0dd5f5a02c1b0204f537b57d0b4b5 (patch) | |
tree | f52e71cd8c4f6e2df6dad122d70e28a00d75e2a1 /editeng | |
parent | 95ebd24a629b4c8cd62cc20c0701683512cc8fa0 (diff) |
editengine-columns: Implement layout
This changes the way how different parts access positions of lines and
paragraphs. Now there is ImpEditEngine::IterateLineAreas, which performs
uniform iteration over all ParaPortions and lines in order, calling a
user-provided callback function for each portion and line; it passes
all information about current portion, line, area, and column to the
callback, and checks the return from the callback, to decide if it needs
to continue iteration (in case when callback indicated that if doesn't
need further data), and if it needs calling the callback for the rest of
current portion's lines.
This allows to have the code that calculates and iterates dimensions of
lines in one central place, without the need to have duplicating logic
in several places.
One important exception is ImpEditEngine::Paint, which iterates without
ImpEditEngine::IterateLineAreas, because it does many atomic paint
operations in different points of iteration process, and implementing
ImpEditEngine::IterateLineAreas to call callback in the required places
would require increased complexity, which is left for a future change.
To make that possible, ImpEditEngine::IterFlag should be extended to
indicate additional requirements.
Note that in fact, ImpEditEngine::Paint was taken as the model for
implementation of ImpEditEngine::IterateLineAreas, with its detailed
handling of all the vertical offsets like additional line spacing and
interparagraph spacings that depend on context.
The notable result of the centralization of the iteration code is slight
change of heights reported by ImpEditEngine::CalcTextHeight. Previously
it simply added all pre-calculated heights of portions, and not taking
into account all the spacing handling that ImpEditEngine::Paint did,
which was inconsistent (calculated height was different from painted
height). Now ImpEditEngine::CalcTextHeight should provide more accurate
results, which required small changes in the unit tests.
Change-Id: I33cbb978deb974b314d36fda8674186a03991107
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/116034
Tested-by: Jenkins
Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
Diffstat (limited to 'editeng')
-rw-r--r-- | editeng/source/editeng/editeng.cxx | 25 | ||||
-rw-r--r-- | editeng/source/editeng/impedit.cxx | 131 | ||||
-rw-r--r-- | editeng/source/editeng/impedit.hxx | 78 | ||||
-rw-r--r-- | editeng/source/editeng/impedit2.cxx | 573 | ||||
-rw-r--r-- | editeng/source/editeng/impedit3.cxx | 223 | ||||
-rw-r--r-- | editeng/source/editeng/impedit4.cxx | 4 | ||||
-rw-r--r-- | editeng/source/outliner/outlin2.cxx | 5 |
7 files changed, 659 insertions, 380 deletions
diff --git a/editeng/source/editeng/editeng.cxx b/editeng/source/editeng/editeng.cxx index cb1244f2e30f..b90ad3127fee 100644 --- a/editeng/source/editeng/editeng.cxx +++ b/editeng/source/editeng/editeng.cxx @@ -564,6 +564,11 @@ void EditEngine::SetMaxAutoPaperSize( const Size& rSz ) pImpEditEngine->SetMaxAutoPaperSize( rSz ); } +void EditEngine::SetMinColumnWrapHeight(tools::Long nVal) +{ + pImpEditEngine->SetMinColumnWrapHeight(nVal); +} + OUString EditEngine::GetText( LineEnd eEnd ) const { return pImpEditEngine->GetEditDoc().GetText( eEnd ); @@ -2022,28 +2027,12 @@ bool EditEngine::IsTextPos( const Point& rPaperPos, sal_uInt16 nBorder ) if ( !pImpEditEngine->IsFormatted() ) pImpEditEngine->FormatDoc(); - bool bTextPos = false; // take unrotated positions for calculation here Point aDocPos = GetDocPos( rPaperPos ); if ( ( aDocPos.Y() > 0 ) && ( aDocPos.Y() < static_cast<tools::Long>(pImpEditEngine->GetTextHeight()) ) ) - { - EditPaM aPaM = pImpEditEngine->GetPaM( aDocPos, false ); - if ( aPaM.GetNode() ) - { - const ParaPortion& rParaPortion = pImpEditEngine->FindParaPortion( aPaM.GetNode() ); - - sal_Int32 nLine = rParaPortion.GetLineNumber( aPaM.GetIndex() ); - const EditLine& rLine = rParaPortion.GetLines()[nLine]; - Range aLineXPosStartEnd = pImpEditEngine->GetLineXPosStartEnd( &rParaPortion, &rLine ); - if ( ( aDocPos.X() >= aLineXPosStartEnd.Min() - nBorder ) && - ( aDocPos.X() <= aLineXPosStartEnd.Max() + nBorder ) ) - { - bTextPos = true; - } - } - } - return bTextPos; + return pImpEditEngine->IsTextPos(aDocPos, nBorder); + return false; } void EditEngine::SetEditTextObjectPool( SfxItemPool* pPool ) diff --git a/editeng/source/editeng/impedit.cxx b/editeng/source/editeng/impedit.cxx index 653072083f66..9fb5ce8d8897 100644 --- a/editeng/source/editeng/impedit.cxx +++ b/editeng/source/editeng/impedit.cxx @@ -521,107 +521,116 @@ void ImpEditView::DrawSelectionXOR( EditSelection aTmpSel, vcl::Region* pRegion, bool bStartHandleVisible = false; bool bEndHandleVisible = false; - for ( sal_Int32 nPara = nStartPara; nPara <= nEndPara; nPara++ ) - { - ParaPortion* pTmpPortion = pEditEngine->GetParaPortions().SafeGetObject( nPara ); - if (!pTmpPortion) + auto DrawHighlight = [&, nStartLine = sal_Int32(0), nEndLine = sal_Int32(0)]( + const ImpEditEngine::LineAreaInfo& rInfo) mutable { + if (!rInfo.pLine) // Begin of ParaPortion { - SAL_WARN( "editeng", "Portion in Selection not found!" ); - continue; - } - - DBG_ASSERT( !pTmpPortion->IsInvalid(), "Portion in Selection not formatted!" ); - - if ( !pTmpPortion->IsVisible() || pTmpPortion->IsInvalid() ) - continue; - - tools::Long nParaStart = pEditEngine->GetParaPortions().GetYOffset( pTmpPortion ); - if ( ( nParaStart + pTmpPortion->GetHeight() ) < GetVisDocTop() ) - continue; - if ( nParaStart > GetVisDocBottom() ) - break; - - sal_uInt16 nStartLine = 0; - sal_uInt16 nEndLine = pTmpPortion->GetLines().Count() -1; - if ( nPara == nStartPara ) - nStartLine = pTmpPortion->GetLines().FindLine( aTmpSel.Min().GetIndex(), false ); - if ( nPara == nEndPara ) - nEndLine = pTmpPortion->GetLines().FindLine( aTmpSel.Max().GetIndex(), true ); + if (rInfo.nPortion < nStartPara) + return ImpEditEngine::CallbackResult::SkipThisPortion; + if (rInfo.nPortion > nEndPara) + return ImpEditEngine::CallbackResult::Stop; + DBG_ASSERT(!rInfo.rPortion.IsInvalid(), "Portion in Selection not formatted!"); + if (rInfo.rPortion.IsInvalid()) + return ImpEditEngine::CallbackResult::SkipThisPortion; + + if (rInfo.nPortion == nStartPara) + nStartLine = rInfo.rPortion.GetLines().FindLine(aTmpSel.Min().GetIndex(), false); + else + nStartLine = 0; - for ( sal_uInt16 nLine = nStartLine; nLine <= nEndLine; nLine++ ) + if (rInfo.nPortion == nEndPara) + nEndLine = rInfo.rPortion.GetLines().FindLine(aTmpSel.Max().GetIndex(), true); + else + nEndLine = rInfo.rPortion.GetLines().Count() - 1; + } + else // This is a correct ParaPortion { - const EditLine& rLine = pTmpPortion->GetLines()[nLine]; + if (rInfo.nLine < nStartLine) + return ImpEditEngine::CallbackResult::Continue; + if (rInfo.nLine > nEndLine) + return ImpEditEngine::CallbackResult::SkipThisPortion; bool bPartOfLine = false; - sal_Int32 nStartIndex = rLine.GetStart(); - sal_Int32 nEndIndex = rLine.GetEnd(); - if ( ( nPara == nStartPara ) && ( nLine == nStartLine ) && ( nStartIndex != aTmpSel.Min().GetIndex() ) ) + sal_Int32 nStartIndex = rInfo.pLine->GetStart(); + sal_Int32 nEndIndex = rInfo.pLine->GetEnd(); + if ((rInfo.nPortion == nStartPara) && (rInfo.nLine == nStartLine) + && (nStartIndex != aTmpSel.Min().GetIndex())) { nStartIndex = aTmpSel.Min().GetIndex(); bPartOfLine = true; } - if ( ( nPara == nEndPara ) && ( nLine == nEndLine ) && ( nEndIndex != aTmpSel.Max().GetIndex() ) ) + if ((rInfo.nPortion == nEndPara) && (rInfo.nLine == nEndLine) + && (nEndIndex != aTmpSel.Max().GetIndex())) { nEndIndex = aTmpSel.Max().GetIndex(); bPartOfLine = true; } // Can happen if at the beginning of a wrapped line. - if ( nEndIndex < nStartIndex ) + if (nEndIndex < nStartIndex) nEndIndex = nStartIndex; - tools::Rectangle aTmpRect( pEditEngine->pImpEditEngine->GetEditCursor( pTmpPortion, nStartIndex ) ); - Point aTopLeft( aTmpRect.TopLeft() ); - Point aBottomRight( aTmpRect.BottomRight() ); - - aTopLeft.AdjustY(nParaStart ); - aBottomRight.AdjustY(nParaStart ); + tools::Rectangle aTmpRect(pEditEngine->pImpEditEngine->GetEditCursor( + &rInfo.rPortion, rInfo.pLine, nStartIndex, GetCursorFlags::NONE)); + aTmpRect.Move(0, pEditEngine->pImpEditEngine->getTopDirectionAware(rInfo.aArea)); // Only paint if in the visible range ... - if ( aTopLeft.Y() > GetVisDocBottom() ) - break; + if (aTmpRect.Top() > GetVisDocBottom()) + return ImpEditEngine::CallbackResult::Continue; - if ( aBottomRight.Y() < GetVisDocTop() ) - continue; + if (aTmpRect.Bottom() < GetVisDocTop()) + return ImpEditEngine::CallbackResult::Continue; - if ( ( nPara == nStartPara ) && ( nLine == nStartLine ) ) + if ((rInfo.nPortion == nStartPara) && (rInfo.nLine == nStartLine)) bStartHandleVisible = true; - if ( ( nPara == nEndPara ) && ( nLine == nEndLine ) ) + if ((rInfo.nPortion == nEndPara) && (rInfo.nLine == nEndLine)) bEndHandleVisible = true; // Now that we have Bidi, the first/last index doesn't have to be the 'most outside' position - if ( !bPartOfLine ) + if (!bPartOfLine) { - Range aLineXPosStartEnd = pEditEngine->GetLineXPosStartEnd(pTmpPortion, &rLine); - aTopLeft.setX( aLineXPosStartEnd.Min() ); - aBottomRight.setX( aLineXPosStartEnd.Max() ); - ImplDrawHighlightRect(rTarget, aTopLeft, aBottomRight, pPolyPoly.get()); + Range aLineXPosStartEnd + = pEditEngine->GetLineXPosStartEnd(&rInfo.rPortion, rInfo.pLine); + aTmpRect.SetLeft(aLineXPosStartEnd.Min()); + aTmpRect.SetRight(aLineXPosStartEnd.Max()); + aTmpRect.Move(pEditEngine->pImpEditEngine->getLeftDirectionAware(rInfo.aArea), 0); + ImplDrawHighlightRect(rTarget, aTmpRect.TopLeft(), aTmpRect.BottomRight(), + pPolyPoly.get()); } else { sal_Int32 nTmpStartIndex = nStartIndex; sal_Int32 nWritingDirStart, nTmpEndIndex; + const sal_Int32 nLeftOffset + = pEditEngine->pImpEditEngine->getLeftDirectionAware(rInfo.aArea); - while ( nTmpStartIndex < nEndIndex ) + while (nTmpStartIndex < nEndIndex) { - pEditEngine->pImpEditEngine->GetRightToLeft( nPara, nTmpStartIndex+1, &nWritingDirStart, &nTmpEndIndex ); - if ( nTmpEndIndex > nEndIndex ) + pEditEngine->pImpEditEngine->GetRightToLeft(rInfo.nPortion, nTmpStartIndex + 1, + &nWritingDirStart, &nTmpEndIndex); + if (nTmpEndIndex > nEndIndex) nTmpEndIndex = nEndIndex; - DBG_ASSERT( nTmpEndIndex > nTmpStartIndex, "DrawSelectionXOR, Start >= End?" ); + DBG_ASSERT(nTmpEndIndex > nTmpStartIndex, "DrawSelectionXOR, Start >= End?"); - tools::Long nX1 = pEditEngine->GetXPos(pTmpPortion, &rLine, nTmpStartIndex, true); - tools::Long nX2 = pEditEngine->GetXPos(pTmpPortion, &rLine, nTmpEndIndex); + tools::Long nX1 + = pEditEngine->GetXPos(&rInfo.rPortion, rInfo.pLine, nTmpStartIndex, true); + tools::Long nX2 + = pEditEngine->GetXPos(&rInfo.rPortion, rInfo.pLine, nTmpEndIndex); - Point aPt1( std::min( nX1, nX2 ), aTopLeft.Y() ); - Point aPt2( std::max( nX1, nX2 ), aBottomRight.Y() ); + aTmpRect.SetLeft(std::min(nX1, nX2)); + aTmpRect.SetRight(std::max(nX1, nX2)); + aTmpRect.Move(nLeftOffset, 0); - ImplDrawHighlightRect(rTarget, aPt1, aPt2, pPolyPoly.get()); + ImplDrawHighlightRect(rTarget, aTmpRect.TopLeft(), aTmpRect.BottomRight(), + pPolyPoly.get()); nTmpStartIndex = nTmpEndIndex; } } } - } + return ImpEditEngine::CallbackResult::Continue; + }; + pEditEngine->pImpEditEngine->IterateLineAreas(DrawHighlight, ImpEditEngine::IterFlag::none); if (comphelper::LibreOfficeKit::isActive() && mpViewShell && pOutWin) lokSelectionCallback(pPolyPoly, bStartHandleVisible, bEndHandleVisible); @@ -1808,6 +1817,8 @@ const SvxFieldItem* ImpEditView::GetField( const Point& rPos, sal_Int32* pPara, Point aDocPos( GetDocPos( rPos ) ); EditPaM aPaM = pEditEngine->GetPaM(aDocPos, false); + if (!aPaM) + return nullptr; if ( aPaM.GetIndex() == aPaM.GetNode()->Len() ) { @@ -1846,6 +1857,8 @@ bool ImpEditView::IsBulletArea( const Point& rPos, sal_Int32* pPara ) Point aDocPos( GetDocPos( rPos ) ); EditPaM aPaM = pEditEngine->GetPaM(aDocPos, false); + if (!aPaM) + return false; if ( aPaM.GetIndex() == 0 ) { diff --git a/editeng/source/editeng/impedit.hxx b/editeng/source/editeng/impedit.hxx index 394165930379..4a39269507ff 100644 --- a/editeng/source/editeng/impedit.hxx +++ b/editeng/source/editeng/impedit.hxx @@ -54,8 +54,10 @@ #include <o3tl/deleter.hxx> #include <o3tl/typed_flags_set.hxx> +#include <functional> #include <optional> #include <memory> +#include <tuple> #include <vector> class EditView; @@ -489,6 +491,7 @@ private: Size aPaperSize; // Layout Size aMinAutoPaperSize; // Layout ? Size aMaxAutoPaperSize; // Layout ? + tools::Long mnMinColumnWrapHeight = 0; // Corresponds to graphic object height EditDoc aEditDoc; // Document content // Engine Specific data ... @@ -548,8 +551,8 @@ private: // For Formatting / Update... std::vector<std::unique_ptr<DeletedNodeInfo> > aDeletedNodes; tools::Rectangle aInvalidRect; - sal_uInt32 nCurTextHeight; - sal_uInt32 nCurTextHeightNTP; // without trailing empty paragraphs + tools::Long nCurTextHeight; + tools::Long nCurTextHeightNTP; // without trailing empty paragraphs sal_uInt16 nOnePixelInRef; IdleFormattter aIdleFormatter; @@ -616,8 +619,9 @@ private: std::unique_ptr<EditTextObject> GetEmptyTextObject(); + std::tuple<const ParaPortion*, const EditLine*, tools::Long> GetPortionAndLine(Point aDocPos); EditPaM GetPaM( Point aDocPos, bool bSmart = true ); - EditPaM GetPaM( ParaPortion* pPortion, Point aPos, bool bSmart ); + bool IsTextPos(const Point& rDocPos, sal_uInt16 nBorder); tools::Long GetXPos(const ParaPortion* pParaPortion, const EditLine* pLine, sal_Int32 nIndex, bool bPreferPortionStart = false) const; tools::Long GetPortionXOffset(const ParaPortion* pParaPortion, const EditLine* pLine, sal_Int32 nTextPortion) const; sal_Int32 GetChar(const ParaPortion* pParaPortion, const EditLine* pLine, tools::Long nX, bool bSmart = true); @@ -764,22 +768,11 @@ private: void SetValidPaperSize( const Size& rSz ); - tools::Long getXDirectionAware(const Point& pt) const; - tools::Long getYDirectionAware(const Point& pt) const; - tools::Long getWidthDirectionAware(const Size& sz) const; - tools::Long getHeightDirectionAware(const Size& sz) const; - void adjustXDirectionAware(Point& pt, tools::Long x) const; - void adjustYDirectionAware(Point& pt, tools::Long y) const; - void setXDirectionAware(Point& pt, tools::Long x) const; - void setYDirectionAware(Point& pt, tools::Long y) const; - bool isYOverflowDirectionAware(const Point& pt, const tools::Rectangle& rectMax) const; - bool isXOverflowDirectionAware(const Point& pt, const tools::Rectangle& rectMax) const; - css::uno::Reference < css::i18n::XBreakIterator > const & ImplGetBreakIterator() const; css::uno::Reference < css::i18n::XExtendedInputSequenceChecker > const & ImplGetInputSequenceChecker() const; - void ImplUpdateOverflowingParaNum( sal_uInt32 ); - void ImplUpdateOverflowingLineNum( sal_uInt32, sal_uInt32, sal_uInt32 ); + void ImplUpdateOverflowingParaNum(tools::Long); + void ImplUpdateOverflowingLineNum(tools::Long, sal_uInt32, tools::Long); void CreateSpellInfo( bool bMultipleDocs ); /// Obtains a view shell ID from the active EditView. @@ -793,6 +786,8 @@ private: const ParaPortionList& GetParaPortions() const { return aParaPortionList; } ParaPortionList& GetParaPortions() { return aParaPortionList; } + tools::Long Calc1ColumnTextHeight(tools::Long* pHeightNTP); + protected: virtual void Notify( SfxBroadcaster& rBC, const SfxHint& rHint ) override; @@ -845,6 +840,8 @@ public: const Size& GetMaxAutoPaperSize() const { return aMaxAutoPaperSize; } void SetMaxAutoPaperSize( const Size& rSz ) { aMaxAutoPaperSize = rSz; } + void SetMinColumnWrapHeight(tools::Long nVal) { mnMinColumnWrapHeight = nVal; } + void FormatDoc(); void FormatFullDoc(); void UpdateViews( EditView* pCurView = nullptr ); @@ -899,7 +896,7 @@ public: EditSelection MoveParagraphs( Range aParagraphs, sal_Int32 nNewPos, EditView* pCurView ); - sal_uInt32 CalcTextHeight( sal_uInt32* pHeightNTP ); + tools::Long CalcTextHeight( tools::Long* pHeightNTP ); sal_uInt32 GetTextHeight() const; sal_uInt32 GetTextHeightNTP() const; sal_uInt32 CalcTextWidth( bool bIgnoreExtraSpace); @@ -931,7 +928,8 @@ public: } tools::Rectangle PaMtoEditCursor( EditPaM aPaM, GetCursorFlags nFlags = GetCursorFlags::NONE ); - tools::Rectangle GetEditCursor( ParaPortion* pPortion, sal_Int32 nIndex, GetCursorFlags nFlags = GetCursorFlags::NONE ); + tools::Rectangle GetEditCursor(const ParaPortion* pPortion, const EditLine* pLine, + sal_Int32 nIndex, GetCursorFlags nFlags); bool IsModified() const { return aEditDoc.IsModified(); } void SetModifyFlag( bool b ) { aEditDoc.SetModified( b ); } @@ -1131,6 +1129,50 @@ public: void Dispose(); void SetLOKSpecialPaperSize(const Size& rSize) { aLOKSpecialPaperSize = rSize; } const Size& GetLOKSpecialPaperSize() const { return aLOKSpecialPaperSize; } + + enum class CallbackResult + { + Continue, + SkipThisPortion, // Do not call callback until next portion + Stop, // Stop iteration + }; + struct LineAreaInfo + { + sal_Int32 nColumn; // Column number; when overflowing, equal to total number of columns + ParaPortion& rPortion; // Current ParaPortion + sal_Int32 nPortion; + EditLine* pLine; // Current line, or nullptr for paragraph start + sal_Int32 nLine; + tools::Rectangle aArea; // The area for the line (or for rPortion's first line offset) + tools::Long nHeightNeededToNotWrap; + }; + using IterateLinesAreasFunc = std::function<CallbackResult(const LineAreaInfo&)>; + enum IterFlag // bitmask + { + none = 0, + inclILS = 1, // rArea includes interline space + }; + + void IterateLineAreas(const IterateLinesAreasFunc& f, IterFlag eOptions); + + tools::Long GetColumnWidth(const Size& rPaperSize) const; + Point MoveToNextLine(Point& rMovePos, tools::Long nLineHeight, sal_Int32& nColumn, + Point aOrigin, tools::Long* pnHeightNeededToNotWrap = nullptr) const; + + tools::Long getXDirectionAware(const Point& pt) const; + tools::Long getYDirectionAware(const Point& pt) const; + tools::Long getWidthDirectionAware(const Size& sz) const; + tools::Long getHeightDirectionAware(const Size& sz) const; + void adjustXDirectionAware(Point& pt, tools::Long x) const; + void adjustYDirectionAware(Point& pt, tools::Long y) const; + void setXDirectionAware(Point& pt, tools::Long x) const; + void setYDirectionAware(Point& pt, tools::Long y) const; + tools::Long getYOverflowDirectionAware(const Point& pt, const tools::Rectangle& rectMax) const; + bool isXOverflowDirectionAware(const Point& pt, const tools::Rectangle& rectMax) const; + tools::Long getLeftDirectionAware(const tools::Rectangle& rect) const; + tools::Long getRightDirectionAware(const tools::Rectangle& rect) const; + tools::Long getTopDirectionAware(const tools::Rectangle& rect) const; + tools::Long getBottomDirectionAware(const tools::Rectangle& rect) const; }; inline EPaM ImpEditEngine::CreateEPaM( const EditPaM& rPaM ) diff --git a/editeng/source/editeng/impedit2.cxx b/editeng/source/editeng/impedit2.cxx index dae90e5e435a..ec3ef06aa4bc 100644 --- a/editeng/source/editeng/impedit2.cxx +++ b/editeng/source/editeng/impedit2.cxx @@ -59,12 +59,14 @@ #include <svl/asiancfg.hxx> #include <i18nutil/unicode.hxx> #include <tools/diagnose_ex.h> +#include <comphelper/flagguard.hxx> #include <comphelper/lok.hxx> #include <comphelper/processfactory.hxx> #include <unotools/configmgr.hxx> #include <unicode/ubidi.h> #include <algorithm> +#include <limits> #include <memory> #include <string_view> #include <fstream> @@ -538,22 +540,47 @@ bool ImpEditEngine::Command( const CommandEvent& rCEvt, EditView* pView ) if ( !IsFormatted() ) FormatDoc(); - ParaPortion* pParaPortion = GetParaPortions().SafeGetObject( GetEditDoc().GetPos( aPaM.GetNode() ) ); + sal_Int32 nPortionPos = GetEditDoc().GetPos(aPaM.GetNode()); + ParaPortion* pParaPortion = GetParaPortions().SafeGetObject(nPortionPos); if (pParaPortion) { - sal_Int32 nLine = pParaPortion->GetLines().FindLine( aPaM.GetIndex(), true ); - const EditLine& rLine = pParaPortion->GetLines()[nLine]; - std::unique_ptr<tools::Rectangle[]> aRects(new tools::Rectangle[ mpIMEInfos->nLen ]); - for (sal_Int32 i = 0; i < mpIMEInfos->nLen; ++i) - { - sal_Int32 nInputPos = mpIMEInfos->aPos.GetIndex() + i; - if ( nInputPos > rLine.GetEnd() ) - nInputPos = rLine.GetEnd(); - tools::Rectangle aR2 = GetEditCursor( pParaPortion, nInputPos ); - aRects[ i ] = pView->GetImpEditView()->GetWindowPos( aR2 ); - } + const sal_Int32 nMinPos = mpIMEInfos->aPos.GetIndex(); + const sal_Int32 nMaxPos = nMinPos + mpIMEInfos->nLen - 1; + std::vector<tools::Rectangle> aRects(mpIMEInfos->nLen); + + auto CollectCharPositions = [&](const LineAreaInfo& rInfo) { + if (!rInfo.pLine) // Start of ParaPortion + { + if (rInfo.nPortion < nPortionPos) + return CallbackResult::SkipThisPortion; + if (rInfo.nPortion > nPortionPos) + return CallbackResult::Stop; + assert(&rInfo.rPortion == pParaPortion); + } + else // This is the needed ParaPortion + { + if (rInfo.pLine->GetStart() > nMaxPos) + return CallbackResult::Stop; + if (rInfo.pLine->GetEnd() < nMinPos) + return CallbackResult::Continue; + for (sal_Int32 n = nMinPos; n <= nMaxPos; ++n) + { + if (rInfo.pLine->IsIn(n)) + { + tools::Rectangle aR = GetEditCursor(pParaPortion, rInfo.pLine, n, + GetCursorFlags::NONE); + aR.Move(getLeftDirectionAware(rInfo.aArea), + getTopDirectionAware(rInfo.aArea)); + aRects[n - nMinPos] = pView->GetImpEditView()->GetWindowPos(aR); + } + } + } + return CallbackResult::Continue; + }; + IterateLineAreas(CollectCharPositions, IterFlag::none); + if (vcl::Window* pWindow = pView->GetWindow()) - pWindow->SetCompositionCharRect( aRects.get(), mpIMEInfos->nLen ); + pWindow->SetCompositionCharRect(aRects.data(), aRects.size()); } } } @@ -3001,71 +3028,242 @@ EditPaM ImpEditEngine::InsertLineBreak(const EditSelection& aCurSel) // Helper functions +tools::Rectangle ImpEditEngine::GetEditCursor(const ParaPortion* pPortion, const EditLine* pLine, + sal_Int32 nIndex, GetCursorFlags nFlags) +{ + assert(pPortion && pLine); + // nIndex might be not in the line + // Search within the line... + tools::Long nX; + + if ((nIndex == pLine->GetStart()) && (nFlags & GetCursorFlags::StartOfLine)) + { + Range aXRange = GetLineXPosStartEnd(pPortion, pLine); + nX = !IsRightToLeft(GetEditDoc().GetPos(pPortion->GetNode())) ? aXRange.Min() + : aXRange.Max(); + } + else if ((nIndex == pLine->GetEnd()) && (nFlags & GetCursorFlags::EndOfLine)) + { + Range aXRange = GetLineXPosStartEnd(pPortion, pLine); + nX = !IsRightToLeft(GetEditDoc().GetPos(pPortion->GetNode())) ? aXRange.Max() + : aXRange.Min(); + } + else + { + nX = GetXPos(pPortion, pLine, nIndex, bool(nFlags & GetCursorFlags::PreferPortionStart)); + } + + tools::Rectangle aEditCursor; + aEditCursor.SetLeft(nX); + aEditCursor.SetRight(nX); + + aEditCursor.SetBottom(pLine->GetHeight() - 1); + if (nFlags & GetCursorFlags::TextOnly) + aEditCursor.SetTop(aEditCursor.Bottom() - pLine->GetTxtHeight() + 1); + else + aEditCursor.SetTop(aEditCursor.Bottom() + - std::min(pLine->GetTxtHeight(), pLine->GetHeight()) + 1); + return aEditCursor; +} + tools::Rectangle ImpEditEngine::PaMtoEditCursor( EditPaM aPaM, GetCursorFlags nFlags ) { OSL_ENSURE( GetUpdateMode(), "Must not be reached when Update=FALSE: PaMtoEditCursor" ); tools::Rectangle aEditCursor; - tools::Long nY = 0; - for ( sal_Int32 nPortion = 0; nPortion < GetParaPortions().Count(); nPortion++ ) - { - ParaPortion& rPortion = GetParaPortions()[nPortion]; - ContentNode* pNode = rPortion.GetNode(); - OSL_ENSURE( pNode, "Invalid Node in Portion!" ); - if ( pNode != aPaM.GetNode() ) + const sal_Int32 nIndex = aPaM.GetIndex(); + const ParaPortion* pPortion = nullptr; + const EditLine* pLastLine = nullptr; + tools::Rectangle aLineArea; + + auto FindPortionLineAndArea + = [&, bEOL(bool(nFlags & GetCursorFlags::EndOfLine))](const LineAreaInfo& rInfo) { + if (!rInfo.pLine) // start of ParaPortion { - nY += rPortion.GetHeight(); + ContentNode* pNode = rInfo.rPortion.GetNode(); + OSL_ENSURE(pNode, "Invalid Node in Portion!"); + if (pNode != aPaM.GetNode()) + return CallbackResult::SkipThisPortion; + pPortion = &rInfo.rPortion; } - else + else // guaranteed that this is the correct ParaPortion { - aEditCursor = GetEditCursor( &rPortion, aPaM.GetIndex(), nFlags ); - aEditCursor.AdjustTop(nY ); - aEditCursor.AdjustBottom(nY ); - return aEditCursor; + pLastLine = rInfo.pLine; + aLineArea = rInfo.aArea; + if ((rInfo.pLine->GetStart() == nIndex) || (rInfo.pLine->IsIn(nIndex, bEOL))) + return CallbackResult::Stop; } + return CallbackResult::Continue; + }; + IterateLineAreas(FindPortionLineAndArea, IterFlag::none); + + if (pLastLine) + { + aEditCursor = GetEditCursor(pPortion, pLastLine, nIndex, nFlags); + aEditCursor.Move(getLeftDirectionAware(aLineArea), getTopDirectionAware(aLineArea)); } - OSL_FAIL( "Portion not found!" ); + else + OSL_FAIL("Line not found!"); + return aEditCursor; } +void ImpEditEngine::IterateLineAreas(const IterateLinesAreasFunc& f, IterFlag eOptions) +{ + const Point aOrigin(0, 0); + Point aLineStart(aOrigin); + const tools::Long nVertLineSpacing = CalcVertLineSpacing(aLineStart); + const tools::Long nColumnWidth = GetColumnWidth(aPaperSize); + sal_Int32 nColumn = 0; + for (sal_Int32 n = 0, nPortions = GetParaPortions().Count(); n < nPortions; ++n) + { + ParaPortion& rPortion = GetParaPortions()[n]; + bool bSkipThis = true; + if (rPortion.IsVisible()) + { + // when typing idle formatting, asynchronous Paint. Invisible Portions may be invalid. + if (rPortion.IsInvalid()) + return; + + LineAreaInfo aInfo{ + nColumn, // nColumn + rPortion, // rPortion + n, // nPortion + nullptr, // pLine + 0, // nLine + { aLineStart, Size{ nColumnWidth, rPortion.GetFirstLineOffset() } }, // aArea + 0 // nHeightNeededToNotWrap + }; + auto eResult = f(aInfo); + if (eResult == CallbackResult::Stop) + return; + bSkipThis = eResult == CallbackResult::SkipThisPortion; + + sal_uInt16 nSBL = 0; + if (!aStatus.IsOutliner()) + { + const SvxLineSpacingItem& rLSItem + = rPortion.GetNode()->GetContentAttribs().GetItem(EE_PARA_SBL); + nSBL = (rLSItem.GetInterLineSpaceRule() == SvxInterLineSpaceRule::Fix) + ? GetYValue(rLSItem.GetInterLineSpace()) + : 0; + } + + adjustYDirectionAware(aLineStart, rPortion.GetFirstLineOffset()); + for (sal_Int32 nLine = 0, nLines = rPortion.GetLines().Count(); nLine < nLines; nLine++) + { + EditLine& rLine = rPortion.GetLines()[nLine]; + tools::Long nLineHeight = rLine.GetHeight(); + if (nLine != nLines - 1) + nLineHeight += nVertLineSpacing; + MoveToNextLine(aLineStart, nLineHeight, nColumn, aOrigin, + &aInfo.nHeightNeededToNotWrap); + const bool bInclILS = eOptions & IterFlag::inclILS; + if (bInclILS && (nLine != nLines - 1) && !aStatus.IsOutliner()) + { + adjustYDirectionAware(aLineStart, nSBL); + nLineHeight += nSBL; + } + + if (!bSkipThis) + { + Point aOtherCorner(aLineStart); + adjustXDirectionAware(aOtherCorner, nColumnWidth); + adjustYDirectionAware(aOtherCorner, -nLineHeight); + + // Calls to f() for each line + aInfo.nColumn = nColumn; + aInfo.pLine = &rLine; + aInfo.nLine = nLine; + aInfo.aArea = tools::Rectangle::Justify(aLineStart, aOtherCorner); + eResult = f(aInfo); + if (eResult == CallbackResult::Stop) + return; + bSkipThis = eResult == CallbackResult::SkipThisPortion; + } + + if (!bInclILS && (nLine != nLines - 1) && !aStatus.IsOutliner()) + adjustYDirectionAware(aLineStart, nSBL); + } + if (!aStatus.IsOutliner()) + { + const SvxULSpaceItem& rULItem + = rPortion.GetNode()->GetContentAttribs().GetItem(EE_PARA_ULSPACE); + tools::Long nUL = GetYValue(rULItem.GetLower()); + adjustYDirectionAware(aLineStart, nUL); + } + } + // Invisible ParaPortion has no height (see ParaPortion::GetHeight), don't handle it + } +} + +std::tuple<const ParaPortion*, const EditLine*, tools::Long> +ImpEditEngine::GetPortionAndLine(Point aDocPos) +{ + // First find the column from the point + sal_Int32 nClickColumn = 0; + for (tools::Long nColumnStart = 0, nColumnWidth = GetColumnWidth(aPaperSize);; + nColumnStart += mnColumnSpacing + nColumnWidth, ++nClickColumn) + { + if (aDocPos.X() <= nColumnStart + nColumnWidth + mnColumnSpacing / 2) + break; + if (nClickColumn >= mnColumns - 1) + break; + } + + const ParaPortion* pLastPortion = nullptr; + const EditLine* pLastLine = nullptr; + tools::Long nLineStartX = 0; + + auto FindLastMatchingPortionAndLine = [&](const LineAreaInfo& rInfo) { + if (rInfo.pLine) // Only handle lines, not ParaPortion starts + { + if (rInfo.nColumn > nClickColumn) + return CallbackResult::Stop; + pLastPortion = &rInfo.rPortion; // Candidate paragraph + pLastLine = rInfo.pLine; // Last visible line not later than click position + nLineStartX = getLeftDirectionAware(rInfo.aArea); + if (rInfo.nColumn == nClickColumn && getBottomDirectionAware(rInfo.aArea) > aDocPos.Y()) + return CallbackResult::Stop; // Found it + } + return CallbackResult::Continue; + }; + IterateLineAreas(FindLastMatchingPortionAndLine, IterFlag::inclILS); + + return { pLastPortion, pLastLine, nLineStartX }; +} + EditPaM ImpEditEngine::GetPaM( Point aDocPos, bool bSmart ) { OSL_ENSURE( GetUpdateMode(), "Must not be reached when Update=FALSE: GetPaM" ); - tools::Long nY = 0; - EditPaM aPaM; - sal_Int32 nPortion; - for ( nPortion = 0; nPortion < GetParaPortions().Count(); nPortion++ ) + if (const auto& [pPortion, pLine, nLineStartX] = GetPortionAndLine(aDocPos); pPortion) { - ParaPortion* pPortion = &GetParaPortions()[nPortion]; - const tools::Long nTmpHeight = pPortion->GetHeight(); // should also be correct for !bVisible! - nY += nTmpHeight; - if ( nY > aDocPos.Y() ) - { - nY -= nTmpHeight; - aDocPos.AdjustY( -nY ); - // Skip invisible Portions: - while ( pPortion && !pPortion->IsVisible() ) - { - nPortion++; - pPortion = GetParaPortions().SafeGetObject( nPortion ); - } - SAL_WARN_IF(!pPortion, "editeng", "worrying lack of any visible paragraph"); - if (!pPortion) - return aPaM; - return GetPaM(pPortion, aDocPos, bSmart); + sal_Int32 nCurIndex + = GetChar(pPortion, pLine, aDocPos.X() - nLineStartX, bSmart); + EditPaM aPaM(pPortion->GetNode(), nCurIndex); + if (nCurIndex && (nCurIndex == pLine->GetEnd()) + && (pLine != &pPortion->GetLines()[pPortion->GetLines().Count() - 1])) + { + aPaM = CursorLeft(aPaM); } + + return aPaM; } - // Then search for the last visible: - nPortion = GetParaPortions().Count()-1; - while ( nPortion && !GetParaPortions()[nPortion].IsVisible() ) - nPortion--; + return {}; +} - OSL_ENSURE( GetParaPortions()[nPortion].IsVisible(), "No visible paragraph found: GetPaM" ); - aPaM.SetNode( GetParaPortions()[nPortion].GetNode() ); - aPaM.SetIndex( GetParaPortions()[nPortion].GetNode()->Len() ); - return aPaM; +bool ImpEditEngine::IsTextPos(const Point& rDocPos, sal_uInt16 nBorder) +{ + if (const auto& [pPortion, pLine, nLineStartX] = GetPortionAndLine(rDocPos); pPortion) + { + Range aLineXPosStartEnd = GetLineXPosStartEnd(pPortion, pLine); + if ((rDocPos.X() >= nLineStartX + aLineXPosStartEnd.Min() - nBorder) + && (rDocPos.X() <= nLineStartX + aLineXPosStartEnd.Max() + nBorder)) + return true; + } + return false; } sal_uInt32 ImpEditEngine::GetTextHeight() const @@ -3209,28 +3407,114 @@ sal_uInt32 ImpEditEngine::GetTextHeightNTP() const return nCurTextHeightNTP; } -sal_uInt32 ImpEditEngine::CalcTextHeight( sal_uInt32* pHeightNTP ) +tools::Long ImpEditEngine::Calc1ColumnTextHeight(tools::Long* pHeightNTP) { - OSL_ENSURE( GetUpdateMode(), "Should not be used when Update=FALSE: CalcTextHeight" ); - sal_uInt32 nY = 0; - sal_uInt32 nPH; - sal_uInt32 nEmptyHeight = 0; - for ( sal_Int32 nPortion = 0; nPortion < GetParaPortions().Count(); nPortion++ ) { - ParaPortion& rPortion = GetParaPortions()[nPortion]; - nPH = rPortion.GetHeight(); - nY += nPH; - if( pHeightNTP ) { - if ( rPortion.IsEmpty() ) - nEmptyHeight += nPH; - else - nEmptyHeight = 0; + tools::Long nHeight = 0; + // Pretend that we have ~infinite height to get total height + comphelper::ValueRestorationGuard aGuard(nCurTextHeight, + std::numeric_limits<tools::Long>::max()); + + auto FindLastLineBottom = [&](const LineAreaInfo& rInfo) { + if (rInfo.pLine) + { + nHeight = getBottomDirectionAware(rInfo.aArea) + 1; + if (pHeightNTP && !rInfo.rPortion.IsEmpty()) + *pHeightNTP = nHeight; } - } + return CallbackResult::Continue; + }; + IterateLineAreas(FindLastLineBottom, IterFlag::none); + return nHeight; +} - if ( pHeightNTP ) - *pHeightNTP = nY - nEmptyHeight; +tools::Long ImpEditEngine::CalcTextHeight(tools::Long* pHeightNTP) +{ + OSL_ENSURE( GetUpdateMode(), "Should not be used when Update=FALSE: CalcTextHeight" ); - return nY; + if (mnColumns <= 1) + return Calc1ColumnTextHeight(pHeightNTP); // All text fits into a single column - done! + + // The final column height can be smaller than total height divided by number of columns (taking + // into account first line offset and interline spacing, that aren't considered in positioning + // after the wrap). The wrap should only happen after the minimal height is exceeded. + tools::Long nTentativeColHeight = mnMinColumnWrapHeight; + tools::Long nWantedIncrease = 0; + tools::Long nCurrentTextHeight; + + // This does the necessary column balancing for the case when the text does not fit min height. + // When the height of column (taken from nCurTextHeight) is too small, the last column will + // overflow, so the resulting height of the text will exceed the set column height. Increasing + // the column height step by step by the minimal value that allows one of columns to accomodate + // one line more, we finally get to the point where all the text fits. At each iteration, the + // height is only increased, so it's impossible to have infinite layout loops. The found value + // is the global minimum. + // + // E.g., given the following four line heights: + // Line 1: 10; + // Line 2: 12; + // Line 3: 10; + // Line 4: 10; + // number of columns 3, and the minimal paper height of 5, the iterations would be: + // * Tentative column height is set to 5 + // <ITERATION 1> + // * Line 1 is attempted to go to column 0. Overflow is 5 => moved to column 1. + // * Line 2 is attempted to go to column 1 after Line 1; overflow is 17 => moved to column 2. + // * Line 3 is attempted to go to column 2 after Line 2; overflow is 17, stays in max column 2. + // * Line 4 goes to column 2 after Line 3. + // * Final iteration columns are: {empty}, {Line 1}, {Line 2, Line 3, Line 4} + // * Total text height is max({0, 10, 32}) == 32 > Tentative column height 5 => NEXT ITERATION + // * Minimal height increase that allows at least one column to accomodate one more line is + // min({5, 17, 17}) = 5. + // * Tentative column height is set to 5 + 5 = 10. + // <ITERATION 2> + // * Line 1 goes to column 0, no overflow. + // * Line 2 is attempted to go to column 0 after Line 1; overflow is 12 => moved to column 1. + // * Line 3 is attempted to go to column 1 after Line 2; overflow is 12 => moved to column 2. + // * Line 4 is attempted to go to column 2 after Line 3; overflow is 10, stays in max column 2. + // * Final iteration columns are: {Line 1}, {Line 2}, {Line 3, Line 4} + // * Total text height is max({10, 12, 20}) == 20 > Tentative column height 10 => NEXT ITERATION + // * Minimal height increase that allows at least one column to accomodate one more line is + // min({12, 12, 10}) = 10. + // * Tentative column height is set to 10 + 10 == 20. + // <ITERATION 3> + // * Line 1 goes to column 0, no overflow. + // * Line 2 is attempted to go to column 0 after Line 1; overflow is 2 => moved to column 1. + // * Line 3 is attempted to go to column 1 after Line 2; overflow is 2 => moved to column 2. + // * Line 4 is attempted to go to column 2 after Line 3; no overflow. + // * Final iteration columns are: {Line 1}, {Line 2}, {Line 3, Line 4} + // * Total text height is max({10, 12, 20}) == 20 == Tentative column height 20 => END. + do + { + nTentativeColHeight += nWantedIncrease; + nWantedIncrease = std::numeric_limits<tools::Long>::max(); + nCurrentTextHeight = 0; + auto GetHeightAndWantedIncrease = [&, minHeight = tools::Long(0), lastCol = sal_Int32(0)]( + const LineAreaInfo& rInfo) mutable { + if (rInfo.pLine) + { + if (lastCol != rInfo.nColumn) + { + minHeight = std::max(nCurrentTextHeight, + minHeight); // total height can't be less than previous columns + nWantedIncrease = std::min(rInfo.nHeightNeededToNotWrap, nWantedIncrease); + } + lastCol = rInfo.nColumn; + nCurrentTextHeight = std::max(getBottomDirectionAware(rInfo.aArea) + 1, minHeight); + if (pHeightNTP) + { + if (rInfo.rPortion.IsEmpty()) + + *pHeightNTP = std::max(*pHeightNTP, minHeight); + else + *pHeightNTP = nCurrentTextHeight; + } + } + return CallbackResult::Continue; + }; + comphelper::ValueRestorationGuard aGuard(nCurTextHeight, nTentativeColHeight); + IterateLineAreas(GetHeightAndWantedIncrease, IterFlag::none); + } while (nCurrentTextHeight > nTentativeColHeight && nWantedIncrease > 0); + return nCurrentTextHeight; } sal_Int32 ImpEditEngine::GetLineCount( sal_Int32 nParagraph ) const @@ -3654,62 +3938,6 @@ Range ImpEditEngine::GetInvalidYOffsets( ParaPortion* pPortion ) return aRange; } -EditPaM ImpEditEngine::GetPaM( ParaPortion* pPortion, Point aDocPos, bool bSmart ) -{ - OSL_ENSURE( pPortion->IsVisible(), "Why GetPaM() for an invisible paragraph?" ); - OSL_ENSURE( IsFormatted(), "GetPaM: Not formatted" ); - - sal_Int32 nCurIndex = 0; - EditPaM aPaM; - aPaM.SetNode( pPortion->GetNode() ); - - const SvxLineSpacingItem& rLSItem = pPortion->GetNode()->GetContentAttribs().GetItem( EE_PARA_SBL ); - sal_uInt16 nSBL = ( rLSItem.GetInterLineSpaceRule() == SvxInterLineSpaceRule::Fix ) - ? GetYValue( rLSItem.GetInterLineSpace() ) : 0; - - tools::Long nY = pPortion->GetFirstLineOffset(); - - OSL_ENSURE( pPortion->GetLines().Count(), "Empty ParaPortion in GetPaM!" ); - - const EditLine* pLine = nullptr; - for ( sal_Int32 nLine = 0; nLine < pPortion->GetLines().Count(); nLine++ ) - { - const EditLine& rTmpLine = pPortion->GetLines()[nLine]; - nY += rTmpLine.GetHeight(); - if ( !aStatus.IsOutliner() ) - nY += nSBL; - if ( nY > aDocPos.Y() ) - { - pLine = &rTmpLine; - break; // correct Y-position is not of interest - } - - nCurIndex = nCurIndex + rTmpLine.GetLen(); - } - - if ( !pLine ) // may happen only in the range of SA! - { -#if OSL_DEBUG_LEVEL > 0 - const SvxULSpaceItem& rULSpace = pPortion->GetNode()->GetContentAttribs().GetItem( EE_PARA_ULSPACE ); - OSL_ENSURE( nY+GetYValue( rULSpace.GetLower() ) >= aDocPos.Y() , "Index in no line, GetPaM ?" ); -#endif - aPaM.SetIndex( pPortion->GetNode()->Len() ); - return aPaM; - } - - // If no line found, only just X-Position => Index - nCurIndex = GetChar( pPortion, pLine, aDocPos.X(), bSmart ); - aPaM.SetIndex( nCurIndex ); - - if ( nCurIndex && ( nCurIndex == pLine->GetEnd() ) && - ( pLine != &pPortion->GetLines()[pPortion->GetLines().Count()-1] ) ) - { - aPaM = CursorLeft( aPaM ); - } - - return aPaM; -} - sal_Int32 ImpEditEngine::GetChar( const ParaPortion* pParaPortion, const EditLine* pLine, tools::Long nXPos, bool bSmart) { @@ -4173,91 +4401,6 @@ void ImpEditEngine::CalcHeight( ParaPortion* pPortion ) } } -tools::Rectangle ImpEditEngine::GetEditCursor( ParaPortion* pPortion, sal_Int32 nIndex, GetCursorFlags nFlags ) -{ - OSL_ENSURE( pPortion->IsVisible(), "Why GetEditCursor() for an invisible paragraph?" ); - OSL_ENSURE( IsFormatted() || GetTextRanger(), "GetEditCursor: Not formatted" ); - - /* - GetCursorFlags::EndOfLine: If after the last character of a wrapped line, remaining - at the end of the line, not the beginning of the next one. - Purpose: - END => really after the last character - - Selection... - */ - - tools::Long nY = pPortion->GetFirstLineOffset(); - - const SvxLineSpacingItem& rLSItem = pPortion->GetNode()->GetContentAttribs().GetItem( EE_PARA_SBL ); - sal_uInt16 nSBL = ( rLSItem.GetInterLineSpaceRule() == SvxInterLineSpaceRule::Fix ) - ? GetYValue( rLSItem.GetInterLineSpace() ) : 0; - - sal_Int32 nCurIndex = 0; - sal_Int32 nLineCount = pPortion->GetLines().Count(); - OSL_ENSURE( nLineCount, "Empty ParaPortion in GetEditCursor!" ); - if (nLineCount == 0) - return tools::Rectangle(); - const EditLine* pLine = nullptr; - bool bEOL( nFlags & GetCursorFlags::EndOfLine ); - for (sal_Int32 nLine = 0; nLine < nLineCount; ++nLine) - { - const EditLine& rTmpLine = pPortion->GetLines()[nLine]; - if ( ( rTmpLine.GetStart() == nIndex ) || ( rTmpLine.IsIn( nIndex, bEOL ) ) ) - { - pLine = &rTmpLine; - break; - } - - nCurIndex = nCurIndex + rTmpLine.GetLen(); - nY += rTmpLine.GetHeight(); - if ( !aStatus.IsOutliner() ) - nY += nSBL; - } - if ( !pLine ) - { - // Cursor at the End of the paragraph. - OSL_ENSURE( nIndex == nCurIndex, "Index dead wrong in GetEditCursor!" ); - - pLine = &pPortion->GetLines()[nLineCount-1]; - nY -= pLine->GetHeight(); - if ( !aStatus.IsOutliner() ) - nY -= nSBL; - } - - tools::Rectangle aEditCursor; - - aEditCursor.SetTop( nY ); - nY += pLine->GetHeight(); - aEditCursor.SetBottom( nY-1 ); - - // Search within the line... - tools::Long nX; - - if ( ( nIndex == pLine->GetStart() ) && ( nFlags & GetCursorFlags::StartOfLine ) ) - { - Range aXRange = GetLineXPosStartEnd( pPortion, pLine ); - nX = !IsRightToLeft( GetEditDoc().GetPos( pPortion->GetNode() ) ) ? aXRange.Min() : aXRange.Max(); - } - else if ( ( nIndex == pLine->GetEnd() ) && ( nFlags & GetCursorFlags::EndOfLine ) ) - { - Range aXRange = GetLineXPosStartEnd( pPortion, pLine ); - nX = !IsRightToLeft( GetEditDoc().GetPos( pPortion->GetNode() ) ) ? aXRange.Max() : aXRange.Min(); - } - else - { - nX = GetXPos( pPortion, pLine, nIndex, bool( nFlags & GetCursorFlags::PreferPortionStart ) ); - } - - aEditCursor.SetLeft(nX); - aEditCursor.SetRight(nX); - - if ( nFlags & GetCursorFlags::TextOnly ) - aEditCursor.SetTop( aEditCursor.Bottom() - pLine->GetTxtHeight() + 1 ); - else - aEditCursor.SetTop( aEditCursor.Bottom() - std::min( pLine->GetTxtHeight(), pLine->GetHeight() ) + 1 ); - - return aEditCursor; -} - void ImpEditEngine::SetValidPaperSize( const Size& rNewSz ) { aPaperSize = rNewSz; diff --git a/editeng/source/editeng/impedit3.cxx b/editeng/source/editeng/impedit3.cxx index 6a14cda16f92..c33db07b2a6b 100644 --- a/editeng/source/editeng/impedit3.cxx +++ b/editeng/source/editeng/impedit3.cxx @@ -70,6 +70,7 @@ #include <osl/diagnose.h> #include <comphelper/string.hxx> #include <memory> +#include <set> #include <vcl/outdev/ScopedStates.hxx> @@ -375,8 +376,8 @@ void ImpEditEngine::FormatDoc() // Here already, so that not always in CreateLines... bool bMapChanged = ImpCheckRefMapMode(); + std::set<sal_Int32> aRepaintParas; - aInvalidRect = tools::Rectangle(); // make empty for ( sal_Int32 nPara = 0; nPara < GetParaPortions().Count(); nPara++ ) { ParaPortion& rParaPortion = GetParaPortions()[nPara]; @@ -410,46 +411,21 @@ void ImpEditEngine::FormatDoc() rParaPortion.SetMustRepaint( false ); } - // InvalidRect set only once... - if ( aInvalidRect.IsEmpty() ) - { - // For Paperwidth 0 (AutoPageSize) it would otherwise be Empty()... - tools::Long nWidth = std::max(tools::Long(1), getWidthDirectionAware(aPaperSize)); - Range aInvRange( GetInvalidYOffsets( &rParaPortion ) ); - aInvalidRect = tools::Rectangle( Point( 0, nY+aInvRange.Min() ), - Size( nWidth, aInvRange.Len() ) ); - } - else - { - aInvalidRect.SetBottom( nY + rParaPortion.GetHeight() ); - } - } - else if ( bGrow ) - { - aInvalidRect.SetBottom( nY + rParaPortion.GetHeight() ); + aRepaintParas.insert(nPara); } nY += rParaPortion.GetHeight(); } + aInvalidRect = tools::Rectangle(); // make empty + // One can also get into the formatting through UpdateMode ON=>OFF=>ON... // enable optimization first after Vobis delivery... { - sal_uInt32 nNewHeightNTP; - sal_uInt32 nNewHeight = CalcTextHeight( &nNewHeightNTP ); + tools::Long nNewHeightNTP; + tools::Long nNewHeight = CalcTextHeight(&nNewHeightNTP); tools::Long nDiff = nNewHeight - nCurTextHeight; if ( nDiff ) aStatus.GetStatusWord() |= !IsVertical() ? EditStatusFlags::TextHeightChanged : EditStatusFlags::TEXTWIDTHCHANGED; - if ( nNewHeight < nCurTextHeight ) - { - aInvalidRect.SetBottom( static_cast<tools::Long>(std::max( nNewHeight, nCurTextHeight )) ); - if ( aInvalidRect.IsEmpty() ) - { - aInvalidRect.SetTop( 0 ); - // Left and Right are not evaluated, are however set due to IsEmpty. - aInvalidRect.SetLeft( 0 ); - aInvalidRect.SetRight(getWidthDirectionAware(aPaperSize)); - } - } nCurTextHeight = nNewHeight; nCurTextHeightNTP = nNewHeightNTP; @@ -473,6 +449,20 @@ void ImpEditEngine::FormatDoc() } } } + + if (nDiff) + aInvalidRect.Union(tools::Rectangle::Justify( + { 0, nNewHeight }, { getWidthDirectionAware(aPaperSize), nCurTextHeight })); + + if (!aRepaintParas.empty()) + { + auto CombineRepaintParasAreas = [&](const LineAreaInfo& rInfo) { + if (aRepaintParas.count(rInfo.nPortion)) + aInvalidRect.Union(rInfo.aArea); + return CallbackResult::Continue; + }; + IterateLineAreas(CombineRepaintParasAreas, IterFlag::inclILS); + } } bIsFormatting = false; @@ -566,10 +556,10 @@ void ImpEditEngine::CheckPageOverflow() { SAL_INFO("editeng.chaining", "[CONTROL_STATUS] AutoPageSize is " << (( aStatus.GetControlWord() & EEControlBits::AUTOPAGESIZE ) ? "ON" : "OFF") ); - sal_uInt32 nBoxHeight = GetMaxAutoPaperSize().Height(); + tools::Long nBoxHeight = GetMaxAutoPaperSize().Height(); SAL_INFO("editeng.chaining", "[OVERFLOW-CHECK] Current MaxAutoPaperHeight is " << nBoxHeight); - sal_uInt32 nTxtHeight = CalcTextHeight(nullptr); + tools::Long nTxtHeight = CalcTextHeight(nullptr); SAL_INFO("editeng.chaining", "[OVERFLOW-CHECK] Current Text Height is " << nTxtHeight); sal_uInt32 nParaCount = GetParaPortions().Count(); @@ -598,6 +588,13 @@ static sal_Int32 ImplCalculateFontIndependentLineSpacing( const sal_Int32 nFontH return ( nFontHeight * 12 ) / 10; // + 20% } +tools::Long ImpEditEngine::GetColumnWidth(const Size& rPaperSize) const +{ + assert(mnColumns >= 1); + tools::Long nWidth = IsVertical() ? rPaperSize.Height() : rPaperSize.Width(); + return (nWidth - mnColumnSpacing * (mnColumns - 1)) / mnColumns; +} + bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY ) { ParaPortion& rParaPortion = GetParaPortions()[nPara]; @@ -785,11 +782,8 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY ) } } - tools::Long nMaxLineWidth; - if ( !IsVertical() ) - nMaxLineWidth = aStatus.AutoPageWidth() ? aMaxAutoPaperSize.Width() : aPaperSize.Width(); - else - nMaxLineWidth = aStatus.AutoPageHeight() ? aMaxAutoPaperSize.Height() : aPaperSize.Height(); + const bool bAutoSize = IsVertical() ? aStatus.AutoPageHeight() : aStatus.AutoPageWidth(); + tools::Long nMaxLineWidth = GetColumnWidth(bAutoSize ? aMaxAutoPaperSize : aPaperSize); nMaxLineWidth -= GetXValue( rLRItem.GetRight() ); nMaxLineWidth -= nStartX; @@ -797,7 +791,7 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY ) // If PaperSize == long_max, one cannot take away any negative // first line indent. (Overflow) if ( ( nMaxLineWidth < 0 ) && ( nStartX < 0 ) ) - nMaxLineWidth = getWidthDirectionAware(aPaperSize) - GetXValue(rLRItem.GetRight()); + nMaxLineWidth = GetColumnWidth(aPaperSize) - GetXValue(rLRItem.GetRight()); // If still less than 0, it may be just the right edge. if ( nMaxLineWidth <= 0 ) @@ -824,7 +818,7 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY ) { GetTextRanger()->SetVertical( IsVertical() ); - tools::Long nTextY = nStartPosY + GetEditCursor( &rParaPortion, pLine->GetStart() ).Top(); + tools::Long nTextY = nStartPosY + GetEditCursor( &rParaPortion, pLine, pLine->GetStart(), GetCursorFlags::NONE ).Top(); if ( !bSameLineAgain ) { SeekCursor( pNode, nTmpPos+1, aTmpFont ); @@ -1468,7 +1462,7 @@ bool ImpEditEngine::CreateLines( sal_Int32 nPara, sal_uInt32 nStartPosY ) // has to be used for the Alignment. If it does not fit or if it // will change the paper width, it will be formatted again for // Justification! = LEFT anyway. - tools::Long nMaxLineWidthFix = getWidthDirectionAware(aPaperSize) + tools::Long nMaxLineWidthFix = GetColumnWidth(aPaperSize) - GetXValue( rLRItem.GetRight() ) - nStartX; if ( aTextSize.Width() < nMaxLineWidthFix ) nMaxLineWidth = nMaxLineWidthFix; @@ -1725,7 +1719,7 @@ void ImpEditEngine::CreateAndInsertEmptyLine( ParaPortion* pParaPortion ) { sal_Int32 nPara = GetParaPortions().GetPos( pParaPortion ); SvxAdjust eJustification = GetJustification( nPara ); - tools::Long nMaxLineWidth = getWidthDirectionAware(aPaperSize); + tools::Long nMaxLineWidth = GetColumnWidth(aPaperSize); nMaxLineWidth -= GetXValue( rLRItem.GetRight() ); if ( nMaxLineWidth < 0 ) nMaxLineWidth = 1; @@ -3020,15 +3014,17 @@ void ImpEditEngine::setYDirectionAware(Point& pt, tools::Long y) const pt.setX(y); } -bool ImpEditEngine::isYOverflowDirectionAware(const Point& pt, const tools::Rectangle& rectMax) const +tools::Long ImpEditEngine::getYOverflowDirectionAware(const Point& pt, + const tools::Rectangle& rectMax) const { + tools::Long nRes; if (!IsVertical()) - return pt.Y() >= rectMax.Bottom(); - - if (IsTopToBottom()) - return pt.X() <= rectMax.Left(); + nRes = pt.Y() - rectMax.Bottom(); + else if (IsTopToBottom()) + nRes = rectMax.Left() - pt.X(); else - return pt.X() >= rectMax.Right(); + nRes = pt.X() - rectMax.Right(); + return std::max(nRes, tools::Long(0)); } bool ImpEditEngine::isXOverflowDirectionAware(const Point& pt, const tools::Rectangle& rectMax) const @@ -3042,6 +3038,95 @@ bool ImpEditEngine::isXOverflowDirectionAware(const Point& pt, const tools::Rect return pt.Y() < rectMax.Top(); } +tools::Long ImpEditEngine::getLeftDirectionAware(const tools::Rectangle& rect) const +{ + if (!IsVertical()) + return rect.Left(); + + if (IsTopToBottom()) + return rect.Top(); + else + return rect.Bottom(); +} + +tools::Long ImpEditEngine::getRightDirectionAware(const tools::Rectangle& rect) const +{ + if (!IsVertical()) + return rect.Right(); + + if (IsTopToBottom()) + return rect.Bottom(); + else + return rect.Top(); +} + +tools::Long ImpEditEngine::getTopDirectionAware(const tools::Rectangle& rect) const +{ + if (!IsVertical()) + return rect.Top(); + + if (IsTopToBottom()) + return rect.Right(); + else + return rect.Left(); +} + +tools::Long ImpEditEngine::getBottomDirectionAware(const tools::Rectangle& rect) const +{ + if (!IsVertical()) + return rect.Bottom(); + + if (IsTopToBottom()) + return rect.Left(); + else + return rect.Right(); +} + +// Returns the resulting shift for the point; allows to apply the same shift to other points +Point ImpEditEngine::MoveToNextLine( + Point& rMovePos, // [in, out] Point that will move to the next line + tools::Long nLineHeight, // [in] Y-direction move distance (direction-aware) + sal_Int32& rColumn, // [in, out] current column number + Point aOrigin, // [in] Origin point to calculate limits and initial Y position in a new column + tools::Long* pnHeightNeededToNotWrap // On column wrap, returns how much more height is needed +) const +{ + const Point aOld = rMovePos; + + // Move the point by the requested distance in Y direction + adjustYDirectionAware(rMovePos, nLineHeight); + // Check if the resulting position has moved beyond the limits, and more columns left. + // The limits are defined by a rectangle starting from aOrigin with width of aPaperSize + // and height of nCurTextHeight + Size aActPaperSize(aPaperSize); + if (IsVertical()) + aActPaperSize.setWidth(nCurTextHeight); + else + aActPaperSize.setHeight(nCurTextHeight); + tools::Long nNeeded = getYOverflowDirectionAware(rMovePos, { aOrigin, aActPaperSize }); + if (pnHeightNeededToNotWrap) + *pnHeightNeededToNotWrap = nNeeded; + if (nNeeded && rColumn < mnColumns) + { + ++rColumn; + // If we didn't fit into the last column, indicate that only by setting the column number + // to the total number of columns; do not adjust + if (rColumn < mnColumns) + { + // Set Y position of the point to that of aOrigin + setYDirectionAware(rMovePos, getYDirectionAware(aOrigin)); + // Move the point by the requested distance in Y direction + adjustYDirectionAware(rMovePos, nLineHeight); + // Move the point by the column+spacing distance in X direction + adjustXDirectionAware(rMovePos, GetColumnWidth(aPaperSize) + mnColumnSpacing); + } + } + + return rMovePos - aOld; +} + +// TODO: use IterateLineAreas in ImpEditEngine::Paint, to avoid algorithm duplication + void ImpEditEngine::Paint( OutputDevice& rOutDev, tools::Rectangle aClipRect, Point aStartPos, bool bStripOnly, Degree10 nOrientation ) { if ( !GetUpdateMode() && !bStripOnly ) @@ -3077,6 +3162,7 @@ void ImpEditEngine::Paint( OutputDevice& rOutDev, tools::Rectangle aClipRect, Po const tools::Long nVertLineSpacing = CalcVertLineSpacing(aStartPos); + sal_Int32 nColumn = 0; // Over all the paragraphs... @@ -3109,8 +3195,6 @@ void ImpEditEngine::Paint( OutputDevice& rOutDev, tools::Rectangle aClipRect, Po adjustYDirectionAware(aStartPos, rPortion.GetFirstLineOffset()); - const Point aParaStart( aStartPos ); - const SvxLineSpacingItem& rLSItem = rPortion.GetNode()->GetContentAttribs().GetItem( EE_PARA_SBL ); sal_uInt16 nSBL = ( rLSItem.GetInterLineSpaceRule() == SvxInterLineSpaceRule::Fix ) ? GetYValue( rLSItem.GetInterLineSpace() ) : 0; @@ -3121,12 +3205,13 @@ void ImpEditEngine::Paint( OutputDevice& rOutDev, tools::Rectangle aClipRect, Po const EditLine* const pLine = &rPortion.GetLines()[nLine]; assert( pLine && "NULL-Pointer in the line iterator in UpdateViews" ); sal_Int32 nIndex = pLine->GetStart(); + tools::Long nLineHeight = pLine->GetHeight(); + if (nLine != nLastLine) + nLineHeight += nVertLineSpacing; + MoveToNextLine(aStartPos, nLineHeight, nColumn, aOrigin); aTmpPos = aStartPos; adjustXDirectionAware(aTmpPos, pLine->GetStartPosX()); - adjustYDirectionAware(aTmpPos, pLine->GetMaxAscent()); - adjustYDirectionAware(aStartPos, pLine->GetHeight()); - if (nLine != nLastLine) - adjustYDirectionAware(aStartPos, nVertLineSpacing); + adjustYDirectionAware(aTmpPos, pLine->GetMaxAscent() - nLineHeight); if ( ( !IsVertical() && ( aStartPos.Y() > aClipRect.Top() ) ) || ( IsVertical() && IsTopToBottom() && aStartPos.X() < aClipRect.Right() ) @@ -3140,7 +3225,9 @@ void ImpEditEngine::Paint( OutputDevice& rOutDev, tools::Rectangle aClipRect, Po // does, too. No change for not-layouting (painting). if(0 == nLine) // && !bStripOnly) { - GetEditEnginePtr()->PaintingFirstLine(n, aParaStart, aOrigin, nOrientation, rOutDev); + Point aLineStart(aStartPos); + adjustYDirectionAware(aLineStart, -nLineHeight); + GetEditEnginePtr()->PaintingFirstLine(n, aLineStart, aOrigin, nOrientation, rOutDev); // Remember whether a bullet was painted. const SfxBoolItem& rBulletState = pEditEngine->GetParaAttrib(n, EE_PARA_BULLETSTATE); @@ -3159,7 +3246,7 @@ void ImpEditEngine::Paint( OutputDevice& rOutDev, tools::Rectangle aClipRect, Po const TextPortion& rTextPortion = rPortion.GetTextPortions()[nPortion]; const tools::Long nPortionXOffset = GetPortionXOffset( &rPortion, pLine, nPortion ); - setXDirectionAware(aTmpPos, getXDirectionAware(aParaStart)); + setXDirectionAware(aTmpPos, getXDirectionAware(aStartPos)); adjustXDirectionAware(aTmpPos, nPortionXOffset); if (isXOverflowDirectionAware(aTmpPos, aClipRect)) break; // No further output in line necessary @@ -3326,8 +3413,8 @@ void ImpEditEngine::Paint( OutputDevice& rOutDev, tools::Rectangle aClipRect, Po // what will lead to a compressed look with multiple lines const sal_uInt16 nMaxAscent(pLine->GetMaxAscent()); - adjustYDirectionAware(aStartPos, nMaxAscent); - adjustYDirectionAware(aTmpPos, nMaxAscent); + aTmpPos += MoveToNextLine(aStartPos, nMaxAscent, + nColumn, aOrigin); } std::vector< sal_Int32 >::iterator curIt = itSubLines; ++itSubLines; @@ -3739,7 +3826,7 @@ void ImpEditEngine::Paint( OutputDevice& rOutDev, tools::Rectangle aClipRect, Po } // no more visible actions? - if (isYOverflowDirectionAware(aStartPos, aClipRect)) + if (getYOverflowDirectionAware(aStartPos, aClipRect)) break; } @@ -3780,7 +3867,7 @@ void ImpEditEngine::Paint( OutputDevice& rOutDev, tools::Rectangle aClipRect, Po pPDFExtOutDevData->EndStructureElement(); // no more visible actions? - if (isYOverflowDirectionAware(aStartPos, aClipRect)) + if (getYOverflowDirectionAware(aStartPos, aClipRect)) break; } } @@ -3958,7 +4045,7 @@ EditSelection ImpEditEngine::MoveParagraphs( Range aOldPositions, sal_Int32 nNew { aInvalidRect = tools::Rectangle(); // make empty aInvalidRect.SetLeft( 0 ); - aInvalidRect.SetRight( aPaperSize.Width() ); + aInvalidRect.SetRight(GetColumnWidth(aPaperSize)); aInvalidRect.SetTop( GetParaPortions().GetYOffset( pUpperPortion ) ); aInvalidRect.SetBottom( GetParaPortions().GetYOffset( pLowerPortion ) + pLowerPortion->GetHeight() ); @@ -4551,10 +4638,10 @@ void ImpEditEngine::ImplExpandCompressedPortions( EditLine* pLine, ParaPortion* } } -void ImpEditEngine::ImplUpdateOverflowingParaNum(sal_uInt32 nPaperHeight) +void ImpEditEngine::ImplUpdateOverflowingParaNum(tools::Long nPaperHeight) { - sal_uInt32 nY = 0; - sal_uInt32 nPH; + tools::Long nY = 0; + tools::Long nPH; for ( sal_Int32 nPara = 0; nPara < GetParaPortions().Count(); nPara++ ) { ParaPortion& rPara = GetParaPortions()[nPara]; @@ -4570,12 +4657,12 @@ void ImpEditEngine::ImplUpdateOverflowingParaNum(sal_uInt32 nPaperHeight) } } -void ImpEditEngine::ImplUpdateOverflowingLineNum(sal_uInt32 nPaperHeight, +void ImpEditEngine::ImplUpdateOverflowingLineNum(tools::Long nPaperHeight, sal_uInt32 nOverflowingPara, - sal_uInt32 nHeightBeforeOverflowingPara) + tools::Long nHeightBeforeOverflowingPara) { - sal_uInt32 nY = nHeightBeforeOverflowingPara; - sal_uInt32 nLH; + tools::Long nY = nHeightBeforeOverflowingPara; + tools::Long nLH; ParaPortion& rPara = GetParaPortions()[nOverflowingPara]; diff --git a/editeng/source/editeng/impedit4.cxx b/editeng/source/editeng/impedit4.cxx index 059d0bc7a523..ee199b663855 100644 --- a/editeng/source/editeng/impedit4.cxx +++ b/editeng/source/editeng/impedit4.cxx @@ -1094,7 +1094,7 @@ std::unique_ptr<EditTextObject> ImpEditEngine::CreateTextObject( EditSelection a // sleeper set up when Olli paragraphs not hacked! if ( bAllowBigObjects && bOnlyFullParagraphs && IsFormatted() && GetUpdateMode() && ( nTextPortions >= nBigObjectStart ) ) { - XParaPortionList* pXList = new XParaPortionList( GetRefDevice(), aPaperSize.Width(), nStretchX, nStretchY ); + XParaPortionList* pXList = new XParaPortionList( GetRefDevice(), GetColumnWidth(aPaperSize), nStretchX, nStretchY ); pTxtObj->mpImpl->SetPortionInfo(std::unique_ptr<XParaPortionList>(pXList)); for ( nNode = nStartNode; nNode <= nEndNode; nNode++ ) { @@ -1177,7 +1177,7 @@ EditSelection ImpEditEngine::InsertTextObject( const EditTextObject& rTextObject bool bUsePortionInfo = false; XParaPortionList* pPortionInfo = rTextObject.mpImpl->GetPortionInfo(); - if ( pPortionInfo && ( static_cast<tools::Long>(pPortionInfo->GetPaperWidth()) == aPaperSize.Width() ) + if ( pPortionInfo && ( static_cast<tools::Long>(pPortionInfo->GetPaperWidth()) == GetColumnWidth(aPaperSize) ) && ( pPortionInfo->GetRefMapMode() == GetRefDevice()->GetMapMode() ) && ( pPortionInfo->GetStretchX() == nStretchX ) && ( pPortionInfo->GetStretchY() == nStretchY ) ) diff --git a/editeng/source/outliner/outlin2.cxx b/editeng/source/outliner/outlin2.cxx index a0ea00d555f6..789d5d54e061 100644 --- a/editeng/source/outliner/outlin2.cxx +++ b/editeng/source/outliner/outlin2.cxx @@ -223,6 +223,11 @@ void Outliner::SetMaxAutoPaperSize( const Size& rSz ) pEditEngine->SetMaxAutoPaperSize( rSz ); } +void Outliner::SetMinColumnWrapHeight(tools::Long nVal) +{ + pEditEngine->SetMinColumnWrapHeight(nVal); +} + bool Outliner::IsExpanded( Paragraph const * pPara ) const { return pParaList->HasVisibleChildren( pPara ); |