PageRenderTime 90ms CodeModel.GetById 16ms app.highlight 65ms RepoModel.GetById 1ms app.codeStats 0ms

/quassel-0.7.3/src/uisupport/multilineedit.cpp

#
C++ | 721 lines | 602 code | 88 blank | 31 comment | 130 complexity | 326ae5bb506fbf7acf813eed4cd0850b MD5 | raw file
Possible License(s): GPL-2.0, GPL-3.0
  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
 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  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
 80MultiLineEdit::~MultiLineEdit() {
 81}
 82
 83void MultiLineEdit::setCustomFont(const QFont &font) {
 84  setFont(font);
 85  updateSizeHint();
 86}
 87
 88void MultiLineEdit::setMode(Mode mode) {
 89  if(mode == _mode)
 90    return;
 91
 92  _mode = mode;
 93}
 94
 95void MultiLineEdit::setMinHeight(int lines) {
 96  if(lines == _minHeight)
 97    return;
 98
 99  _minHeight = lines;
100  updateSizeHint();
101}
102
103void MultiLineEdit::setMaxHeight(int lines) {
104  if(lines == _maxHeight)
105    return;
106
107  _maxHeight = lines;
108  updateSizeHint();
109}
110
111void MultiLineEdit::setScrollBarsEnabled(bool enable) {
112  if(_scrollBarsEnabled == enable)
113    return;
114
115  _scrollBarsEnabled = enable;
116  updateScrollBars();
117}
118
119void 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
133void MultiLineEdit::resizeEvent(QResizeEvent *event) {
134  QTextEdit::resizeEvent(event);
135  updateSizeHint();
136  updateScrollBars();
137}
138
139void 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
160QSize MultiLineEdit::sizeHint() const {
161  if(!_sizeHint.isValid()) {
162    MultiLineEdit *that = const_cast<MultiLineEdit *>(this);
163    that->updateSizeHint();
164  }
165  return _sizeHint;
166}
167
168QSize MultiLineEdit::minimumSizeHint() const {
169  return sizeHint();
170}
171
172void MultiLineEdit::setEmacsMode(bool enable) {
173  _emacsMode = enable;
174}
175
176void MultiLineEdit::setSpellCheckEnabled(bool enable) {
177#ifdef HAVE_KDE
178  setCheckSpellingEnabled(enable);
179#else
180  Q_UNUSED(enable)
181#endif
182}
183
184void MultiLineEdit::setWordWrapEnabled(bool enable) {
185  setLineWrapMode(enable? WidgetWidth : NoWrap);
186  updateSizeHint();
187}
188
189void MultiLineEdit::setPasteProtectionEnabled(bool enable, QWidget *) {
190  _pasteProtectionEnabled = enable;
191}
192
193void MultiLineEdit::historyMoveBack() {
194  addToHistory(convertRichtextToMircCodes(), true);
195
196  if(_idx > 0) {
197    _idx--;
198    showHistoryEntry();
199  }
200}
201
202void 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
217bool 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
241bool 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
257void 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
435QString 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
541bool 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
556QString MultiLineEdit::convertMircCodesToHtml(const QString &text) {
557  QStringList words;
558  QRegExp mircCode = QRegExp("(|||)", 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
630void MultiLineEdit::on_returnPressed() {
631  on_returnPressed(convertRichtextToMircCodes());
632}
633
634void 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
649void 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 lines?", "", 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
697void MultiLineEdit::on_documentHeightChanged(qreal) {
698  updateScrollBars();
699}
700
701void 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
711void 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}