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

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:
786    @override
787    def on_deactivate(self) -> None:
788        self._access_check_timer = None

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