#!/usr/bin/env python3 # -*- coding:UTF-8 -*- # GladeVcp Widget # SpeedControl is a widget specially made to control an adjustment # with a touch screen. It is a replacement to the normal scale widget # which is difficult to slide on a touch screen. # # Copyright (c) 2016 Norbert Schechner # # # 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 from gi.repository import GLib from math import pi import hal # This is needed to make the hal pin, making them directly with hal, will # not allow to use them in glade without linuxcnc being started if __name__ == "__main__": from hal_widgets import _HalSpeedControlBase else: from .hal_widgets import _HalSpeedControlBase class SpeedControl(Gtk.VBox, _HalSpeedControlBase): ''' The SpeedControl Widget serves as a slider with button to increment od decrease the value and a progress bar showing the value with or without units It is designed to be used with touch screens SpeedControl(size, value, min, max, inc_speed, unit, color, template) height = integer : The height of the widget in pixel allowed values are 24 to 96 default is 36 value = float : The start value to set allowed values are in the range from 0.001 to 99999.0 default is 10.0 min = float : The min allowed value allowed values are 0.0 to 99999.0 default is 0.0 max = float : The max allowed value allowed values are 0.001, 99999.0 default is 100.0 increment = float : sets the applied increment per mouse click, -1 means 100 increments from min to max inc_speed = integer : Sets the timer delay for the increment speed holding pressed the buttons allowed values are 20 to 300 default is 100 unit = string : Sets the unit to be shown in the bar after the value any string is allowed default is "" color = Color : Sets the color of the bar any hex color is allowed default is "#FF8116" template = Templ. : Text template to display the value Python formatting is used Any allowed format default is "%.1f" do_hide_button = Bool : Whether to show or hide the increment an decrement button True or False Default = False ''' __gtype_name__ = 'SpeedControl' __gproperties__ = { 'height' : ( GObject.TYPE_INT, 'The height of the widget in pixel', 'Set the height of the widget', 24, 96, 36, GObject.ParamFlags.READWRITE|GObject.ParamFlags.CONSTRUCT), 'value' : (GObject.TYPE_FLOAT, 'Value', 'The value to set', 0.001, 99999.0, 10.0, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'min' : (GObject.TYPE_FLOAT, 'Min Value', 'The min allowed value to apply', 0.0, 99999.0, 0.0, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'max' : (GObject.TYPE_FLOAT, 'Max Value', 'The max allowed value to apply', 0.001, 99999.0, 100.0, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'increment' : (GObject.TYPE_FLOAT, 'Increment Value', 'The increment value to apply, -1 means 100 steps from max to min', -1.0, 99999.0, -1.0, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'inc_speed' : ( GObject.TYPE_INT, 'The speed of the increments', 'Set the timer delay for the increment speed', 20, 300, 100, GObject.ParamFlags.READWRITE|GObject.ParamFlags.CONSTRUCT), 'unit' : ( GObject.TYPE_STRING, 'unit', 'Sets the unit to be shown in the bar after the value', "", GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'color' : (Gdk.RGBA, 'color', 'Sets the color of the bar', GObject.ParamFlags.READWRITE), 'template' : (GObject.TYPE_STRING, 'Text template for bar value', 'Text template to display. Python formatting may be used for one variable', "%.1f", GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'do_hide_button' : ( GObject.TYPE_BOOLEAN, 'Hide the button', 'Display the button + and - to alter the values', False, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), } __gproperties = __gproperties__ __gsignals__ = { 'value_changed': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_FLOAT,)), 'scale_changed': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_FLOAT,)), 'min_reached': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_BOOLEAN,)), 'max_reached': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_BOOLEAN,)), 'exit': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()), } def __init__(self, size = 36, value = 0, min = 0, max = 100, inc_speed = 100, unit = "", color = "#FF8116", template = "%.1f"): super(SpeedControl, self).__init__() # basic settings self._size = size self._value = value self._min = min self._max = max self.color = Gdk.RGBA() self.color.parse(color) self._unit = unit self._increment = (self._max - self._min) / 100.0 self._template = template self._speed = inc_speed self.adjustment = Gtk.Adjustment(value = self._value, lower = self._min, upper = self._max, step_increment = self._increment, page_increment = 0) self.adjustment.connect("value_changed", self._on_value_changed) self.adjustment.connect("changed", self._on_value_changed) self.btn_plus = Gtk.Button(label = "+") self.btn_plus.connect("pressed", self.on_btn_plus_pressed) self.btn_plus.connect("released", self.on_btn_plus_released) self.btn_minus = Gtk.Button(label = "-") self.btn_minus.connect("pressed", self.on_btn_minus_pressed) self.btn_minus.connect("released", self.on_btn_minus_released) self.draw = Gtk.DrawingArea() self.draw.connect("draw", self.expose) self.table = Gtk.Table(n_rows=2,n_columns=5) self.table.attach( self.btn_minus, 0, 1, 0, 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK ) self.table.attach( self.draw, 1, 4, 0, 1, Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND, Gtk.AttachOptions.EXPAND ) self.table.attach( self.btn_plus, 4, 5, 0, 1, Gtk.AttachOptions.SHRINK, Gtk.AttachOptions.SHRINK ) self.add(self.table) self.show_all() self.connect("destroy", Gtk.main_quit) self._update_widget() def _update_widget(self): self.btn_plus.set_size_request(self._size,self._size) self.btn_minus.set_size_request(self._size,self._size) # init the hal pin management def _hal_init(self): _HalSpeedControlBase._hal_init(self) # the scale, as the widget may show units per minute, but linuxcnc expects units per second self.hal_pin_scale = self.hal.newpin(self.hal_name+".scale", hal.HAL_FLOAT, hal.HAL_IN) self.hal_pin_scale.connect("value-changed", self._on_scale_changed) self.hal_pin_scale.set(60.0) # the scaled value to be handled in hal self.hal_pin_scaled_value = self.hal.newpin(self.hal_name+".scaled-value", hal.HAL_FLOAT, hal.HAL_OUT) # pins to allow hardware button to be connected to the software button self.hal_pin_increase = self.hal.newpin(self.hal_name+".increase", hal.HAL_BIT, hal.HAL_IN) self.hal_pin_increase.connect("value-changed", self._on_plus_changed) self.hal_pin_decrease = self.hal.newpin(self.hal_name+".decrease", hal.HAL_BIT, hal.HAL_IN) self.hal_pin_decrease.connect("value-changed", self._on_minus_changed) # this draws our widget on the screen def expose(self, widget, event): # create the cairo window # I do not know why this works without importing cairo self.cr = widget.get_property('window').cairo_create() # call to paint the widget self._draw_widget() # draws the frame, meaning the background def _draw_widget(self): w = self.draw.get_allocated_width() # draw a rectangle with rounded edges and a black frame linewith = self._size / 24 if linewith < 1: linewith = 1 radius = self._size / 7.5 if radius < 1: radius = 1 # fill the rectangle with selected color # first get the width of the area to fill percentage = (self._value - self._min) * 100 / (self._max - self._min) width_to_fill = w * percentage / 100 r, g, b = self.get_color_tuple(self.color) self.cr.set_source_rgb(r, g, b) # get the middle points of the corner radius tl = [radius, radius] # Top Left tr = [width_to_fill - radius, radius] # Top Right br = [width_to_fill - radius, self._size - radius] # Bottom Left bl = [radius, self._size - radius] # Bottom Right # could be written shorter, but this way it is easier to understand self.cr.arc(tl[0], tl[1], radius, 2 * (pi/2), 3 * (pi/2)) self.cr.arc(tr[0], tr[1], radius, 3 * (pi/2), 4 * (pi/2)) self.cr.arc(br[0], br[1], radius, 0 * (pi/2), 1 * (pi/2)) self.cr.arc(bl[0], bl[1], radius, 1 * (pi/2), 2 * (pi/2)) self.cr.close_path() self.cr.fill() self.cr.set_line_width(linewith) self.cr.set_source_rgb(0, 0, 0) # get the middle points of the corner radius tl = [radius, radius] # Top Left tr = [w - radius, radius] # Top Right bl = [w - radius, self._size - radius] # Bottom Left br = [radius, self._size - radius] # Bottom Right # could be written shorter, but this way it is easier to understand self.cr.arc(tl[0], tl[1], radius, 2 * (pi/2), 3 * (pi/2)) self.cr.arc(tr[0], tr[1], radius, 3 * (pi/2), 4 * (pi/2)) self.cr.arc(bl[0], bl[1], radius, 0 * (pi/2), 1 * (pi/2)) self.cr.arc(br[0], br[1], radius, 1 * (pi/2), 2 * (pi/2)) self.cr.close_path() # draw the label in the bar self.cr.set_source_rgb(0 ,0 ,0) self.cr.set_font_size(self._size / 3) tmpl = lambda s: self._template % s label = tmpl(self._value) if self._unit: label += " " + self._unit w,h = self.cr.text_extents(label)[2:4] self.draw.set_size_request(int(w) + int(h), self._size) left = self.draw.get_allocated_width() /2 top = self._size / 2 self.cr.move_to(left - w / 2 , top + h / 2) self.cr.show_text(label) self.cr.stroke() # This allows to set the value from external, i.e. propertys def set_value(self, value): self.adjustment.set_value(value) self.update_button() try: self.hal_pin_scaled_value.set(self._value / self.hal_pin_scale.get()) except: pass self.queue_draw() # Will return the value to external call # so it will do also to hal_widget_base def get_value(self): return self._value # if the value does change from outside, i.e. changing the adjustment value # we are not sync, so def _on_value_changed(self, widget): value = widget.get_value() if value != self._value: self._value = value self.set_value(self._value) self.emit("value_changed", value) # if the value does change from hal side, we have to update the scaled value def _on_scale_changed(self, pin): new_scale = pin.get() self.emit("scale_changed", new_scale) self.set_value(self._value) # we create a timer and repeat the increment command as long as the button is pressed def on_btn_plus_pressed(self, widget): self.timer_id = GLib.timeout_add(self._speed, self.increase) # destroy the timer to finish increasing the value def on_btn_plus_released(self, widget): # we have to put this in a try, as the hal pin changed signal will be emitted # also on creation of the hal pin, but the default is False, but we do not have # a self.timer_id at this state. try: GLib.source_remove(self.timer_id) except: pass # increase the value def increase(self): value = self.adjustment.get_value() value += self._increment if value > self._max: value = self._max self.btn_plus.set_sensitive(False) self.set_value(value) return False elif not self.btn_minus.get_sensitive(): self.btn_minus.set_sensitive(True) self.set_value(value) return True # we create a timer and repeat the decrease command as long as the button is pressed def on_btn_minus_pressed(self, widget): self.timer_id = GLib.timeout_add(self._speed, self.decrease) # destroy the timer to finish increasing the value def on_btn_minus_released(self, widget): # we have to put this in a try, as the hal pin changed signal will be emitted # also on creation of the hal pin, but the default is False, but we do not have # a self.timer_id at this state. try: GLib.source_remove(self.timer_id) except: pass # decrease the value def decrease(self): value = self.adjustment.get_value() value -= self._increment if value < self._min: value = self._min self.btn_minus.set_sensitive(False) self.set_value(value) return False elif not self.btn_plus.get_sensitive(): self.btn_plus.set_sensitive(True) self.set_value(value) return True # if the hal pin changes, we will virtually press the corresponding button def _on_plus_changed(self,pin): if pin.get(): self.on_btn_plus_pressed(None) else: self.on_btn_plus_released(None) # if the hal pin changes, we will virtually press the corresponding button def _on_minus_changed(self,pin): if pin.get(): self.on_btn_minus_pressed(None) else: self.on_btn_minus_released(None) # returns the separate RGB color numbers from the color widget def _convert_to_rgb(self, spec): color = spec.to_string() temp = color.strip("#") r = temp[0:4] g = temp[4:8] b = temp[8:] return (int(r, 16), int(g, 16), int(b, 16)) # returns separate values for red, green and blue of a Gtk_color def get_color_tuple(Gtk_color,c): return (c.red, c.green, c.blue) # set the digits of the shown value def set_digits(self, digits): if int(digits) > 0: self._template = "%.{0}f".format(int(digits)) else: self._template = "%d" # allow changing the adjustment from outside # so the widget can be connected to existing adjustments def set_adjustment(self, adjustment): self.adjustment = adjustment self.adjustment.connect("value_changed", self._on_value_changed) self._min = self.adjustment.get_lower() self._max = self.adjustment.get_upper() self._increment = (self._max - self._min) / 100.0 self.adjustment.set_page_size(adjustment.get_page_size()) self._value = self.adjustment.get_value() self.set_value(self._value) # Hiding the button, the widget can also be used as pure value bar def hide_button(self, state): if state: self.btn_minus.hide() self.btn_plus.hide() else: self.btn_minus.show() self.btn_plus.show() # if the adjustment changes from external command, we need to check # the button state. I.e. the value is equal max value, and the max value # has been changed, the plus button will remain unsensitive def update_button(self): if self._value <= self._min: self._value = self._min self.btn_minus.set_sensitive(False) else: self.btn_minus.set_sensitive(True) if self._value >= self._max: self._value = self._max self.btn_plus.set_sensitive(False) else: self.btn_plus.set_sensitive(True) # Get properties def do_get_property(self, property): name = property.name.replace('-', '_') if name in self.__gproperties.keys(): if name == 'color': col = getattr(self, name) colorstring = col.to_string() return getattr(self, name) return getattr(self, name) else: raise AttributeError('unknown property %s' % property.name) # Set properties def do_set_property(self, property, value): try: name = property.name.replace('-', '_') if name in self.__gproperties.keys(): setattr(self, name, value) if name == "height": self._size = value self._update_widget() if name == "value": self.set_value(value) if name == "min": self._min = value self.adjustment.set_lower(value) self._increment = (self._max - self._min) / 100.0 if name == "max": self._max = value self.adjustment.set_upper(value) self._increment = (self._max - self._min) / 100.0 if name == "increment": if value < 0: self._increment = (self._max - self._min) / 100.0 else: self._increment = value if name == "inc_speed": self._speed = value if name == "unit": self._unit = value if name == "color": self.color = value if name == "template": self._template = value if name == "do_hide_button": self.hide_button(value) self._draw_widget() else: raise AttributeError('unknown property %s' % property.name) except: pass # for testing without glade editor: # to show some behavior and setting options def main(): window = Gtk.Window() #speedcontrol = SpeedControl(size = 48, value = 10000, min = 0, max = 15000, inc_speed = 100, unit = "mm/min", color = "#FF8116", template = "%.3f") speedcontrol = SpeedControl() window.add(speedcontrol) window.set_title("Button Speed Control") window.set_position(Gtk.WindowPosition.CENTER) window.show_all() speedcontrol.set_property("height", 48) speedcontrol.set_property("unit", "mm/min") color = Gdk.RGBA() color.parse("#FF8116") speedcontrol.set_property("color", color) speedcontrol.set_property("min", 0) speedcontrol.set_property("max", 15000) speedcontrol.set_property("increment", 250.123) speedcontrol.set_property("inc_speed", 100) speedcontrol.set_property("value", 10000) speedcontrol.set_property("template", "%.3f") #speedcontrol.set_digits(1) #speedcontrol.hide_button(True) Gtk.main() if __name__ == "__main__": main()