#!/usr/bin/env python3 # hal_dial.py # a pygtk widget that implements a dial with HAL pins # Copyright 2014 Chris Morley # based on # http://www.pygtk.org/articles/cairo-pygtk-widgets/cairo-pygtk-widgets.htm # author: Lawrence Oluyede # date: 16 February 2005 # # 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 import math if __name__ == "__main__": from hal_widgets import _HalJogWheelBase else: from .hal_widgets import _HalJogWheelBase class Hal_Dial(Gtk.DrawingArea, _HalJogWheelBase): __gsignals__ = dict(count_changed=(GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_INT, GObject.TYPE_FLOAT, GObject.TYPE_FLOAT)), scale_changed=(GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_INT, GObject.TYPE_FLOAT)) ) __gtype_name__ = 'Hal_Dial' __gproperties__ = { 'show_counts' : ( GObject.TYPE_BOOLEAN, 'Display the counts in the widget', 'Display or not the counts value', True, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'count_type_shown' : ( GObject.TYPE_INT, 'What to display in center', '0: counts 1:scaled counts 2:delta scaled counts', 0, 2, 0, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'cpr' : ( GObject.TYPE_INT, 'Counts per revolution', 'Set the value of counts per revolution', 25, 360, 100, GObject.ParamFlags.READWRITE|GObject.ParamFlags.CONSTRUCT), 'label' : ( GObject.TYPE_STRING, 'label', 'Sets the string to be shown in the upper part of the widget', "Dial", GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'center_color' : ( Gdk.Color.__gtype__, 'Color of the center', "", GObject.ParamFlags.READWRITE), 'scale_adjustable' : ( GObject.TYPE_BOOLEAN, 'Allow adjustable scaling', 'Clicking can adjust the scaling.', True, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), 'scale' : ( GObject.TYPE_FLOAT, 'Scale', 'Sets the scale. Scale is multiplied to current counts.', .0001,1000,1, GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT), } __gproperties = __gproperties__ def __init__(self): super(Hal_Dial, self).__init__() # Gtk.Widget signals self.connect("draw", self.expose) self.connect("button_press_event", self.button_press) self.connect("button_release_event", self.button_release) self.connect("motion_notify_event", self.motion_notify) self.connect("scroll_event", self._scroll) # public self.size = 100 self.cpr = 100 self.label = "Dial" self.scale = 1.0 self.scale_adjustable = True self.count_type_shown=1 self.center_color = Gdk.Color.parse('#bdefbdefbdef')[1] # gray # private self._minute_offset = 0 # the offset of the pointer hand self._last_offset = 0 self._dragging = False # true if the interface is being dragged self._count = self._delta_scaled = 0.0 self._show_counts = True # unmask events self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.SCROLL_MASK) # init the hal pin management def _hal_init(self): _HalJogWheelBase._hal_init(self) # This function is called from hal_widgets.py def get_value(self): return self._count # This function is called from hal_widgets.py def get_scaled_value(self): return (self._count*self.scale) # This function is called from hal_widgets.py def get_delta_scaled_value(self): return (self._delta_scaled) def set_label(self, labelcontent): self.set_property("label",labelcontent) def expose(self, widget, event): # check for sensitivity flags so color can be changed if self.is_sensitive(): self.alpha = 1 else: self.alpha = 0.3 context = widget.get_property('window').cairo_create() self.set_size_request(100, 100) # set a clip region for the expose event #TODO #context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) #context.clip() self.draw(context) return False def button_press(self, widget, event): button1 = event.button == 1 button2 = event.button == 2 button3 = event.button == 3 if button1: self.start_drag(widget,event) if not self.scale_adjustable: return False if button1 and (event.type == Gdk.EventType._2BUTTON_PRESS): self.scale=self.scale*.10 self.redraw_canvas() self.emit("scale_changed", self._count,self.scale) if button3 and (event.type == Gdk.EventType._2BUTTON_PRESS): self.scale=self.scale*10 self.redraw_canvas() self.emit("scale_changed", self._count,self.scale) def button_release(self, widget, event): button1 = event.button == 1 button2 = event.button == 2 button3 = event.button == 3 shift = event.state & Gdk.ModifierType.SHIFT_MASK if not button1:return if self._dragging: self._dragging = False self.emit_count_changed_signal(event.x, event.y) return False # handle the scroll wheel of the mouse def _scroll(self, widget, event): if event.direction == Gdk.ScrollDirection.UP: self._count += 1 self._minute_offset +=1 self._delta_scaled += self.scale if event.direction == Gdk.ScrollDirection.DOWN: self._count -= 1 self._minute_offset -=1 self._delta_scaled -= self.scale self._last_offset = self._minute_offset self.redraw_canvas() self.emit("count_changed", self._count,self.scale,self._delta_scaled) def start_drag(self,widget,event): minutes = self._minute_offset # from # http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html px = event.x - widget.get_allocation().width / 2 py = widget.get_allocation().height / 2 - event.y lx = math.sin(math.pi / (self.cpr/2.0) * minutes) ly = math.cos(math.pi / (self.cpr/2.0) * minutes) u = lx * px + ly * py # on opposite side of origin if u < 0: return False d2 = math.pow(px - u * lx, 2) + math.pow(py - u * ly, 2) if d2 < 50: # pixels away from the line self._dragging = True return False def motion_notify(self, widget, event): if self._dragging: self.emit_count_changed_signal(event.x, event.y) def emit_count_changed_signal(self, x, y): # decode the minute hand # normalize the coordinates around the origin x -= self.get_allocation().width / 2 y -= self.get_allocation().height / 2 # phi is a bearing from north clockwise, use the same geometry as we # did to position the minute hand originally phi = math.atan2(x, -y) #print phi if phi < 0: phi += math.pi * 2 minute = int(phi * (self.cpr/2.0) / math.pi) self._minute_offset = minute last = int(self._last_offset) delta = last - int(minute) #print last,int(minute),'delta',delta,'count:',self._count if not delta == 0: # epi is a range to check for cross over of zero. # if you move the pointer too fast gtk/python does not update # the position fast enough to detect the actually cross over. epi= self.cpr*.25 if last > (self.cpr-epi) and minute < epi : change = self.cpr-last + minute self._count += change self._delta_scaled += change*self.scale elif minute > (self.cpr-epi) and last < epi : change = self.cpr-minute + last self._count -= change self._delta_scaled -= change*self.scale else: self._count -= delta self._delta_scaled -= delta*self.scale self._last_offset = minute self.redraw_canvas() self.emit("count_changed", self._count,self.scale,self._delta_scaled) def draw(self, context): rect = self.get_allocation() x = self.get_allocated_width()/2 y = self.get_allocated_height()/2 radius = min(rect.width / 2.0, rect.height / 2.0) - 5 # black rim context.arc(x, y, radius, 0, 2.0 * math.pi) context.set_source_rgba(0, 0, 0,self.alpha) context.fill_preserve() # black inner circle outline context.arc(x, y, 0.8 * radius, 0, 2.0 * math.pi) context.stroke() # changeable filled inner circle context.set_source_rgb(0.8, .8, .8,) color = self.center_color context.set_source_rgba(color.red/65535., color.green/65535., color.blue/65535., self.alpha) context.arc(x, y, 0.8 * radius-context.get_line_width()/2, 0, 2.0 * math.pi) context.fill() context.stroke() # rim ticks # we color the zero tick red - which is actually the tick 3/4 the way around context.set_source_rgb(1, 1, 1) for i in range(self.cpr): context.save() if i == self.cpr*.75: context.set_source_rgb(0.9, 0.1, 0.1) elif i == self.cpr*.75+1: context.set_source_rgb(1, 1, 1) if i % 5 == 0: inset = 0.2 * radius else: inset = 0.1 * radius context.set_line_width(0.5 * context.get_line_width()) context.move_to(x + (radius - inset) * math.cos(i * math.pi / (self.cpr/2.0)), y + (radius - inset) * math.sin(i * math.pi / (self.cpr/2.0))) context.line_to(x + radius * math.cos(i * math.pi / (self.cpr/2.0)), y + radius * math.sin(i * math.pi / (self.cpr/2.0))) context.stroke() context.restore() # pointer # the line is rotated n degrees (pi/(ticks/2)) context.set_source_rgba(0, 0, 0,self.alpha) minutes = self._minute_offset # calculate starting and stopping position of pointer line x_calc = radius * math.sin(math.pi / (self.cpr/2.0)* minutes) y_calc = radius * -math.cos(math.pi / (self.cpr/2.0) * minutes) context.move_to(x + 0.50 * x_calc, y + 0.50 * y_calc) context.line_to(x + 0.75 * x_calc, y + 0.75 * y_calc) context.stroke() if self.label: context.set_font_size(int(radius/6)) w,h = context.text_extents(str(self.label))[2:4] context.move_to(x - w / 2 , y + h / 2- 2*h) context.show_text(str(self.label)) # Do we want to see the counts value? If so draw it if self._show_counts: if self.count_type_shown == 0: text=str(self._count) elif self.count_type_shown == 1: text=str(self._count*self.scale) elif self.count_type_shown == 2: text=str(self._delta_scaled) context.set_font_size(int(radius/4)) w,h = context.text_extents(text)[2:4] context.move_to(x - w / 2 , y + h / 2) context.show_text(text) # show scale value, if required if self.scale_adjustable: context.set_font_size(int(radius/8)) w2,h2 = context.text_extents(str(self.scale))[2:4] context.move_to(x - w2 / 2 , y + h2 / 2+2*h) context.show_text(str(self.scale)) def redraw_canvas(self): if self.get_property('window'): alloc = self.get_allocation() self.queue_draw_area(alloc.x, alloc.y, alloc.width, alloc.height) #https://developer.gnome.org/gtkmm/stable/classGdk_1_1Window.html#a6ec5f7e788470159672511c964771285 #self.get_property('window').process_updates(True) self.queue_draw() # Get propertys 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) # Set propertys def do_set_property(self, property, value): try: name = property.name.replace('-', '_') if name in list(self.__gproperties.keys()): setattr(self, name, value) if name == 'show_counts': self._show_counts = value elif name == "size": self.size = value self.set_size_request(self.size, self.size) elif name == "cpr": self.cpr = value elif name == "scale": self.scale = value elif name == "center_color": self.center_color = value elif name == "scale_adjustable": self.scale_adjustable = value elif name == "label": if len(str(value)) > 15: value = str(value)[:15] self.label = str(value) self.queue_draw() else: raise AttributeError('unknown property %s' % property.name) except: pass # For testing directly def count_changed(widget, count,scale,delta_scale): pass #print('delta scale =',delta_scale) #print("Count changed - count %02i scale %f = %f" % (count,scale,count*scale)) def scale_changed(widget, count,scale): pass #print("Scaled changed - count %02i scale %f = %f" % (count,scale,count*scale)) def main(): window = Gtk.Window() wheel = Hal_Dial() wheel.set_property('cpr', 200) wheel.set_property('count_type_shown', 2) wheel.set_property('label', 'Test Dial 12345') wheel.set_property('scale', 10.5) wheel.set_property('scale_adjustable', True) wheel.set_property('center_color', Gdk.Color.parse('#bdefbdefbdef')[1]) window.add(wheel) window.connect("destroy", Gtk.main_quit) window.set_title("Hal_Dial") wheel.connect("count_changed", count_changed) wheel.connect("scale_changed", scale_changed) window.show_all() Gtk.main() if __name__ == "__main__": main()