From 6af9dd16737d7f18000f651171a7955d1a5b55cc Mon Sep 17 00:00:00 2001 From: NeilBrown Date: Sat, 21 Apr 2012 21:12:04 +1000 Subject: [PATCH] Add tappad.py tappad is a python library module to present a simple keypad where two taps can produce (nearly) any character. Signed-off-by: NeilBrown --- lib/tappad.py | 438 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 lib/tappad.py diff --git a/lib/tappad.py b/lib/tappad.py new file mode 100644 index 0000000..865134c --- /dev/null +++ b/lib/tappad.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python + +# Copyright (C) 2011-2012 Neil Brown +# +# 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() + -- 2.39.5