Source code for microscopy.keypad

""":mod:`Tkinter` virtual keypad for motion devices.

This is part of the :mod:`microscopy` package.

The devices must conform to the interface defined in :class:`hal.Actuator`.

:Example:

To create a keypad for three axes X, Y and Z, 
controlled by PageUp/PageDown, Up/Down and Left/Right respectively:

>>> kp = keypad.Keypad(((z, ('Prior', 'Next')), 
                        (x, ('Up', 'Down')), 
                        (y, ('Left', 'Right'))), 
                       title='XYZ')
                       
Pressing a key will make the device move until the key is released.
Pressing Shift+key at the same time will make an incremental step.
Pressing Ctlr+key will increase or decrese the step size.
Arbitrary numbers of axes are supported.

.. Warning::
   Key repeat for the control keys will be disabled while the widget has focus.
   This requires that the module variable :data:`X_KEY_CODES` knows the correspondance
   between Tkinter key names and the X server keycodes (see Tip below).
   
.. Tip::
   - http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/key-names.html
     for the Tkinter key names.
   - http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/event-handlers.html
     for the how to handle key modifiers (shift, control etc.).
   - Use the ``xev`` utility to figure out the X server key codes.

(c) Guillaume Lepert, November 2015

------------------------------------------
"""

import Tkinter as tk
from subprocess import call
from matplotlib.cbook import Stack
import time

#: X server key codes for enabling/disabling key repeats (as returned by xev)
X_KEY_CODES = {
  'Left': 113, 'Right': 114,
  'Up': 111, 'Down': 116,
  'Prior': 112, 'Next': 117,
  'KP_4': 83, 'KP_6': 85,
  'KP_8': 80, 'KP_2': 88}

# tkinter special key modifiers:
#KEY_MODS = {
  #0x0001: 'Shift',
  #0x0002: 'Caps Lock',
  #0x0004: 'Control',
  #0x0008: 'Left-hand Alt',
  #0X0010: 'Num Lock',
  #0x0080: 'Right-hand Alt',
  #0x0100: 'Mouse button 1',
  #0x0200: 'Mouse button 2',
  #0x0400: 'Mouse button 3'}

#: Tkinter key modifier masks (see http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/event-handlers.html)
KEY_MODS = {
  'Shift':     0x001,
  'Caps_Lock': 0x002,
  'Control':   0x004,
  'Alt_L':     0x008,
  'Num_Lock':  0x010,
  'Alt_R':     0x080,
  'Mouse_1':   0x100,
  'Mouse_2':   0x200,
  'Mouse_3':   0x400}

# Not in use. Is it worth pursuing?
def check_key_modifier(event_state, modifiers):
  if isinstance(modifiers, str):
    modifiers = (modifiers, )
  mods = 0
  for m in modifiers:
    mods |= KEY_MODS[m]    
  return event_state & mods > 0

#event.state >> KEY_MODS['Shift']

#class KeyMods(object):
  #Shift = 1
  #Caps_Lock = 2
  #Control = 3
  #Alt_L = 4
  #Num_Lock = 5
  #Alt_R = 6
  #Mouse_1 = 7
  #Mouse_2 = 8
  #Mouse_3 = 9
  
  #def check(self, state, mod):
    #if 
    #return state >> mod

[docs]class Keypad(object): """Tkinter virtual keypad for motion devices.""" def __init__(self, axes, master=None, title=''): """ :param axes: list of :class:`hal.Actuator` objects. :param master: a Tkinter object. The keypad will be created as a :class:`Tkinter.Toplevel` object. If no master is provided, a new :class:`Tkinter.Tk` instance is started :param title: the window title. """ self.root = tk.Tk() if master is None else tk.Toplevel(master) tab = max([len(axis.name) for (axis, keys) in axes]) self.n_axes = len(axes) # set up text widget self.root.geometry('450x%d' % (20*(self.n_axes*2+4))) self.text = tk.Text(self.root, background='black', foreground='white', font=('Monospace', 12)) self.text.pack() self.root.title(title) for i in range(self.n_axes*3): self.text.insert('1.0', '\n') # Attach axes and print position/step size self.axes = [] for (index, (axis, keys)) in enumerate(axes): self.text.insert('%d.0' % (self.n_axes*2+2+index), axis.name + ': ') self.text.insert('%d.%d' % (self.n_axes*2+2+index, tab+2), '%s/%s ' %(keys[0], keys[1])) setattr(self, axis.name, KeypadAxis(axis, keys, self.text, 2*index+1, tab+1, axis.dir)) self.axes.append(getattr(self, axis.name)) # print instructions self.text.delete('%d.%d' % (self.n_axes*2+2, 18), '%d.0-1c' % (self.n_axes*2+2+1)) self.text.insert('%d.%d' % (self.n_axes*2+2, 18), '| +Shift: step motion.') self.text.delete('%d.%d' % (self.n_axes*2+3, 18), '%d.0-1c' % (self.n_axes*2+3+1)) self.text.insert('%d.%d' % (self.n_axes*2+3, 18), '| +Ctrl: change step size.') # Bindings to delete keystrokes (emulates a text-only widget) # and disable key repeat when widget has focus self.root.bind('<Key>', self.read_only) self.root.bind('<FocusIn>', self.disable_key_repeat) self.root.bind('<FocusOut>', self.enable_key_repeat) self.update_rate = 2
[docs] def disable_key_repeat(self, event): """Disable key repeats for all actuator control keys.""" for axis in self.axes: axis.disable_key_repeat()
[docs] def enable_key_repeat(self, event): """Enable key repeats for all actuator control keys.""" for axis in self.axes: axis.enable_key_repeat()
[docs] def read_only(self, event): """Emulates a read-only text widget by immediately deleting all input characters.""" #print event.char if event.char is not '': self.text.delete('insert-1c')
def update_pos(self): for ax in self.axes: ax.print_pos() self.pos_update_id = self.text.after(self.update_rate, self.update_pos)
[docs]class KeypadAxis(object): """Represent individual actuators within a :class:`Keypad` instance.""" def __init__(self, axis, keys, widget, line, tab=10, dir=1): """ :param axis: :type axis: :class:`hal.Actuator` :param keys: the two controlling keys (see http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/key-names.html for info about Tkinter key identifiers) :param widget: Tkinter widget where the axis should be attached. (normally :attr:`Keypad.text`) :param line: in the text widget, number of the line where the axis position should be printed. :type line: int :param tab: number of characters reserved for printing the axis name :type tab: int :param dir: The requested motion will be multiplied by this number. Most useful to swap the direction of motion if the actuator is set up in such a way that it goes up when it is expected to go down. :type dir: float """ self.axis = axis self.keys = keys self.title = axis.name self.text = widget self.line = line # print widget info on this line self.tab = tab self.t_tab = ''.join([' ' for i in range(self.tab)]) widget.bind('<KeyPress-%s>' % keys[0], self.move_up) widget.bind('<KeyPress-%s>' % keys[1], self.move_down) widget.bind('<KeyRelease-%s>' % keys[0], self.stop) widget.bind('<KeyRelease-%s>' % keys[1], self.stop) self.dir = dir #self.rate = 0 # drift rate, in um/h #self.dt = 10 # drift compensation update perdiod, in s #: Position update rate, in seconds. self.update_rate = 2 # Step size is implemented as a matplotlib cookbook Stack. # Use Actuator property (if it exists) to scale the default list. try: step_scaling = self.axis.step_scaling except AttributeError: step_scaling = 1 self.step = Stack() for s in (1, 0.5, 0.1, 0.05, 0.01, 0.005, 0.001, 0.0005, 0.0001): self.step.push(step_scaling * s) #self.update_pos() self.print_pos() self.print_step()
[docs] def enable_key_repeat(self): """Enable key repeats for the control keys.""" if self.axis.disable_key_repeat: call('xset r %d; xset r %d' % (X_KEY_CODES[self.keys[0]], X_KEY_CODES[self.keys[1]]), shell=True)
[docs] def disable_key_repeat(self): """Disable key repeats for the control keys.""" if self.axis.disable_key_repeat: call('xset -r %d; xset -r %d' % (X_KEY_CODES[self.keys[0]], X_KEY_CODES[self.keys[1]]), shell=True)
[docs] def print_pos(self): """Print the current position on text widget.""" self.text.delete(str(self.line)+'.0', str(self.line+1)+'.0-1c') self.text.insert(str(self.line)+'.0', self.title + self.t_tab) self.text.insert('%d.%d' % (self.line, self.tab), '| Position: %f %s' % (self.axis.position(), self.axis.unit))
[docs] def print_step(self): """Print current step size on text widget.""" self.text.delete(str(self.line+1)+'.0', str(self.line+2)+'.0-1c') self.text.insert(str(self.line+1)+'.0', self.t_tab) self.text.insert('%d.%d' % (self.line+1, self.tab), '| Step size: %g %s' %(self.step(), self.axis.unit))
[docs] def update_pos(self): """Update the axis position at regular interval.""" #def func(): # self.print_pos() self.print_pos() self.axis.pos_after_id = self.text.after(self.update_rate, self.update_pos)
[docs] def move_up(self, event): """Returns a event binding function that moves towards the positive direction.""" if event.state & KEY_MODS['Shift']: self.axis.move_by(+self.dir * self.step()) elif event.state & KEY_MODS['Control'] > 0: self.step.back() self.print_step() else: try: self.axis.move(+self.dir) except NotImplementedError: self.axis.move_by(+self.dir * self.step()) self.print_pos()
[docs] def move_down(self, event): """Returns a event binding function that moves towards the negative direction.""" if event.state & KEY_MODS['Shift']: self.axis.move_by(- self.dir * self.step()) elif event.state & KEY_MODS['Control'] > 0: self.step.forward() self.print_step() else: try: self.axis.move(-self.dir) except NotImplementedError: self.axis.move_by(- self.dir * self.step()) self.print_pos()
[docs] def stop(self, event): """Returns a event binding function that stops motion on the specified axis.""" if not event.state & KEY_MODS['Shift']: # no need to call stop() on step motion self.axis.stop() while self.axis.moving(): time.sleep(0.05) self.print_pos()
def compensate_drift(self): self.text.after(dt*1000, self.compensate_drift)
class Drift(object): def __init__(self, ax, rate, dt): self.ax = ax # KeypadAxis self.rate = rate # in mm/s self.dt = dt self.min_motion = 0.0002 # 200nm def update(self): t = time.time() dx = self.rate * (t - self.t) if abs(dx) > self.min_motion: self.ax.move_by(dx) self.t = t self.after_id = self.ax.text.after(self.dt*1000, self.update) def stop(): self.ax.text.after_cancel(self.after_id) def start(): self.t = time.time() self.after_id = self.ax.text.after(self.dt*1000, self.update)