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

The manual tab in the gather UI

ManualGatherTab(window: bauiv1lib.gather.GatherWindow)
 80    def __init__(self, window: GatherWindow) -> None:
 81        super().__init__(window)
 82        self._check_button: bui.Widget | None = None
 83        self._doing_access_check: bool | None = None
 84        self._access_check_count: int | None = None
 85        self._sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS
 86        self._t_addr: bui.Widget | None = None
 87        self._t_accessible: bui.Widget | None = None
 88        self._t_accessible_extra: bui.Widget | None = None
 89        self._access_check_timer: bui.AppTimer | None = None
 90        self._checking_state_text: bui.Widget | None = None
 91        self._container: bui.Widget | None = None
 92        self._join_by_address_text: bui.Widget | None = None
 93        self._favorites_text: bui.Widget | None = None
 94        self._width: int | None = None
 95        self._height: int | None = None
 96        self._scroll_width: int | None = None
 97        self._scroll_height: int | None = None
 98        self._favorites_scroll_width: int | None = None
 99        self._favorites_connect_button: bui.Widget | None = None
100        self._scrollwidget: bui.Widget | None = None
101        self._columnwidget: bui.Widget | None = None
102        self._favorite_selected: str | None = None
103        self._favorite_edit_window: bui.Widget | None = None
104        self._party_edit_name_text: bui.Widget | None = None
105        self._party_edit_addr_text: bui.Widget | None = None
106        self._party_edit_port_text: bui.Widget | None = None
107        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:
109    @override
110    def on_activate(
111        self,
112        parent_widget: bui.Widget,
113        tab_button: bui.Widget,
114        region_width: float,
115        region_height: float,
116        region_left: float,
117        region_bottom: float,
118    ) -> bui.Widget:
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.

@override
def on_deactivate(self) -> None:
776    @override
777    def on_deactivate(self) -> None:
778        self._access_check_timer = None

Called when the tab will no longer be the active one.