bauiv1lib.gather.manualtab

Defines the manual tab in the gather UI.

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

Available sub-tabs.

JOIN_BY_ADDRESS = <SubTabType.JOIN_BY_ADDRESS: 'join_by_address'>
FAVORITES = <SubTabType.FAVORITES: 'favorites'>
@dataclass
class State:
69@dataclass
70class State:
71    """State saved/restored only while the app is running."""
72
73    sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS

State saved/restored only while the app is running.

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

The manual tab in the gather UI

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

Called when the tab becomes the active one.

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

@override
def save_state(self) -> None:
189    @override
190    def save_state(self) -> None:
191        assert bui.app.classic is not None
192        bui.app.ui_v1.window_states[type(self)] = State(sub_tab=self._sub_tab)

Called when the parent window is saving state.

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

Called when the parent window is restoring state.

@override
def on_deactivate(self) -> None:
782    @override
783    def on_deactivate(self) -> None:
784        self._access_check_timer = None

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