# --------------------------------------------------------------------------------- #
# GRADIENTBUTTON wxPython IMPLEMENTATION
#
# Andrea Gavana, @ 07 October 2008
# Latest Revision: 27 Nov 2009, 17.00 GMT
#
#
# TODO List
#
# 1) Anything to do?
#
#
# For all kind of problems, requests of enhancements and bug reports, please
# write to me at:
#
# andrea.gavana@gmail.com
# gavana@kpo.kz
#
# Or, obviously, to the wxPython mailing list!!!
#
#
# End Of Comments
# --------------------------------------------------------------------------------- #

"""
GradientButton is another custom-drawn button class which mimics Windows CE mobile
gradient buttons.


Description
===========

GradientButton is another custom-drawn button class which mimics Windows CE mobile
gradient buttons, using a tri-vertex blended gradient plus some ClearType bold
font (best effect with Tahoma Bold). GradientButton supports:

* Triple blended gradient background, with customizable colours;
* Custom colours for the "pressed" state;
* Rounded-corners buttons;
* Text-only or image+text buttons.

And a lot more. Check the demo for an almost complete review of the functionalities.


Supported Platforms
===================

GradientButton has been tested on the following platforms:
  * Windows (Windows XP).


Window Styles
=============

`No particular window styles are available for this class.`


Events Processing
=================

This class processes the following events:

================= ==================================================
Event Name        Description
================= ==================================================
``wx.EVT_BUTTON`` Process a `wx.wxEVT_COMMAND_BUTTON_CLICKED` event, when the button is clicked. 
================= ==================================================


License And Version
===================

GradientButton is distributed under the wxPython license.

Latest Revision: Andrea Gavana @ 27 Nov 2009, 17.00 GMT

Version 0.3

"""

import wx


HOVER = 1
CLICK = 2

class GradientButtonEvent(wx.PyCommandEvent):
    """ Event sent from L{GradientButton} when the button is activated. """
    
    def __init__(self, eventType, eventId):
        """
        Default class constructor.

        :param `eventType`: the event type;
        :param `eventId`: the event identifier.
        """
        
        wx.PyCommandEvent.__init__(self, eventType, eventId)
        self.isDown = False
        self.theButton = None


    def SetButtonObj(self, btn):
        """
        Sets the event object for the event.

        :param `btn`: the button object, an instance of L{GradientButton}.
        """
        
        self.theButton = btn


    def GetButtonObj(self):
        """ Returns the object associated with this event. """
        
        return self.theButton

    
class GradientButton(wx.PyControl):
    """ This is the main class implementation of L{GradientButton}. """
    
    def __init__(self, parent, id=wx.ID_ANY, bitmap=None, label="", pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=wx.NO_BORDER, validator=wx.DefaultValidator,
                 name="gradientbutton"):
        """
        Default class constructor.

        :param `parent`: the L{GradientButton} parent;
        :param `id`: window identifier. A value of -1 indicates a default value;
        :param `bitmap`: the button bitmap (if any);
        :param `label`: the button text label;
        :param `pos`: the control position. A value of (-1, -1) indicates a default position,
         chosen by either the windowing system or wxPython, depending on platform;
        :param `size`: the control size. A value of (-1, -1) indicates a default size,
         chosen by either the windowing system or wxPython, depending on platform;
        :param `style`: the button style (unused);
        :param `validator`: the validator associated to the button;
        :param `name`: the button name.
        """
        
        wx.PyControl.__init__(self, parent, id, pos, size, style, validator, name)

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda event: None)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave)
        self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter)
        self.Bind(wx.EVT_SET_FOCUS, self.OnGainFocus)
        self.Bind(wx.EVT_KILL_FOCUS, self.OnLoseFocus)
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)

        self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown)

        self._mouseAction = None
        self._bitmap = bitmap
        self._hasFocus = False
        
        self.SetLabel(label)
        self.InheritAttributes()
        self.SetInitialSize(size)

        # The following defaults are better suited to draw the text outline
        self._bottomStartColour = wx.BLACK
        rgba = self._bottomStartColour.Red(), self._bottomStartColour.Green(), \
               self._bottomStartColour.Blue(), self._bottomStartColour.Alpha()
        self._bottomEndColour = self.LightColour(self._bottomStartColour, 20)
        self._topStartColour = self.LightColour(self._bottomStartColour, 40)
        self._topEndColour = self.LightColour(self._bottomStartColour, 25)
        self._pressedTopColour = self.LightColour(self._bottomStartColour, 20)
        self._pressedBottomColour = wx.Colour(*rgba)
        self.SetForegroundColour(wx.WHITE)

        for method in dir(self):
            if method.endswith("Colour"):
                newMethod = method[0:-6] + "Colour"
                if not hasattr(self, newMethod):
                    setattr(self, newMethod, method)
        

    def LightColour(self, colour, percent):
        """
        Return light contrast of `colour`. The colour returned is from the scale of
        `colour` ==> white.

        :param `colour`: the input colour to be brightened;
        :param `percent`: determines how light the colour will be. `percent` = 100
         returns white, `percent` = 0 returns `colour`.
        """

        end_colour = wx.WHITE
        rd = end_colour.Red() - colour.Red()
        gd = end_colour.Green() - colour.Green()
        bd = end_colour.Blue() - colour.Blue()
        high = 100

        # We take the percent way of the colour from colour -. white
        i = percent
        r = colour.Red() + ((i*rd*100)/high)/100
        g = colour.Green() + ((i*gd*100)/high)/100
        b = colour.Blue() + ((i*bd*100)/high)/100

        return wx.Colour(r, g, b)


    def OnSize(self, event):
        """
        Handles the ``wx.EVT_SIZE`` event for L{GradientButton}.

        :param `event`: a `wx.SizeEvent` event to be processed.
        """
        
        event.Skip()
        self.Refresh()


    def OnLeftDown(self, event):
        """
        Handles the ``wx.EVT_LEFT_DOWN`` event for L{GradientButton}.

        :param `event`: a `wx.MouseEvent` event to be processed.
        """

        if not self.IsEnabled():
            return
        
        self._mouseAction = CLICK
        self.CaptureMouse()
        self.Refresh()
        event.Skip()


    def OnLeftUp(self, event):
        """
        Handles the ``wx.EVT_LEFT_UP`` event for L{GradientButton}.

        :param `event`: a `wx.MouseEvent` event to be processed.
        """

        if not self.IsEnabled() or not self.HasCapture():
            return
        
        pos = event.GetPosition()
        rect = self.GetClientRect()

        if self.HasCapture():
            self.ReleaseMouse()
            
        if rect.Contains(pos):
            self._mouseAction = HOVER
            self.Notify()
        else:
            self._mouseAction = None

        self.Refresh()
        event.Skip()


    def OnMouseEnter(self, event):
        """
        Handles the ``wx.EVT_ENTER_WINDOW`` event for L{GradientButton}.

        :param `event`: a `wx.MouseEvent` event to be processed.
        """

        if not self.IsEnabled():
            return
        
        self._mouseAction = HOVER
        self.Refresh()
        event.Skip()


    def OnMouseLeave(self, event):
        """
        Handles the ``wx.EVT_LEAVE_WINDOW`` event for L{GradientButton}.

        :param `event`: a `wx.MouseEvent` event to be processed.
        """

        self._mouseAction = None
        self.Refresh()
        event.Skip()


    def OnGainFocus(self, event):
        """
        Handles the ``wx.EVT_SET_FOCUS`` event for L{GradientButton}.

        :param `event`: a `wx.FocusEvent` event to be processed.
        """
        
        self._hasFocus = True
        self.Refresh()
        self.Update()


    def OnLoseFocus(self, event):
        """
        Handles the ``wx.EVT_KILL_FOCUS`` event for L{GradientButton}.

        :param `event`: a `wx.FocusEvent` event to be processed.
        """

        self._hasFocus = False
        self.Refresh()
        self.Update()


    def OnKeyDown(self, event):
        """
        Handles the ``wx.EVT_KEY_DOWN`` event for L{GradientButton}.

        :param `event`: a `wx.KeyEvent` event to be processed.
        """
        
        if self._hasFocus and event.GetKeyCode() == ord(" "):
            self._mouseAction = HOVER
            self.Refresh()
        event.Skip()


    def OnKeyUp(self, event):
        """
        Handles the ``wx.EVT_KEY_UP`` event for L{GradientButton}.

        :param `event`: a `wx.KeyEvent` event to be processed.
        """
        
        if self._hasFocus and event.GetKeyCode() == ord(" "):
            self._mouseAction = HOVER
            self.Notify()
            self.Refresh()
        event.Skip()


    def OnPaint(self, event):
        """
        Handles the ``wx.EVT_PAINT`` event for L{GradientButton}.

        :param `event`: a `wx.PaintEvent` event to be processed.
        """

        dc = wx.BufferedPaintDC(self)
        gc = wx.GraphicsContext.Create(dc)
        dc.SetBackground(wx.Brush(self.GetParent().GetBackgroundColour()))        
        dc.Clear()
        
        clientRect = self.GetClientRect()
        gradientRect = wx.Rect(*clientRect)
        capture = wx.Window.GetCapture()

        x, y, width, height = clientRect        
        
        gradientRect.SetHeight(gradientRect.GetHeight()/2 + ((capture==self and [1] or [0])[0]))
        if capture != self:
            if self._mouseAction == HOVER:
                topStart, topEnd = self.LightColour(self._topStartColour, 10), self.LightColour(self._topEndColour, 10)
            else:
                topStart, topEnd = self._topStartColour, self._topEndColour

            rc1 = wx.Rect(x, y, width, height/2)
            path1 = self.GetPath(gc, rc1, 8)
            br1 = gc.CreateLinearGradientBrush(x, y, x, y+height/2, topStart, topEnd)
            gc.SetBrush(br1)
            gc.FillPath(path1) #draw main

            path4 = gc.CreatePath()
            path4.AddRectangle(x, y+height/2-8, width, 8)
            path4.CloseSubpath()
            gc.SetBrush(br1)
            gc.FillPath(path4)            
        
        else:
            
            rc1 = wx.Rect(x, y, width, height)
            path1 = self.GetPath(gc, rc1, 8)
            gc.SetPen(wx.Pen(self._pressedTopColour))
            gc.SetBrush(wx.Brush(self._pressedTopColour))
            gc.FillPath(path1)
        
        gradientRect.Offset((0, gradientRect.GetHeight()))

        if capture != self:

            if self._mouseAction == HOVER:
                bottomStart, bottomEnd = self.LightColour(self._bottomStartColour, 10), self.LightColour(self._bottomEndColour, 10)
            else:
                bottomStart, bottomEnd = self._bottomStartColour, self._bottomEndColour

            rc3 = wx.Rect(x, y+height/2, width, height/2)
            path3 = self.GetPath(gc, rc3, 8)
            br3 = gc.CreateLinearGradientBrush(x, y+height/2, x, y+height, bottomStart, bottomEnd)
            gc.SetBrush(br3)
            gc.FillPath(path3) #draw main

            path4 = gc.CreatePath()
            path4.AddRectangle(x, y+height/2, width, 8)
            path4.CloseSubpath()
            gc.SetBrush(br3)
            gc.FillPath(path4)
            
            shadowOffset = 0
        else:
        
            rc2 = wx.Rect(x+1, gradientRect.height/2, gradientRect.width, gradientRect.height)
            path2 = self.GetPath(gc, rc2, 8)
            gc.SetPen(wx.Pen(self._pressedBottomColour))
            gc.SetBrush(wx.Brush(self._pressedBottomColour))
            gc.FillPath(path2)
            shadowOffset = 1

        font = gc.CreateFont(self.GetFont(), self.GetForegroundColour())
        gc.SetFont(font)
        label = self.GetLabel()
		# Note: Using self.GetTextExtent instead of gc.GetTextExtent seems
		# to fix sporadic segfaults with wxPython Phoenix up to 4.0.0a2
		# under Windows (fixed in 4.0.0a3), but self.GetTextExtent is NOT
		# an equivalent replacement for gc.GetTextExtent.
        tw, th = gc.GetTextExtent(label)

        if self._bitmap:
            bw, bh = self._bitmap.GetWidth(), self._bitmap.GetHeight()
        else:
            bw = bh = 0
            
        pos_x = (width-bw-tw)/2+shadowOffset      # adjust for bitmap and text to centre        
        if self._bitmap:
            pos_y =  (height-bh)/2+shadowOffset
            gc.DrawBitmap(self._bitmap, pos_x, pos_y, bw, bh) # draw bitmap if available
            pos_x = pos_x + 2   # extra spacing from bitmap

        gc.DrawText(label, pos_x + bw + shadowOffset, (height-th)/2+shadowOffset) 

        
    def GetPath(self, gc, rc, r):
        """
        Returns a rounded `wx.GraphicsPath` rectangle.

        :param `gc`: an instance of `wx.GraphicsContext`;
        :param `rc`: a client rectangle;
        :param `r`: the radious of the rounded part of the rectangle.
        """
    
        x, y, w, h = rc
        path = gc.CreatePath()
        path.AddRoundedRectangle(x, y, w, h, r)
        path.CloseSubpath()
        return path

    
    def SetInitialSize(self, size=None):
        """
        Given the current font and bezel width settings, calculate
        and set a good size.

        :param `size`: an instance of `wx.Size`.        
        """
        
        if size is None:
            size = wx.DefaultSize            
        wx.PyControl.SetInitialSize(self, size)

    SetBestSize = SetInitialSize
    

    def AcceptsFocus(self):
        """
        Can this window be given focus by mouse click?

        :note: Overridden from `wx.PyControl`.
        """
        
        return self.IsShown() and self.IsEnabled()


    def GetDefaultAttributes(self):
        """
        Overridden base class virtual. By default we should use
        the same font/colour attributes as the native `wx.Button`.
        """
        
        return wx.Button.GetClassDefaultAttributes()


    def ShouldInheritColours(self):
        """
        Overridden base class virtual. Buttons usually don't inherit
        the parent's colours.

        :note: Overridden from `wx.PyControl`.
        """
        
        return False
    

    def Enable(self, enable=True):
        """
        Enables/disables the button.

        :param `enable`: ``True`` to enable the button, ``False`` to disable it.
        
        :note: Overridden from `wx.PyControl`.
        """
        
        wx.PyControl.Enable(self, enable)
        self.Refresh()


    def SetTopStartColour(self, colour):
        """
        Sets the top start colour for the gradient shading.

        :param `colour`: a valid `wx.Colour` object.
        """

        self._topStartColour = colour
        self.Refresh()


    def GetTopStartColour(self):
        """ Returns the top start colour for the gradient shading. """

        return self._topStartColour
    

    def SetTopEndColour(self, colour):
        """
        Sets the top end colour for the gradient shading.

        :param `colour`: a valid `wx.Colour` object.
        """

        self._topEndColour = colour
        self.Refresh()


    def GetTopEndColour(self):
        """ Returns the top end colour for the gradient shading. """

        return self._topEndColour
        

    def SetBottomStartColour(self, colour):
        """
        Sets the top bottom colour for the gradient shading.

        :param `colour`: a valid `wx.Colour` object.
        """

        self._bottomStartColour = colour
        self.Refresh()


    def GetBottomStartColour(self):
        """ Returns the bottom start colour for the gradient shading. """

        return self._bottomStartColour
    
        
    def SetBottomEndColour(self, colour):
        """
        Sets the bottom end colour for the gradient shading.

        :param `colour`: a valid `wx.Colour` object.
        """

        self._bottomEndColour = colour
        self.Refresh()


    def GetBottomEndColour(self):
        """ Returns the bottom end colour for the gradient shading. """

        return self._bottomEndColour
    

    def SetPressedTopColour(self, colour):
        """
        Sets the pressed top start colour for the gradient shading.

        :param `colour`: a valid `wx.Colour` object.
        """

        self._pressedTopColour = colour
        self.Refresh()


    def GetPressedTopColour(self):
        """ Returns the pressed top start colour for the gradient shading. """

        return self._pressedTopColour
    
        
    def SetPressedBottomColour(self, colour):
        """
        Sets the pressed bottom start colour for the gradient shading.

        :param `colour`: a valid `wx.Colour` object.
        """

        self._pressedBottomColour = colour
        self.Refresh()


    def GetPressedBottomColour(self):
        """ Returns the pressed bottom start colour for the gradient shading. """

        return self._pressedBottomColour
    

    def SetForegroundColour(self, colour):
        """
        Sets the L{GradientButton} foreground (text) colour.

        :param `colour`: a valid `wx.Colour` object.

        :note: Overridden from `wx.PyControl`.        
        """

        wx.PyControl.SetForegroundColour(self, colour)
        self.Refresh()
        
        
    def DoGetBestSize(self):
        """
        Overridden base class virtual. Determines the best size of the
        button based on the label and bezel size.
        """

        label = self.GetLabel()
        if not label:
            return wx.Size(112, 48)
        
        dc = wx.ClientDC(self)
        dc.SetFont(self.GetFont())
        retWidth, retHeight = dc.GetTextExtent(label)
        
        bmpWidth = bmpHeight = 0
        constant = 15
        if self._bitmap:
            bmpWidth, bmpHeight = self._bitmap.GetWidth()+10, self._bitmap.GetHeight()
            retWidth += bmpWidth
            retHeight = max(bmpHeight, retHeight)
            constant = 15

        return wx.Size(retWidth+constant, retHeight+constant) 


    def SetDefault(self):
        """ Sets the default button. """
        
        tlw = wx.GetTopLevelParent(self)
        if hasattr(tlw, 'SetDefaultItem'):
            tlw.SetDefaultItem(self)
        

    def Notify(self):
        """ Actually sends a ``wx.EVT_BUTTON`` event to the listener (if any). """
        
        evt = GradientButtonEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, self.GetId())
        evt.SetButtonObj(self)
        evt.SetEventObject(self)
        self.GetEventHandler().ProcessEvent(evt)


