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>
34 #include <QApplication>
36 #include <QFormLayout>
39 #include <QPushButton>
42 #include "decodetrace.hpp"
44 #include <pv/session.hpp>
45 #include <pv/data/decoderstack.hpp>
46 #include <pv/data/decode/decoder.hpp>
47 #include <pv/data/logic.hpp>
48 #include <pv/data/logicsegment.hpp>
49 #include <pv/data/decode/annotation.hpp>
50 #include <pv/view/logicsignal.hpp>
51 #include <pv/view/view.hpp>
52 #include <pv/view/viewport.hpp>
53 #include <pv/widgets/decodergroupbox.hpp>
54 #include <pv/widgets/decodermenu.hpp>
56 using boost::shared_lock;
57 using boost::shared_mutex;
58 using std::dynamic_pointer_cast;
60 using std::lock_guard;
67 using std::shared_ptr;
69 using std::unordered_set;
75 const QColor DecodeTrace::DecodeColours[4] = {
76 QColor(0xEF, 0x29, 0x29), // Red
77 QColor(0xFC, 0xE9, 0x4F), // Yellow
78 QColor(0x8A, 0xE2, 0x34), // Green
79 QColor(0x72, 0x9F, 0xCF) // Blue
82 const QColor DecodeTrace::ErrorBgColour = QColor(0xEF, 0x29, 0x29);
83 const QColor DecodeTrace::NoDecodeColour = QColor(0x88, 0x8A, 0x85);
85 const int DecodeTrace::ArrowSize = 4;
86 const double DecodeTrace::EndCapWidth = 5;
87 const int DecodeTrace::DrawPadding = 100;
89 const QColor DecodeTrace::Colours[16] = {
90 QColor(0xEF, 0x29, 0x29),
91 QColor(0xF6, 0x6A, 0x32),
92 QColor(0xFC, 0xAE, 0x3E),
93 QColor(0xFB, 0xCA, 0x47),
94 QColor(0xFC, 0xE9, 0x4F),
95 QColor(0xCD, 0xF0, 0x40),
96 QColor(0x8A, 0xE2, 0x34),
97 QColor(0x4E, 0xDC, 0x44),
98 QColor(0x55, 0xD7, 0x95),
99 QColor(0x64, 0xD1, 0xD2),
100 QColor(0x72, 0x9F, 0xCF),
101 QColor(0xD4, 0x76, 0xC4),
102 QColor(0x9D, 0x79, 0xB9),
103 QColor(0xAD, 0x7F, 0xA8),
104 QColor(0xC2, 0x62, 0x9B),
105 QColor(0xD7, 0x47, 0x6F)
108 const QColor DecodeTrace::OutlineColours[16] = {
109 QColor(0x77, 0x14, 0x14),
110 QColor(0x7B, 0x35, 0x19),
111 QColor(0x7E, 0x57, 0x1F),
112 QColor(0x7D, 0x65, 0x23),
113 QColor(0x7E, 0x74, 0x27),
114 QColor(0x66, 0x78, 0x20),
115 QColor(0x45, 0x71, 0x1A),
116 QColor(0x27, 0x6E, 0x22),
117 QColor(0x2A, 0x6B, 0x4A),
118 QColor(0x32, 0x68, 0x69),
119 QColor(0x39, 0x4F, 0x67),
120 QColor(0x6A, 0x3B, 0x62),
121 QColor(0x4E, 0x3C, 0x5C),
122 QColor(0x56, 0x3F, 0x54),
123 QColor(0x61, 0x31, 0x4D),
124 QColor(0x6B, 0x23, 0x37)
127 DecodeTrace::DecodeTrace(pv::Session &session,
128 std::shared_ptr<pv::data::DecoderStack> decoder_stack, int index) :
129 Trace(QString::fromUtf8(
130 decoder_stack->stack().front()->decoder()->name)),
132 decoder_stack_(decoder_stack),
134 delete_mapper_(this),
135 show_hide_mapper_(this)
137 assert(decoder_stack_);
139 colour_ = DecodeColours[index % countof(DecodeColours)];
141 connect(decoder_stack_.get(), SIGNAL(new_decode_data()),
142 this, SLOT(on_new_decode_data()));
143 connect(&delete_mapper_, SIGNAL(mapped(int)),
144 this, SLOT(on_delete_decoder(int)));
145 connect(&show_hide_mapper_, SIGNAL(mapped(int)),
146 this, SLOT(on_show_hide_decoder(int)));
149 bool DecodeTrace::enabled() const
154 const std::shared_ptr<pv::data::DecoderStack>& DecodeTrace::decoder() const
156 return decoder_stack_;
159 pair<int, int> DecodeTrace::v_extents() const
161 /// @todo Replace this with an implementation that knows the true
162 /// height of the trace
163 const int row_height = (ViewItemPaintParams::text_height() * 6) / 4;
164 return make_pair(-row_height / 2, row_height * 7 / 2);
167 void DecodeTrace::paint_back(QPainter &p, const ViewItemPaintParams &pp)
169 Trace::paint_back(p, pp);
170 paint_axis(p, pp, get_visual_y());
173 void DecodeTrace::paint_mid(QPainter &p, const ViewItemPaintParams &pp)
175 using namespace pv::data::decode;
177 const int text_height = ViewItemPaintParams::text_height();
178 row_height_ = (text_height * 6) / 4;
179 const int annotation_height = (text_height * 5) / 4;
181 assert(decoder_stack_);
182 const QString err = decoder_stack_->error_message();
185 draw_unresolved_period(
186 p, annotation_height, pp.left(), pp.right());
187 draw_error(p, err, pp);
191 // Iterate through the rows
192 int y = get_visual_y();
193 pair<uint64_t, uint64_t> sample_range = get_sample_range(
194 pp.left(), pp.right());
196 assert(decoder_stack_);
197 const vector<Row> rows(decoder_stack_->get_visible_rows());
199 visible_rows_.clear();
200 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++)
235 const int y = i * row_height_ + get_visual_y();
237 p.setPen(QPen(Qt::NoPen));
238 p.setBrush(QApplication::palette().brush(QPalette::WindowText));
242 const QPointF points[] = {
243 QPointF(pp.left(), y - ArrowSize),
244 QPointF(pp.left() + ArrowSize, y),
245 QPointF(pp.left(), y + ArrowSize)
247 p.drawPolygon(points, countof(points));
250 const QRect r(pp.left() + ArrowSize * 2, y - row_height_ / 2,
251 pp.right() - pp.left(), row_height_);
252 const QString h(visible_rows_[i].title());
253 const int f = Qt::AlignLeft | Qt::AlignVCenter |
257 p.setPen(QApplication::palette().color(QPalette::Base));
258 for (int dx = -1; dx <= 1; dx++)
259 for (int dy = -1; dy <= 1; dy++)
260 if (dx != 0 && dy != 0)
261 p.drawText(r.translated(dx, dy), f, h);
264 p.setPen(QApplication::palette().color(QPalette::WindowText));
269 void DecodeTrace::populate_popup_form(QWidget *parent, QFormLayout *form)
271 using pv::data::decode::Decoder;
275 assert(decoder_stack_);
277 // Add the standard options
278 Trace::populate_popup_form(parent, form);
280 // Add the decoder options
282 channel_selectors_.clear();
283 decoder_forms_.clear();
285 const list< shared_ptr<Decoder> >& stack = decoder_stack_->stack();
289 QLabel *const l = new QLabel(
290 tr("<p><i>No decoders in the stack</i></p>"));
291 l->setAlignment(Qt::AlignCenter);
296 auto iter = stack.cbegin();
297 for (int i = 0; i < (int)stack.size(); i++, iter++) {
298 shared_ptr<Decoder> dec(*iter);
299 create_decoder_form(i, dec, parent, form);
302 form->addRow(new QLabel(
303 tr("<i>* Required channels</i>"), parent));
306 // Add stacking button
307 pv::widgets::DecoderMenu *const decoder_menu =
308 new pv::widgets::DecoderMenu(parent);
309 connect(decoder_menu, SIGNAL(decoder_selected(srd_decoder*)),
310 this, SLOT(on_stack_decoder(srd_decoder*)));
312 QPushButton *const stack_button =
313 new QPushButton(tr("Stack Decoder"), parent);
314 stack_button->setMenu(decoder_menu);
316 QHBoxLayout *stack_button_box = new QHBoxLayout;
317 stack_button_box->addWidget(stack_button, 0, Qt::AlignRight);
318 form->addRow(stack_button_box);
321 QMenu* DecodeTrace::create_context_menu(QWidget *parent)
323 QMenu *const menu = Trace::create_context_menu(parent);
325 menu->addSeparator();
327 QAction *const del = new QAction(tr("Delete"), this);
328 del->setShortcuts(QKeySequence::Delete);
329 connect(del, SIGNAL(triggered()), this, SLOT(on_delete()));
330 menu->addAction(del);
335 void DecodeTrace::draw_annotation(const pv::data::decode::Annotation &a,
336 QPainter &p, int h, const ViewItemPaintParams &pp, int y,
337 size_t base_colour) const
339 double samples_per_pixel, pixels_offset;
340 tie(pixels_offset, samples_per_pixel) =
341 get_pixels_offset_samples_per_pixel();
343 const double start = a.start_sample() / samples_per_pixel -
345 const double end = a.end_sample() / samples_per_pixel -
348 const size_t colour = (base_colour + a.format()) % countof(Colours);
349 const QColor &fill = Colours[colour];
350 const QColor &outline = OutlineColours[colour];
352 if (start > pp.right() + DrawPadding || end < pp.left() - DrawPadding)
355 if (a.start_sample() == a.end_sample())
356 draw_instant(a, p, fill, outline, h, start, y);
358 draw_range(a, p, fill, outline, h, start, end, y);
361 void DecodeTrace::draw_instant(const pv::data::decode::Annotation &a, QPainter &p,
362 QColor fill, QColor outline, int h, double x, int y) const
364 const QString text = a.annotations().empty() ?
365 QString() : a.annotations().back();
366 const double w = min((double)p.boundingRect(QRectF(), 0, text).width(),
368 const QRectF rect(x - w / 2, y - h / 2, w, h);
372 p.drawRoundedRect(rect, h / 2, h / 2);
375 p.drawText(rect, Qt::AlignCenter | Qt::AlignVCenter, text);
378 void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p,
379 QColor fill, QColor outline, int h, double start,
380 double end, int y) const
382 const double top = y + .5 - h / 2;
383 const double bottom = y + .5 + h / 2;
384 const vector<QString> annotations = a.annotations();
389 // If the two ends are within 1 pixel, draw a vertical line
390 if (start + 1.0 > end)
392 p.drawLine(QPointF(start, top), QPointF(start, bottom));
396 const double cap_width = min((end - start) / 4, EndCapWidth);
399 QPointF(start, y + .5f),
400 QPointF(start + cap_width, top),
401 QPointF(end - cap_width, top),
402 QPointF(end, y + .5f),
403 QPointF(end - cap_width, bottom),
404 QPointF(start + cap_width, bottom)
407 p.drawConvexPolygon(pts, countof(pts));
409 if (annotations.empty())
412 QRectF rect(start + cap_width, y - h / 2,
413 end - start - cap_width * 2, h);
414 if (rect.width() <= 4)
419 // Try to find an annotation that will fit
420 QString best_annotation;
423 for (const QString &a : annotations) {
424 const int w = p.boundingRect(QRectF(), 0, a).width();
425 if (w <= rect.width() && w > best_width)
426 best_annotation = a, best_width = w;
429 if (best_annotation.isEmpty())
430 best_annotation = annotations.back();
432 // If not ellide the last in the list
433 p.drawText(rect, Qt::AlignCenter, p.fontMetrics().elidedText(
434 best_annotation, Qt::ElideRight, rect.width()));
437 void DecodeTrace::draw_error(QPainter &p, const QString &message,
438 const ViewItemPaintParams &pp)
440 const int y = get_visual_y();
442 p.setPen(ErrorBgColour.darker());
443 p.setBrush(ErrorBgColour);
445 const QRectF bounding_rect =
446 QRectF(pp.width(), INT_MIN / 2 + y, pp.width(), INT_MAX);
447 const QRectF text_rect = p.boundingRect(bounding_rect,
448 Qt::AlignCenter, message);
449 const float r = text_rect.height() / 4;
451 p.drawRoundedRect(text_rect.adjusted(-r, -r, r, r), r, r,
455 p.drawText(text_rect, message);
458 void DecodeTrace::draw_unresolved_period(QPainter &p, int h, int left,
461 using namespace pv::data;
462 using pv::data::decode::Decoder;
464 double samples_per_pixel, pixels_offset;
466 assert(decoder_stack_);
468 shared_ptr<Logic> data;
469 shared_ptr<LogicSignal> logic_signal;
471 const list< shared_ptr<Decoder> > &stack = decoder_stack_->stack();
473 // We get the logic data of the first channel in the list.
474 // This works because we are currently assuming all
475 // LogicSignals have the same data/segment
476 for (const shared_ptr<Decoder> &dec : stack)
477 if (dec && !dec->channels().empty() &&
478 ((logic_signal = (*dec->channels().begin()).second)) &&
479 ((data = logic_signal->logic_data())))
482 if (!data || data->logic_segments().empty())
485 const shared_ptr<LogicSegment> segment =
486 data->logic_segments().front();
488 const int64_t sample_count = (int64_t)segment->get_sample_count();
489 if (sample_count == 0)
492 const int64_t samples_decoded = decoder_stack_->samples_decoded();
493 if (sample_count == samples_decoded)
496 const int y = get_visual_y();
498 tie(pixels_offset, samples_per_pixel) =
499 get_pixels_offset_samples_per_pixel();
501 const double start = max(samples_decoded /
502 samples_per_pixel - pixels_offset, left - 1.0);
503 const double end = min(sample_count / samples_per_pixel -
504 pixels_offset, right + 1.0);
505 const QRectF no_decode_rect(start, y - h/2 + 0.5, end - start, h);
507 p.setPen(QPen(Qt::NoPen));
508 p.setBrush(Qt::white);
509 p.drawRect(no_decode_rect);
511 p.setPen(NoDecodeColour);
512 p.setBrush(QBrush(NoDecodeColour, Qt::Dense6Pattern));
513 p.drawRect(no_decode_rect);
516 pair<double, double> DecodeTrace::get_pixels_offset_samples_per_pixel() const
519 assert(decoder_stack_);
521 const View *view = owner_->view();
524 const double scale = view->scale();
527 const double pixels_offset =
528 (view->offset() - decoder_stack_->start_time()) / scale;
530 double samplerate = decoder_stack_->samplerate();
532 // Show sample rate as 1Hz when it is unknown
533 if (samplerate == 0.0)
536 return make_pair(pixels_offset, samplerate * scale);
539 pair<uint64_t, uint64_t> DecodeTrace::get_sample_range(
540 int x_start, int x_end) const
542 double samples_per_pixel, pixels_offset;
543 tie(pixels_offset, samples_per_pixel) =
544 get_pixels_offset_samples_per_pixel();
546 const uint64_t start = (uint64_t)max(
547 (x_start + pixels_offset) * samples_per_pixel, 0.0);
548 const uint64_t end = (uint64_t)max(
549 (x_end + pixels_offset) * samples_per_pixel, 0.0);
551 return make_pair(start, end);
554 int DecodeTrace::get_row_at_point(const QPoint &point)
559 const int y = (point.y() - get_visual_y() + row_height_ / 2);
561 /* Integer divison of (x-1)/x would yield 0, so we check for this. */
565 const int row = y / row_height_;
567 if (row >= (int)visible_rows_.size())
573 const QString DecodeTrace::get_annotation_at_point(const QPoint &point)
575 using namespace pv::data::decode;
580 const pair<uint64_t, uint64_t> sample_range =
581 get_sample_range(point.x(), point.x() + 1);
582 const int row = get_row_at_point(point);
586 vector<pv::data::decode::Annotation> annotations;
588 assert(decoder_stack_);
589 decoder_stack_->get_annotation_subset(annotations, visible_rows_[row],
590 sample_range.first, sample_range.second);
592 return (annotations.empty()) ?
593 QString() : annotations[0].annotations().front();
596 void DecodeTrace::hover_point_changed()
600 const View *const view = owner_->view();
603 QPoint hp = view->hover_point();
604 QString ann = get_annotation_at_point(hp);
608 if (!row_height_ || ann.isEmpty()) {
609 QToolTip::hideText();
613 const int hover_row = get_row_at_point(hp);
615 QFontMetrics m(QToolTip::font());
616 const QRect text_size = m.boundingRect(QRect(), 0, ann);
618 // This is OS-specific and unfortunately we can't query it, so
619 // use an approximation to at least try to minimize the error.
620 const int padding = 8;
622 // Make sure the tool tip doesn't overlap with the mouse cursor.
623 // If it did, the tool tip would constantly hide and re-appear.
624 // We also push it up by one row so that it appears above the
625 // decode trace, not below.
626 hp.setX(hp.x() - (text_size.width() / 2) - padding);
628 hp.setY(get_visual_y() - (row_height_ / 2) +
629 (hover_row * row_height_) -
630 row_height_ - text_size.height() - padding);
632 QToolTip::showText(view->viewport()->mapToGlobal(hp), ann);
635 void DecodeTrace::create_decoder_form(int index,
636 shared_ptr<data::decode::Decoder> &dec, QWidget *parent,
642 const srd_decoder *const decoder = dec->decoder();
645 pv::widgets::DecoderGroupBox *const group =
646 new pv::widgets::DecoderGroupBox(
647 QString::fromUtf8(decoder->name));
648 group->set_decoder_visible(dec->shown());
650 delete_mapper_.setMapping(group, index);
651 connect(group, SIGNAL(delete_decoder()), &delete_mapper_, SLOT(map()));
653 show_hide_mapper_.setMapping(group, index);
654 connect(group, SIGNAL(show_hide_decoder()),
655 &show_hide_mapper_, SLOT(map()));
657 QFormLayout *const decoder_form = new QFormLayout;
658 group->add_layout(decoder_form);
660 // Add the mandatory channels
661 for(l = decoder->channels; l; l = l->next) {
662 const struct srd_channel *const pdch =
663 (struct srd_channel *)l->data;
664 QComboBox *const combo = create_channel_selector(parent, dec, pdch);
665 connect(combo, SIGNAL(currentIndexChanged(int)),
666 this, SLOT(on_channel_selected(int)));
667 decoder_form->addRow(tr("<b>%1</b> (%2) *")
668 .arg(QString::fromUtf8(pdch->name))
669 .arg(QString::fromUtf8(pdch->desc)), combo);
671 const ChannelSelector s = {combo, dec, pdch};
672 channel_selectors_.push_back(s);
675 // Add the optional channels
676 for(l = decoder->opt_channels; l; l = l->next) {
677 const struct srd_channel *const pdch =
678 (struct srd_channel *)l->data;
679 QComboBox *const combo = create_channel_selector(parent, dec, pdch);
680 connect(combo, SIGNAL(currentIndexChanged(int)),
681 this, SLOT(on_channel_selected(int)));
682 decoder_form->addRow(tr("<b>%1</b> (%2)")
683 .arg(QString::fromUtf8(pdch->name))
684 .arg(QString::fromUtf8(pdch->desc)), combo);
686 const ChannelSelector s = {combo, dec, pdch};
687 channel_selectors_.push_back(s);
691 shared_ptr<binding::Decoder> binding(
692 new binding::Decoder(decoder_stack_, dec));
693 binding->add_properties_to_form(decoder_form, true);
695 bindings_.push_back(binding);
698 decoder_forms_.push_back(group);
701 QComboBox* DecodeTrace::create_channel_selector(
702 QWidget *parent, const shared_ptr<data::decode::Decoder> &dec,
703 const srd_channel *const pdch)
707 shared_lock<shared_mutex> lock(session_.signals_mutex());
708 const auto &sigs(session_.signals());
710 vector< shared_ptr<Signal> > sig_list(sigs.begin(), sigs.end());
711 std::sort(sig_list.begin(), sig_list.end(),
712 [](const shared_ptr<Signal> &a, const shared_ptr<Signal> b) {
713 return a->name().compare(b->name()) < 0; });
715 assert(decoder_stack_);
716 const auto channel_iter = dec->channels().find(pdch);
718 QComboBox *selector = new QComboBox(parent);
720 selector->addItem("-", qVariantFromValue((void*)NULL));
722 if (channel_iter == dec->channels().end())
723 selector->setCurrentIndex(0);
725 for (const shared_ptr<view::Signal> &s : sig_list) {
727 if (dynamic_pointer_cast<LogicSignal>(s) && s->enabled())
729 selector->addItem(s->name(),
730 qVariantFromValue((void*)s.get()));
731 if ((*channel_iter).second == s)
732 selector->setCurrentIndex(
733 selector->count() - 1);
740 void DecodeTrace::commit_decoder_channels(shared_ptr<data::decode::Decoder> &dec)
744 map<const srd_channel*, shared_ptr<LogicSignal> > channel_map;
746 shared_lock<shared_mutex> lock(session_.signals_mutex());
747 const unordered_set< shared_ptr<Signal> > &sigs(session_.signals());
749 for (const ChannelSelector &s : channel_selectors_)
751 if(s.decoder_ != dec)
754 const LogicSignal *const selection =
755 (LogicSignal*)s.combo_->itemData(
756 s.combo_->currentIndex()).value<void*>();
758 for (shared_ptr<Signal> sig : sigs)
759 if(sig.get() == selection) {
760 channel_map[s.pdch_] =
761 dynamic_pointer_cast<LogicSignal>(sig);
766 dec->set_channels(channel_map);
769 void DecodeTrace::commit_channels()
771 assert(decoder_stack_);
772 for (shared_ptr<data::decode::Decoder> dec : decoder_stack_->stack())
773 commit_decoder_channels(dec);
775 decoder_stack_->begin_decode();
778 void DecodeTrace::on_new_decode_data()
781 owner_->row_item_appearance_changed(false, true);
784 void DecodeTrace::delete_pressed()
789 void DecodeTrace::on_delete()
791 session_.remove_decode_signal(this);
794 void DecodeTrace::on_channel_selected(int)
799 void DecodeTrace::on_stack_decoder(srd_decoder *decoder)
802 assert(decoder_stack_);
803 decoder_stack_->push(shared_ptr<data::decode::Decoder>(
804 new data::decode::Decoder(decoder)));
805 decoder_stack_->begin_decode();
810 void DecodeTrace::on_delete_decoder(int index)
812 decoder_stack_->remove(index);
817 decoder_stack_->begin_decode();
820 void DecodeTrace::on_show_hide_decoder(int index)
822 using pv::data::decode::Decoder;
824 const list< shared_ptr<Decoder> > stack(decoder_stack_->stack());
826 // Find the decoder in the stack
827 auto iter = stack.cbegin();
828 for(int i = 0; i < index; i++, iter++)
829 assert(iter != stack.end());
831 shared_ptr<Decoder> dec = *iter;
834 const bool show = !dec->shown();
837 assert(index < (int)decoder_forms_.size());
838 decoder_forms_[index]->set_decoder_visible(show);
841 owner_->row_item_appearance_changed(false, true);