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