summaryrefslogtreecommitdiffstats
path: root/sw/source/core/txtnode/txtedt.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'sw/source/core/txtnode/txtedt.cxx')
-rw-r--r--sw/source/core/txtnode/txtedt.cxx2288
1 files changed, 2288 insertions, 0 deletions
diff --git a/sw/source/core/txtnode/txtedt.cxx b/sw/source/core/txtnode/txtedt.cxx
new file mode 100644
index 000000000..cbc5f4f39
--- /dev/null
+++ b/sw/source/core/txtnode/txtedt.cxx
@@ -0,0 +1,2288 @@
+/* -*- 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 <svl/itemiter.hxx>
+#include <svl/languageoptions.hxx>
+#include <editeng/splwrap.hxx>
+#include <editeng/langitem.hxx>
+#include <editeng/fontitem.hxx>
+#include <editeng/hangulhanja.hxx>
+#include <i18nutil/transliteration.hxx>
+#include <SwSmartTagMgr.hxx>
+#include <o3tl/safeint.hxx>
+#include <officecfg/Office/Writer.hxx>
+#include <unotools/transliterationwrapper.hxx>
+#include <unotools/charclass.hxx>
+#include <sal/log.hxx>
+#include <swmodule.hxx>
+#include <splargs.hxx>
+#include <viewopt.hxx>
+#include <acmplwrd.hxx>
+#include <doc.hxx>
+#include <IDocumentRedlineAccess.hxx>
+#include <IDocumentLayoutAccess.hxx>
+#include <docsh.hxx>
+#include <txtfld.hxx>
+#include <txatbase.hxx>
+#include <charatr.hxx>
+#include <pam.hxx>
+#include <hints.hxx>
+#include <ndtxt.hxx>
+#include <txtfrm.hxx>
+#include <SwGrammarMarkUp.hxx>
+#include <rootfrm.hxx>
+#include <swscanner.hxx>
+
+#include <breakit.hxx>
+#include <UndoOverwrite.hxx>
+#include <txatritr.hxx>
+#include <redline.hxx>
+#include <docary.hxx>
+#include <scriptinfo.hxx>
+#include <docstat.hxx>
+#include <editsh.hxx>
+#include <unotextmarkup.hxx>
+#include <txtatr.hxx>
+#include <fmtautofmt.hxx>
+#include <istyleaccess.hxx>
+#include <unicode/uchar.h>
+#include <DocumentSettingManager.hxx>
+
+#include <com/sun/star/i18n/WordType.hpp>
+#include <com/sun/star/i18n/ScriptType.hpp>
+#include <com/sun/star/i18n/XBreakIterator.hpp>
+
+#include <vector>
+#include <utility>
+
+#include <unotextrange.hxx>
+
+using namespace ::com::sun::star;
+using namespace ::com::sun::star::frame;
+using namespace ::com::sun::star::i18n;
+using namespace ::com::sun::star::beans;
+using namespace ::com::sun::star::uno;
+using namespace ::com::sun::star::linguistic2;
+using namespace ::com::sun::star::smarttags;
+
+namespace
+{
+ void DetectAndMarkMissingDictionaries( SwDoc* pDoc,
+ const uno::Reference< XSpellChecker1 >& xSpell,
+ const LanguageType eActLang )
+ {
+ if( !pDoc )
+ return;
+
+ if( xSpell.is() && !xSpell->hasLanguage( eActLang.get() ) )
+ pDoc->SetMissingDictionaries( true );
+ else
+ pDoc->SetMissingDictionaries( false );
+ }
+}
+
+struct SwParaIdleData_Impl
+{
+ SwWrongList* pWrong; // for spell checking
+ SwGrammarMarkUp* pGrammarCheck; // for grammar checking / proof reading
+ SwWrongList* pSmartTags;
+ sal_uLong nNumberOfWords;
+ sal_uLong nNumberOfAsianWords;
+ sal_uLong nNumberOfChars;
+ sal_uLong nNumberOfCharsExcludingSpaces;
+ bool bWordCountDirty;
+ SwTextNode::WrongState eWrongDirty; ///< online spell checking needed/done?
+ bool bGrammarCheckDirty;
+ bool bSmartTagDirty;
+ bool bAutoComplDirty; ///< auto complete list dirty
+
+ SwParaIdleData_Impl() :
+ pWrong ( nullptr ),
+ pGrammarCheck ( nullptr ),
+ pSmartTags ( nullptr ),
+ nNumberOfWords ( 0 ),
+ nNumberOfAsianWords ( 0 ),
+ nNumberOfChars ( 0 ),
+ nNumberOfCharsExcludingSpaces ( 0 ),
+ bWordCountDirty ( true ),
+ eWrongDirty ( SwTextNode::WrongState::TODO ),
+ bGrammarCheckDirty ( true ),
+ bSmartTagDirty ( true ),
+ bAutoComplDirty ( true ) {};
+};
+
+/*
+ * This has basically the same function as SwScriptInfo::MaskHiddenRanges,
+ * only for deleted redlines
+ */
+
+static sal_Int32
+lcl_MaskRedlines( const SwTextNode& rNode, OUStringBuffer& rText,
+ sal_Int32 nStt, sal_Int32 nEnd,
+ const sal_Unicode cChar )
+{
+ sal_Int32 nNumOfMaskedRedlines = 0;
+
+ const SwDoc& rDoc = *rNode.GetDoc();
+
+ for ( SwRedlineTable::size_type nAct = rDoc.getIDocumentRedlineAccess().GetRedlinePos( rNode, RedlineType::Any ); nAct < rDoc.getIDocumentRedlineAccess().GetRedlineTable().size(); ++nAct )
+ {
+ const SwRangeRedline* pRed = rDoc.getIDocumentRedlineAccess().GetRedlineTable()[ nAct ];
+
+ if ( pRed->Start()->nNode > rNode.GetIndex() )
+ break;
+
+ if( RedlineType::Delete == pRed->GetType() )
+ {
+ sal_Int32 nRedlineEnd;
+ sal_Int32 nRedlineStart;
+
+ pRed->CalcStartEnd( rNode.GetIndex(), nRedlineStart, nRedlineEnd );
+
+ if ( nRedlineEnd < nStt || nRedlineStart > nEnd )
+ continue;
+
+ while ( nRedlineStart < nRedlineEnd && nRedlineStart < nEnd )
+ {
+ if (nRedlineStart >= nStt)
+ {
+ rText[nRedlineStart] = cChar;
+ ++nNumOfMaskedRedlines;
+ }
+ ++nRedlineStart;
+ }
+ }
+ }
+
+ return nNumOfMaskedRedlines;
+}
+
+/**
+ * Used for spell checking. Deleted redlines and hidden characters are masked
+ */
+static bool
+lcl_MaskRedlinesAndHiddenText( const SwTextNode& rNode, OUStringBuffer& rText,
+ sal_Int32 nStt, sal_Int32 nEnd,
+ const sal_Unicode cChar = CH_TXTATR_INWORD )
+{
+ sal_Int32 nRedlinesMasked = 0;
+ sal_Int32 nHiddenCharsMasked = 0;
+
+ const SwDoc& rDoc = *rNode.GetDoc();
+ const bool bShowChg = IDocumentRedlineAccess::IsShowChanges( rDoc.getIDocumentRedlineAccess().GetRedlineFlags() );
+
+ // If called from word count or from spell checking, deleted redlines
+ // should be masked:
+ if ( bShowChg )
+ {
+ nRedlinesMasked = lcl_MaskRedlines( rNode, rText, nStt, nEnd, cChar );
+ }
+
+ const bool bHideHidden = !SW_MOD()->GetViewOption(rDoc.GetDocumentSettingManager().get(DocumentSettingId::HTML_MODE))->IsShowHiddenChar();
+
+ // If called from word count, we want to mask the hidden ranges even
+ // if they are visible:
+ if ( bHideHidden )
+ {
+ nHiddenCharsMasked =
+ SwScriptInfo::MaskHiddenRanges( rNode, rText, nStt, nEnd, cChar );
+ }
+
+ return (nRedlinesMasked > 0) || (nHiddenCharsMasked > 0);
+}
+
+/**
+ * Used for spell checking. Calculates a rectangle for repaint.
+ */
+static SwRect lcl_CalculateRepaintRect(
+ SwTextFrame & rTextFrame, SwTextNode & rNode,
+ sal_Int32 const nChgStart, sal_Int32 const nChgEnd)
+{
+ TextFrameIndex const iChgStart(rTextFrame.MapModelToView(&rNode, nChgStart));
+ TextFrameIndex const iChgEnd(rTextFrame.MapModelToView(&rNode, nChgEnd));
+
+ SwRect aRect = rTextFrame.GetPaintArea();
+ SwRect aTmp = rTextFrame.GetPaintArea();
+
+ const SwTextFrame* pStartFrame = &rTextFrame;
+ while( pStartFrame->HasFollow() &&
+ iChgStart >= pStartFrame->GetFollow()->GetOffset())
+ pStartFrame = pStartFrame->GetFollow();
+ const SwTextFrame* pEndFrame = pStartFrame;
+ while( pEndFrame->HasFollow() &&
+ iChgEnd >= pEndFrame->GetFollow()->GetOffset())
+ pEndFrame = pEndFrame->GetFollow();
+
+ bool bSameFrame = true;
+
+ if( rTextFrame.HasFollow() )
+ {
+ if( pEndFrame != pStartFrame )
+ {
+ bSameFrame = false;
+ SwRect aStFrame( pStartFrame->GetPaintArea() );
+ {
+ SwRectFnSet aRectFnSet(pStartFrame);
+ aRectFnSet.SetLeft( aTmp, aRectFnSet.GetLeft(aStFrame) );
+ aRectFnSet.SetRight( aTmp, aRectFnSet.GetRight(aStFrame) );
+ aRectFnSet.SetBottom( aTmp, aRectFnSet.GetBottom(aStFrame) );
+ }
+ aStFrame = pEndFrame->GetPaintArea();
+ {
+ SwRectFnSet aRectFnSet(pEndFrame);
+ aRectFnSet.SetTop( aRect, aRectFnSet.GetTop(aStFrame) );
+ aRectFnSet.SetLeft( aRect, aRectFnSet.GetLeft(aStFrame) );
+ aRectFnSet.SetRight( aRect, aRectFnSet.GetRight(aStFrame) );
+ }
+ aRect.Union( aTmp );
+ while( true )
+ {
+ pStartFrame = pStartFrame->GetFollow();
+ if( pStartFrame == pEndFrame )
+ break;
+ aRect.Union( pStartFrame->GetPaintArea() );
+ }
+ }
+ }
+ if( bSameFrame )
+ {
+ SwRectFnSet aRectFnSet(pStartFrame);
+ if( aRectFnSet.GetTop(aTmp) == aRectFnSet.GetTop(aRect) )
+ aRectFnSet.SetLeft( aRect, aRectFnSet.GetLeft(aTmp) );
+ else
+ {
+ SwRect aStFrame( pStartFrame->GetPaintArea() );
+ aRectFnSet.SetLeft( aRect, aRectFnSet.GetLeft(aStFrame) );
+ aRectFnSet.SetRight( aRect, aRectFnSet.GetRight(aStFrame) );
+ aRectFnSet.SetTop( aRect, aRectFnSet.GetTop(aTmp) );
+ }
+
+ if( aTmp.Height() > aRect.Height() )
+ aRect.Height( aTmp.Height() );
+ }
+
+ return aRect;
+}
+
+/**
+ * Used for automatic styles. Used during RstAttr.
+ */
+static bool lcl_HaveCommonAttributes( IStyleAccess& rStyleAccess,
+ const SfxItemSet* pSet1,
+ sal_uInt16 nWhichId,
+ const SfxItemSet& rSet2,
+ std::shared_ptr<SfxItemSet>& pStyleHandle )
+{
+ bool bRet = false;
+
+ std::unique_ptr<SfxItemSet> pNewSet;
+
+ if ( !pSet1 )
+ {
+ OSL_ENSURE( nWhichId, "lcl_HaveCommonAttributes not used correctly" );
+ if ( SfxItemState::SET == rSet2.GetItemState( nWhichId, false ) )
+ {
+ pNewSet = rSet2.Clone();
+ pNewSet->ClearItem( nWhichId );
+ }
+ }
+ else if ( pSet1->Count() )
+ {
+ SfxItemIter aIter( *pSet1 );
+ const SfxPoolItem* pItem = aIter.GetCurItem();
+ do
+ {
+ if ( SfxItemState::SET == rSet2.GetItemState( pItem->Which(), false ) )
+ {
+ if ( !pNewSet )
+ pNewSet = rSet2.Clone();
+ pNewSet->ClearItem( pItem->Which() );
+ }
+
+ pItem = aIter.NextItem();
+ } while (pItem);
+ }
+
+ if ( pNewSet )
+ {
+ if ( pNewSet->Count() )
+ pStyleHandle = rStyleAccess.getAutomaticStyle( *pNewSet, IStyleAccess::AUTO_STYLE_CHAR );
+ bRet = true;
+ }
+
+ return bRet;
+}
+
+/** Delete all attributes
+ *
+ * 5 cases:
+ * 1) The attribute is completely in the deletion range:
+ * -> delete it
+ * 2) The end of the attribute is in the deletion range:
+ * -> delete it, then re-insert it with new end
+ * 3) The start of the attribute is in the deletion range:
+ * -> delete it, then re-insert it with new start
+ * 4) The attribute contains the deletion range:
+ * Split, i.e.,
+ * -> Delete, re-insert from old start to start of deletion range
+ * -> insert new attribute from end of deletion range to old end
+ * 5) The attribute is outside the deletion range
+ * -> nothing to do
+ *
+ * @param rIdx starting position
+ * @param nLen length of the deletion
+ * @param nthat ???
+ * @param pSet ???
+ * @param bInclRefToxMark ???
+ */
+
+void SwTextNode::RstTextAttr(
+ const SwIndex &rIdx,
+ const sal_Int32 nLen,
+ const sal_uInt16 nWhich,
+ const SfxItemSet* pSet,
+ const bool bInclRefToxMark,
+ const bool bExactRange )
+{
+ if ( !GetpSwpHints() )
+ return;
+
+ sal_Int32 nStt = rIdx.GetIndex();
+ sal_Int32 nEnd = nStt + nLen;
+ {
+ // enlarge range for the reset of text attributes in case of an overlapping input field
+ const SwTextInputField* pTextInputField = dynamic_cast<const SwTextInputField*>(GetTextAttrAt( nStt, RES_TXTATR_INPUTFIELD, PARENT ));
+ if ( pTextInputField == nullptr )
+ {
+ pTextInputField = dynamic_cast<const SwTextInputField*>(GetTextAttrAt(nEnd, RES_TXTATR_INPUTFIELD, PARENT ));
+ }
+ if ( pTextInputField != nullptr )
+ {
+ if ( nStt > pTextInputField->GetStart() )
+ {
+ nStt = pTextInputField->GetStart();
+ }
+ if ( nEnd < *(pTextInputField->End()) )
+ {
+ nEnd = *(pTextInputField->End());
+ }
+ }
+ }
+
+ bool bChanged = false;
+
+ // nMin and nMax initialized to maximum / minimum (inverse)
+ sal_Int32 nMin = m_Text.getLength();
+ sal_Int32 nMax = nStt;
+ const bool bNoLen = nMin == 0;
+
+ // We have to remember the "new" attributes that have
+ // been introduced by splitting surrounding attributes (case 2,3,4).
+ std::vector<SwTextAttr *> newAttributes;
+ std::vector<SwTextAttr *> delAttributes;
+
+ // iterate over attribute array until start of attribute is behind deletion range
+ m_pSwpHints->SortIfNeedBe(); // trigger sorting now, we don't want it during iteration
+ size_t i = 0;
+ sal_Int32 nAttrStart = sal_Int32();
+ SwTextAttr *pHt = nullptr;
+ while ( (i < m_pSwpHints->Count())
+ && ( ( ( nAttrStart = m_pSwpHints->GetWithoutResorting(i)->GetStart()) < nEnd )
+ || nLen==0 ) && !bExactRange)
+ {
+ pHt = m_pSwpHints->GetWithoutResorting(i);
+
+ // attributes without end stay in!
+ // but consider <bInclRefToxMark> used by Undo
+ const sal_Int32* const pAttrEnd = pHt->GetEnd();
+ const bool bKeepAttrWithoutEnd =
+ pAttrEnd == nullptr
+ && ( !bInclRefToxMark
+ || ( RES_TXTATR_REFMARK != pHt->Which()
+ && RES_TXTATR_TOXMARK != pHt->Which()
+ && RES_TXTATR_META != pHt->Which()
+ && RES_TXTATR_METAFIELD != pHt->Which() ) );
+ if ( bKeepAttrWithoutEnd )
+ {
+
+ i++;
+ continue;
+ }
+ // attributes with content stay in
+ if ( pHt->HasContent() )
+ {
+ ++i;
+ continue;
+ }
+
+ // Default behavior is to process all attributes:
+ bool bSkipAttr = false;
+ std::shared_ptr<SfxItemSet> pStyleHandle;
+
+ // 1. case: We want to reset only the attributes listed in pSet:
+ if ( pSet )
+ {
+ bSkipAttr = SfxItemState::SET != pSet->GetItemState( pHt->Which(), false );
+ if ( bSkipAttr && RES_TXTATR_AUTOFMT == pHt->Which() )
+ {
+ // if the current attribute is an autostyle, we have to check if the autostyle
+ // and pSet have any attributes in common. If so, pStyleHandle will contain
+ // a handle to AutoStyle / pSet:
+ bSkipAttr = !lcl_HaveCommonAttributes( getIDocumentStyleAccess(), pSet, 0, *static_cast<const SwFormatAutoFormat&>(pHt->GetAttr()).GetStyleHandle(), pStyleHandle );
+ }
+ }
+ else if ( nWhich )
+ {
+ // 2. case: We want to reset only the attributes with WhichId nWhich:
+ bSkipAttr = nWhich != pHt->Which();
+ if ( bSkipAttr && RES_TXTATR_AUTOFMT == pHt->Which() )
+ {
+ bSkipAttr = !lcl_HaveCommonAttributes( getIDocumentStyleAccess(), nullptr, nWhich, *static_cast<const SwFormatAutoFormat&>(pHt->GetAttr()).GetStyleHandle(), pStyleHandle );
+ }
+ }
+ else if ( !bInclRefToxMark )
+ {
+ // 3. case: Reset all attributes except from ref/toxmarks:
+ // skip hints with CH_TXTATR here
+ // (deleting those is ONLY allowed for UNDO!)
+ bSkipAttr = RES_TXTATR_REFMARK == pHt->Which()
+ || RES_TXTATR_TOXMARK == pHt->Which()
+ || RES_TXTATR_META == pHt->Which()
+ || RES_TXTATR_METAFIELD == pHt->Which();
+ }
+
+ if ( bSkipAttr )
+ {
+ i++;
+ continue;
+ }
+
+ if (nStt <= nAttrStart) // Case: 1,3,5
+ {
+ const sal_Int32 nAttrEnd = pAttrEnd != nullptr
+ ? *pAttrEnd
+ : nAttrStart;
+ if (nEnd > nAttrStart
+ || (nEnd == nAttrEnd && nEnd == nAttrStart)) // Case: 1,3
+ {
+ if ( nMin > nAttrStart )
+ nMin = nAttrStart;
+ if ( nMax < nAttrEnd )
+ nMax = nAttrEnd;
+ // If only a no-extent hint is deleted, no resorting is needed
+ bChanged = bChanged || nEnd > nAttrStart || bNoLen;
+ if (nAttrEnd <= nEnd) // Case: 1
+ {
+ delAttributes.push_back(pHt);
+
+ if ( pStyleHandle )
+ {
+ SwTextAttr* pNew = MakeTextAttr( *GetDoc(),
+ *pStyleHandle, nAttrStart, nAttrEnd );
+ newAttributes.push_back(pNew);
+ }
+ }
+ else // Case: 3
+ {
+ bChanged = true;
+ m_pSwpHints->NoteInHistory( pHt );
+ // UGLY: this may temporarily destroy the sorting!
+ pHt->SetStart(nEnd);
+ m_pSwpHints->NoteInHistory( pHt, true );
+
+ if ( pStyleHandle && nAttrStart < nEnd )
+ {
+ SwTextAttr* pNew = MakeTextAttr( *GetDoc(),
+ *pStyleHandle, nAttrStart, nEnd );
+ newAttributes.push_back(pNew);
+ }
+ }
+ }
+ }
+ else if (pAttrEnd != nullptr) // Case: 2,4,5
+ {
+ if (*pAttrEnd > nStt) // Case: 2,4
+ {
+ if (*pAttrEnd < nEnd) // Case: 2
+ {
+ if ( nMin > nAttrStart )
+ nMin = nAttrStart;
+ if ( nMax < *pAttrEnd )
+ nMax = *pAttrEnd;
+ bChanged = true;
+
+ const sal_Int32 nAttrEnd = *pAttrEnd;
+
+ m_pSwpHints->NoteInHistory( pHt );
+ // UGLY: this may temporarily destroy the sorting!
+ pHt->SetEnd(nStt);
+ m_pSwpHints->NoteInHistory( pHt, true );
+
+ if ( pStyleHandle )
+ {
+ SwTextAttr* pNew = MakeTextAttr( *GetDoc(),
+ *pStyleHandle, nStt, nAttrEnd );
+ newAttributes.push_back(pNew);
+ }
+ }
+ else if (nLen) // Case: 4
+ {
+ // for Length 0 both hints would be merged again by
+ // InsertHint, so leave them alone!
+ if ( nMin > nAttrStart )
+ nMin = nAttrStart;
+ if ( nMax < *pAttrEnd )
+ nMax = *pAttrEnd;
+ bChanged = true;
+ const sal_Int32 nTmpEnd = *pAttrEnd;
+ m_pSwpHints->NoteInHistory( pHt );
+ // UGLY: this may temporarily destroy the sorting!
+ pHt->SetEnd(nStt);
+ m_pSwpHints->NoteInHistory( pHt, true );
+
+ if ( pStyleHandle && nStt < nEnd )
+ {
+ SwTextAttr* pNew = MakeTextAttr( *GetDoc(),
+ *pStyleHandle, nStt, nEnd );
+ newAttributes.push_back(pNew);
+ }
+
+ if( nEnd < nTmpEnd )
+ {
+ SwTextAttr* pNew = MakeTextAttr( *GetDoc(),
+ pHt->GetAttr(), nEnd, nTmpEnd );
+ if ( pNew )
+ {
+ SwTextCharFormat* pCharFormat = dynamic_cast<SwTextCharFormat*>(pHt);
+ if ( pCharFormat )
+ static_txtattr_cast<SwTextCharFormat*>(pNew)->SetSortNumber(pCharFormat->GetSortNumber());
+
+ newAttributes.push_back(pNew);
+ }
+ }
+ }
+ }
+ }
+ ++i;
+ }
+
+ if (bExactRange)
+ {
+ // Only delete the hints which start at nStt and end at nEnd.
+ for (i = 0; i < m_pSwpHints->Count(); ++i)
+ {
+ SwTextAttr* pHint = m_pSwpHints->Get(i);
+ if ( (isTXTATR_WITHEND(pHint->Which()) && RES_TXTATR_AUTOFMT != pHint->Which())
+ || pHint->GetStart() != nStt)
+ continue;
+
+ const sal_Int32* pHintEnd = pHint->GetEnd();
+ if (!pHintEnd || *pHintEnd != nEnd)
+ continue;
+
+ delAttributes.push_back(pHint);
+ }
+ }
+
+ if (bChanged && !delAttributes.empty())
+ { // Delete() calls GetStartOf() - requires sorted hints!
+ m_pSwpHints->Resort();
+ }
+
+ // delay deleting the hints because it re-sorts the hints array
+ for (SwTextAttr *const pDel : delAttributes)
+ {
+ m_pSwpHints->Delete(pDel);
+ DestroyAttr(pDel);
+ }
+
+ // delay inserting the hints because it re-sorts the hints array
+ for (SwTextAttr *const pNew : newAttributes)
+ {
+ InsertHint(pNew, SetAttrMode::NOHINTADJUST);
+ }
+
+ TryDeleteSwpHints();
+
+ if (bChanged)
+ {
+ if ( HasHints() )
+ { // possibly sometimes Resort would be sufficient, but...
+ m_pSwpHints->MergePortions(*this);
+ }
+
+ // TextFrame's respond to aHint, others to aNew
+ SwUpdateAttr aHint(
+ nMin,
+ nMax,
+ 0);
+
+ NotifyClients( nullptr, &aHint );
+ SwFormatChg aNew( GetFormatColl() );
+ NotifyClients( nullptr, &aNew );
+ }
+}
+
+static sal_Int32 clipIndexBounds(const OUString &rStr, sal_Int32 nPos)
+{
+ if (nPos < 0)
+ return 0;
+ if (nPos > rStr.getLength())
+ return rStr.getLength();
+ return nPos;
+}
+
+// Return current word:
+// Search from left to right, so find the word before nPos.
+// Except if at the start of the paragraph, then return the first word.
+// If the first word consists only of whitespace, return an empty string.
+OUString SwTextFrame::GetCurWord(SwPosition const& rPos) const
+{
+ TextFrameIndex const nPos(MapModelToViewPos(rPos));
+ SwTextNode *const pTextNode(rPos.nNode.GetNode().GetTextNode());
+ assert(pTextNode);
+ OUString const& rText(GetText());
+ assert(sal_Int32(nPos) <= rText.getLength()); // invalid index
+
+ if (rText.isEmpty() || IsHiddenNow())
+ return OUString();
+
+ assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is());
+ const uno::Reference< XBreakIterator > &rxBreak = g_pBreakIt->GetBreakIter();
+ sal_Int16 nWordType = WordType::DICTIONARY_WORD;
+ lang::Locale aLocale( g_pBreakIt->GetLocale(pTextNode->GetLang(rPos.nContent.GetIndex())) );
+ Boundary aBndry =
+ rxBreak->getWordBoundary(rText, sal_Int32(nPos), aLocale, nWordType, true);
+
+ // if no word was found use previous word (if any)
+ if (aBndry.startPos == aBndry.endPos)
+ {
+ aBndry = rxBreak->previousWord(rText, sal_Int32(nPos), aLocale, nWordType);
+ }
+
+ // check if word was found and if it uses a symbol font, if so
+ // enforce returning an empty string
+ if (aBndry.endPos != aBndry.startPos
+ && IsSymbolAt(TextFrameIndex(aBndry.startPos)))
+ {
+ aBndry.endPos = aBndry.startPos;
+ }
+
+ // can have -1 as start/end of bounds not found
+ aBndry.startPos = clipIndexBounds(rText, aBndry.startPos);
+ aBndry.endPos = clipIndexBounds(rText, aBndry.endPos);
+
+ return rText.copy(aBndry.startPos,
+ aBndry.endPos - aBndry.startPos);
+}
+
+SwScanner::SwScanner( const SwTextNode& rNd, const OUString& rText,
+ const LanguageType* pLang, const ModelToViewHelper& rConvMap,
+ sal_uInt16 nType, sal_Int32 nStart, sal_Int32 nEnd, bool bClp )
+ : SwScanner(
+ [&rNd](sal_Int32 const nBegin, sal_uInt16 const nScript, bool const bNoChar)
+ { return rNd.GetLang(nBegin, bNoChar ? 0 : 1, nScript); }
+ , rText, pLang, rConvMap, nType, nStart, nEnd, bClp)
+{
+}
+
+SwScanner::SwScanner(std::function<LanguageType(sal_Int32, sal_Int32, bool)> const& pGetLangOfChar,
+ const OUString& rText, const LanguageType* pLang,
+ const ModelToViewHelper& rConvMap, sal_uInt16 nType, sal_Int32 nStart,
+ sal_Int32 nEnd, bool bClp)
+ : m_pGetLangOfChar(pGetLangOfChar)
+ , m_aPreDashReplacementText(rText)
+ , m_pLanguage(pLang)
+ , m_ModelToView(rConvMap)
+ , m_nLength(0)
+ , m_nOverriddenDashCount(0)
+ , m_nWordType(nType)
+ , m_bClip(bClp)
+{
+ m_nStartPos = m_nBegin = nStart;
+ m_nEndPos = nEnd;
+
+ //MSWord f.e has special emdash and endash behaviour in that they break
+ //words for the purposes of word counting, while a hyphen etc. doesn't.
+
+ //The default configuration treats emdash/endash as a word break, but
+ //additional ones can be added in under tools->options
+ if (m_nWordType == i18n::WordType::WORD_COUNT)
+ {
+ OUString sDashes = officecfg::Office::Writer::WordCount::AdditionalSeparators::get();
+ OUStringBuffer aBuf(m_aPreDashReplacementText);
+ for (sal_Int32 i = m_nStartPos; i < m_nEndPos; ++i)
+ {
+ if (i < 0)
+ continue;
+ sal_Unicode cChar = aBuf[i];
+ if (sDashes.indexOf(cChar) != -1)
+ {
+ aBuf[i] = ' ';
+ ++m_nOverriddenDashCount;
+ }
+ }
+ m_aText = aBuf.makeStringAndClear();
+ }
+ else
+ m_aText = m_aPreDashReplacementText;
+
+ assert(m_aPreDashReplacementText.getLength() == m_aText.getLength());
+
+ if ( m_pLanguage )
+ {
+ m_aCurrentLang = *m_pLanguage;
+ }
+ else
+ {
+ ModelToViewHelper::ModelPosition aModelBeginPos =
+ m_ModelToView.ConvertToModelPosition( m_nBegin );
+ m_aCurrentLang = m_pGetLangOfChar(aModelBeginPos.mnPos, 0, true);
+ }
+}
+
+namespace
+{
+ //fdo#45271 for Asian words count characters instead of words
+ sal_Int32 forceEachAsianCodePointToWord(const OUString &rText, sal_Int32 nBegin, sal_Int32 nLen)
+ {
+ if (nLen > 1)
+ {
+ const uno::Reference< XBreakIterator > &rxBreak = g_pBreakIt->GetBreakIter();
+
+ sal_uInt16 nCurrScript = rxBreak->getScriptType( rText, nBegin );
+
+ sal_Int32 indexUtf16 = nBegin;
+ rText.iterateCodePoints(&indexUtf16);
+
+ //First character is Asian, consider it a word :-(
+ if (nCurrScript == i18n::ScriptType::ASIAN)
+ {
+ nLen = indexUtf16 - nBegin;
+ return nLen;
+ }
+
+ //First character was not Asian, consider appearance of any Asian character
+ //to be the end of the word
+ while (indexUtf16 < nBegin + nLen)
+ {
+ nCurrScript = rxBreak->getScriptType( rText, indexUtf16 );
+ if (nCurrScript == i18n::ScriptType::ASIAN)
+ {
+ nLen = indexUtf16 - nBegin;
+ return nLen;
+ }
+ rText.iterateCodePoints(&indexUtf16);
+ }
+ }
+ return nLen;
+ }
+}
+
+bool SwScanner::NextWord()
+{
+ m_nBegin = m_nBegin + m_nLength;
+ Boundary aBound;
+
+ CharClass& rCC = GetAppCharClass();
+ LanguageTag aOldLanguageTag = rCC.getLanguageTag();
+
+ while ( true )
+ {
+ // skip non-letter characters:
+ while (m_nBegin < m_aText.getLength())
+ {
+ if (m_nBegin >= 0 && !u_isspace(m_aText[m_nBegin]))
+ {
+ if ( !m_pLanguage )
+ {
+ const sal_uInt16 nNextScriptType = g_pBreakIt->GetBreakIter()->getScriptType( m_aText, m_nBegin );
+ ModelToViewHelper::ModelPosition aModelBeginPos =
+ m_ModelToView.ConvertToModelPosition( m_nBegin );
+ m_aCurrentLang = m_pGetLangOfChar(aModelBeginPos.mnPos, nNextScriptType, false);
+ }
+
+ if ( m_nWordType != i18n::WordType::WORD_COUNT )
+ {
+ rCC.setLanguageTag( LanguageTag( g_pBreakIt->GetLocale( m_aCurrentLang )) );
+ if ( rCC.isLetterNumeric(OUString(m_aText[m_nBegin])) )
+ break;
+ }
+ else
+ break;
+ }
+ ++m_nBegin;
+ }
+
+ if ( m_nBegin >= m_aText.getLength() || m_nBegin >= m_nEndPos )
+ return false;
+
+ // get the word boundaries
+ aBound = g_pBreakIt->GetBreakIter()->getWordBoundary( m_aText, m_nBegin,
+ g_pBreakIt->GetLocale( m_aCurrentLang ), m_nWordType, true );
+ OSL_ENSURE( aBound.endPos >= aBound.startPos, "broken aBound result" );
+
+ // we don't want to include preceding text
+ // to count words in text with mixed script punctuation correctly,
+ // but we want to include preceding symbols (eg. percent sign, section sign,
+ // degree sign defined by dict_word_hu to spell check their affixed forms).
+ if (m_nWordType == i18n::WordType::WORD_COUNT && aBound.startPos < m_nBegin)
+ aBound.startPos = m_nBegin;
+
+ //no word boundaries could be found
+ if(aBound.endPos == aBound.startPos)
+ return false;
+
+ //if a word before is found it has to be searched for the next
+ if(aBound.endPos == m_nBegin)
+ ++m_nBegin;
+ else
+ break;
+ } // end while( true )
+
+ rCC.setLanguageTag( aOldLanguageTag );
+
+ // #i89042, as discussed with HDU: don't evaluate script changes for word count. Use whole word.
+ if ( m_nWordType == i18n::WordType::WORD_COUNT )
+ {
+ m_nBegin = std::max(aBound.startPos, m_nBegin);
+ m_nLength = 0;
+ if (aBound.endPos > m_nBegin)
+ m_nLength = aBound.endPos - m_nBegin;
+ }
+ else
+ {
+ // we have to differentiate between these cases:
+ if ( aBound.startPos <= m_nBegin )
+ {
+ OSL_ENSURE( aBound.endPos >= m_nBegin, "Unexpected aBound result" );
+
+ // restrict boundaries to script boundaries and nEndPos
+ const sal_uInt16 nCurrScript = g_pBreakIt->GetBreakIter()->getScriptType( m_aText, m_nBegin );
+ OUString aTmpWord = m_aText.copy( m_nBegin, aBound.endPos - m_nBegin );
+ const sal_Int32 nScriptEnd = m_nBegin +
+ g_pBreakIt->GetBreakIter()->endOfScript( aTmpWord, 0, nCurrScript );
+ const sal_Int32 nEnd = std::min( aBound.endPos, nScriptEnd );
+
+ // restrict word start to last script change position
+ sal_Int32 nScriptBegin = 0;
+ if ( aBound.startPos < m_nBegin )
+ {
+ // search from nBegin backwards until the next script change
+ aTmpWord = m_aText.copy( aBound.startPos,
+ m_nBegin - aBound.startPos + 1 );
+ nScriptBegin = aBound.startPos +
+ g_pBreakIt->GetBreakIter()->beginOfScript( aTmpWord, m_nBegin - aBound.startPos,
+ nCurrScript );
+ }
+
+ m_nBegin = std::max( aBound.startPos, nScriptBegin );
+ m_nLength = nEnd - m_nBegin;
+ }
+ else
+ {
+ const sal_uInt16 nCurrScript = g_pBreakIt->GetBreakIter()->getScriptType( m_aText, aBound.startPos );
+ OUString aTmpWord = m_aText.copy( aBound.startPos,
+ aBound.endPos - aBound.startPos );
+ const sal_Int32 nScriptEnd = aBound.startPos +
+ g_pBreakIt->GetBreakIter()->endOfScript( aTmpWord, 0, nCurrScript );
+ const sal_Int32 nEnd = std::min( aBound.endPos, nScriptEnd );
+ m_nBegin = aBound.startPos;
+ m_nLength = nEnd - m_nBegin;
+ }
+ }
+
+ // optionally clip the result of getWordBoundaries:
+ if ( m_bClip )
+ {
+ aBound.startPos = std::max( aBound.startPos, m_nStartPos );
+ aBound.endPos = std::min( aBound.endPos, m_nEndPos );
+ if (aBound.endPos < aBound.startPos)
+ {
+ m_nBegin = m_nEndPos;
+ m_nLength = 0; // found word is outside of search interval
+ }
+ else
+ {
+ m_nBegin = aBound.startPos;
+ m_nLength = aBound.endPos - m_nBegin;
+ }
+ }
+
+ if( ! m_nLength )
+ return false;
+
+ if ( m_nWordType == i18n::WordType::WORD_COUNT )
+ m_nLength = forceEachAsianCodePointToWord(m_aText, m_nBegin, m_nLength);
+
+ m_aWord = m_aPreDashReplacementText.copy( m_nBegin, m_nLength );
+
+ return true;
+}
+
+// Note: this is a clone of SwTextFrame::AutoSpell_, so keep them in sync when fixing things!
+bool SwTextNode::Spell(SwSpellArgs* pArgs)
+{
+ // modify string according to redline information and hidden text
+ const OUString aOldText( m_Text );
+ OUStringBuffer buf(m_Text);
+ const bool bRestoreString =
+ lcl_MaskRedlinesAndHiddenText(*this, buf, 0, m_Text.getLength());
+ if (bRestoreString)
+ { // ??? UGLY: is it really necessary to modify m_Text here?
+ m_Text = buf.makeStringAndClear();
+ }
+
+ sal_Int32 nBegin = ( pArgs->pStartNode != this )
+ ? 0
+ : pArgs->pStartIdx->GetIndex();
+
+ sal_Int32 nEnd = ( pArgs->pEndNode != this )
+ ? m_Text.getLength()
+ : pArgs->pEndIdx->GetIndex();
+
+ pArgs->xSpellAlt = nullptr;
+
+ // 4 cases:
+
+ // 1. IsWrongDirty = 0 and GetWrong = 0
+ // Everything is checked and correct
+ // 2. IsWrongDirty = 0 and GetWrong = 1
+ // Everything is checked and errors are identified in the wrong list
+ // 3. IsWrongDirty = 1 and GetWrong = 0
+ // Nothing has been checked
+ // 4. IsWrongDirty = 1 and GetWrong = 1
+ // Text has been checked but there is an invalid range in the wrong list
+
+ // Nothing has to be done for case 1.
+ if ( ( IsWrongDirty() || GetWrong() ) && m_Text.getLength() )
+ {
+ if (nBegin > m_Text.getLength())
+ {
+ nBegin = m_Text.getLength();
+ }
+ if (nEnd > m_Text.getLength())
+ {
+ nEnd = m_Text.getLength();
+ }
+
+ if(!IsWrongDirty())
+ {
+ const sal_Int32 nTemp = GetWrong()->NextWrong( nBegin );
+ if(nTemp > nEnd)
+ {
+ // reset original text
+ if ( bRestoreString )
+ {
+ m_Text = aOldText;
+ }
+ return false;
+ }
+ if(nTemp > nBegin)
+ nBegin = nTemp;
+
+ }
+
+ // In case 2. we pass the wrong list to the scanned, because only
+ // the words in the wrong list have to be checked
+ SwScanner aScanner( *this, m_Text, nullptr, ModelToViewHelper(),
+ WordType::DICTIONARY_WORD,
+ nBegin, nEnd );
+ while( !pArgs->xSpellAlt.is() && aScanner.NextWord() )
+ {
+ const OUString& rWord = aScanner.GetWord();
+
+ // get next language for next word, consider language attributes
+ // within the word
+ LanguageType eActLang = aScanner.GetCurrentLanguage();
+ DetectAndMarkMissingDictionaries( GetTextNode()->GetDoc(), pArgs->xSpeller, eActLang );
+
+ if( rWord.getLength() > 0 && LANGUAGE_NONE != eActLang )
+ {
+ if (pArgs->xSpeller.is())
+ {
+ SvxSpellWrapper::CheckSpellLang( pArgs->xSpeller, eActLang );
+ pArgs->xSpellAlt = pArgs->xSpeller->spell( rWord, static_cast<sal_uInt16>(eActLang),
+ Sequence< PropertyValue >() );
+ }
+ if( pArgs->xSpellAlt.is() )
+ {
+ if (IsSymbolAt(aScanner.GetBegin()))
+ {
+ pArgs->xSpellAlt = nullptr;
+ }
+ else
+ {
+ // make sure the selection build later from the data
+ // below does not include "in word" character to the
+ // left and right in order to preserve those. Therefore
+ // count those "in words" in order to modify the
+ // selection accordingly.
+ const sal_Unicode* pChar = rWord.getStr();
+ sal_Int32 nLeft = 0;
+ while (*pChar++ == CH_TXTATR_INWORD)
+ ++nLeft;
+ pChar = rWord.getLength() ? rWord.getStr() + rWord.getLength() - 1 : nullptr;
+ sal_Int32 nRight = 0;
+ while (pChar && *pChar-- == CH_TXTATR_INWORD)
+ ++nRight;
+
+ pArgs->pStartNode = this;
+ pArgs->pEndNode = this;
+ pArgs->pStartIdx->Assign(this, aScanner.GetEnd() - nRight );
+ pArgs->pEndIdx->Assign(this, aScanner.GetBegin() + nLeft );
+ }
+ }
+ }
+ }
+ }
+
+ // reset original text
+ if ( bRestoreString )
+ {
+ m_Text = aOldText;
+ }
+
+ return pArgs->xSpellAlt.is();
+}
+
+void SwTextNode::SetLanguageAndFont( const SwPaM &rPaM,
+ LanguageType nLang, sal_uInt16 nLangWhichId,
+ const vcl::Font *pFont, sal_uInt16 nFontWhichId )
+{
+ sal_uInt16 aRanges[] = {
+ nLangWhichId, nLangWhichId,
+ nFontWhichId, nFontWhichId,
+ 0, 0, 0 };
+ if (!pFont)
+ aRanges[2] = aRanges[3] = 0; // clear entries with font WhichId
+
+ SwEditShell *pEditShell = GetDoc()->GetEditShell();
+ SfxItemSet aSet( pEditShell->GetAttrPool(), aRanges );
+ aSet.Put( SvxLanguageItem( nLang, nLangWhichId ) );
+
+ OSL_ENSURE( pFont, "target font missing?" );
+ if (pFont)
+ {
+ SvxFontItem aFontItem = static_cast<const SvxFontItem&>( aSet.Get( nFontWhichId ) );
+ aFontItem.SetFamilyName( pFont->GetFamilyName());
+ aFontItem.SetFamily( pFont->GetFamilyType());
+ aFontItem.SetStyleName( pFont->GetStyleName());
+ aFontItem.SetPitch( pFont->GetPitch());
+ aFontItem.SetCharSet( pFont->GetCharSet() );
+ aSet.Put( aFontItem );
+ }
+
+ GetDoc()->getIDocumentContentOperations().InsertItemSet( rPaM, aSet );
+ // SetAttr( aSet ); <- Does not set language attribute of empty paragraphs correctly,
+ // <- because since there is no selection the flag to garbage
+ // <- collect all attributes is set, and therefore attributes spanned
+ // <- over empty selection are removed.
+
+}
+
+bool SwTextNode::Convert( SwConversionArgs &rArgs )
+{
+ // get range of text within node to be converted
+ // (either all the text or the text within the selection
+ // when the conversion was started)
+ const sal_Int32 nTextBegin = ( rArgs.pStartNode == this )
+ ? std::min(rArgs.pStartIdx->GetIndex(), m_Text.getLength())
+ : 0;
+
+ const sal_Int32 nTextEnd = ( rArgs.pEndNode == this )
+ ? std::min(rArgs.pEndIdx->GetIndex(), m_Text.getLength())
+ : m_Text.getLength();
+
+ rArgs.aConvText.clear();
+
+ // modify string according to redline information and hidden text
+ const OUString aOldText( m_Text );
+ OUStringBuffer buf(m_Text);
+ const bool bRestoreString =
+ lcl_MaskRedlinesAndHiddenText(*this, buf, 0, m_Text.getLength());
+ if (bRestoreString)
+ { // ??? UGLY: is it really necessary to modify m_Text here?
+ m_Text = buf.makeStringAndClear();
+ }
+
+ bool bFound = false;
+ sal_Int32 nBegin = nTextBegin;
+ sal_Int32 nLen = 0;
+ LanguageType nLangFound = LANGUAGE_NONE;
+ if (m_Text.isEmpty())
+ {
+ if (rArgs.bAllowImplicitChangesForNotConvertibleText)
+ {
+ // create SwPaM with mark & point spanning empty paragraph
+ //SwPaM aCurPaM( *this, *this, nBegin, nBegin + nLen ); <-- wrong c-tor, does sth different
+ SwPaM aCurPaM( *this, 0 );
+
+ SetLanguageAndFont( aCurPaM,
+ rArgs.nConvTargetLang, RES_CHRATR_CJK_LANGUAGE,
+ rArgs.pTargetFont, RES_CHRATR_CJK_FONT );
+ }
+ }
+ else
+ {
+ SwLanguageIterator aIter( *this, nBegin );
+
+ // Implicit changes require setting new attributes, which in turn destroys
+ // the attribute sequence on that aIter iterates. We store the necessary
+ // coordinates and apply those changes after iterating through the text.
+ typedef std::pair<sal_Int32, sal_Int32> ImplicitChangesRange;
+ std::vector<ImplicitChangesRange> aImplicitChanges;
+
+ // find non zero length text portion of appropriate language
+ do {
+ nLangFound = aIter.GetLanguage();
+ bool bLangOk = (nLangFound == rArgs.nConvSrcLang) ||
+ (editeng::HangulHanjaConversion::IsChinese( nLangFound ) &&
+ editeng::HangulHanjaConversion::IsChinese( rArgs.nConvSrcLang ));
+
+ sal_Int32 nChPos = aIter.GetChgPos();
+ // the position at the end of the paragraph is COMPLETE_STRING and
+ // thus must be cut to the end of the actual string.
+ assert(nChPos != -1);
+ if (nChPos == -1 || nChPos == COMPLETE_STRING)
+ {
+ nChPos = m_Text.getLength();
+ }
+
+ nLen = nChPos - nBegin;
+ bFound = bLangOk && nLen > 0;
+ if (!bFound)
+ {
+ // create SwPaM with mark & point spanning the attributed text
+ //SwPaM aCurPaM( *this, *this, nBegin, nBegin + nLen ); <-- wrong c-tor, does sth different
+ SwPaM aCurPaM( *this, nBegin );
+ aCurPaM.SetMark();
+ aCurPaM.GetPoint()->nContent = nBegin + nLen;
+
+ // check script type of selected text
+ SwEditShell *pEditShell = GetDoc()->GetEditShell();
+ pEditShell->Push(); // save current cursor on stack
+ pEditShell->SetSelection( aCurPaM );
+ bool bIsAsianScript = (SvtScriptType::ASIAN == pEditShell->GetScriptType());
+ pEditShell->Pop(SwCursorShell::PopMode::DeleteCurrent); // restore cursor from stack
+
+ if (!bIsAsianScript && rArgs.bAllowImplicitChangesForNotConvertibleText)
+ {
+ // Store for later use
+ aImplicitChanges.emplace_back(nBegin, nBegin+nLen);
+ }
+ nBegin = nChPos; // start of next language portion
+ }
+ } while (!bFound && aIter.Next()); /* loop while nothing was found and still sth is left to be searched */
+
+ // Apply implicit changes, if any, now that aIter is no longer used
+ for (const auto& rImplicitChange : aImplicitChanges)
+ {
+ SwPaM aPaM( *this, rImplicitChange.first );
+ aPaM.SetMark();
+ aPaM.GetPoint()->nContent = rImplicitChange.second;
+ SetLanguageAndFont( aPaM, rArgs.nConvTargetLang, RES_CHRATR_CJK_LANGUAGE, rArgs.pTargetFont, RES_CHRATR_CJK_FONT );
+ }
+
+ }
+
+ // keep resulting text within selection / range of text to be converted
+ if (nBegin < nTextBegin)
+ nBegin = nTextBegin;
+ if (nBegin + nLen > nTextEnd)
+ nLen = nTextEnd - nBegin;
+ bool bInSelection = nBegin < nTextEnd;
+
+ if (bFound && bInSelection) // convertible text found within selection/range?
+ {
+ OSL_ENSURE( !m_Text.isEmpty(), "convertible text portion missing!" );
+ rArgs.aConvText = m_Text.copy(nBegin, nLen);
+ rArgs.nConvTextLang = nLangFound;
+
+ // position where to start looking in next iteration (after current ends)
+ rArgs.pStartNode = this;
+ rArgs.pStartIdx->Assign(this, nBegin + nLen );
+ // end position (when we have travelled over the whole document)
+ rArgs.pEndNode = this;
+ rArgs.pEndIdx->Assign(this, nBegin );
+ }
+
+ // restore original text
+ if ( bRestoreString )
+ {
+ m_Text = aOldText;
+ }
+
+ return !rArgs.aConvText.isEmpty();
+}
+
+// Note: this is a clone of SwTextNode::Spell, so keep them in sync when fixing things!
+SwRect SwTextFrame::AutoSpell_(SwTextNode & rNode, sal_Int32 nActPos)
+{
+ SwRect aRect;
+ assert(sw::FrameContainsNode(*this, rNode.GetIndex()));
+ SwTextNode *const pNode(&rNode);
+ if (!nActPos)
+ nActPos = COMPLETE_STRING;
+
+ SwAutoCompleteWord& rACW = SwDoc::GetAutoCompleteWords();
+
+ // modify string according to redline information and hidden text
+ const OUString aOldText( pNode->GetText() );
+ OUStringBuffer buf(pNode->m_Text);
+ const bool bRestoreString =
+ lcl_MaskRedlinesAndHiddenText(*pNode, buf, 0, pNode->GetText().getLength());
+ if (bRestoreString)
+ { // ??? UGLY: is it really necessary to modify m_Text here? just for GetLang()?
+ pNode->m_Text = buf.makeStringAndClear();
+ }
+
+ // a change of data indicates that at least one word has been modified
+
+ sal_Int32 nBegin = 0;
+ sal_Int32 nEnd = pNode->GetText().getLength();
+ sal_Int32 nInsertPos = 0;
+ sal_Int32 nChgStart = COMPLETE_STRING;
+ sal_Int32 nChgEnd = 0;
+ sal_Int32 nInvStart = COMPLETE_STRING;
+ sal_Int32 nInvEnd = 0;
+
+ const bool bAddAutoCmpl = pNode->IsAutoCompleteWordDirty() &&
+ SwViewOption::IsAutoCompleteWords();
+
+ if( pNode->GetWrong() )
+ {
+ nBegin = pNode->GetWrong()->GetBeginInv();
+ if( COMPLETE_STRING != nBegin )
+ {
+ nEnd = std::max(pNode->GetWrong()->GetEndInv(), pNode->GetText().getLength());
+ }
+
+ // get word around nBegin, we start at nBegin - 1
+ if ( COMPLETE_STRING != nBegin )
+ {
+ if ( nBegin )
+ --nBegin;
+
+ LanguageType eActLang = pNode->GetLang( nBegin );
+ Boundary aBound =
+ g_pBreakIt->GetBreakIter()->getWordBoundary( pNode->GetText(), nBegin,
+ g_pBreakIt->GetLocale( eActLang ),
+ WordType::DICTIONARY_WORD, true );
+ nBegin = aBound.startPos;
+ }
+
+ // get the position in the wrong list
+ nInsertPos = pNode->GetWrong()->GetWrongPos( nBegin );
+
+ // sometimes we have to skip one entry
+ if( nInsertPos < pNode->GetWrong()->Count() &&
+ nBegin == pNode->GetWrong()->Pos( nInsertPos ) +
+ pNode->GetWrong()->Len( nInsertPos ) )
+ nInsertPos++;
+ }
+
+ bool bFresh = nBegin < nEnd;
+ bool bPending(false);
+
+ if( bFresh )
+ {
+ uno::Reference< XSpellChecker1 > xSpell( ::GetSpellChecker() );
+ SwDoc* pDoc = pNode->GetDoc();
+
+ SwScanner aScanner( *pNode, pNode->GetText(), nullptr, ModelToViewHelper(),
+ WordType::DICTIONARY_WORD, nBegin, nEnd);
+
+ while( aScanner.NextWord() )
+ {
+ const OUString& rWord = aScanner.GetWord();
+ nBegin = aScanner.GetBegin();
+ sal_Int32 nLen = aScanner.GetLen();
+
+ // get next language for next word, consider language attributes
+ // within the word
+ LanguageType eActLang = aScanner.GetCurrentLanguage();
+ DetectAndMarkMissingDictionaries( pDoc, xSpell, eActLang );
+
+ bool bSpell = xSpell.is() && xSpell->hasLanguage( static_cast<sal_uInt16>(eActLang) );
+ if( bSpell && !rWord.isEmpty() )
+ {
+ // check for: bAlter => xHyphWord.is()
+ OSL_ENSURE(!bSpell || xSpell.is(), "NULL pointer");
+
+ if( !xSpell->isValid( rWord, static_cast<sal_uInt16>(eActLang), Sequence< PropertyValue >() ) )
+ {
+ sal_Int32 nSmartTagStt = nBegin;
+ sal_Int32 nDummy = 1;
+ if ( !pNode->GetSmartTags() || !pNode->GetSmartTags()->InWrongWord( nSmartTagStt, nDummy ) )
+ {
+ if( !pNode->GetWrong() )
+ {
+ pNode->SetWrong( new SwWrongList( WRONGLIST_SPELL ) );
+ pNode->GetWrong()->SetInvalid( 0, nEnd );
+ }
+ SwWrongList::FreshState const eState(pNode->GetWrong()->Fresh(
+ nChgStart, nChgEnd, nBegin, nLen, nInsertPos, nActPos));
+ switch (eState)
+ {
+ case SwWrongList::FreshState::FRESH:
+ pNode->GetWrong()->Insert(OUString(), nullptr, nBegin, nLen, nInsertPos++);
+ break;
+ case SwWrongList::FreshState::CURSOR:
+ bPending = true;
+ [[fallthrough]]; // to mark as invalid
+ case SwWrongList::FreshState::NOTHING:
+ nInvStart = nBegin;
+ nInvEnd = nBegin + nLen;
+ break;
+ }
+ }
+ }
+ else if( bAddAutoCmpl && rACW.GetMinWordLen() <= rWord.getLength() )
+ {
+ rACW.InsertWord( rWord, *pDoc );
+ }
+ }
+ }
+ }
+
+ // reset original text
+ // i63141 before calling GetCharRect(..) with formatting!
+ if ( bRestoreString )
+ {
+ pNode->m_Text = aOldText;
+ }
+ if( pNode->GetWrong() )
+ {
+ if( bFresh )
+ pNode->GetWrong()->Fresh( nChgStart, nChgEnd,
+ nEnd, 0, nInsertPos, nActPos );
+
+ // Calculate repaint area:
+
+ if( nChgStart < nChgEnd )
+ {
+ aRect = lcl_CalculateRepaintRect(*this, rNode, nChgStart, nChgEnd);
+
+ // fdo#71558 notify misspelled word to accessibility
+ SwViewShell* pViewSh = getRootFrame() ? getRootFrame()->GetCurrShell() : nullptr;
+ if( pViewSh )
+ pViewSh->InvalidateAccessibleParaAttrs( *this );
+ }
+
+ pNode->GetWrong()->SetInvalid( nInvStart, nInvEnd );
+ pNode->SetWrongDirty(
+ (COMPLETE_STRING != pNode->GetWrong()->GetBeginInv())
+ ? (bPending
+ ? SwTextNode::WrongState::PENDING
+ : SwTextNode::WrongState::TODO)
+ : SwTextNode::WrongState::DONE);
+ if( !pNode->GetWrong()->Count() && ! pNode->IsWrongDirty() )
+ pNode->SetWrong( nullptr );
+ }
+ else
+ pNode->SetWrongDirty(SwTextNode::WrongState::DONE);
+
+ if( bAddAutoCmpl )
+ pNode->SetAutoCompleteWordDirty( false );
+
+ return aRect;
+}
+
+/** Function: SmartTagScan
+
+ Function scans words in current text and checks them in the
+ smarttag libraries. If the check returns true to bounds of the
+ recognized words are stored into a list that is used later for drawing
+ the underline.
+
+ @return SwRect Repaint area
+*/
+SwRect SwTextFrame::SmartTagScan(SwTextNode & rNode)
+{
+ SwRect aRet;
+
+ assert(sw::FrameContainsNode(*this, rNode.GetIndex()));
+ SwTextNode *const pNode = &rNode;
+ const OUString& rText = pNode->GetText();
+
+ // Iterate over language portions
+ SmartTagMgr& rSmartTagMgr = SwSmartTagMgr::Get();
+
+ SwWrongList* pSmartTagList = pNode->GetSmartTags();
+
+ sal_Int32 nBegin = 0;
+ sal_Int32 nEnd = rText.getLength();
+
+ if ( pSmartTagList )
+ {
+ if ( pSmartTagList->GetBeginInv() != COMPLETE_STRING )
+ {
+ nBegin = pSmartTagList->GetBeginInv();
+ nEnd = std::min( pSmartTagList->GetEndInv(), rText.getLength() );
+
+ if ( nBegin < nEnd )
+ {
+ const LanguageType aCurrLang = pNode->GetLang( nBegin );
+ const css::lang::Locale aCurrLocale = g_pBreakIt->GetLocale( aCurrLang );
+ nBegin = g_pBreakIt->GetBreakIter()->beginOfSentence( rText, nBegin, aCurrLocale );
+ nEnd = g_pBreakIt->GetBreakIter()->endOfSentence(rText, nEnd, aCurrLocale);
+ if (nEnd > rText.getLength() || nEnd < 0)
+ nEnd = rText.getLength();
+ }
+ }
+ }
+
+ const sal_uInt16 nNumberOfEntries = pSmartTagList ? pSmartTagList->Count() : 0;
+ sal_uInt16 nNumberOfRemovedEntries = 0;
+ sal_uInt16 nNumberOfInsertedEntries = 0;
+
+ // clear smart tag list between nBegin and nEnd:
+ if ( 0 != nNumberOfEntries )
+ {
+ sal_Int32 nChgStart = COMPLETE_STRING;
+ sal_Int32 nChgEnd = 0;
+ const sal_uInt16 nCurrentIndex = pSmartTagList->GetWrongPos( nBegin );
+ pSmartTagList->Fresh( nChgStart, nChgEnd, nBegin, nEnd - nBegin, nCurrentIndex, COMPLETE_STRING );
+ nNumberOfRemovedEntries = nNumberOfEntries - pSmartTagList->Count();
+ }
+
+ if ( nBegin < nEnd )
+ {
+ // Expand the string:
+ const ModelToViewHelper aConversionMap(*pNode, getRootFrame() /*TODO - replace or expand fields for smart tags?*/);
+ const OUString& aExpandText = aConversionMap.getViewText();
+
+ // Ownership ov ConversionMap is passed to SwXTextMarkup object!
+ uno::Reference<text::XTextMarkup> const xTextMarkup =
+ new SwXTextMarkup(pNode, aConversionMap);
+
+ css::uno::Reference< css::frame::XController > xController = pNode->GetDoc()->GetDocShell()->GetController();
+
+ SwPosition start(*pNode, nBegin);
+ SwPosition end (*pNode, nEnd);
+ Reference< css::text::XTextRange > xRange = SwXTextRange::CreateXTextRange(*pNode->GetDoc(), start, &end);
+
+ rSmartTagMgr.RecognizeTextRange(xRange, xTextMarkup, xController);
+
+ sal_Int32 nLangBegin = nBegin;
+ sal_Int32 nLangEnd;
+
+ // smart tag recognition has to be done for each language portion:
+ SwLanguageIterator aIter( *pNode, nLangBegin );
+
+ do
+ {
+ const LanguageType nLang = aIter.GetLanguage();
+ const css::lang::Locale aLocale = g_pBreakIt->GetLocale( nLang );
+ nLangEnd = std::min<sal_Int32>( nEnd, aIter.GetChgPos() );
+
+ const sal_Int32 nExpandBegin = aConversionMap.ConvertToViewPosition( nLangBegin );
+ const sal_Int32 nExpandEnd = aConversionMap.ConvertToViewPosition( nLangEnd );
+
+ rSmartTagMgr.RecognizeString(aExpandText, xTextMarkup, xController, aLocale, nExpandBegin, nExpandEnd - nExpandBegin );
+
+ nLangBegin = nLangEnd;
+ }
+ while ( aIter.Next() && nLangEnd < nEnd );
+
+ pSmartTagList = pNode->GetSmartTags();
+
+ const sal_uInt16 nNumberOfEntriesAfterRecognize = pSmartTagList ? pSmartTagList->Count() : 0;
+ nNumberOfInsertedEntries = nNumberOfEntriesAfterRecognize - ( nNumberOfEntries - nNumberOfRemovedEntries );
+ }
+
+ if( pSmartTagList )
+ {
+ // Update WrongList stuff
+ pSmartTagList->SetInvalid( COMPLETE_STRING, 0 );
+ pNode->SetSmartTagDirty( COMPLETE_STRING != pSmartTagList->GetBeginInv() );
+
+ if( !pSmartTagList->Count() && !pNode->IsSmartTagDirty() )
+ pNode->SetSmartTags( nullptr );
+
+ // Calculate repaint area:
+ if ( nBegin < nEnd && ( 0 != nNumberOfRemovedEntries ||
+ 0 != nNumberOfInsertedEntries ) )
+ {
+ aRet = lcl_CalculateRepaintRect(*this, rNode, nBegin, nEnd);
+ }
+ }
+ else
+ pNode->SetSmartTagDirty( false );
+
+ return aRet;
+}
+
+void SwTextFrame::CollectAutoCmplWrds(SwTextNode & rNode, sal_Int32 nActPos)
+{
+ assert(sw::FrameContainsNode(*this, rNode.GetIndex())); (void) this;
+ SwTextNode *const pNode(&rNode);
+ if (!nActPos)
+ nActPos = COMPLETE_STRING;
+
+ SwDoc* pDoc = pNode->GetDoc();
+ SwAutoCompleteWord& rACW = SwDoc::GetAutoCompleteWords();
+
+ sal_Int32 nBegin = 0;
+ sal_Int32 nEnd = pNode->GetText().getLength();
+ sal_Int32 nLen;
+ bool bACWDirty = false;
+
+ if( nBegin < nEnd )
+ {
+ int nCnt = 200;
+ SwScanner aScanner( *pNode, pNode->GetText(), nullptr, ModelToViewHelper(),
+ WordType::DICTIONARY_WORD, nBegin, nEnd );
+ while( aScanner.NextWord() )
+ {
+ nBegin = aScanner.GetBegin();
+ nLen = aScanner.GetLen();
+ if( rACW.GetMinWordLen() <= nLen )
+ {
+ const OUString& rWord = aScanner.GetWord();
+
+ if( nActPos < nBegin || ( nBegin + nLen ) < nActPos )
+ {
+ if( rACW.GetMinWordLen() <= rWord.getLength() )
+ rACW.InsertWord( rWord, *pDoc );
+ }
+ else
+ bACWDirty = true;
+ }
+ if( !--nCnt )
+ {
+ // don't wait for TIMER here, so we can finish big paragraphs
+ if (Application::AnyInput(VCL_INPUT_ANY & VclInputFlags(~VclInputFlags::TIMER)))
+ return;
+ nCnt = 100;
+ }
+ }
+ }
+
+ if (!bACWDirty)
+ pNode->SetAutoCompleteWordDirty( false );
+}
+
+SwInterHyphInfoTextFrame::SwInterHyphInfoTextFrame(
+ SwTextFrame const& rFrame, SwTextNode const& rNode,
+ SwInterHyphInfo const& rHyphInfo)
+ : m_nStart(rFrame.MapModelToView(&rNode, rHyphInfo.m_nStart))
+ , m_nEnd(rFrame.MapModelToView(&rNode, rHyphInfo.m_nEnd))
+ , m_nWordStart(0)
+ , m_nWordLen(0)
+{
+}
+
+void SwInterHyphInfoTextFrame::UpdateTextNodeHyphInfo(SwTextFrame const& rFrame,
+ SwTextNode const& rNode, SwInterHyphInfo & o_rHyphInfo)
+{
+ std::pair<SwTextNode const*, sal_Int32> const wordStart(rFrame.MapViewToModel(m_nWordStart));
+ std::pair<SwTextNode const*, sal_Int32> const wordEnd(rFrame.MapViewToModel(m_nWordStart+m_nWordLen));
+ if (wordStart.first != &rNode || wordEnd.first != &rNode)
+ { // not sure if this can happen since nStart/nEnd are in rNode
+ SAL_WARN("sw.core", "UpdateTextNodeHyphInfo: outside of node");
+ return;
+ }
+ o_rHyphInfo.m_nWordStart = wordStart.second;
+ o_rHyphInfo.m_nWordLen = wordEnd.second - wordStart.second;
+ o_rHyphInfo.SetHyphWord(m_xHyphWord);
+}
+
+/// Find the SwTextFrame and call its Hyphenate
+bool SwTextNode::Hyphenate( SwInterHyphInfo &rHyphInf )
+{
+ // shortcut: paragraph doesn't have a language set:
+ if ( LANGUAGE_NONE == GetSwAttrSet().GetLanguage().GetLanguage()
+ && LanguageType(USHRT_MAX) == GetLang(0, m_Text.getLength()))
+ {
+ return false;
+ }
+
+ SwTextFrame *pFrame = ::sw::SwHyphIterCacheLastTextFrame(this,
+ [&rHyphInf, this]() {
+ std::pair<Point, bool> tmp;
+ Point const*const pPoint = rHyphInf.GetCursorPos();
+ if (pPoint)
+ {
+ tmp.first = *pPoint;
+ tmp.second = true;
+ }
+ return static_cast<SwTextFrame*>(this->getLayoutFrame(
+ this->GetDoc()->getIDocumentLayoutAccess().GetCurrentLayout(),
+ nullptr, pPoint ? &tmp : nullptr));
+ });
+ if (!pFrame)
+ {
+ // There was a comment here that claimed that the following assertion
+ // shouldn't exist as it's triggered by "Trennung ueber Sonderbereiche",
+ // (hyphenation across special sections?), whatever that means.
+ OSL_ENSURE( pFrame, "!SwTextNode::Hyphenate: can't find any frame" );
+ return false;
+ }
+ SwInterHyphInfoTextFrame aHyphInfo(*pFrame, *this, rHyphInf);
+
+ pFrame = &(pFrame->GetFrameAtOfst( aHyphInfo.m_nStart ));
+
+ while( pFrame )
+ {
+ if (pFrame->Hyphenate(aHyphInfo))
+ {
+ // The layout is not robust wrt. "direct formatting"
+ // cf. layact.cxx, SwLayAction::TurboAction_(), if( !pCnt->IsValid() ...
+ pFrame->SetCompletePaint();
+ aHyphInfo.UpdateTextNodeHyphInfo(*pFrame, *this, rHyphInf);
+ return true;
+ }
+ pFrame = pFrame->GetFollow();
+ if( pFrame )
+ {
+ aHyphInfo.m_nEnd = aHyphInfo.m_nEnd - (pFrame->GetOffset() - aHyphInfo.m_nStart);
+ aHyphInfo.m_nStart = pFrame->GetOffset();
+ }
+ }
+ return false;
+}
+
+namespace
+{
+ struct swTransliterationChgData
+ {
+ sal_Int32 nStart;
+ sal_Int32 nLen;
+ OUString sChanged;
+ Sequence< sal_Int32 > aOffsets;
+ };
+}
+
+// change text to Upper/Lower/Hiragana/Katakana/...
+void SwTextNode::TransliterateText(
+ utl::TransliterationWrapper& rTrans,
+ sal_Int32 nStt, sal_Int32 nEnd,
+ SwUndoTransliterate* pUndo )
+{
+ if (nStt < nEnd)
+ {
+ // since we don't use Hiragana/Katakana or half-width/full-width transliterations here
+ // it is fine to use ANYWORD_IGNOREWHITESPACES. (ANY_WORD btw is broken and will
+ // occasionally miss words in consecutive sentences). Also with ANYWORD_IGNOREWHITESPACES
+ // text like 'just-in-time' will be converted to 'Just-In-Time' which seems to be the
+ // proper thing to do.
+ const sal_Int16 nWordType = WordType::ANYWORD_IGNOREWHITESPACES;
+
+ // In order to have less trouble with changing text size, e.g. because
+ // of ligatures or German small sz being resolved, we need to process
+ // the text replacements from end to start.
+ // This way the offsets for the yet to be changed words will be
+ // left unchanged by the already replaced text.
+ // For this we temporarily save the changes to be done in this vector
+ std::vector< swTransliterationChgData > aChanges;
+ swTransliterationChgData aChgData;
+
+ if (rTrans.getType() == TransliterationFlags::TITLE_CASE)
+ {
+ // for 'capitalize every word' we need to iterate over each word
+
+ Boundary aSttBndry;
+ Boundary aEndBndry;
+ aSttBndry = g_pBreakIt->GetBreakIter()->getWordBoundary(
+ GetText(), nStt,
+ g_pBreakIt->GetLocale( GetLang( nStt ) ),
+ nWordType,
+ true /*prefer forward direction*/);
+ aEndBndry = g_pBreakIt->GetBreakIter()->getWordBoundary(
+ GetText(), nEnd,
+ g_pBreakIt->GetLocale( GetLang( nEnd ) ),
+ nWordType,
+ false /*prefer backward direction*/);
+
+ // prevent backtracking to the previous word if selection is at word boundary
+ if (aSttBndry.endPos <= nStt)
+ {
+ aSttBndry = g_pBreakIt->GetBreakIter()->nextWord(
+ GetText(), aSttBndry.endPos,
+ g_pBreakIt->GetLocale( GetLang( aSttBndry.endPos ) ),
+ nWordType);
+ }
+ // prevent advancing to the next word if selection is at word boundary
+ if (aEndBndry.startPos >= nEnd)
+ {
+ aEndBndry = g_pBreakIt->GetBreakIter()->previousWord(
+ GetText(), aEndBndry.startPos,
+ g_pBreakIt->GetLocale( GetLang( aEndBndry.startPos ) ),
+ nWordType);
+ }
+
+ Boundary aCurWordBndry( aSttBndry );
+ while (aCurWordBndry.startPos <= aEndBndry.startPos)
+ {
+ nStt = aCurWordBndry.startPos;
+ nEnd = aCurWordBndry.endPos;
+ const sal_Int32 nLen = nEnd - nStt;
+ OSL_ENSURE( nLen > 0, "invalid word length of 0" );
+
+ Sequence <sal_Int32> aOffsets;
+ OUString const sChgd( rTrans.transliterate(
+ GetText(), GetLang(nStt), nStt, nLen, &aOffsets) );
+
+ assert(nStt < m_Text.getLength());
+ if (0 != rtl_ustr_shortenedCompare_WithLength(
+ m_Text.getStr() + nStt, m_Text.getLength() - nStt,
+ sChgd.getStr(), sChgd.getLength(), nLen))
+ {
+ aChgData.nStart = nStt;
+ aChgData.nLen = nLen;
+ aChgData.sChanged = sChgd;
+ aChgData.aOffsets = aOffsets;
+ aChanges.push_back( aChgData );
+ }
+
+ aCurWordBndry = g_pBreakIt->GetBreakIter()->nextWord(
+ GetText(), nStt,
+ g_pBreakIt->GetLocale(GetLang(nStt, 1)),
+ nWordType);
+ }
+ }
+ else if (rTrans.getType() == TransliterationFlags::SENTENCE_CASE)
+ {
+ // for 'sentence case' we need to iterate sentence by sentence
+
+ sal_Int32 nLastStart = g_pBreakIt->GetBreakIter()->beginOfSentence(
+ GetText(), nEnd,
+ g_pBreakIt->GetLocale( GetLang( nEnd ) ) );
+ sal_Int32 nLastEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
+ GetText(), nLastStart,
+ g_pBreakIt->GetLocale( GetLang( nLastStart ) ) );
+
+ // extend nStt, nEnd to the current sentence boundaries
+ sal_Int32 nCurrentStart = g_pBreakIt->GetBreakIter()->beginOfSentence(
+ GetText(), nStt,
+ g_pBreakIt->GetLocale( GetLang( nStt ) ) );
+ sal_Int32 nCurrentEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
+ GetText(), nCurrentStart,
+ g_pBreakIt->GetLocale( GetLang( nCurrentStart ) ) );
+
+ // prevent backtracking to the previous sentence if selection starts at end of a sentence
+ if (nCurrentEnd <= nStt)
+ {
+ // now nCurrentStart is probably located on a non-letter word. (unless we
+ // are in Asian text with no spaces...)
+ // Thus to get the real sentence start we should locate the next real word,
+ // that is one found by DICTIONARY_WORD
+ i18n::Boundary aBndry = g_pBreakIt->GetBreakIter()->nextWord(
+ GetText(), nCurrentEnd,
+ g_pBreakIt->GetLocale( GetLang( nCurrentEnd ) ),
+ i18n::WordType::DICTIONARY_WORD);
+
+ // now get new current sentence boundaries
+ nCurrentStart = g_pBreakIt->GetBreakIter()->beginOfSentence(
+ GetText(), aBndry.startPos,
+ g_pBreakIt->GetLocale( GetLang( aBndry.startPos) ) );
+ nCurrentEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
+ GetText(), nCurrentStart,
+ g_pBreakIt->GetLocale( GetLang( nCurrentStart) ) );
+ }
+ // prevent advancing to the next sentence if selection ends at start of a sentence
+ if (nLastStart >= nEnd)
+ {
+ // now nCurrentStart is probably located on a non-letter word. (unless we
+ // are in Asian text with no spaces...)
+ // Thus to get the real sentence start we should locate the previous real word,
+ // that is one found by DICTIONARY_WORD
+ i18n::Boundary aBndry = g_pBreakIt->GetBreakIter()->previousWord(
+ GetText(), nLastStart,
+ g_pBreakIt->GetLocale( GetLang( nLastStart) ),
+ i18n::WordType::DICTIONARY_WORD);
+ nLastEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
+ GetText(), aBndry.startPos,
+ g_pBreakIt->GetLocale( GetLang( aBndry.startPos) ) );
+ if (nCurrentEnd > nLastEnd)
+ nCurrentEnd = nLastEnd;
+ }
+
+ while (nCurrentStart < nLastEnd)
+ {
+ sal_Int32 nLen = nCurrentEnd - nCurrentStart;
+ OSL_ENSURE( nLen > 0, "invalid word length of 0" );
+
+ Sequence <sal_Int32> aOffsets;
+ OUString const sChgd( rTrans.transliterate(GetText(),
+ GetLang(nCurrentStart), nCurrentStart, nLen, &aOffsets) );
+
+ assert(nStt < m_Text.getLength());
+ if (0 != rtl_ustr_shortenedCompare_WithLength(
+ m_Text.getStr() + nStt, m_Text.getLength() - nStt,
+ sChgd.getStr(), sChgd.getLength(), nLen))
+ {
+ aChgData.nStart = nCurrentStart;
+ aChgData.nLen = nLen;
+ aChgData.sChanged = sChgd;
+ aChgData.aOffsets = aOffsets;
+ aChanges.push_back( aChgData );
+ }
+
+ Boundary aFirstWordBndry = g_pBreakIt->GetBreakIter()->nextWord(
+ GetText(), nCurrentEnd,
+ g_pBreakIt->GetLocale( GetLang( nCurrentEnd ) ),
+ nWordType);
+ nCurrentStart = aFirstWordBndry.startPos;
+ nCurrentEnd = g_pBreakIt->GetBreakIter()->endOfSentence(
+ GetText(), nCurrentStart,
+ g_pBreakIt->GetLocale( GetLang( nCurrentStart ) ) );
+ }
+ }
+ else
+ {
+ // here we may transliterate over complete language portions...
+
+ std::unique_ptr<SwLanguageIterator> pIter;
+ if( rTrans.needLanguageForTheMode() )
+ pIter.reset(new SwLanguageIterator( *this, nStt ));
+
+ sal_Int32 nEndPos = 0;
+ LanguageType nLang = LANGUAGE_NONE;
+ do {
+ if( pIter )
+ {
+ nLang = pIter->GetLanguage();
+ nEndPos = pIter->GetChgPos();
+ if( nEndPos > nEnd )
+ nEndPos = nEnd;
+ }
+ else
+ {
+ nLang = LANGUAGE_SYSTEM;
+ nEndPos = nEnd;
+ }
+ const sal_Int32 nLen = nEndPos - nStt;
+
+ Sequence <sal_Int32> aOffsets;
+ OUString const sChgd( rTrans.transliterate(
+ m_Text, nLang, nStt, nLen, &aOffsets) );
+
+ assert(nStt < m_Text.getLength());
+ if (0 != rtl_ustr_shortenedCompare_WithLength(
+ m_Text.getStr() + nStt, m_Text.getLength() - nStt,
+ sChgd.getStr(), sChgd.getLength(), nLen))
+ {
+ aChgData.nStart = nStt;
+ aChgData.nLen = nLen;
+ aChgData.sChanged = sChgd;
+ aChgData.aOffsets = aOffsets;
+ aChanges.push_back( aChgData );
+ }
+
+ nStt = nEndPos;
+ } while( nEndPos < nEnd && pIter && pIter->Next() );
+ }
+
+ if (!aChanges.empty())
+ {
+ // now apply the changes from end to start to leave the offsets of the
+ // yet unchanged text parts remain the same.
+ size_t nSum(0);
+ for (size_t i = 0; i < aChanges.size(); ++i)
+ { // check this here since AddChanges cannot be moved below
+ // call to ReplaceTextOnly
+ swTransliterationChgData & rData =
+ aChanges[ aChanges.size() - 1 - i ];
+ nSum += rData.sChanged.getLength() - rData.nLen;
+ if (nSum > o3tl::make_unsigned(GetSpaceLeft()))
+ {
+ SAL_WARN("sw.core", "SwTextNode::ReplaceTextOnly: "
+ "node text with insertion > node capacity.");
+ return;
+ }
+ if (pUndo)
+ pUndo->AddChanges( *this, rData.nStart, rData.nLen, rData.aOffsets );
+ ReplaceTextOnly( rData.nStart, rData.nLen, rData.sChanged, rData.aOffsets );
+ }
+ }
+ }
+}
+
+void SwTextNode::ReplaceTextOnly( sal_Int32 nPos, sal_Int32 nLen,
+ const OUString & rText,
+ const Sequence<sal_Int32>& rOffsets )
+{
+ assert(rText.getLength() - nLen <= GetSpaceLeft());
+
+ m_Text = m_Text.replaceAt(nPos, nLen, rText);
+
+ sal_Int32 nTLen = rText.getLength();
+ const sal_Int32* pOffsets = rOffsets.getConstArray();
+ // now look for no 1-1 mapping -> move the indices!
+ sal_Int32 nMyOff = nPos;
+ for( sal_Int32 nI = 0; nI < nTLen; ++nI )
+ {
+ const sal_Int32 nOff = pOffsets[ nI ];
+ if( nOff < nMyOff )
+ {
+ // something is inserted
+ sal_Int32 nCnt = 1;
+ while( nI + nCnt < nTLen && nOff == pOffsets[ nI + nCnt ] )
+ ++nCnt;
+
+ Update( SwIndex( this, nMyOff ), nCnt );
+ nMyOff = nOff;
+ //nMyOff -= nCnt;
+ nI += nCnt - 1;
+ }
+ else if( nOff > nMyOff )
+ {
+ // something is deleted
+ Update( SwIndex( this, nMyOff+1 ), nOff - nMyOff, true );
+ nMyOff = nOff;
+ }
+ ++nMyOff;
+ }
+ if( nMyOff < nLen )
+ // something is deleted at the end
+ Update( SwIndex( this, nMyOff ), nLen - nMyOff, true );
+
+ // notify the layout!
+ SwDelText aDelHint( nPos, nTLen );
+ NotifyClients( nullptr, &aDelHint );
+
+ SwInsText aHint( nPos, nTLen );
+ NotifyClients( nullptr, &aHint );
+}
+
+// the return values allows us to see if we did the heavy-
+// lifting required to actually break and count the words.
+bool SwTextNode::CountWords( SwDocStat& rStat,
+ sal_Int32 nStt, sal_Int32 nEnd ) const
+{
+ if( nStt > nEnd )
+ { // bad call
+ return false;
+ }
+ if (IsInRedlines())
+ { //not counting txtnodes used to hold deleted redline content
+ return false;
+ }
+ bool bCountAll = ( (0 == nStt) && (GetText().getLength() == nEnd) );
+ ++rStat.nAllPara; // #i93174#: count _all_ paragraphs
+ if ( IsHidden() )
+ { // not counting hidden paras
+ return false;
+ }
+ // count words in numbering string if started at beginning of para:
+ bool bCountNumbering = nStt == 0;
+ bool bHasBullet = false, bHasNumbering = false;
+ OUString sNumString;
+ if (bCountNumbering)
+ {
+ sNumString = GetNumString();
+ bHasNumbering = !sNumString.isEmpty();
+ if (!bHasNumbering)
+ bHasBullet = HasBullet();
+ bCountNumbering = bHasNumbering || bHasBullet;
+ }
+
+ if( nStt == nEnd && !bCountNumbering)
+ { // unnumbered empty node or empty selection
+ return false;
+ }
+
+ // count of non-empty paras
+ ++rStat.nPara;
+
+ // Shortcut when counting whole paragraph and current count is clean
+ if ( bCountAll && !IsWordCountDirty() )
+ {
+ // accumulate into DocStat record to return the values
+ if (m_pParaIdleData_Impl)
+ {
+ rStat.nWord += m_pParaIdleData_Impl->nNumberOfWords;
+ rStat.nAsianWord += m_pParaIdleData_Impl->nNumberOfAsianWords;
+ rStat.nChar += m_pParaIdleData_Impl->nNumberOfChars;
+ rStat.nCharExcludingSpaces += m_pParaIdleData_Impl->nNumberOfCharsExcludingSpaces;
+ }
+ return false;
+ }
+
+ // ConversionMap to expand fields, remove invisible and redline deleted text for scanner
+ const ModelToViewHelper aConversionMap(*this,
+ getIDocumentLayoutAccess().GetCurrentLayout(),
+ ExpandMode::ExpandFields | ExpandMode::ExpandFootnote | ExpandMode::HideInvisible | ExpandMode::HideDeletions);
+ const OUString& aExpandText = aConversionMap.getViewText();
+
+ if (aExpandText.isEmpty() && !bCountNumbering)
+ {
+ return false;
+ }
+
+ // map start and end points onto the ConversionMap
+ const sal_Int32 nExpandBegin = aConversionMap.ConvertToViewPosition( nStt );
+ const sal_Int32 nExpandEnd = aConversionMap.ConvertToViewPosition( nEnd );
+
+ //do the count
+ // all counts exclude hidden paras and hidden+redlined within para
+ // definition of space/white chars in SwScanner (and BreakIter!)
+ // uses both u_isspace and BreakIter getWordBoundary in SwScanner
+ sal_uInt32 nTmpWords = 0; // count of all words
+ sal_uInt32 nTmpAsianWords = 0; //count of all Asian codepoints
+ sal_uInt32 nTmpChars = 0; // count of all chars
+ sal_uInt32 nTmpCharsExcludingSpaces = 0; // all non-white chars
+
+ // count words in masked and expanded text:
+ if (!aExpandText.isEmpty())
+ {
+ assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is());
+
+ // zero is NULL for pLanguage -----------v last param = true for clipping
+ SwScanner aScanner( *this, aExpandText, nullptr, aConversionMap, i18n::WordType::WORD_COUNT,
+ nExpandBegin, nExpandEnd, true );
+
+ // used to filter out scanner returning almost empty strings (len=1; unichar=0x0001)
+ const OUString aBreakWord( CH_TXTATR_BREAKWORD );
+
+ while ( aScanner.NextWord() )
+ {
+ if( !aExpandText.match(aBreakWord, aScanner.GetBegin() ))
+ {
+ ++nTmpWords;
+ const OUString &rWord = aScanner.GetWord();
+ if (g_pBreakIt->GetBreakIter()->getScriptType(rWord, 0) == i18n::ScriptType::ASIAN)
+ ++nTmpAsianWords;
+ nTmpCharsExcludingSpaces += g_pBreakIt->getGraphemeCount(rWord);
+ }
+ }
+
+ nTmpCharsExcludingSpaces += aScanner.getOverriddenDashCount();
+
+ nTmpChars = g_pBreakIt->getGraphemeCount(aExpandText, nExpandBegin, nExpandEnd);
+ }
+
+ // no nTmpCharsExcludingSpaces adjust needed neither for blanked out MaskedChars
+ // nor for mid-word selection - set scanner bClip = true at creation
+
+ // count outline number label - ? no expansion into map
+ // always counts all of number-ish label
+ if (bHasNumbering) // count words in numbering string
+ {
+ LanguageType aLanguage = GetLang( 0 );
+
+ SwScanner aScanner( *this, sNumString, &aLanguage, ModelToViewHelper(),
+ i18n::WordType::WORD_COUNT, 0, sNumString.getLength(), true );
+
+ while ( aScanner.NextWord() )
+ {
+ ++nTmpWords;
+ const OUString &rWord = aScanner.GetWord();
+ if (g_pBreakIt->GetBreakIter()->getScriptType(rWord, 0) == i18n::ScriptType::ASIAN)
+ ++nTmpAsianWords;
+ nTmpCharsExcludingSpaces += g_pBreakIt->getGraphemeCount(rWord);
+ }
+
+ nTmpCharsExcludingSpaces += aScanner.getOverriddenDashCount();
+ nTmpChars += g_pBreakIt->getGraphemeCount(sNumString);
+ }
+ else if ( bHasBullet )
+ {
+ ++nTmpWords;
+ ++nTmpChars;
+ ++nTmpCharsExcludingSpaces;
+ }
+
+ // If counting the whole para then update cached values and mark clean
+ if ( bCountAll )
+ {
+ if ( m_pParaIdleData_Impl )
+ {
+ m_pParaIdleData_Impl->nNumberOfWords = nTmpWords;
+ m_pParaIdleData_Impl->nNumberOfAsianWords = nTmpAsianWords;
+ m_pParaIdleData_Impl->nNumberOfChars = nTmpChars;
+ m_pParaIdleData_Impl->nNumberOfCharsExcludingSpaces = nTmpCharsExcludingSpaces;
+ }
+ SetWordCountDirty( false );
+ }
+ // accumulate into DocStat record to return the values
+ rStat.nWord += nTmpWords;
+ rStat.nAsianWord += nTmpAsianWords;
+ rStat.nChar += nTmpChars;
+ rStat.nCharExcludingSpaces += nTmpCharsExcludingSpaces;
+
+ return true;
+}
+
+// Paragraph statistics start -->
+
+void SwTextNode::InitSwParaStatistics( bool bNew )
+{
+ if ( bNew )
+ {
+ m_pParaIdleData_Impl = new SwParaIdleData_Impl;
+ }
+ else if ( m_pParaIdleData_Impl )
+ {
+ delete m_pParaIdleData_Impl->pWrong;
+ delete m_pParaIdleData_Impl->pGrammarCheck;
+ delete m_pParaIdleData_Impl->pSmartTags;
+ delete m_pParaIdleData_Impl;
+ m_pParaIdleData_Impl = nullptr;
+ }
+}
+
+void SwTextNode::SetWrong( SwWrongList* pNew, bool bDelete )
+{
+ if ( m_pParaIdleData_Impl )
+ {
+ if ( bDelete )
+ {
+ delete m_pParaIdleData_Impl->pWrong;
+ }
+ m_pParaIdleData_Impl->pWrong = pNew;
+ }
+}
+
+SwWrongList* SwTextNode::GetWrong()
+{
+ return m_pParaIdleData_Impl ? m_pParaIdleData_Impl->pWrong : nullptr;
+}
+
+// #i71360#
+const SwWrongList* SwTextNode::GetWrong() const
+{
+ return m_pParaIdleData_Impl ? m_pParaIdleData_Impl->pWrong : nullptr;
+}
+
+void SwTextNode::SetGrammarCheck( SwGrammarMarkUp* pNew, bool bDelete )
+{
+ if ( m_pParaIdleData_Impl )
+ {
+ if ( bDelete )
+ {
+ delete m_pParaIdleData_Impl->pGrammarCheck;
+ }
+ m_pParaIdleData_Impl->pGrammarCheck = pNew;
+ }
+}
+
+SwGrammarMarkUp* SwTextNode::GetGrammarCheck()
+{
+ return m_pParaIdleData_Impl ? m_pParaIdleData_Impl->pGrammarCheck : nullptr;
+}
+
+SwWrongList const* SwTextNode::GetGrammarCheck() const
+{
+ return static_cast<SwWrongList const*>(const_cast<SwTextNode*>(this)->GetGrammarCheck());
+}
+
+void SwTextNode::SetSmartTags( SwWrongList* pNew, bool bDelete )
+{
+ OSL_ENSURE( !pNew || SwSmartTagMgr::Get().IsSmartTagsEnabled(),
+ "Weird - we have a smart tag list without any recognizers?" );
+
+ if ( m_pParaIdleData_Impl )
+ {
+ if ( bDelete )
+ {
+ delete m_pParaIdleData_Impl->pSmartTags;
+ }
+ m_pParaIdleData_Impl->pSmartTags = pNew;
+ }
+}
+
+SwWrongList* SwTextNode::GetSmartTags()
+{
+ return m_pParaIdleData_Impl ? m_pParaIdleData_Impl->pSmartTags : nullptr;
+}
+
+SwWrongList const* SwTextNode::GetSmartTags() const
+{
+ return const_cast<SwWrongList const*>(const_cast<SwTextNode*>(this)->GetSmartTags());
+}
+
+void SwTextNode::SetWordCountDirty( bool bNew ) const
+{
+ if ( m_pParaIdleData_Impl )
+ {
+ m_pParaIdleData_Impl->bWordCountDirty = bNew;
+ }
+}
+
+bool SwTextNode::IsWordCountDirty() const
+{
+ return m_pParaIdleData_Impl && m_pParaIdleData_Impl->bWordCountDirty;
+}
+
+void SwTextNode::SetWrongDirty(WrongState eNew) const
+{
+ if ( m_pParaIdleData_Impl )
+ {
+ m_pParaIdleData_Impl->eWrongDirty = eNew;
+ }
+}
+
+auto SwTextNode::GetWrongDirty() const -> WrongState
+{
+ return m_pParaIdleData_Impl ? m_pParaIdleData_Impl->eWrongDirty : WrongState::DONE;
+}
+
+bool SwTextNode::IsWrongDirty() const
+{
+ return m_pParaIdleData_Impl && m_pParaIdleData_Impl->eWrongDirty != WrongState::DONE;
+}
+
+void SwTextNode::SetGrammarCheckDirty( bool bNew ) const
+{
+ if ( m_pParaIdleData_Impl )
+ {
+ m_pParaIdleData_Impl->bGrammarCheckDirty = bNew;
+ }
+}
+
+bool SwTextNode::IsGrammarCheckDirty() const
+{
+ return m_pParaIdleData_Impl && m_pParaIdleData_Impl->bGrammarCheckDirty;
+}
+
+void SwTextNode::SetSmartTagDirty( bool bNew ) const
+{
+ if ( m_pParaIdleData_Impl )
+ {
+ m_pParaIdleData_Impl->bSmartTagDirty = bNew;
+ }
+}
+
+bool SwTextNode::IsSmartTagDirty() const
+{
+ return m_pParaIdleData_Impl && m_pParaIdleData_Impl->bSmartTagDirty;
+}
+
+void SwTextNode::SetAutoCompleteWordDirty( bool bNew ) const
+{
+ if ( m_pParaIdleData_Impl )
+ {
+ m_pParaIdleData_Impl->bAutoComplDirty = bNew;
+ }
+}
+
+bool SwTextNode::IsAutoCompleteWordDirty() const
+{
+ return m_pParaIdleData_Impl && m_pParaIdleData_Impl->bAutoComplDirty;
+}
+
+// <-- Paragraph statistics end
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */