From f614f9f6b217ef7be4f06d11ba869d59c3942ac6 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Thu, 23 May 2024 11:40:33 +0200 Subject: [PATCH 1/8] Support for user components directories --- .../components/components_registry.py | 53 +++++++++ addons/io_hubs_addon/components/types.py | 1 + addons/io_hubs_addon/preferences.py | 107 +++++++++++++++++- 3 files changed, 159 insertions(+), 2 deletions(-) diff --git a/addons/io_hubs_addon/components/components_registry.py b/addons/io_hubs_addon/components/components_registry.py index cb4a1307..c73a4929 100644 --- a/addons/io_hubs_addon/components/components_registry.py +++ b/addons/io_hubs_addon/components/components_registry.py @@ -34,6 +34,35 @@ def get_components_in_dir(dir): return sorted(components) +def get_user_component_names(): + component_names = [] + from ..preferences import get_addon_pref + addon_prefs = get_addon_pref(bpy.context) + for _, entry in enumerate(addon_prefs.user_components_paths): + if entry.path: + component_names = get_components_in_dir(entry.path) + return component_names + + +def get_user_component_definitions(): + modules = [] + component_names = get_user_component_names() + for name in component_names: + from ..preferences import get_addon_pref + addon_prefs = get_addon_pref(bpy.context) + for _, entry in enumerate(addon_prefs.user_components_paths): + file_path = os.path.join(entry.path, name + ".py") + try: + spec = importlib.util.spec_from_file_location(name, file_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + modules.append(mod) + + except Exception as e: + print(f'Failed import of component {name}', e) + return modules + + def get_component_definitions(): components_dir = join(dirname(realpath(__file__)), "definitions") component_module_names = get_components_in_dir(components_dir) @@ -113,6 +142,28 @@ def unregister_component(component_class): print(f"Component unregistered: {component_class.get_name()}") +def load_user_components(): + global __components_registry + for module in get_user_component_definitions(): + for _, member in inspect.getmembers(module): + if inspect.isclass(member) and issubclass(member, HubsComponent) and module.__name__ == member.__module__: + if hasattr(module, 'register_module'): + module.register_module() + register_component(member) + __components_registry[member.get_name()] = member + + +def unload_user_components(): + global __components_registry + for _, component_class in __components_registry.items(): + for module_name in get_user_component_names(): + if module_name == component_class.get_name(): + unregister_component(component_class) + for module in get_user_component_definitions(): + if hasattr(module, 'unregister_module'): + module.unregister_module() + + def load_components_registry(): """Recurse in the components directory to build the components registry""" global __components_registry @@ -125,6 +176,8 @@ def load_components_registry(): register_component(member) __components_registry[member.get_name()] = member + load_user_components() + def unload_components_registry(): """Recurse in the components directory to unload the registered components""" diff --git a/addons/io_hubs_addon/components/types.py b/addons/io_hubs_addon/components/types.py index c28acc6c..a54e5ac3 100644 --- a/addons/io_hubs_addon/components/types.py +++ b/addons/io_hubs_addon/components/types.py @@ -23,6 +23,7 @@ class Category(Enum): MISC = 'Misc' LIGHTS = 'Lights' MEDIA = 'Media' + USER = 'User' class MigrationType(Enum): diff --git a/addons/io_hubs_addon/preferences.py b/addons/io_hubs_addon/preferences.py index c0b9c8e4..fe63ac10 100644 --- a/addons/io_hubs_addon/preferences.py +++ b/addons/io_hubs_addon/preferences.py @@ -130,6 +130,101 @@ def execute(self, context): return {'FINISHED'} +def reload_user_components(): + from .components.components_registry import unload_user_components, load_user_components + unload_user_components() + load_user_components() + + +class HubsUserComponentsPath(bpy.types.PropertyGroup): + name: StringProperty( + name='User components path entry name', + description='The user components path entry name.', + ) + path: StringProperty( + name='User components path path', + description='The user components path path. You can copy external components here.', + subtype='FILE_PATH' + ) + + +class HubsUserComponentsPathAdd(bpy.types.Operator): + bl_idname = "hubs_preferences.add_user_components_path" + bl_label = "Add user components path" + bl_description = "Adds a new component path entry" + bl_options = {'REGISTER', 'UNDO'} + + path: bpy.props.StringProperty(name="User Components Path", default="") + + def execute(self, context): + addon_prefs = addon_prefs = get_addon_pref(bpy.context) + paths = addon_prefs.user_components_paths + new_path = paths.add() + new_path.path = self.path + + return {'FINISHED'} + + +class HubsUserComponentsPathRemove(bpy.types.Operator): + bl_idname = "hubs_preferences.remove_user_components_path" + bl_label = "Remove user components path entry" + bl_options = {'REGISTER', 'UNDO'} + + index: bpy.props.IntProperty(name="User Components Path Index", default=0) + + def execute(self, context): + addon_prefs = addon_prefs = get_addon_pref(bpy.context) + paths = addon_prefs.user_components_paths + paths.remove(self.index) + + return {'FINISHED'} + + +def draw_user_modules_path_panel(context, layout, prefs): + box = layout.box() + box.row().label(text="Additional components directories:") + + dirs_layout = box.row() + + entries = prefs.user_components_paths + + if len(entries) == 0: + dirs_layout.operator(HubsUserComponentsPathAdd.bl_idname, + text="Add", icon='ADD') + return + + dirs_layout.use_property_split = False + dirs_layout.use_property_decorate = False + + box = dirs_layout.box() + split = box.split(factor=0.35) + name_col = split.column() + path_col = split.column() + + row = name_col.row(align=True) # Padding + row.separator() + row.label(text="Name") + + row = path_col.row(align=True) # Padding + row.separator() + row.label(text="Path") + + row.operator(HubsUserComponentsPathAdd.bl_idname, + text="", icon='ADD', emboss=False) + + for i, entry in enumerate(entries): + row = name_col.row() + row.alert = not entry.name + row.prop(entry, "name", text="") + + row = path_col.row() + subrow = row.row() + subrow.alert = not entry.path + subrow.prop(entry, "path", text="") + row.operator(HubsUserComponentsPathRemove.bl_idname, + text="", icon='X', emboss=False).index = i + + class HubsPreferences(AddonPreferences): bl_idname = __package__ @@ -163,6 +258,8 @@ class HubsPreferences(AddonPreferences): chrome_path: StringProperty( name="Chrome executable path", description="Binary path", subtype='FILE_PATH') + user_components_paths: CollectionProperty(type=HubsUserComponentsPath) + def draw(self, context): layout = self.layout box = layout.box() @@ -170,11 +267,11 @@ def draw(self, context): box.row().prop(self, "row_length") box.row().prop(self, "recast_lib_path") - selenium_available = is_module_available("selenium") - modules_available = selenium_available + draw_user_modules_path_panel(context, layout, self) box = layout.box() box.label(text="Scene debugger configuration") + modules_available = is_module_available("selenium") if modules_available: browser_box = box.box() row = browser_box.row() @@ -236,6 +333,9 @@ def draw(self, context): def register(): + bpy.utils.register_class(HubsUserComponentsPath) + bpy.utils.register_class(HubsUserComponentsPathAdd) + bpy.utils.register_class(HubsUserComponentsPathRemove) bpy.utils.register_class(DepsProperty) bpy.utils.register_class(HubsPreferences) bpy.utils.register_class(InstallDepsOperator) @@ -249,3 +349,6 @@ def unregister(): bpy.utils.unregister_class(InstallDepsOperator) bpy.utils.unregister_class(HubsPreferences) bpy.utils.unregister_class(DepsProperty) + bpy.utils.unregister_class(HubsUserComponentsPathRemove) + bpy.utils.unregister_class(HubsUserComponentsPathAdd) + bpy.utils.unregister_class(HubsUserComponentsPath) From 212705b5949206c066a5061d8b7f2e868518e76f Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Fri, 24 May 2024 11:55:45 +0200 Subject: [PATCH 2/8] Load user components only if add-on is enabled --- addons/io_hubs_addon/components/components_registry.py | 6 +++++- addons/io_hubs_addon/utils.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/addons/io_hubs_addon/components/components_registry.py b/addons/io_hubs_addon/components/components_registry.py index c73a4929..9cea8a6b 100644 --- a/addons/io_hubs_addon/components/components_registry.py +++ b/addons/io_hubs_addon/components/components_registry.py @@ -176,7 +176,11 @@ def load_components_registry(): register_component(member) __components_registry[member.get_name()] = member - load_user_components() + # Preferences are not accessible until the add-on is enabled so we need to check that before loading the user + # components as they depend on the preferences. + from ..utils import is_addon_enabled + if is_addon_enabled(): + load_user_components() def unload_components_registry(): diff --git a/addons/io_hubs_addon/utils.py b/addons/io_hubs_addon/utils.py index dbe4c39c..f8a13dd5 100644 --- a/addons/io_hubs_addon/utils.py +++ b/addons/io_hubs_addon/utils.py @@ -286,3 +286,8 @@ def image_type_to_file_ext(image_type): elif image_type == 'TARGA_RAW': file_extension = '.tga' return file_extension + + +def is_addon_enabled(): + import bpy + return get_addon_package() in bpy.context.preferences.addons From 8b95b5a6be1527d2b1f246b3f6b907b9efc9caa1 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Tue, 4 Jun 2024 10:01:43 +0200 Subject: [PATCH 3/8] Address feedback --- .../components/components_registry.py | 14 +++++++++----- addons/io_hubs_addon/preferences.py | 15 +++------------ 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/addons/io_hubs_addon/components/components_registry.py b/addons/io_hubs_addon/components/components_registry.py index 9cea8a6b..04ee40b4 100644 --- a/addons/io_hubs_addon/components/components_registry.py +++ b/addons/io_hubs_addon/components/components_registry.py @@ -39,7 +39,7 @@ def get_user_component_names(): from ..preferences import get_addon_pref addon_prefs = get_addon_pref(bpy.context) for _, entry in enumerate(addon_prefs.user_components_paths): - if entry.path: + if entry.path and os.path.isdir(entry.path): component_names = get_components_in_dir(entry.path) return component_names @@ -147,10 +147,14 @@ def load_user_components(): for module in get_user_component_definitions(): for _, member in inspect.getmembers(module): if inspect.isclass(member) and issubclass(member, HubsComponent) and module.__name__ == member.__module__: - if hasattr(module, 'register_module'): - module.register_module() - register_component(member) - __components_registry[member.get_name()] = member + try: + if hasattr(module, 'register_module'): + module.register_module() + register_component(member) + __components_registry[member.get_name()] = member + except Exception: + import traceback + traceback.print_exc() def unload_user_components(): diff --git a/addons/io_hubs_addon/preferences.py b/addons/io_hubs_addon/preferences.py index fe63ac10..547313c7 100644 --- a/addons/io_hubs_addon/preferences.py +++ b/addons/io_hubs_addon/preferences.py @@ -130,20 +130,14 @@ def execute(self, context): return {'FINISHED'} -def reload_user_components(): - from .components.components_registry import unload_user_components, load_user_components - unload_user_components() - load_user_components() - - class HubsUserComponentsPath(bpy.types.PropertyGroup): name: StringProperty( name='User components path entry name', - description='The user components path entry name.', + description='An optional, user defined label to allow quick discernment between different user component definition directories.', ) path: StringProperty( name='User components path path', - description='The user components path path. You can copy external components here.', + description='The path to a user defined component definitions directory. You can copy external components here and they will be loaded automatically.', subtype='FILE_PATH' ) @@ -154,13 +148,10 @@ class HubsUserComponentsPathAdd(bpy.types.Operator): bl_description = "Adds a new component path entry" bl_options = {'REGISTER', 'UNDO'} - path: bpy.props.StringProperty(name="User Components Path", default="") - def execute(self, context): addon_prefs = addon_prefs = get_addon_pref(bpy.context) paths = addon_prefs.user_components_paths - new_path = paths.add() - new_path.path = self.path + paths.add() return {'FINISHED'} From 1db0eb2f7981480a6ca934ae091bd45242353ac1 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 5 Jun 2024 10:15:09 +0200 Subject: [PATCH 4/8] Improve comment --- addons/io_hubs_addon/components/components_registry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/io_hubs_addon/components/components_registry.py b/addons/io_hubs_addon/components/components_registry.py index 04ee40b4..9447d1f2 100644 --- a/addons/io_hubs_addon/components/components_registry.py +++ b/addons/io_hubs_addon/components/components_registry.py @@ -180,8 +180,9 @@ def load_components_registry(): register_component(member) __components_registry[member.get_name()] = member - # Preferences are not accessible until the add-on is enabled so we need to check that before loading the user - # components as they depend on the preferences. + # When running Blender in factory startup mode and specifying an addon, that addon's register function is called. + # As preferences are not available until the addon is enabled, the user component load fails when accessing them. + # This happens when running tests and this guard avoids crashing in that scenario. from ..utils import is_addon_enabled if is_addon_enabled(): load_user_components() From cf808a574ee55451beb31acfb47a267a6882811e Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 5 Jun 2024 10:23:29 +0200 Subject: [PATCH 5/8] add missing import --- addons/io_hubs_addon/preferences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/io_hubs_addon/preferences.py b/addons/io_hubs_addon/preferences.py index 547313c7..7e19b4a2 100644 --- a/addons/io_hubs_addon/preferences.py +++ b/addons/io_hubs_addon/preferences.py @@ -1,6 +1,6 @@ import bpy from bpy.types import AddonPreferences, Context -from bpy.props import IntProperty, StringProperty, EnumProperty, BoolProperty, PointerProperty +from bpy.props import IntProperty, StringProperty, EnumProperty, BoolProperty, PointerProperty, CollectionProperty from .utils import get_addon_package, is_module_available, get_browser_profile_directory import platform from os.path import join, dirname, realpath From 929bd90145fc4fd36fc771b874fd060c42353ee8 Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 5 Jun 2024 10:33:10 +0200 Subject: [PATCH 6/8] Mark preferences as dirty after adding/removing user dirs --- addons/io_hubs_addon/preferences.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/io_hubs_addon/preferences.py b/addons/io_hubs_addon/preferences.py index 7e19b4a2..8a392127 100644 --- a/addons/io_hubs_addon/preferences.py +++ b/addons/io_hubs_addon/preferences.py @@ -153,6 +153,8 @@ def execute(self, context): paths = addon_prefs.user_components_paths paths.add() + context.preferences.is_dirty = True + return {'FINISHED'} @@ -168,6 +170,8 @@ def execute(self, context): paths = addon_prefs.user_components_paths paths.remove(self.index) + context.preferences.is_dirty = True + return {'FINISHED'} From e2cb3cd721af3821ff3c944ae969eb0f831d27ef Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 5 Jun 2024 11:23:51 +0200 Subject: [PATCH 7/8] Fix user dir entries iteration --- .../components/components_registry.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/addons/io_hubs_addon/components/components_registry.py b/addons/io_hubs_addon/components/components_registry.py index 9447d1f2..72594581 100644 --- a/addons/io_hubs_addon/components/components_registry.py +++ b/addons/io_hubs_addon/components/components_registry.py @@ -38,28 +38,36 @@ def get_user_component_names(): component_names = [] from ..preferences import get_addon_pref addon_prefs = get_addon_pref(bpy.context) - for _, entry in enumerate(addon_prefs.user_components_paths): + for entry in addon_prefs.user_components_paths: if entry.path and os.path.isdir(entry.path): - component_names = get_components_in_dir(entry.path) + component_names.append(get_components_in_dir(entry.path)) return component_names +def get_user_component_paths(): + component_paths = [] + from ..preferences import get_addon_pref + addon_prefs = get_addon_pref(bpy.context) + for entry in addon_prefs.user_components_paths: + if entry.path and os.path.isdir(entry.path): + components = get_components_in_dir(entry.path) + for component in components: + component_paths.append(os.path.join(entry.path, component + ".py")) + return component_paths + + def get_user_component_definitions(): modules = [] - component_names = get_user_component_names() - for name in component_names: - from ..preferences import get_addon_pref - addon_prefs = get_addon_pref(bpy.context) - for _, entry in enumerate(addon_prefs.user_components_paths): - file_path = os.path.join(entry.path, name + ".py") - try: - spec = importlib.util.spec_from_file_location(name, file_path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - modules.append(mod) - - except Exception as e: - print(f'Failed import of component {name}', e) + component_paths = get_user_component_paths() + for component_path in component_paths: + try: + spec = importlib.util.spec_from_file_location(component_path, component_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + modules.append(mod) + + except Exception as e: + print(f'Failed import of component {component_path}', e) return modules From 4b14ca2007314bdb9850614984223127dca2bf1f Mon Sep 17 00:00:00 2001 From: Manuel Martin Date: Wed, 5 Jun 2024 12:09:41 +0200 Subject: [PATCH 8/8] Mark prefs as dirty when user dirs are modified --- addons/io_hubs_addon/preferences.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/io_hubs_addon/preferences.py b/addons/io_hubs_addon/preferences.py index 8a392127..31e7342e 100644 --- a/addons/io_hubs_addon/preferences.py +++ b/addons/io_hubs_addon/preferences.py @@ -130,15 +130,20 @@ def execute(self, context): return {'FINISHED'} +def set_prefs_dirty(self, context): + context.preferences.is_dirty = True + + class HubsUserComponentsPath(bpy.types.PropertyGroup): name: StringProperty( name='User components path entry name', description='An optional, user defined label to allow quick discernment between different user component definition directories.', - ) + update=set_prefs_dirty) path: StringProperty( name='User components path path', description='The path to a user defined component definitions directory. You can copy external components here and they will be loaded automatically.', - subtype='FILE_PATH' + subtype='FILE_PATH', + update=set_prefs_dirty )