From 8c667b8ae052b29a0b18df9d5d5bef99e387c6f5 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Tue, 21 Jul 2026 15:54:59 +0300 Subject: [PATCH 1/3] Bonsai: right-click rename on a material name (#6680) Adds a "Rename Material" entry to the context menu that already extends every button in the properties editor (UI_MT_button_context_menu), triggered when right-clicking a material name button (bim.select_by_material) that points to a real IfcMaterial. This gives a quick entry point to renaming from the Object Material panel without navigating to the scene Materials list. This follows the pattern that #6680's thread converged on: theoryshaw requested a right-click entry (rather than a pencil icon or double-click) that keeps the existing single-click select-by-material behaviour intact. falken10vdl is the issue's assignee; this is offered as a starting point for that discussion, not a replacement for it. Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/material/__init__.py | 1 + .../bonsai/bim/module/material/operator.py | 20 +++++++++++++++++++ src/bonsai/bonsai/bim/ui.py | 16 +++++++++++++++ src/bonsai/bonsai/core/material.py | 8 ++++++++ 4 files changed, 45 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/material/__init__.py b/src/bonsai/bonsai/bim/module/material/__init__.py index 5f14b170ee..50afbd4dff 100644 --- a/src/bonsai/bonsai/bim/module/material/__init__.py +++ b/src/bonsai/bonsai/bim/module/material/__init__.py @@ -56,6 +56,7 @@ classes = ( operator.RemoveMaterial, operator.RemoveMaterialSet, operator.RemoveProfile, + operator.RenameMaterial, operator.ReorderMaterialSetItem, operator.SelectByMaterial, operator.SelectMaterialInMaterialsUI, diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 6ec1ccf188..b2ab1cfa06 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -102,6 +102,26 @@ class EditMaterial(bpy.types.Operator, tool.Ifc.Operator): core.edit_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material)) +class RenameMaterial(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.rename_material" + bl_label = "Rename Material" + bl_description = "Rename an IfcMaterial" + bl_options = {"REGISTER", "UNDO"} + material: bpy.props.IntProperty() + name: bpy.props.StringProperty(name="Name") + + def invoke(self, context, event): + material = tool.Ifc.get().by_id(self.material) + self.name = material.Name or "" + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "name") + + def _execute(self, context): + core.rename_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material), name=self.name) + + class DisableEditingMaterial(bpy.types.Operator): bl_idname = "bim.disable_editing_material" bl_label = "Disable Editing Material" diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 5e79cba3aa..829e0ea25c 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -41,6 +41,7 @@ import bonsai.bim.helper import bonsai.tool as tool from bonsai.bim.ifc import is_cache_locked_by_other_process from bonsai.bim.module.bsdd.prop import BIMBSDDProperties, BSDDProperty +from bonsai.bim.module.material.operator import SelectByMaterial from bonsai.bim.module.model import prop as _model_prop from bonsai.bim.module.model import ui as _model_ui from bonsai.bim.module.pset.prop import IfcProperty @@ -1854,6 +1855,21 @@ def draw_statusbar(self, context): def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None: # https://blender.stackexchange.com/a/275555/86891 + + # Context menu for material name buttons (e.g. `bim.select_by_material`), + # offering a quick "Rename Material" entry instead of having to look up + # the material in the scene Materials panel to rename it. + button_operator = getattr(context, "button_operator", None) + if button_operator is not None and button_operator.bl_rna.identifier == SelectByMaterial.bl_rna.identifier: + ifc_file = tool.Ifc.get() + material = ifc_file.by_id(button_operator.material) if ifc_file else None + if material is not None and material.is_a("IfcMaterial"): + assert self.layout + self.layout.separator() + op = self.layout.operator("bim.rename_material", text="Rename Material", icon="GREASEPENCIL") + op.material = material.id() + return + if ( not hasattr(context, "button_pointer") or not hasattr(context, "button_prop") diff --git a/src/bonsai/bonsai/core/material.py b/src/bonsai/bonsai/core/material.py index 0a08f5e0bf..00dbd8e178 100644 --- a/src/bonsai/bonsai/core/material.py +++ b/src/bonsai/bonsai/core/material.py @@ -107,6 +107,14 @@ def disable_editing_material(material_tool: type[tool.Material]) -> None: material_tool.disable_editing_material() +def rename_material( + ifc: type[tool.Ifc], material_tool: type[tool.Material], material: ifcopenshell.entity_instance, name: str +) -> None: + ifc.run("material.edit_material", material=material, attributes={"Name": name}) + if material_tool.is_editing_materials(): + material_tool.import_material_definitions(material_tool.get_active_material_type()) + + def assign_material( ifc: type[tool.Ifc], material_tool: type[tool.Material], From 16b1b4e7b175a314d0324613127e47f0c231c052 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Tue, 21 Jul 2026 17:11:56 +0300 Subject: [PATCH 2/3] Bonsai: refresh the UI after renaming a material theoryshaw tested #8843 and asked for the new name to show up right away instead of needing a manual refresh. The Object Material panel and the scene Materials list both already re-read live IFC data on their next draw (tool.Ifc.Operator purges those caches after every IFC-mutating operator), so the button text was correct on the next redraw. What was missing was the redraw itself: the material name is a plain button label, not an RNA property Blender tracks, so nothing told the Properties editor to repaint after the rename dialog closed. Tag every area for redraw once the rename completes, the same pattern used elsewhere in Bonsai for popup-triggered edits that need an immediate repaint. Also adds core-layer test coverage for rename_material, which had none. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/bim/module/material/operator.py | 3 +++ src/bonsai/test/core/test_material.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index b2ab1cfa06..d744ea5780 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -120,6 +120,9 @@ class RenameMaterial(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.rename_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material), name=self.name) + if screen := context.screen: + for area in screen.areas: + area.tag_redraw() class DisableEditingMaterial(bpy.types.Operator): diff --git a/src/bonsai/test/core/test_material.py b/src/bonsai/test/core/test_material.py index a2cbf8c433..ba83e9884d 100644 --- a/src/bonsai/test/core/test_material.py +++ b/src/bonsai/test/core/test_material.py @@ -93,6 +93,20 @@ class TestRemoveMaterialSet: subject.remove_material_set(ifc, material, material="material") +class TestRenameMaterial: + def test_renaming_a_material(self, ifc, material): + ifc.run("material.edit_material", material="material", attributes={"Name": "name"}).should_be_called() + material.is_editing_materials().should_be_called().will_return(False) + subject.rename_material(ifc, material, material="material", name="name") + + def test_renaming_a_material_and_reloading_imported_materials(self, ifc, material): + ifc.run("material.edit_material", material="material", attributes={"Name": "name"}).should_be_called() + material.is_editing_materials().should_be_called().will_return(True) + material.get_active_material_type().should_be_called().will_return("material_type") + material.import_material_definitions("material_type").should_be_called() + subject.rename_material(ifc, material, material="material", name="name") + + class TestLoadMaterials: def test_run(self, material): material.import_material_definitions("material_type").should_be_called() From 7ab0628c54fa4ce3f746efd691510d9998aabaa7 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Tue, 21 Jul 2026 21:08:21 +0300 Subject: [PATCH 3/3] Bonsai: refresh material data unconditionally instead of forcing a redraw falken10vdl reviewed 16b1b4e7b1 on #8843 and pointed out that tagging every area for redraw was overkill. The actual problem was that the Object Material panel and the scene Materials list read from plain python caches (ObjectMaterialData and MaterialsData) that only get invalidated when the Materials editing UI list is reloaded, which never happens while you are not in editing mode. The redraw itself was never the issue, closing the rename dialog already triggers one. Removed the tag_redraw loop from RenameMaterial and instead call the existing bonsai.bim.module.material.data.refresh() function from core.rename_material, unconditionally, through a new tool.Material.refresh() method. This is the same invalidate-on-next-load mechanism already used by every other module's Data classes, just wired up for this operator too, instead of introducing a new one. Also updates the core tests to prescribe the new unconditional refresh() call, and adds tool-layer coverage for tool.Material.refresh(). Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/bim/module/material/operator.py | 3 --- src/bonsai/bonsai/core/material.py | 1 + src/bonsai/bonsai/core/tool.py | 1 + src/bonsai/bonsai/tool/material.py | 6 ++++++ src/bonsai/test/core/test_material.py | 2 ++ src/bonsai/test/tool/test_material.py | 11 +++++++++++ 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index d744ea5780..b2ab1cfa06 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -120,9 +120,6 @@ class RenameMaterial(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.rename_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material), name=self.name) - if screen := context.screen: - for area in screen.areas: - area.tag_redraw() class DisableEditingMaterial(bpy.types.Operator): diff --git a/src/bonsai/bonsai/core/material.py b/src/bonsai/bonsai/core/material.py index 00dbd8e178..96c624de64 100644 --- a/src/bonsai/bonsai/core/material.py +++ b/src/bonsai/bonsai/core/material.py @@ -113,6 +113,7 @@ def rename_material( ifc.run("material.edit_material", material=material, attributes={"Name": name}) if material_tool.is_editing_materials(): material_tool.import_material_definitions(material_tool.get_active_material_type()) + material_tool.refresh() def assign_material( diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 51120ad088..8a77d36661 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -667,6 +667,7 @@ class Material: def is_editing_materials(cls): pass def is_material_used_in_sets(cls, material): pass def load_material_attributes(cls, material): pass + def refresh(cls): pass def replace_material_with_material_profile(cls, element): pass def update_elements_using_material(cls, material): pass diff --git a/src/bonsai/bonsai/tool/material.py b/src/bonsai/bonsai/tool/material.py index 4c55123a9c..300df8d4ce 100644 --- a/src/bonsai/bonsai/tool/material.py +++ b/src/bonsai/bonsai/tool/material.py @@ -146,6 +146,12 @@ class Material(bonsai.core.tool.Material): MaterialsData.data["material_styles_data"] = MaterialsData.material_styles_data() + @classmethod + def refresh(cls) -> None: + from bonsai.bim.module.material.data import refresh as refresh_material_data + + refresh_material_data() + @classmethod def is_editing_materials(cls) -> bool: props = tool.Material.get_material_props() diff --git a/src/bonsai/test/core/test_material.py b/src/bonsai/test/core/test_material.py index ba83e9884d..28fe0ba326 100644 --- a/src/bonsai/test/core/test_material.py +++ b/src/bonsai/test/core/test_material.py @@ -97,6 +97,7 @@ class TestRenameMaterial: def test_renaming_a_material(self, ifc, material): ifc.run("material.edit_material", material="material", attributes={"Name": "name"}).should_be_called() material.is_editing_materials().should_be_called().will_return(False) + material.refresh().should_be_called() subject.rename_material(ifc, material, material="material", name="name") def test_renaming_a_material_and_reloading_imported_materials(self, ifc, material): @@ -104,6 +105,7 @@ class TestRenameMaterial: material.is_editing_materials().should_be_called().will_return(True) material.get_active_material_type().should_be_called().will_return("material_type") material.import_material_definitions("material_type").should_be_called() + material.refresh().should_be_called() subject.rename_material(ifc, material, material="material", name="name") diff --git a/src/bonsai/test/tool/test_material.py b/src/bonsai/test/tool/test_material.py index 870417a254..9cee7a5962 100644 --- a/src/bonsai/test/tool/test_material.py +++ b/src/bonsai/test/tool/test_material.py @@ -139,6 +139,17 @@ class TestImportMaterialDefinitions(NewFile): assert props.materials[0].total_elements == 0 +class TestRefresh(NewFile): + def test_run(self): + from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData + + MaterialsData.is_loaded = True + ObjectMaterialData.is_loaded = True + subject.refresh() + assert MaterialsData.is_loaded is False + assert ObjectMaterialData.is_loaded is False + + class TestIsEditingMaterials(NewFile): def test_run(self): props = tool.Material.get_material_props()