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