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