]> git.neil.brown.name Git - freerunner.git/commitdiff
checking tapinput
authorNeil Brown <neilb@suse.de>
Sun, 15 Feb 2009 11:42:17 +0000 (22:42 +1100)
committerNeil Brown <neilb@suse.de>
Sun, 15 Feb 2009 11:42:17 +0000 (22:42 +1100)
tapinput allows entering any character with 2 taps, though you might need to
change 'mode' first to get upper case.

One a 3x3 set of keys, tap the key that show the symbol you want, then
wait a moment and all the keys will change to show a single symbol.
Then tap that symbol to enter it to the focus window.

Once you understand the pattern, you can tap twice without waiting for the
keys to redraw.

In number mode (123) only a single tap is needed for digits.
To get '*' and '#', hold '0' for a moment, then tap which symbol
you want.

The window can be moved by simply dragging it.  If it is moved more than
half off the display, it will disappear.

The window is activated by pressing the keyboard icon in the dock.

input/tapinput.png [new file with mode: 0644]
input/tapinput.py [new file with mode: 0644]
lib/fakeinput.py [new file with mode: 0644]

diff --git a/input/tapinput.png b/input/tapinput.png
new file mode 100644 (file)
index 0000000..a6f815b
Binary files /dev/null and b/input/tapinput.png differ
diff --git a/input/tapinput.py b/input/tapinput.py
new file mode 100644 (file)
index 0000000..2e5f39f
--- /dev/null
@@ -0,0 +1,406 @@
+#!/usr/bin/env python
+
+#
+# experiment with tap input.
+# 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
+from fakeinput import fakeinput
+
+keymap = {}
+
+keymap['lower'] = [
+    ['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','\'','"','|','(',')'],
+    ['[',']',' ','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','\'','"','|','(',')'],
+    ['[',']',' ','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', ' ']
+    ]
+04034
+
+class TapInput(gtk.Window):
+    def __init__(self):
+        gtk.Window.__init__(self, type=gtk.WINDOW_POPUP)
+        self.set_default_size(320, 420)
+        root = gtk.gdk.get_default_root_window()
+        (x,y,width,height,depth) = root.get_geometry()
+        x = int((width-320)/2)
+        y = int((height-420)/2)
+        self.move(x,y)
+
+        self.fi = fakeinput()
+        self.dragx = None
+        self.dragy = None
+        self.moved = False
+
+        self.isize = gtk.icon_size_register("mine", 40, 40)
+
+        self.button_timeout = None
+
+        self.buttons = []
+        v1 = gtk.VBox()
+        v1.show()
+        self.add(v1)
+
+        v = gtk.VBox()
+        v.show()
+        v1.add(v)
+        v.set_homogeneous(True)
+
+        for row in range(3):
+            h = gtk.HBox()
+            h.show()
+            h.set_homogeneous(True)
+            v.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)
+        v.add(h)
+
+        fd = pango.FontDescription('sans 10')
+        fd.set_absolute_size(30 * pango.SCALE)
+        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.update_buttons()
+        self.connect("configure-event", self.update_buttons)
+        self.hide()
+
+    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()
+        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):
+        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
+        fd = pango.FontDescription('sans 10')
+        fd.set_absolute_size(size / 4.5 * pango.SCALE)
+        self.modify_font(fd)
+
+        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
+                            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.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)
+                                (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:
+            sym = keymap[self.mode][self.prefix][rc]
+            self.fi.send_char(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
+            self.button_timeout = gobject.timeout_add(500, self.do_buttons)
+
+    def release(self, widget, ev, click, arg):
+        self.dragx = None
+        self.dragy = 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.fi.send_char(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 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.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('mode')
+        self.set_button_images()
+
+    def delete(self, a):
+        if self.prefix == None:
+            self.fi.send_char("\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()
+
+
+    def activate(self, *a):
+        root = gtk.gdk.get_default_root_window()
+        (x,y,width,height,depth) = root.get_geometry()
+        x = int((width-320)/2)
+        y = int((height-420)/2)
+        self.move(x,y)
+        self.fi.new_window()
+        self.show()
+        self.move(x,y)
+
+
+class KeyboardIcon(gtk.StatusIcon):
+    def __init__(self, x):
+        gtk.StatusIcon.__init__(self)
+        self.set_from_file('/usr/local/pixmaps/tapinput.png')
+        self.connect('activate', x.activate)
+
+win = TapInput()
+ico = KeyboardIcon(win)
+gtk.main()
+
diff --git a/lib/fakeinput.py b/lib/fakeinput.py
new file mode 100644 (file)
index 0000000..3b96cb0
--- /dev/null
@@ -0,0 +1,147 @@
+#!/usr/bin/env python
+
+# fakeinput.py
+# based on pykey, see also "crikey" and http://shallowsky.com/blog/tags/crikey/
+
+import Xlib.display
+import Xlib.X
+import Xlib.XK
+import Xlib.protocol.event
+
+UseXTest = True
+
+try :
+    import Xlib.ext.xtest
+except ImportError:
+    UseXTest = False
+    print "no XTest extension; using XSendEvent"
+
+import sys, time
+
+special_X_keysyms = {
+    '\b': "BackSpace",
+    ' ' : "space",
+    '\t' : "Tab",
+    '\n' : "Return",  # for some reason this needs to be cr, not lf
+    '\r' : "Return",
+    '\e' : "Escape",
+    '!' : "exclam",
+    '#' : "numbersign",
+    '%' : "percent",
+    '$' : "dollar",
+    '&' : "ampersand",
+    '"' : "quotedbl",
+    '\'' : "apostrophe",
+    '(' : "parenleft",
+    ')' : "parenright",
+    '*' : "asterisk",
+    '=' : "equal",
+    '+' : "plus",
+    ',' : "comma",
+    '-' : "minus",
+    '.' : "period",
+    '/' : "slash",
+    ':' : "colon",
+    ';' : "semicolon",
+    '<' : "less",
+    '>' : "greater",
+    '?' : "question",
+    '@' : "at",
+    '[' : "bracketleft",
+    ']' : "bracketright",
+    '\\' : "backslash",
+    '^' : "asciicircum",
+    '_' : "underscore",
+    '`' : "grave",
+    '{' : "braceleft",
+    '|' : "bar",
+    '}' : "braceright",
+    '~' : "asciitilde"
+    }
+
+def get_keysym(ch) :
+    keysym = Xlib.XK.string_to_keysym(ch)
+    if keysym == 0 :
+        # Unfortunately, although this works to get the correct keysym
+        # i.e. keysym for '#' is returned as "numbersign"
+        # the subsequent display.keysym_to_keycode("numbersign") is 0.
+        keysym = Xlib.XK.string_to_keysym(special_X_keysyms[ch])
+    return keysym
+
+def is_shifted(ch) :
+    if ch.isupper() :
+        return True
+    if "~!@#$%^&*()_+{}|:\"<>?".find(ch) >= 0 :
+        return True
+    return False
+
+class fakeinput:
+    def __init__(self, display = None):
+        global UseXTest
+        if display:
+            self.display = display
+        else:
+            self.display = Xlib.display.Display()
+
+        self.UseXTest = UseXTest
+        
+        if UseXTest and not self.display.query_extension("XTEST") :
+            self.UseXTest = False
+
+        self.new_window()
+
+    def new_window(self):
+        if not self.UseXTest:
+            self.window = self.display.get_input_focus()._data["focus"];
+
+    def char_to_keycode(self, ch):
+        keysym = get_keysym(ch)
+        keycode = self.display.keysym_to_keycode(keysym)
+        if keycode == 0 :
+            print "fakeinput: Sorry, can't map", ch
+
+        if (is_shifted(ch)) :
+            shift_mask = Xlib.X.ShiftMask
+        else :
+            shift_mask = 0
+
+        return keycode, shift_mask
+
+    def send_char(self, ch, dosync = True) :
+        keycode, shift_mask = self.char_to_keycode(ch)
+        if (self.UseXTest) :
+            if shift_mask != 0 :
+                Xlib.ext.xtest.fake_input(self.display, Xlib.X.KeyPress, 50)
+            Xlib.ext.xtest.fake_input(self.display, Xlib.X.KeyPress, keycode)
+            Xlib.ext.xtest.fake_input(self.display, Xlib.X.KeyRelease, keycode)
+            if shift_mask != 0 :
+                Xlib.ext.xtest.fake_input(self.display, Xlib.X.KeyRelease, 50)
+        else :
+            event = Xlib.protocol.event.KeyPress(
+                time = int(time.time()),
+                root = self.display.screen().root,
+                window = self.window,
+                same_screen = 0, child = Xlib.X.NONE,
+                root_x = 0, root_y = 0, event_x = 0, event_y = 0,
+                state = shift_mask,
+                detail = keycode
+                )
+            self.window.send_event(event, propagate = True)
+            event = Xlib.protocol.event.KeyRelease(
+                time = int(time.time()),
+                root = self.display.screen().root,
+                window = self.window,
+                same_screen = 0, child = Xlib.X.NONE,
+                root_x = 0, root_y = 0, event_x = 0, event_y = 0,
+                state = shift_mask,
+                detail = keycode
+                )
+            self.window.send_event(event, propagate = True)
+        if dosync:
+            self.display.sync()
+
+    def send_str(self, str):
+        for ch in str:
+            self.send_char(ch, dosync = False)
+        self.display.sync()
+