1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  *
9  * This file incorporates work covered by the following license notice:
10  *
11  *   Licensed to the Apache Software Foundation (ASF) under one or more
12  *   contributor license agreements. See the NOTICE file distributed
13  *   with this work for additional information regarding copyright
14  *   ownership. The ASF licenses this file to you under the Apache
15  *   License, Version 2.0 (the "License"); you may not use this file
16  *   except in compliance with the License. You may obtain a copy of
17  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18  */
19 
20 #include "PresenterTextView.hxx"
21 #include "PresenterCanvasHelper.hxx"
22 #include "PresenterGeometryHelper.hxx"
23 #include "PresenterTimer.hxx"
24 
25 #include <algorithm>
26 #include <cmath>
27 #include <numeric>
28 
29 #include <com/sun/star/accessibility/AccessibleTextType.hpp>
30 #include <com/sun/star/container/XEnumerationAccess.hpp>
31 #include <com/sun/star/i18n/BreakIterator.hpp>
32 #include <com/sun/star/i18n/CharacterIteratorMode.hpp>
33 #include <com/sun/star/i18n/ScriptDirection.hpp>
34 #include <com/sun/star/i18n/WordType.hpp>
35 #include <com/sun/star/rendering/CompositeOperation.hpp>
36 #include <com/sun/star/rendering/TextDirection.hpp>
37 #include <com/sun/star/text/WritingMode2.hpp>
38 #include <o3tl/safeint.hxx>
39 #include <tools/diagnose_ex.h>
40 
41 using namespace ::com::sun::star;
42 using namespace ::com::sun::star::accessibility;
43 using namespace ::com::sun::star::uno;
44 
45 const sal_Int64 CaretBlinkInterval = 500 * 1000 * 1000;
46 
47 //#define SHOW_CHARACTER_BOXES
48 
49 namespace {
Signum(const sal_Int32 nValue)50     sal_Int32 Signum (const sal_Int32 nValue)
51     {
52         if (nValue < 0)
53             return -1;
54         else if (nValue > 0)
55             return +1;
56         else
57             return 0;
58     }
59 }
60 
61 namespace sdext::presenter {
62 
63 //===== PresenterTextView =====================================================
64 
PresenterTextView(const Reference<XComponentContext> & rxContext,const Reference<rendering::XCanvas> & rxCanvas,const::std::function<void (const css::awt::Rectangle &)> & rInvalidator)65 PresenterTextView::PresenterTextView (
66     const Reference<XComponentContext>& rxContext,
67     const Reference<rendering::XCanvas>& rxCanvas,
68     const ::std::function<void (const css::awt::Rectangle&)>& rInvalidator)
69     : mxCanvas(rxCanvas),
70       mxBreakIterator(),
71       mxScriptTypeDetector(),
72       maLocation(0,0),
73       maSize(0,0),
74       mpFont(),
75       maParagraphs(),
76       mpCaret(std::make_shared<PresenterTextCaret>(
77           rxContext,
78           [this] (sal_Int32 const nParagraphIndex, sal_Int32 const nCharacterIndex)
79               { return this->GetCaretBounds(nParagraphIndex, nCharacterIndex); },
80           rInvalidator)),
81       mnLeftOffset(0),
82       mnTopOffset(0),
83       mbIsFormatPending(false),
84       maTextChangeBroadcaster()
85 {
86     Reference<lang::XMultiComponentFactory> xFactory =
87         rxContext->getServiceManager();
88     if ( ! xFactory.is())
89         return;
90 
91     // Create the break iterator that we use to break text into lines.
92     mxBreakIterator = i18n::BreakIterator::create(rxContext);
93 
94     // Create the script type detector that is used to split paragraphs into
95     // portions of the same text direction.
96     mxScriptTypeDetector.set(
97         xFactory->createInstanceWithContext(
98             "com.sun.star.i18n.ScriptTypeDetector",
99             rxContext),
100         UNO_QUERY_THROW);
101 }
102 
SetText(const Reference<text::XText> & rxText)103 void PresenterTextView::SetText (const Reference<text::XText>& rxText)
104 {
105     maParagraphs.clear();
106 
107     Reference<container::XEnumerationAccess> xParagraphAccess (rxText, UNO_QUERY);
108     if ( ! xParagraphAccess.is())
109         return;
110 
111     Reference<container::XEnumeration> xParagraphs =
112         xParagraphAccess->createEnumeration();
113     if ( ! xParagraphs.is())
114         return;
115 
116     if ( ! mpFont || ! mpFont->PrepareFont(mxCanvas))
117         return;
118 
119     sal_Int32 nCharacterCount (0);
120     while (xParagraphs->hasMoreElements())
121     {
122         SharedPresenterTextParagraph pParagraph = std::make_shared<PresenterTextParagraph>(
123             maParagraphs.size(),
124             mxBreakIterator,
125             mxScriptTypeDetector,
126             Reference<text::XTextRange>(xParagraphs->nextElement(), UNO_QUERY),
127             mpCaret);
128         pParagraph->SetupCellArray(mpFont);
129         pParagraph->SetCharacterOffset(nCharacterCount);
130         nCharacterCount += pParagraph->GetCharacterCount();
131         maParagraphs.push_back(pParagraph);
132     }
133 
134     if (mpCaret)
135         mpCaret->HideCaret();
136 
137     RequestFormat();
138 }
139 
SetTextChangeBroadcaster(const::std::function<void ()> & rBroadcaster)140 void PresenterTextView::SetTextChangeBroadcaster (
141     const ::std::function<void ()>& rBroadcaster)
142 {
143     maTextChangeBroadcaster = rBroadcaster;
144 }
145 
SetLocation(const css::geometry::RealPoint2D & rLocation)146 void PresenterTextView::SetLocation (const css::geometry::RealPoint2D& rLocation)
147 {
148     maLocation = rLocation;
149 
150     for (auto& rxParagraph : maParagraphs)
151     {
152         rxParagraph->SetOrigin(
153             maLocation.X - mnLeftOffset,
154             maLocation.Y - mnTopOffset);
155     }
156 }
157 
SetSize(const css::geometry::RealSize2D & rSize)158 void PresenterTextView::SetSize (const css::geometry::RealSize2D& rSize)
159 {
160     maSize = rSize;
161     RequestFormat();
162 }
163 
GetTotalTextHeight()164 double PresenterTextView::GetTotalTextHeight()
165 {
166     if (mbIsFormatPending)
167     {
168         if ( ! mpFont->PrepareFont(mxCanvas))
169             return 0;
170         Format();
171     }
172 
173     return std::accumulate(maParagraphs.begin(), maParagraphs.end(), double(0),
174         [](const double& nTotalHeight, const SharedPresenterTextParagraph& rxParagraph) {
175             return nTotalHeight + rxParagraph->GetTotalTextHeight();
176         });
177 }
178 
SetFont(const PresenterTheme::SharedFontDescriptor & rpFont)179 void PresenterTextView::SetFont (const PresenterTheme::SharedFontDescriptor& rpFont)
180 {
181     mpFont = rpFont;
182     RequestFormat();
183 }
184 
SetOffset(const double nLeft,const double nTop)185 void PresenterTextView::SetOffset(
186     const double nLeft,
187     const double nTop)
188 {
189     mnLeftOffset = nLeft;
190     mnTopOffset = nTop;
191 
192     // Trigger an update of the text origin stored at the individual paragraphs.
193     SetLocation(maLocation);
194 }
195 
MoveCaret(const sal_Int32 nDistance,const sal_Int16 nTextType)196 void PresenterTextView::MoveCaret (
197     const sal_Int32 nDistance,
198     const sal_Int16 nTextType)
199 {
200     if ( ! mpCaret)
201         return;
202 
203     // When the caret has not been visible yet then move it to the beginning
204     // of the text.
205     if (mpCaret->GetParagraphIndex() < 0)
206     {
207         mpCaret->SetPosition(0,0);
208         return;
209     }
210 
211     sal_Int32 nParagraphIndex (mpCaret->GetParagraphIndex());
212     sal_Int32 nCharacterIndex (mpCaret->GetCharacterIndex());
213     switch (nTextType)
214     {
215         default:
216         case AccessibleTextType::CHARACTER:
217             nCharacterIndex += nDistance;
218             break;
219 
220         case AccessibleTextType::WORD:
221         {
222             sal_Int32 nRemainingDistance (nDistance);
223             while (nRemainingDistance != 0)
224             {
225                 SharedPresenterTextParagraph pParagraph (GetParagraph(nParagraphIndex));
226                 if (pParagraph)
227                 {
228                     const sal_Int32 nDelta (Signum(nDistance));
229                     nCharacterIndex = pParagraph->GetWordBoundary(nCharacterIndex, nDelta);
230                     if (nCharacterIndex < 0)
231                     {
232                         // Go to previous or next paragraph.
233                         nParagraphIndex += nDelta;
234                         if (nParagraphIndex < 0)
235                         {
236                             nParagraphIndex = 0;
237                             nCharacterIndex = 0;
238                             nRemainingDistance = 0;
239                         }
240                         else if (o3tl::make_unsigned(nParagraphIndex) >= maParagraphs.size())
241                         {
242                             nParagraphIndex = maParagraphs.size()-1;
243                             pParagraph = GetParagraph(nParagraphIndex);
244                             if (pParagraph)
245                                 nCharacterIndex = pParagraph->GetCharacterCount();
246                             nRemainingDistance = 0;
247                         }
248                         else
249                         {
250                             nRemainingDistance -= nDelta;
251 
252                             // Move caret one character to the end of
253                             // the previous or the start of the next paragraph.
254                             pParagraph = GetParagraph(nParagraphIndex);
255                             if (pParagraph)
256                             {
257                                 if (nDistance<0)
258                                     nCharacterIndex = pParagraph->GetCharacterCount();
259                                 else
260                                     nCharacterIndex = 0;
261                             }
262                         }
263                     }
264                     else
265                         nRemainingDistance -= nDelta;
266                 }
267                 else
268                     break;
269             }
270             break;
271         }
272     }
273 
274     // Move the caret to the new position.
275     mpCaret->SetPosition(nParagraphIndex, nCharacterIndex);
276 }
277 
Paint(const css::awt::Rectangle & rUpdateBox)278 void PresenterTextView::Paint (
279     const css::awt::Rectangle& rUpdateBox)
280 {
281     if ( ! mxCanvas.is())
282         return;
283     if ( ! mpFont->PrepareFont(mxCanvas))
284         return;
285 
286     if (mbIsFormatPending)
287         Format();
288 
289     // Setup the clipping rectangle.  Horizontally we make it a little
290     // larger to allow characters (and the caret) to stick out of their
291     // bounding boxes.  This can happen on some characters (like the
292     // uppercase J) for typographical reasons.
293     const sal_Int32 nAdditionalLeftBorder (10);
294     const sal_Int32 nAdditionalRightBorder (5);
295     double nX (maLocation.X - mnLeftOffset);
296     double nY (maLocation.Y - mnTopOffset);
297     const sal_Int32 nClipLeft (::std::max(
298         PresenterGeometryHelper::Round(maLocation.X)-nAdditionalLeftBorder, rUpdateBox.X));
299     const sal_Int32 nClipTop (::std::max(
300         PresenterGeometryHelper::Round(maLocation.Y), rUpdateBox.Y));
301     const sal_Int32 nClipRight (::std::min(
302         PresenterGeometryHelper::Round(maLocation.X+maSize.Width)+nAdditionalRightBorder, rUpdateBox.X+rUpdateBox.Width));
303     const sal_Int32 nClipBottom (::std::min(
304         PresenterGeometryHelper::Round(maLocation.Y+maSize.Height), rUpdateBox.Y+rUpdateBox.Height));
305     if (nClipLeft>=nClipRight || nClipTop>=nClipBottom)
306         return;
307 
308     const awt::Rectangle aClipBox(
309         nClipLeft,
310         nClipTop,
311         nClipRight - nClipLeft,
312         nClipBottom - nClipTop);
313     Reference<rendering::XPolyPolygon2D> xClipPolygon (
314         PresenterGeometryHelper::CreatePolygon(aClipBox, mxCanvas->getDevice()));
315 
316     const rendering::ViewState aViewState(
317         geometry::AffineMatrix2D(1,0,0, 0,1,0),
318         xClipPolygon);
319 
320     rendering::RenderState aRenderState (
321         geometry::AffineMatrix2D(1,0,nX, 0,1,nY),
322         nullptr,
323         Sequence<double>(4),
324         rendering::CompositeOperation::SOURCE);
325     PresenterCanvasHelper::SetDeviceColor(aRenderState, mpFont->mnColor);
326 
327     for (const auto& rxParagraph : maParagraphs)
328     {
329         rxParagraph->Paint(
330             mxCanvas,
331             maSize,
332             mpFont,
333             aViewState,
334             aRenderState,
335             mnTopOffset,
336             nClipTop,
337             nClipBottom);
338     }
339 
340     aRenderState.AffineTransform.m02 = 0;
341     aRenderState.AffineTransform.m12 = 0;
342 
343 #ifdef SHOW_CHARACTER_BOXES
344     PresenterCanvasHelper::SetDeviceColor(aRenderState, 0x00808080);
345     for (sal_Int32 nParagraphIndex(0), nParagraphCount(GetParagraphCount());
346          nParagraphIndex<nParagraphCount;
347          ++nParagraphIndex)
348     {
349         const SharedPresenterTextParagraph pParagraph (GetParagraph(nParagraphIndex));
350         if ( ! pParagraph)
351             continue;
352         for (sal_Int32 nCharacterIndex(0),nCharacterCount(pParagraph->GetCharacterCount());
353              nCharacterIndex<nCharacterCount; ++nCharacterIndex)
354         {
355             const awt::Rectangle aBox (pParagraph->GetCharacterBounds(nCharacterIndex, false));
356             mxCanvas->drawPolyPolygon (
357                 PresenterGeometryHelper::CreatePolygon(
358                     aBox,
359                     mxCanvas->getDevice()),
360                 aViewState,
361                 aRenderState);
362         }
363     }
364     PresenterCanvasHelper::SetDeviceColor(aRenderState, mpFont->mnColor);
365 #endif
366 
367     if (mpCaret && mpCaret->IsVisible())
368     {
369         mxCanvas->fillPolyPolygon (
370             PresenterGeometryHelper::CreatePolygon(
371                 mpCaret->GetBounds(),
372                 mxCanvas->getDevice()),
373             aViewState,
374             aRenderState);
375     }
376 }
377 
GetCaret() const378 const SharedPresenterTextCaret& PresenterTextView::GetCaret() const
379 {
380     return mpCaret;
381 }
382 
GetCaretBounds(sal_Int32 nParagraphIndex,const sal_Int32 nCharacterIndex) const383 awt::Rectangle PresenterTextView::GetCaretBounds (
384     sal_Int32 nParagraphIndex,
385     const sal_Int32 nCharacterIndex) const
386 {
387     SharedPresenterTextParagraph pParagraph (GetParagraph(nParagraphIndex));
388 
389     if (pParagraph)
390         return pParagraph->GetCharacterBounds(nCharacterIndex, true);
391     else
392         return awt::Rectangle(0,0,0,0);
393 }
394 
395 //----- private ---------------------------------------------------------------
396 
RequestFormat()397 void PresenterTextView::RequestFormat()
398 {
399     mbIsFormatPending = true;
400 }
401 
Format()402 void PresenterTextView::Format()
403 {
404     mbIsFormatPending = false;
405 
406     double nY (0);
407     for (const auto& rxParagraph : maParagraphs)
408     {
409         rxParagraph->Format(nY, maSize.Width, mpFont);
410         nY += rxParagraph->GetTotalTextHeight();
411     }
412 
413     if (maTextChangeBroadcaster)
414         maTextChangeBroadcaster();
415 }
416 
GetParagraphCount() const417 sal_Int32 PresenterTextView::GetParagraphCount() const
418 {
419     return maParagraphs.size();
420 }
421 
GetParagraph(const sal_Int32 nParagraphIndex) const422 SharedPresenterTextParagraph PresenterTextView::GetParagraph (
423     const sal_Int32 nParagraphIndex) const
424 {
425     if (nParagraphIndex < 0)
426         return SharedPresenterTextParagraph();
427     else if (nParagraphIndex>=sal_Int32(maParagraphs.size()))
428         return SharedPresenterTextParagraph();
429     else
430         return maParagraphs[nParagraphIndex];
431 }
432 
433 //===== PresenterTextParagraph ================================================
434 
PresenterTextParagraph(const sal_Int32 nParagraphIndex,const Reference<i18n::XBreakIterator> & rxBreakIterator,const Reference<i18n::XScriptTypeDetector> & rxScriptTypeDetector,const Reference<text::XTextRange> & rxTextRange,const SharedPresenterTextCaret & rpCaret)435 PresenterTextParagraph::PresenterTextParagraph (
436     const sal_Int32 nParagraphIndex,
437     const Reference<i18n::XBreakIterator>& rxBreakIterator,
438     const Reference<i18n::XScriptTypeDetector>& rxScriptTypeDetector,
439     const Reference<text::XTextRange>& rxTextRange,
440     const SharedPresenterTextCaret& rpCaret)
441     : msParagraphText(),
442       mnParagraphIndex(nParagraphIndex),
443       mpCaret(rpCaret),
444       mxBreakIterator(rxBreakIterator),
445       mxScriptTypeDetector(rxScriptTypeDetector),
446       maLines(),
447       mnVerticalOffset(0),
448       mnXOrigin(0),
449       mnYOrigin(0),
450       mnWidth(0),
451       mnAscent(0),
452       mnDescent(0),
453       mnLineHeight(-1),
454       mnWritingMode (text::WritingMode2::LR_TB),
455       mnCharacterOffset(0),
456       maCells()
457 {
458     if (!rxTextRange.is())
459         return;
460 
461     Reference<beans::XPropertySet> xProperties (rxTextRange, UNO_QUERY);
462     try
463     {
464         xProperties->getPropertyValue("WritingMode") >>= mnWritingMode;
465     }
466     catch(beans::UnknownPropertyException&)
467     {
468         // Ignore the exception.  Use the default value.
469     }
470 
471     msParagraphText = rxTextRange->getString();
472 }
473 
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)474 void PresenterTextParagraph::Paint (
475     const Reference<rendering::XCanvas>& rxCanvas,
476     const geometry::RealSize2D& rSize,
477     const PresenterTheme::SharedFontDescriptor& rpFont,
478     const rendering::ViewState& rViewState,
479     rendering::RenderState& rRenderState,
480     const double nTopOffset,
481     const double nClipTop,
482     const double nClipBottom)
483 {
484     if (mnLineHeight <= 0)
485         return;
486 
487     sal_Int8 nTextDirection (GetTextDirection());
488 
489     const double nSavedM12 (rRenderState.AffineTransform.m12);
490 
491     if ( ! IsTextReferencePointLeft())
492         rRenderState.AffineTransform.m02 += rSize.Width;
493 
494 #ifdef SHOW_CHARACTER_BOXES
495     for (sal_Int32 nIndex=0,nCount=maLines.size();
496          nIndex<nCount;
497          ++nIndex)
498     {
499         Line& rLine (maLines[nIndex]);
500         rLine.ProvideLayoutedLine(msParagraphText, rpFont, nTextDirection);
501     }
502 #endif
503 
504     for (sal_Int32 nIndex=0,nCount=maLines.size();
505          nIndex<nCount;
506          ++nIndex, rRenderState.AffineTransform.m12 += mnLineHeight)
507     {
508         Line& rLine (maLines[nIndex]);
509 
510         // Paint only visible lines.
511         const double nLineTop = rLine.mnBaseLine - mnAscent - nTopOffset;
512         if (nLineTop + mnLineHeight< nClipTop)
513             continue;
514         else if (nLineTop > nClipBottom)
515             break;
516         rLine.ProvideLayoutedLine(msParagraphText, rpFont, nTextDirection);
517 
518         rRenderState.AffineTransform.m12 = nSavedM12 + rLine.mnBaseLine;
519 
520         rxCanvas->drawTextLayout (
521             rLine.mxLayoutedLine,
522             rViewState,
523             rRenderState);
524     }
525     rRenderState.AffineTransform.m12 = nSavedM12;
526 
527     if ( ! IsTextReferencePointLeft())
528         rRenderState.AffineTransform.m02 -= rSize.Width;
529 }
530 
Format(const double nY,const double nWidth,const PresenterTheme::SharedFontDescriptor & rpFont)531 void PresenterTextParagraph::Format (
532     const double nY,
533     const double nWidth,
534     const PresenterTheme::SharedFontDescriptor& rpFont)
535 {
536     // Make sure that the text view is in a valid and sane state.
537     if ( ! mxBreakIterator.is() || ! mxScriptTypeDetector.is())
538         return;
539     if (nWidth<=0)
540         return;
541     if ( ! rpFont || ! rpFont->mxFont.is())
542         return;
543 
544     sal_Int32 nPosition (0);
545 
546     mnWidth = nWidth;
547     maLines.clear();
548     mnLineHeight = 0;
549     mnAscent = 0;
550     mnDescent = 0;
551     mnVerticalOffset = nY;
552     maWordBoundaries.clear();
553     maWordBoundaries.push_back(0);
554 
555     const rendering::FontMetrics aMetrics (rpFont->mxFont->getFontMetrics());
556     mnAscent = aMetrics.Ascent;
557     mnDescent = aMetrics.Descent;
558     mnLineHeight = aMetrics.Ascent + aMetrics.Descent + aMetrics.ExternalLeading;
559     nPosition = 0;
560     i18n::Boundary aCurrentLine(0,0);
561     while (true)
562     {
563         const i18n::Boundary aWordBoundary = mxBreakIterator->nextWord(
564             msParagraphText,
565             nPosition,
566             lang::Locale(),
567             i18n::WordType::ANYWORD_IGNOREWHITESPACES);
568         AddWord(nWidth, aCurrentLine, aWordBoundary.startPos, rpFont);
569 
570         // Remember the new word boundary for caret travelling by words.
571         // Prevent duplicates.
572         if (aWordBoundary.startPos > maWordBoundaries.back())
573             maWordBoundaries.push_back(aWordBoundary.startPos);
574 
575         if (aWordBoundary.endPos>aWordBoundary.startPos)
576             AddWord(nWidth, aCurrentLine, aWordBoundary.endPos, rpFont);
577 
578         if (aWordBoundary.startPos<0 || aWordBoundary.endPos<0)
579             break;
580         if (nPosition >= aWordBoundary.endPos)
581             break;
582         nPosition = aWordBoundary.endPos;
583     }
584 
585     if (aCurrentLine.endPos>aCurrentLine.startPos)
586         AddLine(aCurrentLine);
587 
588 }
589 
GetWordBoundary(const sal_Int32 nLocalCharacterIndex,const sal_Int32 nDistance)590 sal_Int32 PresenterTextParagraph::GetWordBoundary(
591     const sal_Int32 nLocalCharacterIndex,
592     const sal_Int32 nDistance)
593 {
594     OSL_ASSERT(nDistance==-1 || nDistance==+1);
595 
596     if (nLocalCharacterIndex < 0)
597     {
598         // The caller asked for the start or end position of the paragraph.
599         if (nDistance < 0)
600             return 0;
601         else
602             return GetCharacterCount();
603     }
604 
605     sal_Int32 nIndex (0);
606     for (sal_Int32 nCount (maWordBoundaries.size()); nIndex<nCount; ++nIndex)
607     {
608         if (maWordBoundaries[nIndex] >= nLocalCharacterIndex)
609         {
610             // When inside the word (not at its start or end) then
611             // first move to the start or end before going the previous or
612             // next word.
613             if (maWordBoundaries[nIndex] > nLocalCharacterIndex)
614                 if (nDistance > 0)
615                     --nIndex;
616             break;
617         }
618     }
619 
620     nIndex += nDistance;
621 
622     if (nIndex < 0)
623         return -1;
624     else if (o3tl::make_unsigned(nIndex)>=maWordBoundaries.size())
625         return -1;
626     else
627         return maWordBoundaries[nIndex];
628 }
629 
GetCaretPosition() const630 sal_Int32 PresenterTextParagraph::GetCaretPosition() const
631 {
632     if (mpCaret && mpCaret->GetParagraphIndex()==mnParagraphIndex)
633         return mpCaret->GetCharacterIndex();
634     else
635         return -1;
636 }
637 
SetCaretPosition(const sal_Int32 nPosition) const638 void PresenterTextParagraph::SetCaretPosition (const sal_Int32 nPosition) const
639 {
640     if (mpCaret && mpCaret->GetParagraphIndex()==mnParagraphIndex)
641         return mpCaret->SetPosition(mnParagraphIndex, nPosition);
642 }
643 
SetOrigin(const double nXOrigin,const double nYOrigin)644 void PresenterTextParagraph::SetOrigin (const double nXOrigin, const double nYOrigin)
645 {
646     mnXOrigin = nXOrigin;
647     mnYOrigin = nYOrigin;
648 }
649 
GetRelativeLocation() const650 awt::Point PresenterTextParagraph::GetRelativeLocation() const
651 {
652     return awt::Point(
653         sal_Int32(mnXOrigin),
654         sal_Int32(mnYOrigin + mnVerticalOffset));
655 }
656 
GetSize() const657 awt::Size PresenterTextParagraph::GetSize() const
658 {
659     return awt::Size(
660         sal_Int32(mnWidth),
661         sal_Int32(GetTotalTextHeight()));
662 }
663 
AddWord(const double nWidth,i18n::Boundary & rCurrentLine,const sal_Int32 nWordBoundary,const PresenterTheme::SharedFontDescriptor & rpFont)664 void PresenterTextParagraph::AddWord (
665     const double nWidth,
666     i18n::Boundary& rCurrentLine,
667     const sal_Int32 nWordBoundary,
668     const PresenterTheme::SharedFontDescriptor& rpFont)
669 {
670     sal_Int32 nLineStart (0);
671     if ( ! maLines.empty())
672         nLineStart = rCurrentLine.startPos;
673 
674     const OUString sLineCandidate (
675         msParagraphText.copy(nLineStart, nWordBoundary-nLineStart));
676 
677     css::geometry::RealRectangle2D aLineBox (
678         PresenterCanvasHelper::GetTextBoundingBox (
679             rpFont->mxFont,
680             sLineCandidate,
681             mnWritingMode));
682     const double nLineWidth (aLineBox.X2 - aLineBox.X1);
683 
684     if (nLineWidth >= nWidth)
685     {
686         // Add new line with a single word (so far).
687         AddLine(rCurrentLine);
688     }
689     rCurrentLine.endPos = nWordBoundary;
690 }
691 
AddLine(i18n::Boundary & rCurrentLine)692 void PresenterTextParagraph::AddLine (
693     i18n::Boundary& rCurrentLine)
694 {
695     Line aLine (rCurrentLine.startPos, rCurrentLine.endPos);
696 
697     // Find the start and end of the line with respect to cells.
698     if (!maLines.empty())
699     {
700         aLine.mnLineStartCellIndex = maLines.back().mnLineEndCellIndex;
701         aLine.mnBaseLine = maLines.back().mnBaseLine + mnLineHeight;
702     }
703     else
704     {
705         aLine.mnLineStartCellIndex = 0;
706         aLine.mnBaseLine = mnVerticalOffset + mnAscent;
707     }
708     sal_Int32 nCellIndex (aLine.mnLineStartCellIndex);
709     double nWidth (0);
710     for ( ; nCellIndex<sal_Int32(maCells.size()); ++nCellIndex)
711     {
712         const Cell& rCell (maCells[nCellIndex]);
713         if (rCell.mnCharacterIndex+rCell.mnCharacterCount > aLine.mnLineEndCharacterIndex)
714             break;
715         nWidth += rCell.mnCellWidth;
716     }
717     aLine.mnLineEndCellIndex = nCellIndex;
718     aLine.mnWidth = nWidth;
719 
720     maLines.push_back(aLine);
721 
722     rCurrentLine.startPos = rCurrentLine.endPos;
723 }
724 
GetTotalTextHeight() const725 double PresenterTextParagraph::GetTotalTextHeight() const
726 {
727     return maLines.size() * mnLineHeight;
728 }
729 
SetCharacterOffset(const sal_Int32 nCharacterOffset)730 void PresenterTextParagraph::SetCharacterOffset (const sal_Int32 nCharacterOffset)
731 {
732     mnCharacterOffset = nCharacterOffset;
733 }
734 
GetCharacterCount() const735 sal_Int32 PresenterTextParagraph::GetCharacterCount() const
736 {
737     return msParagraphText.getLength();
738 }
739 
GetCharacter(const sal_Int32 nGlobalCharacterIndex) const740 sal_Unicode PresenterTextParagraph::GetCharacter (
741     const sal_Int32 nGlobalCharacterIndex) const
742 {
743     if (nGlobalCharacterIndex<mnCharacterOffset
744         || nGlobalCharacterIndex>=mnCharacterOffset+msParagraphText.getLength())
745     {
746         return sal_Unicode();
747     }
748     else
749     {
750         return msParagraphText[nGlobalCharacterIndex - mnCharacterOffset];
751     }
752 }
753 
GetText() const754 const OUString& PresenterTextParagraph::GetText() const
755 {
756     return msParagraphText;
757 }
758 
GetTextSegment(const sal_Int32 nOffset,const sal_Int32 nIndex,const sal_Int16 nTextType) const759 TextSegment PresenterTextParagraph::GetTextSegment (
760     const sal_Int32 nOffset,
761     const sal_Int32 nIndex,
762     const sal_Int16 nTextType) const
763 {
764     switch(nTextType)
765     {
766         case AccessibleTextType::PARAGRAPH:
767             return TextSegment(
768                 msParagraphText,
769                 mnCharacterOffset,
770                 mnCharacterOffset+msParagraphText.getLength());
771 
772         case AccessibleTextType::SENTENCE:
773             if (mxBreakIterator.is())
774             {
775                 const sal_Int32 nStart (mxBreakIterator->beginOfSentence(
776                     msParagraphText, nIndex-mnCharacterOffset, lang::Locale()));
777                 const sal_Int32 nEnd (mxBreakIterator->endOfSentence(
778                     msParagraphText, nIndex-mnCharacterOffset, lang::Locale()));
779                 if (nStart < nEnd)
780                     return TextSegment(
781                         msParagraphText.copy(nStart, nEnd-nStart),
782                         nStart+mnCharacterOffset,
783                         nEnd+mnCharacterOffset);
784             }
785             break;
786 
787         case AccessibleTextType::WORD:
788             if (mxBreakIterator.is())
789                 return GetWordTextSegment(nOffset, nIndex);
790             break;
791 
792         case AccessibleTextType::LINE:
793         {
794             auto iLine = std::find_if(maLines.begin(), maLines.end(),
795                 [nIndex](const Line& rLine) { return nIndex < rLine.mnLineEndCharacterIndex; });
796             if (iLine != maLines.end())
797             {
798                 return TextSegment(
799                     msParagraphText.copy(
800                         iLine->mnLineStartCharacterIndex,
801                         iLine->mnLineEndCharacterIndex - iLine->mnLineStartCharacterIndex),
802                     iLine->mnLineStartCharacterIndex,
803                     iLine->mnLineEndCharacterIndex);
804             }
805         }
806         break;
807 
808         // Handle GLYPH and ATTRIBUTE_RUN like CHARACTER because we can not
809         // do better at the moment.
810         case AccessibleTextType::CHARACTER:
811         case AccessibleTextType::GLYPH:
812         case AccessibleTextType::ATTRIBUTE_RUN:
813             return CreateTextSegment(nIndex+nOffset, nIndex+nOffset+1);
814     }
815 
816     return TextSegment(OUString(), 0,0);
817 }
818 
GetWordTextSegment(const sal_Int32 nOffset,const sal_Int32 nIndex) const819 TextSegment PresenterTextParagraph::GetWordTextSegment (
820     const sal_Int32 nOffset,
821     const sal_Int32 nIndex) const
822 {
823     sal_Int32 nCurrentOffset (nOffset);
824     sal_Int32 nCurrentIndex (nIndex);
825 
826     i18n::Boundary aWordBoundary;
827     if (nCurrentOffset == 0)
828         aWordBoundary = mxBreakIterator->getWordBoundary(
829             msParagraphText,
830             nIndex,
831             lang::Locale(),
832             i18n::WordType::ANYWORD_IGNOREWHITESPACES,
833             true);
834     else if (nCurrentOffset < 0)
835     {
836         while (nCurrentOffset<0 && nCurrentIndex>0)
837         {
838             aWordBoundary = mxBreakIterator->previousWord(
839                 msParagraphText,
840                 nCurrentIndex,
841                 lang::Locale(),
842                 i18n::WordType::ANYWORD_IGNOREWHITESPACES);
843             nCurrentIndex = aWordBoundary.startPos;
844             ++nCurrentOffset;
845         }
846     }
847     else
848     {
849         while (nCurrentOffset>0 && nCurrentIndex<=GetCharacterCount())
850         {
851             aWordBoundary = mxBreakIterator->nextWord(
852                 msParagraphText,
853                 nCurrentIndex,
854                 lang::Locale(),
855                 i18n::WordType::ANYWORD_IGNOREWHITESPACES);
856             nCurrentIndex = aWordBoundary.endPos;
857             --nCurrentOffset;
858         }
859     }
860 
861     return CreateTextSegment(aWordBoundary.startPos, aWordBoundary.endPos);
862 }
863 
CreateTextSegment(sal_Int32 nStartIndex,sal_Int32 nEndIndex) const864 TextSegment PresenterTextParagraph::CreateTextSegment (
865     sal_Int32 nStartIndex,
866     sal_Int32 nEndIndex) const
867 {
868     if (nEndIndex <= nStartIndex)
869         return TextSegment(
870             OUString(),
871             nStartIndex,
872             nEndIndex);
873     else
874         return TextSegment(
875             msParagraphText.copy(nStartIndex, nEndIndex-nStartIndex),
876             nStartIndex,
877             nEndIndex);
878 }
879 
GetCharacterBounds(sal_Int32 nGlobalCharacterIndex,const bool bCaretBox)880 awt::Rectangle PresenterTextParagraph::GetCharacterBounds (
881     sal_Int32 nGlobalCharacterIndex,
882     const bool bCaretBox)
883 {
884     // Find the line that contains the requested character and accumulate
885     // the previous line heights.
886     double nX (mnXOrigin);
887     double nY (mnYOrigin + mnVerticalOffset + mnAscent);
888     const sal_Int8 nTextDirection (GetTextDirection());
889     for (sal_Int32 nLineIndex=0,nLineCount=maLines.size();
890          nLineIndex<nLineCount;
891          ++nLineIndex, nY+=mnLineHeight)
892     {
893         Line& rLine (maLines[nLineIndex]);
894         // Skip lines before the indexed character.
895         if (nGlobalCharacterIndex >= rLine.mnLineEndCharacterIndex)
896             // When in the last line then allow the index past the last char.
897             if (nLineIndex<nLineCount-1)
898                 continue;
899 
900         rLine.ProvideCellBoxes();
901 
902         const sal_Int32 nCellIndex (nGlobalCharacterIndex - rLine.mnLineStartCharacterIndex);
903 
904         // The cell bounding box is defined relative to the origin of
905         // the current line.  Therefore we have to add the absolute
906         // position of the line.
907         geometry::RealRectangle2D rCellBox (rLine.maCellBoxes[
908             ::std::min(nCellIndex, rLine.maCellBoxes.getLength()-1)]);
909 
910         double nLeft = nX + rCellBox.X1;
911         double nRight = nX + rCellBox.X2;
912         if (nTextDirection == rendering::TextDirection::WEAK_RIGHT_TO_LEFT)
913         {
914             const double nOldRight (nRight);
915             nRight = rLine.mnWidth - nLeft;
916             nLeft = rLine.mnWidth - nOldRight;
917         }
918         double nTop = nY - mnAscent;
919         double nBottom;
920         if (bCaretBox)
921         {
922             nBottom = nTop + mnLineHeight;
923             if (nCellIndex >= rLine.maCellBoxes.getLength())
924                 nLeft = nRight-2;
925             if (nLeft < nX)
926                 nLeft = nX;
927             nRight = nLeft+2;
928         }
929         else
930         {
931             nBottom = nTop + mnAscent + mnDescent;
932         }
933         const sal_Int32 nX1 = sal_Int32(floor(nLeft));
934         const sal_Int32 nY1 = sal_Int32(floor(nTop));
935         const sal_Int32 nX2 = sal_Int32(ceil(nRight));
936         const sal_Int32 nY2 = sal_Int32(ceil(nBottom));
937 
938         return awt::Rectangle(nX1,nY1,nX2-nX1+1,nY2-nY1+1);
939     }
940 
941     // We are still here.  That means that the given index lies past the
942     // last character in the paragraph.
943     // Return an empty box that lies past the last character.  Better than nothing.
944     return awt::Rectangle(sal_Int32(nX+0.5), sal_Int32(nY+0.5), 0, 0);
945 }
946 
GetTextDirection() const947 sal_Int8 PresenterTextParagraph::GetTextDirection() const
948 {
949     // Find first portion that has a non-neutral text direction.
950     sal_Int32 nPosition (0);
951     sal_Int32 nTextLength (msParagraphText.getLength());
952     while (nPosition < nTextLength)
953     {
954         const sal_Int16 nScriptDirection (
955             mxScriptTypeDetector->getScriptDirection(
956                 msParagraphText, nPosition, i18n::ScriptDirection::NEUTRAL));
957         switch (nScriptDirection)
958         {
959             case i18n::ScriptDirection::NEUTRAL:
960                 // continue looping.
961                 break;
962             case i18n::ScriptDirection::LEFT_TO_RIGHT:
963                 return rendering::TextDirection::WEAK_LEFT_TO_RIGHT;
964 
965             case i18n::ScriptDirection::RIGHT_TO_LEFT:
966                 return rendering::TextDirection::WEAK_RIGHT_TO_LEFT;
967         }
968 
969         nPosition = mxScriptTypeDetector->endOfScriptDirection(
970             msParagraphText, nPosition, nScriptDirection);
971     }
972 
973     // All text in paragraph is neutral.  Fall back on writing mode taken
974     // from the XText (which may not be properly initialized.)
975     sal_Int8 nTextDirection(rendering::TextDirection::WEAK_LEFT_TO_RIGHT);
976     switch(mnWritingMode)
977     {
978         case text::WritingMode2::LR_TB:
979             nTextDirection = rendering::TextDirection::WEAK_LEFT_TO_RIGHT;
980             break;
981 
982         case text::WritingMode2::RL_TB:
983             nTextDirection = rendering::TextDirection::WEAK_RIGHT_TO_LEFT;
984             break;
985 
986         default:
987         case text::WritingMode2::TB_RL:
988         case text::WritingMode2::TB_LR:
989             // Can not handle this.  Use default and hope for the best.
990             break;
991     }
992     return nTextDirection;
993 }
994 
IsTextReferencePointLeft() const995 bool PresenterTextParagraph::IsTextReferencePointLeft() const
996 {
997     return mnWritingMode != text::WritingMode2::RL_TB;
998 }
999 
SetupCellArray(const PresenterTheme::SharedFontDescriptor & rpFont)1000 void PresenterTextParagraph::SetupCellArray (
1001     const PresenterTheme::SharedFontDescriptor& rpFont)
1002 {
1003     maCells.clear();
1004 
1005     if ( ! rpFont || ! rpFont->mxFont.is())
1006         return;
1007 
1008     sal_Int32 nPosition (0);
1009     sal_Int32 nIndex (0);
1010     const sal_Int32 nTextLength (msParagraphText.getLength());
1011     const sal_Int8 nTextDirection (GetTextDirection());
1012     while (nPosition < nTextLength)
1013     {
1014         const sal_Int32 nNewPosition (mxBreakIterator->nextCharacters(
1015             msParagraphText,
1016             nPosition,
1017             lang::Locale(),
1018             i18n::CharacterIteratorMode::SKIPCELL,
1019             1,
1020             nIndex));
1021 
1022         rendering::StringContext aContext (msParagraphText, nPosition, nNewPosition-nPosition);
1023         Reference<rendering::XTextLayout> xLayout (
1024             rpFont->mxFont->createTextLayout(aContext, nTextDirection, 0));
1025         css::geometry::RealRectangle2D aCharacterBox (xLayout->queryTextBounds());
1026 
1027         maCells.emplace_back(
1028             nPosition,
1029             nNewPosition-nPosition,
1030             aCharacterBox.X2-aCharacterBox.X1);
1031 
1032         nPosition = nNewPosition;
1033     }
1034 }
1035 
1036 //===== PresenterTextCaret ================================================----
1037 
PresenterTextCaret(uno::Reference<uno::XComponentContext> const & xContext,const::std::function<css::awt::Rectangle (const sal_Int32,const sal_Int32)> & rCharacterBoundsAccess,const::std::function<void (const css::awt::Rectangle &)> & rInvalidator)1038 PresenterTextCaret::PresenterTextCaret (
1039         uno::Reference<uno::XComponentContext> const& xContext,
1040     const ::std::function<css::awt::Rectangle (const sal_Int32,const sal_Int32)>& rCharacterBoundsAccess,
1041     const ::std::function<void (const css::awt::Rectangle&)>& rInvalidator)
1042     : m_xContext(xContext)
1043     , mnParagraphIndex(-1),
1044       mnCharacterIndex(-1),
1045       mnCaretBlinkTaskId(0),
1046       mbIsCaretVisible(false),
1047       maCharacterBoundsAccess(rCharacterBoundsAccess),
1048       maInvalidator(rInvalidator),
1049       maBroadcaster(),
1050       maCaretBounds()
1051 {
1052 }
1053 
~PresenterTextCaret()1054 PresenterTextCaret::~PresenterTextCaret()
1055 {
1056     try
1057     {
1058         HideCaret();
1059     }
1060     catch (uno::Exception const&)
1061     {
1062         TOOLS_WARN_EXCEPTION("sdext.presenter", "unexpected exception in ~PresenterTextCaret");
1063     }
1064 }
1065 
ShowCaret()1066 void PresenterTextCaret::ShowCaret()
1067 {
1068     if (mnCaretBlinkTaskId == 0)
1069     {
1070         mnCaretBlinkTaskId = PresenterTimer::ScheduleRepeatedTask (
1071             m_xContext,
1072             [this] (TimeValue const&) { return this->InvertCaret(); },
1073             CaretBlinkInterval,
1074             CaretBlinkInterval);
1075     }
1076     mbIsCaretVisible = true;
1077 }
1078 
HideCaret()1079 void PresenterTextCaret::HideCaret()
1080 {
1081     if (mnCaretBlinkTaskId != 0)
1082     {
1083         PresenterTimer::CancelTask(mnCaretBlinkTaskId);
1084         mnCaretBlinkTaskId = 0;
1085     }
1086     mbIsCaretVisible = false;
1087     // Reset the caret position.
1088     mnParagraphIndex = -1;
1089     mnCharacterIndex = -1;
1090 }
1091 
1092 
SetPosition(const sal_Int32 nParagraphIndex,const sal_Int32 nCharacterIndex)1093 void PresenterTextCaret::SetPosition (
1094     const sal_Int32 nParagraphIndex,
1095     const sal_Int32 nCharacterIndex)
1096 {
1097     if (mnParagraphIndex == nParagraphIndex
1098         && mnCharacterIndex == nCharacterIndex)
1099         return;
1100 
1101     if (mnParagraphIndex >= 0)
1102         maInvalidator(maCaretBounds);
1103 
1104     const sal_Int32 nOldParagraphIndex (mnParagraphIndex);
1105     const sal_Int32 nOldCharacterIndex (mnCharacterIndex);
1106     mnParagraphIndex = nParagraphIndex;
1107     mnCharacterIndex = nCharacterIndex;
1108     maCaretBounds = maCharacterBoundsAccess(mnParagraphIndex, mnCharacterIndex);
1109     if (mnParagraphIndex >= 0)
1110         ShowCaret();
1111     else
1112         HideCaret();
1113 
1114     if (mnParagraphIndex >= 0)
1115         maInvalidator(maCaretBounds);
1116 
1117     if (maBroadcaster)
1118         maBroadcaster(
1119             nOldParagraphIndex,
1120             nOldCharacterIndex,
1121             mnParagraphIndex,
1122             mnCharacterIndex);
1123 }
1124 
1125 
SetCaretMotionBroadcaster(const::std::function<void (sal_Int32,sal_Int32,sal_Int32,sal_Int32)> & rBroadcaster)1126 void PresenterTextCaret::SetCaretMotionBroadcaster (
1127     const ::std::function<void (sal_Int32,sal_Int32,sal_Int32,sal_Int32)>& rBroadcaster)
1128 {
1129     maBroadcaster = rBroadcaster;
1130 }
1131 
GetBounds() const1132 const css::awt::Rectangle& PresenterTextCaret::GetBounds() const
1133 {
1134     return maCaretBounds;
1135 }
1136 
InvertCaret()1137 void PresenterTextCaret::InvertCaret()
1138 {
1139     mbIsCaretVisible = !mbIsCaretVisible;
1140     if (mnParagraphIndex >= 0)
1141         maInvalidator(maCaretBounds);
1142 }
1143 
1144 //===== PresenterTextParagraph::Cell ==========================================
1145 
Cell(const sal_Int32 nCharacterIndex,const sal_Int32 nCharacterCount,const double nCellWidth)1146 PresenterTextParagraph::Cell::Cell (
1147     const sal_Int32 nCharacterIndex,
1148     const sal_Int32 nCharacterCount,
1149     const double nCellWidth)
1150     : mnCharacterIndex(nCharacterIndex),
1151       mnCharacterCount(nCharacterCount),
1152       mnCellWidth(nCellWidth)
1153 {
1154 }
1155 
1156 //===== PresenterTextParagraph::Line ==========================================
1157 
Line(const sal_Int32 nLineStartCharacterIndex,const sal_Int32 nLineEndCharacterIndex)1158 PresenterTextParagraph::Line::Line (
1159     const sal_Int32 nLineStartCharacterIndex,
1160     const sal_Int32 nLineEndCharacterIndex)
1161     : mnLineStartCharacterIndex(nLineStartCharacterIndex),
1162       mnLineEndCharacterIndex(nLineEndCharacterIndex),
1163       mnLineStartCellIndex(-1), mnLineEndCellIndex(-1),
1164       mxLayoutedLine(),
1165       mnBaseLine(0), mnWidth(0),
1166       maCellBoxes()
1167 {
1168 }
1169 
ProvideCellBoxes()1170 void PresenterTextParagraph::Line::ProvideCellBoxes()
1171 {
1172     if ( mnLineStartCharacterIndex < mnLineEndCharacterIndex && !maCellBoxes.hasElements() )
1173     {
1174         if (mxLayoutedLine.is())
1175             maCellBoxes = mxLayoutedLine->queryInkMeasures();
1176         else
1177         {
1178             OSL_ASSERT(mxLayoutedLine.is());
1179         }
1180     }
1181 }
1182 
ProvideLayoutedLine(const OUString & rsParagraphText,const PresenterTheme::SharedFontDescriptor & rpFont,const sal_Int8 nTextDirection)1183 void PresenterTextParagraph::Line::ProvideLayoutedLine (
1184     const OUString& rsParagraphText,
1185     const PresenterTheme::SharedFontDescriptor& rpFont,
1186     const sal_Int8 nTextDirection)
1187 {
1188     if ( ! mxLayoutedLine.is())
1189     {
1190         const rendering::StringContext aContext (
1191             rsParagraphText,
1192             mnLineStartCharacterIndex,
1193             mnLineEndCharacterIndex - mnLineStartCharacterIndex);
1194 
1195         mxLayoutedLine = rpFont->mxFont->createTextLayout(
1196             aContext,
1197             nTextDirection,
1198             0);
1199     }
1200 }
1201 
1202 } // end of namespace ::sdext::presenter
1203 
1204 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
1205