diff --git a/docs/send2ue/customize/extensions.md b/docs/send2ue/customize/extensions.md index c69e22c2..b2ae0cdd 100644 --- a/docs/send2ue/customize/extensions.md +++ b/docs/send2ue/customize/extensions.md @@ -80,7 +80,18 @@ Then in the Send to Unreal addon preferences set the `Extensions Repo Folder` to Alternatively, this can be installed with python: ```python # this is handy for reloading your changes as you develop extensions - bpy.context.preferences.addons['send2ue'].preferences.extensions_repo_path = 'C:\extension_repo' + import bpy + from pathlib import Path + + my_extension_folder = r'C:\extension_repo' + preferences = bpy.context.preferences.addons['send2ue'].preferences + for extension_folder in preferences.extension_folder_list: + if Path(extension_folder.folder_path) == Path(my_extension_folder): + break + else: + extension_folder = preferences.extension_folder_list.add() + extension_folder.folder_path = my_extension_folder + bpy.ops.send2ue.reload_extensions() ``` diff --git a/docs/send2ue/customize/images/extensions/2.png b/docs/send2ue/customize/images/extensions/2.png index 0b78bc02..36224386 100644 Binary files a/docs/send2ue/customize/images/extensions/2.png and b/docs/send2ue/customize/images/extensions/2.png differ diff --git a/src/addons/send2ue/core/extension.py b/src/addons/send2ue/core/extension.py index 06899cea..7b1c4325 100644 --- a/src/addons/send2ue/core/extension.py +++ b/src/addons/send2ue/core/extension.py @@ -344,14 +344,15 @@ def _get_extension_classes(self): # add in the additional extensions from the addons preferences addon = bpy.context.preferences.addons.get(base_package) if addon and addon.preferences: - if os.path.exists(addon.preferences.extensions_repo_path): - for file_name in os.listdir(addon.preferences.extensions_repo_path): - name, file_extension = os.path.splitext(file_name) - if file_extension == '.py': - extension_collector = ExtensionCollector( - os.path.join(addon.preferences.extensions_repo_path, file_name) - ) - extensions.extend(extension_collector.get_extension_classes()) + for extension_folder in addon.preferences.extension_folder_list: # type: ignore + if os.path.exists(extension_folder.folder_path): + for file_name in os.listdir(extension_folder.folder_path): + name, file_extension = os.path.splitext(file_name) + if file_extension == '.py': + extension_collector = ExtensionCollector( + os.path.join(extension_folder.folder_path, file_name) + ) + extensions.extend(extension_collector.get_extension_classes()) # add in the extensions that shipped with the addon for file_name in os.listdir(self.source_path): diff --git a/src/addons/send2ue/operators.py b/src/addons/send2ue/operators.py index 4f4704ab..a2f2fefe 100644 --- a/src/addons/send2ue/operators.py +++ b/src/addons/send2ue/operators.py @@ -4,6 +4,7 @@ import bpy import queue import threading +from bl_ui.generic_ui_list import GenericUIListOperator from .constants import ToolInfo, ExtensionTasks from .core import export, utilities, settings, validations, extension from .ui import file_browser, dialog @@ -254,13 +255,13 @@ class ReloadExtensions(bpy.types.Operator): def execute(self, context): addon = bpy.context.preferences.addons.get(base_package) if addon: - extensions_repo_path = addon.preferences.extensions_repo_path - if extensions_repo_path: - if not os.path.exists(extensions_repo_path) or not os.path.isdir( - extensions_repo_path - ): - self.report(f'"{extensions_repo_path}" is not a folder path on disk.') - return {'FINISHED'} + for extension_folder in addon.preferences.extension_folder_list: # type: ignore + if extension_folder.folder_path: + if not os.path.exists(extension_folder.folder_path) or not os.path.isdir( + extension_folder.folder_path + ): + self.report(f'"{extension_folder.folder_path}" is not a folder path on disk.') + return {'FINISHED'} extension_factory = extension.ExtensionFactory() @@ -303,6 +304,44 @@ class NullOperator(bpy.types.Operator): def execute(self, context): return {'FINISHED'} + + +class UILIST_ADDON_PREFERENCES_OT_entry_remove(GenericUIListOperator, bpy.types.Operator): + """Remove the selected entry from the list""" + + bl_idname = "uilist.addon_preferences_entry_remove" + bl_label = "Remove Selected Entry" + + def execute(self, context): + addon_preferences = context.preferences.addons[ToolInfo.NAME.value] + my_list = self.get_list(addon_preferences) + active_index = self.get_active_index(addon_preferences) + + my_list.remove(active_index) + to_index = min(active_index, len(my_list) - 1) + self.set_active_index(addon_preferences, to_index) + + return {'FINISHED'} + + +class UILIST_ADDON_PREFERENCES_OT_entry_add(GenericUIListOperator, bpy.types.Operator): + """Add an entry to the list after the current active item""" + + bl_idname = "uilist.addon_preferences_entry_add" + bl_label = "Add Entry" + + def execute(self, context): + addon_preferences = context.preferences.addons[ToolInfo.NAME.value] + my_list = self.get_list(addon_preferences) + active_index = self.get_active_index(addon_preferences) + + to_index = min(len(my_list), active_index + 1) + + my_list.add() + my_list.move(len(my_list) - 1, to_index) + self.set_active_index(addon_preferences, to_index) + + return {'FINISHED'} operator_classes = [ @@ -316,6 +355,8 @@ def execute(self, context): ReloadExtensions, StartRPCServers, NullOperator, + UILIST_ADDON_PREFERENCES_OT_entry_remove, + UILIST_ADDON_PREFERENCES_OT_entry_add, ] diff --git a/src/addons/send2ue/properties.py b/src/addons/send2ue/properties.py index fd915808..e747a26c 100644 --- a/src/addons/send2ue/properties.py +++ b/src/addons/send2ue/properties.py @@ -2,11 +2,17 @@ import os import sys -import uuid import bpy from .constants import ToolInfo, PathModes, Template from .core import settings, formatting, extension +class ExtensionFolder(bpy.types.PropertyGroup): + folder_path: bpy.props.StringProperty( + default='', + description='The folder location of the extension repo.', + subtype='FILE_PATH' + ) # type: ignore + class Send2UeAddonProperties: """ @@ -17,14 +23,6 @@ class Send2UeAddonProperties: default=True, description=f"This automatically creates the pre-defined collection (Export)" ) - extensions_repo_path: bpy.props.StringProperty( - name="Extensions Repo Path", - default="", - description=( - "Set this path to the folder that contains your Send to Unreal python extensions. All extensions " - "in this folder will be automatically loaded" - ) - ) # ------------- Remote Execution settings ------------------ rpc_response_timeout: bpy.props.IntProperty( name="RPC Response Timeout", @@ -62,6 +60,9 @@ class Send2UeAddonProperties: ) ) + extension_folder_list: bpy.props.CollectionProperty(type=ExtensionFolder) # type: ignore + extension_folder_list_active_index: bpy.props.IntProperty() # type: ignore + class Send2UeWindowMangerProperties(bpy.types.PropertyGroup): """ diff --git a/src/addons/send2ue/ui/addon_preferences.py b/src/addons/send2ue/ui/addon_preferences.py index fb630771..bfc9f1a7 100644 --- a/src/addons/send2ue/ui/addon_preferences.py +++ b/src/addons/send2ue/ui/addon_preferences.py @@ -1,10 +1,108 @@ # Copyright Epic Games, Inc. All Rights Reserved. import bpy -from ..properties import Send2UeAddonProperties +from pathlib import Path +from ..properties import Send2UeAddonProperties, ExtensionFolder from ..constants import ToolInfo from .. import __package__ +from bl_ui.generic_ui_list import ( + _get_context_attr, # type: ignore + _draw_move_buttons # type: ignore +) + +def _draw_add_remove_buttons( + *, + layout, + list_path, + active_index_path, + list_length, +): + """Draw the +/- buttons to add and remove list entries.""" + props = layout.operator("uilist.addon_preferences_entry_add", text="", icon='ADD') + props.list_path = list_path + props.active_index_path = active_index_path + + row = layout.row() + row.enabled = list_length > 0 + props = row.operator("uilist.addon_preferences_entry_remove", text="", icon='REMOVE') + props.list_path = list_path + props.active_index_path = active_index_path + +def draw_ui_list( + layout, + context, + class_name="UI_UL_list", + *, + unique_id, + list_path, + active_index_path, + insertion_operators=True, + move_operators=True, + menu_class_name="", + **kwargs, +): + """ + This overrides the draw_ui_list function from the generic_ui_list module + so that we can draw the add and remove buttons for a list in the addon preferences. + By default, the generic_ui_list module buttons link to ops that receive the scene + context, which is not what we want in this case. So we had to create new ops that + do this job. + """ + + row = layout.row() + + list_owner_path, list_prop_name = list_path.rsplit('.', 1) + list_owner = _get_context_attr(context, list_owner_path) + + index_owner_path, index_prop_name = active_index_path.rsplit('.', 1) + index_owner = _get_context_attr(context, index_owner_path) + + list_to_draw = _get_context_attr(context, list_path) + + row.template_list( + class_name, + unique_id, + list_owner, list_prop_name, + index_owner, index_prop_name, + rows=4 if list_to_draw else 1, + **kwargs, + ) + + col = row.column() + + if insertion_operators: + _draw_add_remove_buttons( + layout=col, + list_path=list_path, + active_index_path=active_index_path, + list_length=len(list_to_draw), + ) + layout.separator() + + if menu_class_name: + col.menu(menu_class_name, icon='DOWNARROW_HLT', text="") + col.separator() + + if move_operators and list_to_draw: + _draw_move_buttons( + layout=col, + list_path=list_path, + active_index_path=active_index_path, + list_length=len(list_to_draw), + ) + + # Return the right-side column. + return col + + +class FOLDER_UL_extension_path(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_prop_name): + row = layout.row() + row.alert = False + if item.folder_path and not Path(item.folder_path).exists(): + row.alert = True + row.prop(item, "folder_path", text="", emboss=False) class SendToUnrealPreferences(Send2UeAddonProperties, bpy.types.AddonPreferences): """ @@ -35,16 +133,27 @@ def draw(self, context): row.prop(self, 'command_endpoint', text='') row = self.layout.row() - row.label(text='Extensions Repo Path:') + row.label(text='Extensions Repo Paths:') + row = self.layout.row() + draw_ui_list( + row, + context=bpy.context.preferences.addons[ToolInfo.NAME.value], + class_name="FOLDER_UL_extension_path", + list_path="preferences.extension_folder_list", + active_index_path="preferences.extension_folder_list_active_index", + unique_id="extension_folder_list_id", + insertion_operators=True, + move_operators=False, + ) # type: ignore row = self.layout.row() - row = row.split(factor=0.95, align=True) - row.prop(self, 'extensions_repo_path', text='') - row.operator('send2ue.reload_extensions', text='', icon='UV_SYNC_SELECT') + row.operator('send2ue.reload_extensions', text='Reload All Extensions', icon='FILE_REFRESH') def register(): """ Registers the addon preferences when the addon is enabled. """ + bpy.utils.register_class(ExtensionFolder) + bpy.utils.register_class(FOLDER_UL_extension_path) bpy.utils.register_class(SendToUnrealPreferences) @@ -53,3 +162,5 @@ def unregister(): Unregisters the addon preferences when the addon is disabled. """ bpy.utils.unregister_class(SendToUnrealPreferences) + bpy.utils.unregister_class(FOLDER_UL_extension_path) + bpy.utils.unregister_class(ExtensionFolder) diff --git a/tests/utils/base_test_case.py b/tests/utils/base_test_case.py index 5968b00f..4d20a879 100644 --- a/tests/utils/base_test_case.py +++ b/tests/utils/base_test_case.py @@ -144,12 +144,7 @@ def set_extension_repo(self, path): if self.test_environment: path = os.path.normpath(path).replace(os.path.sep, '/') - self.blender.set_addon_property( - 'preferences', - self.addon_name, - 'extensions_repo_path', - path - ) + self.blender.add_extension_repo(path) self.blender.run_addon_operator(self.addon_name, 'reload_extensions') def assert_extension_operators(self, extension_name, extension_operators, exists=True): diff --git a/tests/utils/blender.py b/tests/utils/blender.py index b6987537..c86470e2 100644 --- a/tests/utils/blender.py +++ b/tests/utils/blender.py @@ -4,6 +4,7 @@ import logging import importlib import tempfile +from pathlib import Path try: import bpy @@ -111,6 +112,16 @@ def set_addon_property(context_name, addon_name, property_name, value, data_type break properties = getattr(properties, sub_property_name) + @staticmethod + def add_extension_repo(file_path): + preferences = bpy.context.preferences.addons['send2ue'].preferences + for extension_folder in preferences.extension_folder_list: + if Path(extension_folder.folder_path) == Path(file_path): + break + else: + extension_folder = preferences.extension_folder_list.add() + extension_folder.folder_path = file_path + @staticmethod def check_particles(mesh_name, particle_names): """