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