]> sigrok.org Git - pulseview.git/blob - pv/views/tabular_decoder/model.cpp
938e6add3a6236a6e5cd3ef29fec26ed7029ebe7
[pulseview.git] / pv / views / tabular_decoder / model.cpp
1 /*
2  * This file is part of the PulseView project.
3  *
4  * Copyright (C) 2020 Soeren Apel <soeren@apelpie.net>
5  *
6  * This program 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 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program 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 this program; if not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #include <QApplication>
21 #include <QDebug>
22 #include <QString>
23
24 #include "pv/views/tabular_decoder/view.hpp"
25
26 #include "view.hpp"
27
28 #include "pv/util.hpp"
29 #include "pv/globalsettings.hpp"
30
31 using std::make_shared;
32
33 using pv::util::Timestamp;
34 using pv::util::format_time_si;
35 using pv::util::format_time_minutes;
36 using pv::util::SIPrefix;
37
38 namespace pv {
39 namespace views {
40 namespace tabular_decoder {
41
42 AnnotationCollectionModel::AnnotationCollectionModel(QObject* parent) :
43         QAbstractTableModel(parent),
44         all_annotations_(nullptr),
45         dataset_(nullptr),
46         signal_(nullptr),
47         prev_segment_(0),
48         prev_last_row_(0),
49         start_index_(0),
50         end_index_(0),
51         had_highlight_before_(false),
52         hide_hidden_(false)
53 {
54         // TBD Maybe use empty columns as indentation levels to indicate stacked decoders
55         header_data_.emplace_back(tr("Sample"));     // Column #0
56         header_data_.emplace_back(tr("Time"));       // Column #1
57         header_data_.emplace_back(tr("Decoder"));    // Column #2
58         header_data_.emplace_back(tr("Ann Row"));    // Column #3
59         header_data_.emplace_back(tr("Ann Class"));  // Column #4
60         header_data_.emplace_back(tr("Value"));      // Column #5
61 }
62
63 int AnnotationCollectionModel::get_hierarchy_level(const Annotation* ann) const
64 {
65         int level = 0;
66
67         const unsigned int ann_stack_level = ann->row_data()->row()->decoder()->get_stack_level();
68         level = (signal_->decoder_stack().size() - 1 - ann_stack_level);
69
70         return level;
71 }
72
73 QVariant AnnotationCollectionModel::data_from_ann(const Annotation* ann, int index) const
74 {
75         switch (index) {
76         case 0: return QVariant((qulonglong)ann->start_sample());  // Column #0, Start Sample
77         case 1: {                                                  // Column #1, Start Time
78                         Timestamp t = ann->start_sample() / signal_->get_samplerate();
79                         QString unit = signal_->get_samplerate() ? tr("s") : tr("sa");
80                         QString s;
81                         if ((t < 60) || (signal_->get_samplerate() == 0))  // i.e. if unit is sa
82                                 s = format_time_si(t, SIPrefix::unspecified, 3, unit, false);
83                         else
84                                 s = format_time_minutes(t, 3, false);
85                         return QVariant(s);
86                 }
87         case 2: return QVariant(ann->row()->decoder()->name());    // Column #2, Decoder
88         case 3: return QVariant(ann->row()->description());        // Column #3, Ann Row
89         case 4: return QVariant(ann->ann_class_description());     // Column #4, Ann Class
90         case 5: return QVariant(ann->longest_annotation());        // Column #5, Value
91         default: return QVariant();
92         }
93 }
94
95 QVariant AnnotationCollectionModel::data(const QModelIndex& index, int role) const
96 {
97         if (!signal_ || !index.isValid() || !index.internalPointer())
98                 return QVariant();
99
100         const Annotation* ann =
101                 static_cast<const Annotation*>(index.internalPointer());
102
103         if ((role == Qt::DisplayRole) || (role == Qt::ToolTipRole))
104                 return data_from_ann(ann, index.column());
105
106         if (role == Qt::ForegroundRole) {
107                 if (index.column() >= get_hierarchy_level(ann)) {
108                         // Invert the text color if this cell is highlighted
109                         const bool must_highlight = (highlight_sample_num_ > 0) &&
110                                 ((int64_t)ann->start_sample() <= highlight_sample_num_) &&
111                                 ((int64_t)ann->end_sample() >= highlight_sample_num_);
112
113                         if (must_highlight) {
114                                 if (GlobalSettings::current_theme_is_dark())
115                                         return QApplication::palette().brush(QPalette::Window);
116                                 else
117                                         return QApplication::palette().brush(QPalette::WindowText);
118                         }
119                 }
120
121                 return QApplication::palette().brush(QPalette::WindowText);
122         }
123
124         if (role == Qt::BackgroundRole) {
125                 // Only use custom cell background color if column index reached the hierarchy level
126                 if (index.column() >= get_hierarchy_level(ann)) {
127
128                         QColor color;
129                         const bool must_highlight = (highlight_sample_num_ > 0) &&
130                                 ((int64_t)ann->start_sample() <= highlight_sample_num_) &&
131                                 ((int64_t)ann->end_sample() >= highlight_sample_num_);
132
133                         if (must_highlight)
134                                 color = ann->color();
135                         else
136                                 color = GlobalSettings::current_theme_is_dark() ?
137                                         ann->dark_color() : ann->bright_color();
138
139                         return QBrush(color);
140                 }
141         }
142
143         return QVariant();
144 }
145
146 Qt::ItemFlags AnnotationCollectionModel::flags(const QModelIndex& index) const
147 {
148         if (!index.isValid())
149                 return Qt::NoItemFlags;
150
151         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren;
152 }
153
154 QVariant AnnotationCollectionModel::headerData(int section, Qt::Orientation orientation,
155         int role) const
156 {
157         if ((orientation == Qt::Horizontal) && (role == Qt::DisplayRole))
158                 return header_data_.at(section);
159
160         return QVariant();
161 }
162
163 QModelIndex AnnotationCollectionModel::index(int row, int column,
164         const QModelIndex& parent_idx) const
165 {
166         (void)parent_idx;
167         assert(column >= 0);
168
169         if (!dataset_ || (row < 0))
170                 return QModelIndex();
171
172         QModelIndex idx;
173
174         if (start_index_ == end_index_) {
175                 if ((size_t)row < dataset_->size())
176                         idx = createIndex(row, column, (void*)dataset_->at(row));
177         } else {
178                 if ((size_t)row < (end_index_ - start_index_))
179                         idx = createIndex(row, column, (void*)dataset_->at(start_index_ + row));
180         }
181
182         return idx;
183 }
184
185 QModelIndex AnnotationCollectionModel::parent(const QModelIndex& index) const
186 {
187         (void)index;
188
189         return QModelIndex();
190 }
191
192 int AnnotationCollectionModel::rowCount(const QModelIndex& parent_idx) const
193 {
194         (void)parent_idx;
195
196         if (!dataset_)
197                 return 0;
198
199         if (start_index_ == end_index_)
200                 return dataset_->size();
201         else
202                 return (end_index_ - start_index_);
203 }
204
205 int AnnotationCollectionModel::columnCount(const QModelIndex& parent_idx) const
206 {
207         (void)parent_idx;
208
209         return header_data_.size();
210 }
211
212 void AnnotationCollectionModel::set_signal_and_segment(data::DecodeSignal* signal, uint32_t current_segment)
213 {
214         if (!signal) {
215                 all_annotations_ = nullptr;
216                 dataset_ = nullptr;
217                 signal_ = nullptr;
218
219                 dataChanged(QModelIndex(), QModelIndex());
220                 layoutChanged();
221                 return;
222         }
223
224         disconnect(this, SLOT(on_annotation_visibility_changed()));
225
226         all_annotations_ = signal->get_all_annotations_by_segment(current_segment);
227         signal_ = signal;
228
229         for (const shared_ptr<Decoder>& dec : signal_->decoder_stack())
230                 connect(dec.get(), SIGNAL(annotation_visibility_changed()),
231                         this, SLOT(on_annotation_visibility_changed()));
232
233         if (hide_hidden_)
234                 update_annotations_without_hidden();
235         else
236                 dataset_ = all_annotations_;
237
238         if (!dataset_ || dataset_->empty()) {
239                 prev_segment_ = current_segment;
240                 return;
241         }
242
243         // Re-apply the requested sample range
244         set_sample_range(start_sample_, end_sample_);
245
246         const size_t new_row_count = dataset_->size() - 1;
247
248         // Force the view associated with this model to update when the segment changes
249         if (prev_segment_ != current_segment) {
250                 dataChanged(QModelIndex(), QModelIndex());
251                 layoutChanged();
252         } else {
253                 // Force the view associated with this model to update when we have more annotations
254                 if (prev_last_row_ < new_row_count) {
255                         dataChanged(index(prev_last_row_, 0), index(new_row_count, 0));
256                         layoutChanged();
257                 }
258         }
259
260         prev_segment_ = current_segment;
261         prev_last_row_ = new_row_count;
262 }
263
264 void AnnotationCollectionModel::set_sample_range(uint64_t start_sample, uint64_t end_sample)
265 {
266         // Check if there's even anything to reset
267         if ((start_sample == end_sample) && (start_index_ == end_index_))
268                 return;
269
270         if (!dataset_ || dataset_->empty() || (end_sample == 0)) {
271                 start_index_ = 0;
272                 end_index_ = 0;
273                 start_sample_ = 0;
274                 end_sample_ = 0;
275
276                 dataChanged(QModelIndex(), QModelIndex());
277                 layoutChanged();
278                 return;
279         }
280
281         start_sample_ = start_sample;
282         end_sample_ = end_sample;
283
284         // Determine first and last indices into the annotation list
285         int64_t i = -1;
286         bool ann_outside_range;
287         do {
288                 i++;
289
290                 if (i == (int64_t)dataset_->size()) {
291                         start_index_ = 0;
292                         end_index_ = 0;
293
294                         dataChanged(QModelIndex(), QModelIndex());
295                         layoutChanged();
296                         return;
297                 }
298                 const Annotation* ann = (*dataset_)[i];
299                 ann_outside_range =
300                         ((ann->start_sample() < start_sample) && (ann->end_sample() < start_sample));
301         } while (ann_outside_range);
302         start_index_ = i;
303
304         // Ideally, we would be able to set end_index_ to the last annotation that
305         // is within range. However, as annotations in the list are sorted by
306         // start sample and hierarchy level, we may encounter this scenario:
307         //   [long annotation that spans across view]
308         //   [short annotations that aren't seen]
309         //   [short annotations that are seen]
310         // ..in which our output would only show the first long annotations.
311         // For this reason, we simply show everything after the first visible
312         // annotation for now.
313
314         end_index_ = dataset_->size();
315
316         dataChanged(index(0, 0), index((end_index_ - start_index_), 0));
317         layoutChanged();
318 }
319
320 void AnnotationCollectionModel::set_hide_hidden(bool hide_hidden)
321 {
322         hide_hidden_ = hide_hidden;
323
324         if (hide_hidden_) {
325                 dataset_ = &all_annotations_without_hidden_;
326                 update_annotations_without_hidden();
327         } else {
328                 dataset_ = all_annotations_;
329                 all_annotations_without_hidden_.clear();  // To conserve memory
330         }
331
332         // Re-apply the requested sample range
333         set_sample_range(start_sample_, end_sample_);
334
335         if (dataset_)
336                 dataChanged(index(0, 0), index(dataset_->size(), 0));
337         else
338                 dataChanged(QModelIndex(), QModelIndex());
339
340         layoutChanged();
341 }
342
343 void AnnotationCollectionModel::update_annotations_without_hidden()
344 {
345         uint64_t count = 0;
346
347         if (!all_annotations_ || all_annotations_->empty()) {
348                 all_annotations_without_hidden_.clear();
349                 return;
350         }
351
352         for (const Annotation* ann : *all_annotations_) {
353                 if (!ann->visible())
354                         continue;
355
356                 if (all_annotations_without_hidden_.size() < (count + 100))
357                         all_annotations_without_hidden_.resize(count + 100);
358
359                 all_annotations_without_hidden_[count++] = ann;
360         }
361
362         all_annotations_without_hidden_.resize(count);
363 }
364
365 void AnnotationCollectionModel::update_highlighted_rows(QModelIndex first,
366         QModelIndex last, int64_t sample_num)
367 {
368         bool has_highlight = false;
369
370         highlight_sample_num_ = sample_num;
371
372         if (!dataset_ || dataset_->empty())
373                 return;
374
375         if (sample_num >= 0) {
376                 last = last.sibling(last.row() + 1, 0);
377
378                 // Check if there are any annotations visible in the table view that
379                 // we would need to highlight - only then do we do so
380                 QModelIndex index = first;
381                 do {
382                         const Annotation* ann =
383                                 static_cast<const Annotation*>(index.internalPointer());
384                         assert(ann);
385
386                         if (((int64_t)ann->start_sample() <= sample_num) &&
387                                 ((int64_t)ann->end_sample() >= sample_num)) {
388                                 has_highlight = true;
389                                 break;
390                         }
391
392                         index = index.sibling(index.row() + 1, 0);
393                 } while (index != last);
394         }
395
396         if (has_highlight || had_highlight_before_)
397                 dataChanged(first, last);
398
399         had_highlight_before_ = has_highlight;
400 }
401
402 void AnnotationCollectionModel::on_annotation_visibility_changed()
403 {
404         if (!hide_hidden_)
405                 return;
406
407         update_annotations_without_hidden();
408
409         // Re-apply the requested sample range
410         set_sample_range(start_sample_, end_sample_);
411
412         if (dataset_)
413                 dataChanged(index(0, 0), index(dataset_->size(), 0));
414         else
415                 dataChanged(QModelIndex(), QModelIndex());
416
417         layoutChanged();
418 }
419
420 } // namespace tabular_decoder
421 } // namespace views
422 } // namespace pv