2 * This file is part of the PulseView project.
4 * Copyright (C) 2012 Joel Holdsworth <joel@airwebreathe.org.uk>
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.
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.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22 #include <libsigrokdecode/libsigrokdecode.h>
31 #include <boost/functional/hash.hpp>
32 #include <boost/thread/locks.hpp>
33 #include <boost/thread/shared_mutex.hpp>
36 #include <QApplication>
38 #include <QFormLayout>
41 #include <QPushButton>
44 #include "decodetrace.hpp"
46 #include <pv/session.hpp>
47 #include <pv/data/decoderstack.hpp>
48 #include <pv/data/decode/decoder.hpp>
49 #include <pv/data/logic.hpp>
50 #include <pv/data/logicsegment.hpp>
51 #include <pv/data/decode/annotation.hpp>
52 #include <pv/view/logicsignal.hpp>
53 #include <pv/view/view.hpp>
54 #include <pv/view/viewport.hpp>
55 #include <pv/widgets/decodergroupbox.hpp>
56 #include <pv/widgets/decodermenu.hpp>
58 using boost::shared_lock;
59 using boost::shared_mutex;
60 using std::dynamic_pointer_cast;
62 using std::lock_guard;
69 using std::shared_ptr;
71 using std::unordered_set;
77 const QColor DecodeTrace::DecodeColours[4] = {
78 QColor(0xEF, 0x29, 0x29), // Red
79 QColor(0xFC, 0xE9, 0x4F), // Yellow
80 QColor(0x8A, 0xE2, 0x34), // Green
81 QColor(0x72, 0x9F, 0xCF) // Blue
84 const QColor DecodeTrace::ErrorBgColour = QColor(0xEF, 0x29, 0x29);
85 const QColor DecodeTrace::NoDecodeColour = QColor(0x88, 0x8A, 0x85);
87 const int DecodeTrace::ArrowSize = 4;
88 const double DecodeTrace::EndCapWidth = 5;
89 const int DecodeTrace::DrawPadding = 100;
91 const QColor DecodeTrace::Colours[16] = {
92 QColor(0xEF, 0x29, 0x29),
93 QColor(0xF6, 0x6A, 0x32),
94 QColor(0xFC, 0xAE, 0x3E),
95 QColor(0xFB, 0xCA, 0x47),
96 QColor(0xFC, 0xE9, 0x4F),
97 QColor(0xCD, 0xF0, 0x40),
98 QColor(0x8A, 0xE2, 0x34),
99 QColor(0x4E, 0xDC, 0x44),
100 QColor(0x55, 0xD7, 0x95),
101 QColor(0x64, 0xD1, 0xD2),
102 QColor(0x72, 0x9F, 0xCF),
103 QColor(0xD4, 0x76, 0xC4),
104 QColor(0x9D, 0x79, 0xB9),
105 QColor(0xAD, 0x7F, 0xA8),
106 QColor(0xC2, 0x62, 0x9B),
107 QColor(0xD7, 0x47, 0x6F)
110 const QColor DecodeTrace::OutlineColours[16] = {
111 QColor(0x77, 0x14, 0x14),
112 QColor(0x7B, 0x35, 0x19),
113 QColor(0x7E, 0x57, 0x1F),
114 QColor(0x7D, 0x65, 0x23),
115 QColor(0x7E, 0x74, 0x27),
116 QColor(0x66, 0x78, 0x20),
117 QColor(0x45, 0x71, 0x1A),
118 QColor(0x27, 0x6E, 0x22),
119 QColor(0x2A, 0x6B, 0x4A),
120 QColor(0x32, 0x68, 0x69),
121 QColor(0x39, 0x4F, 0x67),
122 QColor(0x6A, 0x3B, 0x62),
123 QColor(0x4E, 0x3C, 0x5C),
124 QColor(0x56, 0x3F, 0x54),
125 QColor(0x61, 0x31, 0x4D),
126 QColor(0x6B, 0x23, 0x37)
129 DecodeTrace::DecodeTrace(pv::Session &session,
130 std::shared_ptr<pv::data::DecoderStack> decoder_stack, int index) :
131 Trace(QString::fromUtf8(
132 decoder_stack->stack().front()->decoder()->name)),
134 decoder_stack_(decoder_stack),
136 delete_mapper_(this),
137 show_hide_mapper_(this)
139 assert(decoder_stack_);
141 colour_ = DecodeColours[index % countof(DecodeColours)];
143 connect(decoder_stack_.get(), SIGNAL(new_decode_data()),
144 this, SLOT(on_new_decode_data()));
145 connect(&delete_mapper_, SIGNAL(mapped(int)),
146 this, SLOT(on_delete_decoder(int)));
147 connect(&show_hide_mapper_, SIGNAL(mapped(int)),
148 this, SLOT(on_show_hide_decoder(int)));
151 bool DecodeTrace::enabled() const
156 const std::shared_ptr<pv::data::DecoderStack>& DecodeTrace::decoder() const
158 return decoder_stack_;
161 pair<int, int> DecodeTrace::v_extents() const
163 /// @todo Replace this with an implementation that knows the true
164 /// height of the trace
165 const int row_height = (ViewItemPaintParams::text_height() * 6) / 4;
166 return make_pair(-row_height / 2, row_height * 7 / 2);
169 void DecodeTrace::paint_back(QPainter &p, const ViewItemPaintParams &pp)
171 Trace::paint_back(p, pp);
172 paint_axis(p, pp, get_visual_y());
175 void DecodeTrace::paint_mid(QPainter &p, const ViewItemPaintParams &pp)
177 using namespace pv::data::decode;
179 const int text_height = ViewItemPaintParams::text_height();
180 row_height_ = (text_height * 6) / 4;
181 const int annotation_height = (text_height * 5) / 4;
183 assert(decoder_stack_);
184 const QString err = decoder_stack_->error_message();
185 if (!err.isEmpty()) {
186 draw_unresolved_period(
187 p, annotation_height, pp.left(), pp.right());
188 draw_error(p, err, pp);
192 // Iterate through the rows
193 int y = get_visual_y();
194 pair<uint64_t, uint64_t> sample_range = get_sample_range(
195 pp.left(), pp.right());
197 assert(decoder_stack_);
198 const vector<Row> rows(decoder_stack_->get_visible_rows());
200 visible_rows_.clear();
201 for (size_t i = 0; i < rows.size(); i++) {
202 const Row &row = rows[i];
204 size_t base_colour = 0x13579BDF;
205 boost::hash_combine(base_colour, this);
206 boost::hash_combine(base_colour, row.decoder());
207 boost::hash_combine(base_colour, row.row());
210 vector<Annotation> annotations;
211 decoder_stack_->get_annotation_subset(annotations, row,
212 sample_range.first, sample_range.second);
213 if (!annotations.empty()) {
214 for (const Annotation &a : annotations)
215 draw_annotation(a, p, annotation_height,
219 visible_rows_.push_back(rows[i]);
224 draw_unresolved_period(p, annotation_height, pp.left(), pp.right());
227 void DecodeTrace::paint_fore(QPainter &p, const ViewItemPaintParams &pp)
229 using namespace pv::data::decode;
233 for (size_t i = 0; i < visible_rows_.size(); i++) {
234 const int y = i * row_height_ + get_visual_y();
236 p.setPen(QPen(Qt::NoPen));
237 p.setBrush(QApplication::palette().brush(QPalette::WindowText));
240 const QPointF points[] = {
241 QPointF(pp.left(), y - ArrowSize),
242 QPointF(pp.left() + ArrowSize, y),
243 QPointF(pp.left(), y + ArrowSize)
245 p.drawPolygon(points, countof(points));
248 const QRect r(pp.left() + ArrowSize * 2, y - row_height_ / 2,
249 pp.right() - pp.left(), row_height_);
250 const QString h(visible_rows_[i].title());
251 const int f = Qt::AlignLeft | Qt::AlignVCenter |
255 p.setPen(QApplication::palette().color(QPalette::Base));
256 for (int dx = -1; dx <= 1; dx++)
257 for (int dy = -1; dy <= 1; dy++)
258 if (dx != 0 && dy != 0)
259 p.drawText(r.translated(dx, dy), f, h);
262 p.setPen(QApplication::palette().color(QPalette::WindowText));
267 void DecodeTrace::populate_popup_form(QWidget *parent, QFormLayout *form)
269 using pv::data::decode::Decoder;
273 assert(decoder_stack_);
275 // Add the standard options
276 Trace::populate_popup_form(parent, form);
278 // Add the decoder options
280 channel_selectors_.clear();
281 decoder_forms_.clear();
283 const list< shared_ptr<Decoder> >& stack = decoder_stack_->stack();
286 QLabel *const l = new QLabel(
287 tr("<p><i>No decoders in the stack</i></p>"));
288 l->setAlignment(Qt::AlignCenter);
291 auto iter = stack.cbegin();
292 for (int i = 0; i < (int)stack.size(); i++, iter++) {
293 shared_ptr<Decoder> dec(*iter);
294 create_decoder_form(i, dec, parent, form);
297 form->addRow(new QLabel(
298 tr("<i>* Required channels</i>"), parent));
301 // Add stacking button
302 pv::widgets::DecoderMenu *const decoder_menu =
303 new pv::widgets::DecoderMenu(parent);
304 connect(decoder_menu, SIGNAL(decoder_selected(srd_decoder*)),
305 this, SLOT(on_stack_decoder(srd_decoder*)));
307 QPushButton *const stack_button =
308 new QPushButton(tr("Stack Decoder"), parent);
309 stack_button->setMenu(decoder_menu);
311 QHBoxLayout *stack_button_box = new QHBoxLayout;
312 stack_button_box->addWidget(stack_button, 0, Qt::AlignRight);
313 form->addRow(stack_button_box);
316 QMenu* DecodeTrace::create_context_menu(QWidget *parent)
318 QMenu *const menu = Trace::create_context_menu(parent);
320 menu->addSeparator();
322 QAction *const del = new QAction(tr("Delete"), this);
323 del->setShortcuts(QKeySequence::Delete);
324 connect(del, SIGNAL(triggered()), this, SLOT(on_delete()));
325 menu->addAction(del);
330 void DecodeTrace::draw_annotation(const pv::data::decode::Annotation &a,
331 QPainter &p, int h, const ViewItemPaintParams &pp, int y,
332 size_t base_colour) const
334 double samples_per_pixel, pixels_offset;
335 tie(pixels_offset, samples_per_pixel) =
336 get_pixels_offset_samples_per_pixel();
338 const double start = a.start_sample() / samples_per_pixel -
340 const double end = a.end_sample() / samples_per_pixel -
343 const size_t colour = (base_colour + a.format()) % countof(Colours);
344 const QColor &fill = Colours[colour];
345 const QColor &outline = OutlineColours[colour];
347 if (start > pp.right() + DrawPadding || end < pp.left() - DrawPadding)
350 if (a.start_sample() == a.end_sample())
351 draw_instant(a, p, fill, outline, h, start, y);
353 draw_range(a, p, fill, outline, h, start, end, y);
356 void DecodeTrace::draw_instant(const pv::data::decode::Annotation &a, QPainter &p,
357 QColor fill, QColor outline, int h, double x, int y) const
359 const QString text = a.annotations().empty() ?
360 QString() : a.annotations().back();
361 const double w = min((double)p.boundingRect(QRectF(), 0, text).width(),
363 const QRectF rect(x - w / 2, y - h / 2, w, h);
367 p.drawRoundedRect(rect, h / 2, h / 2);
370 p.drawText(rect, Qt::AlignCenter | Qt::AlignVCenter, text);
373 void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p,
374 QColor fill, QColor outline, int h, double start,
375 double end, int y) const
377 const double top = y + .5 - h / 2;
378 const double bottom = y + .5 + h / 2;
379 const vector<QString> annotations = a.annotations();
384 // If the two ends are within 1 pixel, draw a vertical line
385 if (start + 1.0 > end) {
386 p.drawLine(QPointF(start, top), QPointF(start, bottom));
390 const double cap_width = min((end - start) / 4, EndCapWidth);
393 QPointF(start, y + .5f),
394 QPointF(start + cap_width, top),
395 QPointF(end - cap_width, top),
396 QPointF(end, y + .5f),
397 QPointF(end - cap_width, bottom),
398 QPointF(start + cap_width, bottom)
401 p.drawConvexPolygon(pts, countof(pts));
403 if (annotations.empty())
406 QRectF rect(start + cap_width, y - h / 2,
407 end - start - cap_width * 2, h);
408 if (rect.width() <= 4)
413 // Try to find an annotation that will fit
414 QString best_annotation;
417 for (const QString &a : annotations) {
418 const int w = p.boundingRect(QRectF(), 0, a).width();
419 if (w <= rect.width() && w > best_width)
420 best_annotation = a, best_width = w;
423 if (best_annotation.isEmpty())
424 best_annotation = annotations.back();
426 // If not ellide the last in the list
427 p.drawText(rect, Qt::AlignCenter, p.fontMetrics().elidedText(
428 best_annotation, Qt::ElideRight, rect.width()));
431 void DecodeTrace::draw_error(QPainter &p, const QString &message,
432 const ViewItemPaintParams &pp)
434 const int y = get_visual_y();
436 p.setPen(ErrorBgColour.darker());
437 p.setBrush(ErrorBgColour);
439 const QRectF bounding_rect =
440 QRectF(pp.width(), INT_MIN / 2 + y, pp.width(), INT_MAX);
441 const QRectF text_rect = p.boundingRect(bounding_rect,
442 Qt::AlignCenter, message);
443 const float r = text_rect.height() / 4;
445 p.drawRoundedRect(text_rect.adjusted(-r, -r, r, r), r, r,
449 p.drawText(text_rect, message);
452 void DecodeTrace::draw_unresolved_period(QPainter &p, int h, int left,
455 using namespace pv::data;
456 using pv::data::decode::Decoder;
458 double samples_per_pixel, pixels_offset;
460 assert(decoder_stack_);
462 shared_ptr<Logic> data;
463 shared_ptr<LogicSignal> logic_signal;
465 const list< shared_ptr<Decoder> > &stack = decoder_stack_->stack();
467 // We get the logic data of the first channel in the list.
468 // This works because we are currently assuming all
469 // LogicSignals have the same data/segment
470 for (const shared_ptr<Decoder> &dec : stack)
471 if (dec && !dec->channels().empty() &&
472 ((logic_signal = (*dec->channels().begin()).second)) &&
473 ((data = logic_signal->logic_data())))
476 if (!data || data->logic_segments().empty())
479 const shared_ptr<LogicSegment> segment =
480 data->logic_segments().front();
482 const int64_t sample_count = (int64_t)segment->get_sample_count();
483 if (sample_count == 0)
486 const int64_t samples_decoded = decoder_stack_->samples_decoded();
487 if (sample_count == samples_decoded)
490 const int y = get_visual_y();
492 tie(pixels_offset, samples_per_pixel) =
493 get_pixels_offset_samples_per_pixel();
495 const double start = max(samples_decoded /
496 samples_per_pixel - pixels_offset, left - 1.0);
497 const double end = min(sample_count / samples_per_pixel -
498 pixels_offset, right + 1.0);
499 const QRectF no_decode_rect(start, y - h/2 + 0.5, end - start, h);
501 p.setPen(QPen(Qt::NoPen));
502 p.setBrush(Qt::white);
503 p.drawRect(no_decode_rect);
505 p.setPen(NoDecodeColour);
506 p.setBrush(QBrush(NoDecodeColour, Qt::Dense6Pattern));
507 p.drawRect(no_decode_rect);
510 pair<double, double> DecodeTrace::get_pixels_offset_samples_per_pixel() const
513 assert(decoder_stack_);
515 const View *view = owner_->view();
518 const double scale = view->scale();
521 const double pixels_offset =
522 ((view->offset() - decoder_stack_->start_time()) / scale).convert_to<double>();
524 double samplerate = decoder_stack_->samplerate();
526 // Show sample rate as 1Hz when it is unknown
527 if (samplerate == 0.0)
530 return make_pair(pixels_offset, samplerate * scale);
533 pair<uint64_t, uint64_t> DecodeTrace::get_sample_range(
534 int x_start, int x_end) const
536 double samples_per_pixel, pixels_offset;
537 tie(pixels_offset, samples_per_pixel) =
538 get_pixels_offset_samples_per_pixel();
540 const uint64_t start = (uint64_t)max(
541 (x_start + pixels_offset) * samples_per_pixel, 0.0);
542 const uint64_t end = (uint64_t)max(
543 (x_end + pixels_offset) * samples_per_pixel, 0.0);
545 return make_pair(start, end);
548 int DecodeTrace::get_row_at_point(const QPoint &point)
553 const int y = (point.y() - get_visual_y() + row_height_ / 2);
555 /* Integer divison of (x-1)/x would yield 0, so we check for this. */
559 const int row = y / row_height_;
561 if (row >= (int)visible_rows_.size())
567 const QString DecodeTrace::get_annotation_at_point(const QPoint &point)
569 using namespace pv::data::decode;
574 const pair<uint64_t, uint64_t> sample_range =
575 get_sample_range(point.x(), point.x() + 1);
576 const int row = get_row_at_point(point);
580 vector<pv::data::decode::Annotation> annotations;
582 assert(decoder_stack_);
583 decoder_stack_->get_annotation_subset(annotations, visible_rows_[row],
584 sample_range.first, sample_range.second);
586 return (annotations.empty()) ?
587 QString() : annotations[0].annotations().front();
590 void DecodeTrace::hover_point_changed()
594 const View *const view = owner_->view();
597 QPoint hp = view->hover_point();
598 QString ann = get_annotation_at_point(hp);
602 if (!row_height_ || ann.isEmpty()) {
603 QToolTip::hideText();
607 const int hover_row = get_row_at_point(hp);
609 QFontMetrics m(QToolTip::font());
610 const QRect text_size = m.boundingRect(QRect(), 0, ann);
612 // This is OS-specific and unfortunately we can't query it, so
613 // use an approximation to at least try to minimize the error.
614 const int padding = 8;
616 // Make sure the tool tip doesn't overlap with the mouse cursor.
617 // If it did, the tool tip would constantly hide and re-appear.
618 // We also push it up by one row so that it appears above the
619 // decode trace, not below.
620 hp.setX(hp.x() - (text_size.width() / 2) - padding);
622 hp.setY(get_visual_y() - (row_height_ / 2) +
623 (hover_row * row_height_) -
624 row_height_ - text_size.height() - padding);
626 QToolTip::showText(view->viewport()->mapToGlobal(hp), ann);
629 void DecodeTrace::create_decoder_form(int index,
630 shared_ptr<data::decode::Decoder> &dec, QWidget *parent,
636 const srd_decoder *const decoder = dec->decoder();
639 const bool decoder_deletable = index > 0;
641 pv::widgets::DecoderGroupBox *const group =
642 new pv::widgets::DecoderGroupBox(
643 QString::fromUtf8(decoder->name), nullptr, decoder_deletable);
644 group->set_decoder_visible(dec->shown());
646 if (decoder_deletable) {
647 delete_mapper_.setMapping(group, index);
648 connect(group, SIGNAL(delete_decoder()), &delete_mapper_, SLOT(map()));
651 show_hide_mapper_.setMapping(group, index);
652 connect(group, SIGNAL(show_hide_decoder()),
653 &show_hide_mapper_, SLOT(map()));
655 QFormLayout *const decoder_form = new QFormLayout;
656 group->add_layout(decoder_form);
658 // Add the mandatory channels
659 for (l = decoder->channels; l; l = l->next) {
660 const struct srd_channel *const pdch =
661 (struct srd_channel *)l->data;
662 QComboBox *const combo = create_channel_selector(parent, dec, pdch);
663 connect(combo, SIGNAL(currentIndexChanged(int)),
664 this, SLOT(on_channel_selected(int)));
665 decoder_form->addRow(tr("<b>%1</b> (%2) *")
666 .arg(QString::fromUtf8(pdch->name))
667 .arg(QString::fromUtf8(pdch->desc)), combo);
669 const ChannelSelector s = {combo, dec, pdch};
670 channel_selectors_.push_back(s);
673 // Add the optional channels
674 for (l = decoder->opt_channels; l; l = l->next) {
675 const struct srd_channel *const pdch =
676 (struct srd_channel *)l->data;
677 QComboBox *const combo = create_channel_selector(parent, dec, pdch);
678 connect(combo, SIGNAL(currentIndexChanged(int)),
679 this, SLOT(on_channel_selected(int)));
680 decoder_form->addRow(tr("<b>%1</b> (%2)")
681 .arg(QString::fromUtf8(pdch->name))
682 .arg(QString::fromUtf8(pdch->desc)), combo);
684 const ChannelSelector s = {combo, dec, pdch};
685 channel_selectors_.push_back(s);
689 shared_ptr<binding::Decoder> binding(
690 new binding::Decoder(decoder_stack_, dec));
691 binding->add_properties_to_form(decoder_form, true);
693 bindings_.push_back(binding);
696 decoder_forms_.push_back(group);
699 QComboBox* DecodeTrace::create_channel_selector(
700 QWidget *parent, const shared_ptr<data::decode::Decoder> &dec,
701 const srd_channel *const pdch)
705 const auto sigs(session_.signals());
707 vector< shared_ptr<Signal> > sig_list(sigs.begin(), sigs.end());
708 std::sort(sig_list.begin(), sig_list.end(),
709 [](const shared_ptr<Signal> &a, const shared_ptr<Signal> b) {
710 return a->name().compare(b->name()) < 0; });
712 assert(decoder_stack_);
713 const auto channel_iter = dec->channels().find(pdch);
715 QComboBox *selector = new QComboBox(parent);
717 selector->addItem("-", qVariantFromValue((void*)nullptr));
719 if (channel_iter == dec->channels().end())
720 selector->setCurrentIndex(0);
722 for (const shared_ptr<view::Signal> &s : sig_list) {
724 if (dynamic_pointer_cast<LogicSignal>(s) && s->enabled()) {
725 selector->addItem(s->name(),
726 qVariantFromValue((void*)s.get()));
728 if (channel_iter != dec->channels().end() &&
729 (*channel_iter).second == s)
730 selector->setCurrentIndex(
731 selector->count() - 1);
738 void DecodeTrace::commit_decoder_channels(shared_ptr<data::decode::Decoder> &dec)
742 map<const srd_channel*, shared_ptr<LogicSignal> > channel_map;
744 const unordered_set< shared_ptr<Signal> > sigs(session_.signals());
746 for (const ChannelSelector &s : channel_selectors_) {
747 if (s.decoder_ != dec)
750 const LogicSignal *const selection =
751 (LogicSignal*)s.combo_->itemData(
752 s.combo_->currentIndex()).value<void*>();
754 for (shared_ptr<Signal> sig : sigs)
755 if (sig.get() == selection) {
756 channel_map[s.pdch_] =
757 dynamic_pointer_cast<LogicSignal>(sig);
762 dec->set_channels(channel_map);
765 void DecodeTrace::commit_channels()
767 assert(decoder_stack_);
768 for (shared_ptr<data::decode::Decoder> dec : decoder_stack_->stack())
769 commit_decoder_channels(dec);
771 decoder_stack_->begin_decode();
774 void DecodeTrace::on_new_decode_data()
777 owner_->row_item_appearance_changed(false, true);
780 void DecodeTrace::delete_pressed()
785 void DecodeTrace::on_delete()
787 session_.remove_decode_signal(this);
790 void DecodeTrace::on_channel_selected(int)
795 void DecodeTrace::on_stack_decoder(srd_decoder *decoder)
798 assert(decoder_stack_);
799 decoder_stack_->push(shared_ptr<data::decode::Decoder>(
800 new data::decode::Decoder(decoder)));
801 decoder_stack_->begin_decode();
806 void DecodeTrace::on_delete_decoder(int index)
808 decoder_stack_->remove(index);
813 decoder_stack_->begin_decode();
816 void DecodeTrace::on_show_hide_decoder(int index)
818 using pv::data::decode::Decoder;
820 const list< shared_ptr<Decoder> > stack(decoder_stack_->stack());
822 // Find the decoder in the stack
823 auto iter = stack.cbegin();
824 for (int i = 0; i < index; i++, iter++)
825 assert(iter != stack.end());
827 shared_ptr<Decoder> dec = *iter;
830 const bool show = !dec->shown();
833 assert(index < (int)decoder_forms_.size());
834 decoder_forms_[index]->set_decoder_visible(show);
837 owner_->row_item_appearance_changed(false, true);