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