Properly stop #nnn from being clickable
[quassel.git] / src / qtui / chatitem.cpp
index ed24bba..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"
 
@@ -289,14 +292,18 @@ void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
 
 void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
   if(_selectionMode != NoSelection && !event->buttons() & Qt::LeftButton) {
-    QString selection
-        = data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, _selectionEnd), qAbs(_selectionStart - _selectionEnd));
-    chatScene()->putToClipboard(selection);
+    chatScene()->selectionToClipboard(QClipboard::Selection);
     event->accept();
   } else
     event->ignore();
 }
 
+void ChatItem::addActionsToMenu(QMenu *menu, const QPointF &pos) {
+  Q_UNUSED(menu);
+  Q_UNUSED(pos);
+
+}
+
 // ************************************************************
 // SenderChatItem
 // ************************************************************
@@ -349,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)
 {
@@ -403,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.
@@ -440,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]));
     }
@@ -456,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;
@@ -483,18 +512,27 @@ void ContentsChatItem::endHoverMode() {
 
 void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clickMode) {
   if(clickMode == ChatScene::SingleClick) {
-    Clickable click = privateData()->currentClickable;
+    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()));
+          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;
       }
@@ -502,7 +540,7 @@ void ContentsChatItem::handleClick(const QPointF &pos, ChatScene::ClickMode clic
   } else if(clickMode == ChatScene::DoubleClick) {
     chatScene()->setSelectingItem(this);
     setSelectionMode(PartialSelection);
-    Clickable click = privateData()->currentClickable;
+    Clickable click = clickableAt(pos);
     if(click.isValid()) {
       setSelectionStart(click.start);
       setSelectionEnd(click.start + click.length);
@@ -536,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);
@@ -673,3 +729,5 @@ qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() {
   return -1;
 }
 
+/*************************************************************************************************/
+