File: hal_dial.py

package info (click to toggle)
linuxcnc 2.9.0~pre1%2Bgit20230208.f1270d6ed7-1%2Bdeb12u1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 274,984 kB
  • sloc: python: 142,166; ansic: 103,241; cpp: 99,140; tcl: 16,045; xml: 10,608; sh: 10,124; makefile: 1,229; javascript: 226; sql: 72; asm: 15
file content (383 lines) | stat: -rw-r--r-- 15,556 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
#!/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 <l.oluyede@gmail.com>
# 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()