4
$\begingroup$

For clarity, the previous question I asked was closed so I made this one, its now reopened and both are asking basically the same question.

I have a fixed list of RGB values which I want to display to the user inside a panel, when they click on a color I need to run an operator. In a previous question I asked (which was poorly formulated by me) I got a reply from @pyCod3R which showed a palette item which visually looks like what I want but has a few limitations.

asdf

Image showing the template_node_socket and palette created by the code below

  • The list is a fixed length, in the palette there are +, - signs allowing the user to add/remove indices to the list which would need to be disabled for it to be a viable option in this case
  • When a color is clicked by the user I need to run an operator, which doesn't seem to be possible with a palette
  • Ideally the coloured squares would be larger than the small circles created by template_node_socket, the palette squares are perfect in size

This code is adapted from the answer @pyCod3R gave in the previous question:

import bpy
import random


class ColorDemoPanel(bpy.types.Panel):
    """Creates a Panel in the 3d viewport"""
    bl_label = "Color List Display"
    bl_idname = "MATERIAL_PT_colorlist"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    def draw(self, context):
        layout = self.layout

        #the template_node_socket way to display a color is very limited
        #the color is a small circle and cant be clicked so I have to add a separate button
        for i in range(5):
            row = layout.row(align = True)
            r = random.random()
            g = random.random()
            b = random.random()
            row.template_node_socket(color=(r, g, b, 1.0))
            #just a demo operator, not what I'd actually use
            row.operator("object.transform_apply", text = "Color")

        #the palette way is a lot cleaner visually but I need the list to be of fixed length
        #buttons allowing the user to add or remove indices from the list isnt possible to remove afaik
        #there doesnt seem to be a way to call a function when a color is clicked either, which I need
        ts = context.tool_settings
        palette = ts.image_paint.palette
        sel_r, sel_g, sel_b = palette.colors.active.color
        
        if ts.image_paint.palette:
            layout.template_palette(ts.image_paint, "palette", color=True)
            

def register():
    bpy.utils.register_class(ColorDemoPanel)

def unregister():
    bpy.utils.unregister_class(ColorDemoPanel)


if __name__ == "__main__":
    register()
    
    # Add a new pallete
    pal = bpy.data.palettes.get("CustomPalette")
    if pal is None:
        pal = bpy.data.palettes.new("CustomPalette")

        # Add a single color
        red = pal.colors.new()
        red.color = (1, 0, 0)

        # Add random colors to the palette
        from random import random
        for i in range(10):
            col = pal.colors.new()
            col.color = (random(), random(), random())

        # Make red the active/selected color
        pal.colors.active = red

    ts = bpy.context.tool_settings   
    ts.image_paint.palette = pal

Is there some way to adapt a palette or template_node_socket to my needs or is there some other way to display clickable RGB squares that I've missed?

$\endgroup$
2

2 Answers 2

6
$\begingroup$

As has been mentioned elsewhere, template_palette is implemented in C as uiTemplatePalette and that function does not take any arguments, so it is not possible without writing new C code to use template_palette with editing disabled.

A second approach suggested in an answer to your previous question relies on custom icons. That approach could be made to work but it requires the extra overhead to externally create the icons, include them with an add-on, and so forth.

Here is the outline of a third approach which is similar to the custom icon approach but relies instead on images having previews that can be used as icons:

  • Create a dictionary of the color values you want to use. Using a dictionary rather than just an array of RGBA values, allows you to name the colors and makes the code slightly more readable.

  • Walk the dictionary and create an image for each entry. The image is just a block of pixels of the color identified in the dictionary. The image's preview ID is then used as the icon of a button. Remember the list of created images

This has the advantage that, as described in this answer, it is possible to generate images on the fly. While this approach to generating images is slower than using image processing software, Blender icons are only 32 x 32 pixels in size.

Having done that it is possible to create an array of buttons, one per palette entry, representing each of the colors in the dictionary. By giving the button a string property and associating each button with the name of a color and using the image id of that color as the button's icon, two things are accomplished:

  • The buttons each display a unique color.
  • In the execute function of the button it is possible to determine the color associated with the button.

This code is just a demonstration of the concept. It doesn't delete the images when the classes are unregistered, and it's not packaged as a function that can be called from a class draw function. It also doesn't lay the buttons out in a grid, although that would be a trivial change to make to the calling code.

To use the code, simply replace the three dictionary entries in the palette dictionary with one entry for each color, in the form of "color name": (red, green, blue, alpha) entries and run it. As written, it displays this panel:

The panel created from the dictionary

and if you push one of the buttons it prints the color name and RGB value. Obviously you would replace the print with the code you want to execute.

import bpy

# This function is derived from code taken
# from https://blender.stackexchange.com/a/652/42221
def new_icon(name, red, green, blue, alpha):
    icon_size = 32
    icon_image = bpy.data.images.new(name, width = icon_size, height = icon_size)
    pixels = [ None ] * icon_size * icon_size
    for x in range(icon_size):
        for y in range(icon_size):
            pixels[(y * icon_size) + x] = [red, green, blue, alpha]
    # Flatten List
    pixels = [chan for px in pixels for chan in px]
    icon_image.pixels = pixels
    return icon_image

palette = {
    "Red": (1.0, 0.0, 0.0, 1.0), 
    "Green": (0.0, 1.0, 0.0, 1.0), 
    "Blue": (0.0, 0.0, 1.0, 1.0), 
}

def make_icons(palette):
    icon_list = []
    for entry in palette:
        rgba = palette[entry]
        new_image = new_icon(entry, rgba[0], rgba[1], rgba[2], rgba[3])
        icon_list.append(new_image)
    return icon_list

icons = make_icons(palette)

class TLA_OT_Icon(bpy.types.Operator):
    """A button per color"""
    bl_idname = "tla.icon"
    bl_label = "Color"
    bl_description = "Pick color"
    bl_options = {'REGISTER', 'UNDO'}
    color: bpy.props.StringProperty(name="color")
    def execute(self, context):
        rgba= palette[self.color]
        self.report({'INFO'}, f"{self.color} {rgba} chosen")
        return {'FINISHED'}

# From https://blender.stackexchange.com/a/48508/42221
class TLA_PT_Icons(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Palette"
    bl_label = "Palette"
    def draw(self, context):
        layout = self.layout
        for image in icons:
            layout.operator("tla.icon", text="", icon_value=image.preview.icon_id).color=image.name
        if not icons:
            layout.label(text="No Colors in Palette")

classes = [
    TLA_OT_Icon,
    TLA_PT_Icons,
]

def register():
    for aclass in classes:
        bpy.utils.register_class(aclass)


def unregister():
    for aclass in classes:
        bpy.utils.unregister_class(aclass)


if __name__ == "__main__":
    register()

sample info window output:

bpy.ops.tla.icon(color="Red")
Red (1.0, 0.0, 0.0, 1.0) chosen
bpy.ops.tla.icon(color="Blue")
Blue (0.0, 0.0, 1.0, 1.0) chosen
bpy.ops.tla.icon(color="Green")
Green (0.0, 1.0, 0.0, 1.0) chosen

Here is an example of how a small change of code and make the layout look more like a palette. Change the layout from one button per row to a row of buttons:

        row = layout.row()
        for image in icons:
            row.operator("tla.icon", text="", icon_value=image.preview.icon_id).color=image.name

results in:

colors in a row

Changing that code a bit further, you can keep track of how many entries are in a row and generate a new row every N entries.

$\endgroup$
5
  • 1
    $\begingroup$ This answer is great, thanks for taking the time! The third approach is the best for my purposes, only unfortunate part is that the images end up in the image browser but that seems unavoidable. $\endgroup$
    – CybranM
    Commented Jan 31, 2022 at 16:42
  • 1
    $\begingroup$ @CybranM Thank you. I looked into hiding the images but couldn't find a way. If I do, I'll update the answer. $\endgroup$ Commented Jan 31, 2022 at 16:43
  • $\begingroup$ The UI doesn't load the images until I open the image browser, is there a command to "refresh" the image browser or similar to force it to load them? Using Blender 3.0 $\endgroup$
    – CybranM
    Commented Feb 1, 2022 at 10:07
  • $\begingroup$ I'm not sure I understand what you mean by 'load' here. What are the symptoms you are seeing? $\endgroup$ Commented Feb 1, 2022 at 14:45
  • $\begingroup$ You can put a dot "." at the front of datablock names to hide them. $\endgroup$ Commented May 4, 2023 at 16:11
4
$\begingroup$

If you want somewhat similar without all the extra elements coming with template_palette() and execute something when clicking on each color, you can implement some sort of dynamic operator for all colors, and only display a custom icon that represents the operator on the panel (as I mentioned in my previous answer).

The icons for the colors can be generated on the fly by adding a new preview collection as you normally would do for custom icons anyway and by assigning a pixel array to each new icon added: https://docs.blender.org/api/current/bpy.types.ImagePreview.html

enter image description here

Example of displaying 3 unique colors based on Ui Previews Custom Icons template.

import bpy

colors = {
    "Red": (1.0, 0.0, 0.0, 1.0),
    "Green": (0.0, 1.0, 0.0, 1.0),
    "Blue": (0.0, 0.0, 1.0, 1.0),
}

class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    
    color: bpy.props.FloatVectorProperty(
             name = "Color Picker",
             subtype = "COLOR",
             size = 4)

    def execute(self, context):
        context.scene.active_color = self.color
        print(self.color[0], self.color[1], self.color[2], self.color[3])
        return {'FINISHED'}


class PreviewsExamplePanel(bpy.types.Panel):
    """Creates a Panel in the Object properties window"""
    bl_label = "Previews Example Panel"
    bl_idname = "OBJECT_PT_previews"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"

    def draw(self, context):
        layout = self.layout
        pcoll = preview_collections["main"]

        row = layout.row(align=True)
        for icon_name, color in colors.items():
            p = row.operator(
                SimpleOperator.bl_idname, 
                icon_value=pcoll[icon_name].icon_id,
                text="", emboss=True)
            p.color = color

        r, g, b, a = context.scene.active_color
        layout.row().label(text=f"Active Color: {r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}")

# We can store multiple preview collections here,
# however in this example we only store "main"
preview_collections = {}

def register():
    
    # Note that preview collections returned by bpy.utils.previews
    # are regular py objects - you can use them to store custom data.
    import bpy.utils.previews
    pcoll = bpy.utils.previews.new()
    
    for name, color in colors.items():
        size = 32, 32
        pixels = [*color] * size[0] * size[1]

        icon = pcoll.new(name)
        icon.icon_size = size
        icon.is_icon_custom = True
        icon.icon_pixels_float = pixels

    preview_collections["main"] = pcoll
    
    bpy.utils.register_class(SimpleOperator)
    bpy.utils.register_class(PreviewsExamplePanel)
    bpy.types.Scene.active_color = bpy.props.FloatVectorProperty(
         name = "Color Picker",
         subtype = "COLOR",
         default = (1.0,1.0,1.0,1.0),
         size = 4)


def unregister():
    for pcoll in preview_collections.values():
        bpy.utils.previews.remove(pcoll)
    preview_collections.clear()

    bpy.utils.unregister_class(SimpleOperator)
    bpy.utils.unregister_class(PreviewsExamplePanel)
    del bpy.Scene.active_color

if __name__ == "__main__":
    register()

... for clarity: the assumptions of the other answer regarding my previous answer are wrong.

A second approach suggested in an answer to your previous question relies on custom icons. That approach could be made to work but it requires the extra overhead to externally create the icons, include them with an add-on, and so forth.

We do not have to create the icons externally (as proven by this code). However, you can do this and it's a valid option too, see: How to implement custom icons for my script/addon? or the template this code is based of.

Since we use the regular approach for custom icons, the icons generated are not added as images to the blend file and do not become visible in the image editor as the code of the other answer does.

$\endgroup$
1
  • $\begingroup$ This was very useful, thank you so much $\endgroup$
    – Ommadawn
    Commented Apr 30, 2023 at 21:02

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .