From e743db27f4558baca072ab64a61a08ddd941fa6e Mon Sep 17 00:00:00 2001 From: falken10vdl <33285113+falken10vdl@users.noreply.github.com> Date: Wed, 2 Jul 2025 08:11:29 +0200 Subject: [PATCH] Draw attributes with search (#6849) See example in #4325 --- src/bonsai/bonsai/bim/__init__.py | 1 + src/bonsai/bonsai/bim/helper.py | 18 +++- src/bonsai/bonsai/bim/module/material/ui.py | 2 +- src/bonsai/bonsai/bim/operator.py | 95 +++++++++++++++++++++ 4 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 50be50b3bf..da27e8259b 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -125,6 +125,7 @@ classes = [ operator.ShowSystemInfo, prop.StrProperty, operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty + operator.BIM_OT_attribute_search_values, prop.ObjProperty, prop.MultipleFileSelect, prop.Attribute, diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 31be04f874..b19dfa18e2 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -52,26 +52,33 @@ def draw_attributes( copy_operator: Optional[str] = None, popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None, callback: Optional[Callable[[bonsai.bim.prop.Attribute, bpy.types.UILayout], None]] = None, + enable_search: bool = False, ) -> None: """Draw editable UI for prop.Attributes. You can set attribute active in popup with `active_attribute` meaning you will be able to type into attribute's field without having to click on it first + + :param enable_search: Add search button to string, integer, and float attributes """ for attribute in props: row = layout.row(align=True) if attribute == popup_active_attribute: row.activate_init = True - draw_attribute(attribute, row, copy_operator) + draw_attribute(attribute, row, copy_operator, enable_search=enable_search) if callback: callback(attribute, row) def draw_attribute( - attribute: bonsai.bim.prop.Attribute, layout: bpy.types.UILayout, copy_operator: Optional[str] = None + attribute: bonsai.bim.prop.Attribute, + layout: bpy.types.UILayout, + copy_operator: Optional[str] = None, + enable_search: bool = False, ) -> None: value_name = attribute.get_value_name(display_only=True) + if value_name == "enum_value": prop_with_search(layout, attribute, "enum_value", text=attribute.name) elif value_name == "filepath_value": @@ -104,6 +111,13 @@ def draw_attribute( op.target_prop = attribute.path_from_id("string_value") op.include_time = attribute.special_type == "DATETIME" + if enable_search and attribute.data_type in ("string", "integer", "float"): + op = layout.operator("bim.attribute_search_values", text="", icon="VIEWZOOM") + op.attribute_name = attribute.name + op.attribute_ifc_class = attribute.ifc_class + op.data_path = attribute.path_from_id(value_name) + op.data_type = attribute.data_type + if attribute.is_optional: layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index 1b49a4aeee..99efecce65 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -110,7 +110,7 @@ class BIM_PT_materials(Panel): return ifc_definition_id = self.props.active_material_id if self.props.editing_material_type == "ATTRIBUTES": - bonsai.bim.helper.draw_attributes(self.props.material_attributes, self.layout) + bonsai.bim.helper.draw_attributes(self.props.material_attributes, self.layout, enable_search=True) row = self.layout.row(align=True) row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id row.operator("bim.disable_editing_material", text="", icon="CANCEL") diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 5be88d306f..2fbba0223b 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -44,6 +44,7 @@ from pathlib import Path from collections import namedtuple from typing import Union, TYPE_CHECKING from collections.abc import Iterable +from natsort import natsorted if TYPE_CHECKING: from bonsai.bim.prop import MultipleFileSelect @@ -1299,3 +1300,97 @@ class ShowSystemInfo(bpy.types.Operator): col.separator() col.label(text="(The information has been copied to the clipboard.)") + + +def update_attribute_search_value(self, context): + should_click_ok = False + attr_name, attribute_obj = BIM_OT_attribute_search_values.resolve_data_path(self.data_path) + + value = self.search_value + if self.data_type == "integer": + value = int(value) + elif self.data_type == "float": + value = float(value) + + setattr(attribute_obj, attr_name, value) + + if self.first_launch: + self.first_launch = False + else: + if not should_click_ok: + context.window.screen = context.window.screen + + +class BIM_OT_attribute_search_values(bpy.types.Operator): + """Search for attribute values. This implementation is based on bim.enum_property_search""" + + bl_idname = "bim.attribute_search_values" + bl_label = "Search Attribute Values" + bl_description = "Search for attribute values within a collection" + bl_options = {"REGISTER", "UNDO"} + first_launch: bpy.props.BoolProperty(default=True, options={"SKIP_SAVE"}) + attribute_name: bpy.props.StringProperty(name="Attribute Name") + attribute_ifc_class: bpy.props.StringProperty(name="Attribute IFC Class") + data_path: bpy.props.StringProperty(name="Data Path") + data_type: bpy.props.StringProperty(name="Data Type") + search_value: bpy.props.StringProperty( + name="Search", + description="Search for attribute values", + update=update_attribute_search_value, + default="", + options={"SKIP_SAVE"}, + ) + collection_values: bpy.props.CollectionProperty(type=StrProperty, options={"SKIP_SAVE"}) + + @staticmethod + def resolve_data_path(data_path: str) -> tuple[str, object]: + """Resolve the data path of an object's attribute to get the attribute name and the object.""" + path_parts = data_path.split(".") + obj_path = ".".join(path_parts[:-1]) + attr_name = path_parts[-1] + attribute_obj = eval(f"bpy.context.scene.{obj_path}") + return attr_name, attribute_obj + + def invoke(self, context, event): + attr_name, attribute_obj = self.resolve_data_path(self.data_path) + self.search_value = str(getattr(attribute_obj, attr_name, "")) + + unique_values = self.get_unique_attribute_values() + string_values = natsorted(unique_values) + + for value in string_values: + self.collection_values.add().name = value + + return context.window_manager.invoke_props_dialog(self) + + def get_unique_attribute_values(self): + ifc_file = tool.Ifc.get() + unique_values = set() + ifc_class = self.attribute_ifc_class + + elements = ifc_file.by_type(ifc_class, include_subtypes=True) + + for element in elements: + # We check just direct entity attributes and simply check if the attribute exists + if hasattr(element, self.attribute_name): + value = getattr(element, self.attribute_name) + if value is not None: + unique_values.add(str(value)) + + return list(unique_values) + + def draw(self, context): + row = self.layout.row() + row.label(text=f"Select {self.attribute_name} value:") + row = self.layout.row() + row.prop_search( + self, + "search_value", + self, + "collection_values", + text="", + results_are_suggestions=True, + ) + + def execute(self, context): + return {"FINISHED"}