]> git.neil.brown.name Git - plato.git/commitdiff
Add tapboard.py
authorNeilBrown <neilb@suse.de>
Sat, 21 Apr 2012 11:22:04 +0000 (21:22 +1000)
committerNeilBrown <neilb@suse.de>
Sat, 21 Apr 2012 11:22:04 +0000 (21:22 +1000)
This is similar in concept to tappad.py.
However instead of 3x4 it has 10x4 with many more buttons
which only need to be tapped once.  Dragging buttons give
more options.
But the buttons are quite small.

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

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