# GladeVcp HAL lighted button
#
# Copyright (c) 2015  Moses McKnight <moses@texband.net>
#
# 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.

import gi
gi.require_version("Gtk","3.0")
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
gi.require_version("PangoCairo", "1.0")
from gi.repository import Pango, PangoCairo
import cairo
import math

# This creates the custom lighted button widget
# A lighted button has a HAL_OUT or HAL_IO pin for the button.  This pin indicates if the button is pressed or toggled or not.
# If it is an HAL_IO pin, the pin is set True when the button is clicked and must be set False externally.
#    This gives an external component a chance to "see" the pin state.
# If the pin is a HAL_OUT pin, the button acts as a toggle button and the pin state is toggled with each click.
# A HAL pin to enable/disable the button can be created.  One use is to connect it to halui.machine.is-on to enable a button
# when the machine is on and disable it when the machine is off.
# There is also a HAL IN pin for the light, which operates independently of the button state.
#
# If has_hal_pins is true, the button creates several pins with the base name being the name of the widget
#    widgetname-button        OUT or IO   - indicates button state
#    widgetname-button-not    OUT         - only created if 'button_halio' is NOT true, inverse of widgetname-button
#    widgetname-enable        IN          - only created if 'create_enable_pin' is true, allows button to be enabled and disabled by a HAL signal
#    widgetname-light         IN          - controls light on/off
#
# If 'has_hal_pins' is false, no hal pins are created and the button can be completely controlled in code.  It emits the 'clicked'
# signal when pressed.  You can also leave 'has_hal_pins' true, and connect the -light pin to a HAL signal while handling the
# button 'clicked' signal in code.  For this use you want to make sure 'button_halio_pin' is false or the button will remain 'active'
# after the first click, and calls to get_active() will always return True.

if __name__ == "__main__":
    from hal_widgets import _HalWidgetBase, hal, hal_pin_changed_signal
else:
    from .hal_widgets import _HalWidgetBase, hal, hal_pin_changed_signal

clicked_signal = ('clicked', (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_OBJECT,)))

class HAL_LightButton(Gtk.DrawingArea, _HalWidgetBase):
    __gtype_name__ = 'HAL_LightButton'
    __gsignals__ = dict([clicked_signal])
    __gproperties__ = {
        'has_hal_pins' : ( GObject.TYPE_BOOLEAN, 'Has HAL pins', 'Set false if this button will not be controlled using HAL',
                    True, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        'is_momentary' : ( GObject.TYPE_BOOLEAN, 'Is momentary',
                        'Set True if this button will be momentary rather then toggle',
                    False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        'button_halio_pin' : ( GObject.TYPE_BOOLEAN, 'Button pin is HAL_IO', 'If HAL_IO, pin is set true on button press; if HAL_OUT, pin state is toggled by button press',
                    True, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        'create_enable_pin' : ( GObject.TYPE_BOOLEAN, 'Create enable pin', 'Creates an enable pin which enables the button if True, and disables when False',
                    False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        'dual_color' : ( GObject.TYPE_BOOLEAN, 'Dual Color Light', 'If true, light is on always, but changes color for OFF state',
                    False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        #'use_bitmaps' : ( GObject.TYPE_BOOLEAN, 'Use Bitmaps', 'If true, you must select bitmaps for each button state',
        #            False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        'light_is_on' : ( GObject.TYPE_BOOLEAN, 'Light is on', 'Turns light on - for testing in glade',
                    False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        'light_on_color' : ( Gdk.Color.__gtype__, 'Light ON color', "Set color for light ON state",
                        GObject.ParamFlags.READWRITE),
        'light_off_color' : ( Gdk.Color.__gtype__, 'Light OFF color', "Set color for light OFF state",
                        GObject.ParamFlags.READWRITE),
        'border_width' : ( GObject.TYPE_INT, 'Border width', 'Number of pixels extra border around label',
                    0, 50, 6, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        'corner_radius' : ( GObject.TYPE_INT, 'Corner radius', 'Radius of the button corners',
                    1, 20, 4, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        'button_text' : ( GObject.TYPE_STRING, 'Button default/off text', 'Text shown when light is off, or all the time if \"Button ON text\" is blank',
                    "Button", GObject.ParamFlags.READWRITE|GObject.ParamFlags.CONSTRUCT),
        'button_on_text' : ( GObject.TYPE_STRING, 'Button ON text', 'If not blank, this text will be shown on the button when light is on',
                    "", GObject.ParamFlags.READWRITE|GObject.ParamFlags.CONSTRUCT),
        'font_on_color' : ( Gdk.Color.__gtype__, 'Font ON color', "Set color for button text when light is ON",
                        GObject.ParamFlags.READWRITE),
        'font_off_color' : ( Gdk.Color.__gtype__, 'Font OFF color', "Set color for button text when light is OFF",
                        GObject.ParamFlags.READWRITE),
        'font_face' : ( GObject.TYPE_STRING, 'Font name', 'Button text',
                    "Sans", GObject.ParamFlags.READWRITE|GObject.ParamFlags.CONSTRUCT),
        'font_bold' : ( GObject.TYPE_BOOLEAN, 'Bold font', 'Set button font to bold',
                    False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
        'font_size' : ( GObject.TYPE_INT, 'Font size', 'Number of pixels extra border around label',
                    0, 100, 10, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT),
    }
    __gproperties = __gproperties__
    _size_request = (35, 35)

    def __init__(self):
        super(HAL_LightButton, self).__init__()

        self.has_hal_pins = True
        
        # When True, button_pin is HAL_IO.
        # Pressing the button sets the pin True, and it must be reset to False externally
        # When False, button_pin is HAL_OUT, and it's state is toggled by a button press,
        # which means this button becomes a togglebutton.
        self.button_halio_pin = True
        
        self.dual_color = False
        self.use_bitmaps = False  #this feature is not implemented yet
        self.light_on_color = Gdk.Color.parse('green')[1]
        self.light_off_color = Gdk.Color.parse('gray')[1]
        self.border_width = 6
        self.corner_radius = 4
        self.button_text = 'Button'
        self.button_on_text = ''
        self.font_face = 'Sans'
        self.font_bold = False
        self.font_size = 10
        self.font_on_color = Gdk.Color.parse('black')[1]
        self.font_off_color = Gdk.Color.parse('black')[1]
        self.create_enable_pin = False
        
        self.active = False
        self.light_is_on = False
        self.mouseover = False
        
        self.default_pangolayout = self.create_pango_layout(self.button_text)
        self.on_pangolayout = self.create_pango_layout(self.button_on_text)
        
        self.set_size_request(*self._size_request)
        self.set_events( Gdk.EventMask.EXPOSURE_MASK
                       | Gdk.EventMask.ENTER_NOTIFY_MASK
                       | Gdk.EventMask.LEAVE_NOTIFY_MASK
                       | Gdk.EventMask.BUTTON_PRESS_MASK
                       | Gdk.EventMask.BUTTON_RELEASE_MASK
                       | Gdk.EventMask.POINTER_MOTION_MASK
                       | Gdk.EventMask.POINTER_MOTION_HINT_MASK)
 
        self.connect("draw", self.expose)
        #self.connect('button-press-event', self.pressed)
        #self.connect('button-release-event', self.released)
        #self.connect('state-changed', self._on_state_changed)
        
    def do_enter_notify_event(self, event):
        self.mouseover = True
        self.queue_draw()
    def do_leave_notify_event(self, event):
        self.mouseover = False
        self.queue_draw()
        
    def do_button_press_event(self, event):
        if event.button == 1:
            if self.is_momentary:
                active = True
            else:
                active = not self.active

            if (self.has_hal_pins):
                if (self.button_halio_pin):
                    self.set_active(True)
                    try:
                        self.button_pin.set(True)
                    except:
                        pass
                else:
                    self.set_active(active)
                    try:
                        self.button_pin.set(active)
                        self.button_pin_not.set(not active)
                    except:
                        pass

            else:
                self.set_active(active)

            self.emit("clicked", self)
        return True

    def do_button_release_event(self, event):
        if event.button == 1:
            if not self.is_momentary:
                return True
            if (self.has_hal_pins):
                    self.set_active(False)
                    try:
                        self.button_pin.set(False)
                        self.button_pin_not.set(True)
                    except:
                        pass

            else:
                self.set_active(False)

            self.emit("clicked", self)
        return True


    def expose(self, widget, event):
        if self.is_sensitive():
            alpha = 1
        else:
            alpha = 0.3

        w = self.get_allocated_width()
        h = self.get_allocated_height()

        cr = widget.get_property('window').cairo_create()
        def set_color(c):
            return cr.set_source_rgba(c.red_float, c.green_float, c.blue_float, alpha)

        if (self.use_bitmaps == False):
            linewidth = 4
            x = y = linewidth/2
            w2 = w - linewidth
            h2 = h - linewidth
            x_center = x + w2/2
            y_center = y + h2/2
            degrees = math.pi / 180.0
            
            cr.new_sub_path()
            cr.arc(x + w2 - self.corner_radius, y + self.corner_radius, self.corner_radius, -90 * degrees, 0 * degrees)
            cr.arc(x + w2 - self.corner_radius, y + h2 - self.corner_radius, self.corner_radius, 0 * degrees, 90 * degrees)
            cr.arc(x + self.corner_radius, y + h2 - self.corner_radius, self.corner_radius, 90 * degrees, 180 * degrees)
            cr.arc(x + self.corner_radius, y + self.corner_radius, self.corner_radius, 180 * degrees, 270 * degrees)
            cr.close_path()

            def modify_hsv_color(color, h_mult, s_mult, v_mult):
                hsv = Gtk.HSV()
                (h,s,v) = Gtk.rgb_to_hsv(color.red_float, color.green_float, color.blue_float)
                h = h*h_mult
                s = s*s_mult
                v = v*v_mult
                # suppress Gtk assertion error when value is > 1.0 due to rounding errors
                if h > 1.0: h = 1.0
                if s > 1.0: s = 1.0
                if v > 1.0: v = 1.0
                (r,g,b) = hsv.to_rgb(h, s, v)
                return Gdk.Color.from_floats(r,g,b)

            if (self.light_is_on):
                color = self.light_on_color
                if self.mouseover:
                    color = modify_hsv_color(color, 1.0, .5, 1.5)
                color2 = modify_hsv_color(color, 1.0, .25, 1.0)
                linecolor = modify_hsv_color(color, 1.0, 1.0, .95)
            else:
                color = self.light_off_color
                if self.mouseover:
                    color = modify_hsv_color(color, 1.0, .5, 1.5)
                if (self.dual_color):
                    color2 = modify_hsv_color(color, 1.0, .25, 1.0)
                    linecolor = modify_hsv_color(color, 1.0, 1.0, .95)
                else:
                    color1 = modify_hsv_color(color, 1.0, .50, 2)
                    color2 = modify_hsv_color(color, 1.0, 1.0, .50)
                    linecolor = modify_hsv_color(color, 1.0, .50, .75)
            
            cr.set_line_width(linewidth)
            set_color(linecolor)
            cr.stroke_preserve()
            
            if (self.light_is_on == True) or (self.dual_color == True):
                gradient_radius = (w2 + h2)
                g1 = cairo.RadialGradient(x_center, y_center, 0, x_center, y_center, gradient_radius)
                g1.add_color_stop_rgb(0.0, color2.red_float, color2.green_float, color2.blue_float)
                g1.add_color_stop_rgb(1.0, color.red_float, color.green_float, color.blue_float)
            else:
                g1 = cairo.LinearGradient(x, y, x, y + h2)
                g1.add_color_stop_rgb(0.0, color1.red_float, color1.green_float, color1.blue_float)
                g1.add_color_stop_rgb(0.0, color.red_float, color.green_float, color.blue_float)
                g1.add_color_stop_rgb(1.0, color2.red_float, color2.green_float, color2.blue_float)
            cr.set_source(g1)
            cr.fill()
        
        #Using bitmaps is not implemented. Bitmaps should not be scaled unless you figure out a way to make them scale nicely
        #The main reason to use bitmaps would be if someone wants a different look from the default one
        #The code below is the basic way to do it.  update_widget_size() should use the button size instead of text size
        #and there should be properties to set image filenames for each state (light on, light off, mouseover, etc)
        else:
            cr.save()
            image = cairo.ImageSurface.create_from_png('resources/k_green.png')
            img_w = image.get_width()
            img_h = image.get_height()
            print(float(w)/img_w, float(h)/img_h)
            cr.set_source_surface(image, 0, 0)
            cr.paint()
            cr.restore()

        # write text
        _layout = self.default_pangolayout
        if (self.light_is_on):
            set_color(self.font_on_color)
            if (not self.button_on_text == ""):
                _layout = self.on_pangolayout
        else:
            set_color(self.font_off_color)
 
        fontw, fonth = _layout.get_pixel_size()
        cr.move_to((w - fontw)/2, (h - fonth)/2)
        PangoCairo.update_layout(cr, _layout)
        PangoCairo.show_layout(cr, _layout)
        return False
    
    def update_font(self):
        if self.font_bold == True:
            fontweight = "bold"
        else:
            fontweight = ""
        self.default_pangolayout.set_font_description(Pango.FontDescription(self.font_face + ' ' + fontweight + ' ' + str(self.font_size)))
        self.on_pangolayout.set_font_description(Pango.FontDescription(self.font_face + ' ' + fontweight + ' ' + str(self.font_size)))
        self.update_widget_size()
    
    def update_widget_size(self):
        w1, h1 = self.default_pangolayout.get_pixel_size()
        w2, h2 = self.on_pangolayout.get_pixel_size()
        width = max(w1 + self.border_width*2, w2 + self.border_width*2)
        height = max(h1 + self.border_width*2, h2 + self.border_width*2)
        self.set_size_request(int(width), int(height))
        
    # These set_* functions are called by the hal pin callbacks.
    # Set self.has_hal_pins = False if you want to control state with these functions directly
    #**************************************************************
    
    def set_active(self, active):
        self.active = active
        self.queue_draw()
    def get_active(self):
        return self.active
    
    def set_light_on(self, state):
        self.light_is_on = state
        self.queue_draw()
    def get_light_on(self):
        return self.light_is_on
        
    def set_text(self, text):
        self.button_text = text
        self.default_pangolayout.set_text(text, -1)
        self.update_widget_size()
        
    def set_on_text(self, text):
        self.button_on_text = text
        self.on_pangolayout.set_text(text, -1)
        self.update_widget_size()

    #**************************************************************

    def do_get_property(self, property):
        name = property.name.replace('-', '_')
        if name in list(self.__gproperties.keys()):
            return getattr(self, name)
        else:
            raise AttributeError('unknown property %s' % property.name)

    def do_set_property(self, property, value):
        name = property.name.replace('-', '_')
        if name == 'button_text':
            self.set_text(value)
        elif name == 'button_on_text':
            self.set_on_text(value)
        elif name in list(self.__gproperties.keys()):
            setattr(self, name, value)
            if name in ['font_face', 'font_bold', 'font_size']:
                self.update_font()
            if name == 'border_width':
                self.update_widget_size()
            self.queue_draw()
        else:
            raise AttributeError('unknown property %s' % property.name)
        return True

    def _hal_init(self):
        if (self.has_hal_pins):
            _HalWidgetBase._hal_init(self)

            self.set_active(False)
            if (self.button_halio_pin):
                self.button_pin = self.hal.newpin(self.hal_name+'-button', hal.HAL_BIT, hal.HAL_IO)
                self.button_pin.connect('value-changed', self.button_pin_update)
            else:
                self.button_pin = self.hal.newpin(self.hal_name+'-button', hal.HAL_BIT, hal.HAL_OUT)
                self.button_pin_not = self.hal.newpin(self.hal_name+'-button-not', hal.HAL_BIT, hal.HAL_OUT)
            if (self.create_enable_pin):
                self.enable_pin = self.hal.newpin(self.hal_name+'-enable', hal.HAL_BIT, hal.HAL_IN)
                self.enable_pin.connect('value-changed', self.enable_pin_update)

            self.light_pin = self.hal.newpin(self.hal_name+'-light', hal.HAL_BIT, hal.HAL_IN)
            self.light_pin.connect('value-changed', self.light_pin_update)
            
    def button_pin_update(self, hal_pin, data=None):
        self.set_active(bool(self.button_pin.get()))
    def enable_pin_update(self, hal_pin, data=None):
        self.set_sensitive(hal_pin.get())
    def light_pin_update(self, hal_pin, data=None):
        self.set_light_on(bool(self.light_pin.get()))
    
GObject.type_register(HAL_LightButton)

