QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
LogModel.cc
Go to the documentation of this file.
1#include "LogModel.h"
2
3#include "AppSettings.h"
5#include "SettingsManager.h"
6
7QGC_LOGGING_CATEGORY(LogModelLog, "Utilities.LogModel")
8
9LogModel::LogModel(QObject* parent) : LogEntryTableModel(parent)
10{
11 _batchTimer.setSingleShot(true);
12 (void)connect(&_batchTimer, &QChronoTimer::timeout, this, &LogModel::_flushPending);
13
14 _filterTextDebounce.setInterval(kFilterDebounceMs);
15 _filterTextDebounce.setSingleShot(true);
16 connect(&_filterTextDebounce, &QTimer::timeout, this, [this]() {
17 if (_filterText != _pendingFilterText) {
18 _filterText = _pendingFilterText;
19 emit filterTextChanged();
20 if (_filterRegex) {
21 _recompileRegex();
22 }
23 _rebuildFilteredIndices();
24 }
25 });
26}
27
28int LogModel::rowCount(const QModelIndex& parent) const
29{
30 if (parent.isValid()) {
31 return 0;
32 }
33 return _filterBypassed ? static_cast<int>(_entries.size())
34 : static_cast<int>(_filteredIndices.size());
35}
36
37QVariant LogModel::data(const QModelIndex& index, int role) const
38{
39 if (role == Qt::DisplayRole && index.column() == LogEntry::TimestampColumn) {
40 const LogEntry* entry = entryAt(index.row());
41 if (!entry) {
42 return {};
43 }
44 if (!SettingsManager::instance()->appSettings()->showAppLogTimestampAsElapsedTime()->rawValue().toBool()) {
45 return entry->timestamp.toString(QStringLiteral("hh:mm:ss.zzz"));
46 }
47 }
48 return LogEntryTableModel::data(index, role);
49}
50
51void LogModel::multiData(const QModelIndex& index, QModelRoleDataSpan roleDataSpan) const
52{
53 if (index.column() == LogEntry::TimestampColumn) {
54 const LogEntry* entry = entryAt(index.row());
55 if (!entry) {
56 return;
57 }
58 const bool useElapsed = SettingsManager::instance()->appSettings()->showAppLogTimestampAsElapsedTime()->rawValue().toBool();
59 for (auto& roleData : roleDataSpan) {
60 if (roleData.role() == Qt::DisplayRole && !useElapsed) {
61 roleData.setData(entry->timestamp.toString(QStringLiteral("hh:mm:ss.zzz")));
62 } else if (roleData.role() == Qt::DisplayRole) {
63 roleData.setData(entry->columnDisplayData(index.column()));
64 } else {
65 roleData.setData(entry->roleData(roleData.role()));
66 }
67 }
68 return;
69 }
70 LogEntryTableModel::multiData(index, roleDataSpan);
71}
72
73const LogEntry* LogModel::entryAt(int row) const
74{
75 if (row < 0) {
76 return nullptr;
77 }
78
79 if (_filterBypassed) {
80 if (row >= static_cast<int>(_entries.size())) {
81 return nullptr;
82 }
83 return &_entries[row];
84 }
85
86 if (row >= static_cast<int>(_filteredIndices.size())) {
87 return nullptr;
88 }
89 return &_entries[_filteredIndices[row]];
90}
91
93{
94 max = qMax(1000, max);
95 if (_maxEntries != max) {
96 _maxEntries = max;
97 emit maxEntriesChanged();
98 }
99}
100
102{
103 if (_filterLevel != level) {
104 _filterLevel = level;
105 emit filterLevelChanged();
106 _rebuildFilteredIndices();
107 }
108}
109
110void LogModel::setFilterCategory(const QString& category)
111{
112 if (_filterCategory != category) {
113 _filterCategory = category;
115 _rebuildFilteredIndices();
116 }
117}
118
119void LogModel::setFilterText(const QString& text)
120{
121 if (_filterText != text) {
122 _pendingFilterText = text;
123 _filterTextDebounce.stop();
124 _filterText = text;
125 emit filterTextChanged();
126 if (_filterRegex) {
127 _recompileRegex();
128 }
129 _rebuildFilteredIndices();
130 }
131}
132
133void LogModel::setFilterTextDeferred(const QString& text)
134{
135 if (_pendingFilterText != text) {
136 _pendingFilterText = text;
137 _filterTextDebounce.start();
138 }
139}
140
141void LogModel::setFilterRegex(bool enabled)
142{
143 if (_filterRegex != enabled) {
144 _filterRegex = enabled;
145 emit filterRegexChanged();
146 if (enabled && !_filterText.isEmpty()) {
147 _recompileRegex();
148 } else {
150 }
151 _rebuildFilteredIndices();
152 }
153}
154
156{
157 bool changed = false;
158 _filterTextDebounce.stop();
159 _pendingFilterText.clear();
160
161 if (_filterLevel != LogEntry::Debug) {
162 _filterLevel = LogEntry::Debug;
163 emit filterLevelChanged();
164 changed = true;
165 }
166 if (!_filterCategory.isEmpty()) {
167 _filterCategory.clear();
169 changed = true;
170 }
171 if (!_filterText.isEmpty()) {
172 _filterText.clear();
173 emit filterTextChanged();
174 changed = true;
175 }
176 if (_filterRegex) {
177 _filterRegex = false;
178 emit filterRegexChanged();
179 changed = true;
180 }
181
182 if (changed) {
183 _rebuildFilteredIndices();
184 }
185}
186
187bool LogModel::_hasActiveFilter() const
188{
189 return _filterLevel > LogEntry::Debug || !_filterCategory.isEmpty() || !_filterText.isEmpty();
190}
191
192bool LogModel::_passesFilter(const LogEntry& entry) const
193{
194 if (static_cast<int>(entry.level) < _filterLevel) {
195 return false;
196 }
197 if (!_filterCategory.isEmpty() && !entry.category.contains(_filterCategory, Qt::CaseInsensitive)) {
198 return false;
199 }
200 if (!_filterText.isEmpty()) {
201 if (_filterRegex) {
202 if (!_compiledRegex.isValid() || !_compiledRegex.match(entry.message).hasMatch()) {
203 return false;
204 }
205 } else {
206 if (!entry.message.contains(_filterText, Qt::CaseInsensitive)) {
207 return false;
208 }
209 }
210 }
211 return true;
212}
213
214void LogModel::_recompileRegex()
215{
216 _compiledRegex = QRegularExpression(_filterText, QRegularExpression::CaseInsensitiveOption);
218}
219
221{
222 _pendingEntries.push_back(std::move(entry));
223 if (static_cast<int>(_pendingEntries.size()) >= kBatchMaxSize) {
224 _batchTimer.stop();
225 _flushPending();
226 } else if (!_batchTimer.isActive()) {
227 _batchTimer.start();
228 }
229}
230
231void LogModel::_flushPending()
232{
233 _batchTimer.stop();
234
235 if (_pendingEntries.empty()) {
236 return;
237 }
238
239 _trimExcess();
240
241 bool newCategories = false;
242 for (auto& entry : _pendingEntries) {
243 if (!entry.category.isEmpty() && !_categoriesSet.contains(entry.category)) {
244 _categoriesSet.insert(entry.category);
245 newCategories = true;
246 }
247 }
248
249 const int overflow = static_cast<int>(_entries.size() + _pendingEntries.size()) - _maxEntries;
250 if (overflow > 0 && overflow <= static_cast<int>(_pendingEntries.size())) {
251 _pendingEntries.erase(_pendingEntries.begin(), _pendingEntries.begin() + overflow);
252 }
253
254 const int firstNew = static_cast<int>(_entries.size());
255 for (auto& entry : _pendingEntries) {
256 _entries.push_back(std::move(entry));
257 }
258 _pendingEntries.clear();
259
260 const int lastNew = static_cast<int>(_entries.size()) - 1;
261 if (firstNew <= lastNew) {
262 _appendToFiltered(firstNew, lastNew);
263 }
264
265 emit totalCountChanged();
266
267 if (newCategories) {
268 _invalidateCategoryCache();
269 emit categoriesChanged();
270 }
271}
272
273void LogModel::_appendToFiltered(int first, int last)
274{
275 if (_filterBypassed) {
276 // Entries already in _entries; just signal the view
277 beginInsertRows(QModelIndex(), first, last);
278 endInsertRows();
279 return;
280 }
281
282 std::vector<int> newIndices;
283 for (int i = first; i <= last; ++i) {
284 if (_passesFilter(_entries[i])) {
285 newIndices.push_back(i);
286 }
287 }
288
289 if (!newIndices.empty()) {
290 const int proxyFirst = static_cast<int>(_filteredIndices.size());
291 beginInsertRows(QModelIndex(), proxyFirst, proxyFirst + static_cast<int>(newIndices.size()) - 1);
292 _filteredIndices.insert(_filteredIndices.end(), newIndices.begin(), newIndices.end());
293 endInsertRows();
294 }
295}
296
297void LogModel::_trimExcess()
298{
299 const int total = static_cast<int>(_entries.size()) + static_cast<int>(_pendingEntries.size());
300 if (total <= _maxEntries) {
301 return;
302 }
303
304 const int removeCount = total - _maxEntries;
305 const int actualRemove = qMin(removeCount, static_cast<int>(_entries.size()));
306 if (actualRemove <= 0) {
307 return;
308 }
309
310 if (_filterBypassed) {
311 beginRemoveRows(QModelIndex(), 0, actualRemove - 1);
312 _entries.erase(_entries.begin(), _entries.begin() + actualRemove);
313 endRemoveRows();
314 return;
315 }
316
317 int filteredRemoved = 0;
318 while (filteredRemoved < static_cast<int>(_filteredIndices.size()) &&
319 _filteredIndices[filteredRemoved] < actualRemove) {
320 ++filteredRemoved;
321 }
322
323 if (filteredRemoved > 0) {
324 beginRemoveRows(QModelIndex(), 0, filteredRemoved - 1);
325 _filteredIndices.erase(_filteredIndices.begin(), _filteredIndices.begin() + filteredRemoved);
326 endRemoveRows();
327 }
328
329 _entries.erase(_entries.begin(), _entries.begin() + actualRemove);
330
331 for (auto& idx : _filteredIndices) {
332 idx -= actualRemove;
333 }
334}
335
336void LogModel::_rebuildFilteredIndices()
337{
338 beginResetModel();
339 _filteredIndices.clear();
340
341 if (!_hasActiveFilter()) {
342 _filterBypassed = true;
343 } else {
344 _filterBypassed = false;
345 for (int i = 0; i < static_cast<int>(_entries.size()); ++i) {
346 if (_passesFilter(_entries[i])) {
347 _filteredIndices.push_back(i);
348 }
349 }
350 }
351
352 endResetModel();
353}
354
356{
357 _batchTimer.stop();
358 _pendingEntries.clear();
359
360 beginResetModel();
361 _entries.clear();
362 _filteredIndices.clear();
363 _filterBypassed = !_hasActiveFilter();
364 endResetModel();
365
366 emit totalCountChanged();
367 _categoriesSet.clear();
368 _invalidateCategoryCache();
369 emit categoriesChanged();
370}
371
372void LogModel::_invalidateCategoryCache()
373{
374 _categoriesDirty = true;
375}
376
377QStringList LogModel::categoriesList() const
378{
379 if (_categoriesDirty) {
380 _categoriesCache = QStringList(_categoriesSet.begin(), _categoriesSet.end());
381 _categoriesCache.sort();
382 _categoriesDirty = false;
383 }
384 return _categoriesCache;
385}
386
387
388QList<LogEntry> LogModel::filteredEntries() const
389{
390 if (_filterBypassed) {
391 return QList<LogEntry>(_entries.begin(), _entries.end());
392 }
393
394 QList<LogEntry> result;
395 result.reserve(static_cast<int>(_filteredIndices.size()));
396 for (const int idx : _filteredIndices) {
397 result.append(_entries[idx]);
398 }
399 return result;
400}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Base class for table models that display LogEntry data.
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override
void totalCountChanged()
void filterTextChanged()
const LogEntry * entryAt(int row) const override
Return entry at visible row, or nullptr if out of range.
Definition LogModel.cc:73
void enqueue(LogEntry entry)
Definition LogModel.cc:220
void categoriesChanged()
void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override
Definition LogModel.cc:51
void setFilterText(const QString &text)
Definition LogModel.cc:119
void setFilterRegex(bool enabled)
Definition LogModel.cc:141
Q_INVOKABLE void clearFilters()
Definition LogModel.cc:155
void filterCategoryChanged()
void setFilterCategory(const QString &category)
Definition LogModel.cc:110
void setMaxEntries(int max)
Definition LogModel.cc:92
void maxEntriesChanged()
Q_INVOKABLE void clear()
Definition LogModel.cc:355
void filterRegexValidChanged()
QList< LogEntry > filteredEntries() const
Definition LogModel.cc:388
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
Definition LogModel.cc:37
Q_INVOKABLE void setFilterTextDeferred(const QString &text)
Definition LogModel.cc:133
void setFilterLevel(int level)
Definition LogModel.cc:101
void filterRegexChanged()
QStringList categoriesList() const
Definition LogModel.cc:377
void filterLevelChanged()
int rowCount(const QModelIndex &parent=QModelIndex()) const override
Definition LogModel.cc:28
static SettingsManager * instance()
AppSettings * appSettings() const
QString message
Definition LogEntry.h:40
QVariant columnDisplayData(int column) const
Definition LogEntry.cc:91
QDateTime timestamp
Definition LogEntry.h:37
QVariant roleData(int role) const
Definition LogEntry.cc:64
Level level
Definition LogEntry.h:38
QString category
Definition LogEntry.h:39
@ TimestampColumn
Definition LogEntry.h:71