bauiv1lib.gather.manualtab
Defines the manual tab in the gather UI.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines the manual tab in the gather UI.""" 4# pylint: disable=too-many-lines 5 6from __future__ import annotations 7 8import logging 9from enum import Enum 10from threading import Thread 11from dataclasses import dataclass 12from typing import TYPE_CHECKING, cast, override 13from bauiv1lib.gather import GatherTab 14 15import bauiv1 as bui 16import bascenev1 as bs 17 18if TYPE_CHECKING: 19 from typing import Any, Callable 20 21 from bauiv1lib.gather import GatherWindow 22 23 24def _safe_set_text( 25 txt: bui.Widget | None, val: str | bui.Lstr, success: bool = True 26) -> None: 27 if txt: 28 bui.textwidget( 29 edit=txt, text=val, color=(0, 1, 0) if success else (1, 1, 0) 30 ) 31 32 33class _HostLookupThread(Thread): 34 """Thread to fetch an addr.""" 35 36 def __init__( 37 self, name: str, port: int, call: Callable[[str | None, int], Any] 38 ): 39 super().__init__() 40 self._name = name 41 self._port = port 42 self._call = call 43 44 @override 45 def run(self) -> None: 46 result: str | None 47 try: 48 import socket 49 50 aresult = [ 51 item[-1][0] 52 for item in socket.getaddrinfo(self.name, self._port) 53 ][0] 54 if isinstance(aresult, int): 55 raise RuntimeError('Unexpected getaddrinfo int result') 56 result = aresult 57 except Exception: 58 # Hmm should we be logging this? 59 result = None 60 bui.pushcall( 61 lambda: self._call(result, self._port), from_other_thread=True 62 ) 63 64 65class SubTabType(Enum): 66 """Available sub-tabs.""" 67 68 JOIN_BY_ADDRESS = 'join_by_address' 69 FAVORITES = 'favorites' 70 71 72@dataclass 73class State: 74 """State saved/restored only while the app is running.""" 75 76 sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS 77 78 79class ManualGatherTab(GatherTab): 80 """The manual tab in the gather UI""" 81 82 def __init__(self, window: GatherWindow) -> None: 83 super().__init__(window) 84 self._check_button: bui.Widget | None = None 85 self._doing_access_check: bool | None = None 86 self._access_check_count: int | None = None 87 self._sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS 88 self._t_addr: bui.Widget | None = None 89 self._t_accessible: bui.Widget | None = None 90 self._t_accessible_extra: bui.Widget | None = None 91 self._access_check_timer: bui.AppTimer | None = None 92 self._checking_state_text: bui.Widget | None = None 93 self._container: bui.Widget | None = None 94 self._join_by_address_text: bui.Widget | None = None 95 self._favorites_text: bui.Widget | None = None 96 self._width: float | None = None 97 self._height: float | None = None 98 self._scroll_width: float | None = None 99 self._scroll_height: float | None = None 100 self._favorites_scroll_width: int | None = None 101 self._favorites_connect_button: bui.Widget | None = None 102 self._scrollwidget: bui.Widget | None = None 103 self._columnwidget: bui.Widget | None = None 104 self._favorite_selected: str | None = None 105 self._favorite_edit_window: bui.Widget | None = None 106 self._party_edit_name_text: bui.Widget | None = None 107 self._party_edit_addr_text: bui.Widget | None = None 108 self._party_edit_port_text: bui.Widget | None = None 109 self._no_parties_added_text: bui.Widget | None = None 110 111 @override 112 def on_activate( 113 self, 114 parent_widget: bui.Widget, 115 tab_button: bui.Widget, 116 region_width: float, 117 region_height: float, 118 region_left: float, 119 region_bottom: float, 120 ) -> bui.Widget: 121 # pylint: disable=too-many-positional-arguments 122 c_width = region_width 123 c_height = region_height - 20 124 125 self._container = bui.containerwidget( 126 parent=parent_widget, 127 position=( 128 region_left, 129 region_bottom + (region_height - c_height) * 0.5, 130 ), 131 size=(c_width, c_height), 132 background=False, 133 selection_loops_to_parent=True, 134 ) 135 v = c_height - 30 136 self._join_by_address_text = bui.textwidget( 137 parent=self._container, 138 position=(c_width * 0.5 - 245, v - 13), 139 color=(0.6, 1.0, 0.6), 140 scale=1.3, 141 size=(200, 30), 142 maxwidth=250, 143 h_align='center', 144 v_align='center', 145 click_activate=True, 146 selectable=True, 147 autoselect=True, 148 on_activate_call=lambda: self._set_sub_tab( 149 SubTabType.JOIN_BY_ADDRESS, 150 region_width, 151 region_height, 152 playsound=True, 153 ), 154 text=bui.Lstr(resource='gatherWindow.manualJoinSectionText'), 155 glow_type='uniform', 156 ) 157 self._favorites_text = bui.textwidget( 158 parent=self._container, 159 position=(c_width * 0.5 + 45, v - 13), 160 color=(0.6, 1.0, 0.6), 161 scale=1.3, 162 size=(200, 30), 163 maxwidth=250, 164 h_align='center', 165 v_align='center', 166 click_activate=True, 167 selectable=True, 168 autoselect=True, 169 on_activate_call=lambda: self._set_sub_tab( 170 SubTabType.FAVORITES, 171 region_width, 172 region_height, 173 playsound=True, 174 ), 175 text=bui.Lstr(resource='gatherWindow.favoritesText'), 176 glow_type='uniform', 177 ) 178 bui.widget(edit=self._join_by_address_text, up_widget=tab_button) 179 bui.widget( 180 edit=self._favorites_text, 181 left_widget=self._join_by_address_text, 182 up_widget=tab_button, 183 ) 184 bui.widget(edit=tab_button, down_widget=self._favorites_text) 185 bui.widget( 186 edit=self._join_by_address_text, right_widget=self._favorites_text 187 ) 188 self._set_sub_tab(self._sub_tab, region_width, region_height) 189 190 return self._container 191 192 @override 193 def save_state(self) -> None: 194 assert bui.app.classic is not None 195 bui.app.ui_v1.window_states[type(self)] = State(sub_tab=self._sub_tab) 196 197 @override 198 def restore_state(self) -> None: 199 assert bui.app.classic is not None 200 state = bui.app.ui_v1.window_states.get(type(self)) 201 if state is None: 202 state = State() 203 assert isinstance(state, State) 204 self._sub_tab = state.sub_tab 205 206 def _set_sub_tab( 207 self, 208 value: SubTabType, 209 region_width: float, 210 region_height: float, 211 playsound: bool = False, 212 ) -> None: 213 assert self._container 214 if playsound: 215 bui.getsound('click01').play() 216 217 self._sub_tab = value 218 active_color = (0.6, 1.0, 0.6) 219 inactive_color = (0.5, 0.4, 0.5) 220 bui.textwidget( 221 edit=self._join_by_address_text, 222 color=( 223 active_color 224 if value is SubTabType.JOIN_BY_ADDRESS 225 else inactive_color 226 ), 227 ) 228 bui.textwidget( 229 edit=self._favorites_text, 230 color=( 231 active_color 232 if value is SubTabType.FAVORITES 233 else inactive_color 234 ), 235 ) 236 237 # Clear anything existing in the old sub-tab. 238 for widget in self._container.get_children(): 239 if widget and widget not in { 240 self._favorites_text, 241 self._join_by_address_text, 242 }: 243 widget.delete() 244 245 if value is SubTabType.JOIN_BY_ADDRESS: 246 self._build_join_by_address_tab(region_width, region_height) 247 248 if value is SubTabType.FAVORITES: 249 self._build_favorites_tab(region_width, region_height) 250 251 # The old manual tab 252 def _build_join_by_address_tab( 253 self, region_width: float, region_height: float 254 ) -> None: 255 c_width = region_width 256 c_height = region_height - 20 257 last_addr = bui.app.config.get('Last Manual Party Connect Address', '') 258 last_port = bui.app.config.get('Last Manual Party Connect Port', 43210) 259 v = c_height - 70 260 v -= 70 261 bui.textwidget( 262 parent=self._container, 263 position=(c_width * 0.5 - 260 - 50, v), 264 color=(0.6, 1.0, 0.6), 265 scale=1.0, 266 size=(0, 0), 267 maxwidth=130, 268 h_align='right', 269 v_align='center', 270 text=bui.Lstr(resource='gatherWindow.' 'manualAddressText'), 271 ) 272 txt = bui.textwidget( 273 parent=self._container, 274 editable=True, 275 description=bui.Lstr(resource='gatherWindow.' 'manualAddressText'), 276 position=(c_width * 0.5 - 240 - 50, v - 30), 277 text=last_addr, 278 autoselect=True, 279 v_align='center', 280 scale=1.0, 281 maxwidth=380, 282 size=(420, 60), 283 ) 284 assert self._join_by_address_text is not None 285 bui.widget(edit=self._join_by_address_text, down_widget=txt) 286 assert self._favorites_text is not None 287 bui.widget(edit=self._favorites_text, down_widget=txt) 288 bui.textwidget( 289 parent=self._container, 290 position=(c_width * 0.5 - 260 + 490, v), 291 color=(0.6, 1.0, 0.6), 292 scale=1.0, 293 size=(0, 0), 294 maxwidth=80, 295 h_align='right', 296 v_align='center', 297 text=bui.Lstr(resource='gatherWindow.' 'portText'), 298 ) 299 txt2 = bui.textwidget( 300 parent=self._container, 301 editable=True, 302 description=bui.Lstr(resource='gatherWindow.' 'portText'), 303 text=str(last_port), 304 autoselect=True, 305 max_chars=5, 306 position=(c_width * 0.5 - 240 + 490, v - 30), 307 v_align='center', 308 scale=1.0, 309 size=(170, 60), 310 ) 311 312 v -= 110 313 314 btn = bui.buttonwidget( 315 parent=self._container, 316 size=(300, 70), 317 label=bui.Lstr(resource='gatherWindow.' 'manualConnectText'), 318 position=(c_width * 0.5 - 300, v), 319 autoselect=True, 320 on_activate_call=bui.Call(self._connect, txt, txt2), 321 ) 322 savebutton = bui.buttonwidget( 323 parent=self._container, 324 size=(300, 70), 325 label=bui.Lstr(resource='gatherWindow.favoritesSaveText'), 326 position=(c_width * 0.5 - 240 + 490 - 200, v), 327 autoselect=True, 328 on_activate_call=bui.Call(self._save_server, txt, txt2), 329 ) 330 bui.widget(edit=btn, right_widget=savebutton) 331 bui.widget(edit=savebutton, left_widget=btn, up_widget=txt2) 332 bui.textwidget(edit=txt, on_return_press_call=btn.activate) 333 bui.textwidget(edit=txt2, on_return_press_call=btn.activate) 334 v -= 45 335 336 self._check_button = bui.textwidget( 337 parent=self._container, 338 size=(250, 60), 339 text=bui.Lstr(resource='gatherWindow.showMyAddressText'), 340 v_align='center', 341 h_align='center', 342 click_activate=True, 343 position=(c_width * 0.5 - 125, v - 30), 344 autoselect=True, 345 color=(0.5, 0.9, 0.5), 346 scale=0.8, 347 selectable=True, 348 on_activate_call=bui.Call( 349 self._on_show_my_address_button_press, 350 v, 351 self._container, 352 c_width, 353 ), 354 glow_type='uniform', 355 ) 356 bui.widget(edit=self._check_button, up_widget=btn) 357 358 # Tab containing saved favorite addresses 359 def _build_favorites_tab( 360 self, region_width: float, region_height: float 361 ) -> None: 362 c_height = region_height - 20 363 v = c_height - 35 - 25 - 30 364 365 assert bui.app.classic is not None 366 uiscale = bui.app.ui_v1.uiscale 367 # self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040 368 self._width = region_width 369 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 370 self._height = ( 371 578 372 if uiscale is bui.UIScale.SMALL 373 else 670 if uiscale is bui.UIScale.MEDIUM else 800 374 ) 375 376 self._scroll_width = self._width - 130 + 2 * x_inset 377 self._scroll_height = self._height - 180 378 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 379 380 c_height = self._scroll_height - 20 381 sub_scroll_height = c_height - 63 382 self._favorites_scroll_width = sub_scroll_width = ( 383 680 if uiscale is bui.UIScale.SMALL else 640 384 ) 385 386 v = c_height - 30 387 388 b_width = 140 if uiscale is bui.UIScale.SMALL else 178 389 b_height = ( 390 107 391 if uiscale is bui.UIScale.SMALL 392 else 142 if uiscale is bui.UIScale.MEDIUM else 190 393 ) 394 b_space_extra = ( 395 0 396 if uiscale is bui.UIScale.SMALL 397 else -2 if uiscale is bui.UIScale.MEDIUM else -5 398 ) 399 400 btnv = ( 401 c_height 402 - ( 403 48 404 if uiscale is bui.UIScale.SMALL 405 else 45 if uiscale is bui.UIScale.MEDIUM else 40 406 ) 407 - b_height 408 ) 409 410 self._favorites_connect_button = btn1 = bui.buttonwidget( 411 parent=self._container, 412 size=(b_width, b_height), 413 position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv), 414 button_type='square', 415 color=(0.6, 0.53, 0.63), 416 textcolor=(0.75, 0.7, 0.8), 417 on_activate_call=self._on_favorites_connect_press, 418 text_scale=1.0 if uiscale is bui.UIScale.SMALL else 1.2, 419 label=bui.Lstr(resource='gatherWindow.manualConnectText'), 420 autoselect=True, 421 ) 422 if uiscale is bui.UIScale.SMALL: 423 bui.widget( 424 edit=btn1, 425 left_widget=bui.get_special_widget('back_button'), 426 ) 427 btnv -= b_height + b_space_extra 428 bui.buttonwidget( 429 parent=self._container, 430 size=(b_width, b_height), 431 position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv), 432 button_type='square', 433 color=(0.6, 0.53, 0.63), 434 textcolor=(0.75, 0.7, 0.8), 435 on_activate_call=self._on_favorites_edit_press, 436 text_scale=1.0 if uiscale is bui.UIScale.SMALL else 1.2, 437 label=bui.Lstr(resource='editText'), 438 autoselect=True, 439 ) 440 btnv -= b_height + b_space_extra 441 bui.buttonwidget( 442 parent=self._container, 443 size=(b_width, b_height), 444 position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv), 445 button_type='square', 446 color=(0.6, 0.53, 0.63), 447 textcolor=(0.75, 0.7, 0.8), 448 on_activate_call=self._on_favorite_delete_press, 449 text_scale=1.0 if uiscale is bui.UIScale.SMALL else 1.2, 450 label=bui.Lstr(resource='deleteText'), 451 autoselect=True, 452 ) 453 454 v -= sub_scroll_height + 23 455 self._scrollwidget = scrlw = bui.scrollwidget( 456 parent=self._container, 457 position=(290 if uiscale is bui.UIScale.SMALL else 225, v), 458 size=(sub_scroll_width, sub_scroll_height), 459 claims_left_right=True, 460 ) 461 bui.widget( 462 edit=self._favorites_connect_button, right_widget=self._scrollwidget 463 ) 464 self._columnwidget = bui.columnwidget( 465 parent=scrlw, 466 left_border=10, 467 border=2, 468 margin=0, 469 claims_left_right=True, 470 ) 471 472 self._no_parties_added_text = bui.textwidget( 473 parent=self._container, 474 size=(0, 0), 475 h_align='center', 476 v_align='center', 477 text='', 478 color=(0.6, 0.6, 0.6), 479 scale=1.2, 480 position=( 481 ( 482 (240 if uiscale is bui.UIScale.SMALL else 225) 483 + sub_scroll_width * 0.5 484 ), 485 v + sub_scroll_height * 0.5, 486 ), 487 glow_type='uniform', 488 ) 489 490 self._favorite_selected = None 491 self._refresh_favorites() 492 493 def _no_favorite_selected_error(self) -> None: 494 bui.screenmessage( 495 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 496 ) 497 bui.getsound('error').play() 498 499 def _on_favorites_connect_press(self) -> None: 500 if self._favorite_selected is None: 501 self._no_favorite_selected_error() 502 503 else: 504 config = bui.app.config['Saved Servers'][self._favorite_selected] 505 _HostLookupThread( 506 name=config['addr'], 507 port=config['port'], 508 call=bui.WeakCall(self._host_lookup_result), 509 ).start() 510 511 def _on_favorites_edit_press(self) -> None: 512 if self._favorite_selected is None: 513 self._no_favorite_selected_error() 514 return 515 516 c_width = 600 517 c_height = 310 518 assert bui.app.classic is not None 519 uiscale = bui.app.ui_v1.uiscale 520 self._favorite_edit_window = cnt = bui.containerwidget( 521 scale=( 522 1.8 523 if uiscale is bui.UIScale.SMALL 524 else 1.55 if uiscale is bui.UIScale.MEDIUM else 1.0 525 ), 526 size=(c_width, c_height), 527 transition='in_scale', 528 ) 529 530 bui.textwidget( 531 parent=cnt, 532 size=(0, 0), 533 h_align='center', 534 v_align='center', 535 text=bui.Lstr(resource='editText'), 536 color=(0.6, 1.0, 0.6), 537 maxwidth=c_width * 0.8, 538 position=(c_width * 0.5, c_height - 60), 539 ) 540 541 bui.textwidget( 542 parent=cnt, 543 position=(c_width * 0.2 - 15, c_height - 120), 544 color=(0.6, 1.0, 0.6), 545 scale=1.0, 546 size=(0, 0), 547 maxwidth=60, 548 h_align='right', 549 v_align='center', 550 text=bui.Lstr(resource='nameText'), 551 ) 552 553 self._party_edit_name_text = bui.textwidget( 554 parent=cnt, 555 size=(c_width * 0.7, 40), 556 h_align='left', 557 v_align='center', 558 text=bui.app.config['Saved Servers'][self._favorite_selected][ 559 'name' 560 ], 561 editable=True, 562 description=bui.Lstr(resource='nameText'), 563 position=(c_width * 0.2, c_height - 140), 564 autoselect=True, 565 maxwidth=c_width * 0.6, 566 max_chars=200, 567 ) 568 569 bui.textwidget( 570 parent=cnt, 571 position=(c_width * 0.2 - 15, c_height - 180), 572 color=(0.6, 1.0, 0.6), 573 scale=1.0, 574 size=(0, 0), 575 maxwidth=60, 576 h_align='right', 577 v_align='center', 578 text=bui.Lstr(resource='gatherWindow.' 'manualAddressText'), 579 ) 580 581 self._party_edit_addr_text = bui.textwidget( 582 parent=cnt, 583 size=(c_width * 0.4, 40), 584 h_align='left', 585 v_align='center', 586 text=bui.app.config['Saved Servers'][self._favorite_selected][ 587 'addr' 588 ], 589 editable=True, 590 description=bui.Lstr(resource='gatherWindow.manualAddressText'), 591 position=(c_width * 0.2, c_height - 200), 592 autoselect=True, 593 maxwidth=c_width * 0.35, 594 max_chars=200, 595 ) 596 597 bui.textwidget( 598 parent=cnt, 599 position=(c_width * 0.7 - 10, c_height - 180), 600 color=(0.6, 1.0, 0.6), 601 scale=1.0, 602 size=(0, 0), 603 maxwidth=45, 604 h_align='right', 605 v_align='center', 606 text=bui.Lstr(resource='gatherWindow.' 'portText'), 607 ) 608 609 self._party_edit_port_text = bui.textwidget( 610 parent=cnt, 611 size=(c_width * 0.2, 40), 612 h_align='left', 613 v_align='center', 614 text=str( 615 bui.app.config['Saved Servers'][self._favorite_selected]['port'] 616 ), 617 editable=True, 618 description=bui.Lstr(resource='gatherWindow.portText'), 619 position=(c_width * 0.7, c_height - 200), 620 autoselect=True, 621 maxwidth=c_width * 0.2, 622 max_chars=6, 623 ) 624 cbtn = bui.buttonwidget( 625 parent=cnt, 626 label=bui.Lstr(resource='cancelText'), 627 on_activate_call=bui.Call( 628 lambda c: bui.containerwidget(edit=c, transition='out_scale'), 629 cnt, 630 ), 631 size=(180, 60), 632 position=(30, 30), 633 autoselect=True, 634 ) 635 okb = bui.buttonwidget( 636 parent=cnt, 637 label=bui.Lstr(resource='saveText'), 638 size=(180, 60), 639 position=(c_width - 230, 30), 640 on_activate_call=bui.Call(self._edit_saved_party), 641 autoselect=True, 642 ) 643 bui.widget(edit=cbtn, right_widget=okb) 644 bui.widget(edit=okb, left_widget=cbtn) 645 bui.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) 646 647 def _edit_saved_party(self) -> None: 648 server = self._favorite_selected 649 if self._favorite_selected is None: 650 self._no_favorite_selected_error() 651 return 652 if not self._party_edit_name_text or not self._party_edit_addr_text: 653 return 654 new_name_raw = cast( 655 str, bui.textwidget(query=self._party_edit_name_text) 656 ) 657 new_addr_raw = cast( 658 str, bui.textwidget(query=self._party_edit_addr_text) 659 ) 660 new_port_raw = cast( 661 str, bui.textwidget(query=self._party_edit_port_text) 662 ) 663 bui.app.config['Saved Servers'][server]['name'] = new_name_raw 664 bui.app.config['Saved Servers'][server]['addr'] = new_addr_raw 665 try: 666 bui.app.config['Saved Servers'][server]['port'] = int(new_port_raw) 667 except ValueError: 668 # Notify about incorrect port? I'm lazy; simply leave old value. 669 pass 670 bui.app.config.commit() 671 bui.getsound('gunCocking').play() 672 self._refresh_favorites() 673 674 bui.containerwidget( 675 edit=self._favorite_edit_window, transition='out_scale' 676 ) 677 678 def _on_favorite_delete_press(self) -> None: 679 from bauiv1lib import confirm 680 681 if self._favorite_selected is None: 682 self._no_favorite_selected_error() 683 return 684 confirm.ConfirmWindow( 685 bui.Lstr( 686 resource='gameListWindow.deleteConfirmText', 687 subs=[ 688 ( 689 '${LIST}', 690 bui.app.config['Saved Servers'][ 691 self._favorite_selected 692 ]['name'], 693 ) 694 ], 695 ), 696 self._delete_saved_party, 697 450, 698 150, 699 ) 700 701 def _delete_saved_party(self) -> None: 702 if self._favorite_selected is None: 703 self._no_favorite_selected_error() 704 return 705 config = bui.app.config['Saved Servers'] 706 del config[self._favorite_selected] 707 self._favorite_selected = None 708 bui.app.config.commit() 709 bui.getsound('shieldDown').play() 710 self._refresh_favorites() 711 712 def _on_favorite_select(self, server: str) -> None: 713 self._favorite_selected = server 714 715 def _refresh_favorites(self) -> None: 716 assert self._columnwidget is not None 717 for child in self._columnwidget.get_children(): 718 child.delete() 719 t_scale = 1.6 720 721 config = bui.app.config 722 if 'Saved Servers' in config: 723 servers = config['Saved Servers'] 724 725 else: 726 servers = [] 727 728 assert self._favorites_scroll_width is not None 729 assert self._favorites_connect_button is not None 730 731 bui.textwidget( 732 edit=self._no_parties_added_text, 733 text='', 734 ) 735 num_of_fav = 0 736 for i, server in enumerate(servers): 737 txt = bui.textwidget( 738 parent=self._columnwidget, 739 size=(self._favorites_scroll_width / t_scale, 30), 740 selectable=True, 741 color=(1.0, 1, 0.4), 742 always_highlight=True, 743 on_select_call=bui.Call(self._on_favorite_select, server), 744 on_activate_call=self._favorites_connect_button.activate, 745 text=( 746 config['Saved Servers'][server]['name'] 747 if config['Saved Servers'][server]['name'] != '' 748 else config['Saved Servers'][server]['addr'] 749 + ' ' 750 + str(config['Saved Servers'][server]['port']) 751 ), 752 h_align='left', 753 v_align='center', 754 corner_scale=t_scale, 755 maxwidth=(self._favorites_scroll_width / t_scale) * 0.93, 756 ) 757 if i == 0: 758 bui.widget(edit=txt, up_widget=self._favorites_text) 759 self._favorite_selected = server 760 bui.widget( 761 edit=txt, 762 left_widget=self._favorites_connect_button, 763 right_widget=txt, 764 ) 765 num_of_fav = num_of_fav + 1 766 767 # If there's no servers, allow selecting out of the scroll area 768 bui.containerwidget( 769 edit=self._scrollwidget, 770 claims_left_right=bool(servers), 771 claims_up_down=bool(servers), 772 ) 773 assert self._scrollwidget is not None 774 bui.widget( 775 edit=self._scrollwidget, 776 up_widget=self._favorites_text, 777 left_widget=self._favorites_connect_button, 778 ) 779 if num_of_fav == 0: 780 bui.textwidget( 781 edit=self._no_parties_added_text, 782 text=bui.Lstr(resource='gatherWindow.noPartiesAddedText'), 783 ) 784 785 @override 786 def on_deactivate(self) -> None: 787 self._access_check_timer = None 788 789 def _connect( 790 self, textwidget: bui.Widget, port_textwidget: bui.Widget 791 ) -> None: 792 addr = cast(str, bui.textwidget(query=textwidget)) 793 if addr == '': 794 bui.screenmessage( 795 bui.Lstr(resource='internal.invalidAddressErrorText'), 796 color=(1, 0, 0), 797 ) 798 bui.getsound('error').play() 799 return 800 try: 801 port = int(cast(str, bui.textwidget(query=port_textwidget))) 802 except ValueError: 803 port = -1 804 if port > 65535 or port < 0: 805 bui.screenmessage( 806 bui.Lstr(resource='internal.invalidPortErrorText'), 807 color=(1, 0, 0), 808 ) 809 bui.getsound('error').play() 810 return 811 812 _HostLookupThread( 813 name=addr, port=port, call=bui.WeakCall(self._host_lookup_result) 814 ).start() 815 816 def _save_server( 817 self, textwidget: bui.Widget, port_textwidget: bui.Widget 818 ) -> None: 819 addr = cast(str, bui.textwidget(query=textwidget)) 820 if addr == '': 821 bui.screenmessage( 822 bui.Lstr(resource='internal.invalidAddressErrorText'), 823 color=(1, 0, 0), 824 ) 825 bui.getsound('error').play() 826 return 827 try: 828 port = int(cast(str, bui.textwidget(query=port_textwidget))) 829 except ValueError: 830 port = -1 831 if port > 65535 or port < 0: 832 bui.screenmessage( 833 bui.Lstr(resource='internal.invalidPortErrorText'), 834 color=(1, 0, 0), 835 ) 836 bui.getsound('error').play() 837 return 838 config = bui.app.config 839 840 if addr: 841 if not isinstance(config.get('Saved Servers'), dict): 842 config['Saved Servers'] = {} 843 config['Saved Servers'][f'{addr}@{port}'] = { 844 'addr': addr, 845 'port': port, 846 'name': addr, 847 } 848 config.commit() 849 bui.getsound('gunCocking').play() 850 bui.screenmessage( 851 bui.Lstr( 852 resource='addedToFavoritesText', subs=[('${NAME}', addr)] 853 ), 854 color=(0, 1, 0), 855 ) 856 else: 857 bui.screenmessage( 858 bui.Lstr(resource='internal.invalidAddressErrorText'), 859 color=(1, 0, 0), 860 ) 861 bui.getsound('error').play() 862 863 def _host_lookup_result( 864 self, resolved_address: str | None, port: int 865 ) -> None: 866 if resolved_address is None: 867 bui.screenmessage( 868 bui.Lstr(resource='internal.unableToResolveHostText'), 869 color=(1, 0, 0), 870 ) 871 bui.getsound('error').play() 872 else: 873 # Store for later. 874 config = bui.app.config 875 config['Last Manual Party Connect Address'] = resolved_address 876 config['Last Manual Party Connect Port'] = port 877 config.commit() 878 879 # Store UI location to return to when done. 880 if bs.app.classic is not None: 881 bs.app.classic.save_ui_state() 882 883 bs.connect_to_party(resolved_address, port=port) 884 885 def _run_addr_fetch(self) -> None: 886 try: 887 # FIXME: Update this to work with IPv6. 888 import socket 889 890 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 891 sock.connect(('8.8.8.8', 80)) 892 val = sock.getsockname()[0] 893 sock.close() 894 bui.pushcall( 895 bui.Call( 896 _safe_set_text, 897 self._checking_state_text, 898 val, 899 ), 900 from_other_thread=True, 901 ) 902 except Exception as exc: 903 from efro.error import is_udp_communication_error 904 905 if is_udp_communication_error(exc): 906 bui.pushcall( 907 bui.Call( 908 _safe_set_text, 909 self._checking_state_text, 910 bui.Lstr(resource='gatherWindow.' 'noConnectionText'), 911 False, 912 ), 913 from_other_thread=True, 914 ) 915 else: 916 bui.pushcall( 917 bui.Call( 918 _safe_set_text, 919 self._checking_state_text, 920 bui.Lstr( 921 resource='gatherWindow.' 'addressFetchErrorText' 922 ), 923 False, 924 ), 925 from_other_thread=True, 926 ) 927 logging.exception('Error in AddrFetchThread.') 928 929 def _on_show_my_address_button_press( 930 self, v2: float, container: bui.Widget | None, c_width: float 931 ) -> None: 932 if not container: 933 return 934 935 tscl = 0.85 936 tspc = 25 937 938 bui.getsound('swish').play() 939 bui.textwidget( 940 parent=container, 941 position=(c_width * 0.5 - 10, v2), 942 color=(0.6, 1.0, 0.6), 943 scale=tscl, 944 size=(0, 0), 945 maxwidth=c_width * 0.45, 946 flatness=1.0, 947 h_align='right', 948 v_align='center', 949 text=bui.Lstr( 950 resource='gatherWindow.' 'manualYourLocalAddressText' 951 ), 952 ) 953 self._checking_state_text = bui.textwidget( 954 parent=container, 955 position=(c_width * 0.5, v2), 956 color=(0.5, 0.5, 0.5), 957 scale=tscl, 958 size=(0, 0), 959 maxwidth=c_width * 0.45, 960 flatness=1.0, 961 h_align='left', 962 v_align='center', 963 text=bui.Lstr(resource='gatherWindow.' 'checkingText'), 964 ) 965 966 Thread(target=self._run_addr_fetch).start() 967 968 v2 -= tspc 969 bui.textwidget( 970 parent=container, 971 position=(c_width * 0.5 - 10, v2), 972 color=(0.6, 1.0, 0.6), 973 scale=tscl, 974 size=(0, 0), 975 maxwidth=c_width * 0.45, 976 flatness=1.0, 977 h_align='right', 978 v_align='center', 979 text=bui.Lstr( 980 resource='gatherWindow.' 'manualYourAddressFromInternetText' 981 ), 982 ) 983 984 t_addr = bui.textwidget( 985 parent=container, 986 position=(c_width * 0.5, v2), 987 color=(0.5, 0.5, 0.5), 988 scale=tscl, 989 size=(0, 0), 990 maxwidth=c_width * 0.45, 991 h_align='left', 992 v_align='center', 993 flatness=1.0, 994 text=bui.Lstr(resource='gatherWindow.' 'checkingText'), 995 ) 996 v2 -= tspc 997 bui.textwidget( 998 parent=container, 999 position=(c_width * 0.5 - 10, v2), 1000 color=(0.6, 1.0, 0.6), 1001 scale=tscl, 1002 size=(0, 0), 1003 maxwidth=c_width * 0.45, 1004 flatness=1.0, 1005 h_align='right', 1006 v_align='center', 1007 text=bui.Lstr( 1008 resource='gatherWindow.' 'manualJoinableFromInternetText' 1009 ), 1010 ) 1011 1012 t_accessible = bui.textwidget( 1013 parent=container, 1014 position=(c_width * 0.5, v2), 1015 color=(0.5, 0.5, 0.5), 1016 scale=tscl, 1017 size=(0, 0), 1018 maxwidth=c_width * 0.45, 1019 flatness=1.0, 1020 h_align='left', 1021 v_align='center', 1022 text=bui.Lstr(resource='gatherWindow.' 'checkingText'), 1023 ) 1024 v2 -= 28 1025 t_accessible_extra = bui.textwidget( 1026 parent=container, 1027 position=(c_width * 0.5, v2), 1028 color=(1, 0.5, 0.2), 1029 scale=0.7, 1030 size=(0, 0), 1031 maxwidth=c_width * 0.9, 1032 flatness=1.0, 1033 h_align='center', 1034 v_align='center', 1035 text='', 1036 ) 1037 1038 self._doing_access_check = False 1039 self._access_check_count = 0 # Cap our refreshes eventually. 1040 self._access_check_timer = bui.AppTimer( 1041 10.0, 1042 bui.WeakCall( 1043 self._access_check_update, 1044 t_addr, 1045 t_accessible, 1046 t_accessible_extra, 1047 ), 1048 repeat=True, 1049 ) 1050 1051 # Kick initial off. 1052 self._access_check_update(t_addr, t_accessible, t_accessible_extra) 1053 if self._check_button: 1054 self._check_button.delete() 1055 1056 def _access_check_update( 1057 self, 1058 t_addr: bui.Widget, 1059 t_accessible: bui.Widget, 1060 t_accessible_extra: bui.Widget, 1061 ) -> None: 1062 assert bui.app.classic is not None 1063 # If we don't have an outstanding query, start one.. 1064 assert self._doing_access_check is not None 1065 assert self._access_check_count is not None 1066 if not self._doing_access_check and self._access_check_count < 100: 1067 self._doing_access_check = True 1068 self._access_check_count += 1 1069 self._t_addr = t_addr 1070 self._t_accessible = t_accessible 1071 self._t_accessible_extra = t_accessible_extra 1072 bui.app.classic.master_server_v1_get( 1073 'bsAccessCheck', 1074 {'b': bui.app.env.engine_build_number}, 1075 callback=bui.WeakCall(self._on_accessible_response), 1076 ) 1077 1078 def _on_accessible_response(self, data: dict[str, Any] | None) -> None: 1079 t_addr = self._t_addr 1080 t_accessible = self._t_accessible 1081 t_accessible_extra = self._t_accessible_extra 1082 self._doing_access_check = False 1083 color_bad = (1, 1, 0) 1084 color_good = (0, 1, 0) 1085 if data is None or 'address' not in data or 'accessible' not in data: 1086 if t_addr: 1087 bui.textwidget( 1088 edit=t_addr, 1089 text=bui.Lstr(resource='gatherWindow.' 'noConnectionText'), 1090 color=color_bad, 1091 ) 1092 if t_accessible: 1093 bui.textwidget( 1094 edit=t_accessible, 1095 text=bui.Lstr(resource='gatherWindow.' 'noConnectionText'), 1096 color=color_bad, 1097 ) 1098 if t_accessible_extra: 1099 bui.textwidget( 1100 edit=t_accessible_extra, text='', color=color_bad 1101 ) 1102 return 1103 if t_addr: 1104 bui.textwidget(edit=t_addr, text=data['address'], color=color_good) 1105 if t_accessible: 1106 if data['accessible']: 1107 bui.textwidget( 1108 edit=t_accessible, 1109 text=bui.Lstr( 1110 resource='gatherWindow.' 'manualJoinableYesText' 1111 ), 1112 color=color_good, 1113 ) 1114 if t_accessible_extra: 1115 bui.textwidget( 1116 edit=t_accessible_extra, text='', color=color_good 1117 ) 1118 else: 1119 bui.textwidget( 1120 edit=t_accessible, 1121 text=bui.Lstr( 1122 resource='gatherWindow.' 1123 'manualJoinableNoWithAsteriskText' 1124 ), 1125 color=color_bad, 1126 ) 1127 if t_accessible_extra: 1128 bui.textwidget( 1129 edit=t_accessible_extra, 1130 text=bui.Lstr( 1131 resource='gatherWindow.' 1132 'manualRouterForwardingText', 1133 subs=[ 1134 ('${PORT}', str(bs.get_game_port())), 1135 ], 1136 ), 1137 color=color_bad, 1138 )
class
SubTabType(enum.Enum):
66class SubTabType(Enum): 67 """Available sub-tabs.""" 68 69 JOIN_BY_ADDRESS = 'join_by_address' 70 FAVORITES = 'favorites'
Available sub-tabs.
JOIN_BY_ADDRESS =
<SubTabType.JOIN_BY_ADDRESS: 'join_by_address'>
FAVORITES =
<SubTabType.FAVORITES: 'favorites'>
@dataclass
class
State:
73@dataclass 74class State: 75 """State saved/restored only while the app is running.""" 76 77 sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS
State saved/restored only while the app is running.
State( sub_tab: SubTabType = <SubTabType.JOIN_BY_ADDRESS: 'join_by_address'>)
80class ManualGatherTab(GatherTab): 81 """The manual tab in the gather UI""" 82 83 def __init__(self, window: GatherWindow) -> None: 84 super().__init__(window) 85 self._check_button: bui.Widget | None = None 86 self._doing_access_check: bool | None = None 87 self._access_check_count: int | None = None 88 self._sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS 89 self._t_addr: bui.Widget | None = None 90 self._t_accessible: bui.Widget | None = None 91 self._t_accessible_extra: bui.Widget | None = None 92 self._access_check_timer: bui.AppTimer | None = None 93 self._checking_state_text: bui.Widget | None = None 94 self._container: bui.Widget | None = None 95 self._join_by_address_text: bui.Widget | None = None 96 self._favorites_text: bui.Widget | None = None 97 self._width: float | None = None 98 self._height: float | None = None 99 self._scroll_width: float | None = None 100 self._scroll_height: float | None = None 101 self._favorites_scroll_width: int | None = None 102 self._favorites_connect_button: bui.Widget | None = None 103 self._scrollwidget: bui.Widget | None = None 104 self._columnwidget: bui.Widget | None = None 105 self._favorite_selected: str | None = None 106 self._favorite_edit_window: bui.Widget | None = None 107 self._party_edit_name_text: bui.Widget | None = None 108 self._party_edit_addr_text: bui.Widget | None = None 109 self._party_edit_port_text: bui.Widget | None = None 110 self._no_parties_added_text: bui.Widget | None = None 111 112 @override 113 def on_activate( 114 self, 115 parent_widget: bui.Widget, 116 tab_button: bui.Widget, 117 region_width: float, 118 region_height: float, 119 region_left: float, 120 region_bottom: float, 121 ) -> bui.Widget: 122 # pylint: disable=too-many-positional-arguments 123 c_width = region_width 124 c_height = region_height - 20 125 126 self._container = bui.containerwidget( 127 parent=parent_widget, 128 position=( 129 region_left, 130 region_bottom + (region_height - c_height) * 0.5, 131 ), 132 size=(c_width, c_height), 133 background=False, 134 selection_loops_to_parent=True, 135 ) 136 v = c_height - 30 137 self._join_by_address_text = bui.textwidget( 138 parent=self._container, 139 position=(c_width * 0.5 - 245, v - 13), 140 color=(0.6, 1.0, 0.6), 141 scale=1.3, 142 size=(200, 30), 143 maxwidth=250, 144 h_align='center', 145 v_align='center', 146 click_activate=True, 147 selectable=True, 148 autoselect=True, 149 on_activate_call=lambda: self._set_sub_tab( 150 SubTabType.JOIN_BY_ADDRESS, 151 region_width, 152 region_height, 153 playsound=True, 154 ), 155 text=bui.Lstr(resource='gatherWindow.manualJoinSectionText'), 156 glow_type='uniform', 157 ) 158 self._favorites_text = bui.textwidget( 159 parent=self._container, 160 position=(c_width * 0.5 + 45, v - 13), 161 color=(0.6, 1.0, 0.6), 162 scale=1.3, 163 size=(200, 30), 164 maxwidth=250, 165 h_align='center', 166 v_align='center', 167 click_activate=True, 168 selectable=True, 169 autoselect=True, 170 on_activate_call=lambda: self._set_sub_tab( 171 SubTabType.FAVORITES, 172 region_width, 173 region_height, 174 playsound=True, 175 ), 176 text=bui.Lstr(resource='gatherWindow.favoritesText'), 177 glow_type='uniform', 178 ) 179 bui.widget(edit=self._join_by_address_text, up_widget=tab_button) 180 bui.widget( 181 edit=self._favorites_text, 182 left_widget=self._join_by_address_text, 183 up_widget=tab_button, 184 ) 185 bui.widget(edit=tab_button, down_widget=self._favorites_text) 186 bui.widget( 187 edit=self._join_by_address_text, right_widget=self._favorites_text 188 ) 189 self._set_sub_tab(self._sub_tab, region_width, region_height) 190 191 return self._container 192 193 @override 194 def save_state(self) -> None: 195 assert bui.app.classic is not None 196 bui.app.ui_v1.window_states[type(self)] = State(sub_tab=self._sub_tab) 197 198 @override 199 def restore_state(self) -> None: 200 assert bui.app.classic is not None 201 state = bui.app.ui_v1.window_states.get(type(self)) 202 if state is None: 203 state = State() 204 assert isinstance(state, State) 205 self._sub_tab = state.sub_tab 206 207 def _set_sub_tab( 208 self, 209 value: SubTabType, 210 region_width: float, 211 region_height: float, 212 playsound: bool = False, 213 ) -> None: 214 assert self._container 215 if playsound: 216 bui.getsound('click01').play() 217 218 self._sub_tab = value 219 active_color = (0.6, 1.0, 0.6) 220 inactive_color = (0.5, 0.4, 0.5) 221 bui.textwidget( 222 edit=self._join_by_address_text, 223 color=( 224 active_color 225 if value is SubTabType.JOIN_BY_ADDRESS 226 else inactive_color 227 ), 228 ) 229 bui.textwidget( 230 edit=self._favorites_text, 231 color=( 232 active_color 233 if value is SubTabType.FAVORITES 234 else inactive_color 235 ), 236 ) 237 238 # Clear anything existing in the old sub-tab. 239 for widget in self._container.get_children(): 240 if widget and widget not in { 241 self._favorites_text, 242 self._join_by_address_text, 243 }: 244 widget.delete() 245 246 if value is SubTabType.JOIN_BY_ADDRESS: 247 self._build_join_by_address_tab(region_width, region_height) 248 249 if value is SubTabType.FAVORITES: 250 self._build_favorites_tab(region_width, region_height) 251 252 # The old manual tab 253 def _build_join_by_address_tab( 254 self, region_width: float, region_height: float 255 ) -> None: 256 c_width = region_width 257 c_height = region_height - 20 258 last_addr = bui.app.config.get('Last Manual Party Connect Address', '') 259 last_port = bui.app.config.get('Last Manual Party Connect Port', 43210) 260 v = c_height - 70 261 v -= 70 262 bui.textwidget( 263 parent=self._container, 264 position=(c_width * 0.5 - 260 - 50, v), 265 color=(0.6, 1.0, 0.6), 266 scale=1.0, 267 size=(0, 0), 268 maxwidth=130, 269 h_align='right', 270 v_align='center', 271 text=bui.Lstr(resource='gatherWindow.' 'manualAddressText'), 272 ) 273 txt = bui.textwidget( 274 parent=self._container, 275 editable=True, 276 description=bui.Lstr(resource='gatherWindow.' 'manualAddressText'), 277 position=(c_width * 0.5 - 240 - 50, v - 30), 278 text=last_addr, 279 autoselect=True, 280 v_align='center', 281 scale=1.0, 282 maxwidth=380, 283 size=(420, 60), 284 ) 285 assert self._join_by_address_text is not None 286 bui.widget(edit=self._join_by_address_text, down_widget=txt) 287 assert self._favorites_text is not None 288 bui.widget(edit=self._favorites_text, down_widget=txt) 289 bui.textwidget( 290 parent=self._container, 291 position=(c_width * 0.5 - 260 + 490, v), 292 color=(0.6, 1.0, 0.6), 293 scale=1.0, 294 size=(0, 0), 295 maxwidth=80, 296 h_align='right', 297 v_align='center', 298 text=bui.Lstr(resource='gatherWindow.' 'portText'), 299 ) 300 txt2 = bui.textwidget( 301 parent=self._container, 302 editable=True, 303 description=bui.Lstr(resource='gatherWindow.' 'portText'), 304 text=str(last_port), 305 autoselect=True, 306 max_chars=5, 307 position=(c_width * 0.5 - 240 + 490, v - 30), 308 v_align='center', 309 scale=1.0, 310 size=(170, 60), 311 ) 312 313 v -= 110 314 315 btn = bui.buttonwidget( 316 parent=self._container, 317 size=(300, 70), 318 label=bui.Lstr(resource='gatherWindow.' 'manualConnectText'), 319 position=(c_width * 0.5 - 300, v), 320 autoselect=True, 321 on_activate_call=bui.Call(self._connect, txt, txt2), 322 ) 323 savebutton = bui.buttonwidget( 324 parent=self._container, 325 size=(300, 70), 326 label=bui.Lstr(resource='gatherWindow.favoritesSaveText'), 327 position=(c_width * 0.5 - 240 + 490 - 200, v), 328 autoselect=True, 329 on_activate_call=bui.Call(self._save_server, txt, txt2), 330 ) 331 bui.widget(edit=btn, right_widget=savebutton) 332 bui.widget(edit=savebutton, left_widget=btn, up_widget=txt2) 333 bui.textwidget(edit=txt, on_return_press_call=btn.activate) 334 bui.textwidget(edit=txt2, on_return_press_call=btn.activate) 335 v -= 45 336 337 self._check_button = bui.textwidget( 338 parent=self._container, 339 size=(250, 60), 340 text=bui.Lstr(resource='gatherWindow.showMyAddressText'), 341 v_align='center', 342 h_align='center', 343 click_activate=True, 344 position=(c_width * 0.5 - 125, v - 30), 345 autoselect=True, 346 color=(0.5, 0.9, 0.5), 347 scale=0.8, 348 selectable=True, 349 on_activate_call=bui.Call( 350 self._on_show_my_address_button_press, 351 v, 352 self._container, 353 c_width, 354 ), 355 glow_type='uniform', 356 ) 357 bui.widget(edit=self._check_button, up_widget=btn) 358 359 # Tab containing saved favorite addresses 360 def _build_favorites_tab( 361 self, region_width: float, region_height: float 362 ) -> None: 363 c_height = region_height - 20 364 v = c_height - 35 - 25 - 30 365 366 assert bui.app.classic is not None 367 uiscale = bui.app.ui_v1.uiscale 368 # self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040 369 self._width = region_width 370 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 371 self._height = ( 372 578 373 if uiscale is bui.UIScale.SMALL 374 else 670 if uiscale is bui.UIScale.MEDIUM else 800 375 ) 376 377 self._scroll_width = self._width - 130 + 2 * x_inset 378 self._scroll_height = self._height - 180 379 x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 380 381 c_height = self._scroll_height - 20 382 sub_scroll_height = c_height - 63 383 self._favorites_scroll_width = sub_scroll_width = ( 384 680 if uiscale is bui.UIScale.SMALL else 640 385 ) 386 387 v = c_height - 30 388 389 b_width = 140 if uiscale is bui.UIScale.SMALL else 178 390 b_height = ( 391 107 392 if uiscale is bui.UIScale.SMALL 393 else 142 if uiscale is bui.UIScale.MEDIUM else 190 394 ) 395 b_space_extra = ( 396 0 397 if uiscale is bui.UIScale.SMALL 398 else -2 if uiscale is bui.UIScale.MEDIUM else -5 399 ) 400 401 btnv = ( 402 c_height 403 - ( 404 48 405 if uiscale is bui.UIScale.SMALL 406 else 45 if uiscale is bui.UIScale.MEDIUM else 40 407 ) 408 - b_height 409 ) 410 411 self._favorites_connect_button = btn1 = bui.buttonwidget( 412 parent=self._container, 413 size=(b_width, b_height), 414 position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv), 415 button_type='square', 416 color=(0.6, 0.53, 0.63), 417 textcolor=(0.75, 0.7, 0.8), 418 on_activate_call=self._on_favorites_connect_press, 419 text_scale=1.0 if uiscale is bui.UIScale.SMALL else 1.2, 420 label=bui.Lstr(resource='gatherWindow.manualConnectText'), 421 autoselect=True, 422 ) 423 if uiscale is bui.UIScale.SMALL: 424 bui.widget( 425 edit=btn1, 426 left_widget=bui.get_special_widget('back_button'), 427 ) 428 btnv -= b_height + b_space_extra 429 bui.buttonwidget( 430 parent=self._container, 431 size=(b_width, b_height), 432 position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv), 433 button_type='square', 434 color=(0.6, 0.53, 0.63), 435 textcolor=(0.75, 0.7, 0.8), 436 on_activate_call=self._on_favorites_edit_press, 437 text_scale=1.0 if uiscale is bui.UIScale.SMALL else 1.2, 438 label=bui.Lstr(resource='editText'), 439 autoselect=True, 440 ) 441 btnv -= b_height + b_space_extra 442 bui.buttonwidget( 443 parent=self._container, 444 size=(b_width, b_height), 445 position=(140 if uiscale is bui.UIScale.SMALL else 40, btnv), 446 button_type='square', 447 color=(0.6, 0.53, 0.63), 448 textcolor=(0.75, 0.7, 0.8), 449 on_activate_call=self._on_favorite_delete_press, 450 text_scale=1.0 if uiscale is bui.UIScale.SMALL else 1.2, 451 label=bui.Lstr(resource='deleteText'), 452 autoselect=True, 453 ) 454 455 v -= sub_scroll_height + 23 456 self._scrollwidget = scrlw = bui.scrollwidget( 457 parent=self._container, 458 position=(290 if uiscale is bui.UIScale.SMALL else 225, v), 459 size=(sub_scroll_width, sub_scroll_height), 460 claims_left_right=True, 461 ) 462 bui.widget( 463 edit=self._favorites_connect_button, right_widget=self._scrollwidget 464 ) 465 self._columnwidget = bui.columnwidget( 466 parent=scrlw, 467 left_border=10, 468 border=2, 469 margin=0, 470 claims_left_right=True, 471 ) 472 473 self._no_parties_added_text = bui.textwidget( 474 parent=self._container, 475 size=(0, 0), 476 h_align='center', 477 v_align='center', 478 text='', 479 color=(0.6, 0.6, 0.6), 480 scale=1.2, 481 position=( 482 ( 483 (240 if uiscale is bui.UIScale.SMALL else 225) 484 + sub_scroll_width * 0.5 485 ), 486 v + sub_scroll_height * 0.5, 487 ), 488 glow_type='uniform', 489 ) 490 491 self._favorite_selected = None 492 self._refresh_favorites() 493 494 def _no_favorite_selected_error(self) -> None: 495 bui.screenmessage( 496 bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) 497 ) 498 bui.getsound('error').play() 499 500 def _on_favorites_connect_press(self) -> None: 501 if self._favorite_selected is None: 502 self._no_favorite_selected_error() 503 504 else: 505 config = bui.app.config['Saved Servers'][self._favorite_selected] 506 _HostLookupThread( 507 name=config['addr'], 508 port=config['port'], 509 call=bui.WeakCall(self._host_lookup_result), 510 ).start() 511 512 def _on_favorites_edit_press(self) -> None: 513 if self._favorite_selected is None: 514 self._no_favorite_selected_error() 515 return 516 517 c_width = 600 518 c_height = 310 519 assert bui.app.classic is not None 520 uiscale = bui.app.ui_v1.uiscale 521 self._favorite_edit_window = cnt = bui.containerwidget( 522 scale=( 523 1.8 524 if uiscale is bui.UIScale.SMALL 525 else 1.55 if uiscale is bui.UIScale.MEDIUM else 1.0 526 ), 527 size=(c_width, c_height), 528 transition='in_scale', 529 ) 530 531 bui.textwidget( 532 parent=cnt, 533 size=(0, 0), 534 h_align='center', 535 v_align='center', 536 text=bui.Lstr(resource='editText'), 537 color=(0.6, 1.0, 0.6), 538 maxwidth=c_width * 0.8, 539 position=(c_width * 0.5, c_height - 60), 540 ) 541 542 bui.textwidget( 543 parent=cnt, 544 position=(c_width * 0.2 - 15, c_height - 120), 545 color=(0.6, 1.0, 0.6), 546 scale=1.0, 547 size=(0, 0), 548 maxwidth=60, 549 h_align='right', 550 v_align='center', 551 text=bui.Lstr(resource='nameText'), 552 ) 553 554 self._party_edit_name_text = bui.textwidget( 555 parent=cnt, 556 size=(c_width * 0.7, 40), 557 h_align='left', 558 v_align='center', 559 text=bui.app.config['Saved Servers'][self._favorite_selected][ 560 'name' 561 ], 562 editable=True, 563 description=bui.Lstr(resource='nameText'), 564 position=(c_width * 0.2, c_height - 140), 565 autoselect=True, 566 maxwidth=c_width * 0.6, 567 max_chars=200, 568 ) 569 570 bui.textwidget( 571 parent=cnt, 572 position=(c_width * 0.2 - 15, c_height - 180), 573 color=(0.6, 1.0, 0.6), 574 scale=1.0, 575 size=(0, 0), 576 maxwidth=60, 577 h_align='right', 578 v_align='center', 579 text=bui.Lstr(resource='gatherWindow.' 'manualAddressText'), 580 ) 581 582 self._party_edit_addr_text = bui.textwidget( 583 parent=cnt, 584 size=(c_width * 0.4, 40), 585 h_align='left', 586 v_align='center', 587 text=bui.app.config['Saved Servers'][self._favorite_selected][ 588 'addr' 589 ], 590 editable=True, 591 description=bui.Lstr(resource='gatherWindow.manualAddressText'), 592 position=(c_width * 0.2, c_height - 200), 593 autoselect=True, 594 maxwidth=c_width * 0.35, 595 max_chars=200, 596 ) 597 598 bui.textwidget( 599 parent=cnt, 600 position=(c_width * 0.7 - 10, c_height - 180), 601 color=(0.6, 1.0, 0.6), 602 scale=1.0, 603 size=(0, 0), 604 maxwidth=45, 605 h_align='right', 606 v_align='center', 607 text=bui.Lstr(resource='gatherWindow.' 'portText'), 608 ) 609 610 self._party_edit_port_text = bui.textwidget( 611 parent=cnt, 612 size=(c_width * 0.2, 40), 613 h_align='left', 614 v_align='center', 615 text=str( 616 bui.app.config['Saved Servers'][self._favorite_selected]['port'] 617 ), 618 editable=True, 619 description=bui.Lstr(resource='gatherWindow.portText'), 620 position=(c_width * 0.7, c_height - 200), 621 autoselect=True, 622 maxwidth=c_width * 0.2, 623 max_chars=6, 624 ) 625 cbtn = bui.buttonwidget( 626 parent=cnt, 627 label=bui.Lstr(resource='cancelText'), 628 on_activate_call=bui.Call( 629 lambda c: bui.containerwidget(edit=c, transition='out_scale'), 630 cnt, 631 ), 632 size=(180, 60), 633 position=(30, 30), 634 autoselect=True, 635 ) 636 okb = bui.buttonwidget( 637 parent=cnt, 638 label=bui.Lstr(resource='saveText'), 639 size=(180, 60), 640 position=(c_width - 230, 30), 641 on_activate_call=bui.Call(self._edit_saved_party), 642 autoselect=True, 643 ) 644 bui.widget(edit=cbtn, right_widget=okb) 645 bui.widget(edit=okb, left_widget=cbtn) 646 bui.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) 647 648 def _edit_saved_party(self) -> None: 649 server = self._favorite_selected 650 if self._favorite_selected is None: 651 self._no_favorite_selected_error() 652 return 653 if not self._party_edit_name_text or not self._party_edit_addr_text: 654 return 655 new_name_raw = cast( 656 str, bui.textwidget(query=self._party_edit_name_text) 657 ) 658 new_addr_raw = cast( 659 str, bui.textwidget(query=self._party_edit_addr_text) 660 ) 661 new_port_raw = cast( 662 str, bui.textwidget(query=self._party_edit_port_text) 663 ) 664 bui.app.config['Saved Servers'][server]['name'] = new_name_raw 665 bui.app.config['Saved Servers'][server]['addr'] = new_addr_raw 666 try: 667 bui.app.config['Saved Servers'][server]['port'] = int(new_port_raw) 668 except ValueError: 669 # Notify about incorrect port? I'm lazy; simply leave old value. 670 pass 671 bui.app.config.commit() 672 bui.getsound('gunCocking').play() 673 self._refresh_favorites() 674 675 bui.containerwidget( 676 edit=self._favorite_edit_window, transition='out_scale' 677 ) 678 679 def _on_favorite_delete_press(self) -> None: 680 from bauiv1lib import confirm 681 682 if self._favorite_selected is None: 683 self._no_favorite_selected_error() 684 return 685 confirm.ConfirmWindow( 686 bui.Lstr( 687 resource='gameListWindow.deleteConfirmText', 688 subs=[ 689 ( 690 '${LIST}', 691 bui.app.config['Saved Servers'][ 692 self._favorite_selected 693 ]['name'], 694 ) 695 ], 696 ), 697 self._delete_saved_party, 698 450, 699 150, 700 ) 701 702 def _delete_saved_party(self) -> None: 703 if self._favorite_selected is None: 704 self._no_favorite_selected_error() 705 return 706 config = bui.app.config['Saved Servers'] 707 del config[self._favorite_selected] 708 self._favorite_selected = None 709 bui.app.config.commit() 710 bui.getsound('shieldDown').play() 711 self._refresh_favorites() 712 713 def _on_favorite_select(self, server: str) -> None: 714 self._favorite_selected = server 715 716 def _refresh_favorites(self) -> None: 717 assert self._columnwidget is not None 718 for child in self._columnwidget.get_children(): 719 child.delete() 720 t_scale = 1.6 721 722 config = bui.app.config 723 if 'Saved Servers' in config: 724 servers = config['Saved Servers'] 725 726 else: 727 servers = [] 728 729 assert self._favorites_scroll_width is not None 730 assert self._favorites_connect_button is not None 731 732 bui.textwidget( 733 edit=self._no_parties_added_text, 734 text='', 735 ) 736 num_of_fav = 0 737 for i, server in enumerate(servers): 738 txt = bui.textwidget( 739 parent=self._columnwidget, 740 size=(self._favorites_scroll_width / t_scale, 30), 741 selectable=True, 742 color=(1.0, 1, 0.4), 743 always_highlight=True, 744 on_select_call=bui.Call(self._on_favorite_select, server), 745 on_activate_call=self._favorites_connect_button.activate, 746 text=( 747 config['Saved Servers'][server]['name'] 748 if config['Saved Servers'][server]['name'] != '' 749 else config['Saved Servers'][server]['addr'] 750 + ' ' 751 + str(config['Saved Servers'][server]['port']) 752 ), 753 h_align='left', 754 v_align='center', 755 corner_scale=t_scale, 756 maxwidth=(self._favorites_scroll_width / t_scale) * 0.93, 757 ) 758 if i == 0: 759 bui.widget(edit=txt, up_widget=self._favorites_text) 760 self._favorite_selected = server 761 bui.widget( 762 edit=txt, 763 left_widget=self._favorites_connect_button, 764 right_widget=txt, 765 ) 766 num_of_fav = num_of_fav + 1 767 768 # If there's no servers, allow selecting out of the scroll area 769 bui.containerwidget( 770 edit=self._scrollwidget, 771 claims_left_right=bool(servers), 772 claims_up_down=bool(servers), 773 ) 774 assert self._scrollwidget is not None 775 bui.widget( 776 edit=self._scrollwidget, 777 up_widget=self._favorites_text, 778 left_widget=self._favorites_connect_button, 779 ) 780 if num_of_fav == 0: 781 bui.textwidget( 782 edit=self._no_parties_added_text, 783 text=bui.Lstr(resource='gatherWindow.noPartiesAddedText'), 784 ) 785 786 @override 787 def on_deactivate(self) -> None: 788 self._access_check_timer = None 789 790 def _connect( 791 self, textwidget: bui.Widget, port_textwidget: bui.Widget 792 ) -> None: 793 addr = cast(str, bui.textwidget(query=textwidget)) 794 if addr == '': 795 bui.screenmessage( 796 bui.Lstr(resource='internal.invalidAddressErrorText'), 797 color=(1, 0, 0), 798 ) 799 bui.getsound('error').play() 800 return 801 try: 802 port = int(cast(str, bui.textwidget(query=port_textwidget))) 803 except ValueError: 804 port = -1 805 if port > 65535 or port < 0: 806 bui.screenmessage( 807 bui.Lstr(resource='internal.invalidPortErrorText'), 808 color=(1, 0, 0), 809 ) 810 bui.getsound('error').play() 811 return 812 813 _HostLookupThread( 814 name=addr, port=port, call=bui.WeakCall(self._host_lookup_result) 815 ).start() 816 817 def _save_server( 818 self, textwidget: bui.Widget, port_textwidget: bui.Widget 819 ) -> None: 820 addr = cast(str, bui.textwidget(query=textwidget)) 821 if addr == '': 822 bui.screenmessage( 823 bui.Lstr(resource='internal.invalidAddressErrorText'), 824 color=(1, 0, 0), 825 ) 826 bui.getsound('error').play() 827 return 828 try: 829 port = int(cast(str, bui.textwidget(query=port_textwidget))) 830 except ValueError: 831 port = -1 832 if port > 65535 or port < 0: 833 bui.screenmessage( 834 bui.Lstr(resource='internal.invalidPortErrorText'), 835 color=(1, 0, 0), 836 ) 837 bui.getsound('error').play() 838 return 839 config = bui.app.config 840 841 if addr: 842 if not isinstance(config.get('Saved Servers'), dict): 843 config['Saved Servers'] = {} 844 config['Saved Servers'][f'{addr}@{port}'] = { 845 'addr': addr, 846 'port': port, 847 'name': addr, 848 } 849 config.commit() 850 bui.getsound('gunCocking').play() 851 bui.screenmessage( 852 bui.Lstr( 853 resource='addedToFavoritesText', subs=[('${NAME}', addr)] 854 ), 855 color=(0, 1, 0), 856 ) 857 else: 858 bui.screenmessage( 859 bui.Lstr(resource='internal.invalidAddressErrorText'), 860 color=(1, 0, 0), 861 ) 862 bui.getsound('error').play() 863 864 def _host_lookup_result( 865 self, resolved_address: str | None, port: int 866 ) -> None: 867 if resolved_address is None: 868 bui.screenmessage( 869 bui.Lstr(resource='internal.unableToResolveHostText'), 870 color=(1, 0, 0), 871 ) 872 bui.getsound('error').play() 873 else: 874 # Store for later. 875 config = bui.app.config 876 config['Last Manual Party Connect Address'] = resolved_address 877 config['Last Manual Party Connect Port'] = port 878 config.commit() 879 880 # Store UI location to return to when done. 881 if bs.app.classic is not None: 882 bs.app.classic.save_ui_state() 883 884 bs.connect_to_party(resolved_address, port=port) 885 886 def _run_addr_fetch(self) -> None: 887 try: 888 # FIXME: Update this to work with IPv6. 889 import socket 890 891 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 892 sock.connect(('8.8.8.8', 80)) 893 val = sock.getsockname()[0] 894 sock.close() 895 bui.pushcall( 896 bui.Call( 897 _safe_set_text, 898 self._checking_state_text, 899 val, 900 ), 901 from_other_thread=True, 902 ) 903 except Exception as exc: 904 from efro.error import is_udp_communication_error 905 906 if is_udp_communication_error(exc): 907 bui.pushcall( 908 bui.Call( 909 _safe_set_text, 910 self._checking_state_text, 911 bui.Lstr(resource='gatherWindow.' 'noConnectionText'), 912 False, 913 ), 914 from_other_thread=True, 915 ) 916 else: 917 bui.pushcall( 918 bui.Call( 919 _safe_set_text, 920 self._checking_state_text, 921 bui.Lstr( 922 resource='gatherWindow.' 'addressFetchErrorText' 923 ), 924 False, 925 ), 926 from_other_thread=True, 927 ) 928 logging.exception('Error in AddrFetchThread.') 929 930 def _on_show_my_address_button_press( 931 self, v2: float, container: bui.Widget | None, c_width: float 932 ) -> None: 933 if not container: 934 return 935 936 tscl = 0.85 937 tspc = 25 938 939 bui.getsound('swish').play() 940 bui.textwidget( 941 parent=container, 942 position=(c_width * 0.5 - 10, v2), 943 color=(0.6, 1.0, 0.6), 944 scale=tscl, 945 size=(0, 0), 946 maxwidth=c_width * 0.45, 947 flatness=1.0, 948 h_align='right', 949 v_align='center', 950 text=bui.Lstr( 951 resource='gatherWindow.' 'manualYourLocalAddressText' 952 ), 953 ) 954 self._checking_state_text = bui.textwidget( 955 parent=container, 956 position=(c_width * 0.5, v2), 957 color=(0.5, 0.5, 0.5), 958 scale=tscl, 959 size=(0, 0), 960 maxwidth=c_width * 0.45, 961 flatness=1.0, 962 h_align='left', 963 v_align='center', 964 text=bui.Lstr(resource='gatherWindow.' 'checkingText'), 965 ) 966 967 Thread(target=self._run_addr_fetch).start() 968 969 v2 -= tspc 970 bui.textwidget( 971 parent=container, 972 position=(c_width * 0.5 - 10, v2), 973 color=(0.6, 1.0, 0.6), 974 scale=tscl, 975 size=(0, 0), 976 maxwidth=c_width * 0.45, 977 flatness=1.0, 978 h_align='right', 979 v_align='center', 980 text=bui.Lstr( 981 resource='gatherWindow.' 'manualYourAddressFromInternetText' 982 ), 983 ) 984 985 t_addr = bui.textwidget( 986 parent=container, 987 position=(c_width * 0.5, v2), 988 color=(0.5, 0.5, 0.5), 989 scale=tscl, 990 size=(0, 0), 991 maxwidth=c_width * 0.45, 992 h_align='left', 993 v_align='center', 994 flatness=1.0, 995 text=bui.Lstr(resource='gatherWindow.' 'checkingText'), 996 ) 997 v2 -= tspc 998 bui.textwidget( 999 parent=container, 1000 position=(c_width * 0.5 - 10, v2), 1001 color=(0.6, 1.0, 0.6), 1002 scale=tscl, 1003 size=(0, 0), 1004 maxwidth=c_width * 0.45, 1005 flatness=1.0, 1006 h_align='right', 1007 v_align='center', 1008 text=bui.Lstr( 1009 resource='gatherWindow.' 'manualJoinableFromInternetText' 1010 ), 1011 ) 1012 1013 t_accessible = bui.textwidget( 1014 parent=container, 1015 position=(c_width * 0.5, v2), 1016 color=(0.5, 0.5, 0.5), 1017 scale=tscl, 1018 size=(0, 0), 1019 maxwidth=c_width * 0.45, 1020 flatness=1.0, 1021 h_align='left', 1022 v_align='center', 1023 text=bui.Lstr(resource='gatherWindow.' 'checkingText'), 1024 ) 1025 v2 -= 28 1026 t_accessible_extra = bui.textwidget( 1027 parent=container, 1028 position=(c_width * 0.5, v2), 1029 color=(1, 0.5, 0.2), 1030 scale=0.7, 1031 size=(0, 0), 1032 maxwidth=c_width * 0.9, 1033 flatness=1.0, 1034 h_align='center', 1035 v_align='center', 1036 text='', 1037 ) 1038 1039 self._doing_access_check = False 1040 self._access_check_count = 0 # Cap our refreshes eventually. 1041 self._access_check_timer = bui.AppTimer( 1042 10.0, 1043 bui.WeakCall( 1044 self._access_check_update, 1045 t_addr, 1046 t_accessible, 1047 t_accessible_extra, 1048 ), 1049 repeat=True, 1050 ) 1051 1052 # Kick initial off. 1053 self._access_check_update(t_addr, t_accessible, t_accessible_extra) 1054 if self._check_button: 1055 self._check_button.delete() 1056 1057 def _access_check_update( 1058 self, 1059 t_addr: bui.Widget, 1060 t_accessible: bui.Widget, 1061 t_accessible_extra: bui.Widget, 1062 ) -> None: 1063 assert bui.app.classic is not None 1064 # If we don't have an outstanding query, start one.. 1065 assert self._doing_access_check is not None 1066 assert self._access_check_count is not None 1067 if not self._doing_access_check and self._access_check_count < 100: 1068 self._doing_access_check = True 1069 self._access_check_count += 1 1070 self._t_addr = t_addr 1071 self._t_accessible = t_accessible 1072 self._t_accessible_extra = t_accessible_extra 1073 bui.app.classic.master_server_v1_get( 1074 'bsAccessCheck', 1075 {'b': bui.app.env.engine_build_number}, 1076 callback=bui.WeakCall(self._on_accessible_response), 1077 ) 1078 1079 def _on_accessible_response(self, data: dict[str, Any] | None) -> None: 1080 t_addr = self._t_addr 1081 t_accessible = self._t_accessible 1082 t_accessible_extra = self._t_accessible_extra 1083 self._doing_access_check = False 1084 color_bad = (1, 1, 0) 1085 color_good = (0, 1, 0) 1086 if data is None or 'address' not in data or 'accessible' not in data: 1087 if t_addr: 1088 bui.textwidget( 1089 edit=t_addr, 1090 text=bui.Lstr(resource='gatherWindow.' 'noConnectionText'), 1091 color=color_bad, 1092 ) 1093 if t_accessible: 1094 bui.textwidget( 1095 edit=t_accessible, 1096 text=bui.Lstr(resource='gatherWindow.' 'noConnectionText'), 1097 color=color_bad, 1098 ) 1099 if t_accessible_extra: 1100 bui.textwidget( 1101 edit=t_accessible_extra, text='', color=color_bad 1102 ) 1103 return 1104 if t_addr: 1105 bui.textwidget(edit=t_addr, text=data['address'], color=color_good) 1106 if t_accessible: 1107 if data['accessible']: 1108 bui.textwidget( 1109 edit=t_accessible, 1110 text=bui.Lstr( 1111 resource='gatherWindow.' 'manualJoinableYesText' 1112 ), 1113 color=color_good, 1114 ) 1115 if t_accessible_extra: 1116 bui.textwidget( 1117 edit=t_accessible_extra, text='', color=color_good 1118 ) 1119 else: 1120 bui.textwidget( 1121 edit=t_accessible, 1122 text=bui.Lstr( 1123 resource='gatherWindow.' 1124 'manualJoinableNoWithAsteriskText' 1125 ), 1126 color=color_bad, 1127 ) 1128 if t_accessible_extra: 1129 bui.textwidget( 1130 edit=t_accessible_extra, 1131 text=bui.Lstr( 1132 resource='gatherWindow.' 1133 'manualRouterForwardingText', 1134 subs=[ 1135 ('${PORT}', str(bs.get_game_port())), 1136 ], 1137 ), 1138 color=color_bad, 1139 )
The manual tab in the gather UI
ManualGatherTab(window: bauiv1lib.gather.GatherWindow)
83 def __init__(self, window: GatherWindow) -> None: 84 super().__init__(window) 85 self._check_button: bui.Widget | None = None 86 self._doing_access_check: bool | None = None 87 self._access_check_count: int | None = None 88 self._sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS 89 self._t_addr: bui.Widget | None = None 90 self._t_accessible: bui.Widget | None = None 91 self._t_accessible_extra: bui.Widget | None = None 92 self._access_check_timer: bui.AppTimer | None = None 93 self._checking_state_text: bui.Widget | None = None 94 self._container: bui.Widget | None = None 95 self._join_by_address_text: bui.Widget | None = None 96 self._favorites_text: bui.Widget | None = None 97 self._width: float | None = None 98 self._height: float | None = None 99 self._scroll_width: float | None = None 100 self._scroll_height: float | None = None 101 self._favorites_scroll_width: int | None = None 102 self._favorites_connect_button: bui.Widget | None = None 103 self._scrollwidget: bui.Widget | None = None 104 self._columnwidget: bui.Widget | None = None 105 self._favorite_selected: str | None = None 106 self._favorite_edit_window: bui.Widget | None = None 107 self._party_edit_name_text: bui.Widget | None = None 108 self._party_edit_addr_text: bui.Widget | None = None 109 self._party_edit_port_text: bui.Widget | None = None 110 self._no_parties_added_text: bui.Widget | None = None
@override
def
on_activate( self, parent_widget: _bauiv1.Widget, tab_button: _bauiv1.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float) -> _bauiv1.Widget:
112 @override 113 def on_activate( 114 self, 115 parent_widget: bui.Widget, 116 tab_button: bui.Widget, 117 region_width: float, 118 region_height: float, 119 region_left: float, 120 region_bottom: float, 121 ) -> bui.Widget: 122 # pylint: disable=too-many-positional-arguments 123 c_width = region_width 124 c_height = region_height - 20 125 126 self._container = bui.containerwidget( 127 parent=parent_widget, 128 position=( 129 region_left, 130 region_bottom + (region_height - c_height) * 0.5, 131 ), 132 size=(c_width, c_height), 133 background=False, 134 selection_loops_to_parent=True, 135 ) 136 v = c_height - 30 137 self._join_by_address_text = bui.textwidget( 138 parent=self._container, 139 position=(c_width * 0.5 - 245, v - 13), 140 color=(0.6, 1.0, 0.6), 141 scale=1.3, 142 size=(200, 30), 143 maxwidth=250, 144 h_align='center', 145 v_align='center', 146 click_activate=True, 147 selectable=True, 148 autoselect=True, 149 on_activate_call=lambda: self._set_sub_tab( 150 SubTabType.JOIN_BY_ADDRESS, 151 region_width, 152 region_height, 153 playsound=True, 154 ), 155 text=bui.Lstr(resource='gatherWindow.manualJoinSectionText'), 156 glow_type='uniform', 157 ) 158 self._favorites_text = bui.textwidget( 159 parent=self._container, 160 position=(c_width * 0.5 + 45, v - 13), 161 color=(0.6, 1.0, 0.6), 162 scale=1.3, 163 size=(200, 30), 164 maxwidth=250, 165 h_align='center', 166 v_align='center', 167 click_activate=True, 168 selectable=True, 169 autoselect=True, 170 on_activate_call=lambda: self._set_sub_tab( 171 SubTabType.FAVORITES, 172 region_width, 173 region_height, 174 playsound=True, 175 ), 176 text=bui.Lstr(resource='gatherWindow.favoritesText'), 177 glow_type='uniform', 178 ) 179 bui.widget(edit=self._join_by_address_text, up_widget=tab_button) 180 bui.widget( 181 edit=self._favorites_text, 182 left_widget=self._join_by_address_text, 183 up_widget=tab_button, 184 ) 185 bui.widget(edit=tab_button, down_widget=self._favorites_text) 186 bui.widget( 187 edit=self._join_by_address_text, right_widget=self._favorites_text 188 ) 189 self._set_sub_tab(self._sub_tab, region_width, region_height) 190 191 return self._container
Called when the tab becomes the active one.
The tab should create and return a container widget covering the specified region.
@override
def
save_state(self) -> None:
193 @override 194 def save_state(self) -> None: 195 assert bui.app.classic is not None 196 bui.app.ui_v1.window_states[type(self)] = State(sub_tab=self._sub_tab)
Called when the parent window is saving state.
@override
def
restore_state(self) -> None:
198 @override 199 def restore_state(self) -> None: 200 assert bui.app.classic is not None 201 state = bui.app.ui_v1.window_states.get(type(self)) 202 if state is None: 203 state = State() 204 assert isinstance(state, State) 205 self._sub_tab = state.sub_tab
Called when the parent window is restoring state.