1 | /*************************************************************************** |
2 | * Copyright (C) 2005-2014 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 <QPainter> |
27 | #include <QPalette> |
28 | #include <QTextLayout> |
29 | #include <QMenu> |
30 | |
31 | #include "buffermodel.h" |
32 | #include "bufferview.h" |
33 | #include "chatitem.h" |
34 | #include "chatline.h" |
35 | #include "chatlinemodel.h" |
36 | #include "chatview.h" |
37 | #include "contextmenuactionprovider.h" |
38 | #include "iconloader.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 | |
53 | ChatItem::~ChatItem() |
54 | { |
55 | delete _cachedLayout; |
56 | } |
57 | |
58 | |
59 | ChatLine *ChatItem::chatLine() const |
60 | { |
61 | return _parent; |
62 | } |
63 | |
64 | |
65 | ChatScene *ChatItem::chatScene() const |
66 | { |
67 | return chatLine()->chatScene(); |
68 | } |
69 | |
70 | |
71 | ChatView *ChatItem::chatView() const |
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; |
131 | initLayout(_cachedLayout); |
132 | chatView()->setHasCache(chatLine()); |
133 | return _cachedLayout; |
134 | } |
135 | |
136 | |
137 | void ChatItem::clearCache() |
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 | |
180 | UiStyle::FormatList ChatItem::formatList() const |
181 | { |
182 | return data(MessageModel::FormatRole).value<UiStyle::FormatList>(); |
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; |
207 | if (_selectionMode == FullSelection) |
208 | bgBrush = data(ChatLineModel::SelectedBackgroundRole); |
209 | else |
210 | bgBrush = data(ChatLineModel::BackgroundRole); |
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 | |
321 | bool ChatItem::hasSelection() const |
322 | { |
323 | if (_selectionMode == NoSelection) |
324 | return false; |
325 | if (_selectionMode == FullSelection) |
326 | return true; |
327 | // partial |
328 | return _selectionStart != _selectionEnd; |
329 | } |
330 | |
331 | |
332 | QString ChatItem::selection() const |
333 | { |
334 | if (_selectionMode == FullSelection) |
335 | return data(MessageModel::DisplayRole).toString(); |
336 | if (_selectionMode == PartialSelection) |
337 | return data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, _selectionEnd), qAbs(_selectionStart - _selectionEnd)); |
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 | |
351 | void ChatItem::setFullSelection() |
352 | { |
353 | if (_selectionMode != FullSelection) { |
354 | _selectionMode = FullSelection; |
355 | chatLine()->update(); |
356 | } |
357 | } |
358 | |
359 | |
360 | void ChatItem::clearSelection() |
361 | { |
362 | if (_selectionMode != NoSelection) { |
363 | _selectionMode = NoSelection; |
364 | chatLine()->update(); |
365 | } |
366 | } |
367 | |
368 | |
369 | void ChatItem::continueSelecting(const QPointF &pos) |
370 | { |
371 | _selectionMode = PartialSelection; |
372 | _selectionEnd = posToCursor(pos); |
373 | chatLine()->update(); |
374 | } |
375 | |
376 | |
377 | bool ChatItem::isPosOverSelection(const QPointF &pos) const |
378 | { |
379 | if (_selectionMode == FullSelection) |
380 | return true; |
381 | if (_selectionMode == PartialSelection) { |
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); |
422 | _selectionStart = _selectionEnd = posToCursor(pos); |
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; |
436 | _selectionMode = (_selectionStart != _selectionEnd ? PartialSelection : NoSelection); |
437 | chatLine()->update(); |
438 | } |
439 | } |
440 | else { |
441 | setFullSelection(); |
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::(QMenu *, const QPointF &pos) |
473 | { |
474 | Q_UNUSED(pos); |
475 | |
476 | GraphicalUi::contextMenuActionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value<BufferId>()); |
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 | |
557 | ContentsChatItem::ActionProxy ContentsChatItem::_actionProxy; |
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 | |
574 | ContentsChatItem::~ContentsChatItem() |
575 | { |
576 | delete _data; |
577 | } |
578 | |
579 | |
580 | void ContentsChatItem::clearCache() |
581 | { |
582 | delete _data; |
583 | _data = 0; |
584 | ChatItem::clearCache(); |
585 | } |
586 | |
587 | |
588 | ContentsChatItemPrivate *ContentsChatItem::privateData() const |
589 | { |
590 | if (!_data) { |
591 | ContentsChatItem *that = const_cast<ContentsChatItem *>(this); |
592 | that->_data = new ContentsChatItemPrivate(ClickableList::fromString(data(ChatLineModel::DisplayRole).toString()), that); |
593 | } |
594 | return _data; |
595 | } |
596 | |
597 | |
598 | qreal ContentsChatItem::setGeometryByWidth(qreal w) |
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 | { |
628 | ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>(); |
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 | { |
667 | return privateData()->clickables.atCursorPos(posToCursor(pos)); |
668 | } |
669 | |
670 | |
671 | UiStyle::FormatList ContentsChatItem::formatList() const |
672 | { |
673 | UiStyle::FormatList fmtList = ChatItem::formatList(); |
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()) { |
689 | Clickable click = privateData()->currentClickable; |
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 | |
700 | void ContentsChatItem::endHoverMode() |
701 | { |
702 | if (privateData()) { |
703 | if (privateData()->currentClickable.isValid()) { |
704 | chatLine()->unsetCursor(); |
705 | privateData()->currentClickable = Clickable(); |
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); |
717 | Clickable foo = privateData()->clickables.atCursorPos(idx); |
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); |
726 | setSelectionMode(PartialSelection); |
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) { |
745 | setSelection(PartialSelection, 0, data(ChatLineModel::DisplayRole).toString().length()); |
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(); |
755 | ChatItem::mouseMoveEvent(event); |
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 |
778 | BufferId myId = data(MessageModel::BufferIdRole).value<BufferId>(); |
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::(QMenu *, const QPointF &pos) |
795 | { |
796 | if (privateData()->currentClickable.isValid()) { |
797 | Clickable click = privateData()->currentClickable; |
798 | switch (click.type()) { |
799 | case Clickable::Url: |
800 | privateData()->activeClickable = click; |
801 | menu->addAction(SmallIcon("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 | |
823 | void ContentsChatItem::copyLinkToClipboard() |
824 | { |
825 | Clickable click = privateData()->activeClickable; |
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; |
830 | chatScene()->stringToClipboard(url); |
831 | } |
832 | } |
833 | |
834 | |
835 | /******** WEB PREVIEW *****************************************************************************/ |
836 | |
837 | void ContentsChatItem::showWebPreview(const Clickable &click) |
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 | |
860 | void ContentsChatItem::clearWebPreview() |
861 | { |
862 | #ifdef HAVE_WEBKIT |
863 | chatScene()->clearWebPreview(this); |
864 | #endif |
865 | } |
866 | |
867 | |
868 | /*************************************************************************************************/ |
869 | |
870 | ContentsChatItem::WrapColumnFinder::WrapColumnFinder(const ChatItem *_item) |
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 | |
880 | ContentsChatItem::WrapColumnFinder::~WrapColumnFinder() |
881 | { |
882 | } |
883 | |
884 | |
885 | qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn(qreal width) |
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 | /*************************************************************************************************/ |
938 | |