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