Merge pull request #812 from pypeclub/feature/tray_action

Tray action interface
This commit is contained in:
Milan Kolar 2020-12-18 09:52:59 +01:00 committed by GitHub
commit d0030686d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 256 additions and 96 deletions

View file

@ -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
View 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

View file

@ -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"

View file

@ -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()

View file

@ -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",

View 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()

View file

@ -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

View file

@ -1,5 +0,0 @@
from .standalonepublish_module import StandAlonePublishModule
__all__ = (
"StandAlonePublishModule",
)

View file

@ -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."""

View file

@ -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):