diff options
Diffstat (limited to '')
-rw-r--r-- | sd/source/console/PresenterTextView.cxx | 1193 |
1 files changed, 1193 insertions, 0 deletions
diff --git a/sd/source/console/PresenterTextView.cxx b/sd/source/console/PresenterTextView.cxx new file mode 100644 index 0000000000..08d981fe6a --- /dev/null +++ b/sd/source/console/PresenterTextView.cxx @@ -0,0 +1,1193 @@ +/* -*- 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 "PresenterTextView.hxx" +#include "PresenterCanvasHelper.hxx" +#include "PresenterGeometryHelper.hxx" +#include "PresenterTimer.hxx" + +#include <algorithm> +#include <cmath> +#include <numeric> + +#include <com/sun/star/accessibility/AccessibleTextType.hpp> +#include <com/sun/star/container/XEnumerationAccess.hpp> +#include <com/sun/star/i18n/BreakIterator.hpp> +#include <com/sun/star/i18n/CharacterIteratorMode.hpp> +#include <com/sun/star/i18n/ScriptDirection.hpp> +#include <com/sun/star/i18n/WordType.hpp> +#include <com/sun/star/rendering/CompositeOperation.hpp> +#include <com/sun/star/rendering/TextDirection.hpp> +#include <com/sun/star/text/WritingMode2.hpp> +#include <o3tl/safeint.hxx> +#include <utility> +#include <comphelper/diagnose_ex.hxx> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::accessibility; +using namespace ::com::sun::star::uno; + +const sal_Int64 CaretBlinkInterval = 500 * 1000 * 1000; + +//#define SHOW_CHARACTER_BOXES + +namespace { + sal_Int32 Signum (const sal_Int32 nValue) + { + if (nValue < 0) + return -1; + else if (nValue > 0) + return +1; + else + return 0; + } +} + +namespace sdext::presenter { + +//===== PresenterTextView ===================================================== + +PresenterTextView::PresenterTextView ( + const Reference<XComponentContext>& rxContext, + const Reference<rendering::XCanvas>& rxCanvas, + const ::std::function<void (const css::awt::Rectangle&)>& rInvalidator) + : mxCanvas(rxCanvas), + maLocation(0,0), + maSize(0,0), + mpCaret(std::make_shared<PresenterTextCaret>( + rxContext, + [this] (sal_Int32 const nParagraphIndex, sal_Int32 const nCharacterIndex) + { return this->GetCaretBounds(nParagraphIndex, nCharacterIndex); }, + rInvalidator)), + mnLeftOffset(0), + mnTopOffset(0), + mbIsFormatPending(false) +{ + Reference<lang::XMultiComponentFactory> xFactory = + rxContext->getServiceManager(); + if ( ! xFactory.is()) + return; + + // Create the break iterator that we use to break text into lines. + mxBreakIterator = i18n::BreakIterator::create(rxContext); + + // Create the script type detector that is used to split paragraphs into + // portions of the same text direction. + mxScriptTypeDetector.set( + xFactory->createInstanceWithContext( + "com.sun.star.i18n.ScriptTypeDetector", + rxContext), + UNO_QUERY_THROW); +} + +void PresenterTextView::SetText (const Reference<text::XText>& rxText) +{ + maParagraphs.clear(); + + Reference<container::XEnumerationAccess> xParagraphAccess (rxText, UNO_QUERY); + if ( ! xParagraphAccess.is()) + return; + + Reference<container::XEnumeration> xParagraphs = + xParagraphAccess->createEnumeration(); + if ( ! xParagraphs.is()) + return; + + if ( ! mpFont || ! mpFont->PrepareFont(mxCanvas)) + return; + + sal_Int32 nCharacterCount (0); + while (xParagraphs->hasMoreElements()) + { + SharedPresenterTextParagraph pParagraph = std::make_shared<PresenterTextParagraph>( + maParagraphs.size(), + mxBreakIterator, + mxScriptTypeDetector, + Reference<text::XTextRange>(xParagraphs->nextElement(), UNO_QUERY), + mpCaret); + pParagraph->SetupCellArray(mpFont); + pParagraph->SetCharacterOffset(nCharacterCount); + nCharacterCount += pParagraph->GetCharacterCount(); + maParagraphs.push_back(pParagraph); + } + + if (mpCaret) + mpCaret->HideCaret(); + + RequestFormat(); +} + +void PresenterTextView::SetTextChangeBroadcaster ( + const ::std::function<void ()>& rBroadcaster) +{ + maTextChangeBroadcaster = rBroadcaster; +} + +void PresenterTextView::SetLocation (const css::geometry::RealPoint2D& rLocation) +{ + maLocation = rLocation; + + for (auto& rxParagraph : maParagraphs) + { + rxParagraph->SetOrigin( + maLocation.X - mnLeftOffset, + maLocation.Y - mnTopOffset); + } +} + +void PresenterTextView::SetSize (const css::geometry::RealSize2D& rSize) +{ + maSize = rSize; + RequestFormat(); +} + +double PresenterTextView::GetTotalTextHeight() +{ + if (mbIsFormatPending) + { + if ( ! mpFont->PrepareFont(mxCanvas)) + return 0; + Format(); + } + + return std::accumulate(maParagraphs.begin(), maParagraphs.end(), double(0), + [](const double& nTotalHeight, const SharedPresenterTextParagraph& rxParagraph) { + return nTotalHeight + rxParagraph->GetTotalTextHeight(); + }); +} + +void PresenterTextView::SetFont (const PresenterTheme::SharedFontDescriptor& rpFont) +{ + mpFont = rpFont; + RequestFormat(); +} + +void PresenterTextView::SetOffset( + const double nLeft, + const double nTop) +{ + mnLeftOffset = nLeft; + mnTopOffset = nTop; + + // Trigger an update of the text origin stored at the individual paragraphs. + SetLocation(maLocation); +} + +void PresenterTextView::MoveCaret ( + const sal_Int32 nDistance, + const sal_Int16 nTextType) +{ + if ( ! mpCaret) + return; + + // When the caret has not been visible yet then move it to the beginning + // of the text. + if (mpCaret->GetParagraphIndex() < 0) + { + mpCaret->SetPosition(0,0); + return; + } + + sal_Int32 nParagraphIndex (mpCaret->GetParagraphIndex()); + sal_Int32 nCharacterIndex (mpCaret->GetCharacterIndex()); + switch (nTextType) + { + default: + case AccessibleTextType::CHARACTER: + nCharacterIndex += nDistance; + break; + + case AccessibleTextType::WORD: + { + sal_Int32 nRemainingDistance (nDistance); + while (nRemainingDistance != 0) + { + SharedPresenterTextParagraph pParagraph (GetParagraph(nParagraphIndex)); + if (pParagraph) + { + const sal_Int32 nDelta (Signum(nDistance)); + nCharacterIndex = pParagraph->GetWordBoundary(nCharacterIndex, nDelta); + if (nCharacterIndex < 0) + { + // Go to previous or next paragraph. + nParagraphIndex += nDelta; + if (nParagraphIndex < 0) + { + nParagraphIndex = 0; + nCharacterIndex = 0; + nRemainingDistance = 0; + } + else if (o3tl::make_unsigned(nParagraphIndex) >= maParagraphs.size()) + { + nParagraphIndex = maParagraphs.size()-1; + pParagraph = GetParagraph(nParagraphIndex); + if (pParagraph) + nCharacterIndex = pParagraph->GetCharacterCount(); + nRemainingDistance = 0; + } + else + { + nRemainingDistance -= nDelta; + + // Move caret one character to the end of + // the previous or the start of the next paragraph. + pParagraph = GetParagraph(nParagraphIndex); + if (pParagraph) + { + if (nDistance<0) + nCharacterIndex = pParagraph->GetCharacterCount(); + else + nCharacterIndex = 0; + } + } + } + else + nRemainingDistance -= nDelta; + } + else + break; + } + break; + } + } + + // Move the caret to the new position. + mpCaret->SetPosition(nParagraphIndex, nCharacterIndex); +} + +void PresenterTextView::Paint ( + const css::awt::Rectangle& rUpdateBox) +{ + if ( ! mxCanvas.is()) + return; + if ( ! mpFont->PrepareFont(mxCanvas)) + return; + + if (mbIsFormatPending) + Format(); + + // Setup the clipping rectangle. Horizontally we make it a little + // larger to allow characters (and the caret) to stick out of their + // bounding boxes. This can happen on some characters (like the + // uppercase J) for typographical reasons. + const sal_Int32 nAdditionalLeftBorder (10); + const sal_Int32 nAdditionalRightBorder (5); + double nX (maLocation.X - mnLeftOffset); + double nY (maLocation.Y - mnTopOffset); + const sal_Int32 nClipLeft (::std::max( + PresenterGeometryHelper::Round(maLocation.X)-nAdditionalLeftBorder, rUpdateBox.X)); + const sal_Int32 nClipTop (::std::max( + PresenterGeometryHelper::Round(maLocation.Y), rUpdateBox.Y)); + const sal_Int32 nClipRight (::std::min( + PresenterGeometryHelper::Round(maLocation.X+maSize.Width)+nAdditionalRightBorder, rUpdateBox.X+rUpdateBox.Width)); + const sal_Int32 nClipBottom (::std::min( + PresenterGeometryHelper::Round(maLocation.Y+maSize.Height), rUpdateBox.Y+rUpdateBox.Height)); + if (nClipLeft>=nClipRight || nClipTop>=nClipBottom) + return; + + const awt::Rectangle aClipBox( + nClipLeft, + nClipTop, + nClipRight - nClipLeft, + nClipBottom - nClipTop); + Reference<rendering::XPolyPolygon2D> xClipPolygon ( + PresenterGeometryHelper::CreatePolygon(aClipBox, mxCanvas->getDevice())); + + const rendering::ViewState aViewState( + geometry::AffineMatrix2D(1,0,0, 0,1,0), + xClipPolygon); + + rendering::RenderState aRenderState ( + geometry::AffineMatrix2D(1,0,nX, 0,1,nY), + nullptr, + Sequence<double>(4), + rendering::CompositeOperation::SOURCE); + PresenterCanvasHelper::SetDeviceColor(aRenderState, mpFont->mnColor); + + for (const auto& rxParagraph : maParagraphs) + { + rxParagraph->Paint( + mxCanvas, + maSize, + mpFont, + aViewState, + aRenderState, + mnTopOffset, + nClipTop, + nClipBottom); + } + + aRenderState.AffineTransform.m02 = 0; + aRenderState.AffineTransform.m12 = 0; + +#ifdef SHOW_CHARACTER_BOXES + PresenterCanvasHelper::SetDeviceColor(aRenderState, 0x00808080); + for (sal_Int32 nParagraphIndex(0), nParagraphCount(GetParagraphCount()); + nParagraphIndex<nParagraphCount; + ++nParagraphIndex) + { + const SharedPresenterTextParagraph pParagraph (GetParagraph(nParagraphIndex)); + if ( ! pParagraph) + continue; + for (sal_Int32 nCharacterIndex(0),nCharacterCount(pParagraph->GetCharacterCount()); + nCharacterIndex<nCharacterCount; ++nCharacterIndex) + { + const awt::Rectangle aBox (pParagraph->GetCharacterBounds(nCharacterIndex, false)); + mxCanvas->drawPolyPolygon ( + PresenterGeometryHelper::CreatePolygon( + aBox, + mxCanvas->getDevice()), + aViewState, + aRenderState); + } + } + PresenterCanvasHelper::SetDeviceColor(aRenderState, mpFont->mnColor); +#endif + + if (mpCaret && mpCaret->IsVisible()) + { + mxCanvas->fillPolyPolygon ( + PresenterGeometryHelper::CreatePolygon( + mpCaret->GetBounds(), + mxCanvas->getDevice()), + aViewState, + aRenderState); + } +} + +const SharedPresenterTextCaret& PresenterTextView::GetCaret() const +{ + return mpCaret; +} + +awt::Rectangle PresenterTextView::GetCaretBounds ( + sal_Int32 nParagraphIndex, + const sal_Int32 nCharacterIndex) const +{ + SharedPresenterTextParagraph pParagraph (GetParagraph(nParagraphIndex)); + + if (pParagraph) + return pParagraph->GetCharacterBounds(nCharacterIndex, true); + else + return awt::Rectangle(0,0,0,0); +} + +//----- private --------------------------------------------------------------- + +void PresenterTextView::RequestFormat() +{ + mbIsFormatPending = true; +} + +void PresenterTextView::Format() +{ + mbIsFormatPending = false; + + double nY (0); + for (const auto& rxParagraph : maParagraphs) + { + rxParagraph->Format(nY, maSize.Width, mpFont); + nY += rxParagraph->GetTotalTextHeight(); + } + + if (maTextChangeBroadcaster) + maTextChangeBroadcaster(); +} + +sal_Int32 PresenterTextView::GetParagraphCount() const +{ + return maParagraphs.size(); +} + +SharedPresenterTextParagraph PresenterTextView::GetParagraph ( + const sal_Int32 nParagraphIndex) const +{ + if (nParagraphIndex < 0) + return SharedPresenterTextParagraph(); + else if (o3tl::make_unsigned(nParagraphIndex)>=maParagraphs.size()) + return SharedPresenterTextParagraph(); + else + return maParagraphs[nParagraphIndex]; +} + +//===== PresenterTextParagraph ================================================ + +PresenterTextParagraph::PresenterTextParagraph ( + const sal_Int32 nParagraphIndex, + const Reference<i18n::XBreakIterator>& rxBreakIterator, + const Reference<i18n::XScriptTypeDetector>& rxScriptTypeDetector, + const Reference<text::XTextRange>& rxTextRange, + SharedPresenterTextCaret xCaret) + : mnParagraphIndex(nParagraphIndex), + mpCaret(std::move(xCaret)), + mxBreakIterator(rxBreakIterator), + mxScriptTypeDetector(rxScriptTypeDetector), + mnVerticalOffset(0), + mnXOrigin(0), + mnYOrigin(0), + mnWidth(0), + mnAscent(0), + mnDescent(0), + mnLineHeight(-1), + mnWritingMode (text::WritingMode2::LR_TB), + mnCharacterOffset(0) +{ + if (!rxTextRange.is()) + return; + + Reference<beans::XPropertySet> xProperties (rxTextRange, UNO_QUERY); + try + { + xProperties->getPropertyValue("WritingMode") >>= mnWritingMode; + } + catch(beans::UnknownPropertyException&) + { + // Ignore the exception. Use the default value. + } + + msParagraphText = rxTextRange->getString(); +} + +void PresenterTextParagraph::Paint ( + const Reference<rendering::XCanvas>& rxCanvas, + const geometry::RealSize2D& rSize, + const PresenterTheme::SharedFontDescriptor& rpFont, + const rendering::ViewState& rViewState, + rendering::RenderState& rRenderState, + const double nTopOffset, + const double nClipTop, + const double nClipBottom) +{ + if (mnLineHeight <= 0) + return; + + sal_Int8 nTextDirection (GetTextDirection()); + + const double nSavedM12 (rRenderState.AffineTransform.m12); + + if ( ! IsTextReferencePointLeft()) + rRenderState.AffineTransform.m02 += rSize.Width; + +#ifdef SHOW_CHARACTER_BOXES + for (sal_Int32 nIndex=0,nCount=maLines.size(); + nIndex<nCount; + ++nIndex) + { + Line& rLine (maLines[nIndex]); + rLine.ProvideLayoutedLine(msParagraphText, rpFont, nTextDirection); + } +#endif + + for (sal_Int32 nIndex=0,nCount=maLines.size(); + nIndex<nCount; + ++nIndex, rRenderState.AffineTransform.m12 += mnLineHeight) + { + Line& rLine (maLines[nIndex]); + + // Paint only visible lines. + const double nLineTop = rLine.mnBaseLine - mnAscent - nTopOffset; + if (nLineTop + mnLineHeight< nClipTop) + continue; + else if (nLineTop > nClipBottom) + break; + rLine.ProvideLayoutedLine(msParagraphText, rpFont, nTextDirection); + + rRenderState.AffineTransform.m12 = nSavedM12 + rLine.mnBaseLine; + + rxCanvas->drawTextLayout ( + rLine.mxLayoutedLine, + rViewState, + rRenderState); + } + rRenderState.AffineTransform.m12 = nSavedM12; + + if ( ! IsTextReferencePointLeft()) + rRenderState.AffineTransform.m02 -= rSize.Width; +} + +void PresenterTextParagraph::Format ( + const double nY, + const double nWidth, + const PresenterTheme::SharedFontDescriptor& rpFont) +{ + // Make sure that the text view is in a valid and sane state. + if ( ! mxBreakIterator.is() || ! mxScriptTypeDetector.is()) + return; + if (nWidth<=0) + return; + if ( ! rpFont || ! rpFont->mxFont.is()) + return; + + sal_Int32 nPosition (0); + + mnWidth = nWidth; + maLines.clear(); + mnLineHeight = 0; + mnAscent = 0; + mnDescent = 0; + mnVerticalOffset = nY; + maWordBoundaries.clear(); + maWordBoundaries.push_back(0); + + const rendering::FontMetrics aMetrics (rpFont->mxFont->getFontMetrics()); + mnAscent = aMetrics.Ascent; + mnDescent = aMetrics.Descent; + mnLineHeight = aMetrics.Ascent + aMetrics.Descent + aMetrics.ExternalLeading; + nPosition = 0; + i18n::Boundary aCurrentLine(0,0); + while (true) + { + const i18n::Boundary aWordBoundary = mxBreakIterator->nextWord( + msParagraphText, + nPosition, + lang::Locale(), + i18n::WordType::ANYWORD_IGNOREWHITESPACES); + AddWord(nWidth, aCurrentLine, aWordBoundary.startPos, rpFont); + + // Remember the new word boundary for caret travelling by words. + // Prevent duplicates. + if (aWordBoundary.startPos > maWordBoundaries.back()) + maWordBoundaries.push_back(aWordBoundary.startPos); + + if (aWordBoundary.endPos>aWordBoundary.startPos) + AddWord(nWidth, aCurrentLine, aWordBoundary.endPos, rpFont); + + if (aWordBoundary.startPos<0 || aWordBoundary.endPos<0) + break; + if (nPosition >= aWordBoundary.endPos) + break; + nPosition = aWordBoundary.endPos; + } + + if (aCurrentLine.endPos>aCurrentLine.startPos) + AddLine(aCurrentLine); + +} + +sal_Int32 PresenterTextParagraph::GetWordBoundary( + const sal_Int32 nLocalCharacterIndex, + const sal_Int32 nDistance) +{ + OSL_ASSERT(nDistance==-1 || nDistance==+1); + + if (nLocalCharacterIndex < 0) + { + // The caller asked for the start or end position of the paragraph. + if (nDistance < 0) + return 0; + else + return GetCharacterCount(); + } + + sal_Int32 nIndex (0); + for (sal_Int32 nCount (maWordBoundaries.size()); nIndex<nCount; ++nIndex) + { + if (maWordBoundaries[nIndex] >= nLocalCharacterIndex) + { + // When inside the word (not at its start or end) then + // first move to the start or end before going the previous or + // next word. + if (maWordBoundaries[nIndex] > nLocalCharacterIndex) + if (nDistance > 0) + --nIndex; + break; + } + } + + nIndex += nDistance; + + if (nIndex < 0) + return -1; + else if (o3tl::make_unsigned(nIndex)>=maWordBoundaries.size()) + return -1; + else + return maWordBoundaries[nIndex]; +} + +sal_Int32 PresenterTextParagraph::GetCaretPosition() const +{ + if (mpCaret && mpCaret->GetParagraphIndex()==mnParagraphIndex) + return mpCaret->GetCharacterIndex(); + else + return -1; +} + +void PresenterTextParagraph::SetCaretPosition (const sal_Int32 nPosition) const +{ + if (mpCaret && mpCaret->GetParagraphIndex()==mnParagraphIndex) + return mpCaret->SetPosition(mnParagraphIndex, nPosition); +} + +void PresenterTextParagraph::SetOrigin (const double nXOrigin, const double nYOrigin) +{ + mnXOrigin = nXOrigin; + mnYOrigin = nYOrigin; +} + +awt::Point PresenterTextParagraph::GetRelativeLocation() const +{ + return awt::Point( + sal_Int32(mnXOrigin), + sal_Int32(mnYOrigin + mnVerticalOffset)); +} + +awt::Size PresenterTextParagraph::GetSize() const +{ + return awt::Size( + sal_Int32(mnWidth), + sal_Int32(GetTotalTextHeight())); +} + +void PresenterTextParagraph::AddWord ( + const double nWidth, + i18n::Boundary& rCurrentLine, + const sal_Int32 nWordBoundary, + const PresenterTheme::SharedFontDescriptor& rpFont) +{ + sal_Int32 nLineStart (0); + if ( ! maLines.empty()) + nLineStart = rCurrentLine.startPos; + + const OUString sLineCandidate ( + msParagraphText.copy(nLineStart, nWordBoundary-nLineStart)); + + css::geometry::RealRectangle2D aLineBox ( + PresenterCanvasHelper::GetTextBoundingBox ( + rpFont->mxFont, + sLineCandidate, + mnWritingMode)); + const double nLineWidth (aLineBox.X2 - aLineBox.X1); + + if (nLineWidth >= nWidth) + { + // Add new line with a single word (so far). + AddLine(rCurrentLine); + } + rCurrentLine.endPos = nWordBoundary; +} + +void PresenterTextParagraph::AddLine ( + i18n::Boundary& rCurrentLine) +{ + Line aLine (rCurrentLine.startPos, rCurrentLine.endPos); + + // Find the start and end of the line with respect to cells. + if (!maLines.empty()) + { + aLine.mnLineStartCellIndex = maLines.back().mnLineEndCellIndex; + aLine.mnBaseLine = maLines.back().mnBaseLine + mnLineHeight; + } + else + { + aLine.mnLineStartCellIndex = 0; + aLine.mnBaseLine = mnVerticalOffset + mnAscent; + } + sal_Int32 nCellIndex (aLine.mnLineStartCellIndex); + double nWidth (0); + for ( ; nCellIndex<sal_Int32(maCells.size()); ++nCellIndex) + { + const Cell& rCell (maCells[nCellIndex]); + if (rCell.mnCharacterIndex+rCell.mnCharacterCount > aLine.mnLineEndCharacterIndex) + break; + nWidth += rCell.mnCellWidth; + } + aLine.mnLineEndCellIndex = nCellIndex; + aLine.mnWidth = nWidth; + + maLines.push_back(aLine); + + rCurrentLine.startPos = rCurrentLine.endPos; +} + +double PresenterTextParagraph::GetTotalTextHeight() const +{ + return maLines.size() * mnLineHeight; +} + +void PresenterTextParagraph::SetCharacterOffset (const sal_Int32 nCharacterOffset) +{ + mnCharacterOffset = nCharacterOffset; +} + +sal_Int32 PresenterTextParagraph::GetCharacterCount() const +{ + return msParagraphText.getLength(); +} + +sal_Unicode PresenterTextParagraph::GetCharacter ( + const sal_Int32 nGlobalCharacterIndex) const +{ + if (nGlobalCharacterIndex<mnCharacterOffset + || nGlobalCharacterIndex>=mnCharacterOffset+msParagraphText.getLength()) + { + return sal_Unicode(); + } + else + { + return msParagraphText[nGlobalCharacterIndex - mnCharacterOffset]; + } +} + +const OUString& PresenterTextParagraph::GetText() const +{ + return msParagraphText; +} + +TextSegment PresenterTextParagraph::GetTextSegment ( + const sal_Int32 nOffset, + const sal_Int32 nIndex, + const sal_Int16 nTextType) const +{ + switch(nTextType) + { + case AccessibleTextType::PARAGRAPH: + return TextSegment( + msParagraphText, + mnCharacterOffset, + mnCharacterOffset+msParagraphText.getLength()); + + case AccessibleTextType::SENTENCE: + if (mxBreakIterator.is()) + { + const sal_Int32 nStart (mxBreakIterator->beginOfSentence( + msParagraphText, nIndex-mnCharacterOffset, lang::Locale())); + const sal_Int32 nEnd (mxBreakIterator->endOfSentence( + msParagraphText, nIndex-mnCharacterOffset, lang::Locale())); + if (nStart < nEnd) + return TextSegment( + msParagraphText.copy(nStart, nEnd-nStart), + nStart+mnCharacterOffset, + nEnd+mnCharacterOffset); + } + break; + + case AccessibleTextType::WORD: + if (mxBreakIterator.is()) + return GetWordTextSegment(nOffset, nIndex); + break; + + case AccessibleTextType::LINE: + { + auto iLine = std::find_if(maLines.begin(), maLines.end(), + [nIndex](const Line& rLine) { return nIndex < rLine.mnLineEndCharacterIndex; }); + if (iLine != maLines.end()) + { + return TextSegment( + msParagraphText.copy( + iLine->mnLineStartCharacterIndex, + iLine->mnLineEndCharacterIndex - iLine->mnLineStartCharacterIndex), + iLine->mnLineStartCharacterIndex, + iLine->mnLineEndCharacterIndex); + } + } + break; + + // Handle GLYPH and ATTRIBUTE_RUN like CHARACTER because we can not + // do better at the moment. + case AccessibleTextType::CHARACTER: + case AccessibleTextType::GLYPH: + case AccessibleTextType::ATTRIBUTE_RUN: + return CreateTextSegment(nIndex+nOffset, nIndex+nOffset+1); + } + + return TextSegment(OUString(), 0,0); +} + +TextSegment PresenterTextParagraph::GetWordTextSegment ( + const sal_Int32 nOffset, + const sal_Int32 nIndex) const +{ + sal_Int32 nCurrentOffset (nOffset); + sal_Int32 nCurrentIndex (nIndex); + + i18n::Boundary aWordBoundary; + if (nCurrentOffset == 0) + aWordBoundary = mxBreakIterator->getWordBoundary( + msParagraphText, + nIndex, + lang::Locale(), + i18n::WordType::ANYWORD_IGNOREWHITESPACES, + true); + else if (nCurrentOffset < 0) + { + while (nCurrentOffset<0 && nCurrentIndex>0) + { + aWordBoundary = mxBreakIterator->previousWord( + msParagraphText, + nCurrentIndex, + lang::Locale(), + i18n::WordType::ANYWORD_IGNOREWHITESPACES); + nCurrentIndex = aWordBoundary.startPos; + ++nCurrentOffset; + } + } + else + { + while (nCurrentOffset>0 && nCurrentIndex<=GetCharacterCount()) + { + aWordBoundary = mxBreakIterator->nextWord( + msParagraphText, + nCurrentIndex, + lang::Locale(), + i18n::WordType::ANYWORD_IGNOREWHITESPACES); + nCurrentIndex = aWordBoundary.endPos; + --nCurrentOffset; + } + } + + return CreateTextSegment(aWordBoundary.startPos, aWordBoundary.endPos); +} + +TextSegment PresenterTextParagraph::CreateTextSegment ( + sal_Int32 nStartIndex, + sal_Int32 nEndIndex) const +{ + if (nEndIndex <= nStartIndex) + return TextSegment( + OUString(), + nStartIndex, + nEndIndex); + else + return TextSegment( + msParagraphText.copy(nStartIndex, nEndIndex-nStartIndex), + nStartIndex, + nEndIndex); +} + +awt::Rectangle PresenterTextParagraph::GetCharacterBounds ( + sal_Int32 nGlobalCharacterIndex, + const bool bCaretBox) +{ + // Find the line that contains the requested character and accumulate + // the previous line heights. + double nX (mnXOrigin); + double nY (mnYOrigin + mnVerticalOffset + mnAscent); + const sal_Int8 nTextDirection (GetTextDirection()); + for (sal_Int32 nLineIndex=0,nLineCount=maLines.size(); + nLineIndex<nLineCount; + ++nLineIndex, nY+=mnLineHeight) + { + Line& rLine (maLines[nLineIndex]); + // Skip lines before the indexed character. + if (nGlobalCharacterIndex >= rLine.mnLineEndCharacterIndex) + // When in the last line then allow the index past the last char. + if (nLineIndex<nLineCount-1) + continue; + + rLine.ProvideCellBoxes(); + + const sal_Int32 nCellIndex (nGlobalCharacterIndex - rLine.mnLineStartCharacterIndex); + + // The cell bounding box is defined relative to the origin of + // the current line. Therefore we have to add the absolute + // position of the line. + geometry::RealRectangle2D rCellBox (rLine.maCellBoxes[ + ::std::min(nCellIndex, rLine.maCellBoxes.getLength()-1)]); + + double nLeft = nX + rCellBox.X1; + double nRight = nX + rCellBox.X2; + if (nTextDirection == rendering::TextDirection::WEAK_RIGHT_TO_LEFT) + { + const double nOldRight (nRight); + nRight = rLine.mnWidth - nLeft; + nLeft = rLine.mnWidth - nOldRight; + } + double nTop = nY - mnAscent; + double nBottom; + if (bCaretBox) + { + nBottom = nTop + mnLineHeight; + if (nCellIndex >= rLine.maCellBoxes.getLength()) + nLeft = nRight-2; + if (nLeft < nX) + nLeft = nX; + nRight = nLeft+2; + } + else + { + nBottom = nTop + mnAscent + mnDescent; + } + const sal_Int32 nX1 = sal_Int32(floor(nLeft)); + const sal_Int32 nY1 = sal_Int32(floor(nTop)); + const sal_Int32 nX2 = sal_Int32(ceil(nRight)); + const sal_Int32 nY2 = sal_Int32(ceil(nBottom)); + + return awt::Rectangle(nX1,nY1,nX2-nX1+1,nY2-nY1+1); + } + + // We are still here. That means that the given index lies past the + // last character in the paragraph. + // Return an empty box that lies past the last character. Better than nothing. + return awt::Rectangle(sal_Int32(nX+0.5), sal_Int32(nY+0.5), 0, 0); +} + +sal_Int8 PresenterTextParagraph::GetTextDirection() const +{ + // Find first portion that has a non-neutral text direction. + sal_Int32 nPosition (0); + sal_Int32 nTextLength (msParagraphText.getLength()); + while (nPosition < nTextLength) + { + const sal_Int16 nScriptDirection ( + mxScriptTypeDetector->getScriptDirection( + msParagraphText, nPosition, i18n::ScriptDirection::NEUTRAL)); + switch (nScriptDirection) + { + case i18n::ScriptDirection::NEUTRAL: + // continue looping. + break; + case i18n::ScriptDirection::LEFT_TO_RIGHT: + return rendering::TextDirection::WEAK_LEFT_TO_RIGHT; + + case i18n::ScriptDirection::RIGHT_TO_LEFT: + return rendering::TextDirection::WEAK_RIGHT_TO_LEFT; + } + + nPosition = mxScriptTypeDetector->endOfScriptDirection( + msParagraphText, nPosition, nScriptDirection); + } + + // All text in paragraph is neutral. Fall back on writing mode taken + // from the XText (which may not be properly initialized.) + sal_Int8 nTextDirection(rendering::TextDirection::WEAK_LEFT_TO_RIGHT); + switch(mnWritingMode) + { + case text::WritingMode2::LR_TB: + nTextDirection = rendering::TextDirection::WEAK_LEFT_TO_RIGHT; + break; + + case text::WritingMode2::RL_TB: + nTextDirection = rendering::TextDirection::WEAK_RIGHT_TO_LEFT; + break; + + default: + case text::WritingMode2::TB_RL: + case text::WritingMode2::TB_LR: + // Can not handle this. Use default and hope for the best. + break; + } + return nTextDirection; +} + +bool PresenterTextParagraph::IsTextReferencePointLeft() const +{ + return mnWritingMode != text::WritingMode2::RL_TB; +} + +void PresenterTextParagraph::SetupCellArray ( + const PresenterTheme::SharedFontDescriptor& rpFont) +{ + maCells.clear(); + + if ( ! rpFont || ! rpFont->mxFont.is()) + return; + + sal_Int32 nPosition (0); + sal_Int32 nIndex (0); + const sal_Int32 nTextLength (msParagraphText.getLength()); + const sal_Int8 nTextDirection (GetTextDirection()); + while (nPosition < nTextLength) + { + const sal_Int32 nNewPosition (mxBreakIterator->nextCharacters( + msParagraphText, + nPosition, + lang::Locale(), + i18n::CharacterIteratorMode::SKIPCELL, + 1, + nIndex)); + + rendering::StringContext aContext (msParagraphText, nPosition, nNewPosition-nPosition); + Reference<rendering::XTextLayout> xLayout ( + rpFont->mxFont->createTextLayout(aContext, nTextDirection, 0)); + css::geometry::RealRectangle2D aCharacterBox (xLayout->queryTextBounds()); + + maCells.emplace_back( + nPosition, + nNewPosition-nPosition, + aCharacterBox.X2-aCharacterBox.X1); + + nPosition = nNewPosition; + } +} + +//===== PresenterTextCaret ================================================---- + +PresenterTextCaret::PresenterTextCaret ( + uno::Reference<uno::XComponentContext> const& xContext, + ::std::function<css::awt::Rectangle (const sal_Int32,const sal_Int32)> aCharacterBoundsAccess, + ::std::function<void (const css::awt::Rectangle&)> aInvalidator) + : m_xContext(xContext) + , mnParagraphIndex(-1), + mnCharacterIndex(-1), + mnCaretBlinkTaskId(0), + mbIsCaretVisible(false), + maCharacterBoundsAccess(std::move(aCharacterBoundsAccess)), + maInvalidator(std::move(aInvalidator)) +{ +} + +PresenterTextCaret::~PresenterTextCaret() +{ + try + { + HideCaret(); + } + catch (uno::Exception const&) + { + TOOLS_WARN_EXCEPTION("sdext.presenter", "unexpected exception in ~PresenterTextCaret"); + } +} + +void PresenterTextCaret::ShowCaret() +{ + if (mnCaretBlinkTaskId == 0) + { + mnCaretBlinkTaskId = PresenterTimer::ScheduleRepeatedTask ( + m_xContext, + [this] (TimeValue const&) { return this->InvertCaret(); }, + CaretBlinkInterval, + CaretBlinkInterval); + } + mbIsCaretVisible = true; +} + +void PresenterTextCaret::HideCaret() +{ + if (mnCaretBlinkTaskId != 0) + { + PresenterTimer::CancelTask(mnCaretBlinkTaskId); + mnCaretBlinkTaskId = 0; + } + mbIsCaretVisible = false; + // Reset the caret position. + mnParagraphIndex = -1; + mnCharacterIndex = -1; +} + + +void PresenterTextCaret::SetPosition ( + const sal_Int32 nParagraphIndex, + const sal_Int32 nCharacterIndex) +{ + if (mnParagraphIndex == nParagraphIndex + && mnCharacterIndex == nCharacterIndex) + return; + + if (mnParagraphIndex >= 0) + maInvalidator(maCaretBounds); + + const sal_Int32 nOldParagraphIndex (mnParagraphIndex); + const sal_Int32 nOldCharacterIndex (mnCharacterIndex); + mnParagraphIndex = nParagraphIndex; + mnCharacterIndex = nCharacterIndex; + maCaretBounds = maCharacterBoundsAccess(mnParagraphIndex, mnCharacterIndex); + if (mnParagraphIndex >= 0) + ShowCaret(); + else + HideCaret(); + + if (mnParagraphIndex >= 0) + maInvalidator(maCaretBounds); + + if (maBroadcaster) + maBroadcaster( + nOldParagraphIndex, + nOldCharacterIndex, + mnParagraphIndex, + mnCharacterIndex); +} + + +void PresenterTextCaret::SetCaretMotionBroadcaster ( + const ::std::function<void (sal_Int32,sal_Int32,sal_Int32,sal_Int32)>& rBroadcaster) +{ + maBroadcaster = rBroadcaster; +} + +const css::awt::Rectangle& PresenterTextCaret::GetBounds() const +{ + return maCaretBounds; +} + +void PresenterTextCaret::InvertCaret() +{ + mbIsCaretVisible = !mbIsCaretVisible; + if (mnParagraphIndex >= 0) + maInvalidator(maCaretBounds); +} + +//===== PresenterTextParagraph::Cell ========================================== + +PresenterTextParagraph::Cell::Cell ( + const sal_Int32 nCharacterIndex, + const sal_Int32 nCharacterCount, + const double nCellWidth) + : mnCharacterIndex(nCharacterIndex), + mnCharacterCount(nCharacterCount), + mnCellWidth(nCellWidth) +{ +} + +//===== PresenterTextParagraph::Line ========================================== + +PresenterTextParagraph::Line::Line ( + const sal_Int32 nLineStartCharacterIndex, + const sal_Int32 nLineEndCharacterIndex) + : mnLineStartCharacterIndex(nLineStartCharacterIndex), + mnLineEndCharacterIndex(nLineEndCharacterIndex), + mnLineStartCellIndex(-1), mnLineEndCellIndex(-1), + mnBaseLine(0), mnWidth(0) +{ +} + +void PresenterTextParagraph::Line::ProvideCellBoxes() +{ + if ( mnLineStartCharacterIndex < mnLineEndCharacterIndex && !maCellBoxes.hasElements() ) + { + if (mxLayoutedLine.is()) + maCellBoxes = mxLayoutedLine->queryInkMeasures(); + else + { + OSL_ASSERT(mxLayoutedLine.is()); + } + } +} + +void PresenterTextParagraph::Line::ProvideLayoutedLine ( + const OUString& rsParagraphText, + const PresenterTheme::SharedFontDescriptor& rpFont, + const sal_Int8 nTextDirection) +{ + if ( ! mxLayoutedLine.is()) + { + const rendering::StringContext aContext ( + rsParagraphText, + mnLineStartCharacterIndex, + mnLineEndCharacterIndex - mnLineStartCharacterIndex); + + mxLayoutedLine = rpFont->mxFont->createTextLayout( + aContext, + nTextDirection, + 0); + } +} + +} // end of namespace ::sdext::presenter + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |