MythTV  0.27pre
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Groups Pages
mythuitext.cpp
Go to the documentation of this file.
1 
2 #include "mythuitext.h"
3 
4 #include <QCoreApplication>
5 #include <QtGlobal>
6 #include <QDomDocument>
7 #include <QFontMetrics>
8 #include <QString>
9 #include <QHash>
10 
11 #include "mythlogging.h"
12 
13 #include "mythuihelper.h"
14 #include "mythpainter.h"
15 #include "mythmainwindow.h"
16 #include "mythfontproperties.h"
17 #include "mythcorecontext.h"
18 
19 #include "compat.h"
20 
21 MythUIText::MythUIText(MythUIType *parent, const QString &name)
22  : MythUIType(parent, name),
23  m_Justification(Qt::AlignLeft | Qt::AlignTop), m_OrigDisplayRect(),
24  m_AltDisplayRect(), m_Canvas(),
25  m_drawRect(), m_cursorPos(-1, -1),
26  m_Message(""), m_CutMessage(""),
27  m_DefaultMessage(""), m_TemplateText(""),
28  m_ShrinkNarrow(true), m_Cutdown(Qt::ElideRight),
29  m_MultiLine(false), m_Ascent(0),
30  m_Descent(0), m_leftBearing(0),
31  m_rightBearing(0), m_Leading(1),
32  m_extraLeading(0), m_lineHeight(0),
33  m_textCursor(-1),
34  m_Font(new MythFontProperties()), m_colorCycling(false),
35  m_startColor(), m_endColor(),
36  m_numSteps(0), m_curStep(0),
37  curR(0.0), curG(0.0), curB(0.0),
38  incR(0.0), incG(0.0), incB(0.0),
39  m_scrollStartDelay(ScrollBounceDelay),
40  m_scrollReturnDelay(ScrollBounceDelay), m_scrollPause(0),
41  m_scrollForwardRate(70.0 / MythMainWindow::drawRefresh),
42  m_scrollReturnRate(70.0 / MythMainWindow::drawRefresh),
43  m_scrollBounce(false), m_scrollOffset(0),
44  m_scrollPos(0), m_scrollPosWhole(0),
45  m_scrollDirection(ScrollNone), m_scrolling(false),
46  m_textCase(CaseNormal)
47 {
48 #if 0 // Not currently used
49  m_usingAltArea = false;
50 #endif
51  m_EnableInitiator = true;
52 
53  m_FontStates.insert("default", MythFontProperties());
54  *m_Font = m_FontStates["default"];
55 }
56 
57 MythUIText::MythUIText(const QString &text, const MythFontProperties &font,
58  QRect displayRect, QRect altDisplayRect,
59  MythUIType *parent, const QString &name)
60  : MythUIType(parent, name),
61  m_Justification(Qt::AlignLeft | Qt::AlignTop),
62  m_OrigDisplayRect(displayRect), m_AltDisplayRect(altDisplayRect),
63  m_Canvas(0, 0, displayRect.width(), displayRect.height()),
64  m_drawRect(displayRect), m_cursorPos(-1, -1),
65  m_Message(text.trimmed()),
66  m_CutMessage(""), m_DefaultMessage(text),
67  m_Cutdown(Qt::ElideRight), m_Font(new MythFontProperties()),
68  m_colorCycling(false), m_startColor(),
69  m_endColor(), m_numSteps(0),
70  m_curStep(0),
71  curR(0.0), curG(0.0), curB(0.0),
72  incR(0.0), incG(0.0), incB(0.0)
73 {
74 #if 0 // Not currently used
75  m_usingAltArea = false;
76 #endif
77  m_ShrinkNarrow = true;
78  m_MultiLine = false;
79 
81  m_scrollPause = 0;
84  m_scrollBounce = false;
85  m_scrollOffset = 0;
86  m_scrollPos = 0;
87  m_scrollPosWhole = 0;
89  m_scrolling = false;
90 
93  m_Leading = 1;
94  m_extraLeading = 0;
95  m_lineHeight = 0;
96  m_textCursor = -1;
97  m_EnableInitiator = true;
98 
99  SetArea(displayRect);
100  m_FontStates.insert("default", font);
101  *m_Font = m_FontStates["default"];
102 }
103 
105 {
106  delete m_Font;
107  m_Font = NULL;
108 
109  QVector<QTextLayout *>::iterator Ilayout;
110  for (Ilayout = m_Layouts.begin(); Ilayout != m_Layouts.end(); ++Ilayout)
111  delete *Ilayout;
112 }
113 
115 {
117  {
119  SetRedraw();
120  emit DependChanged(true);
121  }
122 
123  SetFontState("default");
124 
126 }
127 
128 void MythUIText::SetText(const QString &text)
129 {
130  QString newtext = text;
131  newtext.detach();
132 
133  if (!m_Layouts.isEmpty() && newtext == m_Message)
134  return;
135 
136  if (newtext.isEmpty())
137  {
139  emit DependChanged(true);
140  }
141  else
142  {
143  m_Message = newtext;
144  emit DependChanged(false);
145  }
146  m_CutMessage.clear();
147  FillCutMessage();
148 
149  SetRedraw();
150 }
151 
152 void MythUIText::SetTextFromMap(QHash<QString, QString> &map)
153 {
154  if (!IsVisible())
155  return;
156 
157  if (map.contains(objectName()))
158  {
159  QString newText = GetTemplateText();
160 
161  if (newText.isEmpty())
162  newText = GetDefaultText();
163 
164  QRegExp regexp("%(([^\\|%]+)?\\||\\|(.))?(\\w+)(\\|(.+))?%");
165  regexp.setMinimal(true);
166 
167  if (!newText.isEmpty() && newText.contains(regexp))
168  {
169  int pos = 0;
170 
171  QString translatedTemplate = qApp->translate("ThemeUI",
172  newText.toUtf8(),
173  NULL,
174  QCoreApplication::UnicodeUTF8);
175 
176  QString tempString = translatedTemplate;
177 
178  while ((pos = regexp.indexIn(translatedTemplate, pos)) != -1)
179  {
180  QString key = regexp.cap(4).toLower().trimmed();
181  QString replacement;
182 
183  if (!map.value(key).isEmpty())
184  {
185  replacement = QString("%1%2%3%4")
186  .arg(regexp.cap(2))
187  .arg(regexp.cap(3))
188  .arg(map.value(key))
189  .arg(regexp.cap(6));
190  }
191 
192  tempString.replace(regexp.cap(0), replacement);
193  pos += regexp.matchedLength();
194  }
195 
196  newText = tempString;
197  }
198  else
199  newText = map.value(objectName());
200 
201  SetText(newText);
202  }
203 }
204 
205 QString MythUIText::GetText(void) const
206 {
207  QString ret = m_Message;
208  ret.detach();
209  return ret;
210 }
211 
212 QString MythUIText::GetDefaultText(void) const
213 {
214  return m_DefaultMessage;
215 }
216 
218 {
219  m_FontStates.insert("default", fontProps);
220  if (m_Font->GetHash() != m_FontStates["default"].GetHash())
221  {
222  *m_Font = m_FontStates["default"];
223  if (!m_Message.isEmpty())
224  {
225  FillCutMessage();
226  SetRedraw();
227  }
228  }
229 }
230 
231 void MythUIText::SetFontState(const QString &state)
232 {
233  if (m_FontStates.contains(state))
234  {
235  if (m_Font->GetHash() == m_FontStates[state].GetHash())
236  return;
237  *m_Font = m_FontStates[state];
238  }
239  else
240  {
241  if (m_Font->GetHash() == m_FontStates["default"].GetHash())
242  return;
243  *m_Font = m_FontStates["default"];
244  }
245  if (!m_Message.isEmpty())
246  {
247  FillCutMessage();
248  SetRedraw();
249  }
250 }
251 
252 #if 0 // Not currently used
254 {
255  if (useAlt && m_AltDisplayRect.width() > 1)
256  {
258  m_usingAltArea = true;
259  }
260  else
261  {
263  m_usingAltArea = false;
264  }
265 
266  FillCutMessage();
267 }
268 #endif
269 
271 {
272  int h = just & Qt::AlignHorizontal_Mask;
273  int v = just & Qt::AlignVertical_Mask;
274 
275  if ((h && (m_Justification & Qt::AlignHorizontal_Mask) ^ h) ||
276  (v && (m_Justification & Qt::AlignVertical_Mask) ^ v))
277  {
278  // preserve the wordbreak attribute, drop everything else
279  m_Justification = m_Justification & Qt::TextWordWrap;
280  m_Justification |= just;
281  if (!m_Message.isEmpty())
282  {
283  FillCutMessage();
284  SetRedraw();
285  }
286  }
287 }
288 
290 {
291  return m_Justification;
292 }
293 
294 void MythUIText::SetCutDown(Qt::TextElideMode mode)
295 {
296  if (mode != m_Cutdown)
297  {
298  m_Cutdown = mode;
299  if (m_scrolling && m_Cutdown != Qt::ElideNone)
300  {
301  LOG(VB_GENERAL, LOG_ERR, QString("'%1' (%2): <scroll> and "
302  "<cutdown> are not combinable.")
303  .arg(objectName()).arg(GetXMLLocation()));
304  m_Cutdown = Qt::ElideNone;
305  }
306  if (!m_Message.isEmpty())
307  {
308  FillCutMessage();
309  SetRedraw();
310  }
311  }
312 }
313 
314 void MythUIText::SetMultiLine(bool multiline)
315 {
316  if (multiline != m_MultiLine)
317  {
318  m_MultiLine = multiline;
319 
320  if (m_MultiLine)
321  m_Justification |= Qt::TextWordWrap;
322  else
323  m_Justification &= ~Qt::TextWordWrap;
324 
325  if (!m_Message.isEmpty())
326  {
327  FillCutMessage();
328  SetRedraw();
329  }
330  }
331 }
332 
333 void MythUIText::SetArea(const MythRect &rect)
334 {
335  MythUIType::SetArea(rect);
336  m_CutMessage.clear();
337 
338  m_drawRect = m_Area;
339  FillCutMessage();
340 }
341 
343 {
346 }
347 
349 {
350  QPoint newpoint(x, y);
351 
352  if (newpoint == m_Canvas.topLeft())
353  return;
354 
355  m_Canvas.moveTopLeft(newpoint);
356  SetRedraw();
357 }
358 
360 {
361  if (x == 0 && y == 0)
362  return;
363 
364  m_Canvas.moveTop(m_Canvas.y() + y);
365  m_Canvas.moveLeft(m_Canvas.x() + x);
366  SetRedraw();
367 }
368 
369 void MythUIText::DrawSelf(MythPainter *p, int xoffset, int yoffset,
370  int alphaMod, QRect clipRect)
371 {
372  if (m_Canvas.isNull())
373  return;
374 
376  QRect drawrect = m_drawRect.toQRect();
377  drawrect.translate(xoffset, yoffset);
378  QRect canvas = m_Canvas.toQRect();
379 
380  int alpha = CalcAlpha(alphaMod);
381 
382  if (m_Ascent)
383  {
384  drawrect.setY(drawrect.y() - m_Ascent);
385  canvas.moveTop(canvas.y() + m_Ascent);
386  canvas.setHeight(canvas.height() + m_Ascent);
387  }
388  if (m_Descent)
389  {
390  drawrect.setHeight(drawrect.height() + m_Descent);
391  canvas.setHeight(canvas.height() + m_Descent);
392  }
393 
394  if (m_leftBearing)
395  {
396  drawrect.setX(drawrect.x() + m_leftBearing);
397  canvas.moveLeft(canvas.x() - m_leftBearing);
398  canvas.setWidth(canvas.width() - m_leftBearing);
399  }
400  if (m_rightBearing)
401  {
402  drawrect.setWidth(drawrect.width() - m_rightBearing);
403  canvas.setWidth(canvas.width() - m_rightBearing);
404  }
405 
406  if (GetFontProperties()->hasOutline())
407  {
408  QTextLayout::FormatRange range;
409 
410  QColor outlineColor;
411  int outlineSize, outlineAlpha;
412 
413  GetFontProperties()->GetOutline(outlineColor, outlineSize,
414  outlineAlpha);
415  outlineColor.setAlpha(outlineAlpha);
416 
417  MythPoint outline(outlineSize, outlineSize);
418  outline.NormPoint(); // scale it to screen resolution
419 
420  QPen pen;
421  pen.setBrush(outlineColor);
422  pen.setWidth(outline.x());
423 
424  range.start = 0;
425  range.length = m_CutMessage.size();
426  range.format.setTextOutline(pen);
427  formats.push_back(range);
428 
429  drawrect.setX(drawrect.x() - outline.x());
430  drawrect.setWidth(drawrect.width() + outline.x());
431  drawrect.setY(drawrect.y() - outline.y());
432  drawrect.setHeight(drawrect.height() + outline.y());
433 
434  /* Canvas pos is where the view port (drawrect) pulls from, so
435  * it needs moved to the right for the left edge to be picked up*/
436  canvas.moveLeft(canvas.x() + outline.x());
437  canvas.setWidth(canvas.width() + outline.x());
438  canvas.moveTop(canvas.y() + outline.y());
439  canvas.setHeight(canvas.height() + outline.y());
440  }
441 
442  if (GetFontProperties()->hasShadow())
443  {
444  QPoint shadowOffset;
445  QColor shadowColor;
446  int shadowAlpha;
447 
448  GetFontProperties()->GetShadow(shadowOffset, shadowColor, shadowAlpha);
449 
450  MythPoint shadow(shadowOffset);
451  shadow.NormPoint(); // scale it to screen resolution
452 
453  drawrect.setWidth(drawrect.width() + shadow.x());
454  drawrect.setHeight(drawrect.height() + shadow.y());
455 
456  canvas.setWidth(canvas.width() + shadow.x());
457  canvas.setHeight(canvas.height() + shadow.y());
458  }
459 
460  p->DrawTextLayout(canvas, m_Layouts, formats,
461  *GetFontProperties(), alpha, drawrect);
462 }
463 
464 bool MythUIText::Layout(QString & paragraph, QTextLayout *layout, bool final,
465  bool & overflow, qreal width, qreal & height,
466  bool force, qreal & last_line_width,
467  QRectF & min_rect, int & num_lines)
468 {
469  int last_line = 0;
470 
471  layout->setText(paragraph);
472  layout->beginLayout();
473  num_lines = 0;
474  for (;;)
475  {
476  QTextLine line = layout->createLine();
477  if (!line.isValid())
478  break;
479 
480  // Try "visible" width first, so alignment works
481  line.setLineWidth(width);
482 
483  if (!m_MultiLine && line.textLength() < paragraph.size())
484  {
485  if (!force && m_Cutdown != Qt::ElideNone)
486  {
487  QFontMetrics fm(GetFontProperties()->face());
488  paragraph = fm.elidedText(paragraph, m_Cutdown,
489  width - fm.averageCharWidth());
490  return false;
491  }
492  // If text does not fit, then expand so canvas size is correct
493  line.setLineWidth(INT_MAX);
494  }
495 
496  height += m_Leading;
497  line.setPosition(QPointF(0, height));
498  height += m_lineHeight;
499  if (!overflow)
500  {
501  if (height > m_Area.height())
502  {
503  LOG(VB_GUI, num_lines ? LOG_DEBUG : LOG_NOTICE,
504  QString("'%1' (%2): height overflow. line height %3 "
505  "paragraph height %4, area height %5")
506  .arg(objectName())
507  .arg(GetXMLLocation())
508  .arg(line.height())
509  .arg(height)
510  .arg(m_Area.height()));
511 
512  if (!m_MultiLine)
513  m_drawRect.setHeight(height);
514  if (m_Cutdown != Qt::ElideNone)
515  {
516  QFontMetrics fm(GetFontProperties()->face());
517  QString cut_line = fm.elidedText
518  (paragraph.mid(last_line),
519  Qt::ElideRight,
520  width - fm.averageCharWidth());
521  paragraph = paragraph.left(last_line) + cut_line;
522  if (last_line == 0)
523  min_rect |= line.naturalTextRect();
524  return false;
525  }
526  overflow = true;
527  }
528  else
529  m_drawRect.setHeight(height);
530  if (!m_MultiLine)
531  overflow = true;
532  }
533 
534  last_line = line.textStart();
535  last_line_width = line.naturalTextWidth();
536  min_rect |= line.naturalTextRect();
537  ++num_lines;
538 
539  if (final && line.textLength())
540  {
549  QFontMetrics fm(GetFontProperties()->face());
550  int bearing;
551 
552  bearing = fm.leftBearing(m_CutMessage[last_line]);
553  if (m_leftBearing > bearing)
554  m_leftBearing = bearing;
555  bearing = fm.rightBearing
556  (m_CutMessage[last_line + line.textLength() - 1]);
557  if (m_rightBearing > bearing)
558  m_rightBearing = bearing;
559  }
560  }
561 
562  layout->endLayout();
563  return true;
564 }
565 
566 bool MythUIText::LayoutParagraphs(const QStringList & paragraphs,
567  const QTextOption & textoption,
568  qreal width, qreal & height,
569  QRectF & min_rect, qreal & last_line_width,
570  int & num_lines, bool final)
571 {
572  QStringList::const_iterator Ipara;
573  QVector<QTextLayout *>::iterator Ilayout;
574  QTextLayout *layout;
575  QString para;
576  bool overflow = false;
577  qreal saved_height;
578  QRectF saved_rect;
579  int idx;
580 
581  for (Ilayout = m_Layouts.begin(); Ilayout != m_Layouts.end(); ++Ilayout)
582  (*Ilayout)->clearLayout();
583 
584  for (Ipara = paragraphs.begin(), idx = 0;
585  Ipara != paragraphs.end(); ++Ipara, ++idx)
586  {
587  layout = m_Layouts[idx];
588  layout->setTextOption(textoption);
589  layout->setFont(m_Font->face());
590 
591  para = *Ipara;
592  saved_height = height;
593  saved_rect = min_rect;
594  if (!Layout(para, layout, final, overflow, width, height, false,
595  last_line_width, min_rect, num_lines))
596  {
597  // Again, with cut down
598  min_rect = saved_rect;
599  height = saved_height;
600  Layout(para, layout, final, overflow, width, height, true,
601  last_line_width, min_rect, num_lines);
602  break;
603  }
604  }
605  m_drawRect.setWidth(width);
606 
607  return (!overflow);
608 }
609 
610 bool MythUIText::GetNarrowWidth(const QStringList & paragraphs,
611  const QTextOption & textoption, qreal & width)
612 {
613  qreal height, last_line_width, lines;
614  int best_width, too_narrow, last_width = -1;
615  int num_lines = 0;
616  int line_height = 0;
617  int attempt = 0;
618  Qt::TextElideMode cutdown = m_Cutdown;
619  m_Cutdown = Qt::ElideNone;
620 
621  line_height = m_Leading + m_lineHeight;
622  width = m_Area.width() / 2.0;
623  best_width = m_Area.width();
624  too_narrow = 0;
625 
626  for (attempt = 0; attempt < 10; ++attempt)
627  {
628  QRectF min_rect;
629 
630  m_drawRect.setWidth(0);
631  height = 0;
632 
633  LayoutParagraphs(paragraphs, textoption, width, height,
634  min_rect, last_line_width, num_lines, false);
635 
636  if (num_lines <= 0)
637  return false;
638 
639  if (height > m_drawRect.height())
640  {
641  if (too_narrow < width)
642  too_narrow = width;
643 
644  // Too narrow? How many lines didn't fit?
645  lines = static_cast<int>
646  ((height - m_drawRect.height()) / line_height);
647  lines -= (1.0 - last_line_width / width);
648  width += (lines * width) /
649  (m_drawRect.height() / line_height);
650 
651  if (width > best_width || static_cast<int>(width) == last_width)
652  {
653  width = best_width;
654  m_Cutdown = cutdown;
655  return true;
656  }
657  }
658  else
659  {
660  if (best_width > width)
661  best_width = width;
662 
663  lines = static_cast<int>
664  (m_Area.height() - height) / line_height;
665  if (lines >= 1)
666  {
667  // Too wide?
668  width -= width *
669  (lines / static_cast<qreal>(num_lines - 1 + lines));
670  if (static_cast<int>(width) == last_width)
671  {
672  m_Cutdown = cutdown;
673  return true;
674  }
675  }
676  else if (last_line_width < m_Area.width())
677  {
678  // Is the last line fully used?
679  width -= (1.0 - last_line_width / width) / num_lines;
680  if (width > last_line_width)
681  width = last_line_width;
682  if (static_cast<int>(width) == last_width)
683  {
684  m_Cutdown = cutdown;
685  return true;
686  }
687  }
688  if (width < too_narrow)
689  width = too_narrow;
690  }
691  last_width = width;
692  }
693 
694  LOG(VB_GENERAL, LOG_ERR, QString("'%1' (%2) GetNarrowWidth: Gave up "
695  "while trying to find optimal width "
696  "for '%3'.")
697  .arg(objectName()).arg(GetXMLLocation()).arg(m_CutMessage));
698 
699  width = best_width;
700  m_Cutdown = cutdown;
701  return false;
702 }
703 
705 {
706  if (m_Area.isNull())
707  return;
708 
709  qreal width, height;
710  QRectF min_rect;
711  QFontMetrics fm(GetFontProperties()->face());
712 
713  m_lineHeight = fm.height();
714  m_Leading = m_MultiLine ? fm.leading() + m_extraLeading : m_extraLeading;
715  m_CutMessage.clear();
716  m_textCursor = -1;
717 
719  {
720  bool isNumber;
721  int value = m_Message.toInt(&isNumber);
722 
723  if (isNumber && m_TemplateText.contains("%n"))
724  {
725  m_CutMessage = qApp->translate("ThemeUI",
726  m_TemplateText.toUtf8(), NULL,
727  QCoreApplication::UnicodeUTF8,
728  qAbs(value));
729  }
730  else if (m_TemplateText.contains("%1"))
731  {
732  QString tmp = qApp->translate("ThemeUI", m_TemplateText.toUtf8(),
733  NULL, QCoreApplication::UnicodeUTF8);
734  m_CutMessage = tmp.arg(m_Message);
735  }
736  }
737 
738  if (m_CutMessage.isEmpty())
740 
741  if (m_CutMessage.isEmpty())
742  {
743  if (m_Layouts.empty())
744  m_Layouts.push_back(new QTextLayout);
745 
746  QTextLine line;
747  QTextOption textoption(static_cast<Qt::Alignment>(m_Justification));
748  QVector<QTextLayout *>::iterator Ilayout = m_Layouts.begin();
749 
750  (*Ilayout)->setTextOption(textoption);
751  (*Ilayout)->setText("");
752  (*Ilayout)->beginLayout();
753  line = (*Ilayout)->createLine();
754  line.setLineWidth(m_Area.width());
755  line.setPosition(QPointF(0, 0));
756  (*Ilayout)->endLayout();
757  m_drawRect.setWidth(m_Area.width());
759 
760  for (++Ilayout ; Ilayout != m_Layouts.end(); ++Ilayout)
761  (*Ilayout)->clearLayout();
762 
764  }
765  else
766  {
767  QStringList templist;
768  QStringList::iterator it;
769 
770  switch (m_textCase)
771  {
772  case CaseUpper :
773  m_CutMessage = m_CutMessage.toUpper();
774  break;
775  case CaseLower :
776  m_CutMessage = m_CutMessage.toLower();
777  break;
778  case CaseCapitaliseFirst :
779  //m_CutMessage = m_CutMessage.toLower();
780  templist = m_CutMessage.split(". ");
781 
782  for (it = templist.begin(); it != templist.end(); ++it)
783  (*it).replace(0, 1, (*it).left(1).toUpper());
784 
785  m_CutMessage = templist.join(". ");
786  break;
787  case CaseCapitaliseAll :
788  //m_CutMessage = m_CutMessage.toLower();
789  templist = m_CutMessage.split(" ");
790 
791  for (it = templist.begin(); it != templist.end(); ++it)
792  (*it).replace(0, 1, (*it).left(1).toUpper());
793 
794  m_CutMessage = templist.join(" ");
795  break;
796  case CaseNormal:
797  break;
798  }
799 
800  QTextOption textoption(static_cast<Qt::Alignment>(m_Justification));
801  textoption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
802 
803  int idx, num_lines;
804  qreal last_line_width;
805  QStringList paragraphs = m_CutMessage.split('\n',
806  QString::KeepEmptyParts);
807 
808  for (idx = m_Layouts.size(); idx < paragraphs.size(); ++idx)
809  m_Layouts.push_back(new QTextLayout);
810 
811  if (m_MultiLine && m_ShrinkNarrow &&
812  m_MinSize.isValid() && !m_CutMessage.isEmpty())
813  GetNarrowWidth(paragraphs, textoption, width);
814  else
815  width = m_Area.width();
816 
817  height = 0;
819  LayoutParagraphs(paragraphs, textoption, width, height,
820  min_rect, last_line_width, num_lines, true);
821 
822  m_Canvas.setRect(0, 0, min_rect.x() + min_rect.width(), height);
824  m_scrollBounce = false;
825 
832  QRect actual = fm.boundingRect(m_CutMessage);
833  m_Ascent = -(actual.y() + fm.ascent());
834  m_Descent = actual.height() - fm.height();
835  }
836 
837  if (m_scrolling)
838  {
839  if (m_scrollDirection == ScrollLeft ||
842  {
843  if (m_Canvas.width() > m_drawRect.width())
844  {
845  m_drawRect.setX(m_Area.x());
846  m_drawRect.setWidth(m_Area.width());
847  m_scrollOffset = m_drawRect.x() - m_Canvas.x();
848  }
849  }
850  else
851  {
852  if (m_Canvas.height() > m_drawRect.height())
853  {
854  m_drawRect.setY(m_Area.y());
855  m_drawRect.setHeight(m_Area.height());
856  m_scrollOffset = m_drawRect.y() - m_Canvas.y();
857  }
858  }
859  }
860 
861  // If any of hcenter|vcenter|Justify, center it all, then adjust later
862  if (m_Justification & (Qt::AlignCenter|Qt::AlignJustify))
863  {
864  m_drawRect.moveCenter(m_Area.center());
865  min_rect.moveCenter(m_Area.center());
866  }
867 
868  // Adjust horizontal
869  if (m_Justification & Qt::AlignLeft)
870  {
871  // If text size is less than allowed min size, center it
872  if (m_ShrinkNarrow && m_MinSize.isValid() && min_rect.isValid() &&
873  min_rect.width() < m_MinSize.x())
874  {
876  (((m_MinSize.x() - min_rect.width() +
877  fm.averageCharWidth()) / 2)));
878  min_rect.setWidth(m_MinSize.x());
879  }
880  else
882 
883  min_rect.moveLeft(m_Area.x());
884  }
885  else if (m_Justification & Qt::AlignRight)
886  {
887  // If text size is less than allowed min size, center it
888  if (m_ShrinkNarrow && m_MinSize.isValid() && min_rect.isValid() &&
889  min_rect.width() < m_MinSize.x())
890  {
891  m_drawRect.moveRight(m_Area.x() + m_Area.width() -
892  (((m_MinSize.x() - min_rect.width() +
893  fm.averageCharWidth()) / 2)));
894  min_rect.setWidth(m_MinSize.x());
895  }
896  else
897  m_drawRect.moveRight(m_Area.x() + m_Area.width());
898 
899  min_rect.moveRight(m_Area.x() + m_Area.width());
900  }
901 
902  // Adjust vertical
903  if (m_Justification & Qt::AlignTop)
904  {
905  // If text size is less than allowed min size, center it
906  if (!m_ShrinkNarrow && m_MinSize.isValid() && min_rect.isValid() &&
907  min_rect.height() < m_MinSize.y())
908  {
910  ((m_MinSize.y() - min_rect.height()) / 2));
911  min_rect.setHeight(m_MinSize.y());
912  }
913  else
915 
916  min_rect.moveTop(m_Area.y());
917  }
918  else if (m_Justification & Qt::AlignBottom)
919  {
920  // If text size is less than allowed min size, center it
921  if (!m_ShrinkNarrow && m_MinSize.isValid() && min_rect.isValid() &&
922  min_rect.height() < m_MinSize.y())
923  {
924  m_drawRect.moveBottom(m_Area.y() + m_Area.height() -
925  ((m_MinSize.y() - min_rect.height()) / 2));
926  min_rect.setHeight(m_MinSize.y());
927  }
928  else
929  m_drawRect.moveBottom(m_Area.y() + m_Area.height());
930 
931  min_rect.moveBottom(m_Area.y() + m_Area.height());
932  }
933 
935  if (m_MinSize.isValid())
936  {
937  // Record the minimal area needed for the message.
938  SetMinArea(min_rect.toRect());
939  }
940 }
941 
943 {
944  int lineNo = -1;
945  int lineCount = 0;
946  int currPos = 0;
947  int layoutStartPos = 0;
948  int xPos;
949 
950  for (int x = 0; x < m_Layouts.count(); x++)
951  {
952  QTextLayout *layout = m_Layouts.at(x);
953 
954  for (int y = 0; y < layout->lineCount(); y++)
955  {
956  lineCount++;
957  if (lineNo != -1)
958  continue;
959 
960  QTextLine line = layout->lineAt(y);
961 
962  if (m_textCursor >= currPos && m_textCursor < currPos + line.textLength()
963  + (line.lineNumber() == layout->lineCount() - 1 ? 1 : 0))
964  {
965  lineNo = lineCount - 1;
966  xPos = line.cursorToX(m_textCursor - layoutStartPos);
967  continue;
968  }
969 
970  currPos += line.textLength();
971  }
972 
973  currPos += 1; // skip the newline
974  layoutStartPos = currPos;
975  }
976 
977  // are we are at the top and need to scroll up
978  if (lineNo == 0 && lines < 0)
979  return -1;
980 
981  // are we at the bottom and need to scroll down
982  if (lineNo == lineCount - 1 && lines > 0)
983  return -1;
984 
985  if (lineNo == -1)
986  {
987  LOG(VB_GENERAL, LOG_ERR,
988  QString("'%1' (%2) MoveCursor offset %3 not found in ANY paragraph!")
989  .arg(objectName()).arg(GetXMLLocation()).arg(m_textCursor));
990  return m_textCursor;
991  }
992 
993  int newLine = lineNo + lines;
994 
995  if (newLine < 0)
996  newLine = 0;
997 
998  if (newLine >= lineCount)
999  newLine = lineCount - 1;
1000 
1001  lineNo = -1;
1002  currPos = 0;
1003  layoutStartPos = 0;
1004 
1005  for (int x = 0; x < m_Layouts.count(); x++)
1006  {
1007  QTextLayout *layout = m_Layouts.at(x);
1008 
1009  for (int y = 0; y < layout->lineCount(); y++)
1010  {
1011  lineNo++;
1012  QTextLine line = layout->lineAt(y);
1013 
1014  if (lineNo == newLine)
1015  return layoutStartPos + line.xToCursor(xPos);
1016  }
1017 
1018  layoutStartPos += layout->text().length() + 1;
1019  }
1020 
1021  // should never reach here
1022  return m_textCursor;
1023 }
1024 
1025 QPoint MythUIText::CursorPosition(int text_offset)
1026 {
1027  if (m_Layouts.empty())
1028  return m_Area.topLeft().toQPoint();
1029 
1030  if (text_offset == m_textCursor)
1031  return m_cursorPos;
1032  m_textCursor = text_offset;
1033 
1034  QVector<QTextLayout *>::const_iterator Ipara;
1035  QPoint pos;
1036  int x, y, mid, line_height;
1037  int offset = text_offset;
1038 
1039  for (Ipara = m_Layouts.constBegin(); Ipara != m_Layouts.constEnd(); ++Ipara)
1040  {
1041  QTextLine line = (*Ipara)->lineForTextPosition(offset);
1042 
1043  if (line.isValid())
1044  {
1045  pos.setX(line.cursorToX(&offset));
1046  pos.setY(line.y());
1047  break;
1048  }
1049  offset -= ((*Ipara)->text().size() + 1); // Account for \n
1050  }
1051  if (Ipara == m_Layouts.constEnd())
1052  {
1053  LOG(VB_GENERAL, LOG_ERR,
1054  QString("'%1' (%2) CursorPosition offset %3 not found in "
1055  "ANY paragraph!")
1056  .arg(objectName()).arg(GetXMLLocation()).arg(text_offset));
1057  return m_Area.topLeft().toQPoint();
1058  }
1059 
1060  mid = m_drawRect.width() / 2;
1061  if (m_Canvas.width() <= m_drawRect.width() || pos.x() <= mid)
1062  x = 0; // start
1063  else if (pos.x() >= m_Canvas.width() - mid) // end
1064  {
1065  x = m_Canvas.width() - m_drawRect.width();
1066  pos.setX(pos.x() - x);
1067  }
1068  else // middle
1069  {
1070  x = pos.x() - mid;
1071  pos.setX(pos.x() - x);
1072  }
1073 
1074  line_height = m_lineHeight + m_Leading;
1075  mid = m_Area.height() / 2;
1076  mid -= (mid % line_height);
1077  y = pos.y() - mid;
1078 
1079  if (y <= 0 || m_Canvas.height() <= m_Area.height()) // Top of buffer
1080  y = 0;
1081  else if (y + m_Area.height() > m_Canvas.height()) // Bottom of buffer
1082  {
1083  int visible_lines = ((m_Area.height() / line_height) * line_height);
1084  y = m_Canvas.height() - visible_lines;
1085  pos.setY(visible_lines - (m_Canvas.height() - pos.y()));
1086  }
1087  else // somewhere in the middle
1088  {
1089  y -= m_Leading;
1090  pos.setY(mid + m_Leading);
1091  }
1092 
1093  m_Canvas.moveTopLeft(QPoint(-x, -y));
1094 
1095  // Compensate for vertical alignment
1096  pos.setY(pos.y() + m_drawRect.y() - m_Area.y());
1097 
1098  pos += m_Area.topLeft().toQPoint();
1099  m_cursorPos = pos;
1100 
1101  return pos;
1102 }
1103 
1105 {
1106  //
1107  // Call out base class pulse which will handle any alpha cycling and
1108  // movement
1109  //
1110 
1112 
1113  if (m_colorCycling)
1114  {
1115  curR += incR;
1116  curG += incG;
1117  curB += incB;
1118 
1119  m_curStep++;
1120 
1121  if (m_curStep >= m_numSteps)
1122  {
1123  m_curStep = 0;
1124  incR *= -1;
1125  incG *= -1;
1126  incB *= -1;
1127  }
1128 
1129  QColor newColor = QColor((int)curR, (int)curG, (int)curB);
1130 
1131  if (newColor != m_Font->color())
1132  {
1133  m_Font->SetColor(newColor);
1134  SetRedraw();
1135  }
1136  }
1137 
1138  if (m_scrolling)
1139  {
1140  int whole;
1141 
1142  if (m_scrollPause > 0)
1143  --m_scrollPause;
1144  else
1145  {
1146  if (m_scrollBounce)
1148  else
1150  }
1151 
1152  whole = static_cast<int>(m_scrollPos);
1153  if (m_scrollPosWhole != whole)
1154  {
1155  int shift = whole - m_scrollPosWhole;
1156  m_scrollPosWhole = whole;
1157 
1158  switch (m_scrollDirection)
1159  {
1160  case ScrollLeft :
1161  if (m_Canvas.width() > m_drawRect.width())
1162  {
1163  ShiftCanvas(-shift, 0);
1164  if (m_Canvas.x() + m_Canvas.width() < 0)
1165  {
1166  SetCanvasPosition(m_drawRect.width(), 0);
1167  m_scrollPos = m_scrollPosWhole = 0;
1168  }
1169  }
1170  break;
1171  case ScrollRight :
1172  if (m_Canvas.width() > m_drawRect.width())
1173  {
1174  ShiftCanvas(shift, 0);
1175  if (m_Canvas.x() > m_drawRect.width())
1176  {
1177  SetCanvasPosition(-m_Canvas.width(), 0);
1178  m_scrollPos = m_scrollPosWhole = 0;
1179  }
1180  }
1181  break;
1182  case ScrollHorizontal:
1183  if (m_Canvas.width() <= m_drawRect.width())
1184  break;
1185  if (m_scrollBounce) // scroll right
1186  {
1187  if (m_Canvas.x() + m_scrollOffset > m_drawRect.x())
1188  {
1189  m_scrollBounce = false;
1191  m_scrollPos = m_scrollPosWhole = 0;
1192  }
1193  else
1194  ShiftCanvas(shift, 0);
1195  }
1196  else // scroll left
1197  {
1198  if (m_Canvas.x() + m_Canvas.width() + m_scrollOffset <
1199  m_drawRect.x() + m_drawRect.width())
1200  {
1201  m_scrollBounce = true;
1203  m_scrollPos = m_scrollPosWhole = 0;
1204  }
1205  else
1206  ShiftCanvas(-shift, 0);
1207  }
1208  break;
1209  case ScrollUp :
1210  if (m_Canvas.height() > m_drawRect.height())
1211  {
1212  ShiftCanvas(0, -shift);
1213  if (m_Canvas.y() + m_Canvas.height() < 0)
1214  {
1215  SetCanvasPosition(0, m_drawRect.height());
1216  m_scrollPos = m_scrollPosWhole = 0;
1217  }
1218  }
1219  break;
1220  case ScrollDown :
1221  if (m_Canvas.height() > m_drawRect.height())
1222  {
1223  ShiftCanvas(0, shift);
1224  if (m_Canvas.y() > m_drawRect.height())
1225  {
1226  SetCanvasPosition(0, -m_Canvas.height());
1227  m_scrollPos = m_scrollPosWhole = 0;
1228  }
1229  }
1230  break;
1231  case ScrollVertical:
1232  if (m_Canvas.height() <= m_drawRect.height())
1233  break;
1234  if (m_scrollBounce) // scroll down
1235  {
1236  if (m_Canvas.y() + m_scrollOffset > m_drawRect.y())
1237  {
1238  m_scrollBounce = false;
1240  m_scrollPos = m_scrollPosWhole = 0;
1241  }
1242  else
1243  ShiftCanvas(0, shift);
1244  }
1245  else // scroll up
1246  {
1247  if (m_Canvas.y() + m_Canvas.height() + m_scrollOffset <
1248  m_drawRect.y() + m_drawRect.height())
1249  {
1250  m_scrollBounce = true;
1252  m_scrollPos = m_scrollPosWhole = 0;
1253  }
1254  else
1255  ShiftCanvas(0, -shift);
1256  }
1257  break;
1258  case ScrollNone:
1259  break;
1260  }
1261  }
1262  }
1263 }
1264 
1265 void MythUIText::CycleColor(QColor startColor, QColor endColor, int numSteps)
1266 {
1267  if (!GetPainter()->SupportsAnimation())
1268  return;
1269 
1270  m_startColor = startColor;
1271  m_endColor = endColor;
1272  m_numSteps = numSteps;
1273  m_curStep = 0;
1274 
1275  curR = startColor.red();
1276  curG = startColor.green();
1277  curB = startColor.blue();
1278 
1279  incR = (endColor.red() * 1.0 - curR) / m_numSteps;
1280  incG = (endColor.green() * 1.0 - curG) / m_numSteps;
1281  incB = (endColor.blue() * 1.0 - curB) / m_numSteps;
1282 
1283  m_colorCycling = true;
1284 }
1285 
1287 {
1288  if (!m_colorCycling)
1289  return;
1290 
1292  m_colorCycling = false;
1293  SetRedraw();
1294 }
1295 
1297  const QString &filename, QDomElement &element, bool showWarnings)
1298 {
1299  if (element.tagName() == "area")
1300  {
1301  SetArea(parseRect(element));
1303  }
1304  // else if (element.tagName() == "altarea") // Unused, but maybe in future?
1305  // m_AltDisplayRect = parseRect(element);
1306  else if (element.tagName() == "font")
1307  {
1308  QString fontname = getFirstText(element);
1309  MythFontProperties *fp = GetFont(fontname);
1310 
1311  if (!fp)
1312  fp = GetGlobalFontMap()->GetFont(fontname);
1313 
1314  if (fp)
1315  {
1316  MythFontProperties font = *fp;
1317  int screenHeight = GetMythMainWindow()->GetUIScreenRect().height();
1318  font.Rescale(screenHeight);
1319  int fontStretch = GetMythUI()->GetFontStretch();
1320  font.AdjustStretch(fontStretch);
1321  QString state = element.attribute("state", "");
1322 
1323  if (!state.isEmpty())
1324  {
1325  m_FontStates.insert(state, font);
1326  }
1327  else
1328  {
1329  m_FontStates.insert("default", font);
1330  *m_Font = m_FontStates["default"];
1331  }
1332  }
1333  }
1334  else if (element.tagName() == "extraleading")
1335  {
1336  m_extraLeading = getFirstText(element).toInt();
1337  }
1338  else if (element.tagName() == "value")
1339  {
1340  if (element.attribute("lang", "").isEmpty())
1341  {
1342  m_Message = qApp->translate("ThemeUI",
1343  parseText(element).toUtf8(), NULL,
1344  QCoreApplication::UnicodeUTF8);
1345  }
1346  else if (element.attribute("lang", "").toLower() ==
1348  {
1349  m_Message = parseText(element);
1350  }
1351  else if (element.attribute("lang", "").toLower() ==
1353  {
1354  m_Message = parseText(element);
1355  }
1356 
1358  SetText(m_Message);
1359  }
1360  else if (element.tagName() == "template")
1361  {
1362  m_TemplateText = parseText(element);
1363  }
1364  else if (element.tagName() == "cutdown")
1365  {
1366  QString mode = getFirstText(element).toLower();
1367 
1368  if (mode == "left")
1369  SetCutDown(Qt::ElideLeft);
1370  else if (mode == "middle")
1371  SetCutDown(Qt::ElideMiddle);
1372  else if (mode == "right" || parseBool(element))
1373  SetCutDown(Qt::ElideRight);
1374  else
1375  SetCutDown(Qt::ElideNone);
1376  }
1377  else if (element.tagName() == "multiline")
1378  {
1379  SetMultiLine(parseBool(element));
1380  }
1381  else if (element.tagName() == "align")
1382  {
1383  QString align = getFirstText(element).toLower();
1385  }
1386  else if (element.tagName() == "colorcycle")
1387  {
1388  if (GetPainter()->SupportsAnimation())
1389  {
1390  QString tmp = element.attribute("start");
1391 
1392  if (!tmp.isEmpty())
1393  m_startColor = QColor(tmp);
1394 
1395  tmp = element.attribute("end");
1396 
1397  if (!tmp.isEmpty())
1398  m_endColor = QColor(tmp);
1399 
1400  tmp = element.attribute("steps");
1401 
1402  if (!tmp.isEmpty())
1403  m_numSteps = tmp.toInt();
1404 
1405  // initialize the rest of the stuff
1407  }
1408  else
1409  m_colorCycling = false;
1410 
1411  m_colorCycling = parseBool(element.attribute("disable"));
1412  }
1413  else if (element.tagName() == "scroll")
1414  {
1415  if (GetPainter()->SupportsAnimation())
1416  {
1417  QString tmp = element.attribute("direction");
1418 
1419  if (!tmp.isEmpty())
1420  {
1421  tmp = tmp.toLower();
1422 
1423  if (tmp == "left")
1425  else if (tmp == "right")
1427  else if (tmp == "up")
1429  else if (tmp == "down")
1431  else if (tmp == "horizontal")
1433  else if (tmp == "vertical")
1435  else
1436  {
1438  LOG(VB_GENERAL, LOG_ERR,
1439  QString("'%1' (%2) Invalid scroll attribute")
1440  .arg(objectName()).arg(GetXMLLocation()));
1441  }
1442  }
1443 
1444  tmp = element.attribute("startdelay");
1445  if (!tmp.isEmpty())
1446  {
1447  float seconds = tmp.toFloat();
1448  m_scrollStartDelay = static_cast<int>(seconds *
1449  static_cast<float>(MythMainWindow::drawRefresh) + 0.5);
1450  }
1451  tmp = element.attribute("returndelay");
1452  if (!tmp.isEmpty())
1453  {
1454  float seconds = tmp.toFloat();
1455  m_scrollReturnDelay = static_cast<int>(seconds *
1456  static_cast<float>(MythMainWindow::drawRefresh) + 0.5);
1457  }
1458  tmp = element.attribute("rate");
1459  if (!tmp.isEmpty())
1460  {
1461 #if 0 // scroll rate as a percentage of 70Hz
1462  float percent = tmp.toFloat() / 100.0;
1463  m_scrollForwardRate = percent *
1464  static_cast<float>(MythMainWindow::drawRefresh) / 70.0;
1465 #else // scroll rate as pixels per second
1466  int pixels = tmp.toInt();
1468  pixels / static_cast<float>(MythMainWindow::drawRefresh);
1469 #endif
1470  }
1471  tmp = element.attribute("returnrate");
1472  if (!tmp.isEmpty())
1473  {
1474 #if 0 // scroll rate as a percentage of 70Hz
1475  float percent = tmp.toFloat() / 100.0;
1476  m_scrollReturnRate = percent *
1477  static_cast<float>(MythMainWindow::drawRefresh) / 70.0;
1478 #else // scroll rate as pixels per second
1479  int pixels = tmp.toInt();
1481  pixels / static_cast<float>(MythMainWindow::drawRefresh);
1482 #endif
1483  }
1484 
1485  m_scrolling = true;
1486  }
1487  else
1488  m_scrolling = false;
1489  }
1490  else if (element.tagName() == "case")
1491  {
1492  QString stringCase = getFirstText(element).toLower();
1493 
1494  if (stringCase == "lower")
1496  else if (stringCase == "upper")
1498  else if (stringCase == "capitalisefirst")
1500  else if (stringCase == "capitaliseall")
1502  else
1504  }
1505  else
1506  {
1507  if (element.tagName() == "minsize" && element.hasAttribute("shrink"))
1508  {
1509  m_ShrinkNarrow = (element.attribute("shrink")
1510  .toLower() != "short");
1511  }
1512 
1513  return MythUIType::ParseElement(filename, element, showWarnings);
1514  }
1515 
1516  return true;
1517 }
1518 
1520 {
1521  MythUIText *text = dynamic_cast<MythUIText *>(base);
1522 
1523  if (!text)
1524  {
1525  LOG(VB_GENERAL, LOG_ERR,
1526  QString("'%1' (%2) ERROR, bad parsing '%3' (%4)")
1527  .arg(objectName()).arg(GetXMLLocation())
1528  .arg(base->objectName()).arg(base->GetXMLLocation()));
1529  return;
1530  }
1531 
1535  m_Canvas = text->m_Canvas;
1536  m_drawRect = text->m_drawRect;
1537 
1539  SetText(text->m_Message);
1540  m_CutMessage = text->m_CutMessage;
1542 
1544  m_Cutdown = text->m_Cutdown;
1545  m_MultiLine = text->m_MultiLine;
1546  m_Leading = text->m_Leading;
1548  m_lineHeight = text->m_lineHeight;
1549  m_textCursor = text->m_textCursor;
1550 
1551  QMutableMapIterator<QString, MythFontProperties> it(text->m_FontStates);
1552 
1553  while (it.hasNext())
1554  {
1555  it.next();
1556  m_FontStates.insert(it.key(), it.value());
1557  }
1558 
1559  *m_Font = m_FontStates["default"];
1560 
1562  m_startColor = text->m_startColor;
1563  m_endColor = text->m_endColor;
1564  m_numSteps = text->m_numSteps;
1565  m_curStep = text->m_curStep;
1566  curR = text->curR;
1567  curG = text->curG;
1568  curB = text->curB;
1569  incR = text->incR;
1570  incG = text->incG;
1571  incB = text->incB;
1572 
1578  m_scrolling = text->m_scrolling;
1579 
1580  m_textCase = text->m_textCase;
1581 
1582  MythUIType::CopyFrom(base);
1583  FillCutMessage();
1584 }
1585 
1587 {
1588  MythUIText *text = new MythUIText(parent, objectName());
1589  text->CopyFrom(this);
1590 }
1591 
1592 
1594 {
1595  if (m_scrolling && m_Cutdown != Qt::ElideNone)
1596  {
1597  LOG(VB_GENERAL, LOG_ERR,
1598  QString("'%1' (%2): <scroll> and <cutdown> are not combinable.")
1599  .arg(objectName()).arg(GetXMLLocation()));
1600  m_Cutdown = Qt::ElideNone;
1601  }
1602  FillCutMessage();
1603 }