diff options
Diffstat (limited to 'vcl/source/edit')
-rw-r--r-- | vcl/source/edit/textdat2.hxx | 281 | ||||
-rw-r--r-- | vcl/source/edit/textdata.cxx | 345 | ||||
-rw-r--r-- | vcl/source/edit/textdoc.cxx | 536 | ||||
-rw-r--r-- | vcl/source/edit/textdoc.hxx | 126 | ||||
-rw-r--r-- | vcl/source/edit/texteng.cxx | 2894 | ||||
-rw-r--r-- | vcl/source/edit/textund2.hxx | 105 | ||||
-rw-r--r-- | vcl/source/edit/textundo.cxx | 331 | ||||
-rw-r--r-- | vcl/source/edit/textundo.hxx | 77 | ||||
-rw-r--r-- | vcl/source/edit/textview.cxx | 2238 | ||||
-rw-r--r-- | vcl/source/edit/txtattr.cxx | 93 | ||||
-rw-r--r-- | vcl/source/edit/vclmedit.cxx | 1509 | ||||
-rw-r--r-- | vcl/source/edit/xtextedt.cxx | 227 |
12 files changed, 8762 insertions, 0 deletions
diff --git a/vcl/source/edit/textdat2.hxx b/vcl/source/edit/textdat2.hxx new file mode 100644 index 0000000000..e0174848f7 --- /dev/null +++ b/vcl/source/edit/textdat2.hxx @@ -0,0 +1,281 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <vcl/seleng.hxx> +#include <vcl/cursor.hxx> +#include <vcl/idle.hxx> +#include <vcl/textdata.hxx> + +#include <cstddef> +#include <limits> +#include <vector> + +class TextNode; +class TextView; + +#define PORTIONKIND_TEXT 0 +#define PORTIONKIND_TAB 1 + +#define DELMODE_SIMPLE 0 +#define DELMODE_RESTOFWORD 1 +#define DELMODE_RESTOFCONTENT 2 + +#define DEL_LEFT 1 +#define DEL_RIGHT 2 +#define TRAVEL_X_DONTKNOW 0xFFFF +#define MAXCHARSINPARA 0x3FFF-CHARPOSGROW + +#define LINE_SEP 0x0A + +class TETextPortion +{ +private: + tools::Long nWidth; + sal_Int32 nLen; + sal_uInt8 nKind; + bool bRightToLeft; + +public: + TETextPortion( sal_Int32 nL ) + : nWidth {-1} + , nLen {nL} + , nKind {PORTIONKIND_TEXT} + , bRightToLeft {false} + {} + + sal_Int32& GetLen() { return nLen; } + sal_Int32 GetLen() const { return nLen; } + tools::Long& GetWidth() { return nWidth; } + sal_uInt8& GetKind() { return nKind; } + void SetRightToLeft(bool b) { bRightToLeft = b; } + bool IsRightToLeft() const { return bRightToLeft; } +}; + +class TETextPortionList +{ +private: + std::vector<TETextPortion> maPortions; + +public: + static constexpr auto npos = std::numeric_limits<std::size_t>::max(); + + TETextPortionList(); + ~TETextPortionList(); + + TETextPortion& operator[]( std::size_t nPos ); + std::vector<TETextPortion>::iterator begin(); + std::vector<TETextPortion>::const_iterator begin() const; + std::vector<TETextPortion>::iterator end(); + std::vector<TETextPortion>::const_iterator end() const; + bool empty() const; + std::size_t size() const; + std::vector<TETextPortion>::iterator erase( const std::vector<TETextPortion>::iterator& aIter ); + std::vector<TETextPortion>::iterator insert( const std::vector<TETextPortion>::iterator& aIter, + const TETextPortion& rTP ); + void push_back( const TETextPortion & aTP ); + + void Reset(); + std::size_t FindPortion( sal_Int32 nCharPos, sal_Int32& rPortionStart, bool bPreferStartingPortion = false ); + void DeleteFromPortion( std::size_t nDelFrom ); +}; + +struct TEWritingDirectionInfo +{ + bool bLeftToRight; + sal_Int32 nStartPos; + sal_Int32 nEndPos; + TEWritingDirectionInfo( bool LeftToRight, sal_Int32 Start, sal_Int32 End ) + : bLeftToRight {LeftToRight} + , nStartPos {Start} + , nEndPos {End} + {} +}; + +class TextLine +{ +private: + sal_Int32 mnStart; + sal_Int32 mnEnd; + std::size_t mnStartPortion; + std::size_t mnEndPortion; + + short mnStartX; + + bool mbInvalid; // for clever formatting/output + +public: + TextLine() + : mnStart {0} + , mnEnd {0} + , mnStartPortion {0} + , mnEndPortion {0} + , mnStartX {0} + , mbInvalid {true} + {} + + bool IsIn( sal_Int32 nIndex, bool bInclEnd ) const + { return nIndex >= mnStart && ( bInclEnd ? nIndex <= mnEnd : nIndex < mnEnd ); } + + void SetStart( sal_Int32 n ) { mnStart = n; } + sal_Int32 GetStart() const { return mnStart; } + + void SetEnd( sal_Int32 n ) { mnEnd = n; } + sal_Int32 GetEnd() const { return mnEnd; } + + void SetStartPortion( std::size_t n ) { mnStartPortion = n; } + std::size_t GetStartPortion() const { return mnStartPortion; } + + void SetEndPortion( std::size_t n ) { mnEndPortion = n; } + std::size_t GetEndPortion() const { return mnEndPortion; } + + sal_Int32 GetLen() const { return mnEnd - mnStart; } + + bool IsInvalid() const { return mbInvalid; } + bool IsValid() const { return !mbInvalid; } + void SetInvalid() { mbInvalid = true; } + void SetValid() { mbInvalid = false; } + + short GetStartX() const { return mnStartX; } + void SetStartX( short n ) { mnStartX = n; } + + inline bool operator == ( const TextLine& rLine ) const; +}; + +inline bool TextLine::operator == ( const TextLine& rLine ) const +{ + return mnStart == rLine.mnStart && + mnEnd == rLine.mnEnd && + mnStartPortion == rLine.mnStartPortion && + mnEndPortion == rLine.mnEndPortion; +} + +class TEParaPortion +{ +private: + TextNode* mpNode; + + std::vector<TextLine> maLines; + TETextPortionList maTextPortions; + std::vector<TEWritingDirectionInfo> maWritingDirectionInfos; + + sal_Int32 mnInvalidPosStart; + sal_Int32 mnInvalidDiff; + + bool mbInvalid; + bool mbSimple; // only type linearly + +public: + TEParaPortion( TextNode* pNode ); + ~TEParaPortion(); + + TEParaPortion( const TEParaPortion& ) = delete; + void operator=( const TEParaPortion& ) = delete; + + bool IsInvalid() const { return mbInvalid; } + bool IsSimpleInvalid() const { return mbSimple; } + void SetNotSimpleInvalid() { mbSimple = false; } + void SetValid() { mbInvalid = false; mbSimple = true;} + + void MarkInvalid( sal_Int32 nStart, sal_Int32 nDiff ); + void MarkSelectionInvalid( sal_Int32 nStart ); + + sal_Int32 GetInvalidPosStart() const { return mnInvalidPosStart; } + sal_Int32 GetInvalidDiff() const { return mnInvalidDiff; } + + TextNode* GetNode() const { return mpNode; } + std::vector<TextLine>& GetLines() { return maLines; } + TETextPortionList& GetTextPortions() { return maTextPortions; } + std::vector<TEWritingDirectionInfo>& GetWritingDirectionInfos() { return maWritingDirectionInfos; } + + std::vector<TextLine>::size_type GetLineNumber( sal_Int32 nIndex, bool bInclEnd ); + void CorrectValuesBehindLastFormattedLine( sal_uInt16 nLastFormattedLine ); +}; + +class TEParaPortions +{ +private: + std::vector<std::unique_ptr<TEParaPortion>> mvData; + +public: + TEParaPortions() : mvData() {} + ~TEParaPortions(); + + sal_uInt32 Count() const { return static_cast<sal_uInt32>(mvData.size()); } + TEParaPortion* GetObject( sal_uInt32 nIndex ) { return mvData[nIndex].get(); } + void Insert( TEParaPortion* pObject, sal_uInt32 nPos ) { mvData.emplace( mvData.begin()+nPos, pObject ); } + void Remove( sal_uInt32 nPos ) { mvData.erase( mvData.begin()+nPos ); } +}; + +class TextSelFunctionSet: public FunctionSet +{ +private: + TextView* mpView; + +public: + TextSelFunctionSet( TextView* pView ); + + virtual void BeginDrag() override; + + virtual void CreateAnchor() override; + + virtual void SetCursorAtPoint( const Point& rPointPixel, bool bDontSelectAtCursor = false ) override; + + virtual bool IsSelectionAtPoint( const Point& rPointPixel ) override; + virtual void DeselectAll() override; + + virtual void DeselectAtPoint( const Point& ) override; + virtual void DestroyAnchor() override; +}; + +class IdleFormatter : public Idle +{ +private: + TextView* mpView; + sal_uInt16 mnRestarts; + +public: + IdleFormatter(); + virtual ~IdleFormatter() override; + + void DoIdleFormat( TextView* pV, sal_uInt16 nMaxRestarts ); + void ForceTimeout(); + TextView* GetView() { return mpView; } +}; + +struct TextDDInfo +{ + vcl::Cursor maCursor; + TextPaM maDropPos; + + bool mbStarterOfDD; + bool mbVisCursor; + + TextDDInfo() + : maCursor() + , maDropPos() + , mbStarterOfDD {false} + , mbVisCursor {false} + { + maCursor.SetStyle( CURSOR_SHADOW ); + } +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/textdata.cxx b/vcl/source/edit/textdata.cxx new file mode 100644 index 0000000000..bf29c1443d --- /dev/null +++ b/vcl/source/edit/textdata.cxx @@ -0,0 +1,345 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> +#include <sal/log.hxx> +#include <osl/diagnose.h> + +#include <cstddef> + +#include <utility> +#include <vcl/textdata.hxx> +#include "textdat2.hxx" + + +TextSelection::TextSelection() +{ +} + +TextSelection::TextSelection( const TextPaM& rPaM ) : + maStartPaM( rPaM ), maEndPaM( rPaM ) +{ +} + +TextSelection::TextSelection( const TextPaM& rStart, const TextPaM& rEnd ) : + maStartPaM( rStart ), maEndPaM( rEnd ) +{ +} + +void TextSelection::Justify() +{ + if ( maEndPaM < maStartPaM ) + { + TextPaM aTemp( maStartPaM ); + maStartPaM = maEndPaM; + maEndPaM = aTemp; + } +} + +TETextPortionList::TETextPortionList() +{ +} + +TETextPortionList::~TETextPortionList() +{ + Reset(); +} + +TETextPortion& TETextPortionList::operator[]( std::size_t nPos ) +{ + return maPortions[ nPos ]; +} + +std::vector<TETextPortion>::iterator TETextPortionList::begin() +{ + return maPortions.begin(); +} + +std::vector<TETextPortion>::const_iterator TETextPortionList::begin() const +{ + return maPortions.begin(); +} + +std::vector<TETextPortion>::iterator TETextPortionList::end() +{ + return maPortions.end(); +} + +std::vector<TETextPortion>::const_iterator TETextPortionList::end() const +{ + return maPortions.end(); +} + +bool TETextPortionList::empty() const +{ + return maPortions.empty(); +} + +std::size_t TETextPortionList::size() const +{ + return maPortions.size(); +} + +std::vector<TETextPortion>::iterator TETextPortionList::erase( const std::vector<TETextPortion>::iterator& aIter ) +{ + return maPortions.erase( aIter ); +} + +std::vector<TETextPortion>::iterator TETextPortionList::insert( const std::vector<TETextPortion>::iterator& aIter, + const TETextPortion& rTP ) +{ + return maPortions.insert( aIter, rTP ); +} + +void TETextPortionList::push_back( const TETextPortion& rTP ) +{ + maPortions.push_back( rTP ); +} + +void TETextPortionList::Reset() +{ + maPortions.clear(); +} + +void TETextPortionList::DeleteFromPortion( std::size_t nDelFrom ) +{ + SAL_WARN_IF( ( nDelFrom >= maPortions.size() ) && ( (nDelFrom != 0) || (!maPortions.empty()) ), "vcl", "DeleteFromPortion: Out of range" ); + maPortions.erase( maPortions.begin() + nDelFrom, maPortions.end() ); +} + +std::size_t TETextPortionList::FindPortion( sal_Int32 nCharPos, sal_Int32& nPortionStart, bool bPreferStartingPortion ) +{ + // find left portion at nCharPos at portion border + sal_Int32 nTmpPos = 0; + for ( std::size_t nPortion = 0; nPortion < maPortions.size(); nPortion++ ) + { + TETextPortion& rPortion = maPortions[ nPortion ]; + nTmpPos += rPortion.GetLen(); + if ( nTmpPos >= nCharPos ) + { + // take this one if we don't prefer the starting portion, or if it's the last one + if ( ( nTmpPos != nCharPos ) || !bPreferStartingPortion || ( nPortion == maPortions.size() - 1 ) ) + { + nPortionStart = nTmpPos - rPortion.GetLen(); + return nPortion; + } + } + } + OSL_FAIL( "FindPortion: Not found!" ); + return ( maPortions.size() - 1 ); +} + +TEParaPortion::TEParaPortion( TextNode* pN ) + : mpNode {pN} + , mnInvalidPosStart {0} + , mnInvalidDiff {0} + , mbInvalid {true} + , mbSimple {false} +{ +} + +TEParaPortion::~TEParaPortion() +{ +} + +void TEParaPortion::MarkInvalid( sal_Int32 nStart, sal_Int32 nDiff ) +{ + if ( !mbInvalid ) + { + mnInvalidPosStart = ( nDiff >= 0 ) ? nStart : ( nStart + nDiff ); + mnInvalidDiff = nDiff; + } + else + { + // simple consecutive typing + if ( ( nDiff > 0 ) && ( mnInvalidDiff > 0 ) && + ( ( mnInvalidPosStart+mnInvalidDiff ) == nStart ) ) + { + mnInvalidDiff = mnInvalidDiff + nDiff; + } + // simple consecutive deleting + else if ( ( nDiff < 0 ) && ( mnInvalidDiff < 0 ) && ( mnInvalidPosStart == nStart ) ) + { + mnInvalidPosStart = mnInvalidPosStart + nDiff; + mnInvalidDiff = mnInvalidDiff + nDiff; + } + else + { + SAL_WARN_IF( ( nDiff < 0 ) && ( (nStart+nDiff) < 0 ), "vcl", "MarkInvalid: Diff out of Range" ); + mnInvalidPosStart = std::min( mnInvalidPosStart, nDiff < 0 ? nStart+nDiff : nDiff ); + mnInvalidDiff = 0; + mbSimple = false; + } + } + + maWritingDirectionInfos.clear(); + + mbInvalid = true; +} + +void TEParaPortion::MarkSelectionInvalid( sal_Int32 nStart ) +{ + if ( !mbInvalid ) + { + mnInvalidPosStart = nStart; + } + else + { + mnInvalidPosStart = std::min( mnInvalidPosStart, nStart ); + } + + maWritingDirectionInfos.clear(); + + mnInvalidDiff = 0; + mbInvalid = true; + mbSimple = false; +} + +std::vector<TextLine>::size_type TEParaPortion::GetLineNumber( sal_Int32 nChar, bool bInclEnd ) +{ + for ( std::vector<TextLine>::size_type nLine = 0; nLine < maLines.size(); nLine++ ) + { + TextLine& rLine = maLines[ nLine ]; + if ( ( bInclEnd && ( rLine.GetEnd() >= nChar ) ) || + ( rLine.GetEnd() > nChar ) ) + { + return nLine; + } + } + + // Then it should be at the end of the last line + OSL_ENSURE(nChar == maLines.back().GetEnd(), "wrong Index"); + OSL_ENSURE(!bInclEnd, "Line not found: FindLine"); + return ( maLines.size() - 1 ); +} + +void TEParaPortion::CorrectValuesBehindLastFormattedLine( sal_uInt16 nLastFormattedLine ) +{ + sal_uInt16 nLines = maLines.size(); + SAL_WARN_IF( !nLines, "vcl", "CorrectPortionNumbersFromLine: Empty portion?" ); + if ( nLastFormattedLine >= ( nLines - 1 ) ) + return; + + const TextLine& rLastFormatted = maLines[ nLastFormattedLine ]; + const TextLine& rUnformatted = maLines[ nLastFormattedLine+1 ]; + std::ptrdiff_t nPortionDiff = rUnformatted.GetStartPortion() - rLastFormatted.GetEndPortion(); + sal_Int32 nTextDiff = rUnformatted.GetStart() - rLastFormatted.GetEnd(); + nTextDiff++; // LastFormatted.GetEnd() was inclusive => subtracted one too much! + + // The first unformatted one has to start exactly one portion past the last + // formatted one. + // If a portion got split in the changed row, nLastEnd could be > nNextStart! + std::ptrdiff_t nPDiff = -( nPortionDiff-1 ); + const sal_Int32 nTDiff = -( nTextDiff-1 ); + if ( !(nPDiff || nTDiff) ) + return; + + for ( sal_uInt16 nL = nLastFormattedLine+1; nL < nLines; nL++ ) + { + TextLine& rLine = maLines[ nL ]; + + rLine.SetStartPortion(rLine.GetStartPortion() + nPDiff); + rLine.SetEndPortion(rLine.GetEndPortion() + nPDiff); + + rLine.SetStart(rLine.GetStart() + nTDiff); + rLine.SetEnd(rLine.GetEnd() + nTDiff); + + rLine.SetValid(); + } +} + +TEParaPortions::~TEParaPortions() +{ +} + +IdleFormatter::IdleFormatter() + : Idle("vcl::TextEngine mpIdleFormatter") + , mpView(nullptr) + , mnRestarts(0) +{ + SetPriority(TaskPriority::HIGH_IDLE); +} + +IdleFormatter::~IdleFormatter() +{ + mpView = nullptr; +} + +void IdleFormatter::DoIdleFormat( TextView* pV, sal_uInt16 nMaxRestarts ) +{ + mpView = pV; + + if ( IsActive() ) + mnRestarts++; + + if ( mnRestarts > nMaxRestarts ) + { + mnRestarts = 0; + Invoke(); + } + else + { + Start(); + } +} + +void IdleFormatter::ForceTimeout() +{ + if ( IsActive() ) + { + Stop(); + mnRestarts = 0; + Invoke(); + } +} + +TextHint::TextHint( SfxHintId Id ) : SfxHint( Id ), mnValue(0) +{ +} + +TextHint::TextHint( SfxHintId Id, sal_Int32 nValue ) : SfxHint( Id ), mnValue(nValue) +{ +} + +TEIMEInfos::TEIMEInfos( const TextPaM& rPos, OUString _aOldTextAfterStartPos ) + : aOldTextAfterStartPos(std::move(_aOldTextAfterStartPos)) + , aPos(rPos) + , nLen(0) + , bWasCursorOverwrite(false) +{ +} + +TEIMEInfos::~TEIMEInfos() +{ +} + +void TEIMEInfos::CopyAttribs(const ExtTextInputAttr* pA, sal_Int32 nL) +{ + nLen = nL; + pAttribs.reset( new ExtTextInputAttr[ nL ] ); + memcpy( pAttribs.get(), pA, nL*sizeof(ExtTextInputAttr) ); +} + +void TEIMEInfos::DestroyAttribs() +{ + pAttribs.reset(); + nLen = 0; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/textdoc.cxx b/vcl/source/edit/textdoc.cxx new file mode 100644 index 0000000000..a6d7bd8c24 --- /dev/null +++ b/vcl/source/edit/textdoc.cxx @@ -0,0 +1,536 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <memory> +#include "textdoc.hxx" +#include <osl/diagnose.h> +#include <sal/log.hxx> +#include <rtl/ustrbuf.hxx> +#include <utility> + +// compare function called by QuickSort +static bool CompareStart( const std::unique_ptr<TextCharAttrib>& pFirst, const std::unique_ptr<TextCharAttrib>& pSecond ) +{ + return pFirst->GetStart() < pSecond->GetStart(); +} + +TextCharAttrib::TextCharAttrib( const TextAttrib& rAttr, sal_Int32 nStart, sal_Int32 nEnd ) + : mpAttr(rAttr.Clone()) + , mnStart(nStart) + , mnEnd(nEnd) +{ +} + +TextCharAttrib::TextCharAttrib( const TextCharAttrib& rTextCharAttrib ) + : mpAttr(rTextCharAttrib.mpAttr->Clone()) + , mnStart(rTextCharAttrib.mnStart) + , mnEnd(rTextCharAttrib.mnEnd) +{ +} + +TextCharAttribList::TextCharAttribList() + : mbHasEmptyAttribs(false) +{ +} + +TextCharAttribList::~TextCharAttribList() +{ + // PTRARR_DEL +} + +void TextCharAttribList::Clear() +{ + maAttribs.clear(); +} + +void TextCharAttribList::InsertAttrib( std::unique_ptr<TextCharAttrib> pAttrib ) +{ + if ( pAttrib->IsEmpty() ) + mbHasEmptyAttribs = true; + + const sal_Int32 nStart = pAttrib->GetStart(); // maybe better for Comp.Opt. + bool bInserted = false; + auto it = std::find_if(maAttribs.begin(), maAttribs.end(), + [nStart](std::unique_ptr<TextCharAttrib>& rAttrib) { return rAttrib->GetStart() > nStart; }); + if (it != maAttribs.end()) + { + maAttribs.insert( it, std::move(pAttrib) ); + bInserted = true; + } + if ( !bInserted ) + maAttribs.push_back( std::move(pAttrib) ); +} + +void TextCharAttribList::ResortAttribs() +{ + std::sort( maAttribs.begin(), maAttribs.end(), CompareStart ); +} + +TextCharAttrib* TextCharAttribList::FindAttrib( sal_uInt16 nWhich, sal_Int32 nPos ) +{ + for (std::vector<std::unique_ptr<TextCharAttrib> >::reverse_iterator it = maAttribs.rbegin(); it != maAttribs.rend(); ++it) + { + if ( (*it)->GetEnd() < nPos ) + return nullptr; + + if ( ( (*it)->Which() == nWhich ) && (*it)->IsIn(nPos) ) + return it->get(); + } + return nullptr; +} + +bool TextCharAttribList::HasBoundingAttrib( sal_Int32 nBound ) +{ + for (std::vector<std::unique_ptr<TextCharAttrib> >::reverse_iterator it = maAttribs.rbegin(); it != maAttribs.rend(); ++it) + { + if ( (*it)->GetEnd() < nBound ) + return false; + + if ( ( (*it)->GetStart() == nBound ) || ( (*it)->GetEnd() == nBound ) ) + return true; + } + return false; +} + +TextCharAttrib* TextCharAttribList::FindEmptyAttrib( sal_uInt16 nWhich, sal_Int32 nPos ) +{ + if ( !mbHasEmptyAttribs ) + return nullptr; + + for (auto const& attrib : maAttribs) + { + if ( attrib->GetStart() > nPos ) + return nullptr; + + if ( ( attrib->GetStart() == nPos ) && ( attrib->GetEnd() == nPos ) && ( attrib->Which() == nWhich ) ) + return attrib.get(); + } + return nullptr; +} + +void TextCharAttribList::DeleteEmptyAttribs() +{ + std::erase_if( + maAttribs, + [] (const std::unique_ptr<TextCharAttrib>& rAttrib) { return rAttrib->IsEmpty(); } ); + mbHasEmptyAttribs = false; +} + +TextNode::TextNode( OUString aText ) : + maText(std::move( aText )) +{ +} + +void TextNode::ExpandAttribs( sal_Int32 nIndex, sal_Int32 nNew ) +{ + if ( !nNew ) + return; + + bool bResort = false; + sal_uInt16 nAttribs = maCharAttribs.Count(); + for ( sal_uInt16 nAttr = 0; nAttr < nAttribs; nAttr++ ) + { + TextCharAttrib& rAttrib = maCharAttribs.GetAttrib( nAttr ); + if ( rAttrib.GetEnd() >= nIndex ) + { + // move all attributes that are behind the cursor + if ( rAttrib.GetStart() > nIndex ) + { + rAttrib.MoveForward( nNew ); + } + // 0: expand empty attribute, if at cursor + else if ( rAttrib.IsEmpty() ) + { + // Do not check the index; empty one may only be here. + // If checking later anyway, special case: + // Start == 0; AbsLen == 1, nNew = 1 => Expand due to new paragraph! + // Start <= nIndex, End >= nIndex => Start=End=nIndex! + rAttrib.Expand( nNew ); + } + // 1: attribute starts before and reaches up to index + else if ( rAttrib.GetEnd() == nIndex ) // start must be before + { + // Only expand if no feature and not in Exclude list! + // Otherwise e.g. an UL would go until the new ULDB, thus expand both. + if ( !maCharAttribs.FindEmptyAttrib( rAttrib.Which(), nIndex ) ) + { + rAttrib.Expand( nNew ); + } + else + bResort = true; + } + // 2: attribute starts before and reaches past the index + else if ( ( rAttrib.GetStart() < nIndex ) && ( rAttrib.GetEnd() > nIndex ) ) + { + rAttrib.Expand( nNew ); + } + // 3: attribute starts at Index + else if ( rAttrib.GetStart() == nIndex ) + { + if ( nIndex == 0 ) + { + rAttrib.Expand( nNew ); + } + else + rAttrib.MoveForward( nNew ); + } + } + + SAL_WARN_IF( rAttrib.GetStart() > rAttrib.GetEnd(), "vcl", "Expand: attribute twisted!" ); + SAL_WARN_IF( ( rAttrib.GetEnd() > maText.getLength() ), "vcl", "Expand: attribute greater than paragraph!" ); + SAL_WARN_IF( rAttrib.IsEmpty(), "vcl", "Empty attribute after ExpandAttribs?" ); + } + + if ( bResort ) + maCharAttribs.ResortAttribs(); +} + +void TextNode::CollapseAttribs( sal_Int32 nIndex, sal_Int32 nDeleted ) +{ + if ( !nDeleted ) + return; + + bool bResort = false; + const sal_Int32 nEndChanges = nIndex+nDeleted; + + for ( sal_uInt16 nAttr = 0; nAttr < maCharAttribs.Count(); nAttr++ ) + { + TextCharAttrib& rAttrib = maCharAttribs.GetAttrib( nAttr ); + bool bDelAttr = false; + if ( rAttrib.GetEnd() >= nIndex ) + { + // move all attributes that are behind the cursor + if ( rAttrib.GetStart() >= nEndChanges ) + { + rAttrib.MoveBackward( nDeleted ); + } + // 1. delete inner attributes + else if ( ( rAttrib.GetStart() >= nIndex ) && ( rAttrib.GetEnd() <= nEndChanges ) ) + { + // special case: attribute covers the region exactly + // => keep as an empty attribute + if ( ( rAttrib.GetStart() == nIndex ) && ( rAttrib.GetEnd() == nEndChanges ) ) + rAttrib.SetEnd(nIndex); // empty + else + bDelAttr = true; + } + // 2. attribute starts before, ends inside or after + else if ( ( rAttrib.GetStart() <= nIndex ) && ( rAttrib.GetEnd() > nIndex ) ) + { + if ( rAttrib.GetEnd() <= nEndChanges ) // ends inside + rAttrib.SetEnd(nIndex); + else + rAttrib.Collaps( nDeleted ); // ends after + } + // 3. attribute starts inside, ends after + else if ( ( rAttrib.GetStart() >= nIndex ) && ( rAttrib.GetEnd() > nEndChanges ) ) + { + // features are not allowed to expand! + rAttrib.SetStart(nEndChanges); + rAttrib.MoveBackward( nDeleted ); + } + } + + SAL_WARN_IF( rAttrib.GetStart() > rAttrib.GetEnd(), "vcl", "Collaps: attribute twisted!" ); + SAL_WARN_IF( ( rAttrib.GetEnd() > maText.getLength()) && !bDelAttr, "vcl", "Collaps: attribute greater than paragraph!" ); + if ( bDelAttr /* || rAttrib.IsEmpty() */ ) + { + bResort = true; + maCharAttribs.RemoveAttrib( nAttr ); + nAttr--; + } + else if ( rAttrib.IsEmpty() ) + maCharAttribs.HasEmptyAttribs() = true; + } + + if ( bResort ) + maCharAttribs.ResortAttribs(); +} + +void TextNode::InsertText( sal_Int32 nPos, std::u16string_view rText ) +{ + maText = maText.replaceAt( nPos, 0, rText ); + ExpandAttribs( nPos, rText.size() ); +} + +void TextNode::InsertText( sal_Int32 nPos, sal_Unicode c ) +{ + maText = maText.replaceAt( nPos, 0, rtl::OUStringChar(c) ); + ExpandAttribs( nPos, 1 ); +} + +void TextNode::RemoveText( sal_Int32 nPos, sal_Int32 nChars ) +{ + maText = maText.replaceAt( nPos, nChars, u"" ); + CollapseAttribs( nPos, nChars ); +} + +std::unique_ptr<TextNode> TextNode::Split( sal_Int32 nPos ) +{ + OUString aNewText; + if ( nPos < maText.getLength() ) + { + aNewText = maText.copy( nPos ); + maText = maText.copy(0, nPos); + } + std::unique_ptr<TextNode> pNew(new TextNode( aNewText )); + + for ( sal_uInt16 nAttr = 0; nAttr < maCharAttribs.Count(); nAttr++ ) + { + TextCharAttrib& rAttrib = maCharAttribs.GetAttrib( nAttr ); + if ( rAttrib.GetEnd() < nPos ) + { + // no change + ; + } + else if ( rAttrib.GetEnd() == nPos ) + { + // must be copied as an empty attribute + // !FindAttrib only sensible if traversing backwards through the list! + if ( !pNew->maCharAttribs.FindAttrib( rAttrib.Which(), 0 ) ) + { + std::unique_ptr<TextCharAttrib> pNewAttrib(new TextCharAttrib( rAttrib )); + pNewAttrib->SetStart(0); + pNewAttrib->SetEnd(0); + pNew->maCharAttribs.InsertAttrib( std::move(pNewAttrib) ); + } + } + else if ( rAttrib.IsInside( nPos ) || ( !nPos && !rAttrib.GetStart() ) ) + { + // If cutting at the very beginning, the attribute has to be + // copied and changed + std::unique_ptr<TextCharAttrib> pNewAttrib(new TextCharAttrib( rAttrib )); + pNewAttrib->SetStart(0); + pNewAttrib->SetEnd(rAttrib.GetEnd()-nPos); + pNew->maCharAttribs.InsertAttrib( std::move(pNewAttrib) ); + // trim + rAttrib.SetEnd(nPos); + } + else + { + SAL_WARN_IF( rAttrib.GetStart() < nPos, "vcl", "Start < nPos!" ); + SAL_WARN_IF( rAttrib.GetEnd() < nPos, "vcl", "End < nPos!" ); + // move all into the new node (this) + pNew->maCharAttribs.InsertAttrib(maCharAttribs.RemoveAttrib(nAttr)); + rAttrib.SetStart( rAttrib.GetStart() - nPos ); + rAttrib.SetEnd( rAttrib.GetEnd() - nPos ); + nAttr--; + } + } + return pNew; +} + +void TextNode::Append( const TextNode& rNode ) +{ + sal_Int32 nOldLen = maText.getLength(); + + maText += rNode.GetText(); + + const sal_uInt16 nAttribs = rNode.GetCharAttribs().Count(); + for ( sal_uInt16 nAttr = 0; nAttr < nAttribs; nAttr++ ) + { + const TextCharAttrib& rAttrib = rNode.GetCharAttrib( nAttr ); + bool bMelted = false; + if ( rAttrib.GetStart() == 0 ) + { + // potentially merge attributes + sal_uInt16 nTmpAttribs = maCharAttribs.Count(); + for ( sal_uInt16 nTmpAttr = 0; nTmpAttr < nTmpAttribs; nTmpAttr++ ) + { + TextCharAttrib& rTmpAttrib = maCharAttribs.GetAttrib( nTmpAttr ); + + if ( rTmpAttrib.GetEnd() == nOldLen ) + { + if ( ( rTmpAttrib.Which() == rAttrib.Which() ) && + ( rTmpAttrib.GetAttr() == rAttrib.GetAttr() ) ) + { + rTmpAttrib.SetEnd( rTmpAttrib.GetEnd() + rAttrib.GetLen() ); + bMelted = true; + break; // there can be only one of this type at this position + } + } + } + } + + if ( !bMelted ) + { + std::unique_ptr<TextCharAttrib> pNewAttrib(new TextCharAttrib( rAttrib )); + pNewAttrib->SetStart( pNewAttrib->GetStart() + nOldLen ); + pNewAttrib->SetEnd( pNewAttrib->GetEnd() + nOldLen ); + maCharAttribs.InsertAttrib( std::move(pNewAttrib) ); + } + } +} + +TextDoc::TextDoc() + : mnLeftMargin(0) +{ +}; + +TextDoc::~TextDoc() +{ + DestroyTextNodes(); +} + +void TextDoc::Clear() +{ + DestroyTextNodes(); +} + +void TextDoc::DestroyTextNodes() +{ + maTextNodes.clear(); +} + +OUString TextDoc::GetText( const sal_Unicode* pSep ) const +{ + sal_uInt32 nNodes = static_cast<sal_uInt32>(maTextNodes.size()); + + OUStringBuffer aASCIIText; + const sal_uInt32 nLastNode = nNodes-1; + for ( sal_uInt32 nNode = 0; nNode < nNodes; ++nNode ) + { + TextNode* pNode = maTextNodes[ nNode ].get(); + aASCIIText.append(pNode->GetText()); + if ( pSep && ( nNode != nLastNode ) ) + aASCIIText.append(pSep); + } + + return aASCIIText.makeStringAndClear(); +} + +OUString TextDoc::GetText( sal_uInt32 nPara ) const +{ + TextNode* pNode = ( nPara < maTextNodes.size() ) ? maTextNodes[ nPara ].get() : nullptr; + if ( pNode ) + return pNode->GetText(); + + return OUString(); +} + +sal_Int32 TextDoc::GetTextLen( const sal_Unicode* pSep, const TextSelection* pSel ) const +{ + sal_Int32 nLen = 0; + sal_uInt32 nNodes = static_cast<sal_uInt32>(maTextNodes.size()); + if ( nNodes ) + { + sal_uInt32 nStartNode = 0; + sal_uInt32 nEndNode = nNodes-1; + if ( pSel ) + { + nStartNode = pSel->GetStart().GetPara(); + nEndNode = pSel->GetEnd().GetPara(); + } + + for ( sal_uInt32 nNode = nStartNode; nNode <= nEndNode; ++nNode ) + { + TextNode* pNode = maTextNodes[ nNode ].get(); + + sal_Int32 nS = 0; + sal_Int32 nE = pNode->GetText().getLength(); + if ( pSel && ( nNode == pSel->GetStart().GetPara() ) ) + nS = pSel->GetStart().GetIndex(); + if ( pSel && ( nNode == pSel->GetEnd().GetPara() ) ) + nE = pSel->GetEnd().GetIndex(); + + nLen += ( nE - nS ); + } + + if ( pSep ) + nLen += (nEndNode-nStartNode) * rtl_ustr_getLength(pSep); + } + + return nLen; +} + +TextPaM TextDoc::InsertText( const TextPaM& rPaM, sal_Unicode c ) +{ + SAL_WARN_IF( c == 0x0A, "vcl", "TextDoc::InsertText: Line separator in paragraph not allowed!" ); + SAL_WARN_IF( c == 0x0D, "vcl", "TextDoc::InsertText: Line separator in paragraph not allowed!" ); + + TextNode* pNode = maTextNodes[ rPaM.GetPara() ].get(); + pNode->InsertText( rPaM.GetIndex(), c ); + + TextPaM aPaM( rPaM.GetPara(), rPaM.GetIndex()+1 ); + return aPaM; +} + +TextPaM TextDoc::InsertText( const TextPaM& rPaM, std::u16string_view rStr ) +{ + SAL_WARN_IF( rStr.find( 0x0A ) != std::u16string_view::npos, "vcl", "TextDoc::InsertText: Line separator in paragraph not allowed!" ); + SAL_WARN_IF( rStr.find( 0x0D ) != std::u16string_view::npos, "vcl", "TextDoc::InsertText: Line separator in paragraph not allowed!" ); + + TextNode* pNode = maTextNodes[ rPaM.GetPara() ].get(); + pNode->InsertText( rPaM.GetIndex(), rStr ); + + TextPaM aPaM( rPaM.GetPara(), rPaM.GetIndex()+rStr.size() ); + return aPaM; +} + +TextPaM TextDoc::InsertParaBreak( const TextPaM& rPaM ) +{ + TextNode* pNode = maTextNodes[ rPaM.GetPara() ].get(); + std::unique_ptr<TextNode> pNew = pNode->Split( rPaM.GetIndex() ); + + SAL_WARN_IF( maTextNodes.size()>=SAL_MAX_UINT32, "vcl", "InsertParaBreak: more than 4Gi paragraphs!" ); + maTextNodes.insert( maTextNodes.begin() + rPaM.GetPara() + 1, std::move(pNew) ); + + TextPaM aPaM( rPaM.GetPara()+1, 0 ); + return aPaM; +} + +TextPaM TextDoc::ConnectParagraphs( TextNode* pLeft, const TextNode* pRight ) +{ + sal_Int32 nPrevLen = pLeft->GetText().getLength(); + pLeft->Append( *pRight ); + + // the paragraph on the right vanishes + maTextNodes.erase( std::find_if( maTextNodes.begin(), maTextNodes.end(), + [&] (std::unique_ptr<TextNode> const & p) { return p.get() == pRight; } ) ); + + sal_Int32 nLeft = ::std::find_if( maTextNodes.begin(), maTextNodes.end(), + [&] (std::unique_ptr<TextNode> const & p) { return p.get() == pLeft; } ) + - maTextNodes.begin(); + TextPaM aPaM( nLeft, nPrevLen ); + return aPaM; +} + +void TextDoc::RemoveChars( const TextPaM& rPaM, sal_Int32 nChars ) +{ + TextNode* pNode = maTextNodes[ rPaM.GetPara() ].get(); + pNode->RemoveText( rPaM.GetIndex(), nChars ); +} + +bool TextDoc::IsValidPaM( const TextPaM& rPaM ) +{ + if ( rPaM.GetPara() >= maTextNodes.size() ) + { + OSL_FAIL( "PaM: Para out of range" ); + return false; + } + TextNode * pNode = maTextNodes[ rPaM.GetPara() ].get(); + if ( rPaM.GetIndex() > pNode->GetText().getLength() ) + { + OSL_FAIL( "PaM: Index out of range" ); + return false; + } + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/textdoc.hxx b/vcl/source/edit/textdoc.hxx new file mode 100644 index 0000000000..3dd0dce8d2 --- /dev/null +++ b/vcl/source/edit/textdoc.hxx @@ -0,0 +1,126 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <rtl/ustring.hxx> +#include <vcl/textdata.hxx> +#include <vcl/txtattr.hxx> +#include <vector> +#include <memory> + +class TextCharAttribList +{ +private: + TextCharAttribList(const TextCharAttribList&) = delete; + TextCharAttribList& operator=(const TextCharAttribList&) = delete; + + std::vector<std::unique_ptr<TextCharAttrib> > maAttribs; + bool mbHasEmptyAttribs; + +public: + TextCharAttribList(); + ~TextCharAttribList(); + + void Clear(); + sal_uInt16 Count() const { return maAttribs.size(); } + + const TextCharAttrib& GetAttrib( sal_uInt16 n ) const { return *maAttribs[n]; } + TextCharAttrib& GetAttrib( sal_uInt16 n ) { return *maAttribs[n]; } + std::unique_ptr<TextCharAttrib> RemoveAttrib( sal_uInt16 n ) + { + std::unique_ptr<TextCharAttrib> pReleased = std::move(maAttribs[n]); + maAttribs.erase( maAttribs.begin() + n ); + return pReleased; + } + + void InsertAttrib( std::unique_ptr<TextCharAttrib> pAttrib ); + + void DeleteEmptyAttribs(); + void ResortAttribs(); + + bool& HasEmptyAttribs() { return mbHasEmptyAttribs; } + + TextCharAttrib* FindAttrib( sal_uInt16 nWhich, sal_Int32 nPos ); + TextCharAttrib* FindEmptyAttrib( sal_uInt16 nWhich, sal_Int32 nPos ); + bool HasBoundingAttrib( sal_Int32 nBound ); +}; + +class TextNode +{ + OUString maText; + TextCharAttribList maCharAttribs; + + void ExpandAttribs( sal_Int32 nIndex, sal_Int32 nNewChars ); + void CollapseAttribs( sal_Int32 nIndex, sal_Int32 nDelChars ); + +public: + TextNode( OUString aText ); + + TextNode( const TextNode& ) = delete; + void operator=( const TextNode& ) = delete; + + const OUString& GetText() const { return maText; } + + const TextCharAttrib& GetCharAttrib(sal_uInt16 nPos) const { return maCharAttribs.GetAttrib(nPos); } + const TextCharAttribList& GetCharAttribs() const { return maCharAttribs; } + TextCharAttribList& GetCharAttribs() { return maCharAttribs; } + + void InsertText( sal_Int32 nPos, std::u16string_view rText ); + void InsertText( sal_Int32 nPos, sal_Unicode c ); + void RemoveText( sal_Int32 nPos, sal_Int32 nChars ); + + std::unique_ptr<TextNode> Split( sal_Int32 nPos ); + void Append( const TextNode& rNode ); +}; + +class TextDoc +{ + std::vector<std::unique_ptr<TextNode>> maTextNodes; + sal_uInt16 mnLeftMargin; + + void DestroyTextNodes(); + +public: + TextDoc(); + ~TextDoc(); + + void Clear(); + + std::vector<std::unique_ptr<TextNode>>& GetNodes() { return maTextNodes; } + const std::vector<std::unique_ptr<TextNode>>& GetNodes() const { return maTextNodes; } + + void RemoveChars( const TextPaM& rPaM, sal_Int32 nChars ); + TextPaM InsertText( const TextPaM& rPaM, sal_Unicode c ); + TextPaM InsertText( const TextPaM& rPaM, std::u16string_view rStr ); + + TextPaM InsertParaBreak( const TextPaM& rPaM ); + TextPaM ConnectParagraphs( TextNode* pLeft, const TextNode* pRight ); + + sal_Int32 GetTextLen( const sal_Unicode* pSep, const TextSelection* pSel = nullptr ) const; + OUString GetText( const sal_Unicode* pSep ) const; + OUString GetText( sal_uInt32 nPara ) const; + + void SetLeftMargin( sal_uInt16 n ) { mnLeftMargin = n; } + sal_uInt16 GetLeftMargin() const { return mnLeftMargin; } + + bool IsValidPaM( const TextPaM& rPaM ); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/texteng.cxx b/vcl/source/edit/texteng.cxx new file mode 100644 index 0000000000..6bf7eddc15 --- /dev/null +++ b/vcl/source/edit/texteng.cxx @@ -0,0 +1,2894 @@ +/* -*- 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 +{ + // get the index that really is first + const sal_Int32 nFirstPos = std::min(rCurSel.GetStart().GetIndex(), rCurSel.GetEnd().GetIndex()); + + bool bIsSequenceChecking = + SvtCTLOptions::IsCTLFontEnabled() && + SvtCTLOptions::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(); + + if (xISC.is()) + { + sal_Int32 nTmpPos = aPaM.GetIndex(); + sal_Int16 nCheckMode = SvtCTLOptions::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 (SvtCTLOptions::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; + } + } + + assert(pTextPortion && "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 ) + { + assert(pPortion); + + 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: */ diff --git a/vcl/source/edit/textund2.hxx b/vcl/source/edit/textund2.hxx new file mode 100644 index 0000000000..7aea9af6b8 --- /dev/null +++ b/vcl/source/edit/textund2.hxx @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include "textundo.hxx" +#include <vcl/textdata.hxx> + +class TextUndoDelPara : public TextUndo +{ +private: + bool mbDelObject; + sal_uInt32 mnPara; + TextNode* mpNode; // points at the valid not-destroyed object + +public: + TextUndoDelPara( TextEngine* pTextEngine, TextNode* pNode, sal_uInt32 nPara ); + virtual ~TextUndoDelPara() override; + + virtual void Undo() override; + virtual void Redo() override; + + virtual OUString GetComment () const override; +}; + +class TextUndoConnectParas : public TextUndo +{ +private: + sal_uInt32 mnPara; + sal_Int32 mnSepPos; + +public: + TextUndoConnectParas( TextEngine* pTextEngine, sal_uInt32 nPara, sal_Int32 nSepPos ); + virtual ~TextUndoConnectParas() override; + + virtual void Undo() override; + virtual void Redo() override; + + virtual OUString GetComment () const override; +}; + +class TextUndoSplitPara : public TextUndo +{ +private: + sal_uInt32 mnPara; + sal_Int32 mnSepPos; + +public: + TextUndoSplitPara( TextEngine* pTextEngine, sal_uInt32 nPara, sal_Int32 nSepPos ); + virtual ~TextUndoSplitPara() override; + + virtual void Undo() override; + virtual void Redo() override; + + virtual OUString GetComment () const override; +}; + +class TextUndoInsertChars : public TextUndo +{ +private: + TextPaM maTextPaM; + OUString maText; + +public: + TextUndoInsertChars( TextEngine* pTextEngine, const TextPaM& rTextPaM, OUString aStr ); + + virtual void Undo() override; + virtual void Redo() override; + + virtual bool Merge( SfxUndoAction *pNextAction ) override; + + virtual OUString GetComment () const override; +}; + +class TextUndoRemoveChars : public TextUndo +{ +private: + TextPaM maTextPaM; + OUString maText; + +public: + TextUndoRemoveChars( TextEngine* pTextEngine, const TextPaM& rTextPaM, OUString aStr ); + + virtual void Undo() override; + virtual void Redo() override; + + virtual OUString GetComment () const override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/textundo.cxx b/vcl/source/edit/textundo.cxx new file mode 100644 index 0000000000..e53e60e5d5 --- /dev/null +++ b/vcl/source/edit/textundo.cxx @@ -0,0 +1,331 @@ +/* -*- 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 "textundo.hxx" +#include "textund2.hxx" +#include <strings.hrc> + +#include <sal/log.hxx> +#include <utility> +#include <vcl/texteng.hxx> +#include <vcl/textview.hxx> +#include <vcl/textdata.hxx> +#include "textdoc.hxx" +#include "textdat2.hxx" +#include <svdata.hxx> + +namespace +{ + +// Shorten() -- inserts ellipsis (...) in the middle of a long text +void Shorten (OUString& rString) +{ + auto const nLen = rString.getLength(); + if (nLen <= 48) + return; + + // If possible, we don't break a word, hence first we look for a space. + // Space before the ellipsis: + auto iFirst = rString.lastIndexOf(' ', 32); + if (iFirst == -1 || iFirst < 16) + iFirst = 24; // not possible + // Space after the ellipsis: + auto iLast = rString.indexOf(' ', nLen - 16); + if (iLast == -1 || iLast > nLen - 4) + iLast = nLen - 8; // not possible + // finally: + rString = + OUString::Concat(rString.subView(0, iFirst + 1)) + + "..." + + rString.subView(iLast); +} + +} // namespace + +TextUndoManager::TextUndoManager( TextEngine* p ) +{ + mpTextEngine = p; +} + +TextUndoManager::~TextUndoManager() +{ +} + +bool TextUndoManager::Undo() +{ + if ( GetUndoActionCount() == 0 ) + return false; + + UndoRedoStart(); + + mpTextEngine->SetIsInUndo( true ); + bool bDone = SfxUndoManager::Undo(); + mpTextEngine->SetIsInUndo( false ); + + UndoRedoEnd(); + + return bDone; +} + +bool TextUndoManager::Redo() +{ + if ( GetRedoActionCount() == 0 ) + return false; + + UndoRedoStart(); + + mpTextEngine->SetIsInUndo( true ); + bool bDone = SfxUndoManager::Redo(); + mpTextEngine->SetIsInUndo( false ); + + UndoRedoEnd(); + + return bDone; +} + +void TextUndoManager::UndoRedoStart() +{ + SAL_WARN_IF( !GetView(), "vcl", "Undo/Redo: Active View?" ); +} + +void TextUndoManager::UndoRedoEnd() +{ + if ( GetView() ) + { + TextSelection aNewSel( GetView()->GetSelection() ); + aNewSel.GetStart() = aNewSel.GetEnd(); + GetView()->ImpSetSelection( aNewSel ); + } + + mpTextEngine->FormatAndUpdate( GetView() ); +} + +TextUndo::TextUndo( TextEngine* p ) +{ + mpTextEngine = p; +} + +TextUndo::~TextUndo() +{ +} + +OUString TextUndo::GetComment() const +{ + return OUString(); +} + +void TextUndo::SetSelection( const TextSelection& rSel ) +{ + if ( GetView() ) + GetView()->ImpSetSelection( rSel ); +} + +TextUndoDelPara::TextUndoDelPara( TextEngine* pTextEngine, TextNode* pNode, sal_uInt32 nPara ) + : TextUndo( pTextEngine ) + , mbDelObject( true) + , mnPara( nPara ) + , mpNode( pNode ) +{ +} + +TextUndoDelPara::~TextUndoDelPara() +{ + if ( mbDelObject ) + delete mpNode; +} + +void TextUndoDelPara::Undo() +{ + GetTextEngine()->InsertContent( std::unique_ptr<TextNode>(mpNode), mnPara ); + mbDelObject = false; // belongs again to the engine + + if ( GetView() ) + { + TextSelection aSel( TextPaM( mnPara, 0 ), TextPaM( mnPara, mpNode->GetText().getLength() ) ); + SetSelection( aSel ); + } +} + +void TextUndoDelPara::Redo() +{ + auto & rDocNodes = GetDoc()->GetNodes(); + // pNode is not valid anymore in case an Undo joined paragraphs + mpNode = rDocNodes[ mnPara ].get(); + + GetTEParaPortions()->Remove( mnPara ); + + // do not delete Node because of Undo! + auto it = ::std::find_if( rDocNodes.begin(), rDocNodes.end(), + [&] (std::unique_ptr<TextNode> const & p) { return p.get() == mpNode; } ); + assert(it != rDocNodes.end()); + // coverity[leaked_storage : FALSE] - ownership transferred to this with mbDelObject + it->release(); + GetDoc()->GetNodes().erase( it ); + + GetTextEngine()->ImpParagraphRemoved( mnPara ); + + mbDelObject = true; // belongs again to the Undo + + const sal_uInt32 nParas = static_cast<sal_uInt32>(GetDoc()->GetNodes().size()); + const sal_uInt32 n = mnPara < nParas ? mnPara : nParas-1; + TextNode* pN = GetDoc()->GetNodes()[ n ].get(); + TextPaM aPaM( n, pN->GetText().getLength() ); + SetSelection( aPaM ); +} + +OUString TextUndoDelPara::GetComment () const +{ + return VclResId(STR_TEXTUNDO_DELPARA); +} + +TextUndoConnectParas::TextUndoConnectParas( TextEngine* pTextEngine, sal_uInt32 nPara, sal_Int32 nPos ) + : TextUndo( pTextEngine ) + , mnPara( nPara ) + , mnSepPos( nPos ) +{ +} + +TextUndoConnectParas::~TextUndoConnectParas() +{ +} + +void TextUndoConnectParas::Undo() +{ + TextPaM aPaM = GetTextEngine()->SplitContent( mnPara, mnSepPos ); + SetSelection( aPaM ); +} + +void TextUndoConnectParas::Redo() +{ + TextPaM aPaM = GetTextEngine()->ConnectContents( mnPara ); + SetSelection( aPaM ); +} + +OUString TextUndoConnectParas::GetComment () const +{ + return VclResId(STR_TEXTUNDO_CONNECTPARAS); +} + +TextUndoSplitPara::TextUndoSplitPara( TextEngine* pTextEngine, sal_uInt32 nPara, sal_Int32 nPos ) + : TextUndo( pTextEngine ) + , mnPara( nPara ) + , mnSepPos ( nPos ) +{ +} + +TextUndoSplitPara::~TextUndoSplitPara() +{ +} + +void TextUndoSplitPara::Undo() +{ + TextPaM aPaM = GetTextEngine()->ConnectContents( mnPara ); + SetSelection( aPaM ); +} + +void TextUndoSplitPara::Redo() +{ + TextPaM aPaM = GetTextEngine()->SplitContent( mnPara, mnSepPos ); + SetSelection( aPaM ); +} + +OUString TextUndoSplitPara::GetComment () const +{ + return VclResId(STR_TEXTUNDO_SPLITPARA); +} + +TextUndoInsertChars::TextUndoInsertChars( TextEngine* pTextEngine, const TextPaM& rTextPaM, OUString aStr ) + : TextUndo( pTextEngine ), + maTextPaM( rTextPaM ), maText(std::move( aStr )) +{ +} + +void TextUndoInsertChars::Undo() +{ + TextSelection aSel( maTextPaM, maTextPaM ); + aSel.GetEnd().GetIndex() += maText.getLength(); + TextPaM aPaM = GetTextEngine()->ImpDeleteText( aSel ); + SetSelection( aPaM ); +} + +void TextUndoInsertChars::Redo() +{ + TextSelection aSel( maTextPaM, maTextPaM ); + GetTextEngine()->ImpInsertText( aSel, maText ); + TextPaM aNewPaM( maTextPaM ); + aNewPaM.GetIndex() += maText.getLength(); + SetSelection( TextSelection( aSel.GetStart(), aNewPaM ) ); +} + +bool TextUndoInsertChars::Merge( SfxUndoAction* pNextAction ) +{ + TextUndoInsertChars* pNext = dynamic_cast<TextUndoInsertChars*>(pNextAction); + if ( !pNext ) + return false; + + if ( maTextPaM.GetPara() != pNext->maTextPaM.GetPara() ) + return false; + + if ( ( maTextPaM.GetIndex() + maText.getLength() ) == pNext->maTextPaM.GetIndex() ) + { + maText += pNext->maText; + return true; + } + return false; +} + +OUString TextUndoInsertChars::GetComment () const +{ + // multiple lines? + OUString sText(maText); + Shorten(sText); + return VclResId(STR_TEXTUNDO_INSERTCHARS).replaceAll("$1", sText); +} + +TextUndoRemoveChars::TextUndoRemoveChars( TextEngine* pTextEngine, const TextPaM& rTextPaM, OUString aStr ) + : TextUndo( pTextEngine ), + maTextPaM( rTextPaM ), maText(std::move( aStr )) +{ +} + +void TextUndoRemoveChars::Undo() +{ + TextSelection aSel( maTextPaM, maTextPaM ); + GetTextEngine()->ImpInsertText( aSel, maText ); + aSel.GetEnd().GetIndex() += maText.getLength(); + SetSelection( aSel ); +} + +void TextUndoRemoveChars::Redo() +{ + TextSelection aSel( maTextPaM, maTextPaM ); + aSel.GetEnd().GetIndex() += maText.getLength(); + TextPaM aPaM = GetTextEngine()->ImpDeleteText( aSel ); + SetSelection( aPaM ); +} + +OUString TextUndoRemoveChars::GetComment () const +{ + // multiple lines? + OUString sText(maText); + Shorten(sText); + return VclResId(STR_TEXTUNDO_REMOVECHARS).replaceAll("$1", sText); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/textundo.hxx b/vcl/source/edit/textundo.hxx new file mode 100644 index 0000000000..571a16c2ce --- /dev/null +++ b/vcl/source/edit/textundo.hxx @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#pragma once + +#include <svl/undo.hxx> +#include <vcl/texteng.hxx> + +class TextEngine; +class TextView; +class TextSelection; +class TextDoc; +class TEParaPortions; + +class TextUndoManager : public SfxUndoManager +{ + TextEngine* mpTextEngine; + +protected: + + void UndoRedoStart(); + void UndoRedoEnd(); + + TextView* GetView() const { return mpTextEngine->GetActiveView(); } + +public: + explicit TextUndoManager( TextEngine* pTextEngine ); + virtual ~TextUndoManager() override; + + using SfxUndoManager::Undo; + virtual bool Undo() override; + using SfxUndoManager::Redo; + virtual bool Redo() override; + +}; + +class TextUndo : public SfxUndoAction +{ +private: + TextEngine* mpTextEngine; + +protected: + + TextView* GetView() const { return mpTextEngine->GetActiveView(); } + void SetSelection( const TextSelection& rSel ); + + TextDoc* GetDoc() const { return mpTextEngine->mpDoc.get(); } + TEParaPortions* GetTEParaPortions() const { return mpTextEngine->mpTEParaPortions.get(); } + +public: + explicit TextUndo( TextEngine* pTextEngine ); + virtual ~TextUndo() override; + + TextEngine* GetTextEngine() const { return mpTextEngine; } + + virtual void Undo() override = 0; + virtual void Redo() override = 0; + + virtual OUString GetComment() const override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/textview.cxx b/vcl/source/edit/textview.cxx new file mode 100644 index 0000000000..ad1d28d199 --- /dev/null +++ b/vcl/source/edit/textview.cxx @@ -0,0 +1,2238 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <memory> +#include <i18nutil/searchopt.hxx> +#include <o3tl/deleter.hxx> +#include <utility> +#include <vcl/textview.hxx> +#include <vcl/texteng.hxx> +#include <vcl/settings.hxx> +#include "textdoc.hxx" +#include <vcl/textdata.hxx> +#include <vcl/transfer.hxx> +#include <vcl/xtextedt.hxx> +#include "textdat2.hxx" +#include <vcl/commandevent.hxx> +#include <vcl/inputctx.hxx> + +#include <svl/undo.hxx> +#include <vcl/cursor.hxx> +#include <vcl/weld.hxx> +#include <vcl/window.hxx> +#include <vcl/svapp.hxx> +#include <tools/stream.hxx> + +#include <sal/log.hxx> +#include <sot/formats.hxx> + +#include <cppuhelper/weak.hxx> +#include <cppuhelper/queryinterface.hxx> +#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/datatransfer/UnsupportedFlavorException.hpp> +#include <com/sun/star/datatransfer/XTransferable.hpp> +#include <com/sun/star/datatransfer/clipboard/XClipboard.hpp> +#include <com/sun/star/datatransfer/clipboard/XFlushableClipboard.hpp> +#include <com/sun/star/datatransfer/dnd/DNDConstants.hpp> +#include <com/sun/star/datatransfer/dnd/XDragGestureRecognizer.hpp> +#include <com/sun/star/datatransfer/dnd/XDropTarget.hpp> +#include <com/sun/star/util/SearchFlags.hpp> + +#include <vcl/toolkit/edit.hxx> + +#include <sot/exchange.hxx> + +#include <algorithm> +#include <cstddef> + +TETextDataObject::TETextDataObject( OUString aText ) : maText(std::move( aText )) +{ +} + +// css::uno::XInterface +css::uno::Any TETextDataObject::queryInterface( const css::uno::Type & rType ) +{ + css::uno::Any aRet = ::cppu::queryInterface( rType, static_cast< css::datatransfer::XTransferable* >(this) ); + return (aRet.hasValue() ? aRet : OWeakObject::queryInterface( rType )); +} + +// css::datatransfer::XTransferable +css::uno::Any TETextDataObject::getTransferData( const css::datatransfer::DataFlavor& rFlavor ) +{ + css::uno::Any aAny; + + SotClipboardFormatId nT = SotExchange::GetFormat( rFlavor ); + if ( nT == SotClipboardFormatId::STRING ) + { + aAny <<= maText; + } + else if ( nT == SotClipboardFormatId::HTML ) + { + sal_uInt64 nLen = GetHTMLStream().TellEnd(); + GetHTMLStream().Seek(0); + + css::uno::Sequence< sal_Int8 > aSeq( nLen ); + memcpy( aSeq.getArray(), GetHTMLStream().GetData(), nLen ); + aAny <<= aSeq; + } + else + { + throw css::datatransfer::UnsupportedFlavorException(); + } + return aAny; +} + +css::uno::Sequence< css::datatransfer::DataFlavor > TETextDataObject::getTransferDataFlavors( ) +{ + GetHTMLStream().Seek( STREAM_SEEK_TO_END ); + bool bHTML = GetHTMLStream().Tell() > 0; + css::uno::Sequence< css::datatransfer::DataFlavor > aDataFlavors( bHTML ? 2 : 1 ); + SotExchange::GetFormatDataFlavor( SotClipboardFormatId::STRING, aDataFlavors.getArray()[0] ); + if ( bHTML ) + SotExchange::GetFormatDataFlavor( SotClipboardFormatId::HTML, aDataFlavors.getArray()[1] ); + return aDataFlavors; +} + +sal_Bool TETextDataObject::isDataFlavorSupported( const css::datatransfer::DataFlavor& rFlavor ) +{ + SotClipboardFormatId nT = SotExchange::GetFormat( rFlavor ); + return ( nT == SotClipboardFormatId::STRING ); +} + +struct ImpTextView +{ + ExtTextEngine* mpTextEngine; + + VclPtr<vcl::Window> mpWindow; + TextSelection maSelection; + Point maStartDocPos; + + std::unique_ptr<vcl::Cursor, o3tl::default_delete<vcl::Cursor>> mpCursor; + + std::unique_ptr<TextDDInfo, o3tl::default_delete<TextDDInfo>> mpDDInfo; + + std::unique_ptr<SelectionEngine> mpSelEngine; + std::unique_ptr<TextSelFunctionSet> mpSelFuncSet; + + css::uno::Reference< css::datatransfer::dnd::XDragSourceListener > mxDnDListener; + + sal_uInt16 mnTravelXPos; + + bool mbAutoScroll : 1; + bool mbInsertMode : 1; + bool mbReadOnly : 1; + bool mbPaintSelection : 1; + bool mbAutoIndent : 1; + bool mbCursorEnabled : 1; + bool mbClickedInSelection : 1; + bool mbCursorAtEndOfLine; +}; + +TextView::TextView( ExtTextEngine* pEng, vcl::Window* pWindow ) : + mpImpl(new ImpTextView) +{ + pWindow->EnableRTL( false ); + + mpImpl->mpWindow = pWindow; + mpImpl->mpTextEngine = pEng; + + mpImpl->mbPaintSelection = true; + mpImpl->mbAutoScroll = true; + mpImpl->mbInsertMode = true; + mpImpl->mbReadOnly = false; + mpImpl->mbAutoIndent = false; + mpImpl->mbCursorEnabled = true; + mpImpl->mbClickedInSelection = false; +// mbInSelection = false; + + mpImpl->mnTravelXPos = TRAVEL_X_DONTKNOW; + + mpImpl->mpSelFuncSet = std::make_unique<TextSelFunctionSet>( this ); + mpImpl->mpSelEngine = std::make_unique<SelectionEngine>( mpImpl->mpWindow, mpImpl->mpSelFuncSet.get() ); + mpImpl->mpSelEngine->SetSelectionMode( SelectionMode::Range ); + mpImpl->mpSelEngine->EnableDrag( true ); + + mpImpl->mpCursor.reset(new vcl::Cursor); + mpImpl->mpCursor->Show(); + pWindow->SetCursor( mpImpl->mpCursor.get() ); + pWindow->SetInputContext( InputContext( pEng->GetFont(), InputContextFlags::Text|InputContextFlags::ExtText ) ); + + pWindow->GetOutDev()->SetLineColor(); + + if ( pWindow->GetDragGestureRecognizer().is() ) + { + mpImpl->mxDnDListener = new vcl::unohelper::DragAndDropWrapper( this ); + + css::uno::Reference< css::datatransfer::dnd::XDragGestureListener> xDGL( mpImpl->mxDnDListener, css::uno::UNO_QUERY ); + pWindow->GetDragGestureRecognizer()->addDragGestureListener( xDGL ); + css::uno::Reference< css::datatransfer::dnd::XDropTargetListener> xDTL( xDGL, css::uno::UNO_QUERY ); + pWindow->GetDropTarget()->addDropTargetListener( xDTL ); + pWindow->GetDropTarget()->setActive( true ); + pWindow->GetDropTarget()->setDefaultActions( css::datatransfer::dnd::DNDConstants::ACTION_COPY_OR_MOVE ); + } +} + +TextView::~TextView() +{ + mpImpl->mpSelEngine.reset(); + mpImpl->mpSelFuncSet.reset(); + + if ( mpImpl->mpWindow->GetCursor() == mpImpl->mpCursor.get() ) + mpImpl->mpWindow->SetCursor( nullptr ); + + mpImpl->mpCursor.reset(); + mpImpl->mpDDInfo.reset(); +} + +void TextView::Invalidate() +{ + mpImpl->mpWindow->Invalidate(); +} + +void TextView::SetSelection( const TextSelection& rTextSel, bool bGotoCursor ) +{ + // if someone left an empty attribute and then the Outliner manipulated the selection + if ( !mpImpl->maSelection.HasRange() ) + mpImpl->mpTextEngine->CursorMoved( mpImpl->maSelection.GetStart().GetPara() ); + + // if the selection is manipulated after a KeyInput + mpImpl->mpTextEngine->CheckIdleFormatter(); + + HideSelection(); + TextSelection aNewSel( rTextSel ); + mpImpl->mpTextEngine->ValidateSelection( aNewSel ); + ImpSetSelection( aNewSel ); + ShowSelection(); + ShowCursor( bGotoCursor ); +} + +void TextView::SetSelection( const TextSelection& rTextSel ) +{ + SetSelection( rTextSel, mpImpl->mbAutoScroll ); +} + +const TextSelection& TextView::GetSelection() const +{ + return mpImpl->maSelection; +} +TextSelection& TextView::GetSelection() +{ + return mpImpl->maSelection; +} + +void TextView::DeleteSelected() +{ +// HideSelection(); + + mpImpl->mpTextEngine->UndoActionStart(); + TextPaM aPaM = mpImpl->mpTextEngine->ImpDeleteText( mpImpl->maSelection ); + mpImpl->mpTextEngine->UndoActionEnd(); + + ImpSetSelection( aPaM ); + mpImpl->mpTextEngine->FormatAndUpdate( this ); + ShowCursor(); +} + +void TextView::ImpPaint(vcl::RenderContext& rRenderContext, const Point& rStartPos, tools::Rectangle const* pPaintArea, TextSelection const* pSelection) +{ + if (!mpImpl->mbPaintSelection) + { + pSelection = nullptr; + } + else + { + // set correct background color; + // unfortunately we cannot detect if it has changed + vcl::Font aFont = mpImpl->mpTextEngine->GetFont(); + Color aColor = rRenderContext.GetBackground().GetColor(); + aColor.SetAlpha(255); + if (aColor != aFont.GetFillColor()) + { + if (aFont.IsTransparent()) + aColor = COL_TRANSPARENT; + aFont.SetFillColor(aColor); + mpImpl->mpTextEngine->maFont = aFont; + } + } + + mpImpl->mpTextEngine->ImpPaint(&rRenderContext, rStartPos, pPaintArea, pSelection); +} + +void TextView::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + ImpPaint(rRenderContext, rRect); +} + +void TextView::ImpPaint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + if ( !mpImpl->mpTextEngine->GetUpdateMode() || mpImpl->mpTextEngine->IsInUndo() ) + return; + + TextSelection *pDrawSelection = nullptr; + if (mpImpl->maSelection.HasRange()) + pDrawSelection = &mpImpl->maSelection; + + Point aStartPos = ImpGetOutputStartPos(mpImpl->maStartDocPos); + ImpPaint(rRenderContext, aStartPos, &rRect, pDrawSelection); +} + +void TextView::ImpSetSelection( const TextSelection& rSelection ) +{ + if (rSelection == mpImpl->maSelection) + return; + + bool bCaret = false, bSelection = false; + const TextPaM &rEnd = rSelection.GetEnd(); + const TextPaM &rOldEnd = mpImpl->maSelection.GetEnd(); + bool bGap = rSelection.HasRange(), bOldGap = mpImpl->maSelection.HasRange(); + if (rEnd != rOldEnd) + bCaret = true; + if (bGap || bOldGap) + bSelection = true; + + mpImpl->maSelection = rSelection; + + if (bSelection) + mpImpl->mpTextEngine->Broadcast(TextHint(SfxHintId::TextViewSelectionChanged)); + + if (bCaret) + mpImpl->mpTextEngine->Broadcast(TextHint(SfxHintId::TextViewCaretChanged)); +} + +void TextView::ShowSelection() +{ + ImpShowHideSelection(); +} + +void TextView::HideSelection() +{ + ImpShowHideSelection(); +} + +void TextView::ShowSelection( const TextSelection& rRange ) +{ + ImpShowHideSelection( &rRange ); +} + +void TextView::ImpShowHideSelection(const TextSelection* pRange) +{ + const TextSelection* pRangeOrSelection = pRange ? pRange : &mpImpl->maSelection; + + if ( !pRangeOrSelection->HasRange() ) + return; + + if( mpImpl->mpWindow->IsPaintTransparent() ) + mpImpl->mpWindow->Invalidate(); + else + { + TextSelection aRange( *pRangeOrSelection ); + aRange.Justify(); + bool bVisCursor = mpImpl->mpCursor->IsVisible(); + mpImpl->mpCursor->Hide(); + Invalidate(); + if (bVisCursor) + mpImpl->mpCursor->Show(); + } +} + +bool TextView::KeyInput( const KeyEvent& rKeyEvent ) +{ + bool bDone = true; + bool bModified = false; + bool bMoved = false; + bool bEndKey = false; // special CursorPosition + bool bAllowIdle = true; + + // check mModified; + // the local bModified is not set e.g. by Cut/Paste, as here + // the update happens somewhere else + bool bWasModified = mpImpl->mpTextEngine->IsModified(); + mpImpl->mpTextEngine->SetModified( false ); + + TextSelection aCurSel( mpImpl->maSelection ); + TextSelection aOldSel( aCurSel ); + + sal_uInt16 nCode = rKeyEvent.GetKeyCode().GetCode(); + KeyFuncType eFunc = rKeyEvent.GetKeyCode().GetFunction(); + if ( eFunc != KeyFuncType::DONTKNOW ) + { + switch ( eFunc ) + { + case KeyFuncType::CUT: + { + if ( !mpImpl->mbReadOnly ) + Cut(); + } + break; + case KeyFuncType::COPY: + { + Copy(); + } + break; + case KeyFuncType::PASTE: + { + if ( !mpImpl->mbReadOnly ) + Paste(); + } + break; + case KeyFuncType::UNDO: + { + if ( !mpImpl->mbReadOnly ) + Undo(); + } + break; + case KeyFuncType::REDO: + { + if ( !mpImpl->mbReadOnly ) + Redo(); + } + break; + + default: // might get processed below + eFunc = KeyFuncType::DONTKNOW; + } + } + if ( eFunc == KeyFuncType::DONTKNOW ) + { + switch ( nCode ) + { + case KEY_UP: + case KEY_DOWN: + case KEY_LEFT: + case KEY_RIGHT: + case KEY_HOME: + case KEY_END: + case KEY_PAGEUP: + case KEY_PAGEDOWN: + case css::awt::Key::MOVE_WORD_FORWARD: + case css::awt::Key::SELECT_WORD_FORWARD: + case css::awt::Key::MOVE_WORD_BACKWARD: + case css::awt::Key::SELECT_WORD_BACKWARD: + case css::awt::Key::MOVE_TO_BEGIN_OF_LINE: + case css::awt::Key::MOVE_TO_END_OF_LINE: + case css::awt::Key::SELECT_TO_BEGIN_OF_LINE: + case css::awt::Key::SELECT_TO_END_OF_LINE: + case css::awt::Key::MOVE_TO_BEGIN_OF_PARAGRAPH: + case css::awt::Key::MOVE_TO_END_OF_PARAGRAPH: + case css::awt::Key::SELECT_TO_BEGIN_OF_PARAGRAPH: + case css::awt::Key::SELECT_TO_END_OF_PARAGRAPH: + case css::awt::Key::MOVE_TO_BEGIN_OF_DOCUMENT: + case css::awt::Key::MOVE_TO_END_OF_DOCUMENT: + case css::awt::Key::SELECT_TO_BEGIN_OF_DOCUMENT: + case css::awt::Key::SELECT_TO_END_OF_DOCUMENT: + { + if ( ( !rKeyEvent.GetKeyCode().IsMod2() || ( nCode == KEY_LEFT ) || ( nCode == KEY_RIGHT ) ) + && !( rKeyEvent.GetKeyCode().IsMod1() && ( nCode == KEY_PAGEUP || nCode == KEY_PAGEDOWN ) ) ) + { + aCurSel = ImpMoveCursor( rKeyEvent ); + if ( aCurSel.HasRange() ) { + css::uno::Reference<css::datatransfer::clipboard::XClipboard> aSelection(GetSystemPrimarySelection()); + Copy( aSelection ); + } + bMoved = true; + if ( nCode == KEY_END ) + bEndKey = true; + } + else + bDone = false; + } + break; + case KEY_BACKSPACE: + case KEY_DELETE: + case css::awt::Key::DELETE_WORD_BACKWARD: + case css::awt::Key::DELETE_WORD_FORWARD: + case css::awt::Key::DELETE_TO_BEGIN_OF_LINE: + case css::awt::Key::DELETE_TO_END_OF_LINE: + { + if ( !mpImpl->mbReadOnly && !rKeyEvent.GetKeyCode().IsMod2() ) + { + sal_uInt8 nDel = ( nCode == KEY_DELETE ) ? DEL_RIGHT : DEL_LEFT; + sal_uInt8 nMode = rKeyEvent.GetKeyCode().IsMod1() ? DELMODE_RESTOFWORD : DELMODE_SIMPLE; + if ( ( nMode == DELMODE_RESTOFWORD ) && rKeyEvent.GetKeyCode().IsShift() ) + nMode = DELMODE_RESTOFCONTENT; + + switch( nCode ) + { + case css::awt::Key::DELETE_WORD_BACKWARD: + nDel = DEL_LEFT; + nMode = DELMODE_RESTOFWORD; + break; + case css::awt::Key::DELETE_WORD_FORWARD: + nDel = DEL_RIGHT; + nMode = DELMODE_RESTOFWORD; + break; + case css::awt::Key::DELETE_TO_BEGIN_OF_LINE: + nDel = DEL_LEFT; + nMode = DELMODE_RESTOFCONTENT; + break; + case css::awt::Key::DELETE_TO_END_OF_LINE: + nDel = DEL_RIGHT; + nMode = DELMODE_RESTOFCONTENT; + break; + default: break; + } + + mpImpl->mpTextEngine->UndoActionStart(); + aCurSel = ImpDelete( nDel, nMode ); + mpImpl->mpTextEngine->UndoActionEnd(); + bModified = true; + bAllowIdle = false; + } + else + bDone = false; + } + break; + case KEY_TAB: + { + if ( !mpImpl->mbReadOnly && !rKeyEvent.GetKeyCode().IsShift() && + !rKeyEvent.GetKeyCode().IsMod1() && !rKeyEvent.GetKeyCode().IsMod2() && + ImplCheckTextLen( u"x" ) ) + { + aCurSel = mpImpl->mpTextEngine->ImpInsertText( aCurSel, '\t', !IsInsertMode() ); + bModified = true; + } + else + bDone = false; + } + break; + case KEY_RETURN: + { + // do not swallow Shift-RETURN, as this would disable multi-line entries + // in dialogs & property editors + if ( !mpImpl->mbReadOnly && !rKeyEvent.GetKeyCode().IsMod1() && + !rKeyEvent.GetKeyCode().IsMod2() && ImplCheckTextLen( u"x" ) ) + { + mpImpl->mpTextEngine->UndoActionStart(); + aCurSel = mpImpl->mpTextEngine->ImpInsertParaBreak( aCurSel ); + if ( mpImpl->mbAutoIndent ) + { + TextNode* pPrev = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aCurSel.GetEnd().GetPara() - 1 ].get(); + sal_Int32 n = 0; + while ( ( n < pPrev->GetText().getLength() ) && ( + ( pPrev->GetText()[ n ] == ' ' ) || + ( pPrev->GetText()[ n ] == '\t' ) ) ) + { + n++; + } + if ( n ) + aCurSel = mpImpl->mpTextEngine->ImpInsertText( aCurSel, pPrev->GetText().copy( 0, n ) ); + } + mpImpl->mpTextEngine->UndoActionEnd(); + bModified = true; + } + else + bDone = false; + } + break; + case KEY_INSERT: + { + if ( !mpImpl->mbReadOnly ) + SetInsertMode( !IsInsertMode() ); + } + break; + default: + { + if ( TextEngine::IsSimpleCharInput( rKeyEvent ) ) + { + sal_Unicode nCharCode = rKeyEvent.GetCharCode(); + if ( !mpImpl->mbReadOnly && ImplCheckTextLen( OUStringChar(nCharCode) ) ) // otherwise swallow the character anyway + { + aCurSel = mpImpl->mpTextEngine->ImpInsertText( nCharCode, aCurSel, !IsInsertMode(), true ); + bModified = true; + } + } + else + bDone = false; + } + } + } + + if ( aCurSel != aOldSel ) // Check if changed, maybe other method already changed mpImpl->maSelection, don't overwrite that! + ImpSetSelection( aCurSel ); + + if ( ( nCode != KEY_UP ) && ( nCode != KEY_DOWN ) ) + mpImpl->mnTravelXPos = TRAVEL_X_DONTKNOW; + + if ( bModified ) + { + // Idle-Formatter only if AnyInput + if ( bAllowIdle && Application::AnyInput( VclInputFlags::KEYBOARD) ) + mpImpl->mpTextEngine->IdleFormatAndUpdate( this ); + else + mpImpl->mpTextEngine->FormatAndUpdate( this); + } + else if ( bMoved ) + { + // selection is painted now in ImpMoveCursor + ImpShowCursor( mpImpl->mbAutoScroll, true, bEndKey ); + } + + if ( mpImpl->mpTextEngine->IsModified() ) + mpImpl->mpTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + else if ( bWasModified ) + mpImpl->mpTextEngine->SetModified( true ); + + return bDone; +} + +void TextView::MouseButtonUp( const MouseEvent& rMouseEvent ) +{ + mpImpl->mbClickedInSelection = false; + mpImpl->mnTravelXPos = TRAVEL_X_DONTKNOW; + mpImpl->mpSelEngine->SelMouseButtonUp( rMouseEvent ); + if ( rMouseEvent.IsMiddle() && !IsReadOnly() && + ( GetWindow()->GetSettings().GetMouseSettings().GetMiddleButtonAction() == MouseMiddleButtonAction::PasteSelection ) ) + { + css::uno::Reference<css::datatransfer::clipboard::XClipboard> aSelection(GetSystemPrimarySelection()); + Paste( aSelection ); + if ( mpImpl->mpTextEngine->IsModified() ) + mpImpl->mpTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + } + else if ( rMouseEvent.IsLeft() && GetSelection().HasRange() ) + { + css::uno::Reference<css::datatransfer::clipboard::XClipboard> aSelection(GetSystemPrimarySelection()); + Copy( aSelection ); + } +} + +void TextView::MouseButtonDown( const MouseEvent& rMouseEvent ) +{ + mpImpl->mpTextEngine->CheckIdleFormatter(); // for fast typing and MouseButtonDown + mpImpl->mnTravelXPos = TRAVEL_X_DONTKNOW; + mpImpl->mbClickedInSelection = IsSelectionAtPoint( rMouseEvent.GetPosPixel() ); + + mpImpl->mpTextEngine->SetActiveView( this ); + + mpImpl->mpSelEngine->SelMouseButtonDown( rMouseEvent ); + + // mbu 20.01.2005 - SelMouseButtonDown() possibly triggers a 'selection changed' + // notification. The appropriate handler could change the current selection, + // which is the case in the MailMerge address block control. To enable select'n'drag + // we need to reevaluate the selection after the notification has been fired. + mpImpl->mbClickedInSelection = IsSelectionAtPoint( rMouseEvent.GetPosPixel() ); + + // special cases + if ( rMouseEvent.IsShift() || ( rMouseEvent.GetClicks() < 2 )) + return; + + if ( rMouseEvent.IsMod2() ) + { + HideSelection(); + ImpSetSelection( mpImpl->maSelection.GetEnd() ); + SetCursorAtPoint( rMouseEvent.GetPosPixel() ); // not set by SelectionEngine for MOD2 + } + + if ( rMouseEvent.GetClicks() == 2 ) + { + // select word + if ( mpImpl->maSelection.GetEnd().GetIndex() < mpImpl->mpTextEngine->GetTextLen( mpImpl->maSelection.GetEnd().GetPara() ) ) + { + HideSelection(); + // tdf#57879 - expand selection to include connector punctuations + TextSelection aNewSel; + mpImpl->mpTextEngine->GetWord( mpImpl->maSelection.GetEnd(), &aNewSel.GetStart(), &aNewSel.GetEnd() ); + ImpSetSelection( aNewSel ); + ShowSelection(); + ShowCursor(); + } + } + else if ( rMouseEvent.GetClicks() == 3 ) + { + // select paragraph + if ( mpImpl->maSelection.GetStart().GetIndex() || ( mpImpl->maSelection.GetEnd().GetIndex() < mpImpl->mpTextEngine->GetTextLen( mpImpl->maSelection.GetEnd().GetPara() ) ) ) + { + HideSelection(); + TextSelection aNewSel( mpImpl->maSelection ); + aNewSel.GetStart().GetIndex() = 0; + aNewSel.GetEnd().GetIndex() = mpImpl->mpTextEngine->mpDoc->GetNodes()[ mpImpl->maSelection.GetEnd().GetPara() ]->GetText().getLength(); + ImpSetSelection( aNewSel ); + ShowSelection(); + ShowCursor(); + } + } +} + +void TextView::MouseMove( const MouseEvent& rMouseEvent ) +{ + mpImpl->mnTravelXPos = TRAVEL_X_DONTKNOW; + mpImpl->mpSelEngine->SelMouseMove( rMouseEvent ); +} + +void TextView::Command( const CommandEvent& rCEvt ) +{ + mpImpl->mpTextEngine->CheckIdleFormatter(); // for fast typing and MouseButtonDown + mpImpl->mpTextEngine->SetActiveView( this ); + + if ( rCEvt.GetCommand() == CommandEventId::StartExtTextInput ) + { + DeleteSelected(); + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ GetSelection().GetEnd().GetPara() ].get(); + mpImpl->mpTextEngine->mpIMEInfos = std::make_unique<TEIMEInfos>( GetSelection().GetEnd(), pNode->GetText().copy( GetSelection().GetEnd().GetIndex() ) ); + mpImpl->mpTextEngine->mpIMEInfos->bWasCursorOverwrite = !IsInsertMode(); + } + else if ( rCEvt.GetCommand() == CommandEventId::EndExtTextInput ) + { + SAL_WARN_IF( !mpImpl->mpTextEngine->mpIMEInfos, "vcl", "CommandEventId::EndExtTextInput => No Start ?" ); + if( mpImpl->mpTextEngine->mpIMEInfos ) + { + TEParaPortion* pPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( mpImpl->mpTextEngine->mpIMEInfos->aPos.GetPara() ); + pPortion->MarkSelectionInvalid( mpImpl->mpTextEngine->mpIMEInfos->aPos.GetIndex() ); + + bool bInsertMode = !mpImpl->mpTextEngine->mpIMEInfos->bWasCursorOverwrite; + + mpImpl->mpTextEngine->mpIMEInfos.reset(); + + mpImpl->mpTextEngine->TextModified(); + mpImpl->mpTextEngine->FormatAndUpdate( this ); + + SetInsertMode( bInsertMode ); + + if ( mpImpl->mpTextEngine->IsModified() ) + mpImpl->mpTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + } + } + else if ( rCEvt.GetCommand() == CommandEventId::ExtTextInput ) + { + SAL_WARN_IF( !mpImpl->mpTextEngine->mpIMEInfos, "vcl", "CommandEventId::ExtTextInput => No Start ?" ); + if( mpImpl->mpTextEngine->mpIMEInfos ) + { + const CommandExtTextInputData* pData = rCEvt.GetExtTextInputData(); + + if ( !pData->IsOnlyCursorChanged() ) + { + TextSelection aSelect( mpImpl->mpTextEngine->mpIMEInfos->aPos ); + aSelect.GetEnd().GetIndex() += mpImpl->mpTextEngine->mpIMEInfos->nLen; + aSelect = mpImpl->mpTextEngine->ImpDeleteText( aSelect ); + aSelect = mpImpl->mpTextEngine->ImpInsertText( aSelect, pData->GetText() ); + + if ( mpImpl->mpTextEngine->mpIMEInfos->bWasCursorOverwrite ) + { + const sal_Int32 nOldIMETextLen = mpImpl->mpTextEngine->mpIMEInfos->nLen; + const sal_Int32 nNewIMETextLen = pData->GetText().getLength(); + + if ( ( nOldIMETextLen > nNewIMETextLen ) && + ( nNewIMETextLen < mpImpl->mpTextEngine->mpIMEInfos->aOldTextAfterStartPos.getLength() ) ) + { + // restore old characters + sal_Int32 nRestore = nOldIMETextLen - nNewIMETextLen; + TextPaM aPaM( mpImpl->mpTextEngine->mpIMEInfos->aPos ); + aPaM.GetIndex() += nNewIMETextLen; + mpImpl->mpTextEngine->ImpInsertText( aPaM, mpImpl->mpTextEngine->mpIMEInfos->aOldTextAfterStartPos.copy( nNewIMETextLen, nRestore ) ); + } + else if ( ( nOldIMETextLen < nNewIMETextLen ) && + ( nOldIMETextLen < mpImpl->mpTextEngine->mpIMEInfos->aOldTextAfterStartPos.getLength() ) ) + { + // overwrite + const sal_Int32 nOverwrite = std::min( nNewIMETextLen, mpImpl->mpTextEngine->mpIMEInfos->aOldTextAfterStartPos.getLength() ) - nOldIMETextLen; + SAL_WARN_IF( !nOverwrite || (nOverwrite >= 0xFF00), "vcl", "IME Overwrite?!" ); + TextPaM aPaM( mpImpl->mpTextEngine->mpIMEInfos->aPos ); + aPaM.GetIndex() += nNewIMETextLen; + TextSelection aSel( aPaM ); + aSel.GetEnd().GetIndex() += nOverwrite; + mpImpl->mpTextEngine->ImpDeleteText( aSel ); + } + } + + if ( pData->GetTextAttr() ) + { + mpImpl->mpTextEngine->mpIMEInfos->CopyAttribs( pData->GetTextAttr(), pData->GetText().getLength() ); + } + else + { + mpImpl->mpTextEngine->mpIMEInfos->DestroyAttribs(); + } + + TEParaPortion* pPPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( mpImpl->mpTextEngine->mpIMEInfos->aPos.GetPara() ); + pPPortion->MarkSelectionInvalid( mpImpl->mpTextEngine->mpIMEInfos->aPos.GetIndex() ); + mpImpl->mpTextEngine->FormatAndUpdate( this ); + } + + TextSelection aNewSel = TextPaM( mpImpl->mpTextEngine->mpIMEInfos->aPos.GetPara(), mpImpl->mpTextEngine->mpIMEInfos->aPos.GetIndex()+pData->GetCursorPos() ); + SetSelection( aNewSel ); + SetInsertMode( !pData->IsCursorOverwrite() ); + + if ( pData->IsCursorVisible() ) + ShowCursor(); + else + HideCursor(); + } + } + else if ( rCEvt.GetCommand() == CommandEventId::CursorPos ) + { + if ( mpImpl->mpTextEngine->mpIMEInfos && mpImpl->mpTextEngine->mpIMEInfos->nLen ) + { + TextPaM aPaM( GetSelection().GetEnd() ); + tools::Rectangle aR1 = mpImpl->mpTextEngine->PaMtoEditCursor( aPaM ); + + sal_Int32 nInputEnd = mpImpl->mpTextEngine->mpIMEInfos->aPos.GetIndex() + mpImpl->mpTextEngine->mpIMEInfos->nLen; + + if ( !mpImpl->mpTextEngine->IsFormatted() ) + mpImpl->mpTextEngine->FormatDoc(); + + TEParaPortion* pParaPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( aPaM.GetPara() ); + std::vector<TextLine>::size_type nLine = pParaPortion->GetLineNumber( aPaM.GetIndex(), true ); + TextLine& rLine = pParaPortion->GetLines()[ nLine ]; + if ( nInputEnd > rLine.GetEnd() ) + nInputEnd = rLine.GetEnd(); + tools::Rectangle aR2 = mpImpl->mpTextEngine->PaMtoEditCursor( TextPaM( aPaM.GetPara(), nInputEnd ) ); + + tools::Long nWidth = aR2.Left()-aR1.Right(); + aR1.Move( -GetStartDocPos().X(), -GetStartDocPos().Y() ); + GetWindow()->SetCursorRect( &aR1, nWidth ); + } + else + { + GetWindow()->SetCursorRect(); + } + } + else + { + mpImpl->mpSelEngine->Command( rCEvt ); + } +} + +void TextView::ShowCursor( bool bGotoCursor, bool bForceVisCursor ) +{ + // this setting has more weight + if ( !mpImpl->mbAutoScroll ) + bGotoCursor = false; + ImpShowCursor( bGotoCursor, bForceVisCursor, false ); +} + +void TextView::HideCursor() +{ + mpImpl->mpCursor->Hide(); +} + +void TextView::Scroll( tools::Long ndX, tools::Long ndY ) +{ + SAL_WARN_IF( !mpImpl->mpTextEngine->IsFormatted(), "vcl", "Scroll: Not formatted!" ); + + if ( !ndX && !ndY ) + return; + + Point aNewStartPos( mpImpl->maStartDocPos ); + + // Vertical: + aNewStartPos.AdjustY( -ndY ); + if ( aNewStartPos.Y() < 0 ) + aNewStartPos.setY( 0 ); + + // Horizontal: + aNewStartPos.AdjustX( -ndX ); + if ( aNewStartPos.X() < 0 ) + aNewStartPos.setX( 0 ); + + tools::Long nDiffX = mpImpl->maStartDocPos.X() - aNewStartPos.X(); + tools::Long nDiffY = mpImpl->maStartDocPos.Y() - aNewStartPos.Y(); + + if ( nDiffX || nDiffY ) + { + bool bVisCursor = mpImpl->mpCursor->IsVisible(); + mpImpl->mpCursor->Hide(); + mpImpl->mpWindow->PaintImmediately(); + mpImpl->maStartDocPos = aNewStartPos; + + if ( mpImpl->mpTextEngine->IsRightToLeft() ) + nDiffX = -nDiffX; + mpImpl->mpWindow->Scroll( nDiffX, nDiffY ); + mpImpl->mpWindow->PaintImmediately(); + mpImpl->mpCursor->SetPos( mpImpl->mpCursor->GetPos() + Point( nDiffX, nDiffY ) ); + if ( bVisCursor && !mpImpl->mbReadOnly ) + mpImpl->mpCursor->Show(); + } + + mpImpl->mpTextEngine->Broadcast( TextHint( SfxHintId::TextViewScrolled ) ); +} + +void TextView::Undo() +{ + mpImpl->mpTextEngine->SetActiveView( this ); + mpImpl->mpTextEngine->GetUndoManager().Undo(); +} + +void TextView::Redo() +{ + mpImpl->mpTextEngine->SetActiveView( this ); + mpImpl->mpTextEngine->GetUndoManager().Redo(); +} + +void TextView::Cut() +{ + mpImpl->mpTextEngine->UndoActionStart(); + Copy(); + DeleteSelected(); + mpImpl->mpTextEngine->UndoActionEnd(); +} + +void TextView::Copy( css::uno::Reference< css::datatransfer::clipboard::XClipboard > const & rxClipboard ) +{ + if ( !rxClipboard.is() ) + return; + + rtl::Reference<TETextDataObject> pDataObj = new TETextDataObject( GetSelected() ); + + SolarMutexReleaser aReleaser; + + try + { + rxClipboard->setContents( pDataObj, nullptr ); + + css::uno::Reference< css::datatransfer::clipboard::XFlushableClipboard > xFlushableClipboard( rxClipboard, css::uno::UNO_QUERY ); + if( xFlushableClipboard.is() ) + xFlushableClipboard->flushClipboard(); + } + catch( const css::uno::Exception& ) + { + } +} + +void TextView::Copy() +{ + css::uno::Reference<css::datatransfer::clipboard::XClipboard> aClipboard(GetWindow()->GetClipboard()); + Copy( aClipboard ); +} + +void TextView::Paste( css::uno::Reference< css::datatransfer::clipboard::XClipboard > const & rxClipboard ) +{ + if ( !rxClipboard.is() ) + return; + + css::uno::Reference< css::datatransfer::XTransferable > xDataObj; + + try + { + SolarMutexReleaser aReleaser; + xDataObj = rxClipboard->getContents(); + } + catch( const css::uno::Exception& ) + { + } + + if ( !xDataObj.is() ) + return; + + css::datatransfer::DataFlavor aFlavor; + SotExchange::GetFormatDataFlavor( SotClipboardFormatId::STRING, aFlavor ); + if ( !xDataObj->isDataFlavorSupported( aFlavor ) ) + return; + + try + { + css::uno::Any aData = xDataObj->getTransferData( aFlavor ); + OUString aText; + aData >>= aText; + bool bWasTruncated = false; + if( mpImpl->mpTextEngine->GetMaxTextLen() != 0 ) + bWasTruncated = ImplTruncateNewText( aText ); + InsertText( aText ); + mpImpl->mpTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + + if( bWasTruncated ) + Edit::ShowTruncationWarning(mpImpl->mpWindow->GetFrameWeld()); + } + catch( const css::datatransfer::UnsupportedFlavorException& ) + { + } +} + +void TextView::Paste() +{ + css::uno::Reference<css::datatransfer::clipboard::XClipboard> aClipboard(GetWindow()->GetClipboard()); + Paste( aClipboard ); +} + +OUString TextView::GetSelected() const +{ + return GetSelected( GetSystemLineEnd() ); +} + +OUString TextView::GetSelected( LineEnd aSeparator ) const +{ + return mpImpl->mpTextEngine->GetText( mpImpl->maSelection, aSeparator ); +} + +void TextView::SetInsertMode( bool bInsert ) +{ + if ( mpImpl->mbInsertMode != bInsert ) + { + mpImpl->mbInsertMode = bInsert; + ShowCursor( mpImpl->mbAutoScroll, false ); + } +} + +void TextView::SetReadOnly( bool bReadOnly ) +{ + if ( mpImpl->mbReadOnly != bReadOnly ) + { + mpImpl->mbReadOnly = bReadOnly; + if ( !mpImpl->mbReadOnly ) + ShowCursor( mpImpl->mbAutoScroll, false ); + else + HideCursor(); + + GetWindow()->SetInputContext( InputContext( mpImpl->mpTextEngine->GetFont(), bReadOnly ? InputContextFlags::Text|InputContextFlags::ExtText : InputContextFlags::NONE ) ); + } +} + +TextSelection const & TextView::ImpMoveCursor( const KeyEvent& rKeyEvent ) +{ + // normally only needed for Up/Down; but who cares + mpImpl->mpTextEngine->CheckIdleFormatter(); + + TextPaM aPaM( mpImpl->maSelection.GetEnd() ); + TextPaM aOldEnd( aPaM ); + + TextDirectionality eTextDirection = TextDirectionality::LeftToRight_TopToBottom; + if ( mpImpl->mpTextEngine->IsRightToLeft() ) + eTextDirection = TextDirectionality::RightToLeft_TopToBottom; + + KeyEvent aTranslatedKeyEvent = rKeyEvent.LogicalTextDirectionality( eTextDirection ); + + bool bCtrl = aTranslatedKeyEvent.GetKeyCode().IsMod1(); + sal_uInt16 nCode = aTranslatedKeyEvent.GetKeyCode().GetCode(); + + bool bSelect = aTranslatedKeyEvent.GetKeyCode().IsShift(); + switch ( nCode ) + { + case KEY_UP: aPaM = CursorUp( aPaM ); + break; + case KEY_DOWN: aPaM = CursorDown( aPaM ); + break; + case KEY_HOME: + if (bCtrl) + { + aPaM = CursorStartOfDoc(); + } + else + { + // tdf#145764 - move cursor to the beginning or the first non-space character in the same line + const TextPaM aFirstWordPaM = CursorFirstWord(aPaM); + aPaM = aPaM.GetIndex() == aFirstWordPaM.GetIndex() ? CursorStartOfLine(aPaM) : aFirstWordPaM; + } + break; + case KEY_END: aPaM = bCtrl ? CursorEndOfDoc() : CursorEndOfLine( aPaM ); + break; + case KEY_PAGEUP: aPaM = bCtrl ? CursorStartOfDoc() : PageUp( aPaM ); + break; + case KEY_PAGEDOWN: aPaM = bCtrl ? CursorEndOfDoc() : PageDown( aPaM ); + break; + case KEY_LEFT: aPaM = bCtrl ? CursorWordLeft( aPaM ) : CursorLeft( aPaM, aTranslatedKeyEvent.GetKeyCode().IsMod2() ? sal_uInt16(css::i18n::CharacterIteratorMode::SKIPCHARACTER) : sal_uInt16(css::i18n::CharacterIteratorMode::SKIPCELL) ); + break; + case KEY_RIGHT: aPaM = bCtrl ? CursorWordRight( aPaM ) : CursorRight( aPaM, aTranslatedKeyEvent.GetKeyCode().IsMod2() ? sal_uInt16(css::i18n::CharacterIteratorMode::SKIPCHARACTER) : sal_uInt16(css::i18n::CharacterIteratorMode::SKIPCELL) ); + break; + case css::awt::Key::SELECT_WORD_FORWARD: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_WORD_FORWARD: + aPaM = CursorWordRight( aPaM ); + break; + case css::awt::Key::SELECT_WORD_BACKWARD: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_WORD_BACKWARD: + aPaM = CursorWordLeft( aPaM ); + break; + case css::awt::Key::SELECT_TO_BEGIN_OF_LINE: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_TO_BEGIN_OF_LINE: + aPaM = CursorStartOfLine( aPaM ); + break; + case css::awt::Key::SELECT_TO_END_OF_LINE: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_TO_END_OF_LINE: + aPaM = CursorEndOfLine( aPaM ); + break; + case css::awt::Key::SELECT_TO_BEGIN_OF_PARAGRAPH: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_TO_BEGIN_OF_PARAGRAPH: + aPaM = CursorStartOfParagraph( aPaM ); + break; + case css::awt::Key::SELECT_TO_END_OF_PARAGRAPH: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_TO_END_OF_PARAGRAPH: + aPaM = CursorEndOfParagraph( aPaM ); + break; + case css::awt::Key::SELECT_TO_BEGIN_OF_DOCUMENT: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_TO_BEGIN_OF_DOCUMENT: + aPaM = CursorStartOfDoc(); + break; + case css::awt::Key::SELECT_TO_END_OF_DOCUMENT: + bSelect = true; + [[fallthrough]]; + case css::awt::Key::MOVE_TO_END_OF_DOCUMENT: + aPaM = CursorEndOfDoc(); + break; + } + + // might cause a CreateAnchor or Deselection all + mpImpl->mpSelEngine->CursorPosChanging( bSelect, aTranslatedKeyEvent.GetKeyCode().IsMod1() ); + + if ( aOldEnd != aPaM ) + { + mpImpl->mpTextEngine->CursorMoved( aOldEnd.GetPara() ); + + TextSelection aNewSelection( mpImpl->maSelection ); + aNewSelection.GetEnd() = aPaM; + if ( bSelect ) + { + // extend the selection + ImpSetSelection( aNewSelection ); + ShowSelection( TextSelection( aOldEnd, aPaM ) ); + } + else + { + aNewSelection.GetStart() = aPaM; + ImpSetSelection( aNewSelection ); + } + } + + return mpImpl->maSelection; +} + +void TextView::InsertText( const OUString& rStr ) +{ + mpImpl->mpTextEngine->UndoActionStart(); + + TextSelection aNewSel = mpImpl->mpTextEngine->ImpInsertText( mpImpl->maSelection, rStr ); + + ImpSetSelection( aNewSel ); + + mpImpl->mpTextEngine->UndoActionEnd(); + + mpImpl->mpTextEngine->FormatAndUpdate( this ); +} + +TextPaM TextView::CursorLeft( const TextPaM& rPaM, sal_uInt16 nCharacterIteratorMode ) +{ + TextPaM aPaM( rPaM ); + + if ( aPaM.GetIndex() ) + { + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aPaM.GetPara() ].get(); + css::uno::Reference < css::i18n::XBreakIterator > xBI = mpImpl->mpTextEngine->GetBreakIterator(); + sal_Int32 nCount = 1; + aPaM.GetIndex() = xBI->previousCharacters( pNode->GetText(), aPaM.GetIndex(), mpImpl->mpTextEngine->GetLocale(), nCharacterIteratorMode, nCount, nCount ); + } + else if ( aPaM.GetPara() ) + { + aPaM.GetPara()--; + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aPaM.GetPara() ].get(); + aPaM.GetIndex() = pNode->GetText().getLength(); + } + return aPaM; +} + +TextPaM TextView::CursorRight( const TextPaM& rPaM, sal_uInt16 nCharacterIteratorMode ) +{ + TextPaM aPaM( rPaM ); + + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aPaM.GetPara() ].get(); + if ( aPaM.GetIndex() < pNode->GetText().getLength() ) + { + css::uno::Reference < css::i18n::XBreakIterator > xBI = mpImpl->mpTextEngine->GetBreakIterator(); + sal_Int32 nCount = 1; + aPaM.GetIndex() = xBI->nextCharacters( pNode->GetText(), aPaM.GetIndex(), mpImpl->mpTextEngine->GetLocale(), nCharacterIteratorMode, nCount, nCount ); + } + else if ( aPaM.GetPara() < ( mpImpl->mpTextEngine->mpDoc->GetNodes().size()-1) ) + { + aPaM.GetPara()++; + aPaM.GetIndex() = 0; + } + + return aPaM; +} + +TextPaM TextView::CursorFirstWord( const TextPaM& rPaM ) +{ + TextPaM aPaM(rPaM); + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[aPaM.GetPara()].get(); + + css::uno::Reference<css::i18n::XBreakIterator> xBI = mpImpl->mpTextEngine->GetBreakIterator(); + aPaM.GetIndex() = xBI->beginOfSentence(pNode->GetText(), 0, mpImpl->mpTextEngine->GetLocale()); + + return aPaM; +} + +TextPaM TextView::CursorWordLeft( const TextPaM& rPaM ) +{ + TextPaM aPaM( rPaM ); + + if ( aPaM.GetIndex() ) + { + // tdf#57879 - expand selection to the left to include connector punctuations + mpImpl->mpTextEngine->GetWord( rPaM, &aPaM ); + if ( aPaM.GetIndex() >= rPaM.GetIndex() ) + { + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aPaM.GetPara() ].get(); + css::uno::Reference < css::i18n::XBreakIterator > xBI = mpImpl->mpTextEngine->GetBreakIterator(); + aPaM.GetIndex() = xBI->previousWord( pNode->GetText(), rPaM.GetIndex(), mpImpl->mpTextEngine->GetLocale(), css::i18n::WordType::ANYWORD_IGNOREWHITESPACES ).startPos; + if ( aPaM.GetIndex() > 0 ) + mpImpl->mpTextEngine->GetWord( aPaM, &aPaM ); + else + aPaM.GetIndex() = 0; + } + } + else if ( aPaM.GetPara() ) + { + aPaM.GetPara()--; + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aPaM.GetPara() ].get(); + aPaM.GetIndex() = pNode->GetText().getLength(); + } + return aPaM; +} + +TextPaM TextView::CursorWordRight( const TextPaM& rPaM ) +{ + TextPaM aPaM( rPaM ); + + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aPaM.GetPara() ].get(); + if ( aPaM.GetIndex() < pNode->GetText().getLength() ) + { + css::uno::Reference < css::i18n::XBreakIterator > xBI = mpImpl->mpTextEngine->GetBreakIterator(); + aPaM.GetIndex() = xBI->nextWord( pNode->GetText(), aPaM.GetIndex(), mpImpl->mpTextEngine->GetLocale(), css::i18n::WordType::ANYWORD_IGNOREWHITESPACES ).endPos; + mpImpl->mpTextEngine->GetWord( aPaM, nullptr, &aPaM ); + } + else if ( aPaM.GetPara() < ( mpImpl->mpTextEngine->mpDoc->GetNodes().size()-1) ) + { + aPaM.GetPara()++; + aPaM.GetIndex() = 0; + } + + return aPaM; +} + +TextPaM TextView::ImpDelete( sal_uInt8 nMode, sal_uInt8 nDelMode ) +{ + if ( mpImpl->maSelection.HasRange() ) // only delete selection + return mpImpl->mpTextEngine->ImpDeleteText( mpImpl->maSelection ); + + TextPaM aStartPaM = mpImpl->maSelection.GetStart(); + TextPaM aEndPaM = aStartPaM; + if ( nMode == DEL_LEFT ) + { + if ( nDelMode == DELMODE_SIMPLE ) + { + aEndPaM = CursorLeft( aEndPaM, sal_uInt16(css::i18n::CharacterIteratorMode::SKIPCHARACTER) ); + } + else if ( nDelMode == DELMODE_RESTOFWORD ) + { + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aEndPaM.GetPara() ].get(); + css::uno::Reference < css::i18n::XBreakIterator > xBI = mpImpl->mpTextEngine->GetBreakIterator(); + css::i18n::Boundary aBoundary = xBI->getWordBoundary( pNode->GetText(), mpImpl->maSelection.GetEnd().GetIndex(), mpImpl->mpTextEngine->GetLocale(), css::i18n::WordType::ANYWORD_IGNOREWHITESPACES, true ); + if ( aBoundary.startPos == mpImpl->maSelection.GetEnd().GetIndex() ) + aBoundary = xBI->previousWord( pNode->GetText(), mpImpl->maSelection.GetEnd().GetIndex(), mpImpl->mpTextEngine->GetLocale(), css::i18n::WordType::ANYWORD_IGNOREWHITESPACES ); + // #i63506# startPos is -1 when the paragraph starts with a tab + aEndPaM.GetIndex() = std::max<sal_Int32>(aBoundary.startPos, 0); + } + else // DELMODE_RESTOFCONTENT + { + if ( aEndPaM.GetIndex() != 0 ) + aEndPaM.GetIndex() = 0; + else if ( aEndPaM.GetPara() ) + { + // previous paragraph + aEndPaM.GetPara()--; + aEndPaM.GetIndex() = 0; + } + } + } + else + { + if ( nDelMode == DELMODE_SIMPLE ) + { + aEndPaM = CursorRight( aEndPaM, sal_uInt16(css::i18n::CharacterIteratorMode::SKIPCELL) ); + } + else if ( nDelMode == DELMODE_RESTOFWORD ) + { + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aEndPaM.GetPara() ].get(); + css::uno::Reference < css::i18n::XBreakIterator > xBI = mpImpl->mpTextEngine->GetBreakIterator(); + css::i18n::Boundary aBoundary = xBI->nextWord( pNode->GetText(), mpImpl->maSelection.GetEnd().GetIndex(), mpImpl->mpTextEngine->GetLocale(), css::i18n::WordType::ANYWORD_IGNOREWHITESPACES ); + aEndPaM.GetIndex() = aBoundary.startPos; + } + else // DELMODE_RESTOFCONTENT + { + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aEndPaM.GetPara() ].get(); + if ( aEndPaM.GetIndex() < pNode->GetText().getLength() ) + aEndPaM.GetIndex() = pNode->GetText().getLength(); + else if ( aEndPaM.GetPara() < ( mpImpl->mpTextEngine->mpDoc->GetNodes().size() - 1 ) ) + { + // next paragraph + aEndPaM.GetPara()++; + TextNode* pNextNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aEndPaM.GetPara() ].get(); + aEndPaM.GetIndex() = pNextNode->GetText().getLength(); + } + } + } + + return mpImpl->mpTextEngine->ImpDeleteText( TextSelection( aStartPaM, aEndPaM ) ); +} + +TextPaM TextView::CursorUp( const TextPaM& rPaM ) +{ + TextPaM aPaM( rPaM ); + + tools::Long nX; + if ( mpImpl->mnTravelXPos == TRAVEL_X_DONTKNOW ) + { + nX = mpImpl->mpTextEngine->GetEditCursor( rPaM, false ).Left(); + mpImpl->mnTravelXPos = static_cast<sal_uInt16>(nX)+1; + } + else + nX = mpImpl->mnTravelXPos; + + TEParaPortion* pPPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( rPaM.GetPara() ); + std::vector<TextLine>::size_type nLine = pPPortion->GetLineNumber( rPaM.GetIndex(), false ); + if ( nLine ) // same paragraph + { + aPaM.GetIndex() = mpImpl->mpTextEngine->GetCharPos( rPaM.GetPara(), nLine-1, nX ); + // If we need to go to the end of a line that was wrapped automatically, + // the cursor ends up at the beginning of the 2nd line + // Problem: Last character of an automatically wrapped line = Cursor + TextLine& rLine = pPPortion->GetLines()[ nLine - 1 ]; + if ( aPaM.GetIndex() && ( aPaM.GetIndex() == rLine.GetEnd() ) ) + --aPaM.GetIndex(); + } + else if ( rPaM.GetPara() ) // previous paragraph + { + aPaM.GetPara()--; + pPPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( aPaM.GetPara() ); + std::vector<TextLine>::size_type nL = pPPortion->GetLines().size() - 1; + aPaM.GetIndex() = mpImpl->mpTextEngine->GetCharPos( aPaM.GetPara(), nL, nX+1 ); + } + + return aPaM; +} + +TextPaM TextView::CursorDown( const TextPaM& rPaM ) +{ + TextPaM aPaM( rPaM ); + + tools::Long nX; + if ( mpImpl->mnTravelXPos == TRAVEL_X_DONTKNOW ) + { + nX = mpImpl->mpTextEngine->GetEditCursor( rPaM, false ).Left(); + mpImpl->mnTravelXPos = static_cast<sal_uInt16>(nX)+1; + } + else + nX = mpImpl->mnTravelXPos; + + TEParaPortion* pPPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( rPaM.GetPara() ); + std::vector<TextLine>::size_type nLine = pPPortion->GetLineNumber( rPaM.GetIndex(), false ); + if ( nLine < ( pPPortion->GetLines().size() - 1 ) ) + { + aPaM.GetIndex() = mpImpl->mpTextEngine->GetCharPos( rPaM.GetPara(), nLine+1, nX ); + + // special case CursorUp + TextLine& rLine = pPPortion->GetLines()[ nLine + 1 ]; + if ( ( aPaM.GetIndex() == rLine.GetEnd() ) && ( aPaM.GetIndex() > rLine.GetStart() ) && aPaM.GetIndex() < pPPortion->GetNode()->GetText().getLength() ) + --aPaM.GetIndex(); + } + else if ( rPaM.GetPara() < ( mpImpl->mpTextEngine->mpDoc->GetNodes().size() - 1 ) ) // next paragraph + { + aPaM.GetPara()++; + pPPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( aPaM.GetPara() ); + aPaM.GetIndex() = mpImpl->mpTextEngine->GetCharPos( aPaM.GetPara(), 0, nX+1 ); + TextLine& rLine = pPPortion->GetLines().front(); + if ( ( aPaM.GetIndex() == rLine.GetEnd() ) && ( aPaM.GetIndex() > rLine.GetStart() ) && ( pPPortion->GetLines().size() > 1 ) ) + --aPaM.GetIndex(); + } + + return aPaM; +} + +TextPaM TextView::CursorStartOfLine( const TextPaM& rPaM ) +{ + TextPaM aPaM( rPaM ); + + TEParaPortion* pPPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( rPaM.GetPara() ); + std::vector<TextLine>::size_type nLine = pPPortion->GetLineNumber( aPaM.GetIndex(), false ); + TextLine& rLine = pPPortion->GetLines()[ nLine ]; + aPaM.GetIndex() = rLine.GetStart(); + + return aPaM; +} + +TextPaM TextView::CursorEndOfLine( const TextPaM& rPaM ) +{ + TextPaM aPaM( rPaM ); + + TEParaPortion* pPPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( rPaM.GetPara() ); + std::vector<TextLine>::size_type nLine = pPPortion->GetLineNumber( aPaM.GetIndex(), false ); + TextLine& rLine = pPPortion->GetLines()[ nLine ]; + aPaM.GetIndex() = rLine.GetEnd(); + + if ( rLine.GetEnd() > rLine.GetStart() ) // empty line + { + sal_Unicode cLastChar = pPPortion->GetNode()->GetText()[ aPaM.GetIndex()-1 ]; + if ( ( cLastChar == ' ' ) && ( aPaM.GetIndex() != pPPortion->GetNode()->GetText().getLength() ) ) + { + // for a blank in an automatically-wrapped line it is better to stand before it, + // as the user will intend to stand behind the prior word. + // If there is a change, special case for Pos1 after End! + --aPaM.GetIndex(); + } + } + return aPaM; +} + +TextPaM TextView::CursorStartOfParagraph( const TextPaM& rPaM ) +{ + TextPaM aPaM( rPaM ); + aPaM.GetIndex() = 0; + return aPaM; +} + +TextPaM TextView::CursorEndOfParagraph( const TextPaM& rPaM ) +{ + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ rPaM.GetPara() ].get(); + TextPaM aPaM( rPaM ); + aPaM.GetIndex() = pNode->GetText().getLength(); + return aPaM; +} + +TextPaM TextView::CursorStartOfDoc() +{ + TextPaM aPaM( 0, 0 ); + return aPaM; +} + +TextPaM TextView::CursorEndOfDoc() +{ + const sal_uInt32 nNode = static_cast<sal_uInt32>(mpImpl->mpTextEngine->mpDoc->GetNodes().size() - 1); + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ nNode ].get(); + TextPaM aPaM( nNode, pNode->GetText().getLength() ); + return aPaM; +} + +TextPaM TextView::PageUp( const TextPaM& rPaM ) +{ + tools::Rectangle aRect = mpImpl->mpTextEngine->PaMtoEditCursor( rPaM ); + Point aTopLeft = aRect.TopLeft(); + aTopLeft.AdjustY( -(mpImpl->mpWindow->GetOutputSizePixel().Height() * 9/10) ); + aTopLeft.AdjustX(1 ); + if ( aTopLeft.Y() < 0 ) + aTopLeft.setY( 0 ); + + TextPaM aPaM = mpImpl->mpTextEngine->GetPaM( aTopLeft ); + return aPaM; +} + +TextPaM TextView::PageDown( const TextPaM& rPaM ) +{ + tools::Rectangle aRect = mpImpl->mpTextEngine->PaMtoEditCursor( rPaM ); + Point aBottomRight = aRect.BottomRight(); + aBottomRight.AdjustY(mpImpl->mpWindow->GetOutputSizePixel().Height() * 9/10 ); + aBottomRight.AdjustX(1 ); + tools::Long nHeight = mpImpl->mpTextEngine->GetTextHeight(); + if ( aBottomRight.Y() > nHeight ) + aBottomRight.setY( nHeight-1 ); + + TextPaM aPaM = mpImpl->mpTextEngine->GetPaM( aBottomRight ); + return aPaM; +} + +void TextView::ImpShowCursor( bool bGotoCursor, bool bForceVisCursor, bool bSpecial ) +{ + if ( mpImpl->mpTextEngine->IsFormatting() ) + return; + if ( !mpImpl->mpTextEngine->GetUpdateMode() ) + return; + if ( mpImpl->mpTextEngine->IsInUndo() ) + return; + + mpImpl->mpTextEngine->CheckIdleFormatter(); + if ( !mpImpl->mpTextEngine->IsFormatted() ) + mpImpl->mpTextEngine->FormatAndUpdate( this ); + + TextPaM aPaM( mpImpl->maSelection.GetEnd() ); + tools::Rectangle aEditCursor = mpImpl->mpTextEngine->PaMtoEditCursor( aPaM, bSpecial ); + + // Remember that we placed the cursor behind the last character of a line + mpImpl->mbCursorAtEndOfLine = false; + if( bSpecial ) + { + TEParaPortion* pParaPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( aPaM.GetPara() ); + mpImpl->mbCursorAtEndOfLine = + pParaPortion->GetLineNumber( aPaM.GetIndex(), true ) != pParaPortion->GetLineNumber( aPaM.GetIndex(), false ); + } + + if ( !IsInsertMode() && !mpImpl->maSelection.HasRange() ) + { + TextNode* pNode = mpImpl->mpTextEngine->mpDoc->GetNodes()[ aPaM.GetPara() ].get(); + if ( !pNode->GetText().isEmpty() && ( aPaM.GetIndex() < pNode->GetText().getLength() ) ) + { + // If we are behind a portion, and the next portion has other direction, we must change position... + aEditCursor.SetLeft( mpImpl->mpTextEngine->GetEditCursor( aPaM, false, true ).Left() ); + aEditCursor.SetRight( aEditCursor.Left() ); + + TEParaPortion* pParaPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( aPaM.GetPara() ); + + sal_Int32 nTextPortionStart = 0; + std::size_t nTextPortion = pParaPortion->GetTextPortions().FindPortion( aPaM.GetIndex(), nTextPortionStart, true ); + TETextPortion& rTextPortion = pParaPortion->GetTextPortions()[ nTextPortion ]; + if ( rTextPortion.GetKind() == PORTIONKIND_TAB ) + { + aEditCursor.AdjustRight(rTextPortion.GetWidth() ); + } + else + { + TextPaM aNext = CursorRight( TextPaM( aPaM.GetPara(), aPaM.GetIndex() ), sal_uInt16(css::i18n::CharacterIteratorMode::SKIPCELL) ); + aEditCursor.SetRight( mpImpl->mpTextEngine->GetEditCursor( aNext, true ).Left() ); + } + } + } + + Size aOutSz = mpImpl->mpWindow->GetOutputSizePixel(); + if ( aEditCursor.GetHeight() > aOutSz.Height() ) + aEditCursor.SetBottom( aEditCursor.Top() + aOutSz.Height() - 1 ); + + aEditCursor.AdjustLeft( -1 ); + + if ( bGotoCursor + // #i81283# protect maStartDocPos against initialization problems + && aOutSz.Width() && aOutSz.Height() + ) + { + tools::Long nVisStartY = mpImpl->maStartDocPos.Y(); + tools::Long nVisEndY = mpImpl->maStartDocPos.Y() + aOutSz.Height(); + tools::Long nVisStartX = mpImpl->maStartDocPos.X(); + tools::Long nVisEndX = mpImpl->maStartDocPos.X() + aOutSz.Width(); + tools::Long nMoreX = aOutSz.Width() / 4; + + Point aNewStartPos( mpImpl->maStartDocPos ); + + if ( aEditCursor.Bottom() > nVisEndY ) + { + aNewStartPos.AdjustY( aEditCursor.Bottom() - nVisEndY); + } + else if ( aEditCursor.Top() < nVisStartY ) + { + aNewStartPos.AdjustY( -( nVisStartY - aEditCursor.Top() ) ); + } + + if ( aEditCursor.Right() >= nVisEndX ) + { + aNewStartPos.AdjustX( aEditCursor.Right() - nVisEndX ); + + // do you want some more? + aNewStartPos.AdjustX(nMoreX ); + } + else if ( aEditCursor.Left() <= nVisStartX ) + { + aNewStartPos.AdjustX( -( nVisStartX - aEditCursor.Left() ) ); + + // do you want some more? + aNewStartPos.AdjustX( -nMoreX ); + } + + // X can be wrong for the 'some more' above: +// sal_uLong nMaxTextWidth = mpImpl->mpTextEngine->GetMaxTextWidth(); +// if ( !nMaxTextWidth || ( nMaxTextWidth > 0x7FFFFFFF ) ) +// nMaxTextWidth = 0x7FFFFFFF; +// long nMaxX = (long)nMaxTextWidth - aOutSz.Width(); + tools::Long nMaxX = mpImpl->mpTextEngine->CalcTextWidth() - aOutSz.Width(); + if ( nMaxX < 0 ) + nMaxX = 0; + + if ( aNewStartPos.X() < 0 ) + aNewStartPos.setX( 0 ); + else if ( aNewStartPos.X() > nMaxX ) + aNewStartPos.setX( nMaxX ); + + // Y should not be further down than needed + tools::Long nYMax = mpImpl->mpTextEngine->GetTextHeight() - aOutSz.Height(); + if ( nYMax < 0 ) + nYMax = 0; + if ( aNewStartPos.Y() > nYMax ) + aNewStartPos.setY( nYMax ); + + if ( aNewStartPos != mpImpl->maStartDocPos ) + Scroll( -(aNewStartPos.X() - mpImpl->maStartDocPos.X()), -(aNewStartPos.Y() - mpImpl->maStartDocPos.Y()) ); + } + + if ( aEditCursor.Right() < aEditCursor.Left() ) + { + tools::Long n = aEditCursor.Left(); + aEditCursor.SetLeft( aEditCursor.Right() ); + aEditCursor.SetRight( n ); + } + + Point aPoint( GetWindowPos( !mpImpl->mpTextEngine->IsRightToLeft() ? aEditCursor.TopLeft() : aEditCursor.TopRight() ) ); + mpImpl->mpCursor->SetPos( aPoint ); + mpImpl->mpCursor->SetSize( aEditCursor.GetSize() ); + if ( bForceVisCursor && mpImpl->mbCursorEnabled ) + mpImpl->mpCursor->Show(); +} + +void TextView::SetCursorAtPoint( const Point& rPosPixel ) +{ + mpImpl->mpTextEngine->CheckIdleFormatter(); + + Point aDocPos = GetDocPos( rPosPixel ); + + TextPaM aPaM = mpImpl->mpTextEngine->GetPaM( aDocPos ); + + // aTmpNewSel: Diff between old and new; not the new selection + TextSelection aTmpNewSel( mpImpl->maSelection.GetEnd(), aPaM ); + TextSelection aNewSel( mpImpl->maSelection ); + aNewSel.GetEnd() = aPaM; + + if ( !mpImpl->mpSelEngine->HasAnchor() ) + { + if ( mpImpl->maSelection.GetStart() != aPaM ) + mpImpl->mpTextEngine->CursorMoved( mpImpl->maSelection.GetStart().GetPara() ); + aNewSel.GetStart() = aPaM; + ImpSetSelection( aNewSel ); + } + else + { + ImpSetSelection( aNewSel ); + ShowSelection( aTmpNewSel ); + } + + bool bForceCursor = !mpImpl->mpDDInfo; // && !mbInSelection + ImpShowCursor( mpImpl->mbAutoScroll, bForceCursor, false ); +} + +bool TextView::IsSelectionAtPoint( const Point& rPosPixel ) +{ + Point aDocPos = GetDocPos( rPosPixel ); + TextPaM aPaM = mpImpl->mpTextEngine->GetPaM( aDocPos ); + // BeginDrag is only called, however, if IsSelectionAtPoint() + // Problem: IsSelectionAtPoint is not called by Command() + // if before MBDown returned false. + return IsInSelection( aPaM ); +} + +bool TextView::IsInSelection( const TextPaM& rPaM ) const +{ + TextSelection aSel = mpImpl->maSelection; + aSel.Justify(); + + const sal_uInt32 nStartNode = aSel.GetStart().GetPara(); + const sal_uInt32 nEndNode = aSel.GetEnd().GetPara(); + const sal_uInt32 nCurNode = rPaM.GetPara(); + + if ( ( nCurNode > nStartNode ) && ( nCurNode < nEndNode ) ) + return true; + + if ( nStartNode == nEndNode ) + { + if ( nCurNode == nStartNode ) + if ( ( rPaM.GetIndex() >= aSel.GetStart().GetIndex() ) && ( rPaM.GetIndex() < aSel.GetEnd().GetIndex() ) ) + return true; + } + else if ( ( nCurNode == nStartNode ) && ( rPaM.GetIndex() >= aSel.GetStart().GetIndex() ) ) + return true; + else if ( ( nCurNode == nEndNode ) && ( rPaM.GetIndex() < aSel.GetEnd().GetIndex() ) ) + return true; + + return false; +} + +void TextView::ImpHideDDCursor() +{ + if ( mpImpl->mpDDInfo && mpImpl->mpDDInfo->mbVisCursor ) + { + mpImpl->mpDDInfo->maCursor.Hide(); + mpImpl->mpDDInfo->mbVisCursor = false; + } +} + +void TextView::ImpShowDDCursor() +{ + if ( !mpImpl->mpDDInfo->mbVisCursor ) + { + tools::Rectangle aCursor = mpImpl->mpTextEngine->PaMtoEditCursor( mpImpl->mpDDInfo->maDropPos, true ); + aCursor.AdjustRight( 1 ); + aCursor.SetPos( GetWindowPos( aCursor.TopLeft() ) ); + + mpImpl->mpDDInfo->maCursor.SetWindow( mpImpl->mpWindow ); + mpImpl->mpDDInfo->maCursor.SetPos( aCursor.TopLeft() ); + mpImpl->mpDDInfo->maCursor.SetSize( aCursor.GetSize() ); + mpImpl->mpDDInfo->maCursor.Show(); + mpImpl->mpDDInfo->mbVisCursor = true; + } +} + +void TextView::SetPaintSelection( bool bPaint ) +{ + if ( bPaint != mpImpl->mbPaintSelection ) + { + mpImpl->mbPaintSelection = bPaint; + ShowSelection( mpImpl->maSelection ); + } +} + +void TextView::Read( SvStream& rInput ) +{ + mpImpl->mpTextEngine->Read( rInput, &mpImpl->maSelection ); + ShowCursor(); +} + +bool TextView::ImplTruncateNewText( OUString& rNewText ) const +{ + bool bTruncated = false; + + const sal_Int32 nMaxLen = mpImpl->mpTextEngine->GetMaxTextLen(); + // 0 means unlimited + if( nMaxLen != 0 ) + { + const sal_Int32 nCurLen = mpImpl->mpTextEngine->GetTextLen(); + + const sal_Int32 nNewLen = rNewText.getLength(); + if ( nCurLen + nNewLen > nMaxLen ) + { + // see how much text will be replaced + const sal_Int32 nSelLen = mpImpl->mpTextEngine->GetTextLen( mpImpl->maSelection ); + if ( nCurLen + nNewLen - nSelLen > nMaxLen ) + { + const sal_Int32 nTruncatedLen = nMaxLen - (nCurLen - nSelLen); + rNewText = rNewText.copy( 0, nTruncatedLen ); + bTruncated = true; + } + } + } + return bTruncated; +} + +bool TextView::ImplCheckTextLen( std::u16string_view rNewText ) const +{ + bool bOK = true; + if ( mpImpl->mpTextEngine->GetMaxTextLen() ) + { + sal_Int32 n = mpImpl->mpTextEngine->GetTextLen() + rNewText.size(); + if ( n > mpImpl->mpTextEngine->GetMaxTextLen() ) + { + // calculate how much text is being deleted + n -= mpImpl->mpTextEngine->GetTextLen( mpImpl->maSelection ); + if ( n > mpImpl->mpTextEngine->GetMaxTextLen() ) + bOK = false; + } + } + return bOK; +} + +void TextView::dragGestureRecognized( const css::datatransfer::dnd::DragGestureEvent& rDGE ) +{ + if ( !mpImpl->mbClickedInSelection ) + return; + + SolarMutexGuard aVclGuard; + + SAL_WARN_IF( !mpImpl->maSelection.HasRange(), "vcl", "TextView::dragGestureRecognized: mpImpl->mbClickedInSelection, but no selection?" ); + + mpImpl->mpDDInfo.reset(new TextDDInfo); + mpImpl->mpDDInfo->mbStarterOfDD = true; + + rtl::Reference<TETextDataObject> pDataObj = new TETextDataObject( GetSelected() ); + + mpImpl->mpCursor->Hide(); + + sal_Int8 nActions = css::datatransfer::dnd::DNDConstants::ACTION_COPY; + if ( !IsReadOnly() ) + nActions |= css::datatransfer::dnd::DNDConstants::ACTION_MOVE; + rDGE.DragSource->startDrag( rDGE, nActions, 0 /*cursor*/, 0 /*image*/, pDataObj, mpImpl->mxDnDListener ); +} + +void TextView::dragDropEnd( const css::datatransfer::dnd::DragSourceDropEvent& ) +{ + ImpHideDDCursor(); + mpImpl->mpDDInfo.reset(); +} + +void TextView::drop( const css::datatransfer::dnd::DropTargetDropEvent& rDTDE ) +{ + SolarMutexGuard aVclGuard; + + if ( !mpImpl->mbReadOnly && mpImpl->mpDDInfo ) + { + ImpHideDDCursor(); + + // Data for deleting after DROP_MOVE: + TextSelection aPrevSel( mpImpl->maSelection ); + aPrevSel.Justify(); + const sal_uInt32 nPrevParaCount = mpImpl->mpTextEngine->GetParagraphCount(); + const sal_Int32 nPrevStartParaLen = mpImpl->mpTextEngine->GetTextLen( aPrevSel.GetStart().GetPara() ); + + bool bStarterOfDD = false; + for ( sal_uInt16 nView = mpImpl->mpTextEngine->GetViewCount(); nView && !bStarterOfDD; ) + bStarterOfDD = mpImpl->mpTextEngine->GetView( --nView )->mpImpl->mpDDInfo && mpImpl->mpTextEngine->GetView( nView )->mpImpl->mpDDInfo->mbStarterOfDD; + + HideSelection(); + ImpSetSelection( mpImpl->mpDDInfo->maDropPos ); + + mpImpl->mpTextEngine->UndoActionStart(); + + OUString aText; + css::uno::Reference< css::datatransfer::XTransferable > xDataObj = rDTDE.Transferable; + if ( xDataObj.is() ) + { + css::datatransfer::DataFlavor aFlavor; + SotExchange::GetFormatDataFlavor( SotClipboardFormatId::STRING, aFlavor ); + if ( xDataObj->isDataFlavorSupported( aFlavor ) ) + { + css::uno::Any aData = xDataObj->getTransferData( aFlavor ); + OUString aOUString; + aData >>= aOUString; + aText = convertLineEnd(aOUString, LINEEND_LF); + } + } + + if ( !aText.isEmpty() && ( aText[ aText.getLength()-1 ] == LINE_SEP ) ) + aText = aText.copy(0, aText.getLength()-1); + + if ( ImplCheckTextLen( aText ) ) + ImpSetSelection( mpImpl->mpTextEngine->ImpInsertText( mpImpl->mpDDInfo->maDropPos, aText ) ); + + if ( aPrevSel.HasRange() && + (( rDTDE.DropAction & css::datatransfer::dnd::DNDConstants::ACTION_MOVE ) || !bStarterOfDD) ) + { + // adjust selection if necessary + if ( ( mpImpl->mpDDInfo->maDropPos.GetPara() < aPrevSel.GetStart().GetPara() ) || + ( ( mpImpl->mpDDInfo->maDropPos.GetPara() == aPrevSel.GetStart().GetPara() ) + && ( mpImpl->mpDDInfo->maDropPos.GetIndex() < aPrevSel.GetStart().GetIndex() ) ) ) + { + const sal_uInt32 nNewParasBeforeSelection = + mpImpl->mpTextEngine->GetParagraphCount() - nPrevParaCount; + + aPrevSel.GetStart().GetPara() += nNewParasBeforeSelection; + aPrevSel.GetEnd().GetPara() += nNewParasBeforeSelection; + + if ( mpImpl->mpDDInfo->maDropPos.GetPara() == aPrevSel.GetStart().GetPara() ) + { + const sal_Int32 nNewChars = + mpImpl->mpTextEngine->GetTextLen( aPrevSel.GetStart().GetPara() ) - nPrevStartParaLen; + + aPrevSel.GetStart().GetIndex() += nNewChars; + if ( aPrevSel.GetStart().GetPara() == aPrevSel.GetEnd().GetPara() ) + aPrevSel.GetEnd().GetIndex() += nNewChars; + } + } + else + { + // adjust current selection + TextPaM aPaM = mpImpl->maSelection.GetStart(); + aPaM.GetPara() -= ( aPrevSel.GetEnd().GetPara() - aPrevSel.GetStart().GetPara() ); + if ( aPrevSel.GetEnd().GetPara() == mpImpl->mpDDInfo->maDropPos.GetPara() ) + { + aPaM.GetIndex() -= aPrevSel.GetEnd().GetIndex(); + if ( aPrevSel.GetStart().GetPara() == mpImpl->mpDDInfo->maDropPos.GetPara() ) + aPaM.GetIndex() += aPrevSel.GetStart().GetIndex(); + } + ImpSetSelection( aPaM ); + + } + mpImpl->mpTextEngine->ImpDeleteText( aPrevSel ); + } + + mpImpl->mpTextEngine->UndoActionEnd(); + + mpImpl->mpDDInfo.reset(); + + mpImpl->mpTextEngine->FormatAndUpdate( this ); + + mpImpl->mpTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + } + rDTDE.Context->dropComplete( false/*bChanges*/ ); +} + +void TextView::dragEnter( const css::datatransfer::dnd::DropTargetDragEnterEvent& ) +{ +} + +void TextView::dragExit( const css::datatransfer::dnd::DropTargetEvent& ) +{ + SolarMutexGuard aVclGuard; + ImpHideDDCursor(); +} + +void TextView::dragOver( const css::datatransfer::dnd::DropTargetDragEvent& rDTDE ) +{ + SolarMutexGuard aVclGuard; + + if (!mpImpl->mpDDInfo) + mpImpl->mpDDInfo.reset(new TextDDInfo); + + TextPaM aPrevDropPos = mpImpl->mpDDInfo->maDropPos; + Point aMousePos( rDTDE.LocationX, rDTDE.LocationY ); + Point aDocPos = GetDocPos( aMousePos ); + mpImpl->mpDDInfo->maDropPos = mpImpl->mpTextEngine->GetPaM( aDocPos ); + + // Don't drop in selection or in read only engine + if ( IsReadOnly() || IsInSelection( mpImpl->mpDDInfo->maDropPos )) + { + ImpHideDDCursor(); + rDTDE.Context->rejectDrag(); + } + else + { + // delete old Cursor + if ( !mpImpl->mpDDInfo->mbVisCursor || ( aPrevDropPos != mpImpl->mpDDInfo->maDropPos ) ) + { + ImpHideDDCursor(); + ImpShowDDCursor(); + } + rDTDE.Context->acceptDrag( rDTDE.DropAction ); + } +} + +Point TextView::ImpGetOutputStartPos( const Point& rStartDocPos ) const +{ + Point aStartPos( -rStartDocPos.X(), -rStartDocPos.Y() ); + if ( mpImpl->mpTextEngine->IsRightToLeft() ) + { + Size aSz = mpImpl->mpWindow->GetOutputSizePixel(); + aStartPos.setX( rStartDocPos.X() + aSz.Width() - 1 ); // -1: Start is 0 + } + return aStartPos; +} + +Point TextView::GetDocPos( const Point& rWindowPos ) const +{ + // Window Position => Document Position + + Point aPoint; + + aPoint.setY( rWindowPos.Y() + mpImpl->maStartDocPos.Y() ); + + if ( !mpImpl->mpTextEngine->IsRightToLeft() ) + { + aPoint.setX( rWindowPos.X() + mpImpl->maStartDocPos.X() ); + } + else + { + Size aSz = mpImpl->mpWindow->GetOutputSizePixel(); + aPoint.setX( ( aSz.Width() - 1 ) - rWindowPos.X() + mpImpl->maStartDocPos.X() ); + } + + return aPoint; +} + +Point TextView::GetWindowPos( const Point& rDocPos ) const +{ + // Document Position => Window Position + + Point aPoint; + + aPoint.setY( rDocPos.Y() - mpImpl->maStartDocPos.Y() ); + + if ( !mpImpl->mpTextEngine->IsRightToLeft() ) + { + aPoint.setX( rDocPos.X() - mpImpl->maStartDocPos.X() ); + } + else + { + Size aSz = mpImpl->mpWindow->GetOutputSizePixel(); + aPoint.setX( ( aSz.Width() - 1 ) - ( rDocPos.X() - mpImpl->maStartDocPos.X() ) ); + } + + return aPoint; +} + +sal_Int32 TextView::GetLineNumberOfCursorInSelection() const +{ + // PROGRESS + sal_Int32 nLineNo = -1; + if( mpImpl->mbCursorEnabled ) + { + TextPaM aPaM = GetSelection().GetEnd(); + TEParaPortion* pPPortion = mpImpl->mpTextEngine->mpTEParaPortions->GetObject( aPaM.GetPara() ); + nLineNo = pPPortion->GetLineNumber( aPaM.GetIndex(), false ); + //TODO: std::vector<TextLine>::size_type -> sal_Int32! + if( mpImpl->mbCursorAtEndOfLine ) + --nLineNo; + } + return nLineNo; +} + +// (+) class TextSelFunctionSet + +TextSelFunctionSet::TextSelFunctionSet( TextView* pView ) +{ + mpView = pView; +} + +void TextSelFunctionSet::BeginDrag() +{ +} + +void TextSelFunctionSet::CreateAnchor() +{ +// TextSelection aSel( mpView->GetSelection() ); +// aSel.GetStart() = aSel.GetEnd(); +// mpView->SetSelection( aSel ); + + // may not be followed by ShowCursor + mpView->HideSelection(); + mpView->ImpSetSelection( mpView->mpImpl->maSelection.GetEnd() ); +} + +void TextSelFunctionSet::SetCursorAtPoint( const Point& rPointPixel, bool ) +{ + mpView->SetCursorAtPoint( rPointPixel ); +} + +bool TextSelFunctionSet::IsSelectionAtPoint( const Point& rPointPixel ) +{ + return mpView->IsSelectionAtPoint( rPointPixel ); +} + +void TextSelFunctionSet::DeselectAll() +{ + CreateAnchor(); +} + +void TextSelFunctionSet::DeselectAtPoint( const Point& ) +{ + // only for multiple selection +} + +void TextSelFunctionSet::DestroyAnchor() +{ + // only for multiple selection +} +TextEngine* TextView::GetTextEngine() const +{ return mpImpl->mpTextEngine; } +vcl::Window* TextView::GetWindow() const +{ return mpImpl->mpWindow; } +void TextView::EnableCursor( bool bEnable ) +{ mpImpl->mbCursorEnabled = bEnable; } +bool TextView::IsCursorEnabled() const +{ return mpImpl->mbCursorEnabled; } +void TextView::SetStartDocPos( const Point& rPos ) +{ mpImpl->maStartDocPos = rPos; } +const Point& TextView::GetStartDocPos() const +{ return mpImpl->maStartDocPos; } +void TextView::SetAutoIndentMode( bool bAutoIndent ) +{ mpImpl->mbAutoIndent = bAutoIndent; } +bool TextView::IsReadOnly() const +{ return mpImpl->mbReadOnly; } +void TextView::SetAutoScroll( bool bAutoScroll ) +{ mpImpl->mbAutoScroll = bAutoScroll; } +bool TextView::IsAutoScroll() const +{ return mpImpl->mbAutoScroll; } +bool TextView::HasSelection() const +{ return mpImpl->maSelection.HasRange(); } +bool TextView::IsInsertMode() const +{ return mpImpl->mbInsertMode; } + +void TextView::MatchGroup() +{ + TextSelection aTmpSel( GetSelection() ); + aTmpSel.Justify(); + if ( ( aTmpSel.GetStart().GetPara() != aTmpSel.GetEnd().GetPara() ) || + ( ( aTmpSel.GetEnd().GetIndex() - aTmpSel.GetStart().GetIndex() ) > 1 ) ) + { + return; + } + + TextSelection aMatchSel = static_cast<ExtTextEngine*>(GetTextEngine())->MatchGroup( aTmpSel.GetStart() ); + if ( aMatchSel.HasRange() ) + SetSelection( aMatchSel ); +} + +void TextView::CenterPaM( const TextPaM& rPaM ) +{ + // Get textview size and the corresponding y-coordinates + Size aOutSz = mpImpl->mpWindow->GetOutputSizePixel(); + tools::Long nVisStartY = mpImpl->maStartDocPos.Y(); + tools::Long nVisEndY = mpImpl->maStartDocPos.Y() + aOutSz.Height(); + + // Retrieve the coordinates of the PaM + tools::Rectangle aRect = mpImpl->mpTextEngine->PaMtoEditCursor(rPaM); + + // Recalculate the offset of the center y-coordinates and scroll + Scroll(0, (nVisStartY + nVisEndY) / 2 - aRect.TopLeft().getY()); +} + +bool TextView::Search( const i18nutil::SearchOptions2& rSearchOptions, bool bForward ) +{ + bool bFound = false; + TextSelection aSel( GetSelection() ); + if ( static_cast<ExtTextEngine*>(GetTextEngine())->Search( aSel, rSearchOptions, bForward ) ) + { + bFound = true; + // First add the beginning of the word to the selection, + // so that the whole word is in the visible region. + SetSelection( aSel.GetStart() ); + ShowCursor( true, false ); + } + else + { + aSel = GetSelection().GetEnd(); + } + + SetSelection( aSel ); + // tdf#49482: Move the start of the selection to the center of the textview + if (bFound) + { + CenterPaM( aSel.GetStart() ); + } + ShowCursor(); + + return bFound; +} + +sal_uInt16 TextView::Replace( const i18nutil::SearchOptions2& rSearchOptions, bool bAll, bool bForward ) +{ + sal_uInt16 nFound = 0; + + if ( !bAll ) + { + if ( GetSelection().HasRange() ) + { + InsertText( rSearchOptions.replaceString ); + nFound = 1; + Search( rSearchOptions, bForward ); // right away to the next + } + else + { + if( Search( rSearchOptions, bForward ) ) + nFound = 1; + } + } + else + { + // the writer replaces all, from beginning to end + + ExtTextEngine* pTextEngine = static_cast<ExtTextEngine*>(GetTextEngine()); + + // HideSelection(); + TextSelection aSel; + + bool bSearchInSelection = (0 != (rSearchOptions.searchFlag & css::util::SearchFlags::REG_NOT_BEGINOFLINE) ); + if ( bSearchInSelection ) + { + aSel = GetSelection(); + aSel.Justify(); + } + + TextSelection aSearchSel( aSel ); + + bool bFound = pTextEngine->Search( aSel, rSearchOptions ); + if ( bFound ) + pTextEngine->UndoActionStart(); + while ( bFound ) + { + nFound++; + + TextPaM aNewStart = pTextEngine->ImpInsertText( aSel, rSearchOptions.replaceString ); + // tdf#64690 - extend selection to include inserted text portions + if ( aSel.GetEnd().GetPara() == aSearchSel.GetEnd().GetPara() ) + { + aSearchSel.GetEnd().GetIndex() += rSearchOptions.replaceString.getLength() - 1; + } + aSel = aSearchSel; + aSel.GetStart() = aNewStart; + bFound = pTextEngine->Search( aSel, rSearchOptions ); + } + if ( nFound ) + { + SetSelection( aSel.GetStart() ); + pTextEngine->FormatAndUpdate( this ); + pTextEngine->UndoActionEnd(); + } + } + return nFound; +} + +bool TextView::ImpIndentBlock( bool bRight ) +{ + bool bDone = false; + + TextSelection aSel = GetSelection(); + aSel.Justify(); + + HideSelection(); + GetTextEngine()->UndoActionStart(); + + const sal_uInt32 nStartPara = aSel.GetStart().GetPara(); + sal_uInt32 nEndPara = aSel.GetEnd().GetPara(); + if ( aSel.HasRange() && !aSel.GetEnd().GetIndex() ) + { + nEndPara--; // do not indent + } + + for ( sal_uInt32 nPara = nStartPara; nPara <= nEndPara; ++nPara ) + { + if ( bRight ) + { + // add tabs + GetTextEngine()->ImpInsertText( TextPaM( nPara, 0 ), '\t' ); + bDone = true; + } + else + { + // remove Tabs/Blanks + OUString aText = GetTextEngine()->GetText( nPara ); + if ( !aText.isEmpty() && ( + ( aText[ 0 ] == '\t' ) || + ( aText[ 0 ] == ' ' ) ) ) + { + GetTextEngine()->ImpDeleteText( TextSelection( TextPaM( nPara, 0 ), TextPaM( nPara, 1 ) ) ); + bDone = true; + } + } + } + + GetTextEngine()->UndoActionEnd(); + + bool bRange = aSel.HasRange(); + if ( bRight ) + { + ++aSel.GetStart().GetIndex(); + if ( bRange && ( aSel.GetEnd().GetPara() == nEndPara ) ) + ++aSel.GetEnd().GetIndex(); + } + else + { + if ( aSel.GetStart().GetIndex() ) + --aSel.GetStart().GetIndex(); + if ( bRange && aSel.GetEnd().GetIndex() ) + --aSel.GetEnd().GetIndex(); + } + + ImpSetSelection( aSel ); + GetTextEngine()->FormatAndUpdate( this ); + + return bDone; +} + +bool TextView::IndentBlock() +{ + return ImpIndentBlock( true ); +} + +bool TextView::UnindentBlock() +{ + return ImpIndentBlock( false ); +} + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/txtattr.cxx b/vcl/source/edit/txtattr.cxx new file mode 100644 index 0000000000..8e979c1e3f --- /dev/null +++ b/vcl/source/edit/txtattr.cxx @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <vcl/txtattr.hxx> +#include <vcl/font.hxx> + +TextAttrib::~TextAttrib() +{ +} + +bool TextAttrib::operator==( const TextAttrib& rAttr ) const +{ + return mnWhich == rAttr.mnWhich; +} + +TextAttribFontColor::TextAttribFontColor( const Color& rColor ) + : TextAttrib( TEXTATTR_FONTCOLOR ), maColor( rColor ) +{ +} + +void TextAttribFontColor::SetFont( vcl::Font& rFont ) const +{ + rFont.SetColor( maColor ); +} + +std::unique_ptr<TextAttrib> TextAttribFontColor::Clone() const +{ + return std::unique_ptr<TextAttrib>(new TextAttribFontColor( *this )); +} + +bool TextAttribFontColor::operator==( const TextAttrib& rAttr ) const +{ + return ( ( TextAttrib::operator==(rAttr ) ) && + ( maColor == static_cast<const TextAttribFontColor&>(rAttr).maColor ) ); +} + +TextAttribFontWeight::TextAttribFontWeight( FontWeight eWeight ) + : TextAttrib( TEXTATTR_FONTWEIGHT ), meWeight( eWeight ) +{ +} + +void TextAttribFontWeight::SetFont( vcl::Font& rFont ) const +{ + rFont.SetWeight( meWeight ); +} + +std::unique_ptr<TextAttrib> TextAttribFontWeight::Clone() const +{ + return std::unique_ptr<TextAttrib>(new TextAttribFontWeight( *this )); +} + +bool TextAttribFontWeight::operator==( const TextAttrib& rAttr ) const +{ + return ( ( TextAttrib::operator==(rAttr ) ) && + ( meWeight == static_cast<const TextAttribFontWeight&>(rAttr).meWeight ) ); +} + +TextAttribProtect::TextAttribProtect() : + TextAttrib( TEXTATTR_PROTECTED ) +{ +} + +void TextAttribProtect::SetFont( vcl::Font& ) const +{ +} + +std::unique_ptr<TextAttrib> TextAttribProtect::Clone() const +{ + return std::unique_ptr<TextAttrib>(new TextAttribProtect()); +} + +bool TextAttribProtect::operator==( const TextAttrib& rAttr ) const +{ + return TextAttrib::operator==(rAttr ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/vclmedit.cxx b/vcl/source/edit/vclmedit.cxx new file mode 100644 index 0000000000..d519735859 --- /dev/null +++ b/vcl/source/edit/vclmedit.cxx @@ -0,0 +1,1509 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <memory> +#include <i18nlangtag/languagetag.hxx> +#include <vcl/commandevent.hxx> +#include <vcl/builder.hxx> +#include <vcl/decoview.hxx> +#include <vcl/event.hxx> +#include <vcl/menu.hxx> +#include <vcl/specialchars.hxx> +#include <vcl/timer.hxx> +#include <vcl/vclevent.hxx> +#include <vcl/xtextedt.hxx> +#include <vcl/textview.hxx> +#include <vcl/ptrstyle.hxx> + +#include <svl/undo.hxx> +#include <svl/lstner.hxx> +#include <vcl/uitest/uiobject.hxx> + +#include <vcl/settings.hxx> +#include <vcl/toolkit/scrbar.hxx> +#include <vcl/toolkit/vclmedit.hxx> +#include <vcl/weld.hxx> +#include <osl/diagnose.h> +#include <tools/json_writer.hxx> +#include <strings.hrc> +#include <svdata.hxx> + +class ImpVclMEdit : public SfxListener +{ +private: + VclPtr<VclMultiLineEdit> pVclMultiLineEdit; + + VclPtr<TextWindow> mpTextWindow; + VclPtr<ScrollBar> mpHScrollBar; + VclPtr<ScrollBar> mpVScrollBar; + VclPtr<ScrollBarBox> mpScrollBox; + + tools::Long mnTextWidth; + mutable Selection maSelection; + +protected: + virtual void Notify( SfxBroadcaster& rBC, const SfxHint& rHint ) override; + void ImpUpdateScrollBarVis( WinBits nWinStyle ); + void ImpInitScrollBars(); + void ImpSetScrollBarRanges(); + void ImpSetHScrollBarThumbPos(); + DECL_LINK( ScrollHdl, ScrollBar*, void ); + +public: + ImpVclMEdit( VclMultiLineEdit* pVclMultiLineEdit, WinBits nWinStyle ); + virtual ~ImpVclMEdit() override; + + void SetModified( bool bMod ); + + void SetReadOnly( bool bRdOnly ); + bool IsReadOnly() const; + + void SetMaxTextLen(sal_Int32 nLen); + sal_Int32 GetMaxTextLen() const; + + void SetMaxTextWidth(tools::Long nMaxWidth); + + void InsertText( const OUString& rStr ); + OUString GetSelected() const; + OUString GetSelected( LineEnd aSeparator ) const; + + void SetSelection( const Selection& rSelection ); + const Selection& GetSelection() const; + + void Cut(); + void Copy(); + void Paste(); + + void SetText( const OUString& rStr ); + OUString GetText() const; + OUString GetText( LineEnd aSeparator ) const; + OUString GetTextLines( LineEnd aSeparator ) const; + + void Resize(); + void GetFocus(); + + bool HandleCommand( const CommandEvent& rCEvt ); + + void Enable( bool bEnable ); + + Size CalcMinimumSize() const; + Size CalcBlockSize( sal_uInt16 nColumns, sal_uInt16 nLines ) const; + void GetMaxVisColumnsAndLines( sal_uInt16& rnCols, sal_uInt16& rnLines ) const; + + void SetAlign( WinBits nWinStyle ); + + void InitFromStyle( WinBits nWinStyle ); + + TextWindow* GetTextWindow() { return mpTextWindow; } + ScrollBar& GetHScrollBar() { return *mpHScrollBar; } + ScrollBar& GetVScrollBar() { return *mpVScrollBar; } +}; + +ImpVclMEdit::ImpVclMEdit( VclMultiLineEdit* pEdt, WinBits nWinStyle ) + : pVclMultiLineEdit(pEdt) + , mpTextWindow(VclPtr<TextWindow>::Create(pEdt)) + , mpHScrollBar(VclPtr<ScrollBar>::Create(pVclMultiLineEdit, WB_HSCROLL|WB_DRAG)) + , mpVScrollBar(VclPtr<ScrollBar>::Create(pVclMultiLineEdit, WB_VSCROLL|WB_DRAG)) + , mpScrollBox(VclPtr<ScrollBarBox>::Create(pVclMultiLineEdit, WB_SIZEABLE)) + , mnTextWidth(0) +{ + mpVScrollBar->SetScrollHdl( LINK( this, ImpVclMEdit, ScrollHdl ) ); + mpHScrollBar->SetScrollHdl( LINK( this, ImpVclMEdit, ScrollHdl ) ); + mpTextWindow->Show(); + InitFromStyle( nWinStyle ); + StartListening( *mpTextWindow->GetTextEngine() ); +} + +void ImpVclMEdit::ImpUpdateScrollBarVis( WinBits nWinStyle ) +{ + const bool bHaveVScroll = mpVScrollBar->IsVisible(); + const bool bHaveHScroll = mpHScrollBar->IsVisible(); + const bool bHaveScrollBox = mpScrollBox->IsVisible(); + + bool bNeedVScroll = ( nWinStyle & WB_VSCROLL ) == WB_VSCROLL; + const bool bNeedHScroll = ( nWinStyle & WB_HSCROLL ) == WB_HSCROLL; + + const bool bAutoVScroll = ( nWinStyle & WB_AUTOVSCROLL ) == WB_AUTOVSCROLL; + if ( !bNeedVScroll && bAutoVScroll ) + { + TextEngine& rEngine( *mpTextWindow->GetTextEngine() ); + tools::Long nOverallTextHeight(0); + for ( sal_uInt32 i=0; i<rEngine.GetParagraphCount(); ++i ) + nOverallTextHeight += rEngine.GetTextHeight( i ); + if ( nOverallTextHeight > mpTextWindow->GetOutputSizePixel().Height() ) + bNeedVScroll = true; + } + + const bool bNeedScrollBox = bNeedVScroll && bNeedHScroll; + + bool bScrollbarsChanged = false; + if ( bHaveVScroll != bNeedVScroll ) + { + mpVScrollBar->Show(bNeedVScroll); + bScrollbarsChanged = true; + } + + if ( bHaveHScroll != bNeedHScroll ) + { + mpHScrollBar->Show(bNeedHScroll); + bScrollbarsChanged = true; + } + + if ( bHaveScrollBox != bNeedScrollBox ) + { + mpScrollBox->Show(bNeedScrollBox); + } + + if ( bScrollbarsChanged ) + { + ImpInitScrollBars(); + Resize(); + } +} + +void ImpVclMEdit::InitFromStyle( WinBits nWinStyle ) +{ + ImpUpdateScrollBarVis( nWinStyle ); + SetAlign( nWinStyle ); + + if ( nWinStyle & WB_NOHIDESELECTION ) + mpTextWindow->SetAutoFocusHide( false ); + else + mpTextWindow->SetAutoFocusHide( true ); + + if ( nWinStyle & WB_READONLY ) + mpTextWindow->GetTextView()->SetReadOnly( true ); + else + mpTextWindow->GetTextView()->SetReadOnly( false ); + + if ( nWinStyle & WB_IGNORETAB ) + { + mpTextWindow->SetIgnoreTab( true ); + } + else + { + mpTextWindow->SetIgnoreTab( false ); + // #103667# VclMultiLineEdit has the flag, but focusable window also needs this flag + WinBits nStyle = mpTextWindow->GetStyle(); + nStyle |= WB_NODIALOGCONTROL; + mpTextWindow->SetStyle( nStyle ); + } +} + +ImpVclMEdit::~ImpVclMEdit() +{ + EndListening( *mpTextWindow->GetTextEngine() ); + mpTextWindow.disposeAndClear(); + mpHScrollBar.disposeAndClear(); + mpVScrollBar.disposeAndClear(); + mpScrollBox.disposeAndClear(); + pVclMultiLineEdit.disposeAndClear(); +} + +void ImpVclMEdit::ImpSetScrollBarRanges() +{ + const tools::Long nTextHeight = mpTextWindow->GetTextEngine()->GetTextHeight(); + mpVScrollBar->SetRange( Range( 0, nTextHeight-1 ) ); + + mpHScrollBar->SetRange( Range( 0, mnTextWidth-1 ) ); +} + +void ImpVclMEdit::ImpInitScrollBars() +{ + static const sal_Unicode sampleChar = { 'x' }; + + ImpSetScrollBarRanges(); + + Size aCharBox; + aCharBox.setWidth( mpTextWindow->GetTextWidth( OUString(sampleChar) ) ); + aCharBox.setHeight( mpTextWindow->GetTextHeight() ); + Size aOutSz = mpTextWindow->GetOutputSizePixel(); + + mpHScrollBar->SetVisibleSize( aOutSz.Width() ); + mpHScrollBar->SetPageSize( aOutSz.Width() * 8 / 10 ); + mpHScrollBar->SetLineSize( aCharBox.Width()*10 ); + ImpSetHScrollBarThumbPos(); + + mpVScrollBar->SetVisibleSize( aOutSz.Height() ); + mpVScrollBar->SetPageSize( aOutSz.Height() * 8 / 10 ); + mpVScrollBar->SetLineSize( aCharBox.Height() ); + mpVScrollBar->SetThumbPos( mpTextWindow->GetTextView()->GetStartDocPos().Y() ); +} + +void ImpVclMEdit::ImpSetHScrollBarThumbPos() +{ + tools::Long nX = mpTextWindow->GetTextView()->GetStartDocPos().X(); + if ( !mpTextWindow->GetTextEngine()->IsRightToLeft() ) + mpHScrollBar->SetThumbPos( nX ); + else + mpHScrollBar->SetThumbPos( mnTextWidth - mpHScrollBar->GetVisibleSize() - nX ); + +} + +IMPL_LINK( ImpVclMEdit, ScrollHdl, ScrollBar*, pCurScrollBar, void ) +{ + tools::Long nDiffX = 0, nDiffY = 0; + + if ( pCurScrollBar == mpVScrollBar ) + nDiffY = mpTextWindow->GetTextView()->GetStartDocPos().Y() - pCurScrollBar->GetThumbPos(); + else if ( pCurScrollBar == mpHScrollBar ) + nDiffX = mpTextWindow->GetTextView()->GetStartDocPos().X() - pCurScrollBar->GetThumbPos(); + + mpTextWindow->GetTextView()->Scroll( nDiffX, nDiffY ); + // mpTextWindow->GetTextView()->ShowCursor( false, true ); +} + +void ImpVclMEdit::SetAlign( WinBits nWinStyle ) +{ + bool bRTL = AllSettings::GetLayoutRTL(); + mpTextWindow->GetTextEngine()->SetRightToLeft( bRTL ); + + if ( nWinStyle & WB_CENTER ) + mpTextWindow->GetTextEngine()->SetTextAlign( TxtAlign::Center ); + else if ( nWinStyle & WB_RIGHT ) + mpTextWindow->GetTextEngine()->SetTextAlign( !bRTL ? TxtAlign::Right : TxtAlign::Left ); + else if ( nWinStyle & WB_LEFT ) + mpTextWindow->GetTextEngine()->SetTextAlign( !bRTL ? TxtAlign::Left : TxtAlign::Right ); +} + +void ImpVclMEdit::SetModified( bool bMod ) +{ + mpTextWindow->GetTextEngine()->SetModified( bMod ); +} + +void ImpVclMEdit::SetReadOnly( bool bRdOnly ) +{ + mpTextWindow->GetTextView()->SetReadOnly( bRdOnly ); + // TODO: Adjust color? +} + +bool ImpVclMEdit::IsReadOnly() const +{ + return mpTextWindow->GetTextView()->IsReadOnly(); +} + +void ImpVclMEdit::SetMaxTextLen(sal_Int32 nLen) +{ + mpTextWindow->GetTextEngine()->SetMaxTextLen(nLen); +} + +sal_Int32 ImpVclMEdit::GetMaxTextLen() const +{ + return mpTextWindow->GetTextEngine()->GetMaxTextLen(); +} + +void ImpVclMEdit::InsertText( const OUString& rStr ) +{ + mpTextWindow->GetTextView()->InsertText( rStr ); +} + +OUString ImpVclMEdit::GetSelected() const +{ + return mpTextWindow->GetTextView()->GetSelected(); +} + +OUString ImpVclMEdit::GetSelected( LineEnd aSeparator ) const +{ + return mpTextWindow->GetTextView()->GetSelected( aSeparator ); +} + +void ImpVclMEdit::SetMaxTextWidth(tools::Long nMaxWidth) +{ + mpTextWindow->GetTextEngine()->SetMaxTextWidth(nMaxWidth); +} + +void ImpVclMEdit::Resize() +{ + int nIteration = 1; + do + { + WinBits nWinStyle( pVclMultiLineEdit->GetStyle() ); + if ( ( nWinStyle & WB_AUTOVSCROLL ) == WB_AUTOVSCROLL ) + ImpUpdateScrollBarVis( nWinStyle ); + + Size aSz = pVclMultiLineEdit->GetOutputSizePixel(); + Size aEditSize = aSz; + tools::Long nSBWidth = pVclMultiLineEdit->GetSettings().GetStyleSettings().GetScrollBarSize(); + nSBWidth = pVclMultiLineEdit->CalcZoom( nSBWidth ); + + if (mpHScrollBar->IsVisible()) + aSz.AdjustHeight( -(nSBWidth+1) ); + if (mpVScrollBar->IsVisible()) + aSz.AdjustWidth( -(nSBWidth+1) ); + + if (!mpHScrollBar->IsVisible()) + mpTextWindow->GetTextEngine()->SetMaxTextWidth( aSz.Width() ); + else + mpHScrollBar->setPosSizePixel( 0, aEditSize.Height()-nSBWidth, aSz.Width(), nSBWidth ); + + Point aTextWindowPos; + if (mpVScrollBar->IsVisible()) + { + if( AllSettings::GetLayoutRTL() ) + { + mpVScrollBar->setPosSizePixel( 0, 0, nSBWidth, aSz.Height() ); + aTextWindowPos.AdjustX(nSBWidth ); + } + else + mpVScrollBar->setPosSizePixel( aEditSize.Width()-nSBWidth, 0, nSBWidth, aSz.Height() ); + } + + if (mpScrollBox->IsVisible()) + mpScrollBox->setPosSizePixel( aSz.Width(), aSz.Height(), nSBWidth, nSBWidth ); + + Size aTextWindowSize( aSz ); + if ( aTextWindowSize.Width() < 0 ) + aTextWindowSize.setWidth( 0 ); + if ( aTextWindowSize.Height() < 0 ) + aTextWindowSize.setHeight( 0 ); + + Size aOldTextWindowSize( mpTextWindow->GetSizePixel() ); + mpTextWindow->SetPosSizePixel( aTextWindowPos, aTextWindowSize ); + if ( aOldTextWindowSize == aTextWindowSize ) + break; + + // Changing the text window size might effectively have changed the need for + // scrollbars, so do another iteration. + ++nIteration; + OSL_ENSURE( nIteration < 3, "ImpVclMEdit::Resize: isn't this expected to terminate with the second iteration?" ); + + } while ( nIteration <= 3 ); // artificial break after four iterations + + ImpInitScrollBars(); +} + +void ImpVclMEdit::GetFocus() +{ + mpTextWindow->GrabFocus(); +} + +void ImpVclMEdit::Cut() +{ + if ( !mpTextWindow->GetTextView()->IsReadOnly() ) + mpTextWindow->GetTextView()->Cut(); +} + +void ImpVclMEdit::Copy() +{ + mpTextWindow->GetTextView()->Copy(); +} + +void ImpVclMEdit::Paste() +{ + if ( !mpTextWindow->GetTextView()->IsReadOnly() ) + mpTextWindow->GetTextView()->Paste(); +} + +void ImpVclMEdit::SetText( const OUString& rStr ) +{ + bool bWasModified = mpTextWindow->GetTextEngine()->IsModified(); + mpTextWindow->GetTextEngine()->SetText( rStr ); + if ( !bWasModified ) + mpTextWindow->GetTextEngine()->SetModified( false ); + + mpTextWindow->GetTextView()->SetSelection( TextSelection() ); + + WinBits nWinStyle( pVclMultiLineEdit->GetStyle() ); + if ( ( nWinStyle & WB_AUTOVSCROLL ) == WB_AUTOVSCROLL ) + ImpUpdateScrollBarVis( nWinStyle ); +} + +OUString ImpVclMEdit::GetText() const +{ + return mpTextWindow->GetTextEngine()->GetText(); +} + +OUString ImpVclMEdit::GetText( LineEnd aSeparator ) const +{ + return mpTextWindow->GetTextEngine()->GetText( aSeparator ); +} + +OUString ImpVclMEdit::GetTextLines( LineEnd aSeparator ) const +{ + return mpTextWindow->GetTextEngine()->GetTextLines( aSeparator ); +} + +void ImpVclMEdit::Notify( SfxBroadcaster&, const SfxHint& rHint ) +{ + const TextHint* pTextHint = dynamic_cast<const TextHint*>(&rHint); + if ( !pTextHint ) + return; + + switch (pTextHint->GetId()) + { + case SfxHintId::TextViewScrolled: + if (mpHScrollBar->IsVisible()) + ImpSetHScrollBarThumbPos(); + if (mpVScrollBar->IsVisible()) + mpVScrollBar->SetThumbPos( mpTextWindow->GetTextView()->GetStartDocPos().Y() ); + break; + + case SfxHintId::TextHeightChanged: + if ( mpTextWindow->GetTextView()->GetStartDocPos().Y() ) + { + tools::Long nOutHeight = mpTextWindow->GetOutputSizePixel().Height(); + tools::Long nTextHeight = mpTextWindow->GetTextEngine()->GetTextHeight(); + if ( nTextHeight < nOutHeight ) + mpTextWindow->GetTextView()->Scroll( 0, mpTextWindow->GetTextView()->GetStartDocPos().Y() ); + } + ImpSetScrollBarRanges(); + break; + + case SfxHintId::TextFormatted: + if (mpHScrollBar->IsVisible()) + { + const tools::Long nWidth = mpTextWindow->GetTextEngine()->CalcTextWidth(); + if ( nWidth != mnTextWidth ) + { + mnTextWidth = nWidth; + mpHScrollBar->SetRange( Range( 0, mnTextWidth-1 ) ); + ImpSetHScrollBarThumbPos(); + } + } + break; + + case SfxHintId::TextModified: + ImpUpdateScrollBarVis(pVclMultiLineEdit->GetStyle()); + pVclMultiLineEdit->Modify(); + break; + + case SfxHintId::TextViewSelectionChanged: + pVclMultiLineEdit->SelectionChanged(); + break; + + case SfxHintId::TextViewCaretChanged: + pVclMultiLineEdit->CaretChanged(); + break; + + default: break; + } +} + +void ImpVclMEdit::SetSelection( const Selection& rSelection ) +{ + OUString aText = mpTextWindow->GetTextEngine()->GetText(); + + Selection aNewSelection( rSelection ); + if ( aNewSelection.Min() < 0 ) + aNewSelection.Min() = 0; + else if ( aNewSelection.Min() > aText.getLength() ) + aNewSelection.Min() = aText.getLength(); + if ( aNewSelection.Max() < 0 ) + aNewSelection.Max() = 0; + else if ( aNewSelection.Max() > aText.getLength() ) + aNewSelection.Max() = aText.getLength(); + + tools::Long nEnd = std::max( aNewSelection.Min(), aNewSelection.Max() ); + TextSelection aTextSel; + sal_uInt32 nPara = 0; + sal_Int32 nChar = 0; + tools::Long x = 0; + while ( x <= nEnd ) + { + if ( x == aNewSelection.Min() ) + aTextSel.GetStart() = TextPaM( nPara, nChar ); + if ( x == aNewSelection.Max() ) + aTextSel.GetEnd() = TextPaM( nPara, nChar ); + + if ( ( x < aText.getLength() ) && ( aText[ x ] == '\n' ) ) + { + nPara++; + nChar = 0; + } + else + nChar++; + x++; + } + mpTextWindow->GetTextView()->SetSelection( aTextSel ); +} + +const Selection& ImpVclMEdit::GetSelection() const +{ + maSelection = Selection(); + TextSelection aTextSel( mpTextWindow->GetTextView()->GetSelection() ); + aTextSel.Justify(); + // flatten selection => every line-break a character + + ExtTextEngine* pExtTextEngine = mpTextWindow->GetTextEngine(); + // paragraphs before + for ( sal_uInt32 n = 0; n < aTextSel.GetStart().GetPara(); ++n ) + { + maSelection.Min() += pExtTextEngine->GetTextLen( n ); + maSelection.Min()++; + } + + // first paragraph with selection + maSelection.Max() = maSelection.Min(); + maSelection.Min() += aTextSel.GetStart().GetIndex(); + + for ( sal_uInt32 n = aTextSel.GetStart().GetPara(); n < aTextSel.GetEnd().GetPara(); ++n ) + { + maSelection.Max() += pExtTextEngine->GetTextLen( n ); + maSelection.Max()++; + } + + maSelection.Max() += aTextSel.GetEnd().GetIndex(); + + return maSelection; +} + +Size ImpVclMEdit::CalcMinimumSize() const +{ + Size aSz( mpTextWindow->GetTextEngine()->CalcTextWidth(), + mpTextWindow->GetTextEngine()->GetTextHeight() ); + + if (mpHScrollBar->IsVisible()) + aSz.AdjustHeight(mpHScrollBar->GetSizePixel().Height() ); + if (mpVScrollBar->IsVisible()) + aSz.AdjustWidth(mpVScrollBar->GetSizePixel().Width() ); + + return aSz; +} + +Size ImpVclMEdit::CalcBlockSize( sal_uInt16 nColumns, sal_uInt16 nLines ) const +{ + static const sal_Unicode sampleChar = 'X'; + + Size aSz; + Size aCharSz; + aCharSz.setWidth( mpTextWindow->GetTextWidth( OUString(sampleChar) ) ); + aCharSz.setHeight( mpTextWindow->GetTextHeight() ); + + if ( nLines ) + aSz.setHeight( nLines*aCharSz.Height() ); + else + aSz.setHeight( mpTextWindow->GetTextEngine()->GetTextHeight() ); + + if ( nColumns ) + aSz.setWidth( nColumns*aCharSz.Width() ); + else + aSz.setWidth( mpTextWindow->GetTextEngine()->CalcTextWidth() ); + + if (mpHScrollBar->IsVisible()) + aSz.AdjustHeight(mpHScrollBar->GetSizePixel().Height() ); + if (mpVScrollBar->IsVisible()) + aSz.AdjustWidth(mpVScrollBar->GetSizePixel().Width() ); + + return aSz; +} + +void ImpVclMEdit::GetMaxVisColumnsAndLines( sal_uInt16& rnCols, sal_uInt16& rnLines ) const +{ + static const sal_Unicode sampleChar = { 'x' }; + Size aOutSz = mpTextWindow->GetOutputSizePixel(); + Size aCharSz( mpTextWindow->GetTextWidth( OUString(sampleChar) ), mpTextWindow->GetTextHeight() ); + rnCols = static_cast<sal_uInt16>(aOutSz.Width()/aCharSz.Width()); + rnLines = static_cast<sal_uInt16>(aOutSz.Height()/aCharSz.Height()); +} + +void ImpVclMEdit::Enable( bool bEnable ) +{ + mpTextWindow->Enable( bEnable ); + if (mpHScrollBar->IsVisible()) + mpHScrollBar->Enable( bEnable ); + if (mpVScrollBar->IsVisible()) + mpVScrollBar->Enable( bEnable ); +} + +bool ImpVclMEdit::HandleCommand( const CommandEvent& rCEvt ) +{ + bool bDone = false; + CommandEventId nCommand = rCEvt.GetCommand(); + if (nCommand == CommandEventId::Wheel || + nCommand == CommandEventId::StartAutoScroll || + nCommand == CommandEventId::AutoScroll || + nCommand == CommandEventId::GesturePan) + { + ScrollBar* pHScrollBar = mpHScrollBar->IsVisible() ? mpHScrollBar.get() : nullptr; + ScrollBar* pVScrollBar = mpVScrollBar->IsVisible() ? mpVScrollBar.get() : nullptr; + mpTextWindow->HandleScrollCommand(rCEvt, pHScrollBar, pVScrollBar); + bDone = true; + } + return bDone; +} + +TextWindow::TextWindow(Edit* pParent) + : Window(pParent) + , mxParent(pParent) +{ + mbInMBDown = false; + mbFocusSelectionHide = false; + mbIgnoreTab = false; + mbActivePopup = false; + mbSelectOnTab = true; + + SetPointer( PointerStyle::Text ); + + mpExtTextEngine.reset(new ExtTextEngine); + mpExtTextEngine->SetMaxTextLen(EDIT_NOLIMIT); + if( pParent->GetStyle() & WB_BORDER ) + mpExtTextEngine->SetLeftMargin( 2 ); + mpExtTextEngine->SetLocale( GetSettings().GetLanguageTag().getLocale() ); + mpExtTextView.reset(new TextView( mpExtTextEngine.get(), this )); + mpExtTextEngine->InsertView( mpExtTextView.get() ); + mpExtTextEngine->EnableUndo( true ); + mpExtTextView->ShowCursor(); + + Color aBackgroundColor = GetSettings().GetStyleSettings().GetWorkspaceColor(); + SetBackground( aBackgroundColor ); + pParent->SetBackground( aBackgroundColor ); +} + +TextWindow::~TextWindow() +{ + disposeOnce(); +} + +void TextWindow::dispose() +{ + mxParent.clear(); + mpExtTextView.reset(); + mpExtTextEngine.reset(); + Window::dispose(); +} + +void TextWindow::MouseMove( const MouseEvent& rMEvt ) +{ + mpExtTextView->MouseMove( rMEvt ); + Window::MouseMove( rMEvt ); +} + +void TextWindow::MouseButtonDown( const MouseEvent& rMEvt ) +{ + mbInMBDown = true; // so that GetFocus does not select everything + mpExtTextView->MouseButtonDown( rMEvt ); + GrabFocus(); + mbInMBDown = false; +} + +void TextWindow::MouseButtonUp( const MouseEvent& rMEvt ) +{ + mpExtTextView->MouseButtonUp( rMEvt ); +} + +void TextWindow::KeyInput( const KeyEvent& rKEvent ) +{ + bool bDone = false; + sal_uInt16 nCode = rKEvent.GetKeyCode().GetCode(); + if ( nCode == css::awt::Key::SELECT_ALL || + ( (nCode == KEY_A) && rKEvent.GetKeyCode().IsMod1() && !rKEvent.GetKeyCode().IsMod2() ) + ) + { + mpExtTextView->SetSelection( TextSelection( TextPaM( 0, 0 ), TextPaM( TEXT_PARA_ALL, TEXT_INDEX_ALL ) ) ); + bDone = true; + } + else if ( (nCode == KEY_S) && rKEvent.GetKeyCode().IsShift() && rKEvent.GetKeyCode().IsMod1() ) + { + if ( vcl::GetGetSpecialCharsFunction() ) + { + // to maintain the selection + mbActivePopup = true; + OUString aChars = vcl::GetGetSpecialCharsFunction()(GetFrameWeld(), GetFont()); + if (!aChars.isEmpty()) + { + mpExtTextView->InsertText( aChars ); + mpExtTextView->GetTextEngine()->SetModified( true ); + } + mbActivePopup = false; + bDone = true; + } + } + else if ( nCode == KEY_TAB ) + { + if (!mbIgnoreTab) + { + if (!rKEvent.GetKeyCode().IsMod1()) + bDone = mpExtTextView->KeyInput(rKEvent); + else + { + // tdf#107625 make ctrl+tab act like tab when MultiLine Edit normally accepts tab as an input char + vcl::KeyCode aKeyCode(rKEvent.GetKeyCode().GetCode(), rKEvent.GetKeyCode().GetModifier() & ~KEY_MOD1); + KeyEvent aKEventWithoutMod1(rKEvent.GetCharCode(), aKeyCode, rKEvent.GetRepeat()); + Window::KeyInput(aKEventWithoutMod1); + bDone = true; + } + } + } + else + { + bDone = mpExtTextView->KeyInput( rKEvent ); + } + + if ( !bDone ) + Window::KeyInput( rKEvent ); +} + +void TextWindow::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + mpExtTextView->Paint(rRenderContext, rRect); +} + +void TextWindow::Resize() +{ +} + +void TextWindow::Command( const CommandEvent& rCEvt ) +{ + if ( rCEvt.GetCommand() == CommandEventId::ContextMenu ) + { + VclPtr<PopupMenu> pPopup = mxParent->CreatePopupMenu(); + bool bEnableCut = true; + bool bEnableCopy = true; + bool bEnableDelete = true; + bool bEnablePaste = true; + bool bEnableSpecialChar = true; + bool bEnableUndo = true; + + if ( !mpExtTextView->HasSelection() ) + { + bEnableCut = false; + bEnableCopy = false; + bEnableDelete = false; + } + if ( mpExtTextView->IsReadOnly() ) + { + bEnableCut = false; + bEnablePaste = false; + bEnableDelete = false; + bEnableSpecialChar = false; + } + if ( !mpExtTextView->GetTextEngine()->HasUndoManager() || !mpExtTextView->GetTextEngine()->GetUndoManager().GetUndoActionCount() ) + { + bEnableUndo = false; + } + pPopup->EnableItem(pPopup->GetItemId(u"cut"), bEnableCut); + pPopup->EnableItem(pPopup->GetItemId(u"copy"), bEnableCopy); + pPopup->EnableItem(pPopup->GetItemId(u"delete"), bEnableDelete); + pPopup->EnableItem(pPopup->GetItemId(u"paste"), bEnablePaste); + pPopup->SetItemText(pPopup->GetItemId(u"specialchar"), + BuilderUtils::convertMnemonicMarkup(VclResId(STR_SPECIAL_CHARACTER_MENU_ENTRY))); + pPopup->EnableItem(pPopup->GetItemId(u"specialchar"), bEnableSpecialChar); + pPopup->EnableItem(pPopup->GetItemId(u"undo"), bEnableUndo); + pPopup->ShowItem(pPopup->GetItemId(u"specialchar"), !vcl::GetGetSpecialCharsFunction()); + + mbActivePopup = true; + Point aPos = rCEvt.GetMousePosPixel(); + if ( !rCEvt.IsMouseEvent() ) + { + // Sometime do show Menu centered in the selection !!! + Size aSize = GetOutputSizePixel(); + aPos = Point( aSize.Width()/2, aSize.Height()/2 ); + } + sal_uInt16 n = pPopup->Execute( this, aPos ); + OUString sCommand = pPopup->GetItemIdent(n); + if (sCommand == "undo") + { + mpExtTextView->Undo(); + mpExtTextEngine->SetModified( true ); + mpExtTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + } + else if (sCommand == "cut") + { + mpExtTextView->Cut(); + mpExtTextEngine->SetModified( true ); + mpExtTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + } + else if (sCommand == "copy") + { + mpExtTextView->Copy(); + } + else if (sCommand == "paste") + { + mpExtTextView->Paste(); + mpExtTextEngine->SetModified( true ); + mpExtTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + } + else if (sCommand == "delete") + { + mpExtTextView->DeleteSelected(); + mpExtTextEngine->SetModified( true ); + mpExtTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + } + else if (sCommand == "selectall") + { + mpExtTextView->SetSelection( TextSelection( TextPaM( 0, 0 ), TextPaM( TEXT_PARA_ALL, TEXT_INDEX_ALL ) ) ); + } + else if (sCommand == "specialchar") + { + OUString aChars = vcl::GetGetSpecialCharsFunction()(GetFrameWeld(), GetFont()); + if (!aChars.isEmpty()) + { + mpExtTextView->InsertText( aChars ); + mpExtTextEngine->SetModified( true ); + mpExtTextEngine->Broadcast( TextHint( SfxHintId::TextModified ) ); + } + } + pPopup.clear(); + mbActivePopup = false; + } + else + { + mpExtTextView->Command( rCEvt ); + } + Window::Command( rCEvt ); +} + +void TextWindow::GetFocus() +{ + Window::GetFocus(); + if ( mbActivePopup ) + return; + + bool bGotoCursor = !mpExtTextView->IsReadOnly(); + if ( mbFocusSelectionHide && IsReallyVisible() && mbSelectOnTab && !mbInMBDown ) + { + // select everything, but do not scroll + bool bAutoScroll = mpExtTextView->IsAutoScroll(); + mpExtTextView->SetAutoScroll( false ); + mpExtTextView->SetSelection( TextSelection( TextPaM( 0, 0 ), TextPaM( TEXT_PARA_ALL, TEXT_INDEX_ALL ) ) ); + mpExtTextView->SetAutoScroll( bAutoScroll ); + bGotoCursor = false; + } + mpExtTextView->SetPaintSelection( true ); + mpExtTextView->ShowCursor( bGotoCursor ); +} + +void TextWindow::LoseFocus() +{ + Window::LoseFocus(); + + if ( mbFocusSelectionHide && !mbActivePopup && mpExtTextView ) + mpExtTextView->SetPaintSelection( false ); +} + +VclMultiLineEdit::VclMultiLineEdit( vcl::Window* pParent, WinBits nWinStyle ) + : Edit( pParent, nWinStyle ) +{ + SetType( WindowType::MULTILINEEDIT ); + pImpVclMEdit.reset(new ImpVclMEdit( this, nWinStyle )); + ImplInitSettings( true ); + + SetCompoundControl( true ); + SetStyle( ImplInitStyle( nWinStyle ) ); +} + +VclMultiLineEdit::~VclMultiLineEdit() +{ + disposeOnce(); +} + +void VclMultiLineEdit::dispose() +{ + pImpVclMEdit.reset(); + Edit::dispose(); +} + +WinBits VclMultiLineEdit::ImplInitStyle( WinBits nStyle ) +{ + if ( !(nStyle & WB_NOTABSTOP) ) + nStyle |= WB_TABSTOP; + + if ( !(nStyle & WB_NOGROUP) ) + nStyle |= WB_GROUP; + + if ( !(nStyle & WB_IGNORETAB )) + nStyle |= WB_NODIALOGCONTROL; + + return nStyle; +} + +void VclMultiLineEdit::ApplySettings(vcl::RenderContext& rRenderContext) +{ + const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings(); + + // The Font has to be adjusted, as the TextEngine does not take care of + // TextColor/Background + + Color aTextColor = rStyleSettings.GetFieldTextColor(); + if (IsControlForeground()) + aTextColor = GetControlForeground(); + + if (!IsEnabled()) + aTextColor = rStyleSettings.GetDisableColor(); + + vcl::Font aFont = rStyleSettings.GetFieldFont(); + aFont.SetTransparent(IsPaintTransparent()); + ApplyControlFont(rRenderContext, aFont); + + vcl::Font theFont = rRenderContext.GetFont(); + theFont.SetColor(aTextColor); + if (IsPaintTransparent()) + theFont.SetFillColor(COL_TRANSPARENT); + else + theFont.SetFillColor(IsControlBackground() ? GetControlBackground() : rStyleSettings.GetFieldColor()); + + pImpVclMEdit->GetTextWindow()->SetFont(theFont); + // FIXME: next call causes infinite invalidation loop, rethink how to properly fix this situation + // pImpVclMEdit->GetTextWindow()->GetTextEngine()->SetFont(theFont); + pImpVclMEdit->GetTextWindow()->SetTextColor(aTextColor); + + if (IsPaintTransparent()) + { + pImpVclMEdit->GetTextWindow()->SetPaintTransparent(true); + pImpVclMEdit->GetTextWindow()->SetBackground(); + pImpVclMEdit->GetTextWindow()->SetControlBackground(); + rRenderContext.SetBackground(); + SetControlBackground(); + } + else + { + if (IsControlBackground()) + pImpVclMEdit->GetTextWindow()->SetBackground(GetControlBackground()); + else + pImpVclMEdit->GetTextWindow()->SetBackground(rStyleSettings.GetFieldColor()); + // also adjust for VclMultiLineEdit as the TextComponent might hide Scrollbars + rRenderContext.SetBackground(pImpVclMEdit->GetTextWindow()->GetBackground()); + } +} + +void VclMultiLineEdit::ImplInitSettings(bool bBackground) +{ + const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings(); + + // The Font has to be adjusted, as the TextEngine does not take care of + // TextColor/Background + + Color aTextColor = rStyleSettings.GetFieldTextColor(); + if (IsControlForeground()) + aTextColor = GetControlForeground(); + if (!IsEnabled()) + aTextColor = rStyleSettings.GetDisableColor(); + + vcl::Font aFont = rStyleSettings.GetFieldFont(); + aFont.SetTransparent(IsPaintTransparent()); + ApplyControlFont(*GetOutDev(), aFont); + + vcl::Font TheFont = GetFont(); + TheFont.SetColor(aTextColor); + if (IsPaintTransparent()) + TheFont.SetFillColor(COL_TRANSPARENT); + else + TheFont.SetFillColor(IsControlBackground() ? GetControlBackground() : rStyleSettings.GetFieldColor()); + pImpVclMEdit->GetTextWindow()->SetFont(TheFont); + pImpVclMEdit->GetTextWindow()->GetTextEngine()->SetFont(TheFont); + pImpVclMEdit->GetTextWindow()->SetTextColor(aTextColor); + + if (!bBackground) + return; + + if (IsPaintTransparent()) + { + pImpVclMEdit->GetTextWindow()->SetPaintTransparent(true); + pImpVclMEdit->GetTextWindow()->SetBackground(); + pImpVclMEdit->GetTextWindow()->SetControlBackground(); + SetBackground(); + SetControlBackground(); + } + else + { + if (IsControlBackground()) + pImpVclMEdit->GetTextWindow()->SetBackground(GetControlBackground()); + else + pImpVclMEdit->GetTextWindow()->SetBackground(rStyleSettings.GetFieldColor()); + // also adjust for VclMultiLineEdit as the TextComponent might hide Scrollbars + SetBackground(pImpVclMEdit->GetTextWindow()->GetBackground()); + } +} + +void VclMultiLineEdit::Modify() +{ + aModifyHdlLink.Call( *this ); + + CallEventListeners( VclEventId::EditModify ); +} + +void VclMultiLineEdit::SelectionChanged() +{ + CallEventListeners(VclEventId::EditSelectionChanged); +} + +void VclMultiLineEdit::CaretChanged() +{ + CallEventListeners(VclEventId::EditCaretChanged); +} + +void VclMultiLineEdit::SetModifyFlag() +{ + pImpVclMEdit->SetModified( true ); +} + +void VclMultiLineEdit::SetReadOnly( bool bReadOnly ) +{ + pImpVclMEdit->SetReadOnly( bReadOnly ); + Edit::SetReadOnly( bReadOnly ); + + // #94921# ReadOnly can be overwritten in InitFromStyle() when WB not set. + WinBits nStyle = GetStyle(); + if ( bReadOnly ) + nStyle |= WB_READONLY; + else + nStyle &= ~WB_READONLY; + SetStyle( nStyle ); +} + +bool VclMultiLineEdit::IsReadOnly() const +{ + if (!pImpVclMEdit) // might be called from within the dtor, when pImpVclMEdit == NULL is a valid state + return true; + + return pImpVclMEdit->IsReadOnly(); +} + +void VclMultiLineEdit::SetMaxTextLen(sal_Int32 nMaxLen) +{ + pImpVclMEdit->SetMaxTextLen(nMaxLen); +} + +void VclMultiLineEdit::SetMaxTextWidth(tools::Long nMaxWidth) +{ + pImpVclMEdit->SetMaxTextWidth(nMaxWidth ); +} + +sal_Int32 VclMultiLineEdit::GetMaxTextLen() const +{ + return pImpVclMEdit->GetMaxTextLen(); +} + +void VclMultiLineEdit::ReplaceSelected( const OUString& rStr ) +{ + pImpVclMEdit->InsertText( rStr ); +} + +void VclMultiLineEdit::DeleteSelected() +{ + pImpVclMEdit->InsertText( OUString() ); +} + +OUString VclMultiLineEdit::GetSelected() const +{ + return pImpVclMEdit->GetSelected(); +} + +OUString VclMultiLineEdit::GetSelected( LineEnd aSeparator ) const +{ + return pImpVclMEdit->GetSelected( aSeparator ); +} + +void VclMultiLineEdit::Cut() +{ + pImpVclMEdit->Cut(); +} + +void VclMultiLineEdit::Copy() +{ + pImpVclMEdit->Copy(); +} + +void VclMultiLineEdit::Paste() +{ + pImpVclMEdit->Paste(); +} + +void VclMultiLineEdit::SetText( const OUString& rStr ) +{ + pImpVclMEdit->SetText( rStr ); +} + +OUString VclMultiLineEdit::GetText() const +{ + return pImpVclMEdit ? pImpVclMEdit->GetText() : OUString(); +} + +OUString VclMultiLineEdit::GetText( LineEnd aSeparator ) const +{ + return pImpVclMEdit ? pImpVclMEdit->GetText( aSeparator ) : OUString(); +} + +OUString VclMultiLineEdit::GetTextLines( LineEnd aSeparator ) const +{ + return pImpVclMEdit ? pImpVclMEdit->GetTextLines( aSeparator ) : OUString(); +} + +void VclMultiLineEdit::Resize() +{ + pImpVclMEdit->Resize(); +} + +void VclMultiLineEdit::GetFocus() +{ + if ( !pImpVclMEdit ) // might be called from within the dtor, when pImpVclMEdit == NULL is a valid state + return; + + pImpVclMEdit->GetFocus(); +} + +void VclMultiLineEdit::SetSelection( const Selection& rSelection ) +{ + pImpVclMEdit->SetSelection( rSelection ); +} + +const Selection& VclMultiLineEdit::GetSelection() const +{ + return pImpVclMEdit->GetSelection(); +} + +Size VclMultiLineEdit::CalcMinimumSize() const +{ + Size aSz = pImpVclMEdit->CalcMinimumSize(); + + sal_Int32 nLeft, nTop, nRight, nBottom; + static_cast<vcl::Window*>(const_cast<VclMultiLineEdit *>(this))->GetBorder( nLeft, nTop, nRight, nBottom ); + aSz.AdjustWidth(nLeft+nRight ); + aSz.AdjustHeight(nTop+nBottom ); + + return aSz; +} + +Size VclMultiLineEdit::CalcAdjustedSize( const Size& rPrefSize ) const +{ + Size aSz = rPrefSize; + sal_Int32 nLeft, nTop, nRight, nBottom; + static_cast<vcl::Window*>(const_cast<VclMultiLineEdit *>(this))->GetBorder( nLeft, nTop, nRight, nBottom ); + + // center vertically for whole lines + + tools::Long nHeight = aSz.Height() - nTop - nBottom; + tools::Long nLineHeight = pImpVclMEdit->CalcBlockSize( 1, 1 ).Height(); + tools::Long nLines = nHeight / nLineHeight; + if ( nLines < 1 ) + nLines = 1; + + aSz.setHeight( nLines * nLineHeight ); + aSz.AdjustHeight(nTop+nBottom ); + + return aSz; +} + +Size VclMultiLineEdit::CalcBlockSize( sal_uInt16 nColumns, sal_uInt16 nLines ) const +{ + Size aSz = pImpVclMEdit->CalcBlockSize( nColumns, nLines ); + + sal_Int32 nLeft, nTop, nRight, nBottom; + static_cast<vcl::Window*>(const_cast<VclMultiLineEdit *>(this))->GetBorder( nLeft, nTop, nRight, nBottom ); + aSz.AdjustWidth(nLeft+nRight ); + aSz.AdjustHeight(nTop+nBottom ); + return aSz; +} + +void VclMultiLineEdit::GetMaxVisColumnsAndLines( sal_uInt16& rnCols, sal_uInt16& rnLines ) const +{ + pImpVclMEdit->GetMaxVisColumnsAndLines( rnCols, rnLines ); +} + +void VclMultiLineEdit::StateChanged( StateChangedType nType ) +{ + if( nType == StateChangedType::Enable ) + { + pImpVclMEdit->Enable( IsEnabled() ); + ImplInitSettings( false ); + } + else if( nType == StateChangedType::ReadOnly ) + { + pImpVclMEdit->SetReadOnly( IsReadOnly() ); + } + else if ( nType == StateChangedType::Zoom ) + { + pImpVclMEdit->GetTextWindow()->SetZoom( GetZoom() ); + ImplInitSettings( false ); + Resize(); + } + else if ( nType == StateChangedType::ControlFont ) + { + ImplInitSettings( false ); + Resize(); + Invalidate(); + } + else if ( nType == StateChangedType::ControlForeground ) + { + ImplInitSettings( false ); + Invalidate(); + } + else if ( nType == StateChangedType::ControlBackground ) + { + ImplInitSettings( true ); + Invalidate(); + } + else if ( nType == StateChangedType::Style ) + { + pImpVclMEdit->InitFromStyle( GetStyle() ); + SetStyle( ImplInitStyle( GetStyle() ) ); + } + else if ( nType == StateChangedType::InitShow ) + { + if( IsPaintTransparent() ) + { + pImpVclMEdit->GetTextWindow()->SetPaintTransparent( true ); + pImpVclMEdit->GetTextWindow()->SetBackground(); + pImpVclMEdit->GetTextWindow()->SetControlBackground(); + SetBackground(); + SetControlBackground(); + } + } + + Control::StateChanged( nType ); +} + +void VclMultiLineEdit::DataChanged( const DataChangedEvent& rDCEvt ) +{ + if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) && + (rDCEvt.GetFlags() & AllSettingsFlags::STYLE) ) + { + ImplInitSettings( true ); + Resize(); + Invalidate(); + } + else + Control::DataChanged( rDCEvt ); +} + +void VclMultiLineEdit::Draw( OutputDevice* pDev, const Point& rPos, SystemTextColorFlags nFlags ) +{ + ImplInitSettings(true); + + Point aPos = pDev->LogicToPixel( rPos ); + Size aSize = GetSizePixel(); + + vcl::Font aFont = pImpVclMEdit->GetTextWindow()->GetDrawPixelFont(pDev); + aFont.SetTransparent( true ); + + pDev->Push(); + pDev->SetMapMode(); + pDev->SetFont( aFont ); + pDev->SetTextFillColor(); + + // Border/Background + pDev->SetLineColor(); + pDev->SetFillColor(); + bool bBorder = (GetStyle() & WB_BORDER); + bool bBackground = IsControlBackground(); + if ( bBorder || bBackground ) + { + tools::Rectangle aRect( aPos, aSize ); + if ( bBorder ) + { + DecorationView aDecoView( pDev ); + aRect = aDecoView.DrawFrame( aRect, DrawFrameStyle::DoubleIn ); + } + if ( bBackground ) + { + pDev->SetFillColor( GetControlBackground() ); + pDev->DrawRect( aRect ); + } + } + + pDev->SetSystemTextColor(nFlags, IsEnabled()); + + OUString aText = GetText(); + Size aTextSz( pDev->GetTextWidth( aText ), pDev->GetTextHeight() ); + sal_uLong nLines = static_cast<sal_uLong>(aSize.Height() / aTextSz.Height()); + if ( !nLines ) + nLines = 1; + aTextSz.setHeight( nLines*aTextSz.Height() ); + tools::Long nOnePixel = GetDrawPixel( pDev, 1 ); + tools::Long nOffX = 3*nOnePixel; + tools::Long nOffY = 2*nOnePixel; + + // Clipping? + if ( ( nOffY < 0 ) || ( (nOffY+aTextSz.Height()) > aSize.Height() ) || ( (nOffX+aTextSz.Width()) > aSize.Width() ) ) + { + tools::Rectangle aClip( aPos, aSize ); + if ( aTextSz.Height() > aSize.Height() ) + aClip.AdjustBottom(aTextSz.Height() - aSize.Height() + 1 ); // so that HP-printer does not 'optimize-away' + pDev->IntersectClipRegion( aClip ); + } + + ExtTextEngine aTE; + aTE.SetText( GetText() ); + aTE.SetMaxTextWidth( aSize.Width() ); + aTE.SetFont( aFont ); + aTE.SetTextAlign( pImpVclMEdit->GetTextWindow()->GetTextEngine()->GetTextAlign() ); + aTE.Draw( pDev, Point( aPos.X() + nOffX, aPos.Y() + nOffY ) ); + + pDev->Pop(); +} + +bool VclMultiLineEdit::EventNotify( NotifyEvent& rNEvt ) +{ + bool bDone = false; + if( rNEvt.GetType() == NotifyEventType::COMMAND ) + { + bDone = pImpVclMEdit->HandleCommand( *rNEvt.GetCommandEvent() ); + } + return bDone || Edit::EventNotify( rNEvt ); +} + +bool VclMultiLineEdit::PreNotify( NotifyEvent& rNEvt ) +{ + bool bDone = false; + + if( ( rNEvt.GetType() == NotifyEventType::KEYINPUT ) && ( !GetTextView()->IsCursorEnabled() ) ) + { + const KeyEvent& rKEvent = *rNEvt.GetKeyEvent(); + if ( !rKEvent.GetKeyCode().IsShift() && ( rKEvent.GetKeyCode().GetGroup() == KEYGROUP_CURSOR ) ) + { + bDone = true; + TextSelection aSel = pImpVclMEdit->GetTextWindow()->GetTextView()->GetSelection(); + if ( aSel.HasRange() ) + { + aSel.GetStart() = aSel.GetEnd(); + pImpVclMEdit->GetTextWindow()->GetTextView()->SetSelection( aSel ); + } + else + { + switch ( rKEvent.GetKeyCode().GetCode() ) + { + case KEY_UP: + { + if ( pImpVclMEdit->GetVScrollBar().IsVisible() ) + pImpVclMEdit->GetVScrollBar().DoScrollAction( ScrollType::LineUp ); + } + break; + case KEY_DOWN: + { + if ( pImpVclMEdit->GetVScrollBar().IsVisible() ) + pImpVclMEdit->GetVScrollBar().DoScrollAction( ScrollType::LineDown ); + } + break; + case KEY_PAGEUP : + { + if ( pImpVclMEdit->GetVScrollBar().IsVisible() ) + pImpVclMEdit->GetVScrollBar().DoScrollAction( ScrollType::PageUp ); + } + break; + case KEY_PAGEDOWN: + { + if ( pImpVclMEdit->GetVScrollBar().IsVisible() ) + pImpVclMEdit->GetVScrollBar().DoScrollAction( ScrollType::PageDown ); + } + break; + case KEY_LEFT: + { + if ( pImpVclMEdit->GetHScrollBar().IsVisible() ) + pImpVclMEdit->GetHScrollBar().DoScrollAction( ScrollType::LineUp ); + } + break; + case KEY_RIGHT: + { + if ( pImpVclMEdit->GetHScrollBar().IsVisible() ) + pImpVclMEdit->GetHScrollBar().DoScrollAction( ScrollType::LineDown ); + } + break; + case KEY_HOME: + { + if ( rKEvent.GetKeyCode().IsMod1() ) + pImpVclMEdit->GetTextWindow()->GetTextView()-> + SetSelection( TextSelection( TextPaM( 0, 0 ) ) ); + } + break; + case KEY_END: + { + if ( rKEvent.GetKeyCode().IsMod1() ) + pImpVclMEdit->GetTextWindow()->GetTextView()-> + SetSelection( TextSelection( TextPaM( TEXT_PARA_ALL, TEXT_INDEX_ALL ) ) ); + } + break; + default: + { + bDone = false; + } + } + } + } + } + + return bDone || Edit::PreNotify( rNEvt ); +} + +// Internals for derived classes, e.g. TextComponent + +ExtTextEngine* VclMultiLineEdit::GetTextEngine() const +{ + return pImpVclMEdit->GetTextWindow()->GetTextEngine(); +} + +TextView* VclMultiLineEdit::GetTextView() const +{ + return pImpVclMEdit->GetTextWindow()->GetTextView(); +} + +ScrollBar& VclMultiLineEdit::GetVScrollBar() const +{ + return pImpVclMEdit->GetVScrollBar(); +} + +void VclMultiLineEdit::EnableFocusSelectionHide( bool bHide ) +{ + pImpVclMEdit->GetTextWindow()->SetAutoFocusHide( bHide ); +} + +void VclMultiLineEdit::DisableSelectionOnFocus() +{ + pImpVclMEdit->GetTextWindow()->DisableSelectionOnFocus(); +} + +void VclMultiLineEdit::EnableCursor( bool bEnable ) +{ + GetTextView()->EnableCursor( bEnable ); +} + +bool VclMultiLineEdit::CanUp() const +{ + TextView* pTextView = GetTextView(); + const TextSelection& rTextSelection = pTextView->GetSelection(); + TextPaM aPaM(rTextSelection.GetEnd()); + return aPaM != pTextView->CursorUp(aPaM); +} + +bool VclMultiLineEdit::CanDown() const +{ + TextView* pTextView = GetTextView(); + const TextSelection& rTextSelection = pTextView->GetSelection(); + TextPaM aPaM(rTextSelection.GetEnd()); + return aPaM != pTextView->CursorDown(aPaM); +} + +TextWindow* VclMultiLineEdit::GetTextWindow() +{ + return pImpVclMEdit->GetTextWindow(); +} + +FactoryFunction VclMultiLineEdit::GetUITestFactory() const +{ + return MultiLineEditUIObject::create; +} + +bool VclMultiLineEdit::set_property(const OUString &rKey, const OUString &rValue) +{ + if (rKey == "cursor-visible") + EnableCursor(toBool(rValue)); + else if (rKey == "accepts-tab") + pImpVclMEdit->GetTextWindow()->SetIgnoreTab(!toBool(rValue)); + else + return Edit::set_property(rKey, rValue); + return true; +} + +void VclMultiLineEdit::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter) +{ + Edit::DumpAsPropertyTree(rJsonWriter); + + rJsonWriter.put("cursor", pImpVclMEdit->GetTextWindow()->GetTextView()->IsCursorEnabled()); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/edit/xtextedt.cxx b/vcl/source/edit/xtextedt.cxx new file mode 100644 index 0000000000..87097a5be2 --- /dev/null +++ b/vcl/source/edit/xtextedt.cxx @@ -0,0 +1,227 @@ +/* -*- 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 <i18nutil/searchopt.hxx> +#include <i18nlangtag/languagetag.hxx> +#include <vcl/textdata.hxx> +#include <vcl/xtextedt.hxx> +#include <vcl/svapp.hxx> +#include <vcl/settings.hxx> +#include <unotools/textsearch.hxx> +#include <com/sun/star/util/SearchFlags.hpp> + +using namespace ::com::sun::star; + +const std::wstring gaGroupChars = L"(){}[]"; + +ExtTextEngine::ExtTextEngine() +{ +} + +ExtTextEngine::~ExtTextEngine() +{ +} + +TextSelection ExtTextEngine::MatchGroup( const TextPaM& rCursor ) const +{ + TextSelection aSel( rCursor ); + const sal_Int32 nPos = rCursor.GetIndex(); + sal_uInt32 nPara = rCursor.GetPara(); + const sal_uInt32 nParas = GetParagraphCount(); + if ( ( nPara < nParas ) && ( nPos < GetTextLen( nPara ) ) ) + { + size_t nMatchIndex = gaGroupChars.find( GetText( rCursor.GetPara() )[ nPos ] ); + if ( nMatchIndex != std::wstring::npos ) + { + if ( ( nMatchIndex % 2 ) == 0 ) + { + // search forwards + sal_Unicode nSC = gaGroupChars[ nMatchIndex ]; + sal_Unicode nEC = gaGroupChars[ nMatchIndex+1 ]; + + sal_Int32 nCur = nPos+1; + sal_uInt16 nLevel = 1; + while ( nLevel && ( nPara < nParas ) ) + { + OUString aStr = GetText( nPara ); + while ( nCur < aStr.getLength() ) + { + if ( aStr[nCur] == nSC ) + nLevel++; + else if ( aStr[nCur] == nEC ) + { + nLevel--; + if ( !nLevel ) + break; // while nCur... + } + nCur++; + } + + if ( nLevel ) + { + nPara++; + nCur = 0; + } + } + if ( nLevel == 0 ) // found + { + aSel.GetStart() = rCursor; + aSel.GetEnd() = TextPaM( nPara, nCur+1 ); + } + } + else + { + // search backwards + sal_Unicode nEC = gaGroupChars[ nMatchIndex ]; + sal_Unicode nSC = gaGroupChars[ nMatchIndex-1 ]; + + sal_Int32 nCur = rCursor.GetIndex()-1; + sal_uInt16 nLevel = 1; + while ( nLevel ) + { + if ( GetTextLen( nPara ) ) + { + OUString aStr = GetText( nPara ); + while ( nCur ) + { + if ( aStr[nCur] == nSC ) + { + nLevel--; + if ( !nLevel ) + break; // while nCur... + } + else if ( aStr[nCur] == nEC ) + nLevel++; + + nCur--; + } + } + + if ( nLevel ) + { + if ( nPara ) + { + nPara--; + nCur = GetTextLen( nPara )-1; // no matter if negative, as if Len() + } + else + break; + } + } + + if ( nLevel == 0 ) // found + { + aSel.GetStart() = rCursor; + ++aSel.GetStart().GetIndex(); // behind the char + aSel.GetEnd() = TextPaM( nPara, nCur ); + } + } + } + } + return aSel; +} + +bool ExtTextEngine::Search( TextSelection& rSel, const i18nutil::SearchOptions2& rSearchOptions, bool bForward ) const +{ + TextSelection aSel( rSel ); + aSel.Justify(); + + bool bSearchInSelection = (0 != (rSearchOptions.searchFlag & util::SearchFlags::REG_NOT_BEGINOFLINE) ); + + TextPaM aStartPaM( aSel.GetEnd() ); + if ( aSel.HasRange() && ( ( bSearchInSelection && bForward ) || ( !bSearchInSelection && !bForward ) ) ) + { + aStartPaM = aSel.GetStart(); + } + + bool bFound = false; + sal_uInt32 nEndNode; + + if ( bSearchInSelection ) + nEndNode = bForward ? aSel.GetEnd().GetPara() : aSel.GetStart().GetPara(); + else + nEndNode = bForward ? (GetParagraphCount()-1) : 0; + + const sal_uInt32 nStartNode = aStartPaM.GetPara(); + + i18nutil::SearchOptions2 aOptions( rSearchOptions ); + aOptions.Locale = Application::GetSettings().GetLanguageTag().getLocale(); + utl::TextSearch aSearcher(aOptions); + + // iterate over the paragraphs + for ( sal_uInt32 nNode = nStartNode; + bForward ? ( nNode <= nEndNode) : ( nNode >= nEndNode ); + bForward ? nNode++ : nNode-- ) + { + OUString aText = GetText( nNode ); + sal_Int32 nStartPos = 0; + sal_Int32 nEndPos = aText.getLength(); + if ( nNode == nStartNode ) + { + if ( bForward ) + nStartPos = aStartPaM.GetIndex(); + else + nEndPos = aStartPaM.GetIndex(); + } + if ( ( nNode == nEndNode ) && bSearchInSelection ) + { + if ( bForward ) + nEndPos = aSel.GetEnd().GetIndex(); + else + nStartPos = aSel.GetStart().GetIndex(); + } + + if ( bForward ) + bFound = aSearcher.SearchForward( aText, &nStartPos, &nEndPos ); + else + bFound = aSearcher.SearchBackward( aText, &nEndPos, &nStartPos ); + + if ( bFound ) + { + rSel.GetStart().GetPara() = nNode; + rSel.GetStart().GetIndex() = nStartPos; + rSel.GetEnd().GetPara() = nNode; + rSel.GetEnd().GetIndex() = nEndPos; + // Select over the paragraph? + // FIXME This should be max long... + if( nEndPos == -1) + { + if ( (rSel.GetEnd().GetPara()+1) < GetParagraphCount() ) + { + rSel.GetEnd().GetPara()++; + rSel.GetEnd().GetIndex() = 0; + } + else + { + rSel.GetEnd().GetIndex() = nStartPos; + bFound = false; + } + } + + break; + } + + if ( !bForward && !nNode ) // if searching backwards, if nEndNode == 0: + break; + } + + return bFound; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |