1 /*
2     Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3 
4     This file is part of CopyQ.
5 
6     CopyQ is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     CopyQ is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 #include "gui/logdialog.h"
20 #include "ui_logdialog.h"
21 
22 #include "common/common.h"
23 #include "common/log.h"
24 #include "common/timer.h"
25 
26 #include <QCheckBox>
27 #include <QElapsedTimer>
28 #include <QRegularExpression>
29 #include <QTextBlock>
30 #include <QTextCharFormat>
31 #include <QTextBlockFormat>
32 #include <QTextCursor>
33 #include <QTimer>
34 
35 namespace {
36 
37 const int maxDisplayLogSize = 128 * 1024;
38 const auto logLinePrefix = "CopyQ ";
39 
showLogLines(QString * content,bool show,LogLevel level)40 void showLogLines(QString *content, bool show, LogLevel level)
41 {
42     if (show)
43         return;
44 
45     const QString label = logLinePrefix + logLevelLabel(level);
46 
47     const QRegularExpression re("\n" + label + "[^\n]*");
48     content->remove(re);
49 
50     const QRegularExpression re2("^" + label + "[^\n]*\n");
51     content->remove(re2);
52 }
53 
54 } // namespace
55 
56 /// Decorates document in batches so it doesn't block UI.
57 class Decorator : public QObject
58 {
59 public:
Decorator(const QRegularExpression & re,QObject * parent)60     Decorator(const QRegularExpression &re, QObject *parent)
61         : QObject(parent)
62         , m_re(re)
63     {
64         initSingleShotTimer(&m_timerDecorate, 0, this, &Decorator::decorateBatch);
65     }
66 
67     /// Start decorating.
decorate(QTextDocument * document)68     void decorate(QTextDocument *document)
69     {
70         m_tc = QTextCursor(document);
71         m_tc.movePosition(QTextCursor::End);
72         decorateBatch();
73     }
74 
75 private:
decorateBatch()76     void decorateBatch()
77     {
78         if (m_tc.isNull())
79             return;
80 
81         QElapsedTimer t;
82         t.start();
83 
84         do {
85             m_tc = m_tc.document()->find(m_re, m_tc, QTextDocument::FindBackward);
86             if (m_tc.isNull())
87                 return;
88 
89             decorate(&m_tc);
90         } while ( t.elapsed() < 20 );
91 
92         m_timerDecorate.start();
93     }
94 
95     virtual void decorate(QTextCursor *tc) = 0;
96 
97     QTimer m_timerDecorate;
98     QTextCursor m_tc;
99     QRegularExpression m_re;
100 };
101 
102 namespace {
103 
104 class LogDecorator final : public Decorator
105 {
106 public:
LogDecorator(const QFont & font,QObject * parent)107     LogDecorator(const QFont &font, QObject *parent)
108         : Decorator(QRegularExpression("^[^\\]]*\\]"), parent)
109         , m_labelNote(logLevelLabel(LogNote))
110         , m_labelError(logLevelLabel(LogError))
111         , m_labelWarning(logLevelLabel(LogWarning))
112         , m_labelDebug(logLevelLabel(LogDebug))
113         , m_labelTrace(logLevelLabel(LogTrace))
114     {
115         QFont boldFont = font;
116         boldFont.setBold(true);
117 
118         QTextCharFormat normalFormat;
119         normalFormat.setFont(boldFont);
120         normalFormat.setBackground(Qt::white);
121         normalFormat.setForeground(Qt::black);
122 
123         m_noteLogLevelFormat = normalFormat;
124 
125         m_errorLogLevelFormat = normalFormat;
126         m_errorLogLevelFormat.setForeground(Qt::red);
127 
128         m_warningLogLevelFormat = normalFormat;
129         m_warningLogLevelFormat.setForeground(Qt::darkRed);
130 
131         m_debugLogLevelFormat = normalFormat;
132         m_debugLogLevelFormat.setForeground(QColor(100, 100, 200));
133 
134         m_traceLogLevelFormat = normalFormat;
135         m_traceLogLevelFormat.setForeground(QColor(200, 150, 100));
136     }
137 
138 private:
decorate(QTextCursor * tc)139     void decorate(QTextCursor *tc) override
140     {
141         const QString text = tc->selectedText();
142         if ( text.startsWith(m_labelNote) )
143             tc->setCharFormat(m_noteLogLevelFormat);
144         else if ( text.startsWith(m_labelError) )
145             tc->setCharFormat(m_errorLogLevelFormat);
146         else if ( text.startsWith(m_labelWarning) )
147             tc->setCharFormat(m_warningLogLevelFormat);
148         else if ( text.startsWith(m_labelDebug) )
149             tc->setCharFormat(m_debugLogLevelFormat);
150         else if ( text.startsWith(m_labelTrace) )
151             tc->setCharFormat(m_traceLogLevelFormat);
152     }
153 
154     QString m_labelNote;
155     QString m_labelError;
156     QString m_labelWarning;
157     QString m_labelDebug;
158     QString m_labelTrace;
159 
160     QTextCharFormat m_noteLogLevelFormat;
161     QTextCharFormat m_errorLogLevelFormat;
162     QTextCharFormat m_warningLogLevelFormat;
163     QTextCharFormat m_debugLogLevelFormat;
164     QTextCharFormat m_traceLogLevelFormat;
165 };
166 
167 class StringDecorator final : public Decorator
168 {
169 public:
StringDecorator(QObject * parent)170     explicit StringDecorator(QObject *parent)
171         : Decorator(QRegularExpression("\"[^\"]*\"|'[^']*'"), parent)
172     {
173         m_stringFormat.setForeground(Qt::darkGreen);
174     }
175 
176 private:
decorate(QTextCursor * tc)177     void decorate(QTextCursor *tc) override
178     {
179         tc->setCharFormat(m_stringFormat);
180     }
181 
182     QTextCharFormat m_stringFormat;
183 };
184 
185 class ThreadNameDecorator final : public Decorator
186 {
187 public:
ThreadNameDecorator(const QFont & font,QObject * parent)188     explicit ThreadNameDecorator(const QFont &font, QObject *parent)
189         : Decorator(QRegularExpression("<[A-Za-z]+-[0-9-]+>"), parent)
190     {
191         QFont boldFont = font;
192         boldFont.setBold(true);
193         m_format.setFont(boldFont);
194     }
195 
196 private:
decorate(QTextCursor * tc)197     void decorate(QTextCursor *tc) override
198     {
199         // Colorize thread label.
200         const auto text = tc->selectedText();
201 
202         const auto hash = qHash(text);
203         const int h = hash % 360;
204         m_format.setForeground( QColor::fromHsv(h, 150, 100) );
205 
206         const auto bg =
207                 text.startsWith("<Server-") ? QColor::fromRgb(255, 255, 200)
208               : text.startsWith("<monitor") ? QColor::fromRgb(220, 240, 255)
209               : text.startsWith("<provide") ? QColor::fromRgb(220, 255, 220)
210               : text.startsWith("<synchronize") ? QColor::fromRgb(220, 255, 240)
211               : QColor(Qt::white);
212         m_format.setBackground(bg);
213 
214         tc->setCharFormat(m_format);
215     }
216 
217     QTextCharFormat m_format;
218 };
219 
220 } // namespace
221 
LogDialog(QWidget * parent)222 LogDialog::LogDialog(QWidget *parent)
223     : QDialog(parent)
224     , ui(new Ui::LogDialog)
225     , m_showError(true)
226     , m_showWarning(true)
227     , m_showNote(true)
228     , m_showDebug(true)
229     , m_showTrace(true)
230 {
231     ui->setupUi(this);
232 
233     auto font = ui->textBrowserLog->font();
234     font.setFamily("Monospace");
235     ui->textBrowserLog->setFont(font);
236 
237     m_logDecorator = new LogDecorator(font, this);
238     m_stringDecorator = new StringDecorator(this);
239     m_threadNameDecorator = new ThreadNameDecorator(font, this);
240 
241     ui->labelLogFileName->setText(logFileName());
242 
243     addFilterCheckBox(LogError, &LogDialog::showError);
244     addFilterCheckBox(LogWarning, &LogDialog::showWarning);
245     addFilterCheckBox(LogNote, &LogDialog::showNote);
246     addFilterCheckBox(LogDebug, &LogDialog::showDebug);
247     addFilterCheckBox(LogTrace, &LogDialog::showTrace);
248     ui->layoutFilters->addStretch(1);
249 
250     updateLog();
251 }
252 
~LogDialog()253 LogDialog::~LogDialog()
254 {
255     delete ui;
256 }
257 
updateLog()258 void LogDialog::updateLog()
259 {
260     QString content = readLogFile(maxDisplayLogSize);
261 
262     // Remove first line if incomplete.
263     if ( !content.startsWith(logLinePrefix) ) {
264         const int i = content.indexOf('\n');
265         content.remove(0, i + 1);
266     }
267 
268     showLogLines(&content, m_showError, LogError);
269     showLogLines(&content, m_showWarning, LogWarning);
270     showLogLines(&content, m_showNote, LogNote);
271     showLogLines(&content, m_showDebug, LogDebug);
272     showLogLines(&content, m_showTrace, LogTrace);
273 
274     // Remove common prefix.
275     const QString prefix = logLinePrefix;
276     if ( content.startsWith(prefix) )
277         content.remove( 0, prefix.size() );
278     content.replace("\n" + prefix, "\n");
279 
280     ui->textBrowserLog->setPlainText(content);
281 
282     ui->textBrowserLog->moveCursor(QTextCursor::End);
283 
284     QTextDocument *doc = ui->textBrowserLog->document();
285     m_logDecorator->decorate(doc);
286     m_stringDecorator->decorate(doc);
287     m_threadNameDecorator->decorate(doc);
288 }
289 
showError(bool show)290 void LogDialog::showError(bool show)
291 {
292     m_showError = show;
293     updateLog();
294 }
295 
showWarning(bool show)296 void LogDialog::showWarning(bool show)
297 {
298     m_showWarning = show;
299     updateLog();
300 }
301 
showNote(bool show)302 void LogDialog::showNote(bool show)
303 {
304     m_showNote = show;
305     updateLog();
306 }
307 
showDebug(bool show)308 void LogDialog::showDebug(bool show)
309 {
310     m_showDebug = show;
311     updateLog();
312 }
313 
showTrace(bool show)314 void LogDialog::showTrace(bool show)
315 {
316     m_showTrace = show;
317     updateLog();
318 }
319 
addFilterCheckBox(LogLevel level,FilterCheckBoxSlot slot)320 void LogDialog::addFilterCheckBox(LogLevel level, FilterCheckBoxSlot slot)
321 {
322     auto checkBox = new QCheckBox(this);
323     checkBox->setText(logLevelLabel(level));
324     checkBox->setChecked(true);
325     QObject::connect(checkBox, &QCheckBox::toggled, this, slot);
326     ui->layoutFilters->addWidget(checkBox);
327 }
328