X-Git-Url: https://sigrok.org/gitweb/?p=pulseview.git;a=blobdiff_plain;f=pv%2Fview%2Fview.cpp;h=fd8781d642f06fff22f1ef2250e7fa6cfd05a6c6;hp=2ae0a7144cba4b9b15abbc34f3e9265e14f43250;hb=4b0af0b67fe57e0880bcb8aa13b4fa807f3aaac0;hpb=cf124e472f9b5be7f85ecf708fe5290d6a6d1c9e diff --git a/pv/view/view.cpp b/pv/view/view.cpp index 2ae0a714..fd8781d6 100644 --- a/pv/view/view.cpp +++ b/pv/view/view.cpp @@ -61,6 +61,8 @@ using boost::shared_mutex; using pv::data::SignalData; using pv::data::Segment; using pv::util::format_time; +using pv::util::TimeUnit; +using pv::util::Timestamp; using std::deque; using std::dynamic_pointer_cast; @@ -82,10 +84,11 @@ using std::weak_ptr; namespace pv { namespace view { -const double View::MaxScale = 1e9; -const double View::MinScale = 1e-15; +const Timestamp View::MaxScale("1e9"); +const Timestamp View::MinScale("1e-12"); const int View::MaxScrollValue = INT_MAX / 2; +const int View::MaxViewAutoUpdateRate = 25; // No more than 25 Hz with sticky scrolling const int View::ScaleUnits[3] = {1, 2, 5}; @@ -98,8 +101,12 @@ View::View(Session &session, QWidget *parent) : scale_(1e-3), offset_(0), updating_scroll_(false), + sticky_scrolling_(false), // Default setting is set in MainWindow::setup_ui() + always_zoom_to_fit_(false), tick_period_(0.0), - tick_prefix_(0), + tick_prefix_(pv::util::SIPrefix::yocto), + tick_precision_(0), + time_unit_(util::TimeUnit::Time), show_cursors_(false), cursors_(new CursorPair(*this)), next_flag_text_('A'), @@ -113,7 +120,7 @@ View::View(Session &session, QWidget *parent) : connect(&session_, SIGNAL(signals_changed()), this, SLOT(signals_changed())); connect(&session_, SIGNAL(capture_state_changed(int)), - this, SLOT(data_updated())); + this, SLOT(capture_state_updated(int))); connect(&session_, SIGNAL(data_received()), this, SLOT(data_updated())); connect(&session_, SIGNAL(frame_ended()), @@ -136,6 +143,11 @@ View::View(Session &session, QWidget *parent) : this, SLOT(process_sticky_events())); lazy_event_handler_.setSingleShot(true); + connect(&delayed_view_updater_, SIGNAL(timeout()), + this, SLOT(perform_delayed_view_update())); + delayed_view_updater_.setSingleShot(true); + delayed_view_updater_.setInterval(1000 / MaxViewAutoUpdateRate); + setViewport(viewport_); viewport_->installEventFilter(this); @@ -199,11 +211,27 @@ double View::scale() const return scale_; } -double View::offset() const +void View::set_scale(double scale) +{ + if (scale_ != scale) { + scale_ = scale; + Q_EMIT scale_changed(); + } +} + +const Timestamp& View::offset() const { return offset_; } +void View::set_offset(const pv::util::Timestamp& offset) +{ + if (offset_ != offset) { + offset_ = offset; + Q_EMIT offset_changed(); + } +} + int View::owner_visual_v_offset() const { return -verticalScrollBar()->sliderPosition(); @@ -221,16 +249,58 @@ unsigned int View::depth() const return 0; } -unsigned int View::tick_prefix() const +pv::util::SIPrefix View::tick_prefix() const { return tick_prefix_; } +void View::set_tick_prefix(pv::util::SIPrefix tick_prefix) +{ + if (tick_prefix_ != tick_prefix) { + tick_prefix_ = tick_prefix; + Q_EMIT tick_prefix_changed(); + } +} + +unsigned int View::tick_precision() const +{ + return tick_precision_; +} + +void View::set_tick_precision(unsigned tick_precision) +{ + if (tick_precision_ != tick_precision) { + tick_precision_ = tick_precision; + Q_EMIT tick_precision_changed(); + } +} + double View::tick_period() const { return tick_period_; } +void View::set_tick_period(double tick_period) +{ + if (tick_period_ != tick_period) { + tick_period_ = tick_period; + Q_EMIT tick_period_changed(); + } +} + +TimeUnit View::time_unit() const +{ + return time_unit_; +} + +void View::set_time_unit(pv::util::TimeUnit time_unit) +{ + if (time_unit_ != time_unit) { + time_unit_ = time_unit; + Q_EMIT time_unit_changed(); + } +} + void View::zoom(double steps) { zoom(steps, viewport_->width() / 2); @@ -241,11 +311,20 @@ void View::zoom(double steps, int offset) set_zoom(scale_ * pow(3.0/2.0, -steps), offset); } -void View::zoom_fit() +void View::zoom_fit(bool gui_state) { - const pair extents = get_time_extents(); - const double delta = extents.second - extents.first; - if (delta < 1e-12) + // Act as one-shot when stopped, toggle along with the GUI otherwise + if (session_.get_capture_state() == Session::Stopped) { + always_zoom_to_fit_ = false; + always_zoom_to_fit_changed(false); + } else { + always_zoom_to_fit_ = gui_state; + always_zoom_to_fit_changed(gui_state); + } + + const pair extents = get_time_extents(); + const Timestamp delta = extents.second - extents.first; + if (delta < Timestamp("1e-12")) return; assert(viewport_); @@ -253,8 +332,8 @@ void View::zoom_fit() if (w <= 0) return; - const double scale = max(min(delta / w, MaxScale), MinScale); - set_scale_offset(scale, extents.first); + const Timestamp scale = max(min(delta / w, MaxScale), MinScale); + set_scale_offset(scale.convert_to(), extents.first); } void View::zoom_one_to_one() @@ -286,17 +365,32 @@ void View::zoom_one_to_one() set_zoom(1.0 / samplerate, w / 2); } -void View::set_scale_offset(double scale, double offset) +void View::set_scale_offset(double scale, const Timestamp& offset) { - scale_ = scale; - offset_ = offset; + // Disable sticky scrolling / always zoom to fit when acquisition runs + // and user drags the viewport + if ((scale_ == scale) && (offset_ != offset) && + (session_.get_capture_state() == Session::Running)) { + + if (sticky_scrolling_) { + sticky_scrolling_ = false; + sticky_scrolling_changed(false); + } + + if (always_zoom_to_fit_) { + always_zoom_to_fit_ = false; + always_zoom_to_fit_changed(false); + } + } + + set_scale(scale); + set_offset(offset); calculate_tick_spacing(); update_scroll(); ruler_->update(); viewport_->update(); - scale_offset_changed(); } set< shared_ptr > View::get_visible_data() const @@ -313,9 +407,9 @@ set< shared_ptr > View::get_visible_data() const return visible_data; } -pair View::get_time_extents() const +pair View::get_time_extents() const { - double left_time = DBL_MAX, right_time = DBL_MIN; + boost::optional left_time, right_time; const set< shared_ptr > visible_data = get_visible_data(); for (const shared_ptr d : visible_data) { @@ -325,18 +419,26 @@ pair View::get_time_extents() const double samplerate = s->samplerate(); samplerate = (samplerate <= 0.0) ? 1.0 : samplerate; - const double start_time = s->start_time(); - left_time = min(left_time, start_time); - right_time = max(right_time, start_time + - d->max_sample_count() / samplerate); + const Timestamp start_time = s->start_time(); + left_time = left_time ? + min(*left_time, start_time) : + start_time; + right_time = right_time ? + max(*right_time, start_time + d->max_sample_count() / samplerate) : + start_time + d->max_sample_count() / samplerate; } } - if (left_time == DBL_MAX && right_time == DBL_MIN) - return make_pair(0.0, 0.0); + if (!left_time || !right_time) + return make_pair(0, 0); - assert(left_time < right_time); - return make_pair(left_time, right_time); + assert(*left_time < *right_time); + return make_pair(*left_time, *right_time); +} + +void View::enable_sticky_scrolling(bool state) +{ + sticky_scrolling_ = state; } bool View::cursors_shown() const @@ -365,7 +467,7 @@ std::shared_ptr View::cursors() const return cursors_; } -void View::add_flag(double time) +void View::add_flag(const Timestamp& time) { flags_.push_back(shared_ptr(new Flag(*this, time, QString("%1").arg(next_flag_text_)))); @@ -421,27 +523,37 @@ void View::restack_all_row_items() r->animate_to_layout_v_offset(); } -void View::get_scroll_layout(double &length, double &offset) const +void View::get_scroll_layout(double &length, Timestamp &offset) const { - const pair extents = get_time_extents(); - length = (extents.second - extents.first) / scale_; + const pair extents = get_time_extents(); + length = ((extents.second - extents.first) / scale_).convert_to(); offset = offset_ / scale_; } void View::set_zoom(double scale, int offset) { - const double cursor_offset = offset_ + scale_ * offset; - const double new_scale = max(min(scale, MaxScale), MinScale); - const double new_offset = cursor_offset - new_scale * offset; - set_scale_offset(new_scale, new_offset); + // Reset the "always zoom to fit" feature as the user changed the zoom + always_zoom_to_fit_ = false; + always_zoom_to_fit_changed(false); + + const Timestamp cursor_offset = offset_ + scale_ * offset; + const Timestamp new_scale = max(min(Timestamp(scale), MaxScale), MinScale); + const Timestamp new_offset = cursor_offset - new_scale * offset; + set_scale_offset(new_scale.convert_to(), new_offset); } void View::calculate_tick_spacing() { - const double SpacingIncrement = 32.0f; - const double MinValueSpacing = 32.0f; + const double SpacingIncrement = 10.0f; + const double MinValueSpacing = 40.0f; - double min_width = SpacingIncrement, typical_width; + // Figure out the highest numeric value visible on a label + const QSize areaSize = viewport_->size(); + const Timestamp max_time = max(fabs(offset_), + fabs(offset_ + scale_ * areaSize.width())); + + double min_width = SpacingIncrement; + double label_width, tick_period_width; QFontMetrics m(QApplication::font()); @@ -451,23 +563,38 @@ void View::calculate_tick_spacing() const int order = (int)floorf(log10f(min_period)); const double order_decimal = pow(10.0, order); + // Allow for a margin of error so that a scale unit of 1 can be used. + // Otherwise, for a SU of 1 the tick period will almost always be below + // the min_period by a small amount - and thus skipped in favor of 2. + // Note: margin assumes that SU[0] and SU[1] contain the smallest values + double tp_margin = (ScaleUnits[0] + ScaleUnits[1]) / 2.0; + double tp_with_margin; unsigned int unit = 0; do { - tick_period_ = order_decimal * ScaleUnits[unit++]; - } while (tick_period_ < min_period && - unit < countof(ScaleUnits)); + tp_with_margin = order_decimal * (ScaleUnits[unit++] + tp_margin); + } while (tp_with_margin < min_period && unit < countof(ScaleUnits)); + + set_tick_period(order_decimal * ScaleUnits[unit - 1]); + set_tick_prefix(static_cast( + (order - pv::util::exponent(pv::util::SIPrefix::yocto)) / 3)); + + // Precision is the number of fractional digits required, not + // taking the prefix into account (and it must never be negative) + set_tick_precision(std::max((int)ceil(log10f(1 / tick_period_)), 0)); + + tick_period_width = tick_period_ / scale_; - tick_prefix_ = (order - pv::util::FirstSIPrefixPower) / 3; + const QString label_text = + format_time(max_time, tick_prefix_, time_unit_, tick_precision_); - typical_width = m.boundingRect(0, 0, INT_MAX, INT_MAX, - Qt::AlignLeft | Qt::AlignTop, - format_time(offset_, tick_prefix_)).width() + + label_width = m.boundingRect(0, 0, INT_MAX, INT_MAX, + Qt::AlignLeft | Qt::AlignTop, label_text).width() + MinValueSpacing; min_width += SpacingIncrement; - } while(typical_width > tick_period_ / scale_); + } while (tick_period_width < label_width); } void View::update_scroll() @@ -477,7 +604,8 @@ void View::update_scroll() const QSize areaSize = viewport_->size(); // Set the horizontal scroll bar - double length = 0, offset = 0; + double length = 0; + Timestamp offset; get_scroll_layout(length, offset); length = max(length - areaSize.width(), 0.0); @@ -490,11 +618,11 @@ void View::update_scroll() if (length < MaxScrollValue) { horizontalScrollBar()->setRange(0, length); - horizontalScrollBar()->setSliderPosition(offset); + horizontalScrollBar()->setSliderPosition(offset.convert_to()); } else { horizontalScrollBar()->setRange(0, MaxScrollValue); horizontalScrollBar()->setSliderPosition( - offset_ * MaxScrollValue / (scale_ * length)); + (offset_ * MaxScrollValue / (scale_ * length)).convert_to()); } updating_scroll_ = false; @@ -596,6 +724,28 @@ vector< shared_ptr > View::extract_new_traces_for_channels( return filtered_traces; } +void View::determine_time_unit() +{ + // Check whether we know the sample rate and hence can use time as the unit + if (time_unit_ == util::TimeUnit::Samples) { + shared_lock lock(session().signals_mutex()); + const unordered_set< shared_ptr > &sigs(session().signals()); + + // Check all signals but... + for (const shared_ptr signal : sigs) { + const shared_ptr data = signal->data(); + + // ...only check first segment of each + const vector< shared_ptr > segments = data->segments(); + if (!segments.empty()) + if (segments[0]->samplerate()) { + set_time_unit(util::TimeUnit::Time); + break; + } + } + } +} + bool View::eventFilter(QObject *object, QEvent *event) { const QEvent::Type type = event->type(); @@ -674,13 +824,21 @@ void View::h_scroll_value_changed(int value) if (updating_scroll_) return; + // Disable sticky scrolling when user moves the horizontal scroll bar + // during a running acquisition + if (sticky_scrolling_ && (session_.get_capture_state() == Session::Running)) { + sticky_scrolling_ = false; + sticky_scrolling_changed(false); + } + const int range = horizontalScrollBar()->maximum(); if (range < MaxScrollValue) - offset_ = scale_ * value; + set_offset(scale_ * value); else { - double length = 0, offset; + double length = 0; + Timestamp offset; get_scroll_layout(length, offset); - offset_ = scale_ * length * value / MaxScrollValue; + set_offset(scale_ * length * value / MaxScrollValue); } ruler_->update(); @@ -819,12 +977,57 @@ void View::signals_changed() viewport_->update(); } +void View::capture_state_updated(int state) +{ + if (state == Session::Running) + set_time_unit(util::TimeUnit::Samples); + + if (state == Session::Stopped) { + // After acquisition has stopped we need to re-calculate the ticks once + // as it's otherwise done when the user pans or zooms, which is too late + calculate_tick_spacing(); + + // Reset "always zoom to fit", the acquisition has stopped + if (always_zoom_to_fit_) { + always_zoom_to_fit_ = false; + always_zoom_to_fit_changed(false); + } + } +} + void View::data_updated() { - // Update the scroll bars - update_scroll(); + if (always_zoom_to_fit_ || sticky_scrolling_) { + if (!delayed_view_updater_.isActive()) + delayed_view_updater_.start(); + } else { + determine_time_unit(); + update_scroll(); + ruler_->update(); + viewport_->update(); + } +} - // Repaint the view +void View::perform_delayed_view_update() +{ + if (always_zoom_to_fit_) + zoom_fit(true); + + if (sticky_scrolling_) { + // Make right side of the view sticky + double length = 0; + Timestamp offset; + get_scroll_layout(length, offset); + + const QSize areaSize = viewport_->size(); + length = max(length - areaSize.width(), 0.0); + + set_offset(scale_ * length); + } + + determine_time_unit(); + update_scroll(); + ruler_->update(); viewport_->update(); }