diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index ff67abad..7ed9fbec 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -1801,6 +1801,7 @@ samsung sandboxing sandyrb + savebutton saxutils sbblk sbblu diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 226a1795..5ab7328e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -17,3 +17,6 @@ ### Ali Borhani - Bug fixes + +### Mr.Smoothy +- Modder \ No newline at end of file diff --git a/assets/src/ba_data/python/bastd/ui/gather/manualtab.py b/assets/src/ba_data/python/bastd/ui/gather/manualtab.py index 64c0c2e3..169f997e 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/manualtab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/manualtab.py @@ -7,13 +7,16 @@ from __future__ import annotations import threading from typing import TYPE_CHECKING, cast -import _ba -import ba +from enum import Enum +from dataclasses import dataclass from bastd.ui.gather.bases import GatherTab +import _ba +import ba if TYPE_CHECKING: - from typing import Callable, Optional, Any, Union, Dict + from typing import Any, Optional, Dict, List, Tuple, Type, Union, Callable from bastd.ui.gather import GatherWindow + from bastd.ui.confirm import ConfirmWindow def _safe_set_text(txt: Optional[ba.Widget], @@ -46,6 +49,18 @@ class _HostLookupThread(threading.Thread): from_other_thread=True) +class SubTabType(Enum): + """Available sub-tabs.""" + NEW = 'new' + SAVED = 'saved' + + +@dataclass +class State: + """State saved/restored only while the app is running.""" + sub_tab: SubTabType = SubTabType.NEW + + class ManualGatherTab(GatherTab): """The manual tab in the gather UI""" @@ -54,12 +69,26 @@ class ManualGatherTab(GatherTab): self._check_button: Optional[ba.Widget] = None self._doing_access_check: Optional[bool] = None self._access_check_count: Optional[int] = None + self._sub_tab: SubTabType = SubTabType.NEW self._t_addr: Optional[ba.Widget] = None self._t_accessible: Optional[ba.Widget] = None self._t_accessible_extra: Optional[ba.Widget] = None self._access_check_timer: Optional[ba.Timer] = None self._checking_state_text: Optional[ba.Widget] = None self._container: Optional[ba.Widget] = None + self._join_new_party_text: Optional[ba.Widget] = None + self._join_saved_party_text: Optional[ba.Widget] = None + self._width: Optional[int] = None + self._height: Optional[int] = None + self._scroll_width: Optional[int] = None + self._scroll_height: Optional[int] = None + self._my_parties_scroll_width: Optional[int] = None + self._my_saved_party_connect_button: Optional[ba.Widget] = None + self._scrollwidget: Optional[ba.Widget] = None + self._columnwidget: Optional[ba.Widget] = None + self._my_saved_party_selected: Optional[str] = None + self._my_saved_party_rename_window: Optional[ba.Widget] = None + self._my_party_rename_text: Optional[ba.Widget] = None def on_activate( self, @@ -72,8 +101,7 @@ class ManualGatherTab(GatherTab): ) -> ba.Widget: c_width = region_width - c_height = 380 - last_addr = ba.app.config.get('Last Manual Party Connect Address', '') + c_height = region_height - 20 self._container = ba.containerwidget( parent=parent_widget, @@ -83,17 +111,105 @@ class ManualGatherTab(GatherTab): background=False, selection_loops_to_parent=True) v = c_height - 30 - ba.textwidget(parent=self._container, - position=(c_width * 0.5, v), - color=(0.6, 1.0, 0.6), - scale=1.3, - size=(0, 0), - maxwidth=c_width * 0.9, - h_align='center', - v_align='center', - text=ba.Lstr(resource='gatherWindow.' - 'manualDescriptionText')) - v -= 30 + self._join_new_party_text = ba.textwidget( + parent=self._container, + position=(c_width * 0.5 - 245, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='left', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_sub_tab( + SubTabType.NEW, + region_width, + region_height, + playsound=True, + ), + text='Join By Address') + self._join_saved_party_text = ba.textwidget( + parent=self._container, + position=(c_width * 0.5 + 45, v - 13), + color=(0.6, 1.0, 0.6), + scale=1.3, + size=(200, 30), + maxwidth=250, + h_align='left', + v_align='center', + click_activate=True, + selectable=True, + autoselect=True, + on_activate_call=lambda: self._set_sub_tab( + SubTabType.SAVED, + region_width, + region_height, + playsound=True, + ), + text='Join Saved Party') + ba.widget(edit=self._join_new_party_text, up_widget=tab_button) + ba.widget(edit=self._join_saved_party_text, + left_widget=self._join_new_party_text, + up_widget=tab_button) + ba.widget(edit=tab_button, down_widget=self._join_saved_party_text) + ba.widget(edit=self._join_new_party_text, + right_widget=self._join_saved_party_text) + self._set_sub_tab(self._sub_tab, region_width, region_height) + + return self._container + + def save_state(self) -> None: + ba.app.ui.window_states[self.__class__.__name__] = State( + sub_tab=self._sub_tab) + + def restore_state(self) -> None: + state = ba.app.ui.window_states.get(self.__class__.__name__) + if state is None: + state = State() + assert isinstance(state, State) + self._sub_tab = state.sub_tab + + def _set_sub_tab(self, + value: SubTabType, + region_width: float, + region_height: float, + playsound: bool = False) -> None: + assert self._container + if playsound: + ba.playsound(ba.getsound('click01')) + + self._sub_tab = value + active_color = (0.6, 1.0, 0.6) + inactive_color = (0.5, 0.4, 0.5) + ba.textwidget( + edit=self._join_new_party_text, + color=active_color if value is SubTabType.NEW else inactive_color) + ba.textwidget(edit=self._join_saved_party_text, + color=active_color + if value is SubTabType.SAVED else inactive_color) + + # Clear anything existing in the old sub-tab. + for widget in self._container.get_children(): + if widget and widget not in { + self._join_saved_party_text, self._join_new_party_text + }: + widget.delete() + + if value is SubTabType.NEW: + self._build_new_party_tab(region_width, region_height) + + if value is SubTabType.SAVED: + self._build_saved_party_tab(region_height) + + # The old manual tab + def _build_new_party_tab(self, region_width: float, + region_height: float) -> None: + c_width = region_width + c_height = region_height - 20 + last_addr = ba.app.config.get('Last Manual Party Connect Address', '') + v = c_height - 70 v -= 70 ba.textwidget(parent=self._container, position=(c_width * 0.5 - 260 - 50, v), @@ -115,6 +231,8 @@ class ManualGatherTab(GatherTab): v_align='center', scale=1.0, size=(420, 60)) + ba.widget(edit=self._join_new_party_text, down_widget=txt) + ba.widget(edit=self._join_saved_party_text, down_widget=txt) ba.textwidget(parent=self._container, position=(c_width * 0.5 - 260 + 490, v), color=(0.6, 1.0, 0.6), @@ -143,11 +261,19 @@ class ManualGatherTab(GatherTab): size=(300, 70), label=ba.Lstr(resource='gatherWindow.' 'manualConnectText'), - position=(c_width * 0.5 - 150, v), + position=(c_width * 0.5 - 300, v), autoselect=True, on_activate_call=ba.Call(self._connect, txt, txt2)) - ba.widget(edit=txt, up_widget=tab_button) + savebutton = ba.buttonwidget( + parent=self._container, + size=(300, 70), + label='Save', + position=(c_width * 0.5 - 240 + 490 - 200, v), + autoselect=True, + on_activate_call=ba.Call(self._save_server, txt, txt2)) + ba.widget(edit=btn, right_widget=savebutton) + ba.widget(edit=savebutton, left_widget=btn, up_widget=txt2) ba.textwidget(edit=txt, on_return_press_call=btn.activate) ba.textwidget(edit=txt2, on_return_press_call=btn.activate) v -= 45 @@ -167,7 +293,255 @@ class ManualGatherTab(GatherTab): selectable=True, on_activate_call=ba.Call(self._on_show_my_address_button_press, v, self._container, c_width)) - return self._container + ba.widget(edit=self._check_button, up_widget=btn) + + # Tab containing saved parties + def _build_saved_party_tab(self, region_height: float) -> None: + + c_height = region_height - 20 + v = c_height - 35 - 25 - 30 + + uiscale = ba.app.ui.uiscale + self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._height = (578 if uiscale is ba.UIScale.SMALL else + 670 if uiscale is ba.UIScale.MEDIUM else 800) + + self._scroll_width = self._width - 130 + 2 * x_inset + self._scroll_height = self._height - 180 + x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + + c_height = self._scroll_height - 20 + sub_scroll_height = c_height - 63 + self._my_parties_scroll_width = sub_scroll_width = ( + 680 if uiscale is ba.UIScale.SMALL else 640) + + v = c_height - 30 + + b_width = 140 if uiscale is ba.UIScale.SMALL else 178 + b_height = (107 if uiscale is ba.UIScale.SMALL else + 142 if uiscale is ba.UIScale.MEDIUM else 190) + b_space_extra = (0 if uiscale is ba.UIScale.SMALL else + -2 if uiscale is ba.UIScale.MEDIUM else -5) + + btnv = (c_height - (48 if uiscale is ba.UIScale.SMALL else + 45 if uiscale is ba.UIScale.MEDIUM else 40) - + b_height) + + self._my_saved_party_connect_button = btn1 = ba.buttonwidget( + parent=self._container, + size=(b_width, b_height), + position=(40 if uiscale is ba.UIScale.SMALL else 40, btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_my_saved_party_press, + text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, + label='Connect', + autoselect=True) + if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + ba.widget(edit=btn1, + left_widget=_ba.get_special_widget('back_button')) + btnv -= b_height + b_space_extra + ba.buttonwidget(parent=self._container, + size=(b_width, b_height), + position=(40 if uiscale is ba.UIScale.SMALL else 40, + btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_my_saved_party_rename_press, + text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, + label='Rename', + autoselect=True) + btnv -= b_height + b_space_extra + ba.buttonwidget(parent=self._container, + size=(b_width, b_height), + position=(40 if uiscale is ba.UIScale.SMALL else 40, + btnv), + button_type='square', + color=(0.6, 0.53, 0.63), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=self._on_my_saved_party_delete_press, + text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, + label='Delete', + autoselect=True) + + v -= sub_scroll_height + 23 + self._scrollwidget = scrlw = ba.scrollwidget( + parent=self._container, + position=(190 if uiscale is ba.UIScale.SMALL else 225, v), + size=(sub_scroll_width, sub_scroll_height), + claims_left_right=True) + ba.widget(edit=self._my_saved_party_connect_button, + right_widget=self._scrollwidget) + ba.containerwidget(edit=self._container, selected_child=scrlw) + self._columnwidget = ba.columnwidget(parent=scrlw, + left_border=10, + border=2, + margin=0, + claims_left_right=True) + + self._my_saved_party_selected = None + self._refresh_my_saved_parties() + + def _no_saved_party_selected_error(self) -> None: + ba.screenmessage(ba.Lstr(resource='nothingIsSelectedErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + + def _on_my_saved_party_press(self) -> None: + if self._my_saved_party_selected is None: + self._no_saved_party_selected_error() + + else: + config = ba.app.config['Saved Servers'][ + self._my_saved_party_selected] + _HostLookupThread(name=config['addr'], + port=config['port'], + call=ba.WeakCall( + self._host_lookup_result)).start() + + def _on_my_saved_party_rename_press(self) -> None: + if self._my_saved_party_selected is None: + self._no_saved_party_selected_error() + return + + c_width = 600 + c_height = 250 + uiscale = ba.app.ui.uiscale + self._my_saved_party_rename_window = cnt = ba.containerwidget( + scale=(1.8 if uiscale is ba.UIScale.SMALL else + 1.55 if uiscale is ba.UIScale.MEDIUM else 1.0), + size=(c_width, c_height), + transition='in_scale') + + ba.textwidget(parent=cnt, + size=(0, 0), + h_align='center', + v_align='center', + text='Enter Name of Party', + maxwidth=c_width * 0.8, + position=(c_width * 0.5, c_height - 60)) + self._my_party_rename_text = txt = ba.textwidget( + parent=cnt, + size=(c_width * 0.8, 40), + h_align='left', + v_align='center', + text=ba.app.config['Saved Servers'][ + self._my_saved_party_selected]['name'], + editable=True, + description='Server name text', + position=(c_width * 0.1, c_height - 140), + autoselect=True, + maxwidth=c_width * 0.7, + max_chars=200) + cbtn = ba.buttonwidget( + parent=cnt, + label=ba.Lstr(resource='cancelText'), + on_activate_call=ba.Call( + lambda c: ba.containerwidget(edit=c, transition='out_scale'), + cnt), + size=(180, 60), + position=(30, 30), + autoselect=True) + okb = ba.buttonwidget(parent=cnt, + label='Rename', + size=(180, 60), + position=(c_width - 230, 30), + on_activate_call=ba.Call( + self._rename_saved_party), + autoselect=True) + ba.widget(edit=cbtn, right_widget=okb) + ba.widget(edit=okb, left_widget=cbtn) + ba.textwidget(edit=txt, on_return_press_call=okb.activate) + ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) + + def _rename_saved_party(self) -> None: + + server = self._my_saved_party_selected + if self._my_saved_party_selected is None: + self._no_saved_party_selected_error() + return + if not self._my_party_rename_text: + return + new_name_raw = cast(str, + ba.textwidget(query=self._my_party_rename_text)) + ba.app.config['Saved Servers'][server]['name'] = new_name_raw + ba.app.config.commit() + ba.screenmessage('Renamed Successfully', color=(0, 1, 0)) + ba.playsound(ba.getsound('gunCocking')) + self._refresh_my_saved_parties() + + ba.containerwidget(edit=self._my_saved_party_rename_window, + transition='out_scale') + + def _on_my_saved_party_delete_press(self) -> None: + from bastd.ui import confirm + if self._my_saved_party_selected is None: + self._no_saved_party_selected_error() + return + confirm.ConfirmWindow( + ba.Lstr(resource='gameListWindow.deleteConfirmText', + subs=[('${LIST}', ba.app.config['Saved Servers'][ + self._my_saved_party_selected]['name'])]), + self._delete_saved_party, 450, 150) + + def _delete_saved_party(self) -> None: + if self._my_saved_party_selected is None: + self._no_saved_party_selected_error() + return + config = ba.app.config['Saved Servers'] + del config[self._my_saved_party_selected] + self._my_saved_party_selected = None + ba.app.config.commit() + ba.playsound(ba.getsound('shieldDown')) + self._refresh_my_saved_parties() + + def _on_my_saved_party_select(self, server: str) -> None: + self._my_saved_party_selected = server + + def _refresh_my_saved_parties(self) -> None: + assert self._columnwidget is not None + for child in self._columnwidget.get_children(): + child.delete() + t_scale = 1.6 + + config = ba.app.config + if 'Saved Servers' in config: + servers = config['Saved Servers'] + + else: + servers = [] + + assert self._my_parties_scroll_width is not None + assert self._my_saved_party_connect_button is not None + for i, server in enumerate(servers): + txt = ba.textwidget( + parent=self._columnwidget, + size=(self._my_parties_scroll_width / t_scale, 30), + selectable=True, + color=(1.0, 1, 0.4), + always_highlight=True, + on_select_call=ba.Call(self._on_my_saved_party_select, server), + on_activate_call=self._my_saved_party_connect_button.activate, + text=(config['Saved Servers'][server]['name'] + if config['Saved Servers'][server]['name'] != '' else + config['Saved Servers'][server]['addr'] + ' ' + + str(config['Saved Servers'][server]['port'])), + h_align='left', + v_align='center', + corner_scale=t_scale, + maxwidth=(self._my_parties_scroll_width / t_scale) * 0.93) + if i == 0: + ba.widget(edit=txt, up_widget=self._join_saved_party_text) + ba.widget(edit=txt, + left_widget=self._my_saved_party_connect_button, + right_widget=txt) + + # If there's no servers, allow selecting out of the scroll area + ba.containerwidget(edit=self._scrollwidget, + claims_left_right=bool(servers)) def on_deactivate(self) -> None: self._access_check_timer = None @@ -195,6 +569,41 @@ class ManualGatherTab(GatherTab): port=port, call=ba.WeakCall(self._host_lookup_result)).start() + def _save_server(self, textwidget: ba.Widget, + port_textwidget: ba.Widget) -> None: + addr = cast(str, ba.textwidget(query=textwidget)) + if addr == '': + ba.screenmessage( + ba.Lstr(resource='internal.invalidAddressErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + try: + port = int(cast(str, ba.textwidget(query=port_textwidget))) + except ValueError: + port = -1 + if port > 65535 or port < 0: + ba.screenmessage(ba.Lstr(resource='internal.invalidPortErrorText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + config = ba.app.config + + if addr: + if not isinstance(config.get('Saved Servers'), dict): + config['Saved Servers'] = {} + config['Saved Servers'][f'{addr}@{port}'] = { + 'addr': addr, + 'port': port, + 'name': addr + } + config.commit() + ba.screenmessage('Saved Successfully', color=(0, 1, 0)) + ba.playsound(ba.getsound('gunCocking')) + else: + ba.screenmessage('Invalid Address', color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + def _host_lookup_result(self, resolved_address: Optional[str], port: int) -> None: if resolved_address is None: diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index afc6fd4b..2a22a0bf 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -758,6 +758,7 @@ safecolor samsung sapspace + savebutton scancode scenetime screenmessage