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