--- /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 4x10 array of buttons. Some buttons enter a symbol,
+# others switch to a different array of buttons.
+# The 4 rows aren't the same, but vary like a qwerty keyboard.
+# Row1: 10 buttons
+# Row2: 9 buttons offset by half a button
+# Row3: 10 buttons just like Row1
+# Row4: 5 buttons each double-width
+#
+# vertial press/drag is passed to caller as movement.
+# press/hold is passed to caller as 'unmap'.
+# horizontal press/drag modifies the selected button
+# on the first 3 rows, it shifts
+# on NUM it goes straight to punctuation
+#
+# Different configs are:
+# lower-case alpha, with minimal punctuation
+# upper-case alpha, with different punctuation
+# numeric with phone and calculator punctuation
+# Remaining punctuation with some cursor control.
+#
+# Bottom row is normally:
+# Shift NUM SPC ENTER BackSpace
+# When 'shift' is pressed, the keyboard flips between
+# upper/lower or numeric/punc
+# and bottom row maybe should become:
+# lock control alt ... something.
+
+import gtk, pango, gobject
+
+keymap = {}
+
+keymap['lower'] = [
+ ['q','w','e','r','t','y','u','i','o','p'],
+ [ 'a','s','d','f','g','h','j','k','l'],
+ ['-','z','x','c','v','b','n','m',',','.']
+]
+keymap['lower-xtra'] = [
+ ['1','2','3','4','5','6','7','8','9','0'],
+ [ ' ',' ',' ',' ',' ',' ',' ',' ',' '],
+ ['$',' ',' ',' ',' ',' ',' ',' ','!','?']
+]
+keymap['lower-shift'] = [
+ ['Q','W','E','R','T','Y','U','I','O','P'],
+ [ 'A','S','D','F','G','H','J','K','L'],
+ ['+','Z','X','C','V','B','N','M','\'',':']
+]
+#keymap['number'] = [
+# ['1','2','3','4','5','6','7','8','9','0'],
+# [ '+','*','-','/','#','(',')','[',']'],
+# ['{','}','<','>','?',',','.','=',':',';']
+#]
+keymap['number'] = [
+ ['+','-','*','7','8','9','{','}','<','>'],
+ [ '/','#','4','5','6','(',')','[',']'],
+ ['?','=','0','1','2','3','.',',',':',';']
+]
+
+keymap['number-shift'] = [
+ ['!','@','#','$','%','^','&','*','(',')'],
+ [ '~','`','_',',','.','<','>','\'','"'],
+ ['\\','|','+','=','_','-','Tab','Escape','Delete','Home']
+]
+
+class TapBoard(gtk.VBox):
+ __gsignals__ = {
+ 'key' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ (gobject.TYPE_STRING,)),
+ 'move': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ (gobject.TYPE_INT, gobject.TYPE_INT)),
+ 'hideme' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ ())
+ }
+ def __init__(self):
+ gtk.rc_parse_string("""
+ style "tap-button-style" {
+ GtkWidget::focus-padding = 0
+ GtkWidget::focus-line-width = 1
+ xthickness = 1
+ ythickness = 0
+ }
+ widget "*.tap-button" style "tap-button-style"
+ """)
+
+ gtk.VBox.__init__(self)
+ self.keysize = 44
+ self.aspect = 1
+ self.width = int(10*self.keysize)
+ self.height = int(4*self.aspect*self.keysize)
+
+ self.dragx = None
+ self.dragy = None
+ self.moved = False
+ self.xmoved = False
+ self.xmin = 100000
+ self.xmax = 0
+
+ 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()
+ self.add(h)
+ bl = []
+ if row == 1:
+ l = gtk.Label(''); l.show()
+ h.pack_start(l, padding=self.keysize/4)
+ l = gtk.Label(''); l.show()
+ h.pack_end(l, padding=self.keysize/4)
+ else:
+ h.set_homogeneous(True)
+ for col in range(9 + abs(row-1)):
+ b = self.add_button(None, self.tap, (row,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(50 * pango.SCALE * self.keysize / 80)
+ b = self.add_button('Shft', self.nextshift, False, h, fd)
+ self.shftbutton = b
+
+ b = self.add_button('Num', self.nextmode, True, h, fd)
+ self.modebutton = b
+ b = self.add_button('SPC', self.tap, (-1,' '), h, fd)
+ b = self.add_button('Entr', self.tap, (-1,'\n'), h, fd)
+ b = self.add_button(gtk.STOCK_UNDO, self.tap, (-1,'\b'), h)
+
+ # mode can be 'lower' or 'number'
+ # shift can be '' or '-shift'
+ # locked can be:
+ # None when shift is ''
+ # False with '-shift' for a single shift
+ # True with '-shift' for a locked shit
+ self.image_mode = ''
+ self.mode = 'lower'
+ self.shift = ''
+ self.locked = 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()
+ b.set_name("tap-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 -= 6
+ 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 / 1.1 * pango.SCALE)
+ fdword = pango.FontDescription('sans 10')
+ fdword.set_absolute_size(size / 1.5 * pango.SCALE)
+ fdxtra = pango.FontDescription('sans 10')
+ fdxtra.set_absolute_size(size/2.3 * 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] = 30*[None]
+ for row in range(3):
+ for col in range(9+abs(1-row)):
+ if not self.buttons[row][col]:
+ continue
+ sym = keymap[mode][row][col]
+ pm = gtk.gdk.Pixmap(self.window, size, size)
+ pm.draw_rectangle(bg, True, 0, 0, size, size)
+ 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(fg,
+ int(size/2 - ew/2),
+ int(size/2 - eh/2),
+ layout)
+ if (mode+'-xtra') in keymap:
+ self.modify_font(fdxtra)
+ layout = self.create_pango_layout(
+ keymap[mode+'-xtra'][row][col])
+ (ink, (ex,ey,ew2,eh2)) = layout.get_pixel_extents()
+ pm.draw_layout(fg, int(size/2)-1+ew2,int(size/2)-eh2,layout)
+ im = gtk.Image()
+ im.set_from_pixmap(pm, None)
+ base_images[mode][row*10+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)
+ self.set_button_images()
+
+ def set_button_images(self):
+ mode = self.mode + self.shift
+ if self.image_mode == mode:
+ return
+ for row in range(3):
+ for col in range(9+abs(row-1)):
+ b = self.buttons[row][col]
+ if not b:
+ continue
+ im = self.base_images[mode][row*10+col]
+ if im:
+ b.set_image(im)
+ self.image_mode = mode
+
+
+ def tap(self, rc, moved):
+ (row,col) = rc
+ m = self.mode + self.shift
+ if moved:
+ if moved == 2 and (self.mode + '-xtra') in keymap\
+ and keymap[self.mode + '-xtra'][row][col] != ' ':
+ m = self.mode + '-xtra'
+ else:
+ m = self.mode + '-shift'
+ if row < 0 :
+ sym = col
+ else:
+ sym = keymap[m][row][col]
+ self.emit('key', sym)
+ if self.shift and not self.locked:
+ self.nextshift(True)
+
+ def press(self, widget, ev, arg):
+ self.dragx = int(ev.x_root)
+ self.dragy = int(ev.y_root)
+ self.moved = False
+ self.xmoved = False
+ self.xmin = self.dragx
+ self.xmax = self.dragx
+
+ # press-and-hold makes us disappear
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ self.button_timeout = gobject.timeout_add(500, self.disappear)
+
+ def release(self, widget, ev, click, arg):
+ dx = self.dragx
+ dy = self.dragy
+ y = int(ev.y_root)
+ self.dragx = None
+ self.dragy = None
+
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ if self.moved:
+ self.moved = False
+ # 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()
+ else:
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ if self.xmoved:
+ if self.xmin < dx and self.xmax > dx:
+ click(arg, 2)
+ elif abs(y-dy) > 40:
+ click(arg, 2)
+ else:
+ click(arg, 1)
+ else:
+ click(arg, 0)
+ self.xmoved = False
+
+ def motion(self, widget, ev):
+ if self.dragx == None:
+ return
+ x = int(ev.x_root)
+ y = int(ev.y_root)
+
+ if (not self.xmoved and abs(y-self.dragy) > 40) or self.moved:
+ if not self.moved:
+ self.emit('move', 0, 0)
+ self.emit('move', x-self.dragx, y-self.dragy)
+ self.moved = True
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = None
+ if (not self.moved and abs(x-self.dragx) > 40) or self.xmoved:
+ self.xmoved = True
+ if x < self.xmin:
+ self.xmin = x
+ if x > self.xmax:
+ self.xmax = x
+ 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.dragx = None
+ self.dragy = None
+ self.emit('hideme')
+
+ def do_buttons(self):
+ self.set_button_images()
+ self.button_timeout = None
+ return False
+
+ def set_buttons_soon(self):
+ if self.button_timeout:
+ gobject.source_remove(self.button_timeout)
+ self.button_timeout = gobject.timeout_add(500, self.do_buttons)
+
+ def nextshift(self, a, moved=False):
+ if self.shift == '' and not a:
+ self.shift = '-shift'
+ self.locked = False
+ lbl = 'Lock'
+ elif not self.locked and not a:
+ self.locked = True
+ lbl = 'UnLk'
+ else:
+ self.shift = ''
+ lbl = 'Shft'
+
+ self.shftbutton.child.set_text(lbl)
+ if self.shift and not self.locked:
+ self.set_buttons_soon()
+ else:
+ self.set_button_images()
+
+ def nextmode(self, a, moved=False):
+ if self.mode == 'lower':
+ self.mode = 'number'
+ self.modebutton.child.set_text('Abc')
+ else:
+ self.mode = 'lower'
+ self.modebutton.child.set_text('Num')
+ self.nextshift(True)
+ if moved:
+ self.nextshift(False)
+ self.nextshift(False)
+ self.set_button_images()
+
+
+if __name__ == "__main__" :
+ w = gtk.Window()
+ w.connect("destroy", lambda w: gtk.main_quit())
+ ti = TapBoard()
+ 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.height)/2)
+ w.move(x,y)
+ def pkey(ti, str):
+ print 'key', str
+ ti.connect('key', pkey)
+ def hideme(ti):
+ print 'hidememe'
+ w.hide()
+ ti.connect('hideme', hideme)
+ ti.show()
+ w.show()
+
+ gtk.main()
+