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