1 /*
2  * This file is part of the PulseView project.
3  *
4  * Copyright (C) 2019 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 <QByteArray>
23 #include <QDebug>
24 #include <QFileDialog>
25 #include <QLabel>
26 #include <QMenu>
27 #include <QMessageBox>
28 #include <QToolBar>
29 #include <QVBoxLayout>
30 
31 #include <libsigrokdecode/libsigrokdecode.h>
32 
33 #include "view.hpp"
34 #include "QHexView.hpp"
35 
36 #include "pv/globalsettings.hpp"
37 #include "pv/session.hpp"
38 #include "pv/util.hpp"
39 #include "pv/data/decode/decoder.hpp"
40 
41 using pv::data::DecodeSignal;
42 using pv::data::SignalBase;
43 using pv::data::decode::Decoder;
44 using pv::util::Timestamp;
45 
46 using std::shared_ptr;
47 
48 namespace pv {
49 namespace views {
50 namespace decoder_binary {
51 
52 const char* SaveTypeNames[SaveTypeCount] = {
53 	"Binary",
54 	"Hex Dump, plain",
55 	"Hex Dump, with offset",
56 	"Hex Dump, canonical"
57 };
58 
59 
View(Session & session,bool is_main_view,QMainWindow * parent)60 View::View(Session &session, bool is_main_view, QMainWindow *parent) :
61 	ViewBase(session, is_main_view, parent),
62 
63 	// Note: Place defaults in View::reset_view_state(), not here
64 	parent_(parent),
65 	decoder_selector_(new QComboBox()),
66 	format_selector_(new QComboBox()),
67 	class_selector_(new QComboBox()),
68 	stacked_widget_(new QStackedWidget()),
69 	hex_view_(new QHexView()),
70 	save_button_(new QToolButton()),
71 	save_action_(new QAction(this)),
72 	signal_(nullptr)
73 {
74 	QVBoxLayout *root_layout = new QVBoxLayout(this);
75 	root_layout->setContentsMargins(0, 0, 0, 0);
76 
77 	// Create toolbar
78 	QToolBar* toolbar = new QToolBar();
79 	toolbar->setContextMenuPolicy(Qt::PreventContextMenu);
80 	parent->addToolBar(toolbar);
81 
82 	// Populate toolbar
83 	toolbar->addWidget(new QLabel(tr("Decoder:")));
84 	toolbar->addWidget(decoder_selector_);
85 	toolbar->addWidget(class_selector_);
86 	toolbar->addSeparator();
87 	toolbar->addWidget(new QLabel(tr("Show data as")));
88 	toolbar->addWidget(format_selector_);
89 	toolbar->addSeparator();
90 	toolbar->addWidget(save_button_);
91 
92 	// Add format types
93 	format_selector_->addItem(tr("Hexdump"), qVariantFromValue(QString("text/hexdump")));
94 
95 	// Add widget stack
96 	root_layout->addWidget(stacked_widget_);
97 	stacked_widget_->addWidget(hex_view_);
98 	stacked_widget_->setCurrentIndex(0);
99 
100 	connect(decoder_selector_, SIGNAL(currentIndexChanged(int)),
101 		this, SLOT(on_selected_decoder_changed(int)));
102 	connect(class_selector_, SIGNAL(currentIndexChanged(int)),
103 		this, SLOT(on_selected_class_changed(int)));
104 
105 	// Configure widgets
106 	decoder_selector_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
107 	class_selector_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
108 
109 	// Configure actions
110 	save_action_->setText(tr("&Save..."));
111 	save_action_->setIcon(QIcon::fromTheme("document-save-as",
112 		QIcon(":/icons/document-save-as.png")));
113 	save_action_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_S));
114 	connect(save_action_, SIGNAL(triggered(bool)),
115 		this, SLOT(on_actionSave_triggered()));
116 
117 	QMenu *save_menu = new QMenu();
118 	connect(save_menu, SIGNAL(triggered(QAction*)),
119 		this, SLOT(on_actionSave_triggered(QAction*)));
120 
121 	for (int i = 0; i < SaveTypeCount; i++) {
122 		QAction *const action =	save_menu->addAction(tr(SaveTypeNames[i]));
123 		action->setData(qVariantFromValue(i));
124 	}
125 
126 	save_button_->setMenu(save_menu);
127 	save_button_->setDefaultAction(save_action_);
128 	save_button_->setPopupMode(QToolButton::MenuButtonPopup);
129 
130 	parent->setSizePolicy(hex_view_->sizePolicy()); // TODO Must be updated when selected widget changes
131 
132 	reset_view_state();
133 }
134 
get_type() const135 ViewType View::get_type() const
136 {
137 	return ViewTypeDecoderBinary;
138 }
139 
reset_view_state()140 void View::reset_view_state()
141 {
142 	ViewBase::reset_view_state();
143 
144 	decoder_selector_->clear();
145 	class_selector_->clear();
146 	format_selector_->setCurrentIndex(0);
147 	save_button_->setEnabled(false);
148 
149 	hex_view_->clear();
150 }
151 
clear_decode_signals()152 void View::clear_decode_signals()
153 {
154 	ViewBase::clear_decode_signals();
155 
156 	reset_data();
157 	reset_view_state();
158 }
159 
add_decode_signal(shared_ptr<data::DecodeSignal> signal)160 void View::add_decode_signal(shared_ptr<data::DecodeSignal> signal)
161 {
162 	ViewBase::add_decode_signal(signal);
163 
164 	connect(signal.get(), SIGNAL(name_changed(const QString&)),
165 		this, SLOT(on_signal_name_changed(const QString&)));
166 	connect(signal.get(), SIGNAL(decoder_stacked(void*)),
167 		this, SLOT(on_decoder_stacked(void*)));
168 	connect(signal.get(), SIGNAL(decoder_removed(void*)),
169 		this, SLOT(on_decoder_removed(void*)));
170 
171 	// Add all decoders provided by this signal
172 	auto stack = signal->decoder_stack();
173 	if (stack.size() > 1) {
174 		for (const shared_ptr<Decoder>& dec : stack)
175 			// Only add the decoder if it has binary output
176 			if (dec->get_binary_class_count() > 0) {
177 				QString title = QString("%1 (%2)").arg(signal->name(), dec->name());
178 				decoder_selector_->addItem(title, QVariant::fromValue((void*)dec.get()));
179 			}
180 	} else
181 		if (!stack.empty()) {
182 			shared_ptr<Decoder>& dec = stack.at(0);
183 			if (dec->get_binary_class_count() > 0)
184 				decoder_selector_->addItem(signal->name(), QVariant::fromValue((void*)dec.get()));
185 		}
186 }
187 
remove_decode_signal(shared_ptr<data::DecodeSignal> signal)188 void View::remove_decode_signal(shared_ptr<data::DecodeSignal> signal)
189 {
190 	// Remove all decoders provided by this signal
191 	for (const shared_ptr<Decoder>& dec : signal->decoder_stack()) {
192 		int index = decoder_selector_->findData(QVariant::fromValue((void*)dec.get()));
193 
194 		if (index != -1)
195 			decoder_selector_->removeItem(index);
196 	}
197 
198 	ViewBase::remove_decode_signal(signal);
199 
200 	if (signal.get() == signal_) {
201 		reset_data();
202 		update_data();
203 		reset_view_state();
204 	}
205 }
206 
save_settings(QSettings & settings) const207 void View::save_settings(QSettings &settings) const
208 {
209 	(void)settings;
210 }
211 
restore_settings(QSettings & settings)212 void View::restore_settings(QSettings &settings)
213 {
214 	// Note: It is assumed that this function is only called once,
215 	// immediately after restoring a previous session.
216 	(void)settings;
217 }
218 
reset_data()219 void View::reset_data()
220 {
221 	signal_ = nullptr;
222 	decoder_ = nullptr;
223 	bin_class_id_ = 0;
224 	binary_data_exists_ = false;
225 
226 	hex_view_->clear();
227 }
228 
update_data()229 void View::update_data()
230 {
231 	if (!signal_)
232 		return;
233 
234 	const DecodeBinaryClass* bin_class =
235 		signal_->get_binary_data_class(current_segment_, decoder_, bin_class_id_);
236 
237 	hex_view_->set_data(bin_class);
238 
239 	if (!binary_data_exists_)
240 		return;
241 
242 	if (!save_button_->isEnabled())
243 		save_button_->setEnabled(true);
244 }
245 
save_data() const246 void View::save_data() const
247 {
248 	assert(decoder_);
249 	assert(signal_);
250 
251 	if (!signal_)
252 		return;
253 
254 	GlobalSettings settings;
255 	const QString dir = settings.value("MainWindow/SaveDirectory").toString();
256 
257 	const QString file_name = QFileDialog::getSaveFileName(
258 		parent_, tr("Save Binary Data"), dir, tr("Binary Data Files (*.bin);;All Files (*)"));
259 
260 	if (file_name.isEmpty())
261 		return;
262 
263 	QFile file(file_name);
264 	if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
265 		pair<size_t, size_t> selection = hex_view_->get_selection();
266 
267 		vector<uint8_t> data;
268 		data.resize(selection.second - selection.first + 1);
269 
270 		signal_->get_merged_binary_data_chunks_by_offset(current_segment_, decoder_,
271 			bin_class_id_, selection.first, selection.second, &data);
272 
273 		int64_t bytes_written = file.write((const char*)data.data(), data.size());
274 
275 		if ((bytes_written == -1) || ((uint64_t)bytes_written != data.size())) {
276 			QMessageBox msg(parent_);
277 			msg.setText(tr("Error") + "\n\n" + tr("File %1 could not be written to.").arg(file_name));
278 			msg.setStandardButtons(QMessageBox::Ok);
279 			msg.setIcon(QMessageBox::Warning);
280 			msg.exec();
281 			return;
282 		}
283 	}
284 }
285 
save_data_as_hex_dump(bool with_offset,bool with_ascii) const286 void View::save_data_as_hex_dump(bool with_offset, bool with_ascii) const
287 {
288 	assert(decoder_);
289 	assert(signal_);
290 
291 	if (!signal_)
292 		return;
293 
294 	GlobalSettings settings;
295 	const QString dir = settings.value("MainWindow/SaveDirectory").toString();
296 
297 	const QString file_name = QFileDialog::getSaveFileName(
298 		parent_, tr("Save Binary Data"), dir, tr("Hex Dumps (*.txt);;All Files (*)"));
299 
300 	if (file_name.isEmpty())
301 		return;
302 
303 	QFile file(file_name);
304 	if (file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
305 		pair<size_t, size_t> selection = hex_view_->get_selection();
306 
307 		vector<uint8_t> data;
308 		data.resize(selection.second - selection.first + 1);
309 
310 		signal_->get_merged_binary_data_chunks_by_offset(current_segment_, decoder_,
311 			bin_class_id_, selection.first, selection.second, &data);
312 
313 		QTextStream out_stream(&file);
314 
315 		uint64_t offset = selection.first;
316 		uint64_t n = hex_view_->get_bytes_per_line();
317 		QString s;
318 
319 		while (offset < selection.second) {
320 			size_t end = std::min((uint64_t)(selection.second), offset + n);
321 			offset = hex_view_->create_hex_line(offset, end, &s, with_offset, with_ascii);
322 			out_stream << s << endl;
323 		}
324 
325 		out_stream << endl;
326 
327 		if (out_stream.status() != QTextStream::Ok) {
328 			QMessageBox msg(parent_);
329 			msg.setText(tr("Error") + "\n\n" + tr("File %1 could not be written to.").arg(file_name));
330 			msg.setStandardButtons(QMessageBox::Ok);
331 			msg.setIcon(QMessageBox::Warning);
332 			msg.exec();
333 			return;
334 		}
335 	}
336 }
337 
on_selected_decoder_changed(int index)338 void View::on_selected_decoder_changed(int index)
339 {
340 	if (signal_)
341 		disconnect(signal_, SIGNAL(new_binary_data(unsigned int, void*, unsigned int)));
342 
343 	reset_data();
344 
345 	decoder_ = (Decoder*)decoder_selector_->itemData(index).value<void*>();
346 
347 	// Find the signal that contains the selected decoder
348 	for (const shared_ptr<DecodeSignal>& ds : decode_signals_)
349 		for (const shared_ptr<Decoder>& dec : ds->decoder_stack())
350 			if (decoder_ == dec.get())
351 				signal_ = ds.get();
352 
353 	class_selector_->clear();
354 
355 	if (signal_) {
356 		// Populate binary class selector
357 		uint32_t bin_classes = decoder_->get_binary_class_count();
358 		for (uint32_t i = 0; i < bin_classes; i++) {
359 			const data::decode::DecodeBinaryClassInfo* class_info = decoder_->get_binary_class(i);
360 			class_selector_->addItem(class_info->description, QVariant::fromValue(i));
361 		}
362 
363 		connect(signal_, SIGNAL(new_binary_data(unsigned int, void*, unsigned int)),
364 			this, SLOT(on_new_binary_data(unsigned int, void*, unsigned int)));
365 	}
366 
367 	update_data();
368 }
369 
on_selected_class_changed(int index)370 void View::on_selected_class_changed(int index)
371 {
372 	bin_class_id_ = class_selector_->itemData(index).value<uint32_t>();
373 
374 	binary_data_exists_ =
375 		signal_->get_binary_data_chunk_count(current_segment_, decoder_, bin_class_id_);
376 
377 	update_data();
378 }
379 
on_signal_name_changed(const QString & name)380 void View::on_signal_name_changed(const QString &name)
381 {
382 	(void)name;
383 
384 	SignalBase* sb = qobject_cast<SignalBase*>(QObject::sender());
385 	assert(sb);
386 
387 	DecodeSignal* signal = dynamic_cast<DecodeSignal*>(sb);
388 	assert(signal);
389 
390 	// Update all decoder entries provided by this signal
391 	auto stack = signal->decoder_stack();
392 	if (stack.size() > 1) {
393 		for (const shared_ptr<Decoder>& dec : stack) {
394 			QString title = QString("%1 (%2)").arg(signal->name(), dec->name());
395 			int index = decoder_selector_->findData(QVariant::fromValue((void*)dec.get()));
396 
397 			if (index != -1)
398 				decoder_selector_->setItemText(index, title);
399 		}
400 	} else
401 		if (!stack.empty()) {
402 			shared_ptr<Decoder>& dec = stack.at(0);
403 			int index = decoder_selector_->findData(QVariant::fromValue((void*)dec.get()));
404 
405 			if (index != -1)
406 				decoder_selector_->setItemText(index, signal->name());
407 		}
408 }
409 
on_new_binary_data(unsigned int segment_id,void * decoder,unsigned int bin_class_id)410 void View::on_new_binary_data(unsigned int segment_id, void* decoder, unsigned int bin_class_id)
411 {
412 	if ((segment_id == current_segment_) && (decoder == decoder_) && (bin_class_id == bin_class_id_))
413 		if (!delayed_view_updater_.isActive())
414 			delayed_view_updater_.start();
415 }
416 
on_decoder_stacked(void * decoder)417 void View::on_decoder_stacked(void* decoder)
418 {
419 	// TODO This doesn't change existing entries for the same signal - but it should as the naming scheme may change
420 
421 	Decoder* d = static_cast<Decoder*>(decoder);
422 
423 	// Only add the decoder if it has binary output
424 	if (d->get_binary_class_count() == 0)
425 		return;
426 
427 	// Find the signal that contains the selected decoder
428 	DecodeSignal* signal = nullptr;
429 
430 	for (const shared_ptr<DecodeSignal>& ds : decode_signals_)
431 		for (const shared_ptr<Decoder>& dec : ds->decoder_stack())
432 			if (d == dec.get())
433 				signal = ds.get();
434 
435 	assert(signal);
436 
437 	// Add the decoder to the list
438 	QString title = QString("%1 (%2)").arg(signal->name(), d->name());
439 	decoder_selector_->addItem(title, QVariant::fromValue((void*)d));
440 }
441 
on_decoder_removed(void * decoder)442 void View::on_decoder_removed(void* decoder)
443 {
444 	Decoder* d = static_cast<Decoder*>(decoder);
445 
446 	// Remove the decoder from the list
447 	int index = decoder_selector_->findData(QVariant::fromValue((void*)d));
448 
449 	if (index != -1)
450 		decoder_selector_->removeItem(index);
451 }
452 
on_actionSave_triggered(QAction * action)453 void View::on_actionSave_triggered(QAction* action)
454 {
455 	int save_type = SaveTypeBinary;
456 	if (action)
457 		save_type = action->data().toInt();
458 
459 	switch (save_type)
460 	{
461 	case SaveTypeBinary: save_data(); break;
462 	case SaveTypeHexDumpPlain: save_data_as_hex_dump(false, false); break;
463 	case SaveTypeHexDumpWithOffset: save_data_as_hex_dump(true, false); break;
464 	case SaveTypeHexDumpComplete: save_data_as_hex_dump(true, true); break;
465 	}
466 }
467 
perform_delayed_view_update()468 void View::perform_delayed_view_update()
469 {
470 	if (signal_ && !binary_data_exists_)
471 		if (signal_->get_binary_data_chunk_count(current_segment_, decoder_, bin_class_id_))
472 			binary_data_exists_ = true;
473 
474 	update_data();
475 }
476 
477 
478 } // namespace decoder_binary
479 } // namespace views
480 } // namespace pv
481