From: Manuel Nickschas Date: Fri, 14 Nov 2008 11:24:13 +0000 (+0100) Subject: Revamping ChatView/ChatScene's mouse handling X-Git-Tag: 0.4.0~431 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=9ce9c0ab3ac6f4bda4e3a70bf13a9c07d2debfe6;hp=229b87f259ab1bc2c65f481eb39c25a872080fe7 Revamping ChatView/ChatScene's mouse handling * Introduce proper single/double/triple click detection. This will make it possible to add some standard selection modes, allows partial selection of links and some more stuff * ChatItem->handleClick() is now called appropriately, in addition to the standard mouse events * Enable drag/drop for selections in ChatView, i.e. you can now drag text somewhere else * Some cleanups --- diff --git a/src/qtui/chatitem.cpp b/src/qtui/chatitem.cpp index 3d2e065e..0900c7e2 100644 --- a/src/qtui/chatitem.cpp +++ b/src/qtui/chatitem.cpp @@ -136,7 +136,7 @@ void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, // painter->drawRect(_boundingRect.adjusted(0, 0, -1, -1)); } -qint16 ChatItem::posToCursor(const QPointF &pos) { +qint16 ChatItem::posToCursor(const QPointF &pos) const { if(pos.y() > height()) return data(MessageModel::DisplayRole).toString().length(); if(pos.y() < 0) return 0; for(int l = layout()->lineCount() - 1; l >= 0; l--) { @@ -148,6 +148,23 @@ qint16 ChatItem::posToCursor(const QPointF &pos) { return 0; } +bool ChatItem::hasSelection() const { + if(_selectionMode == NoSelection) + return false; + if(_selectionMode == FullSelection) + return true; + // partial + return _selectionStart != _selectionEnd; +} + +QString ChatItem::selection() const { + if(_selectionMode == FullSelection) + return data(MessageModel::DisplayRole).toString(); + if(_selectionMode == PartialSelection) + return data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, _selectionEnd), qAbs(_selectionStart - _selectionEnd)); + return QString(); +} + void ChatItem::setFullSelection() { if(_selectionMode != FullSelection) { _selectionMode = FullSelection; @@ -166,6 +183,16 @@ void ChatItem::continueSelecting(const QPointF &pos) { update(); } +bool ChatItem::isPosOverSelection(const QPointF &pos) const { + if(_selectionMode == FullSelection) + return true; + if(_selectionMode == PartialSelection) { + int cursor = posToCursor(pos); qDebug() << cursor << _selectionStart << _selectionEnd; + return cursor >= qMin(_selectionStart, _selectionEnd) && cursor <= qMax(_selectionStart, _selectionEnd); + } + return false; +} + QTextLayout::FormatRange ChatItem::selectionFormat() const { QTextLayout::FormatRange selectFmt; if(_selectionMode != NoSelection) { @@ -215,15 +242,12 @@ QList ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity return resultList; } -void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { - if(event->buttons() == Qt::LeftButton) { +void ChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode) { + if(clickMode == ChatScene::SingleClick) { chatScene()->setSelectingItem(this); - _selectionStart = _selectionEnd = posToCursor(event->pos()); + _selectionStart = _selectionEnd = posToCursor(pos); _selectionMode = NoSelection; // will be set to PartialSelection by mouseMoveEvent update(); - event->accept(); - } else { - event->ignore(); } } @@ -246,6 +270,14 @@ void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { } } +void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { + if(event->buttons() == Qt::LeftButton) { + event->accept(); + } else { + event->ignore(); + } +} + void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if(_selectionMode != NoSelection && !event->buttons() & Qt::LeftButton) { _selectionEnd = posToCursor(event->pos()); diff --git a/src/qtui/chatitem.h b/src/qtui/chatitem.h index 79f8720e..6c080036 100644 --- a/src/qtui/chatitem.h +++ b/src/qtui/chatitem.h @@ -58,25 +58,28 @@ public: QVariant data(int role) const; // selection stuff, to be called by the scene + QString selection() const; void clearSelection(); void setFullSelection(); void continueSelecting(const QPointF &pos); + bool hasSelection() const; + bool isPosOverSelection(const QPointF &pos) const; QList findWords(const QString &searchWord, Qt::CaseSensitivity caseSensitive); + virtual void handleClick(const QPointF &pos, ChatScene::ClickMode); + protected: virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event); virtual void mousePressEvent(QGraphicsSceneMouseEvent *event); virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); - //virtual bool handleClick(ClickMode mode); - inline QTextLayout *layout() const; virtual QTextLayout::FormatRange selectionFormat() const; virtual inline QVector additionalFormats() const { return QVector(); } - qint16 posToCursor(const QPointF &pos); + qint16 posToCursor(const QPointF &pos) const; inline bool hasPrivateData() const { return (bool)_data; } ChatItemPrivate *privateData() const; diff --git a/src/qtui/chatscene.cpp b/src/qtui/chatscene.cpp index ba31ce82..9a3bca66 100644 --- a/src/qtui/chatscene.cpp +++ b/src/qtui/chatscene.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -39,8 +40,9 @@ const qreal minContentsWidth = 200; -ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal width, QObject *parent) - : QGraphicsScene(0, 0, width, 0, parent), +ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal width, ChatView *parent) + : QGraphicsScene(0, 0, width, 0, (QObject *)parent), + _chatView(parent), _idString(idString), _model(model), _singleBufferScene(false), @@ -50,7 +52,10 @@ ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal w _cutoffMode(CutoffRight), _selectingItem(0), _selectionStart(-1), - _isSelecting(false) + _isSelecting(false), + _clickMode(NoClick), + _clickHandled(true), + _leftButtonPressed(false) { MessageFilter *filter = qobject_cast(model); if(filter) { @@ -98,12 +103,33 @@ ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal w _showWebPreview = defaultSettings.showWebPreview(); defaultSettings.notify("ShowWebPreview", this, SLOT(showWebPreviewChanged())); + _clickTimer.setInterval(QApplication::doubleClickInterval()); + _clickTimer.setSingleShot(true); + connect(&_clickTimer, SIGNAL(timeout()), SLOT(clickTimeout())); + setItemIndexMethod(QGraphicsScene::NoIndex); } ChatScene::~ChatScene() { } +ChatView *ChatScene::chatView() const { + return _chatView; +} + +ColumnHandleItem *ChatScene::firstColumnHandle() const { + return _firstColHandle; +} + +ColumnHandleItem *ChatScene::secondColumnHandle() const { + return _secondColHandle; +} + +ChatItem *ChatScene::chatItemAt(const QPointF &scenePos) const { + QGraphicsItem *item = itemAt(scenePos); + return dynamic_cast(item); +} + bool ChatScene::containsBuffer(const BufferId &id) const { MessageFilter *filter = qobject_cast(model()); if(filter) @@ -511,10 +537,25 @@ void ChatScene::updateSelection(const QPointF &pos) { } _lines[curRow]->setSelected(false); _isSelecting = false; + _selectionStart = -1; _selectingItem->continueSelecting(_selectingItem->mapFromScene(pos)); } } +bool ChatScene::isPosOverSelection(const QPointF &pos) const { + ChatItem *chatItem = chatItemAt(pos); + if(!chatItem) + return false; + if(hasGlobalSelection()) { + int row = chatItem->row(); + if(row >= qMin(_selectionStart, _selectionEnd) && row <= qMax(_selectionStart, _selectionEnd)) + return true; + } else { + return chatItem->isPosOverSelection(chatItem->mapFromScene(pos)); + } + return false; +} + bool ChatScene::isScrollingAllowed() const { if(_isSelecting) return false; @@ -524,39 +565,111 @@ bool ChatScene::isScrollingAllowed() const { return true; } +/******** MOUSE HANDLING **************************************************************************/ + void ChatScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { - if(_isSelecting && event->buttons() == Qt::LeftButton) { - updateSelection(event->scenePos()); - emit mouseMoveWhileSelecting(event->scenePos()); - event->accept(); - } else { + if(event->buttons() == Qt::LeftButton) { + if(!_clickHandled && (event->scenePos() - _clickPos).toPoint().manhattanLength() >= QApplication::startDragDistance()) { + if(_clickTimer.isActive()) _clickTimer.stop(); + if(_clickMode == SingleClick && isPosOverSelection(_clickPos)) + initiateDrag(event->widget()); + else + handleClick(Qt::LeftButton, _clickPos); + _clickMode = NoClick; + } + if(_isSelecting) { + updateSelection(event->scenePos()); + emit mouseMoveWhileSelecting(event->scenePos()); + event->accept(); + } else if(_clickHandled) + QGraphicsScene::mouseMoveEvent(event); + } else QGraphicsScene::mouseMoveEvent(event); - } } void ChatScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { - if(_selectionStart >= 0 && event->buttons() == Qt::LeftButton) { - for(int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++) { - _lines[l]->setSelected(false); + if(event->buttons() == Qt::LeftButton) { + _leftButtonPressed = true; + _clickHandled = false; + if(!isPosOverSelection(event->scenePos())) { + // immediately clear selection if clicked outside; otherwise, wait for potential drag + clearSelection(); } - _isSelecting = false; - _selectionStart = -1; - QGraphicsScene::mousePressEvent(event); // so we can start a new local selection - } else { - QGraphicsScene::mousePressEvent(event); + if(_clickMode != NoClick && _clickTimer.isActive()) { + _clickMode = (ClickMode)(_clickMode == TripleClick ? DoubleClick : _clickMode + 1); + handleClick(Qt::LeftButton, event->scenePos()); + } else { + _clickMode = SingleClick; + _clickPos = event->scenePos(); + } + _clickTimer.start(); + } else if(event->buttons() == Qt::RightButton) { + handleClick(Qt::RightButton, event->scenePos()); } + if(event->type() == QEvent::GraphicsSceneMouseDoubleClick) + QGraphicsScene::mouseDoubleClickEvent(event); + else + QGraphicsScene::mousePressEvent(event); +} + +void ChatScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { + // we check for doubleclick ourselves, so just call press handler + mousePressEvent(event); } void ChatScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - if(_isSelecting && !event->buttons() & Qt::LeftButton) { - putToClipboard(selectionToString()); - _isSelecting = false; - event->accept(); - } else { - QGraphicsScene::mouseReleaseEvent(event); + if(!event->buttons() & Qt::LeftButton) { + _leftButtonPressed = false; + if(_clickMode != NoClick) { + clearSelection(); + event->accept(); + if(!_clickTimer.isActive()) + handleClick(Qt::LeftButton, _clickPos); + } else { + // no click -> drag or selection move + if(isGloballySelecting()) { + putToClipboard(selection()); + _isSelecting = false; + event->accept(); + return; + } + } } + QGraphicsScene::mouseReleaseEvent(event); +} + +void ChatScene::clickTimeout() { + if(!_leftButtonPressed && _clickMode == SingleClick) + handleClick(Qt::LeftButton, _clickPos); +} + +void ChatScene::handleClick(Qt::MouseButton button, const QPointF &scenePos) { + if(button == Qt::LeftButton) { + clearSelection(); + + // Now send click down to items + ChatItem *chatItem = chatItemAt(scenePos); + if(chatItem) { + chatItem->handleClick(chatItem->mapFromScene(scenePos), _clickMode); + } + _clickHandled = true; + } else if(button == Qt::RightButton) { + // TODO: context menu + + } +} + +void ChatScene::initiateDrag(QWidget *source) { + QDrag *drag = new QDrag(source); + QMimeData *mimeData = new QMimeData; + mimeData->setText(selection()); + drag->setMimeData(mimeData); + + drag->exec(Qt::CopyAction); } +/******** SELECTIONS ******************************************************************************/ + void ChatScene::putToClipboard(const QString &selection) { // TODO Configure clipboards # ifdef Q_WS_X11 @@ -568,26 +681,46 @@ void ChatScene::putToClipboard(const QString &selection) { } //!\brief Convert current selection to human-readable string. -QString ChatScene::selectionToString() const { +QString ChatScene::selection() const { //TODO Make selection format configurable! - if(!_isSelecting) return QString(); - int start = qMin(_selectionStart, _selectionEnd); - int end = qMax(_selectionStart, _selectionEnd); - if(start < 0 || end >= _lines.count()) { - qDebug() << "Invalid selection range:" << start << end; - return QString(); - } - QString result; - for(int l = start; l <= end; l++) { - if(_selectionMinCol == ChatLineModel::TimestampColumn) - result += _lines[l]->item(ChatLineModel::TimestampColumn).data(MessageModel::DisplayRole).toString() + " "; - if(_selectionMinCol <= ChatLineModel::SenderColumn) - result += _lines[l]->item(ChatLineModel::SenderColumn).data(MessageModel::DisplayRole).toString() + " "; - result += _lines[l]->item(ChatLineModel::ContentsColumn).data(MessageModel::DisplayRole).toString() + "\n"; + if(hasGlobalSelection()) { + int start = qMin(_selectionStart, _selectionEnd); + int end = qMax(_selectionStart, _selectionEnd); + if(start < 0 || end >= _lines.count()) { + qDebug() << "Invalid selection range:" << start << end; + return QString(); + } + QString result; + for(int l = start; l <= end; l++) { + if(_selectionMinCol == ChatLineModel::TimestampColumn) + result += _lines[l]->item(ChatLineModel::TimestampColumn).data(MessageModel::DisplayRole).toString() + " "; + if(_selectionMinCol <= ChatLineModel::SenderColumn) + result += _lines[l]->item(ChatLineModel::SenderColumn).data(MessageModel::DisplayRole).toString() + " "; + result += _lines[l]->item(ChatLineModel::ContentsColumn).data(MessageModel::DisplayRole).toString() + "\n"; + } + return result; + } else if(selectingItem()) + return selectingItem()->selection(); + return QString(); +} + +void ChatScene::clearGlobalSelection() { + if(hasGlobalSelection()) { + for(int l = qMin(_selectionStart, _selectionEnd); l <= qMax(_selectionStart, _selectionEnd); l++) + _lines[l]->setSelected(false); + _isSelecting = false; + _selectionStart = -1; } - return result; } +void ChatScene::clearSelection() { + clearGlobalSelection(); + if(selectingItem()) + selectingItem()->clearSelection(); +} + +/******** *************************************************************************************/ + void ChatScene::requestBacklog() { MessageFilter *filter = qobject_cast(model()); if(filter) @@ -662,6 +795,8 @@ bool ChatScene::event(QEvent *e) { return QGraphicsScene::event(e); } +/******** WEB PREVIEW *****************************************************************************/ + void ChatScene::loadWebPreview(ChatItem *parentItem, const QString &url, const QRectF &urlRect) { #ifndef HAVE_WEBKIT Q_UNUSED(parentItem) diff --git a/src/qtui/chatscene.h b/src/qtui/chatscene.h index b0c80f4d..852a8d1c 100644 --- a/src/qtui/chatscene.h +++ b/src/qtui/chatscene.h @@ -22,17 +22,19 @@ #define CHATSCENE_H_ #include +#include #include #include #include #include "chatlinemodel.h" -#include "columnhandleitem.h" #include "messagefilter.h" class AbstractUiMsg; class ChatItem; class ChatLine; +class ChatView; +class ColumnHandleItem; class WebPreviewItem; class QGraphicsSceneMouseEvent; @@ -53,7 +55,8 @@ public: SenderChatItemType, ContentsChatItemType, SearchHighlightType, - WebPreviewType + WebPreviewType, + ColumnHandleType }; enum ClickMode { @@ -63,7 +66,7 @@ public: TripleClick }; - ChatScene(QAbstractItemModel *model, const QString &idString, qreal width, QObject *parent); + ChatScene(QAbstractItemModel *model, const QString &idString, qreal width, ChatView *parent); virtual ~ChatScene(); inline QAbstractItemModel *model() const { return _model; } @@ -73,16 +76,26 @@ public: inline int rowByScenePos(const QPointF &pos) { return rowByScenePos(pos.y()); } ChatLineModel::ColumnType columnByScenePos(qreal x); inline ChatLineModel::ColumnType columnByScenePos(const QPointF &pos) { return columnByScenePos(pos.x()); } + + ChatView *chatView() const; + ChatItem *chatItemAt(const QPointF &pos) const; + inline bool isSingleBufferScene() const { return _singleBufferScene; } bool containsBuffer(const BufferId &id) const; inline ChatLine *chatLine(int row) { return (row < _lines.count()) ? _lines[row] : 0; } - inline ColumnHandleItem *firstColumnHandle() const { return _firstColHandle; } - inline ColumnHandleItem *secondColumnHandle() const { return _secondColHandle; } + ColumnHandleItem *firstColumnHandle() const; + ColumnHandleItem *secondColumnHandle() const; inline CutoffMode senderCutoffMode() const { return _cutoffMode; } inline void setSenderCutoffMode(CutoffMode mode) { _cutoffMode = mode; } + QString selection() const; + inline bool hasGlobalSelection() const { return _selectionStart >= 0; } + inline bool isGloballySelecting() const { return _isSelecting; } + bool isPosOverSelection(const QPointF &) const; + void initiateDrag(QWidget *source); + bool isScrollingAllowed() const; virtual bool event(QEvent *e); @@ -95,6 +108,9 @@ public: void setSelectingItem(ChatItem *item); ChatItem *selectingItem() const { return _selectingItem; } void startGlobalSelection(ChatItem *item, const QPointF &itemPos); + void clearGlobalSelection(); + void clearSelection(); + void putToClipboard(const QString &); void requestBacklog(); @@ -111,7 +127,8 @@ protected: virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent); virtual void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent); virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent); - //virtual bool handleLeftClick(ClickMode mode); + virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *mouseEvent); + virtual void handleClick(Qt::MouseButton button, const QPointF &scenePos); protected slots: void rowsInserted(const QModelIndex &, int, int); @@ -124,11 +141,13 @@ private slots: void deleteWebPreviewEvent(); void showWebPreviewChanged(); + void clickTimeout(); + private: void setHandleXLimits(); void updateSelection(const QPointF &pos); - QString selectionToString() const; + ChatView *_chatView; QString _idString; QAbstractItemModel *_model; QList _lines; @@ -157,6 +176,10 @@ private: bool _showWebPreview; QTimer _clickTimer; + ClickMode _clickMode; + QPointF _clickPos; + bool _clickHandled; + bool _leftButtonPressed; struct WebPreview { ChatItem *parentItem; diff --git a/src/qtui/columnhandleitem.h b/src/qtui/columnhandleitem.h index 881b899b..3d0d6e72 100644 --- a/src/qtui/columnhandleitem.h +++ b/src/qtui/columnhandleitem.h @@ -26,11 +26,14 @@ #include #include +#include "chatscene.h" + class ColumnHandleItem : public QObject, public QGraphicsItem { Q_OBJECT public: ColumnHandleItem(qreal width, QGraphicsItem *parent = 0); + virtual inline int type() const { return ChatScene::ColumnHandleType; } inline qreal width() const { return _width; } inline QRectF boundingRect() const { return _boundingRect; } @@ -38,11 +41,10 @@ public: inline qreal sceneRight() const { return _sceneRight; } void setXPos(qreal xpos); + void setXLimits(qreal min, qreal max); void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); - void setXLimits(qreal min, qreal max); - public slots: void sceneRectChanged(const QRectF &); inline void setColor(const QColor &color) { _rulerColor = color; }