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