Merge pull request #6638 from falken10vdl/USER_UI_CUSTOMIZATION

User UI customization
This can be enabled in Extras at user discretion.
This commit is contained in:
falken10vdl
2025-12-19 09:16:09 +01:00
committed by GitHub
9 changed files with 1005 additions and 117 deletions
+20
View File
@@ -115,11 +115,13 @@ classes = [
operator.EditBlenderCollection, operator.EditBlenderCollection,
operator.FileAssociate, operator.FileAssociate,
operator.FileUnassociate, operator.FileUnassociate,
operator.LoadBlendMetadataAndIFC,
operator.OpenPath, operator.OpenPath,
operator.OpenUpstream, operator.OpenUpstream,
operator.OpenUri, operator.OpenUri,
operator.ReloadIfcFile, operator.ReloadIfcFile,
operator.RevertClippingPlaneCut, operator.RevertClippingPlaneCut,
operator.SaveBlendMetadataFile,
operator.SelectDir, operator.SelectDir,
operator.SelectIfcFile, operator.SelectIfcFile,
operator.SelectURIAttribute, operator.SelectURIAttribute,
@@ -129,12 +131,21 @@ classes = [
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, operator.BIM_OT_attribute_search_values,
operator.BIM_UL_tab_panels,
operator.BIM_OT_toggle_panel_visibility,
operator.BIM_OT_bookmark_panel,
operator.BIM_OT_manage_tab_panels,
operator.BIM_OT_manage_tab_visibility,
operator.BIM_OT_toggle_tab_visibility,
operator.BIM_OT_reset_ui_layout,
prop.ObjProperty, prop.ObjProperty,
prop.MultipleFileSelect, prop.MultipleFileSelect,
prop.Attribute, prop.Attribute,
prop.ISODuration, prop.ISODuration,
prop.BIMAreaProperties, prop.BIMAreaProperties,
prop.BIMTabProperties, prop.BIMTabProperties,
prop.BIMTabVisibility, # Must be registered before BIMProperties
prop.BIMPanelProperties, # Must be registered before BIMProperties
prop.BIMProperties, prop.BIMProperties,
prop.IfcParameter, prop.IfcParameter,
prop.PsetQto, prop.PsetQto,
@@ -272,6 +283,7 @@ def register():
bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.Curve.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.Camera.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties) bpy.types.PointLight.BIMMeshProperties = bpy.props.PointerProperty(type=prop.BIMMeshProperties)
if hasattr(bpy.types, "UI_MT_button_context_menu"): if hasattr(bpy.types, "UI_MT_button_context_menu"):
bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu) bpy.types.UI_MT_button_context_menu.append(ui.draw_custom_context_menu)
bpy.types.STATUSBAR_HT_header.append(ui.draw_statusbar) bpy.types.STATUSBAR_HT_header.append(ui.draw_statusbar)
@@ -307,6 +319,10 @@ def register():
# RestrictedContext doesn't allow accessing scene attribute, postpone it for a bit. # RestrictedContext doesn't allow accessing scene attribute, postpone it for a bit.
bpy.app.timers.register(tool.Blender.setup_user_data_dir, first_interval=0.1) bpy.app.timers.register(tool.Blender.setup_user_data_dir, first_interval=0.1)
bpy.types.Scene.active_tab_name = bpy.props.StringProperty()
bpy.types.Scene.tab_panels = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)
bpy.types.Scene.active_tab_panel_index = bpy.props.IntProperty()
def unregister(): def unregister():
global icons global icons
@@ -348,3 +364,7 @@ def unregister():
tool.Blender.remove_scene_panel_override(panel) tool.Blender.remove_scene_panel_override(panel)
bpy.app.translations.unregister("bonsai") bpy.app.translations.unregister("bonsai")
del bpy.types.Scene.active_tab_name
del bpy.types.Scene.tab_panels
del bpy.types.Scene.active_tab_panel_index
+152 -2
View File
@@ -539,7 +539,9 @@ def draw_filter(
if preferences.chain_filter_with_set_operations: if preferences.chain_filter_with_set_operations:
show_mode_toggle = j > 0 show_mode_toggle = j > 0
else: else:
show_mode_toggle = preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 # PR 7315 mode show_mode_toggle = (
preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0
) # PR 7315 mode
if show_mode_toggle: if show_mode_toggle:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator( op = row.operator(
@@ -758,7 +760,9 @@ def draw_filter(
if preferences.chain_filter_with_set_operations: if preferences.chain_filter_with_set_operations:
show_mode_toggle = j > 0 show_mode_toggle = j > 0
else: else:
show_mode_toggle = preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 # PR 7315 mode show_mode_toggle = (
preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0
) # PR 7315 mode
if show_mode_toggle: if show_mode_toggle:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"} mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator( op = row.operator(
@@ -784,3 +788,149 @@ def draw_filter(
op.group_index = i op.group_index = i
op.index = j op.index = j
op.module = module op.module = module
# ============================================================================
# UI Panel Visibility Helpers
# ============================================================================
def get_tab_names():
from bonsai.bim.prop import get_tab
enum_items = get_tab(None, None)
# Exclude None separators and the BLENDER tab (not part of BIM tab system)
return [item[0] for item in enum_items if item is not None and item[0] != "BLENDER"]
def get_panel_tab_name(panel_class):
if hasattr(panel_class, "bim_tab_name"):
return panel_class.bim_tab_name
return "PROJECT" # Default fallback
def should_show_panel(panel_id, panel_tab_name, context):
if tool.Blender.is_tab(context, "BOOKMARK"):
return is_panel_bookmarked(panel_id) and get_panel_visibility(panel_id, "BOOKMARK")
if tool.Blender.is_tab(context, panel_tab_name):
return get_tab_visibility(panel_tab_name) and get_panel_visibility(panel_id, panel_tab_name)
return False
def get_tab_visibility(tab_name):
bim_props = tool.Blender.get_bim_props()
tab_vis = bim_props.tab_visibilities.get(tab_name)
return tab_vis.is_visible if tab_vis else True
def set_tab_visibility(tab_name, visible):
bim_props = tool.Blender.get_bim_props()
tab_vis = bim_props.tab_visibilities.get(tab_name)
if tab_vis:
tab_vis.is_visible = visible
else:
new_tab = bim_props.tab_visibilities.add()
new_tab.name = tab_name
new_tab.is_visible = visible
def get_panel_visibility(panel_id, current_tab=None):
panel_config = get_panel_config(panel_id)
if panel_config:
if current_tab == "BOOKMARK":
return panel_config.is_visible_in_bookmarks
else:
return panel_config.is_visible_in_tab
return True
def is_panel_bookmarked(panel_id):
panel_config = get_panel_config(panel_id)
if panel_config:
return panel_config.is_bookmarked
return False
def get_panel_config(panel_id, create_if_missing=False):
try:
bim_props = tool.Blender.get_bim_props()
except (AttributeError, AssertionError):
return None
for prop in bim_props.panel_properties:
if prop.name == panel_id:
return prop
if create_if_missing:
try:
prop = bim_props.panel_properties.add()
prop.name = panel_id
prop.is_visible_in_tab = True
prop.is_visible_in_bookmarks = True
prop.is_bookmarked = False
return prop
except AttributeError:
pass
return None
def get_all_tab_panels(force_refresh=False):
panels = {tab_name: [] for tab_name in get_tab_names() if tab_name != "BOOKMARK"}
panels["BOOKMARK"] = []
bim_props = tool.Blender.get_bim_props()
for prop in bim_props.panel_properties:
panel_class = getattr(bpy.types, prop.name, None)
if panel_class:
tab_name = get_panel_tab_name(panel_class)
if tab_name and tab_name != "BOOKMARK":
bl_label = getattr(panel_class, "bl_label", prop.name)
panels[tab_name].append({"bl_idname": prop.name, "bl_label": bl_label})
if prop.is_bookmarked:
panel_class = getattr(bpy.types, prop.name, None)
if panel_class:
bl_label = getattr(panel_class, "bl_label", prop.name)
panels["BOOKMARK"].append({"bl_idname": prop.name, "bl_label": bl_label})
if not panels["BOOKMARK"]:
panels["BOOKMARK"] = [{}]
return panels
def initialize_tab_visibilities():
bim_props = tool.Blender.get_bim_props()
if len(bim_props.tab_visibilities) > 0:
return
for tab_name in get_tab_names():
tab_vis = bim_props.tab_visibilities.add()
tab_vis.name = tab_name
tab_vis.is_visible = True
def initialize_panel_properties():
bim_props = tool.Blender.get_bim_props()
if len(bim_props.panel_properties) > 0:
return
for attr_name in dir(bpy.types):
if attr_name.startswith("BIM_PT_tab_"):
panel_class = getattr(bpy.types, attr_name)
if not hasattr(panel_class, "bl_idname"):
continue
panel_id = panel_class.bl_idname
prop = bim_props.panel_properties.add()
prop.name = panel_id
prop.is_visible_in_tab = True
prop.is_visible_in_bookmarks = True
prop.is_bookmarked = False
@@ -2093,5 +2093,6 @@ class DecorationsHandler:
if not DecoratorData.is_loaded: if not DecoratorData.is_loaded:
DecoratorData.load(self) DecoratorData.load(self)
for obj, decorator in DecoratorData.data["object_decorators"]: object_decorators = DecoratorData.data.get("object_decorators", [])
for obj, decorator in object_decorators:
decorator.decorate(context, obj) decorator.decorate(context, obj)
@@ -1056,6 +1056,20 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
return tooltip return tooltip
def execute(self, context): def execute(self, context):
if (
tool.Blender.get_addon_preferences().save_metadata_blend_file
and self.should_start_fresh_session
and not self.is_advanced
):
filepath = self.get_filepath()
metadata_path = Path(str(filepath) + ".metadata.blend")
if metadata_path.exists() and metadata_path.is_file():
try:
bpy.ops.bim.load_blend_metadata_and_ifc(filepath=filepath)
return {"FINISHED"}
except Exception as e:
self.report({"WARNING"}, f"Failed to load metadata file, using regular load: {e}")
@persistent @persistent
def load_handler(*args): def load_handler(*args):
bpy.app.handlers.load_post.remove(load_handler) bpy.app.handlers.load_post.remove(load_handler)
@@ -1744,15 +1758,28 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
bim_props = tool.Blender.get_bim_props() bim_props = tool.Blender.get_bim_props()
if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"):
tool.Ifc.set_path(output_file) tool.Ifc.set_path(output_file)
save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath)
if save_blend_file:
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
bim_props.is_dirty = False bim_props.is_dirty = False
if tool.Blender.get_addon_preferences().save_metadata_blend_file:
try:
bpy.ops.bim.save_blend_metadata_file()
blendmetadata_path = output_file + ".metadata.blend"
self.report(
{"INFO"},
f'IFC Project "{os.path.basename(output_file)}" And Metadata File Saved to: {os.path.basename(blendmetadata_path)}',
)
except Exception as e:
self.report({"ERROR"}, f"Failed to save blend metadata file: {e}")
else:
save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath)
if save_blend_file:
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath)
self.report(
{"INFO"},
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
)
bonsai.bim.handler.refresh_ui_data() bonsai.bim.handler.refresh_ui_data()
self.report(
{"INFO"},
f'IFC Project "{os.path.basename(output_file)}" {"" if not save_blend_file else "And Current Blend File Are"} Saved',
)
@classmethod @classmethod
def description(cls, context, properties): def description(cls, context, properties):
@@ -331,6 +331,13 @@ class BIM_PT_project(Panel):
col.prop(props, "ifc_file", text="") col.prop(props, "ifc_file", text="")
row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="") row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="")
if tool.Blender.get_addon_preferences().save_metadata_blend_file:
row = self.layout.row(align=True)
col = row.column()
col.enabled = False
metadata_filename = os.path.basename(props.ifc_file) + ".metadata.blend"
col.label(text=f"Saving session data to: {metadata_filename}")
class BIM_PT_new_project_wizard(Panel): class BIM_PT_new_project_wizard(Panel):
bl_label = "New Project Wizard" bl_label = "New Project Wizard"
+394
View File
@@ -33,6 +33,15 @@ import bonsai.bim
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.bim.handler import bonsai.bim.handler
from enum import Enum from enum import Enum
from bonsai.bim.helper import (
get_all_tab_panels,
get_tab_visibility,
set_tab_visibility,
get_tab_names,
get_panel_config,
initialize_panel_properties,
initialize_tab_visibilities,
)
from bpy_extras.io_utils import ImportHelper from bpy_extras.io_utils import ImportHelper
from bonsai.bim import import_ifc from bonsai.bim import import_ifc
from bonsai.bim.prop import StrProperty from bonsai.bim.prop import StrProperty
@@ -236,6 +245,150 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
return ImportHelper.invoke(self, context, event) return ImportHelper.invoke(self, context, event)
class SaveBlendMetadataFile(bpy.types.Operator):
bl_idname = "bim.save_blend_metadata_file"
bl_label = "Save Blend Metadata File"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
"""
Save the current blend file as a metadata-only file (no geometry), preserving settings, window arrangement, geometry nodes, etc.
"""
props = tool.Blender.get_bim_props()
ifc_file = getattr(props, "ifc_file", None)
if not ifc_file:
self.report({"WARNING"}, "No IFC file path set.")
return {"CANCELLED"}
blendmetadata_path = ifc_file + ".metadata.blend"
# Save a temporary copy of the current blend file
temp_path = bpy.path.abspath("//__temp_blendmetadata.blend")
bpy.ops.wm.save_as_mainfile(filepath=temp_path, copy=True)
cleanup_script = f"""
import bpy
def remove_ifc_collections():
\"\"\"Remove all collections that start with 'IfcProject' and their contents.\"\"\"
collections_to_remove = []
# Find all IfcProject collections
for collection in bpy.data.collections:
if collection.name.startswith('IfcProject'):
collections_to_remove.append(collection)
# First, use bim.override_outliner_delete to properly remove IFC data
for collection in collections_to_remove:
# Collect all objects in the collection and its children
objects_to_delete = []
def collect_objects(col):
for obj in col.objects:
if obj not in objects_to_delete:
objects_to_delete.append(obj)
for child in col.children:
collect_objects(child)
collect_objects(collection)
# Use bim.override_outliner_delete for each object to clean up IFC data
for obj in objects_to_delete:
# Select only this object
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
# Try to use Bonsai's delete operator which cleans up IFC data
try:
bpy.ops.bim.override_outliner_delete()
except:
# If that fails, use standard delete
bpy.data.objects.remove(obj, do_unlink=True)
# Remove any remaining child collections
for child in list(collection.children):
try:
bpy.data.collections.remove(child, do_unlink=True)
except:
pass
# Remove the IfcProject collections themselves
for collection in collections_to_remove:
try:
bpy.data.collections.remove(collection, do_unlink=True)
except:
pass
# Remove IFC-related data
remove_ifc_collections()
# Note: Scene-level BIM properties are saved with the metadata.blend file but will be
# overwritten when the IFC is loaded. Screen-level properties (BIMTabProperties,
# BIMPanelProperties, BIMAreaProperties) preserve UI customization settings.
# Save the metadata file
bpy.ops.wm.save_as_mainfile(filepath=r'{blendmetadata_path}')
"""
import tempfile
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as script_file:
script_file.write(cleanup_script)
script_path = script_file.name
blender_exe = bpy.app.binary_path
import subprocess
result = subprocess.run(
[blender_exe, temp_path, "--background", "--python", script_path], capture_output=True, text=True
)
# Print the output from the background process (includes debug info)
if result.stdout:
print("\n=== Background Blender Output ===")
print(result.stdout)
if result.stderr:
print("\n=== Background Blender Errors ===")
print(result.stderr)
try:
os.remove(temp_path)
os.remove(script_path)
except Exception:
pass
return {"FINISHED"}
class LoadBlendMetadataAndIFC(bpy.types.Operator):
bl_idname = "bim.load_blend_metadata_and_ifc"
bl_label = "Load Blend Metadata and IFC"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(name="IFC File Path", default="")
def execute(self, context):
ifc_file = self.filepath
if not ifc_file:
props = tool.Blender.get_bim_props()
ifc_file = getattr(props, "ifc_file", None)
if not ifc_file:
self.report({"WARNING"}, "No IFC file path set.")
return {"CANCELLED"}
metadata_path = ifc_file + ".metadata.blend"
# Open the metadata blend file
bpy.ops.wm.open_mainfile(filepath=metadata_path)
# After loading metadata, clear blend warning (no geometry loaded yet)
props = tool.Blender.get_bim_props()
props.has_blend_warning = False
# Load the IFC file into the current session (preserve layout)
bpy.ops.bim.load_project(filepath=ifc_file, should_start_fresh_session=False)
self.report({"INFO"}, f"Loaded metadata and IFC: {metadata_path}, {ifc_file}")
return {"FINISHED"}
# TODO: Unused operator. # TODO: Unused operator.
# Is there a need for this or 'DIR_PATH' propety subtype does almost the same, # Is there a need for this or 'DIR_PATH' propety subtype does almost the same,
# but also has alt+click? # but also has alt+click?
@@ -1605,3 +1758,244 @@ class BIM_OT_attribute_remove_subitem(bpy.types.Operator):
attr.is_null = True attr.is_null = True
return {"FINISHED"} return {"FINISHED"}
class BIM_UL_tab_panels(bpy.types.UIList):
"""UIList for Tab Panels"""
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
row = layout.row(align=True)
row.label(text=item["bl_label"])
row.operator(
"bim.toggle_panel_visibility",
text="",
icon="HIDE_OFF" if item.get("visible", True) else "HIDE_ON",
).action = f"TOGGLE_VISIBILITY_{item.name}"
row.operator(
"bim.bookmark_panel",
text="",
icon="SOLO_ON" if item.get("bookmarked", False) else "SOLO_OFF",
).action = f"BOOKMARK_{item.name}"
class BIM_OT_toggle_panel_visibility(bpy.types.Operator):
"""Toggle Panel Visibility"""
bl_idname = "bim.toggle_panel_visibility"
bl_label = "Toggle Panel Visibility"
bl_options = {"REGISTER", "UNDO"}
action: bpy.props.StringProperty()
def execute(self, context):
panel_name = self.action.replace("TOGGLE_VISIBILITY_", "")
active_tab = getattr(context.scene, "active_tab_name", None) or getattr(
tool.Blender.get_bim_props(), "tab", None
)
is_bookmark_tab = active_tab == "BOOKMARK"
panel_config = get_panel_config(panel_name, create_if_missing=True)
if panel_config:
if is_bookmark_tab:
panel_config.is_visible_in_bookmarks = not panel_config.is_visible_in_bookmarks
new_value = panel_config.is_visible_in_bookmarks
else:
panel_config.is_visible_in_tab = not panel_config.is_visible_in_tab
new_value = panel_config.is_visible_in_tab
for item in context.scene.tab_panels:
if item.name == panel_name:
item["visible"] = new_value
break
for area in bpy.context.window.screen.areas:
if area.type == "PROPERTIES":
area.tag_redraw()
tab_context = "Bookmarks" if is_bookmark_tab else "Tab"
self.report({"INFO"}, f"Toggled visibility for {panel_name} in {tab_context}.")
return {"FINISHED"}
class BIM_OT_bookmark_panel(bpy.types.Operator):
"""Bookmark Panel"""
bl_idname = "bim.bookmark_panel"
bl_label = "Bookmark Panel"
bl_options = {"REGISTER", "UNDO"}
action: bpy.props.StringProperty()
def execute(self, context):
panel_name = self.action.replace("BOOKMARK_", "")
panel_config = get_panel_config(panel_name, create_if_missing=True)
if panel_config:
panel_config.is_bookmarked = not panel_config.is_bookmarked
for item in context.scene.tab_panels:
if item.name == panel_name:
item["bookmarked"] = panel_config.is_bookmarked
break
for area in bpy.context.window.screen.areas:
if area.type == "PROPERTIES":
area.tag_redraw()
self.report({"INFO"}, f"Toggled bookmark for {panel_name}.")
return {"FINISHED"}
class BIM_OT_manage_tab_panels(bpy.types.Operator):
"""Manage Tab Panels"""
bl_idname = "bim.manage_tab_panels"
bl_label = "Manage Tab Panels"
bl_options = {"REGISTER", "UNDO"}
tab_name: bpy.props.StringProperty()
def invoke(self, context, event):
context.scene.active_tab_name = self.tab_name
context.scene.tab_panels.clear()
initialize_tab_visibilities()
initialize_panel_properties()
all_panels = get_all_tab_panels(force_refresh=True)
for panel_data in all_panels.get(self.tab_name, []):
panel_name = panel_data.get("bl_idname", "")
panel_label = panel_data.get("bl_label", "")
if not panel_name or not panel_label:
continue
item = context.scene.tab_panels.add()
item.name = panel_name
item["bl_label"] = panel_label
panel_config = get_panel_config(panel_name, create_if_missing=True)
if panel_config:
if self.tab_name == "BOOKMARK":
item["visible"] = panel_config.is_visible_in_bookmarks
else:
item["visible"] = panel_config.is_visible_in_tab
item["bookmarked"] = panel_config.is_bookmarked
else:
item["visible"] = True
item["bookmarked"] = False
return context.window_manager.invoke_popup(self)
def draw(self, context):
layout = self.layout
layout.label(text=f"Manage Panels for {self.tab_name} Tab")
row = layout.row()
row.template_list("BIM_UL_tab_panels", "", context.scene, "tab_panels", context.scene, "active_tab_panel_index")
def execute(self, context):
for item in context.scene.tab_panels:
panel_config = get_panel_config(item.name, create_if_missing=True)
if panel_config:
if self.tab_name == "BOOKMARK":
panel_config.is_visible_in_bookmarks = item["visible"]
else:
panel_config.is_visible_in_tab = item["visible"]
panel_config.is_bookmarked = item["bookmarked"]
self.report({"INFO"}, f"Panels for {self.tab_name} managed successfully.")
return {"FINISHED"}
class BIM_OT_manage_tab_visibility(bpy.types.Operator):
"""Manage Tab Visibility"""
bl_idname = "bim.manage_tab_visibility"
bl_label = "Manage Tab Visibility"
bl_options = {"REGISTER", "UNDO"}
def draw(self, context):
layout = self.layout
row = layout.row()
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.reset_ui_layout", icon="FILE_REFRESH", text="")
row = layout.row()
row = self.layout.row(align=True)
row.alignment = "CENTER"
for tab_name in get_tab_names():
row = layout.row()
row.label(text=tab_name)
is_visible = get_tab_visibility(tab_name)
icon = "HIDE_OFF" if is_visible else "HIDE_ON"
op = row.operator("bim.toggle_tab_visibility", text="", icon=icon)
op.tab_name = tab_name
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self)
class BIM_OT_toggle_tab_visibility(bpy.types.Operator):
"""Toggle Tab Visibility"""
bl_idname = "bim.toggle_tab_visibility"
bl_label = "Toggle Tab Visibility"
bl_options = {"REGISTER", "UNDO"}
tab_name: bpy.props.StringProperty()
def execute(self, context):
if self.tab_name in get_tab_names():
current_visibility = get_tab_visibility(self.tab_name)
set_tab_visibility(self.tab_name, not current_visibility)
for area in bpy.context.window.screen.areas:
if area.type == "PROPERTIES":
area.tag_redraw()
self.report({"INFO"}, f"Toggled visibility for {self.tab_name}.")
return {"FINISHED"}
class BIM_OT_reset_ui_layout(bpy.types.Operator):
"""Reset UI Layout to Default"""
bl_idname = "bim.reset_ui_layout"
bl_label = "Reset UI Layout"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for tab_name in get_tab_names():
set_tab_visibility(tab_name, True)
get_all_tab_panels()["BOOKMARK"] = [{}]
for tab_name, panels in get_all_tab_panels().items():
for panel in panels:
panel_name = panel.get("bl_idname", "")
if not panel_name:
continue
show_prop_name = f"show_{panel_name.lower()}"
if hasattr(context.scene, show_prop_name):
setattr(context.scene, show_prop_name, True)
bookmark_prop_name = f"bookmark_{panel_name.lower()}"
if hasattr(context.scene, bookmark_prop_name):
setattr(context.scene, bookmark_prop_name, False)
for area in bpy.context.window.screen.areas:
if area.type == "PROPERTIES":
area.tag_redraw()
self.report({"INFO"}, "UI layout reset to default.")
return {"FINISHED"}
+27 -1
View File
@@ -493,8 +493,9 @@ def get_tab(
("SCHEDULING", "Costing and Scheduling", "", "NLA", 6), ("SCHEDULING", "Costing and Scheduling", "", "NLA", 6),
("FM", "Facility Management", "", "PACKAGE", 7), ("FM", "Facility Management", "", "PACKAGE", 7),
("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8), ("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8),
("BOOKMARK", "Bookmark", "", "SOLO_ON", 9),
None, None,
("BLENDER", "Blender Properties", "", "BLENDER", 9), ("BLENDER", "Blender Properties", "", "BLENDER", 10),
] ]
return get_tab.enum_items return get_tab.enum_items
@@ -529,6 +530,26 @@ class BIMTabProperties(PropertyGroup):
inactive_tab: bool inactive_tab: bool
class BIMTabVisibility(PropertyGroup):
name: StringProperty(name="Tab Name")
is_visible: BoolProperty(name="Is Visible", default=True)
if TYPE_CHECKING:
name: str
is_visible: bool
class BIMPanelProperties(PropertyGroup):
is_visible_in_tab: BoolProperty(name="Is Visible in Tab", default=True)
is_visible_in_bookmarks: BoolProperty(name="Is Visible in Bookmarks", default=True)
is_bookmarked: BoolProperty(name="Is Bookmarked", default=False)
if TYPE_CHECKING:
is_visible_in_tab: bool
is_visible_in_bookmarks: bool
is_bookmarked: bool
class BIMProperties(PropertyGroup): class BIMProperties(PropertyGroup):
is_dirty: BoolProperty(name="Is Dirty", default=False) is_dirty: BoolProperty(name="Is Dirty", default=False)
schema_dir: StringProperty( schema_dir: StringProperty(
@@ -613,6 +634,9 @@ class BIMProperties(PropertyGroup):
name="Time Unit", name="Time Unit",
default="HOUR", default="HOUR",
) )
tab_visibilities: CollectionProperty(type=BIMTabVisibility, name="Tab Visibilities")
panel_properties: CollectionProperty(type=BIMPanelProperties, name="Panel Properties")
if TYPE_CHECKING: if TYPE_CHECKING:
is_dirty: bool is_dirty: bool
schema_dir: str schema_dir: str
@@ -627,6 +651,8 @@ class BIMProperties(PropertyGroup):
volume_unit: str volume_unit: str
mass_unit: str mass_unit: str
time_unit: str time_unit: str
tab_visibilities: bpy.types.bpy_prop_collection[BIMTabVisibility]
panel_properties: bpy.types.bpy_prop_collection[BIMPanelProperties]
class IfcParameter(PropertyGroup): class IfcParameter(PropertyGroup):
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -2123,7 +2123,15 @@ class Drawing(bonsai.core.tool.Drawing):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2]) value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
value = '"' + str(value).replace('"', '\\"') + '"' value = '"' + str(value).replace('"', '\\"') + '"'
command = command.replace(variable, value) command = command.replace(variable, value)
text = text.replace(original_command, ifcopenshell.util.selector.format(command[2:-2])) # Defensive: skip if command[2:-2] is None or 'None'
command_content = command[2:-2]
if command_content is None or str(command_content).strip().lower() == 'none':
text = text.replace(original_command, "")
else:
try:
text = text.replace(original_command, ifcopenshell.util.selector.format(command_content))
except Exception:
text = text.replace(original_command, "")
for variable in re.findall("{{.*?}}", text): for variable in re.findall("{{.*?}}", text):
value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2]) value = ifcopenshell.util.selector.get_element_value(product, variable[2:-2])
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):