# --------------------------------------------------------------------------------- #
# PEAKMETERCTRL wxPython IMPLEMENTATION
#
# Andrea Gavana, @ 07 October 2008
# Latest Revision: 31 Jul 2014, 21.00 GMT
#
#
# TODO List
#
# 1) Possibly some nicer drawing of bands and leds (using GraphicsContext).
#
#
# For all kind of problems, requests of enhancements and bug reports, please
# write to me at:
#
# andrea.gavana@gmail.com
# andrea.gavana@maerskoil.com
#
# Or, obviously, to the wxPython mailing list!!!
#
# Tags:        phoenix-port, unittest, documented, py3-port
#
# End Of Comments
# --------------------------------------------------------------------------------- #

"""
:class:`~wx.lib.agw.peakmeter.PeakMeterCtrl` mimics the behaviour of equalizers that are usually found in stereos
and MP3 players.


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

:class:`PeakMeterCtrl` mimics the behaviour of equalizers that are usually found in stereos
and MP3 players. This widgets supports:

* Vertical and horizontal led bands;
* Settings number of bands and leds per band;
* Possibility to change the colour for low/medium/high band frequencies;
* Falloff effects;
* Showing a background grid for the bands.

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


Usage
=====

Usage example::

    import wx
    import random

    import wx.lib.agw.peakmeter as PM

    class MyFrame(wx.Frame):

        def __init__(self, parent):

            wx.Frame.__init__(self, parent, -1, "PeakMeterCtrl Demo")

            panel = wx.Panel(self)

            # Initialize Peak Meter control 1
            self.vertPeak = PM.PeakMeterCtrl(panel, -1, style=wx.SIMPLE_BORDER, agwStyle=PM.PM_VERTICAL)
            # Initialize Peak Meter control 2
            self.horzPeak = PM.PeakMeterCtrl(panel, -1, style=wx.SUNKEN_BORDER, agwStyle=PM.PM_HORIZONTAL)

            self.vertPeak.SetMeterBands(10, 15)
            self.horzPeak.SetMeterBands(10, 15)

            # Layout the two PeakMeterCtrl
            mainSizer = wx.BoxSizer(wx.HORIZONTAL)
            mainSizer.Add(self.vertPeak, 0, wx.EXPAND | wx.ALL, 15)
            mainSizer.Add(self.horzPeak, 0, wx.EXPAND | wx.ALL, 15)

            panel.SetSizer(mainSizer)
            mainSizer.Layout()

            self.timer = wx.Timer(self)
            self.Bind(wx.EVT_TIMER, self.OnTimer)

            wx.CallLater(500, self.Start)


        def Start(self):
            ''' Starts the PeakMeterCtrl. '''

            self.timer.Start(1000//2)            # 2 fps

            self.vertPeak.Start(1000//18)        # 18 fps
            self.horzPeak.Start(1000//20)        # 20 fps


        def OnTimer(self, event):
            '''
            Handles the ``wx.EVT_TIMER`` event for :class:`PeakMeterCtrl`.

            :param `event`: a :class:`TimerEvent` event to be processed.
            '''

            # Generate 15 random number and set them as data for the meter
            nElements = 15
            arrayData = []

            for i in xrange(nElements):
                nRandom = random.randint(0, 100)
                arrayData.append(nRandom)

            self.vertPeak.SetData(arrayData, 0, nElements)
            self.horzPeak.SetData(arrayData, 0, nElements)


    # our normal wxApp-derived class, as usual

    app = wx.App(0)

    frame = MyFrame(None)
    app.SetTopWindow(frame)
    frame.Show()

    app.MainLoop()



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

:class:`PeakMeterCtrl` has been tested on the following platforms:
  * Windows (Windows XP).


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

This class supports the following window styles:

======================== =========== ========================================================
Window Styles            Hex Value   Description
======================== =========== ========================================================
``PM_HORIZONTAL``        0x0         Shows horizontal bands in :class:`PeakMeterCtrl`.
``PM_VERTICAL``          0x1         Shows vertical bands in :class:`PeakMeterCtrl`.
``PM_VERTICAL_INVERTED`` 0x2         Shows inverted vertical bands in :class:`PeakMeterCtrl`.
======================== =========== ========================================================


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

`No custom events are available for this class.`


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

:class:`PeakMeterCtrl` is distributed under the wxPython license.

Latest Revision: Andrea Gavana @ 31 Jul 2014, 21.00 GMT

Version 0.4

"""

import wx

# Horizontal or vertical PeakMeterCtrl
PM_HORIZONTAL = 0
""" Shows horizontal bands in :class:`PeakMeterCtrl`. """
PM_VERTICAL = 1
""" Shows vertical bands in :class:`PeakMeterCtrl`. """
PM_VERTICAL_INVERTED = 2
""" Shows inverted vertical bands in :class:`PeakMeterCtrl`. """

# Some useful constants...
BAND_DEFAULT = 8
""" Number of bands in the :class:`PeakMeterCtrl`. """
LEDS_DEFAULT = 8
""" Number of leds per band in the :class:`PeakMeterCtrl`. """
BAND_PERCENT = 10       # 10% of Max Range (Auto Decrease)
""" 10% of max range (auto decrease). """
GRID_INCREASEBY = 15    # Increase Grid colour based on Background colour
""" Increase grid colour based on background colour. """
FALL_INCREASEBY = 60    # Increase Falloff colour based on Background
""" Increase falloff colour based on background colour. """
DEFAULT_SPEED = 10
""" Default increase/decrease speed. """


def InRange(val, valMin, valMax):
    """
    Returns whether the value `val` is between `valMin` and `valMax`.

    :param `val`: the value to test;
    :param `valMin`: the minimum range value;
    :param `valMax`: the maximum range value.
    """

    return val >= valMin and val <= valMax


def LightenColour(crColour, byIncreaseVal):
    """
    Lightens a colour.

    :param `crColour`: a valid :class:`wx.Colour` object;
    :param `byIncreaseVal`: an integer specifying the amount for which the input
     colour should be brightened.
    """

    byRed = crColour.Red()
    byGreen = crColour.Green()
    byBlue = crColour.Blue()

    byRed = (byRed + byIncreaseVal <= 255 and [byRed + byIncreaseVal] or [255])[0]
    byGreen = (byGreen + byIncreaseVal <= 255 and [byGreen + byIncreaseVal] or [255])[0]
    byBlue = (byBlue + byIncreaseVal <= 255 and [byBlue + byIncreaseVal] or [255])[0]

    return wx.Colour(byRed, byGreen, byBlue)


def DarkenColour(crColour, byReduceVal):
    """
    Darkens a colour.

    :param `crColour`: a valid :class:`wx.Colour` object;
    :param `byReduceVal`: an integer specifying the amount for which the input
     colour should be darkened.
    """

    byRed = crColour.Red()
    byGreen = crColour.Green()
    byBlue = crColour.Blue()

    byRed = (byRed >= byReduceVal and [byRed - byReduceVal] or [0])[0]
    byGreen = (byGreen >= byReduceVal and [byGreen - byReduceVal] or [0])[0]
    byBlue = (byBlue >= byReduceVal and [byBlue - byReduceVal] or [0])[0]

    return wx.Colour(byRed, byGreen, byBlue)


class PeakMeterData(object):
    """ A simple class which holds data for our :class:`PeakMeterCtrl`. """

    def __init__(self, value=0, falloff=0, peak=0):
        """
        Default class constructor.

        :param `value`: the current :class:`PeakMeterCtrl` value;
        :param `falloff`: the falloff effect. ``True`` to enable it, ``False`` to
         disable it;
        :param `peak`: the peak value.
        """

        self._value = value
        self._falloff = falloff
        self._peak = peak


    def IsEqual(self, pm):
        """
        Returns whether 2 instances of :class:`PeakMeterData` are the same.

        :param `pm`: another instance of :class:`PeakMeterData`.
        """

        return self._value == pm._value


    def IsGreater(self, pm):
        """
        Returns whether one :class:`PeakMeterData` is greater than another.

        :param `pm`: another instance of :class:`PeakMeterData`.
        """

        return self._value > pm._value


    def IsLower(self, pm):
        """
        Returns whether one :class:`PeakMeterData` is smaller than another.

        :param `pm`: another instance of :class:`PeakMeterData`.
        """

        return self._value < pm._value


class PeakMeterCtrl(wx.Control):
    """ The main :class:`PeakMeterCtrl` implementation. """

    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=0, agwStyle=PM_VERTICAL):
        """
        Default class constructor.

        :param parent: the :class:`PeakMeterCtrl` parent. Must not be ``None``
        :param `id`: window identifier. A value of -1 indicates a default value;
        :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 underlying :class:`wx.Control` window style;
        :param `agwStyle`: the AGW-specific window style, which can be one of the following bits:

         ======================== =========== ========================================================
         Window Styles            Hex Value   Description
         ======================== =========== ========================================================
         ``PM_HORIZONTAL``        0x0         Shows horizontal bands in :class:`PeakMeterCtrl`.
         ``PM_VERTICAL``          0x1         Shows vertical bands in :class:`PeakMeterCtrl`.
         ``PM_VERTICAL_INVERTED`` 0x2         Shows inverted vertical bands in :class:`PeakMeterCtrl`.
         ======================== =========== ========================================================

        """

        wx.Control.__init__(self, parent, id, pos, size, style)
        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

        self._agwStyle = agwStyle
        # Initializes all data
        self.InitData()

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
        self.Bind(wx.EVT_TIMER, self.OnTimer)


    def InitData(self):
        """ Initializes the control. """

        colLime = wx.Colour(0, 255, 0)
        colRed = wx.Colour(255, 0, 0)
        colYellow = wx.Colour(255, 255, 0)

        self._showGrid = False
        self._showFalloff = True
        self._delay = 10
        self._minValue = 60         # Min Range 0-60
        self._medValue = 80         # Med Range 60-80
        self._maxValue = 100        # Max Range 80-100
        self._numBands = BAND_DEFAULT
        self._ledBands = LEDS_DEFAULT
        self._clrBackground = self.GetBackgroundColour()
        self._clrNormal = colLime
        self._clrMedium = colYellow
        self._clrHigh = colRed
        self._speed = DEFAULT_SPEED
        self._timer = wx.Timer(self)

        # clear vector data
        self._meterData = []


    def SetAGWWindowStyleFlag(self, agwStyle):
        """
        Sets the :class:`PeakMeterCtrl` window style flags.

        :param `agwStyle`: the AGW-specific window style. This can be a combination of the
         following bits:

         ======================== =========== ========================================================
         Window Styles            Hex Value   Description
         ======================== =========== ========================================================
         ``PM_HORIZONTAL``        0x0         Shows horizontal bands in :class:`PeakMeterCtrl`.
         ``PM_VERTICAL``          0x1         Shows vertical bands in :class:`PeakMeterCtrl`.
         ``PM_VERTICAL_INVERTED`` 0x2         Shows inverted vertical bands in :class:`PeakMeterCtrl`.
         ======================== =========== ========================================================

        """

        self._agwStyle = agwStyle
        self.Refresh()


    def GetAGWWindowStyleFlag(self):
        """
        Returns the :class:`PeakMeterCtrl` window style.

        :see: :meth:`PeakMeterCtrl.SetAGWWindowStyleFlag` for a list of possible window style flags.
        """

        return self._agwStyle


    def ResetControl(self):
        """ Resets the :class:`PeakMeterCtrl`. """

        # Initialize vector
        for i in range(self._numBands):
            pm = PeakMeterData(self._maxValue, self._maxValue, self._speed)
            self._meterData.append(pm)

        self.Refresh()


    def SetBackgroundColour(self, colourBgnd):
        """
        Changes the background colour of :class:`PeakMeterCtrl`.

        :param `colourBgnd`: the colour to be used as the background colour, pass
         :class:`NullColour` to reset to the default colour.

        :note: The background colour is usually painted by the default :class:`EraseEvent`
         event handler function under Windows and automatically under GTK.

        :note: Setting the background colour does not cause an immediate refresh, so
         you may wish to call :meth:`wx.Window.ClearBackground` or :meth:`wx.Window.Refresh` after
         calling this function.

        :note: Overridden from :class:`wx.Control`.
        """

        wx.Control.SetBackgroundColour(self, colourBgnd)
        self._clrBackground = colourBgnd
        self.Refresh()


    def SetBandsColour(self, colourNormal, colourMedium, colourHigh):
        """
        Set bands colour for :class:`PeakMeterCtrl`.

        :param `colourNormal`: the colour for normal (low) bands, a valid :class:`wx.Colour`
         object;
        :param `colourMedium`: the colour for medium bands, a valid :class:`wx.Colour`
         object;
        :param `colourHigh`: the colour for high bands, a valid :class:`wx.Colour`
         object.
        """

        self._clrNormal = colourNormal
        self._clrMedium = colourMedium
        self._clrHigh   = colourHigh

        self.Refresh()


    def SetMeterBands(self, numBands, ledBands):
        """
        Set number of vertical or horizontal bands to display.

        :param `numBands`: number of bands to display (either vertical or horizontal);
        :param `ledBands`: the number of leds per band.

        :note: You can obtain a smooth effect by setting `nHorz` or `nVert` to "1", these
         cannot be 0.
        """

        assert (numBands > 0 and ledBands > 0)

        self._numBands = numBands
        self._ledBands = ledBands

        # Reset vector
        self.ResetControl()


    def SetRangeValue(self, minVal, medVal, maxVal):
        """
        Sets the ranges for low, medium and high bands.

        :param `minVal`: the value for low bands;
        :param `medVal`: the value for medium bands;
        :param `maxVal`: the value for high bands.

        :note: The conditions to be satisfied are:

         Min: [0 - nMin[,  Med: [nMin - nMed[,  Max: [nMed - nMax]

        """

        assert (maxVal > medVal and medVal > minVal and minVal > 0)

        self._minValue = minVal
        self._medValue = medVal
        self._maxValue = maxVal


    def GetRangeValue(self):
        """ Get range value of :class:`PeakMeterCtrl`. """

        return self._minValue, self._medValue, self._maxValue


    def SetFalloffDelay(self, speed):
        """
        Set peak value speed before falling off.

        :param `speed`: the speed at which the falloff happens.
        """

        self._speed = speed


    def SetFalloffEffect(self, falloffEffect):
        """
        Set falloff effect flag.

        :param `falloffEffect`: ``True`` to enable the falloff effect, ``False``
         to disable it.
        """

        if self._showFalloff != falloffEffect:

            self._showFalloff = falloffEffect
            self.Refresh()


    def GetFalloffEffect(self):
        """ Returns the falloff effect flag. """

        return self._showFalloff


    def ShowGrid(self, showGrid):
        """
        Request to have gridlines visible or not.

        :param `showGrid`: ``True`` to show grid lines, ``False`` otherwise.
        """

        if self._showGrid != showGrid:

            self._showGrid = showGrid
            self.Refresh()


    def IsGridVisible(self):
        """ Returns if gridlines are visible. """

        return self._showGrid


    def SetData(self, arrayValue, offset, size):
        """
        Change data value. Use this function to change only
        a set of values. All bands can be changed or only 1 band,
        depending on the application.

        :param `arrayValue`: a Python list containing the :class:`PeakMeterData` values;
        :param `offset`: the (optional) offset where to start applying the new data;
        :param `size`: the size of the input data.
        """

        assert (offset >= 0 and arrayValue != [])

        isRunning = self.IsStarted()

        # Stop timer if Animation is active
        if isRunning:
            self.Stop()

        maxSize = offset + size

        for i in range(offset, maxSize):

            if i < len(self._meterData):

                pm = self._meterData[i]
                pm._value = arrayValue[i]

                if pm._falloff < pm._value:

                    pm._falloff = pm._value
                    pm._peak = self._speed

                self._meterData[i] = pm

        # Auto-restart
        if isRunning:
            return self.Start(self._delay)

        self.Refresh()

        return True


    def IsStarted(self):
        """ Check if animation is active. """

        return self._timer.IsRunning()


    def Start(self, delay):
        """
        Start the timer and animation effect.

        :param `delay`: the animation effect delay, in milliseconds.
        """

        if not self.IsStarted():
            self._delay = delay
            self._timer.Start(self._delay)
        else:
            return False

        return True


    def Stop(self):
        """ Stop the timer and animation effect. """

        if self.IsStarted():
            self._timer.Stop()
            return True

        return False


    def DoTimerProcessing(self):
        """ :class:`PeakMeterCtrl` animation, does the ``wx.EVT_TIMER`` processing. """

        self.Refresh()

        decValue  = self._maxValue//self._ledBands
        noChange = True

        for pm in self._meterData:

            if pm._value > 0:

                pm._value -= (self._ledBands > 1 and [decValue] or [self._maxValue*BAND_PERCENT//100])[0]
                if pm._value < 0:
                    pm._value = 0

                noChange = False

            if pm._peak > 0:

                pm._peak -= 1
                noChange = False


            if pm._peak == 0 and pm._falloff > 0:

                pm._falloff -= (self._ledBands > 1 and [decValue >> 1] or [5])[0]
                if pm._falloff < 0:
                    pm._falloff = 0

                noChange = False

        if noChange: # Stop timer if no more data

            self.Stop()


    def DoGetBestSize(self):
        """
        Gets the size which best suits the window: for a control, it would be the
        minimal size which doesn't truncate the control, for a panel - the same size
        as it would have after a call to `Fit()`.

        :note: Overridden from :class:`wx.Control`.
        """

        # something is better than nothing...
        return wx.Size(200, 150)


    def OnPaint(self, event):
        """
        Handles the ``wx.EVT_PAINT`` event for :class:`PeakMeterCtrl`.

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

        dc = wx.AutoBufferedPaintDC(self)
        self._clrBackground = self.GetBackgroundColour()
        dc.SetBackground(wx.Brush(self._clrBackground))
        dc.Clear()
        rc = self.GetClientRect()

        pen = wx.Pen(self._clrBackground)
        dc.SetPen(pen)

        if self.GetAGWWindowStyleFlag() & PM_VERTICAL:
            self.DrawVertBand(dc, rc)
        elif self.GetAGWWindowStyleFlag() & PM_VERTICAL_INVERTED:
            self.DrawVertBandInverted(dc, rc)
        else:
            self.DrawHorzBand(dc, rc)


    def OnEraseBackground(self, event):
        """
        Handles the ``wx.EVT_ERASE_BACKGROUND`` event for :class:`PeakMeterCtrl`.

        :param `event`: a :class:`EraseEvent` event to be processed.

        :note: This method is intentionally empty to reduce flicker.
        """

        # This is intentionally empty, to reduce flicker
        pass


    def OnSize(self, event):
        """
        Handles the ``wx.EVT_SIZE`` event for :class:`PeakMeterCtrl`.

        :param `event`: a :class:`wx.SizeEvent` event to be processed.
        """

        self.Refresh()
        event.Skip()


    def OnTimer(self, event):
        """
        Handles the ``wx.EVT_TIMER`` event for :class:`PeakMeterCtrl`.

        :param `event`: a :class:`TimerEvent` event to be processed.
        """

        self.DoTimerProcessing()


    def DrawHorzBand(self, dc, rect):
        """
        Draws horizontal bands.

        :param `dc`: an instance of :class:`wx.DC`;
        :param `rect`: the horizontal bands client rectangle.

        .. todo:: Implement falloff effect for horizontal bands.
        """

        horzBands = (self._ledBands > 1 and [self._ledBands] or [self._maxValue*BAND_PERCENT//100])[0]
        minHorzLimit = self._minValue*horzBands//self._maxValue
        medHorzLimit = self._medValue*horzBands//self._maxValue
        maxHorzLimit = horzBands

        size = wx.Size(rect.width//horzBands, rect.height//self._numBands)
        rectBand = wx.Rect(rect.GetTopLeft(), size)

        # Draw band from top
        rectBand.Offset(0, rect.height-size.y*self._numBands)
        xDecal = (self._ledBands > 1 and [1] or [0])[0]
        yDecal = (self._numBands > 1 and [1] or [0])[0]
        rectPrev = wx.Rect(*rectBand)

        for vert in range(self._numBands):

            self._value = self._meterData[vert]._value
            horzLimit = self._value*horzBands//self._maxValue

            for horz in range(horzBands):

                rectBand.Deflate(0, yDecal)

                # Find colour based on range value
                colourRect = self._clrBackground
                if self._showGrid:
                    colourRect = DarkenColour(self._clrBackground, GRID_INCREASEBY)

                if self._showGrid and (horz == minHorzLimit or horz == (horzBands-1)):

                    points = [wx.Point() for i in range(2)]
                    points[0].x = rectBand.GetTopLeft().x + (rectBand.width >> 1)
                    points[0].y = rectBand.GetTopLeft().y - yDecal
                    points[1].x = points[0].x
                    points[1].y = rectBand.GetBottomRight().y + yDecal
                    dc.DrawLine(points[0], points[1])

                if horz < horzLimit:

                    if InRange(horz, 0, minHorzLimit-1):
                        colourRect = self._clrNormal
                    elif InRange(horz, minHorzLimit, medHorzLimit-1):
                        colourRect = self._clrMedium
                    elif InRange(horz, medHorzLimit, maxHorzLimit):
                        colourRect = self._clrHigh

                dc.SetBrush(wx.Brush(colourRect))
                dc.DrawRectangle(rectBand)

                rectBand.Inflate(0, yDecal)
                rectBand.Offset(size.x, 0)

            # Draw falloff effect (Seems to be working now.)
            if self._showFalloff:
                oldPen = dc.GetPen()
                pen = wx.Pen(DarkenColour(self._clrBackground, FALL_INCREASEBY))
                maxWidth = size.x*horzBands
                points = [wx.Point() for i in range(2)]
                points[0].y = rectPrev.GetTopRight().y - yDecal
                points[0].x = rectPrev.GetBottomLeft().x + self._meterData[vert]._falloff*maxWidth/self._maxValue
                points[1].y = rectPrev.GetBottomLeft().y + yDecal
                points[1].x = points[0].x
                dc.SetPen(pen)
                dc.DrawLine(points[0], points[1])
                dc.SetPen(oldPen)

            # Move to Next Vertical band
            rectBand.Offset(-size.x*horzBands, size.y)


    def DrawVertBand(self, dc, rect):
        """
        Draws vertical bands.

        :param `dc`: an instance of :class:`wx.DC`;
        :param `rect`: the vertical bands client rectangle.
        """

        vertBands = (self._ledBands > 1 and [self._ledBands] or [self._maxValue*BAND_PERCENT//100])[0]
        minVertLimit = self._minValue*vertBands//self._maxValue
        medVertLimit = self._medValue*vertBands//self._maxValue
        maxVertLimit = vertBands

        size = wx.Size(rect.width//self._numBands, rect.height//vertBands)
        rectBand = wx.Rect(rect.GetTopLeft(), size)

        # Draw band from bottom
        rectBand.Offset(0, rect.bottom-size.y)
        xDecal = (self._numBands > 1 and [1] or [0])[0]
        yDecal = (self._ledBands > 1 and [1] or [0])[0]

        for horz in range(self._numBands):

            self._value = self._meterData[horz]._value
            vertLimit = self._value*vertBands//self._maxValue
            rectPrev = wx.Rect(*rectBand)

            for vert in range(vertBands):

                rectBand.Deflate(xDecal, 0)

                # Find colour based on range value
                colourRect = self._clrBackground
                if self._showGrid:
                    colourRect = DarkenColour(self._clrBackground, GRID_INCREASEBY)

                # Draw grid line (level) bar
                if self._showGrid and (vert == minVertLimit or vert == (vertBands-1)):

                    points = [wx.Point() for i in range(2)]
                    points[0].x = rectBand.GetTopLeft().x - xDecal
                    points[0].y = rectBand.GetTopLeft().y + (rectBand.height >> 1)
                    points[1].x = rectBand.GetBottomRight().x + xDecal
                    points[1].y = points[0].y
                    dc.DrawLine(points[0], points[1])

                if vert < vertLimit:

                    if InRange(vert, 0, minVertLimit-1):
                        colourRect = self._clrNormal
                    elif InRange(vert, minVertLimit, medVertLimit-1):
                        colourRect = self._clrMedium
                    elif InRange(vert, medVertLimit, maxVertLimit):
                        colourRect = self._clrHigh

                dc.SetBrush(wx.Brush(colourRect))
                dc.DrawRectangle(rectBand)

                rectBand.Inflate(xDecal, 0)
                rectBand.Offset(0, -size.y)

            # Draw falloff effect
            if self._showFalloff:

                oldPen = dc.GetPen()
                pen = wx.Pen(DarkenColour(self._clrBackground, FALL_INCREASEBY))
                maxHeight = size.y*vertBands
                points = [wx.Point() for i in range(2)]
                points[0].x = rectPrev.GetTopLeft().x + xDecal
                points[0].y = rectPrev.GetBottomRight().y - self._meterData[horz]._falloff*maxHeight//self._maxValue
                points[1].x = rectPrev.GetBottomRight().x - xDecal
                points[1].y = points[0].y
                dc.SetPen(pen)
                dc.DrawLine(points[0], points[1])
                dc.SetPen(oldPen)

            # Move to Next Horizontal band
            rectBand.Offset(size.x, size.y*vertBands)


    def DrawVertBandInverted(self, dc, rect):
        """
        Draws vertical bands inverted.

        :param `dc`: an instance of :class:`wx.DC`;
        :param `rect`: the vertical bands client rectangle.
        """

        vertBands = (self._ledBands > 1 and [self._ledBands] or [self._maxValue*BAND_PERCENT//100])[0]
        minVertLimit = self._minValue*vertBands//self._maxValue
        medVertLimit = self._medValue*vertBands//self._maxValue
        maxVertLimit = vertBands

        size = wx.Size(rect.width//self._numBands, rect.height//vertBands)
        rectBand = wx.Rect(rect.GetTopLeft(), size)

        # Draw band from top?
        rectBand.Offset(0, 0)
        xDecal = (self._numBands > 1 and [1] or [0])[0]
        yDecal = (self._ledBands > 1 and [1] or [0])[0]

        for horz in range(self._numBands):

            self._value = self._meterData[horz]._value
            vertLimit = self._value*vertBands//self._maxValue
            rectPrev = wx.Rect(*rectBand)

            for vert in range(vertBands):

                rectBand.Deflate(xDecal, 0)

                # Find colour based on range value
                colourRect = self._clrBackground
                if self._showGrid:
                    colourRect = DarkenColour(self._clrBackground, GRID_INCREASEBY)

                # Draw grid line (level) bar
                if self._showGrid and (vert == minVertLimit or vert == (vertBands-1)):

                    points = [wx.Point() for i in range(2)]
                    points[0].x = rectBand.GetTopLeft().x - xDecal
                    points[0].y = rectBand.GetTopLeft().y + (rectBand.height >> 1)
                    points[1].x = rectBand.GetBottomRight().x + xDecal
                    points[1].y = points[0].y
                    dc.DrawLine(points[0], points[1])

                if vert < vertLimit:

                    if InRange(vert, 0, minVertLimit-1):
                        colourRect = self._clrNormal
                    elif InRange(vert, minVertLimit, medVertLimit-1):
                        colourRect = self._clrMedium
                    elif InRange(vert, medVertLimit, maxVertLimit):
                        colourRect = self._clrHigh

                dc.SetBrush(wx.Brush(colourRect))
                dc.DrawRectangle(rectBand)

                rectBand.Inflate(xDecal, 0)
                rectBand.Offset(0, size.y)

            # Draw falloff effect
            if self._showFalloff:

                oldPen = dc.GetPen()
                pen = wx.Pen(DarkenColour(self._clrBackground, FALL_INCREASEBY))
                maxHeight = -size.y*vertBands
                points = [wx.Point() for i in range(2)]
                points[0].x = rectPrev.GetBottomLeft().x + xDecal
                points[0].y = rectPrev.GetTopRight().y - self._meterData[horz]._falloff*maxHeight/self._maxValue
                points[1].x = rectPrev.GetTopRight().x - xDecal
                points[1].y = points[0].y
                dc.SetPen(pen)
                dc.DrawLine(points[0], points[1])
                dc.SetPen(oldPen)

            # Move to Next Horizontal band
            rectBand.Offset(size.x, -size.y*vertBands)

if __name__ == '__main__':

    import wx
    import random

    class MyFrame(wx.Frame):

        def __init__(self, parent):

            wx.Frame.__init__(self, parent, -1, "PeakMeterCtrl Demo")

            panel = wx.Panel(self)

            # Initialize Peak Meter control 1
            self.vertPeak = PeakMeterCtrl(panel, -1, style=wx.SIMPLE_BORDER, agwStyle=PM_VERTICAL)
            # Initialize Peak Meter control 2
            self.horzPeak = PeakMeterCtrl(panel, -1, style=wx.SUNKEN_BORDER, agwStyle=PM_HORIZONTAL)

            self.vertPeak.SetMeterBands(10, 15)
            self.horzPeak.SetMeterBands(10, 15)

            # Layout the two PeakMeterCtrl
            mainSizer = wx.BoxSizer(wx.HORIZONTAL)
            mainSizer.Add(self.vertPeak, 0, wx.EXPAND|wx.ALL, 15)
            mainSizer.Add(self.horzPeak, 0, wx.EXPAND|wx.ALL, 15)

            panel.SetSizer(mainSizer)
            mainSizer.Layout()

            self.timer = wx.Timer(self)
            self.Bind(wx.EVT_TIMER, self.OnTimer)

            wx.CallLater(500, self.Start)


        def Start(self):
            ''' Starts the PeakMeterCtrl. '''

            self.timer.Start(1000//2)            # 2 fps

            self.vertPeak.Start(1000//18)        # 18 fps
            self.horzPeak.Start(1000//20)        # 20 fps


        def OnTimer(self, event):
            '''
            Handles the ``wx.EVT_TIMER`` event for :class:`PeakMeterCtrl`.

            :param `event`: a :class:`TimerEvent` event to be processed.
            '''

            # Generate 15 random number and set them as data for the meter
            nElements = 15
            arrayData = []

            for i in range(nElements):
                nRandom = random.randint(0, 100)
                arrayData.append(nRandom)

            self.vertPeak.SetData(arrayData, 0, nElements)
            self.horzPeak.SetData(arrayData, 0, nElements)


    # our normal wxApp-derived class, as usual

    app = wx.App(0)

    frame = MyFrame(None)
    app.SetTopWindow(frame)
    frame.Show()

    app.MainLoop()
