From e52e5e2e58625f1e8c5f74428ab83030d58dfb37 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Mon, 20 Jul 2026 20:39:22 -0500 Subject: [PATCH] Bonsai: add category-level select-all to the Drawings list (#8826) Add an "Is Selected" checkbox to each target-view category header in BIM_UL_drawinglist that toggles selection for all drawings in the category. The toggle only affects drawings currently visible in the list (honoring the show_drawings_on_sheets_only filter), and the header checkbox reflects the aggregate selection state of its drawings. Also make category headers more obvious: wrap them in a box() for a distinct inset background and make the header name clickable to expand/contract the category (same as the disclosure triangle). Ref: #8825 Co-authored-by: Claude Opus 4.8 --- .../bonsai/bim/module/drawing/__init__.py | 1 + .../bonsai/bim/module/drawing/operator.py | 20 ++++++++++++++++ src/bonsai/bonsai/bim/module/drawing/ui.py | 19 +++++++++++++-- src/bonsai/bonsai/tool/drawing.py | 23 +++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/__init__.py b/src/bonsai/bonsai/bim/module/drawing/__init__.py index 12b63d9372..1bca9eab49 100644 --- a/src/bonsai/bonsai/bim/module/drawing/__init__.py +++ b/src/bonsai/bonsai/bim/module/drawing/__init__.py @@ -108,6 +108,7 @@ classes = ( operator.SelectAssignedProduct, operator.SelectSimilarTextLiteralValue, operator.ToggleTargetView, + operator.ToggleDrawingCategorySelection, operator.OpenDocumentationWebUi, operator.FilterSelectedObjectsIfIntersectedByCamera, prop.Variable, diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index cc0232c285..27cfb90ddc 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3874,6 +3874,26 @@ class ToggleTargetView(bpy.types.Operator): return {"FINISHED"} +class ToggleDrawingCategorySelection(bpy.types.Operator): + bl_idname = "bim.toggle_drawing_category_selection" + bl_label = "Toggle Category Selection" + bl_description = "Select or deselect all drawings in this view category" + bl_options = {"REGISTER", "UNDO"} + + target_view: bpy.props.StringProperty() + + if TYPE_CHECKING: + target_view: str + + def execute(self, context): + drawings = tool.Drawing.get_visible_drawings_in_category(self.target_view) + # If everything visible in the category is already selected, deselect all; otherwise select all. + new_state = not all(d.is_selected for d in drawings) + for drawing in drawings: + drawing.is_selected = new_state + return {"FINISHED"} + + class ExpandSheet(bpy.types.Operator): bl_idname = "bim.expand_sheet" bl_label = "Expand Sheet" diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index b9115dc479..5efec1737e 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -874,8 +874,8 @@ class BIM_UL_drawinglist(bpy.types.UIList): layout.label(text="", translate=False) return - row = layout.row(align=True) if item.is_drawing: + row = layout.row(align=True) row.label(text="", icon="BLANK1") selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT" row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False) @@ -896,6 +896,9 @@ class BIM_UL_drawinglist(bpy.types.UIList): item.ifc_definition_id ) else: + # Give category headers a distinct inset background so they stand out from drawing rows. + box = layout.box() + row = box.row(align=True) if item.target_view == "PLAN_VIEW": icon = "UV_FACESEL" elif item.target_view == "ELEVATION_VIEW": @@ -916,7 +919,19 @@ class BIM_UL_drawinglist(bpy.types.UIList): op = row.operator("bim.toggle_target_view", text="", emboss=False, icon="DISCLOSURE_TRI_RIGHT") op.target_view = item.target_view op.option = "EXPAND" - row.prop(item, "name", text="", icon=icon, emboss=False) + group = tool.Drawing.get_visible_drawings_in_category(item.target_view) + all_selected = bool(group) and all(d.is_selected for d in group) + row.operator( + "bim.toggle_drawing_category_selection", + text="", + icon="CHECKBOX_HLT" if all_selected else "CHECKBOX_DEHLT", + emboss=False, + ).target_view = item.target_view + row.separator(factor=0.5, type="SPACE") + # Clicking the header name toggles expand/contract, same as the disclosure triangle. + op = row.operator("bim.toggle_target_view", text=item.name, icon=icon, emboss=False) + op.target_view = item.target_view + op.option = "CONTRACT" if item.is_expanded else "EXPAND" def filter_items(self, context, data: DocProperties, propname: str): drawings = getattr(data, propname) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index ab69aa96c4..d08faa0229 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -2908,6 +2908,29 @@ class Drawing(bonsai.core.tool.Drawing): result.add(drawing.id()) return result + @classmethod + def get_visible_drawings_in_category(cls, target_view: str) -> list[DrawingProperties]: + """Get the drawing items in a target view category that are currently visible in the drawing list. + + Grouping is positional: individual drawing items don't carry their own ``target_view``, they belong to + the most recent header item above them. Only expanded categories contribute drawing items to the + collection, so a collapsed category yields an empty list. Respects the ``show_drawings_on_sheets_only`` + filter so that select-all only affects visible drawings. + """ + props = cls.get_document_props() + drawings: list[DrawingProperties] = [] + in_category = False + for item in props.drawings: + if not item.is_drawing: + # Header row: we're inside the requested category until the next header. + in_category = item.target_view == target_view + elif in_category: + drawings.append(item) + if props.show_drawings_on_sheets_only: + sheeted_ids = cls.get_sheeted_drawing_ids() + drawings = [d for d in drawings if d.ifc_definition_id in sheeted_ids] + return drawings + @classmethod def get_camera_matrix(cls, camera: bpy.types.Object) -> Matrix: matrix_world = camera.matrix_world.copy().normalized()