Vector Keyboard Language Reference
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.
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.