5ace7d6df26888fbe393398d24b1696778230438
[quassel.git] / src / uisupport / multilineedit.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2016 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include <QApplication>
22 #include <QMessageBox>
23 #include <QScrollBar>
24
25 #ifdef HAVE_SONNET
26 #  include <Sonnet/SpellCheckDecorator>
27 #endif
28
29 #include "actioncollection.h"
30 #include "bufferview.h"
31 #include "graphicalui.h"
32 #include "multilineedit.h"
33 #include "tabcompleter.h"
34
35 const int leftMargin = 3;
36
37 MultiLineEdit::MultiLineEdit(QWidget *parent)
38     : MultiLineEditParent(parent),
39     _idx(0),
40     _mode(SingleLine),
41     _singleLine(true),
42     _minHeight(1),
43     _maxHeight(5),
44     _scrollBarsEnabled(true),
45     _pasteProtectionEnabled(true),
46     _emacsMode(false),
47     _completionSpace(0),
48     _lastDocumentHeight(-1)
49 {
50     document()->setDocumentMargin(0);
51
52     setAcceptRichText(false);
53 #ifdef HAVE_KDE
54     enableFindReplace(false);
55 #endif
56
57 #ifdef HAVE_SONNET
58     new Sonnet::SpellCheckDecorator(this);
59 #endif
60
61     setMode(SingleLine);
62     setLineWrapEnabled(false);
63     reset();
64
65     // Prevent QTextHtmlImporter::appendNodeText from eating whitespace
66     document()->setDefaultStyleSheet("span { white-space: pre-wrap; }");
67
68     connect(this, SIGNAL(textChanged()), this, SLOT(on_textChanged()));
69
70     _mircColorMap["00"] = "#ffffff";
71     _mircColorMap["01"] = "#000000";
72     _mircColorMap["02"] = "#000080";
73     _mircColorMap["03"] = "#008000";
74     _mircColorMap["04"] = "#ff0000";
75     _mircColorMap["05"] = "#800000";
76     _mircColorMap["06"] = "#800080";
77     _mircColorMap["07"] = "#ffa500";
78     _mircColorMap["08"] = "#ffff00";
79     _mircColorMap["09"] = "#00ff00";
80     _mircColorMap["10"] = "#008080";
81     _mircColorMap["11"] = "#00ffff";
82     _mircColorMap["12"] = "#4169e1";
83     _mircColorMap["13"] = "#ff00ff";
84     _mircColorMap["14"] = "#808080";
85     _mircColorMap["15"] = "#c0c0c0";
86 }
87
88
89 MultiLineEdit::~MultiLineEdit()
90 {
91 }
92
93 #if defined HAVE_KF5 || defined HAVE_KDE4
94 void MultiLineEdit::createHighlighter()
95 {
96     KTextEdit::createHighlighter();
97     if (highlighter())
98         highlighter()->setAutomatic(false);
99 }
100 #endif
101
102 void MultiLineEdit::setCustomFont(const QFont &font)
103 {
104     setFont(font);
105     updateSizeHint();
106 }
107
108
109 void MultiLineEdit::setMode(Mode mode)
110 {
111     if (mode == _mode)
112         return;
113
114     _mode = mode;
115 }
116
117
118 void MultiLineEdit::setLineWrapEnabled(bool enable)
119 {
120     setLineWrapMode(enable ? WidgetWidth : NoWrap);
121     updateSizeHint();
122 }
123
124
125 void MultiLineEdit::setMinHeight(int lines)
126 {
127     if (lines == _minHeight)
128         return;
129
130     _minHeight = lines;
131     updateSizeHint();
132 }
133
134
135 void MultiLineEdit::setMaxHeight(int lines)
136 {
137     if (lines == _maxHeight)
138         return;
139
140     _maxHeight = lines;
141     updateSizeHint();
142 }
143
144
145 void MultiLineEdit::setScrollBarsEnabled(bool enable)
146 {
147     if (_scrollBarsEnabled == enable)
148         return;
149
150     _scrollBarsEnabled = enable;
151     updateScrollBars();
152 }
153
154
155 void MultiLineEdit::updateScrollBars()
156 {
157     QFontMetrics fm(font());
158     int _maxPixelHeight = fm.lineSpacing() * _maxHeight;
159     if (_scrollBarsEnabled && document()->size().height() > _maxPixelHeight)
160         setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
161     else
162         setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
163
164     if (!_scrollBarsEnabled || isSingleLine())
165         setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
166     else
167         setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
168 }
169
170
171 void MultiLineEdit::resizeEvent(QResizeEvent *event)
172 {
173     QTextEdit::resizeEvent(event);
174     updateSizeHint();
175     updateScrollBars();
176 }
177
178
179 void MultiLineEdit::updateSizeHint()
180 {
181     QFontMetrics fm(font());
182     int minPixelHeight = fm.lineSpacing() * _minHeight;
183     int maxPixelHeight = fm.lineSpacing() * _maxHeight;
184     int scrollBarHeight = horizontalScrollBar()->isVisible() ? horizontalScrollBar()->height() : 0;
185
186     // use the style to determine a decent size
187     int h = qMin(qMax((int)document()->size().height() + scrollBarHeight, minPixelHeight), maxPixelHeight) + 2 * frameWidth();
188     QStyleOptionFrameV2 opt;
189     opt.initFrom(this);
190     opt.rect = QRect(0, 0, 100, h);
191     opt.lineWidth = lineWidth();
192     opt.midLineWidth = midLineWidth();
193     opt.state |= QStyle::State_Sunken;
194     QWidget *widget = this;
195 #ifdef Q_OS_MAC
196     widget = 0;
197 #endif
198     QSize s = style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(100, h).expandedTo(QApplication::globalStrut()), widget);
199     if (s != _sizeHint) {
200         _sizeHint = s;
201         updateGeometry();
202     }
203 }
204
205
206 QSize MultiLineEdit::sizeHint() const
207 {
208     if (!_sizeHint.isValid()) {
209         MultiLineEdit *that = const_cast<MultiLineEdit *>(this);
210         that->updateSizeHint();
211     }
212     return _sizeHint;
213 }
214
215
216 QSize MultiLineEdit::minimumSizeHint() const
217 {
218     return sizeHint();
219 }
220
221
222 void MultiLineEdit::setEmacsMode(bool enable)
223 {
224     _emacsMode = enable;
225 }
226
227
228 void MultiLineEdit::setSpellCheckEnabled(bool enable)
229 {
230 #ifdef HAVE_KDE
231     setCheckSpellingEnabled(enable);
232 #else
233     Q_UNUSED(enable)
234 #endif
235 }
236
237
238 void MultiLineEdit::setPasteProtectionEnabled(bool enable, QWidget *)
239 {
240     _pasteProtectionEnabled = enable;
241 }
242
243
244 void MultiLineEdit::historyMoveBack()
245 {
246     addToHistory(convertRichtextToMircCodes(), true);
247
248     if (_idx > 0) {
249         _idx--;
250         showHistoryEntry();
251     }
252 }
253
254
255 void MultiLineEdit::historyMoveForward()
256 {
257     addToHistory(convertRichtextToMircCodes(), true);
258
259     if (_idx < _history.count()) {
260         _idx++;
261         if (_idx < _history.count() || _tempHistory.contains(_idx)) // tempHistory might have an entry for idx == history.count() + 1
262             showHistoryEntry();
263         else
264             reset();        // equals clear() in this case
265     }
266     else {
267         addToHistory(convertRichtextToMircCodes());
268         reset();
269     }
270 }
271
272
273 bool MultiLineEdit::addToHistory(const QString &text, bool temporary)
274 {
275     if (text.isEmpty())
276         return false;
277
278     Q_ASSERT(0 <= _idx && _idx <= _history.count());
279
280     if (temporary) {
281         // if an entry of the history is changed, we remember it and show it again at this
282         // position until a line was actually sent
283         // sent lines get appended to the history
284         if (_history.isEmpty() || text != _history[_idx - (int)(_idx == _history.count())]) {
285             _tempHistory[_idx] = text;
286             return true;
287         }
288     }
289     else {
290         if (_history.isEmpty() || text != _history.last()) {
291             _history << text;
292             _tempHistory.clear();
293             return true;
294         }
295     }
296     return false;
297 }
298
299
300 bool MultiLineEdit::event(QEvent *e)
301 {
302     // We need to make sure that global shortcuts aren't eaten
303     if (e->type() == QEvent::ShortcutOverride) {
304         QKeyEvent *event = static_cast<QKeyEvent *>(e);
305         QKeySequence key = QKeySequence(event->key() | event->modifiers());
306         foreach(QAction *action, GraphicalUi::actionCollection()->actions()) {
307             if (action->shortcuts().contains(key)) {
308                 e->ignore();
309                 return false;
310             }
311         }
312     }
313
314     return MultiLineEditParent::event(e);
315 }
316
317
318 void MultiLineEdit::keyPressEvent(QKeyEvent *event)
319 {
320     if (event == QKeySequence::InsertLineSeparator) {
321         if (_mode == SingleLine) {
322             event->accept();
323             on_returnPressed();
324             return;
325         }
326         MultiLineEditParent::keyPressEvent(event);
327         return;
328     }
329
330     switch (event->key()) {
331     case Qt::Key_Up:
332         if (event->modifiers() & Qt::ShiftModifier)
333             break;
334         {
335             event->accept();
336             if (!(event->modifiers() & Qt::ControlModifier)) {
337                 int pos = textCursor().position();
338                 moveCursor(QTextCursor::Up);
339                 if (pos == textCursor().position()) // already on top line -> history
340                     historyMoveBack();
341             }
342             else
343                 historyMoveBack();
344             return;
345         }
346
347     case Qt::Key_Down:
348         if (event->modifiers() & Qt::ShiftModifier)
349             break;
350         {
351             event->accept();
352             if (!(event->modifiers() & Qt::ControlModifier)) {
353                 int pos = textCursor().position();
354                 moveCursor(QTextCursor::Down);
355                 if (pos == textCursor().position()) // already on bottom line -> history
356                     historyMoveForward();
357             }
358             else
359                 historyMoveForward();
360             return;
361         }
362
363     case Qt::Key_Return:
364     case Qt::Key_Enter:
365     case Qt::Key_Select:
366         event->accept();
367         on_returnPressed();
368         return;
369
370     // We don't want to have the tab key react even if no completer is installed
371     case Qt::Key_Tab:
372         event->accept();
373         return;
374
375     default:
376         ;
377     }
378
379     if (_emacsMode) {
380         if (event->modifiers() & Qt::ControlModifier) {
381             switch (event->key()) {
382             // move
383             case Qt::Key_A:
384                 moveCursor(QTextCursor::StartOfLine);
385                 return;
386             case Qt::Key_E:
387                 moveCursor(QTextCursor::EndOfLine);
388                 return;
389             case Qt::Key_F:
390                 moveCursor(QTextCursor::Right);
391                 return;
392             case Qt::Key_B:
393                 moveCursor(QTextCursor::Left);
394                 return;
395
396             // modify
397             case Qt::Key_Y:
398                 paste();
399                 return;
400             case Qt::Key_K:
401                 moveCursor(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
402                 cut();
403                 return;
404
405             default:
406                 break;
407             }
408         }
409         else if (event->modifiers() & Qt::MetaModifier ||
410                  event->modifiers() & Qt::AltModifier)
411         {
412             switch (event->key()) {
413             case Qt::Key_Right:
414                 moveCursor(QTextCursor::WordRight);
415                 return;
416             case Qt::Key_Left:
417                 moveCursor(QTextCursor::WordLeft);
418                 return;
419             case Qt::Key_F:
420                 moveCursor(QTextCursor::WordRight);
421                 return;
422             case Qt::Key_B:
423                 moveCursor(QTextCursor::WordLeft);
424                 return;
425             case Qt::Key_Less:
426                 moveCursor(QTextCursor::Start);
427                 return;
428             case Qt::Key_Greater:
429                 moveCursor(QTextCursor::End);
430                 return;
431
432             // modify
433             case Qt::Key_D:
434                 moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
435                 cut();
436                 return;
437
438             case Qt::Key_U: // uppercase word
439                 moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
440                 textCursor().insertText(textCursor().selectedText().toUpper());
441                 return;
442
443             case Qt::Key_L: // lowercase word
444                 moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
445                 textCursor().insertText(textCursor().selectedText().toLower());
446                 return;
447
448             case Qt::Key_C:
449             {           // capitalize word
450                 moveCursor(QTextCursor::WordRight, QTextCursor::KeepAnchor);
451                 QString const text = textCursor().selectedText();
452                 textCursor().insertText(text.left(1).toUpper() + text.mid(1).toLower());
453                 return;
454             }
455
456             case Qt::Key_T:
457             {           // transpose words
458                 moveCursor(QTextCursor::StartOfWord);
459                 moveCursor(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
460                 QString const word1 = textCursor().selectedText();
461                 textCursor().clearSelection();
462                 moveCursor(QTextCursor::WordRight);
463                 moveCursor(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
464                 QString const word2 = textCursor().selectedText();
465                 if (!word2.isEmpty() && !word1.isEmpty()) {
466                     textCursor().insertText(word1);
467                     moveCursor(QTextCursor::WordLeft);
468                     moveCursor(QTextCursor::WordLeft);
469                     moveCursor(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
470                     textCursor().insertText(word2);
471                     moveCursor(QTextCursor::WordRight);
472                     moveCursor(QTextCursor::EndOfWord);
473                 }
474                 return;
475             }
476
477             default:
478                 break;
479             }
480         }
481     }
482
483 #ifdef HAVE_KDE
484     KTextEdit::keyPressEvent(event);
485 #else
486     QTextEdit::keyPressEvent(event);
487 #endif
488 }
489
490
491 QString MultiLineEdit::convertRichtextToMircCodes()
492 {
493     bool underline, bold, italic, color;
494     QString mircText, mircFgColor, mircBgColor;
495     QTextCursor cursor = textCursor();
496     QTextCursor peekcursor = textCursor();
497     cursor.movePosition(QTextCursor::Start);
498
499     underline = bold = italic = color = false;
500
501     while (cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor)) {
502         if (cursor.selectedText() == QString(QChar(QChar::LineSeparator))
503             || cursor.selectedText() == QString(QChar(QChar::ParagraphSeparator))) {
504             if (color) {
505                 color = false;
506                 mircText.append('\x03');
507             }
508             if (underline) {
509                 underline = false;
510                 mircText.append('\x1f');
511             }
512             if (italic) {
513                 italic = false;
514                 mircText.append('\x1d');
515             }
516             if (bold) {
517                 bold = false;
518                 mircText.append('\x02');
519             }
520             mircText.append('\n');
521         }
522         else {
523             if (!bold && cursor.charFormat().font().bold()) {
524                 bold = true;
525                 mircText.append('\x02');
526             }
527             if (!italic && cursor.charFormat().fontItalic()) {
528                 italic = true;
529                 mircText.append('\x1d');
530             }
531             if (!underline && cursor.charFormat().fontUnderline()) {
532                 underline = true;
533                 mircText.append('\x1f');
534             }
535             if (!color && (cursor.charFormat().foreground().isOpaque() || cursor.charFormat().background().isOpaque())) {
536                 color = true;
537                 mircText.append('\x03');
538                 mircFgColor = _mircColorMap.key(cursor.charFormat().foreground().color().name());
539                 mircBgColor = _mircColorMap.key(cursor.charFormat().background().color().name());
540
541                 if (mircFgColor.isEmpty()) {
542                     mircFgColor = "01"; //use black if the current foreground color can't be converted
543                 }
544
545                 mircText.append(mircFgColor);
546                 if (cursor.charFormat().background().isOpaque())
547                     mircText.append("," + mircBgColor);
548             }
549
550             mircText.append(cursor.selectedText());
551
552             peekcursor.setPosition(cursor.position());
553             peekcursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
554
555             if (mircCodesChanged(cursor, peekcursor)) {
556                 if (color) {
557                     color = false;
558                     mircText.append('\x03');
559                 }
560                 if (underline) {
561                     underline = false;
562                     mircText.append('\x1f');
563                 }
564                 if (italic) {
565                     italic = false;
566                     mircText.append('\x1d');
567                 }
568                 if (bold) {
569                     bold = false;
570                     mircText.append('\x02');
571                 }
572             }
573         }
574
575         cursor.clearSelection();
576     }
577
578     if (color)
579         mircText.append('\x03');
580
581     if (underline)
582         mircText.append('\x1f');
583
584     if (italic)
585         mircText.append('\x1d');
586
587     if (bold)
588         mircText.append('\x02');
589
590     return mircText;
591 }
592
593
594 bool MultiLineEdit::mircCodesChanged(QTextCursor &cursor, QTextCursor &peekcursor)
595 {
596     bool changed = false;
597     if (cursor.charFormat().font().bold() != peekcursor.charFormat().font().bold())
598         changed = true;
599     if (cursor.charFormat().fontItalic() != peekcursor.charFormat().fontItalic())
600         changed = true;
601     if (cursor.charFormat().fontUnderline() != peekcursor.charFormat().fontUnderline())
602         changed = true;
603     if (cursor.charFormat().foreground().color() != peekcursor.charFormat().foreground().color())
604         changed = true;
605     if (cursor.charFormat().background().color() != peekcursor.charFormat().background().color())
606         changed = true;
607     return changed;
608 }
609
610
611 QString MultiLineEdit::convertMircCodesToHtml(const QString &text)
612 {
613     QStringList words;
614     QRegExp mircCode = QRegExp("(\ 2|\1d|\1f|\ 3)", Qt::CaseSensitive);
615
616     int posLeft = 0;
617     int posRight = 0;
618
619     for (;;) {
620         posRight = mircCode.indexIn(text, posLeft);
621
622         if (posRight < 0) {
623             words << text.mid(posLeft);
624             break; // no more mirc color codes
625         }
626
627         if (posLeft < posRight) {
628             words << text.mid(posLeft, posRight - posLeft);
629             posLeft = posRight;
630         }
631
632         posRight = text.indexOf(mircCode.cap(), posRight + 1);
633         words << text.mid(posLeft, posRight + 1 - posLeft);
634         posLeft = posRight + 1;
635     }
636
637     for (int i = 0; i < words.count(); i++) {
638         QString style;
639         if (words[i].contains('\x02')) {
640             style.append(" font-weight:600;");
641             words[i].replace('\x02', "");
642         }
643         if (words[i].contains('\x1d')) {
644             style.append(" font-style:italic;");
645             words[i].replace('\x1d', "");
646         }
647         if (words[i].contains('\x1f')) {
648             style.append(" text-decoration: underline;");
649             words[i].replace('\x1f', "");
650         }
651         if (words[i].contains('\x03')) {
652             int pos = words[i].indexOf('\x03');
653             int len = 3;
654             QString fg = words[i].mid(pos + 1, 2);
655             QString bg;
656             if (words[i][pos+3] == ',')
657                 bg = words[i].mid(pos+4, 2);
658
659             style.append(" color:");
660             style.append(_mircColorMap[fg]);
661             style.append(";");
662
663             if (!bg.isEmpty()) {
664                 style.append(" background-color:");
665                 style.append(_mircColorMap[bg]);
666                 style.append(";");
667                 len = 6;
668             }
669             words[i].replace(pos, len, "");
670             words[i].replace('\x03', "");
671         }
672         words[i].replace("&", "&amp;");
673         words[i].replace("<", "&lt;");
674         words[i].replace(">", "&gt;");
675         words[i].replace("\"", "&quot;");
676         if (style.isEmpty()) {
677             words[i] = "<span>" + words[i] + "</span>";
678         }
679         else {
680             words[i] = "<span style=\"" + style + "\">" + words[i] + "</span>";
681         }
682     }
683     return words.join("").replace("\n", "<br />");
684 }
685
686
687 void MultiLineEdit::on_returnPressed()
688 {
689     on_returnPressed(convertRichtextToMircCodes());
690 }
691
692
693 void MultiLineEdit::on_returnPressed(QString text)
694 {
695     if (_completionSpace && text.endsWith(" ")) {
696         text.chop(1);
697     }
698
699     if (!text.isEmpty()) {
700         foreach(const QString &line, text.split('\n', QString::SkipEmptyParts)) {
701             if (line.isEmpty())
702                 continue;
703             addToHistory(line);
704             emit textEntered(line);
705         }
706         reset();
707         _tempHistory.clear();
708     }
709     else {
710         emit noTextEntered();
711     }
712 }
713
714
715 void MultiLineEdit::on_textChanged()
716 {
717     _completionSpace = qMax(_completionSpace - 1, 0);
718
719     QString newText = text();
720     newText.replace("\r\n", "\n");
721     newText.replace('\r', '\n');
722     if (_mode == SingleLine) {
723         if (!pasteProtectionEnabled())
724             newText.replace('\n', ' ');
725         else if (newText.contains('\n')) {
726             QStringList lines = newText.split('\n', QString::SkipEmptyParts);
727             clear();
728
729             if (lines.count() >= 4) {
730                 QString msg = tr("Do you really want to paste %n line(s)?", "", lines.count());
731                 msg += "<p>";
732                 for (int i = 0; i < 4; i++) {
733 #if QT_VERSION < 0x050000
734                     msg += Qt::escape(lines[i].left(40));
735 #else
736                     msg += lines[i].left(40).toHtmlEscaped();
737 #endif
738                     if (lines[i].count() > 40)
739                         msg += "...";
740                     msg += "<br />";
741                 }
742                 msg += "...</p>";
743                 QMessageBox question(QMessageBox::NoIcon, tr("Paste Protection"), msg, QMessageBox::Yes|QMessageBox::No);
744                 question.setDefaultButton(QMessageBox::No);
745 #ifdef Q_OS_MAC
746                 question.setWindowFlags(question.windowFlags() | Qt::Sheet);
747 #endif
748                 if (question.exec() != QMessageBox::Yes)
749                     return;
750             }
751
752             foreach(QString line, lines) {
753                 clear();
754                 insert(line);
755                 on_returnPressed();
756             }
757         }
758     }
759
760     _singleLine = (newText.indexOf('\n') < 0);
761
762     if (document()->size().height() != _lastDocumentHeight) {
763         _lastDocumentHeight = document()->size().height();
764         on_documentHeightChanged(_lastDocumentHeight);
765     }
766     updateSizeHint();
767     ensureCursorVisible();
768 }
769
770
771 void MultiLineEdit::on_documentHeightChanged(qreal)
772 {
773     updateScrollBars();
774 }
775
776
777 void MultiLineEdit::reset()
778 {
779     // every time the MultiLineEdit is cleared we also reset history index
780     _idx = _history.count();
781     clear();
782     QTextBlockFormat format = textCursor().blockFormat();
783     format.setLeftMargin(leftMargin); // we want a little space between the frame and the contents
784     textCursor().setBlockFormat(format);
785     updateScrollBars();
786 }
787
788
789 void MultiLineEdit::showHistoryEntry()
790 {
791     // if the user changed the history, display the changed line
792     setHtml(convertMircCodesToHtml(_tempHistory.contains(_idx) ? _tempHistory[_idx] : _history[_idx]));
793     QTextCursor cursor = textCursor();
794     QTextBlockFormat format = cursor.blockFormat();
795     format.setLeftMargin(leftMargin); // we want a little space between the frame and the contents
796     cursor.setBlockFormat(format);
797     cursor.movePosition(QTextCursor::End);
798     setTextCursor(cursor);
799     updateScrollBars();
800 }
801
802
803 void MultiLineEdit::addCompletionSpace()
804 {
805     // Inserting the space emits textChanged, which should not disable removal
806     _completionSpace = 2;
807     insertPlainText(" ");
808 }
809