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

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: int | None = None
 94        self._height: int | None = None
 95        self._scroll_width: int | None = None
 96        self._scroll_height: int | None = None
 97        self._favorites_scroll_width: int | None = None
 98        self._favorites_connect_button: bui.Widget | None = None
 99        self._scrollwidget: bui.Widget | None = None
100        self._columnwidget: bui.Widget | None = None
101        self._favorite_selected: str | None = None
102        self._favorite_edit_window: bui.Widget | None = None
103        self._party_edit_name_text: bui.Widget | None = None
104        self._party_edit_addr_text: bui.Widget | None = None
105        self._party_edit_port_text: bui.Widget | None = None
106        self._no_parties_added_text: bui.Widget | None = None
@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:
775    @override
776    def on_deactivate(self) -> None:
777        self._access_check_timer = None

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