1 /***************************************************************************
2 * Copyright (C) 2005-2013 by the Quassel Project *
3 * devel@quassel-irc.org *
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. *
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. *
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 ***************************************************************************/
21 #include <QApplication>
24 #include <QGraphicsSceneMouseEvent>
28 #include <QPersistentModelIndex>
42 #include "chatlinemodelitem.h"
43 #include "chatscene.h"
46 #include "clientbacklogmanager.h"
47 #include "columnhandleitem.h"
48 #include "contextmenuactionprovider.h"
49 #include "iconloader.h"
51 #include "markerlineitem.h"
52 #include "messagefilter.h"
54 #include "qtuistyle.h"
55 #include "chatviewsettings.h"
56 #include "webpreviewitem.h"
58 const qreal minContentsWidth = 200;
60 ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal width, ChatView *parent)
61 : QGraphicsScene(0, 0, width, 0, (QObject *)parent),
65 _singleBufferId(BufferId()),
66 _sceneRect(0, 0, width, 0),
69 _markerLine(new MarkerLineItem(width)),
70 _markerLineVisible(false),
71 _markerLineValid(false),
72 _markerLineJumpPending(false),
73 _cutoffMode(CutoffRight),
79 _leftButtonPressed(false)
81 MessageFilter *filter = qobject_cast<MessageFilter *>(model);
82 if (filter && filter->isSingleBufferFilter()) {
83 _singleBufferId = filter->singleBufferId();
87 connect(this, SIGNAL(sceneRectChanged(const QRectF &)), _markerLine, SLOT(sceneRectChanged(const QRectF &)));
89 ChatViewSettings defaultSettings;
90 int defaultFirstColHandlePos = defaultSettings.value("FirstColumnHandlePos", 80).toInt();
91 int defaultSecondColHandlePos = defaultSettings.value("SecondColumnHandlePos", 200).toInt();
93 ChatViewSettings viewSettings(this);
94 _firstColHandlePos = viewSettings.value("FirstColumnHandlePos", defaultFirstColHandlePos).toInt();
95 _secondColHandlePos = viewSettings.value("SecondColumnHandlePos", defaultSecondColHandlePos).toInt();
97 _firstColHandle = new ColumnHandleItem(QtUi::style()->firstColumnSeparator());
98 addItem(_firstColHandle);
99 _firstColHandle->setXPos(_firstColHandlePos);
100 connect(_firstColHandle, SIGNAL(positionChanged(qreal)), this, SLOT(firstHandlePositionChanged(qreal)));
101 connect(this, SIGNAL(sceneRectChanged(const QRectF &)), _firstColHandle, SLOT(sceneRectChanged(const QRectF &)));
103 _secondColHandle = new ColumnHandleItem(QtUi::style()->secondColumnSeparator());
104 addItem(_secondColHandle);
105 _secondColHandle->setXPos(_secondColHandlePos);
106 connect(_secondColHandle, SIGNAL(positionChanged(qreal)), this, SLOT(secondHandlePositionChanged(qreal)));
108 connect(this, SIGNAL(sceneRectChanged(const QRectF &)), _secondColHandle, SLOT(sceneRectChanged(const QRectF &)));
112 if (model->rowCount() > 0)
113 rowsInserted(QModelIndex(), 0, model->rowCount() - 1);
115 connect(model, SIGNAL(rowsInserted(const QModelIndex &, int, int)),
116 this, SLOT(rowsInserted(const QModelIndex &, int, int)));
117 connect(model, SIGNAL(rowsAboutToBeRemoved(const QModelIndex &, int, int)),
118 this, SLOT(rowsAboutToBeRemoved(const QModelIndex &, int, int)));
119 connect(model, SIGNAL(rowsRemoved(QModelIndex, int, int)),
120 this, SLOT(rowsRemoved()));
121 connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), SLOT(dataChanged(QModelIndex, QModelIndex)));
124 webPreview.timer.setSingleShot(true);
125 connect(&webPreview.timer, SIGNAL(timeout()), this, SLOT(webPreviewNextStep()));
127 _showWebPreview = defaultSettings.showWebPreview();
128 defaultSettings.notify("ShowWebPreview", this, SLOT(showWebPreviewChanged()));
130 _clickTimer.setInterval(QApplication::doubleClickInterval());
131 _clickTimer.setSingleShot(true);
132 connect(&_clickTimer, SIGNAL(timeout()), SLOT(clickTimeout()));
134 setItemIndexMethod(QGraphicsScene::NoIndex);
138 ChatScene::~ChatScene()
143 ChatView *ChatScene::chatView() const
149 ColumnHandleItem *ChatScene::firstColumnHandle() const
151 return _firstColHandle;
155 ColumnHandleItem *ChatScene::secondColumnHandle() const
157 return _secondColHandle;
161 ChatLine *ChatScene::chatLine(MsgId msgId, bool matchExact, bool ignoreDayChange) const
166 QList<ChatLine *>::ConstIterator start = _lines.begin();
167 QList<ChatLine *>::ConstIterator end = _lines.end();
168 QList<ChatLine *>::ConstIterator middle;
170 int n = int(end - start);
175 middle = start + half;
176 if ((*middle)->msgId() < msgId) {
185 if (start != end && (*start)->msgId() == msgId && (ignoreDayChange ? (*start)->msgType() != Message::DayChange : true))
191 if (start == _lines.begin()) // not (yet?) in our scene
194 // if we didn't find the exact msgId, take the next-lower one (this makes sense for lastSeen)
196 if (start == end) { // higher than last element
197 if (!ignoreDayChange)
198 return _lines.last();
200 for (int i = _lines.count() -1; i >= 0; i--) {
201 if (_lines.at(i)->msgType() != Message::DayChange)
207 // return the next-lower line
208 if (!ignoreDayChange)
212 if ((*(--start))->msgType() != Message::DayChange)
215 while (start != _lines.begin());
220 ChatItem *ChatScene::chatItemAt(const QPointF &scenePos) const
222 foreach(QGraphicsItem *item, items(scenePos, Qt::IntersectsItemBoundingRect, Qt::AscendingOrder)) {
223 ChatLine *line = qgraphicsitem_cast<ChatLine *>(item);
225 return line->itemAt(line->mapFromScene(scenePos));
231 bool ChatScene::containsBuffer(const BufferId &id) const
233 MessageFilter *filter = qobject_cast<MessageFilter *>(model());
235 return filter->containsBuffer(id);
241 void ChatScene::setMarkerLineVisible(bool visible)
243 _markerLineVisible = visible;
244 if (visible && _markerLineValid)
245 markerLine()->setVisible(true);
247 markerLine()->setVisible(false);
251 void ChatScene::setMarkerLine(MsgId msgId)
253 if (!isSingleBufferScene())
256 if (!msgId.isValid())
257 msgId = Client::markerLine(singleBufferId());
259 if (msgId.isValid()) {
260 ChatLine *line = chatLine(msgId, false, true);
262 markerLine()->setChatLine(line);
263 // if this was the last line, we won't see it because it's outside the sceneRect
264 // .. which is exactly what we want :)
265 markerLine()->setPos(line->pos() + QPointF(0, line->height()));
267 // DayChange messages might have been hidden outside the scene rect, don't make the markerline visible then!
268 if (markerLine()->pos().y() >= sceneRect().y()) {
269 _markerLineValid = true;
270 if (_markerLineVisible)
271 markerLine()->setVisible(true);
272 if (_markerLineJumpPending) {
273 _markerLineJumpPending = false;
274 if (markerLine()->isVisible()) {
275 markerLine()->ensureVisible(QRectF(), 50, 50);
282 _markerLineValid = false;
283 markerLine()->setVisible(false);
287 void ChatScene::jumpToMarkerLine(bool requestBacklog)
289 if (!isSingleBufferScene())
292 if (markerLine()->isVisible()) {
293 markerLine()->ensureVisible(QRectF(), 50, 50);
296 if (!_markerLineValid && requestBacklog) {
297 MsgId msgId = Client::markerLine(singleBufferId());
298 if (msgId.isValid()) {
299 _markerLineJumpPending = true;
300 Client::backlogManager()->requestBacklog(singleBufferId(), msgId, -1, -1, 0);
302 // If we filtered out the lastSeenMsg (by changing filters after setting it), we'd never jump because the above request
303 // won't fetch any prior lines. Thus, trigger a dynamic backlog request just in case, so repeated
304 // jump tries will eventually cause enough backlog to be fetched.
305 // This is a bit hackish, but not wasteful, as jumping to the top of the ChatView would trigger a dynamic fetch anyway.
306 this->requestBacklog();
312 void ChatScene::rowsInserted(const QModelIndex &index, int start, int end)
316 // QModelIndex sidx = model()->index(start, 2);
317 // QModelIndex eidx = model()->index(end, 2);
318 // qDebug() << "rowsInserted:";
320 // QModelIndex ssidx = model()->index(start - 1, 2);
321 // qDebug() << "Start--:" << start - 1 << ssidx.data(MessageModel::MsgIdRole).value<MsgId>()
322 // << ssidx.data(Qt::DisplayRole).toString();
324 // qDebug() << "Start:" << start << sidx.data(MessageModel::MsgIdRole).value<MsgId>()
325 // << sidx.data(Qt::DisplayRole).toString();
326 // qDebug() << "End:" << end << eidx.data(MessageModel::MsgIdRole).value<MsgId>()
327 // << eidx.data(Qt::DisplayRole).toString();
328 // if(end + 1 < model()->rowCount()) {
329 // QModelIndex eeidx = model()->index(end + 1, 2);
330 // qDebug() << "End++:" << end + 1 << eeidx.data(MessageModel::MsgIdRole).value<MsgId>()
331 // << eeidx.data(Qt::DisplayRole).toString();
336 qreal width = _sceneRect.width();
337 bool atBottom = (start == _lines.count());
338 bool atTop = !atBottom && (start == 0);
340 if (start < _lines.count()) {
341 y = _lines.value(start)->y();
343 else if (atBottom && !_lines.isEmpty()) {
344 y = _lines.last()->y() + _lines.last()->height();
347 qreal contentsWidth = width - secondColumnHandle()->sceneRight();
348 qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
349 qreal timestampWidth = firstColumnHandle()->sceneLeft();
350 QPointF contentsPos(secondColumnHandle()->sceneRight(), 0);
351 QPointF senderPos(firstColumnHandle()->sceneRight(), 0);
354 for (int i = end; i >= start; i--) {
355 ChatLine *line = new ChatLine(i, model(),
357 timestampWidth, senderWidth, contentsWidth,
358 senderPos, contentsPos);
360 line->setPos(0, y-h);
361 _lines.insert(start, line);
366 for (int i = start; i <= end; i++) {
367 ChatLine *line = new ChatLine(i, model(),
369 timestampWidth, senderWidth, contentsWidth,
370 senderPos, contentsPos);
371 line->setPos(0, y+h);
373 _lines.insert(i, line);
378 // update existing items
379 for (int i = end+1; i < _lines.count(); i++) {
380 _lines[i]->setRow(i);
384 if (_selectionStart >= 0) {
385 int offset = end - start + 1;
386 int oldStart = _selectionStart;
387 if (_selectionStart >= start)
388 _selectionStart += offset;
389 if (_selectionEnd >= start) {
390 _selectionEnd += offset;
391 if (_selectionStart == oldStart)
392 for (int i = start; i < start + offset; i++)
393 _lines[i]->setSelected(true);
395 if (_firstSelectionRow >= start)
396 _firstSelectionRow += offset;
399 // neither pre- or append means we have to do dirty work: move items...
400 if (!(atTop || atBottom)) {
402 for (int i = 0; i <= end; i++) {
404 line->setPos(0, line->pos().y() - h);
405 if (line == markerLine()->chatLine())
406 markerLine()->setPos(line->pos() + QPointF(0, line->height()));
410 // check if all went right
411 Q_ASSERT(start == 0 || _lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() == _lines.at(start)->pos().y());
413 // if(_lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() != _lines.at(start)->pos().y()) {
414 // qDebug() << "lines:" << _lines.count() << "start:" << start << "end:" << end;
415 // qDebug() << "line[start - 1]:" << _lines.at(start - 1)->pos().y() << "+" << _lines.at(start - 1)->height() << "=" << _lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height();
416 // qDebug() << "line[start]" << _lines.at(start)->pos().y();
417 // qDebug() << "needed moving:" << !(atTop || atBottom) << moveTop << moveStart << moveEnd << offset;
421 Q_ASSERT(end + 1 == _lines.count() || _lines.at(end)->pos().y() + _lines.at(end)->height() == _lines.at(end + 1)->pos().y());
422 // if(end + 1 < _lines.count()) {
423 // if(_lines.at(end)->pos().y() + _lines.at(end)->height() != _lines.at(end + 1)->pos().y()) {
424 // qDebug() << "lines:" << _lines.count() << "start:" << start << "end:" << end;
425 // qDebug() << "line[end]:" << _lines.at(end)->pos().y() << "+" << _lines.at(end)->height() << "=" << _lines.at(end)->pos().y() + _lines.at(end)->height();
426 // qDebug() << "line[end+1]" << _lines.at(end + 1)->pos().y();
427 // qDebug() << "needed moving:" << !(atTop || atBottom) << moveTop << moveStart << moveEnd << offset;
433 if (start < _firstLineRow) {
434 int prevFirstLineRow = _firstLineRow + (end - start + 1);
435 for (int i = end + 1; i < prevFirstLineRow; i++) {
436 _lines.at(i)->show();
439 // force new search for first proper line
444 emit lastLineChanged(_lines.last(), h);
447 // now move the marker line if necessary. we don't need to do anything if we appended lines though...
448 if (!_markerLineValid)
453 void ChatScene::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
457 qreal h = 0; // total height of removed items;
459 bool atTop = (start == 0);
460 bool atBottom = (end == _lines.count() - 1);
463 if (_selectingItem) {
464 int row = _selectingItem->row();
465 if (row >= start && row <= end)
469 // remove items from scene
470 QList<ChatLine *>::iterator lineIter = _lines.begin() + start;
471 int lineCount = start;
472 while (lineIter != _lines.end() && lineCount <= end) {
473 if ((*lineIter) == markerLine()->chatLine())
474 markerLine()->setChatLine(0);
475 h += (*lineIter)->height();
477 lineIter = _lines.erase(lineIter);
481 // update rows of remaining chatlines
482 for (int i = start; i < _lines.count(); i++) {
483 _lines.at(i)->setRow(i);
487 if (_selectionStart >= 0) {
488 int offset = end - start + 1;
489 if (_selectionStart >= start)
490 _selectionStart = qMax(_selectionStart - offset, start);
491 if (_selectionEnd >= start)
492 _selectionEnd -= offset;
493 if (_firstSelectionRow >= start)
494 _firstSelectionRow -= offset;
496 if (_selectionEnd < _selectionStart) {
497 _isSelecting = false;
498 _selectionStart = -1;
502 // neither removing at bottom or top means we have to move items...
503 if (!(atTop || atBottom)) {
506 int moveEnd = _lines.count() - 1;
507 if (start < _lines.count() - start) {
517 for (int i = moveStart; i <= moveEnd; i++) {
519 line->setPos(0, line->pos().y() + offset);
523 Q_ASSERT(start == 0 || start >= _lines.count() || _lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() == _lines.at(start)->pos().y());
526 // when searching for the first non-date-line we have to take into account that our
527 // model still contains the just removed lines so we cannot simply call updateSceneRect()
528 int numRows = model()->rowCount();
529 QModelIndex firstLineIdx;
531 bool needOffset = false;
534 if (_firstLineRow >= start && _firstLineRow <= end) {
535 _firstLineRow = end + 1;
538 firstLineIdx = model()->index(_firstLineRow, 0);
540 while ((Message::Type)(model()->data(firstLineIdx, MessageModel::TypeRole).toInt()) == Message::DayChange && _firstLineRow < numRows);
543 _firstLineRow -= end - start + 1;
548 void ChatScene::rowsRemoved()
550 // move the marker line if necessary
555 void ChatScene::dataChanged(const QModelIndex &tl, const QModelIndex &br)
557 layout(tl.row(), br.row(), _sceneRect.width());
561 void ChatScene::updateForViewport(qreal width, qreal height)
563 _viewportHeight = height;
568 void ChatScene::setWidth(qreal width)
570 if (width == _sceneRect.width())
572 layout(0, _lines.count()-1, width);
576 void ChatScene::layout(int start, int end, qreal width)
578 // clock_t startT = clock();
580 // disabling the index while doing this complex updates is about
581 // 2 to 10 times faster!
582 //setItemIndexMethod(QGraphicsScene::NoIndex);
586 qreal linePos = _lines.at(row)->scenePos().y() + _lines.at(row)->height();
587 qreal contentsWidth = width - secondColumnHandle()->sceneRight();
588 while (row >= start) {
589 _lines.at(row--)->setGeometryByWidth(width, contentsWidth, linePos);
593 // remaining items don't need geometry changes, but maybe repositioning?
594 ChatLine *line = _lines.at(row);
595 qreal offset = linePos - (line->scenePos().y() + line->height());
598 line = _lines.at(row--);
599 line->setPos(0, line->scenePos().y() + offset);
605 //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
607 updateSceneRect(width);
610 emit layoutChanged();
612 // clock_t endT = clock();
613 // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
617 void ChatScene::firstHandlePositionChanged(qreal xpos)
619 if (_firstColHandlePos == xpos)
622 _firstColHandlePos = xpos >= 0 ? xpos : 0;
623 ChatViewSettings viewSettings(this);
624 viewSettings.setValue("FirstColumnHandlePos", _firstColHandlePos);
625 ChatViewSettings defaultSettings;
626 defaultSettings.setValue("FirstColumnHandlePos", _firstColHandlePos);
628 // clock_t startT = clock();
630 // disabling the index while doing this complex updates is about
631 // 2 to 10 times faster!
632 //setItemIndexMethod(QGraphicsScene::NoIndex);
634 QList<ChatLine *>::iterator lineIter = _lines.end();
635 QList<ChatLine *>::iterator lineIterBegin = _lines.begin();
636 qreal timestampWidth = firstColumnHandle()->sceneLeft();
637 qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
638 QPointF senderPos(firstColumnHandle()->sceneRight(), 0);
640 while (lineIter != lineIterBegin) {
642 (*lineIter)->setFirstColumn(timestampWidth, senderWidth, senderPos);
644 //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
648 // clock_t endT = clock();
649 // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
653 void ChatScene::secondHandlePositionChanged(qreal xpos)
655 if (_secondColHandlePos == xpos)
658 _secondColHandlePos = xpos;
659 ChatViewSettings viewSettings(this);
660 viewSettings.setValue("SecondColumnHandlePos", _secondColHandlePos);
661 ChatViewSettings defaultSettings;
662 defaultSettings.setValue("SecondColumnHandlePos", _secondColHandlePos);
664 // clock_t startT = clock();
666 // disabling the index while doing this complex updates is about
667 // 2 to 10 times faster!
668 //setItemIndexMethod(QGraphicsScene::NoIndex);
670 QList<ChatLine *>::iterator lineIter = _lines.end();
671 QList<ChatLine *>::iterator lineIterBegin = _lines.begin();
672 qreal linePos = _sceneRect.y() + _sceneRect.height();
673 qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
674 qreal contentsWidth = _sceneRect.width() - secondColumnHandle()->sceneRight();
675 QPointF contentsPos(secondColumnHandle()->sceneRight(), 0);
676 while (lineIter != lineIterBegin) {
678 (*lineIter)->setSecondColumn(senderWidth, contentsWidth, contentsPos, linePos);
680 //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
684 emit layoutChanged();
686 // clock_t endT = clock();
687 // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
691 void ChatScene::setHandleXLimits()
693 _firstColHandle->setXLimits(0, _secondColHandle->sceneLeft());
694 _secondColHandle->setXLimits(_firstColHandle->sceneRight(), width() - minContentsWidth);
699 void ChatScene::setSelectingItem(ChatItem *item)
701 if (_selectingItem) _selectingItem->clearSelection();
702 _selectingItem = item;
706 void ChatScene::startGlobalSelection(ChatItem *item, const QPointF &itemPos)
708 _selectionStart = _selectionEnd = _firstSelectionRow = item->row();
709 _selectionStartCol = _selectionMinCol = item->column();
711 _lines[_selectionStart]->setSelected(true, (ChatLineModel::ColumnType)_selectionMinCol);
712 updateSelection(item->mapToScene(itemPos));
716 void ChatScene::updateSelection(const QPointF &pos)
718 int curRow = rowByScenePos(pos);
719 if (curRow < 0) return;
720 int curColumn = (int)columnByScenePos(pos);
721 ChatLineModel::ColumnType minColumn = (ChatLineModel::ColumnType)qMin(curColumn, _selectionStartCol);
722 if (minColumn != _selectionMinCol) {
723 _selectionMinCol = minColumn;
724 for (int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++) {
725 _lines[l]->setSelected(true, minColumn);
728 int newstart = qMin(curRow, _firstSelectionRow);
729 int newend = qMax(curRow, _firstSelectionRow);
730 if (newstart < _selectionStart) {
731 for (int l = newstart; l < _selectionStart; l++)
732 _lines[l]->setSelected(true, minColumn);
734 if (newstart > _selectionStart) {
735 for (int l = _selectionStart; l < newstart; l++)
736 _lines[l]->setSelected(false);
738 if (newend > _selectionEnd) {
739 for (int l = _selectionEnd+1; l <= newend; l++)
740 _lines[l]->setSelected(true, minColumn);
742 if (newend < _selectionEnd) {
743 for (int l = newend+1; l <= _selectionEnd; l++)
744 _lines[l]->setSelected(false);
747 _selectionStart = newstart;
748 _selectionEnd = newend;
750 if (newstart == newend && minColumn == ChatLineModel::ContentsColumn) {
751 if (!_selectingItem) {
752 // _selectingItem has been removed already
755 _lines[curRow]->setSelected(false);
756 _isSelecting = false;
757 _selectionStart = -1;
758 _selectingItem->continueSelecting(_selectingItem->mapFromScene(pos));
763 bool ChatScene::isPosOverSelection(const QPointF &pos) const
765 ChatItem *chatItem = chatItemAt(pos);
768 if (hasGlobalSelection()) {
769 int row = chatItem->row();
770 if (row >= qMin(_selectionStart, _selectionEnd) && row <= qMax(_selectionStart, _selectionEnd))
771 return columnByScenePos(pos) >= _selectionMinCol;
774 return chatItem->isPosOverSelection(chatItem->mapFromScene(pos));
780 bool ChatScene::isScrollingAllowed() const
785 // TODO: Handle clicks and single-item selections too
791 /******** MOUSE HANDLING **************************************************************************/
793 void ChatScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
795 QPointF pos = event->scenePos();
798 // zoom actions and similar
799 chatView()->addActionsToMenu(&menu, pos);
802 if (isPosOverSelection(pos))
803 menu.addAction(SmallIcon("edit-copy"), tr("Copy Selection"),
804 this, SLOT(selectionToClipboard()),
807 // item-specific options (select link etc)
808 ChatItem *item = chatItemAt(pos);
810 item->addActionsToMenu(&menu, item->mapFromScene(pos));
812 // no item -> default scene actions
813 GraphicalUi::contextMenuActionProvider()->addActions(&menu, filter(), BufferId());
815 if (QtUi::mainWindow()->menuBar()->isHidden())
816 menu.addAction(QtUi::actionCollection("General")->action("ToggleMenuBar"));
818 menu.exec(event->screenPos());
822 void ChatScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
824 if (event->buttons() == Qt::LeftButton) {
825 if (!_clickHandled && (event->scenePos() - _clickPos).toPoint().manhattanLength() >= QApplication::startDragDistance()) {
826 if (_clickTimer.isActive())
828 if (_clickMode == SingleClick && isPosOverSelection(_clickPos))
829 initiateDrag(event->widget());
831 _clickMode = DragStartClick;
832 handleClick(Qt::LeftButton, _clickPos);
834 _clickMode = NoClick;
837 updateSelection(event->scenePos());
838 emit mouseMoveWhileSelecting(event->scenePos());
841 else if (_clickHandled && _clickMode < DoubleClick)
842 QGraphicsScene::mouseMoveEvent(event);
845 QGraphicsScene::mouseMoveEvent(event);
849 void ChatScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
851 if (event->buttons() == Qt::LeftButton) {
852 _leftButtonPressed = true;
853 _clickHandled = false;
854 if (!isPosOverSelection(event->scenePos())) {
855 // immediately clear selection if clicked outside; otherwise, wait for potential drag
858 if (_clickMode != NoClick && _clickTimer.isActive()) {
859 switch (_clickMode) {
861 _clickMode = SingleClick; break;
863 _clickMode = DoubleClick; break;
865 _clickMode = TripleClick; break;
867 _clickMode = DoubleClick; break;
871 handleClick(Qt::LeftButton, _clickPos);
874 _clickMode = SingleClick;
875 _clickPos = event->scenePos();
879 if (event->type() == QEvent::GraphicsSceneMouseDoubleClick)
880 QGraphicsScene::mouseDoubleClickEvent(event);
882 QGraphicsScene::mousePressEvent(event);
886 void ChatScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
888 // we check for doubleclick ourselves, so just call press handler
889 mousePressEvent(event);
893 void ChatScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
895 if (event->button() == Qt::LeftButton && _leftButtonPressed) {
896 _leftButtonPressed = false;
897 if (_clickMode != NoClick) {
898 if (_clickMode == SingleClick)
901 if (!_clickTimer.isActive())
902 handleClick(Qt::LeftButton, _clickPos);
905 // no click -> drag or selection move
906 if (isGloballySelecting()) {
907 selectionToClipboard(QClipboard::Selection);
908 _isSelecting = false;
914 QGraphicsScene::mouseReleaseEvent(event);
918 void ChatScene::clickTimeout()
920 if (!_leftButtonPressed && _clickMode == SingleClick)
921 handleClick(Qt::LeftButton, _clickPos);
925 void ChatScene::handleClick(Qt::MouseButton button, const QPointF &scenePos)
927 if (button == Qt::LeftButton) {
930 // Now send click down to items
931 ChatItem *chatItem = chatItemAt(scenePos);
933 chatItem->handleClick(chatItem->mapFromScene(scenePos), _clickMode);
935 _clickHandled = true;
940 void ChatScene::initiateDrag(QWidget *source)
942 QDrag *drag = new QDrag(source);
943 QMimeData *mimeData = new QMimeData;
944 mimeData->setText(selection());
945 drag->setMimeData(mimeData);
947 drag->exec(Qt::CopyAction);
951 /******** SELECTIONS ******************************************************************************/
953 void ChatScene::selectionToClipboard(QClipboard::Mode mode)
958 stringToClipboard(selection(), mode);
962 void ChatScene::stringToClipboard(const QString &str_, QClipboard::Mode mode)
965 // remove trailing linefeeds
966 if (str.endsWith('\n'))
970 case QClipboard::Clipboard:
971 QApplication::clipboard()->setText(str);
973 case QClipboard::Selection:
974 if (QApplication::clipboard()->supportsSelection())
975 QApplication::clipboard()->setText(str, QClipboard::Selection);
983 //!\brief Convert current selection to human-readable string.
984 QString ChatScene::selection() const
986 //TODO Make selection format configurable!
987 if (hasGlobalSelection()) {
988 int start = qMin(_selectionStart, _selectionEnd);
989 int end = qMax(_selectionStart, _selectionEnd);
990 if (start < 0 || end >= _lines.count()) {
991 qDebug() << "Invalid selection range:" << start << end;
995 for (int l = start; l <= end; l++) {
996 if (_selectionMinCol == ChatLineModel::TimestampColumn)
997 result += _lines[l]->item(ChatLineModel::TimestampColumn)->data(MessageModel::DisplayRole).toString() + " ";
998 if (_selectionMinCol <= ChatLineModel::SenderColumn)
999 result += _lines[l]->item(ChatLineModel::SenderColumn)->data(MessageModel::DisplayRole).toString() + " ";
1000 result += _lines[l]->item(ChatLineModel::ContentsColumn)->data(MessageModel::DisplayRole).toString() + "\n";
1004 else if (selectingItem())
1005 return selectingItem()->selection();
1010 bool ChatScene::hasSelection() const
1012 return hasGlobalSelection() || (selectingItem() && selectingItem()->hasSelection());
1016 bool ChatScene::hasGlobalSelection() const
1018 return _selectionStart >= 0;
1022 bool ChatScene::isGloballySelecting() const
1024 return _isSelecting;
1028 void ChatScene::clearGlobalSelection()
1030 if (hasGlobalSelection()) {
1031 for (int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++)
1032 _lines[l]->setSelected(false);
1033 _isSelecting = false;
1034 _selectionStart = -1;
1039 void ChatScene::clearSelection()
1041 clearGlobalSelection();
1042 if (selectingItem())
1043 selectingItem()->clearSelection();
1047 /******** *************************************************************************************/
1049 void ChatScene::requestBacklog()
1051 MessageFilter *filter = qobject_cast<MessageFilter *>(model());
1053 return filter->requestBacklog();
1058 ChatLineModel::ColumnType ChatScene::columnByScenePos(qreal x) const
1060 if (x < _firstColHandle->x())
1061 return ChatLineModel::TimestampColumn;
1062 if (x < _secondColHandle->x())
1063 return ChatLineModel::SenderColumn;
1065 return ChatLineModel::ContentsColumn;
1069 int ChatScene::rowByScenePos(qreal y) const
1071 QList<QGraphicsItem *> itemList = items(QPointF(0, y));
1073 // ChatLine should be at the bottom of the list
1074 for (int i = itemList.count()-1; i >= 0; i--) {
1075 ChatLine *line = qgraphicsitem_cast<ChatLine *>(itemList.at(i));
1083 void ChatScene::updateSceneRect(qreal width)
1085 if (_lines.isEmpty()) {
1086 updateSceneRect(QRectF(0, 0, width, 0));
1090 // we hide day change messages at the top by making the scene rect smaller
1091 // and by calling QGraphicsItem::hide() on all leading day change messages
1092 // the first one is needed to ensure proper scrollbar ranges
1093 // the second for cases where the viewport is larger then the set scenerect
1094 // (in this case the items are shown anyways)
1095 if (_firstLineRow == -1) {
1096 int numRows = model()->rowCount();
1098 QModelIndex firstLineIdx;
1099 while (_firstLineRow < numRows) {
1100 firstLineIdx = model()->index(_firstLineRow, 0);
1101 if ((Message::Type)(model()->data(firstLineIdx, MessageModel::TypeRole).toInt()) != Message::DayChange)
1103 _lines.at(_firstLineRow)->hide();
1108 // the following call should be safe. If it crashes something went wrong during insert/remove
1109 if (_firstLineRow < _lines.count()) {
1110 ChatLine *firstLine = _lines.at(_firstLineRow);
1111 ChatLine *lastLine = _lines.last();
1112 updateSceneRect(QRectF(0, firstLine->pos().y(), width, lastLine->pos().y() + lastLine->height() - firstLine->pos().y()));
1116 updateSceneRect(QRectF(0, 0, width, 0));
1121 void ChatScene::updateSceneRect(const QRectF &rect)
1129 // ========================================
1130 // Webkit Only stuff
1131 // ========================================
1133 void ChatScene::loadWebPreview(ChatItem *parentItem, const QUrl &url, const QRectF &urlRect)
1135 if (!_showWebPreview)
1138 if (webPreview.urlRect != urlRect)
1139 webPreview.urlRect = urlRect;
1141 if (webPreview.parentItem != parentItem)
1142 webPreview.parentItem = parentItem;
1144 if (webPreview.url != url) {
1145 webPreview.url = url;
1146 // prepare to load a different URL
1147 if (webPreview.previewItem) {
1148 if (webPreview.previewItem->scene())
1149 removeItem(webPreview.previewItem);
1150 delete webPreview.previewItem;
1151 webPreview.previewItem = 0;
1153 webPreview.previewState = WebPreview::NoPreview;
1156 if (webPreview.url.isEmpty())
1159 // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1160 switch (webPreview.previewState) {
1161 case WebPreview::NoPreview:
1162 webPreview.previewState = WebPreview::NewPreview;
1163 webPreview.timer.start(500);
1165 case WebPreview::NewPreview:
1166 case WebPreview::DelayPreview:
1167 case WebPreview::ShowPreview:
1168 // we're already waiting for the next step or showing the preview
1170 case WebPreview::HidePreview:
1171 // we still have a valid preview
1172 webPreview.previewState = WebPreview::DelayPreview;
1173 webPreview.timer.start(1000);
1176 // qDebug() << " new State:" << webPreview.previewState << webPreview.timer.isActive();
1180 void ChatScene::webPreviewNextStep()
1182 // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1183 switch (webPreview.previewState) {
1184 case WebPreview::NoPreview:
1186 case WebPreview::NewPreview:
1187 Q_ASSERT(!webPreview.previewItem);
1188 webPreview.previewItem = new WebPreviewItem(webPreview.url);
1189 webPreview.previewState = WebPreview::DelayPreview;
1190 webPreview.timer.start(1000);
1192 case WebPreview::DelayPreview:
1193 Q_ASSERT(webPreview.previewItem);
1194 // calc position and show
1196 qreal previewY = webPreview.urlRect.bottom();
1197 qreal previewX = webPreview.urlRect.x();
1198 if (previewY + webPreview.previewItem->boundingRect().height() > sceneRect().bottom())
1199 previewY = webPreview.urlRect.y() - webPreview.previewItem->boundingRect().height();
1201 if (previewX + webPreview.previewItem->boundingRect().width() > sceneRect().width())
1202 previewX = sceneRect().right() - webPreview.previewItem->boundingRect().width();
1204 webPreview.previewItem->setPos(previewX, previewY);
1206 addItem(webPreview.previewItem);
1207 webPreview.previewState = WebPreview::ShowPreview;
1209 case WebPreview::ShowPreview:
1210 qWarning() << "ChatScene::webPreviewNextStep() called while in ShowPreview Step!";
1211 qWarning() << "removing preview";
1212 if (webPreview.previewItem && webPreview.previewItem->scene())
1213 removeItem(webPreview.previewItem);
1214 // Fall through to deletion!
1215 case WebPreview::HidePreview:
1216 if (webPreview.previewItem) {
1217 delete webPreview.previewItem;
1218 webPreview.previewItem = 0;
1220 webPreview.parentItem = 0;
1221 webPreview.url = QUrl();
1222 webPreview.urlRect = QRectF();
1223 webPreview.previewState = WebPreview::NoPreview;
1225 // qDebug() << " new State:" << webPreview.previewState << webPreview.timer.isActive();
1229 void ChatScene::clearWebPreview(ChatItem *parentItem)
1231 // qDebug() << Q_FUNC_INFO << webPreview.previewState;
1232 switch (webPreview.previewState) {
1233 case WebPreview::NewPreview:
1234 webPreview.previewState = WebPreview::NoPreview; // we haven't loaded anything yet
1236 case WebPreview::ShowPreview:
1237 if (parentItem == 0 || webPreview.parentItem == parentItem) {
1238 if (webPreview.previewItem && webPreview.previewItem->scene())
1239 removeItem(webPreview.previewItem);
1241 // fall through into to set hidden state
1242 case WebPreview::DelayPreview:
1243 // we're just loading, so haven't shown the preview yet.
1244 webPreview.previewState = WebPreview::HidePreview;
1245 webPreview.timer.start(5000);
1247 case WebPreview::NoPreview:
1248 case WebPreview::HidePreview:
1251 // qDebug() << " new State:" << webPreview.previewState << webPreview.timer.isActive();
1257 // ========================================
1258 // end of webkit only
1259 // ========================================
1261 void ChatScene::showWebPreviewChanged()
1263 ChatViewSettings settings;
1264 _showWebPreview = settings.showWebPreview();