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