Wrapper to fix empty and out-of-bounds EnumPropertys and fix their garbled UI text.
There are a bunch of EnumProperty properties that use a function to get the list of items for that property. An issue occurs when the currently selected item in the UI is the last in the list of items and then the object/armature/shapekey/etc. corresponding to that item is deleted. The index of the selected item remains the same, but the list of items shrinks such that the index is now out of bounds. This causes a warning to be printed in the console each time UI attempts to draw the property or the property is otherwise accessed. Additionally, attempting to get the value of the EnumProperty that has gone out of bounds will return '' which may not be a valid or expected value (in Blender 2.79, if there is an item at index 0, it returns that instead).
This PR adds a wrapper for the items functions that are used in EnumPropertys. When the property is accessed, if the index of the property is out of bounds, it temporarily adds duplicates of the last element in the list of items until the index is in bounds again and then tries to set the index such that it is back in bounds (ignoring the temporarily added duplicates). Often, getting the list of items occurs during UI drawing, which is prevented from modifying properties, so changing the index to be back in bounds is then deferred and run a little bit later.
By fixing out-of-bounds indices in this way, the Common.reset_context_scenes function is no longer necessary as the properties will automatically be set to valid values whenever they are accessed.
The wrapper also ensures that the list of items is never empty and includes the commonly used method of sorting the list of items to avoid repeated code. The sorting can be disabled via the sort argument and the wrapper can be forced to not run in-place with the in_place argument.
As part of ensuring the list of items is never empty, there is a constant identifier, Common._empty_enum_identifier, used to indicate whether an EnumProperty is empty. is_enum_empty and is_enum_non_empty functions have been added for checking against the constant identifier. The latter isn't really needed since the former can be negated, but I think it makes code more readable to have it.
As Blender 2.79 does not have Application Timers (bpy.app.timers) the temporarily added duplicate items will continue to be added every time the property is accessed until the property is accessed outside of UI drawing. To users, this will let them see the all the temporary duplicates in the UI, but upon closing the UI dropdown/search it will automatically fix itself as that causes the property to be accessed outside of UI drawing. To code that reads the value of the property, there will be no perceivable difference as the temporary duplicate items have the same identifier as the last element of the items. I did figure out a way to use a separate Thread for 2.79 instead of Application Timers, but I don't think it would be safe to use.
Here I have deleted Armature.002 so there is now a duplicate Armature.001 in the list, upon closing the drop down, the duplicate will disappear as that will cause the index to be set back in-bounds.

The wrapper also works around a known bug with EnumProperty properties:
There is a known bug with using a callback, Python must keep a reference to the strings returned by the callback or Blender will misbehave or even crash.
This bug causes UI text to be garbled as presumably it's reading random memory which is probably why it theoretically could cause Blender to crash. This issue isn't very visible when displaying an EnumProperty in UI via row.prop as the currently selected choice and list of choices displays correctly without any fixes, however the descriptions for the choices are almost always garbled. If instead, an EnumProperty is displayed in a UI via row.props_enum, the issue becomes very visible: the currently selected choice becomes garbled when moving objects in the scene around and the unselected choices are almost always garbled.
Garbled description text example:
The wrapper keeps references by interning all the strings in the list of items and then caching the strings into a dictionary for each property path.
I guess that all the assignments to bpy.types.Object.Enum in the various items functions was an attempt to workaround this known bug. Unfortunately, all this does is keep a reference to the list of the last called items function, when it's the strings within all the items lists that Python must keep references to. These assignments have been removed.
I did look at the possibility of replacing EnumPropertys with PointerPropertys, but this didn't seem like a good idea for some of the properties as a PointerProperty can keep reference to an Object/Shapekey/etc even if the PointerProperty's poll function says that the Object/Shapekey/etc is no longer valid and can keep reference to Objects and possibly other types that have been deleted from the scene, which also prevents said deleted Objects from being considered Orphan Data. Maybe these issues could be worked around, but it's not something I've looked into any further than this.
While Cats does not currently have any dynamic EnumPropertys within a PropertyGroup or CollectionProperty, I did make sure that the wrapper also works with EnumPropertys nested in either.
The main limitation of this wrapper is that it only works for properties that are owned by a Scene object as the scheduled task needs to be able to get the owner of the property to fix by the owner's name since keeping reference to the owner is a bad idea as the owner could be deleted prior to the scheduled task running. Fortunately, all of Cats' EnumPropertys are owned by a scene.
If you want to see the out-of-bounds issue yourself or compare behaviour with this PR (show the system console if you want to see the warning spam):
- Create 3 armatures (3 are needed to fully show this issue because the part of the UI is hidden when there is only 1 armature)
- Select the bottom armature in the Cats UI (above the Fix Model button)
- Delete that armature from the scene
- The
armatureproperty of the scene is now out of bounds
An alternative:
- Open the Visemes section of the CATS UI
- Select a mesh with shape keys in the UI
- Delete all the shape key from that mesh
- The
mouth_a,mouth_oandmouth_chproperties of the scene are now out of bounds (in this case because the list of shape keys is now empty)
Short clip showing what happens without this wrapper if you use row.props_enum for the armature instead of row.prop
https://user-images.githubusercontent.com/495015/173363370-a586bb47-b042-43d1-87f8-54be324b8f7e.mp4
Heyhey, thank you for this PR! It looks nice, but I dislike how the list can get extremely long and how the number are randomized every time, but I'm sure the second one has an easy fix. Also I wonder why some armatures appear twice in that list.
I also wanted to do something about the issue with the EnumProperty and my solution would be changing them to a CollectionProperty, which looks something like this:

This fixes the isuue with EnumProperties and uses less space than adding them to a long list.
but I dislike how the list can get extremely long and how the number are randomized every time
If you're referring to the short video I posted that's not what this patch does, that's showing the problems with the current Cats code is and that it's a miracle that it even works currently when using UILayout.prop (though hovering over items in the current Cats dropdowns can still crash Blender). The video simply shows the current Cats but with the armature EnumProperty displayed through a UILayout.props_enum instead of the current UILayout.prop.
This patch fixes the EnumProperties so that they would work if displayed through a UILayout.props_enum and fixes the potential crash when viewing the descriptions of the items in the dropdown of the currently used UILayout.prop.
I also wanted to do something about the issue with the EnumProperty and my solution would be changing them to a CollectionProperty, which looks something like this:
Unfortunately, using a CollectionProperty for each property won't work without changing a lot of existing code because it won't update as armatures in the scene are added/removed. And much of the existing code relies on assuming that the returned Object/Mesh/etc. name is of an Object/Mesh/etc. that does indeed exist. To use a CollectionProperty in this way with the existing code you would have to re-check and update the affected properties whenever the scene is updated. I think the VRM addon does something similar for all of its bone properties.
The property you showed in the gif is a PointerProperty, which would have been my ideal solution, however an important note of using a PointerProperty is that it makes the Scene or whatever it's attached to a user of the object the PointerProperty is set to, preventing it from being automatically deleted when saving the .blend file if it's otherwise not used. Additionally, PointerProperties cannot be used for shape keys or bones, they can only be used with PropertyGroup or ID types, meaning some other property would have to be used for displaying those.
A StringProperty displayed through a UILayout.prop_search is a reasonable choice for shape keys or bones, however it should be noted that the property being displayed in red when the shape key/bone no longer exists only works up to a certain number of shape keys/bones, after which, the UI stops checking.
StringProperties were updated fairly recently to be able to specify a search function that will show candidates for the property. By default, these candidates are suggestions, but that option can be disabled, requiring the property to use a candidate. This also means that the search function will be called on every UI update, much the same as the items functions on all of the current EnumProperties. I have yet to try using this feature of StringProperties, so I don't know what it does when the currently set value is no longer valid.
Another potential solution to fix the out of bounds issues of the EnumProperties is to do all of the property data storage manually by replacing the get (and possibly set) functions, then when getting the current enum index, if the current stored index would be out of bounds of the enum items, the get function would adjust the enum index back in-bounds before returning it. The main problem is that the get and set functions do not take a context argument and it's unclear what sort of context bpy.context would be at this time.
Holy hell, I should have looked thought the commit first. I'm so sorry, I was misguided by the small video. This PR is very impressive, thank you very much!