diff --git a/scripts/startup/bl_ui/properties_workspace.py b/scripts/startup/bl_ui/properties_workspace.py index 032ba28e48e..3c75c91eee2 100644 --- a/scripts/startup/bl_ui/properties_workspace.py +++ b/scripts/startup/bl_ui/properties_workspace.py @@ -40,6 +40,8 @@ class WORKSPACE_PT_main(WorkSpaceButtonsPanel, Panel): class WORKSPACE_PT_addons(WorkSpaceButtonsPanel, Panel): bl_label = "Filter Add-ons" bl_parent_id = "WORKSPACE_PT_main" + addon_map = {} + owner_ids = set() def draw_header(self, context): workspace = context.workspace @@ -47,42 +49,32 @@ class WORKSPACE_PT_addons(WorkSpaceButtonsPanel, Panel): def draw(self, context): layout = self.layout - # align just to pack more tightly - col = layout.box().column(align=True) workspace = context.workspace prefs = context.preferences - col.active = workspace.use_filter_by_owner - import addon_utils - addon_map = {mod.__name__: mod for mod in addon_utils.modules()} - owner_ids = {owner_id.name for owner_id in workspace.owner_ids} - + WORKSPACE_PT_addons.addon_map = {mod.__name__: mod for mod in addon_utils.modules()} + WORKSPACE_PT_addons.owner_ids = {owner_id.name for owner_id in workspace.owner_ids} + known_addons = set() for addon in prefs.addons: - module_name = addon.module - module = addon_map.get(module_name) - if module is None: - continue - info = addon_utils.module_bl_info(module) - is_enabled = module_name in owner_ids - row = col.row() - row.alignment = 'LEFT' - row.operator( - "wm.owner_disable" if is_enabled else "wm.owner_enable", - icon='CHECKBOX_HLT' if is_enabled else 'CHECKBOX_DEHLT', - text=iface_("%s: %s") % (iface_(info["category"]), iface_(info["name"])), - translate=False, - emboss=False, - ).owner_id = module_name - if is_enabled: - owner_ids.remove(module_name) - + if addon.module in WORKSPACE_PT_addons.owner_ids: + known_addons.add(addon.module) + unknown_addons = WORKSPACE_PT_addons.owner_ids.difference(known_addons) + layout.template_list( + "WORKSPACE_UL_addons_items", + "", + context.preferences, + "addons", + context.workspace, + "active_addon", + rows=8, + ) # Detect unused - if owner_ids: + if unknown_addons: layout.label(text="Unknown add-ons", icon='ERROR') col = layout.box().column(align=True) - for module_name in sorted(owner_ids): + for module_name in sorted(unknown_addons): row = col.row() row.alignment = 'LEFT' row.operator( @@ -93,6 +85,86 @@ class WORKSPACE_PT_addons(WorkSpaceButtonsPanel, Panel): ).owner_id = module_name +def addon_category_name(addon): + import addon_utils + module = WORKSPACE_PT_addons.addon_map.get(addon.module) + if not module: + return addon.module + info = addon_utils.module_bl_info(module) + return (iface_("%s: %s") % (iface_(info["category"]), iface_(info["name"]))) + + +class WORKSPACE_UL_addons_items(bpy.types.UIList): + @staticmethod + def _filter_addons_by_category_name(pattern, bitflag, addons, reverse=False): + """ + Set FILTER_ITEM for addons which category and name matches filter_name one (case-insensitive). + pattern is the filtering pattern. + return a list of flags based on given bit flag, or an empty list if no pattern is given + or list addons is empty. + """ + + if not pattern or not addons: # Empty pattern or list = no filtering! + return [] + + import fnmatch + import re + + # Implicitly add heading/trailing wildcards. + pattern_regex = re.compile(fnmatch.translate("*" + pattern + "*"), re.IGNORECASE) + + flags = [0] * len(addons) + + for i, addon in enumerate(addons): + name = addon_category_name(addon) + # This is similar to a logical XOR. + if bool(name and pattern_regex.match(name)) is not reverse: + flags[i] |= bitflag + return flags + + @staticmethod + def _sort_addons_by_category_name(addons): + """ + Re-order addons using their categories and names (case-insensitive). + return a list mapping org_idx -> new_idx, or an empty list if no sorting has been done. + """ + _sort = [(idx, addon_category_name(addon)) for idx, addon in enumerate(addons)] + return bpy.types.UI_UL_list.sort_items_helper(_sort, lambda e: e[1].lower()) + + def filter_items(self, _context, data, property): + addons = getattr(data, property) + flags = [] + indices = [] + + # Filtering by category and name + if self.filter_name: + flags = self._filter_addons_by_category_name( + self.filter_name, self.bitflag_filter_item, addons, reverse=self.use_filter_invert) + if not flags: + flags = [self.bitflag_filter_item] * len(addons) + # Filer addons without registered modules + for idx, addon in enumerate(addons): + if not WORKSPACE_PT_addons.addon_map.get(addon.module): + flags[idx] = 0 + if self.use_filter_sort_alpha: + indices = self._sort_addons_by_category_name(addons) + return flags, indices + + def draw_item(self, context, layout, _data, addon, icon, _active_data, _active_propname, _index): + row = layout.row() + row.active = context.workspace.use_filter_by_owner + row.emboss = 'NONE' + row.label(text=addon_category_name(addon)) + row = row.row() + row.alignment = 'RIGHT' + is_enabled = addon.module in WORKSPACE_PT_addons.owner_ids + row.operator( + "wm.owner_disable" if is_enabled else "wm.owner_enable", + icon='CHECKBOX_HLT' if is_enabled else 'CHECKBOX_DEHLT', + text="", + ).owner_id = addon.module + + class WORKSPACE_PT_custom_props(WorkSpaceButtonsPanel, PropertyPanel, Panel): bl_parent_id = "WORKSPACE_PT_main" @@ -101,11 +173,18 @@ class WORKSPACE_PT_custom_props(WorkSpaceButtonsPanel, PropertyPanel, Panel): classes = ( + WORKSPACE_UL_addons_items, + WORKSPACE_PT_main, WORKSPACE_PT_addons, WORKSPACE_PT_custom_props, ) + +bpy.types.WorkSpace.active_addon = bpy.props.IntProperty( + name="Active Add-on", description="Active Add-on in the Workspace Add-ons filter") + + if __name__ == "__main__": # only for live edit. from bpy.utils import register_class for cls in classes: