From e641ac2d8a2baa146478d902a4222a3243a4f764 Mon Sep 17 00:00:00 2001 From: Demeter Dzadik Date: Thu, 20 Jan 2022 14:21:16 +0100 Subject: Rigify: Add ability to disable installed feature sets This patch is a continuation of D8519, was also suggested in T88711#1170152. It adds a checkbox for each feature set in the Rigify Preferences, to disable/enable that feature set without having to completely remove it from the file system. Challenges that were hopefully successfully tackled: - Keep list in sync when user manually adds or removes things through the file system instead of the "add/remove feature set" buttons in the UI. - Avoid re-building the feature set list all the time so that the checkbox states can actually be stored when the user exits Blender. - Disabling a feature set means calling its unregister function, then rebuilding the rig types and metarigs lists/menus. - Some renaming slipped in because I found the variable name "feature_set" a bit confusing. If needed, I could split this change into a separate patch or just forget about it, but I think the longer names help here. Testing would be welcome, since things turned out a bit more tricky than expected. In a follow-up patch I would like to do a code quality pass, to split the code a bit better here. There is a bit too much stuff in __init__.py in particular. I will get started on that when this gets close to being finalized. Reviewed By: angavrilov Differential Revision: https://developer.blender.org/D12260 --- rigify/__init__.py | 89 ++++++++++++++++++++++++++++++++++------------ rigify/feature_set_list.py | 52 +++++++++++++++++++-------- rigify/metarig_menu.py | 7 ++-- rigify/ui.py | 6 ++-- 4 files changed, 110 insertions(+), 44 deletions(-) diff --git a/rigify/__init__.py b/rigify/__init__.py index 20e18a42..5d8782d4 100644 --- a/rigify/__init__.py +++ b/rigify/__init__.py @@ -149,6 +149,33 @@ class RigifyFeatureSets(bpy.types.PropertyGroup): name: bpy.props.StringProperty() module_name: bpy.props.StringProperty() + def toggle_featureset(self, context): + feature_set_list.call_register_function(self.module_name, self.enabled) + context.preferences.addons[__package__].preferences.update_external_rigs() + + enabled: bpy.props.BoolProperty( + name = "Enabled", + description = "Whether this feature-set is registered or not", + update = toggle_featureset, + default = True + ) + + +class RIGIFY_UL_FeatureSets(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + rigify_prefs = data + feature_sets = rigify_prefs.rigify_feature_sets + active_set = feature_sets[rigify_prefs.active_feature_set_index] + feature_set_entry = item + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row() + row.prop(feature_set_entry, 'name', text="", emboss=False) + + icon = 'CHECKBOX_HLT' if feature_set_entry.enabled else 'CHECKBOX_DEHLT' + row.enabled = feature_set_entry.enabled + layout.prop(feature_set_entry, 'enabled', text="", icon=icon, emboss=False) + elif self.layout_type in {'GRID'}: + pass class RigifyPreferences(AddonPreferences): # this must match the addon name, use '__package__' @@ -157,32 +184,50 @@ class RigifyPreferences(AddonPreferences): def register_feature_sets(self, register): """Call register or unregister of external feature sets""" - for set_name in feature_set_list.get_installed_list(): + for set_name in feature_set_list.get_enabled_modules_names(): feature_set_list.call_register_function(set_name, register) - def update_external_rigs(self, force=False): + def refresh_installed_feature_sets(self): + """Synchronize preferences entries with what's actually in the file system.""" + feature_set_prefs = self.rigify_feature_sets + + module_names = feature_set_list.get_installed_modules_names() + + # If there is a feature set preferences entry with no corresponding + # installed module, user must've manually removed it from the filesystem, + # so let's remove such entries. + to_delete = [ i for i, fs in enumerate(feature_set_prefs) if fs.module_name not in module_names ] + for i in reversed(to_delete): + feature_set_prefs.remove(i) + + # If there is an installed feature set in the file system but no corresponding + # entry, user must've installed it manually. Make sure it has an entry. + for module_name in module_names: + for fs in feature_set_prefs: + if module_name == fs.module_name: + break + else: + fs = feature_set_prefs.add() + fs.name = feature_set_list.get_ui_name(module_name) + fs.module_name = module_name + + def update_external_rigs(self): """Get external feature sets""" - set_list = feature_set_list.get_installed_list() - # Update feature set list - self.rigify_feature_sets.clear() + self.refresh_installed_feature_sets() - for s in set_list: - list_entry = self.rigify_feature_sets.add() - list_entry.name = feature_set_list.get_ui_name(s) - list_entry.module_name = s + set_list = feature_set_list.get_enabled_modules_names() - if force or len(set_list) > 0: - # Reload rigs - print('Reloading external rigs...') - rig_lists.get_external_rigs(set_list) + # Reload rigs + print('Reloading external rigs...') + rig_lists.get_external_rigs(set_list) - # Reload metarigs - print('Reloading external metarigs...') - metarig_menu.get_external_metarigs(set_list) + # Reload metarigs + print('Reloading external metarigs...') + metarig_menu.get_external_metarigs(set_list) - # Re-register rig parameters - register_rig_parameters() + # Re-register rig parameters + register_rig_parameters() rigify_feature_sets: bpy.props.CollectionProperty(type=RigifyFeatureSets) active_feature_set_index: IntProperty() @@ -196,8 +241,8 @@ class RigifyPreferences(AddonPreferences): row = layout.row() row.template_list( - "UI_UL_list", - "rigify_feature_sets", + 'RIGIFY_UL_FeatureSets', + '', self, "rigify_feature_sets", self, 'active_feature_set_index' ) @@ -259,8 +304,7 @@ def draw_feature_set_prefs(layout, context, featureset: RigifyFeatureSets): op = row.operator('wm.url_open', text="Report a Bug", icon='URL') op.url = info['tracker_url'] - op = row.operator("wm.rigify_remove_feature_set", text="Remove", icon='CANCEL') - op.featureset = featureset.module_name + row.operator("wm.rigify_remove_feature_set", text="Remove", icon='CANCEL') class RigifyName(bpy.types.PropertyGroup): @@ -443,6 +487,7 @@ classes = ( RigifyColorSet, RigifySelectionColors, RigifyArmatureLayer, + RIGIFY_UL_FeatureSets, RigifyFeatureSets, RigifyPreferences, ) diff --git a/rigify/feature_set_list.py b/rigify/feature_set_list.py index a50bd010..cb46f5c6 100644 --- a/rigify/feature_set_list.py +++ b/rigify/feature_set_list.py @@ -16,6 +16,8 @@ # #======================= END GPL LICENSE BLOCK ======================== +from typing import List + import bpy from bpy.props import StringProperty import os @@ -44,7 +46,8 @@ def get_install_path(*, create=False): return INSTALL_PATH -def get_installed_list(): +def get_installed_modules_names() -> List[str]: + """Return a list of module names of all feature sets in the file system.""" features_path = get_install_path() if not features_path: return [] @@ -60,6 +63,17 @@ def get_installed_list(): return sets +def get_enabled_modules_names() -> List[str]: + """Return a list of module names of all enabled feature sets.""" + rigify_prefs = bpy.context.preferences.addons[__package__].preferences + installed_module_names = get_installed_modules_names() + rigify_feature_sets = rigify_prefs.rigify_feature_sets + + enabled_module_names = { fs.module_name for fs in rigify_feature_sets if fs.enabled } + + return [name for name in installed_module_names if name in enabled_module_names] + + def get_module(feature_set): return importlib.import_module('.'.join([*NAME_PREFIX, feature_set])) @@ -88,17 +102,17 @@ def get_info_dict(feature_set): return {} -def call_function_safe(feature_set, name, args=[], kwargs={}): - module = get_module_safe(feature_set) +def call_function_safe(module_name, func_name, args=[], kwargs={}): + module = get_module_safe(module_name) if module: - func = getattr(module, name, None) + func = getattr(module, func_name, None) if callable(func): try: return func(*args, **kwargs) except Exception: - print("Rigify Error: Could not call function '%s' of feature set '%s': exception occurred.\n" % (name, feature_set)) + print(f"Rigify Error: Could not call function '{func_name}' of feature set '{module_name}': exception occurred.\n") traceback.print_exc() print("") @@ -128,7 +142,7 @@ def feature_set_items(scene, context): ('rigify', 'Rigify Built-in', 'Rigs bundled with Rigify'), ] - for fs in get_installed_list(): + for fs in get_enabled_modules_names(): ui_name = get_ui_name(fs) items.append((fs, ui_name, ui_name)) @@ -222,8 +236,7 @@ class DATA_OT_rigify_add_feature_set(bpy.types.Operator): addon_prefs.update_external_rigs() - new_index = addon_prefs.rigify_feature_sets.find(get_ui_name(fixed_dirname)) - addon_prefs.active_feature_set_index = new_index + addon_prefs.active_feature_set_index = len(addon_prefs.rigify_feature_sets)-1 return {'FINISHED'} @@ -234,8 +247,6 @@ class DATA_OT_rigify_remove_feature_set(bpy.types.Operator): bl_description = "Remove external feature set (rigs, metarigs, ui templates)" bl_options = {"REGISTER", "UNDO", "INTERNAL"} - featureset: StringProperty(maxlen=1024, options={'HIDDEN', 'SKIP_SAVE'}) - @classmethod def poll(cls, context): return True @@ -245,18 +256,29 @@ class DATA_OT_rigify_remove_feature_set(bpy.types.Operator): def execute(self, context): addon_prefs = context.preferences.addons[__package__].preferences + feature_sets = addon_prefs.rigify_feature_sets + active_idx = addon_prefs.active_feature_set_index + active_fs = feature_sets[active_idx] - # Call the unregister callback of the set being removed - call_register_function(self.featureset, False) + # Call the unregister callback of the set being removed. + if active_fs.enabled: + call_register_function(active_fs.module_name, register=False) + # Remove the feature set's folder from the file system. rigify_config_path = get_install_path() if rigify_config_path: - set_path = os.path.join(rigify_config_path, self.featureset) + set_path = os.path.join(rigify_config_path, active_fs.module_name) if os.path.exists(set_path): rmtree(set_path) - addon_prefs.update_external_rigs(force=True) - addon_prefs.active_feature_set_index = 0 + # Remove the feature set's entry from the addon preferences. + feature_sets.remove(active_idx) + + # Remove the feature set's entries from the metarigs and rig types. + addon_prefs.update_external_rigs() + + # Update active index. + addon_prefs.active_feature_set_index -= 1 return {'FINISHED'} diff --git a/rigify/metarig_menu.py b/rigify/metarig_menu.py index 48da1e89..7a48df44 100644 --- a/rigify/metarig_menu.py +++ b/rigify/metarig_menu.py @@ -214,17 +214,16 @@ def unregister(): for mf in menu_funcs: bpy.types.VIEW3D_MT_armature_add.remove(mf) -def get_external_metarigs(set_list): +def get_external_metarigs(feature_module_names): unregister() # Clear and fill metarigs public variables metarigs.clear() get_internal_metarigs() - - for feature_set in set_list: + for module_name in feature_module_names: try: - base_dir, base_path = feature_set_list.get_dir_path(feature_set, METARIG_DIR) + base_dir, base_path = feature_set_list.get_dir_path(module_name, METARIG_DIR) get_metarigs(metarigs['external'], base_dir, base_path) except Exception: diff --git a/rigify/ui.py b/rigify/ui.py index 59dbf9b6..eac8b673 100644 --- a/rigify/ui.py +++ b/rigify/ui.py @@ -54,7 +54,7 @@ def build_type_list(context, rigify_types): for r in sorted(rig_lists.rigs): if (context.object.data.active_feature_set in ('all', rig_lists.rigs[r]['feature_set']) - or len(feature_set_list.get_installed_list()) == 0 + or len(feature_set_list.get_enabled_modules_names()) == 0 ): a = rigify_types.add() a.name = r @@ -191,7 +191,7 @@ class DATA_PT_rigify_samples(bpy.types.Panel): id_store.rigify_active_type = 0 # Rig type list - if len(feature_set_list.get_installed_list()) > 0: + if len(feature_set_list.get_enabled_modules_names()) > 0: row = layout.row() row.prop(context.object.data, "active_feature_set") row = layout.row() @@ -609,7 +609,7 @@ class BONE_PT_rigify_buttons(bpy.types.Panel): build_type_list(context, id_store.rigify_types) # Rig type field - if len(feature_set_list.get_installed_list()) > 0: + if len(feature_set_list.get_enabled_modules_names()) > 0: row = layout.row() row.prop(context.object.data, "active_feature_set") row = layout.row() -- cgit v1.2.3