]> sigrok.org Git - pulseview.git/blob - pv/views/tabular_decoder/view.cpp
TabularDecModel: Fix multi-row selection issue
[pulseview.git] / pv / views / tabular_decoder / view.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 <climits>
21
22 #include <QApplication>
23 #include <QDebug>
24 #include <QFileDialog>
25 #include <QFontMetrics>
26 #include <QHeaderView>
27 #include <QLabel>
28 #include <QMenu>
29 #include <QMessageBox>
30 #include <QToolBar>
31 #include <QVBoxLayout>
32
33 #include <libsigrokdecode/libsigrokdecode.h>
34
35 #include "view.hpp"
36
37 #include "pv/globalsettings.hpp"
38 #include "pv/session.hpp"
39 #include "pv/util.hpp"
40 #include "pv/data/decode/decoder.hpp"
41
42 using pv::data::DecodeSignal;
43 using pv::data::SignalBase;
44 using pv::data::decode::Decoder;
45 using pv::util::Timestamp;
46
47 using std::make_shared;
48 using std::shared_ptr;
49
50 namespace pv {
51 namespace views {
52 namespace tabular_decoder {
53
54 const char* SaveTypeNames[SaveTypeCount] = {
55         "CSV, commas escaped",
56         "CSV, fields quoted"
57 };
58
59 QSize QCustomTableView::minimumSizeHint() const
60 {
61         QSize size(QTableView::sizeHint());
62
63         int width = 0;
64         for (int i = 0; i < horizontalHeader()->count(); i++)
65                 if (!horizontalHeader()->isSectionHidden(i))
66                         width += horizontalHeader()->sectionSize(i);
67
68         size.setWidth(width + (horizontalHeader()->count() * 1));
69
70         return size;
71 }
72
73 QSize QCustomTableView::sizeHint() const
74 {
75         return minimumSizeHint();
76 }
77
78
79 View::View(Session &session, bool is_main_view, QMainWindow *parent) :
80         ViewBase(session, is_main_view, parent),
81
82         // Note: Place defaults in View::reset_view_state(), not here
83         parent_(parent),
84         decoder_selector_(new QComboBox()),
85         save_button_(new QToolButton()),
86         save_action_(new QAction(this)),
87         table_view_(new QCustomTableView()),
88         model_(new AnnotationCollectionModel()),
89         signal_(nullptr)
90 {
91         QVBoxLayout *root_layout = new QVBoxLayout(this);
92         root_layout->setContentsMargins(0, 0, 0, 0);
93         root_layout->addWidget(table_view_);
94
95         // Create toolbar
96         QToolBar* toolbar = new QToolBar();
97         toolbar->setContextMenuPolicy(Qt::PreventContextMenu);
98         parent->addToolBar(toolbar);
99
100         // Populate toolbar
101         toolbar->addWidget(new QLabel(tr("Decoder:")));
102         toolbar->addWidget(decoder_selector_);
103         toolbar->addSeparator();
104         toolbar->addWidget(save_button_);
105
106         connect(decoder_selector_, SIGNAL(currentIndexChanged(int)),
107                 this, SLOT(on_selected_decoder_changed(int)));
108
109         // Configure widgets
110         decoder_selector_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
111
112         // Configure actions
113         save_action_->setText(tr("&Save..."));
114         save_action_->setIcon(QIcon::fromTheme("document-save-as",
115                 QIcon(":/icons/document-save-as.png")));
116         save_action_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_S));
117         connect(save_action_, SIGNAL(triggered(bool)),
118                 this, SLOT(on_actionSave_triggered()));
119
120         QMenu *save_menu = new QMenu();
121         connect(save_menu, SIGNAL(triggered(QAction*)),
122                 this, SLOT(on_actionSave_triggered(QAction*)));
123
124         for (int i = 0; i < SaveTypeCount; i++) {
125                 QAction *const action = save_menu->addAction(tr(SaveTypeNames[i]));
126                 action->setData(qVariantFromValue(i));
127         }
128
129         save_button_->setMenu(save_menu);
130         save_button_->setDefaultAction(save_action_);
131         save_button_->setPopupMode(QToolButton::MenuButtonPopup);
132
133         // Set up the table view
134         table_view_->setModel(model_);
135         table_view_->setSelectionBehavior(QAbstractItemView::SelectRows);
136         table_view_->setSelectionMode(QAbstractItemView::ContiguousSelection);
137         table_view_->setSortingEnabled(true);
138         table_view_->sortByColumn(0, Qt::AscendingOrder);
139
140         const int font_height = QFontMetrics(QApplication::font()).height();
141         table_view_->verticalHeader()->setDefaultSectionSize((font_height * 5) / 4);
142
143         table_view_->horizontalHeader()->setStretchLastSection(true);
144         table_view_->horizontalHeader()->setCascadingSectionResizes(true);
145         table_view_->horizontalHeader()->setSectionsMovable(true);
146         table_view_->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
147
148         table_view_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
149         parent->setSizePolicy(table_view_->sizePolicy());
150
151         connect(table_view_, SIGNAL(clicked(const QModelIndex&)),
152                 this, SLOT(on_table_item_clicked(const QModelIndex&)));
153         connect(table_view_, SIGNAL(doubleClicked(const QModelIndex&)),
154                 this, SLOT(on_table_item_double_clicked(const QModelIndex&)));
155         connect(table_view_->horizontalHeader(), SIGNAL(customContextMenuRequested(const QPoint&)),
156                 this, SLOT(on_table_header_requested(const QPoint&)));
157
158         reset_view_state();
159 }
160
161 ViewType View::get_type() const
162 {
163         return ViewTypeTabularDecoder;
164 }
165
166 void View::reset_view_state()
167 {
168         ViewBase::reset_view_state();
169
170         decoder_selector_->clear();
171 }
172
173 void View::clear_decode_signals()
174 {
175         ViewBase::clear_decode_signals();
176
177         reset_data();
178         reset_view_state();
179 }
180
181 void View::add_decode_signal(shared_ptr<data::DecodeSignal> signal)
182 {
183         ViewBase::add_decode_signal(signal);
184
185         connect(signal.get(), SIGNAL(name_changed(const QString&)),
186                 this, SLOT(on_signal_name_changed(const QString&)));
187
188         // Note: At time of initial creation, decode signals have no decoders so we
189         // need to watch for decoder stacking events
190
191         connect(signal.get(), SIGNAL(decoder_stacked(void*)),
192                 this, SLOT(on_decoder_stacked(void*)));
193         connect(signal.get(), SIGNAL(decoder_removed(void*)),
194                 this, SLOT(on_decoder_removed(void*)));
195
196         // Add the top-level decoder provided by an already-existing signal
197         auto stack = signal->decoder_stack();
198         if (!stack.empty()) {
199                 shared_ptr<Decoder>& dec = stack.at(0);
200                 decoder_selector_->addItem(signal->name(), QVariant::fromValue((void*)dec.get()));
201         }
202 }
203
204 void View::remove_decode_signal(shared_ptr<data::DecodeSignal> signal)
205 {
206         // Remove all decoders provided by this signal
207         for (const shared_ptr<Decoder>& dec : signal->decoder_stack()) {
208                 int index = decoder_selector_->findData(QVariant::fromValue((void*)dec.get()));
209
210                 if (index != -1)
211                         decoder_selector_->removeItem(index);
212         }
213
214         ViewBase::remove_decode_signal(signal);
215
216         if (signal.get() == signal_) {
217                 reset_data();
218                 update_data();
219                 reset_view_state();
220         }
221 }
222
223 void View::save_settings(QSettings &settings) const
224 {
225         ViewBase::save_settings(settings);
226 }
227
228 void View::restore_settings(QSettings &settings)
229 {
230         // Note: It is assumed that this function is only called once,
231         // immediately after restoring a previous session.
232         ViewBase::restore_settings(settings);
233 }
234
235 void View::reset_data()
236 {
237         signal_ = nullptr;
238         decoder_ = nullptr;
239 }
240
241 void View::update_data()
242 {
243         model_->set_signal_and_segment(signal_, current_segment_);
244 }
245
246 void View::save_data_as_csv(unsigned int save_type) const
247 {
248         // Note: We try to follow RFC 4180 (https://tools.ietf.org/html/rfc4180)
249
250         assert(decoder_);
251         assert(signal_);
252
253         if (!signal_)
254                 return;
255
256         const bool save_all = !table_view_->selectionModel()->hasSelection();
257
258         GlobalSettings settings;
259         const QString dir = settings.value("MainWindow/SaveDirectory").toString();
260
261         const QString file_name = QFileDialog::getSaveFileName(
262                 parent_, tr("Save Annotations as CSV"), dir, tr("CSV Files (*.csv);;Text Files (*.txt);;All Files (*)"));
263
264         if (file_name.isEmpty())
265                 return;
266
267         QFile file(file_name);
268         if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
269                 QTextStream out_stream(&file);
270
271                 if (save_all)
272                         table_view_->selectAll();
273
274                 // Write out header columns in visual order, not logical order
275                 for (int i = 0; i < table_view_->horizontalHeader()->count(); i++) {
276                         int column = table_view_->horizontalHeader()->logicalIndex(i);
277
278                         if (table_view_->horizontalHeader()->isSectionHidden(column))
279                                 continue;
280
281                         const QString title = model_->headerData(column, Qt::Horizontal, Qt::DisplayRole).toString();
282
283                         if (save_type == SaveTypeCSVEscaped)
284                                 out_stream << title;
285                         else
286                                 out_stream << '"' << title << '"';
287
288                         if (i < (table_view_->horizontalHeader()->count() - 1))
289                                 out_stream << ",";
290                 }
291                 out_stream << '\r' << '\n';
292
293
294                 QModelIndexList selected_rows = table_view_->selectionModel()->selectedRows();
295
296                 for (int i = 0; i < selected_rows.size(); i++) {
297                         const int row = selected_rows.at(i).row();
298
299                         // Write out columns in visual order, not logical order
300                         for (int c = 0; c < table_view_->horizontalHeader()->count(); c++) {
301                                 const int column = table_view_->horizontalHeader()->logicalIndex(c);
302
303                                 if (table_view_->horizontalHeader()->isSectionHidden(column))
304                                         continue;
305
306                                 const QModelIndex idx = model_->index(row, column, QModelIndex());
307                                 QString s = model_->data(idx, Qt::DisplayRole).toString();
308
309                                 if (save_type == SaveTypeCSVEscaped)
310                                         out_stream << s.replace(",", "\\,");
311                                 else
312                                         out_stream << '"' << s.replace("\"", "\"\"") << '"';
313
314                                 if (c < (table_view_->horizontalHeader()->count() - 1))
315                                         out_stream << ",";
316                         }
317
318                         out_stream << '\r' << '\n';
319                 }
320
321                 if (out_stream.status() == QTextStream::Ok) {
322                         if (save_all)
323                                 table_view_->clearSelection();
324
325                         return;
326                 }
327         }
328
329         QMessageBox msg(parent_);
330         msg.setText(tr("Error") + "\n\n" + tr("File %1 could not be written to.").arg(file_name));
331         msg.setStandardButtons(QMessageBox::Ok);
332         msg.setIcon(QMessageBox::Warning);
333         msg.exec();
334 }
335
336 void View::on_selected_decoder_changed(int index)
337 {
338         if (signal_) {
339                 disconnect(signal_, SIGNAL(color_changed(QColor)));
340                 disconnect(signal_, SIGNAL(new_annotations()));
341                 disconnect(signal_, SIGNAL(decode_reset()));
342         }
343
344         reset_data();
345
346         decoder_ = (Decoder*)decoder_selector_->itemData(index).value<void*>();
347
348         // Find the signal that contains the selected decoder
349         for (const shared_ptr<DecodeSignal>& ds : decode_signals_)
350                 for (const shared_ptr<Decoder>& dec : ds->decoder_stack())
351                         if (decoder_ == dec.get())
352                                 signal_ = ds.get();
353
354         if (signal_) {
355                 connect(signal_, SIGNAL(color_changed(QColor)), this, SLOT(on_signal_color_changed(QColor)));
356                 connect(signal_, SIGNAL(new_annotations()), this, SLOT(on_new_annotations()));
357                 connect(signal_, SIGNAL(decode_reset()), this, SLOT(on_decoder_reset()));
358         }
359
360         update_data();
361 }
362
363 void View::on_signal_name_changed(const QString &name)
364 {
365         (void)name;
366
367         SignalBase* sb = qobject_cast<SignalBase*>(QObject::sender());
368         assert(sb);
369
370         DecodeSignal* signal = dynamic_cast<DecodeSignal*>(sb);
371         assert(signal);
372
373         // Update the top-level decoder provided by this signal
374         auto stack = signal->decoder_stack();
375         if (!stack.empty()) {
376                 shared_ptr<Decoder>& dec = stack.at(0);
377                 int index = decoder_selector_->findData(QVariant::fromValue((void*)dec.get()));
378
379                 if (index != -1)
380                         decoder_selector_->setItemText(index, signal->name());
381         }
382 }
383
384 void View::on_signal_color_changed(const QColor &color)
385 {
386         (void)color;
387
388         table_view_->update();
389 }
390
391 void View::on_new_annotations()
392 {
393         if (!delayed_view_updater_.isActive())
394                 delayed_view_updater_.start();
395 }
396
397 void View::on_decoder_reset()
398 {
399         // Invalidate the model's data connection immediately - otherwise we
400         // will use a stale pointer in model_->index() when called from the table view
401         model_->set_signal_and_segment(signal_, current_segment_);
402 }
403
404 void View::on_decoder_stacked(void* decoder)
405 {
406         Decoder* d = static_cast<Decoder*>(decoder);
407
408         // Find the signal that contains the selected decoder
409         DecodeSignal* signal = nullptr;
410
411         for (const shared_ptr<DecodeSignal>& ds : decode_signals_)
412                 for (const shared_ptr<Decoder>& dec : ds->decoder_stack())
413                         if (d == dec.get())
414                                 signal = ds.get();
415
416         assert(signal);
417
418         const shared_ptr<Decoder>& dec = signal->decoder_stack().at(0);
419         int index = decoder_selector_->findData(QVariant::fromValue((void*)dec.get()));
420
421         if (index == -1) {
422                 // Add the decoder to the list
423                 decoder_selector_->addItem(signal->name(), QVariant::fromValue((void*)d));
424         }
425 }
426
427 void View::on_decoder_removed(void* decoder)
428 {
429         Decoder* d = static_cast<Decoder*>(decoder);
430
431         // Remove the decoder from the list
432         int index = decoder_selector_->findData(QVariant::fromValue((void*)d));
433
434         if (index != -1)
435                 decoder_selector_->removeItem(index);
436 }
437
438 void View::on_actionSave_triggered(QAction* action)
439 {
440         int save_type = SaveTypeCSVQuoted;
441
442         if (action)
443                 save_type = action->data().toInt();
444
445         save_data_as_csv(save_type);
446 }
447
448 void View::on_table_item_clicked(const QModelIndex& index)
449 {
450         (void)index;
451
452         // Force repaint, otherwise the new selection isn't shown for some reason
453         table_view_->viewport()->update();
454 }
455
456 void View::on_table_item_double_clicked(const QModelIndex& index)
457 {
458         const Annotation* ann = static_cast<const Annotation*>(index.internalPointer());
459
460         shared_ptr<views::ViewBase> main_view = session_.main_view();
461
462         main_view->focus_on_range(ann->start_sample(), ann->end_sample());
463 }
464
465 void View::on_table_header_requested(const QPoint& pos)
466 {
467         QMenu* menu = new QMenu(this);
468
469         for (int i = 0; i < table_view_->horizontalHeader()->count(); i++) {
470                 int column = table_view_->horizontalHeader()->logicalIndex(i);
471
472                 const QString title = model_->headerData(column, Qt::Horizontal, Qt::DisplayRole).toString();
473                 QAction* action = new QAction(title, this);
474
475                 action->setCheckable(true);
476                 action->setChecked(!table_view_->horizontalHeader()->isSectionHidden(column));
477                 action->setData(column);
478
479                 connect(action, SIGNAL(toggled(bool)), this, SLOT(on_table_header_toggled(bool)));
480
481                 menu->addAction(action);
482         }
483
484         menu->popup(table_view_->horizontalHeader()->viewport()->mapToGlobal(pos));
485 }
486
487 void View::on_table_header_toggled(bool checked)
488 {
489         QAction* action = qobject_cast<QAction*>(QObject::sender());
490         assert(action);
491
492         const int column = action->data().toInt();
493
494         table_view_->horizontalHeader()->setSectionHidden(column, !checked);
495 }
496
497 void View::perform_delayed_view_update()
498 {
499         update_data();
500 }
501
502
503 } // namespace tabular_decoder
504 } // namespace views
505 } // namespace pv