]> git.neil.brown.name Git - plato.git/commitdiff
Add tappad.py
authorNeilBrown <neilb@suse.de>
Sat, 21 Apr 2012 11:12:04 +0000 (21:12 +1000)
committerNeilBrown <neilb@suse.de>
Sat, 21 Apr 2012 11:21:05 +0000 (21:21 +1000)
tappad is a python library module to present a simple
keypad where two taps can produce (nearly) any character.

Signed-off-by: NeilBrown <neilb@suse.de>
lib/tappad.py [new file with mode: 0644]

diff --git a/lib/tappad.py b/lib/tappad.py
new file mode 100644 (file)
index 0000000..865134c
--- /dev/null
@@ -0,0 +1,438 @@
+#!/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()
+