4
$\begingroup$

I have a CollectionProperty like this:

class RenderGroups(PropertyGroup):
    image = StringProperty(name="Image", description="The image-texture of this group")

class MHFCMeshProps(PropertyGroup):
    render_groups = CollectionProperty(type=RenderGroups,
                                       name="Render Groups")

Now I don't want duplicate names in the PropertyGroup but I stumbled when I tried to shadow the internal name with a StringProperty. Is there any way the following code:

g = obj.testgroup.render_groups.add()
g.name = "Name"
g = obj.testgroup.render_groups.add()
g.name = "Name"

would result in two different keys "Name" and "Name.001" for example?

$\endgroup$
2
+50
$\begingroup$

Here is my idea: Overwrite the attribute setting function of the class. The following code has been created mostly be the OP, based on that idea. I added some minor formatting for better readability:

import bpy
import re
from bpy.props import *

unique_names = ['name']

class RenderGroups(bpy.types.PropertyGroup):
    image = StringProperty(
            name="Image", 
            description="The image-texture of this group")

    def __setattr__(self, name, value): 

        def collection_from_element(self):
            # this gets the collection that the element is in
            path = self.path_from_id()
            match = re.match('(.*)\[\d*\]', path)
            parent = self.id_data
            try:
                coll_path = match.group(1)
            except AttributeError:
                raise TypeError("Propery not element in a collection.") 
            else:
                return parent.path_resolve(coll_path)

        def new_val(stem, nbr):
            # simply for formatting
            return '{st}.{nbr:03d}'.format(st=stem, nbr=nbr)

        # =====================================================

        if name not in unique_names:
            # don't want to handle
            self[name] = value
            return
        if value == getattr(self, name):
            # check for assignement of current value
            return

        coll = collection_from_element(self)
        if value not in coll:
            # if value is not in the collection, just assign
            self[name] = value
            return

        # see if value is already in a format like 'name.012'
        match = re.match('(.*)\.(\d{3,})', value)
        if match is None:
            stem, nbr = value, 1
        else:
            stem, nbr = match.groups()

        # check for each value if in collection
        new_value = new_val(stem, nbr)
        while new_value in coll:
            nbr += 1
            new_value = new_val(stem, nbr)
        self[name] = new_value

class MHFCMeshProps(bpy.types.PropertyGroup):
    render_groups = CollectionProperty(
            type=RenderGroups,
            name="Render Groups")

if __name__ == "__main__":
    bpy.utils.register_class(RenderGroups)
    bpy.utils.register_class(MHFCMeshProps)
    bpy.types.Object.testprop = PointerProperty(type=MHFCMeshProps)

Now you can do this in the blender python console:

obj=bpy.context.object
g=obj.testprop .render_groups.add()
g.name, g.whatever="testme", "keepme"
g.name, g['whatever']
('testme', 'keepme')

g=obj.testprop .render_groups.add()
g.name, g.whatever="testme", "keepme"
g.name, g['whatever']
('testme.001', 'keepme')

I think the code example above is now pretty much complete. Although there is still one caveat here. This keeps track of reassigning the current value and always assigns the lowest nbr possible.

$\endgroup$
  • $\begingroup$ That is a great approach and possibly solves the issue. Will try to think of a solution to the "one caveat" you are talking about because that would feel a bit weird for the user. $\endgroup$ – WorldSEnder Sep 18 '14 at 9:00
  • $\begingroup$ I added a quick check for avoiding to reassign the same value twice. A better solution could be to store the list of appended numbers (001, 002, ...) in the value_dict instead of only the counter (id). Then when assigning a new value, the old value's appended number could be removed from the value_dict, then the new value's number can be calculated by iterating the value_dict and search for the first "free" entry. Do you want me to change the example code to show the idea in action ? $\endgroup$ – Gaia Clary Sep 18 '14 at 11:42
  • $\begingroup$ Rewritten the script myself based on your idea with __setattr__(). Works fine the way it is, maybe you can find a way to improve or speed it up $\endgroup$ – WorldSEnder Sep 18 '14 at 12:36
  • $\begingroup$ i am not sure if this is really working correct. create a testprop and name it g.name="testme", then create another testprops and name it g.name="testme" the second testprop is now named "testme.001" as expected. now rename the second testprop again g.name="testme" now the name is changed to "testme.002" while i expected it to remain "testme.001". BTW i believe there is a bug in one of the regular expressions. Around line 40: Shouldn't it belike this ? match = re.match('(.*)\.(\d{3,})', value) $\endgroup$ – Gaia Clary Sep 18 '14 at 13:28
  • $\begingroup$ oops, i fixed the bug as well and made some minor changes to improve readability. $\endgroup$ – Gaia Clary Sep 18 '14 at 13:41
0
$\begingroup$

There is no way with the api I think. I would suggest to implement your own function:

#collection:    collection, where you want to add a new entry
#name:          the 'original' name, will maybe modified
def add_new(collection, name): #or add_unique is maybe better
    #check if name allready exists in the given collection
    #if not add it directly
    if name in collection:
        try:
            #check if the name has allready a number
            #if not this will produce an error and except will be executed
            if name[-4:-3] == '.': #check for a point
                int(name[-3:])
                #cut out the 'original' name
                #([:-4] remove last 4 chars)
                name = name[:-4]
        except:
            #if its not an integer, do nothing
            pass

        i = 1
        #generate three chars with '0' ('{:0>3}') and
        #replace it with the next number (.format)
        #until a free number is found
        while (name + '.{:0>3}'.format(str(i))) in collection:
            #otherwise continue counting
            i += 1
        #add number to name
        name = name + '.{:0>3}'.format(str(i))
    #finally add item
    itm = collection.add()
    # ... and assign name
    itm.name = name
    #return that item
    return itm

Then you can use this in your code:

g = add_new(obj.testgroup.render_groups, "Name")
#->g.name= "Name"

g = add_new(obj.testgroup.render_groups, "Name")
#->g.name= "Name.001"

g = add_new(obj.testgroup.render_groups, "Name.001")
#->g.name= "Name.002" not "Name.001.001"
$\endgroup$
  • $\begingroup$ hm, is there a way I don't have to call this function every time I want to add an object, though? As a member of a group of programmers they may forget to use this. $\endgroup$ – WorldSEnder Sep 15 '14 at 20:13
  • $\begingroup$ maybe, but thats beyond my capabilities $\endgroup$ – Leon95 Sep 15 '14 at 20:17
  • $\begingroup$ EDIT: added a line to check for a point before the number $\endgroup$ – Leon95 Sep 15 '14 at 20:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.