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
32const int leftMargin = 3;
33
34MultiLineEdit::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 setLineWrapEnabled(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
80MultiLineEdit::~MultiLineEdit()
81{
82}
83
84
85void MultiLineEdit::setCustomFont(const QFont &font)
86{
87 setFont(font);
88 updateSizeHint();
89}
90
91
92void MultiLineEdit::setMode(Mode mode)
93{
94 if (mode == _mode)
95 return;
96
97 _mode = mode;
98}
99
100
101void MultiLineEdit::setLineWrapEnabled(bool enable)
102{
103 setLineWrapMode(enable ? WidgetWidth : NoWrap);
104 updateSizeHint();
105}
106
107
108void MultiLineEdit::setMinHeight(int lines)
109{
110 if (lines == _minHeight)
111 return;
112
113 _minHeight = lines;
114 updateSizeHint();
115}
116
117
118void MultiLineEdit::setMaxHeight(int lines)
119{
120 if (lines == _maxHeight)
121 return;
122
123 _maxHeight = lines;
124 updateSizeHint();
125}
126
127
128void MultiLineEdit::setScrollBarsEnabled(bool enable)
129{
130 if (_scrollBarsEnabled == enable)
131 return;
132
133 _scrollBarsEnabled = enable;
134 updateScrollBars();
135}
136
137
138void MultiLineEdit::updateScrollBars()
139{
140 QFontMetrics fm(font());
141 int _maxPixelHeight = fm.lineSpacing() * _maxHeight;
142 if (_scrollBarsEnabled && document()->size().height() > _maxPixelHeight)
143 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
144 else
145 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
146
147 if (!_scrollBarsEnabled || isSingleLine())
148 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
149 else
150 setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
151}
152
153
154void MultiLineEdit::resizeEvent(QResizeEvent *event)
155{
156 QTextEdit::resizeEvent(event);
157 updateSizeHint();
158 updateScrollBars();
159}
160
161
162void MultiLineEdit::updateSizeHint()
163{
164 QFontMetrics fm(font());
165 int minPixelHeight = fm.lineSpacing() * _minHeight;
166 int maxPixelHeight = fm.lineSpacing() * _maxHeight;
167 int scrollBarHeight = horizontalScrollBar()->isVisible() ? horizontalScrollBar()->height() : 0;
168
169 // use the style to determine a decent size
170 int h = qMin(qMax((int)document()->size().height() + scrollBarHeight, minPixelHeight), maxPixelHeight) + 2 * frameWidth();
171 QStyleOptionFrameV2 opt;
172 opt.initFrom(this);
173 opt.rect = QRect(0, 0, 100, h);
174 opt.lineWidth = lineWidth();
175 opt.midLineWidth = midLineWidth();
176 opt.state |= QStyle::State_Sunken;
177 QSize s = style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(100, h).expandedTo(QApplication::globalStrut()), this);
178 if (s != _sizeHint) {
179 _sizeHint = s;
180 updateGeometry();
181 }
182}
183
184
185QSize MultiLineEdit::sizeHint() const
186{
187 if (!_sizeHint.isValid()) {
188 MultiLineEdit *that = const_cast<MultiLineEdit *>(this);
189 that->updateSizeHint();
190 }
191 return _sizeHint;
192}
193
194
195QSize MultiLineEdit::minimumSizeHint() const
196{
197 return sizeHint();
198}
199
200
201void MultiLineEdit::setEmacsMode(bool enable)
202{
203 _emacsMode = enable;
204}
205
206
207void MultiLineEdit::setSpellCheckEnabled(bool enable)
208{
209#ifdef HAVE_KDE
210 setCheckSpellingEnabled(enable);
211#else
212 Q_UNUSED(enable)
213#endif
214}
215
216
217void MultiLineEdit::setPasteProtectionEnabled(bool enable, QWidget *)
218{
219 _pasteProtectionEnabled = enable;
220}
221
222
223void MultiLineEdit::historyMoveBack()
224{
225 addToHistory(convertRichtextToMircCodes(), true);
226
227 if (_idx > 0) {
228 _idx--;
229 showHistoryEntry();
230 }
231}
232
233
234void 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
252bool 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
279bool 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
297void 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_OS_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
481QString 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
588bool 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
605QString MultiLineEdit::convertMircCodesToHtml(const QString &text)
606{
607 QStringList words;
608 QRegExp mircCode = QRegExp("(|||)", 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
681void MultiLineEdit::on_returnPressed()
682{
683 on_returnPressed(convertRichtextToMircCodes());
684}
685
686
687void 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
705void 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#if QT_VERSION < 0x050000
722 msg += Qt::escape(lines[i].left(40));
723#else
724 msg += lines[i].left(40).toHtmlEscaped();
725#endif
726 if (lines[i].count() > 40)
727 msg += "...";
728 msg += "<br />";
729 }
730 msg += "...</p>";
731 QMessageBox question(QMessageBox::NoIcon, tr("Paste Protection"), msg, QMessageBox::Yes|QMessageBox::No);
732 question.setDefaultButton(QMessageBox::No);
733#ifdef Q_OS_MAC
734 question.setWindowFlags(question.windowFlags() | Qt::Sheet);
735#endif
736 if (question.exec() != QMessageBox::Yes)
737 return;
738 }
739
740 foreach(QString line, lines) {
741 clear();
742 insert(line);
743 on_returnPressed();
744 }
745 }
746 }
747
748 _singleLine = (newText.indexOf('\n') < 0);
749
750 if (document()->size().height() != _lastDocumentHeight) {
751 _lastDocumentHeight = document()->size().height();
752 on_documentHeightChanged(_lastDocumentHeight);
753 }
754 updateSizeHint();
755 ensureCursorVisible();
756}
757
758
759void MultiLineEdit::on_documentHeightChanged(qreal)
760{
761 updateScrollBars();
762}
763
764
765void MultiLineEdit::reset()
766{
767 // every time the MultiLineEdit is cleared we also reset history index
768 _idx = _history.count();
769 clear();
770 QTextBlockFormat format = textCursor().blockFormat();
771 format.setLeftMargin(leftMargin); // we want a little space between the frame and the contents
772 textCursor().setBlockFormat(format);
773 updateScrollBars();
774}
775
776
777void MultiLineEdit::showHistoryEntry()
778{
779 // if the user changed the history, display the changed line
780 setHtml(convertMircCodesToHtml(_tempHistory.contains(_idx) ? _tempHistory[_idx] : _history[_idx]));
781 QTextCursor cursor = textCursor();
782 QTextBlockFormat format = cursor.blockFormat();
783 format.setLeftMargin(leftMargin); // we want a little space between the frame and the contents
784 cursor.setBlockFormat(format);
785 cursor.movePosition(QTextCursor::End);
786 setTextCursor(cursor);
787 updateScrollBars();
788}
789