Revamping ChatView/ChatScene's mouse handling
authorManuel Nickschas <sputnick@quassel-irc.org>
Fri, 14 Nov 2008 11:24:13 +0000 (12:24 +0100)
committerManuel Nickschas <sputnick@quassel-irc.org>
Thu, 20 Nov 2008 14:14:53 +0000 (15:14 +0100)
* 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

src/qtui/chatitem.cpp
src/qtui/chatitem.h
src/qtui/chatscene.cpp
src/qtui/chatscene.h
src/qtui/columnhandleitem.h

index 3d2e065..0900c7e 100644 (file)
@@ -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<QRectF> 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());
index 79f8720..6c08003 100644 (file)
@@ -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<QRectF> 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<QTextLayout::FormatRange> additionalFormats() const { return QVector<QTextLayout::FormatRange>(); }
 
-  qint16 posToCursor(const QPointF &pos);
+  qint16 posToCursor(const QPointF &pos) const;
 
   inline bool hasPrivateData() const { return (bool)_data; }
   ChatItemPrivate *privateData() const;
index ba31ce8..9a3bca6 100644 (file)
@@ -20,6 +20,7 @@
 
 #include <QApplication>
 #include <QClipboard>
+#include <QDrag>
 #include <QGraphicsSceneMouseEvent>
 #include <QPersistentModelIndex>
 #include <QWebView>
@@ -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<MessageFilter*>(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<ChatItem *>(item);
+}
+
 bool ChatScene::containsBuffer(const BufferId &id) const {
   MessageFilter *filter = qobject_cast<MessageFilter*>(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<MessageFilter*>(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)
index b0c80f4..852a8d1 100644 (file)
 #define CHATSCENE_H_
 
 #include <QAbstractItemModel>
+#include <QGraphicsItem>
 #include <QGraphicsScene>
 #include <QSet>
 #include <QTimer>
 
 #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<ChatLine *> _lines;
@@ -157,6 +176,10 @@ private:
   bool _showWebPreview;
 
   QTimer _clickTimer;
+  ClickMode _clickMode;
+  QPointF _clickPos;
+  bool _clickHandled;
+  bool _leftButtonPressed;
 
   struct WebPreview {
     ChatItem *parentItem;
index 881b899..3d0d6e7 100644 (file)
 #include <QGraphicsScene>
 #include <QTimeLine>
 
+#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; }