multilineedit: handle unterminated mIRC codes
[quassel.git] / src / uisupport / multilineedit.cpp
index b856954..a9a2ae2 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2010 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  *
  *   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 "multilineedit.h"
+
 #include <QApplication>
-#include <QMenu>
 #include <QMessageBox>
 #include <QScrollBar>
 
 #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),
-    _lastDocumentHeight(-1)
-{
-#if QT_VERSION >= 0x040500
-  document()->setDocumentMargin(0); // new in Qt 4.5 and we really don't want it here
-#endif
+MultiLineEdit::MultiLineEdit(QWidget* parent)
+    : MultiLineEditParent(parent)
+{
+    document()->setDocumentMargin(0);
 
-  setAcceptRichText(false);
+    setAcceptRichText(false);
 #ifdef HAVE_KDE
-  enableFindReplace(false);
+    enableFindReplace(false);
 #endif
 
-  setMode(SingleLine);
-  setWordWrapEnabled(false);
-  reset();
-
-  connect(this, SIGNAL(textChanged()), this, SLOT(on_textChanged()));
+#if defined HAVE_SONNET && !defined HAVE_KDE
+    _spellCheckDecorator = new Sonnet::SpellCheckDecorator(this);
+    highlighter()->setActive(highlighter()->checkerEnabledByDefault());
+#endif
 
-  _mircColorMap["00"] = "#ffffff";
-  _mircColorMap["01"] = "#000000";
-  _mircColorMap["02"] = "#000080";
-  _mircColorMap["03"] = "#008000";
-  _mircColorMap["04"] = "#ff0000";
-  _mircColorMap["05"] = "#800000";
-  _mircColorMap["06"] = "#800080";
-  _mircColorMap["07"] = "#ffa500";
-  _mircColorMap["08"] = "#ffff00";
-  _mircColorMap["09"] = "#00ff00";
-  _mircColorMap["10"] = "#008080";
-  _mircColorMap["11"] = "#00ffff";
-  _mircColorMap["12"] = "#4169e1";
-  _mircColorMap["13"] = "#ff00ff";
-  _mircColorMap["14"] = "#808080";
-  _mircColorMap["15"] = "#c0c0c0";
+    setMode(SingleLine);
+    setLineWrapEnabled(false);
+    reset();
 
+    // 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";
+    _mircColorMap["02"] = "#000080";
+    _mircColorMap["03"] = "#008000";
+    _mircColorMap["04"] = "#ff0000";
+    _mircColorMap["05"] = "#800000";
+    _mircColorMap["06"] = "#800080";
+    _mircColorMap["07"] = "#ffa500";
+    _mircColorMap["08"] = "#ffff00";
+    _mircColorMap["09"] = "#00ff00";
+    _mircColorMap["10"] = "#008080";
+    _mircColorMap["11"] = "#00ffff";
+    _mircColorMap["12"] = "#4169e1";
+    _mircColorMap["13"] = "#ff00ff";
+    _mircColorMap["14"] = "#808080";
+    _mircColorMap["15"] = "#c0c0c0";
 }
 
-MultiLineEdit::~MultiLineEdit() {
+#if defined HAVE_SONNET && !defined HAVE_KDE
+Sonnet::Highlighter* MultiLineEdit::highlighter() const
+{
+    return _spellCheckDecorator->highlighter();
 }
 
-void MultiLineEdit::setCustomFont(const QFont &font) {
-  setFont(font);
-  updateSizeHint();
+void MultiLineEdit::setSpellCheckEnabled(bool enabled)
+{
+    highlighter()->setActive(enabled);
+    if (enabled) {
+        highlighter()->slotRehighlight();
+    }
 }
 
-void MultiLineEdit::setMode(Mode mode) {
-  if(mode == _mode)
-    return;
+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);
 
-  _mode = mode;
+    menu->exec(event->globalPos());
+    delete menu;
 }
 
-void MultiLineEdit::setMinHeight(int lines) {
-  if(lines == _minHeight)
-    return;
+#endif
 
-  _minHeight = lines;
-  updateSizeHint();
+void MultiLineEdit::setCustomFont(const QFont& font)
+{
+    setFont(font);
+    updateSizeHint();
 }
 
-void MultiLineEdit::setMaxHeight(int lines) {
-  if(lines == _maxHeight)
-    return;
+void MultiLineEdit::setMode(Mode mode)
+{
+    if (mode == _mode)
+        return;
 
-  _maxHeight = lines;
-  updateSizeHint();
+    _mode = mode;
 }
 
-void MultiLineEdit::setScrollBarsEnabled(bool enable) {
-  if(_scrollBarsEnabled == enable)
-    return;
-
-  _scrollBarsEnabled = enable;
-  updateScrollBars();
+void MultiLineEdit::setLineWrapEnabled(bool enable)
+{
+    setLineWrapMode(enable ? WidgetWidth : NoWrap);
+    updateSizeHint();
 }
 
-void MultiLineEdit::updateScrollBars() {
-  QFontMetrics fm(font());
-  int _maxPixelHeight = fm.lineSpacing() * _maxHeight;
-  if(_scrollBarsEnabled && document()->size().height() > _maxPixelHeight)
-    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
-  else
-    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+void MultiLineEdit::setMinHeight(int lines)
+{
+    if (lines == _minHeight)
+        return;
 
-  if(!_scrollBarsEnabled || isSingleLine())
-    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-  else
-    setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+    _minHeight = lines;
+    updateSizeHint();
 }
 
-void MultiLineEdit::resizeEvent(QResizeEvent *event) {
-  QTextEdit::resizeEvent(event);
-  updateSizeHint();
-  updateScrollBars();
+void MultiLineEdit::setMaxHeight(int lines)
+{
+    if (lines == _maxHeight)
+        return;
+
+    _maxHeight = lines;
+    updateSizeHint();
 }
 
-void MultiLineEdit::updateSizeHint() {
-  QFontMetrics fm(font());
-  int minPixelHeight = fm.lineSpacing() * _minHeight;
-  int maxPixelHeight = fm.lineSpacing() * _maxHeight;
-  int scrollBarHeight = horizontalScrollBar()->isVisible() ? horizontalScrollBar()->height() : 0;
+void MultiLineEdit::setScrollBarsEnabled(bool enable)
+{
+    if (_scrollBarsEnabled == enable)
+        return;
 
-  // use the style to determine a decent size
-  int h = qMin(qMax((int)document()->size().height() + scrollBarHeight, minPixelHeight), maxPixelHeight) + 2 * frameWidth();
-  QStyleOptionFrameV2 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);
-  if(s != _sizeHint) {
-    _sizeHint = s;
-    updateGeometry();
-  }
+    _scrollBarsEnabled = enable;
+    updateScrollBars();
 }
 
-QSize MultiLineEdit::sizeHint() const {
-  if(!_sizeHint.isValid()) {
-    MultiLineEdit *that = const_cast<MultiLineEdit *>(this);
-    that->updateSizeHint();
-  }
-  return _sizeHint;
-}
+void MultiLineEdit::updateScrollBars()
+{
+    QFontMetrics fm(font());
+    int _maxPixelHeight = fm.lineSpacing() * _maxHeight;
+    if (_scrollBarsEnabled && document()->size().height() > _maxPixelHeight)
+        setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+    else
+        setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
 
-QSize MultiLineEdit::minimumSizeHint() const {
-  return sizeHint();
+    if (!_scrollBarsEnabled || isSingleLine())
+        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+    else
+        setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
 }
 
-void MultiLineEdit::setEmacsMode(bool enable) {
-  _emacsMode = enable;
+void MultiLineEdit::resizeEvent(QResizeEvent* event)
+{
+    QTextEdit::resizeEvent(event);
+    updateSizeHint();
+    updateScrollBars();
 }
 
-void MultiLineEdit::setSpellCheckEnabled(bool enable) {
-#ifdef HAVE_KDE
-  setCheckSpellingEnabled(enable);
-#else
-  Q_UNUSED(enable)
+void MultiLineEdit::updateSizeHint()
+{
+    QFontMetrics fm(font());
+    int minPixelHeight = fm.lineSpacing() * _minHeight;
+    int maxPixelHeight = fm.lineSpacing() * _maxHeight;
+    int scrollBarHeight = horizontalScrollBar()->isVisible() ? horizontalScrollBar()->height() : 0;
+
+    // use the style to determine a decent size
+    int h = qMin(qMax((int)document()->size().height() + scrollBarHeight, minPixelHeight), maxPixelHeight) + 2 * frameWidth();
+
+    QStyleOptionFrame opt;
+    opt.initFrom(this);
+    opt.rect = QRect(0, 0, 100, h);
+    opt.lineWidth = lineWidth();
+    opt.midLineWidth = midLineWidth();
+    opt.state |= QStyle::State_Sunken;
+    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();
+    }
 }
 
-void MultiLineEdit::setWordWrapEnabled(bool enable) {
-  setLineWrapMode(enable? WidgetWidth : NoWrap);
-  updateSizeHint();
+QSize MultiLineEdit::sizeHint() const
+{
+    if (!_sizeHint.isValid()) {
+        auto* that = const_cast<MultiLineEdit*>(this);
+        that->updateSizeHint();
+    }
+    return _sizeHint;
 }
 
-void MultiLineEdit::setPasteProtectionEnabled(bool enable, QWidget *) {
-  _pasteProtectionEnabled = enable;
+QSize MultiLineEdit::minimumSizeHint() const
+{
+    return sizeHint();
 }
 
-void MultiLineEdit::historyMoveBack() {
-  addToHistory(convertRichtextToMircCodes(), true);
-
-  if(_idx > 0) {
-    _idx--;
-    showHistoryEntry();
-  }
+void MultiLineEdit::setEmacsMode(bool enable)
+{
+    _emacsMode = enable;
 }
 
-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
-      showHistoryEntry();
-    else
-      reset();              // equals clear() in this case
-  } else {
-    addToHistory(convertRichtextToMircCodes());
-    reset();
-  }
+void MultiLineEdit::setPasteProtectionEnabled(bool enable, QWidget*)
+{
+    _pasteProtectionEnabled = enable;
 }
 
-bool MultiLineEdit::addToHistory(const QString &text, bool temporary) {
-  if(text.isEmpty())
-    return false;
-
-  Q_ASSERT(0 <= _idx && _idx <= _history.count());
+void MultiLineEdit::historyMoveBack()
+{
+    addToHistory(convertRichtextToMircCodes(), true);
 
-  if(temporary) {
-    // if an entry of the history is changed, we remember it and show it again at this
-    // position until a line was actually sent
-    // sent lines get appended to the history
-    if(_history.isEmpty() || text != _history[_idx - (int)(_idx == _history.count())]) {
-      _tempHistory[_idx] = text;
-      return true;
+    if (_idx > 0) {
+        _idx--;
+        showHistoryEntry();
     }
-  } else {
-    if(_history.isEmpty() || text != _history.last()) {
-      _history << text;
-      _tempHistory.clear();
-      return true;
+}
+
+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
+            showHistoryEntry();
+        else
+            reset();  // equals clear() in this case
     }
-  }
-  return false;
-}
-
-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);
-    QKeySequence key = QKeySequence(event->key() | event->modifiers());
-    foreach(QAction *action, GraphicalUi::actionCollection()->actions()) {
-      if(action->shortcuts().contains(key)) {
-        e->ignore();
-        return false;
-      }
+    else {
+        addToHistory(convertRichtextToMircCodes());
+        reset();
     }
-  }
-
-  return MultiLineEditParent::event(e);
 }
 
-void MultiLineEdit::keyPressEvent(QKeyEvent *event) {
-  // Workaround the fact that Qt < 4.5 doesn't know InsertLineSeparator yet
-#if QT_VERSION >= 0x040500
-  if(event == QKeySequence::InsertLineSeparator) {
-#else
+bool MultiLineEdit::addToHistory(const QString& text, bool temporary)
+{
+    if (text.isEmpty())
+        return false;
 
-# ifdef Q_WS_MAC
-  if((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && event->modifiers() & Qt::META) {
-# else
-  if((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) && event->modifiers() & Qt::SHIFT) {
-# endif
-#endif
+    Q_ASSERT(0 <= _idx && _idx <= _history.count());
 
-    if(_mode == SingleLine) {
-      event->accept();
-      on_returnPressed();
-      return;
+    if (temporary) {
+        // if an entry of the history is changed, we remember it and show it again at this
+        // position until a line was actually sent
+        // sent lines get appended to the history
+        if (_history.isEmpty() || text != _history[_idx - (int)(_idx == _history.count())]) {
+            _tempHistory[_idx] = text;
+            return true;
+        }
     }
-    MultiLineEditParent::keyPressEvent(event);
-    return;
-  }
-
-  switch(event->key()) {
-  case Qt::Key_Up:
-    if(event->modifiers() & Qt::ShiftModifier)
-      break;
-    {
-      event->accept();
-      if(!(event->modifiers() & Qt::ControlModifier)) {
-        int pos = textCursor().position();
-        moveCursor(QTextCursor::Up);
-        if(pos == textCursor().position()) // already on top line -> history
-          historyMoveBack();
-      } else
-        historyMoveBack();
-      return;
+    else {
+        if (_history.isEmpty() || text != _history.last()) {
+            _history << text;
+            _tempHistory.clear();
+            return true;
+        }
     }
+    return false;
+}
 
-  case Qt::Key_Down:
-    if(event->modifiers() & Qt::ShiftModifier)
-      break;
-    {
-      event->accept();
-      if(!(event->modifiers() & Qt::ControlModifier)) {
-        int pos = textCursor().position();
-        moveCursor(QTextCursor::Down);
-        if(pos == textCursor().position()) // already on bottom line -> history
-          historyMoveForward();
-      } else
-        historyMoveForward();
-      return;
+bool MultiLineEdit::event(QEvent* e)
+{
+    // We need to make sure that global shortcuts aren't eaten
+    if (e->type() == QEvent::ShortcutOverride) {
+        auto* event = static_cast<QKeyEvent*>(e);
+        QKeySequence key = QKeySequence(event->key() | event->modifiers());
+        foreach (QAction* action, GraphicalUi::actionCollection()->actions()) {
+            if (action->shortcuts().contains(key)) {
+                e->ignore();
+                return false;
+            }
+        }
     }
 
-  case Qt::Key_Return:
-  case Qt::Key_Enter:
-  case Qt::Key_Select:
-    event->accept();
-    on_returnPressed();
-    return;
-
-  // We don't want to have the tab key react even if no completer is installed
-  case Qt::Key_Tab:
-    event->accept();
-    return;
-
-  default:
-    ;
-  }
-
-  if(_emacsMode) {
-    if(event->modifiers() & Qt::ControlModifier) {
-      switch(event->key()) {
-        // move
-      case Qt::Key_A:
-        moveCursor(QTextCursor::StartOfLine);
-        return;
-      case Qt::Key_E:
-        moveCursor(QTextCursor::EndOfLine);
-        return;
-      case Qt::Key_F:
-        moveCursor(QTextCursor::Right);
-        return;
-      case Qt::Key_B:
-        moveCursor(QTextCursor::Left);
-        return;
+    return MultiLineEditParent::event(e);
+}
 
-        // modify
-      case Qt::Key_Y:
-        paste();
-        return;
-      case Qt::Key_K:
-        moveCursor(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
-        cut();
+void MultiLineEdit::keyPressEvent(QKeyEvent* event)
+{
+    if (event == QKeySequence::InsertLineSeparator) {
+        if (_mode == SingleLine) {
+            event->accept();
+            on_returnPressed();
+            return;
+        }
+        MultiLineEditParent::keyPressEvent(event);
         return;
-      }
     }
-    else if(event->modifiers() & Qt::MetaModifier ||
-            event->modifiers() & Qt::AltModifier)
-    {
-      switch(event->key()) {
-      case Qt::Key_Right:
-        moveCursor(QTextCursor::WordRight);
-        return;
-      case Qt::Key_Left:
-        moveCursor(QTextCursor::WordLeft);
-        return;
-      case Qt::Key_F:
-        moveCursor(QTextCursor::WordRight);
-        return;
-      case Qt::Key_B:
-        moveCursor(QTextCursor::WordLeft);
-        return;
-      case Qt::Key_Less:
-        moveCursor(QTextCursor::Start);
+
+    switch (event->key()) {
+    case Qt::Key_Up:
+        if (event->modifiers() & Qt::ShiftModifier)
+            break;
+        {
+            event->accept();
+            if (!(event->modifiers() & Qt::ControlModifier)) {
+                int pos = textCursor().position();
+                moveCursor(QTextCursor::Up);
+                if (pos == textCursor().position())  // already on top line -> history
+                    historyMoveBack();
+            }
+            else
+                historyMoveBack();
+            return;
+        }
+
+    case Qt::Key_Down:
+        if (event->modifiers() & Qt::ShiftModifier)
+            break;
+        {
+            event->accept();
+            if (!(event->modifiers() & Qt::ControlModifier)) {
+                int pos = textCursor().position();
+                moveCursor(QTextCursor::Down);
+                if (pos == textCursor().position())  // already on bottom line -> history
+                    historyMoveForward();
+            }
+            else
+                historyMoveForward();
+            return;
+        }
+
+    case Qt::Key_Return:
+    case Qt::Key_Enter:
+    case Qt::Key_Select:
+        event->accept();
+        on_returnPressed();
         return;
-      case Qt::Key_Greater:
-        moveCursor(QTextCursor::End);
+
+    // We don't want to have the tab key react even if no completer is installed
+    case Qt::Key_Tab:
+        event->accept();
         return;
-      }
+
+    default:
+        ;
+    }
+
+    if (_emacsMode) {
+        if (event->modifiers() & Qt::ControlModifier) {
+            switch (event->key()) {
+            // move
+            case Qt::Key_A:
+                moveCursor(QTextCursor::StartOfLine);
+                return;
+            case Qt::Key_E:
+                moveCursor(QTextCursor::EndOfLine);
+                return;
+            case Qt::Key_F:
+                moveCursor(QTextCursor::Right);
+                return;
+            case Qt::Key_B:
+                moveCursor(QTextCursor::Left);
+                return;
+
+            // modify
+            case Qt::Key_Y:
+                paste();
+                return;
+            case Qt::Key_K:
+                moveCursor(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
+                cut();
+                return;
+
+            default:
+                break;
+            }
+        }
+        else if (event->modifiers() & Qt::MetaModifier || event->modifiers() & Qt::AltModifier) {
+            switch (event->key()) {
+            case Qt::Key_Right:
+                moveCursor(QTextCursor::WordRight);
+                return;
+            case Qt::Key_Left:
+                moveCursor(QTextCursor::WordLeft);
+                return;
+            case Qt::Key_F:
+                moveCursor(QTextCursor::WordRight);
+                return;
+            case Qt::Key_B:
+                moveCursor(QTextCursor::WordLeft);
+                return;
+            case Qt::Key_Less:
+                moveCursor(QTextCursor::Start);
+                return;
+            case Qt::Key_Greater:
+                moveCursor(QTextCursor::End);
+                return;
+
+            // modify
+            case Qt::Key_D:
+                moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
+                cut();
+                return;
+
+            case Qt::Key_U:  // uppercase word
+                moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
+                textCursor().insertText(textCursor().selectedText().toUpper());
+                return;
+
+            case Qt::Key_L:  // lowercase word
+                moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
+                textCursor().insertText(textCursor().selectedText().toLower());
+                return;
+
+            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
+                moveCursor(QTextCursor::StartOfWord);
+                moveCursor(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
+                QString const word1 = textCursor().selectedText();
+                textCursor().clearSelection();
+                moveCursor(QTextCursor::WordRight);
+                moveCursor(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
+                QString const word2 = textCursor().selectedText();
+                if (!word2.isEmpty() && !word1.isEmpty()) {
+                    textCursor().insertText(word1);
+                    moveCursor(QTextCursor::WordLeft);
+                    moveCursor(QTextCursor::WordLeft);
+                    moveCursor(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
+                    textCursor().insertText(word2);
+                    moveCursor(QTextCursor::WordRight);
+                    moveCursor(QTextCursor::EndOfWord);
+                }
+                return;
+            }
+
+            default:
+                break;
+            }
+        }
     }
-  }
 
 #ifdef HAVE_KDE
-  KTextEdit::keyPressEvent(event);
+    KTextEdit::keyPressEvent(event);
 #else
-  QTextEdit::keyPressEvent(event);
+    QTextEdit::keyPressEvent(event);
 #endif
 }
 
-QString MultiLineEdit::convertRichtextToMircCodes() {
-  bool underline, bold, italic, color;
-  QString mircText, mircFgColor, mircBgColor;
-  QTextCursor cursor = textCursor();
-  QTextCursor peekcursor = textCursor();
-  cursor.movePosition(QTextCursor::Start);
-
-  underline = bold = italic = color = false;
+QString MultiLineEdit::convertRichtextToMircCodes()
+{
+    bool underline, bold, italic, color;
+    QString mircText, mircFgColor, mircBgColor;
+    QTextCursor cursor = textCursor();
+    QTextCursor peekcursor = textCursor();
+    cursor.movePosition(QTextCursor::Start);
+
+    underline = bold = italic = color = false;
+
+    while (cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor)) {
+        if (cursor.selectedText() == QString(QChar(QChar::LineSeparator))
+            || cursor.selectedText() == QString(QChar(QChar::ParagraphSeparator))) {
+            if (color) {
+                color = false;
+                mircText.append('\x03');
+            }
+            if (underline) {
+                underline = false;
+                mircText.append('\x1f');
+            }
+            if (italic) {
+                italic = false;
+                mircText.append('\x1d');
+            }
+            if (bold) {
+                bold = false;
+                mircText.append('\x02');
+            }
+            mircText.append('\n');
+        }
+        else {
+            if (!bold && cursor.charFormat().font().bold()) {
+                bold = true;
+                mircText.append('\x02');
+            }
+            if (!italic && cursor.charFormat().fontItalic()) {
+                italic = true;
+                mircText.append('\x1d');
+            }
+            if (!underline && cursor.charFormat().fontUnderline()) {
+                underline = true;
+                mircText.append('\x1f');
+            }
+            if (!color && (cursor.charFormat().foreground().isOpaque() || cursor.charFormat().background().isOpaque())) {
+                color = true;
+                mircText.append('\x03');
+                mircFgColor = _mircColorMap.key(cursor.charFormat().foreground().color().name());
+                mircBgColor = _mircColorMap.key(cursor.charFormat().background().color().name());
+
+                if (mircFgColor.isEmpty()) {
+                    mircFgColor = "01";  // use black if the current foreground color can't be converted
+                }
+
+                mircText.append(mircFgColor);
+                if (cursor.charFormat().background().isOpaque())
+                    mircText.append("," + mircBgColor);
+            }
+
+            mircText.append(cursor.selectedText());
+
+            peekcursor.setPosition(cursor.position());
+            peekcursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
+
+            if (mircCodesChanged(cursor, peekcursor)) {
+                if (color) {
+                    color = false;
+                    mircText.append('\x03');
+                }
+                if (underline) {
+                    underline = false;
+                    mircText.append('\x1f');
+                }
+                if (italic) {
+                    italic = false;
+                    mircText.append('\x1d');
+                }
+                if (bold) {
+                    bold = false;
+                    mircText.append('\x02');
+                }
+            }
+        }
 
-  while (cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor)) {
+        cursor.clearSelection();
+    }
 
-    if (cursor.selectedText() == QString(QChar(QChar::LineSeparator))
-      || cursor.selectedText() == QString(QChar(QChar::ParagraphSeparator))) {
-      if (color) {
-        color = false;
+    if (color)
         mircText.append('\x03');
-      }
-      if (underline) {
-        underline = false;
+
+    if (underline)
         mircText.append('\x1f');
-      }
-      if (italic) {
-        italic = false;
+
+    if (italic)
         mircText.append('\x1d');
-      }
-      if (bold) {
-        bold = false;
-        mircText.append('\x02');
-      }
-      mircText.append('\n');
-    }
-    else {
-      if (!bold && cursor.charFormat().font().bold()) {
-        bold = true;
+
+    if (bold)
         mircText.append('\x02');
-      }
-      if (!italic && cursor.charFormat().fontItalic()) {
-        italic = true;
-        mircText.append('\x1d');
-      }
-      if (!underline && cursor.charFormat().fontUnderline()) {
-        underline = true;
-        mircText.append('\x1f');
-      }
-      if (!color && (cursor.charFormat().foreground().isOpaque() || cursor.charFormat().background().isOpaque())) {
-        color = true;
-        mircText.append('\x03');
-        mircFgColor = _mircColorMap.key(cursor.charFormat().foreground().color().name());
-        mircBgColor = _mircColorMap.key(cursor.charFormat().background().color().name());
 
-        if (mircFgColor.isEmpty()) {
-            mircFgColor = "01"; //use black if the current foreground color can't be converted
-        }
+    return mircText;
+}
 
-        mircText.append(mircFgColor);
-        if (cursor.charFormat().background().isOpaque())
-          mircText.append("," + mircBgColor);
-      }
+bool MultiLineEdit::mircCodesChanged(QTextCursor& cursor, QTextCursor& peekcursor)
+{
+    bool changed = false;
+    if (cursor.charFormat().font().bold() != peekcursor.charFormat().font().bold())
+        changed = true;
+    if (cursor.charFormat().fontItalic() != peekcursor.charFormat().fontItalic())
+        changed = true;
+    if (cursor.charFormat().fontUnderline() != peekcursor.charFormat().fontUnderline())
+        changed = true;
+    if (cursor.charFormat().foreground().color() != peekcursor.charFormat().foreground().color())
+        changed = true;
+    if (cursor.charFormat().background().color() != peekcursor.charFormat().background().color())
+        changed = true;
+    return changed;
+}
+
+QString MultiLineEdit::convertMircCodesToHtml(const QString& text)
+{
+    QStringList words;
+    QRegExp mircCode = QRegExp("(\ 2|\1d|\1f|\ 3)", Qt::CaseSensitive);
 
-      mircText.append(cursor.selectedText());
+    int posLeft = 0;
+    int posRight = 0;
 
-      peekcursor.setPosition(cursor.position());
-      peekcursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
+    for (;;) {
+        posRight = mircCode.indexIn(text, posLeft);
 
-      if (mircCodesChanged(cursor, peekcursor)) {
-        if (color) {
-          color = false;
-          mircText.append('\x03');
-        }
-        if (underline) {
-          underline = false;
-          mircText.append('\x1f');
+        if (posRight < 0) {
+            words << text.mid(posLeft);
+            break;  // no more mirc color codes
         }
-        if (italic) {
-          italic = false;
-          mircText.append('\x1d');
+
+        if (posLeft < posRight) {
+            words << text.mid(posLeft, posRight - posLeft);
+            posLeft = posRight;
         }
-        if (bold) {
-          bold = false;
-          mircText.append('\x02');
+
+        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;
     }
 
-    cursor.clearSelection();
-  }
-  if (color) {
-    color = false;
-    mircText.append('\x03');
-  }
-  if (underline) {
-    underline = false;
-    mircText.append('\x1f');
-  }
-  if (italic) {
-    italic = false;
-    mircText.append('\x1d');
-  }
-  if (bold) {
-    bold = false;
-    mircText.append('\x02');
-  }
-
-  return mircText;
-}
-
-bool MultiLineEdit::mircCodesChanged(QTextCursor &cursor, QTextCursor &peekcursor) {
-  bool changed = false;
-  if (cursor.charFormat().font().bold() != peekcursor.charFormat().font().bold())
-    changed = true;
-  if (cursor.charFormat().fontItalic() != peekcursor.charFormat().fontItalic())
-    changed = true;
-  if (cursor.charFormat().fontUnderline() != peekcursor.charFormat().fontUnderline())
-    changed = true;
-  if (cursor.charFormat().foreground().color() != peekcursor.charFormat().foreground().color())
-    changed = true;
-  if (cursor.charFormat().background().color() != peekcursor.charFormat().background().color())
-    changed = true;
-  return changed;
-}
-
-QString MultiLineEdit::convertMircCodesToHtml(const QString &text) {
-  QStringList words;
-  QRegExp mircCode = QRegExp("(\ 2|\1d|\1f|\ 3)", Qt::CaseSensitive);
-
-  int posLeft = 0;
-  int posRight = 0;
-
-  for(;;) {
-    posRight = mircCode.indexIn(text, posLeft);
-
-    if(posRight < 0) {
-      words << text.mid(posLeft);
-      break; // no more mirc color codes
+    for (int i = 0; i < words.count(); i++) {
+        QString style;
+        if (words[i].contains('\x02')) {
+            style.append(" font-weight:600;");
+            words[i].replace('\x02', "");
+        }
+        if (words[i].contains('\x1d')) {
+            style.append(" font-style:italic;");
+            words[i].replace('\x1d', "");
+        }
+        if (words[i].contains('\x1f')) {
+            style.append(" text-decoration: underline;");
+            words[i].replace('\x1f', "");
+        }
+        if (words[i].contains('\x03')) {
+            int pos = words[i].indexOf('\x03');
+            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);
+
+            style.append(" color:");
+            style.append(_mircColorMap[fg]);
+            style.append(";");
+
+            if (!bg.isEmpty()) {
+                style.append(" background-color:");
+                style.append(_mircColorMap[bg]);
+                style.append(";");
+                len = 6;
+            }
+            words[i].replace(pos, len, "");
+            words[i].replace('\x03', "");
+        }
+        words[i].replace("&", "&amp;");
+        words[i].replace("<", "&lt;");
+        words[i].replace(">", "&gt;");
+        words[i].replace("\"", "&quot;");
+        if (style.isEmpty()) {
+            words[i] = "<span>" + words[i] + "</span>";
+        }
+        else {
+            words[i] = "<span style=\"" + style + "\">" + words[i] + "</span>";
+        }
     }
+    return words.join("").replace("\n", "<br />");
+}
 
-    if (posLeft < posRight) {
-      words << text.mid(posLeft, posRight - posLeft);
-      posLeft = posRight;
+void MultiLineEdit::on_returnPressed()
+{
+    on_returnPressed(convertRichtextToMircCodes());
+}
+
+void MultiLineEdit::on_returnPressed(QString text)
+{
+    if (_completionSpace && text.endsWith(" ")) {
+        text.chop(1);
     }
 
-    posRight = text.indexOf(mircCode.cap(), posRight + 1);
-    words << text.mid(posLeft, posRight + 1 - posLeft);
-    posLeft = posRight + 1;
-  }
-
-  for (int i = 0; i < words.count(); i++) {
-      QString style;
-      if (words[i].contains('\x02')) {
-        style.append(" font-weight:600;");
-        words[i].replace('\x02',"");
-      }
-      if (words[i].contains('\x1d')) {
-        style.append(" font-style:italic;");
-        words[i].replace('\x1d',"");
-      }
-      if (words[i].contains('\x1f')) {
-        style.append(" text-decoration: underline;");
-        words[i].replace('\x1f',"");
-      }
-      if (words[i].contains('\x03')) {
-        int pos = words[i].indexOf('\x03');
-        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);
-
-        style.append(" color:");
-        style.append(_mircColorMap[fg]);
-        style.append(";");
-
-        if (!bg.isEmpty()) {
-          style.append(" background-color:");
-          style.append(_mircColorMap[bg]);
-          style.append(";");
-          len = 6;
+    if (!text.isEmpty()) {
+        foreach (const QString& line, text.split('\n', QString::SkipEmptyParts)) {
+            if (line.isEmpty())
+                continue;
+            addToHistory(line);
+            emit textEntered(line);
         }
-        words[i].replace(pos, len, "");
-        words[i].replace('\x03',"");
-      }
-      words[i].replace("&","&amp;");
-      words[i].replace("<", "&lt;");
-      words[i].replace(">", "&gt;");
-      words[i].replace("\"", "&quot;");
-      if (style.isEmpty()) {
-        words[i] = "<span>" + words[i] + "</span>";
-      }
-      else {
-        words[i] = "<span style=\"" + style + "\">" + words[i] + "</span>";
-      }
-  }
-  return words.join("").replace("\n","<br />");
-}
-
-void MultiLineEdit::on_returnPressed() {
-  on_returnPressed(convertRichtextToMircCodes());
-}
-
-void MultiLineEdit::on_returnPressed(const QString & text) {
-  if(!text.isEmpty()) {
-    foreach(const QString &line, text.split('\n', QString::SkipEmptyParts)) {
-      if(line.isEmpty())
-        continue;
-      addToHistory(line);
-      emit textEntered(line);
+        reset();
+        _tempHistory.clear();
     }
-    reset();
-    _tempHistory.clear();
-  } else {
-    emit noTextEntered();
-  }
-}
-
-void MultiLineEdit::on_textChanged() {
-  QString newText = text();
-  newText.replace("\r\n", "\n");
-  newText.replace('\r', '\n');
-  if(_mode == SingleLine) {
-    if(!pasteProtectionEnabled())
-      newText.replace('\n', ' ');
-    else if(newText.contains('\n')) {
-      QStringList lines = newText.split('\n', QString::SkipEmptyParts);
-      clear();
-
-      if(lines.count() >= 4) {
-        QString msg = tr("Do you really want to paste %n lines?", "", lines.count());
-        msg += "<p>";
-        for(int i = 0; i < 4; i++) {
-          msg += Qt::escape(lines[i].left(40));
-          if(lines[i].count() > 40)
-            msg += "...";
-          msg += "<br />";
-        }
-        msg += "...</p>";
-        QMessageBox question(QMessageBox::NoIcon, tr("Paste Protection"), msg, QMessageBox::Yes|QMessageBox::No);
-        question.setDefaultButton(QMessageBox::No);
-#ifdef Q_WS_MAC
-        question.setWindowFlags(question.windowFlags() | Qt::Sheet);
+    else {
+        emit noTextEntered();
+    }
+}
+
+void MultiLineEdit::on_textChanged()
+{
+    _completionSpace = qMax(_completionSpace - 1, 0);
+
+    QString newText = text();
+    newText.replace("\r\n", "\n");
+    newText.replace('\r', '\n');
+    if (_mode == SingleLine) {
+        if (!pasteProtectionEnabled())
+            newText.replace('\n', ' ');
+        else if (newText.contains('\n')) {
+            QStringList lines = newText.split('\n', QString::SkipEmptyParts);
+            clear();
+
+            if (lines.count() >= 4) {
+                QString msg = tr("Do you really want to paste %n line(s)?", "", lines.count());
+                msg += "<p>";
+                for (int i = 0; i < 4; i++) {
+                    msg += lines[i].left(40).toHtmlEscaped();
+                    if (lines[i].count() > 40)
+                        msg += "...";
+                    msg += "<br />";
+                }
+                msg += "...</p>";
+                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);
 #endif
-        if(question.exec() != QMessageBox::Yes)
-          return;
-      }
+                if (question.exec() != QMessageBox::Yes)
+                    return;
+            }
+
+            foreach (QString line, lines) {
+                clear();
+                insert(line);
+                on_returnPressed();
+            }
+        }
+    }
 
-      foreach(QString line, lines) {
-        clear();
-        insert(line);
-        on_returnPressed();
-      }
+    _singleLine = (newText.indexOf('\n') < 0);
+
+    if (document()->size().height() != _lastDocumentHeight) {
+        _lastDocumentHeight = document()->size().height();
+        on_documentHeightChanged(_lastDocumentHeight);
     }
-  }
-
-  _singleLine = (newText.indexOf('\n') < 0);
-
-  if(document()->size().height() != _lastDocumentHeight) {
-    _lastDocumentHeight = document()->size().height();
-    on_documentHeightChanged(_lastDocumentHeight);
-  }
-  updateSizeHint();
-  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
-  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
-  cursor.setBlockFormat(format);
-  cursor.movePosition(QTextCursor::End);
-  setTextCursor(cursor);
-  updateScrollBars();
+    updateSizeHint();
+    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
+    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
+    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(" ");
 }