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

Available sub-tabs.

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

State saved/restored only while the app is running.

State( sub_tab: SubTabType = <SubTabType.JOIN_BY_ADDRESS: 'join_by_address'>)
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        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            bs.connect_to_party(resolved_address, port=port)
 875
 876    def _run_addr_fetch(self) -> None:
 877        try:
 878            # FIXME: Update this to work with IPv6.
 879            import socket
 880
 881            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 882            sock.connect(('8.8.8.8', 80))
 883            val = sock.getsockname()[0]
 884            sock.close()
 885            bui.pushcall(
 886                bui.Call(
 887                    _safe_set_text,
 888                    self._checking_state_text,
 889                    val,
 890                ),
 891                from_other_thread=True,
 892            )
 893        except Exception as exc:
 894            from efro.error import is_udp_communication_error
 895
 896            if is_udp_communication_error(exc):
 897                bui.pushcall(
 898                    bui.Call(
 899                        _safe_set_text,
 900                        self._checking_state_text,
 901                        bui.Lstr(resource='gatherWindow.' 'noConnectionText'),
 902                        False,
 903                    ),
 904                    from_other_thread=True,
 905                )
 906            else:
 907                bui.pushcall(
 908                    bui.Call(
 909                        _safe_set_text,
 910                        self._checking_state_text,
 911                        bui.Lstr(
 912                            resource='gatherWindow.' 'addressFetchErrorText'
 913                        ),
 914                        False,
 915                    ),
 916                    from_other_thread=True,
 917                )
 918                logging.exception('Error in AddrFetchThread.')
 919
 920    def _on_show_my_address_button_press(
 921        self, v2: float, container: bui.Widget | None, c_width: float
 922    ) -> None:
 923        if not container:
 924            return
 925
 926        tscl = 0.85
 927        tspc = 25
 928
 929        bui.getsound('swish').play()
 930        bui.textwidget(
 931            parent=container,
 932            position=(c_width * 0.5 - 10, v2),
 933            color=(0.6, 1.0, 0.6),
 934            scale=tscl,
 935            size=(0, 0),
 936            maxwidth=c_width * 0.45,
 937            flatness=1.0,
 938            h_align='right',
 939            v_align='center',
 940            text=bui.Lstr(
 941                resource='gatherWindow.' 'manualYourLocalAddressText'
 942            ),
 943        )
 944        self._checking_state_text = bui.textwidget(
 945            parent=container,
 946            position=(c_width * 0.5, v2),
 947            color=(0.5, 0.5, 0.5),
 948            scale=tscl,
 949            size=(0, 0),
 950            maxwidth=c_width * 0.45,
 951            flatness=1.0,
 952            h_align='left',
 953            v_align='center',
 954            text=bui.Lstr(resource='gatherWindow.' 'checkingText'),
 955        )
 956
 957        Thread(target=self._run_addr_fetch).start()
 958
 959        v2 -= tspc
 960        bui.textwidget(
 961            parent=container,
 962            position=(c_width * 0.5 - 10, v2),
 963            color=(0.6, 1.0, 0.6),
 964            scale=tscl,
 965            size=(0, 0),
 966            maxwidth=c_width * 0.45,
 967            flatness=1.0,
 968            h_align='right',
 969            v_align='center',
 970            text=bui.Lstr(
 971                resource='gatherWindow.' 'manualYourAddressFromInternetText'
 972            ),
 973        )
 974
 975        t_addr = bui.textwidget(
 976            parent=container,
 977            position=(c_width * 0.5, v2),
 978            color=(0.5, 0.5, 0.5),
 979            scale=tscl,
 980            size=(0, 0),
 981            maxwidth=c_width * 0.45,
 982            h_align='left',
 983            v_align='center',
 984            flatness=1.0,
 985            text=bui.Lstr(resource='gatherWindow.' 'checkingText'),
 986        )
 987        v2 -= tspc
 988        bui.textwidget(
 989            parent=container,
 990            position=(c_width * 0.5 - 10, v2),
 991            color=(0.6, 1.0, 0.6),
 992            scale=tscl,
 993            size=(0, 0),
 994            maxwidth=c_width * 0.45,
 995            flatness=1.0,
 996            h_align='right',
 997            v_align='center',
 998            text=bui.Lstr(
 999                resource='gatherWindow.' 'manualJoinableFromInternetText'
1000            ),
1001        )
1002
1003        t_accessible = bui.textwidget(
1004            parent=container,
1005            position=(c_width * 0.5, v2),
1006            color=(0.5, 0.5, 0.5),
1007            scale=tscl,
1008            size=(0, 0),
1009            maxwidth=c_width * 0.45,
1010            flatness=1.0,
1011            h_align='left',
1012            v_align='center',
1013            text=bui.Lstr(resource='gatherWindow.' 'checkingText'),
1014        )
1015        v2 -= 28
1016        t_accessible_extra = bui.textwidget(
1017            parent=container,
1018            position=(c_width * 0.5, v2),
1019            color=(1, 0.5, 0.2),
1020            scale=0.7,
1021            size=(0, 0),
1022            maxwidth=c_width * 0.9,
1023            flatness=1.0,
1024            h_align='center',
1025            v_align='center',
1026            text='',
1027        )
1028
1029        self._doing_access_check = False
1030        self._access_check_count = 0  # Cap our refreshes eventually.
1031        self._access_check_timer = bui.AppTimer(
1032            10.0,
1033            bui.WeakCall(
1034                self._access_check_update,
1035                t_addr,
1036                t_accessible,
1037                t_accessible_extra,
1038            ),
1039            repeat=True,
1040        )
1041
1042        # Kick initial off.
1043        self._access_check_update(t_addr, t_accessible, t_accessible_extra)
1044        if self._check_button:
1045            self._check_button.delete()
1046
1047    def _access_check_update(
1048        self,
1049        t_addr: bui.Widget,
1050        t_accessible: bui.Widget,
1051        t_accessible_extra: bui.Widget,
1052    ) -> None:
1053        assert bui.app.classic is not None
1054        # If we don't have an outstanding query, start one..
1055        assert self._doing_access_check is not None
1056        assert self._access_check_count is not None
1057        if not self._doing_access_check and self._access_check_count < 100:
1058            self._doing_access_check = True
1059            self._access_check_count += 1
1060            self._t_addr = t_addr
1061            self._t_accessible = t_accessible
1062            self._t_accessible_extra = t_accessible_extra
1063            bui.app.classic.master_server_v1_get(
1064                'bsAccessCheck',
1065                {'b': bui.app.env.engine_build_number},
1066                callback=bui.WeakCall(self._on_accessible_response),
1067            )
1068
1069    def _on_accessible_response(self, data: dict[str, Any] | None) -> None:
1070        t_addr = self._t_addr
1071        t_accessible = self._t_accessible
1072        t_accessible_extra = self._t_accessible_extra
1073        self._doing_access_check = False
1074        color_bad = (1, 1, 0)
1075        color_good = (0, 1, 0)
1076        if data is None or 'address' not in data or 'accessible' not in data:
1077            if t_addr:
1078                bui.textwidget(
1079                    edit=t_addr,
1080                    text=bui.Lstr(resource='gatherWindow.' 'noConnectionText'),
1081                    color=color_bad,
1082                )
1083            if t_accessible:
1084                bui.textwidget(
1085                    edit=t_accessible,
1086                    text=bui.Lstr(resource='gatherWindow.' 'noConnectionText'),
1087                    color=color_bad,
1088                )
1089            if t_accessible_extra:
1090                bui.textwidget(
1091                    edit=t_accessible_extra, text='', color=color_bad
1092                )
1093            return
1094        if t_addr:
1095            bui.textwidget(edit=t_addr, text=data['address'], color=color_good)
1096        if t_accessible:
1097            if data['accessible']:
1098                bui.textwidget(
1099                    edit=t_accessible,
1100                    text=bui.Lstr(
1101                        resource='gatherWindow.' 'manualJoinableYesText'
1102                    ),
1103                    color=color_good,
1104                )
1105                if t_accessible_extra:
1106                    bui.textwidget(
1107                        edit=t_accessible_extra, text='', color=color_good
1108                    )
1109            else:
1110                bui.textwidget(
1111                    edit=t_accessible,
1112                    text=bui.Lstr(
1113                        resource='gatherWindow.'
1114                        'manualJoinableNoWithAsteriskText'
1115                    ),
1116                    color=color_bad,
1117                )
1118                if t_accessible_extra:
1119                    bui.textwidget(
1120                        edit=t_accessible_extra,
1121                        text=bui.Lstr(
1122                            resource='gatherWindow.'
1123                            'manualRouterForwardingText',
1124                            subs=[
1125                                ('${PORT}', str(bs.get_game_port())),
1126                            ],
1127                        ),
1128                        color=color_bad,
1129                    )

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        c_width = region_width
119        c_height = region_height - 20
120
121        self._container = bui.containerwidget(
122            parent=parent_widget,
123            position=(
124                region_left,
125                region_bottom + (region_height - c_height) * 0.5,
126            ),
127            size=(c_width, c_height),
128            background=False,
129            selection_loops_to_parent=True,
130        )
131        v = c_height - 30
132        self._join_by_address_text = bui.textwidget(
133            parent=self._container,
134            position=(c_width * 0.5 - 245, v - 13),
135            color=(0.6, 1.0, 0.6),
136            scale=1.3,
137            size=(200, 30),
138            maxwidth=250,
139            h_align='center',
140            v_align='center',
141            click_activate=True,
142            selectable=True,
143            autoselect=True,
144            on_activate_call=lambda: self._set_sub_tab(
145                SubTabType.JOIN_BY_ADDRESS,
146                region_width,
147                region_height,
148                playsound=True,
149            ),
150            text=bui.Lstr(resource='gatherWindow.manualJoinSectionText'),
151            glow_type='uniform',
152        )
153        self._favorites_text = bui.textwidget(
154            parent=self._container,
155            position=(c_width * 0.5 + 45, v - 13),
156            color=(0.6, 1.0, 0.6),
157            scale=1.3,
158            size=(200, 30),
159            maxwidth=250,
160            h_align='center',
161            v_align='center',
162            click_activate=True,
163            selectable=True,
164            autoselect=True,
165            on_activate_call=lambda: self._set_sub_tab(
166                SubTabType.FAVORITES,
167                region_width,
168                region_height,
169                playsound=True,
170            ),
171            text=bui.Lstr(resource='gatherWindow.favoritesText'),
172            glow_type='uniform',
173        )
174        bui.widget(edit=self._join_by_address_text, up_widget=tab_button)
175        bui.widget(
176            edit=self._favorites_text,
177            left_widget=self._join_by_address_text,
178            up_widget=tab_button,
179        )
180        bui.widget(edit=tab_button, down_widget=self._favorites_text)
181        bui.widget(
182            edit=self._join_by_address_text, right_widget=self._favorites_text
183        )
184        self._set_sub_tab(self._sub_tab, region_width, region_height)
185
186        return self._container

Called when the tab becomes the active one.

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

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

Called when the parent window is saving state.

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

Called when the parent window is restoring state.

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

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