Add brackets to timestamp when copying if hidden
[quassel.git] / src / qtui / chatscene.cpp
index 42529f3..188eba6 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2012 by the Quassel Project                        *
+ *   Copyright (C) 2005-2016 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
  *   You should have received a copy of the GNU General Public License     *
  *   along with this program; if not, write to the                         *
  *   Free Software Foundation, Inc.,                                       *
- *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
+ *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
 #include <QApplication>
 #include <QClipboard>
+#include <QDesktopServices>
 #include <QDrag>
 #include <QGraphicsSceneMouseEvent>
+#include <QIcon>
 #include <QMenu>
 #include <QMenuBar>
 #include <QMimeData>
 #include <QPersistentModelIndex>
+#include <QUrl>
 
-#ifdef HAVE_KDE
+#ifdef HAVE_KDE4
 #  include <KMenuBar>
 #else
 #  include <QMenuBar>
 #endif
 
-#ifdef HAVE_WEBKIT
+#ifdef HAVE_WEBENGINE
+#  include <QWebEngineView>
+#elif defined HAVE_WEBKIT
 #  include <QWebView>
 #endif
 
@@ -46,7 +51,6 @@
 #include "clientbacklogmanager.h"
 #include "columnhandleitem.h"
 #include "contextmenuactionprovider.h"
-#include "iconloader.h"
 #include "mainwin.h"
 #include "markerlineitem.h"
 #include "messagefilter.h"
@@ -87,12 +91,12 @@ ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal w
     connect(this, SIGNAL(sceneRectChanged(const QRectF &)), _markerLine, SLOT(sceneRectChanged(const QRectF &)));
 
     ChatViewSettings defaultSettings;
-    int defaultFirstColHandlePos = defaultSettings.value("FirstColumnHandlePos", 80).toInt();
-    int defaultSecondColHandlePos = defaultSettings.value("SecondColumnHandlePos", 200).toInt();
+    _defaultFirstColHandlePos = defaultSettings.value("FirstColumnHandlePos", 80).toInt();
+    _defaultSecondColHandlePos = defaultSettings.value("SecondColumnHandlePos", 200).toInt();
 
     ChatViewSettings viewSettings(this);
-    _firstColHandlePos = viewSettings.value("FirstColumnHandlePos", defaultFirstColHandlePos).toInt();
-    _secondColHandlePos = viewSettings.value("SecondColumnHandlePos", defaultSecondColHandlePos).toInt();
+    _firstColHandlePos = viewSettings.value("FirstColumnHandlePos", _defaultFirstColHandlePos).toInt();
+    _secondColHandlePos = viewSettings.value("SecondColumnHandlePos", _defaultSecondColHandlePos).toInt();
 
     _firstColHandle = new ColumnHandleItem(QtUi::style()->firstColumnSeparator());
     addItem(_firstColHandle);
@@ -120,13 +124,20 @@ ChatScene::ChatScene(QAbstractItemModel *model, const QString &idString, qreal w
         this, SLOT(rowsRemoved()));
     connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), SLOT(dataChanged(QModelIndex, QModelIndex)));
 
-#ifdef HAVE_WEBKIT
+#if defined HAVE_WEBKIT || defined HAVE_WEBENGINE
     webPreview.timer.setSingleShot(true);
     connect(&webPreview.timer, SIGNAL(timeout()), this, SLOT(webPreviewNextStep()));
 #endif
     _showWebPreview = defaultSettings.showWebPreview();
     defaultSettings.notify("ShowWebPreview", this, SLOT(showWebPreviewChanged()));
 
+    _showSenderBrackets = defaultSettings.showSenderBrackets();
+    defaultSettings.notify("ShowSenderBrackets", this, SLOT(showSenderBracketsChanged()));
+
+    _timestampFormatString = defaultSettings.timestampFormatString();
+    defaultSettings.notify("TimestampFormat", this, SLOT(timestampFormatStringChanged()));
+    updateTimestampHasBrackets();
+
     _clickTimer.setInterval(QApplication::doubleClickInterval());
     _clickTimer.setSingleShot(true);
     connect(&_clickTimer, SIGNAL(timeout()), SLOT(clickTimeout()));
@@ -157,6 +168,17 @@ ColumnHandleItem *ChatScene::secondColumnHandle() const
     return _secondColHandle;
 }
 
+void ChatScene::resetColumnWidths()
+{
+    //make sure first column is at least 80 px wide, second 120 px
+    int firstColHandlePos = qMax(_defaultFirstColHandlePos,
+                                 80);
+    int secondColHandlePos = qMax(_defaultSecondColHandlePos,
+                                  firstColHandlePos + 120);
+
+    _firstColHandle->setXPos(firstColHandlePos);
+    _secondColHandle->setXPos(secondColHandlePos);
+}
 
 ChatLine *ChatScene::chatLine(MsgId msgId, bool matchExact, bool ignoreDayChange) const
 {
@@ -638,7 +660,7 @@ void ChatScene::firstHandlePositionChanged(qreal xpos)
     QPointF senderPos(firstColumnHandle()->sceneRight(), 0);
 
     while (lineIter != lineIterBegin) {
-        lineIter--;
+        --lineIter;
         (*lineIter)->setFirstColumn(timestampWidth, senderWidth, senderPos);
     }
     //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
@@ -674,7 +696,7 @@ void ChatScene::secondHandlePositionChanged(qreal xpos)
     qreal contentsWidth = _sceneRect.width() - secondColumnHandle()->sceneRight();
     QPointF contentsPos(secondColumnHandle()->sceneRight(), 0);
     while (lineIter != lineIterBegin) {
-        lineIter--;
+        --lineIter;
         (*lineIter)->setSecondColumn(senderWidth, contentsWidth, contentsPos, linePos);
     }
     //setItemIndexMethod(QGraphicsScene::BspTreeIndex);
@@ -799,11 +821,6 @@ void ChatScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
     chatView()->addActionsToMenu(&menu, pos);
     menu.addSeparator();
 
-    if (isPosOverSelection(pos))
-        menu.addAction(SmallIcon("edit-copy"), tr("Copy Selection"),
-            this, SLOT(selectionToClipboard()),
-            QKeySequence::Copy);
-
     // item-specific options (select link etc)
     ChatItem *item = chatItemAt(pos);
     if (item)
@@ -812,9 +829,30 @@ void ChatScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
         // no item -> default scene actions
         GraphicalUi::contextMenuActionProvider()->addActions(&menu, filter(), BufferId());
 
+    // If we have text selected, insert the Copy Selection as first item
+    if (isPosOverSelection(pos)) {
+        QAction *sep = menu.insertSeparator(menu.actions().first());
+        QAction *act = new Action(QIcon::fromTheme("edit-copy"), tr("Copy Selection"), &menu, this,
+            SLOT(selectionToClipboard()), QKeySequence::Copy);
+        menu.insertAction(sep, act);
+
+        QString searchSelectionText = selection();
+        if (searchSelectionText.length() > _webSearchSelectionTextMaxVisible)
+            searchSelectionText = searchSelectionText.left(_webSearchSelectionTextMaxVisible).append(QString::fromUtf8("…"));
+        searchSelectionText = tr("Search '%1'").arg(searchSelectionText);
+
+        QAction *webSearchAction = new Action(QIcon::fromTheme("edit-find"), searchSelectionText, &menu, this, SLOT(webSearchOnSelection()));
+        menu.insertAction(sep, webSearchAction);
+    }
+
     if (QtUi::mainWindow()->menuBar()->isHidden())
         menu.addAction(QtUi::actionCollection("General")->action("ToggleMenuBar"));
 
+    // show column reset action if columns have been resized in this session or there is at least one very narrow column
+    if ((_firstColHandlePos != _defaultFirstColHandlePos) || (_secondColHandlePos != _defaultSecondColHandlePos) ||
+        (_firstColHandlePos <= 10) || (_secondColHandlePos - _firstColHandlePos <= 10))
+        menu.addAction(new Action(tr("Reset Column Widths"), &menu, this, SLOT(resetColumnWidths()), 0));
+
     menu.exec(event->screenPos());
 }
 
@@ -992,12 +1030,34 @@ QString ChatScene::selection() const
             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 (_selectionMinCol == ChatLineModel::TimestampColumn) {
+                ChatItem *item = _lines[l]->item(ChatLineModel::TimestampColumn);
+                if (!_showSenderBrackets && !_timestampHasBrackets) {
+                    // Only re-add brackets if the current timestamp format does not include them
+                    // -and- sender brackets are disabled.  Don't filter on Message::Plain as
+                    // timestamp brackets affect all types of messages.
+                    // Remove any spaces before and after, otherwise it may look weird.
+                    result += QString("[%1] ").arg(item->data(MessageModel::DisplayRole)
+                                                   .toString().trimmed());
+                } else {
+                    result += item->data(MessageModel::DisplayRole).toString() + " ";
+                }
+            }
+            if (_selectionMinCol <= ChatLineModel::SenderColumn) {
+                ChatItem *item = _lines[l]->item(ChatLineModel::SenderColumn);
+                if (!_showSenderBrackets && item->chatLine()->msgType() == Message::Plain) {
+                    // Copying to plain-text.  Only re-add the sender brackets if they're normally
+                    // hidden.
+                    result += QString("<%1> ").arg(item->data(MessageModel::DisplayRole)
+                                                   .toString());
+                } else {
+                    result += item->data(MessageModel::DisplayRole).toString() + " ";
+                }
+            }
+            result += _lines[l]->item(ChatLineModel::ContentsColumn)
+                    ->data(MessageModel::DisplayRole).toString() + "\n";
         }
         return result;
     }
@@ -1044,6 +1104,21 @@ void ChatScene::clearSelection()
 }
 
 
+/******** *************************************************************************************/
+
+void ChatScene::webSearchOnSelection()
+{
+    if (!hasSelection())
+        return;
+
+    ChatViewSettings settings;
+    QString webSearchBaseUrl = settings.webSearchUrlFormatString();
+    QString webSearchUrl = webSearchBaseUrl.replace(QString("%s"), selection());
+    QUrl url = QUrl::fromUserInput(webSearchUrl);
+    QDesktopServices::openUrl(url);
+}
+
+
 /******** *************************************************************************************/
 
 void ChatScene::requestBacklog()
@@ -1127,9 +1202,9 @@ void ChatScene::updateSceneRect(const QRectF &rect)
 
 
 // ========================================
-//  Webkit Only stuff
+//  Webkit/WebEngine Only stuff
 // ========================================
-#ifdef HAVE_WEBKIT
+#if defined HAVE_WEBKIT || defined HAVE_WEBENGINE
 void ChatScene::loadWebPreview(ChatItem *parentItem, const QUrl &url, const QRectF &urlRect)
 {
     if (!_showWebPreview)
@@ -1258,8 +1333,47 @@ void ChatScene::clearWebPreview(ChatItem *parentItem)
 //  end of webkit only
 // ========================================
 
+// Local configuration caching
 void ChatScene::showWebPreviewChanged()
 {
     ChatViewSettings settings;
     _showWebPreview = settings.showWebPreview();
 }
+
+void ChatScene::showSenderBracketsChanged()
+{
+    ChatViewSettings settings;
+    _showSenderBrackets = settings.showSenderBrackets();
+}
+
+void ChatScene::timestampFormatStringChanged()
+{
+    ChatViewSettings settings;
+    _timestampFormatString = settings.timestampFormatString();
+    updateTimestampHasBrackets();
+}
+
+void ChatScene::updateTimestampHasBrackets()
+{
+    // Calculate these parameters only as needed, rather than on-demand
+
+    // Does the timestamp format contain brackets?  For example:
+    // Classic: "[hh:mm:ss]"
+    // Modern:  " hh:mm:ss"
+    //
+    // Match groups of any opening or closing brackets - (), {}, [], <>, (>, {], etc:
+    //   ^\s*[({[<].+[)}\]>]\s*$
+    //   [...]    is a character group containing ...
+    //   ^        matches start of string
+    //   \s*      matches any amount of whitespace
+    //   [({[<]   matches (, {, [, or <
+    //   .+       matches one or more characters
+    //   [)}\]>]  matches ), }, ], or >, escaping the ]
+    //   $        matches end of string
+    // Alternatively, if opening and closing brackets must be in pairs, use this:
+    //   (^\s*\(.+\)\s*$)|(^\s*\{.+\}\s*$)|(^\s*\[.+\]\s*$)|(^\s*<.+>\s*$)
+    // Note that '\' must be escaped as '\\'
+    // Helpful interactive website for debugging and explaining:  https://regex101.com/
+    const QRegExp regExpMatchBrackets("^\\s*[({[<].+[)}\\]>]\\s*$");
+    _timestampHasBrackets = regExpMatchBrackets.exactMatch(_timestampFormatString);
+}