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