From 7b6042168a345c867961aa9eac13dd21325d9159 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Wed, 5 Aug 2026 14:53:04 +0800 Subject: [PATCH] fix(widget): harden TimelineAndTrackView teardown and TimeScaledObject limits - TimelineAndTrackView: disconnect the scrollbar sync connections in the destructor. Child views reset their scene during destruction, which can re-emit QScrollBar::valueChanged into a half-destroyed object and trip Qt's assertObjectType abort. - TimeScaledObject: keep minimum <= maximum when setting scale limits so set_scale()'s std::clamp is never called with inverted limits (UB). Setting the maximum below the minimum now pulls the minimum down, and vice versa, matching QAbstractSlider conventions. --- app/widget/timebased/timescaledobject.cpp | 12 ++++++++++++ app/widget/timelinewidget/timelineandtrackview.cpp | 10 ++++++++++ app/widget/timelinewidget/timelineandtrackview.h | 2 ++ 3 files changed, 24 insertions(+) diff --git a/app/widget/timebased/timescaledobject.cpp b/app/widget/timebased/timescaledobject.cpp index 7e436d368..b72f84349 100644 --- a/app/widget/timebased/timescaledobject.cpp +++ b/app/widget/timebased/timescaledobject.cpp @@ -116,6 +116,12 @@ void TimeScaledObject::set_maximum_scale(const double &max) { max_scale_ = max; + // Keep min <= max so the clamp in set_scale() is never called with + // inverted limits (undefined behavior) + if (min_scale_ > max_scale_) { + min_scale_ = max_scale_; + } + if (get_scale() > max_scale_) { set_scale(max_scale_); } @@ -125,6 +131,12 @@ void TimeScaledObject::set_minimum_scale(const double &min) { min_scale_ = min; + // Keep min <= max so the clamp in set_scale() is never called with + // inverted limits (undefined behavior) + if (max_scale_ < min_scale_) { + max_scale_ = min_scale_; + } + if (get_scale() < min_scale_) { set_scale(min_scale_); } diff --git a/app/widget/timelinewidget/timelineandtrackview.cpp b/app/widget/timelinewidget/timelineandtrackview.cpp index fede8e1f0..76bd1c234 100644 --- a/app/widget/timelinewidget/timelineandtrackview.cpp +++ b/app/widget/timelinewidget/timelineandtrackview.cpp @@ -54,6 +54,16 @@ TimelineAndTrackView::TimelineAndTrackView(Qt::Alignment vertical_alignment, splitter_->setSizes({ 180, width() }); } +TimelineAndTrackView::~TimelineAndTrackView() +{ + // When the child views are destroyed they reset their scene, which can + // push a valueChanged through these connections while this object is + // already half-destroyed (Qt aborts on a slot invoked past its class's + // destructor). Detach first so teardown emissions go nowhere. + disconnect(view_->verticalScrollBar(), nullptr, this, nullptr); + disconnect(track_view_->verticalScrollBar(), nullptr, this, nullptr); +} + QSplitter *TimelineAndTrackView::splitter() const { return splitter_; diff --git a/app/widget/timelinewidget/timelineandtrackview.h b/app/widget/timelinewidget/timelineandtrackview.h index 7181aebcd..b0cafb633 100644 --- a/app/widget/timelinewidget/timelineandtrackview.h +++ b/app/widget/timelinewidget/timelineandtrackview.h @@ -42,6 +42,8 @@ public: TrackView *track_view() const; + virtual ~TimelineAndTrackView() override; + private: QSplitter *splitter_;