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