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'>)
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.