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