Source code for bauiv1.onscreenkeyboard

# Released under the MIT License. See LICENSE for details.
#
"""Provides the built-in on screen keyboard UI."""

from __future__ import annotations

import logging
from typing import cast

from typing import TYPE_CHECKING

import babase

import _bauiv1
from bauiv1._keyboard import Keyboard
from bauiv1._uitypes import Window

if TYPE_CHECKING:
    from babase import StringEditAdapter

    import bauiv1 as bui


[docs] class OnScreenKeyboardWindow(Window): """Simple built-in on-screen keyboard.""" def __init__(self, adapter: StringEditAdapter): self._adapter = adapter self._width = 700 self._height = 400 assert babase.app.classic is not None uiscale = babase.app.ui_v1.uiscale top_extra = 20 if uiscale is babase.UIScale.SMALL else 0 super().__init__( root_widget=_bauiv1.containerwidget( parent=_bauiv1.get_special_widget('overlay_stack'), size=(self._width, self._height + top_extra), transition='in_scale', scale_origin_stack_offset=adapter.screen_space_center, scale=( 2.0 if uiscale is babase.UIScale.SMALL else 1.5 if uiscale is babase.UIScale.MEDIUM else 1.0 ), stack_offset=( (0, 0) if uiscale is babase.UIScale.SMALL else (0, 0) if uiscale is babase.UIScale.MEDIUM else (0, 0) ), ) ) self._cancel_button = _bauiv1.buttonwidget( parent=self._root_widget, scale=0.5, position=(30, self._height - 55), size=(60, 60), label='', enable_sound=False, on_activate_call=self._cancel, autoselect=True, color=(0.55, 0.5, 0.6), icon=_bauiv1.gettexture('crossOut'), iconscale=1.2, ) self._done_button = _bauiv1.buttonwidget( parent=self._root_widget, position=(self._width - 200, 44), size=(140, 60), autoselect=True, label=babase.Lstr(resource='doneText'), on_activate_call=self._done, ) _bauiv1.containerwidget( edit=self._root_widget, on_cancel_call=self._cancel, start_button=self._done_button, ) _bauiv1.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 41), size=(0, 0), scale=0.95, text=adapter.description, maxwidth=self._width - 140, color=babase.app.ui_v1.title_color, h_align='center', v_align='center', ) self._text_field = _bauiv1.textwidget( parent=self._root_widget, position=(70, self._height - 116), max_chars=adapter.max_length, text=adapter.initial_text, on_return_press_call=self._done, autoselect=True, size=(self._width - 140, 55), v_align='center', editable=True, maxwidth=self._width - 175, force_internal_editing=True, always_show_carat=True, ) self._key_color_lit = (1.4, 1.2, 1.4) self._key_color = (0.69, 0.6, 0.74) self._key_color_dark = (0.55, 0.55, 0.71) self._shift_button: bui.Widget | None = None self._backspace_button: bui.Widget | None = None self._space_button: bui.Widget | None = None self._double_press_shift = False self._num_mode_button: bui.Widget | None = None self._emoji_button: bui.Widget | None = None self._char_keys: list[bui.Widget] = [] self._keyboard_index = 0 self._last_space_press = 0.0 self._double_space_interval = 0.3 self._keyboard: bui.Keyboard self._chars: list[str] self._modes: list[str] self._mode: str self._mode_index: int self._load_keyboard() def _load_keyboard(self) -> None: # pylint: disable=too-many-locals self._keyboard = self._get_keyboard() # We want to get just chars without column data, etc. self._chars = [j for i in self._keyboard.chars for j in i] self._modes = ['normal'] + list(self._keyboard.pages) self._mode_index = 0 self._mode = self._modes[self._mode_index] v = self._height - 180.0 key_width = 46 * 10 / len(self._keyboard.chars[0]) key_height = 46 * 3 / len(self._keyboard.chars) key_textcolor = (1, 1, 1) row_starts = (69.0, 95.0, 151.0) key_color = self._key_color key_color_dark = self._key_color_dark self._click_sound = _bauiv1.getsound('click01') # kill prev char keys for key in self._char_keys: key.delete() self._char_keys = [] # dummy data just used for row/column lengths... we don't actually # set things until refresh chars: list[tuple[str, ...]] = self._keyboard.chars for row_num, row in enumerate(chars): h = row_starts[row_num] # shift key before row 3 if row_num == 2 and self._shift_button is None: self._shift_button = _bauiv1.buttonwidget( parent=self._root_widget, position=(h - key_width * 2.0, v), size=(key_width * 1.7, key_height), autoselect=True, textcolor=key_textcolor, color=key_color_dark, label=babase.charstr(babase.SpecialChar.SHIFT), enable_sound=False, extra_touch_border_scale=0.3, button_type='square', ) for _ in row: btn = _bauiv1.buttonwidget( parent=self._root_widget, position=(h, v), size=(key_width, key_height), autoselect=True, enable_sound=False, textcolor=key_textcolor, color=key_color, label='', button_type='square', extra_touch_border_scale=0.1, ) self._char_keys.append(btn) h += key_width + 10 # Add delete key at end of third row. if row_num == 2: if self._backspace_button is not None: self._backspace_button.delete() self._backspace_button = _bauiv1.buttonwidget( parent=self._root_widget, position=(h + 4, v), size=(key_width * 1.8, key_height), autoselect=True, enable_sound=False, repeat=True, textcolor=key_textcolor, color=key_color_dark, label=babase.charstr(babase.SpecialChar.DELETE), button_type='square', on_activate_call=self._del, ) v -= key_height + 9 # Do space bar and stuff. if row_num == 2: if self._num_mode_button is None: self._num_mode_button = _bauiv1.buttonwidget( parent=self._root_widget, position=(112, v - 8), size=(key_width * 2, key_height + 5), enable_sound=False, button_type='square', extra_touch_border_scale=0.3, autoselect=True, textcolor=key_textcolor, color=key_color_dark, label='', ) if self._emoji_button is None: self._emoji_button = _bauiv1.buttonwidget( parent=self._root_widget, position=(56, v - 8), size=(key_width, key_height + 5), autoselect=True, enable_sound=False, textcolor=key_textcolor, color=key_color_dark, label=babase.charstr(babase.SpecialChar.LOGO_FLAT), extra_touch_border_scale=0.3, button_type='square', ) btn1 = self._num_mode_button if self._space_button is None: self._space_button = _bauiv1.buttonwidget( parent=self._root_widget, position=(210, v - 12), size=(key_width * 6.1, key_height + 15), extra_touch_border_scale=0.3, enable_sound=False, autoselect=True, textcolor=key_textcolor, color=key_color_dark, label=babase.Lstr(resource='spaceKeyText'), on_activate_call=babase.Call(self._type_char, ' '), ) # Show change instructions only if we have more than one # keyboard option. keyboards = ( babase.app.meta.scanresults.exports_of_class(Keyboard) if babase.app.meta.scanresults is not None else [] ) if len(keyboards) > 1: _bauiv1.textwidget( parent=self._root_widget, h_align='center', position=(210, v - 70), size=(key_width * 6.1, key_height + 15), text=babase.Lstr( resource='keyboardChangeInstructionsText' ), scale=0.75, ) btn2 = self._space_button btn3 = self._emoji_button _bauiv1.widget(edit=btn1, right_widget=btn2, left_widget=btn3) _bauiv1.widget( edit=btn2, left_widget=btn1, right_widget=self._done_button ) _bauiv1.widget(edit=btn3, left_widget=btn1) _bauiv1.widget(edit=self._done_button, left_widget=btn2) _bauiv1.containerwidget( edit=self._root_widget, selected_child=self._char_keys[14] ) self._refresh() def _get_keyboard(self) -> bui.Keyboard: assert babase.app.meta.scanresults is not None classname = babase.app.meta.scanresults.exports_of_class(Keyboard)[ self._keyboard_index ] kbclass = babase.getclass(classname, Keyboard) return kbclass() def _refresh(self) -> None: chars: list[str] | None = None if self._mode in ['normal', 'caps']: chars = list(self._chars) if self._mode == 'caps': chars = [c.upper() for c in chars] _bauiv1.buttonwidget( edit=self._shift_button, color=( self._key_color_lit if self._mode == 'caps' else self._key_color_dark ), label=babase.charstr(babase.SpecialChar.SHIFT), on_activate_call=self._shift, ) _bauiv1.buttonwidget( edit=self._num_mode_button, label='123#&*', on_activate_call=self._num_mode, ) _bauiv1.buttonwidget( edit=self._emoji_button, color=self._key_color_dark, label=babase.charstr(babase.SpecialChar.LOGO_FLAT), on_activate_call=self._next_mode, ) else: if self._mode == 'num': chars = list(self._keyboard.nums) else: chars = list(self._keyboard.pages[self._mode]) _bauiv1.buttonwidget( edit=self._shift_button, color=self._key_color_dark, label='', on_activate_call=self._null_press, ) _bauiv1.buttonwidget( edit=self._num_mode_button, label='abc', on_activate_call=self._abc_mode, ) _bauiv1.buttonwidget( edit=self._emoji_button, color=self._key_color_dark, label=babase.charstr(babase.SpecialChar.LOGO_FLAT), on_activate_call=self._next_mode, ) for i, btn in enumerate(self._char_keys): assert chars is not None have_char = True if i >= len(chars): # No such char. have_char = False pagename = self._mode if babase.do_once(): errstr = ( f'Size of page "{pagename}" of keyboard' f' "{self._keyboard.name}" is incorrect:' f' {len(chars)} != {len(self._chars)}' f' (size of default "normal" page)' ) logging.error(errstr) _bauiv1.buttonwidget( edit=btn, label=chars[i] if have_char else ' ', on_activate_call=babase.Call( self._type_char, chars[i] if have_char else ' ' ), ) def _null_press(self) -> None: self._click_sound.play() def _abc_mode(self) -> None: self._click_sound.play() self._mode = 'normal' self._refresh() def _num_mode(self) -> None: self._click_sound.play() self._mode = 'num' self._refresh() def _next_mode(self) -> None: self._click_sound.play() self._mode_index = (self._mode_index + 1) % len(self._modes) self._mode = self._modes[self._mode_index] self._refresh() def _next_keyboard(self) -> None: assert babase.app.meta.scanresults is not None kbexports = babase.app.meta.scanresults.exports_of_class(Keyboard) self._keyboard_index = (self._keyboard_index + 1) % len(kbexports) self._load_keyboard() if len(kbexports) < 2: _bauiv1.getsound('error').play() babase.screenmessage( babase.Lstr(resource='keyboardNoOthersAvailableText'), color=(1, 0, 0), ) else: babase.screenmessage( babase.Lstr( resource='keyboardSwitchText', subs=[('${NAME}', self._keyboard.name)], ), color=(0, 1, 0), ) def _shift(self) -> None: self._click_sound.play() if self._mode == 'normal': self._mode = 'caps' self._double_press_shift = False elif self._mode == 'caps': if not self._double_press_shift: self._double_press_shift = True else: self._mode = 'normal' self._refresh() def _del(self) -> None: self._click_sound.play() txt = cast(str, _bauiv1.textwidget(query=self._text_field)) # pylint: disable=unsubscriptable-object txt = txt[:-1] _bauiv1.textwidget(edit=self._text_field, text=txt) def _type_char(self, char: str) -> None: self._click_sound.play() if char.isspace(): if ( babase.apptime() - self._last_space_press < self._double_space_interval ): self._last_space_press = 0 self._next_keyboard() self._del() # We typed unneeded space around 1s ago. return self._last_space_press = babase.apptime() # Operate in unicode so we don't do anything funky like chop utf-8 # chars in half. txt = cast(str, _bauiv1.textwidget(query=self._text_field)) txt += char _bauiv1.textwidget(edit=self._text_field, text=txt) # If we were caps, go back only if not Shift is pressed twice. if self._mode == 'caps' and not self._double_press_shift: self._mode = 'normal' self._refresh() def _cancel(self) -> None: self._adapter.cancel() _bauiv1.getsound('swish').play() _bauiv1.containerwidget(edit=self._root_widget, transition='out_scale') def _done(self) -> None: _bauiv1.containerwidget(edit=self._root_widget, transition='out_scale') self._adapter.apply( cast(str, _bauiv1.textwidget(query=self._text_field)) )