]> sigrok.org Git - pulseview.git/blobdiff - pv/views/trace/view.cpp
Implement MathSignal
[pulseview.git] / pv / views / trace / view.cpp
index 15e65127b2932062b46a5734575febf9d163631c..47cb96b2da3ce6c2125a39472928e8e1a63cb1b0 100644 (file)
 #include <cmath>
 #include <iostream>
 #include <iterator>
-#include <unordered_set>
-
-#include <boost/archive/text_iarchive.hpp>
-#include <boost/archive/text_oarchive.hpp>
-#include <boost/serialization/serialization.hpp>
 
 #include <QApplication>
+#include <QDebug>
 #include <QEvent>
 #include <QFontMetrics>
 #include <QMenu>
 
 #include <libsigrokcxx/libsigrokcxx.hpp>
 
-#include "analogsignal.hpp"
-#include "header.hpp"
-#include "logicsignal.hpp"
-#include "ruler.hpp"
-#include "signal.hpp"
-#include "tracegroup.hpp"
-#include "triggermarker.hpp"
 #include "view.hpp"
-#include "viewport.hpp"
 
-#include "pv/data/logic.hpp"
-#include "pv/data/logicsegment.hpp"
-#include "pv/devices/device.hpp"
 #include "pv/globalsettings.hpp"
+#include "pv/metadata_obj.hpp"
 #include "pv/session.hpp"
 #include "pv/util.hpp"
+#include "pv/data/logic.hpp"
+#include "pv/data/logicsegment.hpp"
+#include "pv/data/signalbase.hpp"
+#include "pv/devices/device.hpp"
+#include "pv/views/trace/mathsignal.hpp"
+#include "pv/views/trace/analogsignal.hpp"
+#include "pv/views/trace/header.hpp"
+#include "pv/views/trace/logicsignal.hpp"
+#include "pv/views/trace/ruler.hpp"
+#include "pv/views/trace/signal.hpp"
+#include "pv/views/trace/tracegroup.hpp"
+#include "pv/views/trace/triggermarker.hpp"
+#include "pv/views/trace/viewport.hpp"
 
 #ifdef ENABLE_DECODE
-#include "decodetrace.hpp"
+#include "pv/views/trace/decodetrace.hpp"
 #endif
 
+using pv::data::SignalBase;
 using pv::data::SignalData;
 using pv::data::Segment;
 using pv::util::TimeUnit;
@@ -75,6 +75,7 @@ using std::back_inserter;
 using std::copy_if;
 using std::count_if;
 using std::inserter;
+using std::lock_guard;
 using std::max;
 using std::make_pair;
 using std::make_shared;
@@ -84,9 +85,6 @@ using std::pair;
 using std::set;
 using std::set_difference;
 using std::shared_ptr;
-using std::stringstream;
-using std::unordered_map;
-using std::unordered_set;
 using std::vector;
 
 namespace pv {
@@ -98,8 +96,10 @@ const Timestamp View::MinScale("1e-12");
 
 const int View::MaxScrollValue = INT_MAX / 2;
 
-const int View::ScaleUnits[3] = {1, 2, 5};
+/* Area at the top and bottom of the view that can't be scrolled out of sight */
+const int View::ViewScrollMargin = 50;
 
+const int View::ScaleUnits[3] = {1, 2, 5};
 
 CustomScrollArea::CustomScrollArea(QWidget *parent) :
        QAbstractScrollArea(parent)
@@ -184,6 +184,13 @@ View::View(Session &session, bool is_main_view, QMainWindow *parent) :
 
        GlobalSettings::add_change_handler(this);
 
+       // Set up metadata objects and event handlers
+       if (is_main_view) {
+               session_.metadata_obj_manager()->create_object(MetadataObjMainViewRange);
+               session_.metadata_obj_manager()->create_object(MetadataObjMousePos);
+       }
+
+       // Set up UI event handlers
        connect(scrollarea_->horizontalScrollBar(), SIGNAL(valueChanged(int)),
                this, SLOT(h_scroll_value_changed(int)));
        connect(scrollarea_->verticalScrollBar(), SIGNAL(valueChanged(int)),
@@ -205,6 +212,7 @@ View::View(Session &session, bool is_main_view, QMainWindow *parent) :
        connect(&lazy_event_handler_, SIGNAL(timeout()),
                this, SLOT(process_sticky_events()));
        lazy_event_handler_.setSingleShot(true);
+       lazy_event_handler_.setInterval(1000 / ViewBase::MaxViewAutoUpdateRate);
 
        // Set up local keyboard shortcuts
        zoom_in_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Plus), this,
@@ -276,8 +284,10 @@ void View::reset_view_state()
        scale_ = 1e-3;
        offset_ = 0;
        ruler_offset_ = 0;
+       zero_offset_ = 0;
+       custom_zero_offset_set_ = false;
        updating_scroll_ = false;
-       settings_restored_ = false;
+       restoring_state_ = false;
        always_zoom_to_fit_ = false;
        tick_period_ = 0;
        tick_prefix_ = pv::util::SIPrefix::yocto;
@@ -291,10 +301,8 @@ void View::reset_view_state()
        grabbed_widget_ = nullptr;
        hover_point_ = QPoint(-1, -1);
        scroll_needs_defaults_ = true;
-       saved_v_offset_ = 0;
        scale_at_acq_start_ = 0;
        offset_at_acq_start_ = 0;
-       suppress_zoom_to_fit_after_acq_ = false;
 
        show_cursors_ = false;
        cursor_state_changed(show_cursors_);
@@ -305,6 +313,8 @@ void View::reset_view_state()
 
        // Make sure the standard bar's segment selector is in sync
        set_segment_display_mode(segment_display_mode_);
+
+       scrollarea_->verticalScrollBar()->setRange(-100000000, 100000000);
 }
 
 Session& View::session()
@@ -317,7 +327,7 @@ const Session& View::session() const
        return session_;
 }
 
-unordered_set< shared_ptr<Signal> > View::signals() const
+vector< shared_ptr<Signal> > View::signals() const
 {
        return signals_;
 }
@@ -335,24 +345,64 @@ shared_ptr<Signal> View::get_signal_by_signalbase(shared_ptr<data::SignalBase> b
        return ret_val;
 }
 
-void View::clear_signals()
+void View::clear_signalbases()
 {
-       ViewBase::clear_signals();
+       ViewBase::clear_signalbases();
        signals_.clear();
 }
 
-void View::add_signal(const shared_ptr<Signal> signal)
+void View::add_signalbase(const shared_ptr<data::SignalBase> signalbase)
 {
-       ViewBase::add_signalbase(signal->base());
-       signals_.insert(signal);
+       ViewBase::add_signalbase(signalbase);
+
+       shared_ptr<Signal> signal;
+
+       switch (signalbase->type()) {
+       case SignalBase::LogicChannel:
+               signal = shared_ptr<Signal>(new LogicSignal(session_, session_.device(), signalbase));
+               break;
+
+       case SignalBase::AnalogChannel:
+               signal = shared_ptr<Signal>(new AnalogSignal(session_, signalbase));
+               break;
+
+       case SignalBase::MathChannel:
+               signal = shared_ptr<Signal>(new MathSignal(session_, signalbase));
+               break;
+
+       default:
+               qDebug() << "Unknown signalbase type:" << signalbase->type();
+               assert(false);
+               break;
+       }
+
+       signals_.push_back(signal);
 
        signal->set_segment_display_mode(segment_display_mode_);
        signal->set_current_segment(current_segment_);
 
+       // Secondary views use the signal's settings in the main view
+       if (!is_main_view()) {
+               shared_ptr<View> main_tv = dynamic_pointer_cast<View>(session_.main_view());
+               shared_ptr<Signal> main_signal = main_tv->get_signal_by_signalbase(signalbase);
+               if (main_signal)
+                       signal->restore_settings(main_signal->save_settings());
+       }
+
        connect(signal->base().get(), SIGNAL(name_changed(const QString&)),
                this, SLOT(on_signal_name_changed()));
 }
 
+void View::remove_signalbase(const shared_ptr<data::SignalBase> signalbase)
+{
+       ViewBase::remove_signalbase(signalbase);
+
+       shared_ptr<Signal> signal = get_signal_by_signalbase(signalbase);
+
+       if (signal)
+               remove_trace(signal);
+}
+
 #ifdef ENABLE_DECODE
 void View::clear_decode_signals()
 {
@@ -380,14 +430,34 @@ void View::remove_decode_signal(shared_ptr<data::DecodeSignal> signal)
        for (auto i = decode_traces_.begin(); i != decode_traces_.end(); i++)
                if ((*i)->base() == signal) {
                        decode_traces_.erase(i);
-                       signals_changed();
-                       return;
+                       break;
                }
 
        ViewBase::remove_decode_signal(signal);
 }
 #endif
 
+void View::remove_trace(shared_ptr<Trace> trace)
+{
+       TraceTreeItemOwner *const owner = trace->owner();
+       assert(owner);
+       owner->remove_child_item(trace);
+
+       for (auto i = signals_.begin(); i != signals_.end(); i++)
+               if ((*i) == trace) {
+                       signals_.erase(i);
+                       break;
+               }
+
+       if (!header_was_shrunk_)
+               resize_header_to_fit();
+
+       update_layout();
+
+       header_->update();
+       viewport_->update();
+}
+
 shared_ptr<Signal> View::get_signal_under_mouse_cursor() const
 {
        return signal_under_mouse_cursor_;
@@ -432,12 +502,12 @@ void View::save_settings(QSettings &settings) const
        settings.setValue("splitter_state", splitter_->saveState());
        settings.setValue("segment_display_mode", segment_display_mode_);
 
-       {
-               stringstream ss;
-               boost::archive::text_oarchive oa(ss);
-               oa << boost::serialization::make_nvp("offset", offset_);
-               settings.setValue("offset", QString::fromStdString(ss.str()));
-       }
+       GlobalSettings::store_timestamp(settings, "offset", offset_);
+
+       if (custom_zero_offset_set_)
+               GlobalSettings::store_timestamp(settings, "zero_offset", -zero_offset_);
+       else
+               settings.remove("zero_offset");
 
        for (const shared_ptr<Signal>& signal : signals_) {
                settings.beginGroup(signal->base()->internal_name());
@@ -455,20 +525,13 @@ void View::restore_settings(QSettings &settings)
                set_scale(settings.value("scale").toDouble());
 
        if (settings.contains("offset")) {
-               util::Timestamp offset;
-               stringstream ss;
-               ss << settings.value("offset").toString().toStdString();
-
-               try {
-                       boost::archive::text_iarchive ia(ss);
-                       ia >> boost::serialization::make_nvp("offset", offset);
-                       // This also updates ruler_offset_
-                       set_offset(offset);
-               } catch (boost::archive::archive_exception&) {
-                       qDebug() << "Could not restore the view offset";
-               }
+               // This also updates ruler_offset_
+               set_offset(GlobalSettings::restore_timestamp(settings, "offset"));
        }
 
+       if (settings.contains("zero_offset"))
+               set_zero_position(GlobalSettings::restore_timestamp(settings, "zero_offset"));
+
        if (settings.contains("splitter_state"))
                splitter_->restoreState(settings.value("splitter_state").toByteArray());
 
@@ -483,14 +546,12 @@ void View::restore_settings(QSettings &settings)
        }
 
        if (settings.contains("v_offset")) {
+               // Note: see eventFilter() for additional information
                saved_v_offset_ = settings.value("v_offset").toInt();
-               set_v_offset(saved_v_offset_);
                scroll_needs_defaults_ = false;
-               // Note: see eventFilter() for additional information
        }
 
-       settings_restored_ = true;
-       suppress_zoom_to_fit_after_acq_ = true;
+       restoring_state_ = true;
 
        // Update the ruler so that it uses the new scale
        calculate_tick_spacing();
@@ -522,6 +583,9 @@ void View::set_scale(double scale)
 {
        if (scale_ != scale) {
                scale_ = scale;
+
+               update_view_range_metaobject();
+
                scale_changed();
        }
 }
@@ -531,6 +595,9 @@ void View::set_offset(const pv::util::Timestamp& offset, bool force_update)
        if ((offset_ != offset) || force_update) {
                offset_ = offset;
                ruler_offset_ = offset_ + zero_offset_;
+
+               update_view_range_metaobject();
+
                offset_changed();
        }
 }
@@ -548,6 +615,7 @@ const Timestamp& View::ruler_offset() const
 void View::set_zero_position(const pv::util::Timestamp& position)
 {
        zero_offset_ = -position;
+       custom_zero_offset_set_ = true;
 
        // Force an immediate update of the offsets
        set_offset(offset_, true);
@@ -558,6 +626,19 @@ void View::reset_zero_position()
 {
        zero_offset_ = 0;
 
+       // When enabled, the first trigger for this segment is used as the zero position
+       GlobalSettings settings;
+       bool trigger_is_zero_time = settings.value(GlobalSettings::Key_View_TriggerIsZeroTime).toBool();
+
+       if (trigger_is_zero_time) {
+               vector<util::Timestamp> triggers = session_.get_triggers(current_segment_);
+
+               if (triggers.size() > 0)
+                       zero_offset_ = triggers.front();
+       }
+
+       custom_zero_offset_set_ = false;
+
        // Force an immediate update of the offsets
        set_offset(offset_, true);
        ruler_->update();
@@ -676,12 +757,8 @@ void View::set_current_segment(uint32_t segment_id)
        for (util::Timestamp timestamp : triggers)
                trigger_markers_.push_back(make_shared<TriggerMarker>(*this, timestamp));
 
-       // When enabled, the first trigger for this segment is used as the zero position
-       GlobalSettings settings;
-       bool trigger_is_zero_time = settings.value(GlobalSettings::Key_View_TriggerIsZeroTime).toBool();
-
-       if (trigger_is_zero_time && (triggers.size() > 0))
-               set_zero_position(triggers.front());
+       if (!custom_zero_offset_set_)
+               reset_zero_position();
 
        viewport_->update();
 
@@ -705,7 +782,7 @@ void View::set_segment_display_mode(Trace::SegmentDisplayMode mode)
        for (const shared_ptr<Signal>& signal : signals_)
                signal->set_segment_display_mode(mode);
 
-       uint32_t last_segment = session_.get_segment_count() - 1;
+       uint32_t last_segment = session_.get_highest_segment_id();
 
        switch (mode) {
        case Trace::ShowLastSegmentOnly:
@@ -779,12 +856,42 @@ void View::zoom_fit(bool gui_state)
        set_scale_offset(scale.convert_to<double>(), extents.first);
 }
 
+void View::focus_on_range(uint64_t start_sample, uint64_t end_sample)
+{
+       assert(viewport_);
+       const uint64_t w = viewport_->width();
+       if (w <= 0)
+               return;
+
+       const double samplerate = session_.get_samplerate();
+       const double samples_per_pixel = samplerate * scale_;
+       const uint64_t viewport_samples = w * samples_per_pixel;
+
+       const uint64_t sample_delta = (end_sample - start_sample);
+
+       // Note: We add 20% margin on the left and 5% on the right
+       const uint64_t ext_sample_delta = sample_delta * 1.25;
+
+       // Check if we can keep the zoom level and just center the supplied range
+       if (viewport_samples >= ext_sample_delta) {
+               // Note: offset is the left edge of the view so to center, we subtract half the view width
+               const int64_t sample_offset = (start_sample + (sample_delta / 2) - (viewport_samples / 2));
+               const Timestamp offset = sample_offset / samplerate;
+               set_scale_offset(scale_, offset);
+       } else {
+               const Timestamp offset = (start_sample - sample_delta * 0.20) / samplerate;
+               const Timestamp delta = ext_sample_delta / samplerate;
+               const Timestamp scale = max(min(delta / w, MaxScale), MinScale);
+               set_scale_offset(scale.convert_to<double>(), offset);
+       }
+}
+
 void View::set_scale_offset(double scale, const Timestamp& 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)) {
+               (session_.get_capture_state() == Session::Running)) {
 
                if (sticky_scrolling_) {
                        sticky_scrolling_ = false;
@@ -807,13 +914,13 @@ void View::set_scale_offset(double scale, const Timestamp& offset)
        viewport_->update();
 }
 
-set< shared_ptr<SignalData> > View::get_visible_data() const
+vector< shared_ptr<SignalData> > View::get_visible_data() const
 {
        // Make a set of all the visible data objects
-       set< shared_ptr<SignalData> > visible_data;
+       vector< shared_ptr<SignalData> > visible_data;
        for (const shared_ptr<Signal>& sig : signals_)
                if (sig->enabled())
-                       visible_data.insert(sig->data());
+                       visible_data.push_back(sig->data());
 
        return visible_data;
 }
@@ -821,8 +928,16 @@ set< shared_ptr<SignalData> > View::get_visible_data() const
 pair<Timestamp, Timestamp> View::get_time_extents() const
 {
        boost::optional<Timestamp> left_time, right_time;
-       const set< shared_ptr<SignalData> > visible_data = get_visible_data();
-       for (const shared_ptr<SignalData>& d : visible_data) {
+
+       vector< shared_ptr<SignalData> > data;
+       if (signals_.size() == 0)
+               return make_pair(0, 0);
+
+       for (shared_ptr<Signal> s : signals_)
+               if (s->data() && (s->data()->segments().size() > 0))
+                       data.push_back(s->data());
+
+       for (const shared_ptr<SignalData>& d : data) {
                const vector< shared_ptr<Segment> > segments = d->segments();
                for (const shared_ptr<Segment>& s : segments) {
                        double samplerate = s->samplerate();
@@ -841,7 +956,7 @@ pair<Timestamp, Timestamp> View::get_time_extents() const
        if (!left_time || !right_time)
                return make_pair(0, 0);
 
-       assert(*left_time < *right_time);
+       assert(*left_time <= *right_time);
        return make_pair(*left_time, *right_time);
 }
 
@@ -1088,15 +1203,8 @@ void View::trigger_event(int segment_id, util::Timestamp location)
        if ((uint32_t)segment_id != current_segment_)
                return;
 
-       // Set zero location if the Key_View_TriggerIsZeroTime setting is set and
-       // if this is the first trigger for this segment.
-       GlobalSettings settings;
-       bool trigger_is_zero_time = settings.value(GlobalSettings::Key_View_TriggerIsZeroTime).toBool();
-
-       size_t trigger_count = session_.get_triggers(current_segment_).size();
-
-       if (trigger_is_zero_time && trigger_count == 1)
-               set_zero_position(location);
+       if (!custom_zero_offset_set_)
+               reset_zero_position();
 
        trigger_markers_.push_back(make_shared<TriggerMarker>(*this, location));
 }
@@ -1251,9 +1359,13 @@ void View::update_scroll()
        const pair<int, int> extents = v_extents();
 
        // Don't change the scrollbar range if there are no traces
-       if (extents.first != extents.second)
-               vscrollbar->setRange(extents.first - areaSize.height(),
-                       extents.second);
+       if (extents.first != extents.second) {
+               int top_margin = ViewScrollMargin;
+               int btm_margin = ViewScrollMargin;
+
+               vscrollbar->setRange(extents.first - areaSize.height() + top_margin,
+                       extents.second - btm_margin);
+       }
 
        if (scroll_needs_defaults_) {
                set_scroll_default();
@@ -1320,16 +1432,17 @@ void View::resize_header_to_fit()
 void View::update_layout()
 {
        update_scroll();
+
+       update_view_range_metaobject();
 }
 
 TraceTreeItemOwner* View::find_prevalent_trace_group(
        const shared_ptr<sigrok::ChannelGroup> &group,
-       const unordered_map<shared_ptr<data::SignalBase>, shared_ptr<Signal> >
-               &signal_map)
+       const map<shared_ptr<data::SignalBase>, shared_ptr<Signal> > &signal_map)
 {
        assert(group);
 
-       unordered_set<TraceTreeItemOwner*> owners;
+       set<TraceTreeItemOwner*> owners;
        vector<TraceTreeItemOwner*> owner_list;
 
        // Make a set and a list of all the owners
@@ -1361,8 +1474,7 @@ TraceTreeItemOwner* View::find_prevalent_trace_group(
 
 vector< shared_ptr<Trace> > View::extract_new_traces_for_channels(
        const vector< shared_ptr<sigrok::Channel> > &channels,
-       const unordered_map<shared_ptr<data::SignalBase>, shared_ptr<Signal> >
-               &signal_map,
+       const map<shared_ptr<data::SignalBase>, shared_ptr<Signal> > &signal_map,
        set< shared_ptr<Trace> > &add_list)
 {
        vector< shared_ptr<Trace> > filtered_traces;
@@ -1460,7 +1572,7 @@ bool View::eventFilter(QObject *object, QEvent *event)
                // resized to their final sizes.
                update_layout();
 
-               if (settings_restored_)
+               if (restoring_state_)
                        determine_if_header_was_shrunk();
                else
                        resize_header_to_fit();
@@ -1470,10 +1582,8 @@ bool View::eventFilter(QObject *object, QEvent *event)
                        scroll_needs_defaults_ = false;
                }
 
-               if (saved_v_offset_) {
+               if (restoring_state_)
                        set_v_offset(saved_v_offset_);
-                       saved_v_offset_ = 0;
-               }
        }
 
        return QObject::eventFilter(object, event);
@@ -1501,6 +1611,31 @@ void View::resizeEvent(QResizeEvent* event)
        update_layout();
 }
 
+void View::update_view_range_metaobject() const
+{
+       const int w = viewport_->width();
+       if (w > 0) {
+               const double samplerate = session_.get_samplerate();
+               // Note: sample_num = time * samplerate
+               // Note: samples_per_pixel = samplerate * scale
+               const int64_t start_sample = (offset_ * samplerate).convert_to<int64_t>();
+               const int64_t end_sample = (offset_ * samplerate).convert_to<int64_t>() +
+                       (w * session_.get_samplerate() * scale_);
+
+               MetadataObject* md_obj =
+                       session_.metadata_obj_manager()->find_object_by_type(MetadataObjMainViewRange);
+
+               const int64_t old_start_sample = md_obj->value(MetadataValueStartSample).toLongLong();
+               const int64_t old_end_sample = md_obj->value(MetadataValueEndSample).toLongLong();
+
+               if (start_sample != old_start_sample)
+                       md_obj->set_value(MetadataValueStartSample, QVariant((qlonglong)start_sample));
+
+               if (end_sample != old_end_sample)
+                       md_obj->set_value(MetadataValueEndSample, QVariant((qlonglong)end_sample));
+       }
+}
+
 void View::update_hover_point()
 {
        // Determine signal that the mouse cursor is hovering over
@@ -1522,8 +1657,19 @@ void View::update_hover_point()
        for (const shared_ptr<TraceTreeItem>& r : trace_tree_items)
                r->hover_point_changed(hover_point_);
 
-       // Notify any other listeners
+       // Notify this view's listeners
        hover_point_changed(hover_widget_, hover_point_);
+
+       // Hover point is -1 when invalid and 0 for the header
+       if (hover_point_.x() > 0) {
+               // Notify global listeners
+               pv::util::Timestamp mouse_time = offset_ + hover_point_.x() * scale_;
+               int64_t sample_num = (mouse_time * session_.get_samplerate()).convert_to<int64_t>();
+
+               MetadataObject* md_obj =
+                       session_.metadata_obj_manager()->find_object_by_type(MetadataObjMousePos);
+               md_obj->set_value(MetadataValueStartSample, QVariant((qlonglong)sample_num));
+       }
 }
 
 void View::row_item_appearance_changed(bool label, bool content)
@@ -1553,8 +1699,8 @@ void View::extents_changed(bool horz, bool vert)
                (horz ? TraceTreeItemHExtentsChanged : 0) |
                (vert ? TraceTreeItemVExtentsChanged : 0);
 
-       lazy_event_handler_.stop();
-       lazy_event_handler_.start();
+       if (!lazy_event_handler_.isActive())
+               lazy_event_handler_.start();
 }
 
 void View::on_signal_name_changed()
@@ -1648,6 +1794,8 @@ void View::signals_changed()
 {
        using sigrok::Channel;
 
+       lock_guard<mutex> lock(signal_mutex_);
+
        vector< shared_ptr<Channel> > channels;
        shared_ptr<sigrok::Device> sr_dev;
        bool signals_added_or_removed = false;
@@ -1670,7 +1818,9 @@ void View::signals_changed()
        vector< shared_ptr<TraceTreeItem> > new_top_level_items;
 
        // Make a list of traces that are being added, and a list of traces
-       // that are being removed
+       // that are being removed. The set_difference() algorithms require
+       // both sets to be in the exact same order, which means that PD signals
+       // must always be last as they interrupt the sort order otherwise
        const vector<shared_ptr<Trace>> prev_trace_list = list_by_type<Trace>();
        const set<shared_ptr<Trace>> prev_traces(
                prev_trace_list.begin(), prev_trace_list.end());
@@ -1680,7 +1830,6 @@ void View::signals_changed()
 #ifdef ENABLE_DECODE
        traces.insert(decode_traces_.begin(), decode_traces_.end());
 #endif
-
        set< shared_ptr<Trace> > add_traces;
        set_difference(traces.begin(), traces.end(),
                prev_traces.begin(), prev_traces.end(),
@@ -1692,8 +1841,7 @@ void View::signals_changed()
                inserter(remove_traces, remove_traces.begin()));
 
        // Make a look-up table of sigrok Channels to pulseview Signals
-       unordered_map<shared_ptr<data::SignalBase>, shared_ptr<Signal> >
-               signal_map;
+       map<shared_ptr<data::SignalBase>, shared_ptr<Signal> > signal_map;
        for (const shared_ptr<Signal>& sig : signals_)
                signal_map[sig->base()] = sig;
 
@@ -1794,7 +1942,11 @@ void View::signals_changed()
 
        // Add and position the pending top levels items
        int offset = v_extents().second;
-       for (auto item : new_top_level_items) {
+       for (shared_ptr<TraceTreeItem> item : new_top_level_items) {
+               // items may already have gained an owner when they were added to a group above
+               if (item->owner())
+                       continue;
+
                add_child_item(item);
 
                // Position the item after the last item or at the top if there is none
@@ -1831,7 +1983,8 @@ void View::capture_state_updated(int state)
                set_time_unit(util::TimeUnit::Samples);
 
                trigger_markers_.clear();
-               set_zero_position(0);
+               if (!custom_zero_offset_set_)
+                       set_zero_position(0);
 
                scale_at_acq_start_ = scale_;
                offset_at_acq_start_ = offset_;
@@ -1840,13 +1993,14 @@ void View::capture_state_updated(int state)
                // the main view of this session (other trace views may be used for
                // zooming and we don't want to mess them up)
                bool state = settings.value(GlobalSettings::Key_View_ZoomToFitDuringAcq).toBool();
-               if (is_main_view_ && state) {
+               if (is_main_view_ && state && !restoring_state_) {
                        always_zoom_to_fit_ = true;
                        always_zoom_to_fit_changed(always_zoom_to_fit_);
                }
 
                // Enable sticky scrolling if the setting is enabled
-               sticky_scrolling_ = settings.value(GlobalSettings::Key_View_StickyScrolling).toBool();
+               sticky_scrolling_ = !restoring_state_ &&
+                       settings.value(GlobalSettings::Key_View_StickyScrolling).toBool();
 
                // Reset all traces to segment 0
                current_segment_ = 0;
@@ -1872,12 +2026,13 @@ void View::capture_state_updated(int state)
                // Only perform zoom-to-fit if the user hasn't altered the viewport and
                // we didn't restore settings in the meanwhile
                if (zoom_to_fit_after_acq &&
-                       !suppress_zoom_to_fit_after_acq_ &&
+                       !restoring_state_ &&
                        (scale_ == scale_at_acq_start_) &&
-                       (offset_ == offset_at_acq_start_))
+                       (sticky_scrolling_ || (offset_ == offset_at_acq_start_))) {
                        zoom_fit(false);  // We're stopped, so the GUI state doesn't matter
+               }
 
-               suppress_zoom_to_fit_after_acq_ = false;
+               restoring_state_ = false;
        }
 }
 
@@ -1914,12 +2069,9 @@ void View::on_segment_changed(int segment)
 
 void View::on_settingViewTriggerIsZeroTime_changed(const QVariant new_value)
 {
-       if (new_value.toBool()) {
-               // The first trigger for this segment is used as the zero position
-               vector<util::Timestamp> triggers = session_.get_triggers(current_segment_);
-               if (triggers.size() > 0)
-                       set_zero_position(triggers.front());
-       } else
+       (void)new_value;
+
+       if (!custom_zero_offset_set_)
                reset_zero_position();
 }