1 /*
2 logview.cpp
3
4 This file is part of GammaRay, the Qt application inspection and
5 manipulation tool.
6
7 Copyright (C) 2016-2021 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
8 Author: Giulio Camuffo <giulio.camuffo@kdab.com>
9
10 Licensees holding valid commercial KDAB GammaRay licenses may use this file in
11 accordance with GammaRay Commercial License Agreement provided with the Software.
12
13 Contact info@kdab.com if any conditions of this licensing are not clear to you.
14
15 This program is free software; you can redistribute it and/or modify
16 it under the terms of the GNU General Public License as published by
17 the Free Software Foundation, either version 2 of the License, or
18 (at your option) any later version.
19
20 This program is distributed in the hope that it will be useful,
21 but WITHOUT ANY WARRANTY; without even the implied warranty of
22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 GNU General Public License for more details.
24
25 You should have received a copy of the GNU General Public License
26 along with this program. If not, see <http://www.gnu.org/licenses/>.
27 */
28
29 #include "logview.h"
30
31 #include <QMouseEvent>
32 #include <QScrollBar>
33 #include <QStaticText>
34 #include <QPainter>
35 #include <QScrollArea>
36 #include <QClipboard>
37 #include <QApplication>
38 #include <QtMath>
39
40 #include "ringbuffer.h"
41
42 namespace GammaRay {
43
44 class View : public QWidget
45 {
46 public:
View(QWidget * p)47 explicit View(QWidget *p)
48 : QWidget(p)
49 , m_lines(5000)
50 , m_metrics(QFont())
51 , m_lineHeight(m_metrics.height())
52 , m_client(0)
53 {
54 resize(0, 0);
55 setFocusPolicy(Qt::ClickFocus);
56 setCursor(Qt::IBeamCursor);
57 }
58
sizeHint() const59 QSize sizeHint() const override
60 {
61 return size();
62 }
63
drawLine(QPainter & painter,const QRect & rect,const QStaticText & line)64 void drawLine(QPainter &painter, const QRect &rect, const QStaticText &line)
65 {
66 painter.setPen(palette().color(QPalette::Text));
67 painter.drawStaticText(0, rect.y(), line);
68 }
69
drawLineSelected(QPainter & painter,const QRect & rect,const QStaticText & line)70 void drawLineSelected(QPainter &painter, const QRect &rect, const QStaticText &line)
71 {
72 painter.fillRect(rect, palette().highlight());
73 painter.setPen(palette().color(QPalette::HighlightedText));
74 painter.drawStaticText(0, rect.y(), line);
75 }
76
drawLinePartialSelected(QPainter & painter,const QRect & rect,const QStaticText & line,int startSelectChar,int endSelectChar)77 void drawLinePartialSelected(QPainter &painter, const QRect &rect, const QStaticText &line, int startSelectChar, int endSelectChar)
78 {
79 const QString &text = line.text();
80 int startX = m_metrics.width(text.left(startSelectChar));
81 int endX = m_metrics.width(text.left(endSelectChar));
82
83 if (startSelectChar > 0) {
84 painter.drawText(QRect(rect.x(), rect.y(), startX, rect.height()), Qt::TextDontClip, text.left(startSelectChar));
85 }
86
87 QRect selectRect(rect.x() + startX, rect.y(), endX - startX, rect.height());
88 painter.fillRect(selectRect, palette().highlight());
89 painter.setPen(palette().color(QPalette::HighlightedText));
90 painter.drawText(selectRect, Qt::TextDontClip, text.mid(startSelectChar, endSelectChar - startSelectChar));
91
92 if (endSelectChar < text.count()) {
93 painter.setPen(palette().color(QPalette::Text));
94 painter.drawText(QRect(rect.x() + endX, rect.y(), m_metrics.width(text) - endX, rect.height()), text.mid(endSelectChar));
95 }
96 }
97
selectionBoundaries(QPoint & start,QPoint & end) const98 void selectionBoundaries(QPoint &start, QPoint &end) const
99 {
100 bool startBeforeEnd = (m_selectionStart.y() < m_selectionEnd.y()) ||
101 (m_selectionStart.y() == m_selectionEnd.y() && m_selectionStart.x() < m_selectionEnd.x());
102 start = startBeforeEnd ? m_selectionStart : m_selectionEnd;
103 end = startBeforeEnd ? m_selectionEnd : m_selectionStart;
104 }
105
106 struct LineSelection
107 {
isNullGammaRay::View::LineSelection108 bool isNull() const { return start == end; }
isFullGammaRay::View::LineSelection109 bool isFull() const { return start == 0 && end < 0; }
110
111 int start;
112 int end;
113 };
lineSelection(int line) const114 LineSelection lineSelection(int line) const
115 {
116 if (m_selectionStart == m_selectionEnd) {
117 return { 0, 0 };
118 }
119
120 QPoint start, end;
121 selectionBoundaries(start, end);
122
123 if (start.y() < line && line < end.y()) {
124 return { 0, m_lines.at(line).text.text().count() };
125 }
126
127 if (start.y() == line || end.y() == line) {
128 int startChar = 0;
129 int endChar = m_lines.at(line).text.text().count();
130 if (start.y() == line)
131 startChar = start.x();
132 if (end.y() == line)
133 endChar = end.x() + 1;
134 return { startChar, endChar };
135 }
136
137 return { 0, 0 };
138 }
139
paintEvent(QPaintEvent * event)140 void paintEvent(QPaintEvent *event) override
141 {
142 if (m_lineHeight < 0) {
143 return;
144 }
145
146 QPainter painter(this);
147
148 QRectF drawRect = event->rect();
149 int startingLine = lineAt(drawRect.y());
150 int y = linePosAt(drawRect.y());
151
152 for (int i = startingLine; i < m_lines.count(); ++i) {
153 if (m_client && m_lines.at(i).pid != m_client) {
154 continue;
155 }
156 const QStaticText &text = m_lines.at(i).text;
157
158 QRect lineRect(QRect(0, y, text.size().width(), m_lineHeight));
159 painter.fillRect(QRectF(0, y, drawRect.width(), m_lineHeight), i % 2 ? palette().base() : palette().alternateBase());
160
161 LineSelection selection = lineSelection(i);
162 if (selection.isNull()) {
163 drawLine(painter, lineRect, text);
164 } else if (selection.isFull()) {
165 drawLineSelected(painter, lineRect, text);
166 } else {
167 drawLinePartialSelected(painter, lineRect, text, selection.start, selection.end);
168 }
169
170 y += m_lineHeight;
171 if (y >= drawRect.bottom())
172 break;
173 }
174 }
175
linesCount() const176 inline int linesCount() const
177 {
178 return m_client ? m_linesCount.value(m_client) : m_lines.count();
179 }
180
linePosAt(int y) const181 inline int linePosAt(int y) const {
182 int line = qMin(y / m_lineHeight, m_lines.count() - 1);
183 return line * m_lineHeight;
184 }
185
lineAt(int y) const186 inline int lineAt(int y) const {
187 int line = qMin(y / m_lineHeight, m_lines.count() - 1);
188 if (!m_client) {
189 return line;
190 }
191
192 for (int i = 0, l = 0; i < m_lines.count(); ++i) {
193 if (m_lines.at(i).pid == m_client) {
194 if (l++ == line) {
195 return i;
196 }
197 }
198 }
199 return line;
200 }
charPosAt(const QPointF & p) const201 inline QPoint charPosAt(const QPointF &p) const
202 {
203 int line = lineAt(p.y());
204 int lineX = 0;
205
206 const QString &text = m_lines.at(line).text.text();
207 for (int x = 0, i = 0; i < text.count(); ++i) {
208 const QChar &c = text.at(i);
209 if (p.x() >= x) {
210 lineX = i;
211 }
212 x += m_metrics.width(c);
213 }
214
215 return {lineX, line};
216 }
217
mousePressEvent(QMouseEvent * e)218 void mousePressEvent(QMouseEvent *e) override
219 {
220 if (e->button() == Qt::LeftButton) {
221 m_selectionStart = m_selectionEnd = charPosAt(e->pos());
222 e->accept();
223 update();
224 }
225 }
226
mouseMoveEvent(QMouseEvent * e)227 void mouseMoveEvent(QMouseEvent *e) override
228 {
229 m_selectionEnd = charPosAt(e->pos());
230 e->accept();
231 update();
232 }
233
selectedText() const234 QString selectedText() const
235 {
236 if (m_selectionStart == m_selectionEnd) {
237 return QString();
238 }
239
240 QPoint start, end;
241 selectionBoundaries(start, end);
242 QString string;
243 for (int i = start.y(); i <= end.y(); ++i) {
244 if (m_client && m_lines.at(i).pid != m_client) {
245 continue;
246 }
247 const QStaticText &line = m_lines.at(i).text;
248 LineSelection selection = lineSelection(i);
249 string += line.text().mid(selection.start, selection.end - selection.start);
250 string += QLatin1Char('\n');
251 }
252 return string;
253 }
254
keyPressEvent(QKeyEvent * e)255 void keyPressEvent(QKeyEvent *e) override
256 {
257 if (e->key() == Qt::Key_C && e->modifiers() == Qt::ControlModifier) {
258 QApplication::clipboard()->setText(selectedText());
259 }
260 }
261
resetSelection()262 void resetSelection()
263 {
264 m_selectionStart = m_selectionEnd = QPoint();
265 update();
266 }
267
268 struct Line {
269 quint64 pid = 0;
270 QStaticText text;
271 int *counter = nullptr;
272
273 Line() = default;
274
LineGammaRay::View::Line275 Line(quint64 p, const QStaticText &t, int *cnt)
276 : pid(p), text(t), counter(cnt)
277 {
278 (*counter)++;
279 }
280
LineGammaRay::View::Line281 Line(const Line &l)
282 : pid(l.pid), text(l.text), counter(l.counter)
283 {
284 (*counter)++;
285 }
286
~LineGammaRay::View::Line287 ~Line() { (*counter)--; }
288
operator =GammaRay::View::Line289 Line &operator=(const Line &l) {
290 (*counter)--;
291
292 pid = l.pid;
293 text = l.text;
294 counter = l.counter;
295
296 (*counter)++;
297 return *this;
298 }
299
300 };
301 RingBuffer<Line> m_lines;
302 QHash<quint64, int> m_linesCount;
303 QFontMetricsF m_metrics;
304 int m_lineHeight;
305 QPoint m_selectionStart;
306 QPoint m_selectionEnd;
307 quint64 m_client;
308 };
309
310
311 class Messages : public QScrollArea
312 {
313 public:
Messages(QWidget * parent)314 explicit Messages(QWidget *parent)
315 : QScrollArea(parent)
316 , m_view(new View(this))
317 {
318 m_view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
319 setWidget(m_view);
320 setWidgetResizable(true);
321 }
322
logMessage(quint64 pid,qint64 time,const QByteArray & msg)323 void logMessage(quint64 pid, qint64 time, const QByteArray &msg)
324 {
325 auto scrollbar = verticalScrollBar();
326 bool scroll = scrollbar->value() >= scrollbar->maximum();
327
328 add(pid, time, msg);
329
330 if (scroll)
331 scrollbar->setValue(scrollbar->maximum());
332 }
333
reset()334 void reset()
335 {
336 m_view->m_lines.clear();
337 m_view->resize(0, 0);
338 }
339
updateSize()340 void updateSize()
341 {
342 QSizeF lineSize = m_view->m_lines.last().text.size();
343
344 int w = m_view->width();
345 int h = m_view->linesCount() * m_view->m_lineHeight;
346
347 if (lineSize.width() > w) {
348 w = lineSize.width();
349 }
350 m_view->resize(w, h);
351 m_view->update();
352 }
353
add(quint64 pid,qint64 time,const QByteArray & m)354 void add(quint64 pid, qint64 time, const QByteArray &m)
355 {
356 m_view->m_lines.append(View::Line(pid, QStaticText(QString("[%1ms] %2").arg(QString::number(time / 1e6), QString(m))), &m_view->m_linesCount[pid]));
357
358 if (m_view->m_client && pid != m_view->m_client) {
359 return;
360 }
361
362 updateSize();
363 }
364
setLoggingClient(quint64 pid)365 void setLoggingClient(quint64 pid)
366 {
367 m_view->m_client = pid;
368
369 auto scrollbar = verticalScrollBar();
370 qreal v = (qreal)scrollbar->value() / (qreal)scrollbar->maximum();
371
372 m_view->resetSelection();
373 updateSize();
374
375 // keep the scrollbar at he same percentage
376 scrollbar->setValue(v * (qreal)scrollbar->maximum());
377 }
378
379 View *m_view;
380 };
381
382
383 class Timeline : public QScrollArea
384 {
385 public:
386 class View : public QWidget
387 {
388 public:
389 struct DataPoint {
390 qint64 time;
391 quint64 pid;
392 QByteArray msg;
393 };
394
View()395 View()
396 : m_data(5000)
397 {
398 resize(100, 100);
399 setAttribute(Qt::WA_OpaquePaintEvent);
400 setMouseTracking(true);
401 }
402
sizeHint() const403 QSize sizeHint() const override
404 {
405 return size();
406 }
407
initialTime() const408 inline qint64 initialTime() const { return m_data.isEmpty() ? 0 : m_data.at(0).time; }
409
paintEvent(QPaintEvent * event)410 void paintEvent(QPaintEvent *event) override
411 {
412 QPainter painter(this);
413 QRectF drawRect = event->rect();
414 const auto palette = this->palette();
415
416 painter.fillRect(drawRect, palette.base());
417
418 qreal l = 1;
419 qreal step = l / m_zoom;
420 while (step < 60) {
421 l *= 10;
422 step = l / m_zoom;
423 }
424
425 int substeps = 5;
426 int mul = 2;
427 while (step / substeps > 60) {
428 substeps *= mul;
429 mul = mul == 2 ? 5 : 2;
430 }
431
432 auto it = initialTime();
433 auto rit = round(it, -1);
434
435 //draw the grid lines
436 qreal linesSpacing = step / substeps;
437 int startLine = drawRect.left() / linesSpacing - (rit - it) / m_zoom; //round the starting position so that we have nice numbers'
438
439 int s = startLine;
440 for (qreal i = startLine * linesSpacing; i < drawRect.right(); i += linesSpacing, s++) {
441 bool isStep = s % substeps == 0;
442 painter.setPen(isStep ? palette.color(QPalette::Highlight) : palette.color(QPalette::Midlight));
443
444 int y = 0;
445 if (isStep) {
446 int stepN = s / substeps;
447 y = 15 * (stepN % 2 + 1);
448 }
449
450 painter.drawLine(i, y, i, drawRect.bottom());
451 }
452
453 //draw the text after having drawn all the lines, so we're sure they don't go over it
454 s = startLine;
455 painter.setPen(palette.color(QPalette::Highlight));
456 for (qreal i = startLine * linesSpacing; i < drawRect.right(); i += step / substeps, s++) { //krazy:exclude=postfixop
457 bool isStep = s % substeps == 0;
458 if (isStep) {
459 painter.drawText(i-100, ((s / substeps) % 2) * 15, 200, 200, Qt::AlignHCenter, QString("%1ms").arg(QString::number(qreal(it + i * m_zoom) / 1e6, 'g', 6)));
460 }
461 }
462
463 //finally draw the event lines
464 painter.setPen(palette.color(QPalette::Text));
465 bool hasDrawn = false;
466 for (int i = 0; i < m_data.count(); ++i) {
467 const auto &point = m_data.at(i);
468 if (m_client && point.pid != m_client) {
469 painter.setPen(palette.color(QPalette::Dark));
470 } else {
471 painter.setPen(palette.color(QPalette::Text));
472 }
473
474 qreal offset = point.time - m_start;
475 qreal x = offset / m_zoom;
476 qreal y = qMax(qreal(40.), drawRect.y());
477 if (!drawRect.contains(QPoint(x, y))) {
478 if (hasDrawn)
479 break;
480 else
481 continue;
482 }
483 hasDrawn = true;
484
485 painter.drawLine(x, y, x, drawRect.bottom());
486 }
487 }
488
mouseMoveEvent(QMouseEvent * e)489 void mouseMoveEvent(QMouseEvent *e) override
490 {
491 const QPointF &pos = e->localPos();
492 for (int i = 0; i < m_data.count(); ++i) {
493 qreal timex = (m_data.at(i).time - m_start) / m_zoom;
494 if (fabs(pos.x() - timex) < 2) {
495 setToolTip(m_data.at(i).msg);
496 return;
497 }
498 }
499 }
500
round(qint64 time,int direction)501 qint64 round(qint64 time, int direction)
502 {
503 qint64 v = time % 200;
504 return time + direction * v;
505 }
506
updateSize()507 void updateSize()
508 {
509 if (m_data.count() == 0)
510 return;
511
512 m_start = round(m_data.at(0).time, -1);
513 m_timespan = round(m_data.last().time, 1) - m_start;
514 resize(m_timespan / m_zoom, height());
515 }
516
517 RingBuffer<DataPoint> m_data;
518 qreal m_zoom = 100000;
519 qint64 m_start = 0;
520 qint64 m_timespan = 0;
521 quint64 m_client = 0;
522 };
523
Timeline(QWidget * parent)524 explicit Timeline(QWidget *parent)
525 : QScrollArea(parent)
526 {
527 m_view.setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
528 setWidget(&m_view);
529 setWidgetResizable(true);
530 m_view.installEventFilter(this);
531 }
532
logMessage(quint64 pid,qint64 time,const QByteArray & msg)533 void logMessage(quint64 pid, qint64 time, const QByteArray &msg)
534 {
535 m_view.m_data.append({ time, pid, msg });
536 m_view.updateSize();
537 }
538
setLoggingClient(quint64 pid)539 void setLoggingClient(quint64 pid)
540 {
541 m_view.m_client = pid;
542 m_view.update();
543 }
544
eventFilter(QObject * o,QEvent * e)545 bool eventFilter(QObject *o, QEvent *e) override
546 {
547 if (o == &m_view && e->type() == QEvent::Wheel) {
548 QWheelEvent *we = static_cast<QWheelEvent *>(e);
549
550 qreal pos = we->posF().x() * m_view.m_zoom;
551 auto sb = horizontalScrollBar();
552 int sbvalue = horizontalScrollBar()->value();
553
554 m_view.m_zoom += (1. - qPow( 5. / 4., qreal(we->angleDelta().y()) / 150.)) * m_view.m_zoom;
555 if (m_view.m_zoom < 10) {
556 m_view.m_zoom = 10;
557 }
558
559 m_view.updateSize();
560
561 //keep the point under the mouse still, if possible
562 pos = pos / m_view.m_zoom;
563 sb->setValue(sbvalue + (0.5 + pos - we->posF().x()));
564 }
565 return QScrollArea::eventFilter(o, e);
566 }
567
568 View m_view;
569 };
570
LogView(QWidget * p)571 LogView::LogView(QWidget *p)
572 : QTabWidget(p)
573 , m_messages(new Messages(this))
574 , m_timeline(new Timeline(this))
575 {
576 setTabPosition(QTabWidget::West);
577 addTab(m_messages, tr("Messages"));
578 addTab(m_timeline, tr("Timeline"));
579 }
580
sizeHint() const581 QSize LogView::sizeHint() const
582 {
583 return {200, 200};
584 }
585
logMessage(quint64 pid,qint64 time,const QByteArray & msg)586 void LogView::logMessage(quint64 pid, qint64 time, const QByteArray &msg)
587 {
588 m_messages->logMessage(pid, time, msg);
589 m_timeline->logMessage(pid, time, msg);
590 }
591
setLoggingClient(quint64 pid)592 void LogView::setLoggingClient(quint64 pid)
593 {
594 m_messages->setLoggingClient(pid);
595 m_timeline->setLoggingClient(pid);
596 }
597
reset()598 void LogView::reset()
599 {
600 m_messages->reset();
601 }
602
603 }
604