feat: group OpenFX plugins under dedicated "OpenFX" category with sub-groups

All OpenFX plugins were previously hardcoded to return kCategoryUnknown,
  causing them to pile up under "Uncategorized" in the node creation menu.

  This commit introduces a two-level grouping system for OFX plugins:

  1. Add new kCategoryOpenFX top-level category
     - Node::CategoryID enum extended with kCategoryOpenFX
     - PluginNode::Category() now returns {kCategoryOpenFX}
     - Node::GetCategoryName() returns "OpenFX"

  2. Add secondary sub-grouping support
     - Node base class gains virtual SubCategory() method
     - PluginNode implements SubCategory() backed by sub_category_ member
     - sub_category_ is set in the constructor from the plugin's OFX context:
         Filter     → "Filter"
         Generator  → "Generator"
         Transition → "Transition"
         others     → "General"

  3. Update NodeFactory::CreateMenu()
     - When a node belongs to kCategoryOpenFX and provides a non-empty
       SubCategory(), creates a second-level submenu under "OpenFX"
     - Nodes without a sub-category are placed directly in the top menu

  Expected menu layout:
    OpenFX
      ├── Filter
      │     ├── ColorCorrect
      │     └── ...
      ├── Generator
      ├── Transition
      └── General

  All 4 test suites pass.
This commit is contained in:
2026-05-14 21:05:29 +08:00
parent b2de04962d
commit e012f16083
5 changed files with 54 additions and 9 deletions
+23 -8
View File
@@ -128,25 +128,40 @@ Menu *NodeFactory::CreateMenu(QWidget *parent, bool create_none_item,
// Make sure nodes are up-to-date with the current translation
n->Retranslate();
Menu *destination = nullptr;
QString category_name = Node::GetCategoryName(
n->Category().isEmpty() ? Node::kCategoryUnknown :
n->Category().first());
// See if a menu with this category name already exists
// Find or create top-level category menu
Menu *top_menu = nullptr;
QList<QAction *> menu_actions = menu->actions();
foreach (QAction *action, menu_actions) {
if (action->menu() && action->menu()->title() == category_name) {
top_menu = static_cast<Menu *>(action->menu());
break;
}
}
if (!top_menu) {
top_menu = new Menu(category_name, menu);
menu->InsertAlphabetically(top_menu);
}
// Determine final destination (support secondary grouping)
Menu *destination = top_menu;
QString sub = n->SubCategory();
if (!sub.isEmpty() &&
n->Category().contains(Node::kCategoryOpenFX)) {
QList<QAction *> sub_actions = top_menu->actions();
foreach (QAction *action, sub_actions) {
if (action->menu() && action->menu()->title() == sub) {
destination = static_cast<Menu *>(action->menu());
break;
}
}
// Create menu here if it doesn't exist
if (!destination) {
destination = new Menu(category_name, menu);
menu->InsertAlphabetically(destination);
if (destination == top_menu) {
destination = new Menu(sub, top_menu);
top_menu->InsertAlphabetically(destination);
}
}
// Add entry to menu
+2
View File
@@ -2295,6 +2295,8 @@ QString Node::GetCategoryName(const CategoryID &c)
return tr("Transition");
case kCategoryProject:
return tr("Project");
case kCategoryOpenFX:
return tr("OpenFX");
case kCategoryTime:
return tr("Time");
case kCategoryUnknown:
+7
View File
@@ -101,6 +101,7 @@ public:
kCategoryTransition,
kCategoryDistort,
kCategoryProject,
kCategoryOpenFX,
kCategoryCount
};
@@ -177,6 +178,12 @@ public:
*/
virtual QVector<CategoryID> Category() const = 0;
/**
* @brief Return a sub-category string for secondary grouping
* within the primary category (e.g. "Filter" under "OpenFX").
*/
virtual QString SubCategory() const { return QString(); }
/**
* @brief Return a description of this node's purpose (optional for subclassing, but recommended)
*
+18 -1
View File
@@ -250,6 +250,18 @@ olive::plugin::PluginNode::PluginNode(
OFX::Host::ImageEffect::Instance *plugin)
{
plugin_instance_=plugin;
const std::string &ctx = plugin_instance_->getContext();
if (ctx == kOfxImageEffectContextFilter) {
sub_category_ = tr("Filter");
} else if (ctx == kOfxImageEffectContextGenerator) {
sub_category_ = tr("Generator");
} else if (ctx == kOfxImageEffectContextTransition) {
sub_category_ = tr("Transition");
} else {
sub_category_ = tr("General");
}
bool has_texture_input = false;
QHash<QString, QString> group_labels;
QHash<QString, QString> page_labels;
@@ -505,7 +517,12 @@ QString olive::plugin::PluginNode::Name() const
QVector<olive::Node::CategoryID> olive::plugin::PluginNode::Category() const
{
return { olive::Node::kCategoryUnknown };
return { olive::Node::kCategoryOpenFX };
}
QString olive::plugin::PluginNode::SubCategory() const
{
return sub_category_;
}
QString olive::plugin::PluginNode::Description() const
+4
View File
@@ -38,6 +38,7 @@ public:
QString Name() const override;
QString id() const override;
QVector<CategoryID> Category() const override;
QString SubCategory() const override;
QString Description() const override;
void AddPushButton();
@@ -76,6 +77,9 @@ public:
*/
virtual void GenerateFrame(FramePtr frame, const GenerateJob &job) const;
private:
QString sub_category_;
public slots:
void pushButtonClicked(QString name);