Vector Keyboard Language Reference

keyboard banner

If you need a language reference keyboard overlay but don’t won’t to buy another keyboard or decals you can use this as a reference. Currently I include Russian, French, Spanish, and German with English as the base language. I might make other keyboards; this is for a 102 key laptop layout. I may add other languages also but it’s easy to add your own. Everything is layered for ease of use. It can be downloaded here and I recommend using Inkscape if you plan to edit. Code shows how to change apps size, keep on top, and overlay.

keyboard preview

Made it into a Gtk 3 application. Download

Here’s the code if you prefer not downloading. It’s important that the keyboards directory is in the same directory as the script for the first run. It will copy the images and create a config in ~/.foreignkeys

The images are included in the Gtk 3 download.

#!/usr/bin/env python
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, Gio, GdkPixbuf
import os
import shutil
import sys
import getpass
import json

ABOUT_MSG = """ ForeignKeys: Language Keyboard Reference

 Created by C. Nichols, Feb. 2022
 Visit me: https://darkartistry.com
 
 You can edit the keyboards in
  ~/.foreignkeys/keyboards directly in Inkscape. """

SIZES = {
    'large': (1920,580),
    'medium': (1440,440),
    'small': (1024,305)
}

def read_conf(conf_path):
    with open(conf_path, 'r') as fh:
        conf = json.loads(fh.read())
    return conf

def write_conf(conf_path, conf):
    conf = json.dumps(conf)
    with open(conf_path, 'w') as fh:
        fh.write(conf)

def set_get_config(app_path):
    """Set default config."""
    config_path = os.path.join(app_path, 'foreignkeys.conf')
    if not os.path.exists(config_path):
        conf = {'AlwaysOnTop': True, 'UserPath': app_path, 'language': 'russian', 'size': 'medium'}
        write_conf(config_path, conf)
    return config_path

def init(app_name='foreignkeys', user=None):
    """Create home directory for config and images."""
    if not user:
        print('Unable to determine user.')
        sys.exit()

    app_path = os.path.join('/home', user, '.%s' % app_name)
    img_path = os.path.join(app_path, 'keyboards')

    try:
        cwd = os.getcwd()
        kbp = os.path.join(cwd, 'keyboards')
        if not os.path.exists(app_path):
            os.mkdir(app_path)

        if os.path.exists(kbp):
            if not os.path.exists(img_path):
                shutil.copytree(kbp, img_path)
        else:
            print('Missing keyboards directory and its resources.')
    except:
        print('Unable to create path at %s' % app_path)
        sys.exit()
    return app_path

def get_overlays(tp):
    """Get images for dropdown."""
    kb_overlays = {}
    for f in os.listdir(tp):
        filename = f.split('.')[0]
        fpath = os.path.join(tp, f)
        kb_overlays[filename] = fpath
    return kb_overlays

def get_screen_size():
    """Get the size of the physical screen."""
    rects = []
    display = Gdk.Display.get_default()
    mcount = display.get_n_monitors()
    for i in range(mcount):
        rects.append(display.get_monitor(i).get_geometry())
    rx = min(rec.x for rec in rects)
    ry = min(rec.y for rec in rects)
    wx = max(rec.x + rec.width for rec in rects)
    hy = max(rec.y + rec.height for rec in rects)
    width = wx - rx
    height = hy - ry
    return (width, height)

class MainWindow(Gtk.Window):

    def __init__(self, base_path, screen_sz):

        Gtk.Window.__init__(self, title="ForeignKeys")
        self.set_position(Gtk.WindowPosition.CENTER)

        # ********* Config *********
        self.conf_path = set_get_config(base_path)
        self.config = read_conf(self.conf_path)

        self.checked_on_top = self.config.get('AlwaysOnTop')
        self.selected_ov = self.config.get('language')
        self.wind_size = self.config.get('size')
        self.width = SIZES[self.wind_size.lower()][0]
        self.height = SIZES[self.wind_size.lower()][-1]
        self.app_path = self.config.get('UserPath')
        self.keyboard_ovl = os.path.join(self.app_path, 'keyboards')
        self.kb_overlays = get_overlays(self.keyboard_ovl)
        # ********* Config *********

        self.set_keep_above(True)
        self.set_resizable(False)
        self.set_default_icon_name("input-keyboard")
        self.set_default_size(self.width, self.height)

        # ********* Create HeaderBar **********
        self.header = Gtk.HeaderBar()
        self.header.set_show_close_button(True)
        self.header.props.title = "ForeignKeys"
        self.set_titlebar(self.header)

        self.popover = Gtk.Popover()
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox.set_size_request(150, 200)

        self.chk_top = Gtk.CheckButton(label="Always on top")
        self.chk_top.set_active(self.checked_on_top)
        self.chk_top.connect("toggled", self.on_checked)
        vbox.pack_start(self.chk_top, True, False, 5)

        self.sel_language = Gtk.ComboBoxText()
        for ov in sorted(self.kb_overlays.keys()):
            self.sel_language.append(ov.lower(), ov.title())

        self.sel_language.set_active_id(self.selected_ov)
        self.sel_language.connect("changed", self.on_language_changed)
        vbox.pack_start(self.sel_language, True, False, 5)

        self.sel_size = Gtk.ComboBoxText()
        self.sel_size.append('large', 'Large')
        self.sel_size.append('medium', 'Medium')
        self.sel_size.append('small', 'Small')
        self.sel_size.set_active_id(self.wind_size.lower())
        self.sel_size.connect("changed", self.on_size_changed)
        vbox.pack_start(self.sel_size, True, False, 5)

        btn_about = Gtk.Button(label="About")
        btn_about.connect("clicked", self.on_about_click)
        vbox.pack_start(btn_about, True, False, 5)

        separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
        vbox.pack_start(separator, True, False, 5)

        btn_quit = Gtk.Button(label="Quit")
        btn_quit.connect("clicked", self.on_quit_click)
        vbox.pack_start(btn_quit, True, False, 5)

        vbox.show_all()

        self.popover.add(vbox)
        self.popover.set_position(Gtk.PositionType.BOTTOM)

        mnu_button = Gtk.MenuButton(popover=self.popover)
        mnu_icon = Gio.ThemedIcon(name="open-menu-symbolic")
        mnu_image = Gtk.Image.new_from_gicon(mnu_icon, Gtk.IconSize.BUTTON)
        mnu_button.add(mnu_image)
        self.header.pack_end(mnu_button)

        # Create Box
        mnu_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        Gtk.StyleContext.add_class(mnu_box.get_style_context(), "linked")

        self.header.pack_start(mnu_box)
        # ********* Create HeaderBar **********

        self.overlay = Gtk.Overlay()
        self.add(self.overlay)
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(self.kb_overlays[self.selected_ov], self.width, self.height)
        self.background = Gtk.Image.new_from_pixbuf(pixbuf)
        self.overlay.add(self.background)

    def on_checked(self, widget):
        state = self.chk_top.get_active()
        self.set_keep_above(state)
        self.show_all()
        self.config['AlwaysOnTop'] = state
        write_conf(self.conf_path, self.config)

    def on_size_changed(self, widget):
        switch_size = self.sel_size.get_active_text().lower()
        self.width  = SIZES[switch_size.lower()][0]
        self.height = SIZES[switch_size.lower()][-1]

        self.set_size_request(self.width, self.height)
        self.overlay.remove(self.background)
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(self.kb_overlays[self.selected_ov], self.width, self.height)
        self.background = Gtk.Image.new_from_pixbuf(pixbuf)
        self.overlay.add(self.background)
        self.overlay.show_all()
        self.show_all()

        self.config['size'] = switch_size
        write_conf(self.conf_path, self.config)

    def on_language_changed(self, widget):
        self.overlay.remove(self.background)
        switch_lang = self.sel_language.get_active_text().lower()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(self.kb_overlays[switch_lang.lower()], self.width, self.height)
        self.background = Gtk.Image.new_from_pixbuf(pixbuf)
        self.overlay.add(self.background)
        self.overlay.show_all()
        self.show_all()

        self.config['language'] = switch_lang
        write_conf(self.conf_path, self.config)

    def on_about_click(self, widget):
        dialog = About(self)
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            dialog.destroy()

    def on_quit_click(self, widget):
        Gtk.main_quit()

class About(Gtk.Dialog):
    def __init__(self, parent):
        super().__init__(title="About", transient_for=parent, flags=0)
        self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
        self.set_default_size(190, 200)
        label = Gtk.Label(label=ABOUT_MSG)
        box = self.get_content_area()
        box.add(label)
        self.show_all()

if __name__ == '__main__':

    screen_size = get_screen_size()
    user_path = init(user=getpass.getuser())

    window = MainWindow(base_path=user_path, screen_sz=screen_size)
    window.connect('delete-event', Gtk.main_quit)
    window.show_all()

    Gtk.main()

See it in action. You might want to change the video to high quality in the player or watch at YouTube as it sucks by default.