UI: preset popover buttons in panel headers.
authorBrecht Van Lommel <brechtvanlommel@gmail.com>
Fri, 27 Apr 2018 11:50:26 +0000 (13:50 +0200)
committerBrecht Van Lommel <brechtvanlommel@gmail.com>
Wed, 13 Jun 2018 13:22:34 +0000 (15:22 +0200)
Moves the preset into a menu for the panel header, so it can be changed
without opening the panel and takes up less space. Two remaining issues:

* For long lists the add new preset button can be scrolled off screen.
* We should support showing the name of the chosen preset in the panel
  header, but the current preset system does not support detecting which
  preset is used.

Differential Revision: https://developer.blender.org/D3366

20 files changed:
intern/cycles/blender/addon/ui.py
release/datafiles/blender_icons.svg
release/datafiles/blender_icons16/icon16_preset.dat [new file with mode: 0644]
release/datafiles/blender_icons32/icon32_preset.dat [new file with mode: 0644]
release/scripts/modules/bpy_types.py
release/scripts/startup/bl_operators/presets.py
release/scripts/startup/bl_ui/properties_data_camera.py
release/scripts/startup/bl_ui/properties_data_lamp.py
release/scripts/startup/bl_ui/properties_particle.py
release/scripts/startup/bl_ui/properties_physics_cloth.py
release/scripts/startup/bl_ui/properties_physics_fluid.py
release/scripts/startup/bl_ui/properties_render.py
release/scripts/startup/bl_ui/properties_scene.py
release/scripts/startup/bl_ui/space_clip.py
release/scripts/startup/bl_ui/space_node.py
source/blender/blenkernel/BKE_screen.h
source/blender/editors/include/UI_icons.h
source/blender/editors/interface/interface_region_popover.c
source/blender/editors/screen/area.c
source/blender/makesrna/intern/rna_ui.c

index c514ec1..c5eafe9 100644 (file)
@@ -18,6 +18,7 @@
 
 import bpy
 from bpy_extras.node_utils import find_node_input, find_output_node
+from bl_operators.presets import PresetMenu
 
 from bpy.types import (
     Panel,
@@ -26,20 +27,20 @@ from bpy.types import (
 )
 
 
-class CYCLES_MT_sampling_presets(Menu):
+class CYCLES_MT_sampling_presets(PresetMenu):
     bl_label = "Sampling Presets"
     preset_subdir = "cycles/sampling"
     preset_operator = "script.execute_preset"
+    preset_add_operator = "render.cycles_sampling_preset_add"
     COMPAT_ENGINES = {'CYCLES'}
-    draw = Menu.draw_preset
 
 
-class CYCLES_MT_integrator_presets(Menu):
+class CYCLES_MT_integrator_presets(PresetMenu):
     bl_label = "Integrator Presets"
     preset_subdir = "cycles/integrator"
     preset_operator = "script.execute_preset"
+    preset_add_operator = "render.cycles_integrator_preset_add"
     COMPAT_ENGINES = {'CYCLES'}
-    draw = Menu.draw_preset
 
 
 class CyclesButtonsPanel:
@@ -144,6 +145,9 @@ class CYCLES_RENDER_PT_sampling(CyclesButtonsPanel, Panel):
     bl_label = "Sampling"
     bl_options = {'DEFAULT_CLOSED'}
 
+    def draw_header_preset(self, context):
+        CYCLES_MT_sampling_presets.draw_panel_header(self.layout)
+
     def draw(self, context):
         layout = self.layout
         layout.use_property_split = False
@@ -151,11 +155,6 @@ class CYCLES_RENDER_PT_sampling(CyclesButtonsPanel, Panel):
         scene = context.scene
         cscene = scene.cycles
 
-        row = layout.row(align=True)
-        row.menu("CYCLES_MT_sampling_presets", text=bpy.types.CYCLES_MT_sampling_presets.bl_label)
-        row.operator("render.cycles_sampling_preset_add", text="", icon="ZOOMIN")
-        row.operator("render.cycles_sampling_preset_add", text="", icon="ZOOMOUT").remove_active = True
-
         layout.use_property_split = True
 
         layout.prop(cscene, "progressive")
@@ -315,17 +314,11 @@ class CYCLES_RENDER_PT_light_paths(CyclesButtonsPanel, Panel):
     bl_label = "Light Paths"
     bl_options = {'DEFAULT_CLOSED'}
 
-    def draw(self, context):
-        layout = self.layout
-        layout.use_property_split = True
-
-        scene = context.scene
-        cscene = scene.cycles
+    def draw_header_preset(self, context):
+        CYCLES_MT_integrator_presets.draw_panel_header(self.layout)
 
-        row = layout.row(align=True)
-        row.menu("CYCLES_MT_integrator_presets", text=bpy.types.CYCLES_MT_integrator_presets.bl_label)
-        row.operator("render.cycles_integrator_preset_add", text="", icon="ZOOMIN")
-        row.operator("render.cycles_integrator_preset_add", text="", icon="ZOOMOUT").remove_active = True
+    def draw(self, context):
+        pass
 
 
 class CYCLES_RENDER_PT_light_paths_max_bounces(CyclesButtonsPanel, Panel):
index 4d53769..d389831 100644 (file)
        id="radialGradient16215"
        xlink:href="#linearGradient18134"
        inkscape:collect="always" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient27277-1-8"
+       id="linearGradient18811"
+       gradientUnits="userSpaceOnUse"
+       x1="4.1933641"
+       y1="199.12067"
+       x2="17.16466"
+       y2="211.01585" />
   </defs>
   <sodipodi:namedview
      id="base"
            y="75.5" />
       </g>
     </g>
+    <g
+       transform="translate(461.71013,377.29483)"
+       style="display:inline;enable-background:new"
+       id="ICON_SOLO_OFF-7">
+      <rect
+         y="198.9792"
+         x="4.9506397"
+         height="16"
+         width="16"
+         id="rect23018-5-4-5"
+         style="display:inline;overflow:visible;visibility:visible;opacity:0;fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.79999995;marker:none;enable-background:accumulate" />
+      <g
+         transform="matrix(0.94058502,0,0,0.94058502,0.9128606,12.74924)"
+         id="g56716-3" />
+    </g>
+    <g
+       transform="translate(461.99301,376.87052)"
+       style="display:inline;enable-background:new"
+       id="ICON_SOLO_OFF-1">
+      <rect
+         y="198.9792"
+         x="4.9506397"
+         height="16"
+         width="16"
+         id="rect23018-5-4-2"
+         style="display:inline;overflow:visible;visibility:visible;opacity:0;fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.79999995;marker:none;enable-background:accumulate" />
+      <g
+         transform="matrix(0.94058502,0,0,0.94058502,0.9128606,12.74924)"
+         id="g56716-7">
+        <path
+           sodipodi:type="star"
+           style="fill:url(#linearGradient18811);fill-opacity:1.0;stroke:#000000;stroke-width:0.96882826;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.80000001"
+           id="path15855-0"
+           sodipodi:sides="5"
+           sodipodi:cx="13.700194"
+           sodipodi:cy="207.20645"
+           sodipodi:r1="7.1873641"
+           sodipodi:r2="3.3158474"
+           sodipodi:arg1="0.94697287"
+           sodipodi:arg2="1.5618338"
+           inkscape:flatsided="false"
+           inkscape:rounded="0"
+           inkscape:randomized="0"
+           d="m 17.898641,213.04008 -4.168729,-2.51791 -4.280439,2.47993 1.106473,-4.74277 -3.6812884,-3.3046 4.8525664,-0.41328 2.005278,-4.52229 1.892578,4.48735 4.920618,0.50967 -3.682889,3.18662 z"
+           inkscape:transform-center-x="-0.010954063"
+           inkscape:transform-center-y="-0.74285516"
+           transform="matrix(1.0972098,0,0,1.0975406,-2.0923019,-19.740595)" />
+        <path
+           style="fill:none;stroke:#ffffff;stroke-width:1.06316817;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:0.51147538"
+           d="m 12.931855,202.51514 -1.201334,2.70994 c -0.137665,0.32193 -0.454082,0.55986 -0.800889,0.60222 l -2.9032248,0.26765 2.2358168,1.97391 c 0.261321,0.2395 0.380487,0.62447 0.300333,0.97022 l -0.667408,2.81032"
+           id="path15869-9"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="ccccccc" />
+      </g>
+    </g>
   </g>
   <g
      inkscape:groupmode="layer"
diff --git a/release/datafiles/blender_icons16/icon16_preset.dat b/release/datafiles/blender_icons16/icon16_preset.dat
new file mode 100644 (file)
index 0000000..52026e9
Binary files /dev/null and b/release/datafiles/blender_icons16/icon16_preset.dat differ
diff --git a/release/datafiles/blender_icons32/icon32_preset.dat b/release/datafiles/blender_icons32/icon32_preset.dat
new file mode 100644 (file)
index 0000000..57d5d25
Binary files /dev/null and b/release/datafiles/blender_icons32/icon32_preset.dat differ
index 3a9704b..6cca60b 100644 (file)
@@ -856,7 +856,8 @@ class Menu(StructRNA, _GenericUI, metaclass=RNAMeta):
 
     def path_menu(self, searchpaths, operator, *,
                   props_default=None, prop_filepath="filepath",
-                  filter_ext=None, filter_path=None, display_name=None):
+                  filter_ext=None, filter_path=None, display_name=None,
+                  add_operator=None):
         """
         Populate a menu from a list of paths.
 
@@ -902,12 +903,16 @@ class Menu(StructRNA, _GenericUI, metaclass=RNAMeta):
 
         files.sort()
 
+        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.
-            props = layout.operator(
+            row = col.row(align=True)
+            name = display_name(filepath) if display_name else bpy.path.display_name(f)
+            props = row.operator(
                 operator,
-                text=display_name(filepath) if display_name else bpy.path.display_name(f),
+                text=name,
                 translate=False,
             )
 
@@ -919,6 +924,25 @@ class Menu(StructRNA, _GenericUI, metaclass=RNAMeta):
             if operator == "script.execute_preset":
                 props.menu_idname = self.bl_idname
 
+            if add_operator:
+                props = row.operator(add_operator, text="", icon='ZOOMOUT')
+                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='ZOOMIN')
+            props.name = wm.preset_name
+
+
     def draw_preset(self, context):
         """
         Define these on the subclass:
@@ -926,16 +950,19 @@ class Menu(StructRNA, _GenericUI, metaclass=RNAMeta):
         - 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)
+                       filter_ext=lambda ext: ext.lower() in ext_valid,
+                       add_operator=add_operator)
 
     @classmethod
     def draw_collapsible(cls, context, layout):
index deca409..074177b 100644 (file)
 # <pep8 compliant>
 
 import bpy
-from bpy.types import Menu, Operator
+from bpy.types import Menu, Operator, Panel, WindowManager
 from bpy.props import StringProperty, BoolProperty
 
+# For preset popover menu
+WindowManager.preset_name = StringProperty(
+    name="Preset Name",
+    description="Name for new preset",
+    default="New Preset"
+)
 
 class AddPresetBase:
     """Base preset class, only for subclassing
@@ -40,6 +46,10 @@ class AddPresetBase:
             maxlen=64,
             options={'SKIP_SAVE'},
             )
+    remove_name = BoolProperty(
+            default=False,
+            options={'HIDDEN', 'SKIP_SAVE'},
+            )
     remove_active = BoolProperty(
             default=False,
             options={'HIDDEN', 'SKIP_SAVE'},
@@ -48,6 +58,7 @@ class AddPresetBase:
     # needed for mix-ins
     order = [
         "name",
+        "remove_name",
         "remove_active",
         ]
 
@@ -85,11 +96,17 @@ class AddPresetBase:
         else:
             ext = ".py"
 
-        if not self.remove_active:
-            name = self.name.strip()
+        name = self.name.strip()
+        if not (self.remove_name or self.remove_active):
+
             if not name:
                 return {'FINISHED'}
 
+            # Reset preset name
+            wm = bpy.data.window_managers[0]
+            if name == wm.preset_name:
+                wm.preset_name = 'New Preset'
+
             filename = self.as_filename(name)
 
             target_path = os.path.join("presets", self.preset_subdir)
@@ -155,15 +172,16 @@ class AddPresetBase:
             preset_menu_class.bl_label = bpy.path.display_name(filename)
 
         else:
-            preset_active = preset_menu_class.bl_label
+            if self.remove_active:
+                name = preset_menu_class.bl_label
 
             # fairly sloppy but convenient.
-            filepath = bpy.utils.preset_find(preset_active,
+            filepath = bpy.utils.preset_find(name,
                                              self.preset_subdir,
                                              ext=ext)
 
             if not filepath:
-                filepath = bpy.utils.preset_find(preset_active,
+                filepath = bpy.utils.preset_find(name,
                                                  self.preset_subdir,
                                                  display_name=True,
                                                  ext=ext)
@@ -194,7 +212,7 @@ class AddPresetBase:
         self.name = self.as_filename(self.name.strip())
 
     def invoke(self, context, event):
-        if not self.remove_active:
+        if not (self.remove_active or self.remove_name):
             wm = context.window_manager
             return wm.invoke_props_dialog(self)
         else:
@@ -241,6 +259,40 @@ class ExecutePreset(Operator):
         return {'FINISHED'}
 
 
+class PresetMenu(Panel):
+    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(cls.bl_space_type,
+                       cls.bl_region_type,
+                       cls.__name__,
+                       icon='PRESET',
+                       text='')
+
+    @classmethod
+    def draw_menu(cls, layout, text=None):
+        if text == None:
+            text = cls.bl_label
+
+        layout.popover(cls.bl_space_type,
+                       cls.bl_region_type,
+                       cls.__name__,
+                       icon='PRESET',
+                       text=text)
+
+    def draw(self, context):
+        layout = self.layout
+        layout.emboss = 'PULLDOWN_MENU'
+        layout.operator_context = 'EXEC_DEFAULT'
+
+        Menu.draw_preset(self, context)
+
+
 class AddPresetRender(AddPresetBase, Operator):
     """Add or remove a Render Preset"""
     bl_idname = "render.preset_add"
@@ -385,35 +437,6 @@ class AddPresetHairDynamics(AddPresetBase, Operator):
         ]
 
 
-class AddPresetSunSky(AddPresetBase, Operator):
-    """Add or remove a Sky & Atmosphere Preset"""
-    bl_idname = "lamp.sunsky_preset_add"
-    bl_label = "Add Sunsky Preset"
-    preset_menu = "LAMP_MT_sunsky_presets"
-
-    preset_defines = [
-        "sky = bpy.context.lamp.sky"
-    ]
-
-    preset_values = [
-        "sky.atmosphere_extinction",
-        "sky.atmosphere_inscattering",
-        "sky.atmosphere_turbidity",
-        "sky.backscattered_light",
-        "sky.horizon_brightness",
-        "sky.spread",
-        "sky.sun_brightness",
-        "sky.sun_intensity",
-        "sky.sun_size",
-        "sky.sky_blend",
-        "sky.sky_blend_type",
-        "sky.sky_color_space",
-        "sky.sky_exposure",
-    ]
-
-    preset_subdir = "sunsky"
-
-
 class AddPresetInteraction(AddPresetBase, Operator):
     """Add or remove an Application Interaction Preset"""
     bl_idname = "wm.interaction_preset_add"
@@ -665,7 +688,6 @@ classes = (
     AddPresetOperator,
     AddPresetRender,
     AddPresetSafeAreas,
-    AddPresetSunSky,
     AddPresetTrackingCamera,
     AddPresetTrackingSettings,
     AddPresetTrackingTrackColor,
index 132a226..22c7964 100644 (file)
@@ -20,6 +20,7 @@
 import bpy
 from bpy.types import Panel, Menu
 from rna_prop_ui import PropertyPanel
+from bl_operators.presets import PresetMenu
 
 
 class CameraButtonsPanel:
@@ -33,20 +34,20 @@ class CameraButtonsPanel:
         return context.camera and (engine in cls.COMPAT_ENGINES)
 
 
-class CAMERA_MT_presets(Menu):
+class CAMERA_MT_presets(PresetMenu):
     bl_label = "Camera Presets"
     preset_subdir = "camera"
     preset_operator = "script.execute_preset"
+    preset_add_operator = "camera.preset_add"
     COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE'}
-    draw = Menu.draw_preset
 
 
-class SAFE_AREAS_MT_presets(Menu):
+class SAFE_AREAS_MT_presets(PresetMenu):
     bl_label = "Camera Presets"
     preset_subdir = "safe_areas"
     preset_operator = "script.execute_preset"
+    preset_add_operator = "safe_areas.preset_add"
     COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE'}
-    draw = Menu.draw_preset
 
 
 class DATA_PT_context_camera(CameraButtonsPanel, Panel):
@@ -185,17 +186,14 @@ class DATA_PT_camera(CameraButtonsPanel, Panel):
     bl_label = "Camera"
     COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE'}
 
+    def draw_header_preset(self, context):
+        CAMERA_MT_presets.draw_panel_header(self.layout)
+
     def draw(self, context):
         layout = self.layout
 
         cam = context.camera
 
-        row = layout.row(align=True)
-
-        row.menu("CAMERA_MT_presets", text=bpy.types.CAMERA_MT_presets.bl_label)
-        row.operator("camera.preset_add", text="", icon='ZOOMIN')
-        row.operator("camera.preset_add", text="", icon='ZOOMOUT').remove_active = True
-
         layout.use_property_split = True
 
         col = layout.column()
@@ -410,6 +408,9 @@ class DATA_PT_camera_safe_areas(CameraButtonsPanel, Panel):
 
         self.layout.prop(cam, "show_safe_areas", text="")
 
+    def draw_header_preset(self, context):
+        SAFE_AREAS_MT_presets.draw_panel_header(self.layout)
+
     def draw(self, context):
         layout = self.layout
         safe_data = context.scene.safe_areas
@@ -430,13 +431,6 @@ def draw_display_safe_settings(layout, safe_data, settings):
 
     layout.use_property_split = True
 
-    row = layout.row(align=True)
-    row.menu("SAFE_AREAS_MT_presets", text=bpy.types.SAFE_AREAS_MT_presets.bl_label)
-    row.operator("safe_areas.preset_add", text="", icon='ZOOMIN')
-    row.operator("safe_areas.preset_add", text="", icon='ZOOMOUT').remove_active = True
-
-    layout.separator()
-
     col = layout.column()
     col.active = show_safe_areas
 
index 28f1b60..d613967 100644 (file)
@@ -22,14 +22,6 @@ from bpy.types import Menu, Panel
 from rna_prop_ui import PropertyPanel
 
 
-class LAMP_MT_sunsky_presets(Menu):
-    bl_label = "Sun & Sky Presets"
-    preset_subdir = "sunsky"
-    preset_operator = "script.execute_preset"
-    COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE'}
-    draw = Menu.draw_preset
-
-
 class DataButtonsPanel:
     bl_space_type = 'PROPERTIES'
     bl_region_type = 'WINDOW'
@@ -352,7 +344,6 @@ class DATA_PT_custom_props_lamp(DataButtonsPanel, PropertyPanel, Panel):
 
 
 classes = (
-    LAMP_MT_sunsky_presets,
     DATA_PT_context_lamp,
     DATA_PT_preview,
     DATA_PT_lamp,
index ebfa5ec..1e08b8e 100644 (file)
@@ -21,6 +21,7 @@ import bpy
 from bpy.types import Panel, Menu
 from rna_prop_ui import PropertyPanel
 from bpy.app.translations import pgettext_iface as iface_
+from bl_operators.presets import PresetMenu
 
 from .properties_physics_common import (
     point_cache_ui,
@@ -82,12 +83,12 @@ class PARTICLE_MT_specials(Menu):
         layout.operator("particle.duplicate_particle_system")
 
 
-class PARTICLE_MT_hair_dynamics_presets(Menu):
+class PARTICLE_MT_hair_dynamics_presets(PresetMenu):
     bl_label = "Hair Dynamics Presets"
     preset_subdir = "hair_dynamics"
     preset_operator = "script.execute_preset"
+    preset_add_operator = "particle.hair_dynamics_preset_add"
     COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE'}
-    draw = Menu.draw_preset
 
 
 class ParticleButtonsPanel:
@@ -340,6 +341,16 @@ class PARTICLE_PT_hair_dynamics(ParticleButtonsPanel, Panel):
         psys = context.particle_system
         self.layout.prop(psys, "use_hair_dynamics", text="")
 
+    def draw_header_preset(self, context):
+        psys = context.particle_system
+
+        if not psys.cloth:
+            return
+
+        layout = self.layout
+        layout.enabled = psys.use_hair_dynamics and psys.point_cache.is_baked is False
+        PARTICLE_MT_hair_dynamics_presets.draw_panel_header(layout)
+
     def draw(self, context):
         layout = self.layout
 
@@ -355,11 +366,6 @@ class PARTICLE_PT_hair_dynamics(ParticleButtonsPanel, Panel):
 
         layout.enabled = psys.use_hair_dynamics and psys.point_cache.is_baked is False
 
-        row = layout.row(align=True)
-        row.menu("PARTICLE_MT_hair_dynamics_presets", text=bpy.types.PARTICLE_MT_hair_dynamics_presets.bl_label)
-        row.operator("particle.hair_dynamics_preset_add", text="", icon='ZOOMIN')
-        row.operator("particle.hair_dynamics_preset_add", text="", icon='ZOOMOUT').remove_active = True
-
         layout.use_property_split = True
 
         layout.separator()
index f450bc6..3234395 100644 (file)
@@ -19,6 +19,7 @@
 # <pep8 compliant>
 import bpy
 from bpy.types import Menu, Panel
+from bl_operators.presets import PresetMenu
 
 from .properties_physics_common import (
     point_cache_ui,
@@ -30,11 +31,11 @@ def cloth_panel_enabled(md):
     return md.point_cache.is_baked is False
 
 
-class CLOTH_MT_presets(Menu):
+class CLOTH_MT_presets(PresetMenu):
     bl_label = "Cloth Presets"
     preset_subdir = "cloth"
     preset_operator = "script.execute_preset"
-    draw = Menu.draw_preset
+    preset_add_operator = "cloth.preset_add"
 
 
 class PhysicButtonsPanel:
@@ -52,6 +53,9 @@ class PHYSICS_PT_cloth(PhysicButtonsPanel, Panel):
     bl_label = "Cloth"
     COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE'}
 
+    def draw_header_preset(self, context):
+        CLOTH_MT_presets.draw_panel_header(self.layout)
+
     def draw(self, context):
         layout = self.layout
 
@@ -63,16 +67,6 @@ class PHYSICS_PT_cloth(PhysicButtonsPanel, Panel):
 
         split = layout.split(percentage=0.25)
 
-        col = split.column()
-
-        split.label(text="Presets:")
-        sub = split.row(align=True)
-        sub.menu("CLOTH_MT_presets", text=bpy.types.CLOTH_MT_presets.bl_label)
-        sub.operator("cloth.preset_add", text="", icon='ZOOMIN')
-        sub.operator("cloth.preset_add", text="", icon='ZOOMOUT').remove_active = True
-
-        split = layout.split(percentage=0.25)
-
         split.label(text="Quality:")
         split.prop(cloth, "quality", text="Steps")
 
index 93f1d28..6b3dd34 100644 (file)
 import bpy
 from bpy.types import Panel, Menu
 from bpy.app.translations import pgettext_iface as iface_
+from bl_operators.presets import PresetMenu
 
 
-class FLUID_MT_presets(Menu):
+class FLUID_MT_presets(PresetMenu):
     bl_label = "Fluid Presets"
     preset_subdir = "fluid"
     preset_operator = "script.execute_preset"
-    draw = Menu.draw_preset
+    preset_add_operator = "fluid.preset_add"
 
 
 class PhysicButtonsPanel:
@@ -240,11 +241,7 @@ class PHYSICS_PT_domain_gravity(PhysicButtonsPanel, Panel):
             col.prop(fluid, "simulation_scale", text="Meters")
 
         col = split.column()
-        col.label(text="Viscosity Presets:")
-        sub = col.row(align=True)
-        sub.menu("FLUID_MT_presets", text=bpy.types.FLUID_MT_presets.bl_label)
-        sub.operator("fluid.preset_add", text="", icon='ZOOMIN')
-        sub.operator("fluid.preset_add", text="", icon='ZOOMOUT').remove_active = True
+        FLUID_MT_presets.draw_menu(col, text="Viscosity Presets")
 
         sub = col.column(align=True)
         sub.prop(fluid, "viscosity_base", text="Base")
index 1702653..3af1691 100644 (file)
 # <pep8 compliant>
 import bpy
 from bpy.types import Menu, Panel, UIList
+from bl_operators.presets import PresetMenu
 
 
-class RENDER_MT_presets(Menu):
+class RENDER_MT_presets(PresetMenu):
     bl_label = "Render Presets"
     preset_subdir = "render"
     preset_operator = "script.execute_preset"
-    draw = Menu.draw_preset
+    preset_add_operator = "render.preset_add"
 
 
-class RENDER_MT_ffmpeg_presets(Menu):
+class RENDER_MT_ffmpeg_presets(PresetMenu):
     bl_label = "FFMPEG Presets"
     preset_subdir = "ffmpeg"
     preset_operator = "script.python_file_run"
-    draw = Menu.draw_preset
 
 
 class RENDER_MT_framerate_presets(Menu):
@@ -83,6 +83,9 @@ class RENDER_PT_dimensions(RenderButtonsPanel, Panel):
     _frame_rate_args_prev = None
     _preset_class = None
 
+    def draw_header_preset(self, context):
+        RENDER_MT_presets.draw_panel_header(self.layout)
+
     @staticmethod
     def _draw_framerate_label(*args):
         # avoids re-creating text string each draw
@@ -131,11 +134,6 @@ class RENDER_PT_dimensions(RenderButtonsPanel, Panel):
         scene = context.scene
         rd = scene.render
 
-        row = layout.row(align=True)
-        row.menu("RENDER_MT_presets", text=bpy.types.RENDER_MT_presets.bl_label)
-        row.operator("render.preset_add", text="", icon='ZOOMIN')
-        row.operator("render.preset_add", text="", icon='ZOOMOUT').remove_active = True
-
         col = layout.column(align=True)
         col.prop(rd, "resolution_x", text="Resolution X")
         col.prop(rd, "resolution_y", text="Y")
@@ -297,6 +295,9 @@ class RENDER_PT_encoding(RenderButtonsPanel, Panel):
     bl_options = {'DEFAULT_CLOSED'}
     COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE'}
 
+    def draw_header_preset(self, context):
+        RENDER_MT_ffmpeg_presets.draw_panel_header(self.layout)
+
     @classmethod
     def poll(cls, context):
         rd = context.scene.render
@@ -308,8 +309,6 @@ class RENDER_PT_encoding(RenderButtonsPanel, Panel):
         rd = context.scene.render
         ffmpeg = rd.ffmpeg
 
-        layout.menu("RENDER_MT_ffmpeg_presets", text="Presets")
-
         split = layout.split()
         split.prop(rd.ffmpeg, "format")
         split.prop(ffmpeg, "use_autosplit")
index bcffe97..9c5cf3a 100644 (file)
@@ -25,6 +25,7 @@ from bpy.types import (
 )
 
 from rna_prop_ui import PropertyPanel
+from bl_operators.presets import PresetMenu
 
 from .properties_physics_common import (
     point_cache_ui,
@@ -32,12 +33,12 @@ from .properties_physics_common import (
 )
 
 
-class SCENE_MT_units_length_presets(Menu):
+class SCENE_MT_units_length_presets(PresetMenu):
     """Unit of measure for properties that use length values"""
     bl_label = "Unit Presets"
     preset_subdir = "units_length"
     preset_operator = "script.execute_preset"
-    draw = Menu.draw_preset
+    preset_add_operator = "scene.units_length_preset_add"
 
 
 class SCENE_UL_keying_set_paths(UIList):
@@ -81,16 +82,14 @@ class SCENE_PT_unit(SceneButtonsPanel, Panel):
     bl_label = "Units"
     COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE'}
 
+    def draw_header_preset(self, context):
+        SCENE_MT_units_length_presets.draw_panel_header(self.layout)
+
     def draw(self, context):
         layout = self.layout
 
         unit = context.scene.unit_settings
 
-        row = layout.row(align=True)
-        row.menu("SCENE_MT_units_length_presets", text=SCENE_MT_units_length_presets.bl_label)
-        row.operator("scene.units_length_preset_add", text="", icon='ZOOMIN')
-        row.operator("scene.units_length_preset_add", text="", icon='ZOOMOUT').remove_active = True
-
         layout.use_property_split = True
 
         col = layout.column()
index a865eab..91e725b 100644 (file)
@@ -21,6 +21,7 @@
 import bpy
 from bpy.types import Panel, Header, Menu, UIList
 from bpy.app.translations import pgettext_iface as iface_
+from bl_operators.presets import PresetMenu
 from .properties_grease_pencil_common import (
     GreasePencilDrawingToolsPanel,
     GreasePencilStrokeEditPanel,
@@ -277,6 +278,9 @@ class CLIP_PT_tracking_settings(CLIP_PT_tracking_panel, Panel):
     bl_label = "Tracking Settings"
     bl_category = "Track"
 
+    def draw_header_preset(self, context):
+        CLIP_MT_tracking_settings_presets.draw_panel_header(self.layout)
+
     def draw(self, context):
 
         sc = context.space_data
@@ -285,14 +289,6 @@ class CLIP_PT_tracking_settings(CLIP_PT_tracking_panel, Panel):
         layout = self.layout
         col = layout.column()
 
-        row = col.row(align=True)
-        label = CLIP_MT_tracking_settings_presets.bl_label
-        row.menu('CLIP_MT_tracking_settings_presets', text=label)
-        row.operator("clip.tracking_settings_preset_add",
-                     text="", icon='ZOOMIN')
-        row.operator("clip.tracking_settings_preset_add",
-                     text="", icon='ZOOMOUT').remove_active = True
-
         row = col.row(align=True)
         row.prop(settings, "use_default_red_channel",
                  text="R", toggle=True)
@@ -625,12 +621,8 @@ class CLIP_PT_track(CLIP_PT_tracking_panel, Panel):
         layout.separator()
 
         row = layout.row(align=True)
-        label = bpy.types.CLIP_MT_track_color_presets.bl_label
-        row.menu('CLIP_MT_track_color_presets', text=label)
+        CLIP_MT_track_color_presets.draw_menu(row, 'Color Presets')
         row.menu('CLIP_MT_track_color_specials', text="", icon='DOWNARROW_HLT')
-        row.operator("clip.track_color_preset_add", text="", icon='ZOOMIN')
-        row.operator("clip.track_color_preset_add",
-                     text="", icon='ZOOMOUT').remove_active = True
 
         row = layout.row()
         row.prop(act_track, "use_custom_color")
@@ -720,19 +712,15 @@ class CLIP_PT_tracking_camera(Panel):
 
         return False
 
+    def draw_header_preset(self, context):
+        CLIP_MT_camera_presets.draw_panel_header(self.layout)
+
     def draw(self, context):
         layout = self.layout
 
         sc = context.space_data
         clip = sc.clip
 
-        row = layout.row(align=True)
-        label = bpy.types.CLIP_MT_camera_presets.bl_label
-        row.menu('CLIP_MT_camera_presets', text=label)
-        row.operator("clip.camera_preset_add", text="", icon='ZOOMIN')
-        row.operator("clip.camera_preset_add", text="",
-                     icon='ZOOMOUT').remove_active = True
-
         col = layout.column(align=True)
         col.label(text="Sensor:")
         col.prop(clip.tracking.camera, "sensor_width", text="Width")
@@ -1431,28 +1419,28 @@ class CLIP_MT_tracking_specials(Menu):
                         text="Unlock Tracks").action = 'UNLOCK'
 
 
-class CLIP_MT_camera_presets(Menu):
+class CLIP_MT_camera_presets(PresetMenu):
     """Predefined tracking camera intrinsics"""
     bl_label = "Camera Presets"
     preset_subdir = "tracking_camera"
     preset_operator = "script.execute_preset"
-    draw = Menu.draw_preset
+    preset_add_operator = "clip.camera_preset_add"
 
 
-class CLIP_MT_track_color_presets(Menu):
+class CLIP_MT_track_color_presets(PresetMenu):
     """Predefined track color"""
     bl_label = "Color Presets"
     preset_subdir = "tracking_track_color"
     preset_operator = "script.execute_preset"
-    draw = Menu.draw_preset
+    preset_add_operator = "clip.track_color_preset_add"
 
 
-class CLIP_MT_tracking_settings_presets(Menu):
+class CLIP_MT_tracking_settings_presets(PresetMenu):
     """Predefined tracking settings"""
     bl_label = "Tracking Presets"
     preset_subdir = "tracking_settings"
     preset_operator = "script.execute_preset"
-    draw = Menu.draw_preset
+    preset_add_operator = "clip.tracking_settings_preset_add"
 
 
 class CLIP_MT_track_color_specials(Menu):
index 1b6baa7..7c9c47f 100644 (file)
@@ -21,6 +21,7 @@ import bpy
 import nodeitems_utils
 from bpy.types import Header, Menu, Panel
 from bpy.app.translations import pgettext_iface as iface_
+from bl_operators.presets import PresetMenu
 from .properties_grease_pencil_common import (
     GreasePencilDrawingToolsPanel,
     GreasePencilStrokeEditPanel,
@@ -289,12 +290,12 @@ class NODE_MT_node(Menu):
         layout.operator("node.read_fullsamplelayers")
 
 
-class NODE_MT_node_color_presets(Menu):
+class NODE_MT_node_color_presets(PresetMenu):
     """Predefined node color"""
     bl_label = "Color Presets"
     preset_subdir = "node_color"
     preset_operator = "script.execute_preset"
-    draw = Menu.draw_preset
+    preset_add_operator = "node.node_color_preset_add"
 
 
 class NODE_MT_node_color_specials(Menu):
@@ -373,6 +374,9 @@ class NODE_PT_active_node_color(Panel):
         node = context.active_node
         self.layout.prop(node, "use_custom_color", text="")
 
+    def draw_header_preset(self, context):
+        NODE_MT_node_color_presets.draw_panel_header(self.layout)
+
     def draw(self, context):
         layout = self.layout
         node = context.active_node
@@ -380,13 +384,8 @@ class NODE_PT_active_node_color(Panel):
         layout.enabled = node.use_custom_color
 
         row = layout.row()
-        col = row.column()
-        col.menu("NODE_MT_node_color_presets")
-        col.prop(node, "color", text="")
-        col = row.column(align=True)
-        col.operator("node.node_color_preset_add", text="", icon='ZOOMIN').remove_active = False
-        col.operator("node.node_color_preset_add", text="", icon='ZOOMOUT').remove_active = True
-        col.menu("NODE_MT_node_color_specials", text="", icon='DOWNARROW_HLT')
+        row.prop(node, "color", text="")
+        row.menu("NODE_MT_node_color_specials", text="", icon='DOWNARROW_HLT')
 
 
 class NODE_PT_active_node_properties(Panel):
index 4270e6e..1b42ce9 100644 (file)
@@ -216,6 +216,8 @@ typedef struct PanelType {
        int (*poll)(const struct bContext *C, struct PanelType *pt);
        /* draw header (optional) */
        void (*draw_header)(const struct bContext *C, struct Panel *pa);
+       /* draw header preset (optional) */
+       void (*draw_header_preset)(const struct bContext *C, struct Panel *pa);
        /* draw entirely, view changes should be handled here */
        void (*draw)(const struct bContext *C, struct Panel *pa);
 
index 3bc1255..659f6c9 100644 (file)
@@ -113,9 +113,7 @@ DEF_ICON(FILE_TICK)
 DEF_ICON(QUIT)
 DEF_ICON(URL)
 DEF_ICON(RECOVER_LAST)
-#ifndef DEF_ICON_BLANK_SKIP
-       DEF_ICON(BLANK038)
-#endif
+DEF_ICON(PRESET)
 DEF_ICON(FULLSCREEN_ENTER)
 DEF_ICON(FULLSCREEN_EXIT)
 DEF_ICON(BLANK1)       // Not actually blank - this is used all over the place
index e769d36..fb14ca7 100644 (file)
@@ -168,11 +168,17 @@ static uiBlock *ui_block_func_POPOVER(bContext *C, uiPopupBlockHandle *handle, v
                        block->my = handle->prev_my;
                }
 
-               /* Prefer popover from header to be positioned into the editor. */
                if (!slideout) {
                        ScrArea *sa = CTX_wm_area(C);
-                       if (sa && ED_area_header_alignment(sa) == RGN_ALIGN_BOTTOM) {
-                               ARegion *ar = CTX_wm_region(C);
+                       ARegion *ar = CTX_wm_region(C);
+
+                       if (ar && ar->panels.first) {
+                               /* For regions with panels, prefer to open to top so we can
+                                * see the values of the buttons below changing. */
+                               UI_block_direction_set(block, UI_DIR_UP | UI_DIR_CENTER_X);
+                       }
+                       else if (sa && ED_area_header_alignment(sa) == RGN_ALIGN_BOTTOM) {
+                               /* Prefer popover from header to be positioned into the editor. */
                                if (ar && ar->regiontype == RGN_TYPE_HEADER) {
                                        UI_block_direction_set(block, UI_DIR_UP | UI_DIR_CENTER_X);
                                }
index 478afa6..02ce64b 100644 (file)
@@ -1861,6 +1861,21 @@ static void ed_panel_draw(const bContext *C,
        /* bad fixed values */
        int xco, yco, h = 0;
 
+       if (pt->draw_header_preset && !(pt->flag & PNL_NO_HEADER) && (open || vertical)) {
+               /* for preset menu */
+               panel->layout = UI_block_layout(
+                       block, UI_LAYOUT_HORIZONTAL, UI_LAYOUT_HEADER,
+                       0, (UI_UNIT_Y * 1.1f) + style->panelspace, UI_UNIT_Y, 1, 0, style);
+
+               pt->draw_header_preset(C, panel);
+
+               int headerend = w - UI_UNIT_X;
+
+               UI_block_layout_resolve(block, &xco, &yco);
+               UI_block_translate(block, headerend - xco, 0);
+               panel->layout = NULL;
+       }
+
        if (pt->draw_header && !(pt->flag & PNL_NO_HEADER) && (open || vertical)) {
                int labelx, labely;
                UI_panel_label_offset(block, &labelx, &labely);
index ebee502..8083ae3 100644 (file)
@@ -166,6 +166,24 @@ static void panel_draw_header(const bContext *C, Panel *pnl)
        RNA_parameter_list_free(&list);
 }
 
+static void panel_draw_header_preset(const bContext *C, Panel *pnl)
+{
+       extern FunctionRNA rna_Panel_draw_header_preset_func;
+
+       PointerRNA ptr;
+       ParameterList list;
+       FunctionRNA *func;
+
+       RNA_pointer_create(&CTX_wm_screen(C)->id, pnl->type->ext.srna, pnl, &ptr);
+       func = &rna_Panel_draw_header_preset_func;
+
+       RNA_parameter_list_create(&list, &ptr, func);
+       RNA_parameter_set_lookup(&list, "context", &C);
+       pnl->type->ext.call((bContext *)C, &ptr, func, &list);
+
+       RNA_parameter_list_free(&list);
+}
+
 static void rna_Panel_unregister(Main *UNUSED(bmain), StructRNA *type)
 {
        ARegionType *art;
@@ -199,7 +217,7 @@ static StructRNA *rna_Panel_register(
        PanelType *pt, *parent = NULL, dummypt = {NULL};
        Panel dummypanel = {NULL};
        PointerRNA dummyptr;
-       int have_function[3];
+       int have_function[4];
 
        /* setup dummy panel & panel type to store static properties in */
        dummypanel.type = &dummypt;
@@ -267,6 +285,7 @@ static StructRNA *rna_Panel_register(
        pt->poll = (have_function[0]) ? panel_poll : NULL;
        pt->draw = (have_function[1]) ? panel_draw : NULL;
        pt->draw_header = (have_function[2]) ? panel_draw_header : NULL;
+       pt->draw_header_preset = (have_function[3]) ? panel_draw_header_preset : NULL;
 
        /* XXX use "no header" flag for some ordering of panels until we have real panel ordering */
        if (pt->flag & PNL_NO_HEADER) {
@@ -1058,6 +1077,12 @@ static void rna_def_panel(BlenderRNA *brna)
        parm = RNA_def_pointer(func, "context", "Context", "", "");
        RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED);
 
+       func = RNA_def_function(srna, "draw_header_preset", NULL);
+       RNA_def_function_ui_description(func, "Draw UI elements for presets in the panel's header");
+       RNA_def_function_flag(func, FUNC_REGISTER_OPTIONAL);
+       parm = RNA_def_pointer(func, "context", "Context", "", "");
+       RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED);
+
        prop = RNA_def_property(srna, "layout", PROP_POINTER, PROP_NONE);
        RNA_def_property_struct_type(prop, "UILayout");
        RNA_def_property_ui_text(prop, "Layout", "Defines the structure of the panel in the UI");