diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
commit | 267c6f2ac71f92999e969232431ba04678e7437e (patch) | |
tree | 358c9467650e1d0a1d7227a21dac2e3d08b622b2 /sw/source/core/text | |
parent | Initial commit. (diff) | |
download | libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip |
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sw/source/core/text')
68 files changed, 49876 insertions, 0 deletions
diff --git a/sw/source/core/text/EnhancedPDFExportHelper.cxx b/sw/source/core/text/EnhancedPDFExportHelper.cxx new file mode 100644 index 0000000000..499dcc2417 --- /dev/null +++ b/sw/source/core/text/EnhancedPDFExportHelper.cxx @@ -0,0 +1,3061 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <EnhancedPDFExportHelper.hxx> + +#include <com/sun/star/embed/XEmbeddedObject.hpp> +#include <com/sun/star/i18n/ScriptType.hpp> +#include <com/sun/star/drawing/XShape.hpp> +#include <com/sun/star/beans/XPropertySet.hpp> +#include <hintids.hxx> + +#include <sot/exchange.hxx> +#include <vcl/outdev.hxx> +#include <vcl/pdfextoutdevdata.hxx> +#include <tools/multisel.hxx> +#include <editeng/adjustitem.hxx> +#include <editeng/lrspitem.hxx> +#include <editeng/langitem.hxx> +#include <tools/urlobj.hxx> +#include <svl/languageoptions.hxx> +#include <svl/numformat.hxx> +#include <svl/zforlist.hxx> +#include <swatrset.hxx> +#include <frmatr.hxx> +#include <paratr.hxx> +#include <ndtxt.hxx> +#include <ndole.hxx> +#include <section.hxx> +#include <tox.hxx> +#include <fmtfld.hxx> +#include <txtinet.hxx> +#include <fmtinfmt.hxx> +#include <fchrfmt.hxx> +#include <charfmt.hxx> +#include <fmtanchr.hxx> +#include <fmturl.hxx> +#include <editsh.hxx> +#include <viscrs.hxx> +#include <txtfld.hxx> +#include <reffld.hxx> +#include <doc.hxx> +#include <IDocumentOutlineNodes.hxx> +#include <mdiexp.hxx> +#include <docufld.hxx> +#include <ftnidx.hxx> +#include <txtftn.hxx> +#include <rootfrm.hxx> +#include <pagefrm.hxx> +#include <txtfrm.hxx> +#include <tabfrm.hxx> +#include <rowfrm.hxx> +#include <cellfrm.hxx> +#include <sectfrm.hxx> +#include <ftnfrm.hxx> +#include <flyfrm.hxx> +#include <notxtfrm.hxx> +#include "porfld.hxx" +#include "pormulti.hxx" +#include <SwStyleNameMapper.hxx> +#include "itrpaint.hxx" +#include <i18nlangtag/languagetag.hxx> +#include <IMark.hxx> +#include <printdata.hxx> +#include <vprint.hxx> +#include <SwNodeNum.hxx> +#include <calbck.hxx> +#include <frmtool.hxx> +#include <strings.hrc> +#include <frameformats.hxx> +#include <tblafmt.hxx> +#include <authfld.hxx> +#include <dcontact.hxx> + +#include <tools/globname.hxx> +#include <svx/svdobj.hxx> + +#include <stack> +#include <map> +#include <set> +#include <optional> + +using namespace ::com::sun::star; + +#if OSL_DEBUG_LEVEL > 1 + +static std::vector< sal_uInt16 > aStructStack; + +void lcl_DBGCheckStack() +{ + /* NonStructElement = 0 Document = 1 Part = 2 + * Article = 3 Section = 4 Division = 5 + * BlockQuote = 6 Caption = 7 TOC = 8 + * TOCI = 9 Index = 10 Paragraph = 11 + * Heading = 12 H1-6 = 13 - 18 List = 19 + * ListItem = 20 LILabel = 21 LIBody = 22 + * Table = 23 TableRow = 24 TableHeader = 25 + * TableData = 26 Span = 27 Quote = 28 + * Note = 29 Reference = 30 BibEntry = 31 + * Code = 32 Link = 33 Figure = 34 + * Formula = 35 Form = 36 Continued frame = 99 + */ + + sal_uInt16 nElement; + for ( const auto& rItem : aStructStack ) + { + nElement = rItem; + } + (void)nElement; +}; + +#endif + +typedef std::set< tools::Long, lt_TableColumn > TableColumnsMapEntry; +typedef std::pair< SwRect, sal_Int32 > IdMapEntry; +typedef std::vector< IdMapEntry > LinkIdMap; +typedef std::map< const SwTable*, TableColumnsMapEntry > TableColumnsMap; +typedef std::map< const SwNumberTreeNode*, sal_Int32 > NumListIdMap; +typedef std::map< const SwNumberTreeNode*, sal_Int32 > NumListBodyIdMap; +typedef std::set<const void*> FrameTagSet; + +struct SwEnhancedPDFState +{ + TableColumnsMap m_TableColumnsMap; + LinkIdMap m_LinkIdMap; + NumListIdMap m_NumListIdMap; + NumListBodyIdMap m_NumListBodyIdMap; + FrameTagSet m_FrameTagSet; + + LanguageType m_eLanguageDefault; + + struct Span + { + FontLineStyle eUnderline; + FontLineStyle eOverline; + FontStrikeout eStrikeout; + FontEmphasisMark eFontEmphasis; + short nEscapement; + SwFontScript nScript; + LanguageType nLang; + OUString StyleName; + }; + + ::std::optional<Span> m_oCurrentSpan; + ::std::optional<SwTextAttr const*> m_oCurrentLink; + + SwEnhancedPDFState(LanguageType const eLanguageDefault) + : m_eLanguageDefault(eLanguageDefault) + { + } +}; + +namespace +{ +// ODF Style Names: +const char aTableHeadingName[] = "Table Heading"; +const char aQuotations[] = "Quotations"; +const char aCaption[] = "Caption"; +const char aHeading[] = "Heading"; +const char aQuotation[] = "Quotation"; +const char aSourceText[] = "Source Text"; + +// PDF Tag Names: +constexpr OUStringLiteral aDocumentString = u"Document"; +constexpr OUString aDivString = u"Div"_ustr; +constexpr OUStringLiteral aSectString = u"Sect"; +constexpr OUStringLiteral aHString = u"H"; +constexpr OUStringLiteral aH1String = u"H1"; +constexpr OUStringLiteral aH2String = u"H2"; +constexpr OUStringLiteral aH3String = u"H3"; +constexpr OUStringLiteral aH4String = u"H4"; +constexpr OUStringLiteral aH5String = u"H5"; +constexpr OUStringLiteral aH6String = u"H6"; +constexpr OUStringLiteral aH7String = u"H7"; +constexpr OUStringLiteral aH8String = u"H8"; +constexpr OUStringLiteral aH9String = u"H9"; +constexpr OUStringLiteral aH10String = u"H10"; +constexpr OUStringLiteral aListString = u"L"; +constexpr OUStringLiteral aListItemString = u"LI"; +constexpr OUStringLiteral aListLabelString = u"Lbl"; +constexpr OUString aListBodyString = u"LBody"_ustr; +constexpr OUStringLiteral aBlockQuoteString = u"BlockQuote"; +constexpr OUString aCaptionString = u"Caption"_ustr; +constexpr OUStringLiteral aIndexString = u"Index"; +constexpr OUStringLiteral aTOCString = u"TOC"; +constexpr OUStringLiteral aTOCIString = u"TOCI"; +constexpr OUStringLiteral aTableString = u"Table"; +constexpr OUStringLiteral aTRString = u"TR"; +constexpr OUStringLiteral aTDString = u"TD"; +constexpr OUStringLiteral aTHString = u"TH"; +constexpr OUStringLiteral aBibEntryString = u"BibEntry"; +constexpr OUStringLiteral aQuoteString = u"Quote"; +constexpr OUString aSpanString = u"Span"_ustr; +constexpr OUStringLiteral aCodeString = u"Code"; +constexpr OUStringLiteral aFigureString = u"Figure"; +constexpr OUStringLiteral aFormulaString = u"Formula"; +constexpr OUString aLinkString = u"Link"_ustr; +constexpr OUStringLiteral aNoteString = u"Note"; + +// returns true if first paragraph in cell frame has 'table heading' style +bool lcl_IsHeadlineCell( const SwCellFrame& rCellFrame ) +{ + bool bRet = false; + + const SwContentFrame *pCnt = rCellFrame.ContainsContent(); + if ( pCnt && pCnt->IsTextFrame() ) + { + SwTextNode const*const pTextNode = static_cast<const SwTextFrame*>(pCnt)->GetTextNodeForParaProps(); + const SwFormat* pTextFormat = pTextNode->GetFormatColl(); + + OUString sStyleName; + SwStyleNameMapper::FillProgName( pTextFormat->GetName(), sStyleName, SwGetPoolIdFromName::TxtColl ); + bRet = sStyleName == aTableHeadingName; + } + + // tdf#153935 wild guessing for 1st row based on table autoformat + if (!bRet && !rCellFrame.GetUpper()->GetPrev()) + { + SwTable const*const pTable(rCellFrame.FindTabFrame()->GetTable()); + assert(pTable); + OUString const& rStyleName(pTable->GetTableStyleName()); + if (!rStyleName.isEmpty()) + { + if (SwTableAutoFormat const*const pTableAF = + pTable->GetFrameFormat()->GetDoc()->GetTableStyles().FindAutoFormat(rStyleName)) + { + bRet |= pTableAF->HasHeaderRow(); + } + } + } + + return bRet; +} + +// List all frames for which the NonStructElement tag is set: +bool lcl_IsInNonStructEnv( const SwFrame& rFrame ) +{ + bool bRet = false; + + if ( nullptr != rFrame.FindFooterOrHeader() && + !rFrame.IsHeaderFrame() && !rFrame.IsFooterFrame() ) + { + bRet = true; + } + else if ( rFrame.IsInTab() && !rFrame.IsTabFrame() ) + { + const SwTabFrame* pTabFrame = rFrame.FindTabFrame(); + if ( rFrame.GetUpper() != pTabFrame && + pTabFrame->IsFollow() && pTabFrame->IsInHeadline( rFrame ) ) + bRet = true; + } + + return bRet; +} + +// Generate key from frame for reopening tags: +void const* lcl_GetKeyFromFrame( const SwFrame& rFrame ) +{ + void const* pKey = nullptr; + + if ( rFrame.IsPageFrame() ) + pKey = static_cast<void const *>(&(static_cast<const SwPageFrame&>(rFrame).GetFormat()->getIDocumentSettingAccess())); + else if ( rFrame.IsTextFrame() ) + pKey = static_cast<void const *>(static_cast<const SwTextFrame&>(rFrame).GetTextNodeFirst()); + else if ( rFrame.IsSctFrame() ) + pKey = static_cast<void const *>(static_cast<const SwSectionFrame&>(rFrame).GetSection()); + else if ( rFrame.IsTabFrame() ) + pKey = static_cast<void const *>(static_cast<const SwTabFrame&>(rFrame).GetTable()); + else if ( rFrame.IsRowFrame() ) + pKey = static_cast<void const *>(static_cast<const SwRowFrame&>(rFrame).GetTabLine()); + else if ( rFrame.IsCellFrame() ) + { + const SwTabFrame* pTabFrame = rFrame.FindTabFrame(); + const SwTable* pTable = pTabFrame->GetTable(); + pKey = static_cast<void const *>(& static_cast<const SwCellFrame&>(rFrame).GetTabBox()->FindStartOfRowSpan(*pTable)); + } + else if (rFrame.IsFootnoteFrame()) + { + pKey = static_cast<void const*>(static_cast<SwFootnoteFrame const&>(rFrame).GetAttr()); + } + + return pKey; +} + +bool lcl_HasPreviousParaSameNumRule(SwTextFrame const& rTextFrame, const SwTextNode& rNode) +{ + bool bRet = false; + SwNodeIndex aIdx( rNode ); + const SwDoc& rDoc = rNode.GetDoc(); + const SwNodes& rNodes = rDoc.GetNodes(); + const SwNode* pNode = &rNode; + const SwNumRule* pNumRule = rNode.GetNumRule(); + + while (pNode != rNodes.DocumentSectionStartNode(const_cast<SwNode*>(static_cast<SwNode const *>(&rNode))) ) + { + sw::GotoPrevLayoutTextFrame(aIdx, rTextFrame.getRootFrame()); + + if (aIdx.GetNode().IsTextNode()) + { + const SwTextNode *const pPrevTextNd = sw::GetParaPropsNode( + *rTextFrame.getRootFrame(), *aIdx.GetNode().GetTextNode()); + const SwNumRule * pPrevNumRule = pPrevTextNd->GetNumRule(); + + // We find the previous text node. Now check, if the previous text node + // has the same numrule like rNode: + if ( (pPrevNumRule == pNumRule) && + (!pPrevTextNd->IsOutline() == !rNode.IsOutline())) + bRet = true; + + break; + } + + pNode = &aIdx.GetNode(); + } + return bRet; +} + +bool lcl_TryMoveToNonHiddenField(SwEditShell& rShell, const SwTextNode& rNd, const SwFormatField& rField) +{ + // 1. Check if the whole paragraph is hidden + // 2. Move to the field + // 3. Check for hidden text attribute + if(rNd.IsHidden()) + return false; + if(!rShell.GotoFormatField(rField) || rShell.IsInHiddenRange(/*bSelect=*/false)) + { + rShell.SwCursorShell::ClearMark(); + return false; + } + return true; +}; + +// tdf#157816: try to check if the rectangle contains actual text +::std::vector<SwRect> GetCursorRectsContainingText(SwCursorShell const& rShell) +{ + ::std::vector<SwRect> ret; + SwRects rects; + rShell.GetLayout()->CalcFrameRects(*rShell.GetCursor_(), rects, SwRootFrame::RectsMode::NoAnchoredFlys); + for (SwRect const& rRect : rects) + { + Point center(rRect.Center()); + SwSpecialPos special; + SwCursorMoveState cms(CursorMoveState::NONE); + cms.m_pSpecialPos = &special; + cms.m_bFieldInfo = true; + SwPosition pos(rShell.GetDoc()->GetNodes()); + auto const [pStart, pEnd] = rShell.GetCursor_()->StartEnd(); + if (rShell.GetLayout()->GetModelPositionForViewPoint(&pos, center, &cms) + && *pStart <= pos && pos <= *pEnd) + { + SwRect charRect; + if (rShell.GetCurrFrame(false)->GetCharRect(charRect, pos, &cms, false) + && rRect.Overlaps(charRect)) + { + ret.push_back(rRect); + } + } + // reset stupid static var that may have gotten set now + SwTextCursor::SetRightMargin(false); // WTF is this crap + } + return ret; +} + +} // end namespace + +SwTaggedPDFHelper::SwTaggedPDFHelper( const Num_Info* pNumInfo, + const Frame_Info* pFrameInfo, + const Por_Info* pPorInfo, + OutputDevice const & rOut ) + : m_nEndStructureElement( 0 ), + m_nRestoreCurrentTag( -1 ), + mpNumInfo( pNumInfo ), + mpFrameInfo( pFrameInfo ), + mpPorInfo( pPorInfo ) +{ + mpPDFExtOutDevData = + dynamic_cast< vcl::PDFExtOutDevData*>( rOut.GetExtOutDevData() ); + + if ( !(mpPDFExtOutDevData && mpPDFExtOutDevData->GetIsExportTaggedPDF()) ) + return; + +#if OSL_DEBUG_LEVEL > 1 + sal_Int32 nCurrentStruct = mpPDFExtOutDevData->GetCurrentStructureElement(); + lcl_DBGCheckStack(); +#endif + if ( mpNumInfo ) + BeginNumberedListStructureElements(); + else if ( mpFrameInfo ) + BeginBlockStructureElements(); + else if ( mpPorInfo ) + BeginInlineStructureElements(); + else + BeginTag( vcl::PDFWriter::NonStructElement, OUString() ); + +#if OSL_DEBUG_LEVEL > 1 + nCurrentStruct = mpPDFExtOutDevData->GetCurrentStructureElement(); + lcl_DBGCheckStack(); + (void)nCurrentStruct; +#endif +} + +SwTaggedPDFHelper::~SwTaggedPDFHelper() +{ + if ( !(mpPDFExtOutDevData && mpPDFExtOutDevData->GetIsExportTaggedPDF()) ) + return; + +#if OSL_DEBUG_LEVEL > 1 + sal_Int32 nCurrentStruct = mpPDFExtOutDevData->GetCurrentStructureElement(); + lcl_DBGCheckStack(); +#endif + EndStructureElements(); + +#if OSL_DEBUG_LEVEL > 1 + nCurrentStruct = mpPDFExtOutDevData->GetCurrentStructureElement(); + lcl_DBGCheckStack(); + (void)nCurrentStruct; +#endif +} + +void const* SwDrawContact::GetPDFAnchorStructureElementKey(SdrObject const& rObj) +{ + SwFrame const*const pAnchorFrame(GetAnchoredObj(&rObj)->GetAnchorFrame()); + return pAnchorFrame ? lcl_GetKeyFromFrame(*pAnchorFrame) : nullptr; +} + +bool SwTaggedPDFHelper::CheckReopenTag() +{ + bool bRet = false; + void const* pReopenKey(nullptr); + bool bContinue = false; // in some cases we just have to reopen a tag without early returning + + if ( mpFrameInfo ) + { + const SwFrame& rFrame = mpFrameInfo->mrFrame; + const SwFrame* pKeyFrame = nullptr; + + // Reopen an existing structure element if + // - rFrame is not the first page frame (reopen Document tag) + // - rFrame is a follow frame (reopen Master tag) + // - rFrame is a fly frame anchored at content (reopen Anchor paragraph tag) + // - rFrame is a fly frame anchored at page (reopen Document tag) + // - rFrame is a follow flow row (reopen TableRow tag) + // - rFrame is a cell frame in a follow flow row (reopen TableData tag) + if ( ( rFrame.IsPageFrame() && static_cast<const SwPageFrame&>(rFrame).GetPrev() ) || + ( rFrame.IsFlowFrame() && SwFlowFrame::CastFlowFrame(&rFrame)->IsFollow() ) || + (rFrame.IsFootnoteFrame() && static_cast<SwFootnoteFrame const&>(rFrame).GetMaster()) || + ( rFrame.IsRowFrame() && rFrame.IsInFollowFlowRow() ) || + ( rFrame.IsCellFrame() && const_cast<SwFrame&>(rFrame).GetPrevCellLeaf() ) ) + { + pKeyFrame = &rFrame; + } + else if (rFrame.IsFlyFrame() && !mpFrameInfo->m_isLink) + { + const SwFormatAnchor& rAnchor = + static_cast<const SwFlyFrame*>(&rFrame)->GetFormat()->GetAnchor(); + if ((RndStdIds::FLY_AT_PARA == rAnchor.GetAnchorId()) || + (RndStdIds::FLY_AT_CHAR == rAnchor.GetAnchorId()) || + (RndStdIds::FLY_AT_PAGE == rAnchor.GetAnchorId())) + { + pKeyFrame = static_cast<const SwFlyFrame&>(rFrame).GetAnchorFrame(); + bContinue = true; + } + } + + if ( pKeyFrame ) + { + void const*const pKey = lcl_GetKeyFromFrame(*pKeyFrame); + FrameTagSet& rFrameTagSet(mpPDFExtOutDevData->GetSwPDFState()->m_FrameTagSet); + if (rFrameTagSet.find(pKey) != rFrameTagSet.end() + || rFrame.IsFlyFrame()) // for hell layer flys + { + pReopenKey = pKey; + } + } + } + + if (pReopenKey) + { + // note: it would be possible to get rid of the SetCurrentStructureElement() + // - which is quite ugly - for most cases by recreating the parents until the + // current ancestor, but there are special cases cell frame rowspan > 1 follow + // and footnote frame follow where the parent of the follow is different from + // the parent of the first one, and so in PDFExtOutDevData the wrong parent + // would be restored and used for next elements. + m_nRestoreCurrentTag = mpPDFExtOutDevData->GetCurrentStructureElement(); + sal_Int32 const id = mpPDFExtOutDevData->EnsureStructureElement(pReopenKey); + mpPDFExtOutDevData->SetCurrentStructureElement(id); + + bRet = true; + } + + return bRet && !bContinue; +} + +void SwTaggedPDFHelper::CheckRestoreTag() const +{ + if ( m_nRestoreCurrentTag != -1 ) + { + mpPDFExtOutDevData->SetCurrentStructureElement( m_nRestoreCurrentTag ); + +#if OSL_DEBUG_LEVEL > 1 + aStructStack.pop_back(); +#endif + } +} + +void SwTaggedPDFHelper::OpenTagImpl(void const*const pKey) +{ + sal_Int32 const id = mpPDFExtOutDevData->EnsureStructureElement(pKey); + mpPDFExtOutDevData->BeginStructureElement(id); + ++m_nEndStructureElement; + +#if OSL_DEBUG_LEVEL > 1 + aStructStack.push_back( 99 ); +#endif +} + +sal_Int32 SwTaggedPDFHelper::BeginTagImpl(void const*const pKey, + vcl::PDFWriter::StructElement const eType, const OUString& rString) +{ + // write new tag + const sal_Int32 nId = mpPDFExtOutDevData->EnsureStructureElement(pKey); + mpPDFExtOutDevData->InitStructureElement(nId, eType, rString); + mpPDFExtOutDevData->BeginStructureElement(nId); + ++m_nEndStructureElement; + +#if OSL_DEBUG_LEVEL > 1 + aStructStack.push_back( o3tl::narrowing<sal_uInt16>(eType) ); +#endif + + return nId; +} + +void SwTaggedPDFHelper::BeginTag( vcl::PDFWriter::StructElement eType, const OUString& rString ) +{ + void const* pKey(nullptr); + + if (mpFrameInfo) + { + const SwFrame& rFrame = mpFrameInfo->mrFrame; + + if ( ( rFrame.IsPageFrame() && !static_cast<const SwPageFrame&>(rFrame).GetPrev() ) || + ( rFrame.IsFlowFrame() && !SwFlowFrame::CastFlowFrame(&rFrame)->IsFollow() && SwFlowFrame::CastFlowFrame(&rFrame)->HasFollow() ) || + rFrame.IsSctFrame() || // all of them, so that opening parent sections works + ( rFrame.IsTextFrame() && rFrame.GetDrawObjs() ) || + (rFrame.IsFootnoteFrame() && static_cast<SwFootnoteFrame const&>(rFrame).GetFollow()) || + ( rFrame.IsRowFrame() && rFrame.IsInSplitTableRow() ) || + ( rFrame.IsCellFrame() && const_cast<SwFrame&>(rFrame).GetNextCellLeaf() ) ) + { + pKey = lcl_GetKeyFromFrame(rFrame); + + if (pKey) + { + FrameTagSet& rFrameTagSet(mpPDFExtOutDevData->GetSwPDFState()->m_FrameTagSet); + assert(rFrameTagSet.find(pKey) == rFrameTagSet.end()); + rFrameTagSet.emplace(pKey); + } + } + } + + sal_Int32 const nId = BeginTagImpl(pKey, eType, rString); + + // Store the id of the current structure element if + // - it is a list structure element + // - it is a list body element with children + // - rFrame is the first page frame + // - rFrame is a master frame + // - rFrame has objects anchored to it + // - rFrame is a row frame or cell frame in a split table row + + if ( mpNumInfo ) + { + const SwTextFrame& rTextFrame = mpNumInfo->mrFrame; + SwTextNode const*const pTextNd = rTextFrame.GetTextNodeForParaProps(); + const SwNodeNum* pNodeNum = pTextNd->GetNum(rTextFrame.getRootFrame()); + + if ( vcl::PDFWriter::List == eType ) + { + NumListIdMap& rNumListIdMap(mpPDFExtOutDevData->GetSwPDFState()->m_NumListIdMap); + rNumListIdMap[ pNodeNum ] = nId; + } + else if ( vcl::PDFWriter::LIBody == eType ) + { + NumListBodyIdMap& rNumListBodyIdMap(mpPDFExtOutDevData->GetSwPDFState()->m_NumListBodyIdMap); + rNumListBodyIdMap[ pNodeNum ] = nId; + } + } + + SetAttributes( eType ); +} + +void SwTaggedPDFHelper::EndTag() +{ + mpPDFExtOutDevData->EndStructureElement(); + +#if OSL_DEBUG_LEVEL > 1 + aStructStack.pop_back(); +#endif +} + +namespace { + + // link the link annotation to the link structured element + void LinkLinkLink(vcl::PDFExtOutDevData & rPDFExtOutDevData, SwRect const& rRect) + { + const LinkIdMap& rLinkIdMap(rPDFExtOutDevData.GetSwPDFState()->m_LinkIdMap); + const Point aCenter = rRect.Center(); + auto aIter = std::find_if(rLinkIdMap.begin(), rLinkIdMap.end(), + [&aCenter](const IdMapEntry& rEntry) { return rEntry.first.Contains(aCenter); }); + if (aIter != rLinkIdMap.end()) + { + sal_Int32 nLinkId = (*aIter).second; + rPDFExtOutDevData.SetStructureAttributeNumerical(vcl::PDFWriter::LinkAnnotation, nLinkId); + } + } +} + +// Sets the attributes according to the structure type. +void SwTaggedPDFHelper::SetAttributes( vcl::PDFWriter::StructElement eType ) +{ + sal_Int32 nVal; + + /* + * ATTRIBUTES FOR BLSE + */ + if ( mpFrameInfo ) + { + vcl::PDFWriter::StructAttributeValue eVal; + const SwFrame* pFrame = &mpFrameInfo->mrFrame; + SwRectFnSet aRectFnSet(pFrame); + + bool bPlacement = false; + bool bWritingMode = false; + bool bSpaceBefore = false; + bool bSpaceAfter = false; + bool bStartIndent = false; + bool bEndIndent = false; + bool bTextIndent = false; + bool bTextAlign = false; + bool bWidth = false; + bool bHeight = false; + bool bBox = false; + bool bRowSpan = false; + bool bAltText = false; + + // Check which attributes to set: + + switch ( eType ) + { + case vcl::PDFWriter::Document : + bWritingMode = true; + break; + + case vcl::PDFWriter::Note: + bPlacement = true; + break; + + case vcl::PDFWriter::Table : + bPlacement = + bWritingMode = + bSpaceBefore = + bSpaceAfter = + bStartIndent = + bEndIndent = + bWidth = + bHeight = + bBox = true; + break; + + case vcl::PDFWriter::TableRow : + bPlacement = + bWritingMode = true; + break; + + case vcl::PDFWriter::TableHeader : + mpPDFExtOutDevData->SetStructureAttribute(vcl::PDFWriter::Scope, vcl::PDFWriter::Column); + [[fallthrough]]; + case vcl::PDFWriter::TableData : + bPlacement = + bWritingMode = + bWidth = + bHeight = + bRowSpan = true; + break; + + case vcl::PDFWriter::Caption: + if (pFrame->IsSctFrame()) + { + break; + } + [[fallthrough]]; + case vcl::PDFWriter::H1 : + case vcl::PDFWriter::H2 : + case vcl::PDFWriter::H3 : + case vcl::PDFWriter::H4 : + case vcl::PDFWriter::H5 : + case vcl::PDFWriter::H6 : + case vcl::PDFWriter::Paragraph : + case vcl::PDFWriter::Heading : + case vcl::PDFWriter::BlockQuote : + + bPlacement = + bWritingMode = + bSpaceBefore = + bSpaceAfter = + bStartIndent = + bEndIndent = + bTextIndent = + bTextAlign = true; + break; + + case vcl::PDFWriter::Formula : + case vcl::PDFWriter::Figure : + bAltText = + bPlacement = + bWidth = + bHeight = + bBox = true; + break; + + case vcl::PDFWriter::Division: + if (pFrame->IsFlyFrame()) // this can be something else too + { + bAltText = true; + bBox = true; + } + break; + + case vcl::PDFWriter::NonStructElement: + if (pFrame->IsHeaderFrame() || pFrame->IsFooterFrame()) + { + // ISO 14289-1:2014, Clause: 7.8 + mpPDFExtOutDevData->SetStructureAttribute(vcl::PDFWriter::Type, vcl::PDFWriter::Pagination); + mpPDFExtOutDevData->SetStructureAttribute(vcl::PDFWriter::Subtype, + pFrame->IsHeaderFrame() + ? vcl::PDFWriter::Header + : vcl::PDFWriter::Footer); + } + break; + + default : + break; + } + + // Set the attributes: + + if ( bPlacement ) + { + eVal = vcl::PDFWriter::TableHeader == eType || + vcl::PDFWriter::TableData == eType ? + vcl::PDFWriter::Inline : + vcl::PDFWriter::Block; + + mpPDFExtOutDevData->SetStructureAttribute( vcl::PDFWriter::Placement, eVal ); + } + + if ( bWritingMode ) + { + eVal = pFrame->IsVertical() ? + vcl::PDFWriter::TbRl : + pFrame->IsRightToLeft() ? + vcl::PDFWriter::RlTb : + vcl::PDFWriter::LrTb; + + if ( vcl::PDFWriter::LrTb != eVal ) + mpPDFExtOutDevData->SetStructureAttribute( vcl::PDFWriter::WritingMode, eVal ); + } + + if ( bSpaceBefore ) + { + nVal = aRectFnSet.GetTopMargin(*pFrame); + if ( 0 != nVal ) + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::SpaceBefore, nVal ); + } + + if ( bSpaceAfter ) + { + nVal = aRectFnSet.GetBottomMargin(*pFrame); + if ( 0 != nVal ) + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::SpaceAfter, nVal ); + } + + if ( bStartIndent ) + { + nVal = aRectFnSet.GetLeftMargin(*pFrame); + if ( 0 != nVal ) + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::StartIndent, nVal ); + } + + if ( bEndIndent ) + { + nVal = aRectFnSet.GetRightMargin(*pFrame); + if ( 0 != nVal ) + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::EndIndent, nVal ); + } + + if ( bTextIndent ) + { + OSL_ENSURE( pFrame->IsTextFrame(), "Frame type <-> tag attribute mismatch" ); + const SvxFirstLineIndentItem& rFirstLine( + static_cast<const SwTextFrame*>(pFrame)->GetTextNodeForParaProps()->GetSwAttrSet().GetFirstLineIndent()); + nVal = rFirstLine.GetTextFirstLineOffset(); + if ( 0 != nVal ) + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::TextIndent, nVal ); + } + + if ( bTextAlign ) + { + OSL_ENSURE( pFrame->IsTextFrame(), "Frame type <-> tag attribute mismatch" ); + const SwAttrSet& aSet = static_cast<const SwTextFrame*>(pFrame)->GetTextNodeForParaProps()->GetSwAttrSet(); + const SvxAdjust nAdjust = aSet.GetAdjust().GetAdjust(); + if ( SvxAdjust::Block == nAdjust || SvxAdjust::Center == nAdjust || + ( (pFrame->IsRightToLeft() && SvxAdjust::Left == nAdjust) || + (!pFrame->IsRightToLeft() && SvxAdjust::Right == nAdjust) ) ) + { + eVal = SvxAdjust::Block == nAdjust ? + vcl::PDFWriter::Justify : + SvxAdjust::Center == nAdjust ? + vcl::PDFWriter::Center : + vcl::PDFWriter::End; + + mpPDFExtOutDevData->SetStructureAttribute( vcl::PDFWriter::TextAlign, eVal ); + } + } + + // ISO 14289-1:2014, Clause: 7.3 + // ISO 14289-1:2014, Clause: 7.7 + // For images (but not embedded objects), an ObjectInfoPrimitive2D is + // created, but it's not evaluated by VclMetafileProcessor2D any more; + // that would require producing StructureTagPrimitive2D here but that + // looks impossible so instead duplicate the code that sets the Alt + // text here again. + if (bAltText) + { + SwFlyFrameFormat const& rFly(*static_cast<SwFlyFrame const*>(pFrame)->GetFormat()); + OUString const sep( + (rFly.GetObjTitle().isEmpty() || rFly.GetObjDescription().isEmpty()) + ? OUString() : OUString(" - ")); + OUString const altText(rFly.GetObjTitle() + sep + rFly.GetObjDescription()); + if (!altText.isEmpty()) + { + mpPDFExtOutDevData->SetAlternateText(altText); + } + } + + if ( bWidth ) + { + nVal = aRectFnSet.GetWidth(pFrame->getFrameArea()); + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::Width, nVal ); + } + + if ( bHeight ) + { + nVal = aRectFnSet.GetHeight(pFrame->getFrameArea()); + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::Height, nVal ); + } + + if ( bBox ) + { + // BBox only for non-split tables: + if ( vcl::PDFWriter::Table != eType || + ( pFrame->IsTabFrame() && + !static_cast<const SwTabFrame*>(pFrame)->IsFollow() && + !static_cast<const SwTabFrame*>(pFrame)->HasFollow() ) ) + { + mpPDFExtOutDevData->SetStructureBoundingBox(pFrame->getFrameArea().SVRect()); + } + } + + if ( bRowSpan ) + { + if ( pFrame->IsCellFrame() ) + { + const SwCellFrame* pThisCell = static_cast<const SwCellFrame*>(pFrame); + nVal = pThisCell->GetTabBox()->getRowSpan(); + if ( nVal > 1 ) + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::RowSpan, nVal ); + + // calculate colspan: + const SwTabFrame* pTabFrame = pThisCell->FindTabFrame(); + const SwTable* pTable = pTabFrame->GetTable(); + + SwRectFnSet fnRectX(pTabFrame); + + const TableColumnsMapEntry& rCols(mpPDFExtOutDevData->GetSwPDFState()->m_TableColumnsMap[pTable]); + + const tools::Long nLeft = fnRectX.GetLeft(pThisCell->getFrameArea()); + const tools::Long nRight = fnRectX.GetRight(pThisCell->getFrameArea()); + const TableColumnsMapEntry::const_iterator aLeftIter = rCols.find( nLeft ); + const TableColumnsMapEntry::const_iterator aRightIter = rCols.find( nRight ); + + OSL_ENSURE( aLeftIter != rCols.end() && aRightIter != rCols.end(), "Colspan trouble" ); + if ( aLeftIter != rCols.end() && aRightIter != rCols.end() ) + { + nVal = std::distance( aLeftIter, aRightIter ); + if ( nVal > 1 ) + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::ColSpan, nVal ); + } + } + } + + if (mpFrameInfo->m_isLink) + { + SwRect const aRect(mpFrameInfo->mrFrame.getFrameArea()); + LinkLinkLink(*mpPDFExtOutDevData, aRect); + } + } + + /* + * ATTRIBUTES FOR ILSE + */ + else if ( mpPorInfo ) + { + const SwLinePortion* pPor = &mpPorInfo->mrPor; + const SwTextPaintInfo& rInf = mpPorInfo->mrTextPainter.GetInfo(); + + bool bActualText = false; + bool bBaselineShift = false; + bool bTextDecorationType = false; + bool bLinkAttribute = false; + bool bLanguage = false; + + // Check which attributes to set: + + switch ( eType ) + { + case vcl::PDFWriter::Span : + case vcl::PDFWriter::Quote : + case vcl::PDFWriter::Code : + if( PortionType::HyphenStr == pPor->GetWhichPor() || PortionType::SoftHyphenStr == pPor->GetWhichPor() || + PortionType::Hyphen == pPor->GetWhichPor() || PortionType::SoftHyphen == pPor->GetWhichPor() ) + bActualText = true; + else + { + bBaselineShift = + bTextDecorationType = + bLanguage = true; + } + break; + + case vcl::PDFWriter::Link : + bTextDecorationType = + bBaselineShift = + bLinkAttribute = + bLanguage = true; + break; + + case vcl::PDFWriter::BibEntry : + bTextDecorationType = + bBaselineShift = + bLinkAttribute = + bLanguage = true; + break; + + case vcl::PDFWriter::RT: + { + SwRubyPortion const*const pRuby(static_cast<SwRubyPortion const*>(pPor)); + vcl::PDFWriter::StructAttributeValue nAlign = {}; + switch (pRuby->GetAdjustment()) + { + case text::RubyAdjust_LEFT: + nAlign = vcl::PDFWriter::RStart; + break; + case text::RubyAdjust_CENTER: + nAlign = vcl::PDFWriter::RCenter; + break; + case text::RubyAdjust_RIGHT: + nAlign = vcl::PDFWriter::REnd; + break; + case text::RubyAdjust_BLOCK: + nAlign = vcl::PDFWriter::RJustify; + break; + case text::RubyAdjust_INDENT_BLOCK: + nAlign = vcl::PDFWriter::RDistribute; + break; + default: + assert(false); + break; + } + ::std::optional<vcl::PDFWriter::StructAttributeValue> oPos; + switch (pRuby->GetRubyPosition()) + { + case RubyPosition::ABOVE: + oPos = vcl::PDFWriter::RBefore; + break; + case RubyPosition::BELOW: + oPos = vcl::PDFWriter::RAfter; + break; + case RubyPosition::RIGHT: + break; // no such thing??? + } + mpPDFExtOutDevData->SetStructureAttribute(vcl::PDFWriter::RubyAlign, nAlign); + if (oPos) + { + mpPDFExtOutDevData->SetStructureAttribute(vcl::PDFWriter::RubyPosition, *oPos); + } + } + break; + + default: + break; + } + + if ( bActualText ) + { + OUString aActualText; + if (pPor->GetWhichPor() == PortionType::SoftHyphen || pPor->GetWhichPor() == PortionType::Hyphen) + aActualText = OUString(u'\x00ad'); // soft hyphen + else + aActualText = rInf.GetText().copy(sal_Int32(rInf.GetIdx()), sal_Int32(pPor->GetLen())); + mpPDFExtOutDevData->SetActualText( aActualText ); + } + + if ( bBaselineShift ) + { + // TODO: Calculate correct values! + nVal = rInf.GetFont()->GetEscapement(); + if ( nVal > 0 ) nVal = 33; + else if ( nVal < 0 ) nVal = -33; + + if ( 0 != nVal ) + { + nVal = nVal * pPor->Height() / 100; + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::BaselineShift, nVal ); + } + } + + if ( bTextDecorationType ) + { + if ( LINESTYLE_NONE != rInf.GetFont()->GetUnderline() ) + mpPDFExtOutDevData->SetStructureAttribute( vcl::PDFWriter::TextDecorationType, vcl::PDFWriter::Underline ); + if ( LINESTYLE_NONE != rInf.GetFont()->GetOverline() ) + mpPDFExtOutDevData->SetStructureAttribute( vcl::PDFWriter::TextDecorationType, vcl::PDFWriter::Overline ); + if ( STRIKEOUT_NONE != rInf.GetFont()->GetStrikeout() ) + mpPDFExtOutDevData->SetStructureAttribute( vcl::PDFWriter::TextDecorationType, vcl::PDFWriter::LineThrough ); + if ( FontEmphasisMark::NONE != rInf.GetFont()->GetEmphasisMark() ) + mpPDFExtOutDevData->SetStructureAttribute( vcl::PDFWriter::TextDecorationType, vcl::PDFWriter::Overline ); + } + + if ( bLanguage ) + { + + const LanguageType nCurrentLanguage = rInf.GetFont()->GetLanguage(); + const LanguageType nDefaultLang(mpPDFExtOutDevData->GetSwPDFState()->m_eLanguageDefault); + + if ( nDefaultLang != nCurrentLanguage ) + mpPDFExtOutDevData->SetStructureAttributeNumerical( vcl::PDFWriter::Language, static_cast<sal_uInt16>(nCurrentLanguage) ); + } + + if ( bLinkAttribute ) + { + SwRect aPorRect; + rInf.CalcRect( *pPor, &aPorRect ); + LinkLinkLink(*mpPDFExtOutDevData, aPorRect); + } + } + else if (mpNumInfo && eType == vcl::PDFWriter::List) + { + SwTextFrame const& rFrame(mpNumInfo->mrFrame); + SwTextNode const& rNode(*rFrame.GetTextNodeForParaProps()); + SwNumRule const*const pNumRule = rNode.GetNumRule(); + assert(pNumRule); // was required for List + + auto ToPDFListNumbering = [](SvxNumberFormat const& rFormat) { + switch (rFormat.GetNumberingType()) + { + case css::style::NumberingType::CHARS_UPPER_LETTER: + return vcl::PDFWriter::UpperAlpha; + case css::style::NumberingType::CHARS_LOWER_LETTER: + return vcl::PDFWriter::LowerAlpha; + case css::style::NumberingType::ROMAN_UPPER: + return vcl::PDFWriter::UpperRoman; + case css::style::NumberingType::ROMAN_LOWER: + return vcl::PDFWriter::LowerRoman; + case css::style::NumberingType::ARABIC: + return vcl::PDFWriter::Decimal; + case css::style::NumberingType::CHAR_SPECIAL: + switch (rFormat.GetBulletChar()) + { + case u'\u2022': case u'\uE12C': case u'\uE01E': case u'\uE437': + return vcl::PDFWriter::Disc; + case u'\u2218': case u'\u25CB': case u'\u25E6': + return vcl::PDFWriter::Circle; + case u'\u25A0': case u'\u25AA': case u'\uE00A': + return vcl::PDFWriter::Square; + default: + return vcl::PDFWriter::NONE; + } + default: // the other 50 types + return vcl::PDFWriter::NONE; + } + }; + + // Note: for every level, BeginNumberedListStructureElements() produces + // a separate List element, so even though in PDF this is limited to + // the whole List we can just export the current level here. + vcl::PDFWriter::StructAttributeValue const value( + ToPDFListNumbering(pNumRule->Get(rNode.GetActualListLevel()))); + // ISO 14289-1:2014, Clause: 7.6 + mpPDFExtOutDevData->SetStructureAttribute(vcl::PDFWriter::ListNumbering, value); + } +} + +void SwTaggedPDFHelper::BeginNumberedListStructureElements() +{ + OSL_ENSURE( mpNumInfo, "List without mpNumInfo?" ); + if ( !mpNumInfo ) + return; + + const SwFrame& rFrame = mpNumInfo->mrFrame; + assert(rFrame.IsTextFrame()); + const SwTextFrame& rTextFrame = static_cast<const SwTextFrame&>(rFrame); + + // Lowers of NonStructureElements should not be considered: + if (lcl_IsInNonStructEnv(rTextFrame)) + return; + + // do it for the first one in the follow chain that has content + for (SwFlowFrame const* pPrecede = rTextFrame.GetPrecede(); pPrecede; pPrecede = pPrecede->GetPrecede()) + { + SwTextFrame const*const pText(static_cast<SwTextFrame const*>(pPrecede)); + if (!pText->HasPara() || pText->GetPara()->HasContentPortions()) + { + return; + } + } + + const SwTextNode *const pTextNd = rTextFrame.GetTextNodeForParaProps(); + const SwNumRule* pNumRule = pTextNd->GetNumRule(); + const SwNodeNum* pNodeNum = pTextNd->GetNum(rTextFrame.getRootFrame()); + + const bool bNumbered = !pTextNd->IsOutline() && pNodeNum && pNodeNum->GetParent() && pNumRule; + + // Check, if we have to reopen a list or a list body: + // First condition: + // Paragraph is numbered/bulleted + if ( !bNumbered ) + return; + + const SwNumberTreeNode* pParent = pNodeNum->GetParent(); + const bool bSameNumbering = lcl_HasPreviousParaSameNumRule(rTextFrame, *pTextNd); + + // Second condition: current numbering is not 'interrupted' + if ( bSameNumbering ) + { + sal_Int32 nReopenTag = -1; + + // Two cases: + // 1. We have to reopen an existing list body tag: + // - If the current node is either the first child of its parent + // and its level > 1 or + // - Numbering should restart at the current node and its level > 1 + // - The current item has no label + const bool bNewSubListStart = pParent->GetParent() && (pParent->IsFirst( pNodeNum ) || pTextNd->IsListRestart() ); + const bool bNoLabel = !pTextNd->IsCountedInList() && !pTextNd->IsListRestart(); + if ( bNewSubListStart || bNoLabel ) + { + // Fine, we try to reopen the appropriate list body + NumListBodyIdMap& rNumListBodyIdMap(mpPDFExtOutDevData->GetSwPDFState()->m_NumListBodyIdMap); + + if ( bNewSubListStart ) + { + // The list body tag associated with the parent has to be reopened + // to start a new list inside the list body + NumListBodyIdMap::const_iterator aIter; + + do + aIter = rNumListBodyIdMap.find( pParent ); + while ( aIter == rNumListBodyIdMap.end() && nullptr != ( pParent = pParent->GetParent() ) ); + + if ( aIter != rNumListBodyIdMap.end() ) + nReopenTag = (*aIter).second; + } + else // if(bNoLabel) + { + // The list body tag of a 'counted' predecessor has to be reopened + const SwNumberTreeNode* pPrevious = pNodeNum->GetPred(true); + while ( pPrevious ) + { + if ( pPrevious->IsCounted()) + { + // get id of list body tag + const NumListBodyIdMap::const_iterator aIter = rNumListBodyIdMap.find( pPrevious ); + if ( aIter != rNumListBodyIdMap.end() ) + { + nReopenTag = (*aIter).second; + break; + } + } + pPrevious = pPrevious->GetPred(true); + } + } + } + // 2. We have to reopen an existing list tag: + else if ( !pParent->IsFirst( pNodeNum ) && !pTextNd->IsListRestart() ) + { + // any other than the first node in a list level has to reopen the current + // list. The current list is associated in a map with the first child of the list: + NumListIdMap& rNumListIdMap(mpPDFExtOutDevData->GetSwPDFState()->m_NumListIdMap); + + // Search backwards and check if any of the previous nodes has a list associated with it: + const SwNumberTreeNode* pPrevious = pNodeNum->GetPred(true); + while ( pPrevious ) + { + // get id of list tag + const NumListIdMap::const_iterator aIter = rNumListIdMap.find( pPrevious ); + if ( aIter != rNumListIdMap.end() ) + { + nReopenTag = (*aIter).second; + break; + } + + pPrevious = pPrevious->GetPred(true); + } + } + + if ( -1 != nReopenTag ) + { + m_nRestoreCurrentTag = mpPDFExtOutDevData->GetCurrentStructureElement(); + mpPDFExtOutDevData->SetCurrentStructureElement( nReopenTag ); + +#if OSL_DEBUG_LEVEL > 1 + aStructStack.push_back( 99 ); +#endif + } + } + else + { + // clear list maps in case a list has been interrupted + NumListIdMap& rNumListIdMap(mpPDFExtOutDevData->GetSwPDFState()->m_NumListIdMap); + rNumListIdMap.clear(); + NumListBodyIdMap& rNumListBodyIdMap(mpPDFExtOutDevData->GetSwPDFState()->m_NumListBodyIdMap); + rNumListBodyIdMap.clear(); + } + + // New tags: + const bool bNewListTag = (pNodeNum->GetParent()->IsFirst( pNodeNum ) || pTextNd->IsListRestart() || !bSameNumbering); + const bool bNewItemTag = bNewListTag || pTextNd->IsCountedInList(); // If the text node is not counted, we do not start a new list item: + + if ( bNewListTag ) + BeginTag( vcl::PDFWriter::List, aListString ); + + if ( bNewItemTag ) + { + BeginTag( vcl::PDFWriter::ListItem, aListItemString ); + assert(rTextFrame.GetPara()); + // check whether to open LBody now or delay until after Lbl + if (!rTextFrame.GetPara()->HasNumberingPortion(SwParaPortion::OnlyNumbering)) + { + BeginTag(vcl::PDFWriter::LIBody, aListBodyString); + } + } +} + +void SwTaggedPDFHelper::BeginBlockStructureElements() +{ + const SwFrame* pFrame = &mpFrameInfo->mrFrame; + + // Lowers of NonStructureElements should not be considered: + + if (lcl_IsInNonStructEnv(*pFrame) && !pFrame->IsFlyFrame()) + return; + + // Check if we have to reopen an existing structure element. + // This has to be done e.g., if pFrame is a follow frame. + if ( CheckReopenTag() ) + return; + + sal_uInt16 nPDFType = USHRT_MAX; + OUString aPDFType; + + switch ( pFrame->GetType() ) + { + /* + * GROUPING ELEMENTS + */ + + case SwFrameType::Page : + + // Document: Document + + nPDFType = vcl::PDFWriter::Document; + aPDFType = aDocumentString; + break; + + case SwFrameType::Header : + case SwFrameType::Footer : + + // Header, Footer: NonStructElement + + nPDFType = vcl::PDFWriter::NonStructElement; + break; + + case SwFrameType::FtnCont : + + // Footnote container: Division + + nPDFType = vcl::PDFWriter::Division; + aPDFType = aDivString; + break; + + case SwFrameType::Ftn : + + // Footnote frame: Note + + // Note: vcl::PDFWriter::Note is actually a ILSE. Nevertheless + // we treat it like a grouping element! + nPDFType = vcl::PDFWriter::Note; + aPDFType = aNoteString; + break; + + case SwFrameType::Section : + + // Section: TOX, Index, or Sect + + { + const SwSection* pSection = + static_cast<const SwSectionFrame*>(pFrame)->GetSection(); + + // open all parent sections, so that the SEs of sections + // are nested in the same way as their SwSectionNodes + std::vector<SwSection const*> parents; + for (SwSection const* pParent = pSection->GetParent(); + pParent != nullptr; pParent = pParent->GetParent()) + { + parents.push_back(pParent); + } + for (auto it = parents.rbegin(); it != parents.rend(); ++it) + { + // key is the SwSection - see lcl_GetKeyFromFrame() + OpenTagImpl(*it); + } + + FrameTagSet& rFrameTagSet(mpPDFExtOutDevData->GetSwPDFState()->m_FrameTagSet); + if (rFrameTagSet.find(pSection) != rFrameTagSet.end()) + { + // special case: section may have *multiple* master frames, + // when it is interrupted by nested section - reopen! + OpenTagImpl(pSection); + break; + } + else if (SectionType::ToxHeader == pSection->GetType()) + { + nPDFType = vcl::PDFWriter::Caption; + aPDFType = aCaptionString; + } + else if (SectionType::ToxContent == pSection->GetType()) + { + const SwTOXBase* pTOXBase = pSection->GetTOXBase(); + if ( pTOXBase ) + { + if ( TOX_INDEX == pTOXBase->GetType() ) + { + nPDFType = vcl::PDFWriter::Index; + aPDFType = aIndexString; + } + else + { + nPDFType = vcl::PDFWriter::TOC; + aPDFType = aTOCString; + } + } + } + else if ( SectionType::Content == pSection->GetType() ) + { + nPDFType = vcl::PDFWriter::Section; + aPDFType = aSectString; + } + } + break; + + /* + * BLOCK-LEVEL STRUCTURE ELEMENTS + */ + + case SwFrameType::Txt : + { + SwTextFrame const& rTextFrame(*static_cast<const SwTextFrame*>(pFrame)); + const SwTextNode *const pTextNd(rTextFrame.GetTextNodeForParaProps()); + + // lazy open LBody after Lbl + if (!pTextNd->IsOutline() + && rTextFrame.GetPara()->HasNumberingPortion(SwParaPortion::OnlyNumbering)) + { + sal_Int32 const nId = BeginTagImpl(nullptr, vcl::PDFWriter::LIBody, aListBodyString); + SwNodeNum const*const pNodeNum(pTextNd->GetNum(rTextFrame.getRootFrame())); + NumListBodyIdMap& rNumListBodyIdMap(mpPDFExtOutDevData->GetSwPDFState()->m_NumListBodyIdMap); + rNumListBodyIdMap[ pNodeNum ] = nId; + } + + const SwFormat* pTextFormat = pTextNd->GetFormatColl(); + const SwFormat* pParentTextFormat = pTextFormat ? pTextFormat->DerivedFrom() : nullptr; + + OUString sStyleName; + OUString sParentStyleName; + + if ( pTextFormat) + SwStyleNameMapper::FillProgName( pTextFormat->GetName(), sStyleName, SwGetPoolIdFromName::TxtColl ); + if ( pParentTextFormat) + SwStyleNameMapper::FillProgName( pParentTextFormat->GetName(), sParentStyleName, SwGetPoolIdFromName::TxtColl ); + + // This is the default. If the paragraph could not be mapped to + // any of the standard pdf tags, we write a user defined tag + // <stylename> with role = P + nPDFType = vcl::PDFWriter::Paragraph; + aPDFType = sStyleName; + + // Quotations: BlockQuote + + if (sStyleName == aQuotations) + { + nPDFType = vcl::PDFWriter::BlockQuote; + aPDFType = aBlockQuoteString; + } + + // Caption: Caption + + else if (sStyleName == aCaption) + { + nPDFType = vcl::PDFWriter::Caption; + aPDFType = aCaptionString; + } + + // Caption: Caption + + else if (sParentStyleName == aCaption) + { + nPDFType = vcl::PDFWriter::Caption; + aPDFType = sStyleName + aCaptionString; + } + + // Heading: H + + else if (sStyleName == aHeading) + { + nPDFType = vcl::PDFWriter::Heading; + aPDFType = aHString; + } + + // Heading: H1 - H6 + + if (int nRealLevel = pTextNd->GetAttrOutlineLevel() - 1; + nRealLevel >= 0 + && !pTextNd->IsInRedlines() + && sw::IsParaPropsNode(*pFrame->getRootFrame(), *pTextNd)) + { + switch(nRealLevel) + { + case 0 : + aPDFType = aH1String; + break; + case 1 : + aPDFType = aH2String; + break; + case 2 : + aPDFType = aH3String; + break; + case 3 : + aPDFType = aH4String; + break; + case 4 : + aPDFType = aH5String; + break; + case 5: + aPDFType = aH6String; + break; + case 6: + aPDFType = aH7String; + break; + case 7: + aPDFType = aH8String; + break; + case 8: + aPDFType = aH9String; + break; + case 9: + aPDFType = aH10String; + break; + default: + assert(false); + break; + } + + // PDF/UA allows unlimited headings, but PDF only up to H6 + // ... and apparently the extra H7.. must be declared in + // RoleMap, or veraPDF complains. + nRealLevel = std::min(nRealLevel, 5); + nPDFType = o3tl::narrowing<sal_uInt16>(vcl::PDFWriter::H1 + nRealLevel); + } + + // Section: TOCI + + else if ( pFrame->IsInSct() ) + { + const SwSectionFrame* pSctFrame = pFrame->FindSctFrame(); + const SwSection* pSection = pSctFrame->GetSection(); + + if ( SectionType::ToxContent == pSection->GetType() ) + { + const SwTOXBase* pTOXBase = pSection->GetTOXBase(); + if ( pTOXBase && TOX_INDEX != pTOXBase->GetType() ) + { + // Special case: Open additional TOCI tag: + BeginTagImpl(nullptr, vcl::PDFWriter::TOCI, aTOCIString); + } + } + } + } + break; + + case SwFrameType::Tab : + + // TabFrame: Table + + nPDFType = vcl::PDFWriter::Table; + aPDFType = aTableString; + + { + // set up table column data: + const SwTabFrame* pTabFrame = static_cast<const SwTabFrame*>(pFrame); + const SwTable* pTable = pTabFrame->GetTable(); + + TableColumnsMap& rTableColumnsMap(mpPDFExtOutDevData->GetSwPDFState()->m_TableColumnsMap); + const TableColumnsMap::const_iterator aIter = rTableColumnsMap.find( pTable ); + + if ( aIter == rTableColumnsMap.end() ) + { + SwRectFnSet aRectFnSet(pTabFrame); + TableColumnsMapEntry& rCols = rTableColumnsMap[ pTable ]; + + const SwTabFrame* pMasterFrame = pTabFrame->IsFollow() ? pTabFrame->FindMaster( true ) : pTabFrame; + + while ( pMasterFrame ) + { + const SwRowFrame* pRowFrame = static_cast<const SwRowFrame*>(pMasterFrame->GetLower()); + + while ( pRowFrame ) + { + const SwFrame* pCellFrame = pRowFrame->GetLower(); + + const tools::Long nLeft = aRectFnSet.GetLeft(pCellFrame->getFrameArea()); + rCols.insert( nLeft ); + + while ( pCellFrame ) + { + const tools::Long nRight = aRectFnSet.GetRight(pCellFrame->getFrameArea()); + rCols.insert( nRight ); + pCellFrame = pCellFrame->GetNext(); + } + pRowFrame = static_cast<const SwRowFrame*>(pRowFrame->GetNext()); + } + pMasterFrame = pMasterFrame->GetFollow(); + } + } + } + + break; + + /* + * TABLE ELEMENTS + */ + + case SwFrameType::Row : + + // RowFrame: TR + + if ( !static_cast<const SwRowFrame*>(pFrame)->IsRepeatedHeadline() ) + { + nPDFType = vcl::PDFWriter::TableRow; + aPDFType = aTRString; + } + else + { + nPDFType = vcl::PDFWriter::NonStructElement; + } + break; + + case SwFrameType::Cell : + + // CellFrame: TH, TD + + { + const SwTabFrame* pTable = static_cast<const SwCellFrame*>(pFrame)->FindTabFrame(); + if ( pTable->IsInHeadline( *pFrame ) || lcl_IsHeadlineCell( *static_cast<const SwCellFrame*>(pFrame) ) ) + { + nPDFType = vcl::PDFWriter::TableHeader; + aPDFType = aTHString; + } + else + { + nPDFType = vcl::PDFWriter::TableData; + aPDFType = aTDString; + } + } + break; + + /* + * ILLUSTRATION + */ + + case SwFrameType::Fly : + + // FlyFrame: Figure, Formula, Control + // fly in content or fly at page + if (mpFrameInfo->m_isLink) + { // tdf#154939 additional inner link element for flys + nPDFType = vcl::PDFWriter::Link; + aPDFType = aLinkString; + } + else + { + const SwFlyFrame* pFly = static_cast<const SwFlyFrame*>(pFrame); + if (pFly->GetAnchorFrame()->FindFooterOrHeader() != nullptr + || pFly->GetFrameFormat().GetAttrSet().Get(RES_DECORATIVE).GetValue()) + { + nPDFType = vcl::PDFWriter::NonStructElement; + } + else if (pFly->Lower() && pFly->Lower()->IsNoTextFrame()) + { + bool bFormula = false; + + const SwNoTextFrame* pNoTextFrame = static_cast<const SwNoTextFrame*>(pFly->Lower()); + SwOLENode* pOLENd = const_cast<SwOLENode*>(pNoTextFrame->GetNode()->GetOLENode()); + if ( pOLENd ) + { + SwOLEObj& aOLEObj = pOLENd->GetOLEObj(); + uno::Reference< embed::XEmbeddedObject > aRef = aOLEObj.GetOleRef(); + if ( aRef.is() ) + { + bFormula = 0 != SotExchange::IsMath( SvGlobalName( aRef->getClassID() ) ); + } + } + if ( bFormula ) + { + nPDFType = vcl::PDFWriter::Formula; + aPDFType = aFormulaString; + } + else + { + nPDFType = vcl::PDFWriter::Figure; + aPDFType = aFigureString; + } + } + else + { + nPDFType = vcl::PDFWriter::Division; + aPDFType = aDivString; + } + } + break; + + default: break; + } + + if ( USHRT_MAX != nPDFType ) + { + BeginTag( static_cast<vcl::PDFWriter::StructElement>(nPDFType), aPDFType ); + } +} + +void SwTaggedPDFHelper::EndStructureElements() +{ + if (mpFrameInfo != nullptr) + { + if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan) + { // close span at end of paragraph + mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan.reset(); + ++m_nEndStructureElement; + } + if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink) + { // close link at end of paragraph + mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.reset(); + ++m_nEndStructureElement; + } + } + + while ( m_nEndStructureElement > 0 ) + { + EndTag(); + --m_nEndStructureElement; + } + + CheckRestoreTag(); +} + +void SwTaggedPDFHelper::EndCurrentLink(OutputDevice const& rOut) +{ + vcl::PDFExtOutDevData *const pPDFExtOutDevData( + dynamic_cast<vcl::PDFExtOutDevData *>(rOut.GetExtOutDevData())); + if (pPDFExtOutDevData && pPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink) + { + pPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.reset(); + pPDFExtOutDevData->EndStructureElement(); +#if OSL_DEBUG_LEVEL > 1 + aStructStack.pop_back(); +#endif + } +} + +void SwTaggedPDFHelper::EndCurrentAll() +{ + if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan) + { + mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan.reset(); + } + if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink) + { + mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.reset(); + } +} + +void SwTaggedPDFHelper::EndCurrentSpan() +{ + mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan.reset(); + EndTag(); // close span +} + +void SwTaggedPDFHelper::CreateCurrentSpan( + SwTextPaintInfo const& rInf, OUString const& rStyleName) +{ + assert(!mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan); + mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan.emplace( + SwEnhancedPDFState::Span{ + rInf.GetFont()->GetUnderline(), + rInf.GetFont()->GetOverline(), + rInf.GetFont()->GetStrikeout(), + rInf.GetFont()->GetEmphasisMark(), + rInf.GetFont()->GetEscapement(), + rInf.GetFont()->GetActual(), + rInf.GetFont()->GetLanguage(), + rStyleName}); + // leave it open to let next portion decide to merge or close + --m_nEndStructureElement; +} + +bool SwTaggedPDFHelper::CheckContinueSpan( + SwTextPaintInfo const& rInf, std::u16string_view const rStyleName, + SwTextAttr const*const pInetFormatAttr) +{ + // for now, don't create span inside of link - this should be very rare + // situation and it looks complicated to implement. + assert(!mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan + || !mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink); + if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink) + { + if (pInetFormatAttr && pInetFormatAttr == *mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink) + { + return true; + } + else + { + mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.reset(); + EndTag(); + return false; + } + } + if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan && pInetFormatAttr) + { + EndCurrentSpan(); + return false; + } + + if (!mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan) + return false; + + SwEnhancedPDFState::Span const& rCurrent(*mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentSpan); + + bool const ret(rCurrent.eUnderline == rInf.GetFont()->GetUnderline() + && rCurrent.eOverline == rInf.GetFont()->GetOverline() + && rCurrent.eStrikeout == rInf.GetFont()->GetStrikeout() + && rCurrent.eFontEmphasis == rInf.GetFont()->GetEmphasisMark() + && rCurrent.nEscapement == rInf.GetFont()->GetEscapement() + && rCurrent.nScript == rInf.GetFont()->GetActual() + && rCurrent.nLang == rInf.GetFont()->GetLanguage() + && rCurrent.StyleName == rStyleName); + if (!ret) + { + EndCurrentSpan(); + } + return ret; +} + +void SwTaggedPDFHelper::BeginInlineStructureElements() +{ + const SwLinePortion* pPor = &mpPorInfo->mrPor; + const SwTextPaintInfo& rInf = mpPorInfo->mrTextPainter.GetInfo(); + const SwTextFrame* pFrame = rInf.GetTextFrame(); + + // Lowers of NonStructureElements should not be considered: + + if ( lcl_IsInNonStructEnv( *pFrame ) ) + return; + + std::pair<SwTextNode const*, sal_Int32> const pos( + pFrame->MapViewToModel(rInf.GetIdx())); + SwTextAttr const*const pInetFormatAttr = + pos.first->GetTextAttrAt(pos.second, RES_TXTATR_INETFMT); + + OUString sStyleName; + if (!pInetFormatAttr) + { + std::vector<SwTextAttr *> const charAttrs( + pos.first->GetTextAttrsAt(pos.second, RES_TXTATR_CHARFMT)); + // TODO: handle more than 1 char style? + const SwCharFormat* pCharFormat = (charAttrs.size()) + ? (*charAttrs.begin())->GetCharFormat().GetCharFormat() : nullptr; + if (pCharFormat) + SwStyleNameMapper::FillProgName( pCharFormat->GetName(), sStyleName, SwGetPoolIdFromName::TxtColl ); + } + + // note: ILSE may be nested, so only end the span if needed to start new one + bool const isContinueSpan(CheckContinueSpan(rInf, sStyleName, pInetFormatAttr)); + + sal_uInt16 nPDFType = USHRT_MAX; + OUString aPDFType; + + switch ( pPor->GetWhichPor() ) + { + case PortionType::Hyphen : + case PortionType::SoftHyphen : + // Check for alternative spelling: + case PortionType::HyphenStr : + case PortionType::SoftHyphenStr : + nPDFType = vcl::PDFWriter::Span; + aPDFType = aSpanString; + break; + + case PortionType::Fly: + // if a link is split by a fly overlap, then there will be multiple + // annotations for the link, and hence there must be multiple SEs, + // so every annotation has its own SE. + if (mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink) + { + mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.reset(); + EndTag(); + } + break; + + case PortionType::Lay : + case PortionType::Text : + case PortionType::Para : + { + // Check for Link: + if( pInetFormatAttr ) + { + if (!isContinueSpan) + { + nPDFType = vcl::PDFWriter::Link; + aPDFType = aLinkString; + assert(!mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink); + mpPDFExtOutDevData->GetSwPDFState()->m_oCurrentLink.emplace(pInetFormatAttr); + // leave it open to let next portion decide to merge or close + --m_nEndStructureElement; + } + } + // Check for Quote/Code character style: + else if (sStyleName == aQuotation) + { + if (!isContinueSpan) + { + nPDFType = vcl::PDFWriter::Quote; + aPDFType = aQuoteString; + CreateCurrentSpan(rInf, sStyleName); + } + } + else if (sStyleName == aSourceText) + { + if (!isContinueSpan) + { + nPDFType = vcl::PDFWriter::Code; + aPDFType = aCodeString; + CreateCurrentSpan(rInf, sStyleName); + } + } + else if (!isContinueSpan) + { + const LanguageType nCurrentLanguage = rInf.GetFont()->GetLanguage(); + const SwFontScript nFont = rInf.GetFont()->GetActual(); + const LanguageType nDefaultLang(mpPDFExtOutDevData->GetSwPDFState()->m_eLanguageDefault); + + if ( LINESTYLE_NONE != rInf.GetFont()->GetUnderline() || + LINESTYLE_NONE != rInf.GetFont()->GetOverline() || + STRIKEOUT_NONE != rInf.GetFont()->GetStrikeout() || + FontEmphasisMark::NONE != rInf.GetFont()->GetEmphasisMark() || + 0 != rInf.GetFont()->GetEscapement() || + SwFontScript::Latin != nFont || + nCurrentLanguage != nDefaultLang || + !sStyleName.isEmpty()) + { + nPDFType = vcl::PDFWriter::Span; + if (!sStyleName.isEmpty()) + aPDFType = sStyleName; + else + aPDFType = aSpanString; + CreateCurrentSpan(rInf, sStyleName); + } + } + } + break; + + case PortionType::Footnote : + nPDFType = vcl::PDFWriter::Link; + aPDFType = aLinkString; + break; + + case PortionType::Field : + { + // check field type: + TextFrameIndex const nIdx = static_cast<const SwFieldPortion*>(pPor)->IsFollow() + ? rInf.GetIdx() - TextFrameIndex(1) + : rInf.GetIdx(); + const SwTextAttr* pHint = mpPorInfo->mrTextPainter.GetAttr( nIdx ); + if ( pHint && RES_TXTATR_FIELD == pHint->Which() ) + { + const SwField* pField = pHint->GetFormatField().GetField(); + if ( SwFieldIds::GetRef == pField->Which() ) + { + nPDFType = vcl::PDFWriter::Link; + aPDFType = aLinkString; + } + else if ( SwFieldIds::TableOfAuthorities == pField->Which() ) + { + nPDFType = vcl::PDFWriter::BibEntry; + aPDFType = aBibEntryString; + } + } + } + break; + + case PortionType::Multi: + { + SwMultiPortion const*const pMulti(static_cast<SwMultiPortion const*>(pPor)); + if (pMulti->IsRuby()) + { + EndCurrentAll(); + switch (mpPorInfo->m_Mode) + { + case 0: + nPDFType = vcl::PDFWriter::Ruby; + aPDFType = "Ruby"; + break; + case 1: + nPDFType = vcl::PDFWriter::RT; + aPDFType = "RT"; + break; + case 2: + nPDFType = vcl::PDFWriter::RB; + aPDFType = "RB"; + break; + } + } + else if (pMulti->IsDouble()) + { + EndCurrentAll(); + switch (mpPorInfo->m_Mode) + { + case 0: + nPDFType = vcl::PDFWriter::Warichu; + aPDFType = "Warichu"; + break; + case 1: + nPDFType = vcl::PDFWriter::WP; + aPDFType = "WP"; + break; + case 2: + nPDFType = vcl::PDFWriter::WT; + aPDFType = "WT"; + break; + } + } + } + break; + + + // for FootnoteNum, is called twice: outer generates Lbl, inner Link + case PortionType::FootnoteNum: + assert(!isContinueSpan); // is at start + if (mpPorInfo->m_Mode == 0) + { // tdf#152218 link both directions + nPDFType = vcl::PDFWriter::Link; + aPDFType = aLinkString; + break; + } + [[fallthrough]]; + case PortionType::Number: + case PortionType::Bullet: + case PortionType::GrfNum: + assert(!isContinueSpan); // is at start + if (mpPorInfo->m_Mode == 1) + { // only works for multiple lines via wrapper from PaintSwFrame + nPDFType = vcl::PDFWriter::LILabel; + aPDFType = aListLabelString; + } + break; + + case PortionType::Tab : + case PortionType::TabRight : + case PortionType::TabCenter : + case PortionType::TabDecimal : + nPDFType = vcl::PDFWriter::NonStructElement; + break; + default: break; + } + + if ( USHRT_MAX != nPDFType ) + { + BeginTag( static_cast<vcl::PDFWriter::StructElement>(nPDFType), aPDFType ); + } +} + +bool SwTaggedPDFHelper::IsExportTaggedPDF( const OutputDevice& rOut ) +{ + vcl::PDFExtOutDevData* pPDFExtOutDevData = dynamic_cast< vcl::PDFExtOutDevData*>( rOut.GetExtOutDevData() ); + return pPDFExtOutDevData && pPDFExtOutDevData->GetIsExportTaggedPDF(); +} + +SwEnhancedPDFExportHelper::SwEnhancedPDFExportHelper( SwEditShell& rSh, + OutputDevice& rOut, + const OUString& rPageRange, + bool bSkipEmptyPages, + bool bEditEngineOnly, + const SwPrintData& rPrintData ) + : mrSh( rSh ), + mrOut( rOut ), + mbSkipEmptyPages( bSkipEmptyPages ), + mbEditEngineOnly( bEditEngineOnly ), + mrPrintData( rPrintData ) +{ + if ( !rPageRange.isEmpty() ) + mpRangeEnum.reset( new StringRangeEnumerator( rPageRange, 0, mrSh.GetPageCount()-1 ) ); + + if ( mbSkipEmptyPages ) + { + maPageNumberMap.resize( mrSh.GetPageCount() ); + const SwPageFrame* pCurrPage = + static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + sal_Int32 nPageNumber = 0; + for ( size_t i = 0, n = maPageNumberMap.size(); i < n && pCurrPage; ++i ) + { + if ( pCurrPage->IsEmptyPage() ) + maPageNumberMap[i] = -1; + else + maPageNumberMap[i] = nPageNumber++; + + pCurrPage = static_cast<const SwPageFrame*>( pCurrPage->GetNext() ); + } + } + +#if OSL_DEBUG_LEVEL > 1 + aStructStack.clear(); +#endif + + const sal_Int16 nScript = SvtLanguageOptions::GetI18NScriptTypeOfLanguage( GetAppLanguage() ); + TypedWhichId<SvxLanguageItem> nLangRes = RES_CHRATR_LANGUAGE; + + if ( i18n::ScriptType::ASIAN == nScript ) + nLangRes = RES_CHRATR_CJK_LANGUAGE; + else if ( i18n::ScriptType::COMPLEX == nScript ) + nLangRes = RES_CHRATR_CTL_LANGUAGE; + + const SvxLanguageItem& rLangItem = mrSh.GetDoc()->GetDefault( nLangRes ); + auto const eLanguageDefault = rLangItem.GetLanguage(); + + EnhancedPDFExport(eLanguageDefault); +} + +SwEnhancedPDFExportHelper::~SwEnhancedPDFExportHelper() +{ +} + +tools::Rectangle SwEnhancedPDFExportHelper::SwRectToPDFRect(const SwPageFrame* pCurrPage, + const tools::Rectangle& rRectangle) const +{ + if (!::sw::IsShrinkPageForPostIts(mrSh, mrPrintData)) // tdf#148729 + { + return rRectangle; + } + //the page has been scaled by 75% and vertically centered, so adjust these + //rectangles equivalently + tools::Rectangle aRect(rRectangle); + Size aRectSize(aRect.GetSize()); + double fScale = 0.75; + aRectSize.setWidth( aRectSize.Width() * fScale ); + aRectSize.setHeight( aRectSize.Height() * fScale ); + tools::Long nOrigHeight = pCurrPage->getFrameArea().Height(); + tools::Long nNewHeight = nOrigHeight*fScale; + tools::Long nShiftY = (nOrigHeight-nNewHeight)/2; + aRect.SetLeft( aRect.Left() * fScale ); + aRect.SetTop( aRect.Top() * fScale ); + aRect.Move(0, nShiftY); + aRect.SetSize(aRectSize); + return aRect; +} + +void SwEnhancedPDFExportHelper::EnhancedPDFExport(LanguageType const eLanguageDefault) +{ + vcl::PDFExtOutDevData* pPDFExtOutDevData = + dynamic_cast< vcl::PDFExtOutDevData*>( mrOut.GetExtOutDevData() ); + + if ( !pPDFExtOutDevData ) + return; + + // set the document locale + + lang::Locale const aDocLocale( LanguageTag(eLanguageDefault).getLocale() ); + pPDFExtOutDevData->SetDocumentLocale( aDocLocale ); + + // Prepare the output device: + + mrOut.Push( vcl::PushFlags::MAPMODE ); + MapMode aMapMode( mrOut.GetMapMode() ); + aMapMode.SetMapUnit( MapUnit::MapTwip ); + mrOut.SetMapMode( aMapMode ); + + // Create new cursor and lock the view: + + SwDoc* pDoc = mrSh.GetDoc(); + mrSh.SwCursorShell::Push(); + mrSh.SwCursorShell::ClearMark(); + const bool bOldLockView = mrSh.IsViewLocked(); + mrSh.LockView( true ); + + if ( !mbEditEngineOnly ) + { + assert(pPDFExtOutDevData->GetSwPDFState() == nullptr); + pPDFExtOutDevData->SetSwPDFState(new SwEnhancedPDFState(eLanguageDefault)); + + // POSTITS + + if ( pPDFExtOutDevData->GetIsExportNotes() ) + { + std::vector<SwFormatField*> vpFields; + mrSh.GetFieldType(SwFieldIds::Postit, OUString())->GatherFields(vpFields); + for(auto pFormatField : vpFields) + { + const SwTextNode* pTNd = pFormatField->GetTextField()->GetpTextNode(); + OSL_ENSURE(nullptr != pTNd, "Enhanced pdf export - text node is missing"); + if(!lcl_TryMoveToNonHiddenField(mrSh, *pTNd, *pFormatField)) + continue; + // Link Rectangle + const SwRect& rNoteRect = mrSh.GetCharRect(); + const SwPageFrame* pCurrPage = static_cast<const SwPageFrame*>(mrSh.GetLayout()->Lower()); + + // Link PageNums + std::vector<sal_Int32> aNotePageNums = CalcOutputPageNums(rNoteRect); + for (sal_Int32 aNotePageNum : aNotePageNums) + { + + // Use the NumberFormatter to get the date string: + const SwPostItField* pField = static_cast<SwPostItField*>(pFormatField->GetField()); + SvNumberFormatter* pNumFormatter = pDoc->GetNumberFormatter(); + const Date aDateDiff(pField->GetDate() - pNumFormatter->GetNullDate()); + const sal_uLong nFormat = pNumFormatter->GetStandardFormat(SvNumFormatType::DATE, pField->GetLanguage()); + OUString sDate; + const Color* pColor; + pNumFormatter->GetOutputString(aDateDiff.GetDate(), nFormat, sDate, &pColor); + + vcl::PDFNote aNote; + // The title should consist of the author and the date: + aNote.Title = pField->GetPar1() + ", " + sDate + ", " + (pField->GetResolved() ? SwResId(STR_RESOLVED) : ""); + // Guess what the contents contains... + aNote.Contents = pField->GetText(); + + // Link Export + tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rNoteRect.SVRect())); + pPDFExtOutDevData->CreateNote(aRect, aNote, aNotePageNum); + } + mrSh.SwCursorShell::ClearMark(); + } + } + + // HYPERLINKS + + SwGetINetAttrs aArr; + mrSh.GetINetAttrs( aArr ); + for( auto &rAttr : aArr ) + { + SwGetINetAttr* p = &rAttr; + OSL_ENSURE( nullptr != p, "Enhanced pdf export - SwGetINetAttr is missing" ); + + const SwTextNode* pTNd = p->rINetAttr.GetpTextNode(); + OSL_ENSURE( nullptr != pTNd, "Enhanced pdf export - text node is missing" ); + + // 1. Check if the whole paragraph is hidden + // 2. Move to the hyperlink + // 3. Check for hidden text attribute + if ( !pTNd->IsHidden() && + mrSh.GotoINetAttr( p->rINetAttr ) && + !mrSh.IsInHiddenRange(/*bSelect=*/false) ) + { + // Select the hyperlink: + mrSh.SwCursorShell::Right( 1, SwCursorSkipMode::Chars ); + if ( mrSh.SwCursorShell::SelectTextAttr( RES_TXTATR_INETFMT, true ) ) + { + // First, we create the destination, because there may be more + // than one link to this destination: + OUString aURL( INetURLObject::decode( + p->rINetAttr.GetINetFormat().GetValue(), + INetURLObject::DecodeMechanism::Unambiguous ) ); + + // We have to distinguish between internal and real URLs + const bool bInternal = '#' == aURL[0]; + + // GetCursor_() is a SwShellCursor, which is derived from + // SwSelPaintRects, therefore the rectangles of the current + // selection can be easily obtained: + // Note: We make a copy of the rectangles, because they may + // be deleted again in JumpToSwMark. + SwRects const aTmp(GetCursorRectsContainingText(mrSh)); + OSL_ENSURE( !aTmp.empty(), "Enhanced pdf export - rectangles are missing" ); + OUString const altText(mrSh.GetSelText()); + + const SwPageFrame* pSelectionPage = + static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + + // Create the destination for internal links: + sal_Int32 nDestId = -1; + if ( bInternal ) + { + aURL = aURL.copy( 1 ); + mrSh.SwCursorShell::ClearMark(); + if (! JumpToSwMark( &mrSh, aURL )) + { + continue; // target deleted + } + + // Destination Rectangle + const SwRect& rDestRect = mrSh.GetCharRect(); + + const SwPageFrame* pCurrPage = + static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + + // Destination PageNum + const sal_Int32 nDestPageNum = CalcOutputPageNum( rDestRect ); + + // Destination Export + if ( -1 != nDestPageNum ) + { + tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect())); + nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum); + } + } + + if ( !bInternal || -1 != nDestId ) + { + // #i44368# Links in Header/Footer + const bool bHeaderFooter = pDoc->IsInHeaderFooter( *pTNd ); + + // Create links for all selected rectangles: + const size_t nNumOfRects = aTmp.size(); + for ( size_t i = 0; i < nNumOfRects; ++i ) + { + // Link Rectangle + const SwRect& rLinkRect( aTmp[ i ] ); + + // Link PageNums + std::vector<sal_Int32> aLinkPageNums = CalcOutputPageNums( rLinkRect ); + + for (sal_Int32 aLinkPageNum : aLinkPageNums) + { + // Link Export + tools::Rectangle aRect(SwRectToPDFRect(pSelectionPage, rLinkRect.SVRect())); + const sal_Int32 nLinkId = + pPDFExtOutDevData->CreateLink(aRect, altText, aLinkPageNum); + + // Store link info for tagged pdf output: + const IdMapEntry aLinkEntry( rLinkRect, nLinkId ); + pPDFExtOutDevData->GetSwPDFState()->m_LinkIdMap.push_back(aLinkEntry); + + // Connect Link and Destination: + if ( bInternal ) + pPDFExtOutDevData->SetLinkDest( nLinkId, nDestId ); + else + pPDFExtOutDevData->SetLinkURL( nLinkId, aURL ); + + // #i44368# Links in Header/Footer + if ( bHeaderFooter ) + MakeHeaderFooterLinks(*pPDFExtOutDevData, *pTNd, rLinkRect, nDestId, aURL, bInternal, altText); + } + } + } + } + } + mrSh.SwCursorShell::ClearMark(); + } + + // HYPERLINKS (Graphics, Frames, OLEs ) + + for(sw::SpzFrameFormat* pFrameFormat: *pDoc->GetSpzFrameFormats()) + { + const SwFormatURL* pItem; + if ( RES_DRAWFRMFMT != pFrameFormat->Which() && + GetFrameOfModify(mrSh.GetLayout(), *pFrameFormat, SwFrameType::Fly) && + (pItem = pFrameFormat->GetAttrSet().GetItemIfSet( RES_URL )) ) + { + const SwPageFrame* pCurrPage = + static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + + OUString aURL( pItem->GetURL() ); + if (aURL.isEmpty()) + continue; + const bool bInternal = '#' == aURL[0]; + + // Create the destination for internal links: + sal_Int32 nDestId = -1; + if ( bInternal ) + { + aURL = aURL.copy( 1 ); + mrSh.SwCursorShell::ClearMark(); + if (! JumpToSwMark( &mrSh, aURL )) + { + continue; // target deleted + } + + // Destination Rectangle + const SwRect& rDestRect = mrSh.GetCharRect(); + + pCurrPage = static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + + // Destination PageNum + const sal_Int32 nDestPageNum = CalcOutputPageNum( rDestRect ); + + // Destination Export + if ( -1 != nDestPageNum ) + { + tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect())); + nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum); + } + } + + if ( !bInternal || -1 != nDestId ) + { + Point aNullPt; + const SwRect aLinkRect = pFrameFormat->FindLayoutRect( false, &aNullPt ); + OUString const formatName(pFrameFormat->GetName()); + // Link PageNums + std::vector<sal_Int32> aLinkPageNums = CalcOutputPageNums( aLinkRect ); + + // Link Export + for (sal_Int32 aLinkPageNum : aLinkPageNums) + { + tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, aLinkRect.SVRect())); + const sal_Int32 nLinkId = + pPDFExtOutDevData->CreateLink(aRect, formatName, aLinkPageNum); + + // Store link info for tagged pdf output: + const IdMapEntry aLinkEntry(aLinkRect, nLinkId); + pPDFExtOutDevData->GetSwPDFState()->m_LinkIdMap.push_back(aLinkEntry); + + // Connect Link and Destination: + if ( bInternal ) + pPDFExtOutDevData->SetLinkDest( nLinkId, nDestId ); + else + pPDFExtOutDevData->SetLinkURL( nLinkId, aURL ); + + // #i44368# Links in Header/Footer + const SwFormatAnchor &rAnch = pFrameFormat->GetAnchor(); + if (RndStdIds::FLY_AT_PAGE != rAnch.GetAnchorId()) + { + const SwNode* pAnchorNode = rAnch.GetAnchorNode(); + if ( pAnchorNode && pDoc->IsInHeaderFooter( *pAnchorNode ) ) + { + const SwTextNode* pTNd = pAnchorNode->GetTextNode(); + if ( pTNd ) + MakeHeaderFooterLinks(*pPDFExtOutDevData, *pTNd, aLinkRect, nDestId, aURL, bInternal, formatName); + } + } + } + } + } + else if (pFrameFormat->Which() == RES_DRAWFRMFMT) + { + // Turn media shapes into Screen annotations. + if (SdrObject* pObject = pFrameFormat->FindRealSdrObject()) + { + SwRect aSnapRect(pObject->GetSnapRect()); + std::vector<sal_Int32> aScreenPageNums = CalcOutputPageNums(aSnapRect); + if (aScreenPageNums.empty()) + continue; + + uno::Reference<drawing::XShape> xShape(pObject->getUnoShape(), uno::UNO_QUERY); + if (xShape->getShapeType() == "com.sun.star.drawing.MediaShape") + { + uno::Reference<beans::XPropertySet> xShapePropSet(xShape, uno::UNO_QUERY); + OUString title; + xShapePropSet->getPropertyValue("Title") >>= title; + OUString description; + xShapePropSet->getPropertyValue("Description") >>= description; + OUString const altText(title.isEmpty() + ? description + : description.isEmpty() + ? title + : OUString::Concat(title) + OUString::Concat("\n") + OUString::Concat(description)); + + OUString aMediaURL; + xShapePropSet->getPropertyValue("MediaURL") >>= aMediaURL; + if (!aMediaURL.isEmpty()) + { + OUString const mimeType(xShapePropSet->getPropertyValue("MediaMimeType").get<OUString>()); + const SwPageFrame* pCurrPage = mrSh.GetLayout()->GetPageAtPos(aSnapRect.Center()); + tools::Rectangle aPDFRect(SwRectToPDFRect(pCurrPage, aSnapRect.SVRect())); + for (sal_Int32 nScreenPageNum : aScreenPageNums) + { + sal_Int32 nScreenId = pPDFExtOutDevData->CreateScreen(aPDFRect, altText, mimeType, nScreenPageNum, pObject); + if (aMediaURL.startsWith("vnd.sun.star.Package:")) + { + // Embedded media. + OUString aTempFileURL; + xShapePropSet->getPropertyValue("PrivateTempFileURL") >>= aTempFileURL; + pPDFExtOutDevData->SetScreenStream(nScreenId, aTempFileURL); + } + else + // Linked media. + pPDFExtOutDevData->SetScreenURL(nScreenId, aMediaURL); + } + } + } + } + } + mrSh.SwCursorShell::ClearMark(); + } + + // REFERENCES + + std::vector<SwFormatField*> vpFields; + mrSh.GetFieldType( SwFieldIds::GetRef, OUString() )->GatherFields(vpFields); + for(auto pFormatField : vpFields ) + { + if( pFormatField->GetTextField() && pFormatField->IsFieldInDoc() ) + { + const SwTextNode* pTNd = pFormatField->GetTextField()->GetpTextNode(); + OSL_ENSURE( nullptr != pTNd, "Enhanced pdf export - text node is missing" ); + if(!lcl_TryMoveToNonHiddenField(mrSh, *pTNd, *pFormatField)) + continue; + // Select the field: + mrSh.SwCursorShell::SetMark(); + mrSh.SwCursorShell::Right( 1, SwCursorSkipMode::Chars ); + + // Link Rectangles + SwRects const aTmp(GetCursorRectsContainingText(mrSh)); + OSL_ENSURE( !aTmp.empty(), "Enhanced pdf export - rectangles are missing" ); + + mrSh.SwCursorShell::ClearMark(); + + // Destination Rectangle + const SwGetRefField* pField = static_cast<SwGetRefField*>(pFormatField->GetField()); + const OUString& rRefName = pField->GetSetRefName(); + mrSh.GotoRefMark( rRefName, pField->GetSubType(), pField->GetSeqNo(), pField->GetFlags() ); + const SwRect& rDestRect = mrSh.GetCharRect(); + + const SwPageFrame* pCurrPage = static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + + // Destination PageNum + const sal_Int32 nDestPageNum = CalcOutputPageNum( rDestRect ); + + if ( -1 != nDestPageNum ) + { + // Destination Export + tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect())); + const sal_Int32 nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum); + + // #i44368# Links in Header/Footer + const bool bHeaderFooter = pDoc->IsInHeaderFooter( *pTNd ); + OUString const content(pField->ExpandField(true, mrSh.GetLayout())); + + // Create links for all selected rectangles: + const size_t nNumOfRects = aTmp.size(); + for ( size_t i = 0; i < nNumOfRects; ++i ) + { + // Link rectangle + const SwRect& rLinkRect( aTmp[ i ] ); + + // Link PageNums + std::vector<sal_Int32> aLinkPageNums = CalcOutputPageNums( rLinkRect ); + + for (sal_Int32 aLinkPageNum : aLinkPageNums) + { + // Link Export + aRect = SwRectToPDFRect(pCurrPage, rLinkRect.SVRect()); + const sal_Int32 nLinkId = + pPDFExtOutDevData->CreateLink(aRect, content, aLinkPageNum); + + // Store link info for tagged pdf output: + const IdMapEntry aLinkEntry( rLinkRect, nLinkId ); + pPDFExtOutDevData->GetSwPDFState()->m_LinkIdMap.push_back(aLinkEntry); + + // Connect Link and Destination: + pPDFExtOutDevData->SetLinkDest( nLinkId, nDestId ); + + // #i44368# Links in Header/Footer + if ( bHeaderFooter ) + { + MakeHeaderFooterLinks(*pPDFExtOutDevData, *pTNd, rLinkRect, nDestId, "", true, content); + } + } + } + } + } + mrSh.SwCursorShell::ClearMark(); + } + + ExportAuthorityEntryLinks(); + + // FOOTNOTES + + const size_t nFootnoteCount = pDoc->GetFootnoteIdxs().size(); + for ( size_t nIdx = 0; nIdx < nFootnoteCount; ++nIdx ) + { + // Set cursor to text node that contains the footnote: + const SwTextFootnote* pTextFootnote = pDoc->GetFootnoteIdxs()[ nIdx ]; + SwTextNode& rTNd = const_cast<SwTextNode&>(pTextFootnote->GetTextNode()); + + mrSh.GetCursor_()->GetPoint()->Assign(rTNd, pTextFootnote->GetStart()); + + // 1. Check if the whole paragraph is hidden + // 2. Check for hidden text attribute + if (rTNd.GetTextNode()->IsHidden() || mrSh.IsInHiddenRange(/*bSelect=*/false) + || (mrSh.GetLayout()->IsHideRedlines() + && sw::IsFootnoteDeleted(pDoc->getIDocumentRedlineAccess(), *pTextFootnote))) + { + continue; + } + + SwCursorSaveState aSaveState( *mrSh.GetCursor_() ); + + // Select the footnote: + mrSh.SwCursorShell::SetMark(); + mrSh.SwCursorShell::Right( 1, SwCursorSkipMode::Chars ); + + // Link Rectangle + SwRects aTmp; + aTmp.insert( aTmp.begin(), mrSh.SwCursorShell::GetCursor_()->begin(), mrSh.SwCursorShell::GetCursor_()->end() ); + OSL_ENSURE( !aTmp.empty(), "Enhanced pdf export - rectangles are missing" ); + + mrSh.GetCursor_()->RestoreSavePos(); + mrSh.SwCursorShell::ClearMark(); + + if (aTmp.empty()) + continue; + + const SwRect aLinkRect( aTmp[ 0 ] ); + + // Goto footnote text: + if ( mrSh.GotoFootnoteText() ) + { + // Destination Rectangle + const SwRect& rDestRect = mrSh.GetCharRect(); + const sal_Int32 nDestPageNum = CalcOutputPageNum( rDestRect ); + if ( -1 != nDestPageNum ) + { + const SwPageFrame* pCurrPage = static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + // Destination PageNum + tools::Rectangle aRect = SwRectToPDFRect(pCurrPage, rDestRect.SVRect()); + // Back link rectangle calculation + const SwPageFrame* fnBodyPage = pCurrPage->getRootFrame()->GetPageByPageNum(nDestPageNum+1); + SwRect fnSymbolRect; + if (fnBodyPage->IsVertical()){ + tools::Long fnSymbolTop = fnBodyPage->GetTopMargin() + fnBodyPage->getFrameArea().Top(); + tools::Long symbolHeight = rDestRect.Top() - fnSymbolTop; + fnSymbolRect = SwRect(rDestRect.Pos().X(),fnSymbolTop,rDestRect.Width(),symbolHeight); + } else { + if (fnBodyPage->IsRightToLeft()){ + tools::Long fnSymbolRight = fnBodyPage->getFrameArea().Right() - fnBodyPage->GetRightMargin(); + tools::Long symbolWidth = fnSymbolRight - rDestRect.Right(); + fnSymbolRect = SwRect(rDestRect.Pos().X(),rDestRect.Pos().Y(),symbolWidth,rDestRect.Height()); + } else { + tools::Long fnSymbolLeft = fnBodyPage->GetLeftMargin() + fnBodyPage->getFrameArea().Left(); + tools::Long symbolWidth = rDestRect.Left() - fnSymbolLeft; + fnSymbolRect = SwRect(fnSymbolLeft,rDestRect.Pos().Y(),symbolWidth,rDestRect.Height()); + } + } + tools::Rectangle aFootnoteSymbolRect = SwRectToPDFRect(pCurrPage, fnSymbolRect.SVRect()); + + OUString const numStrSymbol(pTextFootnote->GetFootnote().GetViewNumStr(*pDoc, mrSh.GetLayout(), true)); + OUString const numStrRef(pTextFootnote->GetFootnote().GetViewNumStr(*pDoc, mrSh.GetLayout(), false)); + + // Export back link + const sal_Int32 nBackLinkId = pPDFExtOutDevData->CreateLink(aFootnoteSymbolRect, numStrSymbol, nDestPageNum); + // Destination Export + const sal_Int32 nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum); + mrSh.GotoFootnoteAnchor(); + // Link PageNums + sal_Int32 aLinkPageNum = CalcOutputPageNum( aLinkRect ); + pCurrPage = static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + // Link Export + aRect = SwRectToPDFRect(pCurrPage, aLinkRect.SVRect()); + const sal_Int32 nLinkId = pPDFExtOutDevData->CreateLink(aRect, numStrRef, aLinkPageNum); + // Back link destination Export + const sal_Int32 nBackDestId = pPDFExtOutDevData->CreateDest(aRect, aLinkPageNum); + // Store link info for tagged pdf output: + const IdMapEntry aLinkEntry( aLinkRect, nLinkId ); + pPDFExtOutDevData->GetSwPDFState()->m_LinkIdMap.push_back(aLinkEntry); + + // Store backlink info for tagged pdf output: + const IdMapEntry aBackLinkEntry( aFootnoteSymbolRect, nBackLinkId ); + pPDFExtOutDevData->GetSwPDFState()->m_LinkIdMap.push_back(aBackLinkEntry); + // Connect Links and Destinations: + pPDFExtOutDevData->SetLinkDest( nLinkId, nDestId ); + pPDFExtOutDevData->SetLinkDest( nBackLinkId, nBackDestId ); + } + } + } + + // OUTLINE + + if( pPDFExtOutDevData->GetIsExportBookmarks() ) + { + typedef std::pair< sal_Int8, sal_Int32 > StackEntry; + std::stack< StackEntry > aOutlineStack; + aOutlineStack.push( StackEntry( -1, -1 ) ); // push default value + + const SwOutlineNodes::size_type nOutlineCount = + mrSh.getIDocumentOutlineNodesAccess()->getOutlineNodesCount(); + for ( SwOutlineNodes::size_type i = 0; i < nOutlineCount; ++i ) + { + // Check if outline is hidden + const SwTextNode* pTNd = mrSh.GetNodes().GetOutLineNds()[ i ]->GetTextNode(); + OSL_ENSURE( nullptr != pTNd, "Enhanced pdf export - text node is missing" ); + + if ( pTNd->IsHidden() || + !sw::IsParaPropsNode(*mrSh.GetLayout(), *pTNd) || + // #i40292# Skip empty outlines: + pTNd->GetText().isEmpty()) + continue; + + // Get parent id from stack: + const sal_Int8 nLevel = static_cast<sal_Int8>(mrSh.getIDocumentOutlineNodesAccess()->getOutlineLevel( i )); + sal_Int8 nLevelOnTopOfStack = aOutlineStack.top().first; + while ( nLevelOnTopOfStack >= nLevel && + nLevelOnTopOfStack != -1 ) + { + aOutlineStack.pop(); + nLevelOnTopOfStack = aOutlineStack.top().first; + } + const sal_Int32 nParent = aOutlineStack.top().second; + + // Destination rectangle + mrSh.GotoOutline(i); + const SwRect& rDestRect = mrSh.GetCharRect(); + + const SwPageFrame* pCurrPage = + static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + + // Destination PageNum + const sal_Int32 nDestPageNum = CalcOutputPageNum( rDestRect ); + + if ( -1 != nDestPageNum ) + { + // Destination Export + tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect())); + const sal_Int32 nDestId = + pPDFExtOutDevData->CreateDest(aRect, nDestPageNum); + + // Outline entry text + const OUString& rEntry = mrSh.getIDocumentOutlineNodesAccess()->getOutlineText( + i, mrSh.GetLayout(), true, false, false ); + + // Create a new outline item: + const sal_Int32 nOutlineId = + pPDFExtOutDevData->CreateOutlineItem( nParent, rEntry, nDestId ); + + // Push current level and nOutlineId on stack: + aOutlineStack.push( StackEntry( nLevel, nOutlineId ) ); + } + } + } + + if( pPDFExtOutDevData->GetIsExportNamedDestinations() ) + { + // #i56629# the iteration to convert the OOo bookmark (#bookmark) + // into PDF named destination, see section 8.2.1 in PDF 1.4 spec + // We need: + // 1. a name for the destination, formed from the standard OOo bookmark name + // 2. the destination, obtained from where the bookmark destination lies + IDocumentMarkAccess* const pMarkAccess = mrSh.GetDoc()->getIDocumentMarkAccess(); + for(IDocumentMarkAccess::const_iterator_t ppMark = pMarkAccess->getBookmarksBegin(); + ppMark != pMarkAccess->getBookmarksEnd(); + ++ppMark) + { + //get the name + const ::sw::mark::IMark* pBkmk = *ppMark; + mrSh.SwCursorShell::ClearMark(); + const OUString& sBkName = pBkmk->GetName(); + + //jump to it + if (! JumpToSwMark( &mrSh, sBkName )) + { + continue; + } + + // Destination Rectangle + const SwRect& rDestRect = mrSh.GetCharRect(); + + const SwPageFrame* pCurrPage = + static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + + // Destination PageNum + const sal_Int32 nDestPageNum = CalcOutputPageNum( rDestRect ); + + // Destination Export + if ( -1 != nDestPageNum ) + { + tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect())); + pPDFExtOutDevData->CreateNamedDest(sBkName, aRect, nDestPageNum); + } + } + mrSh.SwCursorShell::ClearMark(); + //<--- i56629 + } + } + else + { + + // LINKS FROM EDITENGINE + + std::vector< vcl::PDFExtOutDevBookmarkEntry >& rBookmarks = pPDFExtOutDevData->GetBookmarks(); + for ( const auto& rBookmark : rBookmarks ) + { + OUString aBookmarkName( rBookmark.aBookmark ); + const bool bInternal = '#' == aBookmarkName[0]; + if ( bInternal ) + { + aBookmarkName = aBookmarkName.copy( 1 ); + JumpToSwMark( &mrSh, aBookmarkName ); + + // Destination Rectangle + const SwRect& rDestRect = mrSh.GetCharRect(); + + const SwPageFrame* pCurrPage = + static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + + // Destination PageNum + const sal_Int32 nDestPageNum = CalcOutputPageNum( rDestRect ); + + if ( -1 != nDestPageNum ) + { + tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect())); + if ( rBookmark.nLinkId != -1 ) + { + // Destination Export + const sal_Int32 nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum); + + // Connect Link and Destination: + pPDFExtOutDevData->SetLinkDest( rBookmark.nLinkId, nDestId ); + } + else + { + pPDFExtOutDevData->DescribeRegisteredDest(rBookmark.nDestId, aRect, nDestPageNum); + } + } + } + else + pPDFExtOutDevData->SetLinkURL( rBookmark.nLinkId, aBookmarkName ); + } + rBookmarks.clear(); + assert(pPDFExtOutDevData->GetSwPDFState()); + delete pPDFExtOutDevData->GetSwPDFState(); + pPDFExtOutDevData->SetSwPDFState(nullptr); + } + + // Restore view, cursor, and outdev: + mrSh.LockView( bOldLockView ); + mrSh.SwCursorShell::Pop(SwCursorShell::PopMode::DeleteCurrent); + mrOut.Pop(); +} + +void SwEnhancedPDFExportHelper::ExportAuthorityEntryLinks() +{ + auto pPDFExtOutDevData = dynamic_cast<vcl::PDFExtOutDevData*>(mrOut.GetExtOutDevData()); + if (!pPDFExtOutDevData) + { + return; + } + + // Create PDF destinations for bibliography table entries + std::vector<std::tuple<const SwTOXBase*, const OUString*, sal_Int32>> vDestinations; + // string is the row node text, sal_Int32 is number of the destination + // Note: This way of iterating doesn't seem to take into account TOXes + // that are in a frame, probably in some other cases too + { + mrSh.GotoPage(1); + while (mrSh.GotoNextTOXBase()) + { + const SwTOXBase* pIteratedTOX = nullptr; + while ((pIteratedTOX = mrSh.GetCurTOX()) != nullptr + && pIteratedTOX->GetType() == TOX_AUTHORITIES) + { + if (const SwNode& rCurrentNode = mrSh.GetCursor()->GetPoint()->GetNode(); + rCurrentNode.GetNodeType() == SwNodeType::Text) + { + if (mrSh.GetCursor()->GetPoint()->GetNode().FindSectionNode()->GetSection().GetType() + == SectionType::ToxContent) // this checks it's not a heading + { + // Destination Rectangle + const SwRect& rDestRect = mrSh.GetCharRect(); + + const SwPageFrame* pCurrPage = + static_cast<const SwPageFrame*>( mrSh.GetLayout()->Lower() ); + + // Destination PageNum + const sal_Int32 nDestPageNum = CalcOutputPageNum( rDestRect ); + + // Destination Export + if ( -1 != nDestPageNum ) + { + tools::Rectangle aRect(SwRectToPDFRect(pCurrPage, rDestRect.SVRect())); + const sal_Int32 nDestId = pPDFExtOutDevData->CreateDest(aRect, nDestPageNum); + const OUString* vNodeText = &static_cast<const SwTextNode*>(&rCurrentNode)->GetText(); + vDestinations.emplace_back(pIteratedTOX, vNodeText, nDestId); + } + } + } + if (!mrSh.MovePara(GoNextPara, fnParaStart)) + { // Cursor is stuck in the TOX due to document ending immediately afterwards + break; + } + } + } + } + + // Generate links to matching entries in the bibliography tables + std::vector<SwFormatField*> aFields; + SwFieldType* pType = mrSh.GetFieldType(SwFieldIds::TableOfAuthorities, OUString()); + if (!pType) + { + return; + } + + pType->GatherFields(aFields); + const auto pPageFrame = static_cast<const SwPageFrame*>(mrSh.GetLayout()->Lower()); + for (const auto pFormatField : aFields) + { + if (!pFormatField->GetTextField() || !pFormatField->IsFieldInDoc()) + { + continue; + } + + const auto& rAuthorityField + = *static_cast<const SwAuthorityField*>(pFormatField->GetField()); + + if (auto targetType = rAuthorityField.GetTargetType(); + targetType == SwAuthorityField::TargetType::UseDisplayURL + || targetType == SwAuthorityField::TargetType::UseTargetURL) + { + // Since the target type specifies to use an URL, link to it + const OUString& rURL = rAuthorityField.GetAbsoluteURL(); + if (rURL.getLength() == 0) + { + continue; + } + + const SwTextNode& rTextNode = pFormatField->GetTextField()->GetTextNode(); + if (!lcl_TryMoveToNonHiddenField(mrSh, rTextNode, *pFormatField)) + { + continue; + } + + OUString const content(rAuthorityField.ExpandField(true, mrSh.GetLayout())); + + // Select the field. + mrSh.SwCursorShell::SetMark(); + mrSh.SwCursorShell::Right(1, SwCursorSkipMode::Chars); + + // Create the links. + SwRects const rects(GetCursorRectsContainingText(mrSh)); + for (const auto& rLinkRect : rects) + { + for (const auto& rLinkPageNum : CalcOutputPageNums(rLinkRect)) + { + tools::Rectangle aRect(SwRectToPDFRect(pPageFrame, rLinkRect.SVRect())); + sal_Int32 nLinkId = pPDFExtOutDevData->CreateLink(aRect, content, rLinkPageNum); + IdMapEntry aLinkEntry(rLinkRect, nLinkId); + pPDFExtOutDevData->GetSwPDFState()->m_LinkIdMap.push_back(aLinkEntry); + pPDFExtOutDevData->SetLinkURL(nLinkId, rURL); + } + } + mrSh.SwCursorShell::ClearMark(); + } + else if (targetType == SwAuthorityField::TargetType::BibliographyTableRow) + { + // As the target type specifies, try linking to a bibliography table row + sal_Int32 nDestId = -1; + + std::unordered_map<const SwTOXBase*, OUString> vFormattedFieldStrings; + for (const auto& rDestinationTuple : vDestinations) + { + if (vFormattedFieldStrings.find(std::get<0>(rDestinationTuple)) + == vFormattedFieldStrings.end()) + vFormattedFieldStrings.emplace(std::get<0>(rDestinationTuple), + rAuthorityField.GetAuthority(mrSh.GetLayout(), + &std::get<0>(rDestinationTuple)->GetTOXForm())); + + if (vFormattedFieldStrings.at(std::get<0>(rDestinationTuple)) == *std::get<1>(rDestinationTuple)) + { + nDestId = std::get<2>(rDestinationTuple); + break; + } + } + + if (nDestId == -1) + continue; + + const SwTextNode& rTextNode = pFormatField->GetTextField()->GetTextNode(); + if (!lcl_TryMoveToNonHiddenField(mrSh, rTextNode, *pFormatField)) + { + continue; + } + + OUString const content(rAuthorityField.ExpandField(true, mrSh.GetLayout())); + + // Select the field. + mrSh.SwCursorShell::SetMark(); + mrSh.SwCursorShell::Right(1, SwCursorSkipMode::Chars); + + // Create the links. + SwRects const rects(GetCursorRectsContainingText(mrSh)); + for (const auto& rLinkRect : rects) + { + for (const auto& rLinkPageNum : CalcOutputPageNums(rLinkRect)) + { + tools::Rectangle aRect(SwRectToPDFRect(pPageFrame, rLinkRect.SVRect())); + sal_Int32 nLinkId = pPDFExtOutDevData->CreateLink(aRect, content, rLinkPageNum); + IdMapEntry aLinkEntry(rLinkRect, nLinkId); + pPDFExtOutDevData->GetSwPDFState()->m_LinkIdMap.push_back(aLinkEntry); + pPDFExtOutDevData->SetLinkDest(nLinkId, nDestId); + } + } + mrSh.SwCursorShell::ClearMark(); + } + } +} + +// Returns the page number in the output pdf on which the given rect is located. +// If this page is duplicated, method will return first occurrence of it. +sal_Int32 SwEnhancedPDFExportHelper::CalcOutputPageNum( const SwRect& rRect ) const +{ + std::vector< sal_Int32 > aPageNums = CalcOutputPageNums( rRect ); + if ( !aPageNums.empty() ) + return aPageNums[0]; + return -1; +} + +// Returns a vector of the page numbers in the output pdf on which the given +// rect is located. There can be many such pages since StringRangeEnumerator +// allows duplication of its entries. +std::vector< sal_Int32 > SwEnhancedPDFExportHelper::CalcOutputPageNums( + const SwRect& rRect ) const +{ + std::vector< sal_Int32 > aPageNums; + + // Document page number. + sal_Int32 nPageNumOfRect = mrSh.GetPageNumAndSetOffsetForPDF( mrOut, rRect ); + if ( nPageNumOfRect < 0 ) + return aPageNums; + + // What will be the page numbers of page nPageNumOfRect in the output pdf? + if ( mpRangeEnum ) + { + if ( mbSkipEmptyPages ) + // Map the page number to the range without empty pages. + nPageNumOfRect = maPageNumberMap[ nPageNumOfRect ]; + + if ( mpRangeEnum->hasValue( nPageNumOfRect ) ) + { + sal_Int32 nOutputPageNum = 0; + StringRangeEnumerator::Iterator aIter = mpRangeEnum->begin(); + StringRangeEnumerator::Iterator aEnd = mpRangeEnum->end(); + for ( ; aIter != aEnd; ++aIter ) + { + if ( *aIter == nPageNumOfRect ) + aPageNums.push_back( nOutputPageNum ); + ++nOutputPageNum; + } + } + } + else + { + if ( mbSkipEmptyPages ) + { + sal_Int32 nOutputPageNum = 0; + for ( size_t i = 0; i < maPageNumberMap.size(); ++i ) + { + if ( maPageNumberMap[i] >= 0 ) // is not empty? + { + if ( i == static_cast<size_t>( nPageNumOfRect ) ) + { + aPageNums.push_back( nOutputPageNum ); + break; + } + ++nOutputPageNum; + } + } + } + else + aPageNums.push_back( nPageNumOfRect ); + } + + return aPageNums; +} + +void SwEnhancedPDFExportHelper::MakeHeaderFooterLinks( vcl::PDFExtOutDevData& rPDFExtOutDevData, + const SwTextNode& rTNd, + const SwRect& rLinkRect, + sal_Int32 nDestId, + const OUString& rURL, + bool bInternal, + OUString const& rContent) const +{ + // We assume, that the primary link has just been exported. Therefore + // the offset of the link rectangle calculates as follows: + const Point aOffset = rLinkRect.Pos() + mrOut.GetMapMode().GetOrigin(); + + SwIterator<SwTextFrame, SwTextNode, sw::IteratorMode::UnwrapMulti> aIter(rTNd); + for ( SwTextFrame* pTmpFrame = aIter.First(); pTmpFrame; pTmpFrame = aIter.Next() ) + { + // Add offset to current page: + const SwPageFrame* pPageFrame = pTmpFrame->FindPageFrame(); + SwRect aHFLinkRect( rLinkRect ); + aHFLinkRect.Pos() = pPageFrame->getFrameArea().Pos() + aOffset; + + // #i97135# the gcc_x64 optimizer gets aHFLinkRect != rLinkRect wrong + // fool it by comparing the position only (the width and height are the + // same anyway) + if ( aHFLinkRect.Pos() != rLinkRect.Pos() ) + { + // Link PageNums + std::vector<sal_Int32> aHFLinkPageNums = CalcOutputPageNums( aHFLinkRect ); + + for (sal_Int32 aHFLinkPageNum : aHFLinkPageNums) + { + // Link Export + tools::Rectangle aRect(SwRectToPDFRect(pPageFrame, aHFLinkRect.SVRect())); + const sal_Int32 nHFLinkId = + rPDFExtOutDevData.CreateLink(aRect, rContent, aHFLinkPageNum); + + // Connect Link and Destination: + if ( bInternal ) + rPDFExtOutDevData.SetLinkDest( nHFLinkId, nDestId ); + else + rPDFExtOutDevData.SetLinkURL( nHFLinkId, rURL ); + } + } + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/SwGrammarMarkUp.cxx b/sw/source/core/text/SwGrammarMarkUp.cxx new file mode 100644 index 0000000000..53fd4b13c0 --- /dev/null +++ b/sw/source/core/text/SwGrammarMarkUp.cxx @@ -0,0 +1,145 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <SwGrammarMarkUp.hxx> + +SwGrammarMarkUp::~SwGrammarMarkUp() +{ +} + +SwWrongList* SwGrammarMarkUp::Clone() +{ + SwWrongList* pClone = new SwGrammarMarkUp(); + pClone->CopyFrom( *this ); + return pClone; +} + +void SwGrammarMarkUp::CopyFrom( const SwWrongList& rCopy ) +{ + maSentence = static_cast<const SwGrammarMarkUp&>(rCopy).maSentence; + SwWrongList::CopyFrom( rCopy ); +} + +void SwGrammarMarkUp::MoveGrammar( sal_Int32 nPos, sal_Int32 nDiff ) +{ + Move( nPos, nDiff ); + if( maSentence.empty() ) + return; + auto pIter = std::find_if(maSentence.begin(), maSentence.end(), + [nPos](const sal_Int32& rPos) { return rPos >= nPos; }); + const sal_Int32 nEnd = nDiff < 0 ? nPos-nDiff : nPos; + while( pIter != maSentence.end() ) + { + if( *pIter >= nEnd ) + *pIter += nDiff; + else + *pIter = nPos; + ++pIter; + } +} + +std::unique_ptr<SwGrammarMarkUp> SwGrammarMarkUp::SplitGrammarList( sal_Int32 nSplitPos ) +{ + std::unique_ptr<SwGrammarMarkUp> pNew( static_cast<SwGrammarMarkUp*>(SplitList( nSplitPos ).release()) ); + if( maSentence.empty() ) + return pNew; + auto pIter = std::find_if(maSentence.begin(), maSentence.end(), + [nSplitPos](const sal_Int32& rPos) { return rPos >= nSplitPos; }); + if( pIter != maSentence.begin() ) + { + if( !pNew ) { + pNew.reset(new SwGrammarMarkUp()); + pNew->SetInvalid( 0, COMPLETE_STRING ); + } + pNew->maSentence.insert( pNew->maSentence.begin(), maSentence.begin(), pIter ); + maSentence.erase( maSentence.begin(), pIter ); + } + return pNew; +} + +void SwGrammarMarkUp::JoinGrammarList( SwGrammarMarkUp* pNext, sal_Int32 nInsertPos ) +{ + JoinList( pNext, nInsertPos ); + if (pNext) + { + if( pNext->maSentence.empty() ) + return; + for( auto& rPos : pNext->maSentence ) + { + rPos += nInsertPos; + } + maSentence.insert( maSentence.end(), pNext->maSentence.begin(), pNext->maSentence.end() ); + } +} + +void SwGrammarMarkUp::ClearGrammarList( sal_Int32 nSentenceEnd ) +{ + if( COMPLETE_STRING == nSentenceEnd ) { + ClearList(); + maSentence.clear(); + Validate(); + } else if( GetBeginInv() <= nSentenceEnd ) { + std::vector< sal_Int32 >::iterator pIter = maSentence.begin(); + sal_Int32 nStart = 0; + while( pIter != maSentence.end() && *pIter < GetBeginInv() ) + { + nStart = *pIter; + ++pIter; + } + auto pLast = std::find_if(pIter, maSentence.end(), + [nSentenceEnd](const sal_Int32& rPos) { return rPos > nSentenceEnd; }); + maSentence.erase( pIter, pLast ); + RemoveEntry( nStart, nSentenceEnd ); + SetInvalid( nSentenceEnd + 1, COMPLETE_STRING ); + } +} + +void SwGrammarMarkUp::setSentence( sal_Int32 nStart ) +{ + auto pIter = std::find_if(maSentence.begin(), maSentence.end(), + [nStart](const sal_Int32& rPos) { return rPos >= nStart; }); + if( pIter == maSentence.end() || *pIter > nStart ) + maSentence.insert( pIter, nStart ); +} + +sal_Int32 SwGrammarMarkUp::getSentenceStart( sal_Int32 nPos ) +{ + if( maSentence.empty() ) + return 0; + auto pIter = std::find_if(maSentence.begin(), maSentence.end(), + [nPos](const sal_Int32& rPos) { return rPos >= nPos; }); + if( pIter != maSentence.begin() ) + --pIter; + if( pIter != maSentence.end() && *pIter < nPos ) + return *pIter; + return 0; +} + +sal_Int32 SwGrammarMarkUp::getSentenceEnd( sal_Int32 nPos ) +{ + if( maSentence.empty() ) + return COMPLETE_STRING; + auto pIter = std::find_if(maSentence.begin(), maSentence.end(), + [nPos](const sal_Int32& rPos) { return rPos > nPos; }); + if( pIter != maSentence.end() ) + return *pIter; + return COMPLETE_STRING; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/atrhndl.hxx b/sw/source/core/text/atrhndl.hxx new file mode 100644 index 0000000000..851615325a --- /dev/null +++ b/sw/source/core/text/atrhndl.hxx @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once +#define NUM_ATTRIBUTE_STACKS 45 + +#include <vector> +#include <swfntcch.hxx> + +class SwTextAttr; +class SwAttrSet; +class IDocumentSettingAccess; +class SwViewShell; +class SfxPoolItem; +extern const sal_uInt8 StackPos[]; + +/** + * Used by Attribute Iterators to organize attributes on stacks to + * find the valid attribute in each category + */ +class SwAttrHandler +{ +private: + std::vector<const SwTextAttr*> m_aAttrStack[NUM_ATTRIBUTE_STACKS]; // stack collection + const SfxPoolItem* m_pDefaultArray[ NUM_DEFAULT_VALUES ]; + const IDocumentSettingAccess* m_pIDocumentSettingAccess; + const SwViewShell* m_pShell; + + // This is the base font for the paragraph. It is stored in order to have + // a template, if we have to restart the attribute evaluation + std::optional<SwFont> m_oFnt; + + bool m_bVertLayout; + bool m_bVertLayoutLRBT; + + const SwTextAttr* GetTop(sal_uInt16 nStack); + void RemoveFromStack(sal_uInt16 nWhich, const SwTextAttr& rAttr); + + // change font according to pool item + void FontChg(const SfxPoolItem& rItem, SwFont& rFnt, bool bPush ); + + // push attribute to specified stack, returns true, if attribute has + // been pushed on top of stack (important for stacks containing different + // attributes with different priority and redlining) + bool Push( const SwTextAttr& rAttr, const SfxPoolItem& rItem ); + + // apply top attribute on stack to font + void ActivateTop( SwFont& rFnt, sal_uInt16 nStackPos ); + +public: + // Ctor + SwAttrHandler(); + ~SwAttrHandler(); + + // set default attributes to values in rAttrSet or from cache + void Init( const SwAttrSet& rAttrSet, + const IDocumentSettingAccess& rIDocumentSettingAccess ); + void Init( const SfxPoolItem** pPoolItem, const SwAttrSet* pAttrSet, + const IDocumentSettingAccess& rIDocumentSettingAccess, + const SwViewShell* pShell, SwFont& rFnt, + bool bVertLayout, bool bVertLayoutLRBT ); + + bool IsVertLayout() const { return m_bVertLayout; } + + // remove everything from internal stacks, keep default data + void Reset( ); + + // insert specified attribute and change font + void PushAndChg( const SwTextAttr& rAttr, SwFont& rFnt ); + + // remove specified attribute and reset font + void PopAndChg( const SwTextAttr& rAttr, SwFont& rFnt ); + void Pop( const SwTextAttr& rAttr ); + + // apply script dependent attributes + // void ChangeScript( SwFont& rFnt, const sal_uInt8 nScr ); + + // do not call these if you only used the small init function + inline void ResetFont( SwFont& rFnt ) const; + inline const SwFont* GetFont() const; + + void GetDefaultAscentAndHeight(SwViewShell const * pShell, + OutputDevice const & rOut, + sal_uInt16& nAscent, + sal_uInt16& nHeight) const; +}; + +inline void SwAttrHandler::ResetFont( SwFont& rFnt ) const +{ + OSL_ENSURE(m_oFnt, "ResetFont without a font"); + if (m_oFnt) + rFnt = *m_oFnt; +}; + +inline const SwFont* SwAttrHandler::GetFont() const +{ + return m_oFnt ? &*m_oFnt : nullptr; +}; + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/atrstck.cxx b/sw/source/core/text/atrstck.cxx new file mode 100644 index 0000000000..048878292f --- /dev/null +++ b/sw/source/core/text/atrstck.cxx @@ -0,0 +1,851 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atrhndl.hxx" +#include <svl/itemiter.hxx> +#include <vcl/outdev.hxx> +#include <editeng/cmapitem.hxx> +#include <editeng/colritem.hxx> +#include <editeng/contouritem.hxx> +#include <editeng/crossedoutitem.hxx> +#include <editeng/escapementitem.hxx> +#include <editeng/fontitem.hxx> +#include <editeng/fhgtitem.hxx> +#include <editeng/kernitem.hxx> +#include <editeng/charreliefitem.hxx> +#include <editeng/langitem.hxx> +#include <editeng/postitem.hxx> +#include <editeng/shdditem.hxx> +#include <editeng/udlnitem.hxx> +#include <editeng/wghtitem.hxx> +#include <editeng/wrlmitem.hxx> +#include <editeng/autokernitem.hxx> +#include <editeng/charrotateitem.hxx> +#include <editeng/emphasismarkitem.hxx> +#include <editeng/charscaleitem.hxx> +#include <editeng/twolinesitem.hxx> +#include <editeng/charhiddenitem.hxx> +#include <editeng/boxitem.hxx> +#include <editeng/shaditem.hxx> +#include <viewopt.hxx> +#include <charfmt.hxx> +#include <fchrfmt.hxx> +#include <fmtautofmt.hxx> +#include <editeng/brushitem.hxx> +#include <fmtinfmt.hxx> +#include <txtinet.hxx> +#include <IDocumentSettingAccess.hxx> +#include <viewsh.hxx> + +/** + * Attribute to Stack Mapping + * + * Attributes applied to a text are pushed on different stacks. For each + * stack, the top most attribute on the stack is valid. Because some + * kinds of attributes have to be pushed to the same stacks we map their + * ids to stack ids + * Attention: The first NUM_DEFAULT_VALUES ( defined in swfntcch.hxx ) + * are stored in the defaultitem-cache, if you add one, you have to increase + * NUM_DEFAULT_VALUES. + * Also adjust NUM_ATTRIBUTE_STACKS in atrhndl.hxx. + */ +const sal_uInt8 StackPos[ RES_TXTATR_WITHEND_END - RES_CHRATR_BEGIN + 1 ] = +{ + 0, // // 0 + 1, // RES_CHRATR_CASEMAP = RES_CHRATR_BEGIN // 1 + 0, // RES_CHRATR_CHARSETCOLOR, // 2 + 2, // RES_CHRATR_COLOR, // 3 + 3, // RES_CHRATR_CONTOUR, // 4 + 4, // RES_CHRATR_CROSSEDOUT, // 5 + 5, // RES_CHRATR_ESCAPEMENT, // 6 + 6, // RES_CHRATR_FONT, // 7 + 7, // RES_CHRATR_FONTSIZE, // 8 + 8, // RES_CHRATR_KERNING, // 9 + 9, // RES_CHRATR_LANGUAGE, // 10 + 10, // RES_CHRATR_POSTURE, // 11 + 0, // RES_CHRATR_UNUSED1, // 12 + 11, // RES_CHRATR_SHADOWED, // 13 + 12, // RES_CHRATR_UNDERLINE, // 14 + 13, // RES_CHRATR_WEIGHT, // 15 + 14, // RES_CHRATR_WORDLINEMODE, // 16 + 15, // RES_CHRATR_AUTOKERN, // 17 + 16, // RES_CHRATR_BLINK, // 18 + 17, // RES_CHRATR_NOHYPHEN, // 19 + 0, // RES_CHRATR_UNUSED2, // 20 + 18, // RES_CHRATR_BACKGROUND, // 21 + 19, // RES_CHRATR_CJK_FONT, // 22 + 20, // RES_CHRATR_CJK_FONTSIZE, // 23 + 21, // RES_CHRATR_CJK_LANGUAGE, // 24 + 22, // RES_CHRATR_CJK_POSTURE, // 25 + 23, // RES_CHRATR_CJK_WEIGHT, // 26 + 24, // RES_CHRATR_CTL_FONT, // 27 + 25, // RES_CHRATR_CTL_FONTSIZE, // 28 + 26, // RES_CHRATR_CTL_LANGUAGE, // 29 + 27, // RES_CHRATR_CTL_POSTURE, // 30 + 28, // RES_CHRATR_CTL_WEIGHT, // 31 + 29, // RES_CHRATR_ROTATE, // 32 + 30, // RES_CHRATR_EMPHASIS_MARK, // 33 + 31, // RES_CHRATR_TWO_LINES, // 34 + 32, // RES_CHRATR_SCALEW, // 35 + 33, // RES_CHRATR_RELIEF, // 36 + 34, // RES_CHRATR_HIDDEN, // 37 + 35, // RES_CHRATR_OVERLINE, // 38 + 0, // RES_CHRATR_RSID, // 39 + 36, // RES_CHRATR_BOX, // 40 + 37, // RES_CHRATR_SHADOW, // 41 + 38, // RES_CHRATR_HIGHLIGHT, // 42 + 0, // RES_CHRATR_GRABBAG, // 43 + 0, // RES_CHRATR_BIDIRTL, // 44 + 0, // RES_CHRATR_IDCTHINT, // 45 + 39, // RES_TXTATR_REFMARK, // 46 + 40, // RES_TXTATR_TOXMARK, // 47 + 41, // RES_TXTATR_META, // 48 + 41, // RES_TXTATR_METAFIELD, // 49 + 0, // RES_TXTATR_AUTOFMT, // 50 + 0, // RES_TXTATR_INETFMT // 51 + 0, // RES_TXTATR_CHARFMT, // 52 + 42, // RES_TXTATR_CJK_RUBY, // 53 + 0, // RES_TXTATR_UNKNOWN_CONTAINER, // 54 + 43, // RES_TXTATR_INPUTFIELD // 55 + 44, // RES_TXTATR_CONTENTCONTROL // 56 +}; + +namespace CharFormat +{ + +/// Returns the item set associated with a character/inet/auto style +const SfxItemSet* GetItemSet( const SfxPoolItem& rAttr ) +{ + const SfxItemSet* pSet = nullptr; + + if ( RES_TXTATR_AUTOFMT == rAttr.Which() ) + { + pSet = rAttr.StaticWhichCast(RES_TXTATR_AUTOFMT).GetStyleHandle().get(); + } + else + { + // Get the attributes from the template + const SwCharFormat* pFormat = RES_TXTATR_INETFMT == rAttr.Which() ? + rAttr.StaticWhichCast(RES_TXTATR_INETFMT).GetTextINetFormat()->GetCharFormat() : + static_cast<const SwFormatCharFormat&>(rAttr).GetCharFormat(); + if( pFormat ) + { + pSet = &pFormat->GetAttrSet(); + } + } + + return pSet; +} + +/// Extracts pool item of type nWhich from rAttr +const SfxPoolItem* GetItem( const SwTextAttr& rAttr, sal_uInt16 nWhich ) +{ + if ( RES_TXTATR_INETFMT == rAttr.Which() || + RES_TXTATR_CHARFMT == rAttr.Which() || + RES_TXTATR_AUTOFMT == rAttr.Which() ) + { + const SfxItemSet* pSet = CharFormat::GetItemSet( rAttr.GetAttr() ); + if ( !pSet ) return nullptr; + + bool bInParent = RES_TXTATR_AUTOFMT != rAttr.Which(); + const SfxPoolItem* pItem; + bool bRet = SfxItemState::SET == pSet->GetItemState( nWhich, bInParent, &pItem ); + + return bRet ? pItem : nullptr; + } + + return ( nWhich == rAttr.Which() ) ? &rAttr.GetAttr() : nullptr; +} + +/// Checks if item is included in character/inet/auto style +bool IsItemIncluded( const sal_uInt16 nWhich, const SwTextAttr *pAttr ) +{ + bool bRet = false; + + const SfxItemSet* pItemSet = CharFormat::GetItemSet( pAttr->GetAttr() ); + if ( pItemSet ) + bRet = SfxItemState::SET == pItemSet->GetItemState( nWhich ); + + return bRet; +} +} + +/** + * The color of hyperlinks is taken from the associated character attribute, + * depending on its 'visited' state. There are actually two cases, which + * should override the colors from the character attribute: + * 1. We never take the 'visited' color during printing/pdf export/preview + * 2. The user has chosen to override these colors in the view options + */ +static bool lcl_ChgHyperLinkColor( const SwTextAttr& rAttr, + const SfxPoolItem& rItem, + const SwViewShell* pShell, + Color* pColor ) +{ + if ( !pShell || + RES_TXTATR_INETFMT != rAttr.Which() || + RES_CHRATR_COLOR != rItem.Which() ) + return false; + + // #i15455# + // 1. case: + // We do not want to show visited links: + // (printing, pdf export, page preview) + + SwTextINetFormat & rINetAttr(const_cast<SwTextINetFormat&>( + static_txtattr_cast<SwTextINetFormat const&>(rAttr))); + if ( pShell->GetOut()->GetOutDevType() == OUTDEV_PRINTER || + pShell->GetViewOptions()->IsPDFExport() || + pShell->GetViewOptions()->IsPagePreview() ) + { + if (rINetAttr.IsVisited()) + { + if ( pColor ) + { + // take color from character format 'unvisited link' + rINetAttr.SetVisited(false); + const SwCharFormat* pTmpFormat = rINetAttr.GetCharFormat(); + if (const SvxColorItem* pItem = pTmpFormat->GetItemIfSet(RES_CHRATR_COLOR)) + *pColor = pItem->GetValue(); + rINetAttr.SetVisited(true); + } + return true; + } + + return false; + } + + // 2. case: + // We do not want to apply the color set in the hyperlink + // attribute, instead we take the colors from the view options: + + if ( pShell->GetWin() && + ( + (rINetAttr.IsVisited() && pShell->GetViewOptions()->IsVisitedLinks()) || + (!rINetAttr.IsVisited() && pShell->GetViewOptions()->IsLinks()) + ) + ) + { + if ( pColor ) + { + if (rINetAttr.IsVisited()) + { + // take color from view option 'visited link color' + *pColor = pShell->GetViewOptions()->GetVisitedLinksColor(); + } + else + { + // take color from view option 'unvisited link color' + *pColor = pShell->GetViewOptions()->GetLinksColor(); + } + } + return true; + } + + return false; +} + +SwAttrHandler::SwAttrHandler() + : m_pIDocumentSettingAccess(nullptr) + , m_pShell(nullptr) + , m_bVertLayout(false) + , m_bVertLayoutLRBT(false) +{ + memset( m_pDefaultArray, 0, NUM_DEFAULT_VALUES * sizeof(SfxPoolItem*) ); +} + +SwAttrHandler::~SwAttrHandler() +{ +} + +void SwAttrHandler::Init( const SwAttrSet& rAttrSet, + const IDocumentSettingAccess& rIDocumentSettingAcces ) +{ + m_pIDocumentSettingAccess = &rIDocumentSettingAcces; + m_pShell = nullptr; + + for ( sal_uInt16 i = RES_CHRATR_BEGIN; i < RES_CHRATR_END; i++ ) + m_pDefaultArray[ StackPos[ i ] ] = &rAttrSet.Get( i ); +} + +void SwAttrHandler::Init( const SfxPoolItem** pPoolItem, const SwAttrSet* pAS, + const IDocumentSettingAccess& rIDocumentSettingAcces, + const SwViewShell* pSh, + SwFont& rFnt, bool bVL, bool bVertLayoutLRBT ) +{ + // initialize default array + memcpy( m_pDefaultArray, pPoolItem, + NUM_DEFAULT_VALUES * sizeof(SfxPoolItem*) ); + + m_pIDocumentSettingAccess = &rIDocumentSettingAcces; + m_pShell = pSh; + + // do we have to apply additional paragraph attributes? + m_bVertLayout = bVL; + m_bVertLayoutLRBT = bVertLayoutLRBT; + + if ( pAS && pAS->Count() ) + { + SfxItemIter aIter( *pAS ); + sal_uInt16 nWhich; + const SfxPoolItem* pItem = aIter.GetCurItem(); + do + { + nWhich = pItem->Which(); + if (isCHRATR(nWhich)) + { + m_pDefaultArray[ StackPos[ nWhich ] ] = pItem; + FontChg( *pItem, rFnt, true ); + } + + pItem = aIter.NextItem(); + } while (pItem); + } + + // It is possible, that Init is called more than once, e.g., in a + // SwTextFrame::FormatOnceMore situation or (since sw_redlinehide) + // from SwAttrIter::Seek(); in the latter case SwTextSizeInfo::m_pFnt + // is an alias of m_pFnt so it must not be deleted! + if (m_oFnt) + *m_oFnt = rFnt; + else + m_oFnt.emplace(rFnt); +} + +void SwAttrHandler::Reset( ) +{ + for (auto& i : m_aAttrStack) + i.clear(); +} + +void SwAttrHandler::PushAndChg( const SwTextAttr& rAttr, SwFont& rFnt ) +{ + // these special attributes in fact represent a collection of attributes + // they have to be pushed to each stack they belong to + if ( RES_TXTATR_INETFMT == rAttr.Which() || + RES_TXTATR_CHARFMT == rAttr.Which() || + RES_TXTATR_AUTOFMT == rAttr.Which() ) + { + const SfxItemSet* pSet = CharFormat::GetItemSet( rAttr.GetAttr() ); + if ( !pSet ) return; + + for ( sal_uInt16 i = RES_CHRATR_BEGIN; i < RES_CHRATR_END; i++) + { + const SfxPoolItem* pItem; + bool bRet = SfxItemState::SET == pSet->GetItemState( i, rAttr.Which() != RES_TXTATR_AUTOFMT, &pItem ); + + if ( bRet ) + { + // we push rAttr onto the appropriate stack + if ( Push( rAttr, *pItem ) ) + { + // we let pItem change rFnt + Color aColor; + if (lcl_ChgHyperLinkColor(rAttr, *pItem, m_pShell, &aColor)) + { + SvxColorItem aItemNext( aColor, RES_CHRATR_COLOR ); + FontChg( aItemNext, rFnt, true ); + } + else + FontChg( *pItem, rFnt, true ); + } + } + } + } + // this is the usual case, we have a basic attribute, push it onto the + // stack and change the font + else + { + if ( Push( rAttr, rAttr.GetAttr() ) ) + // we let pItem change rFnt + FontChg( rAttr.GetAttr(), rFnt, true ); + } +} + +const SwTextAttr* SwAttrHandler::GetTop(sal_uInt16 nStack) +{ + return m_aAttrStack[nStack].empty() ? nullptr : m_aAttrStack[nStack].back(); +} + +bool SwAttrHandler::Push( const SwTextAttr& rAttr, const SfxPoolItem& rItem ) +{ + OSL_ENSURE( rItem.Which() < RES_TXTATR_WITHEND_END, + "I do not want this attribute, nWhich >= RES_TXTATR_WITHEND_END" ); + + // robust + if ( RES_TXTATR_WITHEND_END <= rItem.Which() ) + return false; + + const sal_uInt16 nStack = StackPos[ rItem.Which() ]; + + // attributes originating from redlining have highest priority + // second priority are hyperlink attributes, which have a color replacement + const SwTextAttr* pTopAttr = GetTop(nStack); + if ( !pTopAttr + || rAttr.IsPriorityAttr() + || ( !pTopAttr->IsPriorityAttr() + && !lcl_ChgHyperLinkColor(*pTopAttr, rItem, m_pShell, nullptr))) + { + m_aAttrStack[nStack].push_back(&rAttr); + return true; + } + + const auto it = m_aAttrStack[nStack].end() - 1; + m_aAttrStack[nStack].insert(it, &rAttr); + return false; +} + +void SwAttrHandler::RemoveFromStack(sal_uInt16 nWhich, const SwTextAttr& rAttr) +{ + auto& rStack = m_aAttrStack[StackPos[nWhich]]; + const auto it = std::find(rStack.begin(), rStack.end(), &rAttr); + if (it != rStack.end()) + rStack.erase(it); +} + +void SwAttrHandler::PopAndChg( const SwTextAttr& rAttr, SwFont& rFnt ) +{ + if ( RES_TXTATR_WITHEND_END <= rAttr.Which() ) + return; // robust + + // these special attributes in fact represent a collection of attributes + // they have to be removed from each stack they belong to + if ( RES_TXTATR_INETFMT == rAttr.Which() || + RES_TXTATR_CHARFMT == rAttr.Which() || + RES_TXTATR_AUTOFMT == rAttr.Which() ) + { + const SfxItemSet* pSet = CharFormat::GetItemSet( rAttr.GetAttr() ); + if ( !pSet ) return; + + for ( sal_uInt16 i = RES_CHRATR_BEGIN; i < RES_CHRATR_END; i++) + { + const SfxPoolItem* pItem; + bool bRet = SfxItemState::SET == pSet->GetItemState( i, RES_TXTATR_AUTOFMT != rAttr.Which(), &pItem ); + if ( bRet ) + { + // we remove rAttr from the appropriate stack + RemoveFromStack(i, rAttr); + // reset font according to attribute on top of stack + // or default value + ActivateTop( rFnt, i ); + } + } + } + // this is the usual case, we have a basic attribute, remove it from the + // stack and reset the font + else + { + RemoveFromStack(rAttr.Which(), rAttr); + // reset font according to attribute on top of stack + // or default value + ActivateTop( rFnt, rAttr.Which() ); + } +} + +/// Only used during redlining +void SwAttrHandler::Pop( const SwTextAttr& rAttr ) +{ + OSL_ENSURE( rAttr.Which() < RES_TXTATR_WITHEND_END, + "I do not have this attribute, nWhich >= RES_TXTATR_WITHEND_END" ); + + if ( rAttr.Which() < RES_TXTATR_WITHEND_END ) + { + RemoveFromStack(rAttr.Which(), rAttr); + } +} + +void SwAttrHandler::ActivateTop( SwFont& rFnt, const sal_uInt16 nAttr ) +{ + OSL_ENSURE( nAttr < RES_TXTATR_WITHEND_END, + "I cannot activate this attribute, nWhich >= RES_TXTATR_WITHEND_END" ); + + const sal_uInt16 nStackPos = StackPos[ nAttr ]; + const SwTextAttr* pTopAt = GetTop(nStackPos); + if ( pTopAt ) + { + const SfxPoolItem* pItemNext(nullptr); + + // check if top attribute is collection of attributes + if ( RES_TXTATR_INETFMT == pTopAt->Which() || + RES_TXTATR_CHARFMT == pTopAt->Which() || + RES_TXTATR_AUTOFMT == pTopAt->Which() ) + { + const SfxItemSet* pSet = CharFormat::GetItemSet( pTopAt->GetAttr() ); + if (pSet) + pSet->GetItemState( nAttr, RES_TXTATR_AUTOFMT != pTopAt->Which(), &pItemNext ); + } + + if (pItemNext) + { + Color aColor; + if (lcl_ChgHyperLinkColor(*pTopAt, *pItemNext, m_pShell, &aColor)) + { + SvxColorItem aItemNext( aColor, RES_CHRATR_COLOR ); + FontChg( aItemNext, rFnt, false ); + } + else + FontChg( *pItemNext, rFnt, false ); + } + else + FontChg( pTopAt->GetAttr(), rFnt, false ); + } + + // default value has to be set, we only have default values for char attribs + else if ( nStackPos < NUM_DEFAULT_VALUES ) + FontChg( *m_pDefaultArray[ nStackPos ], rFnt, false ); + else if ( RES_TXTATR_REFMARK == nAttr ) + rFnt.GetRef()--; + else if ( RES_TXTATR_TOXMARK == nAttr ) + rFnt.GetTox()--; + else if ( (RES_TXTATR_META == nAttr) || (RES_TXTATR_METAFIELD == nAttr) ) + { + rFnt.GetMeta()--; + } + else if (nAttr == RES_TXTATR_CONTENTCONTROL) + { + rFnt.GetContentControl()--; + } + else if ( RES_TXTATR_CJK_RUBY == nAttr ) + { + // ruby stack has no more attributes + // check, if a rotation attribute has to be applied + const sal_uInt16 nTwoLineStack = StackPos[ RES_CHRATR_TWO_LINES ]; + bool bTwoLineAct = false; + const SwTextAttr* pTwoLineAttr = GetTop(nTwoLineStack); + + if ( pTwoLineAttr ) + { + const auto& rTwoLineItem = *CharFormat::GetItem( *pTwoLineAttr, RES_CHRATR_TWO_LINES ); + bTwoLineAct = rTwoLineItem.GetValue(); + } + else + bTwoLineAct = m_pDefaultArray[ nTwoLineStack ]->StaticWhichCast(RES_CHRATR_TWO_LINES).GetValue(); + + if ( bTwoLineAct ) + return; + + // eventually, a rotate attribute has to be activated + const sal_uInt16 nRotateStack = StackPos[ RES_CHRATR_ROTATE ]; + const SwTextAttr* pRotateAttr = GetTop(nRotateStack); + + if ( pRotateAttr ) + { + const auto& rRotateItem = *CharFormat::GetItem( *pRotateAttr, RES_CHRATR_ROTATE ); + rFnt.SetVertical( rRotateItem.GetValue(), m_bVertLayout ); + } + else + rFnt.SetVertical( m_pDefaultArray[ nRotateStack ]->StaticWhichCast(RES_CHRATR_ROTATE).GetValue(), m_bVertLayout ); + } + else if ( RES_TXTATR_INPUTFIELD == nAttr ) + rFnt.GetInputField()--; +} + +/** + * When popping an attribute from the stack, the top more remaining + * attribute in the stack becomes valid. The following function change + * a font depending on the stack id. + */ +void SwAttrHandler::FontChg(const SfxPoolItem& rItem, SwFont& rFnt, bool bPush ) +{ + switch ( rItem.Which() ) + { + case RES_CHRATR_CASEMAP : + rFnt.SetCaseMap( rItem.StaticWhichCast(RES_CHRATR_CASEMAP).GetCaseMap() ); + break; + case RES_CHRATR_COLOR : + rFnt.SetColor( rItem.StaticWhichCast(RES_CHRATR_COLOR).GetValue() ); + break; + case RES_CHRATR_CONTOUR : + rFnt.SetOutline( rItem.StaticWhichCast(RES_CHRATR_CONTOUR).GetValue() ); + break; + case RES_CHRATR_CROSSEDOUT : + rFnt.SetStrikeout( rItem.StaticWhichCast(RES_CHRATR_CROSSEDOUT).GetStrikeout() ); + break; + case RES_CHRATR_ESCAPEMENT : + rFnt.SetEscapement( rItem.StaticWhichCast(RES_CHRATR_ESCAPEMENT).GetEsc() ); + rFnt.SetProportion( rItem.StaticWhichCast(RES_CHRATR_ESCAPEMENT).GetProportionalHeight() ); + break; + case RES_CHRATR_FONT : + { + auto& rFontItem = rItem.StaticWhichCast(RES_CHRATR_FONT); + rFnt.SetName( rFontItem.GetFamilyName(), SwFontScript::Latin ); + rFnt.SetStyleName( rFontItem.GetStyleName(), SwFontScript::Latin ); + rFnt.SetFamily( rFontItem.GetFamily(), SwFontScript::Latin ); + rFnt.SetPitch( rFontItem.GetPitch(), SwFontScript::Latin ); + rFnt.SetCharSet( rFontItem.GetCharSet(), SwFontScript::Latin ); + break; + } + case RES_CHRATR_FONTSIZE : + rFnt.SetSize(Size(0, rItem.StaticWhichCast(RES_CHRATR_FONTSIZE).GetHeight() ), SwFontScript::Latin ); + break; + case RES_CHRATR_KERNING : + rFnt.SetFixKerning( rItem.StaticWhichCast(RES_CHRATR_KERNING).GetValue() ); + break; + case RES_CHRATR_LANGUAGE : + rFnt.SetLanguage( rItem.StaticWhichCast(RES_CHRATR_LANGUAGE).GetLanguage(), SwFontScript::Latin ); + break; + case RES_CHRATR_POSTURE : + rFnt.SetItalic( rItem.StaticWhichCast(RES_CHRATR_POSTURE).GetPosture(), SwFontScript::Latin ); + break; + case RES_CHRATR_SHADOWED : + rFnt.SetShadow( rItem.StaticWhichCast(RES_CHRATR_SHADOWED).GetValue() ); + break; + case RES_CHRATR_UNDERLINE : + { + const sal_uInt16 nStackPos = StackPos[ RES_CHRATR_HIDDEN ]; + const SwTextAttr* pTopAt = GetTop(nStackPos); + + const SfxPoolItem* pTmpItem = pTopAt ? + CharFormat::GetItem( *pTopAt, RES_CHRATR_HIDDEN ) : + m_pDefaultArray[ nStackPos ]; + + if ((m_pShell && !m_pShell->GetWin()) || + (pTmpItem && !pTmpItem->StaticWhichCast(RES_CHRATR_HIDDEN).GetValue()) ) + { + rFnt.SetUnderline( rItem.StaticWhichCast(RES_CHRATR_UNDERLINE).GetLineStyle() ); + rFnt.SetUnderColor( rItem.StaticWhichCast(RES_CHRATR_UNDERLINE).GetColor() ); + } + break; + } + case RES_CHRATR_BOX: + { + const SvxBoxItem& aBoxItem = rItem.StaticWhichCast(RES_CHRATR_BOX); + rFnt.SetTopBorder( aBoxItem.GetTop() ); + rFnt.SetBottomBorder( aBoxItem.GetBottom() ); + rFnt.SetRightBorder( aBoxItem.GetRight() ); + rFnt.SetLeftBorder( aBoxItem.GetLeft() ); + rFnt.SetTopBorderDist( aBoxItem.GetDistance(SvxBoxItemLine::TOP) ); + rFnt.SetBottomBorderDist( aBoxItem.GetDistance(SvxBoxItemLine::BOTTOM) ); + rFnt.SetRightBorderDist( aBoxItem.GetDistance(SvxBoxItemLine::RIGHT) ); + rFnt.SetLeftBorderDist( aBoxItem.GetDistance(SvxBoxItemLine::LEFT) ); + break; + } + case RES_CHRATR_SHADOW: + { + const SvxShadowItem& aShadowItem = rItem.StaticWhichCast(RES_CHRATR_SHADOW); + rFnt.SetShadowColor( aShadowItem.GetColor() ); + rFnt.SetShadowWidth( aShadowItem.GetWidth() ); + rFnt.SetShadowLocation( aShadowItem.GetLocation() ); + break; + } + case RES_CHRATR_OVERLINE : + rFnt.SetOverline( rItem.StaticWhichCast(RES_CHRATR_OVERLINE).GetLineStyle() ); + rFnt.SetOverColor( rItem.StaticWhichCast(RES_CHRATR_OVERLINE).GetColor() ); + break; + case RES_CHRATR_WEIGHT : + rFnt.SetWeight( rItem.StaticWhichCast(RES_CHRATR_WEIGHT).GetWeight(), SwFontScript::Latin ); + break; + case RES_CHRATR_WORDLINEMODE : + rFnt.SetWordLineMode( rItem.StaticWhichCast(RES_CHRATR_WORDLINEMODE).GetValue() ); + break; + case RES_CHRATR_AUTOKERN : + if( rItem.StaticWhichCast(RES_CHRATR_AUTOKERN).GetValue() ) + { + rFnt.SetAutoKern( (!m_pIDocumentSettingAccess || + !m_pIDocumentSettingAccess->get(DocumentSettingId::KERN_ASIAN_PUNCTUATION)) ? + FontKerning::FontSpecific : + FontKerning::Asian ); + } + else + rFnt.SetAutoKern( FontKerning::NONE ); + break; + case RES_CHRATR_BACKGROUND : + rFnt.SetBackColor(rItem.StaticWhichCast(RES_CHRATR_BACKGROUND).GetColor()); + break; + case RES_CHRATR_HIGHLIGHT : + rFnt.SetHighlightColor( rItem.StaticWhichCast(RES_CHRATR_HIGHLIGHT).GetColor() ); + break; + case RES_CHRATR_CJK_FONT : + { + auto& rFontItem = rItem.StaticWhichCast(RES_CHRATR_CJK_FONT); + rFnt.SetName( rFontItem.GetFamilyName(), SwFontScript::CJK ); + rFnt.SetStyleName( rFontItem.GetStyleName(), SwFontScript::CJK ); + rFnt.SetFamily( rFontItem.GetFamily(), SwFontScript::CJK ); + rFnt.SetPitch( rFontItem.GetPitch(), SwFontScript::CJK ); + rFnt.SetCharSet( rFontItem.GetCharSet(), SwFontScript::CJK ); + break; + } + case RES_CHRATR_CJK_FONTSIZE : + rFnt.SetSize(Size( 0, rItem.StaticWhichCast(RES_CHRATR_CJK_FONTSIZE).GetHeight()), SwFontScript::CJK); + break; + case RES_CHRATR_CJK_LANGUAGE : + rFnt.SetLanguage( rItem.StaticWhichCast(RES_CHRATR_CJK_LANGUAGE).GetLanguage(), SwFontScript::CJK ); + break; + case RES_CHRATR_CJK_POSTURE : + rFnt.SetItalic( rItem.StaticWhichCast(RES_CHRATR_CJK_POSTURE).GetPosture(), SwFontScript::CJK ); + break; + case RES_CHRATR_CJK_WEIGHT : + rFnt.SetWeight( rItem.StaticWhichCast(RES_CHRATR_CJK_WEIGHT).GetWeight(), SwFontScript::CJK ); + break; + case RES_CHRATR_CTL_FONT : + { + auto& rFontItem = rItem.StaticWhichCast(RES_CHRATR_CTL_FONT); + rFnt.SetName( rFontItem.GetFamilyName(), SwFontScript::CTL ); + rFnt.SetStyleName( rFontItem.GetStyleName(), SwFontScript::CTL ); + rFnt.SetFamily( rFontItem.GetFamily(), SwFontScript::CTL ); + rFnt.SetPitch( rFontItem.GetPitch(), SwFontScript::CTL ); + rFnt.SetCharSet( rFontItem.GetCharSet(), SwFontScript::CTL ); + break; + } + case RES_CHRATR_CTL_FONTSIZE : + rFnt.SetSize(Size(0, rItem.StaticWhichCast(RES_CHRATR_CTL_FONTSIZE).GetHeight() ), SwFontScript::CTL); + break; + case RES_CHRATR_CTL_LANGUAGE : + rFnt.SetLanguage( rItem.StaticWhichCast(RES_CHRATR_CTL_LANGUAGE).GetLanguage(), SwFontScript::CTL ); + break; + case RES_CHRATR_CTL_POSTURE : + rFnt.SetItalic( rItem.StaticWhichCast(RES_CHRATR_CTL_POSTURE).GetPosture(), SwFontScript::CTL ); + break; + case RES_CHRATR_CTL_WEIGHT : + rFnt.SetWeight( rItem.StaticWhichCast(RES_CHRATR_CTL_WEIGHT).GetWeight(), SwFontScript::CTL ); + break; + case RES_CHRATR_EMPHASIS_MARK : + rFnt.SetEmphasisMark( rItem.StaticWhichCast(RES_CHRATR_EMPHASIS_MARK).GetEmphasisMark() ); + break; + case RES_CHRATR_SCALEW : + rFnt.SetPropWidth( rItem.StaticWhichCast(RES_CHRATR_SCALEW).GetValue() ); + break; + case RES_CHRATR_RELIEF : + rFnt.SetRelief( rItem.StaticWhichCast(RES_CHRATR_RELIEF).GetValue() ); + break; + case RES_CHRATR_HIDDEN : + if (m_pShell && m_pShell->GetWin()) + { + if ( rItem.StaticWhichCast(RES_CHRATR_HIDDEN).GetValue() ) + rFnt.SetUnderline( LINESTYLE_DOTTED ); + else + ActivateTop( rFnt, RES_CHRATR_UNDERLINE ); + } + break; + case RES_CHRATR_ROTATE : + { + // rotate attribute is applied, when: + // 1. ruby stack is empty and + // 2. top of two line stack ( or default attribute )is an + // deactivated two line attribute + const bool bRuby = + 0 != m_aAttrStack[ StackPos[ RES_TXTATR_CJK_RUBY ] ].size(); + + if ( bRuby ) + break; + + const sal_uInt16 nTwoLineStack = StackPos[ RES_CHRATR_TWO_LINES ]; + bool bTwoLineAct = false; + const SwTextAttr* pTwoLineAttr = GetTop(nTwoLineStack); + + if ( pTwoLineAttr ) + { + const auto& rTwoLineItem = *CharFormat::GetItem( *pTwoLineAttr, RES_CHRATR_TWO_LINES ); + bTwoLineAct = rTwoLineItem.GetValue(); + } + else + bTwoLineAct = m_pDefaultArray[ nTwoLineStack ]->StaticWhichCast(RES_CHRATR_TWO_LINES).GetValue(); + + if ( !bTwoLineAct ) + rFnt.SetVertical( rItem.StaticWhichCast(RES_CHRATR_ROTATE).GetValue(), m_bVertLayout, m_bVertLayoutLRBT ); + + break; + } + case RES_CHRATR_TWO_LINES : + { + bool bRuby = 0 != + m_aAttrStack[ StackPos[ RES_TXTATR_CJK_RUBY ] ].size(); + + // two line is activated, if + // 1. no ruby attribute is set and + // 2. attribute is active + if ( !bRuby && rItem.StaticWhichCast(RES_CHRATR_TWO_LINES).GetValue() ) + { + rFnt.SetVertical( 0_deg10, m_bVertLayout ); + break; + } + + // a deactivating two line attribute is on top of stack, + // check if rotate attribute has to be enabled + if ( bRuby ) + break; + + const sal_uInt16 nRotateStack = StackPos[ RES_CHRATR_ROTATE ]; + const SwTextAttr* pRotateAttr = GetTop(nRotateStack); + + if ( pRotateAttr ) + { + const auto& rRotateItem = *CharFormat::GetItem( *pRotateAttr, RES_CHRATR_ROTATE ); + rFnt.SetVertical( rRotateItem.GetValue(), m_bVertLayout ); + } + else + rFnt.SetVertical(m_pDefaultArray[ nRotateStack ]->StaticWhichCast(RES_CHRATR_ROTATE).GetValue(), m_bVertLayout); + break; + } + case RES_TXTATR_CJK_RUBY : + rFnt.SetVertical( 0_deg10, m_bVertLayout ); + break; + case RES_TXTATR_REFMARK : + if ( bPush ) + rFnt.GetRef()++; + else + rFnt.GetRef()--; + break; + case RES_TXTATR_TOXMARK : + if ( bPush ) + rFnt.GetTox()++; + else + rFnt.GetTox()--; + break; + case RES_TXTATR_META: + case RES_TXTATR_METAFIELD: + if ( bPush ) + rFnt.GetMeta()++; + else + rFnt.GetMeta()--; + break; + case RES_TXTATR_CONTENTCONTROL: + if (bPush) + { + rFnt.GetContentControl()++; + } + else + { + rFnt.GetContentControl()--; + } + break; + case RES_TXTATR_INPUTFIELD : + if ( bPush ) + rFnt.GetInputField()++; + else + rFnt.GetInputField()--; + break; + } +} + +/// Takes the default font and calculated the ascent and height +void SwAttrHandler::GetDefaultAscentAndHeight( SwViewShell const * pShell, OutputDevice const & rOut, + sal_uInt16& nAscent, sal_uInt16& nHeight ) const +{ + OSL_ENSURE(m_oFnt, "No font available for GetDefaultAscentAndHeight"); + + if (m_oFnt) + { + SwFont aFont( *m_oFnt ); + nHeight = aFont.GetHeight( pShell, rOut ); + nAscent = aFont.GetAscent( pShell, rOut ); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/frmcrsr.cxx b/sw/source/core/text/frmcrsr.cxx new file mode 100644 index 0000000000..d7f7a2cab9 --- /dev/null +++ b/sw/source/core/text/frmcrsr.cxx @@ -0,0 +1,1691 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <ndtxt.hxx> +#include <pam.hxx> +#include <frmatr.hxx> +#include <frmtool.hxx> +#include <viewopt.hxx> +#include <paratr.hxx> +#include <rootfrm.hxx> +#include <pagefrm.hxx> +#include <colfrm.hxx> +#include <swtypes.hxx> +#include <editeng/lrspitem.hxx> +#include <editeng/tstpitem.hxx> +#include <editeng/ulspitem.hxx> +#include <editeng/lspcitem.hxx> +#include "pormulti.hxx" +#include <doc.hxx> +#include <IDocumentDeviceAccess.hxx> +#include <sortedobjs.hxx> + +#include <unicode/ubidi.h> + +#include <txtfrm.hxx> +#include "inftxt.hxx" +#include "itrtxt.hxx" +#include <crstate.hxx> +#include <viewsh.hxx> +#include <swfntcch.hxx> +#include <flyfrm.hxx> + +#define MIN_OFFSET_STEP 10 + +using namespace ::com::sun::star; + +/* + * - SurvivalKit: For how long do we get past the last char of the line. + * - RightMargin abstains from adjusting position with -1 + * - GetCharRect returns a GetEndCharRect for CursorMoveState::RightMargin + * - GetEndCharRect sets bRightMargin to true + * - SwTextCursor::bRightMargin is set to false by CharCursorToLine + */ + +namespace +{ + +SwTextFrame *GetAdjFrameAtPos( SwTextFrame *pFrame, const SwPosition &rPos, + const bool bRightMargin, const bool bNoScroll = true ) +{ + // RightMargin in the last master line + TextFrameIndex const nOffset = pFrame->MapModelToViewPos(rPos); + SwTextFrame *pFrameAtPos = pFrame; + if( !bNoScroll || pFrame->GetFollow() ) + { + pFrameAtPos = pFrame->GetFrameAtPos( rPos ); + if (nOffset < pFrameAtPos->GetOffset() && + !pFrameAtPos->IsFollow() ) + { + assert(pFrameAtPos->MapModelToViewPos(rPos) == nOffset); + TextFrameIndex nNew(nOffset); + if (nNew < TextFrameIndex(MIN_OFFSET_STEP)) + nNew = TextFrameIndex(0); + else + nNew -= TextFrameIndex(MIN_OFFSET_STEP); + sw_ChangeOffset( pFrameAtPos, nNew ); + } + } + while( pFrame != pFrameAtPos ) + { + pFrame = pFrameAtPos; + pFrame->GetFormatted(); + pFrameAtPos = pFrame->GetFrameAtPos( rPos ); + } + + if( nOffset && bRightMargin ) + { + while (pFrameAtPos && + pFrameAtPos->MapViewToModelPos(pFrameAtPos->GetOffset()) == rPos && + pFrameAtPos->IsFollow() ) + { + pFrameAtPos->GetFormatted(); + pFrameAtPos = pFrameAtPos->FindMaster(); + } + OSL_ENSURE( pFrameAtPos, "+GetCharRect: no frame with my rightmargin" ); + } + return pFrameAtPos ? pFrameAtPos : pFrame; +} + +} + +bool sw_ChangeOffset(SwTextFrame* pFrame, TextFrameIndex nNew) +{ + // Do not scroll in areas and outside of flies + OSL_ENSURE( !pFrame->IsFollow(), "Illegal Scrolling by Follow!" ); + if( pFrame->GetOffset() != nNew && !pFrame->IsInSct() ) + { + SwFlyFrame *pFly = pFrame->FindFlyFrame(); + // Attention: if e.g. in a column frame the size is still invalid + // we must not scroll around just like that + if ( ( pFly && pFly->isFrameAreaDefinitionValid() && + !pFly->GetNextLink() && !pFly->GetPrevLink() ) || + ( !pFly && pFrame->IsInTab() ) ) + { + SwViewShell* pVsh = pFrame->getRootFrame()->GetCurrShell(); + if( pVsh ) + { + if( pVsh->GetRingContainer().size() > 1 || + ( pFrame->GetDrawObjs() && pFrame->GetDrawObjs()->size() ) ) + { + if( !pFrame->GetOffset() ) + return false; + nNew = TextFrameIndex(0); + } + pFrame->SetOffset( nNew ); + pFrame->SetPara( nullptr ); + pFrame->GetFormatted(); + if( pFrame->getFrameArea().HasArea() ) + pFrame->getRootFrame()->GetCurrShell()->InvalidateWindows( pFrame->getFrameArea() ); + return true; + } + } + } + return false; +} + +SwTextFrame& SwTextFrame::GetFrameAtOfst(TextFrameIndex const nWhere) +{ + SwTextFrame* pRet = this; + while( pRet->HasFollow() && nWhere >= pRet->GetFollow()->GetOffset() ) + pRet = pRet->GetFollow(); + return *pRet; +} + +SwTextFrame *SwTextFrame::GetFrameAtPos( const SwPosition &rPos ) +{ + TextFrameIndex const nPos(MapModelToViewPos(rPos)); + SwTextFrame *pFoll = this; + while( pFoll->GetFollow() ) + { + if (nPos > pFoll->GetFollow()->GetOffset()) + pFoll = pFoll->GetFollow(); + else + { + if (nPos == pFoll->GetFollow()->GetOffset() + && !SwTextCursor::IsRightMargin() ) + pFoll = pFoll->GetFollow(); + else + break; + } + } + return pFoll; +} + +/* + * GetCharRect() returns the char's char line described by aPos. + * GetModelPositionForViewPoint() does the reverse: It goes from a document coordinate to + * a Pam. + * Both are virtual in the frame base class and thus are redefined here. + */ + +bool SwTextFrame::GetCharRect( SwRect& rOrig, const SwPosition &rPos, + SwCursorMoveState *pCMS, bool bAllowFarAway ) const +{ + OSL_ENSURE( ! IsVertical() || ! IsSwapped(),"SwTextFrame::GetCharRect with swapped frame" ); + + if (IsLocked()) + return false; + + // Find the right frame first. We need to keep in mind that: + // - the cached information could be invalid (GetPara() == 0) + // - we could have a Follow + // - the Follow chain grows dynamically; the one we end up in + // needs to be formatted + + // Optimisation: reading ahead saves us a GetAdjFrameAtPos + const bool bRightMargin = pCMS && ( CursorMoveState::RightMargin == pCMS->m_eState ); + const bool bNoScroll = pCMS && pCMS->m_bNoScroll; + SwTextFrame *pFrame = GetAdjFrameAtPos( const_cast<SwTextFrame*>(this), rPos, bRightMargin, + bNoScroll ); + pFrame->GetFormatted(); + + const SwFrame* pTmpFrame = pFrame->GetUpper(); + if (pTmpFrame->getFrameArea().Top() == FAR_AWAY && !bAllowFarAway) + return false; + + SwRectFnSet aRectFnSet(pFrame); + const SwTwips nUpperMaxY = aRectFnSet.GetPrtBottom(*pTmpFrame); + const SwTwips nFrameMaxY = aRectFnSet.GetPrtBottom(*pFrame); + + // nMaxY is an absolute value + SwTwips nMaxY = aRectFnSet.IsVert() ? + ( aRectFnSet.IsVertL2R() ? std::min( nFrameMaxY, nUpperMaxY ) : std::max( nFrameMaxY, nUpperMaxY ) ) : + std::min( nFrameMaxY, nUpperMaxY ); + + bool bRet = false; + + if ( pFrame->IsEmpty() || ! aRectFnSet.GetHeight(pFrame->getFramePrintArea()) ) + { + Point aPnt1 = pFrame->getFrameArea().Pos() + pFrame->getFramePrintArea().Pos(); + SwTextNode const*const pTextNd(GetTextNodeForParaProps()); + short nFirstOffset; + pTextNd->GetFirstLineOfsWithNum( nFirstOffset ); + + Point aPnt2; + if ( aRectFnSet.IsVert() ) + { + if( nFirstOffset > 0 ) + aPnt1.AdjustY(nFirstOffset ); + if ( aPnt1.X() < nMaxY && !aRectFnSet.IsVertL2R() ) + aPnt1.setX( nMaxY ); + aPnt2.setX( aPnt1.X() + pFrame->getFramePrintArea().Width() ); + aPnt2.setY( aPnt1.Y() ); + if( aPnt2.X() < nMaxY ) + aPnt2.setX( nMaxY ); + } + else + { + if( nFirstOffset > 0 ) + aPnt1.AdjustX(nFirstOffset ); + + if( aPnt1.Y() > nMaxY ) + aPnt1.setY( nMaxY ); + aPnt2.setX( aPnt1.X() ); + aPnt2.setY( aPnt1.Y() + pFrame->getFramePrintArea().Height() ); + if( aPnt2.Y() > nMaxY ) + aPnt2.setY( nMaxY ); + } + + rOrig = SwRect( aPnt1, aPnt2 ); + + if ( pCMS ) + { + pCMS->m_aRealHeight.setX( 0 ); + pCMS->m_aRealHeight.setY( aRectFnSet.IsVert() ? -rOrig.Width() : rOrig.Height() ); + } + + if ( pFrame->IsRightToLeft() ) + pFrame->SwitchLTRtoRTL( rOrig ); + + bRet = true; + } + else + { + if( !pFrame->HasPara() ) + return false; + + SwFrameSwapper aSwapper( pFrame, true ); + if ( aRectFnSet.IsVert() ) + nMaxY = pFrame->SwitchVerticalToHorizontal( nMaxY ); + + bool bGoOn = true; + TextFrameIndex const nOffset = MapModelToViewPos(rPos); + assert(nOffset != TextFrameIndex(COMPLETE_STRING)); // not going to end well + TextFrameIndex nNextOfst; + + do + { + { + SwTextSizeInfo aInf( pFrame ); + SwTextCursor aLine( pFrame, &aInf ); + nNextOfst = aLine.GetEnd(); + // See comment in AdjustFrame + // Include the line's last char? + if (bRightMargin) + aLine.GetEndCharRect( &rOrig, nOffset, pCMS, nMaxY ); + else + aLine.GetCharRect( &rOrig, nOffset, pCMS, nMaxY ); + bRet = true; + } + + if ( pFrame->IsRightToLeft() ) + pFrame->SwitchLTRtoRTL( rOrig ); + + if ( aRectFnSet.IsVert() ) + pFrame->SwitchHorizontalToVertical( rOrig ); + + if( pFrame->IsUndersized() && pCMS && !pFrame->GetNext() && + aRectFnSet.GetBottom(rOrig) == nUpperMaxY && + pFrame->GetOffset() < nOffset && + !pFrame->IsFollow() && !bNoScroll && + TextFrameIndex(pFrame->GetText().getLength()) != nNextOfst) + { + bGoOn = sw_ChangeOffset( pFrame, nNextOfst ); + } + else + bGoOn = false; + } while ( bGoOn ); + + if ( pCMS ) + { + if ( pFrame->IsRightToLeft() ) + { + if( pCMS->m_b2Lines && pCMS->m_p2Lines) + { + pFrame->SwitchLTRtoRTL( pCMS->m_p2Lines->aLine ); + pFrame->SwitchLTRtoRTL( pCMS->m_p2Lines->aPortion ); + } + } + + if ( aRectFnSet.IsVert() ) + { + if ( pCMS->m_bRealHeight ) + { + pCMS->m_aRealHeight.setY( -pCMS->m_aRealHeight.Y() ); + if ( pCMS->m_aRealHeight.Y() < 0 ) + { + // writing direction is from top to bottom + pCMS->m_aRealHeight.setX( rOrig.Width() - + pCMS->m_aRealHeight.X() + + pCMS->m_aRealHeight.Y() ); + } + } + if( pCMS->m_b2Lines && pCMS->m_p2Lines) + { + pFrame->SwitchHorizontalToVertical( pCMS->m_p2Lines->aLine ); + pFrame->SwitchHorizontalToVertical( pCMS->m_p2Lines->aPortion ); + } + } + + } + } + if( bRet ) + { + SwPageFrame *pPage = pFrame->FindPageFrame(); + OSL_ENSURE( pPage, "Text escaped from page?" ); + const SwTwips nOrigTop = aRectFnSet.GetTop(rOrig); + const SwTwips nPageTop = aRectFnSet.GetTop(pPage->getFrameArea()); + const SwTwips nPageBott = aRectFnSet.GetBottom(pPage->getFrameArea()); + + // We have the following situation: if the frame is in an invalid + // sectionframe, it's possible that the frame is outside the page. + // If we restrict the cursor position to the page area, we enforce + // the formatting of the page, of the section frame and the frame itself. + if( aRectFnSet.YDiff( nPageTop, nOrigTop ) > 0 ) + aRectFnSet.SetTop( rOrig, nPageTop ); + + if ( aRectFnSet.YDiff( nOrigTop, nPageBott ) > 0 ) + aRectFnSet.SetTop( rOrig, nPageBott ); + } + + return bRet; +} + +/* + * GetAutoPos() looks up the char's char line which is described by rPos + * and is used by the auto-positioned frame. + */ + +bool SwTextFrame::GetAutoPos( SwRect& rOrig, const SwPosition &rPos ) const +{ + if( IsHiddenNow() ) + return false; + + TextFrameIndex const nOffset = MapModelToViewPos(rPos); + SwTextFrame* pFrame = &(const_cast<SwTextFrame*>(this)->GetFrameAtOfst( nOffset )); + + pFrame->GetFormatted(); + const SwFrame* pTmpFrame = pFrame->GetUpper(); + + SwRectFnSet aRectFnSet(pTmpFrame); + SwTwips nUpperMaxY = aRectFnSet.GetPrtBottom(*pTmpFrame); + + // nMaxY is in absolute value + SwTwips nMaxY; + if ( aRectFnSet.IsVert() ) + { + if ( aRectFnSet.IsVertL2R() ) + nMaxY = std::min( SwTwips(aRectFnSet.GetPrtBottom(*pFrame)), nUpperMaxY ); + else + nMaxY = std::max( SwTwips(aRectFnSet.GetPrtBottom(*pFrame)), nUpperMaxY ); + } + else + nMaxY = std::min( SwTwips(aRectFnSet.GetPrtBottom(*pFrame)), nUpperMaxY ); + if ( pFrame->IsEmpty() || ! aRectFnSet.GetHeight(pFrame->getFramePrintArea()) ) + { + Point aPnt1 = pFrame->getFrameArea().Pos() + pFrame->getFramePrintArea().Pos(); + Point aPnt2; + if ( aRectFnSet.IsVert() ) + { + if ( aPnt1.X() < nMaxY && !aRectFnSet.IsVertL2R() ) + aPnt1.setX( nMaxY ); + + aPnt2.setX( aPnt1.X() + pFrame->getFramePrintArea().Width() ); + aPnt2.setY( aPnt1.Y() ); + if( aPnt2.X() < nMaxY ) + aPnt2.setX( nMaxY ); + } + else + { + if( aPnt1.Y() > nMaxY ) + aPnt1.setY( nMaxY ); + aPnt2.setX( aPnt1.X() ); + aPnt2.setY( aPnt1.Y() + pFrame->getFramePrintArea().Height() ); + if( aPnt2.Y() > nMaxY ) + aPnt2.setY( nMaxY ); + } + rOrig = SwRect( aPnt1, aPnt2 ); + return true; + } + else + { + if( !pFrame->HasPara() ) + return false; + + SwFrameSwapper aSwapper( pFrame, true ); + if ( aRectFnSet.IsVert() ) + nMaxY = pFrame->SwitchVerticalToHorizontal( nMaxY ); + + SwTextSizeInfo aInf( pFrame ); + SwTextCursor aLine( pFrame, &aInf ); + SwCursorMoveState aTmpState( CursorMoveState::SetOnlyText ); + aTmpState.m_bRealHeight = true; + aLine.GetCharRect( &rOrig, nOffset, &aTmpState, nMaxY ); + if( aTmpState.m_aRealHeight.X() >= 0 ) + { + rOrig.Pos().AdjustY(aTmpState.m_aRealHeight.X() ); + rOrig.Height( aTmpState.m_aRealHeight.Y() ); + } + + if ( pFrame->IsRightToLeft() ) + pFrame->SwitchLTRtoRTL( rOrig ); + + if ( aRectFnSet.IsVert() ) + pFrame->SwitchHorizontalToVertical( rOrig ); + + return true; + } +} + +/** determine top of line for given position in the text frame + + - Top of first paragraph line is the top of the printing area of the text frame + - If a proportional line spacing is applied use top of anchor character as + top of the line. +*/ +bool SwTextFrame::GetTopOfLine( SwTwips& _onTopOfLine, + const SwPosition& _rPos ) const +{ + bool bRet = true; + + // get position offset + TextFrameIndex const nOffset = MapModelToViewPos(_rPos); + + if (TextFrameIndex(GetText().getLength()) < nOffset) + { + bRet = false; + } + else + { + SwRectFnSet aRectFnSet(this); + if ( IsEmpty() || !aRectFnSet.GetHeight(getFramePrintArea()) ) + { + // consider upper space amount considered + // for previous frame and the page grid. + _onTopOfLine = aRectFnSet.GetPrtTop(*this); + } + else + { + // determine formatted text frame that contains the requested position + SwTextFrame* pFrame = &(const_cast<SwTextFrame*>(this)->GetFrameAtOfst( nOffset )); + pFrame->GetFormatted(); + aRectFnSet.Refresh(pFrame); + // If proportional line spacing is applied + // to the text frame, the top of the anchor character is also the + // top of the line. + // Otherwise the line layout determines the top of the line + const SvxLineSpacingItem& rSpace = GetAttrSet()->GetLineSpacing(); + if ( rSpace.GetInterLineSpaceRule() == SvxInterLineSpaceRule::Prop ) + { + SwRect aCharRect; + if ( GetAutoPos( aCharRect, _rPos ) ) + { + _onTopOfLine = aRectFnSet.GetTop(aCharRect); + } + else + { + bRet = false; + } + } + else + { + // assure that text frame is in a horizontal layout + SwFrameSwapper aSwapper( pFrame, true ); + // determine text line that contains the requested position + SwTextSizeInfo aInf( pFrame ); + SwTextCursor aLine( pFrame, &aInf ); + aLine.CharCursorToLine( nOffset ); + // determine top of line + _onTopOfLine = aLine.Y(); + if ( aRectFnSet.IsVert() ) + { + _onTopOfLine = pFrame->SwitchHorizontalToVertical( _onTopOfLine ); + } + } + } + } + + return bRet; +} + +// Minimum distance of non-empty lines is a little less than 2 cm +#define FILL_MIN_DIST 1100 + +struct SwFillData +{ + SwRect aFrame; + const SwCursorMoveState *pCMS; + SwPosition* pPos; + const Point& rPoint; + SwTwips nLineWidth; + bool bFirstLine : 1; + bool bInner : 1; + bool bColumn : 1; + bool bEmpty : 1; + SwFillData( const SwCursorMoveState *pC, SwPosition* pP, const SwRect& rR, + const Point& rPt ) : aFrame( rR ), pCMS( pC ), pPos( pP ), rPoint( rPt ), + nLineWidth( 0 ), bFirstLine( true ), bInner( false ), bColumn( false ), + bEmpty( true ){} + SwFillMode Mode() const { return pCMS->m_pFill->eMode; } + tools::Long X() const { return rPoint.X(); } + tools::Long Y() const { return rPoint.Y(); } + tools::Long Left() const { return aFrame.Left(); } + tools::Long Right() const { return aFrame.Right(); } + tools::Long Bottom() const { return aFrame.Bottom(); } + SwFillCursorPos &Fill() const { return *pCMS->m_pFill; } + void SetTab( sal_uInt16 nNew ) { pCMS->m_pFill->nTabCnt = nNew; } + void SetSpace( sal_uInt16 nNew ) { pCMS->m_pFill->nSpaceCnt = nNew; } + void SetSpaceOnly( sal_uInt16 nNew ) { pCMS->m_pFill->nSpaceOnlyCnt = nNew; } + void SetOrient( const sal_Int16 eNew ){ pCMS->m_pFill->eOrient = eNew; } +}; + +bool SwTextFrame::GetModelPositionForViewPoint_(SwPosition* pPos, const Point& rPoint, + const bool bChgFrame, SwCursorMoveState* pCMS ) const +{ + // GetModelPositionForViewPoint_ is called by GetModelPositionForViewPoint and GetKeyCursorOfst. + // Never just a return false. + + if( IsLocked() || IsHiddenNow() ) + return false; + + const_cast<SwTextFrame*>(this)->GetFormatted(); + + Point aOldPoint( rPoint ); + + if ( IsVertical() ) + { + SwitchVerticalToHorizontal( const_cast<Point&>(rPoint) ); + const_cast<SwTextFrame*>(this)->SwapWidthAndHeight(); + } + + if ( IsRightToLeft() ) + SwitchRTLtoLTR( const_cast<Point&>(rPoint) ); + + std::unique_ptr<SwFillData> pFillData; + if ( pCMS && pCMS->m_pFill ) + pFillData.reset(new SwFillData( pCMS, pPos, getFrameArea(), rPoint )); + + if ( IsEmpty() ) + { + *pPos = MapViewToModelPos(TextFrameIndex(0)); + if( pCMS && pCMS->m_bFieldInfo ) + { + SwTwips nDiff = rPoint.X() - getFrameArea().Left() - getFramePrintArea().Left(); + if( nDiff > 50 || nDiff < 0 ) + pCMS->m_bPosCorr = true; + } + } + else + { + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(this) ); + SwTextCursor aLine( const_cast<SwTextFrame*>(this), &aInf ); + + // See comment in AdjustFrame() + SwTwips nMaxY = getFrameArea().Top() + getFramePrintArea().Top() + getFramePrintArea().Height(); + aLine.TwipsToLine( rPoint.Y() ); + while( aLine.Y() + aLine.GetLineHeight() > nMaxY ) + { + if( !aLine.Prev() ) + break; + } + + if( aLine.GetDropLines() >= aLine.GetLineNr() && 1 != aLine.GetLineNr() + && rPoint.X() < aLine.FirstLeft() + aLine.GetDropLeft() ) + while( aLine.GetLineNr() > 1 ) + aLine.Prev(); + + TextFrameIndex nOffset = aLine.GetModelPositionForViewPoint(pPos, rPoint, bChgFrame, pCMS); + + if( pCMS && pCMS->m_eState == CursorMoveState::NONE && aLine.GetEnd() == nOffset ) + pCMS->m_eState = CursorMoveState::RightMargin; + + // pPos is a pure IN parameter and must not be evaluated. + // pIter->GetModelPositionForViewPoint returns from a nesting with COMPLETE_STRING. + // If SwTextIter::GetModelPositionForViewPoint calls GetModelPositionForViewPoint further by itself + // nNode changes the position. + // In such cases, pPos must not be calculated. + if (TextFrameIndex(COMPLETE_STRING) != nOffset) + { + *pPos = MapViewToModelPos(nOffset); + if( pFillData ) + { + if (TextFrameIndex(GetText().getLength()) > nOffset || + rPoint.Y() < getFrameArea().Top() ) + pFillData->bInner = true; + pFillData->bFirstLine = aLine.GetLineNr() < 2; + if (GetText().getLength()) + { + pFillData->bEmpty = false; + pFillData->nLineWidth = aLine.GetCurr()->Width(); + } + } + } + } + bool bChgFillData = false; + if( pFillData && FindPageFrame()->getFrameArea().Contains( aOldPoint ) ) + { + FillCursorPos( *pFillData ); + bChgFillData = true; + } + + if ( IsVertical() ) + { + if ( bChgFillData ) + SwitchHorizontalToVertical( pFillData->Fill().aCursor.Pos() ); + const_cast<SwTextFrame*>(this)->SwapWidthAndHeight(); + } + + if ( IsRightToLeft() && bChgFillData ) + { + SwitchLTRtoRTL( pFillData->Fill().aCursor.Pos() ); + const sal_Int16 eOrient = pFillData->pCMS->m_pFill->eOrient; + + if ( text::HoriOrientation::LEFT == eOrient ) + pFillData->SetOrient( text::HoriOrientation::RIGHT ); + else if ( text::HoriOrientation::RIGHT == eOrient ) + pFillData->SetOrient( text::HoriOrientation::LEFT ); + } + + const_cast<Point&>(rPoint) = aOldPoint; + + return true; +} + +bool SwTextFrame::GetModelPositionForViewPoint(SwPosition* pPos, Point& rPoint, + SwCursorMoveState* pCMS, bool ) const +{ + const bool bChgFrame = !(pCMS && CursorMoveState::UpDown == pCMS->m_eState); + return GetModelPositionForViewPoint_( pPos, rPoint, bChgFrame, pCMS ); +} + +/* + * Layout-oriented cursor movement to the line start. + */ + +bool SwTextFrame::LeftMargin(SwPaM *pPam) const +{ + assert(GetMergedPara() || &pPam->GetPointNode() == static_cast<SwContentNode const*>(GetDep())); + + SwTextFrame *pFrame = GetAdjFrameAtPos( const_cast<SwTextFrame*>(this), *pPam->GetPoint(), + SwTextCursor::IsRightMargin() ); + pFrame->GetFormatted(); + TextFrameIndex nIndx; + if ( pFrame->IsEmpty() ) + nIndx = TextFrameIndex(0); + else + { + SwTextSizeInfo aInf( pFrame ); + SwTextCursor aLine( pFrame, &aInf ); + + aLine.CharCursorToLine(pFrame->MapModelToViewPos(*pPam->GetPoint())); + nIndx = aLine.GetStart(); + if( pFrame->GetOffset() && !pFrame->IsFollow() && !aLine.GetPrev() ) + { + sw_ChangeOffset(pFrame, TextFrameIndex(0)); + nIndx = TextFrameIndex(0); + } + } + *pPam->GetPoint() = pFrame->MapViewToModelPos(nIndx); + SwTextCursor::SetRightMargin( false ); + return true; +} + +/* + * To the line end: That's the position before the last char of the line. + * Exception: In the last line, it should be able to place the cursor after + * the last char in order to append text. + */ + +bool SwTextFrame::RightMargin(SwPaM *pPam, bool bAPI) const +{ + assert(GetMergedPara() || &pPam->GetPointNode() == static_cast<SwContentNode const*>(GetDep())); + + SwTextFrame *pFrame = GetAdjFrameAtPos( const_cast<SwTextFrame*>(this), *pPam->GetPoint(), + SwTextCursor::IsRightMargin() ); + pFrame->GetFormatted(); + TextFrameIndex nRightMargin(0); + if (!IsEmpty()) + { + SwTextSizeInfo aInf( pFrame ); + SwTextCursor aLine( pFrame, &aInf ); + + aLine.CharCursorToLine(MapModelToViewPos(*pPam->GetPoint())); + nRightMargin = aLine.GetStart() + aLine.GetCurr()->GetLen(); + + // We skip hard line breaks + if( aLine.GetCurr()->GetLen() && + CH_BREAK == aInf.GetText()[sal_Int32(nRightMargin) - 1]) + --nRightMargin; + else if( !bAPI && (aLine.GetNext() || pFrame->GetFollow()) ) + { + while( nRightMargin > aLine.GetStart() && + ' ' == aInf.GetText()[sal_Int32(nRightMargin) - 1]) + --nRightMargin; + } + } + *pPam->GetPoint() = pFrame->MapViewToModelPos(nRightMargin); + SwTextCursor::SetRightMargin( !bAPI ); + return true; +} + +// The following two methods try to put the Cursor into the next/successive +// line. If we do not have a preceding/successive line we forward the call +// to the base class. +// The Cursor's horizontal justification is done afterwards by the CursorShell. + +namespace { + +class SwSetToRightMargin +{ + bool m_bRight; + +public: + SwSetToRightMargin() + : m_bRight(false) + { + } + ~SwSetToRightMargin() { SwTextCursor::SetRightMargin(m_bRight); } + void SetRight(const bool bNew) { m_bRight = bNew; } +}; + +} + +bool SwTextFrame::UnitUp_( SwPaM *pPam, const SwTwips nOffset, + bool bSetInReadOnly ) const +{ + // Set the RightMargin if needed + SwSetToRightMargin aSet; + + if( IsInTab() && + pPam->GetPointNode().StartOfSectionNode() != + pPam->GetMarkNode().StartOfSectionNode() ) + { + // If the PaM is located within different boxes, we have a table selection, + // which is handled by the base class. + return SwContentFrame::UnitUp( pPam, nOffset, bSetInReadOnly ); + } + + const_cast<SwTextFrame*>(this)->GetFormatted(); + const TextFrameIndex nPos = MapModelToViewPos(*pPam->GetPoint()); + SwRect aCharBox; + + if( !IsEmpty() && !IsHiddenNow() ) + { + TextFrameIndex nFormat(COMPLETE_STRING); + do + { + if (nFormat != TextFrameIndex(COMPLETE_STRING) && !IsFollow()) + sw_ChangeOffset( const_cast<SwTextFrame*>(this), nFormat ); + + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(this) ); + SwTextCursor aLine( const_cast<SwTextFrame*>(this), &aInf ); + + // Optimize away flys with no flow and IsDummy() + if( nPos ) + aLine.CharCursorToLine( nPos ); + else + aLine.Top(); + + const SwLineLayout *pPrevLine = aLine.GetPrevLine(); + const TextFrameIndex nStart = aLine.GetStart(); + aLine.GetCharRect( &aCharBox, nPos ); + + bool bSecondOfDouble = ( aInf.IsMulti() && ! aInf.IsFirstMulti() ); + bool bPrevLine = ( pPrevLine && pPrevLine != aLine.GetCurr() ); + + if( !pPrevLine && !bSecondOfDouble && GetOffset() && !IsFollow() ) + { + nFormat = GetOffset(); + TextFrameIndex nDiff = aLine.GetLength(); + if( !nDiff ) + nDiff = TextFrameIndex(MIN_OFFSET_STEP); + if( nFormat > nDiff ) + nFormat = nFormat - nDiff; + else + nFormat = TextFrameIndex(0); + continue; + } + + // We select the target line for the cursor, in case we are in a + // double line portion, prev line = curr line + if( bPrevLine && !bSecondOfDouble ) + { + aLine.PrevLine(); + while ( aLine.GetStart() == nStart && + nullptr != ( pPrevLine = aLine.GetPrevLine() ) && + pPrevLine != aLine.GetCurr() ) + aLine.PrevLine(); + } + + if ( bPrevLine || bSecondOfDouble ) + { + aCharBox.Width( aCharBox.SSize().Width() / 2 ); + aCharBox.Pos().setX( aCharBox.Pos().X() - 150 ); + + // See comment in SwTextFrame::GetModelPositionForViewPoint() +#if OSL_DEBUG_LEVEL > 0 + const SwNodeOffset nOldNode = pPam->GetPoint()->GetNodeIndex(); +#endif + // The node should not be changed + TextFrameIndex nTmpOfst = aLine.GetModelPositionForViewPoint(pPam->GetPoint(), + aCharBox.Pos(), false ); +#if OSL_DEBUG_LEVEL > 0 + OSL_ENSURE( nOldNode == pPam->GetPoint()->GetNodeIndex(), + "SwTextFrame::UnitUp: illegal node change" ); +#endif + + // We make sure that we move up. + if( nTmpOfst >= nStart && nStart && !bSecondOfDouble ) + { + nTmpOfst = nStart; + aSet.SetRight( true ); + } + *pPam->GetPoint() = MapViewToModelPos(nTmpOfst); + return true; + } + + if ( IsFollow() ) + { + aLine.GetCharRect( &aCharBox, nPos ); + aCharBox.Width( aCharBox.SSize().Width() / 2 ); + } + break; + } while ( true ); + } + /* If 'this' is a follow and a prev failed, we need to go to the + * last line of the master, which is us. + * Or: If we are a follow with follow, we need to get the master. + */ + if ( IsFollow() ) + { + const SwTextFrame *pTmpPrev = FindMaster(); + TextFrameIndex nOffs = GetOffset(); + if( pTmpPrev ) + { + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + const bool bProtectedAllowed = pSh && pSh->GetViewOptions()->IsCursorInProtectedArea(); + const SwTextFrame *pPrevPrev = pTmpPrev; + // We skip protected frames and frames without content here + while( pPrevPrev && ( pPrevPrev->GetOffset() == nOffs || + ( !bProtectedAllowed && pPrevPrev->IsProtected() ) ) ) + { + pTmpPrev = pPrevPrev; + nOffs = pTmpPrev->GetOffset(); + if ( pPrevPrev->IsFollow() ) + pPrevPrev = pTmpPrev->FindMaster(); + else + pPrevPrev = nullptr; + } + if ( !pPrevPrev ) + return pTmpPrev->SwContentFrame::UnitUp( pPam, nOffset, bSetInReadOnly ); + aCharBox.Pos().setY( pPrevPrev->getFrameArea().Bottom() - 1 ); + return pPrevPrev->GetKeyCursorOfst( pPam->GetPoint(), aCharBox.Pos() ); + } + } + return SwContentFrame::UnitUp( pPam, nOffset, bSetInReadOnly ); +} + +// Used for Bidi. nPos is the logical position in the string, bLeft indicates +// if left arrow or right arrow was pressed. The return values are: +// nPos: the new visual position +// bLeft: whether the break iterator has to add or subtract from the +// current position +static void lcl_VisualMoveRecursion(const SwLineLayout& rCurrLine, TextFrameIndex nIdx, + TextFrameIndex & nPos, bool& bRight, + sal_uInt8& nCursorLevel, sal_uInt8 nDefaultDir ) +{ + const SwLinePortion* pPor = rCurrLine.GetFirstPortion(); + const SwLinePortion* pLast = nullptr; + + // What's the current portion? + while ( pPor && nIdx + pPor->GetLen() <= nPos ) + { + nIdx = nIdx + pPor->GetLen(); + pLast = pPor; + pPor = pPor->GetNextPortion(); + } + + if ( bRight ) + { + bool bRecurse = pPor && pPor->IsMultiPortion() && + static_cast<const SwMultiPortion*>(pPor)->IsBidi(); + + // 1. special case: at beginning of bidi portion + if ( bRecurse && nIdx == nPos ) + { + nPos = nPos + pPor->GetLen(); + + // leave bidi portion + if ( nCursorLevel != nDefaultDir ) + { + bRecurse = false; + } + else + // special case: + // buffer: abcXYZ123 in LTR paragraph + // view: abc123ZYX + // cursor is between c and X in the buffer and cursor level = 0 + nCursorLevel++; + } + + // 2. special case: at beginning of portion after bidi portion + else if ( pLast && pLast->IsMultiPortion() && + static_cast<const SwMultiPortion*>(pLast)->IsBidi() && nIdx == nPos ) + { + // enter bidi portion + if ( nCursorLevel != nDefaultDir ) + { + bRecurse = true; + nIdx = nIdx - pLast->GetLen(); + pPor = pLast; + } + } + + // Recursion + if ( bRecurse ) + { + const SwLineLayout& rLine = static_cast<const SwMultiPortion*>(pPor)->GetRoot(); + TextFrameIndex nTmpPos = nPos - nIdx; + bool bTmpForward = ! bRight; + sal_uInt8 nTmpCursorLevel = nCursorLevel; + lcl_VisualMoveRecursion(rLine, TextFrameIndex(0), nTmpPos, bTmpForward, + nTmpCursorLevel, nDefaultDir + 1 ); + + nPos = nTmpPos + nIdx; + bRight = bTmpForward; + nCursorLevel = nTmpCursorLevel; + } + + // go forward + else + { + bRight = true; + nCursorLevel = nDefaultDir; + } + + } + else + { + bool bRecurse = pPor && pPor->IsMultiPortion() && static_cast<const SwMultiPortion*>(pPor)->IsBidi(); + + // 1. special case: at beginning of bidi portion + if ( bRecurse && nIdx == nPos ) + { + // leave bidi portion + if ( nCursorLevel == nDefaultDir ) + { + bRecurse = false; + } + } + + // 2. special case: at beginning of portion after bidi portion + else if ( pLast && pLast->IsMultiPortion() && + static_cast<const SwMultiPortion*>(pLast)->IsBidi() && nIdx == nPos ) + { + nPos = nPos - pLast->GetLen(); + + // enter bidi portion + if ( nCursorLevel % 2 == nDefaultDir % 2 ) + { + bRecurse = true; + nIdx = nIdx - pLast->GetLen(); + pPor = pLast; + + // special case: + // buffer: abcXYZ123 in LTR paragraph + // view: abc123ZYX + // cursor is behind 3 in the buffer and cursor level = 2 + if ( nDefaultDir + 2 == nCursorLevel ) + nPos = nPos + pLast->GetLen(); + } + } + + // go forward + if ( bRecurse ) + { + const SwLineLayout& rLine = static_cast<const SwMultiPortion*>(pPor)->GetRoot(); + TextFrameIndex nTmpPos = nPos - nIdx; + bool bTmpForward = ! bRight; + sal_uInt8 nTmpCursorLevel = nCursorLevel; + lcl_VisualMoveRecursion(rLine, TextFrameIndex(0), nTmpPos, bTmpForward, + nTmpCursorLevel, nDefaultDir + 1 ); + + // special case: + // buffer: abcXYZ123 in LTR paragraph + // view: abc123ZYX + // cursor is between Z and 1 in the buffer and cursor level = 2 + if ( nTmpPos == pPor->GetLen() && nTmpCursorLevel == nDefaultDir + 1 ) + { + nTmpPos = nTmpPos - pPor->GetLen(); + nTmpCursorLevel = nDefaultDir; + bTmpForward = ! bTmpForward; + } + + nPos = nTmpPos + nIdx; + bRight = bTmpForward; + nCursorLevel = nTmpCursorLevel; + } + + // go backward + else + { + bRight = false; + nCursorLevel = nDefaultDir; + } + } +} + +void SwTextFrame::PrepareVisualMove(TextFrameIndex & nPos, sal_uInt8& nCursorLevel, + bool& bForward, bool bInsertCursor ) +{ + if( IsEmpty() || IsHiddenNow() ) + return; + + GetFormatted(); + + SwTextSizeInfo aInf(this); + SwTextCursor aLine(this, &aInf); + + if( nPos ) + aLine.CharCursorToLine( nPos ); + else + aLine.Top(); + + const SwLineLayout* pLine = aLine.GetCurr(); + const TextFrameIndex nStt = aLine.GetStart(); + const TextFrameIndex nLen = pLine->GetLen(); + + // We have to distinguish between an insert and overwrite cursor: + // The insert cursor position depends on the cursor level: + // buffer: abcXYZdef in LTR paragraph + // display: abcZYXdef + // If cursor is between c and X in the buffer and cursor level is 0, + // the cursor blinks between c and Z and -> sets the cursor between Z and Y. + // If the cursor level is 1, the cursor blinks between X and d and + // -> sets the cursor between d and e. + // The overwrite cursor simply travels to the next visual character. + if ( bInsertCursor ) + { + lcl_VisualMoveRecursion( *pLine, nStt, nPos, bForward, + nCursorLevel, IsRightToLeft() ? 1 : 0 ); + return; + } + + const sal_uInt8 nDefaultDir = static_cast<sal_uInt8>(IsRightToLeft() ? UBIDI_RTL : UBIDI_LTR); + const bool bVisualRight = ( nDefaultDir == UBIDI_LTR && bForward ) || + ( nDefaultDir == UBIDI_RTL && ! bForward ); + + // Bidi functions from icu 2.0 + + const sal_Unicode* pLineString = GetText().getStr(); + + UErrorCode nError = U_ZERO_ERROR; + UBiDi* pBidi = ubidi_openSized( sal_Int32(nLen), 0, &nError ); + ubidi_setPara( pBidi, reinterpret_cast<const UChar *>(pLineString), + sal_Int32(nLen), nDefaultDir, nullptr, &nError ); + + TextFrameIndex nTmpPos(0); + bool bOutOfBounds = false; + + if ( nPos < nStt + nLen ) + { + nTmpPos = TextFrameIndex(ubidi_getVisualIndex( pBidi, sal_Int32(nPos), &nError )); + + // visual indices are always LTR aligned + if ( bVisualRight ) + { + if (nTmpPos + TextFrameIndex(1) < nStt + nLen) + ++nTmpPos; + else + { + nPos = nDefaultDir == UBIDI_RTL ? TextFrameIndex(0) : nStt + nLen; + bOutOfBounds = true; + } + } + else + { + if ( nTmpPos ) + --nTmpPos; + else + { + nPos = nDefaultDir == UBIDI_RTL ? nStt + nLen : TextFrameIndex(0); + bOutOfBounds = true; + } + } + } + else + { + nTmpPos = nDefaultDir == UBIDI_LTR ? nPos - TextFrameIndex(1) : TextFrameIndex(0); + } + + if ( ! bOutOfBounds ) + { + nPos = TextFrameIndex(ubidi_getLogicalIndex( pBidi, sal_Int32(nTmpPos), &nError )); + + if ( bForward ) + { + if ( nPos ) + --nPos; + else + { + ++nPos; + bForward = ! bForward; + } + } + else + ++nPos; + } + + ubidi_close( pBidi ); +} + +bool SwTextFrame::UnitDown_(SwPaM *pPam, const SwTwips nOffset, + bool bSetInReadOnly ) const +{ + + if ( IsInTab() && + pPam->GetPointNode().StartOfSectionNode() != + pPam->GetMarkNode().StartOfSectionNode() ) + { + // If the PaM is located within different boxes, we have a table selection, + // which is handled by the base class. + return SwContentFrame::UnitDown( pPam, nOffset, bSetInReadOnly ); + } + const_cast<SwTextFrame*>(this)->GetFormatted(); + const TextFrameIndex nPos = MapModelToViewPos(*pPam->GetPoint()); + SwRect aCharBox; + const SwContentFrame *pTmpFollow = nullptr; + + if ( IsVertical() ) + const_cast<SwTextFrame*>(this)->SwapWidthAndHeight(); + + if ( !IsEmpty() && !IsHiddenNow() ) + { + TextFrameIndex nFormat(COMPLETE_STRING); + do + { + if (nFormat != TextFrameIndex(COMPLETE_STRING) && !IsFollow() && + !sw_ChangeOffset( const_cast<SwTextFrame*>(this), nFormat ) ) + break; + + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(this) ); + SwTextCursor aLine( const_cast<SwTextFrame*>(this), &aInf ); + nFormat = aLine.GetEnd(); + + aLine.CharCursorToLine( nPos ); + + const SwLineLayout* pNextLine = aLine.GetNextLine(); + const TextFrameIndex nStart = aLine.GetStart(); + aLine.GetCharRect( &aCharBox, nPos ); + + bool bFirstOfDouble = ( aInf.IsMulti() && aInf.IsFirstMulti() ); + + if( pNextLine || bFirstOfDouble ) + { + aCharBox.Width( aCharBox.SSize().Width() / 2 ); +#if OSL_DEBUG_LEVEL > 0 + // See comment in SwTextFrame::GetModelPositionForViewPoint() + const SwNodeOffset nOldNode = pPam->GetPoint()->GetNodeIndex(); +#endif + if ( pNextLine && ! bFirstOfDouble ) + aLine.NextLine(); + + TextFrameIndex nTmpOfst = aLine.GetModelPositionForViewPoint( pPam->GetPoint(), + aCharBox.Pos(), false ); +#if OSL_DEBUG_LEVEL > 0 + OSL_ENSURE( nOldNode == pPam->GetPoint()->GetNodeIndex(), + "SwTextFrame::UnitDown: illegal node change" ); +#endif + + // We make sure that we move down. + if( nTmpOfst <= nStart && ! bFirstOfDouble ) + nTmpOfst = nStart + TextFrameIndex(1); + *pPam->GetPoint() = MapViewToModelPos(nTmpOfst); + + if ( IsVertical() ) + const_cast<SwTextFrame*>(this)->SwapWidthAndHeight(); + + return true; + } + pTmpFollow = GetFollow(); + if( nullptr != pTmpFollow ) + { // Skip protected follows + const SwContentFrame* pTmp = pTmpFollow; + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + if( !pSh || !pSh->GetViewOptions()->IsCursorInProtectedArea() ) + { + while( pTmpFollow && pTmpFollow->IsProtected() ) + { + pTmp = pTmpFollow; + pTmpFollow = pTmpFollow->GetFollow(); + } + } + if( !pTmpFollow ) // Only protected ones left + { + if ( IsVertical() ) + const_cast<SwTextFrame*>(this)->SwapWidthAndHeight(); + return pTmp->SwContentFrame::UnitDown( pPam, nOffset, bSetInReadOnly ); + } + + aLine.GetCharRect( &aCharBox, nPos ); + aCharBox.Width( aCharBox.SSize().Width() / 2 ); + } + else if( !IsFollow() ) + { + TextFrameIndex nTmpLen(aInf.GetText().getLength()); + if( aLine.GetEnd() < nTmpLen ) + { + if( nFormat <= GetOffset() ) + { + nFormat = std::min(GetOffset() + TextFrameIndex(MIN_OFFSET_STEP), + nTmpLen ); + if( nFormat <= GetOffset() ) + break; + } + continue; + } + } + break; + } while( true ); + } + else + pTmpFollow = GetFollow(); + + if ( IsVertical() ) + const_cast<SwTextFrame*>(this)->SwapWidthAndHeight(); + + // We take a shortcut for follows + if( pTmpFollow ) + { + aCharBox.Pos().setY( pTmpFollow->getFrameArea().Top() + 1 ); + return static_cast<const SwTextFrame*>(pTmpFollow)->GetKeyCursorOfst( pPam->GetPoint(), + aCharBox.Pos() ); + } + return SwContentFrame::UnitDown( pPam, nOffset, bSetInReadOnly ); +} + +bool SwTextFrame::UnitUp(SwPaM *pPam, const SwTwips nOffset, + bool bSetInReadOnly ) const +{ + /* We call ContentNode::GertFrame() in CursorSh::Up(). + * This _always returns the master. + * In order to not mess with cursor travelling, we correct here + * in SwTextFrame. + * We calculate UnitUp for pFrame. pFrame is either a master (= this) or a + * follow (!= this). + */ + const SwTextFrame *pFrame = GetAdjFrameAtPos( const_cast<SwTextFrame*>(this), *(pPam->GetPoint()), + SwTextCursor::IsRightMargin() ); + const bool bRet = pFrame->UnitUp_( pPam, nOffset, bSetInReadOnly ); + + // No SwTextCursor::SetRightMargin( false ); + // Instead we have a SwSetToRightMargin in UnitUp_ + return bRet; +} + +bool SwTextFrame::UnitDown(SwPaM *pPam, const SwTwips nOffset, + bool bSetInReadOnly ) const +{ + const SwTextFrame *pFrame = GetAdjFrameAtPos(const_cast<SwTextFrame*>(this), *(pPam->GetPoint()), + SwTextCursor::IsRightMargin() ); + const bool bRet = pFrame->UnitDown_( pPam, nOffset, bSetInReadOnly ); + SwTextCursor::SetRightMargin( false ); + return bRet; +} + +void SwTextFrame::FillCursorPos( SwFillData& rFill ) const +{ + if( !rFill.bColumn && GetUpper()->IsColBodyFrame() ) // ColumnFrames now with BodyFrame + { + const SwColumnFrame* pTmp = + static_cast<const SwColumnFrame*>(GetUpper()->GetUpper()->GetUpper()->Lower()); // The 1st column + // The first SwFrame in BodyFrame of the first column + const SwFrame* pFrame = static_cast<const SwLayoutFrame*>(pTmp->Lower())->Lower(); + sal_uInt16 nNextCol = 0; + // In which column do we end up in? + while( rFill.X() > pTmp->getFrameArea().Right() && pTmp->GetNext() ) + { + pTmp = static_cast<const SwColumnFrame*>(pTmp->GetNext()); + if( static_cast<const SwLayoutFrame*>(pTmp->Lower())->Lower() ) // ColumnFrames now with BodyFrame + { + pFrame = static_cast<const SwLayoutFrame*>(pTmp->Lower())->Lower(); + nNextCol = 0; + } + else + ++nNextCol; // Empty columns require column breaks + } + if( pTmp != GetUpper()->GetUpper() ) // Did we end up in another column? + { + if( !pFrame ) + return; + if( nNextCol ) + { + while( pFrame->GetNext() ) + pFrame = pFrame->GetNext(); + } + else + { + while( pFrame->GetNext() && pFrame->getFrameArea().Bottom() < rFill.Y() ) + pFrame = pFrame->GetNext(); + } + // No filling, if the last frame in the targeted column does + // not contain a paragraph, but e.g. a table + if( pFrame->IsTextFrame() ) + { + rFill.Fill().nColumnCnt = nNextCol; + rFill.bColumn = true; + if( rFill.pPos ) + { + SwTextFrame const*const pTextFrame(static_cast<const SwTextFrame*>(pFrame)); + *rFill.pPos = pTextFrame->MapViewToModelPos( + TextFrameIndex(pTextFrame->GetText().getLength())); + } + if( nNextCol ) + { + rFill.aFrame = pTmp->getFramePrintArea(); + rFill.aFrame += pTmp->getFrameArea().Pos(); + } + else + rFill.aFrame = pFrame->getFrameArea(); + static_cast<const SwTextFrame*>(pFrame)->FillCursorPos( rFill ); + } + return; + } + } + std::unique_ptr<SwFont> pFnt; + SwTextFormatColl* pColl = GetTextNodeForParaProps()->GetTextColl(); + SwTwips nFirst = GetTextNodeForParaProps()->GetSwAttrSet().GetULSpace().GetLower(); + SwTwips nDiff = rFill.Y() - getFrameArea().Bottom(); + if( nDiff < nFirst ) + nDiff = -1; + else + pColl = &pColl->GetNextTextFormatColl(); + SwAttrSet aSet(const_cast<SwDoc&>(GetDoc()).GetAttrPool(), aTextFormatCollSetRange ); + const SwAttrSet* pSet = &pColl->GetAttrSet(); + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + if (GetTextNodeForParaProps()->HasSwAttrSet()) + { + // sw_redlinehide: pSet is mostly used for para props, but there are + // accesses to char props via pFnt - why does it use only the node's + // props for this, and not hints? + aSet.Put( *GetTextNodeForParaProps()->GetpSwAttrSet() ); + aSet.SetParent( pSet ); + pSet = &aSet; + pFnt.reset(new SwFont( pSet, &GetDoc().getIDocumentSettingAccess() )); + } + else + { + SwFontAccess aFontAccess( pColl, pSh ); + pFnt.reset(new SwFont( aFontAccess.Get()->GetFont() )); + pFnt->CheckFontCacheId( pSh, pFnt->GetActual() ); + } + OutputDevice* pOut = pSh->GetOut(); + if( !pSh->GetViewOptions()->getBrowseMode() || pSh->GetViewOptions()->IsPrtFormat() ) + pOut = GetDoc().getIDocumentDeviceAccess().getReferenceDevice( true ); + + pFnt->SetFntChg( true ); + pFnt->ChgPhysFnt( pSh, *pOut ); + + SwTwips nLineHeight = pFnt->GetHeight( pSh, *pOut ); + + bool bFill = false; + if( nLineHeight ) + { + bFill = true; + const SvxULSpaceItem &rUL = pSet->GetULSpace(); + SwTwips nDist = std::max( rUL.GetLower(), rUL.GetUpper() ); + if( rFill.Fill().nColumnCnt ) + { + rFill.aFrame.Height( nLineHeight ); + nDiff = rFill.Y() - rFill.Bottom(); + nFirst = 0; + } + else if( nDist < nFirst ) + nFirst = nFirst - nDist; + else + nFirst = 0; + nDist = std::max( nDist, SwTwips(GetLineSpace()) ); + nDist += nLineHeight; + nDiff -= nFirst; + + if( nDiff > 0 ) + { + nDiff /= nDist; + rFill.Fill().nParaCnt = o3tl::narrowing<sal_uInt16>(nDiff + 1); + rFill.nLineWidth = 0; + rFill.bInner = false; + rFill.bEmpty = true; + rFill.SetOrient( text::HoriOrientation::LEFT ); + } + else + nDiff = -1; + if( rFill.bInner ) + bFill = false; + else + { + const SvxTabStopItem &rRuler = pSet->GetTabStops(); + const SvxFirstLineIndentItem& rFirstLine(pSet->GetFirstLineIndent()); + const SvxTextLeftMarginItem& rTextLeftMargin(pSet->GetTextLeftMargin()); + const SvxRightMarginItem& rRightMargin(pSet->GetRightMargin()); + + SwRect &rRect = rFill.Fill().aCursor; + rRect.Top( rFill.Bottom() + (nDiff+1) * nDist - nLineHeight ); + if( nFirst && nDiff > -1 ) + rRect.Top( rRect.Top() + nFirst ); + rRect.Height( nLineHeight ); + SwTwips nLeft = rFill.Left() + rTextLeftMargin.GetLeft(rFirstLine) + + GetTextNodeForParaProps()->GetLeftMarginWithNum(); + SwTwips nRight = rFill.Right() - rRightMargin.GetRight(); + SwTwips nCenter = ( nLeft + nRight ) / 2; + rRect.Left( nLeft ); + if( SwFillMode::Margin == rFill.Mode() ) + { + if( rFill.bEmpty ) + { + rFill.SetOrient( text::HoriOrientation::LEFT ); + if( rFill.X() < nCenter ) + { + if( rFill.X() > ( nLeft + 2 * nCenter ) / 3 ) + { + rFill.SetOrient( text::HoriOrientation::CENTER ); + rRect.Left( nCenter ); + } + } + else if( rFill.X() > ( nRight + 2 * nCenter ) / 3 ) + { + rFill.SetOrient( text::HoriOrientation::RIGHT ); + rRect.Left( nRight ); + } + else + { + rFill.SetOrient( text::HoriOrientation::CENTER ); + rRect.Left( nCenter ); + } + } + else + bFill = false; + } + else + { + SwTwips nSpace = 0; + if( SwFillMode::Tab != rFill.Mode() ) + { + SwDrawTextInfo aDrawInf( pSh, *pOut, " ", 0, 2 ); + nSpace = pFnt->GetTextSize_( aDrawInf ).Width()/2; + } + if( rFill.X() >= nRight ) + { + if( SwFillMode::Indent != rFill.Mode() && ( rFill.bEmpty || + rFill.X() > rFill.nLineWidth + FILL_MIN_DIST ) ) + { + rFill.SetOrient( text::HoriOrientation::RIGHT ); + rRect.Left( nRight ); + } + else + bFill = false; + } + else if( SwFillMode::Indent == rFill.Mode() ) + { + SwTwips nIndent = rFill.X(); + if( !rFill.bEmpty || nIndent > nRight ) + bFill = false; + else + { + nIndent -= rFill.Left(); + if( nIndent >= 0 && nSpace ) + { + nIndent /= nSpace; + nIndent *= nSpace; + rFill.SetTab( sal_uInt16( nIndent ) ); + rRect.Left( nIndent + rFill.Left() ); + } + else + bFill = false; + } + } + else if( rFill.X() > nLeft ) + { + SwTwips nTextLeft = rFill.Left() + rTextLeftMargin.GetTextLeft() + + GetTextNodeForParaProps()->GetLeftMarginWithNum(true); + rFill.nLineWidth += rFill.bFirstLine ? nLeft : nTextLeft; + SwTwips nLeftTab; + SwTwips nRightTab = nLeft; + sal_uInt16 nSpaceCnt = 0; + sal_uInt16 nSpaceOnlyCnt = 0; + sal_uInt16 nTabCnt = 0; + sal_uInt16 nIdx = 0; + do + { + nLeftTab = nRightTab; + if( nIdx < rRuler.Count() ) + { + const SvxTabStop &rTabStop = rRuler.operator[](nIdx); + nRightTab = nTextLeft + rTabStop.GetTabPos(); + if( nLeftTab < nTextLeft && nRightTab > nTextLeft ) + nRightTab = nTextLeft; + else + ++nIdx; + if( nRightTab > rFill.nLineWidth ) + ++nTabCnt; + } + else + { + const SvxTabStopItem& rTab = + pSet->GetPool()->GetDefaultItem( RES_PARATR_TABSTOP ); + const SwTwips nDefTabDist = rTab[0].GetTabPos(); + nRightTab = nLeftTab - nTextLeft; + nRightTab /= nDefTabDist; + nRightTab = nRightTab * nDefTabDist + nTextLeft; + while ( nRightTab <= nLeftTab ) + nRightTab += nDefTabDist; + if( nRightTab > rFill.nLineWidth ) + ++nTabCnt; + while ( nRightTab < rFill.X() ) + { + nRightTab += nDefTabDist; + if( nRightTab > rFill.nLineWidth ) + ++nTabCnt; + } + if( nLeftTab < nRightTab - nDefTabDist ) + nLeftTab = nRightTab - nDefTabDist; + } + if( nRightTab > nRight ) + nRightTab = nRight; + } + while( rFill.X() > nRightTab ); + --nTabCnt; + if( SwFillMode::TabSpace == rFill.Mode() ) + { + if( nSpace > 0 ) + { + if( !nTabCnt ) + nLeftTab = rFill.nLineWidth; + while( nLeftTab < rFill.X() ) + { + nLeftTab += nSpace; + ++nSpaceCnt; + } + if( nSpaceCnt ) + { + nLeftTab -= nSpace; + --nSpaceCnt; + } + if( rFill.X() - nLeftTab > nRightTab - rFill.X() ) + { + nSpaceCnt = 0; + ++nTabCnt; + rRect.Left( nRightTab ); + } + else + { + if( rFill.X() - nLeftTab > nSpace/2 ) + { + ++nSpaceCnt; + rRect.Left( nLeftTab + nSpace ); + } + else + rRect.Left( nLeftTab ); + } + } + else if( rFill.X() - nLeftTab < nRightTab - rFill.X() ) + rRect.Left( nLeftTab ); + else + { + if( nRightTab >= nRight ) + { + rFill.SetOrient( text::HoriOrientation::RIGHT ); + rRect.Left( nRight ); + nTabCnt = 0; + nSpaceCnt = 0; + } + else + { + rRect.Left( nRightTab ); + ++nTabCnt; + } + } + } + else if( SwFillMode::Space == rFill.Mode() ) + { + SwTwips nLeftSpace = nLeft; + while( nLeftSpace < rFill.X() ) + { + nLeftSpace += nSpace; + ++nSpaceOnlyCnt; + } + rRect.Left( nLeftSpace ); + } + else + { + if( rFill.X() - nLeftTab < nRightTab - rFill.X() ) + rRect.Left( nLeftTab ); + else + { + if( nRightTab >= nRight ) + { + rFill.SetOrient( text::HoriOrientation::RIGHT ); + rRect.Left( nRight ); + nTabCnt = 0; + nSpaceCnt = 0; + } + else + { + rRect.Left( nRightTab ); + ++nTabCnt; + } + } + } + rFill.SetTab( nTabCnt ); + rFill.SetSpace( nSpaceCnt ); + rFill.SetSpaceOnly( nSpaceOnlyCnt ); + if( bFill ) + { + if( std::abs( rFill.X() - nCenter ) <= + std::abs( rFill.X() - rRect.Left() ) ) + { + rFill.SetOrient( text::HoriOrientation::CENTER ); + rFill.SetTab( 0 ); + rFill.SetSpace( 0 ); + rFill.SetSpaceOnly( 0 ); + rRect.Left( nCenter ); + } + if( !rFill.bEmpty ) + rFill.nLineWidth += FILL_MIN_DIST; + if( rRect.Left() < rFill.nLineWidth ) + bFill = false; + } + } + } + // Do we extend over the page's/column's/etc. lower edge? + const SwFrame* pUp = GetUpper(); + if( pUp->IsInSct() ) + { + if( pUp->IsSctFrame() ) + pUp = pUp->GetUpper(); + else if( pUp->IsColBodyFrame() && + pUp->GetUpper()->GetUpper()->IsSctFrame() ) + pUp = pUp->GetUpper()->GetUpper()->GetUpper(); + } + SwRectFnSet aRectFnSet(this); + SwTwips nLimit = aRectFnSet.GetPrtBottom(*pUp); + SwTwips nRectBottom = rRect.Bottom(); + if ( aRectFnSet.IsVert() ) + nRectBottom = SwitchHorizontalToVertical( nRectBottom ); + + if( aRectFnSet.YDiff( nLimit, nRectBottom ) < 0 ) + bFill = false; + else + rRect.Width( 1 ); + } + } + const_cast<SwCursorMoveState*>(rFill.pCMS)->m_bFillRet = bFill; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/frmform.cxx b/sw/source/core/text/frmform.cxx new file mode 100644 index 0000000000..76a1df5621 --- /dev/null +++ b/sw/source/core/text/frmform.cxx @@ -0,0 +1,2299 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <config_wasm_strip.h> + +#include <sal/config.h> +#include <sal/log.hxx> + +#include <IDocumentRedlineAccess.hxx> +#include <anchoredobject.hxx> +#include <bodyfrm.hxx> +#include <hintids.hxx> +#include <editeng/keepitem.hxx> +#include <editeng/hyphenzoneitem.hxx> +#include <pagefrm.hxx> +#include <ndtxt.hxx> +#include <ftnfrm.hxx> +#include <txtftn.hxx> +#include <fmtftn.hxx> +#include <paratr.hxx> +#include <viewopt.hxx> +#include <viewsh.hxx> +#include <frmatr.hxx> +#include <pam.hxx> +#include <fmtanchr.hxx> +#include "itrform2.hxx" +#include "widorp.hxx" +#include "txtcache.hxx" +#include <sectfrm.hxx> +#include <rootfrm.hxx> +#include <frmfmt.hxx> +#include <sortedobjs.hxx> +#include <editeng/tstpitem.hxx> +#include <redline.hxx> +#include <comphelper/lok.hxx> +#include <flyfrms.hxx> +#include <frmtool.hxx> +#include <layouter.hxx> + +// Tolerance in formatting and text output +#define SLOPPY_TWIPS 5 + +namespace { + +class FormatLevel +{ + static sal_uInt16 s_nLevel; +public: + FormatLevel() { ++s_nLevel; } + ~FormatLevel() { --s_nLevel; } + static sal_uInt16 GetLevel() { return s_nLevel; } + static bool LastLevel() { return 10 < s_nLevel; } +}; + +} + +sal_uInt16 FormatLevel::s_nLevel = 0; + +void ValidateText( SwFrame *pFrame ) // Friend of frame +{ + if ( ( ! pFrame->IsVertical() && + pFrame->getFrameArea().Width() == pFrame->GetUpper()->getFramePrintArea().Width() ) || + ( pFrame->IsVertical() && + pFrame->getFrameArea().Height() == pFrame->GetUpper()->getFramePrintArea().Height() ) ) + { + pFrame->setFrameAreaSizeValid(true); + } +} + +void SwTextFrame::ValidateFrame() +{ + vcl::RenderContext* pRenderContext = getRootFrame()->GetCurrShell()->GetOut(); + // Validate surroundings to avoid oscillation + SwSwapIfSwapped swap( this ); + + if ( !IsInFly() && !IsInTab() ) + { // Only validate 'this' when inside a fly, the rest should actually only be + // needed for footnotes, which do not exist in flys. + SwSectionFrame* pSct = FindSctFrame(); + if( pSct ) + { + if( !pSct->IsColLocked() ) + pSct->ColLock(); + else + pSct = nullptr; + } + + SwFrame *pUp = GetUpper(); + pUp->Calc(pRenderContext); + if( pSct ) + pSct->ColUnlock(); + } + ValidateText( this ); + + // We at least have to save the MustFit flag! + assert(HasPara() && "ResetPreps(), missing ParaPortion, SwCache bug?"); + SwParaPortion *pPara = GetPara(); + const bool bMustFit = pPara->IsPrepMustFit(); + ResetPreps(); + pPara->SetPrepMustFit( bMustFit ); +} + +// After a RemoveFootnote the BodyFrame and all Frames contained within it, need to be +// recalculated, so that the DeadLine is right. +// First we search outwards, on the way back we calculate everything. +static void ValidateBodyFrame_( SwFrame *pFrame ) +{ + vcl::RenderContext* pRenderContext = pFrame ? pFrame->getRootFrame()->GetCurrShell()->GetOut() : nullptr; + if( !pFrame || pFrame->IsCellFrame() ) + return; + + if( !pFrame->IsBodyFrame() && pFrame->GetUpper() ) + ValidateBodyFrame_( pFrame->GetUpper() ); + if( !pFrame->IsSctFrame() ) + pFrame->Calc(pRenderContext); + else + { + const bool bOld = static_cast<SwSectionFrame*>(pFrame)->IsContentLocked(); + static_cast<SwSectionFrame*>(pFrame)->SetContentLock( true ); + pFrame->Calc(pRenderContext); + if( !bOld ) + static_cast<SwSectionFrame*>(pFrame)->SetContentLock( false ); + } +} + +void SwTextFrame::ValidateBodyFrame() +{ + SwSwapIfSwapped swap( this ); + + // See comment in ValidateFrame() + if ( !IsInFly() && !IsInTab() && + !( IsInSct() && FindSctFrame()->Lower()->IsColumnFrame() ) ) + ValidateBodyFrame_( GetUpper() ); +} + +bool SwTextFrame::GetDropRect_( SwRect &rRect ) const +{ + SwSwapIfNotSwapped swap(const_cast<SwTextFrame *>(this)); + + OSL_ENSURE( HasPara(), "SwTextFrame::GetDropRect_: try again next year." ); + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(this) ); + SwTextMargin aLine( const_cast<SwTextFrame*>(this), &aInf ); + if( aLine.GetDropLines() ) + { + rRect.Top( aLine.Y() ); + rRect.Left( aLine.GetLineStart() ); + rRect.Height( aLine.GetDropHeight() ); + rRect.Width( aLine.GetDropLeft() ); + + if ( IsRightToLeft() ) + SwitchLTRtoRTL( rRect ); + + if ( IsVertical() ) + SwitchHorizontalToVertical( rRect ); + return true; + } + + return false; +} + +bool SwTextFrame::CalcFollow(TextFrameIndex const nTextOfst) +{ + vcl::RenderContext* pRenderContext = getRootFrame()->GetCurrShell()->GetOut(); + SwSwapIfSwapped swap( this ); + + OSL_ENSURE( HasFollow(), "CalcFollow: missing Follow." ); + + SwTextFrame* pMyFollow = GetFollow(); + + SwParaPortion *pPara = GetPara(); + const bool bFollowField = pPara && pPara->IsFollowField(); + + if( !pMyFollow->GetOffset() || pMyFollow->GetOffset() != nTextOfst || + bFollowField || pMyFollow->IsFieldFollow() || + ( pMyFollow->IsVertical() && !pMyFollow->getFramePrintArea().Width() ) || + ( ! pMyFollow->IsVertical() && !pMyFollow->getFramePrintArea().Height() ) ) + { +#if OSL_DEBUG_LEVEL > 0 + const SwFrame *pOldUp = GetUpper(); +#endif + + SwRectFnSet aRectFnSet(this); + SwTwips nOldBottom = aRectFnSet.GetBottom(GetUpper()->getFrameArea()); + SwTwips nMyPos = aRectFnSet.GetTop(getFrameArea()); + + const SwPageFrame *pPage = nullptr; + bool bOldInvaContent = true; + if ( !IsInFly() && GetNext() ) + { + pPage = FindPageFrame(); + // Minimize (reset if possible) invalidations: see below + bOldInvaContent = pPage->IsInvalidContent(); + } + + pMyFollow->SetOffset_( nTextOfst ); + pMyFollow->SetFieldFollow( bFollowField ); + if( HasFootnote() || pMyFollow->HasFootnote() ) + { + ValidateFrame(); + ValidateBodyFrame(); + if( pPara ) + { + pPara->GetReformat() = SwCharRange(); + pPara->SetDelta(0); + } + } + + // The footnote area must not get larger + SwSaveFootnoteHeight aSave( FindFootnoteBossFrame( true ), LONG_MAX ); + + pMyFollow->CalcFootnoteFlag(); + if ( !pMyFollow->GetNext() && !pMyFollow->HasFootnote() ) + nOldBottom = aRectFnSet.IsVert() ? 0 : LONG_MAX; + + // tdf#122892 check flag: + // 1. WidowsAndOrphans::FindWidows() determines follow is a widow + // 2. SwTextFrame::PrepWidows() calls SetPrepWidows() on master; + // if it can spare lines, master truncates one line + // 3. SwTextFrame::CalcPreps() on master (below); + // unless IsPrepMustFit(), if master hasn't shrunk via 2., it will SetWidow() + // 4. loop must exit then, because the follow didn't grow so nothing will ever change + while (!IsWidow()) + { + if( !FormatLevel::LastLevel() ) + { + // If the follow is contained within a column section or column + // frame, we need to calculate that first. This is because the + // FormatWidthCols() does not work if it is called from MakeAll + // of the _locked_ follow. + SwSectionFrame* pSct = pMyFollow->FindSctFrame(); + if( pSct && !pSct->IsAnLower( this ) ) + { + if( pSct->GetFollow() ) + pSct->SimpleFormat(); + else if( ( pSct->IsVertical() && !pSct->getFrameArea().Width() ) || + ( ! pSct->IsVertical() && !pSct->getFrameArea().Height() ) ) + break; + } + // i#11760 - Intrinsic format of follow is controlled. + if ( FollowFormatAllowed() ) + { + // i#11760 - No nested format of follows, if + // text frame is contained in a column frame. + // Thus, forbid intrinsic format of follow. + { + bool bIsFollowInColumn = false; + SwFrame* pFollowUpper = pMyFollow->GetUpper(); + while ( pFollowUpper ) + { + if ( pFollowUpper->IsColumnFrame() ) + { + bIsFollowInColumn = true; + break; + } + if ( pFollowUpper->IsPageFrame() || + pFollowUpper->IsFlyFrame() ) + { + break; + } + pFollowUpper = pFollowUpper->GetUpper(); + } + if ( bIsFollowInColumn ) + { + pMyFollow->ForbidFollowFormat(); + } + } + + pMyFollow->Calc(pRenderContext); + // The Follow can tell from its getFrameArea().Height() that something went wrong + OSL_ENSURE( !pMyFollow->GetPrev(), "SwTextFrame::CalcFollow: cheesy follow" ); + if( pMyFollow->GetPrev() ) + { + pMyFollow->Prepare(); + pMyFollow->Calc(pRenderContext); + OSL_ENSURE( !pMyFollow->GetPrev(), "SwTextFrame::CalcFollow: very cheesy follow" ); + } + + // i#11760 - Reset control flag for follow format. + pMyFollow->AllowFollowFormat(); + } + + // Make sure that the Follow gets painted + pMyFollow->SetCompletePaint(); + } + + pPara = GetPara(); + // As long as the Follow requests lines due to Orphans, it is + // passed these and is formatted again if possible + if( pPara && pPara->IsPrepWidows() ) + CalcPreps(); + else + break; + } + + if( HasFootnote() || pMyFollow->HasFootnote() ) + { + ValidateBodyFrame(); + ValidateFrame(); + if( pPara ) + { + pPara->GetReformat() = SwCharRange(); + pPara->SetDelta(0); + } + } + + if ( pPage && !bOldInvaContent ) + pPage->ValidateContent(); + +#if OSL_DEBUG_LEVEL > 0 + OSL_ENSURE( pOldUp == GetUpper(), "SwTextFrame::CalcFollow: heavy follow" ); +#endif + + const tools::Long nRemaining = + - aRectFnSet.BottomDist( GetUpper()->getFrameArea(), nOldBottom ); + if ( nRemaining > 0 && + nRemaining != ( aRectFnSet.IsVert() ? + nMyPos - getFrameArea().Right() : + getFrameArea().Top() - nMyPos ) ) + { + return true; + } + } + + return false; +} + +void SwTextFrame::MakePos() +{ + Point aOldPos = getFrameArea().Pos(); + SwFrame::MakePos(); + + // Recalc split flys if our position changed. + if (aOldPos != getFrameArea().Pos()) + { + // Find the master frame. + const SwTextFrame* pMaster = this; + while (pMaster->IsFollow()) + { + pMaster = pMaster->FindMaster(); + } + // Find which flys are effectively anchored to this frame. + for (const auto& pFly : pMaster->GetSplitFlyDrawObjs()) + { + SwTextFrame* pFlyAnchor = pFly->FindAnchorCharFrame(); + if (pFlyAnchor != this) + { + continue; + } + // Possibly this fly was positioned relative to us, invalidate its position now that our + // position is changed. + SwPageFrame* pPageFrame = pFly->FindPageFrame(); + bool bFlyNeedsPositioning = false; + bool bFlyPageMismatch = false; + if (pPageFrame) + { + // Was the position just adjusted to be inside the page frame? + bFlyNeedsPositioning = pFly->getFrameArea().Pos() == pPageFrame->getFrameArea().Pos(); + // Is the fly on a page different than the anchor frame? + bFlyPageMismatch = pPageFrame != FindPageFrame(); + } + if (bFlyNeedsPositioning || bFlyPageMismatch) + { + // Not really positioned, unlock the position once to allow a recalc. + pFly->UnlockPosition(); + } + pFly->InvalidatePos(); + } + } + + // Inform LOK clients about change in position of redlines (if any) + if(!comphelper::LibreOfficeKit::isActive()) + return; + + SwTextNode const* pTextNode = GetTextNodeFirst(); + const SwRedlineTable& rTable = pTextNode->getIDocumentRedlineAccess().GetRedlineTable(); + for (SwRedlineTable::size_type nRedlnPos = 0; nRedlnPos < rTable.size(); ++nRedlnPos) + { + SwRangeRedline* pRedln = rTable[nRedlnPos]; + if (pTextNode->GetIndex() == pRedln->GetPoint()->GetNode().GetIndex()) + { + pRedln->MaybeNotifyRedlinePositionModification(getFrameArea().Top()); + if (GetMergedPara() + && pRedln->GetType() == RedlineType::Delete + && pRedln->GetPoint()->GetNode() != pRedln->GetMark()->GetNode()) + { + pTextNode = pRedln->End()->GetNode().GetTextNode(); + } + } + } +} + +void SwTextFrame::AdjustFrame( const SwTwips nChgHght, bool bHasToFit ) +{ + vcl::RenderContext* pRenderContext = getRootFrame()->GetCurrShell()->GetOut(); + if( IsUndersized() ) + { + if( GetOffset() && !IsFollow() ) // A scrolled paragraph (undersized) + return; + SetUndersized( nChgHght == 0 || bHasToFit ); + } + + // AdjustFrame is called with a swapped frame during + // formatting but the frame is not swapped during FormatEmpty + SwSwapIfSwapped swap( this ); + SwRectFnSet aRectFnSet(this); + + // The Frame's size variable is incremented by Grow or decremented by Shrink. + // If the size cannot change, nothing should happen! + if( nChgHght >= 0) + { + SwTwips nChgHeight = nChgHght; + if( nChgHght && !bHasToFit ) + { + if( IsInFootnote() && !IsInSct() ) + { + SwTwips nReal = Grow( nChgHght, true ); + if( nReal < nChgHght ) + { + SwTwips nBot = aRectFnSet.YInc( aRectFnSet.GetBottom(getFrameArea()), + nChgHght - nReal ); + SwFrame* pCont = FindFootnoteFrame()->GetUpper(); + + if( aRectFnSet.BottomDist( pCont->getFrameArea(), nBot ) > 0 ) + { + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aRectFnSet.AddBottom( aFrm, nChgHght ); + + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + + if( aRectFnSet.IsVert() ) + { + aPrt.AddWidth(nChgHght ); + } + else + { + aPrt.AddHeight(nChgHght ); + } + + return; + } + } + } + + Grow( nChgHght ); + + if ( IsInFly() ) + { + // If one of the Upper is a Fly, it's very likely that this fly changes its + // position by the Grow. Therefore, my position has to be corrected also or + // the check further down is not meaningful. + // The predecessors need to be calculated, so that the position can be + // calculated correctly. + if ( GetPrev() ) + { + SwFrame *pPre = GetUpper()->Lower(); + do + { pPre->Calc(pRenderContext); + pPre = pPre->GetNext(); + } while ( pPre && pPre != this ); + } + const Point aOldPos( getFrameArea().Pos() ); + MakePos(); + if ( aOldPos != getFrameArea().Pos() ) + { + InvalidateObjs(false); + } + } + nChgHeight = 0; + } + // A Grow() is always accepted by the Layout, even if the + // FixSize of the surrounding layout frame should not allow it. + // We text for this case and correct the values. + // The Frame must NOT be shrunk further than its size permits + // even in the case of an emergency. + SwTwips nRstHeight; + if ( IsVertical() ) + { + OSL_ENSURE( ! IsSwapped(),"Swapped frame while calculating nRstHeight" ); + + if ( IsVertLR() ) + nRstHeight = GetUpper()->getFrameArea().Left() + + GetUpper()->getFramePrintArea().Left() + + GetUpper()->getFramePrintArea().Width() + - getFrameArea().Left(); + else + nRstHeight = getFrameArea().Left() + getFrameArea().Width() - + ( GetUpper()->getFrameArea().Left() + GetUpper()->getFramePrintArea().Left() ); + } + else + nRstHeight = GetUpper()->getFrameArea().Top() + + GetUpper()->getFramePrintArea().Top() + + GetUpper()->getFramePrintArea().Height() + - getFrameArea().Top(); + + // We can get a bit of space in table cells, because there could be some + // left through a vertical alignment to the top. + // Assure that first lower in upper is the current one or is valid. + if ( IsInTab() && + ( GetUpper()->Lower() == this || + GetUpper()->Lower()->isFrameAreaDefinitionValid() ) ) + { + tools::Long nAdd = aRectFnSet.YDiff( aRectFnSet.GetTop(GetUpper()->Lower()->getFrameArea()), + aRectFnSet.GetPrtTop(*GetUpper()) ); + OSL_ENSURE( nAdd >= 0, "Ey" ); + nRstHeight += nAdd; + } + + // nRstHeight < 0 means that the TextFrame is located completely outside of its Upper. + // This can happen, if it's located within a FlyAtContentFrame, which changed sides by a + // Grow(). In such a case, it's wrong to execute the following Grow(). + // In the case of a bug, we end up with an infinite loop. + SwTwips nFrameHeight = aRectFnSet.GetHeight(getFrameArea()); + SwTwips nPrtHeight = aRectFnSet.GetHeight(getFramePrintArea()); + + if( nRstHeight < nFrameHeight ) + { + // It can be that I have the right size, but the Upper is too small and can get me some room + if( ( nRstHeight >= 0 || ( IsInFootnote() && IsInSct() ) ) && !bHasToFit ) + nRstHeight += GetUpper()->Grow( nFrameHeight - nRstHeight ); + // In column sections we do not want to get too big or else more areas are created by + // GetNextSctLeaf. Instead, we shrink and remember bUndersized, so that FormatWidthCols + // can calculate the right column size. + if ( nRstHeight < nFrameHeight ) + { + if( bHasToFit || !IsMoveable() || + ( IsInSct() && !FindSctFrame()->MoveAllowed(this) ) ) + { + SetUndersized( true ); + Shrink( std::min( ( nFrameHeight - nRstHeight), nPrtHeight ) ); + } + else + SetUndersized( false ); + } + } + else if( nChgHeight ) + { + if( nRstHeight - nFrameHeight < nChgHeight ) + nChgHeight = nRstHeight - nFrameHeight; + if( nChgHeight ) + Grow( nChgHeight ); + } + } + else + Shrink( -nChgHght ); +} + +css::uno::Sequence< css::style::TabStop > SwTextFrame::GetTabStopInfo( SwTwips CurrentPos ) +{ + SwTextFormatInfo aInf( getRootFrame()->GetCurrShell()->GetOut(), this ); + SwTextFormatter aLine( this, &aInf ); + SwTextCursor TextCursor( this, &aInf ); + const Point aCharPos( TextCursor.GetTopLeft() ); + + SwTwips nRight = aLine.Right(); + CurrentPos -= aCharPos.X(); + + // get current tab stop information stored in the Frame + const SvxTabStop *pTS = aLine.GetLineInfo().GetTabStop( CurrentPos, nRight ); + + if( !pTS ) + { + return {}; + } + + // copy tab stop information into a Sequence, which only contains one element. + css::style::TabStop ts; + ts.Position = pTS->GetTabPos(); + ts.DecimalChar = pTS->GetDecimal(); + ts.FillChar = pTS->GetFill(); + switch( pTS->GetAdjustment() ) + { + case SvxTabAdjust::Left : ts.Alignment = css::style::TabAlign_LEFT; break; + case SvxTabAdjust::Center : ts.Alignment = css::style::TabAlign_CENTER; break; + case SvxTabAdjust::Right : ts.Alignment = css::style::TabAlign_RIGHT; break; + case SvxTabAdjust::Decimal: ts.Alignment = css::style::TabAlign_DECIMAL; break; + case SvxTabAdjust::Default: ts.Alignment = css::style::TabAlign_DEFAULT; break; + default: break; // prevent warning + } + + return { ts }; +} + +// AdjustFollow expects the following situation: +// The SwTextIter points to the lower end of the Master, the Offset is set in the Follow. +// nOffset holds the Offset in the text string, from which the Master closes +// and the Follow starts. +// If it's 0, the FollowFrame is deleted. +void SwTextFrame::AdjustFollow_( SwTextFormatter &rLine, + const TextFrameIndex nOffset, const TextFrameIndex nEnd, + const sal_uInt8 nMode ) +{ + SwFrameSwapper aSwapper( this, false ); + + // We got the rest of the text mass: Delete all Follows + // DummyPortions() are a special case. + // Special cases are controlled by parameter <nMode>. + bool bDontJoin = nMode & 1; + if( HasFollow() && !bDontJoin && nOffset == nEnd ) + { + while( GetFollow() ) + { + if( GetFollow()->IsLocked() ) + { + // this can happen when follow calls pMaster->GetFormatted() + SAL_INFO("sw.core", "+SwTextFrame::JoinFrame: Follow is locked." ); + return; + } + if (GetFollow()->IsDeleteForbidden()) + return; + + if (HasNonLastSplitFlyDrawObj()) + { + // If a fly frame is anchored to us that has a follow, then don't join the anchor. + // First those fly frames have to be joined. + return; + } + + JoinFrame(); + } + + return; + } + + // Dancing on the volcano: We'll just format the last line quickly + // for the QuoVadis stuff. + // The Offset can move of course: + const TextFrameIndex nNewOfst = (IsInFootnote() && (!GetIndNext() || HasFollow())) + ? rLine.FormatQuoVadis(nOffset) : nOffset; + + bool bHasNonLastSplitFlyDrawObj = false; + if (GetFollow() && GetOffset() == GetFollow()->GetOffset()) + { + bHasNonLastSplitFlyDrawObj = HasNonLastSplitFlyDrawObj(); + } + + if( !bDontJoin ) + { + // We steal text mass from our Follows + // It can happen that we have to join some of them + while( GetFollow() && GetFollow()->GetFollow() && + nNewOfst >= GetFollow()->GetFollow()->GetOffset() ) + { + if (bHasNonLastSplitFlyDrawObj) + { + // A non-last split fly is anchored to us, don't move content from the last frame to + // this one and don't join. + return; + } + + JoinFrame(); + } + } + + if (IsEmptyMasterWithSplitFly()) + { + // A split fly is anchored to us, don't move content from the follow frame to this one. + return; + } + + // The Offset moved + if( GetFollow() ) + { + if (!bDontJoin && bHasNonLastSplitFlyDrawObj) + { + // A non-last split fly is anchored to us, our follow is the last one in the text frame + // chain. No move of text from that follow to this text frame. + return; + } + + if ( nMode ) + GetFollow()->ManipOfst(TextFrameIndex(0)); + + if ( CalcFollow( nNewOfst ) ) // CalcFollow only at the end, we do a SetOffset there + rLine.SetOnceMore( true ); + } +} + +SwContentFrame *SwTextFrame::JoinFrame() +{ + OSL_ENSURE( GetFollow(), "+SwTextFrame::JoinFrame: no follow" ); + SwTextFrame *pFoll = GetFollow(); + + SwTextFrame *pNxt = pFoll->GetFollow(); + + // All footnotes of the to-be-destroyed Follow are relocated to us + TextFrameIndex nStart = pFoll->GetOffset(); + if ( pFoll->HasFootnote() ) + { + SwFootnoteBossFrame *pFootnoteBoss = nullptr; + SwFootnoteBossFrame *pEndBoss = nullptr; + SwTextNode const* pNode(nullptr); + sw::MergedAttrIter iter(*pFoll); + for (SwTextAttr const* pHt = iter.NextAttr(&pNode); pHt; pHt = iter.NextAttr(&pNode)) + { + if (RES_TXTATR_FTN == pHt->Which() + && nStart <= pFoll->MapModelToView(pNode, pHt->GetStart())) + { + if (pHt->GetFootnote().IsEndNote()) + { + if (!pEndBoss) + pEndBoss = pFoll->FindFootnoteBossFrame(); + SwFootnoteBossFrame::ChangeFootnoteRef( pFoll, static_cast<const SwTextFootnote*>(pHt), this ); + } + else + { + if (!pFootnoteBoss) + pFootnoteBoss = pFoll->FindFootnoteBossFrame( true ); + SwFootnoteBossFrame::ChangeFootnoteRef( pFoll, static_cast<const SwTextFootnote*>(pHt), this ); + } + SetFootnote( true ); + } + } + } + +#ifdef DBG_UTIL + else if ( pFoll->isFramePrintAreaValid() || + pFoll->isFrameAreaSizeValid() ) + { + pFoll->CalcFootnoteFlag(); + OSL_ENSURE( !pFoll->HasFootnote(), "Missing FootnoteFlag." ); + } +#endif + + pFoll->MoveFlyInCnt( this, nStart, TextFrameIndex(COMPLETE_STRING) ); + pFoll->SetFootnote( false ); + // i#27138 + // Notify accessibility paragraphs objects about changed CONTENT_FLOWS_FROM/_TO relation. + // Relation CONTENT_FLOWS_FROM for current next paragraph will change + // and relation CONTENT_FLOWS_TO for current previous paragraph, which + // is <this>, will change. +#if !ENABLE_WASM_STRIP_ACCESSIBILITY + { + SwViewShell* pViewShell( pFoll->getRootFrame()->GetCurrShell() ); + if ( pViewShell && pViewShell->GetLayout() && + pViewShell->GetLayout()->IsAnyShellAccessible() ) + { + auto pNext = pFoll->FindNextCnt( true ); + pViewShell->InvalidateAccessibleParaFlowRelation( + pNext ? pNext->DynCastTextFrame() : nullptr, + this ); + } + } +#endif + + pFoll->Cut(); + SetFollow(pNxt); + SwFrame::DestroyFrame(pFoll); + return pNxt; +} + +void SwTextFrame::SplitFrame(TextFrameIndex const nTextPos) +{ + SwSwapIfSwapped swap( this ); + + // The Paste sends a Modify() to me + // I lock myself, so that my data does not disappear + TextFrameLockGuard aLock( this ); + SwTextFrame *const pNew = static_cast<SwTextFrame *>(GetTextNodeFirst()->MakeFrame(this)); + + pNew->SetFollow( GetFollow() ); + SetFollow( pNew ); + + pNew->Paste( GetUpper(), GetNext() ); + // i#27138 + // notify accessibility paragraphs objects about changed CONTENT_FLOWS_FROM/_TO relation. + // Relation CONTENT_FLOWS_FROM for current next paragraph will change + // and relation CONTENT_FLOWS_TO for current previous paragraph, which + // is <this>, will change. +#if !ENABLE_WASM_STRIP_ACCESSIBILITY + { + SwViewShell* pViewShell( pNew->getRootFrame()->GetCurrShell() ); + if ( pViewShell && pViewShell->GetLayout() && + pViewShell->GetLayout()->IsAnyShellAccessible() ) + { + auto pNext = pNew->FindNextCnt( true ); + pViewShell->InvalidateAccessibleParaFlowRelation( + pNext ? pNext->DynCastTextFrame() : nullptr, + this ); + } + } +#endif + + // If footnotes end up in pNew bz our actions, we need + // to re-register them + if ( HasFootnote() ) + { + SwFootnoteBossFrame *pFootnoteBoss = nullptr; + SwFootnoteBossFrame *pEndBoss = nullptr; + SwTextNode const* pNode(nullptr); + sw::MergedAttrIter iter(*this); + for (SwTextAttr const* pHt = iter.NextAttr(&pNode); pHt; pHt = iter.NextAttr(&pNode)) + { + if (RES_TXTATR_FTN == pHt->Which() + && nTextPos <= MapModelToView(pNode, pHt->GetStart())) + { + if (pHt->GetFootnote().IsEndNote()) + { + if (!pEndBoss) + pEndBoss = FindFootnoteBossFrame(); + SwFootnoteBossFrame::ChangeFootnoteRef( this, static_cast<const SwTextFootnote*>(pHt), pNew ); + } + else + { + if (!pFootnoteBoss) + pFootnoteBoss = FindFootnoteBossFrame( true ); + SwFootnoteBossFrame::ChangeFootnoteRef( this, static_cast<const SwTextFootnote*>(pHt), pNew ); + } + pNew->SetFootnote( true ); + } + } + } + +#ifdef DBG_UTIL + else + { + CalcFootnoteFlag( nTextPos - TextFrameIndex(1) ); + OSL_ENSURE( !HasFootnote(), "Missing FootnoteFlag." ); + } +#endif + + MoveFlyInCnt( pNew, nTextPos, TextFrameIndex(COMPLETE_STRING) ); + + // No SetOffset or CalcFollow, because an AdjustFollow follows immediately anyways + + pNew->ManipOfst( nTextPos ); +} + +void SwTextFrame::SetOffset_(TextFrameIndex const nNewOfst) +{ + // We do not need to invalidate our Follow. + // We are a Follow, get formatted right away and call + // SetOffset() from there + mnOffset = nNewOfst; + SwParaPortion *pPara = GetPara(); + if( pPara ) + { + SwCharRange &rReformat = pPara->GetReformat(); + rReformat.Start() = TextFrameIndex(0); + rReformat.Len() = TextFrameIndex(GetText().getLength()); + pPara->SetDelta(sal_Int32(rReformat.Len())); + } + InvalidateSize(); +} + +bool SwTextFrame::CalcPreps() +{ + OSL_ENSURE( ! IsVertical() || ! IsSwapped(), "SwTextFrame::CalcPreps with swapped frame" ); + SwRectFnSet aRectFnSet(this); + + SwParaPortion *pPara = GetPara(); + if ( !pPara ) + return false; + const bool bPrep = pPara->IsPrep(); + const bool bPrepWidows = pPara->IsPrepWidows(); + const bool bPrepAdjust = pPara->IsPrepAdjust(); + const bool bPrepMustFit = pPara->IsPrepMustFit(); + ResetPreps(); + + bool bRet = false; + if( bPrep && !pPara->GetReformat().Len() ) + { + // PrepareHint::Widows means that the orphans rule got activated in the Follow. + // In unfortunate cases we could also have a PrepAdjust! + if( bPrepWidows ) + { + if( !GetFollow() ) + { + OSL_ENSURE( GetFollow(), "+SwTextFrame::CalcPreps: no credits" ); + return false; + } + + // We need to prepare for two cases: + // We were able to hand over a few lines to the Follow + // -> we need to shrink + // or we need to go on the next page + // -> we let our Frame become too big + + SwTwips nChgHeight = GetParHeight(); + if( nChgHeight >= aRectFnSet.GetHeight(getFramePrintArea()) ) + { + if( bPrepMustFit ) + { + GetFollow()->SetJustWidow( true ); + GetFollow()->Prepare(); + } + else if ( aRectFnSet.IsVert() ) + { + { + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aFrm.Width( aFrm.Width() + aFrm.Left() ); + aFrm.Left( 0 ); + } + + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + aPrt.Width( aPrt.Width() + getFrameArea().Left() ); + } + + SetWidow( true ); + } + else + { + // nTmp should be very large, but not so large as to cause overflow later (e.g., + // GetFrameOfModify in sw/source/core/layout/frmtool.cxx calculates nCurrentDist + // from, among others, the square of aDiff.getY(), which can be close to nTmp); + // the previously used value TWIPS_MAX/2 (i.e., (LONG_MAX - 1)/2) depended on + // the range of 'long', while the value (SAL_MAX_INT32 - 1)/2 (which matches the + // old value on platforms where 'long' is 'sal_Int32') is empirically shown to + // be large enough in practice even on platforms where 'long' is 'sal_Int64': + SwTwips const nTmp = sw::WIDOW_MAGIC - (getFrameArea().Top()+10000); + SwTwips nDiff = nTmp - getFrameArea().Height(); + + { + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aFrm.Height( nTmp ); + } + + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + aPrt.Height( aPrt.Height() + nDiff ); + } + + SetWidow( true ); + } + } + else + { + OSL_ENSURE( nChgHeight < aRectFnSet.GetHeight(getFramePrintArea()), + "+SwTextFrame::CalcPrep: want to shrink" ); + + nChgHeight = aRectFnSet.GetHeight(getFramePrintArea()) - nChgHeight; + + GetFollow()->SetJustWidow( true ); + GetFollow()->Prepare(); + Shrink( nChgHeight ); + SwRect &rRepaint = pPara->GetRepaint(); + + if ( aRectFnSet.IsVert() ) + { + SwRect aRepaint( getFrameArea().Pos() + getFramePrintArea().Pos(), getFramePrintArea().SSize() ); + SwitchVerticalToHorizontal( aRepaint ); + rRepaint.Chg( aRepaint.Pos(), aRepaint.SSize() ); + } + else + rRepaint.Chg( getFrameArea().Pos() + getFramePrintArea().Pos(), getFramePrintArea().SSize() ); + + if( 0 >= rRepaint.Width() ) + rRepaint.Width(1); + } + bRet = true; + } + else if ( bPrepAdjust ) + { + if ( HasFootnote() ) + { + if( !CalcPrepFootnoteAdjust() ) + { + if( bPrepMustFit ) + { + SwTextLineAccess aAccess( this ); + aAccess.GetPara()->SetPrepMustFit(true); + } + return false; + } + } + + { + SwSwapIfNotSwapped swap( this ); + + SwTextFormatInfo aInf( getRootFrame()->GetCurrShell()->GetOut(), this ); + SwTextFormatter aLine( this, &aInf ); + + WidowsAndOrphans aFrameBreak( this ); + // Whatever the attributes say: we split the paragraph in + // MustFit case if necessary + if( bPrepMustFit ) + { + aFrameBreak.SetKeep( false ); + aFrameBreak.ClrOrphLines(); + } + // Before calling FormatAdjust, we need to make sure + // that the lines protruding at the bottom get indeed + // truncated + bool bBreak = aFrameBreak.IsBreakNowWidAndOrp( aLine ); + bRet = true; + while( !bBreak && aLine.Next() ) + { + bBreak = aFrameBreak.IsBreakNowWidAndOrp( aLine ); + } + if( bBreak ) + { + // We run into troubles: when TruncLines is called, the + // conditions in IsInside change immediately such that + // IsBreakNow can return different results. + // For this reason, we tell rFrameBreak that the + // end is reached at the location of rLine. + // Let's see if it works ... + aLine.TruncLines(); + aFrameBreak.SetRstHeight( aLine ); + FormatAdjust( aLine, aFrameBreak, TextFrameIndex(aInf.GetText().getLength()), aInf.IsStop() ); + } + else + { + if( !GetFollow() ) + { + FormatAdjust( aLine, aFrameBreak, + TextFrameIndex(aInf.GetText().getLength()), aInf.IsStop() ); + } + else if ( !aFrameBreak.IsKeepAlways() ) + { + // We delete a line before the Master, because the Follow + // could hand over a line + const SwCharRange aFollowRg(GetFollow()->GetOffset(), TextFrameIndex(1)); + pPara->GetReformat() += aFollowRg; + // We should continue! + bRet = false; + } + } + } + + // A final check, if FormatAdjust() didn't help we need to + // truncate + if( bPrepMustFit ) + { + const SwTwips nMust = aRectFnSet.GetPrtBottom(*GetUpper()); + const SwTwips nIs = aRectFnSet.GetBottom(getFrameArea()); + + if( aRectFnSet.IsVert() && nIs < nMust ) + { + Shrink( nMust - nIs ); + + if( getFramePrintArea().Width() < 0 ) + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + aPrt.Width( 0 ); + } + + SetUndersized( true ); + } + else if ( ! aRectFnSet.IsVert() && nIs > nMust ) + { + Shrink( nIs - nMust ); + + if( getFramePrintArea().Height() < 0 ) + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + aPrt.Height( 0 ); + } + + SetUndersized( true ); + } + } + } + } + pPara->SetPrepMustFit( bPrepMustFit ); + return bRet; +} + +// Move the as-character objects - footnotes must be moved by RemoveFootnote! +void SwTextFrame::ChangeOffset( SwTextFrame* pFrame, TextFrameIndex nNew ) +{ + if( pFrame->GetOffset() < nNew ) + pFrame->MoveFlyInCnt( this, TextFrameIndex(0), nNew ); + else if( pFrame->GetOffset() > nNew ) + MoveFlyInCnt( pFrame, nNew, TextFrameIndex(COMPLETE_STRING) ); +} + +void SwTextFrame::FormatAdjust( SwTextFormatter &rLine, + WidowsAndOrphans &rFrameBreak, + TextFrameIndex const nStrLen, + const bool bDummy ) +{ + SwSwapIfNotSwapped swap( this ); + + SwParaPortion *pPara = rLine.GetInfo().GetParaPortion(); + + TextFrameIndex nEnd = rLine.GetStart(); + + const bool bHasToFit = pPara->IsPrepMustFit(); + + // The StopFlag is set by footnotes which want to go onto the next page + // Call base class method <SwTextFrameBreak::IsBreakNow(..)> + // instead of method <WidowsAndOrphans::IsBreakNow(..)> to get a break, + // even if due to widow rule no enough lines exists. + sal_uInt8 nNew = ( !GetFollow() && + nEnd < nStrLen && + ( rLine.IsStop() || + ( bHasToFit + ? ( rLine.GetLineNr() > 1 && + !rFrameBreak.IsInside( rLine ) ) + : rFrameBreak.IsBreakNow( rLine ) ) ) ) + ? 1 : 0; + + SwTextFormatInfo& rInf = rLine.GetInfo(); + if (nNew == 0 && !nStrLen && !rInf.GetTextFly().IsOn() && IsEmptyWithSplitFly()) + { + // Empty paragraph, so IsBreakNow() is not called, but we should split the fly portion and + // the paragraph marker. + nNew = 1; + } + + // i#84870 + // no split of text frame, which only contains an as-character anchored object + bool bOnlyContainsAsCharAnchoredObj = + !IsFollow() && nStrLen == TextFrameIndex(1) && + GetDrawObjs() && GetDrawObjs()->size() == 1 && + (*GetDrawObjs())[0]->GetFrameFormat().GetAnchor().GetAnchorId() == RndStdIds::FLY_AS_CHAR; + + // Still try split text frame if we have columns. + if (FindColFrame()) + bOnlyContainsAsCharAnchoredObj = false; + + if ( nNew && bOnlyContainsAsCharAnchoredObj ) + { + nNew = 0; + } + + if ( nNew ) + { + SplitFrame( nEnd ); + } + + const SwFrame *pBodyFrame = FindBodyFrame(); + + const tools::Long nBodyHeight = pBodyFrame ? ( IsVertical() ? + pBodyFrame->getFrameArea().Width() : + pBodyFrame->getFrameArea().Height() ) : 0; + + // If the current values have been calculated, show that they + // are valid now + pPara->GetReformat() = SwCharRange(); + bool bDelta = pPara->GetDelta() != 0; + pPara->SetDelta(0); + + if( rLine.IsStop() ) + { + rLine.TruncLines( true ); + nNew = 1; + } + + // FindBreak truncates the last line + if( !rFrameBreak.FindBreak( this, rLine, bHasToFit ) ) + { + // If we're done formatting, we set nEnd to the end. + // AdjustFollow might execute JoinFrame() because of this. + // Else, nEnd is the end of the last line in the Master. + TextFrameIndex nOld = nEnd; + // Make sure content from the last floating table anchor is not shifted to previous anchors. + if (!HasNonLastSplitFlyDrawObj()) + { + nEnd = rLine.GetEnd(); + } + if( GetFollow() ) + { + if( nNew && nOld < nEnd ) + RemoveFootnote( nOld, nEnd - nOld ); + ChangeOffset( GetFollow(), nEnd ); + if( !bDelta ) + GetFollow()->ManipOfst( nEnd ); + } + } + else + { // If we pass over lines, we must not call Join in Follows, instead we even + // need to create a Follow. + // We also need to do this if the whole mass of text remains in the Master, + // because a hard line break could necessitate another line (without text mass)! + TextFrameIndex const nOld(nEnd); + nEnd = rLine.GetEnd(); + if( GetFollow() ) + { + // Another case for not joining the follow: + // Text frame has no content, but a numbering. Then, do *not* join. + // Example of this case: When an empty, but numbered paragraph + // at the end of page is completely displaced by a fly frame. + // Thus, the text frame introduced a follow by a + // <SwTextFrame::SplitFrame(..)> - see below. The follow then shows + // the numbering and must stay. + if ( GetFollow()->GetOffset() != nEnd || + GetFollow()->IsFieldFollow() || + (nStrLen == TextFrameIndex(0) && GetTextNodeForParaProps()->GetNumRule())) + { + nNew |= 3; + } + else if (FindTabFrame() && nEnd > TextFrameIndex(0) && + rLine.GetInfo().GetChar(nEnd - TextFrameIndex(1)) == CH_BREAK) + { + // We are in a table, the paragraph has a follow and the text + // ends with a hard line break. Don't join the follow just + // because the follow would have no content, we may still need it + // for the paragraph mark. + nNew |= 1; + } + // move footnotes if the follow is kept - if RemoveFootnote() is + // called in next format iteration, it will be with the *new* + // offset so no effect! + if (nNew && nOld < nEnd) + { + RemoveFootnote(nOld, nEnd - nOld); + } + ChangeOffset( GetFollow(), nEnd ); + + SwFlyAtContentFrame* pNonLastSplitFlyDrawObj = HasNonLastSplitFlyDrawObj(); + if (pNonLastSplitFlyDrawObj && !pNonLastSplitFlyDrawObj->IsWrapOnAllPages()) + { + // Make sure content from the last floating table anchor is not shifted to previous + // anchors, unless we're in the special "wrap on all pages" mode. + nEnd = TextFrameIndex(0); + } + + GetFollow()->ManipOfst( nEnd ); + } + else + { + const SwTextNode* pTextNode = GetTextNodeForParaProps(); + bool bHasVisibleNumRule = nStrLen == TextFrameIndex(0) && pTextNode->GetNumRule(); + + if (!pTextNode->HasVisibleNumberingOrBullet()) + { + bHasVisibleNumRule = false; + } + + // Only split frame, if the frame contains + // content or contains no content, but has a numbering. + // i#84870 - No split, if text frame only contains one + // as-character anchored object. + if ( !bOnlyContainsAsCharAnchoredObj && + (nStrLen > TextFrameIndex(0) || + bHasVisibleNumRule ) + ) + { + SplitFrame( nEnd ); + nNew |= 3; + } + } + // If the remaining height changed e.g by RemoveFootnote() we need to + // fill up in order to avoid oscillation. + if( bDummy && pBodyFrame && + nBodyHeight < ( IsVertical() ? + pBodyFrame->getFrameArea().Width() : + pBodyFrame->getFrameArea().Height() ) ) + rLine.MakeDummyLine(); + } + + // In AdjustFrame() we set ourselves via Grow/Shrink + // In AdjustFollow() we set our FollowFrame + + const SwTwips nDocPrtTop = getFrameArea().Top() + getFramePrintArea().Top(); + const SwTwips nOldHeight = getFramePrintArea().SSize().Height(); + SwTwips nChg = rLine.CalcBottomLine() - nDocPrtTop - nOldHeight; + + //#i84870# - no shrink of text frame, if it only contains one as-character anchored object. + if ( nChg < 0 && !bDelta && bOnlyContainsAsCharAnchoredObj ) + { + nChg = 0; + } + + // Vertical Formatting: + // The (rotated) repaint rectangle's x coordinate refers to the frame. + // If the frame grows (or shirks) the repaint rectangle cannot simply + // be rotated back after formatting, because we use the upper left point + // of the frame for rotation. This point changes when growing/shrinking. + + if ( IsVertical() && !IsVertLR() && nChg ) + { + SwRect &rRepaint = pPara->GetRepaint(); + rRepaint.Left( rRepaint.Left() - nChg ); + rRepaint.Width( rRepaint.Width() - nChg ); + } + + AdjustFrame( nChg, bHasToFit ); + + if( HasFollow() || IsInFootnote() ) + AdjustFollow_( rLine, nEnd, nStrLen, nNew ); + + pPara->SetPrepMustFit( false ); +} + +// bPrev is set whether Reformat.Start() was called because of Prev(). +// Else, we don't know whether we can limit the repaint or not. +bool SwTextFrame::FormatLine( SwTextFormatter &rLine, const bool bPrev ) +{ + OSL_ENSURE( ! IsVertical() || IsSwapped(), + "SwTextFrame::FormatLine( rLine, bPrev) with unswapped frame" ); + SwParaPortion *pPara = rLine.GetInfo().GetParaPortion(); + const SwLineLayout *pOldCur = rLine.GetCurr(); + const TextFrameIndex nOldLen = pOldCur->GetLen(); + const SwTwips nOldAscent = pOldCur->GetAscent(); + const SwTwips nOldHeight = pOldCur->Height(); + const SwTwips nOldWidth = pOldCur->Width() + pOldCur->GetHangingMargin(); + const bool bOldHyph = pOldCur->IsEndHyph(); + SwTwips nOldTop = 0; + SwTwips nOldBottom = 0; + if( rLine.GetCurr()->IsClipping() ) + rLine.CalcUnclipped( nOldTop, nOldBottom ); + + TextFrameIndex const nNewStart = rLine.FormatLine( rLine.GetStart() ); + + OSL_ENSURE( getFrameArea().Pos().Y() + getFramePrintArea().Pos().Y() == rLine.GetFirstPos(), + "SwTextFrame::FormatLine: frame leaves orbit." ); + OSL_ENSURE( rLine.GetCurr()->Height(), + "SwTextFrame::FormatLine: line height is zero" ); + + // The current line break object + const SwLineLayout *pNew = rLine.GetCurr(); + + bool bUnChg = nOldLen == pNew->GetLen() && + bOldHyph == pNew->IsEndHyph(); + if ( bUnChg && !bPrev ) + { + const tools::Long nWidthDiff = nOldWidth > pNew->Width() + ? nOldWidth - pNew->Width() + : pNew->Width() - nOldWidth; + + // we only declare a line as unchanged, if its main values have not + // changed and it is not the last line (!paragraph end symbol!) + bUnChg = nOldHeight == pNew->Height() && + nOldAscent == pNew->GetAscent() && + nWidthDiff <= SLOPPY_TWIPS && + pOldCur->GetNext(); + } + + // Calculate rRepaint + const SwTwips nBottom = rLine.Y() + rLine.GetLineHeight(); + SwRepaint &rRepaint = pPara->GetRepaint(); + if( bUnChg && rRepaint.Top() == rLine.Y() + && (bPrev || nNewStart <= pPara->GetReformat().Start()) + && (nNewStart < TextFrameIndex(GetText().getLength()))) + { + rRepaint.Top( nBottom ); + rRepaint.Height( 0 ); + } + else + { + if( nOldTop ) + { + if( nOldTop < rRepaint.Top() ) + rRepaint.Top( nOldTop ); + if( !rLine.IsUnclipped() || nOldBottom > rRepaint.Bottom() ) + { + rRepaint.Bottom( nOldBottom - 1 ); + rLine.SetUnclipped( true ); + } + } + if( rLine.GetCurr()->IsClipping() && rLine.IsFlyInCntBase() ) + { + SwTwips nTmpTop, nTmpBottom; + rLine.CalcUnclipped( nTmpTop, nTmpBottom ); + if( nTmpTop < rRepaint.Top() ) + rRepaint.Top( nTmpTop ); + if( !rLine.IsUnclipped() || nTmpBottom > rRepaint.Bottom() ) + { + rRepaint.Bottom( nTmpBottom - 1 ); + rLine.SetUnclipped( true ); + } + } + else + { + if( !rLine.IsUnclipped() || nBottom > rRepaint.Bottom() ) + { + rRepaint.Bottom( nBottom - 1 ); + rLine.SetUnclipped( false ); + } + } + SwTwips nRght = std::max( nOldWidth, pNew->Width() + + pNew->GetHangingMargin() ); + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + const SwViewOption *pOpt = pSh ? pSh->GetViewOptions() : nullptr; + if( pOpt && (pOpt->IsParagraph() || pOpt->IsLineBreak()) ) + nRght += ( std::max( nOldAscent, pNew->GetAscent() ) ); + else + nRght += ( std::max( nOldAscent, pNew->GetAscent() ) / 4); + nRght += rLine.GetLeftMargin(); + if( rRepaint.GetOffset() || rRepaint.GetRightOfst() < nRght ) + rRepaint.SetRightOfst( nRght ); + + // Finally we enlarge the repaint rectangle if we found an underscore + // within our line. 40 Twips should be enough + const bool bHasUnderscore = + ( rLine.GetInfo().GetUnderScorePos() < nNewStart ); + if ( bHasUnderscore || rLine.GetCurr()->HasUnderscore() ) + rRepaint.Bottom( rRepaint.Bottom() + 40 ); + + const_cast<SwLineLayout*>(rLine.GetCurr())->SetUnderscore( bHasUnderscore ); + } + + // Calculating the good ol' nDelta + const sal_Int32 nDiff = sal_Int32(pNew->GetLen()) - sal_Int32(nOldLen); + pPara->SetDelta(pPara->GetDelta() - nDiff); + + // Stop! + if( rLine.IsStop() ) + return false; + + // Absolutely another line + if( rLine.IsNewLine() ) + return true; + + // Until the String's end? + if (nNewStart >= TextFrameIndex(GetText().getLength())) + return false; + + if( rLine.GetInfo().IsShift() ) + return true; + + // Reached the Reformat's end? + const TextFrameIndex nEnd = pPara->GetReformat().Start() + + pPara->GetReformat().Len(); + + if( nNewStart <= nEnd ) + return true; + + return 0 != pPara->GetDelta(); +} + +void SwTextFrame::Format_( SwTextFormatter &rLine, SwTextFormatInfo &rInf, + const bool bAdjust ) +{ + OSL_ENSURE( ! IsVertical() || IsSwapped(),"SwTextFrame::Format_ with unswapped frame" ); + + SwParaPortion *pPara = rLine.GetInfo().GetParaPortion(); + rLine.SetUnclipped( false ); + + const OUString & rString = GetText(); + const TextFrameIndex nStrLen(rString.getLength()); + + SwCharRange &rReformat = pPara->GetReformat(); + SwRepaint &rRepaint = pPara->GetRepaint(); + std::unique_ptr<SwRepaint> pFreeze; + + // Due to performance reasons we set rReformat to COMPLETE_STRING in Init() + // In this case we adjust rReformat + if( rReformat.Len() > nStrLen ) + rReformat.Len() = nStrLen; + + if( rReformat.Start() + rReformat.Len() > nStrLen ) + rReformat.Len() = nStrLen - rReformat.Start(); + + SwTwips nOldBottom; + if( GetOffset() && !IsFollow() ) + { + rLine.Bottom(); + nOldBottom = rLine.Y(); + rLine.Top(); + } + else + nOldBottom = 0; + rLine.CharToLine( rReformat.Start() ); + + // When inserting or removing a Space, words can be moved out of the edited + // line and into the preceding line, hence the preceding line must be + // formatted as well. + // Optimization: If rReformat starts after the first word of the line, + // this line cannot possibly influence the previous one. + // ...Turns out that unfortunately it can: Text size changes + FlyFrames; + // the feedback can affect multiple lines (Frames!)! + + // i#46560 + // FME: Yes, consider this case: "(word )" has to go to the next line + // because ")" is a forbidden character at the beginning of a line although + // "(word" would still fit on the previous line. Adding text right in front + // of ")" would not trigger a reformatting of the previous line. Adding 1 + // to the result of FindBrk() does not solve the problem in all cases, + // nevertheless it should be sufficient. + bool bPrev = rLine.GetPrev() && + (FindBrk(rString, rLine.GetStart(), rReformat.Start() + TextFrameIndex(1)) + // i#46560 + + TextFrameIndex(1) + >= rReformat.Start() || + rLine.GetCurr()->IsRest() ); + if( bPrev ) + { + while( rLine.Prev() ) + if( rLine.GetCurr()->GetLen() && !rLine.GetCurr()->IsRest() ) + { + if( !rLine.GetStart() ) + rLine.Top(); // So that NumDone doesn't get confused + break; + } + TextFrameIndex nNew = rLine.GetStart() + rLine.GetLength(); + if( nNew ) + { + --nNew; + if (CH_BREAK == rString[sal_Int32(nNew)]) + { + ++nNew; + rLine.Next(); + bPrev = false; + } + } + rReformat.Len() += rReformat.Start() - nNew; + rReformat.Start() = nNew; + } + + rRepaint.SetOffset( 0 ); + rRepaint.SetRightOfst( 0 ); + rRepaint.Chg( getFrameArea().Pos() + getFramePrintArea().Pos(), getFramePrintArea().SSize() ); + if( pPara->IsMargin() ) + rRepaint.Width( rRepaint.Width() + pPara->GetHangingMargin() ); + rRepaint.Top( rLine.Y() ); + if( 0 >= rRepaint.Width() ) + rRepaint.Width(1); + WidowsAndOrphans aFrameBreak( this, rInf.IsTest() ? 1 : 0 ); + + // rLine is now set to the first line which needs formatting. + // The bFirst flag makes sure that Next() is not called. + // The whole thing looks weird, but we need to make sure that + // rLine stops at the last non-fitting line when calling IsBreakNow. + bool bFirst = true; + bool bFormat = true; + + // The CharToLine() can also get us into the danger zone. + // In that case we need to walk back until rLine is set + // to the non-fitting line. Or else the mass of text is lost, + // because the Ofst was set wrongly in the Follow. + + bool bBreak = ( !pPara->IsPrepMustFit() || rLine.GetLineNr() > 1 ) + && aFrameBreak.IsBreakNowWidAndOrp( rLine ); + if( bBreak ) + { + bool bPrevDone = nullptr != rLine.Prev(); + while( bPrevDone && aFrameBreak.IsBreakNowWidAndOrp(rLine) ) + bPrevDone = nullptr != rLine.Prev(); + if( bPrevDone ) + { + aFrameBreak.SetKeep( false ); + rLine.Next(); + } + rLine.TruncLines(); + + // Play it safe + aFrameBreak.IsBreakNowWidAndOrp(rLine); + } + + /* Meaning if the following flags are set: + + Watch(End/Mid)Hyph: we need to format if we have a break at + the line end/Fly, as long as MaxHyph is reached + + Jump(End/Mid)Flag: the next line which has no break (line end/Fly), + needs to be formatted, because we could wrap now. This might have been + forbidden earlier by MaxHyph + + Watch(End/Mid)Hyph: if the last formatted line got a cutoff point, but + didn't have one before + + Jump(End/Mid)Hyph: if a cutoff point disappears + */ + bool bJumpEndHyph = false; + bool bWatchEndHyph = false; + bool bJumpMidHyph = false; + bool bWatchMidHyph = false; + + const SwAttrSet& rAttrSet = GetTextNodeForParaProps()->GetSwAttrSet(); + rInf.MaxHyph() = rAttrSet.GetHyphenZone().GetMaxHyphens(); + bool bMaxHyph = 0 != rInf.MaxHyph(); + if ( bMaxHyph ) + rLine.InitCntHyph(); + + if( IsFollow() && IsFieldFollow() && rLine.GetStart() == GetOffset() ) + { + SwTextFrame *pMaster = FindMaster(); + OSL_ENSURE( pMaster, "SwTextFrame::Format: homeless follow" ); + const SwLineLayout* pLine=nullptr; + if (pMaster) + { + if (!pMaster->HasPara()) + { // master could be locked because it's being formatted upstack + SAL_WARN("sw", "SwTextFrame::Format_: master not formatted!"); + } + else + { + SwTextSizeInfo aInf( pMaster ); + SwTextIter aMasterLine( pMaster, &aInf ); + aMasterLine.Bottom(); + pLine = aMasterLine.GetCurr(); + assert(aMasterLine.GetEnd() == GetOffset()); + } + } + SwLinePortion* pRest = pLine ? + rLine.MakeRestPortion(pLine, GetOffset()) : nullptr; + if( pRest ) + rInf.SetRest( pRest ); + else + SetFieldFollow( false ); + } + + /* Ad cancel criterion: + * In order to recognize, whether a line does not fit onto the page + * anymore, we need to format it. This overflow is removed again in + * e.g. AdjustFollow. + * Another complication: if we are the Master, we need to traverse + * the lines, because it could happen that one line can overflow + * from the Follow to the Master. + */ + do + { + if( bFirst ) + bFirst = false; + else + { + if ( bMaxHyph ) + { + if ( rLine.GetCurr()->IsEndHyph() ) + rLine.CntEndHyph()++; + else + rLine.CntEndHyph() = 0; + if ( rLine.GetCurr()->IsMidHyph() ) + rLine.CntMidHyph()++; + else + rLine.CntMidHyph() = 0; + } + if( !rLine.Next() ) + { + if( !bFormat ) + { + SwLinePortion* pRest = + rLine.MakeRestPortion( rLine.GetCurr(), rLine.GetEnd() ); + if( pRest ) + rInf.SetRest( pRest ); + } + rLine.Insert( new SwLineLayout() ); + rLine.Next(); + bFormat = true; + } + } + if ( !bFormat && bMaxHyph && + (bWatchEndHyph || bJumpEndHyph || bWatchMidHyph || bJumpMidHyph) ) + { + if ( rLine.GetCurr()->IsEndHyph() ) + { + if ( bWatchEndHyph ) + bFormat = ( rLine.CntEndHyph() == rInf.MaxHyph() ); + } + else + { + bFormat = bJumpEndHyph; + bWatchEndHyph = false; + bJumpEndHyph = false; + } + if ( rLine.GetCurr()->IsMidHyph() ) + { + if ( bWatchMidHyph && !bFormat ) + bFormat = ( rLine.CntEndHyph() == rInf.MaxHyph() ); + } + else + { + bFormat |= bJumpMidHyph; + bWatchMidHyph = false; + bJumpMidHyph = false; + } + } + if( bFormat ) + { + const bool bOldEndHyph = rLine.GetCurr()->IsEndHyph(); + const bool bOldMidHyph = rLine.GetCurr()->IsMidHyph(); + bFormat = FormatLine( rLine, bPrev ); + // There can only be one bPrev ... (???) + bPrev = false; + if ( bMaxHyph ) + { + if ( rLine.GetCurr()->IsEndHyph() != bOldEndHyph ) + { + bWatchEndHyph = !bOldEndHyph; + bJumpEndHyph = bOldEndHyph; + } + if ( rLine.GetCurr()->IsMidHyph() != bOldMidHyph ) + { + bWatchMidHyph = !bOldMidHyph; + bJumpMidHyph = bOldMidHyph; + } + } + } + + if( !rInf.IsNewLine() ) + { + if( !bFormat ) + bFormat = nullptr != rInf.GetRest(); + if( rInf.IsStop() || rInf.GetIdx() >= nStrLen ) + break; + if( !bFormat && ( !bMaxHyph || ( !bWatchEndHyph && + !bJumpEndHyph && !bWatchMidHyph && !bJumpMidHyph ) ) ) + { + if( GetFollow() ) + { + while( rLine.Next() ) + ; //Nothing + pFreeze.reset(new SwRepaint( rRepaint )); // to minimize painting + } + else + break; + } + } + bBreak = aFrameBreak.IsBreakNowWidAndOrp(rLine); + }while( !bBreak ); + + if( pFreeze ) + { + rRepaint = *pFreeze; + pFreeze.reset(); + } + + if( !rLine.IsStop() ) + { + // If we're finished formatting the text and we still + // have other line objects left, these are superfluous + // now because the text has gotten shorter. + bool bTruncLines = false; + if( rLine.GetStart() + rLine.GetLength() >= nStrLen && + rLine.GetCurr()->GetNext() ) + { + bTruncLines = true; + } + else if (GetMergedPara() && rLine.GetCurr()->GetNext()) + { + // We can also have superfluous lines with redlining in case the current line is shorter + // than the text length, but the total length of lines is still more than expected. + // Truncate in this case as well. + TextFrameIndex nLen(0); + for (const SwLineLayout* pLine = pPara; pLine; pLine = pLine->GetNext()) + { + nLen += pLine->GetLen(); + } + bTruncLines = nLen > nStrLen; + } + + if (bTruncLines) + { + rLine.TruncLines(); + rLine.SetTruncLines( true ); + } + } + + if( rInf.IsTest() ) + return; + + // FormatAdjust does not pay off at OnceMore + if( bAdjust || !rLine.GetDropFormat() || !rLine.CalcOnceMore() ) + { + FormatAdjust( rLine, aFrameBreak, nStrLen, rInf.IsStop() ); + } + if( rRepaint.HasArea() ) + SetRepaint(); + rLine.SetTruncLines( false ); + if( nOldBottom ) // We check whether paragraphs that need scrolling can + // be shrunk, so that they don't need scrolling anymore + { + rLine.Bottom(); + SwTwips nNewBottom = rLine.Y(); + if( nNewBottom < nOldBottom ) + SetOffset_(TextFrameIndex(0)); + } +} + +void SwTextFrame::FormatOnceMore( SwTextFormatter &rLine, SwTextFormatInfo &rInf ) +{ + OSL_ENSURE( ! IsVertical() || IsSwapped(), + "A frame is not swapped in SwTextFrame::FormatOnceMore" ); + + SwParaPortion *pPara = rLine.GetInfo().GetParaPortion(); + if( !pPara ) + return; + + // If necessary the pPara + sal_uInt16 nOld = static_cast<const SwTextMargin&>(rLine).GetDropHeight(); + bool bShrink = false; + bool bGrow = false; + bool bGoOn = rLine.IsOnceMore(); + sal_uInt8 nGo = 0; + while( bGoOn ) + { + ++nGo; + rInf.Init(); + rLine.Top(); + if( !rLine.GetDropFormat() ) + rLine.SetOnceMore( false ); + SwCharRange aRange(TextFrameIndex(0), TextFrameIndex(rInf.GetText().getLength())); + pPara->GetReformat() = aRange; + Format_( rLine, rInf ); + + bGoOn = rLine.IsOnceMore(); + if( bGoOn ) + { + const sal_uInt16 nNew = static_cast<const SwTextMargin&>(rLine).GetDropHeight(); + if( nOld == nNew ) + bGoOn = false; + else + { + if( nOld > nNew ) + bShrink = true; + else + bGrow = true; + + if( bShrink == bGrow || 5 < nGo ) + bGoOn = false; + + nOld = nNew; + } + + // If something went wrong, we need to reformat again + if( !bGoOn ) + { + rInf.CtorInitTextFormatInfo( getRootFrame()->GetCurrShell()->GetOut(), this ); + rLine.CtorInitTextFormatter( this, &rInf ); + rLine.SetDropLines( 1 ); + rLine.CalcDropHeight( 1 ); + SwCharRange aTmpRange(TextFrameIndex(0), TextFrameIndex(rInf.GetText().getLength())); + pPara->GetReformat() = aTmpRange; + Format_( rLine, rInf, true ); + // We paint everything ... + SetCompletePaint(); + } + } + } +} + +void SwTextFrame::FormatImpl(vcl::RenderContext* pRenderContext, SwParaPortion *pPara, + std::vector<SwAnchoredObject *> & rIntersectingObjs) +{ + const bool bIsEmpty = GetText().isEmpty(); + + if ( bIsEmpty ) + { + // Empty lines do not get tortured for very long: + // pPara is cleared, which is the same as: + // *pPara = SwParaPortion; + const bool bMustFit = pPara->IsPrepMustFit(); + pPara->Truncate(); + pPara->FormatReset(); + + // delete pSpaceAdd and pKanaComp + pPara->FinishSpaceAdd(); + pPara->FinishKanaComp(); + pPara->ResetFlags(); + pPara->SetPrepMustFit( bMustFit ); + } + + OSL_ENSURE( ! IsSwapped(), "A frame is swapped before Format_" ); + + if ( IsVertical() ) + SwapWidthAndHeight(); + + SwTextFormatInfo aInf( pRenderContext, this ); + SwTextFormatter aLine( this, &aInf ); + + HideAndShowObjects(); + + Format_( aLine, aInf ); + + if( aLine.IsOnceMore() ) + FormatOnceMore( aLine, aInf ); + + if (aInf.GetTextFly().IsOn()) + { + SwRect const aRect(aInf.GetTextFly().GetFrameArea()); + for (SwAnchoredObject *const pObj : aInf.GetTextFly().GetAnchoredObjList()) + { + if (!aInf.GetTextFly().AnchoredObjToRect(pObj, aRect).IsEmpty()) + { + rIntersectingObjs.push_back(pObj); + } + } + } + + if ( IsVertical() ) + SwapWidthAndHeight(); + + OSL_ENSURE( ! IsSwapped(), "A frame is swapped after Format_" ); + + if( 1 >= aLine.GetDropLines() ) + return; + + if( SvxAdjust::Left != aLine.GetAdjust() && + SvxAdjust::Block != aLine.GetAdjust() ) + { + aLine.CalcDropAdjust(); + aLine.SetPaintDrop( true ); + } + + if( aLine.IsPaintDrop() ) + { + aLine.CalcDropRepaint(); + aLine.SetPaintDrop( false ); + } +} + +// We calculate the text frame's size and send a notification. +// Shrink() or Grow() to adjust the frame's size to the changed required space. +void SwTextFrame::Format( vcl::RenderContext* pRenderContext, const SwBorderAttrs * ) +{ + SwRectFnSet aRectFnSet(this); + + CalcAdditionalFirstLineOffset(); + + // The range autopilot or the BASIC interface pass us TextFrames with + // a width <= 0 from time to time + if( aRectFnSet.GetWidth(getFramePrintArea()) <= 0 ) + { + // If MustFit is set, we shrink to the Upper's bottom edge if needed. + SwTextLineAccess aAccess( this ); + + if( aAccess.GetPara()->IsPrepMustFit() ) + { + const SwTwips nLimit = aRectFnSet.GetPrtBottom(*GetUpper()); + const SwTwips nDiff = - aRectFnSet.BottomDist( getFrameArea(), nLimit ); + if( nDiff > 0 ) + Shrink( nDiff ); + } + + tools::Long nFrameHeight = aRectFnSet.GetHeight(getFrameArea()); + const tools::Long nTop = aRectFnSet.GetTopMargin(*this); + + if( nTop > nFrameHeight ) + { + aRectFnSet.SetYMargins( *this, nFrameHeight, 0 ); + } + else if( aRectFnSet.GetHeight(getFramePrintArea()) < 0 ) + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + aRectFnSet.SetHeight( aPrt, 0 ); + } + + return; + } + + TextFrameIndex nStrLen(GetText().getLength()); + + if (HasNonLastSplitFlyDrawObj()) + { + // Non-last part of split fly anchor: consider this empty. + nStrLen = TextFrameIndex(0); + } + + if ( nStrLen || !FormatEmpty() ) + { + + SetEmpty( false ); + // In order to not get confused by nested Formats + FormatLevel aLevel; + if( 12 == FormatLevel::GetLevel() ) + return; + + // We could be possibly not allowed to alter the format information + if( IsLocked() ) + return; + + // Attention: Format() could be triggered by GetFormatted() + if( IsHiddenNow() ) + { + tools::Long nPrtHeight = aRectFnSet.GetHeight(getFramePrintArea()); + if( nPrtHeight ) + { + HideHidden(); + Shrink( nPrtHeight ); + } + else + { + // Assure that objects anchored + // at paragraph resp. at/as character inside paragraph + // are hidden. + HideAndShowObjects(); + } + ChgThisLines(); + return; + } + + // We do not want to be interrupted during formatting + TextFrameLockGuard aLock(this); + + // this is to ensure that the similar code in SwTextFrame::Format_ + // finds the master formatted in case it's needed + if (IsFollow() && IsFieldFollow()) + { + SwTextFrame *pMaster = FindMaster(); + assert(pMaster); + if (!pMaster->HasPara()) + { + pMaster->GetFormatted(); + } + if (!pMaster->HasPara()) + { // master could be locked because it's being formatted upstack + SAL_WARN("sw", "SwTextFrame::Format: failed to format master!"); + } + else + { + SwTextSizeInfo aInf( pMaster ); + SwTextIter aMasterLine( pMaster, &aInf ); + aMasterLine.Bottom(); + SetOffset(aMasterLine.GetEnd()); + } + } + + SwTextLineAccess aAccess( this ); + const bool bNew = !aAccess.IsAvailable(); + const bool bSetOffset = + (GetOffset() && GetOffset() > TextFrameIndex(GetText().getLength())); + + if( CalcPreps() ) + ; // nothing + // We return if already formatted, but if the TextFrame was just created + // and does not have any format information + else if( !bNew && !aAccess.GetPara()->GetReformat().Len() ) + { + if (GetTextNodeForParaProps()->GetSwAttrSet().GetRegister().GetValue()) + { + aAccess.GetPara()->SetPrepAdjust(); + aAccess.GetPara()->SetPrep(); + CalcPreps(); + } + SetWidow( false ); + } + else if( bSetOffset && IsFollow() ) + { + SwTextFrame *pMaster = FindMaster(); + OSL_ENSURE( pMaster, "SwTextFrame::Format: homeless follow" ); + if( pMaster ) + pMaster->Prepare( PrepareHint::FollowFollows ); + SwTwips nMaxY = aRectFnSet.GetPrtBottom(*GetUpper()); + + if( aRectFnSet.OverStep( getFrameArea(), nMaxY ) ) + { + aRectFnSet.SetLimit( *this, nMaxY ); + } + else if( aRectFnSet.BottomDist( getFrameArea(), nMaxY ) < 0 ) + { + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aRectFnSet.AddBottom( aFrm, -aRectFnSet.GetHeight(aFrm) ); + } + } + else + { + // bSetOffset here means that we have the "red arrow situation" + if ( bSetOffset ) + SetOffset_(TextFrameIndex(0)); + + const bool bOrphan = IsWidow(); + const SwFootnoteBossFrame* pFootnoteBoss = HasFootnote() ? FindFootnoteBossFrame() : nullptr; + SwTwips nFootnoteHeight = 0; + if( pFootnoteBoss ) + { + const SwFootnoteContFrame* pCont = pFootnoteBoss->FindFootnoteCont(); + nFootnoteHeight = pCont ? aRectFnSet.GetHeight(pCont->getFrameArea()) : 0; + } + do + { + ::std::vector<SwAnchoredObject *> intersectingObjs; + ::std::vector<SwFrame const*> nexts; + for (SwFrame const* pNext = GetNext(); pNext; pNext = pNext->GetNext()) + { + nexts.push_back(pNext); + } + FormatImpl(pRenderContext, aAccess.GetPara(), intersectingObjs); + if( pFootnoteBoss && nFootnoteHeight ) + { + const SwFootnoteContFrame* pCont = pFootnoteBoss->FindFootnoteCont(); + SwTwips nNewHeight = pCont ? aRectFnSet.GetHeight(pCont->getFrameArea()) : 0; + // If we lost some footnotes, we may have more space + // for our main text, so we have to format again ... + if( nNewHeight < nFootnoteHeight ) + { + nFootnoteHeight = nNewHeight; + continue; + } + } + if (!intersectingObjs.empty()) + { + // assumption is that FormatImpl() only moves frames + // in the next-chain to next page + SwPageFrame *const pPage(FindPageFrame()); + SwTextFrame * pLastMovedAnchor(nullptr); + auto lastIter(nexts.end()); + for (SwAnchoredObject *const pObj : intersectingObjs) + { + SwFrame *const pAnchor(pObj->AnchorFrame()); + SwPageFrame *const pAnchorPage(pAnchor->FindPageFrame()); + if (pAnchorPage != pPage) + { + auto const iter(::std::find(nexts.begin(), nexts.end(), pAnchor)); + if (iter != nexts.end()) + { + assert(pAnchor->IsTextFrame()); + // (can't check SwOszControl::IsInProgress()?) + // called in loop in FormatAnchorFrameAndItsPrevs() + if (static_cast<SwTextFrame const*>(pAnchor)->IsJoinLocked() + // called in loop in SwFrame::PrepareMake() + || pAnchor->IsDeleteForbidden()) + { + // when called via FormatAnchorFrameAndItsPrevs(): + // don't do anything, caller will handle it + pLastMovedAnchor = nullptr; + break; + } + assert(pPage->GetPhyPageNum() < pAnchorPage->GetPhyPageNum()); // how could it move backward? + + if (!pLastMovedAnchor || iter < lastIter) + { + pLastMovedAnchor = static_cast<SwTextFrame *>(pAnchor); + lastIter = iter; + } + } + } + } + SwPageFrame const*const pPrevPage(static_cast<SwPageFrame const*>(pPage->GetPrev())); + if (pLastMovedAnchor) + { + for (SwAnchoredObject *const pObj : intersectingObjs) + { + if (pObj->AnchorFrame() == pLastMovedAnchor) + { + SwPageFrame *const pAnchorPage(pLastMovedAnchor->FindPageFrame()); + SAL_INFO("sw.layout", "SwTextFrame::Format: move anchored " << pObj << " from " << pPage->GetPhyPageNum() << " to " << pAnchorPage->GetPhyPageNum()); + pObj->RegisterAtPage(*pAnchorPage); + // tdf#143239 if the position remains valid, it may not be + // positioned again so would remain on the wrong page! + pObj->InvalidateObjPos(); + ::Notify_Background(pObj->GetDrawObj(), pPage, + pObj->GetObjRect(), PrepareHint::FlyFrameLeave, false); + pObj->SetForceNotifyNewBackground(true); + } + } + if (GetFollow() // this frame was split + && (!pPrevPage // prev page is still valid + || (!pPrevPage->IsInvalid() + && (!pPrevPage->GetSortedObjs() || !pPrevPage->IsInvalidFly())))) + { // this seems a bit risky... + SwLayouter::InsertMovedFwdFrame(GetTextNodeFirst()->GetDoc(), + *pLastMovedAnchor, FindPageFrame()->GetPhyPageNum() + 1); + } + continue; // try again without the fly + } + } + break; + } while ( pFootnoteBoss ); + if( bOrphan ) + { + ValidateFrame(); + SetWidow( false ); + } + } + if( IsEmptyMaster() ) + { + SwFrame* pPre = GetPrev(); + if( pPre && + // i#10826 It's the first, it cannot keep! + pPre->GetIndPrev() && + pPre->GetAttrSet()->GetKeep().GetValue() ) + { + pPre->InvalidatePos(); + } + + if (IsEmptyMasterWithSplitFly()) + { + // A fly is anchored to us, reduce size, so we definitely still fit the current + // page. + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aRectFnSet.SetHeight(aFrm, 0); + + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + aRectFnSet.SetTop(aPrt, 0); + aRectFnSet.SetHeight(aPrt, 0); + } + } + } + + ChgThisLines(); + + // the PrepMustFit should not survive a Format operation + SwParaPortion *pPara = GetPara(); + if ( pPara ) + pPara->SetPrepMustFit( false ); + + CalcBaseOfstForFly(); + CalcHeightOfLastLine(); // i#11860 - Adjust spacing implementation for + // object positioning - Compatibility to MS Word + // tdf#117982 -- Fix cell spacing hides content + // Check if the cell's content has greater size than the row height + if (IsInTab() && GetUpper() && ((GetUpper()->getFramePrintArea().Height() < getFramePrintArea().Height()) + || (getFramePrintArea().Height() <= 0))) + { + SAL_INFO("sw.core", "Warn: Cell content has greater size than cell height!"); + //get font size... + SwTwips aTmpHeight = getFrameArea().Height(); + //...and push it into the text frame + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + //if only bottom margin what we have: + if (GetTopMargin() == 0) + //set the frame to its original location + aPrt.SetTopAndHeight(0, aTmpHeight); + } +} + +// bForceQuickFormat is set if GetFormatted() has been called during the +// painting process. Actually I cannot imagine a situation which requires +// a full formatting of the paragraph during painting, on the other hand +// a full formatting can cause the invalidation of other layout frames, +// e.g., if there are footnotes in this paragraph, and invalid layout +// frames will not calculated during the painting. So I actually want to +// avoid a formatting during painting, but since I'm a coward, I'll only +// force the quick formatting in the situation of issue i29062. +bool SwTextFrame::FormatQuick( bool bForceQuickFormat ) +{ + OSL_ENSURE( ! IsVertical() || ! IsSwapped(), + "SwTextFrame::FormatQuick with swapped frame" ); + + if( IsEmpty() && FormatEmpty() ) + return true; + + // We're very picky: + if( HasPara() || IsWidow() || IsLocked() + || !isFrameAreaSizeValid() || + ( ( IsVertical() ? getFramePrintArea().Width() : getFramePrintArea().Height() ) && IsHiddenNow() ) ) + return false; + + SwTextLineAccess aAccess( this ); + SwParaPortion *pPara = aAccess.GetPara(); + if( !pPara ) + return false; + + SwFrameSwapper aSwapper( this, true ); + + TextFrameLockGuard aLock(this); + SwTextFormatInfo aInf( getRootFrame()->GetCurrShell()->GetOut(), this, false, true ); + if( 0 != aInf.MaxHyph() ) // Respect MaxHyphen! + return false; + + SwTextFormatter aLine( this, &aInf ); + + // DropCaps are too complicated ... + if( aLine.GetDropFormat() ) + return false; + + TextFrameIndex nStart = GetOffset(); + const TextFrameIndex nEnd = GetFollow() + ? GetFollow()->GetOffset() + : TextFrameIndex(aInf.GetText().getLength()); + + int nLoopProtection = 0; + do + { + TextFrameIndex nNewStart = aLine.FormatLine(nStart); + if (nNewStart == nStart) + ++nLoopProtection; + else + nLoopProtection = 0; + nStart = nNewStart; + const bool bWillEndlessInsert = nLoopProtection > 250; + SAL_WARN_IF(bWillEndlessInsert, "sw", "loop detection triggered"); + if ((!bWillEndlessInsert) // Check for special case: line is invisible, + // like in too thin table cell: tdf#66141 + && (aInf.IsNewLine() || (!aInf.IsStop() && nStart < nEnd))) + aLine.Insert( new SwLineLayout() ); + } while( aLine.Next() ); + + // Last exit: the heights need to match + Point aTopLeft( getFrameArea().Pos() ); + aTopLeft += getFramePrintArea().Pos(); + const SwTwips nNewHeight = aLine.Y() + aLine.GetLineHeight(); + const SwTwips nOldHeight = aTopLeft.Y() + getFramePrintArea().Height(); + + if( !bForceQuickFormat && nNewHeight != nOldHeight && !IsUndersized() ) + { + // Attention: This situation can occur due to FormatLevel==12. Don't panic! + TextFrameIndex const nStrt = GetOffset(); + InvalidateRange_( SwCharRange( nStrt, nEnd - nStrt) ); + return false; + } + + if (m_pFollow && nStart != static_cast<SwTextFrame*>(m_pFollow)->GetOffset()) + return false; // can be caused by e.g. Orphans + + // We made it! + + // Set repaint + pPara->GetRepaint().Pos( aTopLeft ); + pPara->GetRepaint().SSize( getFramePrintArea().SSize() ); + + // Delete reformat + pPara->GetReformat() = SwCharRange(); + pPara->SetDelta(0); + + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/frminf.cxx b/sw/source/core/text/frminf.cxx new file mode 100644 index 0000000000..a123691db7 --- /dev/null +++ b/sw/source/core/text/frminf.cxx @@ -0,0 +1,291 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <frminf.hxx> +#include "itrtxt.hxx" + +TextFrameIndex SwTextMargin::GetTextStart() const +{ + const OUString &rText = GetInfo().GetText(); + const TextFrameIndex nEnd = m_nStart + m_pCurr->GetLen(); + + for (TextFrameIndex i = m_nStart; i < nEnd; ++i) + { + const sal_Unicode aChar = rText[sal_Int32(i)]; + if( CH_TAB != aChar && ' ' != aChar ) + return i; + } + return nEnd; +} + +TextFrameIndex SwTextMargin::GetTextEnd() const +{ + const OUString &rText = GetInfo().GetText(); + const TextFrameIndex nEnd = m_nStart + m_pCurr->GetLen(); + for (TextFrameIndex i = nEnd - TextFrameIndex(1); i >= m_nStart; --i) + { + const sal_Unicode aChar = rText[sal_Int32(i)]; + if( CH_TAB != aChar && CH_BREAK != aChar && ' ' != aChar ) + return i + TextFrameIndex(1); + } + return m_nStart; +} + +// Does the paragraph fit into one line? +bool SwTextFrameInfo::IsOneLine() const +{ + const SwLineLayout *pLay = m_pFrame->GetPara(); + if( !pLay ) + return false; + + // For follows false of course + if( m_pFrame->GetFollow() ) + return false; + + pLay = pLay->GetNext(); + while( pLay ) + { + if( pLay->GetLen() ) + return false; + pLay = pLay->GetNext(); + } + return true; +} + +// Is the line filled for X percent? +bool SwTextFrameInfo::IsFilled( const sal_uInt8 nPercent ) const +{ + const SwLineLayout *pLay = m_pFrame->GetPara(); + if( !pLay ) + return false; + + tools::Long nWidth = m_pFrame->getFramePrintArea().Width(); + nWidth *= nPercent; + nWidth /= 100; + return nWidth <= pLay->Width(); +} + +// Where does the text start (without whitespace)? (document global) +SwTwips SwTextFrameInfo::GetLineStart( const SwTextCursor &rLine ) +{ + const TextFrameIndex nTextStart = rLine.GetTextStart(); + if( rLine.GetStart() == nTextStart ) + return rLine.GetLineStart(); + + SwRect aRect; + const_cast<SwTextCursor&>(rLine).GetCharRect( &aRect, nTextStart ); + return aRect.Left(); +} + +// Where does the text start (without whitespace)? (relative in the Frame) +SwTwips SwTextFrameInfo::GetLineStart() const +{ + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(m_pFrame) ); + SwTextCursor aLine( const_cast<SwTextFrame*>(m_pFrame), &aInf ); + return GetLineStart( aLine ) - m_pFrame->getFrameArea().Left() - m_pFrame->getFramePrintArea().Left(); +} + +// Calculates the character's position and returns the middle position +SwTwips SwTextFrameInfo::GetCharPos(TextFrameIndex const nChar, bool bCenter) const +{ + SwRectFnSet aRectFnSet(m_pFrame); + SwFrameSwapper aSwapper( m_pFrame, true ); + + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(m_pFrame) ); + SwTextCursor aLine( const_cast<SwTextFrame*>(m_pFrame), &aInf ); + + SwTwips nStt, nNext; + SwRect aRect; + aLine.GetCharRect( &aRect, nChar ); + if ( aRectFnSet.IsVert() ) + m_pFrame->SwitchHorizontalToVertical( aRect ); + + nStt = aRectFnSet.GetLeft(aRect); + + if( !bCenter ) + return nStt - aRectFnSet.GetLeft(m_pFrame->getFrameArea()); + + aLine.GetCharRect( &aRect, nChar + TextFrameIndex(1) ); + if ( aRectFnSet.IsVert() ) + m_pFrame->SwitchHorizontalToVertical( aRect ); + + nNext = aRectFnSet.GetLeft(aRect); + + return (( nNext + nStt ) / 2 ) - aRectFnSet.GetLeft(m_pFrame->getFrameArea()); +} + +static void +AddRange(std::vector<std::pair<TextFrameIndex, TextFrameIndex>> & rRanges, + TextFrameIndex const nPos, TextFrameIndex const nLen) +{ + assert(rRanges.empty() || rRanges.back().second <= nPos); + if( nLen ) + { + if (!rRanges.empty() && nPos == rRanges.back().second) + { + rRanges.back().second += nLen; + } + else + { + rRanges.emplace_back(nPos, nPos + nLen); + } + } +} + +// Accumulates the whitespace at line start and end in the vector +void SwTextFrameInfo::GetSpaces( + std::vector<std::pair<TextFrameIndex, TextFrameIndex>> & rRanges, + bool const bWithLineBreak) const +{ + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(m_pFrame) ); + SwTextMargin aLine( const_cast<SwTextFrame*>(m_pFrame), &aInf ); + bool bFirstLine = true; + do { + + if( aLine.GetCurr()->GetLen() ) + { + TextFrameIndex nPos = aLine.GetTextStart(); + // Do NOT include the blanks/tabs from the first line + // in the selection + if( !bFirstLine && nPos > aLine.GetStart() ) + AddRange( rRanges, aLine.GetStart(), nPos - aLine.GetStart() ); + + // Do NOT include the blanks/tabs from the last line + // in the selection + if( aLine.GetNext() ) + { + nPos = aLine.GetTextEnd(); + + if( nPos < aLine.GetEnd() ) + { + TextFrameIndex const nOff( !bWithLineBreak && CH_BREAK == + aLine.GetInfo().GetChar(aLine.GetEnd() - TextFrameIndex(1)) + ? 1 : 0 ); + AddRange( rRanges, nPos, aLine.GetEnd() - nPos - nOff ); + } + } + } + bFirstLine = false; + } + while( aLine.Next() ); +} + +// Is there a bullet/symbol etc. at the text position? +// Fonts: CharSet, SYMBOL and DONTKNOW +bool SwTextFrameInfo::IsBullet(TextFrameIndex const nTextStart) const +{ + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(m_pFrame) ); + SwTextMargin aLine( const_cast<SwTextFrame*>(m_pFrame), &aInf ); + aInf.SetIdx( nTextStart ); + return aLine.IsSymbol( nTextStart ); +} + +// Get first line indent +// The precondition for a positive or negative first line indent: +// All lines (except for the first one) have the same left margin. +// We do not want to be so picky and work with a tolerance of TOLERANCE twips. +SwTwips SwTextFrameInfo::GetFirstIndent() const +{ + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(m_pFrame) ); + SwTextCursor aLine( const_cast<SwTextFrame*>(m_pFrame), &aInf ); + const SwTwips nFirst = GetLineStart( aLine ); + const SwTwips TOLERANCE = 20; + + if( !aLine.Next() ) + return 0; + + SwTwips nLeft = GetLineStart( aLine ); + while( aLine.Next() ) + { + if( aLine.GetCurr()->GetLen() ) + { + const SwTwips nCurrLeft = GetLineStart( aLine ); + if( nLeft + TOLERANCE < nCurrLeft || + nLeft - TOLERANCE > nCurrLeft ) + return 0; + } + } + + // At first we only return +1, -1 and 0 + if( nLeft == nFirst ) + return 0; + + if( nLeft > nFirst ) + return -1; + + return 1; +} + +sal_Int32 SwTextFrameInfo::GetBigIndent(TextFrameIndex& rFndPos, + const SwTextFrame *pNextFrame ) const +{ + SwTextSizeInfo aInf( const_cast<SwTextFrame*>(m_pFrame) ); + SwTextCursor aLine( const_cast<SwTextFrame*>(m_pFrame), &aInf ); + SwTwips nNextIndent = 0; + + if( pNextFrame ) + { + // I'm a single line + SwTextSizeInfo aNxtInf( const_cast<SwTextFrame*>(pNextFrame) ); + SwTextCursor aNxtLine( const_cast<SwTextFrame*>(pNextFrame), &aNxtInf ); + nNextIndent = GetLineStart( aNxtLine ); + } + else + { + // I'm multi-line + if( aLine.Next() ) + { + nNextIndent = GetLineStart( aLine ); + aLine.Prev(); + } + } + + if( nNextIndent <= GetLineStart( aLine ) ) + return 0; + + const Point aPoint( nNextIndent, aLine.Y() ); + rFndPos = aLine.GetModelPositionForViewPoint( nullptr, aPoint, false ); + if (TextFrameIndex(1) >= rFndPos) + return 0; + + // Is on front of a non-space + const OUString& rText = aInf.GetText(); + sal_Unicode aChar = rText[sal_Int32(rFndPos)]; + if( CH_TAB == aChar || CH_BREAK == aChar || ' ' == aChar || + (( CH_TXTATR_BREAKWORD == aChar || CH_TXTATR_INWORD == aChar ) && + aInf.HasHint( rFndPos ) ) ) + return 0; + + // and after a space + aChar = rText[sal_Int32(rFndPos) - 1]; + if( CH_TAB != aChar && CH_BREAK != aChar && + ( ( CH_TXTATR_BREAKWORD != aChar && CH_TXTATR_INWORD != aChar ) || + !aInf.HasHint(rFndPos - TextFrameIndex(1))) && + // More than two Blanks! + (' ' != aChar || ' ' != rText[sal_Int32(rFndPos) - 2])) + return 0; + + SwRect aRect; + aLine.GetCharRect( &aRect, rFndPos ); + return static_cast<sal_Int32>(aRect.Left() - m_pFrame->getFrameArea().Left() - m_pFrame->getFramePrintArea().Left()); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/frmpaint.cxx b/sw/source/core/text/frmpaint.cxx new file mode 100644 index 0000000000..57458a0218 --- /dev/null +++ b/sw/source/core/text/frmpaint.cxx @@ -0,0 +1,810 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <memory> +#include <com/sun/star/text/HoriOrientation.hpp> +#include <editeng/pgrditem.hxx> +#include <editeng/lrspitem.hxx> +#include <tgrditem.hxx> +#include <paratr.hxx> + +#include <fmtline.hxx> +#include <lineinfo.hxx> +#include <charfmt.hxx> +#include <rootfrm.hxx> +#include <pagefrm.hxx> +#include <viewsh.hxx> +#include <viewopt.hxx> +#include <frmatr.hxx> +#include <txtfrm.hxx> +#include "itrpaint.hxx" +#include "txtpaint.hxx" +#include "txtcache.hxx" +#include <flyfrm.hxx> +#include "redlnitr.hxx" +#include <redline.hxx> +#include <swmodule.hxx> +#include <tabfrm.hxx> +#include <numrule.hxx> +#include <wrong.hxx> + +#include <EnhancedPDFExportHelper.hxx> + +#include <IDocumentRedlineAccess.hxx> +#include <IDocumentStylePoolAccess.hxx> + +#define REDLINE_DISTANCE 567/4 +#define REDLINE_MINDIST 567/10 + +using namespace ::com::sun::star; + +static bool bInitFont = true; + +namespace { + +class SwExtraPainter +{ + SwSaveClip m_aClip; + SwRect m_aRect; + const SwTextFrame* m_pTextFrame; + SwViewShell *m_pSh; + std::unique_ptr<SwFont> m_pFnt; + const SwLineNumberInfo &m_rLineInf; + SwTwips m_nX; + SwTwips m_nRedX; + sal_Int32 m_nLineNr; + sal_uInt16 m_nDivider; + bool m_bGoLeft; + bool IsClipChg() const { return m_aClip.IsChg(); } + + SwExtraPainter(const SwExtraPainter&) = delete; + SwExtraPainter& operator=(const SwExtraPainter&) = delete; + +public: + SwExtraPainter( const SwTextFrame *pFrame, SwViewShell *pVwSh, + const SwLineNumberInfo &rLnInf, const SwRect &rRct, + sal_Int16 eHor, bool bLnNm ); + SwFont* GetFont() const { return m_pFnt.get(); } + void IncLineNr() { ++m_nLineNr; } + bool HasNumber() const { + assert( m_rLineInf.GetCountBy() != 0 ); + if( m_rLineInf.GetCountBy() == 0 ) + return false; + return !( m_nLineNr % static_cast<sal_Int32>(m_rLineInf.GetCountBy()) ); + } + bool HasDivider() const { + assert( m_rLineInf.GetDividerCountBy() != 0 ); + if( !m_nDivider || m_rLineInf.GetDividerCountBy() == 0 ) + return false; + return !(m_nLineNr % m_rLineInf.GetDividerCountBy()); + } + + void PaintExtra( SwTwips nY, tools::Long nAsc, tools::Long nMax, bool bRed, const OUString* pRedlineText = nullptr ); + void PaintRedline( SwTwips nY, tools::Long nMax ); +}; + +} + +SwExtraPainter::SwExtraPainter( const SwTextFrame *pFrame, SwViewShell *pVwSh, + const SwLineNumberInfo &rLnInf, const SwRect &rRct, + sal_Int16 eHor, bool bLineNum ) + : m_aClip( pVwSh->GetWin() || pFrame->IsUndersized() ? pVwSh->GetOut() : nullptr ) + , m_aRect( rRct ) + , m_pTextFrame( pFrame ) + , m_pSh( pVwSh ) + , m_rLineInf( rLnInf ) + , m_nX(0) + , m_nRedX(0) + , m_nLineNr( 1 ) + , m_nDivider(0) + , m_bGoLeft(false) +{ + if( pFrame->IsUndersized() ) + { + SwTwips nBottom = pFrame->getFrameArea().Bottom(); + if( m_aRect.Bottom() > nBottom ) + m_aRect.Bottom( nBottom ); + } + std::optional<bool> oIsRightPage; + { + /* Initializes the Members necessary for line numbering: + + nDivider, how often do we want a substring; 0 == never + nX, line number's x position + pFnt, line number's font + nLineNr, the first line number + bLineNum is set back to false if the numbering is completely + outside of the paint rect + */ + m_nDivider = !m_rLineInf.GetDivider().isEmpty() ? m_rLineInf.GetDividerCountBy() : 0; + m_nX = pFrame->getFrameArea().Left(); + SwCharFormat* pFormat = m_rLineInf.GetCharFormat( const_cast<IDocumentStylePoolAccess&>(pFrame->GetDoc().getIDocumentStylePoolAccess()) ); + OSL_ENSURE( pFormat, "PaintExtraData without CharFormat" ); + m_pFnt.reset( new SwFont(&pFormat->GetAttrSet(), &pFrame->GetDoc().getIDocumentSettingAccess()) ); + m_pFnt->Invalidate(); + m_pFnt->ChgPhysFnt( m_pSh, *m_pSh->GetOut() ); + m_pFnt->SetVertical( 0_deg10, pFrame->IsVertical() ); + } + + if( bLineNum ) + { + m_nLineNr += pFrame->GetAllLines() - pFrame->GetThisLines(); + LineNumberPosition ePos = m_rLineInf.GetPos(); + if( ePos != LINENUMBER_POS_LEFT && ePos != LINENUMBER_POS_RIGHT ) + { + if( pFrame->FindPageFrame()->OnRightPage() ) + { + oIsRightPage = true; + ePos = ePos == LINENUMBER_POS_INSIDE ? + LINENUMBER_POS_LEFT : LINENUMBER_POS_RIGHT; + } + else + { + oIsRightPage = false; + ePos = ePos == LINENUMBER_POS_OUTSIDE ? + LINENUMBER_POS_LEFT : LINENUMBER_POS_RIGHT; + } + } + if( LINENUMBER_POS_LEFT == ePos ) + { + m_bGoLeft = true; + m_nX -= m_rLineInf.GetPosFromLeft(); + } + else + { + m_bGoLeft = false; + m_nX += pFrame->getFrameArea().Width() + m_rLineInf.GetPosFromLeft(); + } + } + if( eHor == text::HoriOrientation::NONE ) + return; + + if( text::HoriOrientation::INSIDE == eHor || text::HoriOrientation::OUTSIDE == eHor ) + { + if (!oIsRightPage.has_value()) + oIsRightPage = pFrame->FindPageFrame()->OnRightPage(); + if (*oIsRightPage) + eHor = eHor == text::HoriOrientation::INSIDE ? text::HoriOrientation::LEFT : text::HoriOrientation::RIGHT; + else + eHor = eHor == text::HoriOrientation::OUTSIDE ? text::HoriOrientation::LEFT : text::HoriOrientation::RIGHT; + } + const SwFrame* pTmpFrame = pFrame->FindTabFrame(); + if( !pTmpFrame ) + pTmpFrame = pFrame; + m_nRedX = text::HoriOrientation::LEFT == eHor ? pTmpFrame->getFrameArea().Left() - REDLINE_DISTANCE : + pTmpFrame->getFrameArea().Right() + REDLINE_DISTANCE; +} + +void SwExtraPainter::PaintExtra( SwTwips nY, tools::Long nAsc, tools::Long nMax, bool bRed, const OUString* pRedlineText ) +{ + const OUString aTmp( pRedlineText + // Tracked change is stronger than the line number + ? *pRedlineText + : ( HasNumber() + // Line number is stronger than the divider + ? m_rLineInf.GetNumType().GetNumStr( m_nLineNr ) + : m_rLineInf.GetDivider() ) ); + + // Get script type of line numbering: + m_pFnt->SetActual( SwScriptInfo::WhichFont(0, aTmp) ); + + if ( pRedlineText ) + { + m_pFnt->SetColor(NON_PRINTING_CHARACTER_COLOR); + // don't strike out text in Insertions In Margin mode + if ( !m_pSh->GetViewOptions()->IsShowChangesInMargin2() ) + m_pFnt->SetStrikeout( STRIKEOUT_SINGLE ); + m_pFnt->SetSize( Size( 0, 200), m_pFnt->GetActual() ); + } + + SwDrawTextInfo aDrawInf( m_pSh, *m_pSh->GetOut(), aTmp, 0, aTmp.getLength() ); + aDrawInf.SetSpace( 0 ); + aDrawInf.SetWrong( nullptr ); + aDrawInf.SetGrammarCheck( nullptr ); + aDrawInf.SetSmartTags( nullptr ); + aDrawInf.SetFrame( m_pTextFrame ); + aDrawInf.SetFont( m_pFnt.get() ); + aDrawInf.SetSnapToGrid( false ); + aDrawInf.SetIgnoreFrameRTL( true ); + + bool bTooBig = m_pFnt->GetSize( m_pFnt->GetActual() ).Height() > nMax && + m_pFnt->GetHeight( m_pSh, *m_pSh->GetOut() ) > nMax; + SwFont* pTmpFnt; + if( bTooBig ) + { + pTmpFnt = new SwFont( *GetFont() ); + if( nMax >= 20 ) + { + nMax *= 17; + nMax /= 20; + } + pTmpFnt->SetSize( Size( 0, nMax ), pTmpFnt->GetActual() ); + } + else + pTmpFnt = GetFont(); + Point aTmpPos( m_nX, nY ); + aTmpPos.AdjustY(nAsc ); + if ( pRedlineText ) + { + Size aSize = pTmpFnt->GetTextSize_( aDrawInf ); + aTmpPos.AdjustX( -(aSize.Width()) - 200 ); + } + bool bPaint = true; + if( !IsClipChg() ) + { + Size aSize = pTmpFnt->GetTextSize_( aDrawInf ); + if( m_bGoLeft ) + aTmpPos.AdjustX( -(aSize.Width()) ); + // calculate rectangle containing the line number + SwRect aRct( Point( aTmpPos.X(), + aTmpPos.Y() - pTmpFnt->GetAscent( m_pSh, *m_pSh->GetOut() ) + ), aSize ); + if( !m_aRect.Contains( aRct ) ) + { + if( aRct.Intersection( m_aRect ).IsEmpty() ) + bPaint = false; + else + m_aClip.ChgClip( m_aRect, m_pTextFrame ); + } + } + else if( m_bGoLeft ) + aTmpPos.AdjustX( -(pTmpFnt->GetTextSize_( aDrawInf ).Width()) ); + aDrawInf.SetPos( aTmpPos ); + if( bPaint ) + pTmpFnt->DrawText_( aDrawInf ); + + if( bTooBig ) + delete pTmpFnt; + if( bRed ) + { + tools::Long nDiff = m_bGoLeft ? m_nRedX - m_nX : m_nX - m_nRedX; + if( nDiff > REDLINE_MINDIST ) + PaintRedline( nY, nMax ); + } +} + +void SwExtraPainter::PaintRedline( SwTwips nY, tools::Long nMax ) +{ + Point aStart( m_nRedX, nY ); + Point aEnd( m_nRedX, nY + nMax ); + + if( !IsClipChg() ) + { + SwRect aRct( aStart, aEnd ); + if( !m_aRect.Contains( aRct ) ) + { + if( aRct.Intersection( m_aRect ).IsEmpty() ) + return; + m_aClip.ChgClip( m_aRect, m_pTextFrame ); + } + } + const Color aOldCol( m_pSh->GetOut()->GetLineColor() ); + m_pSh->GetOut()->SetLineColor( SW_MOD()->GetRedlineMarkColor() ); + + if ( m_pTextFrame->IsVertical() ) + { + m_pTextFrame->SwitchHorizontalToVertical( aStart ); + m_pTextFrame->SwitchHorizontalToVertical( aEnd ); + } + + m_pSh->GetOut()->DrawLine( aStart, aEnd ); + m_pSh->GetOut()->SetLineColor( aOldCol ); +} + +void SwTextFrame::PaintExtraData( const SwRect &rRect ) const +{ + if( getFrameArea().Top() > rRect.Bottom() || getFrameArea().Bottom() < rRect.Top() ) + return; + + PaintOutlineContentVisibilityButton(); + + SwDoc const& rDoc(GetDoc()); + const IDocumentRedlineAccess& rIDRA = rDoc.getIDocumentRedlineAccess(); + const SwLineNumberInfo &rLineInf = rDoc.GetLineNumberInfo(); + const SwFormatLineNumber &rLineNum = GetAttrSet()->GetLineNumber(); + bool bLineNum = !IsInTab() && rLineInf.IsPaintLineNumbers() && + ( !IsInFly() || rLineInf.IsCountInFlys() ) && rLineNum.IsCount(); + sal_Int16 eHor = static_cast<sal_Int16>(SW_MOD()->GetRedlineMarkPos()); + if (eHor != text::HoriOrientation::NONE + && (!IDocumentRedlineAccess::IsShowChanges(rIDRA.GetRedlineFlags()) + || getRootFrame()->IsHideRedlines())) + { + eHor = text::HoriOrientation::NONE; + } + bool bRedLine = eHor != text::HoriOrientation::NONE; + if ( !bLineNum && !bRedLine ) + return; + + if( IsLocked() || IsHiddenNow() || !getFramePrintArea().Height() ) + return; + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + + SwSwapIfNotSwapped swap(const_cast<SwTextFrame *>(this)); + SwRect rOldRect( rRect ); + + if ( IsVertical() ) + SwitchVerticalToHorizontal( const_cast<SwRect&>(rRect) ); + + SwLayoutModeModifier aLayoutModeModifier( *pSh->GetOut() ); + aLayoutModeModifier.Modify( false ); + + // #i16816# tagged pdf support + SwTaggedPDFHelper aTaggedPDFHelper( nullptr, nullptr, nullptr, *pSh->GetOut() ); + + SwExtraPainter aExtra( this, pSh, rLineInf, rRect, eHor, bLineNum ); + + if( HasPara() ) + { + TextFrameLockGuard aLock(const_cast<SwTextFrame*>(this)); + + SwTextLineAccess aAccess( this ); + aAccess.GetPara(); + + SwTextPaintInfo aInf( const_cast<SwTextFrame*>(this), rRect ); + + aLayoutModeModifier.Modify( false ); + + SwTextPainter aLine( const_cast<SwTextFrame*>(this), &aInf ); + bool bNoDummy = !aLine.GetNext(); // Only one empty line! + + while( aLine.Y() + aLine.GetLineHeight() <= rRect.Top() ) + { + if( !aLine.GetCurr()->IsDummy() && + ( rLineInf.IsCountBlankLines() || + aLine.GetCurr()->HasContent() ) ) + aExtra.IncLineNr(); + if( !aLine.Next() ) + { + const_cast<SwRect&>(rRect) = rOldRect; + return; + } + } + + tools::Long nBottom = rRect.Bottom(); + + bool bNoPrtLine = 0 == GetMinPrtLine(); + if( !bNoPrtLine ) + { + while ( aLine.Y() < GetMinPrtLine() ) + { + if( ( rLineInf.IsCountBlankLines() || aLine.GetCurr()->HasContent() ) + && !aLine.GetCurr()->IsDummy() ) + aExtra.IncLineNr(); + if( !aLine.Next() ) + break; + } + bNoPrtLine = aLine.Y() >= GetMinPrtLine(); + } + const bool bIsShowChangesInMargin = pSh->GetViewOptions()->IsShowChangesInMargin(); + if( bNoPrtLine ) + { + do + { + if( bNoDummy || !aLine.GetCurr()->IsDummy() ) + { + bool bRed = bRedLine && aLine.GetCurr()->HasRedline(); + if( rLineInf.IsCountBlankLines() || aLine.GetCurr()->HasContent() ) + { + bool bRedInMargin = bIsShowChangesInMargin && bRed; + bool bNum = bLineNum && ( aExtra.HasNumber() || aExtra.HasDivider() ); + if( bRedInMargin || bNum ) + { + SwTwips nTmpHeight, nTmpAscent; + aLine.CalcAscentAndHeight( nTmpAscent, nTmpHeight ); + if ( bRedInMargin ) + { + const OUString* pRedlineText = aLine.GetCurr()->GetRedlineText(); + if( !pRedlineText->isEmpty() ) + { + aExtra.PaintExtra( aLine.Y(), nTmpAscent, + nTmpHeight, bRed, pRedlineText ); + bRed = false; + bNum = false; + } + } + if ( bNum ) + { + aExtra.PaintExtra( aLine.Y(), nTmpAscent, nTmpHeight, bRed ); + bRed = false; + } + } + aExtra.IncLineNr(); + } + if( bRed ) + aExtra.PaintRedline( aLine.Y(), aLine.GetLineHeight() ); + } + } while( aLine.Next() && aLine.Y() <= nBottom ); + } + } + else + { + if (SwRedlineTable::npos == rIDRA.GetRedlinePos(*GetTextNodeFirst(), RedlineType::Any)) + { + bRedLine = false; + } + + if( bLineNum && rLineInf.IsCountBlankLines() && + ( aExtra.HasNumber() || aExtra.HasDivider() ) ) + { + aExtra.PaintExtra( getFrameArea().Top()+getFramePrintArea().Top(), aExtra.GetFont() + ->GetAscent( pSh, *pSh->GetOut() ), getFramePrintArea().Height(), bRedLine ); + } + else if( bRedLine ) + aExtra.PaintRedline( getFrameArea().Top()+getFramePrintArea().Top(), getFramePrintArea().Height() ); + } + + const_cast<SwRect&>(rRect) = rOldRect; + +} + +SwRect SwTextFrame::GetPaintSwRect() +{ + // finger layout + OSL_ENSURE( isFrameAreaPositionValid(), "+SwTextFrame::GetPaintSwRect: no Calc()" ); + + SwRect aRet( getFramePrintArea() ); + if ( IsEmpty() || !HasPara() ) + aRet += getFrameArea().Pos(); + else + { + // We return the right paint rect. Use the calculated PaintOfst as the + // left margin + SwRepaint& rRepaint = GetPara()->GetRepaint(); + tools::Long l; + + if ( IsVertLR() && !IsVertLRBT()) // mba: the following line was added, but we don't need it for the existing directions; kept for IsVertLR(), but should be checked + rRepaint.Chg( GetUpper()->getFrameArea().Pos() + GetUpper()->getFramePrintArea().Pos(), GetUpper()->getFramePrintArea().SSize() ); + + if( rRepaint.GetOffset() ) + rRepaint.Left( rRepaint.GetOffset() ); + + l = rRepaint.GetRightOfst(); + if( l && l > rRepaint.Right() ) + rRepaint.Right( l ); + rRepaint.SetOffset( 0 ); + aRet = rRepaint; + + // In case our left edge is the same as the body frame's left edge, + // then extend the rectangle to include the page margin as well, + // otherwise some font will be clipped. + SwLayoutFrame* pBodyFrame = GetUpper(); + if (pBodyFrame->IsBodyFrame() && aRet.Left() == (pBodyFrame->getFrameArea().Left() + pBodyFrame->getFramePrintArea().Left())) + if (SwLayoutFrame* pPageFrame = pBodyFrame->GetUpper()) + aRet.Left(pPageFrame->getFrameArea().Left()); + + if ( IsRightToLeft() ) + SwitchLTRtoRTL( aRet ); + + if ( IsVertical() ) + SwitchHorizontalToVertical( aRet ); + } + ResetRepaint(); + + return aRet; +} + +bool SwTextFrame::PaintEmpty( const SwRect &rRect, bool bCheck ) const +{ + PaintParagraphStylesHighlighting(); + + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + if( pSh && ( pSh->GetViewOptions()->IsParagraph() || bInitFont ) ) + { + bInitFont = false; + SwTextFly aTextFly( this ); + aTextFly.SetTopRule(); + SwRect aRect; + if( bCheck && aTextFly.IsOn() && aTextFly.IsAnyObj( aRect ) ) + return false; + else if( pSh->GetWin() ) + { + std::unique_ptr<SwFont> pFnt; + RedlineType eRedline = RedlineType::None; + const SwTextNode& rTextNode = *GetTextNodeForParaProps(); + if ( rTextNode.HasSwAttrSet() ) + { + const SwAttrSet *pAttrSet = &( rTextNode.GetSwAttrSet() ); + pFnt.reset(new SwFont( pAttrSet, rTextNode.getIDocumentSettingAccess() )); + } + else + { + SwFontAccess aFontAccess( &rTextNode.GetAnyFormatColl(), pSh ); + pFnt.reset(new SwFont( aFontAccess.Get()->GetFont() )); + } + + const IDocumentRedlineAccess& rIDRA = rTextNode.getIDocumentRedlineAccess(); + if (IDocumentRedlineAccess::IsShowChanges(rIDRA.GetRedlineFlags()) + && !getRootFrame()->IsHideRedlines()) + { + const SwRedlineTable::size_type nRedlPos = rIDRA.GetRedlinePos( rTextNode, RedlineType::Any ); + if( SwRedlineTable::npos != nRedlPos ) + { + SwAttrHandler aAttrHandler; + aAttrHandler.Init( rTextNode.GetSwAttrSet(), + *rTextNode.getIDocumentSettingAccess() ); + SwRedlineItr aRedln(rTextNode, *pFnt, aAttrHandler, nRedlPos, SwRedlineItr::Mode::Show); + const SwRangeRedline* pRedline = rIDRA.GetRedlineTable()[nRedlPos]; + // show redlining only on the inserted/deleted empty paragraph, but not on the next one + if ( rTextNode.GetIndex() != pRedline->End()->GetNodeIndex() ) + eRedline = pRedline->GetType(); + // except if the next empty paragraph starts a new redline (e.g. deletion after insertion) + else if ( nRedlPos + 1 < rIDRA.GetRedlineTable().size() ) + { + const SwRangeRedline* pNextRedline = rIDRA.GetRedlineTable()[nRedlPos + 1]; + if ( rTextNode.GetIndex() == pNextRedline->Start()->GetNodeIndex() ) + eRedline = pNextRedline->GetType(); + } + } + } + + if( pSh->GetViewOptions()->IsParagraph() && getFramePrintArea().Height() ) + { + if( RTL_TEXTENCODING_SYMBOL == pFnt->GetCharSet( SwFontScript::Latin ) && + pFnt->GetName( SwFontScript::Latin ) != numfunc::GetDefBulletFontname() ) + { + pFnt->SetFamily( FAMILY_DONTKNOW, SwFontScript::Latin ); + pFnt->SetName( numfunc::GetDefBulletFontname(), SwFontScript::Latin ); + pFnt->SetStyleName(OUString(), SwFontScript::Latin); + pFnt->SetCharSet( RTL_TEXTENCODING_SYMBOL, SwFontScript::Latin ); + } + pFnt->SetVertical( 0_deg10, IsVertical() ); + SwFrameSwapper aSwapper( this, true ); + SwLayoutModeModifier aLayoutModeModifier( *pSh->GetOut() ); + aLayoutModeModifier.Modify( IsRightToLeft() ); + + pFnt->Invalidate(); + pFnt->ChgPhysFnt( pSh, *pSh->GetOut() ); + Point aPos = getFrameArea().Pos() + getFramePrintArea().Pos(); + + const SvxFirstLineIndentItem& rFirstLine( + GetTextNodeForParaProps()->GetSwAttrSet().GetFirstLineIndent()); + + if (0 < rFirstLine.GetTextFirstLineOffset()) + { + aPos.AdjustX(rFirstLine.GetTextFirstLineOffset()); + } + + std::unique_ptr<SwSaveClip, o3tl::default_delete<SwSaveClip>> xClip; + if( IsUndersized() ) + { + xClip.reset(new SwSaveClip( pSh->GetOut() )); + xClip->ChgClip( rRect ); + } + + aPos.AdjustY(pFnt->GetAscent( pSh, *pSh->GetOut() ) ); + + if (GetTextNodeForParaProps()->GetSwAttrSet().GetParaGrid().GetValue() && + IsInDocBody() ) + { + SwTextGridItem const*const pGrid(GetGridItem(FindPageFrame())); + if ( pGrid ) + { + // center character in grid line + aPos.AdjustY(( pGrid->GetBaseHeight() - + pFnt->GetHeight( pSh, *pSh->GetOut() ) ) / 2 ); + + if ( ! pGrid->GetRubyTextBelow() ) + aPos.AdjustY(pGrid->GetRubyHeight() ); + } + } + + // Don't show the paragraph mark for collapsed paragraphs, when they are hidden + // No paragraph marker in the non-last part of a split fly anchor, either. + if ( EmptyHeight( ) > 1 && !HasNonLastSplitFlyDrawObj() ) + { + SwDrawTextInfo aDrawInf( pSh, *pSh->GetOut(), CH_PAR, 0, 1 ); + aDrawInf.SetPos( aPos ); + aDrawInf.SetSpace( 0 ); + aDrawInf.SetKanaComp( 0 ); + aDrawInf.SetWrong( nullptr ); + aDrawInf.SetGrammarCheck( nullptr ); + aDrawInf.SetSmartTags( nullptr ); + aDrawInf.SetFrame( this ); + aDrawInf.SetFont( pFnt.get() ); + aDrawInf.SetSnapToGrid( false ); + + // show redline color and settings drawing a background pilcrow, + // but keep also other formattings (with neutral pilcrow color) + if ( eRedline != RedlineType::None ) + { + pFnt->DrawText_( aDrawInf ); + if ( eRedline == RedlineType::Delete ) + pFnt->SetStrikeout( STRIKEOUT_NONE ); + else + pFnt->SetUnderline( LINESTYLE_NONE ); + } + + pFnt->SetColor(NON_PRINTING_CHARACTER_COLOR); + pFnt->DrawText_( aDrawInf ); + } + } + return true; + } + } + else + return true; + return false; +} + +void SwTextFrame::PaintSwFrame(vcl::RenderContext& rRenderContext, SwRect const& rRect, SwPrintData const*const) const +{ + ResetRepaint(); + + // #i16816# tagged pdf support + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + + if( IsEmpty() && PaintEmpty( rRect, true ) ) + return; + + if( IsLocked() || IsHiddenNow() || ! getFramePrintArea().HasArea() ) + return; + + // It can happen that the IdleCollector withdrew my cached information + if( !HasPara() ) + { + OSL_ENSURE( isFrameAreaPositionValid(), "+SwTextFrame::PaintSwFrame: no Calc()" ); + + // #i29062# pass info that we are currently + // painting. + const_cast<SwTextFrame*>(this)->GetFormatted( true ); + if( IsEmpty() ) + { + PaintEmpty( rRect, false ); + return; + } + if( !HasPara() ) + { + OSL_ENSURE( false, "+SwTextFrame::PaintSwFrame: missing format information" ); + return; + } + } + + // tdf140219-2.odt text frame with only fly portions and a follow is not + // actually a paragraph - delay creating all structured elements to follow. + bool const isPDFTaggingEnabled(!HasFollow() || GetPara()->HasContentPortions()); + ::std::optional<SwTaggedPDFHelper> oTaggedPDFHelperNumbering; + if (isPDFTaggingEnabled) + { + Num_Info aNumInfo(*this); + oTaggedPDFHelperNumbering.emplace(&aNumInfo, nullptr, nullptr, rRenderContext); + } + + // Lbl unfortunately must be able to contain multiple numbering portions + // that may be on multiple lines of text (but apparently always in the + // master frame), so it gets complicated. + ::std::optional<SwTaggedPDFHelper> oTaggedLabel; + // Paragraph tag - if there is a list label, opening should be delayed. + ::std::optional<SwTaggedPDFHelper> oTaggedParagraph; + + if (isPDFTaggingEnabled + && (GetTextNodeForParaProps()->IsOutline() + || !GetPara()->HasNumberingPortion(SwParaPortion::FootnoteToo))) + { // no Lbl needed => open paragraph tag now + Frame_Info aFrameInfo(*this, false); + oTaggedParagraph.emplace(nullptr, &aFrameInfo, nullptr, rRenderContext); + } + + // We don't want to be interrupted while painting. + // Do that after thr Format()! + TextFrameLockGuard aLock(const_cast<SwTextFrame*>(this)); + + // We only paint the part of the TextFrame which changed, is within the + // range and was requested to paint. + // One could think that the area rRect _needs_ to be painted, although + // rRepaint is set. Indeed, we cannot avoid this problem from a formal + // perspective. Luckily we can assume rRepaint to be empty when we need + // paint the while Frame. + SwTextLineAccess aAccess( this ); + SwParaPortion *pPara = aAccess.GetPara(); + + SwRepaint &rRepaint = pPara->GetRepaint(); + + // Switch off recycling when in the FlyContentFrame. + // A DrawRect is called for repainting the line anyways. + if( rRepaint.GetOffset() ) + { + const SwFlyFrame *pFly = FindFlyFrame(); + if( pFly && pFly->IsFlyInContentFrame() ) + rRepaint.SetOffset( 0 ); + } + + // Ge the String for painting. The length is of special interest. + + // Rectangle + OSL_ENSURE( ! IsSwapped(), "A frame is swapped before Paint" ); + SwRect aOldRect( rRect ); + + { + SwSwapIfNotSwapped swap(const_cast<SwTextFrame *>(this)); + + if ( IsVertical() ) + SwitchVerticalToHorizontal( const_cast<SwRect&>(rRect) ); + + if ( IsRightToLeft() ) + SwitchRTLtoLTR( const_cast<SwRect&>(rRect) ); + + SwTextPaintInfo aInf( const_cast<SwTextFrame*>(this), rRect ); + sw::WrongListIterator iterWrong(*this, &SwTextNode::GetWrong); + sw::WrongListIterator iterGrammar(*this, &SwTextNode::GetGrammarCheck); + sw::WrongListIterator iterSmartTags(*this, &SwTextNode::GetSmartTags); + if (iterWrong.LooksUseful()) + { + aInf.SetWrongList( &iterWrong ); + } + if (iterGrammar.LooksUseful()) + { + aInf.SetGrammarCheckList( &iterGrammar ); + } + if (iterSmartTags.LooksUseful()) + { + aInf.SetSmartTags( &iterSmartTags ); + } + aInf.GetTextFly().SetTopRule(); + + SwTextPainter aLine( const_cast<SwTextFrame*>(this), &aInf ); + // Optimization: if no free flying Frame overlaps into our line, the + // SwTextFly just switches off + aInf.GetTextFly().Relax(); + + OutputDevice* pOut = aInf.GetOut(); + const bool bOnWin = pSh->GetWin() != nullptr; + + SwSaveClip aClip( bOnWin || IsUndersized() ? pOut : nullptr ); + + // Output loop: For each Line ... (which is still visible) ... + // adapt rRect (Top + 1, Bottom - 1) + // Because the Iterator attaches the Lines without a gap to each other + aLine.TwipsToLine( rRect.Top() + 1 ); + tools::Long nBottom = rRect.Bottom(); + + bool bNoPrtLine = 0 == GetMinPrtLine(); + if( !bNoPrtLine ) + { + while ( aLine.Y() < GetMinPrtLine() && aLine.Next() ) + ; + bNoPrtLine = aLine.Y() >= GetMinPrtLine(); + } + if( bNoPrtLine ) + { + do + { + aLine.DrawTextLine(rRect, aClip, IsUndersized(), oTaggedLabel, oTaggedParagraph, isPDFTaggingEnabled); + + } while( aLine.Next() && aLine.Y() <= nBottom ); + } + + // Once is enough: + if( aLine.IsPaintDrop() ) + aLine.PaintDropPortion(); + + if( rRepaint.HasArea() ) + rRepaint.Clear(); + } + + PaintParagraphStylesHighlighting(); + + const_cast<SwRect&>(rRect) = aOldRect; + + OSL_ENSURE( ! IsSwapped(), "A frame is swapped after Paint" ); + + assert(!oTaggedLabel); // must have been closed if opened + assert(!isPDFTaggingEnabled || oTaggedParagraph || rRect.GetIntersection(getFrameArea()) != getFrameArea()); // must have been created during complete paint (PDF export is always complete paint) +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/guess.cxx b/sw/source/core/text/guess.cxx new file mode 100644 index 0000000000..3346fe345a --- /dev/null +++ b/sw/source/core/text/guess.cxx @@ -0,0 +1,761 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <editeng/unolingu.hxx> +#include <breakit.hxx> +#include <IDocumentSettingAccess.hxx> +#include "guess.hxx" +#include "inftxt.hxx" +#include <pagefrm.hxx> +#include <tgrditem.hxx> +#include <com/sun/star/i18n/BreakType.hpp> +#include <com/sun/star/i18n/WordType.hpp> +#include <com/sun/star/i18n/XBreakIterator.hpp> +#include <unotools/charclass.hxx> +#include <svl/urihelper.hxx> +#include "porfld.hxx" +#include <paratr.hxx> +#include <doc.hxx> +#include <unotools/linguprops.hxx> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::i18n; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::linguistic2; + +namespace{ + +bool IsBlank(sal_Unicode ch) { return ch == CH_BLANK || ch == CH_FULL_BLANK || ch == CH_NB_SPACE || ch == CH_SIX_PER_EM; } + +// Used when spaces should not be counted in layout +// Returns adjusted cut position +TextFrameIndex AdjustCutPos(TextFrameIndex cutPos, TextFrameIndex& rBreakPos, + const SwTextFormatInfo& rInf) +{ + assert(cutPos >= rInf.GetIdx()); + TextFrameIndex x = rBreakPos = cutPos; + + // we step back until a non blank character has been found + // or there is only one more character left + while (x && x > rInf.GetIdx() + TextFrameIndex(1) && IsBlank(rInf.GetChar(--x))) + --rBreakPos; + + while (IsBlank(rInf.GetChar(cutPos))) + ++cutPos; + + return cutPos; +} + +bool hasBlanksInLine(const SwTextFormatInfo& rInf, TextFrameIndex end) +{ + for (auto x = rInf.GetLineStart(); x < end; ++x) + if (IsBlank(rInf.GetChar(x))) + return true; + return false; +} + +// Called for the last text run in a line; if it is block-adjusted, or center / right-adjusted +// with Word compatibility option set, and it has trailing spaces, then the function sets the +// values, and returns 'false' value that SwTextGuess::Guess should return, to create a +// trailing SwHolePortion. +bool maybeAdjustPositionsForBlockAdjust(TextFrameIndex& rCutPos, TextFrameIndex& rBreakPos, + TextFrameIndex& rBreakStart, sal_uInt16& rBreakWidth, + sal_uInt16& rExtraBlankWidth, sal_uInt16& rMaxSizeDiff, + const SwTextFormatInfo& rInf, const SwScriptInfo& rSI, + sal_uInt16 maxComp) +{ + const auto& adjObj = rInf.GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet().GetAdjust(); + const SvxAdjust& adjust = adjObj.GetAdjust(); + if (adjust == SvxAdjust::Block) + { + if (rInf.DontBlockJustify()) + return true; // See tdf#106234 + } + else + { + // tdf#104668 space chars at the end should be cut if the compatibility option is enabled + if (!rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::MS_WORD_COMP_TRAILING_BLANKS)) + return true; + // for LTR mode only + if (rInf.GetTextFrame()->IsRightToLeft()) + return true; + } + if (auto ch = rInf.GetChar(rCutPos); !ch) // end of paragraph - last line + { + if (adjust == SvxAdjust::Block) + { + // Check adjustment for last line + switch (adjObj.GetLastBlock()) + { + default: + return true; + case SvxAdjust::Center: // tdf#104668 + if (!rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::MS_WORD_COMP_TRAILING_BLANKS)) + return true; + break; + case SvxAdjust::Block: + break; // OK - last line uses block-adjustment + } + } + } + else if (ch != CH_BREAK && !IsBlank(ch)) + return true; + + // tdf#57187: block-adjusted line shorter than full width, terminated by manual + // line break, must not use trailing spaces for adjustment + TextFrameIndex breakPos; + TextFrameIndex newCutPos = AdjustCutPos(rCutPos, breakPos, rInf); + + if (auto ch = rInf.GetChar(newCutPos); ch && ch != CH_BREAK) + return true; // next is neither line break nor paragraph end + if (breakPos == newCutPos) + return true; // no trailing whitespace + if (adjust == SvxAdjust::Block && adjObj.GetOneWord() != SvxAdjust::Block + && !hasBlanksInLine(rInf, breakPos)) + return true; // line can't block-adjust + + // Some trailing spaces actually found, and in case of block adjustment, the text portion + // itself has spaces to be able to block-adjust, or single word is allowed to adjust + rBreakStart = rCutPos = newCutPos; + rBreakPos = breakPos; + + rInf.GetTextSize(&rSI, rInf.GetIdx(), breakPos - rInf.GetIdx(), maxComp, rBreakWidth, + rMaxSizeDiff, rInf.GetCachedVclData().get()); + rInf.GetTextSize(&rSI, breakPos, rBreakStart - breakPos, maxComp, rExtraBlankWidth, + rMaxSizeDiff, rInf.GetCachedVclData().get()); + + return false; // require SwHolePortion creation +} + +} + +// provides information for line break calculation +// returns true if no line break has to be performed +// otherwise possible break or hyphenation position is determined +bool SwTextGuess::Guess( const SwTextPortion& rPor, SwTextFormatInfo &rInf, + const sal_uInt16 nPorHeight, sal_Int32 nSpacesInLine ) +{ + m_nCutPos = rInf.GetIdx(); + + // Empty strings are always 0 + if( !rInf.GetLen() || rInf.GetText().isEmpty() ) + return false; + + OSL_ENSURE( rInf.GetIdx() < TextFrameIndex(rInf.GetText().getLength()), + "+SwTextGuess::Guess: invalid SwTextFormatInfo" ); + + OSL_ENSURE( nPorHeight, "+SwTextGuess::Guess: no height" ); + + sal_uInt16 nMaxSizeDiff; + + const SwScriptInfo& rSI = rInf.GetParaPortion()->GetScriptInfo(); + + sal_uInt16 nMaxComp = ( SwFontScript::CJK == rInf.GetFont()->GetActual() ) && + rSI.CountCompChg() && + ! rInf.IsMulti() && + ! rPor.InFieldGrp() && + ! rPor.IsDropPortion() ? + 10000 : + 0 ; + + SwTwips nLineWidth = rInf.GetLineWidth(); + TextFrameIndex nMaxLen = TextFrameIndex(rInf.GetText().getLength()) - rInf.GetIdx(); + + const SvxAdjust& rAdjust = rInf.GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet().GetAdjust().GetAdjust(); + + // allow up to 20% shrinking of the spaces + if ( nSpacesInLine ) + { + static constexpr OUStringLiteral STR_BLANK = u" "; + sal_Int16 nSpaceWidth = rInf.GetTextSize(STR_BLANK).Width(); + nLineWidth += nSpacesInLine * (nSpaceWidth/0.8 - nSpaceWidth); + } + + if ( rInf.GetLen() < nMaxLen ) + nMaxLen = rInf.GetLen(); + + if( !nMaxLen ) + return false; + + sal_uInt16 nItalic = 0; + if( ITALIC_NONE != rInf.GetFont()->GetItalic() && !rInf.NotEOL() ) + { + bool bAddItalic = true; + + // do not add extra italic value if we have an active character grid + if ( rInf.SnapToGrid() ) + { + SwTextGridItem const*const pGrid( + GetGridItem(rInf.GetTextFrame()->FindPageFrame())); + bAddItalic = !pGrid || GRID_LINES_CHARS != pGrid->GetGridType(); + } + + // do not add extra italic value for an isolated blank: + if (TextFrameIndex(1) == rInf.GetLen() && + CH_BLANK == rInf.GetText()[sal_Int32(rInf.GetIdx())]) + { + bAddItalic = false; + } + + if (rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::TAB_OVER_MARGIN)) + { + // Content is allowed over the margin: in this case over-margin content caused by italic + // formatting is OK. + bAddItalic = false; + } + + nItalic = bAddItalic ? nPorHeight / 12 : 0; + + nLineWidth -= nItalic; + + // #i46524# LineBreak bug with italics + if ( nLineWidth < 0 ) nLineWidth = 0; + } + + const sal_Int32 nLeftRightBorderSpace = + (!rPor.GetJoinBorderWithNext() ? rInf.GetFont()->GetRightBorderSpace() : 0) + + (!rPor.GetJoinBorderWithPrev() ? rInf.GetFont()->GetLeftBorderSpace() : 0); + + nLineWidth -= nLeftRightBorderSpace; + + const bool bUnbreakableNumberings = rInf.GetTextFrame()->GetDoc() + .getIDocumentSettingAccess().get(DocumentSettingId::UNBREAKABLE_NUMBERINGS); + + // first check if everything fits to line + if ( ( nLineWidth * 2 > SwTwips(sal_Int32(nMaxLen)) * nPorHeight ) || + ( bUnbreakableNumberings && rPor.IsNumberPortion() ) ) + { + // call GetTextSize with maximum compression (for kanas) + rInf.GetTextSize( &rSI, rInf.GetIdx(), nMaxLen, + nMaxComp, m_nBreakWidth, nMaxSizeDiff ); + + if ( ( m_nBreakWidth <= nLineWidth ) || ( bUnbreakableNumberings && rPor.IsNumberPortion() ) ) + { + // portion fits to line + m_nCutPos = rInf.GetIdx() + nMaxLen; + bool bRet = rPor.InFieldGrp() + || maybeAdjustPositionsForBlockAdjust(m_nCutPos, m_nBreakPos, m_nBreakStart, + m_nBreakWidth, m_nExtraBlankWidth, + nMaxSizeDiff, rInf, rSI, nMaxComp); + if( nItalic && + (m_nCutPos >= TextFrameIndex(rInf.GetText().getLength()) || + // #i48035# Needed for CalcFitToContent + // if first line ends with a manual line break + rInf.GetText()[sal_Int32(m_nCutPos)] == CH_BREAK)) + m_nBreakWidth += nItalic; + + // save maximum width for later use + if ( nMaxSizeDiff ) + rInf.SetMaxWidthDiff( &rPor, nMaxSizeDiff ); + + m_nBreakWidth += nLeftRightBorderSpace; + + return bRet; + } + } + + bool bHyph = rInf.IsHyphenate() && !rInf.IsHyphForbud(); + TextFrameIndex nHyphPos(0); + + // nCutPos is the first character not fitting to the current line + // nHyphPos is the first character not fitting to the current line, + // considering an additional "-" for hyphenation + if( bHyph ) + { + // nHyphZone is the first character not fitting in the hyphenation zone, + // or 0, if the whole line in the hyphenation zone, + // or -1, if no hyphenation zone defined (i.e. it is 0) + sal_Int32 nHyphZone = -1; + const css::beans::PropertyValues & rHyphValues = rInf.GetHyphValues(); + assert( rHyphValues.getLength() > 5 && rHyphValues[5].Name == UPN_HYPH_ZONE ); + // hyphenation zone (distance from the line end in twips) + sal_uInt16 nTextHyphenZone; + if ( rHyphValues[5].Value >>= nTextHyphenZone ) + nHyphZone = nTextHyphenZone >= nLineWidth + ? 0 + : sal_Int32(rInf.GetTextBreak( nLineWidth - nTextHyphenZone, + nMaxLen, nMaxComp, rInf.GetCachedVclData().get() )); + + m_nCutPos = rInf.GetTextBreak( nLineWidth, nMaxLen, nMaxComp, nHyphPos, rInf.GetCachedVclData().get() ); + + // don't try to hyphenate in the hyphenation zone + if ( nHyphZone != -1 && TextFrameIndex(COMPLETE_STRING) != m_nCutPos ) + { + sal_Int32 nZonePos = sal_Int32(m_nCutPos); + // disable hyphenation, if there is a space within the hyphenation zone + // Note: for better interoperability, not fitting space character at + // rInf.GetIdx()[nHyphZone] always disables the hyphenation, don't need to calculate + // with its fitting part. Moreover, do not check double or more spaces there, they + // are accepted outside of the hyphenation zone, too. + for (; sal_Int32(rInf.GetIdx()) <= nZonePos && nHyphZone <= nZonePos; --nZonePos ) + { + sal_Unicode cChar = rInf.GetText()[nZonePos]; + if ( cChar == CH_BLANK || cChar == CH_FULL_BLANK || cChar == CH_SIX_PER_EM ) + { + bHyph = false; + } + } + } + + if (!rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::HYPHENATE_URLS)) + { + // check URL from preceding space - similar to what AutoFormat does + const CharClass& rCC = GetAppCharClass(); + sal_Int32 begin(m_nCutPos == TextFrameIndex(COMPLETE_STRING) ? rInf.GetText().getLength() : sal_Int32(m_nCutPos)); + sal_Int32 end(begin); + for (; 0 < begin; --begin) + { + sal_Unicode cChar = rInf.GetText()[begin - 1]; + if (cChar == CH_BLANK || cChar == CH_FULL_BLANK || cChar == CH_SIX_PER_EM) + { + break; + } + } + for (; end < rInf.GetText().getLength(); ++end) + { + sal_Unicode cChar = rInf.GetText()[end]; + if (cChar == CH_BLANK || cChar == CH_FULL_BLANK || cChar == CH_SIX_PER_EM) + { + break; + } + } + if (!URIHelper::FindFirstURLInText(rInf.GetText(), begin, end, rCC).isEmpty()) + { + bHyph = false; + } + } + + // search start of the last word, if needed + if ( bHyph ) + { + // nLastWord is the space character before the last word + sal_Int32 nLastWord = rInf.GetText().getLength() - 1; + bool bHyphenationNoLastWord = false; + assert( rHyphValues.getLength() > 3 && rHyphValues[3].Name == UPN_HYPH_NO_LAST_WORD ); + if ( rHyphValues[3].Value >>= bHyphenationNoLastWord ) + { + // skip spaces after the last word + bool bCutBlank = false; + for (; sal_Int32(rInf.GetIdx()) <= nLastWord; --nLastWord ) + { + sal_Unicode cChar = rInf.GetText()[nLastWord]; + if ( cChar != CH_BLANK && cChar != CH_FULL_BLANK && cChar != CH_SIX_PER_EM ) + bCutBlank = true; + else if ( bCutBlank ) + break; + } + } + + // don't hyphenate the last word of the paragraph + if ( bHyphenationNoLastWord && sal_Int32(m_nCutPos) > nLastWord && + TextFrameIndex(COMPLETE_STRING) != m_nCutPos && + // if the last word is multiple line long, e.g. an URL, + // apply this only if the space before the word is there + // in the actual line, i.e. start the long word in a new + // line, but still allows to break its last parts + sal_Int32(rInf.GetIdx()) < nLastWord ) + { + m_nCutPos = TextFrameIndex(nLastWord); + } + } + + if ( !nHyphPos && rInf.GetIdx() ) + nHyphPos = rInf.GetIdx() - TextFrameIndex(1); + } + else + { + m_nCutPos = rInf.GetTextBreak( nLineWidth, nMaxLen, nMaxComp, rInf.GetCachedVclData().get() ); + +#if OSL_DEBUG_LEVEL > 1 + if ( TextFrameIndex(COMPLETE_STRING) != m_nCutPos ) + { + sal_uInt16 nMinSize; + rInf.GetTextSize( &rSI, rInf.GetIdx(), m_nCutPos - rInf.GetIdx(), + nMaxComp, nMinSize, nMaxSizeDiff ); + OSL_ENSURE( nMinSize <= nLineWidth, "What a Guess!!!" ); + } +#endif + } + + if( m_nCutPos > rInf.GetIdx() + nMaxLen ) + { + // second check if everything fits to line + m_nCutPos = m_nBreakPos = rInf.GetIdx() + nMaxLen - TextFrameIndex(1); + rInf.GetTextSize( &rSI, rInf.GetIdx(), nMaxLen, nMaxComp, + m_nBreakWidth, nMaxSizeDiff ); + + // The following comparison should always give true, otherwise + // there likely has been a pixel rounding error in GetTextBreak + if ( m_nBreakWidth <= nLineWidth ) + { + bool bRet = rPor.InFieldGrp() + || maybeAdjustPositionsForBlockAdjust(m_nCutPos, m_nBreakPos, m_nBreakStart, + m_nBreakWidth, m_nExtraBlankWidth, + nMaxSizeDiff, rInf, rSI, nMaxComp); + + if (nItalic && (m_nBreakPos + TextFrameIndex(1)) >= TextFrameIndex(rInf.GetText().getLength())) + m_nBreakWidth += nItalic; + + // save maximum width for later use + if ( nMaxSizeDiff ) + rInf.SetMaxWidthDiff( &rPor, nMaxSizeDiff ); + + m_nBreakWidth += nLeftRightBorderSpace; + + return bRet; + } + } + + // we have to trigger an underflow for a footnote portion + // which does not fit to the current line + if ( rPor.IsFootnotePortion() ) + { + m_nBreakPos = rInf.GetIdx(); + m_nCutPos = TextFrameIndex(-1); + return false; + } + + TextFrameIndex nPorLen(0); + // do not call the break iterator nCutPos is a blank + sal_Unicode cCutChar = rInf.GetChar(m_nCutPos); + if (IsBlank(cCutChar)) + { + m_nCutPos = m_nBreakStart = AdjustCutPos(m_nCutPos, m_nBreakPos, rInf); + nPorLen = m_nBreakPos - rInf.GetIdx(); + } + else + { + // New: We should have a look into the last portion, if it was a + // field portion. For this, we expand the text of the field portion + // into our string. If the line break position is inside of before + // the field portion, we trigger an underflow. + + TextFrameIndex nOldIdx = rInf.GetIdx(); + sal_Unicode cFieldChr = 0; + +#if OSL_DEBUG_LEVEL > 0 + OUString aDebugString; +#endif + + // be careful: a field portion can be both: 0x01 (common field) + // or 0x02 (the follow of a footnode) + if ( rInf.GetLast() && rInf.GetLast()->InFieldGrp() && + ! rInf.GetLast()->IsFootnotePortion() && + rInf.GetIdx() > rInf.GetLineStart() && + CH_TXTATR_BREAKWORD == + (cFieldChr = rInf.GetText()[sal_Int32(rInf.GetIdx()) - 1])) + { + SwFieldPortion* pField = static_cast<SwFieldPortion*>(rInf.GetLast()); + OUString aText; + pField->GetExpText( rInf, aText ); + + if ( !aText.isEmpty() ) + { + m_nFieldDiff = TextFrameIndex(aText.getLength() - 1); + m_nCutPos = m_nCutPos + m_nFieldDiff; + nHyphPos = nHyphPos + m_nFieldDiff; + +#if OSL_DEBUG_LEVEL > 0 + aDebugString = rInf.GetText(); +#endif + + // this is pretty nutso... reverted at the end... + OUString& rOldText = const_cast<OUString&> (rInf.GetText()); + rOldText = rOldText.replaceAt(sal_Int32(rInf.GetIdx()) - 1, 1, aText); + rInf.SetIdx( rInf.GetIdx() + m_nFieldDiff ); + } + else + cFieldChr = 0; + } + + LineBreakHyphenationOptions aHyphOpt; + Reference< XHyphenator > xHyph; + if( bHyph ) + { + xHyph = ::GetHyphenator(); + aHyphOpt = LineBreakHyphenationOptions( xHyph, + rInf.GetHyphValues(), sal_Int32(nHyphPos)); + } + + // Get Language for break iterator. + // We have to switch the current language if we have a script + // change at nCutPos. Otherwise LATIN punctuation would never + // be allowed to be hanging punctuation. + // NEVER call GetLang if the string has been modified!!! + LanguageType aLang = rInf.GetFont()->GetLanguage(); + + // If we are inside a field portion, we use a temporary string which + // differs from the string at the textnode. Therefore we are not allowed + // to call the GetLang function. + if ( m_nCutPos && ! rPor.InFieldGrp() ) + { + const CharClass& rCC = GetAppCharClass(); + + // step back until a non-punctuation character is reached + TextFrameIndex nLangIndex = m_nCutPos; + + // If a field has been expanded right in front of us we do not + // step further than the beginning of the expanded field + // (which is the position of the field placeholder in our + // original string). + const TextFrameIndex nDoNotStepOver = CH_TXTATR_BREAKWORD == cFieldChr + ? rInf.GetIdx() - m_nFieldDiff - TextFrameIndex(1) + : TextFrameIndex(0); + + if ( nLangIndex > nDoNotStepOver && + TextFrameIndex(rInf.GetText().getLength()) == nLangIndex) + --nLangIndex; + + while ( nLangIndex > nDoNotStepOver && + !rCC.isLetterNumeric(rInf.GetText(), sal_Int32(nLangIndex))) + --nLangIndex; + + // last "real" character is not inside our current portion + // we have to check the script type of the last "real" character + if ( nLangIndex < rInf.GetIdx() ) + { + sal_uInt16 nScript = g_pBreakIt->GetRealScriptOfText( rInf.GetText(), + sal_Int32(nLangIndex)); + OSL_ENSURE( nScript, "Script is not between 1 and 4" ); + + // compare current script with script from last "real" character + if ( SwFontScript(nScript - 1) != rInf.GetFont()->GetActual() ) + { + aLang = rInf.GetTextFrame()->GetLangOfChar( + CH_TXTATR_BREAKWORD == cFieldChr + ? nDoNotStepOver + : nLangIndex, + nScript, true); + } + } + } + + const ForbiddenCharacters aForbidden( + *rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().getForbiddenCharacters(aLang, true)); + + const bool bAllowHanging = rInf.IsHanging() && ! rInf.IsMulti() && + ! rInf.GetTextFrame()->IsInTab() && + ! rPor.InFieldGrp(); + + LineBreakUserOptions aUserOpt( + aForbidden.beginLine, aForbidden.endLine, + rInf.HasForbiddenChars(), bAllowHanging, false ); + + // !!! We must have a local copy of the locale, because inside + // getLineBreak the LinguEventListener can trigger a new formatting, + // which can corrupt the locale pointer inside pBreakIt. + const lang::Locale aLocale = g_pBreakIt->GetLocale( aLang ); + + // determines first possible line break from nCutPos to + // start index of current line + LineBreakResults aResult = g_pBreakIt->GetBreakIter()->getLineBreak( + rInf.GetText(), sal_Int32(m_nCutPos), aLocale, + sal_Int32(rInf.GetLineStart()), aHyphOpt, aUserOpt ); + + m_nBreakPos = TextFrameIndex(aResult.breakIndex); + + // if we are formatting multi portions we want to allow line breaks + // at the border between single line and multi line portion + // we have to be careful with footnote portions, they always come in + // with an index 0 + if ( m_nBreakPos < rInf.GetLineStart() && rInf.IsFirstMulti() && + ! rInf.IsFootnoteInside() ) + m_nBreakPos = rInf.GetLineStart(); + + m_nBreakStart = m_nBreakPos; + + bHyph = BreakType::HYPHENATION == aResult.breakType; + + if (bHyph && m_nBreakPos != TextFrameIndex(COMPLETE_STRING)) + { + // found hyphenation position within line + // nBreakPos is set to the hyphenation position + m_xHyphWord = aResult.rHyphenatedWord; + m_nBreakPos += TextFrameIndex(m_xHyphWord->getHyphenationPos() + 1); + + // if not in interactive mode, we have to break behind a soft hyphen + if ( ! rInf.IsInterHyph() && rInf.GetIdx() ) + { + sal_Int32 const nSoftHyphPos = + m_xHyphWord->getWord().indexOf( CHAR_SOFTHYPHEN ); + + if ( nSoftHyphPos >= 0 && + m_nBreakStart + TextFrameIndex(nSoftHyphPos) <= m_nBreakPos && + m_nBreakPos > rInf.GetLineStart() ) + m_nBreakPos = rInf.GetIdx() - TextFrameIndex(1); + } + + if( m_nBreakPos >= rInf.GetIdx() ) + { + nPorLen = m_nBreakPos - rInf.GetIdx(); + if ('-' == rInf.GetText()[ sal_Int32(m_nBreakPos) - 1 ]) + m_xHyphWord = nullptr; + } + } + else if ( !bHyph && m_nBreakPos >= rInf.GetLineStart() ) + { + OSL_ENSURE(sal_Int32(m_nBreakPos) != COMPLETE_STRING, "we should have found a break pos"); + + // found break position within line + m_xHyphWord = nullptr; + + // check, if break position is soft hyphen and an underflow + // has to be triggered + if( m_nBreakPos > rInf.GetLineStart() && rInf.GetIdx() && + CHAR_SOFTHYPHEN == rInf.GetText()[ sal_Int32(m_nBreakPos) - 1 ]) + { + m_nBreakPos = rInf.GetIdx() - TextFrameIndex(1); + } + + if( rAdjust != SvxAdjust::Left ) + { + // Delete any blanks at the end of a line, but be careful: + // If a field has been expanded, we do not want to delete any + // blanks inside the field portion. This would cause an unwanted + // underflow + TextFrameIndex nX = m_nBreakPos; + while( nX > rInf.GetLineStart() && + ( CH_TXTATR_BREAKWORD != cFieldChr || nX > rInf.GetIdx() ) && + ( CH_BLANK == rInf.GetChar( --nX ) || + CH_SIX_PER_EM == rInf.GetChar( nX ) || + CH_FULL_BLANK == rInf.GetChar( nX ) ) ) + m_nBreakPos = nX; + } + if( m_nBreakPos > rInf.GetIdx() ) + nPorLen = m_nBreakPos - rInf.GetIdx(); + } + else + { + // no line break found, setting nBreakPos to COMPLETE_STRING + // causes a break cut + m_nBreakPos = TextFrameIndex(COMPLETE_STRING); + OSL_ENSURE( m_nCutPos >= rInf.GetIdx(), "Deep cut" ); + nPorLen = m_nCutPos - rInf.GetIdx(); + } + + if (m_nBreakPos > m_nCutPos && m_nBreakPos != TextFrameIndex(COMPLETE_STRING)) + { + const TextFrameIndex nHangingLen = m_nBreakPos - m_nCutPos; + SwPosSize aTmpSize = rInf.GetTextSize( &rSI, m_nCutPos, nHangingLen ); + aTmpSize.Width(aTmpSize.Width() + nLeftRightBorderSpace); + OSL_ENSURE( !m_pHanging, "A hanging portion is hanging around" ); + m_pHanging.reset( new SwHangingPortion( std::move(aTmpSize) ) ); + m_pHanging->SetLen( nHangingLen ); + nPorLen = m_nCutPos - rInf.GetIdx(); + } + + // If we expanded a field, we must repair the original string. + // In case we do not trigger an underflow, we correct the nBreakPos + // value, but we cannot correct the nBreakStart value: + // If we have found a hyphenation position, nBreakStart can lie before + // the field. + if ( CH_TXTATR_BREAKWORD == cFieldChr ) + { + if ( m_nBreakPos < rInf.GetIdx() ) + m_nBreakPos = nOldIdx - TextFrameIndex(1); + else if (TextFrameIndex(COMPLETE_STRING) != m_nBreakPos) + { + OSL_ENSURE( m_nBreakPos >= m_nFieldDiff, "I've got field trouble!" ); + m_nBreakPos = m_nBreakPos - m_nFieldDiff; + } + + OSL_ENSURE( m_nCutPos >= rInf.GetIdx() && m_nCutPos >= m_nFieldDiff, + "I've got field trouble, part2!" ); + m_nCutPos = m_nCutPos - m_nFieldDiff; + + OUString& rOldText = const_cast<OUString&> (rInf.GetText()); + OUString aReplacement( cFieldChr ); + rOldText = rOldText.replaceAt(sal_Int32(nOldIdx) - 1, sal_Int32(m_nFieldDiff) + 1, aReplacement); + rInf.SetIdx( nOldIdx ); + +#if OSL_DEBUG_LEVEL > 0 + OSL_ENSURE( aDebugString == rInf.GetText(), + "Somebody, somebody, somebody put something in my string" ); +#endif + } + } + + if( nPorLen ) + { + rInf.GetTextSize( &rSI, rInf.GetIdx(), nPorLen, + nMaxComp, m_nBreakWidth, nMaxSizeDiff, + rInf.GetCachedVclData().get() ); + + // save maximum width for later use + if ( nMaxSizeDiff ) + rInf.SetMaxWidthDiff( &rPor, nMaxSizeDiff ); + + m_nBreakWidth += nItalic + nLeftRightBorderSpace; + } + else + m_nBreakWidth = 0; + + if (m_nBreakStart > rInf.GetIdx() + nPorLen + m_nFieldDiff) + { + rInf.GetTextSize(&rSI, rInf.GetIdx() + nPorLen, + m_nBreakStart - rInf.GetIdx() - nPorLen - m_nFieldDiff, nMaxComp, + m_nExtraBlankWidth, nMaxSizeDiff, rInf.GetCachedVclData().get()); + } + + if( m_pHanging ) + { + m_nBreakPos = m_nCutPos; + // Keep following SwBreakPortion in the same line. + if ( CH_BREAK == rInf.GetChar( m_nBreakPos + m_pHanging->GetLen() ) ) + return true; + } + + return false; +} + +// returns true if word at position nPos has a different spelling +// if hyphenated at this position (old german spelling) +bool SwTextGuess::AlternativeSpelling( const SwTextFormatInfo &rInf, + const TextFrameIndex nPos) +{ + // get word boundaries + Boundary aBound = g_pBreakIt->GetBreakIter()->getWordBoundary( + rInf.GetText(), sal_Int32(nPos), + g_pBreakIt->GetLocale( rInf.GetFont()->GetLanguage() ), + WordType::DICTIONARY_WORD, true ); + m_nBreakStart = TextFrameIndex(aBound.startPos); + sal_Int32 nWordLen = aBound.endPos - sal_Int32(m_nBreakStart); + + // if everything else fails, we want to cut at nPos + m_nCutPos = nPos; + + OUString const aText( rInf.GetText().copy(sal_Int32(m_nBreakStart), nWordLen) ); + + // check, if word has alternative spelling + Reference< XHyphenator > xHyph( ::GetHyphenator() ); + OSL_ENSURE( xHyph.is(), "Hyphenator is missing"); + //! subtract 1 since the UNO-interface is 0 based + m_xHyphWord = xHyph->queryAlternativeSpelling( aText, + g_pBreakIt->GetLocale( rInf.GetFont()->GetLanguage() ), + sal::static_int_cast<sal_Int16>(sal_Int32(nPos - m_nBreakStart)), + rInf.GetHyphValues() ); + return m_xHyphWord.is() && m_xHyphWord->isAlternativeSpelling(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/guess.hxx b/sw/source/core/text/guess.hxx new file mode 100644 index 0000000000..5a7a9ac1cf --- /dev/null +++ b/sw/source/core/text/guess.hxx @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once +#include <memory> + +#include "porrst.hxx" + +#include <com/sun/star/uno/Reference.hxx> + +namespace com::sun::star::linguistic2 { class XHyphenatedWord; } + +class SwTextFormatInfo; + +class SwTextGuess +{ + css::uno::Reference< css::linguistic2::XHyphenatedWord > m_xHyphWord; + std::unique_ptr<SwHangingPortion> m_pHanging; // for hanging punctuation + TextFrameIndex m_nCutPos; // this character doesn't fit + TextFrameIndex m_nBreakStart; // start index of word containing line break + TextFrameIndex m_nBreakPos; // start index of break position + TextFrameIndex m_nFieldDiff; // absolute positions can be wrong if we + // a field in the text has been expanded + sal_uInt16 m_nBreakWidth; // width of the broken portion + sal_uInt16 m_nExtraBlankWidth; // width of spaces after the break +public: + SwTextGuess(): m_nCutPos(0), m_nBreakStart(0), + m_nBreakPos(0), m_nFieldDiff(0), m_nBreakWidth(0), m_nExtraBlankWidth(0) + { } + + // true, if current portion still fits to current line + bool Guess( const SwTextPortion& rPor, SwTextFormatInfo &rInf, + const sal_uInt16 nHeight, sal_Int32 nSpacesInLine = 0 ); + bool AlternativeSpelling( const SwTextFormatInfo &rInf, const TextFrameIndex nPos ); + + SwHangingPortion* GetHangingPortion() const { return m_pHanging.get(); } + SwHangingPortion* ReleaseHangingPortion() { return m_pHanging.release(); } + sal_uInt16 BreakWidth() const { return m_nBreakWidth; } + sal_uInt16 ExtraBlankWidth() const { return m_nExtraBlankWidth; } + TextFrameIndex CutPos() const { return m_nCutPos; } + TextFrameIndex BreakStart() const { return m_nBreakStart; } + TextFrameIndex BreakPos() const {return m_nBreakPos; } + TextFrameIndex FieldDiff() const {return m_nFieldDiff; } + const css::uno::Reference< css::linguistic2::XHyphenatedWord >& HyphWord() const + { return m_xHyphWord; } +}; + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/inftxt.cxx b/sw/source/core/text/inftxt.cxx new file mode 100644 index 0000000000..3d9121ef1e --- /dev/null +++ b/sw/source/core/text/inftxt.cxx @@ -0,0 +1,2150 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <com/sun/star/linguistic2/XHyphenator.hpp> + +#include <unotools/linguprops.hxx> +#include <unotools/lingucfg.hxx> +#include <hintids.hxx> +#include <svl/ctloptions.hxx> +#include <sfx2/infobar.hxx> +#include <sfx2/printer.hxx> +#include <sal/log.hxx> +#include <editeng/hyphenzoneitem.hxx> +#include <editeng/hngpnctitem.hxx> +#include <editeng/scriptspaceitem.hxx> +#include <editeng/splwrap.hxx> +#include <editeng/pgrditem.hxx> +#include <editeng/tstpitem.hxx> +#include <editeng/shaditem.hxx> + +#include <SwSmartTagMgr.hxx> +#include <breakit.hxx> +#include <editeng/forbiddenruleitem.hxx> +#include <swmodule.hxx> +#include <vcl/svapp.hxx> +#include <viewsh.hxx> +#include <viewopt.hxx> +#include <frmtool.hxx> +#include <IDocumentSettingAccess.hxx> +#include <IDocumentDeviceAccess.hxx> +#include <IDocumentMarkAccess.hxx> +#include <paratr.hxx> +#include <rootfrm.hxx> +#include "inftxt.hxx" +#include <noteurl.hxx> +#include "porftn.hxx" +#include "porrst.hxx" +#include "itratr.hxx" +#include "portab.hxx" +#include <wrong.hxx> +#include <doc.hxx> +#include <pam.hxx> +#include <numrule.hxx> +#include <EnhancedPDFExportHelper.hxx> +#include <docsh.hxx> +#include <strings.hrc> +#include <o3tl/deleter.hxx> +#include <vcl/gdimtf.hxx> +#include <vcl/virdev.hxx> +#include <vcl/gradient.hxx> +#include <i18nlangtag/mslangid.hxx> +#include <formatlinebreak.hxx> + +#include <view.hxx> +#include <wrtsh.hxx> +#include <com/sun/star/text/XTextRange.hpp> +#include <unotextrange.hxx> +#include <SwStyleNameMapper.hxx> +#include <unoprnms.hxx> +#include <editeng/unoprnms.hxx> +#include <unomap.hxx> +#include <com/sun/star/awt/FontSlant.hpp> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::linguistic2; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::beans; + +#define CHAR_UNDERSCORE u'_' +#define CHAR_LEFT_ARROW u'\x25C0' +#define CHAR_RIGHT_ARROW u'\x25B6' +#define CHAR_TAB u'\x2192' +#define CHAR_TAB_RTL u'\x2190' +#define CHAR_LINEBREAK u'\x21B5' +#define CHAR_LINEBREAK_RTL u'\x21B3' + +#define DRAW_SPECIAL_OPTIONS_CENTER 1 +#define DRAW_SPECIAL_OPTIONS_ROTATE 2 + +SwLineInfo::SwLineInfo() + : m_pSpace( nullptr ), + m_nVertAlign( SvxParaVertAlignItem::Align::Automatic ), + m_nDefTabStop( 0 ), + m_bListTabStopIncluded( false ), + m_nListTabStopPosition( 0 ) +{ +} + +SwLineInfo::~SwLineInfo() +{ +} + +void SwLineInfo::CtorInitLineInfo( const SwAttrSet& rAttrSet, + const SwTextNode& rTextNode ) +{ + m_oRuler.emplace( rAttrSet.GetTabStops() ); + if ( rTextNode.GetListTabStopPosition( m_nListTabStopPosition ) ) + { + m_bListTabStopIncluded = true; + + // insert the list tab stop into SvxTabItem instance <pRuler> + const SvxTabStop aListTabStop( m_nListTabStopPosition, + SvxTabAdjust::Left ); + m_oRuler->Insert( aListTabStop ); + + // remove default tab stops, which are before the inserted list tab stop + for ( sal_uInt16 i = 0; i < m_oRuler->Count(); i++ ) + { + if ( (*m_oRuler)[i].GetTabPos() < m_nListTabStopPosition && + (*m_oRuler)[i].GetAdjustment() == SvxTabAdjust::Default ) + { + m_oRuler->Remove(i); + continue; + } + } + } + + if ( !rTextNode.getIDocumentSettingAccess()->get(DocumentSettingId::TABS_RELATIVE_TO_INDENT) ) + { + // remove default tab stop at position 0 + for ( sal_uInt16 i = 0; i < m_oRuler->Count(); i++ ) + { + if ( (*m_oRuler)[i].GetTabPos() == 0 && + (*m_oRuler)[i].GetAdjustment() == SvxTabAdjust::Default ) + { + m_oRuler->Remove(i); + break; + } + } + } + + m_pSpace = &rAttrSet.GetLineSpacing(); + m_nVertAlign = rAttrSet.GetParaVertAlign().GetValue(); + m_nDefTabStop = USHRT_MAX; +} + +void SwTextInfo::CtorInitTextInfo( SwTextFrame *pFrame ) +{ + m_pPara = pFrame->GetPara(); + m_nTextStart = pFrame->GetOffset(); + if (!m_pPara) + { + SAL_WARN("sw.core", "+SwTextInfo::CTOR: missing paragraph information"); + pFrame->Format(pFrame->getRootFrame()->GetCurrShell()->GetOut()); + m_pPara = pFrame->GetPara(); + } +} + +SwTextInfo::SwTextInfo( const SwTextInfo &rInf ) + : m_pPara( const_cast<SwTextInfo&>(rInf).GetParaPortion() ) + , m_nTextStart( rInf.GetTextStart() ) +{ } + +#if OSL_DEBUG_LEVEL > 0 + +static void ChkOutDev( const SwTextSizeInfo &rInf ) +{ + if ( !rInf.GetVsh() ) + return; + + const OutputDevice* pOut = rInf.GetOut(); + const OutputDevice* pRef = rInf.GetRefDev(); + OSL_ENSURE( pOut && pRef, "ChkOutDev: invalid output devices" ); +} +#endif + +static TextFrameIndex GetMinLen( const SwTextSizeInfo &rInf ) +{ + const TextFrameIndex nTextLen(rInf.GetText().getLength()); + if (rInf.GetLen() == TextFrameIndex(COMPLETE_STRING)) + return nTextLen; + const TextFrameIndex nInfLen = rInf.GetIdx() + rInf.GetLen(); + return std::min(nTextLen, nInfLen); +} + +SwTextSizeInfo::SwTextSizeInfo() +: m_pKanaComp(nullptr) +, m_pVsh(nullptr) +, m_pOut(nullptr) +, m_pRef(nullptr) +, m_pFnt(nullptr) +, m_pUnderFnt(nullptr) +, m_pFrame(nullptr) +, m_pOpt(nullptr) +, m_pText(nullptr) +, m_nIdx(0) +, m_nLen(0) +, m_nMeasureLen(COMPLETE_STRING) +, m_nKanaIdx(0) +, m_bOnWin (false) +, m_bNotEOL (false) +, m_bURLNotify(false) +, m_bStopUnderflow(false) +, m_bFootnoteInside(false) +, m_bOtherThanFootnoteInside(false) +, m_bMulti(false) +, m_bFirstMulti(false) +, m_bRuby(false) +, m_bHanging(false) +, m_bScriptSpace(false) +, m_bForbiddenChars(false) +, m_bSnapToGrid(false) +, m_nDirection(0) +{} + +SwTextSizeInfo::SwTextSizeInfo( const SwTextSizeInfo &rNew ) + : SwTextInfo( rNew ), + m_pKanaComp(rNew.GetpKanaComp()), + m_pVsh(const_cast<SwTextSizeInfo&>(rNew).GetVsh()), + m_pOut(const_cast<SwTextSizeInfo&>(rNew).GetOut()), + m_pRef(const_cast<SwTextSizeInfo&>(rNew).GetRefDev()), + m_pFnt(const_cast<SwTextSizeInfo&>(rNew).GetFont()), + m_pUnderFnt(rNew.GetUnderFnt()), + m_pFrame(rNew.m_pFrame), + m_pOpt(&rNew.GetOpt()), + m_pText(&rNew.GetText()), + m_nIdx(rNew.GetIdx()), + m_nLen(rNew.GetLen()), + m_nMeasureLen(rNew.GetMeasureLen()), + m_nKanaIdx( rNew.GetKanaIdx() ), + m_bOnWin( rNew.OnWin() ), + m_bNotEOL( rNew.NotEOL() ), + m_bURLNotify( rNew.URLNotify() ), + m_bStopUnderflow( rNew.StopUnderflow() ), + m_bFootnoteInside( rNew.IsFootnoteInside() ), + m_bOtherThanFootnoteInside( rNew.IsOtherThanFootnoteInside() ), + m_bMulti( rNew.IsMulti() ), + m_bFirstMulti( rNew.IsFirstMulti() ), + m_bRuby( rNew.IsRuby() ), + m_bHanging( rNew.IsHanging() ), + m_bScriptSpace( rNew.HasScriptSpace() ), + m_bForbiddenChars( rNew.HasForbiddenChars() ), + m_bSnapToGrid( rNew.SnapToGrid() ), + m_nDirection( rNew.GetDirection() ) +{ +#if OSL_DEBUG_LEVEL > 0 + ChkOutDev( *this ); +#endif +} + +void SwTextSizeInfo::CtorInitTextSizeInfo( OutputDevice* pRenderContext, SwTextFrame *pFrame, + TextFrameIndex const nNewIdx) +{ + m_pKanaComp = nullptr; + m_nKanaIdx = 0; + m_pFrame = pFrame; + CtorInitTextInfo( m_pFrame ); + SwDoc const& rDoc(m_pFrame->GetDoc()); + m_pVsh = m_pFrame->getRootFrame()->GetCurrShell(); + + // Get the output and reference device + if ( m_pVsh ) + { + m_pOut = pRenderContext; + m_pRef = &m_pVsh->GetRefDev(); + m_bOnWin = m_pVsh->GetWin() || OUTDEV_WINDOW == m_pOut->GetOutDevType() || m_pVsh->isOutputToWindow(); + } + else + { + // Access via StarONE. We do not need a Shell or an active one. + if (rDoc.getIDocumentSettingAccess().get(DocumentSettingId::HTML_MODE)) + { + // We can only pick the AppWin here? (there's nothing better to pick?) + m_pOut = Application::GetDefaultDevice(); + } + else + m_pOut = rDoc.getIDocumentDeviceAccess().getPrinter(false); + + m_pRef = m_pOut; + } + +#if OSL_DEBUG_LEVEL > 0 + ChkOutDev( *this ); +#endif + + // Set default layout mode ( LTR or RTL ). + if ( m_pFrame->IsRightToLeft() ) + { + m_pOut->SetLayoutMode( vcl::text::ComplexTextLayoutFlags::BiDiStrong | vcl::text::ComplexTextLayoutFlags::BiDiRtl ); + m_pRef->SetLayoutMode( vcl::text::ComplexTextLayoutFlags::BiDiStrong | vcl::text::ComplexTextLayoutFlags::BiDiRtl ); + m_nDirection = DIR_RIGHT2LEFT; + } + else + { + m_pOut->SetLayoutMode( vcl::text::ComplexTextLayoutFlags::BiDiStrong ); + m_pRef->SetLayoutMode( vcl::text::ComplexTextLayoutFlags::BiDiStrong ); + m_nDirection = DIR_LEFT2RIGHT; + } + + // The Options + + m_pOpt = m_pVsh ? + m_pVsh->GetViewOptions() : + SW_MOD()->GetViewOption(rDoc.getIDocumentSettingAccess().get(DocumentSettingId::HTML_MODE)); // Options from Module, due to StarONE + + // bURLNotify is set if MakeGraphic prepares it + // TODO: Unwind + m_bURLNotify = pNoteURL && !m_bOnWin; + + SetSnapToGrid( m_pFrame->GetTextNodeForParaProps()->GetSwAttrSet().GetParaGrid().GetValue() && + m_pFrame->IsInDocBody() ); + + m_pFnt = nullptr; + m_pUnderFnt = nullptr; + m_pText = &m_pFrame->GetText(); + + m_nIdx = nNewIdx; + m_nLen = m_nMeasureLen = TextFrameIndex(COMPLETE_STRING); + m_bNotEOL = false; + m_bStopUnderflow = m_bFootnoteInside = m_bOtherThanFootnoteInside = false; + m_bMulti = m_bFirstMulti = m_bRuby = m_bHanging = m_bScriptSpace = + m_bForbiddenChars = false; + + SetLen( GetMinLen( *this ) ); +} + +SwTextSizeInfo::SwTextSizeInfo( const SwTextSizeInfo &rNew, const OUString* pText, + TextFrameIndex const nIndex) + : SwTextInfo( rNew ), + m_pKanaComp(rNew.GetpKanaComp()), + m_pVsh(const_cast<SwTextSizeInfo&>(rNew).GetVsh()), + m_pOut(const_cast<SwTextSizeInfo&>(rNew).GetOut()), + m_pRef(const_cast<SwTextSizeInfo&>(rNew).GetRefDev()), + m_pFnt(const_cast<SwTextSizeInfo&>(rNew).GetFont()), + m_pUnderFnt(rNew.GetUnderFnt()), + m_pFrame( rNew.m_pFrame ), + m_pOpt(&rNew.GetOpt()), + m_pText(pText), + m_nIdx(nIndex), + m_nLen(COMPLETE_STRING), + m_nMeasureLen(COMPLETE_STRING), + m_nKanaIdx( rNew.GetKanaIdx() ), + m_bOnWin( rNew.OnWin() ), + m_bNotEOL( rNew.NotEOL() ), + m_bURLNotify( rNew.URLNotify() ), + m_bStopUnderflow( rNew.StopUnderflow() ), + m_bFootnoteInside( rNew.IsFootnoteInside() ), + m_bOtherThanFootnoteInside( rNew.IsOtherThanFootnoteInside() ), + m_bMulti( rNew.IsMulti() ), + m_bFirstMulti( rNew.IsFirstMulti() ), + m_bRuby( rNew.IsRuby() ), + m_bHanging( rNew.IsHanging() ), + m_bScriptSpace( rNew.HasScriptSpace() ), + m_bForbiddenChars( rNew.HasForbiddenChars() ), + m_bSnapToGrid( rNew.SnapToGrid() ), + m_nDirection( rNew.GetDirection() ) +{ +#if OSL_DEBUG_LEVEL > 0 + ChkOutDev( *this ); +#endif + SetLen( GetMinLen( *this ) ); +} + +SwTextSizeInfo::SwTextSizeInfo(SwTextFrame *const pTextFrame, + TextFrameIndex const nIndex) + : m_bOnWin(false) +{ + CtorInitTextSizeInfo( pTextFrame->getRootFrame()->GetCurrShell()->GetOut(), pTextFrame, nIndex ); +} + +void SwTextSizeInfo::SelectFont() +{ + // The path needs to go via ChgPhysFnt or the FontMetricCache gets confused. + // In this case pLastMet has it's old value. + // Wrong: GetOut()->SetFont( GetFont()->GetFnt() ); + GetFont()->Invalidate(); + GetFont()->ChgPhysFnt( m_pVsh, *GetOut() ); +} + +void SwTextSizeInfo::NoteAnimation() const +{ + if( OnWin() ) + SwRootFrame::FlushVout(); + + OSL_ENSURE( m_pOut == m_pVsh->GetOut(), + "SwTextSizeInfo::NoteAnimation() changed m_pOut" ); +} + +SwPosSize SwTextSizeInfo::GetTextSize( OutputDevice* pOutDev, + const SwScriptInfo* pSI, + const OUString& rText, + const TextFrameIndex nIndex, + const TextFrameIndex nLength) const +{ + SwDrawTextInfo aDrawInf( m_pVsh, *pOutDev, pSI, rText, nIndex, nLength ); + aDrawInf.SetFrame( m_pFrame ); + aDrawInf.SetFont( m_pFnt ); + aDrawInf.SetSnapToGrid( SnapToGrid() ); + aDrawInf.SetKanaComp( 0 ); + return SwPosSize(m_pFnt->GetTextSize_( aDrawInf )); +} + +SwPosSize SwTextSizeInfo::GetTextSize() const +{ + const SwScriptInfo& rSI = + const_cast<SwParaPortion*>(GetParaPortion())->GetScriptInfo(); + + // in some cases, compression is not allowed or suppressed for + // performance reasons + sal_uInt16 nComp =( SwFontScript::CJK == GetFont()->GetActual() && + rSI.CountCompChg() && + ! IsMulti() ) ? + GetKanaComp() : + 0 ; + + SwDrawTextInfo aDrawInf( m_pVsh, *m_pOut, &rSI, *m_pText, m_nIdx, m_nLen ); + aDrawInf.SetMeasureLen( m_nMeasureLen ); + aDrawInf.SetFrame( m_pFrame ); + aDrawInf.SetFont( m_pFnt ); + aDrawInf.SetSnapToGrid( SnapToGrid() ); + aDrawInf.SetKanaComp( nComp ); + return SwPosSize(m_pFnt->GetTextSize_( aDrawInf )); +} + +void SwTextSizeInfo::GetTextSize( const SwScriptInfo* pSI, const TextFrameIndex nIndex, + const TextFrameIndex nLength, const sal_uInt16 nComp, + sal_uInt16& nMinSize, sal_uInt16& nMaxSizeDiff, + vcl::text::TextLayoutCache const*const pCache) const +{ + SwDrawTextInfo aDrawInf( m_pVsh, *m_pOut, pSI, *m_pText, nIndex, nLength, + 0, false, pCache); + aDrawInf.SetFrame( m_pFrame ); + aDrawInf.SetFont( m_pFnt ); + aDrawInf.SetSnapToGrid( SnapToGrid() ); + aDrawInf.SetKanaComp( nComp ); + SwPosSize aSize( m_pFnt->GetTextSize_( aDrawInf ) ); + nMaxSizeDiff = o3tl::narrowing<sal_uInt16>(aDrawInf.GetKanaDiff()); + nMinSize = aSize.Width(); +} + +TextFrameIndex SwTextSizeInfo::GetTextBreak( const tools::Long nLineWidth, + const TextFrameIndex nMaxLen, + const sal_uInt16 nComp, + vcl::text::TextLayoutCache const*const pCache) const +{ + const SwScriptInfo& rScriptInfo = + const_cast<SwParaPortion*>(GetParaPortion())->GetScriptInfo(); + + OSL_ENSURE( m_pRef == m_pOut, "GetTextBreak is supposed to use the RefDev" ); + SwDrawTextInfo aDrawInf( m_pVsh, *m_pOut, &rScriptInfo, + *m_pText, GetIdx(), nMaxLen, 0, false, pCache ); + aDrawInf.SetFrame( m_pFrame ); + aDrawInf.SetFont( m_pFnt ); + aDrawInf.SetSnapToGrid( SnapToGrid() ); + aDrawInf.SetKanaComp( nComp ); + aDrawInf.SetHyphPos( nullptr ); + + return m_pFnt->GetTextBreak( aDrawInf, nLineWidth ); +} + +TextFrameIndex SwTextSizeInfo::GetTextBreak( const tools::Long nLineWidth, + const TextFrameIndex nMaxLen, + const sal_uInt16 nComp, + TextFrameIndex& rExtraCharPos, + vcl::text::TextLayoutCache const*const pCache) const +{ + const SwScriptInfo& rScriptInfo = + const_cast<SwParaPortion*>(GetParaPortion())->GetScriptInfo(); + + OSL_ENSURE( m_pRef == m_pOut, "GetTextBreak is supposed to use the RefDev" ); + SwDrawTextInfo aDrawInf( m_pVsh, *m_pOut, &rScriptInfo, + *m_pText, GetIdx(), nMaxLen, 0, false, pCache ); + aDrawInf.SetFrame( m_pFrame ); + aDrawInf.SetFont( m_pFnt ); + aDrawInf.SetSnapToGrid( SnapToGrid() ); + aDrawInf.SetKanaComp( nComp ); + aDrawInf.SetHyphPos( &rExtraCharPos ); + + return m_pFnt->GetTextBreak( aDrawInf, nLineWidth ); +} + +bool SwTextSizeInfo::HasHint(TextFrameIndex const nPos) const +{ + std::pair<SwTextNode const*, sal_Int32> const pos(m_pFrame->MapViewToModel(nPos)); + return pos.first->GetTextAttrForCharAt(pos.second); +} + +void SwTextPaintInfo::CtorInitTextPaintInfo( OutputDevice* pRenderContext, SwTextFrame *pFrame, const SwRect &rPaint ) +{ + CtorInitTextSizeInfo( pRenderContext, pFrame, TextFrameIndex(0) ); + m_aTextFly.CtorInitTextFly( pFrame ); + m_aPaintRect = rPaint; + m_nSpaceIdx = 0; + m_pSpaceAdd = nullptr; + m_pWrongList = nullptr; + m_pGrammarCheckList = nullptr; + m_pSmartTags = nullptr; + m_pBrushItem = nullptr; +} + +SwTextPaintInfo::SwTextPaintInfo( const SwTextPaintInfo &rInf, const OUString* pText ) + : SwTextSizeInfo( rInf, pText ) + , m_pWrongList( rInf.GetpWrongList() ) + , m_pGrammarCheckList( rInf.GetGrammarCheckList() ) + , m_pSmartTags( rInf.GetSmartTags() ) + , m_pSpaceAdd( rInf.GetpSpaceAdd() ), + m_pBrushItem( rInf.GetBrushItem() ), + m_aTextFly( rInf.GetTextFly() ), + m_aPos( rInf.GetPos() ), + m_aPaintRect( rInf.GetPaintRect() ), + m_nSpaceIdx( rInf.GetSpaceIdx() ) +{ } + +SwTextPaintInfo::SwTextPaintInfo( const SwTextPaintInfo &rInf ) + : SwTextSizeInfo( rInf ) + , m_pWrongList( rInf.GetpWrongList() ) + , m_pGrammarCheckList( rInf.GetGrammarCheckList() ) + , m_pSmartTags( rInf.GetSmartTags() ) + , m_pSpaceAdd( rInf.GetpSpaceAdd() ), + m_pBrushItem( rInf.GetBrushItem() ), + m_aTextFly( rInf.GetTextFly() ), + m_aPos( rInf.GetPos() ), + m_aPaintRect( rInf.GetPaintRect() ), + m_nSpaceIdx( rInf.GetSpaceIdx() ) +{ } + +SwTextPaintInfo::SwTextPaintInfo( SwTextFrame *pFrame, const SwRect &rPaint ) +{ + CtorInitTextPaintInfo( pFrame->getRootFrame()->GetCurrShell()->GetOut(), pFrame, rPaint ); +} + +namespace +{ +/** + * Context class that captures the draw operations on rDrawInf's output device for transparency + * purposes. + */ +class SwTransparentTextGuard +{ + ScopedVclPtrInstance<VirtualDevice> m_aContentVDev; + GDIMetaFile m_aContentMetafile; + MapMode m_aNewMapMode; + SwRect m_aPorRect; + SwTextPaintInfo& m_rPaintInf; + SwDrawTextInfo& m_rDrawInf; + +public: + SwTransparentTextGuard(const SwLinePortion& rPor, SwTextPaintInfo& rPaintInf, + SwDrawTextInfo& rDrawInf); + ~SwTransparentTextGuard(); +}; + +SwTransparentTextGuard::SwTransparentTextGuard(const SwLinePortion& rPor, + SwTextPaintInfo& rPaintInf, SwDrawTextInfo& rDrawInf) + : m_aNewMapMode(rPaintInf.GetOut()->GetMapMode()) + , m_rPaintInf(rPaintInf) + , m_rDrawInf(rDrawInf) +{ + rPaintInf.CalcRect(rPor, &m_aPorRect); + rDrawInf.SetOut(*m_aContentVDev); + m_aContentVDev->SetMapMode(rPaintInf.GetOut()->GetMapMode()); + m_aContentMetafile.Record(m_aContentVDev.get()); + m_aContentVDev->SetLineColor(rPaintInf.GetOut()->GetLineColor()); + m_aContentVDev->SetFillColor(rPaintInf.GetOut()->GetFillColor()); + m_aContentVDev->SetFont(rPaintInf.GetOut()->GetFont()); + m_aContentVDev->SetDrawMode(rPaintInf.GetOut()->GetDrawMode()); + m_aContentVDev->SetSettings(rPaintInf.GetOut()->GetSettings()); + m_aContentVDev->SetRefPoint(rPaintInf.GetOut()->GetRefPoint()); +} + +SwTransparentTextGuard::~SwTransparentTextGuard() +{ + m_aContentMetafile.Stop(); + m_aContentMetafile.WindStart(); + m_aNewMapMode.SetOrigin(m_aPorRect.TopLeft()); + m_aContentMetafile.SetPrefMapMode(m_aNewMapMode); + m_aContentMetafile.SetPrefSize(m_aPorRect.SSize()); + m_rDrawInf.SetOut(*m_rPaintInf.GetOut()); + Gradient aVCLGradient; + sal_uInt8 nTransPercentVcl = 255 - m_rPaintInf.GetFont()->GetColor().GetAlpha(); + const Color aTransColor(nTransPercentVcl, nTransPercentVcl, nTransPercentVcl); + aVCLGradient.SetStyle(css::awt::GradientStyle_LINEAR); + aVCLGradient.SetStartColor(aTransColor); + aVCLGradient.SetEndColor(aTransColor); + aVCLGradient.SetAngle(0_deg10); + aVCLGradient.SetBorder(0); + aVCLGradient.SetOfsX(0); + aVCLGradient.SetOfsY(0); + aVCLGradient.SetStartIntensity(100); + aVCLGradient.SetEndIntensity(100); + aVCLGradient.SetSteps(2); + m_rPaintInf.GetOut()->DrawTransparent(m_aContentMetafile, m_aPorRect.TopLeft(), + m_aPorRect.SSize(), aVCLGradient); +} +} + +void SwTextPaintInfo::DrawText_( const OUString &rText, const SwLinePortion &rPor, + TextFrameIndex const nStart, TextFrameIndex const nLength, + const bool bKern, const bool bWrong, + const bool bSmartTag, + const bool bGrammarCheck ) +{ + if( !nLength ) + return; + + // The SwScriptInfo is useless if we are inside a field portion + SwScriptInfo* pSI = nullptr; + if ( ! rPor.InFieldGrp() ) + pSI = &GetParaPortion()->GetScriptInfo(); + + // in some cases, kana compression is not allowed or suppressed for + // performance reasons + sal_uInt16 nComp = 0; + if ( ! IsMulti() ) + nComp = GetKanaComp(); + + bool bCfgIsAutoGrammar = false; + SvtLinguConfig().GetProperty( UPN_IS_GRAMMAR_AUTO ) >>= bCfgIsAutoGrammar; + const bool bBullet = OnWin() && GetOpt().IsBlank() && IsNoSymbol(); + const bool bTmpWrong = bWrong && OnWin() && GetOpt().IsOnlineSpell(); + const bool bTmpGrammarCheck = bGrammarCheck && OnWin() && bCfgIsAutoGrammar && GetOpt().IsOnlineSpell(); + const bool bTmpSmart = bSmartTag && OnWin() && !GetOpt().IsPagePreview() && SwSmartTagMgr::Get().IsSmartTagsEnabled(); + + OSL_ENSURE( GetParaPortion(), "No paragraph!"); + SwDrawTextInfo aDrawInf( m_pFrame->getRootFrame()->GetCurrShell(), *m_pOut, pSI, rText, nStart, nLength, + rPor.Width(), bBullet ); + + aDrawInf.SetUnderFnt( m_pUnderFnt ); + + const tools::Long nSpaceAdd = ( rPor.IsBlankPortion() || rPor.IsDropPortion() || + rPor.InNumberGrp() ) ? 0 : GetSpaceAdd(/*bShrink=*/true); + if ( nSpaceAdd ) + { + TextFrameIndex nCharCnt(0); + // #i41860# Thai justified alignment needs some + // additional information: + aDrawInf.SetNumberOfBlanks( rPor.InTextGrp() ? + static_cast<const SwTextPortion&>(rPor).GetSpaceCnt( *this, nCharCnt ) : + TextFrameIndex(0) ); + } + + aDrawInf.SetSpace( nSpaceAdd ); + aDrawInf.SetKanaComp( nComp ); + + // the font is used to identify the current script via nActual + aDrawInf.SetFont( m_pFnt ); + // the frame is used to identify the orientation + aDrawInf.SetFrame( GetTextFrame() ); + // we have to know if the paragraph should snap to grid + aDrawInf.SetSnapToGrid( SnapToGrid() ); + // for underlining we must know when not to add extra space behind + // a character in justified mode + aDrawInf.SetSpaceStop( ! rPor.GetNextPortion() || + rPor.GetNextPortion()->InFixMargGrp() || + rPor.GetNextPortion()->IsHolePortion() ); + + // Draw text next to the left border + Point aFontPos(m_aPos); + if( m_pFnt->GetLeftBorder() && rPor.InTextGrp() && !static_cast<const SwTextPortion&>(rPor).GetJoinBorderWithPrev() ) + { + const sal_uInt16 nLeftBorderSpace = m_pFnt->GetLeftBorderSpace(); + if ( GetTextFrame()->IsRightToLeft() ) + { + aFontPos.AdjustX( -nLeftBorderSpace ); + } + else + { + switch( m_pFnt->GetOrientation(GetTextFrame()->IsVertical()).get() ) + { + case 0 : + aFontPos.AdjustX(nLeftBorderSpace ); + break; + case 900 : + aFontPos.AdjustY( -nLeftBorderSpace ); + break; + case 1800 : + aFontPos.AdjustX( -nLeftBorderSpace ); + break; + case 2700 : + aFontPos.AdjustY(nLeftBorderSpace ); + break; + } + } + if( aFontPos.X() < 0 ) + aFontPos.setX( 0 ); + if( aFontPos.Y() < 0 ) + aFontPos.setY( 0 ); + } + + // Handle semi-transparent text if necessary. + std::unique_ptr<SwTransparentTextGuard, o3tl::default_delete<SwTransparentTextGuard>> pTransparentText; + if (m_pFnt->GetColor() != COL_AUTO && m_pFnt->GetColor().IsTransparent()) + { + // if drawing to a backend that supports transparency for text color, then we don't need to use this + if (!m_bOnWin || !m_pOut->SupportsOperation(OutDevSupportType::TransparentText) || m_pOut->GetConnectMetaFile()) + pTransparentText.reset(new SwTransparentTextGuard(rPor, *this, aDrawInf)); + } + + if( GetTextFly().IsOn() ) + { + // aPos needs to be the TopLeft, because we cannot calculate the + // ClipRects otherwise + const Point aPoint( aFontPos.X(), aFontPos.Y() - rPor.GetAscent() ); + const Size aSize( rPor.Width(), rPor.Height() ); + aDrawInf.SetPos( aPoint ); + aDrawInf.SetSize( aSize ); + aDrawInf.SetAscent( rPor.GetAscent() ); + aDrawInf.SetKern( bKern ? rPor.Width() : 0 ); + aDrawInf.SetWrong( bTmpWrong ? m_pWrongList : nullptr ); + aDrawInf.SetGrammarCheck( bTmpGrammarCheck ? m_pGrammarCheckList : nullptr ); + aDrawInf.SetSmartTags( bTmpSmart ? m_pSmartTags : nullptr ); + GetTextFly().DrawTextOpaque( aDrawInf ); + } + else + { + aDrawInf.SetPos( aFontPos ); + if( bKern ) + m_pFnt->DrawStretchText_( aDrawInf ); + else + { + aDrawInf.SetWrong( bTmpWrong ? m_pWrongList : nullptr ); + aDrawInf.SetGrammarCheck( bTmpGrammarCheck ? m_pGrammarCheckList : nullptr ); + aDrawInf.SetSmartTags( bTmpSmart ? m_pSmartTags : nullptr ); + m_pFnt->DrawText_( aDrawInf ); + } + } +} + +void SwTextPaintInfo::CalcRect( const SwLinePortion& rPor, + SwRect* pRect, SwRect* pIntersect, + const bool bInsideBox ) const +{ + Size aSize( rPor.Width(), rPor.Height() ); + if( rPor.IsHangingPortion() ) + aSize.setWidth( static_cast<const SwHangingPortion&>(rPor).GetInnerWidth() ); + if( rPor.InSpaceGrp() && GetSpaceAdd() ) + { + SwTwips nAdd = rPor.CalcSpacing( GetSpaceAdd(), *this ); + if( rPor.InFieldGrp() && GetSpaceAdd() < 0 && nAdd ) + nAdd += GetSpaceAdd() / SPACING_PRECISION_FACTOR; + aSize.AdjustWidth(nAdd ); + } + + Point aPoint; + + if( IsRotated() ) + { + tools::Long nTmp = aSize.Width(); + aSize.setWidth( aSize.Height() ); + aSize.setHeight( nTmp ); + if ( 1 == GetDirection() ) + { + aPoint.setX( X() - rPor.GetAscent() ); + aPoint.setY( Y() - aSize.Height() ); + } + else + { + aPoint.setX( X() - rPor.Height() + rPor.GetAscent() ); + aPoint.setY( Y() ); + } + } + else + { + aPoint.setX( X() ); + if (GetTextFrame()->IsVertLR() && !GetTextFrame()->IsVertLRBT()) + aPoint.setY( Y() - rPor.Height() + rPor.GetAscent() ); + else + aPoint.setY( Y() - rPor.GetAscent() ); + } + + // Adjust x coordinate if we are inside a bidi portion + const bool bFrameDir = GetTextFrame()->IsRightToLeft(); + const bool bCounterDir = ( !bFrameDir && DIR_RIGHT2LEFT == GetDirection() ) || + ( bFrameDir && DIR_LEFT2RIGHT == GetDirection() ); + + if ( bCounterDir ) + aPoint.AdjustX( -(aSize.Width()) ); + + SwRect aRect( aPoint, aSize ); + + if ( GetTextFrame()->IsRightToLeft() ) + GetTextFrame()->SwitchLTRtoRTL( aRect ); + + if ( GetTextFrame()->IsVertical() ) + GetTextFrame()->SwitchHorizontalToVertical( aRect ); + + if( bInsideBox && rPor.InTextGrp() ) + { + const bool bJoinWithPrev = + static_cast<const SwTextPortion&>(rPor).GetJoinBorderWithPrev(); + const bool bJoinWithNext = + static_cast<const SwTextPortion&>(rPor).GetJoinBorderWithNext(); + const bool bIsVert = GetTextFrame()->IsVertical(); + const bool bIsVertLRBT = GetTextFrame()->IsVertLRBT(); + aRect.AddTop( GetFont()->CalcShadowSpace(SvxShadowItemSide::TOP, bIsVert, bIsVertLRBT, + bJoinWithPrev, bJoinWithNext)); + aRect.AddBottom( - GetFont()->CalcShadowSpace(SvxShadowItemSide::BOTTOM, bIsVert, bIsVertLRBT, + bJoinWithPrev, bJoinWithNext)); + aRect.AddLeft( GetFont()->CalcShadowSpace(SvxShadowItemSide::LEFT, bIsVert, bIsVertLRBT, + bJoinWithPrev, bJoinWithNext)); + aRect.AddRight( - GetFont()->CalcShadowSpace(SvxShadowItemSide::RIGHT, bIsVert, bIsVertLRBT, + bJoinWithPrev, bJoinWithNext)); + } + + if ( pRect ) + *pRect = aRect; + + if( aRect.HasArea() && pIntersect ) + { + ::SwAlignRect( aRect, GetVsh(), GetOut() ); + + if ( GetOut()->IsClipRegion() ) + { + SwRect aClip( GetOut()->GetClipRegion().GetBoundRect() ); + aRect.Intersection( aClip ); + } + + *pIntersect = aRect; + } +} + +/** + * Draws a special portion + * E.g.: line break portion, tab portion + * + * @param rPor The portion + * @param rRect The rectangle surrounding the character + * @param rCol Specify a color for the character + * @param bCenter Draw the character centered, otherwise left aligned + * @param bRotate Rotate the character if character rotation is set + */ +static void lcl_DrawSpecial( const SwTextPaintInfo& rTextPaintInfo, const SwLinePortion& rPor, + SwRect& rRect, const Color& rCol, sal_Unicode cChar, + sal_uInt8 nOptions ) +{ + bool bCenter = 0 != ( nOptions & DRAW_SPECIAL_OPTIONS_CENTER ); + bool bRotate = 0 != ( nOptions & DRAW_SPECIAL_OPTIONS_ROTATE ); + + // rRect is given in absolute coordinates + if ( rTextPaintInfo.GetTextFrame()->IsRightToLeft() ) + rTextPaintInfo.GetTextFrame()->SwitchRTLtoLTR( rRect ); + if ( rTextPaintInfo.GetTextFrame()->IsVertical() ) + rTextPaintInfo.GetTextFrame()->SwitchVerticalToHorizontal( rRect ); + + const SwFont* pOldFnt = rTextPaintInfo.GetFont(); + + // Font is generated only once: + static SwFont s_aFnt = [&]() + { + SwFont tmp( *pOldFnt ); + tmp.SetFamily( FAMILY_DONTKNOW, tmp.GetActual() ); + tmp.SetName( numfunc::GetDefBulletFontname(), tmp.GetActual() ); + tmp.SetStyleName(OUString(), tmp.GetActual()); + tmp.SetCharSet( RTL_TEXTENCODING_SYMBOL, tmp.GetActual() ); + return tmp; + }(); + + // Some of the current values are set at the font: + if ( ! bRotate ) + s_aFnt.SetVertical( 0_deg10, rTextPaintInfo.GetTextFrame()->IsVertical() ); + else + s_aFnt.SetVertical( pOldFnt->GetOrientation() ); + + s_aFnt.SetColor(rCol); + + Size aFontSize( 0, SPECIAL_FONT_HEIGHT ); + s_aFnt.SetSize( aFontSize, s_aFnt.GetActual() ); + + SwTextPaintInfo& rNonConstTextPaintInfo = const_cast<SwTextPaintInfo&>(rTextPaintInfo); + + rNonConstTextPaintInfo.SetFont( &s_aFnt ); + + // The maximum width depends on the current orientation + const Degree10 nDir = s_aFnt.GetOrientation( rTextPaintInfo.GetTextFrame()->IsVertical() ); + SwTwips nMaxWidth; + if (nDir == 900_deg10 || nDir == 2700_deg10) + nMaxWidth = rRect.Height(); + else + { + assert(nDir == 0_deg10); //Unknown direction set at font + nMaxWidth = rRect.Width(); + } + + // check if char fits into rectangle + const OUString aTmp( cChar ); + aFontSize = rTextPaintInfo.GetTextSize( aTmp ).SvLSize(); + while ( aFontSize.Width() > nMaxWidth ) + { + SwTwips nFactor = ( 100 * aFontSize.Width() ) / nMaxWidth; + const SwTwips nOldWidth = aFontSize.Width(); + + // new height for font + const SwFontScript nAct = s_aFnt.GetActual(); + aFontSize.setHeight( ( 100 * s_aFnt.GetSize( nAct ).Height() ) / nFactor ); + aFontSize.setWidth( ( 100 * s_aFnt.GetSize( nAct).Width() ) / nFactor ); + + if ( !aFontSize.Width() && !aFontSize.Height() ) + break; + + s_aFnt.SetSize( aFontSize, nAct ); + + aFontSize = rTextPaintInfo.GetTextSize( aTmp ).SvLSize(); + + if ( aFontSize.Width() >= nOldWidth ) + break; + } + + const Point aOldPos( rTextPaintInfo.GetPos() ); + + // adjust values so that tab is vertically and horizontally centered + SwTwips nX = rRect.Left(); + SwTwips nY = rRect.Top(); + switch ( nDir.get() ) + { + case 0 : + if ( bCenter ) + nX += ( rRect.Width() - aFontSize.Width() ) / 2; + nY += ( rRect.Height() - aFontSize.Height() ) / 2 + rTextPaintInfo.GetAscent(); + break; + case 900 : + if ( bCenter ) + nX += ( rRect.Width() - aFontSize.Height() ) / 2 + rTextPaintInfo.GetAscent(); + nY += ( rRect.Height() + aFontSize.Width() ) / 2; + break; + case 2700 : + if ( bCenter ) + nX += ( rRect.Width() + aFontSize.Height() ) / 2 - rTextPaintInfo.GetAscent(); + nY += ( rRect.Height() - aFontSize.Width() ) / 2; + break; + } + + Point aTmpPos( nX, nY ); + rNonConstTextPaintInfo.SetPos( aTmpPos ); + sal_uInt16 nOldWidth = rPor.Width(); + const_cast<SwLinePortion&>(rPor).Width( o3tl::narrowing<sal_uInt16>(aFontSize.Width()) ); + rTextPaintInfo.DrawText( aTmp, rPor ); + const_cast<SwLinePortion&>(rPor).Width( nOldWidth ); + rNonConstTextPaintInfo.SetFont( const_cast<SwFont*>(pOldFnt) ); + rNonConstTextPaintInfo.SetPos( aOldPos ); +} + +void SwTextPaintInfo::DrawRect( const SwRect &rRect, bool bRetouche ) const +{ + if ( OnWin() || !bRetouche ) + { + if( m_aTextFly.IsOn() ) + const_cast<SwTextPaintInfo*>(this)->GetTextFly(). + DrawFlyRect( m_pOut, rRect ); + else + m_pOut->DrawRect( rRect.SVRect() ); + } +} + +void SwTextPaintInfo::DrawTab( const SwLinePortion &rPor ) const +{ + if( !OnWin() ) + return; + + SwRect aRect; + CalcRect( rPor, &aRect ); + + if ( ! aRect.HasArea() ) + return; + + const sal_Unicode cChar = GetTextFrame()->IsRightToLeft() ? CHAR_TAB_RTL : CHAR_TAB; + const sal_uInt8 nOptions = DRAW_SPECIAL_OPTIONS_CENTER | DRAW_SPECIAL_OPTIONS_ROTATE; + + lcl_DrawSpecial( *this, rPor, aRect, NON_PRINTING_CHARACTER_COLOR, cChar, nOptions ); +} + +void SwTextPaintInfo::DrawLineBreak( const SwLinePortion &rPor ) const +{ + if( !OnWin() ) + return; + + SwLineBreakClear eClear = SwLineBreakClear::NONE; + if (rPor.IsBreakPortion()) + { + const auto& rBreakPortion = static_cast<const SwBreakPortion&>(rPor); + eClear = rBreakPortion.GetClear(); + } + + sal_uInt16 nOldWidth = rPor.Width(); + const_cast<SwLinePortion&>(rPor).Width( LINE_BREAK_WIDTH ); + + SwRect aRect; + CalcRect( rPor, &aRect ); + + if( aRect.HasArea() ) + { + const sal_Unicode cChar = GetTextFrame()->IsRightToLeft() ? + CHAR_LINEBREAK_RTL : CHAR_LINEBREAK; + const sal_uInt8 nOptions = 0; + + SwRect aTextRect(aRect); + if (eClear == SwLineBreakClear::LEFT || eClear == SwLineBreakClear::ALL) + aTextRect.AddLeft(30); + if (eClear == SwLineBreakClear::RIGHT || eClear == SwLineBreakClear::ALL) + aTextRect.AddRight(-30); + lcl_DrawSpecial( *this, rPor, aTextRect, NON_PRINTING_CHARACTER_COLOR, cChar, nOptions ); + + if (eClear != SwLineBreakClear::NONE) + { + // Paint indicator if this clear is left/right/all. + m_pOut->Push(vcl::PushFlags::LINECOLOR); + m_pOut->SetLineColor(NON_PRINTING_CHARACTER_COLOR); + if (eClear != SwLineBreakClear::RIGHT) + m_pOut->DrawLine(aRect.BottomLeft(), aRect.TopLeft()); + if (eClear != SwLineBreakClear::LEFT) + m_pOut->DrawLine(aRect.BottomRight(), aRect.TopRight()); + m_pOut->Pop(); + } + } + + const_cast<SwLinePortion&>(rPor).Width( nOldWidth ); +} + +void SwTextPaintInfo::DrawRedArrow( const SwLinePortion &rPor ) const +{ + Size aSize( SPECIAL_FONT_HEIGHT, SPECIAL_FONT_HEIGHT ); + SwRect aRect( static_cast<const SwArrowPortion&>(rPor).GetPos(), aSize ); + sal_Unicode cChar; + if( static_cast<const SwArrowPortion&>(rPor).IsLeft() ) + { + aRect.Pos().AdjustY(20 - GetAscent() ); + aRect.Pos().AdjustX(20 ); + if( aSize.Height() > rPor.Height() ) + aRect.Height( rPor.Height() ); + cChar = CHAR_LEFT_ARROW; + } + else + { + if( aSize.Height() > rPor.Height() ) + aRect.Height( rPor.Height() ); + aRect.Pos().AdjustY( -(aRect.Height() + 20) ); + aRect.Pos().AdjustX( -(aRect.Width() + 20) ); + cChar = CHAR_RIGHT_ARROW; + } + + if ( GetTextFrame()->IsVertical() ) + GetTextFrame()->SwitchHorizontalToVertical( aRect ); + + if( aRect.HasArea() ) + { + const sal_uInt8 nOptions = 0; + lcl_DrawSpecial( *this, rPor, aRect, COL_LIGHTRED, cChar, nOptions ); + } +} + +void SwTextPaintInfo::DrawPostIts( bool bScript ) const +{ + if( !OnWin() || !m_pOpt->IsPostIts() ) + return; + + Size aSize; + Point aTmp; + + const sal_uInt16 nPostItsWidth = SwViewOption::GetPostItsWidth( GetOut() ); + const sal_uInt16 nFontHeight = m_pFnt->GetHeight( m_pVsh, *GetOut() ); + const sal_uInt16 nFontAscent = m_pFnt->GetAscent( m_pVsh, *GetOut() ); + + switch ( m_pFnt->GetOrientation( GetTextFrame()->IsVertical() ).get() ) + { + case 0 : + aSize.setWidth( nPostItsWidth ); + aSize.setHeight( nFontHeight ); + aTmp.setX( m_aPos.X() ); + aTmp.setY( m_aPos.Y() - nFontAscent ); + break; + case 900 : + aSize.setHeight( nPostItsWidth ); + aSize.setWidth( nFontHeight ); + aTmp.setX( m_aPos.X() - nFontAscent ); + aTmp.setY( m_aPos.Y() ); + break; + case 2700 : + aSize.setHeight( nPostItsWidth ); + aSize.setWidth( nFontHeight ); + aTmp.setX( m_aPos.X() - nFontHeight + + nFontAscent ); + aTmp.setY( m_aPos.Y() ); + break; + } + + SwRect aTmpRect( aTmp, aSize ); + + if ( GetTextFrame()->IsRightToLeft() ) + GetTextFrame()->SwitchLTRtoRTL( aTmpRect ); + + if ( GetTextFrame()->IsVertical() ) + GetTextFrame()->SwitchHorizontalToVertical( aTmpRect ); + + GetOpt().PaintPostIts( const_cast<OutputDevice*>(GetOut()), aTmpRect, bScript ); + +} + +void SwTextPaintInfo::DrawCheckBox(const SwFieldFormCheckboxPortion &rPor, bool bChecked) const +{ + SwRect aIntersect; + CalcRect( rPor, &aIntersect ); + if ( !aIntersect.HasArea() ) + return; + + if (OnWin() && GetOpt().IsFieldShadings() && + !GetOpt().IsPagePreview()) + { + OutputDevice* pOut = const_cast<OutputDevice*>(GetOut()); + pOut->Push( vcl::PushFlags::LINECOLOR | vcl::PushFlags::FILLCOLOR ); + if( m_pFnt->GetHighlightColor() != COL_TRANSPARENT ) + pOut->SetFillColor(m_pFnt->GetHighlightColor()); + else + pOut->SetFillColor(GetOpt().GetFieldShadingsColor()); + pOut->SetLineColor(); + pOut->DrawRect( aIntersect.SVRect() ); + pOut->Pop(); + } + const int delta = 25; + tools::Rectangle r(aIntersect.Left()+delta, aIntersect.Top()+delta, aIntersect.Right()-delta, aIntersect.Bottom()-delta); + m_pOut->Push( vcl::PushFlags::LINECOLOR | vcl::PushFlags::FILLCOLOR ); + m_pOut->SetLineColor( Color(0, 0, 0)); + m_pOut->SetFillColor(); + m_pOut->DrawRect( r ); + if (bChecked) + { + m_pOut->DrawLine(r.TopLeft(), r.BottomRight()); + m_pOut->DrawLine(r.TopRight(), r.BottomLeft()); + } + m_pOut->Pop(); +} + +void SwTextPaintInfo::DrawBackground( const SwLinePortion &rPor, const Color *pColor ) const +{ + OSL_ENSURE( OnWin(), "SwTextPaintInfo::DrawBackground: printer pollution ?" ); + + SwRect aIntersect; + CalcRect( rPor, nullptr, &aIntersect, true ); + + if ( !aIntersect.HasArea() ) + return; + + OutputDevice* pOut = const_cast<OutputDevice*>(GetOut()); + pOut->Push( vcl::PushFlags::LINECOLOR | vcl::PushFlags::FILLCOLOR ); + + if ( pColor ) + pOut->SetFillColor( *pColor ); + else + pOut->SetFillColor( GetOpt().GetFieldShadingsColor() ); + + pOut->SetLineColor(); + + DrawRect( aIntersect, true ); + pOut->Pop(); +} + +void SwTextPaintInfo::DrawBackBrush( const SwLinePortion &rPor ) const +{ + { + SwRect aIntersect; + CalcRect( rPor, &aIntersect, nullptr, true ); + if(aIntersect.HasArea()) + { + SwPosition const aPosition(m_pFrame->MapViewToModelPos(GetIdx())); + const ::sw::mark::IMark* pFieldmark = + m_pFrame->GetDoc().getIDocumentMarkAccess()->getInnerFieldmarkFor(aPosition); + bool bIsStartMark = (TextFrameIndex(1) == GetLen() + && CH_TXT_ATR_FIELDSTART == GetText()[sal_Int32(GetIdx())]); + if(pFieldmark) { + SAL_INFO("sw.core", "Found Fieldmark " << pFieldmark->ToString()); + } + if(bIsStartMark) + SAL_INFO("sw.core", "Found StartMark"); + if (OnWin() && (pFieldmark!=nullptr || bIsStartMark) && + GetOpt().IsFieldShadings() && + !GetOpt().IsPagePreview()) + { + OutputDevice* pOutDev = const_cast<OutputDevice*>(GetOut()); + pOutDev->Push( vcl::PushFlags::LINECOLOR | vcl::PushFlags::FILLCOLOR ); + pOutDev->SetFillColor( GetOpt().GetFieldShadingsColor() ); + pOutDev->SetLineColor( ); + pOutDev->DrawRect( aIntersect.SVRect() ); + pOutDev->Pop(); + } + } + } + + SwRect aIntersect; + CalcRect( rPor, nullptr, &aIntersect, true ); + + if ( !aIntersect.HasArea() ) + return; + + OutputDevice* pTmpOut = const_cast<OutputDevice*>(GetOut()); + + // #i16816# tagged pdf support + SwTaggedPDFHelper aTaggedPDFHelper( nullptr, nullptr, nullptr, *pTmpOut ); + + Color aFillColor; + + if( m_pFnt->GetHighlightColor() != COL_TRANSPARENT ) + { + aFillColor = m_pFnt->GetHighlightColor(); + } + else + { + if( !m_pFnt->GetBackColor() ) + return; + aFillColor = *m_pFnt->GetBackColor(); + } + + pTmpOut->Push( vcl::PushFlags::LINECOLOR | vcl::PushFlags::FILLCOLOR ); + + pTmpOut->SetFillColor(aFillColor); + pTmpOut->SetLineColor(); + + DrawRect( aIntersect, false ); + + pTmpOut->Pop(); +} + +void SwTextPaintInfo::DrawBorder( const SwLinePortion &rPor ) const +{ + SwRect aDrawArea; + CalcRect( rPor, &aDrawArea ); + if ( aDrawArea.HasArea() ) + { + PaintCharacterBorder(*m_pFnt, aDrawArea, GetTextFrame()->IsVertical(), + GetTextFrame()->IsVertLRBT(), rPor.GetJoinBorderWithPrev(), + rPor.GetJoinBorderWithNext()); + } +} + +namespace { + +bool HasValidPropertyValue(const uno::Any& rAny) +{ + if (bool bValue; rAny >>= bValue) + { + return true; + } + else if (OUString aValue; (rAny >>= aValue) && !(aValue.isEmpty())) + { + return true; + } + else if (awt::FontSlant eValue; rAny >>= eValue) + { + return true; + } + else if (tools::Long nValueLong; rAny >>= nValueLong) + { + return true; + } + else if (double fValue; rAny >>= fValue) + { + return true; + } + else if (short nValueShort; rAny >>= nValueShort) + { + return true; + } + else + return false; +} +} + +void SwTextPaintInfo::DrawCSDFHighlighting(const SwLinePortion &rPor) const +{ + // Don't use GetActiveView() as it does not work as expected when there are multiple open + // documents. + SwView* pView = SwTextFrame::GetView(); + if (!pView) + return; + + StylesHighlighterColorMap& rCharStylesColorMap = pView->GetStylesHighlighterCharColorMap(); + + if (rCharStylesColorMap.empty() && !pView->IsHighlightCharDF()) + return; + + SwRect aRect; + CalcRect(rPor, &aRect, nullptr, true); + if(!aRect.HasArea()) + return; + + SwTextFrame* pFrame = const_cast<SwTextFrame*>(GetTextFrame()); + if (!pFrame) + return; + + SwPosition aPosition(pFrame->MapViewToModelPos(GetIdx())); + SwPosition aMarkPosition(pFrame->MapViewToModelPos(GetIdx() + GetLen())); + + rtl::Reference<SwXTextRange> xRange( + SwXTextRange::CreateXTextRange(pFrame->GetDoc(), aPosition, &aMarkPosition)); + + OUString sCurrentCharStyle; + xRange->getPropertyValue("CharStyleName") >>= sCurrentCharStyle; + + std::optional<OUString> sCSNumberOrDF; // CS number or "df" or not used + std::optional<Color> aFillColor; + + // check for CS formatting, if not CS formatted check for direct character formatting + if (!sCurrentCharStyle.isEmpty()) + { + if (!rCharStylesColorMap.empty()) + { + OUString sCharStyleDisplayName; + sCharStyleDisplayName = SwStyleNameMapper::GetUIName(sCurrentCharStyle, + SwGetPoolIdFromName::ChrFmt); + if (!sCharStyleDisplayName.isEmpty() + && rCharStylesColorMap.find(sCharStyleDisplayName) + != rCharStylesColorMap.end()) + { + aFillColor = rCharStylesColorMap[sCharStyleDisplayName].first; + sCSNumberOrDF = OUString::number(rCharStylesColorMap[sCharStyleDisplayName].second); + } + } + } + // not character style formatted + else if (pView->IsHighlightCharDF()) + { + const std::vector<OUString> aHiddenProperties{ UNO_NAME_RSID, + UNO_NAME_PARA_IS_NUMBERING_RESTART, + UNO_NAME_PARA_STYLE_NAME, + UNO_NAME_PARA_CONDITIONAL_STYLE_NAME, + UNO_NAME_PAGE_STYLE_NAME, + UNO_NAME_NUMBERING_START_VALUE, + UNO_NAME_NUMBERING_IS_NUMBER, + UNO_NAME_PARA_CONTINUEING_PREVIOUS_SUB_TREE, + UNO_NAME_CHAR_STYLE_NAME, + UNO_NAME_NUMBERING_LEVEL, + UNO_NAME_SORTED_TEXT_ID, + UNO_NAME_PARRSID, + UNO_NAME_CHAR_COLOR_THEME, + UNO_NAME_CHAR_COLOR_TINT_OR_SHADE }; + + SfxItemPropertySet const& rPropSet( + *aSwMapProvider.GetPropertySet(PROPERTY_MAP_CHAR_AUTO_STYLE)); + SfxItemPropertyMap const& rMap(rPropSet.getPropertyMap()); + + + const uno::Sequence<beans::Property> aProperties + = xRange->getPropertySetInfo()->getProperties(); + + for (const beans::Property& rProperty : aProperties) + { + const OUString& rPropName = rProperty.Name; + + if (!rMap.hasPropertyByName(rPropName)) + continue; + + if (std::find(aHiddenProperties.begin(), aHiddenProperties.end(), rPropName) + != aHiddenProperties.end()) + continue; + + if (xRange->getPropertyState(rPropName) == beans::PropertyState_DIRECT_VALUE) + { + const uno::Any aAny = xRange->getPropertyValue(rPropName); + if (HasValidPropertyValue(aAny)) + { + sCSNumberOrDF = SwResId(STR_CHARACTER_DIRECT_FORMATTING_TAG); + aFillColor = COL_LIGHTGRAY; + break; + } + } + } + } + if (sCSNumberOrDF) + { + OutputDevice* pTmpOut = const_cast<OutputDevice*>(GetOut()); + pTmpOut->Push(vcl::PushFlags::LINECOLOR | vcl::PushFlags::FILLCOLOR + | vcl::PushFlags::TEXTLAYOUTMODE | vcl::PushFlags::FONT); + + // draw a filled rectangle at the formatted CS or DF text + pTmpOut->SetFillColor(aFillColor.value()); + pTmpOut->SetLineColor(aFillColor.value()); + tools::Rectangle aSVRect(aRect.SVRect()); + pTmpOut->DrawRect(aSVRect); + + // calculate size and position for the CS number or "df" text and rectangle + tools::Long nWidth = pTmpOut->GetTextWidth(sCSNumberOrDF.value()); + tools::Long nHeight = pTmpOut->GetTextHeight(); + aSVRect.SetSize(Size(nWidth, nHeight)); + aSVRect.Move(-(nWidth / 1.5), -(nHeight / 1.5)); + + vcl::Font aFont(pTmpOut->GetFont()); + aFont.SetOrientation(Degree10(0)); + pTmpOut->SetFont(aFont); + + pTmpOut->SetLayoutMode(vcl::text::ComplexTextLayoutFlags::TextOriginLeft); + //pTmpOut->SetLayoutMode(vcl::text::ComplexTextLayoutFlags::BiDiStrong); + + pTmpOut->SetTextFillColor(aFillColor.value()); + pTmpOut->DrawText(aSVRect, sCSNumberOrDF.value(), DrawTextFlags::NONE); + + pTmpOut->Pop(); + } +} + +void SwTextPaintInfo::DrawViewOpt( const SwLinePortion &rPor, + PortionType nWhich, const Color *pColor ) const +{ + if( !OnWin() || IsMulti() ) + return; + + bool bDraw = false; + switch( nWhich ) + { + case PortionType::Footnote: + case PortionType::QuoVadis: + case PortionType::Number: + case PortionType::Field: + case PortionType::Hidden: + case PortionType::Tox: + case PortionType::Ref: + case PortionType::Meta: + case PortionType::ContentControl: + case PortionType::ControlChar: + if ( !GetOpt().IsPagePreview() + && !GetOpt().IsReadonly() + && GetOpt().IsFieldShadings() + && ( PortionType::Number != nWhich + || m_pFrame->GetTextNodeForParaProps()->HasMarkedLabel())) // #i27615# + { + bDraw = PortionType::Footnote != nWhich || m_pFrame->IsFootnoteAllowed(); + } + bDraw &= GetOpt().IsHardBlank(); + break; + case PortionType::Bookmark: + // no shading + break; + case PortionType::InputField: + // input field shading also in read-only mode + if ( !GetOpt().IsPagePreview() + && GetOpt().IsFieldShadings() ) + { + bDraw = true; + } + break; + case PortionType::Tab: + if ( GetOpt().IsTab() ) bDraw = true; + break; + case PortionType::SoftHyphen: + if ( GetOpt().IsSoftHyph() )bDraw = true; + break; + case PortionType::Blank: + if ( GetOpt().IsHardBlank())bDraw = true; + break; + default: + { + OSL_ENSURE( false, "SwTextPaintInfo::DrawViewOpt: don't know how to draw this" ); + break; + } + } + if ( bDraw ) + DrawBackground( rPor, pColor ); +} + +static void lcl_InitHyphValues( PropertyValues &rVals, + sal_Int16 nMinLeading, sal_Int16 nMinTrailing, + bool bNoCapsHyphenation, bool bNoLastWordHyphenation, + sal_Int16 nMinWordLength, sal_Int16 nTextHyphZone ) +{ + sal_Int32 nLen = rVals.getLength(); + + if (0 == nLen) // yet to be initialized? + { + rVals.realloc( 6 ); + PropertyValue *pVal = rVals.getArray(); + + pVal[0].Name = UPN_HYPH_MIN_LEADING; + pVal[0].Handle = UPH_HYPH_MIN_LEADING; + pVal[0].Value <<= nMinLeading; + + pVal[1].Name = UPN_HYPH_MIN_TRAILING; + pVal[1].Handle = UPH_HYPH_MIN_TRAILING; + pVal[1].Value <<= nMinTrailing; + + pVal[2].Name = UPN_HYPH_NO_CAPS; + pVal[2].Handle = UPH_HYPH_NO_CAPS; + pVal[2].Value <<= bNoCapsHyphenation; + + pVal[3].Name = UPN_HYPH_NO_LAST_WORD; + pVal[3].Handle = UPH_HYPH_NO_LAST_WORD; + pVal[3].Value <<= bNoLastWordHyphenation; + + pVal[4].Name = UPN_HYPH_MIN_WORD_LENGTH; + pVal[4].Handle = UPH_HYPH_MIN_WORD_LENGTH; + pVal[4].Value <<= nMinWordLength; + + pVal[5].Name = UPN_HYPH_ZONE; + pVal[5].Handle = UPH_HYPH_ZONE; + pVal[5].Value <<= nTextHyphZone; + } + else if (6 == nLen) // already initialized once? + { + PropertyValue *pVal = rVals.getArray(); + pVal[0].Value <<= nMinLeading; + pVal[1].Value <<= nMinTrailing; + pVal[2].Value <<= bNoCapsHyphenation; + pVal[3].Value <<= bNoLastWordHyphenation; + pVal[4].Value <<= nMinWordLength; + pVal[5].Value <<= nTextHyphZone; + } + else { + OSL_FAIL( "unexpected size of sequence" ); + } +} + +const PropertyValues & SwTextFormatInfo::GetHyphValues() const +{ + OSL_ENSURE( 6 == m_aHyphVals.getLength(), + "hyphenation values not yet initialized" ); + return m_aHyphVals; +} + +bool SwTextFormatInfo::InitHyph( const bool bAutoHyphen ) +{ + const SwAttrSet& rAttrSet = GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet(); + SetHanging( rAttrSet.GetHangingPunctuation().GetValue() ); + SetScriptSpace( rAttrSet.GetScriptSpace().GetValue() ); + SetForbiddenChars( rAttrSet.GetForbiddenRule().GetValue() ); + const SvxHyphenZoneItem &rAttr = rAttrSet.GetHyphenZone(); + MaxHyph() = rAttr.GetMaxHyphens(); + const bool bAuto = bAutoHyphen || rAttr.IsHyphen(); + if( bAuto || m_bInterHyph ) + { + const sal_Int16 nMinimalLeading = std::max(rAttr.GetMinLead(), sal_uInt8(2)); + const sal_Int16 nMinimalTrailing = rAttr.GetMinTrail(); + const sal_Int16 nMinimalWordLength = rAttr.GetMinWordLength(); + const bool bNoCapsHyphenation = rAttr.IsNoCapsHyphenation(); + const bool bNoLastWordHyphenation = rAttr.IsNoLastWordHyphenation(); + const sal_Int16 nTextHyphZone = rAttr.GetTextHyphenZone(); + lcl_InitHyphValues( m_aHyphVals, nMinimalLeading, nMinimalTrailing, + bNoCapsHyphenation, bNoLastWordHyphenation, + nMinimalWordLength, nTextHyphZone ); + } + return bAuto; +} + +void SwTextFormatInfo::CtorInitTextFormatInfo( OutputDevice* pRenderContext, SwTextFrame *pNewFrame, const bool bNewInterHyph, + const bool bNewQuick, const bool bTst ) +{ + CtorInitTextPaintInfo( pRenderContext, pNewFrame, SwRect() ); + + m_bQuick = bNewQuick; + m_bInterHyph = bNewInterHyph; + + //! needs to be done in this order + m_bAutoHyph = InitHyph(); + + m_bIgnoreFly = false; + m_bFakeLineStart = false; + m_bShift = false; + m_bDropInit = false; + m_bTestFormat = bTst; + m_nLeft = 0; + m_nRight = 0; + m_nFirst = 0; + m_nRealWidth = 0; + m_nForcedLeftMargin = 0; + m_pRest = nullptr; + m_nLineHeight = 0; + m_nLineNetHeight = 0; + SetLineStart(TextFrameIndex(0)); + + SvtCTLOptions::TextNumerals const nTextNumerals( + SvtCTLOptions::GetCTLTextNumerals()); + // cannot cache for NUMERALS_CONTEXT because we need to know the string + // for the whole paragraph now + if (nTextNumerals != SvtCTLOptions::NUMERALS_CONTEXT) + { + // set digit mode to what will be used later to get same results + SwDigitModeModifier const m(*m_pRef, LANGUAGE_NONE /*dummy*/); + assert(m_pRef->GetDigitLanguage() != LANGUAGE_NONE); + SetCachedVclData(OutputDevice::CreateTextLayoutCache(*m_pText)); + } + + Init(); +} + +/** + * If the Hyphenator returns ERROR or the language is set to NOLANGUAGE + * we do not hyphenate. + * Else, we always hyphenate if we do interactive hyphenation. + * If we do not do interactive hyphenation, we only hyphenate if ParaFormat is + * set to automatic hyphenation. + */ +bool SwTextFormatInfo::IsHyphenate() const +{ + if( !m_bInterHyph && !m_bAutoHyph ) + return false; + + LanguageType eTmp = GetFont()->GetLanguage(); + // TODO: check for more ideographic langs w/o hyphenation as a concept + if ( LANGUAGE_DONTKNOW == eTmp || LANGUAGE_NONE == eTmp + || !MsLangId::usesHyphenation(eTmp) ) + return false; + + uno::Reference< XHyphenator > xHyph = ::GetHyphenator(); + if (!xHyph.is()) + return false; + + if (m_bInterHyph) + SvxSpellWrapper::CheckHyphLang( xHyph, eTmp ); + + if (!xHyph->hasLocale(g_pBreakIt->GetLocale(eTmp))) + { + SfxObjectShell* pShell = m_pFrame->GetDoc().GetDocShell(); + if (pShell) + { + pShell->AppendInfoBarWhenReady( + "hyphenationmissing", SwResId(STR_HYPH_MISSING), + SwResId(STR_HYPH_MISSING_DETAIL) + .replaceFirst("%1", LanguageTag::convertToBcp47( g_pBreakIt->GetLocale(eTmp))), + InfobarType::WARNING); + } + } + + return xHyph->hasLocale( g_pBreakIt->GetLocale(eTmp) ); +} + +const SwFormatDrop *SwTextFormatInfo::GetDropFormat() const +{ + const SwFormatDrop *pDrop = &GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet().GetDrop(); + if( 1 >= pDrop->GetLines() || + ( !pDrop->GetChars() && !pDrop->GetWholeWord() ) ) + pDrop = nullptr; + return pDrop; +} + +void SwTextFormatInfo::Init() +{ + // Not initialized: pRest, nLeft, nRight, nFirst, nRealWidth + X(0); + m_bArrowDone = m_bFull = m_bFootnoteDone = m_bErgoDone = m_bNumDone = m_bNoEndHyph = + m_bNoMidHyph = m_bStop = m_bNewLine = m_bUnderflow = m_bTabOverflow = false; + + // generally we do not allow number portions in follows, except... + if ( GetTextFrame()->IsFollow() ) + { + const SwTextFrame* pMaster = GetTextFrame()->FindMaster(); + OSL_ENSURE(pMaster, "pTextFrame without Master"); + const SwLinePortion* pTmpPara = pMaster ? pMaster->GetPara() : nullptr; + + // there is a master for this follow and the master does not have + // any contents (especially it does not have a number portion) + m_bNumDone = ! pTmpPara || + ! static_cast<const SwParaPortion*>(pTmpPara)->GetFirstPortion()->IsFlyPortion(); + } + + m_pRoot = nullptr; + m_pLast = nullptr; + m_pFly = nullptr; + m_pLastTab = nullptr; + m_pUnderflow = nullptr; + m_cTabDecimal = 0; + m_nWidth = m_nRealWidth; + m_nForcedLeftMargin = 0; + m_nSoftHyphPos = TextFrameIndex(0); + m_nUnderScorePos = TextFrameIndex(COMPLETE_STRING); + m_nLastBookmarkPos = TextFrameIndex(-1); + m_cHookChar = 0; + SetIdx(TextFrameIndex(0)); + SetLen(TextFrameIndex(GetText().getLength())); + SetPaintOfst(0); +} + +SwTextFormatInfo::SwTextFormatInfo(OutputDevice* pRenderContext, SwTextFrame *pFrame, const bool bInterHyphL, + const bool bQuickL, const bool bTst) +{ + CtorInitTextFormatInfo(pRenderContext, pFrame, bInterHyphL, bQuickL, bTst); +} + +/** + * There are a few differences between a copy constructor + * and the following constructor for multi-line formatting. + * The root is the first line inside the multi-portion, + * the line start is the actual position in the text, + * the line width is the rest width from the surrounding line + * and the bMulti and bFirstMulti-flag has to be set correctly. + */ +SwTextFormatInfo::SwTextFormatInfo( const SwTextFormatInfo& rInf, + SwLineLayout& rLay, SwTwips nActWidth ) : + SwTextPaintInfo( rInf ), + m_pRoot(&rLay), + m_pLast(&rLay), + m_pFly(nullptr), + m_pUnderflow(nullptr), + m_pRest(nullptr), + m_pLastTab(nullptr), + m_nSoftHyphPos(TextFrameIndex(0)), + m_nLineStart(rInf.GetIdx()), + m_nUnderScorePos(TextFrameIndex(COMPLETE_STRING)), + m_nLeft(rInf.m_nLeft), + m_nRight(rInf.m_nRight), + m_nFirst(rInf.m_nLeft), + m_nRealWidth(sal_uInt16(nActWidth)), + m_nWidth(m_nRealWidth), + m_nLineHeight(0), + m_nLineNetHeight(0), + m_nForcedLeftMargin(0), + m_bFull(false), + m_bFootnoteDone(true), + m_bErgoDone(true), + m_bNumDone(true), + m_bArrowDone(true), + m_bStop(false), + m_bNewLine(true), + m_bShift(false), + m_bUnderflow(false), + m_bInterHyph(false), + m_bAutoHyph(false), + m_bDropInit(false), + m_bQuick(rInf.m_bQuick), + m_bNoEndHyph(false), + m_bNoMidHyph(false), + m_bIgnoreFly(false), + m_bFakeLineStart(false), + m_bTabOverflow( false ), + m_bTestFormat(rInf.m_bTestFormat), + m_cTabDecimal(0), + m_cHookChar(0), + m_nMaxHyph(0) +{ + SetMulti( true ); + SetFirstMulti( rInf.IsFirstMulti() ); +} + +void SwTextFormatInfo::UpdateTabSeen(PortionType type) +{ + switch (type) + { + case PortionType::TabLeft: + m_eLastTabsSeen = TabSeen::Left; + break; + case PortionType::TabRight: + m_eLastTabsSeen = TabSeen::Right; + break; + case PortionType::TabCenter: + m_eLastTabsSeen = TabSeen::Center; + break; + case PortionType::TabDecimal: + m_eLastTabsSeen = TabSeen::Decimal; + break; + case PortionType::Break: + m_eLastTabsSeen = TabSeen::None; + break; + default: + break; + } +} + +void SwTextFormatInfo::SetLast(SwLinePortion* pNewLast) +{ + m_pLast = pNewLast; + assert(pNewLast); // We never pass nullptr here. If we start, then a check is needed below. + UpdateTabSeen(pNewLast->GetWhichPor()); +} + +bool SwTextFormatInfo::CheckFootnotePortion_( SwLineLayout const * pCurr ) +{ + const sal_uInt16 nHeight = pCurr->GetRealHeight(); + for( SwLinePortion *pPor = pCurr->GetNextPortion(); pPor; pPor = pPor->GetNextPortion() ) + { + if( pPor->IsFootnotePortion() && nHeight > static_cast<SwFootnotePortion*>(pPor)->Orig() ) + { + SetLineHeight( nHeight ); + SetLineNetHeight( pCurr->Height() ); + return true; + } + } + return false; +} + +TextFrameIndex SwTextFormatInfo::ScanPortionEnd(TextFrameIndex const nStart, + TextFrameIndex const nEnd) +{ + m_cHookChar = 0; + TextFrameIndex i = nStart; + + // Used for decimal tab handling: + const sal_Unicode cTabDec = GetLastTab() ? GetTabDecimal() : 0; + const sal_Unicode cThousandSep = ',' == cTabDec ? '.' : ','; + + // #i45951# German (Switzerland) uses ' as thousand separator + const sal_Unicode cThousandSep2 = ',' == cTabDec ? '.' : '\''; + + bool bNumFound = false; + const bool bTabCompat = GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::TAB_COMPAT); + + for( ; i < nEnd; ++i ) + { + const sal_Unicode cPos = GetChar( i ); + switch( cPos ) + { + case CH_TXTATR_BREAKWORD: + case CH_TXTATR_INWORD: + if( !HasHint( i )) + break; + [[fallthrough]]; + + case CHAR_SOFTHYPHEN: + case CHAR_HARDHYPHEN: + case CHAR_HARDBLANK: + case CH_TAB: + case CH_BREAK: + case CHAR_ZWSP : + case CHAR_WJ : + m_cHookChar = cPos; + return i; + + case CHAR_UNDERSCORE: + if (TextFrameIndex(COMPLETE_STRING) == m_nUnderScorePos) + m_nUnderScorePos = i; + break; + + default: + if ( cTabDec ) + { + if( cTabDec == cPos ) + { + OSL_ENSURE( cPos, "Unexpected end of string" ); + if( cPos ) // robust + { + m_cHookChar = cPos; + return i; + } + } + + // Compatibility: First non-digit character behind a + // a digit character becomes the hook character + if ( bTabCompat ) + { + if ( ( 0x2F < cPos && cPos < 0x3A ) || + ( bNumFound && ( cPos == cThousandSep || cPos == cThousandSep2 ) ) ) + { + bNumFound = true; + } + else + { + if ( bNumFound ) + { + m_cHookChar = cPos; + SetTabDecimal( cPos ); + return i; + } + } + } + } + } + } + + // Check if character *behind* the portion has + // to become the hook: + if (i == nEnd && i < TextFrameIndex(GetText().getLength()) && bNumFound) + { + const sal_Unicode cPos = GetChar( i ); + if ( cPos != cTabDec && cPos != cThousandSep && cPos !=cThousandSep2 && ( 0x2F >= cPos || cPos >= 0x3A ) ) + { + m_cHookChar = GetChar( i ); + SetTabDecimal( m_cHookChar ); + } + } + + return i; +} + +bool SwTextFormatInfo::LastKernPortion() +{ + if( GetLast() ) + { + if( GetLast()->IsKernPortion() ) + return true; + if( GetLast()->Width() || ( GetLast()->GetLen() && + !GetLast()->IsHolePortion() ) ) + return false; + } + SwLinePortion* pPor = GetRoot(); + SwLinePortion *pKern = nullptr; + while( pPor ) + { + if( pPor->IsKernPortion() ) + pKern = pPor; + else if( pPor->Width() || ( pPor->GetLen() && !pPor->IsHolePortion() ) ) + pKern = nullptr; + pPor = pPor->GetNextPortion(); + } + if( pKern ) + { + SetLast( pKern ); + return true; + } + return false; +} + +SwTwips SwTextFormatInfo::GetLineWidth() +{ + SwTwips nLineWidth = Width() - X(); + + const bool bTabOverMargin = GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::TAB_OVER_MARGIN); + const bool bTabOverSpacing = GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::TAB_OVER_SPACING); + if (!bTabOverMargin && !bTabOverSpacing) + return nLineWidth; + + SwTabPortion* pLastTab = GetLastTab(); + if (!pLastTab) + return nLineWidth; + + // Consider tab portions over the printing bounds of the text frame. + if (pLastTab->GetTabPos() <= Width()) + return nLineWidth; + + // Calculate the width that starts at the left (or in case of first line: + // first) margin, but ends after the right paragraph margin: + // + // +--------------------+ + // |LL| |RR| + // +--------------------+ + // ^ m_nLeftMargin (absolute) + // ^ nLeftMarginWidth (relative to m_nLeftMargin), X() is relative to this + // ^ right margin + // ^ paragraph right + // <--------------------> is GetTextFrame()->getFrameArea().Width() + // <--------------> is Width() + // <-----------------> is what we need to be able to compare to X() (nTextFrameWidth) + SwTwips nLeftMarginWidth = m_nLeftMargin - GetTextFrame()->getFrameArea().Left(); + SwTwips nTextFrameWidth = GetTextFrame()->getFrameArea().Width() - nLeftMarginWidth; + + // If there is one such tab portion, then text is allowed to use the full + // text frame area to the right (RR above, but not LL). + nLineWidth = nTextFrameWidth - X(); + + if (!bTabOverMargin) // thus bTabOverSpacing only + { + // right, center, decimal can back-fill all the available space - same as TabOverMargin + if (pLastTab->GetWhichPor() == PortionType::TabLeft) + nLineWidth = nTextFrameWidth - pLastTab->GetTabPos(); + } + return nLineWidth; +} + +SwTextSlot::SwTextSlot( + const SwTextSizeInfo *pNew, + const SwLinePortion *pPor, + bool bTextLen, + bool bExgLists, + OUString const & rCh ) + : pOldText(nullptr) + , m_pOldSmartTagList(nullptr) + , m_pOldGrammarCheckList(nullptr) + , nIdx(0) + , nLen(0) + , nMeasureLen(0) + , pInf(nullptr) +{ + if( rCh.isEmpty() ) + { + bOn = pPor->GetExpText( *pNew, aText ); + } + else + { + aText = rCh; + bOn = true; + } + + // The text is replaced ... + if( !bOn ) + return; + + pInf = const_cast<SwTextSizeInfo*>(pNew); + nIdx = pInf->GetIdx(); + nLen = pInf->GetLen(); + nMeasureLen = pInf->GetMeasureLen(); + pOldText = &(pInf->GetText()); + m_pOldCachedVclData = pInf->GetCachedVclData(); + pInf->SetText( aText ); + pInf->SetIdx(TextFrameIndex(0)); + pInf->SetLen(bTextLen ? TextFrameIndex(pInf->GetText().getLength()) : pPor->GetLen()); + if (nMeasureLen != TextFrameIndex(COMPLETE_STRING)) + pInf->SetMeasureLen(TextFrameIndex(COMPLETE_STRING)); + + pInf->SetCachedVclData(nullptr); + + // ST2 + if ( !bExgLists ) + return; + + m_pOldSmartTagList = static_cast<SwTextPaintInfo*>(pInf)->GetSmartTags(); + if (m_pOldSmartTagList) + { + std::pair<SwTextNode const*, sal_Int32> pos(pNew->GetTextFrame()->MapViewToModel(nIdx)); + SwWrongList const*const pSmartTags(pos.first->GetSmartTags()); + if (pSmartTags) + { + const sal_uInt16 nPos = pSmartTags->GetWrongPos(pos.second); + const sal_Int32 nListPos = pSmartTags->Pos(nPos); + if (nListPos == pos.second && pSmartTags->SubList(nPos) != nullptr) + { + m_pTempIter.reset(new sw::WrongListIterator(*pSmartTags->SubList(nPos))); + static_cast<SwTextPaintInfo*>(pInf)->SetSmartTags(m_pTempIter.get()); + } + else if (!m_pTempList && nPos < pSmartTags->Count() + && nListPos < pos.second && !aText.isEmpty()) + { + m_pTempList.reset(new SwWrongList( WRONGLIST_SMARTTAG )); + m_pTempList->Insert( OUString(), nullptr, 0, aText.getLength(), 0 ); + m_pTempIter.reset(new sw::WrongListIterator(*m_pTempList)); + static_cast<SwTextPaintInfo*>(pInf)->SetSmartTags(m_pTempIter.get()); + } + else + static_cast<SwTextPaintInfo*>(pInf)->SetSmartTags(nullptr); + } + else + static_cast<SwTextPaintInfo*>(pInf)->SetSmartTags(nullptr); + } + m_pOldGrammarCheckList = static_cast<SwTextPaintInfo*>(pInf)->GetGrammarCheckList(); + if (!m_pOldGrammarCheckList) + return; + + std::pair<SwTextNode const*, sal_Int32> pos(pNew->GetTextFrame()->MapViewToModel(nIdx)); + SwWrongList const*const pGrammar(pos.first->GetGrammarCheck()); + if (pGrammar) + { + const sal_uInt16 nPos = pGrammar->GetWrongPos(pos.second); + const sal_Int32 nListPos = pGrammar->Pos(nPos); + if (nListPos == pos.second && pGrammar->SubList(nPos) != nullptr) + { + m_pTempIter.reset(new sw::WrongListIterator(*pGrammar->SubList(nPos))); + static_cast<SwTextPaintInfo*>(pInf)->SetGrammarCheckList(m_pTempIter.get()); + } + else if (!m_pTempList && nPos < pGrammar->Count() + && nListPos < pos.second && !aText.isEmpty()) + { + m_pTempList.reset(new SwWrongList( WRONGLIST_GRAMMAR )); + m_pTempList->Insert( OUString(), nullptr, 0, aText.getLength(), 0 ); + m_pTempIter.reset(new sw::WrongListIterator(*m_pTempList)); + static_cast<SwTextPaintInfo*>(pInf)->SetGrammarCheckList(m_pTempIter.get()); + } + else + static_cast<SwTextPaintInfo*>(pInf)->SetGrammarCheckList(nullptr); + } + else + static_cast<SwTextPaintInfo*>(pInf)->SetGrammarCheckList(nullptr); +} + +SwTextSlot::~SwTextSlot() +{ + if( !bOn ) + return; + + pInf->SetCachedVclData(m_pOldCachedVclData); + pInf->SetText( *pOldText ); + pInf->SetIdx( nIdx ); + pInf->SetLen( nLen ); + pInf->SetMeasureLen( nMeasureLen ); + + // ST2 + // Restore old smart tag list + if (m_pOldSmartTagList) + static_cast<SwTextPaintInfo*>(pInf)->SetSmartTags(m_pOldSmartTagList); + if (m_pOldGrammarCheckList) + static_cast<SwTextPaintInfo*>(pInf)->SetGrammarCheckList(m_pOldGrammarCheckList); +} + +SwFontSave::SwFontSave(const SwTextSizeInfo &rInf, SwFont *pNew, + SwAttrIter* pItr) + : pInf(nullptr) + , pFnt(pNew ? const_cast<SwTextSizeInfo&>(rInf).GetFont() : nullptr) + , pIter(nullptr) +{ + if( !pFnt ) + return; + + pInf = &const_cast<SwTextSizeInfo&>(rInf); + // In these cases we temporarily switch to the new font: + // 1. the fonts have a different magic number + // 2. they have different script types + // 3. their background colors differ (this is not covered by 1.) + if( pFnt->DifferentFontCacheId( pNew, pFnt->GetActual() ) || + pNew->GetActual() != pFnt->GetActual() || + ( ! pNew->GetBackColor() && pFnt->GetBackColor() ) || + ( pNew->GetBackColor() && ! pFnt->GetBackColor() ) || + ( pNew->GetBackColor() && pFnt->GetBackColor() && + ( *pNew->GetBackColor() != *pFnt->GetBackColor() ) ) ) + { + pNew->SetTransparent( true ); + pNew->SetAlign( ALIGN_BASELINE ); + pInf->SetFont( pNew ); + } + else + pFnt = nullptr; + pNew->Invalidate(); + pNew->ChgPhysFnt( pInf->GetVsh(), *pInf->GetOut() ); + if( pItr && pItr->GetFnt() == pFnt ) + { + pIter = pItr; + pIter->SetFnt( pNew ); + } +} + +SwFontSave::~SwFontSave() +{ + if( pFnt ) + { + // Reset SwFont + pFnt->Invalidate(); + pInf->SetFont( pFnt ); + if( pIter ) + { + pIter->SetFnt( pFnt ); + pIter->m_nPosition = COMPLETE_STRING; + } + } +} + +bool SwTextFormatInfo::ChgHyph( const bool bNew ) +{ + const bool bOld = m_bAutoHyph; + if( m_bAutoHyph != bNew ) + { + m_bAutoHyph = bNew; + InitHyph( bNew ); + // Set language in the Hyphenator + if( m_pFnt ) + m_pFnt->ChgPhysFnt( m_pVsh, *m_pOut ); + } + return bOld; +} + + +bool SwTextFormatInfo::CheckCurrentPosBookmark() +{ + if (m_nLastBookmarkPos != GetIdx()) + { + m_nLastBookmarkPos = GetIdx(); + return true; + } + else + { + return false; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/inftxt.hxx b/sw/source/core/text/inftxt.hxx new file mode 100644 index 0000000000..2ddaee7b2e --- /dev/null +++ b/sw/source/core/text/inftxt.hxx @@ -0,0 +1,811 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <memory> +#include <optional> +#include <com/sun/star/beans/PropertyValues.hpp> + +#include <map> + +#include <swtypes.hxx> +#include <swrect.hxx> +#include <txtfly.hxx> +#include <swfont.hxx> +#include "porlay.hxx" +#include <txtfrm.hxx> +#include <ndtxt.hxx> +#include <editeng/paravertalignitem.hxx> + +namespace com::sun::star::linguistic2 { class XHyphenatedWord; } + +class SvxBrushItem; +class SvxLineSpacingItem; +class SvxTabStop; +class SvxTabStopItem; +class SwFlyPortion; +class SwFormatDrop; +class SwLinePortion; +class SwTabPortion; +class SwViewOption; +class SwViewShell; +class SwAttrIter; +struct SwMultiCreator; +class SwMultiPortion; +namespace sw { class WrongListIterator; } + +#define ARROW_WIDTH 200 +#define DIR_LEFT2RIGHT 0 +#define DIR_BOTTOM2TOP 1 +#define DIR_RIGHT2LEFT 2 +#define DIR_TOP2BOTTOM 3 + +// Respects the attribute LineSpace when calculating the Height/Ascent +class SwLineInfo +{ + friend class SwTextIter; + + std::optional<SvxTabStopItem> m_oRuler; + const SvxLineSpacingItem *m_pSpace; + SvxParaVertAlignItem::Align m_nVertAlign; + sal_uInt16 m_nDefTabStop; + bool m_bListTabStopIncluded; + tools::Long m_nListTabStopPosition; + + void CtorInitLineInfo( const SwAttrSet& rAttrSet, + const SwTextNode& rTextNode ); + + SwLineInfo(); + ~SwLineInfo(); +public: + // #i24363# tab stops relative to indent - returns the tab stop following nSearchPos or NULL + const SvxTabStop* GetTabStop(const SwTwips nSearchPos, SwTwips& nRight) const; + const SvxLineSpacingItem *GetLineSpacing() const { return m_pSpace; } + sal_uInt16 GetDefTabStop() const { return m_nDefTabStop; } + void SetDefTabStop( sal_uInt16 nNew ) const + { const_cast<SwLineInfo*>(this)->m_nDefTabStop = nNew; } + + // vertical alignment + SvxParaVertAlignItem::Align GetVertAlign() const { return m_nVertAlign; } + bool HasSpecialAlign( bool bVert ) const + { return bVert ? + ( SvxParaVertAlignItem::Align::Baseline != m_nVertAlign ) : + ( SvxParaVertAlignItem::Align::Baseline != m_nVertAlign && + SvxParaVertAlignItem::Align::Automatic != m_nVertAlign ); } + + sal_uInt16 NumberOfTabStops() const; + + bool IsListTabStopIncluded() const + { + return m_bListTabStopIncluded; + } + tools::Long GetListTabStopPosition() const + { + return m_nListTabStopPosition; + } +}; + +class SwTextInfo +{ + // Implementation in txthyph.cxx + friend void SetParaPortion( SwTextInfo *pInf, SwParaPortion *pRoot ); + SwParaPortion *m_pPara; + TextFrameIndex m_nTextStart; // TextOfst for Follows + +protected: + SwTextInfo() + : m_pPara(nullptr) + , m_nTextStart(0) + {} + +public: + void CtorInitTextInfo( SwTextFrame *pFrame ); + SwTextInfo( const SwTextInfo &rInf ); + explicit SwTextInfo( SwTextFrame *pFrame ) { CtorInitTextInfo( pFrame ); } + SwParaPortion *GetParaPortion() { return m_pPara; } + const SwParaPortion *GetParaPortion() const { return m_pPara; } + TextFrameIndex GetTextStart() const { return m_nTextStart; } +}; + +class SwTextSizeInfo : public SwTextInfo +{ +private: + typedef std::map< SwLinePortion const *, sal_uInt16 > SwTextPortionMap; + +protected: + // during formatting, a small database is built, mapping portion pointers + // to their maximum size (used for kana compression) + SwTextPortionMap m_aMaxWidth; + // for each line, an array of compression values is calculated + // this array is passed over to the info structure + std::deque<sal_uInt16>* m_pKanaComp; + + SwViewShell *m_pVsh; + + // m_pOut is the output device, m_pRef is the device used for formatting + VclPtr<OutputDevice> m_pOut; + VclPtr<OutputDevice> m_pRef; + + // performance hack - this is only used by SwTextFormatInfo but + // because it's not even possible to dynamic_cast these things + // currently it has to be stored here + std::shared_ptr<const vcl::text::TextLayoutCache> m_pCachedVclData; + + SwFont *m_pFnt; + SwUnderlineFont *m_pUnderFnt; // Font for underlining + SwTextFrame *m_pFrame; + const SwViewOption *m_pOpt; + const OUString *m_pText; + TextFrameIndex m_nIdx; + TextFrameIndex m_nLen; + TextFrameIndex m_nMeasureLen; + sal_uInt16 m_nKanaIdx; + bool m_bOnWin : 1; + bool m_bNotEOL : 1; + bool m_bURLNotify : 1; + bool m_bStopUnderflow : 1; // Underflow was stopped e.g. by a FlyPortion + bool m_bFootnoteInside : 1; // the current line contains a footnote + bool m_bOtherThanFootnoteInside : 1; // the current line contains another portion than a footnote portion. + // needed for checking keep together of footnote portion with previous portion + bool m_bMulti : 1; // inside a multiportion + bool m_bFirstMulti : 1; // this flag is used for two purposes: + // - the multiportion is the first lineportion + // - indicates, if we are currently in second + // line of multi portion + bool m_bRuby : 1; // during the formatting of a phonetic line + bool m_bHanging : 1; // formatting of hanging punctuation allowed + bool m_bScriptSpace : 1; // space between different scripts (Asian/Latin) + bool m_bForbiddenChars : 1; // Forbidden start/endline characters + bool m_bSnapToGrid : 1; // paragraph snaps to grid + sal_uInt8 m_nDirection : 2; // writing direction: 0/90/180/270 degree + +protected: + void CtorInitTextSizeInfo( OutputDevice* pRenderContext, SwTextFrame *pFrame, + TextFrameIndex nIdx); + SwTextSizeInfo(); +public: + SwTextSizeInfo( const SwTextSizeInfo &rInf ); + SwTextSizeInfo( const SwTextSizeInfo &rInf, const OUString* pText, + TextFrameIndex nIdx = TextFrameIndex(0) ); + SwTextSizeInfo(SwTextFrame *pTextFrame, TextFrameIndex nIndex = TextFrameIndex(0)); + + // GetMultiAttr returns the text attribute of the multiportion, + // if rPos is inside any multi-line part. + // rPos will set to the end of the multi-line part. + std::optional<SwMultiCreator> GetMultiCreator(TextFrameIndex &rPos, SwMultiPortion const* pM) const; + + bool OnWin() const { return m_bOnWin; } + void SetOnWin( const bool bNew ) { m_bOnWin = bNew; } + bool NotEOL() const { return m_bNotEOL; } + void SetNotEOL( const bool bNew ) { m_bNotEOL = bNew; } + bool URLNotify() const { return m_bURLNotify; } + bool StopUnderflow() const { return m_bStopUnderflow; } + void SetStopUnderflow( const bool bNew ) { m_bStopUnderflow = bNew; } + bool IsFootnoteInside() const { return m_bFootnoteInside; } + void SetFootnoteInside( const bool bNew ) { m_bFootnoteInside = bNew; } + bool IsOtherThanFootnoteInside() const { return m_bOtherThanFootnoteInside; } + void SetOtherThanFootnoteInside( const bool bNew ) { m_bOtherThanFootnoteInside = bNew; } + bool IsMulti() const { return m_bMulti; } + void SetMulti( const bool bNew ) { m_bMulti = bNew; } + bool IsFirstMulti() const { return m_bFirstMulti; } + void SetFirstMulti( const bool bNew ) { m_bFirstMulti = bNew; } + bool IsRuby() const { return m_bRuby; } + void SetRuby( const bool bNew ) { m_bRuby = bNew; } + bool IsHanging() const { return m_bHanging; } + void SetHanging( const bool bNew ) { m_bHanging = bNew; } + bool HasScriptSpace() const { return m_bScriptSpace; } + void SetScriptSpace( const bool bNew ) { m_bScriptSpace = bNew; } + bool HasForbiddenChars() const { return m_bForbiddenChars; } + void SetForbiddenChars( const bool bN ) { m_bForbiddenChars = bN; } + bool SnapToGrid() const { return m_bSnapToGrid; } + void SetSnapToGrid( const bool bN ) { m_bSnapToGrid = bN; } + sal_uInt8 GetDirection() const { return m_nDirection; } + void SetDirection( const sal_uInt8 nNew ) { m_nDirection = nNew; } + bool IsRotated() const { return ( 1 & m_nDirection ); } + + SwViewShell *GetVsh() { return m_pVsh; } + const SwViewShell *GetVsh() const { return m_pVsh; } + + vcl::RenderContext *GetOut() { return m_pOut; } + const vcl::RenderContext *GetOut() const { return m_pOut; } + void SetOut( OutputDevice* pNewOut ) { m_pOut = pNewOut; } + + vcl::RenderContext *GetRefDev() { return m_pRef; } + const vcl::RenderContext *GetRefDev() const { return m_pRef; } + + SwFont *GetFont() { return m_pFnt; } + const SwFont *GetFont() const { return m_pFnt; } + void SetFont( SwFont *pNew ) { m_pFnt = pNew; } + void SelectFont(); + void SetUnderFnt( SwUnderlineFont* pNew ) { m_pUnderFnt = pNew; } + SwUnderlineFont* GetUnderFnt() const { return m_pUnderFnt; } + + const SwViewOption &GetOpt() const { return *m_pOpt; } + const OUString &GetText() const { return *m_pText; } + sal_Unicode GetChar(TextFrameIndex const nPos) const { + if (m_pText && nPos < TextFrameIndex(m_pText->getLength())) return (*m_pText)[sal_Int32(nPos)]; + return 0; + } + + sal_uInt16 GetTextHeight() const; + + SwPosSize GetTextSize( OutputDevice* pOut, const SwScriptInfo* pSI, + const OUString& rText, TextFrameIndex nIdx, + TextFrameIndex nLen ) const; + SwPosSize GetTextSize() const; + void GetTextSize( const SwScriptInfo* pSI, TextFrameIndex nIdx, + TextFrameIndex nLen, const sal_uInt16 nComp, + sal_uInt16& nMinSize, sal_uInt16& nMaxSizeDiff, + vcl::text::TextLayoutCache const* = nullptr) const; + inline SwPosSize GetTextSize(const SwScriptInfo* pSI, TextFrameIndex nIdx, + TextFrameIndex nLen) const; + inline SwPosSize GetTextSize( const OUString &rText ) const; + + TextFrameIndex GetTextBreak( const tools::Long nLineWidth, + const TextFrameIndex nMaxLen, + const sal_uInt16 nComp, + vcl::text::TextLayoutCache const*) const; + TextFrameIndex GetTextBreak( const tools::Long nLineWidth, + const TextFrameIndex nMaxLen, + const sal_uInt16 nComp, + TextFrameIndex& rExtraCharPos, + vcl::text::TextLayoutCache const*) const; + + sal_uInt16 GetAscent() const; + sal_uInt16 GetHangingBaseline() const; + + TextFrameIndex GetIdx() const { return m_nIdx; } + void SetIdx(const TextFrameIndex nNew) { m_nIdx = nNew; } + TextFrameIndex GetLen() const { return m_nLen; } + void SetLen(const TextFrameIndex nNew) { m_nLen = nNew; } + TextFrameIndex GetMeasureLen() const { return m_nMeasureLen; } + void SetMeasureLen(const TextFrameIndex nNew) { m_nMeasureLen = nNew; } + void SetText( const OUString &rNew ){ m_pText = &rNew; } + + // No Bullets for the symbol font! + bool IsNoSymbol() const + { return RTL_TEXTENCODING_SYMBOL != m_pFnt->GetCharSet( m_pFnt->GetActual() ); } + + void NoteAnimation() const; + + // Home is where Your heart is... + SwTextFrame *GetTextFrame() { return m_pFrame; } + const SwTextFrame *GetTextFrame() const { return m_pFrame; } + + bool HasHint(TextFrameIndex nPos) const; + + // If Kana Compression is enabled, a minimum and maximum portion width + // is calculated. We format lines with minimal size and share remaining + // space among compressed kanas. + // During formatting, the maximum values of compressible portions are + // stored in m_aMaxWidth and discarded after a line has been formatted. + void SetMaxWidthDiff( const SwLinePortion *nKey, sal_uInt16 nVal ) + { + m_aMaxWidth.insert( std::make_pair( nKey, nVal ) ); + }; + sal_uInt16 GetMaxWidthDiff( const SwLinePortion *nKey ) + { + SwTextPortionMap::iterator it = m_aMaxWidth.find( nKey ); + + if( it != m_aMaxWidth.end() ) + return it->second; + else + return 0; + }; + void ResetMaxWidthDiff() + { + m_aMaxWidth.clear(); + }; + bool CompressLine() + { + return !m_aMaxWidth.empty(); + }; + + // Feature: Kana Compression + + sal_uInt16 GetKanaIdx() const { return m_nKanaIdx; } + void ResetKanaIdx(){ m_nKanaIdx = 0; } + void SetKanaIdx( sal_uInt16 nNew ) { m_nKanaIdx = nNew; } + void IncKanaIdx() { ++m_nKanaIdx; } + void SetKanaComp( std::deque<sal_uInt16> *pNew ){ m_pKanaComp = pNew; } + std::deque<sal_uInt16>* GetpKanaComp() const { return m_pKanaComp; } + sal_uInt16 GetKanaComp() const + { return ( m_pKanaComp && m_nKanaIdx < m_pKanaComp->size() ) + ? (*m_pKanaComp)[m_nKanaIdx] : 0; } + + const std::shared_ptr<const vcl::text::TextLayoutCache>& GetCachedVclData() const + { + return m_pCachedVclData; + } + void SetCachedVclData(std::shared_ptr<const vcl::text::TextLayoutCache> const& pCachedVclData) + { + m_pCachedVclData = pCachedVclData; + } +}; + +class SwTextPaintInfo : public SwTextSizeInfo +{ + sw::WrongListIterator *m_pWrongList; + sw::WrongListIterator *m_pGrammarCheckList; + sw::WrongListIterator *m_pSmartTags; + std::vector<tools::Long>* m_pSpaceAdd; + const SvxBrushItem *m_pBrushItem; // For the background + SwTextFly m_aTextFly; // Calculate the FlyFrame + Point m_aPos; // Paint position + SwRect m_aPaintRect; // Original paint rect (from Layout paint) + + sal_uInt16 m_nSpaceIdx; + void DrawText_(const OUString &rText, const SwLinePortion &rPor, + const TextFrameIndex nIdx, const TextFrameIndex nLen, + const bool bKern, const bool bWrong = false, + const bool bSmartTag = false, + const bool bGrammarCheck = false ); + + SwTextPaintInfo &operator=(const SwTextPaintInfo&) = delete; + +protected: + SwTextPaintInfo() + : m_pWrongList(nullptr) + , m_pGrammarCheckList(nullptr) + , m_pSmartTags(nullptr) + , m_pSpaceAdd(nullptr) + , m_pBrushItem(nullptr) + , m_nSpaceIdx(0) + {} + +public: + SwTextPaintInfo( const SwTextPaintInfo &rInf ); + SwTextPaintInfo( const SwTextPaintInfo &rInf, const OUString* pText ); + + void CtorInitTextPaintInfo( OutputDevice* pRenderContext, SwTextFrame *pFrame, const SwRect &rPaint ); + + const SvxBrushItem *GetBrushItem() const { return m_pBrushItem; } + + SwTextPaintInfo( SwTextFrame *pFrame, const SwRect &rPaint ); + + SwTwips X() const { return m_aPos.X(); } + void X( const tools::Long nNew ) { m_aPos.setX(nNew); } + SwTwips Y() const { return m_aPos.Y(); } + void Y( const SwTwips nNew ) { m_aPos.setY(nNew); } + + SwTextFly& GetTextFly() { return m_aTextFly; } + const SwTextFly& GetTextFly() const { return m_aTextFly; } + inline void DrawText( const OUString &rText, const SwLinePortion &rPor, + TextFrameIndex nIdx = TextFrameIndex(0), + TextFrameIndex nLen = TextFrameIndex(COMPLETE_STRING), + const bool bKern = false) const; + inline void DrawText( const SwLinePortion &rPor, TextFrameIndex nLen, + const bool bKern = false ) const; + inline void DrawMarkedText( const SwLinePortion &rPor, TextFrameIndex nLen, + const bool bWrong, + const bool bSmartTags, + const bool bGrammarCheck ) const; + + void DrawRect( const SwRect &rRect, bool bRetouche ) const; + + void DrawTab( const SwLinePortion &rPor ) const; + void DrawLineBreak( const SwLinePortion &rPor ) const; + void DrawRedArrow( const SwLinePortion &rPor ) const; + void DrawPostIts( bool bScript ) const; + void DrawBackground( const SwLinePortion &rPor, const Color *pColor=nullptr ) const; + void DrawViewOpt( const SwLinePortion &rPor, PortionType nWhich, const Color *pColor=nullptr ) const; + void DrawBackBrush( const SwLinePortion &rPor ) const; + + /** + * Draw character border around a line portion. + * + * @param[in] rPor line portion around which border have to be drawn. + **/ + void DrawBorder( const SwLinePortion &rPor ) const; + + void DrawCheckBox(const SwFieldFormCheckboxPortion &rPor, bool bChecked) const; + + void DrawCSDFHighlighting(const SwLinePortion &rPor) const; + + /** + * Calculate the rectangular area where the portion takes place. + * @param[in] rPor portion for which the method specify the painting area + * @param[out] pRect whole area of the portion + * @param[out] pIntersect part of the portion area clipped by OutputDevice's clip region + * @param[in] bInsideBox area of portion's content, padding and border, but shadow + * is excluded (e.g. for background) + **/ + void CalcRect( const SwLinePortion& rPor, SwRect* pRect, + SwRect* pIntersect = nullptr, const bool bInsideBox = false ) const; + + inline SwTwips GetPaintOfst() const; + inline void SetPaintOfst( const SwTwips nNew ); + const Point &GetPos() const { return m_aPos; } + void SetPos( const Point &rNew ) { m_aPos = rNew; } + + const SwRect &GetPaintRect() const { return m_aPaintRect; } + + // STUFF FOR JUSTIFIED ALIGNMENT + + sal_uInt16 GetSpaceIdx() const { return m_nSpaceIdx; } + void ResetSpaceIdx(){m_nSpaceIdx = 0; } + void SetSpaceIdx( sal_uInt16 nNew ) { m_nSpaceIdx = nNew; } + void IncSpaceIdx() { ++m_nSpaceIdx; } + void RemoveFirstSpaceAdd() { m_pSpaceAdd->erase( m_pSpaceAdd->begin() ); } + tools::Long GetSpaceAdd( bool bShrink = false ) const + { return ( m_pSpaceAdd && m_nSpaceIdx < m_pSpaceAdd->size() && + // get shrink data only if asked explicitly, otherwise zero it + ( bShrink || (*m_pSpaceAdd)[m_nSpaceIdx] < LONG_MAX/2 ) ) + ? (*m_pSpaceAdd)[m_nSpaceIdx] : 0; } + + void SetpSpaceAdd( std::vector<tools::Long>* pNew ){ m_pSpaceAdd = pNew; } + std::vector<tools::Long>* GetpSpaceAdd() const { return m_pSpaceAdd; } + + void SetWrongList(sw::WrongListIterator *const pNew) { m_pWrongList = pNew; } + sw::WrongListIterator* GetpWrongList() const { return m_pWrongList; } + + void SetGrammarCheckList(sw::WrongListIterator *const pNew) { m_pGrammarCheckList = pNew; } + sw::WrongListIterator* GetGrammarCheckList() const { return m_pGrammarCheckList; } + + void SetSmartTags(sw::WrongListIterator *const pNew) { m_pSmartTags = pNew; } + sw::WrongListIterator* GetSmartTags() const { return m_pSmartTags; } +}; + +class SwTextFormatInfo : public SwTextPaintInfo +{ + // temporary arguments for hyphenation + css::beans::PropertyValues m_aHyphVals; + + SwLineLayout *m_pRoot; // The Root of the current line (pCurr) + SwLinePortion *m_pLast; // The last Portion + SwFlyPortion *m_pFly; // The following FlyPortion + SwLinePortion *m_pUnderflow; // Underflow: Last Portion + SwLinePortion *m_pRest; // The Rest is the start of the next Line + + SwTabPortion *m_pLastTab; // The _last_ TabPortion + + TextFrameIndex m_nSoftHyphPos; ///< SoftHyphPos for Hyphenation + TextFrameIndex m_nLineStart; ///< Current line start in rText + TextFrameIndex m_nUnderScorePos; ///< enlarge repaint if underscore has been found + TextFrameIndex m_nLastBookmarkPos; ///< need to check for bookmarks at every portion + // #i34348# Changed type from sal_uInt16 to SwTwips + SwTwips m_nLeft; // Left margin + SwTwips m_nRight; // Right margin + SwTwips m_nFirst; // EZE + /// First or left margin, depending on context. + SwTwips m_nLeftMargin = 0; + sal_uInt16 m_nRealWidth; // "real" line width + sal_uInt16 m_nWidth; // "virtual" line width + sal_uInt16 m_nLineHeight; // Final height after CalcLine + sal_uInt16 m_nLineNetHeight; // line height without spacing + sal_uInt16 m_nForcedLeftMargin; // Shift of left margin due to frame + + bool m_bFull : 1; // Line is full + bool m_bFootnoteDone : 1; // Footnote already formatted + bool m_bErgoDone : 1; // ErgoDone already formatted + bool m_bNumDone : 1; // bNumDone already formatted + bool m_bArrowDone : 1; // Arrow to the left for scrolling paragraphs + bool m_bStop : 1; // Cancel immediately, discarding the line + bool m_bNewLine : 1; // Format another line + bool m_bShift : 1; // Position change: Repaint until further notice + bool m_bUnderflow : 1; // Context: Underflow() ? + bool m_bInterHyph : 1; // Interactive hyphenation? + bool m_bAutoHyph : 1; // Automatic hyphenation? + bool m_bDropInit : 1; // Set DropWidth + bool m_bQuick : 1; // FormatQuick() + bool m_bNoEndHyph : 1; // Switch off hyphenation at the line end (due to MaxHyphens) + bool m_bNoMidHyph : 1; // Switch off hyphenation before flys (due to MaxHyphens) + bool m_bIgnoreFly : 1; // FitToContent ignores flys + bool m_bFakeLineStart : 1; // String has been replaced by field portion + // info structure only pretends that we are at + // the beginning of a line + bool m_bTabOverflow : 1; // Tabs are expanding after the end margin + bool m_bTestFormat : 1; // Test formatting from WouldFit, no notification etc. + + sal_Unicode m_cTabDecimal; // the current decimal delimiter + sal_Unicode m_cHookChar; // For tabs in fields etc. + sal_uInt8 m_nMaxHyph; // Max. line count of followup hyphenations + + // Used to stop justification after center/right/decimal tab stops - see tdf#tdf#106234 + enum class TabSeen + { + None, + Left, + Center, + Right, + Decimal, + } m_eLastTabsSeen = TabSeen::None; + + // Hyphenating ... + bool InitHyph( const bool bAuto = false ); + bool CheckFootnotePortion_( SwLineLayout const * pCurr ); + +public: + void CtorInitTextFormatInfo( OutputDevice* pRenderContext, SwTextFrame *pFrame, const bool bInterHyph = false, + const bool bQuick = false, const bool bTst = false ); + SwTextFormatInfo(OutputDevice* pRenderContext, SwTextFrame *pFrame, const bool bInterHyphL = false, + const bool bQuickL = false, const bool bTst = false); + + // For the formatting inside a double line in a line (multi-line portion) + // we need a modified text-format-info: + SwTextFormatInfo( const SwTextFormatInfo& rInf, SwLineLayout& rLay, + SwTwips nActWidth ); + + sal_uInt16 Width() const { return m_nWidth; } + void Width( const sal_uInt16 nNew ) { m_nWidth = nNew; } + void Init(); + + /** + * Returns the distance between the current horizontal position and the end + * of the line. + */ + SwTwips GetLineWidth(); + + // Returns the first changed position of the paragraph + inline TextFrameIndex GetReformatStart() const; + + // Margins + SwTwips Left() const { return m_nLeft; } + void Left( const SwTwips nNew ) { m_nLeft = nNew; } + SwTwips Right() const { return m_nRight; } + void Right( const SwTwips nNew ) { m_nRight = nNew; } + SwTwips First() const { return m_nFirst; } + void First( const SwTwips nNew ) { m_nFirst = nNew; } + void LeftMargin( const SwTwips nNew) { m_nLeftMargin = nNew; } + sal_uInt16 RealWidth() const { return m_nRealWidth; } + void RealWidth( const sal_uInt16 nNew ) { m_nRealWidth = nNew; } + sal_uInt16 ForcedLeftMargin() const { return m_nForcedLeftMargin; } + void ForcedLeftMargin( const sal_uInt16 nN ) { m_nForcedLeftMargin = nN; } + + sal_uInt8 &MaxHyph() { return m_nMaxHyph; } + const sal_uInt8 &MaxHyph() const { return m_nMaxHyph; } + + SwLineLayout *GetRoot() { return m_pRoot; } + const SwLineLayout *GetRoot() const { return m_pRoot; } + + void SetRoot( SwLineLayout *pNew ) { m_pRoot = pNew; } + SwLinePortion *GetLast() { return m_pLast; } + void SetLast(SwLinePortion* pNewLast); + bool IsFull() const { return m_bFull; } + void SetFull( const bool bNew ) { m_bFull = bNew; } + bool IsHyphForbud() const + { return m_pFly ? m_bNoMidHyph : m_bNoEndHyph; } + void ChkNoHyph( const sal_uInt8 bEnd, const sal_uInt8 bMid ) + { m_bNoEndHyph = (m_nMaxHyph && bEnd >= m_nMaxHyph); + m_bNoMidHyph = (m_nMaxHyph && bMid >= m_nMaxHyph); } + bool IsIgnoreFly() const { return m_bIgnoreFly; } + void SetIgnoreFly( const bool bNew ) { m_bIgnoreFly = bNew; } + bool IsFakeLineStart() const { return m_bFakeLineStart; } + void SetFakeLineStart( const bool bNew ) { m_bFakeLineStart = bNew; } + bool IsStop() const { return m_bStop; } + void SetStop( const bool bNew ) { m_bStop = bNew; } + SwLinePortion *GetRest() { return m_pRest; } + void SetRest( SwLinePortion *pNewRest ) { m_pRest = pNewRest; } + bool IsNewLine() const { return m_bNewLine; } + void SetNewLine( const bool bNew ) { m_bNewLine = bNew; } + bool IsShift() const { return m_bShift; } + void SetShift( const bool bNew ) { m_bShift = bNew; } + bool IsInterHyph() const { return m_bInterHyph; } + bool IsUnderflow() const { return m_bUnderflow; } + void ClrUnderflow() { m_bUnderflow = false; } + bool IsDropInit() const { return m_bDropInit; } + void SetDropInit( const bool bNew ) { m_bDropInit = bNew; } + bool IsQuick() const { return m_bQuick; } + bool IsTest() const { return m_bTestFormat; } + // see tdf#106234 + void UpdateTabSeen(PortionType); + bool DontBlockJustify() const + { + return m_eLastTabsSeen == TabSeen::Center || m_eLastTabsSeen == TabSeen::Right + || m_eLastTabsSeen == TabSeen::Decimal; + } + + TextFrameIndex GetLineStart() const { return m_nLineStart; } + void SetLineStart(TextFrameIndex const nNew) { m_nLineStart = nNew; } + + // these are used during fly calculation + sal_uInt16 GetLineHeight() const { return m_nLineHeight; } + void SetLineHeight( const sal_uInt16 nNew ) { m_nLineHeight = nNew; } + sal_uInt16 GetLineNetHeight() const { return m_nLineNetHeight; } + void SetLineNetHeight( const sal_uInt16 nNew ) { m_nLineNetHeight = nNew; } + + const SwLinePortion *GetUnderflow() const { return m_pUnderflow; } + SwLinePortion *GetUnderflow() { return m_pUnderflow; } + void SetUnderflow( SwLinePortion *pNew ) + { m_pUnderflow = pNew; m_bUnderflow = true; } + TextFrameIndex GetSoftHyphPos() const { return m_nSoftHyphPos; } + void SetSoftHyphPos(TextFrameIndex const nNew) { m_nSoftHyphPos = nNew; } + + inline void SetParaFootnote(); + + // FlyFrames + SwFlyPortion *GetFly() { return m_pFly; } + void SetFly( SwFlyPortion *pNew ) { m_pFly = pNew; } + + inline const SwAttrSet& GetCharAttr() const; + + // Tabs + SwTabPortion *GetLastTab() { return m_pLastTab; } + void SetLastTab( SwTabPortion *pNew ) { m_pLastTab = pNew; } + sal_Unicode GetTabDecimal() const { return m_cTabDecimal; } + void SetTabDecimal( const sal_Unicode cNew ) { m_cTabDecimal = cNew;} + + void ClearHookChar() { m_cHookChar = 0; } + void SetHookChar( const sal_Unicode cNew ) { m_cHookChar = cNew; } + sal_Unicode GetHookChar() const { return m_cHookChar; } + + // Done-Flags + bool IsFootnoteDone() const { return m_bFootnoteDone; } + void SetFootnoteDone( const bool bNew ) { m_bFootnoteDone = bNew; } + bool IsErgoDone() const { return m_bErgoDone; } + void SetErgoDone( const bool bNew ) { m_bErgoDone = bNew; } + bool IsNumDone() const { return m_bNumDone; } + void SetNumDone( const bool bNew ) { m_bNumDone = bNew; } + bool IsArrowDone() const { return m_bArrowDone; } + void SetArrowDone( const bool bNew ) { m_bArrowDone = bNew; } + + bool CheckCurrentPosBookmark(); + + // For SwTextPortion::Hyphenate + bool ChgHyph( const bool bNew ); + + // Should the hyphenate helper be discarded? + bool IsHyphenate() const; + TextFrameIndex GetUnderScorePos() const { return m_nUnderScorePos; } + void SetUnderScorePos(TextFrameIndex const nNew) { m_nUnderScorePos = nNew; } + + // Calls HyphenateWord() of Hyphenator + css::uno::Reference< css::linguistic2::XHyphenatedWord > + HyphWord( const OUString &rText, const sal_Int32 nMinTrail ); + const css::beans::PropertyValues & GetHyphValues() const; + + bool CheckFootnotePortion( SwLineLayout const * pCurr ) + { return IsFootnoteInside() && CheckFootnotePortion_( pCurr ); } + + // Dropcaps called by SwTextFormatter::CTOR + const SwFormatDrop *GetDropFormat() const; + + // Sets the last SwKernPortion as pLast, if it is followed by empty portions + bool LastKernPortion(); + + // Looks for tabs, TabDec, TXTATR and BRK from nIdx until nEnd. + // Return: Position; sets cHookChar if necessary + TextFrameIndex ScanPortionEnd(TextFrameIndex nStart, TextFrameIndex nEnd); + + void SetTabOverflow( bool bOverflow ) { m_bTabOverflow = bOverflow; } + bool IsTabOverflow() const { return m_bTabOverflow; } + +}; + +/** + * For the text replacement and restoration of SwTextSizeInfo. + * The way this is done is a bit of a hack: Although rInf is const we change it + * anyway. + * Because rInf is restored again in the DTOR, we can do this. + * You could call it a "logical const", if you wish. + */ +class SwTextSlot final +{ + OUString aText; + std::shared_ptr<const vcl::text::TextLayoutCache> m_pOldCachedVclData; + const OUString *pOldText; + sw::WrongListIterator * m_pOldSmartTagList; + sw::WrongListIterator * m_pOldGrammarCheckList; + std::unique_ptr<SwWrongList> m_pTempList; + std::unique_ptr<sw::WrongListIterator> m_pTempIter; + TextFrameIndex nIdx; + TextFrameIndex nLen; + TextFrameIndex nMeasureLen; + bool bOn; + SwTextSizeInfo *pInf; + +public: + // The replacement string originates either from the portion via GetExpText() + // or from the rCh, if it is not empty. + SwTextSlot( const SwTextSizeInfo *pNew, const SwLinePortion *pPor, bool bTextLen, + bool bExgLists, OUString const & rCh = OUString() ); + ~SwTextSlot(); +}; + +class SwFontSave +{ + SwTextSizeInfo *pInf; + SwFont *pFnt; + SwAttrIter *pIter; +public: + SwFontSave( const SwTextSizeInfo &rInf, SwFont *pFnt, + SwAttrIter* pItr = nullptr ); + ~SwFontSave(); +}; + +inline sal_uInt16 SwTextSizeInfo::GetAscent() const +{ + assert(GetOut()); + return const_cast<SwFont*>(GetFont())->GetAscent( m_pVsh, *GetOut() ); +} + +inline sal_uInt16 SwTextSizeInfo::GetTextHeight() const +{ + assert(GetOut()); + return const_cast<SwFont*>(GetFont())->GetHeight( m_pVsh, *GetOut() ); +} + +inline sal_uInt16 SwTextSizeInfo::GetHangingBaseline() const +{ + assert(GetOut()); + return const_cast<SwFont*>(GetFont())->GetHangingBaseline( m_pVsh, *GetOut() ); +} + +inline SwPosSize SwTextSizeInfo::GetTextSize( const OUString &rText ) const +{ + return GetTextSize(m_pOut, nullptr, rText, TextFrameIndex(0), TextFrameIndex(rText.getLength())); +} + +inline SwPosSize SwTextSizeInfo::GetTextSize( const SwScriptInfo* pSI, + TextFrameIndex const nNewIdx, + TextFrameIndex const nNewLen) const +{ + return GetTextSize( m_pOut, pSI, *m_pText, nNewIdx, nNewLen ); +} + +inline SwTwips SwTextPaintInfo::GetPaintOfst() const +{ + return GetParaPortion()->GetRepaint().GetOffset(); +} + +inline void SwTextPaintInfo::SetPaintOfst( const SwTwips nNew ) +{ + GetParaPortion()->GetRepaint().SetOffset( nNew ); +} + +inline void SwTextPaintInfo::DrawText( const OUString &rText, + const SwLinePortion &rPor, + const TextFrameIndex nStart, const TextFrameIndex nLength, + const bool bKern ) const +{ + const_cast<SwTextPaintInfo*>(this)->DrawText_( rText, rPor, nStart, nLength, bKern ); +} + +inline void SwTextPaintInfo::DrawText( const SwLinePortion &rPor, + const TextFrameIndex nLength, const bool bKern ) const +{ + const_cast<SwTextPaintInfo*>(this)->DrawText_( *m_pText, rPor, m_nIdx, nLength, bKern ); +} + +inline void SwTextPaintInfo::DrawMarkedText( const SwLinePortion &rPor, + const TextFrameIndex nLength, + const bool bWrong, + const bool bSmartTags, + const bool bGrammarCheck ) const +{ + const_cast<SwTextPaintInfo*>(this)->DrawText_( *m_pText, rPor, m_nIdx, nLength, false/*bKern*/, bWrong, bSmartTags, bGrammarCheck ); +} + +inline TextFrameIndex SwTextFormatInfo::GetReformatStart() const +{ + return GetParaPortion()->GetReformat().Start(); +} + +inline const SwAttrSet& SwTextFormatInfo::GetCharAttr() const +{ + // sw_redlinehide: this is used for numbering/footnote number portions, so: + return GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet(); +} + +inline void SwTextFormatInfo::SetParaFootnote() +{ + GetTextFrame()->SetFootnote( true ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itradj.cxx b/sw/source/core/text/itradj.cxx new file mode 100644 index 0000000000..4dcaf03df1 --- /dev/null +++ b/sw/source/core/text/itradj.cxx @@ -0,0 +1,852 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <o3tl/safeint.hxx> + +#include <IDocumentSettingAccess.hxx> +#include <doc.hxx> + +#include "itrtxt.hxx" +#include "porglue.hxx" +#include "porlay.hxx" +#include "porfly.hxx" +#include "pormulti.hxx" +#include "portab.hxx" +#include <memory> + +#define MIN_TAB_WIDTH 60 + +using namespace ::com::sun::star; + +void SwTextAdjuster::FormatBlock( ) +{ + // Block format does not apply to the last line. + // And for tabs it doesn't exist out of tradition + // If we have Flys we continue. + + const SwLinePortion *pFly = nullptr; + + bool bSkip = !IsLastBlock() && + m_nStart + m_pCurr->GetLen() >= TextFrameIndex(GetInfo().GetText().getLength()); + + // Multi-line fields are tricky, because we need to check whether there are + // any other text portions in the paragraph. + if( bSkip ) + { + const SwLineLayout *pLay = m_pCurr->GetNext(); + while( pLay && !pLay->GetLen() ) + { + const SwLinePortion *pPor = m_pCurr->GetFirstPortion(); + while( pPor && bSkip ) + { + if( pPor->InTextGrp() ) + bSkip = false; + pPor = pPor->GetNextPortion(); + } + pLay = bSkip ? pLay->GetNext() : nullptr; + } + } + + if( bSkip ) + { + if( !GetInfo().GetParaPortion()->HasFly() ) + { + if( IsLastCenter() ) + CalcFlyAdjust( m_pCurr ); + m_pCurr->FinishSpaceAdd(); + return; + } + else + { + const SwLinePortion *pTmpFly = nullptr; + + // End at the last Fly + const SwLinePortion *pPos = m_pCurr->GetFirstPortion(); + while( pPos ) + { + // Look for the last Fly which has text coming after it: + if( pPos->IsFlyPortion() ) + pTmpFly = pPos; // Found a Fly + else if ( pTmpFly && pPos->InTextGrp() ) + { + pFly = pTmpFly; // A Fly with follow-up text! + pTmpFly = nullptr; + } + pPos = pPos->GetNextPortion(); + } + // End if we didn't find one + if( !pFly ) + { + if( IsLastCenter() ) + CalcFlyAdjust( m_pCurr ); + m_pCurr->FinishSpaceAdd(); + return; + } + } + } + + const TextFrameIndex nOldIdx = GetInfo().GetIdx(); + GetInfo().SetIdx( m_nStart ); + CalcNewBlock( m_pCurr, pFly ); + GetInfo().SetIdx( nOldIdx ); + GetInfo().GetParaPortion()->GetRepaint().SetOffset(0); +} + +static bool lcl_CheckKashidaPositions( SwScriptInfo& rSI, SwTextSizeInfo& rInf, SwTextIter& rItr, + sal_Int32& rKashidas, TextFrameIndex& nGluePortion) +{ + // i60594 validate Kashida justification + TextFrameIndex nIdx = rItr.GetStart(); + TextFrameIndex nEnd = rItr.GetEnd(); + + // Note on calling KashidaJustify(): + // Kashida positions may be marked as invalid. Therefore KashidaJustify may return the clean + // total number of kashida positions, or the number of kashida positions after some positions + // have been dropped. + // Here we want the clean total, which is OK: We have called ClearKashidaInvalid() before. + rKashidas = rSI.KashidaJustify(nullptr, nullptr, rItr.GetStart(), rItr.GetLength()); + + if (rKashidas <= 0) // nothing to do + return true; + + // kashida positions found in SwScriptInfo are not necessarily valid in every font + // if two characters are replaced by a ligature glyph, there will be no place for a kashida + std::vector<TextFrameIndex> aKashidaPos; + rSI.GetKashidaPositions(nIdx, rItr.GetLength(), aKashidaPos); + assert(aKashidaPos.size() >= o3tl::make_unsigned(rKashidas)); + std::vector<TextFrameIndex> aKashidaPosDropped(aKashidaPos.size()); + sal_Int32 nKashidaIdx = 0; + while ( rKashidas && nIdx < nEnd ) + { + rItr.SeekAndChgAttrIter( nIdx, rInf.GetOut() ); + TextFrameIndex nNext = rItr.GetNextAttr(); + + // is there also a script change before? + // if there is, nNext should point to the script change + TextFrameIndex const nNextScript = rSI.NextScriptChg( nIdx ); + if( nNextScript < nNext ) + nNext = nNextScript; + + if (nNext == TextFrameIndex(COMPLETE_STRING) || nNext > nEnd) + nNext = nEnd; + sal_Int32 nKashidasInAttr = rSI.KashidaJustify(nullptr, nullptr, nIdx, nNext - nIdx); + if (nKashidasInAttr > 0) + { + // Kashida glyph looks suspicious, skip Kashida justification + if ( rInf.GetOut()->GetMinKashida() <= 0 ) + { + return false; + } + + sal_Int32 nKashidasDropped = 0; + if ( !SwScriptInfo::IsArabicText( rInf.GetText(), nIdx, nNext - nIdx ) ) + { + nKashidasDropped = nKashidasInAttr; + rKashidas -= nKashidasDropped; + } + else + { + vcl::text::ComplexTextLayoutFlags nOldLayout = rInf.GetOut()->GetLayoutMode(); + rInf.GetOut()->SetLayoutMode ( nOldLayout | vcl::text::ComplexTextLayoutFlags::BiDiRtl ); + nKashidasDropped = rInf.GetOut()->ValidateKashidas( + rInf.GetText(), sal_Int32(nIdx), sal_Int32(nNext - nIdx), + nKashidasInAttr, + reinterpret_cast<sal_Int32*>(aKashidaPos.data() + nKashidaIdx), + reinterpret_cast<sal_Int32*>(aKashidaPosDropped.data())); + rInf.GetOut()->SetLayoutMode ( nOldLayout ); + if ( nKashidasDropped ) + { + rSI.MarkKashidasInvalid(nKashidasDropped, aKashidaPosDropped.data()); + rKashidas -= nKashidasDropped; + nGluePortion -= TextFrameIndex(nKashidasDropped); + } + } + nKashidaIdx += nKashidasInAttr; + } + nIdx = nNext; + } + + // return false if all kashidas have been eliminated + return (rKashidas > 0); +} + +static bool lcl_CheckKashidaWidth ( SwScriptInfo& rSI, SwTextSizeInfo& rInf, SwTextIter& rItr, sal_Int32& rKashidas, + TextFrameIndex& nGluePortion, const tools::Long nGluePortionWidth, tools::Long& nSpaceAdd ) +{ + // check kashida width + // if width is smaller than minimal kashida width allowed by fonts in the current line + // drop one kashida after the other until kashida width is OK + while (rKashidas) + { + bool bAddSpaceChanged = false; + TextFrameIndex nIdx = rItr.GetStart(); + TextFrameIndex nEnd = rItr.GetEnd(); + while ( nIdx < nEnd ) + { + rItr.SeekAndChgAttrIter( nIdx, rInf.GetOut() ); + TextFrameIndex nNext = rItr.GetNextAttr(); + + // is there also a script change before? + // if there is, nNext should point to the script change + TextFrameIndex const nNextScript = rSI.NextScriptChg( nIdx ); + if( nNextScript < nNext ) + nNext = nNextScript; + + if (nNext == TextFrameIndex(COMPLETE_STRING) || nNext > nEnd) + nNext = nEnd; + sal_Int32 nKashidasInAttr = rSI.KashidaJustify(nullptr, nullptr, nIdx, nNext - nIdx); + + tools::Long nFontMinKashida = rInf.GetOut()->GetMinKashida(); + if ( nFontMinKashida && nKashidasInAttr > 0 && SwScriptInfo::IsArabicText( rInf.GetText(), nIdx, nNext - nIdx ) ) + { + sal_Int32 nKashidasDropped = 0; + while ( rKashidas && nGluePortion && nKashidasInAttr > 0 && + nSpaceAdd / SPACING_PRECISION_FACTOR < nFontMinKashida ) + { + --nGluePortion; + --rKashidas; + --nKashidasInAttr; + ++nKashidasDropped; + if( !rKashidas || !nGluePortion ) // nothing left, return false to + return false; // do regular blank justification + + nSpaceAdd = nGluePortionWidth / sal_Int32(nGluePortion); + bAddSpaceChanged = true; + } + if( nKashidasDropped ) + rSI.MarkKashidasInvalid( nKashidasDropped, nIdx, nNext - nIdx ); + } + if ( bAddSpaceChanged ) + break; // start all over again + nIdx = nNext; + } + if ( !bAddSpaceChanged ) + break; // everything was OK + } + return true; +} + +// CalcNewBlock() must only be called _after_ CalcLine()! +// We always span between two RandPortions or FixPortions (Tabs and Flys). +// We count the Glues and call ExpandBlock. +void SwTextAdjuster::CalcNewBlock( SwLineLayout *pCurrent, + const SwLinePortion *pStopAt, SwTwips nReal, bool bSkipKashida ) +{ + OSL_ENSURE( GetInfo().IsMulti() || SvxAdjust::Block == GetAdjust(), + "CalcNewBlock: Why?" ); + OSL_ENSURE( pCurrent->Height(), "SwTextAdjuster::CalcBlockAdjust: missing CalcLine()" ); + + pCurrent->InitSpaceAdd(); + TextFrameIndex nGluePortion(0); + TextFrameIndex nCharCnt(0); + sal_uInt16 nSpaceIdx = 0; + + // i60591: hennerdrews + SwScriptInfo& rSI = GetInfo().GetParaPortion()->GetScriptInfo(); + SwTextSizeInfo aInf ( GetTextFrame() ); + SwTextIter aItr ( GetTextFrame(), &aInf ); + + if ( rSI.CountKashida() ) + { + while (aItr.GetCurr() != pCurrent && aItr.GetNext()) + aItr.Next(); + + if( bSkipKashida ) + { + rSI.SetNoKashidaLine ( aItr.GetStart(), aItr.GetLength()); + } + else + { + rSI.ClearKashidaInvalid ( aItr.GetStart(), aItr.GetLength() ); + rSI.ClearNoKashidaLine( aItr.GetStart(), aItr.GetLength() ); + } + } + + // Do not forget: CalcRightMargin() sets pCurrent->Width() to the line width! + if (!bSkipKashida) + CalcRightMargin( pCurrent, nReal ); + + // #i49277# + const bool bDoNotJustifyLinesWithManualBreak = + GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::DO_NOT_JUSTIFY_LINES_WITH_MANUAL_BREAK); + bool bDoNotJustifyTab = false; + + SwLinePortion *pPos = pCurrent->GetNextPortion(); + // calculate real text width for shrinking + tools::Long nBreakWidth = 0; + + while( pPos ) + { + if ( ( bDoNotJustifyLinesWithManualBreak || bDoNotJustifyTab ) && + pPos->IsBreakPortion() && !IsLastBlock() ) + { + pCurrent->FinishSpaceAdd(); + break; + } + + switch ( pPos->GetWhichPor() ) + { + case PortionType::TabCenter : + case PortionType::TabRight : + case PortionType::TabDecimal : + bDoNotJustifyTab = true; + break; + case PortionType::TabLeft : + case PortionType::Break: + bDoNotJustifyTab = false; + break; + default: break; + } + + if ( pPos->InTextGrp() ) + nGluePortion = nGluePortion + static_cast<SwTextPortion*>(pPos)->GetSpaceCnt( GetInfo(), nCharCnt ); + else if( pPos->IsMultiPortion() ) + { + SwMultiPortion* pMulti = static_cast<SwMultiPortion*>(pPos); + // a multiportion with a tabulator inside breaks the text adjustment + // a ruby portion will not be stretched by text adjustment + // a double line portion takes additional space for each blank + // in the wider line + if( pMulti->HasTabulator() ) + { + if ( nSpaceIdx == pCurrent->GetLLSpaceAddCount() ) + pCurrent->SetLLSpaceAdd( 0, nSpaceIdx ); + + nSpaceIdx++; + nGluePortion = TextFrameIndex(0); + nCharCnt = TextFrameIndex(0); + } + else if( pMulti->IsDouble() ) + nGluePortion = nGluePortion + static_cast<SwDoubleLinePortion*>(pMulti)->GetSpaceCnt(); + else if ( pMulti->IsBidi() ) + nGluePortion = nGluePortion + static_cast<SwBidiPortion*>(pMulti)->GetSpaceCnt( GetInfo() ); // i60594 + } + + if( pPos->InGlueGrp() ) + { + if( pPos->InFixMargGrp() ) + { + if ( nSpaceIdx == pCurrent->GetLLSpaceAddCount() ) + pCurrent->SetLLSpaceAdd( 0, nSpaceIdx ); + + const tools::Long nGluePortionWidth = static_cast<SwGluePortion*>(pPos)->GetPrtGlue() * + SPACING_PRECISION_FACTOR; + + sal_Int32 nKashidas = 0; + if( nGluePortion && rSI.CountKashida() && !bSkipKashida ) + { + // kashida positions found in SwScriptInfo are not necessarily valid in every font + // if two characters are replaced by a ligature glyph, there will be no place for a kashida + if ( !lcl_CheckKashidaPositions ( rSI, aInf, aItr, nKashidas, nGluePortion )) + { + // all kashida positions are invalid + // do regular blank justification + pCurrent->FinishSpaceAdd(); + GetInfo().SetIdx( m_nStart ); + CalcNewBlock( pCurrent, pStopAt, nReal, true ); + return; + } + } + + if( nGluePortion ) + { + tools::Long nSpaceAdd = nGluePortionWidth / sal_Int32(nGluePortion); + // shrink, if portions exceed the line width + tools::Long nSpaceSub = ( nBreakWidth > pCurrent->Width() ) + ? (nBreakWidth - pCurrent->Width()) * SPACING_PRECISION_FACTOR / + sal_Int32(nGluePortion) + LONG_MAX/2 + : 0; + + // i60594 + if( rSI.CountKashida() && !bSkipKashida ) + { + if( !lcl_CheckKashidaWidth( rSI, aInf, aItr, nKashidas, nGluePortion, nGluePortionWidth, nSpaceAdd )) + { + // no kashidas left + // do regular blank justification + pCurrent->FinishSpaceAdd(); + GetInfo().SetIdx( m_nStart ); + CalcNewBlock( pCurrent, pStopAt, nReal, true ); + return; + } + } + + pCurrent->SetLLSpaceAdd( nSpaceSub ? nSpaceSub : nSpaceAdd, nSpaceIdx ); + pPos->Width( static_cast<SwGluePortion*>(pPos)->GetFixWidth() ); + } + else if (IsOneBlock() && nCharCnt > TextFrameIndex(1)) + { + const tools::Long nSpaceAdd = - nGluePortionWidth / (sal_Int32(nCharCnt) - 1); + pCurrent->SetLLSpaceAdd( nSpaceAdd, nSpaceIdx ); + pPos->Width( static_cast<SwGluePortion*>(pPos)->GetFixWidth() ); + } + + nSpaceIdx++; + nGluePortion = TextFrameIndex(0); + nCharCnt = TextFrameIndex(0); + } + else + ++nGluePortion; + } + else + { + nBreakWidth += pPos->Width(); + } + GetInfo().SetIdx( GetInfo().GetIdx() + pPos->GetLen() ); + if ( pPos == pStopAt ) + { + pCurrent->SetLLSpaceAdd( 0, nSpaceIdx ); + break; + } + pPos = pPos->GetNextPortion(); + } +} + +SwTwips SwTextAdjuster::CalcKanaAdj( SwLineLayout* pCurrent ) +{ + OSL_ENSURE( pCurrent->Height(), "SwTextAdjuster::CalcBlockAdjust: missing CalcLine()" ); + OSL_ENSURE( !pCurrent->GetpKanaComp(), "pKanaComp already exists!!" ); + + pCurrent->SetKanaComp( std::make_unique<std::deque<sal_uInt16>>() ); + + const sal_uInt16 nNull = 0; + size_t nKanaIdx = 0; + tools::Long nKanaDiffSum = 0; + SwTwips nRepaintOfst = 0; + SwTwips nX = 0; + bool bNoCompression = false; + + // Do not forget: CalcRightMargin() sets pCurrent->Width() to the line width! + CalcRightMargin( pCurrent ); + + SwLinePortion* pPos = pCurrent->GetNextPortion(); + + while( pPos ) + { + if ( pPos->InTextGrp() ) + { + // get maximum portion width from info structure, calculated + // during text formatting + sal_uInt16 nMaxWidthDiff = GetInfo().GetMaxWidthDiff( pPos ); + + // check, if information is stored under other key + if ( !nMaxWidthDiff && pPos == pCurrent->GetFirstPortion() ) + nMaxWidthDiff = GetInfo().GetMaxWidthDiff( pCurrent ); + + // calculate difference between portion width and max. width + nKanaDiffSum += nMaxWidthDiff; + + // we store the beginning of the first compressible portion + // for repaint + if ( nMaxWidthDiff && !nRepaintOfst ) + nRepaintOfst = nX + GetLeftMargin(); + } + else if( pPos->InGlueGrp() && pPos->InFixMargGrp() ) + { + if ( nKanaIdx == pCurrent->GetKanaComp().size() ) + pCurrent->GetKanaComp().push_back( nNull ); + + tools::Long nRest; + + if ( pPos->InTabGrp() ) + { + nRest = ! bNoCompression && + ( pPos->Width() > MIN_TAB_WIDTH ) ? + pPos->Width() - MIN_TAB_WIDTH : + 0; + + // for simplifying the handling of left, right ... tabs, + // we do expand portions, which are lying behind + // those special tabs + bNoCompression = !pPos->IsTabLeftPortion(); + } + else + { + nRest = ! bNoCompression ? + static_cast<SwGluePortion*>(pPos)->GetPrtGlue() : + 0; + + bNoCompression = false; + } + + if( nKanaDiffSum ) + { + sal_uLong nCompress = ( 10000 * nRest ) / nKanaDiffSum; + + if ( nCompress >= 10000 ) + // kanas can be expanded to 100%, and there is still + // some space remaining + nCompress = 0; + + else + nCompress = 10000 - nCompress; + + ( pCurrent->GetKanaComp() )[ nKanaIdx ] = o3tl::narrowing<sal_uInt16>(nCompress); + nKanaDiffSum = 0; + } + + nKanaIdx++; + } + + nX += pPos->Width(); + pPos = pPos->GetNextPortion(); + } + + // set portion width + nKanaIdx = 0; + sal_uInt16 nCompress = ( pCurrent->GetKanaComp() )[ nKanaIdx ]; + pPos = pCurrent->GetNextPortion(); + tools::Long nDecompress = 0; + + while( pPos ) + { + if ( pPos->InTextGrp() ) + { + const SwTwips nMinWidth = pPos->Width(); + + // get maximum portion width from info structure, calculated + // during text formatting + SwTwips nMaxWidthDiff = GetInfo().GetMaxWidthDiff( pPos ); + + // check, if information is stored under other key + if ( !nMaxWidthDiff && pPos == pCurrent->GetFirstPortion() ) + nMaxWidthDiff = GetInfo().GetMaxWidthDiff( pCurrent ); + pPos->Width( nMinWidth + + ( ( 10000 - nCompress ) * nMaxWidthDiff ) / 10000 ); + nDecompress += pPos->Width() - nMinWidth; + } + else if( pPos->InGlueGrp() && pPos->InFixMargGrp() ) + { + pPos->Width(pPos->Width() - nDecompress); + + if ( pPos->InTabGrp() ) + // set fix width to width + static_cast<SwTabPortion*>(pPos)->SetFixWidth( pPos->Width() ); + + if ( ++nKanaIdx < pCurrent->GetKanaComp().size() ) + nCompress = ( pCurrent->GetKanaComp() )[ nKanaIdx ]; + + nDecompress = 0; + } + pPos = pPos->GetNextPortion(); + } + + return nRepaintOfst; +} + +SwMarginPortion *SwTextAdjuster::CalcRightMargin( SwLineLayout *pCurrent, + SwTwips nReal ) +{ + tools::Long nRealWidth; + const sal_uInt16 nRealHeight = GetLineHeight(); + const sal_uInt16 nLineHeight = pCurrent->Height(); + + sal_uInt16 nPrtWidth = pCurrent->PrtWidth(); + SwLinePortion *pLast = pCurrent->FindLastPortion(); + + if( GetInfo().IsMulti() ) + nRealWidth = nReal; + else + { + nRealWidth = GetLineWidth(); + // For each FlyFrame extending into the right margin, we create a FlyPortion. + const tools::Long nLeftMar = GetLeftMargin(); + SwRect aCurrRect( nLeftMar + nPrtWidth, Y() + nRealHeight - nLineHeight, + nRealWidth - nPrtWidth, nLineHeight ); + + SwFlyPortion *pFly = CalcFlyPortion( nRealWidth, aCurrRect ); + while( pFly && tools::Long( nPrtWidth )< nRealWidth ) + { + pLast->Append( pFly ); + pLast = pFly; + if( pFly->GetFix() > nPrtWidth ) + pFly->Width( ( pFly->GetFix() - nPrtWidth) + pFly->Width() + 1); + nPrtWidth += pFly->Width() + 1; + aCurrRect.Left( nLeftMar + nPrtWidth ); + pFly = CalcFlyPortion( nRealWidth, aCurrRect ); + } + delete pFly; + } + + SwMarginPortion *pRight = new SwMarginPortion; + pLast->Append( pRight ); + + if( tools::Long( nPrtWidth )< nRealWidth ) + pRight->PrtWidth( sal_uInt16( nRealWidth - nPrtWidth ) ); + + // pCurrent->Width() is set to the real size, because we attach the + // MarginPortions. + // This trick gives miraculous results: + // If pCurrent->Width() == nRealWidth, then the adjustment gets overruled + // implicitly. GetLeftMarginAdjust() and IsJustified() think they have a + // line filled with chars. + + pCurrent->PrtWidth( sal_uInt16( nRealWidth ) ); + return pRight; +} + +void SwTextAdjuster::CalcFlyAdjust( SwLineLayout *pCurrent ) +{ + // 1) We insert a left margin: + SwMarginPortion *pLeft = pCurrent->CalcLeftMargin(); + SwGluePortion *pGlue = pLeft; // the last GluePortion + + // 2) We attach a right margin: + // CalcRightMargin also calculates a possible overlap with FlyFrames. + CalcRightMargin( pCurrent ); + + SwLinePortion *pPos = pLeft->GetNextPortion(); + TextFrameIndex nLen(0); + + // If we only have one line, the text portion is consecutive and we center, then ... + bool bComplete = TextFrameIndex(0) == m_nStart; + const bool bTabCompat = GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::TAB_COMPAT); + bool bMultiTab = false; + + while( pPos ) + { + if ( pPos->IsMultiPortion() && static_cast<SwMultiPortion*>(pPos)->HasTabulator() ) + bMultiTab = true; + else if( pPos->InFixMargGrp() && + ( bTabCompat ? ! pPos->InTabGrp() : ! bMultiTab ) ) + { + // in tab compat mode we do not want to change tab portions + // in non tab compat mode we do not want to change margins if we + // found a multi portion with tabs + if( SvxAdjust::Right == GetAdjust() ) + static_cast<SwGluePortion*>(pPos)->MoveAllGlue( pGlue ); + else + { + // We set the first text portion to right-aligned and the last one + // to left-aligned. + // The first text portion gets the whole Glue, but only if we have + // more than one line. + if (bComplete && TextFrameIndex(GetInfo().GetText().getLength()) == nLen) + static_cast<SwGluePortion*>(pPos)->MoveHalfGlue( pGlue ); + else + { + if ( ! bTabCompat ) + { + if( pLeft == pGlue ) + { + // If we only have a left and right margin, the + // margins share the Glue. + if( nLen + pPos->GetLen() >= pCurrent->GetLen() ) + static_cast<SwGluePortion*>(pPos)->MoveHalfGlue( pGlue ); + else + static_cast<SwGluePortion*>(pPos)->MoveAllGlue( pGlue ); + } + else + { + // The last text portion retains its Glue. + if( !pPos->IsMarginPortion() ) + static_cast<SwGluePortion*>(pPos)->MoveHalfGlue( pGlue ); + } + } + else + static_cast<SwGluePortion*>(pPos)->MoveHalfGlue( pGlue ); + } + } + + pGlue = static_cast<SwGluePortion*>(pPos); + bComplete = false; + } + nLen = nLen + pPos->GetLen(); + pPos = pPos->GetNextPortion(); + } + + if( ! bTabCompat && ! bMultiTab && SvxAdjust::Right == GetAdjust() ) + // portions are moved to the right if possible + pLeft->AdjustRight( pCurrent ); +} + +void SwTextAdjuster::CalcAdjLine( SwLineLayout *pCurrent ) +{ + OSL_ENSURE( pCurrent->IsFormatAdj(), "CalcAdjLine: Why?" ); + + pCurrent->SetFormatAdj(false); + + SwParaPortion* pPara = GetInfo().GetParaPortion(); + + switch( GetAdjust() ) + { + case SvxAdjust::Right: + case SvxAdjust::Center: + { + CalcFlyAdjust( pCurrent ); + pPara->GetRepaint().SetOffset( 0 ); + break; + } + case SvxAdjust::Block: + { + FormatBlock(); + break; + } + default : return; + } +} + +// This is a quite complicated calculation: nCurrWidth is the width _before_ +// adding the word, that still fits onto the line! For this reason the FlyPortion's +// width is still correct if we get a deadlock-situation of: +// bFirstWord && !WORDFITS +SwFlyPortion *SwTextAdjuster::CalcFlyPortion( const tools::Long nRealWidth, + const SwRect &rCurrRect ) +{ + SwTextFly aTextFly( GetTextFrame() ); + + const sal_uInt16 nCurrWidth = m_pCurr->PrtWidth(); + SwFlyPortion *pFlyPortion = nullptr; + + SwRect aLineVert( rCurrRect ); + if ( GetTextFrame()->IsRightToLeft() ) + GetTextFrame()->SwitchLTRtoRTL( aLineVert ); + if ( GetTextFrame()->IsVertical() ) + GetTextFrame()->SwitchHorizontalToVertical( aLineVert ); + + // aFlyRect is document-global! + SwRect aFlyRect( aTextFly.GetFrame( aLineVert ) ); + + if ( GetTextFrame()->IsRightToLeft() ) + GetTextFrame()->SwitchRTLtoLTR( aFlyRect ); + if ( GetTextFrame()->IsVertical() ) + GetTextFrame()->SwitchVerticalToHorizontal( aFlyRect ); + + // If a Frame overlapps we open a Portion + if( aFlyRect.HasArea() ) + { + // aLocal is frame-local + SwRect aLocal( aFlyRect ); + aLocal.Pos( aLocal.Left() - GetLeftMargin(), aLocal.Top() ); + if( nCurrWidth > aLocal.Left() ) + aLocal.Left( nCurrWidth ); + + // If the rect is wider than the line, we adjust it to the right size + const tools::Long nLocalWidth = aLocal.Left() + aLocal.Width(); + if( nRealWidth < nLocalWidth ) + aLocal.Width( nRealWidth - aLocal.Left() ); + GetInfo().GetParaPortion()->SetFly(); + pFlyPortion = new SwFlyPortion( aLocal ); + pFlyPortion->Height( sal_uInt16( rCurrRect.Height() ) ); + // The Width could be smaller than the FixWidth, thus: + pFlyPortion->AdjFixWidth(); + } + return pFlyPortion; +} + +// CalcDropAdjust is called at the end by Format() if needed +void SwTextAdjuster::CalcDropAdjust() +{ + OSL_ENSURE( 1<GetDropLines() && SvxAdjust::Left!=GetAdjust() && SvxAdjust::Block!=GetAdjust(), + "CalcDropAdjust: No reason for DropAdjustment." ); + + const sal_Int32 nLineNumber = GetLineNr(); + + // 1) Skip dummies + Top(); + + if( !m_pCurr->IsDummy() || NextLine() ) + { + // Adjust first + GetAdjusted(); + + SwLinePortion *pPor = m_pCurr->GetFirstPortion(); + + // 2) Make sure we include the ropPortion + // 3) pLeft is the GluePor preceding the DropPor + if( pPor->InGlueGrp() && pPor->GetNextPortion() + && pPor->GetNextPortion()->IsDropPortion() ) + { + const SwLinePortion *pDropPor = pPor->GetNextPortion(); + SwGluePortion *pLeft = static_cast<SwGluePortion*>( pPor ); + + // 4) pRight: Find the GluePor coming after the DropPor + pPor = pPor->GetNextPortion(); + while( pPor && !pPor->InFixMargGrp() ) + pPor = pPor->GetNextPortion(); + + SwGluePortion *pRight = ( pPor && pPor->InGlueGrp() ) ? + static_cast<SwGluePortion*>(pPor) : nullptr; + if( pRight && pRight != pLeft ) + { + // 5) Calculate nMinLeft. Who is the most to left? + const auto nDropLineStart = + GetLineStart() + pLeft->Width() + pDropPor->Width(); + auto nMinLeft = nDropLineStart; + for( sal_Int32 i = 1; i < GetDropLines(); ++i ) + { + if( NextLine() ) + { + // Adjust first + GetAdjusted(); + + pPor = m_pCurr->GetFirstPortion(); + const SwMarginPortion *pMar = pPor->IsMarginPortion() ? + static_cast<SwMarginPortion*>(pPor) : nullptr; + if( !pMar ) + nMinLeft = 0; + else + { + const auto nLineStart = + GetLineStart() + pMar->Width(); + if( nMinLeft > nLineStart ) + nMinLeft = nLineStart; + } + } + } + + // 6) Distribute the Glue anew between pLeft and pRight + if( nMinLeft < nDropLineStart ) + { + // The Glue is always passed from pLeft to pRight, so that + // the text moves to the left. + const auto nGlue = nDropLineStart - nMinLeft; + if( !nMinLeft ) + pLeft->MoveAllGlue( pRight ); + else + pLeft->MoveGlue( pRight, nGlue ); + } + } + } + } + + if( nLineNumber != GetLineNr() ) + { + Top(); + while( nLineNumber != GetLineNr() && Next() ) + ; + } +} + +void SwTextAdjuster::CalcDropRepaint() +{ + Top(); + SwRepaint &rRepaint = GetInfo().GetParaPortion()->GetRepaint(); + if( rRepaint.Top() > Y() ) + rRepaint.Top( Y() ); + for( sal_Int32 i = 1; i < GetDropLines(); ++i ) + NextLine(); + const SwTwips nBottom = Y() + GetLineHeight() - 1; + if( rRepaint.Bottom() < nBottom ) + rRepaint.Bottom( nBottom ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itratr.cxx b/sw/source/core/text/itratr.cxx new file mode 100644 index 0000000000..24203ecb53 --- /dev/null +++ b/sw/source/core/text/itratr.cxx @@ -0,0 +1,1632 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <algorithm> + +#include <hintids.hxx> +#include <editeng/charscaleitem.hxx> +#include <svl/itemiter.hxx> +#include <svx/svdobj.hxx> +#include <vcl/svapp.hxx> +#include <fmtanchr.hxx> +#include <fmtfsize.hxx> +#include <fmtornt.hxx> +#include <fmtflcnt.hxx> +#include <fmtcntnt.hxx> +#include <fmtftn.hxx> +#include <frmatr.hxx> +#include <frmfmt.hxx> +#include <fmtfld.hxx> +#include <doc.hxx> +#include <IDocumentLayoutAccess.hxx> +#include <txatbase.hxx> +#include <viewsh.hxx> +#include <rootfrm.hxx> +#include <docary.hxx> +#include <ndtxt.hxx> +#include <fldbas.hxx> +#include <pam.hxx> +#include "itratr.hxx" +#include <htmltbl.hxx> +#include <swtable.hxx> +#include "redlnitr.hxx" +#include <redline.hxx> +#include <fmtsrnd.hxx> +#include "itrtxt.hxx" +#include <breakit.hxx> +#include <com/sun/star/i18n/WordType.hpp> +#include <com/sun/star/i18n/XBreakIterator.hpp> +#include <editeng/lrspitem.hxx> +#include <calbck.hxx> +#include <frameformats.hxx> +#include <sortedobjs.hxx> +#include <anchoredobject.hxx> +#include <flyfrm.hxx> +#include <flyfrms.hxx> + +using namespace ::com::sun::star::i18n; +using namespace ::com::sun::star; + +static sal_Int32 GetNextAttrImpl(SwTextNode const* pTextNode, + size_t nStartIndex, size_t nEndIndex, sal_Int32 nPosition); + +SwAttrIter::SwAttrIter(SwTextNode const * pTextNode) + : m_pViewShell(nullptr) + , m_pFont(nullptr) + , m_pScriptInfo(nullptr) + , m_pLastOut(nullptr) + , m_nChgCnt(0) + , m_nStartIndex(0) + , m_nEndIndex(0) + , m_nPosition(0) + , m_nPropFont(0) + , m_pTextNode(pTextNode) + , m_pMergedPara(nullptr) +{ + m_aFontCacheIds[SwFontScript::Latin] = m_aFontCacheIds[SwFontScript::CJK] = m_aFontCacheIds[SwFontScript::CTL] = nullptr; +} + +SwAttrIter::SwAttrIter(SwTextNode& rTextNode, SwScriptInfo& rScrInf, SwTextFrame const*const pFrame) + : m_pViewShell(nullptr) + , m_pFont(nullptr) + , m_pScriptInfo(nullptr) + , m_pLastOut(nullptr) + , m_nChgCnt(0) + , m_nPropFont(0) + , m_pTextNode(&rTextNode) + , m_pMergedPara(nullptr) +{ + CtorInitAttrIter(rTextNode, rScrInf, pFrame); +} + +void SwAttrIter::Chg( SwTextAttr const *pHt ) +{ + assert(pHt && m_pFont && "No attribute of font available for change"); + if( m_pRedline && m_pRedline->IsOn() ) + m_pRedline->ChangeTextAttr( m_pFont, *pHt, true ); + else + m_aAttrHandler.PushAndChg( *pHt, *m_pFont ); + m_nChgCnt++; +} + +void SwAttrIter::Rst( SwTextAttr const *pHt ) +{ + assert(pHt && m_pFont && "No attribute of font available for reset"); + // get top from stack after removing pHt + if( m_pRedline && m_pRedline->IsOn() ) + m_pRedline->ChangeTextAttr( m_pFont, *pHt, false ); + else + m_aAttrHandler.PopAndChg( *pHt, *m_pFont ); + m_nChgCnt--; +} + +SwAttrIter::~SwAttrIter() +{ + m_pRedline.reset(); + delete m_pFont; +} + +bool SwAttrIter::MaybeHasHints() const +{ + return nullptr != m_pTextNode->GetpSwpHints() || nullptr != m_pMergedPara; +} + +/** + * Returns the attribute for a position + * + * Only if the attribute is exactly at the position @param nPos and + * does not have an EndIndex + * + * We need this function for attributes which should alter formatting without + * changing the content of the string. + * Such "degenerated" attributes are e.g.: fields which retain expanded text and + * line-bound Frames. + * In order to avoid ambiguities between different such attributes, we insert a + * special character at the start of the string, when creating such an attribute. + * The Formatter later on encounters such a special character and retrieves the + * degenerate attribute via GetAttr(). + */ +SwTextAttr *SwAttrIter::GetAttr(TextFrameIndex const nPosition) const +{ + std::pair<SwTextNode const*, sal_Int32> const pos( m_pMergedPara + ? sw::MapViewToModel(*m_pMergedPara, nPosition) + : std::make_pair(m_pTextNode, sal_Int32(nPosition))); + return pos.first->GetTextAttrForCharAt(pos.second); +} + +bool SwAttrIter::SeekAndChgAttrIter(TextFrameIndex const nNewPos, OutputDevice* pOut) +{ + std::pair<SwTextNode const*, sal_Int32> const pos( m_pMergedPara + ? sw::MapViewToModel(*m_pMergedPara, nNewPos) + : std::make_pair(m_pTextNode, sal_Int32(nNewPos))); + bool bChg = m_nStartIndex && pos.first == m_pTextNode && pos.second == m_nPosition + ? m_pFont->IsFntChg() + : Seek( nNewPos ); + if ( m_pLastOut.get() != pOut ) + { + m_pLastOut = pOut; + m_pFont->SetFntChg( true ); + bChg = true; + } + if( bChg ) + { + // if the change counter is zero, we know the cache id of the wanted font + if ( !m_nChgCnt && !m_nPropFont ) + m_pFont->SetFontCacheId( m_aFontCacheIds[ m_pFont->GetActual() ], + m_aFontIdx[ m_pFont->GetActual() ], m_pFont->GetActual() ); + m_pFont->ChgPhysFnt( m_pViewShell, *pOut ); + } + + return bChg; +} + +bool SwAttrIter::IsSymbol(TextFrameIndex const nNewPos) +{ + Seek( nNewPos ); + if ( !m_nChgCnt && !m_nPropFont ) + m_pFont->SetFontCacheId( m_aFontCacheIds[ m_pFont->GetActual() ], + m_aFontIdx[ m_pFont->GetActual() ], m_pFont->GetActual() ); + return m_pFont->IsSymbol( m_pViewShell ); +} + +bool SwTextFrame::IsSymbolAt(TextFrameIndex const nPos) const +{ + SwTextInfo info(const_cast<SwTextFrame*>(this)); + SwTextIter iter(const_cast<SwTextFrame*>(this), &info); + return iter.IsSymbol(nPos); +} + +bool SwAttrIter::SeekStartAndChgAttrIter( OutputDevice* pOut, const bool bParaFont ) +{ + SwTextNode const*const pFirstTextNode(m_pMergedPara ? m_pMergedPara->pFirstNode : m_pTextNode); + if ( m_pRedline && m_pRedline->ExtOn() ) + m_pRedline->LeaveExtend(*m_pFont, pFirstTextNode->GetIndex(), 0); + + if (m_pTextNode != pFirstTextNode) + { + assert(m_pMergedPara); + m_pTextNode = m_pMergedPara->pFirstNode; + InitFontAndAttrHandler(*m_pMergedPara->pParaPropsNode, *m_pTextNode, + m_pMergedPara->mergedText, nullptr, nullptr); + } + + // reset font to its original state + m_aAttrHandler.Reset(); + m_aAttrHandler.ResetFont( *m_pFont ); + + m_nStartIndex = 0; + m_nEndIndex = 0; + m_nPosition = 0; + m_nChgCnt = 0; + if( m_nPropFont ) + m_pFont->SetProportion( m_nPropFont ); + if( m_pRedline ) + { + m_pRedline->Clear( m_pFont ); + if( !bParaFont ) + m_nChgCnt = m_nChgCnt + m_pRedline->Seek(*m_pFont, pFirstTextNode->GetIndex(), 0, COMPLETE_STRING); + else + m_pRedline->Reset(); + } + + SwpHints const*const pHints(m_pTextNode->GetpSwpHints()); + if (pHints && !bParaFont) + { + SwTextAttr *pTextAttr; + // While we've not reached the end of the StartArray && the TextAttribute starts at position 0... + while ((m_nStartIndex < pHints->Count()) && + !((pTextAttr = pHints->Get(m_nStartIndex))->GetStart())) + { + // open the TextAttributes + Chg( pTextAttr ); + m_nStartIndex++; + } + } + + bool bChg = m_pFont->IsFntChg(); + if ( m_pLastOut.get() != pOut ) + { + m_pLastOut = pOut; + m_pFont->SetFntChg( true ); + bChg = true; + } + if( bChg ) + { + // if the application counter is zero, we know the cache id of the wanted font + if ( !m_nChgCnt && !m_nPropFont ) + m_pFont->SetFontCacheId( m_aFontCacheIds[ m_pFont->GetActual() ], + m_aFontIdx[ m_pFont->GetActual() ], m_pFont->GetActual() ); + m_pFont->ChgPhysFnt( m_pViewShell, *pOut ); + } + return bChg; +} + +// AMA: New AttrIter Nov 94 +void SwAttrIter::SeekFwd(const sal_Int32 nOldPos, const sal_Int32 nNewPos) +{ + SwpHints const*const pHints(m_pTextNode->GetpSwpHints()); + SwTextAttr *pTextAttr; + const auto nHintsCount = pHints->Count(); + + if ( m_nStartIndex ) // If attributes have been opened at all ... + { + // Close attributes that are currently open, but stop at nNewPos+1 + + // As long as we've not yet reached the end of EndArray and the + // TextAttribute ends before or at the new position ... + while ((m_nEndIndex < nHintsCount) && + ((pTextAttr = pHints->GetSortedByEnd(m_nEndIndex))->GetAnyEnd() <= nNewPos)) + { + // Close the TextAttributes, whose StartPos were before or at + // the old nPos and are currently open + if (pTextAttr->GetStart() <= nOldPos) Rst( pTextAttr ); + m_nEndIndex++; + } + } + else // skip the not opened ends + { + while ((m_nEndIndex < nHintsCount) && + (pHints->GetSortedByEnd(m_nEndIndex)->GetAnyEnd() <= nNewPos)) + { + m_nEndIndex++; + } + } + + // As long as we've not yet reached the end of EndArray and the + // TextAttribute ends before or at the new position... + while ((m_nStartIndex < nHintsCount) && + ((pTextAttr = pHints->Get(m_nStartIndex))->GetStart() <= nNewPos)) + { + + // open the TextAttributes, whose ends lie behind the new position + if ( pTextAttr->GetAnyEnd() > nNewPos ) Chg( pTextAttr ); + m_nStartIndex++; + } + +} + +bool SwAttrIter::Seek(TextFrameIndex const nNewPos) +{ + // note: nNewPos isn't necessarily an index returned from GetNextAttr + std::pair<SwTextNode const*, sal_Int32> const newPos( m_pMergedPara + ? sw::MapViewToModel(*m_pMergedPara, nNewPos) + : std::make_pair(m_pTextNode, sal_Int32(nNewPos))); + + if ( m_pRedline && m_pRedline->ExtOn() ) + m_pRedline->LeaveExtend(*m_pFont, newPos.first->GetIndex(), newPos.second); + if (m_pTextNode->GetIndex() < newPos.first->GetIndex()) + { + // Skipping to a different node - first seek until the end of this node + // to get rid of all hint items + if (m_pTextNode->GetpSwpHints()) + { + sal_Int32 nPos(m_nPosition); + do + { + sal_Int32 const nOldPos(nPos); + nPos = GetNextAttrImpl(m_pTextNode, m_nStartIndex, m_nEndIndex, nPos); + if (nPos <= m_pTextNode->Len()) + { + SeekFwd(nOldPos, nPos); + } + else + { + SeekFwd(nOldPos, m_pTextNode->Len()); + } + } + while (nPos < m_pTextNode->Len()); + } + // Unapply current para items: + // the SwAttrHandler doesn't appear to be capable of *unapplying* + // items at all; it can only apply a previously effective item. + // So do this by recreating the font from scratch. + // Apply new para items: + assert(m_pMergedPara); + InitFontAndAttrHandler(*m_pMergedPara->pParaPropsNode, *newPos.first, + m_pMergedPara->mergedText, nullptr, nullptr); + // reset to next + m_pTextNode = newPos.first; + m_nStartIndex = 0; + m_nEndIndex = 0; + m_nPosition = 0; + assert(m_pRedline); + } + + // sw_redlinehide: Seek(0) must move before the first character, which + // has a special case where the first node starts with delete redline. + if ((!nNewPos && !m_pMergedPara) + || newPos.first != m_pTextNode + || newPos.second < m_nPosition) + { + if (m_pMergedPara) + { + if (m_pTextNode != newPos.first) + { + m_pTextNode = newPos.first; + // sw_redlinehide: hope it's okay to use the current text node + // here; the AttrHandler shouldn't care about non-char items + InitFontAndAttrHandler(*m_pMergedPara->pParaPropsNode, *m_pTextNode, + m_pMergedPara->mergedText, nullptr, nullptr); + } + } + if (m_pMergedPara || m_pTextNode->GetpSwpHints()) + { + if( m_pRedline ) + m_pRedline->Clear( nullptr ); + + // reset font to its original state + m_aAttrHandler.Reset(); + m_aAttrHandler.ResetFont( *m_pFont ); + + if( m_nPropFont ) + m_pFont->SetProportion( m_nPropFont ); + m_nStartIndex = 0; + m_nEndIndex = 0; + m_nPosition = 0; + m_nChgCnt = 0; + + // Attention! + // resetting the font here makes it necessary to apply any + // changes for extended input directly to the font + if ( m_pRedline && m_pRedline->ExtOn() ) + { + m_pRedline->UpdateExtFont( *m_pFont ); + ++m_nChgCnt; + } + } + } + + if (m_pTextNode->GetpSwpHints()) + { + if (m_pMergedPara) + { + // iterate hint by hint: SeekFwd does not mix ends and starts, + // it always applies all the starts last, so it must be called once + // per position where hints start/end! + sal_Int32 nPos(m_nPosition); + do + { + sal_Int32 const nOldPos(nPos); + nPos = GetNextAttrImpl(m_pTextNode, m_nStartIndex, m_nEndIndex, nPos); + if (nPos <= newPos.second) + { + SeekFwd(nOldPos, nPos); + } + else + { + SeekFwd(nOldPos, newPos.second); + } + } + while (nPos < newPos.second); + } + else + { + SeekFwd(m_nPosition, newPos.second); + } + } + + m_pFont->SetActual( m_pScriptInfo->WhichFont(nNewPos) ); + + if( m_pRedline ) + m_nChgCnt = m_nChgCnt + m_pRedline->Seek(*m_pFont, m_pTextNode->GetIndex(), newPos.second, m_nPosition); + m_nPosition = newPos.second; + + if( m_nPropFont ) + m_pFont->SetProportion( m_nPropFont ); + + return m_pFont->IsFntChg(); +} + +static void InsertCharAttrs(SfxPoolItem const** pAttrs, SfxItemSet const& rItems) +{ + SfxItemIter iter(rItems); + for (SfxPoolItem const* pItem = iter.GetCurItem(); pItem; pItem = iter.NextItem()) + { + auto const nWhich(pItem->Which()); + if (isCHRATR(nWhich) && RES_CHRATR_RSID != nWhich) + { + pAttrs[nWhich - RES_CHRATR_BEGIN] = pItem; + } + else if (nWhich == RES_TXTATR_UNKNOWN_CONTAINER) + { + pAttrs[RES_CHRATR_END - RES_CHRATR_BEGIN] = pItem; + } + } +} + +// if return false: portion ends at start of redline, indexes unchanged +// if return true: portion end not known (past end of redline), indexes point to first hint past end of redline +static bool CanSkipOverRedline( + SwTextNode const& rStartNode, sal_Int32 const nStartRedline, + SwRangeRedline const& rRedline, + size_t & rStartIndex, size_t & rEndIndex, + bool const isTheAnswerYes) +{ + size_t nStartIndex(rStartIndex); + size_t nEndIndex(rEndIndex); + SwPosition const*const pRLEnd(rRedline.End()); + if (!pRLEnd->GetNode().IsTextNode() // if fully deleted... + || pRLEnd->GetContentIndex() == pRLEnd->GetNode().GetTextNode()->Len()) + { + // shortcut: nothing follows redline + // current state is end state + return false; + } + std::vector<SwTextAttr*> activeCharFmts; + // can't compare the SwFont that's stored somewhere, it doesn't have compare + // operator, so try to recreate the situation with some temp arrays here + SfxPoolItem const* activeCharAttrsStart[RES_CHRATR_END - RES_CHRATR_BEGIN + 1] = { nullptr, }; + if (rStartNode != pRLEnd->GetNode()) + { // nodes' attributes are only needed if there are different nodes + InsertCharAttrs(activeCharAttrsStart, rStartNode.GetSwAttrSet()); + } + if (SwpHints const*const pStartHints = rStartNode.GetpSwpHints()) + { + // check hint ends of hints that start before and end within + sal_Int32 const nRedlineEnd(rStartNode == pRLEnd->GetNode() + ? pRLEnd->GetContentIndex() + : rStartNode.Len()); + for ( ; nEndIndex < pStartHints->Count(); ++nEndIndex) + { + SwTextAttr *const pAttr(pStartHints->GetSortedByEnd(nEndIndex)); + if (!pAttr->End()) + { + continue; + } + if (nRedlineEnd < *pAttr->End()) + { + break; + } + if (nStartRedline <= pAttr->GetStart()) + { + continue; + } + if (pAttr->IsFormatIgnoreEnd()) + { + continue; + } + switch (pAttr->Which()) + { + // if any of these ends inside RL then we need a new portion + case RES_TXTATR_REFMARK: + case RES_TXTATR_TOXMARK: + case RES_TXTATR_META: // actually these 2 aren't allowed to overlap ??? + case RES_TXTATR_METAFIELD: + case RES_TXTATR_INETFMT: + case RES_TXTATR_CJK_RUBY: + case RES_TXTATR_INPUTFIELD: + case RES_TXTATR_CONTENTCONTROL: + { + if (!isTheAnswerYes) return false; // always break + } + break; + // these are guaranteed not to overlap + // and come in order of application + case RES_TXTATR_AUTOFMT: + case RES_TXTATR_CHARFMT: + { + if (pAttr->Which() == RES_TXTATR_CHARFMT) + { + activeCharFmts.push_back(pAttr); + } + // pure formatting hints may end inside the redline & + // start again inside the redline, which must not cause + // a new text portion if they have the same items - so + // store the effective items & compare all at the end + SfxItemSet const& rSet((pAttr->Which() == RES_TXTATR_CHARFMT) + ? static_cast<SfxItemSet const&>(pAttr->GetCharFormat().GetCharFormat()->GetAttrSet()) + : *pAttr->GetAutoFormat().GetStyleHandle()); + InsertCharAttrs(activeCharAttrsStart, rSet); + } + break; + // SwTextNode::SetAttr puts it into AUTOFMT which is quite + // sensible so it doesn't actually exist as a hint + case RES_TXTATR_UNKNOWN_CONTAINER: + default: assert(false); + } + } + assert(nEndIndex == pStartHints->Count() || + pRLEnd->GetContentIndex() < pStartHints->GetSortedByEnd(nEndIndex)->GetAnyEnd()); + } + + if (rStartNode != pRLEnd->GetNode()) + { + nStartIndex = 0; + nEndIndex = 0; + } + + // treat para properties as text properties + // ... with the FormatToTextAttr we get autofmts that correspond to the *effective* attr set difference + // effective attr set: para props + charfmts + autofmt *in that order* + // ... and the charfmt must be *nominally* the same + + SfxPoolItem const* activeCharAttrsEnd[RES_CHRATR_END - RES_CHRATR_BEGIN + 1] = { nullptr, }; + if (rStartNode != pRLEnd->GetNode()) + { // nodes' attributes are only needed if there are different nodes + InsertCharAttrs(activeCharAttrsEnd, + pRLEnd->GetNode().GetTextNode()->GetSwAttrSet()); + } + + if (SwpHints *const pEndHints = pRLEnd->GetNode().GetTextNode()->GetpSwpHints()) + { + // check hint starts of hints that start within and end after +#ifndef NDEBUG + sal_Int32 const nRedlineStart(rStartNode == pRLEnd->GetNode() + ? nStartRedline + : 0); +#endif + for ( ; nStartIndex < pEndHints->Count(); ++nStartIndex) + { + SwTextAttr *const pAttr(pEndHints->Get(nStartIndex)); + // compare with < here, not <=, to get the effective formatting + // of the 1st char after the redline; should not cause problems + // with consecutive delete redlines because those are handed by + // GetNextRedln() and here we have the last end pos. + if (pRLEnd->GetContentIndex() < pAttr->GetStart()) + { + break; + } + if (!pAttr->End()) + continue; + if (pAttr->IsFormatIgnoreStart()) + { + continue; + } + assert(nRedlineStart <= pAttr->GetStart()); // we wouldn't be here otherwise? + if (*pAttr->End() <= pRLEnd->GetContentIndex()) + { + continue; + } + switch (pAttr->Which()) + { + case RES_TXTATR_REFMARK: + case RES_TXTATR_TOXMARK: + case RES_TXTATR_META: // actually these 2 aren't allowed to overlap ??? + case RES_TXTATR_METAFIELD: + case RES_TXTATR_INETFMT: + case RES_TXTATR_CJK_RUBY: + case RES_TXTATR_INPUTFIELD: + case RES_TXTATR_CONTENTCONTROL: + { + if (!isTheAnswerYes) return false; + } + break; + case RES_TXTATR_AUTOFMT: + case RES_TXTATR_CHARFMT: + { + // char formats must be *nominally* the same + if (pAttr->Which() == RES_TXTATR_CHARFMT) + { + auto iter = std::find_if(activeCharFmts.begin(), activeCharFmts.end(), + [&pAttr](const SwTextAttr* pCharFmt) { return *pCharFmt == *pAttr; }); + if (iter != activeCharFmts.end()) + activeCharFmts.erase(iter); + else if (!isTheAnswerYes) + return false; + } + SfxItemSet const& rSet((pAttr->Which() == RES_TXTATR_CHARFMT) + ? static_cast<SfxItemSet const&>(pAttr->GetCharFormat().GetCharFormat()->GetAttrSet()) + : *pAttr->GetAutoFormat().GetStyleHandle()); + InsertCharAttrs(activeCharAttrsEnd, rSet); + + } + break; + // SwTextNode::SetAttr puts it into AUTOFMT which is quite + // sensible so it doesn't actually exist as a hint + case RES_TXTATR_UNKNOWN_CONTAINER: + default: assert(false); + } + } + if (rStartNode != pRLEnd->GetNode()) + { + // need to iterate the nEndIndex forward too so the loop in the + // caller can look for the right ends in the next iteration + for (nEndIndex = 0; nEndIndex < pEndHints->Count(); ++nEndIndex) + { + SwTextAttr *const pAttr(pEndHints->GetSortedByEnd(nEndIndex)); + if (!pAttr->End()) + continue; + if (pRLEnd->GetContentIndex() < *pAttr->End()) + { + break; + } + } + } + } + + // if we didn't find a matching start for any end, then it really ends inside + if (!activeCharFmts.empty()) + { + if (!isTheAnswerYes) return false; + } + for (size_t i = 0; i < SAL_N_ELEMENTS(activeCharAttrsStart); ++i) + { + // all of these should be shareable (but we have no SfxItemPool to check it here) + // assert(!activeCharAttrsStart[i] || activeCharAttrsStart[i]->GetItemPool()->Shareable(*activeCharAttrsStart[i])); + if (!SfxPoolItem::areSame(activeCharAttrsStart[i], activeCharAttrsEnd[i])) + { + if (!isTheAnswerYes) return false; + } + } + rStartIndex = nStartIndex; + rEndIndex = nEndIndex; + return true; +} + +static sal_Int32 GetNextAttrImpl(SwTextNode const*const pTextNode, + size_t const nStartIndex, size_t const nEndIndex, + sal_Int32 const nPosition) +{ + // note: this used to be COMPLETE_STRING, but was set to Len() + 1 below, + // which is rather silly, so set it to Len() instead + sal_Int32 nNext = pTextNode->Len(); + if (SwpHints const*const pHints = pTextNode->GetpSwpHints()) + { + // are there attribute starts left? + for (size_t i = nStartIndex; i < pHints->Count(); ++i) + { + SwTextAttr *const pAttr(pHints->Get(i)); + if (!pAttr->IsFormatIgnoreStart()) + { + nNext = pAttr->GetStart(); + break; + } + } + // are there attribute ends left? + for (size_t i = nEndIndex; i < pHints->Count(); ++i) + { + SwTextAttr *const pAttr(pHints->GetSortedByEnd(i)); + if (!pAttr->IsFormatIgnoreEnd()) + { + sal_Int32 const nNextEnd = pAttr->GetAnyEnd(); + nNext = std::min(nNext, nNextEnd); // pick nearest one + break; + } + } + } + // TODO: maybe use hints like FieldHints for this instead of looking at the text... + const sal_Int32 l = std::min(nNext, pTextNode->Len()); + sal_Int32 p = nPosition; + const sal_Unicode* pStr = pTextNode->GetText().getStr(); + while (p < l) + { + sal_Unicode aChar = pStr[p]; + switch (aChar) + { + case CH_TXT_ATR_FORMELEMENT: + case CH_TXT_ATR_FIELDSTART: + case CH_TXT_ATR_FIELDSEP: + case CH_TXT_ATR_FIELDEND: + goto break_; // sigh... + default: + ++p; + } + } +break_: + assert(p <= nNext); + if (p < l) + { + // found a CH_TXT_ATR_FIELD*: if it's same as current position, + // skip behind it so that both before- and after-positions are returned + nNext = (nPosition < p) ? p : p + 1; + } + return nNext; +} + +TextFrameIndex SwAttrIter::GetNextAttr() const +{ + size_t nStartIndex(m_nStartIndex); + size_t nEndIndex(m_nEndIndex); + size_t nPosition(m_nPosition); + SwTextNode const* pTextNode(m_pTextNode); + SwRedlineTable::size_type nActRedline(m_pRedline ? m_pRedline->GetAct() : SwRedlineTable::npos); + + while (true) + { + sal_Int32 nNext = GetNextAttrImpl(pTextNode, nStartIndex, nEndIndex, nPosition); + if( m_pRedline ) + { + std::pair<sal_Int32, std::pair<SwRangeRedline const*, size_t>> const redline( + m_pRedline->GetNextRedln(nNext, pTextNode, nActRedline)); + if (redline.second.first) + { + assert(m_pMergedPara); + assert(redline.second.first->End()->GetNodeIndex() <= m_pMergedPara->pLastNode->GetIndex() + || !redline.second.first->End()->GetNode().IsTextNode()); + if (CanSkipOverRedline(*pTextNode, redline.first, *redline.second.first, + nStartIndex, nEndIndex, m_nPosition == redline.first)) + { // if current position is start of the redline, must skip! + nActRedline += redline.second.second; + if (&redline.second.first->End()->GetNode() != pTextNode) + { + pTextNode = redline.second.first->End()->GetNode().GetTextNode(); + nPosition = redline.second.first->End()->GetContentIndex(); + } + else + { + nPosition = redline.second.first->End()->GetContentIndex(); + } + } + else + { + return sw::MapModelToView(*m_pMergedPara, pTextNode, redline.first); + } + } + else + { + return m_pMergedPara + ? sw::MapModelToView(*m_pMergedPara, pTextNode, redline.first) + : TextFrameIndex(redline.first); + } + } + else + { + return TextFrameIndex(nNext); + } + } +} + +namespace { + +class SwMinMaxArgs +{ +public: + VclPtr<OutputDevice> m_pOut; + SwViewShell const* m_pSh; + sal_uLong& m_rMin; + sal_uLong& m_rAbsMin; + tools::Long m_nRowWidth; + tools::Long m_nWordWidth; + tools::Long m_nWordAdd; + sal_Int32 m_nNoLineBreak; + SwMinMaxArgs(OutputDevice* pOutI, SwViewShell const* pShI, sal_uLong& rMinI, sal_uLong& rAbsI) + : m_pOut(pOutI) + , m_pSh(pShI) + , m_rMin(rMinI) + , m_rAbsMin(rAbsI) + , m_nRowWidth(0) + , m_nWordWidth(0) + , m_nWordAdd(0) + , m_nNoLineBreak(COMPLETE_STRING) + { } + void Minimum( tools::Long nNew ) const { + if (static_cast<tools::Long>(m_rMin) < nNew) + m_rMin = nNew; + } + void NewWord() { m_nWordAdd = m_nWordWidth = 0; } +}; + +} + +static bool lcl_MinMaxString( SwMinMaxArgs& rArg, SwFont* pFnt, const OUString &rText, + sal_Int32 nIdx, sal_Int32 nEnd ) +{ + bool bRet = false; + while( nIdx < nEnd ) + { + sal_Int32 nStop = nIdx; + LanguageType eLang = pFnt->GetLanguage(); + assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is()); + + bool bClear = CH_BLANK == rText[ nStop ]; + Boundary aBndry( g_pBreakIt->GetBreakIter()->getWordBoundary( rText, nIdx, + g_pBreakIt->GetLocale( eLang ), + WordType::DICTIONARY_WORD, true ) ); + nStop = aBndry.endPos; + if (nIdx <= aBndry.startPos && nIdx && nIdx - 1 != rArg.m_nNoLineBreak) + rArg.NewWord(); + if( nStop == nIdx ) + ++nStop; + if( nStop > nEnd ) + nStop = nEnd; + + SwDrawTextInfo aDrawInf(rArg.m_pSh, *rArg.m_pOut, rText, nIdx, nStop - nIdx); + tools::Long nCurrentWidth = pFnt->GetTextSize_( aDrawInf ).Width(); + rArg.m_nRowWidth += nCurrentWidth; + if( bClear ) + rArg.NewWord(); + else + { + rArg.m_nWordWidth += nCurrentWidth; + if (static_cast<tools::Long>(rArg.m_rAbsMin) < rArg.m_nWordWidth) + rArg.m_rAbsMin = rArg.m_nWordWidth; + rArg.Minimum(rArg.m_nWordWidth + rArg.m_nWordAdd); + bRet = true; + } + nIdx = nStop; + } + return bRet; +} + +bool SwTextNode::IsSymbolAt(const sal_Int32 nBegin) const +{ + SwScriptInfo aScriptInfo; + SwAttrIter aIter( *const_cast<SwTextNode*>(this), aScriptInfo ); + aIter.Seek( TextFrameIndex(nBegin) ); + return aIter.GetFnt()->IsSymbol( getIDocumentLayoutAccess().GetCurrentViewShell() ); +} + +namespace { + +class SwMinMaxNodeArgs +{ +public: + sal_uLong m_nMaxWidth; // sum of all frame widths + tools::Long m_nMinWidth; // biggest frame + tools::Long m_nLeftRest; // space not already covered by frames in the left margin + tools::Long m_nRightRest; // space not already covered by frames in the right margin + tools::Long m_nLeftDiff; // Min/Max-difference of the frame in the left margin + tools::Long m_nRightDiff; // Min/Max-difference of the frame in the right margin + SwNodeOffset m_nIndex; // index of the node + void Minimum( tools::Long nNew ) { + if (nNew > m_nMinWidth) + m_nMinWidth = nNew; + } +}; + +} + +static void lcl_MinMaxNode(SwFrameFormat* pNd, SwMinMaxNodeArgs& rIn) +{ + const SwFormatAnchor& rFormatA = pNd->GetAnchor(); + + if ((RndStdIds::FLY_AT_PARA != rFormatA.GetAnchorId()) && + (RndStdIds::FLY_AT_CHAR != rFormatA.GetAnchorId())) + { + return; + } + + const SwNode *pAnchorNode = rFormatA.GetAnchorNode(); + OSL_ENSURE(pAnchorNode, "Unexpected NULL arguments"); + if (!pAnchorNode || rIn.m_nIndex != pAnchorNode->GetIndex()) + return; + + tools::Long nMin, nMax; + SwHTMLTableLayout *pLayout = nullptr; + const bool bIsDrawFrameFormat = pNd->Which()==RES_DRAWFRMFMT; + if( !bIsDrawFrameFormat ) + { + // Does the frame contain a table at the start or the end? + const SwNodes& rNodes = pNd->GetDoc()->GetNodes(); + const SwFormatContent& rFlyContent = pNd->GetContent(); + SwNodeOffset nStt = rFlyContent.GetContentIdx()->GetIndex(); + SwTableNode* pTableNd = rNodes[nStt+1]->GetTableNode(); + if( !pTableNd ) + { + SwNode *pNd2 = rNodes[nStt]; + pNd2 = rNodes[pNd2->EndOfSectionIndex()-1]; + if( pNd2->IsEndNode() ) + pTableNd = pNd2->StartOfSectionNode()->GetTableNode(); + } + + if( pTableNd ) + pLayout = pTableNd->GetTable().GetHTMLTableLayout(); + } + + const SwFormatHoriOrient& rOrient = pNd->GetHoriOrient(); + sal_Int16 eHoriOri = rOrient.GetHoriOrient(); + + tools::Long nDiff; + if( pLayout ) + { + nMin = pLayout->GetMin(); + nMax = pLayout->GetMax(); + nDiff = nMax - nMin; + } + else + { + if( bIsDrawFrameFormat ) + { + const SdrObject* pSObj = pNd->FindSdrObject(); + if( pSObj ) + nMin = pSObj->GetCurrentBoundRect().GetWidth(); + else + nMin = 0; + + } + else + { + const SwFormatFrameSize &rSz = pNd->GetFrameSize(); + nMin = rSz.GetWidth(); + } + nMax = nMin; + nDiff = 0; + } + + const SvxLRSpaceItem &rLR = pNd->GetLRSpace(); + nMin += rLR.GetLeft(); + nMin += rLR.GetRight(); + nMax += rLR.GetLeft(); + nMax += rLR.GetRight(); + + if( css::text::WrapTextMode_THROUGH == pNd->GetSurround().GetSurround() ) + { + rIn.Minimum( nMin ); + return; + } + + // Frames, which are left- or right-aligned are only party considered + // when calculating the maximum, since the border is already being considered. + // Only if the frame extends into the text body, this part is being added + switch( eHoriOri ) + { + case text::HoriOrientation::RIGHT: + { + if( nDiff ) + { + rIn.m_nRightRest -= rIn.m_nRightDiff; + rIn.m_nRightDiff = nDiff; + } + if( text::RelOrientation::FRAME != rOrient.GetRelationOrient() ) + { + if (rIn.m_nRightRest > 0) + rIn.m_nRightRest = 0; + } + rIn.m_nRightRest -= nMin; + break; + } + case text::HoriOrientation::LEFT: + { + if( nDiff ) + { + rIn.m_nLeftRest -= rIn.m_nLeftDiff; + rIn.m_nLeftDiff = nDiff; + } + if (text::RelOrientation::FRAME != rOrient.GetRelationOrient() && rIn.m_nLeftRest < 0) + rIn.m_nLeftRest = 0; + rIn.m_nLeftRest -= nMin; + break; + } + default: + { + rIn.m_nMaxWidth += nMax; + rIn.Minimum(nMin); + } + } +} + +#define FLYINCNT_MIN_WIDTH 284 + +/** + * Changing this method very likely requires changing of GetScalingOfSelectedText + * This one is called exclusively from import filters, so there is no layout. + */ +void SwTextNode::GetMinMaxSize( SwNodeOffset nIndex, sal_uLong& rMin, sal_uLong &rMax, + sal_uLong& rAbsMin ) const +{ + SwViewShell const * pSh = GetDoc().getIDocumentLayoutAccess().GetCurrentViewShell(); + OutputDevice* pOut = nullptr; + if( pSh ) + pOut = pSh->GetWin()->GetOutDev(); + if( !pOut ) + pOut = Application::GetDefaultDevice(); + + MapMode aOldMap( pOut->GetMapMode() ); + pOut->SetMapMode( MapMode( MapUnit::MapTwip ) ); + + rMin = 0; + rMax = 0; + rAbsMin = 0; + + SvxTextLeftMarginItem const& rTextLeftMargin(GetSwAttrSet().GetTextLeftMargin()); + SvxRightMarginItem const& rRightMargin(GetSwAttrSet().GetRightMargin()); + tools::Long nLROffset = rTextLeftMargin.GetTextLeft() + GetLeftMarginWithNum( true ); + short nFLOffs; + // For enumerations a negative first line indentation is probably filled already + if( !GetFirstLineOfsWithNum( nFLOffs ) || nFLOffs > nLROffset ) + nLROffset = nFLOffs; + + SwMinMaxNodeArgs aNodeArgs; + aNodeArgs.m_nMinWidth = 0; + aNodeArgs.m_nMaxWidth = 0; + aNodeArgs.m_nLeftRest = nLROffset; + aNodeArgs.m_nRightRest = rRightMargin.GetRight(); + aNodeArgs.m_nLeftDiff = 0; + aNodeArgs.m_nRightDiff = 0; + if( nIndex ) + { + sw::SpzFrameFormats* pSpzs = const_cast<sw::SpzFrameFormats*>(GetDoc().GetSpzFrameFormats()); + if(pSpzs) + { + aNodeArgs.m_nIndex = nIndex; + for(auto pFormat: *pSpzs) + lcl_MinMaxNode(pFormat, aNodeArgs); + } + } + if (aNodeArgs.m_nLeftRest < 0) + aNodeArgs.Minimum(nLROffset - aNodeArgs.m_nLeftRest); + aNodeArgs.m_nLeftRest -= aNodeArgs.m_nLeftDiff; + if (aNodeArgs.m_nLeftRest < 0) + aNodeArgs.m_nMaxWidth -= aNodeArgs.m_nLeftRest; + + if (aNodeArgs.m_nRightRest < 0) + aNodeArgs.Minimum(rRightMargin.GetRight() - aNodeArgs.m_nRightRest); + aNodeArgs.m_nRightRest -= aNodeArgs.m_nRightDiff; + if (aNodeArgs.m_nRightRest < 0) + aNodeArgs.m_nMaxWidth -= aNodeArgs.m_nRightRest; + + SwScriptInfo aScriptInfo; + SwAttrIter aIter( *const_cast<SwTextNode*>(this), aScriptInfo ); + TextFrameIndex nIdx(0); + aIter.SeekAndChgAttrIter( nIdx, pOut ); + TextFrameIndex nLen(m_Text.getLength()); + tools::Long nCurrentWidth = 0; + tools::Long nAdd = 0; + SwMinMaxArgs aArg( pOut, pSh, rMin, rAbsMin ); + while( nIdx < nLen ) + { + TextFrameIndex nNextChg = aIter.GetNextAttr(); + TextFrameIndex nStop = aScriptInfo.NextScriptChg( nIdx ); + if( nNextChg > nStop ) + nNextChg = nStop; + SwTextAttr *pHint = nullptr; + sal_Unicode cChar = CH_BLANK; + nStop = nIdx; + while( nStop < nLen && nStop < nNextChg && + CH_TAB != (cChar = m_Text[sal_Int32(nStop)]) && + CH_BREAK != cChar && CHAR_HARDBLANK != cChar && + CHAR_HARDHYPHEN != cChar && CHAR_SOFTHYPHEN != cChar && + CH_TXT_ATR_INPUTFIELDSTART != cChar && + CH_TXT_ATR_INPUTFIELDEND != cChar && + CH_TXT_ATR_FORMELEMENT != cChar && + CH_TXT_ATR_FIELDSTART != cChar && + CH_TXT_ATR_FIELDSEP != cChar && + CH_TXT_ATR_FIELDEND != cChar && + !pHint ) + { + // this looks like some defensive programming to handle dummy char + // with missing hint? but it's rather silly because it may pass the + // dummy char to lcl_MinMaxString in that case... + if( ( CH_TXTATR_BREAKWORD != cChar && CH_TXTATR_INWORD != cChar ) + || ( nullptr == ( pHint = aIter.GetAttr( nStop ) ) ) ) + ++nStop; + } + if (lcl_MinMaxString(aArg, aIter.GetFnt(), m_Text, sal_Int32(nIdx), sal_Int32(nStop))) + { + nAdd = 20; + } + nIdx = nStop; + aIter.SeekAndChgAttrIter( nIdx, pOut ); + switch( cChar ) + { + case CH_BREAK : + { + if (static_cast<tools::Long>(rMax) < aArg.m_nRowWidth) + rMax = aArg.m_nRowWidth; + aArg.m_nRowWidth = 0; + aArg.NewWord(); + aIter.SeekAndChgAttrIter( ++nIdx, pOut ); + } + break; + case CH_TAB : + { + aArg.NewWord(); + aIter.SeekAndChgAttrIter( ++nIdx, pOut ); + } + break; + case CHAR_SOFTHYPHEN: + ++nIdx; + break; + case CHAR_HARDBLANK: + case CHAR_HARDHYPHEN: + { + OUString sTmp( cChar ); + SwDrawTextInfo aDrawInf( pSh, + *pOut, sTmp, 0, 1, 0, false ); + nCurrentWidth = aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); + aArg.m_nWordWidth += nCurrentWidth; + aArg.m_nRowWidth += nCurrentWidth; + if (static_cast<tools::Long>(rAbsMin) < aArg.m_nWordWidth) + rAbsMin = aArg.m_nWordWidth; + aArg.Minimum(aArg.m_nWordWidth + aArg.m_nWordAdd); + aArg.m_nNoLineBreak = sal_Int32(nIdx++); + } + break; + case CH_TXTATR_BREAKWORD: + case CH_TXTATR_INWORD: + { + if( !pHint ) + break; + tools::Long nOldWidth = aArg.m_nWordWidth; + tools::Long nOldAdd = aArg.m_nWordAdd; + aArg.NewWord(); + + switch( pHint->Which() ) + { + case RES_TXTATR_FLYCNT : + { + SwFrameFormat *pFrameFormat = pHint->GetFlyCnt().GetFrameFormat(); + const SvxLRSpaceItem &rLR = pFrameFormat->GetLRSpace(); + if( RES_DRAWFRMFMT == pFrameFormat->Which() ) + { + const SdrObject* pSObj = pFrameFormat->FindSdrObject(); + if( pSObj ) + nCurrentWidth = pSObj->GetCurrentBoundRect().GetWidth(); + else + nCurrentWidth = 0; + } + else + { + const SwFormatFrameSize& rTmpSize = pFrameFormat->GetFrameSize(); + if( RES_FLYFRMFMT == pFrameFormat->Which() + && rTmpSize.GetWidthPercent() ) + { + // This is a hack for the following situation: In the paragraph there's a + // text frame with relative size. Then let's take 0.5 cm as minimum width + // and USHRT_MAX as maximum width + // It were cleaner and maybe necessary later on to iterate over the content + // of the text frame and call GetMinMaxSize recursively + nCurrentWidth = FLYINCNT_MIN_WIDTH; // 0.5 cm + rMax = std::max(rMax, sal_uLong(USHRT_MAX)); + } + else + nCurrentWidth = pFrameFormat->GetFrameSize().GetWidth(); + } + nCurrentWidth += rLR.GetLeft(); + nCurrentWidth += rLR.GetRight(); + aArg.m_nWordAdd = nOldWidth + nOldAdd; + aArg.m_nWordWidth = nCurrentWidth; + aArg.m_nRowWidth += nCurrentWidth; + if (static_cast<tools::Long>(rAbsMin) < aArg.m_nWordWidth) + rAbsMin = aArg.m_nWordWidth; + aArg.Minimum(aArg.m_nWordWidth + aArg.m_nWordAdd); + break; + } + case RES_TXTATR_FTN : + { + const OUString aText = pHint->GetFootnote().GetNumStr(); + if( lcl_MinMaxString( aArg, aIter.GetFnt(), aText, 0, + aText.getLength() ) ) + nAdd = 20; + break; + } + + case RES_TXTATR_FIELD : + case RES_TXTATR_ANNOTATION : + { + SwField *pField = const_cast<SwField*>(pHint->GetFormatField().GetField()); + const OUString aText = pField->ExpandField(true, nullptr); + if( lcl_MinMaxString( aArg, aIter.GetFnt(), aText, 0, + aText.getLength() ) ) + nAdd = 20; + break; + } + default: + aArg.m_nWordWidth = nOldWidth; + aArg.m_nWordAdd = nOldAdd; + } + aIter.SeekAndChgAttrIter( ++nIdx, pOut ); + } + break; + case CH_TXT_ATR_INPUTFIELDSTART: + case CH_TXT_ATR_INPUTFIELDEND: + case CH_TXT_ATR_FORMELEMENT: + case CH_TXT_ATR_FIELDSTART: + case CH_TXT_ATR_FIELDSEP: + case CH_TXT_ATR_FIELDEND: + { // just skip it and continue with the content... + aIter.SeekAndChgAttrIter( ++nIdx, pOut ); + } + break; + } + } + if (static_cast<tools::Long>(rMax) < aArg.m_nRowWidth) + rMax = aArg.m_nRowWidth; + + nLROffset += rRightMargin.GetRight(); + + rAbsMin += nLROffset; + rAbsMin += nAdd; + rMin += nLROffset; + rMin += nAdd; + if (static_cast<tools::Long>(rMin) < aNodeArgs.m_nMinWidth) + rMin = aNodeArgs.m_nMinWidth; + if (static_cast<tools::Long>(rAbsMin) < aNodeArgs.m_nMinWidth) + rAbsMin = aNodeArgs.m_nMinWidth; + rMax += aNodeArgs.m_nMaxWidth; + rMax += nLROffset; + rMax += nAdd; + if( rMax < rMin ) // e.g. Frames with flow through only contribute to the minimum + rMax = rMin; + pOut->SetMapMode( aOldMap ); +} + +/** + * Calculates the width of the text part specified by nStart and nEnd, + * the height of the line containing nStart is divided by this width, + * indicating the scaling factor, if the text part is rotated. + * Having CH_BREAKs in the text part, this method returns the scaling + * factor for the longest of the text parts separated by the CH_BREAK + * + * Changing this method very likely requires changing of "GetMinMaxSize" + */ +sal_uInt16 SwTextFrame::GetScalingOfSelectedText( + TextFrameIndex nStart, TextFrameIndex nEnd) +{ + assert(GetOffset() <= nStart && (!GetFollow() || nStart < GetFollow()->GetOffset())); + SwViewShell const*const pSh = getRootFrame()->GetCurrShell(); + assert(pSh); + OutputDevice *const pOut = &pSh->GetRefDev(); + assert(pOut); + + MapMode aOldMap( pOut->GetMapMode() ); + pOut->SetMapMode( MapMode( MapUnit::MapTwip ) ); + + if (nStart == nEnd) + { + assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is()); + + SwScriptInfo aScriptInfo; + SwAttrIter aIter(*GetTextNodeFirst(), aScriptInfo, this); + aIter.SeekAndChgAttrIter( nStart, pOut ); + + Boundary aBound = g_pBreakIt->GetBreakIter()->getWordBoundary( + GetText(), sal_Int32(nStart), + g_pBreakIt->GetLocale( aIter.GetFnt()->GetLanguage() ), + WordType::DICTIONARY_WORD, true ); + + if (sal_Int32(nStart) == aBound.startPos) + { + // cursor is at left or right border of word + pOut->SetMapMode( aOldMap ); + return 100; + } + + nStart = TextFrameIndex(aBound.startPos); + nEnd = TextFrameIndex(aBound.endPos); + + if (nStart == nEnd) + { + pOut->SetMapMode( aOldMap ); + return 100; + } + } + + SwScriptInfo aScriptInfo; + SwAttrIter aIter(*GetTextNodeFirst(), aScriptInfo, this); + + // We do not want scaling attributes to be considered during this + // calculation. For this, we push a temporary scaling attribute with + // scaling value 100 and priority flag on top of the scaling stack + SwAttrHandler& rAH = aIter.GetAttrHandler(); + SvxCharScaleWidthItem aItem(100, RES_CHRATR_SCALEW); + SwTextAttrEnd aAttr( aItem, 0, COMPLETE_STRING ); + aAttr.SetPriorityAttr( true ); + rAH.PushAndChg( aAttr, *(aIter.GetFnt()) ); + + TextFrameIndex nIdx = nStart; + + sal_uLong nWidth = 0; + sal_uLong nProWidth = 0; + + while( nIdx < nEnd ) + { + aIter.SeekAndChgAttrIter( nIdx, pOut ); + + // scan for end of portion + TextFrameIndex const nNextChg = std::min(aIter.GetNextAttr(), aScriptInfo.NextScriptChg(nIdx)); + + TextFrameIndex nStop = nIdx; + sal_Unicode cChar = CH_BLANK; + SwTextAttr* pHint = nullptr; + + // stop at special characters in [ nIdx, nNextChg ] + while( nStop < nEnd && nStop < nNextChg ) + { + cChar = GetText()[sal_Int32(nStop)]; + if ( + CH_TAB == cChar || + CH_BREAK == cChar || + CHAR_HARDBLANK == cChar || + CHAR_HARDHYPHEN == cChar || + CHAR_SOFTHYPHEN == cChar || + CH_TXT_ATR_INPUTFIELDSTART == cChar || + CH_TXT_ATR_INPUTFIELDEND == cChar || + CH_TXT_ATR_FORMELEMENT == cChar || + CH_TXT_ATR_FIELDSTART == cChar || + CH_TXT_ATR_FIELDSEP == cChar || + CH_TXT_ATR_FIELDEND == cChar || + ( + (CH_TXTATR_BREAKWORD == cChar || CH_TXTATR_INWORD == cChar) && + (nullptr == (pHint = aIter.GetAttr(nStop))) + ) + ) + { + break; + } + else + ++nStop; + } + + // calculate text widths up to cChar + if ( nStop > nIdx ) + { + SwDrawTextInfo aDrawInf(pSh, *pOut, GetText(), sal_Int32(nIdx), sal_Int32(nStop - nIdx)); + nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); + } + + nIdx = nStop; + aIter.SeekAndChgAttrIter( nIdx, pOut ); + + if ( cChar == CH_BREAK ) + { + nWidth = std::max( nWidth, nProWidth ); + nProWidth = 0; + nIdx++; + } + else if ( cChar == CH_TAB ) + { + // tab receives width of one space + SwDrawTextInfo aDrawInf(pSh, *pOut, OUStringChar(CH_BLANK), 0, 1); + nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); + nIdx++; + } + else if ( cChar == CHAR_SOFTHYPHEN ) + ++nIdx; + else if ( cChar == CHAR_HARDBLANK || cChar == CHAR_HARDHYPHEN ) + { + OUString sTmp( cChar ); + SwDrawTextInfo aDrawInf(pSh, *pOut, sTmp, 0, 1); + nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); + nIdx++; + } + else if ( pHint && ( cChar == CH_TXTATR_BREAKWORD || cChar == CH_TXTATR_INWORD ) ) + { + switch( pHint->Which() ) + { + case RES_TXTATR_FTN : + { + const OUString aText = pHint->GetFootnote().GetNumStr(); + SwDrawTextInfo aDrawInf(pSh, *pOut, aText, 0, aText.getLength()); + + nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); + break; + } + + case RES_TXTATR_FIELD : + case RES_TXTATR_ANNOTATION : + { + SwField *pField = const_cast<SwField*>(pHint->GetFormatField().GetField()); + OUString const aText = pField->ExpandField(true, getRootFrame()); + SwDrawTextInfo aDrawInf(pSh, *pOut, aText, 0, aText.getLength()); + + nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); + break; + } + + default: + { + // any suggestions for a default action? + } + } // end of switch + nIdx++; + } + else if (CH_TXT_ATR_INPUTFIELDSTART == cChar || + CH_TXT_ATR_INPUTFIELDEND == cChar || + CH_TXT_ATR_FORMELEMENT == cChar || + CH_TXT_ATR_FIELDSTART == cChar || + CH_TXT_ATR_FIELDSEP == cChar || + CH_TXT_ATR_FIELDEND == cChar) + { // just skip it and continue with the content... + ++nIdx; + } + } // end of while + + nWidth = std::max( nWidth, nProWidth ); + + // search for the line containing nStart + if (HasPara()) + { + SwTextInfo aInf(this); + SwTextIter aLine(this, &aInf); + aLine.CharToLine( nStart ); + pOut->SetMapMode( aOldMap ); + return o3tl::narrowing<sal_uInt16>( nWidth ? + ( ( 100 * aLine.GetCurr()->Height() ) / nWidth ) : 0 ); + } + // no frame or no paragraph, we take the height of the character + // at nStart as line height + + aIter.SeekAndChgAttrIter( nStart, pOut ); + pOut->SetMapMode( aOldMap ); + + SwDrawTextInfo aDrawInf(pSh, *pOut, GetText(), sal_Int32(nStart), 1); + return o3tl::narrowing<sal_uInt16>( nWidth ? ((100 * aIter.GetFnt()->GetTextSize_( aDrawInf ).Height()) / nWidth ) : 0 ); +} + +std::vector<SwFlyAtContentFrame*> SwTextFrame::GetSplitFlyDrawObjs() const +{ + std::vector<SwFlyAtContentFrame*> aObjs; + const SwSortedObjs* pSortedObjs = GetDrawObjs(); + if (!pSortedObjs) + { + return aObjs; + } + + for (const auto& pSortedObj : *pSortedObjs) + { + SwFlyFrame* pFlyFrame = pSortedObj->DynCastFlyFrame(); + if (!pFlyFrame) + { + continue; + } + + if (!pFlyFrame->IsFlySplitAllowed()) + { + continue; + } + + aObjs.push_back(static_cast<SwFlyAtContentFrame*>(pFlyFrame)); + } + + return aObjs; +} + +SwFlyAtContentFrame* SwTextFrame::HasNonLastSplitFlyDrawObj() const +{ + const SwTextFrame* pFollow = GetFollow(); + if (!pFollow) + { + return nullptr; + } + + if (mnOffset != pFollow->GetOffset()) + { + return nullptr; + } + + // At this point we know what we're part of a chain that is an anchor for split fly frames, but + // we're not the last one. See if we have a matching fly. + + // Look up the master of the anchor. + const SwTextFrame* pAnchor = this; + while (pAnchor->IsFollow()) + { + pAnchor = pAnchor->FindMaster(); + } + for (const auto& pFly : pAnchor->GetSplitFlyDrawObjs()) + { + // Nominally all flys are anchored in the master; see if this fly is effectively anchored in + // us. + SwTextFrame* pFlyAnchor = pFly->FindAnchorCharFrame(); + if (pFlyAnchor != this) + { + continue; + } + if (pFly->GetFollow()) + { + return pFly; + } + } + + return nullptr; +} + +bool SwTextFrame::IsEmptyMasterWithSplitFly() const +{ + if (!IsEmptyMaster()) + { + return false; + } + + if (!m_pDrawObjs || m_pDrawObjs->size() != 1) + { + return false; + } + + SwFlyFrame* pFlyFrame = (*m_pDrawObjs)[0]->DynCastFlyFrame(); + if (!pFlyFrame || !pFlyFrame->IsFlySplitAllowed()) + { + return false; + } + + if (mnOffset != GetFollow()->GetOffset()) + { + return false; + } + + return true; +} + +bool SwTextFrame::IsEmptyWithSplitFly() const +{ + if (IsFollow()) + { + return false; + } + + if (GetTextNodeFirst()->GetSwAttrSet().HasItem(RES_PAGEDESC)) + { + return false; + } + + if (getFrameArea().Bottom() <= GetUpper()->getFramePrintArea().Bottom()) + { + return false; + } + + // This is a master that doesn't fit the current parent. + if (!m_pDrawObjs || m_pDrawObjs->size() != 1) + { + return false; + } + + SwFlyFrame* pFlyFrame = (*m_pDrawObjs)[0]->DynCastFlyFrame(); + if (!pFlyFrame || !pFlyFrame->IsFlySplitAllowed()) + { + return false; + } + + // It has a split fly anchored to it. + if (pFlyFrame->GetFrameFormat().GetVertOrient().GetPos() >= 0) + { + return false; + } + + // Negative vertical offset means that visually it already may have a first line. + // Consider that, we may need to split the frame, so the fly frame is on one page and the empty + // paragraph's frame is on a next page. + return true; +} + +SwTwips SwTextNode::GetWidthOfLeadingTabs() const +{ + SwTwips nRet = 0; + + sal_Int32 nIdx = 0; + + while ( nIdx < GetText().getLength() ) + { + const sal_Unicode cCh = GetText()[nIdx]; + if ( cCh!='\t' && cCh!=' ' ) + { + break; + } + ++nIdx; + } + + if ( nIdx > 0 ) + { + SwPosition aPos( *this, nIdx ); + + // Find the non-follow text frame: + SwIterator<SwTextFrame, SwTextNode, sw::IteratorMode::UnwrapMulti> aIter(*this); + for( SwTextFrame* pFrame = aIter.First(); pFrame; pFrame = aIter.Next() ) + { + // Only consider master frames: + if (!pFrame->IsFollow() && + pFrame->GetTextNodeForFirstText() == this) + { + SwRectFnSet aRectFnSet(pFrame); + SwRect aRect; + pFrame->GetCharRect( aRect, aPos ); + nRet = pFrame->IsRightToLeft() ? + aRectFnSet.GetPrtRight(*pFrame) - aRectFnSet.GetRight(aRect) : + aRectFnSet.GetLeft(aRect) - aRectFnSet.GetPrtLeft(*pFrame); + break; + } + } + } + + return nRet; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itratr.hxx b/sw/source/core/text/itratr.hxx new file mode 100644 index 0000000000..eb15400cf5 --- /dev/null +++ b/sw/source/core/text/itratr.hxx @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <o3tl/deleter.hxx> +#include "atrhndl.hxx" +#include <swfont.hxx> + +namespace sw { struct MergedPara; } +class SwTextAttr; +class SwTextNode; +class SwRedlineItr; +class SwViewShell; +class SwTextFrame; + +class SwAttrIter +{ + friend class SwFontSave; +protected: + + SwAttrHandler m_aAttrHandler; + SwViewShell *m_pViewShell; + SwFont* m_pFont; + SwScriptInfo* m_pScriptInfo; + +private: + VclPtr<OutputDevice> m_pLastOut; + /// count currently open hints, redlines, ext-input + short m_nChgCnt; + std::unique_ptr<SwRedlineItr, o3tl::default_delete<SwRedlineItr>> m_pRedline; + /// current iteration index in HintStarts + size_t m_nStartIndex; + /// current iteration index in HintEnds + size_t m_nEndIndex; + /// current iteration index in text node + sal_Int32 m_nPosition; + sal_uInt8 m_nPropFont; + o3tl::enumarray<SwFontScript, const void*> m_aFontCacheIds; + o3tl::enumarray<SwFontScript, sal_uInt16> m_aFontIdx; + /// input: the current text node + const SwTextNode* m_pTextNode; + sw::MergedPara const* m_pMergedPara; + + void SeekFwd(sal_Int32 nOldPos, sal_Int32 nNewPos); + void SetFnt( SwFont* pNew ) { m_pFont = pNew; } + void InitFontAndAttrHandler( + SwTextNode const& rPropsNode, SwTextNode const& rTextNode, + std::u16string_view aText, bool const* pbVertLayout, + bool const* pbVertLayoutLRBT); + +protected: + void Chg( SwTextAttr const *pHt ); + void Rst( SwTextAttr const *pHt ); + void CtorInitAttrIter(SwTextNode& rTextNode, SwScriptInfo& rScrInf, SwTextFrame const* pFrame = nullptr); + explicit SwAttrIter(SwTextNode const * pTextNode); + +public: + /// All subclasses of this always have a SwTextFrame passed to the + /// constructor, but SwAttrIter itself may be created without a + /// SwTextFrame in certain special cases via this ctor here + SwAttrIter(SwTextNode& rTextNode, SwScriptInfo& rScrInf, SwTextFrame const*const pFrame = nullptr); + + virtual ~SwAttrIter(); + + SwRedlineItr *GetRedln() { return m_pRedline.get(); } + // The parameter returns the position of the next change before or at the + // char position. + TextFrameIndex GetNextAttr() const; + /// Enables the attributes used at char pos nPos in the logical font + bool Seek(TextFrameIndex nPos); + // Creates the font at the specified position via Seek() and checks + // if it's a symbol font. + bool IsSymbol(TextFrameIndex nPos); + + /** Executes ChgPhysFnt if Seek() returns true + * and change font to merge character border with neighbours. + **/ + bool SeekAndChgAttrIter(TextFrameIndex nPos, OutputDevice* pOut); + bool SeekStartAndChgAttrIter( OutputDevice* pOut, const bool bParaFont ); + + // Do we possibly have nasty things like footnotes? + bool MaybeHasHints() const; + + // Returns the attribute for a position + SwTextAttr *GetAttr(TextFrameIndex nPos) const; + + SwFont *GetFnt() { return m_pFont; } + const SwFont *GetFnt() const { return m_pFont; } + + sal_uInt8 GetPropFont() const { return m_nPropFont; } + void SetPropFont( const sal_uInt8 nNew ) { m_nPropFont = nNew; } + + SwAttrHandler& GetAttrHandler() { return m_aAttrHandler; } +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itrcrsr.cxx b/sw/source/core/text/itrcrsr.cxx new file mode 100644 index 0000000000..a39eb77301 --- /dev/null +++ b/sw/source/core/text/itrcrsr.cxx @@ -0,0 +1,2023 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <ndtxt.hxx> +#include <doc.hxx> +#include <paratr.hxx> +#include <flyfrm.hxx> +#include <pam.hxx> +#include <swselectionlist.hxx> +#include <sortedobjs.hxx> +#include <editeng/adjustitem.hxx> +#include <editeng/lspcitem.hxx> +#include <editeng/lrspitem.hxx> +#include <frmatr.hxx> +#include <tgrditem.hxx> +#include <IDocumentSettingAccess.hxx> +#include <pagefrm.hxx> + +#include "itrtxt.hxx" +#include <txtfrm.hxx> +#include <flyfrms.hxx> +#include "porfld.hxx" +#include "porfly.hxx" +#include "pordrop.hxx" +#include <crstate.hxx> +#include "pormulti.hxx" +#include <numrule.hxx> +#include <com/sun/star/i18n/ScriptType.hpp> + +// Not reentrant !!! +// is set in GetCharRect and is interpreted in UnitUp/Down. +bool SwTextCursor::s_bRightMargin = false; + +// After calculating the position of a character during GetCharRect +// this function allows to find the coordinates of a position (defined +// in pCMS->pSpecialPos) inside a special portion (e.g., a field) +static void lcl_GetCharRectInsideField( SwTextSizeInfo& rInf, SwRect& rOrig, + const SwCursorMoveState& rCMS, + const SwLinePortion& rPor ) +{ + OSL_ENSURE( rCMS.m_pSpecialPos, "Information about special pos missing" ); + + if ( rPor.InFieldGrp() && !static_cast<const SwFieldPortion&>(rPor).GetExp().isEmpty() ) + { + const sal_Int32 nCharOfst = rCMS.m_pSpecialPos->nCharOfst; + sal_Int32 nFieldIdx = 0; + sal_Int32 nFieldLen = 0; + + OUString sString; + const OUString* pString = nullptr; + const SwLinePortion* pPor = &rPor; + do + { + if ( pPor->InFieldGrp() ) + { + sString = static_cast<const SwFieldPortion*>(pPor)->GetExp(); + pString = &sString; + nFieldLen = pString->getLength(); + } + else + { + pString = nullptr; + nFieldLen = 0; + } + + if ( ! pPor->GetNextPortion() || nFieldIdx + nFieldLen > nCharOfst ) + break; + + nFieldIdx = nFieldIdx + nFieldLen; + rOrig.Pos().AdjustX(pPor->Width() ); + pPor = pPor->GetNextPortion(); + + } while ( true ); + + OSL_ENSURE( nCharOfst >= nFieldIdx, "Request of position inside field failed" ); + sal_Int32 nLen = nCharOfst - nFieldIdx + 1; + + if ( pString ) + { + // get script for field portion + rInf.GetFont()->SetActual( SwScriptInfo::WhichFont(0, *pString) ); + + TextFrameIndex const nOldLen = pPor->GetLen(); + const_cast<SwLinePortion*>(pPor)->SetLen(TextFrameIndex(nLen - 1)); + const SwTwips nX1 = pPor->GetLen() ? + pPor->GetTextSize( rInf ).Width() : + 0; + + SwTwips nX2 = 0; + if ( rCMS.m_bRealWidth ) + { + const_cast<SwLinePortion*>(pPor)->SetLen(TextFrameIndex(nLen)); + nX2 = pPor->GetTextSize( rInf ).Width(); + } + + const_cast<SwLinePortion*>(pPor)->SetLen( nOldLen ); + + rOrig.Pos().AdjustX(nX1 ); + rOrig.Width( ( nX2 > nX1 ) ? + ( nX2 - nX1 ) : + 1 ); + } + } + else + { + // special cases: no common fields, e.g., graphic number portion, + // FlyInCntPortions, Notes + rOrig.Width( rCMS.m_bRealWidth && rPor.Width() ? rPor.Width() : 1 ); + } +} + +// #i111284# +namespace { + bool IsLabelAlignmentActive( const SwTextNode& rTextNode ) + { + bool bRet( false ); + + if ( rTextNode.GetNumRule() ) + { + int nListLevel = rTextNode.GetActualListLevel(); + + if (nListLevel < 0) + nListLevel = 0; + + if (nListLevel >= MAXLEVEL) + nListLevel = MAXLEVEL - 1; + + const SwNumFormat& rNumFormat = + rTextNode.GetNumRule()->Get( o3tl::narrowing<sal_uInt16>(nListLevel) ); + if ( rNumFormat.GetPositionAndSpaceMode() == SvxNumberFormat::LABEL_ALIGNMENT ) + { + bRet = true; + } + } + + return bRet; + } +} // end of anonymous namespace + +void SwTextMargin::CtorInitTextMargin( SwTextFrame *pNewFrame, SwTextSizeInfo *pNewInf ) +{ + CtorInitTextIter( pNewFrame, pNewInf ); + + m_pInf = pNewInf; + GetInfo().SetFont( GetFnt() ); + const SwTextNode *const pNode = m_pFrame->GetTextNodeForParaProps(); + + SvxFirstLineIndentItem const& rFirstLine(pNode->GetSwAttrSet().GetFirstLineIndent()); + SvxTextLeftMarginItem const& rTextLeftMargin(pNode->GetSwAttrSet().GetTextLeftMargin()); + // #i95907# + // #i111284# + const SwTextNode *pTextNode = m_pFrame->GetTextNodeForParaProps(); + const bool bLabelAlignmentActive = IsLabelAlignmentActive( *pTextNode ); + const bool bListLevelIndentsApplicable = pTextNode->AreListLevelIndentsApplicable() != ::sw::ListLevelIndents::No; + const bool bListLevelIndentsApplicableAndLabelAlignmentActive = bListLevelIndentsApplicable && bLabelAlignmentActive; + + // Carefully adjust the text formatting ranges. + + // This whole area desperately needs some rework. There are + // quite a couple of values that need to be considered: + // 1. paragraph indent + // 2. paragraph first line indent + // 3. numbering indent + // 4. numbering spacing to text + // 5. paragraph border + // Note: These values have already been used during calculation + // of the printing area of the paragraph. + const int nLMWithNum = pNode->GetLeftMarginWithNum( true ); + if ( m_pFrame->IsRightToLeft() ) + { + // this calculation is identical this the calculation for L2R layout - see below + mnLeft = m_pFrame->getFrameArea().Left() + + m_pFrame->getFramePrintArea().Left() + + nLMWithNum - + pNode->GetLeftMarginWithNum() - + // #i95907# + // #i111284# + // rSpace.GetLeft() + rSpace.GetTextLeft(); + (rTextLeftMargin.GetLeft(rFirstLine) - rTextLeftMargin.GetTextLeft()); + } + else + { + // #i95907# + // #i111284# + if ( bListLevelIndentsApplicableAndLabelAlignmentActive || + !pNode->getIDocumentSettingAccess()->get(DocumentSettingId::IGNORE_FIRST_LINE_INDENT_IN_NUMBERING) ) + { + // this calculation is identical this the calculation for R2L layout - see above + mnLeft = m_pFrame->getFrameArea().Left() + + m_pFrame->getFramePrintArea().Left() + + nLMWithNum - + pNode->GetLeftMarginWithNum() - + // #i95907# + // #i111284# + (rTextLeftMargin.GetLeft(rFirstLine) - rTextLeftMargin.GetTextLeft()); + } + else + { + mnLeft = m_pFrame->getFrameArea().Left() + + std::max(tools::Long(rTextLeftMargin.GetTextLeft() + nLMWithNum), + m_pFrame->getFramePrintArea().Left() ); + } + } + + mnRight = m_pFrame->getFrameArea().Left() + m_pFrame->getFramePrintArea().Left() + m_pFrame->getFramePrintArea().Width(); + + if( mnLeft >= mnRight && + // #i53066# Omit adjustment of nLeft for numbered + // paras inside cells inside new documents: + ( pNode->getIDocumentSettingAccess()->get(DocumentSettingId::IGNORE_FIRST_LINE_INDENT_IN_NUMBERING) || + !m_pFrame->IsInTab() || + (bListLevelIndentsApplicable && nLMWithNum == rTextLeftMargin.GetTextLeft()) + || (!bLabelAlignmentActive && nLMWithNum == 0))) + { + mnLeft = m_pFrame->getFramePrintArea().Left() + m_pFrame->getFrameArea().Left(); + if( mnLeft >= mnRight ) // e.g. with large paragraph indentations in slim table columns + mnRight = mnLeft + 1; // einen goennen wir uns immer + } + + if( m_pFrame->IsFollow() && m_pFrame->GetOffset() ) + mnFirst = mnLeft; + else + { + short nFLOfst = 0; + tools::Long nFirstLineOfs = 0; + if( !pNode->GetFirstLineOfsWithNum( nFLOfst ) && + rFirstLine.IsAutoFirst()) + { + nFirstLineOfs = GetFnt()->GetSize( GetFnt()->GetActual() ).Height(); + LanguageType const aLang = m_pFrame->GetLangOfChar( + TextFrameIndex(0), css::i18n::ScriptType::ASIAN); + if (aLang != LANGUAGE_KOREAN && aLang != LANGUAGE_JAPANESE) + nFirstLineOfs<<=1; + + // tdf#129448: Auto first-line indent should not be effected by line space. + // Below is for compatibility with old documents. + if (!pNode->getIDocumentSettingAccess()->get(DocumentSettingId::AUTO_FIRST_LINE_INDENT_DISREGARD_LINE_SPACE)) + { + const SvxLineSpacingItem *pSpace = m_aLineInf.GetLineSpacing(); + if( pSpace ) + { + switch( pSpace->GetLineSpaceRule() ) + { + case SvxLineSpaceRule::Auto: + break; + case SvxLineSpaceRule::Min: + { + if( nFirstLineOfs < pSpace->GetLineHeight() ) + nFirstLineOfs = pSpace->GetLineHeight(); + break; + } + case SvxLineSpaceRule::Fix: + nFirstLineOfs = pSpace->GetLineHeight(); + break; + default: OSL_FAIL( ": unknown LineSpaceRule" ); + } + switch( pSpace->GetInterLineSpaceRule() ) + { + case SvxInterLineSpaceRule::Off: + break; + case SvxInterLineSpaceRule::Prop: + { + tools::Long nTmp = pSpace->GetPropLineSpace(); + // 50% is the minimum, at 0% we switch to + // the default value 100%... + if( nTmp < 50 ) + nTmp = nTmp ? 50 : 100; + + nTmp *= nFirstLineOfs; + nTmp /= 100; + if( !nTmp ) + ++nTmp; + nFirstLineOfs = nTmp; + break; + } + case SvxInterLineSpaceRule::Fix: + { + nFirstLineOfs += pSpace->GetInterLineSpace(); + break; + } + default: OSL_FAIL( ": unknown InterLineSpaceRule" ); + } + } + } + } + else + nFirstLineOfs = nFLOfst; + + // #i95907# + // #i111284# + if ( m_pFrame->IsRightToLeft() || + bListLevelIndentsApplicableAndLabelAlignmentActive || + !pNode->getIDocumentSettingAccess()->get(DocumentSettingId::IGNORE_FIRST_LINE_INDENT_IN_NUMBERING) ) + { + if ( nFirstLineOfs < 0 && m_pFrame->IsInTab() && + mnLeft == m_pFrame->getFramePrintArea().Left() + m_pFrame->getFrameArea().Left() && + !m_pFrame->IsRightToLeft() && + !bListLevelIndentsApplicableAndLabelAlignmentActive ) + { + // tdf#130218 always show hanging indent in narrow table cells + // to avoid hiding the text content of the first line + mnLeft -= nFirstLineOfs; + } + + mnFirst = mnLeft + nFirstLineOfs; + } + else + { + mnFirst = m_pFrame->getFrameArea().Left() + + std::max(rTextLeftMargin.GetTextLeft() + nLMWithNum + nFirstLineOfs, + m_pFrame->getFramePrintArea().Left() ); + } + + // Note: <SwTextFrame::GetAdditionalFirstLineOffset()> returns a negative + // value for the new list label position and space mode LABEL_ALIGNMENT + // and label alignment CENTER and RIGHT in L2R layout respectively + // label alignment LEFT and CENTER in R2L layout + mnFirst += m_pFrame->GetAdditionalFirstLineOffset(); + + if( mnFirst >= mnRight ) + mnFirst = mnRight - 1; + } + const SvxAdjustItem& rAdjust = m_pFrame->GetTextNodeForParaProps()->GetSwAttrSet().GetAdjust(); + mnAdjust = rAdjust.GetAdjust(); + + // left is left and right is right + if ( m_pFrame->IsRightToLeft() ) + { + if ( SvxAdjust::Left == mnAdjust ) + mnAdjust = SvxAdjust::Right; + else if ( SvxAdjust::Right == mnAdjust ) + mnAdjust = SvxAdjust::Left; + } + + m_bOneBlock = rAdjust.GetOneWord() == SvxAdjust::Block; + m_bLastBlock = rAdjust.GetLastBlock() == SvxAdjust::Block; + m_bLastCenter = rAdjust.GetLastBlock() == SvxAdjust::Center; + + // #i91133# + mnTabLeft = pNode->GetLeftMarginForTabCalculation(); + + DropInit(); +} + +void SwTextMargin::DropInit() +{ + mnDropLeft = mnDropLines = mnDropHeight = mnDropDescent = 0; + const SwParaPortion *pPara = GetInfo().GetParaPortion(); + if( pPara ) + { + const SwDropPortion *pPorDrop = pPara->FindDropPortion(); + if ( pPorDrop ) + { + mnDropLeft = pPorDrop->GetDropLeft(); + mnDropLines = pPorDrop->GetLines(); + mnDropHeight = pPorDrop->GetDropHeight(); + mnDropDescent = pPorDrop->GetDropDescent(); + } + } +} + +// The function is interpreting / observing / evaluating / keeping / respecting the first line indention and the specified width. +SwTwips SwTextMargin::GetLineStart() const +{ + SwTwips nRet = GetLeftMargin(); + if( GetAdjust() != SvxAdjust::Left && + !m_pCurr->GetFirstPortion()->IsMarginPortion() ) + { + // If the first portion is a Margin, then the + // adjustment is expressed by the portions. + if( GetAdjust() == SvxAdjust::Right ) + nRet = Right() - CurrWidth(); + else if( GetAdjust() == SvxAdjust::Center ) + nRet += (GetLineWidth() - CurrWidth()) / 2; + } + return nRet; +} + +void SwTextCursor::CtorInitTextCursor( SwTextFrame *pNewFrame, SwTextSizeInfo *pNewInf ) +{ + CtorInitTextMargin( pNewFrame, pNewInf ); + // 6096: Attention, the iterators are derived! + // GetInfo().SetOut( GetInfo().GetWin() ); +} + +static bool isTrailingDecoration(SwLinePortion* p) +{ + // Optional no-width portion, followed only by no-width portions and/or terminating portions? + for (; p; p = p->GetNextPortion()) + { + if (p->IsMarginPortion() || p->IsBreakPortion()) + return true; + if (p->Width()) + return false; + } + return true; // no more portions +} + +// 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() +{ + SwLinePortion* pPos = m_pCurr->GetNextPortion(); + while (pPos) + { + SwLinePortion* pNextPos = pPos->GetNextPortion(); + // Do it only if it is the last portion that able to handle the cursor, + // else the next portion would miscalculate the cursor position + if (pPos->ExtraBlankWidth() && isTrailingDecoration(pNextPos)) + { + pPos->Width(pPos->Width() + pPos->ExtraBlankWidth()); + pPos->ExtraBlankWidth(0); + } + pPos = pNextPos; + } +} + +// 1170: Ancient bug: Shift-End forgets the last character ... +void SwTextCursor::GetEndCharRect(SwRect* pOrig, const TextFrameIndex nOfst, + SwCursorMoveState* pCMS, const tools::Long nMax ) +{ + // 1170: Ambiguity of document positions + s_bRightMargin = true; + CharCursorToLine(nOfst); + + // Somehow twisted: nOfst names the position behind the last + // character of the last line == This is the position in front of the first character + // of the line, in which we are situated: + if( nOfst != GetStart() || !m_pCurr->GetLen() ) + { + // 8810: Master line RightMargin, after that LeftMargin + GetCharRect( pOrig, nOfst, pCMS, nMax ); + s_bRightMargin = nOfst >= GetEnd() && nOfst < TextFrameIndex(GetInfo().GetText().getLength()); + return; + } + + if( !GetPrev() || !GetPrev()->GetLen() || !PrevLine() ) + { + GetCharRect( pOrig, nOfst, pCMS, nMax ); + return; + } + + // If necessary, as catch up, do the adjustment + GetAdjusted(); + + tools::Long nX = 0; + tools::Long nLast = 0; + SwLinePortion *pPor = m_pCurr->GetFirstPortion(); + + SwTwips nTmpHeight, nTmpAscent; + CalcAscentAndHeight( nTmpAscent, nTmpHeight ); + sal_uInt16 nPorHeight = nTmpHeight; + sal_uInt16 nPorAscent = nTmpAscent; + + // Search for the last Text/EndPortion of the line + while( pPor ) + { + nX = nX + pPor->Width(); + if( pPor->InTextGrp() || ( pPor->GetLen() && !pPor->IsFlyPortion() + && !pPor->IsHolePortion() ) || pPor->IsBreakPortion() ) + { + nLast = nX; + nPorHeight = pPor->Height(); + nPorAscent = pPor->GetAscent(); + } + pPor = pPor->GetNextPortion(); + } + + const Size aCharSize( 1, nTmpHeight ); + pOrig->Pos( GetTopLeft() ); + pOrig->SSize( aCharSize ); + pOrig->Pos().AdjustX(nLast ); + const SwTwips nTmpRight = Right() - 1; + if( pOrig->Left() > nTmpRight ) + pOrig->Pos().setX( nTmpRight ); + + if ( pCMS && pCMS->m_bRealHeight ) + { + if ( nTmpAscent > nPorAscent ) + pCMS->m_aRealHeight.setX( nTmpAscent - nPorAscent ); + else + pCMS->m_aRealHeight.setX( 0 ); + OSL_ENSURE( nPorHeight, "GetCharRect: Missing Portion-Height" ); + pCMS->m_aRealHeight.setY( nPorHeight ); + } +} + +// internal function, called by SwTextCursor::GetCharRect() to calculate +// the relative character position in the current line. +// pOrig refers to x and y coordinates, width and height of the cursor +// pCMS is used for restricting the cursor, if there are different font +// heights in one line ( first value = offset to y of pOrig, second +// value = real height of (shortened) cursor +void SwTextCursor::GetCharRect_( SwRect* pOrig, TextFrameIndex const nOfst, + SwCursorMoveState* pCMS ) +{ + const OUString aText = GetInfo().GetText(); + SwTextSizeInfo aInf( GetInfo(), &aText, m_nStart ); + if( GetPropFont() ) + aInf.GetFont()->SetProportion( GetPropFont() ); + SwTwips nTmpAscent, nTmpHeight; // Line height + CalcAscentAndHeight( nTmpAscent, nTmpHeight ); + const Size aCharSize( 1, nTmpHeight ); + const Point aCharPos; + pOrig->Pos( aCharPos ); + pOrig->SSize( aCharSize ); + + // If we are looking for a position inside a field which covers + // more than one line we may not skip any "empty portions" at the + // beginning of a line + const bool bInsideFirstField = pCMS && pCMS->m_pSpecialPos && + ( pCMS->m_pSpecialPos->nLineOfst || + SwSPExtendRange::BEFORE == + pCMS->m_pSpecialPos->nExtendRange ); + + bool bWidth = pCMS && pCMS->m_bRealWidth; + if( !m_pCurr->GetLen() && !m_pCurr->Width() ) + { + if ( pCMS && pCMS->m_bRealHeight ) + { + pCMS->m_aRealHeight.setX( 0 ); + pCMS->m_aRealHeight.setY( nTmpHeight ); + } + } + else + { + SwTwips nPorHeight = nTmpHeight; + SwTwips nPorAscent = nTmpAscent; + SwTwips nX = 0; + SwTwips nTmpFirst = 0; + SwLinePortion *pPor = m_pCurr->GetFirstPortion(); + SwBidiPortion* pLastBidiPor = nullptr; + TextFrameIndex nLastBidiIdx(-1); + SwTwips nLastBidiPorWidth = 0; + std::deque<sal_uInt16>* pKanaComp = m_pCurr->GetpKanaComp(); + sal_uInt16 nSpaceIdx = 0; + size_t nKanaIdx = 0; + tools::Long nSpaceAdd = m_pCurr->IsSpaceAdd() ? m_pCurr->GetLLSpaceAdd( 0 ) : 0; + + bool bNoText = true; + + // First all portions without Len at beginning of line are skipped. + // Exceptions are the mean special portions from WhichFirstPortion: + // Num, ErgoSum, FootnoteNum, FieldRests + // 8477: but also the only Textportion of an empty line with + // Right/Center-Adjustment! So not just pPor->GetExpandPortion() ... + while( pPor && !pPor->GetLen() && ! bInsideFirstField ) + { + nX += pPor->Width(); + if ( pPor->InSpaceGrp() && nSpaceAdd ) + nX += pPor->CalcSpacing( nSpaceAdd, aInf ); + if( bNoText ) + nTmpFirst = nX; + // 8670: EndPortions count once as TextPortions. + // if( pPor->InTextGrp() || pPor->IsBreakPortion() ) + if( pPor->InTextGrp() || pPor->IsBreakPortion() || pPor->InTabGrp() ) + { + bNoText = false; + nTmpFirst = nX; + } + if( pPor->IsMultiPortion() && static_cast<SwMultiPortion*>(pPor)->HasTabulator() ) + { + if ( m_pCurr->IsSpaceAdd() ) + { + if ( ++nSpaceIdx < m_pCurr->GetLLSpaceAddCount() ) + nSpaceAdd = m_pCurr->GetLLSpaceAdd( nSpaceIdx ); + else + nSpaceAdd = 0; + } + + if( pKanaComp && ( nKanaIdx + 1 ) < pKanaComp->size() ) + ++nKanaIdx; + } + if( pPor->InFixMargGrp() ) + { + if( pPor->IsMarginPortion() ) + bNoText = false; + else + { + // fix margin portion => next SpaceAdd, KanaComp value + if ( m_pCurr->IsSpaceAdd() ) + { + if ( ++nSpaceIdx < m_pCurr->GetLLSpaceAddCount() ) + nSpaceAdd = m_pCurr->GetLLSpaceAdd( nSpaceIdx ); + else + nSpaceAdd = 0; + } + + if( pKanaComp && ( nKanaIdx + 1 ) < pKanaComp->size() ) + ++nKanaIdx; + } + } + pPor = pPor->GetNextPortion(); + } + + if( !pPor ) + { + // There's just Spezialportions. + nX = nTmpFirst; + } + else + { + if( !pPor->IsMarginPortion() && !pPor->IsPostItsPortion() && + (!pPor->InFieldGrp() || pPor->GetAscent() ) ) + { + nPorHeight = pPor->Height(); + nPorAscent = pPor->GetAscent(); + } + while( pPor && !pPor->IsBreakPortion() && ( aInf.GetIdx() < nOfst || + ( bWidth && ( pPor->IsKernPortion() || pPor->IsMultiPortion() ) ) ) ) + { + if( !pPor->IsMarginPortion() && !pPor->IsPostItsPortion() && + (!pPor->InFieldGrp() || pPor->GetAscent() ) ) + { + nPorHeight = pPor->Height(); + nPorAscent = pPor->GetAscent(); + } + + // If we are behind the portion, we add the portion width to + // nX. Special case: nOfst = aInf.GetIdx() + pPor->GetLen(). + // For common portions (including BidiPortions) we want to add + // the portion width to nX. For MultiPortions, nExtra = 0, + // therefore we go to the 'else' branch and start a recursion. + const TextFrameIndex nExtra( (pPor->IsMultiPortion() + && !static_cast<SwMultiPortion*>(pPor)->IsBidi() + && !bWidth) + ? 0 : 1 ); + if ( aInf.GetIdx() + pPor->GetLen() < nOfst + nExtra ) + { + if ( pPor->InSpaceGrp() && nSpaceAdd ) + nX += pPor->PrtWidth() + + pPor->CalcSpacing( nSpaceAdd, aInf ); + else + { + if( pPor->InFixMargGrp() && ! pPor->IsMarginPortion() ) + { + // update to current SpaceAdd, KanaComp values + if ( m_pCurr->IsSpaceAdd() ) + { + if ( ++nSpaceIdx < m_pCurr->GetLLSpaceAddCount() ) + nSpaceAdd = m_pCurr->GetLLSpaceAdd( nSpaceIdx ); + else + nSpaceAdd = 0; + } + + if ( pKanaComp && + ( nKanaIdx + 1 ) < pKanaComp->size() + ) + ++nKanaIdx; + } + if ( !pPor->IsFlyPortion() || ( pPor->GetNextPortion() && + !pPor->GetNextPortion()->IsMarginPortion() ) ) + nX += pPor->PrtWidth(); + } + if( pPor->IsMultiPortion() ) + { + if ( static_cast<SwMultiPortion*>(pPor)->HasTabulator() ) + { + if ( m_pCurr->IsSpaceAdd() ) + { + if ( ++nSpaceIdx < m_pCurr->GetLLSpaceAddCount() ) + nSpaceAdd = m_pCurr->GetLLSpaceAdd( nSpaceIdx ); + else + nSpaceAdd = 0; + } + + if( pKanaComp && ( nKanaIdx + 1 ) < pKanaComp->size() ) + ++nKanaIdx; + } + + // if we are right behind a BidiPortion, we have to + // hold a pointer to the BidiPortion in order to + // find the correct cursor position, depending on the + // cursor level + if ( static_cast<SwMultiPortion*>(pPor)->IsBidi() && + aInf.GetIdx() + pPor->GetLen() == nOfst ) + { + pLastBidiPor = static_cast<SwBidiPortion*>(pPor); + nLastBidiIdx = aInf.GetIdx(); + nLastBidiPorWidth = pLastBidiPor->Width() + + pLastBidiPor->CalcSpacing( nSpaceAdd, aInf ); + } + } + + aInf.SetIdx( aInf.GetIdx() + pPor->GetLen() ); + pPor = pPor->GetNextPortion(); + } + else + { + if( pPor->IsMultiPortion() ) + { + nTmpAscent = AdjustBaseLine( *m_pCurr, pPor ); + GetInfo().SetMulti( true ); + pOrig->Pos().AdjustY(nTmpAscent - nPorAscent ); + + if( pCMS && pCMS->m_b2Lines ) + { + const bool bRecursion (pCMS->m_p2Lines); + if ( !bRecursion ) + { + pCMS->m_p2Lines.reset(new Sw2LinesPos); + pCMS->m_p2Lines->aLine = SwRect(aCharPos, aCharSize); + } + + if( static_cast<SwMultiPortion*>(pPor)->HasRotation() ) + { + if( static_cast<SwMultiPortion*>(pPor)->IsRevers() ) + pCMS->m_p2Lines->nMultiType = MultiPortionType::ROT_270; + else + pCMS->m_p2Lines->nMultiType = MultiPortionType::ROT_90; + } + else if( static_cast<SwMultiPortion*>(pPor)->IsDouble() ) + pCMS->m_p2Lines->nMultiType = MultiPortionType::TWOLINE; + else if( static_cast<SwMultiPortion*>(pPor)->IsBidi() ) + pCMS->m_p2Lines->nMultiType = MultiPortionType::BIDI; + else + pCMS->m_p2Lines->nMultiType = MultiPortionType::RUBY; + + SwTwips nTmpWidth = pPor->Width(); + if( nSpaceAdd ) + nTmpWidth += pPor->CalcSpacing(nSpaceAdd, aInf); + + SwRect aRect( Point(aCharPos.X() + nX, pOrig->Top() ), + Size( nTmpWidth, pPor->Height() ) ); + + if ( ! bRecursion ) + pCMS->m_p2Lines->aPortion = aRect; + else + pCMS->m_p2Lines->aPortion2 = aRect; + } + + // In a multi-portion we use GetCharRect()-function + // recursively and must add the x-position + // of the multi-portion. + TextFrameIndex const nOldStart = m_nStart; + SwTwips nOldY = m_nY; + sal_uInt8 nOldProp = GetPropFont(); + m_nStart = aInf.GetIdx(); + SwLineLayout* pOldCurr = m_pCurr; + m_pCurr = &static_cast<SwMultiPortion*>(pPor)->GetRoot(); + if( static_cast<SwMultiPortion*>(pPor)->IsDouble() ) + SetPropFont( 50 ); + + SwTextGridItem const*const pGrid( + GetGridItem(GetTextFrame()->FindPageFrame())); + const bool bHasGrid = pGrid && GetInfo().SnapToGrid(); + const sal_uInt16 nRubyHeight = bHasGrid ? + pGrid->GetRubyHeight() : 0; + + if( m_nStart + m_pCurr->GetLen() <= nOfst && GetNext() && + ( ! static_cast<SwMultiPortion*>(pPor)->IsRuby() || + static_cast<SwMultiPortion*>(pPor)->OnTop() ) ) + { + sal_uInt16 nOffset; + // in grid mode we may only add the height of the + // ruby line if ruby line is on top + if ( bHasGrid && + static_cast<SwMultiPortion*>(pPor)->IsRuby() && + static_cast<SwMultiPortion*>(pPor)->OnTop() ) + nOffset = nRubyHeight; + else + nOffset = GetLineHeight(); + + pOrig->Pos().AdjustY(nOffset ); + Next(); + } + + const bool bSpaceChg = static_cast<SwMultiPortion*>(pPor)-> + ChgSpaceAdd( m_pCurr, nSpaceAdd ); + Point aOldPos = pOrig->Pos(); + + // Ok, for ruby portions in grid mode we have to + // temporarily set the inner line height to the + // outer line height because that value is needed + // for the adjustment inside the recursion + const sal_uInt16 nOldRubyHeight = m_pCurr->Height(); + const sal_uInt16 nOldRubyRealHeight = m_pCurr->GetRealHeight(); + const bool bChgHeight = + static_cast<SwMultiPortion*>(pPor)->IsRuby() && bHasGrid; + + if ( bChgHeight ) + { + m_pCurr->Height( pOldCurr->Height() - nRubyHeight ); + m_pCurr->SetRealHeight( pOldCurr->GetRealHeight() - + nRubyHeight ); + } + + SwLayoutModeModifier aLayoutModeModifier( *GetInfo().GetOut() ); + if ( static_cast<SwMultiPortion*>(pPor)->IsBidi() ) + { + aLayoutModeModifier.Modify( + static_cast<SwBidiPortion*>(pPor)->GetLevel() % 2 ); + } + + GetCharRect_( pOrig, nOfst, pCMS ); + + if ( bChgHeight ) + { + m_pCurr->Height( nOldRubyHeight ); + m_pCurr->SetRealHeight( nOldRubyRealHeight ); + } + + // if we are still in the first row of + // our 2 line multiportion, we use the FirstMulti flag + // to indicate this + if ( static_cast<SwMultiPortion*>(pPor)->IsDouble() ) + { + // the recursion may have damaged our font size + SetPropFont( nOldProp ); + GetInfo().GetFont()->SetProportion( 100 ); + + if ( m_pCurr == &static_cast<SwMultiPortion*>(pPor)->GetRoot() ) + { + GetInfo().SetFirstMulti( true ); + + // we want to treat a double line portion like a + // single line portion, if there is no text in + // the second line + if ( !m_pCurr->GetNext() || + !m_pCurr->GetNext()->GetLen() ) + GetInfo().SetMulti( false ); + } + } + // ruby portions are treated like single line portions + else if( static_cast<SwMultiPortion*>(pPor)->IsRuby() || + static_cast<SwMultiPortion*>(pPor)->IsBidi() ) + GetInfo().SetMulti( false ); + + // calculate cursor values + if( static_cast<SwMultiPortion*>(pPor)->HasRotation() ) + { + GetInfo().SetMulti( false ); + tools::Long nTmp = pOrig->Width(); + pOrig->Width( pOrig->Height() ); + pOrig->Height( nTmp ); + nTmp = pOrig->Left() - aOldPos.X(); + + // if we travel into our rotated portion from + // a line below, we have to take care, that the + // y coord in pOrig is less than line height: + if ( nTmp ) + nTmp--; + + pOrig->Pos().setX( nX + aOldPos.X() ); + if( static_cast<SwMultiPortion*>(pPor)->IsRevers() ) + pOrig->Pos().setY( aOldPos.Y() + nTmp ); + else + pOrig->Pos().setY( aOldPos.Y() + + pPor->Height() - nTmp - pOrig->Height() ); + if ( pCMS && pCMS->m_bRealHeight ) + { + pCMS->m_aRealHeight.setY( -pCMS->m_aRealHeight.Y() ); + // result for rotated multi portion is not + // correct for reverse (270 degree) portions + if( static_cast<SwMultiPortion*>(pPor)->IsRevers() ) + { + if ( SvxParaVertAlignItem::Align::Automatic == + GetLineInfo().GetVertAlign() ) + // if vertical alignment is set to auto, + // we switch from base line alignment + // to centered alignment + pCMS->m_aRealHeight.setX( + ( pOrig->Width() + + pCMS->m_aRealHeight.Y() ) / 2 ); + else + pCMS->m_aRealHeight.setX( + pOrig->Width() - + pCMS->m_aRealHeight.X() + + pCMS->m_aRealHeight.Y() ); + } + } + } + else + { + pOrig->Pos().AdjustY(aOldPos.Y() ); + if ( static_cast<SwMultiPortion*>(pPor)->IsBidi() ) + { + const SwTwips nPorWidth = pPor->Width() + + pPor->CalcSpacing( nSpaceAdd, aInf ); + const SwTwips nInsideOfst = pOrig->Pos().X(); + pOrig->Pos().setX( nX + nPorWidth - + nInsideOfst - pOrig->Width() ); + } + else + pOrig->Pos().AdjustX(nX ); + + if( static_cast<SwMultiPortion*>(pPor)->HasBrackets() ) + pOrig->Pos().AdjustX( + static_cast<SwDoubleLinePortion*>(pPor)->PreWidth() ); + } + + if( bSpaceChg ) + SwDoubleLinePortion::ResetSpaceAdd( m_pCurr ); + + m_pCurr = pOldCurr; + m_nStart = nOldStart; + m_nY = nOldY; + m_bPrev = false; + + return; + } + if ( pPor->PrtWidth() ) + { + // tdf#30731: To get the correct nOfst width, we need + // to send the whole portion string to GetTextSize() + // and ask it to return the width of nOfst by calling + // SetMeasureLen(). Cutting the string at nOfst can + // give the wrong width if nOfst is in e.g. the middle + // of a ligature. See SwFntObj::DrawText(). + TextFrameIndex const nOldLen = pPor->GetLen(); + aInf.SetLen( pPor->GetLen() ); + pPor->SetLen( nOfst - aInf.GetIdx() ); + aInf.SetMeasureLen(pPor->GetLen()); + if (aInf.GetLen() < aInf.GetMeasureLen()) + { + pPor->SetLen(aInf.GetMeasureLen()); + aInf.SetLen(pPor->GetLen()); + } + if( nX || !pPor->InNumberGrp() ) + { + SeekAndChg( aInf ); + const bool bOldOnWin = aInf.OnWin(); + aInf.SetOnWin( false ); // no BULLETs! + SwTwips nTmp = nX; + aInf.SetKanaComp( pKanaComp ); + aInf.SetKanaIdx( nKanaIdx ); + nX += pPor->GetTextSize( aInf ).Width(); + aInf.SetOnWin( bOldOnWin ); + if ( pPor->InSpaceGrp() && nSpaceAdd ) + nX += pPor->CalcSpacing( nSpaceAdd, aInf ); + if( bWidth ) + { + pPor->SetLen(pPor->GetLen() + TextFrameIndex(1)); + aInf.SetMeasureLen(pPor->GetLen()); + if (aInf.GetLen() < aInf.GetMeasureLen()) + { + pPor->SetLen(aInf.GetMeasureLen()); + aInf.SetLen(pPor->GetLen()); + } + aInf.SetOnWin( false ); // no BULLETs! + nTmp += pPor->GetTextSize( aInf ).Width(); + aInf.SetOnWin( bOldOnWin ); + if ( pPor->InSpaceGrp() && nSpaceAdd ) + nTmp += pPor->CalcSpacing(nSpaceAdd, aInf); + pOrig->Width( nTmp - nX ); + } + } + pPor->SetLen( nOldLen ); + + // Shift the cursor with the right border width + // Note: nX remains positive because GetTextSize() also include the width of the right border + if( aInf.GetIdx() < nOfst && nOfst < aInf.GetIdx() + pPor->GetLen() ) + { + // Find the current drop portion part and use its right border + if( pPor->IsDropPortion() && static_cast<SwDropPortion*>(pPor)->GetLines() > 1 ) + { + SwDropPortion* pDrop = static_cast<SwDropPortion*>(pPor); + const SwDropPortionPart* pCurrPart = pDrop->GetPart(); + TextFrameIndex nSumLength(0); + while( pCurrPart && (nSumLength += pCurrPart->GetLen()) < nOfst - aInf.GetIdx() ) + { + pCurrPart = pCurrPart->GetFollow(); + } + if( pCurrPart && nSumLength != nOfst - aInf.GetIdx() && + pCurrPart->GetFont().GetRightBorder() && !pCurrPart->GetJoinBorderWithNext() ) + { + nX -= pCurrPart->GetFont().GetRightBorderSpace(); + } + } + else if( GetInfo().GetFont()->GetRightBorder() && !pPor->GetJoinBorderWithNext()) + { + nX -= GetInfo().GetFont()->GetRightBorderSpace(); + } + } + } + bWidth = false; + break; + } + } + } + + if( pPor ) + { + OSL_ENSURE( !pPor->InNumberGrp() || bInsideFirstField, "Number surprise" ); + bool bEmptyField = false; + if( pPor->InFieldGrp() && pPor->GetLen() ) + { + SwFieldPortion *pTmp = static_cast<SwFieldPortion*>(pPor); + while( pTmp->HasFollow() && pTmp->GetExp().isEmpty() ) + { + sal_uInt16 nAddX = pTmp->Width(); + SwLinePortion *pNext = pTmp->GetNextPortion(); + while( pNext && !pNext->InFieldGrp() ) + { + OSL_ENSURE( !pNext->GetLen(), "Where's my field follow?" ); + nAddX = nAddX + pNext->Width(); + pNext = pNext->GetNextPortion(); + } + if( !pNext ) + break; + pTmp = static_cast<SwFieldPortion*>(pNext); + nPorHeight = pTmp->Height(); + nPorAscent = pTmp->GetAscent(); + nX += nAddX; + bEmptyField = true; + } + } + // 8513: Fields in justified text, skipped + while( pPor && !pPor->GetLen() && ! bInsideFirstField && + ( pPor->IsFlyPortion() || pPor->IsKernPortion() || + pPor->IsBlankPortion() || pPor->InTabGrp() || + ( !bEmptyField && pPor->InFieldGrp() ) ) ) + { + if ( pPor->InSpaceGrp() && nSpaceAdd ) + nX += pPor->PrtWidth() + + pPor->CalcSpacing( nSpaceAdd, aInf ); + else + { + if( pPor->InFixMargGrp() && ! pPor->IsMarginPortion() ) + { + if ( m_pCurr->IsSpaceAdd() ) + { + if ( ++nSpaceIdx < m_pCurr->GetLLSpaceAddCount() ) + nSpaceAdd = m_pCurr->GetLLSpaceAdd( nSpaceIdx ); + else + nSpaceAdd = 0; + } + + if( pKanaComp && ( nKanaIdx + 1 ) < pKanaComp->size() ) + ++nKanaIdx; + } + if ( !pPor->IsFlyPortion() || ( pPor->GetNextPortion() && + !pPor->GetNextPortion()->IsMarginPortion() ) ) + nX += pPor->PrtWidth(); + } + if( pPor->IsMultiPortion() && + static_cast<SwMultiPortion*>(pPor)->HasTabulator() ) + { + if ( m_pCurr->IsSpaceAdd() ) + { + if ( ++nSpaceIdx < m_pCurr->GetLLSpaceAddCount() ) + nSpaceAdd = m_pCurr->GetLLSpaceAdd( nSpaceIdx ); + else + nSpaceAdd = 0; + } + + if( pKanaComp && ( nKanaIdx + 1 ) < pKanaComp->size() ) + ++nKanaIdx; + } + if( !pPor->IsFlyPortion() ) + { + nPorHeight = pPor->Height(); + nPorAscent = pPor->GetAscent(); + } + pPor = pPor->GetNextPortion(); + } + + if( aInf.GetIdx() == nOfst && pPor && pPor->InHyphGrp() && + pPor->GetNextPortion() && pPor->GetNextPortion()->InFixGrp() ) + { + // All special portions have to be skipped + // Taking the German word "zusammen" as example: zu-[FLY]sammen, 'u' == 19, 's' == 20; Right() + // Without the adjustment we end up in front of '-', with the + // adjustment in front of the 's'. + while( pPor && !pPor->GetLen() ) + { + nX += pPor->Width(); + if( !pPor->IsMarginPortion() ) + { + nPorHeight = pPor->Height(); + nPorAscent = pPor->GetAscent(); + } + pPor = pPor->GetNextPortion(); + } + } + if( pPor && pCMS ) + { + if( pCMS->m_bFieldInfo && pPor->InFieldGrp() && pPor->Width() ) + pOrig->Width( pPor->Width() ); + if( pPor->IsDropPortion() ) + { + nPorAscent = static_cast<SwDropPortion*>(pPor)->GetDropHeight(); + // The drop height is only calculated, if we have more than + // one line. Otherwise it is 0. + if ( ! nPorAscent) + nPorAscent = pPor->Height(); + nPorHeight = nPorAscent; + pOrig->Height( nPorHeight + + static_cast<SwDropPortion*>(pPor)->GetDropDescent() ); + if( nTmpHeight < pOrig->Height() ) + { + nTmpAscent = nPorAscent; + nTmpHeight = sal_uInt16( pOrig->Height() ); + } + } + if( bWidth && pPor->PrtWidth() && pPor->GetLen() && + aInf.GetIdx() == nOfst ) + { + if( !pPor->IsFlyPortion() && pPor->Height() && + pPor->GetAscent() ) + { + nPorHeight = pPor->Height(); + nPorAscent = pPor->GetAscent(); + } + SwTwips nTmp; + if (TextFrameIndex(2) > pPor->GetLen()) + { + nTmp = pPor->Width(); + if ( pPor->InSpaceGrp() && nSpaceAdd ) + nTmp += pPor->CalcSpacing( nSpaceAdd, aInf ); + } + else + { + const bool bOldOnWin = aInf.OnWin(); + TextFrameIndex const nOldLen = pPor->GetLen(); + aInf.SetLen( pPor->GetLen() ); + pPor->SetLen( TextFrameIndex(1) ); + aInf.SetMeasureLen(pPor->GetLen()); + if (aInf.GetLen() < aInf.GetMeasureLen()) + { + pPor->SetLen(aInf.GetMeasureLen()); + aInf.SetLen(pPor->GetLen()); + } + SeekAndChg( aInf ); + aInf.SetOnWin( false ); // no BULLETs! + aInf.SetKanaComp( pKanaComp ); + aInf.SetKanaIdx( nKanaIdx ); + nTmp = pPor->GetTextSize( aInf ).Width(); + aInf.SetOnWin( bOldOnWin ); + if ( pPor->InSpaceGrp() && nSpaceAdd ) + nTmp += pPor->CalcSpacing( nSpaceAdd, aInf ); + pPor->SetLen( nOldLen ); + } + pOrig->Width( nTmp ); + } + + // travel inside field portion? + if ( pCMS->m_pSpecialPos ) + { + // apply attributes to font + Seek( nOfst ); + lcl_GetCharRectInsideField( aInf, *pOrig, *pCMS, *pPor ); + } + } + } + + // special case: We are at the beginning of a BidiPortion or + // directly behind a BidiPortion + if ( pCMS && + ( pLastBidiPor || + ( pPor && + pPor->IsMultiPortion() && + static_cast<SwMultiPortion*>(pPor)->IsBidi() ) ) ) + { + // we determine if the cursor has to blink before or behind + // the bidi portion + if ( pLastBidiPor ) + { + const sal_uInt8 nPortionLevel = pLastBidiPor->GetLevel(); + + if ( pCMS->m_nCursorBidiLevel >= nPortionLevel ) + { + // we came from inside the bidi portion, we want to blink + // behind the portion + pOrig->Pos().AdjustX( -nLastBidiPorWidth ); + + // Again, there is a special case: logically behind + // the portion can actually mean that the cursor is inside + // the portion. This can happen is the last portion + // inside the bidi portion is a nested bidi portion + SwLineLayout& rLineLayout = + static_cast<SwMultiPortion*>(pLastBidiPor)->GetRoot(); + + const SwLinePortion *pLast = rLineLayout.FindLastPortion(); + if ( pLast->IsMultiPortion() ) + { + OSL_ENSURE( static_cast<const SwMultiPortion*>(pLast)->IsBidi(), + "Non-BidiPortion inside BidiPortion" ); + TextFrameIndex const nIdx = aInf.GetIdx(); + // correct the index before using CalcSpacing. + aInf.SetIdx(nLastBidiIdx); + pOrig->Pos().AdjustX(pLast->Width() + + pLast->CalcSpacing( nSpaceAdd, aInf ) ); + aInf.SetIdx(nIdx); + } + } + } + else + { + const sal_uInt8 nPortionLevel = static_cast<SwBidiPortion*>(pPor)->GetLevel(); + + if ( pCMS->m_nCursorBidiLevel >= nPortionLevel ) + { + // we came from inside the bidi portion, we want to blink + // behind the portion + pOrig->Pos().AdjustX(pPor->Width() + + pPor->CalcSpacing( nSpaceAdd, aInf ) ); + } + } + } + + pOrig->Pos().AdjustX(nX ); + + if ( pCMS && pCMS->m_bRealHeight ) + { + nTmpAscent = AdjustBaseLine( *m_pCurr, nullptr, nPorHeight, nPorAscent ); + if ( nTmpAscent > nPorAscent ) + pCMS->m_aRealHeight.setX( nTmpAscent - nPorAscent ); + else + pCMS->m_aRealHeight.setX( 0 ); + OSL_ENSURE( nPorHeight, "GetCharRect: Missing Portion-Height" ); + if ( nTmpHeight > nPorHeight ) + pCMS->m_aRealHeight.setY( nPorHeight ); + else + pCMS->m_aRealHeight.setY( nTmpHeight ); + } + } +} + +void SwTextCursor::GetCharRect( SwRect* pOrig, TextFrameIndex const nOfst, + SwCursorMoveState* pCMS, const tools::Long nMax ) +{ + CharCursorToLine(nOfst); + + // Indicates that a position inside a special portion (field, number portion) + // is requested. + const bool bSpecialPos = pCMS && pCMS->m_pSpecialPos; + TextFrameIndex nFindOfst = nOfst; + + if ( bSpecialPos ) + { + const SwSPExtendRange nExtendRange = pCMS->m_pSpecialPos->nExtendRange; + + OSL_ENSURE( ! pCMS->m_pSpecialPos->nLineOfst || SwSPExtendRange::BEFORE != nExtendRange, + "LineOffset AND Number Portion?" ); + + // portions which are behind the string + if ( SwSPExtendRange::BEHIND == nExtendRange ) + ++nFindOfst; + + // skip lines for fields which cover more than one line + for ( sal_Int32 i = 0; i < pCMS->m_pSpecialPos->nLineOfst; i++ ) + Next(); + } + + // If necessary, as catch up, do the adjustment + GetAdjusted(); + AddExtraBlankWidth(); + + const Point aCharPos( GetTopLeft() ); + + GetCharRect_( pOrig, nFindOfst, pCMS ); + + pOrig->Pos().AdjustX(aCharPos.X() ); + pOrig->Pos().AdjustY(aCharPos.Y() ); + + if( pCMS && pCMS->m_b2Lines && pCMS->m_p2Lines ) + { + pCMS->m_p2Lines->aLine.Pos().AdjustX(aCharPos.X() ); + pCMS->m_p2Lines->aLine.Pos().AdjustY(aCharPos.Y() ); + pCMS->m_p2Lines->aPortion.Pos().AdjustX(aCharPos.X() ); + pCMS->m_p2Lines->aPortion.Pos().AdjustY(aCharPos.Y() ); + } + + if( nMax ) + { + if( pOrig->Top() + pOrig->Height() > nMax ) + { + if( pOrig->Top() > nMax ) + pOrig->Top( nMax ); + pOrig->Height( nMax - pOrig->Top() ); + } + if ( pCMS && pCMS->m_bRealHeight && pCMS->m_aRealHeight.Y() >= 0 ) + { + tools::Long nTmp = pCMS->m_aRealHeight.X() + pOrig->Top(); + if( nTmp >= nMax ) + { + pCMS->m_aRealHeight.setX( nMax - pOrig->Top() ); + pCMS->m_aRealHeight.setY( 0 ); + } + else if( nTmp + pCMS->m_aRealHeight.Y() > nMax ) + pCMS->m_aRealHeight.setY( nMax - nTmp ); + } + } +} + +/** + * Determines if SwTextCursor::GetModelPositionForViewPoint() should consider the next portion when calculating the + * doc model position from a Point. + */ +static bool ConsiderNextPortionForCursorOffset(const SwLinePortion* pPor, SwTwips nWidth30, sal_uInt16 nX) +{ + if (!pPor->GetNextPortion() || pPor->IsBreakPortion()) + { + return false; + } + + // tdf#138592: consider all following zero-width text portions of current text portion, + // like combining characters. + if (nWidth30 == nX && pPor->IsTextPortion() && pPor->GetNextPortion()->IsTextPortion() + && pPor->GetNextPortion()->Width() == 0) + return true; + + // If we're past the target position, stop the iteration in general. + // Exception: don't stop the iteration between as-char fly portions and their comments. + if (nWidth30 >= nX && (!pPor->IsFlyCntPortion() || !pPor->GetNextPortion()->IsPostItsPortion())) + { + // Normally returns false. + + // Another exception: If the cursor is at the very end of the portion, and the next portion is a comment, + // then place the cursor after the zero-width comment. This is primarily to benefit the very end of a line. + return nWidth30 == nX && pPor->GetNextPortion()->IsPostItsPortion(); + } + + return true; +} + +// Return: Offset in String +TextFrameIndex SwTextCursor::GetModelPositionForViewPoint( SwPosition *pPos, const Point &rPoint, + bool bChgNode, SwCursorMoveState* pCMS ) const +{ + // If necessary, as catch up, do the adjustment + GetAdjusted(); + + const OUString &rText = GetInfo().GetText(); + TextFrameIndex nOffset(0); + + // x is the horizontal offset within the line. + SwTwips x = rPoint.X(); + const SwTwips nLeftMargin = GetLineStart(); + SwTwips nRightMargin = GetLineEnd() + + ( GetCurr()->IsHanging() ? GetCurr()->GetHangingMargin() : 0 ); + if( nRightMargin == nLeftMargin ) + nRightMargin += 30; + + const bool bLeftOver = x < nLeftMargin; + if( bLeftOver ) + x = nLeftMargin; + const bool bRightOver = x > nRightMargin; + const bool bRightAllowed = pCMS && ( pCMS->m_eState == CursorMoveState::NONE ); + + // Until here everything in document coordinates. + x -= nLeftMargin; + + SwTwips nX = x; + + // If there are attribute changes in the line, search for the paragraph, + // in which nX is situated. + SwLinePortion *pPor = m_pCurr->GetFirstPortion(); + TextFrameIndex nCurrStart = m_nStart; + bool bLastHyph = false; + + std::deque<sal_uInt16> *pKanaComp = m_pCurr->GetpKanaComp(); + TextFrameIndex const nOldIdx = GetInfo().GetIdx(); + sal_uInt16 nSpaceIdx = 0; + size_t nKanaIdx = 0; + tools::Long nSpaceAdd = m_pCurr->IsSpaceAdd() ? m_pCurr->GetLLSpaceAdd( 0 ) : 0; + short nKanaComp = pKanaComp ? (*pKanaComp)[0] : 0; + + // nWidth is the width of the line, or the width of + // the paragraph with the font change, in which nX is situated. + + SwTwips nWidth = pPor->Width(); + if ( m_pCurr->IsSpaceAdd() || pKanaComp ) + { + if ( pPor->InSpaceGrp() && nSpaceAdd ) + { + const_cast<SwTextSizeInfo&>(GetInfo()).SetIdx( nCurrStart ); + nWidth = nWidth + pPor->CalcSpacing( nSpaceAdd, GetInfo() ); + } + if( ( pPor->InFixMargGrp() && ! pPor->IsMarginPortion() ) || + ( pPor->IsMultiPortion() && static_cast<SwMultiPortion*>(pPor)->HasTabulator() ) + ) + { + if ( m_pCurr->IsSpaceAdd() ) + { + if ( ++nSpaceIdx < m_pCurr->GetLLSpaceAddCount() ) + nSpaceAdd = m_pCurr->GetLLSpaceAdd( nSpaceIdx ); + else + nSpaceAdd = 0; + } + + if( pKanaComp ) + { + if ( nKanaIdx + 1 < pKanaComp->size() ) + nKanaComp = (*pKanaComp)[++nKanaIdx]; + else + nKanaComp = 0; + } + } + } + + SwTwips nWidth30; + if ( pPor->IsPostItsPortion() ) + nWidth30 = 0; + else + nWidth30 = ! nWidth && pPor->GetLen() && pPor->InToxRefOrFieldGrp() ? + 30 : + nWidth; + + while (ConsiderNextPortionForCursorOffset(pPor, nWidth30, nX)) + { + nX = nX - nWidth; + nCurrStart = nCurrStart + pPor->GetLen(); + pPor = pPor->GetNextPortion(); + nWidth = pPor->Width(); + if ( m_pCurr->IsSpaceAdd() || pKanaComp ) + { + if ( pPor->InSpaceGrp() && nSpaceAdd ) + { + const_cast<SwTextSizeInfo&>(GetInfo()).SetIdx( nCurrStart ); + nWidth = nWidth + pPor->CalcSpacing( nSpaceAdd, GetInfo() ); + } + + if( ( pPor->InFixMargGrp() && ! pPor->IsMarginPortion() ) || + ( pPor->IsMultiPortion() && static_cast<SwMultiPortion*>(pPor)->HasTabulator() ) + ) + { + if ( m_pCurr->IsSpaceAdd() ) + { + if ( ++nSpaceIdx < m_pCurr->GetLLSpaceAddCount() ) + nSpaceAdd = m_pCurr->GetLLSpaceAdd( nSpaceIdx ); + else + nSpaceAdd = 0; + } + + if ( pKanaComp ) + { + if( nKanaIdx + 1 < pKanaComp->size() ) + nKanaComp = (*pKanaComp)[++nKanaIdx]; + else + nKanaComp = 0; + } + } + } + + if ( pPor->IsPostItsPortion() ) + nWidth30 = 0; + else + nWidth30 = ! nWidth && pPor->GetLen() && pPor->InToxRefOrFieldGrp() ? + 30 : + nWidth; + if( !pPor->IsFlyPortion() && !pPor->IsMarginPortion() ) + bLastHyph = pPor->InHyphGrp(); + } + + const bool bLastPortion = (nullptr == pPor->GetNextPortion()); + + if( nX==nWidth ) + { + SwLinePortion *pNextPor = pPor->GetNextPortion(); + while( pNextPor && pNextPor->InFieldGrp() && !pNextPor->Width() ) + { + nCurrStart = nCurrStart + pPor->GetLen(); + pPor = pNextPor; + if( !pPor->IsFlyPortion() && !pPor->IsMarginPortion() ) + bLastHyph = pPor->InHyphGrp(); + pNextPor = pPor->GetNextPortion(); + } + } + + const_cast<SwTextSizeInfo&>(GetInfo()).SetIdx( nOldIdx ); + + TextFrameIndex nLength = pPor->GetLen(); + + const bool bFieldInfo = pCMS && pCMS->m_bFieldInfo; + + if( bFieldInfo && ( nWidth30 < nX || bRightOver || bLeftOver || + ( pPor->InNumberGrp() && !pPor->IsFootnoteNumPortion() ) || + ( pPor->IsMarginPortion() && nWidth > nX + 30 ) ) ) + pCMS->m_bPosCorr = true; + + // #i27615# + if (pCMS && pCMS->m_bInFrontOfLabel) + { + if (2 * nX >= nWidth || !pPor->InNumberGrp() || pPor->IsFootnoteNumPortion()) + pCMS->m_bInFrontOfLabel = false; + } + + // 7684: We are exactly ended up at their HyphPortion. It is our task to + // provide, that we end up in the String. + // 7993: If length = 0, then we must exit... + if( !nLength ) + { + if( pCMS ) + { + if( pPor->IsFlyPortion() && bFieldInfo ) + pCMS->m_bPosCorr = true; + + if (!bRightOver && nX) + { + if( pPor->IsFootnoteNumPortion()) + pCMS->m_bFootnoteNoInfo = true; + else if (pPor->InNumberGrp() ) // #i23726# + { + pCMS->m_nInNumPortionOffset = nX; + pCMS->m_bInNumPortion = true; + } + } + } + if( !nCurrStart ) + return TextFrameIndex(0); + + // 7849, 7816: pPor->GetHyphPortion is mandatory! + if( ( !bRightAllowed && bLastHyph ) || + ( pPor->IsMarginPortion() && !pPor->GetNextPortion() && + // 46598: Consider the situation: We might end up behind the last character, + // in the last line of a centered paragraph + nCurrStart < TextFrameIndex(rText.getLength()))) + --nCurrStart; + else if( pPor->InFieldGrp() && static_cast<SwFieldPortion*>(pPor)->IsFollow() + && nWidth > nX ) + { + if( bFieldInfo ) + --nCurrStart; + else + { + sal_uInt16 nHeight = pPor->Height(); + if ( !nHeight || nHeight > nWidth ) + nHeight = nWidth; + if( bChgNode && nWidth - nHeight/2 > nX ) + --nCurrStart; + } + } + if (!pPor->InFieldGrp() || !static_cast<SwFieldPortion const*>(pPor)->IsFollow() + || !pCMS || !pCMS->m_pSpecialPos) + { + return nCurrStart; + } + } + if (TextFrameIndex(1) == nLength || pPor->InFieldGrp()) + { + if ( nWidth ) + { + // no quick return for as-character frames, we want to peek inside + if (!(bChgNode && pPos && pPor->IsFlyCntPortion()) + // if we want to get the position inside the field, we should not return + && (!pCMS || !pCMS->m_pSpecialPos)) + { + if ( pPor->InFieldGrp() || + ( pPor->IsMultiPortion() && + static_cast<SwMultiPortion*>(pPor)->IsBidi() ) ) + { + sal_uInt16 nHeight = 0; + if( !bFieldInfo ) + { + nHeight = pPor->Height(); + if ( !nHeight || nHeight > nWidth ) + nHeight = nWidth; + } + + if( nWidth - nHeight/2 <= nX && + ( ! pPor->InFieldGrp() || + !static_cast<SwFieldPortion*>(pPor)->HasFollow() ) ) + { + if (pPor->InFieldGrp()) + { + nCurrStart += static_cast<SwFieldPortion*>(pPor)->GetFieldLen(); + } + else + { + ++nCurrStart; + } + } + } + else if ( ( !pPor->IsFlyPortion() || ( pPor->GetNextPortion() && + !pPor->GetNextPortion()->IsMarginPortion() && + !pPor->GetNextPortion()->IsHolePortion() ) ) + && ( nWidth/2 < nX ) && + ( !bFieldInfo || + ( pPor->GetNextPortion() && + pPor->GetNextPortion()->IsPostItsPortion() ) ) + && ( bRightAllowed || !bLastHyph )) + ++nCurrStart; + + return nCurrStart; + } + } + else + { + if ( pPor->IsPostItsPortion() || pPor->IsBreakPortion() || + pPor->InToxRefGrp() ) + { + SwPostItsPortion* pPostItsPortion = pPor->IsPostItsPortion() ? dynamic_cast<SwPostItsPortion*>(pPor) : nullptr; + if (pPostItsPortion) + { + if (!pPostItsPortion->IsScript()) // tdf#141079 + { + // Offset would be nCurrStart + nLength below, do the same for post-it portions. + nCurrStart += pPor->GetLen(); + } + } + return nCurrStart; + } + if ( pPor->InFieldGrp() ) + { + if( bRightOver && !static_cast<SwFieldPortion*>(pPor)->HasFollow() ) + { + nCurrStart += static_cast<SwFieldPortion*>(pPor)->GetFieldLen(); + } + return nCurrStart; + } + } + } + + // Skip space at the end of the line + if( bLastPortion && (m_pCurr->GetNext() || m_pFrame->GetFollow() ) + && sal_Int32(nLength) != 0 + && rText[sal_Int32(nCurrStart + nLength) - 1] == ' ') + { + --nLength; + } + + if( nWidth > nX || + ( nWidth == nX && pPor->IsMultiPortion() && static_cast<SwMultiPortion*>(pPor)->IsDouble() ) ) + { + if( pPor->IsMultiPortion() ) + { + // In a multi-portion we use GetModelPositionForViewPoint()-function recursively + SwTwips nTmpY = rPoint.Y() - m_pCurr->GetAscent() + pPor->GetAscent(); + // if we are in the first line of a double line portion, we have + // to add a value to nTmpY for not staying in this line + // we also want to skip the first line, if we are inside ruby + if ( ( static_cast<SwTextSizeInfo*>(m_pInf)->IsMulti() && + static_cast<SwTextSizeInfo*>(m_pInf)->IsFirstMulti() ) || + ( static_cast<SwMultiPortion*>(pPor)->IsRuby() && + static_cast<SwMultiPortion*>(pPor)->OnTop() ) ) + nTmpY += static_cast<SwMultiPortion*>(pPor)->Height(); + + // Important for cursor traveling in ruby portions: + // We have to set nTmpY to 0 in order to stay in the first row + // if the phonetic line is the second row + if ( static_cast<SwMultiPortion*>(pPor)->IsRuby() && + ! static_cast<SwMultiPortion*>(pPor)->OnTop() ) + nTmpY = 0; + + SwTextCursorSave aSave( const_cast<SwTextCursor*>(this), static_cast<SwMultiPortion*>(pPor), + nTmpY, nX, nCurrStart, nSpaceAdd ); + + SwLayoutModeModifier aLayoutModeModifier( *GetInfo().GetOut() ); + if ( static_cast<SwMultiPortion*>(pPor)->IsBidi() ) + { + const sal_uInt8 nBidiLevel = static_cast<SwBidiPortion*>(pPor)->GetLevel(); + aLayoutModeModifier.Modify( nBidiLevel % 2 ); + } + + if( static_cast<SwMultiPortion*>(pPor)->HasRotation() ) + { + nTmpY -= m_nY; + if( !static_cast<SwMultiPortion*>(pPor)->IsRevers() ) + nTmpY = pPor->Height() - nTmpY; + if( nTmpY < 0 ) + nTmpY = 0; + nX = o3tl::narrowing<sal_uInt16>(nTmpY); + } + + if( static_cast<SwMultiPortion*>(pPor)->HasBrackets() ) + { + const sal_uInt16 nPreWidth = static_cast<SwDoubleLinePortion*>(pPor)->PreWidth(); + if ( nX > nPreWidth ) + nX = nX - nPreWidth; + else + nX = 0; + } + + return GetModelPositionForViewPoint( pPos, Point( GetLineStart() + nX, rPoint.Y() ), + bChgNode, pCMS ); + } + if( pPor->InTextGrp() || pPor->IsHolePortion() ) + { + sal_uInt8 nOldProp; + if( GetPropFont() ) + { + const_cast<SwFont*>(GetFnt())->SetProportion( GetPropFont() ); + nOldProp = GetFnt()->GetPropr(); + } + else + nOldProp = 0; + { + SwTextSizeInfo aSizeInf( GetInfo(), &rText, nCurrStart ); + const_cast<SwTextCursor*>(this)->SeekAndChg( aSizeInf ); + SwTextSlot aDiffText( &aSizeInf, pPor, false, false ); + SwFontSave aSave( aSizeInf, pPor->IsDropPortion() ? + static_cast<SwDropPortion*>(pPor)->GetFnt() : nullptr ); + + SwParaPortion* pPara = const_cast<SwParaPortion*>(GetInfo().GetParaPortion()); + OSL_ENSURE( pPara, "No paragraph!" ); + + // protect against bugs elsewhere + SAL_WARN_IF( aSizeInf.GetIdx().get() + pPor->GetLen().get() > aSizeInf.GetText().getLength(), "sw", "portion and text are out of sync" ); + TextFrameIndex nSafeLen( std::min(pPor->GetLen().get(), aSizeInf.GetText().getLength() - aSizeInf.GetIdx().get()) ); + + SwDrawTextInfo aDrawInf( aSizeInf.GetVsh(), + *aSizeInf.GetOut(), + &pPara->GetScriptInfo(), + aSizeInf.GetText(), + aSizeInf.GetIdx(), + nSafeLen ); + + // Drop portion works like a multi portion, just its parts are not portions + if( pPor->IsDropPortion() && static_cast<SwDropPortion*>(pPor)->GetLines() > 1 ) + { + SwDropPortion* pDrop = static_cast<SwDropPortion*>(pPor); + const SwDropPortionPart* pCurrPart = pDrop->GetPart(); + sal_uInt16 nSumWidth = 0; + sal_uInt16 nSumBorderWidth = 0; + // Shift offset with the right and left border of previous parts and left border of actual one + while (pCurrPart && nSumWidth <= nX - sal_Int32(nCurrStart)) + { + nSumWidth += pCurrPart->GetWidth(); + if( pCurrPart->GetFont().GetLeftBorder() && !pCurrPart->GetJoinBorderWithPrev() ) + { + nSumBorderWidth += pCurrPart->GetFont().GetLeftBorderSpace(); + } + if (nSumWidth <= nX - sal_Int32(nCurrStart) && pCurrPart->GetFont().GetRightBorder() && + !pCurrPart->GetJoinBorderWithNext() ) + { + nSumBorderWidth += pCurrPart->GetFont().GetRightBorderSpace(); + } + pCurrPart = pCurrPart->GetFollow(); + } + nX = std::max(static_cast<SwTwips>(0), nX - nSumBorderWidth); + } + // Shift the offset with the left border width + else if( GetInfo().GetFont()->GetLeftBorder() && !pPor->GetJoinBorderWithPrev() ) + { + nX = std::max(static_cast<SwTwips>(0), nX - GetInfo().GetFont()->GetLeftBorderSpace()); + } + + aDrawInf.SetOffset( nX ); + + if ( nSpaceAdd ) + { + TextFrameIndex nCharCnt(0); + // #i41860# Thai justified alignment needs some + // additional information: + aDrawInf.SetNumberOfBlanks( pPor->InTextGrp() ? + static_cast<const SwTextPortion*>(pPor)->GetSpaceCnt( aSizeInf, nCharCnt ) : + TextFrameIndex(0) ); + } + + if ( pPor->InFieldGrp() && pCMS && pCMS->m_pSpecialPos ) + aDrawInf.SetLen( TextFrameIndex(COMPLETE_STRING) ); + + aDrawInf.SetSpace( nSpaceAdd ); + aDrawInf.SetFont( aSizeInf.GetFont() ); + aDrawInf.SetFrame( m_pFrame ); + aDrawInf.SetSnapToGrid( aSizeInf.SnapToGrid() ); + aDrawInf.SetPosMatchesBounds( pCMS && pCMS->m_bPosMatchesBounds ); + + if ( SwFontScript::CJK == aSizeInf.GetFont()->GetActual() && + pPara->GetScriptInfo().CountCompChg() && + ! pPor->InFieldGrp() ) + aDrawInf.SetKanaComp( nKanaComp ); + + nLength = aSizeInf.GetFont()->GetModelPositionForViewPoint_( aDrawInf ); + + // get position inside field portion? + if ( pPor->InFieldGrp() && pCMS && pCMS->m_pSpecialPos ) + { + pCMS->m_pSpecialPos->nCharOfst = sal_Int32(nLength); + // follow portions: need to add the length of all previous + // portions for the same field + if (static_cast<SwFieldPortion const*>(pPor)->IsFollow()) + { + int nLines(0); + std::vector<SwFieldPortion const*> portions; + for (SwLineLayout const* pLine = GetInfo().GetParaPortion(); + true; pLine = pLine->GetNext()) + { + for (SwLinePortion const* pLP = pLine; pLP && pLP != pPor; pLP = pLP->GetNextPortion()) + { + if (pLP->InFieldGrp()) + { + SwFieldPortion const* pField(static_cast<SwFieldPortion const*>(pLP)); + if (!pField->IsFollow()) + { + nLines = 0; + portions.clear(); + } + if (pLine == m_pCurr) + { + portions.emplace_back(pField); + } + } + } + if (pLine == m_pCurr) + { + break; + } + ++nLines; + } + for (SwFieldPortion const* pField : portions) + { + pCMS->m_pSpecialPos->nCharOfst += pField->GetExp().getLength(); + } + pCMS->m_pSpecialPos->nLineOfst = nLines; + } + nLength = TextFrameIndex(0); + } + else if (bFieldInfo && nLength == pPor->GetLen() && + (! pPor->GetNextPortion() || + ! pPor->GetNextPortion()->IsPostItsPortion())) + { + --nLength; + } + + // set cursor bidi level + if ( pCMS ) + pCMS->m_nCursorBidiLevel = + aDrawInf.GetCursorBidiLevel(); + } + if( nOldProp ) + const_cast<SwFont*>(GetFnt())->SetProportion( nOldProp ); + } + else + { + sw::FlyContentPortion* pFlyPor(nullptr); + if(bChgNode && pPos && (pFlyPor = dynamic_cast<sw::FlyContentPortion*>(pPor))) + { + // JP 24.11.94: if the Position is not in Fly, then + // we many not return with COMPLETE_STRING as value! + // (BugId: 9692 + Change in feshview) + SwFlyInContentFrame *pTmp = pFlyPor->GetFlyFrame(); + SwFrame* pLower = pTmp->GetLower(); + // Allow non-text-frames to get SwGrfNode for as-char anchored images into pPos + // instead of the closest SwTextNode, to be consistent with at-char behavior. + bool bChgNodeInner = pLower + && (pLower->IsTextFrame() || pLower->IsLayoutFrame() || pLower->IsNoTextFrame()); + Point aTmpPoint( rPoint ); + + if ( m_pFrame->IsRightToLeft() ) + m_pFrame->SwitchLTRtoRTL( aTmpPoint ); + + if ( m_pFrame->IsVertical() ) + m_pFrame->SwitchHorizontalToVertical( aTmpPoint ); + + if( bChgNodeInner && pTmp->getFrameArea().Contains( aTmpPoint ) && + !( pTmp->IsProtected() ) ) + { + pFlyPor->GetFlyCursorOfst(aTmpPoint, *pPos, pCMS); + // After a change of the frame, our font must be still + // available for/in the OutputDevice. + // For comparison: Paint and new SwFlyCntPortion ! + static_cast<SwTextSizeInfo*>(m_pInf)->SelectFont(); + + // 6776: The pIter->GetModelPositionForViewPoint is returning here + // from a nesting with COMPLETE_STRING. + return TextFrameIndex(COMPLETE_STRING); + } + } + else + nLength = pPor->GetModelPositionForViewPoint( nX ); + } + } + nOffset = nCurrStart + nLength; + + // 7684: We end up in front of the HyphPortion. We must assure + // that we end up in the string. + // If we are at end of line in front of FlyFrames, we must proceed the same way. + if( nOffset && pPor->GetLen() == nLength && pPor->GetNextPortion() && + !pPor->GetNextPortion()->GetLen() && pPor->GetNextPortion()->InHyphGrp() ) + --nOffset; + + return nOffset; +} + +/** Looks for text portions which are inside the given rectangle + + For a rectangular text selection every text portions which is inside the given + rectangle has to be put into the SwSelectionList as SwPaM + From these SwPaM the SwCursors will be created. + + @param rSelList + The container for the overlapped text portions + + @param rRect + A rectangle in document coordinates, text inside this rectangle has to be + selected. + + @return [ true, false ] + true if any overlapping text portion has been found and put into list + false if no portion overlaps, the list has been unchanged +*/ +bool SwTextFrame::FillSelection( SwSelectionList& rSelList, const SwRect& rRect ) const +{ + bool bRet = false; + // GetPaintArea() instead getFrameArea() for negative indents + SwRect aTmpFrame( GetPaintArea() ); + if( !rRect.Overlaps( aTmpFrame ) ) + return false; + if( rSelList.checkContext( this ) ) + { + SwRect aRect( aTmpFrame ); + aRect.Intersection( rRect ); + SwPosition aPosL( MapViewToModelPos(TextFrameIndex(0)) ); + if( IsEmpty() ) + { + SwPaM *pPam = new SwPaM( aPosL, aPosL ); + rSelList.insertPaM( pPam ); + } + else if( aRect.HasArea() ) + { + SwPosition aOld(aPosL.GetNodes().GetEndOfContent()); + SwPosition aPosR( aPosL ); + Point aPoint; + SwTextInfo aInf( const_cast<SwTextFrame*>(this) ); + SwTextIter aLine( const_cast<SwTextFrame*>(this), &aInf ); + // We have to care for top-to-bottom layout, where right becomes top etc. + SwRectFnSet aRectFnSet(this); + SwTwips nTop = aRectFnSet.GetTop(aRect); + SwTwips nBottom = aRectFnSet.GetBottom(aRect); + SwTwips nLeft = aRectFnSet.GetLeft(aRect); + SwTwips nRight = aRectFnSet.GetRight(aRect); + SwTwips nY = aLine.Y(); // Top position of the first line + SwTwips nLastY = nY; + while( nY < nTop && aLine.Next() ) // line above rectangle + { + nLastY = nY; + nY = aLine.Y(); + } + bool bLastLine = false; + if( nY < nTop && !aLine.GetNext() ) + { + bLastLine = true; + nY += aLine.GetLineHeight(); + } + do // check the lines for overlapping + { + if( nLastY < nTop ) // if the last line was above rectangle + nLastY = nTop; + if( nY > nBottom ) // if the current line leaves the rectangle + nY = nBottom; + if( nY >= nLastY ) // gotcha: overlapping + { + nLastY += nY; + nLastY /= 2; + if( aRectFnSet.IsVert() ) + { + aPoint.setX( nLastY ); + aPoint.setY( nLeft ); + } + else + { + aPoint.setX( nLeft ); + aPoint.setY( nLastY ); + } + // Looking for the position of the left border of the rectangle + // in this text line + SwCursorMoveState aState( CursorMoveState::UpDown ); + if( GetModelPositionForViewPoint( &aPosL, aPoint, &aState ) ) + { + if( aRectFnSet.IsVert() ) + { + aPoint.setX( nLastY ); + aPoint.setY( nRight ); + } + else + { + aPoint.setX( nRight ); + aPoint.setY( nLastY ); + } + // If we get a right position and if the left position + // is not the same like the left position of the line before + // which could happen e.g. for field portions or fly frames + // a SwPaM will be inserted with these positions + if( GetModelPositionForViewPoint( &aPosR, aPoint, &aState ) && + aOld != aPosL) + { + SwPaM *pPam = new SwPaM( aPosL, aPosR ); + rSelList.insertPaM( pPam ); + aOld = aPosL; + } + } + } + if( aLine.Next() ) + { + nLastY = nY; + nY = aLine.Y(); + } + else if( !bLastLine ) + { + bLastLine = true; + nLastY = nY; + nY += aLine.GetLineHeight(); + } + else + break; + }while( nLastY < nBottom ); + } + } + if( GetDrawObjs() ) + { + const SwSortedObjs &rObjs = *GetDrawObjs(); + for (SwAnchoredObject* pAnchoredObj : rObjs) + { + const SwFlyFrame* pFly = pAnchoredObj->DynCastFlyFrame(); + if( !pFly ) + continue; + if( pFly->IsFlyInContentFrame() && pFly->FillSelection( rSelList, rRect ) ) + bRet = true; + } + } + return bRet; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itrform2.cxx b/sw/source/core/text/itrform2.cxx new file mode 100644 index 0000000000..c0b4894f8a --- /dev/null +++ b/sw/source/core/text/itrform2.cxx @@ -0,0 +1,3321 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <hintids.hxx> + +#include <memory> +#include <com/sun/star/i18n/ScriptType.hpp> +#include <editeng/lspcitem.hxx> +#include <txtflcnt.hxx> +#include <txtftn.hxx> +#include <flyfrms.hxx> +#include <fmtflcnt.hxx> +#include <fmtftn.hxx> +#include <ftninfo.hxx> +#include <charfmt.hxx> +#include <editeng/charrotateitem.hxx> +#include <layfrm.hxx> +#include <viewsh.hxx> +#include <viewopt.hxx> +#include <paratr.hxx> +#include "itrform2.hxx" +#include "porrst.hxx" +#include "portab.hxx" +#include "porfly.hxx" +#include "portox.hxx" +#include "porref.hxx" +#include "porfld.hxx" +#include "porftn.hxx" +#include "porhyph.hxx" +#include "pordrop.hxx" +#include "redlnitr.hxx" +#include <sortedobjs.hxx> +#include <fmtanchr.hxx> +#include <pagefrm.hxx> +#include <tgrditem.hxx> +#include <doc.hxx> +#include "pormulti.hxx" +#include <unotools/charclass.hxx> +#include <xmloff/odffields.hxx> +#include <IDocumentSettingAccess.hxx> +#include <IMark.hxx> +#include <IDocumentMarkAccess.hxx> +#include <comphelper/processfactory.hxx> +#include <vcl/pdfextoutdevdata.hxx> +#include <comphelper/string.hxx> +#include <docsh.hxx> +#include <unocrsrhelper.hxx> +#include <textcontentcontrol.hxx> +#include <com/sun/star/rdf/Statement.hpp> +#include <com/sun/star/rdf/URI.hpp> +#include <com/sun/star/rdf/URIs.hpp> +#include <com/sun/star/rdf/XDocumentMetadataAccess.hpp> +#include <com/sun/star/rdf/XLiteral.hpp> +#include <com/sun/star/text/XTextContent.hpp> + +using namespace ::com::sun::star; + +namespace { + //! Calculates and sets optimal repaint offset for the current line + tools::Long lcl_CalcOptRepaint( SwTextFormatter &rThis, + SwLineLayout const &rCurr, + TextFrameIndex nOldLineEnd, + const std::vector<tools::Long> &rFlyStarts ); + //! Determine if we need to build hidden portions + bool lcl_BuildHiddenPortion(const SwTextSizeInfo& rInf, TextFrameIndex &rPos); + + // Check whether the two font has the same border + bool lcl_HasSameBorder(const SwFont& rFirst, const SwFont& rSecond); +} + +static void ClearFly( SwTextFormatInfo &rInf ) +{ + delete rInf.GetFly(); + rInf.SetFly(nullptr); +} + +void SwTextFormatter::CtorInitTextFormatter( SwTextFrame *pNewFrame, SwTextFormatInfo *pNewInf ) +{ + CtorInitTextPainter( pNewFrame, pNewInf ); + m_pInf = pNewInf; + m_pDropFormat = GetInfo().GetDropFormat(); + m_pMulti = nullptr; + + m_bOnceMore = false; + m_bFlyInContentBase = false; + m_bTruncLines = false; + m_nContentEndHyph = 0; + m_nContentMidHyph = 0; + m_nLeftScanIdx = TextFrameIndex(COMPLETE_STRING); + m_nRightScanIdx = TextFrameIndex(0); + m_pByEndIter.reset(); + m_pFirstOfBorderMerge = nullptr; + + if (m_nStart > TextFrameIndex(GetInfo().GetText().getLength())) + { + OSL_ENSURE( false, "+SwTextFormatter::CTOR: bad offset" ); + m_nStart = TextFrameIndex(GetInfo().GetText().getLength()); + } + +} + +SwTextFormatter::~SwTextFormatter() +{ + // Extremely unlikely, but still possible + // e.g.: field splits up, widows start to matter + if( GetInfo().GetRest() ) + { + delete GetInfo().GetRest(); + GetInfo().SetRest(nullptr); + } +} + +void SwTextFormatter::Insert( SwLineLayout *pLay ) +{ + // Insert BEHIND the current element + if ( m_pCurr ) + { + pLay->SetNext( m_pCurr->GetNext() ); + m_pCurr->SetNext( pLay ); + } + else + m_pCurr = pLay; +} + +sal_uInt16 SwTextFormatter::GetFrameRstHeight() const +{ + // We want the rest height relative to the page. + // If we're in a table, then pFrame->GetUpper() is not the page. + + // GetFrameRstHeight() is being called with Footnote. + // Wrong: const SwFrame *pUpper = pFrame->GetUpper(); + const SwFrame *pPage = m_pFrame->FindPageFrame(); + const SwTwips nHeight = pPage->getFrameArea().Top() + + pPage->getFramePrintArea().Top() + + pPage->getFramePrintArea().Height() - Y(); + if( 0 > nHeight ) + return m_pCurr->Height(); + else + return sal_uInt16( nHeight ); +} + +bool SwTextFormatter::ClearIfIsFirstOfBorderMerge(const SwLinePortion* pPortion) +{ + if (pPortion == m_pFirstOfBorderMerge) + { + m_pFirstOfBorderMerge = nullptr; + return true; + } + return false; +} + +SwLinePortion *SwTextFormatter::Underflow( SwTextFormatInfo &rInf ) +{ + // Save values and initialize rInf + SwLinePortion *pUnderflow = rInf.GetUnderflow(); + if( !pUnderflow ) + return nullptr; + + // We format backwards, i.e. attribute changes can happen the next + // line again. + // Can be seen in 8081.sdw, if you enter text in the first line + + TextFrameIndex const nSoftHyphPos = rInf.GetSoftHyphPos(); + TextFrameIndex const nUnderScorePos = rInf.GetUnderScorePos(); + + // Save flys and set to 0, or else segmentation fault + // Not ClearFly(rInf) ! + SwFlyPortion *pFly = rInf.GetFly(); + rInf.SetFly( nullptr ); + + FeedInf( rInf ); + rInf.SetLast( m_pCurr ); + // pUnderflow does not need to be deleted, because it will drown in the following + // Truncate() + rInf.SetUnderflow(nullptr); + rInf.SetSoftHyphPos( nSoftHyphPos ); + rInf.SetUnderScorePos( nUnderScorePos ); + rInf.SetPaintOfst( GetLeftMargin() ); + + // We look for the portion with the under-flow position + SwLinePortion *pPor = m_pCurr->GetFirstPortion(); + if( pPor != pUnderflow ) + { + // pPrev will be the last portion before pUnderflow, + // which still has a real width. + // Exception: SoftHyphPortion must not be forgotten, of course! + // Although they don't have a width. + SwLinePortion *pTmpPrev = pPor; + while( pPor && pPor != pUnderflow ) + { + if( !pPor->IsKernPortion() && + ( pPor->Width() || pPor->IsSoftHyphPortion() ) ) + { + while( pTmpPrev != pPor ) + { + pTmpPrev->Move( rInf ); + rInf.SetLast( pTmpPrev ); + pTmpPrev = pTmpPrev->GetNextPortion(); + OSL_ENSURE( pTmpPrev, "Underflow: losing control!" ); + }; + } + pPor = pPor->GetNextPortion(); + } + pPor = pTmpPrev; + if( pPor && // Skip flys and initials when underflow. + ( pPor->IsFlyPortion() || pPor->IsDropPortion() || + pPor->IsFlyCntPortion() ) ) + { + pPor->Move( rInf ); + rInf.SetLast( pPor ); + rInf.SetStopUnderflow( true ); + pPor = pUnderflow; + } + } + + // What? The under-flow portion is not in the portion chain? + OSL_ENSURE( pPor, "SwTextFormatter::Underflow: overflow but underflow" ); + + // Snapshot + if ( pPor==rInf.GetLast() ) + { + // We end up here, if the portion triggering the under-flow + // spans over the whole line. E.g. if a word spans across + // multiple lines and flows into a fly in the second line. + rInf.SetFly( pFly ); + pPor->Truncate(); + return pPor; // Is that enough? + } + // End the snapshot + + // X + Width == 0 with SoftHyph > Line?! + if( !pPor || !(rInf.X() + pPor->Width()) ) + { + delete pFly; + return nullptr; + } + + // Preparing for Format() + // We need to chip off the chain behind pLast, because we Insert after the Format() + SeekAndChg( rInf ); + + // line width is adjusted, so that pPor does not fit to current + // line anymore + rInf.Width( rInf.X() + (pPor->Width() ? pPor->Width() - 1 : 0) ); + rInf.SetLen( pPor->GetLen() ); + rInf.SetFull( false ); + if( pFly ) + { + // We need to recalculate the FlyPortion due to the following reason: + // If the base line is lowered by a big font in the middle of the line, + // causing overlapping with a fly, the FlyPortion has a wrong size/fixed + // size. + rInf.SetFly( pFly ); + CalcFlyWidth( rInf ); + } + rInf.GetLast()->SetNextPortion(nullptr); + + // The SwLineLayout is an exception to this, which splits at the first + // portion change. + // Here only the other way around: + if( rInf.GetLast() == m_pCurr ) + { + if( pPor->InTextGrp() && !pPor->InExpGrp() ) + { + const PortionType nOldWhich = m_pCurr->GetWhichPor(); + *static_cast<SwLinePortion*>(m_pCurr) = *pPor; + m_pCurr->SetNextPortion( pPor->GetNextPortion() ); + m_pCurr->SetWhichPor( nOldWhich ); + pPor->SetNextPortion( nullptr ); + delete pPor; + pPor = m_pCurr; + } + } + + // Make sure that m_pFirstOfBorderMerge does not point to a portion which + // will be deleted by Truncate() below. + SwLinePortion* pNext = pPor->GetNextPortion(); + while (pNext) + { + if (ClearIfIsFirstOfBorderMerge(pNext)) + break; + pNext = pNext->GetNextPortion(); + } + pPor->Truncate(); + SwLinePortion *const pRest( rInf.GetRest() ); + if (pRest && pRest->InFieldGrp() && + static_cast<SwFieldPortion*>(pRest)->IsNoLength()) + { + // HACK: decrement again, so we pick up the suffix in next line! + m_pByEndIter->PrevAttr(); + } + delete pRest; + rInf.SetRest(nullptr); + return pPor; +} + +void SwTextFormatter::InsertPortion( SwTextFormatInfo &rInf, + SwLinePortion *pPor ) +{ + SwLinePortion *pLast = nullptr; + // The new portion is inserted, but everything's different for + // LineLayout... + if( pPor == m_pCurr ) + { + if ( m_pCurr->GetNextPortion() ) + { + pLast = pPor; + pPor = m_pCurr->GetNextPortion(); + } + + // i#112181 - Prevent footnote anchor being wrapped to next line + // without preceding word + rInf.SetOtherThanFootnoteInside( rInf.IsOtherThanFootnoteInside() || !pPor->IsFootnotePortion() ); + } + else + { + pLast = rInf.GetLast(); + if( pLast->GetNextPortion() ) + { + while( pLast->GetNextPortion() ) + pLast = pLast->GetNextPortion(); + rInf.SetLast( pLast ); + } + pLast->Insert( pPor ); + + rInf.SetOtherThanFootnoteInside( rInf.IsOtherThanFootnoteInside() || !pPor->IsFootnotePortion() ); + + // Adjust maxima + if( m_pCurr->Height() < pPor->Height() ) + m_pCurr->Height( pPor->Height(), pPor->IsTextPortion() ); + if( m_pCurr->GetAscent() < pPor->GetAscent() ) + m_pCurr->SetAscent( pPor->GetAscent() ); + if( m_pCurr->GetHangingBaseline() < pPor->GetHangingBaseline() ) + m_pCurr->SetHangingBaseline( pPor->GetHangingBaseline() ); + + if (GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::MS_WORD_COMP_MIN_LINE_HEIGHT_BY_FLY)) + { + // For DOCX with compat=14 the only shape in line defines height of the line in spite of used font + if (pLast->IsFlyCntPortion() && pPor->IsTextPortion() && pPor->GetLen() == TextFrameIndex(0)) + { + m_pCurr->SetAscent(pLast->GetAscent()); + m_pCurr->Height(pLast->Height()); + } + } + } + + // Sometimes chains are constructed (e.g. by hyphenate) + rInf.SetLast( pPor ); + while( pPor ) + { + if (!pPor->IsDropPortion()) + MergeCharacterBorder(*pPor, pLast, rInf); + + pPor->Move( rInf ); + rInf.SetLast( pPor ); + pLast = pPor; + pPor = pPor->GetNextPortion(); + } +} + +void SwTextFormatter::BuildPortions( SwTextFormatInfo &rInf ) +{ + OSL_ENSURE( rInf.GetText().getLength() < COMPLETE_STRING, + "SwTextFormatter::BuildPortions: bad text length in info" ); + + rInf.ChkNoHyph( CntEndHyph(), CntMidHyph() ); + + // First NewTextPortion() decides whether pCurr ends up in pPor. + // We need to make sure that the font is being set in any case. + // This is done automatically in CalcAscent. + rInf.SetLast( m_pCurr ); + rInf.ForcedLeftMargin( 0 ); + + OSL_ENSURE( m_pCurr->FindLastPortion() == m_pCurr, "pLast supposed to equal pCurr" ); + + if( !m_pCurr->GetAscent() && !m_pCurr->Height() ) + CalcAscent( rInf, m_pCurr ); + + SeekAndChg( rInf ); + + // Width() is shortened in CalcFlyWidth if we have a FlyPortion + OSL_ENSURE( !rInf.X() || m_pMulti, "SwTextFormatter::BuildPortion X=0?" ); + CalcFlyWidth( rInf ); + SwFlyPortion *pFly = rInf.GetFly(); + if( pFly ) + { + if ( 0 < pFly->GetFix() ) + ClearFly( rInf ); + else + rInf.SetFull(true); + } + + ::std::optional<TextFrameIndex> oMovedFlyIndex; + if (SwTextFrame const*const pFollow = GetTextFrame()->GetFollow()) + { + // flys are always on master! + if (GetTextFrame()->GetDrawObjs() && pFollow->GetUpper() != GetTextFrame()->GetUpper()) + { + for (SwAnchoredObject const*const pAnchoredObj : *GetTextFrame()->GetDrawObjs()) + { + // tdf#146500 try to stop where a fly is anchored in the follow + // that has recently been moved (presumably by splitting this + // frame); similar to check in SwFlowFrame::MoveBwd() + if (pAnchoredObj->RestartLayoutProcess() + && !pAnchoredObj->IsTmpConsiderWrapInfluence()) + { + SwFormatAnchor const& rAnchor(pAnchoredObj->GetFrameFormat().GetAnchor()); + assert(rAnchor.GetAnchorId() == RndStdIds::FLY_AT_CHAR || rAnchor.GetAnchorId() == RndStdIds::FLY_AT_PARA); + TextFrameIndex const nAnchor(GetTextFrame()->MapModelToViewPos(*rAnchor.GetContentAnchor())); + if (pFollow->GetOffset() <= nAnchor + && (pFollow->GetFollow() == nullptr + || nAnchor < pFollow->GetFollow()->GetOffset())) + { + if (!oMovedFlyIndex || nAnchor < *oMovedFlyIndex) + { + oMovedFlyIndex.emplace(nAnchor); + } + } + } + } + } + } + + SwLinePortion *pPor = NewPortion(rInf, oMovedFlyIndex); + + // Asian grid stuff + SwTextGridItem const*const pGrid(GetGridItem(m_pFrame->FindPageFrame())); + const bool bHasGrid = pGrid && rInf.SnapToGrid() && + GRID_LINES_CHARS == pGrid->GetGridType(); + + + const SwDoc & rDoc = rInf.GetTextFrame()->GetDoc(); + const sal_uInt16 nGridWidth = bHasGrid ? GetGridWidth(*pGrid, rDoc) : 0; + + // used for grid mode only: + // the pointer is stored, because after formatting of non-asian text, + // the width of the kerning portion has to be adjusted + // Inserting a SwKernPortion before a SwTabPortion isn't necessary + // and will break the SwTabPortion. + SwKernPortion* pGridKernPortion = nullptr; + + bool bFull = false; + SwTwips nUnderLineStart = 0; + rInf.Y( Y() ); + + while( pPor && !rInf.IsStop() ) + { + OSL_ENSURE(rInf.GetLen() < TextFrameIndex(COMPLETE_STRING) && + rInf.GetIdx() <= TextFrameIndex(rInf.GetText().getLength()), + "SwTextFormatter::BuildPortions: bad length in info" ); + + // We have to check the script for fields in order to set the + // correct nActual value for the font. + if( pPor->InFieldGrp() ) + static_cast<SwFieldPortion*>(pPor)->CheckScript( rInf ); + + if( ! bHasGrid && rInf.HasScriptSpace() && + rInf.GetLast() && rInf.GetLast()->InTextGrp() && + rInf.GetLast()->Width() && !rInf.GetLast()->InNumberGrp() ) + { + SwFontScript nNxtActual = rInf.GetFont()->GetActual(); + SwFontScript nLstActual = nNxtActual; + sal_uInt16 nLstHeight = o3tl::narrowing<sal_uInt16>(rInf.GetFont()->GetHeight()); + bool bAllowBehind = false; + const CharClass& rCC = GetAppCharClass(); + + // are there any punctuation characters on both sides + // of the kerning portion? + if ( pPor->InFieldGrp() ) + { + OUString aAltText; + if ( static_cast<SwFieldPortion*>(pPor)->GetExpText( rInf, aAltText ) && + !aAltText.isEmpty() ) + { + bAllowBehind = rCC.isLetterNumeric( aAltText, 0 ); + + const SwFont* pTmpFnt = static_cast<SwFieldPortion*>(pPor)->GetFont(); + if ( pTmpFnt ) + nNxtActual = pTmpFnt->GetActual(); + } + } + else + { + const OUString& rText = rInf.GetText(); + sal_Int32 nIdx = sal_Int32(rInf.GetIdx()); + bAllowBehind = nIdx < rText.getLength() && rCC.isLetterNumeric(rText, nIdx); + } + + const SwLinePortion* pLast = rInf.GetLast(); + if ( bAllowBehind && pLast ) + { + bool bAllowBefore = false; + + if ( pLast->InFieldGrp() ) + { + OUString aAltText; + if ( static_cast<const SwFieldPortion*>(pLast)->GetExpText( rInf, aAltText ) && + !aAltText.isEmpty() ) + { + bAllowBefore = rCC.isLetterNumeric( aAltText, aAltText.getLength() - 1 ); + + const SwFont* pTmpFnt = static_cast<const SwFieldPortion*>(pLast)->GetFont(); + if ( pTmpFnt ) + { + nLstActual = pTmpFnt->GetActual(); + nLstHeight = o3tl::narrowing<sal_uInt16>(pTmpFnt->GetHeight()); + } + } + } + else if ( rInf.GetIdx() ) + { + bAllowBefore = rCC.isLetterNumeric(rInf.GetText(), sal_Int32(rInf.GetIdx()) - 1); + // Note: ScriptType returns values in [1,4] + if ( bAllowBefore ) + nLstActual = SwFontScript(m_pScriptInfo->ScriptType(rInf.GetIdx() - TextFrameIndex(1)) - 1); + } + + nLstHeight /= 5; + // does the kerning portion still fit into the line? + if( bAllowBefore && ( nLstActual != nNxtActual ) && + // tdf#89288 we want to insert space between CJK and non-CJK text only. + ( nLstActual == SwFontScript::CJK || nNxtActual == SwFontScript::CJK ) && + nLstHeight && rInf.X() + nLstHeight <= rInf.Width() && + ! pPor->InTabGrp() ) + { + SwKernPortion* pKrn = + new SwKernPortion( *rInf.GetLast(), nLstHeight, + pLast->InFieldGrp() && pPor->InFieldGrp() ); + + // ofz#58550 Direct-leak, pKrn adds itself as the NextPortion + // of rInf.GetLast(), but may use CopyLinePortion to add a copy + // of itself, which will then be left dangling with the following + // SetNextPortion(nullptr) + SwLinePortion *pNext = rInf.GetLast()->GetNextPortion(); + if (pNext != pKrn) + delete pNext; + + rInf.GetLast()->SetNextPortion( nullptr ); + InsertPortion( rInf, pKrn ); + } + } + } + else if ( bHasGrid && ! pGridKernPortion && ! m_pMulti && ! pPor->InTabGrp() ) + { + // insert a grid kerning portion + pGridKernPortion = pPor->IsKernPortion() ? + static_cast<SwKernPortion*>(pPor) : + new SwKernPortion( *m_pCurr ); + + // if we have a new GridKernPortion, we initially calculate + // its size so that its ends on the grid + const SwPageFrame* pPageFrame = m_pFrame->FindPageFrame(); + const SwLayoutFrame* pBody = pPageFrame->FindBodyCont(); + SwRectFnSet aRectFnSet(pPageFrame); + + const tools::Long nGridOrigin = pBody ? + aRectFnSet.GetPrtLeft(*pBody) : + aRectFnSet.GetPrtLeft(*pPageFrame); + + SwTwips nStartX = rInf.X() + GetLeftMargin(); + if ( aRectFnSet.IsVert() ) + { + Point aPoint( nStartX, 0 ); + m_pFrame->SwitchHorizontalToVertical( aPoint ); + nStartX = aPoint.Y(); + } + + const SwTwips nOfst = nStartX - nGridOrigin; + if ( nOfst ) + { + const sal_uLong i = ( nOfst > 0 ) ? + ( ( nOfst - 1 ) / nGridWidth + 1 ) : + 0; + const SwTwips nKernWidth = i * nGridWidth - nOfst; + const SwTwips nRestWidth = rInf.Width() - rInf.X(); + + if ( nKernWidth <= nRestWidth ) + pGridKernPortion->Width( nKernWidth ); + } + + if ( pGridKernPortion != pPor ) + InsertPortion( rInf, pGridKernPortion ); + } + + if( pPor->IsDropPortion() ) + MergeCharacterBorder(*static_cast<SwDropPortion*>(pPor)); + + // the multi-portion has its own format function + if( pPor->IsMultiPortion() && ( !m_pMulti || m_pMulti->IsBidi() ) ) + bFull = BuildMultiPortion( rInf, *static_cast<SwMultiPortion*>(pPor) ); + else + bFull = pPor->Format( rInf ); + + if( rInf.IsRuby() && !rInf.GetRest() ) + bFull = true; + + // if we are underlined, we store the beginning of this underlined + // segment for repaint optimization + if ( LINESTYLE_NONE != m_pFont->GetUnderline() && ! nUnderLineStart ) + nUnderLineStart = GetLeftMargin() + rInf.X(); + + if ( pPor->IsFlyPortion() ) + m_pCurr->SetFly( true ); + // some special cases, where we have to take care for the repaint + // offset: + // 1. Underlined portions due to special underline feature + // 2. Right Tab + // 3. BidiPortions + // 4. other Multiportions + // 5. DropCaps + // 6. Grid Mode + else if ( ( ! rInf.GetPaintOfst() || nUnderLineStart < rInf.GetPaintOfst() ) && + // 1. Underlined portions + nUnderLineStart && + // reformat is at end of an underlined portion and next portion + // is not underlined + ( ( rInf.GetReformatStart() == rInf.GetIdx() && + LINESTYLE_NONE == m_pFont->GetUnderline() + ) || + // reformat is inside portion and portion is underlined + ( rInf.GetReformatStart() >= rInf.GetIdx() && + rInf.GetReformatStart() <= rInf.GetIdx() + pPor->GetLen() && + LINESTYLE_NONE != m_pFont->GetUnderline() ) ) ) + rInf.SetPaintOfst( nUnderLineStart ); + else if ( ! rInf.GetPaintOfst() && + // 2. Right Tab + ( ( pPor->InTabGrp() && !pPor->IsTabLeftPortion() ) || + // 3. BidiPortions + ( pPor->IsMultiPortion() && static_cast<SwMultiPortion*>(pPor)->IsBidi() ) || + // 4. Multi Portion and 5. Drop Caps + ( ( pPor->IsDropPortion() || pPor->IsMultiPortion() ) && + rInf.GetReformatStart() >= rInf.GetIdx() && + rInf.GetReformatStart() <= rInf.GetIdx() + pPor->GetLen() ) + // 6. Grid Mode + || ( bHasGrid && SwFontScript::CJK != m_pFont->GetActual() ) + ) + ) + // we store the beginning of the critical portion as our + // paint offset + rInf.SetPaintOfst( GetLeftMargin() + rInf.X() ); + + // under one of these conditions we are allowed to delete the + // start of the underline portion + if ( IsUnderlineBreak( *pPor, *m_pFont ) ) + nUnderLineStart = 0; + + if( pPor->IsFlyCntPortion() || ( pPor->IsMultiPortion() && + static_cast<SwMultiPortion*>(pPor)->HasFlyInContent() ) ) + SetFlyInCntBase(); + // bUnderflow needs to be reset or we wrap again at the next softhyphen + if ( !bFull ) + { + rInf.ClrUnderflow(); + if( ! bHasGrid && rInf.HasScriptSpace() && pPor->InTextGrp() && + pPor->GetLen() && !pPor->InFieldGrp() ) + { + // The distance between two different scripts is set + // to 20% of the fontheight. + TextFrameIndex const nTmp = rInf.GetIdx() + pPor->GetLen(); + if (nTmp == m_pScriptInfo->NextScriptChg(nTmp - TextFrameIndex(1)) && + nTmp != TextFrameIndex(rInf.GetText().getLength()) && + (m_pScriptInfo->ScriptType(nTmp - TextFrameIndex(1)) == css::i18n::ScriptType::ASIAN || + m_pScriptInfo->ScriptType(nTmp) == css::i18n::ScriptType::ASIAN) ) + { + const SwTwips nDist = rInf.GetFont()->GetHeight()/5; + + if( nDist ) + { + // we do not want a kerning portion if any end + // would be a punctuation character + const CharClass& rCC = GetAppCharClass(); + if (rCC.isLetterNumeric(rInf.GetText(), sal_Int32(nTmp) - 1) + && rCC.isLetterNumeric(rInf.GetText(), sal_Int32(nTmp))) + { + // does the kerning portion still fit into the line? + if ( rInf.X() + pPor->Width() + nDist <= rInf.Width() ) + new SwKernPortion( *pPor, nDist ); + else + bFull = true; + } + } + } + } + } + + if ( bHasGrid && pPor != pGridKernPortion && ! m_pMulti && ! pPor->InTabGrp() ) + { + TextFrameIndex const nTmp = rInf.GetIdx() + pPor->GetLen(); + const SwTwips nRestWidth = rInf.Width() - rInf.X() - pPor->Width(); + + const SwFontScript nCurrScript = m_pFont->GetActual(); // pScriptInfo->ScriptType( rInf.GetIdx() ); + const SwFontScript nNextScript = + nTmp >= TextFrameIndex(rInf.GetText().getLength()) + ? SwFontScript::CJK + : m_pScriptInfo->WhichFont(nTmp); + + // snap non-asian text to grid if next portion is ASIAN or + // there are no more portions in this line + // be careful when handling an underflow event: the gridkernportion + // could have been deleted + if ( nRestWidth > 0 && SwFontScript::CJK != nCurrScript && + ! rInf.IsUnderflow() && ( bFull || SwFontScript::CJK == nNextScript ) ) + { + OSL_ENSURE( pGridKernPortion, "No GridKernPortion available" ); + + // calculate size + SwLinePortion* pTmpPor = pGridKernPortion->GetNextPortion(); + sal_uInt16 nSumWidth = pPor->Width(); + while ( pTmpPor ) + { + nSumWidth = nSumWidth + pTmpPor->Width(); + pTmpPor = pTmpPor->GetNextPortion(); + } + + const SwTwips i = nSumWidth ? + ( nSumWidth - 1 ) / nGridWidth + 1 : + 0; + const SwTwips nTmpWidth = i * nGridWidth; + const SwTwips nKernWidth = std::min(nTmpWidth - nSumWidth, nRestWidth); + const SwTwips nKernWidth_1 = pGrid->IsSnapToChars() ? + nKernWidth / 2 : 0; + + OSL_ENSURE( nKernWidth <= nRestWidth, + "Not enough space left for adjusting non-asian text in grid mode" ); + if (nKernWidth_1) + { + pGridKernPortion->Width( pGridKernPortion->Width() + nKernWidth_1 ); + rInf.X( rInf.X() + nKernWidth_1 ); + } + + if ( ! bFull && nKernWidth - nKernWidth_1 > 0 ) + new SwKernPortion( *pPor, static_cast<short>(nKernWidth - nKernWidth_1), + false, true ); + + pGridKernPortion = nullptr; + } + else if ( pPor->IsMultiPortion() || pPor->InFixMargGrp() || + pPor->IsFlyCntPortion() || pPor->InNumberGrp() || + pPor->InFieldGrp() || nCurrScript != nNextScript ) + // next portion should snap to grid + pGridKernPortion = nullptr; + } + + rInf.SetFull( bFull ); + + // Restportions from fields with multiple lines don't yet have the right ascent + if ( !pPor->GetLen() && !pPor->IsFlyPortion() + && !pPor->IsGrfNumPortion() && ! pPor->InNumberGrp() + && !pPor->IsMultiPortion() ) + CalcAscent( rInf, pPor ); + + InsertPortion( rInf, pPor ); + if (pPor->IsMultiPortion() && (!m_pMulti || m_pMulti->IsBidi())) + { + (void) rInf.CheckCurrentPosBookmark(); // bookmark was already created inside MultiPortion! + } + pPor = NewPortion(rInf, oMovedFlyIndex); + } + + if( !rInf.IsStop() ) + { + // The last right centered, decimal tab + SwTabPortion *pLastTab = rInf.GetLastTab(); + if( pLastTab ) + pLastTab->FormatEOL( rInf ); + else if( rInf.GetLast() && rInf.LastKernPortion() ) + rInf.GetLast()->FormatEOL( rInf ); + } + if( m_pCurr->GetNextPortion() && m_pCurr->GetNextPortion()->InNumberGrp() + && static_cast<SwNumberPortion*>(m_pCurr->GetNextPortion())->IsHide() ) + rInf.SetNumDone( false ); + + // Delete fly in any case + ClearFly( rInf ); + + // Reinit the tab overflow flag after the line + rInf.SetTabOverflow( false ); +} + +void SwTextFormatter::CalcAdjustLine( SwLineLayout *pCurrent ) +{ + if( SvxAdjust::Left != GetAdjust() && !m_pMulti) + { + pCurrent->SetFormatAdj(true); + if( IsFlyInCntBase() ) + { + CalcAdjLine( pCurrent ); + // For e.g. centered fly we need to switch the RefPoint + // That's why bAlways = true + UpdatePos( pCurrent, GetTopLeft(), GetStart(), true ); + } + } +} + +void SwTextFormatter::CalcAscent( SwTextFormatInfo &rInf, SwLinePortion *pPor ) +{ + bool bCalc = false; + if ( pPor->InFieldGrp() && static_cast<SwFieldPortion*>(pPor)->GetFont() ) + { + // Numbering + InterNetFields can keep an own font, then their size is + // independent from hard attribute values + SwFont* pFieldFnt = static_cast<SwFieldPortion*>(pPor)->m_pFont.get(); + SwFontSave aSave( rInf, pFieldFnt ); + pPor->Height( rInf.GetTextHeight() ); + pPor->SetAscent( rInf.GetAscent() ); + bCalc = true; + } + // i#89179 + // tab portion representing the list tab of a list label gets the + // same height and ascent as the corresponding number portion + else if ( pPor->InTabGrp() && pPor->GetLen() == TextFrameIndex(0) && + rInf.GetLast() && rInf.GetLast()->InNumberGrp() && + static_cast<const SwNumberPortion*>(rInf.GetLast())->HasFont() ) + { + const SwLinePortion* pLast = rInf.GetLast(); + pPor->Height( pLast->Height() ); + pPor->SetAscent( pLast->GetAscent() ); + } + else if (pPor->GetWhichPor() == PortionType::Bookmark + && rInf.GetIdx() == TextFrameIndex(rInf.GetText().getLength())) + { + // bookmark at end of paragraph: *don't* advance iterator, use the + // current font instead; it's possible that there's a font size on the + // paragraph and it's overridden on the last line of the paragraph and + // we don't want to apply it via SwBookmarkPortion and grow the line + // height (example: n758883.docx) + SwLinePortion const*const pLast = rInf.GetLast(); + assert(pLast); + pPor->Height( pLast->Height(), false ); + pPor->SetAscent( pLast->GetAscent() ); + } + else + { + const SwLinePortion *pLast = rInf.GetLast(); + bool bChg = false; + + // In empty lines the attributes are switched on via SeekStart + const bool bFirstPor = rInf.GetLineStart() == rInf.GetIdx(); + if ( pPor->IsQuoVadisPortion() ) + bChg = SeekStartAndChg( rInf, true ); + else + { + if( bFirstPor ) + { + if( !rInf.GetText().isEmpty() ) + { + if ( pPor->GetLen() || !rInf.GetIdx() + || ( m_pCurr != pLast && !pLast->IsFlyPortion() ) + || !m_pCurr->IsRest() ) // instead of !rInf.GetRest() + bChg = SeekAndChg( rInf ); + else + bChg = SeekAndChgBefore( rInf ); + } + else if ( m_pMulti ) + // do not open attributes starting at 0 in empty multi + // portions (rotated numbering followed by a footnote + // can cause trouble, because the footnote attribute + // starts at 0, but if we open it, the attribute handler + // cannot handle it. + bChg = false; + else + bChg = SeekStartAndChg( rInf ); + } + else + bChg = SeekAndChg( rInf ); + } + if( bChg || bFirstPor || !pPor->GetAscent() + || !rInf.GetLast()->InTextGrp() ) + { + pPor->SetHangingBaseline( rInf.GetHangingBaseline() ); + pPor->SetAscent( rInf.GetAscent() ); + pPor->Height( rInf.GetTextHeight() ); + bCalc = true; + } + else + { + pPor->Height( pLast->Height() ); + pPor->SetAscent( pLast->GetAscent() ); + } + } + + if( pPor->InTextGrp() && bCalc ) + { + pPor->SetAscent(pPor->GetAscent() + + rInf.GetFont()->GetTopBorderSpace()); + pPor->Height(pPor->Height() + + rInf.GetFont()->GetTopBorderSpace() + + rInf.GetFont()->GetBottomBorderSpace() ); + } +} + +namespace { + +class SwMetaPortion : public SwTextPortion +{ + Color m_aShadowColor; +public: + SwMetaPortion() { SetWhichPor( PortionType::Meta ); } + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + void SetShadowColor(const Color& rCol ) { m_aShadowColor = rCol; } +}; + +/// A content control portion is a text portion that is inside RES_TXTATR_CONTENTCONTROL. +class SwContentControlPortion : public SwTextPortion +{ + SwTextContentControl* m_pTextContentControl; +public: + SwContentControlPortion(SwTextContentControl* pTextContentControl); + virtual void Paint(const SwTextPaintInfo& rInf) const override; + + /// Emits a PDF form widget for this portion on success, does nothing on failure. + bool DescribePDFControl(const SwTextPaintInfo& rInf) const; +}; +} + +void SwMetaPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if ( Width() ) + { + rInf.DrawViewOpt( *this, PortionType::Meta, + // custom shading (RDF metadata) + COL_BLACK == m_aShadowColor + ? nullptr + : &m_aShadowColor ); + + SwTextPortion::Paint( rInf ); + } +} + +SwContentControlPortion::SwContentControlPortion(SwTextContentControl* pTextContentControl) + : m_pTextContentControl(pTextContentControl) +{ + SetWhichPor(PortionType::ContentControl); +} + +bool SwContentControlPortion::DescribePDFControl(const SwTextPaintInfo& rInf) const +{ + auto pPDFExtOutDevData = dynamic_cast<vcl::PDFExtOutDevData*>(rInf.GetOut()->GetExtOutDevData()); + if (!pPDFExtOutDevData) + { + return false; + } + + if (!pPDFExtOutDevData->GetIsExportFormFields()) + { + return false; + } + + if (!m_pTextContentControl) + { + return false; + } + + const SwFormatContentControl& rFormatContentControl = m_pTextContentControl->GetContentControl(); + const std::shared_ptr<SwContentControl>& pContentControl = rFormatContentControl.GetContentControl(); + if (!pContentControl) + { + return false; + } + + SwTextNode* pTextNode = pContentControl->GetTextNode(); + SwDoc& rDoc = pTextNode->GetDoc(); + if (rDoc.IsInHeaderFooter(*pTextNode)) + { + // Form control in header/footer makes no sense, would allow multiple values for the same + // control. + return false; + } + + // Check if this is the first content control portion of this content control. + sal_Int32 nStart = m_pTextContentControl->GetStart(); + sal_Int32 nEnd = *m_pTextContentControl->GetEnd(); + TextFrameIndex nViewStart = rInf.GetTextFrame()->MapModelToView(pTextNode, nStart); + TextFrameIndex nViewEnd = rInf.GetTextFrame()->MapModelToView(pTextNode, nEnd); + // The content control portion starts 1 char after the starting dummy character. + if (rInf.GetIdx() != nViewStart + TextFrameIndex(1)) + { + // Ignore: don't process and also don't emit plain text fallback. + return true; + } + + const SwPaM aPam(*pTextNode, nEnd, *pTextNode, nStart); + static sal_Unicode const aForbidden[] = { + CH_TXTATR_BREAKWORD, + 0 + }; + const OUString aText = comphelper::string::removeAny(aPam.GetText(), aForbidden); + + std::unique_ptr<vcl::PDFWriter::AnyWidget> pDescriptor; + switch (pContentControl->GetType()) + { + case SwContentControlType::RICH_TEXT: + case SwContentControlType::PLAIN_TEXT: + { + pDescriptor = std::make_unique<vcl::PDFWriter::EditWidget>(); + break; + } + case SwContentControlType::CHECKBOX: + { + pDescriptor = std::make_unique<vcl::PDFWriter::CheckBoxWidget>(); + auto pCheckBoxWidget = static_cast<vcl::PDFWriter::CheckBoxWidget*>(pDescriptor.get()); + pCheckBoxWidget->Checked = pContentControl->GetChecked(); + pCheckBoxWidget->OnValue = pContentControl->GetCheckedState(); + pCheckBoxWidget->OffValue = pContentControl->GetUncheckedState(); + break; + } + case SwContentControlType::DROP_DOWN_LIST: + { + pDescriptor = std::make_unique<vcl::PDFWriter::ListBoxWidget>(); + auto pListWidget = static_cast<vcl::PDFWriter::ListBoxWidget*>(pDescriptor.get()); + pListWidget->DropDown = true; + sal_Int32 nIndex = 0; + for (const auto& rItem : pContentControl->GetListItems()) + { + pListWidget->Entries.push_back(rItem.m_aDisplayText); + if (rItem.m_aDisplayText == aText) + pListWidget->SelectedEntries.push_back(nIndex); + ++nIndex; + } + break; + } + case SwContentControlType::COMBO_BOX: + { + pDescriptor = std::make_unique<vcl::PDFWriter::ComboBoxWidget>(); + auto pComboWidget = static_cast<vcl::PDFWriter::ComboBoxWidget*>(pDescriptor.get()); + for (const auto& rItem : pContentControl->GetListItems()) + { + pComboWidget->Entries.push_back(rItem.m_aDisplayText); + } + break; + } + case SwContentControlType::DATE: + { + pDescriptor = std::make_unique<vcl::PDFWriter::EditWidget>(); + auto pEditWidget = static_cast<vcl::PDFWriter::EditWidget*>(pDescriptor.get()); + pEditWidget->Format = vcl::PDFWriter::Date; + // GetDateFormat() uses a syntax that works with SvNumberFormatter::PutEntry(), PDF's + // AFDate_FormatEx() uses a similar syntax, but uses lowercase characters in case of + // "Y", "M" and "D" at least. + pEditWidget->DateFormat = pContentControl->GetDateFormat().toAsciiLowerCase(); + break; + } + default: + break; + } + + if (!pDescriptor) + { + return false; + } + + const SwFont* pFont = rInf.GetFont(); + if (pFont) + { + pDescriptor->TextFont = pFont->GetActualFont(); + } + + // Description for accessibility purposes. + if (!pContentControl->GetAlias().isEmpty()) + { + pDescriptor->Description = pContentControl->GetAlias(); + } + + // Map the text of the content control to the descriptor's text. + pDescriptor->Text = aText; + + // Calculate the bounding rectangle of this content control, which can be one or more layout + // portions in one or more lines. + SwRect aLocation; + auto pTextFrame = const_cast<SwTextFrame*>(rInf.GetTextFrame()); + SwTextSizeInfo aInf(pTextFrame); + SwTextCursor aLine(pTextFrame, &aInf); + SwRect aStartRect, aEndRect; + aLine.GetCharRect(&aStartRect, nViewStart); + aLine.GetCharRect(&aEndRect, nViewEnd); + + // Handling RTL text direction + if(rInf.GetTextFrame()->IsRightToLeft()) + { + rInf.GetTextFrame()->SwitchLTRtoRTL( aStartRect ); + rInf.GetTextFrame()->SwitchLTRtoRTL( aEndRect ); + } + // TODO: handle rInf.GetTextFrame()->IsVertical() + + aLocation = aStartRect; + aLocation.Union(aEndRect); + pDescriptor->Location = aLocation.SVRect(); + + pPDFExtOutDevData->WrapBeginStructureElement(vcl::PDFWriter::Form); + pPDFExtOutDevData->CreateControl(*pDescriptor); + pPDFExtOutDevData->EndStructureElement(); + + return true; +} + +void SwContentControlPortion::Paint(const SwTextPaintInfo& rInf) const +{ + if (Width()) + { + rInf.DrawViewOpt(*this, PortionType::ContentControl); + + if (DescribePDFControl(rInf)) + { + return; + } + + SwTextPortion::Paint(rInf); + } +} + +namespace sw::mark { + OUString ExpandFieldmark(IFieldmark* pBM) + { + if (pBM->GetFieldname() == ODF_FORMCHECKBOX) + { + ::sw::mark::ICheckboxFieldmark const*const pCheckboxFm( + dynamic_cast<ICheckboxFieldmark const*>(pBM)); + assert(pCheckboxFm); + return pCheckboxFm->IsChecked() + ? u"\u2612"_ustr + : u"\u2610"_ustr; + } + assert(pBM->GetFieldname() == ODF_FORMDROPDOWN); + const IFieldmark::parameter_map_t* const pParameters = pBM->GetParameters(); + sal_Int32 nCurrentIdx = 0; + const IFieldmark::parameter_map_t::const_iterator pResult = pParameters->find(ODF_FORMDROPDOWN_RESULT); + if(pResult != pParameters->end()) + pResult->second >>= nCurrentIdx; + + const IFieldmark::parameter_map_t::const_iterator pListEntries = pParameters->find(ODF_FORMDROPDOWN_LISTENTRY); + if (pListEntries != pParameters->end()) + { + uno::Sequence< OUString > vListEntries; + pListEntries->second >>= vListEntries; + if (nCurrentIdx < vListEntries.getLength()) + return vListEntries[nCurrentIdx]; + } + + static constexpr OUStringLiteral vEnSpaces = u"\u2002\u2002\u2002\u2002\u2002"; + return vEnSpaces; + } +} + +SwTextPortion *SwTextFormatter::WhichTextPor( SwTextFormatInfo &rInf ) const +{ + SwTextPortion *pPor = nullptr; + if( GetFnt()->IsTox() ) + { + pPor = new SwToxPortion; + } + else if ( GetFnt()->IsInputField() ) + { + if (rInf.GetOpt().IsFieldName()) + { + OUString aFieldName = SwFieldType::GetTypeStr(SwFieldTypesEnum::Input); + // assume this is only the *first* portion and follows will be created elsewhere => input field must start at Idx + assert(rInf.GetText()[sal_Int32(rInf.GetIdx())] == CH_TXT_ATR_INPUTFIELDSTART); + TextFrameIndex nFieldLen(-1); + for (TextFrameIndex i = rInf.GetIdx() + TextFrameIndex(1); ; ++i) + { + assert(rInf.GetText()[sal_Int32(i)] != CH_TXT_ATR_INPUTFIELDSTART); // can't nest + if (rInf.GetText()[sal_Int32(i)] == CH_TXT_ATR_INPUTFIELDEND) + { + nFieldLen = i + TextFrameIndex(1) - rInf.GetIdx(); + break; + } + } + assert(2 <= sal_Int32(nFieldLen)); + pPor = new SwFieldPortion(aFieldName, nullptr, nFieldLen); + } + else + { + pPor = new SwTextInputFieldPortion(); + } + } + else + { + if( GetFnt()->IsRef() ) + pPor = new SwRefPortion; + else if (GetFnt()->IsMeta()) + { + auto pMetaPor = new SwMetaPortion; + + // set custom LO_EXT_SHADING color, if it exists + SwTextFrame const*const pFrame(rInf.GetTextFrame()); + SwPosition aPosition(pFrame->MapViewToModelPos(rInf.GetIdx())); + SwPaM aPam(aPosition); + uno::Reference<text::XTextContent> const xRet( + SwUnoCursorHelper::GetNestedTextContent( + *aPam.GetPointNode().GetTextNode(), aPosition.GetContentIndex(), false) ); + if (xRet.is()) + { + const SwDoc & rDoc = rInf.GetTextFrame()->GetDoc(); + static uno::Reference< uno::XComponentContext > xContext( + ::comphelper::getProcessComponentContext()); + + static uno::Reference< rdf::XURI > xODF_SHADING( + rdf::URI::createKnown(xContext, rdf::URIs::LO_EXT_SHADING), uno::UNO_SET_THROW); + + uno::Reference<rdf::XDocumentMetadataAccess> xDocumentMetadataAccess( + rDoc.GetDocShell()->GetBaseModel(), uno::UNO_QUERY); + + const css::uno::Reference<css::rdf::XResource> xSubject(xRet, uno::UNO_QUERY); + const uno::Reference<rdf::XRepository>& xRepository = + xDocumentMetadataAccess->getRDFRepository(); + const uno::Reference<container::XEnumeration> xEnum( + xRepository->getStatements(xSubject, xODF_SHADING, nullptr), uno::UNO_SET_THROW); + + while (xEnum->hasMoreElements()) + { + rdf::Statement stmt; + if (!(xEnum->nextElement() >>= stmt)) { + throw uno::RuntimeException(); + } + const uno::Reference<rdf::XLiteral> xObject(stmt.Object, uno::UNO_QUERY); + if (!xObject.is()) continue; + if (xEnum->hasMoreElements()) { + SAL_INFO("sw.uno", "ignoring other odf:shading statements"); + } + Color rColor = Color::STRtoRGB(xObject->getValue()); + pMetaPor->SetShadowColor(rColor); + break; + } + } + pPor = pMetaPor; + } + else if (GetFnt()->IsContentControl()) + { + SwTextFrame const*const pFrame(rInf.GetTextFrame()); + SwPosition aPosition(pFrame->MapViewToModelPos(rInf.GetIdx())); + SwTextNode* pTextNode = aPosition.GetNode().GetTextNode(); + SwTextContentControl* pTextContentControl = nullptr; + if (pTextNode) + { + sal_Int32 nIndex = aPosition.GetContentIndex(); + if (SwTextAttr* pAttr = pTextNode->GetTextAttrAt(nIndex, RES_TXTATR_CONTENTCONTROL, ::sw::GetTextAttrMode::Parent)) + { + pTextContentControl = static_txtattr_cast<SwTextContentControl*>(pAttr); + } + } + pPor = new SwContentControlPortion(pTextContentControl); + } + else + { + // Only at the End! + // If pCurr does not have a width, it can however already have content. + // E.g. for non-displayable characters + + auto const ch(rInf.GetChar(rInf.GetIdx())); + SwTextFrame const*const pFrame(rInf.GetTextFrame()); + SwPosition aPosition(pFrame->MapViewToModelPos(rInf.GetIdx())); + sw::mark::IFieldmark *pBM = pFrame->GetDoc().getIDocumentMarkAccess()->getInnerFieldmarkFor(aPosition); + if(pBM != nullptr && pBM->GetFieldname( ) == ODF_FORMDATE) + { + if (ch == CH_TXT_ATR_FIELDSTART) + pPor = new SwFieldFormDatePortion(pBM, true); + else if (ch == CH_TXT_ATR_FIELDSEP) + pPor = new SwFieldMarkPortion(); // it's added in DateFieldmark? + else if (ch == CH_TXT_ATR_FIELDEND) + pPor = new SwFieldFormDatePortion(pBM, false); + } + else if (ch == CH_TXT_ATR_FIELDSTART) + pPor = new SwFieldMarkPortion(); + else if (ch == CH_TXT_ATR_FIELDSEP) + pPor = new SwFieldMarkPortion(); + else if (ch == CH_TXT_ATR_FIELDEND) + pPor = new SwFieldMarkPortion(); + else if (ch == CH_TXT_ATR_FORMELEMENT) + { + OSL_ENSURE(pBM != nullptr, "Where is my form field bookmark???"); + if (pBM != nullptr) + { + if (pBM->GetFieldname( ) == ODF_FORMCHECKBOX) + { + pPor = new SwFieldFormCheckboxPortion(); + } + else if (pBM->GetFieldname( ) == ODF_FORMDROPDOWN) + { + pPor = new SwFieldFormDropDownPortion(pBM, sw::mark::ExpandFieldmark(pBM)); + } + /* we need to check for ODF_FORMTEXT for scenario having FormFields inside FORMTEXT. + * Otherwise file will crash on open. + */ + else if (pBM->GetFieldname( ) == ODF_FORMTEXT) + { + pPor = new SwFieldMarkPortion(); + } + } + } + if( !pPor ) + { + if( !rInf.X() && !m_pCurr->GetNextPortion() && !m_pCurr->GetLen() ) + pPor = m_pCurr; + else + pPor = new SwTextPortion; + } + } + } + return pPor; +} + +// We calculate the length, the following portion limits are defined: +// 1) Tabs +// 2) Linebreaks +// 3) CH_TXTATR_BREAKWORD / CH_TXTATR_INWORD +// 4) next attribute change + +SwTextPortion *SwTextFormatter::NewTextPortion( SwTextFormatInfo &rInf ) +{ + // If we're at the line's beginning, we take pCurr + // If pCurr is not derived from SwTextPortion, we need to duplicate + Seek( rInf.GetIdx() ); + SwTextPortion *pPor = WhichTextPor( rInf ); + + // until next attribute change: + const TextFrameIndex nNextAttr = GetNextAttr(); + TextFrameIndex nNextChg = std::min(nNextAttr, TextFrameIndex(rInf.GetText().getLength())); + + // end of script type: + const TextFrameIndex nNextScript = m_pScriptInfo->NextScriptChg(rInf.GetIdx()); + nNextChg = std::min( nNextChg, nNextScript ); + + // end of direction: + const TextFrameIndex nNextDir = m_pScriptInfo->NextDirChg(rInf.GetIdx()); + nNextChg = std::min( nNextChg, nNextDir ); + + // hidden change (potentially via bookmark): + const TextFrameIndex nNextHidden = m_pScriptInfo->NextHiddenChg(rInf.GetIdx()); + nNextChg = std::min( nNextChg, nNextHidden ); + + // bookmarks + const TextFrameIndex nNextBookmark = m_pScriptInfo->NextBookmark(rInf.GetIdx()); + nNextChg = std::min(nNextChg, nNextBookmark); + + // Turbo boost: + // We assume that font characters are not larger than twice + // as wide as height. + // Very crazy: we need to take the ascent into account. + + // Mind the trap! GetSize() contains the wished-for height, the real height + // is only known in CalcAscent! + + // The ratio is even crazier: a blank in Times New Roman has an ascent of + // 182, a height of 200 and a width of 53! + // It follows that a line with a lot of blanks is processed incorrectly. + // Therefore we increase from factor 2 to 8 (due to negative kerning). + + pPor->SetLen(TextFrameIndex(1)); + CalcAscent( rInf, pPor ); + + const SwFont* pTmpFnt = rInf.GetFont(); + sal_Int32 nExpect = std::min( sal_Int32( pTmpFnt->GetHeight() ), + sal_Int32( pPor->GetAscent() ) ) / 8; + if ( !nExpect ) + nExpect = 1; + nExpect = sal_Int32(rInf.GetIdx()) + (rInf.GetLineWidth() / nExpect); + if (TextFrameIndex(nExpect) > rInf.GetIdx() && nNextChg > TextFrameIndex(nExpect)) + nNextChg = TextFrameIndex(std::min(nExpect, rInf.GetText().getLength())); + + // we keep an invariant during method calls: + // there are no portion ending characters like hard spaces + // or tabs in [ nLeftScanIdx, nRightScanIdx ] + if ( m_nLeftScanIdx <= rInf.GetIdx() && rInf.GetIdx() <= m_nRightScanIdx ) + { + if ( nNextChg > m_nRightScanIdx ) + nNextChg = m_nRightScanIdx = + rInf.ScanPortionEnd( m_nRightScanIdx, nNextChg ); + } + else + { + m_nLeftScanIdx = rInf.GetIdx(); + nNextChg = m_nRightScanIdx = + rInf.ScanPortionEnd( rInf.GetIdx(), nNextChg ); + } + + pPor->SetLen( nNextChg - rInf.GetIdx() ); + rInf.SetLen( pPor->GetLen() ); + return pPor; +} + +// first portions have no length +SwLinePortion *SwTextFormatter::WhichFirstPortion(SwTextFormatInfo &rInf) +{ + SwLinePortion *pPor = nullptr; + + if( rInf.GetRest() ) + { + // Tabs and fields + if( '\0' != rInf.GetHookChar() ) + return nullptr; + + pPor = rInf.GetRest(); + if( pPor->IsErgoSumPortion() ) + rInf.SetErgoDone(true); + else + if( pPor->IsFootnoteNumPortion() ) + rInf.SetFootnoteDone(true); + else + if( pPor->InNumberGrp() ) + rInf.SetNumDone(true); + + rInf.SetRest(nullptr); + m_pCurr->SetRest( true ); + return pPor; + } + + // We can stand in the follow, it's crucial that + // pFrame->GetOffset() == 0! + if( rInf.GetIdx() ) + { + // We now too can elongate FootnotePortions and ErgoSumPortions + + // 1. The ErgoSumTexts + if( !rInf.IsErgoDone() ) + { + if( m_pFrame->IsInFootnote() && !m_pFrame->GetIndPrev() ) + pPor = NewErgoSumPortion( rInf ); + rInf.SetErgoDone( true ); + } + + // 2. Arrow portions + if( !pPor && !rInf.IsArrowDone() ) + { + if( m_pFrame->GetOffset() && !m_pFrame->IsFollow() && + rInf.GetIdx() == m_pFrame->GetOffset() ) + pPor = new SwArrowPortion( *m_pCurr ); + rInf.SetArrowDone( true ); + } + + // 3. Kerning portions at beginning of line in grid mode + if ( ! pPor && ! m_pCurr->GetNextPortion() ) + { + SwTextGridItem const*const pGrid( + GetGridItem(GetTextFrame()->FindPageFrame())); + if ( pGrid ) + pPor = new SwKernPortion( *m_pCurr ); + } + + // 4. The line rests (multiline fields) + if( !pPor ) + { + pPor = rInf.GetRest(); + // Only for pPor of course + if( pPor ) + { + m_pCurr->SetRest( true ); + rInf.SetRest(nullptr); + } + } + } + else + { + // 5. The foot note count + if( !rInf.IsFootnoteDone() ) + { + OSL_ENSURE( ( ! rInf.IsMulti() && ! m_pMulti ) || m_pMulti->HasRotation(), + "Rotated number portion trouble" ); + + const bool bFootnoteNum = m_pFrame->IsFootnoteNumFrame(); + rInf.GetParaPortion()->SetFootnoteNum( bFootnoteNum ); + if( bFootnoteNum ) + pPor = NewFootnoteNumPortion( rInf ); + rInf.SetFootnoteDone( true ); + } + + // 6. The ErgoSumTexts of course also exist in the TextMaster, + // it's crucial whether the SwFootnoteFrame is aFollow + if( !rInf.IsErgoDone() && !pPor && ! rInf.IsMulti() ) + { + if( m_pFrame->IsInFootnote() && !m_pFrame->GetIndPrev() ) + pPor = NewErgoSumPortion( rInf ); + rInf.SetErgoDone( true ); + } + + // 7. The numbering + if( !rInf.IsNumDone() && !pPor ) + { + OSL_ENSURE( ( ! rInf.IsMulti() && ! m_pMulti ) || m_pMulti->HasRotation(), + "Rotated number portion trouble" ); + + // If we're in the follow, then of course not + if (GetTextFrame()->GetTextNodeForParaProps()->GetNumRule()) + pPor = NewNumberPortion( rInf ); + rInf.SetNumDone( true ); + } + // 8. The DropCaps + if( !pPor && GetDropFormat() && ! rInf.IsMulti() ) + pPor = NewDropPortion( rInf ); + + // 9. Kerning portions at beginning of line in grid mode + if ( !pPor && !m_pCurr->GetNextPortion() ) + { + SwTextGridItem const*const pGrid( + GetGridItem(GetTextFrame()->FindPageFrame())); + if ( pGrid ) + pPor = new SwKernPortion( *m_pCurr ); + } + } + + // 10. Decimal tab portion at the beginning of each line in table cells + if ( !pPor && !m_pCurr->GetNextPortion() && + GetTextFrame()->IsInTab() && + GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::TAB_COMPAT)) + { + pPor = NewTabPortion( rInf, true ); + } + + // 11. suffix of meta-field + if (!pPor) + { + pPor = TryNewNoLengthPortion(rInf); + } + + // 12. bookmarks + // check this *last* so that BuildMultiPortion() can find it! + if (!pPor && rInf.CheckCurrentPosBookmark()) + { + const auto& bookmark = m_pScriptInfo->GetBookmarks(rInf.GetIdx()); + if (!bookmark.empty()) + { + // only for character width, maybe replaced with ] later + sal_Unicode mark = '['; + + pPor = new SwBookmarkPortion(mark, bookmark); + } + } + + return pPor; +} + +static bool lcl_OldFieldRest( const SwLineLayout* pCurr ) +{ + if( !pCurr->GetNext() ) + return false; + const SwLinePortion *pPor = pCurr->GetNext()->GetNextPortion(); + bool bRet = false; + while( pPor && !bRet ) + { + bRet = (pPor->InFieldGrp() && static_cast<const SwFieldPortion*>(pPor)->IsFollow()) || + (pPor->IsMultiPortion() && static_cast<const SwMultiPortion*>(pPor)->IsFollowField()); + if( !pPor->GetLen() ) + break; + pPor = pPor->GetNextPortion(); + } + return bRet; +} + +/* NewPortion sets rInf.nLen + * A SwTextPortion is limited by a tab, break, txtatr or attr change + * We can have three cases: + * 1) The line is full and the wrap was not emulated + * -> return 0; + * 2) The line is full and a wrap was emulated + * -> Reset width and return new FlyPortion + * 3) We need to construct a new portion + * -> CalcFlyWidth emulates the width and return portion, if needed + */ + +SwLinePortion *SwTextFormatter::NewPortion(SwTextFormatInfo &rInf, + ::std::optional<TextFrameIndex> const oMovedFlyIndex) +{ + if (oMovedFlyIndex && *oMovedFlyIndex <= rInf.GetIdx()) + { + SAL_WARN_IF(*oMovedFlyIndex != rInf.GetIdx(), "sw.core", "stopping too late, no portion break at fly anchor?"); + rInf.SetStop(true); + return nullptr; + } + + // Underflow takes precedence + rInf.SetStopUnderflow( false ); + if( rInf.GetUnderflow() ) + { + OSL_ENSURE( rInf.IsFull(), "SwTextFormatter::NewPortion: underflow but not full" ); + return Underflow( rInf ); + } + + // If the line is full, flys and Underflow portions could be waiting ... + if( rInf.IsFull() ) + { + // LineBreaks and Flys (bug05.sdw) + // IsDummy() + if( rInf.IsNewLine() && (!rInf.GetFly() || !m_pCurr->IsDummy()) ) + return nullptr; + + // When the text bumps into the Fly, or when the Fly comes first because + // it juts out over the left edge, GetFly() is returned. + // When IsFull() and no GetFly() is available, naturally zero is returned. + if( rInf.GetFly() ) + { + if( rInf.GetLast()->IsBreakPortion() ) + { + delete rInf.GetFly(); + rInf.SetFly( nullptr ); + } + + return rInf.GetFly(); + } + + // A nasty special case: A frame without wrap overlaps the Footnote area. + // We must declare the Footnote portion as rest of line, so that + // SwTextFrame::Format doesn't abort (the text mass already was formatted). + if( rInf.GetRest() ) + rInf.SetNewLine( true ); + else + { + // When the next line begins with a rest of a field, but now no + // rest remains, the line must definitely be formatted anew! + if( lcl_OldFieldRest( GetCurr() ) ) + rInf.SetNewLine( true ); + else + { + SwLinePortion *pFirst = WhichFirstPortion( rInf ); + if( pFirst ) + { + rInf.SetNewLine( true ); + if( pFirst->InNumberGrp() ) + rInf.SetNumDone( false) ; + delete pFirst; + } + } + } + + return nullptr; + } + + SwLinePortion *pPor = WhichFirstPortion( rInf ); + + // Check for Hidden Portion: + if ( !pPor ) + { + TextFrameIndex nEnd = rInf.GetIdx(); + if ( ::lcl_BuildHiddenPortion( rInf, nEnd ) ) + pPor = new SwHiddenTextPortion( nEnd - rInf.GetIdx() ); + } + + if( !pPor ) + { + if( ( !m_pMulti || m_pMulti->IsBidi() ) && + // i#42734 + // No multi portion if there is a hook character waiting: + ( !rInf.GetRest() || '\0' == rInf.GetHookChar() ) ) + { + // We open a multiportion part, if we enter a multi-line part + // of the paragraph. + TextFrameIndex nEnd = rInf.GetIdx(); + std::optional<SwMultiCreator> pCreate = rInf.GetMultiCreator( nEnd, m_pMulti ); + if( pCreate ) + { + SwMultiPortion* pTmp = nullptr; + + if ( SwMultiCreatorId::Bidi == pCreate->nId ) + pTmp = new SwBidiPortion( nEnd, pCreate->nLevel ); + else if ( SwMultiCreatorId::Ruby == pCreate->nId ) + { + pTmp = new SwRubyPortion( *pCreate, *rInf.GetFont(), + GetTextFrame()->GetDoc().getIDocumentSettingAccess(), + nEnd, TextFrameIndex(0), rInf ); + } + else if( SwMultiCreatorId::Rotate == pCreate->nId ) + { + pTmp = new SwRotatedPortion( *pCreate, nEnd, + GetTextFrame()->IsRightToLeft() ); + GetTextFrame()->SetHasRotatedPortions(true); + } + else + pTmp = new SwDoubleLinePortion( *pCreate, nEnd ); + + pCreate.reset(); + CalcFlyWidth( rInf ); + + return pTmp; + } + } + // Tabs and Fields + sal_Unicode cChar = rInf.GetHookChar(); + + if( cChar ) + { + /* We fetch cChar again to be sure that the tab is pending now and + * didn't move to the next line (as happens behind frames). + * However, when a FieldPortion is in the rest, we must naturally fetch + * the cChar from the field content, e.g. DecimalTabs and fields (22615) + */ + if( !rInf.GetRest() || !rInf.GetRest()->InFieldGrp() ) + cChar = rInf.GetChar( rInf.GetIdx() ); + rInf.ClearHookChar(); + } + else + { + if (rInf.GetIdx() >= TextFrameIndex(rInf.GetText().getLength())) + { + rInf.SetFull(true); + CalcFlyWidth( rInf ); + return pPor; + } + cChar = rInf.GetChar( rInf.GetIdx() ); + } + + switch( cChar ) + { + case CH_TAB: + pPor = NewTabPortion( rInf, false ); break; + + case CH_BREAK: + { + SwTextAttr* pHint = GetAttr(rInf.GetIdx()); + pPor = new SwBreakPortion(*rInf.GetLast(), pHint); + break; + } + + case CHAR_SOFTHYPHEN: // soft hyphen + pPor = new SwSoftHyphPortion; break; + + case CHAR_HARDBLANK: // no-break space + // Please check tdf#115067 if you want to edit the char + pPor = new SwBlankPortion( cChar ); break; + + case CHAR_HARDHYPHEN: // non-breaking hyphen + pPor = new SwBlankPortion( '-' ); break; + + case CHAR_ZWSP: // zero width space + case CHAR_WJ : // word joiner + pPor = new SwControlCharPortion( cChar ); break; + + case CH_TXTATR_BREAKWORD: + case CH_TXTATR_INWORD: + if( rInf.HasHint( rInf.GetIdx() ) ) + { + pPor = NewExtraPortion( rInf ); + break; + } + [[fallthrough]]; + default : + { + SwTabPortion* pLastTabPortion = rInf.GetLastTab(); + if ( pLastTabPortion && cChar == rInf.GetTabDecimal() ) + { + // Abandon dec. tab position if line is full + // We have a decimal tab portion in the line and the next character has to be + // aligned at the tab stop position. We store the width from the beginning of + // the tab stop portion up to the portion containing the decimal separator: + if (GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::TAB_COMPAT) /*rInf.GetVsh()->IsTabCompat();*/ && + PortionType::TabDecimal == pLastTabPortion->GetWhichPor() ) + { + OSL_ENSURE( rInf.X() >= pLastTabPortion->GetFix(), "Decimal tab stop position cannot be calculated" ); + const sal_uInt16 nWidthOfPortionsUpToDecimalPosition = o3tl::narrowing<sal_uInt16>(rInf.X() - pLastTabPortion->GetFix() ); + static_cast<SwTabDecimalPortion*>(pLastTabPortion)->SetWidthOfPortionsUpToDecimalPosition( nWidthOfPortionsUpToDecimalPosition ); + rInf.SetTabDecimal( 0 ); + } + else + rInf.SetFull( rInf.GetLastTab()->Format( rInf ) ); + } + + if( rInf.GetRest() ) + { + if( rInf.IsFull() ) + { + rInf.SetNewLine(true); + return nullptr; + } + pPor = rInf.GetRest(); + rInf.SetRest(nullptr); + } + else + { + if( rInf.IsFull() ) + return nullptr; + pPor = NewTextPortion( rInf ); + } + break; + } + } + + // if a portion is created despite there being a pending RestPortion, + // then it is a field which has been split (e.g. because it contains a Tab) + if( pPor && rInf.GetRest() ) + pPor->SetLen(TextFrameIndex(0)); + + // robust: + if( !pPor || rInf.IsStop() ) + { + delete pPor; + return nullptr; + } + } + + assert(pPor && "can only reach here with pPor existing"); + + // Special portions containing numbers (footnote anchor, footnote number, + // numbering) can be contained in a rotated portion, if the user + // choose a rotated character attribute. + if (!m_pMulti) + { + if ( pPor->IsFootnotePortion() ) + { + const SwTextFootnote* pTextFootnote = static_cast<SwFootnotePortion*>(pPor)->GetTextFootnote(); + + if ( pTextFootnote ) + { + SwFormatFootnote& rFootnote = const_cast<SwFormatFootnote&>(pTextFootnote->GetFootnote()); + const SwDoc *const pDoc = &rInf.GetTextFrame()->GetDoc(); + const SwEndNoteInfo* pInfo; + if( rFootnote.IsEndNote() ) + pInfo = &pDoc->GetEndNoteInfo(); + else + pInfo = &pDoc->GetFootnoteInfo(); + const SwAttrSet& rSet = pInfo->GetAnchorCharFormat(const_cast<SwDoc&>(*pDoc))->GetAttrSet(); + + Degree10 nDir(0); + if( const SvxCharRotateItem* pItem = rSet.GetItemIfSet( RES_CHRATR_ROTATE ) ) + nDir = pItem->GetValue(); + + if ( nDir ) + { + delete pPor; + pPor = new SwRotatedPortion(rInf.GetIdx() + TextFrameIndex(1), + 900_deg10 == nDir + ? DIR_BOTTOM2TOP + : DIR_TOP2BOTTOM ); + } + } + } + else if ( pPor->InNumberGrp() ) + { + const SwFont* pNumFnt = static_cast<SwFieldPortion*>(pPor)->GetFont(); + + if ( pNumFnt ) + { + Degree10 nDir = pNumFnt->GetOrientation( rInf.GetTextFrame()->IsVertical() ); + if ( nDir ) + { + delete pPor; + pPor = new SwRotatedPortion(TextFrameIndex(0), 900_deg10 == nDir + ? DIR_BOTTOM2TOP + : DIR_TOP2BOTTOM ); + + rInf.SetNumDone( false ); + rInf.SetFootnoteDone( false ); + } + } + } + } + + // The font is set in output device, + // the ascent and the height will be calculated. + if( !pPor->GetAscent() && !pPor->Height() ) + CalcAscent( rInf, pPor ); + rInf.SetLen( pPor->GetLen() ); + + // In CalcFlyWidth Width() will be shortened if a FlyPortion is present. + CalcFlyWidth( rInf ); + + // One must not forget that pCurr as GetLast() must provide reasonable values: + if( !m_pCurr->Height() ) + { + OSL_ENSURE( m_pCurr->Height(), "SwTextFormatter::NewPortion: limbo dance" ); + m_pCurr->Height( pPor->Height(), false ); + m_pCurr->SetAscent( pPor->GetAscent() ); + } + + OSL_ENSURE(pPor->Height(), "SwTextFormatter::NewPortion: something went wrong"); + if( pPor->IsPostItsPortion() && rInf.X() >= rInf.Width() && rInf.GetFly() ) + { + delete pPor; + pPor = rInf.GetFly(); + } + return pPor; +} + +TextFrameIndex SwTextFormatter::FormatLine(TextFrameIndex const nStartPos) +{ + OSL_ENSURE( ! m_pFrame->IsVertical() || m_pFrame->IsSwapped(), + "SwTextFormatter::FormatLine( nStartPos ) with unswapped frame" ); + + // For the formatting routines, we set pOut to the reference device. + SwHookOut aHook( GetInfo() ); + if (GetInfo().GetLen() < TextFrameIndex(GetInfo().GetText().getLength())) + GetInfo().SetLen(TextFrameIndex(GetInfo().GetText().getLength())); + + bool bBuild = true; + SetFlyInCntBase( false ); + GetInfo().SetLineHeight( 0 ); + GetInfo().SetLineNetHeight( 0 ); + + // Recycling must be suppressed by changed line height and also + // by changed ascent (lowering of baseline). + const SwTwips nOldHeight = m_pCurr->Height(); + const SwTwips nOldAscent = m_pCurr->GetAscent(); + + m_pCurr->SetEndHyph( false ); + m_pCurr->SetMidHyph( false ); + + // fly positioning can make it necessary format a line several times + // for this, we have to keep a copy of our rest portion + SwLinePortion* pField = GetInfo().GetRest(); + std::unique_ptr<SwFieldPortion> xSaveField; + + if ( pField && pField->InFieldGrp() && !pField->IsFootnotePortion() ) + xSaveField.reset(new SwFieldPortion( *static_cast<SwFieldPortion*>(pField) )); + + // for an optimal repaint rectangle, we want to compare fly portions + // before and after the BuildPortions call + const bool bOptimizeRepaint = AllowRepaintOpt(); + TextFrameIndex const nOldLineEnd = nStartPos + m_pCurr->GetLen(); + std::vector<tools::Long> flyStarts; + + // these are the conditions for a fly position comparison + if ( bOptimizeRepaint && m_pCurr->IsFly() ) + { + SwLinePortion* pPor = m_pCurr->GetFirstPortion(); + tools::Long nPOfst = 0; + while ( pPor ) + { + if ( pPor->IsFlyPortion() ) + // insert start value of fly portion + flyStarts.push_back( nPOfst ); + + nPOfst += pPor->Width(); + pPor = pPor->GetNextPortion(); + } + } + + // Here soon the underflow check follows. + while( bBuild ) + { + GetInfo().SetFootnoteInside( false ); + GetInfo().SetOtherThanFootnoteInside( false ); + + // These values must not be reset by FormatReset(); + const bool bOldNumDone = GetInfo().IsNumDone(); + const bool bOldFootnoteDone = GetInfo().IsFootnoteDone(); + const bool bOldArrowDone = GetInfo().IsArrowDone(); + const bool bOldErgoDone = GetInfo().IsErgoDone(); + + // besides other things, this sets the repaint offset to 0 + FormatReset( GetInfo() ); + + GetInfo().SetNumDone( bOldNumDone ); + GetInfo().SetFootnoteDone(bOldFootnoteDone); + GetInfo().SetArrowDone( bOldArrowDone ); + GetInfo().SetErgoDone( bOldErgoDone ); + + // build new portions for this line + BuildPortions( GetInfo() ); + + if( GetInfo().IsStop() ) + { + m_pCurr->SetLen(TextFrameIndex(0)); + m_pCurr->Height( GetFrameRstHeight() + 1, false ); + m_pCurr->SetRealHeight( GetFrameRstHeight() + 1 ); + + // Don't oversize the line in case of split flys, so we don't try to move the anchor + // of a precede fly forward, next to its follow. + if (m_pFrame->HasNonLastSplitFlyDrawObj()) + { + m_pCurr->SetRealHeight(GetFrameRstHeight()); + } + + m_pCurr->Width(0); + m_pCurr->Truncate(); + return nStartPos; + } + else if( GetInfo().IsDropInit() ) + { + DropInit(); + GetInfo().SetDropInit( false ); + } + + m_pCurr->CalcLine( *this, GetInfo() ); + CalcRealHeight( GetInfo().IsNewLine() ); + + //i#120864 For Special case that at the first calculation couldn't get + //correct height. And need to recalculate for the right height. + SwLinePortion* pPorTmp = m_pCurr->GetNextPortion(); + if ( IsFlyInCntBase() && (!IsQuick() || (pPorTmp && pPorTmp->IsFlyCntPortion() && !pPorTmp->GetNextPortion() && + m_pCurr->Height() > pPorTmp->Height()))) + { + SwTwips nTmpAscent, nTmpHeight; + CalcAscentAndHeight( nTmpAscent, nTmpHeight ); + AlignFlyInCntBase( Y() + tools::Long( nTmpAscent ) ); + m_pCurr->CalcLine( *this, GetInfo() ); + CalcRealHeight(); + } + + // bBuild decides if another lap of honor is done + if ( m_pCurr->GetRealHeight() <= GetInfo().GetLineHeight() ) + { + m_pCurr->SetRealHeight( GetInfo().GetLineHeight() ); + bBuild = false; + } + else + { + bBuild = ( GetInfo().GetTextFly().IsOn() && ChkFlyUnderflow(GetInfo()) ) + || GetInfo().CheckFootnotePortion(m_pCurr); + if( bBuild ) + { + // fdo44018-2.doc: only restore m_bNumDone if a SwNumberPortion will be truncated + for (SwLinePortion * pPor = m_pCurr->GetNextPortion(); pPor; pPor = pPor->GetNextPortion()) + { + if (pPor->InNumberGrp()) + { + GetInfo().SetNumDone( bOldNumDone ); + break; + } + } + GetInfo().ResetMaxWidthDiff(); + + // delete old rest + if ( GetInfo().GetRest() ) + { + delete GetInfo().GetRest(); + GetInfo().SetRest( nullptr ); + } + + // set original rest portion + if ( xSaveField ) + GetInfo().SetRest( new SwFieldPortion( *xSaveField ) ); + + m_pCurr->SetLen(TextFrameIndex(0)); + m_pCurr->Width(0); + m_pCurr->Truncate(); + } + } + } + + // In case of compat mode, it's possible that a tab portion is wider after + // formatting than before. If this is the case, we also have to make sure + // the SwLineLayout is wider as well. + if (GetInfo().GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::TAB_OVER_MARGIN)) + { + sal_uInt16 nSum = 0; + SwLinePortion* pPor = m_pCurr->GetFirstPortion(); + + while (pPor) + { + nSum += pPor->Width(); + pPor = pPor->GetNextPortion(); + } + + if (nSum > m_pCurr->Width()) + m_pCurr->Width(nSum); + } + + // calculate optimal repaint rectangle + if ( bOptimizeRepaint ) + { + GetInfo().SetPaintOfst( ::lcl_CalcOptRepaint( *this, *m_pCurr, nOldLineEnd, flyStarts ) ); + flyStarts.clear(); + } + else + // Special case: we do not allow an optimization of the repaint + // area, but during formatting the repaint offset is set to indicate + // a maximum value for the offset. This value has to be reset: + GetInfo().SetPaintOfst( 0 ); + + // This corrects the start of the reformat range if something has + // moved to the next line. Otherwise IsFirstReformat in AllowRepaintOpt + // will give us a wrong result if we have to reformat another line + GetInfo().GetParaPortion()->GetReformat().LeftMove( GetInfo().GetIdx() ); + + // delete master copy of rest portion + xSaveField.reset(); + + TextFrameIndex const nNewStart = nStartPos + m_pCurr->GetLen(); + + // adjust text if kana compression is enabled + if ( GetInfo().CompressLine() ) + { + SwTwips nRepaintOfst = CalcKanaAdj( m_pCurr ); + + // adjust repaint offset + if ( nRepaintOfst < GetInfo().GetPaintOfst() ) + GetInfo().SetPaintOfst( nRepaintOfst ); + } + + CalcAdjustLine( m_pCurr ); + + if( nOldHeight != m_pCurr->Height() || nOldAscent != m_pCurr->GetAscent() ) + { + SetFlyInCntBase(); + GetInfo().SetPaintOfst( 0 ); // changed line height => no recycling + // all following line must be painted and when Flys are around, + // also formatted + GetInfo().SetShift( true ); + } + + if ( IsFlyInCntBase() && !IsQuick() ) + UpdatePos( m_pCurr, GetTopLeft(), GetStart() ); + + return nNewStart; +} + +void SwTextFormatter::RecalcRealHeight() +{ + do + { + CalcRealHeight(); + } while (Next()); +} + +void SwTextFormatter::CalcRealHeight( bool bNewLine ) +{ + SwTwips nLineHeight = m_pCurr->Height(); + m_pCurr->SetClipping( false ); + + SwTextGridItem const*const pGrid(GetGridItem(m_pFrame->FindPageFrame())); + if ( pGrid && GetInfo().SnapToGrid() ) + { + const sal_uInt16 nGridWidth = pGrid->GetBaseHeight(); + const sal_uInt16 nRubyHeight = pGrid->GetRubyHeight(); + const bool bRubyTop = ! pGrid->GetRubyTextBelow(); + + nLineHeight = nGridWidth + nRubyHeight; + const sal_uInt16 nAmpRatio = (m_pCurr->Height() + nLineHeight - 1)/nLineHeight; + nLineHeight *= nAmpRatio; + + const sal_uInt16 nAsc = m_pCurr->GetAscent() + + ( bRubyTop ? + ( nLineHeight - m_pCurr->Height() + nRubyHeight ) / 2 : + ( nLineHeight - m_pCurr->Height() - nRubyHeight ) / 2 ); + + m_pCurr->Height( nLineHeight, false ); + m_pCurr->SetAscent( nAsc ); + m_pInf->GetParaPortion()->SetFixLineHeight(); + + // we ignore any line spacing options except from ... + const SvxLineSpacingItem* pSpace = m_aLineInf.GetLineSpacing(); + if ( ! IsParaLine() && pSpace && + SvxInterLineSpaceRule::Prop == pSpace->GetInterLineSpaceRule() ) + { + sal_uLong nTmp = pSpace->GetPropLineSpace(); + + if( nTmp < 100 ) + nTmp = 100; + + nTmp *= nLineHeight; + nLineHeight = nTmp / 100; + } + + m_pCurr->SetRealHeight( nLineHeight ); + return; + } + + // The dummy flag is set on lines that only contain flyportions, these shouldn't + // consider register-true and so on. Unfortunately an empty line can be at + // the end of a paragraph (empty paragraphs or behind a Shift-Return), + // which should consider the register. + if (!m_pCurr->IsDummy() || (!m_pCurr->GetNext() + && GetStart() >= TextFrameIndex(GetTextFrame()->GetText().getLength()) + && !bNewLine)) + { + const SvxLineSpacingItem *pSpace = m_aLineInf.GetLineSpacing(); + if( pSpace ) + { + switch( pSpace->GetLineSpaceRule() ) + { + case SvxLineSpaceRule::Auto: + // shrink first line of paragraph too on spacing < 100% + if (IsParaLine() && + pSpace->GetInterLineSpaceRule() == SvxInterLineSpaceRule::Prop + && GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::PROP_LINE_SPACING_SHRINKS_FIRST_LINE)) + { + tools::Long nTmp = pSpace->GetPropLineSpace(); + // Word will render < 50% too but it's just not readable + if( nTmp < 50 ) + nTmp = nTmp ? 50 : 100; + if (nTmp<100) { // code adapted from fixed line height + nTmp *= nLineHeight; + nTmp /= 100; + if( !nTmp ) + ++nTmp; + nLineHeight = nTmp; + sal_uInt16 nAsc = ( 4 * nLineHeight ) / 5; // 80% +#if 0 + // could do clipping here (like Word does) + // but at 0.5 its unreadable either way... + if( nAsc < pCurr->GetAscent() || + nLineHeight - nAsc < pCurr->Height() - + pCurr->GetAscent() ) + pCurr->SetClipping( true ); +#endif + m_pCurr->SetAscent( nAsc ); + m_pCurr->Height( nLineHeight, false ); + m_pInf->GetParaPortion()->SetFixLineHeight(); + } + } + break; + case SvxLineSpaceRule::Min: + { + if( nLineHeight < pSpace->GetLineHeight() ) + nLineHeight = pSpace->GetLineHeight(); + break; + } + case SvxLineSpaceRule::Fix: + { + nLineHeight = pSpace->GetLineHeight(); + const sal_uInt16 nAsc = ( 4 * nLineHeight ) / 5; // 80% + if( nAsc < m_pCurr->GetAscent() || + nLineHeight - nAsc < m_pCurr->Height() - m_pCurr->GetAscent() ) + m_pCurr->SetClipping( true ); + m_pCurr->Height( nLineHeight, false ); + m_pCurr->SetAscent( nAsc ); + m_pInf->GetParaPortion()->SetFixLineHeight(); + } + break; + default: OSL_FAIL( ": unknown LineSpaceRule" ); + } + // Note: for the _first_ line the line spacing of the previous + // paragraph is applied in SwFlowFrame::CalcUpperSpace() + if( !IsParaLine() ) + switch( pSpace->GetInterLineSpaceRule() ) + { + case SvxInterLineSpaceRule::Off: + break; + case SvxInterLineSpaceRule::Prop: + { + tools::Long nTmp = pSpace->GetPropLineSpace(); + // 50% is the minimum, if 0% we switch to the + // default value 100% ... + if( nTmp < 50 ) + nTmp = nTmp ? 50 : 100; + + // extend line height by (nPropLineSpace - 100) percent of the font height + nTmp -= 100; + nTmp *= m_pCurr->GetTextHeight(); + nTmp /= 100; + nTmp += nLineHeight; + if (nTmp < 1) + nTmp = 1; + nLineHeight = nTmp; + break; + } + case SvxInterLineSpaceRule::Fix: + { + nLineHeight = nLineHeight + pSpace->GetInterLineSpace(); + break; + } + default: OSL_FAIL( ": unknown InterLineSpaceRule" ); + } + } + + if( IsRegisterOn() ) + { + SwTwips nTmpY = Y() + m_pCurr->GetAscent() + nLineHeight - m_pCurr->Height(); + SwRectFnSet aRectFnSet(m_pFrame); + if ( aRectFnSet.IsVert() ) + nTmpY = m_pFrame->SwitchHorizontalToVertical( nTmpY ); + nTmpY = aRectFnSet.YDiff( nTmpY, RegStart() ); + const sal_uInt16 nDiff = sal_uInt16( nTmpY % RegDiff() ); + if( nDiff ) + nLineHeight += RegDiff() - nDiff; + } + } + m_pCurr->SetRealHeight( nLineHeight ); +} + +void SwTextFormatter::FeedInf( SwTextFormatInfo &rInf ) const +{ + // delete Fly in any case! + ClearFly( rInf ); + rInf.Init(); + + rInf.ChkNoHyph( CntEndHyph(), CntMidHyph() ); + rInf.SetRoot( m_pCurr ); + rInf.SetLineStart( m_nStart ); + rInf.SetIdx( m_nStart ); + rInf.Left( Left() ); + rInf.Right( Right() ); + rInf.First( FirstLeft() ); + rInf.LeftMargin(GetLeftMargin()); + + rInf.RealWidth( sal_uInt16(rInf.Right() - GetLeftMargin()) ); + rInf.Width( rInf.RealWidth() ); + if( const_cast<SwTextFormatter*>(this)->GetRedln() ) + { + const_cast<SwTextFormatter*>(this)->GetRedln()->Clear( const_cast<SwTextFormatter*>(this)->GetFnt() ); + const_cast<SwTextFormatter*>(this)->GetRedln()->Reset(); + } +} + +void SwTextFormatter::FormatReset( SwTextFormatInfo &rInf ) +{ + m_pFirstOfBorderMerge = nullptr; + m_pCurr->Truncate(); + m_pCurr->Init(); + + // delete pSpaceAdd and pKanaComp + m_pCurr->FinishSpaceAdd(); + m_pCurr->FinishKanaComp(); + m_pCurr->ResetFlags(); + FeedInf( rInf ); +} + +bool SwTextFormatter::CalcOnceMore() +{ + if( m_pDropFormat ) + { + const sal_uInt16 nOldDrop = GetDropHeight(); + CalcDropHeight( m_pDropFormat->GetLines() ); + m_bOnceMore = nOldDrop != GetDropHeight(); + } + else + m_bOnceMore = false; + return m_bOnceMore; +} + +SwTwips SwTextFormatter::CalcBottomLine() const +{ + SwTwips nRet = Y() + GetLineHeight(); + SwTwips nMin = GetInfo().GetTextFly().GetMinBottom(); + if( nMin && ++nMin > nRet ) + { + SwTwips nDist = m_pFrame->getFrameArea().Height() - m_pFrame->getFramePrintArea().Height() + - m_pFrame->getFramePrintArea().Top(); + if( nRet + nDist < nMin ) + { + const bool bRepaint = HasTruncLines() && + GetInfo().GetParaPortion()->GetRepaint().Bottom() == nRet-1; + nRet = nMin - nDist; + if( bRepaint ) + { + const_cast<SwRepaint&>(GetInfo().GetParaPortion() + ->GetRepaint()).Bottom( nRet-1 ); + const_cast<SwTextFormatInfo&>(GetInfo()).SetPaintOfst( 0 ); + } + } + } + return nRet; +} + +// FME/OD: This routine does a limited text formatting. +SwTwips SwTextFormatter::CalcFitToContent_() +{ + FormatReset( GetInfo() ); + BuildPortions( GetInfo() ); + m_pCurr->CalcLine( *this, GetInfo() ); + return m_pCurr->Width(); +} + +// determines if the calculation of a repaint offset is allowed +// otherwise each line is painted from 0 (this is a copy of the beginning +// of the former SwTextFormatter::Recycle() function +bool SwTextFormatter::AllowRepaintOpt() const +{ + // reformat position in front of current line? Only in this case + // we want to set the repaint offset + bool bOptimizeRepaint = m_nStart < GetInfo().GetReformatStart() && + m_pCurr->GetLen(); + + // a special case is the last line of a block adjusted paragraph: + if ( bOptimizeRepaint ) + { + switch( GetAdjust() ) + { + case SvxAdjust::Block: + { + if( IsLastBlock() || IsLastCenter() ) + bOptimizeRepaint = false; + else + { + // ????: blank in the last master line (blocksat.sdw) + bOptimizeRepaint = nullptr == m_pCurr->GetNext() && !m_pFrame->GetFollow(); + if ( bOptimizeRepaint ) + { + SwLinePortion *pPos = m_pCurr->GetFirstPortion(); + while ( pPos && !pPos->IsFlyPortion() ) + pPos = pPos->GetNextPortion(); + bOptimizeRepaint = !pPos; + } + } + break; + } + case SvxAdjust::Center: + case SvxAdjust::Right: + bOptimizeRepaint = false; + break; + default: ; + } + } + + // Again another special case: invisible SoftHyphs + const TextFrameIndex nReformat = GetInfo().GetReformatStart(); + if (bOptimizeRepaint && TextFrameIndex(COMPLETE_STRING) != nReformat) + { + const sal_Unicode cCh = nReformat >= TextFrameIndex(GetInfo().GetText().getLength()) + ? 0 + : GetInfo().GetText()[ sal_Int32(nReformat) ]; + bOptimizeRepaint = ( CH_TXTATR_BREAKWORD != cCh && CH_TXTATR_INWORD != cCh ) + || ! GetInfo().HasHint( nReformat ); + } + + return bOptimizeRepaint; +} + +void SwTextFormatter::CalcUnclipped( SwTwips& rTop, SwTwips& rBottom ) +{ + OSL_ENSURE( ! m_pFrame->IsVertical() || m_pFrame->IsSwapped(), + "SwTextFormatter::CalcUnclipped with unswapped frame" ); + + SwTwips nFlyAsc, nFlyDesc; + m_pCurr->MaxAscentDescent( rTop, rBottom, nFlyAsc, nFlyDesc ); + rTop = Y() + GetCurr()->GetAscent(); + rBottom = rTop + nFlyDesc; + rTop -= nFlyAsc; +} + +void SwTextFormatter::UpdatePos( SwLineLayout *pCurrent, Point aStart, + TextFrameIndex const nStartIdx, bool bAlways) const +{ + OSL_ENSURE( ! m_pFrame->IsVertical() || m_pFrame->IsSwapped(), + "SwTextFormatter::UpdatePos with unswapped frame" ); + + if( GetInfo().IsTest() ) + return; + SwLinePortion *pFirst = pCurrent->GetFirstPortion(); + SwLinePortion *pPos = pFirst; + SwTextPaintInfo aTmpInf( GetInfo() ); + aTmpInf.SetpSpaceAdd( pCurrent->GetpLLSpaceAdd() ); + aTmpInf.ResetSpaceIdx(); + aTmpInf.SetKanaComp( pCurrent->GetpKanaComp() ); + aTmpInf.ResetKanaIdx(); + + // The frame's size + aTmpInf.SetIdx( nStartIdx ); + aTmpInf.SetPos( aStart ); + + SwTwips nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc; + pCurrent->MaxAscentDescent( nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc ); + + const SwTwips nTmpHeight = pCurrent->GetRealHeight(); + SwTwips nAscent = pCurrent->GetAscent() + nTmpHeight - pCurrent->Height(); + AsCharFlags nFlags = AsCharFlags::UlSpace; + if( GetMulti() ) + { + aTmpInf.SetDirection( GetMulti()->GetDirection() ); + if( GetMulti()->HasRotation() ) + { + nFlags |= AsCharFlags::Rotate; + if( GetMulti()->IsRevers() ) + { + nFlags |= AsCharFlags::Reverse; + aTmpInf.X( aTmpInf.X() - nAscent ); + } + else + aTmpInf.X( aTmpInf.X() + nAscent ); + } + else + { + if ( GetMulti()->IsBidi() ) + nFlags |= AsCharFlags::Bidi; + aTmpInf.Y( aTmpInf.Y() + nAscent ); + } + } + else + aTmpInf.Y( aTmpInf.Y() + nAscent ); + + while( pPos ) + { + // We only know one case where changing the position (caused by the + // adjustment) could be relevant for a portion: We need to SetRefPoint + // for FlyCntPortions. + if( ( pPos->IsFlyCntPortion() || pPos->IsGrfNumPortion() ) + && ( bAlways || !IsQuick() ) ) + { + pCurrent->MaxAscentDescent( nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc, pPos ); + + if( pPos->IsGrfNumPortion() ) + { + if( !nFlyAsc && !nFlyDesc ) + { + nTmpAscent = nAscent; + nFlyAsc = nAscent; + nTmpDescent = nTmpHeight - nAscent; + nFlyDesc = nTmpDescent; + } + static_cast<SwGrfNumPortion*>(pPos)->SetBase( nTmpAscent, nTmpDescent, + nFlyAsc, nFlyDesc ); + } + else + { + Point aBase( aTmpInf.GetPos() ); + if ( GetInfo().GetTextFrame()->IsVertical() ) + GetInfo().GetTextFrame()->SwitchHorizontalToVertical( aBase ); + + static_cast<SwFlyCntPortion*>(pPos)->SetBase( *aTmpInf.GetTextFrame(), + aBase, nTmpAscent, nTmpDescent, nFlyAsc, + nFlyDesc, nFlags ); + } + } + if( pPos->IsMultiPortion() && static_cast<SwMultiPortion*>(pPos)->HasFlyInContent() ) + { + OSL_ENSURE( !GetMulti(), "Too much multi" ); + const_cast<SwTextFormatter*>(this)->m_pMulti = static_cast<SwMultiPortion*>(pPos); + SwLineLayout *pLay = &GetMulti()->GetRoot(); + Point aSt( aTmpInf.X(), aStart.Y() ); + + if ( GetMulti()->HasBrackets() ) + { + OSL_ENSURE( GetMulti()->IsDouble(), "Brackets only for doubles"); + aSt.AdjustX(static_cast<SwDoubleLinePortion*>(GetMulti())->PreWidth() ); + } + else if( GetMulti()->HasRotation() ) + { + aSt.AdjustY(pCurrent->GetAscent() - GetMulti()->GetAscent() ); + if( GetMulti()->IsRevers() ) + aSt.AdjustX(GetMulti()->Width() ); + else + aSt.AdjustY(GetMulti()->Height() ); + } + else if ( GetMulti()->IsBidi() ) + // jump to end of the bidi portion + aSt.AdjustX(pLay->Width() ); + + TextFrameIndex nStIdx = aTmpInf.GetIdx(); + do + { + UpdatePos( pLay, aSt, nStIdx, bAlways ); + nStIdx = nStIdx + pLay->GetLen(); + aSt.AdjustY(pLay->Height() ); + pLay = pLay->GetNext(); + } while ( pLay ); + const_cast<SwTextFormatter*>(this)->m_pMulti = nullptr; + } + pPos->Move( aTmpInf ); + pPos = pPos->GetNextPortion(); + } +} + +void SwTextFormatter::AlignFlyInCntBase( tools::Long nBaseLine ) const +{ + OSL_ENSURE( ! m_pFrame->IsVertical() || m_pFrame->IsSwapped(), + "SwTextFormatter::AlignFlyInCntBase with unswapped frame" ); + + if( GetInfo().IsTest() ) + return; + SwLinePortion *pFirst = m_pCurr->GetFirstPortion(); + SwLinePortion *pPos = pFirst; + AsCharFlags nFlags = AsCharFlags::None; + if( GetMulti() && GetMulti()->HasRotation() ) + { + nFlags |= AsCharFlags::Rotate; + if( GetMulti()->IsRevers() ) + nFlags |= AsCharFlags::Reverse; + } + + SwTwips nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc; + + while( pPos ) + { + if( pPos->IsFlyCntPortion() || pPos->IsGrfNumPortion() ) + { + m_pCurr->MaxAscentDescent( nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc, pPos ); + + if( pPos->IsGrfNumPortion() ) + static_cast<SwGrfNumPortion*>(pPos)->SetBase( nTmpAscent, nTmpDescent, + nFlyAsc, nFlyDesc ); + else + { + Point aBase; + if ( GetInfo().GetTextFrame()->IsVertical() ) + { + nBaseLine = GetInfo().GetTextFrame()->SwitchHorizontalToVertical( nBaseLine ); + aBase = Point( nBaseLine, static_cast<SwFlyCntPortion*>(pPos)->GetRefPoint().Y() ); + } + else + aBase = Point( static_cast<SwFlyCntPortion*>(pPos)->GetRefPoint().X(), nBaseLine ); + + static_cast<SwFlyCntPortion*>(pPos)->SetBase( *GetInfo().GetTextFrame(), aBase, nTmpAscent, nTmpDescent, + nFlyAsc, nFlyDesc, nFlags ); + } + } + pPos = pPos->GetNextPortion(); + } +} + +bool SwTextFormatter::ChkFlyUnderflow( SwTextFormatInfo &rInf ) const +{ + OSL_ENSURE( rInf.GetTextFly().IsOn(), "SwTextFormatter::ChkFlyUnderflow: why?" ); + if( GetCurr() ) + { + // First we check, whether a fly overlaps with the line. + // = GetLineHeight() + const sal_uInt16 nHeight = GetCurr()->GetRealHeight(); + SwRect aLine( GetLeftMargin(), Y(), rInf.RealWidth(), nHeight ); + + SwRect aLineVert( aLine ); + if ( m_pFrame->IsVertical() ) + m_pFrame->SwitchHorizontalToVertical( aLineVert ); + SwRect aInter( rInf.GetTextFly().GetFrame( aLineVert ) ); + if ( m_pFrame->IsVertical() ) + m_pFrame->SwitchVerticalToHorizontal( aInter ); + + if( !aInter.HasArea() ) + return false; + + // We now check every portion that could have lowered for overlapping + // with the fly. + const SwLinePortion *pPos = GetCurr()->GetFirstPortion(); + aLine.Pos().setY( Y() + GetCurr()->GetRealHeight() - GetCurr()->Height() ); + aLine.Height( GetCurr()->Height() ); + + while( pPos ) + { + aLine.Width( pPos->Width() ); + + aLineVert = aLine; + if ( m_pFrame->IsVertical() ) + m_pFrame->SwitchHorizontalToVertical( aLineVert ); + aInter = rInf.GetTextFly().GetFrame( aLineVert ); + if ( m_pFrame->IsVertical() ) + m_pFrame->SwitchVerticalToHorizontal( aInter ); + + // New flys from below? + if( !pPos->IsFlyPortion() ) + { + if( aInter.Overlaps( aLine ) ) + { + aInter.Intersection_( aLine ); + if( aInter.HasArea() ) + { + // To be evaluated during reformat of this line: + // RealHeight including spacing + rInf.SetLineHeight( nHeight ); + // Height without extra spacing + rInf.SetLineNetHeight( m_pCurr->Height() ); + return true; + } + } + } + else + { + // The fly portion is not intersected by a fly anymore + if ( ! aInter.Overlaps( aLine ) ) + { + rInf.SetLineHeight( nHeight ); + rInf.SetLineNetHeight( m_pCurr->Height() ); + return true; + } + else + { + aInter.Intersection_( aLine ); + + // No area means a fly has become invalid because of + // lowering the line => reformat the line + // we also have to reformat the line, if the fly size + // differs from the intersection interval's size. + if( ! aInter.HasArea() || + static_cast<const SwFlyPortion*>(pPos)->GetFixWidth() != aInter.Width() ) + { + rInf.SetLineHeight( nHeight ); + rInf.SetLineNetHeight( m_pCurr->Height() ); + return true; + } + } + } + + aLine.Left( aLine.Left() + pPos->Width() ); + pPos = pPos->GetNextPortion(); + } + } + return false; +} + +void SwTextFormatter::CalcFlyWidth( SwTextFormatInfo &rInf ) +{ + if( GetMulti() || rInf.GetFly() ) + return; + + SwTextFly& rTextFly = rInf.GetTextFly(); + if( !rTextFly.IsOn() || rInf.IsIgnoreFly() ) + return; + + const SwLinePortion *pLast = rInf.GetLast(); + + tools::Long nAscent; + tools::Long nTop = Y(); + tools::Long nHeight; + + if( rInf.GetLineHeight() ) + { + // Real line height has already been calculated, we only have to + // search for intersections in the lower part of the strip + nAscent = m_pCurr->GetAscent(); + nHeight = rInf.GetLineNetHeight(); + nTop += rInf.GetLineHeight() - nHeight; + } + else + { + // We make a first guess for the lines real height + if ( ! m_pCurr->GetRealHeight() ) + CalcRealHeight(); + + nAscent = pLast->GetAscent(); + nHeight = pLast->Height(); + + if ( m_pCurr->GetRealHeight() > nHeight ) + nTop += m_pCurr->GetRealHeight() - nHeight; + else + // Important for fixed space between lines + nHeight = m_pCurr->GetRealHeight(); + } + + const tools::Long nLeftMar = GetLeftMargin(); + const tools::Long nLeftMin = (rInf.X() || GetDropLeft()) ? nLeftMar : GetLeftMin(); + + SwRect aLine( rInf.X() + nLeftMin, nTop, rInf.RealWidth() - rInf.X() + + nLeftMar - nLeftMin , nHeight ); + + bool bWordFlyWrap = GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::ADD_VERTICAL_FLY_OFFSETS); + // tdf#116486: consider also the upper margin from getFramePrintArea because intersections + // with this additional space should lead to repositioning of paragraphs + // For compatibility we grab a related compat flag: + if (bWordFlyWrap && IsFirstTextLine()) + { + tools::Long nUpper = m_pFrame->getFramePrintArea().Top(); + // Make sure that increase only happens in case the upper spacing comes from the upper + // margin of the current text frame, not because of a lower spacing of the previous text + // frame. + nUpper -= m_pFrame->GetUpperSpaceAmountConsideredForPrevFrameAndPageGrid(); + // Increase the rectangle + if( nUpper > 0 && nTop >= nUpper ) + aLine.SubTop( nUpper ); + } + + if (IsFirstTextLine()) + { + // Check if a compatibility mode requires considering also the lower margin of this text + // frame, intersections with this additional space should lead to shifting the paragraph + // marker down. + SwTwips nLower = m_pFrame->GetLowerMarginForFlyIntersect(); + if (nLower > 0) + { + aLine.AddBottom(nLower); + } + } + + SwRect aLineVert( aLine ); + if ( m_pFrame->IsRightToLeft() ) + m_pFrame->SwitchLTRtoRTL( aLineVert ); + + if ( m_pFrame->IsVertical() ) + m_pFrame->SwitchHorizontalToVertical( aLineVert ); + + // GetFrame(...) determines and returns the intersection rectangle + SwRect aInter( rTextFly.GetFrame( aLineVert ) ); + + if ( m_pFrame->IsRightToLeft() ) + m_pFrame->SwitchRTLtoLTR( aInter ); + + if ( m_pFrame->IsVertical() ) + m_pFrame->SwitchVerticalToHorizontal( aInter ); + + if (!aInter.IsEmpty() && aInter.Bottom() < nTop) + { + // Intersects with the frame area (with upper margin), but not with the print area (without + // upper margin). Don't reserve space for the fly portion in this case, text is allowed to + // flow there. + aInter.Height(0); + } + + if( !aInter.Overlaps( aLine ) ) + return; + + aLine.Left( rInf.X() + nLeftMar ); + bool bForced = false; + bool bSplitFly = false; + for (const auto& pObj : rTextFly.GetAnchoredObjList()) + { + auto pFlyFrame = pObj->DynCastFlyFrame(); + if (!pFlyFrame) + { + continue; + } + + if (!pFlyFrame->IsFlySplitAllowed()) + { + continue; + } + + bSplitFly = true; + break; + } + if( aInter.Left() <= nLeftMin ) + { + SwTwips nFrameLeft = GetTextFrame()->getFrameArea().Left(); + SwTwips nFramePrintAreaLeft = GetTextFrame()->getFramePrintArea().Left(); + if( nFramePrintAreaLeft < 0 ) + nFrameLeft += GetTextFrame()->getFramePrintArea().Left(); + if( aInter.Left() < nFrameLeft ) + { + aInter.Left(nFrameLeft); // both sets left and reduces width + if (bSplitFly && nFramePrintAreaLeft > 0 && nFramePrintAreaLeft < aInter.Width()) + { + // We wrap around a split fly, the fly portion is on the + // left of the paragraph and we have a positive + // paragraph margin. Don't take space twice in this case + // (margin, fly portion), decrease the width of the fly + // portion accordingly. + aInter.Right(aInter.Right() - nFramePrintAreaLeft); + } + } + + tools::Long nAddMar = 0; + if (GetTextFrame()->IsRightToLeft()) + { + nAddMar = GetTextFrame()->getFrameArea().Right() - Right(); + if ( nAddMar < 0 ) + nAddMar = 0; + } + else + nAddMar = nLeftMar - nFrameLeft; + + aInter.Width( aInter.Width() + nAddMar ); + // For a negative first line indent, we set this flag to show + // that the indentation/margin has been moved. + // This needs to be respected by the DefaultTab at the zero position. + if( IsFirstTextLine() && HasNegFirst() ) + bForced = true; + } + aInter.Intersection( aLine ); + if( !aInter.HasArea() ) + return; + + bool bFullLine = aLine.Left() == aInter.Left() && + aLine.Right() == aInter.Right(); + if (!bFullLine && bWordFlyWrap && !GetTextFrame()->IsInTab()) + { + // Word style: if there is minimal space remaining, then handle that similar to a full line + // and put the actual empty paragraph below the fly. + SwTwips nLimit = MINLAY; + if (bSplitFly) + { + // We wrap around a floating table, that has a larger minimal wrap distance. + nLimit = TEXT_MIN_SMALL; + } + + bFullLine = std::abs(aLine.Left() - aInter.Left()) < nLimit + && std::abs(aLine.Right() - aInter.Right()) < nLimit; + } + + // Although no text is left, we need to format another line, + // because also empty lines need to avoid a Fly with no wrapping. + if (bFullLine && rInf.GetIdx() == TextFrameIndex(rInf.GetText().getLength())) + { + rInf.SetNewLine( true ); + // We know that for dummies, it holds ascent == height + m_pCurr->SetDummy(true); + } + + // aInter becomes frame-local + aInter.Pos().AdjustX( -nLeftMar ); + SwFlyPortion *pFly = new SwFlyPortion( aInter ); + if( bForced ) + { + m_pCurr->SetForcedLeftMargin(); + rInf.ForcedLeftMargin( o3tl::narrowing<sal_uInt16>(aInter.Width()) ); + } + + if( bFullLine ) + { + // In order to properly flow around Flys with different + // wrapping attributes, we need to increase by units of line height. + // The last avoiding line should be adjusted in height, so that + // we don't get a frame spacing effect. + // It is important that ascent == height, because the FlyPortion + // values are transferred to pCurr in CalcLine and IsDummy() relies + // on this behaviour. + // To my knowledge we only have two places where DummyLines can be + // created: here and in MakeFlyDummies. + // IsDummy() is evaluated in IsFirstTextLine(), when moving lines + // and in relation with DropCaps. + pFly->Height( aInter.Height() ); + + // nNextTop now contains the margin's bottom edge, which we avoid + // or the next margin's top edge, which we need to respect. + // That means we can comfortably grow up to this value; that's how + // we save a few empty lines. + tools::Long nNextTop = rTextFly.GetNextTop(); + if ( m_pFrame->IsVertical() ) + nNextTop = m_pFrame->SwitchVerticalToHorizontal( nNextTop ); + if( nNextTop > aInter.Bottom() ) + { + SwTwips nH = nNextTop - aInter.Top(); + if( nH < SAL_MAX_UINT16 ) + pFly->Height( nH ); + } + if( nAscent < pFly->Height() ) + pFly->SetAscent( nAscent ); + else + pFly->SetAscent( pFly->Height() ); + } + else + { + if (rInf.GetIdx() == TextFrameIndex(rInf.GetText().getLength())) + { + // Don't use nHeight, or we have a huge descent + pFly->Height( pLast->Height() ); + pFly->SetAscent( pLast->GetAscent() ); + } + else + { + pFly->Height( aInter.Height() ); + if( nAscent < pFly->Height() ) + pFly->SetAscent( nAscent ); + else + pFly->SetAscent( pFly->Height() ); + } + } + + rInf.SetFly( pFly ); + + if( pFly->GetFix() < rInf.Width() ) + rInf.Width( pFly->GetFix() ); + + SwTextGridItem const*const pGrid(GetGridItem(m_pFrame->FindPageFrame())); + if ( !pGrid ) + return; + + const SwPageFrame* pPageFrame = m_pFrame->FindPageFrame(); + const SwLayoutFrame* pBody = pPageFrame->FindBodyCont(); + + SwRectFnSet aRectFnSet(pPageFrame); + + const tools::Long nGridOrigin = pBody ? + aRectFnSet.GetPrtLeft(*pBody) : + aRectFnSet.GetPrtLeft(*pPageFrame); + + const SwDoc & rDoc = rInf.GetTextFrame()->GetDoc(); + const sal_uInt16 nGridWidth = GetGridWidth(*pGrid, rDoc); + + SwTwips nStartX = GetLeftMargin(); + if ( aRectFnSet.IsVert() ) + { + Point aPoint( nStartX, 0 ); + m_pFrame->SwitchHorizontalToVertical( aPoint ); + nStartX = aPoint.Y(); + } + + const SwTwips nOfst = nStartX - nGridOrigin; + const SwTwips nTmpWidth = rInf.Width() + nOfst; + + const SwTwips i = nTmpWidth / nGridWidth + 1; + + const SwTwips nNewWidth = ( i - 1 ) * nGridWidth - nOfst; + if ( nNewWidth > 0 ) + rInf.Width( nNewWidth ); + else + rInf.Width( 0 ); + + +} + +SwFlyCntPortion *SwTextFormatter::NewFlyCntPortion( SwTextFormatInfo &rInf, + SwTextAttr *pHint ) const +{ + const SwFrame *pFrame = m_pFrame; + + SwFlyInContentFrame *pFly; + SwFrameFormat* pFrameFormat = static_cast<SwTextFlyCnt*>(pHint)->GetFlyCnt().GetFrameFormat(); + if( RES_FLYFRMFMT == pFrameFormat->Which() ) + { + // set Lock pFrame to avoid m_pCurr getting deleted + TextFrameLockGuard aGuard(m_pFrame); + pFly = static_cast<SwTextFlyCnt*>(pHint)->GetFlyFrame(pFrame); + } + else + pFly = nullptr; + // aBase is the document-global position, from which the new extra portion is placed + // aBase.X() = Offset in the line after the current position + // aBase.Y() = LineIter.Y() + Ascent of the current position + + SwTwips nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc; + // i#11859 - use new method <SwLineLayout::MaxAscentDescent(..)> + // to change line spacing behaviour at paragraph - Compatibility to MS Word + //SwLinePortion *pPos = pCurr->GetFirstPortion(); + //lcl_MaxAscDescent( pPos, nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc ); + m_pCurr->MaxAscentDescent( nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc ); + + // If the ascent of the frame is larger than the ascent of the current position, + // we use this one when calculating the base, or the frame would be positioned + // too much to the top, sliding down after all causing a repaint in an area + // he actually never was in. + SwTwips nAscent = 0; + + const bool bTextFrameVertical = GetInfo().GetTextFrame()->IsVertical(); + + const bool bUseFlyAscent = pFly && pFly->isFrameAreaPositionValid() && + 0 != ( bTextFrameVertical ? + pFly->GetRefPoint().X() : + pFly->GetRefPoint().Y() ); + + if ( bUseFlyAscent ) + nAscent = std::abs( int( bTextFrameVertical ? + pFly->GetRelPos().X() : + pFly->GetRelPos().Y() ) ); + + // Check if be prefer to use the ascent of the last portion: + if ( IsQuick() || + !bUseFlyAscent || + nAscent < rInf.GetLast()->GetAscent() ) + { + nAscent = rInf.GetLast()->GetAscent(); + } + else if( nAscent > nFlyAsc ) + nFlyAsc = nAscent; + + Point aBase( GetLeftMargin() + rInf.X(), Y() + nAscent ); + AsCharFlags nMode = IsQuick() ? AsCharFlags::Quick : AsCharFlags::None; + if( GetMulti() && GetMulti()->HasRotation() ) + { + nMode |= AsCharFlags::Rotate; + if( GetMulti()->IsRevers() ) + nMode |= AsCharFlags::Reverse; + } + + Point aTmpBase( aBase ); + if ( GetInfo().GetTextFrame()->IsVertical() ) + GetInfo().GetTextFrame()->SwitchHorizontalToVertical( aTmpBase ); + + SwFlyCntPortion* pRet(nullptr); + if( pFly ) + { + pRet = sw::FlyContentPortion::Create(*GetInfo().GetTextFrame(), pFly, aTmpBase, nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc, nMode); + // We need to make sure that our font is set again in the OutputDevice + // It could be that the FlyInCnt was added anew and GetFlyFrame() would + // in turn cause, that it'd be created anew again. + // This one's frames get formatted right away, which change the font. + rInf.SelectFont(); + if( pRet->GetAscent() > nAscent ) + { + aBase.setY( Y() + pRet->GetAscent() ); + nMode |= AsCharFlags::UlSpace; + if( !rInf.IsTest() ) + { + aTmpBase = aBase; + if ( GetInfo().GetTextFrame()->IsVertical() ) + GetInfo().GetTextFrame()->SwitchHorizontalToVertical( aTmpBase ); + + pRet->SetBase( *rInf.GetTextFrame(), aTmpBase, nTmpAscent, + nTmpDescent, nFlyAsc, nFlyDesc, nMode ); + } + } + } + else + { + pRet = sw::DrawFlyCntPortion::Create(*rInf.GetTextFrame(), *pFrameFormat, aTmpBase, nTmpAscent, nTmpDescent, nFlyAsc, nFlyDesc, nMode); + } + return pRet; +} + +/* Drop portion is a special case, because it has parts which aren't portions + but we have handle them just like portions */ +void SwTextFormatter::MergeCharacterBorder( SwDropPortion const & rPortion ) +{ + if( rPortion.GetLines() <= 1 ) + return; + + SwDropPortionPart* pCurrPart = rPortion.GetPart(); + while( pCurrPart ) + { + if( pCurrPart->GetFollow() && + ::lcl_HasSameBorder(pCurrPart->GetFont(), pCurrPart->GetFollow()->GetFont()) ) + { + pCurrPart->SetJoinBorderWithNext(true); + pCurrPart->GetFollow()->SetJoinBorderWithPrev(true); + } + pCurrPart = pCurrPart->GetFollow(); + } +} + +void SwTextFormatter::MergeCharacterBorder( SwLinePortion& rPortion, SwLinePortion const *pPrev, SwTextFormatInfo& rInf ) +{ + const SwFont aCurFont = *rInf.GetFont(); + if( !aCurFont.HasBorder() ) + return; + + if (pPrev && pPrev->GetJoinBorderWithNext() ) + { + // In some case border merge is called twice to the portion + if( !rPortion.GetJoinBorderWithPrev() ) + { + rPortion.SetJoinBorderWithPrev(true); + if( rPortion.InTextGrp() && rPortion.Width() > aCurFont.GetLeftBorderSpace() ) + rPortion.Width(rPortion.Width() - aCurFont.GetLeftBorderSpace()); + } + } + else + { + rPortion.SetJoinBorderWithPrev(false); + m_pFirstOfBorderMerge = &rPortion; + } + + // Get next portion's font + bool bSeek = false; + if (!rInf.IsFull() && // Not the last portion of the line (in case of line break) + rInf.GetIdx() + rPortion.GetLen() != TextFrameIndex(rInf.GetText().getLength())) // Not the last portion of the paragraph + { + bSeek = Seek(rInf.GetIdx() + rPortion.GetLen()); + } + // Don't join the next portion if SwKernPortion sits between two different boxes. + bool bDisconnect = rPortion.IsKernPortion() && !rPortion.GetJoinBorderWithPrev(); + // If next portion has the same border then merge + if( bSeek && GetFnt()->HasBorder() && ::lcl_HasSameBorder(aCurFont, *GetFnt()) && !bDisconnect ) + { + // In some case border merge is called twice to the portion + if( !rPortion.GetJoinBorderWithNext() ) + { + rPortion.SetJoinBorderWithNext(true); + if( rPortion.InTextGrp() && rPortion.Width() > aCurFont.GetRightBorderSpace() ) + rPortion.Width(rPortion.Width() - aCurFont.GetRightBorderSpace()); + } + } + // If this is the last portion of the merge group then make the real height merge + else + { + rPortion.SetJoinBorderWithNext(false); + if( m_pFirstOfBorderMerge != &rPortion ) + { + // Calculate maximum height and ascent + SwLinePortion* pActPor = m_pFirstOfBorderMerge; + sal_uInt16 nMaxAscent = 0; + sal_uInt16 nMaxHeight = 0; + bool bReachCurrent = false; + while( pActPor ) + { + if( nMaxHeight < pActPor->Height() ) + nMaxHeight = pActPor->Height(); + if( nMaxAscent < pActPor->GetAscent() ) + nMaxAscent = pActPor->GetAscent(); + + pActPor = pActPor->GetNextPortion(); + if( !pActPor && !bReachCurrent ) + { + pActPor = &rPortion; + bReachCurrent = true; + } + } + + // Change all portion's height and ascent + pActPor = m_pFirstOfBorderMerge; + bReachCurrent = false; + while( pActPor ) + { + if( nMaxHeight > pActPor->Height() ) + pActPor->Height(nMaxHeight); + if( nMaxAscent > pActPor->GetAscent() ) + pActPor->SetAscent(nMaxAscent); + + pActPor = pActPor->GetNextPortion(); + if( !pActPor && !bReachCurrent ) + { + pActPor = &rPortion; + bReachCurrent = true; + } + } + m_pFirstOfBorderMerge = nullptr; + } + } + Seek(rInf.GetIdx()); +} + +namespace { + // calculates and sets optimal repaint offset for the current line + tools::Long lcl_CalcOptRepaint( SwTextFormatter &rThis, + SwLineLayout const &rCurr, + TextFrameIndex const nOldLineEnd, + const std::vector<tools::Long> &rFlyStarts ) + { + SwTextFormatInfo& txtFormatInfo = rThis.GetInfo(); + if ( txtFormatInfo.GetIdx() < txtFormatInfo.GetReformatStart() ) + // the reformat position is behind our new line, that means + // something of our text has moved to the next line + return 0; + + TextFrameIndex nReformat = std::min(txtFormatInfo.GetReformatStart(), nOldLineEnd); + + // in case we do not have any fly in our line, our repaint position + // is the changed position - 1 + if ( rFlyStarts.empty() && ! rCurr.IsFly() ) + { + // this is the maximum repaint offset determined during formatting + // for example: the beginning of the first right tab stop + // if this value is 0, this means that we do not have an upper + // limit for the repaint offset + const tools::Long nFormatRepaint = txtFormatInfo.GetPaintOfst(); + + if (nReformat < txtFormatInfo.GetLineStart() + TextFrameIndex(3)) + return 0; + + // step back two positions for smoother repaint + nReformat -= TextFrameIndex(2); + + // i#28795, i#34607, i#38388 + // step back more characters, this is required by complex scripts + // e.g., for Khmer (thank you, Javier!) + static const TextFrameIndex nMaxContext(10); + if (nReformat > txtFormatInfo.GetLineStart() + nMaxContext) + nReformat = nReformat - nMaxContext; + else + { + nReformat = txtFormatInfo.GetLineStart(); + //reset the margin flag - prevent loops + SwTextCursor::SetRightMargin(false); + } + + // Weird situation: Our line used to end with a hole portion + // and we delete some characters at the end of our line. We have + // to take care for repainting the blanks which are not anymore + // covered by the hole portion + while ( nReformat > txtFormatInfo.GetLineStart() && + CH_BLANK == txtFormatInfo.GetChar( nReformat ) ) + --nReformat; + + OSL_ENSURE( nReformat < txtFormatInfo.GetIdx(), "Reformat too small for me!" ); + SwRect aRect; + + // Note: GetChareRect is not const. It definitely changes the + // bMulti flag. We have to save and restore the old value. + bool bOldMulti = txtFormatInfo.IsMulti(); + rThis.GetCharRect( &aRect, nReformat ); + txtFormatInfo.SetMulti( bOldMulti ); + + return nFormatRepaint ? std::min( aRect.Left(), nFormatRepaint ) : + aRect.Left(); + } + else + { + // nReformat may be wrong, if something around flys has changed: + // we compare the former and the new fly positions in this line + // if anything has changed, we carefully have to adjust the right + // repaint position + tools::Long nPOfst = 0; + size_t nCnt = 0; + tools::Long nX = 0; + TextFrameIndex nIdx = rThis.GetInfo().GetLineStart(); + SwLinePortion* pPor = rCurr.GetFirstPortion(); + + while ( pPor ) + { + if ( pPor->IsFlyPortion() ) + { + // compare start of fly with former start of fly + if (nCnt < rFlyStarts.size() && + nX == rFlyStarts[ nCnt ] && + nIdx < nReformat + ) + // found fix position, nothing has changed left from nX + nPOfst = nX + pPor->Width(); + else + break; + + nCnt++; + } + nX = nX + pPor->Width(); + nIdx = nIdx + pPor->GetLen(); + pPor = pPor->GetNextPortion(); + } + + return nPOfst + rThis.GetLeftMargin(); + } + } + + // Determine if we need to build hidden portions + bool lcl_BuildHiddenPortion(const SwTextSizeInfo& rInf, TextFrameIndex & rPos) + { + // Only if hidden text should not be shown: + // if ( rInf.GetVsh() && rInf.GetVsh()->GetWin() && rInf.GetOpt().IsShowHiddenChar() ) + const bool bShowInDocView = rInf.GetVsh() && rInf.GetVsh()->GetWin() && rInf.GetOpt().IsShowHiddenChar(); + const bool bShowForPrinting = rInf.GetOpt().IsShowHiddenChar( true ) && rInf.GetOpt().IsPrinting(); + if (bShowInDocView || bShowForPrinting) + return false; + + const SwScriptInfo& rSI = rInf.GetParaPortion()->GetScriptInfo(); + TextFrameIndex nHiddenStart; + TextFrameIndex nHiddenEnd; + rSI.GetBoundsOfHiddenRange( rPos, nHiddenStart, nHiddenEnd ); + if ( nHiddenEnd ) + { + rPos = nHiddenEnd; + return true; + } + + return false; + } + + bool lcl_HasSameBorder(const SwFont& rFirst, const SwFont& rSecond) + { + return + rFirst.GetTopBorder() == rSecond.GetTopBorder() && + rFirst.GetBottomBorder() == rSecond.GetBottomBorder() && + rFirst.GetLeftBorder() == rSecond.GetLeftBorder() && + rFirst.GetRightBorder() == rSecond.GetRightBorder() && + rFirst.GetTopBorderDist() == rSecond.GetTopBorderDist() && + rFirst.GetBottomBorderDist() == rSecond.GetBottomBorderDist() && + rFirst.GetLeftBorderDist() == rSecond.GetLeftBorderDist() && + rFirst.GetRightBorderDist() == rSecond.GetRightBorderDist() && + rFirst.GetOrientation() == rSecond.GetOrientation() && + rFirst.GetShadowColor() == rSecond.GetShadowColor() && + rFirst.GetShadowWidth() == rSecond.GetShadowWidth() && + rFirst.GetShadowLocation() == rSecond.GetShadowLocation(); + } + +} //end unnamed namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itrform2.hxx b/sw/source/core/text/itrform2.hxx new file mode 100644 index 0000000000..2d71fad34c --- /dev/null +++ b/sw/source/core/text/itrform2.hxx @@ -0,0 +1,245 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include "itrpaint.hxx" + +class SwFlyCntPortion; +class SwDropPortion; +class SwFormatDrop; +class SwTextAttr; +class SwNumberPortion; +class SwErgoSumPortion; +class SwExpandPortion; +class SwMultiPortion; +class SwFootnotePortion; + +class SwTextFormatter : public SwTextPainter +{ + const SwFormatDrop *m_pDropFormat; + SwMultiPortion* m_pMulti; // during formatting a multi-portion + sal_uInt8 m_nContentEndHyph; // Counts consecutive hyphens at the line end + sal_uInt8 m_nContentMidHyph; // Counts consecutive hyphens before flies + TextFrameIndex m_nLeftScanIdx; // for increasing performance during + TextFrameIndex m_nRightScanIdx; // scanning for portion ends + bool m_bOnceMore : 1; // Another round? + bool m_bFlyInContentBase : 1; // Base reference that sets a character-bound frame + bool m_bTruncLines : 1; // Flag for extending the repaint rect, if needed + bool m_bUnclipped : 1; // Flag whether repaint is larger than the fixed line height + std::unique_ptr<sw::MergedAttrIterByEnd> m_pByEndIter; // HACK for TryNewNoLengthPortion + SwLinePortion* m_pFirstOfBorderMerge; // The first text portion of a joined border (during portion building) + + SwLinePortion *NewPortion(SwTextFormatInfo &rInf, ::std::optional<TextFrameIndex>); + SwTextPortion *NewTextPortion( SwTextFormatInfo &rInf ); + SwLinePortion *NewExtraPortion( SwTextFormatInfo &rInf ); + SwTabPortion *NewTabPortion( SwTextFormatInfo &rInf, bool bAuto ) const; + SwNumberPortion *NewNumberPortion( SwTextFormatInfo &rInf ) const; + SwDropPortion *NewDropPortion( SwTextFormatInfo &rInf ); + SwNumberPortion *NewFootnoteNumPortion( SwTextFormatInfo const &rInf ) const; + SwErgoSumPortion *NewErgoSumPortion( SwTextFormatInfo const &rInf ) const; + SwExpandPortion *NewFieldPortion( SwTextFormatInfo &rInf, + const SwTextAttr *pHt ) const; + SwFootnotePortion *NewFootnotePortion( SwTextFormatInfo &rInf, SwTextAttr *pHt ); + + /** + Sets a new portion for an object anchored as character + */ + SwFlyCntPortion *NewFlyCntPortion( SwTextFormatInfo &rInf, + SwTextAttr *pHt ) const; + SwLinePortion *WhichFirstPortion( SwTextFormatInfo &rInf ); + SwTextPortion *WhichTextPor( SwTextFormatInfo &rInf ) const; + SwExpandPortion * TryNewNoLengthPortion( SwTextFormatInfo const & rInfo ); + + // The center piece of formatting + void BuildPortions( SwTextFormatInfo &rInf ); + + bool BuildMultiPortion( SwTextFormatInfo &rInf, SwMultiPortion& rMulti ); + + /** + Calculation of the emulated right side. + + Determines the next object, that reaches into the rest of the line and + constructs the appropriate FlyPortion. + SwTextFly::GetFrame(const SwRect&, bool) will be needed for this. + + The right edge can be shortened by flys + */ + void CalcFlyWidth( SwTextFormatInfo &rInf ); + + // Is overloaded by SwTextFormatter because of UpdatePos + void CalcAdjustLine( SwLineLayout *pCurr ); + + // considers line spacing attributes + void CalcRealHeight( bool bNewLine = false ); + + // Transfers the data to rInf + void FeedInf( SwTextFormatInfo &rInf ) const; + + // Treats underflow situations + SwLinePortion *Underflow( SwTextFormatInfo &rInf ); + + // Calculates the ascent and the height from the fontmetric + void CalcAscent( SwTextFormatInfo &rInf, SwLinePortion *pPor ); + + // determines, if an optimized repaint rectangle is allowed + bool AllowRepaintOpt() const; + + // Is called by FormatLine + void FormatReset( SwTextFormatInfo &rInf ); + + /** + The position of the portions changes with the adjustment. + + This method updates the reference point of the anchored as character objects, + for example after adjustment change (right alignment, justified, etc.) + Mainly to correct the X position. + */ + void UpdatePos(SwLineLayout *pCurr, Point aStart, TextFrameIndex nStartIdx, + bool bAlways = false ) const; + + /** + Set all anchored as character objects to the passed BaseLine + (in Y direction). + */ + void AlignFlyInCntBase( tools::Long nBaseLine ) const; + + /** + This is called after the real height of the line has been calculated + Therefore it is possible, that more flys from below intersect with the + line, or that flys from above do not intersect with the line anymore. + We check this and return true, meaning that the line has to be + formatted again. + */ + bool ChkFlyUnderflow( SwTextFormatInfo &rInf ) const; + + // Insert portion + void InsertPortion( SwTextFormatInfo &rInf, SwLinePortion *pPor ); + + // Guess height for the DropPortion + void GuessDropHeight( const sal_uInt16 nLines ); + +public: + // Calculate the height for the DropPortion + void CalcDropHeight( const sal_uInt16 nLines ); + + // Calculates the paragraphs bottom, takes anchored objects within it into + // account which have a wrap setting of "wrap at 1st paragraph" + SwTwips CalcBottomLine() const; + + // Takes character-bound objects into account when calculating the + // repaint rect in lines with fixed line height + void CalcUnclipped( SwTwips& rTop, SwTwips& rBottom ); + + // Amongst others for DropCaps + bool CalcOnceMore(); + + void CtorInitTextFormatter( SwTextFrame *pFrame, SwTextFormatInfo *pInf ); + SwTextFormatter(SwTextFrame *pTextFrame, SwTextFormatInfo *pTextFormatInf) + : SwTextPainter(pTextFrame->GetTextNodeFirst()) + , m_bUnclipped(false) + { + CtorInitTextFormatter( pTextFrame, pTextFormatInf ); + } + virtual ~SwTextFormatter() override; + + TextFrameIndex FormatLine(TextFrameIndex nStart); + + void RecalcRealHeight(); + + // We format a line for interactive hyphenation + bool Hyphenate(SwInterHyphInfoTextFrame & rInf); + + // A special method for QuoVadis texts: + // nErgo is the page number of the ErgoSum Footnote + // At 0 it's still unclear + TextFrameIndex FormatQuoVadis(TextFrameIndex nStart); + + // The emergency break: Cancel formatting, discard line + bool IsStop() const { return GetInfo().IsStop(); } + + // The counterpart: Continue formatting at all costs + bool IsNewLine() const { return GetInfo().IsNewLine(); } + + // FormatQuick(); Refresh formatting information + bool IsQuick() const { return GetInfo().IsQuick(); } + + // Create a SwLineLayout if needed, which avoids Footnote/Fly to oscillate + void MakeDummyLine(); + + // SwTextIter functionality + void Insert( SwLineLayout *pLine ); + + // The remaining height to the page border + sal_uInt16 GetFrameRstHeight() const; + + // How wide would you be without any bounds (Flys etc.)? + SwTwips CalcFitToContent_( ); + + SwLinePortion* MakeRestPortion(const SwLineLayout* pLine, TextFrameIndex nPos); + + const SwFormatDrop *GetDropFormat() const { return m_pDropFormat; } + void ClearDropFormat() { m_pDropFormat = nullptr; } + + SwMultiPortion *GetMulti() const { return m_pMulti; } + + bool IsOnceMore() const { return m_bOnceMore; } + void SetOnceMore( bool bNew ) { m_bOnceMore = bNew; } + + bool HasTruncLines() const { return m_bTruncLines; } + void SetTruncLines( bool bNew ) { m_bTruncLines = bNew; } + + bool IsUnclipped() const { return m_bUnclipped; } + void SetUnclipped( bool bNew ) { m_bUnclipped = bNew; } + + bool IsFlyInCntBase() const { return m_bFlyInContentBase; } + void SetFlyInCntBase( bool bNew = true ) { m_bFlyInContentBase = bNew; } + + SwTextFormatInfo &GetInfo() + { return static_cast<SwTextFormatInfo&>(SwTextIter::GetInfo()); } + const SwTextFormatInfo &GetInfo() const + { return static_cast<const SwTextFormatInfo&>(SwTextIter::GetInfo()); } + + void InitCntHyph() { CntHyphens( m_nContentEndHyph, m_nContentMidHyph ); } + const sal_uInt8 &CntEndHyph() const { return m_nContentEndHyph; } + const sal_uInt8 &CntMidHyph() const { return m_nContentMidHyph; } + sal_uInt8 &CntEndHyph() { return m_nContentEndHyph; } + sal_uInt8 &CntMidHyph() { return m_nContentMidHyph; } + + /** + * Merge border of the drop portion with modifying the font of + * the portions' part. Removing left or right border. + * @param rPortion drop portion for merge + **/ + static void MergeCharacterBorder( SwDropPortion const & rPortion ); + + /** + * Merge border of the line portion with setting the portion's + * m_bJoinBorderWidthNext and m_bJoinBorderWidthPrev members and + * changing the size (width, height and ascent) of the portion + * to get a merged border. + * @param rPortion portion for merge + * @param pPrev portion immediately before rPortion + * @param rInf contain information + **/ + void MergeCharacterBorder( SwLinePortion& rPortion, SwLinePortion const *pPrev, SwTextFormatInfo& rInf ); + + bool ClearIfIsFirstOfBorderMerge(SwLinePortion const *pPortion); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itrpaint.cxx b/sw/source/core/text/itrpaint.cxx new file mode 100644 index 0000000000..5b6bb1288d --- /dev/null +++ b/sw/source/core/text/itrpaint.cxx @@ -0,0 +1,807 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <hintids.hxx> +#include <viewopt.hxx> +#include <tools/multisel.hxx> +#include <editeng/udlnitem.hxx> +#include <pagefrm.hxx> +#include <tgrditem.hxx> + +#include <EnhancedPDFExportHelper.hxx> +#include <IDocumentSettingAccess.hxx> + +#include <viewsh.hxx> +#include "itrpaint.hxx" +#include <txtfrm.hxx> +#include <swfont.hxx> +#include "txtpaint.hxx" +#include "porfld.hxx" +#include "portab.hxx" +#include <txatbase.hxx> +#include <charfmt.hxx> +#include "redlnitr.hxx" +#include "porrst.hxx" +#include "pormulti.hxx" +#include <doc.hxx> + +// Returns, if we have an underline breaking situation +// Adding some more conditions here means you also have to change them +// in SwTextPainter::CheckSpecialUnderline +bool IsUnderlineBreak( const SwLinePortion& rPor, const SwFont& rFnt ) +{ + return LINESTYLE_NONE == rFnt.GetUnderline() || + rPor.IsFlyPortion() || rPor.IsFlyCntPortion() || + rPor.IsBreakPortion() || rPor.IsMarginPortion() || + rPor.IsHolePortion() || + ( rPor.IsMultiPortion() && ! static_cast<const SwMultiPortion&>(rPor).IsBidi() ) || + rFnt.GetEscapement() < 0 || rFnt.IsWordLineMode() || + SvxCaseMap::SmallCaps == rFnt.GetCaseMap(); +} + +static Color GetUnderColor( const SwFont *pFont ) +{ + return pFont->GetUnderColor() == COL_AUTO ? + pFont->GetColor() : pFont->GetUnderColor(); +} + +void SwTextPainter::CtorInitTextPainter( SwTextFrame *pNewFrame, SwTextPaintInfo *pNewInf ) +{ + CtorInitTextCursor( pNewFrame, pNewInf ); + m_pInf = pNewInf; + SwFont *pMyFnt = GetFnt(); + GetInfo().SetFont( pMyFnt ); + m_bPaintDrop = false; +} + +SwLinePortion *SwTextPainter::CalcPaintOfst(const SwRect &rPaint, bool& rbSkippedNumPortions) +{ + SwLinePortion *pPor = m_pCurr->GetFirstPortion(); + GetInfo().SetPaintOfst( 0 ); + SwTwips nPaintOfst = rPaint.Left(); + + // nPaintOfst was exactly set to the end, therefore <= + // nPaintOfst is document global, therefore add up nLeftMar + // const sal_uInt16 nLeftMar = sal_uInt16(GetLeftMargin()); + // 8310: paint of LineBreaks in empty lines. + if( nPaintOfst && m_pCurr->Width() ) + { + SwLinePortion *pLast = nullptr; + // 7529 and 4757: not <= nPaintOfst + while( pPor && GetInfo().X() + pPor->Width() + (pPor->Height()/2) + < nPaintOfst ) + { + if( pPor->InSpaceGrp() && GetInfo().GetSpaceAdd() ) + { + tools::Long nTmp = GetInfo().X() +pPor->Width() + + pPor->CalcSpacing( GetInfo().GetSpaceAdd(), GetInfo() ); + if( nTmp + (pPor->Height()/2) >= nPaintOfst ) + break; + GetInfo().X( nTmp ); + GetInfo().SetIdx( GetInfo().GetIdx() + pPor->GetLen() ); + } + else + pPor->Move( GetInfo() ); + if (pPor->InNumberGrp() + && !static_cast<SwNumberPortion const*>(pPor)->HasFollow()) + { + rbSkippedNumPortions = true; // all numbering portions were skipped? + } + pLast = pPor; + pPor = pPor->GetNextPortion(); + } + + // 7529: if PostIts return also pLast. + if( pLast && !pLast->Width() && pLast->IsPostItsPortion() ) + { + pPor = pLast; + GetInfo().SetIdx( GetInfo().GetIdx() - pPor->GetLen() ); + } + } + return pPor; +} + +// There are two possibilities to output transparent font: +// 1) DrawRect on the whole line and DrawText afterwards +// (objectively fast, subjectively slow) +// 2) For every portion a DrawRect with subsequent DrawText is done +// (objectively slow, subjectively fast) +// Since the user usually judges subjectively the second method is set as default. +void SwTextPainter::DrawTextLine( const SwRect &rPaint, SwSaveClip &rClip, + const bool bUnderSized, + ::std::optional<SwTaggedPDFHelper> & roTaggedLabel, + ::std::optional<SwTaggedPDFHelper> & roTaggedParagraph, + bool const isPDFTaggingEnabled) +{ +#if OSL_DEBUG_LEVEL > 1 +// sal_uInt16 nFntHeight = GetInfo().GetFont()->GetHeight( GetInfo().GetVsh(), GetInfo().GetOut() ); +// sal_uInt16 nFntAscent = GetInfo().GetFont()->GetAscent( GetInfo().GetVsh(), GetInfo().GetOut() ); +#endif + + // maybe catch-up adjustment + GetAdjusted(); + AddExtraBlankWidth(); + GetInfo().SetpSpaceAdd( m_pCurr->GetpLLSpaceAdd() ); + GetInfo().ResetSpaceIdx(); + GetInfo().SetKanaComp( m_pCurr->GetpKanaComp() ); + GetInfo().ResetKanaIdx(); + // The size of the frame + GetInfo().SetIdx( GetStart() ); + GetInfo().SetPos( GetTopLeft() ); + + const bool bDrawInWindow = GetInfo().OnWin(); + + // 6882: blank lines can't be optimized by removing them if Formatting Marks are shown + const bool bEndPor = GetInfo().GetOpt().IsParagraph() && GetInfo().GetText().isEmpty(); + + bool bSkippedNumPortions(false); + SwLinePortion *pPor = bEndPor ? m_pCurr->GetFirstPortion() : CalcPaintOfst(rPaint, bSkippedNumPortions); + + if (bSkippedNumPortions // ugly but hard to check earlier in PaintSwFrame: + && !GetInfo().GetTextFrame()->GetTextNodeForParaProps()->IsOutline()) + { // there is a num portion but it is outside of the frame area and not painted + assert(!roTaggedLabel); + assert(!roTaggedParagraph); + Frame_Info aFrameInfo(*m_pFrame, false); // open LBody + roTaggedParagraph.emplace(nullptr, &aFrameInfo, nullptr, *GetInfo().GetOut()); + } + + SwTaggedPDFHelper::EndCurrentLink(*GetInfo().GetOut()); + + // Optimization! + SwTwips nMaxRight = std::min<SwTwips>( rPaint.Right(), Right() ); + const SwTwips nTmpLeft = GetInfo().X(); + //compatibility settings: allow tabstop text to exceed right margin + const auto& iDSA = GetInfo().GetTextFrame()->GetDoc().getIDocumentSettingAccess(); + const bool bTabOverMargin = iDSA.get(DocumentSettingId::TAB_OVER_MARGIN); + const bool bTabOverSpacing = iDSA.get(DocumentSettingId::TAB_OVER_SPACING); + if (bTabOverMargin || bTabOverSpacing) + { + SwLinePortion* pPorIter = pPor; + while( pPorIter ) + { + if( pPorIter->InTabGrp() ) + { + const SwTabPortion* pTabPor = static_cast<SwTabPortion*>(pPorIter); + const SwTwips nTabPos = nTmpLeft + pTabPor->GetTabPos(); + if( nMaxRight < nTabPos ) + { + nMaxRight = rPaint.Right(); + break; + } + } + pPorIter = pPorIter->GetNextPortion(); + } + } + if( !bEndPor && nTmpLeft >= nMaxRight ) + return; + + // DropCaps! + // 7538: of course for the printer, too + if( !m_bPaintDrop ) + { + // 8084: Optimization, less painting + // AMA: By 8084 7538 has been revived + // bDrawInWindow removed, so that DropCaps also can be printed + m_bPaintDrop = pPor == m_pCurr->GetFirstPortion() + && GetDropLines() >= GetLineNr(); + } + + SwTwips nTmpHeight, nTmpAscent; + CalcAscentAndHeight( nTmpAscent, nTmpHeight ); + + // bClip decides if there's a need to clip + // The whole thing must be done before retouching + + bool bClip = ( bDrawInWindow || bUnderSized ) && !rClip.IsChg(); + if( bClip && pPor ) + { + // If TopLeft or BottomLeft of the line are outside, the we must clip. + // The check for Right() is done in the output loop ... + + if( GetInfo().GetPos().X() < rPaint.Left() || + GetInfo().GetPos().Y() < rPaint.Top() || + GetInfo().GetPos().Y() + nTmpHeight > rPaint.Top() + rPaint.Height() ) + { + bClip = false; + rClip.ChgClip( rPaint, m_pFrame, m_pCurr->HasUnderscore() ); + } +#if OSL_DEBUG_LEVEL > 1 + static bool bClipAlways = false; + if( bClip && bClipAlways ) + { bClip = false; + rClip.ChgClip( rPaint ); + } +#endif + } + + // Alignment + OutputDevice* pOut = GetInfo().GetOut(); + Point aPnt1( nTmpLeft, GetInfo().GetPos().Y() ); + if ( aPnt1.X() < rPaint.Left() ) + aPnt1.setX( rPaint.Left() ); + if ( aPnt1.Y() < rPaint.Top() ) + aPnt1.setY( rPaint.Top() ); + Point aPnt2( GetInfo().GetPos().X() + nMaxRight - GetInfo().X(), + GetInfo().GetPos().Y() + nTmpHeight ); + if ( aPnt2.X() > rPaint.Right() ) + aPnt2.setX( rPaint.Right() ); + if ( aPnt2.Y() > rPaint.Bottom() ) + aPnt2.setY( rPaint.Bottom() ); + + const SwRect aLineRect( aPnt1, aPnt2 ); + + if( m_pCurr->IsClipping() ) + { + const SwTextFrame& rFrame = *GetInfo().GetTextFrame(); + // tdf#117448 at small fixed line height, enlarge clipping area in table cells + // to show previously clipped text content on the area of paragraph margins + if ( rFrame.IsInTab() ) + rClip.ChgClip( aLineRect, m_pFrame, false, rFrame.GetTopMargin(), rFrame.GetBottomMargin() ); + else + rClip.ChgClip( aLineRect, m_pFrame ); + bClip = false; + } + + if( !pPor && !bEndPor ) + return; + + // Baseline output also if non-TextPortion (compare TabPor with Fill) + // if no special vertical alignment is used, + // we calculate Y value for the whole line + SwTextGridItem const*const pGrid(GetGridItem(GetTextFrame()->FindPageFrame())); + const bool bAdjustBaseLine = + GetLineInfo().HasSpecialAlign( GetTextFrame()->IsVertical() ) || + ( nullptr != pGrid ) || m_pCurr->GetHangingBaseline(); + const SwTwips nLineBaseLine = GetInfo().GetPos().Y() + nTmpAscent; + if ( ! bAdjustBaseLine ) + GetInfo().Y( nLineBaseLine ); + + // 7529: Pre-paint post-its + if( GetInfo().OnWin() && pPor && !pPor->Width() ) + { + SeekAndChg( GetInfo() ); + + if( bAdjustBaseLine ) + { + const SwTwips nOldY = GetInfo().Y(); + + GetInfo().Y( GetInfo().GetPos().Y() + AdjustBaseLine( *m_pCurr, nullptr, + GetInfo().GetFont()->GetHeight( GetInfo().GetVsh(), *pOut ), + GetInfo().GetFont()->GetAscent( GetInfo().GetVsh(), *pOut ) + ) ); + + pPor->PrePaint( GetInfo(), pPor ); + GetInfo().Y( nOldY ); + } + else + pPor->PrePaint( GetInfo(), pPor ); + } + + // 7923: EndPortions output chars, too, that's why we change the font + if( bEndPor ) + SeekStartAndChg( GetInfo() ); + + const bool bRest = m_pCurr->IsRest(); + bool bFirst = true; + + SwArrowPortion *pArrow = nullptr; + // Reference portion for the paragraph end portion + SwLinePortion* pEndTempl = m_pCurr->GetFirstPortion(); + + while( pPor ) + { + bool bSeeked = true; + GetInfo().SetLen( pPor->GetLen() ); + + const SwTwips nOldY = GetInfo().Y(); + + if ( bAdjustBaseLine ) + { + GetInfo().Y( GetInfo().GetPos().Y() + AdjustBaseLine( *m_pCurr, pPor ) ); + + // we store the last portion, because a possible paragraph + // end character has the same font as this portion + // (only in special vertical alignment case, otherwise the first + // portion of the line is used) + if ( pPor->Width() && pPor->InTextGrp() ) + pEndTempl = pPor; + } + + // set redlining for line break symbol + if ( pPor->IsBreakPortion() && GetInfo().GetOpt().IsParagraph() && GetRedln() ) + { + SeekAndChg( GetInfo() ); + if ( m_pCurr->GetRedlineEndType() != RedlineType::None ) + static_cast<SwBreakPortion&>(*pPor).SetRedline( m_pCurr->GetRedlineEndType() ); + } + + // A special case are GluePortions which output blanks. + + // 6168: Avoid that the rest of a FieldPortion gets the attributes of the + // next portion with SeekAndChgBefore(): + if( bRest && pPor->InFieldGrp() && !pPor->GetLen() ) + SeekAndChgBefore( GetInfo() ); + else if ( pPor->IsQuoVadisPortion() ) + { + // A remark on QuoVadis/ErgoSum: + // We use the Font set for the Paragraph for these portions. + // Thus, we initialize: + TextFrameIndex nOffset = GetInfo().GetIdx(); + SeekStartAndChg( GetInfo(), true ); + if( GetRedln() && m_pCurr->HasRedline() ) + { + std::pair<SwTextNode const*, sal_Int32> const pos( + GetTextFrame()->MapViewToModel(nOffset)); + GetRedln()->Seek(*m_pFont, pos.first->GetIndex(), pos.second, 0); + } + } + else if( pPor->InTextGrp() || pPor->InFieldGrp() || pPor->InTabGrp() ) + SeekAndChg( GetInfo() ); + else if ( !bFirst && pPor->IsBreakPortion() && GetInfo().GetOpt().IsParagraph() ) + { + // Paragraph symbols should have the same font as the paragraph in front of them, + // except for the case that there's redlining in the paragraph + if( GetRedln() ) + SeekAndChg( GetInfo() ); + else + SeekAndChgBefore( GetInfo() ); + } + else + bSeeked = false; + + // bRest = false; + + // If the end of the portion juts out, it is clipped. + // A safety distance of half the height is added, so that + // TTF-"f" isn't overlapping into the page margin. + if( bClip && + GetInfo().X() + pPor->Width() + ( pPor->Height() / 2 ) > nMaxRight ) + { + bClip = false; + rClip.ChgClip( rPaint, m_pFrame, m_pCurr->HasUnderscore() ); + } + + // Portions, which lay "below" the text like post-its + SwLinePortion *pNext = pPor->GetNextPortion(); + if( GetInfo().OnWin() && pNext && !pNext->Width() ) + { + // Fix 11289: Fields were omitted here because of Last!=Owner during + // loading Brief.sdw. Now the fields are allowed again, + // by bSeeked Last!=Owner is being avoided. + if ( !bSeeked ) + SeekAndChg( GetInfo() ); + pNext->PrePaint( GetInfo(), pPor ); + } + + // We calculate a separate font for underlining. + CheckSpecialUnderline( pPor, bAdjustBaseLine ? nOldY : 0 ); + SwUnderlineFont* pUnderLineFnt = GetInfo().GetUnderFnt(); + if ( pUnderLineFnt ) + { + const Point aTmpPoint( GetInfo().X(), + bAdjustBaseLine ? + pUnderLineFnt->GetPos().Y() : + nLineBaseLine ); + pUnderLineFnt->SetPos( aTmpPoint ); + } + + // in extended input mode we do not want a common underline font. + SwUnderlineFont* pOldUnderLineFnt = nullptr; + if ( GetRedln() && GetRedln()->ExtOn() ) + { + pOldUnderLineFnt = GetInfo().GetUnderFnt(); + GetInfo().SetUnderFnt( nullptr ); + } + + // multiple numbering portions are possible :( + if ((pPor->InNumberGrp() // also footnote label + // weird special case, bullet with soft hyphen + || (pPor->InHyphGrp() && pNext && pNext->InNumberGrp())) + && !GetInfo().GetTextFrame()->GetTextNodeForParaProps()->IsOutline() + && !roTaggedLabel) // note: CalcPaintOfst may skip some portions + { + assert(isPDFTaggingEnabled); + Por_Info aPorInfo(*pPor, *this, 1); // open Lbl + roTaggedLabel.emplace(nullptr, nullptr, &aPorInfo, *pOut); + } + + { + // #i16816# tagged pdf support + Por_Info aPorInfo(*pPor, *this, 0); + SwTaggedPDFHelper aTaggedPDFHelper( nullptr, nullptr, &aPorInfo, *pOut ); + + if( pPor->IsMultiPortion() ) + PaintMultiPortion( rPaint, static_cast<SwMultiPortion&>(*pPor) ); + else + pPor->Paint( GetInfo() ); + } + + // 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 + && !static_cast<SwNumberPortion const*>(pPor)->HasFollow()) // so wait for the last one + { + if (!GetInfo().GetTextFrame()->GetTextNodeForParaProps()->IsOutline()) + { + assert(roTaggedLabel); + roTaggedLabel.reset(); // close Lbl + assert(!roTaggedParagraph); + Frame_Info aFrameInfo(*m_pFrame, false); // open LBody + roTaggedParagraph.emplace(nullptr, &aFrameInfo, nullptr, *pOut); + } + else + { + assert(!roTaggedLabel); + } + } + + // reset underline font + if ( pOldUnderLineFnt ) + GetInfo().SetUnderFnt( pOldUnderLineFnt ); + + // reset (for special vertical alignment) + GetInfo().Y( nOldY ); + + bFirst &= !pPor->GetLen(); + if( pNext || !pPor->IsMarginPortion() ) + pPor->Move( GetInfo() ); + if( pPor->IsArrowPortion() && GetInfo().OnWin() && !pArrow ) + pArrow = static_cast<SwArrowPortion*>(pPor); + + pPor = bDrawInWindow || GetInfo().X() <= nMaxRight || + // #i16816# tagged pdf support + ( GetInfo().GetVsh() && + GetInfo().GetVsh()->GetViewOptions()->IsPDFExport() && + pNext && pNext->IsHolePortion() ) ? + pNext : + nullptr; + if (!pPor && isPDFTaggingEnabled && (roTaggedLabel || !roTaggedParagraph)) + { // check if the end of the list label is off-screen + auto FindEndOfNumbering = [&](SwLinePortion const* pP) { + while (pP) + { + if (pP->InNumberGrp() + && !static_cast<SwNumberPortion const*>(pP)->HasFollow()) + { + if (roTaggedLabel) + { + roTaggedLabel.reset(); + } // else, if the numbering isn't visible at all, no Lbl + if (!GetInfo().GetTextFrame()->GetTextNodeForParaProps()->IsOutline()) + { + Frame_Info aFrameInfo(*m_pFrame, false); // open LBody + roTaggedParagraph.emplace(nullptr, &aFrameInfo, nullptr, *GetInfo().GetOut()); + } + return true; + } + pP = pP->GetNextPortion(); + } + return false; + }; + if (!FindEndOfNumbering(pNext)) // check rest of current line + { + // check lines that will be cut off + if (rPaint.Bottom() < Y() + GetLineHeight()) + { + for (SwLineLayout const* pLine = GetNext(); pLine; pLine = pLine->GetNext()) + { + if (FindEndOfNumbering(pLine->GetFirstPortion())) + { + break; + } + } + } + } + } + } + + // delete underline font + delete GetInfo().GetUnderFnt(); + GetInfo().SetUnderFnt( nullptr ); + + // paint remaining stuff + if( bDrawInWindow ) + { + // If special vertical alignment is enabled, GetInfo().Y() is the + // top of the current line. Therefore is has to be adjusted for + // the painting of the remaining stuff. We first store the old value. + const SwTwips nOldY = GetInfo().Y(); + + if( !GetNextLine() && + GetInfo().GetVsh() && !GetInfo().GetVsh()->IsPreview() && + GetInfo().GetOpt().IsParagraph() && !GetTextFrame()->GetFollow() && + GetInfo().GetIdx() >= TextFrameIndex(GetInfo().GetText().getLength())) + { + bool bHasRedlineEnd( GetRedln() && m_pCurr->HasRedlineEnd() ); + RedlineType eRedlineEnd = bHasRedlineEnd ? m_pCurr->GetRedlineEndType() : RedlineType::None; + if( bHasRedlineEnd ) + { + TextFrameIndex nOffset = GetInfo().GetIdx(); + SeekStartAndChg( GetInfo(), true ); + std::pair<SwTextNode const*, sal_Int32> const pos( + GetTextFrame()->MapViewToModel(nOffset)); + GetRedln()->Seek(*m_pFont, pos.first->GetIndex(), pos.second, 0); + } + const SwTmpEndPortion aEnd( *pEndTempl, + bHasRedlineEnd && eRedlineEnd != RedlineType::Delete ? m_pFont->GetUnderline() : LINESTYLE_NONE, + bHasRedlineEnd && eRedlineEnd == RedlineType::Delete ? m_pFont->GetStrikeout() : STRIKEOUT_NONE, + bHasRedlineEnd ? m_pFont->GetColor() : COL_AUTO ); + GetFnt()->ChgPhysFnt( GetInfo().GetVsh(), *pOut ); + + if ( bAdjustBaseLine ) + GetInfo().Y( GetInfo().GetPos().Y() + + AdjustBaseLine( *m_pCurr, &aEnd ) ); + GetInfo().X( GetInfo().X() + + ( GetCurr()->IsHanging() ? GetCurr()->GetHangingMargin() : 0 ) ); + aEnd.Paint( GetInfo() ); + GetInfo().Y( nOldY ); + } + if( GetInfo().GetVsh() && !GetInfo().GetVsh()->IsPreview() ) + { + const bool bNextUndersized = + ( GetTextFrame()->GetNext() && + 0 == GetTextFrame()->GetNext()->getFramePrintArea().Height() && + GetTextFrame()->GetNext()->IsTextFrame() && + static_cast<SwTextFrame*>(GetTextFrame()->GetNext())->IsUndersized() ) ; + + if( bUnderSized || bNextUndersized ) + { + if ( bAdjustBaseLine ) + GetInfo().Y( GetInfo().GetPos().Y() + m_pCurr->GetAscent() ); + + // Left arrow (text overflowing) + if( pArrow ) + GetInfo().DrawRedArrow( *pArrow ); + + // GetInfo().Y() must be current baseline + SwTwips nDiff = GetInfo().Y() + nTmpHeight - nTmpAscent - GetTextFrame()->getFrameArea().Bottom(); + if( ( nDiff > 0 && + (GetEnd() < TextFrameIndex(GetInfo().GetText().getLength()) || + ( nDiff > nTmpHeight/2 && GetPrevLine() ) ) ) || + (nDiff >= 0 && bNextUndersized) ) + + { + // Right arrow (text overflowing) + SwArrowPortion aArrow( GetInfo() ); + GetInfo().DrawRedArrow( aArrow ); + } + + GetInfo().Y( nOldY ); + } + } + } + + if( m_pCurr->IsClipping() ) + rClip.ChgClip( rPaint, m_pFrame ); +} + +void SwTextPainter::CheckSpecialUnderline( const SwLinePortion* pPor, + tools::Long nAdjustBaseLine ) +{ + // Check if common underline should not be continued + if ( IsUnderlineBreak( *pPor, *m_pFont ) ) + { + // delete underline font + delete GetInfo().GetUnderFnt(); + GetInfo().SetUnderFnt( nullptr ); + return; + } + // Reuse calculated underline font as much as possible. + if (GetInfo().GetUnderFnt() && + GetInfo().GetIdx() + pPor->GetLen() <= GetInfo().GetUnderFnt()->GetEnd() + TextFrameIndex(1)) + { + SwFont &rFont = GetInfo().GetUnderFnt()->GetFont(); + const Color aColor = GetUnderColor( GetInfo().GetFont() ); + if ( GetUnderColor( &rFont ) != aColor ) + rFont.SetColor( aColor ); + return; + } + + // If current underline matches the common underline font, we continue + // to use the common underline font. + // Bug 120769:Color of underline display wrongly + if ( GetInfo().GetUnderFnt() && + GetInfo().GetUnderFnt()->GetFont().GetUnderline() == GetFnt()->GetUnderline() && + GetInfo().GetFont() && GetInfo().GetFont()->GetUnderColor() != COL_AUTO ) + return; + //Bug 120769(End) + + OSL_ENSURE( GetFnt() && LINESTYLE_NONE != GetFnt()->GetUnderline(), + "CheckSpecialUnderline without underlined font" ); + MultiSelection aUnderMulti( Range( 0, GetInfo().GetText().getLength() ) ); + const SwFont* pParaFnt = GetAttrHandler().GetFont(); + if( pParaFnt && pParaFnt->GetUnderline() == GetFnt()->GetUnderline() ) + aUnderMulti.SelectAll(); + + if (sw::MergedPara const*const pMerged = GetTextFrame()->GetMergedPara()) + { + // first, add the paragraph properties to MultiSelection - if there are + // Hints too, they will override the positions if they're added later + sal_Int32 nTmp(0); + for (auto const& e : pMerged->extents) + { + if (const SvxUnderlineItem* pItem = e.pNode->GetSwAttrSet().GetItemIfSet( + RES_CHRATR_UNDERLINE)) + { + const bool bUnderSelect(m_pFont->GetUnderline() == + pItem->GetLineStyle()); + aUnderMulti.Select(Range(nTmp, nTmp + e.nEnd - e.nStart - 1), + bUnderSelect); + } + nTmp += e.nEnd - e.nStart; + } + } + + SwTextNode const* pNode(nullptr); + sw::MergedAttrIter iter(*GetTextFrame()); + for (SwTextAttr const* pTextAttr = iter.NextAttr(&pNode); pTextAttr; + pTextAttr = iter.NextAttr(&pNode)) + { + SvxUnderlineItem const*const pItem = + CharFormat::GetItem(*pTextAttr, RES_CHRATR_UNDERLINE); + + if (pItem) + { + TextFrameIndex const nStart( + GetTextFrame()->MapModelToView(pNode, pTextAttr->GetStart())); + TextFrameIndex const nEnd( + GetTextFrame()->MapModelToView(pNode, *pTextAttr->End())); + if (nEnd > nStart) + { + const bool bUnderSelect = m_pFont->GetUnderline() == pItem->GetLineStyle(); + aUnderMulti.Select(Range(sal_Int32(nStart), sal_Int32(nEnd) - 1), + bUnderSelect); + } + } + } + + const TextFrameIndex nIndx = GetInfo().GetIdx(); + TextFrameIndex nUnderEnd(0); + const size_t nCnt = aUnderMulti.GetRangeCount(); + + // find the underline range the current portion is contained in + for( size_t i = 0; i < nCnt; ++i ) + { + const Range& rRange = aUnderMulti.GetRange( i ); + if (nUnderEnd == TextFrameIndex(rRange.Min())) + nUnderEnd = TextFrameIndex(rRange.Max()); + else if (nIndx >= TextFrameIndex(rRange.Min())) + { + nUnderEnd = TextFrameIndex(rRange.Max()); + } + else + break; + } + + if ( GetEnd() && GetEnd() <= nUnderEnd ) + nUnderEnd = GetEnd() - TextFrameIndex(1); + + // calculate the new common underline font + SwFont* pUnderlineFnt = nullptr; + Point aCommonBaseLine; + + // check, if underlining is not isolated + if (nIndx + GetInfo().GetLen() < nUnderEnd + TextFrameIndex(1)) + { + // here starts the algorithm for calculating the underline font + SwScriptInfo& rScriptInfo = GetInfo().GetParaPortion()->GetScriptInfo(); + SwAttrIter aIter(*GetInfo().GetTextFrame()->GetTextNodeFirst(), + rScriptInfo, GetTextFrame()); + + TextFrameIndex nTmpIdx = nIndx; + sal_uLong nSumWidth = 0; + sal_uLong nSumHeight = 0; + sal_uLong nBold = 0; + sal_uInt16 nMaxBaseLineOfst = 0; + int nNumberOfPortions = 0; + + while (nTmpIdx <= nUnderEnd && pPor) + { + if ( pPor->IsFlyPortion() || pPor->IsFlyCntPortion() || + pPor->IsBreakPortion() || pPor->IsMarginPortion() || + pPor->IsHolePortion() || + ( pPor->IsMultiPortion() && ! static_cast<const SwMultiPortion*>(pPor)->IsBidi() ) ) + break; + + aIter.Seek( nTmpIdx ); + if ( aIter.GetFnt()->GetEscapement() < 0 || m_pFont->IsWordLineMode() || + SvxCaseMap::SmallCaps == m_pFont->GetCaseMap() ) + break; + + if ( !aIter.GetFnt()->GetEscapement() ) + { + nSumWidth += pPor->Width(); + const sal_uLong nFontHeight = aIter.GetFnt()->GetHeight(); + + // If we do not have a common baseline we take the baseline + // and the font of the lowest portion. + if ( nAdjustBaseLine ) + { + const sal_uInt16 nTmpBaseLineOfst = AdjustBaseLine( *m_pCurr, pPor ); + if ( nMaxBaseLineOfst < nTmpBaseLineOfst ) + { + nMaxBaseLineOfst = nTmpBaseLineOfst; + nSumHeight = nFontHeight; + } + } + // in horizontal layout we build a weighted sum of the heights + else + nSumHeight += pPor->Width() * nFontHeight; + + if ( WEIGHT_NORMAL != aIter.GetFnt()->GetWeight() ) + nBold += pPor->Width(); + } + + ++nNumberOfPortions; + + nTmpIdx += pPor->GetLen(); + pPor = pPor->GetNextPortion(); + } + + // resulting height + if ( nNumberOfPortions > 1 && nSumWidth ) + { + const sal_uLong nNewFontHeight = nAdjustBaseLine ? + nSumHeight : + nSumHeight / nSumWidth; + + pUnderlineFnt = new SwFont( *GetInfo().GetFont() ); + + // font height + const SwFontScript nActual = pUnderlineFnt->GetActual(); + pUnderlineFnt->SetSize( Size( pUnderlineFnt->GetSize( nActual ).Width(), + nNewFontHeight ), nActual ); + + // font weight + if ( 2 * nBold > nSumWidth ) + pUnderlineFnt->SetWeight( WEIGHT_BOLD, nActual ); + else + pUnderlineFnt->SetWeight( WEIGHT_NORMAL, nActual ); + + // common base line + aCommonBaseLine.setY( nAdjustBaseLine + nMaxBaseLineOfst ); + } + } + + // an escaped redlined portion should also have a special underlining + if( ! pUnderlineFnt && m_pFont->GetEscapement() > 0 && GetRedln() && + GetRedln()->ChkSpecialUnderline() ) + pUnderlineFnt = new SwFont( *m_pFont ); + + delete GetInfo().GetUnderFnt(); + + if ( pUnderlineFnt ) + { + pUnderlineFnt->SetProportion( 100 ); + pUnderlineFnt->SetEscapement( 0 ); + pUnderlineFnt->SetStrikeout( STRIKEOUT_NONE ); + pUnderlineFnt->SetOverline( LINESTYLE_NONE ); + const Color aFillColor( COL_TRANSPARENT ); + pUnderlineFnt->SetFillColor( aFillColor ); + + GetInfo().SetUnderFnt( new SwUnderlineFont( *pUnderlineFnt, nUnderEnd, + aCommonBaseLine ) ); + } + else + // I'm sorry, we do not have a special underlining font for you. + GetInfo().SetUnderFnt( nullptr ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itrpaint.hxx b/sw/source/core/text/itrpaint.hxx new file mode 100644 index 0000000000..893db371db --- /dev/null +++ b/sw/source/core/text/itrpaint.hxx @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include "itrtxt.hxx" + +#include <optional> + +class SwSaveClip; // SwTextPainter +class SwMultiPortion; +class SwTaggedPDFHelper; + +class SwTextPainter : public SwTextCursor +{ + bool m_bPaintDrop; + + SwLinePortion *CalcPaintOfst(const SwRect &rPaint, bool& rbSkippedNumPortions); + void CheckSpecialUnderline( const SwLinePortion* pPor, + tools::Long nAdjustBaseLine = 0 ); +protected: + void CtorInitTextPainter( SwTextFrame *pFrame, SwTextPaintInfo *pInf ); + explicit SwTextPainter(SwTextNode const * pTextNode) + : SwTextCursor(pTextNode) + , m_bPaintDrop(false) + { + } + +public: + SwTextPainter(SwTextFrame *pTextFrame, SwTextPaintInfo *pTextPaintInf) + : SwTextCursor(pTextFrame->GetTextNodeFirst()) + { + CtorInitTextPainter( pTextFrame, pTextPaintInf ); + } + void DrawTextLine( const SwRect &rPaint, SwSaveClip &rClip, + const bool bUnderSz, + ::std::optional<SwTaggedPDFHelper> & roTaggedLabel, + ::std::optional<SwTaggedPDFHelper> & roTaggedParagraph, + bool isPDFTaggingEnabled); + void PaintDropPortion(); + // if PaintMultiPortion is called recursively, we have to pass the + // surrounding SwBidiPortion + void PaintMultiPortion( const SwRect &rPaint, SwMultiPortion& rMulti, + const SwMultiPortion* pEnvPor = nullptr ); + void SetPaintDrop( const bool bNew ) { m_bPaintDrop = bNew; } + bool IsPaintDrop() const { return m_bPaintDrop; } + SwTextPaintInfo &GetInfo() + { return static_cast<SwTextPaintInfo&>(SwTextIter::GetInfo()); } + const SwTextPaintInfo &GetInfo() const + { return static_cast<const SwTextPaintInfo&>(SwTextIter::GetInfo()); } +}; + +bool IsUnderlineBreak( const SwLinePortion& rPor, const SwFont& rFnt ); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itrtxt.cxx b/sw/source/core/text/itrtxt.cxx new file mode 100644 index 0000000000..1d1eed3e08 --- /dev/null +++ b/sw/source/core/text/itrtxt.cxx @@ -0,0 +1,465 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <ndtxt.hxx> +#include <txatbase.hxx> +#include <paratr.hxx> +#include <vcl/outdev.hxx> +#include <editeng/paravertalignitem.hxx> + +#include "pormulti.hxx" +#include <pagefrm.hxx> +#include <tgrditem.hxx> +#include "porfld.hxx" +#include "porrst.hxx" + +#include "itrtxt.hxx" +#include <txtfrm.hxx> + +void SwTextIter::CtorInitTextIter( SwTextFrame *pNewFrame, SwTextInfo *pNewInf ) +{ + assert(pNewFrame->GetPara()); + + CtorInitAttrIter( *pNewFrame->GetTextNodeFirst(), pNewFrame->GetPara()->GetScriptInfo(), pNewFrame ); + + SwTextNode const*const pNode = pNewFrame->GetTextNodeForParaProps(); + + m_pFrame = pNewFrame; + m_pInf = pNewInf; + m_aLineInf.CtorInitLineInfo( pNode->GetSwAttrSet(), *pNode ); + m_nFrameStart = m_pFrame->getFrameArea().Pos().Y() + m_pFrame->getFramePrintArea().Pos().Y(); + SwTextIter::Init(); + + // Order is important: only execute FillRegister if GetValue!=0 + m_bRegisterOn = pNode->GetSwAttrSet().GetRegister().GetValue() + && m_pFrame->FillRegister( m_nRegStart, m_nRegDiff ); +} + +void SwTextIter::Init() +{ + m_pCurr = m_pInf->GetParaPortion(); + m_nStart = m_pInf->GetTextStart(); + m_nY = m_nFrameStart; + m_bPrev = true; + m_pPrev = nullptr; + m_nLineNr = 1; +} + +void SwTextIter::CalcAscentAndHeight( SwTwips &rAscent, SwTwips &rHeight ) const +{ + rHeight = GetLineHeight(); + rAscent = m_pCurr->GetAscent() + rHeight - m_pCurr->Height(); +} + +SwLineLayout *SwTextIter::GetPrev_() +{ + m_pPrev = nullptr; + m_bPrev = true; + SwLineLayout *pLay = m_pInf->GetParaPortion(); + if( m_pCurr == pLay ) + return nullptr; + while( pLay->GetNext() != m_pCurr ) + pLay = pLay->GetNext(); + m_pPrev = pLay; + return m_pPrev; +} + +const SwLineLayout *SwTextIter::GetPrev() +{ + if(! m_bPrev) + GetPrev_(); + return m_pPrev; +} + +const SwLineLayout *SwTextIter::Prev() +{ + if( !m_bPrev ) + GetPrev_(); + if( m_pPrev ) + { + m_bPrev = false; + m_pCurr = m_pPrev; + m_nStart = m_nStart - m_pCurr->GetLen(); + m_nY = m_nY - GetLineHeight(); + if( !m_pCurr->IsDummy() && !(--m_nLineNr) ) + ++m_nLineNr; + return m_pCurr; + } + else + return nullptr; +} + +const SwLineLayout *SwTextIter::Next() +{ + if(m_pCurr->GetNext()) + { + m_pPrev = m_pCurr; + m_bPrev = true; + m_nStart = m_nStart + m_pCurr->GetLen(); + m_nY += GetLineHeight(); + if( m_pCurr->GetLen() || ( m_nLineNr>1 && !m_pCurr->IsDummy() ) ) + ++m_nLineNr; + m_pCurr = m_pCurr->GetNext(); + return m_pCurr; + } + else + return nullptr; +} + +const SwLineLayout *SwTextIter::NextLine() +{ + const SwLineLayout *pNext = Next(); + while( pNext && pNext->IsDummy() && pNext->GetNext() ) + { + pNext = Next(); + } + return pNext; +} + +const SwLineLayout *SwTextIter::GetNextLine() const +{ + const SwLineLayout *pNext = m_pCurr->GetNext(); + while( pNext && pNext->IsDummy() && pNext->GetNext() ) + { + pNext = pNext->GetNext(); + } + return pNext; +} + +const SwLineLayout *SwTextIter::GetPrevLine() +{ + const SwLineLayout *pRoot = m_pInf->GetParaPortion(); + if( pRoot == m_pCurr ) + return nullptr; + const SwLineLayout *pLay = pRoot; + + while( pLay->GetNext() != m_pCurr ) + pLay = pLay->GetNext(); + + if( pLay->IsDummy() ) + { + const SwLineLayout *pTmp = pRoot; + pLay = pRoot->IsDummy() ? nullptr : pRoot; + while( pTmp->GetNext() != m_pCurr ) + { + if( !pTmp->IsDummy() ) + pLay = pTmp; + pTmp = pTmp->GetNext(); + } + } + + // If nothing has changed, then there are only dummy's + return pLay; +} + +const SwLineLayout *SwTextIter::PrevLine() +{ + const SwLineLayout *pMyPrev = Prev(); + if( !pMyPrev ) + return nullptr; + + const SwLineLayout *pLast = pMyPrev; + while( pMyPrev && pMyPrev->IsDummy() ) + { + pLast = pMyPrev; + pMyPrev = Prev(); + } + return pMyPrev ? pMyPrev : pLast; +} + +void SwTextIter::Bottom() +{ + while( Next() ) + { + // nothing + } +} + +void SwTextIter::CharToLine(TextFrameIndex const nChar) +{ + while( m_nStart + m_pCurr->GetLen() <= nChar && Next() ) + ; + while( m_nStart > nChar && Prev() ) + ; +} + +// 1170: takes into account ambiguities: +const SwLineLayout *SwTextCursor::CharCursorToLine(TextFrameIndex const nPosition) +{ + CharToLine( nPosition ); + if( nPosition != m_nStart ) + s_bRightMargin = false; + bool bPrevious = s_bRightMargin && m_pCurr->GetLen() && GetPrev() && + GetPrev()->GetLen(); + if (bPrevious && nPosition && CH_BREAK == GetInfo().GetChar(nPosition - TextFrameIndex(1))) + bPrevious = false; + return bPrevious ? PrevLine() : m_pCurr; +} + +SwTwips SwTextCursor::AdjustBaseLine( const SwLineLayout& rLine, + const SwLinePortion* pPor, + SwTwips nPorHeight, SwTwips nPorAscent, + const bool bAutoToCentered ) const +{ + if ( pPor ) + { + nPorHeight = pPor->Height(); + nPorAscent = pPor->GetAscent(); + } + + SwTwips nOfst = rLine.GetRealHeight() - rLine.Height(); + + SwTextGridItem const*const pGrid(GetGridItem(m_pFrame->FindPageFrame())); + + if ( pGrid && GetInfo().SnapToGrid() && pGrid->IsSquaredMode() ) + { + const sal_uInt16 nRubyHeight = pGrid->GetRubyHeight(); + const bool bRubyTop = ! pGrid->GetRubyTextBelow(); + + if ( GetInfo().IsMulti() ) + // we are inside the GetCharRect recursion for multi portions + // we center the portion in its surrounding line + nOfst = ( m_pCurr->Height() - nPorHeight ) / 2 + nPorAscent; + else + { + // We have to take care for ruby portions. + // The ruby portion is NOT centered + nOfst = nOfst + nPorAscent; + + if ( ! pPor || ! pPor->IsMultiPortion() || + ! static_cast<const SwMultiPortion*>(pPor)->IsRuby() ) + { + // Portions which are bigger than on grid distance are + // centered inside the whole line. + + //for text refactor + const sal_uInt16 nLineNet = rLine.Height() - nRubyHeight; + //const sal_uInt16 nLineNet = ( nPorHeight > nGridWidth ) ? + // rLine.Height() - nRubyHeight : + // nGridWidth; + nOfst += ( nLineNet - nPorHeight ) / 2; + if ( bRubyTop ) + nOfst += nRubyHeight; + } + } + } + else + { + switch ( GetLineInfo().GetVertAlign() ) { + case SvxParaVertAlignItem::Align::Top : + nOfst = nOfst + nPorAscent; + break; + case SvxParaVertAlignItem::Align::Center : + OSL_ENSURE( rLine.Height() >= nPorHeight, "Portion height > Line height"); + nOfst += ( rLine.Height() - nPorHeight ) / 2 + nPorAscent; + break; + case SvxParaVertAlignItem::Align::Bottom : + nOfst += rLine.Height() - nPorHeight + nPorAscent; + break; + case SvxParaVertAlignItem::Align::Automatic : + if ( bAutoToCentered || GetInfo().GetTextFrame()->IsVertical() ) + { + // Vertical text has these cases to calculate the baseline: + // - Implicitly TB and RL: the origo is the top right corner, offset is the + // ascent. + // - (Implicitly TB and) LR: the origo is the top left corner, offset is the + // descent. + // - BT and LR: the origo is the bottom left corner, offset is the ascent. + if (GetInfo().GetTextFrame()->IsVertLR() && !GetInfo().GetTextFrame()->IsVertLRBT()) + nOfst += rLine.Height() - ( rLine.Height() - nPorHeight ) / 2 - nPorAscent; + else + { + SwTwips nLineHeight = 0; + bool bHadClearingBreak = false; + if (GetInfo().GetTextFrame()->IsVertical()) + { + // Ignore the height of clearing break portions in the automatic + // alignment case. + const SwLinePortion* pLinePor = rLine.GetFirstPortion(); + while (pLinePor) + { + bool bClearingBreak = false; + if (pLinePor->IsBreakPortion()) + { + auto pBreakPortion = static_cast<const SwBreakPortion*>(pLinePor); + bClearingBreak = pBreakPortion->GetClear() != SwLineBreakClear::NONE; + if (bClearingBreak) + { + bHadClearingBreak = true; + } + } + if (!bClearingBreak && pLinePor->Height() > nLineHeight) + { + nLineHeight = pLinePor->Height(); + } + pLinePor = pLinePor->GetNextPortion(); + } + } + + if (!bHadClearingBreak) + { + nLineHeight = rLine.Height(); + } + + nOfst += ( nLineHeight - nPorHeight ) / 2 + nPorAscent; + } + break; + } + [[fallthrough]]; + case SvxParaVertAlignItem::Align::Baseline : + // base line + if (pPor && pPor->GetHangingBaseline()) + { + nOfst += rLine.GetAscent() // Romn baseline of the line. + - rLine.GetHangingBaseline() // Hanging baseline of the line. + + pPor->GetHangingBaseline(); // Romn baseline of the portion. + } + else + nOfst = nOfst + rLine.GetAscent(); + break; + } + } + + return nOfst; +} + +void SwTextIter::TwipsToLine( const SwTwips y) +{ + while( m_nY + GetLineHeight() <= y && Next() ) + ; + while( m_nY > y && Prev() ) + ; +} + +// Local helper function to check, if pCurr needs a field rest portion: +static bool lcl_NeedsFieldRest( const SwLineLayout* pCurr ) +{ + const SwLinePortion *pPor = pCurr->GetNextPortion(); + bool bRet = false; + while( pPor && !bRet ) + { + bRet = pPor->InFieldGrp() && static_cast<const SwFieldPortion*>(pPor)->HasFollow(); + if( !pPor->GetNextPortion() || !pPor->GetNextPortion()->InFieldGrp() ) + break; + pPor = pPor->GetNextPortion(); + } + return bRet; +} + +void SwTextIter::TruncLines( bool bNoteFollow ) +{ + SwLineLayout *pDel = m_pCurr->GetNext(); + TextFrameIndex const nEnd = m_nStart + m_pCurr->GetLen(); + + if( pDel ) + { + m_pCurr->SetNext( nullptr ); + if (MaybeHasHints() && bNoteFollow) + { + GetInfo().GetParaPortion()->SetFollowField( pDel->IsRest() || + lcl_NeedsFieldRest( m_pCurr ) ); + + // bug 88534: wrong positioning of flys + SwTextFrame* pFollow = GetTextFrame()->GetFollow(); + if ( pFollow && ! pFollow->IsLocked() && + nEnd == pFollow->GetOffset() ) + { + TextFrameIndex nRangeEnd = nEnd; + SwLineLayout* pLine = pDel; + + // determine range to be searched for flys anchored as characters + while ( pLine ) + { + nRangeEnd = nRangeEnd + pLine->GetLen(); + pLine = pLine->GetNext(); + } + + // examine hints in range nEnd - (nEnd + nRangeChar) + SwTextNode const* pNode(nullptr); + sw::MergedAttrIter iter(*GetTextFrame()); + for (SwTextAttr const* pHt = iter.NextAttr(&pNode); pHt; pHt = iter.NextAttr(&pNode)) + { + if( RES_TXTATR_FLYCNT == pHt->Which() ) + { + // check if hint is in our range + TextFrameIndex const nTmpPos( + GetTextFrame()->MapModelToView(pNode, pHt->GetStart())); + if ( nEnd <= nTmpPos && nTmpPos < nRangeEnd ) + pFollow->InvalidateRange_( + SwCharRange( nTmpPos, nTmpPos ) ); + } + } + } + } + delete pDel; + } + if( m_pCurr->IsDummy() && + !m_pCurr->GetLen() && + m_nStart < TextFrameIndex(GetTextFrame()->GetText().getLength())) + { + m_pCurr->SetRealHeight( 1 ); + } + if (MaybeHasHints()) + m_pFrame->RemoveFootnote( nEnd ); +} + +void SwTextIter::CntHyphens( sal_uInt8 &nEndCnt, sal_uInt8 &nMidCnt) const +{ + nEndCnt = 0; + nMidCnt = 0; + if ( m_bPrev && m_pPrev && !m_pPrev->IsEndHyph() && !m_pPrev->IsMidHyph() ) + return; + SwLineLayout *pLay = m_pInf->GetParaPortion(); + if( m_pCurr == pLay ) + return; + while( pLay != m_pCurr ) + { + if ( pLay->IsEndHyph() ) + nEndCnt++; + else + nEndCnt = 0; + if ( pLay->IsMidHyph() ) + nMidCnt++; + else + nMidCnt = 0; + pLay = pLay->GetNext(); + } +} + +// Change current output device to formatting device, this has to be done before +// formatting. +SwHookOut::SwHookOut( SwTextSizeInfo& rInfo ) : + pInf( &rInfo ), + pOut( rInfo.GetOut() ), + bOnWin( rInfo.OnWin() ) +{ + OSL_ENSURE( rInfo.GetRefDev(), "No reference device for text formatting" ); + + // set new values + rInfo.SetOut( rInfo.GetRefDev() ); + rInfo.SetOnWin( false ); +} + +SwHookOut::~SwHookOut() +{ + pInf->SetOut( pOut ); + pInf->SetOnWin( bOnWin ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itrtxt.hxx b/sw/source/core/text/itrtxt.hxx new file mode 100644 index 0000000000..8db063d616 --- /dev/null +++ b/sw/source/core/text/itrtxt.hxx @@ -0,0 +1,341 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <swtypes.hxx> +#include "itratr.hxx" +#include "inftxt.hxx" + +struct SwPosition; +struct SwCursorMoveState; +class SwMarginPortion; +class SwFlyPortion; + +class SwTextIter : public SwAttrIter +{ +protected: + SwLineInfo m_aLineInf; + SwTextFrame *m_pFrame; + SwTextInfo *m_pInf; + SwLineLayout *m_pCurr; + SwLineLayout *m_pPrev; + SwTwips m_nFrameStart; + SwTwips m_nY; + SwTwips m_nRegStart; // The register's start position (Y) + TextFrameIndex m_nStart; // Start in the text string, end = pCurr->GetLen() + sal_uInt16 m_nRegDiff; // Register's line distance + sal_Int32 m_nLineNr; // Line number + bool m_bPrev : 1; + bool m_bRegisterOn : 1; // Keep in register + bool m_bOneBlock : 1; // Justified text: Dispose single words + bool m_bLastBlock : 1; // Justified text: Also the last line + bool m_bLastCenter : 1; // Justified text: Center last line + + SwLineLayout *GetPrev_(); + + // Reset in the first line + void Init(); + void CtorInitTextIter( SwTextFrame *pFrame, SwTextInfo *pInf ); + explicit SwTextIter(SwTextNode const * pTextNode) + : SwAttrIter(pTextNode) + , m_pFrame(nullptr) + , m_pInf(nullptr) + , m_pCurr(nullptr) + , m_pPrev(nullptr) + , m_nFrameStart(0) + , m_nY(0) + , m_nRegStart(0) + , m_nStart(0) + , m_nRegDiff(0) + , m_nLineNr(0) + , m_bPrev(false) + , m_bRegisterOn(false) + , m_bOneBlock(false) + , m_bLastBlock(false) + , m_bLastCenter(false) + { + } +public: + SwTextIter(SwTextFrame *pTextFrame, SwTextInfo *pTextInf) + : SwAttrIter(pTextFrame->GetTextNodeFirst()) + , m_bOneBlock(false) + , m_bLastBlock(false) + , m_bLastCenter(false) + { + CtorInitTextIter(pTextFrame, pTextInf); + } + const SwLineLayout *GetCurr() const { return m_pCurr; } // NEVER 0! + const SwLineLayout *GetNext() const { return m_pCurr->GetNext(); } + const SwLineLayout *GetPrev(); + TextFrameIndex GetLength() const { return m_pCurr->GetLen(); } + sal_Int32 GetLineNr() const { return m_nLineNr; } + TextFrameIndex GetStart() const { return m_nStart; } + TextFrameIndex GetEnd() const { return GetStart() + GetLength(); } + SwTwips Y() const { return m_nY; } + + SwTwips RegStart() const { return m_nRegStart; } + sal_uInt16 RegDiff() const { return m_nRegDiff; } + bool IsRegisterOn() const { return m_bRegisterOn; } + + SwTextInfo &GetInfo() { return *m_pInf; } + const SwTextInfo &GetInfo() const { return *m_pInf; } + + void Top() { Init(); } + void Bottom(); + const SwLineLayout *Next(); + const SwLineLayout *Prev(); + + // Skips the FlyFrames dummy line + const SwLineLayout *NextLine(); + const SwLineLayout *PrevLine(); + const SwLineLayout *GetNextLine() const; + const SwLineLayout *GetPrevLine(); + + void CharToLine(TextFrameIndex); + void TwipsToLine(const SwTwips); + + // Truncates all after pCurr + void TruncLines( bool bNoteFollow = false ); + + SwTwips GetLineHeight() const { return m_pCurr->GetRealHeight(); } + void CalcAscentAndHeight( SwTwips &rAscent, SwTwips &rHeight ) const; + + // Lots of trouble for querying pCurr == pPara + bool IsFirstTextLine() const + { return m_nStart == GetInfo().GetTextStart() && + !( m_pCurr->IsDummy() && GetNextLine() ); } + + // Replacement for the old IsFirstLine() + bool IsParaLine() const + { return m_pCurr == m_pInf->GetParaPortion(); } + + const SwLineInfo &GetLineInfo() const { return m_aLineInf; } + SwTwips GetFirstPos() const { return m_nFrameStart; } + inline bool SeekAndChg( SwTextSizeInfo &rInf ); + inline bool SeekAndChgBefore( SwTextSizeInfo &rInf ); + inline bool SeekStartAndChg( SwTextSizeInfo &rInf, const bool bPara=false ); + + SwTextFrame *GetTextFrame() { return m_pFrame; } + const SwTextFrame *GetTextFrame() const { return m_pFrame; } + + // Counts consecutive hyphens in order to be within the boundary given by MaxHyphens + void CntHyphens( sal_uInt8 &nEndCnt, sal_uInt8 &nMidCnt) const; +}; + +class SwTextMargin : public SwTextIter +{ +private: + SwTwips mnLeft; + SwTwips mnRight; + SwTwips mnFirst; + sal_uInt16 mnDropLeft; + sal_uInt16 mnDropHeight; + sal_uInt16 mnDropDescent; + sal_uInt16 mnDropLines; + SvxAdjust mnAdjust; + // #i91133# + SwTwips mnTabLeft; + +protected: + // For FormatQuoVadis + void Right( const SwTwips nNew ) { mnRight = nNew; } + + void CtorInitTextMargin( SwTextFrame *pFrame, SwTextSizeInfo *pInf ); + explicit SwTextMargin(SwTextNode const * pTextNode) + : SwTextIter(pTextNode) + , mnLeft(0) + , mnRight(0) + , mnFirst(0) + , mnDropLeft(0) + , mnDropHeight(0) + , mnDropDescent(0) + , mnDropLines(0) + , mnAdjust(SvxAdjust::Left) + , mnTabLeft(0) + { + } +public: + SwTextMargin(SwTextFrame *pTextFrame, SwTextSizeInfo *pTextSizeInf) + : SwTextIter(pTextFrame->GetTextNodeFirst()) + { + CtorInitTextMargin( pTextFrame, pTextSizeInf ); + } + inline SwTwips GetLeftMargin() const; + inline SwTwips Left() const; + SwTwips Right() const { return mnRight; } + SwTwips FirstLeft() const { return mnFirst; } + SwTwips CurrWidth() const { return m_pCurr->PrtWidth(); } + SwTwips GetLineStart() const; + SwTwips GetLineEnd() const { return GetLineStart() + CurrWidth(); } + Point GetTopLeft() const { return Point( GetLineStart(), Y() ); } + bool IsOneBlock() const { return m_bOneBlock; } + bool IsLastBlock() const { return m_bLastBlock; } + bool IsLastCenter() const { return m_bLastCenter; } + SvxAdjust GetAdjust() const { return mnAdjust; } + sal_uInt16 GetLineWidth() const + { return sal_uInt16( Right() - GetLeftMargin() + 1 ); } + SwTwips GetLeftMin() const { return std::min(mnFirst, mnLeft); } + bool HasNegFirst() const { return mnFirst < mnLeft; } + + // #i91133# + SwTwips GetTabLeft() const + { + return mnTabLeft; + } + // DropCaps + sal_uInt16 GetDropLines() const { return mnDropLines; } + void SetDropLines( const sal_uInt16 nNew ) { mnDropLines = nNew; } + sal_uInt16 GetDropLeft() const { return mnDropLeft; } + sal_uInt16 GetDropHeight() const { return mnDropHeight; } + void SetDropHeight( const sal_uInt16 nNew ) { mnDropHeight = nNew; } + sal_uInt16 GetDropDescent() const { return mnDropDescent; } + void SetDropDescent( const sal_uInt16 nNew ) { mnDropDescent = nNew; } + void DropInit(); + + // Returns the TextPos for start and end of the current line without whitespace + // Implemented in frminf.cxx + TextFrameIndex GetTextStart() const; + TextFrameIndex GetTextEnd() const; + + SwTextSizeInfo &GetInfo() + { return static_cast<SwTextSizeInfo&>(SwTextIter::GetInfo()); } + const SwTextSizeInfo &GetInfo() const + { return static_cast<const SwTextSizeInfo&>(SwTextIter::GetInfo()); } + +}; + +class SwTextAdjuster : public SwTextMargin +{ + // Adjusts the portion, if we have adjustment and FlyFrames + void CalcFlyAdjust( SwLineLayout *pCurr ); + + // Calls SplitGlues and CalcBlockAdjust + void FormatBlock( ); + + // Creates the glue chain for short lines + SwMarginPortion* CalcRightMargin( SwLineLayout *pCurr, SwTwips nReal = 0 ); + + // Calculate the adjustment (FlyPortions) + SwFlyPortion *CalcFlyPortion( const tools::Long nRealWidth, + const SwRect &rCurrRect ); + +protected: + explicit SwTextAdjuster(SwTextNode const * pTextNode) : SwTextMargin(pTextNode) { } + // Creates the Glues for adjusted paragraphs + void CalcNewBlock( SwLineLayout *pCurr, const SwLinePortion *pStopAt, + SwTwips nReal = 0, bool bSkipKashida = false ); + SwTwips CalcKanaAdj( SwLineLayout *pCurr ); + +public: + // Is overloaded by SwTextFormatter due to UpdatePos + void CalcAdjLine( SwLineLayout *pCurr ); + + // For adjusting afterwards + void GetAdjusted() const + { + if( m_pCurr->IsFormatAdj() ) + const_cast<SwTextAdjuster*>(this)->CalcAdjLine( m_pCurr ); + } + + // Special treatment for DropCaps + void CalcDropAdjust(); + void CalcDropRepaint(); +}; + +class SwTextCursor : public SwTextAdjuster +{ + // A small helper-class to save SwTextCursor member, manipulate them + // and to restore them + friend class SwTextCursorSave; + + // Ambiguities + static bool s_bRightMargin; + void GetCharRect_(SwRect *, TextFrameIndex, SwCursorMoveState *); +protected: + void CtorInitTextCursor( SwTextFrame *pFrame, SwTextSizeInfo *pInf ); + explicit SwTextCursor(SwTextNode const * pTextNode) : SwTextAdjuster(pTextNode) { } + void AddExtraBlankWidth(); +public: + SwTextCursor( SwTextFrame *pTextFrame, SwTextSizeInfo *pTextSizeInf ) + : SwTextAdjuster(pTextFrame->GetTextNodeFirst()) + { + CtorInitTextCursor(pTextFrame, pTextSizeInf); + } + void GetCharRect(SwRect *, TextFrameIndex, SwCursorMoveState* = nullptr, + const tools::Long nMax = 0 ); + void GetEndCharRect(SwRect *, TextFrameIndex, SwCursorMoveState* = nullptr, + const tools::Long nMax = 0 ); + TextFrameIndex GetModelPositionForViewPoint( SwPosition *pPos, const Point &rPoint, + bool bChgNode, SwCursorMoveState* = nullptr ) const; + // Respects ambiguities: For the implementation see below + const SwLineLayout *CharCursorToLine(TextFrameIndex const nPos); + + // calculates baseline for portion rPor + // bAutoToCentered indicates, if AUTOMATIC mode means CENTERED or BASELINE + SwTwips AdjustBaseLine( const SwLineLayout& rLine, const SwLinePortion* pPor, + SwTwips nPorHeight = 0, SwTwips nAscent = 0, + const bool bAutoToCentered = false ) const; + + static void SetRightMargin( const bool bNew ){ s_bRightMargin = bNew; } + static bool IsRightMargin() { return s_bRightMargin; } +}; + +// Change current output device to printer, this has to be done before +// formatting. +class SwHookOut +{ + SwTextSizeInfo* pInf; + VclPtr<OutputDevice> pOut; + bool bOnWin; +public: + explicit SwHookOut( SwTextSizeInfo& rInfo ); + ~SwHookOut(); +}; + +inline bool SwTextIter::SeekAndChg( SwTextSizeInfo &rInf ) +{ + return SeekAndChgAttrIter( rInf.GetIdx(), rInf.GetOut() ); +} + +inline bool SwTextIter::SeekAndChgBefore( SwTextSizeInfo &rInf ) +{ + if ( rInf.GetIdx() ) + return SeekAndChgAttrIter(rInf.GetIdx() - TextFrameIndex(1), rInf.GetOut()); + else + return SeekAndChgAttrIter( rInf.GetIdx(), rInf.GetOut() ); +} + +inline bool SwTextIter::SeekStartAndChg( SwTextSizeInfo &rInf, const bool bPara ) +{ + return SeekStartAndChgAttrIter( rInf.GetOut(), bPara ); +} + +inline SwTwips SwTextMargin::GetLeftMargin() const +{ + return IsFirstTextLine() ? mnFirst : Left(); +} + +inline SwTwips SwTextMargin::Left() const +{ + return (mnDropLines >= m_nLineNr && 1 != m_nLineNr) ? mnFirst + mnDropLeft : mnLeft; +} + + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/noteurl.cxx b/sw/source/core/text/noteurl.cxx new file mode 100644 index 0000000000..fa91ea252d --- /dev/null +++ b/sw/source/core/text/noteurl.cxx @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <noteurl.hxx> + +// Global variable +SwNoteURL* pNoteURL = nullptr; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/pordrop.hxx b/sw/source/core/text/pordrop.hxx new file mode 100644 index 0000000000..50dd00e566 --- /dev/null +++ b/sw/source/core/text/pordrop.hxx @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include "portxt.hxx" +#include <swfont.hxx> + +#include <memory> + +class SwFont; + +// DropCap cache, global variable initialized/destroyed in txtinit.cxx +// and used in txtdrop.cxx for initial calculation + +class SwDropCapCache; +extern SwDropCapCache *pDropCapCache; + +// A drop portion can consist of one or more parts in order to allow +// attribute changes inside them. +class SwDropPortionPart +{ + std::unique_ptr<SwDropPortionPart> m_pFollow; + std::unique_ptr<SwFont> m_pFnt; + TextFrameIndex m_nLen; + sal_uInt16 m_nWidth; + bool m_bJoinBorderWithNext; + bool m_bJoinBorderWithPrev; + +public: + SwDropPortionPart( SwFont& rFont, const TextFrameIndex nL ) + : m_pFnt( &rFont ), m_nLen( nL ), m_nWidth( 0 ), m_bJoinBorderWithNext(false), m_bJoinBorderWithPrev(false) {}; + ~SwDropPortionPart(); + + SwDropPortionPart* GetFollow() const { return m_pFollow.get(); }; + void SetFollow( std::unique_ptr<SwDropPortionPart> pNew ) { m_pFollow = std::move(pNew); }; + SwFont& GetFont() const { return *m_pFnt; } + TextFrameIndex GetLen() const { return m_nLen; } + sal_uInt16 GetWidth() const { return m_nWidth; } + void SetWidth( sal_uInt16 nNew ) { m_nWidth = nNew; } + + bool GetJoinBorderWithPrev() const { return m_bJoinBorderWithPrev; } + bool GetJoinBorderWithNext() const { return m_bJoinBorderWithNext; } + void SetJoinBorderWithPrev( const bool bJoinPrev ) { m_bJoinBorderWithPrev = bJoinPrev; } + void SetJoinBorderWithNext( const bool bJoinNext ) { m_bJoinBorderWithNext = bJoinNext; } +}; + +/// Text portion for the Format -> Paragraph -> Drop Caps functionality. +class SwDropPortion : public SwTextPortion +{ + friend class SwDropCapCache; + std::unique_ptr<SwDropPortionPart> m_pPart; // due to script/attribute changes + sal_uInt16 m_nLines; // Line count + sal_uInt16 m_nDropHeight; // Height + sal_uInt16 m_nDropDescent; // Distance to the next line + sal_uInt16 m_nDistance; // Distance to the text + sal_uInt16 m_nFix; // Fixed position + short m_nY; // Y Offset + + bool FormatText( SwTextFormatInfo &rInf ); + void PaintText( const SwTextPaintInfo &rInf ) const; + +public: + SwDropPortion( const sal_uInt16 nLineCnt, + const sal_uInt16 nDropHeight, + const sal_uInt16 nDropDescent, + const sal_uInt16 nDistance ); + virtual ~SwDropPortion() override; + + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + void PaintDrop( const SwTextPaintInfo &rInf ) const; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual SwPosSize GetTextSize( const SwTextSizeInfo &rInfo ) const override; + virtual TextFrameIndex GetModelPositionForViewPoint(sal_uInt16 nOfst) const override; + + sal_uInt16 GetLines() const { return m_nLines; } + sal_uInt16 GetDistance() const { return m_nDistance; } + sal_uInt16 GetDropHeight() const { return m_nDropHeight; } + sal_uInt16 GetDropDescent() const { return m_nDropDescent; } + sal_uInt16 GetDropLeft() const { return Width() + m_nFix; } + + SwDropPortionPart* GetPart() const { return m_pPart.get(); } + void SetPart( std::unique_ptr<SwDropPortionPart> pNew ) { m_pPart = std::move(pNew); } + + void SetY( short nNew ) { m_nY = nNew; } + + SwFont* GetFnt() const { return m_pPart ? &m_pPart->GetFont() : nullptr; } + + static void DeleteDropCapCache(); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porexp.cxx b/sw/source/core/text/porexp.cxx new file mode 100644 index 0000000000..0884db6fce --- /dev/null +++ b/sw/source/core/text/porexp.cxx @@ -0,0 +1,317 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <viewopt.hxx> +#include <IDocumentSettingAccess.hxx> +#include <SwPortionHandler.hxx> +#include "inftxt.hxx" +#include "porexp.hxx" + +TextFrameIndex SwExpandPortion::GetModelPositionForViewPoint(const sal_uInt16 nOfst) const +{ return SwLinePortion::GetModelPositionForViewPoint( nOfst ); } + +bool SwExpandPortion::GetExpText( const SwTextSizeInfo&, OUString &rText ) const +{ + rText.clear(); + // Do not do: return 0 != rText.Len(); + // Reason being: empty fields replace CH_TXTATR with an empty string + return true; +} + +void SwExpandPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Special( GetLen(), OUString(), GetWhichPor() ); +} + +void SwExpandPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwExpandPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterEndElement(pWriter); +} + +SwPosSize SwExpandPortion::GetTextSize( const SwTextSizeInfo &rInf ) const +{ + SwTextSlot aDiffText( &rInf, this, false, false ); + return rInf.GetTextSize(); +} + +bool SwExpandPortion::Format( SwTextFormatInfo &rInf ) +{ + SwTextSlot aDiffText( &rInf, this, true, false ); + TextFrameIndex const nFullLen = rInf.GetLen(); + + // As odd as it may seem: the query for GetLen() must return + // false due to the ExpandPortions _after_ the aDiffText (see SoftHyphs) + // caused by the SetFull ... + if( !nFullLen ) + { + // Do not Init(), because we need height and ascent + Width(0); + return false; + } + return SwTextPortion::Format( rInf ); +} + +void SwExpandPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + rInf.DrawCSDFHighlighting(*this); // here it detects as CS and not DF + + SwTextSlot aDiffText( &rInf, this, true, true ); + const SwFont aOldFont = *rInf.GetFont(); + if( GetJoinBorderWithPrev() ) + const_cast<SwTextPaintInfo&>(rInf).GetFont()->SetLeftBorder(nullptr); + if( GetJoinBorderWithNext() ) + const_cast<SwTextPaintInfo&>(rInf).GetFont()->SetRightBorder(nullptr); +// rInf.DrawCSDFHighlighting(*this); // here it detects as DF and only the '/' is detected as CS + + rInf.DrawBackBrush( *this ); + rInf.DrawBorder( *this ); + + // Do we have to repaint a post it portion? + if( rInf.OnWin() && mpNextPortion && !mpNextPortion->Width() ) + mpNextPortion->PrePaint( rInf, this ); + + // The contents of field portions is not considered during the + // calculation of the directions. Therefore we let vcl handle + // the calculation by removing the BIDI_STRONG_FLAG temporarily. + SwLayoutModeModifier aLayoutModeModifier( *rInf.GetOut() ); + aLayoutModeModifier.SetAuto(); + + // ST2 + if ( rInf.GetSmartTags() || rInf.GetGrammarCheckList() ) + rInf.DrawMarkedText( *this, rInf.GetLen(), false, + nullptr != rInf.GetSmartTags(), nullptr != rInf.GetGrammarCheckList() ); + else + rInf.DrawText( *this, rInf.GetLen() ); + + if( GetJoinBorderWithPrev() || GetJoinBorderWithNext() ) + *const_cast<SwTextPaintInfo&>(rInf).GetFont() = aOldFont; +} + +SwLinePortion *SwBlankPortion::Compress() { return this; } + +/** + * If a Line is full of HardBlanks and overflows, we must not generate + * underflows! + * Causes problems with Fly + */ +sal_uInt16 SwBlankPortion::MayUnderflow( const SwTextFormatInfo &rInf, + TextFrameIndex const nIdx, bool bUnderflow) +{ + if( rInf.StopUnderflow() ) + return 0; + const SwLinePortion *pPos = rInf.GetRoot(); + if( pPos->GetNextPortion() ) + pPos = pPos->GetNextPortion(); + while( pPos && pPos->IsBlankPortion() ) + pPos = pPos->GetNextPortion(); + if( !pPos || !rInf.GetIdx() || ( !pPos->GetLen() && pPos == rInf.GetRoot() ) ) + return 0; // There are just BlankPortions left + + // If a Blank is preceding us, we do not need to trigger underflow + // If a Blank is succeeding us, we do not need to pass on the underflow + if (bUnderflow + && nIdx + TextFrameIndex(1) < TextFrameIndex(rInf.GetText().getLength()) + && CH_BLANK == rInf.GetText()[sal_Int32(nIdx) + 1]) + { + return 0; + } + if( nIdx && !const_cast<SwTextFormatInfo&>(rInf).GetFly() ) + { + while( pPos && !pPos->IsFlyPortion() ) + pPos = pPos->GetNextPortion(); + if( !pPos ) + { + // We check to see if there are useful line breaks, blanks or fields etc. left + // In case there still are some, no underflow + // If there are Flys, we still allow the underflow + TextFrameIndex nBlank = nIdx; + while( --nBlank > rInf.GetLineStart() ) + { + const sal_Unicode cCh = rInf.GetChar( nBlank ); + if( CH_BLANK == cCh || + (( CH_TXTATR_BREAKWORD == cCh || CH_TXTATR_INWORD == cCh ) + && rInf.HasHint( nBlank ) ) ) + break; + } + if( nBlank <= rInf.GetLineStart() ) + return 0; + } + } + if (nIdx < TextFrameIndex(2)) + return 1; + sal_Unicode const cCh(rInf.GetChar(nIdx - TextFrameIndex(1))); + if (CH_BLANK == cCh) + return 1; + if( CH_BREAK == cCh ) + return 0; + return 2; +} + +/** + * Format End of Line + */ +void SwBlankPortion::FormatEOL( SwTextFormatInfo &rInf ) +{ + sal_uInt16 nMay = MayUnderflow( rInf, rInf.GetIdx() - mnLineLength, true ); + if( !nMay ) + return; + + if( nMay > 1 ) + { + if( rInf.GetLast() == this ) + rInf.SetLast( FindPrevPortion( rInf.GetRoot() ) ); + rInf.X( rInf.X() - PrtWidth() ); + rInf.SetIdx( rInf.GetIdx() - GetLen() ); + } + Truncate(); + rInf.SetUnderflow( this ); + if( rInf.GetLast()->IsKernPortion() ) + rInf.SetUnderflow( rInf.GetLast() ); +} + +/** + * Pass on the underflows and trigger them ourselves! + */ +bool SwBlankPortion::Format( SwTextFormatInfo &rInf ) +{ + const bool bFull = rInf.IsUnderflow() || SwExpandPortion::Format( rInf ); + if( bFull && MayUnderflow( rInf, rInf.GetIdx(), rInf.IsUnderflow() ) ) + { + Truncate(); + rInf.SetUnderflow( this ); + if( rInf.GetLast()->IsKernPortion() ) + rInf.SetUnderflow( rInf.GetLast() ); + } + return bFull; +} + +void SwBlankPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + // Draw field shade (can be disabled individually) + if (!m_bMulti) // No gray background for multiportion brackets + rInf.DrawViewOpt(*this, PortionType::Blank); + SwExpandPortion::Paint(rInf); + + if (m_cChar == CHAR_HARDBLANK) + { + if (rInf.GetOpt().IsBlank()) + { + // Draw tilde or degree sign + OUString aMarker = (rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess() + .get(DocumentSettingId::USE_VARIABLE_WIDTH_NBSP) + ? u"~"_ustr + : u"°"_ustr); + + SwPosSize aMarkerSize(rInf.GetTextSize(aMarker)); + Point aPos(rInf.GetPos()); + + std::shared_ptr<SwRect> pPortionRect = std::make_shared<SwRect>(); + rInf.CalcRect(*this, pPortionRect.get()); + aPos.AdjustX((pPortionRect->Width() / 2) - (aMarkerSize.Width() / 2)); + + SwTextPaintInfo aInf(rInf, &aMarker); + aInf.SetPos(aPos); + SwTextPortion aMarkerPor; + aMarkerPor.Width(aMarkerSize.Width()); + aMarkerPor.Height(aMarkerSize.Height()); + aMarkerPor.SetAscent(GetAscent()); + + Color colorBackup = aInf.GetFont()->GetColor(); + aInf.GetFont()->SetColor(NON_PRINTING_CHARACTER_COLOR); + aInf.DrawText(aMarkerPor, TextFrameIndex(aMarker.getLength()), true); + aInf.GetFont()->SetColor(colorBackup); + } + } +} + +bool SwBlankPortion::GetExpText( const SwTextSizeInfo& rInf, OUString &rText ) const +{ + if (m_cChar == CHAR_HARDBLANK + && rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::USE_VARIABLE_WIDTH_NBSP)) + rText = OUString(CH_BLANK); + else + rText = OUString(m_cChar); + + return true; +} + +void SwBlankPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Special( GetLen(), OUString( m_cChar ), GetWhichPor() ); +} + +void SwBlankPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwBlankPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("char"), + BAD_CAST(OUString(m_cChar).toUtf8().getStr())); + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("multi"), + BAD_CAST(OString::boolean(m_bMulti).getStr())); + + (void)xmlTextWriterEndElement(pWriter); +} + +SwPostItsPortion::SwPostItsPortion( bool bScrpt ) + : m_bScript( bScrpt ) +{ + mnLineLength = TextFrameIndex(1); + SetWhichPor( PortionType::PostIts ); +} + +void SwPostItsPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( rInf.OnWin() && Width() ) + rInf.DrawPostIts( IsScript() ); +} + +sal_uInt16 SwPostItsPortion::GetViewWidth( const SwTextSizeInfo &rInf ) const +{ + // Unbelievable: PostIts are always visible + return rInf.OnWin() ? SwViewOption::GetPostItsWidth( rInf.GetOut() ) : 0; +} + +bool SwPostItsPortion::Format( SwTextFormatInfo &rInf ) +{ + const bool bRet = SwLinePortion::Format( rInf ); + // PostIts should not have an effect on line height etc. + SetAscent( 1 ); + Height( 1 ); + return bRet; +} + +bool SwPostItsPortion::GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const +{ + if( rInf.OnWin() && rInf.GetOpt().IsPostIts() ) + rText = " "; + else + rText.clear(); + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porexp.hxx b/sw/source/core/text/porexp.hxx new file mode 100644 index 0000000000..82de5b1be4 --- /dev/null +++ b/sw/source/core/text/porexp.hxx @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include "portxt.hxx" + +class SwExpandPortion : public SwTextPortion +{ +public: + SwExpandPortion() { SetWhichPor( PortionType::Expand ); } + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual TextFrameIndex GetModelPositionForViewPoint(sal_uInt16 nOfst) const override; + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + virtual SwPosSize GetTextSize( const SwTextSizeInfo &rInfo ) const override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const override; +}; + +/// Non-breaking space or non-breaking hyphen. +class SwBlankPortion : public SwExpandPortion +{ + sal_Unicode m_cChar; + bool m_bMulti; // For multiportion brackets +public: + SwBlankPortion( sal_Unicode cCh, bool bMult = false ) + : m_cChar( cCh ), m_bMulti( bMult ) + { SetLen(TextFrameIndex(1)); SetWhichPor( PortionType::Blank ); } + + virtual SwLinePortion *Compress() override; + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + virtual void FormatEOL( SwTextFormatInfo &rInf ) override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + static sal_uInt16 MayUnderflow(const SwTextFormatInfo &rInf, TextFrameIndex nIdx, + bool bUnderflow ); + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const override; +}; + +class SwPostItsPortion : public SwExpandPortion +{ + bool m_bScript; +public: + explicit SwPostItsPortion( bool bScrpt ); + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual sal_uInt16 GetViewWidth( const SwTextSizeInfo &rInf ) const override; + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + bool IsScript() const { return m_bScript; } +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porfld.cxx b/sw/source/core/text/porfld.cxx new file mode 100644 index 0000000000..1a30a4ecd7 --- /dev/null +++ b/sw/source/core/text/porfld.cxx @@ -0,0 +1,1462 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <hintids.hxx> + +#include <com/sun/star/i18n/ScriptType.hpp> +#include <com/sun/star/i18n/XBreakIterator.hpp> +#include <utility> + +#include <comphelper/string.hxx> +#include <vcl/graph.hxx> +#include <editeng/brushitem.hxx> +#include <vcl/metric.hxx> +#include <vcl/outdev.hxx> +#include <vcl/pdfextoutdevdata.hxx> +#include <vcl/pdfwriter.hxx> +#include <viewopt.hxx> +#include <SwPortionHandler.hxx> +#include "porlay.hxx" +#include "porfld.hxx" +#include "inftxt.hxx" +#include <fmtornt.hxx> +#include <frmatr.hxx> +#include <frmtool.hxx> +#include <viewsh.hxx> +#include <doc.hxx> +#include <IDocumentSettingAccess.hxx> +#include <rootfrm.hxx> +#include <breakit.hxx> +#include "porftn.hxx" +#include <accessibilityoptions.hxx> +#include <editeng/lrspitem.hxx> +#include <unicode/ubidi.h> +#include <bookmark.hxx> +#include <docufld.hxx> + +using namespace ::com::sun::star; + +SwLinePortion *SwFieldPortion::Compress() +{ return (GetLen() || !m_aExpand.isEmpty() || SwLinePortion::Compress()) ? this : nullptr; } + +SwFieldPortion *SwFieldPortion::Clone( const OUString &rExpand ) const +{ + std::unique_ptr<SwFont> pNewFnt; + if( m_pFont ) + { + pNewFnt.reset(new SwFont( *m_pFont )); + } + // #i107143# + // pass placeholder property to created <SwFieldPortion> instance. + SwFieldPortion* pClone = new SwFieldPortion(rExpand, std::move(pNewFnt)); + pClone->SetNextOffset( m_nNextOffset ); + pClone->m_bNoLength = m_bNoLength; + return pClone; +} + +void SwFieldPortion::TakeNextOffset( const SwFieldPortion* pField ) +{ + OSL_ENSURE( pField, "TakeNextOffset: Missing Source" ); + m_nNextOffset = pField->GetNextOffset(); + m_aExpand = m_aExpand.replaceAt(0, sal_Int32(m_nNextOffset), u""); + m_bFollow = true; +} + +SwFieldPortion::SwFieldPortion(OUString aExpand, std::unique_ptr<SwFont> pFont, TextFrameIndex const nFieldLen) + : m_aExpand(std::move(aExpand)), m_pFont(std::move(pFont)), m_nNextOffset(0) + , m_nNextScriptChg(COMPLETE_STRING), m_nFieldLen(nFieldLen), m_nViewWidth(0) + , m_bFollow( false ), m_bLeft( false), m_bHide( false) + , m_bCenter (false), m_bHasFollow( false ) + , m_bAnimated( false), m_bNoPaint( false) + , m_bReplace(false) + , m_bNoLength( false ) +{ + SetWhichPor( PortionType::Field ); +} + +SwFieldPortion::SwFieldPortion( const SwFieldPortion& rField ) + : SwExpandPortion( rField ) + , m_aExpand( rField.GetExp() ) + , m_nNextOffset( rField.GetNextOffset() ) + , m_nNextScriptChg( rField.m_nNextScriptChg ) + , m_nFieldLen(rField.m_nFieldLen) + , m_nViewWidth( rField.m_nViewWidth ) + , m_bFollow( rField.IsFollow() ) + , m_bLeft( rField.IsLeft() ) + , m_bHide( rField.IsHide() ) + , m_bCenter( rField.IsCenter() ) + , m_bHasFollow( rField.HasFollow() ) + , m_bAnimated ( rField.m_bAnimated ) + , m_bNoPaint( rField.m_bNoPaint) + , m_bReplace( rField.m_bReplace ) + , m_bNoLength( rField.m_bNoLength ) +{ + if ( rField.HasFont() ) + m_pFont.reset( new SwFont( *rField.GetFont() ) ); + + SetWhichPor( PortionType::Field ); +} + +SwFieldPortion::~SwFieldPortion() +{ + m_pFont.reset(); +} + +sal_uInt16 SwFieldPortion::GetViewWidth( const SwTextSizeInfo &rInf ) const +{ + // even though this is const, nViewWidth should be computed at the very end: + SwFieldPortion* pThis = const_cast<SwFieldPortion*>(this); + if( !Width() && rInf.OnWin() && !rInf.GetOpt().IsPagePreview() && + !rInf.GetOpt().IsReadonly() && rInf.GetOpt().IsFieldShadings() ) + { + if( !m_nViewWidth ) + pThis->m_nViewWidth = rInf.GetTextSize(OUString(' ')).Width(); + } + else + pThis->m_nViewWidth = 0; + return m_nViewWidth; +} + +namespace { + +/** + * Never just use SetLen(0) + */ +class SwFieldSlot +{ + std::shared_ptr<const vcl::text::TextLayoutCache> m_pOldCachedVclData; + const OUString *pOldText; + OUString aText; + TextFrameIndex nIdx; + TextFrameIndex nLen; + sal_Unicode nOrigHookChar; + SwTextFormatInfo *pInf; + bool bOn; +public: + SwFieldSlot( const SwTextFormatInfo* pNew, const SwFieldPortion *pPor ); + ~SwFieldSlot(); +}; + +} + +SwFieldSlot::SwFieldSlot( const SwTextFormatInfo* pNew, const SwFieldPortion *pPor ) + : pOldText(nullptr) + , nIdx(0) + , nLen(0) + , nOrigHookChar(0) + , pInf(nullptr) +{ + bOn = pPor->GetExpText( *pNew, aText ); + + // The text will be replaced ... + if( !bOn ) + return; + + pInf = const_cast<SwTextFormatInfo*>(pNew); + nIdx = pInf->GetIdx(); + nLen = pInf->GetLen(); + pOldText = &(pInf->GetText()); + nOrigHookChar = pInf->GetHookChar(); + m_pOldCachedVclData = pInf->GetCachedVclData(); + pInf->SetLen(TextFrameIndex(aText.getLength())); + pInf->SetCachedVclData(nullptr); + if( pPor->IsFollow() ) + { + pInf->SetFakeLineStart( nIdx > pInf->GetLineStart() ); + pInf->SetIdx(TextFrameIndex(0)); + } + else + { + TextFrameIndex nEnd(pOldText->getLength()); + if (nIdx < nEnd) + { + sal_Int32 const nFieldLen(pPor->GetFieldLen()); + aText = (*pOldText).replaceAt(sal_Int32(nIdx), nFieldLen, aText); + } + else if (nIdx == nEnd) + aText = *pOldText + aText; + else + SAL_WARN("sw.core", "SwFieldSlot bad SwFieldPortion index."); + } + pInf->SetText( aText ); +} + +SwFieldSlot::~SwFieldSlot() +{ + if( bOn ) + { + pInf->SetCachedVclData(m_pOldCachedVclData); + pInf->SetText( *pOldText ); + // ofz#64109 at last for ruby-text when we restore the original text to + // continue laying out the 'body' text of the ruby, then a tab or other + // 'hook char' in the text drawn above it shouldn't affect the 'body' + // While there are other cases, such as tdf#148360, where the tab in an + // inline expanded field, that should affect the body. + if (pInf->IsRuby()) + pInf->SetHookChar(nOrigHookChar); + pInf->SetIdx( nIdx ); + pInf->SetLen( nLen ); + pInf->SetFakeLineStart( false ); + } +} + +void SwFieldPortion::CheckScript( const SwTextSizeInfo &rInf ) +{ + OUString aText; + if (!GetExpText(rInf, aText) || aText.isEmpty()) + return; + + SwFontScript nActual = m_pFont ? m_pFont->GetActual() : rInf.GetFont()->GetActual(); + sal_uInt16 nScript = g_pBreakIt->GetBreakIter()->getScriptType( aText, 0 ); + sal_Int32 nChg = 0; + if( i18n::ScriptType::WEAK == nScript ) + { + nChg = g_pBreakIt->GetBreakIter()->endOfScript(aText,0,nScript); + if (nChg < aText.getLength() && nChg >= 0) + nScript = g_pBreakIt->GetBreakIter()->getScriptType( aText, nChg ); + } + + // nNextScriptChg will be evaluated during SwFieldPortion::Format() + + if (nChg < aText.getLength() && nChg >= 0) + m_nNextScriptChg = TextFrameIndex( + g_pBreakIt->GetBreakIter()->endOfScript(aText, nChg, nScript)); + else + m_nNextScriptChg = TextFrameIndex(aText.getLength()); + + SwFontScript nTmp; + switch ( nScript ) { + case i18n::ScriptType::LATIN : nTmp = SwFontScript::Latin; break; + case i18n::ScriptType::ASIAN : nTmp = SwFontScript::CJK; break; + case i18n::ScriptType::COMPLEX : nTmp = SwFontScript::CTL; break; + default: nTmp = nActual; + } + + // #i16354# Change script type for RTL text to CTL. + const SwScriptInfo& rSI = rInf.GetParaPortion()->GetScriptInfo(); + // #i98418# + const sal_uInt8 nFieldDir = (IsNumberPortion() || IsFootnoteNumPortion()) + ? rSI.GetDefaultDir() + : rSI.DirType(IsFollow() ? rInf.GetIdx() - m_nFieldLen : rInf.GetIdx()); + + { + UErrorCode nError = U_ZERO_ERROR; + UBiDi* pBidi = ubidi_openSized( aText.getLength(), 0, &nError ); + ubidi_setPara( pBidi, reinterpret_cast<const UChar *>(aText.getStr()), aText.getLength(), nFieldDir, nullptr, &nError ); + int32_t nEnd; + UBiDiLevel nCurrDir; + ubidi_getLogicalRun( pBidi, 0, &nEnd, &nCurrDir ); + ubidi_close( pBidi ); + const TextFrameIndex nNextDirChg(nEnd); + m_nNextScriptChg = std::min( m_nNextScriptChg, nNextDirChg ); + + // #i89825# change the script type also to CTL + // if there is no strong LTR char in the LTR run (numbers) + if (nCurrDir != UBIDI_RTL && + (UBIDI_LTR != nFieldDir || i18n::ScriptType::COMPLEX == nScript)) + { + nCurrDir = UBIDI_RTL; + for( sal_Int32 nCharIdx = 0; nCharIdx < nEnd; ++nCharIdx ) + { + UCharDirection nCharDir = u_charDirection ( aText[ nCharIdx ]); + if ( nCharDir == U_LEFT_TO_RIGHT || + nCharDir == U_LEFT_TO_RIGHT_EMBEDDING || + nCharDir == U_LEFT_TO_RIGHT_OVERRIDE ) + { + nCurrDir = UBIDI_LTR; + break; + } + } + } + + if (nCurrDir == UBIDI_RTL) + { + nTmp = SwFontScript::CTL; + // If we decided that this range was RTL after all and the + // previous range was complex but clipped to the start of this + // range, then extend it to be complex over the additional RTL range + if (nScript == i18n::ScriptType::COMPLEX) + m_nNextScriptChg = nNextDirChg; + } + } + + // #i98418# + // keep determined script type for footnote portions as preferred script type. + // For footnote portions a font can not be created directly - see footnote + // portion format method. + if ( IsFootnotePortion() ) + { + static_cast<SwFootnotePortion*>(this)->SetPreferredScriptType( nTmp ); + } + else if ( nTmp != nActual ) + { + if( !m_pFont ) + m_pFont.reset( new SwFont( *rInf.GetFont() ) ); + m_pFont->SetActual( nTmp ); + } + +} + +bool SwFieldPortion::Format( SwTextFormatInfo &rInf ) +{ + // Scope wegen aDiffText::DTOR! + bool bFull = false; + bool bEOL = false; + TextFrameIndex const nTextRest = TextFrameIndex(rInf.GetText().getLength()) - rInf.GetIdx(); + { + TextFrameIndex nRest; + SwFieldSlot aDiffText( &rInf, this ); + SwLayoutModeModifier aLayoutModeModifier( *rInf.GetOut() ); + aLayoutModeModifier.SetAuto(); + + // Field portion has to be split in several parts if + // 1. There are script/direction changes inside the field + // 2. There are portion breaks (tab, break) inside the field: + const TextFrameIndex nOldFullLen = rInf.GetLen(); + TextFrameIndex nFullLen = rInf.ScanPortionEnd(rInf.GetIdx(), rInf.GetIdx() + nOldFullLen) - rInf.GetIdx(); + if ( m_nNextScriptChg < nFullLen ) + { + nFullLen = m_nNextScriptChg; + rInf.SetHookChar( 0 ); + } + rInf.SetLen( nFullLen ); + + if (TextFrameIndex(COMPLETE_STRING) != rInf.GetUnderScorePos() && + rInf.GetUnderScorePos() > rInf.GetIdx() ) + rInf.SetUnderScorePos( rInf.GetIdx() ); + + if( m_pFont ) + m_pFont->AllocFontCacheId( rInf.GetVsh(), m_pFont->GetActual() ); + + SwFontSave aSave( rInf, m_pFont.get() ); + + // Length must be 0: the length is set for bFull after format + // and passed along in nRest. Or else the old length would be + // retained and be used for nRest! + SetLen(TextFrameIndex(0)); + TextFrameIndex const nFollow(IsFollow() ? TextFrameIndex(0) : m_nFieldLen); + + // As odd is may seem: the query for GetLen() must return false due + // to the ExpandPortions _after_ aDiffText (see SoftHyphs), caused + // by SetFull. + if( !nFullLen ) + { + // Don't Init(), as we need height and ascent + Width(0); + bFull = rInf.Width() <= rInf.GetPos().X(); + } + else + { + TextFrameIndex const nOldLineStart = rInf.GetLineStart(); + if( IsFollow() ) + rInf.SetLineStart(TextFrameIndex(0)); + rInf.SetNotEOL( nFullLen == nOldFullLen && nTextRest > nFollow ); + + // the height depending on the fields font is set, + // this is required for SwTextGuess::Guess + Height( rInf.GetTextHeight() + rInf.GetFont()->GetTopBorderSpace() + + rInf.GetFont()->GetBottomBorderSpace() ); + // If a kerning portion is inserted after our field portion, + // the ascent and height must be known + SetAscent( rInf.GetAscent() + rInf.GetFont()->GetTopBorderSpace() ); + bFull = SwTextPortion::Format( rInf ); + rInf.SetNotEOL( false ); + rInf.SetLineStart( nOldLineStart ); + } + TextFrameIndex const nTmpLen = GetLen(); + bEOL = !nTmpLen && nFollow && bFull; + nRest = nOldFullLen - nTmpLen; + + // The char is held in the first position + // Unconditionally after format! + SetLen( m_bNoLength ? TextFrameIndex(0) : nFollow ); + + if( nRest ) + { + // aExpand has not yet been shortened; the new Ofst is a + // result of nRest + TextFrameIndex nNextOfst = TextFrameIndex(m_aExpand.getLength()) - nRest; + + if ( IsQuoVadisPortion() ) + nNextOfst = nNextOfst + TextFrameIndex(static_cast<SwQuoVadisPortion*>(this)->GetContText().getLength()); + + OUString aNew( m_aExpand.copy(sal_Int32(nNextOfst)) ); + m_aExpand = m_aExpand.copy(0, sal_Int32(nNextOfst)); + + // These characters should not be contained in the follow + // field portion. They are handled via the HookChar mechanism. + const sal_Unicode nNew = !aNew.isEmpty() ? aNew[0] : 0; + auto IsHook = [](const sal_Unicode cNew) -> bool + { + switch (cNew) + { + case CH_BREAK: + case CH_TAB: + case CHAR_HARDHYPHEN: // non-breaking hyphen + case CHAR_SOFTHYPHEN: + case CHAR_HARDBLANK: + case CHAR_ZWSP: + case CHAR_WJ: + case CH_TXTATR_BREAKWORD: + case CH_TXTATR_INWORD: + { + return true; + } + default: + return false; + } + }; + if (IsHook(nNew)) + { + if (nNew == CH_BREAK) + { + bFull = true; + } + aNew = aNew.copy(1); + ++nNextOfst; + } + + // Even if there is no more text left for a follow field, + // we have to build a follow field portion (without font), + // otherwise the HookChar mechanism would not work. + SwFieldPortion *pField = Clone( aNew ); + if( !aNew.isEmpty() && !pField->GetFont() ) + { + pField->SetFont( std::make_unique<SwFont>( *rInf.GetFont() ) ); + } + if (IsFollow() || Compress()) + { // empty this will be deleted in SwLineLayout::CalcLine() + // anyway so make sure pField doesn't have a stale flag + pField->SetFollow( true ); + } + if (pField->Compress() && !std::all_of(std::u16string_view(aNew).begin(), + std::u16string_view(aNew).end(), IsHook)) + { // empty pField will be deleted in SwLineLayout::CalcLine() + // anyway so make sure this one doesn't have a stale flag + SetHasFollow( true ); + } + + // For a newly created field, nNextOffset contains the Offset + // of its start of the original string + // If a FollowField is created when formatting, this FollowField's + // Offset is being held in nNextOffset + m_nNextOffset = m_nNextOffset + nNextOfst; + pField->SetNextOffset( m_nNextOffset ); + rInf.SetRest( pField ); + } + } + + if( bEOL && rInf.GetLast() && !rInf.GetUnderflow() ) + rInf.GetLast()->FormatEOL( rInf ); + return bFull; +} + +void SwFieldPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + SwFontSave aSave( rInf, m_pFont.get() ); + +// OSL_ENSURE(GetLen() <= TextFrameIndex(1), "SwFieldPortion::Paint: rest-portion pollution?"); + if (Width() && !m_bContentControl) + { + // A very liberal use of the background + rInf.DrawViewOpt( *this, PortionType::Field ); + SwExpandPortion::Paint( rInf ); + } +} + +bool SwFieldPortion::GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const +{ + rText = m_aExpand; + if( rText.isEmpty() && rInf.OnWin() && + !rInf.GetOpt().IsPagePreview() && !rInf.GetOpt().IsReadonly() && + rInf.GetOpt().IsFieldShadings() && + !HasFollow() ) + rText = " "; + return true; +} + +void SwFieldPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Special( GetLen(), m_aExpand, GetWhichPor() ); +} + +void SwFieldPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwFieldPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("expand"), BAD_CAST(m_aExpand.toUtf8().getStr())); + + if (m_pFont) + { + m_pFont->dumpAsXml(pWriter); + } + + (void)xmlTextWriterEndElement(pWriter); +} + +SwPosSize SwFieldPortion::GetTextSize( const SwTextSizeInfo &rInf ) const +{ + SwFontSave aSave( rInf, m_pFont.get() ); + SwPosSize aSize( SwExpandPortion::GetTextSize( rInf ) ); + return aSize; +} + +SwFieldPortion *SwHiddenPortion::Clone(const OUString &rExpand ) const +{ + std::unique_ptr<SwFont> pNewFnt; + if( m_pFont ) + pNewFnt.reset(new SwFont( *m_pFont )); + return new SwHiddenPortion( rExpand, std::move(pNewFnt) ); +} + +void SwHiddenPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( Width() ) + { + SwFontSave aSave( rInf, m_pFont.get() ); + rInf.DrawViewOpt( *this, PortionType::Hidden ); + SwExpandPortion::Paint( rInf ); + } +} + +bool SwHiddenPortion::GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const +{ + // Do not query for IsHidden()! + return SwFieldPortion::GetExpText( rInf, rText ); +} + +SwNumberPortion::SwNumberPortion( const OUString &rExpand, + std::unique_ptr<SwFont> pFont, + const bool bLft, + const bool bCntr, + const sal_uInt16 nMinDst, + const bool bLabelAlignmentPosAndSpaceModeActive ) + : SwFieldPortion(rExpand, std::move(pFont), TextFrameIndex(0)) + , m_nFixWidth(0) + , m_nMinDist(nMinDst) + , mbLabelAlignmentPosAndSpaceModeActive(bLabelAlignmentPosAndSpaceModeActive) +{ + SetWhichPor( PortionType::Number ); + SetLeft( bLft ); + SetHide( false ); + SetCenter( bCntr ); +} + +TextFrameIndex SwNumberPortion::GetModelPositionForViewPoint(const sal_uInt16) const +{ + return TextFrameIndex(0); +} + +SwFieldPortion *SwNumberPortion::Clone( const OUString &rExpand ) const +{ + std::unique_ptr<SwFont> pNewFnt; + if( m_pFont ) + pNewFnt.reset(new SwFont( *m_pFont )); + + return new SwNumberPortion( rExpand, std::move(pNewFnt), IsLeft(), IsCenter(), + m_nMinDist, mbLabelAlignmentPosAndSpaceModeActive ); +} + +/** + * We can create multiple NumFields + * Tricky, if one enters enough previous-text in the dialog box + * to cause the line to overflow + * We need to keep the Fly's evasion tactics in mind + */ +bool SwNumberPortion::Format( SwTextFormatInfo &rInf ) +{ + SetHide( false ); + const bool bFull = SwFieldPortion::Format( rInf ); + SetLen(TextFrameIndex(0)); + // a numbering portion can be contained in a rotated portion!!! + m_nFixWidth = rInf.IsMulti() ? Height() : Width(); + rInf.SetNumDone( !rInf.GetRest() ); + if( rInf.IsNumDone() ) + { +// SetAscent( rInf.GetAscent() ); + OSL_ENSURE( Height() && mnAscent, "NumberPortions without Height | Ascent" ); + + tools::Long nDiff( 0 ); + + if ( !mbLabelAlignmentPosAndSpaceModeActive ) + { + if (!rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::IGNORE_FIRST_LINE_INDENT_IN_NUMBERING) && + // #i32902# + !IsFootnoteNumPortion() ) + { + nDiff = rInf.Left() + + rInf.GetTextFrame()->GetTextNodeForParaProps()-> + GetSwAttrSet().GetFirstLineIndent().GetTextFirstLineOffset() + - rInf.First() + + rInf.ForcedLeftMargin(); + } + else + { + nDiff = rInf.Left() - rInf.First() + rInf.ForcedLeftMargin(); + } + } + // The text part of the numbering should always at least + // start at the left margin + if( nDiff < 0 ) + nDiff = 0; + else if ( nDiff > rInf.X() ) + nDiff -= rInf.X(); + else + nDiff = 0; + + if( nDiff < m_nFixWidth + m_nMinDist ) + nDiff = m_nFixWidth + m_nMinDist; + + // Numbering evades the Fly, no nDiff in the second round + // Tricky special case: FlyFrame is in an Area we're just about to + // acquire + // The NumberPortion is marked as hidden + const bool bFly = rInf.GetFly() || + ( rInf.GetLast() && rInf.GetLast()->IsFlyPortion() ); + if( nDiff > rInf.Width() ) + { + nDiff = rInf.Width(); + if ( bFly ) + SetHide( true ); + } + + // A numbering portion can be inside a SwRotatedPortion. Then the + // Height has to be changed + if ( rInf.IsMulti() ) + { + if ( Height() < nDiff ) + Height( nDiff ); + } + else if( Width() < nDiff ) + Width( nDiff ); + } + return bFull; +} + + +/** + * A FormatEOL indicates that the subsequent text did not fit onto + * the line anymore. In order for the Numbering to follow through, + * we hide this NumberPortion + */ +void SwNumberPortion::FormatEOL( SwTextFormatInfo& ) +{ + + // This caused trouble with flys anchored as characters. + // If one of these is numbered but does not fit to the line, + // it calls this function, causing a loop because both the number + // portion and the fly portion go to the next line +// SetHide( true ); +} + + +/** + * A hidden NumberPortion is not displayed, unless there are TextPortions in + * this line or there's just one line at all + */ +void SwNumberPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if ( IsHide() && rInf.GetParaPortion() && rInf.GetParaPortion()->GetNext() ) + { + SwLinePortion *pTmp = GetNextPortion(); + while ( pTmp && !pTmp->InTextGrp() ) + pTmp = pTmp->GetNextPortion(); + if ( !pTmp ) + return; + } + + // calculate the width of the number portion, including follows + const sal_uInt16 nOldWidth = Width(); + sal_uInt16 nSumWidth = 0; + sal_uInt16 nOffset = 0; + + const SwLinePortion* pTmp = this; + while ( pTmp && pTmp->InNumberGrp() ) + { + nSumWidth = nSumWidth + pTmp->Width(); + if ( static_cast<const SwNumberPortion*>(pTmp)->HasFollow() ) + pTmp = pTmp->GetNextPortion(); + else + { + nOffset = pTmp->Width() - static_cast<const SwNumberPortion*>(pTmp)->m_nFixWidth; + break; + } + } + + // The master portion takes care for painting the background of the + // follow field portions + if ( ! IsFollow() ) + { + SwNumberPortion *pThis = const_cast<SwNumberPortion*>(this); + pThis->Width( nSumWidth ); + rInf.DrawViewOpt( *this, PortionType::Number ); + pThis->Width( nOldWidth ); + } + + if( m_aExpand.isEmpty() ) + return; + + const SwFont *pTmpFnt = rInf.GetFont(); + bool bPaintSpace = ( LINESTYLE_NONE != pTmpFnt->GetUnderline() || + LINESTYLE_NONE != pTmpFnt->GetOverline() || + STRIKEOUT_NONE != pTmpFnt->GetStrikeout() ) && + !pTmpFnt->IsWordLineMode(); + if( bPaintSpace && m_pFont ) + bPaintSpace = ( LINESTYLE_NONE != m_pFont->GetUnderline() || + LINESTYLE_NONE != m_pFont->GetOverline() || + STRIKEOUT_NONE != m_pFont->GetStrikeout() ) && + !m_pFont->IsWordLineMode(); + + SwFontSave aSave( rInf, m_pFont.get() ); + + if( m_nFixWidth == Width() && ! HasFollow() ) + SwExpandPortion::Paint( rInf ); + else + { + // logical const: reset width + SwNumberPortion *pThis = const_cast<SwNumberPortion*>(this); + bPaintSpace = bPaintSpace && m_nFixWidth < nOldWidth; + sal_uInt16 nSpaceOffs = m_nFixWidth; + pThis->Width( m_nFixWidth ); + + if( ( IsLeft() && ! rInf.GetTextFrame()->IsRightToLeft() ) || + ( ! IsLeft() && ! IsCenter() && rInf.GetTextFrame()->IsRightToLeft() ) ) + SwExpandPortion::Paint( rInf ); + else + { + SwTextPaintInfo aInf( rInf ); + if( nOffset < m_nMinDist ) + nOffset = 0; + else + { + if( IsCenter() ) + { + /* #110778# a / 2 * 2 == a is not a tautology */ + sal_uInt16 nTmpOffset = nOffset; + nOffset /= 2; + if( nOffset < m_nMinDist ) + nOffset = nTmpOffset - m_nMinDist; + } + else + nOffset = nOffset - m_nMinDist; + } + aInf.X( aInf.X() + nOffset ); + SwExpandPortion::Paint( aInf ); + if( bPaintSpace ) + nSpaceOffs = nSpaceOffs + nOffset; + } + if( bPaintSpace && nOldWidth > nSpaceOffs ) + { + SwTextPaintInfo aInf( rInf ); + aInf.X( aInf.X() + nSpaceOffs ); + + // #i53199# Adjust position of underline: + if ( rInf.GetUnderFnt() ) + { + const Point aNewPos( aInf.GetPos().X(), rInf.GetUnderFnt()->GetPos().Y() ); + rInf.GetUnderFnt()->SetPos( aNewPos ); + } + + pThis->Width( nOldWidth - nSpaceOffs + 12 ); + { + SwTextSlot aDiffText( &aInf, this, true, false, " " ); + aInf.DrawText( *this, aInf.GetLen(), true ); + } + } + pThis->Width( nOldWidth ); + } +} + +SwBulletPortion::SwBulletPortion( const sal_UCS4 cBullet, + std::u16string_view rBulletFollowedBy, + std::unique_ptr<SwFont> pFont, + const bool bLft, + const bool bCntr, + const sal_uInt16 nMinDst, + const bool bLabelAlignmentPosAndSpaceModeActive ) + : SwNumberPortion( OUString(&cBullet, 1) + rBulletFollowedBy, + std::move(pFont), bLft, bCntr, nMinDst, + bLabelAlignmentPosAndSpaceModeActive ) +{ + SetWhichPor( PortionType::Bullet ); +} + +#define GRFNUM_SECURE 10 + +SwGrfNumPortion::SwGrfNumPortion( + const OUString& rGraphicFollowedBy, + const SvxBrushItem* pGrfBrush, OUString const & referer, + const SwFormatVertOrient* pGrfOrient, const Size& rGrfSize, + const bool bLft, const bool bCntr, const sal_uInt16 nMinDst, + const bool bLabelAlignmentPosAndSpaceModeActive ) : + SwNumberPortion( rGraphicFollowedBy, nullptr, bLft, bCntr, nMinDst, + bLabelAlignmentPosAndSpaceModeActive ), + m_pBrush( new SvxBrushItem(RES_BACKGROUND) ), m_nId( 0 ) +{ + SetWhichPor( PortionType::GrfNum ); + SetAnimated( false ); + m_bReplace = false; + if( pGrfBrush ) + { + m_pBrush.reset(pGrfBrush->Clone()); + const Graphic* pGraph = pGrfBrush->GetGraphic(referer); + if( pGraph ) + SetAnimated( pGraph->IsAnimated() ); + else + m_bReplace = true; + } + if( pGrfOrient ) + { + m_nYPos = pGrfOrient->GetPos(); + m_eOrient = pGrfOrient->GetVertOrient(); + } + else + { + m_nYPos = 0; + m_eOrient = text::VertOrientation::TOP; + } + Width( rGrfSize.Width() + 2 * GRFNUM_SECURE ); + m_nFixWidth = Width(); + m_nGrfHeight = rGrfSize.Height() + 2 * GRFNUM_SECURE; + Height( sal_uInt16(m_nGrfHeight) ); + m_bNoPaint = false; +} + +SwGrfNumPortion::~SwGrfNumPortion() +{ + if ( IsAnimated() ) + { + Graphic* pGraph = const_cast<Graphic*>(m_pBrush->GetGraphic()); + if (pGraph) + pGraph->StopAnimation( nullptr, m_nId ); + } + m_pBrush.reset(); +} + +void SwGrfNumPortion::StopAnimation( const OutputDevice* pOut ) +{ + if ( IsAnimated() ) + { + Graphic* pGraph = const_cast<Graphic*>(m_pBrush->GetGraphic()); + if (pGraph) + pGraph->StopAnimation( pOut, m_nId ); + } +} + +bool SwGrfNumPortion::Format( SwTextFormatInfo &rInf ) +{ + SetHide( false ); +// Width( nFixWidth ); + sal_uInt16 nFollowedByWidth( 0 ); + if ( mbLabelAlignmentPosAndSpaceModeActive ) + { + SwFieldPortion::Format( rInf ); + nFollowedByWidth = Width(); + SetLen(TextFrameIndex(0)); + } + Width( m_nFixWidth + nFollowedByWidth ); + const bool bFull = rInf.Width() < rInf.X() + Width(); + const bool bFly = rInf.GetFly() || + ( rInf.GetLast() && rInf.GetLast()->IsFlyPortion() ); + SetAscent( GetRelPos() > 0 ? GetRelPos() : 0 ); + if( GetAscent() > Height() ) + Height( GetAscent() ); + + if( bFull ) + { + Width( rInf.Width() - rInf.X() ); + if( bFly ) + { + SetLen(TextFrameIndex(0)); + m_bNoPaint = true; + rInf.SetNumDone( false ); + return true; + } + } + rInf.SetNumDone( true ); +// long nDiff = rInf.Left() - rInf.First() + rInf.ForcedLeftMargin(); + tools::Long nDiff = mbLabelAlignmentPosAndSpaceModeActive + ? 0 + : rInf.Left() - rInf.First() + rInf.ForcedLeftMargin(); + // The TextPortion should at least always start on the + // left margin + if( nDiff < 0 ) + nDiff = 0; + else if ( nDiff > rInf.X() ) + nDiff -= rInf.X(); + if( nDiff < m_nFixWidth + m_nMinDist ) + nDiff = m_nFixWidth + m_nMinDist; + + // Numbering evades Fly, no nDiff in the second round + // Tricky special case: FlyFrame is in the Area we were just + // about to get a hold of. + // The NumberPortion is marked as hidden + if( nDiff > rInf.Width() ) + { + nDiff = rInf.Width(); + if( bFly ) + SetHide( true ); + } + + if( Width() < nDiff ) + Width( nDiff ); + return bFull; +} + + +/** + * A hidden NumberPortion is not displayed, unless there are TextPortions in + * this line or there's only one line at all + */ +void SwGrfNumPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( m_bNoPaint ) + return; + if ( IsHide() && rInf.GetParaPortion() && rInf.GetParaPortion()->GetNext() ) + { + SwLinePortion *pTmp = GetNextPortion(); + while ( pTmp && !pTmp->InTextGrp() ) + pTmp = pTmp->GetNextPortion(); + if ( !pTmp ) + return; + } + Point aPos( rInf.X() + GRFNUM_SECURE, rInf.Y() - GetRelPos() + GRFNUM_SECURE ); + tools::Long nTmpWidth = std::max( tools::Long(0), static_cast<tools::Long>(m_nFixWidth - 2 * GRFNUM_SECURE) ); + Size aSize( nTmpWidth, GetGrfHeight() - 2 * GRFNUM_SECURE ); + + const bool bTmpLeft = mbLabelAlignmentPosAndSpaceModeActive || + ( IsLeft() && ! rInf.GetTextFrame()->IsRightToLeft() ) || + ( ! IsLeft() && ! IsCenter() && rInf.GetTextFrame()->IsRightToLeft() ); + + if( m_nFixWidth < Width() && !bTmpLeft ) + { + sal_uInt16 nOffset = Width() - m_nFixWidth; + if( nOffset < m_nMinDist ) + nOffset = 0; + else + { + if( IsCenter() ) + { + nOffset /= 2; + if( nOffset < m_nMinDist ) + nOffset = Width() - m_nFixWidth - m_nMinDist; + } + else + nOffset = nOffset - m_nMinDist; + } + aPos.AdjustX(nOffset ); + } + + if( m_bReplace ) + { + const tools::Long nTmpH = GetNextPortion() ? GetNextPortion()->GetAscent() : 120; + aSize = Size( nTmpH, nTmpH ); + aPos.setY( rInf.Y() - nTmpH ); + } + SwRect aTmp( aPos, aSize ); + + bool bDraw = true; + + if ( IsAnimated() ) + { + bDraw = !rInf.GetOpt().IsGraphic(); + if( !m_nId ) + { + SetId( reinterpret_cast<sal_IntPtr>( rInf.GetTextFrame() ) ); + rInf.GetTextFrame()->SetAnimation(); + } + if( aTmp.Overlaps( rInf.GetPaintRect() ) && !bDraw ) + { + rInf.NoteAnimation(); + const SwViewShell* pViewShell = rInf.GetVsh(); + + // virtual device, not pdf export + if( OUTDEV_VIRDEV == rInf.GetOut()->GetOutDevType() && + pViewShell && pViewShell->GetWin() ) + { + Graphic* pGraph = const_cast<Graphic*>(m_pBrush->GetGraphic()); + if (pGraph) + pGraph->StopAnimation(nullptr,m_nId); + rInf.GetTextFrame()->getRootFrame()->GetCurrShell()->InvalidateWindows( aTmp ); + } + + else if ( pViewShell && + !pViewShell->GetAccessibilityOptions()->IsStopAnimatedGraphics() && + !pViewShell->IsPreview() && + // #i9684# Stop animation during printing/pdf export. + pViewShell->GetWin() ) + { + Graphic* pGraph = const_cast<Graphic*>(m_pBrush->GetGraphic()); + if (pGraph) + { + const OutputDevice* pOut = rInf.GetOut(); + assert(pOut); + pGraph->StartAnimation( + *const_cast<OutputDevice*>(pOut), aPos, aSize, m_nId); + } + } + + // pdf export, printing, preview, stop animations... + else + bDraw = true; + } + if( bDraw ) + { + + Graphic* pGraph = const_cast<Graphic*>(m_pBrush->GetGraphic()); + if (pGraph) + pGraph->StopAnimation( nullptr, m_nId ); + } + } + + SwRect aRepaint( rInf.GetPaintRect() ); + const SwTextFrame& rFrame = *rInf.GetTextFrame(); + if( rFrame.IsVertical() ) + { + rFrame.SwitchHorizontalToVertical( aTmp ); + rFrame.SwitchHorizontalToVertical( aRepaint ); + } + + if( rFrame.IsRightToLeft() ) + { + rFrame.SwitchLTRtoRTL( aTmp ); + rFrame.SwitchLTRtoRTL( aRepaint ); + } + + if( bDraw && aTmp.HasArea() ) + { + const OutputDevice* pOut = rInf.GetOut(); + assert(pOut); + DrawGraphic( m_pBrush.get(), *const_cast<OutputDevice*>(pOut), + aTmp, aRepaint, m_bReplace ? GRFNUM_REPLACE : GRFNUM_YES ); + } +} + +void SwGrfNumPortion::SetBase( tools::Long nLnAscent, tools::Long nLnDescent, + tools::Long nFlyAsc, tools::Long nFlyDesc ) +{ + if ( GetOrient() == text::VertOrientation::NONE ) + return; + + SetRelPos( 0 ); + if ( GetOrient() == text::VertOrientation::CENTER ) + SetRelPos( GetGrfHeight() / 2 ); + else if ( GetOrient() == text::VertOrientation::TOP ) + SetRelPos( GetGrfHeight() - GRFNUM_SECURE ); + else if ( GetOrient() == text::VertOrientation::BOTTOM ) + ; + else if ( GetOrient() == text::VertOrientation::CHAR_CENTER ) + SetRelPos( ( GetGrfHeight() + nLnAscent - nLnDescent ) / 2 ); + else if ( GetOrient() == text::VertOrientation::CHAR_TOP ) + SetRelPos( nLnAscent ); + else if ( GetOrient() == text::VertOrientation::CHAR_BOTTOM ) + SetRelPos( GetGrfHeight() - nLnDescent ); + else + { + if( GetGrfHeight() >= nFlyAsc + nFlyDesc ) + { + // If I'm as large as the line, I do not need to adjust + // at the line; I'll leave the max. ascent unchanged + SetRelPos( nFlyAsc ); + } + else if ( GetOrient() == text::VertOrientation::LINE_CENTER ) + SetRelPos( ( GetGrfHeight() + nFlyAsc - nFlyDesc ) / 2 ); + else if ( GetOrient() == text::VertOrientation::LINE_TOP ) + SetRelPos( nFlyAsc ); + else if ( GetOrient() == text::VertOrientation::LINE_BOTTOM ) + SetRelPos( GetGrfHeight() - nFlyDesc ); + } +} + +void SwTextFrame::StopAnimation( const OutputDevice* pOut ) +{ + OSL_ENSURE( HasAnimation(), "SwTextFrame::StopAnimation: Which Animation?" ); + if( !HasPara() ) + return; + + SwLineLayout *pLine = GetPara(); + while( pLine ) + { + SwLinePortion *pPor = pLine->GetNextPortion(); + while( pPor ) + { + if( pPor->IsGrfNumPortion() ) + static_cast<SwGrfNumPortion*>(pPor)->StopAnimation( pOut ); + // The NumberPortion is always at the first char, + // which means we can cancel as soon as we've reached a portion + // with a length > 0 + pPor = pPor->GetLen() ? nullptr : pPor->GetNextPortion(); + } + pLine = pLine->GetLen() ? nullptr : pLine->GetNext(); + } +} + +/** + * Initializes the script array and clears the width array + */ +SwCombinedPortion::SwCombinedPortion( const OUString &rText ) + : SwFieldPortion( rText ) + , m_aWidth{ static_cast<sal_uInt16>(0), + static_cast<sal_uInt16>(0), + static_cast<sal_uInt16>(0) } + , m_nUpPos(0) + , m_nLowPos(0) + , m_nProportion(55) +{ + SetLen(TextFrameIndex(1)); + SetWhichPor( PortionType::Combined ); + if( m_aExpand.getLength() > 6 ) + m_aExpand = m_aExpand.copy( 0, 6 ); + + // Initialization of the scripttype array, + // the arrays of width and position are filled by the format function + assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is()); + + SwFontScript nScr = SW_SCRIPTS; + for( sal_Int32 i = 0; i < rText.getLength(); ++i ) + { + switch ( g_pBreakIt->GetBreakIter()->getScriptType( rText, i ) ) { + case i18n::ScriptType::LATIN : nScr = SwFontScript::Latin; break; + case i18n::ScriptType::ASIAN : nScr = SwFontScript::CJK; break; + case i18n::ScriptType::COMPLEX : nScr = SwFontScript::CTL; break; + } + m_aScrType[i] = nScr; + } +} + +void SwCombinedPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + OSL_ENSURE(GetLen() <= TextFrameIndex(1), "SwFieldPortion::Paint: rest-portion pollution?"); + if( !Width() ) + return; + + rInf.DrawBackBrush( *this ); + rInf.DrawViewOpt( *this, PortionType::Field ); + + // do we have to repaint a post it portion? + if( rInf.OnWin() && mpNextPortion && !mpNextPortion->Width() ) + mpNextPortion->PrePaint( rInf, this ); + + const sal_Int32 nCount = m_aExpand.getLength(); + if( !nCount ) + return; + OSL_ENSURE( nCount < 7, "Too much combined characters" ); + + // the first character of the second row + const sal_Int32 nTop = ( nCount + 1 ) / 2; + + SwFont aTmpFont( *rInf.GetFont() ); + aTmpFont.SetProportion( m_nProportion ); // a smaller font + SwFontSave aFontSave( rInf, &aTmpFont ); + + Point aOldPos = rInf.GetPos(); + Point aOutPos( aOldPos.X(), aOldPos.Y() - m_nUpPos );// Y of the first row + for( sal_Int32 i = 0 ; i < nCount; ++i ) + { + if( i == nTop ) // change the row + aOutPos.setY( aOldPos.Y() + m_nLowPos ); // Y of the second row + aOutPos.setX( aOldPos.X() + m_aPos[i] ); // X position + const SwFontScript nAct = m_aScrType[i]; // script type + aTmpFont.SetActual( nAct ); + + // if there're more than 4 characters to display, we choose fonts + // with 2/3 of the original font width. + if( m_aWidth[ nAct ] ) + { + Size aTmpSz = aTmpFont.GetSize( nAct ); + if( aTmpSz.Width() != m_aWidth[ nAct ] ) + { + aTmpSz.setWidth( m_aWidth[ nAct ] ); + aTmpFont.SetSize( aTmpSz, nAct ); + } + } + const_cast<SwTextPaintInfo&>(rInf).SetPos( aOutPos ); + rInf.DrawText(m_aExpand, *this, TextFrameIndex(i), TextFrameIndex(1)); + } + // rInf is const, so we have to take back our manipulations + const_cast<SwTextPaintInfo&>(rInf).SetPos( aOldPos ); + +} + +bool SwCombinedPortion::Format( SwTextFormatInfo &rInf ) +{ + const sal_Int32 nCount = m_aExpand.getLength(); + if( !nCount ) + { + Width( 0 ); + return false; + } + + OSL_ENSURE( nCount < 7, "Too much combined characters" ); + + // If there are leading "weak"-scripttyped characters in this portion, + // they get the actual scripttype. + for( sal_Int32 i = 0; i < nCount && SW_SCRIPTS == m_aScrType[i]; ++i ) + m_aScrType[i] = rInf.GetFont()->GetActual(); + if( nCount > 4 ) + { + // more than four? Ok, then we need the 2/3 font width + for( sal_Int32 i = 0; i < m_aExpand.getLength(); ++i ) + { + OSL_ENSURE( m_aScrType[i] < SW_SCRIPTS, "Combined: Script fault" ); + if( !m_aWidth[ m_aScrType[i] ] ) + { + rInf.GetOut()->SetFont( rInf.GetFont()->GetFnt( m_aScrType[i] ) ); + m_aWidth[ m_aScrType[i] ] = + o3tl::narrowing<sal_uInt16>(2 * rInf.GetOut()->GetFontMetric().GetFontSize().Width() / 3); + } + } + } + + const sal_Int32 nTop = ( nCount + 1 ) / 2; // the first character of the second line + SwViewShell *pSh = rInf.GetTextFrame()->getRootFrame()->GetCurrShell(); + SwFont aTmpFont( *rInf.GetFont() ); + SwFontSave aFontSave( rInf, &aTmpFont ); + m_nProportion = 55; + // In nMainAscent/Descent we store the ascent and descent + // of the original surrounding font + sal_uInt16 nMaxDescent, nMaxAscent, nMaxWidth; + sal_uInt16 nMainDescent = rInf.GetFont()->GetHeight( pSh, *rInf.GetOut() ); + const sal_uInt16 nMainAscent = rInf.GetFont()->GetAscent( pSh, *rInf.GetOut() ); + nMainDescent = nMainDescent - nMainAscent; + // we start with a 50% font, but if we notice that the combined portion + // becomes bigger than the surrounding font, we check 45% and maybe 40%. + do + { + m_nProportion -= 5; + aTmpFont.SetProportion( m_nProportion ); + memset( &m_aPos, 0, sizeof(m_aPos) ); + nMaxDescent = 0; + nMaxAscent = 0; + nMaxWidth = 0; + m_nUpPos = m_nLowPos = 0; + + // Now we get the width of all characters. + // The ascent and the width of the first line are stored in the + // ascent member of the portion, the descent in nLowPos. + // The ascent, descent and width of the second line are stored in the + // local nMaxAscent, nMaxDescent and nMaxWidth variables. + for( sal_Int32 i = 0; i < nCount; ++i ) + { + SwFontScript nScrp = m_aScrType[i]; + aTmpFont.SetActual( nScrp ); + if( m_aWidth[ nScrp ] ) + { + Size aFontSize( aTmpFont.GetSize( nScrp ) ); + aFontSize.setWidth( m_aWidth[ nScrp ] ); + aTmpFont.SetSize( aFontSize, nScrp ); + } + + SwDrawTextInfo aDrawInf(pSh, *rInf.GetOut(), m_aExpand, i, 1); + Size aSize = aTmpFont.GetTextSize_( aDrawInf ); + const sal_uInt16 nAsc = aTmpFont.GetAscent( pSh, *rInf.GetOut() ); + m_aPos[ i ] = o3tl::narrowing<sal_uInt16>(aSize.Width()); + if( i == nTop ) // enter the second line + { + m_nLowPos = nMaxDescent; + Height( nMaxDescent + nMaxAscent ); + Width( nMaxWidth ); + SetAscent( nMaxAscent ); + nMaxAscent = 0; + nMaxDescent = 0; + nMaxWidth = 0; + } + nMaxWidth = nMaxWidth + m_aPos[ i ]; + if( nAsc > nMaxAscent ) + nMaxAscent = nAsc; + if( aSize.Height() - nAsc > nMaxDescent ) + nMaxDescent = aSize.Height() - nAsc; + } + // for one or two characters we double the width of the portion + if( nCount < 3 ) + { + nMaxWidth *= 2; + Width( 2*Width() ); + if( nCount < 2 ) + { + Height( nMaxAscent + nMaxDescent ); + m_nLowPos = nMaxDescent; + } + } + Height( Height() + nMaxDescent + nMaxAscent ); + m_nUpPos = nMaxAscent; + SetAscent( Height() - nMaxDescent - m_nLowPos ); + } while( m_nProportion > 40 && ( GetAscent() > nMainAscent || + Height() - GetAscent() > nMainDescent ) ); + // if the combined portion is smaller than the surrounding text, + // the portion grows. This looks better, if there's a character background. + if( GetAscent() < nMainAscent ) + { + Height( Height() + nMainAscent - GetAscent() ); + SetAscent( nMainAscent ); + } + if( Height() < nMainAscent + nMainDescent ) + Height( nMainAscent + nMainDescent ); + + // We calculate the x positions of the characters in both lines... + sal_uInt16 nTopDiff = 0; + sal_uInt16 nBotDiff = 0; + if( nMaxWidth > Width() ) + { + nTopDiff = ( nMaxWidth - Width() ) / 2; + Width( nMaxWidth ); + } + else + nBotDiff = ( Width() - nMaxWidth ) / 2; + switch( nTop) + { + case 3: m_aPos[1] = m_aPos[0] + nTopDiff; + [[fallthrough]]; + case 2: m_aPos[nTop-1] = Width() - m_aPos[nTop-1]; + } + m_aPos[0] = 0; + switch( nCount ) + { + case 5: m_aPos[4] = m_aPos[3] + nBotDiff; + [[fallthrough]]; + case 3: m_aPos[nTop] = nBotDiff; break; + case 6: m_aPos[4] = m_aPos[3] + nBotDiff; + [[fallthrough]]; + case 4: m_aPos[nTop] = 0; + [[fallthrough]]; + case 2: m_aPos[nCount-1] = Width() - m_aPos[nCount-1]; + } + + // Does the combined portion fit the line? + const bool bFull = rInf.Width() < rInf.X() + Width(); + if( bFull ) + { + if( rInf.GetLineStart() == rInf.GetIdx() && (!rInf.GetLast()->InFieldGrp() + || !static_cast<SwFieldPortion*>(rInf.GetLast())->IsFollow() ) ) + Width( rInf.Width() - rInf.X() ); + else + { + Truncate(); + Width( 0 ); + SetLen(TextFrameIndex(0)); + if( rInf.GetLast() ) + rInf.GetLast()->FormatEOL( rInf ); + } + } + return bFull; +} + +sal_uInt16 SwCombinedPortion::GetViewWidth( const SwTextSizeInfo &rInf ) const +{ + if( !GetLen() ) // for the dummy part at the end of the line, where + return 0; // the combined portion doesn't fit. + return SwFieldPortion::GetViewWidth( rInf ); +} + +SwFieldPortion *SwFieldFormDropDownPortion::Clone(const OUString &rExpand) const +{ + return new SwFieldFormDropDownPortion(m_pFieldMark, rExpand); +} + +void SwFieldFormDropDownPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + SwFieldPortion::Paint( rInf ); + + ::sw::mark::DropDownFieldmark* pDropDownField = dynamic_cast< ::sw::mark::DropDownFieldmark* >(m_pFieldMark); + if(pDropDownField) + { + SwRect aPaintArea; + rInf.CalcRect( *this, &aPaintArea ); + pDropDownField->SetPortionPaintArea(aPaintArea); + } +} + +SwFieldPortion *SwFieldFormDatePortion::Clone(const OUString &/*rExpand*/) const +{ + return new SwFieldFormDatePortion(m_pFieldMark, m_bStart); +} + +void SwFieldFormDatePortion::Paint( const SwTextPaintInfo &rInf ) const +{ + SwFieldPortion::Paint( rInf ); + + ::sw::mark::DateFieldmark* pDateField = dynamic_cast< ::sw::mark::DateFieldmark* >(m_pFieldMark); + if(pDateField) + { + SwRect aPaintArea; + rInf.CalcRect( *this, &aPaintArea ); + if(m_bStart) + pDateField->SetPortionPaintAreaStart(aPaintArea); + else + pDateField->SetPortionPaintAreaEnd(aPaintArea); + } +} + +SwFieldPortion* SwJumpFieldPortion::Clone(const OUString& rExpand) const +{ + auto pRet = new SwJumpFieldPortion(*this); + pRet->m_aExpand = rExpand; + return pRet; +} + +bool SwJumpFieldPortion::DescribePDFControl(const SwTextPaintInfo& rInf) const +{ + auto pPDFExtOutDevData + = dynamic_cast<vcl::PDFExtOutDevData*>(rInf.GetOut()->GetExtOutDevData()); + if (!pPDFExtOutDevData) + return false; + + if (!pPDFExtOutDevData->GetIsExportFormFields()) + return false; + + if (m_nFormat != SwJumpEditFormat::JE_FMT_TEXT) + return false; + + vcl::PDFWriter::EditWidget aDescriptor; + + aDescriptor.Border = true; + aDescriptor.BorderColor = COL_BLACK; + + SwRect aLocation; + rInf.CalcRect(*this, &aLocation); + aDescriptor.Location = aLocation.SVRect(); + + // Map the text of the field to the descriptor's text. + static sal_Unicode constexpr aForbidden[] = { CH_TXTATR_BREAKWORD, 0 }; + aDescriptor.Text = comphelper::string::removeAny(GetExp(), aForbidden); + + // Description for accessibility purposes. + if (!m_sHelp.isEmpty()) + aDescriptor.Description = m_sHelp; + + pPDFExtOutDevData->WrapBeginStructureElement(vcl::PDFWriter::Form); + pPDFExtOutDevData->CreateControl(aDescriptor); + pPDFExtOutDevData->EndStructureElement(); + + return true; +} + +void SwJumpFieldPortion::Paint(const SwTextPaintInfo& rInf) const +{ + if (Width() && DescribePDFControl(rInf)) + return; + + if (rInf.GetOpt().IsShowPlaceHolderFields()) + SwFieldPortion::Paint(rInf); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porfld.hxx b/sw/source/core/text/porfld.hxx new file mode 100644 index 0000000000..b923729424 --- /dev/null +++ b/sw/source/core/text/porfld.hxx @@ -0,0 +1,285 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <sal/config.h> + +#include <string_view> + +#include <swtypes.hxx> +#include <swfont.hxx> +#include "porexp.hxx" +#include <o3tl/enumarray.hxx> + +class SvxBrushItem; +class SwFormatVertOrient; + +class SwFieldPortion : public SwExpandPortion +{ + friend class SwTextFormatter; +protected: + OUString m_aExpand; // The expanded field + std::unique_ptr<SwFont> m_pFont; // For multi-line fields + TextFrameIndex m_nNextOffset; // Offset of the follow in the original string + TextFrameIndex m_nNextScriptChg; + TextFrameIndex m_nFieldLen; //< Length of field text, 1 for normal fields, any number for input fields + // TODO ^ do we need this as member or is base class len enough? + sal_uInt16 m_nViewWidth; // Screen width for empty fields + bool m_bFollow : 1; // 2nd or later part of a field + bool m_bLeft : 1; // Used by SwNumberPortion + bool m_bHide : 1; // Used by SwNumberPortion + bool m_bCenter : 1; // Used by SwNumberPortion + bool m_bHasFollow : 1; // Continues on the next line + bool m_bAnimated : 1; // Used by SwGrfNumPortion + bool m_bNoPaint : 1; // Used by SwGrfNumPortion + bool m_bReplace : 1; // Used by SwGrfNumPortion + bool m_bNoLength : 1; // HACK for meta suffix (no CH_TXTATR) + bool m_bContentControl = false; + + void SetFont( std::unique_ptr<SwFont> pNew ) { m_pFont = std::move(pNew); } + bool IsNoLength() const { return m_bNoLength; } + void SetNoLength() { m_bNoLength = true; } + +public: + SwFieldPortion( const SwFieldPortion& rField ); + SwFieldPortion(OUString aExpand, std::unique_ptr<SwFont> pFnt = nullptr, TextFrameIndex nLen = TextFrameIndex(1)); + virtual ~SwFieldPortion() override; + + void TakeNextOffset( const SwFieldPortion* pField ); + void CheckScript( const SwTextSizeInfo &rInf ); + bool HasFont() const { return nullptr != m_pFont; } + // #i89179# - made public + const SwFont *GetFont() const { return m_pFont.get(); } + + const OUString& GetExp() const { return m_aExpand; } + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + + // Empty fields are also allowed + virtual SwLinePortion *Compress() override; + + virtual sal_uInt16 GetViewWidth( const SwTextSizeInfo &rInf ) const override; + + bool IsFollow() const { return m_bFollow; } + void SetFollow( bool bNew ) { m_bFollow = bNew; } + + bool IsLeft() const { return m_bLeft; } + void SetLeft( bool bNew ) { m_bLeft = bNew; } + + bool IsHide() const { return m_bHide; } + void SetHide( bool bNew ) { m_bHide = bNew; } + + bool IsCenter() const { return m_bCenter; } + void SetCenter( bool bNew ) { m_bCenter = bNew; } + + bool HasFollow() const { return m_bHasFollow; } + void SetHasFollow( bool bNew ) { m_bHasFollow = bNew; } + + TextFrameIndex GetNextOffset() const { return m_nNextOffset; } + void SetNextOffset(TextFrameIndex nNew) { m_nNextOffset = nNew; } + + TextFrameIndex GetFieldLen() const { return m_nFieldLen; } + + // Field cloner for SplitGlue + virtual SwFieldPortion *Clone( const OUString &rExpand ) const; + + // Extra GetTextSize because of pFnt + virtual SwPosSize GetTextSize( const SwTextSizeInfo &rInfo ) const override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; + + void SetContentControl(bool bContentControl) { m_bContentControl = bContentControl; } + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const override; +}; + +/** + * Distinguish only for painting/hide + */ +class SwHiddenPortion : public SwFieldPortion +{ +public: + SwHiddenPortion( const OUString &rExpand, std::unique_ptr<SwFont> pFntL = nullptr ) + : SwFieldPortion( rExpand, std::move(pFntL) ) + { SetLen(TextFrameIndex(1)); SetWhichPor( PortionType::Hidden ); } + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + + // Field cloner for SplitGlue + virtual SwFieldPortion *Clone( const OUString &rExpand ) const override; +}; + +class SwNumberPortion : public SwFieldPortion +{ +protected: + sal_uInt16 m_nFixWidth; // See Glues + sal_uInt16 m_nMinDist; // Minimal distance to the text + bool mbLabelAlignmentPosAndSpaceModeActive; + +public: + SwNumberPortion( const OUString &rExpand, + std::unique_ptr<SwFont> pFnt, + const bool bLeft, + const bool bCenter, + const sal_uInt16 nMinDst, + const bool bLabelAlignmentPosAndSpaceModeActive ); + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual TextFrameIndex GetModelPositionForViewPoint(sal_uInt16 nOfst) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + + // Field cloner for SplitGlue + virtual SwFieldPortion *Clone( const OUString &rExpand ) const override; + virtual void FormatEOL( SwTextFormatInfo &rInf ) override; +}; + +class SwBulletPortion : public SwNumberPortion +{ +public: + SwBulletPortion( const sal_UCS4 cCh, + std::u16string_view rBulletFollowedBy, + std::unique_ptr<SwFont> pFnt, + const bool bLeft, + const bool bCenter, + const sal_uInt16 nMinDst, + const bool bLabelAlignmentPosAndSpaceModeActive ); +}; + +class SwGrfNumPortion : public SwNumberPortion +{ + std::unique_ptr<SvxBrushItem> m_pBrush; + tools::Long m_nId; // For StopAnimation + SwTwips m_nYPos; // _Always_ contains the current RelPos + SwTwips m_nGrfHeight; + sal_Int16 m_eOrient; +public: + SwGrfNumPortion( const OUString& rGraphicFollowedBy, + const SvxBrushItem* pGrfBrush, + OUString const & referer, + const SwFormatVertOrient* pGrfOrient, + const Size& rGrfSize, + const bool bLeft, + const bool bCenter, + const sal_uInt16 nMinDst, + const bool bLabelAlignmentPosAndSpaceModeActive ); + virtual ~SwGrfNumPortion() override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + + void SetBase( tools::Long nLnAscent, tools::Long nLnDescent, + tools::Long nFlyAscent, tools::Long nFlyDescent ); + + void StopAnimation( const OutputDevice* pOut ); + + bool IsAnimated() const { return m_bAnimated; } + void SetAnimated( bool bNew ) { m_bAnimated = bNew; } + void SetRelPos( SwTwips nNew ) { m_nYPos = nNew; } + void SetId( tools::Long nNew ) const + { const_cast<SwGrfNumPortion*>(this)->m_nId = nNew; } + SwTwips GetRelPos() const { return m_nYPos; } + SwTwips GetGrfHeight() const { return m_nGrfHeight; } + sal_Int16 GetOrient() const { return m_eOrient; } +}; + +/** + * Used in for asian layout specialities to display up to six characters + * in 2 rows and 2-3 columns. + * E.g.: <pre> + * A.. A.. A.B A.B A.B.C A.B.C + * ... ..B .C. C.D .D.E. D.E.F + * </pre> + */ +class SwCombinedPortion : public SwFieldPortion +{ + sal_uInt16 m_aPos[6]; // up to six X positions + o3tl::enumarray<SwFontScript,sal_uInt16> m_aWidth; // one width for every scripttype + SwFontScript m_aScrType[6]; // scripttype of every character + sal_uInt16 m_nUpPos; // the Y position of the upper baseline + sal_uInt16 m_nLowPos; // the Y position of the lower baseline + sal_uInt8 m_nProportion; // relative font height +public: + explicit SwCombinedPortion( const OUString &rExpand ); + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual sal_uInt16 GetViewWidth( const SwTextSizeInfo &rInf ) const override; +}; + +namespace sw::mark { class IFieldmark; } + +class SwFieldFormDropDownPortion : public SwFieldPortion +{ +public: + explicit SwFieldFormDropDownPortion(sw::mark::IFieldmark *pFieldMark, const OUString &rExpand) + : SwFieldPortion(rExpand) + , m_pFieldMark(pFieldMark) + { + } + // Field cloner for SplitGlue + virtual SwFieldPortion *Clone( const OUString &rExpand ) const override; + + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + +private: + sw::mark::IFieldmark* m_pFieldMark; +}; + +class SwFieldFormDatePortion : public SwFieldPortion +{ +public: + explicit SwFieldFormDatePortion(sw::mark::IFieldmark *pFieldMark, bool bStart) + : SwFieldPortion("") + , m_pFieldMark(pFieldMark) + , m_bStart(bStart) + { + } + // Field cloner for SplitGlue + virtual SwFieldPortion *Clone( const OUString &rExpand) const override; + + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + +private: + sw::mark::IFieldmark* m_pFieldMark; + bool m_bStart; +}; + +class SwJumpFieldPortion final : public SwFieldPortion +{ +public: + explicit SwJumpFieldPortion(OUString aExpand, OUString aHelp, std::unique_ptr<SwFont> pFont, + sal_uInt32 nFormat) + : SwFieldPortion(std::move(aExpand), std::move(pFont)) + , m_nFormat(nFormat) + , m_sHelp(std::move(aHelp)) + { + } + virtual SwFieldPortion* Clone(const OUString& rExpand) const override; + + virtual void Paint(const SwTextPaintInfo& rInf) const override; + +private: + sal_uInt32 m_nFormat; // SwJumpEditFormat from SwField::GetFormat() + OUString m_sHelp; + + bool DescribePDFControl(const SwTextPaintInfo& rInf) const; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porfly.cxx b/sw/source/core/text/porfly.cxx new file mode 100644 index 0000000000..14d1bf6eaa --- /dev/null +++ b/sw/source/core/text/porfly.cxx @@ -0,0 +1,423 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <dcontact.hxx> +#include <dflyobj.hxx> +#include <pam.hxx> +#include "portab.hxx" +#include <flyfrm.hxx> +#include <rootfrm.hxx> +#include <frmfmt.hxx> +#include <viewsh.hxx> +#include <textboxhelper.hxx> +#include <IDocumentState.hxx> + +#include <sal/log.hxx> +#include <fmtanchr.hxx> +#include <fmtflcnt.hxx> +#include <flyfrms.hxx> +#include <txatbase.hxx> +#include "porfly.hxx" +#include "porlay.hxx" +#include "inftxt.hxx" + +#include <sortedobjs.hxx> +#include <officecfg/Office/Common.hxx> +#include <PostItMgr.hxx> + +/** + * class SwFlyPortion => we expect a frame-locale SwRect! + */ + +void SwFlyPortion::Paint( const SwTextPaintInfo& ) const +{ +} + +bool SwFlyPortion::Format( SwTextFormatInfo &rInf ) +{ + OSL_ENSURE( GetFix() >= rInf.X(), "SwFlyPortion::Format" ); + + // tabs must be expanded + if( rInf.GetLastTab() ) + rInf.GetLastTab()->FormatEOL( rInf ); + + rInf.GetLast()->FormatEOL( rInf ); + + SetBlankWidth(0); + if (auto blankWidth = rInf.GetLast()->ExtraBlankWidth()) + { + // Swallow previous blank width + SetBlankWidth(blankWidth); + rInf.GetLast()->ExtraBlankWidth(0); + rInf.X(rInf.X() - blankWidth); // Step back + } + + PrtWidth( o3tl::narrowing<sal_uInt16>(GetFix() - rInf.X() + PrtWidth()) ); + if( !Width() ) + { + OSL_ENSURE( Width(), "+SwFlyPortion::Format: a fly is a fly is a fly" ); + Width(1); + } + + // resetting + rInf.SetFly( nullptr ); + rInf.Width( rInf.RealWidth() ); + rInf.GetParaPortion()->SetFly(); + + // trailing blank: + if( rInf.GetIdx() < TextFrameIndex(rInf.GetText().getLength()) + && TextFrameIndex(1) < rInf.GetIdx() + && !rInf.GetRest() + && ' ' == rInf.GetChar( rInf.GetIdx() ) + && ' ' != rInf.GetChar(rInf.GetIdx() - TextFrameIndex(1)) + && ( !rInf.GetLast() || !rInf.GetLast()->IsBreakPortion() ) ) + { + SetBlankWidth(GetBlankWidth() + rInf.GetTextSize(OUString(' ')).Width()); + SetLen(TextFrameIndex(1)); + } + + const sal_uInt16 nNewWidth = o3tl::narrowing<sal_uInt16>(rInf.X() + PrtWidth()); + if( rInf.Width() <= nNewWidth ) + { + Truncate(); + if( nNewWidth > rInf.Width() ) + { + PrtWidth( nNewWidth - rInf.Width() ); + SetFixWidth( PrtWidth() ); + } + return true; + } + return false; +} + +bool SwFlyCntPortion::Format( SwTextFormatInfo &rInf ) +{ + bool bFull = rInf.Width() < rInf.X() + PrtWidth(); + + if( bFull ) + { + // If the line is full, and the character-bound frame is at + // the beginning of a line + // If it is not possible to side step into a Fly + // "Begin of line" criteria ( ! rInf.X() ) has to be extended. + // KerningPortions at beginning of line, e.g., for grid layout + // must be considered. + const SwLinePortion* pLastPor = rInf.GetLast(); + const auto nLeft = ( pLastPor && + ( pLastPor->IsKernPortion() || + pLastPor->IsErgoSumPortion() ) ) ? + pLastPor->Width() : + 0; + + if( nLeft == rInf.X() && ! rInf.GetFly() ) + { + Width( rInf.Width() ); + bFull = false; // so that notes can still be placed in this line + } + else + { + if( !rInf.GetFly() ) + rInf.SetNewLine( true ); + Width(0); + SetAscent(0); + SetLen(TextFrameIndex(0)); + if( rInf.GetLast() ) + rInf.GetLast()->FormatEOL( rInf ); + + return bFull; + } + } + + rInf.GetParaPortion()->SetFly(); + return bFull; +} + +//TODO: improve documentation +/** move character-bound objects inside the given area + * + * This allows moving those objects from Master to Follow, or vice versa. + * + * @param pNew + * @param nStart + * @param nEnd + */ +void SwTextFrame::MoveFlyInCnt(SwTextFrame *pNew, + TextFrameIndex const nStart, TextFrameIndex const nEnd) +{ + SwSortedObjs *pObjs = GetDrawObjs(); + if ( nullptr == pObjs ) + return; + + for ( size_t i = 0; GetDrawObjs() && i < pObjs->size(); ++i ) + { + // Consider changed type of <SwSortedList> entries + SwAnchoredObject* pAnchoredObj = (*pObjs)[i]; + const SwFormatAnchor& rAnch = pAnchoredObj->GetFrameFormat().GetAnchor(); + if (rAnch.GetAnchorId() == RndStdIds::FLY_AS_CHAR) + { + const SwPosition* pPos = rAnch.GetContentAnchor(); + TextFrameIndex const nIndex(MapModelToViewPos(*pPos)); + if (nStart <= nIndex && nIndex < nEnd) + { + if ( auto pFlyFrame = pAnchoredObj->DynCastFlyFrame() ) + { + RemoveFly( pFlyFrame ); + pNew->AppendFly( pFlyFrame ); + } + else if ( dynamic_cast< const SwAnchoredDrawObject *>( pAnchoredObj ) != nullptr ) + { + RemoveDrawObj( *pAnchoredObj ); + pNew->AppendDrawObj( *pAnchoredObj ); + } + --i; + } + } + } +} + +TextFrameIndex SwTextFrame::CalcFlyPos( SwFrameFormat const * pSearch ) +{ + sw::MergedAttrIter iter(*this); + for (SwTextAttr const* pHt = iter.NextAttr(); pHt; pHt = iter.NextAttr()) + { + if( RES_TXTATR_FLYCNT == pHt->Which() ) + { + SwFrameFormat* pFrameFormat = pHt->GetFlyCnt().GetFrameFormat(); + if( pFrameFormat == pSearch ) + { + return TextFrameIndex(pHt->GetStart()); + } + } + } + OSL_ENSURE(false, "CalcFlyPos: Not Found!"); + return TextFrameIndex(COMPLETE_STRING); +} + +void sw::FlyContentPortion::Paint(const SwTextPaintInfo& rInf) const +{ + // Baseline output + // Re-paint everything at a CompletePaint call + SwRect aRepaintRect(rInf.GetPaintRect()); + + if(rInf.GetTextFrame()->IsRightToLeft()) + rInf.GetTextFrame()->SwitchLTRtoRTL(aRepaintRect); + + if(rInf.GetTextFrame()->IsVertical()) + rInf.GetTextFrame()->SwitchHorizontalToVertical(aRepaintRect); + + if(!((m_pFly->IsCompletePaint() || + m_pFly->getFrameArea().Overlaps(aRepaintRect)) && + SwFlyFrame::IsPaint(m_pFly->GetVirtDrawObj(), m_pFly->getRootFrame()->GetCurrShell()))) + return; + + SwRect aRect(m_pFly->getFrameArea()); + if(!m_pFly->IsCompletePaint()) + aRect.Intersection_(aRepaintRect); + + // GetFlyFrame() may change the layout mode at the output device. + { + SwLayoutModeModifier aLayoutModeModifier(*rInf.GetOut()); + m_pFly->PaintSwFrame(const_cast<vcl::RenderContext&>(*rInf.GetOut()), aRect); + + // track changes: cross out the image, if it is deleted + const SwFrame *pFrame = m_pFly->Lower(); + if ( GetAuthor() != std::string::npos && IsDeleted() && pFrame ) + { + SwRect aPaintRect( pFrame->GetPaintArea() ); + + const AntialiasingFlags nFormerAntialiasing( rInf.GetOut()->GetAntialiasing() ); + const bool bIsAntiAliasing = officecfg::Office::Common::Drawinglayer::AntiAliasing::get(); + if ( bIsAntiAliasing ) + const_cast<vcl::RenderContext&>(*rInf.GetOut()).SetAntialiasing(AntialiasingFlags::Enable); + tools::Long startX = aPaintRect.Left( ), endX = aPaintRect.Right(); + tools::Long startY = aPaintRect.Top( ), endY = aPaintRect.Bottom(); + const_cast<vcl::RenderContext&>(*rInf.GetOut()).SetLineColor( + SwPostItMgr::GetColorAnchor(GetAuthor()) ); + const_cast<vcl::RenderContext&>(*rInf.GetOut()).DrawLine(Point(startX, startY), Point(endX, endY)); + const_cast<vcl::RenderContext&>(*rInf.GetOut()).DrawLine(Point(startX, endY), Point(endX, startY)); + if ( bIsAntiAliasing ) + const_cast<vcl::RenderContext&>(*rInf.GetOut()).SetAntialiasing(nFormerAntialiasing); + } + } + const_cast<SwTextPaintInfo&>(rInf).GetRefDev()->SetLayoutMode(rInf.GetOut()->GetLayoutMode()); + + // As the OutputDevice might be anything, the font must be re-selected. + // Being in const method should not be a problem. + const_cast<SwTextPaintInfo&>(rInf).SelectFont(); + + assert(rInf.GetVsh()); + SAL_WARN_IF(rInf.GetVsh()->GetOut() != rInf.GetOut(), "sw.core", "SwFlyCntPortion::Paint: Outdev has changed"); + if(rInf.GetVsh()) + const_cast<SwTextPaintInfo&>(rInf).SetOut(rInf.GetVsh()->GetOut()); +} + +void sw::DrawFlyCntPortion::Paint(const SwTextPaintInfo&) const +{ + if(!m_pContact->GetAnchorFrame()) + { + // No direct positioning of the drawing object is needed + m_pContact->ConnectToLayout(); + } +} + +/** + * Use the dimensions of pFly->OutRect() + */ +SwFlyCntPortion::SwFlyCntPortion() + : m_bMax(false) + , m_bDeleted(false) + , m_nAuthor(std::string::npos) + , m_eAlign(sw::LineAlign::NONE) +{ + mnLineLength = TextFrameIndex(1); + SetWhichPor(PortionType::FlyCnt); +} + +sw::FlyContentPortion::FlyContentPortion(SwFlyInContentFrame* pFly) + : m_pFly(pFly) +{ + SAL_WARN_IF(!pFly, "sw.core", "SwFlyCntPortion::SwFlyCntPortion: no SwFlyInContentFrame!"); +} + +sw::DrawFlyCntPortion::DrawFlyCntPortion(SwFrameFormat const & rFormat) + : m_pContact(nullptr) +{ + rFormat.CallSwClientNotify(sw::CreatePortionHint(&m_pContact)); + assert(m_pContact); +} + +sw::FlyContentPortion* sw::FlyContentPortion::Create(const SwTextFrame& rFrame, SwFlyInContentFrame* pFly, const Point& rBase, tools::Long nLnAscent, tools::Long nLnDescent, tools::Long nFlyAsc, tools::Long nFlyDesc, AsCharFlags nFlags) +{ + auto pNew(new sw::FlyContentPortion(pFly)); + pNew->SetBase(rFrame, rBase, nLnAscent, nLnDescent, nFlyAsc, nFlyDesc, nFlags | AsCharFlags::UlSpace | AsCharFlags::Init); + return pNew; +} + +sw::DrawFlyCntPortion* sw::DrawFlyCntPortion::Create(const SwTextFrame& rFrame, SwFrameFormat const & rFormat, const Point& rBase, tools::Long nLnAscent, tools::Long nLnDescent, tools::Long nFlyAsc, tools::Long nFlyDesc, AsCharFlags nFlags) +{ + auto pNew(new DrawFlyCntPortion(rFormat)); + pNew->SetBase(rFrame, rBase, nLnAscent, nLnDescent, nFlyAsc, nFlyDesc, nFlags | AsCharFlags::UlSpace | AsCharFlags::Init); + return pNew; +} + +sw::DrawFlyCntPortion::~DrawFlyCntPortion() {}; +sw::FlyContentPortion::~FlyContentPortion() {}; + +SdrObject* sw::FlyContentPortion::GetSdrObj(const SwTextFrame&) +{ + return m_pFly->GetVirtDrawObj(); +} + +SdrObject* sw::DrawFlyCntPortion::GetSdrObj(const SwTextFrame& rFrame) +{ + SdrObject* pSdrObj; + // Determine drawing object ('master' or 'virtual') by frame + pSdrObj = m_pContact->GetDrawObjectByAnchorFrame(rFrame); + if(!pSdrObj) + { + SAL_WARN("sw.core", "SwFlyCntPortion::SetBase(..) - No drawing object found by <GetDrawContact()->GetDrawObjectByAnchorFrame( rFrame )>"); + pSdrObj = m_pContact->GetMaster(); + } + + // Call <SwAnchoredDrawObject::MakeObjPos()> to assure that flag at + // the <DrawFrameFormat> and at the <SwAnchoredDrawObject> instance are + // correctly set + if(pSdrObj) + m_pContact->GetAnchoredObj(pSdrObj)->MakeObjPos(); + return pSdrObj; +} + +/** + * After setting the RefPoints, the ascent needs to be recalculated + * because it is dependent on RelPos + * + * @param rBase CAUTION: needs to be an absolute value! + */ +void SwFlyCntPortion::SetBase( const SwTextFrame& rFrame, const Point &rBase, + tools::Long nLnAscent, tools::Long nLnDescent, + tools::Long nFlyAsc, tools::Long nFlyDesc, + AsCharFlags nFlags ) +{ + // Use new class to position object + // Determine drawing object + SdrObject* pSdrObj = GetSdrObj(rFrame); + if (!pSdrObj) + return; + + // position object + objectpositioning::SwAsCharAnchoredObjectPosition aObjPositioning( + *pSdrObj, + rBase, nFlags, + nLnAscent, nLnDescent, nFlyAsc, nFlyDesc ); + + // Scope of local variable <aObjPosInProgress> + { + SwObjPositioningInProgress aObjPosInProgress( *pSdrObj ); + aObjPositioning.CalcPosition(); + } + + if (auto pFormat = FindFrameFormat(pSdrObj)) + { + if (pFormat->GetOtherTextBoxFormats() + && pFormat->GetAnchor().GetAnchorId() == RndStdIds::FLY_AS_CHAR) + { + // TODO: Improve security with moving this sync call to other place, + // where it works for typing but not during layout calc. + const bool bModified = pFormat->GetDoc()->getIDocumentState().IsEnableSetModified(); + pFormat->GetDoc()->getIDocumentState().SetEnableSetModified(false); + SwTextBoxHelper::synchronizeGroupTextBoxProperty(SwTextBoxHelper::changeAnchor, pFormat, + pFormat->FindRealSdrObject()); + SwTextBoxHelper::synchronizeGroupTextBoxProperty(SwTextBoxHelper::syncTextBoxSize, + pFormat, pFormat->FindRealSdrObject()); + pFormat->GetDoc()->getIDocumentState().SetEnableSetModified(bModified); + } + } + + SetAlign( aObjPositioning.GetLineAlignment() ); + + m_aRef = aObjPositioning.GetAnchorPos(); + if( nFlags & AsCharFlags::Rotate ) + SvXSize( aObjPositioning.GetObjBoundRectInclSpacing().SSize() ); + else + SvLSize( aObjPositioning.GetObjBoundRectInclSpacing().SSize() ); + if( Height() ) + { + // GetRelPosY returns the relative position to baseline (if 0, the + // upper border of the FlyCnt if on the baseline of a line) + SwTwips nRelPos = aObjPositioning.GetRelPosY(); + if ( nRelPos < 0 ) + { + mnAscent = -nRelPos; + if( mnAscent > Height() ) + Height( mnAscent ); + } + else + { + mnAscent = 0; + Height( Height() + o3tl::narrowing<sal_uInt16>(nRelPos) ); + } + } + else + { + Height( 1 ); + mnAscent = 0; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porfly.hxx b/sw/source/core/text/porfly.hxx new file mode 100644 index 0000000000..a519c1109c --- /dev/null +++ b/sw/source/core/text/porfly.hxx @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <ascharanchoredobjectposition.hxx> + +#include "porglue.hxx" +#include <flyfrms.hxx> + +class SwDrawContact; +class SwTextFrame; +struct SwCursorMoveState; + +class SwFlyPortion : public SwFixPortion +{ + sal_uInt16 m_nBlankWidth; +public: + explicit SwFlyPortion( const SwRect &rFlyRect ) + : SwFixPortion(rFlyRect), m_nBlankWidth( 0 ) { SetWhichPor( PortionType::Fly ); } + sal_uInt16 GetBlankWidth( ) const { return m_nBlankWidth; } + void SetBlankWidth( const sal_uInt16 nNew ) { m_nBlankWidth = nNew; } + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; +}; + +/// This portion represents an as-character anchored fly (shape, frame, etc.) +class SwFlyCntPortion : public SwLinePortion +{ + Point m_aRef; // Relatively to this point we calculate the AbsPos + bool m_bMax; // Line adjustment and height == line height + bool m_bDeleted; // Part of tracked deletion: it needs strikethrough + size_t m_nAuthor; // Redline author for color of the strikethrough + sw::LineAlign m_eAlign; + + virtual SdrObject* GetSdrObj(const SwTextFrame&) =0; + +public: + SwFlyCntPortion(); + const Point& GetRefPoint() const { return m_aRef; } + bool IsMax() const { return m_bMax; } + bool IsDeleted() const { return m_bDeleted; } + void SetAuthor(size_t nAuthor) { m_nAuthor = nAuthor; } + size_t GetAuthor() const { return m_nAuthor; } + sw::LineAlign GetAlign() const { return m_eAlign; } + void SetAlign(sw::LineAlign eAlign) { m_eAlign = eAlign; } + void SetMax(bool bMax) { m_bMax = bMax; } + void SetDeleted(bool bDeleted) { m_bDeleted = bDeleted; } + void SetBase(const SwTextFrame& rFrame, const Point& rBase, tools::Long nLnAscent, tools::Long nLnDescent, tools::Long nFlyAscent, tools::Long nFlyDescent, AsCharFlags nFlags); + virtual bool Format(SwTextFormatInfo& rInf) override; +}; + +namespace sw +{ + class FlyContentPortion final : public SwFlyCntPortion + { + SwFlyInContentFrame* m_pFly; + virtual SdrObject* GetSdrObj(const SwTextFrame&) override; + public: + FlyContentPortion(SwFlyInContentFrame* pFly); + static FlyContentPortion* Create(const SwTextFrame& rFrame, SwFlyInContentFrame* pFly, const Point& rBase, tools::Long nAscent, tools::Long nDescent, tools::Long nFlyAsc, tools::Long nFlyDesc, AsCharFlags nFlags); + SwFlyInContentFrame* GetFlyFrame() { return m_pFly; } + void GetFlyCursorOfst(Point& rPoint, SwPosition& rPos, SwCursorMoveState* pCMS) const { m_pFly->GetModelPositionForViewPoint(&rPos, rPoint, pCMS); }; + virtual void Paint(const SwTextPaintInfo& rInf) const override; + virtual ~FlyContentPortion() override; + }; + class DrawFlyCntPortion final : public SwFlyCntPortion + { + SwDrawContact* m_pContact; + virtual SdrObject* GetSdrObj(const SwTextFrame&) override; + public: + DrawFlyCntPortion(SwFrameFormat const & rFormat); + static DrawFlyCntPortion* Create(const SwTextFrame& rFrame, SwFrameFormat const & rFormat, const Point& rBase, tools::Long nAsc, tools::Long nDescent, tools::Long nFlyAsc, tools::Long nFlyDesc, AsCharFlags nFlags); + virtual void Paint(const SwTextPaintInfo& rInf) const override; + virtual ~DrawFlyCntPortion() override; + }; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porftn.hxx b/sw/source/core/text/porftn.hxx new file mode 100644 index 0000000000..925a04f779 --- /dev/null +++ b/sw/source/core/text/porftn.hxx @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <sal/config.h> + +#include <string_view> + +#include "porfld.hxx" + +class SwTextFootnote; + +class SwFootnotePortion : public SwFieldPortion +{ + SwTextFootnote *m_pFootnote; + sal_uInt16 m_nOrigHeight; + // #i98418# + bool mbPreferredScriptTypeSet; + SwFontScript mnPreferredScriptType; +public: + SwFootnotePortion( const OUString &rExpand, SwTextFootnote *pFootnote, + sal_uInt16 nOrig = USHRT_MAX ); + sal_uInt16& Orig() { return m_nOrigHeight; } + + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + virtual SwPosSize GetTextSize( const SwTextSizeInfo &rInfo ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + + // #i98418# + void SetPreferredScriptType( SwFontScript nPreferredScriptType ); + + const SwTextFootnote* GetTextFootnote() const { return m_pFootnote; }; +}; + +class SwFootnoteNumPortion : public SwNumberPortion +{ +public: + SwFootnoteNumPortion( const OUString &rExpand, std::unique_ptr<SwFont> pFntL ) + : SwNumberPortion( rExpand, std::move(pFntL), true, false, 0, false ) + { SetWhichPor( PortionType::FootnoteNum ); } +}; + +/** + * Used in footnotes if they break across pages, master has this portion at the end. + * + * Created only in case Tools -> Footnotes and Endnotes sets the End of footnote to a non-empty + * value. + */ +class SwQuoVadisPortion : public SwFieldPortion +{ + OUString m_aErgo; +public: + SwQuoVadisPortion( const OUString &rExp, OUString aStr ); + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + + void SetNumber( const OUString& rStr ) { m_aErgo = rStr; } + const OUString& GetQuoText() const { return m_aExpand; } + const OUString &GetContText() const { return m_aErgo; } + + // Field cloner for SplitGlue + virtual SwFieldPortion *Clone( const OUString &rExpand ) const override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; +}; + +/** + * Used in footnotes if they break across pages, follow starts with this portion. + * + * Created only in case Tools -> Footnotes and Endnotes sets the Start of next page to a non-empty + * value. + */ +class SwErgoSumPortion : public SwFieldPortion +{ +public: + SwErgoSumPortion( const OUString &rExp, std::u16string_view rStr ); + virtual TextFrameIndex GetModelPositionForViewPoint(sal_uInt16 nOfst) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + + // Field cloner for SplitGlue + virtual SwFieldPortion *Clone( const OUString &rExpand ) const override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porglue.cxx b/sw/source/core/text/porglue.cxx new file mode 100644 index 0000000000..5f1fd2a7dc --- /dev/null +++ b/sw/source/core/text/porglue.cxx @@ -0,0 +1,285 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <swrect.hxx> +#include <viewopt.hxx> +#include "porglue.hxx" +#include "inftxt.hxx" +#include "porlay.hxx" +#include "porfly.hxx" +#include <comphelper/string.hxx> + +SwGluePortion::SwGluePortion( const sal_uInt16 nInitFixWidth ) + : m_nFixWidth( nInitFixWidth ) +{ + PrtWidth( m_nFixWidth ); + SetWhichPor( PortionType::Glue ); +} + +TextFrameIndex SwGluePortion::GetModelPositionForViewPoint(const sal_uInt16 nOfst) const +{ + // FIXME why nOfst > GetLen() ? is that supposed to be > Width() ? + if( !GetLen() || nOfst > sal_Int32(GetLen()) || !Width() ) + return SwLinePortion::GetModelPositionForViewPoint( nOfst ); + else + return TextFrameIndex(nOfst / (Width() / sal_Int32(GetLen()))); +} + +SwPosSize SwGluePortion::GetTextSize( const SwTextSizeInfo &rInf ) const +{ + if (TextFrameIndex(1) >= GetLen() || rInf.GetLen() > GetLen() || !Width() || !GetLen()) + return SwPosSize(*this); + else + return SwPosSize((Width() / sal_Int32(GetLen())) * sal_Int32(rInf.GetLen()), Height()); +} + +bool SwGluePortion::GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const +{ + if( GetLen() && rInf.OnWin() && + rInf.GetOpt().IsBlank() && rInf.IsNoSymbol() ) + { + OUStringBuffer aBuf(GetLen().get()); + comphelper::string::padToLength(aBuf, sal_Int32(GetLen()), CH_BULLET); + rText = aBuf.makeStringAndClear(); + return true; + } + return false; +} + +void SwGluePortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( !GetLen() ) + return; + + if( rInf.GetFont()->IsPaintBlank() ) + { + const sal_Int32 nCount = GetFixWidth() / sal_Int32(GetLen()); + OUStringBuffer aBuf(nCount); + comphelper::string::padToLength(aBuf, nCount, ' '); + OUString aText(aBuf.makeStringAndClear()); + SwTextPaintInfo aInf( rInf, &aText ); + aInf.DrawText(*this, TextFrameIndex(aText.getLength()), true); + } + + if( !(rInf.OnWin() && rInf.GetOpt().IsBlank() && rInf.IsNoSymbol()) ) + return; + +#if OSL_DEBUG_LEVEL > 0 + const sal_Unicode cChar = rInf.GetChar( rInf.GetIdx() ); + OSL_ENSURE( CH_BLANK == cChar || CH_BULLET == cChar, + "SwGluePortion::Paint: blank expected" ); +#endif + if (TextFrameIndex(1) == GetLen()) + { + OUString aBullet( CH_BULLET ); + SwPosSize aBulletSize( rInf.GetTextSize( aBullet ) ); + Point aPos( rInf.GetPos() ); + aPos.AdjustX((Width()/2) - (aBulletSize.Width()/2) ); + SwTextPaintInfo aInf( rInf, &aBullet ); + aInf.SetPos( aPos ); + SwTextPortion aBulletPor; + aBulletPor.Width( aBulletSize.Width() ); + aBulletPor.Height( aBulletSize.Height() ); + aBulletPor.SetAscent( GetAscent() ); + aInf.DrawText(aBulletPor, TextFrameIndex(aBullet.getLength()), true); + } + else + { + SwTextSlot aSlot( &rInf, this, true, false ); + rInf.DrawText( *this, rInf.GetLen(), true ); + } +} + +void SwGluePortion::MoveGlue( SwGluePortion *pTarget, const tools::Long nPrtGlue ) +{ + auto nPrt = std::min( nPrtGlue, GetPrtGlue() ); + if( 0 < nPrt ) + { + pTarget->AddPrtWidth( nPrt ); //TODO: overflow + SubPrtWidth( nPrt ); //TODO: overflow + } +} + +void SwGluePortion::Join( SwGluePortion *pVictim ) +{ + // The GluePortion is extracted and flushed away ... + AddPrtWidth( pVictim->PrtWidth() ); + SetLen( pVictim->GetLen() + GetLen() ); + if( Height() < pVictim->Height() ) + Height( pVictim->Height() ); + + AdjFixWidth(); + Cut( pVictim ); + delete pVictim; +} + +void SwGluePortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwGluePortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("fix-width"), BAD_CAST(OString::number(m_nFixWidth).getStr())); + + (void)xmlTextWriterEndElement(pWriter); +} + +/** + * We're expecting a frame-local SwRect! + */ +SwFixPortion::SwFixPortion( const SwRect &rRect ) + :SwGluePortion( sal_uInt16(rRect.Width()) ), m_nFix( sal_uInt16(rRect.Left()) ) +{ + Height( sal_uInt16(rRect.Height()) ); + SetWhichPor( PortionType::Fix ); +} + +SwFixPortion::SwFixPortion() + : SwGluePortion(0), m_nFix(0) +{ + SetWhichPor( PortionType::Fix ); +} + +void SwFixPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwFixPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("fix"), + BAD_CAST(OString::number(m_nFix).getStr())); + + (void)xmlTextWriterEndElement(pWriter); +} + +SwMarginPortion::SwMarginPortion() + :SwGluePortion( 0 ) +{ + SetWhichPor( PortionType::Margin ); +} + +/** + * In the outer loop all portions are inspected - the GluePortions + * at the end are processed first. + * The end is shifted forwardly till no more GluePortions remain. + * Always GluePortion-pairs (pLeft and pRight) are treated, where + * textportions between pLeft and pRight are moved at the back of + * pRight if pRight has enough Glue. With every move part of the + * Glue is transferred from pRight to pLeft. + * The next loop starts with the processed pLeft as pRight. + */ +void SwMarginPortion::AdjustRight( const SwLineLayout *pCurr ) +{ + SwGluePortion *pRight = nullptr; + bool bNoMove = nullptr != pCurr->GetpKanaComp(); + while( pRight != this ) + { + + // 1) We search for the left Glue + SwLinePortion *pPos = this; + SwGluePortion *pLeft = nullptr; + while( pPos ) + { + if( pPos->InFixMargGrp() ) + pLeft = static_cast<SwGluePortion*>(pPos); + pPos = pPos->GetNextPortion(); + if( pPos == pRight) + pPos = nullptr; + } + + // Two adjoining FlyPortions are merged + if( pRight && pLeft && pLeft->GetNextPortion() == pRight ) + { + pRight->MoveAllGlue( pLeft ); + pRight = nullptr; + } + auto nRightGlue = pRight && 0 < pRight->GetPrtGlue() + ? pRight->GetPrtGlue() : 0; + // 2) balance left and right Glue + // But not for tabs ... + if( pLeft && nRightGlue && !pRight->InTabGrp() ) + { + // pPrev is the portion immediately before pRight + SwLinePortion *pPrev = pRight->FindPrevPortion( pLeft ); + + if ( pRight->IsFlyPortion() && pRight->GetLen() ) + { + SwFlyPortion *pFly = static_cast<SwFlyPortion *>(pRight); + if ( pFly->GetBlankWidth() < nRightGlue ) + { + // Creating new TextPortion, that takes over the + // Blank previously swallowed by the Fly. + nRightGlue = nRightGlue - pFly->GetBlankWidth(); + pFly->SubPrtWidth( pFly->GetBlankWidth() ); + pFly->SetLen(TextFrameIndex(0)); + SwTextPortion *pNewPor = new SwTextPortion; + pNewPor->SetLen(TextFrameIndex(1)); + pNewPor->Height( pFly->Height() ); + pNewPor->Width( pFly->GetBlankWidth() ); + pFly->Insert( pNewPor ); + } + else + pPrev = pLeft; + } + while( pPrev != pLeft ) + { + if( bNoMove || pPrev->PrtWidth() >= nRightGlue || + pPrev->InHyphGrp() || pPrev->IsKernPortion() ) + { + // The portion before the pRight cannot be moved + // because no Glue is remaining. + // We set the break condition: + pPrev = pLeft; + } + else + { + nRightGlue = nRightGlue - pPrev->PrtWidth(); + // pPrev is moved behind pRight. For this the + // Glue value between pRight and pLeft gets balanced. + pRight->MoveGlue( pLeft, pPrev->PrtWidth() ); + // Now fix the linking of our portions. + SwLinePortion *pPrevPrev = pPrev->FindPrevPortion( pLeft ); + pPrevPrev->SetNextPortion( pRight ); + pPrev->SetNextPortion( pRight->GetNextPortion() ); + pRight->SetNextPortion( pPrev ); + if ( pPrev->GetNextPortion() && pPrev->InTextGrp() + && pPrev->GetNextPortion()->IsHolePortion() ) + { + SwHolePortion *pHolePor = + static_cast<SwHolePortion*>(pPrev->GetNextPortion()); + if ( !pHolePor->GetNextPortion() || + !pHolePor->GetNextPortion()->InFixMargGrp() ) + { + pPrev->AddPrtWidth( pHolePor->GetBlankWidth() ); + pPrev->SetLen(pPrev->GetLen() + TextFrameIndex(1)); + pPrev->SetNextPortion( pHolePor->GetNextPortion() ); + delete pHolePor; + } + } + pPrev = pPrevPrev; + } + } + } + // If no left Glue remains, we set the break condition. + pRight = pLeft ? pLeft : this; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porglue.hxx b/sw/source/core/text/porglue.hxx new file mode 100644 index 0000000000..aaaeec339f --- /dev/null +++ b/sw/source/core/text/porglue.hxx @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include "porlin.hxx" + +class SwRect; +class SwLineLayout; + +/// A glue portion is either a base class for other portions that want to have a certain width to +/// push text out (e.g. tab portions) or used to align SwQuoVadisPortion instances on the right for +/// footnotes. +class SwGluePortion : public SwLinePortion +{ +private: + sal_uInt16 m_nFixWidth; +public: + explicit SwGluePortion( const sal_uInt16 nInitFixWidth ); + + void Join( SwGluePortion *pVictim ); + + inline tools::Long GetPrtGlue() const; + sal_uInt16 GetFixWidth() const { return m_nFixWidth; } + void SetFixWidth( const sal_uInt16 nNew ) { m_nFixWidth = nNew; } + void MoveGlue( SwGluePortion *pTarget, const tools::Long nPrtGlue ); + inline void MoveAllGlue( SwGluePortion *pTarget ); + inline void MoveHalfGlue( SwGluePortion *pTarget ); + inline void AdjFixWidth(); + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual TextFrameIndex GetModelPositionForViewPoint(sal_uInt16 nOfst) const override; + virtual SwPosSize GetTextSize( const SwTextSizeInfo &rInfo ) const override; + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, TextFrameIndex& nOffset) const override; +}; + +class SwFixPortion : public SwGluePortion +{ + sal_uInt16 m_nFix; // The width offset in the line +public: + explicit SwFixPortion( const SwRect &rFlyRect ); + SwFixPortion(); + void SetFix( const sal_uInt16 nNewFix ) { m_nFix = nNewFix; } + sal_uInt16 GetFix() const { return m_nFix; } + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, TextFrameIndex& nOffset) const override; +}; + +class SwMarginPortion : public SwGluePortion +{ +public: + explicit SwMarginPortion(); + void AdjustRight( const SwLineLayout* pCurr ); +}; + +inline tools::Long SwGluePortion::GetPrtGlue() const +{ return Width() - m_nFixWidth; } + +// The FixWidth MUST NEVER be larger than the accumulated width! +inline void SwGluePortion::AdjFixWidth() +{ + if( m_nFixWidth > PrtWidth() ) + m_nFixWidth = PrtWidth(); +} + +inline void SwGluePortion::MoveAllGlue( SwGluePortion *pTarget ) +{ + MoveGlue( pTarget, GetPrtGlue() ); +} + +inline void SwGluePortion::MoveHalfGlue( SwGluePortion *pTarget ) +{ + MoveGlue( pTarget, GetPrtGlue() / 2 ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porhyph.hxx b/sw/source/core/text/porhyph.hxx new file mode 100644 index 0000000000..dc3c6651d0 --- /dev/null +++ b/sw/source/core/text/porhyph.hxx @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <sal/config.h> + +#include <string_view> + +#include "porexp.hxx" + +class SwHyphPortion : public SwExpandPortion +{ +public: + SwHyphPortion() + { + SetWhichPor( PortionType::Hyphen ); + } + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const override; +}; + +class SwHyphStrPortion : public SwHyphPortion +{ + OUString m_aExpand; +public: + explicit SwHyphStrPortion(std::u16string_view rStr) + : m_aExpand(OUString::Concat(rStr) + "-") + { + SetWhichPor( PortionType::HyphenStr ); + } + + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; +}; + +class SwSoftHyphPortion : public SwHyphPortion +{ + bool m_bExpand; + sal_uInt16 m_nViewWidth; + +public: + SwSoftHyphPortion(); + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + virtual SwLinePortion *Compress() override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual void FormatEOL( SwTextFormatInfo &rInf ) override; + void SetExpand( const bool bNew ) { m_bExpand = bNew; } + bool IsExpand() const { return m_bExpand; } + + virtual sal_uInt16 GetViewWidth( const SwTextSizeInfo &rInf ) const override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; +}; + +class SwSoftHyphStrPortion : public SwHyphStrPortion +{ +public: + explicit SwSoftHyphStrPortion( std::u16string_view rStr ); + virtual void Paint( const SwTextPaintInfo &rInf ) const override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porlay.cxx b/sw/source/core/text/porlay.cxx new file mode 100644 index 0000000000..cae54845be --- /dev/null +++ b/sw/source/core/text/porlay.cxx @@ -0,0 +1,2996 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <string_view> + +#include "porlay.hxx" +#include "itrform2.hxx" +#include "porglue.hxx" +#include "redlnitr.hxx" +#include "porfly.hxx" +#include "porrst.hxx" +#include "pormulti.hxx" +#include "pordrop.hxx" +#include <breakit.hxx> +#include <unicode/uchar.h> +#include <com/sun/star/i18n/ScriptType.hpp> +#include <com/sun/star/i18n/CharacterIteratorMode.hpp> +#include <com/sun/star/i18n/UnicodeType.hpp> +#include <com/sun/star/i18n/WordType.hpp> +#include <com/sun/star/i18n/XBreakIterator.hpp> +#include <paratr.hxx> +#include <sal/log.hxx> +#include <optional> +#include <editeng/adjustitem.hxx> +#include <editeng/charhiddenitem.hxx> +#include <editeng/escapementitem.hxx> +#include <svl/asiancfg.hxx> +#include <svl/languageoptions.hxx> +#include <tools/multisel.hxx> +#include <unotools/charclass.hxx> +#include <charfmt.hxx> +#include <docary.hxx> +#include <fmtanchr.hxx> +#include <redline.hxx> +#include <calbck.hxx> +#include <doc.hxx> +#include <swscanner.hxx> +#include <txatbase.hxx> +#include <IDocumentRedlineAccess.hxx> +#include <IDocumentSettingAccess.hxx> +#include <IDocumentContentOperations.hxx> +#include <IMark.hxx> +#include <sortedobjs.hxx> +#include <com/sun/star/frame/XModel.hpp> +#include <com/sun/star/text/XBookmarksSupplier.hpp> +#include <officecfg/Office/Common.hxx> +#include <comphelper/processfactory.hxx> +#include <docsh.hxx> +#include <unobookmark.hxx> +#include <unocrsrhelper.hxx> +#include <frmatr.hxx> +#include <vcl/kernarray.hxx> +#include <editeng/ulspitem.hxx> +#include <com/sun/star/rdf/Statement.hpp> +#include <com/sun/star/rdf/URI.hpp> +#include <com/sun/star/rdf/URIs.hpp> +#include <com/sun/star/rdf/XDocumentMetadataAccess.hpp> +#include <com/sun/star/rdf/XLiteral.hpp> +#include <com/sun/star/text/XTextContent.hpp> + +#include <unicode/ubidi.h> +#include <i18nutil/scripttypedetector.hxx> +#include <i18nutil/unicode.hxx> + +using namespace ::com::sun::star; +using namespace i18n::ScriptType; + +/* + https://www.khtt.net/en/page/1821/the-big-kashida-secret + + the rules of priorities that govern the addition of kashidas in Arabic text + made ... for ... Explorer 5.5 browser. + + The kashida justification is based on a connection priority scheme that + decides where kashidas are put automatically. + + This is how the software decides on kashida-inserting priorities: + 1. First it looks for characters with the highest priority in each word, + which means kashida-extensions will only been used in one position in each + word. Not more. + 2. The kashida will be connected to the character with the highest priority. + 3. If kashida connection opportunities are found with an equal level of + priority in one word, the kashida will be placed towards the end of the + word. + + The priority list of characters and the positioning is as follows: + 1. after a kashida that is manually placed in the text by the user, + 2. after a Seen or Sad (initial and medial form), + 3. before the final form of Taa Marbutah, Haa, Dal, + 4. before the final form of Alef, Tah Lam, Kaf and Gaf, + 5. before the preceding medial Baa of Ra, Ya and Alef Maqsurah, + 6. before the final form of Waw, Ain, Qaf and Fa, + 7. before the final form of other characters that can be connected. +*/ + +#define IS_JOINING_GROUP(c, g) ( u_getIntPropertyValue( (c), UCHAR_JOINING_GROUP ) == U_JG_##g ) +#define isAinChar(c) IS_JOINING_GROUP((c), AIN) +#define isAlefChar(c) IS_JOINING_GROUP((c), ALEF) +#define isDalChar(c) IS_JOINING_GROUP((c), DAL) +#define isFehChar(c) (IS_JOINING_GROUP((c), FEH) || IS_JOINING_GROUP((c), AFRICAN_FEH)) +#define isGafChar(c) IS_JOINING_GROUP((c), GAF) +#define isHehChar(c) IS_JOINING_GROUP((c), HEH) +#define isKafChar(c) IS_JOINING_GROUP((c), KAF) +#define isLamChar(c) IS_JOINING_GROUP((c), LAM) +#define isQafChar(c) (IS_JOINING_GROUP((c), QAF) || IS_JOINING_GROUP((c), AFRICAN_QAF)) +#define isRehChar(c) IS_JOINING_GROUP((c), REH) +#define isTahChar(c) IS_JOINING_GROUP((c), TAH) +#define isTehMarbutaChar(c) IS_JOINING_GROUP((c), TEH_MARBUTA) +#define isWawChar(c) IS_JOINING_GROUP((c), WAW) +#define isSeenOrSadChar(c) (IS_JOINING_GROUP((c), SAD) || IS_JOINING_GROUP((c), SEEN)) + +// Beh and characters that behave like Beh in medial form. +static bool isBehChar(sal_Unicode cCh) +{ + bool bRet = false; + switch (u_getIntPropertyValue(cCh, UCHAR_JOINING_GROUP)) + { + case U_JG_BEH: + case U_JG_NOON: + case U_JG_AFRICAN_NOON: + case U_JG_NYA: + case U_JG_YEH: + case U_JG_FARSI_YEH: + case U_JG_BURUSHASKI_YEH_BARREE: + bRet = true; + break; + default: + bRet = false; + break; + } + + return bRet; +} + +// Yeh and characters that behave like Yeh in final form. +static bool isYehChar(sal_Unicode cCh) +{ + bool bRet = false; + switch (u_getIntPropertyValue(cCh, UCHAR_JOINING_GROUP)) + { + case U_JG_YEH: + case U_JG_FARSI_YEH: + case U_JG_YEH_BARREE: + case U_JG_BURUSHASKI_YEH_BARREE: + case U_JG_YEH_WITH_TAIL: + bRet = true; + break; + default: + bRet = false; + break; + } + + return bRet; +} + +static bool isTransparentChar ( sal_Unicode cCh ) +{ + return u_getIntPropertyValue( cCh, UCHAR_JOINING_TYPE ) == U_JT_TRANSPARENT; +} + +// Checks if cCh + cNectCh builds a ligature (used for Kashidas) +static bool lcl_IsLigature( sal_Unicode cCh, sal_Unicode cNextCh ) +{ + // Lam + Alef + return ( isLamChar ( cCh ) && isAlefChar ( cNextCh )); +} + +// Checks if cCh is connectable to cPrevCh (used for Kashidas) +static bool lcl_ConnectToPrev( sal_Unicode cCh, sal_Unicode cPrevCh ) +{ + const int32_t nJoiningType = u_getIntPropertyValue( cPrevCh, UCHAR_JOINING_TYPE ); + bool bRet = nJoiningType != U_JT_RIGHT_JOINING && nJoiningType != U_JT_NON_JOINING; + + // check for ligatures cPrevChar + cChar + if( bRet ) + bRet = !lcl_IsLigature( cPrevCh, cCh ); + + return bRet; +} + +static bool lcl_HasStrongLTR ( std::u16string_view rText, sal_Int32 nStart, sal_Int32 nEnd ) + { + for( sal_Int32 nCharIdx = nStart; nCharIdx < nEnd; ++nCharIdx ) + { + const UCharDirection nCharDir = u_charDirection ( rText[ nCharIdx ] ); + if ( nCharDir == U_LEFT_TO_RIGHT || + nCharDir == U_LEFT_TO_RIGHT_EMBEDDING || + nCharDir == U_LEFT_TO_RIGHT_OVERRIDE ) + return true; + } + return false; + } + +// This is (meant to be) functionally equivalent to 'delete m_pNext' where +// deleting a SwLineLayout recursively deletes the owned m_pNext SwLineLayout. +// +// Here, instead of using a potentially deep stack, iterate over all the +// SwLineLayouts that would be deleted recursively and delete them linearly +void SwLineLayout::DeleteNext() +{ + if (!m_pNext) + return; + SwLineLayout* pNext = m_pNext; + do + { + SwLineLayout* pLastNext = pNext; + pNext = pNext->GetNext(); + pLastNext->SetNext(nullptr); + delete pLastNext; + } + while (pNext); +} + +void SwLineLayout::Height(const SwTwips nNew, const bool bText) +{ + SwPosSize::Height(nNew); + if (bText) + m_nTextHeight = nNew; +} + +// class SwLineLayout: This is the layout of a single line, which is made +// up of its dimension, the character count and the word spacing in the line. +// Line objects are managed in an own pool, in order to store them continuously +// in memory so that they are paged out together and don't fragment memory. +SwLineLayout::~SwLineLayout() +{ + Truncate(); + DeleteNext(); + m_pLLSpaceAdd.reset(); + m_pKanaComp.reset(); +} + +SwLinePortion *SwLineLayout::Insert( SwLinePortion *pIns ) +{ + // First attribute change: copy mass and length from *pIns into the first + // text portion + if( !mpNextPortion ) + { + if( GetLen() ) + { + mpNextPortion = SwTextPortion::CopyLinePortion(*this); + if( IsBlinking() ) + { + SetBlinking( false ); + } + } + else + { + SetNextPortion( pIns ); + return pIns; + } + } + // Call with scope or we'll end up with recursion! + return mpNextPortion->SwLinePortion::Insert( pIns ); +} + +SwLinePortion *SwLineLayout::Append( SwLinePortion *pIns ) +{ + // First attribute change: copy mass and length from *pIns into the first + // text portion + if( !mpNextPortion ) + mpNextPortion = SwTextPortion::CopyLinePortion(*this); + // Call with scope or we'll end up with recursion! + return mpNextPortion->SwLinePortion::Append( pIns ); +} + +// For special treatment of empty lines + +bool SwLineLayout::Format( SwTextFormatInfo &rInf ) +{ + if( GetLen() ) + return SwTextPortion::Format( rInf ); + + Height( rInf.GetTextHeight() ); + return true; +} + +// We collect all FlyPortions at the beginning of the line and make that a +// MarginPortion. +SwMarginPortion *SwLineLayout::CalcLeftMargin() +{ + SwMarginPortion *pLeft = (GetNextPortion() && GetNextPortion()->IsMarginPortion()) ? + static_cast<SwMarginPortion *>(GetNextPortion()) : nullptr; + if( !GetNextPortion() ) + SetNextPortion(SwTextPortion::CopyLinePortion(*this)); + if( !pLeft ) + { + pLeft = new SwMarginPortion; + pLeft->SetNextPortion( GetNextPortion() ); + SetNextPortion( pLeft ); + } + else + { + pLeft->Height( 0 ); + pLeft->Width( 0 ); + pLeft->SetLen(TextFrameIndex(0)); + pLeft->SetAscent( 0 ); + pLeft->SetNextPortion( nullptr ); + pLeft->SetFixWidth(0); + } + + SwLinePortion *pPos = pLeft->GetNextPortion(); + while( pPos ) + { + if( pPos->IsFlyPortion() ) + { + // The FlyPortion gets sucked out... + pLeft->Join( static_cast<SwGluePortion*>(pPos) ); + pPos = pLeft->GetNextPortion(); + if( GetpKanaComp() && !GetKanaComp().empty() ) + GetKanaComp().pop_front(); + } + else + pPos = nullptr; + } + return pLeft; +} + +void SwLineLayout::InitSpaceAdd() +{ + if ( !m_pLLSpaceAdd ) + CreateSpaceAdd(); + else + SetLLSpaceAdd( 0, 0 ); +} + +void SwLineLayout::CreateSpaceAdd( const tools::Long nInit ) +{ + m_pLLSpaceAdd.reset( new std::vector<tools::Long> ); + SetLLSpaceAdd( nInit, 0 ); +} + +// #i3952# Returns true if there are only blanks in [nStt, nEnd[ +// Used to implement IgnoreTabsAndBlanksForLineCalculation compat flag +static bool lcl_HasOnlyBlanks(std::u16string_view rText, TextFrameIndex nStt, TextFrameIndex nEnd) +{ + while ( nStt < nEnd ) + { + switch (rText[sal_Int32(nStt++)]) + { + case 0x0020: // SPACE + case 0x2002: // EN SPACE + case 0x2003: // EM SPACE + case 0x2005: // FOUR-PER-EM SPACE + case 0x3000: // IDEOGRAPHIC SPACE + continue; + default: + return false; + } + } + return true; +} + +// Swapped out from FormatLine() +void SwLineLayout::CalcLine( SwTextFormatter &rLine, SwTextFormatInfo &rInf ) +{ + const sal_uInt16 nLineWidth = rInf.RealWidth(); + + sal_uInt16 nFlyAscent = 0; + sal_uInt16 nFlyHeight = 0; + sal_uInt16 nFlyDescent = 0; + + // If this line has a clearing break, then this is the portion's height. + sal_uInt16 nBreakHeight = 0; + + bool bOnlyPostIts = true; + SetHanging( false ); + + bool bTmpDummy = !GetLen(); + SwFlyCntPortion* pFlyCnt = nullptr; + if( bTmpDummy ) + { + nFlyAscent = 0; + nFlyHeight = 0; + nFlyDescent = 0; + } + + // #i3952# + const bool bIgnoreBlanksAndTabsForLineHeightCalculation = + rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::IGNORE_TABS_AND_BLANKS_FOR_LINE_CALCULATION); + + bool bHasBlankPortion = false; + bool bHasOnlyBlankPortions = true; + bool bHasFlyPortion = false; + + if( mpNextPortion ) + { + SetContent( false ); + if( mpNextPortion->IsBreakPortion() ) + { + SetLen( mpNextPortion->GetLen() ); + if( GetLen() ) + bTmpDummy = false; + } + else + { + const SwTwips nLineHeight = Height(); + Init( GetNextPortion() ); + SwLinePortion *pPos = mpNextPortion; + SwLinePortion *pLast = this; + sal_uInt16 nMaxDescent = 0; + + // A group is a segment in the portion chain of pCurr or a fixed + // portion spanning to the end or the next fixed portion + while( pPos ) + { + SAL_WARN_IF( PortionType::NONE == pPos->GetWhichPor(), + "sw.core", "SwLineLayout::CalcLine: don't use SwLinePortions !" ); + + // Null portions are eliminated. They can form if two FlyFrames + // overlap. + // coverity[deref_arg] - "Cut" means next "GetNextPortion" returns a different Portion + if( !pPos->Compress() ) + { + // Only take over Height and Ascent if the rest of the line + // is empty. + if( !pPos->GetNextPortion() ) + { + if( !Height() ) + Height( pPos->Height(), false ); + if( !GetAscent() ) + SetAscent( pPos->GetAscent() ); + } + SwLinePortion* pPortion = pLast->Cut( pPos ); + rLine.ClearIfIsFirstOfBorderMerge(pPortion); + delete pPortion; + pPos = pLast->GetNextPortion(); + continue; + } + + TextFrameIndex const nPorSttIdx = rInf.GetLineStart() + mnLineLength; + mnLineLength += pPos->GetLen(); + AddPrtWidth( pPos->Width() ); + + // #i3952# + if (bIgnoreBlanksAndTabsForLineHeightCalculation && !rInf.GetLineStart()) + { + if ( pPos->InTabGrp() || pPos->IsHolePortion() || + ( pPos->IsTextPortion() && + lcl_HasOnlyBlanks( rInf.GetText(), nPorSttIdx, nPorSttIdx + pPos->GetLen() ) ) ) + { + pLast = pPos; + pPos = pPos->GetNextPortion(); + bHasBlankPortion = true; + continue; + } + } + + // Ignore drop portion height + // tdf#130804 ... and bookmark portions + if ((pPos->IsDropPortion() && static_cast<SwDropPortion*>(pPos)->GetLines() > 1) + || pPos->GetWhichPor() == PortionType::Bookmark) + { + pLast = pPos; + pPos = pPos->GetNextPortion(); + continue; + } + + bHasOnlyBlankPortions = false; + + // We had an attribute change: Sum up/build maxima of length and mass + + SwTwips nPosHeight = pPos->Height(); + SwTwips nPosAscent = pPos->GetAscent(); + + SAL_WARN_IF( nPosHeight < nPosAscent, + "sw.core", "SwLineLayout::CalcLine: bad ascent or height" ); + + if( pPos->IsHangingPortion() ) + { + SetHanging(true); + rInf.GetParaPortion()->SetMargin(); + } + else if( !bHasFlyPortion && ( pPos->IsFlyCntPortion() || pPos->IsFlyPortion() ) ) + bHasFlyPortion = true; + + // A line break portion only influences the height of the line in case it's the only + // portion in the line, except when it's a clearing break. + bool bClearingBreak = false; + if (pPos->IsBreakPortion()) + { + auto pBreakPortion = static_cast<SwBreakPortion*>(pPos); + bClearingBreak = pBreakPortion->GetClear() != SwLineBreakClear::NONE; + nBreakHeight = nPosHeight; + } + if (!(pPos->IsBreakPortion() && !bClearingBreak) || !Height()) + { + if (!pPos->IsPostItsPortion()) bOnlyPostIts = false; + + if( bTmpDummy && !mnLineLength ) + { + if( pPos->IsFlyPortion() ) + { + if( nFlyHeight < nPosHeight ) + nFlyHeight = nPosHeight; + if( nFlyAscent < nPosAscent ) + nFlyAscent = nPosAscent; + if( nFlyDescent < nPosHeight - nPosAscent ) + nFlyDescent = nPosHeight - nPosAscent; + } + else + { + if( pPos->InNumberGrp() ) + { + sal_uInt16 nTmp = rInf.GetFont()->GetAscent( + rInf.GetVsh(), *rInf.GetOut() ); + if( nTmp > nPosAscent ) + { + nPosHeight += nTmp - nPosAscent; + nPosAscent = nTmp; + } + nTmp = rInf.GetFont()->GetHeight( rInf.GetVsh(), + *rInf.GetOut() ); + if( nTmp > nPosHeight ) + nPosHeight = nTmp; + } + Height( nPosHeight, false ); + mnAscent = nPosAscent; + nMaxDescent = nPosHeight - nPosAscent; + } + } + else if( !pPos->IsFlyPortion() ) + { + if( Height() < nPosHeight ) + { + // Height is set to 0 when Init() is called. + if (bIgnoreBlanksAndTabsForLineHeightCalculation && pPos->IsFlyCntPortion()) + // Compat flag set: take the line height, if it's larger. + Height(std::max(nPosHeight, nLineHeight), false); + else + // Just care about the portion height. + Height(nPosHeight, pPos->IsTextPortion()); + } + SwFlyCntPortion* pAsFly(nullptr); + if(pPos->IsFlyCntPortion()) + pAsFly = static_cast<SwFlyCntPortion*>(pPos); + if( pAsFly || ( pPos->IsMultiPortion() + && static_cast<SwMultiPortion*>(pPos)->HasFlyInContent() ) ) + rLine.SetFlyInCntBase(); + if(pAsFly && pAsFly->GetAlign() != sw::LineAlign::NONE) + { + pAsFly->SetMax(false); + if( !pFlyCnt || pPos->Height() > pFlyCnt->Height() ) + pFlyCnt = pAsFly; + } + else + { + if( mnAscent < nPosAscent ) + mnAscent = nPosAscent; + if( nMaxDescent < nPosHeight - nPosAscent ) + nMaxDescent = nPosHeight - nPosAscent; + } + } + } + else if( pPos->GetLen() ) + bTmpDummy = false; + + if( !HasContent() && !pPos->InNumberGrp() ) + { + if ( pPos->InExpGrp() ) + { + OUString aText; + if( pPos->GetExpText( rInf, aText ) && !aText.isEmpty() ) + SetContent(true); + } + else if( ( pPos->InTextGrp() || pPos->IsMultiPortion() ) && + pPos->GetLen() ) + SetContent(true); + } + + bTmpDummy &= !HasContent() && ( !pPos->Width() || pPos->IsFlyPortion() ); + + pLast = pPos; + pPos = pPos->GetNextPortion(); + } + + if( pFlyCnt ) + { + if( pFlyCnt->Height() == Height() ) + { + pFlyCnt->SetMax( true ); + if( Height() > nMaxDescent + mnAscent ) + { + if( sw::LineAlign::BOTTOM == pFlyCnt->GetAlign() ) + mnAscent = Height() - nMaxDescent; + else if( sw::LineAlign::CENTER == pFlyCnt->GetAlign() ) + mnAscent = ( Height() + mnAscent - nMaxDescent ) / 2; + } + pFlyCnt->SetAscent( mnAscent ); + } + } + + if( bTmpDummy && nFlyHeight ) + { + mnAscent = nFlyAscent; + if( nFlyDescent > nFlyHeight - nFlyAscent ) + Height( nFlyHeight + nFlyDescent, false ); + else + { + if (nBreakHeight > nFlyHeight) + { + // The line has no content, but it has a clearing break: then the line + // height is not only the intersection of the fly and line's rectangle, but + // also includes the clearing break's height. + Height(nBreakHeight, false); + } + else + { + Height(nFlyHeight, false); + } + } + } + else if( nMaxDescent > Height() - mnAscent ) + Height( nMaxDescent + mnAscent, false ); + + if( bOnlyPostIts && !( bHasBlankPortion && bHasOnlyBlankPortions ) ) + { + Height( rInf.GetFont()->GetHeight( rInf.GetVsh(), *rInf.GetOut() ) ); + mnAscent = rInf.GetFont()->GetAscent( rInf.GetVsh(), *rInf.GetOut() ); + } + } + } + else + { + SetContent( !bTmpDummy ); + + // #i3952# + if ( bIgnoreBlanksAndTabsForLineHeightCalculation && + lcl_HasOnlyBlanks( rInf.GetText(), rInf.GetLineStart(), rInf.GetLineStart() + GetLen() ) ) + { + bHasBlankPortion = true; + } + } + + // #i3952# Whitespace does not increase line height + if ( bHasBlankPortion && bHasOnlyBlankPortions ) + { + sal_uInt16 nTmpAscent = GetAscent(); + sal_uInt16 nTmpHeight = Height(); + rLine.GetAttrHandler().GetDefaultAscentAndHeight( rInf.GetVsh(), *rInf.GetOut(), nTmpAscent, nTmpHeight ); + + short nEscapement = rLine.GetAttrHandler().GetFont()->GetEscapement(); + if (GetAscent() && Height() && !nTmpAscent && !nTmpHeight + && (nEscapement == DFLT_ESC_AUTO_SUPER || nEscapement == DFLT_ESC_AUTO_SUB)) + { + // We already had a calculated ascent + height, it would be cleared, automatic + // sub/superscript is set and we have no content. In this case it makes no sense to + // clear the old, correct ascent/height. + nTmpAscent = GetAscent(); + nTmpHeight = Height(); + } + + if (nTmpAscent < GetAscent() || GetAscent() <= 0) + SetAscent(nTmpAscent); + if (nTmpHeight < Height() || Height() <= 0) + Height(nTmpHeight, false); + } + + // Robust: + if( nLineWidth < Width() ) + Width( nLineWidth ); + SAL_WARN_IF( nLineWidth < Width(), "sw.core", "SwLineLayout::CalcLine: line is bursting" ); + SetDummy( bTmpDummy ); + std::pair<SwTextNode const*, sal_Int32> const start( + rInf.GetTextFrame()->MapViewToModel(rLine.GetStart())); + std::pair<SwTextNode const*, sal_Int32> const end( + rInf.GetTextFrame()->MapViewToModel(rLine.GetEnd())); + bool bHasRedline = rLine.GetRedln(); + if( bHasRedline ) + { + OUString sRedlineText; + bool bHasRedlineEnd; + enum RedlineType eRedlineEnd; + bHasRedline = rLine.GetRedln()->CheckLine(start.first->GetIndex(), start.second, + end.first->GetIndex(), end.second, sRedlineText, bHasRedlineEnd, eRedlineEnd); + if( bHasRedline ) + { + SetRedlineText( sRedlineText ); + if( bHasRedlineEnd ) + SetRedlineEnd( bHasRedlineEnd ); + if( eRedlineEnd != RedlineType::None ) + SetRedlineEndType( eRedlineEnd ); + } + } + SetRedline( bHasRedline ); + + // redlining: set crossing out for deleted anchored objects + if ( !bHasFlyPortion ) + return; + + SwLinePortion *pPos = mpNextPortion; + TextFrameIndex nLineLength; + while ( pPos ) + { + TextFrameIndex const nPorSttIdx = rInf.GetLineStart() + nLineLength; + nLineLength += pPos->GetLen(); + // anchored as characters + if( pPos->IsFlyCntPortion() ) + { + bool bDeleted = false; + size_t nAuthor = std::string::npos; + if ( bHasRedline ) + { + OUString sRedlineText; + bool bHasRedlineEnd; + enum RedlineType eRedlineEnd; + std::pair<SwTextNode const*, sal_Int32> const flyStart( + rInf.GetTextFrame()->MapViewToModel(nPorSttIdx)); + bool bHasFlyRedline = rLine.GetRedln()->CheckLine(flyStart.first->GetIndex(), + flyStart.second, flyStart.first->GetIndex(), flyStart.second, sRedlineText, + bHasRedlineEnd, eRedlineEnd, /*pAuthorAtPos=*/&nAuthor); + bDeleted = bHasFlyRedline && eRedlineEnd == RedlineType::Delete; + } + static_cast<SwFlyCntPortion*>(pPos)->SetDeleted(bDeleted); + static_cast<SwFlyCntPortion*>(pPos)->SetAuthor(nAuthor); + } + // anchored to characters + else if ( pPos->IsFlyPortion() ) + { + const IDocumentRedlineAccess& rIDRA = + rInf.GetTextFrame()->GetDoc().getIDocumentRedlineAccess(); + SwSortedObjs *pObjs = rInf.GetTextFrame()->GetDrawObjs(); + if ( pObjs && IDocumentRedlineAccess::IsShowChanges( rIDRA.GetRedlineFlags() ) ) + { + for ( size_t i = 0; rInf.GetTextFrame()->GetDrawObjs() && i < pObjs->size(); ++i ) + { + SwAnchoredObject* pAnchoredObj = (*rInf.GetTextFrame()->GetDrawObjs())[i]; + if ( auto pFly = pAnchoredObj->DynCastFlyFrame() ) + { + bool bDeleted = false; + size_t nAuthor = std::string::npos; + const SwFormatAnchor& rAnchor = pAnchoredObj->GetFrameFormat().GetAnchor(); + if ( rAnchor.GetAnchorId() == RndStdIds::FLY_AT_CHAR ) + { + SwPosition aAnchor = *rAnchor.GetContentAnchor(); + SwRedlineTable::size_type n = 0; + const SwRangeRedline* pFnd = + rIDRA.GetRedlineTable().FindAtPosition( aAnchor, n ); + if ( pFnd && RedlineType::Delete == pFnd->GetType() ) + { + bDeleted = true; + nAuthor = pFnd->GetAuthor(); + } + } + pFly->SetDeleted(bDeleted); + pFly->SetAuthor(nAuthor); + } + } + } + } + pPos = pPos->GetNextPortion(); + } +} + +// #i47162# - add optional parameter <_bNoFlyCntPorAndLinePor> +// to control, if the fly content portions and line portion are considered. +void SwLineLayout::MaxAscentDescent( SwTwips& _orAscent, + SwTwips& _orDescent, + SwTwips& _orObjAscent, + SwTwips& _orObjDescent, + const SwLinePortion* _pDontConsiderPortion, + const bool _bNoFlyCntPorAndLinePor ) const +{ + _orAscent = 0; + _orDescent = 0; + _orObjAscent = 0; + _orObjDescent = 0; + + const SwLinePortion* pTmpPortion = this; + if ( !pTmpPortion->GetLen() && pTmpPortion->GetNextPortion() ) + { + pTmpPortion = pTmpPortion->GetNextPortion(); + } + + while ( pTmpPortion ) + { + if ( !pTmpPortion->IsBreakPortion() && !pTmpPortion->IsFlyPortion() && + // tdf#130804 ignore bookmark portions + pTmpPortion->GetWhichPor() != PortionType::Bookmark && + ( !_bNoFlyCntPorAndLinePor || + ( !pTmpPortion->IsFlyCntPortion() && + !(pTmpPortion == this && pTmpPortion->GetNextPortion() ) ) ) ) + { + SwTwips nPortionAsc = pTmpPortion->GetAscent(); + SwTwips nPortionDesc = pTmpPortion->Height() - nPortionAsc; + + const bool bFlyCmp = pTmpPortion->IsFlyCntPortion() ? + static_cast<const SwFlyCntPortion*>(pTmpPortion)->IsMax() : + ( pTmpPortion != _pDontConsiderPortion ); + + if ( bFlyCmp ) + { + _orObjAscent = std::max( _orObjAscent, nPortionAsc ); + _orObjDescent = std::max( _orObjDescent, nPortionDesc ); + } + + if ( !pTmpPortion->IsFlyCntPortion() && !pTmpPortion->IsGrfNumPortion() ) + { + _orAscent = std::max( _orAscent, nPortionAsc ); + _orDescent = std::max( _orDescent, nPortionDesc ); + } + } + pTmpPortion = pTmpPortion->GetNextPortion(); + } +} + +void SwLineLayout::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwLineLayout")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterEndElement(pWriter); +} + +void SwLineLayout::ResetFlags() +{ + m_bFormatAdj = m_bDummy = m_bEndHyph = m_bMidHyph = m_bFly + = m_bRest = m_bBlinking = m_bClipping = m_bContent = m_bRedline + = m_bRedlineEnd = m_bForcedLeftMargin = m_bHanging = false; + m_eRedlineEnd = RedlineType::None; +} + +SwLineLayout::SwLineLayout() + : m_pNext( nullptr ), + m_nRealHeight( 0 ), + m_nTextHeight( 0 ), + m_bUnderscore( false ) +{ + ResetFlags(); + SetWhichPor( PortionType::Lay ); +} + +SwLinePortion *SwLineLayout::GetFirstPortion() const +{ + const SwLinePortion *pRet = mpNextPortion ? mpNextPortion : this; + return const_cast<SwLinePortion*>(pRet); +} + +SwCharRange &SwCharRange::operator+=(const SwCharRange &rRange) +{ + if (TextFrameIndex(0) != rRange.m_nLen) + { + if (TextFrameIndex(0) == m_nLen) { + m_nStart = rRange.m_nStart; + m_nLen = rRange.m_nLen ; + } + else { + if(rRange.m_nStart + rRange.m_nLen > m_nStart + m_nLen) { + m_nLen = rRange.m_nStart + rRange.m_nLen - m_nStart; + } + if(rRange.m_nStart < m_nStart) { + m_nLen += m_nStart - rRange.m_nStart; + m_nStart = rRange.m_nStart; + } + } + } + return *this; +} + +SwScriptInfo::SwScriptInfo() + : m_nInvalidityPos(0) + , m_nDefaultDir(0) +{ +}; + +SwScriptInfo::~SwScriptInfo() +{ +} + +// Converts i18n Script Type (LATIN, ASIAN, COMPLEX, WEAK) to +// Sw Script Types (SwFontScript::Latin, SwFontScript::CJK, SwFontScript::CTL), used to identify the font +static SwFontScript lcl_ScriptToFont(sal_uInt16 const nScript) +{ + switch ( nScript ) { + case i18n::ScriptType::LATIN : return SwFontScript::Latin; + case i18n::ScriptType::ASIAN : return SwFontScript::CJK; + case i18n::ScriptType::COMPLEX : return SwFontScript::CTL; + } + + OSL_FAIL( "Somebody tells lies about the script type!" ); + return SwFontScript::Latin; +} + +SwFontScript SwScriptInfo::WhichFont(TextFrameIndex const nIdx) const +{ + const sal_uInt16 nScript(ScriptType(nIdx)); + return lcl_ScriptToFont(nScript); +} + +SwFontScript SwScriptInfo::WhichFont(sal_Int32 nIdx, OUString const& rText) +{ + const sal_uInt16 nScript(g_pBreakIt->GetRealScriptOfText(rText, nIdx)); + return lcl_ScriptToFont(nScript); +} + +static Color getBookmarkColor(const SwTextNode& rNode, const sw::mark::IBookmark* pBookmark) +{ + // search custom color in metadata, otherwise use COL_TRANSPARENT; + Color c = COL_TRANSPARENT; + + try + { + SwDoc& rDoc = const_cast<SwDoc&>(rNode.GetDoc()); + const rtl::Reference< SwXBookmark > xRef = SwXBookmark::CreateXBookmark(rDoc, + const_cast<sw::mark::IMark*>(static_cast<const sw::mark::IMark*>(pBookmark))); + const css::uno::Reference<css::rdf::XResource> xSubject(xRef); + uno::Reference<frame::XModel> xModel = rDoc.GetDocShell()->GetBaseModel(); + + static uno::Reference< uno::XComponentContext > xContext( + ::comphelper::getProcessComponentContext()); + + static uno::Reference< rdf::XURI > xODF_SHADING( + rdf::URI::createKnown(xContext, rdf::URIs::LO_EXT_SHADING), uno::UNO_SET_THROW); + + uno::Reference<rdf::XDocumentMetadataAccess> xDocumentMetadataAccess( + rDoc.GetDocShell()->GetBaseModel(), uno::UNO_QUERY); + const uno::Reference<rdf::XRepository>& xRepository = + xDocumentMetadataAccess->getRDFRepository(); + const uno::Reference<container::XEnumeration> xEnum( + xRepository->getStatements(xSubject, xODF_SHADING, nullptr), uno::UNO_SET_THROW); + + rdf::Statement stmt; + if ( xEnum->hasMoreElements() && (xEnum->nextElement() >>= stmt) ) + { + const uno::Reference<rdf::XLiteral> xObject(stmt.Object, uno::UNO_QUERY); + if ( xObject.is() ) + c = Color::STRtoRGB(xObject->getValue()); + } + } + catch (const lang::IllegalArgumentException&) + { + } + + return c; +} + +static void InitBookmarks( + std::optional<std::vector<sw::Extent>::const_iterator> oPrevIter, + std::vector<sw::Extent>::const_iterator iter, + std::vector<sw::Extent>::const_iterator const end, + TextFrameIndex nOffset, + std::vector<std::pair<sw::mark::IBookmark const*, SwScriptInfo::MarkKind>> & rBookmarks, + std::vector<std::tuple<TextFrameIndex, SwScriptInfo::MarkKind, Color, OUString>> & o_rBookmarks) +{ + SwTextNode const*const pNode(iter->pNode); + for (auto const& it : rBookmarks) + { + assert(iter->pNode == pNode || pNode->GetIndex() < iter->pNode->GetIndex()); + assert(!oPrevIter || (*oPrevIter)->pNode->GetIndex() <= pNode->GetIndex()); + + // search for custom bookmark boundary mark color + Color c = getBookmarkColor(*pNode, it.first); + + switch (it.second) + { + case SwScriptInfo::MarkKind::Start: + { + // SwUndoSaveContent::DelContentIndex() is rather messy but + // apparently bookmarks "on the edge" are deleted if + // * point: equals start-of-selection (not end-of-selection) + // * expanded: one position equals edge of selection + // and other does not (is inside) + // interesting case: if end[/start] of the mark is on the + // start of first[/end of last] extent, and the other one + // is outside this merged paragraph, is it deleted or not? + // assume "no" because the line break it contains isn't deleted. + SwPosition const& rStart(it.first->GetMarkStart()); + SwPosition const& rEnd(it.first->GetMarkEnd()); + assert(&rStart.GetNode() == pNode); + while (iter != end) + { + if (&rStart.GetNode() != iter->pNode // iter moved to next node + || rStart.GetContentIndex() < iter->nStart) + { + if (rEnd.GetNodeIndex() < iter->pNode->GetIndex() + || (&rEnd.GetNode() == iter->pNode && rEnd.GetContentIndex() <= iter->nStart)) + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back(nOffset, it.second, c, it.first->GetName()); + break; + } + } + else if (rStart.GetContentIndex() <= iter->nEnd) + { + auto const iterNext(iter + 1); + if (rStart.GetContentIndex() == iter->nEnd + && (iterNext == end + ? &rEnd.GetNode() == iter->pNode + : (rEnd.GetNodeIndex() < iterNext->pNode->GetIndex() + || (&rEnd.GetNode() == iterNext->pNode && rEnd.GetContentIndex() < iterNext->nStart)))) + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back( + nOffset + TextFrameIndex(rStart.GetContentIndex() - iter->nStart), + it.second, c, it.first->GetName()); + break; + } + } + else + { + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + oPrevIter = iter; + ++iter; // bookmarks are sorted... + } + } + if (iter == end) + { + if (pNode->GetIndex() < rEnd.GetNodeIndex()) // pNode is last node of merged + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back(nOffset, it.second, c, it.first->GetName()); + } + } + break; + } + case SwScriptInfo::MarkKind::End: + { + SwPosition const& rEnd(it.first->GetMarkEnd()); + assert(&rEnd.GetNode() == pNode); + while (true) + { + if (iter == end + || &rEnd.GetNode() != iter->pNode // iter moved to next node + || rEnd.GetContentIndex() <= iter->nStart) + { + SwPosition const& rStart(it.first->GetMarkStart()); + // oPrevIter may point to pNode or a preceding node + if (oPrevIter + ? ((*oPrevIter)->pNode->GetIndex() < rStart.GetNodeIndex() + || ((*oPrevIter)->pNode == &rStart.GetNode() + && ((iter != end && &rEnd.GetNode() == iter->pNode && rEnd.GetContentIndex() == iter->nStart) + ? (*oPrevIter)->nEnd < rStart.GetContentIndex() + : (*oPrevIter)->nEnd <= rStart.GetContentIndex()))) + : rStart.GetNode() == rEnd.GetNode()) + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back(nOffset, it.second, c, it.first->GetName()); + break; + } + } + else if (rEnd.GetContentIndex() <= iter->nEnd) + { + o_rBookmarks.emplace_back( + nOffset + TextFrameIndex(rEnd.GetContentIndex() - iter->nStart), + it.second, c, it.first->GetName()); + break; + } + else + { + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + oPrevIter = iter; + ++iter; + } + } + break; + } + case SwScriptInfo::MarkKind::Point: + { + SwPosition const& rPos(it.first->GetMarkPos()); + assert(&rPos.GetNode() == pNode); + while (iter != end) + { + if (&rPos.GetNode() != iter->pNode // iter moved to next node + || rPos.GetContentIndex() < iter->nStart) + { + break; // deleted - skip it + } + else if (rPos.GetContentIndex() <= iter->nEnd) + { + if (rPos.GetContentIndex() == iter->nEnd + && rPos.GetContentIndex() != iter->pNode->Len()) + { + break; // deleted - skip it + } + else + { + o_rBookmarks.emplace_back( + nOffset + TextFrameIndex(rPos.GetContentIndex() - iter->nStart), + it.second, c, it.first->GetName()); + } + break; + } + else + { + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + oPrevIter = iter; + ++iter; + } + } + break; + } + } + if (iter == end) + { + break; // remaining marks are hidden + } + } +} + +// searches for script changes in rText and stores them +void SwScriptInfo::InitScriptInfo(const SwTextNode& rNode, + sw::MergedPara const*const pMerged) +{ + InitScriptInfo( rNode, pMerged, m_nDefaultDir == UBIDI_RTL ); +} + +void SwScriptInfo::InitScriptInfo(const SwTextNode& rNode, + sw::MergedPara const*const pMerged, bool bRTL) +{ + assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is()); + + const OUString& rText(pMerged ? pMerged->mergedText : rNode.GetText()); + + // HIDDEN TEXT INFORMATION + + m_Bookmarks.clear(); + m_HiddenChg.clear(); + if (pMerged) + { + SwTextNode const* pNode(nullptr); + TextFrameIndex nOffset(0); + std::optional<std::vector<sw::Extent>::const_iterator> oPrevIter; + for (auto iter = pMerged->extents.begin(); iter != pMerged->extents.end(); + oPrevIter = iter) + { + if (iter->pNode == pNode) + { + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + ++iter; + continue; // skip extents at end of previous node + } + pNode = iter->pNode; + Range aRange( 0, pNode->Len() > 0 ? pNode->Len() - 1 : 0 ); + MultiSelection aHiddenMulti( aRange ); + std::vector<std::pair<sw::mark::IBookmark const*, MarkKind>> bookmarks; + CalcHiddenRanges(*pNode, aHiddenMulti, &bookmarks); + + InitBookmarks(oPrevIter, iter, pMerged->extents.end(), nOffset, bookmarks, m_Bookmarks); + + for (sal_Int32 i = 0; i < aHiddenMulti.GetRangeCount(); ++i) + { + const Range& rRange = aHiddenMulti.GetRange( i ); + const sal_Int32 nStart = rRange.Min(); + const sal_Int32 nEnd = rRange.Max() + 1; + bool isStartHandled(false); + ::std::optional<sal_Int32> oExtend; + + if (nEnd <= iter->nStart) + { // entirely in gap, skip this hidden range + continue; + } + + do + { + if (!isStartHandled && nStart <= iter->nEnd) + { + isStartHandled = true; + if (nStart <= iter->nStart && !m_HiddenChg.empty() + && m_HiddenChg.back() == nOffset) + { + // previous one went until end of extent, extend it + oExtend.emplace(::std::min(iter->nEnd, nEnd) - ::std::max(iter->nStart, nStart)); + } + else + { + m_HiddenChg.push_back(nOffset + TextFrameIndex(::std::max(nStart - iter->nStart, sal_Int32(0)))); + } + } + else if (oExtend) + { + *oExtend += ::std::min(iter->nEnd, nEnd) - iter->nStart; + } + if (nEnd <= iter->nEnd) + { + if (oExtend) + { + m_HiddenChg.back() += TextFrameIndex(*oExtend); + } + else + { + m_HiddenChg.push_back(nOffset + TextFrameIndex(::std::max(nEnd - iter->nStart, sal_Int32(0)))); + } + break; // iterate to next hidden range + } + nOffset += TextFrameIndex(iter->nEnd - iter->nStart); + ++iter; + } + while (iter != pMerged->extents.end() && iter->pNode == pNode); + if (iter == pMerged->extents.end() || iter->pNode != pNode) + { + if (isStartHandled) + { // dangling end + if (oExtend) + { + m_HiddenChg.back() += TextFrameIndex(*oExtend); + } + else + { + m_HiddenChg.push_back(nOffset); + } + } // else: beyond last extent in node, ignore + break; // skip hidden ranges beyond last extent in node + } + } + } + } + else + { + Range aRange( 0, !rText.isEmpty() ? rText.getLength() - 1 : 0 ); + MultiSelection aHiddenMulti( aRange ); + std::vector<std::pair<sw::mark::IBookmark const*, MarkKind>> bookmarks; + CalcHiddenRanges(rNode, aHiddenMulti, &bookmarks); + + for (auto const& it : bookmarks) + { + // don't show __RefHeading__ bookmarks, which are hidden in Navigator, too + // (They are inserted automatically e.g. with the ToC at the beginning of + // the headings) + if (it.first->GetName().startsWith( + IDocumentMarkAccess::GetCrossRefHeadingBookmarkNamePrefix())) + { + continue; + } + + // search for custom bookmark boundary mark color + Color c = getBookmarkColor(rNode, it.first); + + switch (it.second) + { + case MarkKind::Start: + m_Bookmarks.emplace_back(TextFrameIndex(it.first->GetMarkStart().GetContentIndex()), it.second, c, it.first->GetName()); + break; + case MarkKind::End: + m_Bookmarks.emplace_back(TextFrameIndex(it.first->GetMarkEnd().GetContentIndex()), it.second, c, it.first->GetName()); + break; + case MarkKind::Point: + m_Bookmarks.emplace_back(TextFrameIndex(it.first->GetMarkPos().GetContentIndex()), it.second, c, it.first->GetName()); + break; + } + } + + m_HiddenChg.reserve( aHiddenMulti.GetRangeCount() * 2 ); + for (sal_Int32 i = 0; i < aHiddenMulti.GetRangeCount(); ++i) + { + const Range& rRange = aHiddenMulti.GetRange( i ); + const sal_Int32 nStart = rRange.Min(); + const sal_Int32 nEnd = rRange.Max() + (rText.isEmpty() ? 0 : 1); + + m_HiddenChg.push_back( TextFrameIndex(nStart) ); + m_HiddenChg.push_back( TextFrameIndex(nEnd) ); + } + } + + // SCRIPT AND SCRIPT RELATED INFORMATION + + TextFrameIndex nChg = m_nInvalidityPos; + + // COMPLETE_STRING means the data structure is up to date + m_nInvalidityPos = TextFrameIndex(COMPLETE_STRING); + + // this is the default direction + m_nDefaultDir = static_cast<sal_uInt8>(bRTL ? UBIDI_RTL : UBIDI_LTR); + + // counter for script info arrays + size_t nCnt = 0; + // counter for compression information arrays + size_t nCntComp = 0; + // counter for kashida array + size_t nCntKash = 0; + + sal_Int16 nScript = i18n::ScriptType::LATIN; + + // compression type + const CharCompressType aCompEnum = rNode.getIDocumentSettingAccess()->getCharacterCompressionType(); + + auto const& rParaItems((pMerged ? *pMerged->pParaPropsNode : rNode).GetSwAttrSet()); + // justification type + const bool bAdjustBlock = SvxAdjust::Block == rParaItems.GetAdjust().GetAdjust(); + + // FIND INVALID RANGES IN SCRIPT INFO ARRAYS: + + if( nChg ) + { + // if change position = 0 we do not use any data from the arrays + // because by deleting all characters of the first group at the beginning + // of a paragraph nScript is set to a wrong value + SAL_WARN_IF( !CountScriptChg(), "sw.core", "Where're my changes of script?" ); + while( nCnt < CountScriptChg() ) + { + if ( nChg > GetScriptChg( nCnt ) ) + nCnt++; + else + { + nScript = GetScriptType( nCnt ); + break; + } + } + if( CharCompressType::NONE != aCompEnum ) + { + while( nCntComp < CountCompChg() ) + { + if ( nChg <= GetCompStart( nCntComp ) ) + break; + nCntComp++; + } + } + if ( bAdjustBlock ) + { + while( nCntKash < CountKashida() ) + { + if ( nChg <= GetKashida( nCntKash ) ) + break; + nCntKash++; + } + } + } + + // ADJUST nChg VALUE: + + // by stepping back one position we know that we are inside a group + // declared as an nScript group + if ( nChg ) + --nChg; + + const TextFrameIndex nGrpStart = nCnt ? GetScriptChg(nCnt - 1) : TextFrameIndex(0); + + // we go back in our group until we reach the first character of + // type nScript + while ( nChg > nGrpStart && + nScript != g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nChg))) + --nChg; + + // If we are at the start of a group, we do not trust nScript, + // we better get nScript from the breakiterator: + if ( nChg == nGrpStart ) + nScript = static_cast<sal_uInt8>(g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nChg))); + + // INVALID DATA FROM THE SCRIPT INFO ARRAYS HAS TO BE DELETED: + + // remove invalid entries from script information arrays + m_ScriptChanges.erase(m_ScriptChanges.begin() + nCnt, m_ScriptChanges.end()); + + // get the start of the last compression group + TextFrameIndex nLastCompression = nChg; + if( nCntComp ) + { + --nCntComp; + nLastCompression = GetCompStart( nCntComp ); + if( nChg >= nLastCompression + GetCompLen( nCntComp ) ) + { + nLastCompression = nChg; + ++nCntComp; + } + } + + // remove invalid entries from compression information arrays + m_CompressionChanges.erase(m_CompressionChanges.begin() + nCntComp, + m_CompressionChanges.end()); + + // get the start of the last kashida group + TextFrameIndex nLastKashida = nChg; + if( nCntKash && i18n::ScriptType::COMPLEX == nScript ) + { + --nCntKash; + nLastKashida = GetKashida( nCntKash ); + } + + // remove invalid entries from kashida array + m_Kashida.erase(m_Kashida.begin() + nCntKash, m_Kashida.end()); + + // TAKE CARE OF WEAK CHARACTERS: WE MUST FIND AN APPROPRIATE + // SCRIPT FOR WEAK CHARACTERS AT THE BEGINNING OF A PARAGRAPH + + if (WEAK == g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nChg))) + { + // If the beginning of the current group is weak, this means that + // all of the characters in this group are weak. We have to assign + // the scripts to these characters depending on the fonts which are + // set for these characters to display them. + TextFrameIndex nEnd( + g_pBreakIt->GetBreakIter()->endOfScript(rText, sal_Int32(nChg), WEAK)); + + if (nEnd > TextFrameIndex(rText.getLength()) || nEnd < TextFrameIndex(0)) + nEnd = TextFrameIndex(rText.getLength()); + + nScript = SvtLanguageOptions::GetI18NScriptTypeOfLanguage( GetAppLanguage() ); + + SAL_WARN_IF( i18n::ScriptType::LATIN != nScript && + i18n::ScriptType::ASIAN != nScript && + i18n::ScriptType::COMPLEX != nScript, "sw.core", "Wrong default language" ); + + nChg = nEnd; + + // Get next script type or set to weak in order to exit + sal_uInt8 nNextScript = (nEnd < TextFrameIndex(rText.getLength())) + ? static_cast<sal_uInt8>(g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nEnd))) + : sal_uInt8(WEAK); + + if ( nScript != nNextScript ) + { + m_ScriptChanges.emplace_back(nEnd, nScript); + nCnt++; + nScript = nNextScript; + } + } + + // UPDATE THE SCRIPT INFO ARRAYS: + + while (nChg < TextFrameIndex(rText.getLength()) + || (m_ScriptChanges.empty() && rText.isEmpty())) + { + SAL_WARN_IF( i18n::ScriptType::WEAK == nScript, + "sw.core", "Inserting WEAK into SwScriptInfo structure" ); + + TextFrameIndex nSearchStt = nChg; + nChg = TextFrameIndex(g_pBreakIt->GetBreakIter()->endOfScript( + rText, sal_Int32(nSearchStt), nScript)); + + if (nChg > TextFrameIndex(rText.getLength()) || nChg < TextFrameIndex(0)) + nChg = TextFrameIndex(rText.getLength()); + + // special case for dotted circle since it can be used with complex + // before a mark, so we want it associated with the mark's script + // tdf#112594: another special case for NNBSP followed by a Mongolian + // character, since NNBSP has special uses in Mongolian (tdf#112594) + auto nPos = sal_Int32(nChg); + auto nPrevPos = nPos; + auto nPrevChar = rText.iterateCodePoints(&nPrevPos, -1); + if (nChg < TextFrameIndex(rText.getLength()) && nChg > TextFrameIndex(0) && + i18n::ScriptType::WEAK == g_pBreakIt->GetBreakIter()->getScriptType(rText, nPrevPos)) + { + auto nChar = rText.iterateCodePoints(&nPos, 0); + auto nType = unicode::getUnicodeType(nChar); + if (nType == css::i18n::UnicodeType::NON_SPACING_MARK || + nType == css::i18n::UnicodeType::ENCLOSING_MARK || + nType == css::i18n::UnicodeType::COMBINING_SPACING_MARK || + (nPrevChar == CHAR_NNBSP && + u_getIntPropertyValue(nChar, UCHAR_SCRIPT) == USCRIPT_MONGOLIAN)) + { + nPos = nPrevPos; + } + } + m_ScriptChanges.emplace_back(TextFrameIndex(nPos), nScript); + ++nCnt; + + // if current script is asian, we search for compressible characters + // in this range + if ( CharCompressType::NONE != aCompEnum && + i18n::ScriptType::ASIAN == nScript ) + { + CompType ePrevState = NONE; + CompType eState = NONE; + TextFrameIndex nPrevChg = nLastCompression; + + while ( nLastCompression < nChg ) + { + sal_Unicode cChar = rText[ sal_Int32(nLastCompression) ]; + + // examine current character + switch ( cChar ) + { + // Left punctuation found + case 0x3008: case 0x300A: case 0x300C: case 0x300E: + case 0x3010: case 0x3014: case 0x3016: case 0x3018: + case 0x301A: case 0x301D: + case 0xFF08: case 0xFF3B: case 0xFF5B: + eState = SPECIAL_LEFT; + break; + // Right punctuation found + case 0x3009: case 0x300B: + case 0x300D: case 0x300F: case 0x3011: case 0x3015: + case 0x3017: case 0x3019: case 0x301B: case 0x301E: + case 0x301F: + case 0xFF09: case 0xFF3D: case 0xFF5D: + eState = SPECIAL_RIGHT; + break; + case 0x3001: case 0x3002: // Fullstop or comma + case 0xFF0C: case 0xFF0E: case 0xFF1A: case 0xFF1B: + eState = SPECIAL_MIDDLE ; + break; + default: + eState = ( 0x3040 <= cChar && 0x3100 > cChar ) ? KANA : NONE; + } + + // insert range of compressible characters + if( ePrevState != eState ) + { + if ( ePrevState != NONE ) + { + // insert start and type + if ( CharCompressType::PunctuationAndKana == aCompEnum || + ePrevState != KANA ) + { + m_CompressionChanges.emplace_back(nPrevChg, + nLastCompression - nPrevChg, ePrevState); + } + } + + ePrevState = eState; + nPrevChg = nLastCompression; + } + + nLastCompression++; + } + + // we still have to examine last entry + if ( ePrevState != NONE ) + { + // insert start and type + if ( CharCompressType::PunctuationAndKana == aCompEnum || + ePrevState != KANA ) + { + m_CompressionChanges.emplace_back(nPrevChg, + nLastCompression - nPrevChg, ePrevState); + } + } + } + + // we search for connecting opportunities (kashida) + else if ( bAdjustBlock && i18n::ScriptType::COMPLEX == nScript ) + { + // sw_redlinehide: this is the only place that uses SwScanner with + // frame text, so we convert to sal_Int32 here + std::function<LanguageType (sal_Int32, sal_Int32, bool)> const pGetLangOfCharM( + [&pMerged](sal_Int32 const nBegin, sal_uInt16 const script, bool const bNoChar) + { + std::pair<SwTextNode const*, sal_Int32> const pos( + sw::MapViewToModel(*pMerged, TextFrameIndex(nBegin))); + return pos.first->GetLang(pos.second, bNoChar ? 0 : 1, script); + }); + std::function<LanguageType (sal_Int32, sal_Int32, bool)> const pGetLangOfChar1( + [&rNode](sal_Int32 const nBegin, sal_uInt16 const script, bool const bNoChar) + { return rNode.GetLang(nBegin, bNoChar ? 0 : 1, script); }); + auto pGetLangOfChar(pMerged ? pGetLangOfCharM : pGetLangOfChar1); + SwScanner aScanner( std::move(pGetLangOfChar), rText, nullptr, ModelToViewHelper(), + i18n::WordType::DICTIONARY_WORD, + sal_Int32(nLastKashida), sal_Int32(nChg)); + + // the search has to be performed on a per word base + while ( aScanner.NextWord() ) + { + const OUString& rWord = aScanner.GetWord(); + + sal_Int32 nIdx = 0, nPrevIdx = 0; + sal_Int32 nKashidaPos = -1; + sal_Unicode cCh, cPrevCh = 0; + + int nPriorityLevel = 7; // 0..6 = level found + // 7 not found + + sal_Int32 nWordLen = rWord.getLength(); + + // ignore trailing vowel chars + while( nWordLen && isTransparentChar( rWord[ nWordLen - 1 ] )) + --nWordLen; + + while (nIdx < nWordLen) + { + cCh = rWord[ nIdx ]; + + // 1. Priority: + // after user inserted kashida + if ( 0x640 == cCh ) + { + nKashidaPos = aScanner.GetBegin() + nIdx; + nPriorityLevel = 0; + } + + // 2. Priority: + // after a Seen or Sad + if (nPriorityLevel >= 1 && nIdx < nWordLen - 1) + { + if( isSeenOrSadChar( cCh ) + && (rWord[ nIdx+1 ] != 0x200C) ) // #i98410#: prevent ZWNJ expansion + { + nKashidaPos = aScanner.GetBegin() + nIdx; + nPriorityLevel = 1; + } + } + + // 3. Priority: + // before final form of Teh Marbuta, Heh, Dal + if ( nPriorityLevel >= 2 && nIdx > 0 ) + { + if ( isTehMarbutaChar ( cCh ) || // Teh Marbuta (right joining) + isDalChar ( cCh ) || // Dal (right joining) final form may appear in the middle of word + ( isHehChar ( cCh ) && nIdx == nWordLen - 1)) // Heh (dual joining) only at end of word + { + + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 2; + } + } + } + + // 4. Priority: + // before final form of Alef, Tah, Lam, Kaf or Gaf + if ( nPriorityLevel >= 3 && nIdx > 0 ) + { + if ( isAlefChar ( cCh ) || // Alef (right joining) final form may appear in the middle of word + (( isLamChar ( cCh ) || // Lam, + isTahChar ( cCh ) || // Tah, + isKafChar ( cCh ) || // Kaf (all dual joining) + isGafChar ( cCh ) ) + && nIdx == nWordLen - 1)) // only at end of word + { + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 3; + } + } + } + + // 5. Priority: + // before medial Beh-like + if ( nPriorityLevel >= 4 && nIdx > 0 && nIdx < nWordLen - 1 ) + { + if ( isBehChar ( cCh ) ) + { + // check if next character is Reh or Yeh-like + sal_Unicode cNextCh = rWord[ nIdx + 1 ]; + if ( isRehChar ( cNextCh ) || isYehChar ( cNextCh )) + { + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 4; + } + } + } + } + + // 6. Priority: + // before the final form of Waw, Ain, Qaf and Feh + if ( nPriorityLevel >= 5 && nIdx > 0 ) + { + if ( isWawChar ( cCh ) || // Wav (right joining) + // final form may appear in the middle of word + (( isAinChar ( cCh ) || // Ain (dual joining) + isQafChar ( cCh ) || // Qaf (dual joining) + isFehChar ( cCh ) ) // Feh (dual joining) + && nIdx == nWordLen - 1)) // only at end of word + { + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 5; + } + } + } + + // other connecting possibilities + if ( nPriorityLevel >= 6 && nIdx > 0 ) + { + // Reh, Zain + if ( isRehChar ( cCh ) ) + { + SAL_WARN_IF( 0 == cPrevCh, "sw.core", "No previous character" ); + // check if character is connectable to previous character, + if ( lcl_ConnectToPrev( cCh, cPrevCh ) ) + { + nKashidaPos = aScanner.GetBegin() + nPrevIdx; + nPriorityLevel = 6; + } + } + } + + // Do not consider vowel marks when checking if a character + // can be connected to previous character. + if ( !isTransparentChar ( cCh) ) + { + cPrevCh = cCh; + nPrevIdx = nIdx; + } + + ++nIdx; + } // end of current word + + if ( -1 != nKashidaPos ) + { + m_Kashida.insert(m_Kashida.begin() + nCntKash, TextFrameIndex(nKashidaPos)); + nCntKash++; + } + } // end of kashida search + } + + if (nChg < TextFrameIndex(rText.getLength())) + nScript = static_cast<sal_uInt8>(g_pBreakIt->GetBreakIter()->getScriptType(rText, sal_Int32(nChg))); + + nLastCompression = nChg; + nLastKashida = nChg; + } + +#if OSL_DEBUG_LEVEL > 0 + // check kashida data + TextFrameIndex nTmpKashidaPos(-1); + bool bWrongKash = false; + for (size_t i = 0; i < m_Kashida.size(); ++i) + { + TextFrameIndex nCurrKashidaPos = GetKashida( i ); + if ( nCurrKashidaPos <= nTmpKashidaPos ) + { + bWrongKash = true; + break; + } + nTmpKashidaPos = nCurrKashidaPos; + } + SAL_WARN_IF( bWrongKash, "sw.core", "Kashida array contains wrong data" ); +#endif + + // remove invalid entries from direction information arrays + m_DirectionChanges.clear(); + + // Perform Unicode Bidi Algorithm for text direction information + { + UpdateBidiInfo( rText ); + + // #i16354# Change script type for RTL text to CTL: + // 1. All text in RTL runs will use the CTL font + // #i89825# change the script type also to CTL (hennerdrewes) + // 2. Text in embedded LTR runs that does not have any strong LTR characters (numbers!) + for (size_t nDirIdx = 0; nDirIdx < m_DirectionChanges.size(); ++nDirIdx) + { + const sal_uInt8 nCurrDirType = GetDirType( nDirIdx ); + // nStart is start of RTL run: + const TextFrameIndex nStart = nDirIdx > 0 ? GetDirChg(nDirIdx - 1) : TextFrameIndex(0); + // nEnd is end of RTL run: + const TextFrameIndex nEnd = GetDirChg( nDirIdx ); + + if ( nCurrDirType % 2 == UBIDI_RTL || // text in RTL run + (nCurrDirType > UBIDI_LTR && // non-strong text in embedded LTR run + !lcl_HasStrongLTR(rText, sal_Int32(nStart), sal_Int32(nEnd)))) + { + // nScriptIdx points into the ScriptArrays: + size_t nScriptIdx = 0; + + // Skip entries in ScriptArray which are not inside the RTL run: + // Make nScriptIdx become the index of the script group with + // 1. nStartPosOfGroup <= nStart and + // 2. nEndPosOfGroup > nStart + while ( GetScriptChg( nScriptIdx ) <= nStart ) + ++nScriptIdx; + + const TextFrameIndex nStartPosOfGroup = nScriptIdx + ? GetScriptChg(nScriptIdx - 1) + : TextFrameIndex(0); + const sal_uInt8 nScriptTypeOfGroup = GetScriptType( nScriptIdx ); + + SAL_WARN_IF( nStartPosOfGroup > nStart || GetScriptChg( nScriptIdx ) <= nStart, + "sw.core", "Script override with CTL font trouble" ); + + // Check if we have to insert a new script change at + // position nStart. If nStartPosOfGroup < nStart, + // we have to insert a new script change: + if (nStart > TextFrameIndex(0) && nStartPosOfGroup < nStart) + { + m_ScriptChanges.insert(m_ScriptChanges.begin() + nScriptIdx, + ScriptChangeInfo(nStart, nScriptTypeOfGroup) ); + ++nScriptIdx; + } + + // Remove entries in ScriptArray which end inside the RTL run: + while (nScriptIdx < m_ScriptChanges.size() + && GetScriptChg(nScriptIdx) <= nEnd) + { + m_ScriptChanges.erase(m_ScriptChanges.begin() + nScriptIdx); + } + + // Insert a new entry in ScriptArray for the end of the RTL run: + m_ScriptChanges.insert(m_ScriptChanges.begin() + nScriptIdx, + ScriptChangeInfo(nEnd, i18n::ScriptType::COMPLEX) ); + +#if OSL_DEBUG_LEVEL > 1 + // Check that ScriptChangeInfos are in increasing order of + // position and that we don't have "empty" changes. + sal_uInt8 nLastTyp = i18n::ScriptType::WEAK; + TextFrameIndex nLastPos = TextFrameIndex(0); + for (const auto& rScriptChange : m_ScriptChanges) + { + SAL_WARN_IF( nLastTyp == rScriptChange.type || + nLastPos >= rScriptChange.position, + "sw.core", "Heavy InitScriptType() confusion" ); + nLastPos = rScriptChange.position; + nLastTyp = rScriptChange.type; + } +#endif + } + } + } +} + +void SwScriptInfo::UpdateBidiInfo( const OUString& rText ) +{ + // remove invalid entries from direction information arrays + m_DirectionChanges.clear(); + + // Bidi functions from icu 2.0 + + UErrorCode nError = U_ZERO_ERROR; + UBiDi* pBidi = ubidi_openSized( rText.getLength(), 0, &nError ); + nError = U_ZERO_ERROR; + + ubidi_setPara( pBidi, reinterpret_cast<const UChar *>(rText.getStr()), rText.getLength(), + m_nDefaultDir, nullptr, &nError ); + nError = U_ZERO_ERROR; + int nCount = ubidi_countRuns( pBidi, &nError ); + int32_t nStart = 0; + int32_t nEnd; + UBiDiLevel nCurrDir; + for ( int nIdx = 0; nIdx < nCount; ++nIdx ) + { + ubidi_getLogicalRun( pBidi, nStart, &nEnd, &nCurrDir ); + m_DirectionChanges.emplace_back(TextFrameIndex(nEnd), nCurrDir); + nStart = nEnd; + } + + ubidi_close( pBidi ); +} + +// returns the position of the next character which belongs to another script +// than the character of the actual (input) position. +// If there's no script change until the end of the paragraph, it will return +// COMPLETE_STRING. +// Scripts are Asian (Chinese, Japanese, Korean), +// Latin ( English etc.) +// and Complex ( Hebrew, Arabian ) +TextFrameIndex SwScriptInfo::NextScriptChg(const TextFrameIndex nPos) const +{ + const size_t nEnd = CountScriptChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + if( nPos < GetScriptChg( nX ) ) + return GetScriptChg( nX ); + } + + return TextFrameIndex(COMPLETE_STRING); +} + +// returns the script of the character at the input position +sal_Int16 SwScriptInfo::ScriptType(const TextFrameIndex nPos) const +{ + const size_t nEnd = CountScriptChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + if( nPos < GetScriptChg( nX ) ) + return GetScriptType( nX ); + } + + // the default is the application language script + return SvtLanguageOptions::GetI18NScriptTypeOfLanguage( GetAppLanguage() ); +} + +TextFrameIndex SwScriptInfo::NextDirChg(const TextFrameIndex nPos, + const sal_uInt8* pLevel ) const +{ + const sal_uInt8 nCurrDir = pLevel ? *pLevel : 62; + const size_t nEnd = CountDirChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + if( nPos < GetDirChg( nX ) && + ( nX + 1 == nEnd || GetDirType( nX + 1 ) <= nCurrDir ) ) + return GetDirChg( nX ); + } + + return TextFrameIndex(COMPLETE_STRING); +} + +sal_uInt8 SwScriptInfo::DirType(const TextFrameIndex nPos) const +{ + const size_t nEnd = CountDirChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + if( nPos < GetDirChg( nX ) ) + return GetDirType( nX ); + } + + return 0; +} + +TextFrameIndex SwScriptInfo::NextHiddenChg(TextFrameIndex const nPos) const +{ + for (auto const& it : m_HiddenChg) + { + if (nPos < it) + { + return it; + } + } + return TextFrameIndex(COMPLETE_STRING); +} + +TextFrameIndex SwScriptInfo::NextBookmark(TextFrameIndex const nPos) const +{ + for (auto const& it : m_Bookmarks) + { + if (nPos < std::get<0>(it)) + { + return std::get<0>(it); + } + } + return TextFrameIndex(COMPLETE_STRING); +} + +std::vector<std::tuple<SwScriptInfo::MarkKind, Color, OUString>> + SwScriptInfo::GetBookmarks(TextFrameIndex const nPos) +{ + std::vector<std::tuple<SwScriptInfo::MarkKind, Color, OUString>> aColors; + for (auto const& it : m_Bookmarks) + { + if (nPos == std::get<0>(it)) + { + const OUString& sName = std::get<3>(it); + // filter hidden bookmarks imported from OOXML + // TODO import them as hidden bookmarks + if ( !( sName.startsWith("_Toc") || sName.startsWith("_Ref") ) ) + aColors.push_back(std::tuple<MarkKind, Color, + OUString>(std::get<1>(it), std::get<2>(it), std::get<3>(it))); + } + else if (nPos < std::get<0>(it)) + { + break; + } + } + + // sort bookmark boundary marks at the same position + // mark order: ] | [ + // color order: [c1 [c2 [c3 ... c3] c2] c1] + sort(aColors.begin(), aColors.end(), + [](std::tuple<MarkKind, Color, OUString> const a, std::tuple<MarkKind, Color, OUString> const b) { + return (MarkKind::End == std::get<0>(a) && MarkKind::End != std::get<0>(b)) || + (MarkKind::Point == std::get<0>(a) && MarkKind::Start == std::get<0>(b)) || + // if both are end or start, order by color + (MarkKind::End == std::get<0>(a) && MarkKind::End == std::get<0>(b) && std::get<1>(a) < std::get<1>(b)) || + (MarkKind::Start == std::get<0>(a) && MarkKind::Start == std::get<0>(b) && std::get<1>(b) < std::get<1>(a));}); + + return aColors; +} + +// Takes a string and replaced the hidden ranges with cChar. +sal_Int32 SwScriptInfo::MaskHiddenRanges( const SwTextNode& rNode, OUStringBuffer & rText, + const sal_Int32 nStt, const sal_Int32 nEnd, + const sal_Unicode cChar ) +{ + assert(rNode.GetText().getLength() == rText.getLength()); + + std::vector<sal_Int32> aList; + sal_Int32 nHiddenStart; + sal_Int32 nHiddenEnd; + sal_Int32 nNumOfHiddenChars = 0; + GetBoundsOfHiddenRange( rNode, 0, nHiddenStart, nHiddenEnd, &aList ); + auto rFirst( aList.crbegin() ); + auto rLast( aList.crend() ); + while ( rFirst != rLast ) + { + nHiddenEnd = *(rFirst++); + nHiddenStart = *(rFirst++); + + if ( nHiddenEnd < nStt || nHiddenStart > nEnd ) + continue; + + while ( nHiddenStart < nHiddenEnd && nHiddenStart < nEnd ) + { + if (nHiddenStart >= nStt) + { + rText[nHiddenStart] = cChar; + ++nNumOfHiddenChars; + } + ++nHiddenStart; + } + } + + return nNumOfHiddenChars; +} + +// Takes a SwTextNode and deletes the hidden ranges from the node. +void SwScriptInfo::DeleteHiddenRanges( SwTextNode& rNode ) +{ + std::vector<sal_Int32> aList; + sal_Int32 nHiddenStart; + sal_Int32 nHiddenEnd; + GetBoundsOfHiddenRange( rNode, 0, nHiddenStart, nHiddenEnd, &aList ); + auto rFirst( aList.crbegin() ); + auto rLast( aList.crend() ); + while ( rFirst != rLast ) + { + nHiddenEnd = *(rFirst++); + nHiddenStart = *(rFirst++); + + SwPaM aPam( rNode, nHiddenStart, rNode, nHiddenEnd ); + rNode.getIDocumentContentOperations().DeleteRange( aPam ); + } +} + +bool SwScriptInfo::GetBoundsOfHiddenRange( const SwTextNode& rNode, sal_Int32 nPos, + sal_Int32& rnStartPos, sal_Int32& rnEndPos, + std::vector<sal_Int32>* pList ) +{ + rnStartPos = COMPLETE_STRING; + rnEndPos = 0; + + bool bNewContainsHiddenChars = false; + + // Optimization: First examine the flags at the text node: + + if ( !rNode.IsCalcHiddenCharFlags() ) + { + bool bWholePara = rNode.HasHiddenCharAttribute( true ); + bool bContainsHiddenChars = rNode.HasHiddenCharAttribute( false ); + if ( !bContainsHiddenChars ) + return false; + + if ( bWholePara ) + { + if ( pList ) + { + pList->push_back( 0 ); + pList->push_back(rNode.GetText().getLength()); + } + + rnStartPos = 0; + rnEndPos = rNode.GetText().getLength(); + return true; + } + } + + // sw_redlinehide: this won't work if it's merged +#if 0 + const SwScriptInfo* pSI = SwScriptInfo::GetScriptInfo( rNode ); + if ( pSI ) + { + + // Check first, if we have a valid SwScriptInfo object for this text node: + + bNewContainsHiddenChars = pSI->GetBoundsOfHiddenRange( nPos, rnStartPos, rnEndPos, pList ); + const bool bNewHiddenCharsHidePara = + rnStartPos == 0 && rnEndPos >= rNode.GetText().getLength(); + rNode.SetHiddenCharAttribute( bNewHiddenCharsHidePara, bNewContainsHiddenChars ); + } + else +#endif + { + + // No valid SwScriptInfo Object, we have to do it the hard way: + + Range aRange(0, (!rNode.GetText().isEmpty()) + ? rNode.GetText().getLength() - 1 + : 0); + MultiSelection aHiddenMulti( aRange ); + SwScriptInfo::CalcHiddenRanges(rNode, aHiddenMulti, nullptr); + for( sal_Int32 i = 0; i < aHiddenMulti.GetRangeCount(); ++i ) + { + const Range& rRange = aHiddenMulti.GetRange( i ); + const sal_Int32 nHiddenStart = rRange.Min(); + const sal_Int32 nHiddenEnd = rRange.Max() + 1; + + if ( nHiddenStart > nPos ) + break; + if (nPos < nHiddenEnd) + { + rnStartPos = nHiddenStart; + rnEndPos = std::min<sal_Int32>(nHiddenEnd, + rNode.GetText().getLength()); + break; + } + } + + if ( pList ) + { + for( sal_Int32 i = 0; i < aHiddenMulti.GetRangeCount(); ++i ) + { + const Range& rRange = aHiddenMulti.GetRange( i ); + pList->push_back( rRange.Min() ); + pList->push_back( rRange.Max() + 1 ); + } + } + + bNewContainsHiddenChars = aHiddenMulti.GetRangeCount() > 0; + } + + return bNewContainsHiddenChars; +} + +bool SwScriptInfo::GetBoundsOfHiddenRange(TextFrameIndex nPos, + TextFrameIndex & rnStartPos, TextFrameIndex & rnEndPos) const +{ + rnStartPos = TextFrameIndex(COMPLETE_STRING); + rnEndPos = TextFrameIndex(0); + + const size_t nEnd = CountHiddenChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + const TextFrameIndex nHiddenStart = GetHiddenChg( nX++ ); + const TextFrameIndex nHiddenEnd = GetHiddenChg( nX ); + + if ( nHiddenStart > nPos ) + break; + if (nPos < nHiddenEnd) + { + rnStartPos = nHiddenStart; + rnEndPos = nHiddenEnd; + break; + } + } + + return CountHiddenChg() > 0; +} + +bool SwScriptInfo::IsInHiddenRange( const SwTextNode& rNode, sal_Int32 nPos ) +{ + sal_Int32 nStartPos; + sal_Int32 nEndPos; + SwScriptInfo::GetBoundsOfHiddenRange( rNode, nPos, nStartPos, nEndPos ); + return nStartPos != COMPLETE_STRING; +} + +#ifdef DBG_UTIL +// returns the type of the compressed character +SwScriptInfo::CompType SwScriptInfo::DbgCompType(const TextFrameIndex nPos) const +{ + const size_t nEnd = CountCompChg(); + for( size_t nX = 0; nX < nEnd; ++nX ) + { + const TextFrameIndex nChg = GetCompStart(nX); + + if ( nPos < nChg ) + return NONE; + + if( nPos < nChg + GetCompLen( nX ) ) + return GetCompType( nX ); + } + return NONE; +} +#endif + +// returns, if there are compressible kanas or specials +// between nStart and nEnd +size_t SwScriptInfo::HasKana(TextFrameIndex const nStart, TextFrameIndex const nLen) const +{ + const size_t nCnt = CountCompChg(); + TextFrameIndex nEnd = nStart + nLen; + + for( size_t nX = 0; nX < nCnt; ++nX ) + { + TextFrameIndex nKanaStart = GetCompStart(nX); + TextFrameIndex nKanaEnd = nKanaStart + GetCompLen(nX); + + if ( nKanaStart >= nEnd ) + return SAL_MAX_SIZE; + + if ( nStart < nKanaEnd ) + return nX; + } + + return SAL_MAX_SIZE; +} + +tools::Long SwScriptInfo::Compress(KernArray& rKernArray, TextFrameIndex nIdx, TextFrameIndex nLen, + const sal_uInt16 nCompress, const sal_uInt16 nFontHeight, + bool bCenter, + Point* pPoint ) const +{ + SAL_WARN_IF( !nCompress, "sw.core", "Compression without compression?!" ); + SAL_WARN_IF( !nLen, "sw.core", "Compression without text?!" ); + const size_t nCompCount = CountCompChg(); + + // In asian typography, there are full width and half width characters. + // Full width punctuation characters can be compressed by 50% + // to determine this, we compare the font width with 75% of its height + const tools::Long nMinWidth = ( 3 * nFontHeight ) / 4; + + size_t nCompIdx = HasKana( nIdx, nLen ); + + if ( SAL_MAX_SIZE == nCompIdx ) + return 0; + + TextFrameIndex nChg = GetCompStart( nCompIdx ); + TextFrameIndex nCompLen = GetCompLen( nCompIdx ); + sal_Int32 nI = 0; + nLen += nIdx; + + if( nChg > nIdx ) + { + nI = sal_Int32(nChg - nIdx); + nIdx = nChg; + } + else if( nIdx < nChg + nCompLen ) + nCompLen -= nIdx - nChg; + + if( nIdx > nLen || nCompIdx >= nCompCount ) + return 0; + + tools::Long nSub = 0; + tools::Long nLast = nI ? rKernArray[ nI - 1 ] : 0; + do + { + const CompType nType = GetCompType( nCompIdx ); +#ifdef DBG_UTIL + SAL_WARN_IF( nType != DbgCompType( nIdx ), "sw.core", "Gimme the right type!" ); +#endif + nCompLen += nIdx; + if( nCompLen > nLen ) + nCompLen = nLen; + + // are we allowed to compress the character? + if ( rKernArray[ nI ] - nLast < nMinWidth ) + { + nIdx++; nI++; + } + else + { + while( nIdx < nCompLen ) + { + SAL_WARN_IF( SwScriptInfo::NONE == nType, "sw.core", "None compression?!" ); + + // nLast is width of current character + nLast -= rKernArray[ nI ]; + + nLast *= nCompress; + tools::Long nMove = 0; + if( SwScriptInfo::KANA != nType ) + { + nLast /= 24000; + if( pPoint && SwScriptInfo::SPECIAL_LEFT == nType ) + { + if( nI ) + nMove = nLast; + else + { + pPoint->AdjustX(nLast ); + nLast = 0; + } + } + else if( bCenter && SwScriptInfo::SPECIAL_MIDDLE == nType ) + nMove = nLast / 2; + } + else + nLast /= 100000; + nSub -= nLast; + nLast = rKernArray[ nI ]; + if( nI && nMove ) + rKernArray.adjust(nI - 1, nMove); + rKernArray.adjust(nI, -nSub); + ++nI; + ++nIdx; + } + } + + if( nIdx >= nLen ) + break; + + TextFrameIndex nTmpChg = nLen; + if( ++nCompIdx < nCompCount ) + { + nTmpChg = GetCompStart( nCompIdx ); + if( nTmpChg > nLen ) + nTmpChg = nLen; + nCompLen = GetCompLen( nCompIdx ); + } + + while( nIdx < nTmpChg ) + { + nLast = rKernArray[ nI ]; + rKernArray.adjust(nI, -nSub); + ++nI; + ++nIdx; + } + } while( nIdx < nLen ); + return nSub; +} + +// Note on calling KashidaJustify(): +// Kashida positions may be marked as invalid. Therefore KashidaJustify may return the clean +// total number of kashida positions, or the number of kashida positions after some positions +// have been dropped, depending on the state of the m_KashidaInvalid set. + +sal_Int32 SwScriptInfo::KashidaJustify( KernArray* pKernArray, + sal_Bool* pKashidaArray, + TextFrameIndex const nStt, + TextFrameIndex const nLen, + tools::Long nSpaceAdd ) const +{ + SAL_WARN_IF( !nLen, "sw.core", "Kashida justification without text?!" ); + + if( !IsKashidaLine(nStt)) + return -1; + + // evaluate kashida information in collected in SwScriptInfo + + size_t nCntKash = 0; + while( nCntKash < CountKashida() ) + { + if ( nStt <= GetKashida( nCntKash ) ) + break; + ++nCntKash; + } + + const TextFrameIndex nEnd = nStt + nLen; + + size_t nCntKashEnd = nCntKash; + while ( nCntKashEnd < CountKashida() ) + { + if ( nEnd <= GetKashida( nCntKashEnd ) ) + break; + ++nCntKashEnd; + } + + size_t nActualKashCount = nCntKashEnd - nCntKash; + for (size_t i = nCntKash; i < nCntKashEnd; ++i) + { + if ( nActualKashCount && !IsKashidaValid ( i ) ) + --nActualKashCount; + } + + if ( !pKernArray ) + return nActualKashCount; + + // do nothing if there is no more kashida + if ( nCntKash < CountKashida() ) + { + // skip any invalid kashidas + while (nCntKash < nCntKashEnd && !IsKashidaValid(nCntKash)) + ++nCntKash; + + TextFrameIndex nIdx = nCntKash < nCntKashEnd && IsKashidaValid(nCntKash) + ? GetKashida(nCntKash) + : nEnd; + tools::Long nKashAdd = nSpaceAdd; + + while ( nIdx < nEnd ) + { + TextFrameIndex nArrayPos = nIdx - nStt; + + // mark Kashida insertion positions, code in VCL will use this + // array to know where to insert Kashida. + if (pKashidaArray) + pKashidaArray[sal_Int32(nArrayPos)] = true; + + // next kashida position + ++nCntKash; + while (nCntKash < nCntKashEnd && !IsKashidaValid(nCntKash)) + ++nCntKash; + + nIdx = nCntKash < nCntKashEnd && IsKashidaValid(nCntKash) ? GetKashida(nCntKash) : nEnd; + if ( nIdx > nEnd ) + nIdx = nEnd; + + const TextFrameIndex nArrayEnd = nIdx - nStt; + + while ( nArrayPos < nArrayEnd ) + { + pKernArray->adjust(sal_Int32(nArrayPos), nKashAdd); + ++nArrayPos; + } + nKashAdd += nSpaceAdd; + } + } + + return 0; +} + +// Checks if the current text is 'Arabic' text. Note that only the first +// character has to be checked because a ctl portion only contains one +// script, see NewTextPortion +bool SwScriptInfo::IsArabicText(const OUString& rText, + TextFrameIndex const nStt, TextFrameIndex const nLen) +{ + using namespace ::com::sun::star::i18n; + static const ScriptTypeList typeList[] = { + { UnicodeScript_kArabic, UnicodeScript_kArabic, sal_Int16(UnicodeScript_kArabic) }, // 11, + { UnicodeScript_kScriptCount, UnicodeScript_kScriptCount, sal_Int16(UnicodeScript_kScriptCount) } // 88 + }; + + // go forward if current position does not hold a regular character: + const CharClass& rCC = GetAppCharClass(); + sal_Int32 nIdx = sal_Int32(nStt); + const sal_Int32 nEnd = sal_Int32(nStt + nLen); + while ( nIdx < nEnd && !rCC.isLetterNumeric( rText, nIdx ) ) + { + ++nIdx; + } + + if( nIdx == nEnd ) + { + // no regular character found in this portion. Go backward: + --nIdx; + while ( nIdx >= 0 && !rCC.isLetterNumeric( rText, nIdx ) ) + { + --nIdx; + } + } + + if( nIdx >= 0 ) + { + const sal_Unicode cCh = rText[nIdx]; + const sal_Int16 type = unicode::getUnicodeScriptType( cCh, typeList, sal_Int16(UnicodeScript_kScriptCount) ); + return type == sal_Int16(UnicodeScript_kArabic); + } + return false; +} + +bool SwScriptInfo::IsKashidaValid(size_t const nKashPos) const +{ + return m_KashidaInvalid.find(nKashPos) == m_KashidaInvalid.end(); +} + +void SwScriptInfo::ClearKashidaInvalid(size_t const nKashPos) +{ + m_KashidaInvalid.erase(nKashPos); +} + +// bMark == true: +// marks the first valid kashida in the given text range as invalid +// bMark == false: +// clears all kashida invalid flags in the given text range +bool SwScriptInfo::MarkOrClearKashidaInvalid( + TextFrameIndex const nStt, TextFrameIndex const nLen, + bool bMark, sal_Int32 nMarkCount) +{ + size_t nCntKash = 0; + while( nCntKash < CountKashida() ) + { + if ( nStt <= GetKashida( nCntKash ) ) + break; + nCntKash++; + } + + const TextFrameIndex nEnd = nStt + nLen; + + while ( nCntKash < CountKashida() ) + { + if ( nEnd <= GetKashida( nCntKash ) ) + break; + if(bMark) + { + if ( MarkKashidaInvalid ( nCntKash ) ) + { + --nMarkCount; + if (!nMarkCount) + return true; + } + } + else + { + ClearKashidaInvalid ( nCntKash ); + } + nCntKash++; + } + return false; +} + +bool SwScriptInfo::MarkKashidaInvalid(size_t const nKashPos) +{ + return m_KashidaInvalid.insert(nKashPos).second; +} + +// retrieve the kashida positions in the given text range +void SwScriptInfo::GetKashidaPositions( + TextFrameIndex const nStt, TextFrameIndex const nLen, + std::vector<TextFrameIndex>& rKashidaPosition) +{ + size_t nCntKash = 0; + while( nCntKash < CountKashida() ) + { + if ( nStt <= GetKashida( nCntKash ) ) + break; + nCntKash++; + } + + const TextFrameIndex nEnd = nStt + nLen; + + size_t nCntKashEnd = nCntKash; + while ( nCntKashEnd < CountKashida() ) + { + if ( nEnd <= GetKashida( nCntKashEnd ) ) + break; + rKashidaPosition.push_back(GetKashida(nCntKashEnd)); + nCntKashEnd++; + } +} + +void SwScriptInfo::SetNoKashidaLine(TextFrameIndex const nStt, TextFrameIndex const nLen) +{ + m_NoKashidaLine.push_back( nStt ); + m_NoKashidaLineEnd.push_back( nStt + nLen ); +} + +// determines if the line uses kashida justification +bool SwScriptInfo::IsKashidaLine(TextFrameIndex const nCharIdx) const +{ + for (size_t i = 0; i < m_NoKashidaLine.size(); ++i) + { + if (nCharIdx >= m_NoKashidaLine[i] && nCharIdx < m_NoKashidaLineEnd[i]) + return false; + } + return true; +} + +void SwScriptInfo::ClearNoKashidaLine(TextFrameIndex const nStt, TextFrameIndex const nLen) +{ + size_t i = 0; + while (i < m_NoKashidaLine.size()) + { + if (nStt + nLen >= m_NoKashidaLine[i] && nStt < m_NoKashidaLineEnd[i]) + { + m_NoKashidaLine.erase(m_NoKashidaLine.begin() + i); + m_NoKashidaLineEnd.erase(m_NoKashidaLineEnd.begin() + i); + } + else + ++i; + } +} + +// mark the given character indices as invalid kashida positions +void SwScriptInfo::MarkKashidasInvalid(sal_Int32 const nCnt, + const TextFrameIndex* pKashidaPositions) +{ + SAL_WARN_IF( !pKashidaPositions || nCnt == 0, "sw.core", "Where are kashidas?" ); + + size_t nCntKash = 0; + sal_Int32 nKashidaPosIdx = 0; + + while (nCntKash < CountKashida() && nKashidaPosIdx < nCnt) + { + if ( pKashidaPositions [nKashidaPosIdx] > GetKashida( nCntKash ) ) + { + ++nCntKash; + continue; + } + + if ( pKashidaPositions [nKashidaPosIdx] != GetKashida( nCntKash ) || !IsKashidaValid ( nCntKash ) ) + return; // something is wrong + + MarkKashidaInvalid ( nCntKash ); + nKashidaPosIdx++; + } +} + +TextFrameIndex SwScriptInfo::ThaiJustify( std::u16string_view aText, KernArray* pKernArray, + TextFrameIndex const nStt, + TextFrameIndex const nLen, + TextFrameIndex nNumberOfBlanks, + tools::Long nSpaceAdd ) +{ + SAL_WARN_IF( nStt + nLen > TextFrameIndex(aText.size()), "sw.core", "String in ThaiJustify too small" ); + + SwTwips nNumOfTwipsToDistribute = nSpaceAdd * sal_Int32(nNumberOfBlanks) / + SPACING_PRECISION_FACTOR; + + tools::Long nSpaceSum = 0; + TextFrameIndex nCnt(0); + + for (sal_Int32 nI = 0; nI < sal_Int32(nLen); ++nI) + { + const sal_Unicode cCh = aText[sal_Int32(nStt) + nI]; + + // check if character is not above or below base + if ( ( 0xE34 > cCh || cCh > 0xE3A ) && + ( 0xE47 > cCh || cCh > 0xE4E ) && cCh != 0xE31 ) + { + if (nNumberOfBlanks > TextFrameIndex(0)) + { + nSpaceAdd = nNumOfTwipsToDistribute / sal_Int32(nNumberOfBlanks); + --nNumberOfBlanks; + nNumOfTwipsToDistribute -= nSpaceAdd; + } + nSpaceSum += nSpaceAdd; + ++nCnt; + } + + if (pKernArray) + pKernArray->adjust(nI, nSpaceSum); + } + + return nCnt; +} + +SwScriptInfo* SwScriptInfo::GetScriptInfo( const SwTextNode& rTNd, + SwTextFrame const**const o_ppFrame, + bool const bAllowInvalid) +{ + SwIterator<SwTextFrame, SwTextNode, sw::IteratorMode::UnwrapMulti> aIter(rTNd); + SwScriptInfo* pScriptInfo = nullptr; + + for( SwTextFrame* pLast = aIter.First(); pLast; pLast = aIter.Next() ) + { + pScriptInfo = const_cast<SwScriptInfo*>(pLast->GetScriptInfo()); + if ( pScriptInfo ) + { + if (bAllowInvalid || + TextFrameIndex(COMPLETE_STRING) == pScriptInfo->GetInvalidityA()) + { + if (o_ppFrame) + { + *o_ppFrame = pLast; + } + break; + } + pScriptInfo = nullptr; + } + } + + return pScriptInfo; +} + +SwParaPortion::SwParaPortion() +{ + FormatReset(); + m_bFlys = m_bFootnoteNum = m_bMargin = false; + SetWhichPor( PortionType::Para ); +} + +SwParaPortion::~SwParaPortion() +{ +} + +TextFrameIndex SwParaPortion::GetParLen() const +{ + TextFrameIndex nLen(0); + const SwLineLayout *pLay = this; + while( pLay ) + { + nLen += pLay->GetLen(); + pLay = pLay->GetNext(); + } + return nLen; +} + +bool SwParaPortion::HasNumberingPortion(FootnoteOrNot const eFootnote) const +{ + SwLinePortion const* pPortion(nullptr); + // the first line may contain only fly portion... + for (SwLineLayout const* pLine = this; pLine && !pPortion; pLine = pLine->GetNext()) + { + pPortion = pLine->GetFirstPortion(); + while (pPortion && (pPortion->InGlueGrp() || pPortion->IsKernPortion() || pPortion->IsFlyPortion())) + { // skip margins and fly spacers - numbering should be first then + pPortion = pPortion->GetNextPortion(); + } + } + if (pPortion && pPortion->InHyphGrp()) + { // weird special case, bullet with soft hyphen + pPortion = pPortion->GetNextPortion(); + } + return pPortion && pPortion->InNumberGrp() + && (eFootnote == SwParaPortion::FootnoteToo || !pPortion->IsFootnoteNumPortion()); +} + +bool SwParaPortion::HasContentPortions() const +{ + SwLinePortion const* pPortion(nullptr); + for (SwLineLayout const* pLine = this; pLine && !pPortion; pLine = pLine->GetNext()) + { + pPortion = pLine->GetFirstPortion(); + while (pPortion && (pPortion->InGlueGrp() || pPortion->IsKernPortion() || pPortion->IsFlyPortion())) + { // skip margins and fly spacers + pPortion = pPortion->GetNextPortion(); + } + } + return pPortion != nullptr; +} + +const SwDropPortion *SwParaPortion::FindDropPortion() const +{ + const SwLineLayout *pLay = this; + while( pLay && pLay->IsDummy() ) + pLay = pLay->GetNext(); + while( pLay ) + { + const SwLinePortion *pPos = pLay->GetNextPortion(); + while ( pPos && !pPos->GetLen() ) + pPos = pPos->GetNextPortion(); + if( pPos && pPos->IsDropPortion() ) + return static_cast<const SwDropPortion *>(pPos); + pLay = pLay->GetLen() ? nullptr : pLay->GetNext(); + } + return nullptr; +} + +void SwParaPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwParaPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterEndElement(pWriter); +} + +void SwLineLayout::Init( SwLinePortion* pNextPortion ) +{ + Height( 0, false ); + Width( 0 ); + SetLen(TextFrameIndex(0)); + SetAscent( 0 ); + SetRealHeight( 0 ); + SetNextPortion( pNextPortion ); +} + +// looks for hanging punctuation portions in the paragraph +// and return the maximum right offset of them. +// If no such portion is found, the Margin/Hanging-flags will be updated. +SwTwips SwLineLayout::GetHangingMargin_() const +{ + SwLinePortion* pPor = GetNextPortion(); + bool bFound = false; + SwTwips nDiff = 0; + while( pPor) + { + if( pPor->IsHangingPortion() ) + { + nDiff = static_cast<SwHangingPortion*>(pPor)->GetInnerWidth() - pPor->Width(); + if( nDiff ) + bFound = true; + } + // the last post its portion + else if ( pPor->IsPostItsPortion() && ! pPor->GetNextPortion() ) + nDiff = mnAscent; + + pPor = pPor->GetNextPortion(); + } + if( !bFound ) // update the hanging-flag + const_cast<SwLineLayout*>(this)->SetHanging( false ); + return nDiff; +} + +SwTwips SwTextFrame::HangingMargin() const +{ + SAL_WARN_IF( !HasPara(), "sw.core", "Don't call me without a paraportion" ); + if( !GetPara()->IsMargin() ) + return 0; + const SwLineLayout* pLine = GetPara(); + SwTwips nRet = 0; + do + { + SwTwips nDiff = pLine->GetHangingMargin(); + if( nDiff > nRet ) + nRet = nDiff; + pLine = pLine->GetNext(); + } while ( pLine ); + if( !nRet ) // update the margin-flag + const_cast<SwParaPortion*>(GetPara())->SetMargin( false ); + return nRet; +} + +SwTwips SwTextFrame::GetLowerMarginForFlyIntersect() const +{ + const IDocumentSettingAccess& rIDSA = GetDoc().getIDocumentSettingAccess(); + if (!rIDSA.get(DocumentSettingId::TAB_OVER_MARGIN)) + { + // Word >= 2013 style or Writer style: lower margin is ignored when determining the text + // frame height. + return 0; + } + + const SwAttrSet* pAttrSet = GetTextNodeForParaProps()->GetpSwAttrSet(); + if (!pAttrSet) + { + return 0; + } + + // If it has multiple lines, then probably it already has the needed fly portion. + // Limit this to empty paragraphs for now. + if ((GetPara() && GetPara()->GetNext()) || !GetText().isEmpty()) + { + return 0; + } + + return pAttrSet->GetULSpace().GetLower(); +} + +void SwScriptInfo::selectHiddenTextProperty(const SwTextNode& rNode, + MultiSelection & rHiddenMulti, + std::vector<std::pair<sw::mark::IBookmark const*, MarkKind>> *const pBookmarks) +{ + assert((rNode.GetText().isEmpty() && rHiddenMulti.GetTotalRange().Len() == 1) + || (rNode.GetText().getLength() == rHiddenMulti.GetTotalRange().Len())); + + const SvxCharHiddenItem* pItem = rNode.GetSwAttrSet().GetItemIfSet( RES_CHRATR_HIDDEN ); + if( pItem && pItem->GetValue() ) + { + rHiddenMulti.SelectAll(); + } + + const SwpHints* pHints = rNode.GetpSwpHints(); + + if( pHints ) + { + for( size_t nTmp = 0; nTmp < pHints->Count(); ++nTmp ) + { + const SwTextAttr* pTextAttr = pHints->Get( nTmp ); + const SvxCharHiddenItem* pHiddenItem = CharFormat::GetItem( *pTextAttr, RES_CHRATR_HIDDEN ); + if( pHiddenItem ) + { + const sal_Int32 nSt = pTextAttr->GetStart(); + const sal_Int32 nEnd = *pTextAttr->End(); + if( nEnd > nSt ) + { + Range aTmp( nSt, nEnd - 1 ); + rHiddenMulti.Select( aTmp, pHiddenItem->GetValue() ); + } + } + } + } + + for (const SwContentIndex* pIndex = rNode.GetFirstIndex(); pIndex; pIndex = pIndex->GetNext()) + { + const sw::mark::IMark* pMark = pIndex->GetMark(); + const sw::mark::IBookmark* pBookmark = dynamic_cast<const sw::mark::IBookmark*>(pMark); + if (pBookmarks && pBookmark) + { + if (!pBookmark->IsExpanded()) + { + pBookmarks->emplace_back(pBookmark, MarkKind::Point); + } + else if (pIndex == &pBookmark->GetMarkStart().nContent) + { + pBookmarks->emplace_back(pBookmark, MarkKind::Start); + } + else + { + assert(pIndex == &pBookmark->GetMarkEnd().nContent); + pBookmarks->emplace_back(pBookmark, MarkKind::End); + } + } + + // condition is evaluated in DocumentFieldsManager::UpdateExpFields() + if (pBookmark && pBookmark->IsHidden()) + { + // intersect bookmark range with textnode range and add the intersection to rHiddenMulti + + const sal_Int32 nSt = pBookmark->GetMarkStart().GetContentIndex(); + const sal_Int32 nEnd = pBookmark->GetMarkEnd().GetContentIndex(); + + if( nEnd > nSt ) + { + Range aTmp( nSt, nEnd - 1 ); + rHiddenMulti.Select(aTmp, true); + } + } + } +} + +void SwScriptInfo::selectRedLineDeleted(const SwTextNode& rNode, MultiSelection &rHiddenMulti, bool bSelect) +{ + assert((rNode.GetText().isEmpty() && rHiddenMulti.GetTotalRange().Len() == 1) + || (rNode.GetText().getLength() == rHiddenMulti.GetTotalRange().Len())); + + const IDocumentRedlineAccess& rIDRA = rNode.getIDocumentRedlineAccess(); + if ( !IDocumentRedlineAccess::IsShowChanges( rIDRA.GetRedlineFlags() ) ) + return; + + SwRedlineTable::size_type nAct = rIDRA.GetRedlinePos( rNode, RedlineType::Any ); + + for ( ; nAct < rIDRA.GetRedlineTable().size(); nAct++ ) + { + const SwRangeRedline* pRed = rIDRA.GetRedlineTable()[ nAct ]; + + if (pRed->Start()->GetNode() > rNode) + break; + + if (pRed->GetType() != RedlineType::Delete) + continue; + + sal_Int32 nRedlStart; + sal_Int32 nRedlnEnd; + pRed->CalcStartEnd( rNode.GetIndex(), nRedlStart, nRedlnEnd ); + //clip it if the redline extends past the end of the nodes text + nRedlnEnd = std::min<sal_Int32>(nRedlnEnd, rNode.GetText().getLength()); + if ( nRedlnEnd > nRedlStart ) + { + Range aTmp( nRedlStart, nRedlnEnd - 1 ); + rHiddenMulti.Select( aTmp, bSelect ); + } + } +} + +// Returns a MultiSection indicating the hidden ranges. +void SwScriptInfo::CalcHiddenRanges( const SwTextNode& rNode, + MultiSelection & rHiddenMulti, + std::vector<std::pair<sw::mark::IBookmark const*, MarkKind>> *const pBookmarks) +{ + selectHiddenTextProperty(rNode, rHiddenMulti, pBookmarks); + + // If there are any hidden ranges in the current text node, we have + // to unhide the redlining ranges: + selectRedLineDeleted(rNode, rHiddenMulti, false); + + // We calculated a lot of stuff. Finally we can update the flags at the text node. + + const bool bNewContainsHiddenChars = rHiddenMulti.GetRangeCount() > 0; + bool bNewHiddenCharsHidePara = false; + if ( bNewContainsHiddenChars ) + { + const Range& rRange = rHiddenMulti.GetRange( 0 ); + const sal_Int32 nHiddenStart = rRange.Min(); + const sal_Int32 nHiddenEnd = rRange.Max() + 1; + bNewHiddenCharsHidePara = + (nHiddenStart == 0 && nHiddenEnd >= rNode.GetText().getLength()); + } + rNode.SetHiddenCharAttribute( bNewHiddenCharsHidePara, bNewContainsHiddenChars ); +} + +TextFrameIndex SwScriptInfo::CountCJKCharacters(const OUString &rText, + TextFrameIndex nPos, TextFrameIndex const nEnd, LanguageType aLang) +{ + TextFrameIndex nCount(0); + if (nEnd > nPos) + { + sal_Int32 nDone = 0; + const lang::Locale &rLocale = g_pBreakIt->GetLocale( aLang ); + while ( nPos < nEnd ) + { + nPos = TextFrameIndex(g_pBreakIt->GetBreakIter()->nextCharacters( + rText, sal_Int32(nPos), + rLocale, + i18n::CharacterIteratorMode::SKIPCELL, 1, nDone)); + nCount++; + } + } + else + nCount = nEnd - nPos ; + + return nCount; +} + +void SwScriptInfo::CJKJustify( const OUString& rText, KernArray& rKernArray, + TextFrameIndex const nStt, + TextFrameIndex const nLen, LanguageType aLang, + tools::Long nSpaceAdd, bool bIsSpaceStop ) +{ + assert( sal_Int32(nStt) >= 0 ); + if (sal_Int32(nLen) <= 0) + return; + + tools::Long nSpaceSum = 0; + const lang::Locale &rLocale = g_pBreakIt->GetLocale( aLang ); + sal_Int32 nDone = 0; + sal_Int32 nNext(nStt); + for ( sal_Int32 nI = 0; nI < sal_Int32(nLen); ++nI ) + { + if (nI + sal_Int32(nStt) == nNext) + { + nNext = g_pBreakIt->GetBreakIter()->nextCharacters( rText, nNext, + rLocale, + i18n::CharacterIteratorMode::SKIPCELL, 1, nDone ); + if (nNext < sal_Int32(nStt + nLen) || !bIsSpaceStop) + nSpaceSum += nSpaceAdd; + } + rKernArray.adjust(nI, nSpaceSum); + } +} +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porlay.hxx b/sw/source/core/text/porlay.hxx new file mode 100644 index 0000000000..3b07b70161 --- /dev/null +++ b/sw/source/core/text/porlay.hxx @@ -0,0 +1,352 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <scriptinfo.hxx> + +#include <swrect.hxx> +#include <swtypes.hxx> +#include "portxt.hxx" +#include <svx/ctredlin.hxx> + +#include <vector> +#include <deque> + +class SwMarginPortion; +class SwDropPortion; +class SwTextFormatter; + +class SwCharRange +{ +private: + TextFrameIndex m_nStart; + TextFrameIndex m_nLen; + +public: + SwCharRange(TextFrameIndex const nInitStart = TextFrameIndex(0), + TextFrameIndex const nInitLen = TextFrameIndex(0)) + : m_nStart( nInitStart ), m_nLen(nInitLen) {} + TextFrameIndex & Start() { return m_nStart; } + TextFrameIndex const& Start() const { return m_nStart; } + void LeftMove(TextFrameIndex const nNew) + { if ( nNew < m_nStart ) { m_nLen += m_nStart-nNew; m_nStart = nNew; } } + TextFrameIndex & Len() { return m_nLen; } + TextFrameIndex const& Len() const { return m_nLen; } + bool operator<(const SwCharRange &rRange) const + { return m_nStart < rRange.m_nStart; } + bool operator>(const SwCharRange &rRange) const + { return m_nStart + m_nLen > rRange.m_nStart + rRange.m_nLen; } + bool operator!=(const SwCharRange &rRange) const + { return *this < rRange || *this > rRange; } + SwCharRange &operator+=(const SwCharRange &rRange); +}; + +// SwRepaint is a document-global SwRect +// nOfst states from where in the first line should be painted +// nRightOfst gives the right margin +class SwRepaint : public SwRect +{ + SwTwips m_nOffset; + SwTwips m_nRightOffset; +public: + SwRepaint() : SwRect(), m_nOffset( 0 ), m_nRightOffset( 0 ) {} + + SwTwips GetOffset() const { return m_nOffset; } + void SetOffset( const SwTwips nNew ) { m_nOffset = nNew; } + SwTwips GetRightOfst() const { return m_nRightOffset; } + void SetRightOfst( const SwTwips nNew ) { m_nRightOffset = nNew; } +}; + +/// Collection of SwLinePortion instances, representing one line of text. +/// Typically owned by an SwParaPortion. +class SW_DLLPUBLIC SwLineLayout : public SwTextPortion +{ +private: + SwLineLayout *m_pNext; // The next Line + std::unique_ptr<std::vector<tools::Long>> m_pLLSpaceAdd; // Used for justified alignment + std::unique_ptr<std::deque<sal_uInt16>> m_pKanaComp; // Used for Kana compression + SwTwips m_nRealHeight; // The height resulting from line spacing and register + SwTwips m_nTextHeight; // The max height of all non-FlyCnt portions in this Line + bool m_bFormatAdj : 1; + bool m_bDummy : 1; + bool m_bEndHyph : 1; + bool m_bMidHyph : 1; + bool m_bFly : 1; + bool m_bRest : 1; + bool m_bBlinking : 1; + bool m_bClipping : 1; // Clipping needed for exact line height + bool m_bContent : 1; // Text for line numbering + bool m_bRedline : 1; // The Redlining + bool m_bRedlineEnd: 1; // Redlining for paragraph mark: tracked change at the end + bool m_bForcedLeftMargin : 1; // Left adjustment moved by the Fly + bool m_bHanging : 1; // Contains a hanging portion in the margin + bool m_bUnderscore : 1; + + enum RedlineType m_eRedlineEnd; // redline type of pilcrow and line break symbols + + OUString m_sRedlineText; // shortened text of (first) tracked deletion shown in margin + + SwTwips GetHangingMargin_() const; + + void DeleteNext(); +public: + // From SwPosSize + using SwPosSize::Height; + virtual void Height(const SwTwips nNew, const bool bText = true) override; + + // From SwLinePortion + virtual SwLinePortion *Insert( SwLinePortion *pPortion ) override; + virtual SwLinePortion *Append( SwLinePortion *pPortion ) override; + SwLinePortion *GetFirstPortion() const; + + // Flags + void ResetFlags(); + void SetFormatAdj( const bool bNew ) { m_bFormatAdj = bNew; } + bool IsFormatAdj() const { return m_bFormatAdj; } + void SetEndHyph( const bool bNew ) { m_bEndHyph = bNew; } + bool IsEndHyph() const { return m_bEndHyph; } + void SetMidHyph( const bool bNew ) { m_bMidHyph = bNew; } + bool IsMidHyph() const { return m_bMidHyph; } + void SetFly( const bool bNew ) { m_bFly = bNew; } + bool IsFly() const { return m_bFly; } + void SetRest( const bool bNew ) { m_bRest = bNew; } + bool IsRest() const { return m_bRest; } + void SetBlinking( const bool bNew ) { m_bBlinking = bNew; } + bool IsBlinking() const { return m_bBlinking; } + void SetContent( const bool bNew ) { m_bContent = bNew; } + bool HasContent() const { return m_bContent; } + void SetRedline( const bool bNew ) { m_bRedline = bNew; } + bool HasRedline() const { return m_bRedline; } + void SetRedlineEnd( const bool bNew ) { m_bRedlineEnd = bNew; } + bool HasRedlineEnd() const { return m_bRedlineEnd; } + void SetRedlineEndType( const enum RedlineType eNew ) { m_eRedlineEnd = eNew; } + RedlineType GetRedlineEndType() const { return m_eRedlineEnd; } + void SetRedlineText ( const OUString& sText ) { m_sRedlineText = sText; } + const OUString* GetRedlineText() const { return &m_sRedlineText; } + void SetForcedLeftMargin() { m_bForcedLeftMargin = true; } + bool HasForcedLeftMargin() const { return m_bForcedLeftMargin; } + void SetHanging( const bool bNew ) { m_bHanging = bNew; } + bool IsHanging() const { return m_bHanging; } + void SetUnderscore( const bool bNew ) { m_bUnderscore = bNew; } + bool HasUnderscore() const { return m_bUnderscore; } + + // Respecting empty dummy lines + void SetDummy( const bool bNew ) { m_bDummy = bNew; } + bool IsDummy() const { return m_bDummy; } + + void SetClipping( const bool bNew ) { m_bClipping = bNew; } + bool IsClipping() const { return m_bClipping; } + + SwLineLayout(); + virtual ~SwLineLayout() override; + + SwLineLayout *GetNext() { return m_pNext; } + const SwLineLayout *GetNext() const { return m_pNext; } + void SetNext( SwLineLayout *pNew ) { m_pNext = pNew; } + + void Init( SwLinePortion *pNextPortion = nullptr); + + // Collects the data for the line + void CalcLine( SwTextFormatter &rLine, SwTextFormatInfo &rInf ); + + void SetRealHeight( SwTwips nNew ) { m_nRealHeight = nNew; } + SwTwips GetRealHeight() const { return m_nRealHeight; } + + SwTwips GetTextHeight() const { return m_nTextHeight; } + + // Creates the glue chain for short lines + SwMarginPortion *CalcLeftMargin(); + + SwTwips GetHangingMargin() const + { return GetHangingMargin_(); } + + // For special treatment for empty lines + virtual bool Format( SwTextFormatInfo &rInf ) override; + + // Stuff for justified alignment + bool IsSpaceAdd() const { return m_pLLSpaceAdd != nullptr; } + void InitSpaceAdd(); // Creates pLLSpaceAdd if necessary + void CreateSpaceAdd( const tools::Long nInit = 0 ); + void FinishSpaceAdd() { m_pLLSpaceAdd.reset(); } + sal_uInt16 GetLLSpaceAddCount() const { return sal::static_int_cast< sal_uInt16 >(m_pLLSpaceAdd->size()); } + void SetLLSpaceAdd( tools::Long nNew, sal_uInt16 nIdx ) + { + if ( nIdx == GetLLSpaceAddCount() ) + m_pLLSpaceAdd->push_back( nNew ); + else + (*m_pLLSpaceAdd)[ nIdx ] = nNew; + } + tools::Long GetLLSpaceAdd( sal_uInt16 nIdx ) { return (*m_pLLSpaceAdd)[ nIdx ]; } + void RemoveFirstLLSpaceAdd() { m_pLLSpaceAdd->erase( m_pLLSpaceAdd->begin() ); } + std::vector<tools::Long>* GetpLLSpaceAdd() const { return m_pLLSpaceAdd.get(); } + + // Stuff for Kana compression + void SetKanaComp( std::unique_ptr<std::deque<sal_uInt16>> pNew ){ m_pKanaComp = std::move(pNew); } + void FinishKanaComp() { m_pKanaComp.reset(); } + std::deque<sal_uInt16>* GetpKanaComp() const { return m_pKanaComp.get(); } + std::deque<sal_uInt16>& GetKanaComp() { return *m_pKanaComp; } + + /** determine ascent and descent for positioning of as-character anchored + object + + OD 07.01.2004 #i11859# - previously local method <lcl_MaxAscDescent> + Method calculates maximum ascents and descents of the line layout. + One value considering as-character anchored objects, one without these + objects. + Portions for other anchored objects aren't considered. + OD 2005-05-20 #i47162# - add optional parameter <_bNoFlyCntPorAndLinePor> + to control, if the fly content portions and line portion are considered. + + @param _orAscent + output parameter - maximum ascent without as-character anchored objects + + @param _orDescent + output parameter - maximum descent without as-character anchored objects + + @param _orObjAscent + output parameter - maximum ascent with as-character anchored objects + + @param _orObjDescent + output parameter - maximum descent with as-character anchored objects + + @param _pDontConsiderPortion + input parameter - portion, which isn't considered for calculating + <_orObjAscent> and <_orObjDescent>, if it isn't a portion for a + as-character anchored object or it isn't as high as the line. + + @param _bNoFlyCntPorAndLinePor + optional input parameter - boolean, indicating that fly content portions + and the line portion are considered or not. + */ + void MaxAscentDescent( SwTwips& _orAscent, + SwTwips& _orDescent, + SwTwips& _orObjAscent, + SwTwips& _orObjDescent, + const SwLinePortion* _pDontConsiderPortion = nullptr, + const bool _bNoFlyCntPorAndLinePor = false ) const; + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const override; +}; + +/// Collection of SwLineLayout instances, represents the paragraph text in Writer layout. +/// Typically owned by an SwTextFrame. +class SwParaPortion : public SwLineLayout +{ + // Area that needs repainting + SwRepaint m_aRepaint; + // Area that needs reformatting + SwCharRange m_aReformat; + SwScriptInfo m_aScriptInfo; + + // Fraction aZoom; + tools::Long m_nDelta; + + // If a SwTextFrame is locked, no changes occur to the formatting data (under + // pLine) (compare with Orphans) + bool m_bFlys : 1; // Overlapping Flys? + bool m_bPrep : 1; // PREP_* + bool m_bPrepWidows : 1; // PrepareHint::Widows + bool m_bPrepAdjust : 1; // PrepareHint::AdjustSizeWithoutFormatting + bool m_bPrepMustFit : 1; // PrepareHint::MustFit + bool m_bFollowField : 1; // We have a bit of field left for the Follow + + bool m_bFixLineHeight : 1; // Fixed line height + bool m_bFootnoteNum : 1; // is the frame that may contain a footnotenumberportion + bool m_bMargin : 1; // contains a hanging punctuation in the margin + +public: + SwParaPortion(); + virtual ~SwParaPortion() override; + + // Resets all formatting information (except for bFlys) + inline void FormatReset(); + + // Resets the Flags + inline void ResetPreps(); + + // Get/Set methods + SwRepaint& GetRepaint() { return m_aRepaint; } + const SwRepaint& GetRepaint() const { return m_aRepaint; } + SwCharRange& GetReformat() { return m_aReformat; } + const SwCharRange& GetReformat() const { return m_aReformat; } + void SetDelta(tools::Long nDelta) { m_nDelta = nDelta; } + tools::Long GetDelta() const { return m_nDelta; } + SwScriptInfo& GetScriptInfo() { return m_aScriptInfo; } + const SwScriptInfo& GetScriptInfo() const { return m_aScriptInfo; } + + // For SwTextFrame::Format: returns the paragraph's current length + TextFrameIndex GetParLen() const; + + // For Prepare() + bool UpdateQuoVadis( std::u16string_view rQuo ); + + // Flags + void SetFly() { m_bFlys = true; } + bool HasFly() const { return m_bFlys; } + + // Preps + void SetPrep() { m_bPrep = true; } + bool IsPrep() const { return m_bPrep; } + void SetPrepWidows() { m_bPrepWidows = true; } + bool IsPrepWidows() const { return m_bPrepWidows; } + void SetPrepMustFit( const bool bNew ) { m_bPrepMustFit = bNew; } + bool IsPrepMustFit() const { return m_bPrepMustFit; } + void SetPrepAdjust() { m_bPrepAdjust = true; } + bool IsPrepAdjust() const { return m_bPrepAdjust; } + void SetFollowField( const bool bNew ) { m_bFollowField = bNew; } + bool IsFollowField() const { return m_bFollowField; } + void SetFixLineHeight() { m_bFixLineHeight = true; } + bool IsFixLineHeight() const { return m_bFixLineHeight; } + + void SetFootnoteNum( const bool bNew ) { m_bFootnoteNum = bNew; } + bool IsFootnoteNum() const { return m_bFootnoteNum; } + void SetMargin( const bool bNew = true ) { m_bMargin = bNew; } + bool IsMargin() const { return m_bMargin; } + enum FootnoteOrNot { OnlyNumbering, FootnoteToo }; + bool HasNumberingPortion(FootnoteOrNot) const; + bool HasContentPortions() const; + + // Set nErgo in the QuoVadisPortion + void SetErgoSumNum( const OUString &rErgo ); + + const SwDropPortion *FindDropPortion() const; + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const override; +}; + +inline void SwParaPortion::ResetPreps() +{ + m_bPrep = m_bPrepWidows = m_bPrepAdjust = m_bPrepMustFit = false; +} + +inline void SwParaPortion::FormatReset() +{ + m_nDelta = 0; + m_aReformat = SwCharRange(TextFrameIndex(0), TextFrameIndex(COMPLETE_STRING)); + // bFlys needs to be retained in SwTextFrame::Format_() so that empty + // paragraphs that needed to avoid Frames with no flow, reformat + // when the Frame disappears from the Area + // bFlys = false; + ResetPreps(); + m_bFollowField = m_bFixLineHeight = m_bMargin = false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porlin.cxx b/sw/source/core/text/porlin.cxx new file mode 100644 index 0000000000..a420d64301 --- /dev/null +++ b/sw/source/core/text/porlin.cxx @@ -0,0 +1,358 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <vcl/outdev.hxx> +#include <SwPortionHandler.hxx> + +#include "porlin.hxx" +#include "portxt.hxx" +#include "inftxt.hxx" +#include "pormulti.hxx" +#if OSL_DEBUG_LEVEL > 0 + +static bool ChkChain( SwLinePortion *pStart ) +{ + SwLinePortion *pPor = pStart->GetNextPortion(); + sal_uInt16 nCount = 0; + while( pPor ) + { + ++nCount; + OSL_ENSURE( nCount < 200 && pPor != pStart, + "ChkChain(): lost in chains" ); + if( nCount >= 200 || pPor == pStart ) + { + // the lifesaver + pPor = pStart->GetNextPortion(); + pStart->SetNextPortion(nullptr); + pPor->Truncate(); + pStart->SetNextPortion( pPor ); + return false; + } + pPor = pPor->GetNextPortion(); + } + return true; +} +#endif + +SwLinePortion::~SwLinePortion() +{ +} + +SwLinePortion *SwLinePortion::Compress() +{ + return GetLen() || Width() ? this : nullptr; +} + +sal_uInt16 SwLinePortion::GetViewWidth( const SwTextSizeInfo & ) const +{ + return 0; +} + +SwLinePortion::SwLinePortion( ) : + mpNextPortion( nullptr ), + mnLineLength( 0 ), + mnAscent( 0 ), + mnHangingBaseline( 0 ), + mnWhichPor( PortionType::NONE ), + m_bJoinBorderWithPrev(false), + m_bJoinBorderWithNext(false) +{ +} + +void SwLinePortion::PrePaint( const SwTextPaintInfo& rInf, + const SwLinePortion* pLast ) const +{ + OSL_ENSURE( rInf.OnWin(), "SwLinePortion::PrePaint: don't prepaint on a printer"); + OSL_ENSURE( !Width(), "SwLinePortion::PrePaint: For Width()==0 only!"); + + const sal_uInt16 nViewWidth = GetViewWidth( rInf ); + + if( ! nViewWidth ) + return; + + const sal_uInt16 nHalfView = nViewWidth / 2; + sal_uInt16 nLastWidth = pLast->Width() + pLast->ExtraBlankWidth(); + + if ( pLast->InSpaceGrp() && rInf.GetSpaceAdd(/*bShrink=*/true) ) + nLastWidth += pLast->CalcSpacing( rInf.GetSpaceAdd(/*bShrink=*/true), rInf ); + + sal_uInt16 nPos; + SwTextPaintInfo aInf( rInf ); + + const bool bBidiPor = rInf.GetTextFrame()->IsRightToLeft() != + bool( vcl::text::ComplexTextLayoutFlags::BiDiRtl & rInf.GetOut()->GetLayoutMode() ); + + Degree10 nDir = bBidiPor ? + 1800_deg10 : + rInf.GetFont()->GetOrientation( rInf.GetTextFrame()->IsVertical() ); + + // pLast == this *only* for the 1st portion in the line so nLastWidth is 0; + // allow this too, will paint outside the frame but might look better... + if (nLastWidth > nHalfView || pLast == this) + { + switch (nDir.get()) + { + case 0: + nPos = sal_uInt16( rInf.X() ); + nPos += nLastWidth - nHalfView; + aInf.X( nPos ); + break; + case 900: + nPos = sal_uInt16( rInf.Y() ); + nPos -= nLastWidth - nHalfView; + aInf.Y( nPos ); + break; + case 1800: + nPos = sal_uInt16( rInf.X() ); + nPos -= nLastWidth - nHalfView; + aInf.X( nPos ); + break; + case 2700: + nPos = sal_uInt16( rInf.Y() ); + nPos += nLastWidth - nHalfView; + aInf.Y( nPos ); + break; + } + } + + SwLinePortion *pThis = const_cast<SwLinePortion*>(this); + pThis->Width( nViewWidth ); + Paint( aInf ); + pThis->Width(0); +} + +void SwLinePortion::CalcTextSize( const SwTextSizeInfo &rInf ) +{ + if( GetLen() == rInf.GetLen() ) + *static_cast<SwPosSize*>(this) = GetTextSize( rInf ); + else + { + SwTextSizeInfo aInf( rInf ); + aInf.SetLen( GetLen() ); + *static_cast<SwPosSize*>(this) = GetTextSize( aInf ); + } +} + +// all following portions will be deleted +void SwLinePortion::Truncate_() +{ + SwLinePortion *pPos = mpNextPortion; + do + { + OSL_ENSURE( pPos != this, "SwLinePortion::Truncate: loop" ); + SwLinePortion *pLast = pPos; + pPos = pPos->GetNextPortion(); + pLast->SetNextPortion( nullptr ); + delete pLast; + + } while( pPos ); + + mpNextPortion = nullptr; +} + +// It always will be inserted after us. +SwLinePortion *SwLinePortion::Insert( SwLinePortion *pIns ) +{ + pIns->FindLastPortion()->SetNextPortion( mpNextPortion ); + SetNextPortion( pIns ); +#if OSL_DEBUG_LEVEL > 0 + ChkChain( this ); +#endif + return pIns; +} + +SwLinePortion *SwLinePortion::FindLastPortion() +{ + SwLinePortion *pPos = this; + // Find the end and link pLinPortion to the last one... + while( pPos->GetNextPortion() ) + { + pPos = pPos->GetNextPortion(); + } + return pPos; +} + +SwLinePortion *SwLinePortion::Append( SwLinePortion *pIns ) +{ + SwLinePortion *pPos = FindLastPortion(); + pPos->SetNextPortion( pIns ); + pIns->SetNextPortion( nullptr ); +#if OSL_DEBUG_LEVEL > 0 + ChkChain( this ); +#endif + return pIns; +} + +SwLinePortion *SwLinePortion::Cut( SwLinePortion *pVictim ) +{ + SwLinePortion *pPrev = pVictim->FindPrevPortion( this ); + OSL_ENSURE( pPrev, "SwLinePortion::Cut(): can't cut" ); + // note: if pVictim is a follow then clearing pPrev's m_bHasFollow here is + // tricky because it could be that the HookChar inserted a tab portion + // between 2 field portions + pPrev->SetNextPortion( pVictim->GetNextPortion() ); + pVictim->SetNextPortion(nullptr); + return pVictim; +} + +SwLinePortion *SwLinePortion::FindPrevPortion( const SwLinePortion *pRoot ) +{ + OSL_ENSURE( pRoot != this, "SwLinePortion::FindPrevPortion(): invalid root" ); + SwLinePortion *pPos = const_cast<SwLinePortion*>(pRoot); + while( pPos->GetNextPortion() && pPos->GetNextPortion() != this ) + { + pPos = pPos->GetNextPortion(); + } + OSL_ENSURE( pPos->GetNextPortion(), + "SwLinePortion::FindPrevPortion: blowing in the wind"); + return pPos; +} + +TextFrameIndex SwLinePortion::GetModelPositionForViewPoint(const sal_uInt16 nOfst) const +{ + if( nOfst > ( PrtWidth() / 2 ) ) + return GetLen(); + else + return TextFrameIndex(0); +} + +SwPosSize SwLinePortion::GetTextSize( const SwTextSizeInfo & ) const +{ + OSL_ENSURE( false, "SwLinePortion::GetTextSize: don't ask me about sizes, " + "I'm only a stupid SwLinePortion" ); + return SwPosSize(); +} + +bool SwLinePortion::Format( SwTextFormatInfo &rInf ) +{ + if( rInf.X() > rInf.Width() ) + { + Truncate(); + rInf.SetUnderflow( this ); + return true; + } + + const SwLinePortion *pLast = rInf.GetLast(); + Height( pLast->Height() ); + SetAscent( pLast->GetAscent() ); + const sal_uInt16 nNewWidth = o3tl::narrowing<sal_uInt16>(rInf.X() + PrtWidth()); + // Only portions with true width can return true + // Notes for example never set bFull==true + if( rInf.Width() <= nNewWidth && PrtWidth() && ! IsKernPortion() ) + { + Truncate(); + if( nNewWidth > rInf.Width() ) + PrtWidth( nNewWidth - rInf.Width() ); + rInf.GetLast()->FormatEOL( rInf ); + return true; + } + return false; +} + +// Format end of line + +void SwLinePortion::FormatEOL( SwTextFormatInfo & ) +{ } + +void SwLinePortion::Move(SwTextPaintInfo & rInf) const +{ + bool bB2T = rInf.GetDirection() == DIR_BOTTOM2TOP; + const bool bFrameDir = rInf.GetTextFrame()->IsRightToLeft(); + bool bCounterDir = ( ! bFrameDir && DIR_RIGHT2LEFT == rInf.GetDirection() ) || + ( bFrameDir && DIR_LEFT2RIGHT == rInf.GetDirection() ); + + SwTwips nTmp = PrtWidth() + ExtraBlankWidth(); + if ( InSpaceGrp() && rInf.GetSpaceAdd(/*bShrink=*/true) ) + { + nTmp += CalcSpacing( rInf.GetSpaceAdd(/*bShrink=*/true), rInf ); + if( rInf.IsRotated() ) + rInf.Y( rInf.Y() + ( bB2T ? -nTmp : nTmp ) ); + else if ( bCounterDir ) + rInf.X( rInf.X() - nTmp ); + else + rInf.X( rInf.X() + nTmp ); + } + else + { + if( InFixMargGrp() && !IsMarginPortion() ) + { + rInf.IncSpaceIdx(); + rInf.IncKanaIdx(); + } + if( rInf.IsRotated() ) + rInf.Y(rInf.Y() + (bB2T ? -nTmp : nTmp)); + else if ( bCounterDir ) + rInf.X(rInf.X() - nTmp); + else + rInf.X(rInf.X() + nTmp); + } + if (IsMultiPortion() && static_cast<SwMultiPortion const*>(this)->HasTabulator()) + rInf.IncSpaceIdx(); + + rInf.SetIdx( rInf.GetIdx() + GetLen() ); +} + +SwTwips SwLinePortion::CalcSpacing( tools::Long , const SwTextSizeInfo & ) const +{ + return 0; +} + +bool SwLinePortion::GetExpText( const SwTextSizeInfo &, OUString & ) const +{ + return false; +} + +void SwLinePortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Special( GetLen(), OUString(), GetWhichPor() ); +} + +void SwLinePortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwLinePortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterEndElement(pWriter); +} + +void SwLinePortion::dumpAsXmlAttributes(xmlTextWriterPtr pWriter, std::u16string_view rText, TextFrameIndex nOffset) const +{ + (void)xmlTextWriterWriteFormatAttribute(pWriter, BAD_CAST("ptr"), "%p", this); + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("symbol"), BAD_CAST(typeid(*this).name())); + (void)xmlTextWriterWriteAttribute( + pWriter, BAD_CAST("width"), + BAD_CAST(OString::number(Width()).getStr())); + (void)xmlTextWriterWriteAttribute( + pWriter, BAD_CAST("height"), + BAD_CAST(OString::number(Height()).getStr())); + (void)xmlTextWriterWriteAttribute( + pWriter, BAD_CAST("length"), + BAD_CAST(OString::number(static_cast<sal_Int32>(mnLineLength)).getStr())); + (void)xmlTextWriterWriteAttribute( + pWriter, BAD_CAST("type"), + BAD_CAST(sw::PortionTypeToString(GetWhichPor()))); + OUString aText( rText.substr(sal_Int32(nOffset), sal_Int32(GetLen())) ); + for (int i = 0; i < 32; ++i) + aText = aText.replace(i, '*'); + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("portion"), + BAD_CAST(aText.toUtf8().getStr())); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porlin.hxx b/sw/source/core/text/porlin.hxx new file mode 100644 index 0000000000..50ee3ed159 --- /dev/null +++ b/sw/source/core/text/porlin.hxx @@ -0,0 +1,220 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <libxml/xmlwriter.h> + +#include "possiz.hxx" +#include <txttypes.hxx> +#include <TextFrameIndex.hxx> +#include <rtl/ustring.hxx> + +class SwTextSizeInfo; +class SwTextPaintInfo; +class SwTextFormatInfo; +class SwPortionHandler; + +/// Portion groups +/// @see enum PortionType in txttypes.hxx +#define PORGRP_TXT 0x8000 +#define PORGRP_EXP 0x4000 +#define PORGRP_FLD 0x2000 +#define PORGRP_HYPH 0x1000 +#define PORGRP_NUMBER 0x0800 +#define PORGRP_GLUE 0x0400 +#define PORGRP_FIX 0x0200 +#define PORGRP_TAB 0x0100 +// Small special groups +#define PORGRP_FIXMARG 0x0040 +//#define PORGRP_? 0x0020 +#define PORGRP_TABNOTLFT 0x0010 +#define PORGRP_TOXREF 0x0008 + +/// Base class for anything that can be part of a line in the Writer layout. +/// Typically owned by SwLineLayout. +class SAL_DLLPUBLIC_RTTI SwLinePortion: public SwPosSize +{ +protected: + // Here we have areas with different attributes + SwLinePortion *mpNextPortion; + // Count of chars and spaces on the line + TextFrameIndex mnLineLength; + SwTwips mnAscent; // Maximum ascender + SwTwips mnHangingBaseline; + + SwLinePortion(); +private: + PortionType mnWhichPor; // Who's who? + bool m_bJoinBorderWithPrev; + bool m_bJoinBorderWithNext; + SwTwips m_nExtraBlankWidth = 0; // width of spaces after the break + + void Truncate_(); + +public: + explicit inline SwLinePortion(const SwLinePortion &rPortion); + virtual ~SwLinePortion(); + + // Access methods + SwLinePortion *GetNextPortion() const { return mpNextPortion; } + inline SwLinePortion &operator=(const SwLinePortion &rPortion); + TextFrameIndex GetLen() const { return mnLineLength; } + void SetLen(TextFrameIndex const nLen) { mnLineLength = nLen; } + void SetNextPortion( SwLinePortion *pNew ){ mpNextPortion = pNew; } + SwTwips &GetAscent() { return mnAscent; } + SwTwips GetAscent() const { return mnAscent; } + void SetAscent( const SwTwips nNewAsc ) { mnAscent = nNewAsc; } + void PrtWidth( SwTwips nNewWidth ) { Width( nNewWidth ); } + SwTwips PrtWidth() const { return Width(); } + void AddPrtWidth( const SwTwips nNew ) { Width( Width() + nNew ); } + void SubPrtWidth( const SwTwips nNew ) { Width( Width() - nNew ); } + SwTwips ExtraBlankWidth() const { return m_nExtraBlankWidth; } + void ExtraBlankWidth(const SwTwips nNew) { m_nExtraBlankWidth = nNew; } + SwTwips GetHangingBaseline() const { return mnHangingBaseline; } + void SetHangingBaseline( const SwTwips nNewBaseline ) { mnHangingBaseline = nNewBaseline; } + + // Insert methods + virtual SwLinePortion *Insert( SwLinePortion *pPortion ); + virtual SwLinePortion *Append( SwLinePortion *pPortion ); + SwLinePortion *Cut( SwLinePortion *pVictim ); + inline void Truncate(); + + // Returns 0, if there's no payload + virtual SwLinePortion *Compress(); + + void SetWhichPor( const PortionType nNew ) { mnWhichPor = nNew; } + PortionType GetWhichPor( ) const { return mnWhichPor; } + +// Group queries + bool InTextGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_TXT) != 0; } + bool InGlueGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_GLUE) != 0; } + bool InTabGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_TAB) != 0; } + bool InHyphGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_HYPH) != 0; } + bool InNumberGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_NUMBER) != 0; } + bool InFixGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_FIX) != 0; } + bool InFieldGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_FLD) != 0; } + bool InToxRefGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_TOXREF) != 0; } + bool InToxRefOrFieldGrp() const { return (sal_uInt16(mnWhichPor) & ( PORGRP_FLD | PORGRP_TOXREF )) != 0; } + bool InExpGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_EXP) != 0; } + bool InFixMargGrp() const { return (sal_uInt16(mnWhichPor) & PORGRP_FIXMARG) != 0; } + bool InSpaceGrp() const { return InTextGrp() || IsMultiPortion(); } +// Individual queries + bool IsGrfNumPortion() const { return mnWhichPor == PortionType::GrfNum; } + bool IsFlyCntPortion() const { return mnWhichPor == PortionType::FlyCnt; } + bool IsBlankPortion() const { return mnWhichPor == PortionType::Blank; } + bool IsBreakPortion() const { return mnWhichPor == PortionType::Break; } + bool IsErgoSumPortion() const { return mnWhichPor == PortionType::ErgoSum; } + bool IsQuoVadisPortion() const { return mnWhichPor == PortionType::QuoVadis; } + bool IsTabLeftPortion() const { return mnWhichPor == PortionType::TabLeft; } + bool IsTabRightPortion() const { return mnWhichPor == PortionType::TabRight; } + bool IsTabCenterPortion() const { return mnWhichPor == PortionType::TabCenter; } + bool IsTabDecimalPortion() const { return mnWhichPor == PortionType::TabDecimal; } + bool IsFootnoteNumPortion() const { return mnWhichPor == PortionType::FootnoteNum; } + bool IsFootnotePortion() const { return mnWhichPor == PortionType::Footnote; } + bool IsDropPortion() const { return mnWhichPor == PortionType::Drop; } + bool IsLayPortion() const { return mnWhichPor == PortionType::Lay; } + bool IsParaPortion() const { return mnWhichPor == PortionType::Para; } + bool IsMarginPortion() const { return mnWhichPor == PortionType::Margin; } + bool IsFlyPortion() const { return mnWhichPor == PortionType::Fly; } + bool IsHolePortion() const { return mnWhichPor == PortionType::Hole; } + bool IsSoftHyphPortion() const { return mnWhichPor == PortionType::SoftHyphen; } + bool IsPostItsPortion() const { return mnWhichPor == PortionType::PostIts; } + bool IsCombinedPortion() const { return mnWhichPor == PortionType::Combined; } + bool IsTextPortion() const { return mnWhichPor == PortionType::Text; } + bool IsHangingPortion() const { return mnWhichPor == PortionType::Hanging; } + bool IsKernPortion() const { return mnWhichPor == PortionType::Kern; } + bool IsArrowPortion() const { return mnWhichPor == PortionType::Arrow; } + bool IsMultiPortion() const { return mnWhichPor == PortionType::Multi; } + bool IsNumberPortion() const { return mnWhichPor == PortionType::Number; } // #i23726# + bool IsControlCharPortion() const { return mnWhichPor == PortionType::ControlChar || mnWhichPor == PortionType::Bookmark; } + + // Positioning + SwLinePortion *FindPrevPortion( const SwLinePortion *pRoot ); + SwLinePortion *FindLastPortion(); + + /// the parameter is actually SwTwips apparently? + virtual TextFrameIndex GetModelPositionForViewPoint(sal_uInt16 nOfst) const; + virtual SwPosSize GetTextSize( const SwTextSizeInfo &rInfo ) const; + void CalcTextSize( const SwTextSizeInfo &rInfo ); + + // Output + virtual void Paint( const SwTextPaintInfo &rInf ) const = 0; + void PrePaint( const SwTextPaintInfo &rInf, const SwLinePortion *pLast ) const; + + virtual bool Format( SwTextFormatInfo &rInf ); + // Is called for the line's last portion + virtual void FormatEOL( SwTextFormatInfo &rInf ); + void Move(SwTextPaintInfo & rInf) const; + + // For SwTextSlot + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const; + + // For SwFieldPortion, SwSoftHyphPortion + virtual sal_uInt16 GetViewWidth( const SwTextSizeInfo &rInf ) const; + + // for text- and multi-portions + virtual SwTwips CalcSpacing( tools::Long nSpaceAdd, const SwTextSizeInfo &rInf ) const; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const; + + bool GetJoinBorderWithPrev() const { return m_bJoinBorderWithPrev; } + bool GetJoinBorderWithNext() const { return m_bJoinBorderWithNext; } + void SetJoinBorderWithPrev( const bool bJoinPrev ) { m_bJoinBorderWithPrev = bJoinPrev; } + void SetJoinBorderWithNext( const bool bJoinNext ) { m_bJoinBorderWithNext = bJoinNext; } + + virtual void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& rOffset) const; + void dumpAsXmlAttributes(xmlTextWriterPtr writer, std::u16string_view rText, + TextFrameIndex nOffset) const; +}; + +inline SwLinePortion &SwLinePortion::operator=(const SwLinePortion &rPortion) +{ + *static_cast<SwPosSize*>(this) = rPortion; + mnLineLength = rPortion.mnLineLength; + mnAscent = rPortion.mnAscent; + mnHangingBaseline = rPortion.mnHangingBaseline; + mnWhichPor = rPortion.mnWhichPor; + m_bJoinBorderWithPrev = rPortion.m_bJoinBorderWithPrev; + m_bJoinBorderWithNext = rPortion.m_bJoinBorderWithNext; + m_nExtraBlankWidth = rPortion.m_nExtraBlankWidth; + return *this; +} + +inline SwLinePortion::SwLinePortion(const SwLinePortion &rPortion) : + SwPosSize( rPortion ), + mpNextPortion( nullptr ), + mnLineLength( rPortion.mnLineLength ), + mnAscent( rPortion.mnAscent ), + mnHangingBaseline( rPortion.mnHangingBaseline ), + mnWhichPor( rPortion.mnWhichPor ), + m_bJoinBorderWithPrev( rPortion.m_bJoinBorderWithPrev ), + m_bJoinBorderWithNext( rPortion.m_bJoinBorderWithNext ), + m_nExtraBlankWidth(rPortion.m_nExtraBlankWidth) +{ +} + +inline void SwLinePortion::Truncate() +{ + if ( mpNextPortion ) + Truncate_(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/pormulti.cxx b/sw/source/core/text/pormulti.cxx new file mode 100644 index 0000000000..7771ab9b5e --- /dev/null +++ b/sw/source/core/text/pormulti.cxx @@ -0,0 +1,2635 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <deque> +#include <memory> + +#include <hintids.hxx> + +#include <editeng/twolinesitem.hxx> +#include <editeng/charrotateitem.hxx> +#include <vcl/outdev.hxx> +#include <txatbase.hxx> +#include <fmtruby.hxx> +#include <txtatr.hxx> +#include <charfmt.hxx> +#include <layfrm.hxx> +#include <SwPortionHandler.hxx> +#include <EnhancedPDFExportHelper.hxx> +#include "pormulti.hxx" +#include "inftxt.hxx" +#include "itrpaint.hxx" +#include <viewopt.hxx> +#include "itrform2.hxx" +#include "porfld.hxx" +#include "porglue.hxx" +#include "porrst.hxx" +#include <pagefrm.hxx> +#include <rowfrm.hxx> +#include <tgrditem.hxx> +#include <swtable.hxx> +#include <fmtfsize.hxx> +#include <doc.hxx> + +using namespace ::com::sun::star; + +// A SwMultiPortion is not a simple portion, +// it's a container, which contains almost a SwLineLayoutPortion. +// This SwLineLayout could be followed by other textportions via pPortion +// and by another SwLineLayout via pNext to realize a doubleline portion. +SwMultiPortion::~SwMultiPortion() +{ +} + +void SwMultiPortion::Paint( const SwTextPaintInfo & ) const +{ + OSL_FAIL( "Don't try SwMultiPortion::Paint, try SwTextPainter::PaintMultiPortion" ); +} + +// Summarize the internal lines to calculate the (external) size. +// The internal line has to calculate first. +void SwMultiPortion::CalcSize( SwTextFormatter& rLine, SwTextFormatInfo &rInf ) +{ + Width( 0 ); + Height( 0 ); + SetAscent( 0 ); + SetFlyInContent( false ); + SwLineLayout *pLay = &GetRoot(); + do + { + pLay->CalcLine( rLine, rInf ); + if( rLine.IsFlyInCntBase() ) + SetFlyInContent( true ); + if( IsRuby() && ( OnTop() == ( pLay == &GetRoot() ) ) ) + { + // An empty phonetic line don't need an ascent or a height. + if( !pLay->Width() ) + { + pLay->SetAscent( 0 ); + pLay->Height( 0 ); + } + if( OnTop() ) + SetAscent( GetAscent() + pLay->Height() ); + } + else + SetAscent( GetAscent() + pLay->GetAscent() ); + + // Increase the line height, except for ruby text on the right. + if ( !IsRuby() || !OnRight() || pLay == &GetRoot() ) + Height( Height() + pLay->Height() ); + else + { + // We already added the width after building the portion, + // so no need to add it twice. + break; + } + + if( Width() < pLay->Width() ) + Width( pLay->Width() ); + pLay = pLay->GetNext(); + } while ( pLay ); + if( !HasBrackets() ) + return; + + sal_uInt16 nTmp = static_cast<SwDoubleLinePortion*>(this)->GetBrackets()->nHeight; + if( nTmp > Height() ) + { + const sal_uInt16 nAdd = ( nTmp - Height() ) / 2; + GetRoot().SetAscent( GetRoot().GetAscent() + nAdd ); + GetRoot().Height( GetRoot().Height() + nAdd ); + Height( nTmp ); + } + nTmp = static_cast<SwDoubleLinePortion*>(this)->GetBrackets()->nAscent; + if( nTmp > GetAscent() ) + SetAscent( nTmp ); +} + +SwTwips SwMultiPortion::CalcSpacing( tools::Long , const SwTextSizeInfo & ) const +{ + return 0; +} + +bool SwMultiPortion::ChgSpaceAdd( SwLineLayout*, tools::Long ) const +{ + return false; +} + +void SwMultiPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Text( GetLen(), GetWhichPor() ); +} + +void SwMultiPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwMultiPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + // Intentionally not incrementing nOffset here, one of the child portions will do that. + + const SwLineLayout* pLine = &GetRoot(); + while (pLine) + { + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwLineLayout")); + pLine->dumpAsXmlAttributes(pWriter, rText, nOffset); + const SwLinePortion* pPor = pLine->GetFirstPortion(); + while (pPor) + { + pPor->dumpAsXml(pWriter, rText, nOffset); + pPor = pPor->GetNextPortion(); + } + (void)xmlTextWriterEndElement(pWriter); + pLine = pLine->GetNext(); + } + + (void)xmlTextWriterEndElement(pWriter); +} + +// sets the tabulator-flag, if there's any tabulator-portion inside. +void SwMultiPortion::ActualizeTabulator() +{ + SwLinePortion* pPor = GetRoot().GetFirstPortion(); + // First line + for( m_bTab1 = m_bTab2 = false; pPor; pPor = pPor->GetNextPortion() ) + if( pPor->InTabGrp() ) + SetTab1( true ); + if( GetRoot().GetNext() ) + { + // Second line + pPor = GetRoot().GetNext()->GetFirstPortion(); + do + { + if( pPor->InTabGrp() ) + SetTab2( true ); + pPor = pPor->GetNextPortion(); + } while ( pPor ); + } +} + +SwRotatedPortion::SwRotatedPortion( const SwMultiCreator& rCreate, + TextFrameIndex const nEnd, bool bRTL ) + : SwMultiPortion( nEnd ) +{ + const SvxCharRotateItem* pRot = static_cast<const SvxCharRotateItem*>(rCreate.pItem); + if( !pRot ) + { + const SwTextAttr& rAttr = *rCreate.pAttr; + const SfxPoolItem *const pItem = + CharFormat::GetItem(rAttr, RES_CHRATR_ROTATE); + if ( pItem ) + { + pRot = static_cast<const SvxCharRotateItem*>(pItem); + } + } + if( pRot ) + { + sal_uInt8 nDir; + if ( bRTL ) + nDir = pRot->IsBottomToTop() ? 3 : 1; + else + nDir = pRot->IsBottomToTop() ? 1 : 3; + + SetDirection( nDir ); + } +} + +SwBidiPortion::SwBidiPortion(TextFrameIndex const nEnd, sal_uInt8 nLv) + : SwMultiPortion( nEnd ), m_nLevel( nLv ) +{ + SetBidi(); + + if ( m_nLevel % 2 ) + SetDirection( DIR_RIGHT2LEFT ); + else + SetDirection( DIR_LEFT2RIGHT ); +} + +SwTwips SwBidiPortion::CalcSpacing( tools::Long nSpaceAdd, const SwTextSizeInfo& rInf ) const +{ + return HasTabulator() ? 0 : sal_Int32(GetSpaceCnt(rInf)) * nSpaceAdd / SPACING_PRECISION_FACTOR; +} + +bool SwBidiPortion::ChgSpaceAdd( SwLineLayout* pCurr, tools::Long nSpaceAdd ) const +{ + if( !HasTabulator() && nSpaceAdd > 0 && !pCurr->IsSpaceAdd() ) + { + pCurr->CreateSpaceAdd(); + pCurr->SetLLSpaceAdd( nSpaceAdd, 0 ); + return true; + } + + return false; +} + +TextFrameIndex SwBidiPortion::GetSpaceCnt(const SwTextSizeInfo &rInf) const +{ + // Calculate number of blanks for justified alignment + TextFrameIndex nTmpStart = rInf.GetIdx(); + TextFrameIndex nNull(0); + TextFrameIndex nBlanks(0); + + for (SwLinePortion* pPor = GetRoot().GetFirstPortion(); pPor; pPor = pPor->GetNextPortion()) + { + if( pPor->InTextGrp() ) + nBlanks = nBlanks + static_cast<SwTextPortion*>(pPor)->GetSpaceCnt( rInf, nNull ); + else if ( pPor->IsMultiPortion() && + static_cast<SwMultiPortion*>(pPor)->IsBidi() ) + nBlanks = nBlanks + static_cast<SwBidiPortion*>(pPor)->GetSpaceCnt( rInf ); + + const_cast<SwTextSizeInfo &>(rInf).SetIdx( rInf.GetIdx() + pPor->GetLen() ); + } + const_cast<SwTextSizeInfo &>(rInf).SetIdx( nTmpStart ); + return nBlanks; +} + +// This constructor is for the continuation of a doubleline portion +// in the next line. +// It takes the same brackets and if the original has no content except +// brackets, these will be deleted. +SwDoubleLinePortion::SwDoubleLinePortion( + SwDoubleLinePortion& rDouble, TextFrameIndex const nEnd) + : SwMultiPortion(nEnd) + , m_nLineDiff(0) + , m_nBlank1(0) + , m_nBlank2(0) +{ + SetDirection( rDouble.GetDirection() ); + SetDouble(); + if( rDouble.GetBrackets() ) + { + SetBrackets( rDouble ); + // An empty multiportion needs no brackets. + // Notice: GetLen() might be zero, if the multiportion contains + // the second part of a field and the width might be zero, if + // it contains a note only. In this cases the brackets are okay. + // But if the length and the width are both zero, the portion + // is really empty. + if( rDouble.Width() == rDouble.BracketWidth() ) + rDouble.ClearBrackets(); + } +} + +// This constructor uses the textattribute to get the right brackets. +// The textattribute could be a 2-line-attribute or a character- or +// internet style, which contains the 2-line-attribute. +SwDoubleLinePortion::SwDoubleLinePortion( + const SwMultiCreator& rCreate, TextFrameIndex const nEnd) + : SwMultiPortion(nEnd) + , m_pBracket(new SwBracket) + , m_nLineDiff(0) + , m_nBlank1(0) + , m_nBlank2(0) +{ + m_pBracket->nAscent = 0; + m_pBracket->nHeight = 0; + m_pBracket->nPreWidth = 0; + m_pBracket->nPostWidth = 0; + + SetDouble(); + const SvxTwoLinesItem* pTwo = static_cast<const SvxTwoLinesItem*>(rCreate.pItem); + if( pTwo ) + m_pBracket->nStart = TextFrameIndex(0); + else + { + const SwTextAttr& rAttr = *rCreate.pAttr; + m_pBracket->nStart = rCreate.nStartOfAttr; + + const SfxPoolItem * const pItem = + CharFormat::GetItem( rAttr, RES_CHRATR_TWO_LINES ); + if ( pItem ) + { + pTwo = static_cast<const SvxTwoLinesItem*>(pItem); + } + } + if( pTwo ) + { + m_pBracket->cPre = pTwo->GetStartBracket(); + m_pBracket->cPost = pTwo->GetEndBracket(); + } + else + { + m_pBracket->cPre = 0; + m_pBracket->cPost = 0; + } + SwFontScript nTmp = SW_SCRIPTS; + if( m_pBracket->cPre > 255 ) + { + OUString aText(m_pBracket->cPre); + nTmp = SwScriptInfo::WhichFont(0, aText); + } + m_pBracket->nPreScript = nTmp; + nTmp = SW_SCRIPTS; + if( m_pBracket->cPost > 255 ) + { + OUString aText(m_pBracket->cPost); + nTmp = SwScriptInfo::WhichFont(0, aText); + } + m_pBracket->nPostScript = nTmp; + + if( !m_pBracket->cPre && !m_pBracket->cPost ) + { + m_pBracket.reset(); + } + + // double line portions have the same direction as the frame directions + if ( rCreate.nLevel % 2 ) + SetDirection( DIR_RIGHT2LEFT ); + else + SetDirection( DIR_LEFT2RIGHT ); +} + +// paints the wished bracket, +// if the multiportion has surrounding brackets. +// The X-position of the SwTextPaintInfo will be modified: +// the open bracket sets position behind itself, +// the close bracket in front of itself. +void SwDoubleLinePortion::PaintBracket( SwTextPaintInfo &rInf, + tools::Long nSpaceAdd, + bool bOpen ) const +{ + sal_Unicode cCh = bOpen ? m_pBracket->cPre : m_pBracket->cPost; + if( !cCh ) + return; + const sal_uInt16 nChWidth = bOpen ? PreWidth() : PostWidth(); + if( !nChWidth ) + return; + if( !bOpen ) + rInf.X( rInf.X() + Width() - PostWidth() + + ( nSpaceAdd > 0 ? CalcSpacing( nSpaceAdd, rInf ) : 0 ) ); + + SwBlankPortion aBlank( cCh, true ); + aBlank.SetAscent( m_pBracket->nAscent ); + aBlank.Width( nChWidth ); + aBlank.Height( m_pBracket->nHeight ); + { + SwFont aTmpFnt( *rInf.GetFont() ); + SwFontScript nAct = bOpen ? m_pBracket->nPreScript : m_pBracket->nPostScript; + if( SW_SCRIPTS > nAct ) + aTmpFnt.SetActual( nAct ); + aTmpFnt.SetProportion( 100 ); + SwFontSave aSave( rInf, &aTmpFnt ); + aBlank.Paint( rInf ); + } + if( bOpen ) + rInf.X( rInf.X() + PreWidth() ); +} + +// creates the bracket-structure +// and fills it, if not both characters are 0x00. +void SwDoubleLinePortion::SetBrackets( const SwDoubleLinePortion& rDouble ) +{ + if( rDouble.m_pBracket ) + { + m_pBracket.reset( new SwBracket ); + m_pBracket->cPre = rDouble.m_pBracket->cPre; + m_pBracket->cPost = rDouble.m_pBracket->cPost; + m_pBracket->nPreScript = rDouble.m_pBracket->nPreScript; + m_pBracket->nPostScript = rDouble.m_pBracket->nPostScript; + m_pBracket->nStart = rDouble.m_pBracket->nStart; + } +} + +// calculates the size of the brackets => pBracket, +// reduces the nMaxWidth-parameter ( minus bracket-width ) +// and moves the rInf-x-position behind the opening bracket. +void SwDoubleLinePortion::FormatBrackets( SwTextFormatInfo &rInf, SwTwips& nMaxWidth ) +{ + nMaxWidth -= rInf.X(); + SwFont aTmpFnt( *rInf.GetFont() ); + aTmpFnt.SetProportion( 100 ); + m_pBracket->nAscent = 0; + m_pBracket->nHeight = 0; + if( m_pBracket->cPre ) + { + OUString aStr( m_pBracket->cPre ); + SwFontScript nActualScr = aTmpFnt.GetActual(); + if( SW_SCRIPTS > m_pBracket->nPreScript ) + aTmpFnt.SetActual( m_pBracket->nPreScript ); + SwFontSave aSave( rInf, &aTmpFnt ); + SwPosSize aSize = rInf.GetTextSize( aStr ); + m_pBracket->nAscent = rInf.GetAscent(); + m_pBracket->nHeight = aSize.Height(); + aTmpFnt.SetActual( nActualScr ); + if( nMaxWidth > aSize.Width() ) + { + m_pBracket->nPreWidth = aSize.Width(); + nMaxWidth -= aSize.Width(); + rInf.X( rInf.X() + aSize.Width() ); + } + else + { + m_pBracket->nPreWidth = 0; + nMaxWidth = 0; + } + } + else + m_pBracket->nPreWidth = 0; + if( m_pBracket->cPost ) + { + OUString aStr( m_pBracket->cPost ); + if( SW_SCRIPTS > m_pBracket->nPostScript ) + aTmpFnt.SetActual( m_pBracket->nPostScript ); + SwFontSave aSave( rInf, &aTmpFnt ); + SwPosSize aSize = rInf.GetTextSize( aStr ); + const sal_uInt16 nTmpAsc = rInf.GetAscent(); + if( nTmpAsc > m_pBracket->nAscent ) + { + m_pBracket->nHeight += nTmpAsc - m_pBracket->nAscent; + m_pBracket->nAscent = nTmpAsc; + } + if( aSize.Height() > m_pBracket->nHeight ) + m_pBracket->nHeight = aSize.Height(); + if( nMaxWidth > aSize.Width() ) + { + m_pBracket->nPostWidth = aSize.Width(); + nMaxWidth -= aSize.Width(); + } + else + { + m_pBracket->nPostWidth = 0; + nMaxWidth = 0; + } + } + else + m_pBracket->nPostWidth = 0; + nMaxWidth += rInf.X(); +} + +// calculates the number of blanks in each line and +// the difference of the width of the two lines. +// These results are used from the text adjustment. +void SwDoubleLinePortion::CalcBlanks( SwTextFormatInfo &rInf ) +{ + SwLinePortion* pPor = GetRoot().GetFirstPortion(); + TextFrameIndex nNull(0); + TextFrameIndex nStart = rInf.GetIdx(); + SetTab1( false ); + SetTab2( false ); + for (m_nBlank1 = TextFrameIndex(0); pPor; pPor = pPor->GetNextPortion()) + { + if( pPor->InTextGrp() ) + m_nBlank1 = m_nBlank1 + static_cast<SwTextPortion*>(pPor)->GetSpaceCnt( rInf, nNull ); + rInf.SetIdx( rInf.GetIdx() + pPor->GetLen() ); + if( pPor->InTabGrp() ) + SetTab1( true ); + } + m_nLineDiff = GetRoot().Width(); + if( GetRoot().GetNext() ) + { + pPor = GetRoot().GetNext()->GetFirstPortion(); + m_nLineDiff -= GetRoot().GetNext()->Width(); + } + for (m_nBlank2 = TextFrameIndex(0); pPor; pPor = pPor->GetNextPortion()) + { + if( pPor->InTextGrp() ) + m_nBlank2 = m_nBlank2 + static_cast<SwTextPortion*>(pPor)->GetSpaceCnt( rInf, nNull ); + rInf.SetIdx( rInf.GetIdx() + pPor->GetLen() ); + if( pPor->InTabGrp() ) + SetTab2( true ); + } + rInf.SetIdx( nStart ); +} + +SwTwips SwDoubleLinePortion::CalcSpacing( tools::Long nSpaceAdd, const SwTextSizeInfo & ) const +{ + return HasTabulator() ? 0 : sal_Int32(GetSpaceCnt()) * nSpaceAdd / SPACING_PRECISION_FACTOR; +} + +// Merges the spaces for text adjustment from the inner and outer part. +// Inside the doubleline portion the wider line has no spaceadd-array, the +// smaller line has such an array to reach width of the wider line. +// If the surrounding line has text adjustment and the doubleline portion +// contains no tabulator, it is necessary to create/manipulate the inner +// space arrays. +bool SwDoubleLinePortion::ChgSpaceAdd( SwLineLayout* pCurr, + tools::Long nSpaceAdd ) const +{ + bool bRet = false; + if( !HasTabulator() && nSpaceAdd > 0 ) + { + if( !pCurr->IsSpaceAdd() ) + { + // The wider line gets the spaceadd from the surrounding line direct + pCurr->CreateSpaceAdd(); + pCurr->SetLLSpaceAdd( nSpaceAdd, 0 ); + bRet = true; + } + else + { + sal_Int32 const nMyBlank = sal_Int32(GetSmallerSpaceCnt()); + sal_Int32 const nOther = sal_Int32(GetSpaceCnt()); + SwTwips nMultiSpace = pCurr->GetLLSpaceAdd( 0 ) * nMyBlank + nOther * nSpaceAdd; + + if( nMyBlank ) + nMultiSpace /= sal_Int32(nMyBlank); + +// pCurr->SetLLSpaceAdd( nMultiSpace, 0 ); + // #i65711# SetLLSpaceAdd replaces the first value, + // instead we want to insert a new first value: + std::vector<tools::Long>* pVec = pCurr->GetpLLSpaceAdd(); + pVec->insert( pVec->begin(), nMultiSpace ); + bRet = true; + } + } + return bRet; +} +// cancels the manipulation from SwDoubleLinePortion::ChangeSpaceAdd(..) +void SwDoubleLinePortion::ResetSpaceAdd( SwLineLayout* pCurr ) +{ + pCurr->RemoveFirstLLSpaceAdd(); + if( !pCurr->GetLLSpaceAddCount() ) + pCurr->FinishSpaceAdd(); +} + +SwDoubleLinePortion::~SwDoubleLinePortion() +{ +} + +// constructs a ruby portion, i.e. an additional text is displayed +// beside the main text, e.g. phonetic characters. +SwRubyPortion::SwRubyPortion(const SwRubyPortion& rRuby, TextFrameIndex const nEnd) + : SwMultiPortion( nEnd ) + , m_nRubyOffset( rRuby.GetRubyOffset() ) + , m_nAdjustment( rRuby.GetAdjustment() ) +{ + SetDirection( rRuby.GetDirection() ); + SetRubyPosition( rRuby.GetRubyPosition() ); + SetRuby(); +} + +// constructs a ruby portion, i.e. an additional text is displayed +// beside the main text, e.g. phonetic characters. +SwRubyPortion::SwRubyPortion( const SwMultiCreator& rCreate, const SwFont& rFnt, + const IDocumentSettingAccess& rIDocumentSettingAccess, + TextFrameIndex const nEnd, TextFrameIndex const nOffs, + const SwTextSizeInfo &rInf ) + : SwMultiPortion( nEnd ) +{ + SetRuby(); + OSL_ENSURE( SwMultiCreatorId::Ruby == rCreate.nId, "Ruby expected" ); + OSL_ENSURE( RES_TXTATR_CJK_RUBY == rCreate.pAttr->Which(), "Wrong attribute" ); + const SwFormatRuby& rRuby = rCreate.pAttr->GetRuby(); + m_nAdjustment = rRuby.GetAdjustment(); + m_nRubyOffset = nOffs; + + const SwTextFrame *pFrame = rInf.GetTextFrame(); + RubyPosition ePos = static_cast<RubyPosition>( rRuby.GetPosition() ); + + // RIGHT is designed for horizontal writing mode only. + if ( ePos == RubyPosition::RIGHT && pFrame->IsVertical() ) + ePos = RubyPosition::ABOVE; + + // In grid mode we force the ruby text to the upper or lower line + if ( rInf.SnapToGrid() ) + { + SwTextGridItem const*const pGrid( GetGridItem(pFrame->FindPageFrame()) ); + if ( pGrid ) + ePos = pGrid->GetRubyTextBelow() ? RubyPosition::BELOW : RubyPosition::ABOVE; + } + + SetRubyPosition( ePos ); + + const SwCharFormat *const pFormat = + static_txtattr_cast<SwTextRuby const*>(rCreate.pAttr)->GetCharFormat(); + std::unique_ptr<SwFont> pRubyFont; + if( pFormat ) + { + const SwAttrSet& rSet = pFormat->GetAttrSet(); + pRubyFont.reset(new SwFont( rFnt )); + pRubyFont->SetDiffFnt( &rSet, &rIDocumentSettingAccess ); + + // we do not allow a vertical font for the ruby text + pRubyFont->SetVertical( rFnt.GetOrientation() , OnRight() ); + } + + OUString aStr = rRuby.GetText().copy( sal_Int32(nOffs) ); + SwFieldPortion *pField = new SwFieldPortion( aStr, std::move(pRubyFont) ); + pField->SetNextOffset( nOffs ); + pField->SetFollow( true ); + + if( OnTop() ) + GetRoot().SetNextPortion( pField ); + else + { + GetRoot().SetNext( new SwLineLayout() ); + GetRoot().GetNext()->SetNextPortion( pField ); + } + + // ruby portions have the same direction as the frame directions + if ( rCreate.nLevel % 2 ) + { + // switch right and left ruby adjustment in rtl environment + if ( css::text::RubyAdjust_LEFT == m_nAdjustment ) + m_nAdjustment = css::text::RubyAdjust_RIGHT; + else if ( css::text::RubyAdjust_RIGHT == m_nAdjustment ) + m_nAdjustment = css::text::RubyAdjust_LEFT; + + SetDirection( DIR_RIGHT2LEFT ); + } + else + SetDirection( DIR_LEFT2RIGHT ); +} + +// In ruby portion there are different alignments for +// the ruby text and the main text. +// Left, right, centered and two possibilities of block adjustment +// The block adjustment is realized by spacing between the characters, +// either with a half space or no space in front of the first letter and +// a half space at the end of the last letter. +// Notice: the smaller line will be manipulated, normally it's the ruby line, +// but it could be the main text, too. +// If there is a tabulator in smaller line, no adjustment is possible. +void SwRubyPortion::Adjust_( SwTextFormatInfo &rInf ) +{ + SwTwips nLineDiff = GetRoot().Width() - GetRoot().GetNext()->Width(); + TextFrameIndex const nOldIdx = rInf.GetIdx(); + if( !nLineDiff ) + return; + SwLineLayout *pCurr; + if( nLineDiff < 0 ) + { // The first line has to be adjusted. + if( GetTab1() ) + return; + pCurr = &GetRoot(); + nLineDiff = -nLineDiff; + } + else + { // The second line has to be adjusted. + if( GetTab2() ) + return; + pCurr = GetRoot().GetNext(); + rInf.SetIdx( nOldIdx + GetRoot().GetLen() ); + } + sal_uInt16 nLeft = 0; // the space in front of the first letter + sal_uInt16 nRight = 0; // the space at the end of the last letter + TextFrameIndex nSub(0); + switch ( m_nAdjustment ) + { + case css::text::RubyAdjust_CENTER: nRight = o3tl::narrowing<sal_uInt16>(nLineDiff / 2); + [[fallthrough]]; + case css::text::RubyAdjust_RIGHT: nLeft = o3tl::narrowing<sal_uInt16>(nLineDiff - nRight); break; + case css::text::RubyAdjust_BLOCK: nSub = TextFrameIndex(1); + [[fallthrough]]; + case css::text::RubyAdjust_INDENT_BLOCK: + { + TextFrameIndex nCharCnt(0); + SwLinePortion *pPor; + for( pPor = pCurr->GetFirstPortion(); pPor; pPor = pPor->GetNextPortion() ) + { + if( pPor->InTextGrp() ) + static_cast<SwTextPortion*>(pPor)->GetSpaceCnt( rInf, nCharCnt ); + rInf.SetIdx( rInf.GetIdx() + pPor->GetLen() ); + } + if( nCharCnt > nSub ) + { + SwTwips nCalc = nLineDiff / sal_Int32(nCharCnt - nSub); + short nTmp; + if( nCalc < SHRT_MAX ) + nTmp = -short(nCalc); + else + nTmp = SHRT_MIN; + + pCurr->CreateSpaceAdd( SPACING_PRECISION_FACTOR * nTmp ); + nLineDiff -= nCalc * (sal_Int32(nCharCnt) - 1); + } + if( nLineDiff > 1 ) + { + nRight = o3tl::narrowing<sal_uInt16>(nLineDiff / 2); + nLeft = o3tl::narrowing<sal_uInt16>(nLineDiff - nRight); + } + break; + } + default: OSL_FAIL( "New ruby adjustment" ); + } + if( nLeft || nRight ) + { + if( !pCurr->GetNextPortion() ) + pCurr->SetNextPortion(SwTextPortion::CopyLinePortion(*pCurr)); + if( nLeft ) + { + SwMarginPortion *pMarg = new SwMarginPortion; + pMarg->AddPrtWidth( nLeft ); + pMarg->SetNextPortion( pCurr->GetNextPortion() ); + pCurr->SetNextPortion( pMarg ); + } + if( nRight ) + { + SwMarginPortion *pMarg = new SwMarginPortion; + pMarg->AddPrtWidth( nRight ); + pCurr->FindLastPortion()->Append( pMarg ); + } + } + + pCurr->Width( Width() ); + rInf.SetIdx( nOldIdx ); +} + +// has to change the nRubyOffset, if there's a fieldportion +// in the phonetic line. +// The nRubyOffset is the position in the rubystring, where the +// next SwRubyPortion has start the displaying of the phonetics. +void SwRubyPortion::CalcRubyOffset() +{ + const SwLineLayout *pCurr = &GetRoot(); + if( !OnTop() ) + { + pCurr = pCurr->GetNext(); + if( !pCurr ) + return; + } + const SwLinePortion *pPor = pCurr->GetFirstPortion(); + const SwFieldPortion *pField = nullptr; + while( pPor ) + { + if( pPor->InFieldGrp() ) + pField = static_cast<const SwFieldPortion*>(pPor); + pPor = pPor->GetNextPortion(); + } + if( pField ) + { + if( pField->HasFollow() ) + m_nRubyOffset = pField->GetNextOffset(); + else + m_nRubyOffset = TextFrameIndex(COMPLETE_STRING); + } +} + +// A little helper function for GetMultiCreator(..) +// It extracts the 2-line-format from a 2-line-attribute or a character style. +// The rValue is set to true, if the 2-line-attribute's value is set and +// no 2-line-format reference is passed. If there is a 2-line-format reference, +// then the rValue is set only, if the 2-line-attribute's value is set _and_ +// the 2-line-formats has the same brackets. +static bool lcl_Check2Lines(const SfxPoolItem *const pItem, + const SvxTwoLinesItem* &rpRef, bool &rValue) +{ + if( pItem ) + { + rValue = static_cast<const SvxTwoLinesItem*>(pItem)->GetValue(); + if( !rpRef ) + rpRef = static_cast<const SvxTwoLinesItem*>(pItem); + else if( static_cast<const SvxTwoLinesItem*>(pItem)->GetEndBracket() != + rpRef->GetEndBracket() || + static_cast<const SvxTwoLinesItem*>(pItem)->GetStartBracket() != + rpRef->GetStartBracket() ) + rValue = false; + return true; + } + return false; +} + +static bool lcl_Has2Lines(const SwTextAttr& rAttr, + const SvxTwoLinesItem* &rpRef, bool &rValue) +{ + const SfxPoolItem* pItem = CharFormat::GetItem(rAttr, RES_CHRATR_TWO_LINES); + return lcl_Check2Lines(pItem, rpRef, rValue); +} + +// is a little help function for GetMultiCreator(..) +// It extracts the charrotation from a charrotate-attribute or a character style. +// The rValue is set to true, if the charrotate-attribute's value is set and +// no charrotate-format reference is passed. +// If there is a charrotate-format reference, then the rValue is set only, +// if the charrotate-attribute's value is set _and_ identical +// to the charrotate-format's value. +static bool lcl_CheckRotation(const SfxPoolItem *const pItem, + const SvxCharRotateItem* &rpRef, bool &rValue) +{ + if ( pItem ) + { + rValue = static_cast<const SvxCharRotateItem*>(pItem)->GetValue() != 0_deg10; + if( !rpRef ) + rpRef = static_cast<const SvxCharRotateItem*>(pItem); + else if( static_cast<const SvxCharRotateItem*>(pItem)->GetValue() != + rpRef->GetValue() ) + rValue = false; + return true; + } + + return false; +} + +static bool lcl_HasRotation(const SwTextAttr& rAttr, + const SvxCharRotateItem* &rpRef, bool &rValue) +{ + const SfxPoolItem* pItem = CharFormat::GetItem( rAttr, RES_CHRATR_ROTATE ); + return lcl_CheckRotation(pItem, rpRef, rValue); +} + +namespace sw { + namespace { + + // need to use a very special attribute iterator here that returns + // both the hints and the nodes, so that GetMultiCreator() can handle + // items in the nodes' set properly + class MergedAttrIterMulti + : public MergedAttrIterBase + { + private: + bool m_First = true; + public: + MergedAttrIterMulti(SwTextFrame const& rFrame) : MergedAttrIterBase(rFrame) {} + SwTextAttr const* NextAttr(SwTextNode const*& rpNode); + // can't have operator= because m_pMerged/m_pNode const + void Assign(MergedAttrIterMulti const& rOther) + { + assert(m_pMerged == rOther.m_pMerged); + assert(m_pNode == rOther.m_pNode); + m_CurrentExtent = rOther.m_CurrentExtent; + m_CurrentHint = rOther.m_CurrentHint; + m_First = rOther.m_First; + } + }; + + } + + SwTextAttr const* MergedAttrIterMulti::NextAttr(SwTextNode const*& rpNode) + { + if (m_First) + { + m_First = false; + rpNode = m_pMerged + ? !m_pMerged->extents.empty() + ? m_pMerged->extents[0].pNode + : m_pMerged->pFirstNode + : m_pNode; + return nullptr; + } + if (m_pMerged) + { + const auto nExtentsSize = m_pMerged->extents.size(); + while (m_CurrentExtent < nExtentsSize) + { + sw::Extent const& rExtent(m_pMerged->extents[m_CurrentExtent]); + if (SwpHints const*const pHints = rExtent.pNode->GetpSwpHints()) + { + auto nHintsCount = pHints->Count(); + while (m_CurrentHint < nHintsCount) + { + SwTextAttr const*const pHint(pHints->Get(m_CurrentHint)); + if (rExtent.nEnd < pHint->GetStart()) + { + break; + } + ++m_CurrentHint; + if (rExtent.nStart <= pHint->GetStart()) + { + rpNode = rExtent.pNode; + return pHint; + } + } + } + ++m_CurrentExtent; + if (m_CurrentExtent < nExtentsSize && + rExtent.pNode != m_pMerged->extents[m_CurrentExtent].pNode) + { + m_CurrentHint = 0; // reset + rpNode = m_pMerged->extents[m_CurrentExtent].pNode; + return nullptr; + } + } + return nullptr; + } + else + { + SwpHints const*const pHints(m_pNode->GetpSwpHints()); + if (pHints) + { + if (m_CurrentHint < pHints->Count()) + { + SwTextAttr const*const pHint(pHints->Get(m_CurrentHint)); + ++m_CurrentHint; + rpNode = m_pNode; + return pHint; + } + } + return nullptr; + } + } +} + +// If we (e.g. the position rPos) are inside a two-line-attribute or +// a ruby-attribute, the attribute will be returned in a SwMultiCreator-struct, +// otherwise the function returns zero. +// The rPos parameter is set to the end of the multiportion, +// normally this is the end of the attribute, +// but sometimes it is the start of another attribute, which finished or +// interrupts the first attribute. +// E.g. a ruby portion interrupts a 2-line-attribute, a 2-line-attribute +// with different brackets interrupts another 2-line-attribute. +std::optional<SwMultiCreator> SwTextSizeInfo::GetMultiCreator(TextFrameIndex &rPos, + SwMultiPortion const * pMulti ) const +{ + SwScriptInfo& rSI = const_cast<SwParaPortion*>(GetParaPortion())->GetScriptInfo(); + + // get the last embedding level + sal_uInt8 nCurrLevel; + if ( pMulti ) + { + OSL_ENSURE( pMulti->IsBidi(), "Nested MultiPortion is not BidiPortion" ); + // level associated with bidi-portion; + nCurrLevel = static_cast<SwBidiPortion const *>(pMulti)->GetLevel(); + } + else + // no nested bidi portion required + nCurrLevel = GetTextFrame()->IsRightToLeft() ? 1 : 0; + + // check if there is a field at rPos: + sal_uInt8 nNextLevel = nCurrLevel; + bool bFieldBidi = false; + + if (rPos < TextFrameIndex(GetText().getLength()) && CH_TXTATR_BREAKWORD == GetChar(rPos)) + { + bFieldBidi = true; + } + else + nNextLevel = rSI.DirType( rPos ); + + if (TextFrameIndex(GetText().getLength()) != rPos && nNextLevel > nCurrLevel) + { + rPos = bFieldBidi ? rPos + TextFrameIndex(1) : rSI.NextDirChg(rPos, &nCurrLevel); + if (TextFrameIndex(COMPLETE_STRING) == rPos) + return {}; + SwMultiCreator aRet; + aRet.pItem = nullptr; + aRet.pAttr = nullptr; + aRet.nStartOfAttr = TextFrameIndex(-1); + aRet.nId = SwMultiCreatorId::Bidi; + aRet.nLevel = nCurrLevel + 1; + return aRet; + } + + // a bidi portion can only contain other bidi portions + if ( pMulti ) + return {}; + + // need the node that contains input rPos + std::pair<SwTextNode const*, sal_Int32> startPos(m_pFrame->MapViewToModel(rPos)); + const SvxCharRotateItem* pActiveRotateItem(nullptr); + const SvxCharRotateItem* pNodeRotateItem(nullptr); + const SvxTwoLinesItem* pActiveTwoLinesItem(nullptr); + const SvxTwoLinesItem* pNodeTwoLinesItem(nullptr); + SwTextAttr const* pActiveTwoLinesHint(nullptr); + SwTextAttr const* pActiveRotateHint(nullptr); + const SwTextAttr *pRuby = nullptr; + sw::MergedAttrIterMulti iterAtStartOfNode(*m_pFrame); + bool bTwo = false; + bool bRot = false; + + for (sw::MergedAttrIterMulti iter = *m_pFrame; ; ) + { + SwTextNode const* pNode(nullptr); + SwTextAttr const*const pAttr = iter.NextAttr(pNode); + if (!pNode) + { + break; + } + if (pAttr) + { + assert(pNode->GetIndex() <= startPos.first->GetIndex()); // should break earlier + if (startPos.first->GetIndex() <= pNode->GetIndex()) + { + if (startPos.first->GetIndex() != pNode->GetIndex() + || startPos.second < pAttr->GetStart()) + { + break; + } + if (startPos.second < pAttr->GetAnyEnd()) + { + // sw_redlinehide: ruby *always* splits + if (RES_TXTATR_CJK_RUBY == pAttr->Which()) + pRuby = pAttr; + else + { + const SvxCharRotateItem* pRoTmp = nullptr; + if (lcl_HasRotation( *pAttr, pRoTmp, bRot )) + { + pActiveRotateHint = bRot ? pAttr : nullptr; + pActiveRotateItem = pRoTmp; + } + const SvxTwoLinesItem* p2Tmp = nullptr; + if (lcl_Has2Lines( *pAttr, p2Tmp, bTwo )) + { + pActiveTwoLinesHint = bTwo ? pAttr : nullptr; + pActiveTwoLinesItem = p2Tmp; + } + } + } + } + } + else if (pNode) // !pAttr && pNode means the node changed + { + if (startPos.first->GetIndex() < pNode->GetIndex()) + { + break; // only one node initially + } + if (startPos.first->GetIndex() == pNode->GetIndex()) + { + iterAtStartOfNode.Assign(iter); + if (SfxItemState::SET == pNode->GetSwAttrSet().GetItemState( + RES_CHRATR_ROTATE, true, &pNodeRotateItem) && + pNodeRotateItem->GetValue()) + { + pActiveRotateItem = pNodeRotateItem; + } + else + { + pNodeRotateItem = nullptr; + } + if (SfxItemState::SET == startPos.first->GetSwAttrSet().GetItemState( + RES_CHRATR_TWO_LINES, true, &pNodeTwoLinesItem) && + pNodeTwoLinesItem->GetValue()) + { + pActiveTwoLinesItem = pNodeTwoLinesItem; + } + else + { + pNodeTwoLinesItem = nullptr; + } + } + } + } + if (!pRuby && !pActiveTwoLinesItem && !pActiveRotateItem) + return {}; + + if( pRuby ) + { // The winner is ... a ruby attribute and so + // the end of the multiportion is the end of the ruby attribute. + rPos = m_pFrame->MapModelToView(startPos.first, *pRuby->End()); + SwMultiCreator aRet; + aRet.pItem = nullptr; + aRet.pAttr = pRuby; + aRet.nStartOfAttr = m_pFrame->MapModelToView(startPos.first, aRet.pAttr->GetStart()); + aRet.nId = SwMultiCreatorId::Ruby; + aRet.nLevel = GetTextFrame()->IsRightToLeft() ? 1 : 0; + return aRet; + } + if (pActiveTwoLinesHint || + (pNodeTwoLinesItem && SfxPoolItem::areSame(pNodeTwoLinesItem, pActiveTwoLinesItem) && + rPos < TextFrameIndex(GetText().getLength()))) + { // The winner is a 2-line-attribute, + // the end of the multiportion depends on the following attributes... + SwMultiCreator aRet; + + // We note the endpositions of the 2-line attributes in aEnd as stack + std::deque<TextFrameIndex> aEnd; + + // The bOn flag signs the state of the last 2-line attribute in the + // aEnd-stack, it is compatible with the winner-attribute or + // it interrupts the other attribute. + bool bOn = true; + + if (pActiveTwoLinesHint) + { + aRet.pItem = nullptr; + aRet.pAttr = pActiveTwoLinesHint; + aRet.nStartOfAttr = m_pFrame->MapModelToView(startPos.first, aRet.pAttr->GetStart()); + if (pNodeTwoLinesItem) + { + aEnd.push_front(m_pFrame->MapModelToView(startPos.first, startPos.first->Len())); + bOn = pNodeTwoLinesItem->GetEndBracket() == + pActiveTwoLinesItem->GetEndBracket() && + pNodeTwoLinesItem->GetStartBracket() == + pActiveTwoLinesItem->GetStartBracket(); + } + else + { + aEnd.push_front(m_pFrame->MapModelToView(startPos.first, *aRet.pAttr->End())); + } + } + else + { + aRet.pItem = pNodeTwoLinesItem; + aRet.pAttr = nullptr; + aRet.nStartOfAttr = TextFrameIndex(-1); + aEnd.push_front(m_pFrame->MapModelToView(startPos.first, startPos.first->Len())); + } + aRet.nId = SwMultiCreatorId::Double; + aRet.nLevel = GetTextFrame()->IsRightToLeft() ? 1 : 0; + + // pActiveTwoLinesHint is the last 2-line-attribute, which contains + // the actual position. + + // At this moment we know that at position rPos the "winner"-attribute + // causes a 2-line-portion. The end of the attribute is the end of the + // portion, if there's no interrupting attribute. + // There are two kinds of interrupters: + // - ruby attributes stops the 2-line-attribute, the end of the + // multiline is the start of the ruby attribute + // - 2-line-attributes with value "Off" or with different brackets, + // these attributes may interrupt the winner, but they could be + // neutralized by another 2-line-attribute starting at the same + // position with the same brackets as the winner-attribute. + + // In the following loop rPos is the critical position and it will be + // evaluated, if at rPos starts an interrupting or a maintaining + // continuity attribute. + + // iterAtStartOfNode is positioned to the first hint of the node + // (if any); the node item itself has already been handled above + for (sw::MergedAttrIterMulti iter = iterAtStartOfNode; ; ) + { + SwTextNode const* pNode(nullptr); + SwTextAttr const*const pTmp = iter.NextAttr(pNode); + if (!pNode) + { + break; + } + assert(startPos.first->GetIndex() <= pNode->GetIndex()); + TextFrameIndex nTmpStart; + TextFrameIndex nTmpEnd; + if (pTmp) + { + nTmpEnd = m_pFrame->MapModelToView(pNode, pTmp->GetAnyEnd()); + if (nTmpEnd <= rPos) + continue; + nTmpStart = m_pFrame->MapModelToView(pNode, pTmp->GetStart()); + } + else + { + pNodeTwoLinesItem = pNode->GetSwAttrSet().GetItemIfSet( + RES_CHRATR_TWO_LINES); + nTmpStart = m_pFrame->MapModelToView(pNode, 0); + nTmpEnd = m_pFrame->MapModelToView(pNode, pNode->Len()); + assert(rPos <= nTmpEnd); // next node must not have smaller index + } + + if (rPos < nTmpStart) + { + // If bOn is false and the next attribute starts later than rPos + // the winner attribute is interrupted at rPos. + // If the start of the next attribute is behind the end of + // the last attribute on the aEnd-stack, this is the endposition + // on the stack is the end of the 2-line portion. + if (!bOn || aEnd.back() < nTmpStart) + break; + // At this moment, bOn is true and the next attribute starts + // behind rPos, so we could move rPos to the next startpoint + rPos = nTmpStart; + // We clean up the aEnd-stack, endpositions equal to rPos are + // superfluous. + while( !aEnd.empty() && aEnd.back() <= rPos ) + { + bOn = !bOn; + aEnd.pop_back(); + } + // If the endstack is empty, we simulate an attribute with + // state true and endposition rPos + if( aEnd.empty() ) + { + aEnd.push_front( rPos ); + bOn = true; + } + } + // A ruby attribute stops the 2-line immediately + if (pTmp && RES_TXTATR_CJK_RUBY == pTmp->Which()) + return aRet; + if (pTmp ? lcl_Has2Lines(*pTmp, pActiveTwoLinesItem, bTwo) + : lcl_Check2Lines(pNodeTwoLinesItem, pActiveTwoLinesItem, bTwo)) + { // We have an interesting attribute... + if( bTwo == bOn ) + { // .. with the same state, so the last attribute could + // be continued. + if (aEnd.back() < nTmpEnd) + aEnd.back() = nTmpEnd; + } + else + { // .. with a different state. + bOn = bTwo; + // If this is smaller than the last on the stack, we put + // it on the stack. If it has the same endposition, the last + // could be removed. + if (nTmpEnd < aEnd.back()) + aEnd.push_back( nTmpEnd ); + else if( aEnd.size() > 1 ) + aEnd.pop_back(); + else + aEnd.back() = nTmpEnd; + } + } + } + if( bOn && !aEnd.empty() ) + rPos = aEnd.back(); + return aRet; + } + if (pActiveRotateHint || + (pNodeRotateItem && SfxPoolItem::areSame(pNodeRotateItem, pActiveRotateItem) && + rPos < TextFrameIndex(GetText().getLength()))) + { // The winner is a rotate-attribute, + // the end of the multiportion depends on the following attributes... + SwMultiCreator aRet; + aRet.nId = SwMultiCreatorId::Rotate; + + // We note the endpositions of the 2-line attributes in aEnd as stack + std::deque<TextFrameIndex> aEnd; + + // The bOn flag signs the state of the last 2-line attribute in the + // aEnd-stack, which could interrupts the winning rotation attribute. + bool bOn = pNodeTwoLinesItem != nullptr; + aEnd.push_front(TextFrameIndex(GetText().getLength())); + + // first, search for the start position of the next TWOLINE portion + // because the ROTATE portion must end there at the latest + TextFrameIndex n2Start = rPos; + for (sw::MergedAttrIterMulti iter = iterAtStartOfNode; ; ) + { + SwTextNode const* pNode(nullptr); + SwTextAttr const*const pTmp = iter.NextAttr(pNode); + if (!pNode) + { + break; + } + assert(startPos.first->GetIndex() <= pNode->GetIndex()); + TextFrameIndex nTmpStart; + TextFrameIndex nTmpEnd; + if (pTmp) + { + nTmpEnd = m_pFrame->MapModelToView(pNode, pTmp->GetAnyEnd()); + if (nTmpEnd <= n2Start) + continue; + nTmpStart = m_pFrame->MapModelToView(pNode, pTmp->GetStart()); + } + else + { + pNodeTwoLinesItem = pNode->GetSwAttrSet().GetItemIfSet( + RES_CHRATR_TWO_LINES); + nTmpStart = m_pFrame->MapModelToView(pNode, 0); + nTmpEnd = m_pFrame->MapModelToView(pNode, pNode->Len()); + assert(n2Start <= nTmpEnd); // next node must not have smaller index + } + + if (n2Start < nTmpStart) + { + if (bOn || aEnd.back() < nTmpStart) + break; + n2Start = nTmpStart; + while( !aEnd.empty() && aEnd.back() <= n2Start ) + { + bOn = !bOn; + aEnd.pop_back(); + } + if( aEnd.empty() ) + { + aEnd.push_front( n2Start ); + bOn = false; + } + } + // A ruby attribute stops immediately + if (pTmp && RES_TXTATR_CJK_RUBY == pTmp->Which()) + { + bOn = true; + break; + } + const SvxTwoLinesItem* p2Lines = nullptr; + if (pTmp ? lcl_Has2Lines(*pTmp, p2Lines, bTwo) + : lcl_Check2Lines(pNodeTwoLinesItem, p2Lines, bTwo)) + { + if( bTwo == bOn ) + { + if (aEnd.back() < nTmpEnd) + aEnd.back() = nTmpEnd; + } + else + { + bOn = bTwo; + if (nTmpEnd < aEnd.back()) + aEnd.push_back( nTmpEnd ); + else if( aEnd.size() > 1 ) + aEnd.pop_back(); + else + aEnd.back() = nTmpEnd; + } + } + } + if( !bOn && !aEnd.empty() ) + n2Start = aEnd.back(); + + aEnd.clear(); + + // now, search for the end of the ROTATE portion, similar to above + bOn = true; + if (pActiveRotateHint) + { + aRet.pItem = nullptr; + aRet.pAttr = pActiveRotateHint; + aRet.nStartOfAttr = m_pFrame->MapModelToView(startPos.first, aRet.pAttr->GetStart()); + if (pNodeRotateItem) + { + aEnd.push_front(m_pFrame->MapModelToView(startPos.first, startPos.first->Len())); + bOn = pNodeRotateItem->GetValue() == + pActiveRotateItem->GetValue(); + } + else + { + aEnd.push_front(m_pFrame->MapModelToView(startPos.first, *aRet.pAttr->End())); + } + } + else + { + aRet.pItem = pNodeRotateItem; + aRet.pAttr = nullptr; + aRet.nStartOfAttr = TextFrameIndex(-1); + aEnd.push_front(m_pFrame->MapModelToView(startPos.first, startPos.first->Len())); + } + for (sw::MergedAttrIterMulti iter = iterAtStartOfNode; ; ) + { + SwTextNode const* pNode(nullptr); + SwTextAttr const*const pTmp = iter.NextAttr(pNode); + if (!pNode) + { + break; + } + assert(startPos.first->GetIndex() <= pNode->GetIndex()); + TextFrameIndex nTmpStart; + TextFrameIndex nTmpEnd; + if (pTmp) + { + nTmpEnd = m_pFrame->MapModelToView(pNode, pTmp->GetAnyEnd()); + if (nTmpEnd <= rPos) + continue; + nTmpStart = m_pFrame->MapModelToView(pNode, pTmp->GetStart()); + } + else + { + pNodeRotateItem = pNode->GetSwAttrSet().GetItemIfSet( + RES_CHRATR_ROTATE); + nTmpStart = m_pFrame->MapModelToView(pNode, 0); + nTmpEnd = m_pFrame->MapModelToView(pNode, pNode->Len()); + assert(rPos <= nTmpEnd); // next node must not have smaller index + } + + if (rPos < nTmpStart) + { + if (!bOn || aEnd.back() < nTmpStart) + break; + rPos = nTmpStart; + while( !aEnd.empty() && aEnd.back() <= rPos ) + { + bOn = !bOn; + aEnd.pop_back(); + } + if( aEnd.empty() ) + { + aEnd.push_front( rPos ); + bOn = true; + } + } + if (pTmp && RES_TXTATR_CJK_RUBY == pTmp->Which()) + { + bOn = false; + break; + } + // TODO why does this use bTwo, not bRot ??? + if (pTmp ? lcl_HasRotation(*pTmp, pActiveRotateItem, bTwo) + : lcl_CheckRotation(pNodeRotateItem, pActiveRotateItem, bTwo)) + { + if( bTwo == bOn ) + { + if (aEnd.back() < nTmpEnd) + aEnd.back() = nTmpEnd; + } + else + { + bOn = bTwo; + if (nTmpEnd < aEnd.back()) + aEnd.push_back( nTmpEnd ); + else if( aEnd.size() > 1 ) + aEnd.pop_back(); + else + aEnd.back() = nTmpEnd; + } + } + } + if( bOn && !aEnd.empty() ) + rPos = aEnd.back(); + if( rPos > n2Start ) + rPos = n2Start; + return aRet; + } + return {}; +} + +namespace { + +// A little helper class to manage the spaceadd-arrays of the text adjustment +// during a PaintMultiPortion. +// The constructor prepares the array for the first line of multiportion, +// the SecondLine-function restores the values for the first line and prepares +// the second line. +// The destructor restores the values of the last manipulation. +class SwSpaceManipulator +{ + SwTextPaintInfo& m_rInfo; + SwMultiPortion& m_rMulti; + std::vector<tools::Long>* m_pOldSpaceAdd; + sal_uInt16 m_nOldSpaceIndex; + tools::Long m_nSpaceAdd; + bool m_bSpaceChg; + sal_uInt8 m_nOldDir; + +public: + SwSpaceManipulator( SwTextPaintInfo& rInf, SwMultiPortion& rMult ); + ~SwSpaceManipulator(); + void SecondLine(); + tools::Long GetSpaceAdd() const { return m_nSpaceAdd; } +}; + +} + +SwSpaceManipulator::SwSpaceManipulator(SwTextPaintInfo& rInf, SwMultiPortion& rMult) + : m_rInfo(rInf) + , m_rMulti(rMult) + , m_nSpaceAdd(0) +{ + m_pOldSpaceAdd = m_rInfo.GetpSpaceAdd(); + m_nOldSpaceIndex = m_rInfo.GetSpaceIdx(); + m_nOldDir = m_rInfo.GetDirection(); + m_rInfo.SetDirection(m_rMulti.GetDirection()); + m_bSpaceChg = false; + + if (m_rMulti.IsDouble()) + { + m_nSpaceAdd = (m_pOldSpaceAdd && !m_rMulti.HasTabulator()) ? m_rInfo.GetSpaceAdd() : 0; + if (m_rMulti.GetRoot().IsSpaceAdd()) + { + m_rInfo.SetpSpaceAdd(m_rMulti.GetRoot().GetpLLSpaceAdd()); + m_rInfo.ResetSpaceIdx(); + m_bSpaceChg = m_rMulti.ChgSpaceAdd(&m_rMulti.GetRoot(), m_nSpaceAdd); + } + else if (m_rMulti.HasTabulator()) + m_rInfo.SetpSpaceAdd(nullptr); + } + else if (!m_rMulti.IsBidi()) + { + m_rInfo.SetpSpaceAdd(m_rMulti.GetRoot().GetpLLSpaceAdd()); + m_rInfo.ResetSpaceIdx(); + } +} + +void SwSpaceManipulator::SecondLine() +{ + if (m_bSpaceChg) + { + m_rInfo.RemoveFirstSpaceAdd(); + m_bSpaceChg = false; + } + SwLineLayout* pLay = m_rMulti.GetRoot().GetNext(); + if( pLay->IsSpaceAdd() ) + { + m_rInfo.SetpSpaceAdd(pLay->GetpLLSpaceAdd()); + m_rInfo.ResetSpaceIdx(); + m_bSpaceChg = m_rMulti.ChgSpaceAdd(pLay, m_nSpaceAdd); + } + else + { + m_rInfo.SetpSpaceAdd((!m_rMulti.IsDouble() || m_rMulti.HasTabulator()) ? nullptr + : m_pOldSpaceAdd); + m_rInfo.SetSpaceIdx(m_nOldSpaceIndex); + } +} + +SwSpaceManipulator::~SwSpaceManipulator() +{ + if (m_bSpaceChg) + { + m_rInfo.RemoveFirstSpaceAdd(); + m_bSpaceChg = false; + } + m_rInfo.SetpSpaceAdd(m_pOldSpaceAdd); + m_rInfo.SetSpaceIdx(m_nOldSpaceIndex); + m_rInfo.SetDirection(m_nOldDir); +} + +// Manages the paint for a SwMultiPortion. +// External, for the calling function, it seems to be a normal Paint-function, +// internal it is like a SwTextFrame::PaintSwFrame with multiple DrawTextLines +void SwTextPainter::PaintMultiPortion( const SwRect &rPaint, + SwMultiPortion& rMulti, const SwMultiPortion* pEnvPor ) +{ + SwTextGridItem const*const pGrid(GetGridItem(m_pFrame->FindPageFrame())); + const bool bHasGrid = pGrid && GetInfo().SnapToGrid(); + sal_uInt16 nRubyHeight = 0; + bool bRubyTop = true; + + if ( bHasGrid && pGrid->IsSquaredMode() ) + { + nRubyHeight = pGrid->GetRubyHeight(); + bRubyTop = ! pGrid->GetRubyTextBelow(); + } + + // do not allow grid mode for first line in ruby portion + const bool bRubyInGrid = bHasGrid && rMulti.IsRuby(); + + const sal_uInt16 nOldHeight = rMulti.Height(); + const bool bOldGridModeAllowed = GetInfo().SnapToGrid(); + + if ( bRubyInGrid ) + { + GetInfo().SetSnapToGrid( ! bRubyTop ); + if (pGrid->IsSquaredMode()) + rMulti.Height( m_pCurr->Height() ); + } + + SwLayoutModeModifier aLayoutModeModifier( *GetInfo().GetOut() ); + bool bEnvDir = false; + bool bThisDir = false; + bool bFrameDir = false; + if ( rMulti.IsBidi() ) + { + // these values are needed for the calculation of the x coordinate + // and the layout mode + OSL_ENSURE( ! pEnvPor || pEnvPor->IsBidi(), + "Oh no, I expected a BidiPortion" ); + bFrameDir = GetInfo().GetTextFrame()->IsRightToLeft(); + bEnvDir = pEnvPor ? ((static_cast<const SwBidiPortion*>(pEnvPor)->GetLevel() % 2) != 0) : bFrameDir; + bThisDir = (static_cast<SwBidiPortion&>(rMulti).GetLevel() % 2) != 0; + } + +#if OSL_DEBUG_LEVEL > 1 + // only paint first level bidi portions + if( rMulti.Width() > 1 && ! pEnvPor ) + GetInfo().DrawViewOpt( rMulti, PortionType::Field ); +#endif + + if ( bRubyInGrid && pGrid->IsSquaredMode() ) + rMulti.Height( nOldHeight ); + + // do we have to repaint a post it portion? + if( GetInfo().OnWin() && rMulti.GetNextPortion() && + ! rMulti.GetNextPortion()->Width() ) + rMulti.GetNextPortion()->PrePaint( GetInfo(), &rMulti ); + + // old values must be saved and restored at the end + TextFrameIndex const nOldLen = GetInfo().GetLen(); + const SwTwips nOldX = GetInfo().X(); + const SwTwips nOldY = GetInfo().Y(); + TextFrameIndex const nOldIdx = GetInfo().GetIdx(); + + SwSpaceManipulator aManip( GetInfo(), rMulti ); + + std::optional<SwFontSave> oFontSave; + std::unique_ptr<SwFont> pTmpFnt; + + if( rMulti.IsDouble() ) + { + pTmpFnt.reset(new SwFont( *GetInfo().GetFont() )); + if( rMulti.IsDouble() ) + { + SetPropFont( 50 ); + pTmpFnt->SetProportion( GetPropFont() ); + } + oFontSave.emplace( GetInfo(), pTmpFnt.get(), this ); + } + else + { + pTmpFnt = nullptr; + } + + if( rMulti.HasBrackets() ) + { + // WP is mandatory + Por_Info const por(rMulti, *this, 1); + SwTaggedPDFHelper const tag(nullptr, nullptr, &por, *GetInfo().GetOut()); + + TextFrameIndex const nTmpOldIdx = GetInfo().GetIdx(); + GetInfo().SetIdx(static_cast<SwDoubleLinePortion&>(rMulti).GetBrackets()->nStart); + SeekAndChg( GetInfo() ); + static_cast<SwDoubleLinePortion&>(rMulti).PaintBracket( GetInfo(), 0, true ); + GetInfo().SetIdx( nTmpOldIdx ); + } + + const SwTwips nTmpX = GetInfo().X(); + + SwLineLayout* pLay = &rMulti.GetRoot();// the first line of the multiportion + SwLinePortion* pPor = pLay->GetFirstPortion();//first portion of these line + SwTwips nOfst = 0; + + // GetInfo().Y() is the baseline from the surrounding line. We must switch + // this temporary to the baseline of the inner lines of the multiportion. + if( rMulti.HasRotation() ) + { + if( rMulti.IsRevers() ) + { + GetInfo().Y( nOldY - rMulti.GetAscent() ); + nOfst = nTmpX + rMulti.Width(); + } + else + { + GetInfo().Y( nOldY - rMulti.GetAscent() + rMulti.Height() ); + nOfst = nTmpX; + } + } + else if ( rMulti.IsBidi() ) + { + // does the current bidi portion has the same direction + // as its environment? + if ( bEnvDir != bThisDir ) + { + // different directions, we have to adjust the x coordinate + SwTwips nMultiWidth = rMulti.Width() + + rMulti.CalcSpacing( GetInfo().GetSpaceAdd(), GetInfo() ); + + if ( bFrameDir == bThisDir ) + GetInfo().X( GetInfo().X() - nMultiWidth ); + else + GetInfo().X( GetInfo().X() + nMultiWidth ); + } + + nOfst = nOldY - rMulti.GetAscent(); + + // set layout mode + aLayoutModeModifier.Modify( bThisDir ); + } + else + nOfst = nOldY - rMulti.GetAscent(); + + bool bRest = pLay->IsRest(); + bool bFirst = true; + + OSL_ENSURE( nullptr == GetInfo().GetUnderFnt() || rMulti.IsBidi(), + " Only BiDi portions are allowed to use the common underlining font" ); + + ::std::optional<SwTaggedPDFHelper> oTag; + if (rMulti.IsDouble()) + { + Por_Info const por(rMulti, *this, 2); + oTag.emplace(nullptr, nullptr, &por, *GetInfo().GetOut()); + } + else if (rMulti.IsRuby()) + { + Por_Info const por(rMulti, *this, bRubyTop ? 1 : 2); + oTag.emplace(nullptr, nullptr, &por, *GetInfo().GetOut()); + GetInfo().SetRuby( rMulti.OnTop() ); + } + + do + { + if ( bHasGrid && pGrid->IsSquaredMode() ) + { + if( rMulti.HasRotation() ) + { + const sal_uInt16 nAdjustment = ( pLay->Height() - pPor->Height() ) / 2 + + pPor->GetAscent(); + if( rMulti.IsRevers() ) + GetInfo().X( nOfst - nAdjustment ); + else + GetInfo().X( nOfst + nAdjustment ); + } + else + { + // special treatment for ruby portions in grid mode + SwTwips nAdjustment = 0; + if ( rMulti.IsRuby() ) + { + if ( bRubyTop != ( pLay == &rMulti.GetRoot() ) ) + // adjust base text + nAdjustment = ( m_pCurr->Height() - nRubyHeight - pPor->Height() ) / 2; + else if ( bRubyTop ) + // adjust upper ruby text + nAdjustment = nRubyHeight - pPor->Height(); + // else adjust lower ruby text + } + + GetInfo().Y( nOfst + nAdjustment + pPor->GetAscent() ); + } + } + else if( rMulti.HasRotation() ) + { + if( rMulti.IsRevers() ) + GetInfo().X( nOfst - AdjustBaseLine( *pLay, pPor, 0, 0, true ) ); + else + GetInfo().X( nOfst + AdjustBaseLine( *pLay, pPor ) ); + } + else if ( rMulti.IsRuby() && rMulti.OnRight() && GetInfo().IsRuby() ) + { + SwTwips nLineDiff = std::max(( rMulti.GetRoot().Height() - pPor->Width() ) / 2, static_cast<SwTwips>(0) ); + GetInfo().Y( nOfst + nLineDiff ); + // Draw the ruby text on top of the preserved space. + GetInfo().X( GetInfo().X() - pPor->Height() ); + } + else + GetInfo().Y( nOfst + AdjustBaseLine( *pLay, pPor ) ); + + bool bSeeked = true; + GetInfo().SetLen( pPor->GetLen() ); + + if( bRest && pPor->InFieldGrp() && !pPor->GetLen() ) + { + if( static_cast<SwFieldPortion*>(pPor)->HasFont() ) + bSeeked = false; + else + SeekAndChgBefore( GetInfo() ); + } + else if( pPor->InTextGrp() || pPor->InFieldGrp() || pPor->InTabGrp() ) + SeekAndChg( GetInfo() ); + else if ( !bFirst && pPor->IsBreakPortion() && GetInfo().GetOpt().IsParagraph() ) + { + if( GetRedln() ) + SeekAndChg( GetInfo() ); + else + SeekAndChgBefore( GetInfo() ); + } + else + bSeeked = false; + + SwLinePortion *pNext = pPor->GetNextPortion(); + if(GetInfo().OnWin() && pNext && !pNext->Width() ) + { + if ( !bSeeked ) + SeekAndChg( GetInfo() ); + pNext->PrePaint( GetInfo(), pPor ); + } + + CheckSpecialUnderline( pPor ); + SwUnderlineFont* pUnderLineFnt = GetInfo().GetUnderFnt(); + if ( pUnderLineFnt ) + { + if ( rMulti.IsDouble() ) + pUnderLineFnt->GetFont().SetProportion( 50 ); + pUnderLineFnt->SetPos( GetInfo().GetPos() ); + } + + if ( rMulti.IsBidi() ) + { + // we do not allow any rotation inside a bidi portion + SwFont* pTmpFont = GetInfo().GetFont(); + pTmpFont->SetVertical( 0_deg10, GetInfo().GetTextFrame()->IsVertical() ); + } + + if( pPor->IsMultiPortion() && static_cast<SwMultiPortion*>(pPor)->IsBidi() ) + { + // but we do allow nested bidi portions + OSL_ENSURE( rMulti.IsBidi(), "Only nesting of bidi portions is allowed" ); + PaintMultiPortion( rPaint, static_cast<SwMultiPortion&>(*pPor), &rMulti ); + } + else + { + Por_Info const por(*pPor, *this, 0); + SwTaggedPDFHelper const tag(nullptr, nullptr, &por, *GetInfo().GetOut()); + + pPor->Paint( GetInfo() ); + } + + bFirst &= !pPor->GetLen(); + if( pNext || !pPor->IsMarginPortion() ) + pPor->Move( GetInfo() ); + + pPor = pNext; + + // If there's no portion left, we go to the next line + if( !pPor && pLay->GetNext() ) + { + pLay = pLay->GetNext(); + pPor = pLay->GetFirstPortion(); + bRest = pLay->IsRest(); + aManip.SecondLine(); + + // delete underline font + delete GetInfo().GetUnderFnt(); + GetInfo().SetUnderFnt( nullptr ); + + if( rMulti.HasRotation() ) + { + if( rMulti.IsRevers() ) + { + nOfst += pLay->Height(); + GetInfo().Y( nOldY - rMulti.GetAscent() ); + } + else + { + nOfst -= pLay->Height(); + GetInfo().Y( nOldY - rMulti.GetAscent() + rMulti.Height() ); + } + } + else if ( bHasGrid && rMulti.IsRuby() ) + { + GetInfo().SetSnapToGrid( bRubyTop ); + GetInfo().X( nTmpX ); + if (pGrid->IsSquaredMode() ) + { + if ( bRubyTop ) + nOfst += nRubyHeight; + else + nOfst += m_pCurr->Height() - nRubyHeight; + } + else + { + nOfst += rMulti.GetRoot().Height(); + } + } + else if ( rMulti.IsRuby() && rMulti.OnRight() ) + { + GetInfo().SetDirection( DIR_TOP2BOTTOM ); + GetInfo().SetRuby( true ); + } else + { + GetInfo().X( nTmpX ); + // We switch to the baseline of the next inner line + nOfst += rMulti.GetRoot().Height(); + } + if (rMulti.IsRuby()) + { + oTag.reset(); + Por_Info const por(rMulti, *this, bRubyTop ? 2 : 1); + oTag.emplace(nullptr, nullptr, &por, *GetInfo().GetOut()); + } + } + } while( pPor ); + + if (rMulti.IsDouble()) + { + oTag.reset(); + } + + if ( bRubyInGrid ) + GetInfo().SetSnapToGrid( bOldGridModeAllowed ); + + // delete underline font + if ( ! rMulti.IsBidi() ) + { + delete GetInfo().GetUnderFnt(); + GetInfo().SetUnderFnt( nullptr ); + } + + GetInfo().SetIdx( nOldIdx ); + GetInfo().Y( nOldY ); + + if( rMulti.HasBrackets() ) + { + // WP is mandatory + Por_Info const por(rMulti, *this, 1); + SwTaggedPDFHelper const tag(nullptr, nullptr, &por, *GetInfo().GetOut()); + + TextFrameIndex const nTmpOldIdx = GetInfo().GetIdx(); + GetInfo().SetIdx(static_cast<SwDoubleLinePortion&>(rMulti).GetBrackets()->nStart); + SeekAndChg( GetInfo() ); + GetInfo().X( nOldX ); + static_cast<SwDoubleLinePortion&>(rMulti).PaintBracket( GetInfo(), + aManip.GetSpaceAdd(), false ); + GetInfo().SetIdx( nTmpOldIdx ); + } + // Restore the saved values + GetInfo().X( nOldX ); + GetInfo().SetLen( nOldLen ); + oFontSave.reset(); + pTmpFnt.reset(); + SetPropFont( 0 ); +} + +static bool lcl_ExtractFieldFollow( SwLineLayout* pLine, SwLinePortion* &rpField ) +{ + SwLinePortion* pLast = pLine; + rpField = pLine->GetNextPortion(); + while( rpField && !rpField->InFieldGrp() ) + { + pLast = rpField; + rpField = rpField->GetNextPortion(); + } + bool bRet = rpField != nullptr; + if( bRet ) + { + if( static_cast<SwFieldPortion*>(rpField)->IsFollow() ) + { + rpField->Truncate(); + pLast->SetNextPortion( nullptr ); + } + else + rpField = nullptr; + } + pLine->Truncate(); + return bRet; +} + +// If a multi portion completely has to go to the +// next line, this function is called to truncate +// the rest of the remaining multi portion +static void lcl_TruncateMultiPortion( SwMultiPortion& rMulti, SwTextFormatInfo& rInf, + TextFrameIndex const nStartIdx) +{ + rMulti.GetRoot().Truncate(); + rMulti.GetRoot().SetLen(TextFrameIndex(0)); + rMulti.GetRoot().Width(0); +// rMulti.CalcSize( *this, aInf ); + if ( rMulti.GetRoot().GetNext() ) + { + rMulti.GetRoot().GetNext()->Truncate(); + rMulti.GetRoot().GetNext()->SetLen(TextFrameIndex(0)); + rMulti.GetRoot().GetNext()->Width( 0 ); + } + rMulti.Width( 0 ); + rMulti.SetLen(TextFrameIndex(0)); + rInf.SetIdx( nStartIdx ); +} + +// Manages the formatting of a SwMultiPortion. External, for the calling +// function, it seems to be a normal Format-function, internal it is like a +// SwTextFrame::Format_ with multiple BuildPortions +bool SwTextFormatter::BuildMultiPortion( SwTextFormatInfo &rInf, + SwMultiPortion& rMulti ) +{ + SwTwips nMaxWidth = rInf.Width(); + SwTwips nOldX = 0; + + if( rMulti.HasBrackets() ) + { + TextFrameIndex const nOldIdx = rInf.GetIdx(); + rInf.SetIdx( static_cast<SwDoubleLinePortion&>(rMulti).GetBrackets()->nStart ); + SeekAndChg( rInf ); + nOldX = GetInfo().X(); + static_cast<SwDoubleLinePortion&>(rMulti).FormatBrackets( rInf, nMaxWidth ); + rInf.SetIdx( nOldIdx ); + } + + SeekAndChg( rInf ); + std::optional<SwFontSave> oFontSave; + std::unique_ptr<SwFont> xTmpFont; + if( rMulti.IsDouble() ) + { + xTmpFont.reset(new SwFont( *rInf.GetFont() )); + if( rMulti.IsDouble() ) + { + SetPropFont( 50 ); + xTmpFont->SetProportion( GetPropFont() ); + } + oFontSave.emplace(rInf, xTmpFont.get(), this); + } + + SwLayoutModeModifier aLayoutModeModifier( *GetInfo().GetOut() ); + if ( rMulti.IsBidi() ) + { + // set layout mode + aLayoutModeModifier.Modify( ! rInf.GetTextFrame()->IsRightToLeft() ); + } + + SwTwips nTmpX = 0; + + if( rMulti.HasRotation() ) + { + // For nMaxWidth we take the height of the body frame. + // #i25067#: If the current frame is inside a table, we restrict + // nMaxWidth to the current frame height, unless the frame size + // attribute is set to variable size: + + // We set nTmpX (which is used for portion calculating) to the + // current Y value + const SwPageFrame* pPage = m_pFrame->FindPageFrame(); + OSL_ENSURE( pPage, "No page in frame!"); + const SwLayoutFrame* pUpperFrame = pPage; + + if ( m_pFrame->IsInTab() ) + { + pUpperFrame = m_pFrame->GetUpper(); + while ( pUpperFrame && !pUpperFrame->IsCellFrame() ) + pUpperFrame = pUpperFrame->GetUpper(); + assert(pUpperFrame); //pFrame is in table but does not have an upper cell frame + if (!pUpperFrame) + return false; + const SwTableLine* pLine = static_cast<const SwRowFrame*>(pUpperFrame->GetUpper())->GetTabLine(); + const SwFormatFrameSize& rFrameFormatSize = pLine->GetFrameFormat()->GetFrameSize(); + if ( SwFrameSize::Variable == rFrameFormatSize.GetHeightSizeType() ) + pUpperFrame = pPage; + } + if ( pUpperFrame == pPage && !m_pFrame->IsInFootnote() ) + pUpperFrame = pPage->FindBodyCont(); + + nMaxWidth = pUpperFrame ? + ( rInf.GetTextFrame()->IsVertical() ? + pUpperFrame->getFramePrintArea().Width() : + pUpperFrame->getFramePrintArea().Height() ) : + USHRT_MAX; + } + else + nTmpX = rInf.X(); + + SwMultiPortion* pOldMulti = m_pMulti; + + m_pMulti = &rMulti; + SwLineLayout *pOldCurr = m_pCurr; + TextFrameIndex const nOldStart = GetStart(); + SwTwips nMinWidth = nTmpX + 1; + SwTwips nActWidth = nMaxWidth; + const TextFrameIndex nStartIdx = rInf.GetIdx(); + TextFrameIndex nMultiLen = rMulti.GetLen(); + + SwLinePortion *pFirstRest; + SwLinePortion *pSecondRest; + if( rMulti.IsFormatted() ) + { + if( !lcl_ExtractFieldFollow( &rMulti.GetRoot(), pFirstRest ) + && rMulti.IsDouble() && rMulti.GetRoot().GetNext() ) + lcl_ExtractFieldFollow( rMulti.GetRoot().GetNext(), pFirstRest ); + if( !rMulti.IsDouble() && rMulti.GetRoot().GetNext() ) + lcl_ExtractFieldFollow( rMulti.GetRoot().GetNext(), pSecondRest ); + else + pSecondRest = nullptr; + } + else + { + pFirstRest = rMulti.GetRoot().GetNextPortion(); + pSecondRest = rMulti.GetRoot().GetNext() ? + rMulti.GetRoot().GetNext()->GetNextPortion() : nullptr; + if( pFirstRest ) + rMulti.GetRoot().SetNextPortion( nullptr ); + if( pSecondRest ) + rMulti.GetRoot().GetNext()->SetNextPortion( nullptr ); + rMulti.SetFormatted(); + nMultiLen = nMultiLen - rInf.GetIdx(); + } + + // save some values + const OUString* pOldText = &(rInf.GetText()); + const SwTwips nOldPaintOfst = rInf.GetPaintOfst(); + std::shared_ptr<const vcl::text::TextLayoutCache> const pOldCachedVclData(rInf.GetCachedVclData()); + rInf.SetCachedVclData(nullptr); + + OUString const aMultiStr( rInf.GetText().copy(0, sal_Int32(nMultiLen + rInf.GetIdx())) ); + rInf.SetText( aMultiStr ); + SwTextFormatInfo aInf( rInf, rMulti.GetRoot(), nActWidth ); + // Do we allow break cuts? The FirstMulti-Flag is evaluated during + // line break determination. + bool bFirstMulti = rInf.GetIdx() != rInf.GetLineStart(); + + SwLinePortion *pNextFirst = nullptr; + SwLinePortion *pNextSecond = nullptr; + bool bRet = false; + + SwTextGridItem const*const pGrid(GetGridItem(m_pFrame->FindPageFrame())); + const bool bHasGrid = pGrid && GRID_LINES_CHARS == pGrid->GetGridType(); + + bool bRubyTop = false; + + if ( bHasGrid ) + bRubyTop = ! pGrid->GetRubyTextBelow(); + + do + { + m_pCurr = &rMulti.GetRoot(); + m_nStart = nStartIdx; + bRet = false; + FormatReset( aInf ); + aInf.X( nTmpX ); + aInf.Width( sal_uInt16(nActWidth) ); + aInf.RealWidth( sal_uInt16(nActWidth) ); + aInf.SetFirstMulti( bFirstMulti ); + aInf.SetNumDone( rInf.IsNumDone() ); + aInf.SetFootnoteDone( rInf.IsFootnoteDone() ); + + // if there's a bookmark at the start of the MultiPortion, it will be + // painted with the rotation etc. of the MultiPortion; move it *inside* + // so it gets positioned correctly; currently there's no other portion + // inserted between the end of WhichFirstPortion() and + // BuildMultiPortion() + if (rInf.GetLast()->GetWhichPor() == PortionType::Bookmark) + { + auto const pBookmark(static_cast<SwBookmarkPortion*>(rInf.GetLast())); + auto *const pPrevious = pBookmark->FindPrevPortion(rInf.GetRoot()); + assert(!pPrevious || pPrevious->GetNextPortion() == pBookmark); + if (pPrevious) + { + pPrevious->SetNextPortion(nullptr); + } + rInf.SetLast(pPrevious); + assert(m_pCurr->GetNextPortion() == nullptr); + m_pCurr->SetNextPortion(pBookmark); + } + + if( pFirstRest ) + { + OSL_ENSURE( pFirstRest->InFieldGrp(), "BuildMulti: Fieldrest expected"); + SwFieldPortion *pField = + static_cast<SwFieldPortion*>(pFirstRest)->Clone( + static_cast<SwFieldPortion*>(pFirstRest)->GetExp() ); + pField->SetFollow( true ); + aInf.SetRest( pField ); + } + aInf.SetRuby( rMulti.IsRuby() && rMulti.OnTop() ); + + // in grid mode we temporarily have to disable the grid for the ruby line + const bool bOldGridModeAllowed = GetInfo().SnapToGrid(); + if ( bHasGrid && aInf.IsRuby() && bRubyTop ) + aInf.SetSnapToGrid( false ); + + // If there's no more rubytext, then buildportion is forbidden + if( pFirstRest || !aInf.IsRuby() ) + BuildPortions( aInf ); + + aInf.SetSnapToGrid( bOldGridModeAllowed ); + + rMulti.CalcSize( *this, aInf ); + m_pCurr->SetRealHeight( m_pCurr->Height() ); + + if( rMulti.IsBidi() ) + { + pNextFirst = aInf.GetRest(); + break; + } + + if( rMulti.HasRotation() && !rMulti.IsDouble() ) + break; + // second line has to be formatted + else if( m_pCurr->GetLen()<nMultiLen || rMulti.IsRuby() || aInf.GetRest()) + { + TextFrameIndex const nFirstLen = m_pCurr->GetLen(); + delete m_pCurr->GetNext(); + m_pCurr->SetNext( new SwLineLayout() ); + m_pCurr = m_pCurr->GetNext(); + m_nStart = aInf.GetIdx(); + aInf.X( nTmpX ); + SwTextFormatInfo aTmp( aInf, *m_pCurr, nActWidth ); + if( rMulti.IsRuby() ) + { + aTmp.SetRuby( !rMulti.OnTop() ); + pNextFirst = aInf.GetRest(); + if( pSecondRest ) + { + OSL_ENSURE( pSecondRest->InFieldGrp(), "Fieldrest expected"); + SwFieldPortion *pField = static_cast<SwFieldPortion*>(pSecondRest)->Clone( + static_cast<SwFieldPortion*>(pSecondRest)->GetExp() ); + pField->SetFollow( true ); + aTmp.SetRest( pField ); + } + if( !rMulti.OnTop() && nFirstLen < nMultiLen ) + bRet = true; + } + else + aTmp.SetRest( aInf.GetRest() ); + aInf.SetRest( nullptr ); + + // in grid mode we temporarily have to disable the grid for the ruby line + if ( bHasGrid && aTmp.IsRuby() && ! bRubyTop ) + aTmp.SetSnapToGrid( false ); + + BuildPortions( aTmp ); + + const SwLinePortion *pRightPortion = rMulti.OnRight() ? + rMulti.GetRoot().GetNext()->GetNextPortion() : nullptr; + if (pRightPortion) + { + // The ruby text on the right is vertical. + // The width and the height are swapped. + SwTwips nHeight = pRightPortion->Height(); + // Keep room for the ruby text. + rMulti.GetRoot().FindLastPortion()->AddPrtWidth( nHeight ); + } + + aTmp.SetSnapToGrid( bOldGridModeAllowed ); + + rMulti.CalcSize( *this, aInf ); + rMulti.GetRoot().SetRealHeight( rMulti.GetRoot().Height() ); + m_pCurr->SetRealHeight( m_pCurr->Height() ); + if( rMulti.IsRuby() ) + { + pNextSecond = aTmp.GetRest(); + if( pNextFirst ) + bRet = true; + } + else + pNextFirst = aTmp.GetRest(); + if( ( !aTmp.IsRuby() && nFirstLen + m_pCurr->GetLen() < nMultiLen ) + || aTmp.GetRest() ) + // our guess for width of multiportion was too small, + // text did not fit into multiportion + bRet = true; + } + if( rMulti.IsRuby() ) + break; + if( bRet ) + { + // our guess for multiportion width was too small, + // we set min to act + nMinWidth = nActWidth; + nActWidth = ( 3 * nMaxWidth + nMinWidth + 3 ) / 4; + if ( nActWidth == nMaxWidth && rInf.GetLineStart() == rInf.GetIdx() ) + // we have too less space, we must allow break cuts + // ( the first multi flag is considered during TextPortion::Format_() ) + bFirstMulti = false; + if( nActWidth <= nMinWidth ) + break; + } + else + { + // For Solaris, this optimization can causes trouble: + // Setting this to the portion width ( = rMulti.Width() ) + // can make GetTextBreak inside SwTextGuess::Guess return too small + // values. Therefore we add some extra twips. + if( nActWidth > nTmpX + rMulti.Width() + 6 ) + nActWidth = nTmpX + rMulti.Width() + 6; + nMaxWidth = nActWidth; + nActWidth = ( 3 * nMaxWidth + nMinWidth + 3 ) / 4; + if( nActWidth >= nMaxWidth ) + break; + // we do not allow break cuts during formatting + bFirstMulti = true; + } + delete pNextFirst; + pNextFirst = nullptr; + } while ( true ); + + m_pMulti = pOldMulti; + + m_pCurr = pOldCurr; + m_nStart = nOldStart; + SetPropFont( 0 ); + + rMulti.SetLen( rMulti.GetRoot().GetLen() + ( rMulti.GetRoot().GetNext() ? + rMulti.GetRoot().GetNext()->GetLen() : TextFrameIndex(0) ) ); + + if( rMulti.IsDouble() ) + { + static_cast<SwDoubleLinePortion&>(rMulti).CalcBlanks( rInf ); + if( static_cast<SwDoubleLinePortion&>(rMulti).GetLineDiff() ) + { + SwLineLayout* pLine = &rMulti.GetRoot(); + if( static_cast<SwDoubleLinePortion&>(rMulti).GetLineDiff() > 0 ) + { + rInf.SetIdx( nStartIdx + pLine->GetLen() ); + pLine = pLine->GetNext(); + } + if( pLine ) + { + GetInfo().SetMulti( true ); + + // If the fourth element bSkipKashida of function CalcNewBlock is true, multiportion will be showed in justification. + // Kashida (Persian) is a type of justification used in some cursive scripts, particularly Arabic. + // In contrast to white-space justification, which increases the length of a line of text by expanding spaces between words or individual letters, + // kashida justification is accomplished by elongating characters at certain chosen points. + // Kashida justification can be combined with white-space justification to various extents. + // The default value of bSkipKashida (the 4th parameter passed to 'CalcNewBlock') is false. + // Only when Adjust is SvxAdjust::Block ( alignment is justify ), multiportion will be showed in justification in new code. + CalcNewBlock( pLine, nullptr, rMulti.Width(), GetAdjust() != SvxAdjust::Block ); + + GetInfo().SetMulti( false ); + } + rInf.SetIdx( nStartIdx ); + } + if( static_cast<SwDoubleLinePortion&>(rMulti).GetBrackets() ) + { + rMulti.Width( rMulti.Width() + + static_cast<SwDoubleLinePortion&>(rMulti).BracketWidth() ); + GetInfo().X( nOldX ); + } + } + else + { + rMulti.ActualizeTabulator(); + if( rMulti.IsRuby() ) + { + static_cast<SwRubyPortion&>(rMulti).Adjust( rInf ); + static_cast<SwRubyPortion&>(rMulti).CalcRubyOffset(); + } + } + if( rMulti.HasRotation() ) + { + SwTwips nH = rMulti.Width(); + SwTwips nAsc = rMulti.GetAscent() + ( nH - rMulti.Height() )/2; + if( nAsc > nH ) + nAsc = nH; + else if( nAsc < 0 ) + nAsc = 0; + rMulti.Width( rMulti.Height() ); + rMulti.Height( sal_uInt16(nH) ); + rMulti.SetAscent( sal_uInt16(nAsc) ); + bRet = ( rInf.GetPos().X() + rMulti.Width() > rInf.Width() ) && + nStartIdx != rInf.GetLineStart(); + } + else if ( rMulti.IsBidi() ) + { + bRet = rMulti.GetLen() < nMultiLen || pNextFirst; + } + + // line break has to be performed! + if( bRet ) + { + OSL_ENSURE( !pNextFirst || pNextFirst->InFieldGrp(), + "BuildMultiPortion: Surprising restportion, field expected" ); + SwMultiPortion *pTmp; + if( rMulti.IsDouble() ) + pTmp = new SwDoubleLinePortion( static_cast<SwDoubleLinePortion&>(rMulti), + nMultiLen + rInf.GetIdx() ); + else if( rMulti.IsRuby() ) + { + OSL_ENSURE( !pNextSecond || pNextSecond->InFieldGrp(), + "BuildMultiPortion: Surprising restportion, field expected" ); + + if ( rInf.GetIdx() == rInf.GetLineStart() ) + { + // the ruby portion has to be split in two portions + pTmp = new SwRubyPortion( static_cast<SwRubyPortion&>(rMulti), + nMultiLen + rInf.GetIdx() ); + + if( pNextSecond ) + { + pTmp->GetRoot().SetNext( new SwLineLayout() ); + pTmp->GetRoot().GetNext()->SetNextPortion( pNextSecond ); + } + pTmp->SetFollowField(); + } + else + { + // we try to keep our ruby portion together + lcl_TruncateMultiPortion( rMulti, rInf, nStartIdx ); + pTmp = nullptr; + // A follow field portion may still be waiting. If nobody wants + // it, we delete it. + delete pNextSecond; + } + } + else if( rMulti.HasRotation() ) + { + // we try to keep our rotated portion together + lcl_TruncateMultiPortion( rMulti, rInf, nStartIdx ); + pTmp = new SwRotatedPortion( nMultiLen + rInf.GetIdx(), + rMulti.GetDirection() ); + } + // during a recursion of BuildMultiPortions we may not build + // a new SwBidiPortion, this would cause a memory leak + else if( rMulti.IsBidi() && ! m_pMulti ) + { + if ( ! rMulti.GetLen() ) + lcl_TruncateMultiPortion( rMulti, rInf, nStartIdx ); + + // If there is a HolePortion at the end of the bidi portion, + // it has to be moved behind the bidi portion. Otherwise + // the visual cursor travelling gets into trouble. + SwLineLayout& aRoot = rMulti.GetRoot(); + SwLinePortion* pPor = aRoot.GetFirstPortion(); + while ( pPor ) + { + if ( pPor->GetNextPortion() && pPor->GetNextPortion()->IsHolePortion() ) + { + SwLinePortion* pHolePor = pPor->GetNextPortion(); + pPor->SetNextPortion( nullptr ); + aRoot.SetLen( aRoot.GetLen() - pHolePor->GetLen() ); + rMulti.SetLen( rMulti.GetLen() - pHolePor->GetLen() ); + rMulti.SetNextPortion( pHolePor ); + break; + } + pPor = pPor->GetNextPortion(); + } + + pTmp = new SwBidiPortion( nMultiLen + rInf.GetIdx(), + static_cast<SwBidiPortion&>(rMulti).GetLevel() ); + } + else + pTmp = nullptr; + + if ( ! rMulti.GetLen() && rInf.GetLast() ) + { + SeekAndChgBefore( rInf ); + rInf.GetLast()->FormatEOL( rInf ); + } + + if( pNextFirst && pTmp ) + { + pTmp->SetFollowField(); + pTmp->GetRoot().SetNextPortion( pNextFirst ); + } + else + // A follow field portion is still waiting. If nobody wants it, + // we delete it. + delete pNextFirst; + + rInf.SetRest( pTmp ); + } + + rInf.SetCachedVclData(pOldCachedVclData); + rInf.SetText( *pOldText ); + rInf.SetPaintOfst( nOldPaintOfst ); + rInf.SetStop( aInf.IsStop() ); + rInf.SetNumDone( true ); + rInf.SetFootnoteDone( true ); + SeekAndChg( rInf ); + delete pFirstRest; + delete pSecondRest; + oFontSave.reset(); + return bRet; +} + +static bool IsIncompleteRuby(const SwMultiPortion& rHelpMulti) +{ + return rHelpMulti.IsRuby() && static_cast<const SwRubyPortion&>(rHelpMulti).GetRubyOffset() < TextFrameIndex(COMPLETE_STRING); +} + +// When a fieldportion at the end of line breaks and needs a following +// fieldportion in the next line, then the "restportion" of the formatinfo +// has to be set. Normally this happens during the formatting of the first +// part of the fieldportion. +// But sometimes the formatting starts at the line with the following part, +// especially when the following part is on the next page. +// In this case the MakeRestPortion-function has to create the following part. +// The first parameter is the line that contains possibly a first part +// of a field. When the function finds such field part, it creates the right +// restportion. This may be a multiportion, e.g. if the field is surrounded by +// a doubleline- or ruby-portion. +// The second parameter is the start index of the line. +SwLinePortion* SwTextFormatter::MakeRestPortion( const SwLineLayout* pLine, + TextFrameIndex nPosition) +{ + if( !nPosition ) + return nullptr; + TextFrameIndex nMultiPos = nPosition - pLine->GetLen(); + const SwMultiPortion *pTmpMulti = nullptr; + const SwMultiPortion *pHelpMulti = nullptr; + const SwLinePortion* pPor = pLine->GetFirstPortion(); + SwFieldPortion *pField = nullptr; + while( pPor ) + { + if( pPor->GetLen() && !pHelpMulti ) + { + nMultiPos = nMultiPos + pPor->GetLen(); + pTmpMulti = nullptr; + } + if( pPor->InFieldGrp() ) + { + if( !pHelpMulti ) + pTmpMulti = nullptr; + pField = const_cast<SwFieldPortion*>(static_cast<const SwFieldPortion*>(pPor)); + } + else if( pPor->IsMultiPortion() ) + { + OSL_ENSURE( !pHelpMulti || pHelpMulti->IsBidi(), + "Nested multiportions are forbidden." ); + + pField = nullptr; + pTmpMulti = static_cast<const SwMultiPortion*>(pPor); + } + pPor = pPor->GetNextPortion(); + // If the last portion is a multi-portion, we enter it + // and look for a field portion inside. + // If we are already in a multiportion, we could change to the + // next line + if( !pPor && pTmpMulti ) + { + if( pHelpMulti ) + { // We're already inside the multiportion, let's take the second + // line, if we are in a double line portion + if( !pHelpMulti->IsRuby() ) + pPor = pHelpMulti->GetRoot().GetNext(); + pTmpMulti = nullptr; + } + else + { // Now we enter a multiportion, in a ruby portion we take the + // main line, not the phonetic line, in a doublelineportion we + // starts with the first line. + pHelpMulti = pTmpMulti; + nMultiPos = nMultiPos - pHelpMulti->GetLen(); + if( pHelpMulti->IsRuby() && pHelpMulti->OnTop() ) + pPor = pHelpMulti->GetRoot().GetNext(); + else + pPor = pHelpMulti->GetRoot().GetFirstPortion(); + } + } + } + if( pField && !pField->HasFollow() ) + pField = nullptr; + + SwLinePortion *pRest = nullptr; + if( pField ) + { + const SwTextAttr *pHint = GetAttr(nPosition - TextFrameIndex(1)); + if ( pHint + && ( pHint->Which() == RES_TXTATR_FIELD + || pHint->Which() == RES_TXTATR_ANNOTATION ) ) + { + pRest = NewFieldPortion( GetInfo(), pHint ); + if( pRest->InFieldGrp() ) + static_cast<SwFieldPortion*>(pRest)->TakeNextOffset( pField ); + else + { + delete pRest; + pRest = nullptr; + } + } + } + if( !pHelpMulti ) + return pRest; + + nPosition = nMultiPos + pHelpMulti->GetLen(); + std::optional<SwMultiCreator> pCreate = GetInfo().GetMultiCreator( nMultiPos, nullptr ); + + if ( !pCreate ) + { + OSL_ENSURE( !pHelpMulti->GetLen(), "Multiportion without attribute?" ); + if ( nMultiPos ) + --nMultiPos; + pCreate = GetInfo().GetMultiCreator( --nMultiPos, nullptr ); + } + + if (!pCreate) + return pRest; + + if( pRest || nMultiPos > nPosition || IsIncompleteRuby(*pHelpMulti)) + { + SwMultiPortion* pTmp; + if( pHelpMulti->IsDouble() ) + pTmp = new SwDoubleLinePortion( *pCreate, nMultiPos ); + else if( pHelpMulti->IsBidi() ) + pTmp = new SwBidiPortion( nMultiPos, pCreate->nLevel ); + else if (IsIncompleteRuby(*pHelpMulti) && pCreate->pAttr) + { + TextFrameIndex nRubyOffset = static_cast<const SwRubyPortion*>(pHelpMulti)->GetRubyOffset(); + pTmp = new SwRubyPortion( *pCreate, *GetInfo().GetFont(), + m_pFrame->GetDoc().getIDocumentSettingAccess(), + nMultiPos, nRubyOffset, + GetInfo() ); + } + else if( pHelpMulti->HasRotation() ) + pTmp = new SwRotatedPortion( nMultiPos, pHelpMulti->GetDirection() ); + else + { + return pRest; + } + pCreate.reset(); + pTmp->SetFollowField(); + if( pRest ) + { + SwLineLayout *pLay = &pTmp->GetRoot(); + if( pTmp->IsRuby() && pTmp->OnTop() ) + { + pLay->SetNext( new SwLineLayout() ); + pLay = pLay->GetNext(); + } + pLay->SetNextPortion( pRest ); + } + return pTmp; + } + return pRest; +} + +// SwTextCursorSave notes the start and current line of a SwTextCursor, +// sets them to the values for GetModelPositionForViewPoint inside a multiportion +// and restores them in the destructor. +SwTextCursorSave::SwTextCursorSave( SwTextCursor* pCursor, + SwMultiPortion* pMulti, + SwTwips nY, + SwTwips& nX, + TextFrameIndex const nCurrStart, + tools::Long nSpaceAdd ) + : pTextCursor(pCursor), + pCurr(pCursor->m_pCurr), + nStart(pCursor->m_nStart) +{ + pCursor->m_nStart = nCurrStart; + pCursor->m_pCurr = &pMulti->GetRoot(); + while( pCursor->Y() + pCursor->GetLineHeight() < nY && + pCursor->Next() ) + ; // nothing + nWidth = pCursor->m_pCurr->Width(); + nOldProp = pCursor->GetPropFont(); + + if ( pMulti->IsDouble() || pMulti->IsBidi() ) + { + bSpaceChg = pMulti->ChgSpaceAdd( pCursor->m_pCurr, nSpaceAdd ); + + TextFrameIndex nSpaceCnt; + if ( pMulti->IsDouble() ) + { + pCursor->SetPropFont( 50 ); + nSpaceCnt = static_cast<SwDoubleLinePortion*>(pMulti)->GetSpaceCnt(); + } + else + { + TextFrameIndex const nOldIdx = pCursor->GetInfo().GetIdx(); + pCursor->GetInfo().SetIdx ( nCurrStart ); + nSpaceCnt = static_cast<SwBidiPortion*>(pMulti)->GetSpaceCnt(pCursor->GetInfo()); + pCursor->GetInfo().SetIdx ( nOldIdx ); + } + + if( nSpaceAdd > 0 && !pMulti->HasTabulator() ) + pCursor->m_pCurr->Width( o3tl::narrowing<sal_uInt16>(nWidth + nSpaceAdd * sal_Int32(nSpaceCnt) / SPACING_PRECISION_FACTOR) ); + + // For a BidiPortion we have to calculate the offset from the + // end of the portion + if ( nX && pMulti->IsBidi() ) + nX = pCursor->m_pCurr->Width() - nX; + } + else + bSpaceChg = false; +} + +SwTextCursorSave::~SwTextCursorSave() +{ + if( bSpaceChg ) + SwDoubleLinePortion::ResetSpaceAdd( pTextCursor->m_pCurr ); + pTextCursor->m_pCurr->Width( nWidth ); + pTextCursor->m_pCurr = pCurr; + pTextCursor->m_nStart = nStart; + pTextCursor->SetPropFont( nOldProp ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/pormulti.hxx b/sw/source/core/text/pormulti.hxx new file mode 100644 index 0000000000..e5a3da2b32 --- /dev/null +++ b/sw/source/core/text/pormulti.hxx @@ -0,0 +1,256 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <memory> +#include "porlay.hxx" +#include <com/sun/star/text/RubyAdjust.hpp> + +class IDocumentSettingAccess; +class SwTextFormatInfo; +class SwTextCursor; +class SwTextPaintInfo; +class SwTextAttr; +class SfxPoolItem; +class SwFont; + +// SwMultiCreator is a small structure to create a multiportion. +// It contains the kind of multiportion and a textattribute +// or a poolitem. +// The GetMultiCreator-function fills this structure and +// the Ctor of the SwMultiPortion uses it. +enum class SwMultiCreatorId +{ + Double, Ruby, Rotate, Bidi +}; + +enum class RubyPosition : sal_uInt16 +{ + ABOVE = 0, + BELOW = 1, + RIGHT = 2 +}; + +struct SwMultiCreator +{ + TextFrameIndex nStartOfAttr; + const SwTextAttr* pAttr; + const SfxPoolItem* pItem; + SwMultiCreatorId nId; + sal_uInt8 nLevel; +}; + +// A two-line-portion (SwMultiPortion) could have surrounding brackets, +// in this case the structure SwBracket will be used. +struct SwBracket +{ + TextFrameIndex nStart; // Start of text attribute determines the font + sal_uInt16 nAscent; // Ascent of the brackets + sal_uInt16 nHeight; // Height of them + sal_uInt16 nPreWidth; // Width of the opening bracket + sal_uInt16 nPostWidth; // Width of the closing bracket + sal_Unicode cPre; // Initial character, e.g. '(' + sal_Unicode cPost; // Final character, e.g. ')' + SwFontScript nPreScript; // Script of the initial character + SwFontScript nPostScript; // Script of the final character +}; + +// The SwMultiPortion is line portion inside a line portion, +// it's a group of portions, +// e.g. a double line portion in a line +// or phonetics (ruby) +// or combined characters +// or a rotated portion. +class SAL_DLLPUBLIC_RTTI SwMultiPortion : public SwLinePortion +{ + SwLineLayout m_aRoot; // One or more lines + bool m_bTab1 :1; // First line tabulator + bool m_bTab2 :1; // Second line includes tabulator + bool m_bDouble :1; // Double line + bool m_bRuby :1; // Phonetics + bool m_bBidi :1; + bool m_bFormatted :1; // Already formatted + bool m_bFollowField :1; // Field follow inside + bool m_bFlyInContent:1; // Fly as character inside + RubyPosition m_eRubyPosition; // Phonetic position + sal_uInt8 m_nDirection:2; // Direction (0/90/180/270 degrees) +protected: + explicit SwMultiPortion(TextFrameIndex const nEnd) + : m_bTab1(false) + , m_bTab2(false) + , m_bDouble(false) + , m_bRuby(false) + , m_bBidi(false) + , m_bFormatted(false) + , m_bFollowField(false) + , m_bFlyInContent(false) + , m_eRubyPosition( RubyPosition::ABOVE ) + , m_nDirection(0) + { + SetWhichPor(PortionType::Multi); + SetLen(nEnd); + } + void SetDouble() { m_bDouble = true; } + void SetRuby() { m_bRuby = true; } + void SetBidi() { m_bBidi = true; } + void SetRubyPosition( RubyPosition eNew ) { m_eRubyPosition = eNew; } + void SetTab1( bool bNew ) { m_bTab1 = bNew; } + void SetTab2( bool bNew ) { m_bTab2 = bNew; } + void SetDirection( sal_uInt8 nNew ) { m_nDirection = nNew; } + bool GetTab1() const { return m_bTab1; } + bool GetTab2() const { return m_bTab2; } +public: + virtual ~SwMultiPortion() override; + const SwLineLayout& GetRoot() const { return m_aRoot; } + SwLineLayout& GetRoot() { return m_aRoot; } + + bool HasTabulator() const { return m_bTab1 || m_bTab2; } + bool IsFormatted() const { return m_bFormatted; } + void SetFormatted() { m_bFormatted = true; } + bool IsFollowField() const { return m_bFollowField; } + void SetFollowField() { m_bFollowField = true; } + bool HasFlyInContent() const { return m_bFlyInContent; } + void SetFlyInContent( bool bNew ) { m_bFlyInContent = bNew; } + bool IsDouble() const { return m_bDouble; } + bool IsRuby() const { return m_bRuby; } + bool IsBidi() const { return m_bBidi; } + bool OnTop() const { return m_eRubyPosition == RubyPosition::ABOVE; } + bool OnRight() const { return m_eRubyPosition == RubyPosition::RIGHT; } + RubyPosition GetRubyPosition() const { return m_eRubyPosition; } + void ActualizeTabulator(); + + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual SwTwips CalcSpacing( tools::Long nSpaceAdd, const SwTextSizeInfo &rInf ) const override; + virtual bool ChgSpaceAdd( SwLineLayout* pCurr, tools::Long nSpaceAdd ) const; + + // Summarize the internal lines to calculate the (external) size + void CalcSize( SwTextFormatter& rLine, SwTextFormatInfo &rInf ); + + inline bool HasBrackets() const; + bool HasRotation() const { return 0 != (1 & m_nDirection); } + bool IsRevers() const { return 0 != (2 & m_nDirection); } + sal_uInt8 GetDirection() const { return m_nDirection; } + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const override; +}; + +class SwDoubleLinePortion : public SwMultiPortion +{ + std::unique_ptr<SwBracket> m_pBracket; // Surrounding brackets + SwTwips m_nLineDiff; // Difference of the width of the both lines + TextFrameIndex m_nBlank1; ///< Number of blanks in the first line + TextFrameIndex m_nBlank2; ///< Number of blanks in the second line +public: + SwDoubleLinePortion(SwDoubleLinePortion& rDouble, TextFrameIndex nEnd); + SwDoubleLinePortion(const SwMultiCreator& rCreate, TextFrameIndex nEnd); + virtual ~SwDoubleLinePortion() override; + + SwBracket* GetBrackets() const { return m_pBracket.get(); } + void SetBrackets( const SwDoubleLinePortion& rDouble ); + void PaintBracket( SwTextPaintInfo& rInf, tools::Long nSpaceAdd, bool bOpen ) const; + void FormatBrackets( SwTextFormatInfo &rInf, SwTwips& nMaxWidth ); + sal_uInt16 PreWidth() const { return m_pBracket->nPreWidth; }; + sal_uInt16 PostWidth() const { return m_pBracket->nPostWidth; } + void ClearBrackets() + { m_pBracket->nPreWidth = m_pBracket->nPostWidth=0; Width( 0 ); } + sal_uInt16 BracketWidth(){ return PreWidth() + PostWidth(); } + + void CalcBlanks( SwTextFormatInfo &rInf ); + static void ResetSpaceAdd( SwLineLayout* pCurr ); + SwTwips GetLineDiff() const { return m_nLineDiff; } + TextFrameIndex GetSpaceCnt() const + { return ( m_nLineDiff < 0 ) ? m_nBlank2 : m_nBlank1; } + TextFrameIndex GetSmallerSpaceCnt() const + { return ( m_nLineDiff < 0 ) ? m_nBlank1 : m_nBlank2; } + + virtual SwTwips CalcSpacing( tools::Long nSpaceAdd, const SwTextSizeInfo &rInf ) const override; + virtual bool ChgSpaceAdd( SwLineLayout* pCurr, tools::Long nSpaceAdd ) const override; +}; + +class SwRubyPortion : public SwMultiPortion +{ + TextFrameIndex m_nRubyOffset; + css::text::RubyAdjust m_nAdjustment; + void Adjust_( SwTextFormatInfo &rInf); +public: + SwRubyPortion(const SwRubyPortion& rRuby, TextFrameIndex nEnd); + + SwRubyPortion( const SwMultiCreator& rCreate, const SwFont& rFnt, + const IDocumentSettingAccess& rIDocumentSettingAccess, + TextFrameIndex nEnd, TextFrameIndex nOffs, + const SwTextSizeInfo &rInf ); + + void CalcRubyOffset(); + void Adjust( SwTextFormatInfo &rInf ) + { if(m_nAdjustment != css::text::RubyAdjust_LEFT && GetRoot().GetNext()) Adjust_(rInf); } + css::text::RubyAdjust GetAdjustment() const { return m_nAdjustment; } + TextFrameIndex GetRubyOffset() const { return m_nRubyOffset; } +}; + +class SwRotatedPortion : public SwMultiPortion +{ +public: + SwRotatedPortion(TextFrameIndex const nEnd, sal_uInt8 nDir) + : SwMultiPortion( nEnd ) { SetDirection( nDir ); } + SwRotatedPortion( const SwMultiCreator& rCreate, TextFrameIndex nEnd, + bool bRTL ); +}; + +class SwBidiPortion : public SwMultiPortion +{ + sal_uInt8 m_nLevel; + +public: + SwBidiPortion(TextFrameIndex nEnd, sal_uInt8 nLv); + + sal_uInt8 GetLevel() const { return m_nLevel; } + // Get number of blanks for justified alignment + TextFrameIndex GetSpaceCnt(const SwTextSizeInfo &rInf) const; + // Calculates extra spacing based on number of blanks + virtual SwTwips CalcSpacing( tools::Long nSpaceAdd, const SwTextSizeInfo &rInf ) const override; + // Manipulate the spacing array at pCurr + virtual bool ChgSpaceAdd( SwLineLayout* pCurr, tools::Long nSpaceAdd ) const override; +}; + +// For cursor travelling in multiportions + +class SwTextCursorSave +{ + SwTextCursor* pTextCursor; + SwLineLayout* pCurr; + TextFrameIndex nStart; + sal_uInt16 nWidth; + sal_uInt8 nOldProp; + bool bSpaceChg; +public: + SwTextCursorSave( SwTextCursor* pTextCursor, SwMultiPortion* pMulti, + SwTwips nY, SwTwips& nX, TextFrameIndex nCurrStart, tools::Long nSpaceAdd); + ~SwTextCursorSave(); +}; + +inline bool SwMultiPortion::HasBrackets() const +{ + return IsDouble() && nullptr != static_cast<const SwDoubleLinePortion*>(this)->GetBrackets(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porref.cxx b/sw/source/core/text/porref.cxx new file mode 100644 index 0000000000..a64f2df1fe --- /dev/null +++ b/sw/source/core/text/porref.cxx @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <SwPortionHandler.hxx> +#include <viewopt.hxx> + +#include "porref.hxx" +#include "inftxt.hxx" + +void SwRefPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( Width() ) + { + rInf.DrawViewOpt( *this, PortionType::Ref ); + SwTextPortion::Paint( rInf ); + } +} + +SwLinePortion *SwIsoRefPortion::Compress() { return this; } + +SwIsoRefPortion::SwIsoRefPortion() : m_nViewWidth(0) +{ + SetLen(TextFrameIndex(1)); + SetWhichPor( PortionType::IsoRef ); +} + +sal_uInt16 SwIsoRefPortion::GetViewWidth( const SwTextSizeInfo &rInf ) const +{ + // Although we are const, nViewWidth should be calculated in the last + // moment possible + SwIsoRefPortion* pThis = const_cast<SwIsoRefPortion*>(this); + if( !Width() && rInf.OnWin() && rInf.GetOpt().IsFieldShadings() && + !rInf.GetOpt().IsReadonly() && !rInf.GetOpt().IsPagePreview() ) + { + if( !m_nViewWidth ) + pThis->m_nViewWidth = rInf.GetTextSize(OUString(' ')).Width(); + } + else + pThis->m_nViewWidth = 0; + return m_nViewWidth; +} + +bool SwIsoRefPortion::Format( SwTextFormatInfo &rInf ) +{ + return SwLinePortion::Format( rInf ); +} + +void SwIsoRefPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( Width() ) + rInf.DrawViewOpt( *this, PortionType::Ref ); +} + +void SwIsoRefPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Special( GetLen(), OUString(), GetWhichPor() ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porref.hxx b/sw/source/core/text/porref.hxx new file mode 100644 index 0000000000..d128030803 --- /dev/null +++ b/sw/source/core/text/porref.hxx @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include "portxt.hxx" + +class SwRefPortion : public SwTextPortion +{ +public: + SwRefPortion() { SetWhichPor(PortionType::Ref); } + virtual void Paint(const SwTextPaintInfo& rInf) const override; +}; + +class SwIsoRefPortion : public SwRefPortion +{ + sal_uInt16 m_nViewWidth; + +public: + SwIsoRefPortion(); + virtual bool Format(SwTextFormatInfo& rInf) override; + virtual void Paint(const SwTextPaintInfo& rInf) const override; + virtual SwLinePortion* Compress() override; + virtual sal_uInt16 GetViewWidth(const SwTextSizeInfo& rInf) const override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion(SwPortionHandler& rPH) const override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porrst.cxx b/sw/source/core/text/porrst.cxx new file mode 100644 index 0000000000..029adca753 --- /dev/null +++ b/sw/source/core/text/porrst.cxx @@ -0,0 +1,949 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <editeng/lspcitem.hxx> +#include <editeng/adjustitem.hxx> +#include <editeng/escapementitem.hxx> +#include <editeng/lrspitem.hxx> +#include <editeng/pgrditem.hxx> +#include <vcl/svapp.hxx> +#include <comphelper/scopeguard.hxx> + +#include <viewsh.hxx> +#include <viewopt.hxx> +#include <ndtxt.hxx> +#include <pagefrm.hxx> +#include <paratr.hxx> +#include <SwPortionHandler.hxx> +#include "porrst.hxx" +#include "inftxt.hxx" +#include "txtpaint.hxx" +#include <swfntcch.hxx> +#include <tgrditem.hxx> +#include <pagedesc.hxx> +#include <frmatr.hxx> +#include "redlnitr.hxx" +#include "atrhndl.hxx" +#include <rootfrm.hxx> +#include <formatlinebreak.hxx> +#include <txatbase.hxx> + +#include <IDocumentRedlineAccess.hxx> +#include <IDocumentSettingAccess.hxx> +#include <IDocumentDeviceAccess.hxx> + +#include <crsrsh.hxx> +#include <swtypes.hxx> +#include <strings.hrc> +#include <flyfrms.hxx> +#include <bodyfrm.hxx> + +SwTmpEndPortion::SwTmpEndPortion( const SwLinePortion &rPortion, + const FontLineStyle eUL, + const FontStrikeout eStrkout, + const Color& rCol ) : + m_eUnderline( eUL ), m_eStrikeout( eStrkout ), m_aColor( rCol ) +{ + Height( rPortion.Height() ); + SetAscent( rPortion.GetAscent() ); + SetWhichPor( PortionType::TempEnd ); +} + +void SwTmpEndPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if (!(rInf.OnWin() && rInf.GetOpt().IsParagraph())) + return; + + const SwFont* pOldFnt = rInf.GetFont(); + + SwFont aFont(*pOldFnt); + + // Paint strikeout/underline based on redline color and settings + // (with an extra pilcrow in the background, because there is + // no SetStrikeoutColor(), also SetUnderColor() doesn't work()). + if ( m_eUnderline != LINESTYLE_NONE || m_eStrikeout != STRIKEOUT_NONE ) + { + aFont.SetColor( m_aColor ); + aFont.SetUnderline( m_eUnderline ); + aFont.SetStrikeout( m_eStrikeout ); + + const_cast<SwTextPaintInfo&>(rInf).SetFont(&aFont); + + // draw the pilcrow with strikeout/underline in redline color + rInf.DrawText(CH_PAR, *this); + + } + + aFont.SetColor( NON_PRINTING_CHARACTER_COLOR ); + aFont.SetStrikeout( STRIKEOUT_NONE ); + aFont.SetUnderline( LINESTYLE_NONE ); + const_cast<SwTextPaintInfo&>(rInf).SetFont(&aFont); + + // draw the pilcrow + rInf.DrawText(CH_PAR, *this); + + const_cast<SwTextPaintInfo&>(rInf).SetFont(const_cast<SwFont*>(pOldFnt)); +} + +SwBreakPortion::SwBreakPortion( const SwLinePortion &rPortion, const SwTextAttr* pAttr ) + : SwLinePortion( rPortion ) +{ + mnLineLength = TextFrameIndex(1); + m_eRedline = RedlineType::None; + SetWhichPor( PortionType::Break ); + + m_eClear = SwLineBreakClear::NONE; + if (pAttr && pAttr->Which() == RES_TXTATR_LINEBREAK) + { + m_eClear = pAttr->GetLineBreak().GetValue(); + } + m_nTextHeight = 0; +} + +TextFrameIndex SwBreakPortion::GetModelPositionForViewPoint(const sal_uInt16) const +{ + return TextFrameIndex(0); +} + +sal_uInt16 SwBreakPortion::GetViewWidth( const SwTextSizeInfo & ) const +{ return 0; } + +SwLinePortion *SwBreakPortion::Compress() +{ return (GetNextPortion() && GetNextPortion()->InTextGrp() ? nullptr : this); } + +void SwBreakPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( !(rInf.OnWin() && rInf.GetOpt().IsLineBreak()) ) + return; + + // Reduce height to text height for the duration of the print, so the vertical height will look + // correct for the line break character, even for clearing breaks. + SwTwips nHeight = Height(); + SwTwips nVertPosOffset = (nHeight - m_nTextHeight) / 2; + auto pPortion = const_cast<SwBreakPortion*>(this); + pPortion->Height(m_nTextHeight, false); + if (rInf.GetTextFrame()->IsVertical()) + { + // Compensate for the offset done in SwTextCursor::AdjustBaseLine() for the vertical case. + const_cast<SwTextPaintInfo&>(rInf).Y(rInf.Y() + nVertPosOffset); + } + comphelper::ScopeGuard g( + [pPortion, nHeight, &rInf, nVertPosOffset] + { + if (rInf.GetTextFrame()->IsVertical()) + { + const_cast<SwTextPaintInfo&>(rInf).Y(rInf.Y() - nVertPosOffset); + } + pPortion->Height(nHeight, false); + }); + + rInf.DrawLineBreak( *this ); + + // paint redlining + if (m_eRedline == RedlineType::None) + return; + + sal_Int16 nNoBreakWidth = rInf.GetTextSize(S_NOBREAK_FOR_REDLINE).Width(); + if ( nNoBreakWidth > 0 ) + { + // approximate portion size with multiple no-break spaces + // and draw these spaces (at least a single one) by DrawText + // painting the requested redline underline/strikeout + sal_Int16 nSpaces = (LINE_BREAK_WIDTH + nNoBreakWidth/2) / nNoBreakWidth; + OUStringBuffer aBuf(S_NOBREAK_FOR_REDLINE); + for (sal_Int16 i = 1; i < nSpaces; ++i) + aBuf.append(S_NOBREAK_FOR_REDLINE); + + const SwFont* pOldFnt = rInf.GetFont(); + + SwFont aFont(*pOldFnt); + + if (m_eRedline == RedlineType::Delete) + aFont.SetUnderline( LINESTYLE_NONE ); + else + aFont.SetStrikeout( STRIKEOUT_NONE ); + + const_cast<SwTextPaintInfo&>(rInf).SetFont(&aFont); + + rInf.DrawText(aBuf.makeStringAndClear(), *this); + + const_cast<SwTextPaintInfo&>(rInf).SetFont(const_cast<SwFont*>(pOldFnt)); + } +} + +bool SwBreakPortion::Format( SwTextFormatInfo &rInf ) +{ + const SwLinePortion *pRoot = rInf.GetRoot(); + Width( 0 ); + Height( pRoot->Height() ); + m_nTextHeight = Height(); + + // See if this is a clearing break. If so, calculate how much we need to "jump down" so the next + // line can again use the full text width. + SwLineBreakClear eClear = m_eClear; + if (rInf.GetTextFrame()->IsRightToLeft() && eClear != SwLineBreakClear::ALL) + { + // RTL ignores left/right breaks. + eClear = SwLineBreakClear::NONE; + } + if (eClear != SwLineBreakClear::NONE) + { + SwTextFly& rTextFly = rInf.GetTextFly(); + if (rTextFly.IsOn()) + { + SwTwips nHeight = rTextFly.GetMaxBottom(*this, rInf) - rInf.Y(); + if (nHeight > Height()) + { + Height(nHeight, /*bText=*/false); + } + } + } + + SetAscent( pRoot->GetAscent() ); + if (rInf.GetIdx() + TextFrameIndex(1) == TextFrameIndex(rInf.GetText().getLength())) + rInf.SetNewLine( true ); + return true; +} + +void SwBreakPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Text( GetLen(), GetWhichPor() ); +} + +void SwBreakPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, TextFrameIndex& + nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwBreakPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("text-height"), + BAD_CAST(OString::number(m_nTextHeight).getStr())); + + (void)xmlTextWriterEndElement(pWriter); +} + +SwLineBreakClear SwBreakPortion::GetClear() const { return m_eClear; } + +SwKernPortion::SwKernPortion( SwLinePortion &rPortion, short nKrn, + bool bBG, bool bGK ) : + m_nKern( nKrn ), m_bBackground( bBG ), m_bGridKern( bGK ) +{ + Height( rPortion.Height() ); + SetAscent( rPortion.GetAscent() ); + mnLineLength = TextFrameIndex(0); + SetWhichPor( PortionType::Kern ); + if( m_nKern > 0 ) + Width( m_nKern ); + rPortion.Insert( this ); +} + +SwKernPortion::SwKernPortion( const SwLinePortion& rPortion ) : + m_nKern( 0 ), m_bBackground( false ), m_bGridKern( true ) +{ + Height( rPortion.Height() ); + SetAscent( rPortion.GetAscent() ); + + mnLineLength = TextFrameIndex(0); + SetWhichPor( PortionType::Kern ); +} + +void SwKernPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( !Width() ) + return; + + // bBackground is set for Kerning Portions between two fields + if ( m_bBackground ) + rInf.DrawViewOpt( *this, PortionType::Field ); + + rInf.DrawBackBrush( *this ); + if (GetJoinBorderWithNext() ||GetJoinBorderWithPrev()) + rInf.DrawBorder( *this ); + + // do we have to repaint a post it portion? + if( rInf.OnWin() && mpNextPortion && !mpNextPortion->Width() ) + mpNextPortion->PrePaint( rInf, this ); + + if( rInf.GetFont()->IsPaintBlank() ) + { + SwRect aClipRect; + rInf.CalcRect( *this, &aClipRect ); + SwSaveClip aClip( const_cast<OutputDevice*>(rInf.GetOut()) ); + aClip.ChgClip( aClipRect ); + rInf.DrawText(" ", *this, TextFrameIndex(0), TextFrameIndex(2), true ); + } +} + +void SwKernPortion::FormatEOL( SwTextFormatInfo &rInf ) +{ + if ( m_bGridKern ) + return; + + if( rInf.GetLast() == this ) + rInf.SetLast( FindPrevPortion( rInf.GetRoot() ) ); + if( m_nKern < 0 ) + Width( -m_nKern ); + else + Width( 0 ); + rInf.GetLast()->FormatEOL( rInf ); +} + +SwArrowPortion::SwArrowPortion( const SwLinePortion &rPortion ) : + m_bLeft( true ) +{ + Height( rPortion.Height() ); + SetAscent( rPortion.GetAscent() ); + mnLineLength = TextFrameIndex(0); + SetWhichPor( PortionType::Arrow ); +} + +SwArrowPortion::SwArrowPortion( const SwTextPaintInfo &rInf ) + : m_bLeft( false ) +{ + Height( o3tl::narrowing<sal_uInt16>(rInf.GetTextFrame()->getFramePrintArea().Height()) ); + m_aPos.setX( rInf.GetTextFrame()->getFrameArea().Left() + + rInf.GetTextFrame()->getFramePrintArea().Right() ); + m_aPos.setY( rInf.GetTextFrame()->getFrameArea().Top() + + rInf.GetTextFrame()->getFramePrintArea().Bottom() ); + SetWhichPor( PortionType::Arrow ); +} + +void SwArrowPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + const_cast<SwArrowPortion*>(this)->m_aPos = rInf.GetPos(); +} + +SwLinePortion *SwArrowPortion::Compress() { return this; } + +SwTwips SwTextFrame::EmptyHeight() const +{ + if (IsCollapse()) { + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + if ( auto pCrSh = dynamic_cast<SwCursorShell*>( pSh ) ) { + // this is called during formatting so avoid recursive layout + SwContentFrame const*const pCurrFrame = pCrSh->GetCurrFrame(false); + if (pCurrFrame==static_cast<SwContentFrame const *>(this)) { + // do nothing + } else { + return 1; + } + } else { + return 1; + } + } + OSL_ENSURE( ! IsVertical() || ! IsSwapped(),"SwTextFrame::EmptyHeight with swapped frame" ); + + std::unique_ptr<SwFont> pFnt; + const SwTextNode& rTextNode = *GetTextNodeForParaProps(); + const IDocumentSettingAccess* pIDSA = rTextNode.getIDocumentSettingAccess(); + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + if ( rTextNode.HasSwAttrSet() ) + { + const SwAttrSet *pAttrSet = &( rTextNode.GetSwAttrSet() ); + pFnt.reset(new SwFont( pAttrSet, pIDSA )); + } + else + { + SwFontAccess aFontAccess( &rTextNode.GetAnyFormatColl(), pSh); + pFnt.reset(new SwFont( aFontAccess.Get()->GetFont() )); + pFnt->CheckFontCacheId( pSh, pFnt->GetActual() ); + } + + if ( IsVertical() ) + pFnt->SetVertical( 2700_deg10 ); + + OutputDevice* pOut = pSh ? pSh->GetOut() : nullptr; + if ( !pOut || !pSh->GetViewOptions()->getBrowseMode() || + pSh->GetViewOptions()->IsPrtFormat() ) + { + pOut = rTextNode.getIDocumentDeviceAccess().getReferenceDevice(true); + } + + const IDocumentRedlineAccess& rIDRA = rTextNode.getIDocumentRedlineAccess(); + if (IDocumentRedlineAccess::IsShowChanges(rIDRA.GetRedlineFlags()) + && !getRootFrame()->IsHideRedlines()) + { + const SwRedlineTable::size_type nRedlPos = rIDRA.GetRedlinePos( rTextNode, RedlineType::Any ); + if( SwRedlineTable::npos != nRedlPos ) + { + SwAttrHandler aAttrHandler; + aAttrHandler.Init(rTextNode.GetSwAttrSet(), + *rTextNode.getIDocumentSettingAccess()); + SwRedlineItr aRedln( rTextNode, *pFnt, aAttrHandler, + nRedlPos, SwRedlineItr::Mode::Show); + } + } + + SwTwips nRet; + if( !pOut ) + nRet = IsVertical() ? + getFramePrintArea().SSize().Width() + 1 : + getFramePrintArea().SSize().Height() + 1; + else + { + pFnt->SetFntChg( true ); + pFnt->ChgPhysFnt( pSh, *pOut ); + nRet = pFnt->GetHeight( pSh, *pOut ); + } + return nRet; +} + +bool SwTextFrame::FormatEmpty() +{ + OSL_ENSURE( ! IsVertical() || ! IsSwapped(),"SwTextFrame::FormatEmpty with swapped frame" ); + + bool bCollapse = EmptyHeight( ) == 1 && IsCollapse( ); + + // sw_redlinehide: just disable FormatEmpty optimisation for now + // Split fly frames: non-last parts of the anchor want this optimization to clear the old + // content. + SwFlyAtContentFrame* pNonLastSplitFlyDrawObj = HasNonLastSplitFlyDrawObj(); + bool bHasNonLastSplitFlyDrawObj = pNonLastSplitFlyDrawObj != nullptr; + + if (pNonLastSplitFlyDrawObj && pNonLastSplitFlyDrawObj->IsWrapOnAllPages()) + { + // Split fly: the anchor is non-empty on all pages in the "wrap on all pages" case. + bHasNonLastSplitFlyDrawObj = false; + } + + if ((HasFollow() && !bHasNonLastSplitFlyDrawObj) || GetMergedPara() || (GetTextNodeFirst()->GetpSwpHints() && !bHasNonLastSplitFlyDrawObj) || + nullptr != GetTextNodeForParaProps()->GetNumRule() || + GetTextNodeFirst()->HasHiddenCharAttribute(true) || + IsInFootnote() || ( HasPara() && GetPara()->IsPrepMustFit() ) ) + return false; + const SwAttrSet& aSet = GetTextNodeForParaProps()->GetSwAttrSet(); + const SvxAdjust nAdjust = aSet.GetAdjust().GetAdjust(); + if( !bCollapse && ( ( ( ! IsRightToLeft() && ( SvxAdjust::Left != nAdjust ) ) || + ( IsRightToLeft() && ( SvxAdjust::Right != nAdjust ) ) ) || + aSet.GetRegister().GetValue() ) ) + return false; + const SvxLineSpacingItem &rSpacing = aSet.GetLineSpacing(); + if( !bCollapse && ( SvxLineSpaceRule::Min == rSpacing.GetLineSpaceRule() || + SvxLineSpaceRule::Fix == rSpacing.GetLineSpaceRule() || + aSet.GetFirstLineIndent().IsAutoFirst())) + { + return false; + } + + SwTextFly aTextFly( this ); + SwRect aRect; + bool bFirstFlyCheck = 0 != getFramePrintArea().Height(); + if ( !bCollapse && bFirstFlyCheck && + aTextFly.IsOn() && aTextFly.IsAnyObj( aRect ) && !bHasNonLastSplitFlyDrawObj ) + return false; + + if (IsEmptyWithSplitFly()) + { + // We don't want this optimization in case the paragraph is not really empty, because it has + // a fly frame and it also needs space for the empty paragraph in a next line. + return false; + } + + // only need to check one node because of early return on GetMerged() + for (SwContentIndex const* pIndex = GetTextNodeFirst()->GetFirstIndex(); + pIndex; pIndex = pIndex->GetNext()) + { + sw::mark::IMark const*const pMark = pIndex->GetMark(); + if (dynamic_cast<const sw::mark::IBookmark*>(pMark) != nullptr) + { // need bookmark portions! + return false; + } + } + + SwTwips nHeight = EmptyHeight(); + + if (aSet.GetParaGrid().GetValue() && + IsInDocBody() ) + { + SwTextGridItem const*const pGrid(GetGridItem(FindPageFrame())); + if ( pGrid ) + nHeight = pGrid->GetBaseHeight() + pGrid->GetRubyHeight(); + } + + SwRectFnSet aRectFnSet(this); + SwTwips nChg = nHeight - aRectFnSet.GetHeight(getFramePrintArea()); + const SwBodyFrame* pBody = FindBodyFrame(); + if (pNonLastSplitFlyDrawObj && pBody) + { + // See if we need to increase the text frame height due to split flys. This is necessary for + // anchors of inner floating tables, where moving to a next page moves indirectly, so we + // want a correct text frame height. + SwTwips nFrameBottom = aRectFnSet.GetBottom(getFrameArea()) + nChg; + SwTwips nFlyBottom = aRectFnSet.GetBottom(pNonLastSplitFlyDrawObj->getFrameArea()); + SwTwips nBodyBottom = aRectFnSet.GetBottom(pBody->getFrameArea()); + if (nFlyBottom > nBodyBottom) + { + // This is the legacy case where flys may overlap with footer frames. + nFlyBottom = nBodyBottom; + } + if (pNonLastSplitFlyDrawObj->isFrameAreaPositionValid() && nFlyBottom > nFrameBottom) + { + nChg += (nFlyBottom - nFrameBottom); + } + } + + if( !nChg ) + SetUndersized( false ); + AdjustFrame( nChg ); + + if (GetHasRotatedPortions()) + { + ClearPara(); + SetHasRotatedPortions(false); + } + + RemoveFromCache(); + if( !IsEmpty() ) + { + SetEmpty( true ); + SetCompletePaint(); + } + if( !bCollapse && !bFirstFlyCheck && + aTextFly.IsOn() && aTextFly.IsAnyObj( aRect ) ) + return false; + + // #i35635# - call method <HideAndShowObjects()> + // to assure that objects anchored at the empty paragraph are + // correctly visible resp. invisible. + HideAndShowObjects(); + return true; +} + +bool SwTextFrame::FillRegister( SwTwips& rRegStart, sal_uInt16& rRegDiff ) +{ + const SwFrame *pFrame = this; + rRegDiff = 0; + while( !( ( SwFrameType::Body | SwFrameType::Fly ) + & pFrame->GetType() ) && pFrame->GetUpper() ) + pFrame = pFrame->GetUpper(); + if( ( SwFrameType::Body| SwFrameType::Fly ) & pFrame->GetType() ) + { + SwRectFnSet aRectFnSet(pFrame); + rRegStart = aRectFnSet.GetPrtTop(*pFrame); + pFrame = pFrame->FindPageFrame(); + if( pFrame->IsPageFrame() ) + { + SwPageDesc* pDesc = const_cast<SwPageFrame*>(static_cast<const SwPageFrame*>(pFrame))->FindPageDesc(); + if( pDesc ) + { + rRegDiff = pDesc->GetRegHeight(); + if( !rRegDiff ) + { + const SwTextFormatColl *pFormat = pDesc->GetRegisterFormatColl(); + if( pFormat ) + { + const SvxLineSpacingItem &rSpace = pFormat->GetLineSpacing(); + if( SvxLineSpaceRule::Fix == rSpace.GetLineSpaceRule() ) + { + rRegDiff = rSpace.GetLineHeight(); + pDesc->SetRegHeight( rRegDiff ); + pDesc->SetRegAscent( ( 4 * rRegDiff ) / 5 ); + } + else + { + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + SwFontAccess aFontAccess( pFormat, pSh ); + SwFont aFnt( aFontAccess.Get()->GetFont() ); + + OutputDevice *pOut = nullptr; + if( !pSh || !pSh->GetViewOptions()->getBrowseMode() || + pSh->GetViewOptions()->IsPrtFormat() ) + pOut = GetDoc().getIDocumentDeviceAccess().getReferenceDevice( true ); + + if( pSh && !pOut ) + pOut = pSh->GetWin()->GetOutDev(); + + if( !pOut ) + pOut = Application::GetDefaultDevice(); + + MapMode aOldMap( pOut->GetMapMode() ); + pOut->SetMapMode( MapMode( MapUnit::MapTwip ) ); + + aFnt.ChgFnt( pSh, *pOut ); + rRegDiff = aFnt.GetHeight( pSh, *pOut ); + sal_uInt16 nNetHeight = rRegDiff; + + switch( rSpace.GetLineSpaceRule() ) + { + case SvxLineSpaceRule::Auto: + break; + case SvxLineSpaceRule::Min: + { + if( rRegDiff < rSpace.GetLineHeight() ) + rRegDiff = rSpace.GetLineHeight(); + break; + } + default: + OSL_FAIL( ": unknown LineSpaceRule" ); + } + switch( rSpace.GetInterLineSpaceRule() ) + { + case SvxInterLineSpaceRule::Off: + break; + case SvxInterLineSpaceRule::Prop: + { + tools::Long nTmp = rSpace.GetPropLineSpace(); + if( nTmp < 50 ) + nTmp = nTmp ? 50 : 100; + nTmp *= rRegDiff; + nTmp /= 100; + if( !nTmp ) + ++nTmp; + rRegDiff = o3tl::narrowing<sal_uInt16>(nTmp); + nNetHeight = rRegDiff; + break; + } + case SvxInterLineSpaceRule::Fix: + { + rRegDiff = rRegDiff + rSpace.GetInterLineSpace(); + nNetHeight = rRegDiff; + break; + } + default: OSL_FAIL( ": unknown InterLineSpaceRule" ); + } + pDesc->SetRegHeight( rRegDiff ); + pDesc->SetRegAscent( rRegDiff - nNetHeight + + aFnt.GetAscent( pSh, *pOut ) ); + pOut->SetMapMode( aOldMap ); + } + } + } + const tools::Long nTmpDiff = pDesc->GetRegAscent() - rRegDiff; + if ( aRectFnSet.IsVert() ) + rRegStart -= nTmpDiff; + else + rRegStart += nTmpDiff; + } + } + } + return ( 0 != rRegDiff ); +} + +void SwHiddenTextPortion::Paint( const SwTextPaintInfo & rInf) const +{ +#ifdef DBG_UTIL + OutputDevice* pOut = const_cast<OutputDevice*>(rInf.GetOut()); + Color aCol( rInf.GetOpt().GetFieldShadingsColor() ); + Color aOldColor( pOut->GetFillColor() ); + pOut->SetFillColor( aCol ); + Point aPos( rInf.GetPos() ); + aPos.AdjustY( -150 ); + aPos.AdjustX( -25 ); + SwRect aRect( aPos, Size( 100, 200 ) ); + pOut->DrawRect( aRect.SVRect() ); + pOut->SetFillColor( aOldColor ); +#else + (void)rInf; +#endif +} + +bool SwHiddenTextPortion::Format( SwTextFormatInfo &rInf ) +{ + Width( 0 ); + rInf.GetTextFrame()->HideFootnotes( rInf.GetIdx(), rInf.GetIdx() + GetLen() ); + + return false; +}; + +bool SwControlCharPortion::DoPaint(SwTextPaintInfo const& rTextPaintInfo, + OUString & rOutString, SwFont & rTmpFont, int &) const +{ + if (mcChar == CHAR_WJ || !rTextPaintInfo.GetOpt().IsFieldShadings()) + { + return false; + } + + switch (mcChar) + { + case CHAR_ZWSP: + rOutString = "/"; break; +// case CHAR_LRM : +// rText = sal_Unicode(0x2514); break; +// case CHAR_RLM : +// rText = sal_Unicode(0x2518); break; + default: + assert(false); + break; + } + + rTmpFont.SetEscapement( CHAR_ZWSP == mcChar ? DFLT_ESC_AUTO_SUB : -25 ); + const sal_uInt16 nProp = 40; + rTmpFont.SetProportion( nProp ); // a smaller font + + return true; +} + +bool SwBookmarkPortion::DoPaint(SwTextPaintInfo const& rTextPaintInfo, + OUString & rOutString, SwFont & rFont, int & rDeltaY) const +{ + // custom color is visible without field shading, too + if (!rTextPaintInfo.GetOpt().IsShowBookmarks()) + { + return false; + } + + rOutString = OUStringChar(mcChar); + + // init font: we want OpenSymbol to ensure it doesn't look too crazy; + // thin and a bit higher than the surrounding text + auto const nOrigAscent(rFont.GetAscent(rTextPaintInfo.GetVsh(), *rTextPaintInfo.GetOut())); + rFont.SetName("OpenSymbol", rFont.GetActual()); + Size aSize(rFont.GetSize(rFont.GetActual())); + // use also the external leading (line gap) of the portion, but don't use + // 100% of it because i can't figure out how to baseline align that + assert(aSize.Height() != 0); + auto const nFactor = aSize.Height() > 0 ? (Height() * 95) / aSize.Height() : Height(); + rFont.SetProportion(nFactor); + rFont.SetWeight(WEIGHT_THIN, rFont.GetActual()); + rFont.SetColor(rTextPaintInfo.GetOpt().GetFieldShadingsColor()); + // reset these to default... + rFont.SetAlign(ALIGN_BASELINE); + rFont.SetUnderline(LINESTYLE_NONE); + rFont.SetOverline(LINESTYLE_NONE); + rFont.SetStrikeout(STRIKEOUT_NONE); + rFont.SetOutline(false); + rFont.SetShadow(false); + rFont.SetTransparent(false); + rFont.SetEmphasisMark(FontEmphasisMark::NONE); + rFont.SetEscapement(0); + rFont.SetPitch(PITCH_DONTKNOW, rFont.GetActual()); + rFont.SetRelief(FontRelief::NONE); + + // adjust Y position to account for different baselines of the fonts + auto const nOSAscent(rFont.GetAscent(rTextPaintInfo.GetVsh(), *rTextPaintInfo.GetOut())); + rDeltaY = nOSAscent - nOrigAscent; + + return true; +} + +void SwControlCharPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if ( !Width() ) // is only set during prepaint mode + return; + + rInf.DrawViewOpt(*this, GetWhichPor()); + + int deltaY(0); + SwFont aTmpFont( *rInf.GetFont() ); + OUString aOutString; + + if (!(rInf.OnWin() + && !rInf.GetOpt().IsPagePreview() + && !rInf.GetOpt().IsReadonly() + && DoPaint(rInf, aOutString, aTmpFont, deltaY))) + return; + + SwFontSave aFontSave( rInf, &aTmpFont ); + + if ( !mnHalfCharWidth ) + mnHalfCharWidth = rInf.GetTextSize( aOutString ).Width() / 2; + + Point aOldPos = rInf.GetPos(); + Point aNewPos( aOldPos ); + auto const deltaX((Width() / 2) - mnHalfCharWidth); + switch (rInf.GetFont()->GetOrientation(rInf.GetTextFrame()->IsVertical()).get()) + { + case 0: + aNewPos.AdjustX(deltaX); + aNewPos.AdjustY(deltaY); + break; + case 900: + aNewPos.AdjustY(-deltaX); + aNewPos.AdjustX(deltaY); + break; + case 2700: + aNewPos.AdjustY(deltaX); + aNewPos.AdjustX(-deltaY); + break; + default: + assert(false); + break; + } + const_cast< SwTextPaintInfo& >( rInf ).SetPos( aNewPos ); + + rInf.DrawText( aOutString, *this ); + + const_cast< SwTextPaintInfo& >( rInf ).SetPos( aOldPos ); +} + +void SwBookmarkPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if ( !Width() ) // is only set during prepaint mode + return; + + rInf.DrawViewOpt(*this, GetWhichPor()); + + int deltaY(0); + SwFont aTmpFont( *rInf.GetFont() ); + OUString aOutString; + + if (!(rInf.OnWin() + && !rInf.GetOpt().IsPagePreview() + && !rInf.GetOpt().IsReadonly() + && DoPaint(rInf, aOutString, aTmpFont, deltaY))) + return; + + SwFontSave aFontSave( rInf, &aTmpFont ); + + if ( !mnHalfCharWidth ) + mnHalfCharWidth = rInf.GetTextSize( aOutString ).Width() / 2; + + Point aOldPos = rInf.GetPos(); + Point aNewPos( aOldPos ); + auto const deltaX((Width() / 2) - mnHalfCharWidth); + switch (rInf.GetFont()->GetOrientation(rInf.GetTextFrame()->IsVertical()).get()) + { + case 0: + aNewPos.AdjustX(deltaX); + aNewPos.AdjustY(deltaY); + break; + case 900: + aNewPos.AdjustY(-deltaX); + aNewPos.AdjustX(deltaY); + break; + case 2700: + aNewPos.AdjustY(deltaX); + aNewPos.AdjustX(-deltaY); + break; + default: + assert(false); + break; + } + + // draw end marks before the character position + if ( m_nStart == 0 || m_nEnd == 0 ) + { + // single type boundary marks are there outside of the bookmark text + // some |text| here + // [[ ]] + if (m_nStart > 1) + aNewPos.AdjustX(static_cast<tools::Long>(mnHalfCharWidth) * -2 * (m_oColors.size() - 1)); + } + else if ( m_nStart != 0 && m_nEnd != 0 ) + // both end and start boundary marks: adjust them around the bookmark position + // |te|xt| + // ]] [[ + aNewPos.AdjustX(static_cast<tools::Long>(mnHalfCharWidth) * -(2 * m_nEnd - 1 + m_nPoint) ); + + const_cast< SwTextPaintInfo& >( rInf ).SetPos( aNewPos ); + + for ( const auto& it : m_oColors ) + { + // set bold for custom colored bookmark symbol + // and draw multiple symbols showing all custom colors + aTmpFont.SetWeight( COL_TRANSPARENT == std::get<1>(it) ? WEIGHT_THIN : WEIGHT_BOLD, aTmpFont.GetActual() ); + aTmpFont.SetColor( COL_TRANSPARENT == std::get<1>(it) ? rInf.GetOpt().GetFieldShadingsColor() : std::get<1>(it) ); + aOutString = OUString(std::get<0>(it) == SwScriptInfo::MarkKind::Start ? '[' : ']'); + + // MarkKind::Point: drawn I-beam (e.g. U+2336) as overlapping ][ + if ( std::get<0>(it) == SwScriptInfo::MarkKind::Point ) + { + aNewPos.AdjustX(-mnHalfCharWidth * 5/16); + const_cast< SwTextPaintInfo& >( rInf ).SetPos( aNewPos ); + rInf.DrawText( aOutString, *this ); + + // when the overlapping vertical lines are 50 pixel width on the screen, + // this distance (half width * 5/8) still results precise overlapping + aNewPos.AdjustX(mnHalfCharWidth * 5/8); + const_cast< SwTextPaintInfo& >( rInf ).SetPos( aNewPos ); + aOutString = OUString('['); + } + rInf.DrawText( aOutString, *this ); + // place the next symbol after the previous one + // TODO: fix orientation and start/end + aNewPos.AdjustX(mnHalfCharWidth * 2); + const_cast< SwTextPaintInfo& >( rInf ).SetPos( aNewPos ); + } + + const_cast< SwTextPaintInfo& >( rInf ).SetPos( aOldPos ); +} + +void SwBookmarkPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + OUStringBuffer aStr; + for ( const auto& it : m_oColors ) + { + aStr.append("#" + std::get<2>(it) + " " + SwResId(STR_BOOKMARK_DEF_NAME)); + switch (std::get<0>(it)) + { + case SwScriptInfo::MarkKind::Point: + break; + case SwScriptInfo::MarkKind::Start: + aStr.append(" " + SwResId(STR_CAPTION_BEGINNING)); + break; + case SwScriptInfo::MarkKind::End: + aStr.append(" " + SwResId(STR_CAPTION_END)); + break; + } + } + + rPH.Special( GetLen(), aStr.makeStringAndClear(), GetWhichPor() ); +} + +void SwBookmarkPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwBookmarkPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + if (!m_oColors.empty()) + { + OUStringBuffer aStr; + for (const auto& rColor : m_oColors) + { + aStr.append("#" + std::get<2>(rColor) + " " + SwResId(STR_BOOKMARK_DEF_NAME)); + switch (std::get<0>(rColor)) + { + case SwScriptInfo::MarkKind::Point: + break; + case SwScriptInfo::MarkKind::Start: + aStr.append(" " + SwResId(STR_CAPTION_BEGINNING)); + break; + case SwScriptInfo::MarkKind::End: + aStr.append(" " + SwResId(STR_CAPTION_END)); + break; + } + } + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("colors"), + BAD_CAST(aStr.makeStringAndClear().toUtf8().getStr())); + } + + (void)xmlTextWriterEndElement(pWriter); +} + +bool SwControlCharPortion::Format( SwTextFormatInfo &rInf ) +{ + const SwLinePortion* pRoot = rInf.GetRoot(); + Width( 0 ); + Height( pRoot->Height() ); + SetAscent( pRoot->GetAscent() ); + + return false; +} + +sal_uInt16 SwControlCharPortion::GetViewWidth( const SwTextSizeInfo& rInf ) const +{ + if( !mnViewWidth ) + mnViewWidth = rInf.GetTextSize(OUString(' ')).Width(); + + return mnViewWidth; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/porrst.hxx b/sw/source/core/text/porrst.hxx new file mode 100644 index 0000000000..c5e57c688a --- /dev/null +++ b/sw/source/core/text/porrst.hxx @@ -0,0 +1,222 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <tools/gen.hxx> + +#include <TextFrameIndex.hxx> +#include <txttypes.hxx> +#include <txtfrm.hxx> +#include <svx/ctredlin.hxx> +#include <scriptinfo.hxx> + +#include "porlin.hxx" +#include "portxt.hxx" +#include "possiz.hxx" + +class SwPortionHandler; +class SwTextPaintInfo; +class SwTextSizeInfo; +class SwFont; + +#define LINE_BREAK_WIDTH 150 +#define SPECIAL_FONT_HEIGHT 200 + +class SwTextFormatInfo; + +class SwTmpEndPortion : public SwLinePortion +{ + const FontLineStyle m_eUnderline; + const FontStrikeout m_eStrikeout; + Color m_aColor; + +public: + explicit SwTmpEndPortion( const SwLinePortion &rPortion, + const FontLineStyle eUnderline, + const FontStrikeout eStrikeout, + const Color& rColor ); + virtual void Paint( const SwTextPaintInfo &rInf ) const override; +}; + +enum class SwLineBreakClear; + +class SwBreakPortion : public SwLinePortion +{ + RedlineType m_eRedline; + + /// Tracks the type of the breaking clear from SwTextLineBreak, if there is one. + SwLineBreakClear m_eClear; + + /// Height of the line-break character itself, without spacing added for clearing. + SwTwips m_nTextHeight; + +public: + explicit SwBreakPortion(const SwLinePortion& rPortion, const SwTextAttr* pAttr); + // Returns 0 if we have no usable data + virtual SwLinePortion *Compress() override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual sal_uInt16 GetViewWidth( const SwTextSizeInfo &rInf ) const override; + virtual TextFrameIndex GetModelPositionForViewPoint(sal_uInt16 nOfst) const override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const override; + + static constexpr OUString S_NOBREAK_FOR_REDLINE = u"\u00A0"_ustr; + void SetRedline( const RedlineType eRedline ) { m_eRedline = eRedline; } + + SwLineBreakClear GetClear() const; +}; + +class SwKernPortion : public SwLinePortion +{ + short m_nKern; + bool m_bBackground; + bool m_bGridKern; + +public: + + // This constructor automatically appends the portion to rPortion + // bBG indicates, that the background of the kerning portion has to + // be painted, e.g., if the portion if positioned between to fields. + // bGridKern indicates, that the kerning portion is used to provide + // additional space in grid mode. + SwKernPortion( SwLinePortion &rPortion, short nKrn, + bool bBG = false, bool bGridKern = false ); + + // This constructor only sets the height and ascent to the values + // of rPortion. It is only used for kerning portions for grid mode + explicit SwKernPortion( const SwLinePortion &rPortion ); + + virtual void FormatEOL( SwTextFormatInfo &rInf ) override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; +}; + +/// Indicator that the content does not fit into a fixed height frame (red triangle on the UI). +class SwArrowPortion : public SwLinePortion +{ + Point m_aPos; + bool m_bLeft; +public: + explicit SwArrowPortion( const SwLinePortion &rPortion ); + explicit SwArrowPortion( const SwTextPaintInfo &rInf ); + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual SwLinePortion *Compress() override; + bool IsLeft() const { return m_bLeft; } + const Point& GetPos() const { return m_aPos; } +}; + +// The characters which are forbidden at the start of a line like the dot and +// other punctuation marks are allowed to display in the margin of the page +// by a user option. +// The SwHangingPortion is the corresponding textportion to do that. +class SwHangingPortion : public SwTextPortion +{ + sal_uInt16 m_nInnerWidth; +public: + explicit SwHangingPortion( SwPosSize aSize ) : m_nInnerWidth( aSize.Width() ) + { + SetWhichPor( PortionType::Hanging ); + SetLen(TextFrameIndex(1)); + Height( aSize.Height() ); + } + + sal_uInt16 GetInnerWidth() const { return m_nInnerWidth; } +}; + +// Used to hide text +class SwHiddenTextPortion : public SwLinePortion +{ +public: + explicit SwHiddenTextPortion(TextFrameIndex const nLen) + { + SetWhichPor( PortionType::HiddenText ); SetLen( nLen ); + } + + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; +}; + +class SwControlCharPortion : public SwLinePortion +{ + +private: + mutable sal_uInt16 mnViewWidth; // used to cache a calculated value +protected: + mutable sal_uInt16 mnHalfCharWidth; // used to cache a calculated value + sal_Unicode mcChar; + +public: + + explicit SwControlCharPortion( sal_Unicode cChar ) + : mnViewWidth( 0 ), mnHalfCharWidth( 0 ), mcChar( cChar ) + { + SetWhichPor( PortionType::ControlChar ); SetLen( TextFrameIndex(1) ); + } + + virtual bool DoPaint(SwTextPaintInfo const& rInf, + OUString & rOutString, SwFont & rTmpFont, int & rDeltaY) const; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual sal_uInt16 GetViewWidth( const SwTextSizeInfo& rInf ) const override; +}; + +/// for showing bookmark starts and ends; note that in contrast to +/// SwControlCharPortion these do not have a character in the text. +class SwBookmarkPortion : public SwControlCharPortion +{ + // custom colors defined by metadata + std::vector<std::tuple<SwScriptInfo::MarkKind, Color, OUString>> m_oColors; + // number of MarkKind marks + sal_Int16 m_nStart, m_nEnd, m_nPoint; + bool m_bHasCustomColor; + +public: + explicit SwBookmarkPortion(sal_Unicode const cChar, std::vector<std::tuple<SwScriptInfo::MarkKind, Color, OUString>>rColors) + : SwControlCharPortion(cChar), m_oColors(rColors), m_nStart(0), m_nEnd(0), m_nPoint(0), m_bHasCustomColor(false) + { + SetWhichPor(PortionType::Bookmark); + SetLen(TextFrameIndex(0)); + for (const auto& it : m_oColors) + { + if (std::get<0>(it) == SwScriptInfo::MarkKind::Start) + m_nStart++; + else if (std::get<0>(it) == SwScriptInfo::MarkKind::End) + m_nEnd++; + else + m_nPoint++; + + if (!m_bHasCustomColor && COL_TRANSPARENT != std::get<1>(it)) + m_bHasCustomColor = true; + } + } + + virtual bool DoPaint(SwTextPaintInfo const& rInf, + OUString & rOutString, SwFont & rTmpFont, int & rDeltaY) const override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual SwLinePortion * Compress() override { return this; } + virtual void HandlePortion(SwPortionHandler& rPH) const override; + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& rOffset) const override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/portab.hxx b/sw/source/core/text/portab.hxx new file mode 100644 index 0000000000..9ffe7be506 --- /dev/null +++ b/sw/source/core/text/portab.hxx @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include "porglue.hxx" + +class SwTabPortion : public SwFixPortion +{ + const sal_uInt16 m_nTabPos; + const sal_Unicode m_cFill; + const bool m_bAutoTabStop; + + // Format() branches either into PreFormat() or PostFormat() + bool PreFormat( SwTextFormatInfo &rInf ); +public: + SwTabPortion( const sal_uInt16 nTabPos, const sal_Unicode cFill, const bool bAutoTab = true ); + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual void FormatEOL( SwTextFormatInfo &rInf ) override; + bool PostFormat( SwTextFormatInfo &rInf ); + bool IsFilled() const { return 0 != m_cFill; } + sal_uInt16 GetTabPos() const { return m_nTabPos; } + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; +}; + +class SwTabLeftPortion : public SwTabPortion +{ +public: + SwTabLeftPortion( const sal_uInt16 nTabPosVal, const sal_Unicode cFillChar, bool bAutoTab ) + : SwTabPortion( nTabPosVal, cFillChar, bAutoTab ) + { SetWhichPor( PortionType::TabLeft ); } +}; + +class SwTabRightPortion : public SwTabPortion +{ +public: + SwTabRightPortion( const sal_uInt16 nTabPosVal, const sal_Unicode cFillChar ) + : SwTabPortion( nTabPosVal, cFillChar ) + { SetWhichPor( PortionType::TabRight ); } +}; + +class SwTabCenterPortion : public SwTabPortion +{ +public: + SwTabCenterPortion( const sal_uInt16 nTabPosVal, const sal_Unicode cFillChar ) + : SwTabPortion( nTabPosVal, cFillChar ) + { SetWhichPor( PortionType::TabCenter ); } +}; + +class SwTabDecimalPortion : public SwTabPortion +{ + const sal_Unicode mcTab; + + /* + * During text formatting, we already store the width of the portions + * following the tab stop up to the decimal position. This value is + * evaluated during pLastTab->FormatEOL. FME 2006-01-06 #127428#. + */ + sal_uInt16 mnWidthOfPortionsUpTpDecimalPosition; + +public: + SwTabDecimalPortion( const sal_uInt16 nTabPosVal, const sal_Unicode cTab, + const sal_Unicode cFillChar ) + : SwTabPortion( nTabPosVal, cFillChar ), + mcTab(cTab), + mnWidthOfPortionsUpTpDecimalPosition( USHRT_MAX ) + { SetWhichPor( PortionType::TabDecimal ); } + + sal_Unicode GetTabDecimal() const { return mcTab; } + + void SetWidthOfPortionsUpToDecimalPosition( sal_uInt16 nNew ) + { + mnWidthOfPortionsUpTpDecimalPosition = nNew; + } + sal_uInt16 GetWidthOfPortionsUpToDecimalPosition() const + { + return mnWidthOfPortionsUpTpDecimalPosition; + } +}; + +class SwAutoTabDecimalPortion : public SwTabDecimalPortion +{ +public: + SwAutoTabDecimalPortion( const sal_uInt16 nTabPosVal, const sal_Unicode cTab, + const sal_Unicode cFillChar ) + : SwTabDecimalPortion( nTabPosVal, cTab, cFillChar ) + { + SetLen(TextFrameIndex(0)); + } + virtual void Paint( const SwTextPaintInfo &rInf ) const override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/portox.cxx b/sw/source/core/text/portox.cxx new file mode 100644 index 0000000000..bd72e83b04 --- /dev/null +++ b/sw/source/core/text/portox.cxx @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <SwPortionHandler.hxx> +#include <viewopt.hxx> + +#include "portox.hxx" +#include "inftxt.hxx" + +void SwToxPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( Width() ) + { + rInf.DrawViewOpt( *this, PortionType::Tox ); + SwTextPortion::Paint( rInf ); + } +} + +SwLinePortion *SwIsoToxPortion::Compress() { return this; } + +SwIsoToxPortion::SwIsoToxPortion() : m_nViewWidth(0) +{ + SetLen(TextFrameIndex(1)); + SetWhichPor( PortionType::IsoTox ); +} + +sal_uInt16 SwIsoToxPortion::GetViewWidth( const SwTextSizeInfo &rInf ) const +{ + // Although we are const, nViewWidth should be calculated in the last + // moment possible + SwIsoToxPortion* pThis = const_cast<SwIsoToxPortion*>(this); + // nViewWidth need to be calculated + if( !Width() && rInf.OnWin() && + !rInf.GetOpt().IsPagePreview() && + !rInf.GetOpt().IsReadonly() && rInf.GetOpt().IsFieldShadings() ) + { + if( !m_nViewWidth ) + pThis->m_nViewWidth = rInf.GetTextSize(OUString(' ')).Width(); + } + else + pThis->m_nViewWidth = 0; + return m_nViewWidth; +} + +bool SwIsoToxPortion::Format( SwTextFormatInfo &rInf ) +{ + return SwLinePortion::Format( rInf ); +} + +void SwIsoToxPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( Width() ) + rInf.DrawViewOpt( *this, PortionType::Tox ); +} + +void SwIsoToxPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Special( GetLen(), OUString(), GetWhichPor() ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/portox.hxx b/sw/source/core/text/portox.hxx new file mode 100644 index 0000000000..07d7c44fb2 --- /dev/null +++ b/sw/source/core/text/portox.hxx @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include "portxt.hxx" + +class SwToxPortion : public SwTextPortion +{ +public: + SwToxPortion() { SetWhichPor(PortionType::Tox); } + virtual void Paint(const SwTextPaintInfo& rInf) const override; +}; + +class SwIsoToxPortion : public SwToxPortion +{ + sal_uInt16 m_nViewWidth; + +public: + SwIsoToxPortion(); + virtual bool Format(SwTextFormatInfo& rInf) override; + virtual void Paint(const SwTextPaintInfo& rInf) const override; + virtual SwLinePortion* Compress() override; + virtual sal_uInt16 GetViewWidth(const SwTextSizeInfo& rInf) const override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion(SwPortionHandler& rPH) const override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/portxt.cxx b/sw/source/core/text/portxt.cxx new file mode 100644 index 0000000000..85691ef21e --- /dev/null +++ b/sw/source/core/text/portxt.cxx @@ -0,0 +1,925 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <com/sun/star/i18n/ScriptType.hpp> +#include <com/sun/star/i18n/XBreakIterator.hpp> +#include <i18nlangtag/mslangid.hxx> +#include <breakit.hxx> +#include <hintids.hxx> +#include <EnhancedPDFExportHelper.hxx> +#include <SwPortionHandler.hxx> +#include "porlay.hxx" +#include "inftxt.hxx" +#include "guess.hxx" +#include "porfld.hxx" +#include <pagefrm.hxx> +#include <tgrditem.hxx> +#include <IDocumentSettingAccess.hxx> +#include <IDocumentMarkAccess.hxx> + +#include <IMark.hxx> +#include <pam.hxx> +#include <doc.hxx> +#include <xmloff/odffields.hxx> +#include <viewopt.hxx> + +using namespace ::sw::mark; +using namespace ::com::sun::star; +using namespace ::com::sun::star::i18n::ScriptType; + +// Returns for how many characters an extra space has to be added +// (for justified alignment). +static TextFrameIndex lcl_AddSpace(const SwTextSizeInfo &rInf, + const OUString* pStr, const SwLinePortion& rPor) +{ + TextFrameIndex nPos, nEnd; + const SwScriptInfo* pSI = nullptr; + + if ( pStr ) + { + // passing a string means we are inside a field + nPos = TextFrameIndex(0); + nEnd = TextFrameIndex(pStr->getLength()); + } + else + { + nPos = rInf.GetIdx(); + nEnd = rInf.GetIdx() + rPor.GetLen(); + pStr = &rInf.GetText(); + pSI = &const_cast<SwParaPortion*>(rInf.GetParaPortion())->GetScriptInfo(); + } + + TextFrameIndex nCnt(0); + sal_uInt8 nScript = 0; + + // If portion consists of Asian characters and language is not + // Korean, we add extra space to each character. + // first we get the script type + if ( pSI ) + nScript = pSI->ScriptType( nPos ); + else + nScript = static_cast<sal_uInt8>( + g_pBreakIt->GetBreakIter()->getScriptType(*pStr, sal_Int32(nPos))); + + // Note: rInf.GetIdx() can differ from nPos, + // e.g., when rPor is a field portion. nPos refers to the string passed + // to the function, rInf.GetIdx() refers to the original string. + + // We try to find out which justification mode is required. This is done by + // evaluating the script type and the language attribute set for this portion + + // Asian Justification: Each character get some extra space + if ( nEnd > nPos && ASIAN == nScript ) + { + LanguageType aLang = + rInf.GetTextFrame()->GetLangOfChar(rInf.GetIdx(), nScript); + + if (!MsLangId::isKorean(aLang)) + { + const SwLinePortion* pPor = rPor.GetNextPortion(); + if ( pPor && ( pPor->IsKernPortion() || + pPor->IsControlCharPortion() || + pPor->IsPostItsPortion() ) ) + pPor = pPor->GetNextPortion(); + + nCnt += SwScriptInfo::CountCJKCharacters( *pStr, nPos, nEnd, aLang ); + + if ( !pPor || pPor->IsHolePortion() || pPor->InFixMargGrp() || + pPor->IsBreakPortion() ) + --nCnt; + + return nCnt; + } + } + + // Kashida Justification: Insert Kashidas + if ( nEnd > nPos && pSI && COMPLEX == nScript ) + { + if ( SwScriptInfo::IsArabicText( *pStr, nPos, nEnd - nPos ) && pSI->CountKashida() ) + { + const sal_Int32 nKashRes = pSI->KashidaJustify(nullptr, nullptr, nPos, nEnd - nPos); + // i60591: need to check result of KashidaJustify + // determine if kashida justification is applicable + if (nKashRes != -1) + return TextFrameIndex(nKashRes); + } + } + + // Thai Justification: Each character cell gets some extra space + if ( nEnd > nPos && COMPLEX == nScript ) + { + LanguageType aLang = + rInf.GetTextFrame()->GetLangOfChar(rInf.GetIdx(), nScript); + + if ( LANGUAGE_THAI == aLang ) + { + nCnt = SwScriptInfo::ThaiJustify(*pStr, nullptr, nPos, nEnd - nPos); + + const SwLinePortion* pPor = rPor.GetNextPortion(); + if ( pPor && ( pPor->IsKernPortion() || + pPor->IsControlCharPortion() || + pPor->IsPostItsPortion() ) ) + pPor = pPor->GetNextPortion(); + + if ( nCnt && ( ! pPor || pPor->IsHolePortion() || pPor->InFixMargGrp() ) ) + --nCnt; + + return nCnt; + } + } + + // Here starts the good old "Look for blanks and add space to them" part. + // Note: We do not want to add space to an isolated latin blank in front + // of some complex characters in RTL environment + const bool bDoNotAddSpace = + LATIN == nScript && (nEnd == nPos + TextFrameIndex(1)) && pSI && + ( i18n::ScriptType::COMPLEX == + pSI->ScriptType(nPos + TextFrameIndex(1))) && + rInf.GetTextFrame() && rInf.GetTextFrame()->IsRightToLeft(); + + if ( bDoNotAddSpace ) + return nCnt; + + TextFrameIndex nTextEnd = std::min(nEnd, TextFrameIndex(pStr->getLength())); + for ( ; nPos < nTextEnd; ++nPos ) + { + if (CH_BLANK == (*pStr)[ sal_Int32(nPos) ]) + ++nCnt; + } + + // We still have to examine the next character: + // If the next character is ASIAN and not KOREAN we have + // to add an extra space + // nPos refers to the original string, even if a field string has + // been passed to this function + nPos = rInf.GetIdx() + rPor.GetLen(); + if (nPos < TextFrameIndex(rInf.GetText().getLength())) + { + sal_uInt8 nNextScript = 0; + const SwLinePortion* pPor = rPor.GetNextPortion(); + if ( pPor && pPor->IsKernPortion() ) + pPor = pPor->GetNextPortion(); + + if (!pPor || pPor->InFixMargGrp()) + return nCnt; + + // next character is inside a field? + if ( CH_TXTATR_BREAKWORD == rInf.GetChar( nPos ) && pPor->InExpGrp() ) + { + bool bOldOnWin = rInf.OnWin(); + const_cast<SwTextSizeInfo &>(rInf).SetOnWin( false ); + + OUString aStr; + pPor->GetExpText( rInf, aStr ); + const_cast<SwTextSizeInfo &>(rInf).SetOnWin( bOldOnWin ); + + nNextScript = static_cast<sal_uInt8>(g_pBreakIt->GetBreakIter()->getScriptType( aStr, 0 )); + } + else + nNextScript = static_cast<sal_uInt8>( + g_pBreakIt->GetBreakIter()->getScriptType(rInf.GetText(), sal_Int32(nPos))); + + if( ASIAN == nNextScript ) + { + LanguageType aLang = + rInf.GetTextFrame()->GetLangOfChar(nPos, nNextScript); + + if (!MsLangId::isKorean(aLang)) + ++nCnt; + } + } + + return nCnt; +} + +SwTextPortion * SwTextPortion::CopyLinePortion(const SwLinePortion &rPortion) +{ + SwTextPortion *const pNew(new SwTextPortion); + static_cast<SwLinePortion&>(*pNew) = rPortion; + pNew->SetWhichPor( PortionType::Text ); // overwrite that! + return pNew; +} + +void SwTextPortion::BreakCut( SwTextFormatInfo &rInf, const SwTextGuess &rGuess ) +{ + // The word/char is larger than the line + // Special case 1: The word is larger than the line + // We truncate ... + const sal_uInt16 nLineWidth = o3tl::narrowing<sal_uInt16>(rInf.Width() - rInf.X()); + TextFrameIndex nLen = rGuess.CutPos() - rInf.GetIdx(); + if (nLen > TextFrameIndex(0)) + { + // special case: guess does not always provide the correct + // width, only in common cases. + if ( !rGuess.BreakWidth() ) + { + rInf.SetLen( nLen ); + SetLen( nLen ); + CalcTextSize( rInf ); + + // changing these values requires also changing them in + // guess.cxx + sal_uInt16 nItalic = 0; + if( ITALIC_NONE != rInf.GetFont()->GetItalic() && !rInf.NotEOL() ) + { + nItalic = Height() / 12; + } + Width( Width() + nItalic ); + } + else + { + Width( rGuess.BreakWidth() ); + SetLen( nLen ); + } + } + // special case: first character does not fit to line + else if ( rGuess.CutPos() == rInf.GetLineStart() ) + { + SetLen( TextFrameIndex(1) ); + Width( nLineWidth ); + } + else + { + SetLen( TextFrameIndex(0) ); + Width( 0 ); + } +} + +void SwTextPortion::BreakUnderflow( SwTextFormatInfo &rInf ) +{ + Truncate(); + Height( 0 ); + Width( 0 ); + SetLen( TextFrameIndex(0) ); + SetAscent( 0 ); + rInf.SetUnderflow( this ); +} + +static bool lcl_HasContent( const SwFieldPortion& rField, SwTextFormatInfo const &rInf ) +{ + OUString aText; + return rField.GetExpText( rInf, aText ) && !aText.isEmpty(); +} + +bool SwTextPortion::Format_( SwTextFormatInfo &rInf ) +{ + // 5744: If only the hyphen does not fit anymore, we still need to wrap + // the word, or else return true! + if( rInf.IsUnderflow() && rInf.GetSoftHyphPos() ) + { + // soft hyphen portion has triggered an underflow event because + // of an alternative spelling position + bool bFull = false; + const bool bHyph = rInf.ChgHyph( true ); + if( rInf.IsHyphenate() ) + { + SwTextGuess aGuess; + // check for alternative spelling left from the soft hyphen + // this should usually be true but + aGuess.AlternativeSpelling(rInf, rInf.GetSoftHyphPos() - TextFrameIndex(1)); + bFull = CreateHyphen( rInf, aGuess ); + OSL_ENSURE( bFull, "Problem with hyphenation!!!" ); + } + rInf.ChgHyph( bHyph ); + rInf.SetSoftHyphPos( TextFrameIndex(0) ); + return bFull; + } + + SwTextGuess aGuess; + bool bFull = !aGuess.Guess( *this, rInf, Height() ); + + // tdf#158776 for the last full text portion, call Guess() again to allow more text in the + // adjusted line by shrinking spaces using the know space count from the first Guess() call + const SvxAdjust& rAdjust = rInf.GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet().GetAdjust().GetAdjust(); + if ( bFull && rAdjust == SvxAdjust::Block && + aGuess.BreakPos() != TextFrameIndex(COMPLETE_STRING) && + rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::JUSTIFY_LINES_WITH_SHRINKING) && + // tdf#158436 avoid shrinking at underflow, e.g. no-break space after a + // very short word resulted endless loop + !rInf.IsUnderflow() ) + { + sal_Int32 nSpacesInLine(0); + for (sal_Int32 i = sal_Int32(rInf.GetLineStart()); i < sal_Int32(aGuess.BreakPos()); ++i) + { + sal_Unicode cChar = rInf.GetText()[i]; + if ( cChar == CH_BLANK ) + ++nSpacesInLine; + } + + // call with an extra space: shrinking can result a new word in the line + // and a new space before that, which is also a shrank space + // (except if the line was already broken at a soft hyphen, i.e. inside a word) + if ( aGuess.BreakPos() < TextFrameIndex(rInf.GetText().getLength()) && + rInf.GetText()[sal_Int32(aGuess.BreakPos())] != CHAR_SOFTHYPHEN ) + { + ++nSpacesInLine; + } + + bFull = !aGuess.Guess( *this, rInf, Height(), nSpacesInLine ); + } + + // these are the possible cases: + // A Portion fits to current line + // B Portion does not fit to current line but a possible line break + // within the portion has been found by the break iterator, 2 subcases + // B1 break is hyphen + // B2 break is word end + // C Portion does not fit to current line and no possible line break + // has been found by break iterator, 2 subcases: + // C1 break iterator found a possible line break in portion before us + // ==> this break is used (underflow) + // C2 break iterator does not found a possible line break at all: + // ==> line break + + // case A: line not yet full + if ( !bFull ) + { + Width( aGuess.BreakWidth() ); + ExtraBlankWidth(aGuess.ExtraBlankWidth()); + // Caution! + if( !InExpGrp() || InFieldGrp() ) + SetLen( rInf.GetLen() ); + + short nKern = rInf.GetFont()->CheckKerning(); + if( nKern > 0 && rInf.Width() < rInf.X() + Width() + nKern ) + { + nKern = static_cast<short>(rInf.Width() - rInf.X() - Width() - 1); + if( nKern < 0 ) + nKern = 0; + } + if( nKern ) + new SwKernPortion( *this, nKern ); + } + // special case: hanging portion + else if( aGuess.GetHangingPortion() ) + { + Width( aGuess.BreakWidth() ); + SetLen( aGuess.BreakPos() - rInf.GetIdx() ); + aGuess.GetHangingPortion()->SetAscent( GetAscent() ); + Insert( aGuess.ReleaseHangingPortion() ); + } + // breakPos >= index + else if (aGuess.BreakPos() >= rInf.GetIdx() && aGuess.BreakPos() != TextFrameIndex(COMPLETE_STRING)) + { + // case B1 + if( aGuess.HyphWord().is() && aGuess.BreakPos() > rInf.GetLineStart() + && ( aGuess.BreakPos() > rInf.GetIdx() || + ( rInf.GetLast() && ! rInf.GetLast()->IsFlyPortion() ) ) ) + { + CreateHyphen( rInf, aGuess ); + if ( rInf.GetFly() ) + rInf.GetRoot()->SetMidHyph( true ); + else + rInf.GetRoot()->SetEndHyph( true ); + } + // case C1 + // - Footnote portions with fake line start (i.e., not at beginning of line) + // should keep together with the text portion. (Note: no keep together + // with only footnote portions. + // - TabPortions not at beginning of line should keep together with the + // text portion, if they are not followed by a blank + // (work around different definition of tab stop character - breaking or + // non breaking character - in compatibility mode) + else if ( ( IsFootnotePortion() && rInf.IsFakeLineStart() && + + rInf.IsOtherThanFootnoteInside() ) || + ( rInf.GetLast() && + rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::TAB_COMPAT) && + rInf.GetLast()->InTabGrp() && + rInf.GetLineStart() + rInf.GetLast()->GetLen() < rInf.GetIdx() && + aGuess.BreakPos() == rInf.GetIdx() && + CH_BLANK != rInf.GetChar( rInf.GetIdx() ) && + CH_FULL_BLANK != rInf.GetChar( rInf.GetIdx() ) && + CH_SIX_PER_EM != rInf.GetChar( rInf.GetIdx() ) ) ) + BreakUnderflow( rInf ); + // case B2 + else if( rInf.GetIdx() > rInf.GetLineStart() || + aGuess.BreakPos() > rInf.GetIdx() || + // this is weird: during formatting the follow of a field + // the values rInf.GetIdx and rInf.GetLineStart are replaced + // IsFakeLineStart indicates GetIdx > GetLineStart + rInf.IsFakeLineStart() || + rInf.GetFly() || + rInf.IsFirstMulti() || + ( rInf.GetLast() && + ( rInf.GetLast()->IsFlyPortion() || + ( rInf.GetLast()->InFieldGrp() && + ! rInf.GetLast()->InNumberGrp() && + ! rInf.GetLast()->IsErgoSumPortion() && + lcl_HasContent(*static_cast<SwFieldPortion*>(rInf.GetLast()),rInf ) ) ) ) ) + { + Width( aGuess.BreakWidth() ); + + SetLen( aGuess.BreakPos() - rInf.GetIdx() ); + + OSL_ENSURE( aGuess.BreakStart() >= aGuess.FieldDiff(), + "Trouble with expanded field portions during line break" ); + TextFrameIndex const nRealStart = aGuess.BreakStart() - aGuess.FieldDiff(); + if( aGuess.BreakPos() < nRealStart && !InExpGrp() ) + { + SwHolePortion *pNew = new SwHolePortion( *this ); + pNew->SetLen( nRealStart - aGuess.BreakPos() ); + pNew->Width(0); + pNew->ExtraBlankWidth( aGuess.ExtraBlankWidth() ); + Insert( pNew ); + + // UAX #14 Unicode Line Breaking Algorithm Non-tailorable Line breaking rule LB6: + // https://www.unicode.org/reports/tr14/#LB6 Do not break before hard line breaks + if (auto ch = rInf.GetChar(aGuess.BreakStart()); !ch || ch == CH_BREAK) + bFull = false; // Keep following SwBreakPortion / para break in the same line + } + } + else // case C2, last exit + BreakCut( rInf, aGuess ); + } + // breakPos < index or no breakpos at all + else + { + bool bFirstPor = rInf.GetLineStart() == rInf.GetIdx(); + if (aGuess.BreakPos() != TextFrameIndex(COMPLETE_STRING) && + aGuess.BreakPos() != rInf.GetLineStart() && + ( !bFirstPor || rInf.GetFly() || rInf.GetLast()->IsFlyPortion() || + rInf.IsFirstMulti() ) && + ( !rInf.GetLast()->IsBlankPortion() || + SwBlankPortion::MayUnderflow(rInf, rInf.GetIdx() - TextFrameIndex(1), true))) + { // case C1 (former BreakUnderflow()) + BreakUnderflow( rInf ); + } + else + // case C2, last exit + BreakCut(rInf, aGuess); + } + + return bFull; +} + +bool SwTextPortion::Format( SwTextFormatInfo &rInf ) +{ + // GetLineWidth() takes care of DocumentSettingId::TAB_OVER_MARGIN. + if( rInf.GetLineWidth() < 0 || (!GetLen() && !InExpGrp()) ) + { + Height( 0 ); + Width( 0 ); + SetLen( TextFrameIndex(0) ); + SetAscent( 0 ); + SetNextPortion( nullptr ); // ???? + return true; + } + + OSL_ENSURE( rInf.RealWidth() || (rInf.X() == rInf.Width()), + "SwTextPortion::Format: missing real width" ); + OSL_ENSURE( Height(), "SwTextPortion::Format: missing height" ); + + return Format_( rInf ); +} + +// Format end of line +// 5083: We can have awkward cases e.g.: +// "from {Santa}" +// Santa wraps, "from " turns into "from" and " " in a justified +// paragraph, in which the glue gets expanded instead of merged +// with the MarginPortion. + +// rInf.nIdx points to the next word, nIdx-1 is the portion's last char +void SwTextPortion::FormatEOL( SwTextFormatInfo &rInf ) +{ + if( ( GetNextPortion() && + ( !GetNextPortion()->IsKernPortion() || GetNextPortion()->GetNextPortion() ) ) || + !GetLen() || + rInf.GetIdx() >= TextFrameIndex(rInf.GetText().getLength()) || + TextFrameIndex(1) >= rInf.GetIdx() || + ' ' != rInf.GetChar(rInf.GetIdx() - TextFrameIndex(1)) || + rInf.GetLast()->IsHolePortion() ) + return; + + // calculate number of blanks + TextFrameIndex nX(rInf.GetIdx() - TextFrameIndex(1)); + TextFrameIndex nHoleLen(1); + while( nX && nHoleLen < GetLen() && CH_BLANK == rInf.GetChar( --nX ) ) + nHoleLen++; + + // First set ourselves and the insert, because there could be + // a SwLineLayout + sal_uInt16 nBlankSize; + if( nHoleLen == GetLen() ) + nBlankSize = Width(); + else + nBlankSize = sal_Int32(nHoleLen) * rInf.GetTextSize(OUString(' ')).Width(); + Width( Width() - nBlankSize ); + rInf.X( rInf.X() - nBlankSize ); + SetLen( GetLen() - nHoleLen ); + SwLinePortion *pHole = new SwHolePortion( *this ); + static_cast<SwHolePortion *>( pHole )->SetBlankWidth( nBlankSize ); + static_cast<SwHolePortion *>( pHole )->SetLen( nHoleLen ); + Insert( pHole ); + +} + +TextFrameIndex SwTextPortion::GetModelPositionForViewPoint(const sal_uInt16 nOfst) const +{ + OSL_ENSURE( false, "SwTextPortion::GetModelPositionForViewPoint: don't use this method!" ); + return SwLinePortion::GetModelPositionForViewPoint( nOfst ); +} + +// The GetTextSize() assumes that the own length is correct +SwPosSize SwTextPortion::GetTextSize( const SwTextSizeInfo &rInf ) const +{ + SwPosSize aSize = rInf.GetTextSize(); + if( !GetJoinBorderWithPrev() ) + aSize.Width(aSize.Width() + rInf.GetFont()->GetLeftBorderSpace() ); + if( !GetJoinBorderWithNext() ) + aSize.Width(aSize.Width() + rInf.GetFont()->GetRightBorderSpace() ); + + aSize.Height(aSize.Height() + + rInf.GetFont()->GetTopBorderSpace() + + rInf.GetFont()->GetBottomBorderSpace() ); + + return aSize; +} + +void SwTextPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if (rInf.OnWin() && TextFrameIndex(1) == rInf.GetLen() + && CH_TXT_ATR_FIELDEND == rInf.GetText()[sal_Int32(rInf.GetIdx())]) + { + assert(false); // this is some debugging only code + rInf.DrawBackBrush( *this ); + const OUString aText(CH_TXT_ATR_SUBST_FIELDEND); + rInf.DrawText(aText, *this, TextFrameIndex(0), TextFrameIndex(aText.getLength())); + } + else if (rInf.OnWin() && TextFrameIndex(1) == rInf.GetLen() + && CH_TXT_ATR_FIELDSTART == rInf.GetText()[sal_Int32(rInf.GetIdx())]) + { + assert(false); // this is some debugging only code + rInf.DrawBackBrush( *this ); + const OUString aText(CH_TXT_ATR_SUBST_FIELDSTART); + rInf.DrawText(aText, *this, TextFrameIndex(0), TextFrameIndex(aText.getLength())); + } + else if( GetLen() ) + { + rInf.DrawBackBrush( *this ); + rInf.DrawBorder( *this ); + + rInf.DrawCSDFHighlighting(*this); + + // do we have to repaint a post it portion? + if( rInf.OnWin() && mpNextPortion && !mpNextPortion->Width() ) + mpNextPortion->PrePaint( rInf, this ); + + auto const* pWrongList = rInf.GetpWrongList(); + auto const* pGrammarCheckList = rInf.GetGrammarCheckList(); + auto const* pSmarttags = rInf.GetSmartTags(); + + const bool bWrong = nullptr != pWrongList; + const bool bGrammarCheck = nullptr != pGrammarCheckList; + const bool bSmartTags = nullptr != pSmarttags; + + if ( bWrong || bSmartTags || bGrammarCheck ) + rInf.DrawMarkedText( *this, rInf.GetLen(), bWrong, bSmartTags, bGrammarCheck ); + else + rInf.DrawText( *this, rInf.GetLen() ); + } +} + +bool SwTextPortion::GetExpText( const SwTextSizeInfo &, OUString & ) const +{ + return false; +} + +// Responsible for the justified paragraph. They calculate the blank +// count and the resulting added space. +TextFrameIndex SwTextPortion::GetSpaceCnt(const SwTextSizeInfo &rInf, + TextFrameIndex& rCharCnt) const +{ + TextFrameIndex nCnt(0); + TextFrameIndex nPos(0); + + if ( rInf.SnapToGrid() ) + { + SwTextGridItem const*const pGrid(GetGridItem(rInf.GetTextFrame()->FindPageFrame())); + if (pGrid && GRID_LINES_CHARS == pGrid->GetGridType() && pGrid->IsSnapToChars()) + return TextFrameIndex(0); + } + + if ( InExpGrp() || PortionType::InputField == GetWhichPor() ) + { + if (OUString ExpOut; + (!IsBlankPortion() + || (GetExpText(rInf, ExpOut) && OUStringChar(CH_BLANK) == ExpOut)) + && !InNumberGrp() && !IsCombinedPortion()) + { + // OnWin() likes to return a blank instead of an empty string from + // time to time. We cannot use that here at all, however. + bool bOldOnWin = rInf.OnWin(); + const_cast<SwTextSizeInfo &>(rInf).SetOnWin( false ); + + OUString aStr; + GetExpText( rInf, aStr ); + const_cast<SwTextSizeInfo &>(rInf).SetOnWin( bOldOnWin ); + + nCnt = nCnt + lcl_AddSpace( rInf, &aStr, *this ); + nPos = TextFrameIndex(aStr.getLength()); + } + } + else if( !IsDropPortion() ) + { + nCnt = nCnt + lcl_AddSpace( rInf, nullptr, *this ); + nPos = GetLen(); + } + rCharCnt = rCharCnt + nPos; + return nCnt; +} + +SwTwips SwTextPortion::CalcSpacing( tools::Long nSpaceAdd, const SwTextSizeInfo &rInf ) const +{ + TextFrameIndex nCnt(0); + + if ( rInf.SnapToGrid() ) + { + SwTextGridItem const*const pGrid(GetGridItem(rInf.GetTextFrame()->FindPageFrame())); + if (pGrid && GRID_LINES_CHARS == pGrid->GetGridType() && pGrid->IsSnapToChars()) + return 0; + } + + if ( InExpGrp() || PortionType::InputField == GetWhichPor() ) + { + if (OUString ExpOut; + (!IsBlankPortion() + || (GetExpText(rInf, ExpOut) && OUStringChar(CH_BLANK) == ExpOut)) + && !InNumberGrp() && !IsCombinedPortion()) + { + // OnWin() likes to return a blank instead of an empty string from + // time to time. We cannot use that here at all, however. + bool bOldOnWin = rInf.OnWin(); + const_cast<SwTextSizeInfo &>(rInf).SetOnWin( false ); + + OUString aStr; + GetExpText( rInf, aStr ); + const_cast<SwTextSizeInfo &>(rInf).SetOnWin( bOldOnWin ); + if( nSpaceAdd > 0 ) + nCnt = nCnt + lcl_AddSpace( rInf, &aStr, *this ); + else + { + nSpaceAdd = -nSpaceAdd; + nCnt = TextFrameIndex(aStr.getLength()); + } + } + } + else if( !IsDropPortion() ) + { + if( nSpaceAdd > 0 ) + nCnt = nCnt + lcl_AddSpace( rInf, nullptr, *this ); + else + { + nSpaceAdd = -nSpaceAdd; + nCnt = GetLen(); + SwLinePortion* pPor = GetNextPortion(); + + // we do not want an extra space in front of margin portions + if ( nCnt ) + { + while ( pPor && !pPor->Width() && ! pPor->IsHolePortion() ) + pPor = pPor->GetNextPortion(); + + if ( !pPor || pPor->InFixMargGrp() || pPor->IsHolePortion() ) + --nCnt; + } + } + } + + return sal_Int32(nCnt) * (nSpaceAdd > LONG_MAX/2 ? LONG_MAX/2 - nSpaceAdd : nSpaceAdd) + / SPACING_PRECISION_FACTOR; +} + +void SwTextPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Text( GetLen(), GetWhichPor() ); +} + +SwTextInputFieldPortion::SwTextInputFieldPortion() +{ + SetWhichPor( PortionType::InputField ); +} + +bool SwTextInputFieldPortion::Format(SwTextFormatInfo &rTextFormatInfo) +{ + return SwTextPortion::Format(rTextFormatInfo); +} + +void SwTextInputFieldPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if ( Width() ) + { + rInf.DrawViewOpt( *this, PortionType::InputField ); + SwTextSlot aPaintText( &rInf, this, true, true, OUString() ); + SwTextPortion::Paint( rInf ); + } + else + { + // highlight empty input field, elsewhere they are completely invisible for the user + SwRect aIntersect; + rInf.CalcRect(*this, &aIntersect); + const sal_uInt16 aAreaWidth = rInf.GetTextSize(OUString(' ')).Width(); + aIntersect.Left(aIntersect.Left() - aAreaWidth/2); + aIntersect.Width(aAreaWidth); + + if (aIntersect.HasArea() + && rInf.OnWin() + && rInf.GetOpt().IsFieldShadings() + && !rInf.GetOpt().IsPagePreview()) + { + OutputDevice* pOut = const_cast<OutputDevice*>(rInf.GetOut()); + pOut->Push(vcl::PushFlags::LINECOLOR | vcl::PushFlags::FILLCOLOR); + pOut->SetFillColor(rInf.GetOpt().GetFieldShadingsColor()); + pOut->SetLineColor(); + pOut->DrawRect(aIntersect.SVRect()); + pOut->Pop(); + } + } +} + +bool SwTextInputFieldPortion::GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const +{ + sal_Int32 nIdx(rInf.GetIdx()); + sal_Int32 nLen(GetLen()); + if ( rInf.GetChar( rInf.GetIdx() ) == CH_TXT_ATR_INPUTFIELDSTART ) + { + ++nIdx; + --nLen; + } + if (rInf.GetChar(rInf.GetIdx() + GetLen() - TextFrameIndex(1)) == CH_TXT_ATR_INPUTFIELDEND) + { + --nLen; + } + rText = rInf.GetText().copy( nIdx, std::min( nLen, rInf.GetText().getLength() - nIdx ) ); + + return true; +} + +SwPosSize SwTextInputFieldPortion::GetTextSize( const SwTextSizeInfo &rInf ) const +{ + SwTextSlot aFormatText( &rInf, this, true, false ); + if (rInf.GetLen() == TextFrameIndex(0)) + { + return SwPosSize( 0, 0 ); + } + + return rInf.GetTextSize(); +} + +SwHolePortion::SwHolePortion( const SwTextPortion &rPor ) + : m_nBlankWidth( 0 ) +{ + SetLen( TextFrameIndex(1) ); + Height( rPor.Height() ); + Width(0); + SetAscent( rPor.GetAscent() ); + SetWhichPor( PortionType::Hole ); +} + +SwLinePortion *SwHolePortion::Compress() { return this; } + +// The GetTextSize() assumes that the own length is correct +SwPosSize SwHolePortion::GetTextSize(const SwTextSizeInfo& rInf) const +{ + SwPosSize aSize = rInf.GetTextSize(); + if (!GetJoinBorderWithPrev()) + aSize.Width(aSize.Width() + rInf.GetFont()->GetLeftBorderSpace()); + if (!GetJoinBorderWithNext()) + aSize.Width(aSize.Width() + rInf.GetFont()->GetRightBorderSpace()); + + aSize.Height(aSize.Height() + + rInf.GetFont()->GetTopBorderSpace() + + rInf.GetFont()->GetBottomBorderSpace()); + + return aSize; +} + +void SwHolePortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( !rInf.GetOut() ) + return; + + bool bPDFExport = rInf.GetVsh()->GetViewOptions()->IsPDFExport(); + + // #i16816# export stuff only needed for tagged pdf support + if (bPDFExport && !SwTaggedPDFHelper::IsExportTaggedPDF( *rInf.GetOut()) ) + return; + + // #i68503# the hole must have no decoration for a consistent visual appearance + const SwFont* pOrigFont = rInf.GetFont(); + std::unique_ptr<SwFont> pHoleFont; + std::optional<SwFontSave> oFontSave; + if( pOrigFont->GetUnderline() != LINESTYLE_NONE + || pOrigFont->GetOverline() != LINESTYLE_NONE + || pOrigFont->GetStrikeout() != STRIKEOUT_NONE ) + { + pHoleFont.reset(new SwFont( *pOrigFont )); + pHoleFont->SetUnderline( LINESTYLE_NONE ); + pHoleFont->SetOverline( LINESTYLE_NONE ); + pHoleFont->SetStrikeout( STRIKEOUT_NONE ); + oFontSave.emplace( rInf, pHoleFont.get() ); + } + + if (bPDFExport) + { + rInf.DrawText(" ", *this, TextFrameIndex(0), TextFrameIndex(1)); + } + else + { + // tdf#43244: Paint spaces even at end of line, + // but only if this paint is not called for pdf export, to keep that pdf export intact + rInf.DrawText(*this, rInf.GetLen()); + } + + oFontSave.reset(); + pHoleFont.reset(); +} + +bool SwHolePortion::Format( SwTextFormatInfo &rInf ) +{ + return rInf.IsFull() || rInf.X() >= rInf.Width(); +} + +void SwHolePortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Text( GetLen(), GetWhichPor() ); +} + +void SwHolePortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwHolePortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("blank-width"), + BAD_CAST(OString::number(m_nBlankWidth).getStr())); + + (void)xmlTextWriterEndElement(pWriter); +} + +void SwFieldMarkPortion::Paint( const SwTextPaintInfo & /*rInf*/) const +{ + // These shouldn't be painted! + //SwTextPortion::Paint(rInf); +} + +bool SwFieldMarkPortion::Format( SwTextFormatInfo & ) +{ + Width(0); + return false; +} + +void SwFieldFormCheckboxPortion::Paint( const SwTextPaintInfo& rInf ) const +{ + SwPosition const aPosition(rInf.GetTextFrame()->MapViewToModelPos(rInf.GetIdx())); + + IFieldmark const*const pBM = rInf.GetTextFrame()->GetDoc().getIDocumentMarkAccess()->getFieldmarkAt(aPosition); + + OSL_ENSURE(pBM && pBM->GetFieldname( ) == ODF_FORMCHECKBOX, + "Where is my form field bookmark???"); + + if (pBM && pBM->GetFieldname( ) == ODF_FORMCHECKBOX) + { + const ICheckboxFieldmark* pCheckboxFm = dynamic_cast<ICheckboxFieldmark const*>(pBM); + bool bChecked = pCheckboxFm && pCheckboxFm->IsChecked(); + rInf.DrawCheckBox(*this, bChecked); + } +} + +bool SwFieldFormCheckboxPortion::Format( SwTextFormatInfo & rInf ) +{ + SwPosition const aPosition(rInf.GetTextFrame()->MapViewToModelPos(rInf.GetIdx())); + IFieldmark const*const pBM = rInf.GetTextFrame()->GetDoc().getIDocumentMarkAccess()->getFieldmarkAt(aPosition); + OSL_ENSURE(pBM && pBM->GetFieldname( ) == ODF_FORMCHECKBOX, "Where is my form field bookmark???"); + if (pBM && pBM->GetFieldname( ) == ODF_FORMCHECKBOX) + { + // the width of the checkbox portion is the same as its height since it's a square + // and that size depends on the font size. + // See: + // http://document-foundation-mail-archive.969070.n3.nabble.com/Wrong-copy-paste-in-SwFieldFormCheckboxPortion-Format-td4269112.html + Width( rInf.GetTextHeight( ) ); + Height( rInf.GetTextHeight( ) ); + SetAscent( rInf.GetAscent( ) ); + } + return false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/portxt.hxx b/sw/source/core/text/portxt.hxx new file mode 100644 index 0000000000..c826395272 --- /dev/null +++ b/sw/source/core/text/portxt.hxx @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include "porlin.hxx" + +class SwTextGuess; + +/// This portion represents a part of the paragraph string. +class SwTextPortion : public SwLinePortion +{ + void BreakCut( SwTextFormatInfo &rInf, const SwTextGuess &rGuess ); + void BreakUnderflow( SwTextFormatInfo &rInf ); + bool Format_( SwTextFormatInfo &rInf ); + +public: + SwTextPortion(){ SetWhichPor( PortionType::Text ); } + static SwTextPortion * CopyLinePortion(const SwLinePortion &rPortion); + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual void FormatEOL( SwTextFormatInfo &rInf ) override; + virtual TextFrameIndex GetModelPositionForViewPoint(sal_uInt16 nOfst) const override; + virtual SwPosSize GetTextSize( const SwTextSizeInfo &rInfo ) const override; + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + virtual SwTwips CalcSpacing( tools::Long nSpaceAdd, const SwTextSizeInfo &rInf ) const override; + + // Counts the spaces for justified paragraph + TextFrameIndex GetSpaceCnt(const SwTextSizeInfo &rInf, TextFrameIndex& rCnt) const; + + bool CreateHyphen( SwTextFormatInfo &rInf, SwTextGuess const &rGuess ); + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; +}; + +class SwTextInputFieldPortion : public SwTextPortion +{ +public: + SwTextInputFieldPortion(); + + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const override; + virtual SwPosSize GetTextSize( const SwTextSizeInfo &rInfo ) const override; +}; + +class SwHolePortion : public SwLinePortion +{ + sal_uInt16 m_nBlankWidth; +public: + explicit SwHolePortion( const SwTextPortion &rPor ); + sal_uInt16 GetBlankWidth( ) const { return m_nBlankWidth; } + void SetBlankWidth( const sal_uInt16 nNew ) { m_nBlankWidth = nNew; } + virtual SwLinePortion *Compress() override; + virtual bool Format( SwTextFormatInfo &rInf ) override; + virtual SwPosSize GetTextSize(const SwTextSizeInfo& rInfo) const override; + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + + // Accessibility: pass information about this portion to the PortionHandler + virtual void HandlePortion( SwPortionHandler& rPH ) const override; + + void dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const override; +}; + +class SwFieldMarkPortion : public SwTextPortion +{ + public: + SwFieldMarkPortion() : SwTextPortion() + { + SetWhichPor(PortionType::FieldMark); + } + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; +}; + +class SwFieldFormCheckboxPortion : public SwTextPortion +{ +public: + SwFieldFormCheckboxPortion() : SwTextPortion() + { + SetWhichPor(PortionType::FieldFormCheckbox); + } + virtual void Paint( const SwTextPaintInfo &rInf ) const override; + virtual bool Format( SwTextFormatInfo &rInf ) override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/possiz.hxx b/sw/source/core/text/possiz.hxx new file mode 100644 index 0000000000..f66bd1988c --- /dev/null +++ b/sw/source/core/text/possiz.hxx @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <tools/gen.hxx> +#include <swtypes.hxx> + +// Compared to the SV sizes SwPosSize is always positive +class SwPosSize +{ + SwTwips m_nWidth; + SwTwips m_nHeight; +public: + SwPosSize( const SwTwips nW = 0, const SwTwips nH = 0 ) + : m_nWidth(nW) + , m_nHeight(nH) + { + } + explicit SwPosSize( const Size &rSize ) + : m_nWidth(SwTwips(rSize.Width())) + , m_nHeight(SwTwips(rSize.Height())) + { + } +#if defined(__COVERITY__) + virtual ~SwPosSize() COVERITY_NOEXCEPT_FALSE {} +#else + virtual ~SwPosSize() {} +#endif + SwPosSize(SwPosSize const &) = default; + SwPosSize(SwPosSize &&) = default; + SwPosSize & operator =(SwPosSize const &) = default; + SwPosSize & operator =(SwPosSize &&) = default; + SwTwips Height() const { return m_nHeight; } + virtual void Height(const SwTwips nNew, const bool = true) { m_nHeight = nNew; } + SwTwips Width() const { return m_nWidth; } + void Width( const SwTwips nNew ) { m_nWidth = nNew; } + Size SvLSize() const { return Size( m_nWidth, m_nHeight ); } + void SvLSize( const Size &rSize ) + { + m_nWidth = SwTwips(rSize.Width()); + m_nHeight = SwTwips(rSize.Height()); + } + void SvXSize( const Size &rSize ) + { + m_nHeight = SwTwips(rSize.Width()); + m_nWidth = SwTwips(rSize.Height()); + } + SwPosSize& operator=( const Size &rSize ) + { + m_nWidth = SwTwips(rSize.Width()); + m_nHeight = SwTwips(rSize.Height()); + return *this; + } +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/redlnitr.cxx b/sw/source/core/text/redlnitr.cxx new file mode 100644 index 0000000000..0e89b7f75a --- /dev/null +++ b/sw/source/core/text/redlnitr.cxx @@ -0,0 +1,1201 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <string_view> + +#include <hintids.hxx> +#include <o3tl/safeint.hxx> +#include <svl/whiter.hxx> +#include <com/sun/star/i18n/ScriptType.hpp> +#include <scriptinfo.hxx> +#include <swmodule.hxx> +#include <redline.hxx> +#include <txatbase.hxx> +#include <docary.hxx> +#include "itratr.hxx" +#include <ndtxt.hxx> +#include <doc.hxx> +#include <IDocumentRedlineAccess.hxx> +#include <IDocumentLayoutAccess.hxx> +#include <IDocumentMarkAccess.hxx> +#include <IMark.hxx> +#include <bookmark.hxx> +#include <rootfrm.hxx> +#include <breakit.hxx> +#include <vcl/commandevent.hxx> +#include <vcl/settings.hxx> +#include <txtfrm.hxx> +#include <ftnfrm.hxx> +#include <vcl/svapp.hxx> +#include "redlnitr.hxx" +#include <extinput.hxx> +#include <fmtpdsc.hxx> +#include <editeng/charhiddenitem.hxx> +#include <editeng/colritem.hxx> +#include <editeng/crossedoutitem.hxx> +#include <editeng/formatbreakitem.hxx> +#include <editeng/udlnitem.hxx> + +using namespace ::com::sun::star; + +namespace { + +class HideIterator +{ +private: + IDocumentRedlineAccess const& m_rIDRA; + IDocumentMarkAccess const& m_rIDMA; + bool const m_isHideRedlines; + sw::FieldmarkMode const m_eFieldmarkMode; + bool const m_isHideParagraphBreaks; + SwPosition const m_Start; + /// next redline + SwRedlineTable::size_type m_RedlineIndex; + /// next fieldmark + std::pair<sw::mark::IFieldmark const*, std::optional<SwPosition>> m_Fieldmark; + std::optional<SwPosition> m_oNextFieldmarkHide; + /// previous paragraph break - because m_pStartPos/EndPos are non-owning + std::optional<std::pair<SwPosition, SwPosition>> m_oParagraphBreak; + /// current start/end pair + SwPosition const* m_pStartPos; + SwPosition const* m_pEndPos; + +public: + SwPosition const* GetStartPos() const { return m_pStartPos; } + SwPosition const* GetEndPos() const { return m_pEndPos; } + + HideIterator(SwTextNode & rTextNode, + bool const isHideRedlines, sw::FieldmarkMode const eMode, + sw::ParagraphBreakMode const ePBMode) + : m_rIDRA(rTextNode.getIDocumentRedlineAccess()) + , m_rIDMA(*rTextNode.getIDocumentMarkAccess()) + , m_isHideRedlines(isHideRedlines) + , m_eFieldmarkMode(eMode) + , m_isHideParagraphBreaks(ePBMode == sw::ParagraphBreakMode::Hidden) + , m_Start(rTextNode, 0) + , m_RedlineIndex(isHideRedlines ? m_rIDRA.GetRedlinePos(rTextNode, RedlineType::Any) : SwRedlineTable::npos) + , m_pStartPos(nullptr) + , m_pEndPos(&m_Start) + { + } + + // delete redlines and fieldmarks can't overlap, due to sw::CalcBreaks() + // and no combining of adjacent redlines + // -> dummy chars are delete-redlined *iff* entire fieldmark is + // Note: caller is responsible for checking for immediately adjacent hides + bool Next() + { + SwPosition const* pNextRedlineHide(nullptr); + assert(m_pEndPos); + if (m_isHideRedlines) + { + // position on current or next redline + for (; m_RedlineIndex < m_rIDRA.GetRedlineTable().size(); ++m_RedlineIndex) + { + SwRangeRedline const*const pRed = m_rIDRA.GetRedlineTable()[m_RedlineIndex]; + + if (m_pEndPos->GetNodeIndex() < pRed->Start()->GetNodeIndex()) + break; + + if (pRed->GetType() != RedlineType::Delete) + continue; + + auto [pStart, pEnd] = pRed->StartEnd(); // SwPosition* + if (*pStart == *pEnd) + { // only allowed while moving (either way?) +// assert(IDocumentRedlineAccess::IsHideChanges(rIDRA.GetRedlineFlags())); + continue; + } + if (pStart->GetNode().IsTableNode()) + { + assert(pEnd->GetNode() == m_Start.GetNode() && pEnd->GetContentIndex() == 0); + continue; // known pathology, ignore it + } + if (*m_pEndPos <= *pStart) + { + pNextRedlineHide = pStart; + break; // the next one + } + } + } + + // position on current or next fieldmark + m_oNextFieldmarkHide.reset(); + if (m_eFieldmarkMode != sw::FieldmarkMode::ShowBoth) + { + sal_Unicode const magic(m_eFieldmarkMode == sw::FieldmarkMode::ShowResult + ? CH_TXT_ATR_FIELDSTART + : CH_TXT_ATR_FIELDSEP); + SwTextNode* pTextNode = m_pEndPos->GetNode().GetTextNode(); + sal_Int32 const nPos = pTextNode ? pTextNode->GetText().indexOf( + magic, m_pEndPos->GetContentIndex()) : -1; + if (nPos != -1) + { + m_oNextFieldmarkHide.emplace(*pTextNode, nPos); + sw::mark::IFieldmark const*const pFieldmark( + m_eFieldmarkMode == sw::FieldmarkMode::ShowResult + ? m_rIDMA.getFieldmarkAt(*m_oNextFieldmarkHide) + : m_rIDMA.getInnerFieldmarkFor(*m_oNextFieldmarkHide)); + assert(pFieldmark); + m_Fieldmark.first = pFieldmark; + // for cursor travelling, there should be 2 visible chars; + // whichever char is hidden, the cursor travelling needs to + // be adapted in any case to skip in some situation or other; + // always hide the CH_TXT_ATR_FIELDSEP for now + if (m_eFieldmarkMode == sw::FieldmarkMode::ShowResult) + { + m_Fieldmark.second.emplace( + sw::mark::FindFieldSep(*m_Fieldmark.first)); + m_Fieldmark.second->AdjustContent(+1); + m_oNextFieldmarkHide->AdjustContent(+1); // skip start + } + else + { + m_Fieldmark.second.emplace(pFieldmark->GetMarkEnd()); + m_Fieldmark.second->AdjustContent(-1); + } + } + } + + // == can happen only if redline starts inside field command, and in + // that case redline will end before field separator + assert(!pNextRedlineHide || !m_oNextFieldmarkHide + || *pNextRedlineHide != *m_oNextFieldmarkHide + || *m_rIDRA.GetRedlineTable()[m_RedlineIndex]->End() < *m_Fieldmark.second); + if (pNextRedlineHide + && (!m_oNextFieldmarkHide || *pNextRedlineHide < *m_oNextFieldmarkHide)) + { + SwRangeRedline const*const pRed(m_rIDRA.GetRedlineTable()[m_RedlineIndex]); + m_pStartPos = pRed->Start(); + m_pEndPos = pRed->End(); + ++m_RedlineIndex; + return true; + } + else if (m_oNextFieldmarkHide) + { + assert(!pNextRedlineHide || *m_oNextFieldmarkHide <= *pNextRedlineHide); + m_pStartPos = &*m_oNextFieldmarkHide; + m_pEndPos = &*m_Fieldmark.second; + return true; + } + else + { + assert(!pNextRedlineHide && !m_oNextFieldmarkHide); + auto const hasHiddenItem = [](auto const& rNode) { + auto const& rpSet(rNode.GetAttr(RES_PARATR_LIST_AUTOFMT).GetStyleHandle()); + return rpSet ? rpSet->Get(RES_CHRATR_HIDDEN).GetValue() : false; + }; + auto const hasBreakBefore = [](SwTextNode const& rNode) { + if (rNode.GetAttr(RES_PAGEDESC).GetPageDesc()) + { + return true; + } + switch (rNode.GetAttr(RES_BREAK).GetBreak()) + { + case SvxBreak::ColumnBefore: + case SvxBreak::ColumnBoth: + case SvxBreak::PageBefore: + case SvxBreak::PageBoth: + return true; + default: + break; + } + return false; + }; + auto const hasBreakAfter = [](SwTextNode const& rNode) { + switch (rNode.GetAttr(RES_BREAK).GetBreak()) + { + case SvxBreak::ColumnAfter: + case SvxBreak::ColumnBoth: + case SvxBreak::PageAfter: + case SvxBreak::PageBoth: + return true; + default: + break; + } + return false; + }; + if (m_isHideParagraphBreaks + && m_pEndPos->GetNode().IsTextNode() // ooo27109-1.sxw + // only merge if next node is also text node + && m_pEndPos->GetNodes()[m_pEndPos->GetNodeIndex()+1]->IsTextNode() + && hasHiddenItem(*m_pEndPos->GetNode().GetTextNode()) + // no merge if there's a page break on any node + && !hasBreakBefore(*m_pEndPos->GetNodes()[m_pEndPos->GetNodeIndex()+1]->GetTextNode()) + // first node, see SwTextFrame::GetBreak() + && !hasBreakAfter(*m_Start.GetNode().GetTextNode())) + { + m_oParagraphBreak.emplace( + SwPosition(*m_pEndPos->GetNode().GetTextNode(), m_pEndPos->GetNode().GetTextNode()->Len()), + SwPosition(*m_pEndPos->GetNodes()[m_pEndPos->GetNodeIndex()+1]->GetTextNode(), 0)); + m_pStartPos = &m_oParagraphBreak->first; + m_pEndPos = &m_oParagraphBreak->second; + return true; + } + else // nothing + { + m_pStartPos = nullptr; + m_pEndPos = nullptr; + return false; + } + } + } +}; + +} + +namespace sw { + +std::unique_ptr<sw::MergedPara> +CheckParaRedlineMerge(SwTextFrame & rFrame, SwTextNode & rTextNode, + FrameMode const eMode) +{ + if (!rFrame.getRootFrame()->HasMergedParas()) + { + return nullptr; + } + bool bHaveRedlines(false); + std::vector<SwTextNode *> nodes{ &rTextNode }; + std::vector<SwTableNode *> tables; + std::vector<SwSectionNode *> sections; + std::vector<sw::Extent> extents; + OUStringBuffer mergedText; + SwTextNode * pParaPropsNode(nullptr); + SwTextNode * pNode(&rTextNode); + sal_Int32 nLastEnd(0); + for (auto iter = HideIterator(rTextNode, + rFrame.getRootFrame()->IsHideRedlines(), + rFrame.getRootFrame()->GetFieldmarkMode(), + rFrame.getRootFrame()->GetParagraphBreakMode()); + iter.Next(); ) + { + SwPosition const*const pStart(iter.GetStartPos()); + SwPosition const*const pEnd(iter.GetEndPos()); + bHaveRedlines = true; + assert(pNode != &rTextNode || &pStart->GetNode() == &rTextNode); // detect calls with wrong start node + if (pStart->GetContentIndex() != nLastEnd) // not 0 so we eliminate adjacent deletes + { + extents.emplace_back(pNode, nLastEnd, pStart->GetContentIndex()); + mergedText.append(pNode->GetText().subView(nLastEnd, pStart->GetContentIndex() - nLastEnd)); + } + if (&pEnd->GetNode() != pNode) + { + if (pNode == &rTextNode) + { + pNode->SetRedlineMergeFlag(SwNode::Merge::First); + } // else: was already set before + int nLevel(0); + for (SwNodeOffset j = pNode->GetIndex() + 1; j < pEnd->GetNodeIndex(); ++j) + { + SwNode *const pTmp(pNode->GetNodes()[j]); + if (nLevel == 0) + { + if (pTmp->IsTextNode()) + { + nodes.push_back(pTmp->GetTextNode()); + } + else if (pTmp->IsTableNode()) + { + tables.push_back(pTmp->GetTableNode()); + } + else if (pTmp->IsSectionNode()) + { + sections.push_back(pTmp->GetSectionNode()); + } + } + if (pTmp->IsStartNode()) + { + ++nLevel; + } + else if (pTmp->IsEndNode()) + { + --nLevel; + } + pTmp->SetRedlineMergeFlag(SwNode::Merge::Hidden); + } + // note: in DelLastPara() case, the end node is not actually merged + // and is likely a SwTableNode! + if (!pEnd->GetNode().IsTextNode()) + { + assert(pEnd->GetNode() != pStart->GetNode()); + // must set pNode too because it will mark the last node + pNode = nodes.back(); + assert(pNode == pNode->GetNodes()[pEnd->GetNodeIndex() - 1]); + if (pNode != &rTextNode) + { // something might depend on last merged one being NonFirst? + pNode->SetRedlineMergeFlag(SwNode::Merge::NonFirst); + } + nLastEnd = pNode->Len(); + } + else + { + pNode = pEnd->GetNode().GetTextNode(); + nodes.push_back(pNode); + pNode->SetRedlineMergeFlag(SwNode::Merge::NonFirst); + nLastEnd = pEnd->GetContentIndex(); + } + } + else + { + nLastEnd = pEnd->GetContentIndex(); + } + } + if (pNode == &rTextNode) + { + if (rTextNode.GetRedlineMergeFlag() != SwNode::Merge::None) + { + rTextNode.SetRedlineMergeFlag(SwNode::Merge::None); + } + } + // Reset flag of the following text node since we know it's not merged; + // also any table/sections in between. + // * the following SwTextNode is in same nodes section as pNode (nLevel=0) + // * the start nodes that don't have a SwTextNode before them + // on their level, and their corresponding end nodes + // * the first SwTextNode inside each start node of the previous point + // Other (non-first) SwTextNodes in nested sections shouldn't be reset! + int nLevel(0); + for (SwNodeOffset j = pNode->GetIndex() + 1; j < pNode->GetNodes().Count(); ++j) + { + SwNode *const pTmp(pNode->GetNodes()[j]); + if (!pTmp->IsCreateFrameWhenHidingRedlines()) + { // clear stale flag caused by editing with redlines shown + pTmp->SetRedlineMergeFlag(SwNode::Merge::None); + } + if (pTmp->IsStartNode()) + { + ++nLevel; + } + else if (pTmp->IsEndNode()) + { + if (nLevel == 0) + { + break; // there is no following text node; avoid leaving section + } + --nLevel; + } + else if (pTmp->IsTextNode()) + { + if (nLevel == 0) + { + break; // done + } + else + { // skip everything other than 1st text node in section! + j = pTmp->EndOfSectionIndex() - 1; // will be incremented again + } + } + } + if (!bHaveRedlines) + { + if (rTextNode.IsInList() && !rTextNode.GetNum(rFrame.getRootFrame())) + { + rTextNode.AddToListRLHidden(); // try to add it... + } + return nullptr; + } + if (nLastEnd != pNode->Len()) + { + extents.emplace_back(pNode, nLastEnd, pNode->Len()); + mergedText.append(pNode->GetText().subView(nLastEnd, pNode->Len() - nLastEnd)); + } + if (extents.empty()) // there was no text anywhere + { + assert(mergedText.isEmpty()); + pParaPropsNode = pNode; // if every node is empty, the last one wins + } + else + { + assert(!mergedText.isEmpty()); + pParaPropsNode = extents.begin()->pNode; // para props from first node that isn't empty + } +// pParaPropsNode = &rTextNode; // well, actually... + // keep lists up to date with visible nodes + if (pParaPropsNode->IsInList() && !pParaPropsNode->GetNum(rFrame.getRootFrame())) + { + pParaPropsNode->AddToListRLHidden(); // try to add it... + } + for (auto const pTextNode : nodes) + { + if (pTextNode != pParaPropsNode) + { + pTextNode->RemoveFromListRLHidden(); + } + } + if (eMode == FrameMode::Existing) + { + // remove existing footnote frames for first node; + // for non-first nodes with own frames, DelFrames will remove all + // (could possibly call lcl_ChangeFootnoteRef, not sure if worth it) + // note: must be done *before* changing listeners! + // for non-first nodes that are already merged with this frame, + // need to remove here too, otherwise footnotes can be removed only + // by lucky accident, e.g. TruncLines(). + auto itExtent(extents.begin()); + for (auto const pTextNode : nodes) + { + sal_Int32 nLast(0); + std::vector<std::pair<sal_Int32, sal_Int32>> hidden; + for ( ; itExtent != extents.end(); ++itExtent) + { + if (itExtent->pNode != pTextNode) + { + break; + } + if (itExtent->nStart != 0) + { + assert(itExtent->nStart != nLast); + hidden.emplace_back(nLast, itExtent->nStart); + } + nLast = itExtent->nEnd; + } + if (nLast != pTextNode->Len()) + { + hidden.emplace_back(nLast, pTextNode->Len()); + } + sw::RemoveFootnotesForNode(*rFrame.getRootFrame(), *pTextNode, &hidden); + } + // unfortunately DelFrames() must be done before StartListening too, + // otherwise footnotes cannot be deleted by SwTextFootnote::DelFrames! + auto const end(--nodes.rend()); + for (auto iter = nodes.rbegin(); iter != end; ++iter) + { + (**iter).DelFrames(rFrame.getRootFrame()); + } + // also delete tables & sections here; not necessary, but convenient + for (auto const pTableNode : tables) + { + pTableNode->DelFrames(rFrame.getRootFrame()); + } + for (auto const pSectionNode : sections) + { + pSectionNode->GetSection().GetFormat()->DelFrames(/*rFrame.getRootFrame()*/); + } + } + auto pRet(std::make_unique<sw::MergedPara>(rFrame, std::move(extents), + mergedText.makeStringAndClear(), pParaPropsNode, &rTextNode, + nodes.back())); + for (SwTextNode * pTmp : nodes) + { + pRet->listener.StartListening(pTmp); + } + rFrame.EndListeningAll(); + return pRet; +} + +} // namespace sw + +void SwAttrIter::InitFontAndAttrHandler( + SwTextNode const& rPropsNode, + SwTextNode const& rTextNode, + std::u16string_view aText, + bool const*const pbVertLayout, + bool const*const pbVertLayoutLRBT) +{ + // Build a font matching the default paragraph style: + SwFontAccess aFontAccess( &rPropsNode.GetAnyFormatColl(), m_pViewShell ); + // It is possible that Init is called more than once, e.g., in a + // SwTextFrame::FormatOnceMore situation or (since sw_redlinehide) + // from SwAttrIter::Seek(); in the latter case SwTextSizeInfo::m_pFnt + // is an alias of m_pFont so it must not be deleted! + if (m_pFont) + { + *m_pFont = aFontAccess.Get()->GetFont(); + } + else + { + m_pFont = new SwFont( aFontAccess.Get()->GetFont() ); + } + + // set font to vertical if frame layout is vertical + // if it's a re-init, the vert flag never changes + bool bVertLayoutLRBT = false; + if (pbVertLayoutLRBT) + bVertLayoutLRBT = *pbVertLayoutLRBT; + if (pbVertLayout ? *pbVertLayout : m_aAttrHandler.IsVertLayout()) + { + m_pFont->SetVertical(m_pFont->GetOrientation(), true, bVertLayoutLRBT); + } + + // Initialize the default attribute of the attribute handler + // based on the attribute array cached together with the font. + // If any further attributes for the paragraph are given in pAttrSet + // consider them during construction of the default array, and apply + // them to the font + m_aAttrHandler.Init(aFontAccess.Get()->GetDefault(), rTextNode.GetpSwAttrSet(), + *rTextNode.getIDocumentSettingAccess(), m_pViewShell, *m_pFont, + pbVertLayout ? *pbVertLayout : m_aAttrHandler.IsVertLayout(), + bVertLayoutLRBT ); + + m_aFontCacheIds[SwFontScript::Latin] = m_aFontCacheIds[SwFontScript::CJK] = m_aFontCacheIds[SwFontScript::CTL] = nullptr; + + assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is()); + + m_pFont->SetActual( m_pScriptInfo->WhichFont(TextFrameIndex(0)) ); + + TextFrameIndex nChg(0); + size_t nCnt = 0; + + do + { + if ( nCnt >= m_pScriptInfo->CountScriptChg() ) + break; + nChg = m_pScriptInfo->GetScriptChg( nCnt ); + SwFontScript nTmp = SW_SCRIPTS; + switch ( m_pScriptInfo->GetScriptType( nCnt++ ) ) { + case i18n::ScriptType::ASIAN : + if( !m_aFontCacheIds[SwFontScript::CJK] ) nTmp = SwFontScript::CJK; + break; + case i18n::ScriptType::COMPLEX : + if( !m_aFontCacheIds[SwFontScript::CTL] ) nTmp = SwFontScript::CTL; + break; + default: + if( !m_aFontCacheIds[SwFontScript::Latin ] ) nTmp = SwFontScript::Latin; + } + if( nTmp < SW_SCRIPTS ) + { + m_pFont->CheckFontCacheId( m_pViewShell, nTmp ); + m_pFont->GetFontCacheId( m_aFontCacheIds[ nTmp ], m_aFontIdx[ nTmp ], nTmp ); + } + } + while (nChg < TextFrameIndex(aText.size())); +} + +void SwAttrIter::CtorInitAttrIter(SwTextNode & rTextNode, + SwScriptInfo & rScriptInfo, SwTextFrame const*const pFrame) +{ + // during HTML-Import it can happen, that no layout exists + SwRootFrame* pRootFrame = rTextNode.getIDocumentLayoutAccess().GetCurrentLayout(); + m_pViewShell = pRootFrame ? pRootFrame->GetCurrShell() : nullptr; + + m_pScriptInfo = &rScriptInfo; + + // set font to vertical if frame layout is vertical + bool bVertLayout = false; + bool bVertLayoutLRBT = false; + bool bRTL = false; + if ( pFrame ) + { + if ( pFrame->IsVertical() ) + { + bVertLayout = true; + } + if (pFrame->IsVertLRBT()) + { + bVertLayoutLRBT = true; + } + bRTL = pFrame->IsRightToLeft(); + m_pMergedPara = pFrame->GetMergedPara(); + } + + // determine script changes if not already done for current paragraph + assert(m_pScriptInfo); + if (m_pScriptInfo->GetInvalidityA() != TextFrameIndex(COMPLETE_STRING)) + m_pScriptInfo->InitScriptInfo(rTextNode, m_pMergedPara, bRTL); + + InitFontAndAttrHandler( + m_pMergedPara ? *m_pMergedPara->pParaPropsNode : rTextNode, + rTextNode, + m_pMergedPara ? m_pMergedPara->mergedText : rTextNode.GetText(), + & bVertLayout, + & bVertLayoutLRBT); + + m_nStartIndex = m_nEndIndex = m_nPosition = m_nChgCnt = 0; + m_nPropFont = 0; + SwDoc& rDoc = rTextNode.GetDoc(); + const IDocumentRedlineAccess& rIDRA = rTextNode.getIDocumentRedlineAccess(); + + // sw_redlinehide: this is a Ring - pExtInp is the first PaM that's inside + // the node. It's not clear whether there can be more than 1 PaM in the + // Ring, and this code doesn't handle that case; neither did the old code. + const SwExtTextInput* pExtInp = rDoc.GetExtTextInput( rTextNode ); + if (!pExtInp && m_pMergedPara) + { + SwTextNode const* pNode(&rTextNode); + for (auto const& rExtent : m_pMergedPara->extents) + { + if (rExtent.pNode != pNode) + { + pNode = rExtent.pNode; + pExtInp = rDoc.GetExtTextInput(*pNode); + if (pExtInp) + break; + } + } + } + const bool bShow = IDocumentRedlineAccess::IsShowChanges(rIDRA.GetRedlineFlags()) + && pRootFrame && !pRootFrame->IsHideRedlines(); + if (!(pExtInp || m_pMergedPara || bShow)) + return; + + SwRedlineTable::size_type nRedlPos = rIDRA.GetRedlinePos( rTextNode, RedlineType::Any ); + if (SwRedlineTable::npos == nRedlPos && m_pMergedPara) + { + SwTextNode const* pNode(&rTextNode); + for (auto const& rExtent : m_pMergedPara->extents) + { // note: have to search because extents based only on Delete + if (rExtent.pNode != pNode) + { + pNode = rExtent.pNode; + nRedlPos = rIDRA.GetRedlinePos(*pNode, RedlineType::Any); + if (SwRedlineTable::npos != nRedlPos) + break; + } + } + // TODO this is true initially but after delete ops it may be false... need to delete m_pMerged somewhere? + // assert(SwRedlineTable::npos != nRedlPos); + // false now with fieldmarks + assert(!pRootFrame + || pRootFrame->GetFieldmarkMode() != sw::FieldmarkMode::ShowBoth + || SwRedlineTable::npos != nRedlPos || m_pMergedPara->extents.size() <= 1); + } + if (!(pExtInp || m_pMergedPara || SwRedlineTable::npos != nRedlPos)) + return; + + const std::vector<ExtTextInputAttr> *pArr = nullptr; + if( pExtInp ) + { + pArr = &pExtInp->GetAttrs(); + Seek( TextFrameIndex(0) ); + } + + m_pRedline.reset(new SwRedlineItr( rTextNode, *m_pFont, m_aAttrHandler, nRedlPos, + (pRootFrame && pRootFrame->IsHideRedlines()) + ? SwRedlineItr::Mode::Hide + : bShow + ? SwRedlineItr::Mode::Show + : SwRedlineItr::Mode::Ignore, + pArr, pExtInp ? pExtInp->Start() : nullptr)); + + if( m_pRedline->IsOn() ) + ++m_nChgCnt; +} + +// The Redline-Iterator +// The following information/states exist in RedlineIterator: +// +// m_nFirst is the first index of RedlineTable, which overlaps with the paragraph. +// +// m_nAct is the currently active (if m_bOn is set) or the next possible index. +// m_nStart and m_nEnd give you the borders of the object within the paragraph. +// +// If m_bOn is set, the font has been manipulated according to it. +// +// If m_nAct is set to SwRedlineTable::npos (via Reset()), then currently no +// Redline is active, m_nStart and m_nEnd are invalid. +SwRedlineItr::SwRedlineItr( const SwTextNode& rTextNd, SwFont& rFnt, + SwAttrHandler& rAH, sal_Int32 nRed, + Mode const mode, + const std::vector<ExtTextInputAttr> *pArr, + SwPosition const*const pExtInputStart) + : m_rDoc( rTextNd.GetDoc() ) + , m_rAttrHandler( rAH ) + , m_nNdIdx( rTextNd.GetIndex() ) + , m_nFirst( nRed ) + , m_nAct( SwRedlineTable::npos ) + , m_nStart( COMPLETE_STRING ) + , m_nEnd( COMPLETE_STRING ) + , m_bOn( false ) + , m_eMode( mode ) +{ + if( pArr ) + { + assert(pExtInputStart); + m_pExt.reset( new SwExtend(*pArr, pExtInputStart->GetNodeIndex(), + pExtInputStart->GetContentIndex()) ); + } + else + m_pExt = nullptr; + assert(m_pExt || m_eMode != Mode::Ignore); // only create if necessary + Seek(rFnt, m_nNdIdx, 0, COMPLETE_STRING); +} + +SwRedlineItr::~SwRedlineItr() COVERITY_NOEXCEPT_FALSE +{ + Clear( nullptr ); + m_pExt.reset(); +} + +// The return value of SwRedlineItr::Seek tells you if the current font +// has been manipulated by leaving (-1) or accessing (+1) of a section +short SwRedlineItr::Seek(SwFont& rFnt, + SwNodeOffset const nNode, sal_Int32 const nNew, sal_Int32 const nOld) +{ + short nRet = 0; + if( ExtOn() ) + return 0; // Abbreviation: if we're within an ExtendTextInputs + // there can't be other changes of attributes (not even by redlining) + if (m_eMode == Mode::Show) + { + if (m_bOn) + { + if (nNew >= m_nEnd) + { + --nRet; + Clear_( &rFnt ); // We go behind the current section + ++m_nAct; // and check the next one + } + else if (nNew < m_nStart) + { + --nRet; + Clear_( &rFnt ); // We go in front of the current section + if (m_nAct > m_nFirst) + m_nAct = m_nFirst; // the test has to run from the beginning + else + return nRet + EnterExtend(rFnt, nNode, nNew); // There's none prior to us + } + else + return nRet + EnterExtend(rFnt, nNode, nNew); // We stayed in the same section + } + if (SwRedlineTable::npos == m_nAct || nOld > nNew) + m_nAct = m_nFirst; + + m_nStart = COMPLETE_STRING; + m_nEnd = COMPLETE_STRING; + const SwRedlineTable& rTable = m_rDoc.getIDocumentRedlineAccess().GetRedlineTable(); + + for ( ; m_nAct < rTable.size() ; ++m_nAct) + { + rTable[ m_nAct ]->CalcStartEnd(nNode, m_nStart, m_nEnd); + + if (nNew < m_nEnd) + { + if (nNew >= m_nStart) // only possible candidate + { + m_bOn = true; + const SwRangeRedline *pRed = rTable[ m_nAct ]; + + if (m_pSet) + m_pSet->ClearItem(); + else + { + SwAttrPool& rPool = + const_cast<SwDoc&>(m_rDoc).GetAttrPool(); + m_pSet = std::make_unique<SfxItemSetFixed<RES_CHRATR_BEGIN, RES_CHRATR_END-1>>(rPool); + } + + if( 1 < pRed->GetStackCount() ) + FillHints( pRed->GetAuthor( 1 ), pRed->GetType( 1 ) ); + FillHints( pRed->GetAuthor(), pRed->GetType() ); + + SfxWhichIter aIter( *m_pSet ); + + // moved text: dark green with double underline or strikethrough + if ( pRed->IsMoved() ) + { + m_pSet->Put(SvxColorItem( COL_GREEN, RES_CHRATR_COLOR )); + if (SfxItemState::SET == m_pSet->GetItemState(RES_CHRATR_CROSSEDOUT, true)) + m_pSet->Put(SvxCrossedOutItem( STRIKEOUT_DOUBLE, RES_CHRATR_CROSSEDOUT )); + else + m_pSet->Put(SvxUnderlineItem( LINESTYLE_DOUBLE, RES_CHRATR_UNDERLINE )); + } + + sal_uInt16 nWhich = aIter.FirstWhich(); + while( nWhich ) + { + const SfxPoolItem* pItem; + if( ( nWhich < RES_CHRATR_END ) && + ( SfxItemState::SET == aIter.GetItemState( true, &pItem ) ) ) + { + SwTextAttr* pAttr = MakeRedlineTextAttr( + const_cast<SwDoc&>(m_rDoc), + *const_cast<SfxPoolItem*>(pItem) ); + pAttr->SetPriorityAttr( true ); + m_Hints.push_back(pAttr); + m_rAttrHandler.PushAndChg( *pAttr, rFnt ); + } + nWhich = aIter.NextWhich(); + } + + ++nRet; + } + break; + } + m_nStart = COMPLETE_STRING; + m_nEnd = COMPLETE_STRING; + } + } + else if (m_eMode == Mode::Hide) + { // ... just iterate to update m_nAct for GetNextRedln(); + // there is no need to care about formatting in this mode + if (m_nAct == SwRedlineTable::npos || nOld == COMPLETE_STRING) + { // reset, or move backward + m_nAct = m_nFirst; + } + for ( ; m_nAct < m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size(); ++m_nAct) + { // only Start matters in this mode + // Seeks until it finds a RL that starts at or behind the seek pos. + // - then update m_nStart/m_nEnd to the intersection of it with the + // current node (if any). + // The only way to skip to a different node is if there is a Delete + // RL, so if there is no intersection we'll never skip again. + // Note: here, assume that delete can't nest inside delete! + SwRangeRedline const*const pRedline( + m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[m_nAct]); + SwPosition const*const pStart(pRedline->Start()); + if (pRedline->GetType() == RedlineType::Delete + && (nNode < pStart->GetNodeIndex() + || (nNode == pStart->GetNodeIndex() + && nNew <= pStart->GetContentIndex()))) + { + pRedline->CalcStartEnd(nNode, m_nStart, m_nEnd); + break; + } + m_nStart = COMPLETE_STRING; + m_nEnd = COMPLETE_STRING; + } + } + return nRet + EnterExtend(rFnt, nNode, nNew); +} + +void SwRedlineItr::FillHints( std::size_t nAuthor, RedlineType eType ) +{ + switch ( eType ) + { + case RedlineType::Insert: + SW_MOD()->GetInsertAuthorAttr(nAuthor, *m_pSet); + break; + case RedlineType::Delete: + SW_MOD()->GetDeletedAuthorAttr(nAuthor, *m_pSet); + break; + case RedlineType::Format: + case RedlineType::FmtColl: + SW_MOD()->GetFormatAuthorAttr(nAuthor, *m_pSet); + break; + default: + break; + } +} + +void SwRedlineItr::ChangeTextAttr( SwFont* pFnt, SwTextAttr const &rHt, bool bChg ) +{ + OSL_ENSURE( IsOn(), "SwRedlineItr::ChangeTextAttr: Off?" ); + + if (m_eMode != Mode::Show && !m_pExt) + return; + + if( bChg ) + { + if (m_pExt && m_pExt->IsOn()) + m_rAttrHandler.PushAndChg( rHt, *m_pExt->GetFont() ); + else + m_rAttrHandler.PushAndChg( rHt, *pFnt ); + } + else + { + OSL_ENSURE( ! m_pExt || ! m_pExt->IsOn(), "Pop of attribute during opened extension" ); + m_rAttrHandler.PopAndChg( rHt, *pFnt ); + } +} + +void SwRedlineItr::Clear_( SwFont* pFnt ) +{ + OSL_ENSURE( m_bOn, "SwRedlineItr::Clear: Off?" ); + m_bOn = false; + for (auto const& hint : m_Hints) + { + if( pFnt ) + m_rAttrHandler.PopAndChg( *hint, *pFnt ); + else + m_rAttrHandler.Pop( *hint ); + SwTextAttr::Destroy(hint, const_cast<SwDoc&>(m_rDoc).GetAttrPool() ); + } + m_Hints.clear(); +} + +/// Ignore mode: does nothing. +/// Show mode: returns end of redline if currently in one, or start of next +/// Hide mode: returns start of next redline in current node, plus (if it's a +/// Delete) its end position and number of consecutive RLs +std::pair<sal_Int32, std::pair<SwRangeRedline const*, size_t>> +SwRedlineItr::GetNextRedln(sal_Int32 nNext, SwTextNode const*const pNode, + SwRedlineTable::size_type & rAct) +{ + sal_Int32 nStart(m_nStart); + sal_Int32 nEnd(m_nEnd); + nNext = NextExtend(pNode->GetIndex(), nNext); + if (m_eMode == Mode::Ignore || SwRedlineTable::npos == m_nFirst) + return std::make_pair(nNext, std::make_pair(nullptr, 0)); + if (SwRedlineTable::npos == rAct) + { + rAct = m_nFirst; + } + if (rAct != m_nAct) + { + while (rAct < m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size()) + { + SwRangeRedline const*const pRedline( + m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[rAct]); + pRedline->CalcStartEnd(pNode->GetIndex(), nStart, nEnd); + if (m_eMode != Mode::Hide + || pRedline->GetType() == RedlineType::Delete) + { + break; + } + ++rAct; // Hide mode: search a Delete RL + } + } + if (rAct == m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size()) + { + return std::make_pair(nNext, std::make_pair(nullptr, 0)); // no Delete here + } + if (m_bOn || (m_eMode == Mode::Show && nStart == 0)) + { // in Ignore mode, the end of redlines isn't relevant, except as returned in the second in the pair! + if (nEnd < nNext) + nNext = nEnd; + } + else if (nStart <= nNext) + { + if (m_eMode == Mode::Show) + { + nNext = nStart; + } + else + { + assert(m_eMode == Mode::Hide); + SwRangeRedline const* pRedline( + m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[rAct]); + assert(pRedline->GetType() == RedlineType::Delete); //? + if (pRedline->GetType() == RedlineType::Delete) + { + nNext = nStart; + size_t nSkipped(1); // (consecutive) candidates to be skipped + while (rAct + nSkipped < + m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size()) + { + SwRangeRedline const*const pNext = + m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[rAct + nSkipped]; + if (*pRedline->End() < *pNext->Start()) + { + break; // done for now + } + else if (*pNext->Start() == *pRedline->End() && + pNext->GetType() == RedlineType::Delete) + { + // consecutive delete - continue + pRedline = pNext; + } + ++nSkipped; + } + return std::make_pair(nNext, std::make_pair(pRedline, nSkipped)); + } + } + } + return std::make_pair(nNext, std::make_pair(nullptr, 0)); +} + +bool SwRedlineItr::ChkSpecialUnderline_() const +{ + // If the underlining or the escapement is caused by redlining, + // we always apply the SpecialUnderlining, i.e. the underlining + // below the base line + for (SwTextAttr* pHint : m_Hints) + { + const sal_uInt16 nWhich = pHint->Which(); + if( RES_CHRATR_UNDERLINE == nWhich || + RES_CHRATR_ESCAPEMENT == nWhich ) + return true; + } + return false; +} + +bool SwRedlineItr::CheckLine( + SwNodeOffset const nStartNode, sal_Int32 const nChkStart, + SwNodeOffset const nEndNode, sal_Int32 nChkEnd, OUString& rRedlineText, + bool& bRedlineEnd, RedlineType& eRedlineEnd, size_t* pAuthorAtPos) +{ + // note: previously this would return true in the (!m_bShow && m_pExt) + // case, but surely that was a bug? + if (m_nFirst == SwRedlineTable::npos || m_eMode != Mode::Show) + return false; + if( nChkEnd == nChkStart && pAuthorAtPos == nullptr ) // empty lines look one char further + ++nChkEnd; + sal_Int32 nOldStart = m_nStart; + sal_Int32 nOldEnd = m_nEnd; + SwRedlineTable::size_type const nOldAct = m_nAct; + bool bRet = bRedlineEnd = false; + eRedlineEnd = RedlineType::None; + + SwPosition const start(*m_rDoc.GetNodes()[nStartNode]->GetContentNode(), nChkStart); + SwPosition const end(*m_rDoc.GetNodes()[nEndNode]->GetContentNode(), nChkEnd); + SwRangeRedline const* pPrevRedline = nullptr; + bool isBreak(false); + for (m_nAct = m_nFirst; m_nAct < m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size(); ++m_nAct) + { + SwRangeRedline const*const pRedline( + m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[ m_nAct ] ); + // collect text of the hidden redlines at the end of the line + bool isExtendText(false); + switch (ComparePosition(*pRedline->Start(), *pRedline->End(), start, end)) + { + case SwComparePosition::Behind: + isBreak = true; + break; + case SwComparePosition::OverlapBehind: + case SwComparePosition::CollideStart: + case SwComparePosition::Outside: + case SwComparePosition::Equal: + // store redlining at line end (for line break formatting) + eRedlineEnd = pRedline->GetType(); + bRedlineEnd = true; + isBreak = true; + if (pAuthorAtPos) + *pAuthorAtPos = pRedline->GetAuthor(); + [[fallthrough]]; + case SwComparePosition::OverlapBefore: + case SwComparePosition::CollideEnd: + case SwComparePosition::Inside: + { + bRet = true; + // start to collect text of invisible redlines for ChangesInMargin layout + if (rRedlineText.isEmpty() && !pRedline->IsVisible()) + { + rRedlineText = const_cast<SwRangeRedline*>(pRedline)->GetDescr(/*bSimplified=*/true); + pPrevRedline = pRedline; + isExtendText = true; + } + // join the text of the next invisible redlines in the same position + // i.e. characters deleted by pressing backspace or delete + else if (pPrevRedline && !pRedline->IsVisible() && + *pRedline->Start() == *pPrevRedline->Start() && *pRedline->End() == *pPrevRedline->End() ) + { + OUString sExtendText(const_cast<SwRangeRedline*>(pRedline)->GetDescr(/*bSimplified=*/true)); + if (!sExtendText.isEmpty()) + { + if (rRedlineText.getLength() < 12) + { + // TODO: remove extra space from GetDescr(true), + // but show deletion of paragraph or line break + rRedlineText = rRedlineText + + const_cast<SwRangeRedline*>(pRedline)->GetDescr(/*bSimplified=*/true).subView(1); + } + else + rRedlineText = OUString::Concat(rRedlineText.subView(0, rRedlineText.getLength() - 3)) + "..."; + } + isExtendText = true; + } + break; + } + case SwComparePosition::Before: + break; // -Werror=switch + } + if (isBreak && !isExtendText) + { + break; + } + } + + m_nStart = nOldStart; + m_nEnd = nOldEnd; + m_nAct = nOldAct; + return bRet; +} + +void SwExtend::ActualizeFont( SwFont &rFnt, ExtTextInputAttr nAttr ) +{ + if ( nAttr & ExtTextInputAttr::Underline ) + rFnt.SetUnderline( LINESTYLE_SINGLE ); + else if ( nAttr & ExtTextInputAttr::DoubleUnderline ) + rFnt.SetUnderline( LINESTYLE_DOUBLE ); + else if ( nAttr & ExtTextInputAttr::BoldUnderline ) + rFnt.SetUnderline( LINESTYLE_BOLD ); + else if ( nAttr & ExtTextInputAttr::DottedUnderline ) + rFnt.SetUnderline( LINESTYLE_DOTTED ); + else if ( nAttr & ExtTextInputAttr::DashDotUnderline ) + rFnt.SetUnderline( LINESTYLE_DOTTED ); + + if ( nAttr & ExtTextInputAttr::RedText ) + rFnt.SetColor( COL_RED ); + + if ( nAttr & ExtTextInputAttr::Highlight ) + { + const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings(); + rFnt.SetColor( rStyleSettings.GetHighlightTextColor() ); + rFnt.SetBackColor( rStyleSettings.GetHighlightColor() ); + } + if ( nAttr & ExtTextInputAttr::GrayWaveline ) + rFnt.SetGreyWave( true ); +} + +short SwExtend::Enter(SwFont& rFnt, SwNodeOffset const nNode, sal_Int32 const nNew) +{ + OSL_ENSURE( !m_pFont, "SwExtend: Enter with Font" ); + if (nNode != m_nNode) + return 0; + OSL_ENSURE( !Inside(), "SwExtend: Enter without Leave" ); + m_nPos = nNew; + if( Inside() ) + { + m_pFont.reset( new SwFont(rFnt) ); + ActualizeFont( rFnt, m_rArr[m_nPos - m_nStart] ); + return 1; + } + return 0; +} + +bool SwExtend::Leave_(SwFont& rFnt, SwNodeOffset const nNode, sal_Int32 const nNew) +{ + OSL_ENSURE(nNode == m_nNode && Inside(), "SwExtend: Leave without Enter"); + if (nNode != m_nNode) + return true; + const ExtTextInputAttr nOldAttr = m_rArr[m_nPos - m_nStart]; + m_nPos = nNew; + if( Inside() ) + { // We stayed within the ExtendText-section + const ExtTextInputAttr nAttr = m_rArr[m_nPos - m_nStart]; + if( nOldAttr != nAttr ) // Is there an (inner) change of attributes? + { + rFnt = *m_pFont; + ActualizeFont( rFnt, nAttr ); + } + } + else + { + rFnt = *m_pFont; + m_pFont.reset(); + return true; + } + return false; +} + +sal_Int32 SwExtend::Next(SwNodeOffset const nNode, sal_Int32 nNext) +{ + if (nNode != m_nNode) + return nNext; + if (m_nPos < m_nStart) + { + if (nNext > m_nStart) + nNext = m_nStart; + } + else if (m_nPos < m_nEnd) + { + sal_Int32 nIdx = m_nPos - m_nStart; + const ExtTextInputAttr nAttr = m_rArr[ nIdx ]; + while (o3tl::make_unsigned(++nIdx) < m_rArr.size() && nAttr == m_rArr[nIdx]) + ; //nothing + nIdx = nIdx + m_nStart; + if( nNext > nIdx ) + nNext = nIdx; + } + return nNext; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/redlnitr.hxx b/sw/source/core/text/redlnitr.hxx new file mode 100644 index 0000000000..5c30131264 --- /dev/null +++ b/sw/source/core/text/redlnitr.hxx @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <docary.hxx> +#include <swfont.hxx> + +#include <vcl/commandevent.hxx> + +#include <cstddef> +#include <deque> +#include <memory> +#include <vector> + +class SwTextNode; +class SwDoc; +class SfxItemSet; +class SwAttrHandler; + +class SwExtend +{ + std::unique_ptr<SwFont> m_pFont; + const std::vector<ExtTextInputAttr> &m_rArr; + /// position of start of SwExtTextInput + SwNodeOffset const m_nNode; + sal_Int32 const m_nStart; + /// current position (inside) + sal_Int32 m_nPos; + /// position of end of SwExtTextInput (in same node as start) + sal_Int32 const m_nEnd; + bool Leave_(SwFont& rFnt, SwNodeOffset nNode, sal_Int32 nNew); + bool Inside() const { return (m_nPos >= m_nStart && m_nPos < m_nEnd); } + static void ActualizeFont( SwFont &rFnt, ExtTextInputAttr nAttr ); +public: + SwExtend(const std::vector<ExtTextInputAttr> &rArr, + SwNodeOffset const nNode, sal_Int32 const nStart) + : m_rArr(rArr) + , m_nNode(nNode) + , m_nStart(nStart) + , m_nPos(COMPLETE_STRING) + , m_nEnd(m_nStart + rArr.size()) + {} + bool IsOn() const { return m_pFont != nullptr; } + void Reset() { m_pFont.reset(); m_nPos = COMPLETE_STRING; } + bool Leave(SwFont& rFnt, SwNodeOffset const nNode, sal_Int32 const nNew) + { return m_pFont && Leave_(rFnt, nNode, nNew); } + short Enter(SwFont& rFnt, SwNodeOffset nNode, sal_Int32 nNew); + sal_Int32 Next(SwNodeOffset nNode, sal_Int32 nNext); + SwFont* GetFont() { return m_pFont.get(); } + void UpdateFont(SwFont &rFont) { ActualizeFont(rFont, m_rArr[m_nPos - m_nStart]); } +}; + +class SwRedlineItr +{ + std::deque<SwTextAttr *> m_Hints; + const SwDoc& m_rDoc; + SwAttrHandler& m_rAttrHandler; + std::unique_ptr<SfxItemSet> m_pSet; + std::unique_ptr<SwExtend> m_pExt; + // note: this isn't actually used in the merged-para (Hide) case + SwNodeOffset const m_nNdIdx; + SwRedlineTable::size_type const m_nFirst; + SwRedlineTable::size_type m_nAct; + sal_Int32 m_nStart; + sal_Int32 m_nEnd; + bool m_bOn; +public: + enum class Mode { Show, Ignore, Hide }; +private: + Mode const m_eMode; + + void Clear_( SwFont* pFnt ); + bool ChkSpecialUnderline_() const; + void FillHints( std::size_t nAuthor, RedlineType eType ); + short EnterExtend(SwFont& rFnt, SwNodeOffset const nNode, sal_Int32 const nNew) + { + if (m_pExt) return m_pExt->Enter(rFnt, nNode, nNew); + return 0; + } + sal_Int32 NextExtend(SwNodeOffset const nNode, sal_Int32 const nNext) { + if (m_pExt) return m_pExt->Next(nNode, nNext); + return nNext; + } +public: + SwRedlineItr( const SwTextNode& rTextNd, SwFont& rFnt, SwAttrHandler& rAH, + sal_Int32 nRedlPos, Mode mode, + const std::vector<ExtTextInputAttr> *pArr = nullptr, + SwPosition const* pExtInputStart = nullptr); + ~SwRedlineItr() COVERITY_NOEXCEPT_FALSE; + SwRedlineTable::size_type GetAct() const { return m_nAct; } + bool IsOn() const { return m_bOn || (m_pExt && m_pExt->IsOn()); } + void Clear( SwFont* pFnt ) { if (m_bOn) Clear_( pFnt ); } + void ChangeTextAttr( SwFont* pFnt, SwTextAttr const &rHt, bool bChg ); + short Seek(SwFont& rFnt, SwNodeOffset nNode, sal_Int32 nNew, sal_Int32 nOld); + void Reset() { + if (m_nAct != m_nFirst) m_nAct = SwRedlineTable::npos; + if (m_pExt) m_pExt->Reset(); + } + std::pair<sal_Int32, std::pair<SwRangeRedline const*, size_t>> GetNextRedln( + sal_Int32 nNext, SwTextNode const* pNode, SwRedlineTable::size_type & rAct); + bool ChkSpecialUnderline() const + { return IsOn() && ChkSpecialUnderline_(); } + bool CheckLine(SwNodeOffset nStartNode, sal_Int32 nChkStart, SwNodeOffset nEndNode, + sal_Int32 nChkEnd, OUString& rRedlineText, bool& bRedlineEnd, + RedlineType& eRedlineEnd, size_t* pAuthorAtPos = nullptr); + bool LeaveExtend(SwFont& rFnt, SwNodeOffset const nNode, sal_Int32 const nNew) + { return m_pExt->Leave(rFnt, nNode, nNew); } + bool ExtOn() { + if (m_pExt) return m_pExt->IsOn(); + return false; + } + void UpdateExtFont( SwFont &rFnt ) { + OSL_ENSURE( ExtOn(), "UpdateExtFont without ExtOn" ); + m_pExt->UpdateFont( rFnt ); + } +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtcache.cxx b/sw/source/core/text/txtcache.cxx new file mode 100644 index 0000000000..188fa7bec6 --- /dev/null +++ b/sw/source/core/text/txtcache.cxx @@ -0,0 +1,193 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "txtcache.hxx" +#include <txtfrm.hxx> +#include "porlay.hxx" + +#include <sfx2/viewsh.hxx> +#include <osl/diagnose.h> +#include <view.hxx> + +SwTextLine::SwTextLine( SwTextFrame const *pFrame, std::unique_ptr<SwParaPortion> pNew ) : + SwCacheObj( static_cast<void const *>(pFrame) ), + m_pLine( std::move(pNew) ) +{ +} + +SwTextLine::~SwTextLine() +{ +} + +void SwTextLine::UpdateCachePos() +{ + // note: SwTextFrame lives longer than its SwTextLine, see ~SwTextFrame + assert(m_pOwner); + const_cast<SwTextFrame *>(static_cast<SwTextFrame const *>(m_pOwner))->SetCacheIdx(GetCachePos()); +} + +SwCacheObj *SwTextLineAccess::NewObj() +{ + return new SwTextLine( static_cast<SwTextFrame const *>(m_pOwner) ); +} + +SwParaPortion *SwTextLineAccess::GetPara() +{ + SwTextLine *pRet; + if ( m_pObj ) + pRet = static_cast<SwTextLine*>(m_pObj); + else + { + pRet = static_cast<SwTextLine*>(Get(false)); + const_cast<SwTextFrame *>(static_cast<SwTextFrame const *>(m_pOwner))->SetCacheIdx( pRet->GetCachePos() ); + } + if ( !pRet->GetPara() ) + pRet->SetPara( new SwParaPortion, true/*bDelete*/ ); + return pRet->GetPara(); +} + +SwTextLineAccess::SwTextLineAccess( const SwTextFrame *pOwn ) : + SwCacheAccess( *SwTextFrame::GetTextCache(), pOwn, pOwn->GetCacheIdx() ) +{ +} + +bool SwTextLineAccess::IsAvailable() const +{ + return m_pObj && static_cast<SwTextLine*>(m_pObj)->GetPara(); +} + +bool SwTextFrame::HasPara_() const +{ + SwTextLine *pTextLine = static_cast<SwTextLine*>(SwTextFrame::GetTextCache()-> + Get( this, GetCacheIdx(), false )); + if ( pTextLine ) + { + if ( pTextLine->GetPara() ) + return true; + } + else + const_cast<SwTextFrame*>(this)->mnCacheIndex = USHRT_MAX; + + return false; +} + +SwParaPortion *SwTextFrame::GetPara() +{ + if ( GetCacheIdx() != USHRT_MAX ) + { + SwTextLine *pLine = static_cast<SwTextLine*>(SwTextFrame::GetTextCache()-> + Get( this, GetCacheIdx(), false )); + if ( pLine ) + return pLine->GetPara(); + else + mnCacheIndex = USHRT_MAX; + } + return nullptr; +} + +void SwTextFrame::ClearPara() +{ + OSL_ENSURE( !IsLocked(), "+SwTextFrame::ClearPara: this is locked." ); + if ( !IsLocked() && GetCacheIdx() != USHRT_MAX ) + { + SwTextLine *pTextLine = static_cast<SwTextLine*>(SwTextFrame::GetTextCache()-> + Get( this, GetCacheIdx(), false )); + if ( pTextLine ) + { + pTextLine->SetPara( nullptr, true/*bDelete*/ ); + } + else + mnCacheIndex = USHRT_MAX; + } +} + +void SwTextFrame::RemoveFromCache() +{ + if (GetCacheIdx() != USHRT_MAX) + { + s_pTextCache->Delete(this, GetCacheIdx()); + SetCacheIdx(USHRT_MAX); + } +} + +void SwTextFrame::SetPara( SwParaPortion *pNew, bool bDelete ) +{ + if ( GetCacheIdx() != USHRT_MAX ) + { + // Only change the information, the CacheObj stays there + SwTextLine *pTextLine = static_cast<SwTextLine*>(SwTextFrame::GetTextCache()-> + Get( this, GetCacheIdx(), false )); + if ( pTextLine ) + { + pTextLine->SetPara( pNew, bDelete ); + } + else + { + OSL_ENSURE( !pNew, "+SetPara: Losing SwParaPortion" ); + mnCacheIndex = USHRT_MAX; + } + } + else if ( pNew ) + { // Insert a new one + SwTextLine *pTextLine = new SwTextLine( this, std::unique_ptr<SwParaPortion>(pNew) ); + if (SwTextFrame::GetTextCache()->Insert(pTextLine, false)) + mnCacheIndex = pTextLine->GetCachePos(); + else + { + OSL_FAIL( "+SetPara: InsertCache failed." ); + } + } +} + +/** Prevent the SwParaPortions of the *visible* paragraphs from being deleted; + they would just be recreated on the next paint. + + Heuristic: 100 per view are visible + + If the cache is too small, enlarge it to ensure there are sufficient free + entries for the layout so it doesn't have to throw away a node's + SwParaPortion when it starts formatting the next node. +*/ +SwSaveSetLRUOfst::SwSaveSetLRUOfst() +{ + sal_uInt16 nVisibleShells(0); + for (auto pView = SfxViewShell::GetFirst(true, checkSfxViewShell<SwView>); + pView != nullptr; + pView = SfxViewShell::GetNext(*pView, true, checkSfxViewShell<SwView>)) + { + // Apparently we are not interested here what document pView is for, but only in the + // total number of shells in the process? + ++nVisibleShells; + } + + sal_uInt16 const nPreserved(100 * nVisibleShells); + SwCache & rCache(*SwTextFrame::GetTextCache()); + if (rCache.GetCurMax() < nPreserved + 250) + { + rCache.IncreaseMax(nPreserved + 250 - rCache.GetCurMax()); + } + rCache.SetLRUOfst(nPreserved); +} + +SwSaveSetLRUOfst::~SwSaveSetLRUOfst() +{ + SwTextFrame::GetTextCache()->ResetLRUOfst(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtcache.hxx b/sw/source/core/text/txtcache.hxx new file mode 100644 index 0000000000..ee18f14c3b --- /dev/null +++ b/sw/source/core/text/txtcache.hxx @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <swcache.hxx> +#include "porlay.hxx" +#include <memory> + +class SwTextFrame; + +class SwTextLine : public SwCacheObj +{ + std::unique_ptr<SwParaPortion> m_pLine; + + virtual void UpdateCachePos() override; + +public: + SwTextLine(SwTextFrame const* pFrame, std::unique_ptr<SwParaPortion> pNew = nullptr); + virtual ~SwTextLine() override; + + SwParaPortion* GetPara() { return m_pLine.get(); } + const SwParaPortion* GetPara() const { return m_pLine.get(); } + + void SetPara(SwParaPortion* pNew, bool bDelete) + { + if (!bDelete) + { + // coverity[leaked_storage] - intentional, ownership transferred + (void)m_pLine.release(); + } + m_pLine.reset(pNew); + } +}; + +class SwTextLineAccess : public SwCacheAccess +{ +protected: + virtual SwCacheObj* NewObj() override; + +public: + explicit SwTextLineAccess(const SwTextFrame* pOwner); + + SwParaPortion* GetPara(); + + bool IsAvailable() const; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtdrop.cxx b/sw/source/core/text/txtdrop.cxx new file mode 100644 index 0000000000..afbe3b1470 --- /dev/null +++ b/sw/source/core/text/txtdrop.cxx @@ -0,0 +1,1093 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <hintids.hxx> +#include <vcl/svapp.hxx> +#include <paratr.hxx> +#include <txtfrm.hxx> +#include <charfmt.hxx> +#include <viewopt.hxx> +#include <viewsh.hxx> +#include "pordrop.hxx" +#include "itrform2.hxx" +#include "txtpaint.hxx" +#include <breakit.hxx> +#include <com/sun/star/i18n/ScriptType.hpp> +#include <com/sun/star/i18n/WordType.hpp> +#include <com/sun/star/i18n/XBreakIterator.hpp> +#include <editeng/langitem.hxx> +#include <charatr.hxx> +#include <editeng/fhgtitem.hxx> +#include <calbck.hxx> +#include <doc.hxx> +#include <IDocumentSettingAccess.hxx> + +using namespace ::com::sun::star::i18n; +using namespace ::com::sun::star; + +/** + * Calculates if a drop caps portion intersects with a fly + * The width and height of the drop caps portion are passed as arguments, + * the position is calculated from the values in rInf + */ +static bool lcl_IsDropFlyInter( const SwTextFormatInfo &rInf, + sal_uInt16 nWidth, sal_uInt16 nHeight ) +{ + const SwTextFly& rTextFly = rInf.GetTextFly(); + if( rTextFly.IsOn() ) + { + SwRect aRect( rInf.GetTextFrame()->getFrameArea().Pos(), Size( nWidth, nHeight) ); + aRect.Pos() += rInf.GetTextFrame()->getFramePrintArea().Pos(); + aRect.Pos().AdjustX(rInf.X() ); + aRect.Pos().setY( rInf.Y() ); + aRect = rTextFly.GetFrame( aRect ); + return aRect.HasArea(); + } + + return false; +} + +namespace { + +class SwDropSave +{ + SwTextPaintInfo* pInf; + sal_Int32 nIdx; + sal_Int32 nLen; + tools::Long nX; + tools::Long nY; + +public: + explicit SwDropSave( const SwTextPaintInfo &rInf ); + ~SwDropSave(); +}; + +} + +SwDropSave::SwDropSave( const SwTextPaintInfo &rInf ) : + pInf( const_cast<SwTextPaintInfo*>(&rInf) ), nIdx( rInf.GetIdx() ), + nLen( rInf.GetLen() ), nX( rInf.X() ), nY( rInf.Y() ) +{ +} + +SwDropSave::~SwDropSave() +{ + pInf->SetIdx(TextFrameIndex(nIdx)); + pInf->SetLen(TextFrameIndex(nLen)); + pInf->X( nX ); + pInf->Y( nY ); +} + +/// SwDropPortionPart DTor +SwDropPortionPart::~SwDropPortionPart() +{ + m_pFollow.reset(); + m_pFnt.reset(); +} + +/// SwDropPortion CTor, DTor +SwDropPortion::SwDropPortion( const sal_uInt16 nLineCnt, + const sal_uInt16 nDrpHeight, + const sal_uInt16 nDrpDescent, + const sal_uInt16 nDist ) + : m_nLines( nLineCnt ), + m_nDropHeight(nDrpHeight), + m_nDropDescent(nDrpDescent), + m_nDistance(nDist), + m_nFix(0), + m_nY(0) +{ + SetWhichPor( PortionType::Drop ); +} + +SwDropPortion::~SwDropPortion() +{ + m_pPart.reset(); +} + +/// nWishLen = 0 indicates that we want a whole word +sal_Int32 SwTextNode::GetDropLen( sal_Int32 nWishLen ) const +{ + sal_Int32 nEnd = GetText().getLength(); + if( nWishLen && nWishLen < nEnd ) + nEnd = nWishLen; + + if (! nWishLen) + { + // find first word + const SwAttrSet& rAttrSet = GetSwAttrSet(); + const sal_uInt16 nTextScript = g_pBreakIt->GetRealScriptOfText( GetText(), 0 ); + + LanguageType eLanguage; + + switch ( nTextScript ) + { + case i18n::ScriptType::ASIAN : + eLanguage = rAttrSet.GetCJKLanguage().GetLanguage(); + break; + case i18n::ScriptType::COMPLEX : + eLanguage = rAttrSet.GetCTLLanguage().GetLanguage(); + break; + default : + eLanguage = rAttrSet.GetLanguage().GetLanguage(); + break; + } + + Boundary aBound = + g_pBreakIt->GetBreakIter()->getWordBoundary( GetText(), 0, + g_pBreakIt->GetLocale( eLanguage ), WordType::DICTIONARY_WORD, true ); + + nEnd = aBound.endPos; + } + + sal_Int32 i = 0; + for( ; i < nEnd; ++i ) + { + sal_Unicode const cChar = GetText()[i]; + if( CH_TAB == cChar || CH_BREAK == cChar || + (( CH_TXTATR_BREAKWORD == cChar || CH_TXTATR_INWORD == cChar ) + && GetTextAttrForCharAt(i)) ) + break; + } + return i; +} + +/// nWishLen = 0 indicates that we want a whole word +TextFrameIndex SwTextFrame::GetDropLen(TextFrameIndex const nWishLen) const +{ + TextFrameIndex nEnd(GetText().getLength()); + if (nWishLen && nWishLen < nEnd) + nEnd = nWishLen; + + if (! nWishLen) + { + // find first word + const SwAttrSet& rAttrSet = GetTextNodeForParaProps()->GetSwAttrSet(); + const sal_uInt16 nTextScript = g_pBreakIt->GetRealScriptOfText(GetText(), 0); + + LanguageType eLanguage; + + switch ( nTextScript ) + { + case i18n::ScriptType::ASIAN : + eLanguage = rAttrSet.GetCJKLanguage().GetLanguage(); + break; + case i18n::ScriptType::COMPLEX : + eLanguage = rAttrSet.GetCTLLanguage().GetLanguage(); + break; + default : + eLanguage = rAttrSet.GetLanguage().GetLanguage(); + break; + } + + Boundary aBound = g_pBreakIt->GetBreakIter()->getWordBoundary( + GetText(), 0, g_pBreakIt->GetLocale(eLanguage), + WordType::DICTIONARY_WORD, true ); + + nEnd = TextFrameIndex(aBound.endPos); + } + + TextFrameIndex i(0); + for ( ; i < nEnd; ++i) + { + sal_Unicode const cChar = GetText()[sal_Int32(i)]; + if (CH_TAB == cChar || CH_BREAK == cChar || + CH_TXTATR_BREAKWORD == cChar || CH_TXTATR_INWORD == cChar) + { +#ifndef NDEBUG + if (CH_TXTATR_BREAKWORD == cChar || CH_TXTATR_INWORD == cChar) + { + std::pair<SwTextNode const*, sal_Int32> const pos(MapViewToModel(i)); + assert(pos.first->GetTextAttrForCharAt(pos.second) != nullptr); + } +#endif + break; + } + } + return i; +} + +/** + * If a dropcap is found the return value is true otherwise false. The + * drop cap sizes passed back by reference are font height, drop height + * and drop descent. + */ +bool SwTextNode::GetDropSize(int& rFontHeight, int& rDropHeight, int& rDropDescent) const +{ + rFontHeight = 0; + rDropHeight = 0; + rDropDescent =0; + + const SwAttrSet& rSet = GetSwAttrSet(); + const SwFormatDrop& rDrop = rSet.GetDrop(); + + // Return (0,0) if there is no drop cap at this paragraph + if( 1 >= rDrop.GetLines() || + ( !rDrop.GetChars() && !rDrop.GetWholeWord() ) ) + { + return false; + } + + // get text frame + SwIterator<SwTextFrame, SwTextNode, sw::IteratorMode::UnwrapMulti> aIter(*this); + for( SwTextFrame* pLastFrame = aIter.First(); pLastFrame; pLastFrame = aIter.Next() ) + { + // Only (master-) text frames can have a drop cap. + if (!pLastFrame->IsFollow() && + pLastFrame->GetTextNodeForFirstText() == this) + { + + if( !pLastFrame->HasPara() ) + pLastFrame->GetFormatted(); + + if ( !pLastFrame->IsEmpty() ) + { + const SwParaPortion* pPara = pLastFrame->GetPara(); + OSL_ENSURE( pPara, "GetDropSize could not find the ParaPortion, I'll guess the drop cap size" ); + + if ( pPara ) + { + const SwLinePortion* pFirstPor = pPara->GetFirstPortion(); + if (pFirstPor && pFirstPor->IsDropPortion()) + { + const SwDropPortion* pDrop = static_cast<const SwDropPortion*>(pFirstPor); + rDropHeight = pDrop->GetDropHeight(); + rDropDescent = pDrop->GetDropDescent(); + if (const SwFont *pFont = pDrop->GetFnt()) + rFontHeight = pFont->GetSize(pFont->GetActual()).Height(); + else + { + const SvxFontHeightItem& rItem = rSet.Get(RES_CHRATR_FONTSIZE); + rFontHeight = rItem.GetHeight(); + } + } + } + } + break; + } + } + + if (rFontHeight==0 && rDropHeight==0 && rDropDescent==0) + { + const sal_uInt16 nLines = rDrop.GetLines(); + + const SvxFontHeightItem& rItem = rSet.Get( RES_CHRATR_FONTSIZE ); + rFontHeight = rItem.GetHeight(); + rDropHeight = nLines * rFontHeight; + rDropDescent = rFontHeight / 5; + return false; + } + + return true; +} + +/// Manipulate the width, otherwise the chars are being stretched +void SwDropPortion::PaintText( const SwTextPaintInfo &rInf ) const +{ + OSL_ENSURE( m_nDropHeight && m_pPart && m_nLines != 1, "Drop Portion painted twice" ); + + const SwDropPortionPart* pCurrPart = GetPart(); + const TextFrameIndex nOldLen = GetLen(); + const sal_uInt16 nOldWidth = Width(); + const sal_uInt16 nOldAscent = GetAscent(); + + const SwTwips nBasePosY = rInf.Y(); + const_cast<SwTextPaintInfo&>(rInf).Y( nBasePosY + m_nY ); + const_cast<SwDropPortion*>(this)->SetAscent( nOldAscent + m_nY ); + SwDropSave aSave( rInf ); + // for text inside drop portions we let vcl handle the text directions + SwLayoutModeModifier aLayoutModeModifier( *rInf.GetOut() ); + aLayoutModeModifier.SetAuto(); + + while ( pCurrPart ) + { + const_cast<SwDropPortion*>(this)->SetLen( pCurrPart->GetLen() ); + const_cast<SwDropPortion*>(this)->Width( pCurrPart->GetWidth() ); + const_cast<SwTextPaintInfo&>(rInf).SetLen( pCurrPart->GetLen() ); + SwFontSave aFontSave( rInf, &pCurrPart->GetFont() ); + const_cast<SwDropPortion*>(this)->SetJoinBorderWithNext(pCurrPart->GetJoinBorderWithNext()); + const_cast<SwDropPortion*>(this)->SetJoinBorderWithPrev(pCurrPart->GetJoinBorderWithPrev()); + + if ( rInf.OnWin() && + !rInf.GetOpt().IsPagePreview() && !rInf.GetOpt().IsReadonly() && rInf.GetOpt().IsFieldShadings() && + (!pCurrPart->GetFont().GetBackColor() || *pCurrPart->GetFont().GetBackColor() == COL_TRANSPARENT) ) + { + rInf.DrawBackground( *this ); + } + + SwTextPortion::Paint( rInf ); + + const_cast<SwTextPaintInfo&>(rInf).SetIdx( rInf.GetIdx() + pCurrPart->GetLen() ); + const_cast<SwTextPaintInfo&>(rInf).X( rInf.X() + pCurrPart->GetWidth() ); + pCurrPart = pCurrPart->GetFollow(); + } + + const_cast<SwTextPaintInfo&>(rInf).Y( nBasePosY ); + const_cast<SwDropPortion*>(this)->Width( nOldWidth ); + const_cast<SwDropPortion*>(this)->SetLen( nOldLen ); + const_cast<SwDropPortion*>(this)->SetAscent( nOldAscent ); + const_cast<SwDropPortion*>(this)->SetJoinBorderWithNext(false); + const_cast<SwDropPortion*>(this)->SetJoinBorderWithPrev(false); +} + +void SwDropPortion::PaintDrop( const SwTextPaintInfo &rInf ) const +{ + // normal output is being done during the normal painting + if( ! m_nDropHeight || ! m_pPart || m_nLines == 1 ) + return; + + // set the lying values + const sal_uInt16 nOldHeight = Height(); + const sal_uInt16 nOldWidth = Width(); + const sal_uInt16 nOldAscent = GetAscent(); + const SwTwips nOldPosY = rInf.Y(); + const SwTwips nOldPosX = rInf.X(); + const SwParaPortion *pPara = rInf.GetParaPortion(); + const Point aOutPos( nOldPosX, nOldPosY - pPara->GetAscent() + - pPara->GetRealHeight() + pPara->Height() ); + // make good for retouching + + // Set baseline + const_cast<SwTextPaintInfo&>(rInf).Y( aOutPos.Y() + m_nDropHeight ); + + // for background + const_cast<SwDropPortion*>(this)->Height( m_nDropHeight + m_nDropDescent ); + const_cast<SwDropPortion*>(this)->SetAscent( m_nDropHeight ); + + // Always adapt Clipregion to us, never set it off using the existing ClipRect + // as that could be set for the line + SwRect aClipRect; + if ( rInf.OnWin() ) + { + aClipRect = SwRect( aOutPos, SvLSize() ); + aClipRect.Intersection( rInf.GetPaintRect() ); + } + SwSaveClip aClip( const_cast<OutputDevice*>(rInf.GetOut()) ); + aClip.ChgClip( aClipRect, rInf.GetTextFrame() ); + + // Just do, what we always do ... + PaintText( rInf ); + + // save old values + const_cast<SwDropPortion*>(this)->Height( nOldHeight ); + const_cast<SwDropPortion*>(this)->Width( nOldWidth ); + const_cast<SwDropPortion*>(this)->SetAscent( nOldAscent ); + const_cast<SwTextPaintInfo&>(rInf).Y( nOldPosY ); +} + +void SwDropPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + // normal output is being done here + if( !(! m_nDropHeight || ! m_pPart || 1 == m_nLines) ) + return; + + if ( rInf.OnWin() && + !rInf.GetOpt().IsPagePreview() && !rInf.GetOpt().IsReadonly() && rInf.GetOpt().IsFieldShadings() ) + rInf.DrawBackground( *this ); + + // make sure that font is not rotated + std::unique_ptr<SwFont> pTmpFont; + if ( rInf.GetFont()->GetOrientation( rInf.GetTextFrame()->IsVertical() ) ) + { + pTmpFont.reset(new SwFont( *rInf.GetFont() )); + pTmpFont->SetVertical( 0_deg10, rInf.GetTextFrame()->IsVertical() ); + } + + SwFontSave aFontSave( rInf, pTmpFont.get() ); + // for text inside drop portions we let vcl handle the text directions + SwLayoutModeModifier aLayoutModeModifier( *rInf.GetOut() ); + aLayoutModeModifier.SetAuto(); + + SwTextPortion::Paint( rInf ); +} + +bool SwDropPortion::FormatText( SwTextFormatInfo &rInf ) +{ + const TextFrameIndex nOldLen = GetLen(); + const TextFrameIndex nOldInfLen = rInf.GetLen(); + if (!SwTextPortion::Format( rInf )) + return false; + + // looks like shit, but what can we do? + rInf.SetUnderflow( nullptr ); + Truncate(); + SetLen( nOldLen ); + rInf.SetLen( nOldInfLen ); + + return true; +} + +SwPosSize SwDropPortion::GetTextSize( const SwTextSizeInfo &rInf ) const +{ + sal_uInt16 nMyX = 0; + TextFrameIndex nIdx(0); + + const SwDropPortionPart* pCurrPart = GetPart(); + + // skip parts + while ( pCurrPart && nIdx + pCurrPart->GetLen() < rInf.GetLen() ) + { + nMyX = nMyX + pCurrPart->GetWidth(); + nIdx = nIdx + pCurrPart->GetLen(); + pCurrPart = pCurrPart->GetFollow(); + } + + TextFrameIndex const nOldIdx = rInf.GetIdx(); + TextFrameIndex const nOldLen = rInf.GetLen(); + + const_cast<SwTextSizeInfo&>(rInf).SetIdx( nIdx ); + const_cast<SwTextSizeInfo&>(rInf).SetLen( rInf.GetLen() - nIdx ); + + if( pCurrPart ) + { + const_cast<SwDropPortion*>(this)->SetJoinBorderWithNext(pCurrPart->GetJoinBorderWithNext()); + const_cast<SwDropPortion*>(this)->SetJoinBorderWithPrev(pCurrPart->GetJoinBorderWithPrev()); + } + + // robust + SwFontSave aFontSave( rInf, pCurrPart ? &pCurrPart->GetFont() : nullptr ); + SwPosSize aPosSize( SwTextPortion::GetTextSize( rInf ) ); + aPosSize.Width( aPosSize.Width() + nMyX ); + + const_cast<SwTextSizeInfo&>(rInf).SetIdx( nOldIdx ); + const_cast<SwTextSizeInfo&>(rInf).SetLen( nOldLen ); + if( pCurrPart ) + { + const_cast<SwDropPortion*>(this)->SetJoinBorderWithNext(false); + const_cast<SwDropPortion*>(this)->SetJoinBorderWithPrev(false); + } + + return aPosSize; +} + +TextFrameIndex SwDropPortion::GetModelPositionForViewPoint(const sal_uInt16) const +{ + return TextFrameIndex(0); +} + +void SwTextFormatter::CalcDropHeight( const sal_uInt16 nLines ) +{ + const SwLinePortion *const pOldCurr = GetCurr(); + sal_uInt16 nDropHght = 0; + SwTwips nAscent = 0; + SwTwips nHeight = 0; + sal_uInt16 nDropLns = 0; + const bool bRegisterOld = IsRegisterOn(); + m_bRegisterOn = false; + + Top(); + + while( GetCurr()->IsDummy() ) + { + if ( !Next() ) + break; + } + + // If we have only one line we return 0 + if( GetNext() || GetDropLines() == 1 ) + { + for( ; nDropLns < nLines; nDropLns++ ) + { + if ( GetCurr()->IsDummy() ) + break; + else + { + CalcAscentAndHeight( nAscent, nHeight ); + nDropHght = nDropHght + nHeight; + m_bRegisterOn = bRegisterOld; + } + if ( !Next() ) + { + nDropLns++; + break; + } + } + + // We hit the line ascent when reaching the last line! + nDropHght = nDropHght - nHeight; + nDropHght = nDropHght + nAscent; + Top(); + } + m_bRegisterOn = bRegisterOld; + SetDropDescent( nHeight - nAscent ); + SetDropHeight( nDropHght ); + SetDropLines( nDropLns ); + // Find old position! + while( pOldCurr != GetCurr() ) + { + if( !Next() ) + { + OSL_ENSURE( false, "SwTextFormatter::_CalcDropHeight: left Toulouse" ); + break; + } + } +} + +/** + * We assume that the font height doesn't change and that at first there + * are at least as many lines, as the DropCap-setting claims + */ +void SwTextFormatter::GuessDropHeight( const sal_uInt16 nLines ) +{ + OSL_ENSURE( nLines, "GuessDropHeight: Give me more Lines!" ); + SwTwips nAscent = 0; + SwTwips nHeight = 0; + SetDropLines( nLines ); + if ( GetDropLines() > 1 ) + { + CalcRealHeight(); + CalcAscentAndHeight( nAscent, nHeight ); + } + SetDropDescent( nHeight - nAscent ); + SetDropHeight( nHeight * nLines - GetDropDescent() ); +} + +SwDropPortion *SwTextFormatter::NewDropPortion( SwTextFormatInfo &rInf ) +{ + if( !m_pDropFormat ) + return nullptr; + + TextFrameIndex nPorLen(m_pDropFormat->GetWholeWord() ? 0 : m_pDropFormat->GetChars()); + nPorLen = m_pFrame->GetDropLen( nPorLen ); + if( !nPorLen ) + { + ClearDropFormat(); + return nullptr; + } + + SwDropPortion *pDropPor = nullptr; + + // first or second round? + if ( !( GetDropHeight() || IsOnceMore() ) ) + { + if ( GetNext() ) + CalcDropHeight( m_pDropFormat->GetLines() ); + else + GuessDropHeight( m_pDropFormat->GetLines() ); + } + + // the DropPortion + if( GetDropHeight() ) + pDropPor = new SwDropPortion( GetDropLines(), GetDropHeight(), + GetDropDescent(), m_pDropFormat->GetDistance() ); + else + pDropPor = new SwDropPortion( 0,0,0,m_pDropFormat->GetDistance() ); + + pDropPor->SetLen( nPorLen ); + + // If it was not possible to create a proper drop cap portion + // due to avoiding endless loops. We return a drop cap portion + // with an empty SwDropCapPart. For these portions the current + // font is used. + if ( GetDropLines() < 2 ) + { + SetPaintDrop( true ); + return pDropPor; + } + + // build DropPortionParts: + OSL_ENSURE( ! rInf.GetIdx(), "Drop Portion not at 0 position!" ); + TextFrameIndex nNextChg(0); + const SwCharFormat* pFormat = m_pDropFormat->GetCharFormat(); + SwDropPortionPart* pCurrPart = nullptr; + + while ( nNextChg < nPorLen ) + { + // check for attribute changes and if the portion has to split: + Seek( nNextChg ); + + // the font is deleted in the destructor of the drop portion part + SwFont* pTmpFnt = new SwFont( *rInf.GetFont() ); + if ( pFormat ) + { + const SwAttrSet& rSet = pFormat->GetAttrSet(); + pTmpFnt->SetDiffFnt(&rSet, &m_pFrame->GetDoc().getIDocumentSettingAccess()); + } + + // we do not allow a vertical font for the drop portion + pTmpFnt->SetVertical( 0_deg10, rInf.GetTextFrame()->IsVertical() ); + + // find next attribute change / script change + const TextFrameIndex nTmpIdx = nNextChg; + TextFrameIndex nNextAttr = GetNextAttr(); + nNextChg = m_pScriptInfo->NextScriptChg( nTmpIdx ); + if( nNextChg > nNextAttr ) + nNextChg = nNextAttr; + if ( nNextChg > nPorLen ) + nNextChg = nPorLen; + + std::unique_ptr<SwDropPortionPart> pPart( + new SwDropPortionPart( *pTmpFnt, nNextChg - nTmpIdx ) ); + auto pPartTemp = pPart.get(); + + if ( ! pCurrPart ) + pDropPor->SetPart( std::move(pPart) ); + else + pCurrPart->SetFollow( std::move(pPart) ); + + pCurrPart = pPartTemp; + } + + SetPaintDrop( true ); + return pDropPor; +} + +void SwTextPainter::PaintDropPortion() +{ + const SwDropPortion *pDrop = GetInfo().GetParaPortion()->FindDropPortion(); + OSL_ENSURE( pDrop, "DrapCop-Portion not available." ); + if( !pDrop ) + return; + + const SwTwips nOldY = GetInfo().Y(); + + Top(); + + GetInfo().SetpSpaceAdd( m_pCurr->GetpLLSpaceAdd() ); + GetInfo().ResetSpaceIdx(); + GetInfo().SetKanaComp( m_pCurr->GetpKanaComp() ); + GetInfo().ResetKanaIdx(); + + // 8047: Drops and Dummies + while( !m_pCurr->GetLen() && Next() ) + ; + + // MarginPortion and Adjustment! + const SwLinePortion *pPor = m_pCurr->GetFirstPortion(); + tools::Long nX = 0; + while( pPor && !pPor->IsDropPortion() ) + { + nX = nX + pPor->Width(); + pPor = pPor->GetNextPortion(); + } + Point aLineOrigin( GetTopLeft() ); + + aLineOrigin.AdjustX(nX ); + SwTwips nTmpAscent, nTmpHeight; + CalcAscentAndHeight( nTmpAscent, nTmpHeight ); + aLineOrigin.AdjustY(nTmpAscent ); + GetInfo().SetIdx( GetStart() ); + GetInfo().SetPos( aLineOrigin ); + GetInfo().SetLen( pDrop->GetLen() ); + + pDrop->PaintDrop( GetInfo() ); + + GetInfo().Y( nOldY ); +} + +// Since the calculation of the font size is expensive, this is being +// channeled through a DropCapCache +#define DROP_CACHE_SIZE 10 + +class SwDropCapCache +{ + const void* m_aFontCacheId[ DROP_CACHE_SIZE ] = {}; + OUString m_aText[ DROP_CACHE_SIZE ]; + sal_uInt16 m_aFactor[ DROP_CACHE_SIZE ]; + sal_uInt16 m_aWishedHeight[ DROP_CACHE_SIZE ] = {}; + short m_aDescent[ DROP_CACHE_SIZE ]; + sal_uInt16 m_nIndex = 0; +public: + SwDropCapCache() = default; + void CalcFontSize( SwDropPortion* pDrop, SwTextFormatInfo &rInf ); +}; + +void SwDropPortion::DeleteDropCapCache() +{ + delete pDropCapCache; +} + +void SwDropCapCache::CalcFontSize( SwDropPortion* pDrop, SwTextFormatInfo &rInf ) +{ + const void* nFntCacheId = nullptr; + sal_uInt16 nTmpIdx = 0; + + OSL_ENSURE( pDrop->GetPart(),"DropPortion without part during font calculation"); + + SwDropPortionPart* pCurrPart = pDrop->GetPart(); + const bool bUseCache = ! pCurrPart->GetFollow() && !pCurrPart->GetFont().HasBorder(); + TextFrameIndex nIdx = rInf.GetIdx(); + OUString aStr(rInf.GetText().copy(sal_Int32(nIdx), sal_Int32(pCurrPart->GetLen()))); + + tools::Long nDescent = 0; + tools::Long nFactor = -1; + + if ( bUseCache ) + { + SwFont& rFnt = pCurrPart->GetFont(); + rFnt.CheckFontCacheId( rInf.GetVsh(), rFnt.GetActual() ); + rFnt.GetFontCacheId( nFntCacheId, nTmpIdx, rFnt.GetActual() ); + + nTmpIdx = 0; + + while( nTmpIdx < DROP_CACHE_SIZE && + ( m_aText[ nTmpIdx ] != aStr || m_aFontCacheId[ nTmpIdx ] != nFntCacheId || + m_aWishedHeight[ nTmpIdx ] != pDrop->GetDropHeight() ) ) + ++nTmpIdx; + } + + // we have to calculate a new font scaling factor if + // 1. we did not find a scaling factor in the cache or + // 2. we are not allowed to use the cache because the drop portion + // consists of more than one part + if( nTmpIdx >= DROP_CACHE_SIZE || ! bUseCache ) + { + ++m_nIndex; + m_nIndex %= DROP_CACHE_SIZE; + nTmpIdx = m_nIndex; + + tools::Long nWishedHeight = pDrop->GetDropHeight(); + tools::Long nAscent = 0; + + // find out biggest font size for initial scaling factor + tools::Long nMaxFontHeight = 1; + while ( pCurrPart ) + { + const SwFont& rFnt = pCurrPart->GetFont(); + const tools::Long nCurrHeight = rFnt.GetHeight( rFnt.GetActual() ); + if ( nCurrHeight > nMaxFontHeight ) + nMaxFontHeight = nCurrHeight; + + pCurrPart = pCurrPart->GetFollow(); + } + + nFactor = ( 1000 * nWishedHeight ) / nMaxFontHeight; + + if ( bUseCache ) + { + // save keys for cache + m_aFontCacheId[ nTmpIdx ] = nFntCacheId; + m_aText[ nTmpIdx ] = aStr; + m_aWishedHeight[ nTmpIdx ] = sal_uInt16(nWishedHeight); + // save initial scaling factor + m_aFactor[ nTmpIdx ] = o3tl::narrowing<sal_uInt16>(nFactor); + } + + bool bGrow = (pDrop->GetLen() != TextFrameIndex(0)); + + // for growing control + tools::Long nMax = USHRT_MAX; + tools::Long nMin = 0; +#if OSL_DEBUG_LEVEL > 1 + tools::Long nGrow = 0; +#endif + + bool bWinUsed = false; + vcl::Font aOldFnt; + MapMode aOldMap( MapUnit::MapTwip ); + OutputDevice* pOut = rInf.GetOut(); + OutputDevice* pWin; + if( rInf.GetVsh() && rInf.GetVsh()->GetWin() ) + pWin = rInf.GetVsh()->GetWin()->GetOutDev(); + else + pWin = Application::GetDefaultDevice(); + + // adjust punctuation? + bool bKeepBaseline = rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess() + .get(DocumentSettingId::DROP_CAP_PUNCTUATION) && + !rInf.GetDropFormat()->GetWholeWord(); // && rInf.GetDropFormat()->GetChars() == 1; + + while( bGrow ) + { + // reset pCurrPart to first part + pCurrPart = pDrop->GetPart(); + bool bFirstGlyphRect = true; + tools::Rectangle aCommonRect, aRect; + + while ( pCurrPart ) + { + // current font + SwFont& rFnt = pCurrPart->GetFont(); + + // Get height including proportion + const tools::Long nCurrHeight = rFnt.GetHeight( rFnt.GetActual() ); + + // Get without proportion + const sal_uInt8 nOldProp = rFnt.GetPropr(); + rFnt.SetProportion( 100 ); + Size aOldSize( 0, rFnt.GetHeight( rFnt.GetActual() ) ); + + Size aNewSize( 0, ( nFactor * nCurrHeight ) / 1000 ); + rFnt.SetSize( aNewSize, rFnt.GetActual() ); + rFnt.ChgPhysFnt( rInf.GetVsh(), *pOut ); + + nAscent = rFnt.GetAscent( rInf.GetVsh(), *pOut ); + + // we get the rectangle that covers all chars + bool bHaveGlyphRect = pOut->GetTextBoundRect( aRect, rInf.GetText(), 0, + sal_Int32(nIdx), sal_Int32(pCurrPart->GetLen())) + && ! aRect.IsEmpty(); + + if ( ! bHaveGlyphRect ) + { + // getting glyph boundaries failed for some reason, + // we take the window for calculating sizes + if ( pWin ) + { + if ( ! bWinUsed ) + { + bWinUsed = true; + aOldMap = pWin->GetMapMode( ); + pWin->SetMapMode( MapMode( MapUnit::MapTwip ) ); + aOldFnt = pWin->GetFont(); + } + pWin->SetFont( rFnt.GetActualFont() ); + + bHaveGlyphRect = pWin->GetTextBoundRect( aRect, rInf.GetText(), 0, + sal_Int32(nIdx), sal_Int32(pCurrPart->GetLen())) + && ! aRect.IsEmpty(); + } + if (!bHaveGlyphRect) + { + // We do not have a window or our window could not + // give us glyph boundaries. + aRect = tools::Rectangle( Point( 0, 0 ), Size( 0, nAscent ) ); + } + } + + // extend rectangle to the baseline to avoid of giant dashes, + // quotation marks, bullet, asterisks etc. + if ( bKeepBaseline && aRect.Top() < 0 ) + { + aRect.SetBottom(0); + aRect.SetTop(aRect.Top() - nAscent/60); + } + + // Now we (hopefully) have a bounding rectangle for the + // glyphs of the current portion and the ascent of the current + // font + + // reset font size and proportion + rFnt.SetSize( aOldSize, rFnt.GetActual() ); + rFnt.SetProportion( nOldProp ); + + // Modify the bounding rectangle with the borders + // Robust: If the padding is so big as drop cap letter has no enough space than + // remove all padding. + if( rFnt.GetTopBorderSpace() + rFnt.GetBottomBorderSpace() >= nWishedHeight ) + { + rFnt.SetTopBorderDist(0); + rFnt.SetBottomBorderDist(0); + rFnt.SetRightBorderDist(0); + rFnt.SetLeftBorderDist(0); + } + + if( rFnt.GetTopBorder() ) + { + aRect.setHeight(aRect.GetHeight() + rFnt.GetTopBorderSpace()); + aRect.SetPosY(aRect.Top() - rFnt.GetTopBorderSpace()); + } + + if( rFnt.GetBottomBorder() ) + { + aRect.setHeight(aRect.GetHeight() + rFnt.GetBottomBorderSpace()); + } + + if ( bFirstGlyphRect ) + { + aCommonRect = aRect; + bFirstGlyphRect = false; + } + else + aCommonRect.Union( aRect ); + + nIdx = nIdx + pCurrPart->GetLen(); + pCurrPart = pCurrPart->GetFollow(); + } + + // now we have a union ( aCommonRect ) of all glyphs with + // respect to a common baseline : 0 + + // get descent and ascent from union + if ( rInf.GetTextFrame()->IsVertical() ) + { + nDescent = aCommonRect.Left(); + nAscent = aCommonRect.Right(); + + if ( nDescent < 0 ) + nDescent = -nDescent; + } + else + { + nDescent = aCommonRect.Bottom(); + nAscent = aCommonRect.Top(); + } + if ( nAscent < 0 ) + nAscent = -nAscent; + + const tools::Long nHght = nAscent + nDescent; + if ( nHght ) + { + if ( nHght > nWishedHeight ) + nMax = nFactor; + else + { + if ( bUseCache ) + m_aFactor[ nTmpIdx ] = o3tl::narrowing<sal_uInt16>(nFactor); + nMin = nFactor; + } + + nFactor = ( nFactor * nWishedHeight ) / nHght; + bGrow = ( nFactor > nMin ) && ( nFactor < nMax ); +#if OSL_DEBUG_LEVEL > 1 + if ( bGrow ) + nGrow++; +#endif + nIdx = rInf.GetIdx(); + } + else + bGrow = false; + } + + if ( bWinUsed ) + { + // reset window if it has been used + pWin->SetMapMode( aOldMap ); + pWin->SetFont( aOldFnt ); + } + + if ( bUseCache ) + m_aDescent[ nTmpIdx ] = -short( nDescent ); + } + + pCurrPart = pDrop->GetPart(); + + // did made any new calculations or did we use the cache? + if ( -1 == nFactor ) + { + nFactor = m_aFactor[ nTmpIdx ]; + nDescent = m_aDescent[ nTmpIdx ]; + } + else + nDescent = -nDescent; + + while ( pCurrPart ) + { + // scale current font + SwFont& rFnt = pCurrPart->GetFont(); + Size aNewSize( 0, ( nFactor * rFnt.GetHeight( rFnt.GetActual() ) ) / 1000 ); + + const sal_uInt8 nOldProp = rFnt.GetPropr(); + rFnt.SetProportion( 100 ); + rFnt.SetSize( aNewSize, rFnt.GetActual() ); + rFnt.SetProportion( nOldProp ); + + pCurrPart = pCurrPart->GetFollow(); + } + pDrop->SetY( static_cast<short>(nDescent) ); +} + +bool SwDropPortion::Format( SwTextFormatInfo &rInf ) +{ + bool bFull = false; + m_nFix = o3tl::narrowing<sal_uInt16>(rInf.X()); + + SwLayoutModeModifier aLayoutModeModifier( *rInf.GetOut() ); + aLayoutModeModifier.SetAuto(); + + if( m_nDropHeight && m_pPart && m_nLines!=1 ) + { + if( !pDropCapCache ) + pDropCapCache = new SwDropCapCache; + + // adjust font sizes to fit into the rectangle + pDropCapCache->CalcFontSize( this, rInf ); + + const tools::Long nOldX = rInf.X(); + { + SwDropSave aSave( rInf ); + SwDropPortionPart* pCurrPart = m_pPart.get(); + + while ( pCurrPart ) + { + rInf.SetLen( pCurrPart->GetLen() ); + SwFont& rFnt = pCurrPart->GetFont(); + { + SwFontSave aFontSave( rInf, &rFnt ); + SetJoinBorderWithNext(pCurrPart->GetJoinBorderWithNext()); + SetJoinBorderWithPrev(pCurrPart->GetJoinBorderWithPrev()); + bFull = FormatText( rInf ); + + if ( bFull ) + break; + } + + const SwTwips nTmpWidth = + ( InSpaceGrp() && rInf.GetSpaceAdd() ) ? + Width() + CalcSpacing( rInf.GetSpaceAdd(), rInf ) : + Width(); + + // set values + pCurrPart->SetWidth( o3tl::narrowing<sal_uInt16>(nTmpWidth) ); + + // Move + rInf.SetIdx( rInf.GetIdx() + pCurrPart->GetLen() ); + rInf.X( rInf.X() + nTmpWidth ); + pCurrPart = pCurrPart->GetFollow(); + } + SetJoinBorderWithNext(false); + SetJoinBorderWithPrev(false); + Width( o3tl::narrowing<sal_uInt16>(rInf.X() - nOldX) ); + } + + // reset my length + SetLen( rInf.GetLen() ); + + // Quit when Flys are overlapping + if( ! bFull ) + bFull = lcl_IsDropFlyInter( rInf, Width(), m_nDropHeight ); + + if( bFull ) + { + // FormatText could have caused nHeight to be 0 + if ( !Height() ) + Height( rInf.GetTextHeight() ); + + // And now for another round + m_nDropHeight = m_nLines = 0; + m_pPart.reset(); + + // Meanwhile use normal formatting + bFull = SwTextPortion::Format( rInf ); + } + else + rInf.SetDropInit( true ); + + Height( rInf.GetTextHeight() ); + SetAscent( rInf.GetAscent() ); + } + else + bFull = SwTextPortion::Format( rInf ); + + if( bFull ) + m_nDistance = 0; + else + { + const sal_uInt16 nWant = Width() + GetDistance(); + const sal_uInt16 nRest = o3tl::narrowing<sal_uInt16>(rInf.Width() - rInf.X()); + if( ( nWant > nRest ) || + lcl_IsDropFlyInter( rInf, Width() + GetDistance(), m_nDropHeight ) ) + m_nDistance = 0; + + Width( Width() + m_nDistance ); + } + return bFull; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtfld.cxx b/sw/source/core/text/txtfld.cxx new file mode 100644 index 0000000000..84f32e3097 --- /dev/null +++ b/sw/source/core/text/txtfld.cxx @@ -0,0 +1,715 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <hintids.hxx> +#include <fmtfld.hxx> +#include <txtfld.hxx> +#include <charfmt.hxx> +#include <fmtautofmt.hxx> + +#include <viewsh.hxx> +#include <doc.hxx> +#include <rootfrm.hxx> +#include <pagefrm.hxx> +#include <ndtxt.hxx> +#include <fldbas.hxx> +#include <viewopt.hxx> +#include <flyfrm.hxx> +#include <viewimp.hxx> +#include <swfont.hxx> +#include <swmodule.hxx> +#include "porfld.hxx" +#include "porftn.hxx" +#include "porref.hxx" +#include "portox.hxx" +#include "porfly.hxx" +#include "itrform2.hxx" +#include <chpfld.hxx> +#include <dbfld.hxx> +#include <expfld.hxx> +#include <docufld.hxx> +#include <pagedesc.hxx> +#include <fmtmeta.hxx> +#include <reffld.hxx> +#include <flddat.hxx> +#include <IDocumentSettingAccess.hxx> +#include <IDocumentRedlineAccess.hxx> +#include <redline.hxx> +#include <sfx2/docfile.hxx> +#include <svl/grabbagitem.hxx> +#include <svl/itemiter.hxx> +#include <svl/whiter.hxx> +#include <editeng/colritem.hxx> +#include <editeng/udlnitem.hxx> +#include <editeng/crossedoutitem.hxx> +#include <officecfg/Office/Writer.hxx> + +static bool lcl_IsInBody( SwFrame const *pFrame ) +{ + if ( pFrame->IsInDocBody() ) + return true; + + while (const SwFlyFrame* pFly = pFrame->FindFlyFrame()) + pFrame = pFly->GetAnchorFrame(); + return pFrame->IsInDocBody(); +} + +static OUString ExpandField(const SwField& rField, const SwTextFormatter& rFormatter, + const SwTextFormatInfo& rInf) +{ + if (rInf.GetOpt().IsFieldName()) + return rField.GetFieldName(); + + const SwViewShell* pSh = rInf.GetVsh(); + const SwDoc* pDoc(pSh ? pSh->GetDoc() : nullptr); + const bool bInClipboard(!pDoc || pDoc->IsClipBoard()); + return rField.ExpandField(bInClipboard, rFormatter.GetTextFrame()->getRootFrame()); +} + +SwExpandPortion *SwTextFormatter::NewFieldPortion( SwTextFormatInfo &rInf, + const SwTextAttr *pHint ) const +{ + SwField *pField = const_cast<SwField*>(pHint->GetFormatField().GetField()); + const bool bName = rInf.GetOpt().IsFieldName(); + + // set language + const_cast<SwTextFormatter*>(this)->SeekAndChg( rInf ); + if (pField->GetLanguage() != GetFnt()->GetLanguage()) + pField->SetLanguage( GetFnt()->GetLanguage() ); + + SwViewShell *pSh = rInf.GetVsh(); + + switch (pField->GetTyp()->Which()) + { + case SwFieldIds::Script: + case SwFieldIds::Postit: + return new SwPostItsPortion(SwFieldIds::Script == pField->GetTyp()->Which()); + case SwFieldIds::CombinedChars: + if (!bName) + return new SwCombinedPortion(ExpandField(*pField, *this, rInf)); + break; + case SwFieldIds::HiddenText: + return new SwHiddenPortion(ExpandField(*pField, *this, rInf)); + case SwFieldIds::Chapter: + if (!bName && pSh && !pSh->Imp()->IsUpdateExpFields()) + { + static_cast<SwChapterField*>(pField)->ChangeExpansion( + *m_pFrame, &static_txtattr_cast<SwTextField const*>(pHint)->GetTextNode()); + } + break; + case SwFieldIds::DocStat: + if (!bName && pSh && !pSh->Imp()->IsUpdateExpFields()) + { + static_cast<SwDocStatField*>(pField)->ChangeExpansion(m_pFrame); + } + break; + case SwFieldIds::PageNumber: + if (!bName && pSh && pSh->GetLayout() && !pSh->Imp()->IsUpdateExpFields()) + { + auto pPageNr = static_cast<SwPageNumberFieldType*>(pField->GetTyp()); + + const SwRootFrame* pTmpRootFrame = pSh->GetLayout(); + const bool bVirt = pTmpRootFrame->IsVirtPageNum(); + + sal_uInt16 nVirtNum = m_pFrame->GetVirtPageNum(); + sal_uInt16 nNumPages = pTmpRootFrame->GetPageNum(); + SvxNumType nNumFormat = SvxNumType(-1); + if (SVX_NUM_PAGEDESC == pField->GetFormat()) + nNumFormat + = m_pFrame->FindPageFrame()->GetPageDesc()->GetNumType().GetNumberingType(); + static_cast<SwPageNumberField*>(pField)->ChangeExpansion(nVirtNum, nNumPages); + pPageNr->ChangeExpansion(pSh->GetDoc(), bVirt, + nNumFormat != SvxNumType(-1) ? &nNumFormat : nullptr); + } + break; + case SwFieldIds::GetExp: + if (!bName && pSh && !pSh->Imp()->IsUpdateExpFields()) + { + auto pExpField = static_cast<SwGetExpField*>(pField); + if (!::lcl_IsInBody(m_pFrame)) + { + pExpField->ChgBodyTextFlag(false); + pExpField->ChangeExpansion(*m_pFrame, + *static_txtattr_cast<SwTextField const*>(pHint)); + } + else if (!pExpField->IsInBodyText()) + { + // Was something else previously, thus: expand first, then convert it! + pExpField->ChangeExpansion(*m_pFrame, + *static_txtattr_cast<SwTextField const*>(pHint)); + pExpField->ChgBodyTextFlag(true); + } + } + break; + case SwFieldIds::Database: + if (!bName) + { + static_cast<SwDBField*>(pField)->ChgBodyTextFlag(::lcl_IsInBody(m_pFrame)); + } + break; + case SwFieldIds::RefPageGet: + if (!bName && pSh && !pSh->Imp()->IsUpdateExpFields()) + { + static_cast<SwRefPageGetField*>(pField)->ChangeExpansion( + *m_pFrame, static_txtattr_cast<SwTextField const*>(pHint)); + } + break; + case SwFieldIds::JumpEdit: + { + std::unique_ptr<SwFont> pFont; + if (!bName) + { + pFont = std::make_unique<SwFont>(*m_pFont); + pFont->SetDiffFnt( + &static_cast<SwJumpEditField*>(pField)->GetCharFormat()->GetAttrSet(), + &m_pFrame->GetDoc().getIDocumentSettingAccess()); + } + return new SwJumpFieldPortion(ExpandField(*pField, *this, rInf), pField->GetPar2(), + std::move(pFont), pField->GetFormat()); + } + case SwFieldIds::GetRef: + if (!bName) + { + auto pGetRef = static_cast<SwGetRefField*>(pField); + if (pGetRef->GetSubType() == REF_STYLE) + pGetRef->UpdateField(static_txtattr_cast<SwTextField const*>(pHint), m_pFrame); + } + break; + default: + break; + } + return new SwFieldPortion(ExpandField(*pField, *this, rInf)); +} + +static SwFieldPortion * lcl_NewMetaPortion(SwTextAttr & rHint, const bool bPrefix) +{ + ::sw::Meta *const pMeta( + static_cast<SwFormatMeta &>(rHint.GetAttr()).GetMeta() ); + OUString fix; + ::sw::MetaField *const pField( dynamic_cast< ::sw::MetaField * >(pMeta) ); + OSL_ENSURE(pField, "lcl_NewMetaPortion: no meta field?"); + if (pField) + { + OUString color; + pField->GetPrefixAndSuffix(bPrefix ? &fix : nullptr, bPrefix ? nullptr : &fix, &color); + } + return new SwFieldPortion( fix ); +} + +/** + * Try to create a new portion with zero length, for an end of a hint + * (where there is no CH_TXTATR). Because there may be multiple hint ends at a + * given index, m_pByEndIter is used to keep track of the already created + * portions. But the portions created here may actually be deleted again, + * due to Underflow. In that case, m_pByEndIter must be decremented, + * so the portion will be created again on the next line. + */ +SwExpandPortion * SwTextFormatter::TryNewNoLengthPortion(SwTextFormatInfo const & rInfo) +{ + const TextFrameIndex nIdx(rInfo.GetIdx()); + + // sw_redlinehide: because there is a dummy character at the start of these + // hints, it's impossible to have ends of hints from different nodes at the + // same view position, so it's sufficient to check the hints of the current + // node. However, m_pByEndIter exists for the whole text frame, so + // it's necessary to iterate all hints for that purpose... + if (!m_pByEndIter) + { + m_pByEndIter.reset(new sw::MergedAttrIterByEnd(*rInfo.GetTextFrame())); + } + SwTextNode const* pNode(nullptr); + for (SwTextAttr const* pHint = m_pByEndIter->NextAttr(pNode); pHint; + pHint = m_pByEndIter->NextAttr(pNode)) + { + SwTextAttr & rHint(const_cast<SwTextAttr&>(*pHint)); + TextFrameIndex const nEnd( + rInfo.GetTextFrame()->MapModelToView(pNode, rHint.GetAnyEnd())); + if (nEnd > nIdx) + { + m_pByEndIter->PrevAttr(); + break; + } + if (nEnd == nIdx) + { + if (RES_TXTATR_METAFIELD == rHint.Which()) + { + SwFieldPortion *const pPortion( + lcl_NewMetaPortion(rHint, false)); + pPortion->SetNoLength(); // no CH_TXTATR at hint end! + return pPortion; + } + } + } + return nullptr; +} + +SwLinePortion *SwTextFormatter::NewExtraPortion( SwTextFormatInfo &rInf ) +{ + SwTextAttr *pHint = GetAttr( rInf.GetIdx() ); + SwLinePortion *pRet = nullptr; + if( !pHint ) + { + pRet = new SwTextPortion; + pRet->SetLen(TextFrameIndex(1)); + rInf.SetLen(TextFrameIndex(1)); + return pRet; + } + + switch( pHint->Which() ) + { + case RES_TXTATR_FLYCNT : + { + pRet = NewFlyCntPortion( rInf, pHint ); + break; + } + case RES_TXTATR_FTN : + { + pRet = NewFootnotePortion( rInf, pHint ); + break; + } + case RES_TXTATR_FIELD : + case RES_TXTATR_ANNOTATION : + { + pRet = NewFieldPortion( rInf, pHint ); + break; + } + case RES_TXTATR_REFMARK : + { + pRet = new SwIsoRefPortion; + break; + } + case RES_TXTATR_TOXMARK : + { + pRet = new SwIsoToxPortion; + break; + } + case RES_TXTATR_METAFIELD: + { + pRet = lcl_NewMetaPortion( *pHint, true ); + break; + } + default: ; + } + if( !pRet ) + { + auto pFieldPortion = new SwFieldPortion( "" ); + if (pHint->Which() == RES_TXTATR_CONTENTCONTROL) + { + pFieldPortion->SetContentControl(true); + } + pRet = pFieldPortion; + rInf.SetLen(TextFrameIndex(1)); + } + return pRet; +} + +/** + * OOXML spec says that w:rPr inside w:pPr specifies formatting for the paragraph mark symbol (i.e. the control + * character than can be configured to be shown). However, in practice MSO also uses it as direct formatting + * for numbering in that paragraph. I don't know if the problem is in the spec or in MSWord. + */ +static void checkApplyParagraphMarkFormatToNumbering(SwFont* pNumFnt, SwTextFormatInfo& rInf, + const IDocumentSettingAccess* pIDSA, + const SwAttrSet* pFormat) +{ + if( !pIDSA->get(DocumentSettingId::APPLY_PARAGRAPH_MARK_FORMAT_TO_NUMBERING )) + return; + + SwFormatAutoFormat const& rListAutoFormat(rInf.GetTextFrame()->GetTextNodeForParaProps()->GetAttr(RES_PARATR_LIST_AUTOFMT)); + std::shared_ptr<SfxItemSet> pSet(rListAutoFormat.GetStyleHandle()); + + // Check each item and in case it should be ignored, then clear it. + if (!pSet) + return; + + std::unique_ptr<SfxItemSet> const pCleanedSet = pSet->Clone(); + + if (pCleanedSet->HasItem(RES_TXTATR_CHARFMT)) + { + // Insert attributes of referenced char format into current set + const SwFormatCharFormat& rCharFormat = pCleanedSet->Get(RES_TXTATR_CHARFMT); + const SwAttrSet& rStyleAttrs = static_cast<const SwCharFormat *>(rCharFormat.GetRegisteredIn())->GetAttrSet(); + SfxWhichIter aIter(rStyleAttrs); + sal_uInt16 nWhich = aIter.FirstWhich(); + while (nWhich) + { + if (!SwTextNode::IsIgnoredCharFormatForNumbering(nWhich, /*bIsCharStyle=*/true) + && !pCleanedSet->HasItem(nWhich) + && !(pFormat && pFormat->HasItem(nWhich)) + && rStyleAttrs.GetItemState(nWhich) > SfxItemState::DEFAULT) + { + // Copy from parent sets only allowed items which will not overwrite + // values explicitly defined in current set (pCleanedSet) or in pFormat + if (const SfxPoolItem* pItem = rStyleAttrs.GetItem(nWhich, true)) + pCleanedSet->Put(*pItem); + } + nWhich = aIter.NextWhich(); + } + + // It is not required here anymore, all referenced items are inserted + pCleanedSet->ClearItem(RES_TXTATR_CHARFMT); + }; + + SfxItemIter aIter(*pSet); + const SfxPoolItem* pItem = aIter.GetCurItem(); + while (pItem) + { + if (SwTextNode::IsIgnoredCharFormatForNumbering(pItem->Which())) + pCleanedSet->ClearItem(pItem->Which()); + else if (pFormat && pFormat->HasItem(pItem->Which())) + pCleanedSet->ClearItem(pItem->Which()); + else if (pItem->Which() == RES_CHRATR_BACKGROUND) + { + bool bShadingWasImported = false; + // If Shading was imported, it should not be converted to a Highlight, + // but remain as Shading which is ignored for numbering. + if (pCleanedSet->HasItem(RES_CHRATR_GRABBAG)) + { + SfxGrabBagItem aGrabBag = pCleanedSet->Get(RES_CHRATR_GRABBAG, /*bSrchInParent=*/false); + std::map<OUString, css::uno::Any>& rMap = aGrabBag.GetGrabBag(); + auto aIterator = rMap.find("CharShadingMarker"); + if (aIterator != rMap.end()) + aIterator->second >>= bShadingWasImported; + } + + // If used, BACKGROUND is converted to HIGHLIGHT. So also ignore if a highlight already exists. + if (bShadingWasImported + || pCleanedSet->HasItem(RES_CHRATR_HIGHLIGHT) + || (pFormat && pFormat->HasItem(RES_CHRATR_HIGHLIGHT))) + { + pCleanedSet->ClearItem(pItem->Which()); + } + } + pItem = aIter.NextItem(); + }; + + // SetDiffFnt resets the background color (why?), so capture it and re-apply if it had a value, + // because an existing value should override anything inherited from the paragraph marker. + const std::optional<Color> oFontBackColor = pNumFnt->GetBackColor(); + // The same is true for the highlight color. + const Color aHighlight = pNumFnt->GetHighlightColor(); + + pNumFnt->SetDiffFnt(pCleanedSet.get(), pIDSA); + + if (oFontBackColor) + pNumFnt->SetBackColor(oFontBackColor); + if (aHighlight != COL_TRANSPARENT) + pNumFnt->SetHighlightColor(aHighlight); +} + +static const SwRangeRedline* lcl_GetRedlineAtNodeInsertionOrDeletion( const SwTextNode& rTextNode, + bool& bIsMoved ) +{ + const SwDoc& rDoc = rTextNode.GetDoc(); + SwRedlineTable::size_type nRedlPos = rDoc.getIDocumentRedlineAccess().GetRedlinePos( rTextNode, RedlineType::Any ); + + if( SwRedlineTable::npos != nRedlPos ) + { + const SwNodeOffset nNdIdx = rTextNode.GetIndex(); + const SwRedlineTable& rTable = rDoc.getIDocumentRedlineAccess().GetRedlineTable(); + for( ; nRedlPos < rTable.size() ; ++nRedlPos ) + { + const SwRangeRedline* pTmp = rTable[ nRedlPos ]; + SwNodeOffset nStart = pTmp->GetPoint()->GetNodeIndex(), + nEnd = pTmp->GetMark()->GetNodeIndex(); + if( nStart > nEnd ) + std::swap(nStart, nEnd); + if( RedlineType::Delete == pTmp->GetType() || + RedlineType::Insert == pTmp->GetType() ) + { + if( nStart <= nNdIdx && nEnd > nNdIdx ) + { + bIsMoved = pTmp->IsMoved(); + return pTmp; + } + } + if( nStart > nNdIdx ) + break; + } + } + return nullptr; +} + +static bool lcl_setRedlineAttr( SwTextFormatInfo &rInf, const SwTextNode& rTextNode, const std::unique_ptr<SwFont>& pNumFnt ) +{ + if ( rInf.GetVsh()->GetLayout()->IsHideRedlines() ) + return false; + + bool bIsMoved; + const SwRangeRedline* pRedlineNum = lcl_GetRedlineAtNodeInsertionOrDeletion( rTextNode, bIsMoved ); + if (!pRedlineNum) + return false; + + // moved text: dark green with double underline or strikethrough + if ( bIsMoved ) + { + pNumFnt->SetColor(COL_GREEN); + if ( RedlineType::Delete == pRedlineNum->GetType() ) + pNumFnt->SetStrikeout(STRIKEOUT_DOUBLE); + else + pNumFnt->SetUnderline(LINESTYLE_DOUBLE); + return true; + } + + SwAttrPool& rPool = rInf.GetVsh()->GetDoc()->GetAttrPool(); + SfxItemSetFixed<RES_CHRATR_BEGIN, RES_CHRATR_END-1> aSet(rPool); + + std::size_t aAuthor = (1 < pRedlineNum->GetStackCount()) + ? pRedlineNum->GetAuthor( 1 ) + : pRedlineNum->GetAuthor(); + + if ( RedlineType::Delete == pRedlineNum->GetType() ) + SW_MOD()->GetDeletedAuthorAttr(aAuthor, aSet); + else + SW_MOD()->GetInsertAuthorAttr(aAuthor, aSet); + + if (const SvxColorItem* pItem = aSet.GetItemIfSet(RES_CHRATR_COLOR)) + pNumFnt->SetColor(pItem->GetValue()); + if (const SvxUnderlineItem* pItem = aSet.GetItemIfSet(RES_CHRATR_UNDERLINE)) + pNumFnt->SetUnderline(pItem->GetLineStyle()); + if (const SvxCrossedOutItem* pItem = aSet.GetItemIfSet(RES_CHRATR_CROSSEDOUT)) + pNumFnt->SetStrikeout( pItem->GetStrikeout() ); + + return true; +} + +SwNumberPortion *SwTextFormatter::NewNumberPortion( SwTextFormatInfo &rInf ) const +{ + if( rInf.IsNumDone() || rInf.GetTextStart() != m_nStart + || rInf.GetTextStart() != rInf.GetIdx() ) + return nullptr; + + SwNumberPortion *pRet = nullptr; + // sw_redlinehide: at this point it's certain that pTextNd is the node with + // the numbering of the frame; only the actual number-vector (GetNumString) + // depends on the hide-mode in the layout so other calls don't need to care + const SwTextNode *const pTextNd = GetTextFrame()->GetTextNodeForParaProps(); + const SwNumRule* pNumRule = pTextNd->GetNumRule(); + + // Has a "valid" number? + // sw_redlinehide: check that pParaPropsNode is the correct one + assert(pTextNd->IsNumbered(m_pFrame->getRootFrame()) == pTextNd->IsNumbered(nullptr)); + if (pTextNd->IsNumbered(m_pFrame->getRootFrame()) && pTextNd->IsCountedInList()) + { + int nLevel = pTextNd->GetActualListLevel(); + + if (nLevel < 0) + nLevel = 0; + + if (nLevel >= MAXLEVEL) + nLevel = MAXLEVEL - 1; + + const SwNumFormat &rNumFormat = pNumRule->Get( nLevel ); + const bool bLeft = SvxAdjust::Left == rNumFormat.GetNumAdjust(); + const bool bCenter = SvxAdjust::Center == rNumFormat.GetNumAdjust(); + const bool bLabelAlignmentPosAndSpaceModeActive( + rNumFormat.GetPositionAndSpaceMode() == SvxNumberFormat::LABEL_ALIGNMENT ); + const sal_uInt16 nMinDist = bLabelAlignmentPosAndSpaceModeActive + ? 0 : rNumFormat.GetCharTextDistance(); + + if( SVX_NUM_BITMAP == rNumFormat.GetNumberingType() ) + { + OUString referer; + if (auto const sh1 = rInf.GetVsh()) { + if (auto const doc = sh1->GetDoc()) { + auto const sh2 = doc->GetPersist(); + if (sh2 != nullptr && sh2->HasName()) { + referer = sh2->GetMedium()->GetName(); + } + } + } + pRet = new SwGrfNumPortion( pTextNd->GetLabelFollowedBy(), + rNumFormat.GetBrush(), referer, + rNumFormat.GetGraphicOrientation(), + rNumFormat.GetGraphicSize(), + bLeft, bCenter, nMinDist, + bLabelAlignmentPosAndSpaceModeActive ); + tools::Long nTmpA = rInf.GetLast()->GetAscent(); + tools::Long nTmpD = rInf.GetLast()->Height() - nTmpA; + if( !rInf.IsTest() ) + static_cast<SwGrfNumPortion*>(pRet)->SetBase( nTmpA, nTmpD, nTmpA, nTmpD ); + } + else + { + // The SwFont is created dynamically and passed in the ctor, + // as the CharFormat only returns an SV-Font. + // In the dtor of SwNumberPortion, the SwFont is deleted. + const SwAttrSet* pFormat = rNumFormat.GetCharFormat() ? + &rNumFormat.GetCharFormat()->GetAttrSet() : + nullptr; + const IDocumentSettingAccess* pIDSA = pTextNd->getIDocumentSettingAccess(); + + if( SVX_NUM_CHAR_SPECIAL == rNumFormat.GetNumberingType() ) + { + const std::optional<vcl::Font> pFormatFnt = rNumFormat.GetBulletFont(); + + // Build a new bullet font basing on the current paragraph font: + std::unique_ptr<SwFont> pNumFnt(new SwFont( &rInf.GetCharAttr(), pIDSA )); + + // #i53199# + if ( !pIDSA->get(DocumentSettingId::DO_NOT_RESET_PARA_ATTRS_FOR_NUM_FONT) ) + { + // i18463: + // Underline style of paragraph font should not be considered + // Overline style of paragraph font should not be considered + // Weight style of paragraph font should not be considered + // Posture style of paragraph font should not be considered + pNumFnt->SetUnderline( LINESTYLE_NONE ); + pNumFnt->SetOverline( LINESTYLE_NONE ); + pNumFnt->SetItalic( ITALIC_NONE, SwFontScript::Latin ); + pNumFnt->SetItalic( ITALIC_NONE, SwFontScript::CJK ); + pNumFnt->SetItalic( ITALIC_NONE, SwFontScript::CTL ); + pNumFnt->SetWeight( WEIGHT_NORMAL, SwFontScript::Latin ); + pNumFnt->SetWeight( WEIGHT_NORMAL, SwFontScript::CJK ); + pNumFnt->SetWeight( WEIGHT_NORMAL, SwFontScript::CTL ); + } + + // Apply the explicit attributes from the character style + // associated with the numbering to the new bullet font. + if( pFormat ) + pNumFnt->SetDiffFnt( pFormat, pIDSA ); + + checkApplyParagraphMarkFormatToNumbering(pNumFnt.get(), rInf, pIDSA, pFormat); + + if ( pFormatFnt ) + { + const SwFontScript nAct = pNumFnt->GetActual(); + pNumFnt->SetFamily( pFormatFnt->GetFamilyType(), nAct ); + pNumFnt->SetName( pFormatFnt->GetFamilyName(), nAct ); + pNumFnt->SetStyleName( pFormatFnt->GetStyleName(), nAct ); + pNumFnt->SetCharSet( pFormatFnt->GetCharSet(), nAct ); + pNumFnt->SetPitch( pFormatFnt->GetPitch(), nAct ); + } + + // we do not allow a vertical font + pNumFnt->SetVertical( pNumFnt->GetOrientation(), + m_pFrame->IsVertical() ); + + lcl_setRedlineAttr( rInf, *pTextNd, pNumFnt ); + + // --> OD 2008-01-23 #newlistelevelattrs# + if (rNumFormat.GetBulletChar()) + { + pRet = new SwBulletPortion(rNumFormat.GetBulletChar(), + pTextNd->GetLabelFollowedBy(), + std::move(pNumFnt), + bLeft, bCenter, nMinDist, + bLabelAlignmentPosAndSpaceModeActive); + } + } + else + { + // Show Changes mode shows the actual numbering (SwListRedlineType::HIDDEN) and + // the original one (SwListRedlineType::ORIGTEXT) instead of the fake numbering + // (SwListRedlineType::SHOW, which counts removed and inserted numbered paragraphs + // in a single list) + bool bHasHiddenNum = false; + OUString aText( pTextNd->GetNumString(true, MAXLEVEL, m_pFrame->getRootFrame(), SwListRedlineType::HIDDEN) ); + const SwDoc& rDoc = pTextNd->GetDoc(); + const SwRedlineTable& rTable = rDoc.getIDocumentRedlineAccess().GetRedlineTable(); + if ( rTable.size() && !rInf.GetVsh()->GetLayout()->IsHideRedlines() ) + { + OUString aHiddenText( pTextNd->GetNumString(true, MAXLEVEL, m_pFrame->getRootFrame(), SwListRedlineType::ORIGTEXT) ); + + if ( !aText.isEmpty() || !aHiddenText.isEmpty() ) + { + bool bDisplayChangedParagraphNumbering = officecfg::Office::Writer::Comparison::DisplayChangedParagraphNumbering::get(); + if (bDisplayChangedParagraphNumbering && aText != aHiddenText && !aHiddenText.isEmpty()) + { + bHasHiddenNum = true; + // show also original number after the actual one enclosed in [ and ], + // and replace tabulator with space to avoid messy indentation + // resulted by the longer numbering, e.g. "1.[2.]" instead of "1.". + aText = aText + "[" + aHiddenText + "]" + + pTextNd->GetLabelFollowedBy().replaceAll("\t", " "); + } + else if (!aText.isEmpty()) + aText += pTextNd->GetLabelFollowedBy(); + } + } + else if (pTextNd->getIDocumentSettingAccess()->get(DocumentSettingId::NO_NUMBERING_SHOW_FOLLOWBY) + || !aText.isEmpty()) + aText += pTextNd->GetLabelFollowedBy(); + + // Not just an optimization ... + // A number portion without text will be assigned a width of 0. + // The succeeding text portion will flow into the BreakCut in the BreakLine, + // although we have rInf.GetLast()->GetFlyPortion()! + if( !aText.isEmpty() ) + { + + // Build a new numbering font basing on the current paragraph font: + std::unique_ptr<SwFont> pNumFnt(new SwFont( &rInf.GetCharAttr(), pIDSA )); + + const SwTextNode& rTextNode = *rInf.GetTextFrame()->GetTextNodeForParaProps(); + if (const SwpHints* pHints = rTextNode.GetpSwpHints()) + { + // Also look for an empty character hint that sits at the paragraph end: + for (size_t i = 0; i < pHints->Count(); ++i) + { + const SwTextAttr* pHint = pHints->GetSortedByEnd(i); + if (pHint->Which() == RES_TXTATR_AUTOFMT && pHint->GetEnd() + && pHint->GetStart() == *pHint->GetEnd() + && pHint->GetStart() == rTextNode.GetText().getLength()) + { + std::shared_ptr<SfxItemSet> pSet + = pHint->GetAutoFormat().GetStyleHandle(); + if (pSet) + { + pNumFnt->SetDiffFnt(pSet.get(), pIDSA); + break; + } + } + } + } + + // #i53199# + if ( !pIDSA->get(DocumentSettingId::DO_NOT_RESET_PARA_ATTRS_FOR_NUM_FONT) ) + { + // i18463: + // Underline style of paragraph font should not be considered + pNumFnt->SetUnderline( LINESTYLE_NONE ); + // Overline style of paragraph font should not be considered + pNumFnt->SetOverline( LINESTYLE_NONE ); + } + + // Apply the explicit attributes from the character style + // associated with the numbering to the new bullet font. + if( pFormat ) + pNumFnt->SetDiffFnt( pFormat, pIDSA ); + + checkApplyParagraphMarkFormatToNumbering(pNumFnt.get(), rInf, pIDSA, pFormat); + + if ( !lcl_setRedlineAttr( rInf, *pTextNd, pNumFnt ) && bHasHiddenNum ) + pNumFnt->SetColor(NON_PRINTING_CHARACTER_COLOR); + + // we do not allow a vertical font + pNumFnt->SetVertical( pNumFnt->GetOrientation(), m_pFrame->IsVertical() ); + + pRet = new SwNumberPortion( aText, std::move(pNumFnt), + bLeft, bCenter, nMinDist, + bLabelAlignmentPosAndSpaceModeActive ); + } + } + } + } + return pRet; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtfly.cxx b/sw/source/core/text/txtfly.cxx new file mode 100644 index 0000000000..ff5566f537 --- /dev/null +++ b/sw/source/core/text/txtfly.cxx @@ -0,0 +1,1493 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <vcl/outdev.hxx> + +#include <pagefrm.hxx> +#include <rootfrm.hxx> +#include <pam.hxx> +#include <swfont.hxx> +#include <swregion.hxx> +#include <dflyobj.hxx> +#include <drawfont.hxx> +#include <flyfrm.hxx> +#include <flyfrms.hxx> +#include <fmtornt.hxx> +#include <frmatr.hxx> +#include <frmtool.hxx> +#include <ndtxt.hxx> +#include <txtfly.hxx> +#include "inftxt.hxx" +#include "porrst.hxx" +#include "txtpaint.hxx" +#include <notxtfrm.hxx> +#include <fmtcnct.hxx> +#include <svx/obj3d.hxx> +#include <editeng/txtrange.hxx> +#include <editeng/lrspitem.hxx> +#include <editeng/ulspitem.hxx> +#include <fmtsrnd.hxx> +#include <fmtanchr.hxx> +#include <frmfmt.hxx> +#include <fmtfollowtextflow.hxx> +#include <pagedesc.hxx> +#include <sortedobjs.hxx> +#include <IDocumentDrawModelAccess.hxx> +#include <IDocumentSettingAccess.hxx> +#include <formatlinebreak.hxx> +#include <svx/svdoedge.hxx> + +#ifdef DBG_UTIL +#include <viewsh.hxx> +#include <doc.hxx> +#endif + +using namespace ::com::sun::star; + +namespace +{ + // #i68520# + struct AnchoredObjOrder + { + bool mbR2L; + SwRectFn mfnRect; + + AnchoredObjOrder( const bool bR2L, + SwRectFn fnRect ) + : mbR2L( bR2L ), + mfnRect( fnRect ) + {} + + bool operator()( const SwAnchoredObject* pListedAnchoredObj, + const SwAnchoredObject* pNewAnchoredObj ) + { + const SwRect& aBoundRectOfListedObj( pListedAnchoredObj->GetObjRectWithSpaces() ); + const SwRect& aBoundRectOfNewObj( pNewAnchoredObj->GetObjRectWithSpaces() ); + if ( ( mbR2L && + ( (aBoundRectOfListedObj.*mfnRect->fnGetRight)() == + (aBoundRectOfNewObj.*mfnRect->fnGetRight)() ) ) || + ( !mbR2L && + ( (aBoundRectOfListedObj.*mfnRect->fnGetLeft)() == + (aBoundRectOfNewObj.*mfnRect->fnGetLeft)() ) ) ) + { + SwTwips nTopDiff = + (*mfnRect->fnYDiff)( (aBoundRectOfNewObj.*mfnRect->fnGetTop)(), + (aBoundRectOfListedObj.*mfnRect->fnGetTop)() ); + if ( nTopDiff == 0 && + ( ( mbR2L && + ( (aBoundRectOfNewObj.*mfnRect->fnGetLeft)() > + (aBoundRectOfListedObj.*mfnRect->fnGetLeft)() ) ) || + ( !mbR2L && + ( (aBoundRectOfNewObj.*mfnRect->fnGetRight)() < + (aBoundRectOfListedObj.*mfnRect->fnGetRight)() ) ) ) ) + { + return true; + } + else if ( nTopDiff > 0 ) + { + return true; + } + } + else if ( ( mbR2L && + ( (aBoundRectOfListedObj.*mfnRect->fnGetRight)() > + (aBoundRectOfNewObj.*mfnRect->fnGetRight)() ) ) || + ( !mbR2L && + ( (aBoundRectOfListedObj.*mfnRect->fnGetLeft)() < + (aBoundRectOfNewObj.*mfnRect->fnGetLeft)() ) ) ) + { + return true; + } + + return false; + } + }; +} + +SwContourCache::SwContourCache() : + mnPointCount( 0 ) +{ +} + +SwContourCache::~SwContourCache() +{ +} + +void SwContourCache::ClrObject( sal_uInt16 nPos ) +{ + mnPointCount -= mvItems[ nPos ].mxTextRanger->GetPointCount(); + mvItems.erase(mvItems.begin() + nPos); +} + +void ClrContourCache( const SdrObject *pObj ) +{ + if( pContourCache && pObj ) + for( sal_uInt16 i = 0; i < pContourCache->GetCount(); ++i ) + if( pObj == pContourCache->GetObject( i ) ) + { + pContourCache->ClrObject( i ); + break; + } +} + +void ClrContourCache() +{ + if( pContourCache ) + { + pContourCache->mvItems.clear(); + pContourCache->mnPointCount = 0; + } +} + +// #i68520# +SwRect SwContourCache::CalcBoundRect( const SwAnchoredObject* pAnchoredObj, + const SwRect &rLine, + const SwTextFrame* pFrame, + const tools::Long nXPos, + const bool bRight ) +{ + SwRect aRet; + const SwFrameFormat* pFormat = &(pAnchoredObj->GetFrameFormat()); + bool bHandleContour(pFormat->GetSurround().IsContour()); + + if(!bHandleContour) + { + // RotateFlyFrame3: Object has no set contour, but for rotated + // FlyFrames we can create a 'default' contour to make text + // flow around the free, non-covered + const SwFlyFreeFrame* pSwFlyFreeFrame(dynamic_cast< const SwFlyFreeFrame* >(pAnchoredObj)); + + if(nullptr != pSwFlyFreeFrame && pSwFlyFreeFrame->supportsAutoContour()) + { + bHandleContour = true; + } + } + + if( bHandleContour && + ( pAnchoredObj->DynCastFlyFrame() == nullptr || + ( static_cast<const SwFlyFrame*>(pAnchoredObj)->Lower() && + static_cast<const SwFlyFrame*>(pAnchoredObj)->Lower()->IsNoTextFrame() ) ) ) + { + aRet = pAnchoredObj->GetObjRectWithSpaces(); + if( aRet.Overlaps( rLine ) ) + { + if( !pContourCache ) + pContourCache = new SwContourCache; + + aRet = pContourCache->ContourRect( + pFormat, pAnchoredObj->GetDrawObj(), pFrame, rLine, nXPos, bRight ); + } + else + aRet.Width( 0 ); + } + else + { + aRet = pAnchoredObj->GetObjRectWithSpaces(); + } + + return aRet; +} + +SwRect SwContourCache::ContourRect( const SwFormat* pFormat, + const SdrObject* pObj, const SwTextFrame* pFrame, const SwRect &rLine, + const tools::Long nXPos, const bool bRight ) +{ + SwRect aRet; + sal_uInt16 nPos = 0; // Search in the Cache + while( nPos < GetCount() && pObj != mvItems[ nPos ].mpSdrObj ) + ++nPos; + if( GetCount() == nPos ) // Not found + { + if( GetCount() == POLY_CNT ) + { + mnPointCount -= mvItems.back().mxTextRanger->GetPointCount(); + mvItems.pop_back(); + } + ::basegfx::B2DPolyPolygon aPolyPolygon; + std::optional<::basegfx::B2DPolyPolygon> pPolyPolygon; + + if ( auto pVirtFlyDrawObj = dynamic_cast< const SwVirtFlyDrawObj *>( pObj ) ) + { + // GetContour() causes the graphic to be loaded, which may cause + // the graphic to change its size, call ClrObject() + tools::PolyPolygon aPoly; + if( !pVirtFlyDrawObj->GetFlyFrame()->GetContour( aPoly ) ) + aPoly = tools::PolyPolygon( pVirtFlyDrawObj-> + GetFlyFrame()->getFrameArea().SVRect() ); + aPolyPolygon.clear(); + aPolyPolygon.append(aPoly.getB2DPolyPolygon()); + } + else + { + if( DynCastE3dObject( pObj ) == nullptr ) + { + aPolyPolygon = pObj->TakeXorPoly(); + } + + pPolyPolygon = pObj->TakeContour(); + } + const SvxLRSpaceItem &rLRSpace = pFormat->GetLRSpace(); + const SvxULSpaceItem &rULSpace = pFormat->GetULSpace(); + CacheItem item { + pObj, // due to #37347 the Object must be entered only after GetContour() + std::make_unique<TextRanger>( aPolyPolygon, pPolyPolygon ? &*pPolyPolygon : nullptr, 20, + o3tl::narrowing<sal_uInt16>(rLRSpace.GetLeft()), o3tl::narrowing<sal_uInt16>(rLRSpace.GetRight()), + pFormat->GetSurround().IsOutside(), false, pFrame->IsVertical() ) + }; + mvItems.insert(mvItems.begin(), std::move(item)); + mvItems[0].mxTextRanger->SetUpper( rULSpace.GetUpper() ); + mvItems[0].mxTextRanger->SetLower( rULSpace.GetLower() ); + + pPolyPolygon.reset(); + + mnPointCount += mvItems[0].mxTextRanger->GetPointCount(); + while( mnPointCount > POLY_MAX && mvItems.size() > POLY_MIN ) + { + mnPointCount -= mvItems.back().mxTextRanger->GetPointCount(); + mvItems.pop_back(); + } + } + else if( nPos ) + { + CacheItem item = std::move(mvItems[nPos]); + mvItems.erase(mvItems.begin() + nPos); + mvItems.insert(mvItems.begin(), std::move(item)); + } + SwRectFnSet aRectFnSet(pFrame); + tools::Long nTmpTop = aRectFnSet.GetTop(rLine); + // fnGetBottom is top + height + tools::Long nTmpBottom = aRectFnSet.GetBottom(rLine); + + Range aRange( std::min( nTmpTop, nTmpBottom ), std::max( nTmpTop, nTmpBottom ) ); + + std::deque<tools::Long>* pTmp = mvItems[0].mxTextRanger->GetTextRanges( aRange ); + + const size_t nCount = pTmp->size(); + if( 0 != nCount ) + { + size_t nIdx = 0; + while( nIdx < nCount && (*pTmp)[ nIdx ] < nXPos ) + ++nIdx; + bool bOdd = nIdx % 2; + bool bSet = true; + if( bOdd ) + --nIdx; // within interval + else if( ! bRight && ( nIdx >= nCount || (*pTmp)[ nIdx ] != nXPos ) ) + { + if( nIdx ) + nIdx -= 2; // an interval to the left + else + bSet = false; // before the first interval + } + + if( bSet && nIdx < nCount ) + { + aRectFnSet.SetTopAndHeight( aRet, aRectFnSet.GetTop(rLine), + aRectFnSet.GetHeight(rLine) ); + aRectFnSet.SetLeft( aRet, (*pTmp)[ nIdx ] ); + aRectFnSet.SetRight( aRet, (*pTmp)[ nIdx + 1 ] + 1 ); + } + } + return aRet; +} + +SwTextFly::SwTextFly() + : m_pPage(nullptr) + , mpCurrAnchoredObj(nullptr) + , m_pCurrFrame(nullptr) + , m_pMaster(nullptr) + , m_nMinBottom(0) + , m_nNextTop(0) + , m_nCurrFrameNodeIndex(0) + , m_bOn(false) + , m_bTopRule(false) + , mbIgnoreCurrentFrame(false) + , mbIgnoreContour(false) + , mbIgnoreObjsInHeaderFooter(false) + +{ +} + +SwTextFly::SwTextFly( const SwTextFrame *pFrame ) +{ + CtorInitTextFly( pFrame ); +} + +SwTextFly::SwTextFly( const SwTextFly& rTextFly ) +{ + m_pPage = rTextFly.m_pPage; + mpCurrAnchoredObj = rTextFly.mpCurrAnchoredObj; + m_pCurrFrame = rTextFly.m_pCurrFrame; + m_pMaster = rTextFly.m_pMaster; + if( rTextFly.mpAnchoredObjList ) + { + mpAnchoredObjList.reset( new SwAnchoredObjList( *(rTextFly.mpAnchoredObjList) ) ); + } + + m_bOn = rTextFly.m_bOn; + m_bTopRule = rTextFly.m_bTopRule; + m_nMinBottom = rTextFly.m_nMinBottom; + m_nNextTop = rTextFly.m_nNextTop; + m_nCurrFrameNodeIndex = rTextFly.m_nCurrFrameNodeIndex; + mbIgnoreCurrentFrame = rTextFly.mbIgnoreCurrentFrame; + mbIgnoreContour = rTextFly.mbIgnoreContour; + mbIgnoreObjsInHeaderFooter = rTextFly.mbIgnoreObjsInHeaderFooter; +} + +SwTextFly::~SwTextFly() +{ +} + +void SwTextFly::CtorInitTextFly( const SwTextFrame *pFrame ) +{ + mbIgnoreCurrentFrame = false; + mbIgnoreContour = false; + mbIgnoreObjsInHeaderFooter = false; + m_pPage = pFrame->FindPageFrame(); + const SwFlyFrame* pTmp = pFrame->FindFlyFrame(); + // #i68520# + mpCurrAnchoredObj = pTmp; + m_pCurrFrame = pFrame; + m_pMaster = m_pCurrFrame->IsFollow() ? nullptr : m_pCurrFrame; + // If we're not overlapped by a frame or if a FlyCollection does not exist + // at all, we switch off forever. + // It could be, however, that a line is added while formatting, that + // extends into a frame. + // That's why we do not optimize for: bOn = pSortedFlys && IsAnyFrame(); + m_bOn = m_pPage->GetSortedObjs() != nullptr; + m_bTopRule = true; + m_nMinBottom = 0; + m_nNextTop = 0; + m_nCurrFrameNodeIndex = NODE_OFFSET_MAX; +} + +SwRect SwTextFly::GetFrame_( const SwRect &rRect ) const +{ + SwRect aRet; + if( ForEach( rRect, &aRet, true ) ) + { + SwRectFnSet aRectFnSet(m_pCurrFrame); + aRectFnSet.SetTop( aRet, aRectFnSet.GetTop(rRect) ); + + // Do not always adapt the bottom + const SwTwips nRetBottom = aRectFnSet.GetBottom(aRet); + const SwTwips nRectBottom = aRectFnSet.GetBottom(rRect); + if ( aRectFnSet.YDiff( nRetBottom, nRectBottom ) > 0 || + aRectFnSet.GetHeight(aRet) < 0 ) + aRectFnSet.SetBottom( aRet, nRectBottom ); + } + return aRet; +} + +bool SwTextFly::IsAnyFrame() const +{ + SwSwapIfSwapped swap(const_cast<SwTextFrame *>(m_pCurrFrame)); + + OSL_ENSURE( m_bOn, "IsAnyFrame: Why?" ); + SwRect aRect(m_pCurrFrame->getFrameArea().Pos() + m_pCurrFrame->getFramePrintArea().Pos(), + m_pCurrFrame->getFramePrintArea().SSize()); + + return ForEach( aRect, nullptr, false ); +} + +bool SwTextFly::IsAnyObj( const SwRect &rRect ) const +{ + OSL_ENSURE( m_bOn, "SwTextFly::IsAnyObj: Who's knocking?" ); + + SwRect aRect( rRect ); + if ( aRect.IsEmpty() ) + { + aRect = SwRect(m_pCurrFrame->getFrameArea().Pos() + m_pCurrFrame->getFramePrintArea().Pos(), + m_pCurrFrame->getFramePrintArea().SSize()); + + SwTwips nLower = m_pCurrFrame->GetLowerMarginForFlyIntersect(); + if (nLower > 0) + { + aRect.AddBottom(nLower); + } + } + + const SwSortedObjs *pSorted = m_pPage->GetSortedObjs(); + if( pSorted ) // bOn actually makes sure that we have objects on the side, + // but who knows who deleted something in the meantime? + { + for ( size_t i = 0; i < pSorted->size(); ++i ) + { + const SwAnchoredObject* pObj = (*pSorted)[i]; + + const SwRect aBound( pObj->GetObjRectWithSpaces() ); + + // Optimization + if( pObj->GetObjRect().Left() > aRect.Right() ) + continue; + + // #i68520# + if( mpCurrAnchoredObj != pObj && aBound.Overlaps( aRect ) ) + return true; + } + } + return false; +} + +const SwTextFrame* SwTextFly::GetMaster_() +{ + m_pMaster = m_pCurrFrame; + while (m_pMaster && m_pMaster->IsFollow()) + m_pMaster = m_pMaster->FindMaster(); + return m_pMaster; +} + +void SwTextFly::DrawTextOpaque( SwDrawTextInfo &rInf ) +{ + SwSaveClip aClipSave( rInf.GetpOut() ); + SwRect aRect( rInf.GetPos(), rInf.GetSize() ); + if( rInf.GetSpace() ) + { + TextFrameIndex const nTmpLen = TextFrameIndex(COMPLETE_STRING) == rInf.GetLen() + ? TextFrameIndex(rInf.GetText().getLength()) + : rInf.GetLen(); + if( rInf.GetSpace() > 0 ) + { + sal_Int32 nSpaceCnt = 0; + const TextFrameIndex nEndPos = rInf.GetIdx() + nTmpLen; + for (TextFrameIndex nPos = rInf.GetIdx(); nPos < nEndPos; ++nPos) + { + if (CH_BLANK == rInf.GetText()[sal_Int32(nPos)]) + ++nSpaceCnt; + } + if( nSpaceCnt ) + aRect.Width( aRect.Width() + nSpaceCnt * rInf.GetSpace() ); + } + else + aRect.Width( aRect.Width() - sal_Int32(nTmpLen) * rInf.GetSpace() ); + } + + if( aClipSave.IsOn() && rInf.GetOut().IsClipRegion() ) + { + SwRect aClipRect( rInf.GetOut().GetClipRegion().GetBoundRect() ); + aRect.Intersection( aClipRect ); + } + + SwRegionRects aRegion( aRect ); + + bool bOpaque = false; + // #i68520# + const sal_uInt32 nCurrOrd = mpCurrAnchoredObj + ? mpCurrAnchoredObj->GetDrawObj()->GetOrdNum() + : SAL_MAX_UINT32; + OSL_ENSURE( !m_bTopRule, "DrawTextOpaque: Wrong TopRule" ); + + // #i68520# + const SwAnchoredObjList::size_type nCount( m_bOn ? GetAnchoredObjList().size() : 0 ); + if (nCount > 0) + { + const SdrLayerID nHellId = m_pPage->getRootFrame()->GetCurrShell()->getIDocumentDrawModelAccess().GetHellId(); + for( SwAnchoredObjList::size_type i = 0; i < nCount; ++i ) + { + // #i68520# + const SwAnchoredObject* pTmpAnchoredObj = (*mpAnchoredObjList)[i]; + const SwFlyFrame* pFly = pTmpAnchoredObj->DynCastFlyFrame(); + if( pFly && mpCurrAnchoredObj != pTmpAnchoredObj ) + { + // #i68520# + if( aRegion.GetOrigin().Overlaps( pFly->getFrameArea() ) ) + { + const SwFrameFormat *pFormat = pFly->GetFormat(); + const SwFormatSurround &rSur = pFormat->GetSurround(); + const SwFormatAnchor& rAnchor = pFormat->GetAnchor(); + // Only the ones who are opaque and more to the top + if( ! pFly->IsBackgroundTransparent() && + css::text::WrapTextMode_THROUGH == rSur.GetSurround() && + ( !rSur.IsAnchorOnly() || + // #i68520# + GetMaster() == pFly->GetAnchorFrame() || + ((RndStdIds::FLY_AT_PARA != rAnchor.GetAnchorId()) && + (RndStdIds::FLY_AT_CHAR != rAnchor.GetAnchorId()) + ) + ) && + // #i68520# + pTmpAnchoredObj->GetDrawObj()->GetLayer() != nHellId && + nCurrOrd < pTmpAnchoredObj->GetDrawObj()->GetOrdNum() + ) + { + // Except for the content is transparent + const SwNoTextFrame *pNoText = + pFly->Lower() && pFly->Lower()->IsNoTextFrame() + ? static_cast<const SwNoTextFrame*>(pFly->Lower()) + : nullptr; + if ( !pNoText || + (!pNoText->IsTransparent() && !rSur.IsContour()) ) + { + bOpaque = true; + aRegion -= pFly->getFrameArea(); + } + } + } + } + } + } + + Point aPos( rInf.GetPos().X(), rInf.GetPos().Y() + rInf.GetAscent() ); + const Point aOldPos(rInf.GetPos()); + rInf.SetPos( aPos ); + + if( !bOpaque ) + { + if( rInf.GetKern() ) + rInf.GetFont()->DrawStretchText_( rInf ); + else + rInf.GetFont()->DrawText_( rInf ); + rInf.SetPos(aOldPos); + return; + } + else if( !aRegion.empty() ) + { + // What a huge effort ... + SwSaveClip aClipVout( rInf.GetpOut() ); + for( size_t i = 0; i < aRegion.size(); ++i ) + { + SwRect &rRect = aRegion[i]; + if( rRect != aRegion.GetOrigin() ) + aClipVout.ChgClip( rRect ); + if( rInf.GetKern() ) + rInf.GetFont()->DrawStretchText_( rInf ); + else + rInf.GetFont()->DrawText_( rInf ); + } + } + rInf.SetPos(aOldPos); +} + +void SwTextFly::DrawFlyRect( OutputDevice* pOut, const SwRect &rRect ) +{ + SwRegionRects aRegion( rRect ); + OSL_ENSURE( !m_bTopRule, "DrawFlyRect: Wrong TopRule" ); + // #i68520# + const SwAnchoredObjList::size_type nCount( m_bOn ? GetAnchoredObjList().size() : 0 ); + if (nCount > 0) + { + const SdrLayerID nHellId = m_pPage->getRootFrame()->GetCurrShell()->getIDocumentDrawModelAccess().GetHellId(); + for( SwAnchoredObjList::size_type i = 0; i < nCount; ++i ) + { + // #i68520# + const SwAnchoredObject* pAnchoredObjTmp = (*mpAnchoredObjList)[i]; + if (mpCurrAnchoredObj == pAnchoredObjTmp) + continue; + + // #i68520# + const SwFlyFrame* pFly = pAnchoredObjTmp->DynCastFlyFrame(); + if (pFly) + { + // #i68520# + const SwFormatSurround& rSur = pAnchoredObjTmp->GetFrameFormat().GetSurround(); + + // OD 24.01.2003 #106593# - correct clipping of fly frame area. + // Consider that fly frame background/shadow can be transparent + // and <SwAlignRect(..)> fly frame area + // #i47804# - consider transparent graphics + // and OLE objects. + bool bClipFlyArea = + ( ( css::text::WrapTextMode_THROUGH == rSur.GetSurround() ) + // #i68520# + ? (pAnchoredObjTmp->GetDrawObj()->GetLayer() != nHellId) + : !rSur.IsContour() ) && + !pFly->IsBackgroundTransparent() && + ( !pFly->Lower() || + !pFly->Lower()->IsNoTextFrame() || + !static_cast<const SwNoTextFrame*>(pFly->Lower())->IsTransparent() ); + if ( bClipFlyArea ) + { + // #i68520# + SwRect aFly( pAnchoredObjTmp->GetObjRect() ); + // OD 24.01.2003 #106593# + ::SwAlignRect( aFly, m_pPage->getRootFrame()->GetCurrShell(), pOut ); + if( !aFly.IsEmpty() ) + aRegion -= aFly; + } + } + } + } + + for( size_t i = 0; i < aRegion.size(); ++i ) + { + pOut->DrawRect( aRegion[i].SVRect() ); + } +} + +/** + * #i26945# - change first parameter + * Now it's the <SwAnchoredObject> instance of the floating screen object + */ +bool SwTextFly::GetTop( const SwAnchoredObject* _pAnchoredObj, + const bool bInFootnote, + const bool bInFooterOrHeader ) +{ + // #i68520# + // <mpCurrAnchoredObj> is set, if <m_pCurrFrame> is inside a fly frame + if( _pAnchoredObj != mpCurrAnchoredObj ) + { + // #i26945# + const SdrObject* pNew = _pAnchoredObj->GetDrawObj(); + // #102344# Ignore connectors which have one or more connections + if (const SdrEdgeObj* pEdgeObj = dynamic_cast<const SdrEdgeObj*>(pNew)) + { + if (pEdgeObj->GetConnectedNode(true) || pEdgeObj->GetConnectedNode(false)) + { + return false; + } + } + + if( ( bInFootnote || bInFooterOrHeader ) && m_bTopRule ) + { + // #i26945# + const SwFrameFormat& rFrameFormat = _pAnchoredObj->GetFrameFormat(); + const SwFormatAnchor& rNewA = rFrameFormat.GetAnchor(); + if (RndStdIds::FLY_AT_PAGE == rNewA.GetAnchorId()) + { + if ( bInFootnote ) + return false; + + if ( bInFooterOrHeader ) + { + const SwFormatVertOrient& aVert( rFrameFormat.GetVertOrient() ); + bool bVertPrt = aVert.GetRelationOrient() == text::RelOrientation::PRINT_AREA || + aVert.GetRelationOrient() == text::RelOrientation::PAGE_PRINT_AREA; + if( bVertPrt ) + return false; + } + } + } + + // #i68520# + // bEvade: consider pNew, if we are not inside a fly + // consider pNew, if pNew is lower of <mpCurrAnchoredObj> + bool bEvade = !mpCurrAnchoredObj || + Is_Lower_Of( mpCurrAnchoredObj->DynCastFlyFrame(), pNew); + + auto pFly = _pAnchoredObj->DynCastFlyFrame(); + if (pFly && pFly->IsFlySplitAllowed()) + { + // Check if _pAnchoredObj is a split fly inside an other split fly. Always collect such + // flys, otherwise the inner anchor text will overlap with the inner fly. + SwFrame* pFlyAnchor = const_cast<SwAnchoredObject*>(_pAnchoredObj) + ->GetAnchorFrameContainingAnchPos(); + if (pFlyAnchor && pFlyAnchor->IsInFly()) + { + auto pOuterFly = pFlyAnchor->FindFlyFrame(); + if (pOuterFly && pOuterFly->IsFlySplitAllowed()) + { + return true; + } + } + } + + if ( !bEvade ) + { + // We are currently inside a fly frame and pNew is not + // inside this fly frame. We can do some more checks if + // we have to consider pNew. + + // If bTopRule is not set, we ignore the frame types. + // We directly check the z-order + if ( !m_bTopRule ) + bEvade = true; + else + { + // Within chained Flys we only avoid Lower + // #i68520# + const SwFormatChain &rChain = mpCurrAnchoredObj->GetFrameFormat().GetChain(); + if ( !rChain.GetPrev() && !rChain.GetNext() ) + { + // #i26945# + const SwFormatAnchor& rNewA = _pAnchoredObj->GetFrameFormat().GetAnchor(); + // #i68520# + const SwFormatAnchor& rCurrA = mpCurrAnchoredObj->GetFrameFormat().GetAnchor(); + + // If <mpCurrAnchoredObj> is anchored as character, its content + // does not wrap around pNew + if (RndStdIds::FLY_AS_CHAR == rCurrA.GetAnchorId()) + return false; + + // If pNew is anchored to page and <mpCurrAnchoredObj is not anchored + // to page, the content of <mpCurrAnchoredObj> does not wrap around pNew + // If both pNew and <mpCurrAnchoredObj> are anchored to page, we can do + // some more checks + if (RndStdIds::FLY_AT_PAGE == rNewA.GetAnchorId()) + { + if (RndStdIds::FLY_AT_PAGE == rCurrA.GetAnchorId()) + { + bEvade = true; + } + else + return false; + } + else if (RndStdIds::FLY_AT_PAGE == rCurrA.GetAnchorId()) + return false; // Page anchored ones only avoid page anchored ones + else if (RndStdIds::FLY_AT_FLY == rNewA.GetAnchorId()) + bEvade = true; // Non-page anchored ones avoid frame anchored ones + else if( RndStdIds::FLY_AT_FLY == rCurrA.GetAnchorId() ) + return false; // Frame anchored ones do not avoid paragraph anchored ones + // #i57062# + // In order to avoid loop situation, it's decided to adjust + // the wrapping behaviour of content of at-paragraph/at-character + // anchored objects to one in the page header/footer and + // the document body --> content of at-paragraph/at-character + // anchored objects doesn't wrap around each other. + else + return false; + } + } + + // But: we never avoid a subordinate one and additionally we only avoid when overlapping. + // #i68520# + bEvade &= ( mpCurrAnchoredObj->GetDrawObj()->GetOrdNum() < pNew->GetOrdNum() ); + if( bEvade ) + { + // #i68520# + const SwRect& aTmp( _pAnchoredObj->GetObjRectWithSpaces() ); + if ( !aTmp.Overlaps( mpCurrAnchoredObj->GetObjRectWithSpaces() ) ) + bEvade = false; + } + } + + if ( bEvade ) + { + // #i26945# + const SwFormatAnchor& rNewA = _pAnchoredObj->GetFrameFormat().GetAnchor(); + OSL_ENSURE( RndStdIds::FLY_AS_CHAR != rNewA.GetAnchorId(), + "Don't call GetTop with a FlyInContentFrame" ); + if (RndStdIds::FLY_AT_PAGE == rNewA.GetAnchorId()) + return true; // We always avoid page anchored ones + + // If Flys anchored at paragraph are caught in a FlyCnt, then + // their influence ends at the borders of the FlyCnt! + // If we are currently formatting the text of the FlyCnt, then + // it has to get out of the way of the Frame anchored at paragraph! + // m_pCurrFrame is the anchor of pNew? + // #i26945# + const SwFrame* pTmp = _pAnchoredObj->GetAnchorFrame(); + if (pTmp == m_pCurrFrame) + return true; + if( pTmp->IsTextFrame() && ( pTmp->IsInFly() || pTmp->IsInFootnote() ) ) + { + // #i26945# + Point aPos = _pAnchoredObj->GetObjRect().Pos(); + pTmp = GetVirtualUpper( pTmp, aPos ); + } + // #i26945# + // If <pTmp> is a text frame inside a table, take the upper + // of the anchor frame, which contains the anchor position. + else if ( pTmp->IsTextFrame() && pTmp->IsInTab() ) + { + pTmp = const_cast<SwAnchoredObject*>(_pAnchoredObj) + ->GetAnchorFrameContainingAnchPos()->GetUpper(); + } + // #i28701# - consider all objects in same context, + // if wrapping style is considered on object positioning. + // Thus, text will wrap around negative positioned objects. + // #i3317# - remove condition on checking, + // if wrappings style is considered on object positioning. + // Thus, text is wrapping around negative positioned objects. + // #i35640# - no consideration of negative + // positioned objects, if wrapping style isn't considered on + // object position and former text wrapping is applied. + // This condition is typically for documents imported from the + // OpenOffice.org file format. + const IDocumentSettingAccess* pIDSA = &m_pCurrFrame->GetDoc().getIDocumentSettingAccess(); + if ( ( pIDSA->get(DocumentSettingId::CONSIDER_WRAP_ON_OBJECT_POSITION) || + !pIDSA->get(DocumentSettingId::USE_FORMER_TEXT_WRAPPING) ) && + ::FindContext( pTmp, SwFrameType::None ) == ::FindContext(m_pCurrFrame, SwFrameType::None)) + { + return true; + } + + const SwFrame* pHeader = nullptr; + if (m_pCurrFrame->GetNext() != pTmp && + (IsFrameInSameContext( pTmp, m_pCurrFrame ) || + // #i13832#, #i24135# wrap around objects in page header + ( !pIDSA->get(DocumentSettingId::USE_FORMER_TEXT_WRAPPING) && + nullptr != ( pHeader = pTmp->FindFooterOrHeader() ) && + m_pCurrFrame->IsInDocBody()))) + { + if( pHeader || RndStdIds::FLY_AT_FLY == rNewA.GetAnchorId() ) + return true; + + // Compare indices: + // The Index of the other is retrieved from the anchor attr. + SwNodeOffset nTmpIndex = rNewA.GetAnchorNode()->GetIndex(); + // Now check whether the current paragraph is before the anchor + // of the displaced object in the text, then we don't have to + // get out of its way. + // If possible determine Index via SwFormatAnchor because + // otherwise it's quite expensive. + if (NODE_OFFSET_MAX == m_nCurrFrameNodeIndex) + m_nCurrFrameNodeIndex = m_pCurrFrame->GetTextNodeFirst()->GetIndex(); + + if (FrameContainsNode(*m_pCurrFrame, nTmpIndex) || nTmpIndex < m_nCurrFrameNodeIndex) + return true; + } + } + } + return false; +} + +SwRect SwTextFly::GetFrameArea() const +{ + // i#28701 - consider complete frame area for new text wrapping + SwRect aRect; + if (m_pCurrFrame->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::USE_FORMER_TEXT_WRAPPING)) + { + aRect = m_pCurrFrame->getFramePrintArea(); + aRect += m_pCurrFrame->getFrameArea().Pos(); + } + else + { + aRect = m_pCurrFrame->getFrameArea(); + } + return aRect; +} + +// #i68520# +SwAnchoredObjList& SwTextFly::InitAnchoredObjList() +{ + OSL_ENSURE( m_pCurrFrame, "InitFlyList: No Frame, no FlyList" ); + // #i68520# + OSL_ENSURE( !mpAnchoredObjList, "InitFlyList: FlyList already initialized" ); + + SwSwapIfSwapped swap(const_cast<SwTextFrame *>(m_pCurrFrame)); + + const SwSortedObjs *pSorted = m_pPage->GetSortedObjs(); + const size_t nCount = pSorted ? pSorted->size() : 0; + // --> #108724# Page header/footer content doesn't have to wrap around + // floating screen objects + // which was added simply to be compatible with MS Office. + // MSO still allows text to wrap around in-table-flies in headers/footers/footnotes + const bool bFooterHeader = nullptr != m_pCurrFrame->FindFooterOrHeader(); + const IDocumentSettingAccess* pIDSA = &m_pCurrFrame->GetDoc().getIDocumentSettingAccess(); + // #i40155# - check, if frame is marked not to wrap + const bool bAllowCompatWrap = m_pCurrFrame->IsInTab() && (bFooterHeader || m_pCurrFrame->IsInFootnote()); + const bool bWrapAllowed = ( pIDSA->get(DocumentSettingId::USE_FORMER_TEXT_WRAPPING) || + bAllowCompatWrap || + (!m_pCurrFrame->IsInFootnote() && !bFooterHeader)); + + m_bOn = false; + + // #i68520# + mpAnchoredObjList.reset(new SwAnchoredObjList); + + if( nCount && bWrapAllowed ) + { + SwRect const aRect(GetFrameArea()); + // Make ourselves a little smaller than we are, + // so that 1-Twip-overlappings are ignored (#49532) + SwRectFnSet aRectFnSet(m_pCurrFrame); + const tools::Long nRight = aRectFnSet.GetRight(aRect) - 1; + const tools::Long nLeft = aRectFnSet.GetLeft(aRect) + 1; + const bool bR2L = m_pCurrFrame->IsRightToLeft(); + + const IDocumentDrawModelAccess& rIDDMA = m_pCurrFrame->GetDoc().getIDocumentDrawModelAccess(); + + for( size_t i = 0; i < nCount; ++i ) + { + // #i68520# + // do not consider hidden objects + // check, if object has to be considered for text wrap + // #118809# - If requested, do not consider + // objects in page header|footer for text frames not in page + // header|footer. This is requested for the calculation of + // the base offset for objects <SwTextFrame::CalcBaseOfstForFly()> + // #i20505# Do not consider oversized objects + SwAnchoredObject* pAnchoredObj = (*pSorted)[ i ]; + assert(pAnchoredObj); + if ( !pAnchoredObj || + !rIDDMA.IsVisibleLayerId( pAnchoredObj->GetDrawObj()->GetLayer() ) || + !pAnchoredObj->ConsiderForTextWrap() || + ( mbIgnoreObjsInHeaderFooter && !bFooterHeader && + pAnchoredObj->GetAnchorFrame()->FindFooterOrHeader() ) || + ( bAllowCompatWrap && !pAnchoredObj->GetFrameFormat().GetFollowTextFlow().GetValue() ) + ) + { + continue; + } + + const SwRect aBound( pAnchoredObj->GetObjRectWithSpaces() ); + if ( nRight < aRectFnSet.GetLeft(aBound) || + aRectFnSet.YDiff( aRectFnSet.GetTop(aRect), + aRectFnSet.GetBottom(aBound) ) > 0 || + nLeft > aRectFnSet.GetRight(aBound) || + aRectFnSet.GetHeight(aBound) > + 2 * aRectFnSet.GetHeight(m_pPage->getFrameArea()) ) + { + continue; + } + + // #i26945# - pass <pAnchoredObj> to method + // <GetTop(..)> instead of only the <SdrObject> instance of the + // anchored object + if (GetTop(pAnchoredObj, m_pCurrFrame->IsInFootnote(), bFooterHeader)) + { + // OD 11.03.2003 #107862# - adjust insert position: + // overlapping objects should be sorted from left to right and + // inside left to right sorting from top to bottom. + // If objects on the same position are found, they are sorted + // on its width. + // #i68520# + { + SwAnchoredObjList::iterator aInsPosIter = + std::lower_bound( mpAnchoredObjList->begin(), + mpAnchoredObjList->end(), + pAnchoredObj, + AnchoredObjOrder( bR2L, aRectFnSet.FnRect() ) ); + + mpAnchoredObjList->insert( aInsPosIter, pAnchoredObj ); + } + + const SwFormatSurround &rFlyFormat = pAnchoredObj->GetFrameFormat().GetSurround(); + // #i68520# + if ( rFlyFormat.IsAnchorOnly() && + pAnchoredObj->GetAnchorFrame() == GetMaster() ) + { + const SwFormatVertOrient &rTmpFormat = + pAnchoredObj->GetFrameFormat().GetVertOrient(); + if( text::VertOrientation::BOTTOM != rTmpFormat.GetVertOrient() ) + m_nMinBottom = ( aRectFnSet.IsVert() && m_nMinBottom ) ? + std::min( m_nMinBottom, aBound.Left() ) : + std::max( m_nMinBottom, aRectFnSet.GetBottom(aBound) ); + } + + m_bOn = true; + } + } + if( m_nMinBottom ) + { + SwTwips nMax = aRectFnSet.GetPrtBottom(*m_pCurrFrame->GetUpper()); + if( aRectFnSet.YDiff( m_nMinBottom, nMax ) > 0 ) + m_nMinBottom = nMax; + } + } + + // #i68520# + return *mpAnchoredObjList; +} + +SwTwips SwTextFly::CalcMinBottom() const +{ + SwTwips nRet = 0; + const SwContentFrame *pLclMaster = GetMaster(); + OSL_ENSURE(pLclMaster, "SwTextFly without master"); + const SwSortedObjs *pDrawObj = pLclMaster ? pLclMaster->GetDrawObjs() : nullptr; + const size_t nCount = pDrawObj ? pDrawObj->size() : 0; + if( nCount ) + { + SwTwips nEndOfFrame = m_pCurrFrame->getFrameArea().Bottom(); + for( size_t i = 0; i < nCount; ++i ) + { + SwAnchoredObject* pAnchoredObj = (*pDrawObj)[ i ]; + const SwFormatSurround &rFlyFormat = pAnchoredObj->GetFrameFormat().GetSurround(); + if( rFlyFormat.IsAnchorOnly() ) + { + const SwFormatVertOrient &rTmpFormat = + pAnchoredObj->GetFrameFormat().GetVertOrient(); + if( text::VertOrientation::BOTTOM != rTmpFormat.GetVertOrient() ) + { + const SwRect& aBound( pAnchoredObj->GetObjRectWithSpaces() ); + if( aBound.Top() < nEndOfFrame ) + nRet = std::max( nRet, SwTwips(aBound.Bottom()) ); + } + } + } + SwTwips nMax = m_pCurrFrame->GetUpper()->getFrameArea().Top() + + m_pCurrFrame->GetUpper()->getFramePrintArea().Bottom(); + if( nRet > nMax ) + nRet = nMax; + } + return nRet; +} + +SwTwips SwTextFly::GetMaxBottom(const SwBreakPortion& rPortion, const SwTextFormatInfo& rInfo) const +{ + // Note that m_pCurrFrame is already swapped at this stage, so it's correct to bypass + // SwRectFnSet here. + SwTwips nRet = 0; + size_t nCount(m_bOn ? GetAnchoredObjList().size() : 0); + + // Get the horizontal position of the break portion in absolute twips. The frame area is in + // absolute twips, the frame's print area is relative to the frame area. Finally the portion's + // position is relative to the frame's print area. + SwTwips nX = rInfo.X(); + nX += m_pCurrFrame->getFrameArea().Left(); + nX += m_pCurrFrame->getFramePrintArea().Left(); + + for (size_t i = 0; i < nCount; ++i) + { + const SwAnchoredObject* pAnchoredObj = (*mpAnchoredObjList)[i]; + + if (pAnchoredObj->GetAnchorFrame()->FindFooterOrHeader()) + { + // Anchored in the header or footer, ignore it for clearing break purposes. + continue; + } + + SwRect aRect(pAnchoredObj->GetObjRectWithSpaces()); + + if (m_pCurrFrame->IsVertical()) + { + m_pCurrFrame->SwitchVerticalToHorizontal(aRect); + } + + if (rPortion.GetClear() == SwLineBreakClear::LEFT) + { + if (nX < aRect.Left()) + { + // Want to jump down to the first line that's unblocked on the left. This object is + // on the right of the break, ignore it. + continue; + } + } + if (rPortion.GetClear() == SwLineBreakClear::RIGHT) + { + if (nX > aRect.Right()) + { + // Want to jump down to the first line that's unblocked on the right. This object is + // on the left of the break, ignore it. + continue; + } + } + SwTwips nBottom = aRect.Top() + aRect.Height(); + if (nBottom > nRet) + { + nRet = nBottom; + } + } + return nRet; +} + +bool SwTextFly::ForEach( const SwRect &rRect, SwRect* pRect, bool bAvoid ) const +{ + SwSwapIfSwapped swap(const_cast<SwTextFrame *>(m_pCurrFrame)); + + // Optimization + SwRectFnSet aRectFnSet(m_pCurrFrame); + + // tdf#127235 stop if the area is larger than the page + if( aRectFnSet.GetHeight(m_pPage->getFrameArea()) < aRectFnSet.GetHeight(rRect)) + { + // get the doc model description + const SwPageDesc* pPageDesc = m_pPage->GetPageDesc(); + + // if there is no next page style or it is the same as the current + // => stop trying to place the frame (it would end in an infinite loop) + if( pPageDesc && + ( !pPageDesc->GetFollow() || pPageDesc->GetFollow() == pPageDesc) ) + { + return false; + } + } + + bool bRet = false; + // #i68520# + const SwAnchoredObjList::size_type nCount( m_bOn ? GetAnchoredObjList().size() : 0 ); + if (nCount > 0) + { + for( SwAnchoredObjList::size_type i = 0; i < nCount; ++i ) + { + // #i68520# + const SwAnchoredObject* pAnchoredObj = (*mpAnchoredObjList)[i]; + + SwRect aRect( pAnchoredObj->GetObjRectWithSpaces() ); + + if( aRectFnSet.GetLeft(aRect) > aRectFnSet.GetRight(rRect) ) + break; + + // #i68520# + if ( mpCurrAnchoredObj != pAnchoredObj && aRect.Overlaps( rRect ) ) + { + // #i68520# + const SwFormat* pFormat( &(pAnchoredObj->GetFrameFormat()) ); + const SwFormatSurround &rSur = pFormat->GetSurround(); + if( bAvoid ) + { + // If the text flows below, it has no influence on + // formatting. In LineIter::DrawText() it is "just" + // necessary to cleverly set the ClippingRegions + const SwFormatAnchor& rAnchor = pFormat->GetAnchor(); + if( ( css::text::WrapTextMode_THROUGH == rSur.GetSurround() && + ( !rSur.IsAnchorOnly() || + // #i68520# + GetMaster() == pAnchoredObj->GetAnchorFrame() || + ((RndStdIds::FLY_AT_PARA != rAnchor.GetAnchorId()) && + (RndStdIds::FLY_AT_CHAR != rAnchor.GetAnchorId())) ) ) + || aRect.Top() == FAR_AWAY ) + continue; + } + + // #i58642# + // Compare <GetMaster()> instead of <m_pCurrFrame> with the + // anchor frame of the anchored object, because a follow frame + // has to ignore the anchored objects of its master frame. + // Note: Anchored objects are always registered at the master + // frame, exception are as-character anchored objects, + // but these aren't handled here. + // #i68520# + if ( mbIgnoreCurrentFrame && + GetMaster() == pAnchoredObj->GetAnchorFrame() ) + continue; + + if( pRect ) + { + // #i68520# + SwRect aFly = AnchoredObjToRect( pAnchoredObj, rRect ); + if( aFly.IsEmpty() || !aFly.Overlaps( rRect ) ) + continue; + if( !bRet || ( + (!m_pCurrFrame->IsRightToLeft() && + ( aRectFnSet.GetLeft(aFly) < + aRectFnSet.GetLeft(*pRect) ) ) || + (m_pCurrFrame->IsRightToLeft() && + ( aRectFnSet.GetRight(aFly) > + aRectFnSet.GetRight(*pRect) ) ) ) ) + *pRect = aFly; + if( rSur.IsContour() ) + { + bRet = true; + continue; + } + } + bRet = true; + break; + } + } + } + + return bRet; +} + +// #i68520# +SwAnchoredObjList::size_type SwTextFly::GetPos( const SwAnchoredObject* pAnchoredObj ) const +{ + SwAnchoredObjList::size_type nCount = GetAnchoredObjList().size(); + SwAnchoredObjList::size_type nRet = 0; + while ( nRet < nCount && pAnchoredObj != (*mpAnchoredObjList)[ nRet ] ) + ++nRet; + return nRet; +} + +// #i68520# +void SwTextFly::CalcRightMargin( SwRect &rFly, + SwAnchoredObjList::size_type nFlyPos, + const SwRect &rLine ) const +{ + // Usually the right margin is the right margin of the Printarea + OSL_ENSURE( !m_pCurrFrame->IsVertical() || !m_pCurrFrame->IsSwapped(), + "SwTextFly::CalcRightMargin with swapped frame" ); + SwRectFnSet aRectFnSet(m_pCurrFrame); + // #118796# - correct determination of right of printing area + SwTwips nRight = aRectFnSet.GetPrtRight(*m_pCurrFrame); + SwTwips nFlyRight = aRectFnSet.GetRight(rFly); + SwRect aLine( rLine ); + aRectFnSet.SetRight( aLine, nRight ); + aRectFnSet.SetLeft( aLine, aRectFnSet.GetLeft(rFly) ); + + // It is possible that there is another object that is _above_ us + // and protrudes into the same line. + // Flys with run-through are invisible for those below, i.e., they + // are ignored for computing the margins of other Flys. + // 3301: pNext->getFrameArea().Overlaps( rLine ) is necessary + // #i68520# + css::text::WrapTextMode eSurroundForTextWrap; + + bool bStop = false; + // #i68520# + SwAnchoredObjList::size_type nPos = 0; + + // #i68520# + while( nPos < mpAnchoredObjList->size() && !bStop ) + { + if( nPos == nFlyPos ) + { + ++nPos; + continue; + } + // #i68520# + const SwAnchoredObject* pNext = (*mpAnchoredObjList)[ nPos++ ]; + if ( pNext == mpCurrAnchoredObj ) + continue; + eSurroundForTextWrap = GetSurroundForTextWrap( pNext ); + if( css::text::WrapTextMode_THROUGH == eSurroundForTextWrap ) + continue; + + const SwRect aTmp( SwContourCache::CalcBoundRect + ( pNext, aLine, m_pCurrFrame, nFlyRight, true ) ); + SwTwips nTmpRight = aRectFnSet.GetRight(aTmp); + + // optimization: + // Record in nNextTop at which Y-position frame related changes are + // likely. This is so that, despite only looking at frames in the + // current line height, for frames without wrap the line height is + // incremented so that with a single line the lower border of the frame + // (or possibly the upper border of another frame) is reached. + // Especially in HTML documents there are often (dummy) paragraphs in + // 2 pt font, and they used to only evade big frames after huge numbers + // of empty lines. + const tools::Long nTmpTop = aRectFnSet.GetTop(aTmp); + if( aRectFnSet.YDiff( nTmpTop, aRectFnSet.GetTop(aLine) ) > 0 ) + { + if( aRectFnSet.YDiff( m_nNextTop, nTmpTop ) > 0 ) + SetNextTop( nTmpTop ); // upper border of next frame + } + else if (!aRectFnSet.GetWidth(aTmp)) // typical for Objects with contour wrap + { // For Objects with contour wrap that start before the current + // line, and end below it, but do not actually overlap it, the + // optimization has to be disabled, because the circumstances + // can change in the next line. + if( ! aRectFnSet.GetHeight(aTmp) || + aRectFnSet.YDiff( aRectFnSet.GetBottom(aTmp), + aRectFnSet.GetTop(aLine) ) > 0 ) + SetNextTop( 0 ); + } + if( aTmp.Overlaps( aLine ) && nTmpRight > nFlyRight ) + { + nFlyRight = nTmpRight; + if( css::text::WrapTextMode_RIGHT == eSurroundForTextWrap || + css::text::WrapTextMode_PARALLEL == eSurroundForTextWrap ) + { + // overrule the FlyFrame + if( nRight > nFlyRight ) + nRight = nFlyRight; + bStop = true; + } + } + } + aRectFnSet.SetRight( rFly, nRight ); +} + +// #i68520# +void SwTextFly::CalcLeftMargin( SwRect &rFly, + SwAnchoredObjList::size_type nFlyPos, + const SwRect &rLine ) const +{ + OSL_ENSURE( !m_pCurrFrame->IsVertical() || !m_pCurrFrame->IsSwapped(), + "SwTextFly::CalcLeftMargin with swapped frame" ); + SwRectFnSet aRectFnSet(m_pCurrFrame); + // #118796# - correct determination of left of printing area + SwTwips nLeft = aRectFnSet.GetPrtLeft(*m_pCurrFrame); + const SwTwips nFlyLeft = aRectFnSet.GetLeft(rFly); + + if( nLeft > nFlyLeft ) + nLeft = rFly.Left(); + + SwRect aLine( rLine ); + aRectFnSet.SetLeft( aLine, nLeft ); + + // It is possible that there is another object that is _above_ us + // and protrudes into the same line. + // Flys with run-through are invisible for those below, i.e., they + // are ignored for computing the margins of other Flys. + // 3301: pNext->getFrameArea().Overlaps( rLine ) is necessary + + // #i68520# + SwAnchoredObjList::size_type nMyPos = nFlyPos; + while( ++nFlyPos < mpAnchoredObjList->size() ) + { + // #i68520# + const SwAnchoredObject* pNext = (*mpAnchoredObjList)[ nFlyPos ]; + const SwRect& aTmp( pNext->GetObjRectWithSpaces() ); + if( aRectFnSet.GetLeft(aTmp) >= nFlyLeft ) + break; + } + + while( nFlyPos ) + { + if( --nFlyPos == nMyPos ) + continue; + // #i68520# + const SwAnchoredObject* pNext = (*mpAnchoredObjList)[ nFlyPos ]; + if( pNext == mpCurrAnchoredObj ) + continue; + css::text::WrapTextMode eSurroundForTextWrap = GetSurroundForTextWrap( pNext ); + if( css::text::WrapTextMode_THROUGH == eSurroundForTextWrap ) + continue; + + const SwRect aTmp( SwContourCache::CalcBoundRect + (pNext, aLine, m_pCurrFrame, nFlyLeft, false) ); + + if( aRectFnSet.GetLeft(aTmp) < nFlyLeft && aTmp.Overlaps( aLine ) ) + { + // #118796# - no '+1', because <..fnGetRight> + // returns the correct value. + SwTwips nTmpRight = aRectFnSet.GetRight(aTmp); + if ( nLeft <= nTmpRight ) + nLeft = nTmpRight; + + break; + } + } + aRectFnSet.SetLeft( rFly, nLeft ); +} + +// #i68520# +SwRect SwTextFly::AnchoredObjToRect( const SwAnchoredObject* pAnchoredObj, + const SwRect &rLine ) const +{ + SwRectFnSet aRectFnSet(m_pCurrFrame); + + const tools::Long nXPos = m_pCurrFrame->IsRightToLeft() ? + rLine.Right() : + aRectFnSet.GetLeft(rLine); + + SwRect aFly = mbIgnoreContour ? + pAnchoredObj->GetObjRectWithSpaces() : + SwContourCache::CalcBoundRect(pAnchoredObj, rLine, m_pCurrFrame, + nXPos, !m_pCurrFrame->IsRightToLeft()); + + if( !aFly.Width() ) + return aFly; + + // so the line may grow up to the lower edge of the frame + SetNextTop( aRectFnSet.GetBottom(aFly) ); + SwAnchoredObjList::size_type nFlyPos = GetPos( pAnchoredObj ); + + // LEFT and RIGHT, we grow the rectangle. + // We have some problems, when several frames are to be seen. + // At the moment, only the easier case is assumed: + // + LEFT means that the text must flow on the left of the frame, + // that is the frame expands to the right edge of the print area + // or to the next frame. + // + RIGHT is the opposite. + // Otherwise the set distance between text and frame is always + // added up. + switch( GetSurroundForTextWrap( pAnchoredObj ) ) + { + case css::text::WrapTextMode_LEFT : + { + CalcRightMargin( aFly, nFlyPos, rLine ); + break; + } + case css::text::WrapTextMode_RIGHT : + { + CalcLeftMargin( aFly, nFlyPos, rLine ); + break; + } + case css::text::WrapTextMode_NONE : + { + CalcRightMargin( aFly, nFlyPos, rLine ); + CalcLeftMargin( aFly, nFlyPos, rLine ); + break; + } + default: + break; + } + return aFly; +} + +// #i68520# + +// Wrap only on sides with at least 2cm space for the text +#define TEXT_MIN 1134 + +// Wrap on both sides up to a frame width of 1.5cm +#define FRAME_MAX 850 + +css::text::WrapTextMode SwTextFly::GetSurroundForTextWrap( const SwAnchoredObject* pAnchoredObj ) const +{ + const SwFrameFormat* pFormat = &(pAnchoredObj->GetFrameFormat()); + const SwFormatSurround &rFlyFormat = pFormat->GetSurround(); + css::text::WrapTextMode eSurroundForTextWrap = rFlyFormat.GetSurround(); + + if( rFlyFormat.IsAnchorOnly() && pAnchoredObj->GetAnchorFrame() != GetMaster() ) + { + const SwFormatAnchor& rAnchor = pFormat->GetAnchor(); + if ((RndStdIds::FLY_AT_PARA == rAnchor.GetAnchorId()) || + (RndStdIds::FLY_AT_CHAR == rAnchor.GetAnchorId())) + { + return css::text::WrapTextMode_NONE; + } + } + + // in cause of run-through and nowrap ignore smartly + if( css::text::WrapTextMode_THROUGH == eSurroundForTextWrap || + css::text::WrapTextMode_NONE == eSurroundForTextWrap ) + return eSurroundForTextWrap; + + // left is left and right is right + if (m_pCurrFrame->IsRightToLeft()) + { + if ( css::text::WrapTextMode_LEFT == eSurroundForTextWrap ) + eSurroundForTextWrap = css::text::WrapTextMode_RIGHT; + else if ( css::text::WrapTextMode_RIGHT == eSurroundForTextWrap ) + eSurroundForTextWrap = css::text::WrapTextMode_LEFT; + } + + // "ideal page wrap": + if ( css::text::WrapTextMode_DYNAMIC == eSurroundForTextWrap ) + { + SwRectFnSet aRectFnSet(m_pCurrFrame); + const tools::Long nCurrLeft = aRectFnSet.GetPrtLeft(*m_pCurrFrame); + const tools::Long nCurrRight = aRectFnSet.GetPrtRight(*m_pCurrFrame); + const SwRect& aRect( pAnchoredObj->GetObjRectWithSpaces() ); + tools::Long nFlyLeft = aRectFnSet.GetLeft(aRect); + tools::Long nFlyRight = aRectFnSet.GetRight(aRect); + + if ( nFlyRight < nCurrLeft || nFlyLeft > nCurrRight ) + eSurroundForTextWrap = css::text::WrapTextMode_PARALLEL; + else + { + tools::Long nLeft = nFlyLeft - nCurrLeft; + tools::Long nRight = nCurrRight - nFlyRight; + if( nFlyRight - nFlyLeft > FRAME_MAX ) + { + if( nLeft < nRight ) + nLeft = 0; + else + nRight = 0; + } + const int textMin = GetMaster()->GetDoc() + .getIDocumentSettingAccess().get(DocumentSettingId::SURROUND_TEXT_WRAP_SMALL ) + ? TEXT_MIN_SMALL : TEXT_MIN; + + // In case there is no space on either side, then css::text::WrapTextMode_PARALLEL + // gives the same result when doing the initial layout or a layout + // update after editing, so prefer that over css::text::WrapTextMode_NONE. + if (nLeft == 0 && nRight == 0) + return css::text::WrapTextMode_PARALLEL; + + if( nLeft < textMin ) + nLeft = 0; + if( nRight < textMin ) + nRight = 0; + if( nLeft ) + eSurroundForTextWrap = nRight ? css::text::WrapTextMode_PARALLEL : css::text::WrapTextMode_LEFT; + else + eSurroundForTextWrap = nRight ? css::text::WrapTextMode_RIGHT: css::text::WrapTextMode_NONE; + } + } + + return eSurroundForTextWrap; +} + +bool SwTextFly::IsAnyFrame( const SwRect &rLine ) const +{ + + SwSwapIfSwapped swap(const_cast<SwTextFrame *>(m_pCurrFrame)); + + OSL_ENSURE( m_bOn, "IsAnyFrame: Why?" ); + + return ForEach( rLine, nullptr, false ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtfrm.cxx b/sw/source/core/text/txtfrm.cxx new file mode 100644 index 0000000000..18eb78db83 --- /dev/null +++ b/sw/source/core/text/txtfrm.cxx @@ -0,0 +1,4219 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <config_wasm_strip.h> + +#include <hintids.hxx> +#include <hints.hxx> +#include <svl/ctloptions.hxx> +#include <editeng/lspcitem.hxx> +#include <editeng/lrspitem.hxx> +#include <editeng/brushitem.hxx> +#include <editeng/pgrditem.hxx> +#include <unotools/configmgr.hxx> +#include <swmodule.hxx> +#include <SwSmartTagMgr.hxx> +#include <doc.hxx> +#include <IDocumentSettingAccess.hxx> +#include <IDocumentDeviceAccess.hxx> +#include <IDocumentFieldsAccess.hxx> +#include <rootfrm.hxx> +#include <pagefrm.hxx> +#include <viewsh.hxx> +#include <pam.hxx> +#include <ndtxt.hxx> +#include <paratr.hxx> +#include <viewopt.hxx> +#include <flyfrm.hxx> +#include <tabfrm.hxx> +#include <frmatr.hxx> +#include <frmtool.hxx> +#include <tgrditem.hxx> +#include <dbg_lay.hxx> +#include <fmtfld.hxx> +#include <fmtftn.hxx> +#include <txtfld.hxx> +#include <txtftn.hxx> +#include <ftninfo.hxx> +#include <fmtline.hxx> +#include <txtfrm.hxx> +#include <notxtfrm.hxx> +#include <sectfrm.hxx> +#include "itrform2.hxx" +#include "widorp.hxx" +#include "txtcache.hxx" +#include <fntcache.hxx> +#include <SwGrammarMarkUp.hxx> +#include <lineinfo.hxx> +#include <SwPortionHandler.hxx> +#include <dcontact.hxx> +#include <sortedobjs.hxx> +#include <txtflcnt.hxx> +#include <fmtflcnt.hxx> +#include <fmtcntnt.hxx> +#include <numrule.hxx> +#include <GrammarContact.hxx> +#include <calbck.hxx> +#include <ftnidx.hxx> +#include <ftnfrm.hxx> + +#include <wrtsh.hxx> +#include <view.hxx> +#include <edtwin.hxx> +#include <FrameControlsManager.hxx> + +namespace sw { + + MergedAttrIterBase::MergedAttrIterBase(SwTextFrame const& rFrame) + : m_pMerged(rFrame.GetMergedPara()) + , m_pNode(m_pMerged ? nullptr : rFrame.GetTextNodeFirst()) + , m_CurrentExtent(0) + , m_CurrentHint(0) + { + } + + SwTextAttr const* MergedAttrIter::NextAttr(SwTextNode const** ppNode) + { + if (m_pMerged) + { + while (m_CurrentExtent < m_pMerged->extents.size()) + { + sw::Extent const& rExtent(m_pMerged->extents[m_CurrentExtent]); + if (SwpHints const*const pHints = rExtent.pNode->GetpSwpHints()) + { + while (m_CurrentHint < pHints->Count()) + { + SwTextAttr *const pHint(pHints->Get(m_CurrentHint)); + if (rExtent.nEnd < pHint->GetStart() + // <= if it has no end or isn't empty + || (rExtent.nEnd == pHint->GetStart() + && (!pHint->GetEnd() + || *pHint->GetEnd() != pHint->GetStart()))) + { + break; + } + ++m_CurrentHint; + if (rExtent.nStart <= pHint->GetStart()) + { + if (ppNode) + { + *ppNode = rExtent.pNode; + } + return pHint; + } + } + } + ++m_CurrentExtent; + if (m_CurrentExtent < m_pMerged->extents.size() && + rExtent.pNode != m_pMerged->extents[m_CurrentExtent].pNode) + { + m_CurrentHint = 0; // reset + } + } + return nullptr; + } + else + { + SwpHints const*const pHints(m_pNode->GetpSwpHints()); + if (pHints) + { + if (m_CurrentHint < pHints->Count()) + { + SwTextAttr const*const pHint(pHints->Get(m_CurrentHint)); + ++m_CurrentHint; + if (ppNode) + { + *ppNode = m_pNode; + } + return pHint; + } + } + return nullptr; + } + } + + MergedAttrIterByEnd::MergedAttrIterByEnd(SwTextFrame const& rFrame) + : m_pNode(rFrame.GetMergedPara() ? nullptr : rFrame.GetTextNodeFirst()) + , m_CurrentHint(0) + { + if (!m_pNode) + { + MergedAttrIterReverse iter(rFrame); + SwTextNode const* pNode(nullptr); + while (SwTextAttr const* pHint = iter.PrevAttr(&pNode)) + { + m_Hints.emplace_back(pNode, pHint); + } + } + } + + SwTextAttr const* MergedAttrIterByEnd::NextAttr(SwTextNode const*& rpNode) + { + if (m_pNode) + { + SwpHints const*const pHints(m_pNode->GetpSwpHints()); + if (pHints) + { + if (m_CurrentHint < pHints->Count()) + { + SwTextAttr const*const pHint( + pHints->GetSortedByEnd(m_CurrentHint)); + ++m_CurrentHint; + rpNode = m_pNode; + return pHint; + } + } + return nullptr; + } + else + { + if (m_CurrentHint < m_Hints.size()) + { + auto const ret = m_Hints[m_Hints.size() - m_CurrentHint - 1]; + ++m_CurrentHint; + rpNode = ret.first; + return ret.second; + } + return nullptr; + } + } + + void MergedAttrIterByEnd::PrevAttr() + { + assert(0 < m_CurrentHint); // should only rewind as far as 0 + --m_CurrentHint; + } + + MergedAttrIterReverse::MergedAttrIterReverse(SwTextFrame const& rFrame) + : MergedAttrIterBase(rFrame) + { + if (m_pMerged) + { + m_CurrentExtent = m_pMerged->extents.size(); + SwpHints const*const pHints(0 < m_CurrentExtent + ? m_pMerged->extents[m_CurrentExtent-1].pNode->GetpSwpHints() + : nullptr); + if (pHints) + { + pHints->SortIfNeedBe(); + m_CurrentHint = pHints->Count(); + } + } + else + { + if (SwpHints const*const pHints = m_pNode->GetpSwpHints()) + { + pHints->SortIfNeedBe(); + m_CurrentHint = pHints->Count(); + } + } + } + + SwTextAttr const* MergedAttrIterReverse::PrevAttr(SwTextNode const** ppNode) + { + if (m_pMerged) + { + while (0 < m_CurrentExtent) + { + sw::Extent const& rExtent(m_pMerged->extents[m_CurrentExtent-1]); + if (SwpHints const*const pHints = rExtent.pNode->GetpSwpHints()) + { + while (0 < m_CurrentHint) + { + SwTextAttr *const pHint( + pHints->GetSortedByEnd(m_CurrentHint - 1)); + if (pHint->GetAnyEnd() < rExtent.nStart + // <= if it has end and isn't empty + || (pHint->GetEnd() + && *pHint->GetEnd() != pHint->GetStart() + && *pHint->GetEnd() == rExtent.nStart)) + { + break; + } + --m_CurrentHint; + if (pHint->GetAnyEnd() <= rExtent.nEnd) + { + if (ppNode) + { + *ppNode = rExtent.pNode; + } + return pHint; + } + } + } + --m_CurrentExtent; + if (0 < m_CurrentExtent && + rExtent.pNode != m_pMerged->extents[m_CurrentExtent-1].pNode) + { + SwpHints const*const pHints( + m_pMerged->extents[m_CurrentExtent-1].pNode->GetpSwpHints()); + m_CurrentHint = pHints ? pHints->Count() : 0; // reset + if (pHints) + pHints->SortIfNeedBe(); + } + } + return nullptr; + } + else + { + SwpHints const*const pHints(m_pNode->GetpSwpHints()); + if (pHints && 0 < m_CurrentHint) + { + SwTextAttr const*const pHint(pHints->GetSortedByEnd(m_CurrentHint - 1)); + --m_CurrentHint; + if (ppNode) + { + *ppNode = m_pNode; + } + return pHint; + } + return nullptr; + } + } + + bool FrameContainsNode(SwContentFrame const& rFrame, SwNodeOffset const nNodeIndex) + { + if (rFrame.IsTextFrame()) + { + SwTextFrame const& rTextFrame(static_cast<SwTextFrame const&>(rFrame)); + if (sw::MergedPara const*const pMerged = rTextFrame.GetMergedPara()) + { + SwNodeOffset const nFirst(pMerged->pFirstNode->GetIndex()); + SwNodeOffset const nLast(pMerged->pLastNode->GetIndex()); + return (nFirst <= nNodeIndex && nNodeIndex <= nLast); + } + else + { + return rTextFrame.GetTextNodeFirst()->GetIndex() == nNodeIndex; + } + } + else + { + assert(rFrame.IsNoTextFrame()); + return static_cast<SwNoTextFrame const&>(rFrame).GetNode()->GetIndex() == nNodeIndex; + } + } + + bool IsParaPropsNode(SwRootFrame const& rLayout, SwTextNode const& rNode) + { + if (rLayout.HasMergedParas()) + { + if (SwTextFrame const*const pFrame = static_cast<SwTextFrame*>(rNode.getLayoutFrame(&rLayout))) + { + sw::MergedPara const*const pMerged(pFrame->GetMergedPara()); + if (pMerged && pMerged->pParaPropsNode != &rNode) + { + return false; + } + } + } + return true; + } + + SwTextNode * + GetParaPropsNode(SwRootFrame const& rLayout, SwNode const& rPos) + { + const SwTextNode *const pTextNode(rPos.GetTextNode()); + if (pTextNode && !sw::IsParaPropsNode(rLayout, *pTextNode)) + { + return static_cast<SwTextFrame*>(pTextNode->getLayoutFrame(&rLayout))->GetMergedPara()->pParaPropsNode; + } + else + { + return const_cast<SwTextNode*>(pTextNode); + } + } + + SwPosition + GetParaPropsPos(SwRootFrame const& rLayout, SwPosition const& rPos) + { + SwPosition pos(rPos); + SwTextNode const*const pNode(pos.GetNode().GetTextNode()); + if (pNode) + pos.Assign( *sw::GetParaPropsNode(rLayout, *pNode) ); + return pos; + } + + std::pair<SwTextNode *, SwTextNode *> + GetFirstAndLastNode(SwRootFrame const& rLayout, SwNode const& rPos) + { + SwTextNode *const pTextNode(const_cast<SwTextNode*>(rPos.GetTextNode())); + if (pTextNode && rLayout.HasMergedParas()) + { + if (SwTextFrame const*const pFrame = static_cast<SwTextFrame*>(pTextNode->getLayoutFrame(&rLayout))) + { + if (sw::MergedPara const*const pMerged = pFrame->GetMergedPara()) + { + return std::make_pair(pMerged->pFirstNode, const_cast<SwTextNode*>(pMerged->pLastNode)); + } + } + } + return std::make_pair(pTextNode, pTextNode); + } + + SwTextNode const& GetAttrMerged(SfxItemSet & rFormatSet, + SwTextNode const& rNode, SwRootFrame const*const pLayout) + { + rNode.SwContentNode::GetAttr(rFormatSet); + if (pLayout && pLayout->HasMergedParas()) + { + auto pFrame = static_cast<SwTextFrame*>(rNode.getLayoutFrame(pLayout)); + if (sw::MergedPara const*const pMerged = pFrame ? pFrame->GetMergedPara() : nullptr) + { + if (pMerged->pFirstNode != &rNode) + { + rFormatSet.ClearItem(RES_PAGEDESC); + rFormatSet.ClearItem(RES_BREAK); + static_assert(RES_PAGEDESC + 1 == sal_uInt16(RES_BREAK), + "first-node items must be adjacent"); + SfxItemSetFixed<RES_PAGEDESC, RES_BREAK> firstSet(*rFormatSet.GetPool()); + pMerged->pFirstNode->SwContentNode::GetAttr(firstSet); + rFormatSet.Put(firstSet); + + } + if (pMerged->pParaPropsNode != &rNode) + { + for (sal_uInt16 i = RES_PARATR_BEGIN; i != RES_FRMATR_END; ++i) + { + if (i != RES_PAGEDESC && i != RES_BREAK) + { + rFormatSet.ClearItem(i); + } + } + for (sal_uInt16 i = XATTR_FILL_FIRST; i <= XATTR_FILL_LAST; ++i) + { + rFormatSet.ClearItem(i); + } + SfxItemSetFixed<RES_PARATR_BEGIN, RES_PAGEDESC, + RES_BREAK+1, RES_FRMATR_END, + XATTR_FILL_FIRST, XATTR_FILL_LAST+1> + propsSet(*rFormatSet.GetPool()); + pMerged->pParaPropsNode->SwContentNode::GetAttr(propsSet); + rFormatSet.Put(propsSet); + return *pMerged->pParaPropsNode; + } + // keep all the CHRATR/UNKNOWNATR anyway... + } + } + return rNode; + } + +} // namespace sw + +/// Switches width and height of the text frame +void SwTextFrame::SwapWidthAndHeight() +{ + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + + if ( ! mbIsSwapped ) + { + const tools::Long nPrtOfstX = aPrt.Pos().X(); + aPrt.Pos().setX( aPrt.Pos().Y() ); + + if( IsVertLR() ) + { + aPrt.Pos().setY( nPrtOfstX ); + } + else + { + aPrt.Pos().setY( getFrameArea().Width() - ( nPrtOfstX + aPrt.Width() ) ); + } + } + else + { + const tools::Long nPrtOfstY = aPrt.Pos().Y(); + aPrt.Pos().setY( aPrt.Pos().X() ); + + if( IsVertLR() ) + { + aPrt.Pos().setX( nPrtOfstY ); + } + else + { + aPrt.Pos().setX( getFrameArea().Height() - ( nPrtOfstY + aPrt.Height() ) ); + } + } + + const tools::Long nPrtWidth = aPrt.Width(); + aPrt.Width( aPrt.Height() ); + aPrt.Height( nPrtWidth ); + } + + { + const tools::Long nFrameWidth = getFrameArea().Width(); + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aFrm.Width( aFrm.Height() ); + aFrm.Height( nFrameWidth ); + } + + mbIsSwapped = ! mbIsSwapped; +} + +/** + * Calculates the coordinates of a rectangle when switching from + * horizontal to vertical layout. + */ +void SwTextFrame::SwitchHorizontalToVertical( SwRect& rRect ) const +{ + // calc offset inside frame + tools::Long nOfstX, nOfstY; + if ( IsVertLR() ) + { + if (IsVertLRBT()) + { + // X and Y offsets here mean the position of the point that will be the top left corner + // after the switch. + nOfstX = rRect.Left() + rRect.Width() - getFrameArea().Left(); + nOfstY = rRect.Top() - getFrameArea().Top(); + } + else + { + nOfstX = rRect.Left() - getFrameArea().Left(); + nOfstY = rRect.Top() - getFrameArea().Top(); + } + } + else + { + nOfstX = rRect.Left() - getFrameArea().Left(); + nOfstY = rRect.Top() + rRect.Height() - getFrameArea().Top(); + } + + const tools::Long nWidth = rRect.Width(); + const tools::Long nHeight = rRect.Height(); + + if ( IsVertLR() ) + { + rRect.Left(getFrameArea().Left() + nOfstY); + } + else + { + if ( mbIsSwapped ) + rRect.Left( getFrameArea().Left() + getFrameArea().Height() - nOfstY ); + else + // frame is rotated + rRect.Left( getFrameArea().Left() + getFrameArea().Width() - nOfstY ); + } + + if (IsVertLRBT()) + { + if (mbIsSwapped) + rRect.Top(getFrameArea().Top() + getFrameArea().Width() - nOfstX); + else + rRect.Top(getFrameArea().Top() + getFrameArea().Height() - nOfstX); + } + else + rRect.Top(getFrameArea().Top() + nOfstX); + rRect.Width( nHeight ); + rRect.Height( nWidth ); +} + +/** + * Calculates the coordinates of a point when switching from + * horizontal to vertical layout. + */ +void SwTextFrame::SwitchHorizontalToVertical( Point& rPoint ) const +{ + if (IsVertLRBT()) + { + // The horizontal origo is the top left corner, the LRBT origo is the + // bottom left corner. Finally x and y has to be swapped. + SAL_WARN_IF(!mbIsSwapped, "sw.core", + "SwTextFrame::SwitchHorizontalToVertical, IsVertLRBT, not swapped"); + Point aPoint(rPoint); + rPoint.setX(getFrameArea().Left() + (aPoint.Y() - getFrameArea().Top())); + // This would be bottom - x delta, but bottom is top + height, finally + // width (and not height), as it's swapped. + rPoint.setY(getFrameArea().Top() + getFrameArea().Width() + - (aPoint.X() - getFrameArea().Left())); + return; + } + + // calc offset inside frame + const tools::Long nOfstX = rPoint.X() - getFrameArea().Left(); + const tools::Long nOfstY = rPoint.Y() - getFrameArea().Top(); + if ( IsVertLR() ) + rPoint.setX( getFrameArea().Left() + nOfstY ); + else + { + if ( mbIsSwapped ) + rPoint.setX( getFrameArea().Left() + getFrameArea().Height() - nOfstY ); + else + // calc rotated coords + rPoint.setX( getFrameArea().Left() + getFrameArea().Width() - nOfstY ); + } + + rPoint.setY( getFrameArea().Top() + nOfstX ); +} + +/** + * Calculates the a limit value when switching from + * horizontal to vertical layout. + */ +tools::Long SwTextFrame::SwitchHorizontalToVertical( tools::Long nLimit ) const +{ + Point aTmp( 0, nLimit ); + SwitchHorizontalToVertical( aTmp ); + return aTmp.X(); +} + +/** + * Calculates the coordinates of a rectangle when switching from + * vertical to horizontal layout. + */ +void SwTextFrame::SwitchVerticalToHorizontal( SwRect& rRect ) const +{ + tools::Long nOfstX; + + // calc offset inside frame + if ( IsVertLR() ) + nOfstX = rRect.Left() - getFrameArea().Left(); + else + { + if ( mbIsSwapped ) + nOfstX = getFrameArea().Left() + getFrameArea().Height() - ( rRect.Left() + rRect.Width() ); + else + nOfstX = getFrameArea().Left() + getFrameArea().Width() - ( rRect.Left() + rRect.Width() ); + } + + tools::Long nOfstY; + if (IsVertLRBT()) + { + // Note that mbIsSwapped only affects the frame area, not rRect, so rRect.Height() is used + // here unconditionally. + if (mbIsSwapped) + nOfstY = getFrameArea().Top() + getFrameArea().Width() - (rRect.Top() + rRect.Height()); + else + nOfstY = getFrameArea().Top() + getFrameArea().Height() - (rRect.Top() + rRect.Height()); + } + else + nOfstY = rRect.Top() - getFrameArea().Top(); + const tools::Long nWidth = rRect.Height(); + const tools::Long nHeight = rRect.Width(); + + // calc rotated coords + rRect.Left( getFrameArea().Left() + nOfstY ); + rRect.Top( getFrameArea().Top() + nOfstX ); + rRect.Width( nWidth ); + rRect.Height( nHeight ); +} + +/** + * Calculates the coordinates of a point when switching from + * vertical to horizontal layout. + */ +void SwTextFrame::SwitchVerticalToHorizontal( Point& rPoint ) const +{ + tools::Long nOfstX; + + // calc offset inside frame + if ( IsVertLR() ) + // X offset is Y - left. + nOfstX = rPoint.X() - getFrameArea().Left(); + else + { + // X offset is right - X. + if ( mbIsSwapped ) + nOfstX = getFrameArea().Left() + getFrameArea().Height() - rPoint.X(); + else + nOfstX = getFrameArea().Left() + getFrameArea().Width() - rPoint.X(); + } + + tools::Long nOfstY; + if (IsVertLRBT()) + { + // Y offset is bottom - Y. + if (mbIsSwapped) + nOfstY = getFrameArea().Top() + getFrameArea().Width() - rPoint.Y(); + else + nOfstY = getFrameArea().Top() + getFrameArea().Height() - rPoint.Y(); + } + else + // Y offset is Y - top. + nOfstY = rPoint.Y() - getFrameArea().Top(); + + // calc rotated coords + rPoint.setX( getFrameArea().Left() + nOfstY ); + rPoint.setY( getFrameArea().Top() + nOfstX ); +} + +/** + * Calculates the a limit value when switching from + * vertical to horizontal layout. + */ +tools::Long SwTextFrame::SwitchVerticalToHorizontal( tools::Long nLimit ) const +{ + Point aTmp( nLimit, 0 ); + SwitchVerticalToHorizontal( aTmp ); + return aTmp.Y(); +} + +SwFrameSwapper::SwFrameSwapper( const SwTextFrame* pTextFrame, bool bSwapIfNotSwapped ) + : pFrame( pTextFrame ), bUndo( false ) +{ + if (pFrame->IsVertical() && bSwapIfNotSwapped != pFrame->IsSwapped()) + { + bUndo = true; + const_cast<SwTextFrame*>(pFrame)->SwapWidthAndHeight(); + } +} + +SwFrameSwapper::~SwFrameSwapper() +{ + if ( bUndo ) + const_cast<SwTextFrame*>(pFrame)->SwapWidthAndHeight(); +} + +void SwTextFrame::SwitchLTRtoRTL( SwRect& rRect ) const +{ + SwSwapIfNotSwapped swap(const_cast<SwTextFrame *>(this)); + + tools::Long nWidth = rRect.Width(); + rRect.Left( 2 * ( getFrameArea().Left() + getFramePrintArea().Left() ) + + getFramePrintArea().Width() - rRect.Right() - 1 ); + + rRect.Width( nWidth ); +} + +void SwTextFrame::SwitchLTRtoRTL( Point& rPoint ) const +{ + SwSwapIfNotSwapped swap(const_cast<SwTextFrame *>(this)); + + rPoint.setX( 2 * ( getFrameArea().Left() + getFramePrintArea().Left() ) + getFramePrintArea().Width() - rPoint.X() - 1 ); +} + +SwLayoutModeModifier::SwLayoutModeModifier( const OutputDevice& rOutp ) : + m_rOut( rOutp ), m_nOldLayoutMode( rOutp.GetLayoutMode() ) +{ +} + +SwLayoutModeModifier::~SwLayoutModeModifier() +{ + const_cast<OutputDevice&>(m_rOut).SetLayoutMode( m_nOldLayoutMode ); +} + +void SwLayoutModeModifier::Modify( bool bChgToRTL ) +{ + const_cast<OutputDevice&>(m_rOut).SetLayoutMode( bChgToRTL ? + vcl::text::ComplexTextLayoutFlags::BiDiStrong | vcl::text::ComplexTextLayoutFlags::BiDiRtl : + vcl::text::ComplexTextLayoutFlags::BiDiStrong ); +} + +void SwLayoutModeModifier::SetAuto() +{ + const vcl::text::ComplexTextLayoutFlags nNewLayoutMode = m_nOldLayoutMode & ~vcl::text::ComplexTextLayoutFlags::BiDiStrong; + const_cast<OutputDevice&>(m_rOut).SetLayoutMode( nNewLayoutMode ); +} + +SwDigitModeModifier::SwDigitModeModifier( const OutputDevice& rOutp, LanguageType eCurLang ) : + rOut( rOutp ), nOldLanguageType( rOutp.GetDigitLanguage() ) +{ + LanguageType eLang = eCurLang; + if (utl::ConfigManager::IsFuzzing()) + eLang = LANGUAGE_ENGLISH_US; + else + { + const SvtCTLOptions::TextNumerals nTextNumerals = SvtCTLOptions::GetCTLTextNumerals(); + + if ( SvtCTLOptions::NUMERALS_HINDI == nTextNumerals ) + eLang = LANGUAGE_ARABIC_SAUDI_ARABIA; + else if ( SvtCTLOptions::NUMERALS_ARABIC == nTextNumerals ) + eLang = LANGUAGE_ENGLISH; + else if ( SvtCTLOptions::NUMERALS_SYSTEM == nTextNumerals ) + eLang = ::GetAppLanguage(); + } + + const_cast<OutputDevice&>(rOut).SetDigitLanguage( eLang ); +} + +SwDigitModeModifier::~SwDigitModeModifier() +{ + const_cast<OutputDevice&>(rOut).SetDigitLanguage( nOldLanguageType ); +} + +void SwTextFrame::Init() +{ + OSL_ENSURE( !IsLocked(), "+SwTextFrame::Init: this is locked." ); + if( !IsLocked() ) + { + ClearPara(); + SetHasRotatedPortions(false); + // set flags directly to save a ResetPreps call, + // and thereby an unnecessary GetPara call + // don't set bOrphan, bLocked or bWait to false! + // bOrphan = bFlag7 = bFlag8 = false; + } +} + +SwTextFrame::SwTextFrame(SwTextNode * const pNode, SwFrame* pSib, + sw::FrameMode const eMode) + : SwContentFrame( pNode, pSib ) + , mnAllLines( 0 ) + , mnThisLines( 0 ) + , mnFlyAnchorOfst( 0 ) + , mnFlyAnchorOfstNoWrap( 0 ) + , mnFlyAnchorVertOfstNoWrap( 0 ) + , mnFootnoteLine( 0 ) + , mnHeightOfLastLine( 0 ) + , mnAdditionalFirstLineOffset( 0 ) + , mnOffset( 0 ) + , mnCacheIndex( USHRT_MAX ) + , mbLocked( false ) + , mbWidow( false ) + , mbJustWidow( false ) + , mbEmpty( false ) + , mbInFootnoteConnect( false ) + , mbFootnote( false ) + , mbRepaint( false ) + , mbHasRotatedPortions( false ) + , mbFieldFollow( false ) + , mbHasAnimation( false ) + , mbIsSwapped( false ) + , mbFollowFormatAllowed( true ) +{ + mnFrameType = SwFrameType::Txt; + // note: this may call SwClientNotify if it's in a list so do it last + // note: this may change this->pRegisteredIn to m_pMergedPara->listeners + m_pMergedPara = CheckParaRedlineMerge(*this, *pNode, eMode); +} + +void SwTextFrame::dumpAsXmlAttributes(xmlTextWriterPtr writer) const +{ + SwContentFrame::dumpAsXmlAttributes(writer); + + const SwTextNode *pTextNode = GetTextNodeFirst(); + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "txtNodeIndex" ), "%" SAL_PRIdINT32, sal_Int32(pTextNode->GetIndex()) ); + + OString aMode = "Horizontal"_ostr; + if (IsVertLRBT()) + { + aMode = "VertBTLR"_ostr; + } + else if (IsVertLR()) + { + aMode = "VertLR"_ostr; + } + else if (IsVertical()) + { + aMode = "Vertical"_ostr; + } + (void)xmlTextWriterWriteAttribute(writer, BAD_CAST("WritingMode"), BAD_CAST(aMode.getStr())); +} + +void SwTextFrame::dumpAsXml(xmlTextWriterPtr writer) const +{ + (void)xmlTextWriterStartElement(writer, reinterpret_cast<const xmlChar*>("txt")); + dumpAsXmlAttributes( writer ); + if ( HasFollow() ) + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "follow" ), "%" SAL_PRIuUINT32, GetFollow()->GetFrameId() ); + + if (m_pPrecede != nullptr) + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "precede" ), "%" SAL_PRIuUINT32, static_cast<SwTextFrame*>(m_pPrecede)->GetFrameId() ); + + (void)xmlTextWriterWriteAttribute(writer, BAD_CAST("offset"), BAD_CAST(OString::number(static_cast<sal_Int32>(mnOffset)).getStr())); + + sw::MergedPara const*const pMerged(GetMergedPara()); + if (pMerged) + { + (void)xmlTextWriterStartElement( writer, BAD_CAST( "merged" ) ); + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "paraPropsNodeIndex" ), "%" SAL_PRIdINT32, sal_Int32(pMerged->pParaPropsNode->GetIndex()) ); + for (auto const& e : pMerged->extents) + { + (void)xmlTextWriterStartElement( writer, BAD_CAST( "extent" ) ); + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "txtNodeIndex" ), "%" SAL_PRIdINT32, sal_Int32(e.pNode->GetIndex()) ); + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "start" ), "%" SAL_PRIdINT32, e.nStart ); + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "end" ), "%" SAL_PRIdINT32, e.nEnd ); + (void)xmlTextWriterEndElement( writer ); + } + (void)xmlTextWriterEndElement( writer ); + } + + (void)xmlTextWriterStartElement(writer, BAD_CAST("infos")); + dumpInfosAsXml(writer); + (void)xmlTextWriterEndElement(writer); + + // Dump Anchored objects if any + const SwSortedObjs* pAnchored = GetDrawObjs(); + if ( pAnchored && pAnchored->size() > 0 ) + { + (void)xmlTextWriterStartElement( writer, BAD_CAST( "anchored" ) ); + + for (SwAnchoredObject* pObject : *pAnchored) + { + pObject->dumpAsXml( writer ); + } + + (void)xmlTextWriterEndElement( writer ); + } + + // Dump the children + OUString aText = GetText( ); + for ( int i = 0; i < 32; i++ ) + { + aText = aText.replace( i, '*' ); + } + auto nTextOffset = static_cast<sal_Int32>(GetOffset()); + sal_Int32 nTextLength = aText.getLength() - nTextOffset; + if (const SwTextFrame* pTextFrameFollow = GetFollow()) + { + nTextLength = static_cast<sal_Int32>(pTextFrameFollow->GetOffset() - GetOffset()); + } + if (nTextLength > 0) + { + OString aText8 + = OUStringToOString(aText.subView(nTextOffset, nTextLength), RTL_TEXTENCODING_UTF8); + (void)xmlTextWriterWriteString( writer, + reinterpret_cast<const xmlChar *>(aText8.getStr( )) ); + } + if (const SwParaPortion* pPara = GetPara()) + { + (void)xmlTextWriterStartElement(writer, BAD_CAST("SwParaPortion")); + TextFrameIndex nOffset(0); + const OUString& rText = GetText(); + (void)xmlTextWriterWriteFormatAttribute(writer, BAD_CAST("ptr"), "%p", pPara); + const SwLineLayout* pLine = pPara; + if (IsFollow()) + { + nOffset += GetOffset(); + } + while (pLine) + { + (void)xmlTextWriterStartElement(writer, BAD_CAST("SwLineLayout")); + pLine->dumpAsXmlAttributes(writer, rText, nOffset); + const SwLinePortion* pPor = pLine->GetFirstPortion(); + while (pPor) + { + pPor->dumpAsXml(writer, rText, nOffset); + pPor = pPor->GetNextPortion(); + } + (void)xmlTextWriterEndElement(writer); + pLine = pLine->GetNext(); + } + (void)xmlTextWriterEndElement(writer); + } + + (void)xmlTextWriterEndElement(writer); +} + +namespace sw { + +SwTextFrame * MakeTextFrame(SwTextNode & rNode, SwFrame *const pSibling, + sw::FrameMode const eMode) +{ + return new SwTextFrame(&rNode, pSibling, eMode); +} + +void RemoveFootnotesForNode( + SwRootFrame const& rLayout, SwTextNode const& rTextNode, + std::vector<std::pair<sal_Int32, sal_Int32>> const*const pExtents) +{ + if (pExtents && pExtents->empty()) + { + return; // nothing to do + } + const SwFootnoteIdxs &rFootnoteIdxs = rTextNode.GetDoc().GetFootnoteIdxs(); + size_t nPos = 0; + SwNodeOffset const nIndex = rTextNode.GetIndex(); + rFootnoteIdxs.SeekEntry( rTextNode, &nPos ); + if (nPos < rFootnoteIdxs.size()) + { + while (nPos && rTextNode == (rFootnoteIdxs[ nPos ]->GetTextNode())) + --nPos; + if (nPos || rTextNode != (rFootnoteIdxs[ nPos ]->GetTextNode())) + ++nPos; + } + size_t iter(0); + for ( ; nPos < rFootnoteIdxs.size(); ++nPos) + { + SwTextFootnote* pTextFootnote = rFootnoteIdxs[ nPos ]; + if (pTextFootnote->GetTextNode().GetIndex() > nIndex) + break; + if (pExtents) + { + while ((*pExtents)[iter].second <= pTextFootnote->GetStart()) + { + ++iter; + if (iter == pExtents->size()) + { + return; + } + } + if (pTextFootnote->GetStart() < (*pExtents)[iter].first) + { + continue; + } + } + pTextFootnote->DelFrames(&rLayout); + } +} + +} // namespace sw + +void SwTextFrame::DestroyImpl() +{ + // Remove associated SwParaPortion from s_pTextCache + ClearPara(); + + assert(!GetDoc().IsInDtor()); // this shouldn't be happening with ViewShell owning layout + if (!GetDoc().IsInDtor() && HasFootnote()) + { + if (m_pMergedPara) + { + SwTextNode const* pNode(nullptr); + for (auto const& e : m_pMergedPara->extents) + { + if (e.pNode != pNode) + { + pNode = e.pNode; + // sw_redlinehide: not sure if it's necessary to check + // if the nodes are still alive here, which would require + // accessing WriterMultiListener::m_vDepends + sw::RemoveFootnotesForNode(*getRootFrame(), *pNode, nullptr); + } + } + } + else + { + SwTextNode *const pNode(static_cast<SwTextNode*>(GetDep())); + if (pNode) + { + sw::RemoveFootnotesForNode(*getRootFrame(), *pNode, nullptr); + } + } + } + + if (!GetDoc().IsInDtor()) + { + if (SwView* pView = GetActiveView()) + pView->GetEditWin().GetFrameControlsManager().RemoveControls(this); + } + + SwContentFrame::DestroyImpl(); +} + +SwTextFrame::~SwTextFrame() +{ + RemoveFromCache(); +} + +namespace sw { + +// 1. if real insert => correct nStart/nEnd for full nLen +// 2. if rl un-delete => do not correct nStart/nEnd but just include un-deleted +static TextFrameIndex UpdateMergedParaForInsert(MergedPara & rMerged, + bool const isRealInsert, + SwTextNode const& rNode, sal_Int32 const nIndex, sal_Int32 const nLen) +{ + assert(!isRealInsert || nLen); // can 0 happen? yes, for redline in empty node + assert(nIndex <= rNode.Len()); + assert(nIndex + nLen <= rNode.Len()); + assert(rMerged.pFirstNode->GetIndex() <= rNode.GetIndex() && rNode.GetIndex() <= rMerged.pLastNode->GetIndex()); + if (!nLen) + { + return TextFrameIndex(0); + } + OUStringBuffer text(rMerged.mergedText); + sal_Int32 nTFIndex(0); // index used for insertion at the end + sal_Int32 nInserted(0); + bool bInserted(false); + bool bFoundNode(false); + auto itInsert(rMerged.extents.end()); + for (auto it = rMerged.extents.begin(); it != rMerged.extents.end(); ++it) + { + if (it->pNode == &rNode) + { + if (isRealInsert) + { + bFoundNode = true; + if (it->nStart <= nIndex && nIndex <= it->nEnd) + { // note: this can happen only once + text.insert(nTFIndex + (nIndex - it->nStart), + rNode.GetText().subView(nIndex, nLen)); + it->nEnd += nLen; + nInserted = nLen; + assert(!bInserted); + bInserted = true; + } + else if (nIndex < it->nStart) + { + if (itInsert == rMerged.extents.end()) + { + itInsert = it; + } + it->nStart += nLen; + it->nEnd += nLen; + } + } + else + { + assert(it == rMerged.extents.begin() || (it-1)->pNode != &rNode || (it-1)->nEnd < nIndex); + if (nIndex + nLen < it->nStart) + { + itInsert = it; + break; + } + if (nIndex < it->nStart) + { + text.insert(nTFIndex, + rNode.GetText().subView(nIndex, it->nStart - nIndex)); + nInserted += it->nStart - nIndex; + it->nStart = nIndex; + bInserted = true; + } + assert(it->nStart <= nIndex); + if (nIndex <= it->nEnd) + { + nTFIndex += it->nEnd - it->nStart; + while (it->nEnd < nIndex + nLen) + { + auto *const pNext( + (it+1) != rMerged.extents.end() && (it+1)->pNode == it->pNode + ? &*(it+1) + : nullptr); + if (pNext && pNext->nStart <= nIndex + nLen) + { + text.insert(nTFIndex, + rNode.GetText().subView(it->nEnd, pNext->nStart - it->nEnd)); + nTFIndex += pNext->nStart - it->nEnd; + nInserted += pNext->nStart - it->nEnd; + pNext->nStart = it->nStart; + it = rMerged.extents.erase(it); + } + else + { + text.insert(nTFIndex, + rNode.GetText().subView(it->nEnd, nIndex + nLen - it->nEnd)); + nTFIndex += nIndex + nLen - it->nEnd; + nInserted += nIndex + nLen - it->nEnd; + it->nEnd = nIndex + nLen; + } + } + bInserted = true; + break; + } + } + } + else if (rNode.GetIndex() < it->pNode->GetIndex() || bFoundNode) + { + if (itInsert == rMerged.extents.end()) + { + itInsert = it; + } + break; + } + if (itInsert == rMerged.extents.end()) + { + nTFIndex += it->nEnd - it->nStart; + } + } +// assert((bFoundNode || rMerged.extents.empty()) && "text node not found - why is it sending hints to us"); + if (!bInserted) + { // must be in a gap + rMerged.extents.emplace(itInsert, const_cast<SwTextNode*>(&rNode), nIndex, nIndex + nLen); + text.insert(nTFIndex, rNode.GetText().subView(nIndex, nLen)); + nInserted = nLen; + if (rMerged.extents.size() == 1 // also if it was empty! + || rMerged.pParaPropsNode->GetIndex() < rNode.GetIndex()) + { // text inserted after current para-props node + rMerged.pParaPropsNode->RemoveFromListRLHidden(); + rMerged.pParaPropsNode = &const_cast<SwTextNode&>(rNode); + rMerged.pParaPropsNode->AddToListRLHidden(); + } + // called from SwRangeRedline::InvalidateRange() + if (rNode.GetRedlineMergeFlag() == SwNode::Merge::Hidden) + { + const_cast<SwTextNode&>(rNode).SetRedlineMergeFlag(SwNode::Merge::NonFirst); + } + } + rMerged.mergedText = text.makeStringAndClear(); + return TextFrameIndex(nInserted); +} + +// 1. if real delete => correct nStart/nEnd for full nLen +// 2. if rl delete => do not correct nStart/nEnd but just exclude deleted +TextFrameIndex UpdateMergedParaForDelete(MergedPara & rMerged, + bool const isRealDelete, + SwTextNode const& rNode, sal_Int32 nIndex, sal_Int32 const nLen) +{ + assert(nIndex <= rNode.Len()); + assert(rMerged.pFirstNode->GetIndex() <= rNode.GetIndex() && rNode.GetIndex() <= rMerged.pLastNode->GetIndex()); + OUStringBuffer text(rMerged.mergedText); + sal_Int32 nTFIndex(0); + sal_Int32 nToDelete(nLen); + sal_Int32 nDeleted(0); + size_t nFoundNode(0); + size_t nErased(0); + auto it = rMerged.extents.begin(); + for (; it != rMerged.extents.end(); ) + { + bool bErase(false); + if (it->pNode == &rNode) + { + ++nFoundNode; + if (nIndex + nToDelete < it->nStart) + { + nToDelete = 0; + if (!isRealDelete) + { + break; + } + it->nStart -= nLen; + it->nEnd -= nLen; + } + else + { + if (nIndex < it->nStart) + { + // do not adjust nIndex into the text frame index space! + nToDelete -= it->nStart - nIndex; + nIndex = it->nStart; + // note: continue with the if check below, no else! + } + if (it->nStart <= nIndex && nIndex < it->nEnd) + { + sal_Int32 const nDeleteHere(nIndex + nToDelete <= it->nEnd + ? nToDelete + : it->nEnd - nIndex); + text.remove(nTFIndex + (nIndex - it->nStart), nDeleteHere); + bErase = nDeleteHere == it->nEnd - it->nStart; + if (bErase) + { + ++nErased; + assert(it->nStart == nIndex); + it = rMerged.extents.erase(it); + } + else if (isRealDelete) + { // adjust for deleted text + it->nStart -= (nLen - nToDelete); + it->nEnd -= (nLen - nToDelete + nDeleteHere); + if (it != rMerged.extents.begin() + && (it-1)->pNode == &rNode + && (it-1)->nEnd == it->nStart) + { // merge adjacent extents + nTFIndex += it->nEnd - it->nStart; + (it-1)->nEnd = it->nEnd; + it = rMerged.extents.erase(it); + bErase = true; // skip increment + } + } + else + { // exclude text marked as deleted + if (nIndex + nDeleteHere == it->nEnd) + { + it->nEnd -= nDeleteHere; + } + else + { + if (nIndex == it->nStart) + { + it->nStart += nDeleteHere; + } + else + { + sal_Int32 const nOldEnd(it->nEnd); + it->nEnd = nIndex; + it = rMerged.extents.emplace(it+1, + it->pNode, nIndex + nDeleteHere, nOldEnd); + } + assert(nDeleteHere == nToDelete); + } + } + nDeleted += nDeleteHere; + nToDelete -= nDeleteHere; + nIndex += nDeleteHere; + if (!isRealDelete && nToDelete == 0) + { + break; + } + } + } + } + else if (nFoundNode != 0) + { + break; + } + if (!bErase) + { + nTFIndex += it->nEnd - it->nStart; + ++it; + } + } +// assert(nFoundNode != 0 && "text node not found - why is it sending hints to us"); + assert(nIndex <= rNode.Len() + nLen); + // if there's a remaining deletion, it must be in gap at the end of the node +// can't do: might be last one in node was erased assert(nLen == 0 || rMerged.empty() || (it-1)->nEnd <= nIndex); + // note: if first node gets deleted then that must call DelFrames as + // pFirstNode is never updated + if (nErased && nErased == nFoundNode) + { // all visible text from node was erased +#if 1 + if (rMerged.pParaPropsNode == &rNode) + { + rMerged.pParaPropsNode->RemoveFromListRLHidden(); + rMerged.pParaPropsNode = rMerged.extents.empty() + ? const_cast<SwTextNode*>(rMerged.pLastNode) + : rMerged.extents.front().pNode; + rMerged.pParaPropsNode->AddToListRLHidden(); + } +#endif +// NOPE must listen on all non-hidden nodes; particularly on pLastNode rMerged.listener.EndListening(&const_cast<SwTextNode&>(rNode)); + } + rMerged.mergedText = text.makeStringAndClear(); + return TextFrameIndex(nDeleted); +} + +std::pair<SwTextNode*, sal_Int32> +MapViewToModel(MergedPara const& rMerged, TextFrameIndex const i_nIndex) +{ + sal_Int32 nIndex(i_nIndex); + sw::Extent const* pExtent(nullptr); + for (const auto& rExt : rMerged.extents) + { + pExtent = &rExt; + if (nIndex < (pExtent->nEnd - pExtent->nStart)) + { + return std::make_pair(pExtent->pNode, pExtent->nStart + nIndex); + } + nIndex = nIndex - (pExtent->nEnd - pExtent->nStart); + } + assert(nIndex == 0 && "view index out of bounds"); + return pExtent + ? std::make_pair(pExtent->pNode, pExtent->nEnd) //1-past-the-end index + : std::make_pair(const_cast<SwTextNode*>(rMerged.pLastNode), rMerged.pLastNode->Len()); +} + +TextFrameIndex MapModelToView(MergedPara const& rMerged, SwTextNode const*const pNode, sal_Int32 const nIndex) +{ + assert(rMerged.pFirstNode->GetIndex() <= pNode->GetIndex() + && pNode->GetIndex() <= rMerged.pLastNode->GetIndex()); + sal_Int32 nRet(0); + bool bFoundNode(false); + for (auto const& e : rMerged.extents) + { + if (pNode->GetIndex() < e.pNode->GetIndex()) + { + return TextFrameIndex(nRet); + } + if (e.pNode == pNode) + { + if (e.nStart <= nIndex && nIndex <= e.nEnd) + { + return TextFrameIndex(nRet + (nIndex - e.nStart)); + } + else if (nIndex < e.nStart) + { + // in gap before this extent => map to 0 here TODO??? + return TextFrameIndex(nRet); + } + bFoundNode = true; + } + else if (bFoundNode) + { + break; + } + nRet += e.nEnd - e.nStart; + } + if (bFoundNode) + { + // must be in a gap at the end of the node + assert(nIndex <= pNode->Len()); + return TextFrameIndex(nRet); + } + else if (rMerged.extents.empty()) + { + assert(nIndex <= pNode->Len()); + return TextFrameIndex(0); + } + return TextFrameIndex(rMerged.mergedText.getLength()); +} + +} // namespace sw + +std::pair<SwTextNode*, sal_Int32> +SwTextFrame::MapViewToModel(TextFrameIndex const nIndex) const +{ +//nope assert(GetPara()); + sw::MergedPara const*const pMerged(GetMergedPara()); + if (pMerged) + { + return sw::MapViewToModel(*pMerged, nIndex); + } + else + { + return std::make_pair(static_cast<SwTextNode*>(const_cast<sw::BroadcastingModify*>( + SwFrame::GetDep())), sal_Int32(nIndex)); + } +} + +SwPosition SwTextFrame::MapViewToModelPos(TextFrameIndex const nIndex) const +{ + std::pair<SwTextNode*, sal_Int32> const ret(MapViewToModel(nIndex)); + return SwPosition(*ret.first, ret.second); +} + +TextFrameIndex SwTextFrame::MapModelToView(SwTextNode const*const pNode, sal_Int32 const nIndex) const +{ +//nope assert(GetPara()); + sw::MergedPara const*const pMerged(GetMergedPara()); + if (pMerged) + { + return sw::MapModelToView(*pMerged, pNode, nIndex); + } + else + { + assert(static_cast<const SwTextNode*>(SwFrame::GetDep()) == pNode); + return TextFrameIndex(nIndex); + } +} + +TextFrameIndex SwTextFrame::MapModelToViewPos(SwPosition const& rPos) const +{ + SwTextNode const*const pNode(rPos.GetNode().GetTextNode()); + sal_Int32 const nIndex(rPos.GetContentIndex()); + return MapModelToView(pNode, nIndex); +} + +void SwTextFrame::SetMergedPara(std::unique_ptr<sw::MergedPara> p) +{ + SwTextNode *const pFirst(m_pMergedPara ? m_pMergedPara->pFirstNode : nullptr); + m_pMergedPara = std::move(p); + if (pFirst) + { + if (m_pMergedPara) + { + assert(pFirst == m_pMergedPara->pFirstNode); + } + else + { + pFirst->Add(this); // must register at node again + } + } + // postcondition: frame must be listening somewhere + assert(m_pMergedPara || GetDep()); +} + +const OUString& SwTextFrame::GetText() const +{ +//nope assert(GetPara()); + sw::MergedPara const*const pMerged(GetMergedPara()); + if (pMerged) + return pMerged->mergedText; + else + return static_cast<SwTextNode const*>(SwFrame::GetDep())->GetText(); +} + +SwTextNode const* SwTextFrame::GetTextNodeForParaProps() const +{ + // FIXME can GetPara be 0 ? yes... this is needed in SwContentNotify::SwContentNotify() which is called before any formatting is started +//nope assert(GetPara()); + sw::MergedPara const*const pMerged(GetMergedPara()); + if (pMerged) + { +// assert(pMerged->pFirstNode == pMerged->pParaPropsNode); // surprising news! + return pMerged->pParaPropsNode; + } + else + return static_cast<SwTextNode const*>(SwFrame::GetDep()); +} + +SwTextNode const* SwTextFrame::GetTextNodeForFirstText() const +{ + sw::MergedPara const*const pMerged(GetMergedPara()); + if (pMerged) + return pMerged->extents.empty() + ? pMerged->pFirstNode + : pMerged->extents.front().pNode; + else + return static_cast<SwTextNode const*>(SwFrame::GetDep()); +} + +SwTextNode const* SwTextFrame::GetTextNodeFirst() const +{ +//nope assert(GetPara()); + sw::MergedPara const*const pMerged(GetMergedPara()); + if (pMerged) + return pMerged->pFirstNode; + else + return static_cast<SwTextNode const*>(SwFrame::GetDep()); +} + +SwDoc const& SwTextFrame::GetDoc() const +{ + return GetTextNodeFirst()->GetDoc(); +} + +LanguageType SwTextFrame::GetLangOfChar(TextFrameIndex const nIndex, + sal_uInt16 const nScript, bool const bNoChar) const +{ + // a single character can be mapped uniquely! + std::pair<SwTextNode const*, sal_Int32> const pos(MapViewToModel(nIndex)); + return pos.first->GetLang(pos.second, bNoChar ? 0 : 1, nScript); +} + +void SwTextFrame::ResetPreps() +{ + if ( GetCacheIdx() != USHRT_MAX ) + { + if (SwParaPortion *pPara = GetPara()) + pPara->ResetPreps(); + } +} + +bool SwTextFrame::IsHiddenNow() const +{ + SwFrameSwapper aSwapper( this, true ); + + if( !getFrameArea().Width() && isFrameAreaDefinitionValid() && GetUpper()->isFrameAreaDefinitionValid() ) // invalid when stack overflows (StackHack)! + { +// OSL_FAIL( "SwTextFrame::IsHiddenNow: thin frame" ); + return true; + } + + bool bHiddenCharsHidePara(false); + bool bHiddenParaField(false); + if (m_pMergedPara) + { + TextFrameIndex nHiddenStart(COMPLETE_STRING); + TextFrameIndex nHiddenEnd(0); + if (auto const pScriptInfo = GetScriptInfo()) + { + pScriptInfo->GetBoundsOfHiddenRange(TextFrameIndex(0), + nHiddenStart, nHiddenEnd); + } + else // ParaPortion is created in Format, but this is called earlier + { + SwScriptInfo aInfo; + aInfo.InitScriptInfo(*m_pMergedPara->pFirstNode, m_pMergedPara.get(), IsRightToLeft()); + aInfo.GetBoundsOfHiddenRange(TextFrameIndex(0), + nHiddenStart, nHiddenEnd); + } + if (TextFrameIndex(0) == nHiddenStart && + TextFrameIndex(GetText().getLength()) <= nHiddenEnd) + { + bHiddenCharsHidePara = true; + } + sw::MergedAttrIter iter(*this); + SwTextNode const* pNode(nullptr); + int nNewResultWeight = 0; + for (SwTextAttr const* pHint = iter.NextAttr(&pNode); pHint; pHint = iter.NextAttr(&pNode)) + { + if (pHint->Which() == RES_TXTATR_FIELD) + { + // see also SwpHints::CalcHiddenParaField() + const SwFormatField& rField = pHint->GetFormatField(); + int nCurWeight = pNode->GetDoc().FieldCanHideParaWeight(rField.GetField()->GetTyp()->Which()); + if (nCurWeight > nNewResultWeight) + { + nNewResultWeight = nCurWeight; + bHiddenParaField = pNode->GetDoc().FieldHidesPara(*rField.GetField()); + } + else if (nCurWeight == nNewResultWeight && bHiddenParaField) + { + // Currently, for both supported hiding types (HiddenPara, Database), "Don't hide" + // takes precedence - i.e., if there's a "Don't hide" field of that weight, we only + // care about fields of higher weight. + bHiddenParaField = pNode->GetDoc().FieldHidesPara(*rField.GetField()); + } + } + } + } + else + { + bHiddenCharsHidePara = static_cast<SwTextNode const*>(SwFrame::GetDep())->HasHiddenCharAttribute( true ); + bHiddenParaField = static_cast<SwTextNode const*>(SwFrame::GetDep())->IsHiddenByParaField(); + } + const SwViewShell* pVsh = getRootFrame()->GetCurrShell(); + + if ( pVsh && ( bHiddenCharsHidePara || bHiddenParaField ) ) + { + + if ( + ( bHiddenParaField && + ( !pVsh->GetViewOptions()->IsShowHiddenPara() && + !pVsh->GetViewOptions()->IsFieldName() ) ) || + ( bHiddenCharsHidePara && + !pVsh->GetViewOptions()->IsShowHiddenChar() ) ) + { + // in order to put the cursor in the body text, one paragraph must + // be visible - check this for the 1st body paragraph + if (IsInDocBody() && FindPrevCnt() == nullptr) + { + bool isAllHidden(true); + for (SwContentFrame const* pNext = FindNextCnt(true); + pNext != nullptr; pNext = pNext->FindNextCnt(true)) + { + if (!pNext->IsTextFrame() + || !static_cast<SwTextFrame const*>(pNext)->IsHiddenNow()) + { + isAllHidden = false; + break; + } + } + if (isAllHidden) + { + SAL_INFO("sw.core", "unhiding one body paragraph"); + return false; + } + } + return true; + } + } + + return false; +} + +/// Removes Textfrm's attachments, when it's hidden +void SwTextFrame::HideHidden() +{ + OSL_ENSURE( !GetFollow() && IsHiddenNow(), + "HideHidden on visible frame of hidden frame has follow" ); + + HideFootnotes(GetOffset(), TextFrameIndex(COMPLETE_STRING)); + HideAndShowObjects(); + + // format information is obsolete + ClearPara(); +} + +void SwTextFrame::HideFootnotes(TextFrameIndex const nStart, TextFrameIndex const nEnd) +{ + SwPageFrame *pPage = nullptr; + sw::MergedAttrIter iter(*this); + SwTextNode const* pNode(nullptr); + for (SwTextAttr const* pHt = iter.NextAttr(&pNode); pHt; pHt = iter.NextAttr(&pNode)) + { + if (pHt->Which() == RES_TXTATR_FTN) + { + TextFrameIndex const nIdx(MapModelToView(pNode, pHt->GetStart())); + if (nEnd < nIdx) + break; + if (nStart <= nIdx) + { + if (!pPage) + pPage = FindPageFrame(); + pPage->RemoveFootnote( this, static_cast<const SwTextFootnote*>(pHt) ); + } + } + } +} + +/** + * as-character anchored graphics, which are used for a graphic bullet list. + * As long as these graphic bullet list aren't imported, do not hide a + * at-character anchored object, if + * (a) the document is an imported WW8 document - + * checked by checking certain compatibility options - + * (b) the paragraph is the last content in the document and + * (c) the anchor character is an as-character anchored graphic. + */ +bool sw_HideObj( const SwTextFrame& _rFrame, + const RndStdIds _eAnchorType, + SwFormatAnchor const& rFormatAnchor, + SwAnchoredObject* _pAnchoredObj ) +{ + bool bRet( true ); + + if (_eAnchorType == RndStdIds::FLY_AT_CHAR) + { + const IDocumentSettingAccess *const pIDSA = &_rFrame.GetDoc().getIDocumentSettingAccess(); + if ( !pIDSA->get(DocumentSettingId::USE_FORMER_TEXT_WRAPPING) && + !pIDSA->get(DocumentSettingId::OLD_LINE_SPACING) && + !pIDSA->get(DocumentSettingId::USE_FORMER_OBJECT_POS) && + pIDSA->get(DocumentSettingId::CONSIDER_WRAP_ON_OBJECT_POSITION) && + _rFrame.IsInDocBody() && !_rFrame.FindNextCnt() ) + { + SwTextNode const& rNode(*rFormatAnchor.GetAnchorNode()->GetTextNode()); + assert(FrameContainsNode(_rFrame, rNode.GetIndex())); + sal_Int32 const nObjAnchorPos(rFormatAnchor.GetAnchorContentOffset()); + const sal_Unicode cAnchorChar = nObjAnchorPos < rNode.Len() + ? rNode.GetText()[nObjAnchorPos] + : 0; + if (cAnchorChar == CH_TXTATR_BREAKWORD) + { + const SwTextAttr* const pHint( + rNode.GetTextAttrForCharAt(nObjAnchorPos, RES_TXTATR_FLYCNT)); + if ( pHint ) + { + const SwFrameFormat* pFrameFormat = + static_cast<const SwTextFlyCnt*>(pHint)->GetFlyCnt().GetFrameFormat(); + if ( pFrameFormat->Which() == RES_FLYFRMFMT ) + { + SwNodeIndex nContentIndex = *(pFrameFormat->GetContent().GetContentIdx()); + ++nContentIndex; + if ( nContentIndex.GetNode().IsNoTextNode() ) + { + bRet = false; + // set needed data structure values for object positioning + SwRectFnSet aRectFnSet(&_rFrame); + SwRect aLastCharRect( _rFrame.getFrameArea() ); + aRectFnSet.SetWidth( aLastCharRect, 1 ); + _pAnchoredObj->maLastCharRect = aLastCharRect; + _pAnchoredObj->mnLastTopOfLine = aRectFnSet.GetTop(aLastCharRect); + } + } + } + } + } + } + + return bRet; +} + +/** + * Hide/show objects + * + * Method hides respectively shows objects, which are anchored at paragraph, + * at/as a character of the paragraph, corresponding to the paragraph and + * paragraph portion visibility. + * + * - is called from HideHidden() - should hide objects in hidden paragraphs and + * - from Format_() - should hide/show objects in partly visible paragraphs + */ +void SwTextFrame::HideAndShowObjects() +{ + if ( GetDrawObjs() ) + { + if ( IsHiddenNow() ) + { + // complete paragraph is hidden. Thus, hide all objects + for (SwAnchoredObject* i : *GetDrawObjs()) + { + SdrObject* pObj = i->DrawObj(); + SwContact* pContact = static_cast<SwContact*>(pObj->GetUserCall()); + // under certain conditions + const RndStdIds eAnchorType( pContact->GetAnchorId() ); + if ((eAnchorType != RndStdIds::FLY_AT_CHAR) || + sw_HideObj(*this, eAnchorType, pContact->GetAnchorFormat(), + i )) + { + pContact->MoveObjToInvisibleLayer( pObj ); + } + } + } + else + { + // paragraph is visible, but can contain hidden text portion. + // first we check if objects are allowed to be hidden: + const SwViewShell* pVsh = getRootFrame()->GetCurrShell(); + const bool bShouldBeHidden = !pVsh || !pVsh->GetWin() || + !pVsh->GetViewOptions()->IsShowHiddenChar(); + + // Thus, show all objects, which are anchored at paragraph and + // hide/show objects, which are anchored at/as character, according + // to the visibility of the anchor character. + for (SwAnchoredObject* i : *GetDrawObjs()) + { + SdrObject* pObj = i->DrawObj(); + SwContact* pContact = static_cast<SwContact*>(pObj->GetUserCall()); + // Determine anchor type only once + const RndStdIds eAnchorType( pContact->GetAnchorId() ); + + if (eAnchorType == RndStdIds::FLY_AT_PARA) + { + pContact->MoveObjToVisibleLayer( pObj ); + } + else if ((eAnchorType == RndStdIds::FLY_AT_CHAR) || + (eAnchorType == RndStdIds::FLY_AS_CHAR)) + { + sal_Int32 nHiddenStart; + sal_Int32 nHiddenEnd; + const SwFormatAnchor& rAnchorFormat = pContact->GetAnchorFormat(); + SwScriptInfo::GetBoundsOfHiddenRange( + *rAnchorFormat.GetAnchorNode()->GetTextNode(), + rAnchorFormat.GetAnchorContentOffset(), nHiddenStart, nHiddenEnd); + // Under certain conditions + if ( nHiddenStart != COMPLETE_STRING && bShouldBeHidden && + sw_HideObj(*this, eAnchorType, rAnchorFormat, i)) + { + pContact->MoveObjToInvisibleLayer( pObj ); + } + else + pContact->MoveObjToVisibleLayer( pObj ); + } + else + { + OSL_FAIL( "<SwTextFrame::HideAndShowObjects()> - object not anchored at/inside paragraph!?" ); + } + } + } + } + + if (IsFollow()) + { + SwTextFrame *pMaster = FindMaster(); + OSL_ENSURE(pMaster, "SwTextFrame without master"); + if (pMaster) + pMaster->HideAndShowObjects(); + } +} + +/** + * Returns the first possible break point in the current line. + * This method is used in SwTextFrame::Format() to decide whether the previous + * line has to be formatted as well. + * nFound is <= nEndLine. + */ +TextFrameIndex SwTextFrame::FindBrk(std::u16string_view aText, + const TextFrameIndex nStart, + const TextFrameIndex nEnd) +{ + sal_Int32 nFound = sal_Int32(nStart); + const sal_Int32 nEndLine = std::min(sal_Int32(nEnd), sal_Int32(aText.size()) - 1); + + // Skip all leading blanks. + while( nFound <= nEndLine && ' ' == aText[nFound] ) + { + nFound++; + } + + // A tricky situation with the TextAttr-Dummy-character (in this case "$"): + // "Dr.$Meyer" at the beginning of the second line. Typing a blank after that + // doesn't result in the word moving into first line, even though that would work. + // For this reason we don't skip the dummy char. + while( nFound <= nEndLine && ' ' != aText[nFound] ) + { + nFound++; + } + + return TextFrameIndex(nFound); +} + +bool SwTextFrame::IsIdxInside(TextFrameIndex const nPos, TextFrameIndex const nLen) const +{ + if (nPos == TextFrameIndex(COMPLETE_STRING)) // the "not found" range + return false; +// Silence over-eager warning emitted at least by GCC trunk towards 6: +#if defined __GNUC__ && !defined __clang__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstrict-overflow" +#endif + if (nLen != TextFrameIndex(COMPLETE_STRING) && GetOffset() > nPos + nLen) // the range preceded us +#if defined __GNUC__ && !defined __clang__ +#pragma GCC diagnostic pop +#endif + return false; + + if( !GetFollow() ) // the range doesn't precede us, + return true; // nobody follows us. + + TextFrameIndex const nMax = GetFollow()->GetOffset(); + + // either the range overlap or our text has been deleted + // sw_redlinehide: GetText() should be okay here because it has already + // been updated in the INS/DEL hint case + if (nMax > nPos || nMax > TextFrameIndex(GetText().getLength())) + return true; + + // changes made in the first line of a follow can modify the master + const SwParaPortion* pPara = GetFollow()->GetPara(); + return pPara && ( nPos <= nMax + pPara->GetLen() ); +} + +inline void SwTextFrame::InvalidateRange(const SwCharRange &aRange, const tools::Long nD) +{ + if ( IsIdxInside( aRange.Start(), aRange.Len() ) ) + InvalidateRange_( aRange, nD ); +} + +void SwTextFrame::InvalidateRange_( const SwCharRange &aRange, const tools::Long nD) +{ + if ( !HasPara() ) + { InvalidateSize(); + return; + } + + SetWidow( false ); + SwParaPortion *pPara = GetPara(); + + bool bInv = false; + if( 0 != nD ) + { + // In nDelta the differences between old and new + // linelengths are being added, that's why it's negative + // if chars have been added and positive, if chars have + // deleted + pPara->SetDelta(pPara->GetDelta() + nD); + bInv = true; + } + SwCharRange &rReformat = pPara->GetReformat(); + if(aRange != rReformat) { + if (TextFrameIndex(COMPLETE_STRING) == rReformat.Len()) + rReformat = aRange; + else + rReformat += aRange; + bInv = true; + } + if(bInv) + { + InvalidateSize(); + } +} + +void SwTextFrame::CalcLineSpace() +{ + OSL_ENSURE( ! IsVertical() || ! IsSwapped(), + "SwTextFrame::CalcLineSpace with swapped frame!" ); + + if( IsLocked() || !HasPara() ) + return; + + if( GetDrawObjs() || + GetTextNodeForParaProps()->GetSwAttrSet().GetFirstLineIndent().IsAutoFirst()) + { + Init(); + return; + } + + SwParaPortion *const pPara(GetPara()); + assert(pPara); + if (pPara->IsFixLineHeight()) + { + Init(); + return; + } + + Size aNewSize( getFramePrintArea().SSize() ); + + SwTextFormatInfo aInf( getRootFrame()->GetCurrShell()->GetOut(), this ); + SwTextFormatter aLine( this, &aInf ); + if( aLine.GetDropLines() ) + { + Init(); + return; + } + + aLine.Top(); + aLine.RecalcRealHeight(); + + aNewSize.setHeight( (aLine.Y() - getFrameArea().Top()) + aLine.GetLineHeight() ); + + SwTwips nDelta = aNewSize.Height() - getFramePrintArea().Height(); + // Underflow with free-flying frames + if( aInf.GetTextFly().IsOn() ) + { + SwRect aTmpFrame( getFrameArea() ); + if( nDelta < 0 ) + aTmpFrame.Height( getFramePrintArea().Height() ); + else + aTmpFrame.Height( aNewSize.Height() ); + if( aInf.GetTextFly().Relax( aTmpFrame ) ) + { + Init(); + return; + } + } + + if( !nDelta ) + return; + + SwTextFrameBreak aBreak( this ); + if( GetFollow() || aBreak.IsBreakNow( aLine ) ) + { + // if there is a Follow() or if we need to break here, reformat + Init(); + } + else + { + // everything is business as usual... + pPara->SetPrepAdjust(); + pPara->SetPrep(); + } +} + +static void lcl_SetWrong( SwTextFrame& rFrame, SwTextNode const& rNode, + sal_Int32 const nPos, sal_Int32 const nCnt, bool const bMove) +{ + if ( !rFrame.IsFollow() ) + { + SwTextNode& rTextNode = const_cast<SwTextNode&>(rNode); + sw::GrammarContact* pGrammarContact = sw::getGrammarContactFor(rTextNode); + SwGrammarMarkUp* pWrongGrammar = pGrammarContact ? + pGrammarContact->getGrammarCheck( rTextNode, false ) : + rTextNode.GetGrammarCheck(); + bool bGrammarProxy = pWrongGrammar != rTextNode.GetGrammarCheck(); + if( bMove ) + { + if( rTextNode.GetWrong() ) + rTextNode.GetWrong()->Move( nPos, nCnt ); + if( pWrongGrammar ) + pWrongGrammar->MoveGrammar( nPos, nCnt ); + if( bGrammarProxy && rTextNode.GetGrammarCheck() ) + rTextNode.GetGrammarCheck()->MoveGrammar( nPos, nCnt ); + if( rTextNode.GetSmartTags() ) + rTextNode.GetSmartTags()->Move( nPos, nCnt ); + } + else + { + if( rTextNode.GetWrong() ) + rTextNode.GetWrong()->Invalidate( nPos, nCnt ); + if( pWrongGrammar ) + pWrongGrammar->Invalidate( nPos, nCnt ); + if( rTextNode.GetSmartTags() ) + rTextNode.GetSmartTags()->Invalidate( nPos, nCnt ); + } + const sal_Int32 nEnd = nPos + (nCnt > 0 ? nCnt : 1 ); + if ( !rTextNode.GetWrong() && !rTextNode.IsWrongDirty() ) + { + rTextNode.SetWrong( std::make_unique<SwWrongList>( WRONGLIST_SPELL ) ); + rTextNode.GetWrong()->SetInvalid( nPos, nEnd ); + } + if ( !rTextNode.GetSmartTags() && !rTextNode.IsSmartTagDirty() ) + { + rTextNode.SetSmartTags( std::make_unique<SwWrongList>( WRONGLIST_SMARTTAG ) ); + rTextNode.GetSmartTags()->SetInvalid( nPos, nEnd ); + } + rTextNode.SetWrongDirty(sw::WrongState::TODO); + rTextNode.SetGrammarCheckDirty( true ); + rTextNode.SetWordCountDirty( true ); + rTextNode.SetAutoCompleteWordDirty( true ); + rTextNode.SetSmartTagDirty( true ); + } + + SwRootFrame *pRootFrame = rFrame.getRootFrame(); + if (pRootFrame) + { + pRootFrame->SetNeedGrammarCheck( true ); + } + + SwPageFrame *pPage = rFrame.FindPageFrame(); + if( pPage ) + { + pPage->InvalidateSpelling(); + pPage->InvalidateAutoCompleteWords(); + pPage->InvalidateWordCount(); + pPage->InvalidateSmartTags(); + } +} + +static void lcl_SetScriptInval(SwTextFrame& rFrame, TextFrameIndex const nPos) +{ + if( rFrame.GetPara() ) + rFrame.GetPara()->GetScriptInfo().SetInvalidityA( nPos ); +} + +// note: SwClientNotify will be called once for every frame => just fix own Ofst +static void lcl_ModifyOfst(SwTextFrame & rFrame, + TextFrameIndex const nPos, TextFrameIndex const nLen, + TextFrameIndex (* op)(TextFrameIndex const&, TextFrameIndex const&)) +{ + assert(nLen != TextFrameIndex(COMPLETE_STRING)); + if (rFrame.IsFollow() && nPos < rFrame.GetOffset()) + { + rFrame.ManipOfst( std::max(nPos, op(rFrame.GetOffset(), nLen)) ); + assert(sal_Int32(rFrame.GetOffset()) <= rFrame.GetText().getLength()); + } +} + +namespace { + +void UpdateMergedParaForMove(sw::MergedPara & rMerged, + SwTextFrame & rTextFrame, + bool & o_rbRecalcFootnoteFlag, + SwTextNode const& rDestNode, + SwTextNode const& rNode, + sal_Int32 const nDestStart, + sal_Int32 const nSourceStart, + sal_Int32 const nLen) +{ + std::vector<std::pair<sal_Int32, sal_Int32>> deleted; + sal_Int32 const nSourceEnd(nSourceStart + nLen); + sal_Int32 nLastEnd(0); + for (const auto& rExt : rMerged.extents) + { + if (rExt.pNode == &rNode) + { + sal_Int32 const nStart(std::max(nLastEnd, nSourceStart)); + sal_Int32 const nEnd(std::min(rExt.nStart, nSourceEnd)); + if (nStart < nEnd) + { + deleted.emplace_back(nStart, nEnd); + } + nLastEnd = rExt.nEnd; + if (nSourceEnd <= rExt.nEnd) + { + break; + } + } + else if (rNode.GetIndex() < rExt.pNode->GetIndex()) + { + break; + } + } + if (nLastEnd != rNode.Len()) // without nLen, string yet to be removed + { + if (nLastEnd < nSourceEnd) + { + deleted.emplace_back(std::max(nLastEnd, nSourceStart), nSourceEnd); + } + } + if (deleted.empty()) + return; + + o_rbRecalcFootnoteFlag = true; + for (auto const& it : deleted) + { + sal_Int32 const nStart(it.first - nSourceStart + nDestStart); + TextFrameIndex const nDeleted = UpdateMergedParaForDelete(rMerged, false, + rDestNode, nStart, it.second - it.first); +//FIXME asserts valid for join - but if called from split, the new node isn't there yet and it will be added later... assert(nDeleted); +// assert(nDeleted == it.second - it.first); + if(nDeleted) + { + // InvalidateRange/lcl_SetScriptInval was called sufficiently for InsertText + lcl_SetWrong(rTextFrame, rDestNode, nStart, it.first - it.second, false); + TextFrameIndex const nIndex(sw::MapModelToView(rMerged, &rDestNode, nStart)); + lcl_ModifyOfst(rTextFrame, nIndex, nDeleted, &o3tl::operator-<sal_Int32, Tag_TextFrameIndex>); + } + } +} + +} // namespace + +/** + * Related: fdo#56031 filter out attribute changes that don't matter for + * humans/a11y to stop flooding the destination mortal with useless noise + */ +#if !ENABLE_WASM_STRIP_ACCESSIBILITY +static bool isA11yRelevantAttribute(sal_uInt16 nWhich) +{ + return nWhich != RES_CHRATR_RSID; +} + +static bool hasA11yRelevantAttribute( const std::vector<sal_uInt16>& rWhichFmtAttr ) +{ + for( sal_uInt16 nWhich : rWhichFmtAttr ) + if ( isA11yRelevantAttribute( nWhich ) ) + return true; + + return false; +} +#endif // ENABLE_WASM_STRIP_ACCESSIBILITY + +// Note: for now this overrides SwClient::SwClientNotify; the intermediary +// classes still override SwClient::Modify, which should continue to work +// as their implementation of SwClientNotify is SwClient's which calls Modify. +// Therefore we also don't need to call SwClient::SwClientNotify(rModify, rHint) +// because that's all it does, and this implementation calls +// SwContentFrame::SwClientNotify() when appropriate. +void SwTextFrame::SwClientNotify(SwModify const& rModify, SfxHint const& rHint) +{ + SfxPoolItem const* pOld(nullptr); + SfxPoolItem const* pNew(nullptr); + sw::MoveText const* pMoveText(nullptr); + sw::InsertText const* pInsertText(nullptr); + sw::DeleteText const* pDeleteText(nullptr); + sw::DeleteChar const* pDeleteChar(nullptr); + sw::RedlineDelText const* pRedlineDelText(nullptr); + sw::RedlineUnDelText const* pRedlineUnDelText(nullptr); + + sal_uInt16 nWhich = 0; + if (rHint.GetId() == SfxHintId::SwLegacyModify) + { + auto pHint = static_cast<const sw::LegacyModifyHint*>(&rHint); + pOld = pHint->m_pOld; + pNew = pHint->m_pNew; + nWhich = pHint->GetWhich(); + } + else if (rHint.GetId() == SfxHintId::SwInsertText) + { + pInsertText = static_cast<const sw::InsertText*>(&rHint); + } + else if (rHint.GetId() == SfxHintId::SwDeleteText) + { + pDeleteText = static_cast<const sw::DeleteText*>(&rHint); + } + else if (rHint.GetId() == SfxHintId::SwDeleteChar) + { + pDeleteChar = static_cast<const sw::DeleteChar*>(&rHint); + } + else if (rHint.GetId() == SfxHintId::SwDocPosUpdateAtIndex) + { + auto pDocPosAt = static_cast<const sw::DocPosUpdateAtIndex*>(&rHint); + Broadcast(SfxHint()); // notify SwAccessibleParagraph + if(IsLocked()) + return; + if(pDocPosAt->m_nDocPos > getFrameArea().Top()) + return; + TextFrameIndex const nIndex(MapModelToView( + &pDocPosAt->m_rNode, + pDocPosAt->m_nIndex)); + InvalidateRange(SwCharRange(nIndex, TextFrameIndex(1))); + return; + } + else if (rHint.GetId() == SfxHintId::SwVirtPageNumHint) + { + auto& rVirtPageNumHint = const_cast<sw::VirtPageNumHint&>(static_cast<const sw::VirtPageNumHint&>(rHint)); + if(!IsInDocBody() || IsFollow() || rVirtPageNumHint.IsFound()) + return; + if(const SwPageFrame* pPage = FindPageFrame()) + pPage->UpdateVirtPageNumInfo(rVirtPageNumHint, this); + return; + } + else if (auto const pHt = dynamic_cast<sw::MoveText const*>(&rHint)) + { + pMoveText = pHt; + } + else if (auto const pHynt = dynamic_cast<sw::RedlineDelText const*>(&rHint)) + { + pRedlineDelText = pHynt; + } + else if (auto const pHnt = dynamic_cast<sw::RedlineUnDelText const*>(&rHint)) + { + pRedlineUnDelText = pHnt; + } + else + { + assert(!"unexpected hint"); + } + + if (m_pMergedPara) + { + assert(m_pMergedPara->listener.IsListeningTo(&rModify)); + } + + SwTextNode const& rNode(static_cast<SwTextNode const&>(rModify)); + + // modifications concerning frame attributes are processed by the base class + if( IsInRange( aFrameFormatSetRange, nWhich ) || RES_FMT_CHG == nWhich ) + { + if (m_pMergedPara) + { // ignore item set changes that don't apply + SwTextNode const*const pAttrNode( + (nWhich == RES_PAGEDESC || nWhich == RES_BREAK) + ? m_pMergedPara->pFirstNode + : m_pMergedPara->pParaPropsNode); + if (pAttrNode != &rModify) + { + return; + } + } + SwContentFrame::SwClientNotify(rModify, sw::LegacyModifyHint(pOld, pNew)); + if( nWhich == RES_FMT_CHG && getRootFrame()->GetCurrShell() ) + { + // collection has changed + Prepare(); + InvalidatePrt_(); + lcl_SetWrong( *this, rNode, 0, COMPLETE_STRING, false ); + SetDerivedR2L( false ); + CheckDirChange(); + // Force complete paint due to existing indents. + SetCompletePaint(); + InvalidateLineNum(); + } + return; + } + + if (m_pMergedPara && m_pMergedPara->pParaPropsNode != &rModify) + { + if (isPARATR(nWhich) || isPARATR_LIST(nWhich)) // FRMATR handled above + { + return; // ignore it + } + } + + Broadcast(SfxHint()); // notify SwAccessibleParagraph + + // while locked ignore all modifications + if( IsLocked() ) + return; + + // save stack + // warning: one has to ensure that all variables are set + TextFrameIndex nPos; + TextFrameIndex nLen; + bool bSetFieldsDirty = false; + bool bRecalcFootnoteFlag = false; + + if (pRedlineDelText) + { + if (m_pMergedPara) + { + sal_Int32 const nNPos = pRedlineDelText->nStart; + sal_Int32 const nNLen = pRedlineDelText->nLen; + nPos = MapModelToView(&rNode, nNPos); + // update merged before doing anything else + nLen = UpdateMergedParaForDelete(*m_pMergedPara, false, rNode, nNPos, nNLen); + const sal_Int32 m = -nNLen; + if (nLen && IsIdxInside(nPos, nLen)) + { + InvalidateRange( SwCharRange(nPos, TextFrameIndex(1)), m ); + } + lcl_SetWrong( *this, rNode, nNPos, m, false ); + if (nLen) + { + lcl_SetScriptInval( *this, nPos ); + bSetFieldsDirty = bRecalcFootnoteFlag = true; + lcl_ModifyOfst(*this, nPos, nLen, &o3tl::operator-<sal_Int32, Tag_TextFrameIndex>); + } + } + } + else if (pRedlineUnDelText) + { + if (m_pMergedPara) + { + sal_Int32 const nNPos = pRedlineUnDelText->nStart; + sal_Int32 const nNLen = pRedlineUnDelText->nLen; + nPos = MapModelToView(&rNode, nNPos); + nLen = UpdateMergedParaForInsert(*m_pMergedPara, false, rNode, nNPos, nNLen); + if (IsIdxInside(nPos, nLen)) + { + if (!nLen) + { + // Refresh NumPortions even when line is empty! + if (nPos) + InvalidateSize(); + else + Prepare(); + } + else + InvalidateRange_( SwCharRange( nPos, nLen ), nNLen ); + } + lcl_SetWrong( *this, rNode, nNPos, nNLen, false ); + lcl_SetScriptInval( *this, nPos ); + bSetFieldsDirty = true; + lcl_ModifyOfst(*this, nPos, nLen, &o3tl::operator+<sal_Int32, Tag_TextFrameIndex>); + } + } + else if (pMoveText) + { + if (m_pMergedPara + && m_pMergedPara->pFirstNode->GetIndex() <= pMoveText->pDestNode->GetIndex() + && pMoveText->pDestNode->GetIndex() <= m_pMergedPara->pLastNode->GetIndex()) + { // if it's not 2 nodes in merged frame, assume the target node doesn't have frames at all + assert(abs(rNode.GetIndex() - pMoveText->pDestNode->GetIndex()) == SwNodeOffset(1)); + UpdateMergedParaForMove(*m_pMergedPara, + *this, + bRecalcFootnoteFlag, + *pMoveText->pDestNode, rNode, + pMoveText->nDestStart, + pMoveText->nSourceStart, + pMoveText->nLen); + } + else + { + // there is a situation where this is okay: from JoinNext, which will then call CheckResetRedlineMergeFlag, which will then create merged from scratch for this frame + // assert(!m_pMergedPara || !getRootFrame()->IsHideRedlines() || !pMoveText->pDestNode->getLayoutFrame(getRootFrame())); + } + } + else if (pInsertText) + { + nPos = MapModelToView(&rNode, pInsertText->nPos); + // unlike redlines, inserting into fieldmark must be explicitly handled + bool isHidden(false); + switch (getRootFrame()->GetFieldmarkMode()) + { + case sw::FieldmarkMode::ShowCommand: + isHidden = pInsertText->isInsideFieldmarkResult; + break; + case sw::FieldmarkMode::ShowResult: + isHidden = pInsertText->isInsideFieldmarkCommand; + break; + case sw::FieldmarkMode::ShowBoth: // just to avoid the warning + break; + } + if (!isHidden) + { + nLen = TextFrameIndex(pInsertText->nLen); + if (m_pMergedPara) + { + UpdateMergedParaForInsert(*m_pMergedPara, true, rNode, pInsertText->nPos, pInsertText->nLen); + } + if( IsIdxInside( nPos, nLen ) ) + { + if( !nLen ) + { + // Refresh NumPortions even when line is empty! + if( nPos ) + InvalidateSize(); + else + Prepare(); + } + else + InvalidateRange_( SwCharRange( nPos, nLen ), pInsertText->nLen ); + } + lcl_SetScriptInval( *this, nPos ); + bSetFieldsDirty = true; + lcl_ModifyOfst(*this, nPos, nLen, &o3tl::operator+<sal_Int32, Tag_TextFrameIndex>); + } + lcl_SetWrong( *this, rNode, pInsertText->nPos, pInsertText->nLen, true ); + } + else if (pDeleteText) + { + nPos = MapModelToView(&rNode, pDeleteText->nStart); + if (m_pMergedPara) + { // update merged before doing anything else + nLen = UpdateMergedParaForDelete(*m_pMergedPara, true, rNode, pDeleteText->nStart, pDeleteText->nLen); + } + else + { + nLen = TextFrameIndex(pDeleteText->nLen); + } + const sal_Int32 m = -pDeleteText->nLen; + if ((!m_pMergedPara || nLen) && IsIdxInside(nPos, nLen)) + { + if( !nLen ) + InvalidateSize(); + else + InvalidateRange( SwCharRange(nPos, TextFrameIndex(1)), m ); + } + lcl_SetWrong( *this, rNode, pDeleteText->nStart, m, true ); + if (nLen) + { + lcl_SetScriptInval( *this, nPos ); + bSetFieldsDirty = bRecalcFootnoteFlag = true; + lcl_ModifyOfst(*this, nPos, nLen, &o3tl::operator-<sal_Int32, Tag_TextFrameIndex>); + } + } + else if (pDeleteChar) + { + nPos = MapModelToView(&rNode, pDeleteChar->m_nPos); + if (m_pMergedPara) + { + nLen = UpdateMergedParaForDelete(*m_pMergedPara, true, rNode, pDeleteChar->m_nPos, 1); + } + else + { + nLen = TextFrameIndex(1); + } + lcl_SetWrong( *this, rNode, pDeleteChar->m_nPos, -1, true ); + if (nLen) + { + InvalidateRange( SwCharRange(nPos, nLen), -1 ); + lcl_SetScriptInval( *this, nPos ); + bSetFieldsDirty = bRecalcFootnoteFlag = true; + lcl_ModifyOfst(*this, nPos, nLen, &o3tl::operator-<sal_Int32, Tag_TextFrameIndex>); + } + } + else switch (nWhich) + { + case RES_LINENUMBER: + { + assert(false); // should have been forwarded to SwContentFrame + InvalidateLineNum(); + } + break; + case RES_UPDATE_ATTR: + { + const SwUpdateAttr* pNewUpdate = static_cast<const SwUpdateAttr*>(pNew); + + sal_Int32 const nNPos = pNewUpdate->getStart(); + sal_Int32 const nNLen = pNewUpdate->getEnd() - nNPos; + nPos = MapModelToView(&rNode, nNPos); + nLen = MapModelToView(&rNode, nNPos + nNLen) - nPos; + if( IsIdxInside( nPos, nLen ) ) + { + // We need to reformat anyways, even if the invalidated + // range is empty. + // E.g.: empty line, set 14 pt! + + // FootnoteNumbers need to be formatted + if( !nLen ) + nLen = TextFrameIndex(1); + + InvalidateRange_( SwCharRange( nPos, nLen) ); + const sal_uInt16 nTmp = pNewUpdate->getWhichAttr(); + + if( ! nTmp || RES_TXTATR_CHARFMT == nTmp || RES_TXTATR_INETFMT == nTmp || RES_TXTATR_AUTOFMT == nTmp || + RES_FMT_CHG == nTmp || RES_ATTRSET_CHG == nTmp ) + { + lcl_SetWrong( *this, rNode, nNPos, nNPos + nNLen, false ); + lcl_SetScriptInval( *this, nPos ); + } + } + +#if !ENABLE_WASM_STRIP_ACCESSIBILITY + if( isA11yRelevantAttribute( pNewUpdate->getWhichAttr() ) && + hasA11yRelevantAttribute( pNewUpdate->getFmtAttrs() ) ) + { + SwViewShell* pViewSh = getRootFrame() ? getRootFrame()->GetCurrShell() : nullptr; + if ( pViewSh ) + { + pViewSh->InvalidateAccessibleParaAttrs( *this ); + } + } +#endif + } + break; + case RES_OBJECTDYING: + break; + + case RES_PARATR_LINESPACING: + { + CalcLineSpace(); + InvalidateSize(); + InvalidatePrt_(); + if( IsInSct() && !GetPrev() ) + { + SwSectionFrame *pSect = FindSctFrame(); + if( pSect->ContainsAny() == this ) + pSect->InvalidatePrt(); + } + + // i#11859 + // (1) Also invalidate next frame on next page/column. + // (2) Skip empty sections and hidden paragraphs + // Thus, use method <InvalidateNextPrtArea()> + InvalidateNextPrtArea(); + + SetCompletePaint(); + } + break; + + case RES_TXTATR_FIELD: + case RES_TXTATR_ANNOTATION: + { + sal_Int32 const nNPos = static_cast<const SwFormatField*>(pNew)->GetTextField()->GetStart(); + nPos = MapModelToView(&rNode, nNPos); + if (IsIdxInside(nPos, TextFrameIndex(1))) + { + if (SfxPoolItem::areSame( pNew, pOld )) + { + // only repaint + // opt: invalidate window? + InvalidatePage(); + SetCompletePaint(); + } + else + InvalidateRange_(SwCharRange(nPos, TextFrameIndex(1))); + } + bSetFieldsDirty = true; + // ST2 + if ( SwSmartTagMgr::Get().IsSmartTagsEnabled() ) + lcl_SetWrong( *this, rNode, nNPos, nNPos + 1, false ); + } + break; + + case RES_TXTATR_FTN : + { + if (!IsInFootnote()) + { // the hint may be sent from the anchor node, or from a + // node in the footnote; the anchor index is only valid in the + // anchor node! + assert(rNode == static_cast<const SwFormatFootnote*>(pNew)->GetTextFootnote()->GetTextNode()); + nPos = MapModelToView(&rNode, + static_cast<const SwFormatFootnote*>(pNew)->GetTextFootnote()->GetStart()); + } +#ifdef _MSC_VER + else nPos = TextFrameIndex(42); // shut up MSVC 2017 spurious warning C4701 +#endif + if (IsInFootnote() || IsIdxInside(nPos, TextFrameIndex(1))) + Prepare( PrepareHint::FootnoteInvalidation, static_cast<const SwFormatFootnote*>(pNew)->GetTextFootnote() ); + break; + } + + case RES_ATTRSET_CHG: + { + InvalidateLineNum(); + + const SwAttrSet& rNewSet = *static_cast<const SwAttrSetChg*>(pNew)->GetChgSet(); + int nClear = 0; + sal_uInt16 nCount = rNewSet.Count(); + + if( const SwFormatFootnote* pItem = rNewSet.GetItemIfSet( RES_TXTATR_FTN, false ) ) + { + nPos = MapModelToView(&rNode, pItem->GetTextFootnote()->GetStart()); + if (IsIdxInside(nPos, TextFrameIndex(1))) + Prepare( PrepareHint::FootnoteInvalidation, pNew ); + nClear = 0x01; + --nCount; + } + + if( const SwFormatField* pItem = rNewSet.GetItemIfSet( RES_TXTATR_FIELD, false ) ) + { + nPos = MapModelToView(&rNode, pItem->GetTextField()->GetStart()); + if (IsIdxInside(nPos, TextFrameIndex(1))) + { + const SfxPoolItem* pOldItem = pOld ? + &(static_cast<const SwAttrSetChg*>(pOld)->GetChgSet()->Get(RES_TXTATR_FIELD)) : nullptr; + if (SfxPoolItem::areSame( pItem, pOldItem )) + { + InvalidatePage(); + SetCompletePaint(); + } + else + InvalidateRange_(SwCharRange(nPos, TextFrameIndex(1))); + } + nClear |= 0x02; + --nCount; + } + bool bLineSpace = SfxItemState::SET == rNewSet.GetItemState( + RES_PARATR_LINESPACING, false ), + bRegister = SfxItemState::SET == rNewSet.GetItemState( + RES_PARATR_REGISTER, false ); + if ( bLineSpace || bRegister ) + { + if (!m_pMergedPara || m_pMergedPara->pParaPropsNode == &rModify) + { + Prepare( bRegister ? PrepareHint::Register : PrepareHint::AdjustSizeWithoutFormatting ); + CalcLineSpace(); + InvalidateSize(); + InvalidatePrt_(); + + // i#11859 + // (1) Also invalidate next frame on next page/column. + // (2) Skip empty sections and hidden paragraphs + // Thus, use method <InvalidateNextPrtArea()> + InvalidateNextPrtArea(); + + SetCompletePaint(); + } + nClear |= 0x04; + if ( bLineSpace ) + { + --nCount; + if ((!m_pMergedPara || m_pMergedPara->pParaPropsNode == &rModify) + && IsInSct() && !GetPrev()) + { + SwSectionFrame *pSect = FindSctFrame(); + if( pSect->ContainsAny() == this ) + pSect->InvalidatePrt(); + } + } + if ( bRegister ) + --nCount; + } + if ( SfxItemState::SET == rNewSet.GetItemState( RES_PARATR_SPLIT, + false )) + { + if (!m_pMergedPara || m_pMergedPara->pParaPropsNode == &rModify) + { + if (GetPrev()) + CheckKeep(); + Prepare(); + InvalidateSize(); + } + nClear |= 0x08; + --nCount; + } + + if( SfxItemState::SET == rNewSet.GetItemState( RES_BACKGROUND, false) + && (!m_pMergedPara || m_pMergedPara->pParaPropsNode == &rModify) + && !IsFollow() && GetDrawObjs() ) + { + SwSortedObjs *pObjs = GetDrawObjs(); + for ( size_t i = 0; GetDrawObjs() && i < pObjs->size(); ++i ) + { + SwAnchoredObject* pAnchoredObj = (*pObjs)[i]; + if ( auto pFly = pAnchoredObj->DynCastFlyFrame() ) + { + if( !pFly->IsFlyInContentFrame() ) + { + const SvxBrushItem &rBack = + pFly->GetAttrSet()->GetBackground(); + // #GetTransChg# + // following condition determines, if the fly frame + // "inherites" the background color of text frame. + // This is the case, if fly frame background + // color is "no fill"/"auto fill" and if the fly frame + // has no background graphic. + // Thus, check complete fly frame background + // color and *not* only its transparency value + if ( (rBack.GetColor() == COL_TRANSPARENT) && + rBack.GetGraphicPos() == GPOS_NONE ) + { + pFly->SetCompletePaint(); + pFly->InvalidatePage(); + } + } + } + } + } + + if ( SfxItemState::SET == + rNewSet.GetItemState( RES_TXTATR_CHARFMT, false ) ) + { + lcl_SetWrong( *this, rNode, 0, COMPLETE_STRING, false ); + lcl_SetScriptInval( *this, TextFrameIndex(0) ); + } + else if ( SfxItemState::SET == + rNewSet.GetItemState( RES_CHRATR_LANGUAGE, false ) || + SfxItemState::SET == + rNewSet.GetItemState( RES_CHRATR_CJK_LANGUAGE, false ) || + SfxItemState::SET == + rNewSet.GetItemState( RES_CHRATR_CTL_LANGUAGE, false ) ) + lcl_SetWrong( *this, rNode, 0, COMPLETE_STRING, false ); + else if ( SfxItemState::SET == + rNewSet.GetItemState( RES_CHRATR_FONT, false ) || + SfxItemState::SET == + rNewSet.GetItemState( RES_CHRATR_CJK_FONT, false ) || + SfxItemState::SET == + rNewSet.GetItemState( RES_CHRATR_CTL_FONT, false ) ) + lcl_SetScriptInval( *this, TextFrameIndex(0) ); + else if ( SfxItemState::SET == + rNewSet.GetItemState( RES_FRAMEDIR, false ) + && (!m_pMergedPara || m_pMergedPara->pParaPropsNode == &rModify)) + { + SetDerivedR2L( false ); + CheckDirChange(); + // Force complete paint due to existing indents. + SetCompletePaint(); + } + + if( nCount ) + { + if( getRootFrame()->GetCurrShell() ) + { + Prepare(); + InvalidatePrt_(); + } + + if (nClear || (m_pMergedPara && + (m_pMergedPara->pParaPropsNode != &rModify || + m_pMergedPara->pFirstNode != &rModify))) + { + assert(pOld); + SwAttrSetChg aOldSet( *static_cast<const SwAttrSetChg*>(pOld) ); + SwAttrSetChg aNewSet( *static_cast<const SwAttrSetChg*>(pNew) ); + + if (m_pMergedPara && m_pMergedPara->pParaPropsNode != &rModify) + { + for (sal_uInt16 i = RES_PARATR_BEGIN; i != RES_FRMATR_END; ++i) + { + if (i != RES_BREAK && i != RES_PAGEDESC) + { + aOldSet.ClearItem(i); + aNewSet.ClearItem(i); + } + } + for (sal_uInt16 i = XATTR_FILL_FIRST; i <= XATTR_FILL_LAST; ++i) + { + aOldSet.ClearItem(i); + aNewSet.ClearItem(i); + } + } + if (m_pMergedPara && m_pMergedPara->pFirstNode != &rModify) + { + aOldSet.ClearItem(RES_BREAK); + aNewSet.ClearItem(RES_BREAK); + aOldSet.ClearItem(RES_PAGEDESC); + aNewSet.ClearItem(RES_PAGEDESC); + } + + if( 0x01 & nClear ) + { + aOldSet.ClearItem( RES_TXTATR_FTN ); + aNewSet.ClearItem( RES_TXTATR_FTN ); + } + if( 0x02 & nClear ) + { + aOldSet.ClearItem( RES_TXTATR_FIELD ); + aNewSet.ClearItem( RES_TXTATR_FIELD ); + } + if ( 0x04 & nClear ) + { + if ( bLineSpace ) + { + aOldSet.ClearItem( RES_PARATR_LINESPACING ); + aNewSet.ClearItem( RES_PARATR_LINESPACING ); + } + if ( bRegister ) + { + aOldSet.ClearItem( RES_PARATR_REGISTER ); + aNewSet.ClearItem( RES_PARATR_REGISTER ); + } + } + if ( 0x08 & nClear ) + { + aOldSet.ClearItem( RES_PARATR_SPLIT ); + aNewSet.ClearItem( RES_PARATR_SPLIT ); + } + if (aOldSet.Count() || aNewSet.Count()) + { + SwContentFrame::SwClientNotify(rModify, sw::LegacyModifyHint(&aOldSet, &aNewSet)); + } + } + else + SwContentFrame::SwClientNotify(rModify, sw::LegacyModifyHint(pOld, pNew)); + } + +#if !ENABLE_WASM_STRIP_ACCESSIBILITY + if (isA11yRelevantAttribute(nWhich)) + { + SwViewShell* pViewSh = getRootFrame() ? getRootFrame()->GetCurrShell() : nullptr; + if ( pViewSh ) + { + pViewSh->InvalidateAccessibleParaAttrs( *this ); + } + } +#endif + } + break; + case RES_PARATR_SPLIT: + if ( GetPrev() ) + CheckKeep(); + Prepare(); + bSetFieldsDirty = true; + break; + case RES_FRAMEDIR : + assert(false); // should have been forwarded to SwContentFrame + SetDerivedR2L( false ); + CheckDirChange(); + break; + default: + { + Prepare(); + InvalidatePrt_(); + if ( !nWhich ) + { + // is called by e. g. HiddenPara with 0 + SwFrame *pNxt = FindNext(); + if ( nullptr != pNxt ) + pNxt->InvalidatePrt(); + } + } + } // switch + + if( bSetFieldsDirty ) + GetDoc().getIDocumentFieldsAccess().SetFieldsDirty( true, &rNode, SwNodeOffset(1) ); + + if ( bRecalcFootnoteFlag ) + CalcFootnoteFlag(); +} + +void SwTextFrame::PrepWidows( const sal_uInt16 nNeed, bool bNotify ) +{ + OSL_ENSURE(GetFollow() && nNeed, "+SwTextFrame::Prepare: lost all friends"); + + SwParaPortion *pPara = GetPara(); + if ( !pPara ) + return; + pPara->SetPrepWidows(); + + sal_uInt16 nHave = nNeed; + + // We yield a few lines and shrink in CalcPreps() + SwSwapIfNotSwapped swap( this ); + + SwTextSizeInfo aInf( this ); + SwTextMargin aLine( this, &aInf ); + aLine.Bottom(); + TextFrameIndex nTmpLen = aLine.GetCurr()->GetLen(); + while( nHave && aLine.PrevLine() ) + { + if( nTmpLen ) + --nHave; + nTmpLen = aLine.GetCurr()->GetLen(); + } + + // If it's certain that we can yield lines, the Master needs + // to check the widow rule + if( !nHave ) + { + bool bSplit = true; + if( !IsFollow() ) // only a master decides about orphans + { + const WidowsAndOrphans aWidOrp( this ); + bSplit = ( aLine.GetLineNr() >= aWidOrp.GetOrphansLines() && + aLine.GetLineNr() >= aLine.GetDropLines() ); + } + + if( bSplit ) + { + GetFollow()->SetOffset( aLine.GetEnd() ); + aLine.TruncLines( true ); + if( pPara->IsFollowField() ) + GetFollow()->SetFieldFollow( true ); + } + } + if ( bNotify ) + { + InvalidateSize_(); + InvalidatePage(); + } +} + +static bool lcl_ErgoVadis(SwTextFrame* pFrame, TextFrameIndex & rPos, const PrepareHint ePrep) +{ + const SwFootnoteInfo &rFootnoteInfo = pFrame->GetDoc().GetFootnoteInfo(); + if( ePrep == PrepareHint::ErgoSum ) + { + if( rFootnoteInfo.m_aErgoSum.isEmpty() ) + return false; + rPos = pFrame->GetOffset(); + } + else + { + if( rFootnoteInfo.m_aQuoVadis.isEmpty() ) + return false; + if( pFrame->HasFollow() ) + rPos = pFrame->GetFollow()->GetOffset(); + else + rPos = TextFrameIndex(pFrame->GetText().getLength()); + if( rPos ) + --rPos; // our last character + } + return true; +} + +// Silence over-eager warning emitted at least by GCC 5.3.1 +#if defined __GNUC__ && !defined __clang__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wstrict-overflow" +#endif +bool SwTextFrame::Prepare( const PrepareHint ePrep, const void* pVoid, + bool bNotify ) +{ + bool bParaPossiblyInvalid = false; + + SwFrameSwapper aSwapper( this, false ); + + if ( IsEmpty() ) + { + switch ( ePrep ) + { + case PrepareHint::BossChanged: + SetInvalidVert( true ); // Test + [[fallthrough]]; + case PrepareHint::WidowsOrphans: + case PrepareHint::Widows: + case PrepareHint::FootnoteInvalidationGone : return bParaPossiblyInvalid; + + case PrepareHint::FramePositionChanged : + { + // We also need an InvalidateSize for Areas (with and without columns), + // so that we format and bUndersized is set (if needed) + if( IsInFly() || IsInSct() ) + { + SwTwips nTmpBottom = GetUpper()->getFrameArea().Top() + + GetUpper()->getFramePrintArea().Bottom(); + if( nTmpBottom < getFrameArea().Bottom() ) + break; + } + // Are there any free-flying frames on this page? + SwTextFly aTextFly( this ); + if( aTextFly.IsOn() ) + { + // Does any free-flying frame overlap? + if ( aTextFly.Relax() || IsUndersized() ) + break; + } + if (GetTextNodeForParaProps()->GetSwAttrSet().GetRegister().GetValue()) + break; + + SwTextGridItem const*const pGrid(GetGridItem(FindPageFrame())); + if (pGrid && GetTextNodeForParaProps()->GetSwAttrSet().GetParaGrid().GetValue()) + break; + + // i#28701 - consider anchored objects + if ( GetDrawObjs() ) + break; + + return bParaPossiblyInvalid; + } + default: + break; + } + } + + // Split fly anchors are technically empty (have no SwParaPortion), but otherwise behave like + // other split text frames, which are non-empty. + bool bSplitFlyAnchor = GetOffset() == TextFrameIndex(0) && HasFollow() + && GetFollow()->GetOffset() == TextFrameIndex(0); + + if( !HasPara() && !bSplitFlyAnchor && PrepareHint::MustFit != ePrep ) + { + SetInvalidVert( true ); // Test + OSL_ENSURE( !IsLocked(), "SwTextFrame::Prepare: three of a perfect pair" ); + if ( bNotify ) + InvalidateSize(); + else + InvalidateSize_(); + return bParaPossiblyInvalid; + } + + // Get object from cache while locking + SwTextLineAccess aAccess( this ); + SwParaPortion *pPara = aAccess.GetPara(); + + switch( ePrep ) + { + case PrepareHint::FootnoteMove : + { + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aFrm.Height(0); + } + + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + aPrt.Height(0); + } + + InvalidatePrt_(); + InvalidateSize_(); + [[fallthrough]]; + case PrepareHint::AdjustSizeWithoutFormatting : + pPara->SetPrepAdjust(); + if( IsFootnoteNumFrame() != pPara->IsFootnoteNum() || + IsUndersized() ) + { + InvalidateRange(SwCharRange(TextFrameIndex(0), TextFrameIndex(1)), 1); + if( GetOffset() && !IsFollow() ) + SetOffset_(TextFrameIndex(0)); + } + break; + case PrepareHint::MustFit : + pPara->SetPrepMustFit(true); + [[fallthrough]]; + case PrepareHint::WidowsOrphans : + pPara->SetPrepAdjust(); + break; + case PrepareHint::Widows : + // MustFit is stronger than anything else + if( pPara->IsPrepMustFit() ) + return bParaPossiblyInvalid; + // see comment in WidowsAndOrphans::FindOrphans and CalcPreps() + PrepWidows( *static_cast<const sal_uInt16 *>(pVoid), bNotify ); + break; + + case PrepareHint::FootnoteInvalidation : + { + SwTextFootnote const *pFootnote = static_cast<SwTextFootnote const *>(pVoid); + if( IsInFootnote() ) + { + // Am I the first TextFrame of a footnote? + if( !GetPrev() ) + // So we're a TextFrame of the footnote, which has + // to display the footnote number or the ErgoSum text + InvalidateRange(SwCharRange(TextFrameIndex(0), TextFrameIndex(1)), 1); + + if( !GetNext() ) + { + // We're the last Footnote; we need to update the + // QuoVadis texts now + const SwFootnoteInfo &rFootnoteInfo = GetDoc().GetFootnoteInfo(); + if( !pPara->UpdateQuoVadis( rFootnoteInfo.m_aQuoVadis ) ) + { + TextFrameIndex nPos = pPara->GetParLen(); + if( nPos ) + --nPos; + InvalidateRange( SwCharRange(nPos, TextFrameIndex(1)), 1); + } + } + } + else + { + // We are the TextFrame _with_ the footnote + TextFrameIndex const nPos = MapModelToView( + &pFootnote->GetTextNode(), pFootnote->GetStart()); + InvalidateRange(SwCharRange(nPos, TextFrameIndex(1)), 1); + } + break; + } + case PrepareHint::BossChanged : + { + // Test + { + SetInvalidVert( false ); + bool bOld = IsVertical(); + SetInvalidVert( true ); + if( bOld != IsVertical() ) + InvalidateRange(SwCharRange(GetOffset(), TextFrameIndex(COMPLETE_STRING))); + } + + if( HasFollow() ) + { + TextFrameIndex nNxtOfst = GetFollow()->GetOffset(); + if( nNxtOfst ) + --nNxtOfst; + InvalidateRange(SwCharRange( nNxtOfst, TextFrameIndex(1)), 1); + } + if( IsInFootnote() ) + { + TextFrameIndex nPos; + if( lcl_ErgoVadis( this, nPos, PrepareHint::QuoVadis ) ) + InvalidateRange( SwCharRange( nPos, TextFrameIndex(1)) ); + if( lcl_ErgoVadis( this, nPos, PrepareHint::ErgoSum ) ) + InvalidateRange( SwCharRange( nPos, TextFrameIndex(1)) ); + } + // If we have a page number field, we must invalidate those spots + SwTextNode const* pNode(nullptr); + sw::MergedAttrIter iter(*this); + TextFrameIndex const nEnd = GetFollow() + ? GetFollow()->GetOffset() : TextFrameIndex(COMPLETE_STRING); + for (SwTextAttr const* pHt = iter.NextAttr(&pNode); pHt; pHt = iter.NextAttr(&pNode)) + { + TextFrameIndex const nStart(MapModelToView(pNode, pHt->GetStart())); + if (nStart >= GetOffset()) + { + if (nStart >= nEnd) + break; + + // If we're flowing back and own a Footnote, the Footnote also flows + // with us. So that it doesn't obstruct us, we send ourselves + // an ADJUST_FRM. + // pVoid != 0 means MoveBwd() + const sal_uInt16 nWhich = pHt->Which(); + if (RES_TXTATR_FIELD == nWhich || + (HasFootnote() && pVoid && RES_TXTATR_FTN == nWhich)) + InvalidateRange(SwCharRange(nStart, TextFrameIndex(1)), 1); + } + } + // A new boss, a new chance for growing + if( IsUndersized() ) + { + InvalidateSize_(); + InvalidateRange(SwCharRange(GetOffset(), TextFrameIndex(1)), 1); + } + break; + } + + case PrepareHint::FramePositionChanged : + { + if ( isFramePrintAreaValid() ) + { + SwTextGridItem const*const pGrid(GetGridItem(FindPageFrame())); + if (pGrid && GetTextNodeForParaProps()->GetSwAttrSet().GetParaGrid().GetValue()) + InvalidatePrt(); + } + + // If we don't overlap with anybody: + // did any free-flying frame overlapped _before_ the position change? + bool bFormat = pPara->HasFly(); + if( !bFormat ) + { + if( IsInFly() ) + { + SwTwips nTmpBottom = GetUpper()->getFrameArea().Top() + + GetUpper()->getFramePrintArea().Bottom(); + if( nTmpBottom < getFrameArea().Bottom() ) + bFormat = true; + } + if( !bFormat ) + { + if ( GetDrawObjs() ) + { + const size_t nCnt = GetDrawObjs()->size(); + for ( size_t i = 0; i < nCnt; ++i ) + { + SwAnchoredObject* pAnchoredObj = (*GetDrawObjs())[i]; + // i#28701 - consider all + // to-character anchored objects + if ( pAnchoredObj->GetFrameFormat().GetAnchor().GetAnchorId() + == RndStdIds::FLY_AT_CHAR ) + { + bFormat = true; + break; + } + } + } + if( !bFormat ) + { + // Are there any free-flying frames on this page? + SwTextFly aTextFly( this ); + if( aTextFly.IsOn() ) + { + // Does any free-flying frame overlap? + const bool bRelaxed = aTextFly.Relax(); + bFormat = bRelaxed || IsUndersized(); + if (bRelaxed) + { + // It's possible that pPara was deleted above; retrieve it again + pPara = aAccess.GetPara(); + } + } + } + } + } + + if( bFormat ) + { + if( !IsLocked() ) + { + if( pPara->GetRepaint().HasArea() ) + SetCompletePaint(); + Init(); + pPara = nullptr; + InvalidateSize_(); + } + } + else + { + if (GetTextNodeForParaProps()->GetSwAttrSet().GetRegister().GetValue()) + bParaPossiblyInvalid = Prepare( PrepareHint::Register, nullptr, bNotify ); + // The Frames need to be readjusted, which caused by changes + // in position + else if( HasFootnote() ) + { + bParaPossiblyInvalid = Prepare( PrepareHint::AdjustSizeWithoutFormatting, nullptr, bNotify ); + InvalidateSize_(); + } + else + return bParaPossiblyInvalid; // So that there's no SetPrep() + + if (bParaPossiblyInvalid) + { + // It's possible that pPara was deleted above; retrieve it again + pPara = aAccess.GetPara(); + } + + } + break; + } + case PrepareHint::Register: + if (GetTextNodeForParaProps()->GetSwAttrSet().GetRegister().GetValue()) + { + pPara->SetPrepAdjust(); + CalcLineSpace(); + + // It's possible that pPara was deleted above; retrieve it again + bParaPossiblyInvalid = true; + pPara = aAccess.GetPara(); + + InvalidateSize(); + InvalidatePrt_(); + SwFrame* pNxt = GetIndNext(); + if ( nullptr != pNxt ) + { + pNxt->InvalidatePrt_(); + if ( pNxt->IsLayoutFrame() ) + pNxt->InvalidatePage(); + } + SetCompletePaint(); + } + break; + case PrepareHint::FootnoteInvalidationGone : + { + // If a Follow is calling us, because a footnote is being deleted, our last + // line has to be formatted, so that the first line of the Follow can flow up. + // Which had flowed to the next page to be together with the footnote (this is + // especially true for areas with columns) + OSL_ENSURE( GetFollow(), "PrepareHint::FootnoteInvalidationGone may only be called by Follow" ); + TextFrameIndex nPos = GetFollow()->GetOffset(); + if( IsFollow() && GetOffset() == nPos ) // If we don't have a mass of text, we call our + FindMaster()->Prepare( PrepareHint::FootnoteInvalidationGone ); // Master's Prepare + if( nPos ) + --nPos; // The char preceding our Follow + InvalidateRange(SwCharRange(nPos, TextFrameIndex(1))); + return bParaPossiblyInvalid; + } + case PrepareHint::ErgoSum: + case PrepareHint::QuoVadis: + { + TextFrameIndex nPos; + if( lcl_ErgoVadis( this, nPos, ePrep ) ) + InvalidateRange(SwCharRange(nPos, TextFrameIndex(1))); + } + break; + case PrepareHint::FlyFrameAttributesChanged: + { + if( pVoid ) + { + TextFrameIndex const nWhere = CalcFlyPos( static_cast<SwFrameFormat const *>(pVoid) ); + OSL_ENSURE( TextFrameIndex(COMPLETE_STRING) != nWhere, "Prepare: Why me?" ); + InvalidateRange(SwCharRange(nWhere, TextFrameIndex(1))); + return bParaPossiblyInvalid; + } + [[fallthrough]]; // else: continue with default case block + } + case PrepareHint::Clear: + default: + { + if( IsLocked() ) + { + if( PrepareHint::FlyFrameArrive == ePrep || PrepareHint::FlyFrameLeave == ePrep ) + { + TextFrameIndex const nLen = (GetFollow() + ? GetFollow()->GetOffset() + : TextFrameIndex(COMPLETE_STRING)) + - GetOffset(); + InvalidateRange( SwCharRange( GetOffset(), nLen ) ); + } + } + else + { + if( pPara->GetRepaint().HasArea() ) + SetCompletePaint(); + Init(); + pPara = nullptr; + if( GetOffset() && !IsFollow() ) + SetOffset_( TextFrameIndex(0) ); + if ( bNotify ) + InvalidateSize(); + else + InvalidateSize_(); + } + return bParaPossiblyInvalid; // no SetPrep() happened + } + } + if( pPara ) + { + pPara->SetPrep(); + } + + return bParaPossiblyInvalid; +} +#if defined __GNUC__ && !defined __clang__ +# pragma GCC diagnostic pop +#endif + +/** + * Small Helper class: + * Prepares a test format. + * The frame is changed in size and position, its SwParaPortion is moved aside + * and a new one is created. + * To achieve this, run formatting with bTestFormat flag set. + * In the destructor the TextFrame is reset to its original state. + */ +class SwTestFormat +{ + SwTextFrame *pFrame; + SwParaPortion *pOldPara; + SwRect aOldFrame, aOldPrt; +public: + SwTestFormat( SwTextFrame* pTextFrame, const SwFrame* pPrv, SwTwips nMaxHeight ); + ~SwTestFormat(); +}; + +SwTestFormat::SwTestFormat( SwTextFrame* pTextFrame, const SwFrame* pPre, SwTwips nMaxHeight ) + : pFrame( pTextFrame ) +{ + aOldFrame = pFrame->getFrameArea(); + aOldPrt = pFrame->getFramePrintArea(); + + SwRectFnSet aRectFnSet(pFrame); + SwTwips nLower = aRectFnSet.GetBottomMargin(*pFrame); + + { + // indeed, here the GetUpper()->getFramePrintArea() gets copied and manipulated + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*pFrame); + aFrm.setSwRect(pFrame->GetUpper()->getFramePrintArea()); + aFrm += pFrame->GetUpper()->getFrameArea().Pos(); + aRectFnSet.SetHeight( aFrm, nMaxHeight ); + + if( pFrame->GetPrev() ) + { + aRectFnSet.SetPosY( + aFrm, + aRectFnSet.GetBottom(pFrame->GetPrev()->getFrameArea()) - ( aRectFnSet.IsVert() ? nMaxHeight + 1 : 0 ) ); + } + } + + SwBorderAttrAccess aAccess( SwFrame::GetCache(), pFrame ); + const SwBorderAttrs &rAttrs = *aAccess.Get(); + + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*pFrame); + aRectFnSet.SetPosX(aPrt, rAttrs.CalcLeft( pFrame ) ); + } + + if( pPre ) + { + SwTwips nUpper = pFrame->CalcUpperSpace( &rAttrs, pPre ); + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*pFrame); + aRectFnSet.SetPosY(aPrt, nUpper ); + } + + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*pFrame); + aRectFnSet.SetHeight( aPrt, std::max( tools::Long(0) , aRectFnSet.GetHeight(pFrame->getFrameArea()) - aRectFnSet.GetTop(aPrt) - nLower ) ); + aRectFnSet.SetWidth( aPrt, aRectFnSet.GetWidth(pFrame->getFrameArea()) - ( rAttrs.CalcLeft( pFrame ) + rAttrs.CalcRight( pFrame ) ) ); + } + + pOldPara = pFrame->HasPara() ? pFrame->GetPara() : nullptr; + pFrame->SetPara( new SwParaPortion(), false ); + OSL_ENSURE( ! pFrame->IsSwapped(), "A frame is swapped before Format_" ); + + if ( pFrame->IsVertical() ) + pFrame->SwapWidthAndHeight(); + + SwTextFormatInfo aInf( pFrame->getRootFrame()->GetCurrShell()->GetOut(), pFrame, false, true, true ); + SwTextFormatter aLine( pFrame, &aInf ); + + pFrame->Format_( aLine, aInf ); + + if ( pFrame->IsVertical() ) + pFrame->SwapWidthAndHeight(); + + OSL_ENSURE( ! pFrame->IsSwapped(), "A frame is swapped after Format_" ); +} + +SwTestFormat::~SwTestFormat() +{ + { + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*pFrame); + aFrm.setSwRect(aOldFrame); + } + + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*pFrame); + aPrt.setSwRect(aOldPrt); + } + + pFrame->SetPara( pOldPara ); +} + +bool SwTextFrame::TestFormat( const SwFrame* pPrv, SwTwips &rMaxHeight, bool &bSplit ) +{ + PROTOCOL_ENTER( this, PROT::TestFormat, DbgAction::NONE, nullptr ) + + if( IsLocked() && GetUpper()->getFramePrintArea().Width() <= 0 ) + return false; + + SwTestFormat aSave( this, pPrv, rMaxHeight ); + + return SwTextFrame::WouldFit(rMaxHeight, bSplit, true, false); +} + +/** + * We should not and don't need to reformat. + * We assume that we already formatted and that the formatting + * data is still current. + * + * We also assume that the frame width of the Master and Follow + * are the same. That's why we're not calling FindBreak() for + * FindOrphans(). + * The required height is coming from nMaxHeight. + * + * @returns true if I can split + */ +bool SwTextFrame::WouldFit(SwTwips &rMaxHeight, bool &bSplit, bool bTst, bool bMoveBwd) +{ + OSL_ENSURE( ! IsVertical() || ! IsSwapped(), + "SwTextFrame::WouldFit with swapped frame" ); + SwRectFnSet aRectFnSet(this); + + if( IsLocked() ) + return false; + + // it can happen that the IdleCollector removed the cached information + if( !IsEmpty() ) + GetFormatted(); + + // i#27801 - correction: 'short cut' for empty paragraph + // can *not* be applied, if test format is in progress. The test format doesn't + // adjust the frame and the printing area - see method <SwTextFrame::Format_(..)>, + // which is called in <SwTextFrame::TestFormat(..)> + if ( IsEmpty() && !bTst ) + { + bSplit = false; + SwTwips nHeight = aRectFnSet.IsVert() ? getFramePrintArea().SSize().Width() : getFramePrintArea().SSize().Height(); + if( rMaxHeight < nHeight ) + return false; + else + { + rMaxHeight -= nHeight; + return true; + } + } + + // GetPara can still be 0 in edge cases + // We return true in order to be reformatted on the new Page + OSL_ENSURE( HasPara() || IsHiddenNow(), "WouldFit: GetFormatted() and then !HasPara()" ); + if( !HasPara() || ( !aRectFnSet.GetHeight(getFrameArea()) && IsHiddenNow() ) ) + return true; + + // Because the Orphan flag only exists for a short moment, we also check + // whether the Framesize is set to very huge by CalcPreps, in order to + // force a MoveFwd + if (IsWidow() || (aRectFnSet.IsVert() + ? (0 == getFrameArea().Left()) + : (sw::WIDOW_MAGIC - 20000 < getFrameArea().Bottom()))) + { + SetWidow(false); + if ( GetFollow() ) + { + // If we've ended up here due to a Widow request by our Follow, we check + // whether there's a Follow with a real height at all. + // Else (e.g. for newly created SctFrames) we ignore the IsWidow() and + // still check if we can find enough room + if (((!aRectFnSet.IsVert() && getFrameArea().Bottom() <= sw::WIDOW_MAGIC - 20000) || + ( aRectFnSet.IsVert() && 0 < getFrameArea().Left() ) ) && + ( GetFollow()->IsVertical() ? + !GetFollow()->getFrameArea().Width() : + !GetFollow()->getFrameArea().Height() ) ) + { + SwTextFrame* pFoll = GetFollow()->GetFollow(); + while( pFoll && + ( pFoll->IsVertical() ? + !pFoll->getFrameArea().Width() : + !pFoll->getFrameArea().Height() ) ) + pFoll = pFoll->GetFollow(); + if( pFoll ) + return false; + } + else + return false; + } + } + + SwSwapIfNotSwapped swap( this ); + + SwTextSizeInfo aInf( this ); + SwTextMargin aLine( this, &aInf ); + + WidowsAndOrphans aFrameBreak( this, rMaxHeight, bSplit ); + + bool bRet = true; + + aLine.Bottom(); + // is breaking necessary? + bSplit = !aFrameBreak.IsInside( aLine ); + if ( bSplit ) + bRet = !aFrameBreak.IsKeepAlways() && aFrameBreak.WouldFit(aLine, rMaxHeight, bTst, bMoveBwd); + else + { + // we need the total height including the current line + aLine.Top(); + do + { + rMaxHeight -= aLine.GetLineHeight(); + } while ( aLine.Next() ); + } + + return bRet; +} + +SwTwips SwTextFrame::GetParHeight() const +{ + OSL_ENSURE( ! IsVertical() || ! IsSwapped(), + "SwTextFrame::GetParHeight with swapped frame" ); + + if( !HasPara() ) + { // For non-empty paragraphs this is a special case + // For UnderSized we can simply just ask 1 Twip more + sal_uInt16 nRet = o3tl::narrowing<sal_uInt16>(getFramePrintArea().SSize().Height()); + if( IsUndersized() ) + { + if( IsEmpty() || GetText().isEmpty() ) + nRet = o3tl::narrowing<sal_uInt16>(EmptyHeight()); + else + ++nRet; + } + return nRet; + } + + // TODO: Refactor and improve code + const SwLineLayout* pLineLayout = GetPara(); + SwTwips nHeight = pLineLayout ? pLineLayout->GetRealHeight() : 0; + + // Is this paragraph scrolled? Our height until now is at least + // one line height too low then + if( GetOffset() && !IsFollow() ) + nHeight *= 2; + + while ( pLineLayout && pLineLayout->GetNext() ) + { + pLineLayout = pLineLayout->GetNext(); + nHeight = nHeight + pLineLayout->GetRealHeight(); + } + + return nHeight; +} + +/** + * @returns this _always_ in the formatted state! + */ +SwTextFrame* SwTextFrame::GetFormatted( bool bForceQuickFormat ) +{ + vcl::RenderContext* pRenderContext = getRootFrame()->GetCurrShell()->GetOut(); + SwSwapIfSwapped swap( this ); + + // In case the SwLineLayout was cleared out of the s_pTextCache, recreate it + // Not for empty paragraphs + if( !HasPara() && !(isFrameAreaDefinitionValid() && IsEmpty()) ) + { + // Calc() must be called, because frame position can be wrong + const bool bFormat = isFrameAreaSizeValid(); + Calc(pRenderContext); // calls Format() if invalid + + // If the flags were valid (hence bFormat=true), Calc did nothing, + // so Format() must be called manually in order to recreate + // the SwLineLayout that has been deleted from the + // SwTextFrame::s_pTextCache (hence !HasPara() above). + // Optimization with FormatQuick() + if( bFormat && !FormatQuick( bForceQuickFormat ) ) + Format(getRootFrame()->GetCurrShell()->GetOut()); + } + + return this; +} + +SwTwips SwTextFrame::CalcFitToContent() +{ + // i#31490 + // If we are currently locked, we better return with a + // fairly reasonable value: + if ( IsLocked() ) + return getFramePrintArea().Width(); + + SwParaPortion* pOldPara = GetPara(); + SwParaPortion *pDummy = new SwParaPortion(); + SetPara( pDummy, false ); + const SwPageFrame* pPage = FindPageFrame(); + + const Point aOldFramePos = getFrameArea().Pos(); + const SwTwips nOldFrameWidth = getFrameArea().Width(); + const SwTwips nOldPrtWidth = getFramePrintArea().Width(); + const SwTwips nPageWidth = GetUpper()->IsVertical() ? + pPage->getFramePrintArea().Height() : + pPage->getFramePrintArea().Width(); + + { + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aFrm.Width( nPageWidth ); + } + + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + aPrt.Width( nPageWidth ); + } + + // i#25422 objects anchored as character in RTL + if ( IsRightToLeft() ) + { + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aFrm.Pos().AdjustX(nOldFrameWidth - nPageWidth ); + } + + TextFrameLockGuard aLock( this ); + + SwTextFormatInfo aInf( getRootFrame()->GetCurrShell()->GetOut(), this, false, true, true ); + aInf.SetIgnoreFly( true ); + SwTextFormatter aLine( this, &aInf ); + SwHookOut aHook( aInf ); + + // i#54031 - assure minimum of MINLAY twips. + const SwTwips nMax = std::max( SwTwips(MINLAY), aLine.CalcFitToContent_() + 1 ); + + { + SwFrameAreaDefinition::FrameAreaWriteAccess aFrm(*this); + aFrm.Width( nOldFrameWidth ); + + // i#25422 objects anchored as character in RTL + if ( IsRightToLeft() ) + { + aFrm.Pos() = aOldFramePos; + } + } + + { + SwFrameAreaDefinition::FramePrintAreaWriteAccess aPrt(*this); + aPrt.Width( nOldPrtWidth ); + } + + SetPara( pOldPara ); + + return nMax; +} + +/** + * Simulate format for a list item paragraph, whose list level attributes + * are in LABEL_ALIGNMENT mode, in order to determine additional first + * line offset for the real text formatting due to the value of label + * adjustment attribute of the list level. + */ +void SwTextFrame::CalcAdditionalFirstLineOffset() +{ + if ( IsLocked() ) + return; + + // reset additional first line offset + mnAdditionalFirstLineOffset = 0; + + const SwTextNode* pTextNode( GetTextNodeForParaProps() ); + // sw_redlinehide: check that pParaPropsNode is the correct one + assert(pTextNode->IsNumbered(getRootFrame()) == pTextNode->IsNumbered(nullptr)); + if (!(pTextNode->IsNumbered(getRootFrame()) && + pTextNode->IsCountedInList() && pTextNode->GetNumRule())) + return; + + int nListLevel = pTextNode->GetActualListLevel(); + + if (nListLevel < 0) + nListLevel = 0; + + if (nListLevel >= MAXLEVEL) + nListLevel = MAXLEVEL - 1; + + const SwNumFormat& rNumFormat = + pTextNode->GetNumRule()->Get( o3tl::narrowing<sal_uInt16>(nListLevel) ); + if ( rNumFormat.GetPositionAndSpaceMode() != SvxNumberFormat::LABEL_ALIGNMENT ) + return; + + // keep current paragraph portion and apply dummy paragraph portion + SwParaPortion* pOldPara = GetPara(); + SwParaPortion *pDummy = new SwParaPortion(); + SetPara( pDummy, false ); + + // lock paragraph + TextFrameLockGuard aLock( this ); + + // simulate text formatting + SwTextFormatInfo aInf( getRootFrame()->GetCurrShell()->GetOut(), this, false, true, true ); + aInf.SetIgnoreFly( true ); + SwTextFormatter aLine( this, &aInf ); + SwHookOut aHook( aInf ); + aLine.CalcFitToContent_(); + + // determine additional first line offset + const SwLinePortion* pFirstPortion = aLine.GetCurr()->GetFirstPortion(); + if ( pFirstPortion->InNumberGrp() && !pFirstPortion->IsFootnoteNumPortion() ) + { + SwTwips nNumberPortionWidth( pFirstPortion->Width() ); + + const SwLinePortion* pPortion = pFirstPortion->GetNextPortion(); + while ( pPortion && + pPortion->InNumberGrp() && !pPortion->IsFootnoteNumPortion()) + { + nNumberPortionWidth += pPortion->Width(); + pPortion = pPortion->GetNextPortion(); + } + + if ( ( IsRightToLeft() && + rNumFormat.GetNumAdjust() == SvxAdjust::Left ) || + ( !IsRightToLeft() && + rNumFormat.GetNumAdjust() == SvxAdjust::Right ) ) + { + mnAdditionalFirstLineOffset = -nNumberPortionWidth; + } + else if ( rNumFormat.GetNumAdjust() == SvxAdjust::Center ) + { + mnAdditionalFirstLineOffset = -(nNumberPortionWidth/2); + } + } + + // restore paragraph portion + SetPara( pOldPara ); +} + +/** + * Determine the height of the last line for the calculation of + * the proportional line spacing + * + * Height of last line will be stored in new member + * mnHeightOfLastLine and can be accessed via method + * GetHeightOfLastLine() + * + * @param _bUseFont force the usage of the former algorithm to + * determine the height of the last line, which + * uses the font + */ +void SwTextFrame::CalcHeightOfLastLine( const bool _bUseFont ) +{ + // i#71281 + // Invalidate printing area, if height of last line changes + const SwTwips nOldHeightOfLastLine( mnHeightOfLastLine ); + + // determine output device + SwViewShell* pVsh = getRootFrame()->GetCurrShell(); + OSL_ENSURE( pVsh, "<SwTextFrame::_GetHeightOfLastLineForPropLineSpacing()> - no SwViewShell" ); + + // i#78921 + // There could be no <SwViewShell> instance in the case of loading a binary + // StarOffice file format containing an embedded Writer document. + if ( !pVsh ) + { + return; + } + OutputDevice* pOut = pVsh->GetOut(); + const IDocumentSettingAccess *const pIDSA = &GetDoc().getIDocumentSettingAccess(); + if ( !pVsh->GetViewOptions()->getBrowseMode() || + pVsh->GetViewOptions()->IsPrtFormat() ) + { + pOut = GetDoc().getIDocumentDeviceAccess().getReferenceDevice( true ); + } + OSL_ENSURE( pOut, "<SwTextFrame::_GetHeightOfLastLineForPropLineSpacing()> - no OutputDevice" ); + + if ( !pOut ) + { + return; + } + + // determine height of last line + if ( _bUseFont || pIDSA->get(DocumentSettingId::OLD_LINE_SPACING ) ) + { + // former determination of last line height for proportional line + // spacing - take height of font set at the paragraph + // FIXME actually... must the font match across all nodes? + SwFont aFont( &GetTextNodeForParaProps()->GetSwAttrSet(), pIDSA ); + + // we must ensure that the font is restored correctly on the OutputDevice + // otherwise Last!=Owner could occur + if ( pLastFont ) + { + SwFntObj *pOldFont = pLastFont; + pLastFont = nullptr; + aFont.SetFntChg( true ); + aFont.ChgPhysFnt( pVsh, *pOut ); + mnHeightOfLastLine = aFont.GetHeight( pVsh, *pOut ); + assert(pLastFont && "coverity[var_deref_model] - pLastFont should be set in SwSubFont::ChgFnt"); + pLastFont->Unlock(); + pLastFont = pOldFont; + pLastFont->SetDevFont( pVsh, *pOut ); + } + else + { + vcl::Font aOldFont = pOut->GetFont(); + aFont.SetFntChg( true ); + aFont.ChgPhysFnt( pVsh, *pOut ); + mnHeightOfLastLine = aFont.GetHeight( pVsh, *pOut ); + assert(pLastFont && "coverity[var_deref_model] - pLastFont should be set in SwSubFont::ChgFnt"); + pLastFont->Unlock(); + pLastFont = nullptr; + pOut->SetFont( aOldFont ); + } + } + else + { + // new determination of last line height - take actually height of last line + // i#89000 + // assure same results, if paragraph is undersized + if ( IsUndersized() ) + { + mnHeightOfLastLine = 0; + } + else + { + bool bCalcHeightOfLastLine = true; + if ( ( !HasPara() && IsEmpty( ) ) || GetText().isEmpty() ) + { + mnHeightOfLastLine = EmptyHeight(); + bCalcHeightOfLastLine = false; + } + + if ( bCalcHeightOfLastLine ) + { + OSL_ENSURE( HasPara(), + "<SwTextFrame::CalcHeightOfLastLine()> - missing paragraph portions." ); + const SwLineLayout* pLineLayout = GetPara(); + while ( pLineLayout && pLineLayout->GetNext() ) + { + // iteration to last line + pLineLayout = pLineLayout->GetNext(); + } + if ( pLineLayout ) + { + SwTwips nAscent, nDescent, nDummy1, nDummy2; + // i#47162 - suppress consideration of + // fly content portions and the line portion. + pLineLayout->MaxAscentDescent( nAscent, nDescent, + nDummy1, nDummy2, + nullptr, true ); + // i#71281 + // Suppress wrong invalidation of printing area, if method is + // called recursive. + // Thus, member <mnHeightOfLastLine> is only set directly, if + // no recursive call is needed. + const SwTwips nNewHeightOfLastLine = nAscent + nDescent; + // i#47162 - if last line only contains + // fly content portions, <mnHeightOfLastLine> is zero. + // In this case determine height of last line by the font + if ( nNewHeightOfLastLine == 0 ) + { + CalcHeightOfLastLine( true ); + } + else + { + mnHeightOfLastLine = nNewHeightOfLastLine; + } + } + } + } + } + // i#71281 + // invalidate printing area, if height of last line changes + if ( mnHeightOfLastLine != nOldHeightOfLastLine ) + { + InvalidatePrt(); + } +} + +/** + * Method returns the value of the inter line spacing for a text frame. + * Such a value exists for proportional line spacings ("1,5 Lines", + * "Double", "Proportional" and for leading line spacing ("Leading"). + * + * @param _bNoPropLineSpacing (default = false) control whether the + * value of a proportional line spacing is + * returned or not + */ +tools::Long SwTextFrame::GetLineSpace( const bool _bNoPropLineSpace ) const +{ + tools::Long nRet = 0; + + const SvxLineSpacingItem &rSpace = GetTextNodeForParaProps()->GetSwAttrSet().GetLineSpacing(); + + switch( rSpace.GetInterLineSpaceRule() ) + { + case SvxInterLineSpaceRule::Prop: + { + if ( _bNoPropLineSpace ) + { + break; + } + + // i#11860 - adjust spacing implementation for object positioning + // - compatibility to MS Word + nRet = GetHeightOfLastLine(); + + tools::Long nTmp = nRet; + nTmp *= rSpace.GetPropLineSpace(); + nTmp /= 100; + nTmp -= nRet; + if ( nTmp > 0 ) + nRet = nTmp; + else + nRet = 0; + } + break; + case SvxInterLineSpaceRule::Fix: + { + if ( rSpace.GetInterLineSpace() > 0 ) + nRet = rSpace.GetInterLineSpace(); + } + break; + default: + break; + } + return nRet; +} + +sal_uInt16 SwTextFrame::FirstLineHeight() const +{ + if ( !HasPara() ) + { + if( IsEmpty() && isFrameAreaDefinitionValid() ) + return IsVertical() ? o3tl::narrowing<sal_uInt16>(getFramePrintArea().Width()) : o3tl::narrowing<sal_uInt16>(getFramePrintArea().Height()); + return USHRT_MAX; + } + const SwParaPortion *pPara = GetPara(); + if ( !pPara ) + return USHRT_MAX; + + // tdf#146500 Lines with only fly overlap cannot be "moved", so the idea + // here is to continue until there's some text. + // FIXME ideally we want to count a fly to the line in which it is anchored + // - it may even be anchored in some other paragraph! SwFlyPortion doesn't + // have a pointer sadly so no way to find out. + sal_uInt16 nHeight(0); + for (SwLineLayout const* pLine = pPara; pLine; pLine = pLine->GetNext()) + { + nHeight += pLine->Height(); + if (::sw::FindNonFlyPortion(*pLine)) + { + break; + } + } + return nHeight; +} + +sal_Int32 SwTextFrame::GetLineCount(TextFrameIndex const nPos) +{ + sal_Int32 nRet = 0; + SwTextFrame *pFrame = this; + do + { + pFrame->GetFormatted(); + if( !pFrame->HasPara() ) + break; + SwTextSizeInfo aInf( pFrame ); + SwTextMargin aLine( pFrame, &aInf ); + if (TextFrameIndex(COMPLETE_STRING) == nPos) + aLine.Bottom(); + else + aLine.CharToLine( nPos ); + nRet = nRet + aLine.GetLineNr(); + pFrame = pFrame->GetFollow(); + } while ( pFrame && pFrame->GetOffset() <= nPos ); + return nRet; +} + +void SwTextFrame::ChgThisLines() +{ + // not necessary to format here (GetFormatted etc.), because we have to come from there! + sal_Int32 nNew = 0; + const SwLineNumberInfo &rInf = GetDoc().GetLineNumberInfo(); + if ( !GetText().isEmpty() && HasPara() ) + { + SwTextSizeInfo aInf( this ); + SwTextMargin aLine( this, &aInf ); + if ( rInf.IsCountBlankLines() ) + { + aLine.Bottom(); + nNew = aLine.GetLineNr(); + } + else + { + do + { + if( aLine.GetCurr()->HasContent() ) + ++nNew; + } while ( aLine.NextLine() ); + } + } + else if ( rInf.IsCountBlankLines() ) + nNew = 1; + + if ( nNew == mnThisLines ) + return; + + if (!IsInTab() && GetTextNodeForParaProps()->GetSwAttrSet().GetLineNumber().IsCount()) + { + mnAllLines -= mnThisLines; + mnThisLines = nNew; + mnAllLines += mnThisLines; + SwFrame *pNxt = GetNextContentFrame(); + while( pNxt && pNxt->IsInTab() ) + { + pNxt = pNxt->FindTabFrame(); + if( nullptr != pNxt ) + pNxt = pNxt->FindNextCnt(); + } + if( pNxt ) + pNxt->InvalidateLineNum(); + + // Extend repaint to the bottom. + if ( HasPara() ) + { + SwRepaint& rRepaint = GetPara()->GetRepaint(); + rRepaint.Bottom( std::max( rRepaint.Bottom(), + getFrameArea().Top()+getFramePrintArea().Bottom())); + } + } + else // Paragraphs which are not counted should not manipulate the AllLines. + mnThisLines = nNew; +} + +void SwTextFrame::RecalcAllLines() +{ + ValidateLineNum(); + + if ( IsInTab() ) + return; + + const sal_Int32 nOld = GetAllLines(); + const SwFormatLineNumber &rLineNum = GetTextNodeForParaProps()->GetSwAttrSet().GetLineNumber(); + sal_Int32 nNewNum; + const bool bRestart = GetDoc().GetLineNumberInfo().IsRestartEachPage(); + + if ( !IsFollow() && rLineNum.GetStartValue() && rLineNum.IsCount() ) + nNewNum = rLineNum.GetStartValue() - 1; + // If it is a follow or not has not be considered if it is a restart at each page; the + // restart should also take effect at follows. + else if ( bRestart && FindPageFrame()->FindFirstBodyContent() == this ) + { + nNewNum = 0; + } + else + { + SwContentFrame *pPrv = GetPrevContentFrame(); + while ( pPrv && + (pPrv->IsInTab() || pPrv->IsInDocBody() != IsInDocBody()) ) + pPrv = pPrv->GetPrevContentFrame(); + + // i#78254 Restart line numbering at page change + // First body content may be in table! + if ( bRestart && pPrv && pPrv->FindPageFrame() != FindPageFrame() ) + pPrv = nullptr; + + nNewNum = pPrv ? static_cast<SwTextFrame*>(pPrv)->GetAllLines() : 0; + } + if ( rLineNum.IsCount() ) + nNewNum += GetThisLines(); + + if ( nOld == nNewNum ) + return; + + mnAllLines = nNewNum; + SwContentFrame *pNxt = GetNextContentFrame(); + while ( pNxt && + (pNxt->IsInTab() || pNxt->IsInDocBody() != IsInDocBody()) ) + pNxt = pNxt->GetNextContentFrame(); + if ( pNxt ) + { + if ( pNxt->GetUpper() != GetUpper() ) + pNxt->InvalidateLineNum(); + else + pNxt->InvalidateLineNum_(); + } +} + +void SwTextFrame::VisitPortions( SwPortionHandler& rPH ) const +{ + const SwParaPortion* pPara = isFrameAreaDefinitionValid() ? GetPara() : nullptr; + + if (pPara) + { + if ( IsFollow() ) + rPH.Skip( GetOffset() ); + + const SwLineLayout* pLine = pPara; + while ( pLine ) + { + const SwLinePortion* pPor = pLine->GetFirstPortion(); + while ( pPor ) + { + pPor->HandlePortion( rPH ); + pPor = pPor->GetNextPortion(); + } + + rPH.LineBreak(); + pLine = pLine->GetNext(); + } + } + + rPH.Finish(); +} + +const SwScriptInfo* SwTextFrame::GetScriptInfo() const +{ + const SwParaPortion* pPara = GetPara(); + return pPara ? &pPara->GetScriptInfo() : nullptr; +} + +/** + * Helper function for SwTextFrame::CalcBasePosForFly() + */ +static SwTwips lcl_CalcFlyBasePos( const SwTextFrame& rFrame, SwRect aFlyRect, + SwTextFly const & rTextFly ) +{ + SwRectFnSet aRectFnSet(&rFrame); + SwTwips nRet = rFrame.IsRightToLeft() ? + aRectFnSet.GetRight(rFrame.getFrameArea()) : + aRectFnSet.GetLeft(rFrame.getFrameArea()); + + do + { + SwRect aRect = rTextFly.GetFrame( aFlyRect ); + if ( 0 != aRectFnSet.GetWidth(aRect) ) + { + if ( rFrame.IsRightToLeft() ) + { + if ( aRectFnSet.GetRight(aRect) - + aRectFnSet.GetRight(aFlyRect) >= 0 ) + { + aRectFnSet.SetRight( +aFlyRect, aRectFnSet.GetLeft(aRect) ); + nRet = aRectFnSet.GetLeft(aRect); + } + else + break; + } + else + { + if ( aRectFnSet.GetLeft(aFlyRect) - + aRectFnSet.GetLeft(aRect) >= 0 ) + { + aRectFnSet.SetLeft( +aFlyRect, aRectFnSet.GetRight(aRect) + 1 ); + nRet = aRectFnSet.GetRight(aRect); + } + else + break; + } + } + else + break; + } + while ( aRectFnSet.GetWidth(aFlyRect) > 0 ); + + return nRet; +} + +void SwTextFrame::CalcBaseOfstForFly() +{ + OSL_ENSURE( !IsVertical() || !IsSwapped(), + "SwTextFrame::CalcBasePosForFly with swapped frame!" ); + + if (!GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::ADD_FLY_OFFSETS)) + return; + + SwRectFnSet aRectFnSet(this); + + SwRect aFlyRect( getFrameArea().Pos() + getFramePrintArea().Pos(), getFramePrintArea().SSize() ); + + // Get first 'real' line and adjust position and height of line rectangle. + // Correct behaviour if no 'real' line exists + // (empty paragraph with and without a dummy portion) + SwTwips nFlyAnchorVertOfstNoWrap = 0; + { + SwTwips nTop = aRectFnSet.GetTop(aFlyRect); + const SwLineLayout* pLay = GetPara(); + SwTwips nLineHeight = 200; + while( pLay && pLay->IsDummy() && pLay->GetNext() ) + { + nTop += pLay->Height(); + nFlyAnchorVertOfstNoWrap += pLay->Height(); + pLay = pLay->GetNext(); + } + if ( pLay ) + { + nLineHeight = pLay->Height(); + } + aRectFnSet.SetTopAndHeight( aFlyRect, nTop, nLineHeight ); + } + + SwTextFly aTextFly( this ); + aTextFly.SetIgnoreCurrentFrame( true ); + aTextFly.SetIgnoreContour( true ); + // ignore objects in page header|footer for + // text frames not in page header|footer + aTextFly.SetIgnoreObjsInHeaderFooter( true ); + SwTwips nRet1 = lcl_CalcFlyBasePos( *this, aFlyRect, aTextFly ); + aTextFly.SetIgnoreCurrentFrame( false ); + SwTwips nRet2 = lcl_CalcFlyBasePos( *this, aFlyRect, aTextFly ); + + // make values relative to frame start position + SwTwips nLeft = IsRightToLeft() ? + aRectFnSet.GetRight(getFrameArea()) : + aRectFnSet.GetLeft(getFrameArea()); + + mnFlyAnchorOfst = nRet1 - nLeft; + mnFlyAnchorOfstNoWrap = nRet2 - nLeft; + + if (!GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::ADD_VERTICAL_FLY_OFFSETS)) + return; + + if (mnFlyAnchorOfstNoWrap > 0) + mnFlyAnchorVertOfstNoWrap = nFlyAnchorVertOfstNoWrap; +} + +SwTwips SwTextFrame::GetBaseVertOffsetForFly(bool bIgnoreFlysAnchoredAtThisFrame) const +{ + return bIgnoreFlysAnchoredAtThisFrame ? 0 : mnFlyAnchorVertOfstNoWrap; +} + +/** + * Repaint all text frames of the given text node + */ +void SwTextFrame::repaintTextFrames( const SwTextNode& rNode ) +{ + SwIterator<SwTextFrame, SwTextNode, sw::IteratorMode::UnwrapMulti> aIter(rNode); + for( const SwTextFrame *pFrame = aIter.First(); pFrame; pFrame = aIter.Next() ) + { + SwRect aRec( pFrame->GetPaintArea() ); + const SwRootFrame *pRootFrame = pFrame->getRootFrame(); + SwViewShell *pCurShell = pRootFrame ? pRootFrame->GetCurrShell() : nullptr; + if( pCurShell ) + pCurShell->InvalidateWindows( aRec ); + } +} + +void SwTextFrame::UpdateOutlineContentVisibilityButton(SwWrtShell* pWrtSh) const +{ + if (pWrtSh && pWrtSh->GetViewOptions()->IsShowOutlineContentVisibilityButton() && + GetTextNodeFirst()->IsOutline()) + { + SwEditWin& rEditWin = pWrtSh->GetView().GetEditWin(); + SwFrameControlsManager& rMngr = rEditWin.GetFrameControlsManager(); + rMngr.SetOutlineContentVisibilityButton(this); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtftn.cxx b/sw/source/core/text/txtftn.cxx new file mode 100644 index 0000000000..c1fa749c93 --- /dev/null +++ b/sw/source/core/text/txtftn.cxx @@ -0,0 +1,1590 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <string_view> + +#include <utility> +#include <viewsh.hxx> +#include <doc.hxx> +#include <IDocumentLayoutAccess.hxx> +#include <pagefrm.hxx> +#include <rootfrm.hxx> +#include <ndtxt.hxx> +#include <SwPortionHandler.hxx> +#include <txtftn.hxx> +#include <flyfrm.hxx> +#include <fmtftn.hxx> +#include <ftninfo.hxx> +#include <charfmt.hxx> +#include <rowfrm.hxx> +#include <editeng/brushitem.hxx> +#include <editeng/charrotateitem.hxx> +#include <tabfrm.hxx> +#include <sortedobjs.hxx> + +#include <swfont.hxx> +#include "porftn.hxx" +#include "porfly.hxx" +#include "porlay.hxx" +#include <txtfrm.hxx> +#include "itrform2.hxx" +#include <ftnfrm.hxx> +#include <pagedesc.hxx> +#include "redlnitr.hxx" +#include <sectfrm.hxx> +#include <layouter.hxx> +#include <frmtool.hxx> +#include <ndindex.hxx> +#include <IDocumentSettingAccess.hxx> +#include <IDocumentRedlineAccess.hxx> +#include <swmodule.hxx> +#include <unotextrange.hxx> +#include <redline.hxx> +#include <editeng/colritem.hxx> +#include <editeng/udlnitem.hxx> +#include <editeng/crossedoutitem.hxx> + +#include <com/sun/star/beans/XPropertySet.hpp> +#include <com/sun/star/awt/CharSet.hpp> +#include <com/sun/star/text/XTextRange.hpp> + +using namespace ::com::sun::star; + +bool SwTextFrame::IsFootnoteNumFrame_() const +{ + if (IsInTab()) + return false; // tdf#102073 first frame in cell doesn't have mpPrev set + const SwFootnoteFrame* pFootnote = FindFootnoteFrame()->GetMaster(); + while( pFootnote && !pFootnote->ContainsContent() ) + pFootnote = pFootnote->GetMaster(); + return !pFootnote; +} + +/** + * Looks for the TextFrame matching the SwTextFootnote within a master-follow chain + */ +SwTextFrame *SwTextFrame::FindFootnoteRef( const SwTextFootnote *pFootnote ) +{ + SwTextFrame *pFrame = this; + const bool bFwd = MapModelToView(&pFootnote->GetTextNode(), pFootnote->GetStart()) >= GetOffset(); + while( pFrame ) + { + if( SwFootnoteBossFrame::FindFootnote( pFrame, pFootnote ) ) + return pFrame; + pFrame = bFwd ? pFrame->GetFollow() : + pFrame->IsFollow() ? pFrame->FindMaster() : nullptr; + } + return pFrame; +} + +void SwTextFrame::SetHasRotatedPortions(bool bHasRotatedPortions) +{ + mbHasRotatedPortions = bHasRotatedPortions; +} + +#ifdef DBG_UTIL +void SwTextFrame::CalcFootnoteFlag(TextFrameIndex nStop) // For testing the SplitFrame +#else +void SwTextFrame::CalcFootnoteFlag() +#endif +{ + mbFootnote = false; + +#ifdef DBG_UTIL + const TextFrameIndex nEnd = nStop != TextFrameIndex(COMPLETE_STRING) + ? nStop + : GetFollow() ? GetFollow()->GetOffset() : TextFrameIndex(COMPLETE_STRING); +#else + const TextFrameIndex nEnd = GetFollow() + ? GetFollow()->GetOffset() + : TextFrameIndex(COMPLETE_STRING); +#endif + + SwTextNode const* pNode(nullptr); + sw::MergedAttrIter iter(*this); + for (SwTextAttr const* pHt = iter.NextAttr(&pNode); pHt; pHt = iter.NextAttr(&pNode)) + { + if ( pHt->Which() == RES_TXTATR_FTN ) + { + TextFrameIndex const nIdx(MapModelToView(pNode, pHt->GetStart())); + if ( nEnd < nIdx ) + break; + if( GetOffset() <= nIdx ) + { + mbFootnote = true; + break; + } + } + } +} + +bool SwTextFrame::CalcPrepFootnoteAdjust() +{ + OSL_ENSURE( HasFootnote(), "Who´s calling me?" ); + SwFootnoteBossFrame *pBoss = FindFootnoteBossFrame( true ); + const SwFootnoteFrame *pFootnote = pBoss->FindFirstFootnote( this ); + if (pFootnote && FTNPOS_CHAPTER != GetDoc().GetFootnoteInfo().m_ePos && + ( !pBoss->GetUpper()->IsSctFrame() || + !static_cast<SwSectionFrame*>(pBoss->GetUpper())->IsFootnoteAtEnd() ) ) + { + const SwFootnoteContFrame *pCont = pBoss->FindFootnoteCont(); + bool bReArrange = true; + + SwRectFnSet aRectFnSet(this); + if ( pCont && aRectFnSet.YDiff( aRectFnSet.GetTop(pCont->getFrameArea()), + aRectFnSet.GetBottom(getFrameArea()) ) > 0 ) + { + pBoss->RearrangeFootnotes( aRectFnSet.GetBottom(getFrameArea()), false, + pFootnote->GetAttr() ); + ValidateBodyFrame(); + ValidateFrame(); + pFootnote = pBoss->FindFirstFootnote( this ); + } + else + bReArrange = false; + if( !pCont || !pFootnote || bReArrange != (pFootnote->FindFootnoteBossFrame() == pBoss) ) + { + SwTextFormatInfo aInf( getRootFrame()->GetCurrShell()->GetOut(), this ); + SwTextFormatter aLine( this, &aInf ); + aLine.TruncLines(); + SetPara( nullptr ); // May be deleted! + ResetPreps(); + return false; + } + } + return true; +} + +/** + * Local helper function. Checks if nLower should be taken as the boundary + * for the footnote. + */ +static SwTwips lcl_GetFootnoteLower( const SwTextFrame* pFrame, SwTwips nLower ) +{ + // nLower is an absolute value. It denotes the bottom of the line + // containing the footnote. + SwRectFnSet aRectFnSet(pFrame); + + OSL_ENSURE( !pFrame->IsVertical() || !pFrame->IsSwapped(), + "lcl_GetFootnoteLower with swapped frame" ); + + SwTwips nAdd; + SwTwips nRet = nLower; + + // Check if text is inside a table. + if ( pFrame->IsInTab() ) + { + // If pFrame is inside a table, we have to check if + // a) The table is not allowed to split or + // b) The table row is not allowed to split + + // Inside a table, there are no footnotes, + // see SwFrame::FindFootnoteBossFrame. So we don't have to check + // the case that pFrame is inside a (footnote collecting) section + // within the table. + const SwFrame* pRow = pFrame; + while( !pRow->IsRowFrame() || !pRow->GetUpper()->IsTabFrame() ) + pRow = pRow->GetUpper(); + const SwTabFrame* pTabFrame = static_cast<const SwTabFrame*>(pRow->GetUpper()); + + OSL_ENSURE( pTabFrame && pRow && + pRow->GetUpper()->IsTabFrame(), "Upper of row should be tab" ); + + const bool bDontSplit = !pTabFrame->IsFollow() && + !pTabFrame->IsLayoutSplitAllowed(); + + SwTwips nMin = 0; + if ( bDontSplit ) + nMin = aRectFnSet.GetBottom(pTabFrame->getFrameArea()); + else if ( !static_cast<const SwRowFrame*>(pRow)->IsRowSplitAllowed() ) + nMin = aRectFnSet.GetBottom(pRow->getFrameArea()); + + if ( nMin && aRectFnSet.YDiff( nMin, nLower ) > 0 ) + nRet = nMin; + + nAdd = aRectFnSet.GetBottomMargin(*pRow->GetUpper()); + } + else + nAdd = aRectFnSet.GetBottomMargin(*pFrame); + + if( nAdd > 0 ) + { + if ( aRectFnSet.IsVert() ) + nRet -= nAdd; + else + nRet += nAdd; + } + + // #i10770#: If there are fly frames anchored at previous paragraphs, + // the deadline should consider their lower borders. + const SwFrame* pStartFrame = pFrame->GetUpper()->GetLower(); + OSL_ENSURE( pStartFrame, "Upper has no lower" ); + SwTwips nFlyLower = aRectFnSet.IsVert() ? LONG_MAX : 0; + while ( pStartFrame != pFrame ) + { + OSL_ENSURE( pStartFrame, "Frame chain is broken" ); + if ( pStartFrame->GetDrawObjs() ) + { + const SwSortedObjs &rObjs = *pStartFrame->GetDrawObjs(); + for (SwAnchoredObject* pAnchoredObj : rObjs) + { + SwRect aRect( pAnchoredObj->GetObjRect() ); + + auto pFlyFrame = pAnchoredObj->DynCastFlyFrame(); + if ( !pFlyFrame || + pFlyFrame->isFrameAreaDefinitionValid() ) + { + const SwTwips nBottom = aRectFnSet.GetBottom(aRect); + if ( aRectFnSet.YDiff( nBottom, nFlyLower ) > 0 ) + nFlyLower = nBottom; + } + } + } + + pStartFrame = pStartFrame->GetNext(); + } + + if ( aRectFnSet.IsVert() ) + nRet = std::min( nRet, nFlyLower ); + else + nRet = std::max( nRet, nFlyLower ); + + return nRet; +} + +SwTwips SwTextFrame::GetFootnoteLine( const SwTextFootnote *pFootnote ) const +{ + OSL_ENSURE( ! IsVertical() || ! IsSwapped(), + "SwTextFrame::GetFootnoteLine with swapped frame" ); + + SwTextFrame *pThis = const_cast<SwTextFrame*>(this); + + if( !HasPara() ) + { + // #109071# GetFormatted() does not work here, because most probably + // the frame is currently locked. We return the previous value. + return pThis->mnFootnoteLine > 0 ? + pThis->mnFootnoteLine : + IsVertical() ? getFrameArea().Left() : getFrameArea().Bottom(); + } + + SwTwips nRet; + { + SwSwapIfNotSwapped swap(const_cast<SwTextFrame *>(this)); + + SwTextInfo aInf( pThis ); + SwTextIter aLine( pThis, &aInf ); + TextFrameIndex const nPos(MapModelToView( + &pFootnote->GetTextNode(), pFootnote->GetStart())); + aLine.CharToLine( nPos ); + + nRet = aLine.Y() + aLine.GetLineHeight(); + if( IsVertical() ) + nRet = SwitchHorizontalToVertical( nRet ); + } + + nRet = lcl_GetFootnoteLower( pThis, nRet ); + + pThis->mnFootnoteLine = nRet; + return nRet; +} + +/** + * Calculates the maximum reachable height for the TextFrame in the Footnote Area. + * The cell's bottom margin with the Footnote Reference limit's this height. + */ +SwTwips SwTextFrame::GetFootnoteFrameHeight_() const +{ + OSL_ENSURE( !IsFollow() && IsInFootnote(), "SwTextFrame::SetFootnoteLine: moon walk" ); + + const SwFootnoteFrame *pFootnoteFrame = FindFootnoteFrame(); + const SwTextFrame *pRef = static_cast<const SwTextFrame *>(pFootnoteFrame->GetRef()); + const SwFootnoteBossFrame *pBoss = FindFootnoteBossFrame(); + if( pBoss != pRef->FindFootnoteBossFrame( !pFootnoteFrame->GetAttr()-> + GetFootnote().IsEndNote() ) ) + return 0; + + SwSwapIfSwapped swap(const_cast<SwTextFrame *>(this)); + + SwTwips nHeight = pRef->IsInFootnoteConnect() ? + 1 : pRef->GetFootnoteLine( pFootnoteFrame->GetAttr() ); + if( nHeight ) + { + // As odd as it may seem: the first Footnote on the page may not touch the + // Footnote Reference, when entering text in the Footnote Area. + const SwFrame *pCont = pFootnoteFrame->GetUpper(); + + // Height within the Container which we're allowed to consume anyways + SwRectFnSet aRectFnSet(pCont); + SwTwips nTmp = aRectFnSet.YDiff( aRectFnSet.GetPrtBottom(*pCont), + aRectFnSet.GetTop(getFrameArea()) ); + +#if OSL_DEBUG_LEVEL > 0 + if( nTmp < 0 ) + { + bool bInvalidPos = false; + const SwLayoutFrame* pTmp = GetUpper(); + while( !bInvalidPos && pTmp ) + { + bInvalidPos = !pTmp->isFrameAreaPositionValid() || + !pTmp->Lower()->isFrameAreaPositionValid(); + if( pTmp == pCont ) + break; + pTmp = pTmp->GetUpper(); + } + OSL_ENSURE( bInvalidPos, "Hanging below FootnoteCont" ); + } +#endif + + if ( aRectFnSet.YDiff( aRectFnSet.GetTop(pCont->getFrameArea()), nHeight) > 0 ) + { + // Growth potential of the container + if ( !pRef->IsInFootnoteConnect() ) + { + SwSaveFootnoteHeight aSave( const_cast<SwFootnoteBossFrame*>(pBoss), nHeight ); + nHeight = const_cast<SwFootnoteContFrame*>(static_cast<const SwFootnoteContFrame*>(pCont))->Grow( LONG_MAX, true ); + } + else + nHeight = const_cast<SwFootnoteContFrame*>(static_cast<const SwFootnoteContFrame*>(pCont))->Grow( LONG_MAX, true ); + + nHeight += nTmp; + if( nHeight < 0 ) + nHeight = 0; + } + else + { // The container has to shrink + nTmp += aRectFnSet.YDiff( aRectFnSet.GetTop(pCont->getFrameArea()), nHeight); + if( nTmp > 0 ) + nHeight = nTmp; + else + nHeight = 0; + } + } + + return nHeight; +} + +SwTextFrame *SwTextFrame::FindQuoVadisFrame() +{ + // Check whether we're in a FootnoteFrame + if( GetIndPrev() || !IsInFootnote() ) + return nullptr; + + // To the preceding FootnoteFrame + SwFootnoteFrame *pFootnoteFrame = FindFootnoteFrame()->GetMaster(); + if( !pFootnoteFrame ) + return nullptr; + + // Now the last Content + SwContentFrame *pCnt = pFootnoteFrame->ContainsContent(); + if( !pCnt ) + return nullptr; + SwContentFrame *pLast; + do + { pLast = pCnt; + pCnt = pCnt->GetNextContentFrame(); + } while( pCnt && pFootnoteFrame->IsAnLower( pCnt ) ); + return static_cast<SwTextFrame*>(pLast); +} + +void SwTextFrame::RemoveFootnote(TextFrameIndex const nStart, TextFrameIndex const nLen) +{ + if ( !IsFootnoteAllowed() ) + return; + + bool bRollBack = nLen != TextFrameIndex(COMPLETE_STRING); + TextFrameIndex nEnd; + SwTextFrame* pSource; + if( bRollBack ) + { + nEnd = nStart + nLen; + pSource = GetFollow(); + if( !pSource ) + return; + } + else + { + nEnd = TextFrameIndex(COMPLETE_STRING); + pSource = this; + } + + SwPageFrame* pUpdate = nullptr; + bool bRemove = false; + SwFootnoteBossFrame *pFootnoteBoss = nullptr; + SwFootnoteBossFrame *pEndBoss = nullptr; + bool bFootnoteEndDoc = FTNPOS_CHAPTER == GetDoc().GetFootnoteInfo().m_ePos; + SwTextNode const* pNode(nullptr); + sw::MergedAttrIterReverse iter(*this); + for (SwTextAttr const* pHt = iter.PrevAttr(&pNode); pHt; pHt = iter.PrevAttr(&pNode)) + { + if (RES_TXTATR_FTN != pHt->Which()) + continue; + + TextFrameIndex const nIdx(MapModelToView(pNode, pHt->GetStart())); + if (nStart > nIdx) + break; + + if (nEnd >= nIdx) + { + SwTextFootnote const*const pFootnote(static_cast<SwTextFootnote const*>(pHt)); + const bool bEndn = pFootnote->GetFootnote().IsEndNote(); + + if (bEndn) + { + if (!pEndBoss) + pEndBoss = pSource->FindFootnoteBossFrame(); + } + else + { + if (!pFootnoteBoss) + { + pFootnoteBoss = pSource->FindFootnoteBossFrame( true ); + if( pFootnoteBoss->GetUpper()->IsSctFrame() ) + { + SwSectionFrame* pSect = static_cast<SwSectionFrame*>( + pFootnoteBoss->GetUpper()); + if (pSect->IsFootnoteAtEnd()) + bFootnoteEndDoc = false; + } + } + } + + // We don't delete, but move instead. + // Three cases are to be considered: + // 1) There's neither Follow nor PrevFollow: + // -> RemoveFootnote() (maybe even a OSL_ENSURE(value)) + // + // 2) nStart > GetOffset, I have a Follow + // -> Footnote moves into Follow + // + // 3) nStart < GetOffset, I am a Follow + // -> Footnote moves into the PrevFollow + // + // Both need to be on one Page/in one Column + SwFootnoteFrame *pFootnoteFrame = SwFootnoteBossFrame::FindFootnote(pSource, pFootnote); + + if (pFootnoteFrame) + { + const bool bEndDoc = bEndn || bFootnoteEndDoc; + if( bRollBack ) + { + while (pFootnoteFrame) + { + pFootnoteFrame->SetRef( this ); + pFootnoteFrame = pFootnoteFrame->GetFollow(); + SetFootnote( true ); + } + } + else if (GetFollow()) + { + SwContentFrame *pDest = GetFollow(); + while (pDest->GetFollow() && static_cast<SwTextFrame*>(pDest-> + GetFollow())->GetOffset() <= nIdx) + pDest = pDest->GetFollow(); + OSL_ENSURE( !SwFootnoteBossFrame::FindFootnote( + pDest,pFootnote),"SwTextFrame::RemoveFootnote: footnote exists"); + + // Never deregister; always move + if (bEndDoc || + !pFootnoteFrame->FindFootnoteBossFrame()->IsBefore(pDest->FindFootnoteBossFrame(!bEndn)) + ) + { + SwPageFrame* pTmp = pFootnoteFrame->FindPageFrame(); + if( pUpdate && pUpdate != pTmp ) + pUpdate->UpdateFootnoteNum(); + pUpdate = pTmp; + while ( pFootnoteFrame ) + { + pFootnoteFrame->SetRef( pDest ); + pFootnoteFrame = pFootnoteFrame->GetFollow(); + } + } + else + { + pFootnoteBoss->MoveFootnotes( this, pDest, pFootnote ); + bRemove = true; + } + static_cast<SwTextFrame*>(pDest)->SetFootnote( true ); + + OSL_ENSURE( SwFootnoteBossFrame::FindFootnote( pDest, + pFootnote),"SwTextFrame::RemoveFootnote: footnote ChgRef failed"); + } + else + { + if (!bEndDoc || ( bEndn && pEndBoss->IsInSct() && + !SwLayouter::Collecting( &GetDoc(), + pEndBoss->FindSctFrame(), nullptr ) )) + { + if( bEndn ) + pEndBoss->RemoveFootnote( this, pFootnote ); + else + pFootnoteBoss->RemoveFootnote( this, pFootnote ); + bRemove = bRemove || !bEndDoc; + OSL_ENSURE( !SwFootnoteBossFrame::FindFootnote( this, pFootnote ), + "SwTextFrame::RemoveFootnote: can't get off that footnote" ); + } + } + } + } + } + if (pUpdate) + pUpdate->UpdateFootnoteNum(); + + // We break the oscillation + if (bRemove && !bFootnoteEndDoc && HasPara()) + { + ValidateBodyFrame(); + ValidateFrame(); + } + + // We call the RemoveFootnote from within the FindBreak, because the last line is + // to be passed to the Follow. The Offset of the Follow is, however, outdated; + // it'll be set soon. CalcFntFlag depends on a correctly set Follow Offset. + // Therefore we temporarily calculate the Follow Offset here + TextFrameIndex nOldOfst(COMPLETE_STRING); + if( HasFollow() && nStart > GetOffset() ) + { + nOldOfst = GetFollow()->GetOffset(); + GetFollow()->ManipOfst(nStart + (bRollBack ? nLen : TextFrameIndex(0))); + } + pSource->CalcFootnoteFlag(); + if (nOldOfst < TextFrameIndex(COMPLETE_STRING)) + GetFollow()->ManipOfst( nOldOfst ); +} + + +/** + * We basically only have two possibilities: + * + * a) The Footnote is already present + * => we move it, if another pSrcFrame has been found + * + * b) The Footnote is not present + * => we have it created for us + * + * Whether the Footnote ends up on our Page/Column, doesn't matter in this + * context. + * + * Optimization for Endnotes. + * + * Another problem: if the Deadline falls within the Footnote Area, we need + * to move the Footnote. + * + * @returns false on any type of error + */ +void SwTextFrame::ConnectFootnote( SwTextFootnote *pFootnote, const SwTwips nDeadLine ) +{ + OSL_ENSURE( !IsVertical() || !IsSwapped(), + "SwTextFrame::ConnectFootnote with swapped frame" ); + + mbFootnote = true; + mbInFootnoteConnect = true; // Just reset! + // See if pFootnote is an endnote on a separate endnote page. + const IDocumentSettingAccess& rSettings = GetDoc().getIDocumentSettingAccess(); + const bool bContinuousEndnotes = rSettings.get(DocumentSettingId::CONTINUOUS_ENDNOTES); + const bool bEnd = pFootnote->GetFootnote().IsEndNote(); + + // We want to store this value, because it is needed as a fallback + // in GetFootnoteLine(), if there is no paragraph information available + mnFootnoteLine = nDeadLine; + + // We always need a parent (Page/Column) + SwSectionFrame *pSect; + SwContentFrame *pContent = this; + if( bEnd && IsInSct() ) + { + pSect = FindSctFrame(); + if( pSect->IsEndnAtEnd() ) + pContent = pSect->FindLastContent( SwFindMode::EndNote ); + if( !pContent ) + pContent = this; + } + + SwFootnoteBossFrame *pBoss = pContent->FindFootnoteBossFrame( !bEnd ); + + pSect = pBoss->FindSctFrame(); + bool bDocEnd = bEnd ? !( pSect && pSect->IsEndnAtEnd() ) : + ( !( pSect && pSect->IsFootnoteAtEnd() ) && + FTNPOS_CHAPTER == GetDoc().GetFootnoteInfo().m_ePos); + + // Footnote can be registered with the Follow + SwContentFrame *pSrcFrame = FindFootnoteRef( pFootnote ); + + if( bDocEnd ) + { + if ((pSect || bContinuousEndnotes) && pSrcFrame) + { + SwFootnoteFrame *pFootnoteFrame = SwFootnoteBossFrame::FindFootnote( pSrcFrame, pFootnote ); + if (pFootnoteFrame && (pFootnoteFrame->IsInSct() || bContinuousEndnotes)) + { + // We either have a foot/endnote that goes to the end of the section or are in Word + // compatibility mode where endnotes go to the end of the document. Handle both + // cases by removing the footnote here, then later appending them to the correct + // last page of the document or section. + pBoss->RemoveFootnote( pSrcFrame, pFootnote ); + pSrcFrame = nullptr; + } + } + } + else if( bEnd && pSect ) + { + SwFootnoteFrame *pFootnoteFrame = pSrcFrame ? SwFootnoteBossFrame::FindFootnote( pSrcFrame, pFootnote ) : nullptr; + if( pFootnoteFrame && !pFootnoteFrame->GetUpper() ) + pFootnoteFrame = nullptr; + SwDoc *const pDoc = &GetDoc(); + if( SwLayouter::Collecting( pDoc, pSect, pFootnoteFrame ) ) + { + if( !pSrcFrame ) + { + SwFootnoteFrame *pNew = new SwFootnoteFrame(pDoc->GetDfltFrameFormat(),this,this,pFootnote); + SwNodeIndex aIdx( *pFootnote->GetStartNode(), 1 ); + ::InsertCnt_( pNew, pDoc, aIdx.GetIndex() ); + pDoc->getIDocumentLayoutAccess().GetLayouter()->CollectEndnote( pNew ); + } + else if( pSrcFrame != this ) + SwFootnoteBossFrame::ChangeFootnoteRef( pSrcFrame, pFootnote, this ); + mbInFootnoteConnect = false; + return; + } + else if (pSrcFrame && pFootnoteFrame) + { + SwFootnoteBossFrame *pFootnoteBoss = pFootnoteFrame->FindFootnoteBossFrame(); + if( !pFootnoteBoss->IsInSct() || + pFootnoteBoss->ImplFindSctFrame()->GetSection()!=pSect->GetSection() ) + { + pBoss->RemoveFootnote( pSrcFrame, pFootnote ); + pSrcFrame = nullptr; + } + } + } + + if( bDocEnd || bEnd ) + { + if( !pSrcFrame ) + pBoss->AppendFootnote( this, pFootnote ); + else if( pSrcFrame != this ) + SwFootnoteBossFrame::ChangeFootnoteRef( pSrcFrame, pFootnote, this ); + mbInFootnoteConnect = false; + return; + } + + SwSaveFootnoteHeight aHeight( pBoss, nDeadLine ); + + if( !pSrcFrame ) // No Footnote was found at all + pBoss->AppendFootnote( this, pFootnote ); + else + { + SwFootnoteFrame *pFootnoteFrame = SwFootnoteBossFrame::FindFootnote( pSrcFrame, pFootnote ); + SwFootnoteBossFrame *pFootnoteBoss = pFootnoteFrame->FindFootnoteBossFrame(); + + bool bBrutal = false; + + if( pFootnoteBoss == pBoss ) // Ref and Footnote are on the same Page/Column + { + SwFrame *pCont = pFootnoteFrame->GetUpper(); + + SwRectFnSet aRectFnSet(pCont); + tools::Long nDiff = aRectFnSet.YDiff( aRectFnSet.GetTop(pCont->getFrameArea()), + nDeadLine ); + + if( nDiff >= 0 ) + { + // If the Footnote has been registered to a Follow, we need to + // rewire it now too + if ( pSrcFrame != this ) + SwFootnoteBossFrame::ChangeFootnoteRef( pSrcFrame, pFootnote, this ); + + // We have some room left, so the Footnote can grow + if ( pFootnoteFrame->GetFollow() && nDiff > 0 ) + { + SwFrameDeleteGuard aDeleteGuard(pCont); + SwTwips nHeight = aRectFnSet.GetHeight(pCont->getFrameArea()); + pBoss->RearrangeFootnotes( nDeadLine, false, pFootnote ); + ValidateBodyFrame(); + ValidateFrame(); + SwViewShell *pSh = getRootFrame()->GetCurrShell(); + if ( pSh && nHeight == aRectFnSet.GetHeight(pCont->getFrameArea()) ) + // So that we don't miss anything + pSh->InvalidateWindows( pCont->getFrameArea() ); + } + mbInFootnoteConnect = false; + return; + } + else + bBrutal = true; + } + else + { + // Ref and Footnote are not on one Page; attempt to move is necessary + SwFrame* pTmp = this; + while( pTmp->GetNext() && pSrcFrame != pTmp ) + pTmp = pTmp->GetNext(); + if( pSrcFrame == pTmp ) + bBrutal = true; + else + { // If our Parent is in a column Area, but the Page already has a + // FootnoteContainer, we can only brute force it + if( pSect && pSect->FindFootnoteBossFrame( !bEnd )->FindFootnoteCont() ) + bBrutal = true; + + else if ( !pFootnoteFrame->GetPrev() || + pFootnoteBoss->IsBefore( pBoss ) + ) + { + SwFootnoteBossFrame *pSrcBoss = pSrcFrame->FindFootnoteBossFrame( !bEnd ); + pSrcBoss->MoveFootnotes( pSrcFrame, this, pFootnote ); + } + else + SwFootnoteBossFrame::ChangeFootnoteRef( pSrcFrame, pFootnote, this ); + } + } + + // The brute force method: Remove Footnote and append. + // We need to call SetFootnoteDeadLine(), as we can more easily adapt the + // nMaxFootnoteHeight after RemoveFootnote + if( bBrutal ) + { + pBoss->RemoveFootnote( pSrcFrame, pFootnote, false ); + std::unique_ptr<SwSaveFootnoteHeight> pHeight(bEnd ? nullptr : new SwSaveFootnoteHeight( pBoss, nDeadLine )); + pBoss->AppendFootnote( this, pFootnote ); + } + } + + // In column Areas, that not yet reach the Page's border a RearrangeFootnotes is not + // useful yet, as the Footnote container has not yet been calculated + if( !pSect || !pSect->Growable() ) + { + // Validate environment, to avoid oscillation + SwSaveFootnoteHeight aNochmal( pBoss, nDeadLine ); + ValidateBodyFrame(); + pBoss->RearrangeFootnotes( nDeadLine, true ); + ValidateFrame(); + } + else if( pSect->IsFootnoteAtEnd() ) + { + ValidateBodyFrame(); + ValidateFrame(); + } + + mbInFootnoteConnect = false; +} + +/** + * The portion for the Footnote Reference in the Text + */ +SwFootnotePortion *SwTextFormatter::NewFootnotePortion( SwTextFormatInfo &rInf, + SwTextAttr *pHint ) +{ + OSL_ENSURE( ! m_pFrame->IsVertical() || m_pFrame->IsSwapped(), + "NewFootnotePortion with unswapped frame" ); + + SwTextFootnote *pFootnote = static_cast<SwTextFootnote*>(pHint); + + if( !m_pFrame->IsFootnoteAllowed() ) + return new SwFootnotePortion("", pFootnote); + + const SwFormatFootnote& rFootnote = pFootnote->GetFootnote(); + SwDoc *const pDoc = &m_pFrame->GetDoc(); + + if( rInf.IsTest() ) + return new SwFootnotePortion(rFootnote.GetViewNumStr(*pDoc, m_pFrame->getRootFrame()), pFootnote); + + SwSwapIfSwapped swap(m_pFrame); + + sal_uInt16 nReal; + { + sal_uInt16 nOldReal = m_pCurr->GetRealHeight(); + sal_uInt16 nOldAscent = m_pCurr->GetAscent(); + sal_uInt16 nOldHeight = m_pCurr->Height(); + CalcRealHeight(); + nReal = m_pCurr->GetRealHeight(); + if( nReal < nOldReal ) + nReal = nOldReal; + m_pCurr->SetRealHeight( nOldReal ); + m_pCurr->Height( nOldHeight ); + m_pCurr->SetAscent( nOldAscent ); + } + + SwTwips nLower = Y() + nReal; + + const bool bVertical = m_pFrame->IsVertical(); + if( bVertical ) + nLower = m_pFrame->SwitchHorizontalToVertical( nLower ); + + nLower = lcl_GetFootnoteLower( m_pFrame, nLower ); + + // We just refresh. + // The Connect does not do anything useful in this case, but will + // mostly throw away the Footnote and create it anew. + if( !rInf.IsQuick() ) + m_pFrame->ConnectFootnote( pFootnote, nLower ); + + SwTextFrame *pScrFrame = m_pFrame->FindFootnoteRef( pFootnote ); + SwFootnoteBossFrame *pBoss = m_pFrame->FindFootnoteBossFrame( !rFootnote.IsEndNote() ); + SwFootnoteFrame *pFootnoteFrame = nullptr; + if( pScrFrame ) + pFootnoteFrame = SwFootnoteBossFrame::FindFootnote( pScrFrame, pFootnote ); + + // We see whether our Append has caused some Footnote to + // still be on the Page/Column. If not, our line disappears too, + // which will lead to the following undesired behaviour: + // Footnote1 still fits onto the Page/Column, but Footnote2 doesn't. + // The Footnote2 Reference remains on the Page/Column. The Footnote itself + // is on the next Page/Column. + // + // Exception: If the Page/Column cannot accommodate another line, + // the Footnote Reference should be moved to the next one. + if( !rFootnote.IsEndNote() ) + { + SwSectionFrame *pSct = pBoss->FindSctFrame(); + bool bAtSctEnd = pSct && pSct->IsFootnoteAtEnd(); + if( FTNPOS_CHAPTER != pDoc->GetFootnoteInfo().m_ePos || bAtSctEnd ) + { + SwFrame* pFootnoteCont = pBoss->FindFootnoteCont(); + // If the Parent is within an Area, it can only be a Column of this + // Area. If this one is not the first Column, we can avoid it. + if( !m_pFrame->IsInTab() && ( GetLineNr() > 1 || m_pFrame->GetPrev() || + ( !bAtSctEnd && m_pFrame->GetIndPrev() ) || + ( pSct && pBoss->GetPrev() ) ) ) + { + if( !pFootnoteCont ) + { + rInf.SetStop( true ); + return nullptr; + } + else + { + // There must not be any Footnote Containers in column Areas and at the same time on the + // Page/Page column + if( pSct && !bAtSctEnd ) // Is the Container in a (column) Area? + { + SwFootnoteBossFrame* pTmp = pBoss->FindSctFrame()->FindFootnoteBossFrame( true ); + SwFootnoteContFrame* pFootnoteC = pTmp->FindFootnoteCont(); + if( pFootnoteC ) + { + SwFootnoteFrame* pTmpFrame = static_cast<SwFootnoteFrame*>(pFootnoteC->Lower()); + if( pTmpFrame && *pTmpFrame < pFootnote ) + { + rInf.SetStop( true ); + return nullptr; + } + } + } + // Is this the last Line that fits? + SwTwips nTmpBot = Y() + nReal * 2; + + if( bVertical ) + nTmpBot = m_pFrame->SwitchHorizontalToVertical( nTmpBot ); + + SwRectFnSet aRectFnSet(pFootnoteCont); + + const tools::Long nDiff = aRectFnSet.YDiff( + aRectFnSet.GetTop(pFootnoteCont->getFrameArea()), + nTmpBot ); + + if( pScrFrame && nDiff < 0 ) + { + if( pFootnoteFrame ) + { + SwFootnoteBossFrame *pFootnoteBoss = pFootnoteFrame->FindFootnoteBossFrame(); + if( pFootnoteBoss != pBoss ) + { + // We're in the last Line and the Footnote has moved + // to another Page. We also want to be on that Page! + rInf.SetStop( true ); + return nullptr; + } + } + } + } + } + } + } + // Finally: Create FootnotePortion and exit ... + SwFootnotePortion *pRet = new SwFootnotePortion( + rFootnote.GetViewNumStr(*pDoc, m_pFrame->getRootFrame()), + pFootnote, nReal ); + rInf.SetFootnoteInside( true ); + + return pRet; +} + +/** + * The portion for the Footnote Numbering in the Footnote Area + */ +SwNumberPortion *SwTextFormatter::NewFootnoteNumPortion( SwTextFormatInfo const &rInf ) const +{ + OSL_ENSURE( m_pFrame->IsInFootnote() && !m_pFrame->GetIndPrev() && !rInf.IsFootnoteDone(), + "This is the wrong place for a ftnnumber" ); + if( rInf.GetTextStart() != m_nStart || + rInf.GetTextStart() != rInf.GetIdx() ) + return nullptr; + + const SwFootnoteFrame* pFootnoteFrame = m_pFrame->FindFootnoteFrame(); + const SwTextFootnote* pFootnote = pFootnoteFrame->GetAttr(); + + // Aha! So we're in the Footnote Area! + SwFormatFootnote& rFootnote = const_cast<SwFormatFootnote&>(pFootnote->GetFootnote()); + + SwDoc *const pDoc = &m_pFrame->GetDoc(); + OUString aFootnoteText(rFootnote.GetViewNumStr(*pDoc, m_pFrame->getRootFrame(), true)); + + const SwEndNoteInfo* pInfo; + if( rFootnote.IsEndNote() ) + pInfo = &pDoc->GetEndNoteInfo(); + else + pInfo = &pDoc->GetFootnoteInfo(); + + const SwAttrSet* pParSet = &rInf.GetCharAttr(); + const IDocumentSettingAccess* pIDSA = &pDoc->getIDocumentSettingAccess(); + std::unique_ptr<SwFont> pNumFnt(new SwFont( pParSet, pIDSA )); + + // #i37142# + // Underline style of paragraph font should not be considered + // Overline style of paragraph font should not be considered + // Weight style of paragraph font should not be considered + // Posture style of paragraph font should not be considered + // See also #i18463# and SwTextFormatter::NewNumberPortion() + pNumFnt->SetUnderline( LINESTYLE_NONE ); + pNumFnt->SetOverline( LINESTYLE_NONE ); + pNumFnt->SetItalic( ITALIC_NONE, SwFontScript::Latin ); + pNumFnt->SetItalic( ITALIC_NONE, SwFontScript::CJK ); + pNumFnt->SetItalic( ITALIC_NONE, SwFontScript::CTL ); + pNumFnt->SetWeight( WEIGHT_NORMAL, SwFontScript::Latin ); + pNumFnt->SetWeight( WEIGHT_NORMAL, SwFontScript::CJK ); + pNumFnt->SetWeight( WEIGHT_NORMAL, SwFontScript::CTL ); + + const rtl::Reference<SwXTextRange> xAnchor = rFootnote.getAnchor(*pDoc); + if (xAnchor.is()) + { + auto aAny = xAnchor->getPropertyValue("CharFontCharSet"); + sal_Int16 eCharSet; + if ((aAny >>= eCharSet) && eCharSet == awt::CharSet::SYMBOL) + { + OUString aFontName; + aAny = xAnchor->getPropertyValue("CharFontName"); + if (aAny >>= aFontName) + { + pNumFnt->SetName(aFontName, SwFontScript::Latin); + pNumFnt->SetName(aFontName, SwFontScript::CJK); + pNumFnt->SetName(aFontName, SwFontScript::CTL); + pNumFnt->SetCharSet(RTL_TEXTENCODING_SYMBOL, SwFontScript::Latin); + pNumFnt->SetCharSet(RTL_TEXTENCODING_SYMBOL, SwFontScript::CJK); + pNumFnt->SetCharSet(RTL_TEXTENCODING_SYMBOL, SwFontScript::CTL); + } + } + } + + const SwAttrSet& rSet = pInfo->GetCharFormat(*pDoc)->GetAttrSet(); + pNumFnt->SetDiffFnt(&rSet, pIDSA ); + pNumFnt->SetVertical( pNumFnt->GetOrientation(), m_pFrame->IsVertical() ); + + // tdf#85610 apply redline coloring to the footnote numbering in the footnote area + SwUnoInternalPaM aPam(*pDoc); + if ( ::sw::XTextRangeToSwPaM(aPam, xAnchor) ) + { + SwRedlineTable::size_type nRedlinePos = 0; + const SwRedlineTable& rTable = pDoc->getIDocumentRedlineAccess().GetRedlineTable(); + const SwRangeRedline* pRedline = rTable.FindAtPosition( *aPam.Start(), nRedlinePos ); + if (pRedline) + { + SwAttrPool& rPool = pDoc->GetAttrPool(); + SfxItemSetFixed<RES_CHRATR_BEGIN, RES_CHRATR_END-1> aSet(rPool); + + std::size_t aAuthor = (1 < pRedline->GetStackCount()) + ? pRedline->GetAuthor( 1 ) + : pRedline->GetAuthor(); + + if ( RedlineType::Delete == pRedline->GetType() ) + SW_MOD()->GetDeletedAuthorAttr(aAuthor, aSet); + else + SW_MOD()->GetInsertAuthorAttr(aAuthor, aSet); + + if (const SvxColorItem* pItem = aSet.GetItemIfSet(RES_CHRATR_COLOR)) + pNumFnt->SetColor(pItem->GetValue()); + if (const SvxUnderlineItem* pItem = aSet.GetItemIfSet(RES_CHRATR_UNDERLINE)) + pNumFnt->SetUnderline(pItem->GetLineStyle()); + if (const SvxCrossedOutItem* pItem = aSet.GetItemIfSet(RES_CHRATR_CROSSEDOUT)) + pNumFnt->SetStrikeout( pItem->GetStrikeout() ); + } + } + + SwFootnoteNumPortion* pNewPor = new SwFootnoteNumPortion( aFootnoteText, std::move(pNumFnt) ); + pNewPor->SetLeft( !m_pFrame->IsRightToLeft() ); + return pNewPor; +} + +static OUString lcl_GetPageNumber( const SwPageFrame* pPage ) +{ + OSL_ENSURE( pPage, "GetPageNumber: Homeless TextFrame" ); + const sal_uInt16 nVirtNum = pPage->GetVirtPageNum(); + const SvxNumberType& rNum = pPage->GetPageDesc()->GetNumType(); + return rNum.GetNumStr( nVirtNum ); +} + +SwErgoSumPortion *SwTextFormatter::NewErgoSumPortion( SwTextFormatInfo const &rInf ) const +{ + // We cannot assume we're a Follow + if( !m_pFrame->IsInFootnote() || m_pFrame->GetPrev() || + rInf.IsErgoDone() || rInf.GetIdx() != m_pFrame->GetOffset() || + m_pFrame->ImplFindFootnoteFrame()->GetAttr()->GetFootnote().IsEndNote() ) + return nullptr; + + // we are in the footnote container + const SwFootnoteInfo &rFootnoteInfo = m_pFrame->GetDoc().GetFootnoteInfo(); + SwTextFrame *pQuoFrame = m_pFrame->FindQuoVadisFrame(); + if( !pQuoFrame ) + return nullptr; + const SwPageFrame* pPage = m_pFrame->FindPageFrame(); + const SwPageFrame* pQuoPage = pQuoFrame->FindPageFrame(); + if( pPage == pQuoFrame->FindPageFrame() ) + return nullptr; // If the QuoVadis is on the same Column/Page + const OUString aPage = lcl_GetPageNumber( pPage ); + SwParaPortion *pPara = pQuoFrame->GetPara(); + if( pPara ) + pPara->SetErgoSumNum( aPage ); + if( rFootnoteInfo.m_aErgoSum.isEmpty() ) + return nullptr; + SwErgoSumPortion *pErgo = new SwErgoSumPortion( rFootnoteInfo.m_aErgoSum, + lcl_GetPageNumber( pQuoPage ) ); + return pErgo; +} + +TextFrameIndex SwTextFormatter::FormatQuoVadis(TextFrameIndex const nOffset) +{ + OSL_ENSURE( ! m_pFrame->IsVertical() || ! m_pFrame->IsSwapped(), + "SwTextFormatter::FormatQuoVadis with swapped frame" ); + + if( !m_pFrame->IsInFootnote() || m_pFrame->ImplFindFootnoteFrame()->GetAttr()->GetFootnote().IsEndNote() ) + return nOffset; + + const SwFrame* pErgoFrame = m_pFrame->FindFootnoteFrame()->GetFollow(); + if( !pErgoFrame && m_pFrame->HasFollow() ) + pErgoFrame = m_pFrame->GetFollow(); + if( !pErgoFrame ) + return nOffset; + + if( pErgoFrame == m_pFrame->GetNext() ) + { + SwFrame *pCol = m_pFrame->FindColFrame(); + while( pCol && !pCol->GetNext() ) + pCol = pCol->GetUpper()->FindColFrame(); + if( pCol ) + return nOffset; + } + else + { + const SwPageFrame* pPage = m_pFrame->FindPageFrame(); + const SwPageFrame* pErgoPage = pErgoFrame->FindPageFrame(); + if( pPage == pErgoPage ) + return nOffset; // If the ErgoSum is on the same Page + } + + SwTextFormatInfo &rInf = GetInfo(); + const SwFootnoteInfo &rFootnoteInfo = m_pFrame->GetDoc().GetFootnoteInfo(); + if( rFootnoteInfo.m_aQuoVadis.isEmpty() ) + return nOffset; + + // A remark on QuoVadis/ErgoSum: + // We use the Font set for the Paragraph for these texts. + // Thus, we initialize: + // TODO: ResetFont(); + FeedInf( rInf ); + SeekStartAndChg( rInf, true ); + if( GetRedln() && m_pCurr->HasRedline() ) + { + std::pair<SwTextNode const*, sal_Int32> const pos( + GetTextFrame()->MapViewToModel(nOffset)); + GetRedln()->Seek(*m_pFont, pos.first->GetIndex(), pos.second, 0); + } + + // A tricky special case: Flyfrms extend into the Line and are at the + // position we want to insert the Quovadis text + // Let's see if it is that bad indeed: + SwLinePortion *pPor = m_pCurr->GetFirstPortion(); + sal_uInt16 nLastLeft = 0; + while( pPor ) + { + if ( pPor->IsFlyPortion() ) + nLastLeft = static_cast<SwFlyPortion*>(pPor)->GetFix() + + static_cast<SwFlyPortion*>(pPor)->Width(); + pPor = pPor->GetNextPortion(); + } + + // The old game all over again: we want the Line to wrap around + // at a certain point, so we adjust the width. + // nLastLeft is now basically the right margin + const sal_uInt16 nOldRealWidth = rInf.RealWidth(); + rInf.RealWidth( nOldRealWidth - nLastLeft ); + + OUString aErgo = lcl_GetPageNumber( pErgoFrame->FindPageFrame() ); + SwQuoVadisPortion *pQuo = new SwQuoVadisPortion(rFootnoteInfo.m_aQuoVadis, aErgo ); + pQuo->SetAscent( rInf.GetAscent() ); + pQuo->Height( rInf.GetTextHeight() ); + pQuo->Format( rInf ); + sal_uInt16 nQuoWidth = pQuo->Width(); + SwLinePortion* pCurrPor = pQuo; + + while ( rInf.GetRest() ) + { + SwLinePortion* pFollow = rInf.GetRest(); + rInf.SetRest( nullptr ); + pCurrPor->Move( rInf ); + + OSL_ENSURE( pFollow->IsQuoVadisPortion(), + "Quo Vadis, rest of QuoVadisPortion" ); + + // format the rest and append it to the other QuoVadis parts + pFollow->Format( rInf ); + nQuoWidth = nQuoWidth + pFollow->Width(); + + pCurrPor->Append( pFollow ); + pCurrPor = pFollow; + } + + Right( Right() - nQuoWidth ); + + TextFrameIndex nRet; + { + SwSwapIfNotSwapped swap(m_pFrame); + + nRet = FormatLine( m_nStart ); + } + + Right( rInf.Left() + nOldRealWidth - 1 ); + + nLastLeft = nOldRealWidth - m_pCurr->Width(); + FeedInf( rInf ); + + // It's possible that there's a Margin Portion at the end, which would + // just cause a lot of trouble, when respanning + pPor = m_pCurr->FindLastPortion(); + SwGluePortion *pGlue = pPor->IsMarginPortion() ? static_cast<SwMarginPortion*>(pPor) : nullptr; + if( pGlue ) + { + pGlue->Height( 0 ); + pGlue->Width( 0 ); + pGlue->SetLen(TextFrameIndex(0)); + pGlue->SetAscent( 0 ); + pGlue->SetNextPortion( nullptr ); + pGlue->SetFixWidth(0); + } + + // Luxury: We make sure the QuoVadis text appears on the right, by + // using Glues. + nLastLeft = nLastLeft - nQuoWidth; + if( nLastLeft ) + { + if( nLastLeft > pQuo->GetAscent() ) // Minimum distance + { + switch( GetAdjust() ) + { + case SvxAdjust::Block: + { + if( !m_pCurr->GetLen() || + CH_BREAK != GetInfo().GetChar(m_nStart + m_pCurr->GetLen() - TextFrameIndex(1))) + nLastLeft = pQuo->GetAscent(); + nQuoWidth = nQuoWidth + nLastLeft; + break; + } + case SvxAdjust::Right: + { + nLastLeft = pQuo->GetAscent(); + nQuoWidth = nQuoWidth + nLastLeft; + break; + } + case SvxAdjust::Center: + { + nQuoWidth = nQuoWidth + pQuo->GetAscent(); + tools::Long nDiff = nLastLeft - nQuoWidth; + if( nDiff < 0 ) + { + nLastLeft = pQuo->GetAscent(); + nQuoWidth = o3tl::narrowing<sal_uInt16>(-nDiff + nLastLeft); + } + else + { + nQuoWidth = 0; + nLastLeft = sal_uInt16(( pQuo->GetAscent() + nDiff ) / 2); + } + break; + } + default: + nQuoWidth = nQuoWidth + nLastLeft; + } + } + else + nQuoWidth = nQuoWidth + nLastLeft; + if( nLastLeft ) + { + pGlue = new SwGluePortion(0); + pGlue->Width( nLastLeft ); + pPor->Append( pGlue ); + pPor = pPor->GetNextPortion(); + } + } + + // Finally: we insert the QuoVadis Portion + pCurrPor = pQuo; + while ( pCurrPor ) + { + // pPor->Append deletes the pPortion pointer of pPor. + // Therefore we have to keep a pointer to the next portion + pQuo = static_cast<SwQuoVadisPortion*>(pCurrPor->GetNextPortion()); + pPor->Append( pCurrPor ); + pPor = pPor->GetNextPortion(); + pCurrPor = pQuo; + } + + m_pCurr->Width( m_pCurr->Width() + nQuoWidth ); + + // And adjust again, due to the adjustment and due to the following special + // case: + // The DummyUser has set a smaller Font in the Line than the one used + // by the QuoVadis text ... + CalcAdjustLine( m_pCurr ); + + return nRet; +} + +/** + * This function creates a Line that reaches to the other Page Margin. + * DummyLines or DummyPortions make sure, that oscillations stop, because + * there's no way to flow back. + * They are used for Footnotes in paragraph-bound Frames and for Footnote + * oscillations + */ +void SwTextFormatter::MakeDummyLine() +{ + sal_uInt16 nRstHeight = GetFrameRstHeight(); + if( m_pCurr && nRstHeight > m_pCurr->Height() ) + { + SwLineLayout *pLay = new SwLineLayout; + nRstHeight = nRstHeight - m_pCurr->Height(); + pLay->Height( nRstHeight ); + pLay->SetAscent( nRstHeight ); + Insert( pLay ); + Next(); + } +} + +namespace { + +class SwFootnoteSave +{ + SwTextSizeInfo* m_pInf; + SwFont* m_pFnt; + std::unique_ptr<SwFont> m_pOld; + + SwFootnoteSave(const SwFootnoteSave&) = delete; + SwFootnoteSave& operator=(const SwFootnoteSave&) = delete; + +public: + SwFootnoteSave( const SwTextSizeInfo &rInf, + const SwTextFootnote *pTextFootnote, + const bool bApplyGivenScriptType, + const SwFontScript nGivenScriptType ); + ~SwFootnoteSave() COVERITY_NOEXCEPT_FALSE; +}; + +} + +SwFootnoteSave::SwFootnoteSave(const SwTextSizeInfo& rInf, const SwTextFootnote* pTextFootnote, + const bool bApplyGivenScriptType, + const SwFontScript nGivenScriptType) + : m_pInf(&const_cast<SwTextSizeInfo&>(rInf)) + , m_pFnt(nullptr) +{ + if( pTextFootnote && rInf.GetTextFrame() ) + { + m_pFnt = const_cast<SwTextSizeInfo&>(rInf).GetFont(); + m_pOld.reset(new SwFont(*m_pFnt)); + m_pOld->GetTox() = m_pFnt->GetTox(); + m_pFnt->GetTox() = 0; + SwFormatFootnote& rFootnote = const_cast<SwFormatFootnote&>(pTextFootnote->GetFootnote()); + const SwDoc *const pDoc = &rInf.GetTextFrame()->GetDoc(); + + // #i98418# + if ( bApplyGivenScriptType ) + { + m_pFnt->SetActual(nGivenScriptType); + } + else + { + // examine text and set script + OUString aTmpStr(rFootnote.GetViewNumStr(*pDoc, rInf.GetTextFrame()->getRootFrame())); + m_pFnt->SetActual(SwScriptInfo::WhichFont(0, aTmpStr)); + } + + const SwEndNoteInfo* pInfo; + if( rFootnote.IsEndNote() ) + pInfo = &pDoc->GetEndNoteInfo(); + else + pInfo = &pDoc->GetFootnoteInfo(); + const SwAttrSet& rSet = pInfo->GetAnchorCharFormat(const_cast<SwDoc&>(*pDoc))->GetAttrSet(); + m_pFnt->SetDiffFnt(&rSet, &pDoc->getIDocumentSettingAccess()); + + // we reduce footnote size, if we are inside a double line portion + if (!m_pOld->GetEscapement() && 50 == m_pOld->GetPropr()) + { + Size aSize = m_pFnt->GetSize(m_pFnt->GetActual()); + m_pFnt->SetSize(Size(aSize.Width() / 2, aSize.Height() / 2), m_pFnt->GetActual()); + } + + // set the correct rotation at the footnote font + if( const SvxCharRotateItem* pItem = rSet.GetItemIfSet( RES_CHRATR_ROTATE ) ) + m_pFnt->SetVertical(pItem->GetValue(), + rInf.GetTextFrame()->IsVertical()); + + m_pFnt->ChgPhysFnt(m_pInf->GetVsh(), *m_pInf->GetOut()); + + if( const SvxBrushItem* pItem = rSet.GetItemIfSet( RES_CHRATR_BACKGROUND ) ) + m_pFnt->SetBackColor(pItem->GetColor()); + } + else + m_pFnt = nullptr; +} + +SwFootnoteSave::~SwFootnoteSave() COVERITY_NOEXCEPT_FALSE +{ + if (m_pFnt) + { + // Put back SwFont + *m_pFnt = *m_pOld; + m_pFnt->GetTox() = m_pOld->GetTox(); + m_pFnt->ChgPhysFnt(m_pInf->GetVsh(), *m_pInf->GetOut()); + m_pOld.reset(); + } +} + +SwFootnotePortion::SwFootnotePortion( const OUString &rExpand, + SwTextFootnote *pFootn, sal_uInt16 nReal ) + : SwFieldPortion( rExpand, nullptr ) + , m_pFootnote(pFootn) + , m_nOrigHeight( nReal ) + // #i98418# + , mbPreferredScriptTypeSet( false ) + , mnPreferredScriptType( SwFontScript::Latin ) +{ + SetLen(TextFrameIndex(1)); + SetWhichPor( PortionType::Footnote ); +} + +bool SwFootnotePortion::GetExpText( const SwTextSizeInfo &, OUString &rText ) const +{ + rText = m_aExpand; + return true; +} + +bool SwFootnotePortion::Format( SwTextFormatInfo &rInf ) +{ + // #i98418# +// SwFootnoteSave aFootnoteSave( rInf, pFootnote ); + SwFootnoteSave aFootnoteSave( rInf, m_pFootnote, mbPreferredScriptTypeSet, mnPreferredScriptType ); + // the idx is manipulated in SwExpandPortion::Format + // this flag indicates, that a footnote is allowed to trigger + // an underflow during SwTextGuess::Guess + rInf.SetFakeLineStart( rInf.GetIdx() > rInf.GetLineStart() ); + const bool bFull = SwFieldPortion::Format( rInf ); + rInf.SetFakeLineStart( false ); + SetAscent( rInf.GetAscent() ); + Height( rInf.GetTextHeight() ); + rInf.SetFootnoteDone( !bFull ); + if( !bFull ) + rInf.SetParaFootnote(); + return bFull; +} + +void SwFootnotePortion::Paint( const SwTextPaintInfo &rInf ) const +{ + // #i98418# +// SwFootnoteSave aFootnoteSave( rInf, pFootnote ); + SwFootnoteSave aFootnoteSave( rInf, m_pFootnote, mbPreferredScriptTypeSet, mnPreferredScriptType ); + rInf.DrawViewOpt( *this, PortionType::Footnote ); + SwExpandPortion::Paint( rInf ); +} + +SwPosSize SwFootnotePortion::GetTextSize( const SwTextSizeInfo &rInfo ) const +{ + // #i98418# +// SwFootnoteSave aFootnoteSave( rInfo, pFootnote ); + SwFootnoteSave aFootnoteSave( rInfo, m_pFootnote, mbPreferredScriptTypeSet, mnPreferredScriptType ); + return SwExpandPortion::GetTextSize( rInfo ); +} + +// #i98418# +void SwFootnotePortion::SetPreferredScriptType( SwFontScript nPreferredScriptType ) +{ + mbPreferredScriptTypeSet = true; + mnPreferredScriptType = nPreferredScriptType; +} + +SwFieldPortion *SwQuoVadisPortion::Clone( const OUString &rExpand ) const +{ + return new SwQuoVadisPortion( rExpand, m_aErgo ); +} + +SwQuoVadisPortion::SwQuoVadisPortion( const OUString &rExp, OUString aStr ) + : SwFieldPortion( rExp ), m_aErgo(std::move(aStr)) +{ + SetLen(TextFrameIndex(0)); + SetWhichPor( PortionType::QuoVadis ); +} + +bool SwQuoVadisPortion::Format( SwTextFormatInfo &rInf ) +{ + // First try; maybe the Text fits + CheckScript( rInf ); + bool bFull = SwFieldPortion::Format( rInf ); + SetLen(TextFrameIndex(0)); + + if( bFull ) + { + // Second try; we make the String shorter + m_aExpand = "..."; + bFull = SwFieldPortion::Format( rInf ); + SetLen(TextFrameIndex(0)); + if( bFull ) + // Third try; we're done: we crush + Width( sal_uInt16(rInf.Width() - rInf.X()) ); + + // No multiline Fields for QuoVadis and ErgoSum + if( rInf.GetRest() ) + { + delete rInf.GetRest(); + rInf.SetRest( nullptr ); + } + } + return bFull; +} + +bool SwQuoVadisPortion::GetExpText( const SwTextSizeInfo &, OUString &rText ) const +{ + rText = m_aExpand; + // if this QuoVadisPortion has a follow, the follow is responsible for + // the ergo text. + if ( ! HasFollow() ) + rText += m_aErgo; + return true; +} + +void SwQuoVadisPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Special( GetLen(), m_aExpand + m_aErgo, GetWhichPor() ); +} + +void SwQuoVadisPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + // We _always_ want to output per DrawStretchText, because nErgo + // can quickly switch + if( PrtWidth() ) + { + rInf.DrawViewOpt( *this, PortionType::QuoVadis ); + SwTextSlot aDiffText( &rInf, this, true, false ); + SwFontSave aSave( rInf, m_pFont.get() ); + rInf.DrawText( *this, rInf.GetLen(), true ); + } +} + +SwFieldPortion *SwErgoSumPortion::Clone( const OUString &rExpand ) const +{ + return new SwErgoSumPortion( rExpand, std::u16string_view() ); +} + +SwErgoSumPortion::SwErgoSumPortion(const OUString &rExp, std::u16string_view rStr) + : SwFieldPortion( rExp ) +{ + SetLen(TextFrameIndex(0)); + m_aExpand += rStr; + + // One blank distance to the text + m_aExpand += " "; + SetWhichPor( PortionType::ErgoSum ); +} + +TextFrameIndex SwErgoSumPortion::GetModelPositionForViewPoint(const sal_uInt16) const +{ + return TextFrameIndex(0); +} + +bool SwErgoSumPortion::Format( SwTextFormatInfo &rInf ) +{ + const bool bFull = SwFieldPortion::Format( rInf ); + SetLen(TextFrameIndex(0)); + rInf.SetErgoDone( true ); + + // No multiline Fields for QuoVadis and ErgoSum + if( bFull && rInf.GetRest() ) + { + delete rInf.GetRest(); + rInf.SetRest( nullptr ); + } + + // We return false in order to get some text into the current line, + // even if it's full (better than looping) + return false; +} + +void SwParaPortion::SetErgoSumNum( const OUString& rErgo ) +{ + SwLineLayout *pLay = this; + while( pLay->GetNext() ) + { + pLay = pLay->GetNext(); + } + SwLinePortion *pPor = pLay; + SwQuoVadisPortion *pQuo = nullptr; + while( pPor && !pQuo ) + { + if ( pPor->IsQuoVadisPortion() ) + pQuo = static_cast<SwQuoVadisPortion*>(pPor); + pPor = pPor->GetNextPortion(); + } + if( pQuo ) + pQuo->SetNumber( rErgo ); +} + +/** + * Is called in SwTextFrame::Prepare() + */ +bool SwParaPortion::UpdateQuoVadis( std::u16string_view rQuo ) +{ + SwLineLayout *pLay = this; + while( pLay->GetNext() ) + { + pLay = pLay->GetNext(); + } + SwLinePortion *pPor = pLay; + SwQuoVadisPortion *pQuo = nullptr; + while( pPor && !pQuo ) + { + if ( pPor->IsQuoVadisPortion() ) + pQuo = static_cast<SwQuoVadisPortion*>(pPor); + pPor = pPor->GetNextPortion(); + } + + if( !pQuo ) + return false; + + return pQuo->GetQuoText() == rQuo; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txthyph.cxx b/sw/source/core/text/txthyph.cxx new file mode 100644 index 0000000000..e9f6a9d0e6 --- /dev/null +++ b/sw/source/core/text/txthyph.cxx @@ -0,0 +1,582 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <breakit.hxx> +#include <editeng/unolingu.hxx> +#include <com/sun/star/i18n/WordType.hpp> +#include <com/sun/star/i18n/XBreakIterator.hpp> +#include <viewopt.hxx> +#include <viewsh.hxx> +#include <SwPortionHandler.hxx> +#include "porhyph.hxx" +#include "inftxt.hxx" +#include "itrform2.hxx" +#include "guess.hxx" +#include <rootfrm.hxx> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::linguistic2; +using namespace ::com::sun::star::i18n; + +Reference< XHyphenatedWord > SwTextFormatInfo::HyphWord( + const OUString &rText, const sal_Int32 nMinTrail ) +{ + if( rText.getLength() < 4 || m_pFnt->IsSymbol(m_pVsh) ) + return nullptr; + Reference< XHyphenator > xHyph = ::GetHyphenator(); + Reference< XHyphenatedWord > xHyphWord; + + if( xHyph.is() ) + xHyphWord = xHyph->hyphenate( rText, + g_pBreakIt->GetLocale( m_pFnt->GetLanguage() ), + rText.getLength() - nMinTrail, GetHyphValues() ); + return xHyphWord; + +} + +/** + * We format a row for interactive hyphenation + */ +bool SwTextFrame::Hyphenate(SwInterHyphInfoTextFrame & rHyphInf) +{ + vcl::RenderContext* pRenderContext = getRootFrame()->GetCurrShell()->GetOut(); + OSL_ENSURE( ! IsVertical() || ! IsSwapped(),"swapped frame at SwTextFrame::Hyphenate" ); + + assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is()); + + // We lock it, to start with + OSL_ENSURE( !IsLocked(), "SwTextFrame::Hyphenate: this is locked" ); + + // The frame::Frame must have a valid SSize! + Calc(pRenderContext); + GetFormatted(); + + bool bRet = false; + if( !IsEmpty() ) + { + // We always need to enable hyphenation + // Don't be afraid: the SwTextIter saves the old row in the hyphenate + TextFrameLockGuard aLock( this ); + + if ( IsVertical() ) + SwapWidthAndHeight(); + + SwTextFormatInfo aInf( getRootFrame()->GetCurrShell()->GetOut(), this, true ); // true for interactive hyph! + SwTextFormatter aLine( this, &aInf ); + aLine.CharToLine( rHyphInf.m_nStart ); + + // If we're within the first word of a row, it could've been hyphenated + // in the row earlier. + // That's why we go one row back. + if( aLine.Prev() ) + { + SwLinePortion *pPor = aLine.GetCurr()->GetFirstPortion(); + while( pPor->GetNextPortion() ) + pPor = pPor->GetNextPortion(); + if( pPor->GetWhichPor() == PortionType::SoftHyphen || + pPor->GetWhichPor() == PortionType::SoftHyphenStr ) + aLine.Next(); + } + + const TextFrameIndex nEnd = rHyphInf.m_nEnd; + while( !bRet && aLine.GetStart() < nEnd ) + { + bRet = aLine.Hyphenate( rHyphInf ); + if( !aLine.Next() ) + break; + } + + if ( IsVertical() ) + SwapWidthAndHeight(); + } + return bRet; +} + +/** + * We format a row for interactive hyphenation + * We can assume that we've already formatted. + * We just reformat the row, the hyphenator will be prepared like + * the UI expects it to be. + * TODO: We can of course optimize this a lot. + */ +void SetParaPortion( SwTextInfo *pInf, SwParaPortion *pRoot ) +{ + OSL_ENSURE( pRoot, "SetParaPortion: no root anymore" ); + pInf->m_pPara = pRoot; +} + +bool SwTextFormatter::Hyphenate(SwInterHyphInfoTextFrame & rHyphInf) +{ + SwTextFormatInfo &rInf = GetInfo(); + + // We never need to hyphenate anything in the last row + // Except for, if it contains a FlyPortion or if it's the + // last row of the Master + if( !GetNext() && !rInf.GetTextFly().IsOn() && !m_pFrame->GetFollow() ) + return false; + + TextFrameIndex nWrdStart = m_nStart; + + // We need to retain the old row + // E.g.: The attribute for hyphenation was not set, but + // it's always set in SwTextFrame::Hyphenate, because we want + // to set breakpoints. + SwLineLayout *pOldCurr = m_pCurr; + + InitCntHyph(); + + // 5298: IsParaLine() (ex.IsFirstLine) calls GetParaPortion(). + // We have to create the same conditions: in the first line + // we format SwParaPortions... + if( pOldCurr->IsParaPortion() ) + { + SwParaPortion *pPara = new SwParaPortion(); + SetParaPortion( &rInf, pPara ); + m_pCurr = pPara; + OSL_ENSURE( IsParaLine(), "SwTextFormatter::Hyphenate: not the first" ); + } + else + m_pCurr = new SwLineLayout(); + + nWrdStart = FormatLine( nWrdStart ); + + // You always should keep in mind that for example there are fields + // which can be hyphenated + if( m_pCurr->PrtWidth() && m_pCurr->GetLen() ) + { + // We must be prepared that there are FlyFrames in the line, + // at which line breaking is possible. So we search for the first + // HyphPortion in the specified range. + + SwLinePortion *pPos = m_pCurr->GetNextPortion(); + const TextFrameIndex nPamStart = rHyphInf.m_nStart; + nWrdStart = m_nStart; + const TextFrameIndex nEnd = rHyphInf.m_nEnd; + while( pPos ) + { + // Either we are above or we are running into a HyphPortion + // at the end of line or before a Fly. + if( nWrdStart >= nEnd ) + { + nWrdStart = TextFrameIndex(0); + break; + } + + if( nWrdStart >= nPamStart && pPos->InHyphGrp() + && ( !pPos->IsSoftHyphPortion() + || static_cast<SwSoftHyphPortion*>(pPos)->IsExpand() ) ) + { + nWrdStart = nWrdStart + pPos->GetLen(); + break; + } + + nWrdStart = nWrdStart + pPos->GetLen(); + pPos = pPos->GetNextPortion(); + } + // When pPos is null, no hyphen position was found. + if( !pPos ) + nWrdStart = TextFrameIndex(0); + } + else + // In case the whole line is zero-length, that's the same situation as + // above when the portion iteration ends without explicitly breaking + // from the loop. + nWrdStart = TextFrameIndex(0); + + // the old LineLayout is set again ... + delete m_pCurr; + m_pCurr = pOldCurr; + + if( pOldCurr->IsParaPortion() ) + { + SetParaPortion( &rInf, static_cast<SwParaPortion*>(pOldCurr) ); + OSL_ENSURE( IsParaLine(), "SwTextFormatter::Hyphenate: even not the first" ); + } + + if (nWrdStart == TextFrameIndex(0)) + return false; + + // nWrdStart contains the position in string that should be hyphenated + rHyphInf.m_nWordStart = nWrdStart; + + TextFrameIndex nLen(0); + const TextFrameIndex nEnd = nWrdStart; + + // we search forwards + Reference< XHyphenatedWord > xHyphWord; + + Boundary const aBound = g_pBreakIt->GetBreakIter()->getWordBoundary( + rInf.GetText(), sal_Int32(nWrdStart), + g_pBreakIt->GetLocale( rInf.GetFont()->GetLanguage() ), WordType::DICTIONARY_WORD, true ); + nWrdStart = TextFrameIndex(aBound.startPos); + nLen = TextFrameIndex(aBound.endPos) - nWrdStart; + if (nLen == TextFrameIndex(0)) + return false; + + OUString const aSelText(rInf.GetText().copy(sal_Int32(nWrdStart), sal_Int32(nLen))); + const sal_Int32 nMinTrail = (nWrdStart + nLen > nEnd) + ? sal_Int32(nWrdStart + nLen - nEnd) - 1 + : 0; + + //!! rHyphInf.SetHyphWord( ... ) must done here + xHyphWord = rInf.HyphWord( aSelText, nMinTrail ); + if ( xHyphWord.is() ) + { + rHyphInf.SetHyphWord( xHyphWord ); + rHyphInf.m_nWordStart = nWrdStart; + rHyphInf.m_nWordLen = nLen; + return true; + } + + return false; +} + +bool SwTextPortion::CreateHyphen( SwTextFormatInfo &rInf, SwTextGuess const &rGuess ) +{ + const Reference< XHyphenatedWord >& xHyphWord = rGuess.HyphWord(); + + OSL_ENSURE( !mpNextPortion, "SwTextPortion::CreateHyphen(): another portion, another planet..." ); + OSL_ENSURE( xHyphWord.is(), "SwTextPortion::CreateHyphen(): You are lucky! The code is robust here." ); + + if( rInf.IsHyphForbud() || + mpNextPortion || // robust + !xHyphWord.is() || // more robust + // multi-line fields can't be hyphenated interactively + ( rInf.IsInterHyph() && InFieldGrp() ) ) + return false; + + std::unique_ptr<SwHyphPortion> pHyphPor; + TextFrameIndex nPorEnd; + SwTextSizeInfo aInf( rInf ); + + // first case: hyphenated word has alternative spelling + if ( xHyphWord->isAlternativeSpelling() ) + { + SvxAlternativeSpelling aAltSpell = SvxGetAltSpelling( xHyphWord ); + OSL_ENSURE( aAltSpell.bIsAltSpelling, "no alternative spelling" ); + + OUString aAltText = aAltSpell.aReplacement; + nPorEnd = TextFrameIndex(aAltSpell.nChangedPos) + rGuess.BreakStart() - rGuess.FieldDiff(); + sal_Int32 nTmpLen = 0; + + // soft hyphen at alternative spelling position? + if( rInf.GetText()[sal_Int32(rInf.GetSoftHyphPos())] == CHAR_SOFTHYPHEN ) + { + pHyphPor.reset(new SwSoftHyphStrPortion( aAltText )); + nTmpLen = 1; + } + else { + pHyphPor.reset(new SwHyphStrPortion( aAltText )); + } + + // length of pHyphPor is adjusted + pHyphPor->SetLen( TextFrameIndex(aAltText.getLength() + 1) ); + static_cast<SwPosSize&>(*pHyphPor) = pHyphPor->GetTextSize( rInf ); + pHyphPor->SetLen( TextFrameIndex(aAltSpell.nChangedLength + nTmpLen) ); + } + else + { + // second case: no alternative spelling + pHyphPor.reset(new SwHyphPortion); + pHyphPor->SetLen(TextFrameIndex(1)); + + static const void* nLastFontCacheId = nullptr; + static sal_uInt16 aMiniCacheH = 0, aMiniCacheW = 0; + const void* nTmpFontCacheId; + sal_uInt16 nFntIdx; + rInf.GetFont()->GetFontCacheId( nTmpFontCacheId, nFntIdx, rInf.GetFont()->GetActual() ); + if( !nLastFontCacheId || nLastFontCacheId != nTmpFontCacheId ) { + nLastFontCacheId = nTmpFontCacheId; + static_cast<SwPosSize&>(*pHyphPor) = pHyphPor->GetTextSize( rInf ); + aMiniCacheH = pHyphPor->Height(); + aMiniCacheW = pHyphPor->Width(); + } else { + pHyphPor->Height( aMiniCacheH ); + pHyphPor->Width( aMiniCacheW ); + } + pHyphPor->SetLen(TextFrameIndex(0)); + + // values required for this + nPorEnd = TextFrameIndex(xHyphWord->getHyphenPos() + 1) + + rGuess.BreakStart() - rGuess.FieldDiff(); + } + + // portion end must be in front of us + // we do not put hyphens at start of line + if ( nPorEnd > rInf.GetIdx() || + ( nPorEnd == rInf.GetIdx() && rInf.GetLineStart() != rInf.GetIdx() ) ) + { + aInf.SetLen( nPorEnd - rInf.GetIdx() ); + pHyphPor->SetAscent( GetAscent() ); + SetLen( aInf.GetLen() ); + CalcTextSize( aInf ); + + Insert( pHyphPor.release() ); + + short nKern = rInf.GetFont()->CheckKerning(); + if( nKern ) + new SwKernPortion( *this, nKern ); + + return true; + } + + // last exit for the lost + pHyphPor.reset(); + BreakCut( rInf, rGuess ); + return false; +} + +bool SwHyphPortion::GetExpText( const SwTextSizeInfo &/*rInf*/, OUString &rText ) const +{ + rText = "-"; + return true; +} + +void SwHyphPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Special( GetLen(), OUString('-'), GetWhichPor() ); +} + +void SwHyphPortion::dumpAsXml(xmlTextWriterPtr pWriter, const OUString& rText, + TextFrameIndex& nOffset) const +{ + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("SwHyphPortion")); + dumpAsXmlAttributes(pWriter, rText, nOffset); + nOffset += GetLen(); + + (void)xmlTextWriterEndElement(pWriter); +} + +bool SwHyphPortion::Format( SwTextFormatInfo &rInf ) +{ + const SwLinePortion *pLast = rInf.GetLast(); + Height( pLast->Height() ); + SetAscent( pLast->GetAscent() ); + OUString aText; + + if( !GetExpText( rInf, aText ) ) + return false; + + PrtWidth( rInf.GetTextSize( aText ).Width() ); + const bool bFull = rInf.Width() <= rInf.X() + PrtWidth(); + if( bFull && !rInf.IsUnderflow() ) { + Truncate(); + rInf.SetUnderflow( this ); + } + + return bFull; +} + +bool SwHyphStrPortion::GetExpText( const SwTextSizeInfo &, OUString &rText ) const +{ + rText = m_aExpand; + return true; +} + +void SwHyphStrPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Special( GetLen(), m_aExpand, GetWhichPor() ); +} + +SwLinePortion *SwSoftHyphPortion::Compress() { return this; } + +SwSoftHyphPortion::SwSoftHyphPortion() : + m_bExpand(false), m_nViewWidth(0) +{ + SetLen(TextFrameIndex(1)); + SetWhichPor( PortionType::SoftHyphen ); +} + +sal_uInt16 SwSoftHyphPortion::GetViewWidth( const SwTextSizeInfo &rInf ) const +{ + // Although we're in the const, nViewWidth should be calculated at + // the last possible moment + if( !Width() && rInf.OnWin() && rInf.GetOpt().IsSoftHyph() && !IsExpand() ) + { + if( !m_nViewWidth ) + const_cast<SwSoftHyphPortion*>(this)->m_nViewWidth + = rInf.GetTextSize(OUString('-')).Width(); + } + else + const_cast<SwSoftHyphPortion*>(this)->m_nViewWidth = 0; + return m_nViewWidth; +} + +/** + * Cases: + * + * 1) SoftHyph is in the line, ViewOpt off + * -> invisible, neighbors unchanged + * 2) SoftHyph is in the line, ViewOpt on + * -> visible, neighbors unchanged + * 3) SoftHyph is at the end of the line, ViewOpt on or off + * -> always visible, neighbors unchanged + */ +void SwSoftHyphPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + if( Width() ) + { + rInf.DrawViewOpt( *this, PortionType::SoftHyphen ); + SwExpandPortion::Paint( rInf ); + } +} + +/** + * We get the final width from the FormatEOL() + * + * During the underflow-phase we determine, whether or not + * there's an alternative spelling at all ... + * + * Case 1: "Au-to" + * 1) {Au}{-}{to}, {to} does not fit anymore => underflow + * 2) {-} calls hyphenate => no alternative + * 3) FormatEOL() and bFull = true + * + * Case 2: "Zuc-ker" + * 1) {Zuc}{-}{ker}, {ker} does not fit anymore => underflow + * 2) {-} calls hyphenate => alternative! + * 3) Underflow() and bFull = true + * 4) {Zuc} calls hyphenate => {Zuk}{-}{ker} + */ +bool SwSoftHyphPortion::Format( SwTextFormatInfo &rInf ) +{ + bool bFull = true; + + // special case for old German spelling + if( rInf.IsUnderflow() ) + { + if( rInf.GetSoftHyphPos() ) + return true; + + const bool bHyph = rInf.ChgHyph( true ); + if( rInf.IsHyphenate() ) + { + rInf.SetSoftHyphPos( rInf.GetIdx() ); + Width(0); + // if the soft hyphened word has an alternative spelling + // when hyphenated (old German spelling), the soft hyphen + // portion has to trigger an underflow + SwTextGuess aGuess; + bFull = rInf.IsInterHyph() || + !aGuess.AlternativeSpelling(rInf, rInf.GetIdx() - TextFrameIndex(1)); + } + rInf.ChgHyph( bHyph ); + + if( bFull && !rInf.IsHyphForbud() ) + { + rInf.SetSoftHyphPos(TextFrameIndex(0)); + FormatEOL( rInf ); + if ( rInf.GetFly() ) + rInf.GetRoot()->SetMidHyph( true ); + else + rInf.GetRoot()->SetEndHyph( true ); + } + else + { + rInf.SetSoftHyphPos( rInf.GetIdx() ); + Truncate(); + rInf.SetUnderflow( this ); + } + return true; + } + + rInf.SetSoftHyphPos(TextFrameIndex(0)); + SetExpand( true ); + bFull = SwHyphPortion::Format( rInf ); + SetExpand( false ); + if( !bFull ) + { + // By default, we do not have a width, but we do have a height + Width(0); + } + return bFull; +} + +/** + * Format End of Line + */ +void SwSoftHyphPortion::FormatEOL( SwTextFormatInfo &rInf ) +{ + if( IsExpand() ) + return; + + SetExpand( true ); + if( rInf.GetLast() == this ) + rInf.SetLast( FindPrevPortion( rInf.GetRoot() ) ); + + // We need to reset the old values + const SwTwips nOldX = rInf.X(); + TextFrameIndex const nOldIdx = rInf.GetIdx(); + rInf.X( rInf.X() - PrtWidth() ); + rInf.SetIdx( rInf.GetIdx() - GetLen() ); + const bool bFull = SwHyphPortion::Format( rInf ); + + // Shady business: We're allowed to get wider, but a Fly is also + // being processed, which needs a correct X position + if( bFull || !rInf.GetFly() ) + rInf.X( nOldX ); + else + rInf.X( nOldX + Width() ); + rInf.SetIdx( nOldIdx ); +} + +/** + * We're expanding: + * - if the special characters should be visible + * - if we're at the end of the line + * - if we're before a (real/emulated) line break + */ +bool SwSoftHyphPortion::GetExpText( const SwTextSizeInfo &rInf, OUString &rText ) const +{ + if( IsExpand() || ( rInf.OnWin() && rInf.GetOpt().IsSoftHyph() ) || + ( GetNextPortion() && ( GetNextPortion()->InFixGrp() || + GetNextPortion()->IsDropPortion() || GetNextPortion()->IsLayPortion() || + GetNextPortion()->IsParaPortion() || GetNextPortion()->IsBreakPortion() ) ) ) + { + return SwHyphPortion::GetExpText( rInf, rText ); + } + return false; +} + +void SwSoftHyphPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + const PortionType nWhich = ! Width() ? + PortionType::SoftHyphenComp : + GetWhichPor(); + rPH.Special( GetLen(), OUString('-'), nWhich ); +} + +void SwSoftHyphStrPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + // Bug or feature?: + // {Zu}{k-}{ker}, {k-} will be gray instead of {-} + rInf.DrawViewOpt( *this, PortionType::SoftHyphen ); + SwHyphStrPortion::Paint( rInf ); +} + +SwSoftHyphStrPortion::SwSoftHyphStrPortion( std::u16string_view rStr ) + : SwHyphStrPortion( rStr ) +{ + SetLen(TextFrameIndex(1)); + SetWhichPor( PortionType::SoftHyphenStr ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtinit.cxx b/sw/source/core/text/txtinit.cxx new file mode 100644 index 0000000000..2b8bb2e95d --- /dev/null +++ b/sw/source/core/text/txtinit.cxx @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <swcache.hxx> +#include <fntcache.hxx> +#include <swfntcch.hxx> +#include <txtfrm.hxx> +#include "pordrop.hxx" +#include <init.hxx> +#include <txtfly.hxx> +#include <dbg_lay.hxx> + +SwCache *SwTextFrame::s_pTextCache = nullptr; +SwContourCache *pContourCache = nullptr; +SwDropCapCache *pDropCapCache = nullptr; + +// Are ONLY used in init.cxx. +// There we have extern void TextFinit() +// and extern void TextInit_(...) + +void TextInit_() +{ + pFntCache = new SwFntCache; // Cache for SwSubFont -> SwFntObj = { Font aFont, Font* pScrFont, Font* pPrtFont, OutputDevice* pPrinter, ... } + pSwFontCache = new SwFontCache; // Cache for SwTextFormatColl -> SwFontObj = { SwFont aSwFont, SfxPoolItem* pDefaultArray } + SwCache *pTextCache = new SwCache( 250 // Cache for SwTextFrame -> SwTextLine = { SwParaPortion* pLine } +#ifdef DBG_UTIL + , "static SwTextFrame::s_pTextCache"_ostr +#endif + ); + SwTextFrame::SetTextCache( pTextCache ); + PROTOCOL_INIT +} + +void TextFinit() +{ + PROTOCOL_STOP + delete SwTextFrame::GetTextCache(); + delete pSwFontCache; + delete pFntCache; + delete pContourCache; + SwDropPortion::DeleteDropCapCache(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtpaint.cxx b/sw/source/core/text/txtpaint.cxx new file mode 100644 index 0000000000..ccd9647bd9 --- /dev/null +++ b/sw/source/core/text/txtpaint.cxx @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "txtpaint.hxx" +#include <txtfrm.hxx> +#include <swrect.hxx> +#include <rootfrm.hxx> + +SwSaveClip::~SwSaveClip() +{ + // We recover the old state + if( !(m_pOut && m_bChg) ) + return; + + if ( m_pOut->GetConnectMetaFile() ) + m_pOut->Pop(); + else + { + if( m_bOn ) + m_pOut->SetClipRegion( m_aClip ); + else + m_pOut->SetClipRegion(); + } + m_bChg = false; +} + +void SwSaveClip::ChgClip_( const SwRect &rRect, const SwTextFrame* pFrame, + bool bEnlargeRect, + sal_Int32 nEnlargeTop, + sal_Int32 nEnlargeBottom ) +{ + SwRect aOldRect( rRect ); + const bool bVertical = pFrame && pFrame->IsVertical(); + + if ( pFrame && pFrame->IsRightToLeft() ) + pFrame->SwitchLTRtoRTL( const_cast<SwRect&>(rRect) ); + + if ( bVertical ) + pFrame->SwitchHorizontalToVertical( const_cast<SwRect&>(rRect) ); + + if ( !m_pOut || (!rRect.HasArea() && !m_pOut->IsClipRegion()) ) + { + const_cast<SwRect&>(rRect) = aOldRect; + return; + } + + if ( !m_bChg ) + { + if ( m_pOut->GetConnectMetaFile() ) + m_pOut->Push(); + else if ( m_bOn ) + m_aClip = m_pOut->GetClipRegion(); + } + + if ( !rRect.HasArea() ) + m_pOut->SetClipRegion(); + else + { + tools::Rectangle aRect( rRect.SVRect() ); + + // Having underscores in our line, we enlarged the repaint area + // (see frmform.cxx) because for some fonts it could be too small. + // Consequently, we have to enlarge the clipping rectangle as well. + if ( bEnlargeRect && ! bVertical ) + aRect.AdjustBottom(40 ); + + // enlarge clip for paragraph margins at small fixed line height + if ( nEnlargeTop > 0 ) + aRect.AdjustTop( -nEnlargeTop ); + + if ( nEnlargeBottom > 0 ) + aRect.AdjustBottom( nEnlargeBottom ); + + // If the ClipRect is identical, nothing will happen + if( m_pOut->IsClipRegion() ) // no && because of Mac + { + if ( aRect == m_pOut->GetClipRegion().GetBoundRect() ) + { + const_cast<SwRect&>(rRect) = aOldRect; + return; + } + } + + if( SwRootFrame::HasSameRect( rRect ) ) + m_pOut->SetClipRegion(); + else + { + const vcl::Region aClipRegion( aRect ); + m_pOut->SetClipRegion( aClipRegion ); + } + } + m_bChg = true; + + const_cast<SwRect&>(rRect) = aOldRect; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txtpaint.hxx b/sw/source/core/text/txtpaint.hxx new file mode 100644 index 0000000000..1f9700af4c --- /dev/null +++ b/sw/source/core/text/txtpaint.hxx @@ -0,0 +1,126 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <vcl/outdev.hxx> + +class SwRect; // SwSaveClip +class SwTextFrame; + +class SwSaveClip final +{ + vcl::Region m_aClip; + const bool m_bOn; + bool m_bChg; + + VclPtr<OutputDevice> m_pOut; + void ChgClip_( const SwRect &rRect, const SwTextFrame* pFrame, + bool bEnlargeRect, + sal_Int32 nEnlargeTop, + sal_Int32 nEnlargeBottom ); +public: + explicit SwSaveClip(OutputDevice* pOutDev) + : m_bOn(pOutDev && pOutDev->IsClipRegion()) + , m_bChg(false) + , m_pOut(pOutDev) + { + } + + ~SwSaveClip(); + void ChgClip( const SwRect &rRect, const SwTextFrame* pFrame = nullptr, + bool bEnlargeRect = false, + sal_Int32 nEnlargeTop = 0, + sal_Int32 nEnlargeBottom = 0) + { if( m_pOut ) ChgClip_( rRect, pFrame, + bEnlargeRect, nEnlargeTop, nEnlargeBottom ); } + bool IsOn() const { return m_bOn; } + bool IsChg() const { return m_bChg; } +}; + + +#ifdef DBG_UTIL + +class SwDbgOut +{ +protected: + VclPtr<OutputDevice> pOut; +public: + inline SwDbgOut( OutputDevice* pOutDev, const bool bOn ); +}; + +class DbgBackColor : public SwDbgOut +{ + Color aOldFillColor; +public: + DbgBackColor( OutputDevice* pOut, const bool bOn ); + ~DbgBackColor(); +}; + +class DbgRect : public SwDbgOut +{ +public: + DbgRect( OutputDevice* pOut, const tools::Rectangle &rRect, + const bool bOn, + Color eColor ); +}; + +inline SwDbgOut::SwDbgOut( OutputDevice* pOutDev, const bool bOn ) + :pOut( bOn ? pOutDev : nullptr ) +{ } + +inline DbgBackColor::DbgBackColor( OutputDevice* pOutDev, const bool bOn ) + :SwDbgOut( pOutDev, bOn ) +{ + if( pOut ) + { + aOldFillColor = pOut->GetFillColor(); + pOut->SetFillColor( COL_RED ); + } +} + +inline DbgBackColor::~DbgBackColor() +{ + if( pOut ) + { + pOut->SetFillColor( aOldFillColor ); + } +} + +inline DbgRect::DbgRect( OutputDevice* pOutDev, const tools::Rectangle &rRect, + const bool bOn, + Color eColor ) + : SwDbgOut( pOutDev, bOn ) +{ + if( pOut ) + { + const Color aColor( eColor ); + Color aLineColor = pOut->GetLineColor(); + pOut->SetLineColor( aColor ); + Color aFillColor = pOut->GetFillColor(); + pOut->SetFillColor( COL_TRANSPARENT ); + pOut->DrawRect( rRect ); + pOut->SetLineColor( aLineColor ); + pOut->SetFillColor( aFillColor ); + } +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/txttab.cxx b/sw/source/core/text/txttab.cxx new file mode 100644 index 0000000000..e7141aaec5 --- /dev/null +++ b/sw/source/core/text/txttab.cxx @@ -0,0 +1,663 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <hintids.hxx> +#include <comphelper/string.hxx> +#include <editeng/tstpitem.hxx> +#include <rtl/ustrbuf.hxx> +#include <IDocumentSettingAccess.hxx> +#include <doc.hxx> +#include <SwPortionHandler.hxx> + +#include <viewopt.hxx> +#include "portab.hxx" +#include "inftxt.hxx" +#include "itrform2.hxx" +#include <txtfrm.hxx> +#include "porfld.hxx" +#include <memory> + +/** + * #i24363# tab stops relative to indent + * + * Return the first tab stop that is > nSearchPos. + * If the tab stop is outside the print area, we + * return 0 if it is not the first tab stop. + */ +const SvxTabStop* SwLineInfo::GetTabStop(const SwTwips nSearchPos, SwTwips& nRight) const +{ + for( sal_uInt16 i = 0; i < m_oRuler->Count(); ++i ) + { + const SvxTabStop &rTabStop = m_oRuler->operator[](i); + if (nRight && rTabStop.GetTabPos() > nRight) + { + // Consider the first tabstop to always be in-bounds. + if (!i) + nRight = rTabStop.GetTabPos(); + return i ? nullptr : &rTabStop; + } + if( rTabStop.GetTabPos() > nSearchPos ) + { + if (!i && !nRight) + nRight = rTabStop.GetTabPos(); + return &rTabStop; + } + } + return nullptr; +} + +sal_uInt16 SwLineInfo::NumberOfTabStops() const +{ + return m_oRuler->Count(); +} + +SwTabPortion *SwTextFormatter::NewTabPortion( SwTextFormatInfo &rInf, bool bAuto ) const +{ + IDocumentSettingAccess const& rIDSA(rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess()); + const bool bTabOverSpacing = rIDSA.get(DocumentSettingId::TAB_OVER_SPACING); + const bool bTabsRelativeToIndent = rIDSA.get(DocumentSettingId::TABS_RELATIVE_TO_INDENT); + + // Update search location - since Center/Decimal tabstops' width is dependent on the following text. + SwTabPortion* pTmpLastTab = rInf.GetLastTab(); + if (pTmpLastTab && (pTmpLastTab->IsTabCenterPortion() || pTmpLastTab->IsTabDecimalPortion())) + pTmpLastTab->PostFormat(rInf); + + sal_Unicode cFill = 0; + sal_Unicode cDec = 0; + SvxTabAdjust eAdj; + + sal_uInt16 nNewTabPos; + bool bAutoTabStop = true; + { + const bool bRTL = m_pFrame->IsRightToLeft(); + // #i24363# tab stops relative to indent + // nTabLeft: The absolute value, the tab stops are relative to: Tabs origin. + + // #i91133# + const SwTwips nTabLeft = bRTL + ? m_pFrame->getFrameArea().Right() - + ( bTabsRelativeToIndent ? GetTabLeft() : 0 ) + : m_pFrame->getFrameArea().Left() + + ( bTabsRelativeToIndent ? GetTabLeft() : 0 ); + + // The absolute position, where we started the line formatting + SwTwips nLinePos = GetLeftMargin(); + if ( bRTL ) + { + Point aPoint( nLinePos, 0 ); + m_pFrame->SwitchLTRtoRTL( aPoint ); + nLinePos = aPoint.X(); + } + + // The current position, relative to the line start + SwTwips nTabPos = rInf.GetLastTab() ? rInf.GetLastTab()->GetTabPos() : 0; + if( nTabPos < rInf.X() ) + { + nTabPos = rInf.X(); + } + + // The current position in absolute coordinates + const SwTwips nCurrentAbsPos = bRTL ? + nLinePos - nTabPos : + nLinePos + nTabPos; + + SwTwips nMyRight; + if ( m_pFrame->IsVertLR() ) + nMyRight = Left(); + else + nMyRight = Right(); + + if ( m_pFrame->IsVertical() ) + { + Point aRightTop( nMyRight, m_pFrame->getFrameArea().Top() ); + m_pFrame->SwitchHorizontalToVertical( aRightTop ); + nMyRight = aRightTop.Y(); + } + + SwTwips nNextPos = 0; + bool bAbsoluteNextPos = false; + + // #i24363# tab stops relative to indent + // nSearchPos: The current position relative to the tabs origin + const SwTwips nSearchPos = bRTL ? + nTabLeft - nCurrentAbsPos : + nCurrentAbsPos - nTabLeft; + + // First, we examine the tab stops set at the paragraph style or + // any hard set tab stops: + // Note: If there are no user defined tab stops, there is always a + // default tab stop. + const SwTwips nOldRight = nMyRight; + // Accept left-tabstops beyond the paragraph margin for bTabOverSpacing + if (bTabOverSpacing) + nMyRight = 0; + const SvxTabStop* pTabStop = m_aLineInf.GetTabStop( nSearchPos, nMyRight ); + if (!nMyRight) + nMyRight = nOldRight; + if (pTabStop) + { + cFill = ' ' != pTabStop->GetFill() ? pTabStop->GetFill() : 0; + cDec = pTabStop->GetDecimal(); + eAdj = pTabStop->GetAdjustment(); + nNextPos = pTabStop->GetTabPos(); + if(!bTabsRelativeToIndent && eAdj == SvxTabAdjust::Default && nSearchPos < 0) + { + //calculate default tab position of default tabs in negative indent + nNextPos = ( nSearchPos / nNextPos ) * nNextPos; + } + else if (pTabStop->GetTabPos() > nMyRight + && pTabStop->GetAdjustment() != SvxTabAdjust::Left) + { + // A rather special situation. The tabstop found is: + // 1.) in a document compatible with MS formats + // 2.) not a left tabstop. + // 3.) not the first tabstop (in that case nMyRight was adjusted to match tabPos). + // 4.) beyond the end of the text area + // Therefore, they act like right-tabstops at the edge of the para area. + // This benefits DOCX 2013+, and doesn't hurt the earlier formats, + // since up till now these were just treated as automatic tabstops. + eAdj = SvxTabAdjust::Right; + bAbsoluteNextPos = true; + nNextPos = rInf.Width(); + } + bAutoTabStop = false; + } + else + { + sal_uInt16 nDefTabDist = m_aLineInf.GetDefTabStop(); + if( USHRT_MAX == nDefTabDist ) + { + const SvxTabStopItem& rTab = + m_pFrame->GetAttrSet()->GetPool()->GetDefaultItem( RES_PARATR_TABSTOP ); + if( rTab.Count() ) + nDefTabDist = o3tl::narrowing<sal_uInt16>(rTab[0].GetTabPos()); + else + nDefTabDist = SVX_TAB_DEFDIST; + m_aLineInf.SetDefTabStop( nDefTabDist ); + } + SwTwips nCount = nSearchPos; + + // Minimum tab stop width is 1 + if (nDefTabDist <= 0) + nDefTabDist = 1; + + nCount /= nDefTabDist; + nNextPos = ( nCount < 0 || ( !nCount && nSearchPos <= 0 ) ) + ? ( nCount * nDefTabDist ) + : ( ( nCount + 1 ) * nDefTabDist ); + + // --> FME 2004-09-21 #117919 Minimum tab stop width is 1 or 51 twips: + const SwTwips nMinimumTabWidth = m_pFrame->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::TAB_COMPAT) ? 0 : 50; + if( ( bRTL && nTabLeft - nNextPos >= nCurrentAbsPos - nMinimumTabWidth ) || + ( !bRTL && nNextPos + nTabLeft <= nCurrentAbsPos + nMinimumTabWidth ) ) + { + nNextPos += nDefTabDist; + } + cFill = 0; + eAdj = SvxTabAdjust::Left; + } + + // #i115705# - correction and refactoring: + // overrule determined next tab stop position in order to apply + // a tab stop at the left margin under the following conditions: + // - the new tab portion is inside the hanging indent + // - a tab stop at the left margin is allowed + // - the determined next tab stop is a default tab stop position OR + // the determined next tab stop is beyond the left margin + { + tools::Long nLeftMarginTabPos = 0; + { + if ( !bTabsRelativeToIndent ) + { + if ( bRTL ) + { + Point aPoint( Left(), 0 ); + m_pFrame->SwitchLTRtoRTL( aPoint ); + nLeftMarginTabPos = m_pFrame->getFrameArea().Right() - aPoint.X(); + } + else + { + nLeftMarginTabPos = Left() - m_pFrame->getFrameArea().Left(); + } + } + if( m_pCurr->HasForcedLeftMargin() ) + { + SwLinePortion* pPor = m_pCurr->GetNextPortion(); + while( pPor && !pPor->IsFlyPortion() ) + { + pPor = pPor->GetNextPortion(); + } + if ( pPor ) + { + nLeftMarginTabPos += pPor->Width(); + } + } + } + const bool bNewTabPortionInsideHangingIndent = + bRTL ? nCurrentAbsPos > nTabLeft - nLeftMarginTabPos + : nCurrentAbsPos < nTabLeft + nLeftMarginTabPos; + if ( bNewTabPortionInsideHangingIndent ) + { + // If the paragraph is not inside a list having a list tab stop following + // the list label or no further tab stop found in such a paragraph or + // the next tab stop position does not equal the list tab stop, + // a tab stop at the left margin can be applied. If this condition is + // not hold, it is overruled by compatibility option TAB_AT_LEFT_INDENT_FOR_PARA_IN_LIST. + const bool bTabAtLeftMarginAllowed = + ( !m_aLineInf.IsListTabStopIncluded() || + !pTabStop || + nNextPos != m_aLineInf.GetListTabStopPosition() ) || + // compatibility option TAB_AT_LEFT_INDENT_FOR_PARA_IN_LIST: + m_pFrame->GetDoc().getIDocumentSettingAccess(). + get(DocumentSettingId::TAB_AT_LEFT_INDENT_FOR_PARA_IN_LIST); + if ( bTabAtLeftMarginAllowed ) + { + if ( !pTabStop || eAdj == SvxTabAdjust::Default || + ( nNextPos > nLeftMarginTabPos ) ) + { + eAdj = SvxTabAdjust::Default; + cFill = 0; + nNextPos = nLeftMarginTabPos; + } + } + } + } + + if (!bAbsoluteNextPos) + nNextPos += bRTL ? nLinePos - nTabLeft : nTabLeft - nLinePos; + OSL_ENSURE( nNextPos >= 0, "GetTabStop: Don't go back!" ); + nNewTabPos = sal_uInt16(nNextPos); + } + + SwTabPortion *pTabPor = nullptr; + if ( bAuto ) + { + if ( SvxTabAdjust::Decimal == eAdj && + 1 == m_aLineInf.NumberOfTabStops() ) + pTabPor = new SwAutoTabDecimalPortion( nNewTabPos, cDec, cFill ); + } + else + { + switch( eAdj ) + { + case SvxTabAdjust::Right : + { + pTabPor = new SwTabRightPortion( nNewTabPos, cFill ); + break; + } + case SvxTabAdjust::Center : + { + pTabPor = new SwTabCenterPortion( nNewTabPos, cFill ); + break; + } + case SvxTabAdjust::Decimal : + { + pTabPor = new SwTabDecimalPortion( nNewTabPos, cDec, cFill ); + break; + } + default: + { + OSL_ENSURE( SvxTabAdjust::Left == eAdj || SvxTabAdjust::Default == eAdj, + "+SwTextFormatter::NewTabPortion: unknown adjustment" ); + pTabPor = new SwTabLeftPortion( nNewTabPos, cFill, bAutoTabStop ); + break; + } + } + } + if (pTabPor) + rInf.UpdateTabSeen(pTabPor->GetWhichPor()); + + return pTabPor; +} + +/** + * The base class is initialized without setting anything + */ +SwTabPortion::SwTabPortion( const sal_uInt16 nTabPosition, const sal_Unicode cFillChar, const bool bAutoTab ) + : m_nTabPos(nTabPosition), m_cFill(cFillChar), m_bAutoTabStop( bAutoTab ) +{ + mnLineLength = TextFrameIndex(1); + OSL_ENSURE(!IsFilled() || ' ' != m_cFill, "SwTabPortion::CTOR: blanks ?!"); + SetWhichPor( PortionType::Tab ); +} + +bool SwTabPortion::Format( SwTextFormatInfo &rInf ) +{ + SwTabPortion *pLastTab = rInf.GetLastTab(); + if( pLastTab == this ) + return PostFormat( rInf ); + if( pLastTab ) + pLastTab->PostFormat( rInf ); + return PreFormat( rInf ); +} + +void SwTabPortion::FormatEOL( SwTextFormatInfo &rInf ) +{ + if( rInf.GetLastTab() == this ) + PostFormat( rInf ); +} + +bool SwTabPortion::PreFormat( SwTextFormatInfo &rInf ) +{ + OSL_ENSURE( rInf.X() <= GetTabPos(), "SwTabPortion::PreFormat: rush hour" ); + + // Here we settle down ... + SetFix( o3tl::narrowing<sal_uInt16>(rInf.X()) ); + + IDocumentSettingAccess const& rIDSA(rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess()); + const bool bTabCompat = rIDSA.get(DocumentSettingId::TAB_COMPAT); + const bool bTabOverflow = rIDSA.get(DocumentSettingId::TAB_OVERFLOW); + const bool bTabOverMargin = rIDSA.get(DocumentSettingId::TAB_OVER_MARGIN); + const bool bTabOverSpacing = rIDSA.get(DocumentSettingId::TAB_OVER_SPACING); + const sal_Int32 nTextFrameWidth = rInf.GetTextFrame()->getFrameArea().Width(); + + // The minimal width of a tab is one blank at least. + // #i37686# In compatibility mode, the minimum width + // should be 1, even for non-left tab stops. + sal_uInt16 nMinimumTabWidth = 1; + if ( !bTabCompat ) + { + // #i89179# + // tab portion representing the list tab of a list label gets the + // same font as the corresponding number portion + std::optional< SwFontSave > oSave; + if ( GetLen() == TextFrameIndex(0) && + rInf.GetLast() && rInf.GetLast()->InNumberGrp() && + static_cast<SwNumberPortion*>(rInf.GetLast())->HasFont() ) + { + const SwFont* pNumberPortionFont = + static_cast<SwNumberPortion*>(rInf.GetLast())->GetFont(); + oSave.emplace( rInf, const_cast<SwFont*>(pNumberPortionFont) ); + } + OUString aTmp( ' ' ); + SwTextSizeInfo aInf( rInf, &aTmp ); + nMinimumTabWidth = aInf.GetTextSize().Width(); + } + PrtWidth( nMinimumTabWidth ); + + // Break tab stop to next line if: + // 1. Minimal width does not fit to line anymore. + // 2. An underflow event was called for the tab portion. + bool bFull = ( bTabCompat && rInf.IsUnderflow() ) || + ( rInf.Width() <= rInf.X() + PrtWidth() && rInf.X() <= rInf.Width() ) ; + + // #95477# Rotated tab stops get the width of one blank + const Degree10 nDir = rInf.GetFont()->GetOrientation( rInf.GetTextFrame()->IsVertical() ); + + if( ! bFull && 0_deg10 == nDir ) + { + const PortionType nWhich = GetWhichPor(); + switch( nWhich ) + { + case PortionType::TabRight: + case PortionType::TabDecimal: + case PortionType::TabCenter: + { + if( PortionType::TabDecimal == nWhich ) + rInf.SetTabDecimal( + static_cast<SwTabDecimalPortion*>(this)->GetTabDecimal()); + rInf.SetLastTab( this ); + break; + } + case PortionType::TabLeft: + { + // handle this case in PostFormat + if ((bTabOverMargin || bTabOverSpacing) && GetTabPos() > rInf.Width() + && (!m_bAutoTabStop || (!bTabOverMargin && rInf.X() > rInf.Width()))) + { + if (bTabOverMargin || GetTabPos() < nTextFrameWidth) + { + rInf.SetLastTab(this); + break; + } + else + { + assert(!bTabOverMargin && bTabOverSpacing && GetTabPos() >= nTextFrameWidth); + bFull = true; + break; + } + } + + PrtWidth( o3tl::narrowing<sal_uInt16>(GetTabPos() - rInf.X()) ); + bFull = rInf.Width() <= rInf.X() + PrtWidth(); + + // In tabulator compatibility mode, we reset the bFull flag + // if the tabulator is at the end of the paragraph and the + // tab stop position is outside the frame: + bool bAtParaEnd = rInf.GetIdx() + GetLen() == TextFrameIndex(rInf.GetText().getLength()); + if ( bFull && bTabCompat && + ( ( bTabOverflow && ( rInf.IsTabOverflow() || !m_bAutoTabStop ) ) || bAtParaEnd ) && + GetTabPos() >= nTextFrameWidth) + { + bFull = false; + if ( bTabOverflow && !m_bAutoTabStop ) + rInf.SetTabOverflow( true ); + } + + break; + } + default: OSL_ENSURE( false, "SwTabPortion::PreFormat: unknown adjustment" ); + } + } + + if( bFull ) + { + // We have to look for endless loops, if the width is smaller than one blank + if( rInf.GetIdx() == rInf.GetLineStart() && + // #119175# TabStop should be forced to current + // line if there is a fly reducing the line width: + !rInf.GetFly() ) + { + PrtWidth( o3tl::narrowing<sal_uInt16>(rInf.Width() - rInf.X()) ); + SetFixWidth( PrtWidth() ); + } + else + { + Height( 0 ); + Width( 0 ); + SetLen( TextFrameIndex(0) ); + SetAscent( 0 ); + SetNextPortion( nullptr ); //????? + } + return true; + } + else + { + // A trick with impact: The new Tabportions now behave like + // FlyFrames, located in the line - including adjustment ! + SetFixWidth( PrtWidth() ); + return false; + } +} + +bool SwTabPortion::PostFormat( SwTextFormatInfo &rInf ) +{ + bool bTabOverMargin = rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::TAB_OVER_MARGIN); + bool bTabOverSpacing = rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::TAB_OVER_SPACING); + if (rInf.GetTextFrame()->IsInSct()) + bTabOverMargin = false; + + // If the tab position is larger than the right margin, it gets scaled down by default. + // However, if compat mode enabled, we allow tabs to go over the margin: the rest of the paragraph is not broken into lines. + const sal_uInt16 nRight + = bTabOverMargin + ? GetTabPos() + : bTabOverSpacing + ? std::min<long>(GetTabPos(), rInf.GetTextFrame()->getFrameArea().Right()) + : std::min(GetTabPos(), rInf.Width()); + const SwLinePortion *pPor = GetNextPortion(); + + sal_uInt16 nPorWidth = 0; + while( pPor ) + { + nPorWidth = nPorWidth + pPor->Width(); + pPor = pPor->GetNextPortion(); + } + + const PortionType nWhich = GetWhichPor(); + const bool bTabCompat = rInf.GetTextFrame()->GetDoc().getIDocumentSettingAccess().get(DocumentSettingId::TAB_COMPAT); + + if ((bTabOverMargin || bTabOverSpacing) && PortionType::TabLeft == nWhich) + { + nPorWidth = 0; + } + + // #127428# Abandon dec. tab position if line is full + if ( bTabCompat && PortionType::TabDecimal == nWhich ) + { + sal_uInt16 nPrePorWidth = static_cast<const SwTabDecimalPortion*>(this)->GetWidthOfPortionsUpToDecimalPosition(); + + // no value was set => no decimal character was found + if ( USHRT_MAX != nPrePorWidth ) + { + if ( !bTabOverMargin && nPrePorWidth && nPorWidth - nPrePorWidth > rInf.Width() - nRight ) + { + nPrePorWidth += nPorWidth - nPrePorWidth - ( rInf.Width() - nRight ); + } + + nPorWidth = nPrePorWidth - 1; + } + } + + if( PortionType::TabCenter == nWhich ) + { + // centered tabs are problematic: + // We have to detect how much fits into the line. + sal_uInt16 nNewWidth = nPorWidth /2; + if (!bTabOverMargin && !bTabOverSpacing && nNewWidth > rInf.Width() - nRight) + nNewWidth = nPorWidth - (rInf.Width() - nRight); + nPorWidth = nNewWidth; + } + + const sal_uInt16 nDiffWidth = nRight - GetFix(); + + if( nDiffWidth > nPorWidth ) + { + const sal_uInt16 nOldWidth = GetFixWidth(); + const sal_uInt16 nAdjDiff = nDiffWidth - nPorWidth; + if( nAdjDiff > GetFixWidth() ) + PrtWidth( nAdjDiff ); + // Don't be afraid: we have to move rInf further. + // The right-tab till now only had the width of one blank. + // Now that we stretched, the difference had to be added to rInf.X() ! + rInf.X( rInf.X() + PrtWidth() - nOldWidth ); + } + SetFixWidth( PrtWidth() ); + // reset last values + rInf.SetLastTab(nullptr); + if( PortionType::TabDecimal == nWhich ) + rInf.SetTabDecimal(0); + + return rInf.Width() <= rInf.X(); +} + +/** + * Ex: LineIter::DrawTab() + */ +void SwTabPortion::Paint( const SwTextPaintInfo &rInf ) const +{ + // #i89179# + // tab portion representing the list tab of a list label gets the + // same font as the corresponding number portion + std::optional< SwFontSave > oSave; + bool bAfterNumbering = false; + if (GetLen() == TextFrameIndex(0)) + { + const SwLinePortion* pPrevPortion = + const_cast<SwTabPortion*>(this)->FindPrevPortion( rInf.GetParaPortion() ); + if ( pPrevPortion && + pPrevPortion->InNumberGrp() && + static_cast<const SwNumberPortion*>(pPrevPortion)->HasFont() ) + { + const SwFont* pNumberPortionFont = + static_cast<const SwNumberPortion*>(pPrevPortion)->GetFont(); + oSave.emplace( rInf, const_cast<SwFont*>(pNumberPortionFont) ); + bAfterNumbering = true; + } + } + rInf.DrawBackBrush( *this ); + if( !bAfterNumbering ) + rInf.DrawBorder( *this ); + + // do we have to repaint a post it portion? + if( rInf.OnWin() && mpNextPortion && !mpNextPortion->Width() ) + mpNextPortion->PrePaint( rInf, this ); + + // display special characters + if( rInf.OnWin() && rInf.GetOpt().IsTab() ) + { + // filled tabs are shaded in gray + if( IsFilled() ) + rInf.DrawViewOpt( *this, PortionType::Tab ); + else + rInf.DrawTab( *this ); + } + + // Tabs should be underlined at once + if( rInf.GetFont()->IsPaintBlank() ) + { + // Tabs with filling/filled tabs + const sal_uInt16 nCharWidth = rInf.GetTextSize(OUString(' ')).Width(); + + // Robust: + if( nCharWidth ) + { + // Always with kerning, also on printer! + sal_uInt16 nChar = Width() / nCharWidth; + OUStringBuffer aBuf(nChar); + comphelper::string::padToLength(aBuf, nChar, ' '); + rInf.DrawText(aBuf.makeStringAndClear(), *this, TextFrameIndex(0), + TextFrameIndex(nChar), true); + } + } + + // Display fill characters + if( !IsFilled() ) + return; + + // Tabs with filling/filled tabs + const sal_uInt16 nCharWidth = rInf.GetTextSize(OUString(m_cFill)).Width(); + OSL_ENSURE( nCharWidth, "!SwTabPortion::Paint: sophisticated tabchar" ); + + // Robust: + if( nCharWidth ) + { + // Always with kerning, also on printer! + sal_uInt16 nChar = Width() / nCharWidth; + if ( m_cFill == '_' ) + ++nChar; // to avoid gaps + OUStringBuffer aBuf(nChar); + comphelper::string::padToLength(aBuf, nChar, m_cFill); + rInf.DrawText(aBuf.makeStringAndClear(), *this, TextFrameIndex(0), + TextFrameIndex(nChar), true); + } +} + +void SwAutoTabDecimalPortion::Paint( const SwTextPaintInfo & ) const +{ +} + +void SwTabPortion::HandlePortion( SwPortionHandler& rPH ) const +{ + rPH.Text( GetLen(), GetWhichPor() ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/widorp.cxx b/sw/source/core/text/widorp.cxx new file mode 100644 index 0000000000..11e4193d7b --- /dev/null +++ b/sw/source/core/text/widorp.cxx @@ -0,0 +1,673 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <layfrm.hxx> +#include <ftnboss.hxx> +#include <ndtxt.hxx> +#include <paratr.hxx> +#include <editeng/orphitem.hxx> +#include <editeng/widwitem.hxx> +#include <editeng/keepitem.hxx> +#include <editeng/spltitem.hxx> +#include <frmatr.hxx> +#include <txtftn.hxx> +#include <fmtftn.hxx> +#include <rowfrm.hxx> + +#include "widorp.hxx" +#include <txtfrm.hxx> +#include "itrtxt.hxx" +#include <sectfrm.hxx> +#include <ftnfrm.hxx> +#include <pagefrm.hxx> +#include <IDocumentSettingAccess.hxx> +#include <sortedobjs.hxx> +#include <anchoredobject.hxx> +#include <flyfrm.hxx> + +#undef WIDOWTWIPS + +namespace +{ + +// A Follow on the same page as its master is nasty. +bool IsNastyFollow( const SwTextFrame *pFrame ) +{ + OSL_ENSURE( !pFrame->IsFollow() || !pFrame->GetPrev() || + static_cast<const SwTextFrame*>(pFrame->GetPrev())->GetFollow() == pFrame, + "IsNastyFollow: What is going on here?" ); + return pFrame->IsFollow() && pFrame->GetPrev(); +} + +} + +SwTextFrameBreak::SwTextFrameBreak( SwTextFrame *pNewFrame, const SwTwips nRst ) + : m_nRstHeight(nRst), m_pFrame(pNewFrame) +{ + SwSwapIfSwapped swap(m_pFrame); + SwRectFnSet aRectFnSet(m_pFrame); + m_nOrigin = aRectFnSet.GetPrtTop(*m_pFrame); + m_bKeep = !m_pFrame->IsMoveable() || IsNastyFollow( m_pFrame ); + if( !m_bKeep && m_pFrame->IsInSct() ) + { + const SwSectionFrame* const pSct = m_pFrame->FindSctFrame(); + m_bKeep = pSct->Lower()->IsColumnFrame() && !pSct->MoveAllowed( m_pFrame ); + } + m_bKeep = m_bKeep || !m_pFrame->GetTextNodeForParaProps()->GetSwAttrSet().GetSplit().GetValue() || + m_pFrame->GetTextNodeForParaProps()->GetSwAttrSet().GetKeep().GetValue(); + m_bBreak = false; + + if( !m_nRstHeight && !m_pFrame->IsFollow() && m_pFrame->IsInFootnote() && m_pFrame->HasPara() ) + { + m_nRstHeight = m_pFrame->GetFootnoteFrameHeight(); + m_nRstHeight += aRectFnSet.GetHeight(m_pFrame->getFramePrintArea()) - + aRectFnSet.GetHeight(m_pFrame->getFrameArea()); + if( m_nRstHeight < 0 ) + m_nRstHeight = 0; + } +} + +/** + * BP 18.6.93: Widows. + * In contrast to the first implementation the Widows are not calculated + * in advance but detected when formatting the split Follow. + * In Master the Widows-calculation is dropped completely + * (nWidows is manipulated). If the Follow detects that the + * Widows rule applies it sends a Prepare to its predecessor. + * A special problem is when the Widow rule applies but in Master + * there are some lines available. + * + * BP(22.07.92): Calculation of Widows and Orphans. + * The method returns true if one of the rules matches. + * + * One difficulty with Widows and different formats between + * Master- and Follow-Frame: + * Example: If the first column is 3cm and the second is 4cm and + * Widows is set to 3, the decision if the Widows rule matches can not + * be done until the Follow is formatted. Unfortunately this is crucial + * to decide if the whole paragraph goes to the next page or not. + */ +bool SwTextFrameBreak::IsInside( SwTextMargin const &rLine ) const +{ + bool bFit = false; + + SwSwapIfSwapped swap(m_pFrame); + SwRectFnSet aRectFnSet(m_pFrame); + // nOrigin is an absolute value, rLine refers to the swapped situation. + + SwTwips nTmpY; + if ( m_pFrame->IsVertical() ) + nTmpY = m_pFrame->SwitchHorizontalToVertical( rLine.Y() + rLine.GetLineHeight() ); + else + nTmpY = rLine.Y() + rLine.GetLineHeight(); + + SwTwips nLineHeight = aRectFnSet.YDiff( nTmpY , m_nOrigin ); + + // Calculate extra space for bottom border. + nLineHeight += aRectFnSet.GetBottomMargin(*m_pFrame); + + if( m_nRstHeight ) + bFit = m_nRstHeight >= nLineHeight; + else + { + // The Frame has a height to fit on the page. + SwTwips nHeight = + aRectFnSet.YDiff( aRectFnSet.GetPrtBottom(*m_pFrame->GetUpper()), m_nOrigin ); + SwTwips nDiff = nHeight - nLineHeight; + + // Hide whitespace may require not to insert a new page. + SwPageFrame* pPageFrame = m_pFrame->FindPageFrame(); + if (!pPageFrame->CheckPageHeightValidForHideWhitespace(nDiff)) + nDiff = 0; + + // If everything is inside the existing frame the result is true; + bFit = nDiff >= 0; + + // If it didn't fit, try to add the space of footnotes that are anchored + // in frames below (in next-chain of) this one as they will need to move + // forward anyway if this frame is split. + // - except if in tables (need to check if row is splittable? + // also, multiple columns looks difficult) + if (!bFit && !m_pFrame->IsInTab()) + { + if (SwFootnoteBossFrame const*const pBoss = m_pFrame->FindFootnoteBossFrame()) + { + if (SwFootnoteContFrame const*const pCont = pBoss->FindFootnoteCont()) + { + SwContentFrame const* pContent(m_pFrame); + while (pContent->HasFollow()) + { + pContent = pContent->GetFollow(); + } + // start with first text frame that isn't a follow + // (ignoring Keep attribute for now, MakeAll should handle it?) + pContent = pContent->GetNextContentFrame(); + ::std::set<SwContentFrame const*> nextFrames; + while (pBoss->IsAnLower(pContent)) + { + nextFrames.insert(pContent); + pContent = pContent->GetNextContentFrame(); + } + SwTwips nNextFootnotes(0); + for (SwFootnoteFrame const* pFootnote = static_cast<SwFootnoteFrame const*>(pCont->Lower()); + pFootnote != nullptr; + pFootnote = static_cast<SwFootnoteFrame const*>(pFootnote->GetNext())) + { + SwContentFrame const*const pAnchor = pFootnote->GetRef(); + if (nextFrames.find(pAnchor) != nextFrames.end()) + { + nNextFootnotes += aRectFnSet.GetHeight(pFootnote->getFrameArea()); + } + } + bFit = 0 <= nDiff + nNextFootnotes; + SAL_INFO_IF(bFit, "sw.core", "no text frame break because ignoring " + << nNextFootnotes << " footnote height"); + } + } + } + if (!bFit && rLine.MaybeHasHints() && m_pFrame->GetFollow() + // tdf#153319 RemoveFootnote only works if this frame doesn't + && !rLine.GetNext() // contain the footnote portion + // if using same footnote container as the follow, pointless to try? + && m_pFrame->FindFootnoteBossFrame() != m_pFrame->GetFollow()->FindFootnoteBossFrame()) + { + // possibly a footnote that is anchored beyond the end of this + // (the last) line is in the way, try to remove it and check again + m_pFrame->RemoveFootnote(rLine.GetEnd()); + nHeight = aRectFnSet.YDiff( aRectFnSet.GetPrtBottom(*m_pFrame->GetUpper()), m_nOrigin ); + bFit = nHeight >= nLineHeight; + } + if ( !bFit ) + { + if ( rLine.GetNext() && + m_pFrame->IsInTab() && !m_pFrame->GetFollow() && !m_pFrame->GetIndNext() ) + { + // add additional space taken as lower space as last content in a table + // for all text lines except the last one. + nHeight += m_pFrame->CalcAddLowerSpaceAsLastInTableCell(); + bFit = nHeight >= nLineHeight; + } + } + if( !bFit ) + { + // The LineHeight exceeds the current Frame height. + // Call a test Grow to detect if the Frame could + // grow the requested area. + nHeight += m_pFrame->GrowTst( LONG_MAX ); + + // The Grow() returns the height by which the Upper of the TextFrame + // would let the TextFrame grow. + // The TextFrame itself can grow as much as it wants. + bFit = nHeight >= nLineHeight; + } + } + + return bFit; +} + +bool SwTextFrameBreak::IsBreakNow( SwTextMargin &rLine ) +{ + SwSwapIfSwapped swap(m_pFrame); + + // bKeep is stronger than IsBreakNow() + // Is there enough space ? + if( m_bKeep || IsInside( rLine ) ) + m_bBreak = false; + else + { + /* This class assumes that the SwTextMargin is processed from Top to + * Bottom. Because of performance reasons we stop splitting in the + * following cases: + * If only one line does not fit. + * Special case: with DummyPortions there is LineNr == 1, though we + * want to split. + */ + // Include DropLines + + bool bFirstLine = 1 == rLine.GetLineNr() && !rLine.GetPrev(); + m_bBreak = true; + + if (bFirstLine && m_pFrame->IsEmptyWithSplitFly()) + { + // Not really the first line, visually we may have a previous line (including the fly + // frame) already. + bFirstLine = false; + } + + if( ( bFirstLine && m_pFrame->GetIndPrev() ) + || ( rLine.GetLineNr() <= rLine.GetDropLines() ) ) + { + m_bKeep = true; + m_bBreak = false; + } + else if(bFirstLine && m_pFrame->IsInFootnote() && !m_pFrame->FindFootnoteFrame()->GetPrev()) + { + SwLayoutFrame* pTmp = m_pFrame->FindFootnoteBossFrame()->FindBodyCont(); + if( !pTmp || !pTmp->Lower() ) + m_bBreak = false; + } + } + + return m_bBreak; +} + +void SwTextFrameBreak::SetRstHeight( const SwTextMargin &rLine ) +{ + // Consider bottom margin + SwRectFnSet aRectFnSet(m_pFrame); + + m_nRstHeight = aRectFnSet.GetBottomMargin(*m_pFrame); + + if ( aRectFnSet.IsVert() ) + { + if ( m_pFrame->IsVertLR() ) + m_nRstHeight = aRectFnSet.YDiff( m_pFrame->SwitchHorizontalToVertical( rLine.Y() ) , m_nOrigin ); + else + m_nRstHeight += m_nOrigin - m_pFrame->SwitchHorizontalToVertical( rLine.Y() ); + } + else + m_nRstHeight += rLine.Y() - m_nOrigin; +} + +WidowsAndOrphans::WidowsAndOrphans( SwTextFrame *pNewFrame, const SwTwips nRst, + bool bChkKeep ) + : SwTextFrameBreak( pNewFrame, nRst ), m_nWidLines( 0 ), m_nOrphLines( 0 ) +{ + SwSwapIfSwapped swap(m_pFrame); + + if( m_bKeep ) + { + // If paragraph should not be split but is larger than + // the page, then bKeep is overruled. + if( bChkKeep && !m_pFrame->GetPrev() && !m_pFrame->IsInFootnote() && + m_pFrame->IsMoveable() && + ( !m_pFrame->IsInSct() || m_pFrame->FindSctFrame()->MoveAllowed(m_pFrame) ) ) + m_bKeep = false; + // Even if Keep is set, Orphans has to be respected. + // e.g. if there are chained frames where a Follow in the last frame + // receives a Keep, because it is not (forward) movable - + // nevertheless the paragraph can request lines from the Master + // because of the Orphan rule. + if( m_pFrame->IsFollow() ) + m_nWidLines = m_pFrame->GetTextNodeForParaProps()->GetSwAttrSet().GetWidows().GetValue(); + } + else + { + const SwAttrSet& rSet = m_pFrame->GetTextNodeForParaProps()->GetSwAttrSet(); + const SvxOrphansItem &rOrph = rSet.GetOrphans(); + if ( rOrph.GetValue() > 1 ) + m_nOrphLines = rOrph.GetValue(); + if ( m_pFrame->IsFollow() ) + m_nWidLines = rSet.GetWidows().GetValue(); + + } + + if ( !(m_bKeep || m_nWidLines || m_nOrphLines) ) + return; + + bool bResetFlags = false; + + bool bWordTableCell = false; + if (m_pFrame->IsInFly()) + { + // Enable widow / orphan control in Word-style table cells in split rows, at least inside + // flys. + const SwDoc& rDoc = m_pFrame->GetTextNodeForParaProps()->GetDoc(); + const IDocumentSettingAccess& rIDSA = rDoc.getIDocumentSettingAccess(); + bWordTableCell = rIDSA.get(DocumentSettingId::TABLE_ROW_KEEP); + } + + if ( m_pFrame->IsInTab() && !bWordTableCell ) + { + // For compatibility reasons, we disable Keep/Widows/Orphans + // inside splittable row frames: + if ( m_pFrame->GetNextCellLeaf() || m_pFrame->IsInFollowFlowRow() ) + { + const SwFrame* pTmpFrame = m_pFrame->GetUpper(); + while ( !pTmpFrame->IsRowFrame() ) + pTmpFrame = pTmpFrame->GetUpper(); + if ( static_cast<const SwRowFrame*>(pTmpFrame)->IsRowSplitAllowed() ) + bResetFlags = true; + } + } + + if( m_pFrame->IsInFootnote() && !m_pFrame->GetIndPrev() ) + { + // Inside of footnotes there are good reasons to turn off the Keep attribute + // as well as Widows/Orphans. + SwFootnoteFrame *pFootnote = m_pFrame->FindFootnoteFrame(); + const bool bFt = !pFootnote->GetAttr()->GetFootnote().IsEndNote(); + if( !pFootnote->GetPrev() && + pFootnote->FindFootnoteBossFrame( bFt ) != pFootnote->GetRef()->FindFootnoteBossFrame( bFt ) + && ( !m_pFrame->IsInSct() || m_pFrame->FindSctFrame()->MoveAllowed(m_pFrame) ) ) + { + bResetFlags = true; + } + } + + if ( bResetFlags ) + { + m_bKeep = false; + m_nOrphLines = 0; + m_nWidLines = 0; + } +} + +/** + * The Find*-Methods do not only search, but adjust the SwTextMargin to the + * line where the paragraph should have a break and truncate the paragraph there. + * FindBreak() + */ +bool WidowsAndOrphans::FindBreak( SwTextFrame *pFrame, SwTextMargin &rLine, + bool bHasToFit ) +{ + // i#16128 - Why member <pFrame> _*and*_ parameter <pFrame>?? + // Thus, assertion on situation, that these are different to figure out why. + OSL_ENSURE( m_pFrame == pFrame, "<WidowsAndOrphans::FindBreak> - pFrame != pFrame" ); + + SwSwapIfSwapped swap(m_pFrame); + + bool bRet = true; + sal_uInt16 nOldOrphans = m_nOrphLines; + if( bHasToFit ) + m_nOrphLines = 0; + rLine.Bottom(); + + if( !IsBreakNowWidAndOrp( rLine ) ) + bRet = false; + if( !FindWidows( pFrame, rLine ) ) + { + bool bBack = false; + + while( IsBreakNowWidAndOrp( rLine ) ) + { + if( rLine.PrevLine() ) + bBack = true; + else + break; + } + // Usually Orphans are not taken into account for HasToFit. + // But if Dummy-Lines are concerned and the Orphans rule is violated + // we make an exception: We leave behind one Dummyline and take + // the whole text to the next page/column. + if( rLine.GetLineNr() <= nOldOrphans && + rLine.GetInfo().GetParaPortion()->IsDummy() && + ( ( bHasToFit && bRet ) || IsBreakNow( rLine ) ) ) + rLine.Top(); + + rLine.TruncLines( true ); + bRet = bBack; + } + m_nOrphLines = nOldOrphans; + + return bRet; +} + +/** + * FindWidows positions the SwTextMargin of the Master to the line where to + * break by examining and formatting the Follow. + * Returns true if the Widows-rule matches, that means that the + * paragraph should not be split (keep) ! + */ +bool WidowsAndOrphans::FindWidows( SwTextFrame *pFrame, SwTextMargin &rLine ) +{ + OSL_ENSURE( ! pFrame->IsVertical() || ! pFrame->IsSwapped(), + "WidowsAndOrphans::FindWidows with swapped frame" ); + + if( !m_nWidLines || !pFrame->IsFollow() ) + return false; + + rLine.Bottom(); + + // We can still cut something off + SwTextFrame *pMaster = pFrame->FindMaster(); + OSL_ENSURE(pMaster, "+WidowsAndOrphans::FindWidows: Widows in a master?"); + if( !pMaster ) + return false; + + // If the first line of the Follow does not fit, the master + // probably is full of Dummies. In this case a PrepareHint::Widows would be fatal. + if( pMaster->GetOffset() == pFrame->GetOffset() ) + return false; + + // Remaining height of the master + SwRectFnSet aRectFnSet(pFrame); + + const SwTwips nDocPrtTop = aRectFnSet.GetPrtTop(*pFrame); + SwTwips nOldHeight; + SwTwips nTmpY = rLine.Y() + rLine.GetLineHeight(); + + if ( aRectFnSet.IsVert() ) + { + nTmpY = pFrame->SwitchHorizontalToVertical( nTmpY ); + nOldHeight = -aRectFnSet.GetHeight(pFrame->getFramePrintArea()); + } + else + nOldHeight = aRectFnSet.GetHeight(pFrame->getFramePrintArea()); + + const SwTwips nChg = aRectFnSet.YDiff( nTmpY, nDocPrtTop + nOldHeight ); + + // below the Widows-threshold... + if( rLine.GetLineNr() >= m_nWidLines ) + { + // Follow to Master I + // If the Follow *grows*, there is the chance for the Master to + // receive lines, that it was forced to hand over to the Follow lately: + // Prepare(Need); check that below nChg! + // (0W, 2O, 2M, 2F) + 1F = 3M, 2F + if( rLine.GetLineNr() > m_nWidLines && pFrame->IsJustWidow() ) + { + // If the Master is locked, it has probably just donated a line + // to us, we don't return that just because we turned it into + // multiple lines (e.g. via frames). + if( !pMaster->IsLocked() && pMaster->GetUpper() ) + { + const SwTwips nTmpRstHeight = aRectFnSet.BottomDist( pMaster->getFrameArea(), + aRectFnSet.GetPrtBottom(*pMaster->GetUpper()) ); + if ( nTmpRstHeight >= + rLine.GetInfo().GetParaPortion()->Height() ) + { + pMaster->Prepare( PrepareHint::AdjustSizeWithoutFormatting ); + pMaster->InvalidateSize_(); + pMaster->InvalidatePage(); + } + } + + pFrame->SetJustWidow( false ); + } + return false; + } + + // Follow to Master II + // If the Follow *shrinks*, maybe the Master can absorb the whole Orphan. + // (0W, 2O, 2M, 1F) - 1F = 3M, 0F -> PrepareHint::AdjustSizeWithoutFormatting + // (0W, 2O, 3M, 2F) - 1F = 2M, 2F -> PrepareHint::Widows + + if( 0 > nChg && !pMaster->IsLocked() && pMaster->GetUpper() ) + { + SwTwips nTmpRstHeight = aRectFnSet.BottomDist( pMaster->getFrameArea(), + aRectFnSet.GetPrtBottom(*pMaster->GetUpper()) ); + if( nTmpRstHeight >= rLine.GetInfo().GetParaPortion()->Height() ) + { + pMaster->Prepare( PrepareHint::AdjustSizeWithoutFormatting ); + pMaster->InvalidateSize_(); + pMaster->InvalidatePage(); + pFrame->SetJustWidow( false ); + return false; + } + } + + // Master to Follow + // If the Follow contains fewer lines than Widows after formatting, + // we still can move over some lines from the Master. If this triggers + // the Orphans rule of the Master, the Master frame must be Grow()n + // in its CalcPreps(), such that it won't fit onto its page anymore. + // But if the Master Frame can still lose a few lines, we need to + // do a Shrink() in the CalcPreps(); the Follow with the Widows then + // moves onto the page of the Master, but remains unsplit, so that + // it (finally) moves onto the next page. So much for the theory! + // + // We only request one line at a time for now, because a Master's line + // could result in multiple lines for us. + // Therefore, the CalcFollow() remains in control until the Follow got all + // necessary lines. + sal_Int32 nNeed = 1; // was: nWidLines - rLine.GetLineNr(); + + // Special case: Master cannot give lines to follow + // i#91421 + if ( !pMaster->GetIndPrev() ) + { + pMaster->ChgThisLines(); + sal_Int32 nLines = pMaster->GetThisLines(); + if(nLines == 0 && pMaster->HasPara()) + { + const SwParaPortion *pMasterPara = pMaster->GetPara(); + if(pMasterPara && pMasterPara->GetNext()) + nLines = 2; + } + if( nLines <= nNeed ) + return false; + + if (pFrame->IsInTab()) + { + const SwFrame* pRow = pFrame; + while (pRow && !pRow->IsRowFrame()) + { + pRow = pRow->GetUpper(); + } + + if (pRow && pRow->HasFixSize()) + { + // This is a follow frame and our side is fixed. + const SwAttrSet& rSet = pFrame->GetTextNodeForParaProps()->GetSwAttrSet(); + const SvxOrphansItem& rOrph = rSet.GetOrphans(); + if (nLines <= static_cast<sal_Int32>(rOrph.GetValue())) + { + // If the master gives us a line as part of widow control, then its orphan + // control will move everything to the follow, which is worse than having no + // widow / orphan control at all. Don't send a Widows prepare hint, in this + // case. + return true; + } + } + } + } + + pMaster->Prepare( PrepareHint::Widows, static_cast<void*>(&nNeed) ); + return true; +} + +namespace sw { + +auto FindNonFlyPortion(SwLineLayout const& rLine) -> bool +{ + for (SwLinePortion const* pPortion = rLine.GetFirstPortion(); + pPortion; pPortion = pPortion->GetNextPortion()) + { + switch (pPortion->GetWhichPor()) + { + case PortionType::Fly: + case PortionType::Glue: + case PortionType::Margin: + break; + default: + { + return true; + } + } + } + return false; +}; + +} // namespace sw + +bool WidowsAndOrphans::WouldFit( SwTextMargin &rLine, SwTwips &rMaxHeight, bool bTst, bool bMoveBwd ) +{ + // Here it does not matter, if pFrame is swapped or not. + // IsInside() takes care of itself + + // We expect that rLine is set to the last line + OSL_ENSURE( !rLine.GetNext(), "WouldFit: aLine::Bottom missed!" ); + sal_Int32 nLineCnt = rLine.GetLineNr(); + + // First satisfy the Orphans-rule and the wish for initials ... + const sal_uInt16 nMinLines = std::max( GetOrphansLines(), rLine.GetDropLines() ); + if ( nLineCnt < nMinLines ) + return false; + + rLine.Top(); + SwTwips nLineSum = rLine.GetLineHeight(); + + // tdf#146500 for MoveBwd(), want at least 1 line with non-fly + bool hasNonFly(!bMoveBwd); + if (!hasNonFly) + { + hasNonFly = ::sw::FindNonFlyPortion(*rLine.GetCurr()); + } + while (nMinLines > rLine.GetLineNr() || !hasNonFly) + { + if( !rLine.NextLine() ) + { + if (nMinLines > rLine.GetLineNr()) + return false; + else + break; + } + nLineSum += rLine.GetLineHeight(); + if (!hasNonFly) + { + hasNonFly = ::sw::FindNonFlyPortion(*rLine.GetCurr()); + } + } + + // We do not fit + if( !IsInside( rLine ) ) + return false; + + // Check the Widows-rule + if( !m_nWidLines && !m_pFrame->IsFollow() ) + { + // Usually we only have to check for Widows if we are a Follow. + // On WouldFit the rule has to be checked for the Master too, + // because we are just in the middle of calculating the break. + // In Ctor of WidowsAndOrphans the nWidLines are only calced for + // Follows from the AttrSet - so we catch up now: + const SwAttrSet& rSet = m_pFrame->GetTextNodeForParaProps()->GetSwAttrSet(); + m_nWidLines = rSet.GetWidows().GetValue(); + } + + // After Orphans/Initials, do enough lines remain for Widows? + // If we are currently doing a test formatting, we may not + // consider the widows rule for two reasons: + // 1. The columns may have different widths. + // Widow lines would have wrong width. + // 2. Test formatting is only done up to the given space. + // we do not have any lines for widows at all. + if( bTst || nLineCnt - nMinLines >= m_nWidLines ) + { + if( rMaxHeight >= nLineSum ) + { + rMaxHeight -= nLineSum; + return true; + } + } + return false; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/widorp.hxx b/sw/source/core/text/widorp.hxx new file mode 100644 index 0000000000..996a7fc913 --- /dev/null +++ b/sw/source/core/text/widorp.hxx @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <swtypes.hxx> +#include "itrtxt.hxx" + +class SwTextFrame; + +class SwTextFrameBreak +{ +private: + SwTwips m_nRstHeight; + SwTwips m_nOrigin; +protected: + SwTextFrame *m_pFrame; + bool m_bBreak; + bool m_bKeep; +public: + SwTextFrameBreak( SwTextFrame *pFrame, const SwTwips nRst = 0 ); + bool IsBreakNow( SwTextMargin &rLine ); + bool IsKeepAlways() const { return m_bKeep; } + + void SetKeep( const bool bNew ) { m_bKeep = bNew; } + + bool IsInside( SwTextMargin const &rLine ) const; + + // In order to be able to handle special cases with Footnote. + // SetRstHeight sets the rest height for SwTextFrameBreak. This is needed + // to call TruncLines() without IsBreakNow() returning another value. + // We assume that rLine is pointing to the last non-fitting line. + + void SetRstHeight( const SwTextMargin &rLine ); +}; + +class WidowsAndOrphans : public SwTextFrameBreak +{ +private: + sal_uInt16 m_nWidLines, m_nOrphLines; + +public: + WidowsAndOrphans( SwTextFrame *pFrame, const SwTwips nRst = 0, + bool bCheckKeep = true ); + bool FindWidows( SwTextFrame *pFrame, SwTextMargin &rLine ); + sal_uInt16 GetOrphansLines() const + { return m_nOrphLines; } + void ClrOrphLines(){ m_nOrphLines = 0; } + + bool FindBreak( SwTextFrame *pFrame, SwTextMargin &rLine, bool bHasToFit ); + bool WouldFit( SwTextMargin &rLine, SwTwips &rMaxHeight, bool bTest, bool bMoveBwd ); + // i#16128 - This method is named this way to avoid confusion with + // base class method <SwTextFrameBreak::IsBreakNow>, which isn't virtual. + bool IsBreakNowWidAndOrp( SwTextMargin &rLine ) + { + bool isOnFirstLine = (rLine.GetLineNr() == 1 && !rLine.GetPrev()); + if ( isOnFirstLine && rLine.GetCurr()->IsDummy()) { + return IsBreakNow( rLine ); + } + if ( rLine.GetLineNr() > m_nOrphLines ) { + return IsBreakNow( rLine ); + } + return false; + } +}; + +namespace sw { + +auto FindNonFlyPortion(SwLineLayout const& rLine) -> bool; + +} // namespace sw + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/wrong.cxx b/sw/source/core/text/wrong.cxx new file mode 100644 index 0000000000..00be8d5fac --- /dev/null +++ b/sw/source/core/text/wrong.cxx @@ -0,0 +1,938 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <swtypes.hxx> + +#include <SwGrammarMarkUp.hxx> +#include <ndtxt.hxx> +#include <txtfrm.hxx> +#include <utility> + +#include <osl/diagnose.h> + +SwWrongArea::SwWrongArea( OUString aType, WrongListType listType, + css::uno::Reference< css::container::XStringKeyMap > const & xPropertyBag, + sal_Int32 nPos, + sal_Int32 nLen) +: maType(std::move(aType)), mxPropertyBag(xPropertyBag), mnPos(nPos), mnLen(nLen), mpSubList(nullptr) +{ + mColor = getWrongAreaColor(listType, xPropertyBag); + mLineType = getWrongAreaLineType(listType, xPropertyBag); +} + +SwWrongArea::SwWrongArea( OUString aType, + css::uno::Reference< css::container::XStringKeyMap > const & xPropertyBag, + sal_Int32 nPos, + sal_Int32 nLen, + SwWrongList* pSubList) +: maType(std::move(aType)), mxPropertyBag(xPropertyBag), mnPos(nPos), mnLen(nLen), mpSubList(pSubList), mLineType(WRONGAREA_NONE) +{ + if (pSubList != nullptr) + { + mColor = getWrongAreaColor(pSubList->GetWrongListType(), xPropertyBag); + mLineType = getWrongAreaLineType(pSubList->GetWrongListType(), xPropertyBag); + } +} + +SwWrongList::SwWrongList( WrongListType eType ) : + meType (eType), + mnBeginInvalid(COMPLETE_STRING), // everything correct... (the invalid area starts beyond the string) + mnEndInvalid (COMPLETE_STRING) +{ + maList.reserve( 5 ); +} + +SwWrongList::~SwWrongList() +{ + ClearList(); +} + +SwWrongList* SwWrongList::Clone() +{ + SwWrongList* pClone = new SwWrongList( meType ); + pClone->CopyFrom( *this ); + return pClone; +} + +void SwWrongList::CopyFrom( const SwWrongList& rCopy ) +{ + maList = rCopy.maList; + meType = rCopy.meType; + mnBeginInvalid = rCopy.mnBeginInvalid; + mnEndInvalid = rCopy.mnEndInvalid; + for(SwWrongArea & i : maList) + { + if( i.mpSubList ) + i.mpSubList = i.mpSubList->Clone(); + } +} + +void SwWrongList::ClearList() +{ + for (SwWrongArea & i : maList) + { + delete i.mpSubList; + i.mpSubList = nullptr; + } + maList.clear(); +} + +/** If a word is incorrectly selected, this method returns begin and length of it. + + @param[in,out] rChk starting position of the word to check + @param[out] rLn length of the word + + @return <true> if incorrectly selected, <false> otherwise + */ +bool SwWrongList::InWrongWord( sal_Int32 &rChk, sal_Int32 &rLn ) const +{ + const sal_uInt16 nPos = GetWrongPos( rChk ); + if ( nPos >= Count() ) + return false; + const sal_Int32 nWrPos = Pos( nPos ); + if ( nWrPos <= rChk ) + { + rLn = Len( nPos ); + if( nWrPos + rLn <= rChk ) + return false; + rChk = nWrPos; + return true; + } + return false; +} + +/** Calculate first incorrectly selected area. + + @param[in,out] rChk starting position of the word to check + @param[in,out] rLn length of the word + + @return <true> if incorrectly selected area was found, <false> otherwise + */ +bool SwWrongList::Check( sal_Int32 &rChk, sal_Int32 &rLn ) const +{ + sal_uInt16 nPos = GetWrongPos( rChk ); + rLn += rChk; + + if( nPos == Count() ) + return false; + + sal_Int32 nWrPos = Pos( nPos ); + sal_Int32 nEnd = nWrPos + Len( nPos ); + if( nEnd == rChk ) + { + ++nPos; + if( nPos == Count() ) + return false; + + nWrPos = Pos( nPos ); + nEnd = nWrPos + Len( nPos ); + } + if( nEnd > rChk && nWrPos < rLn ) + { + if( nWrPos > rChk ) + rChk = nWrPos; + if( nEnd < rLn ) + rLn = nEnd; + rLn -= rChk; + return 0 != rLn; + } + return false; +} + +/** Find next incorrectly selected position. + + @param[in] rChk starting position of the word to check + + @return starting position of incorrectly selected area, <COMPLETE_STRING> otherwise + */ +sal_Int32 SwWrongList::NextWrong( sal_Int32 nChk ) const +{ + sal_Int32 nRet = COMPLETE_STRING; + sal_uInt16 nPos = GetWrongPos( nChk ); + if( nPos < Count() ) + { + nRet = Pos( nPos ); + if( nRet < nChk && nRet + Len( nPos ) <= nChk ) + { + if( ++nPos < Count() ) + nRet = Pos( nPos ); + else + nRet = COMPLETE_STRING; + } + } + if( nRet > GetBeginInv() && nChk < GetEndInv() ) + nRet = std::max(nChk, GetBeginInv()); + return nRet; +} + +/** Find the first position that is greater or equal to the given value. + + @note Resulting position might be behind the last element of the array. + @param[in] nValue value for comparison + + @return first position that is greater or equal to the given value + */ +sal_uInt16 SwWrongList::GetWrongPos( sal_Int32 nValue ) const +{ + sal_uInt16 nMax = Count(); + sal_uInt16 nMin = 0; + + if( nMax > 0 ) + { + // For smart tag lists, we may not use a binary search. We return the + // position of the first smart tag which covers nValue + if ( !maList[0].maType.isEmpty() || maList[0].mpSubList ) + { + auto aIter = std::find_if(maList.begin(), maList.end(), + [nValue](const SwWrongArea& rST) { + return (rST.mnPos <= nValue && nValue < rST.mnPos + rST.mnLen) + || (rST.mnPos > nValue); + }); + return o3tl::narrowing<sal_uInt16>(std::distance(maList.begin(), aIter)); + } + + --nMax; + sal_uInt16 nMid = 0; + while( nMin <= nMax ) + { + nMid = nMin + ( nMax - nMin ) / 2; + const sal_Int32 nTmp = Pos( nMid ); + if( nTmp == nValue ) + { + nMin = nMid; + break; + } + else if( nTmp < nValue ) + { + if( nTmp + Len( nMid ) >= nValue ) + { + nMin = nMid; + break; + } + nMin = nMid + 1; + } + else if( nMid == 0 ) + { + break; + } + else + nMax = nMid - 1; + } + } + + // nMin now points to an index i into the wrong list which + // 1. nValue is inside [ Area[i].pos, Area[i].pos + Area[i].len ] (inclusive!!!) + // 2. nValue < Area[i].pos + + return nMin; +} + +void SwWrongList::Invalidate_( sal_Int32 nBegin, sal_Int32 nEnd ) +{ + if ( nBegin < GetBeginInv() ) + mnBeginInvalid = nBegin; + if ( nEnd > GetEndInv() || GetEndInv() == COMPLETE_STRING ) + mnEndInvalid = nEnd; +} + +void SwWrongList::SetInvalid( sal_Int32 nBegin, sal_Int32 nEnd ) +{ + mnBeginInvalid = nBegin; + mnEndInvalid = nEnd; +} + +/** Change all values after the given position. + + Needed after insert/deletion of characters. + + @param nPos position after that everything should be changed + @param nDiff amount how much the positions should be moved + */ +void SwWrongList::Move( sal_Int32 nPos, sal_Int32 nDiff ) +{ + sal_uInt16 i = GetWrongPos( nPos ); + if( nDiff < 0 ) + { + const sal_Int32 nEnd = nPos - nDiff; + sal_uInt16 nLst = i; + bool bJump = false; + while( nLst < Count() && Pos( nLst ) < nEnd ) + ++nLst; + if( nLst > i ) + { + const sal_Int32 nWrPos = Pos( nLst - 1 ); + if ( nWrPos <= nPos ) + { + sal_Int32 nWrLen = Len( nLst - 1 ); + // calculate new length of word + nWrLen = ( nEnd > nWrPos + nWrLen ) ? + nPos - nWrPos : + nWrLen + nDiff; + if( nWrLen ) + { + maList[--nLst].mnLen = nWrLen; + bJump = true; + } + } + } + Remove( i, nLst - i ); + + if ( bJump ) + ++i; + if( COMPLETE_STRING == GetBeginInv() ) + SetInvalid( nPos ? nPos - 1 : nPos, nPos + 1 ); + else + { + ShiftLeft( mnBeginInvalid, nPos, nEnd ); + if( mnEndInvalid != COMPLETE_STRING ) + ShiftLeft( mnEndInvalid, nPos, nEnd ); + Invalidate_( nPos ? nPos - 1 : nPos, nPos + 1 ); + } + } + else + { + const sal_Int32 nEnd = nPos + nDiff; + if( COMPLETE_STRING != GetBeginInv() ) + { + if( mnBeginInvalid > nPos ) + mnBeginInvalid += nDiff; + if( mnEndInvalid >= nPos && mnEndInvalid != COMPLETE_STRING ) + mnEndInvalid += nDiff; + } + // If the pointer is in the middle of a wrong word, + // invalidation must happen from the beginning of that word. + if( i < Count() ) + { + const sal_Int32 nWrPos = Pos( i ); + if (nPos >= nWrPos) + { + Invalidate( nWrPos, nEnd ); + const sal_Int32 nWrLen = Len( i ) + nDiff; + maList[i++].mnLen = nWrLen; + Invalidate( nWrPos, nWrPos + nWrLen ); + } + } + else + Invalidate( nPos, nEnd ); + } + while( i < Count() ) + { + maList[i++].mnPos += nDiff; + } +} + +// TODO: Complete documentation +/** Remove given range of entries + + For a given range [nPos, nPos + nLen[ and an index nIndex, this function + basically counts the number of SwWrongArea entries starting with nIndex + up to nPos + nLen. All these entries are removed. + + @param rStart ??? + @param rEnd ??? + @param nPos starting position of the range + @param nLen length of the range + @param nIndex index to start lookup at + @param nCursorPos ??? + + @return <true> if ??? + */ +auto SwWrongList::Fresh( sal_Int32 &rStart, sal_Int32 &rEnd, sal_Int32 nPos, + sal_Int32 nLen, sal_uInt16 nIndex, sal_Int32 nCursorPos ) -> FreshState +{ + // length of word must be greater than 0 + // only report a spelling error if the cursor position is outside the word, + // so that the user is not annoyed while typing + FreshState eRet = nLen + ? (nCursorPos > nPos + nLen || nCursorPos < nPos) + ? FreshState::FRESH + : FreshState::CURSOR + : FreshState::NOTHING; + + sal_Int32 nWrPos = 0; + sal_Int32 nWrEnd = rEnd; + sal_uInt16 nCnt = nIndex; + if( nCnt < Count() ) + { + nWrPos = Pos( nCnt ); + if( nWrPos < nPos && rStart > nWrPos ) + rStart = nWrPos; + } + + while( nCnt < Count() ) + { + nWrPos = Pos( nCnt ); + if ( nWrPos >= nPos ) + break; + nWrEnd = nWrPos + Len( nCnt++ ); + } + + if( nCnt < Count() && nWrPos == nPos && Len( nCnt ) == nLen ) + { + ++nCnt; + eRet = FreshState::FRESH; + } + else + { + if (FreshState::FRESH == eRet) + { + if( rStart > nPos ) + rStart = nPos; + nWrEnd = nPos + nLen; + } + } + + nPos += nLen; + + if( nCnt < Count() ) + { + nWrPos = Pos( nCnt ); + if( nWrPos < nPos && rStart > nWrPos ) + rStart = nWrPos; + } + + while( nCnt < Count() ) + { + nWrPos = Pos( nCnt ); + if ( nWrPos >= nPos ) + break; + nWrEnd = nWrPos + Len( nCnt++ ); + } + + if( rEnd < nWrEnd ) + rEnd = nWrEnd; + + Remove( nIndex, nCnt - nIndex ); + + return eRet; +} + +void SwWrongList::Invalidate( sal_Int32 nBegin, sal_Int32 nEnd ) +{ + if (COMPLETE_STRING == GetBeginInv()) + SetInvalid( nBegin, nEnd ); + else + Invalidate_( nBegin, nEnd ); +} + +bool SwWrongList::InvalidateWrong( ) +{ + if( Count() ) + { + const sal_Int32 nFirst = Pos( 0 ); + const sal_Int32 nLast = Pos( Count() - 1 ) + Len( Count() - 1 ); + Invalidate( nFirst, nLast ); + return true; + } + return false; +} + +std::unique_ptr<SwWrongList> SwWrongList::SplitList( sal_Int32 nSplitPos ) +{ + std::unique_ptr<SwWrongList> pRet; + sal_uInt16 nLst = 0; + while( nLst < Count() && Pos( nLst ) < nSplitPos ) + ++nLst; + if( nLst ) + { + sal_Int32 nWrPos = Pos( nLst - 1 ); + sal_Int32 nWrLen = Len( nLst - 1 ); + if ( nWrPos+nWrLen > nSplitPos ) + { + nWrLen += nWrPos - nSplitPos; + maList[--nLst].mnPos = nSplitPos; + maList[nLst].mnLen = nWrLen; + } + } + if( nLst ) + { + if( WRONGLIST_GRAMMAR == GetWrongListType() ) + pRet.reset(new SwGrammarMarkUp()); + else + pRet.reset(new SwWrongList( GetWrongListType() )); + pRet->Insert(0, maList.begin(), ( nLst >= maList.size() ? maList.end() : maList.begin() + nLst ) ); + pRet->SetInvalid( GetBeginInv(), GetEndInv() ); + pRet->Invalidate_( nSplitPos ? nSplitPos - 1 : nSplitPos, nSplitPos ); + Remove( 0, nLst ); + } + if( COMPLETE_STRING == GetBeginInv() ) + SetInvalid( 0, 1 ); + else + { + ShiftLeft( mnBeginInvalid, 0, nSplitPos ); + if( mnEndInvalid != COMPLETE_STRING ) + ShiftLeft( mnEndInvalid, 0, nSplitPos ); + Invalidate_( 0, 1 ); + } + for (nLst = 0; nLst < Count(); ++nLst ) + { + maList[nLst].mnPos -= nSplitPos; + } + return pRet; +} + +void SwWrongList::JoinList( SwWrongList* pNext, sal_Int32 nInsertPos ) +{ + if (pNext) + { + OSL_ENSURE( GetWrongListType() == pNext->GetWrongListType(), "type mismatch with next list" ); + + sal_uInt16 nCnt = Count(); + pNext->Move( 0, nInsertPos ); + Insert(nCnt, pNext->maList.begin(), pNext->maList.end()); + + Invalidate( pNext->GetBeginInv(), pNext->GetEndInv() ); + if( nCnt && Count() > nCnt ) + { + sal_Int32 nWrPos = Pos( nCnt ); + sal_Int32 nWrLen = Len( nCnt ); + if( !nWrPos ) + { + nWrPos += nInsertPos; + nWrLen -= nInsertPos; + maList[nCnt].mnPos = nWrPos; + maList[nCnt].mnLen = nWrLen; + } + if( nWrPos == Pos( nCnt - 1 ) + Len( nCnt - 1 ) ) + { + nWrLen += Len( nCnt - 1 ); + maList[nCnt - 1].mnLen = nWrLen; + Remove( nCnt, 1 ); + } + } + } + Invalidate( nInsertPos ? nInsertPos - 1 : nInsertPos, nInsertPos + 1 ); +} + +void SwWrongList::InsertSubList( sal_Int32 nNewPos, sal_Int32 nNewLen, sal_uInt16 nWhere, SwWrongList* pSubList ) +{ + if (pSubList) + { + OSL_ENSURE( GetWrongListType() == pSubList->GetWrongListType(), "type mismatch with sub list" ); + } + std::vector<SwWrongArea>::iterator i = maList.begin(); + if ( nWhere >= maList.size() ) + i = maList.end(); // robust + else + i += nWhere; + maList.insert(i, SwWrongArea( OUString(), nullptr, nNewPos, nNewLen, pSubList ) ); +} + +// New functions: Necessary because SwWrongList has been changed to use std::vector +void SwWrongList::Insert(sal_uInt16 nWhere, std::vector<SwWrongArea>::iterator startPos, std::vector<SwWrongArea>::iterator const & endPos) +{ + std::vector<SwWrongArea>::iterator i = maList.begin(); + if ( nWhere >= maList.size() ) + i = maList.end(); // robust + else + i += nWhere; + maList.insert(i, startPos, endPos); // insert [startPos, endPos[ before i + + // ownership of the sublist is passed to maList, therefore we have to set the + // pSubList-Pointers to 0 + while ( startPos != endPos ) + { + (*startPos).mpSubList = nullptr; + ++startPos; + } +} + +void SwWrongList::Remove(sal_uInt16 nIdx, sal_uInt16 nLen ) +{ + if ( nIdx >= maList.size() ) return; + std::vector<SwWrongArea>::iterator i1 = maList.begin(); + i1 += nIdx; + + std::vector<SwWrongArea>::iterator i2 = i1; + if ( nIdx + nLen >= o3tl::narrowing<sal_uInt16>(maList.size()) ) + i2 = maList.end(); // robust + else + i2 += nLen; + + std::vector<SwWrongArea>::iterator iLoop = i1; + while ( iLoop != i2 ) + { + delete (*iLoop).mpSubList; + ++iLoop; + } + +#if OSL_DEBUG_LEVEL > 0 + const int nOldSize = Count(); +#endif + + maList.erase(i1, i2); + +#if OSL_DEBUG_LEVEL > 0 + OSL_ENSURE( Count() + nLen == nOldSize, "SwWrongList::Remove() trouble" ); +#endif +} + +void SwWrongList::RemoveEntry( sal_Int32 nBegin, sal_Int32 nEnd ) { + std::vector<SwWrongArea>::const_iterator aEnd(maList.end()); + auto aDelIter = std::find_if(maList.cbegin(), aEnd, + [nBegin](const SwWrongArea& rST) { return rST.mnPos >= nBegin; }); + auto aIter = aDelIter; + if( WRONGLIST_GRAMMAR == GetWrongListType() ) + { + if( nBegin < nEnd ) + { + aIter = std::find_if(aDelIter, aEnd, + [nEnd](const SwWrongArea& rST) { return rST.mnPos >= nEnd; }); + } + } + else + { + aIter = std::find_if(aDelIter, aEnd, + [nBegin, nEnd](const SwWrongArea& rST) { + return (rST.mnPos != nBegin) || ((rST.mnPos + rST.mnLen) != nEnd); + }); + } + auto nDel = o3tl::narrowing<sal_uInt16>(std::distance(aDelIter, aIter)); + if( nDel ) + { + auto nDelPos = o3tl::narrowing<sal_uInt16>(std::distance(maList.cbegin(), aDelIter)); + Remove( nDelPos, nDel ); + } +} + +bool SwWrongList::LookForEntry( sal_Int32 nBegin, sal_Int32 nEnd ) { + auto aIter = std::find_if(maList.begin(), maList.end(), + [nBegin](const SwWrongArea& rST) { return rST.mnPos >= nBegin; }); + return aIter != maList.end() + && nBegin == (*aIter).mnPos + && nEnd == (*aIter).mnPos + (*aIter).mnLen; +} + +void SwWrongList::Insert( const OUString& rType, + css::uno::Reference< css::container::XStringKeyMap > const & xPropertyBag, + sal_Int32 nNewPos, sal_Int32 nNewLen ) +{ + auto aIter = std::find_if(maList.begin(), maList.end(), + [nNewPos](const SwWrongArea& rST) { return nNewPos <= rST.mnPos; }); + if ( aIter != maList.end() && nNewPos == (*aIter).mnPos ) + { + const sal_Int32 nSTPos = (*aIter).mnPos; + + aIter = std::find_if(aIter, maList.end(), + [nSTPos, nNewLen](const SwWrongArea& rST) { return rST.mnPos != nSTPos || nNewLen < rST.mnLen; }); + } + + maList.insert(aIter, SwWrongArea( rType, meType, xPropertyBag, nNewPos, nNewLen) ); +} + +namespace sw { + +WrongListIteratorBase::WrongListIteratorBase(SwTextFrame const& rFrame, + SwWrongList const* (SwTextNode::*pGetWrongList)() const) + : m_pGetWrongList(pGetWrongList) + , m_pMergedPara(rFrame.GetMergedPara()) + , m_CurrentExtent(0) + , m_CurrentIndex(0) + , m_pWrongList(m_pMergedPara + ? nullptr + : (rFrame.GetTextNodeFirst()->*pGetWrongList)()) +{ +} + +WrongListIteratorBase::WrongListIteratorBase(SwWrongList const& rWrongList) + : m_pGetWrongList(nullptr) + , m_pMergedPara(nullptr) + , m_CurrentExtent(0) + , m_CurrentIndex(0) + , m_pWrongList(&rWrongList) +{ +} + +WrongListIterator::WrongListIterator(SwTextFrame const& rFrame, + SwWrongList const* (SwTextNode::*pGetWrongList)() const) + : WrongListIteratorBase(rFrame, pGetWrongList) +{ +} + +WrongListIterator::WrongListIterator(SwWrongList const& rWrongList) + : WrongListIteratorBase(rWrongList) +{ +} + +bool WrongListIterator::Check(TextFrameIndex & rStart, TextFrameIndex & rLen) +{ + if (m_pMergedPara) + { + if (rStart < m_CurrentIndex) + { // rewind + m_CurrentExtent = 0; + m_CurrentIndex = TextFrameIndex(0); + } + while (m_CurrentExtent < m_pMergedPara->extents.size()) + { + sw::Extent const& rExtent(m_pMergedPara->extents[m_CurrentExtent]); + if (rStart + rLen <= m_CurrentIndex) + { + return false; + } + else if (rStart < m_CurrentIndex) + { + rLen -= m_CurrentIndex - rStart; + assert(0 < sal_Int32(rLen)); + rStart = m_CurrentIndex; + } + if (m_CurrentIndex <= rStart && + rStart < m_CurrentIndex + TextFrameIndex(rExtent.nEnd - rExtent.nStart)) + { + SwWrongList const*const pWrongList((rExtent.pNode->*m_pGetWrongList)()); + // found the extent containing start - first, call Check + sal_Int32 nStart(rExtent.nStart + sal_Int32(rStart - m_CurrentIndex)); // (m_CurrentIndex - m_CurrentNodeIndex)); + sal_Int32 nLen; + if (sal_Int32(rLen) < rExtent.nEnd - nStart) + { + nLen = sal_Int32(rLen); + } + else + { + sal_Int32 nInLen(rLen); + nLen = rExtent.nEnd - nStart; + nInLen -= nLen; + for (size_t i = m_CurrentExtent + 1; + i < m_pMergedPara->extents.size(); ++i) + { + sw::Extent const& rExtentEnd(m_pMergedPara->extents[i]); + if (rExtentEnd.pNode != rExtent.pNode) + { + break; + } + // add gap too + nLen += rExtentEnd.nStart - m_pMergedPara->extents[i-1].nEnd; + if (nInLen <= rExtentEnd.nEnd - rExtentEnd.nStart) + { + nLen += nInLen; + break; + } + nLen += rExtentEnd.nEnd - rExtentEnd.nStart; + nInLen -= rExtentEnd.nEnd - rExtentEnd.nStart; + } + } + if (pWrongList && pWrongList->Check(nStart, nLen)) + { + // check if there's overlap with this extent + if (rExtent.nStart <= nStart && nStart < rExtent.nEnd) + { + // yes - now compute end position / length + sal_Int32 const nEnd(nStart + nLen); + rStart = m_CurrentIndex + TextFrameIndex(nStart - rExtent.nStart); + TextFrameIndex const nOrigLen(rLen); + if (nEnd <= rExtent.nEnd) + { + rLen = TextFrameIndex(nEnd - nStart); + } + else // have to search other extents for the end... + { + rLen = TextFrameIndex(rExtent.nEnd - nStart); + for (size_t i = m_CurrentExtent + 1; + i < m_pMergedPara->extents.size(); ++i) + { + sw::Extent const& rExtentEnd(m_pMergedPara->extents[i]); + if (rExtentEnd.pNode != rExtent.pNode + || nEnd <= rExtentEnd.nStart) + { + break; + } + if (nEnd <= rExtentEnd.nEnd) + { + rLen += TextFrameIndex(nEnd - rExtentEnd.nStart); + break; + } + rLen += TextFrameIndex(rExtentEnd.nEnd - rExtentEnd.nStart); + } + } + assert(rLen <= nOrigLen); (void) nOrigLen; + return true; + } + } + } + m_CurrentIndex += TextFrameIndex(rExtent.nEnd - rExtent.nStart); + ++m_CurrentExtent; + } + return false; + } + else if (m_pWrongList) + { + sal_Int32 nStart(rStart); + sal_Int32 nLen(rLen); + bool const bRet(m_pWrongList->Check(nStart, nLen)); + rStart = TextFrameIndex(nStart); + rLen = TextFrameIndex(nLen); + return bRet; + } + return false; +} + +const SwWrongArea* +WrongListIterator::GetWrongElement(TextFrameIndex const nStart) +{ + if (m_pMergedPara) + { + if (nStart < m_CurrentIndex) + { // rewind + m_CurrentExtent = 0; + m_CurrentIndex = TextFrameIndex(0); + } + while (m_CurrentExtent < m_pMergedPara->extents.size()) + { + sw::Extent const& rExtent(m_pMergedPara->extents[m_CurrentExtent]); + if (m_CurrentIndex <= nStart && + nStart <= m_CurrentIndex + TextFrameIndex(rExtent.nEnd - rExtent.nStart)) + { + // note: the returned object isn't wrapped because fntcache.cxx + // does not look at its positions, only its formatting props + SwWrongList const*const pWrongList((rExtent.pNode->*m_pGetWrongList)()); + if (pWrongList) + { + sal_Int32 const nNStart(rExtent.nStart + sal_Int32(nStart - m_CurrentIndex)); // (m_CurrentIndex - m_CurrentNodeIndex)); + sal_Int16 const nPos(pWrongList->GetWrongPos(nNStart)); + return pWrongList->GetElement(nPos); + } + } + m_CurrentIndex += TextFrameIndex(rExtent.nEnd - rExtent.nStart); + ++m_CurrentExtent; + } + return nullptr; + } + else if (m_pWrongList) + { + sal_Int16 const nPos(m_pWrongList->GetWrongPos(sal_Int32(nStart))); + return m_pWrongList->GetElement(nPos); + } + return nullptr; +} + +WrongListIteratorCounter::WrongListIteratorCounter(SwTextFrame const& rFrame, + SwWrongList const* (SwTextNode::*pGetWrongList)() const) + : WrongListIteratorBase(rFrame, pGetWrongList) +{ +} + +WrongListIteratorCounter::WrongListIteratorCounter(SwWrongList const& rWrongList) + : WrongListIteratorBase(rWrongList) +{ +} + +sal_uInt16 WrongListIteratorCounter::GetElementCount() +{ + if (m_pMergedPara) + { + sal_uInt16 nRet(0); + m_CurrentExtent = 0; + m_CurrentIndex = TextFrameIndex(0); + SwNode const* pNode(nullptr); + sal_uInt16 InCurrentNode(0); + while (m_CurrentExtent < m_pMergedPara->extents.size()) + { + sw::Extent const& rExtent(m_pMergedPara->extents[m_CurrentExtent]); + if (rExtent.pNode != pNode) + { + InCurrentNode = 0; + pNode = rExtent.pNode; + } + SwWrongList const*const pWrongList((rExtent.pNode->*m_pGetWrongList)()); + for (; pWrongList && InCurrentNode < pWrongList->Count(); ++InCurrentNode) + { + SwWrongArea const*const pWrong(pWrongList->GetElement(InCurrentNode)); + TextFrameIndex const nExtentEnd( + m_CurrentIndex + TextFrameIndex(rExtent.nEnd - rExtent.nStart)); + if (nExtentEnd <= TextFrameIndex(pWrong->mnPos)) + { + break; // continue outer loop + } + if (m_CurrentIndex < TextFrameIndex(pWrong->mnPos + pWrong->mnLen)) + { + ++nRet; + } + } + m_CurrentIndex += TextFrameIndex(rExtent.nEnd - rExtent.nStart); + ++m_CurrentExtent; + } + return nRet; + } + else if (m_pWrongList) + { + return m_pWrongList->Count(); + } + return 0; +} + +std::optional<std::pair<TextFrameIndex, TextFrameIndex>> +WrongListIteratorCounter::GetElementAt(sal_uInt16 nIndex) +{ + if (m_pMergedPara) + { + m_CurrentExtent = 0; + m_CurrentIndex = TextFrameIndex(0); + SwNode const* pNode(nullptr); + sal_uInt16 InCurrentNode(0); + while (m_CurrentExtent < m_pMergedPara->extents.size()) + { + sw::Extent const& rExtent(m_pMergedPara->extents[m_CurrentExtent]); + if (rExtent.pNode != pNode) + { + InCurrentNode = 0; + pNode = rExtent.pNode; + } + SwWrongList const*const pWrongList((rExtent.pNode->*m_pGetWrongList)()); + for (; pWrongList && InCurrentNode < pWrongList->Count(); ++InCurrentNode) + { + SwWrongArea const*const pWrong(pWrongList->GetElement(InCurrentNode)); + TextFrameIndex const nExtentEnd( + m_CurrentIndex + TextFrameIndex(rExtent.nEnd - rExtent.nStart)); + if (nExtentEnd <= TextFrameIndex(pWrong->mnPos)) + { + break; // continue outer loop + } + if (m_CurrentIndex < TextFrameIndex(pWrong->mnPos + pWrong->mnLen)) + { + if (nIndex == 0) + { + return std::optional<std::pair<TextFrameIndex, TextFrameIndex>>( + std::pair<TextFrameIndex, TextFrameIndex>( + m_CurrentIndex - TextFrameIndex(rExtent.nStart - + std::max(rExtent.nStart, pWrong->mnPos)), + m_CurrentIndex - TextFrameIndex(rExtent.nStart - + std::min(pWrong->mnPos + pWrong->mnLen, rExtent.nEnd)))); + } + --nIndex; + } + } + m_CurrentIndex += TextFrameIndex(rExtent.nEnd - rExtent.nStart); + ++m_CurrentExtent; + } + return std::optional<std::pair<TextFrameIndex, TextFrameIndex>>(); + } + else if (m_pWrongList) + { + SwWrongArea const*const pWrong(m_pWrongList->GetElement(nIndex)); + return std::optional<std::pair<TextFrameIndex, TextFrameIndex>>( + std::pair<TextFrameIndex, TextFrameIndex>( + TextFrameIndex(pWrong->mnPos), + TextFrameIndex(pWrong->mnPos + pWrong->mnLen))); + } + return std::optional<std::pair<TextFrameIndex, TextFrameIndex>>(); +} + +} // namespace sw + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/xmldump.cxx b/sw/source/core/text/xmldump.cxx new file mode 100644 index 0000000000..a14c5a4851 --- /dev/null +++ b/sw/source/core/text/xmldump.cxx @@ -0,0 +1,203 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <frame.hxx> +#include <frmfmt.hxx> +#include <ftnfrm.hxx> +#include <sectfrm.hxx> +#include <tabfrm.hxx> +#include <pagefrm.hxx> +#include <txtfrm.hxx> +#include <cellfrm.hxx> +#include <flyfrm.hxx> +#include <hffrm.hxx> +#include <rootfrm.hxx> +#include <ndtxt.hxx> +#include <sortedobjs.hxx> +#include <swfont.hxx> +#include <txttypes.hxx> +#include <anchoredobject.hxx> +#include <libxml/xmlwriter.h> +#include <SwPortionHandler.hxx> +#include <view.hxx> +#include <flyfrms.hxx> +#include <svx/svdobj.hxx> + +#include "porlay.hxx" + +const char* sw::PortionTypeToString(PortionType nType) +{ + switch (nType) + { + case PortionType::NONE: + return "PortionType::NONE"; + case PortionType::FlyCnt: + return "PortionType::FlyCnt"; + + case PortionType::Hole: + return "PortionType::Hole"; + case PortionType::TempEnd: + return "PortionType::TempEnd"; + case PortionType::Break: + return "PortionType::Break"; + case PortionType::Kern: + return "PortionType::Kern"; + case PortionType::Arrow: + return "PortionType::Arrow"; + case PortionType::Multi: + return "PortionType::Multi"; + case PortionType::HiddenText: + return "PortionType::HiddenText"; + case PortionType::ControlChar: + return "PortionType::ControlChar"; + case PortionType::Bookmark: + return "PortionType::Bookmark"; + + case PortionType::Text: + return "PortionType::Text"; + case PortionType::Lay: + return "PortionType::Lay"; + case PortionType::Para: + return "PortionType::Para"; + case PortionType::Hanging: + return "PortionType::Hanging"; + + case PortionType::Drop: + return "PortionType::Drop"; + case PortionType::Tox: + return "PortionType::Tox"; + case PortionType::IsoTox: + return "PortionType::IsoTox"; + case PortionType::Ref: + return "PortionType::Ref"; + case PortionType::IsoRef: + return "PortionType::IsoRef"; + case PortionType::Meta: + return "PortionType::Meta"; + case PortionType::ContentControl: + return "PortionType::ContentControl"; + case PortionType::FieldMark: + return "PortionType::FieldMark"; + case PortionType::FieldFormCheckbox: + return "PortionType::FieldFormCheckbox"; + case PortionType::InputField: + return "PortionType::InputField"; + + case PortionType::Expand: + return "PortionType::Expand"; + case PortionType::Blank: + return "PortionType::Blank"; + case PortionType::PostIts: + return "PortionType::PostIts"; + + case PortionType::Hyphen: + return "PortionType::Hyphen"; + case PortionType::HyphenStr: + return "PortionType::HyphenStr"; + case PortionType::SoftHyphen: + return "PortionType::SoftHyphen"; + case PortionType::SoftHyphenStr: + return "PortionType::SoftHyphenStr"; + case PortionType::SoftHyphenComp: + return "PortionType::SoftHyphenComp"; + + case PortionType::Field: + return "PortionType::Field"; + case PortionType::Hidden: + return "PortionType::Hidden"; + case PortionType::QuoVadis: + return "PortionType::QuoVadis"; + case PortionType::ErgoSum: + return "PortionType::ErgoSum"; + case PortionType::Combined: + return "PortionType::Combined"; + case PortionType::Footnote: + return "PortionType::Footnote"; + + case PortionType::FootnoteNum: + return "PortionType::FootnoteNum"; + case PortionType::Number: + return "PortionType::Number"; + case PortionType::Bullet: + return "PortionType::Bullet"; + case PortionType::GrfNum: + return "PortionType::GrfNum"; + + case PortionType::Glue: + return "PortionType::Glue"; + + case PortionType::Margin: + return "PortionType::Margin"; + + case PortionType::Fix: + return "PortionType::Fix"; + case PortionType::Fly: + return "PortionType::Fly"; + + case PortionType::Tab: + return "PortionType::Tab"; + + case PortionType::TabRight: + return "PortionType::TabRight"; + case PortionType::TabCenter: + return "PortionType::TabCenter"; + case PortionType::TabDecimal: + return "PortionType::TabDecimal"; + + case PortionType::TabLeft: + return "PortionType::TabLeft"; + default: + return "Unknown"; + } +} + +void SwFrame::dumpTopMostAsXml(xmlTextWriterPtr writer) const +{ + const SwFrame* pFrame = this; + while (pFrame->GetUpper()) + { + pFrame = pFrame->GetUpper(); + } + + pFrame->dumpAsXml(writer); +} + +void SwFrame::dumpInfosAsXml( xmlTextWriterPtr writer ) const +{ + // output the Frame + (void)xmlTextWriterStartElement( writer, BAD_CAST( "bounds" ) ); + getFrameArea().dumpAsXmlAttributes(writer); + (void)xmlTextWriterWriteAttribute(writer, BAD_CAST("mbFixSize"), BAD_CAST(OString::boolean(HasFixSize()).getStr())); + (void)xmlTextWriterWriteAttribute(writer, BAD_CAST("mbFrameAreaPositionValid"), BAD_CAST(OString::boolean(isFrameAreaPositionValid()).getStr())); + (void)xmlTextWriterWriteAttribute(writer, BAD_CAST("mbFrameAreaSizeValid"), BAD_CAST(OString::boolean(isFrameAreaSizeValid()).getStr())); + (void)xmlTextWriterWriteAttribute(writer, BAD_CAST("mbFramePrintAreaValid"), BAD_CAST(OString::boolean(isFramePrintAreaValid()).getStr())); + (void)xmlTextWriterEndElement( writer ); + + // output the print area + (void)xmlTextWriterStartElement( writer, BAD_CAST( "prtBounds" ) ); + getFramePrintArea().dumpAsXmlAttributes(writer); + (void)xmlTextWriterEndElement( writer ); +} + +void SwFrame::dumpAsXmlAttributes( xmlTextWriterPtr writer ) const +{ + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "ptr" ), "%p", this ); + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "id" ), "%" SAL_PRIuUINT32, GetFrameId() ); + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "symbol" ), "%s", BAD_CAST( typeid( *this ).name( ) ) ); + if ( GetNext( ) ) + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "next" ), "%" SAL_PRIuUINT32, GetNext()->GetFrameId() ); + if ( GetPrev( ) ) + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "prev" ), "%" SAL_PRIuUINT32, GetPrev()->GetFrameId() ); + if ( GetUpper( ) ) + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "upper" ), "%" SAL_PRIuUINT32, GetUpper()->GetFrameId() ); + if ( GetLower( ) ) + (void)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "lower" ), "%" SAL_PRIuUINT32, GetLower()->GetFrameId() ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |