tdf#152872 sw: conditionally hide paragraph breaks
[LibreOffice.git] / sw / source / core / text / redlnitr.cxx
blobc44b527536c1736312011812fcedb82b1d7fde07
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include <sal/config.h>
22 #include <string_view>
24 #include <hintids.hxx>
25 #include <o3tl/safeint.hxx>
26 #include <svl/whiter.hxx>
27 #include <com/sun/star/i18n/ScriptType.hpp>
28 #include <scriptinfo.hxx>
29 #include <swmodule.hxx>
30 #include <redline.hxx>
31 #include <txatbase.hxx>
32 #include <docary.hxx>
33 #include "itratr.hxx"
34 #include <ndtxt.hxx>
35 #include <doc.hxx>
36 #include <IDocumentRedlineAccess.hxx>
37 #include <IDocumentLayoutAccess.hxx>
38 #include <IDocumentMarkAccess.hxx>
39 #include <IMark.hxx>
40 #include <bookmark.hxx>
41 #include <rootfrm.hxx>
42 #include <breakit.hxx>
43 #include <vcl/commandevent.hxx>
44 #include <vcl/settings.hxx>
45 #include <txtfrm.hxx>
46 #include <ftnfrm.hxx>
47 #include <vcl/svapp.hxx>
48 #include "redlnitr.hxx"
49 #include <extinput.hxx>
50 #include <fmtpdsc.hxx>
51 #include <editeng/charhiddenitem.hxx>
52 #include <editeng/colritem.hxx>
53 #include <editeng/crossedoutitem.hxx>
54 #include <editeng/formatbreakitem.hxx>
55 #include <editeng/udlnitem.hxx>
57 using namespace ::com::sun::star;
59 namespace {
61 class HideIterator
63 private:
64 IDocumentRedlineAccess const& m_rIDRA;
65 IDocumentMarkAccess const& m_rIDMA;
66 bool const m_isHideRedlines;
67 sw::FieldmarkMode const m_eFieldmarkMode;
68 bool const m_isHideParagraphBreaks;
69 SwPosition const m_Start;
70 /// next redline
71 SwRedlineTable::size_type m_RedlineIndex;
72 /// next fieldmark
73 std::pair<sw::mark::IFieldmark const*, std::optional<SwPosition>> m_Fieldmark;
74 std::optional<SwPosition> m_oNextFieldmarkHide;
75 /// previous paragraph break - because m_pStartPos/EndPos are non-owning
76 std::optional<std::pair<SwPosition, SwPosition>> m_oParagraphBreak;
77 /// current start/end pair
78 SwPosition const* m_pStartPos;
79 SwPosition const* m_pEndPos;
81 public:
82 SwPosition const* GetStartPos() const { return m_pStartPos; }
83 SwPosition const* GetEndPos() const { return m_pEndPos; }
85 HideIterator(SwTextNode & rTextNode,
86 bool const isHideRedlines, sw::FieldmarkMode const eMode,
87 sw::ParagraphBreakMode const ePBMode)
88 : m_rIDRA(rTextNode.getIDocumentRedlineAccess())
89 , m_rIDMA(*rTextNode.getIDocumentMarkAccess())
90 , m_isHideRedlines(isHideRedlines)
91 , m_eFieldmarkMode(eMode)
92 , m_isHideParagraphBreaks(ePBMode == sw::ParagraphBreakMode::Hidden)
93 , m_Start(rTextNode, 0)
94 , m_RedlineIndex(isHideRedlines ? m_rIDRA.GetRedlinePos(rTextNode, RedlineType::Any) : SwRedlineTable::npos)
95 , m_pStartPos(nullptr)
96 , m_pEndPos(&m_Start)
100 // delete redlines and fieldmarks can't overlap, due to sw::CalcBreaks()
101 // and no combining of adjacent redlines
102 // -> dummy chars are delete-redlined *iff* entire fieldmark is
103 // Note: caller is responsible for checking for immediately adjacent hides
104 bool Next()
106 SwPosition const* pNextRedlineHide(nullptr);
107 assert(m_pEndPos);
108 if (m_isHideRedlines)
110 // position on current or next redline
111 for (; m_RedlineIndex < m_rIDRA.GetRedlineTable().size(); ++m_RedlineIndex)
113 SwRangeRedline const*const pRed = m_rIDRA.GetRedlineTable()[m_RedlineIndex];
115 if (m_pEndPos->GetNodeIndex() < pRed->Start()->GetNodeIndex())
116 break;
118 if (pRed->GetType() != RedlineType::Delete)
119 continue;
121 auto [pStart, pEnd] = pRed->StartEnd(); // SwPosition*
122 if (*pStart == *pEnd)
123 { // only allowed while moving (either way?)
124 // assert(IDocumentRedlineAccess::IsHideChanges(rIDRA.GetRedlineFlags()));
125 continue;
127 if (pStart->GetNode().IsTableNode())
129 assert(pEnd->GetNode() == m_Start.GetNode() && pEnd->GetContentIndex() == 0);
130 continue; // known pathology, ignore it
132 if (*m_pEndPos <= *pStart)
134 pNextRedlineHide = pStart;
135 break; // the next one
140 // position on current or next fieldmark
141 m_oNextFieldmarkHide.reset();
142 if (m_eFieldmarkMode != sw::FieldmarkMode::ShowBoth)
144 sal_Unicode const magic(m_eFieldmarkMode == sw::FieldmarkMode::ShowResult
145 ? CH_TXT_ATR_FIELDSTART
146 : CH_TXT_ATR_FIELDSEP);
147 SwTextNode* pTextNode = m_pEndPos->GetNode().GetTextNode();
148 sal_Int32 const nPos = pTextNode ? pTextNode->GetText().indexOf(
149 magic, m_pEndPos->GetContentIndex()) : -1;
150 if (nPos != -1)
152 m_oNextFieldmarkHide.emplace(*pTextNode, nPos);
153 sw::mark::IFieldmark const*const pFieldmark(
154 m_eFieldmarkMode == sw::FieldmarkMode::ShowResult
155 ? m_rIDMA.getFieldmarkAt(*m_oNextFieldmarkHide)
156 : m_rIDMA.getFieldmarkFor(*m_oNextFieldmarkHide));
157 assert(pFieldmark);
158 m_Fieldmark.first = pFieldmark;
159 // for cursor travelling, there should be 2 visible chars;
160 // whichever char is hidden, the cursor travelling needs to
161 // be adapted in any case to skip in some situation or other;
162 // always hide the CH_TXT_ATR_FIELDSEP for now
163 if (m_eFieldmarkMode == sw::FieldmarkMode::ShowResult)
165 m_Fieldmark.second.emplace(
166 sw::mark::FindFieldSep(*m_Fieldmark.first));
167 m_Fieldmark.second->AdjustContent(+1);
168 m_oNextFieldmarkHide->AdjustContent(+1); // skip start
170 else
172 m_Fieldmark.second.emplace(pFieldmark->GetMarkEnd());
173 m_Fieldmark.second->AdjustContent(-1);
178 // == can happen only if redline starts inside field command, and in
179 // that case redline will end before field separator
180 assert(!pNextRedlineHide || !m_oNextFieldmarkHide
181 || *pNextRedlineHide != *m_oNextFieldmarkHide
182 || *m_rIDRA.GetRedlineTable()[m_RedlineIndex]->End() < *m_Fieldmark.second);
183 if (pNextRedlineHide
184 && (!m_oNextFieldmarkHide || *pNextRedlineHide < *m_oNextFieldmarkHide))
186 SwRangeRedline const*const pRed(m_rIDRA.GetRedlineTable()[m_RedlineIndex]);
187 m_pStartPos = pRed->Start();
188 m_pEndPos = pRed->End();
189 ++m_RedlineIndex;
190 return true;
192 else if (m_oNextFieldmarkHide)
194 assert(!pNextRedlineHide || *m_oNextFieldmarkHide <= *pNextRedlineHide);
195 m_pStartPos = &*m_oNextFieldmarkHide;
196 m_pEndPos = &*m_Fieldmark.second;
197 return true;
199 else
201 assert(!pNextRedlineHide && !m_oNextFieldmarkHide);
202 auto const hasHiddenItem = [](auto const& rNode) {
203 auto const& rpSet(rNode.GetAttr(RES_PARATR_LIST_AUTOFMT).GetStyleHandle());
204 return rpSet ? rpSet->Get(RES_CHRATR_HIDDEN).GetValue() : false;
206 auto const hasBreakBefore = [](SwTextNode const& rNode) {
207 if (rNode.GetAttr(RES_PAGEDESC).GetPageDesc())
209 return true;
211 switch (rNode.GetAttr(RES_BREAK).GetBreak())
213 case SvxBreak::ColumnBefore:
214 case SvxBreak::ColumnBoth:
215 case SvxBreak::PageBefore:
216 case SvxBreak::PageBoth:
217 return true;
218 default:
219 break;
221 return false;
223 auto const hasBreakAfter = [](SwTextNode const& rNode) {
224 switch (rNode.GetAttr(RES_BREAK).GetBreak())
226 case SvxBreak::ColumnAfter:
227 case SvxBreak::ColumnBoth:
228 case SvxBreak::PageAfter:
229 case SvxBreak::PageBoth:
230 return true;
231 default:
232 break;
234 return false;
236 if (m_isHideParagraphBreaks
237 && m_pEndPos->GetNode().IsTextNode() // ooo27109-1.sxw
238 // only merge if next node is also text node
239 && m_pEndPos->GetNodes()[m_pEndPos->GetNodeIndex()+1]->IsTextNode()
240 && hasHiddenItem(*m_pEndPos->GetNode().GetTextNode())
241 // no merge if there's a page break on any node
242 && !hasBreakBefore(*m_pEndPos->GetNodes()[m_pEndPos->GetNodeIndex()+1]->GetTextNode())
243 // first node, see SwTextFrame::GetBreak()
244 && !hasBreakAfter(*m_Start.GetNode().GetTextNode()))
246 m_oParagraphBreak.emplace(
247 SwPosition(*m_pEndPos->GetNode().GetTextNode(), m_pEndPos->GetNode().GetTextNode()->Len()),
248 SwPosition(*m_pEndPos->GetNodes()[m_pEndPos->GetNodeIndex()+1]->GetTextNode(), 0));
249 m_pStartPos = &m_oParagraphBreak->first;
250 m_pEndPos = &m_oParagraphBreak->second;
251 return true;
253 else // nothing
255 m_pStartPos = nullptr;
256 m_pEndPos = nullptr;
257 return false;
265 namespace sw {
267 std::unique_ptr<sw::MergedPara>
268 CheckParaRedlineMerge(SwTextFrame & rFrame, SwTextNode & rTextNode,
269 FrameMode const eMode)
271 if (!rFrame.getRootFrame()->HasMergedParas())
273 return nullptr;
275 bool bHaveRedlines(false);
276 std::vector<SwTextNode *> nodes{ &rTextNode };
277 std::vector<SwTableNode *> tables;
278 std::vector<SwSectionNode *> sections;
279 std::vector<sw::Extent> extents;
280 OUStringBuffer mergedText;
281 SwTextNode * pParaPropsNode(nullptr);
282 SwTextNode * pNode(&rTextNode);
283 sal_Int32 nLastEnd(0);
284 for (auto iter = HideIterator(rTextNode,
285 rFrame.getRootFrame()->IsHideRedlines(),
286 rFrame.getRootFrame()->GetFieldmarkMode(),
287 rFrame.getRootFrame()->GetParagraphBreakMode());
288 iter.Next(); )
290 SwPosition const*const pStart(iter.GetStartPos());
291 SwPosition const*const pEnd(iter.GetEndPos());
292 bHaveRedlines = true;
293 assert(pNode != &rTextNode || &pStart->GetNode() == &rTextNode); // detect calls with wrong start node
294 if (pStart->GetContentIndex() != nLastEnd) // not 0 so we eliminate adjacent deletes
296 extents.emplace_back(pNode, nLastEnd, pStart->GetContentIndex());
297 mergedText.append(pNode->GetText().subView(nLastEnd, pStart->GetContentIndex() - nLastEnd));
299 if (&pEnd->GetNode() != pNode)
301 if (pNode == &rTextNode)
303 pNode->SetRedlineMergeFlag(SwNode::Merge::First);
304 } // else: was already set before
305 int nLevel(0);
306 for (SwNodeOffset j = pNode->GetIndex() + 1; j < pEnd->GetNodeIndex(); ++j)
308 SwNode *const pTmp(pNode->GetNodes()[j]);
309 if (nLevel == 0)
311 if (pTmp->IsTextNode())
313 nodes.push_back(pTmp->GetTextNode());
315 else if (pTmp->IsTableNode())
317 tables.push_back(pTmp->GetTableNode());
319 else if (pTmp->IsSectionNode())
321 sections.push_back(pTmp->GetSectionNode());
324 if (pTmp->IsStartNode())
326 ++nLevel;
328 else if (pTmp->IsEndNode())
330 --nLevel;
332 pTmp->SetRedlineMergeFlag(SwNode::Merge::Hidden);
334 // note: in DelLastPara() case, the end node is not actually merged
335 // and is likely a SwTableNode!
336 if (!pEnd->GetNode().IsTextNode())
338 assert(pEnd->GetNode() != pStart->GetNode());
339 // must set pNode too because it will mark the last node
340 pNode = nodes.back();
341 assert(pNode == pNode->GetNodes()[pEnd->GetNodeIndex() - 1]);
342 if (pNode != &rTextNode)
343 { // something might depend on last merged one being NonFirst?
344 pNode->SetRedlineMergeFlag(SwNode::Merge::NonFirst);
346 nLastEnd = pNode->Len();
348 else
350 pNode = pEnd->GetNode().GetTextNode();
351 nodes.push_back(pNode);
352 pNode->SetRedlineMergeFlag(SwNode::Merge::NonFirst);
353 nLastEnd = pEnd->GetContentIndex();
356 else
358 nLastEnd = pEnd->GetContentIndex();
361 if (pNode == &rTextNode)
363 if (rTextNode.GetRedlineMergeFlag() != SwNode::Merge::None)
365 rTextNode.SetRedlineMergeFlag(SwNode::Merge::None);
368 // Reset flag of the following text node since we know it's not merged;
369 // also any table/sections in between.
370 // * the following SwTextNode is in same nodes section as pNode (nLevel=0)
371 // * the start nodes that don't have a SwTextNode before them
372 // on their level, and their corresponding end nodes
373 // * the first SwTextNode inside each start node of the previous point
374 // Other (non-first) SwTextNodes in nested sections shouldn't be reset!
375 int nLevel(0);
376 for (SwNodeOffset j = pNode->GetIndex() + 1; j < pNode->GetNodes().Count(); ++j)
378 SwNode *const pTmp(pNode->GetNodes()[j]);
379 if (!pTmp->IsCreateFrameWhenHidingRedlines())
380 { // clear stale flag caused by editing with redlines shown
381 pTmp->SetRedlineMergeFlag(SwNode::Merge::None);
383 if (pTmp->IsStartNode())
385 ++nLevel;
387 else if (pTmp->IsEndNode())
389 if (nLevel == 0)
391 break; // there is no following text node; avoid leaving section
393 --nLevel;
395 else if (pTmp->IsTextNode())
397 if (nLevel == 0)
399 break; // done
401 else
402 { // skip everything other than 1st text node in section!
403 j = pTmp->EndOfSectionIndex() - 1; // will be incremented again
407 if (!bHaveRedlines)
409 if (rTextNode.IsInList() && !rTextNode.GetNum(rFrame.getRootFrame()))
411 rTextNode.AddToListRLHidden(); // try to add it...
413 return nullptr;
415 if (nLastEnd != pNode->Len())
417 extents.emplace_back(pNode, nLastEnd, pNode->Len());
418 mergedText.append(pNode->GetText().subView(nLastEnd, pNode->Len() - nLastEnd));
420 if (extents.empty()) // there was no text anywhere
422 assert(mergedText.isEmpty());
423 pParaPropsNode = pNode; // if every node is empty, the last one wins
425 else
427 assert(!mergedText.isEmpty());
428 pParaPropsNode = extents.begin()->pNode; // para props from first node that isn't empty
430 // pParaPropsNode = &rTextNode; // well, actually...
431 // keep lists up to date with visible nodes
432 if (pParaPropsNode->IsInList() && !pParaPropsNode->GetNum(rFrame.getRootFrame()))
434 pParaPropsNode->AddToListRLHidden(); // try to add it...
436 for (auto const pTextNode : nodes)
438 if (pTextNode != pParaPropsNode)
440 pTextNode->RemoveFromListRLHidden();
443 if (eMode == FrameMode::Existing)
445 // remove existing footnote frames for first node;
446 // for non-first nodes with own frames, DelFrames will remove all
447 // (could possibly call lcl_ChangeFootnoteRef, not sure if worth it)
448 // note: must be done *before* changing listeners!
449 // for non-first nodes that are already merged with this frame,
450 // need to remove here too, otherwise footnotes can be removed only
451 // by lucky accident, e.g. TruncLines().
452 auto itExtent(extents.begin());
453 for (auto const pTextNode : nodes)
455 sal_Int32 nLast(0);
456 std::vector<std::pair<sal_Int32, sal_Int32>> hidden;
457 for ( ; itExtent != extents.end(); ++itExtent)
459 if (itExtent->pNode != pTextNode)
461 break;
463 if (itExtent->nStart != 0)
465 assert(itExtent->nStart != nLast);
466 hidden.emplace_back(nLast, itExtent->nStart);
468 nLast = itExtent->nEnd;
470 if (nLast != pTextNode->Len())
472 hidden.emplace_back(nLast, pTextNode->Len());
474 sw::RemoveFootnotesForNode(*rFrame.getRootFrame(), *pTextNode, &hidden);
476 // unfortunately DelFrames() must be done before StartListening too,
477 // otherwise footnotes cannot be deleted by SwTextFootnote::DelFrames!
478 auto const end(--nodes.rend());
479 for (auto iter = nodes.rbegin(); iter != end; ++iter)
481 (**iter).DelFrames(rFrame.getRootFrame());
483 // also delete tables & sections here; not necessary, but convenient
484 for (auto const pTableNode : tables)
486 pTableNode->DelFrames(rFrame.getRootFrame());
488 for (auto const pSectionNode : sections)
490 pSectionNode->GetSection().GetFormat()->DelFrames(/*rFrame.getRootFrame()*/);
493 auto pRet(std::make_unique<sw::MergedPara>(rFrame, std::move(extents),
494 mergedText.makeStringAndClear(), pParaPropsNode, &rTextNode,
495 nodes.back()));
496 for (SwTextNode * pTmp : nodes)
498 pRet->listener.StartListening(pTmp);
500 rFrame.EndListeningAll();
501 return pRet;
504 } // namespace sw
506 void SwAttrIter::InitFontAndAttrHandler(
507 SwTextNode const& rPropsNode,
508 SwTextNode const& rTextNode,
509 std::u16string_view aText,
510 bool const*const pbVertLayout,
511 bool const*const pbVertLayoutLRBT)
513 // Build a font matching the default paragraph style:
514 SwFontAccess aFontAccess( &rPropsNode.GetAnyFormatColl(), m_pViewShell );
515 // It is possible that Init is called more than once, e.g., in a
516 // SwTextFrame::FormatOnceMore situation or (since sw_redlinehide)
517 // from SwAttrIter::Seek(); in the latter case SwTextSizeInfo::m_pFnt
518 // is an alias of m_pFont so it must not be deleted!
519 if (m_pFont)
521 *m_pFont = aFontAccess.Get()->GetFont();
523 else
525 m_pFont = new SwFont( aFontAccess.Get()->GetFont() );
528 // set font to vertical if frame layout is vertical
529 // if it's a re-init, the vert flag never changes
530 bool bVertLayoutLRBT = false;
531 if (pbVertLayoutLRBT)
532 bVertLayoutLRBT = *pbVertLayoutLRBT;
533 if (pbVertLayout ? *pbVertLayout : m_aAttrHandler.IsVertLayout())
535 m_pFont->SetVertical(m_pFont->GetOrientation(), true, bVertLayoutLRBT);
538 // Initialize the default attribute of the attribute handler
539 // based on the attribute array cached together with the font.
540 // If any further attributes for the paragraph are given in pAttrSet
541 // consider them during construction of the default array, and apply
542 // them to the font
543 m_aAttrHandler.Init(aFontAccess.Get()->GetDefault(), rTextNode.GetpSwAttrSet(),
544 *rTextNode.getIDocumentSettingAccess(), m_pViewShell, *m_pFont,
545 pbVertLayout ? *pbVertLayout : m_aAttrHandler.IsVertLayout(),
546 bVertLayoutLRBT );
548 m_aFontCacheIds[SwFontScript::Latin] = m_aFontCacheIds[SwFontScript::CJK] = m_aFontCacheIds[SwFontScript::CTL] = nullptr;
550 assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is());
552 m_pFont->SetActual( m_pScriptInfo->WhichFont(TextFrameIndex(0)) );
554 TextFrameIndex nChg(0);
555 size_t nCnt = 0;
559 if ( nCnt >= m_pScriptInfo->CountScriptChg() )
560 break;
561 nChg = m_pScriptInfo->GetScriptChg( nCnt );
562 SwFontScript nTmp = SW_SCRIPTS;
563 switch ( m_pScriptInfo->GetScriptType( nCnt++ ) ) {
564 case i18n::ScriptType::ASIAN :
565 if( !m_aFontCacheIds[SwFontScript::CJK] ) nTmp = SwFontScript::CJK;
566 break;
567 case i18n::ScriptType::COMPLEX :
568 if( !m_aFontCacheIds[SwFontScript::CTL] ) nTmp = SwFontScript::CTL;
569 break;
570 default:
571 if( !m_aFontCacheIds[SwFontScript::Latin ] ) nTmp = SwFontScript::Latin;
573 if( nTmp < SW_SCRIPTS )
575 m_pFont->CheckFontCacheId( m_pViewShell, nTmp );
576 m_pFont->GetFontCacheId( m_aFontCacheIds[ nTmp ], m_aFontIdx[ nTmp ], nTmp );
579 while (nChg < TextFrameIndex(aText.size()));
582 void SwAttrIter::CtorInitAttrIter(SwTextNode & rTextNode,
583 SwScriptInfo & rScriptInfo, SwTextFrame const*const pFrame)
585 // during HTML-Import it can happen, that no layout exists
586 SwRootFrame* pRootFrame = rTextNode.getIDocumentLayoutAccess().GetCurrentLayout();
587 m_pViewShell = pRootFrame ? pRootFrame->GetCurrShell() : nullptr;
589 m_pScriptInfo = &rScriptInfo;
591 // set font to vertical if frame layout is vertical
592 bool bVertLayout = false;
593 bool bVertLayoutLRBT = false;
594 bool bRTL = false;
595 if ( pFrame )
597 if ( pFrame->IsVertical() )
599 bVertLayout = true;
601 if (pFrame->IsVertLRBT())
603 bVertLayoutLRBT = true;
605 bRTL = pFrame->IsRightToLeft();
606 m_pMergedPara = pFrame->GetMergedPara();
609 // determine script changes if not already done for current paragraph
610 assert(m_pScriptInfo);
611 if (m_pScriptInfo->GetInvalidityA() != TextFrameIndex(COMPLETE_STRING))
612 m_pScriptInfo->InitScriptInfo(rTextNode, m_pMergedPara, bRTL);
614 InitFontAndAttrHandler(
615 m_pMergedPara ? *m_pMergedPara->pParaPropsNode : rTextNode,
616 rTextNode,
617 m_pMergedPara ? m_pMergedPara->mergedText : rTextNode.GetText(),
618 & bVertLayout,
619 & bVertLayoutLRBT);
621 m_nStartIndex = m_nEndIndex = m_nPosition = m_nChgCnt = 0;
622 m_nPropFont = 0;
623 SwDoc& rDoc = rTextNode.GetDoc();
624 const IDocumentRedlineAccess& rIDRA = rTextNode.getIDocumentRedlineAccess();
626 // sw_redlinehide: this is a Ring - pExtInp is the first PaM that's inside
627 // the node. It's not clear whether there can be more than 1 PaM in the
628 // Ring, and this code doesn't handle that case; neither did the old code.
629 const SwExtTextInput* pExtInp = rDoc.GetExtTextInput( rTextNode );
630 if (!pExtInp && m_pMergedPara)
632 SwTextNode const* pNode(&rTextNode);
633 for (auto const& rExtent : m_pMergedPara->extents)
635 if (rExtent.pNode != pNode)
637 pNode = rExtent.pNode;
638 pExtInp = rDoc.GetExtTextInput(*pNode);
639 if (pExtInp)
640 break;
644 const bool bShow = IDocumentRedlineAccess::IsShowChanges(rIDRA.GetRedlineFlags())
645 && pRootFrame && !pRootFrame->IsHideRedlines();
646 if (!(pExtInp || m_pMergedPara || bShow))
647 return;
649 SwRedlineTable::size_type nRedlPos = rIDRA.GetRedlinePos( rTextNode, RedlineType::Any );
650 if (SwRedlineTable::npos == nRedlPos && m_pMergedPara)
652 SwTextNode const* pNode(&rTextNode);
653 for (auto const& rExtent : m_pMergedPara->extents)
654 { // note: have to search because extents based only on Delete
655 if (rExtent.pNode != pNode)
657 pNode = rExtent.pNode;
658 nRedlPos = rIDRA.GetRedlinePos(*pNode, RedlineType::Any);
659 if (SwRedlineTable::npos != nRedlPos)
660 break;
663 // TODO this is true initially but after delete ops it may be false... need to delete m_pMerged somewhere?
664 // assert(SwRedlineTable::npos != nRedlPos);
665 // false now with fieldmarks
666 assert(!pRootFrame
667 || pRootFrame->GetFieldmarkMode() != sw::FieldmarkMode::ShowBoth
668 || SwRedlineTable::npos != nRedlPos || m_pMergedPara->extents.size() <= 1);
670 if (!(pExtInp || m_pMergedPara || SwRedlineTable::npos != nRedlPos))
671 return;
673 const std::vector<ExtTextInputAttr> *pArr = nullptr;
674 if( pExtInp )
676 pArr = &pExtInp->GetAttrs();
677 Seek( TextFrameIndex(0) );
680 m_pRedline.reset(new SwRedlineItr( rTextNode, *m_pFont, m_aAttrHandler, nRedlPos,
681 (pRootFrame && pRootFrame->IsHideRedlines())
682 ? SwRedlineItr::Mode::Hide
683 : bShow
684 ? SwRedlineItr::Mode::Show
685 : SwRedlineItr::Mode::Ignore,
686 pArr, pExtInp ? pExtInp->Start() : nullptr));
688 if( m_pRedline->IsOn() )
689 ++m_nChgCnt;
692 // The Redline-Iterator
693 // The following information/states exist in RedlineIterator:
695 // m_nFirst is the first index of RedlineTable, which overlaps with the paragraph.
697 // m_nAct is the currently active (if m_bOn is set) or the next possible index.
698 // m_nStart and m_nEnd give you the borders of the object within the paragraph.
700 // If m_bOn is set, the font has been manipulated according to it.
702 // If m_nAct is set to SwRedlineTable::npos (via Reset()), then currently no
703 // Redline is active, m_nStart and m_nEnd are invalid.
704 SwRedlineItr::SwRedlineItr( const SwTextNode& rTextNd, SwFont& rFnt,
705 SwAttrHandler& rAH, sal_Int32 nRed,
706 Mode const mode,
707 const std::vector<ExtTextInputAttr> *pArr,
708 SwPosition const*const pExtInputStart)
709 : m_rDoc( rTextNd.GetDoc() )
710 , m_rAttrHandler( rAH )
711 , m_nNdIdx( rTextNd.GetIndex() )
712 , m_nFirst( nRed )
713 , m_nAct( SwRedlineTable::npos )
714 , m_nStart( COMPLETE_STRING )
715 , m_nEnd( COMPLETE_STRING )
716 , m_bOn( false )
717 , m_eMode( mode )
719 if( pArr )
721 assert(pExtInputStart);
722 m_pExt.reset( new SwExtend(*pArr, pExtInputStart->GetNodeIndex(),
723 pExtInputStart->GetContentIndex()) );
725 else
726 m_pExt = nullptr;
727 assert(m_pExt || m_eMode != Mode::Ignore); // only create if necessary
728 Seek(rFnt, m_nNdIdx, 0, COMPLETE_STRING);
731 SwRedlineItr::~SwRedlineItr() COVERITY_NOEXCEPT_FALSE
733 Clear( nullptr );
734 m_pExt.reset();
737 // The return value of SwRedlineItr::Seek tells you if the current font
738 // has been manipulated by leaving (-1) or accessing (+1) of a section
739 short SwRedlineItr::Seek(SwFont& rFnt,
740 SwNodeOffset const nNode, sal_Int32 const nNew, sal_Int32 const nOld)
742 short nRet = 0;
743 if( ExtOn() )
744 return 0; // Abbreviation: if we're within an ExtendTextInputs
745 // there can't be other changes of attributes (not even by redlining)
746 if (m_eMode == Mode::Show)
748 if (m_bOn)
750 if (nNew >= m_nEnd)
752 --nRet;
753 Clear_( &rFnt ); // We go behind the current section
754 ++m_nAct; // and check the next one
756 else if (nNew < m_nStart)
758 --nRet;
759 Clear_( &rFnt ); // We go in front of the current section
760 if (m_nAct > m_nFirst)
761 m_nAct = m_nFirst; // the test has to run from the beginning
762 else
763 return nRet + EnterExtend(rFnt, nNode, nNew); // There's none prior to us
765 else
766 return nRet + EnterExtend(rFnt, nNode, nNew); // We stayed in the same section
768 if (SwRedlineTable::npos == m_nAct || nOld > nNew)
769 m_nAct = m_nFirst;
771 m_nStart = COMPLETE_STRING;
772 m_nEnd = COMPLETE_STRING;
773 const SwRedlineTable& rTable = m_rDoc.getIDocumentRedlineAccess().GetRedlineTable();
775 for ( ; m_nAct < rTable.size() ; ++m_nAct)
777 rTable[ m_nAct ]->CalcStartEnd(nNode, m_nStart, m_nEnd);
779 if (nNew < m_nEnd)
781 if (nNew >= m_nStart) // only possible candidate
783 m_bOn = true;
784 const SwRangeRedline *pRed = rTable[ m_nAct ];
786 if (m_pSet)
787 m_pSet->ClearItem();
788 else
790 SwAttrPool& rPool =
791 const_cast<SwDoc&>(m_rDoc).GetAttrPool();
792 m_pSet = std::make_unique<SfxItemSetFixed<RES_CHRATR_BEGIN, RES_CHRATR_END-1>>(rPool);
795 if( 1 < pRed->GetStackCount() )
796 FillHints( pRed->GetAuthor( 1 ), pRed->GetType( 1 ) );
797 FillHints( pRed->GetAuthor(), pRed->GetType() );
799 SfxWhichIter aIter( *m_pSet );
801 // moved text: dark green with double underline or strikethrough
802 if ( pRed->IsMoved() )
804 m_pSet->Put(SvxColorItem( COL_GREEN, RES_CHRATR_COLOR ));
805 if (SfxItemState::SET == m_pSet->GetItemState(RES_CHRATR_CROSSEDOUT, true))
806 m_pSet->Put(SvxCrossedOutItem( STRIKEOUT_DOUBLE, RES_CHRATR_CROSSEDOUT ));
807 else
808 m_pSet->Put(SvxUnderlineItem( LINESTYLE_DOUBLE, RES_CHRATR_UNDERLINE ));
811 sal_uInt16 nWhich = aIter.FirstWhich();
812 while( nWhich )
814 const SfxPoolItem* pItem;
815 if( ( nWhich < RES_CHRATR_END ) &&
816 ( SfxItemState::SET == aIter.GetItemState( true, &pItem ) ) )
818 SwTextAttr* pAttr = MakeRedlineTextAttr(
819 const_cast<SwDoc&>(m_rDoc),
820 *const_cast<SfxPoolItem*>(pItem) );
821 pAttr->SetPriorityAttr( true );
822 m_Hints.push_back(pAttr);
823 m_rAttrHandler.PushAndChg( *pAttr, rFnt );
825 nWhich = aIter.NextWhich();
828 ++nRet;
830 break;
832 m_nStart = COMPLETE_STRING;
833 m_nEnd = COMPLETE_STRING;
836 else if (m_eMode == Mode::Hide)
837 { // ... just iterate to update m_nAct for GetNextRedln();
838 // there is no need to care about formatting in this mode
839 if (m_nAct == SwRedlineTable::npos || nOld == COMPLETE_STRING)
840 { // reset, or move backward
841 m_nAct = m_nFirst;
843 for ( ; m_nAct < m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size(); ++m_nAct)
844 { // only Start matters in this mode
845 // Seeks until it finds a RL that starts at or behind the seek pos.
846 // - then update m_nStart/m_nEnd to the intersection of it with the
847 // current node (if any).
848 // The only way to skip to a different node is if there is a Delete
849 // RL, so if there is no intersection we'll never skip again.
850 // Note: here, assume that delete can't nest inside delete!
851 SwRangeRedline const*const pRedline(
852 m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[m_nAct]);
853 SwPosition const*const pStart(pRedline->Start());
854 if (pRedline->GetType() == RedlineType::Delete
855 && (nNode < pStart->GetNodeIndex()
856 || (nNode == pStart->GetNodeIndex()
857 && nNew <= pStart->GetContentIndex())))
859 pRedline->CalcStartEnd(nNode, m_nStart, m_nEnd);
860 break;
862 m_nStart = COMPLETE_STRING;
863 m_nEnd = COMPLETE_STRING;
866 return nRet + EnterExtend(rFnt, nNode, nNew);
869 void SwRedlineItr::FillHints( std::size_t nAuthor, RedlineType eType )
871 switch ( eType )
873 case RedlineType::Insert:
874 SW_MOD()->GetInsertAuthorAttr(nAuthor, *m_pSet);
875 break;
876 case RedlineType::Delete:
877 SW_MOD()->GetDeletedAuthorAttr(nAuthor, *m_pSet);
878 break;
879 case RedlineType::Format:
880 case RedlineType::FmtColl:
881 SW_MOD()->GetFormatAuthorAttr(nAuthor, *m_pSet);
882 break;
883 default:
884 break;
888 void SwRedlineItr::ChangeTextAttr( SwFont* pFnt, SwTextAttr const &rHt, bool bChg )
890 OSL_ENSURE( IsOn(), "SwRedlineItr::ChangeTextAttr: Off?" );
892 if (m_eMode != Mode::Show && !m_pExt)
893 return;
895 if( bChg )
897 if (m_pExt && m_pExt->IsOn())
898 m_rAttrHandler.PushAndChg( rHt, *m_pExt->GetFont() );
899 else
900 m_rAttrHandler.PushAndChg( rHt, *pFnt );
902 else
904 OSL_ENSURE( ! m_pExt || ! m_pExt->IsOn(), "Pop of attribute during opened extension" );
905 m_rAttrHandler.PopAndChg( rHt, *pFnt );
909 void SwRedlineItr::Clear_( SwFont* pFnt )
911 OSL_ENSURE( m_bOn, "SwRedlineItr::Clear: Off?" );
912 m_bOn = false;
913 for (auto const& hint : m_Hints)
915 if( pFnt )
916 m_rAttrHandler.PopAndChg( *hint, *pFnt );
917 else
918 m_rAttrHandler.Pop( *hint );
919 SwTextAttr::Destroy(hint, const_cast<SwDoc&>(m_rDoc).GetAttrPool() );
921 m_Hints.clear();
924 /// Ignore mode: does nothing.
925 /// Show mode: returns end of redline if currently in one, or start of next
926 /// Hide mode: returns start of next redline in current node, plus (if it's a
927 /// Delete) its end position and number of consecutive RLs
928 std::pair<sal_Int32, std::pair<SwRangeRedline const*, size_t>>
929 SwRedlineItr::GetNextRedln(sal_Int32 nNext, SwTextNode const*const pNode,
930 SwRedlineTable::size_type & rAct)
932 sal_Int32 nStart(m_nStart);
933 sal_Int32 nEnd(m_nEnd);
934 nNext = NextExtend(pNode->GetIndex(), nNext);
935 if (m_eMode == Mode::Ignore || SwRedlineTable::npos == m_nFirst)
936 return std::make_pair(nNext, std::make_pair(nullptr, 0));
937 if (SwRedlineTable::npos == rAct)
939 rAct = m_nFirst;
941 if (rAct != m_nAct)
943 while (rAct < m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size())
945 SwRangeRedline const*const pRedline(
946 m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[rAct]);
947 pRedline->CalcStartEnd(pNode->GetIndex(), nStart, nEnd);
948 if (m_eMode != Mode::Hide
949 || pRedline->GetType() == RedlineType::Delete)
951 break;
953 ++rAct; // Hide mode: search a Delete RL
956 if (rAct == m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size())
958 return std::make_pair(nNext, std::make_pair(nullptr, 0)); // no Delete here
960 if (m_bOn || (m_eMode == Mode::Show && nStart == 0))
961 { // in Ignore mode, the end of redlines isn't relevant, except as returned in the second in the pair!
962 if (nEnd < nNext)
963 nNext = nEnd;
965 else if (nStart <= nNext)
967 if (m_eMode == Mode::Show)
969 nNext = nStart;
971 else
973 assert(m_eMode == Mode::Hide);
974 SwRangeRedline const* pRedline(
975 m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[rAct]);
976 assert(pRedline->GetType() == RedlineType::Delete); //?
977 if (pRedline->GetType() == RedlineType::Delete)
979 nNext = nStart;
980 size_t nSkipped(1); // (consecutive) candidates to be skipped
981 while (rAct + nSkipped <
982 m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size())
984 SwRangeRedline const*const pNext =
985 m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[rAct + nSkipped];
986 if (*pRedline->End() < *pNext->Start())
988 break; // done for now
990 else if (*pNext->Start() == *pRedline->End() &&
991 pNext->GetType() == RedlineType::Delete)
993 // consecutive delete - continue
994 pRedline = pNext;
996 ++nSkipped;
998 return std::make_pair(nNext, std::make_pair(pRedline, nSkipped));
1002 return std::make_pair(nNext, std::make_pair(nullptr, 0));
1005 bool SwRedlineItr::ChkSpecialUnderline_() const
1007 // If the underlining or the escapement is caused by redlining,
1008 // we always apply the SpecialUnderlining, i.e. the underlining
1009 // below the base line
1010 for (SwTextAttr* pHint : m_Hints)
1012 const sal_uInt16 nWhich = pHint->Which();
1013 if( RES_CHRATR_UNDERLINE == nWhich ||
1014 RES_CHRATR_ESCAPEMENT == nWhich )
1015 return true;
1017 return false;
1020 bool SwRedlineItr::CheckLine(
1021 SwNodeOffset const nStartNode, sal_Int32 const nChkStart,
1022 SwNodeOffset const nEndNode, sal_Int32 nChkEnd, OUString& rRedlineText,
1023 bool& bRedlineEnd, RedlineType& eRedlineEnd, size_t* pAuthorAtPos)
1025 // note: previously this would return true in the (!m_bShow && m_pExt)
1026 // case, but surely that was a bug?
1027 if (m_nFirst == SwRedlineTable::npos || m_eMode != Mode::Show)
1028 return false;
1029 if( nChkEnd == nChkStart && pAuthorAtPos == nullptr ) // empty lines look one char further
1030 ++nChkEnd;
1031 sal_Int32 nOldStart = m_nStart;
1032 sal_Int32 nOldEnd = m_nEnd;
1033 SwRedlineTable::size_type const nOldAct = m_nAct;
1034 bool bRet = bRedlineEnd = false;
1035 eRedlineEnd = RedlineType::None;
1037 SwPosition const start(*m_rDoc.GetNodes()[nStartNode]->GetContentNode(), nChkStart);
1038 SwPosition const end(*m_rDoc.GetNodes()[nEndNode]->GetContentNode(), nChkEnd);
1039 SwRangeRedline const* pPrevRedline = nullptr;
1040 bool isBreak(false);
1041 for (m_nAct = m_nFirst; m_nAct < m_rDoc.getIDocumentRedlineAccess().GetRedlineTable().size(); ++m_nAct)
1043 SwRangeRedline const*const pRedline(
1044 m_rDoc.getIDocumentRedlineAccess().GetRedlineTable()[ m_nAct ] );
1045 // collect text of the hidden redlines at the end of the line
1046 bool isExtendText(false);
1047 switch (ComparePosition(*pRedline->Start(), *pRedline->End(), start, end))
1049 case SwComparePosition::Behind:
1050 isBreak = true;
1051 break;
1052 case SwComparePosition::OverlapBehind:
1053 case SwComparePosition::CollideStart:
1054 case SwComparePosition::Outside:
1055 case SwComparePosition::Equal:
1056 // store redlining at line end (for line break formatting)
1057 eRedlineEnd = pRedline->GetType();
1058 bRedlineEnd = true;
1059 isBreak = true;
1060 if (pAuthorAtPos)
1061 *pAuthorAtPos = pRedline->GetAuthor();
1062 [[fallthrough]];
1063 case SwComparePosition::OverlapBefore:
1064 case SwComparePosition::CollideEnd:
1065 case SwComparePosition::Inside:
1067 bRet = true;
1068 // start to collect text of invisible redlines for ChangesInMargin layout
1069 if (rRedlineText.isEmpty() && !pRedline->IsVisible())
1071 rRedlineText = const_cast<SwRangeRedline*>(pRedline)->GetDescr(/*bSimplified=*/true);
1072 pPrevRedline = pRedline;
1073 isExtendText = true;
1075 // join the text of the next invisible redlines in the same position
1076 // i.e. characters deleted by pressing backspace or delete
1077 else if (pPrevRedline && !pRedline->IsVisible() &&
1078 *pRedline->Start() == *pPrevRedline->Start() && *pRedline->End() == *pPrevRedline->End() )
1080 OUString sExtendText(const_cast<SwRangeRedline*>(pRedline)->GetDescr(/*bSimplified=*/true));
1081 if (!sExtendText.isEmpty())
1083 if (rRedlineText.getLength() < 12)
1085 // TODO: remove extra space from GetDescr(true),
1086 // but show deletion of paragraph or line break
1087 rRedlineText = rRedlineText +
1088 const_cast<SwRangeRedline*>(pRedline)->GetDescr(/*bSimplified=*/true).subView(1);
1090 else
1091 rRedlineText = OUString::Concat(rRedlineText.subView(0, rRedlineText.getLength() - 3)) + "...";
1093 isExtendText = true;
1095 break;
1097 case SwComparePosition::Before:
1098 break; // -Werror=switch
1100 if (isBreak && !isExtendText)
1102 break;
1106 m_nStart = nOldStart;
1107 m_nEnd = nOldEnd;
1108 m_nAct = nOldAct;
1109 return bRet;
1112 void SwExtend::ActualizeFont( SwFont &rFnt, ExtTextInputAttr nAttr )
1114 if ( nAttr & ExtTextInputAttr::Underline )
1115 rFnt.SetUnderline( LINESTYLE_SINGLE );
1116 else if ( nAttr & ExtTextInputAttr::DoubleUnderline )
1117 rFnt.SetUnderline( LINESTYLE_DOUBLE );
1118 else if ( nAttr & ExtTextInputAttr::BoldUnderline )
1119 rFnt.SetUnderline( LINESTYLE_BOLD );
1120 else if ( nAttr & ExtTextInputAttr::DottedUnderline )
1121 rFnt.SetUnderline( LINESTYLE_DOTTED );
1122 else if ( nAttr & ExtTextInputAttr::DashDotUnderline )
1123 rFnt.SetUnderline( LINESTYLE_DOTTED );
1125 if ( nAttr & ExtTextInputAttr::RedText )
1126 rFnt.SetColor( COL_RED );
1128 if ( nAttr & ExtTextInputAttr::Highlight )
1130 const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings();
1131 rFnt.SetColor( rStyleSettings.GetHighlightTextColor() );
1132 rFnt.SetBackColor( rStyleSettings.GetHighlightColor() );
1134 if ( nAttr & ExtTextInputAttr::GrayWaveline )
1135 rFnt.SetGreyWave( true );
1138 short SwExtend::Enter(SwFont& rFnt, SwNodeOffset const nNode, sal_Int32 const nNew)
1140 OSL_ENSURE( !m_pFont, "SwExtend: Enter with Font" );
1141 if (nNode != m_nNode)
1142 return 0;
1143 OSL_ENSURE( !Inside(), "SwExtend: Enter without Leave" );
1144 m_nPos = nNew;
1145 if( Inside() )
1147 m_pFont.reset( new SwFont(rFnt) );
1148 ActualizeFont( rFnt, m_rArr[m_nPos - m_nStart] );
1149 return 1;
1151 return 0;
1154 bool SwExtend::Leave_(SwFont& rFnt, SwNodeOffset const nNode, sal_Int32 const nNew)
1156 OSL_ENSURE(nNode == m_nNode && Inside(), "SwExtend: Leave without Enter");
1157 if (nNode != m_nNode)
1158 return true;
1159 const ExtTextInputAttr nOldAttr = m_rArr[m_nPos - m_nStart];
1160 m_nPos = nNew;
1161 if( Inside() )
1162 { // We stayed within the ExtendText-section
1163 const ExtTextInputAttr nAttr = m_rArr[m_nPos - m_nStart];
1164 if( nOldAttr != nAttr ) // Is there an (inner) change of attributes?
1166 rFnt = *m_pFont;
1167 ActualizeFont( rFnt, nAttr );
1170 else
1172 rFnt = *m_pFont;
1173 m_pFont.reset();
1174 return true;
1176 return false;
1179 sal_Int32 SwExtend::Next(SwNodeOffset const nNode, sal_Int32 nNext)
1181 if (nNode != m_nNode)
1182 return nNext;
1183 if (m_nPos < m_nStart)
1185 if (nNext > m_nStart)
1186 nNext = m_nStart;
1188 else if (m_nPos < m_nEnd)
1190 sal_Int32 nIdx = m_nPos - m_nStart;
1191 const ExtTextInputAttr nAttr = m_rArr[ nIdx ];
1192 while (o3tl::make_unsigned(++nIdx) < m_rArr.size() && nAttr == m_rArr[nIdx])
1193 ; //nothing
1194 nIdx = nIdx + m_nStart;
1195 if( nNext > nIdx )
1196 nNext = nIdx;
1198 return nNext;
1201 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */