Use QStringLiteral
[kdepim.git] / kleopatra / utils / kdlogtextwidget.cpp
blob421db52dc96a52b42a0dc0924ddcfe27699b6b42
1 /****************************************************************************
2 ** Copyright (C) 2001-2010 Klaralvdalens Datakonsult AB. All rights reserved.
3 **
4 ** This file is part of the KD Tools library.
5 **
6 ** Licensees holding valid commercial KD Tools licenses may use this file in
7 ** accordance with the KD Tools Commercial License Agreement provided with
8 ** the Software.
9 **
11 ** This file may be distributed and/or modified under the terms of the
12 ** GNU Lesser General Public License version 2 and version 3 as published by the
13 ** Free Software Foundation and appearing in the file LICENSE.LGPL included.
15 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
16 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
18 ** Contact info@kdab.com if any conditions of this licensing are not
19 ** clear to you.
21 **********************************************************************/
23 #include <config-kleopatra.h>
25 #include "kdlogtextwidget.h"
27 #include <QBasicTimer>
28 #include <QHash>
29 #include <QPainter>
30 #include <QPaintEvent>
31 #include <QScrollBar>
33 #include <cassert>
34 #include <algorithm>
35 #include <iterator>
37 /*!
38 \class KDLogTextWidget
39 \brief A high-speed text display widget.
41 This widget provides very fast display of large amounts of
42 line-oriented text, as commonly found in application log
43 viewers. The feature set and implementation are optimized for
44 frequent appends.
46 You can set initial text using setLines(), and append lines with
47 calls to message(). You can limit the number of lines kept in the
48 view using setHistorySize().
50 Text formatting is currently limited to per-line text color, but is
51 expected to be enhanced on client request in upcoming versions. You
52 can pass the color to use to calls to message().
55 class KDLogTextWidget::Private
57 friend class ::KDLogTextWidget;
58 KDLogTextWidget *const q;
59 public:
60 explicit Private(KDLogTextWidget *qq);
61 ~Private();
63 void updateCache() const;
65 void triggerTimer()
67 if (!timer.isActive()) {
68 timer.start(500, q);
72 void addPendingLines();
73 void enforceHistorySize();
74 void updateScrollRanges();
75 QPair<int, int> visibleLines(int top, int bottom)
77 return qMakePair(qMax(0, lineByYCoordinate(top)),
78 qMax(0, 1 + lineByYCoordinate(bottom)));
80 int lineByYCoordinate(int x) const;
82 QPoint scrollOffset() const;
84 QRect lineRect(int idx) const
86 assert(!cache.dirty);
87 return QRect(0, idx * cache.fontMetrics.lineSpacing, cache.dimensions.longestLineLength, cache.fontMetrics.lineSpacing - 1);
90 struct Style {
91 QColor color;
93 friend inline uint qHash(const Style &style)
95 return qHash(style.color.rgba());
97 bool operator==(const Style &other) const
99 return this->color.rgba() == other.color.rgba();
101 bool operator<(const Style &other) const
103 return this->color.rgba() < other.color.rgba();
107 struct LineItem {
108 QString text;
109 unsigned int styleID;
112 unsigned int findOrAddStyle(const Style &style);
114 private:
115 QHash<unsigned int, Style> styleByID;
116 QHash<Style, unsigned int> idByStyle;
118 QVector<LineItem> lines, pendingLines;
120 unsigned int historySize;
121 unsigned int minimumVisibleLines;
122 unsigned int minimumVisibleColumns;
124 bool alternatingRowColors;
126 QBasicTimer timer;
128 mutable struct Cache {
129 enum { Dimensions = 1, FontMetrics = 2, All = FontMetrics | Dimensions };
130 Cache() : dirty(All) {}
131 int dirty;
133 struct {
134 int lineSpacing;
135 int ascent;
136 int averageCharWidth;
137 QVector<int> lineWidths;
138 } fontMetrics;
140 struct {
141 int indexOfLongestLine;
142 int longestLineLength;
143 } dimensions;
144 } cache;
148 Constructor. Creates an empty KDLogTextWidget.
150 KDLogTextWidget::KDLogTextWidget(QWidget *parent_)
151 : QAbstractScrollArea(parent_), d(new Private(this))
157 Destructor.
159 KDLogTextWidget::~KDLogTextWidget() {}
162 \property KDLogTextWidget::historySize
164 Specifies the maximum number of lines this widget will hold before
165 dropping old lines. The default is INT_MAX (ie. essentially unlimited).
167 Get this property's value using %historySize(), and set it with
168 %setHistorySize().
170 void KDLogTextWidget::setHistorySize(unsigned int hs)
172 if (hs == d->historySize) {
173 return;
175 d->historySize = hs;
176 d->enforceHistorySize();
177 d->updateScrollRanges();
178 viewport()->update();
181 unsigned int KDLogTextWidget::historySize() const
183 return d->historySize;
187 \property KDLogTextWidget::text
189 Contains the current %text as a single string. Equivalent to
190 \code
191 lines().join( "\n" )
192 \endcode
194 QString KDLogTextWidget::text() const
196 return lines().join(QStringLiteral("\n"));
200 \property KDLogTextWidget::lines
202 Contains the current %text as a string list. The default empty.
204 Get this property's value using %lines(), and set it with
205 %setLines().
207 void KDLogTextWidget::setLines(const QStringList &l)
209 clear();
210 Q_FOREACH (const QString &s, l) {
211 message(s);
215 QStringList KDLogTextWidget::lines() const
217 QStringList result;
218 Q_FOREACH (const Private::LineItem &li, d->lines) {
219 result.push_back(li.text);
221 Q_FOREACH (const Private::LineItem &li, d->pendingLines) {
222 result.push_back(li.text);
224 return result;
228 \property KDLogTextWidget::minimumVisibleLines
230 Specifies the number of lines that should be visible at any one
231 time. The default is 1 (one).
233 Get this property's value using %minimumVisibleLines(), and set it
234 using %setMinimumVisibleLines().
236 void KDLogTextWidget::setMinimumVisibleLines(unsigned int num)
238 if (num == d->minimumVisibleLines) {
239 return;
241 d->minimumVisibleLines = num;
242 updateGeometry();
245 unsigned int KDLogTextWidget::minimumVisibleLines() const
247 return d->minimumVisibleLines;
251 \property KDLogTextWidget::minimumVisibleColumns
253 Specifies the number of columns that should be visible at any one
254 time. The default is 1 (one). The width is calculated using
255 QFontMetrics::averageCharWidth(), if that is available. Otherwise,
256 the width of \c M is used.
258 Get this property's value using %minimumVisibleColumns(), and set it
259 using %setMinimumVisibleColumns().
261 void KDLogTextWidget::setMinimumVisibleColumns(unsigned int num)
263 if (num == d->minimumVisibleColumns) {
264 return;
266 d->minimumVisibleColumns = num;
267 updateGeometry();
270 unsigned int KDLogTextWidget::minimumVisibleColumns() const
272 return d->minimumVisibleColumns;
276 \property KDLogTextWidget::alternatingRowColors
278 Specifies whether the background should be drawn using
279 row-alternating colors. The default is \c false.
281 Get this property's value using %alternatingRowColors(), and set it
282 using %setAlternatingRowColors().
284 void KDLogTextWidget::setAlternatingRowColors(bool on)
286 if (on == d->alternatingRowColors) {
287 return;
289 d->alternatingRowColors = on;
290 update();
293 bool KDLogTextWidget::alternatingRowColors() const
295 return d->alternatingRowColors;
298 QSize KDLogTextWidget::minimumSizeHint() const
300 d->updateCache();
301 const QSize base = QAbstractScrollArea::minimumSizeHint();
302 const QSize view(d->minimumVisibleColumns * d->cache.fontMetrics.averageCharWidth,
303 d->minimumVisibleLines * d->cache.fontMetrics.lineSpacing);
304 const QSize scrollbars(verticalScrollBar() ? verticalScrollBar()->minimumSizeHint().width() : 0,
305 horizontalScrollBar() ? horizontalScrollBar()->minimumSizeHint().height() : 0);
306 return base + view + scrollbars;
309 QSize KDLogTextWidget::sizeHint() const
311 if (d->minimumVisibleLines > 1 || d->minimumVisibleColumns > 1) {
312 return minimumSizeHint();
313 } else {
314 return 2 * minimumSizeHint();
319 Clears the text.
321 \post lines().empty() == true
323 void KDLogTextWidget::clear()
325 d->timer.stop();
326 d->lines.clear();
327 d->pendingLines.clear();
328 d->styleByID.clear();
329 d->idByStyle.clear();
330 d->cache.dirty = Private::Cache::All;
331 viewport()->update();
335 Appends \a str to the view, highlighting the line in \a color.
337 \post lines().back() == str (modulo trailing whitespace and contained newlines)
339 void KDLogTextWidget::message(const QString &str, const QColor &color)
341 const Private::Style s = { color };
342 const Private::LineItem li = { str, d->findOrAddStyle(s) };
343 d->pendingLines.push_back(li);
344 d->triggerTimer();
348 \overload
350 Uses the default text color set in this widget's palette.
352 void KDLogTextWidget::message(const QString &str)
354 const Private::LineItem li = { str, 0 };
355 d->pendingLines.push_back(li);
356 d->triggerTimer();
359 void KDLogTextWidget::paintEvent(QPaintEvent *e)
362 d->updateCache();
364 QPainter p(viewport());
366 p.translate(-d->scrollOffset());
368 const QRect visible = p.matrix().inverted().mapRect(e->rect());
370 const QPair<int, int> visibleLines
371 = d->visibleLines(visible.top(), visible.bottom());
373 assert(visibleLines.first <= visibleLines.second);
375 const Private::Style defaultStyle = { p.pen().color() };
377 const Private::Cache &cache = d->cache;
379 p.setPen(Qt::NoPen);
381 p.setBrush(palette().base());
383 if (d->alternatingRowColors) {
385 p.drawRect(visible);
387 #if 0 // leaves garbage
388 for (unsigned int i = visibleLines.first % 2 ? visibleLines.first + 1 : visibleLines.first, end = visibleLines.second ; i < end ; i += 2) {
389 p.drawRect(d->lineRect(i));
392 if (visibleLines.second >= 0) {
393 const int lastY = d->lineRect(visibleLines.second - 1).y();
394 if (lastY < visible.bottom()) {
395 p.drawRect(0, lastY + 1, cache.dimensions.longestLineLength, visible.bottom() - lastY);
398 #endif
400 p.setBrush(palette().alternateBase());
401 for (unsigned int i = visibleLines.first % 2 ? visibleLines.first : visibleLines.first + 1, end = visibleLines.second ; i < end ; i += 2) {
402 p.drawRect(d->lineRect(i));
405 } else {
407 p.drawRect(visible);
411 // ### unused optimization: paint lines by styles to minimise pen changes.
412 for (unsigned int i = visibleLines.first, end = visibleLines.second ; i != end ; ++i) {
413 const Private::LineItem &li = d->lines[i];
414 assert(!li.styleID || d->styleByID.contains(li.styleID));
415 const Private::Style &st = li.styleID ? d->styleByID[li.styleID] : defaultStyle ;
417 p.setPen(st.color);
418 p.drawText(0, i * cache.fontMetrics.lineSpacing + cache.fontMetrics.ascent, li.text);
423 void KDLogTextWidget::timerEvent(QTimerEvent *e)
425 if (e->timerId() == d->timer.timerId()) {
426 d->addPendingLines();
427 d->timer.stop();
428 } else {
429 QAbstractScrollArea::timerEvent(e);
433 void KDLogTextWidget::changeEvent(QEvent *e)
435 QAbstractScrollArea::changeEvent(e);
436 d->cache.dirty |= Private::Cache::FontMetrics;
439 void KDLogTextWidget::resizeEvent(QResizeEvent *)
441 d->updateScrollRanges();
444 KDLogTextWidget::Private::Private(KDLogTextWidget *qq)
445 : q(qq),
446 styleByID(),
447 idByStyle(),
448 lines(),
449 pendingLines(),
450 historySize(0xFFFFFFFF),
451 minimumVisibleLines(1),
452 minimumVisibleColumns(1),
453 alternatingRowColors(false),
454 timer(),
455 cache()
457 // PENDING(marc) find all the magic flags we need here...
458 QWidget *const vp = qq->viewport();
459 vp->setBackgroundRole(QPalette::Base);
460 vp->setAttribute(Qt::WA_StaticContents);
461 vp->setAttribute(Qt::WA_NoSystemBackground);
462 #ifndef QT_NO_CURSOR
463 vp->setCursor(Qt::IBeamCursor);
464 #endif
467 KDLogTextWidget::Private::~Private() {}
469 void KDLogTextWidget::Private::updateCache() const
472 if (cache.dirty >= Cache::FontMetrics) {
473 const QFontMetrics &fm = q->fontMetrics();
474 cache.fontMetrics.lineSpacing = fm.lineSpacing();
475 cache.fontMetrics.ascent = fm.ascent();
476 cache.fontMetrics.averageCharWidth = fm.averageCharWidth();
478 QVector<int> &lw = cache.fontMetrics.lineWidths;
479 lw.clear();
480 lw.reserve(lines.size());
481 Q_FOREACH (const LineItem &li, lines) {
482 lw.push_back(fm.width(li.text));
486 if (cache.dirty >= Cache::Dimensions) {
487 const QVector<int> &lw = cache.fontMetrics.lineWidths;
488 const QVector<int>::const_iterator it =
489 std::max_element(lw.begin(), lw.end());
490 if (it == lw.end()) {
491 cache.dimensions.indexOfLongestLine = -1;
492 cache.dimensions.longestLineLength = 0;
493 } else {
494 cache.dimensions.indexOfLongestLine = it - lw.begin();
495 cache.dimensions.longestLineLength = *it;
499 cache.dirty = false;
502 unsigned int KDLogTextWidget::Private::findOrAddStyle(const Style &s)
504 if (idByStyle.contains(s)) {
505 const unsigned int id = idByStyle[s];
506 assert(styleByID.contains(id));
507 assert(styleByID[id] == s);
508 return id;
509 } else {
510 static unsigned int nextID = 0; // remember, 0 is reserved
511 const unsigned int id = ++nextID;
512 idByStyle.insert(s, id);
513 styleByID.insert(id, s);
514 return id;
518 void KDLogTextWidget::Private::enforceHistorySize()
520 const size_t numLimes = lines.size();
521 if (numLimes <= historySize) {
522 return;
524 const int remove = numLimes - historySize ;
525 lines.erase(lines.begin(), lines.begin() + remove);
527 // can't quickly update the dimensions if the fontMetrics aren't uptodate.
528 if (cache.dirty & Cache::FontMetrics) {
529 cache.dirty |= Cache::Dimensions;
530 return;
533 QVector<int> &lw = cache.fontMetrics.lineWidths;
535 assert(lw.size() > remove);
536 lw.erase(lw.begin(), lw.begin() + remove);
538 if (cache.dirty & Cache::Dimensions) {
539 return;
542 if (cache.dimensions.indexOfLongestLine >= remove) {
543 cache.dimensions.indexOfLongestLine -= remove;
544 } else {
545 cache.dirty |= Cache::Dimensions;
549 static void set_scrollbar_properties(QScrollBar &sb, int document, int viewport, int singleStep, Qt::Orientation o)
551 const int min = 0;
552 const int max = std::max(0, document - viewport);
553 const int value = sb.value();
554 const bool wasAtEnd = value == sb.maximum();
555 sb.setRange(min, max);
556 sb.setPageStep(viewport);
557 sb.setSingleStep(singleStep);
558 sb.setValue(o == Qt::Vertical && wasAtEnd ? sb.maximum() : value);
561 void KDLogTextWidget::Private::updateScrollRanges()
564 updateCache();
566 if (QScrollBar *const sb = q->verticalScrollBar()) {
567 const int document = lines.size() * cache.fontMetrics.lineSpacing ;
568 const int viewport = q->viewport()->height();
569 const int singleStep = cache.fontMetrics.lineSpacing;
570 set_scrollbar_properties(*sb, document, viewport, singleStep, Qt::Vertical);
573 if (QScrollBar *const sb = q->horizontalScrollBar()) {
574 const int document = cache.dimensions.longestLineLength;
575 const int viewport = q->viewport()->width();
576 const int singleStep = cache.fontMetrics.lineSpacing; // rather randomly chosen
577 set_scrollbar_properties(*sb, document, viewport, singleStep, Qt::Horizontal);
581 void KDLogTextWidget::Private::addPendingLines()
583 if (pendingLines.empty()) {
584 return;
587 const unsigned int oldNumLines = lines.size();
589 lines += pendingLines;
591 // if the cache isn't dirty, we can quickly update it without
592 // invalidation:
594 if (!cache.dirty) {
596 // update fontMetrics:
597 const QFontMetrics &fm = q->fontMetrics();
598 QVector<int> plw;
599 plw.reserve(pendingLines.size());
600 Q_FOREACH (const LineItem &li, pendingLines) {
601 plw.push_back(fm.width(li.text));
604 // update dimensions:
605 const QVector<int>::const_iterator it =
606 std::max_element(plw.constBegin(), plw.constEnd());
607 if (*it >= cache.dimensions.longestLineLength) {
608 cache.dimensions.longestLineLength = *it;
609 cache.dimensions.indexOfLongestLine = oldNumLines + (it - plw.constBegin());
613 pendingLines.clear();
615 enforceHistorySize();
616 updateScrollRanges();
617 q->viewport()->update();
620 int KDLogTextWidget::Private::lineByYCoordinate(int y) const
622 updateCache();
623 if (cache.fontMetrics.lineSpacing == 0) {
624 return -1;
626 const int raw = y / cache.fontMetrics.lineSpacing ;
627 if (raw < 0) {
628 return -1;
630 if (raw >= lines.size()) {
631 return lines.size() - 1;
633 return raw;
636 static int get_scrollbar_offset(const QScrollBar *sb)
638 return sb ? sb->value() : 0 ;
641 QPoint KDLogTextWidget::Private::scrollOffset() const
643 return QPoint(get_scrollbar_offset(q->horizontalScrollBar()),
644 get_scrollbar_offset(q->verticalScrollBar()));