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