From 026ff94b5e639315f83f4dbc282c536df1776ff9 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Mon, 20 Jul 2026 02:05:01 +0800 Subject: [PATCH] refactor: invert the last engine-to-UI dependencies - NodeFactory's menu creation moves to UI-side widget/menu/factorymenu (the factory only exposes its node library read-only now) - DiskManager's cache-settings dialog is created through a registered std::function handler (registered by Core at startup) - OlivePluginInstance creates progress UIs through a PluginProgressReporter interface (Null fallback headless) and queries the active viewer through a provider callback, both registered by Core - factory.h, diskmanager and pluginSupport no longer reference any widget//dialog//panel//window headers or classes --- app/core.cpp | 45 ++++++ app/dialog/progress/CMakeLists.txt | 2 + .../progress/pluginprogressdialogreporter.cpp | 64 ++++++++ .../progress/pluginprogressdialogreporter.h | 61 ++++++++ app/node/factory.cpp | 108 -------------- app/node/factory.h | 22 +-- app/pluginSupport/CMakeLists.txt | 2 + app/pluginSupport/oliveplugininstance.cpp | 60 +++----- app/pluginSupport/oliveplugininstance.h | 18 ++- app/pluginSupport/pluginprogressreporter.cpp | 69 +++++++++ app/pluginSupport/pluginprogressreporter.h | 84 +++++++++++ app/render/diskmanager.cpp | 18 ++- app/render/diskmanager.h | 17 +++ app/widget/menu/CMakeLists.txt | 2 + app/widget/menu/factorymenu.cpp | 137 ++++++++++++++++++ app/widget/menu/factorymenu.h | 62 ++++++++ app/widget/nodecombobox/nodecombobox.cpp | 5 +- .../nodeparamview/nodeparamviewcontext.cpp | 6 +- app/widget/nodeview/nodeview.cpp | 6 +- app/widget/toolbar/toolbar.cpp | 6 +- tests/gtest/project_factory_test.cpp | 15 +- 21 files changed, 630 insertions(+), 179 deletions(-) create mode 100644 app/dialog/progress/pluginprogressdialogreporter.cpp create mode 100644 app/dialog/progress/pluginprogressdialogreporter.h create mode 100644 app/pluginSupport/pluginprogressreporter.cpp create mode 100644 app/pluginSupport/pluginprogressreporter.h create mode 100644 app/widget/menu/factorymenu.cpp create mode 100644 app/widget/menu/factorymenu.h diff --git a/app/core.cpp b/app/core.cpp index aa98a8cf5..977f17368 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -44,11 +44,13 @@ #include "config/config.h" #include "dialog/about/about.h" #include "dialog/autorecovery/autorecoverydialog.h" +#include "dialog/diskcache/diskcachedialog.h" #include "dialog/export/export.h" #include "dialog/footagerelink/footagerelinkdialog.h" #ifdef USE_OTIO #include "dialog/otioproperties/otiopropertiesdialog.h" #endif +#include "dialog/progress/pluginprogressdialogreporter.h" #include "dialog/projectproperties/projectproperties.h" #include "dialog/sequence/sequence.h" #include "dialog/task/task.h" @@ -56,7 +58,11 @@ #include "node/nodeundo.h" #include "panel/panelmanager.h" #include "panel/project/project.h" +#include "panel/timebased/timebased.h" +#include "panel/timeline/timeline.h" #include "panel/viewer/viewer.h" +#include "pluginSupport/oliveplugininstance.h" +#include "pluginSupport/pluginprogressreporter.h" #include "render/diskmanager.h" #ifdef USE_OTIO #include "task/project/loadotio/loadotio.h" @@ -103,6 +109,45 @@ Core::Core(const CoreParams ¶ms) return DialogImportOTIOShow(sequences); }); #endif + + // Disk cache settings dialog (engine -> UI) + DiskManager::set_show_disk_cache_settings_handler( + [](DiskCacheFolder *folder, QWidget *parent) { + DiskCacheDialog d(folder, parent); + d.exec(); + }); + + // OFX plugin progress dialog (engine -> UI) + plugin::set_plugin_progress_reporter_factory( + [](const QString &message, + const QString &title) -> plugin::PluginProgressReporter * { + return new PluginProgressDialogReporter(message, title); + }); + + // OFX timeline suite: resolve the active viewer through the panels + plugin::set_active_viewer_provider([]() -> ViewerOutput * { + PanelManager *manager = PanelManager::instance(); + if (!manager) { + return nullptr; + } + + if (auto *time_panel = + manager->most_recently_focused()) { + if (time_panel->get_connected_viewer()) { + return time_panel->get_connected_viewer(); + } + } + + QList timelines = + manager->get_panels_of_type(); + for (TimelinePanel *panel : timelines) { + if (panel && panel->get_connected_viewer()) { + return panel->get_connected_viewer(); + } + } + + return nullptr; + }); } void Core::start() diff --git a/app/dialog/progress/CMakeLists.txt b/app/dialog/progress/CMakeLists.txt index c1c880a66..5470c2acd 100644 --- a/app/dialog/progress/CMakeLists.txt +++ b/app/dialog/progress/CMakeLists.txt @@ -16,6 +16,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + dialog/progress/pluginprogressdialogreporter.h + dialog/progress/pluginprogressdialogreporter.cpp dialog/progress/progress.h dialog/progress/progress.cpp PARENT_SCOPE diff --git a/app/dialog/progress/pluginprogressdialogreporter.cpp b/app/dialog/progress/pluginprogressdialogreporter.cpp new file mode 100644 index 000000000..ded2e38af --- /dev/null +++ b/app/dialog/progress/pluginprogressdialogreporter.cpp @@ -0,0 +1,64 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "pluginprogressdialogreporter.h" + +#include "dialog/progress/progress.h" + +namespace olive +{ + +PluginProgressDialogReporter::PluginProgressDialogReporter( + const QString &message, const QString &title) + : dialog_(new ProgressDialog(message, title, nullptr)) +{ + dialog_->setAttribute(Qt::WA_DeleteOnClose); + connect(dialog_, &ProgressDialog::cancelled, this, + &PluginProgressReporter::cancelled); +} + +PluginProgressDialogReporter::~PluginProgressDialogReporter() +{ + delete dialog_; +} + +void PluginProgressDialogReporter::set_progress(double value) +{ + if (dialog_) { + dialog_->set_progress(value); + } +} + +void PluginProgressDialogReporter::show() +{ + if (dialog_) { + dialog_->show(); + } +} + +void PluginProgressDialogReporter::close() +{ + if (dialog_) { + dialog_->close(); + } +} + +} diff --git a/app/dialog/progress/pluginprogressdialogreporter.h b/app/dialog/progress/pluginprogressdialogreporter.h new file mode 100644 index 000000000..2f9a05da7 --- /dev/null +++ b/app/dialog/progress/pluginprogressdialogreporter.h @@ -0,0 +1,61 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_PLUGINPROGRESSDIALOGREPORTER_H +#define OAK_PLUGINPROGRESSDIALOGREPORTER_H + +#include + +#include "pluginSupport/pluginprogressreporter.h" + +namespace olive +{ + +class ProgressDialog; + +/** + * @brief PluginProgressReporter implementation wrapping a ProgressDialog + * + * UI-side reporter created through the plugin progress reporter factory. + * Forwards progress calls to the dialog and the dialog's cancelled() signal + * back to the engine. The dialog is deleted on close; the reporter itself + * is destroyed by the engine with deleteLater(). + */ +class PluginProgressDialogReporter : public plugin::PluginProgressReporter { + Q_OBJECT +public: + PluginProgressDialogReporter(const QString &message, const QString &title); + + virtual ~PluginProgressDialogReporter() override; + + virtual void set_progress(double value) override; + + virtual void show() override; + + virtual void close() override; + +private: + QPointer dialog_; +}; + +} + +#endif // OAK_PLUGINPROGRESSDIALOGREPORTER_H diff --git a/app/node/factory.cpp b/app/node/factory.cpp index 1590ad220..ed0200051 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -21,7 +21,6 @@ #include "factory.h" -#include #include #include "audio/pan/pan.h" @@ -105,113 +104,6 @@ void NodeFactory::destroy() library.clear(); } -Menu *NodeFactory::create_menu(QWidget *parent, bool create_none_item, - Node::CategoryID restrict_to, - uint64_t restrict_flags) -{ - Menu *menu = new Menu(parent); - menu->setToolTipsVisible(true); - - for (int i = 0; i < library.size(); i++) { - Node *n = library.at(i); - - if (restrict_to != Node::k_category_unknown && - !n->category().contains(restrict_to)) { - // Skip this node - continue; - } - - if (restrict_flags && !(n->get_flags() & restrict_flags)) { - continue; - } - - if (n->get_flags() & Node::k_dont_show_in_create_menu) { - continue; - } - - // Make sure nodes are up-to-date with the current translation - n->retranslate(); - - QString category_name = Node::get_category_name( - n->category().isEmpty() ? Node::k_category_unknown : - n->category().first()); - - // Find or create top-level category menu - Menu *top_menu = nullptr; - QList menu_actions = menu->actions(); - foreach (QAction *action, menu_actions) { - if (action->menu() && action->menu()->title() == category_name) { - top_menu = static_cast(action->menu()); - break; - } - } - if (!top_menu) { - top_menu = new Menu(category_name, menu); - menu->insert_alphabetically(top_menu); - } - - // Determine final destination (support secondary grouping) - Menu *destination = top_menu; - QString sub = n->sub_category(); - if (!sub.isEmpty() && n->category().contains(Node::k_category_open_fx)) { - QList sub_actions = top_menu->actions(); - foreach (QAction *action, sub_actions) { - if (action->menu() && action->menu()->title() == sub) { - destination = static_cast(action->menu()); - break; - } - } - if (destination == top_menu) { - destination = new Menu(sub, top_menu); - top_menu->insert_alphabetically(destination); - } - } - - // Add entry to menu - QAction *a = destination->insert_alphabetically(n->name()); - a->setData(i); - a->setToolTip(n->description()); - } - - if (create_none_item) { - QAction *none_item = new QAction( - QCoreApplication::translate("NodeFactory", "None"), menu); - - none_item->setData(-1); - - if (menu->actions().isEmpty()) { - menu->addAction(none_item); - } else { - QAction *separator = menu->insertSeparator(menu->actions().first()); - menu->insertAction(separator, none_item); - } - } - - return menu; -} - -Node *NodeFactory::CreateFromMenuAction(QAction *action) -{ - int index = action->data().toInt(); - - if (index == -1) { - return nullptr; - } - - return library.at(index)->copy(); -} - -QString NodeFactory::GetIDFromMenuAction(QAction *action) -{ - int index = action->data().toInt(); - - if (index == -1) { - return QString(); - } - - return library.at(action->data().toInt())->id(); -} - QString NodeFactory::get_name_from_id(const QString &id) { if (!id.isEmpty()) { diff --git a/app/node/factory.h b/app/node/factory.h index a7b6de0d4..0f98a8ef3 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -25,7 +25,6 @@ #include #include "node.h" -#include "widget/menu/menu.h" namespace olive { @@ -97,15 +96,6 @@ public: static void destroy(); - static Menu * - create_menu(QWidget *parent, bool create_none_item = false, - Node::CategoryID restrict_to = Node::k_category_unknown, - uint64_t restrict_flags = 0); - - static Node *CreateFromMenuAction(QAction *action); - - static QString GetIDFromMenuAction(QAction *action); - static QString get_name_from_id(const QString &id); static Node *create_from_id(const QString &id); @@ -113,6 +103,18 @@ public: static Node *create_from_factory_index(const InternalID &id); + /** + * @brief Access the internal node library + * + * Exposed for the UI layer (e.g. widget/menu/factorymenu), which builds + * node creation menus from the library. The library itself stays + * UI-independent. + */ + static const QList &get_library() + { + return library; + } + private: static QList library; }; diff --git a/app/pluginSupport/CMakeLists.txt b/app/pluginSupport/CMakeLists.txt index c2f67121a..3ecd77cdb 100644 --- a/app/pluginSupport/CMakeLists.txt +++ b/app/pluginSupport/CMakeLists.txt @@ -3,6 +3,8 @@ target_sources(libolive-editor PRIVATE olivehost.cpp oliveplugininstance.h oliveplugininstance.cpp + pluginprogressreporter.h + pluginprogressreporter.cpp oliveclip.cpp oliveclip.h paraminstance.cpp diff --git a/app/pluginSupport/oliveplugininstance.cpp b/app/pluginSupport/oliveplugininstance.cpp index a6d0bea14..4443d9d16 100644 --- a/app/pluginSupport/oliveplugininstance.cpp +++ b/app/pluginSupport/oliveplugininstance.cpp @@ -23,11 +23,8 @@ #include "ofxMessage.h" #include "common/current.h" #include "coreengine.h" -#include "dialog/progress/progress.h" +#include "pluginprogressreporter.h" #include "node/output/viewer/viewer.h" -#include "panel/panelmanager.h" -#include "panel/timebased/timebased.h" -#include "panel/timeline/timeline.h" #include #include @@ -125,31 +122,19 @@ private: bool skip_first_redo_ = true; }; +ActiveViewerProvider active_viewer_provider_; + ViewerOutput *get_active_viewer_output() { - PanelManager *manager = PanelManager::instance(); - if (!manager) { - return nullptr; - } - - if (auto *time_panel = manager->most_recently_focused()) { - if (time_panel->get_connected_viewer()) { - return time_panel->get_connected_viewer(); - } - } - - QList timelines = - manager->get_panels_of_type(); - for (TimelinePanel *panel : timelines) { - if (panel && panel->get_connected_viewer()) { - return panel->get_connected_viewer(); - } - } - - return nullptr; + return active_viewer_provider_ ? active_viewer_provider_() : nullptr; } } // namespace +void set_active_viewer_provider(ActiveViewerProvider provider) +{ + active_viewer_provider_ = std::move(provider); +} + const std::string &OlivePluginInstance::getDefaultOutputFielding() const { return field_order_for_params(params_); @@ -452,21 +437,20 @@ void OlivePluginInstance::progressStart(const std::string &message, return; } - if (progress_dialog_) { - progress_dialog_->close(); - progress_dialog_->deleteLater(); + if (progress_reporter_) { + progress_reporter_->close(); + progress_reporter_->deleteLater(); } QString dialog_message = message.empty() ? QStringLiteral("Processing...") : QString::fromStdString(message); - progress_dialog_ = new ::olive::ProgressDialog( - dialog_message, QStringLiteral("OpenFX"), nullptr); - progress_dialog_->setAttribute(Qt::WA_DeleteOnClose); - QObject::connect(progress_dialog_, &::olive::ProgressDialog::cancelled, - progress_dialog_, + progress_reporter_ = create_plugin_progress_reporter( + dialog_message, QStringLiteral("OpenFX")); + QObject::connect(progress_reporter_, &PluginProgressReporter::cancelled, + progress_reporter_, [this]() { progress_cancelled_ = true; }); - progress_dialog_->show(); + progress_reporter_->show(); } void OlivePluginInstance::progressEnd() @@ -474,9 +458,9 @@ void OlivePluginInstance::progressEnd() progress_active_ = false; progress_cancelled_ = false; - if (progress_dialog_) { - progress_dialog_->close(); - progress_dialog_->deleteLater(); + if (progress_reporter_) { + progress_reporter_->close(); + progress_reporter_->deleteLater(); } } @@ -486,9 +470,9 @@ bool OlivePluginInstance::progressUpdate(double t) return true; } - if (progress_dialog_) { + if (progress_reporter_) { double clamped = qBound(0.0, t, 1.0); - progress_dialog_->set_progress(clamped); + progress_reporter_->set_progress(clamped); } return !progress_cancelled_; diff --git a/app/pluginSupport/oliveplugininstance.h b/app/pluginSupport/oliveplugininstance.h index fd8208c92..03e02c4f9 100644 --- a/app/pluginSupport/oliveplugininstance.h +++ b/app/pluginSupport/oliveplugininstance.h @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -43,15 +44,28 @@ inline bool is_gui_thread() } return true; } -class ProgressDialog; +class ViewerOutput; namespace plugin { class PluginNode; +class PluginProgressReporter; enum class ErrorType { error, warning, message }; struct PersistentErrors { ErrorType type; QString message; }; + +/** + * @brief Provider returning the currently active viewer + * + * Registered by the UI layer, which resolves the viewer through the panel + * manager. Without a provider, the OFX timeline suite falls back to its + * safe defaults (current time 0, empty bounds, seeking does nothing). + */ +using ActiveViewerProvider = std::function; + +void set_active_viewer_provider(ActiveViewerProvider provider); + class OlivePluginInstance : public OFX::Host::ImageEffect::Instance { public: OlivePluginInstance(OFX::Host::ImageEffect::ImageEffectPlugin *plugin, @@ -214,7 +228,7 @@ private: QString edit_label_; QString edit_first_label_; int edit_param_count_ = 0; - QPointer progress_dialog_; + QPointer progress_reporter_; bool progress_cancelled_ = false; bool progress_active_ = false; bool open_gl_enabled_ = false; diff --git a/app/pluginSupport/pluginprogressreporter.cpp b/app/pluginSupport/pluginprogressreporter.cpp new file mode 100644 index 000000000..8104bb806 --- /dev/null +++ b/app/pluginSupport/pluginprogressreporter.cpp @@ -0,0 +1,69 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "pluginprogressreporter.h" + +namespace olive +{ +namespace plugin +{ +namespace +{ + +/** + * @brief No-op reporter used when no UI factory is registered + * + * Never emits cancelled(), so processing always continues. + */ +class NullPluginProgressReporter : public PluginProgressReporter { +public: + void set_progress(double value) override + { + (void)value; + } + + void show() override + { + } + + void close() override + { + } +}; + +PluginProgressReporterFactory reporter_factory_; + +} + +void set_plugin_progress_reporter_factory( + PluginProgressReporterFactory factory) +{ + reporter_factory_ = std::move(factory); +} + +PluginProgressReporter * +create_plugin_progress_reporter(const QString &message, const QString &title) +{ + if (reporter_factory_) { + return reporter_factory_(message, title); + } + + return new NullPluginProgressReporter(); +} + +} +} diff --git a/app/pluginSupport/pluginprogressreporter.h b/app/pluginSupport/pluginprogressreporter.h new file mode 100644 index 000000000..cd467612e --- /dev/null +++ b/app/pluginSupport/pluginprogressreporter.h @@ -0,0 +1,84 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2025 Olive CE Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef OAK_PLUGIN_PROGRESS_REPORTER_H +#define OAK_PLUGIN_PROGRESS_REPORTER_H + +#include +#include + +#include + +namespace olive +{ +namespace plugin +{ + +/** + * @brief UI-independent interface for reporting plugin progress + * + * The engine cannot show UI itself, so OFX progress reporting goes through + * this interface. The UI layer registers a factory (see + * set_plugin_progress_reporter_factory()) that creates a reporter wrapping a + * ProgressDialog; without a factory, a no-op reporter is used instead. + */ +class PluginProgressReporter : public QObject { + Q_OBJECT +public: + explicit PluginProgressReporter(QObject *parent = nullptr) + : QObject(parent) + { + } + + virtual ~PluginProgressReporter() override = default; + + virtual void set_progress(double value) = 0; + + virtual void show() = 0; + + virtual void close() = 0; + +signals: + void cancelled(); +}; + +/** + * @brief Factory creating a PluginProgressReporter for a progress session + * + * Registered by the UI layer at startup. The caller takes ownership of the + * returned reporter. + */ +using PluginProgressReporterFactory = + std::function; + +void set_plugin_progress_reporter_factory( + PluginProgressReporterFactory factory); + +/** + * @brief Create a progress reporter through the registered factory + * + * Without a factory, returns a no-op reporter so engine code can run + * headless. The caller takes ownership of the returned reporter. + */ +PluginProgressReporter * +create_plugin_progress_reporter(const QString &message, const QString &title); + +} +} + +#endif // OAK_PLUGIN_PROGRESS_REPORTER_H diff --git a/app/render/diskmanager.cpp b/app/render/diskmanager.cpp index c72bbfb06..1cc317ddc 100644 --- a/app/render/diskmanager.cpp +++ b/app/render/diskmanager.cpp @@ -31,13 +31,15 @@ #include "common/filefunctions.h" #include "config/config.h" #include "coreengine.h" -#include "dialog/diskcache/diskcachedialog.h" namespace olive { DiskManager *DiskManager::instance_ = nullptr; +DiskManager::ShowDiskCacheSettingsHandler + DiskManager::show_disk_cache_settings_handler_; + DiskManager::DiskManager() { // Add default cache location @@ -181,11 +183,21 @@ QString DiskManager::get_default_disk_cache_path() .filePath("mediacache"); } +void DiskManager::set_show_disk_cache_settings_handler( + ShowDiskCacheSettingsHandler handler) +{ + show_disk_cache_settings_handler_ = std::move(handler); +} + void DiskManager::show_disk_cache_settings_dialog(DiskCacheFolder *folder, QWidget *parent) { - DiskCacheDialog d(folder, parent); - d.exec(); + if (show_disk_cache_settings_handler_) { + show_disk_cache_settings_handler_(folder, parent); + return; + } + + qWarning() << "No disk cache settings dialog handler registered, skipping"; } void DiskManager::show_disk_cache_settings_dialog(const QString &path, diff --git a/app/render/diskmanager.h b/app/render/diskmanager.h index 75d1e41c5..eff893d23 100644 --- a/app/render/diskmanager.h +++ b/app/render/diskmanager.h @@ -27,6 +27,8 @@ #include #include +#include + #include "common/define.h" #include "node/project.h" @@ -143,6 +145,19 @@ public: static QString get_default_disk_cache_path(); + /** + * @brief Handler showing the disk cache settings dialog for a folder + * + * Registered by the UI layer (e.g. a DiskCacheDialog-based + * implementation), since the engine cannot show dialogs itself. Without + * a handler, the request is logged and skipped. + */ + using ShowDiskCacheSettingsHandler = + std::function; + + static void set_show_disk_cache_settings_handler( + ShowDiskCacheSettingsHandler handler); + void show_disk_cache_settings_dialog(DiskCacheFolder *folder, QWidget *parent); void show_disk_cache_settings_dialog(const QString &path, QWidget *parent); @@ -165,6 +180,8 @@ private: static DiskManager *instance_; + static ShowDiskCacheSettingsHandler show_disk_cache_settings_handler_; + QVector open_folders_; }; diff --git a/app/widget/menu/CMakeLists.txt b/app/widget/menu/CMakeLists.txt index 4620c05ea..8ffe2e0c1 100644 --- a/app/widget/menu/CMakeLists.txt +++ b/app/widget/menu/CMakeLists.txt @@ -16,6 +16,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + widget/menu/factorymenu.cpp + widget/menu/factorymenu.h widget/menu/menu.cpp widget/menu/menu.h widget/menu/menushared.cpp diff --git a/app/widget/menu/factorymenu.cpp b/app/widget/menu/factorymenu.cpp new file mode 100644 index 000000000..9da24a9fc --- /dev/null +++ b/app/widget/menu/factorymenu.cpp @@ -0,0 +1,137 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "factorymenu.h" + +#include + +namespace olive +{ + +Menu *create_node_menu(QWidget *parent, bool create_none_item, + Node::CategoryID restrict_to, uint64_t restrict_flags) +{ + const QList &library = NodeFactory::get_library(); + + Menu *menu = new Menu(parent); + menu->setToolTipsVisible(true); + + for (int i = 0; i < library.size(); i++) { + Node *n = library.at(i); + + if (restrict_to != Node::k_category_unknown && + !n->category().contains(restrict_to)) { + // Skip this node + continue; + } + + if (restrict_flags && !(n->get_flags() & restrict_flags)) { + continue; + } + + if (n->get_flags() & Node::k_dont_show_in_create_menu) { + continue; + } + + // Make sure nodes are up-to-date with the current translation + n->retranslate(); + + QString category_name = Node::get_category_name( + n->category().isEmpty() ? Node::k_category_unknown : + n->category().first()); + + // Find or create top-level category menu + Menu *top_menu = nullptr; + QList menu_actions = menu->actions(); + foreach (QAction *action, menu_actions) { + if (action->menu() && action->menu()->title() == category_name) { + top_menu = static_cast(action->menu()); + break; + } + } + if (!top_menu) { + top_menu = new Menu(category_name, menu); + menu->insert_alphabetically(top_menu); + } + + // Determine final destination (support secondary grouping) + Menu *destination = top_menu; + QString sub = n->sub_category(); + if (!sub.isEmpty() && n->category().contains(Node::k_category_open_fx)) { + QList sub_actions = top_menu->actions(); + foreach (QAction *action, sub_actions) { + if (action->menu() && action->menu()->title() == sub) { + destination = static_cast(action->menu()); + break; + } + } + if (destination == top_menu) { + destination = new Menu(sub, top_menu); + top_menu->insert_alphabetically(destination); + } + } + + // Add entry to menu + QAction *a = destination->insert_alphabetically(n->name()); + a->setData(i); + a->setToolTip(n->description()); + } + + if (create_none_item) { + QAction *none_item = new QAction( + QCoreApplication::translate("NodeFactory", "None"), menu); + + none_item->setData(-1); + + if (menu->actions().isEmpty()) { + menu->addAction(none_item); + } else { + QAction *separator = menu->insertSeparator(menu->actions().first()); + menu->insertAction(separator, none_item); + } + } + + return menu; +} + +Node *create_node_from_menu_action(QAction *action) +{ + int index = action->data().toInt(); + + if (index == -1) { + return nullptr; + } + + return NodeFactory::get_library().at(index)->copy(); +} + +QString get_node_id_from_menu_action(QAction *action) +{ + int index = action->data().toInt(); + + if (index == -1) { + return QString(); + } + + return NodeFactory::get_library().at(action->data().toInt())->id(); +} + +} diff --git a/app/widget/menu/factorymenu.h b/app/widget/menu/factorymenu.h new file mode 100644 index 000000000..2ff9fd0f5 --- /dev/null +++ b/app/widget/menu/factorymenu.h @@ -0,0 +1,62 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_FACTORYMENU_H +#define OAK_FACTORYMENU_H + +#include + +#include "node/factory.h" +#include "widget/menu/menu.h" + +namespace olive +{ + +/** + * @brief Create a menu of all available nodes, grouped by category + * + * UI-side counterpart of the node library: NodeFactory stays UI-independent + * while this builds an olive::Menu from NodeFactory::get_library(). Each + * leaf action carries the node's library index in its data, so it can be + * resolved back with create_node_from_menu_action() or + * get_node_id_from_menu_action(). + */ +Menu *create_node_menu(QWidget *parent, bool create_none_item = false, + Node::CategoryID restrict_to = Node::k_category_unknown, + uint64_t restrict_flags = 0); + +/** + * @brief Create a node from an action of a menu built by create_node_menu() + * + * Returns nullptr for the "None" item. + */ +Node *create_node_from_menu_action(QAction *action); + +/** + * @brief Get the node ID from an action of a menu built by create_node_menu() + * + * Returns an empty string for the "None" item. + */ +QString get_node_id_from_menu_action(QAction *action); + +} + +#endif // OAK_FACTORYMENU_H diff --git a/app/widget/nodecombobox/nodecombobox.cpp b/app/widget/nodecombobox/nodecombobox.cpp index e8ec76913..6259c29ec 100644 --- a/app/widget/nodecombobox/nodecombobox.cpp +++ b/app/widget/nodecombobox/nodecombobox.cpp @@ -27,6 +27,7 @@ #include "node/factory.h" #include "ui/icons/icons.h" +#include "widget/menu/factorymenu.h" #include "widget/menu/menu.h" namespace olive @@ -39,12 +40,12 @@ NodeComboBox::NodeComboBox(QWidget *parent) void NodeComboBox::showPopup() { - Menu *m = NodeFactory::create_menu(this, true); + Menu *m = create_node_menu(this, true); QAction *selected = m->exec(parentWidget()->mapToGlobal(pos())); if (selected) { - QString new_id = NodeFactory::GetIDFromMenuAction(selected); + QString new_id = get_node_id_from_menu_action(selected); set_node_internal(new_id, true); } diff --git a/app/widget/nodeparamview/nodeparamviewcontext.cpp b/app/widget/nodeparamview/nodeparamviewcontext.cpp index b16b253db..5422ce201 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.cpp +++ b/app/widget/nodeparamview/nodeparamviewcontext.cpp @@ -24,8 +24,8 @@ #include #include "node/block/clip/clip.h" -#include "node/factory.h" #include "node/nodeundo.h" +#include "widget/menu/factorymenu.h" namespace olive { @@ -147,7 +147,7 @@ void NodeParamViewContext::add_effect_button_clicked() } Menu *m = - NodeFactory::create_menu(this, false, Node::k_category_unknown, flag); + create_node_menu(this, false, Node::k_category_unknown, flag); connect(m, &Menu::triggered, this, &NodeParamViewContext::add_effect_menu_item_triggered); m->exec(QCursor::pos()); @@ -156,7 +156,7 @@ void NodeParamViewContext::add_effect_button_clicked() void NodeParamViewContext::add_effect_menu_item_triggered(QAction *a) { - Node *n = NodeFactory::CreateFromMenuAction(a); + Node *n = create_node_from_menu_action(a); if (n) { NodeInput new_node_input = n->get_effect_input(); diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index df7ee85be..86d3e6940 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -31,13 +31,13 @@ #include "node/audio/volume/volume.h" #include "node/distort/transform/transformdistortnode.h" -#include "node/factory.h" #include "node/group/group.h" #include "node/nodeundo.h" #include "node/project/serializer/serializer.h" #include "panel/panelmanager.h" #include "node/traverser.h" #include "ui/icons/icons.h" +#include "widget/menu/factorymenu.h" #include "widget/menu/menushared.h" #include "widget/timebased/timebasedview.h" @@ -936,7 +936,7 @@ void NodeView::show_context_menu(const QPoint &pos) void NodeView::create_node_slot(QAction *action) { - Node *new_node = NodeFactory::CreateFromMenuAction(action); + Node *new_node = create_node_from_menu_action(action); if (new_node) { NodeViewItem *new_item = new NodeViewItem(new_node, nullptr); @@ -1381,7 +1381,7 @@ bool NodeView::get_assumed_position_for_selected_node(Node *node, Menu *NodeView::create_add_menu(Menu *parent) { - Menu *add_menu = NodeFactory::create_menu(parent); + Menu *add_menu = create_node_menu(parent); add_menu->setTitle(tr("Add")); connect(add_menu, &Menu::triggered, this, &NodeView::create_node_slot); return add_menu; diff --git a/app/widget/toolbar/toolbar.cpp b/app/widget/toolbar/toolbar.cpp index 32d4f61b0..d4e4c3d83 100644 --- a/app/widget/toolbar/toolbar.cpp +++ b/app/widget/toolbar/toolbar.cpp @@ -27,8 +27,8 @@ #include #include -#include "node/factory.h" #include "ui/icons/icons.h" +#include "widget/menu/factorymenu.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" @@ -206,7 +206,7 @@ void Toolbar::add_button_clicked() void Toolbar::transition_button_clicked() { - Menu *m = NodeFactory::create_menu(this, false, Node::k_category_transition); + Menu *m = create_node_menu(this, false, Node::k_category_transition); connect(m, &QMenu::triggered, this, &Toolbar::transition_menu_item_triggered); @@ -217,7 +217,7 @@ void Toolbar::transition_button_clicked() void Toolbar::transition_menu_item_triggered(QAction *a) { - emit selected_transition_changed(NodeFactory::GetIDFromMenuAction(a)); + emit selected_transition_changed(get_node_id_from_menu_action(a)); } } diff --git a/tests/gtest/project_factory_test.cpp b/tests/gtest/project_factory_test.cpp index 94dbcc6bc..43dcaaf41 100644 --- a/tests/gtest/project_factory_test.cpp +++ b/tests/gtest/project_factory_test.cpp @@ -31,6 +31,7 @@ #include "node/serializeddata.h" #include "core.h" #include "render/diskmanager.h" +#include "widget/menu/factorymenu.h" #include "widget/menu/menu.h" namespace @@ -532,15 +533,15 @@ TEST(NodeFactory, CreateMenuWithNoneItem) olive::NodeFactory::initialize(); std::unique_ptr menu( - olive::NodeFactory::create_menu(nullptr, true)); + olive::create_node_menu(nullptr, true)); ASSERT_NE(menu, nullptr); ASSERT_FALSE(menu->actions().isEmpty()); // The "None" item is inserted at the very top and maps to nothing QAction *none_item = menu->actions().first(); EXPECT_EQ(none_item->data().toInt(), -1); - EXPECT_EQ(olive::NodeFactory::CreateFromMenuAction(none_item), nullptr); - EXPECT_TRUE(olive::NodeFactory::GetIDFromMenuAction(none_item).isEmpty()); + EXPECT_EQ(olive::create_node_from_menu_action(none_item), nullptr); + EXPECT_TRUE(olive::get_node_id_from_menu_action(none_item).isEmpty()); // Leaf actions carry a library index that maps back to node ids QList leaves; @@ -552,10 +553,10 @@ TEST(NodeFactory, CreateMenuWithNoneItem) if (leaf->data().toInt() < 0) { continue; } - const QString id = olive::NodeFactory::GetIDFromMenuAction(leaf); + const QString id = olive::get_node_id_from_menu_action(leaf); EXPECT_FALSE(id.isEmpty()); std::unique_ptr node( - olive::NodeFactory::CreateFromMenuAction(leaf)); + olive::create_node_from_menu_action(leaf)); ASSERT_NE(node, nullptr); EXPECT_EQ(node->id(), id); ++created; @@ -569,7 +570,7 @@ TEST(NodeFactory, CreateMenuRestrictedToCategory) { olive::NodeFactory::initialize(); - std::unique_ptr menu(olive::NodeFactory::create_menu( + std::unique_ptr menu(olive::create_node_menu( nullptr, false, olive::Node::k_category_math)); ASSERT_NE(menu, nullptr); @@ -579,7 +580,7 @@ TEST(NodeFactory, CreateMenuRestrictedToCategory) for (QAction *leaf : leaves) { std::unique_ptr node( - olive::NodeFactory::CreateFromMenuAction(leaf)); + olive::create_node_from_menu_action(leaf)); ASSERT_NE(node, nullptr); EXPECT_TRUE(node->category().contains(olive::Node::k_category_math)) << node->id().toStdString();