GUI and

I’m rebuilding the GUI for a plugin and I’m running across an issue that I can’t seem to figure out. I’m assuming it’s because of my own inexperience (I’m still learning), and I’ve honestly spent a ton of time trying to figure it out myself. But now I’m asking for help, maybe it’s something super simple.

This is how I’m rebuilding the GUI. Reason being, that I added more tools which made the previous layout start to seem cluttered to me. I just wanted to clean it up a little more to make it easier to work with. So I’ve worked it into the image below and I’m quite liking it. The buttons on this menu toggle the visibility of a particular tools options and run button. Each button is a custom Boolean Property.


Now the part where I’m running into issues is that I only want one of the buttons to be in the “On” state (or True) at any time. Clicking a new button would deselect the old button. As it works now, each button (which is a bool prop) can be set to be on. That makes all the tools options just keep stacking below, this gets cluttered very quickly. I only want one tool set to be visible at a time.

So ideally it would work almost exactly like the EnumProperty, which is a single property with multiple items. Problem with EnumProperty is that I don’t think you can add an icon to the Enum button or spread buttons out over multiple rows and columns. It’s important for me to keep the buttons with icons in the layout above, which enum won’t allow (as far as I can tell).

This code below is the menu and should work just copy/paste run (creates panel in toolbar menu. My attempt to make only one button, allowed to be “True”, fails horribly. That is the function “deselect_others(prop_name)”, which checks if a button is true, then sets all others to false. This did not work like and is probably sloppy anyways. That function is commented out so the code actually runs (it’s under each property). So my ultimate question, if it’s possible. How do I make it so this group of buttons can only have 1 selected at a time?

import bpy, selection_utils
from bpy.props import FloatProperty, EnumProperty, IntProperty, BoolProperty, FloatVectorProperty


# Class to define properties
class MyProps(bpy.types.PropertyGroup):
    enabled = IntProperty(default=0)

    tool_objectTrace = BoolProperty(name="Object Trace", default=False, description="Trace selected mesh object with a curve")
    tool_objectsConnect = BoolProperty(name="Objects Connect", default=False, description="Connect objects with a curve controlled by hooks")
    tool_particleTrace = BoolProperty(name="Particle Trace", default=False, description="Trace particle path with a  curve")
    tool_meshFollow = BoolProperty(name="Mesh Follow", default=False, description="Follow selection items on animated mesh object")
    tool_particleConnect = BoolProperty(name="Particle Connect", default=False, description="Connect particles with a curves and animated over particle lifetime")
    tool_handwrite = BoolProperty(name="Handwriting", default=False, description="Create and Animate curve using the grease pencil")
    tool_fcurve = BoolProperty(name="F-Curve Noise", default=False, description="Add F-Curve noise to selected objects")
    animation_settings = BoolProperty(name="Animation Settings", default=False, description="Show the settings for the Animations")


# Start Panel
class addMyPanel(bpy.types.Panel):
    bl_label = "Menu"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_context = 'objectmode'

    def draw(self, context):
        layout = self.layout
        mypropslist = bpy.context.window_manager.custom_props

        def deselect_others(prop_name):  #This is the attempt to deselect the others
            mytools = [mypropslist.tool_objectTrace, mypropslist.tool_objectsConnect, mypropslist.tool_meshFollow, mypropslist.tool_handwrite, mypropslist.tool_particleTrace, mypropslist.tool_particleConnect, mypropslist.animation_settings, mypropslist.tool_fcurve]
            mytoolsnames = ['tool_objectTrace', 'tool_objectsConnect', 'tool_meshFollow', 'tool_handwrite', 'tool_particleTrace', 'tool_particleConnect', 'animation_settings', 'tool_fcurve']
            if mypropslist[prop_name]:
                index = mytoolsnames.index(prop_name)
                mytoolsnames.pop(index)
                for i in mytoolsnames:
                    mypropslist[i] = False

        col = self.layout.column(align=True)
        row = col.row()
        row.prop(mypropslist, "tool_objectTrace", text="Ojbect Trace", icon="FORCE_MAGNETIC")
        # deselect_others("tool_objectTrace")  # Deselect all others if true
        row.prop(mypropslist, "tool_objectsConnect", text="Object Connect", icon="OUTLINER_OB_EMPTY")
        # deselect_others("tool_objectsConnect")  # Deselect all others if true
        row = col.row()
        row.prop(mypropslist, "tool_meshFollow", text="Mesh Follow", icon="DRIVER")
        # deselect_others("tool_meshFollow")  # Deselect all others if true
        row.prop(mypropslist, "tool_handwrite", text="Handwriting", icon='BRUSH_DATA')
        # deselect_others("tool_handwrite")  # Deselect all others if true
        row = col.row()   
        row.prop(mypropslist, "tool_particleTrace", icon="PARTICLES", text="Particle Trace")
        # deselect_others("tool_particleTrace")  # Deselect all others if true
        row.prop(mypropslist, "tool_particleConnect", icon="MOD_PARTICLES", text="Particle Connect")
        # deselect_others("tool_particleConnect")  # Deselect all others if true
        row = col.row()
        row.prop(mypropslist, "animation_settings", icon="META_BALL", text="Grow Animation")
        # deselect_others("animation_settings")  # Deselect all others if true
        row.prop(mypropslist, "tool_fcurve", text="Fcurve Noise", icon='RNDCURVE')
        # deselect_others("tool_fcurve")  # Deselect all others if true

### Define Classes to register
classes = [MyProps,
    addMyPanel
    ]

def register():
    for c in classes:
        bpy.utils.register_class(c)
    bpy.types.WindowManager.custom_props = bpy.props.PointerProperty(type=MyProps)
def unregister():
    for c in classes:
        bpy.utils.unregister_class(c)
    del bpy.types.WindowManager.custom_props
if __name__ == "__main__":
    register()

Any advice would be much appreciated! :o

I like the propertygroup… been doing something similar with a vectorbool, but you don’t get the nice descriptions on each. I approach this by putting a selected prop on the panel and change that with an update


import bpy, selection_utils
from bpy.props import FloatProperty, EnumProperty, IntProperty, BoolProperty, FloatVectorProperty
def deselect_others(ob,context):  #This is the attempt to deselect the others
   
    selected = addMyPanel.selected
    ob[selected] = False
    keys = [key for key in ob.keys() if ob[key]]  # all the True keys
    if len(keys) <= 0:
        ob[selected] = True  # reselect
        print("NONE")
        return None
    
    for key in keys:
        print("change")
        addMyPanel.selected = key
        ob[key] = True


# Class to define properties
class MyProps(bpy.types.PropertyGroup):
    enabled = IntProperty(default=0)

    tool_objectTrace = BoolProperty(name="Object Trace", default=True, description="Trace selected mesh object with a curve", update=deselect_others)
    tool_objectsConnect = BoolProperty(name="Objects Connect", default=False, description="Connect objects with a curve controlled by hooks", update=deselect_others)
    tool_particleTrace = BoolProperty(name="Particle Trace", default=False, description="Trace particle path with a  curve", update=deselect_others)
    tool_meshFollow = BoolProperty(name="Mesh Follow", default=False, description="Follow selection items on animated mesh object", update=deselect_others)
    tool_particleConnect = BoolProperty(name="Particle Connect", default=False, description="Connect particles with a curves and animated over particle lifetime", update=deselect_others)
    tool_handwrite = BoolProperty(name="Handwriting", default=False, description="Create and Animate curve using the grease pencil", update=deselect_others)
    tool_fcurve = BoolProperty(name="F-Curve Noise", default=False, description="Add F-Curve noise to selected objects", update=deselect_others)
    animation_settings = BoolProperty(name="Animation Settings", default=False, description="Show the settings for the Animations", update=deselect_others)





# Start Panel
class addMyPanel(bpy.types.Panel):
    bl_label = "Menu"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_context = 'objectmode'
    selected = "tool_objectTrace"
    def draw(self, context):
        layout = self.layout
       
        mypropslist = props = context.window_manager.custom_props  



        col = self.layout.column(align=True)
        row = col.row()
        row.prop(mypropslist, "tool_objectTrace", text="Ojbect Trace", icon="FORCE_MAGNETIC")
        # deselect_others("tool_objectTrace")  # Deselect all others if true
        row.prop(mypropslist, "tool_objectsConnect", text="Object Connect", icon="OUTLINER_OB_EMPTY")
        # deselect_others("tool_objectsConnect")  # Deselect all others if true
        row = col.row()
        row.prop(mypropslist, "tool_meshFollow", text="Mesh Follow", icon="DRIVER")
        # deselect_others("tool_meshFollow")  # Deselect all others if true
        row.prop(mypropslist, "tool_handwrite", text="Handwriting", icon='BRUSH_DATA')
        # deselect_others("tool_handwrite")  # Deselect all others if true
        row = col.row()   
        row.prop(mypropslist, "tool_particleTrace", icon="PARTICLES", text="Particle Trace")
        # deselect_others("tool_particleTrace")  # Deselect all others if true
        row.prop(mypropslist, "tool_particleConnect", icon="MOD_PARTICLES", text="Particle Connect")
        # deselect_others("tool_particleConnect")  # Deselect all others if true
        row = col.row()
        row.prop(mypropslist, "animation_settings", icon="META_BALL", text="Grow Animation")
        # deselect_others("animation_settings")  # Deselect all others if true
        row.prop(mypropslist, "tool_fcurve", text="Fcurve Noise", icon='RNDCURVE')
        # deselect_others("tool_fcurve")  # Deselect all others if true

### Define Classes to register
classes = [MyProps,
    addMyPanel
    ]

def register():
    for c in classes:
        bpy.utils.register_class(c)
    bpy.types.WindowManager.custom_props = bpy.props.PointerProperty(type=MyProps, update=deselect_others)
def unregister():
    for c in classes:
        bpy.utils.unregister_class(c)
    del bpy.types.WindowManager.custom_props
if __name__ == "__main__":
    register()

What about something like this. Basically use an operator as your buttons for the various tool types but pass an index along with it. Then you have a single click processor for the entire button set. An EnumProperty defines the tool index. The draw routine looks at the index to determine which IF block to display for the active tool. When a tool is switched to another tool, the click processor assigns the index it received back to the EnumProperty so it can draw the new tool set. Use ICONS for the enum button to give visual feedback based upon which tool is selected. I’m not sure how to make an operator look selected, but maybe you could use that for visual feedback as well.


import bpy, selection_utils
from bpy.props import FloatProperty, EnumProperty, IntProperty, BoolProperty, FloatVectorProperty

# A generic operator that accepts input from the caller.
class OBJECT_OT_genericButton(bpy.types.Operator):
    bl_label = "Generic Button"
    bl_idname = "generic.button"
    index = bpy.props.IntProperty(default = 0)
    
    def invoke(self, context, event):
        index = self.index
        ob = context.object
        if ob != None:
            print ("Generic Button Received " + str(index))
            mypropslist = bpy.context.window_manager.custom_props
            mypropslist.active_tool = str(index)
        return {'FINISHED'}

# Class to define properties
class MyProps(bpy.types.PropertyGroup):
    enabled = IntProperty(default=0)
    tool_types = [
                ("0","objectTrace","Object Trace"),
                ("1","objectsConnect","Objects Connect"),
                ("2","particleTrace","Particle Trace"),
                ("3","meshFollow","Mesh Follow"),
                ("4","particleConnect","Particle Connect"),
                ("5","handWrite","Hand Writing"),
                ("6","fCurve","F-Curve"),
                ("7","animationSettings","Animation Settings"),
                ]
    active_tool= EnumProperty(name="Active Tool", description="Tools", default="0", items=tool_types)


# Start Panel
class addMyPanel(bpy.types.Panel):
    bl_label = "Atom's Menu"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_context = 'objectmode'

    def draw(self, context):
        layout = self.layout
        col = self.layout.column(align=True)
        row = col.row()
        
        mypropslist = bpy.context.window_manager.custom_props
        #print(mypropslist)
        tool_index = int(mypropslist.active_tool[0])
        print(tool_index)
        
        if tool_index == 0:
            row.operator("generic.button", icon="FORCE_MAGNETIC", text="").index = 0
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 1
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 2
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 3
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 4
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 5
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 6
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 7
            # Add additional controls here for this tool type.
        if tool_index == 1:
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 0
            row.operator("generic.button", icon="OUTLINER_OB_EMPTY", text="").index = 1
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 2
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 3
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 4
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 5
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 6
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 7
            # Add additional controls here for this tool type.
        if tool_index == 2:
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 0
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 1
            row.operator("generic.button", icon="DRIVER", text="").index = 2
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 3
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 4
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 5
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 6
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 7
            # Add additional controls here for this tool type.
        if tool_index == 3:
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 0
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 1
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 2
            row.operator("generic.button", icon="BRUSH_DATA", text="").index = 3
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 4
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 5
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 6
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 7
            # Add additional controls here for this tool type.
        if tool_index == 4:
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 0
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 1
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 2
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 3
            row.operator("generic.button", icon="PARTICLES", text="").index = 4
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 5
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 6
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 7
            # Add additional controls here for this tool type.
        if tool_index == 5:
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 0
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 1
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 2
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 3
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 4
            row.operator("generic.button", icon="MOD_PARTICLES", text="").index = 5
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 6
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 7
            # Add additional controls here for this tool type.
        if tool_index == 6:
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 0
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 1
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 2
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 3
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 4
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 5
            row.operator("generic.button", icon="META_BALL", text="").index = 6
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 7
            # Add additional controls here for this tool type.
        if tool_index == 7:
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 0
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 1
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 2
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 3
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 4
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 5
            row.operator("generic.button", icon="ZOOMOUT", text="").index = 6
            row.operator("generic.button", icon="RNDCURVE", text="").index = 7
            # Add additional controls here for this tool type.

### Define Classes to register
classes = [MyProps,
    addMyPanel
    ]

def register():
    for c in classes:
        bpy.utils.register_class(c)
    bpy.types.WindowManager.custom_props = bpy.props.PointerProperty(type=MyProps)
    bpy.utils.register_class(OBJECT_OT_genericButton)
    
def unregister():
    for c in classes:
        bpy.utils.unregister_class(c)
    del bpy.types.WindowManager.custom_props
    bpy.utils.unregister_class(OBJECT_OT_genericButton)
    
#if __name__ == "__main__":
print("Registering...")
register()

The image shows one possible way to setup the feedback via icons.

Attachments


@batFINGER and Atom, thank you so much for these samples!!! I’m away from my computer right now, but hopefully I can give this a try tomorrow (traveling for a wedding). Just reading over these seem to make sense, can’t wait to play around with it. Thanks again!

indeed nice examples … :wink:

Just had a chance to look at these more and play with the scripts inside blender. My brain hurts, I think I just learned something… :slight_smile:

I think think batFINGERS code achieve the exact functionality that I want. But Atom’s code is really cool and opens up some ideas for me. I think I’ll be using that system on another section here in the future. Thanks again so much guys for taking the time to help me out with this. This community rocks.