bauiv1lib.gather.manualtab

Defines the manual tab in the gather UI.

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

Available sub-tabs.

JOIN_BY_ADDRESS = <SubTabType.JOIN_BY_ADDRESS: 'join_by_address'>
FAVORITES = <SubTabType.FAVORITES: 'favorites'>
@dataclass
class State:
73@dataclass
74class State:
75    """State saved/restored only while the app is running."""
76
77    sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS

State saved/restored only while the app is running.

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

The manual tab in the gather UI

ManualGatherTab(window: bauiv1lib.gather.GatherWindow)
 83    def __init__(self, window: GatherWindow) -> None:
 84        super().__init__(window)
 85        self._check_button: bui.Widget | None = None
 86        self._doing_access_check: bool | None = None
 87        self._access_check_count: int | None = None
 88        self._sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS
 89        self._t_addr: bui.Widget | None = None
 90        self._t_accessible: bui.Widget | None = None
 91        self._t_accessible_extra: bui.Widget | None = None
 92        self._access_check_timer: bui.AppTimer | None = None
 93        self._checking_state_text: bui.Widget | None = None
 94        self._container: bui.Widget | None = None
 95        self._join_by_address_text: bui.Widget | None = None
 96        self._favorites_text: bui.Widget | None = None
 97        self._width: float | None = None
 98        self._height: float | None = None
 99        self._scroll_width: float | None = None
100        self._scroll_height: float | None = None
101        self._favorites_scroll_width: int | None = None
102        self._favorites_connect_button: bui.Widget | None = None
103        self._scrollwidget: bui.Widget | None = None
104        self._columnwidget: bui.Widget | None = None
105        self._favorite_selected: str | None = None
106        self._favorite_edit_window: bui.Widget | None = None
107        self._party_edit_name_text: bui.Widget | None = None
108        self._party_edit_addr_text: bui.Widget | None = None
109        self._party_edit_port_text: bui.Widget | None = None
110        self._no_parties_added_text: bui.Widget | None = None
@override
def on_activate( self, parent_widget: _bauiv1.Widget, tab_button: _bauiv1.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float) -> _bauiv1.Widget:
112    @override
113    def on_activate(
114        self,
115        parent_widget: bui.Widget,
116        tab_button: bui.Widget,
117        region_width: float,
118        region_height: float,
119        region_left: float,
120        region_bottom: float,
121    ) -> bui.Widget:
122        # pylint: disable=too-many-positional-arguments
123        c_width = region_width
124        c_height = region_height - 20
125
126        self._container = bui.containerwidget(
127            parent=parent_widget,
128            position=(
129                region_left,
130                region_bottom + (region_height - c_height) * 0.5,
131            ),
132            size=(c_width, c_height),
133            background=False,
134            selection_loops_to_parent=True,
135        )
136        v = c_height - 30
137        self._join_by_address_text = bui.textwidget(
138            parent=self._container,
139            position=(c_width * 0.5 - 245, v - 13),
140            color=(0.6, 1.0, 0.6),
141            scale=1.3,
142            size=(200, 30),
143            maxwidth=250,
144            h_align='center',
145            v_align='center',
146            click_activate=True,
147            selectable=True,
148            autoselect=True,
149            on_activate_call=lambda: self._set_sub_tab(
150                SubTabType.JOIN_BY_ADDRESS,
151                region_width,
152                region_height,
153                playsound=True,
154            ),
155            text=bui.Lstr(resource='gatherWindow.manualJoinSectionText'),
156            glow_type='uniform',
157        )
158        self._favorites_text = bui.textwidget(
159            parent=self._container,
160            position=(c_width * 0.5 + 45, v - 13),
161            color=(0.6, 1.0, 0.6),
162            scale=1.3,
163            size=(200, 30),
164            maxwidth=250,
165            h_align='center',
166            v_align='center',
167            click_activate=True,
168            selectable=True,
169            autoselect=True,
170            on_activate_call=lambda: self._set_sub_tab(
171                SubTabType.FAVORITES,
172                region_width,
173                region_height,
174                playsound=True,
175            ),
176            text=bui.Lstr(resource='gatherWindow.favoritesText'),
177            glow_type='uniform',
178        )
179        bui.widget(edit=self._join_by_address_text, up_widget=tab_button)
180        bui.widget(
181            edit=self._favorites_text,
182            left_widget=self._join_by_address_text,
183            up_widget=tab_button,
184        )
185        bui.widget(edit=tab_button, down_widget=self._favorites_text)
186        bui.widget(
187            edit=self._join_by_address_text, right_widget=self._favorites_text
188        )
189        self._set_sub_tab(self._sub_tab, region_width, region_height)
190
191        return self._container

Called when the tab becomes the active one.

The tab should create and return a container widget covering the specified region.

@override
def save_state(self) -> None:
193    @override
194    def save_state(self) -> None:
195        assert bui.app.classic is not None
196        bui.app.ui_v1.window_states[type(self)] = State(sub_tab=self._sub_tab)

Called when the parent window is saving state.

@override
def restore_state(self) -> None:
198    @override
199    def restore_state(self) -> None:
200        assert bui.app.classic is not None
201        state = bui.app.ui_v1.window_states.get(type(self))
202        if state is None:
203            state = State()
204        assert isinstance(state, State)
205        self._sub_tab = state.sub_tab

Called when the parent window is restoring state.

@override
def on_deactivate(self) -> None:
785    @override
786    def on_deactivate(self) -> None:
787        self._access_check_timer = None

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