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,
prop.StrProperty,
operator.BIM_OT_enum_property_search, # /!\ Register AFTER prop.StrProperty
operator.BIM_OT_attribute_search_values,
prop.ObjProperty,
prop.MultipleFileSelect,
prop.Attribute,
+16 -2
View File
@@ -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="")
+1 -1
View File
@@ -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")
+95
View File
@@ -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"}