# Copyright 2008 by Kate Scheppke and Wade Brainerd.
# This file is part of Typing Turtle.
#
# Typing Turtle 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 3 of the License, or
# (at your option) any later version.
#
# Typing Turtle 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 Typing Turtle. If not, see .
#!/usr/bin/env python
# vi:sw=4 et
import cairo
import copy
import os, glob, re
from gi.repository import Gtk
from gi.repository import Pango
from gi.repository import PangoCairo
from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import GdkPixbuf
import StringIO
from port import json
import subprocess
from layouts.olpc import OLPC_LAYOUT
from layouts.olpcm import OLPCM_LAYOUT
# Tweaking variables.
HAND_YOFFSET = -15
# Unicode symbol for the paragraph key.
PARAGRAPH_CODE = u'\xb6'
# List of all key properties in the keyboard layout description.
#
# Keyboard Layouts use a property inheritance scheme similar to CSS (cascading style sheets):
# - Keys inherit properties from their groups, if not explicitly set.
# - Groups inherit properties from the layout.
# - The layout inherits properties from defaults values defined below.
#
# Therefore it is possible to set any property once in the Layout, and have
# it automatically filter down to all Keys, yet still be able to override it
# individually per key.
KEY_PROPS = [
# Name of the layout.
{ 'name': 'layout-name', 'default': '' },
# Source dimensions of the layout.
# This is the coordinate system that key sizes and coordinates are defined in.
# It can be any units, for example inches, millimeters, percentages, etc.
{ 'name': 'layout-width', 'default': 100 },
{ 'name': 'layout-height', 'default': 100 },
# Name of the group.
{ 'name': 'group-name', 'default': '' },
# Position of group in layout coordinates.
{ 'name': 'group-x', 'default': 0 },
{ 'name': 'group-y', 'default': 0 },
# Layout algorithm for the group.
# Possibilities are: 'horizontal', 'vertical', 'custom'.
{ 'name': 'group-layout', 'default': 'custom' },
# Position of key in layout coordinates. Used by 'custom' layout algorithm.
{ 'name': 'key-x', 'default': 0 },
{ 'name': 'key-y', 'default': 0 },
# Dimensions of a key in the layout coordinates.
{ 'name': 'key-width', 'default': 0 },
{ 'name': 'key-height', 'default': 0 },
# Gap between keys. Used by 'horizontal' and 'verical' layout algorithms.
{ 'name': 'key-gap', 'default': 0 },
# Keyboard scan code for this key.
{ 'name': 'key-scan', 'default': 0 },
# Text label to be displayed on keys which do not generate keys.
{ 'name': 'key-label', 'default': '' },
# Image filename showing a finger pressing this key.
{ 'name': 'key-hand-image', 'default': '' },
# Which finger should be used to press the key.
# Options are [LR][TIMRP], so LM would mean the left middle finger.
{ 'name': 'key-finger', 'default': '' },
# True if the key is currently pressed.
{ 'name': 'key-pressed', 'default': False },
]
def _is_olpcm_model():
"""Check via setxkbmap if the keyboard model is olpcm.
Keyboard model code is 'olpcm' for non-membrane, mechanical
keyboard, and 'olpc' for membrane keyboard.
"""
code = None
p = subprocess.Popen(["setxkbmap", "-query"], stdout=subprocess.PIPE)
out, err = p.communicate()
for line in out.splitlines():
if line.startswith('model:'):
code = line.split()[1]
return code == 'olpcm'
def get_layout():
if _is_olpcm_model():
return OLPCM_LAYOUT
else:
return OLPC_LAYOUT
class KeyboardImages:
def __init__(self, width, height):
self.width = width
self.height = height
self.images = {}
def load_images(self):
# This is for not changing all the numbers of olpcm layout,
# that was made based on the original olpc layout.
scale_width = self.width
if _is_olpcm_model():
scale_width = int(scale_width * 1.1625)
for filename in glob.iglob('images/OLPC*.svg'):
image = GdkPixbuf.Pixbuf.new_from_file_at_scale(
filename, scale_width, self.height, False)
name = os.path.basename(filename)
self.images[name] = image
class KeyboardData:
def __init__(self):
# This array contains the current keyboard layout.
self.keys = None
self.key_scan_map = None
self.letter_map = {}
# Access the current GTK keymap.
self.keymap = Gdk.Keymap.get_default()
def set_layout(self, layout):
self._build_key_list(layout)
self._layout_keys()
def _build_key_list(self, layout):
"""Builds a list of Keys objects from a layout description.
Also fills in derived and inherited key properties.
The layout description can be discarded afterwards."""
self.keys = []
self.key_scan_map = {}
group_count = 0
for g in layout['groups']:
key_count = 0
for k in g['keys']:
# Create and fill out a unique property list for this key.
key = k.copy()
# Assign key and group index.
key['key-index'] = key_count
key['group-index'] = group_count
# Inherit undefined properties from group, layout and
# defaults, in that order.
for p in KEY_PROPS:
pname = p['name']
if not key.has_key(pname):
if g.has_key(pname):
key[pname] = g[pname]
elif layout.has_key(pname):
key[pname] = layout[pname]
else:
key[pname] = p['default']
# Add to internal list.
self.keys.append(key)
key_count += 1
# Add to scan code mapping table.
if key['key-scan']:
self.key_scan_map[key['key-scan']] = key
group_count += 1
def _layout_keys(self):
"""Assigns positions and sizes to the individual keys."""
# Note- We know self.keys is sorted by group, and by index within the group.
# The layout algorithms depend on this order.
x, y = None, None
cur_group = None
for k in self.keys:
# Reset the working coordinates with each new group.
if k['group-index'] != cur_group:
cur_group = k['group-index']
x = k['group-x']
y = k['group-y']
# Apply the current layout.
if k['group-layout'] == 'horizontal':
k['key-x'] = x
k['key-y'] = y
x += k['key-width']
x += k['key-gap']
elif k['group-layout'] == 'vertical':
k['key-x'] = x
k['key-y'] = y
y += k['key-height']
y += k['key-gap']
else: # k['group-layout'] == 'custom' or unsupported
pass
def load_letter_map(self, filename):
self.letter_map = json.loads(open(filename, 'r').read())
def save_letter_map(self, filename):
text = json.dumps(self.letter_map, ensure_ascii=False, sort_keys=True, indent=4)
f = open(filename, 'w')
f.write(text)
f.close()
def format_key_sig(self, scan, state, group):
sig = 'scan%d' % scan
if state & Gdk.ModifierType.SHIFT_MASK:
sig += ' shift'
if state & Gdk.ModifierType.MOD5_MASK:
sig += ' altgr'
if group != 0:
sig += ' group%d' % group
return sig
KEY_SIG_RE = re.compile(r'scan(?P\d+) ?(?Pshift)? ?(?Paltgr)?( group)?(?P\d+)?')
def parse_key_sig(self, sig):
m = KeyboardData.KEY_SIG_RE.match(sig)
state = 0
if m.group('shift'):
state |= Gdk.ModifierType.SHIFT_MASK
if m.group('altgr'):
state |= Gdk.ModifierType.MOD5_MASK
scan = int(m.group('scan'))
group = 0
if m.group('group'):
group = int(m.group('group'))
return scan, state, group
def find_key_by_label(self, label):
for k in self.keys:
if k['key-label'] == label:
return k
return None
def get_key_state_group_for_letter(self, letter):
# Special processing for some keys.
if letter == '\n' or letter == PARAGRAPH_CODE:
return self.find_key_by_label('enter'), 0, 0
# Try the letter map, if loaded.
best_score = 3
best_result = None
for sig, l in self.letter_map.items():
if l == letter:
scan, state, group = self.parse_key_sig(sig)
# Choose the key with the fewest modifiers.
score = 0
if state & Gdk.ModifierType.SHIFT_MASK: score += 1
if state & Gdk.ModifierType.MOD5_MASK: score += 1
if score < best_score:
best_score = score
best_result = scan, state, group
if best_result is not None:
for k in self.keys:
if k['key-scan'] == best_result[0]:
return k, best_result[1], best_result[2]
# Try the GDK keymap.
keyval = Gdk.unicode_to_keyval(ord(letter))
valid, entries = self.keymap.get_entries_for_keyval(keyval)
for e in entries:
for k in self.keys:
if k['key-scan'] == e.keycode:
# TODO: Level -> state calculations are hardcoded to what the XO keyboard does.
# They were discovered through experimentation.
state = 0
if e.level & 1:
state |= Gdk.ModifierType.SHIFT_MASK
if e.level & 2:
state |= Gdk.ModifierType.MOD5_MASK
return k, state, e.group
# Fail!
return None, None, None
def get_letter_for_key_state_group(self, key, state, group):
sig = self.format_key_sig(key['key-scan'], state, group)
if self.letter_map.has_key(sig):
return self.letter_map[sig]
else:
success, keyval, effective_group, level, consumed_modifiers = \
self.keymap.translate_keyboard_state(
key['key-scan'], self.active_state, self.active_group)
if success:
return unichr(Gdk.keyval_to_unicode(keyval)).encode('utf-8')
return ''
class KeyboardWidget(KeyboardData, Gtk.DrawingArea):
"""A GTK widget which implements an interactive visual keyboard, with support
for custom data driven layouts."""
def __init__(self, image, root_window, poll_keys=False):
KeyboardData.__init__(self)
GObject.GObject.__init__(self)
self.image = image
self.root_window = root_window
# Match the image cache in dimensions.
self.set_size_request(image.width, image.height)
self.connect("draw", self._draw_cb)
#self.modify_font(Pango.FontDescription('Monospace 10'))
# Active language group and modifier state.
# See http://www.pygtk.org/docs/pygtk/class-gdkkeymap.html for more
# information about key group and state.
self.active_group = 0
self.active_state = 0
# still in development
#self.keymap.connect("keys-changed", self._keys_changed_cb)
self.hilite_letter = None
self.draw_hands = False
self.modify_bg(Gtk.StateType.NORMAL, Gdk.Color.parse('#d0d0d0')[1])
# Connect keyboard grabbing and releasing callbacks.
if poll_keys:
self.connect('realize', self._realize_cb)
self.connect('unrealize', self._unrealize_cb)
def _realize_cb(self, widget):
# Setup keyboard event snooping in the root window.
self.root_window.add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK)
self.key_press_cb_id = self.root_window.connect('key-press-event', self.key_press_release_cb)
self.key_release_cb_id = self.root_window.connect('key-release-event', self.key_press_release_cb)
def _unrealize_cb(self, widget):
self.root_window.disconnect(self.key_press_cb_id)
self.root_window.disconnect(self.key_release_cb_id)
def set_layout(self, layout):
"""Sets the keyboard's layout from a layout description."""
KeyboardData.set_layout(self, layout)
# Scale the keyboard to match the images.
width_scale = float(self.image.width) / self.keys[0]['layout-width']
height_scale = float(self.image.height) / self.keys[0]['layout-height']
for k in self.keys:
k['key-x'] = int(k['key-x'] * width_scale)
k['key-y'] = int(k['key-y'] * height_scale)
k['key-width'] = int(k['key-width'] * width_scale)
k['key-height'] = int(k['key-height'] * height_scale)
def _draw_key(self, k, cr):
bounds = self.get_allocation()
# HACK: this is a hack used when the widget is not shown yet,
# in that case bounds will be gtk.gdk.Rectangle(-1, -1, 1, 1)
# and the key will be outside the canvas. This is used only
# for the first key that appears below the instructions
if bounds.x == -1:
screen_x = screen_y = 0
else:
screen_x = int(bounds.width - self.image.width) / 2
screen_y = int(bounds.height - self.image.height) / 2
x1 = k['key-x'] + screen_x
y1 = k['key-y'] + screen_y
x2 = x1 + k['key-width']
y2 = y1 + k['key-height']
corner = 5
points = [
(x1 + corner, y1),
(x2 - corner, y1),
(x2, y1 + corner),
(x2, y2 - corner),
(x2 - corner, y2),
(x1 + corner, y2),
(x1, y2 - corner),
(x1, y1 + corner)
]
cr.new_path()
cr.set_source_rgb(0.396, 0.698, 0.392)
cr.set_line_width(2)
for point in points:
cr.line_to(*point)
cr.close_path()
cr.fill_preserve()
cr.stroke()
text = ''
if k['key-label']:
text = k['key-label']
else:
text = self.get_letter_for_key_state_group(
k, self.active_state, self.active_group)
cr.set_source_rgb(0, 0, 0)
pango_layout = PangoCairo.create_layout(cr)
fd = Pango.FontDescription('Monospace')
fd.set_size(10 * Pango.SCALE)
pango_layout.set_font_description(fd)
pango_layout.set_text(text, len(text))
cr.move_to(x1 + 8, y2 - 23)
PangoCairo.update_layout(cr, pango_layout)
PangoCairo.show_layout(cr, pango_layout)
def _expose_hands(self, cr):
lhand_image = self.image.images['OLPC_Lhand_HOMEROW.svg']
rhand_image = self.image.images['OLPC_Rhand_HOMEROW.svg']
if self.hilite_letter:
key, state, group = self.get_key_state_group_for_letter(self.hilite_letter)
if key:
handle = self.image.images[key['key-hand-image']]
finger = key['key-finger']
# Assign the key image to the correct side.
if finger and handle:
if finger[0] == 'L':
lhand_image = handle
else:
rhand_image = handle
# Put the other hand on the SHIFT key if needed.
if state & Gdk.ModifierType.SHIFT_MASK:
if finger[0] == 'L':
rhand_image = self.image.images['OLPC_Rhand_SHIFT.svg']
else:
lhand_image = self.image.images['OLPC_Lhand_SHIFT.svg']
# TODO: Do something about ALTGR.
bounds = self.get_allocation()
cr.save()
Gdk.cairo_set_source_pixbuf(cr, lhand_image, 0, 0)
cr.rectangle(0, 0, lhand_image.get_width(),
lhand_image.get_height())
cr.paint()
cr.restore()
Gdk.cairo_set_source_pixbuf(cr, rhand_image, 0, 0)
cr.rectangle(0, 0, rhand_image.get_width(),
rhand_image.get_height())
cr.paint()
def _draw_cb(self, area, cr):
# Draw the keys.
for k in self.keys:
self._draw_key(k, cr)
# Draw overlay images.
if self.draw_hands:
self._expose_hands(cr)
return True
def key_press_release_cb(self, widget, event):
key = self.key_scan_map.get(event.hardware_keycode)
if key:
key['key-pressed'] = event.type == Gdk.EventType.KEY_PRESS
# Hack to get the current modifier state - which will not be represented by the event.
# state = Gdk.device_get_core_pointer().get_state(self.get_window())[1]
# This is a NEW HACK for the Gtk3 version of Typing Turtle. I
# didn't find the way to translate the old hack into a new one
state = event.state
if event.hardware_keycode in (50, 62):
if event.type == Gdk.EventType.KEY_PRESS:
state |= Gdk.ModifierType.SHIFT_MASK
else:
state ^= Gdk.ModifierType.SHIFT_MASK
if event.hardware_keycode == 92:
if event.type == Gdk.EventType.KEY_PRESS:
state |= Gdk.ModifierType.MOD5_MASK
else:
state ^= Gdk.ModifierType.MOD5_MASK
if self.active_group != event.group or self.active_state != state:
self.active_group = event.group
self.active_state = state
self.queue_draw()
if event.string:
sig = self.format_key_sig(event.hardware_keycode, state, event.group)
if not self.letter_map.has_key(sig):
self.letter_map[sig] = event.string
self.queue_draw()
return False
def clear_hilite(self):
self.hilite_letter = None
self.queue_draw()
def set_hilite_letter(self, letter):
self.hilite_letter = letter
self.queue_draw()
def set_draw_hands(self, enable):
self.draw_hands = enable
self.queue_draw()
def get_key_pixbuf(self, key, state=0, group=0, scale=1):
w = int(key['key-width'] * scale)
h = int(key['key-height'] * scale)
old_state, old_group = self.active_state, self.active_group
self.active_state, self.active_group = state, group
surface = cairo.ImageSurface(cairo.FORMAT_RGB24, w, h)
cr = cairo.Context(surface)
cr.set_source_rgb(1, 1, 1)
cr.rectangle(0, 0, w, h)
cr.fill()
# Duplicate the Key to be able to change its position values
key = copy.deepcopy(key)
key['key-x'] = 0
key['key-y'] = 0
self._draw_key(key, cr)
# Convert cairo.Surface to Pixbuf
pixbuf_data = StringIO.StringIO()
surface.write_to_png(pixbuf_data)
pxb_loader = GdkPixbuf.PixbufLoader.new_with_type('png')
pxb_loader.write(pixbuf_data.getvalue())
temp_pix = pxb_loader.get_pixbuf()
pxb_loader.close()
self.active_state, self.active_group = old_state, old_group
return temp_pix