From 478693d0ab6dbec4b76c8d9b80c6812766766895 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 11:47:23 +0200 Subject: [PATCH 01/11] added tine addon --- openpype/modules/example_addons/tiny_addon.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 openpype/modules/example_addons/tiny_addon.py diff --git a/openpype/modules/example_addons/tiny_addon.py b/openpype/modules/example_addons/tiny_addon.py new file mode 100644 index 00000000000..62962954f5f --- /dev/null +++ b/openpype/modules/example_addons/tiny_addon.py @@ -0,0 +1,9 @@ +from openpype.modules import OpenPypeAddOn + + +class TinyAddon(OpenPypeAddOn): + """This is tiniest possible addon. + + This addon won't do much but will exist in OpenPype modules environment. + """ + name = "tiniest_addon_ever" From 3dd69032510e8087a889c3bf38fbde96ce1204d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 11:53:59 +0200 Subject: [PATCH 02/11] added base of example addon --- .../example_addons/example_addon/__init__.py | 13 ++ .../example_addons/example_addon/addon.py | 124 ++++++++++++++++++ .../example_addon/interfaces.py | 28 ++++ .../plugins/publish/example_plugin.py | 10 ++ .../settings/defaults/project_settings.json | 1 + .../project_dynamic_schemas.json | 6 + .../system_dynamic_schemas.json | 6 + .../schemas/project_schemas/main.json | 29 ++++ .../schemas/project_schemas/the_template.json | 30 +++++ .../settings/schemas/system_schemas/main.json | 14 ++ .../example_addons/example_addon/widgets.py | 30 +++++ 11 files changed, 291 insertions(+) create mode 100644 openpype/modules/example_addons/example_addon/__init__.py create mode 100644 openpype/modules/example_addons/example_addon/addon.py create mode 100644 openpype/modules/example_addons/example_addon/interfaces.py create mode 100644 openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/widgets.py diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py new file mode 100644 index 00000000000..df4d61650b6 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -0,0 +1,13 @@ +""" Addon class definition and Settings definition must be imported here. + +If addon class or settings definition won't be here their definition won't +be found by OpenPype discovery. +""" + +from .addon import ( + AddonSettingsDef, +) + +__all__ = ( + "AddonSettingsDef", +) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py new file mode 100644 index 00000000000..64504be756c --- /dev/null +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -0,0 +1,124 @@ +"""Addon definition is located here. + +Import of python packages that may not be available should not be imported +in global space here until are required or used. +- Qt related imports +- imports of Python 3 packages + - we still support Python 2 hosts where addon definition should available +""" + +import os + +from openpype.modules import ( + JsonFilesSettingsDef, + OpenPypeAddOn +) +# Import interface defined by this addon to be able find other addons using it +from openpype_interfaces import ( + IExampleInterface, + IPluginPaths, + ITrayAction +) + + +# Settings definiton of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definiton using json files +# to define settings and store defaul values +class AddonSettingsDef(JsonFilesSettingsDef): + # This will add prefix to every schema and template from `schemas` + # subfolder. + # - it is not required to fill the prefix but it is highly + # recommended as schemas and templates may have name clashes across + # multiple addons + # - it is also recommended that prefix has addon name in it + schema_prefix = "addon_with_settings" + + def get_settings_root_path(self): + """Implemented abstract class of JsonFilesSettingsDef. + + Return directory path where json files defying addon settings are + located. + """ + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "settings" + ) + + +class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): + """This Addon has defined it's settings and interface. + + This example has system settings with enabled option. And use + few other interfaces: + - `IPluginPaths` to define custom plugin paths + - `ITrayAction` to be shown in tray tool + """ + label = "Example Addon" + name = "example_addon" + + def initialize(self, settings): + """Initialization of addon.""" + module_settings = settings[self.name] + # Enabled by settings + self.enabled = module_settings.get("enabled", False) + + # Prepare variables that can be used or set afterwards + self._connected_modules = None + # UI which must not be created at this time + self._dialog = None + + def connect_with_modules(self, enabled_modules): + """Method where you should find connected modules. + + It is triggered by OpenPype modules manager at the best possible time. + Some addons and modules may required to connect with other modules + before their main logic is executed so changes would require to restart + whole process. + """ + self._connected_modules = [] + for module in enabled_modules: + if isinstance(module, IExampleInterface): + self._connected_modules.append(module) + + def _create_dialog(self): + # Don't recreate dialog if already exists + if self._dialog is not None: + return + + from .widgets import MyExampleDialog + + self._dialog = MyExampleDialog() + + def show_dialog(self): + """Show dialog with connected modules. + + This can be called from anywhere but can also crash in headless mode. + There is not way how to prevent addon to do invalid operations if he's + not handling them. + """ + # Make sure dialog is created + self._create_dialog() + # Change value of dialog by current state + self._dialog.set_connected_modules(self.get_connected_modules()) + # Show dialog + self._dialog.open() + + def get_connected_modules(self): + """Custom implementation of addon.""" + names = set() + if self._connected_modules is not None: + for module in self._connected_modules: + names.add(module.name) + return names + + def on_action_trigger(self): + """Implementation of abstract method for `ITrayAction`.""" + self.show_dialog() + + def get_plugin_paths(self): + """Implementation of abstract method for `IPluginPaths`.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + + return { + "publish": [os.path.join(current_dir, "plugins", "publish")] + } diff --git a/openpype/modules/example_addons/example_addon/interfaces.py b/openpype/modules/example_addons/example_addon/interfaces.py new file mode 100644 index 00000000000..371536efc7a --- /dev/null +++ b/openpype/modules/example_addons/example_addon/interfaces.py @@ -0,0 +1,28 @@ +""" Using interfaces is one way of connecting multiple OpenPype Addons/Modules. + +Interfaces must be in `interfaces.py` file (or folder). Interfaces should not +import module logic or other module in global namespace. That is because +all of them must be imported before all OpenPype AddOns and Modules. + +Ideally they should just define abstract and helper methods. If interface +require any logic or connection it should be defined in module. + +Keep in mind that attributes and methods will be added to other addon +attributes and methods so they should be unique and ideally contain +addon name in it's name. +""" + +from abc import abstractmethod +from openpype.modules import OpenPypeInterface + + +class IExampleInterface(OpenPypeInterface): + """Example interface of addon.""" + _example_module = None + + def get_example_module(self): + return self._example_module + + @abstractmethod + def example_method_of_example_interface(self): + pass diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py new file mode 100644 index 00000000000..8e7fb410bd9 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -0,0 +1,10 @@ +import os +import pyblish.api + + +class CollectExampleAddon(pyblish.api.ContextPlugin): + order = pyblish.api.CollectorOrder + 0.4 + label = "Collect Example Addon" + + def process(self, context): + self.log.info("I'm in example addon's plugin!") diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -0,0 +1 @@ +{} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json new file mode 100644 index 00000000000..f6b7d5d146a --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "project_settings/global": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json new file mode 100644 index 00000000000..6895fb8f6d2 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "system_settings/modules": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json new file mode 100644 index 00000000000..80e53ace7f7 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "key": "exmaple_addon", + "collapsible": true, + "children": [ + { + "type": "number", + "key": "number", + "label": "This is your lucky number:", + "minimum": 7, + "maximum": 7, + "decimals": 0 + }, + { + "type": "template", + "name": "example_addon/the_template", + "template_data": [ + { + "name": "color_1", + "lable": "Color 1" + }, + { + "name": "color_2", + "lable": "Color 2" + } + ] + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json new file mode 100644 index 00000000000..af8fd9dae47 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json @@ -0,0 +1,30 @@ +[ + { + "type": "list-strict", + "key": "{name}", + "label": "{label}", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + } + ] + } +] diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json new file mode 100644 index 00000000000..0fb0a7c1bea --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json @@ -0,0 +1,14 @@ +{ + "type": "dict", + "key": "example_addon", + "label": "Example addon", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py new file mode 100644 index 00000000000..8a74ad859fe --- /dev/null +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -0,0 +1,30 @@ +from Qt import QtWidgets + + +class MyExampleDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(MyExampleDialog, self).__init__(parent) + + self.setWindowTitle("Connected modules") + + label_widget = QtWidgets.QLabel(self) + + ok_btn = QtWidgets.QPushButton("OK", self) + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(label_widget) + layout.addLayout(btns_layout) + + self._label_widget = label_widget + + def set_connected_modules(self, connected_modules): + if connected_modules: + message = "\n".join(connected_modules) + else: + message = ( + "Other enabled modules/addons are not using my interface." + ) + self._label_widget.setText(message) From 8c37b2b1419d1b5e3065a4ebfe42e305165c1273 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:18 +0200 Subject: [PATCH 03/11] fixed settings and widget of example addon --- .../example_addons/example_addon/__init__.py | 2 ++ .../example_addons/example_addon/addon.py | 10 +++++++++- .../settings/defaults/project_settings.json | 16 +++++++++++++++- .../settings/defaults/system_settings.json | 5 +++++ .../dynamic_schemas/project_dynamic_schemas.json | 2 +- .../dynamic_schemas/system_dynamic_schemas.json | 2 +- .../settings/schemas/project_schemas/main.json | 7 ++++--- .../example_addons/example_addon/widgets.py | 9 +++++++++ 8 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py index df4d61650b6..721d924436f 100644 --- a/openpype/modules/example_addons/example_addon/__init__.py +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -6,8 +6,10 @@ from .addon import ( AddonSettingsDef, + ExampleAddon ) __all__ = ( "AddonSettingsDef", + "ExampleAddon" ) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 64504be756c..5a25b80616d 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -31,7 +31,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): # recommended as schemas and templates may have name clashes across # multiple addons # - it is also recommended that prefix has addon name in it - schema_prefix = "addon_with_settings" + schema_prefix = "example_addon" def get_settings_root_path(self): """Implemented abstract class of JsonFilesSettingsDef. @@ -67,6 +67,14 @@ def initialize(self, settings): # UI which must not be created at this time self._dialog = None + def tray_init(self): + """Implementation of abstract method for `ITrayAction`. + + We're definetely in trat tool so we can precreate dialog. + """ + + self._create_dialog() + def connect_with_modules(self, enabled_modules): """Method where you should find connected modules. diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json index 0967ef424bc..0a01fa89778 100644 --- a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -1 +1,15 @@ -{} +{ + "project_settings/example_addon": { + "number": 0, + "color_1": [ + 0.0, + 0.0, + 0.0 + ], + "color_2": [ + 0.0, + 0.0, + 0.0 + ] + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json new file mode 100644 index 00000000000..1e773563738 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json @@ -0,0 +1,5 @@ +{ + "modules/example_addon": { + "enabled": true + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json index f6b7d5d146a..1f3da7b37f0 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -1,6 +1,6 @@ { "project_settings/global": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json index 6895fb8f6d2..6faa48ba744 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -1,6 +1,6 @@ { "system_settings/modules": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json index 80e53ace7f7..ba692d860e8 100644 --- a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -1,6 +1,7 @@ { "type": "dict", - "key": "exmaple_addon", + "key": "example_addon", + "label": "Example addon", "collapsible": true, "children": [ { @@ -17,11 +18,11 @@ "template_data": [ { "name": "color_1", - "lable": "Color 1" + "label": "Color 1" }, { "name": "color_2", - "lable": "Color 2" + "label": "Color 2" } ] } diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py index 8a74ad859fe..0acf2384098 100644 --- a/openpype/modules/example_addons/example_addon/widgets.py +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -1,5 +1,7 @@ from Qt import QtWidgets +from openpype.style import load_stylesheet + class MyExampleDialog(QtWidgets.QDialog): def __init__(self, parent=None): @@ -18,8 +20,15 @@ def __init__(self, parent=None): layout.addWidget(label_widget) layout.addLayout(btns_layout) + ok_btn.clicked.connect(self._on_ok_clicked) + self._label_widget = label_widget + self.setStyleSheet(load_stylesheet()) + + def _on_ok_clicked(self): + self.done(1) + def set_connected_modules(self, connected_modules): if connected_modules: message = "\n".join(connected_modules) From 4b0c8abcd52cd0254e4df73c314b98a2ec49f2fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:36 +0200 Subject: [PATCH 04/11] added new dynamic schema with name `system_settings/modules` --- .../entities/schemas/system_schema/schema_modules.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e047319..4287dd79053 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -242,6 +242,10 @@ "label": "Enabled" } ] + }, + { + "type": "dynamic_schema", + "name": "system_settings/modules" } ] } From fa8383859cc0c3d01520a0e6213ff3caf3d65da5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:10 +0200 Subject: [PATCH 05/11] added ability to use schema as first children of dynamic schema definition --- openpype/settings/entities/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index f207322dee7..bf3868c08db 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -168,9 +168,13 @@ def resolve_dynamic_schema(self, dynamic_key): if isinstance(def_schema, dict): def_schema = [def_schema] + all_def_schema = [] for item in def_schema: - item["_dynamic_schema_id"] = def_id - output.extend(def_schema) + items = self.resolve_schema_data(item) + for _item in items: + _item["_dynamic_schema_id"] = def_id + all_def_schema.extend(items) + output.extend(all_def_schema) return output def get_template_name(self, item_def, default=None): From 5bd1fa8a56b41913932df4aeb61a101109892c88 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:22 +0200 Subject: [PATCH 06/11] skip OpenPypeAddOn items too --- openpype/modules/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 01c3cebe60a..2cd11e5b947 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -495,6 +495,7 @@ def initialize_modules(self): if ( not inspect.isclass(modules_item) or modules_item is OpenPypeModule + or modules_item is OpenPypeAddOn or not issubclass(modules_item, OpenPypeModule) ): continue From 7f7c7e00620e4a20143b6d1a2d14a51958a5fc9e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:19:42 +0200 Subject: [PATCH 07/11] added few more information about addons settings to readme --- openpype/modules/README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a3733518acc..a6857b2c517 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -10,8 +10,6 @@ OpenPype modules should contain separated logic of specific kind of implementati - add module/addon manifest - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - defying that folder is content of a module or an addon -- module/addon have it's settings schemas and default values outside OpenPype -- add general setting of paths to modules ## Base class `OpenPypeModule` - abstract class as base for each module @@ -25,6 +23,26 @@ OpenPype modules should contain separated logic of specific kind of implementati - also keep in mind that they may be initialized in headless mode - connection with other modules is made with help of interfaces +## Addon class `OpenPypeAddOn` +- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) + +## How to add addons/modules +- in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder +- for openpype example addons use `{OPENPYPE_REPOS_ROOT}/openpype/modules/example_addons` + +## Addon/module settings +- addons/modules may have defined custom settings definitions with default values +- it is based on settings type `dynamic_schema` which has `name` + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherit from `BaseModuleSettingsDef` + - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons +- settings definition returns schemas by dynamic schemas names + # 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 From 0932ea8aba1ec9af50fcbafa87b7022a7ac6dca0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 13:25:13 +0200 Subject: [PATCH 08/11] removed unsused import --- .../example_addon/plugins/publish/example_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py index 8e7fb410bd9..695120e93b2 100644 --- a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -1,4 +1,3 @@ -import os import pyblish.api From 46f38b1e02888769f93727ae77baedc6182c5c26 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:18:02 +0200 Subject: [PATCH 09/11] Fix typos --- openpype/modules/README.md | 166 ++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a6857b2c517..abc7ed3961b 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -1,31 +1,31 @@ # OpenPype modules/addons -OpenPype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering or may contain only special plugins. Addons work the same way currently there is no difference in module and addon. +OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its usage code, Deadline farm rendering or may contain only special plugins. Addons work the same way currently, there is no difference between module and addon functionality. ## Modules concept -- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the modulo located -- modules or addons should never be imported directly even if you know possible full import path - - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts +- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the module located +- modules or addons should never be imported directly, even if you know possible full import path + - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts ### TODOs - add module/addon manifest - - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - - defying that folder is content of a module or an addon + - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) + - defying that folder is content of a module or an addon ## Base class `OpenPypeModule` - 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) +- implementation should contain module's api without GUI parts +- may implement `get_global_environments` method which should return dictionary of environments that are globally applicable 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 + - `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 overridden 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 ## Addon class `OpenPypeAddOn` -- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods - - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) +- inherits from `OpenPypeModule` but is enabled by default and doesn't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) ## How to add addons/modules - in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder @@ -34,110 +34,110 @@ OpenPype modules should contain separated logic of specific kind of implementati ## Addon/module settings - addons/modules may have defined custom settings definitions with default values - it is based on settings type `dynamic_schema` which has `name` - - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults - - they can't be added to any schema hierarchy - - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) - - addons may define it's dynamic schema items -- they can be defined with class which inherit from `BaseModuleSettingsDef` - - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values - - check it's docstring and check for `example_addon` in example addons + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherits from `BaseModuleSettingsDef` + - it is recommended to use pre implemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons - settings definition returns schemas by dynamic schemas names # Interfaces -- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods +- interface is class that has defined abstract methods to implement and may contain pre implemented 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 +- it is easy to find which module object inherited from which interfaces with 100% chance they have implemented required methods - interfaces can be defined in `interfaces.py` inside module directory - - the file can't use relative imports or import anything from other parts - of module itself at the header of file - - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation + - the file can't use relative imports or import anything from other parts + of module itself at the header of file + - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation ## Base class `OpenPypeInterface` - has nothing implemented - has ABCMeta as metaclass - is defined to be able find out classes which inherit from this base to be - able tell this is an Interface + able tell this is an Interface ## Global interfaces - few interfaces are implemented for global usage ### IPluginPaths -- module want to add directory path/s to avalon or publish plugins +- module wants 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 + - each key may contain list or string with a 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 +- module has more logic when used in a tray + - it is possible that module can be used only in the 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 + - `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 implementation 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 + - 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 +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to submenu "Services" in tray widget menu with icon and label +- abstract attribute `label` + - label shown in menu +- interface has pre implemented 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 +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to tray widget menu with label +- abstract attribute `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 + - what should happen when an action is triggered +- NOTE: It is a good idea to implement logic in `on_action_trigger` to the api method and trigger that method 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 -- +- modules may have defined their own interfaces to be able to 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 +- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which other modules want to add paths to server/user event handlers + - Clockify module use `IFtrackEventHandlerPaths` and returns paths to clockify ftrack synchronizers -- Clockify has more inharitance it's class definition looks like +- Clockify inherits from more interfaces. It's class definition looks like: ``` class ClockifyModule( - OpenPypeModule, # 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. + OpenPypeModule, # 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 +- collects 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 + - `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": [] - } - ``` + - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them + - `collect_plugin_paths` collects plugin paths from all enabled modules + - output is always dictionary with all keys and values as an list + ``` + { + "publish": [], + "create": [], + "load": [], + "actions": [] + } + ``` ### TrayModulesManager -- inherit from `ModulesManager` -- has specific implementations for Pype Tray tool and handle `ITrayModule` methods +- inherits from `ModulesManager` +- has specific implementation for Pype Tray tool and handle `ITrayModule` methods \ No newline at end of file From a7932ff6d536fb00415158b617d59a348afaa455 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:38:15 +0200 Subject: [PATCH 10/11] Fix typos --- .../modules/example_addons/example_addon/addon.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 5a25b80616d..5573e33cc15 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -21,11 +21,11 @@ ) -# Settings definiton of this addon using `JsonFilesSettingsDef` -# - JsonFilesSettingsDef is prepared settings definiton using json files -# to define settings and store defaul values +# Settings definition of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definition using json files +# to define settings and store default values class AddonSettingsDef(JsonFilesSettingsDef): - # This will add prefix to every schema and template from `schemas` + # This will add prefixes to every schema and template from `schemas` # subfolder. # - it is not required to fill the prefix but it is highly # recommended as schemas and templates may have name clashes across @@ -48,7 +48,7 @@ def get_settings_root_path(self): class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): """This Addon has defined it's settings and interface. - This example has system settings with enabled option. And use + This example has system settings with an enabled option. And use few other interfaces: - `IPluginPaths` to define custom plugin paths - `ITrayAction` to be shown in tray tool @@ -70,7 +70,7 @@ def initialize(self, settings): def tray_init(self): """Implementation of abstract method for `ITrayAction`. - We're definetely in trat tool so we can precreate dialog. + We're definitely in tray tool so we can pre create dialog. """ self._create_dialog() @@ -101,7 +101,7 @@ def show_dialog(self): """Show dialog with connected modules. This can be called from anywhere but can also crash in headless mode. - There is not way how to prevent addon to do invalid operations if he's + There is no way to prevent addon to do invalid operations if he's not handling them. """ # Make sure dialog is created From 0cb2cbb14f39a52befde9a6e7d7f4635594618fc Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 13 Sep 2021 12:27:31 +0200 Subject: [PATCH 11/11] Update openpype/modules/README.md --- openpype/modules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index abc7ed3961b..57163243651 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -9,7 +9,7 @@ OpenPype modules should contain separated logic of specific kind of implementati ### TODOs - add module/addon manifest - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) - - defying that folder is content of a module or an addon + - defining a folder as a content of a module or an addon ## Base class `OpenPypeModule` - abstract class as base for each module