| 45 | } // namespace |
| 46 | |
| 47 | SourceTimelineController::SourceTimelineController(Timeline* widget, AppSession* session, QObject* parent) |
| 48 | : QObject(parent), widget_(widget), session_(session) { |
| 49 | range_recompute_timer_ = new QTimer(this); |
| 50 | range_recompute_timer_->setSingleShot(true); |
| 51 | range_recompute_timer_->setInterval(kRangeRecomputeThrottleMs); |
| 52 | connect(range_recompute_timer_, &QTimer::timeout, this, [this]() { session_->recomputeRange(); }); |
| 53 | |
| 54 | SessionManager& sessions = session_->sessionManager(); |
| 55 | CatalogModel& catalog = session_->catalogModel(); |
| 56 | PlaybackEngine& playback = session_->playbackEngine(); |
| 57 | |
| 58 | // --- widget intents -> runtime writes --- |
| 59 | connect(widget_, &Timeline::offsetChangeRequested, this, [this, &sessions](quint64 id, qint64 new_offset) { |
| 60 | sessions.setDisplayOffset(static_cast<DatasetId>(id), DisplayOffset{Duration{new_offset}}); |
| 61 | scheduleRangeRecompute(); |
| 62 | }); |
| 63 | connect(widget_, &Timeline::alignRequested, this, &SourceTimelineController::alignStarts); |
| 64 | connect(widget_, &Timeline::mergeRequested, this, &SourceTimelineController::onMergeRequested); |
| 65 | connect(widget_, &Timeline::tracksReordered, this, [this](const QList<quint64>& ids) { |
| 66 | // Adopt the user's drag order so it survives later rebuilds (offset edits, etc.). |
| 67 | display_order_.clear(); |
| 68 | display_order_.reserve(static_cast<std::size_t>(ids.size())); |
| 69 | for (const quint64 id : ids) { |
| 70 | display_order_.push_back(static_cast<DatasetId>(id)); |
| 71 | } |
| 72 | rebuildTracks(); |
| 73 | }); |
| 74 | connect(widget_, &Timeline::playheadSeeked, this, [&playback](double display_seconds) { |
| 75 | // The widget already emits in the playback frame (it undoes its time-frame |
| 76 | // offset in integer ns), so this is a direct, precise hand-off — wrapped |
| 77 | // through the canonical axis-double door rather than constructed raw. |
| 78 | playback.setCurrentTime(fromAxisDouble(display_seconds)); |
| 79 | }); |
| 80 | |
| 81 | // --- runtime state -> widget --- |
| 82 | connect(&catalog, &CatalogModel::itemsAdded, this, [this](const std::vector<CatalogItem>&) { rebuildTracks(); }); |
| 83 | connect(&catalog, &CatalogModel::itemsRemoved, this, [this](const QStringList&) { rebuildTracks(); }); |
| 84 | connect(&catalog, &CatalogModel::cleared, this, [this]() { |
| 85 | merged_ids_.clear(); // fresh session: nothing is a merge result anymore |
| 86 | display_order_.clear(); // ...and no user track ordering to honor |
| 87 | colors_.clear(); // ...so palette assignment restarts at slot 0 (ids are re-minted) |
| 88 | rebuildTracks(); |
| 89 | }); |
| 90 | connect(&sessions, qOverload<PJ::DatasetId>(&SessionManager::displayOffsetChanged), this, [this](DatasetId) { |
| 91 | // Offset-only change (drag/align/reset): refresh just the offsets, NOT a full |
| 92 | // rebuildTracks() — the raw ranges are unchanged, so re-scanning every |
| 93 | // dataset's datasetRawTimeRange on every drag tick would be wasted work. |
| 94 | updateTrackOffsets(); |
| 95 | scheduleRangeRecompute(); |
| 96 | }); |
| 97 | connect(&sessions, qOverload<>(&SessionManager::displayOffsetChanged), this, [this, &sessions]() { |
| 98 | // Global frame change (the "Use time offset" toggle). It does NOT move the bars |
| 99 | // (those follow only the per-source alignment, refreshed here for safety); it |
| 100 | // reframes the NUMBERS (relative-to-epoch vs absolute unix seconds) and shifts |
| 101 | // the playhead/range frame. Push the new global reference to the widget as the |
| 102 | // integer-ns frame offset; the playhead/range re-arrive (re-seeded by MainWindow). |
| 103 | widget_->setTimeFrameOffsetNs(sessions.globalTimeReference()); |
| 104 | widget_->setAbsoluteTimeLabels(!sessions.useTimeOffset()); |
nothing calls this directly
no test coverage detected