Quassel IRC  Pre-Release
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros
multilineedit.cpp
Go to the documentation of this file.
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 
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 
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 
79 {
80 }
81 
82 
83 void MultiLineEdit::setCustomFont(const QFont &font)
84 {
85  setFont(font);
87 }
88 
89 
91 {
92  if (mode == _mode)
93  return;
94 
95  _mode = mode;
96 }
97 
98 
100 {
101  setLineWrapMode(enable ? WidgetWidth : NoWrap);
102  updateSizeHint();
103 }
104 
105 
107 {
108  if (lines == _minHeight)
109  return;
110 
111  _minHeight = lines;
112  updateSizeHint();
113 }
114 
115 
117 {
118  if (lines == _maxHeight)
119  return;
120 
121  _maxHeight = lines;
122  updateSizeHint();
123 }
124 
125 
127 {
128  if (_scrollBarsEnabled == enable)
129  return;
130 
131  _scrollBarsEnabled = enable;
133 }
134 
135 
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 
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();
157 }
158 
159 
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 
184 {
185  if (!_sizeHint.isValid()) {
186  MultiLineEdit *that = const_cast<MultiLineEdit *>(this);
187  that->updateSizeHint();
188  }
189  return _sizeHint;
190 }
191 
192 
194 {
195  return sizeHint();
196 }
197 
198 
200 {
201  _emacsMode = enable;
202 }
203 
204 
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 
222 {
224 
225  if (_idx > 0) {
226  _idx--;
228  }
229 }
230 
231 
233 {
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
240  else
241  reset(); // equals clear() in this case
242  }
243  else {
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())]) {
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();
301  return;
302  }
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
334  }
335  else
337  return;
338  }
339 
340  case Qt::Key_Return:
341  case Qt::Key_Enter:
342  case Qt::Key_Select:
343  event->accept();
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
462 #else
464 #endif
465 }
466 
467 
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 
555  if (color)
556  mircText.append('\x03');
557 
558  if (underline)
559  mircText.append('\x1f');
560 
561  if (italic)
562  mircText.append('\x1d');
563 
564  if (bold)
565  mircText.append('\x02');
566 
567  return mircText;
568 }
569 
570 
571 bool MultiLineEdit::mircCodesChanged(QTextCursor &cursor, QTextCursor &peekcursor)
572 {
573  bool changed = false;
574  if (cursor.charFormat().font().bold() != peekcursor.charFormat().font().bold())
575  changed = true;
576  if (cursor.charFormat().fontItalic() != peekcursor.charFormat().fontItalic())
577  changed = true;
578  if (cursor.charFormat().fontUnderline() != peekcursor.charFormat().fontUnderline())
579  changed = true;
580  if (cursor.charFormat().foreground().color() != peekcursor.charFormat().foreground().color())
581  changed = true;
582  if (cursor.charFormat().background().color() != peekcursor.charFormat().background().color())
583  changed = true;
584  return changed;
585 }
586 
587 
588 QString MultiLineEdit::convertMircCodesToHtml(const QString &text)
589 {
590  QStringList words;
591  QRegExp mircCode = QRegExp("(|||)", Qt::CaseSensitive);
592 
593  int posLeft = 0;
594  int posRight = 0;
595 
596  for (;;) {
597  posRight = mircCode.indexIn(text, posLeft);
598 
599  if (posRight < 0) {
600  words << text.mid(posLeft);
601  break; // no more mirc color codes
602  }
603 
604  if (posLeft < posRight) {
605  words << text.mid(posLeft, posRight - posLeft);
606  posLeft = posRight;
607  }
608 
609  posRight = text.indexOf(mircCode.cap(), posRight + 1);
610  words << text.mid(posLeft, posRight + 1 - posLeft);
611  posLeft = posRight + 1;
612  }
613 
614  for (int i = 0; i < words.count(); i++) {
615  QString style;
616  if (words[i].contains('\x02')) {
617  style.append(" font-weight:600;");
618  words[i].replace('\x02', "");
619  }
620  if (words[i].contains('\x1d')) {
621  style.append(" font-style:italic;");
622  words[i].replace('\x1d', "");
623  }
624  if (words[i].contains('\x1f')) {
625  style.append(" text-decoration: underline;");
626  words[i].replace('\x1f', "");
627  }
628  if (words[i].contains('\x03')) {
629  int pos = words[i].indexOf('\x03');
630  int len = 3;
631  QString fg = words[i].mid(pos + 1, 2);
632  QString bg;
633  if (words[i][pos+3] == ',')
634  bg = words[i].mid(pos+4, 2);
635 
636  style.append(" color:");
637  style.append(_mircColorMap[fg]);
638  style.append(";");
639 
640  if (!bg.isEmpty()) {
641  style.append(" background-color:");
642  style.append(_mircColorMap[bg]);
643  style.append(";");
644  len = 6;
645  }
646  words[i].replace(pos, len, "");
647  words[i].replace('\x03', "");
648  }
649  words[i].replace("&", "&amp;");
650  words[i].replace("<", "&lt;");
651  words[i].replace(">", "&gt;");
652  words[i].replace("\"", "&quot;");
653  if (style.isEmpty()) {
654  words[i] = "<span>" + words[i] + "</span>";
655  }
656  else {
657  words[i] = "<span style=\"" + style + "\">" + words[i] + "</span>";
658  }
659  }
660  return words.join("").replace("\n", "<br />");
661 }
662 
663 
665 {
667 }
668 
669 
670 void MultiLineEdit::on_returnPressed(const QString &text)
671 {
672  if (!text.isEmpty()) {
673  foreach(const QString &line, text.split('\n', QString::SkipEmptyParts)) {
674  if (line.isEmpty())
675  continue;
676  addToHistory(line);
677  emit textEntered(line);
678  }
679  reset();
680  _tempHistory.clear();
681  }
682  else {
683  emit noTextEntered();
684  }
685 }
686 
687 
689 {
690  QString newText = text();
691  newText.replace("\r\n", "\n");
692  newText.replace('\r', '\n');
693  if (_mode == SingleLine) {
694  if (!pasteProtectionEnabled())
695  newText.replace('\n', ' ');
696  else if (newText.contains('\n')) {
697  QStringList lines = newText.split('\n', QString::SkipEmptyParts);
698  clear();
699 
700  if (lines.count() >= 4) {
701  QString msg = tr("Do you really want to paste %n line(s)?", "", lines.count());
702  msg += "<p>";
703  for (int i = 0; i < 4; i++) {
704 #if QT_VERSION < 0x050000
705  msg += Qt::escape(lines[i].left(40));
706 #else
707  msg += lines[i].left(40).toHtmlEscaped();
708 #endif
709  if (lines[i].count() > 40)
710  msg += "...";
711  msg += "<br />";
712  }
713  msg += "...</p>";
714  QMessageBox question(QMessageBox::NoIcon, tr("Paste Protection"), msg, QMessageBox::Yes|QMessageBox::No);
715  question.setDefaultButton(QMessageBox::No);
716 #ifdef Q_OS_MAC
717  question.setWindowFlags(question.windowFlags() | Qt::Sheet);
718 #endif
719  if (question.exec() != QMessageBox::Yes)
720  return;
721  }
722 
723  foreach(QString line, lines) {
724  clear();
725  insert(line);
727  }
728  }
729  }
730 
731  _singleLine = (newText.indexOf('\n') < 0);
732 
733  if (document()->size().height() != _lastDocumentHeight) {
734  _lastDocumentHeight = document()->size().height();
736  }
737  updateSizeHint();
738  ensureCursorVisible();
739 }
740 
741 
743 {
745 }
746 
747 
749 {
750  // every time the MultiLineEdit is cleared we also reset history index
751  _idx = _history.count();
752  clear();
753  QTextBlockFormat format = textCursor().blockFormat();
754  format.setLeftMargin(leftMargin); // we want a little space between the frame and the contents
755  textCursor().setBlockFormat(format);
757 }
758 
759 
761 {
762  // if the user changed the history, display the changed line
764  QTextCursor cursor = textCursor();
765  QTextBlockFormat format = cursor.blockFormat();
766  format.setLeftMargin(leftMargin); // we want a little space between the frame and the contents
767  cursor.setBlockFormat(format);
768  cursor.movePosition(QTextCursor::End);
769  setTextCursor(cursor);
771 }