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