diff --git a/dzgui/api/servers.py b/dzgui/api/servers.py index aa54712..d054225 100644 --- a/dzgui/api/servers.py +++ b/dzgui/api/servers.py @@ -442,7 +442,7 @@ def get_rules(record: Record) -> list["DayzMod"]: return [] -def query_by_id(addr: str, key: str) -> Optional[dict]: +def query_by_id(addr: str, key: str) -> dict[Any] | None: """ Used with numeric Battlemetrics IDs """ @@ -469,12 +469,12 @@ def query_playercount(record: Record) -> tuple[int, int] | None: return None -def query_by_ip(addr: str) -> Optional[dict]: +def query_by_ip(addr: str) -> dict[Any] | None: record = short_ip_to_record(addr) return query_by_record(record) -def query_by_record(record: Record) -> Optional[dict]: +def query_by_record(record: Record) -> dict[Any] | None: try: return query_direct(record.ip, record.qport) except Exception as e: diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index a1d8eac..13ea054 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -179,6 +179,7 @@ class ContextMenuGroup(Enum): Groupings of context menu items """ + SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,) MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.DELETE_MOD) LOG = (ContextMenu.COPY_LOG_CLIPBOARD,) SERVER_BROWSER = ( diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 29a55a7..b13d06a 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING import dzgui.api.servers as Servers @@ -27,37 +27,28 @@ class ConnectionManager: @call_on_thread(dialog.querying) def connect_by_id(self, addr: str, key: str) -> None: res = Servers.query_by_id(addr, key) - if res is None: - self.thread_man.set_cleanup_func( - StoredFunc(self._server_timeout), destroy_first=True - ) - - # TODO: resolve real ip - print("UNIMPLEMENTED: get rules") - print(res) + self.prepare_connection(res) @call_on_thread(dialog.querying) def connect_by_ip(self, addr: str) -> None: res = Servers.query_by_ip(addr) - if res is None: - self.thread_man.set_cleanup_func( - StoredFunc(self._server_timeout), destroy_first=True - ) - - # TODO: resolve real ip - print("UNIMPLEMENTED: get rules") - print(res) + self.prepare_connection(res) @call_on_thread(dialog.querying) def connect_by_record(self, record: Servers.Record) -> None: res = Servers.query_by_record(record) + self.prepare_connection(res) + + def prepare_connection(self, res: dict[Any] | None) -> None: if res is None: self.thread_man.set_cleanup_func( StoredFunc(self._server_timeout), destroy_first=True ) + return - # TODO: proper error handling (currently returns empty list) + record = Servers.response_to_record(res) try: + # TODO: proper error handling (currently returns empty list) mods = self.query_modlist(record) except Exception: self.thread_man.set_cleanup_func( diff --git a/dzgui/managers/contextmenu.py b/dzgui/managers/contextmenu.py index 51a158a..33aea9a 100644 --- a/dzgui/managers/contextmenu.py +++ b/dzgui/managers/contextmenu.py @@ -11,6 +11,7 @@ from dzgui.views.dialogs.note import NoteDialog from dzgui.views.trees.tree_mods import ModTreeView from dzgui.views.trees.tree_log import LogTreeView from dzgui.views.trees.tree_servers import ServerTreeView +from dzgui.views.trees.tree_server_mods import ServerModTreeView gi.require_version("Gtk", "3.0") @@ -26,7 +27,7 @@ if TYPE_CHECKING: class ContextMenuManager: def __init__( self, - treeview: LogTreeView | ModTreeView | ServerTreeView, + treeview: LogTreeView | ModTreeView | ServerTreeView | ServerModTreeView, controller: "Controller", ) -> None: self.controller = controller @@ -83,6 +84,11 @@ class ContextMenuManager: case ContextMenu.COPY_LOG_CLIPBOARD: self.copy_log() + if isinstance(self.treeview, ServerModTreeView): + match action: + case ContextMenu.OPEN_WORKSHOP: + self.open_mod_page() + def copy_server_ip(self) -> None: if not isinstance(self.treeview, ServerTreeView): return @@ -96,7 +102,7 @@ class ContextMenuManager: copy_clipboard(name) def open_mod_page(self) -> None: - if not isinstance(self.treeview, ModTreeView): + if not hasattr(self.treeview, "get_selected_mod"): return mod = self.treeview.get_selected_mod() cmd = self.controller.query_config(Preferences.CLIENT) diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index ec93aa5..56acc93 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -288,6 +288,9 @@ class ServerModelManager: self.emitter.emit("already_saved_server") return config_man.add_saved_server(fqip) + # 2026-05-04 + # TODO: this is valid if saved servers tab is already open, + # but not if app was just booted if proxy_man.has_control_model() is False: self._get_proxy_man().push(records) else: @@ -373,8 +376,8 @@ class ServerModelManager: if self.preserve_on_fail is False: # CHORE: test if maps are cleared on failure self.tv.set_model(None) - #filter_man = self.tv.get_filter_man() - #filter_man.set_unique_maps([]) + # filter_man = self.tv.get_filter_man() + # filter_man.set_unique_maps([]) # TODO: emit signal to not disable widget sensitivity # TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty" diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index 75665b6..935ae1f 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -9,7 +9,7 @@ from dzgui.const.constants import ( ) from dayzquery import DayzMod from dzgui.views.components.frame import HeadingFrame -from dzgui.views.trees.tree_server_mods import ServerModTree +from dzgui.views.trees.tree_server_mods import ServerModTreeView import gi @@ -42,7 +42,6 @@ class PreConnectionAssistant(Gtk.Box): self.controller.register_widget("preconnect", self) - # TODO: strings self.back = Gtk.Button(label="Back") # TODO: dynamic button text if no mods needed @@ -58,13 +57,22 @@ class PreConnectionAssistant(Gtk.Box): # set visibility if warnings > 1 frame = HeadingFrame(Gtk.Label(label="ITEM ONE"), "Warnings") - self.tree = ServerModTree(self.controller) + self.tree = ServerModTreeView(self.controller) self.scrolled = Gtk.ScrolledWindow() self.scrolled.add(self.tree) self.scrolled.set_size_request(600, 400) - self.add(self.scrolled) + self.tree_frame = HeadingFrame(self.scrolled, "Mods") + + # TODO: should form a part of tree_frame above + self.mod_count = Gtk.Label(label="") + + self.title = Gtk.Label(label="") + + self.add(self.title) + self.add(self.tree_frame) + self.add(self.mod_count) self.add(frame) self.add(self.button_box) @@ -78,8 +86,17 @@ class PreConnectionAssistant(Gtk.Box): self.controller.open_page(page) def populate(self, res: dict[Any], mods: list["DayzMod"]) -> None: - # TODO: embed tree from dialog, but not dialog itself self.tree.populate(mods) + total = len(mods) + self.title.set_text(f"Connecting to {res["name"]}") + if total < 1: + self.tree.set_visible(False) + self.mod_count.set_visible(False) + return + else: + self.tree.set_visible(True) + self.mod_count.set_visible(True) + self.mod_count.set_text(f"Total mods: {str(total)}") # steam_path = self.controller.get_config_man().lookup(Preferences.DEFAULT) # dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ) # dayz_exp_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP) @@ -94,3 +111,4 @@ class PreConnectionAssistant(Gtk.Box): # TODO: icon for mod signature issue # or "Update mods and connect" + # also handle servers with no mods; do not show tree diff --git a/dzgui/views/trees/tree_server_mods.py b/dzgui/views/trees/tree_server_mods.py index d239d1a..afec950 100644 --- a/dzgui/views/trees/tree_server_mods.py +++ b/dzgui/views/trees/tree_server_mods.py @@ -1,5 +1,6 @@ +from dzgui.const.enum import ContextMenuGroup from dzgui.model.model_factory import ModelFactory -from dzgui.views.mixins.cursor_mixin import CursorMixin +from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.trees.tree_base import TreeView from dzgui.util import strings @@ -14,22 +15,19 @@ if TYPE_CHECKING: from dzgui.controllers.mc import Controller -class ServerModTree(TreeView): # type: ignore +class ServerModTreeView(ContextMixin, TreeView): # type: ignore def __init__(self, controller: "Controller") -> None: - super().__init__(controller) + super().__init__(controller, menu=ContextMenuGroup.SERVER_MOD) - # self.view = TreeView(controller) self.mod_store = ModelFactory().make_server_mod_store() self.set_fixed_height_mode(True) self.set_headers_visible(True) self.set_model(self.mod_store) - # self.scrollable_tree = Gtk.ScrolledWindow() - # self.scrollable_tree.add(self.view) - # self.scrollable_tree.set_size_request(700, 400) - - # self.view.connect("row-activated", self._on_row_activated) + self.connect("button-press-event", self.present_menu) + self.connect("key-press-event", self.present_menu) + self.connect("row-activated", self._on_row_activated) for i, column_title in enumerate(strings.server_mod_cols): renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END) @@ -46,9 +44,26 @@ class ServerModTree(TreeView): # type: ignore case _: pass - # mod_count = len(mods) - # self._set_footer(mod_count) + # TODO: could be problematic if user downloads mods out of band + def _on_row_activated( + self, + treeview: Gtk.TreeView, + path: Gtk.TreePath, + col: Gtk.TreeViewColumn, + ) -> None: + mod = self.get_value_at_index(1) + self.controller.open_workshop_page(mod) + + def get_selected_mod(self) -> str: + path = self.get_focused_row_path() + model = self.get_model() + tree_iter = model.get_iter(path) + mod = model.get(tree_iter, 1)[0] + return str(mod) def populate(self, mods: list[list[str]]) -> None: + self.mod_store.clear() for mod in mods: self.mod_store.append(mod) + path = Gtk.TreePath.new_from_indices([0]) + self.set_cursor(path)