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