Improve UI for multi input string fields (#1617)

* Add utility operator for CollectionProperty

* Support Multi input string fields

* Simplify people & organisation UI code

* General-purpose code cleanup

* Use more compact and user friendly UI
This commit is contained in:
Gorgious56
2021-08-03 01:07:19 +02:00
committed by GitHub
parent 557f8e6d69
commit 7c0405fab0
4 changed files with 148 additions and 115 deletions
@@ -2,6 +2,7 @@ import bpy
from . import ui, prop, operator from . import ui, prop, operator
classes = ( classes = (
operator.AddOrRemoveElementFromCollection,
operator.EnableEditingPerson, operator.EnableEditingPerson,
operator.DisableEditingPerson, operator.DisableEditingPerson,
operator.AddPerson, operator.AddPerson,
@@ -1,10 +1,49 @@
import bpy import bpy
import json
import ifcopenshell.api import ifcopenshell.api
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.owner.data import Data 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): class EnableEditingPerson(bpy.types.Operator):
bl_idname = "bim.enable_editing_person" bl_idname = "bim.enable_editing_person"
bl_label = "Enable Editing Person" bl_label = "Enable Editing Person"
@@ -17,12 +56,13 @@ class EnableEditingPerson(bpy.types.Operator):
props.active_person_id = self.person_id props.active_person_id = self.person_id
data = Data.people[self.person_id] data = Data.people[self.person_id]
name = data["Id"] if self.file.schema == "IFC2X3" else data["Identification"] name = data["Id"] if self.file.schema == "IFC2X3" else data["Identification"]
props.person.name = name or "" person = props.person
props.person.family_name = data["FamilyName"] or "" person.name = name or ""
props.person.given_name = data["GivenName"] or "" person.family_name = data["FamilyName"] or ""
props.person.middle_names = json.dumps(data["MiddleNames"]) if data["MiddleNames"] else "" person.given_name = data["GivenName"] or ""
props.person.prefix_titles = json.dumps(data["PrefixTitles"]) if data["PrefixTitles"] else "" populate_collection(person.middle_names, data.get("MiddleNames", None))
props.person.suffix_titles = json.dumps(data["SuffixTitles"]) if data["SuffixTitles"] else "" populate_collection(person.prefix_titles, data.get("PrefixTitles", None))
populate_collection(person.suffix_titles, data.get("SuffixTitles", None))
return {"FINISHED"} return {"FINISHED"}
@@ -61,13 +101,14 @@ class EditPerson(bpy.types.Operator):
def _execute(self, context): def _execute(self, context):
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
props = context.scene.BIMOwnerProperties props = context.scene.BIMOwnerProperties
person = props.person
attributes = { attributes = {
"Identification": props.person.name or None, "Identification": person.name or None,
"FamilyName": props.person.family_name or None, "FamilyName": person.family_name or None,
"GivenName": props.person.given_name or None, "GivenName": person.given_name or None,
"MiddleNames": json.loads(props.person.middle_names) if props.person.middle_names else None, "MiddleNames": flatten_collection(person.middle_names),
"PrefixTitles": json.loads(props.person.prefix_titles) if props.person.prefix_titles else None, "PrefixTitles": flatten_collection(person.prefix_titles),
"SuffixTitles": json.loads(props.person.suffix_titles) if props.person.suffix_titles else None, "SuffixTitles": flatten_collection(person.suffix_titles),
} }
if self.file.schema == "IFC2X3": if self.file.schema == "IFC2X3":
attributes["Id"] = attributes["Identification"] attributes["Id"] = attributes["Identification"]
@@ -215,29 +256,28 @@ class EnableEditingAddress(bpy.types.Operator):
props = context.scene.BIMOwnerProperties props = context.scene.BIMOwnerProperties
props.active_address_id = self.address_id props.active_address_id = self.address_id
data = Data.addresses[self.address_id] data = Data.addresses[self.address_id]
props.address.name = data["type"] address = props.address
props.address.purpose = data["Purpose"] or "None" address.name = data["type"]
props.address.description = data["Description"] or "" address.purpose = data["Purpose"] or "None"
props.address.user_defined_purpose = data["UserDefinedPurpose"] or "" address.description = data["Description"] or ""
address.user_defined_purpose = data["UserDefinedPurpose"] or ""
if data["type"] == "IfcTelecomAddress": if data["type"] == "IfcTelecomAddress":
props.address.telephone_numbers = json.dumps(data["TelephoneNumbers"]) if data["TelephoneNumbers"] else "" populate_collection(address.telephone_numbers, data.get("TelephoneNumbers", None))
props.address.facsimile_numbers = json.dumps(data["FacsimileNumbers"]) if data["FacsimileNumbers"] else "" populate_collection(address.facsimile_numbers, data.get("FacsimileNumbers", None))
props.address.pager_number = data["PagerNumber"] or "" address.pager_number = data["PagerNumber"] or ""
props.address.electronic_mail_addresses = ( populate_collection(address.electronic_mail_addresses, data.get("ElectronicMailAddresses", None))
json.dumps(data["ElectronicMailAddresses"]) if data["ElectronicMailAddresses"] else "" address.www_home_page_url = data["WWWHomePageURL"] or ""
)
props.address.www_home_page_url = data["WWWHomePageURL"] or ""
if self.file.schema != "IFC2X3": 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": elif data["type"] == "IfcPostalAddress":
props.address.internal_location = data["InternalLocation"] or "" address.internal_location = data["InternalLocation"] or ""
props.address.address_lines = json.dumps(data["AddressLines"]) if data["AddressLines"] else "" populate_collection(address.address_lines, data.get("AddressLines", None))
props.address.postal_box = data["PostalBox"] or "" address.postal_box = data["PostalBox"] or ""
props.address.town = data["Town"] or "" address.town = data["Town"] or ""
props.address.region = data["Region"] or "" address.region = data["Region"] or ""
props.address.postal_code = data["PostalCode"] or "" address.postal_code = data["PostalCode"] or ""
props.address.country = data["Country"] or "" address.country = data["Country"] or ""
return {"FINISHED"} return {"FINISHED"}
@@ -273,18 +313,12 @@ class EditAddress(bpy.types.Operator):
if address.is_a("IfcTelecomAddress"): if address.is_a("IfcTelecomAddress"):
attributes.update( attributes.update(
{ {
"TelephoneNumbers": json.loads(props.address.telephone_numbers) "TelephoneNumbers": flatten_collection(props.address.telephone_numbers),
if props.address.telephone_numbers "FacsimileNumbers": flatten_collection(props.address.facsimile_numbers),
else None,
"FacsimileNumbers": json.loads(props.address.facsimile_numbers)
if props.address.facsimile_numbers
else None,
"PagerNumber": props.address.pager_number or None, "PagerNumber": props.address.pager_number or None,
"ElectronicMailAddresses": json.loads(props.address.electronic_mail_addresses) "ElectronicMailAddresses": flatten_collection(props.address.electronic_mail_addresses),
if props.address.electronic_mail_addresses
else None,
"WWWHomePageURL": props.address.www_home_page_url or None, "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": if self.file.schema == "IFC2X3":
@@ -293,7 +327,7 @@ class EditAddress(bpy.types.Operator):
attributes.update( attributes.update(
{ {
"InternalLocation": props.address.internal_location or None, "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, "PostalBox": props.address.postal_box or None,
"Town": props.address.town or None, "Town": props.address.town or None,
"Region": props.address.region or None, "Region": props.address.region or None,
@@ -54,19 +54,19 @@ class Address(PropertyGroup):
user_defined_purpose: StringProperty(name="Custom Purpose") user_defined_purpose: StringProperty(name="Custom Purpose")
internal_location: StringProperty(name="Internal Location") internal_location: StringProperty(name="Internal Location")
address_lines: StringProperty(name="Address") address_lines: CollectionProperty(type=StrProperty, name="Address")
postal_box: StringProperty(name="Postal Box") postal_box: StringProperty(name="Postal Box")
town: StringProperty(name="Town") town: StringProperty(name="Town")
region: StringProperty(name="Region") region: StringProperty(name="Region")
postal_code: StringProperty(name="Postal Code") postal_code: StringProperty(name="Postal Code")
country: StringProperty(name="Country") country: StringProperty(name="Country")
telephone_numbers: StringProperty(name="Telephone Numbers") telephone_numbers: CollectionProperty(type=StrProperty, name="Telephone Numbers")
facsimile_numbers: StringProperty(name="Facsimile Numbers") facsimile_numbers: CollectionProperty(type=StrProperty, name="Facsimile Numbers")
pager_number: StringProperty(name="Pager Number") pager_number: StringProperty(name="Pager Number")
electronic_mail_addresses: StringProperty(name="Emails") electronic_mail_addresses: CollectionProperty(type=StrProperty, name="Emails")
www_home_page_url: StringProperty(name="Websites") www_home_page_url: StringProperty(name="Website")
messaging_ids: StringProperty(name="IMs") messaging_ids: CollectionProperty(type=StrProperty, name="IMs")
class Role(PropertyGroup): class Role(PropertyGroup):
@@ -112,9 +112,9 @@ class Person(PropertyGroup):
name: StringProperty(name="Identification") name: StringProperty(name="Identification")
family_name: StringProperty(name="Family Name") family_name: StringProperty(name="Family Name")
given_name: StringProperty(name="Given Name") given_name: StringProperty(name="Given Name")
middle_names: StringProperty(name="Middle Names") middle_names: CollectionProperty(type=StrProperty, name="Middle Names")
prefix_titles: StringProperty(name="Prefixes") prefix_titles: CollectionProperty(type=StrProperty, name="Prefixes")
suffix_titles: StringProperty(name="Suffixes") suffix_titles: CollectionProperty(type=StrProperty, name="Suffixes")
class BIMOwnerProperties(PropertyGroup): class BIMOwnerProperties(PropertyGroup):
@@ -2,6 +2,35 @@ import bpy
from bpy.types import Panel from bpy.types import Panel
from ifcopenshell.api.owner.data import Data from ifcopenshell.api.owner.data import Data
from blenderbim.bim.ifc import IfcStore 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): 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: if props.active_role_id == role_id:
blender_role = props.role blender_role = props.role
box2 = box.box() box2 = box.box()
row = box2.row(align=True) row = draw_prop_on_new_row(box2, blender_role, "name", align=True, icon="MOD_CLOTH", text="")
row.prop(blender_role, "name", icon="MOD_CLOTH", text="")
row.operator("bim.edit_role", icon="CHECKMARK", text="") row.operator("bim.edit_role", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_role", icon="X", text="") row.operator("bim.disable_editing_role", icon="X", text="")
if blender_role.name == "USERDEFINED": if blender_role.name == "USERDEFINED":
row = box2.row() draw_prop_on_new_row(box2, blender_role, "user_defined_role")
row.prop(blender_role, "user_defined_role") draw_prop_on_new_row(box2, blender_role, "description")
row = box2.row()
row.prop(blender_role, "description")
else: else:
row = box.row(align=True) row = box.row(align=True)
row.label(text=role["UserDefinedRole"] or role["Role"]) 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: if props.active_address_id == address_id:
blender_address = props.address blender_address = props.address
box2 = box.box() box2 = box.box()
row = box2.row(align=True) row = draw_prop_on_new_row(box2, blender_address, "purpose", align=True, icon="MOD_CLOTH", text="")
row.prop(blender_address, "purpose", icon="MOD_CLOTH", text="")
row.operator("bim.edit_address", icon="CHECKMARK", text="") row.operator("bim.edit_address", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_address", icon="X", text="") row.operator("bim.disable_editing_address", icon="X", text="")
if blender_address.purpose == "USERDEFINED": if blender_address.purpose == "USERDEFINED":
row = box2.row() draw_prop_on_new_row(box2, blender_address, "user_defined_purpose")
row.prop(blender_address, "user_defined_purpose") draw_prop_on_new_row(box2, blender_address, "description")
row = box2.row()
row.prop(blender_address, "description")
if address["type"] == "IfcTelecomAddress": if address["type"] == "IfcTelecomAddress":
row = box2.row() draw_string_collection(box2, blender_address, "telephone_numbers")
row.prop(blender_address, "telephone_numbers") draw_string_collection(box2, blender_address, "facsimile_numbers")
row = box2.row() draw_prop_on_new_row(box2, blender_address, "pager_number")
row.prop(blender_address, "facsimile_numbers") draw_string_collection(box2, blender_address, "electronic_mail_addresses")
row = box2.row() draw_prop_on_new_row(box2, blender_address, "www_home_page_url")
row.prop(blender_address, "pager_number") if file.schema != "IFC2X3":
row = box2.row() draw_string_collection(box2, blender_address, "messaging_ids")
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")
elif address["type"] == "IfcPostalAddress": elif address["type"] == "IfcPostalAddress":
row = box2.row() draw_prop_on_new_row(box2, blender_address, "internal_location")
row.prop(blender_address, "internal_location") draw_string_collection(box2, blender_address, "address_lines")
row = box2.row() draw_prop_on_new_row(box2, blender_address, "postal_box")
row.prop(blender_address, "address_lines") draw_prop_on_new_row(box2, blender_address, "town")
row = box2.row() draw_prop_on_new_row(box2, blender_address, "region")
row.prop(blender_address, "postal_box") draw_prop_on_new_row(box2, blender_address, "postal_code")
row = box2.row() draw_prop_on_new_row(box2, blender_address, "country")
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")
else: else:
row = box.row(align=True) row = box.row(align=True)
row.label(text=address["type"]) 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 row.operator("bim.remove_address", icon="X", text="").address_id = address_id
class BIM_PT_people(Panel): class BIM_PT_people(Panel):
bl_label = "IFC People" bl_label = "IFC People"
bl_idname = "BIM_PT_people" bl_idname = "BIM_PT_people"
@@ -120,21 +129,14 @@ class BIM_PT_people(Panel):
if props.active_person_id == person_id: if props.active_person_id == person_id:
blender_person = props.person blender_person = props.person
box = self.layout.box() box = self.layout.box()
row = box.row(align=True) row = draw_prop_on_new_row(box, blender_person, "name", align=True, icon="USER", text="")
row.prop(blender_person, "name", icon="USER", text="")
row.operator("bim.edit_person", icon="CHECKMARK", text="") row.operator("bim.edit_person", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_person", icon="X", text="") row.operator("bim.disable_editing_person", icon="X", text="")
row = box.row() draw_prop_on_new_row(box, blender_person, "family_name")
row.prop(blender_person, "family_name") draw_prop_on_new_row(box, blender_person, "given_name")
row = box.row() draw_string_collection(box, blender_person, "middle_names")
row.prop(blender_person, "given_name") draw_string_collection(box, blender_person, "prefix_titles")
row = box.row() draw_string_collection(box, blender_person, "suffix_titles")
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_roles_ui(box, person_id, person["Roles"], context) draw_roles_ui(box, person_id, person["Roles"], context)
draw_addresses_ui(box, person_id, person["Addresses"], self.file, context) draw_addresses_ui(box, person_id, person["Addresses"], self.file, context)
else: else:
@@ -183,11 +185,9 @@ class BIM_PT_organisations(Panel):
row = box.row(align=True) row = box.row(align=True)
row.prop(blender_organisation, "name", icon="USER", text="") row.prop(blender_organisation, "name", icon="USER", text="")
row.operator("bim.edit_organisation", icon="CHECKMARK", text="") row.operator("bim.edit_organisation", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_organisation", icon="X", text="") row.operator("bim.disable_editing_organisation", icon="X", text="")
row = box.row() draw_prop_on_new_row(box, blender_organisation, "identification")
row.prop(blender_organisation, "identification") draw_prop_on_new_row(box, blender_organisation, "description")
row = box.row()
row.prop(blender_organisation, "description")
draw_roles_ui(box, organisation_id, organisation["Roles"], context) draw_roles_ui(box, organisation_id, organisation["Roles"], context)
draw_addresses_ui(box, organisation_id, organisation["Addresses"], self.file, 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: if not Data.people:
self.layout.label(text="No people found.") self.layout.label(text="No people found.")
else: else:
row = self.layout.row() draw_prop_on_new_row(self.layout, props, "user_person")
row.prop(props, "user_person")
if not Data.organisations: if not Data.organisations:
self.layout.label(text="No organisations found.") self.layout.label(text="No organisations found.")
else: else:
row = self.layout.row() draw_prop_on_new_row(self.layout, props, "user_organisation")
row.prop(props, "user_organisation")