Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changed extension repo to list #103

Merged
merged 5 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion docs/send2ue/customize/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
```

Expand Down
Binary file modified docs/send2ue/customize/images/extensions/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 9 additions & 8 deletions src/addons/send2ue/core/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
74 changes: 66 additions & 8 deletions src/addons/send2ue/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -254,13 +254,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()

Expand Down Expand Up @@ -303,6 +303,62 @@ class NullOperator(bpy.types.Operator):

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: bpy.props.StringProperty() # type: ignore
active_index_path: bpy.props.StringProperty() # type: ignore

def get_list(self, context):
return addon_preferences.get_context_attr(context, self.list_path)

def get_active_index(self, context):
return addon_preferences.get_context_attr(context, self.active_index_path)

def set_active_index(self, context, index):
addon_preferences.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"""

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 = [
Expand All @@ -316,6 +372,8 @@ def execute(self, context):
ReloadExtensions,
StartRPCServers,
NullOperator,
UILIST_ADDON_PREFERENCES_OT_entry_remove,
UILIST_ADDON_PREFERENCES_OT_entry_add,
]


Expand Down
19 changes: 10 additions & 9 deletions src/addons/send2ue/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand All @@ -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",
Expand Down Expand Up @@ -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):
"""
Expand Down
117 changes: 112 additions & 5 deletions src/addons/send2ue/ui/addon_preferences.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,106 @@
# 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__


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

# 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):
"""
This class creates the settings interface in the send to unreal addon.
Expand Down Expand Up @@ -35,16 +130,26 @@ 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
) # 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)


Expand All @@ -53,3 +158,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)
13 changes: 5 additions & 8 deletions tests/utils/base_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"')
Expand All @@ -144,12 +145,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):
Expand Down Expand Up @@ -240,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)

Expand Down
16 changes: 16 additions & 0 deletions tests/utils/blender.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import importlib
import tempfile
from pathlib import Path

try:
import bpy
Expand Down Expand Up @@ -111,6 +112,21 @@ 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 clear_extension_repos():
preferences = bpy.context.preferences.addons['send2ue'].preferences
preferences.extension_folder_list.clear()

@staticmethod
def check_particles(mesh_name, particle_names):
"""
Expand Down
Loading