Looking for a DirectGUI color picker

Alright, I made a DirectColorPicker class that wraps several DirectGui objects into a rather basic - but fully functional - color picker object. You can create one by clicking any of the 3 “Color me” buttons (only if there isn’t one already for that button). You can drag it around using the left mouse button on an empty space on the color picker’s frame. Click the color spectrum to select the color at the clicked pixel. Use the slider to change the color’s brightness. Click the OK button to apply the selected color to the corresponding “Color me” button.

Here’s the code:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.DirectGui import *



class ColorSpectrum(object):

    def __init__(self, app, click_handler, **kwargs):

        self._app = app
        self._frame = DirectFrame(**kwargs)
        self._marker = DirectFrame(parent=self._frame,
                                    frameColor=(0., 0., 0., 1.),
                                    frameSize=(-.06, .06, -.06, .06),
                                    pos=(-.89, 0., -.89))
        self._marker_center = DirectFrame(parent=self._marker,
                                          frameColor=(.5, .5, .5, 1.),
                                          frameSize=(-.02, .02, -.02, .02))
        
        texture_filename = self._frame['image']
        self._palette_img = img = PNMImage(Filename.fromOsSpecific(texture_filename))
        width = img.getReadXSize()
        height = img.getReadYSize()
        self._palette_size = (width, height)
        self._frame['state'] = DGG.NORMAL
        self._frame.bind(DGG.B1PRESS, command=click_handler)
        

    def getColorUnderMouse(self, update_marker=False):
        
        if not self._app.mouseWatcherNode.hasMouse():
            return
        
        x, y = self._app.mouseWatcherNode.getMouse()
        win_w, win_h = self._app.win.getSize()
        width, height = self._palette_size

        if win_w < win_h:
            y *= 1. * win_h / win_w
        else:
            x *= 1. * win_w / win_h

        screen = self._app.aspect2d
        x -= self._frame.getX(screen)
        y -= self._frame.getZ(screen)
        img_scale = self._frame['image_scale']
        sx = self._frame.getSx(screen) * img_scale[0]
        sy = self._frame.getSz(screen) * img_scale[2]
        marker_x = max(-.89, min(.89, x / sx))
        marker_y = max(-.89, min(.89, y / sy))
        x = (.5 + x / (2. * sx)) * width
        y = (.5 + y / -(2. * sy)) * height

        if 0 <= x < width and 0 <= y < height:

            r, g, b = color = self._palette_img.getXel(int(x), int(y))
            
            if update_marker:
                self._marker_center['frameColor'] = (r, g, b, 1.)
                self._marker.setPos(marker_x, 0., marker_y)

            return color


    def setMarkerCoord(self, coord):

        x, y = coord
        marker_x = max(-.89, min(.89, x))
        marker_y = max(-.89, min(.89, y))
        self._marker.setPos(marker_x, 0., marker_y)


    def getMarkerCoord(self):

        marker_x, y, marker_y = self._marker.getPos()

        return (marker_x, marker_y)


    def setMarkerColor(self, color):

        r, g, b = color
        self._marker_center['frameColor'] = (r, g, b, 1.)



class ColorSwatch(object):

    def __init__(self, parent, color=None):

        self._frame = DirectFrame(parent=parent,
                                  relief=DGG.SUNKEN,
                                  borderWidth=(.05, .05),
                                  frameColor=(.3, .3, .3, 1.),
                                  frameSize=(-.4, .4, -.3, .3),
                                  scale=(.5, 1., .5))
        self._swatch = DirectFrame(parent=self._frame,
                                    frameColor=color if color else (.5, .5, .5, 1.),
                                    frameSize=(-.35, .35, -.25, .25))
        self._color = color


    def setColor(self, color=None):

        self._swatch['frameColor'] = color if color else (.5, .5, .5, 1.)
        self._color = color


    def getColor(self):

        return self._color


    def setPos(self, *pos):

        self._frame.setPos(*pos)



class ColorData(object):

    def __init__(self, color=None, brightness=None, spectrum_coord=None):

        self._color = color if color else Vec3(.5, .5, .5)
        self._brightness = .5 if brightness is None else brightness
        self._coord = spectrum_coord if spectrum_coord else (-1., -1.)


    def copy(self):

        color_data = ColorData(self._color, self._brightness, self._coord)

        return color_data


    def setColor(self, color):

        self._color = color


    def getColor(self):

        return self._color


    def setBrightness(self, brightness):

        self._brightness = brightness


    def getBrightness(self):

        return self._brightness


    def getColorShade(self):
            
        if self._brightness < .5:
            color = self._color
        else:
            color = Vec3(1., 1., 1.) - self._color

        color = color * 2. * (self._brightness - .5)
        color += self._color
        r, g, b = color

        return VBase4(r, g, b, 1.)


    def setCoord(self, coord):

        self._coord = coord


    def getCoord(self):

        return self._coord



class DirectColorPicker(object):

    def __init__(self, app, picker_command, on_destroy, selected_color=None, **kwargs):

        if 'frameSize' not in kwargs:
            kwargs['frameSize'] = (-.8, .8, -1., 1.)

        self._app = app
        self._on_destroy = on_destroy
        self._frame = DirectFrame(**kwargs)
        self._frame['state'] = DGG.NORMAL
        self._frame.bind(DGG.B1PRESS, command=self.__startDrag)
        self._frame.bind(DGG.B1RELEASE, command=self.__stopDrag)
        
        spectrum_filename = 'color_spectrum.png'
        self._spectrum = ColorSpectrum(app,
                                        self.__handleColorSelection,
                                        parent=self._frame,
                                        relief=DGG.SUNKEN,
                                        borderWidth=(.05, .05),
                                        image=spectrum_filename,
                                        image_scale=(.95, 1., .95),
                                        frameColor=(.3, .3, .3, 1.),
                                        frameSize=(-1., 1., -1., 1.),
                                        pos=(-.15, 0., .3),
                                        scale=(.5, 1., .5))
        
        self._selected_color = selected_color if selected_color else ColorData()
        self._spectrum.setMarkerCoord(self._selected_color.getCoord())
        self._spectrum.setMarkerColor(self._selected_color.getColor())
        
        def updateBrightness():
        
            self._selected_color.setBrightness(self._slider['value'])
            self._swatch_selected.setColor(self._selected_color.getColorShade())
        
        brightness_filename = 'brightness.png'
        brightness = self._selected_color.getBrightness()
        self._slider = DirectSlider(parent=self._frame,
                                    orientation=DGG.VERTICAL,
                                    relief=DGG.SUNKEN,
                                    borderWidth=(.05, .05),
                                    image=brightness_filename,
                                    image_scale=(.05, 1., .95),
                                    frameColor=(.3, .3, .3, 1.),
                                    frameSize=(-.4, .4, -1., 1.),
                                    thumb_frameSize=(-.2, .2, -.1, .1),
                                    command=updateBrightness,
                                    value=brightness,
                                    pos=(.55, 0., .3),
                                    scale=(.5, 1., .5))
                                    
        self._swatch_selected = ColorSwatch(self._frame)
        self._swatch_selected.setPos(-.45, 0., -.4)
        self._swatch_current = ColorSwatch(self._frame)
        self._swatch_current.setPos(.15, 0., -.4)
        
        self._task = app.taskMgr.add(self.__showColorUnderMouse, 'show_color')
        self._drag_start = Point2() # used when dragging the color picker frame
        self._drag_offset = Vec2() # used when dragging the color picker frame
        
        def onOK():
        
            self.destroy()
            picker_command(self._selected_color)
        
        self._btn_ok = DirectButton(parent=self._frame,
                                    borderWidth=(.05, .05),
                                    frameColor=(.35, .35, .35, 1.),
                                    frameSize=(-.3, .3, -.15, .15),
                                    command=onOK,
                                    text='OK',
                                    text_pos=(0., -.03),
                                    text_scale=(.12, .12),
                                    pos=(-.35, 0., -.75))
        self._btn_cancel = DirectButton(parent=self._frame,
                                        borderWidth=(.05, .05),
                                        frameColor=(.35, .35, .35, 1.),
                                        frameSize=(-.3, .3, -.15, .15),
                                        command=self.destroy,
                                        text='Cancel',
                                        text_pos=(0., -.03),
                                        text_scale=(.12, .12),
                                        pos=(.35, 0., -.75))


    def destroy(self):

        self._task.remove()
        del self._task
        self._frame.destroy()
        del self._frame
        self._on_destroy()


    def __handleColorSelection(self, *args):

        color = self._spectrum.getColorUnderMouse(update_marker=True)

        if color:
            coord = self._spectrum.getMarkerCoord()
            self._selected_color.setCoord(coord)
            self._selected_color.setColor(color)
            self._swatch_selected.setColor(self._selected_color.getColorShade())


    def __showColorUnderMouse(self, task):

        color = self._spectrum.getColorUnderMouse()

        if color:
            r, g, b = color
            self._swatch_current.setColor(VBase4(r, g, b, 1.))

        return task.cont


    def __drag(self, task):
        
        if not self._app.mouseWatcherNode.hasMouse():
            return task.cont
        
        x, y = self._app.mouseWatcherNode.getMouse()
        win_w, win_h = self._app.win.getSize()

        if win_w < win_h:
            y *= 1. * win_h / win_w
        else:
            x *= 1. * win_w / win_h
        
        pos = Point2(x, y)
        x, z = pos + self._drag_offset
        self._frame.setPos(x, 0., z)
        self._drag_start = Point2(pos)

        return task.cont


    def __startDrag(self, *args):
        
        x, y = self._app.mouseWatcherNode.getMouse()
        win_w, win_h = self._app.win.getSize()

        if win_w < win_h:
            y *= 1. * win_h / win_w
        else:
            x *= 1. * win_w / win_h
        
        self._drag_start = Point2(x, y)
        x, y, z = self._frame.getPos()
        self._drag_offset = Point2(x, z) - self._drag_start
        self._app.taskMgr.add(self.__drag, 'drag_color_picker')


    def __stopDrag(self, *args):
        
        self._app.taskMgr.remove('drag_color_picker')



class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.btns = []
        self.btn_colors = []
        self.col_pickers = {}

        for i in range(3):
            getCommand = lambda btn_id: lambda: self.createColorPicker(btn_id)
            btn = DirectButton(borderWidth=(.05, .05),
                                frameColor=(.5, .5, .5, 1.),
                                frameSize=(-.4, .4, -.15, .15),
                                command=getCommand(i),
                                text='Color me',
                                text_pos=(0., -.03),
                                text_scale=(.12, .12),
                                pos=(.85 * i - .85, 0., -.65))
            self.btns.append(btn)
            color_data = ColorData(Vec3(.5, .5, .5))
            self.btn_colors.append(color_data)
            self.col_pickers[i] = None
    
    
    def setColor(self, btn_id, color_data):
    
        self.btns[btn_id]['frameColor'] = color_data.getColorShade()
        self.btn_colors[btn_id] = color_data.copy()
    
    
    def removeColorPicker(self, btn_id):
    
        self.col_pickers[btn_id] = None


    def createColorPicker(self, btn_id):
        
        if not self.col_pickers[btn_id]:
            color_data = self.btn_colors[btn_id]
            col_picker = DirectColorPicker(self,
                                            lambda color: self.setColor(btn_id, color),
                                            lambda: self.removeColorPicker(btn_id),
                                            color_data.copy(),
                                            relief=DGG.RAISED,
                                            borderWidth=(.05, .05),
                                            frameColor=(.5, .5, .5, 1.),
                                            pos=(.85 * btn_id - .85, 0., .35),
                                            scale=(.5, 1., .5))
            self.col_pickers[btn_id] = col_picker



app = MyApp()
app.run()

And these are the images I used:


Hope it will be useful.