Skip to content

Commit 99be74c

Browse files
[cuegui] Add dynamic plugin support with initial progress bar plugin (#1712)
- Implemented a dynamic plugin architecture under `cuegui/cueguiplugin/`, enabling developers to extend CueGUI with custom actions without modifying core code. - Each plugin resides in its own subdirectory containing a `plugin.py` file that defines a `Plugin` class with metadata and context menu actions. - Created `cuegui/cueguiplugin/loader.py` to auto-discover and load all available plugins at runtime, using `.cueguipluginrc.yaml` for enable/disable control. - Integrated plugin action hooks into `MenuAction.py`, `JobMonitorTree.py`, and `CueJobMonitorTree.py` so that job context menus include plugin-defined `QAction`s dynamically. - Added the first plugin: `cueprogbar`. This plugin introduces a "Show Progress Bar" context menu action for jobs, opening a visual widget to monitor job progress in real time. - The progress bar widget displays frame state breakdowns and supports pause/unpause/kill operations. - Right-click the bar to access control actions based on job state (e.g., Pause Job, Unpause Job, Kill Job). - Left-click the bar to view a tooltip with the job name and frame state breakdown (SUCCEEDED, RUNNING, WAITING, DEPEND, DEAD, EATEN). - Supports launching multiple progress bar windows for multiple selected jobs. - Added `cueguiplugin/.cueguipluginrc.yaml`, `cueprogbar/config.yaml`, and plugin icons to `cuegui/setup.py` - Ensures plugin system loads properly after `pip install cuegui/` **Usage (`cueprogbar`):** 1. Open **Cuetopia** (Monitor Jobs) or **Cuecommander** (MonitorCue) from CueGUI. 2. Right-click on any job or jobs listed. 3. Select "Show Progress Bar" from the context menu. This opens a visual widget per job with: - Real-time progress bar (color-coded by frame state) for each line selected - Labels with job info and progress summary - Right-click actions to: - Pause / Unpause the job - Kill the job - Left-click to view a breakdown of frame states (SUCCEEDED, RUNNING, WAITING, DEPEND, DEAD, EATEN) This modular system enables future plugin additions without modifying the core GUI logic. **Link the Issue(s) this Pull Request is related to.** Add dynamic plugin system to CueGUI with Initial cueprogbar plugin #1711
1 parent 0a75d2f commit 99be74c

File tree

16 files changed

+1090
-3
lines changed

16 files changed

+1090
-3
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
*.idea/
2+
__pycache__
23
*.pyc
34
.DS_Store
45
venv*/
6+
build/
57
/build/
68
/VERSION
79
.scannerwork/

cuegui/cuegui/CueJobMonitorTree.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
import cuegui.Style
4343
import cuegui.Utils
4444

45+
from cuegui.cueguiplugin import loader as plugin_loader
46+
4547

4648
logger = cuegui.Logger.getLogger(__file__)
4749
Body = namedtuple("Body", "group_names, group_ids, job_names, job_ids")
@@ -580,6 +582,36 @@ def contextMenuEvent(self, e):
580582
menu.addSeparator()
581583
self.__menuActions.jobs().addAction(menu, "kill")
582584

585+
# Dynamically add plugin actions for right-clicked job(s)
586+
plugins_by_type = {}
587+
for job in selectedObjects:
588+
for plugin in plugin_loader.load_plugins(job=job, parent=self):
589+
plugins_by_type[type(plugin)] = plugin
590+
591+
for plugin_type, plugin_instance in plugins_by_type.items():
592+
if plugin_type.__name__ == "Plugin":
593+
# pylint: disable=protected-access
594+
label = plugin_instance._config.get("menu_label", "Unnamed Plugin")
595+
action = QtWidgets.QAction(label, self)
596+
597+
def make_launch_all(ptype):
598+
def launch_all():
599+
for job in selectedObjects:
600+
plugin = ptype(job=job, parent=self)
601+
plugin.launch_subprocess()
602+
603+
return launch_all
604+
605+
action.triggered.connect(make_launch_all(plugin_type))
606+
menu.addSeparator()
607+
menu.addAction(action)
608+
else:
609+
actions = plugin_instance.menuAction()
610+
if actions:
611+
menu.addSeparator()
612+
for action in actions:
613+
menu.addAction(action)
614+
583615
menu.exec_(e.globalPos())
584616

585617
def actionEatSelectedItems(self):

cuegui/cuegui/JobMonitorTree.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
import cuegui.Style
4242
import cuegui.Utils
4343

44+
from cuegui.cueguiplugin import loader as plugin_loader
45+
4446

4547
logger = cuegui.Logger.getLogger(__file__)
4648

@@ -518,6 +520,38 @@ def contextMenuEvent(self, e):
518520
menu.addSeparator()
519521
self.__menuActions.jobs().addAction(menu, "kill")
520522

523+
# Dynamically add plugin actions for right-clicked job(s)
524+
if __selectedObjects:
525+
# Group plugins by type so we don’t load duplicates
526+
plugins_by_type = {}
527+
for job in __selectedObjects:
528+
for plugin in plugin_loader.load_plugins(job=job, parent=self):
529+
plugins_by_type[type(plugin)] = plugin
530+
531+
for plugin_type, plugin_instance in plugins_by_type.items():
532+
if plugin_type.__name__ == "Plugin":
533+
# Create single action that calls all subprocesses
534+
# pylint: disable=protected-access
535+
label = plugin_instance._config.get("menu_label", "Unnamed Plugin")
536+
action = QtWidgets.QAction(label, self)
537+
538+
def make_launch_all(ptype):
539+
def launch_all():
540+
for job in __selectedObjects:
541+
plugin = ptype(job=job, parent=self)
542+
plugin.launch_subprocess()
543+
return launch_all
544+
545+
action.triggered.connect(make_launch_all(plugin_type))
546+
menu.addSeparator()
547+
menu.addAction(action)
548+
else:
549+
actions = plugin_instance.menuAction()
550+
if actions:
551+
menu.addSeparator()
552+
for action in actions:
553+
menu.addAction(action)
554+
521555
menu.exec_(e.globalPos())
522556

523557
def actionRemoveSelectedItems(self):

cuegui/cuegui/MenuActions.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
import cuegui.UnbookDialog
6464
import cuegui.Utils
6565

66+
from cuegui.cueguiplugin import loader as plugin_loader
67+
6668

6769
logger = cuegui.Logger.getLogger(__file__)
6870

@@ -221,7 +223,49 @@ class JobActions(AbstractActions):
221223
"""Actions for jobs."""
222224

223225
def __init__(self, *args):
224-
AbstractActions.__init__(self, *args)
226+
"""
227+
Initialize JobActions and load associated plugins if a job source is available.
228+
229+
The plugin loading mechanism uses a double-callable pattern:
230+
- `self._getSource` is expected to return a function
231+
- that function is then called to retrieve the actual job object.
232+
233+
Plugins are loaded and initialized only if a valid job is retrieved.
234+
"""
235+
super().__init__(*args)
236+
self._pluginActions = []
237+
238+
# Attempt to retrieve the job object from the callable chain
239+
source = None
240+
try:
241+
if callable(self._getSource):
242+
maybe_func = self._getSource()
243+
if callable(maybe_func):
244+
source = maybe_func()
245+
except Exception as e:
246+
# Optional: log if needed
247+
logger.warning("Failed to resolve plugin source: %s", e)
248+
249+
# Load plugins only if source is valid
250+
if source:
251+
self._pluginActions = plugin_loader.load_plugins(job=source, parent=self._caller)
252+
253+
def addPluginActions(self, menu):
254+
"""
255+
Add plugin-defined actions to the given context menu.
256+
257+
Args:
258+
menu (QMenu): The Qt menu to which plugin actions will be appended.
259+
"""
260+
for plugin in self._pluginActions:
261+
actions = plugin.menuAction()
262+
if not actions:
263+
continue
264+
if isinstance(actions, list):
265+
for action in actions:
266+
menu.addAction(action)
267+
else:
268+
menu.addAction(actions)
225269

226270
unmonitor_info = ["Unmonitor", "Unmonitor selected jobs", "eject"]
227271

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright Contributors to the OpenCue Project
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# ------------------------------------------------------------------------------
16+
# CueGUI Plugin control configuration
17+
# ------------------------------------------------------------------------------
18+
# This file allows you to centrally manage which plugins are enabled or disabled.
19+
#
20+
# Plugin names must match the folder name inside `cueguiplugin/`.
21+
# Example:
22+
# cueguiplugin/
23+
# ├── cueprogbar/
24+
# ├── mycustomplugin1/
25+
# └── mycustomplugin2/
26+
# ...
27+
#
28+
# This config is optional. If omitted, all plugins default to enabled unless
29+
# disabled in their own config.yaml.
30+
#
31+
# ------------------------------------------------------------------------------
32+
33+
# Whitelist of plugins that are allowed to load
34+
# If this list is present, only these plugins will be loaded.
35+
enabled_plugins:
36+
- cueprogbar # Adds a visual floating job progress bar widget
37+
38+
# List of plugins that should always be disabled, regardless of their own config.yaml
39+
disabled_plugins: [] # Currently no plugins are explicitly blocked
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# CueGUI Plugins
2+
3+
This folder contains **optional plugins for CueGUI**. Plugins can add new menu actions, custom UI components, visualizations, and more - all without modifying CueGUI's core source code.
4+
5+
## Contents
6+
- [Plugin structure](#plugin-structure)
7+
- [Example of plugin folder structure](#example-of-plugin-folder-structure)
8+
- [Plugin loading](#plugin-loading)
9+
- [Adding a new plugin](#adding-a-new-plugin)
10+
- [List of plugins](#list-of-plugins)
11+
- [1) cueprogbar](#1-cueprogbar)
12+
- [How to use?](#how-to-use)
13+
- [Standalone Mode (CLI)](#standalone-mode-cli)
14+
- [Notes](#notes)
15+
- [Tips](#tips)
16+
17+
18+
## Plugin structure
19+
20+
Each plugin lives in its own subfolder under `cueguiplugin/`, and should contain the following:
21+
22+
- `plugin.py`: Defines a `Plugin` class that extends `CueGuiPlugin`.
23+
- `config.yaml` (optional): Used to configure menu labels, icons, or behavior.
24+
- Additional supporting files (e.g., plugin code, icons, etc.).
25+
26+
Note:
27+
- Plugin enablement is **only** managed via the `.cueguipluginrc.yaml` file.
28+
29+
Go back to [Contents](#contents).
30+
31+
### Example of plugin folder structure
32+
33+
```
34+
cueguiplugin/
35+
├── README.md
36+
├── __init__.py # Plugin interface definition
37+
├── loader.py # Plugin discovery and loading
38+
└── cueprogbar/ # Example plugin
39+
├── __init__.py
40+
├── __main__.py # For standalone use (CLI)
41+
├── config.yaml # Plugin-specific config (label, icon, etc.)
42+
├── darkmojo.py # Custom dark UI palette
43+
├── images/
44+
│ └── cueprogbar_icon.png
45+
├── main.py # Widget logic
46+
└── plugin.py # Plugin class entrypoint
47+
```
48+
49+
Go back to [Contents](#contents).
50+
51+
## Plugin loading
52+
53+
Plugins are dynamically loaded using `cueguiplugin/loader.py`, and automatically integrated into:
54+
- `MenuActions.py`
55+
- `JobMonitorTree.py` (Cuetopia)
56+
- `CueJobMonitorTree.py` (Cuecommander)
57+
58+
### Available in both:
59+
- **Cuetopia**: Right-click on a job in the Monitor Jobs view.
60+
- **Cuecommander**: Right-click on a job in the Show/Job hierarchy view.
61+
62+
There is no need to modify CueGUI itself. To add a plugin, just drop it into the `cueguiplugin/` directory. CueGUI will automatically discover and load it at runtime.
63+
64+
Global plugin control is defined via `.cueguipluginrc.yaml` in this directory.
65+
66+
Go back to [Contents](#contents).
67+
68+
## Adding a new plugin
69+
70+
1. Create a new folder inside `cueguiplugin/`, for example:
71+
72+
```bash
73+
mkdir cueguiplugin/myplugin
74+
```
75+
76+
2. Add a `plugin.py` with the following structure:
77+
78+
```python
79+
from qtpy.QtWidgets import QAction
80+
from cuegui.cueguiplugin import CueGuiPlugin
81+
82+
class Plugin(CueGuiPlugin):
83+
def __init__(self, job, parent=None, config=None):
84+
super().__init__(job=job, parent=parent, config=config)
85+
86+
def menuAction(self):
87+
action = QAction("My Plugin Action", self._parent)
88+
action.triggered.connect(self.run)
89+
return action
90+
91+
def run(self):
92+
print(f"Running plugin for job: {self._job.name()}")
93+
```
94+
95+
3. Optionally, add a `config.yaml` to define:
96+
97+
```yaml
98+
menu_label: My Plugin Action
99+
icon: images/my_icon.png
100+
```
101+
102+
Go back to [Contents](#contents).
103+
104+
## List of plugins
105+
106+
### 1) `cueprogbar`
107+
108+
The `cueprogbar` plugin adds a visual job progress bar for OpenCue jobs. It provides real-time color-coded frame status and basic job control.
109+
110+
Go back to [Contents](#contents).
111+
112+
#### How to use?
113+
114+
1. Open **Cuetopia** (Monitor Jobs) or **Cuecommander** (MonitorCue) from CueGUI.
115+
2. Right-click on any job or jobs listed.
116+
3. Select "Show Progress Bar" from the context menu.
117+
118+
This opens window(s) with:
119+
120+
- Real-time progress bar (color-coded by frame state) for each line selected
121+
- Labels with job info and progress summary
122+
- Right-click actions to:
123+
- Pause / Unpause the job
124+
- Kill the job
125+
- Left-click to view a breakdown of frame states (SUCCEEDED, RUNNING, WAITING, DEPEND, DEAD, EATEN)
126+
127+
Go back to [Contents](#contents).
128+
129+
#### Standalone Mode (CLI)
130+
131+
You can also launch the plugin directly:
132+
133+
```bash
134+
cd OpenCue/
135+
python -m cuegui.cueguiplugin.cueprogbar <job_name>
136+
```
137+
138+
Example:
139+
140+
```bash
141+
python -m cuegui.cueguiplugin.cueprogbar testing-test_shot-my_render_job
142+
```
143+
144+
This is useful for testing or displaying CueGUI plugins outside of CueGUI.
145+
146+
Go back to [Contents](#contents).
147+
148+
## Notes
149+
150+
- All plugins must be Python 3 compatible.
151+
- Qt compatibility is maintained via qtpy (supports PySide2, PyQt5, PySide6, etc.).
152+
- Make sure plugins are lightweight and responsive - they run inside the CueGUI main process.
153+
154+
Go back to [Contents](#contents).
155+
156+
## Tips
157+
158+
- You can enable or disable plugins centrally via `.cueguipluginrc.yaml`
159+
- Each plugin can still have its own `config.yaml` for UI customization (label, icon, etc.)
160+
- Use `darkmojo.py` if you want a consistent dark theme for all plugin UIs
161+
- Plugins are useful for extending CueGUI without having to fork or patch the core.
162+
163+
Go back to [Contents](#contents).

0 commit comments

Comments
 (0)