]> git.neil.brown.name Git - plato.git/blob - lib/tapboard.py
tapboard: make '|' more easily accessible.
[plato.git] / lib / tapboard.py
1 #!/usr/bin/env python
2
3 # Copyright (C) 2011-2012 Neil Brown <neilb@suse.de>
4 #
5 #    This program is free software; you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation; either version 2 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14 #
15 #    You should have received a copy of the GNU General Public License along
16 #    with this program; if not, write to the Free Software Foundation, Inc.,
17 #    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19 #
20 # a library to draw a widget for tap-input of text
21 #
22 # Have a 4x10 array of buttons.  Some buttons enter a symbol,
23 # others switch to a different array of buttons.
24 # The 4 rows aren't the same, but vary like a qwerty keyboard.
25 # Row1:  10 buttons
26 # Row2:   9 buttons offset by half a button
27 # Row3:  10 buttons just like Row1
28 # Row4:   5 buttons each double-width
29 #
30 # vertial press/drag is passed to caller as movement.
31 # press/hold is passed to caller as 'unmap'.
32 # horizontal press/drag modifies the selected button
33 #  on the first 3 rows, it shifts
34 #  on NUM it goes straight to punctuation
35 #
36 # Different configs are:
37 # lower-case alpha, with minimal punctuation
38 # upper-case alpha, with different punctuation
39 # numeric with phone and calculator punctuation
40 # Remaining punctuation with some cursor control.
41 #
42 # Bottom row is normally:
43 #   Shift NUM  SPC ENTER BackSpace
44 # When 'shift' is pressed, the keyboard flips between
45 #   upper/lower or numeric/punc
46 # and bottom row maybe should become:
47 #   lock control alt ... something.
48 #
49 # Need:
50 #  P-up Pdown Arrows
51 #  control alt
52 #
53 # ESC - enter-drag
54 # TAB - space-drag
55 # ctrl - Shift-drag
56 # alt  - num-drag
57 #
58
59 import gtk, pango, gobject
60
61 keymap = {}
62
63 keymap['lower'] = [
64     ['q','w','e','r','t','y','u','i','o','p'],
65     [  'a','s','d','f','g','h','j','k','l'],
66     ['-','z','x','c','v','b','n','m',',','.']
67 ]
68 keymap['lower-xtra'] = [
69     ['1','2','3','4','5','6','7','8','9','0'],
70     [  '/','|',' ',' ',' ',' ',' ',' ',' '],
71     ['$','*',' ',' ',' ',' ','<','>','!','?']
72 ]
73 keymap['lower-shift'] = [
74     ['Q','W','E','R','T','Y','U','I','O','P'],
75     [  'A','S','D','F','G','H','J','K','L'],
76     ['+','Z','X','C','V','B','N','M','\'',':']
77 ]
78 keymap['lower-ctrl'] = [
79     ['q','w','e','r','t','y','u','i','o','p'],
80     [  'a','s','d','f','g','h','j','k','l'],
81     ['Dwn','z','x','c','v','b','n','m','Lft','Rgt']
82 ]
83
84 #keymap['number'] = [
85 #    ['1','2','3','4','5','6','7','8','9','0'],
86 #    [  '+','*','-','/','#','(',')','[',']'],
87 #    ['{','}','<','>','?',',','.','=',':',';']
88 #]
89 keymap['number'] = [
90     ['+','-','*','7','8','9','{','}','<','>'],
91     [  '/','#','4','5','6','(',')','[',']'],
92     ['?','=','0','1','2','3','.',',',':',';']
93 ]
94
95 keymap['number-shift'] = [
96     ['!','@','#','$','%','^','&','*','(',')'],
97     [  '~','`','_',',','.','<','>','\'','"'],
98     ['\\','|','+','=','_','-','Home','End','Insert','Delete']
99 ]
100
101 symmap = {
102     'Home' : '\x1bOH',
103     'End'  : '\x1bOF',
104     'Insert': '\x1b[2~',
105     'Delete': '\x1b[3~',
106     'Del'  :  '\0177',
107     'PgUp' : '\x1b[5~',
108     'PgDn' : '\x1b[6~',
109     'Up'   : '\x1b[A',
110     'Dwn'  : '\x1b[B',
111     'Rgt'  : '\x1b[C',
112     'Lft'  : '\x1b[D',
113 }
114
115 class TapBoard(gtk.VBox):
116     __gsignals__ = {
117         'key' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
118                  (gobject.TYPE_STRING,)),
119         'move': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
120                  (gobject.TYPE_INT, gobject.TYPE_INT)),
121         'hideme' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
122                   ())
123         }
124     def __init__(self, mode='lower'):
125         gtk.rc_parse_string("""
126        style "tap-button-style" {
127              GtkWidget::focus-padding = 0
128              GtkWidget::focus-line-width = 1
129              xthickness = 1
130              ythickness = 0
131          }
132          widget "*.tap-button" style "tap-button-style"
133          """)
134
135         gtk.VBox.__init__(self)
136         self.keysize = 44
137         self.aspect = 1
138         self.width = int(10*self.keysize)
139         self.height = int(4*self.aspect*self.keysize)
140
141         self.dragx = None
142         self.dragy = None
143         self.moved = False
144         self.xmoved = False
145         self.xmin = 100000
146         self.xmax = 0
147
148         self.isize = gtk.icon_size_register("mine", 40, 40)
149
150         self.button_timeout = None
151
152         self.buttons = []
153
154         self.set_homogeneous(True)
155
156         for row in range(3):
157             h = gtk.HBox()
158             h.show()
159             self.add(h)
160             bl = []
161             if row == 1:
162                 l = gtk.Label(''); l.show()
163                 h.pack_start(l, padding=self.keysize/4)
164                 l = gtk.Label(''); l.show()
165                 h.pack_end(l, padding=self.keysize/4)
166             else:
167                 h.set_homogeneous(True)
168             for col in range(9 + abs(row-1)):
169                 b = self.add_button(None, self.tap, (row,col), h)
170                 bl.append(b)
171             self.buttons.append(bl)
172
173         h = gtk.HBox()
174         h.show()
175         h.set_homogeneous(True)
176         self.add(h)
177
178         fd = pango.FontDescription('sans 10')
179         fd.set_absolute_size(50 * pango.SCALE * self.keysize / 80)
180         b = self.add_button('Shft', self.nextshift, False, h, fd)
181         self.shftbutton = b
182
183         b = self.add_button('Num', self.nextmode, True, h, fd)
184         self.modebutton = b
185         b = self.add_button('SPC', self.tap, (-1,(' ','\t')), h, fd)
186         b = self.add_button('Entr', self.tap, (-1,('\n','\x1b')), h, fd)
187         b = self.add_button(gtk.STOCK_UNDO, self.tap, (-1,('\b','Up')), h)
188
189         # mode can be 'lower' or 'number'
190         # shift can be '' or '-shift'
191         # locked can be:
192         #   None when shift is ''
193         #   False with '-shift' for a single shift
194         #   True with '-shift' for a locked shit
195         self.image_mode = ''
196         self.mode = mode
197         self.shift = ''
198         self.locked = None
199         self.size = 0
200         self.connect("size-allocate", self.update_buttons)
201         self.connect("realize", self.update_buttons)
202
203
204     def add_button(self, label, click, arg, box, font = None):
205         if not label:
206             b = gtk.Button()
207             b.set_name("tap-button")
208         elif label[0:4] == 'gtk-':
209             img = gtk.image_new_from_stock(label, self.isize)
210             img.show()
211             b = gtk.Button()
212             b.add(img)
213         else:
214             b = gtk.Button(label)
215         b.show()
216         b.set_property('can-focus', False)
217         if font:
218             b.child.modify_font(font)
219         b.connect('button_press_event', self.press, arg)
220         b.connect('button_release_event', self.release, click, arg)
221         b.connect('motion_notify_event', self.motion)
222         b.add_events(gtk.gdk.POINTER_MOTION_MASK|
223                      gtk.gdk.POINTER_MOTION_HINT_MASK)
224
225         box.add(b)
226         return b
227
228     def update_buttons(self, *a):
229         if self.window == None:
230             return
231
232         alloc = self.buttons[0][0].get_allocation()
233         w = alloc.width; h = alloc.height
234         if w > h:
235             size = h
236         else:
237             size = w
238         size -= 6
239         if size <= 10 or size == self.size:
240             return
241         #print "update buttons", size
242         self.size = size
243
244         # For each button in 3x3 we need 10 images,
245         # one for initial state, and one for each of the new states
246         # So there are two fonts we want.
247         # First we make the initial images
248         fdsingle = pango.FontDescription('sans 10')
249         fdsingle.set_absolute_size(size / 1.1 * pango.SCALE)
250         fdword = pango.FontDescription('sans 10')
251         fdword.set_absolute_size(size / 1.5 * pango.SCALE)
252         fdxtra = pango.FontDescription('sans 10')
253         fdxtra.set_absolute_size(size/2.3 * pango.SCALE)
254
255         bg = self.get_style().bg_gc[gtk.STATE_NORMAL]
256         fg = self.get_style().fg_gc[gtk.STATE_NORMAL]
257         red = self.window.new_gc()
258         red.set_foreground(self.get_colormap().alloc_color(gtk.gdk.color_parse('red')))
259         base_images = {}
260         for mode in keymap.keys():
261             base_images[mode] = 30*[None]
262             for row in range(3):
263                 for col in range(9+abs(1-row)):
264                     if not self.buttons[row][col]:
265                         continue
266                     sym = keymap[mode][row][col]
267                     pm = gtk.gdk.Pixmap(self.window, size, size)
268                     pm.draw_rectangle(bg, True, 0, 0, size, size)
269                     if len(sym) == 1:
270                         self.modify_font(fdsingle)
271                     else:
272                         self.modify_font(fdword)
273                     layout = self.create_pango_layout(sym[0:3])
274                     (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
275                     pm.draw_layout(fg,
276                                    int(size/2 - ew/2),
277                                    int(size/2 - eh/2),
278                                    layout)
279                     if (mode+'-xtra') in keymap:
280                         self.modify_font(fdxtra)
281                         layout = self.create_pango_layout(
282                             keymap[mode+'-xtra'][row][col])
283                         (ink, (ex,ey,ew2,eh2)) = layout.get_pixel_extents()
284                         pm.draw_layout(fg, int(size/2)-1+ew2,int(size/2)-eh2,layout)
285                     im = gtk.Image()
286                     im.set_from_pixmap(pm, None)
287                     base_images[mode][row*10+col] = im
288         self.base_images = base_images
289         fd = pango.FontDescription('sans 10')
290         fd.set_absolute_size(size / 1.5 * pango.SCALE)
291         self.modify_font(fd)
292         self.set_button_images()
293
294     def set_button_images(self):
295         if (self.mode + self.shift) in keymap:
296             mode = self.mode + self.shift
297         else:
298             # probably -ctrl: no separe keymap
299             mode = self.mode
300         if self.image_mode == mode:
301             return
302         for row in range(3):
303             for col in range(9+abs(row-1)):
304                 b = self.buttons[row][col]
305                 if not b:
306                     continue
307                 im = self.base_images[mode][row*10+col]
308                 if im:
309                     b.set_image(im)
310         self.image_mode = mode
311
312
313     def tap(self, rc, moved):
314         (row,col) = rc
315         if (self.mode + self.shift) in keymap:
316             m = self.mode + self.shift
317         else:
318             m = self.mode
319         if moved:
320             if moved == 2 and (self.mode + '-xtra') in keymap\
321                     and keymap[self.mode + '-xtra'][row][col] != ' ':
322                 m = self.mode + '-xtra'
323             else:
324                 m = self.mode + '-shift'
325         if row < 0 :
326             if moved:
327                 sym = col[1]
328             else:
329                 sym = col[0]
330         else:
331             sym = keymap[m][row][col]
332         if self.shift == '-ctrl' and len(sym) == 1 and sym.isalpha():
333             sym = chr(ord(sym) & 31)
334
335         if sym in symmap:
336             sym = symmap[sym]
337         self.emit('key', sym)
338         if self.shift and not self.locked:
339             self.nextshift(True)
340
341     def press(self, widget, ev, arg):
342         self.dragx = int(ev.x_root)
343         self.dragy = int(ev.y_root)
344         self.moved = False
345         self.xmoved = False
346         self.xmin = self.dragx
347         self.xmax = self.dragx
348
349         # press-and-hold makes us disappear
350         if self.button_timeout:
351             gobject.source_remove(self.button_timeout)
352             self.button_timeout = None
353         self.button_timeout = gobject.timeout_add(500, self.disappear)
354
355     def release(self, widget, ev, click, arg):
356         dx = self.dragx
357         dy = self.dragy
358         y = int(ev.y_root)
359         self.dragx = None
360         self.dragy = None
361
362         if self.button_timeout:
363             gobject.source_remove(self.button_timeout)
364             self.button_timeout = None
365         if self.moved:
366             self.moved = False
367             # If we are half way off the screen, hide
368             root = gtk.gdk.get_default_root_window()
369             (rx,ry,rwidth,rheight,depth) = root.get_geometry()
370             (x,y,width,height,depth) = self.window.get_geometry()
371             xmid = int(x + width / 2)
372             ymid = int(y + height / 2)
373             if xmid < 0 or xmid > rwidth or \
374                ymid < 0 or ymid > rheight:
375                 self.hide()
376         else:
377             if self.button_timeout:
378                 gobject.source_remove(self.button_timeout)
379                 self.button_timeout = None
380             if self.xmoved:
381                 if self.xmin < dx and self.xmax > dx:
382                     click(arg, 2)
383                 elif abs(y-dy) > 40:
384                     click(arg, 2)
385                 else:
386                     click(arg, 1)
387             else:
388                 click(arg, 0)
389             self.xmoved = False
390
391     def motion(self, widget, ev):
392         if self.dragx == None:
393             return
394         x = int(ev.x_root)
395         y = int(ev.y_root)
396
397         if (not self.xmoved and abs(y-self.dragy) > 40) or self.moved:
398             if not self.moved:
399                 self.emit('move', 0, 0)
400             self.emit('move', x-self.dragx, y-self.dragy)
401             self.moved = True
402             if self.button_timeout:
403                 gobject.source_remove(self.button_timeout)
404                 self.button_timeout = None
405         if (not self.moved and abs(x-self.dragx) > 40) or self.xmoved:
406             self.xmoved = True
407             if x < self.xmin:
408                 self.xmin = x
409             if x > self.xmax:
410                 self.xmax = x
411             if self.button_timeout:
412                 gobject.source_remove(self.button_timeout)
413                 self.button_timeout = None
414         if ev.is_hint:
415             gtk.gdk.flush()
416             ev.window.get_pointer()
417
418     def disappear(self):
419         self.dragx = None
420         self.dragy = None
421         self.emit('hideme')
422
423     def do_buttons(self):
424         self.set_button_images()
425         self.button_timeout = None
426         return False
427
428     def set_buttons_soon(self):
429         if self.button_timeout:
430             gobject.source_remove(self.button_timeout)
431         self.button_timeout = gobject.timeout_add(500, self.do_buttons)
432
433     def nextshift(self, a, moved=False):
434         if moved:
435             lbl = '(ctrl)'
436             self.shift = '-ctrl'
437             self.locked = False
438         elif self.shift == '' and not a:
439             self.shift = '-shift'
440             self.locked = False
441             lbl = 'Lock'
442         elif not self.locked and not a:
443             self.locked = True
444             lbl = 'UnLk'
445             if self.shift == '-ctrl':
446                 lbl = '(ctrl-lock)'
447         else:
448             self.shift = ''
449             lbl = 'Shft'
450
451         self.shftbutton.child.set_text(lbl)
452         if self.shift and not self.locked:
453             self.set_buttons_soon()
454         else:
455             self.set_button_images()
456
457     def nextmode(self, a, moved=False):
458         if self.mode == 'lower':
459             self.mode = 'number'
460             self.modebutton.child.set_text('Abc')
461         else:
462             self.mode = 'lower'
463             self.modebutton.child.set_text('Num')
464         self.nextshift(True)
465         if moved:
466             self.nextshift(False)
467             self.nextshift(False)
468         self.set_button_images()
469
470
471 if __name__ == "__main__" :
472     w = gtk.Window()
473     w.connect("destroy", lambda w: gtk.main_quit())
474     ti = TapBoard()
475     w.add(ti)
476     w.set_default_size(ti.width, ti.height)
477     root = gtk.gdk.get_default_root_window()
478     (x,y,width,height,depth) = root.get_geometry()
479     x = int((width-ti.width)/2)
480     y = int((height-ti.height)/2)
481     w.move(x,y)
482     def pkey(ti, str):
483         print 'key', str
484     ti.connect('key', pkey)
485     def hideme(ti):
486         print 'hidememe'
487         w.hide()
488     ti.connect('hideme', hideme)
489     ti.show()
490     w.show()
491
492     gtk.main()
493