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