sw/inc/IDocumentSettingAccess.hxx | 1 sw/source/core/doc/DocumentSettingManager.cxx | 10 ++++ sw/source/core/inc/DocumentSettingManager.hxx | 1 sw/source/core/inc/txtfrm.hxx | 7 +- sw/source/core/layout/frmtool.cxx | 6 ++ sw/source/core/text/inftxt.cxx | 24 ++++++--- sw/source/core/text/inftxt.hxx | 7 ++ sw/source/core/text/itratr.cxx | 40 ++++++++++++++++ sw/source/core/text/itratr.hxx | 2 sw/source/core/text/itrpaint.cxx | 1 sw/source/core/text/itrtxt.cxx | 18 ++++++- sw/source/core/text/redlnitr.cxx | 56 ++++++++++++++++++----- sw/source/core/text/txtfrm.cxx | 21 ++++++-- sw/source/filter/ww8/ww8par.cxx | 1 sw/source/uibase/uno/SwXDocumentSettings.cxx | 18 +++++++ sw/source/writerfilter/dmapper/SettingsTable.cxx | 2 16 files changed, 183 insertions(+), 32 deletions(-)
New commits: commit d3ebd16bd284d9aa7ac988cd5d7437eaff0f6ed0 Author: Michael Stahl <[email protected]> AuthorDate: Wed Oct 8 19:29:46 2025 +0200 Commit: Thorsten Behrens <[email protected]> CommitDate: Mon Feb 16 05:06:48 2026 +0100 sw: text formatting: implement per-line paragraph properties like Word This doesn't make a whole lot of sense. Add compatibility setting "HiddenParagraphMarkPerLineProperties" for RTF and DOCX compatibilityMode < 15. Apparently what Word's "Compatibility Mode" is doing in case a paragraph mark has hidden formatting is that it merges the last line of the first paragraph and the first line of the second paragraph together, and applies the first paragraph's properties to the line (and the preceding lines); but for the second line of the second paragraph, it applies the second paragraph's properties. This is now implemented here; firstly, by adding a flag to the MergedPara's Extents so that the situation can be distinguished (if the paragraphs are joined by a delete redline, Word does something different of course). Because it's possible that the hidden paragraph break is on a paragraph that doesn't have any extents, but a preceding paragraph has extents that are affected, this sometimes requires 0-length dummy extents. FindParaPropsNodeIgnoreHidden() sets the last paragraph as pParaPropsNode, which is used for all non-per-line properties. Note that it's somewhat likely that the various Update functions in txtfrm.cxx don't maintain the isHiddenParaMerge flag on extents correctly, so it may look different than Word when editing. Currently it affects these properties: * line spacing * tab stops These are now set per line by SwTextIter::Next() calling SwAttrIter::GetTextNodeForLinePropsWordCompat() and a factored out SwLineInfo::InitLineInfo(). Evidently Word also does adjustment this way, but it's not implemented here. Reviewed-on: https://gerrit.libreoffice.org/c/core/+/192077 Tested-by: Jenkins Reviewed-by: Michael Stahl <[email protected]> (cherry picked from commit 0849ddd0b1b3c384c5f3de8fe0bbb9df558fa786) sw: fix too early reset of m_oParagraphBreak Thanks to Mike Kaganski for finding this (regression from commit 0849ddd0b1b3c384c5f3de8fe0bbb9df558fa786) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/192105 Reviewed-by: Michael Stahl <[email protected]> Tested-by: Jenkins (cherry picked from commit 2ae71afbef8e767a313584949268b62e5e695279) Conflicts: sw/source/core/text/inftxt.hxx sw/source/core/text/redlnitr.cxx sw/source/writerfilter/dmapper/SettingsTable.cxx Change-Id: I216d9e2afdac9ab6f97c0ea822d4d501689df7a6 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/198689 Tested-by: Jenkins CollaboraOffice <[email protected]> Reviewed-by: Thorsten Behrens <[email protected]> diff --git a/sw/inc/IDocumentSettingAccess.hxx b/sw/inc/IDocumentSettingAccess.hxx index 1314f60479bc..28d5c4d8cb90 100644 --- a/sw/inc/IDocumentSettingAccess.hxx +++ b/sw/inc/IDocumentSettingAccess.hxx @@ -101,6 +101,7 @@ enum class DocumentSettingId JUSTIFY_LINES_WITH_SHRINKING, APPLY_TEXT_ATTR_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH, APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH, + HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES, DO_NOT_MIRROR_RTL_DRAW_OBJS, // COMPATIBILITY FLAGS END BROWSE_MODE, diff --git a/sw/source/core/doc/DocumentSettingManager.cxx b/sw/source/core/doc/DocumentSettingManager.cxx index 5106cb78ddfc..1ed663d9cc02 100644 --- a/sw/source/core/doc/DocumentSettingManager.cxx +++ b/sw/source/core/doc/DocumentSettingManager.cxx @@ -264,6 +264,8 @@ bool sw::DocumentSettingManager::get(/*[in]*/ DocumentSettingId id) const return mbApplyTextAttrToEmptyLineAtEndOfParagraph; case DocumentSettingId::APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH: return mbApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph; + case DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES: + return mbHiddenParagraphMarkPerLineProperties; case DocumentSettingId::DO_NOT_BREAK_WRAPPED_TABLES: return mbDoNotBreakWrappedTables; case DocumentSettingId::ALLOW_TEXT_AFTER_FLOATING_TABLE_BREAK: @@ -477,6 +479,9 @@ void sw::DocumentSettingManager::set(/*[in]*/ DocumentSettingId id, /*[in]*/ boo mbApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph = value; break; + case DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES: + mbHiddenParagraphMarkPerLineProperties = value; + break; case DocumentSettingId::DO_NOT_MIRROR_RTL_DRAW_OBJS: mbDoNotMirrorRtlDrawObjs = value; @@ -1177,6 +1182,11 @@ void sw::DocumentSettingManager::dumpAsXml(xmlTextWriterPtr pWriter) const BAD_CAST(OString::boolean(mbApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph).getStr())); (void)xmlTextWriterEndElement(pWriter); + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("mbHiddenParagraphMarkPerLineProperties")); + (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("value"), + BAD_CAST(OString::boolean(mbHiddenParagraphMarkPerLineProperties).getStr())); + (void)xmlTextWriterEndElement(pWriter); + (void)xmlTextWriterStartElement(pWriter, BAD_CAST("mbDoNotMirrorRtlDrawObjs")); (void)xmlTextWriterWriteAttribute(pWriter, BAD_CAST("value"), BAD_CAST(OString::boolean(mbDoNotMirrorRtlDrawObjs).getStr())); diff --git a/sw/source/core/inc/DocumentSettingManager.hxx b/sw/source/core/inc/DocumentSettingManager.hxx index 92dfe6f834e8..c5e92d45f468 100644 --- a/sw/source/core/inc/DocumentSettingManager.hxx +++ b/sw/source/core/inc/DocumentSettingManager.hxx @@ -180,6 +180,7 @@ class DocumentSettingManager final : bool mbJustifyLinesWithShrinking = false; bool mbApplyTextAttrToEmptyLineAtEndOfParagraph = false; // this was a mistake bool mbApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph = false; + bool mbHiddenParagraphMarkPerLineProperties = false; bool mbIgnoreHiddenCharsForLineCalculation = true; bool mbDoNotMirrorRtlDrawObjs = false; // If this is on as_char flys wrapping will be handled the same like in Word diff --git a/sw/source/core/inc/txtfrm.hxx b/sw/source/core/inc/txtfrm.hxx index c1db19420eeb..8b368707080c 100644 --- a/sw/source/core/inc/txtfrm.hxx +++ b/sw/source/core/inc/txtfrm.hxx @@ -93,11 +93,12 @@ struct Extent SwTextNode * /*const logically, but need assignment for std::vector*/ pNode; sal_Int32 nStart; sal_Int32 nEnd; - Extent(SwTextNode *const p, sal_Int32 const s, sal_Int32 const e) - : pNode(p), nStart(s), nEnd(e) + bool isHiddenParaMerge; //< for Word Compatibility Mode + Extent(SwTextNode *const p, sal_Int32 const s, sal_Int32 const e, bool const b) + : pNode(p), nStart(s), nEnd(e), isHiddenParaMerge(b) { assert(pNode); - assert(nStart != nEnd); + assert(nStart != nEnd || isHiddenParaMerge); } }; diff --git a/sw/source/core/layout/frmtool.cxx b/sw/source/core/layout/frmtool.cxx index eca2e8e1829c..1696b82b4bf5 100644 --- a/sw/source/core/layout/frmtool.cxx +++ b/sw/source/core/layout/frmtool.cxx @@ -1138,7 +1138,11 @@ static bool IsShown(SwNodeOffset const nIndex, } for (auto iter = *pIter; iter != *pEnd; ++iter) { - assert(iter->nStart != iter->nEnd); // TODO possible? + if (iter->nStart == iter->nEnd) + { + assert(iter->isHiddenParaMerge); + continue; + } assert(iter->pNode->GetIndex() == nIndex); if (rAnch.GetAnchorContentOffset() < iter->nStart) { diff --git a/sw/source/core/text/inftxt.cxx b/sw/source/core/text/inftxt.cxx index e701e2746a77..d1d0a1872f3d 100644 --- a/sw/source/core/text/inftxt.cxx +++ b/sw/source/core/text/inftxt.cxx @@ -113,11 +113,11 @@ SwLineInfo::~SwLineInfo() { } -void SwLineInfo::CtorInitLineInfo( const SwAttrSet& rAttrSet, - const SwTextNode& rTextNode ) +void SwLineInfo::InitLineInfo(SwTextNode const& rTextNodeForLineProps) { - m_oRuler.emplace( rAttrSet.GetTabStops() ); - if ( rTextNode.GetListTabStopPosition( m_nListTabStopPosition ) ) + SwAttrSet const& rAttrSetForLineProps{rTextNodeForLineProps.GetSwAttrSet()}; + m_oRuler.emplace(rAttrSetForLineProps.GetTabStops()); + if (rTextNodeForLineProps.GetListTabStopPosition(m_nListTabStopPosition)) { m_bListTabStopIncluded = true; @@ -138,7 +138,7 @@ void SwLineInfo::CtorInitLineInfo( const SwAttrSet& rAttrSet, } } - if ( !rTextNode.getIDocumentSettingAccess()->get(DocumentSettingId::TABS_RELATIVE_TO_INDENT) ) + if (!rTextNodeForLineProps.getIDocumentSettingAccess()->get(DocumentSettingId::TABS_RELATIVE_TO_INDENT)) { // remove default tab stop at position 0 for ( sal_uInt16 i = 0; i < m_oRuler->Count(); i++ ) @@ -152,7 +152,13 @@ void SwLineInfo::CtorInitLineInfo( const SwAttrSet& rAttrSet, } } - m_pSpace = &rAttrSet.GetLineSpacing(); + m_pSpace = &rAttrSetForLineProps.GetLineSpacing(); +} + +void SwLineInfo::CtorInitLineInfo(const SwAttrSet& rAttrSet, + const SwTextNode& rTextNodeForLineProps) +{ + InitLineInfo(rTextNodeForLineProps); m_nVertAlign = rAttrSet.GetParaVertAlign().GetValue(); m_nDefTabStop = std::numeric_limits<SwTwips>::max(); } @@ -533,6 +539,7 @@ SwTextPaintInfo::SwTextPaintInfo( const SwTextPaintInfo &rInf, const OUString* p m_aPos( rInf.GetPos() ), m_aPaintRect( rInf.GetPaintRect() ), m_nSpaceIdx( rInf.GetSpaceIdx() ) + , m_pLineInfo(rInf.m_pLineInfo) { } SwTextPaintInfo::SwTextPaintInfo( const SwTextPaintInfo &rInf ) @@ -546,6 +553,7 @@ SwTextPaintInfo::SwTextPaintInfo( const SwTextPaintInfo &rInf ) m_aPos( rInf.GetPos() ), m_aPaintRect( rInf.GetPaintRect() ), m_nSpaceIdx( rInf.GetSpaceIdx() ) + , m_pLineInfo(rInf.m_pLineInfo) { } SwTextPaintInfo::SwTextPaintInfo( SwTextFrame *pFrame, const SwRect &rPaint ) @@ -798,8 +806,8 @@ void SwTextPaintInfo::CalcRect( const SwLinePortion& rPor, SwRect* pRect, SwRect* pIntersect, const bool bInsideBox ) const { - const SwAttrSet& rAttrSet = GetTextFrame()->GetTextNodeForParaProps()->GetSwAttrSet(); - const SvxLineSpacingItem& rSpace = rAttrSet.GetLineSpacing(); + assert(m_pLineInfo); + SvxLineSpacingItem const& rSpace{*m_pLineInfo->GetLineSpacing()}; tools::Long nPropLineSpace = rSpace.GetPropLineSpace(); SwTwips nHeight = rPor.Height(); diff --git a/sw/source/core/text/inftxt.hxx b/sw/source/core/text/inftxt.hxx index 70e17059adda..15aed5a00c6a 100644 --- a/sw/source/core/text/inftxt.hxx +++ b/sw/source/core/text/inftxt.hxx @@ -68,8 +68,9 @@ class SwLineInfo bool m_bListTabStopIncluded; tools::Long m_nListTabStopPosition; + void InitLineInfo(SwTextNode const& rTextNodeForLineProps); void CtorInitLineInfo( const SwAttrSet& rAttrSet, - const SwTextNode& rTextNode ); + const SwTextNode& rTextNodeForLineProps); SW_DLLPUBLIC SwLineInfo(); SW_DLLPUBLIC ~SwLineInfo(); @@ -366,9 +367,12 @@ class SwTextPaintInfo : public SwTextSizeInfo SwRect m_aPaintRect; // Original paint rect (from Layout paint) sal_uInt16 m_nSpaceIdx; + SwLineInfo const* m_pLineInfo{nullptr}; // hack: need this to get line props + bool m_bOmitPaint = false; bool m_bInsertColorPaint = false; bool m_bDeleteColorPaint = false; + void DrawText_(const OUString &rText, const SwLinePortion &rPor, const TextFrameIndex nIdx, const TextFrameIndex nLen, const bool bKern, const bool bWrong = false, @@ -485,6 +489,7 @@ public: void SetSmartTags(sw::WrongListIterator *const pNew) { m_pSmartTags = pNew; } sw::WrongListIterator* GetSmartTags() const { return m_pSmartTags; } + void SetLineInfo(SwLineInfo const*const pLineInfo) { m_pLineInfo = pLineInfo; } void SetOmitPaint(bool bOmitPaint) { m_bOmitPaint = bOmitPaint; } void SetInsertColorPaint(bool bInsertColorPaint) { m_bInsertColorPaint = bInsertColorPaint; } void SetDeleteColorPaint(bool bDeleteColorPaint) { m_bDeleteColorPaint = bDeleteColorPaint; } diff --git a/sw/source/core/text/itratr.cxx b/sw/source/core/text/itratr.cxx index aa3fd8353b0d..7e9a7b1fdf70 100644 --- a/sw/source/core/text/itratr.cxx +++ b/sw/source/core/text/itratr.cxx @@ -340,13 +340,14 @@ SwAttrIter::SeekNewPos(TextFrameIndex const nNewPos, bool *const o_pIsToEnd) bool isToEnd{false}; if (m_pMergedPara) { - if (m_pMergedPara->extents.empty()) + if (m_pMergedPara->mergedText.isEmpty()) { isToEnd = true; assert(m_pMergedPara->pLastNode == newPos.first); } else { + assert(!m_pMergedPara->extents.empty()); auto const& rLast{m_pMergedPara->extents.back()}; isToEnd = rLast.pNode == newPos.first && rLast.nEnd == newPos.second; // for text formatting: use *last* node if all text is hidden @@ -955,6 +956,43 @@ TextFrameIndex SwAttrIter::GetNextLayoutBreakAttr() const return TextFrameIndex{ nNext }; } +SwTextNode const& +SwAttrIter::GetTextNodeForLinePropsWordCompat(TextFrameIndex const nStart) +{ + if (m_pMergedPara) + { + // skip any hidden to find the first non-hidden character on the line + TextFrameIndex nHiddenStart{COMPLETE_STRING}; + TextFrameIndex nHiddenEnd{0}; + m_pScriptInfo->GetBoundsOfHiddenRange(nStart, nHiddenStart, nHiddenEnd); + sal_Int32 nIndex(::std::max(nStart, nHiddenEnd)); + // now, find the hidden paragraph break that follows the first + // non-hidden character on the line + for (auto it{m_pMergedPara->extents.begin()}; it != m_pMergedPara->extents.end(); ++it) + { + if (nIndex < (it->nEnd - it->nStart)) + { + nIndex = 0; + } + if (nIndex == 0) + { + if (it->isHiddenParaMerge) + { + return *it->pNode; + } + } + else + { + nIndex = nIndex - (it->nEnd - it->nStart); + } + } + // no hidden paragraph break => use default + assert(nIndex == 0 && "view index out of bounds"); + return *m_pMergedPara->pParaPropsNode; + } + return *m_pTextNode; +} + namespace { class SwMinMaxArgs diff --git a/sw/source/core/text/itratr.hxx b/sw/source/core/text/itratr.hxx index 2e2b01d68492..52cd4f7befef 100644 --- a/sw/source/core/text/itratr.hxx +++ b/sw/source/core/text/itratr.hxx @@ -114,6 +114,8 @@ public: void SetPropFont( const sal_uInt8 nNew ) { m_nPropFont = nNew; } SwAttrHandler& GetAttrHandler() { return m_aAttrHandler; } + + SwTextNode const& GetTextNodeForLinePropsWordCompat(TextFrameIndex nStart); }; /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sw/source/core/text/itrpaint.cxx b/sw/source/core/text/itrpaint.cxx index 711605d8cb60..75ec544960ef 100644 --- a/sw/source/core/text/itrpaint.cxx +++ b/sw/source/core/text/itrpaint.cxx @@ -72,6 +72,7 @@ void SwTextPainter::CtorInitTextPainter( SwTextFrame *pNewFrame, SwTextPaintInfo SwFont *pMyFnt = GetFnt(); GetInfo().SetFont( pMyFnt ); m_bPaintDrop = false; + GetInfo().SetLineInfo(&GetLineInfo()); } SwLinePortion *SwTextPainter::CalcPaintOfst(const SwRect &rPaint, bool& rbSkippedNumPortions) diff --git a/sw/source/core/text/itrtxt.cxx b/sw/source/core/text/itrtxt.cxx index 8c4bac91d563..645fff7d7227 100644 --- a/sw/source/core/text/itrtxt.cxx +++ b/sw/source/core/text/itrtxt.cxx @@ -24,6 +24,8 @@ #include <editeng/paravertalignitem.hxx> #include "pormulti.hxx" +#include <IDocumentSettingAccess.hxx> +#include <rootfrm.hxx> #include <pagefrm.hxx> #include <tgrditem.hxx> #include "porfld.hxx" @@ -42,10 +44,17 @@ void SwTextIter::CtorInitTextIter( SwTextFrame *pNewFrame, SwTextInfo *pNewInf ) 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(); + SwTextNode const& rTextNodeForLineProps{ + (pNode->getIDocumentSettingAccess()->get(DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES) + && m_pFrame->getRootFrame()->GetParagraphBreakMode() == ::sw::ParagraphBreakMode::Hidden) + ? GetTextNodeForLinePropsWordCompat(m_nStart) + : *pNode}; + m_aLineInf.CtorInitLineInfo(pNode->GetSwAttrSet(), rTextNodeForLineProps); + // Order is important: only execute FillRegister if GetValue!=0 m_bRegisterOn = pNode->GetSwAttrSet().GetRegister().GetValue() && m_pFrame->FillRegister( m_nRegStart, m_nRegDiff ); @@ -116,6 +125,13 @@ const SwLineLayout *SwTextIter::Next() if( m_pCurr->GetLen() || ( m_nLineNr>1 && !m_pCurr->IsDummy() ) ) ++m_nLineNr; m_pCurr = m_pCurr->GetNext(); + if (m_pFrame->GetDoc().getIDocumentSettingAccess().get( + DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES) + && m_pFrame->getRootFrame()->GetParagraphBreakMode() == ::sw::ParagraphBreakMode::Hidden) + { + SwTextNode const& rNode{GetTextNodeForLinePropsWordCompat(m_nStart)}; + m_aLineInf.InitLineInfo(rNode); + } return m_pCurr; } else diff --git a/sw/source/core/text/redlnitr.cxx b/sw/source/core/text/redlnitr.cxx index 949ea51dafdf..e0b3a9949204 100644 --- a/sw/source/core/text/redlnitr.cxx +++ b/sw/source/core/text/redlnitr.cxx @@ -36,6 +36,7 @@ #include <IDocumentRedlineAccess.hxx> #include <IDocumentLayoutAccess.hxx> #include <IDocumentMarkAccess.hxx> +#include <IDocumentSettingAccess.hxx> #include <IMark.hxx> #include <bookmark.hxx> #include <rootfrm.hxx> @@ -86,6 +87,10 @@ private: public: SwPosition const* GetStartPos() const { return m_pStartPos; } SwPosition const* GetEndPos() const { return m_pEndPos; } + bool IsHiddenParagraphBreak() const + { // only if it is merged *by* hidden break (it could be deleted at the same time) + return m_oParagraphBreak.has_value(); + } HideIterator(SwTextNode & rTextNode, bool const isHideRedlines, sw::FieldmarkMode const eMode, @@ -212,6 +217,7 @@ public: m_pStartPos = pRed->Start(); m_pEndPos = pRed->End(); ++m_RedlineIndex; + m_oParagraphBreak.reset(); return true; } else if (m_oNextFieldmarkHide) @@ -219,6 +225,7 @@ public: assert(!pNextRedlineHide || *m_oNextFieldmarkHide <= *pNextRedlineHide); m_pStartPos = &*m_oNextFieldmarkHide; m_pEndPos = &*m_Fieldmark.second; + m_oParagraphBreak.reset(); return true; } else @@ -299,6 +306,7 @@ public: m_pStartPos = nullptr; m_pEndPos = nullptr; + m_oParagraphBreak.reset(); return false; } } @@ -326,23 +334,36 @@ void FindParaPropsNodeIgnoreHidden(sw::MergedPara & rMerged, pScriptInfo->GetBoundsOfHiddenRange(TextFrameIndex{0}, nHiddenStart, nHiddenEnd); if (TextFrameIndex{0} == nHiddenStart) { - if (nHiddenEnd == TextFrameIndex{rMerged.mergedText.getLength()}) + // Word compatibilityMode < 15 changes properties per line, so just set the last node here + if (rMerged.pLastNode->getIDocumentSettingAccess()->get( + DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES)) { rMerged.pParaPropsNode = const_cast<SwTextNode*>(rMerged.pLastNode); } - else - { // this requires MapViewToModel to never return a position at - // the end of a node (when all its text is hidden) - rMerged.pParaPropsNode = sw::MapViewToModel(rMerged, nHiddenEnd).first; + else // Word compatibilityMode 15 works differently! + { // (and this is just an approximation of what it does) + if (nHiddenEnd == TextFrameIndex{rMerged.mergedText.getLength()}) + { + rMerged.pParaPropsNode = const_cast<SwTextNode*>(rMerged.pLastNode); + } + else + { // this requires MapViewToModel to never return a position at + // the end of a node (when all its text is hidden) + rMerged.pParaPropsNode = sw::MapViewToModel(rMerged, nHiddenEnd).first; + } } return; } } - if (!rMerged.extents.empty()) + for (auto const& it : rMerged.extents) { // para props from first node that isn't empty (OOo/LO compat) - rMerged.pParaPropsNode = rMerged.extents.begin()->pNode; + if (it.nStart != it.nEnd) // filter isHiddenParaMerge dummy extents + { + rMerged.pParaPropsNode = it.pNode; + return; + } + else assert(it.isHiddenParaMerge); } - else { // if every node is empty, the last one wins (Word compat) // (OOo/LO historically used first one) rMerged.pParaPropsNode = const_cast<SwTextNode*>(rMerged.pLastNode); @@ -377,11 +398,22 @@ CheckParaRedlineMerge(SwTextFrame & rFrame, SwTextNode & rTextNode, 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()); + extents.emplace_back(pNode, nLastEnd, pStart->GetContentIndex(), false); mergedText.append(pNode->GetText().subView(nLastEnd, pStart->GetContentIndex() - nLastEnd)); } if (&pEnd->GetNode() != pNode) { + if (iter.IsHiddenParagraphBreak()) + { + if (!extents.empty() && extents.back().pNode == pNode) + { + extents.back().isHiddenParaMerge = true; + } + else + { // dummy extent - must have "true" on one that has pNode! + extents.emplace_back(pNode, pNode->Len(), pNode->Len(), true); + } + } if (pNode == &rTextNode) { pNode->SetRedlineMergeFlag(SwNode::Merge::First); @@ -498,7 +530,7 @@ CheckParaRedlineMerge(SwTextFrame & rFrame, SwTextNode & rTextNode, } if (nLastEnd != pNode->Len()) { - extents.emplace_back(pNode, nLastEnd, pNode->Len()); + extents.emplace_back(pNode, nLastEnd, pNode->Len(), false); mergedText.append(pNode->GetText().subView(nLastEnd, pNode->Len() - nLastEnd)); } if (extents.empty()) // there was no text anywhere @@ -507,7 +539,9 @@ CheckParaRedlineMerge(SwTextFrame & rFrame, SwTextNode & rTextNode, } else { - assert(!mergedText.isEmpty()); + assert(!mergedText.isEmpty() + || ::std::all_of(extents.begin(), extents.end(), + [](auto const& it){ return it.isHiddenParaMerge; })); } auto pRet{std::make_unique<sw::MergedPara>(rFrame, std::move(extents), mergedText.makeStringAndClear(), &rTextNode, nodes.back())}; diff --git a/sw/source/core/text/txtfrm.cxx b/sw/source/core/text/txtfrm.cxx index a96b465d9c7f..c3cde216cb7a 100644 --- a/sw/source/core/text/txtfrm.cxx +++ b/sw/source/core/text/txtfrm.cxx @@ -839,6 +839,7 @@ void SwTextFrame::dumpAsXml(xmlTextWriterPtr writer) const (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)xmlTextWriterWriteFormatAttribute( writer, BAD_CAST( "isHPM" ), "%d", e.isHiddenParaMerge ? 1 : 0 ); (void)xmlTextWriterEndElement( writer ); } (void)xmlTextWriterEndElement( writer ); @@ -1126,7 +1127,7 @@ static TextFrameIndex UpdateMergedParaForInsert(MergedPara & rMerged, // 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); + rMerged.extents.emplace(itInsert, const_cast<SwTextNode*>(&rNode), nIndex, nIndex + nLen, false); text.insert(nTFIndex, rNode.GetText().subView(nIndex, nLen)); nInserted = nLen; // called from SwRangeRedline::InvalidateRange() @@ -1235,7 +1236,7 @@ TextFrameIndex UpdateMergedParaForDelete(MergedPara & rMerged, sal_Int32 const nOldEnd(it->nEnd); it->nEnd = nIndex; it = rMerged.extents.emplace(it+1, - it->pNode, nIndex + nDeleteHere, nOldEnd); + it->pNode, nIndex + nDeleteHere, nOldEnd, it->isHiddenParaMerge); } assert(nDeleteHere == nToDelete); } @@ -1301,7 +1302,7 @@ MapViewToModel(MergedPara const& rMerged, TextFrameIndex const i_nIndex) nIndex = nIndex - (pExtent->nEnd - pExtent->nStart); } assert(nIndex == 0 && "view index out of bounds"); - return pExtent + return (pExtent && pExtent->nStart != pExtent->nEnd) // skip isHiddenParaMerge dummys ? std::make_pair(pExtent->pNode, pExtent->nEnd) //1-past-the-end index : std::make_pair(const_cast<SwTextNode*>(rMerged.pLastNode), rMerged.pLastNode->Len()); } @@ -1443,9 +1444,17 @@ SwTextNode const* SwTextFrame::GetTextNodeForFirstText() const { sw::MergedPara const*const pMerged(GetMergedPara()); if (pMerged) - return pMerged->extents.empty() - ? pMerged->pFirstNode - : pMerged->extents.front().pNode; + { + for (auto const& it : pMerged->extents) + { + if (it.nStart != it.nEnd) // skip isHiddenParaMerge dummy extents + { + return it.pNode; + } + else assert(it.isHiddenParaMerge); + } + return pMerged->pFirstNode; + } else return static_cast<SwTextNode const*>(SwFrame::GetDep()); } diff --git a/sw/source/filter/ww8/ww8par.cxx b/sw/source/filter/ww8/ww8par.cxx index acae502032b9..c541a769024d 100644 --- a/sw/source/filter/ww8/ww8par.cxx +++ b/sw/source/filter/ww8/ww8par.cxx @@ -2017,6 +2017,7 @@ void SwWW8ImplReader::ImportDop() m_rDoc.getIDocumentSettingAccess().set(DocumentSettingId::CONTINUOUS_ENDNOTES, true); // rely on default for HYPHENATE_URLS=false m_rDoc.getIDocumentSettingAccess().set(DocumentSettingId::APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH, true); + m_rDoc.getIDocumentSettingAccess().set(DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES, true); // rely on default for IGNORE_HIDDEN_CHARS_FOR_LINE_CALCULATION=true IDocumentSettingAccess& rIDSA = m_rDoc.getIDocumentSettingAccess(); diff --git a/sw/source/uibase/uno/SwXDocumentSettings.cxx b/sw/source/uibase/uno/SwXDocumentSettings.cxx index f4add8c2644e..24891150009c 100644 --- a/sw/source/uibase/uno/SwXDocumentSettings.cxx +++ b/sw/source/uibase/uno/SwXDocumentSettings.cxx @@ -162,6 +162,7 @@ enum SwDocumentSettingsPropertyHandles HANDLE_USE_VARIABLE_WIDTH_NBSP, HANDLE_APPLY_TEXT_ATTR_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH, HANDLE_APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH, + HANDLE_HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES, HANDLE_DO_NOT_MIRROR_RTL_DRAW_OBJS, HANDLE_PAINT_HELL_OVER_HEADER_FOOTER, HANDLE_MIN_ROW_HEIGHT_INCL_BORDER, @@ -276,6 +277,7 @@ static rtl::Reference<MasterPropertySetInfo> lcl_createSettingsInfo() { u"UseVariableWidthNBSP"_ustr, HANDLE_USE_VARIABLE_WIDTH_NBSP, cppu::UnoType<bool>::get(), 0 }, { u"ApplyTextAttrToEmptyLineAtEndOfParagraph"_ustr, HANDLE_APPLY_TEXT_ATTR_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH, cppu::UnoType<bool>::get(), 0 }, { u"ApplyParagraphMarkFormatToEmptyLineAtEndOfParagraph"_ustr, HANDLE_APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH, cppu::UnoType<bool>::get(), 0 }, + { u"HiddenParagraphMarkPerLineProperties"_ustr, HANDLE_HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES, cppu::UnoType<bool>::get(), 0 }, { u"DoNotMirrorRtlDrawObjs"_ustr, HANDLE_DO_NOT_MIRROR_RTL_DRAW_OBJS, cppu::UnoType<bool>::get(), 0 }, { u"PaintHellOverHeaderFooter"_ustr, HANDLE_PAINT_HELL_OVER_HEADER_FOOTER, cppu::UnoType<bool>::get(), 0 }, { u"MinRowHeightInclBorder"_ustr, HANDLE_MIN_ROW_HEIGHT_INCL_BORDER, cppu::UnoType<bool>::get(), 0 }, @@ -1118,6 +1120,16 @@ void SwXDocumentSettings::_setSingleValue( const comphelper::PropertyInfo & rInf } } break; + case HANDLE_HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES: + { + bool bTmp; + if (rValue >>= bTmp) + { + mpDoc->getIDocumentSettingAccess().set( + DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES, bTmp); + } + } + break; case HANDLE_DO_NOT_MIRROR_RTL_DRAW_OBJS: { bool bTmp; @@ -1767,6 +1779,12 @@ void SwXDocumentSettings::_getSingleValue( const comphelper::PropertyInfo & rInf DocumentSettingId::APPLY_PARAGRAPH_MARK_FORMAT_TO_EMPTY_LINE_AT_END_OF_PARAGRAPH); } break; + case HANDLE_HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES: + { + rValue <<= mpDoc->getIDocumentSettingAccess().get( + DocumentSettingId::HIDDEN_PARAGRAPH_MARK_PER_LINE_PROPERTIES); + } + break; case HANDLE_DO_NOT_MIRROR_RTL_DRAW_OBJS: { rValue <<= mpDoc->getIDocumentSettingAccess().get( diff --git a/sw/source/writerfilter/dmapper/SettingsTable.cxx b/sw/source/writerfilter/dmapper/SettingsTable.cxx index 4f309109a31f..77bfcf96f6ca 100644 --- a/sw/source/writerfilter/dmapper/SettingsTable.cxx +++ b/sw/source/writerfilter/dmapper/SettingsTable.cxx @@ -664,6 +664,8 @@ void SettingsTable::ApplyProperties(rtl::Reference<SwXTextDocument> const& xDoc) { xDocumentSettings->setPropertyValue(u"MsWordCompMinLineHeightByFly"_ustr, uno::Any(true)); xDocumentSettings->setPropertyValue(u"TabOverMargin"_ustr, uno::Any(true)); + xDocumentSettings->setPropertyValue(u"HiddenParagraphMarkPerLineProperties"_ustr, + uno::Any(true)); } // Show changes value
