Skip to content

Commit

Permalink
Merge pull request #290 from Hubs-Foundation/user-modules-dirs
Browse files Browse the repository at this point in the history
Support for user components directories
  • Loading branch information
keianhzo authored Jun 5, 2024
2 parents 95cea92 + 4b14ca2 commit 0b96d30
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 3 deletions.
70 changes: 70 additions & 0 deletions addons/io_hubs_addon/components/components_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,43 @@ 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 addon_prefs.user_components_paths:
if entry.path and os.path.isdir(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_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


def get_component_definitions():
components_dir = join(dirname(realpath(__file__)), "definitions")
component_module_names = get_components_in_dir(components_dir)
Expand Down Expand Up @@ -113,6 +150,32 @@ 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__:
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():
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
Expand All @@ -125,6 +188,13 @@ def load_components_registry():
register_component(member)
__components_registry[member.get_name()] = member

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


def unload_components_registry():
"""Recurse in the components directory to unload the registered components"""
Expand Down
1 change: 1 addition & 0 deletions addons/io_hubs_addon/components/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Category(Enum):
MISC = 'Misc'
LIGHTS = 'Lights'
MEDIA = 'Media'
USER = 'User'


class MigrationType(Enum):
Expand Down
109 changes: 106 additions & 3 deletions addons/io_hubs_addon/preferences.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -130,6 +130,101 @@ 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',
update=set_prefs_dirty
)


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'}

def execute(self, context):
addon_prefs = addon_prefs = get_addon_pref(bpy.context)
paths = addon_prefs.user_components_paths
paths.add()

context.preferences.is_dirty = True

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)

context.preferences.is_dirty = True

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__

Expand Down Expand Up @@ -163,18 +258,20 @@ 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()

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()
Expand Down Expand Up @@ -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)
Expand All @@ -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)
5 changes: 5 additions & 0 deletions addons/io_hubs_addon/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 0b96d30

Please sign in to comment.