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_by_name( 'bauiv1.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_by_name( 'bauiv1.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_by_name( 'bauiv1.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)) )
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum