KirilStrezikozin/BakeMaster-Blender-Addon

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

KirilStrezikozin opened this issue · 2 comments

This feature request is:

  • not a duplicate
  • implemented

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

  • mike_fr (Discord) suggested auto-populating the New Preset Name field by the name of the last used preset in the preset menu pop-ups. This allows users to overwrite presets quicker by not manually entering the whole name. To create new presets the user would click the Preset Name field and replace its value with the desired preset name.

    Example:
    The last used preset is named AO_4k_highq. The user applied this preset. When they open the preset menu, they see AO_4k_highq instead of New Preset. This indicates that they previously used AO_4k_highq preset. From there, they can press 'Update' button ('Update' icon is displayed instead of 'Plus' because AO_4k_highq preset exists) to overwrite the preset. They can erase the value in the Name field and overwrite any other preset or create a new one.

  • Display 'Update' button on the left to the 'Remove' button to allow users to update any preset without manually entering their name. When pressing 'Update' in the list, the current settings are read and fed into the preset on which the 'Update' button was pressed. Internally, this involves creating a new preset with an existing name (this name is taken from the preset on which the 'Update' button was pressed).

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:

class BM_PresetPanel:
bl_space_type = 'PROPERTIES'
bl_region_type = 'HEADER'
bl_label = "Presets"
path_menu = Menu.path_menu
@classmethod
def draw_panel_header(cls, layout):
layout.emboss = 'NONE'
layout.popover(
panel=cls.__name__,
icon='PRESET',
text="",
)
@classmethod
def draw_menu(cls, layout, text=None):
if text is None:
text = cls.bl_label
layout.popover(
panel=cls.__name__,
icon='PRESET',
text=text,
)
def draw(self, context):
layout = self.layout
layout.emboss = 'PULLDOWN_MENU'
# from https://docs.blender.org/api/current/bpy.ops.html#execution-context
# EXEC_DEFAULT is used by default, running only the execute() method,
# but you may want the operator to take user interaction with
# INVOKE_DEFAULT which will also call invoke() if existing.
layout.operator_context = 'INVOKE_DEFAULT'
Menu.draw_preset(self, context)

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')