1 /***************************************************************************
2 * Copyright (C) 2005-2010 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 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
19 ***************************************************************************/
21 #include <QApplication>
24 #include <QGraphicsSceneMouseEvent>
27 #include <QPersistentModelIndex>
41 #include "chatlinemodelitem.h"
42 #include "chatscene.h"
45 #include "clientbacklogmanager.h"
46 #include "columnhandleitem.h"
47 #include "contextmenuactionprovider.h"
48 #include "iconloader.h"
49 #include "messagefilter.h"
51 #include "qtuistyle.h"
52 #include "chatviewsettings.h"
53 #include "webpreviewitem.h"
55 const qreal minContentsWidth = 200;
57 ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal width, ChatView *parent)
58 : QGraphicsScene(0, 0, width, 0, (QObject *)parent),
62 _singleBufferId(BufferId()),
63 _sceneRect(0, 0, width, 0),
66 _cutoffMode(CutoffRight),
72 _leftButtonPressed(false)
74 MessageFilter *filter = qobject_cast<MessageFilter*>(model);
75 if(filter && filter->isSingleBufferFilter()) {
76 _singleBufferId = filter->singleBufferId();
79 ChatViewSettings defaultSettings;
80 int defaultFirstColHandlePos = defaultSettings.value("FirstColumnHandlePos", 80).toInt();
81 int defaultSecondColHandlePos = defaultSettings.value("SecondColumnHandlePos", 200).toInt();
83 ChatViewSettings viewSettings(this);
84 _firstColHandlePos = viewSettings.value("FirstColumnHandlePos", defaultFirstColHandlePos).toInt();
85 _secondColHandlePos = viewSettings.value("SecondColumnHandlePos", defaultSecondColHandlePos).toInt();
87 _firstColHandle = new ColumnHandleItem(QtUi::style()->firstColumnSeparator());
88 addItem(_firstColHandle);
89 _firstColHandle->setXPos(_firstColHandlePos);
90 connect(_firstColHandle, SIGNAL(positionChanged(qreal)), this, SLOT(firstHandlePositionChanged(qreal)));
91 connect(this, SIGNAL(sceneRectChanged(const QRectF &)), _firstColHandle, SLOT(sceneRectChanged(const QRectF &)));
93 _secondColHandle = new ColumnHandleItem(QtUi::style()->secondColumnSeparator());
94 addItem(_secondColHandle);
95 _secondColHandle->setXPos(_secondColHandlePos);
96 connect(_secondColHandle, SIGNAL(positionChanged(qreal)), this, SLOT(secondHandlePositionChanged(qreal)));
98 connect(this, SIGNAL(sceneRectChanged(const QRectF &)), _secondColHandle, SLOT(sceneRectChanged(const QRectF &)));
102 if(model->rowCount() > 0)
103 rowsInserted(QModelIndex(), 0, model->rowCount() - 1);
105 connect(model, SIGNAL(rowsInserted(const QModelIndex &, int, int)),
106 this, SLOT(rowsInserted(const QModelIndex &, int, int)));
107 connect(model, SIGNAL(rowsAboutToBeRemoved(const QModelIndex &, int, int)),
108 this, SLOT(rowsAboutToBeRemoved(const QModelIndex &, int, int)));
109 connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), SLOT(dataChanged(QModelIndex, QModelIndex)));
112 webPreview.timer.setSingleShot(true);
113 connect(&webPreview.timer, SIGNAL(timeout()), this, SLOT(webPreviewNextStep()));
115 _showWebPreview = defaultSettings.showWebPreview();
116 defaultSettings.notify("ShowWebPreview", this, SLOT(showWebPreviewChanged()));
118 _clickTimer.setInterval(QApplication::doubleClickInterval());
119 _clickTimer.setSingleShot(true);
120 connect(&_clickTimer, SIGNAL(timeout()), SLOT(clickTimeout()));
122 setItemIndexMethod(QGraphicsScene::NoIndex);
125 ChatScene::~ChatScene() {
128 ChatView *ChatScene::chatView() const {
132 ColumnHandleItem *ChatScene::firstColumnHandle() const {
133 return _firstColHandle;
136 ColumnHandleItem *ChatScene::secondColumnHandle() const {
137 return _secondColHandle;
140 ChatItem *ChatScene::chatItemAt(const QPointF &scenePos) const {
141 ChatLine *line = qgraphicsitem_cast<ChatLine*>(itemAt(scenePos));
143 return line->itemAt(line->mapFromScene(scenePos));
147 bool ChatScene::containsBuffer(const BufferId &id) const {
148 MessageFilter *filter = qobject_cast<MessageFilter*>(model());
150 return filter->containsBuffer(id);
155 void ChatScene::rowsInserted(const QModelIndex &index, int start, int end) {
159 // QModelIndex sidx = model()->index(start, 2);
160 // QModelIndex eidx = model()->index(end, 2);
161 // qDebug() << "rowsInserted:";
163 // QModelIndex ssidx = model()->index(start - 1, 2);
164 // qDebug() << "Start--:" << start - 1 << ssidx.data(MessageModel::MsgIdRole).value<MsgId>()
165 // << ssidx.data(Qt::DisplayRole).toString();
167 // qDebug() << "Start:" << start << sidx.data(MessageModel::MsgIdRole).value<MsgId>()
168 // << sidx.data(Qt::DisplayRole).toString();
169 // qDebug() << "End:" << end << eidx.data(MessageModel::MsgIdRole).value<MsgId>()
170 // << eidx.data(Qt::DisplayRole).toString();
171 // if(end + 1 < model()->rowCount()) {
172 // QModelIndex eeidx = model()->index(end + 1, 2);
173 // qDebug() << "End++:" << end + 1 << eeidx.data(MessageModel::MsgIdRole).value<MsgId>()
174 // << eeidx.data(Qt::DisplayRole).toString();
179 qreal width = _sceneRect.width();
180 bool atBottom = (start == _lines.count());
181 bool atTop = !atBottom && (start == 0);
183 if(start < _lines.count()) {
184 y = _lines.value(start)->y();
185 } else if(atBottom && !_lines.isEmpty()) {
186 y = _lines.last()->y() + _lines.last()->height();
189 qreal contentsWidth = width - secondColumnHandle()->sceneRight();
190 qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
191 qreal timestampWidth = firstColumnHandle()->sceneLeft();
192 QPointF contentsPos(secondColumnHandle()->sceneRight(), 0);
193 QPointF senderPos(firstColumnHandle()->sceneRight(), 0);
196 for(int i = end; i >= start; i--) {
197 ChatLine *line = new ChatLine(i, model(),
199 timestampWidth, senderWidth, contentsWidth,
200 senderPos, contentsPos);
202 line->setPos(0, y-h);
203 _lines.insert(start, line);
207 for(int i = start; i <= end; i++) {
208 ChatLine *line = new ChatLine(i, model(),
210 timestampWidth, senderWidth, contentsWidth,
211 senderPos, contentsPos);
212 line->setPos(0, y+h);
214 _lines.insert(i, line);
219 // update existing items
220 for(int i = end+1; i < _lines.count(); i++) {
221 _lines[i]->setRow(i);
225 if(_selectionStart >= 0) {
226 int offset = end - start + 1;
227 int oldStart = _selectionStart;
228 if(_selectionStart >= start)
229 _selectionStart += offset;
230 if(_selectionEnd >= start) {
231 _selectionEnd += offset;
232 if(_selectionStart == oldStart)
233 for(int i = start; i < start + offset; i++)
234 _lines[i]->setSelected(true);
236 if(_firstSelectionRow >= start)
237 _firstSelectionRow += offset;
240 // neither pre- or append means we have to do dirty work: move items...
241 if(!(atTop || atBottom)) {
243 for(int i = 0; i <= end; i++) {
245 line->setPos(0, line->pos().y() - h);
249 // check if all went right
250 Q_ASSERT(start == 0 || _lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() == _lines.at(start)->pos().y());
252 // if(_lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() != _lines.at(start)->pos().y()) {
253 // qDebug() << "lines:" << _lines.count() << "start:" << start << "end:" << end;
254 // 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();
255 // qDebug() << "line[start]" << _lines.at(start)->pos().y();
256 // qDebug() << "needed moving:" << !(atTop || atBottom) << moveTop << moveStart << moveEnd << offset;
260 Q_ASSERT(end + 1 == _lines.count() || _lines.at(end)->pos().y() + _lines.at(end)->height() == _lines.at(end + 1)->pos().y());
261 // if(end + 1 < _lines.count()) {
262 // if(_lines.at(end)->pos().y() + _lines.at(end)->height() != _lines.at(end + 1)->pos().y()) {
263 // qDebug() << "lines:" << _lines.count() << "start:" << start << "end:" << end;
264 // qDebug() << "line[end]:" << _lines.at(end)->pos().y() << "+" << _lines.at(end)->height() << "=" << _lines.at(end)->pos().y() + _lines.at(end)->height();
265 // qDebug() << "line[end+1]" << _lines.at(end + 1)->pos().y();
266 // qDebug() << "needed moving:" << !(atTop || atBottom) << moveTop << moveStart << moveEnd << offset;
272 if(start < _firstLineRow) {
273 int prevFirstLineRow = _firstLineRow + (end - start + 1);
274 for(int i = end + 1; i < prevFirstLineRow; i++) {
275 _lines.at(i)->show();
278 // force new search for first proper line
283 emit lastLineChanged(_lines.last(), h);
287 void ChatScene::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) {
290 qreal h = 0; // total height of removed items;
292 bool atTop = (start == 0);
293 bool atBottom = (end == _lines.count() - 1);
294 bool moveTop = false;
298 int row = _selectingItem->row();
299 if(row >= start && row <= end)
303 // remove items from scene
304 QList<ChatLine *>::iterator lineIter = _lines.begin() + start;
305 int lineCount = start;
306 while(lineIter != _lines.end() && lineCount <= end) {
307 h += (*lineIter)->height();
309 lineIter = _lines.erase(lineIter);
313 // update rows of remaining chatlines
314 for(int i = start; i < _lines.count(); i++) {
315 _lines.at(i)->setRow(i);
319 if(_selectionStart >= 0) {
320 int offset = end - start + 1;
321 if(_selectionStart >= start)
322 _selectionStart = qMax(_selectionStart -= offset, start);
323 if(_selectionEnd >= start)
324 _selectionEnd -= offset;
325 if(_firstSelectionRow >= start)
326 _firstSelectionRow -= offset;
328 if(_selectionEnd < _selectionStart) {
329 _isSelecting = false;
330 _selectionStart = -1;
334 // neither removing at bottom or top means we have to move items...
335 if(!(atTop || atBottom)) {
338 int moveEnd = _lines.count() - 1;
339 if(start < _lines.count() - start) {
349 for(int i = moveStart; i <= moveEnd; i++) {
351 line->setPos(0, line->pos().y() + offset);
355 Q_ASSERT(start == 0 || start >= _lines.count() || _lines.at(start - 1)->pos().y() + _lines.at(start - 1)->height() == _lines.at(start)->pos().y());
358 // when searching for the first non-date-line we have to take into account that our
359 // model still contains the just removed lines so we cannot simply call updateSceneRect()
360 int numRows = model()->rowCount();
361 QModelIndex firstLineIdx;
363 bool needOffset = false;
366 if(_firstLineRow >= start && _firstLineRow <= end) {
367 _firstLineRow = end + 1;
370 firstLineIdx = model()->index(_firstLineRow, 0);
371 } while((Message::Type)(model()->data(firstLineIdx, MessageModel::TypeRole).toInt()) == Message::DayChange && _firstLineRow < numRows);
374 _firstLineRow -= end - start + 1;
378 void ChatScene::dataChanged(const QModelIndex &tl, const QModelIndex &br) {
379 layout(tl.row(), br.row(), _sceneRect.width());
382 void ChatScene::updateForViewport(qreal width, qreal height) {
383 _viewportHeight = height;
387 void ChatScene::setWidth(qreal width) {
388 if(width == _sceneRect.width())
390 layout(0, _lines.count()-1, width);
393 void ChatScene::layout(int start, int end, qreal width) {
394 // clock_t startT = clock();
396 // disabling the index while doing this complex updates is about
397 // 2 to 10 times faster!
398 //setItemIndexMethod(QGraphicsScene::NoIndex);
402 qreal linePos = _lines.at(row)->scenePos().y() + _lines.at(row)->height();
403 qreal contentsWidth = width - secondColumnHandle()->sceneRight();
404 while(row >= start) {
405 _lines.at(row--)->setGeometryByWidth(width, contentsWidth, linePos);
409 // remaining items don't need geometry changes, but maybe repositioning?
410 ChatLine *line = _lines.at(row);
411 qreal offset = linePos - (line->scenePos().y() + line->height());
414 line = _lines.at(row--);
415 line->setPos(0, line->scenePos().y() + offset);
421 //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
423 updateSceneRect(width);
425 emit layoutChanged();
427 // clock_t endT = clock();
428 // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
431 void ChatScene::firstHandlePositionChanged(qreal xpos) {
432 if(_firstColHandlePos == xpos)
435 _firstColHandlePos = xpos >= 0 ? xpos : 0;
436 ChatViewSettings viewSettings(this);
437 viewSettings.setValue("FirstColumnHandlePos", _firstColHandlePos);
438 ChatViewSettings defaultSettings;
439 defaultSettings.setValue("FirstColumnHandlePos", _firstColHandlePos);
441 // clock_t startT = clock();
443 // disabling the index while doing this complex updates is about
444 // 2 to 10 times faster!
445 //setItemIndexMethod(QGraphicsScene::NoIndex);
447 QList<ChatLine *>::iterator lineIter = _lines.end();
448 QList<ChatLine *>::iterator lineIterBegin = _lines.begin();
449 qreal timestampWidth = firstColumnHandle()->sceneLeft();
450 qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
451 QPointF senderPos(firstColumnHandle()->sceneRight(), 0);
453 while(lineIter != lineIterBegin) {
455 (*lineIter)->setFirstColumn(timestampWidth, senderWidth, senderPos);
457 //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
461 // clock_t endT = clock();
462 // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
465 void ChatScene::secondHandlePositionChanged(qreal xpos) {
466 if(_secondColHandlePos == xpos)
469 _secondColHandlePos = xpos;
470 ChatViewSettings viewSettings(this);
471 viewSettings.setValue("SecondColumnHandlePos", _secondColHandlePos);
472 ChatViewSettings defaultSettings;
473 defaultSettings.setValue("SecondColumnHandlePos", _secondColHandlePos);
475 // clock_t startT = clock();
477 // disabling the index while doing this complex updates is about
478 // 2 to 10 times faster!
479 //setItemIndexMethod(QGraphicsScene::NoIndex);
481 QList<ChatLine *>::iterator lineIter = _lines.end();
482 QList<ChatLine *>::iterator lineIterBegin = _lines.begin();
483 qreal linePos = _sceneRect.y() + _sceneRect.height();
484 qreal senderWidth = secondColumnHandle()->sceneLeft() - firstColumnHandle()->sceneRight();
485 qreal contentsWidth = _sceneRect.width() - secondColumnHandle()->sceneRight();
486 QPointF contentsPos(secondColumnHandle()->sceneRight(), 0);
487 while(lineIter != lineIterBegin) {
489 (*lineIter)->setSecondColumn(senderWidth, contentsWidth, contentsPos, linePos);
491 //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
495 emit layoutChanged();
497 // clock_t endT = clock();
498 // qDebug() << "resized" << _lines.count() << "in" << (float)(endT - startT) / CLOCKS_PER_SEC << "sec";
501 void ChatScene::setHandleXLimits() {
502 _firstColHandle->setXLimits(0, _secondColHandle->sceneLeft());
503 _secondColHandle->setXLimits(_firstColHandle->sceneRight(), width() - minContentsWidth);
507 void ChatScene::setSelectingItem(ChatItem *item) {
508 if(_selectingItem) _selectingItem->clearSelection();
509 _selectingItem = item;
512 void ChatScene::startGlobalSelection(ChatItem *item, const QPointF &itemPos) {
513 _selectionStart = _selectionEnd = _firstSelectionRow = item->row();
514 _selectionStartCol = _selectionMinCol = item->column();
516 _lines[_selectionStart]->setSelected(true, (ChatLineModel::ColumnType)_selectionMinCol);
517 updateSelection(item->mapToScene(itemPos));
520 void ChatScene::updateSelection(const QPointF &pos) {
521 int curRow = rowByScenePos(pos);
522 if(curRow < 0) return;
523 int curColumn = (int)columnByScenePos(pos);
524 ChatLineModel::ColumnType minColumn = (ChatLineModel::ColumnType)qMin(curColumn, _selectionStartCol);
525 if(minColumn != _selectionMinCol) {
526 _selectionMinCol = minColumn;
527 for(int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++) {
528 _lines[l]->setSelected(true, minColumn);
531 int newstart = qMin(curRow, _firstSelectionRow);
532 int newend = qMax(curRow, _firstSelectionRow);
533 if(newstart < _selectionStart) {
534 for(int l = newstart; l < _selectionStart; l++)
535 _lines[l]->setSelected(true, minColumn);
537 if(newstart > _selectionStart) {
538 for(int l = _selectionStart; l < newstart; l++)
539 _lines[l]->setSelected(false);
541 if(newend > _selectionEnd) {
542 for(int l = _selectionEnd+1; l <= newend; l++)
543 _lines[l]->setSelected(true, minColumn);
545 if(newend < _selectionEnd) {
546 for(int l = newend+1; l <= _selectionEnd; l++)
547 _lines[l]->setSelected(false);
550 _selectionStart = newstart;
551 _selectionEnd = newend;
553 if(newstart == newend && minColumn == ChatLineModel::ContentsColumn) {
554 if(!_selectingItem) {
555 // _selectingItem has been removed already
558 _lines[curRow]->setSelected(false);
559 _isSelecting = false;
560 _selectionStart = -1;
561 _selectingItem->continueSelecting(_selectingItem->mapFromScene(pos));
565 bool ChatScene::isPosOverSelection(const QPointF &pos) const {
566 ChatItem *chatItem = chatItemAt(pos);
569 if(hasGlobalSelection()) {
570 int row = chatItem->row();
571 if(row >= qMin(_selectionStart, _selectionEnd) && row <= qMax(_selectionStart, _selectionEnd))
572 return columnByScenePos(pos) >= _selectionMinCol;
574 return chatItem->isPosOverSelection(chatItem->mapFromScene(pos));
579 bool ChatScene::isScrollingAllowed() const {
583 // TODO: Handle clicks and single-item selections too
588 /******** MOUSE HANDLING **************************************************************************/
590 void ChatScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
591 QPointF pos = event->scenePos();
594 // zoom actions and similar
595 chatView()->addActionsToMenu(&menu, pos);
598 if(isPosOverSelection(pos))
599 menu.addAction(SmallIcon("edit-copy"), tr("Copy Selection"),
600 this, SLOT(selectionToClipboard()),
603 // item-specific options (select link etc)
604 ChatItem *item = chatItemAt(pos);
606 item->addActionsToMenu(&menu, item->mapFromScene(pos));
608 // no item -> default scene actions
609 GraphicalUi::contextMenuActionProvider()->addActions(&menu, filter(), BufferId());
611 if (QtUi::mainWindow()->menuBar()->isHidden())
612 menu.addAction(QtUi::actionCollection("General")->action("ToggleMenuBar"));
614 menu.exec(event->screenPos());
618 void ChatScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
619 if(event->buttons() == Qt::LeftButton) {
620 if(!_clickHandled && (event->scenePos() - _clickPos).toPoint().manhattanLength() >= QApplication::startDragDistance()) {
621 if(_clickTimer.isActive())
623 if(_clickMode == SingleClick && isPosOverSelection(_clickPos))
624 initiateDrag(event->widget());
626 _clickMode = DragStartClick;
627 handleClick(Qt::LeftButton, _clickPos);
629 _clickMode = NoClick;
632 updateSelection(event->scenePos());
633 emit mouseMoveWhileSelecting(event->scenePos());
635 } else if(_clickHandled && _clickMode < DoubleClick)
636 QGraphicsScene::mouseMoveEvent(event);
638 QGraphicsScene::mouseMoveEvent(event);
641 void ChatScene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
642 if(event->buttons() == Qt::LeftButton) {
643 _leftButtonPressed = true;
644 _clickHandled = false;
645 if(!isPosOverSelection(event->scenePos())) {
646 // immediately clear selection if clicked outside; otherwise, wait for potential drag
649 if(_clickMode != NoClick && _clickTimer.isActive()) {
651 case NoClick: _clickMode = SingleClick; break;
652 case SingleClick: _clickMode = DoubleClick; break;
653 case DoubleClick: _clickMode = TripleClick; break;
654 case TripleClick: _clickMode = DoubleClick; break;
655 case DragStartClick: break;
657 handleClick(Qt::LeftButton, _clickPos);
659 _clickMode = SingleClick;
660 _clickPos = event->scenePos();
664 if(event->type() == QEvent::GraphicsSceneMouseDoubleClick)
665 QGraphicsScene::mouseDoubleClickEvent(event);
667 QGraphicsScene::mousePressEvent(event);
670 void ChatScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
671 // we check for doubleclick ourselves, so just call press handler
672 mousePressEvent(event);
675 void ChatScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
676 if(event->button() == Qt::LeftButton && _leftButtonPressed) {
677 _leftButtonPressed = false;
678 if(_clickMode != NoClick) {
679 if(_clickMode == SingleClick)
682 if(!_clickTimer.isActive())
683 handleClick(Qt::LeftButton, _clickPos);
685 // no click -> drag or selection move
686 if(isGloballySelecting()) {
687 selectionToClipboard(QClipboard::Selection);
688 _isSelecting = false;
694 QGraphicsScene::mouseReleaseEvent(event);
697 void ChatScene::clickTimeout() {
698 if(!_leftButtonPressed && _clickMode == SingleClick)
699 handleClick(Qt::LeftButton, _clickPos);
702 void ChatScene::handleClick(Qt::MouseButton button, const QPointF &scenePos) {
703 if(button == Qt::LeftButton) {
706 // Now send click down to items
707 ChatItem *chatItem = chatItemAt(scenePos);
709 chatItem->handleClick(chatItem->mapFromScene(scenePos), _clickMode);
711 _clickHandled = true;
715 void ChatScene::initiateDrag(QWidget *source) {
716 QDrag *drag = new QDrag(source);
717 QMimeData *mimeData = new QMimeData;
718 mimeData->setText(selection());
719 drag->setMimeData(mimeData);
721 drag->exec(Qt::CopyAction);
724 /******** SELECTIONS ******************************************************************************/
726 void ChatScene::selectionToClipboard(QClipboard::Mode mode) {
730 stringToClipboard(selection(), mode);
733 void ChatScene::stringToClipboard(const QString &str_, QClipboard::Mode mode) {
735 // remove trailing linefeeds
736 if(str.endsWith('\n'))
740 case QClipboard::Clipboard:
741 QApplication::clipboard()->setText(str);
743 case QClipboard::Selection:
744 if(QApplication::clipboard()->supportsSelection())
745 QApplication::clipboard()->setText(str, QClipboard::Selection);
752 //!\brief Convert current selection to human-readable string.
753 QString ChatScene::selection() const {
754 //TODO Make selection format configurable!
755 if(hasGlobalSelection()) {
756 int start = qMin(_selectionStart, _selectionEnd);
757 int end = qMax(_selectionStart, _selectionEnd);
758 if(start < 0 || end >= _lines.count()) {
759 qDebug() << "Invalid selection range:" << start << end;
763 for(int l = start; l <= end; l++) {
764 if(_selectionMinCol == ChatLineModel::TimestampColumn)
765 result += _lines[l]->item(ChatLineModel::TimestampColumn)->data(MessageModel::DisplayRole).toString() + " ";
766 if(_selectionMinCol <= ChatLineModel::SenderColumn)
767 result += _lines[l]->item(ChatLineModel::SenderColumn)->data(MessageModel::DisplayRole).toString() + " ";
768 result += _lines[l]->item(ChatLineModel::ContentsColumn)->data(MessageModel::DisplayRole).toString() + "\n";
771 } else if(selectingItem())
772 return selectingItem()->selection();
776 bool ChatScene::hasSelection() const {
777 return hasGlobalSelection() || (selectingItem() && selectingItem()->hasSelection());
780 bool ChatScene::hasGlobalSelection() const {
781 return _selectionStart >= 0;
784 bool ChatScene::isGloballySelecting() const {
788 void ChatScene::clearGlobalSelection() {
789 if(hasGlobalSelection()) {
790 for(int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++)
791 _lines[l]->setSelected(false);
792 _isSelecting = false;
793 _selectionStart = -1;
797 void ChatScene::clearSelection() {
798 clearGlobalSelection();
800 selectingItem()->clearSelection();
803 /******** *************************************************************************************/
805 void ChatScene::requestBacklog() {
806 MessageFilter *filter = qobject_cast<MessageFilter*>(model());
808 return filter->requestBacklog();
812 ChatLineModel::ColumnType ChatScene::columnByScenePos(qreal x) const {
813 if(x < _firstColHandle->x())
814 return ChatLineModel::TimestampColumn;
815 if(x < _secondColHandle->x())
816 return ChatLineModel::SenderColumn;
818 return ChatLineModel::ContentsColumn;
821 int ChatScene::rowByScenePos(qreal y) const {
822 QList<QGraphicsItem*> itemList = items(QPointF(0, y));
824 // ChatLine should be at the bottom of the list
825 for(int i = itemList.count()-1; i >= 0; i--) {
826 ChatLine *line = qgraphicsitem_cast<ChatLine *>(itemList.at(i));
833 void ChatScene::updateSceneRect(qreal width) {
834 if(_lines.isEmpty()) {
835 updateSceneRect(QRectF(0, 0, width, 0));
839 // we hide day change messages at the top by making the scene rect smaller
840 // and by calling QGraphicsItem::hide() on all leading day change messages
841 // the first one is needed to ensure proper scrollbar ranges
842 // the second for cases where the viewport is larger then the set scenerect
843 // (in this case the items are shown anyways)
844 if(_firstLineRow == -1) {
845 int numRows = model()->rowCount();
847 QModelIndex firstLineIdx;
848 while(_firstLineRow < numRows) {
849 firstLineIdx = model()->index(_firstLineRow, 0);
850 if((Message::Type)(model()->data(firstLineIdx, MessageModel::TypeRole).toInt()) != Message::DayChange)
852 _lines.at(_firstLineRow)->hide();
857 // the following call should be safe. If it crashes something went wrong during insert/remove
858 if(_firstLineRow < _lines.count()) {
859 ChatLine *firstLine = _lines.at(_firstLineRow);
860 ChatLine *lastLine = _lines.last();
861 updateSceneRect(QRectF(0, firstLine->pos().y(), width, lastLine->pos().y() + lastLine->height() - firstLine->pos().y()));
864 updateSceneRect(QRectF(0, 0, width, 0));
868 void ChatScene::updateSceneRect(const QRectF &rect) {
874 bool ChatScene::event(QEvent *e) {
875 if(e->type() == QEvent::ApplicationPaletteChange) {
876 _firstColHandle->setColor(QApplication::palette().windowText().color());
877 _secondColHandle->setColor(QApplication::palette().windowText().color());
879 return QGraphicsScene::event(e);
882 // ========================================
884 // ========================================
886 void ChatScene::loadWebPreview(ChatItem *parentItem, const QUrl &url, const QRectF &urlRect) {
890 if(webPreview.urlRect != urlRect)
891 webPreview.urlRect = urlRect;
893 if(webPreview.parentItem != parentItem)
894 webPreview.parentItem = parentItem;
896 if(webPreview.url != url) {
897 webPreview.url = url;
898 // prepare to load a different URL
899 if(webPreview.previewItem) {
900 if(webPreview.previewItem->scene())
901 removeItem(webPreview.previewItem);
902 delete webPreview.previewItem;
903 webPreview.previewItem = 0;
905 webPreview.previewState = WebPreview::NoPreview;
908 if(webPreview.url.isEmpty())
911 // qDebug() << Q_FUNC_INFO << webPreview.previewState;
912 switch(webPreview.previewState) {
913 case WebPreview::NoPreview:
914 webPreview.previewState = WebPreview::NewPreview;
915 webPreview.timer.start(500);
917 case WebPreview::NewPreview:
918 case WebPreview::DelayPreview:
919 case WebPreview::ShowPreview:
920 // we're already waiting for the next step or showing the preview
922 case WebPreview::HidePreview:
923 // we still have a valid preview
924 webPreview.previewState = WebPreview::DelayPreview;
925 webPreview.timer.start(1000);
928 // qDebug() << " new State:" << webPreview.previewState << webPreview.timer.isActive();
931 void ChatScene::webPreviewNextStep() {
932 // qDebug() << Q_FUNC_INFO << webPreview.previewState;
933 switch(webPreview.previewState) {
934 case WebPreview::NoPreview:
936 case WebPreview::NewPreview:
937 Q_ASSERT(!webPreview.previewItem);
938 webPreview.previewItem = new WebPreviewItem(webPreview.url);
939 webPreview.previewState = WebPreview::DelayPreview;
940 webPreview.timer.start(1000);
942 case WebPreview::DelayPreview:
943 Q_ASSERT(webPreview.previewItem);
944 // calc position and show
946 qreal previewY = webPreview.urlRect.bottom();
947 qreal previewX = webPreview.urlRect.x();
948 if(previewY + webPreview.previewItem->boundingRect().height() > sceneRect().bottom())
949 previewY = webPreview.urlRect.y() - webPreview.previewItem->boundingRect().height();
951 if(previewX + webPreview.previewItem->boundingRect().width() > sceneRect().width())
952 previewX = sceneRect().right() - webPreview.previewItem->boundingRect().width();
954 webPreview.previewItem->setPos(previewX, previewY);
956 addItem(webPreview.previewItem);
957 webPreview.previewState = WebPreview::ShowPreview;
959 case WebPreview::ShowPreview:
960 qWarning() << "ChatScene::webPreviewNextStep() called while in ShowPreview Step!";
961 qWarning() << "removing preview";
962 if(webPreview.previewItem && webPreview.previewItem->scene())
963 removeItem(webPreview.previewItem);
964 // Fall through to deletion!
965 case WebPreview::HidePreview:
966 if(webPreview.previewItem) {
967 delete webPreview.previewItem;
968 webPreview.previewItem = 0;
970 webPreview.parentItem = 0;
971 webPreview.url = QUrl();
972 webPreview.urlRect = QRectF();
973 webPreview.previewState = WebPreview::NoPreview;
975 // qDebug() << " new State:" << webPreview.previewState << webPreview.timer.isActive();
978 void ChatScene::clearWebPreview(ChatItem *parentItem) {
979 // qDebug() << Q_FUNC_INFO << webPreview.previewState;
980 switch(webPreview.previewState) {
981 case WebPreview::NewPreview:
982 webPreview.previewState = WebPreview::NoPreview; // we haven't loaded anything yet
984 case WebPreview::ShowPreview:
985 if(parentItem == 0 || webPreview.parentItem == parentItem) {
986 if(webPreview.previewItem && webPreview.previewItem->scene())
987 removeItem(webPreview.previewItem);
989 // fall through into to set hidden state
990 case WebPreview::DelayPreview:
991 // we're just loading, so haven't shown the preview yet.
992 webPreview.previewState = WebPreview::HidePreview;
993 webPreview.timer.start(5000);
995 case WebPreview::NoPreview:
996 case WebPreview::HidePreview:
999 // qDebug() << " new State:" << webPreview.previewState << webPreview.timer.isActive();
1003 // ========================================
1004 // end of webkit only
1005 // ========================================
1007 void ChatScene::showWebPreviewChanged() {
1008 ChatViewSettings settings;
1009 _showWebPreview = settings.showWebPreview();