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