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