diff --git a/src/blenderbim/blenderbim/bim/module/owner/__init__.py b/src/blenderbim/blenderbim/bim/module/owner/__init__.py index a097f0fb07..592e0e0167 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/owner/__init__.py @@ -2,6 +2,7 @@ import bpy from . import ui, prop, operator classes = ( + operator.AddOrRemoveElementFromCollection, operator.EnableEditingPerson, operator.DisableEditingPerson, operator.AddPerson, diff --git a/src/blenderbim/blenderbim/bim/module/owner/operator.py b/src/blenderbim/blenderbim/bim/module/owner/operator.py index 20e4affca3..744822911a 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/operator.py +++ b/src/blenderbim/blenderbim/bim/module/owner/operator.py @@ -1,10 +1,49 @@ import bpy -import json import ifcopenshell.api from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.owner.data import Data +def flatten_collection(collection): + return [v.name for v in collection] if collection else None + + +def populate_collection(collection, collection_data): + collection.clear() + if collection_data: + for value in collection_data: + collection.add().name = value + else: + collection.add() + + +class AddOrRemoveElementFromCollection(bpy.types.Operator): + bl_idname = "bim.add_or_remove_element_from_collection" + bl_label = "Add or Remove Element From Collection" + bl_options = {"REGISTER", "UNDO"} + operation : bpy.props.EnumProperty( + items=( + ("+", 'Add', "Add item to collection"), + ("-", 'Remove', "Remove item from collection") + ), + default="+", + ) + collection_path : bpy.props.StringProperty() + selected_item_idx : bpy.props.IntProperty(default=-1) + + def execute(self, context): + # Ugly but I hate using eval() + collection = context.scene + for attr in self.collection_path.split("."): + if hasattr(collection, attr): + collection = getattr(collection, attr) + if self.operation == "+" and hasattr(collection, "add"): + collection.add() + elif hasattr(collection, "remove") and 0 <= self.selected_item_idx < len(collection): + collection.remove(self.selected_item_idx) + return {"FINISHED"} + + class EnableEditingPerson(bpy.types.Operator): bl_idname = "bim.enable_editing_person" bl_label = "Enable Editing Person" @@ -17,12 +56,13 @@ class EnableEditingPerson(bpy.types.Operator): props.active_person_id = self.person_id data = Data.people[self.person_id] name = data["Id"] if self.file.schema == "IFC2X3" else data["Identification"] - props.person.name = name or "" - props.person.family_name = data["FamilyName"] or "" - props.person.given_name = data["GivenName"] or "" - props.person.middle_names = json.dumps(data["MiddleNames"]) if data["MiddleNames"] else "" - props.person.prefix_titles = json.dumps(data["PrefixTitles"]) if data["PrefixTitles"] else "" - props.person.suffix_titles = json.dumps(data["SuffixTitles"]) if data["SuffixTitles"] else "" + person = props.person + person.name = name or "" + person.family_name = data["FamilyName"] or "" + person.given_name = data["GivenName"] or "" + populate_collection(person.middle_names, data.get("MiddleNames", None)) + populate_collection(person.prefix_titles, data.get("PrefixTitles", None)) + populate_collection(person.suffix_titles, data.get("SuffixTitles", None)) return {"FINISHED"} @@ -61,13 +101,14 @@ class EditPerson(bpy.types.Operator): def _execute(self, context): self.file = IfcStore.get_file() props = context.scene.BIMOwnerProperties + person = props.person attributes = { - "Identification": props.person.name or None, - "FamilyName": props.person.family_name or None, - "GivenName": props.person.given_name or None, - "MiddleNames": json.loads(props.person.middle_names) if props.person.middle_names else None, - "PrefixTitles": json.loads(props.person.prefix_titles) if props.person.prefix_titles else None, - "SuffixTitles": json.loads(props.person.suffix_titles) if props.person.suffix_titles else None, + "Identification": person.name or None, + "FamilyName": person.family_name or None, + "GivenName": person.given_name or None, + "MiddleNames": flatten_collection(person.middle_names), + "PrefixTitles": flatten_collection(person.prefix_titles), + "SuffixTitles": flatten_collection(person.suffix_titles), } if self.file.schema == "IFC2X3": attributes["Id"] = attributes["Identification"] @@ -215,29 +256,28 @@ class EnableEditingAddress(bpy.types.Operator): props = context.scene.BIMOwnerProperties props.active_address_id = self.address_id data = Data.addresses[self.address_id] - props.address.name = data["type"] - props.address.purpose = data["Purpose"] or "None" - props.address.description = data["Description"] or "" - props.address.user_defined_purpose = data["UserDefinedPurpose"] or "" + address = props.address + address.name = data["type"] + address.purpose = data["Purpose"] or "None" + address.description = data["Description"] or "" + address.user_defined_purpose = data["UserDefinedPurpose"] or "" - if data["type"] == "IfcTelecomAddress": - props.address.telephone_numbers = json.dumps(data["TelephoneNumbers"]) if data["TelephoneNumbers"] else "" - props.address.facsimile_numbers = json.dumps(data["FacsimileNumbers"]) if data["FacsimileNumbers"] else "" - props.address.pager_number = data["PagerNumber"] or "" - props.address.electronic_mail_addresses = ( - json.dumps(data["ElectronicMailAddresses"]) if data["ElectronicMailAddresses"] else "" - ) - props.address.www_home_page_url = data["WWWHomePageURL"] or "" + if data["type"] == "IfcTelecomAddress": + populate_collection(address.telephone_numbers, data.get("TelephoneNumbers", None)) + populate_collection(address.facsimile_numbers, data.get("FacsimileNumbers", None)) + address.pager_number = data["PagerNumber"] or "" + populate_collection(address.electronic_mail_addresses, data.get("ElectronicMailAddresses", None)) + address.www_home_page_url = data["WWWHomePageURL"] or "" if self.file.schema != "IFC2X3": - props.address.messaging_ids = json.dumps(data["MessagingIDs"]) if data["MessagingIDs"] else "" + populate_collection(address.messaging_ids, data.get("MessagingIDs", None)) elif data["type"] == "IfcPostalAddress": - props.address.internal_location = data["InternalLocation"] or "" - props.address.address_lines = json.dumps(data["AddressLines"]) if data["AddressLines"] else "" - props.address.postal_box = data["PostalBox"] or "" - props.address.town = data["Town"] or "" - props.address.region = data["Region"] or "" - props.address.postal_code = data["PostalCode"] or "" - props.address.country = data["Country"] or "" + address.internal_location = data["InternalLocation"] or "" + populate_collection(address.address_lines, data.get("AddressLines", None)) + address.postal_box = data["PostalBox"] or "" + address.town = data["Town"] or "" + address.region = data["Region"] or "" + address.postal_code = data["PostalCode"] or "" + address.country = data["Country"] or "" return {"FINISHED"} @@ -273,18 +313,12 @@ class EditAddress(bpy.types.Operator): if address.is_a("IfcTelecomAddress"): attributes.update( { - "TelephoneNumbers": json.loads(props.address.telephone_numbers) - if props.address.telephone_numbers - else None, - "FacsimileNumbers": json.loads(props.address.facsimile_numbers) - if props.address.facsimile_numbers - else None, + "TelephoneNumbers": flatten_collection(props.address.telephone_numbers), + "FacsimileNumbers": flatten_collection(props.address.facsimile_numbers), "PagerNumber": props.address.pager_number or None, - "ElectronicMailAddresses": json.loads(props.address.electronic_mail_addresses) - if props.address.electronic_mail_addresses - else None, + "ElectronicMailAddresses": flatten_collection(props.address.electronic_mail_addresses), "WWWHomePageURL": props.address.www_home_page_url or None, - "MessagingIDs": json.loads(props.address.messaging_ids) if props.address.messaging_ids else None, + "MessagingIDs": flatten_collection(props.address.messaging_ids), } ) if self.file.schema == "IFC2X3": @@ -293,7 +327,7 @@ class EditAddress(bpy.types.Operator): attributes.update( { "InternalLocation": props.address.internal_location or None, - "AddressLines": json.loads(props.address.address_lines) if props.address.address_lines else None, + "AddressLines": flatten_collection(props.address.address_lines), "PostalBox": props.address.postal_box or None, "Town": props.address.town or None, "Region": props.address.region or None, diff --git a/src/blenderbim/blenderbim/bim/module/owner/prop.py b/src/blenderbim/blenderbim/bim/module/owner/prop.py index a92a6488fe..359d1cc129 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/prop.py +++ b/src/blenderbim/blenderbim/bim/module/owner/prop.py @@ -54,19 +54,19 @@ class Address(PropertyGroup): user_defined_purpose: StringProperty(name="Custom Purpose") internal_location: StringProperty(name="Internal Location") - address_lines: StringProperty(name="Address") + address_lines: CollectionProperty(type=StrProperty, name="Address") postal_box: StringProperty(name="Postal Box") town: StringProperty(name="Town") region: StringProperty(name="Region") postal_code: StringProperty(name="Postal Code") country: StringProperty(name="Country") - telephone_numbers: StringProperty(name="Telephone Numbers") - facsimile_numbers: StringProperty(name="Facsimile Numbers") + telephone_numbers: CollectionProperty(type=StrProperty, name="Telephone Numbers") + facsimile_numbers: CollectionProperty(type=StrProperty, name="Facsimile Numbers") pager_number: StringProperty(name="Pager Number") - electronic_mail_addresses: StringProperty(name="Emails") - www_home_page_url: StringProperty(name="Websites") - messaging_ids: StringProperty(name="IMs") + electronic_mail_addresses: CollectionProperty(type=StrProperty, name="Emails") + www_home_page_url: StringProperty(name="Website") + messaging_ids: CollectionProperty(type=StrProperty, name="IMs") class Role(PropertyGroup): @@ -112,9 +112,9 @@ class Person(PropertyGroup): name: StringProperty(name="Identification") family_name: StringProperty(name="Family Name") given_name: StringProperty(name="Given Name") - middle_names: StringProperty(name="Middle Names") - prefix_titles: StringProperty(name="Prefixes") - suffix_titles: StringProperty(name="Suffixes") + middle_names: CollectionProperty(type=StrProperty, name="Middle Names") + prefix_titles: CollectionProperty(type=StrProperty, name="Prefixes") + suffix_titles: CollectionProperty(type=StrProperty, name="Suffixes") class BIMOwnerProperties(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/module/owner/ui.py b/src/blenderbim/blenderbim/bim/module/owner/ui.py index e4ea5c943d..42c91c5603 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/ui.py +++ b/src/blenderbim/blenderbim/bim/module/owner/ui.py @@ -2,6 +2,35 @@ import bpy from bpy.types import Panel from ifcopenshell.api.owner.data import Data from blenderbim.bim.ifc import IfcStore +from .operator import AddOrRemoveElementFromCollection + + +def draw_string_collection(layout, owner, collection_name): + column = layout.column(align=True) + collection = getattr(owner, collection_name) + for i in range(len(collection)): + if i == 0: + row = draw_prop_on_new_row( + column, + collection[i], + "name", + align=True, + text=f"{owner.bl_rna.properties[collection_name].name}") + add_op = row.operator(AddOrRemoveElementFromCollection.bl_idname, icon="ADD", text="") + add_op.operation = "+" + add_op.collection_path = collection.path_from_id() + else: + row = draw_prop_on_new_row(column, collection[i], "name", align=True, text=f"#{i + 1}") + rem_op = row.operator(AddOrRemoveElementFromCollection.bl_idname, icon="REMOVE", text="") + rem_op.operation = "-" + rem_op.collection_path = collection.path_from_id() + rem_op.selected_item_idx = i + + +def draw_prop_on_new_row(layout, owner, attribute, align=False, **kwargs): + row = layout.row(align=align) + row.prop(owner, attribute, **kwargs) + return row def draw_roles_ui(box, assigned_object_id, roles, context): @@ -14,15 +43,12 @@ def draw_roles_ui(box, assigned_object_id, roles, context): if props.active_role_id == role_id: blender_role = props.role box2 = box.box() - row = box2.row(align=True) - row.prop(blender_role, "name", icon="MOD_CLOTH", text="") + row = draw_prop_on_new_row(box2, blender_role, "name", align=True, icon="MOD_CLOTH", text="") row.operator("bim.edit_role", icon="CHECKMARK", text="") row.operator("bim.disable_editing_role", icon="X", text="") if blender_role.name == "USERDEFINED": - row = box2.row() - row.prop(blender_role, "user_defined_role") - row = box2.row() - row.prop(blender_role, "description") + draw_prop_on_new_row(box2, blender_role, "user_defined_role") + draw_prop_on_new_row(box2, blender_role, "description") else: row = box.row(align=True) row.label(text=role["UserDefinedRole"] or role["Role"]) @@ -45,45 +71,29 @@ def draw_addresses_ui(box, assigned_object_id, addresses, file, context): if props.active_address_id == address_id: blender_address = props.address box2 = box.box() - row = box2.row(align=True) - row.prop(blender_address, "purpose", icon="MOD_CLOTH", text="") + row = draw_prop_on_new_row(box2, blender_address, "purpose", align=True, icon="MOD_CLOTH", text="") row.operator("bim.edit_address", icon="CHECKMARK", text="") row.operator("bim.disable_editing_address", icon="X", text="") if blender_address.purpose == "USERDEFINED": - row = box2.row() - row.prop(blender_address, "user_defined_purpose") - row = box2.row() - row.prop(blender_address, "description") + draw_prop_on_new_row(box2, blender_address, "user_defined_purpose") + draw_prop_on_new_row(box2, blender_address, "description") if address["type"] == "IfcTelecomAddress": - row = box2.row() - row.prop(blender_address, "telephone_numbers") - row = box2.row() - row.prop(blender_address, "facsimile_numbers") - row = box2.row() - row.prop(blender_address, "pager_number") - row = box2.row() - row.prop(blender_address, "electronic_mail_addresses") - row = box2.row() - row.prop(blender_address, "www_home_page_url") - if file.schema != "IFC2X3": - row = box2.row() - row.prop(blender_address, "messaging_ids") + draw_string_collection(box2, blender_address, "telephone_numbers") + draw_string_collection(box2, blender_address, "facsimile_numbers") + draw_prop_on_new_row(box2, blender_address, "pager_number") + draw_string_collection(box2, blender_address, "electronic_mail_addresses") + draw_prop_on_new_row(box2, blender_address, "www_home_page_url") + if file.schema != "IFC2X3": + draw_string_collection(box2, blender_address, "messaging_ids") elif address["type"] == "IfcPostalAddress": - row = box2.row() - row.prop(blender_address, "internal_location") - row = box2.row() - row.prop(blender_address, "address_lines") - row = box2.row() - row.prop(blender_address, "postal_box") - row = box2.row() - row.prop(blender_address, "town") - row = box2.row() - row.prop(blender_address, "region") - row = box2.row() - row.prop(blender_address, "postal_code") - row = box2.row() - row.prop(blender_address, "country") + draw_prop_on_new_row(box2, blender_address, "internal_location") + draw_string_collection(box2, blender_address, "address_lines") + draw_prop_on_new_row(box2, blender_address, "postal_box") + draw_prop_on_new_row(box2, blender_address, "town") + draw_prop_on_new_row(box2, blender_address, "region") + draw_prop_on_new_row(box2, blender_address, "postal_code") + draw_prop_on_new_row(box2, blender_address, "country") else: row = box.row(align=True) row.label(text=address["type"]) @@ -91,7 +101,6 @@ def draw_addresses_ui(box, assigned_object_id, addresses, file, context): row.operator("bim.remove_address", icon="X", text="").address_id = address_id - class BIM_PT_people(Panel): bl_label = "IFC People" bl_idname = "BIM_PT_people" @@ -120,21 +129,14 @@ class BIM_PT_people(Panel): if props.active_person_id == person_id: blender_person = props.person box = self.layout.box() - row = box.row(align=True) - row.prop(blender_person, "name", icon="USER", text="") + row = draw_prop_on_new_row(box, blender_person, "name", align=True, icon="USER", text="") row.operator("bim.edit_person", icon="CHECKMARK", text="") row.operator("bim.disable_editing_person", icon="X", text="") - row = box.row() - row.prop(blender_person, "family_name") - row = box.row() - row.prop(blender_person, "given_name") - row = box.row() - row.prop(blender_person, "middle_names") - row = box.row() - row.prop(blender_person, "prefix_titles") - row = box.row() - row.prop(blender_person, "suffix_titles") - + draw_prop_on_new_row(box, blender_person, "family_name") + draw_prop_on_new_row(box, blender_person, "given_name") + draw_string_collection(box, blender_person, "middle_names") + draw_string_collection(box, blender_person, "prefix_titles") + draw_string_collection(box, blender_person, "suffix_titles") draw_roles_ui(box, person_id, person["Roles"], context) draw_addresses_ui(box, person_id, person["Addresses"], self.file, context) else: @@ -183,11 +185,9 @@ class BIM_PT_organisations(Panel): row = box.row(align=True) row.prop(blender_organisation, "name", icon="USER", text="") row.operator("bim.edit_organisation", icon="CHECKMARK", text="") - row.operator("bim.disable_editing_organisation", icon="X", text="") - row = box.row() - row.prop(blender_organisation, "identification") - row = box.row() - row.prop(blender_organisation, "description") + row.operator("bim.disable_editing_organisation", icon="X", text="") + draw_prop_on_new_row(box, blender_organisation, "identification") + draw_prop_on_new_row(box, blender_organisation, "description") draw_roles_ui(box, organisation_id, organisation["Roles"], context) draw_addresses_ui(box, organisation_id, organisation["Addresses"], self.file, context) @@ -224,11 +224,9 @@ class BIM_PT_owner(Panel): if not Data.people: self.layout.label(text="No people found.") else: - row = self.layout.row() - row.prop(props, "user_person") + draw_prop_on_new_row(self.layout, props, "user_person") if not Data.organisations: self.layout.label(text="No organisations found.") else: - row = self.layout.row() - row.prop(props, "user_organisation") + draw_prop_on_new_row(self.layout, props, "user_organisation")