--- /dev/null
+#!/usr/bin/env python
+
+# Copyright (C) 2011-2012 Neil Brown <neilb@suse.de>
+#
+# 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.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+#
+# A library to draw a widget for tap-input of text
+#
+# Have a 3x4 array of buttons.
+# Enter any symbol by tapping two buttons from the top 3x3,
+# or bottom-middle
+# Other Bottom buttons are: mode and cancel/delete
+# mode cycles : lower, caps, upper, number (abc Abc ABC 123)
+# cancel is effective after a single tap, delete when no pending tap
+#
+# The 3x3 keys normally show a 3x3 matrix of what they enable
+# When one is tapped, all keys change to show a single symbol (after a pause).
+#
+# "123" works in 'single tap' mode. A single tap makes the symbol in the center
+# of the key appear. To get other symbols (*, #), hold the relevant key until
+# other symbols appear. Then tap.
+#
+# The window can be dragged by touch-and-drag anywhere.
+# If the window is dragged more than 1/2 off the screen, it disappears.
+
+import gtk, pango, gobject
+
+keymap = {}
+
+keymap['lower'] = [
+ ['0','1','Tab','2','3','Delete','?','@','#'],
+ ['b','c','d','f','g','h',' ','Down',' '],
+ ['<','4','5','>','6','7','Return','{','}'],
+ ['j','k','~','l','m','Right','n','p','`'],
+ ['a','e','i','o',' ','u','r','s','t'],
+ ['\\',';',':','Left','\'','"','|','(',')'],
+ ['[',']','Escape','8','9','=','+','-','_'],
+ [' ','Up',' ','q','v','w','x','y','z'],
+ ['!','$','%','^','*','/','&',',','.'],
+ None,
+ [' ',' ',' ',' ',' ',' ','Left','Up','Right',' ','Down',' ']
+
+ ]
+keymap['UPPER'] = [
+ ['0','1','Tab','2','3',' ','?','@','#'],
+ ['B','C','D','F','G','H',' ','Down',' '],
+ ['<','4','5','>','6','7','Return','{','}'],
+ ['J','K','~','L','M','Right','N','P','`'],
+ ['A','E','I','O',' ','U','R','S','T'],
+ ['\\',';',':','Left','\'','"','|','(',')'],
+ ['[',']','Escape','8','9','=','+','-','_'],
+ [' ','Up',' ','Q','V','W','X','Y','Z'],
+ ['!','$','%','^','*','/','&',',','.'],
+ None,
+ [' ',' ',' ',' ',' ',' ','Left','Up','Right',' ','Down',' ']
+ ]
+keymap['number'] = [
+ ['1',' ',' ',' ',' ',' ',' ',' ',' '],
+ [' ','2',' ',' ',' ',' ',' ',' ',' '],
+ [' ',' ','3',' ',' ',' ',' ',' ',' '],
+ [' ',' ',' ','4',' ',' ',' ',' ',' '],
+ [' ',' ',' ',' ','5',' ',' ',' ',' '],
+ [' ',' ',' ',' ',' ','6',' ',' ',' '],
+ [' ',' ',' ',' ',' ',' ','7',' ',' '],
+ [' ',' ',' ',' ',' ',' ',' ','8',' '],
+ [' ',' ',' ',' ',' ',' ',' ',' ','9'],
+ None,
+ ['+',' ','-','.',' ','/','*',' ','#', ' ', '0', ' ']
+ ]
+
+class TapPad(gtk.VBox):
+ __gsignals__ = {
+ 'key' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ (gobject.TYPE_STRING,))
+ }
+ def __init__(self):
+ gtk.VBox.__init__(self)
+ self.keysize = 80
+ self.width = int(3*self.keysize)
+ self.height = int(4.2*self.keysize)
+
+ self.dragx = None
+ self.dragy = None
+ self.moved = False
+
+ self.isize = gtk.icon_size_register("mine", 40, 40)
+
+ self.button_timeout = None
+
+ self.buttons = []
+
+ self.set_homogeneous(True)
+
+ for row in range(3):
+ h = gtk.HBox()
+ h.show()
+ h.set_homogeneous(True)
+ self.add(h)
+ bl = []
+ for col in range(3):
+ b = self.add_button(None, self.tap, row*3+col, h)
+ bl.append(b)
+ self.buttons.append(bl)
+
+ h = gtk.HBox()
+ h.show()
+ h.set_homogeneous(True)
+ self.add(h)
+
+ fd = pango.FontDescription('sans 10')
+ fd.set_absolute_size(30 * pango.SCALE * self.keysize / 80)
+ b = self.add_button('abc', self.nextmode, None, h, fd)
+ self.modebutton = b
+
+ b = self.add_button(None, self.tap, 10, h)
+ self.buttons.append([None, b, None])
+
+ b = self.add_button(gtk.STOCK_UNDO, self.delete, None, h)
+
+ self.mode = 'lower'
+ self.taps = 2
+ self.single = False
+ self.prefix = None
+ self.prefix1 = None
+ self.size = 0
+ self.connect("size-allocate", self.update_buttons)
+ self.connect("realize", self.update_buttons)
+
+
+ def add_button(self, label, click, arg, box, font = None):
+ if not label:
+ b = gtk.Button()
+ elif label[0:4] == 'gtk-':
+ img = gtk.image_new_from_stock(label, self.isize)
+ img.show()
+ b = gtk.Button()
+ b.add(img)
+ else:
+ b = gtk.Button(label)
+ b.show()
+ b.set_property('can-focus', False)
+ if font:
+ b.child.modify_font(font)
+ b.connect('button_press_event', self.press, arg)
+ b.connect('button_release_event', self.release, click, arg)
+ #b.connect('motion_notify_event', self.motion)
+ b.add_events(gtk.gdk.POINTER_MOTION_MASK|
+ gtk.gdk.POINTER_MOTION_HINT_MASK)
+
+ box.add(b)
+ return b
+
+ def update_buttons(self, *a):
+ if self.window == None:
+ return
+
+ alloc = self.buttons[0][0].get_allocation()
+ w = alloc.width; h = alloc.height
+ if w > h:
+ size = h
+ else:
+ size = w
+ size -= 12
+ if size <= 10 or size == self.size:
+ return
+ #print "update buttons", size
+ self.size = size
+
+ # For each button in 3x3 we need 10 images,
+ # one for initial state, and one for each of the new states
+ # So there are two fonts we want.
+ # First we make the initial images
+ fdsingle = pango.FontDescription('sans 10')
+ fdsingle.set_absolute_size(size / 3.5 * pango.SCALE)
+ fdword = pango.FontDescription('sans 10')
+ fdword.set_absolute_size(size / 4.5 * pango.SCALE)
+
+ bg = self.get_style().bg_gc[gtk.STATE_NORMAL]
+ fg = self.get_style().fg_gc[gtk.STATE_NORMAL]
+ red = self.window.new_gc()
+ red.set_foreground(self.get_colormap().alloc_color(gtk.gdk.color_parse('red')))
+ base_images = {}
+ for mode in keymap.keys():
+ base_images[mode] = 12*[None]
+ for row in range(4):
+ for col in range(3):
+ if not self.buttons[row][col]:
+ continue
+ if row*3+col >= len(keymap[mode]):
+ continue
+ syms = keymap[mode][row*3+col]
+ pm = gtk.gdk.Pixmap(self.window, size, size)
+ pm.draw_rectangle(bg, True, 0, 0, size, size)
+ for r in range(4):
+ for c in range(3):
+ if r*3+c >= len(syms):
+ continue
+ sym = syms[r*3+c]
+ if sym == ' ':
+ continue
+ xpos = ((c-col+1)*2+1)
+ ypos = ((r-row+1)*2+1)
+ colour = fg
+ if xpos != xpos%6:
+ xpos = xpos%6
+ colour = red
+ if ypos != ypos%6:
+ ypos = ypos%6
+ colour = red
+ if len(sym) == 1:
+ self.modify_font(fdsingle)
+ else:
+ self.modify_font(fdword)
+ layout = self.create_pango_layout(sym[0:3])
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ pm.draw_layout(colour,
+ int(xpos*size/6 - ew/2),
+ int(ypos*size/6 - eh/2),
+ layout)
+ im = gtk.Image()
+ im.set_from_pixmap(pm, None)
+ base_images[mode][row*3+col] = im
+ self.base_images = base_images
+ fd = pango.FontDescription('sans 10')
+ fd.set_absolute_size(size / 1.5 * pango.SCALE)
+ self.modify_font(fd)
+ sup_images = {}
+ sup_by_sym = {}
+ for mode in keymap.keys():
+ sup_images[mode] = 12*[None]
+ for row in range(4):
+ for col in range(3):
+ ilist = 12 * [None]
+ for r in range(4):
+ for c in range(3):
+ if r*3+c >= len(keymap[mode]):
+ continue
+ if keymap[mode][r*3+c] == None:
+ continue
+ if row*3+col >= len(keymap[mode][r*3+c]):
+ continue
+ sym = keymap[mode][r*3+c][row*3+col]
+ if sym == ' ':
+ continue
+ if sym in sup_by_sym:
+ im = sup_by_sym[sym]
+ else:
+ pm = gtk.gdk.Pixmap(self.window, size, size)
+ pm.draw_rectangle(bg, True, 0, 0, size, size)
+ layout = self.create_pango_layout(sym[0:3])
+ (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
+ pm.draw_layout(fg,
+ int((size - ew)/2), int((size - eh)/2),
+ layout)
+ im = gtk.Image()
+ im.set_from_pixmap(pm, None)
+ sup_by_sym[sym] = im
+ ilist[r*3+c] = im
+ sup_images[mode][row*3+col] = ilist
+ self.sup_images = sup_images
+ self.set_button_images()
+
+
+ def set_button_images(self):
+ for row in range(4):
+ for col in range(3):
+ b = self.buttons[row][col]
+ if not b:
+ continue
+ p = self.prefix
+ if p == None:
+ p = self.prefix1
+ if p == None:
+ im = self.base_images[self.mode][row*3+col]
+ else:
+ im = self.sup_images[self.mode][row*3+col][p]
+ if im:
+ b.set_image(im)
+
+
+ def tap(self, rc):
+ if self.prefix == None:
+ self.prefix = rc
+ if self.taps == 2:
+ self.button_timeout = gobject.timeout_add(500, self.do_buttons)
+ else:
+ kmap = keymap[self.mode][self.prefix]
+ if rc < len(kmap):
+ sym = kmap[rc]
+ self.emit('key', sym)
+ self.noprefix()
+
+ def press(self, widget, ev, arg):
+ self.dragx = int(ev.x_root)
+ self.dragy = int(ev.y_root)
+ #self.startx, self.starty = self.get_position()
+ if arg != None and self.taps == 1 and self.button_timeout == None and self.prefix == None:
+ self.prefix1 = arg
+ if not self.button_timeout:
+ self.button_timeout = gobject.timeout_add(300, self.do_buttons)
+ if arg == None:
+ # press-and-hold makes us disappear
+ if not self.button_timeout:
+ self.button_timeout = gobject.timeout_add(500, self.disappear)
+
+ def release(self, widget, ev, click, arg):
+ self.dragx = None
+ self.dragy = None
+ if arg == None:
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ if self.moved:
+ self.moved = False
+ self.noprefix()
+ # If we are half way off the screen, hide
+ root = gtk.gdk.get_default_root_window()
+ (rx,ry,rwidth,rheight,depth) = root.get_geometry()
+ (x,y,width,height,depth) = self.window.get_geometry()
+ xmid = int(x + width / 2)
+ ymid = int(y + height / 2)
+ if xmid < 0 or xmid > rwidth or \
+ ymid < 0 or ymid > rheight:
+ self.hide()
+ elif arg != None and self.taps == 1 and self.button_timeout:
+ # quick tap in single tap mode, just enter the symbol
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ num = arg
+ sym = keymap[self.mode][num][num]
+ self.emit('key', sym)
+ else:
+ click(arg)
+
+ def motion(self, widget, ev):
+ if self.dragx == None:
+ return
+ x = int(ev.x_root)
+ y = int(ev.y_root)
+
+ if abs(x-self.dragx)+abs(y-self.dragy) > 40 or self.moved:
+ self.move(self.startx+x-self.dragx,
+ self.starty+y-self.dragy);
+ self.moved = True
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ if ev.is_hint:
+ gtk.gdk.flush()
+ ev.window.get_pointer()
+
+ def disappear(self):
+ self.hide()
+ self.dragx = None
+ self.dragy = None
+
+ def do_buttons(self):
+ self.set_button_images()
+ self.button_timeout = None
+ return False
+
+
+ def nextmode(self, a):
+ if self.prefix:
+ return self.noprefix()
+ if self.prefix1:
+ self.noprefix()
+ if self.mode == 'lower':
+ self.mode = 'UPPER'
+ self.single = True
+ self.modebutton.child.set_text('Abc')
+ elif self.mode == 'UPPER' and self.single:
+ self.single = False
+ self.modebutton.child.set_text('ABC')
+ elif self.mode == 'UPPER' and not self.single:
+ self.mode = 'number'
+ self.taps = 1
+ self.modebutton.child.set_text('123')
+ else:
+ self.mode = 'lower'
+ self.taps = 2
+ self.modebutton.child.set_text('abc')
+ self.set_button_images()
+
+ def delete(self, a):
+ if self.prefix == None:
+ self.emit('key', '\b')
+ else:
+ self.noprefix()
+
+ def noprefix(self):
+ self.prefix = None
+ self.prefix1 = None
+
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ else:
+ self.set_button_images()
+
+ if self.single:
+ self.mode = 'lower'
+ self.single = False
+ self.modebutton.child.set_text('abc')
+ self.set_button_images()
+
+if __name__ == "__main__" :
+ w = gtk.Window()
+ w.connect("destroy", lambda w: gtk.main_quit())
+ ti = TapPad()
+ w.add(ti)
+ w.set_default_size(ti.width, ti.height)
+ root = gtk.gdk.get_default_root_window()
+ (x,y,width,height,depth) = root.get_geometry()
+ x = int((width-ti.width)/2)
+ y = int((height-ti.width)/2)
+ w.move(x,y)
+ def pkey(ti, str):
+ print 'key', str
+ ti.connect('key', pkey)
+ ti.show()
+ w.show()
+
+ gtk.main()
+