[Bf-blender-cvs] [75d62285831] master: PyAPI: Generic UIList for CollectionProperties

demeterdzadik@gmail.com noreply at git.blender.org
Fri Jan 27 14:52:22 CET 2023


Commit: 75d62285831636a1c2878dc2b5aa8a2836be4eb1
Author: demeterdzadik at gmail.com
Date:   Fri Jan 27 14:44:06 2023 +0100
Branches: master
https://developer.blender.org/rB75d62285831636a1c2878dc2b5aa8a2836be4eb1

PyAPI: Generic UIList for CollectionProperties

This patch adds a draw_ui_list() function, which is a wrapper around
layout.template_list(). It implements generic add/remove/move buttons,
passing the correct "row" integer to template_list(), as well as a
drop-down menu, if provided, making it a complete solution for
consistent UILists for addons.

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

===================================================================

M	release/scripts/startup/bl_ui/__init__.py
A	release/scripts/startup/bl_ui/generic_ui_list.py
A	release/scripts/templates_py/ui_list_generic.py

===================================================================

diff --git a/release/scripts/startup/bl_ui/__init__.py b/release/scripts/startup/bl_ui/__init__.py
index 01ae0730fab..7d20962ae4e 100644
--- a/release/scripts/startup/bl_ui/__init__.py
+++ b/release/scripts/startup/bl_ui/__init__.py
@@ -52,6 +52,7 @@ _modules = [
     "properties_texture",
     "properties_world",
     "properties_collection",
+    "generic_ui_list",
 
     # Generic Space Modules
     #
diff --git a/release/scripts/startup/bl_ui/generic_ui_list.py b/release/scripts/startup/bl_ui/generic_ui_list.py
new file mode 100644
index 00000000000..02157a4588d
--- /dev/null
+++ b/release/scripts/startup/bl_ui/generic_ui_list.py
@@ -0,0 +1,253 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+import bpy
+from bpy.types import Operator, UILayout, Context
+from bpy.props import EnumProperty, StringProperty
+
+"""
+This module (in particular the draw_ui_list function) lets you draw the commonly
+used UIList layout, seen all over Blender.
+
+This includes the list itself, and a column of buttons to the right of it, which
+contains buttons to add, remove, and move entries up or down, as well as a
+drop-down menu.
+
+You can get an example of how to use this via the Blender Text Editor->
+Templates->Ui List Generic.
+"""
+
+
+def draw_ui_list(
+        layout: UILayout,
+        context: Context,
+        class_name="UI_UL_list",
+        *,
+        unique_id="",
+        list_path: str,
+        active_idx_path: str,
+        insertion_operators=True,
+        move_operators=True,
+        menu_class_name="",
+        **kwargs) -> UILayout:
+    """
+    Draw a UIList with Add/Remove/Move buttons and a menu.
+
+    :param layout:
+        UILayout to draw the list in.
+    :param context:
+        Blender context to get the list data from.
+    :param class_name:
+        Name of the UIList class to draw. The default is the
+        UIList class that ships with Blender.
+    :param unique_id:
+        Optional identifier, in case wanting to draw multiple unique copies of a list.
+
+    :param list_path:
+        Data path of the list relative to context, eg. "object.vertex_groups".
+    :param active_idx_path:
+        Data path of the list active index integer relative to context,
+        eg. "object.vertex_groups.active_index".
+
+    :param insertion_operators:
+        Whether to draw Add/Remove buttons.
+    :param move_operators:
+        Whether to draw Move Up/Down buttons.
+    :param menu_class_name:
+        Name of a Menu that should be drawn as a drop-down.
+
+    Additional keyword arguments are passed to template_list().
+    """
+    row = layout.row()
+
+    list_owner_path, list_prop_name = list_path.rsplit('.', 1)
+    list_owner = _get_context_attr(context, list_owner_path)
+
+    idx_owner_path, idx_prop_name = active_idx_path.rsplit('.', 1)
+    idx_owner = _get_context_attr(context, idx_owner_path)
+
+    list_to_draw = _get_context_attr(context, list_path)
+
+    row.template_list(
+        class_name,
+        unique_id,
+        list_owner, list_prop_name,
+        idx_owner, idx_prop_name,
+        rows=4 if len(list_to_draw) > 0 else 1,
+        **kwargs
+    )
+
+    col = row.column()
+
+    if insertion_operators:
+        _draw_add_remove_buttons(
+            layout=col,
+            list_path=list_path,
+            active_idx_path=active_idx_path,
+            list_length=len(list_to_draw)
+        )
+        layout.separator()
+
+    if menu_class_name:
+        col.menu(menu_class_name, icon='DOWNARROW_HLT', text="")
+        col.separator()
+
+    if move_operators and len(list_to_draw) > 0:
+        _draw_move_buttons(
+            layout=col,
+            list_path=list_path,
+            active_idx_path=active_idx_path,
+            list_length=len(list_to_draw)
+        )
+
+    # Return the right-side column.
+    return col
+
+
+def _draw_add_remove_buttons(
+    *,
+    layout: UILayout,
+    list_path: str,
+    active_idx_path: str,
+    list_length: int
+) -> None:
+    """Draw the +/- buttons to add and remove list entries."""
+    add_op = layout.operator(UILIST_OT_entry_add.bl_idname, text="", icon='ADD')
+    add_op.list_path = list_path
+    add_op.active_idx_path = active_idx_path
+
+    row = layout.row()
+    row.enabled = list_length > 0
+    remove_op = row.operator(UILIST_OT_entry_remove.bl_idname, text="", icon='REMOVE')
+    remove_op.list_path = list_path
+    remove_op.active_idx_path = active_idx_path
+
+
+def _draw_move_buttons(
+    *,
+    layout: UILayout,
+    list_path: str,
+    active_idx_path: str,
+    list_length: int
+) -> None:
+    """Draw the up/down arrows to move elements in the list."""
+    col = layout.column()
+    col.enabled = list_length > 0
+    move_up_op = layout.operator(UILIST_OT_entry_move.bl_idname, text="", icon='TRIA_UP')
+    move_up_op.direction = 'UP'
+    move_up_op.list_path = list_path
+    move_up_op.active_idx_path = active_idx_path
+
+    move_down_op = layout.operator(UILIST_OT_entry_move.bl_idname, text="", icon='TRIA_DOWN')
+    move_down_op.direction = 'DOWN'
+    move_down_op.list_path = list_path
+    move_down_op.active_idx_path = active_idx_path
+
+
+def _get_context_attr(context: Context, data_path: str) -> object:
+    """Return the value of a context member based on its data path."""
+    return context.path_resolve(data_path)
+
+
+def _set_context_attr(context: Context, data_path: str, value: object) -> None:
+    """Set the value of a context member based on its data path."""
+    owner_path, attr_name = data_path.rsplit('.', 1)
+    owner = context.path_resolve(owner_path)
+    setattr(owner, attr_name, value)
+
+
+class GenericUIListOperator:
+    """Mix-in class containing functionality shared by operators
+    that deal with managing Blender list entries."""
+    bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
+
+    list_path: StringProperty()
+    active_idx_path: StringProperty()
+
+    def get_list(self, context) -> str:
+        return _get_context_attr(context, self.list_path)
+
+    def get_active_index(self, context) -> str:
+        return _get_context_attr(context, self.active_idx_path)
+
+    def set_active_index(self, context, index):
+        _set_context_attr(context, self.active_idx_path, index)
+
+
+# noinspection PyPep8Naming
+class UILIST_OT_entry_remove(GenericUIListOperator, Operator):
+    """Remove the selected entry from the list"""
+
+    bl_idname = "uilist.entry_remove"
+    bl_label = "Remove Selected Entry"
+
+    def execute(self, context):
+        my_list = self.get_list(context)
+        active_index = self.get_active_index(context)
+
+        my_list.remove(active_index)
+        to_index = min(active_index, len(my_list) - 1)
+        self.set_active_index(context, to_index)
+
+        return {'FINISHED'}
+
+
+# noinspection PyPep8Naming
+class UILIST_OT_entry_add(GenericUIListOperator, Operator):
+    """Add an entry to the list after the current active item"""
+
+    bl_idname = "uilist.entry_add"
+    bl_label = "Add Entry"
+
+    def execute(self, context):
+        my_list = self.get_list(context)
+        active_index = self.get_active_index(context)
+
+        to_index = min(len(my_list), active_index + 1)
+
+        my_list.add()
+        my_list.move(len(my_list) - 1, to_index)
+        self.set_active_index(context, to_index)
+
+        return {'FINISHED'}
+
+
+# noinspection PyPep8Naming
+class UILIST_OT_entry_move(GenericUIListOperator, Operator):
+    """Move an entry in the list up or down"""
+
+    bl_idname = "uilist.entry_move"
+    bl_label = "Move Entry"
+
+    direction: EnumProperty(
+        name="Direction",
+        items=[('UP', 'UP', 'UP'),
+               ('DOWN', 'DOWN', 'DOWN')],
+        default='UP'
+    )
+
+    def execute(self, context):
+        my_list = self.get_list(context)
+        active_index = self.get_active_index(context)
+
+        delta = {
+            "DOWN": 1,
+            "UP": -1,
+        }[self.direction]
+
+        to_index = (active_index + delta) % len(my_list)
+
+        my_list.move(active_index, to_index)
+        self.set_active_index(context, to_index)
+
+        return {'FINISHED'}
+
+
+# =============================================
+# Registration
+
+classes = (
+    UILIST_OT_entry_remove,
+    UILIST_OT_entry_add,
+    UILIST_OT_entry_move,
+)
+
+register, unregister = bpy.utils.register_classes_factory(classes)
diff --git a/release/scripts/templates_py/ui_list_generic.py b/release/scripts/templates_py/ui_list_generic.py
new file mode 100644
index 00000000000..ad370ec4305
--- /dev/null
+++ b/release/scripts/templates_py/ui_list_generic.py
@@ -0,0 +1,46 @@
+import bpy
+from bl_ui.generic_ui_list import draw_ui_list
+
+
+class MyPropGroup(bpy.types.PropertyGroup):
+    name: bpy.props.StringProperty()
+
+
+class MyPanel(bpy.types.Panel):
+    bl_label = "My Label"
+    bl_idname = "SCENE_PT_list_demo"
+    bl_space_type = 'VIEW_3D'
+    bl_region_type = 'UI'
+    bl_category = 'My Category'
+
+    def draw(self, context):
+        layout = self.layout
+        draw_ui_list(
+            layout,
+            context,
+            list_context_path="scene.my_list",
+            active_idx_context_path="scene.my_list_active_idx"
+        )
+
+
+classes = [
+    MyPropGroup,
+    MyPanel
+]
+
+class_register, class_unregister = bpy.utils.register_classes_factory(classes)
+
+
+def register():
+    class_register()
+    bpy.types.Scene.my_list = bpy.props.CollectionProperty(type=MyPropGroup)
+    bpy.types.Scene.my_list_active_idx = bpy.props.IntProperty()
+
+
+def unregister():
+    class_unregister()
+    del bpy.types.Scene.my_list
+    del bpy.types.Scene.my_list_active_idx
+
+
+register()



More information about the Bf-blender-cvs mailing list