KirilStrezikozin / BakeMaster-Blender-Addon

Welcome to BakeMaster, a powerful and feature-packed baking solution created for Blender - an open-source 3D Computer graphics software.
Other
34 stars 6 forks source link

REQUEST: Save last used preset name, display 'Update' button icon on overwrite #88

Closed KirilStrezikozin closed 5 months ago

KirilStrezikozin commented 5 months ago

This feature request is:

Context Users can overwrite presets in BakeMaster by creating a new one with an existing name. BakeMaster does not show any UI signs of this behavior, so this issue is about enhancing this.

Describe the solution you'd like to be implemented

KirilStrezikozin commented 5 months ago

BakeMaster draws presets by utilizing a BM_PresetPanel helper class for preset panels. It provides a classmethod call to draw a preset header (a button in panel headers with a preset icon to invoke a preset panel), and calls Blender's Menu.draw_preset(...) to draw a preset menu:

https://github.com/KirilStrezikozin/BakeMaster-Blender-Addon/blob/acf0b5b1b9dc1f2d77e06d0ac4fbb2b2a74e6808/presets.py#L1251-L1286

This issue will replace Menu.draw_preset(...) call with a custom written draw_preset method similar to Blender's Menu.draw_preset implementation, but with functionality described in todos above:

class Menu(StructRNA, _GenericUI, metaclass=RNAMeta):
    __slots__ = ()

    def path_menu(self, searchpaths, operator, *,
                  props_default=None, prop_filepath="filepath",
                  filter_ext=None, filter_path=None, display_name=None,
                  add_operator=None):
        """
        Populate a menu from a list of paths.

        :arg searchpaths: Paths to scan.
        :type searchpaths: sequence of strings.
        :arg operator: The operator id to use with each file.
        :type operator: string
        :arg prop_filepath: Optional operator filepath property (defaults to "filepath").
        :type prop_filepath: string
        :arg props_default: Properties to assign to each operator.
        :type props_default: dict
        :arg filter_ext: Optional callback that takes the file extensions.

           Returning false excludes the file from the list.

        :type filter_ext: Callable that takes a string and returns a bool.
        :arg display_name: Optional callback that takes the full path, returns the name to display.
        :type display_name: Callable that takes a string and returns a string.
        """

        layout = self.layout

        import os
        import re
        import bpy.utils
        from bpy.app.translations import pgettext_iface as iface_

        layout = self.layout

        if not searchpaths:
            layout.label(text="* Missing Paths *")

        # collect paths
        files = []
        for directory in searchpaths:
            files.extend([
                (f, os.path.join(directory, f))
                for f in os.listdir(directory)
                if (not f.startswith("."))
                if ((filter_ext is None) or
                    (filter_ext(os.path.splitext(f)[1])))
                if ((filter_path is None) or
                    (filter_path(f)))
            ])

        # Perform a "natural sort", so 20 comes after 3 (for example).
        files.sort(
            key=lambda file_path:
            tuple(int(t) if t.isdigit() else t for t in re.split(r"(\d+)", file_path[0].lower())),
        )

        col = layout.column(align=True)

        for f, filepath in files:
            # Intentionally pass the full path to 'display_name' callback,
            # since the callback may want to use part a directory in the name.
            row = col.row(align=True)
            name = display_name(filepath) if display_name else bpy.path.display_name(f)
            props = row.operator(
                operator,
                text=iface_(name),
                translate=False,
            )

            if props_default is not None:
                for attr, value in props_default.items():
                    setattr(props, attr, value)

            setattr(props, prop_filepath, filepath)
            if operator == "script.execute_preset":
                props.menu_idname = self.bl_idname

            if add_operator:
                props = row.operator(add_operator, text="", icon='REMOVE')
                props.name = name
                props.remove_name = True

        if add_operator:
            wm = bpy.data.window_managers[0]

            layout.separator()
            row = layout.row()

            sub = row.row()
            sub.emboss = 'NORMAL'
            sub.prop(wm, "preset_name", text="")

            props = row.operator(add_operator, text="", icon='ADD')
            props.name = wm.preset_name

    def draw_preset(self, _context):
        """
        Define these on the subclass:
        - preset_operator (string)
        - preset_subdir (string)

        Optionally:
        - preset_add_operator (string)
        - preset_extensions (set of strings)
        - preset_operator_defaults (dict of keyword args)
        """
        import bpy
        ext_valid = getattr(self, "preset_extensions", {".py", ".xml"})
        props_default = getattr(self, "preset_operator_defaults", None)
        add_operator = getattr(self, "preset_add_operator", None)
        self.path_menu(
            bpy.utils.preset_paths(self.preset_subdir),
            self.preset_operator,
            props_default=props_default,
            filter_ext=lambda ext: ext.lower() in ext_valid,
            add_operator=add_operator,
            display_name=lambda name: bpy.path.display_name(name, title_case=False)
        )

    @classmethod
    def draw_collapsible(cls, context, layout):
        # helper function for (optionally) collapsed header menus
        # only usable within headers
        if context.area.show_menus:
            # Align menus to space them closely.
            layout.row(align=True).menu_contents(cls.__name__)
        else:
            layout.menu(cls.__name__, icon='COLLAPSEMENU')
KirilStrezikozin commented 5 months ago

image image