/ lib / python / gladevcp / hal_lightbutton.py
hal_lightbutton.py
  1  # GladeVcp HAL lighted button
  2  #
  3  # Copyright (c) 2015  Moses McKnight <moses@texband.net>
  4  #
  5  # This program is free software: you can redistribute it and/or modify
  6  # it under the terms of the GNU General Public License as published by
  7  # the Free Software Foundation, either version 2 of the License, or
  8  # (at your option) any later version.
  9  #
 10  # This program is distributed in the hope that it will be useful,
 11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13  # GNU General Public License for more details.
 14  
 15  import gtk
 16  import gobject
 17  import cairo
 18  import pango
 19  import math
 20  import gtk.glade
 21  
 22  # This creates the custom lighted button widget
 23  # 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.
 24  # If it is an HAL_IO pin, the pin is set True when the button is clicked and must be set False externally.
 25  #    This gives an external component a chance to "see" the pin state.
 26  # If the pin is a HAL_OUT pin, the button acts as a toggle button and the pin state is toggled with each click.
 27  # 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
 28  # when the machine is on and disable it when the machine is off.
 29  # There is also a HAL IN pin for the light, which operates independently of the button state.
 30  #
 31  # If has_hal_pins is true, the button creates several pins with the base name being the name of the widget
 32  #    widgetname-button        OUT or IO   - indicates button state
 33  #    widgetname-button-not    OUT         - only created if 'button_halio' is NOT true, inverse of widgetname-button
 34  #    widgetname-enable        IN          - only created if 'create_enable_pin' is true, allows button to be enabled and disabled by a HAL signal
 35  #    widgetname-light         IN          - controls light on/off
 36  #
 37  # If 'has_hal_pins' is false, no hal pins are created and the button can be completely controlled in code.  It emits the 'clicked'
 38  # signal when pressed.  You can also leave 'has_hal_pins' true, and connect the -light pin to a HAL signal while handling the
 39  # button 'clicked' signal in code.  For this use you want to make sure 'button_halio_pin' is false or the button will remain 'active'
 40  # after the first click, and calls to get_active() will always return True.
 41  
 42  from hal_widgets import _HalWidgetBase, hal, hal_pin_changed_signal
 43  
 44  clicked_signal = ('clicked', (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)))
 45  
 46  class HAL_LightButton(gtk.DrawingArea, _HalWidgetBase):
 47      __gtype_name__ = 'HAL_LightButton'
 48      __gsignals__ = dict([clicked_signal])
 49      __gproperties__ = {
 50          'has_hal_pins' : ( gobject.TYPE_BOOLEAN, 'Has HAL pins', 'Set false if this button will not be controlled using HAL',
 51                      True, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 52          'is_momentary' : ( gobject.TYPE_BOOLEAN, 'Is momentary',
 53                          'Set True if this button will be momentary rather then toggle',
 54                      False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 55          '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',
 56                      True, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 57          'create_enable_pin' : ( gobject.TYPE_BOOLEAN, 'Create enable pin', 'Creates an enable pin which enables the button if True, and disables when False',
 58                      False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 59          'dual_color' : ( gobject.TYPE_BOOLEAN, 'Dual Color Light', 'If true, light is on always, but changes color for OFF state',
 60                      False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 61          #'use_bitmaps' : ( gobject.TYPE_BOOLEAN, 'Use Bitmaps', 'If true, you must select bitmaps for each button state',
 62          #            False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 63          'light_is_on' : ( gobject.TYPE_BOOLEAN, 'Light is on', 'Turns light on - for testing in glade',
 64                      False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 65          'light_on_color' : ( gtk.gdk.Color.__gtype__, 'Light ON color', "Set color for light ON state",
 66                          gobject.PARAM_READWRITE),
 67          'light_off_color' : ( gtk.gdk.Color.__gtype__, 'Light OFF color', "Set color for light OFF state",
 68                          gobject.PARAM_READWRITE),
 69          'border_width' : ( gobject.TYPE_INT, 'Border width', 'Number of pixels extra border around label',
 70                      0, 50, 6, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 71          'corner_radius' : ( gobject.TYPE_INT, 'Corner radius', 'Radius of the button corners',
 72                      1, 20, 4, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 73          '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',
 74                      "Button", gobject.PARAM_READWRITE|gobject.PARAM_CONSTRUCT),
 75          'button_on_text' : ( gobject.TYPE_STRING, 'Button ON text', 'If not blank, this text will be shown on the button when light is on',
 76                      "", gobject.PARAM_READWRITE|gobject.PARAM_CONSTRUCT),
 77          'font_on_color' : ( gtk.gdk.Color.__gtype__, 'Font ON color', "Set color for button text when light is ON",
 78                          gobject.PARAM_READWRITE),
 79          'font_off_color' : ( gtk.gdk.Color.__gtype__, 'Font OFF color', "Set color for button text when light is OFF",
 80                          gobject.PARAM_READWRITE),
 81          'font_face' : ( gobject.TYPE_STRING, 'Font name', 'Button text',
 82                      "Sans", gobject.PARAM_READWRITE|gobject.PARAM_CONSTRUCT),
 83          'font_bold' : ( gobject.TYPE_BOOLEAN, 'Bold font', 'Set button font to bold',
 84                      False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 85          'font_size' : ( gobject.TYPE_INT, 'Font size', 'Number of pixels extra border around label',
 86                      0, 100, 10, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
 87      }
 88      __gproperties = __gproperties__
 89      _size_request = (35, 35)
 90  
 91      def __init__(self):
 92          super(HAL_LightButton, self).__init__()
 93  
 94          self.has_hal_pins = True
 95          
 96          # When True, button_pin is HAL_IO.
 97          # Pressing the button sets the pin True, and it must be reset to False externally
 98          # When False, button_pin is HAL_OUT, and it's state is toggled by a button press,
 99          # which means this button becomes a togglebutton.
100          self.button_halio_pin = True
101          
102          self.dual_color = False
103          self.use_bitmaps = False  #this feature is not implemented yet
104          self.light_on_color = gtk.gdk.Color('green')
105          self.light_off_color = gtk.gdk.Color('gray')
106          self.border_width = 6
107          self.corner_radius = 4
108          self.button_text = 'Button'
109          self.button_on_text = ''
110          self.font_face = 'Sans'
111          self.font_bold = False
112          self.font_size = 10
113          self.font_on_color = gtk.gdk.Color('black')
114          self.font_off_color = gtk.gdk.Color('black')
115          self.create_enable_pin = False
116          
117          self.active = False
118          self.light_is_on = False
119          self.mouseover = False
120          
121          self.default_pangolayout = self.create_pango_layout(self.button_text)
122          self.on_pangolayout = self.create_pango_layout(self.button_on_text)
123          
124          self.set_size_request(*self._size_request)
125          self.set_events(gtk.gdk.EXPOSURE_MASK
126                         | gtk.gdk.ENTER_NOTIFY_MASK
127                         | gtk.gdk.LEAVE_NOTIFY_MASK
128                         | gtk.gdk.BUTTON_PRESS_MASK
129                         | gtk.gdk.BUTTON_RELEASE_MASK
130                         | gtk.gdk.POINTER_MOTION_MASK
131                         | gtk.gdk.POINTER_MOTION_HINT_MASK)
132   
133          self.connect("expose-event", self.expose)
134          #self.connect('button-press-event', self.pressed)
135          #self.connect('button-release-event', self.released)
136          #self.connect('state-changed', self._on_state_changed)
137          
138      def do_enter_notify_event(self, event):
139          self.mouseover = True
140          self.queue_draw()
141      def do_leave_notify_event(self, event):
142          self.mouseover = False
143          self.queue_draw()
144          
145      def do_button_press_event(self, event):
146          if event.button == 1:
147              if self.is_momentary:
148                  active = True
149              else:
150                  active = not self.active
151  
152              if (self.has_hal_pins):
153                  if (self.button_halio_pin):
154                      self.set_active(True)
155                      try:
156                          self.button_pin.set(True)
157                      except:
158                          pass
159                  else:
160                      self.set_active(active)
161                      try:
162                          self.button_pin.set(active)
163                          self.button_pin_not.set(not active)
164                      except:
165                          pass
166  
167              else:
168                  self.set_active(active)
169  
170              self.emit("clicked", self)
171          return True
172  
173      def do_button_release_event(self, event):
174          if event.button == 1:
175              if not self.is_momentary:
176                  return True
177              if (self.has_hal_pins):
178                      self.set_active(False)
179                      try:
180                          self.button_pin.set(False)
181                          self.button_pin_not.set(True)
182                      except:
183                          pass
184  
185              else:
186                  self.set_active(False)
187  
188              self.emit("clicked", self)
189          return True
190  
191  
192      def expose(self, widget, event):
193          if (self.flags() & gtk.PARENT_SENSITIVE) and (self.flags() & gtk.SENSITIVE):
194              alpha = 1
195          else:
196              alpha = 0.3
197  
198          w = self.allocation.width
199          h = self.allocation.height
200  
201          cr = widget.window.cairo_create()
202          def set_color(c):
203              return cr.set_source_rgba(c.red_float, c.green_float, c.blue_float, alpha)
204  
205          if (self.use_bitmaps == False):
206              linewidth = 4
207              x = y = linewidth/2
208              w2 = w - linewidth
209              h2 = h - linewidth
210              x_center = x + w2/2
211              y_center = y + h2/2
212              degrees = math.pi / 180.0
213              
214              cr.new_sub_path()
215              cr.arc(x + w2 - self.corner_radius, y + self.corner_radius, self.corner_radius, -90 * degrees, 0 * degrees)
216              cr.arc(x + w2 - self.corner_radius, y + h2 - self.corner_radius, self.corner_radius, 0 * degrees, 90 * degrees)
217              cr.arc(x + self.corner_radius, y + h2 - self.corner_radius, self.corner_radius, 90 * degrees, 180 * degrees)
218              cr.arc(x + self.corner_radius, y + self.corner_radius, self.corner_radius, 180 * degrees, 270 * degrees)
219              cr.close_path()
220              
221              if (self.light_is_on):
222                  color = self.light_on_color
223                  if self.mouseover:
224                      color = gtk.gdk.color_from_hsv(color.hue, color.saturation * .5, color.value * 1.5)
225                  color2 = gtk.gdk.color_from_hsv(color.hue, color.saturation * .25, color.value)
226                  linecolor = gtk.gdk.color_from_hsv(color.hue, color.saturation, color.value * .95)
227              else:
228                  color = self.light_off_color
229                  if self.mouseover:
230                      color = gtk.gdk.color_from_hsv(color.hue, color.saturation * .5, color.value * 1.5)
231                  if (self.dual_color):
232                      color2 = gtk.gdk.color_from_hsv(color.hue, color.saturation * .25, color.value)
233                      linecolor = gtk.gdk.color_from_hsv(color.hue, color.saturation, color.value * .95)
234                  else:
235                      color1 = gtk.gdk.color_from_hsv(color.hue, color.saturation * .50, color.value * 2)
236                      color2 = gtk.gdk.color_from_hsv(color.hue, color.saturation, color.value * .50)
237                      linecolor = gtk.gdk.color_from_hsv(color.hue, color.saturation * .50, color.value * .75)
238              
239              cr.set_line_width(linewidth)
240              set_color(linecolor)
241              cr.stroke_preserve()
242              
243              if (self.light_is_on == True) or (self.dual_color == True):
244                  gradient_radius = (w2 + h2)
245                  g1 = cairo.RadialGradient(x_center, y_center, 0, x_center, y_center, gradient_radius)
246                  g1.add_color_stop_rgb(0.0, color2.red_float, color2.green_float, color2.blue_float)
247                  g1.add_color_stop_rgb(1.0, color.red_float, color.green_float, color.blue_float)
248              else:
249                  g1 = cairo.LinearGradient(x, y, x, y + h2)
250                  g1.add_color_stop_rgb(0.0, color1.red_float, color1.green_float, color1.blue_float)
251                  g1.add_color_stop_rgb(0.0, color.red_float, color.green_float, color.blue_float)
252                  g1.add_color_stop_rgb(1.0, color2.red_float, color2.green_float, color2.blue_float)
253              cr.set_source(g1)
254              cr.fill()
255          
256          #Using bitmaps is not implemented. Bitmaps should not be scaled unless you figure out a way to make them scale nicely
257          #The main reason to use bitmaps would be if someone wants a different look from the default one
258          #The code below is the basic way to do it.  update_widget_size() should use the button size instead of text size
259          #and there should be properties to set image filenames for each state (light on, light off, mouseover, etc)
260          else:
261              cr.save()
262              image = cairo.ImageSurface.create_from_png('resources/k_green.png')
263              img_w = image.get_width()
264              img_h = image.get_height()
265              print float(w)/img_w, float(h)/img_h
266              cr.set_source_surface(image, 0, 0)
267              cr.paint()
268              cr.restore()
269  
270          # write text
271          _layout = self.default_pangolayout
272          if (self.light_is_on):
273              set_color(self.font_on_color)
274              if (not self.button_on_text == ""):
275                  _layout = self.on_pangolayout
276          else:
277              set_color(self.font_off_color)
278   
279          fontw, fonth = _layout.get_pixel_size()
280          cr.move_to((w - fontw)/2, (h - fonth)/2)
281          cr.update_layout(_layout)
282          cr.show_layout(_layout)
283          return False
284      
285      def update_font(self):
286          if self.font_bold == True:
287              fontweight = "bold"
288          else:
289              fontweight = ""
290          self.default_pangolayout.set_font_description(pango.FontDescription(self.font_face + ' ' + fontweight + ' ' + str(self.font_size)))
291          self.on_pangolayout.set_font_description(pango.FontDescription(self.font_face + ' ' + fontweight + ' ' + str(self.font_size)))
292          self.update_widget_size()
293      
294      def update_widget_size(self):
295          w1, h1 = self.default_pangolayout.get_pixel_size()
296          w2, h2 = self.on_pangolayout.get_pixel_size()
297          width = max(w1 + self.border_width*2, w2 + self.border_width*2)
298          height = max(h1 + self.border_width*2, h2 + self.border_width*2)
299          self.set_size_request(int(width), int(height))
300          
301      # These set_* functions are called by the hal pin callbacks.
302      # Set self.has_hal_pins = False if you want to control state with these functions directly
303      #**************************************************************
304      
305      def set_active(self, active):
306          self.active = active
307          self.queue_draw()
308      def get_active(self):
309          return self.active
310      
311      def set_light_on(self, state):
312          self.light_is_on = state
313          self.queue_draw()
314      def get_light_on(self):
315          return self.light_is_on
316          
317      def set_text(self, text):
318          self.button_text = text
319          self.default_pangolayout.set_text(text)
320          self.update_widget_size()
321          
322      def set_on_text(self, text):
323          self.button_on_text = text
324          self.on_pangolayout.set_text(text)
325          self.update_widget_size()
326  
327      #**************************************************************
328  
329      def do_get_property(self, property):
330          name = property.name.replace('-', '_')
331          if name in self.__gproperties.keys():
332              return getattr(self, name)
333          else:
334              raise AttributeError('unknown property %s' % property.name)
335  
336      def do_set_property(self, property, value):
337          name = property.name.replace('-', '_')
338          if name == 'button_text':
339              self.set_text(value)
340          elif name == 'button_on_text':
341              self.set_on_text(value)
342          elif name in self.__gproperties.keys():
343              setattr(self, name, value)
344              if name in ['font_face', 'font_bold', 'font_size']:
345                  self.update_font()
346              if name == 'border_width':
347                  self.update_widget_size()
348              self.queue_draw()
349          else:
350              raise AttributeError('unknown property %s' % property.name)
351          return True
352  
353      def _hal_init(self):
354          if (self.has_hal_pins):
355              _HalWidgetBase._hal_init(self)
356  
357              self.set_active(False)
358              if (self.button_halio_pin):
359                  self.button_pin = self.hal.newpin(self.hal_name+'-button', hal.HAL_BIT, hal.HAL_IO)
360                  self.button_pin.connect('value-changed', self.button_pin_update)
361              else:
362                  self.button_pin = self.hal.newpin(self.hal_name+'-button', hal.HAL_BIT, hal.HAL_OUT)
363                  self.button_pin_not = self.hal.newpin(self.hal_name+'-button-not', hal.HAL_BIT, hal.HAL_OUT)
364              if (self.create_enable_pin):
365                  self.enable_pin = self.hal.newpin(self.hal_name+'-enable', hal.HAL_BIT, hal.HAL_IN)
366                  self.enable_pin.connect('value-changed', self.enable_pin_update)
367  
368              self.light_pin = self.hal.newpin(self.hal_name+'-light', hal.HAL_BIT, hal.HAL_IN)
369              self.light_pin.connect('value-changed', self.light_pin_update)
370              
371      def button_pin_update(self, hal_pin, data=None):
372          self.set_active(bool(self.button_pin.get()))
373      def enable_pin_update(self, hal_pin, data=None):
374          self.set_sensitive(hal_pin.get())
375      def light_pin_update(self, hal_pin, data=None):
376          self.set_light_on(bool(self.light_pin.get()))
377      
378  gobject.type_register(HAL_LightButton)
379