Quassel IRC  Pre-Release
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros
chatitem.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * Copyright (C) 2005-2015 by the Quassel Project *
3  * devel@quassel-irc.org *
4  * *
5  * This program is free software; you can redistribute it and/or modify *
6  * it under the terms of the GNU General Public License as published by *
7  * the Free Software Foundation; either version 2 of the License, or *
8  * (at your option) version 3. *
9  * *
10  * This program is distributed in the hope that it will be useful, *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13  * GNU General Public License for more details. *
14  * *
15  * You should have received a copy of the GNU General Public License *
16  * along with this program; if not, write to the *
17  * Free Software Foundation, Inc., *
18  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
19  ***************************************************************************/
20 
21 #include <QApplication>
22 #include <QClipboard>
23 #include <QDesktopServices>
24 #include <QFontMetrics>
25 #include <QGraphicsSceneMouseEvent>
26 #include <QIcon>
27 #include <QPainter>
28 #include <QPalette>
29 #include <QTextLayout>
30 #include <QMenu>
31 
32 #include "buffermodel.h"
33 #include "bufferview.h"
34 #include "chatitem.h"
35 #include "chatline.h"
36 #include "chatlinemodel.h"
37 #include "chatview.h"
39 #include "mainwin.h"
40 #include "qtui.h"
41 #include "qtuistyle.h"
42 
43 ChatItem::ChatItem(const QRectF &boundingRect, ChatLine *parent)
44  : _parent(parent),
45  _boundingRect(boundingRect),
46  _selectionMode(NoSelection),
47  _selectionStart(-1),
48  _cachedLayout(0)
49 {
50 }
51 
52 
54 {
55  delete _cachedLayout;
56 }
57 
58 
60 {
61  return _parent;
62 }
63 
64 
66 {
67  return chatLine()->chatScene();
68 }
69 
70 
72 {
73  return chatScene()->chatView();
74 }
75 
76 
77 const QAbstractItemModel *ChatItem::model() const
78 {
79  return chatLine()->model();
80 }
81 
82 
83 int ChatItem::row() const
84 {
85  return chatLine()->row();
86 }
87 
88 
89 QPointF ChatItem::mapToLine(const QPointF &p) const
90 {
91  return p + pos();
92 }
93 
94 
95 QPointF ChatItem::mapFromLine(const QPointF &p) const
96 {
97  return p - pos();
98 }
99 
100 
101 // relative to the ChatLine
102 QPointF ChatItem::mapToScene(const QPointF &p) const
103 {
104  return chatLine()->mapToScene(p /* + pos() */);
105 }
106 
107 
108 QPointF ChatItem::mapFromScene(const QPointF &p) const
109 {
110  return chatLine()->mapFromScene(p) /* - pos() */;
111 }
112 
113 
114 QVariant ChatItem::data(int role) const
115 {
116  QModelIndex index = model()->index(row(), column());
117  if (!index.isValid()) {
118  qWarning() << "ChatItem::data(): model index is invalid!" << index;
119  return QVariant();
120  }
121  return model()->data(index, role);
122 }
123 
124 
125 QTextLayout *ChatItem::layout() const
126 {
127  if (_cachedLayout)
128  return _cachedLayout;
129 
130  _cachedLayout = new QTextLayout;
133  return _cachedLayout;
134 }
135 
136 
138 {
139  delete _cachedLayout;
140  _cachedLayout = 0;
141 }
142 
143 
144 void ChatItem::initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode wrapMode, Qt::Alignment alignment) const
145 {
146  Q_ASSERT(layout);
147 
148  layout->setText(data(MessageModel::DisplayRole).toString());
149 
150  QTextOption option;
151  option.setWrapMode(wrapMode);
152  option.setAlignment(alignment);
153  layout->setTextOption(option);
154 
155  QList<QTextLayout::FormatRange> formatRanges
156  = QtUi::style()->toTextLayoutList(formatList(), layout->text().length(), data(ChatLineModel::MsgLabelRole).toUInt());
157  layout->setAdditionalFormats(formatRanges);
158 }
159 
160 
161 void ChatItem::initLayout(QTextLayout *layout) const
162 {
163  initLayoutHelper(layout, QTextOption::NoWrap);
164  doLayout(layout);
165 }
166 
167 
168 void ChatItem::doLayout(QTextLayout *layout) const
169 {
170  layout->beginLayout();
171  QTextLine line = layout->createLine();
172  if (line.isValid()) {
173  line.setLineWidth(width());
174  line.setPosition(QPointF(0, 0));
175  }
176  layout->endLayout();
177 }
178 
179 
181 {
183 }
184 
185 
186 qint16 ChatItem::posToCursor(const QPointF &posInLine) const
187 {
188  QPointF pos = mapFromLine(posInLine);
189  if (pos.y() > height())
190  return data(MessageModel::DisplayRole).toString().length();
191  if (pos.y() < 0)
192  return 0;
193 
194  for (int l = layout()->lineCount() - 1; l >= 0; l--) {
195  QTextLine line = layout()->lineAt(l);
196  if (pos.y() >= line.y()) {
197  return line.xToCursor(pos.x(), QTextLine::CursorOnCharacter);
198  }
199  }
200  return 0;
201 }
202 
203 
204 void ChatItem::paintBackground(QPainter *painter)
205 {
206  QVariant bgBrush;
209  else
211  if (bgBrush.isValid())
212  painter->fillRect(boundingRect(), bgBrush.value<QBrush>());
213 }
214 
215 
216 // NOTE: This is not the most time-efficient implementation, but it saves space by not caching unnecessary data
217 // This is a deliberate trade-off. (-> selectFmt creation, data() call)
218 void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
219 {
220  Q_UNUSED(option); Q_UNUSED(widget);
221  painter->save();
222  painter->setClipRect(boundingRect());
223  paintBackground(painter);
224 
225  layout()->draw(painter, pos(), additionalFormats(), boundingRect());
226 
227  // layout()->draw(painter, QPointF(0,0), formats, boundingRect());
228 
229  // Debuging Stuff
230  // uncomment partially or all of the following stuff:
231  //
232  // 0) alternativ painter color for debug stuff
233 // if(row() % 2)
234 // painter->setPen(Qt::red);
235 // else
236 // painter->setPen(Qt::blue);
237 // 1) draw wordwrap points in the first line
238 // if(column() == 2) {
239 // ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
240 // foreach(ChatLineModel::Word word, wrapList) {
241 // if(word.endX > width())
242 // break;
243 // painter->drawLine(word.endX, 0, word.endX, height());
244 // }
245 // }
246 // 2) draw MsgId over the time column
247 // if(column() == 0) {
248 // QString msgIdString = QString::number(data(MessageModel::MsgIdRole).value<MsgId>().toInt());
249 // QPointF bottomPoint = boundingRect().bottomLeft();
250 // bottomPoint.ry() -= 2;
251 // painter->drawText(bottomPoint, msgIdString);
252 // }
253 // 3) draw bounding rect
254 // painter->drawRect(_boundingRect.adjusted(0, 0, -1, -1));
255 
256  painter->restore();
257 }
258 
259 
260 void ChatItem::overlayFormat(UiStyle::FormatList &fmtList, int start, int end, quint32 overlayFmt) const
261 {
262  for (int i = 0; i < fmtList.count(); i++) {
263  int fmtStart = fmtList.at(i).first;
264  int fmtEnd = (i < fmtList.count()-1 ? fmtList.at(i+1).first : data(MessageModel::DisplayRole).toString().length());
265 
266  if (fmtEnd <= start)
267  continue;
268  if (fmtStart >= end)
269  break;
270 
271  // split the format if necessary
272  if (fmtStart < start) {
273  fmtList.insert(i, fmtList.at(i));
274  fmtList[++i].first = start;
275  }
276  if (end < fmtEnd) {
277  fmtList.insert(i, fmtList.at(i));
278  fmtList[i+1].first = end;
279  }
280 
281  fmtList[i].second |= overlayFmt;
282  }
283 }
284 
285 
286 QVector<QTextLayout::FormatRange> ChatItem::additionalFormats() const
287 {
288  return selectionFormats();
289 }
290 
291 
292 QVector<QTextLayout::FormatRange> ChatItem::selectionFormats() const
293 {
294  if (!hasSelection())
295  return QVector<QTextLayout::FormatRange>();
296 
297  int start, end;
298  if (_selectionMode == FullSelection) {
299  start = 0;
300  end = data(MessageModel::DisplayRole).toString().length();
301  }
302  else {
303  start = qMin(_selectionStart, _selectionEnd);
304  end = qMax(_selectionStart, _selectionEnd);
305  }
306 
307  UiStyle::FormatList fmtList = formatList();
308 
309  while (fmtList.count() > 1 && fmtList.at(1).first <= start)
310  fmtList.removeFirst();
311 
312  fmtList.first().first = start;
313 
314  while (fmtList.count() > 1 && fmtList.last().first >= end)
315  fmtList.removeLast();
316 
317  return QtUi::style()->toTextLayoutList(fmtList, end, UiStyle::Selected|data(ChatLineModel::MsgLabelRole).toUInt()).toVector();
318 }
319 
320 
322 {
324  return false;
326  return true;
327  // partial
328  return _selectionStart != _selectionEnd;
329 }
330 
331 
332 QString ChatItem::selection() const
333 {
335  return data(MessageModel::DisplayRole).toString();
338  return QString();
339 }
340 
341 
342 void ChatItem::setSelection(SelectionMode mode, qint16 start, qint16 end)
343 {
344  _selectionMode = mode;
345  _selectionStart = start;
346  _selectionEnd = end;
347  chatLine()->update();
348 }
349 
350 
352 {
353  if (_selectionMode != FullSelection) {
355  chatLine()->update();
356  }
357 }
358 
359 
361 {
362  if (_selectionMode != NoSelection) {
364  chatLine()->update();
365  }
366 }
367 
368 
369 void ChatItem::continueSelecting(const QPointF &pos)
370 {
372  _selectionEnd = posToCursor(pos);
373  chatLine()->update();
374 }
375 
376 
377 bool ChatItem::isPosOverSelection(const QPointF &pos) const
378 {
380  return true;
382  int cursor = posToCursor(pos);
383  return cursor >= qMin(_selectionStart, _selectionEnd) && cursor <= qMax(_selectionStart, _selectionEnd);
384  }
385  return false;
386 }
387 
388 
389 QList<QRectF> ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity caseSensitive)
390 {
391  QList<QRectF> resultList;
392  const QAbstractItemModel *model_ = model();
393  if (!model_)
394  return resultList;
395 
396  QString plainText = model_->data(model_->index(row(), column()), MessageModel::DisplayRole).toString();
397  QList<int> indexList;
398  int searchIdx = plainText.indexOf(searchWord, 0, caseSensitive);
399  while (searchIdx != -1) {
400  indexList << searchIdx;
401  searchIdx = plainText.indexOf(searchWord, searchIdx + 1, caseSensitive);
402  }
403 
404  foreach(int idx, indexList) {
405  QTextLine line = layout()->lineForTextPosition(idx);
406  qreal x = line.cursorToX(idx);
407  qreal width = line.cursorToX(idx + searchWord.count()) - x;
408  qreal height = line.height();
409  qreal y = height * line.lineNumber();
410  resultList << QRectF(x, y, width, height);
411  }
412 
413  return resultList;
414 }
415 
416 
417 void ChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
418 {
419  // single clicks are already handled by the scene (for clearing the selection)
420  if (clickMode == ChatScene::DragStartClick) {
421  chatScene()->setSelectingItem(this);
423  _selectionMode = NoSelection; // will be set to PartialSelection by mouseMoveEvent
424  chatLine()->update();
425  }
426 }
427 
428 
429 void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
430 {
431  if (event->buttons() == Qt::LeftButton) {
432  if (boundingRect().contains(event->pos())) {
433  qint16 end = posToCursor(event->pos());
434  if (end != _selectionEnd) {
435  _selectionEnd = end;
437  chatLine()->update();
438  }
439  }
440  else {
442  chatScene()->startGlobalSelection(this, event->pos());
443  }
444  event->accept();
445  }
446  else {
447  event->ignore();
448  }
449 }
450 
451 
452 void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
453 {
454  if (event->buttons() == Qt::LeftButton)
455  event->accept();
456  else
457  event->ignore();
458 }
459 
460 
461 void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
462 {
463  if (_selectionMode != NoSelection && event->button() == Qt::LeftButton) {
464  chatScene()->selectionToClipboard(QClipboard::Selection);
465  event->accept();
466  }
467  else
468  event->ignore();
469 }
470 
471 
472 void ChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos)
473 {
474  Q_UNUSED(pos);
475 
477 }
478 
479 
480 // ************************************************************
481 // SenderChatItem
482 // ************************************************************
483 
484 void SenderChatItem::initLayout(QTextLayout *layout) const
485 {
486  initLayoutHelper(layout, QTextOption::ManualWrap, Qt::AlignRight);
487  doLayout(layout);
488 }
489 
490 
491 void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
492 {
493  Q_UNUSED(option); Q_UNUSED(widget);
494  painter->save();
495  painter->setClipRect(boundingRect());
496  paintBackground(painter);
497 
498  qreal layoutWidth = layout()->minimumWidth();
499  qreal offset = 0;
500  if (chatScene()->senderCutoffMode() == ChatScene::CutoffLeft)
501  offset = qMin(width() - layoutWidth, (qreal)0);
502  else
503  offset = qMax(layoutWidth - width(), (qreal)0);
504 
505  if (layoutWidth > width()) {
506  // Draw a nice gradient for longer items
507  // Qt's text drawing with a gradient brush sucks, so we use compositing instead
508  QPixmap pixmap(layout()->boundingRect().toRect().size());
509  pixmap.fill(Qt::transparent);
510 
511  QPainter pixPainter(&pixmap);
512  layout()->draw(&pixPainter, QPointF(qMax(offset, (qreal)0), 0), additionalFormats());
513 
514  // Create alpha channel mask
515  QLinearGradient gradient;
516  if (offset < 0) {
517  gradient.setStart(0, 0);
518  gradient.setFinalStop(12, 0);
519  gradient.setColorAt(0, Qt::transparent);
520  gradient.setColorAt(1, Qt::white);
521  }
522  else {
523  gradient.setStart(width()-10, 0);
524  gradient.setFinalStop(width(), 0);
525  gradient.setColorAt(0, Qt::white);
526  gradient.setColorAt(1, Qt::transparent);
527  }
528  pixPainter.setCompositionMode(QPainter::CompositionMode_DestinationIn); // gradient's alpha gets applied to the pixmap
529  pixPainter.fillRect(pixmap.rect(), gradient);
530  painter->drawPixmap(pos(), pixmap);
531  }
532  else {
533  layout()->draw(painter, pos(), additionalFormats(), boundingRect());
534  }
535  painter->restore();
536 }
537 
538 
539 void SenderChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
540 {
541  if (clickMode == ChatScene::DoubleClick) {
542  BufferInfo curBufInfo = Client::networkModel()->bufferInfo(data(MessageModel::BufferIdRole).value<BufferId>());
543  QString nick = data(MessageModel::EditRole).toString();
544  // check if the nick is a valid ircUser
545  if (!nick.isEmpty() && Client::network(curBufInfo.networkId())->ircUser(nick))
546  Client::bufferModel()->switchToOrStartQuery(curBufInfo.networkId(), nick);
547  }
548  else
549  ChatItem::handleClick(pos, clickMode);
550 }
551 
552 
553 // ************************************************************
554 // ContentsChatItem
555 // ************************************************************
556 
558 
559 ContentsChatItem::ContentsChatItem(const QPointF &pos, const qreal &width, ChatLine *parent)
560  : ChatItem(QRectF(pos, QSizeF(width, 0)), parent),
561  _data(0)
562 {
563  setPos(pos);
564  setGeometryByWidth(width);
565 }
566 
567 
568 QFontMetricsF *ContentsChatItem::fontMetrics() const
569 {
570  return QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value<UiStyle::FormatList>().at(0).second, 0);
571 }
572 
573 
575 {
576  delete _data;
577 }
578 
579 
581 {
582  delete _data;
583  _data = 0;
585 }
586 
587 
589 {
590  if (!_data) {
591  ContentsChatItem *that = const_cast<ContentsChatItem *>(this);
593  }
594  return _data;
595 }
596 
597 
599 {
600  // We use this for reloading layout info as well, so we can't bail out if the width doesn't change
601 
602  // compute height
603  int lines = 1;
604  WrapColumnFinder finder(this);
605  while (finder.nextWrapColumn(w) > 0)
606  lines++;
607  qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height()); // cope with negative leading()
608  qreal h = lines * spacing;
609  delete _data;
610  _data = 0;
611 
612  if (w != width() || h != height())
613  setGeometry(w, h);
614 
615  return h;
616 }
617 
618 
619 void ContentsChatItem::initLayout(QTextLayout *layout) const
620 {
621  initLayoutHelper(layout, QTextOption::WrapAtWordBoundaryOrAnywhere);
622  doLayout(layout);
623 }
624 
625 
626 void ContentsChatItem::doLayout(QTextLayout *layout) const
627 {
629  if (!wrapList.count()) return; // empty chatitem
630 
631  qreal h = 0;
632  qreal spacing = qMax(fontMetrics()->lineSpacing(), fontMetrics()->height()); // cope with negative leading()
633  WrapColumnFinder finder(this);
634  layout->beginLayout();
635  forever {
636  QTextLine line = layout->createLine();
637  if (!line.isValid())
638  break;
639 
640  int col = finder.nextWrapColumn(width());
641  if (col < 0)
642  col = layout->text().length();
643  int num = col - line.textStart();
644 
645  line.setNumColumns(num);
646 
647  // Sometimes, setNumColumns will create a line that's too long (cf. Qt bug 238249)
648  // We verify this and try setting the width again, making it shorter each time until the lengths match.
649  // Dead fugly, but seems to work…
650  for (int i = line.textLength()-1; i >= 0 && line.textLength() > num; i--) {
651  line.setNumColumns(i);
652  }
653  if (num != line.textLength()) {
654  qWarning() << "WARNING: Layout engine couldn't workaround Qt bug 238249, please report!";
655  // qDebug() << num << line.textLength() << t.mid(line.textStart(), line.textLength()) << t.mid(line.textStart() + line.textLength());
656  }
657 
658  line.setPosition(QPointF(0, h));
659  h += spacing;
660  }
661  layout->endLayout();
662 }
663 
664 
665 Clickable ContentsChatItem::clickableAt(const QPointF &pos) const
666 {
668 }
669 
670 
672 {
674  for (int i = 0; i < privateData()->clickables.count(); i++) {
675  Clickable click = privateData()->clickables.at(i);
676  if (click.type() == Clickable::Url) {
677  overlayFormat(fmtList, click.start(), click.start() + click.length(), UiStyle::Url);
678  }
679  }
680  return fmtList;
681 }
682 
683 
684 QVector<QTextLayout::FormatRange> ContentsChatItem::additionalFormats() const
685 {
686  QVector<QTextLayout::FormatRange> fmt = ChatItem::additionalFormats();
687  // mark a clickable if hovered upon
688  if (privateData()->currentClickable.isValid()) {
690  QTextLayout::FormatRange f;
691  f.start = click.start();
692  f.length = click.length();
693  f.format.setFontUnderline(true);
694  fmt.append(f);
695  }
696  return fmt;
697 }
698 
699 
701 {
702  if (privateData()) {
703  if (privateData()->currentClickable.isValid()) {
704  chatLine()->unsetCursor();
706  }
707  clearWebPreview();
708  chatLine()->update();
709  }
710 }
711 
712 
713 void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode)
714 {
715  if (clickMode == ChatScene::SingleClick) {
716  qint16 idx = posToCursor(pos);
718  if (foo.isValid()) {
719  NetworkId networkId = Client::networkModel()->networkId(data(MessageModel::BufferIdRole).value<BufferId>());
720  QString text = data(ChatLineModel::DisplayRole).toString();
721  foo.activate(networkId, text);
722  }
723  }
724  else if (clickMode == ChatScene::DoubleClick) {
725  chatScene()->setSelectingItem(this);
727  Clickable click = clickableAt(pos);
728  if (click.isValid()) {
729  setSelectionStart(click.start());
730  setSelectionEnd(click.start() + click.length());
731  }
732  else {
733  // find word boundary
734  QString str = data(ChatLineModel::DisplayRole).toString();
735  qint16 cursor = posToCursor(pos);
736  qint16 start = str.lastIndexOf(QRegExp("\\W"), cursor) + 1;
737  qint16 end = qMin(str.indexOf(QRegExp("\\W"), cursor), str.length());
738  if (end < 0) end = str.length();
739  setSelectionStart(start);
740  setSelectionEnd(end);
741  }
742  chatLine()->update();
743  }
744  else if (clickMode == ChatScene::TripleClick) {
746  }
747  ChatItem::handleClick(pos, clickMode);
748 }
749 
750 
751 void ContentsChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
752 {
753  // mouse move events always mean we're not hovering anymore...
754  endHoverMode();
756 }
757 
758 
759 void ContentsChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
760 {
761  endHoverMode();
762  event->accept();
763 }
764 
765 
766 void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
767 {
768  bool onClickable = false;
769  Clickable click = clickableAt(event->pos());
770  if (click.isValid()) {
771  if (click.type() == Clickable::Url) {
772  onClickable = true;
773  showWebPreview(click);
774  }
775  else if (click.type() == Clickable::Channel) {
776  QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
777  // don't make clickable if it's our own name
779  if (Client::networkModel()->bufferName(myId) != name)
780  onClickable = true;
781  }
782  if (onClickable) {
783  chatLine()->setCursor(Qt::PointingHandCursor);
784  privateData()->currentClickable = click;
785  chatLine()->update();
786  return;
787  }
788  }
789  if (!onClickable) endHoverMode();
790  event->accept();
791 }
792 
793 
794 void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos)
795 {
796  if (privateData()->currentClickable.isValid()) {
798  switch (click.type()) {
799  case Clickable::Url:
800  privateData()->activeClickable = click;
801  menu->addAction(QIcon::fromTheme("edit-copy"), tr("Copy Link Address"),
802  &_actionProxy, SLOT(copyLinkToClipboard()))->setData(QVariant::fromValue<void *>(this));
803  break;
804  case Clickable::Channel:
805  {
806  // Remove existing menu actions, they confuse us when right-clicking on a clickable
807  menu->clear();
808  QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
809  GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value<BufferId>(), name);
810  break;
811  }
812  default:
813  break;
814  }
815  }
816  else {
817  // Buffer-specific actions
818  ChatItem::addActionsToMenu(menu, pos);
819  }
820 }
821 
822 
824 {
826  if (click.isValid() && click.type() == Clickable::Url) {
827  QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
828  if (!url.contains("://"))
829  url = "http://" + url;
831  }
832 }
833 
834 
835 /******** WEB PREVIEW *****************************************************************************/
836 
838 {
839 #ifndef HAVE_WEBKIT
840  Q_UNUSED(click);
841 #else
842  QTextLine line = layout()->lineForTextPosition(click.start());
843  qreal x = line.cursorToX(click.start());
844  qreal width = line.cursorToX(click.start() + click.length()) - x;
845  qreal height = line.height();
846  qreal y = height * line.lineNumber();
847 
848  QPointF topLeft = mapToScene(pos()) + QPointF(x, y);
849  QRectF urlRect = QRectF(topLeft.x(), topLeft.y(), width, height);
850 
851  QString urlstr = data(ChatLineModel::DisplayRole).toString().mid(click.start(), click.length());
852  if (!urlstr.contains("://"))
853  urlstr = "http://" + urlstr;
854  QUrl url = QUrl::fromEncoded(urlstr.toUtf8(), QUrl::TolerantMode);
855  chatScene()->loadWebPreview(this, url, urlRect);
856 #endif
857 }
858 
859 
861 {
862 #ifdef HAVE_WEBKIT
863  chatScene()->clearWebPreview(this);
864 #endif
865 }
866 
867 
868 /*************************************************************************************************/
869 
871  : item(_item),
872  wrapList(item->data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>()),
873  wordidx(0),
874  lineCount(0),
875  choppedTrailing(0)
876 {
877 }
878 
879 
881 {
882 }
883 
884 
886 {
887  if (wordidx >= wrapList.count())
888  return -1;
889 
890  lineCount++;
891  qreal targetWidth = lineCount * width + choppedTrailing;
892 
893  qint16 start = wordidx;
894  qint16 end = wrapList.count() - 1;
895 
896  // check if the whole line fits
897  if (wrapList.at(end).endX <= targetWidth) // || start == end)
898  return -1;
899 
900  // check if we have a very long word that needs inter word wrap
901  if (wrapList.at(start).endX > targetWidth) {
902  if (!line.isValid()) {
903  item->initLayoutHelper(&layout, QTextOption::NoWrap);
904  layout.beginLayout();
905  line = layout.createLine();
906  layout.endLayout();
907  }
908  return line.xToCursor(targetWidth, QTextLine::CursorOnCharacter);
909  }
910 
911  while (true) {
912  if (start + 1 == end) {
913  wordidx = end;
914  const ChatLineModel::Word &lastWord = wrapList.at(start); // the last word we were able to squeeze in
915 
916  // both cases should be cought preliminary
917  Q_ASSERT(lastWord.endX <= targetWidth); // ensure that "start" really fits in
918  Q_ASSERT(end < wrapList.count()); // ensure that start isn't the last word
919 
920  choppedTrailing += lastWord.trailing - (targetWidth - lastWord.endX);
921  return wrapList.at(wordidx).start;
922  }
923 
924  qint16 pivot = (end + start) / 2;
925  if (wrapList.at(pivot).endX > targetWidth) {
926  end = pivot;
927  }
928  else {
929  start = pivot;
930  }
931  }
932  Q_ASSERT(false);
933  return -1;
934 }
935 
936 
937 /*************************************************************************************************/