bastd.ui.onscreenkeyboard

Provides the built-in on screen keyboard UI.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides the built-in on screen keyboard UI."""
  4
  5from __future__ import annotations
  6
  7from typing import TYPE_CHECKING, cast
  8
  9import _ba
 10import ba
 11from ba import charstr
 12from ba import SpecialChar as SpCh
 13
 14if TYPE_CHECKING:
 15    pass
 16
 17
 18class OnScreenKeyboardWindow(ba.Window):
 19    """Simple built-in on-screen keyboard."""
 20
 21    def __init__(self, textwidget: ba.Widget, label: str, max_chars: int):
 22        self._target_text = textwidget
 23        self._width = 700
 24        self._height = 400
 25        uiscale = ba.app.ui.uiscale
 26        top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
 27        super().__init__(root_widget=ba.containerwidget(
 28            parent=_ba.get_special_widget('overlay_stack'),
 29            size=(self._width, self._height + top_extra),
 30            transition='in_scale',
 31            scale_origin_stack_offset=self._target_text.
 32            get_screen_space_center(),
 33            scale=(2.0 if uiscale is ba.UIScale.SMALL else
 34                   1.5 if uiscale is ba.UIScale.MEDIUM else 1.0),
 35            stack_offset=(0, 0) if uiscale is ba.UIScale.SMALL else (
 36                0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0)))
 37        self._done_button = ba.buttonwidget(parent=self._root_widget,
 38                                            position=(self._width - 200, 44),
 39                                            size=(140, 60),
 40                                            autoselect=True,
 41                                            label=ba.Lstr(resource='doneText'),
 42                                            on_activate_call=self._done)
 43        ba.containerwidget(edit=self._root_widget,
 44                           on_cancel_call=self._cancel,
 45                           start_button=self._done_button)
 46
 47        ba.textwidget(parent=self._root_widget,
 48                      position=(self._width * 0.5, self._height - 41),
 49                      size=(0, 0),
 50                      scale=0.95,
 51                      text=label,
 52                      maxwidth=self._width - 140,
 53                      color=ba.app.ui.title_color,
 54                      h_align='center',
 55                      v_align='center')
 56
 57        self._text_field = ba.textwidget(
 58            parent=self._root_widget,
 59            position=(70, self._height - 116),
 60            max_chars=max_chars,
 61            text=cast(str, ba.textwidget(query=self._target_text)),
 62            on_return_press_call=self._done,
 63            autoselect=True,
 64            size=(self._width - 140, 55),
 65            v_align='center',
 66            editable=True,
 67            maxwidth=self._width - 175,
 68            force_internal_editing=True,
 69            always_show_carat=True)
 70
 71        self._key_color_lit = (1.4, 1.2, 1.4)
 72        self._key_color = (0.69, 0.6, 0.74)
 73        self._key_color_dark = (0.55, 0.55, 0.71)
 74
 75        self._shift_button: ba.Widget | None = None
 76        self._backspace_button: ba.Widget | None = None
 77        self._space_button: ba.Widget | None = None
 78        self._double_press_shift = False
 79        self._num_mode_button: ba.Widget | None = None
 80        self._emoji_button: ba.Widget | None = None
 81        self._char_keys: list[ba.Widget] = []
 82        self._keyboard_index = 0
 83        self._last_space_press = 0.0
 84        self._double_space_interval = 0.3
 85
 86        self._keyboard: ba.Keyboard
 87        self._chars: list[str]
 88        self._modes: list[str]
 89        self._mode: str
 90        self._mode_index: int
 91        self._load_keyboard()
 92
 93    def _load_keyboard(self) -> None:
 94        # pylint: disable=too-many-locals
 95        self._keyboard = self._get_keyboard()
 96        # We want to get just chars without column data, etc.
 97        self._chars = [j for i in self._keyboard.chars for j in i]
 98        self._modes = ['normal'] + list(self._keyboard.pages)
 99        self._mode_index = 0
100        self._mode = self._modes[self._mode_index]
101
102        v = self._height - 180.0
103        key_width = 46 * 10 / len(self._keyboard.chars[0])
104        key_height = 46 * 3 / len(self._keyboard.chars)
105        key_textcolor = (1, 1, 1)
106        row_starts = (69.0, 95.0, 151.0)
107        key_color = self._key_color
108        key_color_dark = self._key_color_dark
109
110        self._click_sound = ba.getsound('click01')
111
112        # kill prev char keys
113        for key in self._char_keys:
114            key.delete()
115        self._char_keys = []
116
117        # dummy data just used for row/column lengths... we don't actually
118        # set things until refresh
119        chars: list[tuple[str, ...]] = self._keyboard.chars
120
121        for row_num, row in enumerate(chars):
122            h = row_starts[row_num]
123            # shift key before row 3
124            if row_num == 2 and self._shift_button is None:
125                self._shift_button = ba.buttonwidget(
126                    parent=self._root_widget,
127                    position=(h - key_width * 2.0, v),
128                    size=(key_width * 1.7, key_height),
129                    autoselect=True,
130                    textcolor=key_textcolor,
131                    color=key_color_dark,
132                    label=charstr(SpCh.SHIFT),
133                    enable_sound=False,
134                    extra_touch_border_scale=0.3,
135                    button_type='square',
136                )
137
138            for _ in row:
139                btn = ba.buttonwidget(
140                    parent=self._root_widget,
141                    position=(h, v),
142                    size=(key_width, key_height),
143                    autoselect=True,
144                    enable_sound=False,
145                    textcolor=key_textcolor,
146                    color=key_color,
147                    label='',
148                    button_type='square',
149                    extra_touch_border_scale=0.1,
150                )
151                self._char_keys.append(btn)
152                h += key_width + 10
153
154            # Add delete key at end of third row.
155            if row_num == 2:
156                if self._backspace_button is not None:
157                    self._backspace_button.delete()
158
159                self._backspace_button = ba.buttonwidget(
160                    parent=self._root_widget,
161                    position=(h + 4, v),
162                    size=(key_width * 1.8, key_height),
163                    autoselect=True,
164                    enable_sound=False,
165                    repeat=True,
166                    textcolor=key_textcolor,
167                    color=key_color_dark,
168                    label=charstr(SpCh.DELETE),
169                    button_type='square',
170                    on_activate_call=self._del)
171            v -= (key_height + 9)
172            # Do space bar and stuff.
173            if row_num == 2:
174                if self._num_mode_button is None:
175                    self._num_mode_button = ba.buttonwidget(
176                        parent=self._root_widget,
177                        position=(112, v - 8),
178                        size=(key_width * 2, key_height + 5),
179                        enable_sound=False,
180                        button_type='square',
181                        extra_touch_border_scale=0.3,
182                        autoselect=True,
183                        textcolor=key_textcolor,
184                        color=key_color_dark,
185                        label='',
186                    )
187                if self._emoji_button is None:
188                    self._emoji_button = ba.buttonwidget(
189                        parent=self._root_widget,
190                        position=(56, v - 8),
191                        size=(key_width, key_height + 5),
192                        autoselect=True,
193                        enable_sound=False,
194                        textcolor=key_textcolor,
195                        color=key_color_dark,
196                        label=charstr(SpCh.LOGO_FLAT),
197                        extra_touch_border_scale=0.3,
198                        button_type='square',
199                    )
200                btn1 = self._num_mode_button
201                if self._space_button is None:
202                    self._space_button = ba.buttonwidget(
203                        parent=self._root_widget,
204                        position=(210, v - 12),
205                        size=(key_width * 6.1, key_height + 15),
206                        extra_touch_border_scale=0.3,
207                        enable_sound=False,
208                        autoselect=True,
209                        textcolor=key_textcolor,
210                        color=key_color_dark,
211                        label=ba.Lstr(resource='spaceKeyText'),
212                        on_activate_call=ba.Call(self._type_char, ' '))
213
214                    # Show change instructions only if we have more than one
215                    # keyboard option.
216                    keyboards = (ba.app.meta.scanresults.exports_of_class(
217                        ba.Keyboard) if ba.app.meta.scanresults is not None
218                                 else [])
219                    if len(keyboards) > 1:
220                        ba.textwidget(
221                            parent=self._root_widget,
222                            h_align='center',
223                            position=(210, v - 70),
224                            size=(key_width * 6.1, key_height + 15),
225                            text=ba.Lstr(
226                                resource='keyboardChangeInstructionsText'),
227                            scale=0.75)
228                btn2 = self._space_button
229                btn3 = self._emoji_button
230                ba.widget(edit=btn1, right_widget=btn2, left_widget=btn3)
231                ba.widget(edit=btn2,
232                          left_widget=btn1,
233                          right_widget=self._done_button)
234                ba.widget(edit=btn3, left_widget=btn1)
235                ba.widget(edit=self._done_button, left_widget=btn2)
236
237        ba.containerwidget(edit=self._root_widget,
238                           selected_child=self._char_keys[14])
239
240        self._refresh()
241
242    def _get_keyboard(self) -> ba.Keyboard:
243        assert ba.app.meta.scanresults is not None
244        classname = ba.app.meta.scanresults.exports_of_class(
245            ba.Keyboard)[self._keyboard_index]
246        kbclass = ba.getclass(classname, ba.Keyboard)
247        return kbclass()
248
249    def _refresh(self) -> None:
250        chars: list[str] | None = None
251        if self._mode in ['normal', 'caps']:
252            chars = list(self._chars)
253            if self._mode == 'caps':
254                chars = [c.upper() for c in chars]
255            ba.buttonwidget(edit=self._shift_button,
256                            color=self._key_color_lit
257                            if self._mode == 'caps' else self._key_color_dark,
258                            label=charstr(SpCh.SHIFT),
259                            on_activate_call=self._shift)
260            ba.buttonwidget(edit=self._num_mode_button,
261                            label='123#&*',
262                            on_activate_call=self._num_mode)
263            ba.buttonwidget(edit=self._emoji_button,
264                            color=self._key_color_dark,
265                            label=charstr(SpCh.LOGO_FLAT),
266                            on_activate_call=self._next_mode)
267        else:
268            if self._mode == 'num':
269                chars = list(self._keyboard.nums)
270            else:
271                chars = list(self._keyboard.pages[self._mode])
272            ba.buttonwidget(edit=self._shift_button,
273                            color=self._key_color_dark,
274                            label='',
275                            on_activate_call=self._null_press)
276            ba.buttonwidget(edit=self._num_mode_button,
277                            label='abc',
278                            on_activate_call=self._abc_mode)
279            ba.buttonwidget(edit=self._emoji_button,
280                            color=self._key_color_dark,
281                            label=charstr(SpCh.LOGO_FLAT),
282                            on_activate_call=self._next_mode)
283
284        for i, btn in enumerate(self._char_keys):
285            assert chars is not None
286            have_char = True
287            if i >= len(chars):
288                # No such char.
289                have_char = False
290                pagename = self._mode
291                ba.print_error(
292                    f'Size of page "{pagename}" of keyboard'
293                    f' "{self._keyboard.name}" is incorrect:'
294                    f' {len(chars)} != {len(self._chars)}'
295                    f' (size of default "normal" page)',
296                    once=True)
297            ba.buttonwidget(edit=btn,
298                            label=chars[i] if have_char else ' ',
299                            on_activate_call=ba.Call(
300                                self._type_char,
301                                chars[i] if have_char else ' '))
302
303    def _null_press(self) -> None:
304        ba.playsound(self._click_sound)
305
306    def _abc_mode(self) -> None:
307        ba.playsound(self._click_sound)
308        self._mode = 'normal'
309        self._refresh()
310
311    def _num_mode(self) -> None:
312        ba.playsound(self._click_sound)
313        self._mode = 'num'
314        self._refresh()
315
316    def _next_mode(self) -> None:
317        ba.playsound(self._click_sound)
318        self._mode_index = (self._mode_index + 1) % len(self._modes)
319        self._mode = self._modes[self._mode_index]
320        self._refresh()
321
322    def _next_keyboard(self) -> None:
323        assert ba.app.meta.scanresults is not None
324        kbexports = ba.app.meta.scanresults.exports_of_class(ba.Keyboard)
325        self._keyboard_index = (self._keyboard_index + 1) % len(kbexports)
326
327        self._load_keyboard()
328        if len(kbexports) < 2:
329            ba.playsound(ba.getsound('error'))
330            ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'),
331                             color=(1, 0, 0))
332        else:
333            ba.screenmessage(ba.Lstr(resource='keyboardSwitchText',
334                                     subs=[('${NAME}', self._keyboard.name)]),
335                             color=(0, 1, 0))
336
337    def _shift(self) -> None:
338        ba.playsound(self._click_sound)
339        if self._mode == 'normal':
340            self._mode = 'caps'
341            self._double_press_shift = False
342        elif self._mode == 'caps':
343            if not self._double_press_shift:
344                self._double_press_shift = True
345            else:
346                self._mode = 'normal'
347        self._refresh()
348
349    def _del(self) -> None:
350        ba.playsound(self._click_sound)
351        txt = cast(str, ba.textwidget(query=self._text_field))
352        # pylint: disable=unsubscriptable-object
353        txt = txt[:-1]
354        ba.textwidget(edit=self._text_field, text=txt)
355
356    def _type_char(self, char: str) -> None:
357        ba.playsound(self._click_sound)
358        if char.isspace():
359            if (ba.time(ba.TimeType.REAL) - self._last_space_press <
360                    self._double_space_interval):
361                self._last_space_press = 0
362                self._next_keyboard()
363                self._del()  # We typed unneeded space around 1s ago.
364                return
365            self._last_space_press = ba.time(ba.TimeType.REAL)
366
367        # Operate in unicode so we don't do anything funky like chop utf-8
368        # chars in half.
369        txt = cast(str, ba.textwidget(query=self._text_field))
370        txt += char
371        ba.textwidget(edit=self._text_field, text=txt)
372
373        # If we were caps, go back only if not Shift is pressed twice.
374        if self._mode == 'caps' and not self._double_press_shift:
375            self._mode = 'normal'
376        self._refresh()
377
378    def _cancel(self) -> None:
379        ba.playsound(ba.getsound('swish'))
380        ba.containerwidget(edit=self._root_widget, transition='out_scale')
381
382    def _done(self) -> None:
383        ba.containerwidget(edit=self._root_widget, transition='out_scale')
384        if self._target_text:
385            ba.textwidget(edit=self._target_text,
386                          text=cast(str,
387                                    ba.textwidget(query=self._text_field)))
class OnScreenKeyboardWindow(ba.ui.Window):
 19class OnScreenKeyboardWindow(ba.Window):
 20    """Simple built-in on-screen keyboard."""
 21
 22    def __init__(self, textwidget: ba.Widget, label: str, max_chars: int):
 23        self._target_text = textwidget
 24        self._width = 700
 25        self._height = 400
 26        uiscale = ba.app.ui.uiscale
 27        top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
 28        super().__init__(root_widget=ba.containerwidget(
 29            parent=_ba.get_special_widget('overlay_stack'),
 30            size=(self._width, self._height + top_extra),
 31            transition='in_scale',
 32            scale_origin_stack_offset=self._target_text.
 33            get_screen_space_center(),
 34            scale=(2.0 if uiscale is ba.UIScale.SMALL else
 35                   1.5 if uiscale is ba.UIScale.MEDIUM else 1.0),
 36            stack_offset=(0, 0) if uiscale is ba.UIScale.SMALL else (
 37                0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0)))
 38        self._done_button = ba.buttonwidget(parent=self._root_widget,
 39                                            position=(self._width - 200, 44),
 40                                            size=(140, 60),
 41                                            autoselect=True,
 42                                            label=ba.Lstr(resource='doneText'),
 43                                            on_activate_call=self._done)
 44        ba.containerwidget(edit=self._root_widget,
 45                           on_cancel_call=self._cancel,
 46                           start_button=self._done_button)
 47
 48        ba.textwidget(parent=self._root_widget,
 49                      position=(self._width * 0.5, self._height - 41),
 50                      size=(0, 0),
 51                      scale=0.95,
 52                      text=label,
 53                      maxwidth=self._width - 140,
 54                      color=ba.app.ui.title_color,
 55                      h_align='center',
 56                      v_align='center')
 57
 58        self._text_field = ba.textwidget(
 59            parent=self._root_widget,
 60            position=(70, self._height - 116),
 61            max_chars=max_chars,
 62            text=cast(str, ba.textwidget(query=self._target_text)),
 63            on_return_press_call=self._done,
 64            autoselect=True,
 65            size=(self._width - 140, 55),
 66            v_align='center',
 67            editable=True,
 68            maxwidth=self._width - 175,
 69            force_internal_editing=True,
 70            always_show_carat=True)
 71
 72        self._key_color_lit = (1.4, 1.2, 1.4)
 73        self._key_color = (0.69, 0.6, 0.74)
 74        self._key_color_dark = (0.55, 0.55, 0.71)
 75
 76        self._shift_button: ba.Widget | None = None
 77        self._backspace_button: ba.Widget | None = None
 78        self._space_button: ba.Widget | None = None
 79        self._double_press_shift = False
 80        self._num_mode_button: ba.Widget | None = None
 81        self._emoji_button: ba.Widget | None = None
 82        self._char_keys: list[ba.Widget] = []
 83        self._keyboard_index = 0
 84        self._last_space_press = 0.0
 85        self._double_space_interval = 0.3
 86
 87        self._keyboard: ba.Keyboard
 88        self._chars: list[str]
 89        self._modes: list[str]
 90        self._mode: str
 91        self._mode_index: int
 92        self._load_keyboard()
 93
 94    def _load_keyboard(self) -> None:
 95        # pylint: disable=too-many-locals
 96        self._keyboard = self._get_keyboard()
 97        # We want to get just chars without column data, etc.
 98        self._chars = [j for i in self._keyboard.chars for j in i]
 99        self._modes = ['normal'] + list(self._keyboard.pages)
100        self._mode_index = 0
101        self._mode = self._modes[self._mode_index]
102
103        v = self._height - 180.0
104        key_width = 46 * 10 / len(self._keyboard.chars[0])
105        key_height = 46 * 3 / len(self._keyboard.chars)
106        key_textcolor = (1, 1, 1)
107        row_starts = (69.0, 95.0, 151.0)
108        key_color = self._key_color
109        key_color_dark = self._key_color_dark
110
111        self._click_sound = ba.getsound('click01')
112
113        # kill prev char keys
114        for key in self._char_keys:
115            key.delete()
116        self._char_keys = []
117
118        # dummy data just used for row/column lengths... we don't actually
119        # set things until refresh
120        chars: list[tuple[str, ...]] = self._keyboard.chars
121
122        for row_num, row in enumerate(chars):
123            h = row_starts[row_num]
124            # shift key before row 3
125            if row_num == 2 and self._shift_button is None:
126                self._shift_button = ba.buttonwidget(
127                    parent=self._root_widget,
128                    position=(h - key_width * 2.0, v),
129                    size=(key_width * 1.7, key_height),
130                    autoselect=True,
131                    textcolor=key_textcolor,
132                    color=key_color_dark,
133                    label=charstr(SpCh.SHIFT),
134                    enable_sound=False,
135                    extra_touch_border_scale=0.3,
136                    button_type='square',
137                )
138
139            for _ in row:
140                btn = ba.buttonwidget(
141                    parent=self._root_widget,
142                    position=(h, v),
143                    size=(key_width, key_height),
144                    autoselect=True,
145                    enable_sound=False,
146                    textcolor=key_textcolor,
147                    color=key_color,
148                    label='',
149                    button_type='square',
150                    extra_touch_border_scale=0.1,
151                )
152                self._char_keys.append(btn)
153                h += key_width + 10
154
155            # Add delete key at end of third row.
156            if row_num == 2:
157                if self._backspace_button is not None:
158                    self._backspace_button.delete()
159
160                self._backspace_button = ba.buttonwidget(
161                    parent=self._root_widget,
162                    position=(h + 4, v),
163                    size=(key_width * 1.8, key_height),
164                    autoselect=True,
165                    enable_sound=False,
166                    repeat=True,
167                    textcolor=key_textcolor,
168                    color=key_color_dark,
169                    label=charstr(SpCh.DELETE),
170                    button_type='square',
171                    on_activate_call=self._del)
172            v -= (key_height + 9)
173            # Do space bar and stuff.
174            if row_num == 2:
175                if self._num_mode_button is None:
176                    self._num_mode_button = ba.buttonwidget(
177                        parent=self._root_widget,
178                        position=(112, v - 8),
179                        size=(key_width * 2, key_height + 5),
180                        enable_sound=False,
181                        button_type='square',
182                        extra_touch_border_scale=0.3,
183                        autoselect=True,
184                        textcolor=key_textcolor,
185                        color=key_color_dark,
186                        label='',
187                    )
188                if self._emoji_button is None:
189                    self._emoji_button = ba.buttonwidget(
190                        parent=self._root_widget,
191                        position=(56, v - 8),
192                        size=(key_width, key_height + 5),
193                        autoselect=True,
194                        enable_sound=False,
195                        textcolor=key_textcolor,
196                        color=key_color_dark,
197                        label=charstr(SpCh.LOGO_FLAT),
198                        extra_touch_border_scale=0.3,
199                        button_type='square',
200                    )
201                btn1 = self._num_mode_button
202                if self._space_button is None:
203                    self._space_button = ba.buttonwidget(
204                        parent=self._root_widget,
205                        position=(210, v - 12),
206                        size=(key_width * 6.1, key_height + 15),
207                        extra_touch_border_scale=0.3,
208                        enable_sound=False,
209                        autoselect=True,
210                        textcolor=key_textcolor,
211                        color=key_color_dark,
212                        label=ba.Lstr(resource='spaceKeyText'),
213                        on_activate_call=ba.Call(self._type_char, ' '))
214
215                    # Show change instructions only if we have more than one
216                    # keyboard option.
217                    keyboards = (ba.app.meta.scanresults.exports_of_class(
218                        ba.Keyboard) if ba.app.meta.scanresults is not None
219                                 else [])
220                    if len(keyboards) > 1:
221                        ba.textwidget(
222                            parent=self._root_widget,
223                            h_align='center',
224                            position=(210, v - 70),
225                            size=(key_width * 6.1, key_height + 15),
226                            text=ba.Lstr(
227                                resource='keyboardChangeInstructionsText'),
228                            scale=0.75)
229                btn2 = self._space_button
230                btn3 = self._emoji_button
231                ba.widget(edit=btn1, right_widget=btn2, left_widget=btn3)
232                ba.widget(edit=btn2,
233                          left_widget=btn1,
234                          right_widget=self._done_button)
235                ba.widget(edit=btn3, left_widget=btn1)
236                ba.widget(edit=self._done_button, left_widget=btn2)
237
238        ba.containerwidget(edit=self._root_widget,
239                           selected_child=self._char_keys[14])
240
241        self._refresh()
242
243    def _get_keyboard(self) -> ba.Keyboard:
244        assert ba.app.meta.scanresults is not None
245        classname = ba.app.meta.scanresults.exports_of_class(
246            ba.Keyboard)[self._keyboard_index]
247        kbclass = ba.getclass(classname, ba.Keyboard)
248        return kbclass()
249
250    def _refresh(self) -> None:
251        chars: list[str] | None = None
252        if self._mode in ['normal', 'caps']:
253            chars = list(self._chars)
254            if self._mode == 'caps':
255                chars = [c.upper() for c in chars]
256            ba.buttonwidget(edit=self._shift_button,
257                            color=self._key_color_lit
258                            if self._mode == 'caps' else self._key_color_dark,
259                            label=charstr(SpCh.SHIFT),
260                            on_activate_call=self._shift)
261            ba.buttonwidget(edit=self._num_mode_button,
262                            label='123#&*',
263                            on_activate_call=self._num_mode)
264            ba.buttonwidget(edit=self._emoji_button,
265                            color=self._key_color_dark,
266                            label=charstr(SpCh.LOGO_FLAT),
267                            on_activate_call=self._next_mode)
268        else:
269            if self._mode == 'num':
270                chars = list(self._keyboard.nums)
271            else:
272                chars = list(self._keyboard.pages[self._mode])
273            ba.buttonwidget(edit=self._shift_button,
274                            color=self._key_color_dark,
275                            label='',
276                            on_activate_call=self._null_press)
277            ba.buttonwidget(edit=self._num_mode_button,
278                            label='abc',
279                            on_activate_call=self._abc_mode)
280            ba.buttonwidget(edit=self._emoji_button,
281                            color=self._key_color_dark,
282                            label=charstr(SpCh.LOGO_FLAT),
283                            on_activate_call=self._next_mode)
284
285        for i, btn in enumerate(self._char_keys):
286            assert chars is not None
287            have_char = True
288            if i >= len(chars):
289                # No such char.
290                have_char = False
291                pagename = self._mode
292                ba.print_error(
293                    f'Size of page "{pagename}" of keyboard'
294                    f' "{self._keyboard.name}" is incorrect:'
295                    f' {len(chars)} != {len(self._chars)}'
296                    f' (size of default "normal" page)',
297                    once=True)
298            ba.buttonwidget(edit=btn,
299                            label=chars[i] if have_char else ' ',
300                            on_activate_call=ba.Call(
301                                self._type_char,
302                                chars[i] if have_char else ' '))
303
304    def _null_press(self) -> None:
305        ba.playsound(self._click_sound)
306
307    def _abc_mode(self) -> None:
308        ba.playsound(self._click_sound)
309        self._mode = 'normal'
310        self._refresh()
311
312    def _num_mode(self) -> None:
313        ba.playsound(self._click_sound)
314        self._mode = 'num'
315        self._refresh()
316
317    def _next_mode(self) -> None:
318        ba.playsound(self._click_sound)
319        self._mode_index = (self._mode_index + 1) % len(self._modes)
320        self._mode = self._modes[self._mode_index]
321        self._refresh()
322
323    def _next_keyboard(self) -> None:
324        assert ba.app.meta.scanresults is not None
325        kbexports = ba.app.meta.scanresults.exports_of_class(ba.Keyboard)
326        self._keyboard_index = (self._keyboard_index + 1) % len(kbexports)
327
328        self._load_keyboard()
329        if len(kbexports) < 2:
330            ba.playsound(ba.getsound('error'))
331            ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'),
332                             color=(1, 0, 0))
333        else:
334            ba.screenmessage(ba.Lstr(resource='keyboardSwitchText',
335                                     subs=[('${NAME}', self._keyboard.name)]),
336                             color=(0, 1, 0))
337
338    def _shift(self) -> None:
339        ba.playsound(self._click_sound)
340        if self._mode == 'normal':
341            self._mode = 'caps'
342            self._double_press_shift = False
343        elif self._mode == 'caps':
344            if not self._double_press_shift:
345                self._double_press_shift = True
346            else:
347                self._mode = 'normal'
348        self._refresh()
349
350    def _del(self) -> None:
351        ba.playsound(self._click_sound)
352        txt = cast(str, ba.textwidget(query=self._text_field))
353        # pylint: disable=unsubscriptable-object
354        txt = txt[:-1]
355        ba.textwidget(edit=self._text_field, text=txt)
356
357    def _type_char(self, char: str) -> None:
358        ba.playsound(self._click_sound)
359        if char.isspace():
360            if (ba.time(ba.TimeType.REAL) - self._last_space_press <
361                    self._double_space_interval):
362                self._last_space_press = 0
363                self._next_keyboard()
364                self._del()  # We typed unneeded space around 1s ago.
365                return
366            self._last_space_press = ba.time(ba.TimeType.REAL)
367
368        # Operate in unicode so we don't do anything funky like chop utf-8
369        # chars in half.
370        txt = cast(str, ba.textwidget(query=self._text_field))
371        txt += char
372        ba.textwidget(edit=self._text_field, text=txt)
373
374        # If we were caps, go back only if not Shift is pressed twice.
375        if self._mode == 'caps' and not self._double_press_shift:
376            self._mode = 'normal'
377        self._refresh()
378
379    def _cancel(self) -> None:
380        ba.playsound(ba.getsound('swish'))
381        ba.containerwidget(edit=self._root_widget, transition='out_scale')
382
383    def _done(self) -> None:
384        ba.containerwidget(edit=self._root_widget, transition='out_scale')
385        if self._target_text:
386            ba.textwidget(edit=self._target_text,
387                          text=cast(str,
388                                    ba.textwidget(query=self._text_field)))

Simple built-in on-screen keyboard.

OnScreenKeyboardWindow(textwidget: _ba.Widget, label: str, max_chars: int)
22    def __init__(self, textwidget: ba.Widget, label: str, max_chars: int):
23        self._target_text = textwidget
24        self._width = 700
25        self._height = 400
26        uiscale = ba.app.ui.uiscale
27        top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
28        super().__init__(root_widget=ba.containerwidget(
29            parent=_ba.get_special_widget('overlay_stack'),
30            size=(self._width, self._height + top_extra),
31            transition='in_scale',
32            scale_origin_stack_offset=self._target_text.
33            get_screen_space_center(),
34            scale=(2.0 if uiscale is ba.UIScale.SMALL else
35                   1.5 if uiscale is ba.UIScale.MEDIUM else 1.0),
36            stack_offset=(0, 0) if uiscale is ba.UIScale.SMALL else (
37                0, 0) if uiscale is ba.UIScale.MEDIUM else (0, 0)))
38        self._done_button = ba.buttonwidget(parent=self._root_widget,
39                                            position=(self._width - 200, 44),
40                                            size=(140, 60),
41                                            autoselect=True,
42                                            label=ba.Lstr(resource='doneText'),
43                                            on_activate_call=self._done)
44        ba.containerwidget(edit=self._root_widget,
45                           on_cancel_call=self._cancel,
46                           start_button=self._done_button)
47
48        ba.textwidget(parent=self._root_widget,
49                      position=(self._width * 0.5, self._height - 41),
50                      size=(0, 0),
51                      scale=0.95,
52                      text=label,
53                      maxwidth=self._width - 140,
54                      color=ba.app.ui.title_color,
55                      h_align='center',
56                      v_align='center')
57
58        self._text_field = ba.textwidget(
59            parent=self._root_widget,
60            position=(70, self._height - 116),
61            max_chars=max_chars,
62            text=cast(str, ba.textwidget(query=self._target_text)),
63            on_return_press_call=self._done,
64            autoselect=True,
65            size=(self._width - 140, 55),
66            v_align='center',
67            editable=True,
68            maxwidth=self._width - 175,
69            force_internal_editing=True,
70            always_show_carat=True)
71
72        self._key_color_lit = (1.4, 1.2, 1.4)
73        self._key_color = (0.69, 0.6, 0.74)
74        self._key_color_dark = (0.55, 0.55, 0.71)
75
76        self._shift_button: ba.Widget | None = None
77        self._backspace_button: ba.Widget | None = None
78        self._space_button: ba.Widget | None = None
79        self._double_press_shift = False
80        self._num_mode_button: ba.Widget | None = None
81        self._emoji_button: ba.Widget | None = None
82        self._char_keys: list[ba.Widget] = []
83        self._keyboard_index = 0
84        self._last_space_press = 0.0
85        self._double_space_interval = 0.3
86
87        self._keyboard: ba.Keyboard
88        self._chars: list[str]
89        self._modes: list[str]
90        self._mode: str
91        self._mode_index: int
92        self._load_keyboard()
Inherited Members
ba.ui.Window
get_root_widget