bauiv1lib.settings.gamepad
Settings UI functionality related to gamepads.
1# Released under the MIT License. See LICENSE for details. 2# 3# pylint: disable=too-many-lines 4"""Settings UI functionality related to gamepads.""" 5 6from __future__ import annotations 7 8import logging 9from typing import TYPE_CHECKING 10 11from bauiv1lib.popup import PopupMenuWindow 12import bascenev1 as bs 13import bauiv1 as bui 14 15if TYPE_CHECKING: 16 from typing import Any, Callable 17 from bauiv1lib.popup import PopupWindow 18 19 20class GamepadSettingsWindow(bui.MainWindow): 21 """Window for configuring a gamepad.""" 22 23 # pylint: disable=too-many-public-methods 24 25 def __init__( 26 self, 27 gamepad: bs.InputDevice, 28 is_main_menu: bool = True, 29 transition: str = 'in_right', 30 transition_out: str = 'out_right', 31 origin_widget: bui.Widget | None = None, 32 settings: dict | None = None, 33 ): 34 self._input = gamepad 35 36 # If our input-device went away, just return an empty zombie. 37 if not self._input: 38 return 39 40 self._name = self._input.name 41 42 self._r = 'configGamepadWindow' 43 self._transition_out = transition_out 44 45 # We're a secondary gamepad if supplied with settings. 46 self._is_secondary = settings is not None 47 self._ext = '_B' if self._is_secondary else '' 48 self._is_main_menu = is_main_menu 49 self._displayname = self._name 50 self._width = 700 if self._is_secondary else 730 51 self._height = 440 if self._is_secondary else 450 52 self._spacing = 40 53 assert bui.app.classic is not None 54 uiscale = bui.app.ui_v1.uiscale 55 super().__init__( 56 root_widget=bui.containerwidget( 57 size=(self._width, self._height), 58 scale=( 59 1.63 60 if uiscale is bui.UIScale.SMALL 61 else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 62 ), 63 stack_offset=( 64 (-20, -16) if uiscale is bui.UIScale.SMALL else (0, 0) 65 ), 66 transition=transition, 67 ), 68 transition=transition, 69 origin_widget=origin_widget, 70 ) 71 72 self._settings: dict[str, int] = {} 73 if not self._is_secondary: 74 self._get_config_mapping() 75 # Don't ask to config joysticks while we're in here. 76 self._rebuild_ui() 77 78 def _get_config_mapping(self, default: bool = False) -> None: 79 for button in [ 80 'buttonJump', 81 'buttonJump_B', 82 'buttonPunch', 83 'buttonPunch_B', 84 'buttonBomb', 85 'buttonBomb_B', 86 'buttonPickUp', 87 'buttonPickUp_B', 88 'buttonStart', 89 'buttonStart_B', 90 'buttonStart2', 91 'buttonStart2_B', 92 'buttonUp', 93 'buttonUp_B', 94 'buttonDown', 95 'buttonDown_B', 96 'buttonLeft', 97 'buttonLeft_B', 98 'buttonRight', 99 'buttonRight_B', 100 'buttonRun1', 101 'buttonRun1_B', 102 'buttonRun2', 103 'buttonRun2_B', 104 'triggerRun1', 105 'triggerRun1_B', 106 'triggerRun2', 107 'triggerRun2_B', 108 'buttonIgnored', 109 'buttonIgnored_B', 110 'buttonIgnored2', 111 'buttonIgnored2_B', 112 'buttonIgnored3', 113 'buttonIgnored3_B', 114 'buttonIgnored4', 115 'buttonIgnored4_B', 116 'buttonVRReorient', 117 'buttonVRReorient_B', 118 'analogStickDeadZone', 119 'analogStickDeadZone_B', 120 'dpad', 121 'dpad_B', 122 'unassignedButtonsRun', 123 'unassignedButtonsRun_B', 124 'startButtonActivatesDefaultWidget', 125 'startButtonActivatesDefaultWidget_B', 126 'uiOnly', 127 'uiOnly_B', 128 'ignoreCompletely', 129 'ignoreCompletely_B', 130 'autoRecalibrateAnalogStick', 131 'autoRecalibrateAnalogStick_B', 132 'analogStickLR', 133 'analogStickLR_B', 134 'analogStickUD', 135 'analogStickUD_B', 136 'enableSecondary', 137 ]: 138 assert bui.app.classic is not None 139 val = bui.app.classic.get_input_device_mapped_value( 140 self._input, button, default 141 ) 142 if val != -1: 143 self._settings[button] = val 144 145 def _rebuild_ui(self, is_reset: bool = False) -> None: 146 # pylint: disable=too-many-statements 147 148 assert bui.app.classic is not None 149 150 # Clear existing UI. 151 for widget in self._root_widget.get_children(): 152 widget.delete() 153 154 self._textwidgets: dict[str, bui.Widget] = {} 155 156 back_button: bui.Widget | None 157 158 if self._is_secondary: 159 back_button = bui.buttonwidget( 160 parent=self._root_widget, 161 position=(self._width - 180, self._height - 65), 162 autoselect=True, 163 size=(160, 60), 164 label=bui.Lstr(resource='doneText'), 165 scale=0.9, 166 on_activate_call=self._save, 167 ) 168 bui.containerwidget( 169 edit=self._root_widget, 170 start_button=back_button, 171 on_cancel_call=back_button.activate, 172 ) 173 cancel_button = None 174 else: 175 cancel_button = bui.buttonwidget( 176 parent=self._root_widget, 177 position=(51, self._height - 65), 178 autoselect=True, 179 size=(160, 60), 180 label=bui.Lstr(resource='cancelText'), 181 scale=0.9, 182 on_activate_call=self._cancel, 183 ) 184 bui.containerwidget( 185 edit=self._root_widget, cancel_button=cancel_button 186 ) 187 188 save_button: bui.Widget | None 189 if not self._is_secondary: 190 save_button = bui.buttonwidget( 191 parent=self._root_widget, 192 position=(self._width - 195, self._height - 65), 193 size=(180, 60), 194 autoselect=True, 195 label=bui.Lstr(resource='saveText'), 196 scale=0.9, 197 on_activate_call=self._save, 198 ) 199 bui.containerwidget( 200 edit=self._root_widget, start_button=save_button 201 ) 202 else: 203 save_button = None 204 205 if not self._is_secondary: 206 v = self._height - 59 207 bui.textwidget( 208 parent=self._root_widget, 209 position=(0, v + 5), 210 size=(self._width, 25), 211 text=bui.Lstr(resource=f'{self._r}.titleText'), 212 color=bui.app.ui_v1.title_color, 213 maxwidth=310, 214 h_align='center', 215 v_align='center', 216 ) 217 v -= 48 218 219 bui.textwidget( 220 parent=self._root_widget, 221 position=(0, v + 3), 222 size=(self._width, 25), 223 text=self._name, 224 color=bui.app.ui_v1.infotextcolor, 225 maxwidth=self._width * 0.9, 226 h_align='center', 227 v_align='center', 228 ) 229 v -= self._spacing * 1 230 231 bui.textwidget( 232 parent=self._root_widget, 233 position=(50, v + 10), 234 size=(self._width - 100, 30), 235 text=bui.Lstr(resource=f'{self._r}.appliesToAllText'), 236 maxwidth=330, 237 scale=0.65, 238 color=(0.5, 0.6, 0.5, 1.0), 239 h_align='center', 240 v_align='center', 241 ) 242 v -= 70 243 self._enable_check_box = None 244 else: 245 v = self._height - 49 246 bui.textwidget( 247 parent=self._root_widget, 248 position=(0, v + 5), 249 size=(self._width, 25), 250 text=bui.Lstr(resource=f'{self._r}.secondaryText'), 251 color=bui.app.ui_v1.title_color, 252 maxwidth=300, 253 h_align='center', 254 v_align='center', 255 ) 256 v -= self._spacing * 1 257 258 bui.textwidget( 259 parent=self._root_widget, 260 position=(50, v + 10), 261 size=(self._width - 100, 30), 262 text=bui.Lstr(resource=f'{self._r}.secondHalfText'), 263 maxwidth=300, 264 scale=0.65, 265 color=(0.6, 0.8, 0.6, 1.0), 266 h_align='center', 267 ) 268 self._enable_check_box = bui.checkboxwidget( 269 parent=self._root_widget, 270 position=(self._width * 0.5 - 80, v - 73), 271 value=self.get_enable_secondary_value(), 272 autoselect=True, 273 on_value_change_call=self._enable_check_box_changed, 274 size=(200, 30), 275 text=bui.Lstr(resource=f'{self._r}.secondaryEnableText'), 276 scale=1.2, 277 ) 278 v = self._height - 205 279 280 h_offs = 160 281 dist = 70 282 d_color = (0.4, 0.4, 0.8) 283 sclx = 1.2 284 scly = 0.98 285 dpm = bui.Lstr(resource=f'{self._r}.pressAnyButtonOrDpadText') 286 dpm2 = bui.Lstr(resource=f'{self._r}.ifNothingHappensTryAnalogText') 287 self._capture_button( 288 pos=(h_offs, v + scly * dist), 289 color=d_color, 290 button='buttonUp' + self._ext, 291 texture=bui.gettexture('upButton'), 292 scale=1.0, 293 message=dpm, 294 message2=dpm2, 295 ) 296 self._capture_button( 297 pos=(h_offs - sclx * dist, v), 298 color=d_color, 299 button='buttonLeft' + self._ext, 300 texture=bui.gettexture('leftButton'), 301 scale=1.0, 302 message=dpm, 303 message2=dpm2, 304 ) 305 self._capture_button( 306 pos=(h_offs + sclx * dist, v), 307 color=d_color, 308 button='buttonRight' + self._ext, 309 texture=bui.gettexture('rightButton'), 310 scale=1.0, 311 message=dpm, 312 message2=dpm2, 313 ) 314 self._capture_button( 315 pos=(h_offs, v - scly * dist), 316 color=d_color, 317 button='buttonDown' + self._ext, 318 texture=bui.gettexture('downButton'), 319 scale=1.0, 320 message=dpm, 321 message2=dpm2, 322 ) 323 324 dpm3 = bui.Lstr(resource=f'{self._r}.ifNothingHappensTryDpadText') 325 self._capture_button( 326 pos=(h_offs + 130, v - 125), 327 color=(0.4, 0.4, 0.6), 328 button='analogStickLR' + self._ext, 329 maxwidth=140, 330 texture=bui.gettexture('analogStick'), 331 scale=1.2, 332 message=bui.Lstr(resource=f'{self._r}.pressLeftRightText'), 333 message2=dpm3, 334 ) 335 336 self._capture_button( 337 pos=(self._width * 0.5, v), 338 color=(0.4, 0.4, 0.6), 339 button='buttonStart' + self._ext, 340 texture=bui.gettexture('startButton'), 341 scale=0.7, 342 ) 343 344 h_offs = self._width - 160 345 346 self._capture_button( 347 pos=(h_offs, v + scly * dist), 348 color=(0.6, 0.4, 0.8), 349 button='buttonPickUp' + self._ext, 350 texture=bui.gettexture('buttonPickUp'), 351 scale=1.0, 352 ) 353 self._capture_button( 354 pos=(h_offs - sclx * dist, v), 355 color=(0.7, 0.5, 0.1), 356 button='buttonPunch' + self._ext, 357 texture=bui.gettexture('buttonPunch'), 358 scale=1.0, 359 ) 360 self._capture_button( 361 pos=(h_offs + sclx * dist, v), 362 color=(0.5, 0.2, 0.1), 363 button='buttonBomb' + self._ext, 364 texture=bui.gettexture('buttonBomb'), 365 scale=1.0, 366 ) 367 self._capture_button( 368 pos=(h_offs, v - scly * dist), 369 color=(0.2, 0.5, 0.2), 370 button='buttonJump' + self._ext, 371 texture=bui.gettexture('buttonJump'), 372 scale=1.0, 373 ) 374 375 self._more_button = bui.buttonwidget( 376 parent=self._root_widget, 377 autoselect=True, 378 label='...', 379 text_scale=0.9, 380 color=(0.45, 0.4, 0.5), 381 textcolor=(0.65, 0.6, 0.7), 382 position=(self._width - 300, 30), 383 size=(130, 40), 384 on_activate_call=self._do_more, 385 ) 386 387 try: 388 if cancel_button is not None and save_button is not None: 389 bui.widget(edit=cancel_button, right_widget=save_button) 390 bui.widget(edit=save_button, left_widget=cancel_button) 391 if is_reset: 392 bui.containerwidget( 393 edit=self._root_widget, 394 selected_child=self._more_button, 395 ) 396 except Exception: 397 logging.exception('Error wiring up gamepad config window.') 398 399 def get_r(self) -> str: 400 """(internal)""" 401 return self._r 402 403 def get_advanced_button(self) -> bui.Widget: 404 """(internal)""" 405 return self._more_button 406 407 def get_is_secondary(self) -> bool: 408 """(internal)""" 409 return self._is_secondary 410 411 def get_settings(self) -> dict[str, Any]: 412 """(internal)""" 413 assert self._settings is not None 414 return self._settings 415 416 def get_ext(self) -> str: 417 """(internal)""" 418 return self._ext 419 420 def get_input(self) -> bs.InputDevice: 421 """(internal)""" 422 return self._input 423 424 def _do_advanced(self) -> None: 425 # pylint: disable=cyclic-import 426 from bauiv1lib.settings import gamepadadvanced 427 428 gamepadadvanced.GamepadAdvancedSettingsWindow(self) 429 430 def _enable_check_box_changed(self, value: bool) -> None: 431 assert self._settings is not None 432 if value: 433 self._settings['enableSecondary'] = 1 434 else: 435 # Just clear since this is default. 436 if 'enableSecondary' in self._settings: 437 del self._settings['enableSecondary'] 438 439 def get_unassigned_buttons_run_value(self) -> bool: 440 """(internal)""" 441 assert self._settings is not None 442 val = self._settings.get('unassignedButtonsRun', True) 443 assert isinstance(val, bool) 444 return val 445 446 def set_unassigned_buttons_run_value(self, value: bool) -> None: 447 """(internal)""" 448 assert self._settings is not None 449 if value: 450 if 'unassignedButtonsRun' in self._settings: 451 # Clear since this is default. 452 del self._settings['unassignedButtonsRun'] 453 return 454 self._settings['unassignedButtonsRun'] = False 455 456 def get_start_button_activates_default_widget_value(self) -> bool: 457 """(internal)""" 458 assert self._settings is not None 459 val = self._settings.get('startButtonActivatesDefaultWidget', True) 460 assert isinstance(val, bool) 461 return val 462 463 def set_start_button_activates_default_widget_value( 464 self, value: bool 465 ) -> None: 466 """(internal)""" 467 assert self._settings is not None 468 if value: 469 if 'startButtonActivatesDefaultWidget' in self._settings: 470 # Clear since this is default. 471 del self._settings['startButtonActivatesDefaultWidget'] 472 return 473 self._settings['startButtonActivatesDefaultWidget'] = False 474 475 def get_ui_only_value(self) -> bool: 476 """(internal)""" 477 assert self._settings is not None 478 val = self._settings.get('uiOnly', False) 479 assert isinstance(val, bool) 480 return val 481 482 def set_ui_only_value(self, value: bool) -> None: 483 """(internal)""" 484 assert self._settings is not None 485 if not value: 486 if 'uiOnly' in self._settings: 487 # Clear since this is default. 488 del self._settings['uiOnly'] 489 return 490 self._settings['uiOnly'] = True 491 492 def get_ignore_completely_value(self) -> bool: 493 """(internal)""" 494 assert self._settings is not None 495 val = self._settings.get('ignoreCompletely', False) 496 assert isinstance(val, bool) 497 return val 498 499 def set_ignore_completely_value(self, value: bool) -> None: 500 """(internal)""" 501 assert self._settings is not None 502 if not value: 503 if 'ignoreCompletely' in self._settings: 504 # Clear since this is default. 505 del self._settings['ignoreCompletely'] 506 return 507 self._settings['ignoreCompletely'] = True 508 509 def get_auto_recalibrate_analog_stick_value(self) -> bool: 510 """(internal)""" 511 assert self._settings is not None 512 val = self._settings.get('autoRecalibrateAnalogStick', False) 513 assert isinstance(val, bool) 514 return val 515 516 def set_auto_recalibrate_analog_stick_value(self, value: bool) -> None: 517 """(internal)""" 518 assert self._settings is not None 519 if not value: 520 if 'autoRecalibrateAnalogStick' in self._settings: 521 # Clear since this is default. 522 del self._settings['autoRecalibrateAnalogStick'] 523 else: 524 self._settings['autoRecalibrateAnalogStick'] = True 525 526 def get_enable_secondary_value(self) -> bool: 527 """(internal)""" 528 assert self._settings is not None 529 if not self._is_secondary: 530 raise RuntimeError('Enable value only applies to secondary editor.') 531 val = self._settings.get('enableSecondary', False) 532 assert isinstance(val, bool) 533 return val 534 535 def show_secondary_editor(self) -> None: 536 """(internal)""" 537 GamepadSettingsWindow( 538 self._input, 539 is_main_menu=False, 540 settings=self._settings, 541 transition='in_scale', 542 transition_out='out_scale', 543 ) 544 545 def get_control_value_name(self, control: str) -> str | bui.Lstr: 546 """(internal)""" 547 # pylint: disable=too-many-return-statements 548 assert self._settings is not None 549 if control == 'analogStickLR' + self._ext: 550 # This actually shows both LR and UD. 551 sval1 = ( 552 self._settings['analogStickLR' + self._ext] 553 if 'analogStickLR' + self._ext in self._settings 554 else 5 if self._is_secondary else None 555 ) 556 sval2 = ( 557 self._settings['analogStickUD' + self._ext] 558 if 'analogStickUD' + self._ext in self._settings 559 else 6 if self._is_secondary else None 560 ) 561 assert isinstance(sval1, (int, type(None))) 562 assert isinstance(sval2, (int, type(None))) 563 if sval1 is not None and sval2 is not None: 564 return ( 565 self._input.get_axis_name(sval1) 566 + ' / ' 567 + self._input.get_axis_name(sval2) 568 ) 569 return bui.Lstr(resource=f'{self._r}.unsetText') 570 571 # If they're looking for triggers. 572 if control in ['triggerRun1' + self._ext, 'triggerRun2' + self._ext]: 573 if control in self._settings: 574 return self._input.get_axis_name(self._settings[control]) 575 return bui.Lstr(resource=f'{self._r}.unsetText') 576 577 # Dead-zone. 578 if control == 'analogStickDeadZone' + self._ext: 579 if control in self._settings: 580 return str(self._settings[control]) 581 return str(1.0) 582 583 # For dpad buttons: show individual buttons if any are set. 584 # Otherwise show whichever dpad is set. 585 dpad_buttons = [ 586 'buttonLeft' + self._ext, 587 'buttonRight' + self._ext, 588 'buttonUp' + self._ext, 589 'buttonDown' + self._ext, 590 ] 591 if control in dpad_buttons: 592 # If *any* dpad buttons are assigned, show only button assignments. 593 if any(b in self._settings for b in dpad_buttons): 594 if control in self._settings: 595 return self._input.get_button_name(self._settings[control]) 596 return bui.Lstr(resource=f'{self._r}.unsetText') 597 598 # No dpad buttons - show the dpad number for all 4. 599 dpadnum = ( 600 self._settings['dpad' + self._ext] 601 if 'dpad' + self._ext in self._settings 602 else 2 if self._is_secondary else None 603 ) 604 assert isinstance(dpadnum, (int, type(None))) 605 if dpadnum is not None: 606 return bui.Lstr( 607 value='${A} ${B}', 608 subs=[ 609 ('${A}', bui.Lstr(resource=f'{self._r}.dpadText')), 610 ( 611 '${B}', 612 str(dpadnum), 613 ), 614 ], 615 ) 616 return bui.Lstr(resource=f'{self._r}.unsetText') 617 618 # Other buttons. 619 if control in self._settings: 620 return self._input.get_button_name(self._settings[control]) 621 return bui.Lstr(resource=f'{self._r}.unsetText') 622 623 def _gamepad_event( 624 self, 625 control: str, 626 event: dict[str, Any], 627 dialog: AwaitGamepadInputWindow, 628 ) -> None: 629 # pylint: disable=too-many-branches 630 assert self._settings is not None 631 ext = self._ext 632 633 # For our dpad-buttons we're looking for either a button-press or a 634 # hat-switch press. 635 if control in [ 636 'buttonUp' + ext, 637 'buttonLeft' + ext, 638 'buttonDown' + ext, 639 'buttonRight' + ext, 640 ]: 641 if event['type'] in ['BUTTONDOWN', 'HATMOTION']: 642 # If its a button-down. 643 if event['type'] == 'BUTTONDOWN': 644 value = event['button'] 645 self._settings[control] = value 646 647 # If its a dpad. 648 elif event['type'] == 'HATMOTION': 649 # clear out any set dir-buttons 650 for btn in [ 651 'buttonUp' + ext, 652 'buttonLeft' + ext, 653 'buttonRight' + ext, 654 'buttonDown' + ext, 655 ]: 656 if btn in self._settings: 657 del self._settings[btn] 658 if event['hat'] == (2 if self._is_secondary else 1): 659 self._settings['dpad' + ext] = event['hat'] 660 661 # Update the 4 dpad button txt widgets. 662 bui.textwidget( 663 edit=self._textwidgets['buttonUp' + ext], 664 text=self.get_control_value_name('buttonUp' + ext), 665 ) 666 bui.textwidget( 667 edit=self._textwidgets['buttonLeft' + ext], 668 text=self.get_control_value_name('buttonLeft' + ext), 669 ) 670 bui.textwidget( 671 edit=self._textwidgets['buttonRight' + ext], 672 text=self.get_control_value_name('buttonRight' + ext), 673 ) 674 bui.textwidget( 675 edit=self._textwidgets['buttonDown' + ext], 676 text=self.get_control_value_name('buttonDown' + ext), 677 ) 678 bui.getsound('gunCocking').play() 679 dialog.die() 680 681 elif control == 'analogStickLR' + ext: 682 if event['type'] == 'AXISMOTION': 683 # Ignore small values or else we might get triggered by noise. 684 if abs(event['value']) > 0.5: 685 axis = event['axis'] 686 if axis == (5 if self._is_secondary else 1): 687 self._settings['analogStickLR' + ext] = axis 688 bui.textwidget( 689 edit=self._textwidgets['analogStickLR' + ext], 690 text=self.get_control_value_name('analogStickLR' + ext), 691 ) 692 bui.getsound('gunCocking').play() 693 dialog.die() 694 695 # Now launch the up/down listener. 696 AwaitGamepadInputWindow( 697 self._input, 698 'analogStickUD' + ext, 699 self._gamepad_event, 700 bui.Lstr(resource=f'{self._r}.pressUpDownText'), 701 ) 702 703 elif control == 'analogStickUD' + ext: 704 if event['type'] == 'AXISMOTION': 705 # Ignore small values or else we might get triggered by noise. 706 if abs(event['value']) > 0.5: 707 axis = event['axis'] 708 709 # Ignore our LR axis. 710 if 'analogStickLR' + ext in self._settings: 711 lr_axis = self._settings['analogStickLR' + ext] 712 else: 713 lr_axis = 5 if self._is_secondary else 1 714 if axis != lr_axis: 715 if axis == (6 if self._is_secondary else 2): 716 self._settings['analogStickUD' + ext] = axis 717 bui.textwidget( 718 edit=self._textwidgets['analogStickLR' + ext], 719 text=self.get_control_value_name( 720 'analogStickLR' + ext 721 ), 722 ) 723 bui.getsound('gunCocking').play() 724 dialog.die() 725 else: 726 # For other buttons we just want a button-press. 727 if event['type'] == 'BUTTONDOWN': 728 value = event['button'] 729 self._settings[control] = value 730 731 # Update the button's text widget. 732 bui.textwidget( 733 edit=self._textwidgets[control], 734 text=self.get_control_value_name(control), 735 ) 736 bui.getsound('gunCocking').play() 737 dialog.die() 738 739 def _capture_button( 740 self, 741 pos: tuple[float, float], 742 color: tuple[float, float, float], 743 texture: bui.Texture, 744 button: str, 745 scale: float = 1.0, 746 message: bui.Lstr | None = None, 747 message2: bui.Lstr | None = None, 748 maxwidth: float = 80.0, 749 ) -> bui.Widget: 750 if message is None: 751 message = bui.Lstr(resource=f'{self._r}.pressAnyButtonText') 752 base_size = 79 753 btn = bui.buttonwidget( 754 parent=self._root_widget, 755 position=( 756 pos[0] - base_size * 0.5 * scale, 757 pos[1] - base_size * 0.5 * scale, 758 ), 759 autoselect=True, 760 size=(base_size * scale, base_size * scale), 761 texture=texture, 762 label='', 763 color=color, 764 ) 765 766 # Make this in a timer so that it shows up on top of all other buttons. 767 768 def doit() -> None: 769 uiscale = 0.9 * scale 770 txt = bui.textwidget( 771 parent=self._root_widget, 772 position=(pos[0] + 0.0 * scale, pos[1] - 58.0 * scale), 773 color=(1, 1, 1, 0.3), 774 size=(0, 0), 775 h_align='center', 776 v_align='center', 777 scale=uiscale, 778 text=self.get_control_value_name(button), 779 maxwidth=maxwidth, 780 ) 781 self._textwidgets[button] = txt 782 bui.buttonwidget( 783 edit=btn, 784 on_activate_call=bui.Call( 785 AwaitGamepadInputWindow, 786 self._input, 787 button, 788 self._gamepad_event, 789 message, 790 message2, 791 ), 792 ) 793 794 bui.pushcall(doit) 795 return btn 796 797 def _cancel(self) -> None: 798 # from bauiv1lib.settings.controls import ControlsSettingsWindow 799 800 # no-op if our underlying widget is dead or on its way out. 801 if not self._root_widget or self._root_widget.transitioning_out: 802 return 803 804 bui.containerwidget( 805 edit=self._root_widget, transition=self._transition_out 806 ) 807 if self._is_main_menu: 808 self.main_window_back() 809 # assert bui.app.classic is not None 810 # bui.app.ui_v1.set_main_window( 811 # ControlsSettingsWindow(transition='in_left'), 812 # from_window=self, 813 # is_back=True, 814 # ) 815 816 def _reset(self) -> None: 817 from bauiv1lib.confirm import ConfirmWindow 818 819 assert bui.app.classic is not None 820 821 # efro note: I think it's ok to reset without a confirm here 822 # because the user can see pretty clearly what changes and can 823 # cancel out of the settings window without saving if they want. 824 if bool(False): 825 ConfirmWindow( 826 # TODO: Implement a translation string for this! 827 'Are you sure you want to reset your button mapping?\n' 828 'This will also reset your advanced mappings\n' 829 'and secondary controller button mappings.', 830 self._do_reset, 831 width=490, 832 height=150, 833 ) 834 else: 835 self._do_reset() 836 837 def _do_reset(self) -> None: 838 """Resets the input's mapping settings.""" 839 from babase import InputDeviceNotFoundError 840 841 self._settings = {} 842 # Unplugging the controller while performing a 843 # mapping reset makes things go bonkers a little. 844 try: 845 self._get_config_mapping(default=True) 846 except InputDeviceNotFoundError: 847 pass 848 849 self._rebuild_ui(is_reset=True) 850 bui.getsound('gunCocking').play() 851 852 def _do_more(self) -> None: 853 """Show a burger menu with extra settings.""" 854 # pylint: disable=cyclic-import 855 choices: list[str] = [ 856 'advanced', 857 'reset', 858 ] 859 choices_display: list[bui.Lstr] = [ 860 bui.Lstr(resource=f'{self._r}.advancedText'), 861 bui.Lstr(resource='settingsWindowAdvanced.resetText'), 862 ] 863 864 uiscale = bui.app.ui_v1.uiscale 865 PopupMenuWindow( 866 position=self._more_button.get_screen_space_center(), 867 scale=( 868 2.3 869 if uiscale is bui.UIScale.SMALL 870 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 871 ), 872 width=150, 873 choices=choices, 874 choices_display=choices_display, 875 current_choice='advanced', 876 delegate=self, 877 ) 878 879 def popup_menu_selected_choice( 880 self, popup_window: PopupMenuWindow, choice: str 881 ) -> None: 882 """Called when a choice is selected in the popup.""" 883 del popup_window # unused 884 if choice == 'reset': 885 self._reset() 886 elif choice == 'advanced': 887 self._do_advanced() 888 else: 889 print(f'invalid choice: {choice}') 890 891 def popup_menu_closing(self, popup_window: PopupWindow) -> None: 892 """Called when the popup is closing.""" 893 894 def _save(self) -> None: 895 classic = bui.app.classic 896 assert classic is not None 897 898 # no-op if our underlying widget is dead or on its way out. 899 if not self._root_widget or self._root_widget.transitioning_out: 900 return 901 902 bui.containerwidget( 903 edit=self._root_widget, transition=self._transition_out 904 ) 905 906 # If we're a secondary editor we just go away (we were editing our 907 # parent's settings dict). 908 if self._is_secondary: 909 return 910 911 assert self._settings is not None 912 if self._input: 913 dst = classic.get_input_device_config(self._input, default=True) 914 dst2: dict[str, Any] = dst[0][dst[1]] 915 dst2.clear() 916 917 # Store any values that aren't -1. 918 for key, val in list(self._settings.items()): 919 if val != -1: 920 dst2[key] = val 921 922 # If we're allowed to phone home, send this config so we can 923 # generate more defaults in the future. 924 inputhash = classic.get_input_device_map_hash(self._input) 925 classic.master_server_v1_post( 926 'controllerConfig', 927 { 928 'ua': classic.legacy_user_agent_string, 929 'b': bui.app.env.engine_build_number, 930 'name': self._name, 931 'inputMapHash': inputhash, 932 'config': dst2, 933 'v': 2, 934 }, 935 ) 936 bui.app.config.apply_and_commit() 937 bui.getsound('gunCocking').play() 938 else: 939 bui.getsound('error').play() 940 941 if self._is_main_menu: 942 from bauiv1lib.settings.controls import ControlsSettingsWindow 943 944 assert bui.app.classic is not None 945 bui.app.ui_v1.set_main_window( 946 ControlsSettingsWindow(transition='in_left'), 947 from_window=self, 948 is_back=True, 949 ) 950 951 952class AwaitGamepadInputWindow(bui.Window): 953 """Window for capturing a gamepad button press.""" 954 955 def __init__( 956 self, 957 gamepad: bs.InputDevice, 958 button: str, 959 callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any], 960 message: bui.Lstr | None = None, 961 message2: bui.Lstr | None = None, 962 ): 963 if message is None: 964 print('AwaitGamepadInputWindow message is None!') 965 # Shouldn't get here. 966 message = bui.Lstr(value='Press any button...') 967 self._callback = callback 968 self._input = gamepad 969 self._capture_button = button 970 width = 400 971 height = 150 972 assert bui.app.classic is not None 973 uiscale = bui.app.ui_v1.uiscale 974 super().__init__( 975 root_widget=bui.containerwidget( 976 scale=( 977 2.0 978 if uiscale is bui.UIScale.SMALL 979 else 1.9 if uiscale is bui.UIScale.MEDIUM else 1.0 980 ), 981 size=(width, height), 982 transition='in_scale', 983 ), 984 ) 985 bui.textwidget( 986 parent=self._root_widget, 987 position=(0, (height - 60) if message2 is None else (height - 50)), 988 size=(width, 25), 989 text=message, 990 maxwidth=width * 0.9, 991 h_align='center', 992 v_align='center', 993 ) 994 if message2 is not None: 995 bui.textwidget( 996 parent=self._root_widget, 997 position=(width * 0.5, height - 60), 998 size=(0, 0), 999 text=message2, 1000 maxwidth=width * 0.9, 1001 scale=0.47, 1002 color=(0.7, 1.0, 0.7, 0.6), 1003 h_align='center', 1004 v_align='center', 1005 ) 1006 self._counter = 5 1007 self._count_down_text = bui.textwidget( 1008 parent=self._root_widget, 1009 h_align='center', 1010 position=(0, height - 110), 1011 size=(width, 25), 1012 color=(1, 1, 1, 0.3), 1013 text=str(self._counter), 1014 ) 1015 self._decrement_timer: bui.AppTimer | None = bui.AppTimer( 1016 1.0, bui.Call(self._decrement), repeat=True 1017 ) 1018 bs.capture_gamepad_input(bui.WeakCall(self._event_callback)) 1019 1020 def __del__(self) -> None: 1021 pass 1022 1023 def die(self) -> None: 1024 """Kill the window.""" 1025 1026 # This strong-refs us; killing it allow us to die now. 1027 self._decrement_timer = None 1028 bs.release_gamepad_input() 1029 if self._root_widget: 1030 bui.containerwidget(edit=self._root_widget, transition='out_scale') 1031 1032 def _event_callback(self, event: dict[str, Any]) -> None: 1033 input_device = event['input_device'] 1034 assert isinstance(input_device, bs.InputDevice) 1035 1036 # Update - we now allow *any* input device of this type. 1037 if ( 1038 self._input 1039 and input_device 1040 and input_device.name == self._input.name 1041 ): 1042 self._callback(self._capture_button, event, self) 1043 1044 def _decrement(self) -> None: 1045 self._counter -= 1 1046 if self._counter >= 1: 1047 if self._count_down_text: 1048 bui.textwidget( 1049 edit=self._count_down_text, text=str(self._counter) 1050 ) 1051 else: 1052 bui.getsound('error').play() 1053 self.die()
class
GamepadSettingsWindow(bauiv1._uitypes.MainWindow):
21class GamepadSettingsWindow(bui.MainWindow): 22 """Window for configuring a gamepad.""" 23 24 # pylint: disable=too-many-public-methods 25 26 def __init__( 27 self, 28 gamepad: bs.InputDevice, 29 is_main_menu: bool = True, 30 transition: str = 'in_right', 31 transition_out: str = 'out_right', 32 origin_widget: bui.Widget | None = None, 33 settings: dict | None = None, 34 ): 35 self._input = gamepad 36 37 # If our input-device went away, just return an empty zombie. 38 if not self._input: 39 return 40 41 self._name = self._input.name 42 43 self._r = 'configGamepadWindow' 44 self._transition_out = transition_out 45 46 # We're a secondary gamepad if supplied with settings. 47 self._is_secondary = settings is not None 48 self._ext = '_B' if self._is_secondary else '' 49 self._is_main_menu = is_main_menu 50 self._displayname = self._name 51 self._width = 700 if self._is_secondary else 730 52 self._height = 440 if self._is_secondary else 450 53 self._spacing = 40 54 assert bui.app.classic is not None 55 uiscale = bui.app.ui_v1.uiscale 56 super().__init__( 57 root_widget=bui.containerwidget( 58 size=(self._width, self._height), 59 scale=( 60 1.63 61 if uiscale is bui.UIScale.SMALL 62 else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 63 ), 64 stack_offset=( 65 (-20, -16) if uiscale is bui.UIScale.SMALL else (0, 0) 66 ), 67 transition=transition, 68 ), 69 transition=transition, 70 origin_widget=origin_widget, 71 ) 72 73 self._settings: dict[str, int] = {} 74 if not self._is_secondary: 75 self._get_config_mapping() 76 # Don't ask to config joysticks while we're in here. 77 self._rebuild_ui() 78 79 def _get_config_mapping(self, default: bool = False) -> None: 80 for button in [ 81 'buttonJump', 82 'buttonJump_B', 83 'buttonPunch', 84 'buttonPunch_B', 85 'buttonBomb', 86 'buttonBomb_B', 87 'buttonPickUp', 88 'buttonPickUp_B', 89 'buttonStart', 90 'buttonStart_B', 91 'buttonStart2', 92 'buttonStart2_B', 93 'buttonUp', 94 'buttonUp_B', 95 'buttonDown', 96 'buttonDown_B', 97 'buttonLeft', 98 'buttonLeft_B', 99 'buttonRight', 100 'buttonRight_B', 101 'buttonRun1', 102 'buttonRun1_B', 103 'buttonRun2', 104 'buttonRun2_B', 105 'triggerRun1', 106 'triggerRun1_B', 107 'triggerRun2', 108 'triggerRun2_B', 109 'buttonIgnored', 110 'buttonIgnored_B', 111 'buttonIgnored2', 112 'buttonIgnored2_B', 113 'buttonIgnored3', 114 'buttonIgnored3_B', 115 'buttonIgnored4', 116 'buttonIgnored4_B', 117 'buttonVRReorient', 118 'buttonVRReorient_B', 119 'analogStickDeadZone', 120 'analogStickDeadZone_B', 121 'dpad', 122 'dpad_B', 123 'unassignedButtonsRun', 124 'unassignedButtonsRun_B', 125 'startButtonActivatesDefaultWidget', 126 'startButtonActivatesDefaultWidget_B', 127 'uiOnly', 128 'uiOnly_B', 129 'ignoreCompletely', 130 'ignoreCompletely_B', 131 'autoRecalibrateAnalogStick', 132 'autoRecalibrateAnalogStick_B', 133 'analogStickLR', 134 'analogStickLR_B', 135 'analogStickUD', 136 'analogStickUD_B', 137 'enableSecondary', 138 ]: 139 assert bui.app.classic is not None 140 val = bui.app.classic.get_input_device_mapped_value( 141 self._input, button, default 142 ) 143 if val != -1: 144 self._settings[button] = val 145 146 def _rebuild_ui(self, is_reset: bool = False) -> None: 147 # pylint: disable=too-many-statements 148 149 assert bui.app.classic is not None 150 151 # Clear existing UI. 152 for widget in self._root_widget.get_children(): 153 widget.delete() 154 155 self._textwidgets: dict[str, bui.Widget] = {} 156 157 back_button: bui.Widget | None 158 159 if self._is_secondary: 160 back_button = bui.buttonwidget( 161 parent=self._root_widget, 162 position=(self._width - 180, self._height - 65), 163 autoselect=True, 164 size=(160, 60), 165 label=bui.Lstr(resource='doneText'), 166 scale=0.9, 167 on_activate_call=self._save, 168 ) 169 bui.containerwidget( 170 edit=self._root_widget, 171 start_button=back_button, 172 on_cancel_call=back_button.activate, 173 ) 174 cancel_button = None 175 else: 176 cancel_button = bui.buttonwidget( 177 parent=self._root_widget, 178 position=(51, self._height - 65), 179 autoselect=True, 180 size=(160, 60), 181 label=bui.Lstr(resource='cancelText'), 182 scale=0.9, 183 on_activate_call=self._cancel, 184 ) 185 bui.containerwidget( 186 edit=self._root_widget, cancel_button=cancel_button 187 ) 188 189 save_button: bui.Widget | None 190 if not self._is_secondary: 191 save_button = bui.buttonwidget( 192 parent=self._root_widget, 193 position=(self._width - 195, self._height - 65), 194 size=(180, 60), 195 autoselect=True, 196 label=bui.Lstr(resource='saveText'), 197 scale=0.9, 198 on_activate_call=self._save, 199 ) 200 bui.containerwidget( 201 edit=self._root_widget, start_button=save_button 202 ) 203 else: 204 save_button = None 205 206 if not self._is_secondary: 207 v = self._height - 59 208 bui.textwidget( 209 parent=self._root_widget, 210 position=(0, v + 5), 211 size=(self._width, 25), 212 text=bui.Lstr(resource=f'{self._r}.titleText'), 213 color=bui.app.ui_v1.title_color, 214 maxwidth=310, 215 h_align='center', 216 v_align='center', 217 ) 218 v -= 48 219 220 bui.textwidget( 221 parent=self._root_widget, 222 position=(0, v + 3), 223 size=(self._width, 25), 224 text=self._name, 225 color=bui.app.ui_v1.infotextcolor, 226 maxwidth=self._width * 0.9, 227 h_align='center', 228 v_align='center', 229 ) 230 v -= self._spacing * 1 231 232 bui.textwidget( 233 parent=self._root_widget, 234 position=(50, v + 10), 235 size=(self._width - 100, 30), 236 text=bui.Lstr(resource=f'{self._r}.appliesToAllText'), 237 maxwidth=330, 238 scale=0.65, 239 color=(0.5, 0.6, 0.5, 1.0), 240 h_align='center', 241 v_align='center', 242 ) 243 v -= 70 244 self._enable_check_box = None 245 else: 246 v = self._height - 49 247 bui.textwidget( 248 parent=self._root_widget, 249 position=(0, v + 5), 250 size=(self._width, 25), 251 text=bui.Lstr(resource=f'{self._r}.secondaryText'), 252 color=bui.app.ui_v1.title_color, 253 maxwidth=300, 254 h_align='center', 255 v_align='center', 256 ) 257 v -= self._spacing * 1 258 259 bui.textwidget( 260 parent=self._root_widget, 261 position=(50, v + 10), 262 size=(self._width - 100, 30), 263 text=bui.Lstr(resource=f'{self._r}.secondHalfText'), 264 maxwidth=300, 265 scale=0.65, 266 color=(0.6, 0.8, 0.6, 1.0), 267 h_align='center', 268 ) 269 self._enable_check_box = bui.checkboxwidget( 270 parent=self._root_widget, 271 position=(self._width * 0.5 - 80, v - 73), 272 value=self.get_enable_secondary_value(), 273 autoselect=True, 274 on_value_change_call=self._enable_check_box_changed, 275 size=(200, 30), 276 text=bui.Lstr(resource=f'{self._r}.secondaryEnableText'), 277 scale=1.2, 278 ) 279 v = self._height - 205 280 281 h_offs = 160 282 dist = 70 283 d_color = (0.4, 0.4, 0.8) 284 sclx = 1.2 285 scly = 0.98 286 dpm = bui.Lstr(resource=f'{self._r}.pressAnyButtonOrDpadText') 287 dpm2 = bui.Lstr(resource=f'{self._r}.ifNothingHappensTryAnalogText') 288 self._capture_button( 289 pos=(h_offs, v + scly * dist), 290 color=d_color, 291 button='buttonUp' + self._ext, 292 texture=bui.gettexture('upButton'), 293 scale=1.0, 294 message=dpm, 295 message2=dpm2, 296 ) 297 self._capture_button( 298 pos=(h_offs - sclx * dist, v), 299 color=d_color, 300 button='buttonLeft' + self._ext, 301 texture=bui.gettexture('leftButton'), 302 scale=1.0, 303 message=dpm, 304 message2=dpm2, 305 ) 306 self._capture_button( 307 pos=(h_offs + sclx * dist, v), 308 color=d_color, 309 button='buttonRight' + self._ext, 310 texture=bui.gettexture('rightButton'), 311 scale=1.0, 312 message=dpm, 313 message2=dpm2, 314 ) 315 self._capture_button( 316 pos=(h_offs, v - scly * dist), 317 color=d_color, 318 button='buttonDown' + self._ext, 319 texture=bui.gettexture('downButton'), 320 scale=1.0, 321 message=dpm, 322 message2=dpm2, 323 ) 324 325 dpm3 = bui.Lstr(resource=f'{self._r}.ifNothingHappensTryDpadText') 326 self._capture_button( 327 pos=(h_offs + 130, v - 125), 328 color=(0.4, 0.4, 0.6), 329 button='analogStickLR' + self._ext, 330 maxwidth=140, 331 texture=bui.gettexture('analogStick'), 332 scale=1.2, 333 message=bui.Lstr(resource=f'{self._r}.pressLeftRightText'), 334 message2=dpm3, 335 ) 336 337 self._capture_button( 338 pos=(self._width * 0.5, v), 339 color=(0.4, 0.4, 0.6), 340 button='buttonStart' + self._ext, 341 texture=bui.gettexture('startButton'), 342 scale=0.7, 343 ) 344 345 h_offs = self._width - 160 346 347 self._capture_button( 348 pos=(h_offs, v + scly * dist), 349 color=(0.6, 0.4, 0.8), 350 button='buttonPickUp' + self._ext, 351 texture=bui.gettexture('buttonPickUp'), 352 scale=1.0, 353 ) 354 self._capture_button( 355 pos=(h_offs - sclx * dist, v), 356 color=(0.7, 0.5, 0.1), 357 button='buttonPunch' + self._ext, 358 texture=bui.gettexture('buttonPunch'), 359 scale=1.0, 360 ) 361 self._capture_button( 362 pos=(h_offs + sclx * dist, v), 363 color=(0.5, 0.2, 0.1), 364 button='buttonBomb' + self._ext, 365 texture=bui.gettexture('buttonBomb'), 366 scale=1.0, 367 ) 368 self._capture_button( 369 pos=(h_offs, v - scly * dist), 370 color=(0.2, 0.5, 0.2), 371 button='buttonJump' + self._ext, 372 texture=bui.gettexture('buttonJump'), 373 scale=1.0, 374 ) 375 376 self._more_button = bui.buttonwidget( 377 parent=self._root_widget, 378 autoselect=True, 379 label='...', 380 text_scale=0.9, 381 color=(0.45, 0.4, 0.5), 382 textcolor=(0.65, 0.6, 0.7), 383 position=(self._width - 300, 30), 384 size=(130, 40), 385 on_activate_call=self._do_more, 386 ) 387 388 try: 389 if cancel_button is not None and save_button is not None: 390 bui.widget(edit=cancel_button, right_widget=save_button) 391 bui.widget(edit=save_button, left_widget=cancel_button) 392 if is_reset: 393 bui.containerwidget( 394 edit=self._root_widget, 395 selected_child=self._more_button, 396 ) 397 except Exception: 398 logging.exception('Error wiring up gamepad config window.') 399 400 def get_r(self) -> str: 401 """(internal)""" 402 return self._r 403 404 def get_advanced_button(self) -> bui.Widget: 405 """(internal)""" 406 return self._more_button 407 408 def get_is_secondary(self) -> bool: 409 """(internal)""" 410 return self._is_secondary 411 412 def get_settings(self) -> dict[str, Any]: 413 """(internal)""" 414 assert self._settings is not None 415 return self._settings 416 417 def get_ext(self) -> str: 418 """(internal)""" 419 return self._ext 420 421 def get_input(self) -> bs.InputDevice: 422 """(internal)""" 423 return self._input 424 425 def _do_advanced(self) -> None: 426 # pylint: disable=cyclic-import 427 from bauiv1lib.settings import gamepadadvanced 428 429 gamepadadvanced.GamepadAdvancedSettingsWindow(self) 430 431 def _enable_check_box_changed(self, value: bool) -> None: 432 assert self._settings is not None 433 if value: 434 self._settings['enableSecondary'] = 1 435 else: 436 # Just clear since this is default. 437 if 'enableSecondary' in self._settings: 438 del self._settings['enableSecondary'] 439 440 def get_unassigned_buttons_run_value(self) -> bool: 441 """(internal)""" 442 assert self._settings is not None 443 val = self._settings.get('unassignedButtonsRun', True) 444 assert isinstance(val, bool) 445 return val 446 447 def set_unassigned_buttons_run_value(self, value: bool) -> None: 448 """(internal)""" 449 assert self._settings is not None 450 if value: 451 if 'unassignedButtonsRun' in self._settings: 452 # Clear since this is default. 453 del self._settings['unassignedButtonsRun'] 454 return 455 self._settings['unassignedButtonsRun'] = False 456 457 def get_start_button_activates_default_widget_value(self) -> bool: 458 """(internal)""" 459 assert self._settings is not None 460 val = self._settings.get('startButtonActivatesDefaultWidget', True) 461 assert isinstance(val, bool) 462 return val 463 464 def set_start_button_activates_default_widget_value( 465 self, value: bool 466 ) -> None: 467 """(internal)""" 468 assert self._settings is not None 469 if value: 470 if 'startButtonActivatesDefaultWidget' in self._settings: 471 # Clear since this is default. 472 del self._settings['startButtonActivatesDefaultWidget'] 473 return 474 self._settings['startButtonActivatesDefaultWidget'] = False 475 476 def get_ui_only_value(self) -> bool: 477 """(internal)""" 478 assert self._settings is not None 479 val = self._settings.get('uiOnly', False) 480 assert isinstance(val, bool) 481 return val 482 483 def set_ui_only_value(self, value: bool) -> None: 484 """(internal)""" 485 assert self._settings is not None 486 if not value: 487 if 'uiOnly' in self._settings: 488 # Clear since this is default. 489 del self._settings['uiOnly'] 490 return 491 self._settings['uiOnly'] = True 492 493 def get_ignore_completely_value(self) -> bool: 494 """(internal)""" 495 assert self._settings is not None 496 val = self._settings.get('ignoreCompletely', False) 497 assert isinstance(val, bool) 498 return val 499 500 def set_ignore_completely_value(self, value: bool) -> None: 501 """(internal)""" 502 assert self._settings is not None 503 if not value: 504 if 'ignoreCompletely' in self._settings: 505 # Clear since this is default. 506 del self._settings['ignoreCompletely'] 507 return 508 self._settings['ignoreCompletely'] = True 509 510 def get_auto_recalibrate_analog_stick_value(self) -> bool: 511 """(internal)""" 512 assert self._settings is not None 513 val = self._settings.get('autoRecalibrateAnalogStick', False) 514 assert isinstance(val, bool) 515 return val 516 517 def set_auto_recalibrate_analog_stick_value(self, value: bool) -> None: 518 """(internal)""" 519 assert self._settings is not None 520 if not value: 521 if 'autoRecalibrateAnalogStick' in self._settings: 522 # Clear since this is default. 523 del self._settings['autoRecalibrateAnalogStick'] 524 else: 525 self._settings['autoRecalibrateAnalogStick'] = True 526 527 def get_enable_secondary_value(self) -> bool: 528 """(internal)""" 529 assert self._settings is not None 530 if not self._is_secondary: 531 raise RuntimeError('Enable value only applies to secondary editor.') 532 val = self._settings.get('enableSecondary', False) 533 assert isinstance(val, bool) 534 return val 535 536 def show_secondary_editor(self) -> None: 537 """(internal)""" 538 GamepadSettingsWindow( 539 self._input, 540 is_main_menu=False, 541 settings=self._settings, 542 transition='in_scale', 543 transition_out='out_scale', 544 ) 545 546 def get_control_value_name(self, control: str) -> str | bui.Lstr: 547 """(internal)""" 548 # pylint: disable=too-many-return-statements 549 assert self._settings is not None 550 if control == 'analogStickLR' + self._ext: 551 # This actually shows both LR and UD. 552 sval1 = ( 553 self._settings['analogStickLR' + self._ext] 554 if 'analogStickLR' + self._ext in self._settings 555 else 5 if self._is_secondary else None 556 ) 557 sval2 = ( 558 self._settings['analogStickUD' + self._ext] 559 if 'analogStickUD' + self._ext in self._settings 560 else 6 if self._is_secondary else None 561 ) 562 assert isinstance(sval1, (int, type(None))) 563 assert isinstance(sval2, (int, type(None))) 564 if sval1 is not None and sval2 is not None: 565 return ( 566 self._input.get_axis_name(sval1) 567 + ' / ' 568 + self._input.get_axis_name(sval2) 569 ) 570 return bui.Lstr(resource=f'{self._r}.unsetText') 571 572 # If they're looking for triggers. 573 if control in ['triggerRun1' + self._ext, 'triggerRun2' + self._ext]: 574 if control in self._settings: 575 return self._input.get_axis_name(self._settings[control]) 576 return bui.Lstr(resource=f'{self._r}.unsetText') 577 578 # Dead-zone. 579 if control == 'analogStickDeadZone' + self._ext: 580 if control in self._settings: 581 return str(self._settings[control]) 582 return str(1.0) 583 584 # For dpad buttons: show individual buttons if any are set. 585 # Otherwise show whichever dpad is set. 586 dpad_buttons = [ 587 'buttonLeft' + self._ext, 588 'buttonRight' + self._ext, 589 'buttonUp' + self._ext, 590 'buttonDown' + self._ext, 591 ] 592 if control in dpad_buttons: 593 # If *any* dpad buttons are assigned, show only button assignments. 594 if any(b in self._settings for b in dpad_buttons): 595 if control in self._settings: 596 return self._input.get_button_name(self._settings[control]) 597 return bui.Lstr(resource=f'{self._r}.unsetText') 598 599 # No dpad buttons - show the dpad number for all 4. 600 dpadnum = ( 601 self._settings['dpad' + self._ext] 602 if 'dpad' + self._ext in self._settings 603 else 2 if self._is_secondary else None 604 ) 605 assert isinstance(dpadnum, (int, type(None))) 606 if dpadnum is not None: 607 return bui.Lstr( 608 value='${A} ${B}', 609 subs=[ 610 ('${A}', bui.Lstr(resource=f'{self._r}.dpadText')), 611 ( 612 '${B}', 613 str(dpadnum), 614 ), 615 ], 616 ) 617 return bui.Lstr(resource=f'{self._r}.unsetText') 618 619 # Other buttons. 620 if control in self._settings: 621 return self._input.get_button_name(self._settings[control]) 622 return bui.Lstr(resource=f'{self._r}.unsetText') 623 624 def _gamepad_event( 625 self, 626 control: str, 627 event: dict[str, Any], 628 dialog: AwaitGamepadInputWindow, 629 ) -> None: 630 # pylint: disable=too-many-branches 631 assert self._settings is not None 632 ext = self._ext 633 634 # For our dpad-buttons we're looking for either a button-press or a 635 # hat-switch press. 636 if control in [ 637 'buttonUp' + ext, 638 'buttonLeft' + ext, 639 'buttonDown' + ext, 640 'buttonRight' + ext, 641 ]: 642 if event['type'] in ['BUTTONDOWN', 'HATMOTION']: 643 # If its a button-down. 644 if event['type'] == 'BUTTONDOWN': 645 value = event['button'] 646 self._settings[control] = value 647 648 # If its a dpad. 649 elif event['type'] == 'HATMOTION': 650 # clear out any set dir-buttons 651 for btn in [ 652 'buttonUp' + ext, 653 'buttonLeft' + ext, 654 'buttonRight' + ext, 655 'buttonDown' + ext, 656 ]: 657 if btn in self._settings: 658 del self._settings[btn] 659 if event['hat'] == (2 if self._is_secondary else 1): 660 self._settings['dpad' + ext] = event['hat'] 661 662 # Update the 4 dpad button txt widgets. 663 bui.textwidget( 664 edit=self._textwidgets['buttonUp' + ext], 665 text=self.get_control_value_name('buttonUp' + ext), 666 ) 667 bui.textwidget( 668 edit=self._textwidgets['buttonLeft' + ext], 669 text=self.get_control_value_name('buttonLeft' + ext), 670 ) 671 bui.textwidget( 672 edit=self._textwidgets['buttonRight' + ext], 673 text=self.get_control_value_name('buttonRight' + ext), 674 ) 675 bui.textwidget( 676 edit=self._textwidgets['buttonDown' + ext], 677 text=self.get_control_value_name('buttonDown' + ext), 678 ) 679 bui.getsound('gunCocking').play() 680 dialog.die() 681 682 elif control == 'analogStickLR' + ext: 683 if event['type'] == 'AXISMOTION': 684 # Ignore small values or else we might get triggered by noise. 685 if abs(event['value']) > 0.5: 686 axis = event['axis'] 687 if axis == (5 if self._is_secondary else 1): 688 self._settings['analogStickLR' + ext] = axis 689 bui.textwidget( 690 edit=self._textwidgets['analogStickLR' + ext], 691 text=self.get_control_value_name('analogStickLR' + ext), 692 ) 693 bui.getsound('gunCocking').play() 694 dialog.die() 695 696 # Now launch the up/down listener. 697 AwaitGamepadInputWindow( 698 self._input, 699 'analogStickUD' + ext, 700 self._gamepad_event, 701 bui.Lstr(resource=f'{self._r}.pressUpDownText'), 702 ) 703 704 elif control == 'analogStickUD' + ext: 705 if event['type'] == 'AXISMOTION': 706 # Ignore small values or else we might get triggered by noise. 707 if abs(event['value']) > 0.5: 708 axis = event['axis'] 709 710 # Ignore our LR axis. 711 if 'analogStickLR' + ext in self._settings: 712 lr_axis = self._settings['analogStickLR' + ext] 713 else: 714 lr_axis = 5 if self._is_secondary else 1 715 if axis != lr_axis: 716 if axis == (6 if self._is_secondary else 2): 717 self._settings['analogStickUD' + ext] = axis 718 bui.textwidget( 719 edit=self._textwidgets['analogStickLR' + ext], 720 text=self.get_control_value_name( 721 'analogStickLR' + ext 722 ), 723 ) 724 bui.getsound('gunCocking').play() 725 dialog.die() 726 else: 727 # For other buttons we just want a button-press. 728 if event['type'] == 'BUTTONDOWN': 729 value = event['button'] 730 self._settings[control] = value 731 732 # Update the button's text widget. 733 bui.textwidget( 734 edit=self._textwidgets[control], 735 text=self.get_control_value_name(control), 736 ) 737 bui.getsound('gunCocking').play() 738 dialog.die() 739 740 def _capture_button( 741 self, 742 pos: tuple[float, float], 743 color: tuple[float, float, float], 744 texture: bui.Texture, 745 button: str, 746 scale: float = 1.0, 747 message: bui.Lstr | None = None, 748 message2: bui.Lstr | None = None, 749 maxwidth: float = 80.0, 750 ) -> bui.Widget: 751 if message is None: 752 message = bui.Lstr(resource=f'{self._r}.pressAnyButtonText') 753 base_size = 79 754 btn = bui.buttonwidget( 755 parent=self._root_widget, 756 position=( 757 pos[0] - base_size * 0.5 * scale, 758 pos[1] - base_size * 0.5 * scale, 759 ), 760 autoselect=True, 761 size=(base_size * scale, base_size * scale), 762 texture=texture, 763 label='', 764 color=color, 765 ) 766 767 # Make this in a timer so that it shows up on top of all other buttons. 768 769 def doit() -> None: 770 uiscale = 0.9 * scale 771 txt = bui.textwidget( 772 parent=self._root_widget, 773 position=(pos[0] + 0.0 * scale, pos[1] - 58.0 * scale), 774 color=(1, 1, 1, 0.3), 775 size=(0, 0), 776 h_align='center', 777 v_align='center', 778 scale=uiscale, 779 text=self.get_control_value_name(button), 780 maxwidth=maxwidth, 781 ) 782 self._textwidgets[button] = txt 783 bui.buttonwidget( 784 edit=btn, 785 on_activate_call=bui.Call( 786 AwaitGamepadInputWindow, 787 self._input, 788 button, 789 self._gamepad_event, 790 message, 791 message2, 792 ), 793 ) 794 795 bui.pushcall(doit) 796 return btn 797 798 def _cancel(self) -> None: 799 # from bauiv1lib.settings.controls import ControlsSettingsWindow 800 801 # no-op if our underlying widget is dead or on its way out. 802 if not self._root_widget or self._root_widget.transitioning_out: 803 return 804 805 bui.containerwidget( 806 edit=self._root_widget, transition=self._transition_out 807 ) 808 if self._is_main_menu: 809 self.main_window_back() 810 # assert bui.app.classic is not None 811 # bui.app.ui_v1.set_main_window( 812 # ControlsSettingsWindow(transition='in_left'), 813 # from_window=self, 814 # is_back=True, 815 # ) 816 817 def _reset(self) -> None: 818 from bauiv1lib.confirm import ConfirmWindow 819 820 assert bui.app.classic is not None 821 822 # efro note: I think it's ok to reset without a confirm here 823 # because the user can see pretty clearly what changes and can 824 # cancel out of the settings window without saving if they want. 825 if bool(False): 826 ConfirmWindow( 827 # TODO: Implement a translation string for this! 828 'Are you sure you want to reset your button mapping?\n' 829 'This will also reset your advanced mappings\n' 830 'and secondary controller button mappings.', 831 self._do_reset, 832 width=490, 833 height=150, 834 ) 835 else: 836 self._do_reset() 837 838 def _do_reset(self) -> None: 839 """Resets the input's mapping settings.""" 840 from babase import InputDeviceNotFoundError 841 842 self._settings = {} 843 # Unplugging the controller while performing a 844 # mapping reset makes things go bonkers a little. 845 try: 846 self._get_config_mapping(default=True) 847 except InputDeviceNotFoundError: 848 pass 849 850 self._rebuild_ui(is_reset=True) 851 bui.getsound('gunCocking').play() 852 853 def _do_more(self) -> None: 854 """Show a burger menu with extra settings.""" 855 # pylint: disable=cyclic-import 856 choices: list[str] = [ 857 'advanced', 858 'reset', 859 ] 860 choices_display: list[bui.Lstr] = [ 861 bui.Lstr(resource=f'{self._r}.advancedText'), 862 bui.Lstr(resource='settingsWindowAdvanced.resetText'), 863 ] 864 865 uiscale = bui.app.ui_v1.uiscale 866 PopupMenuWindow( 867 position=self._more_button.get_screen_space_center(), 868 scale=( 869 2.3 870 if uiscale is bui.UIScale.SMALL 871 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 872 ), 873 width=150, 874 choices=choices, 875 choices_display=choices_display, 876 current_choice='advanced', 877 delegate=self, 878 ) 879 880 def popup_menu_selected_choice( 881 self, popup_window: PopupMenuWindow, choice: str 882 ) -> None: 883 """Called when a choice is selected in the popup.""" 884 del popup_window # unused 885 if choice == 'reset': 886 self._reset() 887 elif choice == 'advanced': 888 self._do_advanced() 889 else: 890 print(f'invalid choice: {choice}') 891 892 def popup_menu_closing(self, popup_window: PopupWindow) -> None: 893 """Called when the popup is closing.""" 894 895 def _save(self) -> None: 896 classic = bui.app.classic 897 assert classic is not None 898 899 # no-op if our underlying widget is dead or on its way out. 900 if not self._root_widget or self._root_widget.transitioning_out: 901 return 902 903 bui.containerwidget( 904 edit=self._root_widget, transition=self._transition_out 905 ) 906 907 # If we're a secondary editor we just go away (we were editing our 908 # parent's settings dict). 909 if self._is_secondary: 910 return 911 912 assert self._settings is not None 913 if self._input: 914 dst = classic.get_input_device_config(self._input, default=True) 915 dst2: dict[str, Any] = dst[0][dst[1]] 916 dst2.clear() 917 918 # Store any values that aren't -1. 919 for key, val in list(self._settings.items()): 920 if val != -1: 921 dst2[key] = val 922 923 # If we're allowed to phone home, send this config so we can 924 # generate more defaults in the future. 925 inputhash = classic.get_input_device_map_hash(self._input) 926 classic.master_server_v1_post( 927 'controllerConfig', 928 { 929 'ua': classic.legacy_user_agent_string, 930 'b': bui.app.env.engine_build_number, 931 'name': self._name, 932 'inputMapHash': inputhash, 933 'config': dst2, 934 'v': 2, 935 }, 936 ) 937 bui.app.config.apply_and_commit() 938 bui.getsound('gunCocking').play() 939 else: 940 bui.getsound('error').play() 941 942 if self._is_main_menu: 943 from bauiv1lib.settings.controls import ControlsSettingsWindow 944 945 assert bui.app.classic is not None 946 bui.app.ui_v1.set_main_window( 947 ControlsSettingsWindow(transition='in_left'), 948 from_window=self, 949 is_back=True, 950 )
Window for configuring a gamepad.
GamepadSettingsWindow( gamepad: _bascenev1.InputDevice, is_main_menu: bool = True, transition: str = 'in_right', transition_out: str = 'out_right', origin_widget: _bauiv1.Widget | None = None, settings: dict | None = None)
26 def __init__( 27 self, 28 gamepad: bs.InputDevice, 29 is_main_menu: bool = True, 30 transition: str = 'in_right', 31 transition_out: str = 'out_right', 32 origin_widget: bui.Widget | None = None, 33 settings: dict | None = None, 34 ): 35 self._input = gamepad 36 37 # If our input-device went away, just return an empty zombie. 38 if not self._input: 39 return 40 41 self._name = self._input.name 42 43 self._r = 'configGamepadWindow' 44 self._transition_out = transition_out 45 46 # We're a secondary gamepad if supplied with settings. 47 self._is_secondary = settings is not None 48 self._ext = '_B' if self._is_secondary else '' 49 self._is_main_menu = is_main_menu 50 self._displayname = self._name 51 self._width = 700 if self._is_secondary else 730 52 self._height = 440 if self._is_secondary else 450 53 self._spacing = 40 54 assert bui.app.classic is not None 55 uiscale = bui.app.ui_v1.uiscale 56 super().__init__( 57 root_widget=bui.containerwidget( 58 size=(self._width, self._height), 59 scale=( 60 1.63 61 if uiscale is bui.UIScale.SMALL 62 else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0 63 ), 64 stack_offset=( 65 (-20, -16) if uiscale is bui.UIScale.SMALL else (0, 0) 66 ), 67 transition=transition, 68 ), 69 transition=transition, 70 origin_widget=origin_widget, 71 ) 72 73 self._settings: dict[str, int] = {} 74 if not self._is_secondary: 75 self._get_config_mapping() 76 # Don't ask to config joysticks while we're in here. 77 self._rebuild_ui()
Create a MainWindow given a root widget and transition info.
Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.
Inherited Members
- bauiv1._uitypes.MainWindow
- main_window_back_state
- main_window_close
- can_change_main_window
- main_window_back
- main_window_replace
- on_main_window_close
- get_main_window_state
- bauiv1._uitypes.Window
- get_root_widget
class
AwaitGamepadInputWindow(bauiv1._uitypes.Window):
953class AwaitGamepadInputWindow(bui.Window): 954 """Window for capturing a gamepad button press.""" 955 956 def __init__( 957 self, 958 gamepad: bs.InputDevice, 959 button: str, 960 callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any], 961 message: bui.Lstr | None = None, 962 message2: bui.Lstr | None = None, 963 ): 964 if message is None: 965 print('AwaitGamepadInputWindow message is None!') 966 # Shouldn't get here. 967 message = bui.Lstr(value='Press any button...') 968 self._callback = callback 969 self._input = gamepad 970 self._capture_button = button 971 width = 400 972 height = 150 973 assert bui.app.classic is not None 974 uiscale = bui.app.ui_v1.uiscale 975 super().__init__( 976 root_widget=bui.containerwidget( 977 scale=( 978 2.0 979 if uiscale is bui.UIScale.SMALL 980 else 1.9 if uiscale is bui.UIScale.MEDIUM else 1.0 981 ), 982 size=(width, height), 983 transition='in_scale', 984 ), 985 ) 986 bui.textwidget( 987 parent=self._root_widget, 988 position=(0, (height - 60) if message2 is None else (height - 50)), 989 size=(width, 25), 990 text=message, 991 maxwidth=width * 0.9, 992 h_align='center', 993 v_align='center', 994 ) 995 if message2 is not None: 996 bui.textwidget( 997 parent=self._root_widget, 998 position=(width * 0.5, height - 60), 999 size=(0, 0), 1000 text=message2, 1001 maxwidth=width * 0.9, 1002 scale=0.47, 1003 color=(0.7, 1.0, 0.7, 0.6), 1004 h_align='center', 1005 v_align='center', 1006 ) 1007 self._counter = 5 1008 self._count_down_text = bui.textwidget( 1009 parent=self._root_widget, 1010 h_align='center', 1011 position=(0, height - 110), 1012 size=(width, 25), 1013 color=(1, 1, 1, 0.3), 1014 text=str(self._counter), 1015 ) 1016 self._decrement_timer: bui.AppTimer | None = bui.AppTimer( 1017 1.0, bui.Call(self._decrement), repeat=True 1018 ) 1019 bs.capture_gamepad_input(bui.WeakCall(self._event_callback)) 1020 1021 def __del__(self) -> None: 1022 pass 1023 1024 def die(self) -> None: 1025 """Kill the window.""" 1026 1027 # This strong-refs us; killing it allow us to die now. 1028 self._decrement_timer = None 1029 bs.release_gamepad_input() 1030 if self._root_widget: 1031 bui.containerwidget(edit=self._root_widget, transition='out_scale') 1032 1033 def _event_callback(self, event: dict[str, Any]) -> None: 1034 input_device = event['input_device'] 1035 assert isinstance(input_device, bs.InputDevice) 1036 1037 # Update - we now allow *any* input device of this type. 1038 if ( 1039 self._input 1040 and input_device 1041 and input_device.name == self._input.name 1042 ): 1043 self._callback(self._capture_button, event, self) 1044 1045 def _decrement(self) -> None: 1046 self._counter -= 1 1047 if self._counter >= 1: 1048 if self._count_down_text: 1049 bui.textwidget( 1050 edit=self._count_down_text, text=str(self._counter) 1051 ) 1052 else: 1053 bui.getsound('error').play() 1054 self.die()
Window for capturing a gamepad button press.
AwaitGamepadInputWindow( gamepad: _bascenev1.InputDevice, button: str, callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any], message: babase.Lstr | None = None, message2: babase.Lstr | None = None)
956 def __init__( 957 self, 958 gamepad: bs.InputDevice, 959 button: str, 960 callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any], 961 message: bui.Lstr | None = None, 962 message2: bui.Lstr | None = None, 963 ): 964 if message is None: 965 print('AwaitGamepadInputWindow message is None!') 966 # Shouldn't get here. 967 message = bui.Lstr(value='Press any button...') 968 self._callback = callback 969 self._input = gamepad 970 self._capture_button = button 971 width = 400 972 height = 150 973 assert bui.app.classic is not None 974 uiscale = bui.app.ui_v1.uiscale 975 super().__init__( 976 root_widget=bui.containerwidget( 977 scale=( 978 2.0 979 if uiscale is bui.UIScale.SMALL 980 else 1.9 if uiscale is bui.UIScale.MEDIUM else 1.0 981 ), 982 size=(width, height), 983 transition='in_scale', 984 ), 985 ) 986 bui.textwidget( 987 parent=self._root_widget, 988 position=(0, (height - 60) if message2 is None else (height - 50)), 989 size=(width, 25), 990 text=message, 991 maxwidth=width * 0.9, 992 h_align='center', 993 v_align='center', 994 ) 995 if message2 is not None: 996 bui.textwidget( 997 parent=self._root_widget, 998 position=(width * 0.5, height - 60), 999 size=(0, 0), 1000 text=message2, 1001 maxwidth=width * 0.9, 1002 scale=0.47, 1003 color=(0.7, 1.0, 0.7, 0.6), 1004 h_align='center', 1005 v_align='center', 1006 ) 1007 self._counter = 5 1008 self._count_down_text = bui.textwidget( 1009 parent=self._root_widget, 1010 h_align='center', 1011 position=(0, height - 110), 1012 size=(width, 25), 1013 color=(1, 1, 1, 0.3), 1014 text=str(self._counter), 1015 ) 1016 self._decrement_timer: bui.AppTimer | None = bui.AppTimer( 1017 1.0, bui.Call(self._decrement), repeat=True 1018 ) 1019 bs.capture_gamepad_input(bui.WeakCall(self._event_callback))
def
die(self) -> None:
1024 def die(self) -> None: 1025 """Kill the window.""" 1026 1027 # This strong-refs us; killing it allow us to die now. 1028 self._decrement_timer = None 1029 bs.release_gamepad_input() 1030 if self._root_widget: 1031 bui.containerwidget(edit=self._root_widget, transition='out_scale')
Kill the window.
Inherited Members
- bauiv1._uitypes.Window
- get_root_widget