bauiv1lib.ingamemenu
Implements the in-gmae menu window.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Implements the in-gmae menu window.""" 4 5from __future__ import annotations 6 7from typing import TYPE_CHECKING, override 8import logging 9 10import bauiv1 as bui 11import bascenev1 as bs 12 13if TYPE_CHECKING: 14 from typing import Any, Callable 15 16 17class InGameMenuWindow(bui.MainWindow): 18 """The menu that can be invoked while in a game.""" 19 20 def __init__( 21 self, 22 transition: str | None = 'in_right', 23 origin_widget: bui.Widget | None = None, 24 ): 25 26 # Make a vanilla container; we'll modify it to our needs in 27 # refresh. 28 super().__init__( 29 root_widget=bui.containerwidget( 30 toolbar_visibility=('menu_in_game') 31 ), 32 transition=transition, 33 origin_widget=origin_widget, 34 ) 35 36 # Grab this stuff in case it changes. 37 self._is_demo = bui.app.env.demo 38 self._is_arcade = bui.app.env.arcade 39 40 self._p_index = 0 41 self._use_autoselect = True 42 self._button_width = 200.0 43 self._button_height = 45.0 44 self._width = 100.0 45 self._height = 100.0 46 47 self._refresh() 48 49 @override 50 def get_main_window_state(self) -> bui.MainWindowState: 51 # Support recreating our window for back/refresh purposes. 52 cls = type(self) 53 return bui.BasicMainWindowState( 54 create_call=lambda transition, origin_widget: cls( 55 transition=transition, origin_widget=origin_widget 56 ) 57 ) 58 59 def _refresh(self) -> None: 60 61 # Clear everything that was there. 62 children = self._root_widget.get_children() 63 for child in children: 64 child.delete() 65 66 self._r = 'mainMenu' 67 68 self._input_device = input_device = bs.get_ui_input_device() 69 70 # Are we connected to a local player? 71 self._input_player = input_device.player if input_device else None 72 73 # Are we connected to a remote player?. 74 self._connected_to_remote_player = ( 75 input_device.is_attached_to_player() 76 if (input_device and self._input_player is None) 77 else False 78 ) 79 80 positions: list[tuple[float, float, float]] = [] 81 self._p_index = 0 82 83 self._refresh_in_game(positions) 84 85 h, v, scale = positions[self._p_index] 86 self._p_index += 1 87 88 # If we're in a replay, we have a 'Leave Replay' button. 89 if bs.is_in_replay(): 90 bui.buttonwidget( 91 parent=self._root_widget, 92 position=(h - self._button_width * 0.5 * scale, v), 93 scale=scale, 94 size=(self._button_width, self._button_height), 95 autoselect=self._use_autoselect, 96 label=bui.Lstr(resource='replayEndText'), 97 on_activate_call=self._confirm_end_replay, 98 ) 99 elif bs.get_foreground_host_session() is not None: 100 bui.buttonwidget( 101 parent=self._root_widget, 102 position=(h - self._button_width * 0.5 * scale, v), 103 scale=scale, 104 size=(self._button_width, self._button_height), 105 autoselect=self._use_autoselect, 106 label=bui.Lstr( 107 resource=self._r 108 + ( 109 '.endTestText' 110 if self._is_benchmark() 111 else '.endGameText' 112 ) 113 ), 114 on_activate_call=( 115 self._confirm_end_test 116 if self._is_benchmark() 117 else self._confirm_end_game 118 ), 119 ) 120 else: 121 # Assume we're in a client-session. 122 bui.buttonwidget( 123 parent=self._root_widget, 124 position=(h - self._button_width * 0.5 * scale, v), 125 scale=scale, 126 size=(self._button_width, self._button_height), 127 autoselect=self._use_autoselect, 128 label=bui.Lstr(resource=f'{self._r}.leavePartyText'), 129 on_activate_call=self._confirm_leave_party, 130 ) 131 132 # Add speed-up/slow-down buttons for replays. Ideally this 133 # should be part of a fading-out playback bar like most media 134 # players but this works for now. 135 if bs.is_in_replay(): 136 b_size = 50.0 137 b_buffer_1 = 50.0 138 b_buffer_2 = 10.0 139 t_scale = 0.75 140 assert bui.app.classic is not None 141 uiscale = bui.app.ui_v1.uiscale 142 if uiscale is bui.UIScale.SMALL: 143 b_size *= 0.6 144 b_buffer_1 *= 0.8 145 b_buffer_2 *= 1.0 146 v_offs = -40 147 t_scale = 0.5 148 elif uiscale is bui.UIScale.MEDIUM: 149 v_offs = -70 150 else: 151 v_offs = -100 152 self._replay_speed_text = bui.textwidget( 153 parent=self._root_widget, 154 text=bui.Lstr( 155 resource='watchWindow.playbackSpeedText', 156 subs=[('${SPEED}', str(1.23))], 157 ), 158 position=(h, v + v_offs + 15 * t_scale), 159 h_align='center', 160 v_align='center', 161 size=(0, 0), 162 scale=t_scale, 163 ) 164 165 # Update to current value. 166 self._change_replay_speed(0) 167 168 # Keep updating in a timer in case it gets changed elsewhere. 169 self._change_replay_speed_timer = bui.AppTimer( 170 0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True 171 ) 172 btn = bui.buttonwidget( 173 parent=self._root_widget, 174 position=( 175 h - b_size - b_buffer_1, 176 v - b_size - b_buffer_2 + v_offs, 177 ), 178 button_type='square', 179 size=(b_size, b_size), 180 label='', 181 autoselect=True, 182 on_activate_call=bui.Call(self._change_replay_speed, -1), 183 ) 184 bui.textwidget( 185 parent=self._root_widget, 186 draw_controller=btn, 187 text='-', 188 position=( 189 h - b_size * 0.5 - b_buffer_1, 190 v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, 191 ), 192 h_align='center', 193 v_align='center', 194 size=(0, 0), 195 scale=3.0 * t_scale, 196 ) 197 btn = bui.buttonwidget( 198 parent=self._root_widget, 199 position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs), 200 button_type='square', 201 size=(b_size, b_size), 202 label='', 203 autoselect=True, 204 on_activate_call=bui.Call(self._change_replay_speed, 1), 205 ) 206 bui.textwidget( 207 parent=self._root_widget, 208 draw_controller=btn, 209 text='+', 210 position=( 211 h + b_size * 0.5 + b_buffer_1, 212 v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, 213 ), 214 h_align='center', 215 v_align='center', 216 size=(0, 0), 217 scale=3.0 * t_scale, 218 ) 219 self._pause_resume_button = btn = bui.buttonwidget( 220 parent=self._root_widget, 221 position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs), 222 button_type='square', 223 size=(b_size, b_size), 224 label=bui.charstr( 225 bui.SpecialChar.PLAY_BUTTON 226 if bs.is_replay_paused() 227 else bui.SpecialChar.PAUSE_BUTTON 228 ), 229 autoselect=True, 230 on_activate_call=bui.Call(self._pause_or_resume_replay), 231 ) 232 btn = bui.buttonwidget( 233 parent=self._root_widget, 234 position=( 235 h - b_size * 1.5 - b_buffer_1 * 2, 236 v - b_size - b_buffer_2 + v_offs, 237 ), 238 button_type='square', 239 size=(b_size, b_size), 240 label='', 241 autoselect=True, 242 on_activate_call=bui.WeakCall(self._rewind_replay), 243 ) 244 bui.textwidget( 245 parent=self._root_widget, 246 draw_controller=btn, 247 # text='<<', 248 text=bui.charstr(bui.SpecialChar.REWIND_BUTTON), 249 position=( 250 h - b_size - b_buffer_1 * 2, 251 v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, 252 ), 253 h_align='center', 254 v_align='center', 255 size=(0, 0), 256 scale=2.0 * t_scale, 257 ) 258 btn = bui.buttonwidget( 259 parent=self._root_widget, 260 position=( 261 h + b_size * 0.5 + b_buffer_1 * 2, 262 v - b_size - b_buffer_2 + v_offs, 263 ), 264 button_type='square', 265 size=(b_size, b_size), 266 label='', 267 autoselect=True, 268 on_activate_call=bui.WeakCall(self._forward_replay), 269 ) 270 bui.textwidget( 271 parent=self._root_widget, 272 draw_controller=btn, 273 # text='>>', 274 text=bui.charstr(bui.SpecialChar.FAST_FORWARD_BUTTON), 275 position=( 276 h + b_size + b_buffer_1 * 2, 277 v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, 278 ), 279 h_align='center', 280 v_align='center', 281 size=(0, 0), 282 scale=2.0 * t_scale, 283 ) 284 285 def _rewind_replay(self) -> None: 286 bs.seek_replay(-2 * pow(2, bs.get_replay_speed_exponent())) 287 288 def _forward_replay(self) -> None: 289 bs.seek_replay(2 * pow(2, bs.get_replay_speed_exponent())) 290 291 def _refresh_in_game( 292 self, positions: list[tuple[float, float, float]] 293 ) -> tuple[float, float, float]: 294 # pylint: disable=too-many-branches 295 # pylint: disable=too-many-locals 296 # pylint: disable=too-many-statements 297 assert bui.app.classic is not None 298 custom_menu_entries: list[dict[str, Any]] = [] 299 session = bs.get_foreground_host_session() 300 if session is not None: 301 try: 302 custom_menu_entries = session.get_custom_menu_entries() 303 for cme in custom_menu_entries: 304 cme_any: Any = cme # Type check may not hold true. 305 if ( 306 not isinstance(cme_any, dict) 307 or 'label' not in cme 308 or not isinstance(cme['label'], (str, bui.Lstr)) 309 or 'call' not in cme 310 or not callable(cme['call']) 311 ): 312 raise ValueError( 313 'invalid custom menu entry: ' + str(cme) 314 ) 315 except Exception: 316 custom_menu_entries = [] 317 logging.exception( 318 'Error getting custom menu entries for %s.', session 319 ) 320 self._width = 250.0 321 self._height = 250.0 if self._input_player else 180.0 322 if (self._is_demo or self._is_arcade) and self._input_player: 323 self._height -= 40 324 # if not self._have_settings_button: 325 self._height -= 50 326 if self._connected_to_remote_player: 327 # In this case we have a leave *and* a disconnect button. 328 self._height += 50 329 self._height += 50 * (len(custom_menu_entries)) 330 uiscale = bui.app.ui_v1.uiscale 331 bui.containerwidget( 332 edit=self._root_widget, 333 size=(self._width, self._height), 334 scale=( 335 2.15 336 if uiscale is bui.UIScale.SMALL 337 else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0 338 ), 339 ) 340 h = 125.0 341 v = self._height - 80.0 if self._input_player else self._height - 60 342 h_offset = 0 343 d_h_offset = 0 344 v_offset = -50 345 for _i in range(6 + len(custom_menu_entries)): 346 positions.append((h, v, 1.0)) 347 v += v_offset 348 h += h_offset 349 h_offset += d_h_offset 350 # self._play_button = None 351 bui.app.classic.pause() 352 353 # Player name if applicable. 354 if self._input_player: 355 player_name = self._input_player.getname() 356 h, v, scale = positions[self._p_index] 357 v += 35 358 bui.textwidget( 359 parent=self._root_widget, 360 position=(h - self._button_width / 2, v), 361 size=(self._button_width, self._button_height), 362 color=(1, 1, 1, 0.5), 363 scale=0.7, 364 h_align='center', 365 text=bui.Lstr(value=player_name), 366 ) 367 else: 368 player_name = '' 369 h, v, scale = positions[self._p_index] 370 self._p_index += 1 371 btn = bui.buttonwidget( 372 parent=self._root_widget, 373 position=(h - self._button_width / 2, v), 374 size=(self._button_width, self._button_height), 375 scale=scale, 376 label=bui.Lstr(resource=f'{self._r}.resumeText'), 377 autoselect=self._use_autoselect, 378 on_activate_call=self._resume, 379 ) 380 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 381 382 # Add any custom options defined by the current game. 383 for entry in custom_menu_entries: 384 h, v, scale = positions[self._p_index] 385 self._p_index += 1 386 387 # Ask the entry whether we should resume when we call 388 # it (defaults to true). 389 resume = bool(entry.get('resume_on_call', True)) 390 391 if resume: 392 call = bui.Call(self._resume_and_call, entry['call']) 393 else: 394 call = bui.Call(entry['call'], bui.WeakCall(self._resume)) 395 396 bui.buttonwidget( 397 parent=self._root_widget, 398 position=(h - self._button_width / 2, v), 399 size=(self._button_width, self._button_height), 400 scale=scale, 401 on_activate_call=call, 402 label=entry['label'], 403 autoselect=self._use_autoselect, 404 ) 405 406 # Add a 'leave' button if the menu-owner has a player. 407 if (self._input_player or self._connected_to_remote_player) and not ( 408 self._is_demo or self._is_arcade 409 ): 410 h, v, scale = positions[self._p_index] 411 self._p_index += 1 412 btn = bui.buttonwidget( 413 parent=self._root_widget, 414 position=(h - self._button_width / 2, v), 415 size=(self._button_width, self._button_height), 416 scale=scale, 417 on_activate_call=self._leave, 418 label='', 419 autoselect=self._use_autoselect, 420 ) 421 422 if ( 423 player_name != '' 424 and player_name[0] != '<' 425 and player_name[-1] != '>' 426 ): 427 txt = bui.Lstr( 428 resource=f'{self._r}.justPlayerText', 429 subs=[('${NAME}', player_name)], 430 ) 431 else: 432 txt = bui.Lstr(value=player_name) 433 bui.textwidget( 434 parent=self._root_widget, 435 position=( 436 h, 437 v 438 + self._button_height 439 * (0.64 if player_name != '' else 0.5), 440 ), 441 size=(0, 0), 442 text=bui.Lstr(resource=f'{self._r}.leaveGameText'), 443 scale=(0.83 if player_name != '' else 1.0), 444 color=(0.75, 1.0, 0.7), 445 h_align='center', 446 v_align='center', 447 draw_controller=btn, 448 maxwidth=self._button_width * 0.9, 449 ) 450 bui.textwidget( 451 parent=self._root_widget, 452 position=(h, v + self._button_height * 0.27), 453 size=(0, 0), 454 text=txt, 455 color=(0.75, 1.0, 0.7), 456 h_align='center', 457 v_align='center', 458 draw_controller=btn, 459 scale=0.45, 460 maxwidth=self._button_width * 0.9, 461 ) 462 return h, v, scale 463 464 def _change_replay_speed(self, offs: int) -> None: 465 if not self._replay_speed_text: 466 if bui.do_once(): 467 print('_change_replay_speed called without widget') 468 return 469 bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs) 470 actual_speed = pow(2.0, bs.get_replay_speed_exponent()) 471 bui.textwidget( 472 edit=self._replay_speed_text, 473 text=bui.Lstr( 474 resource='watchWindow.playbackSpeedText', 475 subs=[('${SPEED}', str(actual_speed))], 476 ), 477 ) 478 479 def _pause_or_resume_replay(self) -> None: 480 if bs.is_replay_paused(): 481 bs.resume_replay() 482 bui.buttonwidget( 483 edit=self._pause_resume_button, 484 label=bui.charstr(bui.SpecialChar.PAUSE_BUTTON), 485 ) 486 else: 487 bs.pause_replay() 488 bui.buttonwidget( 489 edit=self._pause_resume_button, 490 label=bui.charstr(bui.SpecialChar.PLAY_BUTTON), 491 ) 492 493 def _is_benchmark(self) -> bool: 494 session = bs.get_foreground_host_session() 495 return getattr(session, 'benchmark_type', None) == 'cpu' or ( 496 bui.app.classic is not None 497 and bui.app.classic.stress_test_update_timer is not None 498 ) 499 500 def _confirm_end_game(self) -> None: 501 # pylint: disable=cyclic-import 502 from bauiv1lib.confirm import ConfirmWindow 503 504 # FIXME: Currently we crash calling this on client-sessions. 505 506 # Select cancel by default; this occasionally gets called by accident 507 # in a fit of button mashing and this will help reduce damage. 508 ConfirmWindow( 509 bui.Lstr(resource=f'{self._r}.exitToMenuText'), 510 self._end_game, 511 cancel_is_selected=True, 512 ) 513 514 def _confirm_end_test(self) -> None: 515 # pylint: disable=cyclic-import 516 from bauiv1lib.confirm import ConfirmWindow 517 518 # Select cancel by default; this occasionally gets called by accident 519 # in a fit of button mashing and this will help reduce damage. 520 ConfirmWindow( 521 bui.Lstr(resource=f'{self._r}.exitToMenuText'), 522 self._end_game, 523 cancel_is_selected=True, 524 ) 525 526 def _confirm_end_replay(self) -> None: 527 # pylint: disable=cyclic-import 528 from bauiv1lib.confirm import ConfirmWindow 529 530 # Select cancel by default; this occasionally gets called by accident 531 # in a fit of button mashing and this will help reduce damage. 532 ConfirmWindow( 533 bui.Lstr(resource=f'{self._r}.exitToMenuText'), 534 self._end_game, 535 cancel_is_selected=True, 536 ) 537 538 def _confirm_leave_party(self) -> None: 539 # pylint: disable=cyclic-import 540 from bauiv1lib.confirm import ConfirmWindow 541 542 # Select cancel by default; this occasionally gets called by accident 543 # in a fit of button mashing and this will help reduce damage. 544 ConfirmWindow( 545 bui.Lstr(resource=f'{self._r}.leavePartyConfirmText'), 546 self._leave_party, 547 cancel_is_selected=True, 548 ) 549 550 def _leave_party(self) -> None: 551 bs.disconnect_from_host() 552 553 def _end_game(self) -> None: 554 assert bui.app.classic is not None 555 556 # no-op if our underlying widget is dead or on its way out. 557 if not self._root_widget or self._root_widget.transitioning_out: 558 return 559 560 bui.containerwidget(edit=self._root_widget, transition='out_left') 561 bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) 562 563 def _leave(self) -> None: 564 if self._input_player: 565 self._input_player.remove_from_game() 566 elif self._connected_to_remote_player: 567 if self._input_device: 568 self._input_device.detach_from_player() 569 self._resume() 570 571 def _resume_and_call(self, call: Callable[[], Any]) -> None: 572 self._resume() 573 call() 574 575 def _resume(self) -> None: 576 classic = bui.app.classic 577 578 assert classic is not None 579 classic.resume() 580 581 bui.app.ui_v1.clear_main_window() 582 583 # If there's callbacks waiting for us to resume, call them. 584 for call in classic.main_menu_resume_callbacks: 585 try: 586 call() 587 except Exception: 588 logging.exception('Error in classic resume callback.') 589 590 classic.main_menu_resume_callbacks.clear()
class
InGameMenuWindow(bauiv1._uitypes.MainWindow):
18class InGameMenuWindow(bui.MainWindow): 19 """The menu that can be invoked while in a game.""" 20 21 def __init__( 22 self, 23 transition: str | None = 'in_right', 24 origin_widget: bui.Widget | None = None, 25 ): 26 27 # Make a vanilla container; we'll modify it to our needs in 28 # refresh. 29 super().__init__( 30 root_widget=bui.containerwidget( 31 toolbar_visibility=('menu_in_game') 32 ), 33 transition=transition, 34 origin_widget=origin_widget, 35 ) 36 37 # Grab this stuff in case it changes. 38 self._is_demo = bui.app.env.demo 39 self._is_arcade = bui.app.env.arcade 40 41 self._p_index = 0 42 self._use_autoselect = True 43 self._button_width = 200.0 44 self._button_height = 45.0 45 self._width = 100.0 46 self._height = 100.0 47 48 self._refresh() 49 50 @override 51 def get_main_window_state(self) -> bui.MainWindowState: 52 # Support recreating our window for back/refresh purposes. 53 cls = type(self) 54 return bui.BasicMainWindowState( 55 create_call=lambda transition, origin_widget: cls( 56 transition=transition, origin_widget=origin_widget 57 ) 58 ) 59 60 def _refresh(self) -> None: 61 62 # Clear everything that was there. 63 children = self._root_widget.get_children() 64 for child in children: 65 child.delete() 66 67 self._r = 'mainMenu' 68 69 self._input_device = input_device = bs.get_ui_input_device() 70 71 # Are we connected to a local player? 72 self._input_player = input_device.player if input_device else None 73 74 # Are we connected to a remote player?. 75 self._connected_to_remote_player = ( 76 input_device.is_attached_to_player() 77 if (input_device and self._input_player is None) 78 else False 79 ) 80 81 positions: list[tuple[float, float, float]] = [] 82 self._p_index = 0 83 84 self._refresh_in_game(positions) 85 86 h, v, scale = positions[self._p_index] 87 self._p_index += 1 88 89 # If we're in a replay, we have a 'Leave Replay' button. 90 if bs.is_in_replay(): 91 bui.buttonwidget( 92 parent=self._root_widget, 93 position=(h - self._button_width * 0.5 * scale, v), 94 scale=scale, 95 size=(self._button_width, self._button_height), 96 autoselect=self._use_autoselect, 97 label=bui.Lstr(resource='replayEndText'), 98 on_activate_call=self._confirm_end_replay, 99 ) 100 elif bs.get_foreground_host_session() is not None: 101 bui.buttonwidget( 102 parent=self._root_widget, 103 position=(h - self._button_width * 0.5 * scale, v), 104 scale=scale, 105 size=(self._button_width, self._button_height), 106 autoselect=self._use_autoselect, 107 label=bui.Lstr( 108 resource=self._r 109 + ( 110 '.endTestText' 111 if self._is_benchmark() 112 else '.endGameText' 113 ) 114 ), 115 on_activate_call=( 116 self._confirm_end_test 117 if self._is_benchmark() 118 else self._confirm_end_game 119 ), 120 ) 121 else: 122 # Assume we're in a client-session. 123 bui.buttonwidget( 124 parent=self._root_widget, 125 position=(h - self._button_width * 0.5 * scale, v), 126 scale=scale, 127 size=(self._button_width, self._button_height), 128 autoselect=self._use_autoselect, 129 label=bui.Lstr(resource=f'{self._r}.leavePartyText'), 130 on_activate_call=self._confirm_leave_party, 131 ) 132 133 # Add speed-up/slow-down buttons for replays. Ideally this 134 # should be part of a fading-out playback bar like most media 135 # players but this works for now. 136 if bs.is_in_replay(): 137 b_size = 50.0 138 b_buffer_1 = 50.0 139 b_buffer_2 = 10.0 140 t_scale = 0.75 141 assert bui.app.classic is not None 142 uiscale = bui.app.ui_v1.uiscale 143 if uiscale is bui.UIScale.SMALL: 144 b_size *= 0.6 145 b_buffer_1 *= 0.8 146 b_buffer_2 *= 1.0 147 v_offs = -40 148 t_scale = 0.5 149 elif uiscale is bui.UIScale.MEDIUM: 150 v_offs = -70 151 else: 152 v_offs = -100 153 self._replay_speed_text = bui.textwidget( 154 parent=self._root_widget, 155 text=bui.Lstr( 156 resource='watchWindow.playbackSpeedText', 157 subs=[('${SPEED}', str(1.23))], 158 ), 159 position=(h, v + v_offs + 15 * t_scale), 160 h_align='center', 161 v_align='center', 162 size=(0, 0), 163 scale=t_scale, 164 ) 165 166 # Update to current value. 167 self._change_replay_speed(0) 168 169 # Keep updating in a timer in case it gets changed elsewhere. 170 self._change_replay_speed_timer = bui.AppTimer( 171 0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True 172 ) 173 btn = bui.buttonwidget( 174 parent=self._root_widget, 175 position=( 176 h - b_size - b_buffer_1, 177 v - b_size - b_buffer_2 + v_offs, 178 ), 179 button_type='square', 180 size=(b_size, b_size), 181 label='', 182 autoselect=True, 183 on_activate_call=bui.Call(self._change_replay_speed, -1), 184 ) 185 bui.textwidget( 186 parent=self._root_widget, 187 draw_controller=btn, 188 text='-', 189 position=( 190 h - b_size * 0.5 - b_buffer_1, 191 v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, 192 ), 193 h_align='center', 194 v_align='center', 195 size=(0, 0), 196 scale=3.0 * t_scale, 197 ) 198 btn = bui.buttonwidget( 199 parent=self._root_widget, 200 position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs), 201 button_type='square', 202 size=(b_size, b_size), 203 label='', 204 autoselect=True, 205 on_activate_call=bui.Call(self._change_replay_speed, 1), 206 ) 207 bui.textwidget( 208 parent=self._root_widget, 209 draw_controller=btn, 210 text='+', 211 position=( 212 h + b_size * 0.5 + b_buffer_1, 213 v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, 214 ), 215 h_align='center', 216 v_align='center', 217 size=(0, 0), 218 scale=3.0 * t_scale, 219 ) 220 self._pause_resume_button = btn = bui.buttonwidget( 221 parent=self._root_widget, 222 position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs), 223 button_type='square', 224 size=(b_size, b_size), 225 label=bui.charstr( 226 bui.SpecialChar.PLAY_BUTTON 227 if bs.is_replay_paused() 228 else bui.SpecialChar.PAUSE_BUTTON 229 ), 230 autoselect=True, 231 on_activate_call=bui.Call(self._pause_or_resume_replay), 232 ) 233 btn = bui.buttonwidget( 234 parent=self._root_widget, 235 position=( 236 h - b_size * 1.5 - b_buffer_1 * 2, 237 v - b_size - b_buffer_2 + v_offs, 238 ), 239 button_type='square', 240 size=(b_size, b_size), 241 label='', 242 autoselect=True, 243 on_activate_call=bui.WeakCall(self._rewind_replay), 244 ) 245 bui.textwidget( 246 parent=self._root_widget, 247 draw_controller=btn, 248 # text='<<', 249 text=bui.charstr(bui.SpecialChar.REWIND_BUTTON), 250 position=( 251 h - b_size - b_buffer_1 * 2, 252 v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, 253 ), 254 h_align='center', 255 v_align='center', 256 size=(0, 0), 257 scale=2.0 * t_scale, 258 ) 259 btn = bui.buttonwidget( 260 parent=self._root_widget, 261 position=( 262 h + b_size * 0.5 + b_buffer_1 * 2, 263 v - b_size - b_buffer_2 + v_offs, 264 ), 265 button_type='square', 266 size=(b_size, b_size), 267 label='', 268 autoselect=True, 269 on_activate_call=bui.WeakCall(self._forward_replay), 270 ) 271 bui.textwidget( 272 parent=self._root_widget, 273 draw_controller=btn, 274 # text='>>', 275 text=bui.charstr(bui.SpecialChar.FAST_FORWARD_BUTTON), 276 position=( 277 h + b_size + b_buffer_1 * 2, 278 v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, 279 ), 280 h_align='center', 281 v_align='center', 282 size=(0, 0), 283 scale=2.0 * t_scale, 284 ) 285 286 def _rewind_replay(self) -> None: 287 bs.seek_replay(-2 * pow(2, bs.get_replay_speed_exponent())) 288 289 def _forward_replay(self) -> None: 290 bs.seek_replay(2 * pow(2, bs.get_replay_speed_exponent())) 291 292 def _refresh_in_game( 293 self, positions: list[tuple[float, float, float]] 294 ) -> tuple[float, float, float]: 295 # pylint: disable=too-many-branches 296 # pylint: disable=too-many-locals 297 # pylint: disable=too-many-statements 298 assert bui.app.classic is not None 299 custom_menu_entries: list[dict[str, Any]] = [] 300 session = bs.get_foreground_host_session() 301 if session is not None: 302 try: 303 custom_menu_entries = session.get_custom_menu_entries() 304 for cme in custom_menu_entries: 305 cme_any: Any = cme # Type check may not hold true. 306 if ( 307 not isinstance(cme_any, dict) 308 or 'label' not in cme 309 or not isinstance(cme['label'], (str, bui.Lstr)) 310 or 'call' not in cme 311 or not callable(cme['call']) 312 ): 313 raise ValueError( 314 'invalid custom menu entry: ' + str(cme) 315 ) 316 except Exception: 317 custom_menu_entries = [] 318 logging.exception( 319 'Error getting custom menu entries for %s.', session 320 ) 321 self._width = 250.0 322 self._height = 250.0 if self._input_player else 180.0 323 if (self._is_demo or self._is_arcade) and self._input_player: 324 self._height -= 40 325 # if not self._have_settings_button: 326 self._height -= 50 327 if self._connected_to_remote_player: 328 # In this case we have a leave *and* a disconnect button. 329 self._height += 50 330 self._height += 50 * (len(custom_menu_entries)) 331 uiscale = bui.app.ui_v1.uiscale 332 bui.containerwidget( 333 edit=self._root_widget, 334 size=(self._width, self._height), 335 scale=( 336 2.15 337 if uiscale is bui.UIScale.SMALL 338 else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0 339 ), 340 ) 341 h = 125.0 342 v = self._height - 80.0 if self._input_player else self._height - 60 343 h_offset = 0 344 d_h_offset = 0 345 v_offset = -50 346 for _i in range(6 + len(custom_menu_entries)): 347 positions.append((h, v, 1.0)) 348 v += v_offset 349 h += h_offset 350 h_offset += d_h_offset 351 # self._play_button = None 352 bui.app.classic.pause() 353 354 # Player name if applicable. 355 if self._input_player: 356 player_name = self._input_player.getname() 357 h, v, scale = positions[self._p_index] 358 v += 35 359 bui.textwidget( 360 parent=self._root_widget, 361 position=(h - self._button_width / 2, v), 362 size=(self._button_width, self._button_height), 363 color=(1, 1, 1, 0.5), 364 scale=0.7, 365 h_align='center', 366 text=bui.Lstr(value=player_name), 367 ) 368 else: 369 player_name = '' 370 h, v, scale = positions[self._p_index] 371 self._p_index += 1 372 btn = bui.buttonwidget( 373 parent=self._root_widget, 374 position=(h - self._button_width / 2, v), 375 size=(self._button_width, self._button_height), 376 scale=scale, 377 label=bui.Lstr(resource=f'{self._r}.resumeText'), 378 autoselect=self._use_autoselect, 379 on_activate_call=self._resume, 380 ) 381 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 382 383 # Add any custom options defined by the current game. 384 for entry in custom_menu_entries: 385 h, v, scale = positions[self._p_index] 386 self._p_index += 1 387 388 # Ask the entry whether we should resume when we call 389 # it (defaults to true). 390 resume = bool(entry.get('resume_on_call', True)) 391 392 if resume: 393 call = bui.Call(self._resume_and_call, entry['call']) 394 else: 395 call = bui.Call(entry['call'], bui.WeakCall(self._resume)) 396 397 bui.buttonwidget( 398 parent=self._root_widget, 399 position=(h - self._button_width / 2, v), 400 size=(self._button_width, self._button_height), 401 scale=scale, 402 on_activate_call=call, 403 label=entry['label'], 404 autoselect=self._use_autoselect, 405 ) 406 407 # Add a 'leave' button if the menu-owner has a player. 408 if (self._input_player or self._connected_to_remote_player) and not ( 409 self._is_demo or self._is_arcade 410 ): 411 h, v, scale = positions[self._p_index] 412 self._p_index += 1 413 btn = bui.buttonwidget( 414 parent=self._root_widget, 415 position=(h - self._button_width / 2, v), 416 size=(self._button_width, self._button_height), 417 scale=scale, 418 on_activate_call=self._leave, 419 label='', 420 autoselect=self._use_autoselect, 421 ) 422 423 if ( 424 player_name != '' 425 and player_name[0] != '<' 426 and player_name[-1] != '>' 427 ): 428 txt = bui.Lstr( 429 resource=f'{self._r}.justPlayerText', 430 subs=[('${NAME}', player_name)], 431 ) 432 else: 433 txt = bui.Lstr(value=player_name) 434 bui.textwidget( 435 parent=self._root_widget, 436 position=( 437 h, 438 v 439 + self._button_height 440 * (0.64 if player_name != '' else 0.5), 441 ), 442 size=(0, 0), 443 text=bui.Lstr(resource=f'{self._r}.leaveGameText'), 444 scale=(0.83 if player_name != '' else 1.0), 445 color=(0.75, 1.0, 0.7), 446 h_align='center', 447 v_align='center', 448 draw_controller=btn, 449 maxwidth=self._button_width * 0.9, 450 ) 451 bui.textwidget( 452 parent=self._root_widget, 453 position=(h, v + self._button_height * 0.27), 454 size=(0, 0), 455 text=txt, 456 color=(0.75, 1.0, 0.7), 457 h_align='center', 458 v_align='center', 459 draw_controller=btn, 460 scale=0.45, 461 maxwidth=self._button_width * 0.9, 462 ) 463 return h, v, scale 464 465 def _change_replay_speed(self, offs: int) -> None: 466 if not self._replay_speed_text: 467 if bui.do_once(): 468 print('_change_replay_speed called without widget') 469 return 470 bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs) 471 actual_speed = pow(2.0, bs.get_replay_speed_exponent()) 472 bui.textwidget( 473 edit=self._replay_speed_text, 474 text=bui.Lstr( 475 resource='watchWindow.playbackSpeedText', 476 subs=[('${SPEED}', str(actual_speed))], 477 ), 478 ) 479 480 def _pause_or_resume_replay(self) -> None: 481 if bs.is_replay_paused(): 482 bs.resume_replay() 483 bui.buttonwidget( 484 edit=self._pause_resume_button, 485 label=bui.charstr(bui.SpecialChar.PAUSE_BUTTON), 486 ) 487 else: 488 bs.pause_replay() 489 bui.buttonwidget( 490 edit=self._pause_resume_button, 491 label=bui.charstr(bui.SpecialChar.PLAY_BUTTON), 492 ) 493 494 def _is_benchmark(self) -> bool: 495 session = bs.get_foreground_host_session() 496 return getattr(session, 'benchmark_type', None) == 'cpu' or ( 497 bui.app.classic is not None 498 and bui.app.classic.stress_test_update_timer is not None 499 ) 500 501 def _confirm_end_game(self) -> None: 502 # pylint: disable=cyclic-import 503 from bauiv1lib.confirm import ConfirmWindow 504 505 # FIXME: Currently we crash calling this on client-sessions. 506 507 # Select cancel by default; this occasionally gets called by accident 508 # in a fit of button mashing and this will help reduce damage. 509 ConfirmWindow( 510 bui.Lstr(resource=f'{self._r}.exitToMenuText'), 511 self._end_game, 512 cancel_is_selected=True, 513 ) 514 515 def _confirm_end_test(self) -> None: 516 # pylint: disable=cyclic-import 517 from bauiv1lib.confirm import ConfirmWindow 518 519 # Select cancel by default; this occasionally gets called by accident 520 # in a fit of button mashing and this will help reduce damage. 521 ConfirmWindow( 522 bui.Lstr(resource=f'{self._r}.exitToMenuText'), 523 self._end_game, 524 cancel_is_selected=True, 525 ) 526 527 def _confirm_end_replay(self) -> None: 528 # pylint: disable=cyclic-import 529 from bauiv1lib.confirm import ConfirmWindow 530 531 # Select cancel by default; this occasionally gets called by accident 532 # in a fit of button mashing and this will help reduce damage. 533 ConfirmWindow( 534 bui.Lstr(resource=f'{self._r}.exitToMenuText'), 535 self._end_game, 536 cancel_is_selected=True, 537 ) 538 539 def _confirm_leave_party(self) -> None: 540 # pylint: disable=cyclic-import 541 from bauiv1lib.confirm import ConfirmWindow 542 543 # Select cancel by default; this occasionally gets called by accident 544 # in a fit of button mashing and this will help reduce damage. 545 ConfirmWindow( 546 bui.Lstr(resource=f'{self._r}.leavePartyConfirmText'), 547 self._leave_party, 548 cancel_is_selected=True, 549 ) 550 551 def _leave_party(self) -> None: 552 bs.disconnect_from_host() 553 554 def _end_game(self) -> None: 555 assert bui.app.classic is not None 556 557 # no-op if our underlying widget is dead or on its way out. 558 if not self._root_widget or self._root_widget.transitioning_out: 559 return 560 561 bui.containerwidget(edit=self._root_widget, transition='out_left') 562 bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) 563 564 def _leave(self) -> None: 565 if self._input_player: 566 self._input_player.remove_from_game() 567 elif self._connected_to_remote_player: 568 if self._input_device: 569 self._input_device.detach_from_player() 570 self._resume() 571 572 def _resume_and_call(self, call: Callable[[], Any]) -> None: 573 self._resume() 574 call() 575 576 def _resume(self) -> None: 577 classic = bui.app.classic 578 579 assert classic is not None 580 classic.resume() 581 582 bui.app.ui_v1.clear_main_window() 583 584 # If there's callbacks waiting for us to resume, call them. 585 for call in classic.main_menu_resume_callbacks: 586 try: 587 call() 588 except Exception: 589 logging.exception('Error in classic resume callback.') 590 591 classic.main_menu_resume_callbacks.clear()
The menu that can be invoked while in a game.
InGameMenuWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
21 def __init__( 22 self, 23 transition: str | None = 'in_right', 24 origin_widget: bui.Widget | None = None, 25 ): 26 27 # Make a vanilla container; we'll modify it to our needs in 28 # refresh. 29 super().__init__( 30 root_widget=bui.containerwidget( 31 toolbar_visibility=('menu_in_game') 32 ), 33 transition=transition, 34 origin_widget=origin_widget, 35 ) 36 37 # Grab this stuff in case it changes. 38 self._is_demo = bui.app.env.demo 39 self._is_arcade = bui.app.env.arcade 40 41 self._p_index = 0 42 self._use_autoselect = True 43 self._button_width = 200.0 44 self._button_height = 45.0 45 self._width = 100.0 46 self._height = 100.0 47 48 self._refresh()
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.
50 @override 51 def get_main_window_state(self) -> bui.MainWindowState: 52 # Support recreating our window for back/refresh purposes. 53 cls = type(self) 54 return bui.BasicMainWindowState( 55 create_call=lambda transition, origin_widget: cls( 56 transition=transition, origin_widget=origin_widget 57 ) 58 )
Return a WindowState to recreate this window, if supported.
Inherited Members
- bauiv1._uitypes.MainWindow
- main_window_back_state
- main_window_close
- can_change_main_window
- main_window_back
- main_window_replace
- on_main_window_close
- bauiv1._uitypes.Window
- get_root_widget