2 ############################################################################
4 ## Copyright (C), all rights reserved:
5 ## 2007-2009 Alexander Macdonald
6 ## 2008 Juho Vepsalainen
7 ## 2009 Thomas Iorns <yobbobandana@yahoo.co.nz>
9 ## This program is free software; you can redistribute it and/or
10 ## modify it under the terms of the GNU General Public License version 2
12 ## Graphics Tablet Applet
14 ############################################################################
37 def xswGetDefault(devicename
, propertyname
, *args
):
38 ''' Get the default value for some option using xsetwacom.
43 cmd
.extend(['getdefault', devicename
, propertyname
])
44 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
45 output
= p
.communicate()[0]
50 def xswGet(devicename
, propertyname
, *args
):
51 ''' Get the current value for some option using xsetwacom.
56 cmd
.extend(['get', devicename
, propertyname
])
57 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
58 output
= p
.communicate()[0].strip()
61 if propertyname
in bool_options
:
62 return bool(int(output
))
63 elif propertyname
in int_options
:
69 def xswSet(devicename
, propertyname
, value
, *args
):
70 ''' Set an option using xsetwacom.
78 value
= xswGetDefault(devicename
, propertyname
)
81 cmd
.extend(['set', devicename
, propertyname
, str(value
)])
82 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
83 output
= p
.communicate()[0]
88 def GetPressCurve(devicename
):
89 ''' Get the pressure curve of a device using xsetwacom
92 output
= subprocess
.Popen(["xsetwacom", "-x", "get", devicename
, "PressCurve"], stdout
=subprocess
.PIPE
).communicate()[0]
94 if bits
[1] == "\"PressCurve\"":
95 return [float(x
) for x
in bits
[2].strip("\"").split(",")]
99 def SetPressCurve(devicename
, points
):
100 ''' Set the pressure curve of a device using xsetwacom
103 output
= subprocess
.Popen(["xsetwacom", "set", devicename
, "PressCurve", str(points
[0]), str(points
[1]), str(points
[2]), str(points
[3])])
107 def GetClickForce(devicename
):
108 ''' Get the ClickForce of a device using xsetwacom.
109 Values are in the range 1 - 21.
111 return xswGet(devicename
, "ClickForce")
113 def SetClickForce(devicename
, force
):
114 ''' Set the ClickForce of a device using xsetwacom.
115 Values are in the range 1 - 21.
118 force
= xswGetDefault(devicename
, "ClickForce")
124 return xswSet(devicename
, "ClickForce", force
)
126 def GetMode(devicename
):
127 ''' Get the device Mode using xsetwacom.
130 output
= subprocess
.Popen(["xsetwacom", "get", devicename
, "Mode"], stdout
=subprocess
.PIPE
).communicate()[0]
131 return int(output
.strip())
135 def SetMode(devicename
, m
):
136 ''' Set the device Mode using xsetwacom.
139 output
= subprocess
.Popen(["xsetwacom", "set", devicename
, "Mode", str(m
)])
140 return int(output
.strip())
144 class PressureCurveWidget(gtk
.DrawingArea
):
145 ''' A widget for displaying and modifying the pressure curve (PressCurve)
146 for a wacom-compatible drawing tablet. The curve may be
147 modified by dragging the control point.
151 gtk
.DrawingArea
.__init
__(self
)
155 self
.ClickForceLine
= None
158 self
.ControlPointStroke
= 2.0
159 self
.ControlPointDiameter
= (self
.Radius
* 2) + self
.ControlPointStroke
160 self
.WindowSize
= None
165 self
.DraggingCP
= False
166 self
.DraggingCF
= False
168 self
.set_events(gtk
.gdk
.POINTER_MOTION_MASK | gtk
.gdk
.BUTTON_MOTION_MASK | gtk
.gdk
.BUTTON1_MOTION_MASK | gtk
.gdk
.BUTTON2_MOTION_MASK | gtk
.gdk
.BUTTON3_MOTION_MASK | gtk
.gdk
.BUTTON_PRESS_MASK | gtk
.gdk
.BUTTON_RELEASE_MASK
)
170 self
.connect("configure-event", self
.ConfigureEvent
)
171 self
.connect("expose-event", self
.ExposeEvent
)
172 self
.connect("motion-notify-event", self
.MotionEvent
)
173 self
.connect("button-press-event", self
.ButtonPress
)
174 self
.connect("button-release-event", self
.ButtonRelease
)
175 self
.set_size_request(100,100)
178 ''' Return the control points as xsetwacom would like them.
183 points
.append(int(self
.ClampValue(self
.CP
[0]) * 100.0))
184 points
.append(int(self
.ClampValue(1.0 - self
.CP
[1]) * 100.0))
185 points
.append(100 - points
[1])
186 points
.append(100 - points
[0])
189 def SetDevice(self
, name
):
190 ''' Change or refresh the current device.
192 self
.DeviceName
= name
193 cf
= GetClickForce(name
)
196 self
.ClickForceLine
= None
198 self
.ClickForceLine
= self
.ClampValue((cf
- 1) / 20.0)
200 points
= GetPressCurve(name
)
204 self
.CP
= [points
[0] / 100.0, 1.0 - (points
[1] / 100.0)]
212 def ClampValue(self
, v
):
213 ''' Make sure v is between 0.0 and 1.0 inclusive.
222 def ConfigureEvent(self
, widget
, event
):
223 ''' Handle widget resize.
225 self
.WindowSize
= self
.window
.get_size()
226 xscale
= self
.WindowSize
[0] - self
.ControlPointDiameter
227 yscale
= self
.WindowSize
[1] - self
.ControlPointDiameter
228 self
.Scale
= (xscale
, yscale
)
230 def MotionEvent(self
, widget
, event
):
231 ''' Handle motion notify events.
233 pos
= event
.get_coords()
234 pos
= (pos
[0] / self
.Scale
[0], pos
[1] / self
.Scale
[1])
240 self
.CP
[0] = self
.ClampValue(pos
[0])
241 self
.CP
[1] = self
.ClampValue(pos
[1])
244 elif self
.DraggingCF
:
245 cf
= int(pos
[0] * 20)
246 self
.ClickForceLine
= self
.ClampValue(cf
/ 20.0)
248 def ButtonPress(self
, widget
, event
):
249 ''' Handle button press events.
254 if self
.DraggingCP
or self
.DraggingCF
:
257 pos
= event
.get_coords()
258 pos
= (pos
[0] / self
.Scale
[0], pos
[1] / self
.Scale
[1])
260 dx
= abs(pos
[0] - self
.CP
[0]) * self
.Scale
[0]
261 dy
= abs(pos
[1] - self
.CP
[1]) * self
.Scale
[1]
263 if dx
< self
.ControlPointDiameter
and dy
< self
.ControlPointDiameter
:
264 self
.DraggingCP
= True
267 if self
.ClickForceLine
== None:
270 dx
= abs(pos
[0] - self
.ClickForceLine
) * self
.Scale
[0]
272 if dx
< self
.ControlPointDiameter
:
273 self
.DraggingCF
= True
276 def ButtonRelease(self
, widget
, event
):
277 ''' Handle button release events.
282 def DragFinished(self
):
283 ''' Clean up after finished drag.
285 self
.DraggingCP
= False
286 self
.DraggingCF
= False
288 if self
.ClickForceLine
!= None:
289 SetClickForce(self
.DeviceName
, int(self
.ClickForceLine
* 20.0) + 1)
291 points
= self
.GetPoints()
293 SetPressCurve(self
.DeviceName
, points
)
295 def DrawGrid(self
, cr
):
296 ''' Draw a 10 by 10 grid.
298 cr
.set_line_width(0.5)
299 cr
.set_source_rgba(0.0, 0.0, 0.0, 0.25)
301 cr
.scale(self
.Scale
[0], self
.Scale
[1])
304 cr
.move_to(x
* 0.1, 0.0)
305 cr
.line_to(x
* 0.1, 1.0)
307 cr
.move_to(0.0, y
* 0.1)
308 cr
.line_to(1.0, y
* 0.1)
312 def DrawLinear(self
, cr
):
313 ''' Draw a line to indicate the default (linear) pressure curve.
315 cr
.set_line_width(1.0)
317 cr
.scale(self
.Scale
[0], self
.Scale
[1])
324 def DrawClickForce(self
, cr
):
325 ''' Draw a vertical line indicating the current click force.
327 if self
.ClickForceLine
== None:
329 cr
.set_line_width(1.0)
330 cr
.set_source_rgba(1.0, 0.0, 0.0, 0.25)
332 cr
.scale(self
.Scale
[0], self
.Scale
[1])
334 cr
.move_to(self
.ClickForceLine
, 0.0)
335 cr
.line_to(self
.ClickForceLine
, 1.0)
339 def DrawPressure(self
, cr
, x0
, y0
, x1
, y1
, click
=True):
340 ''' Draw the current pressure.
342 if not self
.Pressure
:
345 if self
.ClickForceLine
!= None:
346 pmin
= self
.ClickForceLine
* 20 / 100.0
349 cr
.scale(self
.Scale
[0], self
.Scale
[1])
350 cr
.rectangle(0.0, 0.0, self
.Pressure
, 1.0)
353 if self
.Pressure
>= pmin
:
354 cr
.set_source_rgba(114.0 / 255.0, 159.0 / 255.0, 207.0 / 255.0, 0.5)
356 cr
.set_source_rgba(207.0 / 255.0, 114.0 / 255.0, 114.0 / 255.0, 0.5)
358 cr
.curve_to(x0
, y0
, x1
, y1
, 1.0, 0.0)
363 def DrawPressureCurve(self
, cr
, x0
, y0
, x1
, y1
):
364 ''' Draw the active pressure curve.
366 cr
.set_line_width(2.0)
367 cr
.set_source_rgba(32.0 / 255.0, 74.0 / 255.0, 135.0 / 255.0, 1.0)
369 cr
.scale(self
.Scale
[0], self
.Scale
[1])
372 cr
.curve_to(x0
, y0
, x1
, y1
, 1.0, 0.0)
376 def ExposeEvent(self
, widget
, event
):
379 cr
= widget
.window
.cairo_create()
380 cr
.set_line_cap(cairo
.LINE_CAP_ROUND
);
383 cr
.translate(self
.ControlPointDiameter
/ 2.0, self
.ControlPointDiameter
/ 2.0)
387 if self
.Pressure
== None:
404 self
.DrawPressure(cr
, x0
, y0
, x1
, y1
)
405 self
.DrawPressureCurve(cr
, x0
, y0
, x1
, y1
)
412 cr
.set_line_width(2.0)
413 cr
.set_source_rgba(0.0, 0.0, 0.0, 0.5)
416 cr
.scale(self
.Scale
[0], self
.Scale
[1])
424 cr
.set_line_width(2.0)
427 cr
.arc(x0
* self
.Scale
[0], y0
* self
.Scale
[1], self
.Radius
, 0.0, 2.0 * math
.pi
);
428 cr
.set_source_rgba(237.0 / 255.0, 212.0 / 255.0, 0.0, 0.5)
430 cr
.set_source_rgba(239.0 / 255.0, 41.0 / 255.0, 41.0 / 255.0, 1.0)
436 class DrawingTestWidget(gtk
.DrawingArea
):
437 ''' A widget for testing the pressure sensitivity of an input device.
441 gtk
.DrawingArea
.__init
__(self
)
446 self
.WindowSize
= None
450 self
.set_events(gtk
.gdk
.POINTER_MOTION_MASK | gtk
.gdk
.BUTTON_MOTION_MASK | gtk
.gdk
.BUTTON1_MOTION_MASK | gtk
.gdk
.BUTTON2_MOTION_MASK | gtk
.gdk
.BUTTON3_MOTION_MASK | gtk
.gdk
.BUTTON_PRESS_MASK | gtk
.gdk
.BUTTON_RELEASE_MASK
)
451 self
.set_extension_events(gtk
.gdk
.EXTENSION_EVENTS_ALL
)
453 self
.connect("configure-event", self
.ConfigureEvent
)
454 self
.connect("expose-event", self
.ExposeEvent
)
455 self
.connect("motion-notify-event", self
.MotionEvent
)
456 self
.connect("button-press-event", self
.ButtonPress
)
457 self
.connect("button-release-event", self
.ButtonRelease
)
458 self
.set_size_request(100,100)
460 def ConfigureEvent(self
, widget
, event
):
461 ''' Handle widget resize.
463 self
.WindowSize
= self
.window
.get_size()
464 self
.Raster
= self
.window
.cairo_create().get_target().create_similar(cairo
.CONTENT_COLOR
, self
.WindowSize
[0], self
.WindowSize
[1])
465 self
.RasterCr
= cairo
.Context(self
.Raster
)
466 self
.RasterCr
.set_source_rgba(1.0, 1.0, 1.0, 1.0)
467 self
.RasterCr
.rectangle(0.0, 0.0, self
.WindowSize
[0], self
.WindowSize
[1])
470 def GetPressure(self
):
471 ''' Return the current device pressure.
473 dev
= gtk
.gdk
.devices_list()[self
.Device
]
474 state
= dev
.get_state(self
.window
)
475 return dev
.get_axis(state
[0], gtk
.gdk
.AXIS_PRESSURE
)
477 def MotionEvent(self
, widget
, event
):
478 ''' Handle motion events.
481 pos
= event
.get_coords()
482 p
= self
.GetPressure()
486 self
.RasterCr
.set_line_width(2)
487 self
.RasterCr
.set_source_rgba(p
, 1.0, 0.0, 0.5)
489 self
.RasterCr
.arc(pos
[0], pos
[1],r
, 0.0, 2 * math
.pi
);
491 # draw something to indicate no pressure
492 self
.RasterCr
.move_to(pos
[0] - 4, pos
[1] - 4)
493 self
.RasterCr
.line_to(pos
[0] + 4, pos
[1] + 4)
494 self
.RasterCr
.move_to(pos
[0] - 4, pos
[1] + 4)
495 self
.RasterCr
.line_to(pos
[0] + 4, pos
[1] - 4)
497 self
.RasterCr
.fill_preserve()
498 self
.RasterCr
.set_source_rgba(0.5, 0.2, p
, 0.5)
499 self
.RasterCr
.stroke()
500 reg
= gtk
.gdk
.Region()
501 reg
.union_with_rect((int(pos
[0] - r
- 2), int(pos
[1] - r
- 2), int(2 * (r
+ 2)), int(2 * (r
+ 2))))
502 self
.window
.invalidate_region(reg
, False)
504 def ButtonPress(self
, widget
, event
):
505 ''' Handle button press events.
509 def ButtonRelease(self
, widget
, event
):
510 ''' Handle button release events.
514 def ExposeEvent(self
, widget
, event
):
517 cr
= widget
.window
.cairo_create()
518 cr
.set_source_surface(self
.Raster
, 0.0, 0.0)
521 cr
.set_source_rgba(0.0, 0.0, 0.0, 0.25)
522 cr
.rectangle(0.0, 0.0, self
.WindowSize
[0], self
.WindowSize
[1])
525 class GraphicsTabletApplet
:
526 ''' GUI for configuring wacom-compatible drawing tablets.
528 def __init__(self
, gladefile
, statusicon
):
530 self
.WidgetTree
= gtk
.glade
.XML(gladefile
)
531 self
.StatusIcon
= statusicon
533 self
.MainWindow
= self
.WidgetTree
.get_widget("MainWindow")
534 self
.DeviceCombo
= self
.WidgetTree
.get_widget("DeviceCombo")
535 self
.ModeCombo
= self
.WidgetTree
.get_widget("ModeCombo")
536 self
.XTilt
= self
.WidgetTree
.get_widget("xtilt")
537 self
.YTilt
= self
.WidgetTree
.get_widget("ytilt")
538 self
.Wheel
= self
.WidgetTree
.get_widget("wheel")
539 self
.ToolID
= self
.WidgetTree
.get_widget("ToolID")
540 self
.TPCRadioButton
= self
.WidgetTree
.get_widget("tpcbutton")
541 self
.ClickForceScale
= self
.WidgetTree
.get_widget("ClickForceScale")
542 self
.ClickForceFrame
= self
.WidgetTree
.get_widget("ClickForceFrame")
543 self
.SideSwitchFrame
= self
.WidgetTree
.get_widget("SideSwitchFrame")
544 self
.TiltFrame
= self
.WidgetTree
.get_widget("TiltFrame")
545 self
.WheelFrame
= self
.WidgetTree
.get_widget("WheelFrame")
547 self
.Curve
= PressureCurveWidget()
549 self
.WidgetTree
.get_widget("PressureVBox").add(self
.Curve
)
551 self
.DrawingArea
= DrawingTestWidget()
552 self
.DrawingArea
.show()
553 self
.WidgetTree
.get_widget("DrawingAlignment").add(self
.DrawingArea
)
556 self
.DeviceMode
= None
557 self
.DeviceName
= None
559 self
.DeviceCombo
.connect("changed", self
.DeviceSelected
)
560 self
.ModeCombo
.connect("changed", self
.ModeChanged
)
561 self
.ClickForceScale
.connect("value-changed", self
.ClickForceChanged
)
562 self
.TPCRadioButton
.connect("toggled", self
.TPCButtonToggled
)
564 self
.DeviceList
= gtk
.ListStore(str)
565 self
.DeviceCell
= gtk
.CellRendererText()
566 self
.DeviceCombo
.pack_start(self
.DeviceCell
, True)
567 self
.DeviceCombo
.add_attribute(self
.DeviceCell
, 'text', 0)
568 self
.DeviceCombo
.set_model(self
.DeviceList
)
570 self
.ClickForce
= None
571 self
.TPCButton
= None
574 ''' Set up device list and start main window app.
576 for d
in gtk
.gdk
.devices_list():
577 devicename
= str(d
.name
)
578 self
.DeviceList
.append([devicename
])
579 toolID
= xswGet(devicename
, "ToolID")
580 if toolID
>= 0: # valid wacom device
581 self
.Device
= max(len(self
.DeviceList
) - 1, 0)
585 self
.DeviceCombo
.set_active(self
.Device
)
586 self
.DeviceName
= gtk
.gdk
.devices_list()[self
.Device
].name
587 self
.UpdateChildren()
588 gobject
.idle_add(self
.Update
)
590 self
.MainWindow
.run()
591 self
.MainWindow
.hide()
593 def GetPressure(self
):
594 ''' Return current device pressure.
596 dev
= gtk
.gdk
.devices_list()[self
.Device
]
597 state
= dev
.get_state(self
.DrawingArea
.window
)
598 return dev
.get_axis(state
[0], gtk
.gdk
.AXIS_PRESSURE
)
601 ''' Return current device tilt as (xtilt, ytilt).
603 dev
= gtk
.gdk
.devices_list()[self
.Device
]
604 state
= dev
.get_state(self
.DrawingArea
.window
)
606 x
= float(dev
.get_axis(state
[0], gtk
.gdk
.AXIS_XTILT
))
607 y
= float(dev
.get_axis(state
[0], gtk
.gdk
.AXIS_YTILT
))
616 ''' Return current device wheel state.
618 dev
= gtk
.gdk
.devices_list()[self
.Device
]
619 state
= dev
.get_state(self
.DrawingArea
.window
)
621 wheel
= dev
.get_axis(state
[0], gtk
.gdk
.AXIS_WHEEL
)
629 def ModeChanged(self
, widget
):
630 ''' Set changed mode using xsetwacom.
632 xswSet(self
.DeviceName
, "Mode", widget
.get_active())
634 def ClickForceChanged(self
, widget
):
635 ''' Do callback for ClickForce slider "value-changed" event.
637 cf
= widget
.get_value()
638 self
.Curve
.ClickForceLine
= (cf
- 1) / 20.0
639 SetClickForce(self
.DeviceName
, cf
)
641 def TPCButtonToggled(self
, widget
):
642 ''' Handle "toggled" event from the TPCButton radio button.
644 if self
.TPCButton
== widget
.get_active():
646 self
.TPCButton
= widget
.get_active()
647 xswSet(self
.DeviceName
, "TPCButton", self
.TPCButton
)
649 def UpdateDeviceMode(self
):
650 ''' Update the device mode combo box.
652 self
.DeviceMode
= xswGet(self
.DeviceName
, "Mode")
653 if self
.DeviceMode
== None:
654 self
.ModeCombo
.set_sensitive(False)
656 self
.ModeCombo
.set_sensitive(True)
657 self
.ModeCombo
.set_active(self
.DeviceMode
)
659 def UpdateClickForce(self
):
660 ''' Update the click force slider.
662 self
.ClickForce
= GetClickForce(self
.DeviceName
)
663 if self
.ClickForce
== None:
664 self
.ClickForceFrame
.hide()
666 self
.ClickForceFrame
.show()
667 self
.ClickForceScale
.set_value(self
.ClickForce
)
669 def UpdateTPCButton(self
):
670 ''' Update the TPCButton radio button group.
672 self
.TPCButton
= xswGet(self
.DeviceName
, "TPCButton")
674 if self
.TPCButton
== None:
675 self
.SideSwitchFrame
.hide()
678 self
.SideSwitchFrame
.show()
679 self
.TPCButton
= bool(self
.TPCButton
)
681 if self
.TPCButton
== self
.TPCRadioButton
.get_active():
685 self
.TPCRadioButton
.set_active(True)
688 for button
in self
.TPCRadioButton
.get_group():
689 if button
!= TPCRadioButton
: # there's only one other
690 button
.set_active(True)
693 def UpdateChildren(self
):
694 ''' Update the child widgets to reflect current settings.
696 self
.UpdateDeviceMode()
697 self
.UpdateClickForce()
698 self
.UpdateTPCButton()
700 def DeviceSelected(self
, widget
):
701 ''' Update the various parts of the applet for a new device.
703 self
.Device
= widget
.get_active()
704 self
.DrawingArea
.Device
= self
.Device
705 self
.DeviceName
= gtk
.gdk
.devices_list()[self
.Device
].name
706 self
.Curve
.SetDevice(self
.DeviceName
)
707 self
.UpdateChildren()
710 p
= self
.GetPressure()
713 self
.Curve
.Pressure
= None
716 self
.Curve
.Pressure
= p
722 self
.TiltFrame
.hide()
724 self
.TiltFrame
.show()
725 self
.XTilt
.set_adjustment(gtk
.Adjustment(t
[0], -1.0, 1.0))
726 self
.YTilt
.set_adjustment(gtk
.Adjustment(t
[1], -1.0, 1.0))
731 self
.WheelFrame
.hide()
733 self
.WheelFrame
.show()
734 self
.Wheel
.set_adjustment(gtk
.Adjustment(w
, -1.0, 1.0))
736 id = xswGet(self
.DeviceName
, "ToolID")
741 self
.ToolID
.set_label(str(id))
745 ################################################################################
748 if __name__
== '__main__':
751 parser
= optparse
.OptionParser()
752 parser
.add_option("-l", "--local", action
="store_true", dest
="runlocal",
753 default
=False, help="Run from current directory.")
754 parser
.add_option("-f", "--file", dest
="filename",
755 help="write config to FILE", metavar
="FILE")
756 parser
.add_option("-s", "--set", action
="store_true", dest
="setandexit",
757 default
=False, help="set options and exit (don't run GUI)")
758 parser
.add_option("-g", "--get", action
="store_true", dest
="getandexit",
759 default
=False, help="get options and exit (don't run GUI)")
760 (options
, args
) = parser
.parse_args()
763 statusicon
= gtk
.status_icon_new_from_file("./tabletconfig.svg")
764 gladefile
= "./tabletconfig.glade"
766 statusicon
= gtk
.status_icon_new_from_file("/usr/share/icons/tabletconfig.svg")
767 gladefile
= "/usr/share/tablet-apps/tabletconfig.glade"
769 a
= GraphicsTabletApplet(gladefile
, statusicon
)