Draw attributes with search (#6849)

See example in #4325
This commit is contained in:
falken10vdl
2025-07-02 08:11:29 +02:00
committed by GitHub
parent bb329affb8
commit e743db27f4
4 changed files with 113 additions and 3 deletions
+1
View File
@@ -125,6 +125,7 @@ classes = [
operator.ShowSystemInfo, operator.ShowSystemInfo,
prop.StrProperty, prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
operator.BIM_OT_attribute_search_values,
prop.ObjProperty, prop.ObjProperty,
prop.MultipleFileSelect, prop.MultipleFileSelect,
prop.Attribute, prop.Attribute,
+16 -2
View File
@@ -52,26 +52,33 @@ def draw_attributes(
copy_operator: Optional[str] = None, copy_operator: Optional[str] = None,
popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None, popup_active_attribute: Optional[bonsai.bim.prop.Attribute] = None,
callback: Optional[Callable[[bonsai.bim.prop.Attribute, bpy.types.UILayout], None]] = None, callback: Optional[Callable[[bonsai.bim.prop.Attribute, bpy.types.UILayout], None]] = None,
enable_search: bool = False,
) -> None: ) -> None:
"""Draw editable UI for prop.Attributes. """Draw editable UI for prop.Attributes.
You can set attribute active in popup with `active_attribute` 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 meaning you will be able to type into attribute's field without having to click
on it first on it first
:param enable_search: Add search button to string, integer, and float attributes
""" """
for attribute in props: for attribute in props:
row = layout.row(align=True) row = layout.row(align=True)
if attribute == popup_active_attribute: if attribute == popup_active_attribute:
row.activate_init = True row.activate_init = True
draw_attribute(attribute, row, copy_operator) draw_attribute(attribute, row, copy_operator, enable_search=enable_search)
if callback: if callback:
callback(attribute, row) callback(attribute, row)
def draw_attribute( 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: ) -> None:
value_name = attribute.get_value_name(display_only=True) value_name = attribute.get_value_name(display_only=True)
if value_name == "enum_value": if value_name == "enum_value":
prop_with_search(layout, attribute, "enum_value", text=attribute.name) prop_with_search(layout, attribute, "enum_value", text=attribute.name)
elif value_name == "filepath_value": elif value_name == "filepath_value":
@@ -104,6 +111,13 @@ def draw_attribute(
op.target_prop = attribute.path_from_id("string_value") op.target_prop = attribute.path_from_id("string_value")
op.include_time = attribute.special_type == "DATETIME" 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: if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
+1 -1
View File
@@ -110,7 +110,7 @@ class BIM_PT_materials(Panel):
return return
ifc_definition_id = self.props.active_material_id ifc_definition_id = self.props.active_material_id
if self.props.editing_material_type == "ATTRIBUTES": 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 = self.layout.row(align=True)
row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id row.operator("bim.edit_material", text="Save Material", icon="CHECKMARK").material = ifc_definition_id
row.operator("bim.disable_editing_material", text="", icon="CANCEL") row.operator("bim.disable_editing_material", text="", icon="CANCEL")
+95
View File
@@ -44,6 +44,7 @@ from pathlib import Path
from collections import namedtuple from collections import namedtuple
from typing import Union, TYPE_CHECKING from typing import Union, TYPE_CHECKING
from collections.abc import Iterable from collections.abc import Iterable
from natsort import natsorted
if TYPE_CHECKING: if TYPE_CHECKING:
from bonsai.bim.prop import MultipleFileSelect from bonsai.bim.prop import MultipleFileSelect
@@ -1299,3 +1300,97 @@ class ShowSystemInfo(bpy.types.Operator):
col.separator() col.separator()
col.label(text="(The information has been copied to the clipboard.)") 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"}