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