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