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