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

The manual tab in the gather UI

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

Called when the tab becomes the active one.

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

def save_state(self) -> None:
181    def save_state(self) -> None:
182        assert bui.app.classic is not None
183        bui.app.ui_v1.window_states[type(self)] = State(sub_tab=self._sub_tab)

Called when the parent window is saving state.

def restore_state(self) -> None:
185    def restore_state(self) -> None:
186        assert bui.app.classic is not None
187        state = bui.app.ui_v1.window_states.get(type(self))
188        if state is None:
189            state = State()
190        assert isinstance(state, State)
191        self._sub_tab = state.sub_tab

Called when the parent window is restoring state.

def on_deactivate(self) -> None:
740    def on_deactivate(self) -> None:
741        self._access_check_timer = None

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