3 # Copyright (C) 2011-2012 Neil Brown <neilb@suse.de>
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.
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.
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.
20 # a library to draw a widget for tap-input of text
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.
26 # Row2: 9 buttons offset by half a button
27 # Row3: 10 buttons just like Row1
28 # Row4: 5 buttons each double-width
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
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.
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.
59 import gtk, pango, gobject
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',',','.']
68 keymap['lower-xtra'] = [
69 ['1','2','3','4','5','6','7','8','9','0'],
70 [ '/','|',' ',' ',' ',' ',' ',' ',' '],
71 ['$','*',' ',' ',' ',' ','<','>','!','?']
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','\'',':']
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']
85 # ['1','2','3','4','5','6','7','8','9','0'],
86 # [ '+','*','-','/','#','(',')','[',']'],
87 # ['{','}','<','>','?',',','.','=',':',';']
90 ['+','-','*','7','8','9','{','}','<','>'],
91 [ '/','#','4','5','6','(',')','[',']'],
92 ['?','=','0','1','2','3','.',',',':',';']
95 keymap['number-shift'] = [
96 ['!','@','#','$','%','^','&','*','(',')'],
97 [ '~','`','_',',','.','<','>','\'','"'],
98 ['\\','|','+','=','_','-','Home','End','Insert','Delete']
115 class TapBoard(gtk.VBox):
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,
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
132 widget "*.tap-button" style "tap-button-style"
135 gtk.VBox.__init__(self)
138 self.width = int(10*self.keysize)
139 self.height = int(4*self.aspect*self.keysize)
148 self.isize = gtk.icon_size_register("mine", 40, 40)
150 self.button_timeout = None
154 self.set_homogeneous(True)
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)
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)
171 self.buttons.append(bl)
175 h.set_homogeneous(True)
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)
183 b = self.add_button('Num', self.nextmode, True, h, fd)
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)
189 # mode can be 'lower' or 'number'
190 # shift can be '' or '-shift'
192 # None when shift is ''
193 # False with '-shift' for a single shift
194 # True with '-shift' for a locked shit
200 self.connect("size-allocate", self.update_buttons)
201 self.connect("realize", self.update_buttons)
204 def add_button(self, label, click, arg, box, font = None):
207 b.set_name("tap-button")
208 elif label[0:4] == 'gtk-':
209 img = gtk.image_new_from_stock(label, self.isize)
214 b = gtk.Button(label)
216 b.set_property('can-focus', False)
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)
228 def update_buttons(self, *a):
229 if self.window == None:
232 alloc = self.buttons[0][0].get_allocation()
233 w = alloc.width; h = alloc.height
239 if size <= 10 or size == self.size:
241 #print "update buttons", size
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)
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')))
260 for mode in keymap.keys():
261 base_images[mode] = 30*[None]
263 for col in range(9+abs(1-row)):
264 if not self.buttons[row][col]:
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)
270 self.modify_font(fdsingle)
272 self.modify_font(fdword)
273 layout = self.create_pango_layout(sym[0:3])
274 (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents()
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)
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)
292 self.set_button_images()
294 def set_button_images(self):
295 if (self.mode + self.shift) in keymap:
296 mode = self.mode + self.shift
298 # probably -ctrl: no separe keymap
300 if self.image_mode == mode:
303 for col in range(9+abs(row-1)):
304 b = self.buttons[row][col]
307 im = self.base_images[mode][row*10+col]
310 self.image_mode = mode
313 def tap(self, rc, moved):
315 if (self.mode + self.shift) in keymap:
316 m = self.mode + self.shift
320 if moved == 2 and (self.mode + '-xtra') in keymap\
321 and keymap[self.mode + '-xtra'][row][col] != ' ':
322 m = self.mode + '-xtra'
324 m = self.mode + '-shift'
331 sym = keymap[m][row][col]
332 if self.shift == '-ctrl' and len(sym) == 1 and sym.isalpha():
333 sym = chr(ord(sym) & 31)
337 self.emit('key', sym)
338 if self.shift and not self.locked:
341 def press(self, widget, ev, arg):
342 self.dragx = int(ev.x_root)
343 self.dragy = int(ev.y_root)
346 self.xmin = self.dragx
347 self.xmax = self.dragx
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)
355 def release(self, widget, ev, click, arg):
362 if self.button_timeout:
363 gobject.source_remove(self.button_timeout)
364 self.button_timeout = None
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:
377 if self.button_timeout:
378 gobject.source_remove(self.button_timeout)
379 self.button_timeout = None
381 if self.xmin < dx and self.xmax > dx:
391 def motion(self, widget, ev):
392 if self.dragx == None:
397 if (not self.xmoved and abs(y-self.dragy) > 40) or self.moved:
399 self.emit('move', 0, 0)
400 self.emit('move', x-self.dragx, y-self.dragy)
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:
411 if self.button_timeout:
412 gobject.source_remove(self.button_timeout)
413 self.button_timeout = None
416 ev.window.get_pointer()
423 def do_buttons(self):
424 self.set_button_images()
425 self.button_timeout = None
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)
433 def nextshift(self, a, moved=False):
438 elif self.shift == '' and not a:
439 self.shift = '-shift'
442 elif not self.locked and not a:
445 if self.shift == '-ctrl':
451 self.shftbutton.child.set_text(lbl)
452 if self.shift and not self.locked:
453 self.set_buttons_soon()
455 self.set_button_images()
457 def nextmode(self, a, moved=False):
458 if self.mode == 'lower':
460 self.modebutton.child.set_text('Abc')
463 self.modebutton.child.set_text('Num')
466 self.nextshift(False)
467 self.nextshift(False)
468 self.set_button_images()
471 if __name__ == "__main__" :
473 w.connect("destroy", lambda w: gtk.main_quit())
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)
484 ti.connect('key', pkey)
488 ti.connect('hideme', hideme)