@Gorgious
Thanks, very insightful answer! :-)
Another question:
What would be best practice for saving the settings in the Blender UI?
For example, the end-user would like to save this configuration in the UI:
My idea was to store the settings in a text file somewhere. Kind of similar to Navisworks Search Sets. But after struggling for a while I have realized it might not be such a good idea. Is there no native Blender functionality which is capable of doing this?
There are two ways to store these settings natively in Blender : inside a single file, or across all blend files. If you don't mind each file having its own set of settings, you can add them to the Scene object type as a CollectionProperty and do your thing. I would advise against it, since it will only be available in this specific file and pretty much impossible to export to another file.
It is possible to store these settings natively across blender files using the addon preferences. here's an example how to do it. Depending on how you defined the properties you want to save, it might be as easy as using this PropertyGroup as a PointerProperty or CollectionProperty in your addons prefs and adding a custom UI to save / load it. Note : you do not have to expose the addon preferences custom properties to the user. Use a custom draw method and show only what you want.
I've done something like that in one of my addons. I self-imposed the constraint of not having external files to store this because I don't think it would be a good experience for the user. It does make the code more complicated than it needs to.
I think in your case it would be more straightforward to use a txt file, that way users can reliably save their preferences, and share it with other users easily. You can add all sorts of custom information in your files. You should however explore json formatting which is a reliable way to serialize / deserialize custom data. That way you don't have to design a parser from scratch. One aded benefit is that you can theoretically use these files in other platforms, you won't be dependant on Blender's implementation. You could even use a format that is universally recognized for doing such tasks, etc.
I think in your case it would be more straightforward to use a txt file, that way users can reliably save their preferences, and share it with other users easily. You can add all sorts of custom information in your files. You should however explore json formatting which is a reliable way to serialize / deserialize custom data. That way you don't have to design a parser from scratch. One aded benefit is that you can theoretically use these files in other platforms, you won't be dependant on Blender's implementation. You could even use a format that is universally recognized for doing such tasks, etc.
I will try to answer on a practical standpoint for your last few lines, they can be considered an anti-pattern. First and foremost, in python you don't compare to boolean values with the equality symbol. You use if a is True, not if a == True. I won't go into technical details but you can look it up if you want. It can be reduced to ifc_properties.my_ifcproductname = bool(property_value_from_json), but I would use another method.
I also don't know where ifc_properties.my_ifcproductname comes from ?
I think you can reduce this whole for loop with just :
for property_name_from_json, property_value_from_json in selection_configuration.items():
if not hasattr(ifc_properties, property_name_from_json):
continue # don't bother if we can't reliably copy the property
setattr(ifc_properties, property_name_from_json, property_value_from_json)
You may need to cast the values if you don't want blender to complain, since json is formatting everything to a string format (except boolean values I guess ?).
The set configuration method is being called in another class ConfirmSelection like so:
for property_name_from_json, property_value_from_json in selection_configuration.items():
if property_name_from_json.startswith('my_ifccustomproperty'):
set_configuration(context, property_set=property_name_from_json, property_name=property_value_from_json)
The set configuration method is being called in another class ConfirmSelection like so:
for property_name_from_json, property_value_from_json in selection_configuration.items():
if property_name_from_json.startswith('my_ifccustomproperty'):
set_configuration(context, property_set=property_name_from_json, property_name=property_value_from_json)
Hehe looks like my work here is done, you're ready to troubleshoot your addon on your own :)
Just kidding, don't hesitate to post questions here if need be ;)
BTW custom_collection.items.clear doesn't call the method, you have to use custom_collection.items.clear()
I personally think it's better UX wise to let the user delete specific items from a collection. You could modify your operator a bit by adding an index input
class CustomCollectionActions(bpy.types.Operator):
bl_idname = "custom.collection_actions"
bl_label = "Execute"
action: bpy.props.EnumProperty(
items=(
("add",) * 3,
("remove",) * 3,
),
)
index: bpy.props.IntProperty(default=-1)
def execute(self, context):
custom_collection = context.scene.custom_collection
if self.action == "add":
item = custom_collection.items.add()
if self.action == "remove":
if self.index < 0:
custom_collection.items.remove(len(custom_collection.items) - 1 )
else:
custom_collection.items.remove(index) # Be careful here we're not checking if this is a valid index, might throw an error
return {"FINISHED"}
and in your panel draw method
def draw(self, context):
# a bunch of things
box = self.layout.box()
for i, item in enumerate(context.scene.custom_collection.items):
op = box.operator("custom.collection_actions")
op.action = "remove"
op.index = i
box.operator("custom.collection_actions").action = "add"
or something like that (not tested)
Here's an example of how I used a similar concept in a fork of the prj addon :
What would be best practice to open an external file from Blender using python?
I've read the documentation here: https://docs.blender.org/api/current/bpy.ops.file.html
But there doesn't seem to be an operator to open a file, while in the UI there is the possibility to click on the folder icon:
When I look in the console and click the folder icon it gives no output. While it opens a file.
I could use the os module of python, but I have no way of testing it on other operating systems except windows.
That's why I am researching if it's possible to do it with Blender.
Don't really understand why the default should be -1? Doesn't that just remove the last item from the list?
I thought the index should be the specific item in the list the user wants to remove from the propertyset list?
EDIT: should have read your example
box = col.box()
column = box.column(align=True)
row = column.row(align=True)
row.prop(settings, "back_subjects_collections", text="Back subjects collections")
op = row.operator("prj.container_collection_add_or_remove", icon="ADD", text="")
op.operation = "ADD"
for i, container in enumerate(settings.back_subjects_collections):
row = column.row()
row.prop(container, "collection", text=f"Collection {i + 1}")
op = row.operator("prj.container_collection_add_or_remove", icon="REMOVE", text="")
op.operation = "REMOVE"
op.idx = i
Just kidding, don't hesitate to post questions here if need be ;)
I thought I understood the code, but I am missing something. Which I think is reasonably simple.
What I have at the moment:
What I like to achieve:
At the moment this is the code:
in prop.py
class CustomItem(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name ="Property",
description ="Use the PropertySet name and Property name divided by a .",
default ="PropertySet.Property"
)
class CustomCollection(bpy.types.PropertyGroup):
items: bpy.props.CollectionProperty(type=CustomItem)
I understand the enumerated i is the index which can be used to delete a certain item from the list, but I am struggling to create a delete button at the end of the box after the property as I have drawn with red rectangles.
The UI layout is created by gluing UI elements together. If you use prop, it will usually add a single row with the property as a field. If you want to use several properties on the same row, you need to use a custom row. Actually the same things as what you did with the add and delete operators.
for i, item in enumerate(custom_collection.items):
row = box.row(align=True)
row .prop(item, "name")
op = row.operator("custom.collection_actions", text="", icon="REMOVE")
op.action = "remove"
op.index = i
@Gorgious said:
The UI layout is created by gluing UI elements together. If you use prop, it will usually add a single row with the property as a field. If you want to use several properties on the same row, you need to use a custom row. Actually the same things as what you did with the add and delete operators.
for i, item in enumerate(custom_collection.items):
row = box.row(align=True)
row .prop(item, "name")
op = row.operator("custom.collection_actions", text="", icon="REMOVE")
op.action = "remove"
op.index = i
I like for the Add button to be on top of the list, that way it doesn't move when you add new elements. It can be annoying when you want to add several items at a time
I like for the Add button to be on top of the list, that way it doesn't move when you add new elements. It can be annoying when you want to add several items at a time
This is a very good point, thanks for your help and fast reply once again @Gorgious . To recap I eventually made this:
And made this to prevent the dataframe from making columns twice if the user accidently adds the same property:
for custom_property in custom_collection.items:
custom_propertyset_list.append(custom_property.name)
custom_property_unique_list = []
seen = set()
for item in custom_propertyset_list:
if item not in seen:
seen.add(item)
custom_property_unique_list.append(item)
I would like to add a user dialog for the user to close their spreadsheet file when it's already opened. Similar like this:
I tried several python scripts to see if an instance of an application is running on a specific OS. But this became really complex and I abandoned the idea. Now I just check if there is a value in this field so I can assume the user has a spreadsheet opened:
When there is a value in this field and they click 'Create Spreadsheet' again I would like to have a dialog pop up box which says. "Please close your spreadsheet first at C:\my_spreadsheetfile" .
I googled it and found this SO post
I've added and registered the classes from this example in my operator.py and ui.py.
Only I am bit confused on how to call this class when this condition is met I described:
This is the code:
class ExportToSpreadSheet(bpy.types.Operator):
bl_idname = "export.tospreadsheet"
bl_label = "Create spreadsheet"
def execute(self, context):
ifc_properties = context.scene.ifc_properties
if len(ifc_properties.my_spreadsheetfile) == 0:
self.create_spreadsheet(context)
if len(ifc_properties.my_spreadsheetfile) > 1:
if self.get_current_ui_settings(context) == self.get_stored_ui_settings():
self.open_file_on_each_os(spreadsheet_filepath=ifc_properties.my_spreadsheetfile)
if self.get_current_ui_settings(context) != self.get_stored_ui_settings():
if (self.check_if_file_is_open(spreadsheet_filepath=ifc_properties.my_spreadsheetfile)):
print ("Please close the spreadsheet file first")
#CALL THE CLASS HERE WHICH GIVES THE POP UP DIALOG
else:
self.create_spreadsheet(context)
return {'FINISHED'}
Alright, this is one instance where if I understand correctly you might be overengineering your program a bit.
Programs should be self contained and not rely too much on external programs or variable user input, to reduce bugs, technical debt and.. erm... user error :)
If you're beginning to chain if statements you might be going too deep into the user's brain.
You can try to go the other route, don't ask for permission, ask for forgiveness. Try to save the file, if it's not writable (because there can be several reasons why, for example the user doesn't have permission to write in this folder or in this network drive, or the folder you're trying to write to doesn't exist) catch the Error in a try / except, and if there is an error, do your thing. That way you don't have to rely on OS specific ways to check if a file is open, or if the user has permission, etc. My 2 cents.
Also, IMO it is reasonable to expect the user to close the file they're trying to overwrite on their own if they have it open.
Comments
You can fetch the relevant data from ifc and dynamically update the enum items with a callback. For example here's where the types are populated and here's where the dynamic enums are used to fetch the data. Remember to always have a permanent handle to the dynamic enum items or else you'll start seeing weird bugs in how they're displayed. See https://blender.stackexchange.com/questions/216230/is-there-a-workaround-for-the-known-bug-in-dynamic-enumproperty or the warning snippet in the docs for more information
@Gorgious
Thanks, very insightful answer! :-)
Another question:
What would be best practice for saving the settings in the Blender UI?
For example, the end-user would like to save this configuration in the UI:
My idea was to store the settings in a text file somewhere. Kind of similar to Navisworks Search Sets. But after struggling for a while I have realized it might not be such a good idea. Is there no native Blender functionality which is capable of doing this?
There are two ways to store these settings natively in Blender : inside a single file, or across all blend files. If you don't mind each file having its own set of settings, you can add them to the
Scene
object type as aCollectionProperty
and do your thing. I would advise against it, since it will only be available in this specific file and pretty much impossible to export to another file.It is possible to store these settings natively across blender files using the addon preferences. here's an example how to do it. Depending on how you defined the properties you want to save, it might be as easy as using this
PropertyGroup
as aPointerProperty
orCollectionProperty
in your addons prefs and adding a custom UI to save / load it. Note : you do not have to expose the addon preferences custom properties to the user. Use a customdraw
method and show only what you want.I've done something like that in one of my addons. I self-imposed the constraint of not having external files to store this because I don't think it would be a good experience for the user. It does make the code more complicated than it needs to.
I think in your case it would be more straightforward to use a txt file, that way users can reliably save their preferences, and share it with other users easily. You can add all sorts of custom information in your files. You should however explore
json
formatting which is a reliable way to serialize / deserialize custom data. That way you don't have to design a parser from scratch. One aded benefit is that you can theoretically use these files in other platforms, you won't be dependant on Blender's implementation. You could even use a format that is universally recognized for doing such tasks, etc.Thanks, this is a really useful tip
I managed to create a json file from all the Properties in my PropertyGroup class.
How would I set all the properties accordingly to this json file in the Blender UI? I am struggling with the logic.
So far I came up with this:
But sometimes it seems to work and other times not, I am getting a bit confused.
I will try to answer on a practical standpoint for your last few lines, they can be considered an anti-pattern. First and foremost, in python you don't compare to boolean values with the equality symbol. You use
if a is True
, notif a == True
. I won't go into technical details but you can look it up if you want. It can be reduced toifc_properties.my_ifcproductname = bool(property_value_from_json)
, but I would use another method.I also don't know where
ifc_properties.my_ifcproductname
comes from ?I think you can reduce this whole
for
loop with just :You may need to cast the values if you don't want blender to complain, since json is formatting everything to a string format (except boolean values I guess ?).
Just dropping in to say this thread is a goldmine of knowledge and a huge thanks to @Gorgious for being so generous with his knowledge!
Hehe thank you it means a lot. I feel like I'm just doing my part in giving back a part of what was given to me by others :)
@Gorgious
Thank you so much! I did not know about the
setattr
function.Found here a good post when to use
is
and when to use==
I'm really struggling with this, I want to update the EnumProperty from a Json file.
So far I have this:
The
set configuration
method is being called in another classConfirmSelection
like so:A section of the json file looks like this:
I think I need to update
action: bpy.props.EnumProperty
. but before I can update it with a value I need to add an empty item?I was overthinking this, it was suprisingly easy:
How would I clear an enumproperty?
Found this
custom_collection.items.clear
but I don't see it doing anything.Disregard, I spoke too soon, the method is going through a loop so
custom_collection.items.remove(1)
was sufficient. Thank you for reading.The result, you can now load a json file and will store the settings:
Hehe looks like my work here is done, you're ready to troubleshoot your addon on your own :)
Just kidding, don't hesitate to post questions here if need be ;)
BTW
custom_collection.items.clear
doesn't call the method, you have to usecustom_collection.items.clear()
I personally think it's better UX wise to let the user delete specific items from a collection. You could modify your operator a bit by adding an index input
and in your panel
draw
methodor something like that (not tested)
Here's an example of how I used a similar concept in a fork of the prj addon :
Quite simple question:
What would be best practice to open an external file from Blender using python?
I've read the documentation here:
https://docs.blender.org/api/current/bpy.ops.file.html
But there doesn't seem to be an operator to open a file, while in the UI there is the possibility to click on the folder icon:
When I look in the console and click the folder icon it gives no output. While it opens a file.
I could use the
os
module of python, but I have no way of testing it on other operating systems except windows.That's why I am researching if it's possible to do it with Blender.
@Gorgious
Don't really understand why the default should be -1? Doesn't that just remove the last item from the list?
I thought the index should be the specific item in the list the user wants to remove from the propertyset list?
EDIT: should have read your example
bpy.ops.bim.load_project(filepath="/path/to/file.ifc")
I meant more like opening an external document on your native os, like a text or xml file from Blender, not an ifc file. But thanks :-)
Found it in this SO post
@Gorgious
I thought I understood the code, but I am missing something. Which I think is reasonably simple.
What I have at the moment:
What I like to achieve:
At the moment this is the code:
in prop.py
in ui.py
in operator.py:
I understand the enumerated
i
is the index which can be used to delete a certain item from the list, but I am struggling to create a delete button at the end of the box after the property as I have drawn with red rectangles.The UI layout is created by gluing UI elements together. If you use
prop
, it will usually add a single row with the property as a field. If you want to use several properties on the same row, you need to use a customrow
. Actually the same things as what you did with the add and delete operators.should do the trick (not tested)
oh wow that was easy XD
I like for the Add button to be on top of the list, that way it doesn't move when you add new elements. It can be annoying when you want to add several items at a time
This is a very good point, thanks for your help and fast reply once again @Gorgious . To recap I eventually made this:
in
operator.py
in
ui.py
And made this to prevent the dataframe from making columns twice if the user accidently adds the same property:
This wil keep order of the
set
as well.I would like to add a user dialog for the user to close their spreadsheet file when it's already opened. Similar like this:
I tried several python scripts to see if an instance of an application is running on a specific OS. But this became really complex and I abandoned the idea. Now I just check if there is a value in this field so I can assume the user has a spreadsheet opened:
When there is a value in this field and they click 'Create Spreadsheet' again I would like to have a dialog pop up box which says. "Please close your spreadsheet first at C:\my_spreadsheetfile" .
I googled it and found this SO post
I've added and registered the classes from this example in my
operator.py
andui.py
.Only I am bit confused on how to call this class when this condition is met I described:
This is the code:
Alright, this is one instance where if I understand correctly you might be overengineering your program a bit.
Programs should be self contained and not rely too much on external programs or variable user input, to reduce bugs, technical debt and.. erm... user error :)
If you're beginning to chain
if
statements you might be going too deep into the user's brain.You can try to go the other route, don't ask for permission, ask for forgiveness. Try to save the file, if it's not writable (because there can be several reasons why, for example the user doesn't have permission to write in this folder or in this network drive, or the folder you're trying to write to doesn't exist) catch the
Error
in atry / except
, and if there is an error, do your thing. That way you don't have to rely on OS specific ways to check if a file is open, or if the user has permission, etc. My 2 cents.Also, IMO it is reasonable to expect the user to close the file they're trying to overwrite on their own if they have it open.
How would I update the
Length
String property in real time withN
xCenter to Center
multiplication?Do you want to be able to modify either of the three parameters and the other two update or Length can be readonly ?