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

State saved/restored only while the app is running.

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

The manual tab in the gather UI

ManualGatherTab(window: bastd.ui.gather.GatherWindow)
 74    def __init__(self, window: GatherWindow) -> None:
 75        super().__init__(window)
 76        self._check_button: ba.Widget | None = None
 77        self._doing_access_check: bool | None = None
 78        self._access_check_count: int | None = None
 79        self._sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS
 80        self._t_addr: ba.Widget | None = None
 81        self._t_accessible: ba.Widget | None = None
 82        self._t_accessible_extra: ba.Widget | None = None
 83        self._access_check_timer: ba.Timer | None = None
 84        self._checking_state_text: ba.Widget | None = None
 85        self._container: ba.Widget | None = None
 86        self._join_by_address_text: ba.Widget | None = None
 87        self._favorites_text: ba.Widget | None = None
 88        self._width: int | None = None
 89        self._height: int | None = None
 90        self._scroll_width: int | None = None
 91        self._scroll_height: int | None = None
 92        self._favorites_scroll_width: int | None = None
 93        self._favorites_connect_button: ba.Widget | None = None
 94        self._scrollwidget: ba.Widget | None = None
 95        self._columnwidget: ba.Widget | None = None
 96        self._favorite_selected: str | None = None
 97        self._favorite_edit_window: ba.Widget | None = None
 98        self._party_edit_name_text: ba.Widget | None = None
 99        self._party_edit_addr_text: ba.Widget | None = None
100        self._party_edit_port_text: ba.Widget | None = None
def on_activate( self, parent_widget: _ba.Widget, tab_button: _ba.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float) -> _ba.Widget:
102    def on_activate(
103        self,
104        parent_widget: ba.Widget,
105        tab_button: ba.Widget,
106        region_width: float,
107        region_height: float,
108        region_left: float,
109        region_bottom: float,
110    ) -> ba.Widget:
111
112        c_width = region_width
113        c_height = region_height - 20
114
115        self._container = ba.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 = ba.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=ba.Lstr(resource='gatherWindow.manualJoinSectionText'),
145        )
146        self._favorites_text = ba.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=ba.Lstr(resource='gatherWindow.favoritesText'),
165        )
166        ba.widget(edit=self._join_by_address_text, up_widget=tab_button)
167        ba.widget(
168            edit=self._favorites_text,
169            left_widget=self._join_by_address_text,
170            up_widget=tab_button,
171        )
172        ba.widget(edit=tab_button, down_widget=self._favorites_text)
173        ba.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

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:
180    def save_state(self) -> None:
181        ba.app.ui.window_states[type(self)] = State(sub_tab=self._sub_tab)

Called when the parent window is saving state.

def restore_state(self) -> None:
183    def restore_state(self) -> None:
184        state = ba.app.ui.window_states.get(type(self))
185        if state is None:
186            state = State()
187        assert isinstance(state, State)
188        self._sub_tab = state.sub_tab

Called when the parent window is restoring state.

def on_deactivate(self) -> None:
736    def on_deactivate(self) -> None:
737        self._access_check_timer = None

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

Inherited Members
bastd.ui.gather.GatherTab
window