From 5c8f80480d2f160fbf3a0c84f28a3a10e7a30401 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Mon, 13 Jul 2026 02:10:51 +0800 Subject: [PATCH] UI: flatten preferences behavior tabs and add node parameter editor shortcut - Move Behavior-General options (hover focus, slider ladder, scroll zooms) into the General preferences tab. - Move Behavior-Audio option (audio scrubbing) into the Audio preferences tab. - Promote remaining Behavior categories (Timeline, Playback, Project, Nodes, Rendering) to top-level sidebar entries without the 'Behavior - ' prefix. - Update Chinese translations for the new sidebar titles and fill unfinished Behavior tab strings. NodeView: - Remove double-click jump-to-parameter-editor behavior; keep expand/collapse. - Add right-click context menu item 'Show in Parameter Editor'. - Add Shift+P shortcut bound to the same action. Render: - Fix crash in PreviewAutoCacher::ClearSingleFrameRenders when proxy playback causes a render ticket to finish synchronously before the watcher pointer is returned. Defer the watcher Finished signal via queued connection so the caller can safely register the watcher before it is deleted. --- app/dialog/preferences/preferences.cpp | 26 +++--- .../preferences/tabs/preferencesaudiotab.cpp | 7 ++ .../preferences/tabs/preferencesaudiotab.h | 4 + .../tabs/preferencesbehaviortab.cpp | 17 ---- .../preferences/tabs/preferencesbehaviortab.h | 14 ++-- .../tabs/preferencesgeneraltab.cpp | 28 +++++++ .../preferences/tabs/preferencesgeneraltab.h | 4 + app/render/previewautocacher.cpp | 10 ++- app/render/renderticket.cpp | 8 +- app/ts/zh_CN.ts | 79 +++++++++++-------- app/widget/nodeview/nodeview.cpp | 52 +++++++++--- app/widget/nodeview/nodeview.h | 4 + 12 files changed, 168 insertions(+), 85 deletions(-) diff --git a/app/dialog/preferences/preferences.cpp b/app/dialog/preferences/preferences.cpp index 596e6bb84..dd6c955a7 100644 --- a/app/dialog/preferences/preferences.cpp +++ b/app/dialog/preferences/preferences.cpp @@ -45,22 +45,18 @@ PreferencesDialog::PreferencesDialog(MainWindow *main_window, int start_tab) AddTab(new PreferencesGeneralTab(), tr("General")); AddTab(new PreferencesAppearanceTab(), tr("Appearance")); - AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryGeneral), - tr("Behavior - General")); - AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryAudio), - tr("Behavior - Audio")); - AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryTimeline), - tr("Behavior - Timeline")); - AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryPlayback), - tr("Behavior - Playback")); - AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryProject), - tr("Behavior - Project")); - AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryNodes), - tr("Behavior - Nodes")); - AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryRendering), - tr("Behavior - Rendering")); - AddTab(new PreferencesDiskTab(), tr("Disk")); AddTab(new PreferencesAudioTab(), tr("Audio")); + AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryTimeline), + tr("Timeline")); + AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryPlayback), + tr("Playback")); + AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryProject), + tr("Project")); + AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryNodes), + tr("Nodes")); + AddTab(new PreferencesBehaviorTab(PreferencesBehaviorTab::kCategoryRendering), + tr("Rendering")); + AddTab(new PreferencesDiskTab(), tr("Disk")); AddTab(new PreferencesKeyboardTab(main_window), tr("Keyboard")); SetCurrentTab(start_tab); diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.cpp b/app/dialog/preferences/tabs/preferencesaudiotab.cpp index 19cad4815..399f0f4e1 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.cpp +++ b/app/dialog/preferences/tabs/preferencesaudiotab.cpp @@ -54,6 +54,11 @@ PreferencesAudioTab::PreferencesAudioTab() audio_tab_layout->addLayout(main_layout); } + audio_scrubbing_ = new QCheckBox( + PreferencesBehaviorTab::BehaviorPrefTr("Enable audio scrubbing")); + audio_scrubbing_->setChecked(OLIVE_CONFIG("AudioScrubbing").toBool()); + audio_tab_layout->addWidget(audio_scrubbing_); + { QGroupBox *groupbox = new QGroupBox(); audio_tab_layout->addWidget(groupbox); @@ -232,6 +237,8 @@ void PreferencesAudioTab::Accept(MultiUndoCommand *command) .to_string()); emit AudioManager::instance() -> OutputParamsChanged(); + + OLIVE_CONFIG("AudioScrubbing") = audio_scrubbing_->isChecked(); } void PreferencesAudioTab::RefreshBackends() diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.h b/app/dialog/preferences/tabs/preferencesaudiotab.h index 20b62d39d..7c82a84ff 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.h +++ b/app/dialog/preferences/tabs/preferencesaudiotab.h @@ -24,10 +24,12 @@ #include #include +#include #include "dialog/configbase/configdialogbase.h" #include "dialog/export/exportaudiotab.h" #include "dialog/export/exportformatcombobox.h" +#include "preferencesbehaviortab.h" namespace olive { @@ -70,6 +72,8 @@ private: ExportAudioTab *record_options_; + QCheckBox *audio_scrubbing_; + private slots: void RefreshBackends(); diff --git a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp index 12f819ac9..db2adb294 100644 --- a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp +++ b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp @@ -36,23 +36,6 @@ PreferencesBehaviorTab::PreferencesBehaviorTab(Category category) layout->setAlignment(Qt::AlignTop); switch (category_) { - case kCategoryGeneral: - AddItem(tr("Enable hover focus"), QStringLiteral("HoverFocus"), - tr("Panels will be considered focused when the mouse cursor is " - "over them without having to click them.")); - AddItem(tr("Enable slider ladder"), QStringLiteral("UseSliderLadders")); - AddItem( - tr("Scrolling zooms by default"), QStringLiteral("ScrollZooms"), - tr("By default, scrolling will move the view around, and holding " - "Ctrl/Cmd will make it zoom instead. Enabling this will switch " - "those, scrolling will zoom by default, and holding Ctrl/Cmd will " - "move the view instead.")); - break; - - case kCategoryAudio: - AddItem(tr("Enable audio scrubbing"), QStringLiteral("AudioScrubbing")); - break; - case kCategoryTimeline: AddItems({ { tr("Auto-Seek to Imported Clips"), diff --git a/app/dialog/preferences/tabs/preferencesbehaviortab.h b/app/dialog/preferences/tabs/preferencesbehaviortab.h index d9fda9216..cc2fd4f55 100644 --- a/app/dialog/preferences/tabs/preferencesbehaviortab.h +++ b/app/dialog/preferences/tabs/preferencesbehaviortab.h @@ -24,6 +24,7 @@ #include #include +#include #include #include "dialog/configbase/configdialogbase.h" @@ -35,8 +36,6 @@ class PreferencesBehaviorTab : public ConfigDialogBaseTab { Q_OBJECT public: enum Category { - kCategoryGeneral, - kCategoryAudio, kCategoryTimeline, kCategoryPlayback, kCategoryProject, @@ -48,11 +47,16 @@ public: virtual void Accept(MultiUndoCommand *command) override; + static QString BehaviorPrefTr(const char *text) + { + return QCoreApplication::translate("olive::PreferencesBehaviorTab", text); + } + private: struct Item { QString text; QString config_key; - QString tooltip; + QString tooltip = QString(); }; void AddItems(const QVector &items); @@ -61,9 +65,9 @@ private: QMap config_map_; - QComboBox *graphics_backend_combobox_; - Category category_; + + QComboBox *graphics_backend_combobox_; }; } diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp index 1a7f94dc7..df771fb47 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp @@ -28,6 +28,7 @@ #include "common/autoscroll.h" #include "core.h" +#include "preferencesbehaviortab.h" namespace olive { @@ -167,6 +168,29 @@ PreferencesGeneralTab::PreferencesGeneralTab() autorecovery_layout->addWidget(browse_autorecoveries, row, 1); } + { + QGroupBox *behavior_groupbox = new QGroupBox(PreferencesBehaviorTab::BehaviorPrefTr("Behavior")); + QVBoxLayout *behavior_layout = new QVBoxLayout(behavior_groupbox); + layout->addWidget(behavior_groupbox); + + hover_focus_ = new QCheckBox(PreferencesBehaviorTab::BehaviorPrefTr("Enable hover focus")); + hover_focus_->setToolTip(PreferencesBehaviorTab::BehaviorPrefTr( + "Panels will be considered focused when the mouse cursor is over them without having to click them.")); + hover_focus_->setChecked(OLIVE_CONFIG("HoverFocus").toBool()); + behavior_layout->addWidget(hover_focus_); + + slider_ladder_ = new QCheckBox(PreferencesBehaviorTab::BehaviorPrefTr("Enable slider ladder")); + slider_ladder_->setChecked(OLIVE_CONFIG("UseSliderLadders").toBool()); + behavior_layout->addWidget(slider_ladder_); + + scroll_zooms_ = new QCheckBox(PreferencesBehaviorTab::BehaviorPrefTr("Scrolling zooms by default")); + scroll_zooms_->setToolTip(PreferencesBehaviorTab::BehaviorPrefTr( + "By default, scrolling will move the view around, and holding Ctrl/Cmd will make it zoom instead. " + "Enabling this will switch those, scrolling will zoom by default, and holding Ctrl/Cmd will move the view instead.")); + scroll_zooms_->setChecked(OLIVE_CONFIG("ScrollZooms").toBool()); + behavior_layout->addWidget(scroll_zooms_); + } + layout->addStretch(); } @@ -201,6 +225,10 @@ void PreferencesGeneralTab::Accept(MultiUndoCommand *command) QVariant::fromValue(autorecovery_maximum_->GetValue()); Core::instance()->SetAutorecoveryInterval( autorecovery_interval_->GetValue()); + + OLIVE_CONFIG("HoverFocus") = hover_focus_->isChecked(); + OLIVE_CONFIG("UseSliderLadders") = slider_ladder_->isChecked(); + OLIVE_CONFIG("ScrollZooms") = scroll_zooms_->isChecked(); } void PreferencesGeneralTab::AddLanguage(const QString &locale_name) diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.h b/app/dialog/preferences/tabs/preferencesgeneraltab.h index 841a36db0..89f6fdd50 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.h +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.h @@ -57,6 +57,10 @@ private: IntegerSlider *autorecovery_interval_; IntegerSlider *autorecovery_maximum_; + + QCheckBox *hover_focus_; + QCheckBox *slider_ladder_; + QCheckBox *scroll_zooms_; }; } diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index f13df8bbd..1c42a327b 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -557,7 +557,9 @@ void PreviewAutoCacher::TryRender() copy, QtUtils::ValueToPtr(t->property("viewer")), t->property("time").value(), nullptr, t->property("dry").toBool()); - video_immediate_passthroughs_[watcher].append(t); + if (watcher) { + video_immediate_passthroughs_[watcher].append(t); + } } else { qWarning() << "Failed to find copied node for SFR ticket, requeueing"; single_frame_render_ = t; @@ -709,6 +711,12 @@ RenderTicketWatcher *PreviewAutoCacher::RenderFrame(Node *node, watcher->SetTicket(RenderManager::instance()->RenderFrame(rvp)); + // If the ticket finished synchronously, VideoRendered has already deleted the + // watcher. The caller must not use this pointer in that case. + if (!running_video_tasks_.contains(watcher)) { + return nullptr; + } + return watcher; } diff --git a/app/render/renderticket.cpp b/app/render/renderticket.cpp index 63faf4040..991ba0168 100644 --- a/app/render/renderticket.cpp +++ b/app/render/renderticket.cpp @@ -157,9 +157,11 @@ void RenderTicketWatcher::SetTicket(RenderTicketPtr ticket) &RenderTicketWatcher::TicketFinished); if (!ticket_->IsRunning(false) && ticket_->GetFinishCount(false) > 0) { - // Ticket has already finished before, so we emit a signal - locker.unlock(); - TicketFinished(); + // Ticket has already finished before, so we emit a signal asynchronously + // to avoid deleting this watcher before the caller has a chance to use + // the returned pointer. + QMetaObject::invokeMethod(this, &RenderTicketWatcher::TicketFinished, + Qt::QueuedConnection); } } diff --git a/app/ts/zh_CN.ts b/app/ts/zh_CN.ts index 7768edd03..efa8b1fad 100644 --- a/app/ts/zh_CN.ts +++ b/app/ts/zh_CN.ts @@ -5919,7 +5919,12 @@ Do you wish to paste values onto the existing nodes or paste new nodes? 在查看器中打开 - + + Show in Parameter Editor + 在参数编辑器中显示 + + + P&roperties 属性(&R) @@ -6438,7 +6443,7 @@ This is equivalent to multiplying a video by a number between 0.0 and 1.0. Enable audio scrubbing - 启用音频清理 + 启用音频搓擦 @@ -6545,11 +6550,7 @@ This is equivalent to multiplying a video by a number between 0.0 and 1.0.Enable slider ladder 启用滑块阶梯 - - - Setting - - + Scrolling zooms by default @@ -6561,30 +6562,30 @@ This is equivalent to multiplying a video by a number between 0.0 and 1.0.默认情况下,滚动将移动视图,按住Ctrl/Cmd将使其变为缩放。启用此选项将切换这些视图,默认情况下滚动将缩放,而按住Ctrl/Cmd将移动视图。 - + Rendering - + 渲染 - + Graphics Backend - + 图形后端 - + Selects the graphics API Oak should request on next launch. Vulkan is experimental: on most systems it will fall back to OpenGL or use a prototype Vulkan path that is not yet fully validated. - + 选择 Oak 在下次启动时应请求的图形 API。Vulkan 是实验性的:在大多数系统上它会回退到 OpenGL,或使用尚未完全验证的原型 Vulkan 路径。 - - + + OpenGL - + OpenGL - + Vulkan (experimental) - + Vulkan(实验性) @@ -6611,24 +6612,40 @@ This is equivalent to multiplying a video by a number between 0.0 and 1.0. - Behavior - 行为 - - - - Disk - 磁盘 - - - Audio 音频 - - Keyboard - 键盘 + + Timeline + 时间线 + + + Playback + 回放 + + + + Project + 项目 + + + + Nodes + 节点 + + + + Rendering + 渲染 + + + + Disk + 磁盘 + + olive::PreferencesDiskTab diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 07d816c86..48281d378 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -58,6 +58,7 @@ NodeView::NodeView(QWidget *parent) , overlay_view_(nullptr) , scale_(1.0) , dont_emit_selection_signals_(false) + , show_in_param_editor_action_(nullptr) { setScene(&scene_); SetDefaultDragMode(RubberBandDrag); @@ -73,6 +74,12 @@ NodeView::NodeView(QWidget *parent) SetFlowDirection(NodeViewCommon::kLeftToRight); + show_in_param_editor_action_ = new QAction(tr("Show in Parameter Editor"), this); + Menu::ConformItem(show_in_param_editor_action_, QStringLiteral("shownodeparams"), QKeySequence(tr("Shift+P"))); + show_in_param_editor_action_->setShortcutContext(Qt::WindowShortcut); + addAction(show_in_param_editor_action_); + connect(show_in_param_editor_action_, &QAction::triggered, this, &NodeView::ShowSelectedNodeInParamEditor); + UpdateSceneBoundingRect(); connect(&scene_, &QGraphicsScene::changed, this, &NodeView::UpdateSceneBoundingRect); @@ -643,19 +650,6 @@ void NodeView::mouseDoubleClickEvent(QMouseEvent *event) dynamic_cast(itemAt(event->pos())); if (item_at_cursor) { item_at_cursor->ToggleExpanded(); - if (PanelManager::instance()) { - if (PanelWidget *panel = PanelManager::instance() - ->GetPanelWithName( - QStringLiteral("ParamPanel"))) { - panel->show(); - panel->raise(); - panel->setFocus(Qt::OtherFocusReason); - } - } - - // Scroll the parameter editor to this node - emit NodeSelectionChangedWithContexts( - { { item_at_cursor->GetNode(), item_at_cursor->GetContext() } }); } } } @@ -865,6 +859,14 @@ void NodeView::ShowContextMenu(const QPoint &pos) m.addSeparator(); + // Show in Parameter Editor + QAction *show_in_param_editor_action = m.addAction( + tr("Show in Parameter Editor")); + show_in_param_editor_action->setShortcut( + show_in_param_editor_action_->shortcut()); + connect(show_in_param_editor_action, &QAction::triggered, this, + &NodeView::ShowSelectedNodeInParamEditor); + // Properties QAction *properties_action = m.addAction(tr("P&roperties")); connect(properties_action, &QAction::triggered, this, @@ -1641,6 +1643,30 @@ void NodeView::ShowNodeProperties() } } +void NodeView::ShowSelectedNodeInParamEditor() +{ + if (selected_nodes_.isEmpty()) { + return; + } + + NodeViewItem *item = scene_.GetSelectedItems().first(); + if (!item || !item->GetNode()) { + return; + } + + if (PanelManager::instance()) { + if (PanelWidget *panel = PanelManager::instance()->GetPanelWithName( + QStringLiteral("ParamPanel"))) { + panel->show(); + panel->raise(); + panel->setFocus(Qt::OtherFocusReason); + } + } + + emit NodeSelectionChangedWithContexts( + { { item->GetNode(), item->GetContext() } }); +} + void NodeView::LabelSelectedNodes() { Core::instance()->LabelNodes(selected_nodes_); diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index b28e60326..8b4ec47cb 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -241,6 +241,8 @@ private: bool dont_emit_selection_signals_; + QAction *show_in_param_editor_action_; + static const double kMinimumScale; static const int kMaximumContexts; @@ -287,6 +289,8 @@ private slots: void ShowNodeProperties(); + void ShowSelectedNodeInParamEditor(); + void ItemAboutToBeDeleted(NodeViewItem *item); void CloseOverlay();