From c5d00fb7aca74ce433c8e80ce34f3a38ec26e5d0 Mon Sep 17 00:00:00 2001 From: Jack Yao Date: Sat, 16 Nov 2024 16:48:14 -0600 Subject: [PATCH 1/5] Changed extension repo to list --- docs/send2ue/customize/extensions.md | 13 +- .../send2ue/customize/images/extensions/2.png | Bin 4143 -> 8531 bytes src/addons/send2ue/core/extension.py | 17 +-- src/addons/send2ue/operators.py | 55 +++++++- src/addons/send2ue/properties.py | 19 +-- src/addons/send2ue/ui/addon_preferences.py | 121 +++++++++++++++++- tests/utils/base_test_case.py | 7 +- tests/utils/blender.py | 11 ++ 8 files changed, 207 insertions(+), 36 deletions(-) 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 0b78bc0253bbebd0f0ddb430fc00f24621d7f8e1..3622438620736d0ff967db4d2463109377573183 100644 GIT binary patch literal 8531 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 literal 4143 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 Np`NL3G4g)ce*jsD79jut 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): """ From a57a64ae4989456a4305fefd642b20488855f81d Mon Sep 17 00:00:00 2001 From: Jack Yao Date: Sat, 16 Nov 2024 17:53:30 -0600 Subject: [PATCH 2/5] removed blender 4 code and implemented ui list to be compatible with 3.6 --- src/addons/send2ue/operators.py | 19 ++++++++++++++++++- src/addons/send2ue/ui/addon_preferences.py | 19 ++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/addons/send2ue/operators.py b/src/addons/send2ue/operators.py index a2f2fefe..1c4a71f1 100644 --- a/src/addons/send2ue/operators.py +++ b/src/addons/send2ue/operators.py @@ -4,7 +4,6 @@ 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 @@ -306,6 +305,24 @@ def execute(self, context): return {'FINISHED'} +class GenericUIListOperator: + """Mix-in class containing functionality shared by operators + that deal with managing Blender list entries.""" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + list_path: StringProperty() + active_index_path: StringProperty() + + def get_list(self, context): + return _get_context_attr(context, self.list_path) + + def get_active_index(self, context): + return _get_context_attr(context, self.active_index_path) + + def set_active_index(self, context, index): + _set_context_attr(context, self.active_index_path, index) + + class UILIST_ADDON_PREFERENCES_OT_entry_remove(GenericUIListOperator, bpy.types.Operator): """Remove the selected entry from the list""" diff --git a/src/addons/send2ue/ui/addon_preferences.py b/src/addons/send2ue/ui/addon_preferences.py index bfc9f1a7..45f90da0 100644 --- a/src/addons/send2ue/ui/addon_preferences.py +++ b/src/addons/send2ue/ui/addon_preferences.py @@ -6,10 +6,9 @@ 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 _get_context_attr(context, data_path): + """Return the value of a context member based on its data path.""" + return context.path_resolve(data_path) def _draw_add_remove_buttons( *, @@ -38,7 +37,6 @@ def draw_ui_list( list_path, active_index_path, insertion_operators=True, - move_operators=True, menu_class_name="", **kwargs, ): @@ -84,14 +82,6 @@ def draw_ui_list( 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 @@ -142,8 +132,7 @@ def draw(self, context): 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, + insertion_operators=True ) # type: ignore row = self.layout.row() row.operator('send2ue.reload_extensions', text='Reload All Extensions', icon='FILE_REFRESH') From de38977be2793155ea2dca2274ccd4406152dd78 Mon Sep 17 00:00:00 2001 From: Jack Yao Date: Sat, 16 Nov 2024 18:39:56 -0600 Subject: [PATCH 3/5] Refactor context attribute handling in GenericUIListOperator to use from addon_preferences --- src/addons/send2ue/operators.py | 12 ++++++------ src/addons/send2ue/ui/addon_preferences.py | 15 +++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/addons/send2ue/operators.py b/src/addons/send2ue/operators.py index 1c4a71f1..925931b4 100644 --- a/src/addons/send2ue/operators.py +++ b/src/addons/send2ue/operators.py @@ -6,7 +6,7 @@ import threading from .constants import ToolInfo, ExtensionTasks from .core import export, utilities, settings, validations, extension -from .ui import file_browser, dialog +from .ui import file_browser, dialog, addon_preferences from .dependencies import unreal from .dependencies.rpc import blender_server from .properties import register_scene_properties, unregister_scene_properties @@ -310,17 +310,17 @@ class GenericUIListOperator: that deal with managing Blender list entries.""" bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - list_path: StringProperty() - active_index_path: StringProperty() + list_path: bpy.props.StringProperty() # type: ignore + active_index_path: bpy.props.StringProperty() # type: ignore def get_list(self, context): - return _get_context_attr(context, self.list_path) + return addon_preferences.get_context_attr(context, self.list_path) def get_active_index(self, context): - return _get_context_attr(context, self.active_index_path) + return addon_preferences.get_context_attr(context, self.active_index_path) def set_active_index(self, context, index): - _set_context_attr(context, self.active_index_path, index) + addon_preferences.set_context_attr(context, self.active_index_path, index) class UILIST_ADDON_PREFERENCES_OT_entry_remove(GenericUIListOperator, bpy.types.Operator): diff --git a/src/addons/send2ue/ui/addon_preferences.py b/src/addons/send2ue/ui/addon_preferences.py index 45f90da0..c7b23192 100644 --- a/src/addons/send2ue/ui/addon_preferences.py +++ b/src/addons/send2ue/ui/addon_preferences.py @@ -6,10 +6,17 @@ from ..constants import ToolInfo from .. import __package__ -def _get_context_attr(context, data_path): + +def get_context_attr(context, data_path): """Return the value of a context member based on its data path.""" return context.path_resolve(data_path) +def set_context_attr(context, data_path, value): + """Set the value of a context member based on its data path.""" + owner_path, attr_name = data_path.rsplit('.', 1) + owner = context.path_resolve(owner_path) + setattr(owner, attr_name, value) + def _draw_add_remove_buttons( *, layout, @@ -51,12 +58,12 @@ def draw_ui_list( row = layout.row() list_owner_path, list_prop_name = list_path.rsplit('.', 1) - list_owner = _get_context_attr(context, list_owner_path) + 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) + index_owner = get_context_attr(context, index_owner_path) - list_to_draw = _get_context_attr(context, list_path) + list_to_draw = get_context_attr(context, list_path) row.template_list( class_name, From ec3062c052b129ea34c0dbc9928f409b9a41c7a6 Mon Sep 17 00:00:00 2001 From: Jack Yao Date: Sat, 16 Nov 2024 19:13:49 -0600 Subject: [PATCH 4/5] cleared extension folder at end of test --- tests/utils/blender.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/utils/blender.py b/tests/utils/blender.py index c86470e2..1b60ac69 100644 --- a/tests/utils/blender.py +++ b/tests/utils/blender.py @@ -115,6 +115,12 @@ def set_addon_property(context_name, addon_name, property_name, value, data_type @staticmethod def add_extension_repo(file_path): preferences = bpy.context.preferences.addons['send2ue'].preferences + + # clear the list if the file path is empty + if not file_path: + preferences.extension_folder_list.clear() + return + for extension_folder in preferences.extension_folder_list: if Path(extension_folder.folder_path) == Path(file_path): break From 11e0f3dbd68745dc242c24248eb2bd87434087f3 Mon Sep 17 00:00:00 2001 From: Jack Yao Date: Sun, 17 Nov 2024 09:48:56 -0600 Subject: [PATCH 5/5] fixed logic that clears extension repo paths --- tests/utils/base_test_case.py | 6 ++++-- tests/utils/blender.py | 11 +++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/utils/base_test_case.py b/tests/utils/base_test_case.py index 4d20a879..e3c247dd 100644 --- a/tests/utils/base_test_case.py +++ b/tests/utils/base_test_case.py @@ -135,7 +135,8 @@ def __init__(self, *args, **kwargs): def setUp(self): super().setUp() - self.set_extension_repo('') + self.blender.clear_extension_repos() + self.blender.run_addon_operator(self.addon_name, 'reload_extensions') def set_extension_repo(self, path): self.log(f'Setting the addon extension repo to "{path}"') @@ -235,7 +236,8 @@ def run_extension_tests(self, extensions): self.assert_extension(extension_name, extensions_data) # check that external extensions are removed are being removed correctly - self.set_extension_repo('') + self.blender.clear_extension_repos() + self.blender.run_addon_operator(self.addon_name, 'reload_extensions') for extension_name, extensions_data in external_extensions.items(): self.assert_extension(extension_name, extensions_data, False) diff --git a/tests/utils/blender.py b/tests/utils/blender.py index 1b60ac69..108863c1 100644 --- a/tests/utils/blender.py +++ b/tests/utils/blender.py @@ -115,12 +115,6 @@ def set_addon_property(context_name, addon_name, property_name, value, data_type @staticmethod def add_extension_repo(file_path): preferences = bpy.context.preferences.addons['send2ue'].preferences - - # clear the list if the file path is empty - if not file_path: - preferences.extension_folder_list.clear() - return - for extension_folder in preferences.extension_folder_list: if Path(extension_folder.folder_path) == Path(file_path): break @@ -128,6 +122,11 @@ def add_extension_repo(file_path): extension_folder = preferences.extension_folder_list.add() extension_folder.folder_path = file_path + @staticmethod + def clear_extension_repos(): + preferences = bpy.context.preferences.addons['send2ue'].preferences + preferences.extension_folder_list.clear() + @staticmethod def check_particles(mesh_name, particle_names): """