# Rijnhuizen Python toolkit: An array of leds
#
# FOM Institute for Plasma Physics Rijnhuizen
# Edisonbaan 14
# 3439 MN  Nieuwegein
# The Netherlands
# http://www.rijnhuizen.nl/
#
# Copyright (C) 2007 FOM Institute for Plasma Physics Rijnhuizen
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
# $Id: qpyledarray.py,v 1.7 2009/07/13 07:11:53 beveren Exp $
from PyQt4 import QtGui, QtCore
import sys
import random
import math

"""
    This package contains the QLedArray widget. See documentation of
    the class for details.
"""

def _colorPropertyFactory(idx):
    def colorSetter(self, color):
        self.setColorAt(idx, color)
    
    def colorGetter(self):
        return self.getColorAt(idx)
    
    def colorResetter(self):
        self.resetColorAt(idx)
                             
    return QtCore.pyqtProperty("QColor", colorGetter, colorSetter, colorResetter)         

def _valuePropertyFactory(idx):
    def valueSetter(self, color):
        self.setValueAt(idx, color)
    
    def valueGetter(self):
        return self.getValueAt(idx)
    
    def valueResetter(self):
        self.setValueAt(idx, 0)
                             
    return QtCore.pyqtProperty("int", valueGetter, valueSetter, valueResetter)         

class QPyLedArray(QtGui.QWidget):
    """
        QPyLedArray is a QtWidget which displays an an array of leds. Each
        led can take on multiple colors, depending on the value set. Each
        led can differentiate at maximum 5 states:
            - off
            - bit for value 1 set (index 0)
            - bit for value 2 set (index 1)
            - bit for value 3 set (index 2)
            - bit for value 4 set (index 3)
        
        Each state is mapped to a color. If the bit is set at multiple
        values the highest bit-color is chosen. As an example:
        
        Bit      0 1 2 3 4 5 6 7
        Value 0: . * * . . * . .  [G] Green    [.] clear   
        Value 1: . . * * . . . *  [Y] Yellow   [*] set
        Value 2: . . . * * . * .  [R] Red
        Value 3: . . . . * * . .  [B] Black
        Result : O G Y R B B R Y  [O] Off
        
        Explanation:
        The first bit (0) is clear for all values, which means that the resulting
        led displays the off color (O). The second bit (1) is set only at value 1, 
        this means the led displays green (G). The Third (2) bit is set in the first
        and second value. But only the highest value is used for coloring, so yellow
        is shown. The 6th bit (5) is set the first and the fourth bit. The highest
        value (4) is chosen, so the led displays the 'black' color.
        
        Note that this is just an example, the colors associated with each value 
        can be changed.   
        
        Note that in QtDesigner the indexes (0-3) map to A-D. So the first
        value is valueA, the second valueB, and the first color is colorA.
        
        HORIZONTAL (default) mode:
        
        Sizes and spacing are calculated in the following manner:
         ___A__          _E_        D
        /      \        /   \      / \
        +------+-+------+---+------+-+------+
        | text | | text |   | text | | text |
        +------+ +------+   +------+-+------+ \
        |                                   | |C
        +------+ +------+   +------+-+------+ /
        | B[]  | |  []  |   |  []  | |  []  |
        +------+-+------+---+------+-+------+
        
        VERTICAL mode:
            C
           / \
        +--+-+------+            A. text width
        |[]| |text  |            B. led width and height
        +--+ +------+ \          C. text spacing
        |           |  | D       D. cell spacing
        +--+ +------+ /          E. group spacing
        |[]| |text  |
        +--+ +------+ \
        |           |  | E
        |           |  |
        +--+ +------+ /
        |[]| |text  |
        +--+-+------+
             \______/
                A
               
    """

    DEFAULT_COLORS = [ QtGui.QColor(QtCore.Qt.green),
                      QtGui.QColor(QtCore.Qt.yellow),
                      QtGui.QColor(QtCore.Qt.red),
                      QtGui.QColor(QtCore.Qt.darkGray)]

    __NO_PEN = QtGui.QPen(QtCore.Qt.NoPen)
    
    DEFAULT_LED_COUNT = 8
    DEFAULT_TEXT_WIDTH = 0
    DEFAULT_LED_SIZE = QtCore.QSize(14, 14)
    DEFAULT_GROUPING = 4
    DEFAULT_GROUP_SPACING = 8
    DEFAULT_LED_SPACING = 2
    DEFAULT_TEXT_SPACING = 2
    
    DEFAULT_DARKER = 250
    
    DEFAULT_COLOR_OFF = DEFAULT_COLORS[0].darker(DEFAULT_DARKER)
    
    class __QLedColor(QtCore.QObject):
        
        def __init__(self, color):
            QtCore.QObject.__init__(self)
            self.__color = color
            self.__pal = QtGui.QApplication.palette()
            self._updateColors(True)
        
        def color(self):
            return self.__color
        
        def getBrush(self, size):
            colors = self.__colors
            
            offs = size * 0.5
            radius = size * 0.5           
            
            gradient = QtGui.QRadialGradient(offs, offs, radius)
            gradient.setColorAt(0, colors[0])
            gradient.setColorAt(0.5, colors[1])
            gradient.setColorAt(1, colors[2])
            
            return QtGui.QBrush(gradient)
        
            
        def _setColor(self, color):
            self.__color = color
        
        def _updateColors(self, enabled):
            color = self.__color
            if not enabled:
                r, g, b, a = self.__pal.color(QtGui.QPalette.Button).getRgb()
                r2, g2, b2, a2 = color.getRgb()
                r = int(r * 0.7 + r2 * 0.3)
                g = int(g * 0.7 + g2 * 0.3)
                b = int(b * 0.7 + b2 * 0.3)
                a = int(a * 0.7 + a2 * 0.3)
                color = QtGui.QColor(r, g, b, a)
            
            self.__colors = (color.darker(90), color, color.darker(110))
    
    def __init__(self, parent=None):
        """
            Initialize the ledarray.
        """
        QtGui.QWidget.__init__(self, parent)
        self.__values = [0, 0, 0, 0]
        self.__pal = QtGui.QApplication.palette()
        self.__showNames = True
        self.__firstNumber = 1
        self.__names = QtCore.QStringList()

        self.__colors = []
        for color in QPyLedArray.DEFAULT_COLORS:
            self.__colors.append(QPyLedArray.__QLedColor(color))
            
        self.__ledSize = QtCore.QSize(QPyLedArray.DEFAULT_LED_SIZE)
        self.__textAreaWidth = QPyLedArray.DEFAULT_TEXT_WIDTH
        self.__grouping = QPyLedArray.DEFAULT_GROUPING
        self.__groupSpacing = QPyLedArray.DEFAULT_GROUP_SPACING
        self.__offColor = QPyLedArray.__QLedColor(QPyLedArray.DEFAULT_COLOR_OFF)
        self.__ledCount = QPyLedArray.DEFAULT_LED_COUNT
        self.__ledSpacing = QPyLedArray.DEFAULT_LED_SPACING
        self.__textSpacing = QPyLedArray.DEFAULT_TEXT_SPACING
        self.__verticalMode = False
        self.__autoTextWidth = -1   # not scanned yet
        self.__metrics = QtGui.QFontMetrics(self.font())


    def getNames(self):
        return self.__names
    
    def setNames(self, names):
        self.__names = names
        self.__updateGeometry()
    
    names = QtCore.pyqtProperty("QStringList",
        getNames, setNames)    
    
    @QtCore.pyqtSignature("setFirstNumber(int)")
    def setFirstNumber(self, firstNumber):
        self.__firstNumber = firstNumber
        self.__safeRepaint()
        
    def getFirstNumber(self):
        return self.__firstNumber
    
    @QtCore.pyqtSignature("resetLedSize()")
    def resetFirstNumber(self):
        self.setFirstNumber(1)

    firstNumber = QtCore.pyqtProperty("int", getFirstNumber, setFirstNumber, resetFirstNumber)

    @QtCore.pyqtSignature("setTextSpacing(int)")
    def setTextSpacing(self, textSpacing):
        self.__textSpacing = textSpacing
        self.__updateGeometry()
        
    def getTextSpacing(self):
        return self.__textSpacing
    
    @QtCore.pyqtSignature("resetTextSpacing()")
    def resetTextSpacing(self):
        self.setTextSpacing(QPyLedArray.DEFAULT_TEXT_SPACING)

    textSpacing = QtCore.pyqtProperty("int", getTextSpacing, setTextSpacing, resetTextSpacing)


    @QtCore.pyqtSignature("setVerticalMode(bool)")
    def setVerticalMode(self, verticalMode):
        self.__verticalMode = verticalMode
        self.__updateGeometry()
        
    def getVerticalMode(self):
        return self.__verticalMode
    
    @QtCore.pyqtSignature("resetVerticalMode()")
    def resetVerticalMode(self):
        self.setVerticalMode(False)
    
    verticalMode = QtCore.pyqtProperty("bool", getVerticalMode, setVerticalMode, resetVerticalMode)


    @QtCore.pyqtSignature("setLedSize(QSize)")
    def setLedSize(self, size):
        self.__ledSize = ledSize
        self.__updateGeometry()
        
    def getLedSize(self):
        return self.__ledSize
    
    @QtCore.pyqtSignature("resetLedSize()")
    def resetLedSize(self):
        self.setLedSize(QtCore.QSize(QPyLedArray.DEFAULT_LED_SIZE))
#    
#    ledSize = QtCore.pyqtProperty("QSize", getLedSize, setLedSize, resetLedSize)

    @QtCore.pyqtSignature("setLedWidth(int)")
    def setLedWidth(self, width):
        if width < 1:
            raise BaseException, "LedWidth may not be < 1"
        self.__ledSize.setWidth(width)
        self.__updateGeometry()

    def getLedWidth(self):
        return self.__ledSize.width()

    @QtCore.pyqtSignature("resetLedWidth()")
    def resetLedWidth(self):
        self.setLedWidth(QPyLedArray.DEFAULT_LED_SIZE.width())
        
    ledWidth = QtCore.pyqtProperty("int", getLedWidth, setLedWidth, resetLedWidth)

    
    def setTextAreaWidth(self, width):
        self.__textAreaWidth = width
        self.__updateGeometry()
    
    def getTextAreaWidth(self):
        return self.__textAreaWidth
    
    def resetTextAreaWidth(self):
        self.setTextAreaWidth(QPyLedArray.DEFAULT_TEXT_WIDTH)
        
    textAreaWidth = QtCore.pyqtProperty("int", getTextAreaWidth, setTextAreaWidth, resetTextAreaWidth)

    @QtCore.pyqtSignature("setLedHeight(int)")
    def setLedHeight(self, height):
        if height < 1:
            raise BaseException, "LedHeight may not be < 1"
        self.__ledSize.setHeight(height)
        self.__updateGeometry()

    def getLedHeight(self):
        return self.__ledSize.height()

    @QtCore.pyqtSignature("resetLedHeight()")
    def resetLedHeight(self):
        self.setLedHeight(QPyLedArray.DEFAULT_LED_SIZE.height())
        
    ledHeight = QtCore.pyqtProperty("int", getLedHeight, setLedHeight, resetLedHeight)
    
    @QtCore.pyqtSignature("resetOffColor()")
    def resetOffColor(self):
        self.setOffColor(QPyLedArray.DEFAULT_COLOR_OFF)
    
    @QtCore.pyqtSignature("setOffColor(QColor)")
    def setOffColor(self, color):
        self.__offColor._setColor(color)
        self.__offColor._updateColors(self.isEnabled())
        self.__safeRepaint()
    
    def getOffColor(self):
        return self.__offColor.color()
    
    offColor = QtCore.pyqtProperty("QColor", getOffColor, setOffColor, resetOffColor)        

    def getColorAt(self, idx):
        return self.__colors[idx].color()
    
    @QtCore.pyqtSignature("setColorAt(int, QColor)")
    def setColorAt(self, idx, color):
        if idx == 0:
            wasDarker = self.__offColor.color() == self.__colors[0].color().darker(QPyLedArray.DEFAULT_DARKER)
        else:
            wasDarker = False
        self.__colors[idx]._setColor(color)
        self.__colors[idx]._updateColors(self.isEnabled())
        
        if wasDarker:
            self.__offColor._setColor(self.__colors[0].color().darker(QPyLedArray.DEFAULT_DARKER))
            self.__colors[idx]._updateColors(self.isEnabled())
        self.__safeRepaint()

    @QtCore.pyqtSignature("resetColorAt(int)")
    def resetColorAt(self, idx):
        self.setColorAt(idx, QPyLedArray.DEFAULT_COLORS[idx])        

    color0 = _colorPropertyFactory(0)
    color1 = _colorPropertyFactory(1)
    color2 = _colorPropertyFactory(2)
    color3 = _colorPropertyFactory(3) 
    
    def changeEvent(self, qEvent):
        QtGui.QWidget.changeEvent(self, qEvent)
        if qEvent.type() == QtCore.QEvent.EnabledChange:
            self.__updatePaintColors()
            self.__safeRepaint()
        if qEvent.type() == QtCore.QEvent.FontChange:
            self.__metrics = QtGui.QFontMetrics(self.font())
            self.__updateGeometry()

    def __updatePaintColors(self):
        for color in self.__colors:
            color._updateColors(self.isEnabled())
        self.__offColor._updateColors(self.isEnabled())

    def getLedSpacing(self):
        return self.__ledSpacing

    @QtCore.pyqtSignature("setLedSpacing(int)")
    def setLedSpacing(self, spacing):
        self.__ledSpacing = spacing
        self.__updateGeometry()
    
    @QtCore.pyqtSignature("resetLedSpacing()")
    def resetLedSpacing(self):
        self.setLedSpacing(QPyLedArray.DEFAULT_LED_SPACING)

    ledSpacing = QtCore.pyqtProperty("int", getLedSpacing, setLedSpacing, resetLedSpacing)

    @QtCore.pyqtSignature("setGrouping(int)")
    def setGrouping(self, grouping):
        self.__grouping = grouping
        self.__updateGeometry()
        
    
    def __safeRepaint(self):
        #self.emit(QtCore.SIGNAL("repaint"))
        self.update()
    
    def __updateGeometry(self):
        self.__autoTextWidth = -1
        self.resize(self.sizeHint())
    
    def getGrouping(self):
        return self.__grouping
    
    @QtCore.pyqtSignature("resetGrouping()")
    def resetGrouping(self):
        self.setGrouping(QPyLedArray.DEFAULT_GROUPING)
    
    grouping = QtCore.pyqtProperty("int", getGrouping, setGrouping, resetGrouping)     

    @QtCore.pyqtSignature("setGroupSpacing(int)")
    def setGroupSpacing(self, groupSpacing):
        self.__groupSpacing = groupSpacing
        self.__updateGeometry()
        
    def getGroupSpacing(self):
        return self.__groupSpacing
    
    @QtCore.pyqtSignature("resetGroupSpacing()")
    def resetGroupSpacing(self):
        self.setGroupSpacing(QPyLedArray.DEFAULT_GROUP_SPACING)

    groupSpacing = QtCore.pyqtProperty("int", getGroupSpacing, setGroupSpacing, resetGroupSpacing)     
   
    def __isGrouping(self):
        return self.__grouping > 0 and self.__groupSpacing > 0


    @QtCore.pyqtSignature("setLedCount(int)")    
    def setLedCount(self, count):
        if count < 1 or count > 32:
            raise BaseError, "Must be in range of 1 to 32" 
        
        self.__ledCount = count
        self.__updateGeometry()

    def getLedCount(self):
        return self.__ledCount
    
    @QtCore.pyqtSignature("resetLedCount()")
    def resetLedCount(self):
        self.setLedCount(QPyLedArray.DEFAULT_LED_COUNT)
    
    ledCount = QtCore.pyqtProperty("int", getLedCount, setLedCount, resetLedCount)
    
    @QtCore.pyqtSignature("setValue(int)")
    def setValue(self, val):
        """
            Set the base value (mask index 0).
        """
        self.setValueAt(0, val)
    
    def getValue(self):
        """
            Get the base value (mask index 0).
        """
        return valueAt(0)

        
    def getValueAt(self, idx):
        """
            Get the value at the specified index (0..3).
        """
        return self.__values[idx]
    
    @QtCore.pyqtSignature("setValueAt(int, int)")
    def setValueAt(self, idx, val):
        """
            Set the value at the specified index (0..3).
        """
        self.__values[idx] = val
        self.__safeRepaint()

    value0 = _valuePropertyFactory(0)
    value1 = _valuePropertyFactory(1)
    value2 = _valuePropertyFactory(2)
    value3 = _valuePropertyFactory(3)
    
    @QtCore.pyqtSignature("setShowNames(bool)")
    def setShowNames(self, show):
        self.__showNames = show
        self.__updateGeometry()
    
    def getShowNames(self):
        return self.__showNames
    
    showNames = QtCore.pyqtProperty("bool", getShowNames, setShowNames)    
    

    def __currentCellWidth(self):   # used in horizontal mode
        ledWidth = self.getLedSize().width()
        textWidth = self.getTextAreaWidth()
        return max((ledWidth, textWidth))

    def __textWidth(self):
        
        if self.__autoTextWidth < 0:
            maxW = 0
            
            for i in range(self.getLedCount()):
                w = self.__metrics.width(self.__ledLabel(i))
                if w > maxW:
                    maxW = w
            
            self.__autoTextWidth = maxW
        
        return self.__autoTextWidth
                        
    
    def sizeHint(self):
        c = self.getLedCount()
        groups = int(math.ceil(float(c) / self.getGrouping())) - 1 \
            if self.__isGrouping() else 0
        
        if self.__verticalMode:
            height = self.getLedHeight() * c
            height += groups * self.__groupSpacing
            height += (c - groups) * self.__ledSpacing
            
            width = self.getLedWidth()
            if self.__showNames:                         
                width += self.__textSpacing
                width += self.__textWidth()

        else:       # horizontal (default) mode
            width = self.__currentCellWidth() * c

            width += groups * self.__groupSpacing
            width += (c - groups) * self.__ledSpacing 
    
            height = self.getLedHeight()
            if self.__showNames:
                height += self.__textSpacing
                height += self.__metrics.tightBoundingRect("0").height()
            
        return QtCore.QSize(width, height)
    
    def paintEvent(self, event):
        QtGui.QWidget.paintEvent(self, event)
        painter = QtGui.QPainter(self)
        ledSize = self.getLedSize()
        border = min(ledSize.width(), ledSize.height()) / 12.0
        r = max(ledSize.width(), ledSize.height())
        textPen = QtGui.QPen(self.__pal.color(QtGui.QPalette.Text))
        
        if self.__verticalMode:
            xHeight = self.__metrics.tightBoundingRect('X').height()
            yoff = 0
            for i in range(self.getLedCount()):
                if i != 0:
                    yoff += ledSize.height()

                    if self.__isGrouping() and (i % self.__grouping) == 0:
                        yoff += self.__groupSpacing
                    else:
                        yoff += self.__ledSpacing
                    
                painter.save()
                
                painter.translate(0, yoff)
                self.__drawLed(painter, i, r, ledSize, border)                                
                
                if self.__showNames:
                    s = self.__ledLabel(i)

                    center = (ledSize.height() + xHeight - 1) / 2
                    
                    painter.translate(ledSize.width() + self.__textSpacing, 0)
                    
                    painter.setPen(textPen)                    
                    painter.drawText(QtCore.QPoint(0, center), s)
                    
                
                painter.restore()
                
        else:   # horizontal mode
            widthPerBit = self.__currentCellWidth()
            xoff = widthPerBit / 2
                                         
            for i in range(self.getLedCount()):
                
                if i != 0:
                    xoff += widthPerBit
                    if self.__isGrouping() and (i % self.__grouping) == 0:
                        xoff += self.__groupSpacing
                    else:
                        xoff += self.__ledSpacing
                        
                painter.save()
                
                if self.__showNames:
                    
                    s = self.__ledLabel(i)
                    
                    size = self.__metrics.tightBoundingRect(s)
                    
                    painter.translate(xoff, size.height())
                    
                    painter.setPen(textPen)
                    
                    center = round(size.width() / 2.0) + size.left()
                    
                    painter.drawText(QtCore.QPoint(-center, 0), s)
                    
                    painter.translate(0, self.__textSpacing)
                else:
                    painter.translate(xoff, 0)
                    
                painter.translate(-(ledSize.width() / 2), 0)
                
                self.__drawLed(painter, i, r, ledSize, border)
                
                painter.restore()
                
            
            painter.end()
            del painter

    def __drawLed(self, painter, i, r, ledSize, border):
        painter.setPen(QPyLedArray.__NO_PEN)
        
        painter.setBrush(QtGui.QBrush(self.__pal.color(QtGui.QPalette.Mid)))
        painter.drawRect(0, 0, ledSize.width() - border, ledSize.height() - border)
        painter.setBrush(QtGui.QBrush(self.__pal.color(QtGui.QPalette.Light)))
        painter.drawRect(border, border, ledSize.width() - border, ledSize.height() - border)
        
                    
        level = self.__findLevel(i)
        if level >= 0:
            brush = self.__colors[level].getBrush(r)
            painter.setBrush(brush)
        else:
            painter.setBrush(self.__offColor.getBrush(r))
        
        
        painter.drawRect(border, border, ledSize.width() - 2 * border, ledSize.height() - 2 * border)

    def __ledLabel(self, index):
        if (index >= len(self.__names)):
            s = str(index + self.__firstNumber)
        else:
            s = self.__names[index]
        return s
            
    def __findLevel(self, index):
        shift = 1 << index
        for i in range(3, - 1, - 1):
            
            if self.__values[i] & shift:
                return i
        return - 1

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    
    
    widget = QtGui.QWidget()
    widget.resize(800, 200)
    leds = QPyLedArray(widget)
    leds.setVerticalMode(True)
    
#    leds.setShowNames(False)
    
    #leds.setEnabled(False)
    
    leds.setValueAt(0, random.randint(0, 2 ** 16))
    leds.setValueAt(1, random.randint(0, 2 ** 16))
    leds.setValueAt(2, random.randint(0, 2 ** 16))
    leds.setValueAt(3, random.randint(0, 2 ** 16))
    
    def testMt(leds):
        import time
        print "Starting sequence"
        time.sleep(2)
        leds.setLedCount(8)
        time.sleep(1)
        leds.setGrouping(2)
        time.sleep(1)
        leds.setGroupSpacing(4)
        time.sleep(1)
        leds.setShowNames(False)
        time.sleep(1)
        leds.setShowNames(True)
        leds.getShowNames()

    import thread
#    thread.start_new_thread(testMt, (leds,))
    
    widget.show()
    sys.exit(app.exec_())

