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