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