mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge pull request #812 from pypeclub/feature/tray_action
Tray action interface
This commit is contained in:
commit
d0030686d1
10 changed files with 256 additions and 96 deletions
|
|
@ -18,10 +18,20 @@ def modules_from_path(folder_path):
|
|||
Returns:
|
||||
List of modules.
|
||||
"""
|
||||
modules = []
|
||||
# Just skip and return empty list if path is not set
|
||||
if not folder_path:
|
||||
return modules
|
||||
|
||||
# Do not allow relative imports
|
||||
if folder_path.startswith("."):
|
||||
log.warning((
|
||||
"BUG: Relative paths are not allowed for security reasons. {}"
|
||||
).format(folder_path))
|
||||
return modules
|
||||
|
||||
folder_path = os.path.normpath(folder_path)
|
||||
|
||||
modules = []
|
||||
if not os.path.isdir(folder_path):
|
||||
log.warning("Not a directory path: {}".format(folder_path))
|
||||
return modules
|
||||
|
|
|
|||
103
pype/modules/README.md
Normal file
103
pype/modules/README.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Pype modules
|
||||
Pype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering.
|
||||
|
||||
## Base class `PypeModule`
|
||||
- abstract class as base for each module
|
||||
- implementation should be module's api withou GUI parts
|
||||
- may implement `get_global_environments` method which should return dictionary of environments that are globally appliable and value is the same for whole studio if launched at any workstation (except os specific paths)
|
||||
- abstract parts:
|
||||
- `name` attribute - name of a module
|
||||
- `initialize` method - method for own initialization of a module (should not override `__init__`)
|
||||
- `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules
|
||||
- `__init__` should not be overriden and `initialize` should not do time consuming part but only prepare base data about module
|
||||
- also keep in mind that they may be initialized in headless mode
|
||||
- connection with other modules is made with help of interfaces
|
||||
|
||||
# Interfaces
|
||||
- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods
|
||||
- module that inherit from an interface must implement those abstract methods otherwise won't be initialized
|
||||
- it is easy to find which module object inherited from which interfaces withh 100% chance they have implemented required methods
|
||||
|
||||
## Global interfaces
|
||||
- few interfaces are implemented for global usage
|
||||
|
||||
### IPluginPaths
|
||||
- module want to add directory path/s to avalon or publish plugins
|
||||
- module must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"`
|
||||
- each key may contain list or string with path to directory with plugins
|
||||
|
||||
### ITrayModule
|
||||
- module has more logic when used in tray
|
||||
- it is possible that module can be used only in tray
|
||||
- abstract methods
|
||||
- `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules`
|
||||
- `tray_menu` - add actions to tray widget's menu that represent the module
|
||||
- `tray_start` - start of module's login in tray
|
||||
- module is initialized and connected with other modules
|
||||
- `tray_exit` - module's cleanup like stop and join threads etc.
|
||||
- order of calling is based on implementation this order is how it works with `TrayModulesManager`
|
||||
- it is recommended to import and use GUI implementaion only in these methods
|
||||
- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init`
|
||||
- if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations
|
||||
|
||||
### ITrayService
|
||||
- inherit from `ITrayModule` and implement `tray_menu` method for you
|
||||
- add action to submenu "Services" in tray widget menu with icon and label
|
||||
- abstract atttribute `label`
|
||||
- label shown in menu
|
||||
- interface has preimplemented methods to change icon color
|
||||
- `set_service_running` - green icon
|
||||
- `set_service_failed` - red icon
|
||||
- `set_service_idle` - orange icon
|
||||
- these states must be set by module itself `set_service_running` is default state on initialization
|
||||
|
||||
### ITrayAction
|
||||
- inherit from `ITrayModule` and implement `tray_menu` method for you
|
||||
- add action to tray widget menu with label
|
||||
- abstract atttribute `label`
|
||||
- label shown in menu
|
||||
- abstract method `on_action_trigger`
|
||||
- what should happen when action is triggered
|
||||
- NOTE: It is good idea to implement logic in `on_action_trigger` to api method and trigger that methods on callbacks this gives ability to trigger that method outside tray
|
||||
|
||||
## Modules interfaces
|
||||
- modules may have defined their interfaces to be able recognize other modules that would want to use their features
|
||||
-
|
||||
### Example:
|
||||
- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which of other modules want to add paths to server/user event handlers
|
||||
- Clockify module use `IFtrackEventHandlerPaths` and return paths to clockify ftrack synchronizers
|
||||
|
||||
- Clockify has more inharitance it's class definition looks like
|
||||
```
|
||||
class ClockifyModule(
|
||||
PypeModule, # Says it's Pype module so ModulesManager will try to initialize.
|
||||
ITrayModule, # Says has special implementation when used in tray.
|
||||
IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher).
|
||||
IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server.
|
||||
ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module.
|
||||
):
|
||||
```
|
||||
|
||||
### ModulesManager
|
||||
- collect module classes and tries to initialize them
|
||||
- important attributes
|
||||
- `modules` - list of available attributes
|
||||
- `modules_by_id` - dictionary of modules mapped by their ids
|
||||
- `modules_by_name` - dictionary of modules mapped by their names
|
||||
- all these attributes contain all found modules even if are not enabled
|
||||
- helper methods
|
||||
- `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them
|
||||
- `collect_plugin_paths` collect plugin paths from all enabled modules
|
||||
- output is always dictionary with all keys and values as list
|
||||
```
|
||||
{
|
||||
"publish": [],
|
||||
"create": [],
|
||||
"load": [],
|
||||
"actions": []
|
||||
}
|
||||
```
|
||||
|
||||
### TrayModulesManager
|
||||
- inherit from `ModulesManager`
|
||||
- has specific implementations for Pype Tray tool and handle `ITrayModule` methods
|
||||
|
|
@ -2,12 +2,13 @@
|
|||
from .base import (
|
||||
PypeModule,
|
||||
ITrayModule,
|
||||
ITrayAction,
|
||||
ITrayService,
|
||||
IPluginPaths,
|
||||
ModulesManager,
|
||||
TrayModulesManager
|
||||
)
|
||||
from .settings_module import SettingsModule
|
||||
from .settings_action import SettingsAction
|
||||
from .rest_api import (
|
||||
RestApiModule,
|
||||
IRestApi
|
||||
|
|
@ -25,6 +26,7 @@ from .timers_manager import (
|
|||
ITimersManager
|
||||
)
|
||||
from .avalon_apps import AvalonModule
|
||||
from .launcher_action import LauncherAction
|
||||
from .ftrack import (
|
||||
FtrackModule,
|
||||
IFtrackEventHandlerPaths
|
||||
|
|
@ -32,7 +34,7 @@ from .ftrack import (
|
|||
from .clockify import ClockifyModule
|
||||
from .logging import LoggingModule
|
||||
from .muster import MusterModule
|
||||
from .standalonepublish import StandAlonePublishModule
|
||||
from .standalonepublish_action import StandAlonePublishAction
|
||||
from .websocket_server import WebsocketModule
|
||||
from .sync_server import SyncServer
|
||||
|
||||
|
|
@ -40,12 +42,13 @@ from .sync_server import SyncServer
|
|||
__all__ = (
|
||||
"PypeModule",
|
||||
"ITrayModule",
|
||||
"ITrayAction",
|
||||
"ITrayService",
|
||||
"IPluginPaths",
|
||||
"ModulesManager",
|
||||
"TrayModulesManager",
|
||||
|
||||
"SettingsModule",
|
||||
"SettingsAction",
|
||||
|
||||
"UserModule",
|
||||
"IUserModule",
|
||||
|
|
@ -60,6 +63,7 @@ __all__ = (
|
|||
"IRestApi",
|
||||
|
||||
"AvalonModule",
|
||||
"LauncherAction",
|
||||
|
||||
"FtrackModule",
|
||||
"IFtrackEventHandlerPaths",
|
||||
|
|
@ -68,7 +72,7 @@ __all__ = (
|
|||
"IdleManager",
|
||||
"LoggingModule",
|
||||
"MusterModule",
|
||||
"StandAlonePublishModule",
|
||||
"StandAlonePublishAction",
|
||||
|
||||
"WebsocketModule",
|
||||
"SyncServer"
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi):
|
|||
)
|
||||
|
||||
# Tray attributes
|
||||
self.app_launcher = None
|
||||
self.libraryloader = None
|
||||
self.rest_api_obj = None
|
||||
|
||||
|
|
@ -99,29 +98,8 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi):
|
|||
exc_info=True
|
||||
)
|
||||
|
||||
# Add launcher
|
||||
try:
|
||||
from pype.tools.launcher import LauncherWindow
|
||||
self.app_launcher = LauncherWindow()
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Couldn't load Launch for tray.",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def connect_with_modules(self, _enabled_modules):
|
||||
plugin_paths = self.manager.collect_plugin_paths()["actions"]
|
||||
if plugin_paths:
|
||||
env_paths_str = os.environ.get("AVALON_ACTIONS") or ""
|
||||
env_paths = env_paths_str.split(os.pathsep)
|
||||
env_paths.extend(plugin_paths)
|
||||
os.environ["AVALON_ACTIONS"] = os.pathsep.join(env_paths)
|
||||
|
||||
if self.tray_initialized:
|
||||
from pype.tools.launcher import actions
|
||||
# actions.register_default_actions()
|
||||
actions.register_config_actions()
|
||||
actions.register_environment_actions()
|
||||
return
|
||||
|
||||
def rest_api_initialization(self, rest_api_module):
|
||||
if self.tray_initialized:
|
||||
|
|
@ -132,15 +110,12 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi):
|
|||
def tray_menu(self, tray_menu):
|
||||
from Qt import QtWidgets
|
||||
# Actions
|
||||
action_launcher = QtWidgets.QAction("Launcher", tray_menu)
|
||||
action_library_loader = QtWidgets.QAction(
|
||||
"Library loader", tray_menu
|
||||
)
|
||||
|
||||
action_launcher.triggered.connect(self.show_launcher)
|
||||
action_library_loader.triggered.connect(self.show_library_loader)
|
||||
|
||||
tray_menu.addAction(action_launcher)
|
||||
tray_menu.addAction(action_library_loader)
|
||||
|
||||
def tray_start(self, *_a, **_kw):
|
||||
|
|
@ -149,12 +124,6 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi):
|
|||
def tray_exit(self, *_a, **_kw):
|
||||
return
|
||||
|
||||
def show_launcher(self):
|
||||
# if app_launcher don't exist create it/otherwise only show main window
|
||||
self.app_launcher.show()
|
||||
self.app_launcher.raise_()
|
||||
self.app_launcher.activateWindow()
|
||||
|
||||
def show_library_loader(self):
|
||||
self.libraryloader.show()
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,41 @@ class ITrayModule:
|
|||
pass
|
||||
|
||||
|
||||
class ITrayAction(ITrayModule):
|
||||
"""Implementation of Tray action.
|
||||
|
||||
Add action to tray menu which will trigger `on_action_trigger`.
|
||||
It is expected to be used for showing tools.
|
||||
|
||||
Methods `tray_start`, `tray_exit` and `connect_with_modules` are overriden
|
||||
as it's not expected that action will use them. But it is possible if
|
||||
necessary.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def label(self):
|
||||
"""Service label showed in menu."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def on_action_trigger(self):
|
||||
"""What happens on actions click."""
|
||||
pass
|
||||
|
||||
def tray_menu(self, tray_menu):
|
||||
from Qt import QtWidgets
|
||||
action = QtWidgets.QAction(self.label, tray_menu)
|
||||
action.triggered.connect(self.on_action_trigger)
|
||||
tray_menu.addAction(action)
|
||||
|
||||
def tray_start(self):
|
||||
return
|
||||
|
||||
def tray_exit(self):
|
||||
return
|
||||
|
||||
|
||||
class ITrayService(ITrayModule):
|
||||
# Module's property
|
||||
menu_action = None
|
||||
|
|
@ -287,7 +322,13 @@ class ModulesManager:
|
|||
enabled_modules = self.get_enabled_modules()
|
||||
self.log.debug("Has {} enabled modules.".format(len(enabled_modules)))
|
||||
for module in enabled_modules:
|
||||
module.connect_with_modules(enabled_modules)
|
||||
try:
|
||||
module.connect_with_modules(enabled_modules)
|
||||
except Exception:
|
||||
self.log.error(
|
||||
"BUG: Module failed on connection with other modules.",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def get_enabled_modules(self):
|
||||
"""Enabled modules initialized by the manager.
|
||||
|
|
@ -387,6 +428,7 @@ class TrayModulesManager(ModulesManager):
|
|||
"user",
|
||||
"ftrack",
|
||||
"muster",
|
||||
"launcher_tool",
|
||||
"avalon",
|
||||
"clockify",
|
||||
"standalonepublish_tool",
|
||||
|
|
|
|||
44
pype/modules/launcher_action.py
Normal file
44
pype/modules/launcher_action.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from . import PypeModule, ITrayAction
|
||||
|
||||
|
||||
class LauncherAction(PypeModule, ITrayAction):
|
||||
label = "Launcher"
|
||||
name = "launcher_tool"
|
||||
|
||||
def initialize(self, _modules_settings):
|
||||
# This module is always enabled
|
||||
self.enabled = True
|
||||
|
||||
# Tray attributes
|
||||
self.window = None
|
||||
|
||||
def tray_init(self):
|
||||
self.create_window()
|
||||
|
||||
def tray_start(self):
|
||||
return
|
||||
|
||||
def connect_with_modules(self, enabled_modules):
|
||||
# Register actions
|
||||
if self.tray_initialized:
|
||||
from pype.tools.launcher import actions
|
||||
# actions.register_default_actions()
|
||||
actions.register_config_actions()
|
||||
actions_paths = self.manager.collect_plugin_paths()["actions"]
|
||||
actions.register_actions_from_paths(actions_paths)
|
||||
actions.register_environment_actions()
|
||||
|
||||
def create_window(self):
|
||||
if self.window:
|
||||
return
|
||||
from pype.tools.launcher import LauncherWindow
|
||||
self.window = LauncherWindow()
|
||||
|
||||
def on_action_trigger(self):
|
||||
self.show_launcher()
|
||||
|
||||
def show_launcher(self):
|
||||
if self.window:
|
||||
self.window.show()
|
||||
self.window.raise_()
|
||||
self.window.activateWindow()
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
from . import PypeModule, ITrayModule
|
||||
from . import PypeModule, ITrayAction
|
||||
|
||||
|
||||
class SettingsModule(PypeModule, ITrayModule):
|
||||
class SettingsAction(PypeModule, ITrayAction):
|
||||
"""Action to show Setttings tool."""
|
||||
name = "settings"
|
||||
label = "Settings"
|
||||
|
||||
def initialize(self, _modules_settings):
|
||||
# This module is always enabled
|
||||
# This action is always enabled
|
||||
self.enabled = True
|
||||
|
||||
# User role
|
||||
|
|
@ -18,13 +20,28 @@ class SettingsModule(PypeModule, ITrayModule):
|
|||
def connect_with_modules(self, *_a, **_kw):
|
||||
return
|
||||
|
||||
def tray_init(self):
|
||||
"""Initialization in tray implementation of ITrayAction."""
|
||||
self.create_settings_window()
|
||||
|
||||
def on_action_trigger(self):
|
||||
"""Implementation for action trigger of ITrayAction."""
|
||||
self.show_settings_window()
|
||||
|
||||
def create_settings_window(self):
|
||||
"""Initializa Settings Qt window."""
|
||||
if self.settings_window:
|
||||
return
|
||||
from pype.tools.settings import MainWidget
|
||||
self.settings_window = MainWidget(self.user_role)
|
||||
|
||||
def show_settings_window(self):
|
||||
"""Show settings tool window.
|
||||
|
||||
Raises:
|
||||
AssertionError: Window must be already created. Call
|
||||
`create_settings_window` before callint this method.
|
||||
"""
|
||||
if not self.settings_window:
|
||||
raise AssertionError("Window is not initialized.")
|
||||
|
||||
|
|
@ -33,21 +50,3 @@ class SettingsModule(PypeModule, ITrayModule):
|
|||
# Pull window to the front.
|
||||
self.settings_window.raise_()
|
||||
self.settings_window.activateWindow()
|
||||
|
||||
def tray_init(self):
|
||||
self.create_settings_window()
|
||||
|
||||
def tray_menu(self, tray_menu):
|
||||
"""Add **change credentials** option to tray menu."""
|
||||
from Qt import QtWidgets
|
||||
|
||||
# Actions
|
||||
action = QtWidgets.QAction("Settings", tray_menu)
|
||||
action.triggered.connect(self.show_settings_window)
|
||||
tray_menu.addAction(action)
|
||||
|
||||
def tray_start(self):
|
||||
return
|
||||
|
||||
def tray_exit(self):
|
||||
return
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from .standalonepublish_module import StandAlonePublishModule
|
||||
|
||||
__all__ = (
|
||||
"StandAlonePublishModule",
|
||||
)
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import pype
|
||||
from .. import PypeModule, ITrayModule
|
||||
from . import PypeModule, ITrayAction
|
||||
|
||||
|
||||
class StandAlonePublishModule(PypeModule, ITrayModule):
|
||||
menu_label = "Publish"
|
||||
class StandAlonePublishAction(PypeModule, ITrayAction):
|
||||
label = "Publish"
|
||||
name = "standalonepublish_tool"
|
||||
|
||||
def initialize(self, modules_settings):
|
||||
import pype
|
||||
self.enabled = modules_settings[self.name]["enabled"]
|
||||
self.publish_paths = [
|
||||
os.path.join(
|
||||
|
|
@ -20,17 +20,8 @@ class StandAlonePublishModule(PypeModule, ITrayModule):
|
|||
def tray_init(self):
|
||||
return
|
||||
|
||||
def tray_start(self):
|
||||
return
|
||||
|
||||
def tray_exit(self):
|
||||
return
|
||||
|
||||
def tray_menu(self, parent_menu):
|
||||
from Qt import QtWidgets
|
||||
run_action = QtWidgets.QAction(self.menu_label, parent_menu)
|
||||
run_action.triggered.connect(self.run_standalone_publisher)
|
||||
parent_menu.addAction(run_action)
|
||||
def on_action_trigger(self):
|
||||
self.run_standalone_publisher()
|
||||
|
||||
def connect_with_modules(self, enabled_modules):
|
||||
"""Collect publish paths from other modules."""
|
||||
|
|
@ -85,29 +85,32 @@ def register_config_actions():
|
|||
config.register_launcher_actions()
|
||||
|
||||
|
||||
def register_environment_actions():
|
||||
"""Register actions from AVALON_ACTIONS for Launcher."""
|
||||
|
||||
paths = os.environ.get("AVALON_ACTIONS")
|
||||
def register_actions_from_paths(paths):
|
||||
if not paths:
|
||||
return
|
||||
|
||||
for path in paths.split(os.pathsep):
|
||||
for path in paths:
|
||||
if not path:
|
||||
continue
|
||||
|
||||
if path.startswith("."):
|
||||
print((
|
||||
"BUG: Relative paths are not allowed for security reasons. {}"
|
||||
).format(path))
|
||||
continue
|
||||
|
||||
if not os.path.exists(path):
|
||||
print("Path was not found: {}".format(path))
|
||||
continue
|
||||
|
||||
api.register_plugin_path(api.Action, path)
|
||||
|
||||
# Run "register" if found.
|
||||
for module in lib.modules_from_path(path):
|
||||
if "register" not in dir(module):
|
||||
continue
|
||||
|
||||
try:
|
||||
module.register()
|
||||
except Exception as e:
|
||||
print(
|
||||
"Register method in {0} failed: {1}".format(
|
||||
module, str(e)
|
||||
)
|
||||
)
|
||||
def register_environment_actions():
|
||||
"""Register actions from AVALON_ACTIONS for Launcher."""
|
||||
|
||||
paths_str = os.environ.get("AVALON_ACTIONS") or ""
|
||||
register_actions_from_paths(paths_str.split(os.pathsep))
|
||||
|
||||
|
||||
class ApplicationAction(api.Action):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue