Properly stop #nnn from being clickable
[quassel.git] / src / qtui / chatitem.cpp
index 0900c7e..be19367 100644 (file)
 #include <QTextLayout>
 #include <QMenu>
 
-
+#include "buffermodel.h"
+#include "bufferview.h"
 #include "chatitem.h"
 #include "chatlinemodel.h"
+#include "iconloader.h"
+#include "mainwin.h"
 #include "qtui.h"
 #include "qtuistyle.h"
 
@@ -165,6 +168,13 @@ QString ChatItem::selection() const {
   return QString();
 }
 
+void ChatItem::setSelection(SelectionMode mode, qint16 start, qint16 end) {
+  _selectionMode = mode;
+  _selectionStart = start;
+  _selectionEnd = end;
+  update();
+}
+
 void ChatItem::setFullSelection() {
   if(_selectionMode != FullSelection) {
     _selectionMode = FullSelection;
@@ -173,8 +183,10 @@ void ChatItem::setFullSelection() {
 }
 
 void ChatItem::clearSelection() {
-  _selectionMode = NoSelection;
-  update();
+  if(_selectionMode != NoSelection) {
+    _selectionMode = NoSelection;
+    update();
+  }
 }
 
 void ChatItem::continueSelecting(const QPointF &pos) {
@@ -187,7 +199,7 @@ bool ChatItem::isPosOverSelection(const QPointF &pos) const {
   if(_selectionMode == FullSelection)
     return true;
   if(_selectionMode == PartialSelection) {
-    int cursor = posToCursor(pos); qDebug() << cursor << _selectionStart << _selectionEnd;
+    int cursor = posToCursor(pos);
     return cursor >= qMin(_selectionStart, _selectionEnd) && cursor <= qMax(_selectionStart, _selectionEnd);
   }
   return false;
@@ -243,7 +255,8 @@ QList<QRectF> ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity
 }
 
 void ChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode) {
-  if(clickMode == ChatScene::SingleClick) {
+  // single clicks are already handled by the scene (for clearing the selection)
+  if(clickMode == ChatScene::DragStartClick) {
     chatScene()->setSelectingItem(this);
     _selectionStart = _selectionEnd = posToCursor(pos);
     _selectionMode = NoSelection; // will be set to PartialSelection by mouseMoveEvent
@@ -271,23 +284,24 @@ void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
 }
 
 void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
-  if(event->buttons() == Qt::LeftButton) {
+  if(event->buttons() == Qt::LeftButton)
     event->accept();
-  } else {
+  else
     event->ignore();
-  }
 }
 
 void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
   if(_selectionMode != NoSelection && !event->buttons() & Qt::LeftButton) {
-    _selectionEnd = posToCursor(event->pos());
-    QString selection
-        = data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, _selectionEnd), qAbs(_selectionStart - _selectionEnd));
-    chatScene()->putToClipboard(selection);
+    chatScene()->selectionToClipboard(QClipboard::Selection);
     event->accept();
-  } else {
+  } else
     event->ignore();
-  }
+}
+
+void ChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos) {
+  Q_UNUSED(menu);
+  Q_UNUSED(pos);
+
 }
 
 // ************************************************************
@@ -342,6 +356,9 @@ void SenderChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *op
 // ************************************************************
 // ContentsChatItem
 // ************************************************************
+
+ContentsChatItem::ActionProxy ContentsChatItem::_actionProxy;
+
 ContentsChatItem::ContentsChatItem(const qreal &width, const QPointF &pos, QGraphicsItem *parent)
   : ChatItem(0, 0, pos, parent)
 {
@@ -396,7 +413,7 @@ QList<ContentsChatItem::Clickable> ContentsChatItem::findClickables() const {
   static QRegExp regExp[] = {
     // URL
     // QRegExp(QString("((?:https?://|s?ftp://|irc://|mailto:|www\\.)%1+|%1+\\.[a-z]{2,4}(?:?=/%1+|\\b))%2").arg(urlChars, urlEnd)),
-    QRegExp(QString("((?:(?:https?://|s?ftp://|irc://|mailto:)|www)%1+)%2").arg(urlChars, urlEnd), Qt::CaseInsensitive),
+    QRegExp(QString("((?:(?:https?://|s?ftp://|irc://|gopher://|mailto:)|www)%1+)%2").arg(urlChars, urlEnd), Qt::CaseInsensitive),
 
     // Channel name
     // We don't match for channel names starting with + or &, because that gives us a lot of false positives.
@@ -433,8 +450,17 @@ QList<ContentsChatItem::Clickable> ContentsChatItem::findClickables() const {
     }
     if(type >= 0) {
       idx = matchEnd[type];
+      QString match = str.mid(matches[type], matchEnd[type] - matches[type]);
       if(type == Clickable::Url && str.at(idx-1) == ')') {  // special case: closing paren only matches if we had an open one
-        if(!str.mid(matches[type], matchEnd[type]-matches[type]).contains('(')) matchEnd[type]--;
+        if(!match.contains('(')) {
+          matchEnd[type]--;
+          match.chop(1);
+        }
+      }
+      if(type == Clickable::Channel) {
+        // don't make clickable if it could be a #number
+        if(QRegExp("^#\\d+$").exactMatch(match))
+          continue;
       }
       result.append(Clickable((Clickable::Type)type, matches[type], matchEnd[type] - matches[type]));
     }
@@ -449,6 +475,16 @@ QList<ContentsChatItem::Clickable> ContentsChatItem::findClickables() const {
   return result;
 }
 
+ContentsChatItem::Clickable ContentsChatItem::clickableAt(const QPointF &pos) const {
+  qint16 idx = posToCursor(pos);
+  for(int i = 0; i < privateData()->clickables.count(); i++) {
+    Clickable click = privateData()->clickables.at(i);
+    if(idx >= click.start && idx < click.start + click.length)
+      return click;
+  }
+  return Clickable();
+}
+
 QVector<QTextLayout::FormatRange> ContentsChatItem::additionalFormats() const {
   // mark a clickable if hovered upon
   QVector<QTextLayout::FormatRange> fmt;
@@ -474,41 +510,60 @@ void ContentsChatItem::endHoverMode() {
   }
 }
 
-void ContentsChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
-  privateData()->hasDragged = false;
-  ChatItem::mousePressEvent(event);
-}
-
-void ContentsChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
-  if(!event->buttons() && !privateData()->hasDragged) {
-    // got a click
-    Clickable click = privateData()->currentClickable;
+void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode) {
+  if(clickMode == ChatScene::SingleClick) {
+    Clickable click = clickableAt(pos);
     if(click.isValid()) {
       QString str = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length);
       switch(click.type) {
         case Clickable::Url:
-         if(!str.contains("://"))
-           str = "http://" + str;
-          QDesktopServices::openUrl(QUrl::fromEncoded(str.toAscii()));
+          if(!str.contains("://"))
+            str = "http://" + str;
+          QDesktopServices::openUrl(QUrl::fromEncoded(str.toUtf8(), QUrl::TolerantMode));
           break;
-        case Clickable::Channel:
-          // TODO join or whatever...
+        case Clickable::Channel: {
+          NetworkId networkId = Client::networkModel()->networkId(data(MessageModel::BufferIdRole).value<BufferId>());
+          BufferId bufId = Client::networkModel()->bufferId(networkId, str);
+          if(bufId.isValid()) {
+            QModelIndex targetIdx = Client::networkModel()->bufferIndex(bufId);
+            Client::bufferModel()->switchToBuffer(bufId);
+            if(!targetIdx.data(NetworkModel::ItemActiveRole).toBool())
+              Client::userInput(BufferInfo::fakeStatusBuffer(networkId), QString("/JOIN %1").arg(str));
+          } else
+              Client::userInput(BufferInfo::fakeStatusBuffer(networkId), QString("/JOIN %1").arg(str));
           break;
+        }
         default:
           break;
       }
     }
+  } else if(clickMode == ChatScene::DoubleClick) {
+    chatScene()->setSelectingItem(this);
+    setSelectionMode(PartialSelection);
+    Clickable click = clickableAt(pos);
+    if(click.isValid()) {
+      setSelectionStart(click.start);
+      setSelectionEnd(click.start + click.length);
+    } else {
+      // find word boundary
+      QString str = data(ChatLineModel::DisplayRole).toString();
+      qint16 cursor = posToCursor(pos);
+      qint16 start = str.lastIndexOf(QRegExp("\\W"), cursor) + 1;
+      qint16 end = qMin(str.indexOf(QRegExp("\\W"), cursor), str.length());
+      if(end < 0) end = str.length();
+      setSelectionStart(start);
+      setSelectionEnd(end);
+    }
+    update();
+  } else if(clickMode == ChatScene::TripleClick) {
+    setSelection(PartialSelection, 0, data(ChatLineModel::DisplayRole).toString().length());
   }
-  ChatItem::mouseReleaseEvent(event);
+  ChatItem::handleClick(pos, clickMode);
 }
 
 void ContentsChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
   // mouse move events always mean we're not hovering anymore...
   endHoverMode();
-  // also, check if we have dragged the mouse
-  if(hasPrivateData() && !privateData()->hasDragged && event->buttons() & Qt::LeftButton
-    && (event->buttonDownScreenPos(Qt::LeftButton) - event->screenPos()).manhattanLength() >= QApplication::startDragDistance())
-    privateData()->hasDragged = true;
   ChatItem::mouseMoveEvent(event);
 }
 
@@ -519,52 +574,70 @@ void ContentsChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) {
 
 void ContentsChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {
   bool onClickable = false;
-  qint16 idx = posToCursor(event->pos());
-  for(int i = 0; i < privateData()->clickables.count(); i++) {
-    Clickable click = privateData()->clickables.at(i);
-    if(idx >= click.start && idx < click.start + click.length) {
-      if(click.type == Clickable::Url) {
+  Clickable click = clickableAt(event->pos());
+  if(click.isValid()) {
+    if(click.type == Clickable::Url) {
+      onClickable = true;
+      showWebPreview(click);
+    } else if(click.type == Clickable::Channel) {
+      QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length);
+      // don't make clickable if it's our own name
+      BufferId myId = data(MessageModel::BufferIdRole).value<BufferId>();
+      if(Client::networkModel()->bufferName(myId) != name)
         onClickable = true;
-       showWebPreview(click);
-      } else if(click.type == Clickable::Channel) {
-        // TODO: don't make clickable if it's our own name
-        //onClickable = true; //FIXME disabled for now
-      }
-      if(onClickable) {
-        setCursor(Qt::PointingHandCursor);
-        privateData()->currentClickable = click;
-        update();
-        break;
-      }
+    }
+    if(onClickable) {
+      setCursor(Qt::PointingHandCursor);
+      privateData()->currentClickable = click;
+      update();
+      return;
     }
   }
   if(!onClickable) endHoverMode();
   event->accept();
 }
 
-void ContentsChatItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
-  qint16 idx = posToCursor(event->pos());
-  for(int i = 0; i < privateData()->clickables.count(); i++) {
-    Clickable click = privateData()->clickables.at(i);
-    if(idx >= click.start && idx < click.start + click.length) {
-      if(click.type == Clickable::Url) {
-        QMenu menu;
-        QAction *copyToClipboard = menu.addAction(QObject::tr("Copy to Clipboard"));
-        QAction *selected = menu.exec(event->screenPos());
-        if(selected == copyToClipboard) {
-          QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length);
-#   ifdef Q_WS_X11
-          QApplication::clipboard()->setText(url, QClipboard::Selection);
-#   endif
-//# else
-          QApplication::clipboard()->setText(url);
-//# endif
-        }
+void ContentsChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos) {
+  Q_UNUSED(pos); // we assume that the current mouse cursor pos is the point of invocation
+
+  if(privateData()->currentClickable.isValid()) {
+    Clickable click = privateData()->currentClickable;
+    switch(click.type) {
+      case Clickable::Url:
+        privateData()->activeClickable = click;
+        menu->addAction(SmallIcon("edit-copy"), tr("Copy Link Address"),
+                         &_actionProxy, SLOT(copyLinkToClipboard()))->setData(QVariant::fromValue<void *>(this));
+        break;
+      case Clickable::Channel: {
+        // Hide existing menu actions, they confuse us when right-clicking on a clickable
+        foreach(QAction *action, menu->actions())
+          action->setVisible(false);
+        QString name = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length);
+        Client::mainUi()->actionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value<BufferId>(), name);
+        break;
       }
+      default:
+        break;
     }
+  } else {
+
+    // Buffer-specific actions
+    Client::mainUi()->actionProvider()->addActions(menu, chatScene()->filter(), data(MessageModel::BufferIdRole).value<BufferId>());
+  }
+}
+
+void ContentsChatItem::copyLinkToClipboard() {
+  Clickable click = privateData()->activeClickable;
+  if(click.isValid() && click.type == Clickable::Url) {
+    QString url = data(ChatLineModel::DisplayRole).toString().mid(click.start, click.length);
+    if(!url.contains("://"))
+      url = "http://" + url;
+    chatScene()->stringToClipboard(url);
   }
 }
 
+/******** WEB PREVIEW *****************************************************************************/
+
 void ContentsChatItem::showWebPreview(const Clickable &click) {
 #ifndef HAVE_WEBKIT
   Q_UNUSED(click);
@@ -656,3 +729,5 @@ qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() {
   return -1;
 }
 
+/*************************************************************************************************/
+