diff --git a/wx/lib/colourchooser/canvas.py b/wx/lib/colourchooser/canvas.py index 5a94f4cc..2b749c26 100644 --- a/wx/lib/colourchooser/canvas.py +++ b/wx/lib/colourchooser/canvas.py @@ -80,13 +80,16 @@ class Canvas(wx.Window): """ def __init__(self, parent, id, pos=wx.DefaultPosition, - size=wx.DefaultSize, - style=wx.SIMPLE_BORDER): + style=wx.SIMPLE_BORDER, + forceClientSize=None): """Creates a canvas instance and initializes the off-screen buffer. Also sets the handler for rendering the canvas automatically via size and paint calls from the windowing system.""" - wx.Window.__init__(self, parent, id, pos, size, style) + wx.Window.__init__(self, parent, id, pos, style=style) + if forceClientSize: + self.SetMaxClientSize(forceClientSize) + self.SetMinClientSize(forceClientSize) # Perform an intial sizing self.ReDraw() @@ -96,7 +99,7 @@ class Canvas(wx.Window): self.Bind(wx.EVT_PAINT, self.onPaint) def MakeNewBuffer(self): - size = self.GetSize() + size = self.GetClientSize() self.buffer = BitmapBuffer(size[0], size[1], self.GetBackgroundColour()) diff --git a/wx/lib/colourchooser/pycolourbox.py b/wx/lib/colourchooser/pycolourbox.py index a5ecbc90..5c7cb5d2 100644 --- a/wx/lib/colourchooser/pycolourbox.py +++ b/wx/lib/colourchooser/pycolourbox.py @@ -67,6 +67,7 @@ class PyColourBox(wx.Panel): """Sets the box's current couple to the given tuple.""" self.colour = colour self.colour_box.SetBackgroundColour(wx.Colour(*self.colour)) + self.colour_box.Refresh() def Update(self): wx.Panel.Update(self) diff --git a/wx/lib/colourchooser/pycolourchooser.py b/wx/lib/colourchooser/pycolourchooser.py index 940bd514..0e4a75a6 100644 --- a/wx/lib/colourchooser/pycolourchooser.py +++ b/wx/lib/colourchooser/pycolourchooser.py @@ -28,6 +28,7 @@ from __future__ import absolute_import # Tags: phoenix-port import wx +import wx.lib.newevent as newevent from . import pycolourbox from . import pypalette @@ -37,6 +38,19 @@ from . import intl from .intl import _ # _ +ColourChangedEventBase, EVT_COLOUR_CHANGED = newevent.NewEvent() + +class ColourChangedEvent(ColourChangedEventBase): + """Adds GetColour()/GetValue() for compatibility with ColourPickerCtrl and colourselect""" + def __init__(self, newColour): + super().__init__(newColour = newColour) + + def GetColour(self): + return self.newColour + + def GetValue(self): + return self.newColour + class PyColourChooser(wx.Panel): """A Pure-Python implementation of the colour chooser dialog. @@ -169,6 +183,9 @@ class PyColourChooser(wx.Panel): self.palette = pypalette.PyPalette(self, -1) self.colour_slider = pycolourslider.PyColourSlider(self, -1) + self.colour_slider.Bind(wx.EVT_LEFT_DOWN, self.onSliderDown) + self.colour_slider.Bind(wx.EVT_LEFT_UP, self.onSliderUp) + self.colour_slider.Bind(wx.EVT_MOTION, self.onSliderMotion) self.slider = wx.Slider( self, self.idSCROLL, 86, 0, self.colour_slider.HEIGHT - 1, style=wx.SL_VERTICAL, size=(15, self.colour_slider.HEIGHT) @@ -185,7 +202,6 @@ class PyColourChooser(wx.Panel): self.palette.Bind(wx.EVT_LEFT_DOWN, self.onPaletteDown) self.palette.Bind(wx.EVT_LEFT_UP, self.onPaletteUp) self.palette.Bind(wx.EVT_MOTION, self.onPaletteMotion) - self.mouse_down = False self.solid = pycolourbox.PyColourBox(self, -1, size=(75, 50)) slabel = wx.StaticText(self, -1, _("Solid Colour")) @@ -209,6 +225,10 @@ class PyColourChooser(wx.Panel): (slabel, 0, wx.ALIGN_CENTER_VERTICAL), (self.sentry, 0, wx.FIXED_MINSIZE), (vlabel, 0, wx.ALIGN_CENTER_VERTICAL), (self.ventry, 0, wx.FIXED_MINSIZE), ]) + + self.hentry.Bind(wx.EVT_KILL_FOCUS, self.onHSVKillFocus) + self.sentry.Bind(wx.EVT_KILL_FOCUS, self.onHSVKillFocus) + self.ventry.Bind(wx.EVT_KILL_FOCUS, self.onHSVKillFocus) rlabel = wx.StaticText(self, -1, _("R:")) self.rentry = wx.TextCtrl(self, -1) @@ -226,6 +246,10 @@ class PyColourChooser(wx.Panel): (blabel, 0, wx.ALIGN_CENTER_VERTICAL), (self.bentry, 0, wx.FIXED_MINSIZE), ]) + self.rentry.Bind(wx.EVT_KILL_FOCUS, self.onRGBKillFocus) + self.gentry.Bind(wx.EVT_KILL_FOCUS, self.onRGBKillFocus) + self.bentry.Bind(wx.EVT_KILL_FOCUS, self.onRGBKillFocus) + gsizer = wx.GridSizer(rows=2, cols=1, vgap=0, hgap=0) gsizer.SetVGap (10) gsizer.SetHGap (2) @@ -308,18 +332,81 @@ class PyColourChooser(wx.Panel): colour slider and set everything to its original position.""" self.custom_boxs[index].SetColour(true_colour) self.custom_colours[index] = (base_colour, slidepos) + + def setSliderToV(self, v): + """Set a new HSV value for the v slider. Does not update displayed colour.""" + min = self.slider.GetMin() + max = self.slider.GetMax() + val = (1 - v) * max + self.slider.SetValue(val) + + def getVFromSlider(self): + """Get the current value of "V" from the v slider.""" + val = self.slider.GetValue() + min = self.slider.GetMin() + max = self.slider.GetMax() + + # Snap to exact min/max values + if val == 0: + return 1 + if val == max - 1: + return 0 + + return 1 - (val / max) + + def colourToHSV(self, colour): + """Convert wx.Colour to hsv triplet""" + return colorsys.rgb_to_hsv(colour.Red() / 255.0, colour.Green() / 255.0, colour.Blue() / 255.0) + + def hsvToColour(self, hsv): + """Convert hsv triplet to wx.Colour""" + # Allow values to go full range from 0 to 255 + r, g, b = colorsys.hsv_to_rgb(hsv[0], hsv[1], hsv[2]) + + r *= 255.0 + g *= 255.0 + b *= 255.0 + + return wx.Colour(r, g, b) + + def getColourFromControls(self): + """ + Calculate current colour from HS box position and V slider. + return - wx.Colour + """ + # This allows colours to be exactly 0,0,0 or 255, 255, 255 + baseColour = self.colour_slider.GetBaseColour() + h,s,v = self.colourToHSV(baseColour) + v = self.getVFromSlider() + if s < 0.04: # Allow pure white + s = 0 + + return self.hsvToColour((h, s, v)) + + def updateDisplayColour(self, colour): + """Update the displayed color box (solid) and send the EVT_COLOUR_CHANGED""" + self.solid.SetColour(colour) + evt = ColourChangedEvent(newColour=colour) + wx.PostEvent(self, evt) def UpdateColour(self, colour): - """Performs necessary updates for when the colour selection has - changed.""" - # Reset the palette to erase any highlighting - self.palette.ReDraw() - + """Updates displayed colour and HSV controls with the new colour""" # Set the color info - self.solid.SetColour(colour) + self.updateDisplayColour(colour) self.colour_slider.SetBaseColour(colour) self.colour_slider.ReDraw() - self.slider.SetValue(0) + + # Update the Vslider and the HS current selection dot + h,s,v = self.colourToHSV(colour) + self.setSliderToV(v) + + # Convert RGB to (x,y) == (hue, saturation) + + width, height = self.palette.GetSize() + x = width * h + y = height * (1 - s) + self.palette.HighlightPoint(x, y) + self.UpdateEntries(colour) def UpdateEntries(self, colour): @@ -339,46 +426,136 @@ class PyColourChooser(wx.Panel): self.hentry.SetValue("%.2f" % (h)) self.sentry.SetValue("%.2f" % (s)) self.ventry.SetValue("%.2f" % (v)) + + def onColourSliderClick(self, y): + """Shared helper for onSliderDown()/onSliderMotion()""" + v = self.colour_slider.GetVFromClick(y) + self.setSliderToV(v) + + # Now with the slider updated, update all controls + colour = self.getColourFromControls() + + self.updateDisplayColour(colour) # Update display + self.UpdateEntries(colour) + + # We don't move on the palette... + + # width, height = self.palette.GetSize() + # x = width * h + # y = height * (1 - s) + # self.palette.HighlightPoint(x, y) + + def onSliderDown(self, event): + """Handle mouse click on the colour slider palette""" + self.onColourSliderClick(event.GetY()) + self.colour_slider.CaptureMouse() + + def onSliderUp(self, event): + self.colour_slider.ReleaseMouse() + + def onSliderMotion(self, event): + """Handle mouse-down drag on the colour slider palette""" + if event.LeftIsDown(): + self.onColourSliderClick(event.GetY()) def onPaletteDown(self, event): """Stores state that the mouse has been pressed and updates the selected colour values.""" - self.mouse_down = True - self.palette.ReDraw() self.doPaletteClick(event.GetX(), event.GetY()) + + # Prevent mouse from leaving window, so that we will also get events + # when mouse is dragged along the edges of the rectangle. + self.palette.CaptureMouse() def onPaletteUp(self, event): """Stores state that the mouse is no longer depressed.""" - self.mouse_down = False + self.palette.ReleaseMouse() # Must call once for each CaputreMouse() def onPaletteMotion(self, event): """Updates the colour values during mouse motion while the mouse button is depressed.""" - if self.mouse_down: + if event.LeftIsDown(): self.doPaletteClick(event.GetX(), event.GetY()) + + def onPaletteCaptureLost(self, event): + pass # I don't think we have to call ReleaseMouse in this event def doPaletteClick(self, m_x, m_y): """Updates the colour values based on the mouse location over the palette.""" - # Get the colour value and update + # Get the colour value, combine with H slider value, and update colour = self.palette.GetValue(m_x, m_y) - self.UpdateColour(colour) - - # Highlight a fresh selected area - self.palette.ReDraw() - self.palette.HighlightPoint(m_x, m_y) - - # Force an onscreen update - self.solid.Update() - self.colour_slider.Refresh() - - def onScroll(self, event): - """Updates the solid colour display to reflect the changing slider.""" - value = self.slider.GetValue() - colour = self.colour_slider.GetValue(value) - self.solid.SetColour(colour) + + # Update colour, but do not move V slider + self.colour_slider.SetBaseColour(colour) + self.colour_slider.ReDraw() + + colour = self.getColourFromControls() + + self.updateDisplayColour(colour) # Update display self.UpdateEntries(colour) + # Highlight a fresh selected area + self.palette.HighlightPoint(m_x, m_y) + + def onScroll(self, event): + """Updates the display to reflect the new "Value".""" + value = self.slider.GetValue() + colour = self.getColourFromControls() + self.updateDisplayColour(colour) + self.UpdateEntries(colour) + + def getValueAsFloat(self, textctrl): + """If you type garbage, you get, literally, nothing (0)""" + try: + return float(textctrl.GetValue()) + except ValueError: + return 0 + + def onHSVKillFocus(self, event): + + h = self.getValueAsFloat(self.hentry) + s = self.getValueAsFloat(self.sentry) + v = self.getValueAsFloat(self.ventry) + + if h > 0.9999: + h = 0.9999 + if s > 0.9999: + s = 0.9999 + if v > 0.9999: + v = 0.9999 + + if h < 0: + h = 0 + if s < 0: + s = 0 + if v < 0: + v = 0 + + colour = self.hsvToColour((h, s, v)) + self.SetValue(colour) # infinite loop? + + def onRGBKillFocus(self, event): + r = self.getValueAsFloat(self.rentry) + g = self.getValueAsFloat(self.gentry) + b = self.getValueAsFloat(self.bentry) + + if r > 255: + r = 255 + if g > 255: + g = 255 + if b > 255: + b = 255 + + if r < 0: + r = 0 + if g < 0: + g = 0 + if b < 0: + b = 0 + + self.SetValue(wx.Colour((r, g, b))) + def SetValue(self, colour): """Updates the colour chooser to reflect the given wxColour.""" self.UpdateColour(colour) @@ -389,9 +566,46 @@ class PyColourChooser(wx.Panel): def main(): """Simple test display.""" + + class CCTestDialog(wx.Dialog): + def __init__(self, parent, initColour): + super().__init__(parent, title="Pick A Colo(u)r") + + sizer = wx.BoxSizer(wx.VERTICAL) + self.chooser = PyColourChooser(self, wx.ID_ANY) + self.chooser.SetValue(initColour) + sizer.Add(self.chooser) + + self.SetSizer(sizer) + sizer.Fit(self) + + class CCTestFrame(wx.Frame): + def __init__(self): + super().__init__(None, -1, 'PyColourChooser Test') + sizer = wx.BoxSizer(wx.VERTICAL) + + sizer.Add(wx.StaticText(self, label="CLICK ME"), 0, wx.CENTER) + + self.box = pycolourbox.PyColourBox(self, id=wx.ID_ANY, size=(100,100)) + sizer.Add(self.box, 0, wx.EXPAND) + self.box.SetColour(wx.Colour((0x7f, 0x90, 0x21))) + self.box.colour_box.Bind(wx.EVT_LEFT_DOWN, self.onClick) # should be an event. :( + + self.SetSizer(sizer) + sizer.Fit(self) + + def onClick(self, cmdEvt): + with CCTestDialog(self, self.box.GetColour()) as dialog: + dialog.chooser.Bind(EVT_COLOUR_CHANGED, self.onColourChanged) + dialog.ShowModal() + self.box.SetColour(dialog.chooser.GetValue()) + + def onColourChanged(self, event): + self.box.SetColour(event.GetValue()) + class App(wx.App): def OnInit(self): - frame = wx.Frame(None, -1, 'PyColourChooser Test') + frame = CCTestFrame() # Added here because that's where it's supposed to be, # not embedded in the library. If it's embedded in the @@ -399,16 +613,10 @@ def main(): # handlers. wx.InitAllImageHandlers() - chooser = PyColourChooser(frame, -1) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(chooser, 0, 0) - frame.SetAutoLayout(True) - frame.SetSizer(sizer) - sizer.Fit(frame) - frame.Show(True) self.SetTopWindow(frame) return True + app = App(False) app.MainLoop() diff --git a/wx/lib/colourchooser/pycolourslider.py b/wx/lib/colourchooser/pycolourslider.py index f67c0785..0d4f9ee0 100644 --- a/wx/lib/colourchooser/pycolourslider.py +++ b/wx/lib/colourchooser/pycolourslider.py @@ -54,7 +54,7 @@ class PyColourSlider(canvas.Canvas): # drawing function self.SetBaseColour(colour) - canvas.Canvas.__init__(self, parent, id, size=(self.WIDTH, self.HEIGHT)) + canvas.Canvas.__init__(self, parent, id, forceClientSize=(self.WIDTH, self.HEIGHT)) def SetBaseColour(self, colour): """Sets the base, or target colour, to use as the central colour @@ -66,11 +66,17 @@ class PyColourSlider(canvas.Canvas): the slider.""" return self.base_colour - def GetValue(self, pos): - """Returns the colour value for a position on the slider. The position - must be within the valid height of the slider, or results can be - unpredictable.""" - return self.buffer.GetPixelColour(0, pos) + def GetVFromClick(self, pos): + """ + Returns the HSV value "V" based on the location of a mouse click at y offset "pos" + """ + _, height = self.GetClientSize() + if pos < 0: + return 1 # Snap to max + if pos >= height - 1: + return 0 # Snap to 0 + + return 1 - (pos / self.HEIGHT) def DrawBuffer(self): """Actual implementation of the widget's drawing. We simply draw diff --git a/wx/lib/colourchooser/pypalette.py b/wx/lib/colourchooser/pypalette.py index 6f32597c..2cebd60e 100644 --- a/wx/lib/colourchooser/pypalette.py +++ b/wx/lib/colourchooser/pypalette.py @@ -37,6 +37,9 @@ import colorsys from wx.lib.embeddedimage import PyEmbeddedImage +# Size of Image +IMAGE_SIZE = (200,192) + Image = PyEmbeddedImage( "iVBORw0KGgoAAAANSUhEUgAAAMgAAADACAYAAABBCyzzAAAABHNCSVQICAgIfAhkiAAACwNJ" "REFUeJztnc16o0YQRZHt8SJZJO//lMkiWWQsKwsLK7S49dNdjTyTczYYhBBd7vqoom7B6bIs" @@ -112,7 +115,7 @@ class PyPalette(canvas.Canvas): HORIZONTAL_STEP = 2 VERTICAL_STEP = 4 - + def __init__(self, parent, id): """Creates a palette object.""" # Load the pre-generated palette XPM @@ -123,27 +126,56 @@ class PyPalette(canvas.Canvas): #wx.InitAllImageHandlers() self.palette = Image.GetBitmap() - canvas.Canvas.__init__ (self, parent, id, size=(200, 192)) + self.point = None + + canvas.Canvas.__init__ (self, parent, id, forceClientSize=IMAGE_SIZE) + + def DoGetBestClientSize(self): + """Overridden to create a client window that exactly fits our bitmap""" + return self.palette.GetSize() + + def xInBounds(self, x): + """Limit x to [0,width)""" + if x < 0: + x = 0 + if x >= self.buffer.width: + x = self.buffer.width - 1 + return x + + def yInBounds(self, y): + """Limit y to [0,height)""" + if y < 0: + y = 0 + if y >= self.buffer.height: + y = self.buffer.height - 1 + return y def GetValue(self, x, y): """Returns a colour value at a specific x, y coordinate pair. This is useful for determining the colour found a specific mouse click in an external event handler.""" + x = self.xInBounds(x) + y = self.yInBounds(y) return self.buffer.GetPixelColour(x, y) def DrawBuffer(self): """Draws the palette XPM into the memory buffer.""" - #self.GeneratePaletteBMP ("foo.bmp") self.buffer.DrawBitmap(self.palette, 0, 0, 0) + + if self.point: + colour = wx.Colour(0, 0, 0) + self.buffer.SetPen(wx.Pen(colour, 1, wx.PENSTYLE_SOLID)) + self.buffer.SetBrush(wx.Brush(colour, wx.BRUSHSTYLE_TRANSPARENT)) + self.buffer.DrawCircle(self.point[0], self.point[1], 3) def HighlightPoint(self, x, y): """Highlights an area of the palette with a little circle around the coordinate point""" - colour = wx.Colour(0, 0, 0) - self.buffer.SetPen(wx.Pen(colour, 1, wx.PENSTYLE_SOLID)) - self.buffer.SetBrush(wx.Brush(colour, wx.BRUSHSTYLE_TRANSPARENT)) - self.buffer.DrawCircle(x, y, 3) - self.Refresh() + self.point = (self.xInBounds(x), self.yInBounds(y)) + self.ReDraw() + + def ClearPoint(self): + self.point = None def GeneratePaletteBMP(self, file_name, granularity=1): """The actual palette drawing algorithm.