diff options
Diffstat (limited to 'vcl/source/edit/texteng.cxx')
-rw-r--r-- | vcl/source/edit/texteng.cxx | 2895 |
1 files changed, 2895 insertions, 0 deletions
diff --git a/vcl/source/edit/texteng.cxx b/vcl/source/edit/texteng.cxx new file mode 100644 index 000000000..6171e0417 --- /dev/null +++ b/vcl/source/edit/texteng.cxx @@ -0,0 +1,2895 @@ +/* -*- 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 <tools/stream.hxx> + +#include <vcl/texteng.hxx> +#include <vcl/textview.hxx> +#include <vcl/commandevent.hxx> +#include <vcl/inputctx.hxx> +#include "textdoc.hxx" +#include "textdat2.hxx" +#include "textundo.hxx" +#include "textund2.hxx" +#include <svl/ctloptions.hxx> +#include <vcl/window.hxx> +#include <vcl/settings.hxx> +#include <vcl/toolkit/edit.hxx> +#include <vcl/virdev.hxx> +#include <sal/log.hxx> +#include <osl/diagnose.h> + +#include <com/sun/star/i18n/XBreakIterator.hpp> + +#include <com/sun/star/i18n/CharacterIteratorMode.hpp> + +#include <com/sun/star/i18n/WordType.hpp> + +#include <com/sun/star/i18n/InputSequenceChecker.hpp> +#include <com/sun/star/i18n/InputSequenceCheckMode.hpp> +#include <com/sun/star/i18n/ScriptType.hpp> + +#include <comphelper/processfactory.hxx> + +#include <unotools/localedatawrapper.hxx> +#include <vcl/unohelp.hxx> + +#include <vcl/svapp.hxx> + +#include <unicode/ubidi.h> + +#include <algorithm> +#include <cstddef> +#include <cstdlib> +#include <memory> +#include <o3tl/sorted_vector.hxx> +#include <string_view> +#include <vector> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::uno; + +TextEngine::TextEngine() + : mpActiveView {nullptr} + , maTextColor {COL_BLACK} + , mnMaxTextLen {0} + , mnMaxTextWidth {0} + , mnCharHeight {0} + , mnCurTextWidth {-1} + , mnCurTextHeight {0} + , mnDefTab {0} + , meAlign {TxtAlign::Left} + , mbIsFormatting {false} + , mbFormatted {false} + , mbUpdate {true} + , mbModified {false} + , mbUndoEnabled {false} + , mbIsInUndo {false} + , mbDowning {false} + , mbRightToLeft {false} + , mbHasMultiLineParas {false} +{ + mpViews.reset( new TextViews ); + + mpIdleFormatter.reset( new IdleFormatter ); + mpIdleFormatter->SetInvokeHandler( LINK( this, TextEngine, IdleFormatHdl ) ); + + mpRefDev = VclPtr<VirtualDevice>::Create(); + + ImpInitLayoutMode( mpRefDev ); + + ImpInitDoc(); + + vcl::Font aFont(mpRefDev->GetFont().GetFamilyName(), Size(0, 0)); + aFont.SetTransparent( false ); + Color aFillColor( aFont.GetFillColor() ); + aFillColor.SetAlpha( 255 ); + aFont.SetFillColor( aFillColor ); + SetFont( aFont ); +} + +TextEngine::~TextEngine() +{ + mbDowning = true; + + mpIdleFormatter.reset(); + mpDoc.reset(); + mpTEParaPortions.reset(); + mpViews.reset(); // only the list, not the Views + mpRefDev.disposeAndClear(); + mpUndoManager.reset(); + mpIMEInfos.reset(); + mpLocaleDataWrapper.reset(); +} + +void TextEngine::InsertView( TextView* pTextView ) +{ + mpViews->push_back( pTextView ); + pTextView->SetSelection( TextSelection() ); + + if ( !GetActiveView() ) + SetActiveView( pTextView ); +} + +void TextEngine::RemoveView( TextView* pTextView ) +{ + TextViews::iterator it = std::find( mpViews->begin(), mpViews->end(), pTextView ); + if( it != mpViews->end() ) + { + pTextView->HideCursor(); + mpViews->erase( it ); + if ( pTextView == GetActiveView() ) + SetActiveView( nullptr ); + } +} + +sal_uInt16 TextEngine::GetViewCount() const +{ + return mpViews->size(); +} + +TextView* TextEngine::GetView( sal_uInt16 nView ) const +{ + return (*mpViews)[ nView ]; +} + + +void TextEngine::SetActiveView( TextView* pTextView ) +{ + if ( pTextView != mpActiveView ) + { + if ( mpActiveView ) + mpActiveView->HideSelection(); + + mpActiveView = pTextView; + + if ( mpActiveView ) + mpActiveView->ShowSelection(); + } +} + +void TextEngine::SetFont( const vcl::Font& rFont ) +{ + if ( rFont == maFont ) + return; + + maFont = rFont; + // #i40221# As the font's color now defaults to transparent (since i35764) + // we have to choose a useful textcolor in this case. + // Otherwise maTextColor and maFont.GetColor() are both transparent... + if( rFont.GetColor() == COL_TRANSPARENT ) + maTextColor = COL_BLACK; + else + maTextColor = rFont.GetColor(); + + // Do not allow transparent fonts because of selection + // (otherwise delete the background in ImplPaint later differently) + maFont.SetTransparent( false ); + // Tell VCL not to use the font color, use text color from OutputDevice + maFont.SetColor( COL_TRANSPARENT ); + Color aFillColor( maFont.GetFillColor() ); + aFillColor.SetAlpha( 255 ); + maFont.SetFillColor( aFillColor ); + + maFont.SetAlignment( ALIGN_TOP ); + mpRefDev->SetFont( maFont ); + mnDefTab = mpRefDev->GetTextWidth(" "); + if ( !mnDefTab ) + mnDefTab = mpRefDev->GetTextWidth("XXXX"); + if ( !mnDefTab ) + mnDefTab = 1; + mnCharHeight = mpRefDev->GetTextHeight(); + + FormatFullDoc(); + UpdateViews(); + + for ( auto nView = mpViews->size(); nView; ) + { + TextView* pView = (*mpViews)[ --nView ]; + pView->GetWindow()->SetInputContext( InputContext( GetFont(), !pView->IsReadOnly() ? InputContextFlags::Text|InputContextFlags::ExtText : InputContextFlags::NONE ) ); + } + +} + +void TextEngine::SetMaxTextLen( sal_Int32 nLen ) +{ + mnMaxTextLen = nLen>=0 ? nLen : EDIT_NOLIMIT; +} + +void TextEngine::SetMaxTextWidth( tools::Long nMaxWidth ) +{ + if ( nMaxWidth>=0 && nMaxWidth != mnMaxTextWidth ) + { + mnMaxTextWidth = nMaxWidth; + FormatFullDoc(); + UpdateViews(); + } +} + +const sal_Unicode static_aLFText[] = { '\n', 0 }; +const sal_Unicode static_aCRText[] = { '\r', 0 }; +const sal_Unicode static_aCRLFText[] = { '\r', '\n', 0 }; + +static const sal_Unicode* static_getLineEndText( LineEnd aLineEnd ) +{ + const sal_Unicode* pRet = nullptr; + + switch( aLineEnd ) + { + case LINEEND_LF: + pRet = static_aLFText; + break; + case LINEEND_CR: + pRet = static_aCRText; + break; + case LINEEND_CRLF: + pRet = static_aCRLFText; + break; + } + return pRet; +} + +void TextEngine::ReplaceText(const TextSelection& rSel, const OUString& rText) +{ + ImpInsertText( rSel, rText ); +} + +OUString TextEngine::GetText( LineEnd aSeparator ) const +{ + return mpDoc->GetText( static_getLineEndText( aSeparator ) ); +} + +OUString TextEngine::GetTextLines( LineEnd aSeparator ) const +{ + OUStringBuffer aText; + const sal_uInt32 nParas = mpTEParaPortions->Count(); + const sal_Unicode* pSep = static_getLineEndText( aSeparator ); + for ( sal_uInt32 nP = 0; nP < nParas; ++nP ) + { + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nP ); + + const size_t nLines = pTEParaPortion->GetLines().size(); + for ( size_t nL = 0; nL < nLines; ++nL ) + { + TextLine& rLine = pTEParaPortion->GetLines()[nL]; + aText.append( pTEParaPortion->GetNode()->GetText().subView(rLine.GetStart(), rLine.GetEnd() - rLine.GetStart()) ); + if ( pSep && ( ( (nP+1) < nParas ) || ( (nL+1) < nLines ) ) ) + aText.append(pSep); + } + } + return aText.makeStringAndClear(); +} + +OUString TextEngine::GetText( sal_uInt32 nPara ) const +{ + return mpDoc->GetText( nPara ); +} + +sal_Int32 TextEngine::GetTextLen() const +{ + return mpDoc->GetTextLen( static_getLineEndText( LINEEND_LF ) ); +} + +sal_Int32 TextEngine::GetTextLen( const TextSelection& rSel ) const +{ + TextSelection aSel( rSel ); + aSel.Justify(); + ValidateSelection( aSel ); + return mpDoc->GetTextLen( static_getLineEndText( LINEEND_LF ), &aSel ); +} + +sal_Int32 TextEngine::GetTextLen( const sal_uInt32 nPara ) const +{ + return mpDoc->GetNodes()[ nPara ]->GetText().getLength(); +} + +void TextEngine::SetUpdateMode( bool bUpdate ) +{ + if ( bUpdate != mbUpdate ) + { + mbUpdate = bUpdate; + if ( mbUpdate ) + { + FormatAndUpdate( GetActiveView() ); + if ( GetActiveView() ) + GetActiveView()->ShowCursor(); + } + } +} + +bool TextEngine::DoesKeyChangeText( const KeyEvent& rKeyEvent ) +{ + bool bDoesChange = false; + + KeyFuncType eFunc = rKeyEvent.GetKeyCode().GetFunction(); + if ( eFunc != KeyFuncType::DONTKNOW ) + { + switch ( eFunc ) + { + case KeyFuncType::UNDO: + case KeyFuncType::REDO: + case KeyFuncType::CUT: + case KeyFuncType::PASTE: + bDoesChange = true; + break; + default: + // might get handled below + eFunc = KeyFuncType::DONTKNOW; + } + } + if ( eFunc == KeyFuncType::DONTKNOW ) + { + switch ( rKeyEvent.GetKeyCode().GetCode() ) + { + case KEY_DELETE: + case KEY_BACKSPACE: + if ( !rKeyEvent.GetKeyCode().IsMod2() ) + bDoesChange = true; + break; + case KEY_RETURN: + case KEY_TAB: + if ( !rKeyEvent.GetKeyCode().IsMod1() && !rKeyEvent.GetKeyCode().IsMod2() ) + bDoesChange = true; + break; + default: + bDoesChange = TextEngine::IsSimpleCharInput( rKeyEvent ); + } + } + return bDoesChange; +} + +bool TextEngine::IsSimpleCharInput( const KeyEvent& rKeyEvent ) +{ + return rKeyEvent.GetCharCode() >= 32 && rKeyEvent.GetCharCode() != 127 && + KEY_MOD1 != (rKeyEvent.GetKeyCode().GetModifier() & ~KEY_SHIFT) && // (ssa) #i45714#: + KEY_MOD2 != (rKeyEvent.GetKeyCode().GetModifier() & ~KEY_SHIFT); // check for Ctrl and Alt separately +} + +void TextEngine::ImpInitDoc() +{ + if ( mpDoc ) + mpDoc->Clear(); + else + mpDoc.reset( new TextDoc ); + + mpTEParaPortions.reset(new TEParaPortions); + + std::unique_ptr<TextNode> pNode(new TextNode( OUString() )); + mpDoc->GetNodes().insert( mpDoc->GetNodes().begin(), std::move(pNode) ); + + TEParaPortion* pIniPortion = new TEParaPortion( mpDoc->GetNodes().begin()->get() ); + mpTEParaPortions->Insert( pIniPortion, 0 ); + + mbFormatted = false; + + ImpParagraphRemoved( TEXT_PARA_ALL ); + ImpParagraphInserted( 0 ); +} + +OUString TextEngine::GetText( const TextSelection& rSel, LineEnd aSeparator ) const +{ + if ( !rSel.HasRange() ) + return OUString(); + + TextSelection aSel( rSel ); + aSel.Justify(); + + OUStringBuffer aText; + const sal_uInt32 nStartPara = aSel.GetStart().GetPara(); + const sal_uInt32 nEndPara = aSel.GetEnd().GetPara(); + const sal_Unicode* pSep = static_getLineEndText( aSeparator ); + for ( sal_uInt32 nNode = aSel.GetStart().GetPara(); nNode <= nEndPara; ++nNode ) + { + TextNode* pNode = mpDoc->GetNodes()[ nNode ].get(); + + sal_Int32 nStartPos = 0; + sal_Int32 nEndPos = pNode->GetText().getLength(); + if ( nNode == nStartPara ) + nStartPos = aSel.GetStart().GetIndex(); + if ( nNode == nEndPara ) // may also be == nStart! + nEndPos = aSel.GetEnd().GetIndex(); + + aText.append(pNode->GetText().subView(nStartPos, nEndPos-nStartPos)); + if ( nNode < nEndPara ) + aText.append(pSep); + } + return aText.makeStringAndClear(); +} + +void TextEngine::ImpRemoveText() +{ + ImpInitDoc(); + + const TextSelection aEmptySel; + for (TextView* pView : *mpViews) + { + pView->ImpSetSelection( aEmptySel ); + } + ResetUndo(); +} + +void TextEngine::SetText( const OUString& rText ) +{ + ImpRemoveText(); + + const bool bUndoCurrentlyEnabled = IsUndoEnabled(); + // the manually inserted text cannot be reversed by the user + EnableUndo( false ); + + const TextSelection aEmptySel; + + TextPaM aPaM; + if ( !rText.isEmpty() ) + aPaM = ImpInsertText( aEmptySel, rText ); + + for (TextView* pView : *mpViews) + { + pView->ImpSetSelection( aEmptySel ); + + // if no text, then no Format&Update => the text remains + if ( rText.isEmpty() && GetUpdateMode() ) + pView->Invalidate(); + } + + if( rText.isEmpty() ) // otherwise needs invalidation later; !bFormatted is sufficient + mnCurTextHeight = 0; + + FormatAndUpdate(); + + EnableUndo( bUndoCurrentlyEnabled ); + SAL_WARN_IF( HasUndoManager() && GetUndoManager().GetUndoActionCount(), "vcl", "SetText: Undo!" ); +} + +void TextEngine::CursorMoved( sal_uInt32 nNode ) +{ + // delete empty attribute; but only if paragraph is not empty! + TextNode* pNode = mpDoc->GetNodes()[ nNode ].get(); + if ( pNode && pNode->GetCharAttribs().HasEmptyAttribs() && !pNode->GetText().isEmpty() ) + pNode->GetCharAttribs().DeleteEmptyAttribs(); +} + +void TextEngine::ImpRemoveChars( const TextPaM& rPaM, sal_Int32 nChars ) +{ + SAL_WARN_IF( !nChars, "vcl", "ImpRemoveChars: 0 Chars?!" ); + if ( IsUndoEnabled() && !IsInUndo() ) + { + // attributes have to be saved for UNDO before RemoveChars! + TextNode* pNode = mpDoc->GetNodes()[ rPaM.GetPara() ].get(); + OUString aStr( pNode->GetText().copy( rPaM.GetIndex(), nChars ) ); + + // check if attributes are being deleted or changed + const sal_Int32 nStart = rPaM.GetIndex(); + const sal_Int32 nEnd = nStart + nChars; + for ( sal_uInt16 nAttr = pNode->GetCharAttribs().Count(); nAttr; ) + { + TextCharAttrib& rAttr = pNode->GetCharAttribs().GetAttrib( --nAttr ); + if ( ( rAttr.GetEnd() >= nStart ) && ( rAttr.GetStart() < nEnd ) ) + { + break; // for + } + } + InsertUndo( std::make_unique<TextUndoRemoveChars>( this, rPaM, aStr ) ); + } + + mpDoc->RemoveChars( rPaM, nChars ); + ImpCharsRemoved( rPaM.GetPara(), rPaM.GetIndex(), nChars ); +} + +TextPaM TextEngine::ImpConnectParagraphs( sal_uInt32 nLeft, sal_uInt32 nRight ) +{ + SAL_WARN_IF( nLeft == nRight, "vcl", "ImpConnectParagraphs: connect the very same paragraph ?" ); + + TextNode* pLeft = mpDoc->GetNodes()[ nLeft ].get(); + TextNode* pRight = mpDoc->GetNodes()[ nRight ].get(); + + if ( IsUndoEnabled() && !IsInUndo() ) + InsertUndo( std::make_unique<TextUndoConnectParas>( this, nLeft, pLeft->GetText().getLength() ) ); + + // first lookup Portions, as pRight is gone after ConnectParagraphs + TEParaPortion* pLeftPortion = mpTEParaPortions->GetObject( nLeft ); + TEParaPortion* pRightPortion = mpTEParaPortions->GetObject( nRight ); + SAL_WARN_IF( !pLeft || !pLeftPortion, "vcl", "ImpConnectParagraphs(1): Hidden Portion" ); + SAL_WARN_IF( !pRight || !pRightPortion, "vcl", "ImpConnectParagraphs(2): Hidden Portion" ); + + TextPaM aPaM = mpDoc->ConnectParagraphs( pLeft, pRight ); + ImpParagraphRemoved( nRight ); + + pLeftPortion->MarkSelectionInvalid( aPaM.GetIndex() ); + + mpTEParaPortions->Remove( nRight ); + // the right Node is deleted by EditDoc::ConnectParagraphs() + + return aPaM; +} + +TextPaM TextEngine::ImpDeleteText( const TextSelection& rSel ) +{ + if ( !rSel.HasRange() ) + return rSel.GetStart(); + + TextSelection aSel( rSel ); + aSel.Justify(); + TextPaM aStartPaM( aSel.GetStart() ); + TextPaM aEndPaM( aSel.GetEnd() ); + + CursorMoved( aStartPaM.GetPara() ); // so that newly-adjusted attributes vanish + CursorMoved( aEndPaM.GetPara() ); // so that newly-adjusted attributes vanish + + SAL_WARN_IF( !mpDoc->IsValidPaM( aStartPaM ), "vcl", "ImpDeleteText(1): bad Index" ); + SAL_WARN_IF( !mpDoc->IsValidPaM( aEndPaM ), "vcl", "ImpDeleteText(2): bad Index" ); + + const sal_uInt32 nStartNode = aStartPaM.GetPara(); + sal_uInt32 nEndNode = aEndPaM.GetPara(); + + // remove all Nodes inbetween + for ( sal_uInt32 z = nStartNode+1; z < nEndNode; ++z ) + { + // always nStartNode+1, because of Remove()! + ImpRemoveParagraph( nStartNode+1 ); + } + + if ( nStartNode != nEndNode ) + { + // the remainder of StartNodes... + TextNode* pLeft = mpDoc->GetNodes()[ nStartNode ].get(); + sal_Int32 nChars = pLeft->GetText().getLength() - aStartPaM.GetIndex(); + if ( nChars ) + { + ImpRemoveChars( aStartPaM, nChars ); + TEParaPortion* pPortion = mpTEParaPortions->GetObject( nStartNode ); + SAL_WARN_IF( !pPortion, "vcl", "ImpDeleteText(3): bad Index" ); + pPortion->MarkSelectionInvalid( aStartPaM.GetIndex() ); + } + + // the beginning of EndNodes... + nEndNode = nStartNode+1; // the other paragraphs were deleted + nChars = aEndPaM.GetIndex(); + if ( nChars ) + { + aEndPaM.GetPara() = nEndNode; + aEndPaM.GetIndex() = 0; + ImpRemoveChars( aEndPaM, nChars ); + TEParaPortion* pPortion = mpTEParaPortions->GetObject( nEndNode ); + SAL_WARN_IF( !pPortion, "vcl", "ImpDeleteText(4): bad Index" ); + pPortion->MarkSelectionInvalid( 0 ); + } + + // connect... + aStartPaM = ImpConnectParagraphs( nStartNode, nEndNode ); + } + else + { + const sal_Int32 nChars = aEndPaM.GetIndex() - aStartPaM.GetIndex(); + ImpRemoveChars( aStartPaM, nChars ); + TEParaPortion* pPortion = mpTEParaPortions->GetObject( nStartNode ); + SAL_WARN_IF( !pPortion, "vcl", "ImpDeleteText(5): bad Index" ); + pPortion->MarkInvalid( aEndPaM.GetIndex(), aStartPaM.GetIndex() - aEndPaM.GetIndex() ); + } + +// UpdateSelections(); + TextModified(); + return aStartPaM; +} + +void TextEngine::ImpRemoveParagraph( sal_uInt32 nPara ) +{ + std::unique_ptr<TextNode> pNode = std::move(mpDoc->GetNodes()[ nPara ]); + + // the Node is handled by Undo and is deleted if appropriate + mpDoc->GetNodes().erase( mpDoc->GetNodes().begin() + nPara ); + if ( IsUndoEnabled() && !IsInUndo() ) + InsertUndo( std::make_unique<TextUndoDelPara>( this, pNode.release(), nPara ) ); + + mpTEParaPortions->Remove( nPara ); + + ImpParagraphRemoved( nPara ); +} + +uno::Reference < i18n::XExtendedInputSequenceChecker > const & TextEngine::GetInputSequenceChecker() +{ + if ( !mxISC.is() ) + { + mxISC = i18n::InputSequenceChecker::create( ::comphelper::getProcessComponentContext() ); + } + return mxISC; +} + +bool TextEngine::IsInputSequenceCheckingRequired( sal_Unicode c, const TextSelection& rCurSel ) const +{ + SvtCTLOptions aCTLOptions; + + // get the index that really is first + const sal_Int32 nFirstPos = std::min(rCurSel.GetStart().GetIndex(), rCurSel.GetEnd().GetIndex()); + + bool bIsSequenceChecking = + aCTLOptions.IsCTLFontEnabled() && + aCTLOptions.IsCTLSequenceChecking() && + nFirstPos != 0; /* first char needs not to be checked */ + + if (bIsSequenceChecking) + { + uno::Reference< i18n::XBreakIterator > xBI = const_cast<TextEngine *>(this)->GetBreakIterator(); + bIsSequenceChecking = xBI.is() && i18n::ScriptType::COMPLEX == xBI->getScriptType( OUString( c ), 0 ); + } + + return bIsSequenceChecking; +} + +TextPaM TextEngine::ImpInsertText( const TextSelection& rCurSel, sal_Unicode c, bool bOverwrite ) +{ + return ImpInsertText( c, rCurSel, bOverwrite ); +} + +TextPaM TextEngine::ImpInsertText( sal_Unicode c, const TextSelection& rCurSel, bool bOverwrite, bool bIsUserInput ) +{ + SAL_WARN_IF( c == '\n', "vcl", "InsertText: NewLine!" ); + SAL_WARN_IF( c == '\r', "vcl", "InsertText: NewLine!" ); + + TextPaM aPaM( rCurSel.GetStart() ); + TextNode* pNode = mpDoc->GetNodes()[ aPaM.GetPara() ].get(); + + bool bDoOverwrite = bOverwrite && ( aPaM.GetIndex() < pNode->GetText().getLength() ); + + bool bUndoAction = rCurSel.HasRange() || bDoOverwrite; + + if ( bUndoAction ) + UndoActionStart(); + + if ( rCurSel.HasRange() ) + { + aPaM = ImpDeleteText( rCurSel ); + } + else if ( bDoOverwrite ) + { + // if selection, then don't overwrite a character + TextSelection aTmpSel( aPaM ); + ++aTmpSel.GetEnd().GetIndex(); + ImpDeleteText( aTmpSel ); + } + + if (bIsUserInput && IsInputSequenceCheckingRequired( c, rCurSel )) + { + uno::Reference < i18n::XExtendedInputSequenceChecker > xISC = GetInputSequenceChecker(); + SvtCTLOptions aCTLOptions; + + if (xISC.is()) + { + sal_Int32 nTmpPos = aPaM.GetIndex(); + sal_Int16 nCheckMode = aCTLOptions.IsCTLSequenceCheckingRestricted() ? + i18n::InputSequenceCheckMode::STRICT : i18n::InputSequenceCheckMode::BASIC; + + // the text that needs to be checked is only the one + // before the current cursor position + OUString aOldText( mpDoc->GetText( aPaM.GetPara() ).copy(0, nTmpPos) ); + if (aCTLOptions.IsCTLSequenceCheckingTypeAndReplace()) + { + OUString aNewText( aOldText ); + xISC->correctInputSequence( aNewText, nTmpPos - 1, c, nCheckMode ); + + // find position of first character that has changed + const sal_Int32 nOldLen = aOldText.getLength(); + const sal_Int32 nNewLen = aNewText.getLength(); + const sal_Unicode *pOldTxt = aOldText.getStr(); + const sal_Unicode *pNewTxt = aNewText.getStr(); + sal_Int32 nChgPos = 0; + while ( nChgPos < nOldLen && nChgPos < nNewLen && + pOldTxt[nChgPos] == pNewTxt[nChgPos] ) + ++nChgPos; + + OUString aChgText( aNewText.copy( nChgPos ) ); + + // select text from first pos to be changed to current pos + TextSelection aSel( TextPaM( aPaM.GetPara(), nChgPos ), aPaM ); + + if (!aChgText.isEmpty()) + // ImpInsertText implicitly handles undo... + return ImpInsertText( aSel, aChgText ); + else + return aPaM; + } + else + { + // should the character be ignored (i.e. not get inserted) ? + if (!xISC->checkInputSequence( aOldText, nTmpPos - 1, c, nCheckMode )) + return aPaM; // nothing to be done -> no need for undo + } + } + + // at this point now we will insert the character 'normally' some lines below... + } + + if ( IsUndoEnabled() && !IsInUndo() ) + { + std::unique_ptr<TextUndoInsertChars> pNewUndo(new TextUndoInsertChars( this, aPaM, OUString(c) )); + bool bTryMerge = !bDoOverwrite && ( c != ' ' ); + InsertUndo( std::move(pNewUndo), bTryMerge ); + } + + TEParaPortion* pPortion = mpTEParaPortions->GetObject( aPaM.GetPara() ); + pPortion->MarkInvalid( aPaM.GetIndex(), 1 ); + if ( c == '\t' ) + pPortion->SetNotSimpleInvalid(); + aPaM = mpDoc->InsertText( aPaM, c ); + ImpCharsInserted( aPaM.GetPara(), aPaM.GetIndex()-1, 1 ); + + TextModified(); + + if ( bUndoAction ) + UndoActionEnd(); + + return aPaM; +} + +TextPaM TextEngine::ImpInsertText( const TextSelection& rCurSel, const OUString& rStr ) +{ + UndoActionStart(); + + TextPaM aPaM; + + if ( rCurSel.HasRange() ) + aPaM = ImpDeleteText( rCurSel ); + else + aPaM = rCurSel.GetEnd(); + + OUString aText(convertLineEnd(rStr, LINEEND_LF)); + + sal_Int32 nStart = 0; + while ( nStart < aText.getLength() ) + { + sal_Int32 nEnd = aText.indexOf( LINE_SEP, nStart ); + if (nEnd == -1) + nEnd = aText.getLength(); // do not dereference! + + // Start == End => empty line + if ( nEnd > nStart ) + { + OUString aLine(aText.copy(nStart, nEnd-nStart)); + if ( IsUndoEnabled() && !IsInUndo() ) + InsertUndo( std::make_unique<TextUndoInsertChars>( this, aPaM, aLine ) ); + + TEParaPortion* pPortion = mpTEParaPortions->GetObject( aPaM.GetPara() ); + pPortion->MarkInvalid( aPaM.GetIndex(), aLine.getLength() ); + if (aLine.indexOf( '\t' ) != -1) + pPortion->SetNotSimpleInvalid(); + + aPaM = mpDoc->InsertText( aPaM, aLine ); + ImpCharsInserted( aPaM.GetPara(), aPaM.GetIndex()-aLine.getLength(), aLine.getLength() ); + + } + if ( nEnd < aText.getLength() ) + aPaM = ImpInsertParaBreak( aPaM ); + + if ( nEnd == aText.getLength() ) // #108611# prevent overflow in "nStart = nEnd+1" calculation + break; + + nStart = nEnd+1; + } + + UndoActionEnd(); + + TextModified(); + return aPaM; +} + +TextPaM TextEngine::ImpInsertParaBreak( const TextSelection& rCurSel ) +{ + TextPaM aPaM; + if ( rCurSel.HasRange() ) + aPaM = ImpDeleteText( rCurSel ); + else + aPaM = rCurSel.GetEnd(); + + return ImpInsertParaBreak( aPaM ); +} + +TextPaM TextEngine::ImpInsertParaBreak( const TextPaM& rPaM ) +{ + if ( IsUndoEnabled() && !IsInUndo() ) + InsertUndo( std::make_unique<TextUndoSplitPara>( this, rPaM.GetPara(), rPaM.GetIndex() ) ); + + TextNode* pNode = mpDoc->GetNodes()[ rPaM.GetPara() ].get(); + bool bFirstParaContentChanged = rPaM.GetIndex() < pNode->GetText().getLength(); + + TextPaM aPaM( mpDoc->InsertParaBreak( rPaM ) ); + + TEParaPortion* pPortion = mpTEParaPortions->GetObject( rPaM.GetPara() ); + SAL_WARN_IF( !pPortion, "vcl", "ImpInsertParaBreak: Hidden Portion" ); + pPortion->MarkInvalid( rPaM.GetIndex(), 0 ); + + TextNode* pNewNode = mpDoc->GetNodes()[ aPaM.GetPara() ].get(); + TEParaPortion* pNewPortion = new TEParaPortion( pNewNode ); + mpTEParaPortions->Insert( pNewPortion, aPaM.GetPara() ); + ImpParagraphInserted( aPaM.GetPara() ); + + CursorMoved( rPaM.GetPara() ); // if empty attribute created + TextModified(); + + if ( bFirstParaContentChanged ) + Broadcast( TextHint( SfxHintId::TextParaContentChanged, rPaM.GetPara() ) ); + + return aPaM; +} + +tools::Rectangle TextEngine::PaMtoEditCursor( const TextPaM& rPaM, bool bSpecial ) +{ + SAL_WARN_IF( !GetUpdateMode(), "vcl", "PaMtoEditCursor: GetUpdateMode()" ); + + tools::Rectangle aEditCursor; + tools::Long nY = 0; + + if ( !mbHasMultiLineParas ) + { + nY = rPaM.GetPara() * mnCharHeight; + } + else + { + for ( sal_uInt32 nPortion = 0; nPortion < rPaM.GetPara(); ++nPortion ) + { + TEParaPortion* pPortion = mpTEParaPortions->GetObject(nPortion); + nY += pPortion->GetLines().size() * mnCharHeight; + } + } + + aEditCursor = GetEditCursor( rPaM, bSpecial ); + aEditCursor.AdjustTop(nY ); + aEditCursor.AdjustBottom(nY ); + return aEditCursor; +} + +tools::Rectangle TextEngine::GetEditCursor( const TextPaM& rPaM, bool bSpecial, bool bPreferPortionStart ) +{ + if ( !IsFormatted() && !IsFormatting() ) + FormatAndUpdate(); + + TEParaPortion* pPortion = mpTEParaPortions->GetObject( rPaM.GetPara() ); + //TextNode* pNode = mpDoc->GetNodes().GetObject( rPaM.GetPara() ); + + /* + bSpecial: If behind the last character of a made up line, stay at the + end of the line, not at the start of the next line. + Purpose: - really END = > behind the last character + - to selection... + + */ + + tools::Long nY = 0; + sal_Int32 nCurIndex = 0; + TextLine* pLine = nullptr; + for (TextLine & rTmpLine : pPortion->GetLines()) + { + if ( ( rTmpLine.GetStart() == rPaM.GetIndex() ) || ( rTmpLine.IsIn( rPaM.GetIndex(), bSpecial ) ) ) + { + pLine = &rTmpLine; + break; + } + + nCurIndex = nCurIndex + rTmpLine.GetLen(); + nY += mnCharHeight; + } + if ( !pLine ) + { + // Cursor at end of paragraph + SAL_WARN_IF( rPaM.GetIndex() != nCurIndex, "vcl", "GetEditCursor: Bad Index!" ); + + pLine = & ( pPortion->GetLines().back() ); + nY -= mnCharHeight; + } + + tools::Rectangle aEditCursor; + + aEditCursor.SetTop( nY ); + nY += mnCharHeight; + aEditCursor.SetBottom( nY-1 ); + + // search within the line + tools::Long nX = ImpGetXPos( rPaM.GetPara(), pLine, rPaM.GetIndex(), bPreferPortionStart ); + aEditCursor.SetLeft(nX); + aEditCursor.SetRight(nX); + return aEditCursor; +} + +tools::Long TextEngine::ImpGetXPos( sal_uInt32 nPara, TextLine* pLine, sal_Int32 nIndex, bool bPreferPortionStart ) +{ + SAL_WARN_IF( ( nIndex < pLine->GetStart() ) || ( nIndex > pLine->GetEnd() ) , "vcl", "ImpGetXPos: Bad parameters!" ); + + bool bDoPreferPortionStart = bPreferPortionStart; + // Assure that the portion belongs to this line + if ( nIndex == pLine->GetStart() ) + bDoPreferPortionStart = true; + else if ( nIndex == pLine->GetEnd() ) + bDoPreferPortionStart = false; + + TEParaPortion* pParaPortion = mpTEParaPortions->GetObject( nPara ); + + sal_Int32 nTextPortionStart = 0; + std::size_t nTextPortion = pParaPortion->GetTextPortions().FindPortion( nIndex, nTextPortionStart, bDoPreferPortionStart ); + + SAL_WARN_IF( ( nTextPortion < pLine->GetStartPortion() ) || ( nTextPortion > pLine->GetEndPortion() ), "vcl", "GetXPos: Portion not in current line!" ); + + TETextPortion& rPortion = pParaPortion->GetTextPortions()[ nTextPortion ]; + + tools::Long nX = ImpGetPortionXOffset( nPara, pLine, nTextPortion ); + + tools::Long nPortionTextWidth = rPortion.GetWidth(); + + if ( nTextPortionStart != nIndex ) + { + // Search within portion... + if ( nIndex == ( nTextPortionStart + rPortion.GetLen() ) ) + { + // End of Portion + if ( ( rPortion.GetKind() == PORTIONKIND_TAB ) || + ( !IsRightToLeft() && !rPortion.IsRightToLeft() ) || + ( IsRightToLeft() && rPortion.IsRightToLeft() ) ) + { + nX += nPortionTextWidth; + if ( ( rPortion.GetKind() == PORTIONKIND_TAB ) && ( (nTextPortion+1) < pParaPortion->GetTextPortions().size() ) ) + { + TETextPortion& rNextPortion = pParaPortion->GetTextPortions()[ nTextPortion+1 ]; + if (rNextPortion.GetKind() != PORTIONKIND_TAB && IsRightToLeft() != rNextPortion.IsRightToLeft()) + { + // End of the tab portion, use start of next for cursor pos + SAL_WARN_IF( bPreferPortionStart, "vcl", "ImpGetXPos: How can we get here!" ); + nX = ImpGetXPos( nPara, pLine, nIndex, true ); + } + + } + } + } + else if ( rPortion.GetKind() == PORTIONKIND_TEXT ) + { + SAL_WARN_IF( nIndex == pLine->GetStart(), "vcl", "ImpGetXPos: Strange behavior" ); + + tools::Long nPosInPortion = CalcTextWidth( nPara, nTextPortionStart, nIndex-nTextPortionStart ); + + if (IsRightToLeft() == rPortion.IsRightToLeft()) + { + nX += nPosInPortion; + } + else + { + nX += nPortionTextWidth - nPosInPortion; + } + } + } + else // if ( nIndex == pLine->GetStart() ) + { + if (rPortion.GetKind() != PORTIONKIND_TAB && IsRightToLeft() != rPortion.IsRightToLeft()) + { + nX += nPortionTextWidth; + } + } + + return nX; +} + +const TextAttrib* TextEngine::FindAttrib( const TextPaM& rPaM, sal_uInt16 nWhich ) const +{ + const TextAttrib* pAttr = nullptr; + const TextCharAttrib* pCharAttr = FindCharAttrib( rPaM, nWhich ); + if ( pCharAttr ) + pAttr = &pCharAttr->GetAttr(); + return pAttr; +} + +const TextCharAttrib* TextEngine::FindCharAttrib( const TextPaM& rPaM, sal_uInt16 nWhich ) const +{ + const TextCharAttrib* pAttr = nullptr; + TextNode* pNode = mpDoc->GetNodes()[ rPaM.GetPara() ].get(); + if (pNode && (rPaM.GetIndex() <= pNode->GetText().getLength())) + pAttr = pNode->GetCharAttribs().FindAttrib( nWhich, rPaM.GetIndex() ); + return pAttr; +} + +TextPaM TextEngine::GetPaM( const Point& rDocPos ) +{ + SAL_WARN_IF( !GetUpdateMode(), "vcl", "GetPaM: GetUpdateMode()" ); + + tools::Long nY = 0; + for ( sal_uInt32 nPortion = 0; nPortion < mpTEParaPortions->Count(); ++nPortion ) + { + TEParaPortion* pPortion = mpTEParaPortions->GetObject( nPortion ); + tools::Long nTmpHeight = pPortion->GetLines().size() * mnCharHeight; + nY += nTmpHeight; + if ( nY > rDocPos.Y() ) + { + nY -= nTmpHeight; + Point aPosInPara( rDocPos ); + aPosInPara.AdjustY( -nY ); + + TextPaM aPaM( nPortion, 0 ); + aPaM.GetIndex() = ImpFindIndex( nPortion, aPosInPara ); + return aPaM; + } + } + + // not found - go to last visible + const sal_uInt32 nLastNode = static_cast<sal_uInt32>(mpDoc->GetNodes().size() - 1); + TextNode* pLast = mpDoc->GetNodes()[ nLastNode ].get(); + return TextPaM( nLastNode, pLast->GetText().getLength() ); +} + +sal_Int32 TextEngine::ImpFindIndex( sal_uInt32 nPortion, const Point& rPosInPara ) +{ + SAL_WARN_IF( !IsFormatted(), "vcl", "GetPaM: Not formatted" ); + TEParaPortion* pPortion = mpTEParaPortions->GetObject( nPortion ); + + sal_Int32 nCurIndex = 0; + + tools::Long nY = 0; + TextLine* pLine = nullptr; + std::vector<TextLine>::size_type nLine; + for ( nLine = 0; nLine < pPortion->GetLines().size(); nLine++ ) + { + TextLine& rmpLine = pPortion->GetLines()[ nLine ]; + nY += mnCharHeight; + if ( nY > rPosInPara.Y() ) // that's it + { + pLine = &rmpLine; + break; // correct Y-Position not needed + } + } + + assert(pLine && "ImpFindIndex: pLine ?"); + + nCurIndex = GetCharPos( nPortion, nLine, rPosInPara.X() ); + + if ( nCurIndex && ( nCurIndex == pLine->GetEnd() ) && + ( pLine != &( pPortion->GetLines().back() ) ) ) + { + uno::Reference < i18n::XBreakIterator > xBI = GetBreakIterator(); + sal_Int32 nCount = 1; + nCurIndex = xBI->previousCharacters( pPortion->GetNode()->GetText(), nCurIndex, GetLocale(), i18n::CharacterIteratorMode::SKIPCELL, nCount, nCount ); + } + return nCurIndex; +} + +sal_Int32 TextEngine::GetCharPos( sal_uInt32 nPortion, std::vector<TextLine>::size_type nLine, tools::Long nXPos ) +{ + + TEParaPortion* pPortion = mpTEParaPortions->GetObject( nPortion ); + TextLine& rLine = pPortion->GetLines()[ nLine ]; + + sal_Int32 nCurIndex = rLine.GetStart(); + + tools::Long nTmpX = rLine.GetStartX(); + if ( nXPos <= nTmpX ) + return nCurIndex; + + for ( std::size_t i = rLine.GetStartPortion(); i <= rLine.GetEndPortion(); i++ ) + { + TETextPortion& rTextPortion = pPortion->GetTextPortions()[ i ]; + nTmpX += rTextPortion.GetWidth(); + + if ( nTmpX > nXPos ) + { + if( rTextPortion.GetLen() > 1 ) + { + nTmpX -= rTextPortion.GetWidth(); // position before Portion + // TODO: Optimize: no GetTextBreak if fixed-width Font + vcl::Font aFont; + SeekCursor( nPortion, nCurIndex+1, aFont, nullptr ); + mpRefDev->SetFont( aFont); + tools::Long nPosInPortion = nXPos-nTmpX; + if ( IsRightToLeft() != rTextPortion.IsRightToLeft() ) + nPosInPortion = rTextPortion.GetWidth() - nPosInPortion; + nCurIndex = mpRefDev->GetTextBreak( pPortion->GetNode()->GetText(), nPosInPortion, nCurIndex ); + // MT: GetTextBreak should assure that we are not within a CTL cell... + } + return nCurIndex; + } + nCurIndex += rTextPortion.GetLen(); + } + return nCurIndex; +} + +tools::Long TextEngine::GetTextHeight() const +{ + SAL_WARN_IF( !GetUpdateMode(), "vcl", "GetTextHeight: GetUpdateMode()" ); + + if ( !IsFormatted() && !IsFormatting() ) + const_cast<TextEngine*>(this)->FormatAndUpdate(); + + return mnCurTextHeight; +} + +tools::Long TextEngine::GetTextHeight( sal_uInt32 nParagraph ) const +{ + SAL_WARN_IF( !GetUpdateMode(), "vcl", "GetTextHeight: GetUpdateMode()" ); + + if ( !IsFormatted() && !IsFormatting() ) + const_cast<TextEngine*>(this)->FormatAndUpdate(); + + return CalcParaHeight( nParagraph ); +} + +tools::Long TextEngine::CalcTextWidth( sal_uInt32 nPara ) +{ + tools::Long nParaWidth = 0; + TEParaPortion* pPortion = mpTEParaPortions->GetObject( nPara ); + for ( auto nLine = pPortion->GetLines().size(); nLine; ) + { + tools::Long nLineWidth = 0; + TextLine& rLine = pPortion->GetLines()[ --nLine ]; + for ( std::size_t nTP = rLine.GetStartPortion(); nTP <= rLine.GetEndPortion(); nTP++ ) + { + TETextPortion& rTextPortion = pPortion->GetTextPortions()[ nTP ]; + nLineWidth += rTextPortion.GetWidth(); + } + if ( nLineWidth > nParaWidth ) + nParaWidth = nLineWidth; + } + return nParaWidth; +} + +tools::Long TextEngine::CalcTextWidth() +{ + if ( !IsFormatted() && !IsFormatting() ) + FormatAndUpdate(); + + if ( mnCurTextWidth < 0 ) + { + mnCurTextWidth = 0; + for ( sal_uInt32 nPara = mpTEParaPortions->Count(); nPara; ) + { + const tools::Long nParaWidth = CalcTextWidth( --nPara ); + if ( nParaWidth > mnCurTextWidth ) + mnCurTextWidth = nParaWidth; + } + } + return mnCurTextWidth+1;// wider by 1, as CreateLines breaks at >= +} + +tools::Long TextEngine::CalcTextHeight() const +{ + SAL_WARN_IF( !GetUpdateMode(), "vcl", "CalcTextHeight: GetUpdateMode()" ); + + tools::Long nY = 0; + for ( auto nPortion = mpTEParaPortions->Count(); nPortion; ) + nY += CalcParaHeight( --nPortion ); + return nY; +} + +tools::Long TextEngine::CalcTextWidth( sal_uInt32 nPara, sal_Int32 nPortionStart, sal_Int32 nLen ) +{ +#ifdef DBG_UTIL + // within the text there must not be a Portion change (attribute/tab)! + sal_Int32 nTabPos = mpDoc->GetNodes()[ nPara ]->GetText().indexOf( '\t', nPortionStart ); + SAL_WARN_IF( nTabPos != -1 && nTabPos < (nPortionStart+nLen), "vcl", "CalcTextWidth: Tab!" ); +#endif + + vcl::Font aFont; + SeekCursor( nPara, nPortionStart+1, aFont, nullptr ); + mpRefDev->SetFont( aFont ); + TextNode* pNode = mpDoc->GetNodes()[ nPara ].get(); + tools::Long nWidth = mpRefDev->GetTextWidth( pNode->GetText(), nPortionStart, nLen ); + return nWidth; +} + +void TextEngine::GetTextPortionRange(const TextPaM& rPaM, sal_Int32& nStart, sal_Int32& nEnd) +{ + nStart = 0; + nEnd = 0; + TEParaPortion* pParaPortion = mpTEParaPortions->GetObject( rPaM.GetPara() ); + for ( std::size_t i = 0; i < pParaPortion->GetTextPortions().size(); ++i ) + { + TETextPortion& rTextPortion = pParaPortion->GetTextPortions()[ i ]; + if (nStart + rTextPortion.GetLen() > rPaM.GetIndex()) + { + nEnd = nStart + rTextPortion.GetLen(); + return; + } + else + { + nStart += rTextPortion.GetLen(); + } + } +} + +sal_uInt16 TextEngine::GetLineCount( sal_uInt32 nParagraph ) const +{ + SAL_WARN_IF( nParagraph >= mpTEParaPortions->Count(), "vcl", "GetLineCount: Out of range" ); + + TEParaPortion* pPPortion = mpTEParaPortions->GetObject( nParagraph ); + if ( pPPortion ) + return pPPortion->GetLines().size(); + + return 0; +} + +sal_Int32 TextEngine::GetLineLen( sal_uInt32 nParagraph, sal_uInt16 nLine ) const +{ + SAL_WARN_IF( nParagraph >= mpTEParaPortions->Count(), "vcl", "GetLineCount: Out of range" ); + + TEParaPortion* pPPortion = mpTEParaPortions->GetObject( nParagraph ); + if ( pPPortion && ( nLine < pPPortion->GetLines().size() ) ) + { + return pPPortion->GetLines()[ nLine ].GetLen(); + } + + return 0; +} + +tools::Long TextEngine::CalcParaHeight( sal_uInt32 nParagraph ) const +{ + tools::Long nHeight = 0; + + TEParaPortion* pPPortion = mpTEParaPortions->GetObject( nParagraph ); + SAL_WARN_IF( !pPPortion, "vcl", "GetParaHeight: paragraph not found" ); + if ( pPPortion ) + nHeight = pPPortion->GetLines().size() * mnCharHeight; + + return nHeight; +} + +Range TextEngine::GetInvalidYOffsets( sal_uInt32 nPortion ) +{ + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPortion ); + sal_uInt16 nLines = pTEParaPortion->GetLines().size(); + sal_uInt16 nLastInvalid, nFirstInvalid = 0; + sal_uInt16 nLine; + for ( nLine = 0; nLine < nLines; nLine++ ) + { + TextLine& rL = pTEParaPortion->GetLines()[ nLine ]; + if ( rL.IsInvalid() ) + { + nFirstInvalid = nLine; + break; + } + } + + for ( nLastInvalid = nFirstInvalid; nLastInvalid < nLines; nLastInvalid++ ) + { + TextLine& rL = pTEParaPortion->GetLines()[ nLine ]; + if ( rL.IsValid() ) + break; + } + + if ( nLastInvalid >= nLines ) + nLastInvalid = nLines-1; + + return Range( nFirstInvalid*mnCharHeight, ((nLastInvalid+1)*mnCharHeight)-1 ); +} + +sal_uInt32 TextEngine::GetParagraphCount() const +{ + return static_cast<sal_uInt32>(mpDoc->GetNodes().size()); +} + +void TextEngine::EnableUndo( bool bEnable ) +{ + // delete list when switching mode + if ( bEnable != IsUndoEnabled() ) + ResetUndo(); + + mbUndoEnabled = bEnable; +} + +SfxUndoManager& TextEngine::GetUndoManager() +{ + if ( !mpUndoManager ) + mpUndoManager.reset( new TextUndoManager( this ) ); + return *mpUndoManager; +} + +void TextEngine::UndoActionStart( sal_uInt16 nId ) +{ + if ( IsUndoEnabled() && !IsInUndo() ) + { + GetUndoManager().EnterListAction( OUString(), OUString(), nId, ViewShellId(-1) ); + } +} + +void TextEngine::UndoActionEnd() +{ + if ( IsUndoEnabled() && !IsInUndo() ) + GetUndoManager().LeaveListAction(); +} + +void TextEngine::InsertUndo( std::unique_ptr<TextUndo> pUndo, bool bTryMerge ) +{ + SAL_WARN_IF( IsInUndo(), "vcl", "InsertUndo: in Undo mode!" ); + GetUndoManager().AddUndoAction( std::move(pUndo), bTryMerge ); +} + +void TextEngine::ResetUndo() +{ + if ( mpUndoManager ) + mpUndoManager->Clear(); +} + +void TextEngine::InsertContent( std::unique_ptr<TextNode> pNode, sal_uInt32 nPara ) +{ + SAL_WARN_IF( !pNode, "vcl", "InsertContent: NULL-Pointer!" ); + SAL_WARN_IF( !IsInUndo(), "vcl", "InsertContent: only in Undo()!" ); + TEParaPortion* pNew = new TEParaPortion( pNode.get() ); + mpTEParaPortions->Insert( pNew, nPara ); + mpDoc->GetNodes().insert( mpDoc->GetNodes().begin() + nPara, std::move(pNode) ); + ImpParagraphInserted( nPara ); +} + +TextPaM TextEngine::SplitContent( sal_uInt32 nNode, sal_Int32 nSepPos ) +{ +#ifdef DBG_UTIL + TextNode* pNode = mpDoc->GetNodes()[ nNode ].get(); + SAL_WARN_IF( !pNode, "vcl", "SplitContent: Invalid Node!" ); + SAL_WARN_IF( !IsInUndo(), "vcl", "SplitContent: only in Undo()!" ); + SAL_WARN_IF( nSepPos > pNode->GetText().getLength(), "vcl", "SplitContent: Bad index" ); +#endif + TextPaM aPaM( nNode, nSepPos ); + return ImpInsertParaBreak( aPaM ); +} + +TextPaM TextEngine::ConnectContents( sal_uInt32 nLeftNode ) +{ + SAL_WARN_IF( !IsInUndo(), "vcl", "ConnectContent: only in Undo()!" ); + return ImpConnectParagraphs( nLeftNode, nLeftNode+1 ); +} + +void TextEngine::SeekCursor( sal_uInt32 nPara, sal_Int32 nPos, vcl::Font& rFont, OutputDevice* pOutDev ) +{ + rFont = maFont; + if ( pOutDev ) + pOutDev->SetTextColor( maTextColor ); + + TextNode* pNode = mpDoc->GetNodes()[ nPara ].get(); + sal_uInt16 nAttribs = pNode->GetCharAttribs().Count(); + for ( sal_uInt16 nAttr = 0; nAttr < nAttribs; nAttr++ ) + { + TextCharAttrib& rAttrib = pNode->GetCharAttribs().GetAttrib( nAttr ); + if ( rAttrib.GetStart() > nPos ) + break; + + // When seeking don't use Attr that start there! + // Do not use empty attributes: + // - If just being setup and empty => no effect on Font + // - Characters that are setup in an empty paragraph become visible right away. + if ( ( ( rAttrib.GetStart() < nPos ) && ( rAttrib.GetEnd() >= nPos ) ) + || pNode->GetText().isEmpty() ) + { + if ( rAttrib.Which() != TEXTATTR_FONTCOLOR ) + { + rAttrib.GetAttr().SetFont(rFont); + } + else + { + if ( pOutDev ) + pOutDev->SetTextColor( static_cast<const TextAttribFontColor&>(rAttrib.GetAttr()).GetColor() ); + } + } + } + + if ( !(mpIMEInfos && mpIMEInfos->pAttribs && ( mpIMEInfos->aPos.GetPara() == nPara ) && + ( nPos > mpIMEInfos->aPos.GetIndex() ) && ( nPos <= ( mpIMEInfos->aPos.GetIndex() + mpIMEInfos->nLen ) )) ) + return; + + ExtTextInputAttr nAttr = mpIMEInfos->pAttribs[ nPos - mpIMEInfos->aPos.GetIndex() - 1 ]; + if ( nAttr & ExtTextInputAttr::Underline ) + rFont.SetUnderline( LINESTYLE_SINGLE ); + else if ( nAttr & ExtTextInputAttr::DoubleUnderline ) + rFont.SetUnderline( LINESTYLE_DOUBLE ); + else if ( nAttr & ExtTextInputAttr::BoldUnderline ) + rFont.SetUnderline( LINESTYLE_BOLD ); + else if ( nAttr & ExtTextInputAttr::DottedUnderline ) + rFont.SetUnderline( LINESTYLE_DOTTED ); + else if ( nAttr & ExtTextInputAttr::DashDotUnderline ) + rFont.SetUnderline( LINESTYLE_DOTTED ); + if ( nAttr & ExtTextInputAttr::RedText ) + rFont.SetColor( COL_RED ); + else if ( nAttr & ExtTextInputAttr::HalfToneText ) + rFont.SetColor( COL_LIGHTGRAY ); + if ( nAttr & ExtTextInputAttr::Highlight ) + { + const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings(); + rFont.SetColor( rStyleSettings.GetHighlightTextColor() ); + rFont.SetFillColor( rStyleSettings.GetHighlightColor() ); + rFont.SetTransparent( false ); + } + else if ( nAttr & ExtTextInputAttr::GrayWaveline ) + { + rFont.SetUnderline( LINESTYLE_WAVE ); +// if( pOut ) +// pOut->SetTextLineColor( COL_LIGHTGRAY ); + } +} + +void TextEngine::FormatAndUpdate( TextView* pCurView ) +{ + if ( mbDowning ) + return; + + if ( IsInUndo() ) + IdleFormatAndUpdate( pCurView ); + else + { + FormatDoc(); + UpdateViews( pCurView ); + } +} + +void TextEngine::IdleFormatAndUpdate( TextView* pCurView, sal_uInt16 nMaxTimerRestarts ) +{ + mpIdleFormatter->DoIdleFormat( pCurView, nMaxTimerRestarts ); +} + +void TextEngine::TextModified() +{ + mbFormatted = false; + mbModified = true; +} + +void TextEngine::UpdateViews( TextView* pCurView ) +{ + if ( !GetUpdateMode() || IsFormatting() || maInvalidRect.IsEmpty() ) + return; + + SAL_WARN_IF( !IsFormatted(), "vcl", "UpdateViews: Doc not formatted!" ); + + for (TextView* pView : *mpViews) + { + pView->HideCursor(); + + tools::Rectangle aClipRect( maInvalidRect ); + const Size aOutSz = pView->GetWindow()->GetOutputSizePixel(); + const tools::Rectangle aVisArea( pView->GetStartDocPos(), aOutSz ); + aClipRect.Intersection( aVisArea ); + if ( !aClipRect.IsEmpty() ) + { + // translate into window coordinates + Point aNewPos = pView->GetWindowPos( aClipRect.TopLeft() ); + if ( IsRightToLeft() ) + aNewPos.AdjustX( -(aOutSz.Width() - 1) ); + aClipRect.SetPos( aNewPos ); + + pView->GetWindow()->Invalidate( aClipRect ); + } + } + + if ( pCurView ) + { + pCurView->ShowCursor( pCurView->IsAutoScroll() ); + } + + maInvalidRect = tools::Rectangle(); +} + +IMPL_LINK_NOARG(TextEngine, IdleFormatHdl, Timer *, void) +{ + FormatAndUpdate( mpIdleFormatter->GetView() ); +} + +void TextEngine::CheckIdleFormatter() +{ + mpIdleFormatter->ForceTimeout(); +} + +void TextEngine::FormatFullDoc() +{ + for ( sal_uInt32 nPortion = 0; nPortion < mpTEParaPortions->Count(); ++nPortion ) + { + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPortion ); + pTEParaPortion->MarkSelectionInvalid( 0 ); + } + mbFormatted = false; + FormatDoc(); +} + +void TextEngine::FormatDoc() +{ + if ( IsFormatted() || !GetUpdateMode() || IsFormatting() ) + return; + + mbIsFormatting = true; + mbHasMultiLineParas = false; + + tools::Long nY = 0; + bool bGrow = false; + + maInvalidRect = tools::Rectangle(); // clear + for ( sal_uInt32 nPara = 0; nPara < mpTEParaPortions->Count(); ++nPara ) + { + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPara ); + if ( pTEParaPortion->IsInvalid() ) + { + const tools::Long nOldParaWidth = mnCurTextWidth >= 0 ? CalcTextWidth( nPara ) : -1; + + Broadcast( TextHint( SfxHintId::TextFormatPara, nPara ) ); + + if ( CreateLines( nPara ) ) + bGrow = true; + + // set InvalidRect only once + if ( maInvalidRect.IsEmpty() ) + { + // otherwise remains Empty() for Paperwidth 0 (AutoPageSize) + const tools::Long nWidth = mnMaxTextWidth + ? mnMaxTextWidth + : std::numeric_limits<tools::Long>::max(); + const Range aInvRange( GetInvalidYOffsets( nPara ) ); + maInvalidRect = tools::Rectangle( Point( 0, nY+aInvRange.Min() ), + Size( nWidth, aInvRange.Len() ) ); + } + else + { + maInvalidRect.SetBottom( nY + CalcParaHeight( nPara ) ); + } + + if ( mnCurTextWidth >= 0 ) + { + const tools::Long nNewParaWidth = CalcTextWidth( nPara ); + if ( nNewParaWidth >= mnCurTextWidth ) + mnCurTextWidth = nNewParaWidth; + else if ( nOldParaWidth >= mnCurTextWidth ) + mnCurTextWidth = -1; + } + } + else if ( bGrow ) + { + maInvalidRect.SetBottom( nY + CalcParaHeight( nPara ) ); + } + nY += CalcParaHeight( nPara ); + if ( !mbHasMultiLineParas && pTEParaPortion->GetLines().size() > 1 ) + mbHasMultiLineParas = true; + } + + if ( !maInvalidRect.IsEmpty() ) + { + const tools::Long nNewHeight = CalcTextHeight(); + const tools::Long nDiff = nNewHeight - mnCurTextHeight; + if ( nNewHeight < mnCurTextHeight ) + { + maInvalidRect.SetBottom( std::max( nNewHeight, mnCurTextHeight ) ); + if ( maInvalidRect.IsEmpty() ) + { + maInvalidRect.SetTop( 0 ); + // Left and Right are not evaluated, but set because of IsEmpty + maInvalidRect.SetLeft( 0 ); + maInvalidRect.SetRight( mnMaxTextWidth ); + } + } + + mnCurTextHeight = nNewHeight; + if ( nDiff ) + { + mbFormatted = true; + Broadcast( TextHint( SfxHintId::TextHeightChanged ) ); + } + } + + mbIsFormatting = false; + mbFormatted = true; + + Broadcast( TextHint( SfxHintId::TextFormatted ) ); +} + +void TextEngine::CreateAndInsertEmptyLine( sal_uInt32 nPara ) +{ + TextNode* pNode = mpDoc->GetNodes()[ nPara ].get(); + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPara ); + + TextLine aTmpLine; + aTmpLine.SetStart( pNode->GetText().getLength() ); + aTmpLine.SetEnd( aTmpLine.GetStart() ); + + if ( ImpGetAlign() == TxtAlign::Center ) + aTmpLine.SetStartX( static_cast<short>(mnMaxTextWidth / 2) ); + else if ( ImpGetAlign() == TxtAlign::Right ) + aTmpLine.SetStartX( static_cast<short>(mnMaxTextWidth) ); + else + aTmpLine.SetStartX( mpDoc->GetLeftMargin() ); + + bool bLineBreak = !pNode->GetText().isEmpty(); + + TETextPortion aDummyPortion( 0 ); + aDummyPortion.GetWidth() = 0; + pTEParaPortion->GetTextPortions().push_back( aDummyPortion ); + + if ( bLineBreak ) + { + // -2: The new one is already inserted. + const std::size_t nPos = pTEParaPortion->GetTextPortions().size() - 1; + aTmpLine.SetStartPortion( nPos ); + aTmpLine.SetEndPortion( nPos ); + } + pTEParaPortion->GetLines().push_back( aTmpLine ); +} + +void TextEngine::ImpBreakLine( sal_uInt32 nPara, TextLine* pLine, sal_Int32 nPortionStart, tools::Long nRemainingWidth ) +{ + TextNode* pNode = mpDoc->GetNodes()[ nPara ].get(); + + // Font still should be adjusted + sal_Int32 nMaxBreakPos = mpRefDev->GetTextBreak( pNode->GetText(), nRemainingWidth, nPortionStart ); + + SAL_WARN_IF( nMaxBreakPos >= pNode->GetText().getLength(), "vcl", "ImpBreakLine: Break?!" ); + + if ( nMaxBreakPos == -1 ) // GetTextBreak() != GetTextSize() + nMaxBreakPos = pNode->GetText().getLength() - 1; + + uno::Reference < i18n::XBreakIterator > xBI = GetBreakIterator(); + i18n::LineBreakHyphenationOptions aHyphOptions( nullptr, uno::Sequence< beans::PropertyValue >(), 1 ); + + i18n::LineBreakUserOptions aUserOptions; + aUserOptions.forbiddenBeginCharacters = ImpGetLocaleDataWrapper()->getForbiddenCharacters().beginLine; + aUserOptions.forbiddenEndCharacters = ImpGetLocaleDataWrapper()->getForbiddenCharacters().endLine; + aUserOptions.applyForbiddenRules = true; + aUserOptions.allowPunctuationOutsideMargin = false; + aUserOptions.allowHyphenateEnglish = false; + + static const css::lang::Locale aDefLocale; + i18n::LineBreakResults aLBR = xBI->getLineBreak( pNode->GetText(), nMaxBreakPos, aDefLocale, pLine->GetStart(), aHyphOptions, aUserOptions ); + sal_Int32 nBreakPos = aLBR.breakIndex; + if ( nBreakPos <= pLine->GetStart() ) + { + nBreakPos = nMaxBreakPos; + if ( nBreakPos <= pLine->GetStart() ) + nBreakPos = pLine->GetStart() + 1; // infinite loop otherwise! + } + + // the damaged Portion is the End Portion + pLine->SetEnd( nBreakPos ); + const std::size_t nEndPortion = SplitTextPortion( nPara, nBreakPos ); + + if ( nBreakPos >= pLine->GetStart() && + nBreakPos < pNode->GetText().getLength() && + pNode->GetText()[ nBreakPos ] == ' ' ) + { + // generally suppress blanks at the end of line + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPara ); + TETextPortion& rTP = pTEParaPortion->GetTextPortions()[ nEndPortion ]; + SAL_WARN_IF( nBreakPos <= pLine->GetStart(), "vcl", "ImpBreakLine: SplitTextPortion at beginning of line?" ); + rTP.GetWidth() = CalcTextWidth( nPara, nBreakPos-rTP.GetLen(), rTP.GetLen()-1 ); + } + pLine->SetEndPortion( nEndPortion ); +} + +std::size_t TextEngine::SplitTextPortion( sal_uInt32 nPara, sal_Int32 nPos ) +{ + + // the Portion at nPos is being split, unless there is already a switch at nPos + if ( nPos == 0 ) + return 0; + + std::size_t nSplitPortion; + sal_Int32 nTmpPos = 0; + TETextPortion* pTextPortion = nullptr; + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPara ); + const std::size_t nPortions = pTEParaPortion->GetTextPortions().size(); + for ( nSplitPortion = 0; nSplitPortion < nPortions; nSplitPortion++ ) + { + TETextPortion& rTP = pTEParaPortion->GetTextPortions()[nSplitPortion]; + nTmpPos += rTP.GetLen(); + if ( nTmpPos >= nPos ) + { + if ( nTmpPos == nPos ) // nothing needs splitting + return nSplitPortion; + pTextPortion = &rTP; + break; + } + } + + SAL_WARN_IF( !pTextPortion, "vcl", "SplitTextPortion: position outside of region!" ); + + const sal_Int32 nOverlapp = nTmpPos - nPos; + pTextPortion->GetLen() -= nOverlapp; + pTextPortion->GetWidth() = CalcTextWidth( nPara, nPos-pTextPortion->GetLen(), pTextPortion->GetLen() ); + TETextPortion aNewPortion( nOverlapp ); + pTEParaPortion->GetTextPortions().insert( pTEParaPortion->GetTextPortions().begin() + nSplitPortion + 1, aNewPortion ); + + return nSplitPortion; +} + +void TextEngine::CreateTextPortions( sal_uInt32 nPara, sal_Int32 nStartPos ) +{ + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPara ); + TextNode* pNode = pTEParaPortion->GetNode(); + SAL_WARN_IF( pNode->GetText().isEmpty(), "vcl", "CreateTextPortions: should not be used for empty paragraphs!" ); + + o3tl::sorted_vector<sal_Int32> aPositions; + o3tl::sorted_vector<sal_Int32>::const_iterator aPositionsIt; + aPositions.insert(0); + + const sal_uInt16 nAttribs = pNode->GetCharAttribs().Count(); + for ( sal_uInt16 nAttr = 0; nAttr < nAttribs; nAttr++ ) + { + TextCharAttrib& rAttrib = pNode->GetCharAttribs().GetAttrib( nAttr ); + + aPositions.insert( rAttrib.GetStart() ); + aPositions.insert( rAttrib.GetEnd() ); + } + aPositions.insert( pNode->GetText().getLength() ); + + const std::vector<TEWritingDirectionInfo>& rWritingDirections = pTEParaPortion->GetWritingDirectionInfos(); + for ( const auto& rWritingDirection : rWritingDirections ) + aPositions.insert( rWritingDirection.nStartPos ); + + if ( mpIMEInfos && mpIMEInfos->pAttribs && ( mpIMEInfos->aPos.GetPara() == nPara ) ) + { + ExtTextInputAttr nLastAttr = ExtTextInputAttr(0xffff); + for( sal_Int32 n = 0; n < mpIMEInfos->nLen; n++ ) + { + if ( mpIMEInfos->pAttribs[n] != nLastAttr ) + { + aPositions.insert( mpIMEInfos->aPos.GetIndex() + n ); + nLastAttr = mpIMEInfos->pAttribs[n]; + } + } + } + + sal_Int32 nTabPos = pNode->GetText().indexOf( '\t' ); + while ( nTabPos != -1 ) + { + aPositions.insert( nTabPos ); + aPositions.insert( nTabPos + 1 ); + nTabPos = pNode->GetText().indexOf( '\t', nTabPos+1 ); + } + + // Delete starting with... + // Unfortunately, the number of TextPortions does not have to be + // equal to aPositions.Count(), because of linebreaks + sal_Int32 nPortionStart = 0; + std::size_t nInvPortion = 0; + std::size_t nP; + for ( nP = 0; nP < pTEParaPortion->GetTextPortions().size(); nP++ ) + { + TETextPortion& rTmpPortion = pTEParaPortion->GetTextPortions()[nP]; + nPortionStart += rTmpPortion.GetLen(); + if ( nPortionStart >= nStartPos ) + { + nPortionStart -= rTmpPortion.GetLen(); + nInvPortion = nP; + break; + } + } + OSL_ENSURE(nP < pTEParaPortion->GetTextPortions().size() + || pTEParaPortion->GetTextPortions().empty(), + "CreateTextPortions: Nothing to delete!"); + if ( nInvPortion && ( nPortionStart+pTEParaPortion->GetTextPortions()[nInvPortion].GetLen() > nStartPos ) ) + { + // better one before... + // But only if it was within the Portion; otherwise it might be + // the only one in the previous line! + nInvPortion--; + nPortionStart -= pTEParaPortion->GetTextPortions()[nInvPortion].GetLen(); + } + pTEParaPortion->GetTextPortions().DeleteFromPortion( nInvPortion ); + + // a Portion might have been created by a line break + aPositions.insert( nPortionStart ); + + aPositionsIt = aPositions.find( nPortionStart ); + SAL_WARN_IF( aPositionsIt == aPositions.end(), "vcl", "CreateTextPortions: nPortionStart not found" ); + + if ( aPositionsIt != aPositions.end() ) + { + o3tl::sorted_vector<sal_Int32>::const_iterator nextIt = aPositionsIt; + for ( ++nextIt; nextIt != aPositions.end(); ++aPositionsIt, ++nextIt ) + { + TETextPortion aNew( *nextIt - *aPositionsIt ); + pTEParaPortion->GetTextPortions().push_back( aNew ); + } + } + OSL_ENSURE(pTEParaPortion->GetTextPortions().size(), "CreateTextPortions: No Portions?!"); +} + +void TextEngine::RecalcTextPortion( sal_uInt32 nPara, sal_Int32 nStartPos, sal_Int32 nNewChars ) +{ + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPara ); + OSL_ENSURE(pTEParaPortion->GetTextPortions().size(), "RecalcTextPortion: no Portions!"); + OSL_ENSURE(nNewChars, "RecalcTextPortion: Diff == 0"); + + TextNode* const pNode = pTEParaPortion->GetNode(); + if ( nNewChars > 0 ) + { + // If an Attribute is starting/ending at nStartPos, or there is a tab + // before nStartPos => a new Portion starts. + // Otherwise the Portion is extended at nStartPos. + // Or if at the very beginning ( StartPos 0 ) followed by a tab... + if ( ( pNode->GetCharAttribs().HasBoundingAttrib( nStartPos ) ) || + ( nStartPos && ( pNode->GetText()[ nStartPos - 1 ] == '\t' ) ) || + ( !nStartPos && ( nNewChars < pNode->GetText().getLength() ) && pNode->GetText()[ nNewChars ] == '\t' ) ) + { + std::size_t nNewPortionPos = 0; + if ( nStartPos ) + nNewPortionPos = SplitTextPortion( nPara, nStartPos ) + 1; + + // Here could be an empty Portion if the paragraph was empty, + // or a new line was created by a hard line-break. + if ( ( nNewPortionPos < pTEParaPortion->GetTextPortions().size() ) && + !pTEParaPortion->GetTextPortions()[nNewPortionPos].GetLen() ) + { + // use the empty Portion + pTEParaPortion->GetTextPortions()[nNewPortionPos].GetLen() = nNewChars; + } + else + { + TETextPortion aNewPortion( nNewChars ); + pTEParaPortion->GetTextPortions().insert( pTEParaPortion->GetTextPortions().begin() + nNewPortionPos, aNewPortion ); + } + } + else + { + sal_Int32 nPortionStart {0}; + const std::size_t nTP = pTEParaPortion->GetTextPortions().FindPortion( nStartPos, nPortionStart ); + TETextPortion& rTP = pTEParaPortion->GetTextPortions()[ nTP ]; + rTP.GetLen() += nNewChars; + rTP.GetWidth() = -1; + } + } + else + { + // Shrink or remove Portion + // Before calling this function, ensure that no Portions were in the deleted range! + + // There must be no Portion reaching into or starting within, + // thus: nStartPos <= nPos <= nStartPos - nNewChars(neg.) + std::size_t nPortion = 0; + sal_Int32 nPos = 0; + const sal_Int32 nEnd = nStartPos-nNewChars; + const std::size_t nPortions = pTEParaPortion->GetTextPortions().size(); + TETextPortion* pTP = nullptr; + for ( nPortion = 0; nPortion < nPortions; nPortion++ ) + { + pTP = &pTEParaPortion->GetTextPortions()[ nPortion ]; + if ( ( nPos+pTP->GetLen() ) > nStartPos ) + { + SAL_WARN_IF( nPos > nStartPos, "vcl", "RecalcTextPortion: Bad Start!" ); + SAL_WARN_IF( nPos+pTP->GetLen() < nEnd, "vcl", "RecalcTextPortion: Bad End!" ); + break; + } + nPos += pTP->GetLen(); + } + SAL_WARN_IF( !pTP, "vcl", "RecalcTextPortion: Portion not found!" ); + if ( ( nPos == nStartPos ) && ( (nPos+pTP->GetLen()) == nEnd ) ) + { + // remove Portion + pTEParaPortion->GetTextPortions().erase( pTEParaPortion->GetTextPortions().begin() + nPortion ); + } + else + { + SAL_WARN_IF( pTP->GetLen() <= (-nNewChars), "vcl", "RecalcTextPortion: Portion too small to shrink!" ); + pTP->GetLen() += nNewChars; + } + OSL_ENSURE( pTEParaPortion->GetTextPortions().size(), + "RecalcTextPortion: none are left!" ); + } +} + +void TextEngine::ImpPaint( OutputDevice* pOutDev, const Point& rStartPos, tools::Rectangle const* pPaintArea, TextSelection const* pSelection ) +{ + if ( !GetUpdateMode() ) + return; + + if ( !IsFormatted() ) + FormatDoc(); + + vcl::Window* const pOutWin = pOutDev->GetOwnerWindow(); + const bool bTransparent = (pOutWin && pOutWin->IsPaintTransparent()); + + tools::Long nY = rStartPos.Y(); + + TextPaM const* pSelStart = nullptr; + TextPaM const* pSelEnd = nullptr; + if ( pSelection && pSelection->HasRange() ) + { + const bool bInvers = pSelection->GetEnd() < pSelection->GetStart(); + pSelStart = !bInvers ? &pSelection->GetStart() : &pSelection->GetEnd(); + pSelEnd = bInvers ? &pSelection->GetStart() : &pSelection->GetEnd(); + } + + const StyleSettings& rStyleSettings = pOutDev->GetSettings().GetStyleSettings(); + + // for all paragraphs + for ( sal_uInt32 nPara = 0; nPara < mpTEParaPortions->Count(); ++nPara ) + { + TEParaPortion* pPortion = mpTEParaPortions->GetObject( nPara ); + // in case while typing Idle-Formatting, asynchronous Paint + if ( pPortion->IsInvalid() ) + return; + + const tools::Long nParaHeight = CalcParaHeight( nPara ); + if ( !pPaintArea || ( ( nY + nParaHeight ) > pPaintArea->Top() ) ) + { + // for all lines of the paragraph + sal_Int32 nIndex = 0; + for ( auto & rLine : pPortion->GetLines() ) + { + Point aTmpPos( rStartPos.X() + rLine.GetStartX(), nY ); + + if ( !pPaintArea || ( ( nY + mnCharHeight ) > pPaintArea->Top() ) ) + { + // for all Portions of the line + nIndex = rLine.GetStart(); + for ( std::size_t y = rLine.GetStartPortion(); y <= rLine.GetEndPortion(); y++ ) + { + OSL_ENSURE(pPortion->GetTextPortions().size(), + "ImpPaint: Line without Textportion!"); + TETextPortion& rTextPortion = pPortion->GetTextPortions()[ y ]; + + ImpInitLayoutMode( pOutDev /*, pTextPortion->IsRightToLeft() */); + + const tools::Long nTxtWidth = rTextPortion.GetWidth(); + aTmpPos.setX( rStartPos.X() + ImpGetOutputOffset( nPara, &rLine, nIndex, nIndex ) ); + + // only print if starting in the visible region + if ( ( aTmpPos.X() + nTxtWidth ) >= 0 ) + { + switch ( rTextPortion.GetKind() ) + { + case PORTIONKIND_TEXT: + { + vcl::Font aFont; + SeekCursor( nPara, nIndex+1, aFont, pOutDev ); + if( bTransparent ) + aFont.SetTransparent( true ); + else if ( pSelection ) + aFont.SetTransparent( false ); + pOutDev->SetFont( aFont ); + + sal_Int32 nTmpIndex = nIndex; + sal_Int32 nEnd = nTmpIndex + rTextPortion.GetLen(); + Point aPos = aTmpPos; + + bool bDone = false; + if ( pSelStart ) + { + // is a part of it in the selection? + const TextPaM aTextStart( nPara, nTmpIndex ); + const TextPaM aTextEnd( nPara, nEnd ); + if ( ( aTextStart < *pSelEnd ) && ( aTextEnd > *pSelStart ) ) + { + // 1) vcl::Region before Selection + if ( aTextStart < *pSelStart ) + { + const sal_Int32 nL = pSelStart->GetIndex() - nTmpIndex; + pOutDev->SetFont( aFont); + pOutDev->SetTextFillColor(); + aPos.setX( rStartPos.X() + ImpGetOutputOffset( nPara, &rLine, nTmpIndex, nTmpIndex+nL ) ); + pOutDev->DrawText( aPos, pPortion->GetNode()->GetText(), nTmpIndex, nL ); + nTmpIndex = nTmpIndex + nL; + + } + // 2) vcl::Region with Selection + sal_Int32 nL = nEnd - nTmpIndex; + if ( aTextEnd > *pSelEnd ) + nL = pSelEnd->GetIndex() - nTmpIndex; + if ( nL ) + { + const Color aOldTextColor = pOutDev->GetTextColor(); + pOutDev->SetTextColor( rStyleSettings.GetHighlightTextColor() ); + pOutDev->SetTextFillColor( rStyleSettings.GetHighlightColor() ); + aPos.setX( rStartPos.X() + ImpGetOutputOffset( nPara, &rLine, nTmpIndex, nTmpIndex+nL ) ); + pOutDev->DrawText( aPos, pPortion->GetNode()->GetText(), nTmpIndex, nL ); + pOutDev->SetTextColor( aOldTextColor ); + pOutDev->SetTextFillColor(); + nTmpIndex = nTmpIndex + nL; + } + + // 3) vcl::Region after Selection + if ( nTmpIndex < nEnd ) + { + nL = nEnd-nTmpIndex; + pOutDev->SetTextFillColor(); + aPos.setX( rStartPos.X() + ImpGetOutputOffset( nPara, &rLine, nTmpIndex, nTmpIndex+nL ) ); + pOutDev->DrawText( aPos, pPortion->GetNode()->GetText(), nTmpIndex, nEnd-nTmpIndex ); + } + bDone = true; + } + } + if ( !bDone ) + { + pOutDev->SetTextFillColor(); + aPos.setX( rStartPos.X() + ImpGetOutputOffset( nPara, &rLine, nTmpIndex, nEnd ) ); + pOutDev->DrawText( aPos, pPortion->GetNode()->GetText(), nTmpIndex, nEnd-nTmpIndex ); + } + } + break; + case PORTIONKIND_TAB: + // for HideSelection() only Range, pSelection = 0. + if ( pSelStart ) // also implies pSelEnd + { + const tools::Rectangle aTabArea( aTmpPos, Point( aTmpPos.X()+nTxtWidth, aTmpPos.Y()+mnCharHeight-1 ) ); + // is the Tab in the Selection??? + const TextPaM aTextStart(nPara, nIndex); + const TextPaM aTextEnd(nPara, nIndex + 1); + if ((aTextStart < *pSelEnd) && (aTextEnd > *pSelStart)) + { + const Color aOldColor = pOutDev->GetFillColor(); + pOutDev->SetFillColor( + rStyleSettings.GetHighlightColor()); + pOutDev->DrawRect(aTabArea); + pOutDev->SetFillColor(aOldColor); + } + else + { + pOutDev->Erase( aTabArea ); + } + } + break; + default: + OSL_FAIL( "ImpPaint: Unknown Portion-Type !" ); + } + } + + nIndex += rTextPortion.GetLen(); + } + } + + nY += mnCharHeight; + + if ( pPaintArea && ( nY >= pPaintArea->Bottom() ) ) + break; // no more visible actions + } + } + else + { + nY += nParaHeight; + } + + if ( pPaintArea && ( nY > pPaintArea->Bottom() ) ) + break; // no more visible actions + } +} + +bool TextEngine::CreateLines( sal_uInt32 nPara ) +{ + // bool: changing Height of Paragraph Yes/No - true/false + + TextNode* pNode = mpDoc->GetNodes()[ nPara ].get(); + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPara ); + SAL_WARN_IF( !pTEParaPortion->IsInvalid(), "vcl", "CreateLines: Portion not invalid!" ); + + const auto nOldLineCount = pTEParaPortion->GetLines().size(); + + // fast special case for empty paragraphs + if ( pTEParaPortion->GetNode()->GetText().isEmpty() ) + { + if ( !pTEParaPortion->GetTextPortions().empty() ) + pTEParaPortion->GetTextPortions().Reset(); + pTEParaPortion->GetLines().clear(); + CreateAndInsertEmptyLine( nPara ); + pTEParaPortion->SetValid(); + return nOldLineCount != pTEParaPortion->GetLines().size(); + } + + // initialization + if ( pTEParaPortion->GetLines().empty() ) + { + pTEParaPortion->GetLines().emplace_back( ); + } + + const sal_Int32 nInvalidDiff = pTEParaPortion->GetInvalidDiff(); + const sal_Int32 nInvalidStart = pTEParaPortion->GetInvalidPosStart(); + const sal_Int32 nInvalidEnd = nInvalidStart + std::abs( nInvalidDiff ); + bool bQuickFormat = false; + + if ( pTEParaPortion->GetWritingDirectionInfos().empty() ) + ImpInitWritingDirections( nPara ); + + if ( pTEParaPortion->GetWritingDirectionInfos().size() == 1 && pTEParaPortion->IsSimpleInvalid() ) + { + bQuickFormat = nInvalidDiff != 0; + if ( nInvalidDiff < 0 ) + { + // check if deleting across Portion border + sal_Int32 nPos = 0; + for ( const auto & rTP : pTEParaPortion->GetTextPortions() ) + { + // there must be no Start/End in the deleted region + nPos += rTP.GetLen(); + if ( nPos > nInvalidStart && nPos < nInvalidEnd ) + { + bQuickFormat = false; + break; + } + } + } + } + + if ( bQuickFormat ) + RecalcTextPortion( nPara, nInvalidStart, nInvalidDiff ); + else + CreateTextPortions( nPara, nInvalidStart ); + + // search for line with InvalidPos; start a line prior + // flag lines => do not remove! + + sal_uInt16 nLine = pTEParaPortion->GetLines().size()-1; + for ( sal_uInt16 nL = 0; nL <= nLine; nL++ ) + { + TextLine& rLine = pTEParaPortion->GetLines()[ nL ]; + if ( rLine.GetEnd() > nInvalidStart ) + { + nLine = nL; + break; + } + rLine.SetValid(); + } + // start a line before... + // if typing at the end, the line before cannot change + if ( nLine && ( !pTEParaPortion->IsSimpleInvalid() || ( nInvalidEnd < pNode->GetText().getLength() ) || ( nInvalidDiff <= 0 ) ) ) + nLine--; + + TextLine* pLine = &( pTEParaPortion->GetLines()[ nLine ] ); + + // format all lines starting here + std::size_t nDelFromLine = TETextPortionList::npos; + + sal_Int32 nIndex = pLine->GetStart(); + TextLine aSaveLine( *pLine ); + + while ( nIndex < pNode->GetText().getLength() ) + { + bool bEOL = false; + sal_Int32 nPortionStart = 0; + sal_Int32 nPortionEnd = 0; + + sal_Int32 nTmpPos = nIndex; + std::size_t nTmpPortion = pLine->GetStartPortion(); + tools::Long nTmpWidth = mpDoc->GetLeftMargin(); + // do not subtract margin; it is included in TmpWidth + tools::Long nXWidth = std::max( + mnMaxTextWidth ? mnMaxTextWidth : std::numeric_limits<tools::Long>::max(), nTmpWidth); + + // search for Portion that does not fit anymore into line + TETextPortion* pPortion = nullptr; + bool bBrokenLine = false; + + while ( ( nTmpWidth <= nXWidth ) && !bEOL && ( nTmpPortion < pTEParaPortion->GetTextPortions().size() ) ) + { + nPortionStart = nTmpPos; + pPortion = &pTEParaPortion->GetTextPortions()[ nTmpPortion ]; + SAL_WARN_IF( !pPortion->GetLen(), "vcl", "CreateLines: Empty Portion!" ); + if ( pNode->GetText()[ nTmpPos ] == '\t' ) + { + tools::Long nCurPos = nTmpWidth-mpDoc->GetLeftMargin(); + nTmpWidth = ((nCurPos/mnDefTab)+1)*mnDefTab+mpDoc->GetLeftMargin(); + pPortion->GetWidth() = nTmpWidth - nCurPos - mpDoc->GetLeftMargin(); + // infinite loop, if this is the first token of the line and nTmpWidth > aPaperSize.Width !!! + if ( ( nTmpWidth >= nXWidth ) && ( nTmpPortion == pLine->GetStartPortion() ) ) + { + // adjust Tab + pPortion->GetWidth() = nXWidth-1; + nTmpWidth = pPortion->GetWidth(); + bEOL = true; + bBrokenLine = true; + } + pPortion->GetKind() = PORTIONKIND_TAB; + } + else + { + + pPortion->GetWidth() = CalcTextWidth( nPara, nTmpPos, pPortion->GetLen() ); + nTmpWidth += pPortion->GetWidth(); + + pPortion->SetRightToLeft( ImpGetRightToLeft( nPara, nTmpPos+1 ) ); + pPortion->GetKind() = PORTIONKIND_TEXT; + } + + nTmpPos += pPortion->GetLen(); + nPortionEnd = nTmpPos; + nTmpPortion++; + } + + // this was perhaps one Portion too far + bool bFixedEnd = false; + if ( nTmpWidth > nXWidth ) + { + nPortionEnd = nTmpPos; + nTmpPos -= pPortion->GetLen(); + nPortionStart = nTmpPos; + nTmpPortion--; + bEOL = false; + + nTmpWidth -= pPortion->GetWidth(); + if ( pPortion->GetKind() == PORTIONKIND_TAB ) + { + bEOL = true; + bFixedEnd = true; + } + } + else + { + bEOL = true; + pLine->SetEnd( nPortionEnd ); + OSL_ENSURE(pTEParaPortion->GetTextPortions().size(), + "CreateLines: No TextPortions?"); + pLine->SetEndPortion( pTEParaPortion->GetTextPortions().size() - 1 ); + } + + if ( bFixedEnd ) + { + pLine->SetEnd( nPortionStart ); + pLine->SetEndPortion( nTmpPortion-1 ); + } + else if ( bBrokenLine ) + { + pLine->SetEnd( nPortionStart+1 ); + pLine->SetEndPortion( nTmpPortion-1 ); + } + else if ( !bEOL ) + { + SAL_WARN_IF( (nPortionEnd-nPortionStart) != pPortion->GetLen(), "vcl", "CreateLines: There is a Portion after all?!" ); + const tools::Long nRemainingWidth = mnMaxTextWidth - nTmpWidth; + ImpBreakLine( nPara, pLine, nPortionStart, nRemainingWidth ); + } + + if ( ( ImpGetAlign() == TxtAlign::Center ) || ( ImpGetAlign() == TxtAlign::Right ) ) + { + // adjust + tools::Long nTextWidth = 0; + for ( std::size_t nTP = pLine->GetStartPortion(); nTP <= pLine->GetEndPortion(); nTP++ ) + { + TETextPortion& rTextPortion = pTEParaPortion->GetTextPortions()[ nTP ]; + nTextWidth += rTextPortion.GetWidth(); + } + const tools::Long nSpace = mnMaxTextWidth - nTextWidth; + if ( nSpace > 0 ) + { + if ( ImpGetAlign() == TxtAlign::Center ) + pLine->SetStartX( static_cast<sal_uInt16>(nSpace / 2) ); + else // TxtAlign::Right + pLine->SetStartX( static_cast<sal_uInt16>(nSpace) ); + } + } + else + { + pLine->SetStartX( mpDoc->GetLeftMargin() ); + } + + // check if the line has to be printed again + pLine->SetInvalid(); + + if ( pTEParaPortion->IsSimpleInvalid() ) + { + // Change due to simple TextChange... + // Do not abort formatting, as Portions might have to be split! + // Once it is ok to abort, then validate the following lines! + // But mark as valid, thus reduce printing... + if ( pLine->GetEnd() < nInvalidStart ) + { + if ( *pLine == aSaveLine ) + { + pLine->SetValid(); + } + } + else + { + const sal_Int32 nStart = pLine->GetStart(); + const sal_Int32 nEnd = pLine->GetEnd(); + + if ( nStart > nInvalidEnd ) + { + if ( ( ( nStart-nInvalidDiff ) == aSaveLine.GetStart() ) && + ( ( nEnd-nInvalidDiff ) == aSaveLine.GetEnd() ) ) + { + pLine->SetValid(); + if ( bQuickFormat ) + { + pTEParaPortion->CorrectValuesBehindLastFormattedLine( nLine ); + break; + } + } + } + else if ( bQuickFormat && ( nEnd > nInvalidEnd) ) + { + // If the invalid line ends such that the next line starts + // at the 'same' position as before (no change in line breaks), + // the text width does not have to be recalculated. + if ( nEnd == ( aSaveLine.GetEnd() + nInvalidDiff ) ) + { + pTEParaPortion->CorrectValuesBehindLastFormattedLine( nLine ); + break; + } + } + } + } + + nIndex = pLine->GetEnd(); // next line Start = previous line End + // because nEnd is past the last char! + + const std::size_t nEndPortion = pLine->GetEndPortion(); + + // next line or new line + pLine = nullptr; + if ( nLine < pTEParaPortion->GetLines().size()-1 ) + pLine = &( pTEParaPortion->GetLines()[ ++nLine ] ); + if ( pLine && ( nIndex >= pNode->GetText().getLength() ) ) + { + nDelFromLine = nLine; + break; + } + if ( !pLine ) + { + if ( nIndex < pNode->GetText().getLength() ) + { + ++nLine; + pTEParaPortion->GetLines().insert( pTEParaPortion->GetLines().begin() + nLine, TextLine() ); + pLine = &pTEParaPortion->GetLines()[nLine]; + } + else + { + break; + } + } + aSaveLine = *pLine; + pLine->SetStart( nIndex ); + pLine->SetEnd( nIndex ); + pLine->SetStartPortion( nEndPortion+1 ); + pLine->SetEndPortion( nEndPortion+1 ); + + } // while ( Index < Len ) + + if (nDelFromLine != TETextPortionList::npos) + { + pTEParaPortion->GetLines().erase( pTEParaPortion->GetLines().begin() + nDelFromLine, + pTEParaPortion->GetLines().end() ); + } + + SAL_WARN_IF( pTEParaPortion->GetLines().empty(), "vcl", "CreateLines: No Line!" ); + + pTEParaPortion->SetValid(); + + return nOldLineCount != pTEParaPortion->GetLines().size(); +} + +OUString TextEngine::GetWord( const TextPaM& rCursorPos, TextPaM* pStartOfWord, TextPaM* pEndOfWord ) +{ + OUString aWord; + if ( rCursorPos.GetPara() < mpDoc->GetNodes().size() ) + { + TextSelection aSel( rCursorPos ); + TextNode* pNode = mpDoc->GetNodes()[ rCursorPos.GetPara() ].get(); + uno::Reference < i18n::XBreakIterator > xBI = GetBreakIterator(); + i18n::Boundary aBoundary = xBI->getWordBoundary( pNode->GetText(), rCursorPos.GetIndex(), GetLocale(), i18n::WordType::ANYWORD_IGNOREWHITESPACES, true ); + // tdf#57879 - expand selection to the left to include connector punctuations and search for additional word boundaries + if (aBoundary.startPos > 0 && aBoundary.startPos < pNode->GetText().getLength() && u_charType(pNode->GetText()[aBoundary.startPos]) == U_CONNECTOR_PUNCTUATION) + { + aBoundary.startPos = xBI->getWordBoundary(pNode->GetText(), aBoundary.startPos - 1, + GetLocale(), css::i18n::WordType::ANYWORD_IGNOREWHITESPACES, true).startPos; + } + while (aBoundary.startPos > 0 && u_charType(pNode->GetText()[aBoundary.startPos - 1]) == U_CONNECTOR_PUNCTUATION) + { + aBoundary.startPos = std::min(aBoundary.startPos, + xBI->getWordBoundary( pNode->GetText(), aBoundary.startPos - 2, + GetLocale(), css::i18n::WordType::ANYWORD_IGNOREWHITESPACES, true).startPos); + } + // tdf#57879 - expand selection to the right to include connector punctuations and search for additional word boundaries + if (aBoundary.endPos > 0 && aBoundary.endPos < pNode->GetText().getLength() && u_charType(pNode->GetText()[aBoundary.endPos - 1]) == U_CONNECTOR_PUNCTUATION) + { + aBoundary.endPos = xBI->getWordBoundary(pNode->GetText(), aBoundary.endPos, + GetLocale(), css::i18n::WordType::ANYWORD_IGNOREWHITESPACES, true).endPos; + } + while (aBoundary.endPos < pNode->GetText().getLength() && u_charType(pNode->GetText()[aBoundary.endPos]) == U_CONNECTOR_PUNCTUATION) + { + aBoundary.endPos = xBI->getWordBoundary(pNode->GetText(), aBoundary.endPos + 1, + GetLocale(), css::i18n::WordType::ANYWORD_IGNOREWHITESPACES, true).endPos; + } + aSel.GetStart().GetIndex() = aBoundary.startPos; + aSel.GetEnd().GetIndex() = aBoundary.endPos; + aWord = pNode->GetText().copy( aSel.GetStart().GetIndex(), aSel.GetEnd().GetIndex() - aSel.GetStart().GetIndex() ); + if ( pStartOfWord ) + *pStartOfWord = aSel.GetStart(); + if (pEndOfWord) + *pEndOfWord = aSel.GetEnd(); + } + return aWord; +} + +bool TextEngine::Read( SvStream& rInput, const TextSelection* pSel ) +{ + const bool bUpdate = GetUpdateMode(); + SetUpdateMode( false ); + + UndoActionStart(); + TextSelection aSel; + if ( pSel ) + aSel = *pSel; + else + { + const sal_uInt32 nParas = static_cast<sal_uInt32>(mpDoc->GetNodes().size()); + TextNode* pNode = mpDoc->GetNodes()[ nParas - 1 ].get(); + aSel = TextPaM( nParas-1 , pNode->GetText().getLength() ); + } + + if ( aSel.HasRange() ) + aSel = ImpDeleteText( aSel ); + + OStringBuffer aLine; + bool bDone = rInput.ReadLine( aLine ); + OUString aTmpStr(OStringToOUString(aLine, rInput.GetStreamCharSet())); + while ( bDone ) + { + aSel = ImpInsertText( aSel, aTmpStr ); + bDone = rInput.ReadLine( aLine ); + aTmpStr = OStringToOUString(aLine, rInput.GetStreamCharSet()); + if ( bDone ) + aSel = ImpInsertParaBreak( aSel.GetEnd() ); + } + + UndoActionEnd(); + + const TextSelection aNewSel( aSel.GetEnd(), aSel.GetEnd() ); + + // so that FormatAndUpdate does not access the invalid selection + if ( GetActiveView() ) + GetActiveView()->ImpSetSelection( aNewSel ); + + SetUpdateMode( bUpdate ); + FormatAndUpdate( GetActiveView() ); + + return rInput.GetError() == ERRCODE_NONE; +} + +void TextEngine::Write( SvStream& rOutput ) +{ + TextSelection aSel; + const sal_uInt32 nParas = static_cast<sal_uInt32>(mpDoc->GetNodes().size()); + TextNode* pSelNode = mpDoc->GetNodes()[ nParas - 1 ].get(); + aSel.GetStart() = TextPaM( 0, 0 ); + aSel.GetEnd() = TextPaM( nParas-1, pSelNode->GetText().getLength() ); + + for ( sal_uInt32 nPara = aSel.GetStart().GetPara(); nPara <= aSel.GetEnd().GetPara(); ++nPara ) + { + TextNode* pNode = mpDoc->GetNodes()[ nPara ].get(); + + const sal_Int32 nStartPos = nPara == aSel.GetStart().GetPara() + ? aSel.GetStart().GetIndex() : 0; + const sal_Int32 nEndPos = nPara == aSel.GetEnd().GetPara() + ? aSel.GetEnd().GetIndex() : pNode->GetText().getLength(); + + const OUString aText = pNode->GetText().copy( nStartPos, nEndPos-nStartPos ); + rOutput.WriteLine(OUStringToOString(aText, rOutput.GetStreamCharSet())); + } +} + +void TextEngine::RemoveAttribs( sal_uInt32 nPara ) +{ + if ( nPara >= mpDoc->GetNodes().size() ) + return; + + TextNode* pNode = mpDoc->GetNodes()[ nPara ].get(); + if ( pNode->GetCharAttribs().Count() ) + { + pNode->GetCharAttribs().Clear(); + + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPara ); + pTEParaPortion->MarkSelectionInvalid( 0 ); + + mbFormatted = false; + + IdleFormatAndUpdate( nullptr, 0xFFFF ); + } +} + +void TextEngine::SetAttrib( const TextAttrib& rAttr, sal_uInt32 nPara, sal_Int32 nStart, sal_Int32 nEnd ) +{ + + // For now do not check if Attributes overlap! + // This function is for TextEditors that want to _quickly_ generate the Syntax-Highlight + + // As TextEngine is currently intended only for TextEditors, there is no Undo for Attributes! + + if ( nPara >= mpDoc->GetNodes().size() ) + return; + + TextNode* pNode = mpDoc->GetNodes()[ nPara ].get(); + TEParaPortion* pTEParaPortion = mpTEParaPortions->GetObject( nPara ); + + const sal_Int32 nMax = pNode->GetText().getLength(); + if ( nStart > nMax ) + nStart = nMax; + if ( nEnd > nMax ) + nEnd = nMax; + + pNode->GetCharAttribs().InsertAttrib( std::make_unique<TextCharAttrib>( rAttr, nStart, nEnd ) ); + pTEParaPortion->MarkSelectionInvalid( nStart ); + + mbFormatted = false; + IdleFormatAndUpdate( nullptr, 0xFFFF ); +} + +void TextEngine::SetTextAlign( TxtAlign eAlign ) +{ + if ( eAlign != meAlign ) + { + meAlign = eAlign; + FormatFullDoc(); + UpdateViews(); + } +} + +void TextEngine::ValidateSelection( TextSelection& rSel ) const +{ + ValidatePaM( rSel.GetStart() ); + ValidatePaM( rSel.GetEnd() ); +} + +void TextEngine::ValidatePaM( TextPaM& rPaM ) const +{ + const sal_uInt32 nParas = static_cast<sal_uInt32>(mpDoc->GetNodes().size()); + if ( rPaM.GetPara() >= nParas ) + { + rPaM.GetPara() = nParas ? nParas-1 : 0; + rPaM.GetIndex() = TEXT_INDEX_ALL; + } + + const sal_Int32 nMaxIndex = GetTextLen( rPaM.GetPara() ); + if ( rPaM.GetIndex() > nMaxIndex ) + rPaM.GetIndex() = nMaxIndex; +} + +// adjust State & Selection + +void TextEngine::ImpParagraphInserted( sal_uInt32 nPara ) +{ + // No adjustment needed for the active View; + // but for all passive Views the Selection needs adjusting. + if ( mpViews->size() > 1 ) + { + for ( auto nView = mpViews->size(); nView; ) + { + TextView* pView = (*mpViews)[ --nView ]; + if ( pView != GetActiveView() ) + { + for ( int n = 0; n <= 1; n++ ) + { + TextPaM& rPaM = n ? pView->GetSelection().GetStart(): pView->GetSelection().GetEnd(); + if ( rPaM.GetPara() >= nPara ) + rPaM.GetPara()++; + } + } + } + } + Broadcast( TextHint( SfxHintId::TextParaInserted, nPara ) ); +} + +void TextEngine::ImpParagraphRemoved( sal_uInt32 nPara ) +{ + if ( mpViews->size() > 1 ) + { + for ( auto nView = mpViews->size(); nView; ) + { + TextView* pView = (*mpViews)[ --nView ]; + if ( pView != GetActiveView() ) + { + const sal_uInt32 nParas = static_cast<sal_uInt32>(mpDoc->GetNodes().size()); + for ( int n = 0; n <= 1; n++ ) + { + TextPaM& rPaM = n ? pView->GetSelection().GetStart(): pView->GetSelection().GetEnd(); + if ( rPaM.GetPara() > nPara ) + rPaM.GetPara()--; + else if ( rPaM.GetPara() == nPara ) + { + rPaM.GetIndex() = 0; + if ( rPaM.GetPara() >= nParas ) + rPaM.GetPara()--; + } + } + } + } + } + Broadcast( TextHint( SfxHintId::TextParaRemoved, nPara ) ); +} + +void TextEngine::ImpCharsRemoved( sal_uInt32 nPara, sal_Int32 nPos, sal_Int32 nChars ) +{ + if ( mpViews->size() > 1 ) + { + for ( auto nView = mpViews->size(); nView; ) + { + TextView* pView = (*mpViews)[ --nView ]; + if ( pView != GetActiveView() ) + { + const sal_Int32 nEnd = nPos + nChars; + for ( int n = 0; n <= 1; n++ ) + { + TextPaM& rPaM = n ? pView->GetSelection().GetStart(): pView->GetSelection().GetEnd(); + if ( rPaM.GetPara() == nPara ) + { + if ( rPaM.GetIndex() > nEnd ) + rPaM.GetIndex() = rPaM.GetIndex() - nChars; + else if ( rPaM.GetIndex() > nPos ) + rPaM.GetIndex() = nPos; + } + } + } + } + } + Broadcast( TextHint( SfxHintId::TextParaContentChanged, nPara ) ); +} + +void TextEngine::ImpCharsInserted( sal_uInt32 nPara, sal_Int32 nPos, sal_Int32 nChars ) +{ + if ( mpViews->size() > 1 ) + { + for ( auto nView = mpViews->size(); nView; ) + { + TextView* pView = (*mpViews)[ --nView ]; + if ( pView != GetActiveView() ) + { + for ( int n = 0; n <= 1; n++ ) + { + TextPaM& rPaM = n ? pView->GetSelection().GetStart(): pView->GetSelection().GetEnd(); + if ( rPaM.GetPara() == nPara ) + { + if ( rPaM.GetIndex() >= nPos ) + rPaM.GetIndex() += nChars; + } + } + } + } + } + Broadcast( TextHint( SfxHintId::TextParaContentChanged, nPara ) ); +} + +void TextEngine::Draw( OutputDevice* pDev, const Point& rPos ) +{ + ImpPaint( pDev, rPos, nullptr ); +} + +void TextEngine::SetLeftMargin( sal_uInt16 n ) +{ + mpDoc->SetLeftMargin( n ); +} + +uno::Reference< i18n::XBreakIterator > const & TextEngine::GetBreakIterator() +{ + if ( !mxBreakIterator.is() ) + mxBreakIterator = vcl::unohelper::CreateBreakIterator(); + SAL_WARN_IF( !mxBreakIterator.is(), "vcl", "BreakIterator: Failed to create!" ); + return mxBreakIterator; +} + +void TextEngine::SetLocale( const css::lang::Locale& rLocale ) +{ + maLocale = rLocale; + mpLocaleDataWrapper.reset(); +} + +css::lang::Locale const & TextEngine::GetLocale() +{ + if ( maLocale.Language.isEmpty() ) + { + maLocale = Application::GetSettings().GetUILanguageTag().getLocale(); // TODO: why UI locale? + } + return maLocale; +} + +LocaleDataWrapper* TextEngine::ImpGetLocaleDataWrapper() +{ + if ( !mpLocaleDataWrapper ) + mpLocaleDataWrapper.reset( new LocaleDataWrapper( LanguageTag( GetLocale()) ) ); + + return mpLocaleDataWrapper.get(); +} + +void TextEngine::SetRightToLeft( bool bR2L ) +{ + if ( mbRightToLeft != bR2L ) + { + mbRightToLeft = bR2L; + meAlign = bR2L ? TxtAlign::Right : TxtAlign::Left; + FormatFullDoc(); + UpdateViews(); + } +} + +void TextEngine::ImpInitWritingDirections( sal_uInt32 nPara ) +{ + TEParaPortion* pParaPortion = mpTEParaPortions->GetObject( nPara ); + std::vector<TEWritingDirectionInfo>& rInfos = pParaPortion->GetWritingDirectionInfos(); + rInfos.clear(); + + if ( !pParaPortion->GetNode()->GetText().isEmpty() ) + { + const UBiDiLevel nBidiLevel = IsRightToLeft() ? 1 /*RTL*/ : 0 /*LTR*/; + OUString aText( pParaPortion->GetNode()->GetText() ); + + // Bidi functions from icu 2.0 + + UErrorCode nError = U_ZERO_ERROR; + UBiDi* pBidi = ubidi_openSized( aText.getLength(), 0, &nError ); + nError = U_ZERO_ERROR; + + ubidi_setPara( pBidi, reinterpret_cast<const UChar *>(aText.getStr()), aText.getLength(), nBidiLevel, nullptr, &nError ); + nError = U_ZERO_ERROR; + + tools::Long nCount = ubidi_countRuns( pBidi, &nError ); + + int32_t nStart = 0; + int32_t nEnd; + UBiDiLevel nCurrDir; + + for ( tools::Long nIdx = 0; nIdx < nCount; ++nIdx ) + { + ubidi_getLogicalRun( pBidi, nStart, &nEnd, &nCurrDir ); + // bit 0 of nCurrDir indicates direction + rInfos.emplace_back( /*bLeftToRight*/ nCurrDir % 2 == 0, nStart, nEnd ); + nStart = nEnd; + } + + ubidi_close( pBidi ); + } + + // No infos mean no CTL and default dir is L2R... + if ( rInfos.empty() ) + rInfos.emplace_back( 0, 0, pParaPortion->GetNode()->GetText().getLength() ); + +} + +bool TextEngine::ImpGetRightToLeft( sal_uInt32 nPara, sal_Int32 nPos ) +{ + bool bRightToLeft = false; + + TextNode* pNode = mpDoc->GetNodes()[ nPara ].get(); + if ( pNode && !pNode->GetText().isEmpty() ) + { + TEParaPortion* pParaPortion = mpTEParaPortions->GetObject( nPara ); + if ( pParaPortion->GetWritingDirectionInfos().empty() ) + ImpInitWritingDirections( nPara ); + + std::vector<TEWritingDirectionInfo>& rDirInfos = pParaPortion->GetWritingDirectionInfos(); + for ( const auto& rWritingDirectionInfo : rDirInfos ) + { + if ( rWritingDirectionInfo.nStartPos <= nPos && rWritingDirectionInfo.nEndPos >= nPos ) + { + bRightToLeft = !rWritingDirectionInfo.bLeftToRight; + break; + } + } + } + return bRightToLeft; +} + +tools::Long TextEngine::ImpGetPortionXOffset( sal_uInt32 nPara, TextLine const * pLine, std::size_t nTextPortion ) +{ + tools::Long nX = pLine->GetStartX(); + + TEParaPortion* pParaPortion = mpTEParaPortions->GetObject( nPara ); + + for ( std::size_t i = pLine->GetStartPortion(); i < nTextPortion; i++ ) + { + TETextPortion& rPortion = pParaPortion->GetTextPortions()[ i ]; + nX += rPortion.GetWidth(); + } + + TETextPortion& rDestPortion = pParaPortion->GetTextPortions()[ nTextPortion ]; + if ( rDestPortion.GetKind() != PORTIONKIND_TAB ) + { + if ( !IsRightToLeft() && rDestPortion.IsRightToLeft() ) + { + // Portions behind must be added, visual before this portion + std::size_t nTmpPortion = nTextPortion+1; + while ( nTmpPortion <= pLine->GetEndPortion() ) + { + TETextPortion& rNextTextPortion = pParaPortion->GetTextPortions()[ nTmpPortion ]; + if ( rNextTextPortion.IsRightToLeft() && ( rNextTextPortion.GetKind() != PORTIONKIND_TAB ) ) + nX += rNextTextPortion.GetWidth(); + else + break; + nTmpPortion++; + } + // Portions before must be removed, visual behind this portion + nTmpPortion = nTextPortion; + while ( nTmpPortion > pLine->GetStartPortion() ) + { + --nTmpPortion; + TETextPortion& rPrevTextPortion = pParaPortion->GetTextPortions()[ nTmpPortion ]; + if ( rPrevTextPortion.IsRightToLeft() && ( rPrevTextPortion.GetKind() != PORTIONKIND_TAB ) ) + nX -= rPrevTextPortion.GetWidth(); + else + break; + } + } + else if ( IsRightToLeft() && !rDestPortion.IsRightToLeft() ) + { + // Portions behind must be removed, visual behind this portion + std::size_t nTmpPortion = nTextPortion+1; + while ( nTmpPortion <= pLine->GetEndPortion() ) + { + TETextPortion& rNextTextPortion = pParaPortion->GetTextPortions()[ nTmpPortion ]; + if ( !rNextTextPortion.IsRightToLeft() && ( rNextTextPortion.GetKind() != PORTIONKIND_TAB ) ) + nX += rNextTextPortion.GetWidth(); + else + break; + nTmpPortion++; + } + // Portions before must be added, visual before this portion + nTmpPortion = nTextPortion; + while ( nTmpPortion > pLine->GetStartPortion() ) + { + --nTmpPortion; + TETextPortion& rPrevTextPortion = pParaPortion->GetTextPortions()[ nTmpPortion ]; + if ( !rPrevTextPortion.IsRightToLeft() && ( rPrevTextPortion.GetKind() != PORTIONKIND_TAB ) ) + nX -= rPrevTextPortion.GetWidth(); + else + break; + } + } + } + + return nX; +} + +void TextEngine::ImpInitLayoutMode( OutputDevice* pOutDev ) +{ + vcl::text::ComplexTextLayoutFlags nLayoutMode = pOutDev->GetLayoutMode(); + + nLayoutMode &= ~vcl::text::ComplexTextLayoutFlags(vcl::text::ComplexTextLayoutFlags::BiDiRtl | vcl::text::ComplexTextLayoutFlags::BiDiStrong ); + + pOutDev->SetLayoutMode( nLayoutMode ); +} + +TxtAlign TextEngine::ImpGetAlign() const +{ + TxtAlign eAlign = meAlign; + if ( IsRightToLeft() ) + { + if ( eAlign == TxtAlign::Left ) + eAlign = TxtAlign::Right; + else if ( eAlign == TxtAlign::Right ) + eAlign = TxtAlign::Left; + } + return eAlign; +} + +tools::Long TextEngine::ImpGetOutputOffset( sal_uInt32 nPara, TextLine* pLine, sal_Int32 nIndex, sal_Int32 nIndex2 ) +{ + TEParaPortion* pPortion = mpTEParaPortions->GetObject( nPara ); + + sal_Int32 nPortionStart {0}; + const std::size_t nPortion = pPortion->GetTextPortions().FindPortion( nIndex, nPortionStart, true ); + + TETextPortion& rTextPortion = pPortion->GetTextPortions()[ nPortion ]; + + tools::Long nX; + + if ( ( nIndex == nPortionStart ) && ( nIndex == nIndex2 ) ) + { + // Output of full portion, so we need portion x offset. + // Use ImpGetPortionXOffset, because GetXPos may deliver left or right position from portion, depending on R2L, L2R + nX = ImpGetPortionXOffset( nPara, pLine, nPortion ); + if ( IsRightToLeft() ) + { + nX = -nX - rTextPortion.GetWidth(); + } + } + else + { + nX = ImpGetXPos( nPara, pLine, nIndex, nIndex == nPortionStart ); + if ( nIndex2 != nIndex ) + { + const tools::Long nX2 = ImpGetXPos( nPara, pLine, nIndex2 ); + if ( ( !IsRightToLeft() && ( nX2 < nX ) ) || + ( IsRightToLeft() && ( nX2 > nX ) ) ) + { + nX = nX2; + } + } + if ( IsRightToLeft() ) + { + nX = -nX; + } + } + + return nX; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |