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

The manual tab in the gather UI

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

Called when the parent window is restoring state.

def on_deactivate(self) -> None:
618    def on_deactivate(self) -> None:
619        self._access_check_timer = None

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

Inherited Members
bastd.ui.gather.GatherTab
window