1 /****************************************************************************
2 ** Copyright (C) 2001-2010 Klaralvdalens Datakonsult AB. All rights reserved.
4 ** This file is part of the KD Tools library.
6 ** Licensees holding valid commercial KD Tools licenses may use this file in
7 ** accordance with the KD Tools Commercial License Agreement provided with
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
21 **********************************************************************/
23 #include <config-kleopatra.h>
25 #include "kdlogtextwidget.h"
27 #include <QBasicTimer>
30 #include <QPaintEvent>
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
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
;
60 explicit Private(KDLogTextWidget
*qq
);
63 void updateCache() const;
67 if (!timer
.isActive()) {
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
87 return QRect(0, idx
* cache
.fontMetrics
.lineSpacing
, cache
.dimensions
.longestLineLength
, cache
.fontMetrics
.lineSpacing
- 1);
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();
109 unsigned int styleID
;
112 unsigned int findOrAddStyle(const Style
&style
);
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
;
128 mutable struct Cache
{
129 enum { Dimensions
= 1, FontMetrics
= 2, All
= FontMetrics
| Dimensions
};
130 Cache() : dirty(All
) {}
136 int averageCharWidth
;
137 QVector
<int> lineWidths
;
141 int indexOfLongestLine
;
142 int longestLineLength
;
148 Constructor. Creates an empty KDLogTextWidget.
150 KDLogTextWidget::KDLogTextWidget(QWidget
*parent_
)
151 : QAbstractScrollArea(parent_
), d(new Private(this))
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
170 void KDLogTextWidget::setHistorySize(unsigned int hs
)
172 if (hs
== d
->historySize
) {
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
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
207 void KDLogTextWidget::setLines(const QStringList
&l
)
210 Q_FOREACH (const QString
&s
, l
) {
215 QStringList
KDLogTextWidget::lines() const
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
);
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
) {
241 d
->minimumVisibleLines
= num
;
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
) {
266 d
->minimumVisibleColumns
= num
;
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
) {
289 d
->alternatingRowColors
= on
;
293 bool KDLogTextWidget::alternatingRowColors() const
295 return d
->alternatingRowColors
;
298 QSize
KDLogTextWidget::minimumSizeHint() const
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();
314 return 2 * minimumSizeHint();
321 \post lines().empty() == true
323 void KDLogTextWidget::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
);
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
);
359 void KDLogTextWidget::paintEvent(QPaintEvent
*e
)
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
;
381 p
.setBrush(palette().base());
383 if (d
->alternatingRowColors
) {
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
);
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
));
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
;
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();
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
)
450 historySize(0xFFFFFFFF),
451 minimumVisibleLines(1),
452 minimumVisibleColumns(1),
453 alternatingRowColors(false),
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
);
463 vp
->setCursor(Qt::IBeamCursor
);
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
;
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;
494 cache
.dimensions
.indexOfLongestLine
= it
- lw
.begin();
495 cache
.dimensions
.longestLineLength
= *it
;
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
);
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
);
518 void KDLogTextWidget::Private::enforceHistorySize()
520 const size_t numLimes
= lines
.size();
521 if (numLimes
<= historySize
) {
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
;
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
) {
542 if (cache
.dimensions
.indexOfLongestLine
>= remove
) {
543 cache
.dimensions
.indexOfLongestLine
-= remove
;
545 cache
.dirty
|= Cache::Dimensions
;
549 static void set_scrollbar_properties(QScrollBar
&sb
, int document
, int viewport
, int singleStep
, Qt::Orientation o
)
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()
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()) {
587 const unsigned int oldNumLines
= lines
.size();
589 lines
+= pendingLines
;
591 // if the cache isn't dirty, we can quickly update it without
596 // update fontMetrics:
597 const QFontMetrics
&fm
= q
->fontMetrics();
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
623 if (cache
.fontMetrics
.lineSpacing
== 0) {
626 const int raw
= y
/ cache
.fontMetrics
.lineSpacing
;
630 if (raw
>= lines
.size()) {
631 return lines
.size() - 1;
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()));