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