multilineedit: handle unterminated mIRC codes
[quassel.git] / src / uisupport / multilineedit.cpp
index 8956484..a9a2ae2 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2015 by the Quassel Project                        *
+ *   Copyright (C) 2005-2019 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
+#include "multilineedit.h"
+
 #include <QApplication>
 #include <QMessageBox>
 #include <QScrollBar>
 
-#ifdef HAVE_SONNET
-#  include <Sonnet/SpellCheckDecorator>
-#endif
-
 #include "actioncollection.h"
 #include "bufferview.h"
 #include "graphicalui.h"
-#include "multilineedit.h"
 #include "tabcompleter.h"
 
 const int leftMargin = 3;
 
-MultiLineEdit::MultiLineEdit(QWidget *parent)
-    : MultiLineEditParent(parent),
-    _idx(0),
-    _mode(SingleLine),
-    _singleLine(true),
-    _minHeight(1),
-    _maxHeight(5),
-    _scrollBarsEnabled(true),
-    _pasteProtectionEnabled(true),
-    _emacsMode(false),
-    _lastDocumentHeight(-1)
+MultiLineEdit::MultiLineEdit(QWidget* parent)
+    : MultiLineEditParent(parent)
 {
     document()->setDocumentMargin(0);
 
@@ -53,15 +41,19 @@ MultiLineEdit::MultiLineEdit(QWidget *parent)
     enableFindReplace(false);
 #endif
 
-#ifdef HAVE_SONNET
-    new Sonnet::SpellCheckDecorator(this);
+#if defined HAVE_SONNET && !defined HAVE_KDE
+    _spellCheckDecorator = new Sonnet::SpellCheckDecorator(this);
+    highlighter()->setActive(highlighter()->checkerEnabledByDefault());
 #endif
 
     setMode(SingleLine);
     setLineWrapEnabled(false);
     reset();
 
-    connect(this, SIGNAL(textChanged()), this, SLOT(on_textChanged()));
+    // Prevent QTextHtmlImporter::appendNodeText from eating whitespace
+    document()->setDefaultStyleSheet("span { white-space: pre-wrap; }");
+
+    connect(this, &QTextEdit::textChanged, this, &MultiLineEdit::on_textChanged);
 
     _mircColorMap["00"] = "#ffffff";
     _mircColorMap["01"] = "#000000";
@@ -81,19 +73,42 @@ MultiLineEdit::MultiLineEdit(QWidget *parent)
     _mircColorMap["15"] = "#c0c0c0";
 }
 
+#if defined HAVE_SONNET && !defined HAVE_KDE
+Sonnet::Highlighter* MultiLineEdit::highlighter() const
+{
+    return _spellCheckDecorator->highlighter();
+}
+
+void MultiLineEdit::setSpellCheckEnabled(bool enabled)
+{
+    highlighter()->setActive(enabled);
+    if (enabled) {
+        highlighter()->slotRehighlight();
+    }
+}
 
-MultiLineEdit::~MultiLineEdit()
+void MultiLineEdit::contextMenuEvent(QContextMenuEvent* event)
 {
+    QMenu* menu = createStandardContextMenu();
+    menu->addSeparator();
+
+    auto action = menu->addAction(tr("Auto Spell Check"));
+    action->setCheckable(true);
+    action->setChecked(highlighter()->isActive());
+    connect(action, &QAction::toggled, this, &MultiLineEdit::setSpellCheckEnabled);
+
+    menu->exec(event->globalPos());
+    delete menu;
 }
 
+#endif
 
-void MultiLineEdit::setCustomFont(const QFont &font)
+void MultiLineEdit::setCustomFont(const QFontfont)
 {
     setFont(font);
     updateSizeHint();
 }
 
-
 void MultiLineEdit::setMode(Mode mode)
 {
     if (mode == _mode)
@@ -102,14 +117,12 @@ void MultiLineEdit::setMode(Mode mode)
     _mode = mode;
 }
 
-
 void MultiLineEdit::setLineWrapEnabled(bool enable)
 {
     setLineWrapMode(enable ? WidgetWidth : NoWrap);
     updateSizeHint();
 }
 
-
 void MultiLineEdit::setMinHeight(int lines)
 {
     if (lines == _minHeight)
@@ -119,7 +132,6 @@ void MultiLineEdit::setMinHeight(int lines)
     updateSizeHint();
 }
 
-
 void MultiLineEdit::setMaxHeight(int lines)
 {
     if (lines == _maxHeight)
@@ -129,7 +141,6 @@ void MultiLineEdit::setMaxHeight(int lines)
     updateSizeHint();
 }
 
-
 void MultiLineEdit::setScrollBarsEnabled(bool enable)
 {
     if (_scrollBarsEnabled == enable)
@@ -139,7 +150,6 @@ void MultiLineEdit::setScrollBarsEnabled(bool enable)
     updateScrollBars();
 }
 
-
 void MultiLineEdit::updateScrollBars()
 {
     QFontMetrics fm(font());
@@ -155,15 +165,13 @@ void MultiLineEdit::updateScrollBars()
         setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
 }
 
-
-void MultiLineEdit::resizeEvent(QResizeEvent *event)
+void MultiLineEdit::resizeEvent(QResizeEvent* event)
 {
     QTextEdit::resizeEvent(event);
     updateSizeHint();
     updateScrollBars();
 }
 
-
 void MultiLineEdit::updateSizeHint()
 {
     QFontMetrics fm(font());
@@ -173,58 +181,48 @@ void MultiLineEdit::updateSizeHint()
 
     // use the style to determine a decent size
     int h = qMin(qMax((int)document()->size().height() + scrollBarHeight, minPixelHeight), maxPixelHeight) + 2 * frameWidth();
-    QStyleOptionFrameV2 opt;
+
+    QStyleOptionFrame opt;
     opt.initFrom(this);
     opt.rect = QRect(0, 0, 100, h);
     opt.lineWidth = lineWidth();
     opt.midLineWidth = midLineWidth();
     opt.state |= QStyle::State_Sunken;
-    QSize s = style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(100, h).expandedTo(QApplication::globalStrut()), this);
+    QWidget* widget = this;
+#ifdef Q_OS_MAC
+    widget = 0;
+#endif
+    QSize s = style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(100, h).expandedTo(QApplication::globalStrut()), widget);
     if (s != _sizeHint) {
         _sizeHint = s;
         updateGeometry();
     }
 }
 
-
 QSize MultiLineEdit::sizeHint() const
 {
     if (!_sizeHint.isValid()) {
-        MultiLineEdit *that = const_cast<MultiLineEdit *>(this);
+        auto* that = const_cast<MultiLineEdit*>(this);
         that->updateSizeHint();
     }
     return _sizeHint;
 }
 
-
 QSize MultiLineEdit::minimumSizeHint() const
 {
     return sizeHint();
 }
 
-
 void MultiLineEdit::setEmacsMode(bool enable)
 {
     _emacsMode = enable;
 }
 
-
-void MultiLineEdit::setSpellCheckEnabled(bool enable)
-{
-#ifdef HAVE_KDE
-    setCheckSpellingEnabled(enable);
-#else
-    Q_UNUSED(enable)
-#endif
-}
-
-
-void MultiLineEdit::setPasteProtectionEnabled(bool enable, QWidget *)
+void MultiLineEdit::setPasteProtectionEnabled(bool enable, QWidget*)
 {
     _pasteProtectionEnabled = enable;
 }
 
-
 void MultiLineEdit::historyMoveBack()
 {
     addToHistory(convertRichtextToMircCodes(), true);
@@ -235,17 +233,16 @@ void MultiLineEdit::historyMoveBack()
     }
 }
 
-
 void MultiLineEdit::historyMoveForward()
 {
     addToHistory(convertRichtextToMircCodes(), true);
 
     if (_idx < _history.count()) {
         _idx++;
-        if (_idx < _history.count() || _tempHistory.contains(_idx)) // tempHistory might have an entry for idx == history.count() + 1
+        if (_idx < _history.count() || _tempHistory.contains(_idx))  // tempHistory might have an entry for idx == history.count() + 1
             showHistoryEntry();
         else
-            reset();        // equals clear() in this case
+            reset();  // equals clear() in this case
     }
     else {
         addToHistory(convertRichtextToMircCodes());
@@ -253,8 +250,7 @@ void MultiLineEdit::historyMoveForward()
     }
 }
 
-
-bool MultiLineEdit::addToHistory(const QString &text, bool temporary)
+bool MultiLineEdit::addToHistory(const QString& text, bool temporary)
 {
     if (text.isEmpty())
         return false;
@@ -280,14 +276,13 @@ bool MultiLineEdit::addToHistory(const QString &text, bool temporary)
     return false;
 }
 
-
-bool MultiLineEdit::event(QEvent *e)
+bool MultiLineEdit::event(QEvent* e)
 {
     // We need to make sure that global shortcuts aren't eaten
     if (e->type() == QEvent::ShortcutOverride) {
-        QKeyEvent *event = static_cast<QKeyEvent *>(e);
+        auto* event = static_cast<QKeyEvent*>(e);
         QKeySequence key = QKeySequence(event->key() | event->modifiers());
-        foreach(QAction *action, GraphicalUi::actionCollection()->actions()) {
+        foreach (QAction* action, GraphicalUi::actionCollection()->actions()) {
             if (action->shortcuts().contains(key)) {
                 e->ignore();
                 return false;
@@ -298,8 +293,7 @@ bool MultiLineEdit::event(QEvent *e)
     return MultiLineEditParent::event(e);
 }
 
-
-void MultiLineEdit::keyPressEvent(QKeyEvent *event)
+void MultiLineEdit::keyPressEvent(QKeyEvent* event)
 {
     if (event == QKeySequence::InsertLineSeparator) {
         if (_mode == SingleLine) {
@@ -320,7 +314,7 @@ void MultiLineEdit::keyPressEvent(QKeyEvent *event)
             if (!(event->modifiers() & Qt::ControlModifier)) {
                 int pos = textCursor().position();
                 moveCursor(QTextCursor::Up);
-                if (pos == textCursor().position()) // already on top line -> history
+                if (pos == textCursor().position())  // already on top line -> history
                     historyMoveBack();
             }
             else
@@ -336,7 +330,7 @@ void MultiLineEdit::keyPressEvent(QKeyEvent *event)
             if (!(event->modifiers() & Qt::ControlModifier)) {
                 int pos = textCursor().position();
                 moveCursor(QTextCursor::Down);
-                if (pos == textCursor().position()) // already on bottom line -> history
+                if (pos == textCursor().position())  // already on bottom line -> history
                     historyMoveForward();
             }
             else
@@ -390,9 +384,7 @@ void MultiLineEdit::keyPressEvent(QKeyEvent *event)
                 break;
             }
         }
-        else if (event->modifiers() & Qt::MetaModifier ||
-                 event->modifiers() & Qt::AltModifier)
-        {
+        else if (event->modifiers() & Qt::MetaModifier || event->modifiers() & Qt::AltModifier) {
             switch (event->key()) {
             case Qt::Key_Right:
                 moveCursor(QTextCursor::WordRight);
@@ -419,26 +411,24 @@ void MultiLineEdit::keyPressEvent(QKeyEvent *event)
                 cut();
                 return;
 
-            case Qt::Key_U: // uppercase word
+            case Qt::Key_U:  // uppercase word
                 moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
                 textCursor().insertText(textCursor().selectedText().toUpper());
                 return;
 
-            case Qt::Key_L: // lowercase word
+            case Qt::Key_L:  // lowercase word
                 moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
                 textCursor().insertText(textCursor().selectedText().toLower());
                 return;
 
-            case Qt::Key_C:
-            {           // capitalize word
+            case Qt::Key_C: {  // capitalize word
                 moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
                 QString const text = textCursor().selectedText();
                 textCursor().insertText(text.left(1).toUpper() + text.mid(1).toLower());
                 return;
             }
 
-            case Qt::Key_T:
-            {           // transpose words
+            case Qt::Key_T: {  // transpose words
                 moveCursor(QTextCursor::StartOfWord);
                 moveCursor(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
                 QString const word1 = textCursor().selectedText();
@@ -471,7 +461,6 @@ void MultiLineEdit::keyPressEvent(QKeyEvent *event)
 #endif
 }
 
-
 QString MultiLineEdit::convertRichtextToMircCodes()
 {
     bool underline, bold, italic, color;
@@ -523,7 +512,7 @@ QString MultiLineEdit::convertRichtextToMircCodes()
                 mircBgColor = _mircColorMap.key(cursor.charFormat().background().color().name());
 
                 if (mircFgColor.isEmpty()) {
-                    mircFgColor = "01"; //use black if the current foreground color can't be converted
+                    mircFgColor = "01";  // use black if the current foreground color can't be converted
                 }
 
                 mircText.append(mircFgColor);
@@ -574,8 +563,7 @@ QString MultiLineEdit::convertRichtextToMircCodes()
     return mircText;
 }
 
-
-bool MultiLineEdit::mircCodesChanged(QTextCursor &cursor, QTextCursor &peekcursor)
+bool MultiLineEdit::mircCodesChanged(QTextCursor& cursor, QTextCursor& peekcursor)
 {
     bool changed = false;
     if (cursor.charFormat().font().bold() != peekcursor.charFormat().font().bold())
@@ -591,8 +579,7 @@ bool MultiLineEdit::mircCodesChanged(QTextCursor &cursor, QTextCursor &peekcurso
     return changed;
 }
 
-
-QString MultiLineEdit::convertMircCodesToHtml(const QString &text)
+QString MultiLineEdit::convertMircCodesToHtml(const QString& text)
 {
     QStringList words;
     QRegExp mircCode = QRegExp("(\ 2|\1d|\1f|\ 3)", Qt::CaseSensitive);
@@ -605,7 +592,7 @@ QString MultiLineEdit::convertMircCodesToHtml(const QString &text)
 
         if (posRight < 0) {
             words << text.mid(posLeft);
-            break; // no more mirc color codes
+            break;  // no more mirc color codes
         }
 
         if (posLeft < posRight) {
@@ -614,6 +601,10 @@ QString MultiLineEdit::convertMircCodesToHtml(const QString &text)
         }
 
         posRight = text.indexOf(mircCode.cap(), posRight + 1);
+        if (posRight == -1) {
+            words << text.mid(posLeft);
+            break;  // unclosed color code; can't process
+        }
         words << text.mid(posLeft, posRight + 1 - posLeft);
         posLeft = posRight + 1;
     }
@@ -637,8 +628,8 @@ QString MultiLineEdit::convertMircCodesToHtml(const QString &text)
             int len = 3;
             QString fg = words[i].mid(pos + 1, 2);
             QString bg;
-            if (words[i][pos+3] == ',')
-                bg = words[i].mid(pos+4, 2);
+            if (words[i][pos + 3] == ',')
+                bg = words[i].mid(pos + 4, 2);
 
             style.append(" color:");
             style.append(_mircColorMap[fg]);
@@ -667,17 +658,19 @@ QString MultiLineEdit::convertMircCodesToHtml(const QString &text)
     return words.join("").replace("\n", "<br />");
 }
 
-
 void MultiLineEdit::on_returnPressed()
 {
     on_returnPressed(convertRichtextToMircCodes());
 }
 
-
-void MultiLineEdit::on_returnPressed(const QString &text)
+void MultiLineEdit::on_returnPressed(QString text)
 {
+    if (_completionSpace && text.endsWith(" ")) {
+        text.chop(1);
+    }
+
     if (!text.isEmpty()) {
-        foreach(const QString &line, text.split('\n', QString::SkipEmptyParts)) {
+        foreach (const QString& line, text.split('\n', QString::SkipEmptyParts)) {
             if (line.isEmpty())
                 continue;
             addToHistory(line);
@@ -691,9 +684,10 @@ void MultiLineEdit::on_returnPressed(const QString &text)
     }
 }
 
-
 void MultiLineEdit::on_textChanged()
 {
+    _completionSpace = qMax(_completionSpace - 1, 0);
+
     QString newText = text();
     newText.replace("\r\n", "\n");
     newText.replace('\r', '\n');
@@ -708,17 +702,13 @@ void MultiLineEdit::on_textChanged()
                 QString msg = tr("Do you really want to paste %n line(s)?", "", lines.count());
                 msg += "<p>";
                 for (int i = 0; i < 4; i++) {
-#if QT_VERSION < 0x050000
-                    msg += Qt::escape(lines[i].left(40));
-#else
                     msg += lines[i].left(40).toHtmlEscaped();
-#endif
                     if (lines[i].count() > 40)
                         msg += "...";
                     msg += "<br />";
                 }
                 msg += "...</p>";
-                QMessageBox question(QMessageBox::NoIcon, tr("Paste Protection"), msg, QMessageBox::Yes|QMessageBox::No);
+                QMessageBox question(QMessageBox::NoIcon, tr("Paste Protection"), msg, QMessageBox::Yes | QMessageBox::No);
                 question.setDefaultButton(QMessageBox::No);
 #ifdef Q_OS_MAC
                 question.setWindowFlags(question.windowFlags() | Qt::Sheet);
@@ -727,7 +717,7 @@ void MultiLineEdit::on_textChanged()
                     return;
             }
 
-            foreach(QString line, lines) {
+            foreach (QString line, lines) {
                 clear();
                 insert(line);
                 on_returnPressed();
@@ -745,34 +735,38 @@ void MultiLineEdit::on_textChanged()
     ensureCursorVisible();
 }
 
-
 void MultiLineEdit::on_documentHeightChanged(qreal)
 {
     updateScrollBars();
 }
 
-
 void MultiLineEdit::reset()
 {
     // every time the MultiLineEdit is cleared we also reset history index
     _idx = _history.count();
     clear();
     QTextBlockFormat format = textCursor().blockFormat();
-    format.setLeftMargin(leftMargin); // we want a little space between the frame and the contents
+    format.setLeftMargin(leftMargin);  // we want a little space between the frame and the contents
     textCursor().setBlockFormat(format);
     updateScrollBars();
 }
 
-
 void MultiLineEdit::showHistoryEntry()
 {
     // if the user changed the history, display the changed line
     setHtml(convertMircCodesToHtml(_tempHistory.contains(_idx) ? _tempHistory[_idx] : _history[_idx]));
     QTextCursor cursor = textCursor();
     QTextBlockFormat format = cursor.blockFormat();
-    format.setLeftMargin(leftMargin); // we want a little space between the frame and the contents
+    format.setLeftMargin(leftMargin);  // we want a little space between the frame and the contents
     cursor.setBlockFormat(format);
     cursor.movePosition(QTextCursor::End);
     setTextCursor(cursor);
     updateScrollBars();
 }
+
+void MultiLineEdit::addCompletionSpace()
+{
+    // Inserting the space emits textChanged, which should not disable removal
+    _completionSpace = 2;
+    insertPlainText(" ");
+}