#!/usr/bin/python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Pango

ROWS = [
    ['1','2','3','4','5','6','7','8','9','0','-','='],
    ['q','w','e','r','t','y','u','i','o','p','[',']'],
    ['a','s','d','f','g','h','j','k','l',';',"'"],
    ['z','x','c','v','b','n','m',',','.','/'],
]
SHIFT_MAP = {
    '1':'!','2':'@','3':'#','4':'$','5':'%','6':'^','7':'&','8':'*','9':'(','0':')',
    '-':'_','=':'+','[':'{',']':'}',';':':','\'':'"',',':'<','.':'>','/':'?',
}

class App(Gtk.Window):
    def __init__(self):
        super().__init__(title='On-Screen Keyboard')
        self.set_default_size(760, 380)
        self.caps = False
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        vbox.set_margin_top(8); vbox.set_margin_bottom(8)
        vbox.set_margin_start(8); vbox.set_margin_end(8)
        self.add(vbox)
        sw = Gtk.ScrolledWindow(); sw.set_size_request(-1, 80)
        self.output = Gtk.TextView()
        self.output.set_wrap_mode(Gtk.WrapMode.WORD)
        self.output.override_font(Pango.FontDescription('16'))
        sw.add(self.output)
        vbox.pack_start(sw, False, False, 0)
        kb_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
        self.key_btns = {}
        for row_keys in ROWS:
            row = Gtk.Box(spacing=3)
            row.set_halign(Gtk.Align.CENTER)
            for k in row_keys:
                btn = Gtk.Button(label=k.upper() if self.caps else k)
                btn.set_size_request(48, 40)
                btn.connect('clicked', self.key_press, k)
                row.pack_start(btn, False, False, 0)
                self.key_btns[k] = btn
            kb_box.pack_start(row, False, False, 0)
        # special keys row
        spec_row = Gtk.Box(spacing=3)
        spec_row.set_halign(Gtk.Align.CENTER)
        self.caps_btn = Gtk.ToggleButton(label='Caps Lock')
        self.caps_btn.set_size_request(90, 40)
        self.caps_btn.connect('toggled', self.toggle_caps)
        spec_row.pack_start(self.caps_btn, False, False, 0)
        space_btn = Gtk.Button(label='Space')
        space_btn.set_size_request(200, 40)
        space_btn.connect('clicked', lambda *_: self.insert_char(' '))
        spec_row.pack_start(space_btn, False, False, 0)
        enter_btn = Gtk.Button(label='Enter ↵')
        enter_btn.set_size_request(80, 40)
        enter_btn.connect('clicked', lambda *_: self.insert_char('\n'))
        spec_row.pack_start(enter_btn, False, False, 0)
        back_btn = Gtk.Button(label='⌫ Back')
        back_btn.set_size_request(80, 40)
        back_btn.connect('clicked', self.backspace)
        spec_row.pack_start(back_btn, False, False, 0)
        clear_btn = Gtk.Button(label='Clear')
        clear_btn.set_size_request(60, 40)
        clear_btn.connect('clicked', lambda *_: self.output.get_buffer().set_text(''))
        spec_row.pack_start(clear_btn, False, False, 0)
        kb_box.pack_start(spec_row, False, False, 0)
        vbox.pack_start(kb_box, True, True, 0)
    def insert_char(self, ch):
        buf = self.output.get_buffer()
        buf.insert_at_cursor(ch)
    def key_press(self, _, k):
        if self.caps:
            ch = SHIFT_MAP.get(k, k.upper())
        else:
            ch = k
        self.insert_char(ch)
    def backspace(self, *_):
        buf = self.output.get_buffer()
        cursor = buf.get_iter_at_mark(buf.get_insert())
        if cursor.backward_char():
            buf.delete(cursor, buf.get_iter_at_mark(buf.get_insert()))
    def toggle_caps(self, btn):
        self.caps = btn.get_active()
        for k, b in self.key_btns.items():
            if self.caps:
                b.set_label(SHIFT_MAP.get(k, k.upper()))
            else:
                b.set_label(k)

win = App()
win.connect('destroy', Gtk.main_quit)
win.show_all()
Gtk.main()
