diff --git a/dzgui/api/servers.py b/dzgui/api/servers.py index 1181312..84e2e1d 100644 --- a/dzgui/api/servers.py +++ b/dzgui/api/servers.py @@ -234,6 +234,7 @@ class Ping: @dataclass(slots=True, frozen=True) class Details: + name: Union[str, None] data: Union[list, None] description: str success: bool @@ -272,22 +273,25 @@ def get_prereqs(ip: str, qport: int) -> Prereqs: return Prereqs(is_password, gameport, appid, version) -def details(ip: str, qport: int) -> Details: +def details(record: Record) -> Details: + ip = record.ip + qport = record.qport default_str = strings.none_provided try: info = a2s.info((ip, qport)) + name = info.server_name except TimeoutError: - return Details(None, default_str, False) + return Details(None, None, default_str, False) try: - rules = dayzquery.dayz_rules((ip, int(qport))) + rules = dayzquery.dayz_rules((ip, qport)) except TimeoutError: - return Details(None, default_str, False) + return Details(None, None, default_str, False) try: keywords = info.keywords.split(",") except AttributeError: - return Details(None, default_str, False) + return Details(None, None, default_str, False) battleye = strings.disabled if "battleye" in keywords: @@ -362,7 +366,7 @@ def details(ip: str, qport: int) -> Details: ["Version", version], ] - return Details(rows, description, True) + return Details(name, rows, description, True) def ping(iteration: int, addr: list, qport: int, ping: int) -> Ping: diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index daddfc5..728bdc2 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -449,6 +449,7 @@ class Controller(GObject.GObject): model[path][4] = True self.emitter.emit("mods_highlighted") + # TODO: delegate to mod manager @call_on_thread(strings.dialog.working) def highlight_stale(self) -> None: # TODO: set progress bar for number of mods @@ -609,4 +610,8 @@ class Controller(GObject.GObject): ConnectionManager(self).connect_by_ip(addr) def connect_by_record(self, record: "Record") -> None: + # NOTE: calls on thread ConnectionManager(self).connect_by_record(record) + + def get_details(self, record: "Record") -> None: + ConnectionManager(self).query_details(record) diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index a855f4d..bbd35ba 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -2,9 +2,9 @@ from typing import TYPE_CHECKING import dzgui.api.servers as Servers from dzgui.managers.thread_man import call_on_thread, StoredFunc, ThreadingManager -from dzgui.util.strings import api_warn_msg, dialog +from dzgui.util.strings import api_warn_msg, dialog, server_timeout from dzgui.views.dialogs.generic import ExceptionDialog - +from dzgui.views.dialogs.server_details import ServerDetailsDialog import gi @@ -25,28 +25,57 @@ class ConnectionManager: 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._connection_failure)) + self.thread_man.set_cleanup_func( + StoredFunc(self._server_timeout), destroy_first=True + ) + print("DEBUG") print(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._connection_failure)) + self.thread_man.set_cleanup_func( + StoredFunc(self._server_timeout), destroy_first=True + ) + print("DEBUG") print(res) @call_on_thread(dialog.querying) def connect_by_record(self, record: Servers.Record) -> None: res = Servers.query_by_record(record) if res is None: - self.thread_man.set_cleanup_func(StoredFunc(self._connection_failure)) + self.thread_man.set_cleanup_func( + StoredFunc(self._server_timeout), destroy_first=True + ) # TODO: add to history if successful + print("DEBUG") print(res) - def _connection_failure(self) -> None: - # TODO: more explicit warning message, not necessarily API failure? - dialog = ExceptionDialog(self.controller, api_warn_msg) + @call_on_thread(dialog.querying) + def query_details(self, record: Servers.Record) -> None: + details = Servers.details(record) + if details.success is False: + self.thread_man.set_cleanup_func( + StoredFunc(self._server_timeout), destroy_first=True + ) + return + self.thread_man.set_cleanup_func( + StoredFunc(self._present_details_dialog, details), destroy_first=True + ) + + def _present_details_dialog(self, details: Servers.Details) -> None: + dialog = ServerDetailsDialog(self.controller, details) dialog.run() + + def _server_timeout(self) -> None: + dialog = ExceptionDialog(self.controller, server_timeout) + dialog.run() + + # def _connection_failure(self) -> None: + # # FIXME: returns api warning, but this is more likely a localized server issue + # dialog = ExceptionDialog(self.controller, api_warn_msg) + # dialog.run() diff --git a/dzgui/managers/contextmenu.py b/dzgui/managers/contextmenu.py index 727d28a..771e0e3 100644 --- a/dzgui/managers/contextmenu.py +++ b/dzgui/managers/contextmenu.py @@ -3,7 +3,6 @@ import logging from typing import TYPE_CHECKING from dzgui.const.enum import ContextMenu, Preferences -from dzgui.managers.thread_man import ThreadingManager from dzgui.util import strings from dzgui.util.clip import copy_clipboard from dzgui.util.open_links import open_workshop_page @@ -29,7 +28,6 @@ class ContextMenuManager: ) -> None: self.controller = controller self.treeview = treeview - self.thread_man = ThreadingManager(parent=controller) def process(self, action: ContextMenu) -> None: match action: @@ -80,8 +78,8 @@ class ContextMenuManager: # reverse of ADD_SERVER pass case ContextMenu.SHOW_DETAILS: - print("DEBUG: unimplemented") - pass + record = self.treeview.get_record() + self.controller.get_details(record) case ContextMenu.SHOW_MODS: print("DEBUG: unimplemented") pass diff --git a/dzgui/managers/thread_man.py b/dzgui/managers/thread_man.py index bfe62f1..3aa1283 100644 --- a/dzgui/managers/thread_man.py +++ b/dzgui/managers/thread_man.py @@ -50,7 +50,9 @@ class ThreadingManager: self.parent = parent self.jobs = 1 self.cleanup_func = None - self.alternate_statusbar = None + self.destroy_first = False + + # self.alternate_statusbar = None def call_on_thread(self, dialog_str: str, func: StoredFunc) -> None: def callback() -> None: @@ -72,30 +74,39 @@ class ThreadingManager: GLib.idle_add(lambda: self.wait_dialog.increment(text)) # TODO: this should not be delegated here, set in cleanup func - def set_alternate_statusbar(self, msg: str) -> None: - self.alternate_statusbar = msg + # def set_alternate_statusbar(self, msg: str) -> None: + # self.alternate_statusbar = msg - def get_alternate_statusbar(self) -> Optional[str]: - return self.alternate_statusbar + # def get_alternate_statusbar(self) -> Optional[str]: + # return self.alternate_statusbar - def set_cleanup_func(self, func: StoredFunc) -> None: + def set_cleanup_func(self, func: StoredFunc, destroy_first: bool = False) -> None: if type(func) not in (StoredFunc, type(None)): msg = f"Callback function '{func}' is not of type StoredFunc or None" logger.critical(msg) raise TypeError(msg) + self.destroy_first = destroy_first self.cleanup_func = func def get_cleanup_func(self) -> StoredFunc: return self.cleanup_func - def _d(self) -> None: - self.wait_dialog.hide() - return False + # def _d(self) -> None: + # self.wait_dialog.hide() + # return False def _destroy_on_idle(self) -> GLib.SOURCE_REMOVE: + if self.destroy_first: + self.wait_dialog.destroy() + func = self.get_cleanup_func() if func is not None: func.call() self.set_cleanup_func(None) - self.wait_dialog.destroy() + if not self.destroy_first: + self.wait_dialog.destroy() + return GLib.SOURCE_REMOVE + + def get_wait_dialog(self) -> WaitDialog: + return self.wait_dialog diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index d87d664..01c6acc 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -320,7 +320,7 @@ class ServerModelManager: # TODO: disable map, keyword, and filter widgets if model is None # -> signal driven (servers_empty, servers_failed_to_load) - # NOTE: used by refresh action + # NOTE: used by refresh button action if not self.preserve_on_fail: self.tv.set_model(None) filter_man = self.tv.get_filter_man() @@ -330,6 +330,8 @@ class ServerModelManager: # TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty" # customize statusbar and dialog accordingly self.emitter.emit("servers_loaded", self.enum) + # TODO: destroy wait dialog first + # see threadman.set_cleanup_func(_, destroy_first=True) if show_dialog: dialog = ExceptionDialog(self.controller, api_warn_msg) dialog.run() diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 4ef3159..a737c30 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -102,7 +102,7 @@ build_corrupted = ( api_warn_msg = """No servers returned. Please wait and try again. If this issue persists, your API key may be defunct or your network is blocking requests. """ -server_timeout = "Timed out when querying server, check IP or try again later" +server_timeout = "Timed out when querying server, check IP or try again later." server_error = ( "Error while contacting server, possibly timed out. Please wait and try again." ) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index c1897a5..062e839 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -19,13 +19,6 @@ if TYPE_CHECKING: # dialog_type = Gtk.MessageType.INFO # button_type = Gtk.ButtonsType.OK # header_text = strings.modlist -# case Popup.DETAILS: -# dialog_type = Gtk.MessageType.INFO -# button_type = Gtk.ButtonsType.OK -# header_text = strings.server_details -# TODO: unused -# def update_label(self, text: str) -> None: -# self.format_secondary_text(text) class GenericDialog(Gtk.MessageDialog): diff --git a/dzgui/views/dialogs/server_details.py b/dzgui/views/dialogs/server_details.py index 18aa3d1..3ef6948 100644 --- a/dzgui/views/dialogs/server_details.py +++ b/dzgui/views/dialogs/server_details.py @@ -1,20 +1,36 @@ import gi + +import dzgui.api.servers as Servers +from dzgui.const.constants import EXPAND, FILL, NO_PADDING +from dzgui.const.enum import Popup +from dzgui.util import css +from dzgui.util import strings +from dzgui.util.format import format_hyperlinks +from dzgui.views.dialogs.generic import GenericDialog + +from typing import Self, TYPE_CHECKING + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa -from dzgui.const.constants import EXPAND, FILL, NO_PADDING -from dzgui.const.enum import Popup +if TYPE_CHECKING: + from dzgui.api.servers import Details + class ServerDetailsDialog(GenericDialog): - def __init__(self, server_name: str, ip: str, qport: int): - super().__init__(server_name, Popup.DETAILS) + def __init__(self, controller, details: "Details"): + super().__init__( + controller=controller, + text=details.name, + buttons=Gtk.ButtonsType.OK, + mtype=Gtk.MessageType.INFO, + secondary=strings.server_details, + ) dialog_box = self.get_content_area() self.set_default_response(Gtk.ResponseType.OK) self.set_size_request(800, 700) - self.ip = ip.split(":")[0] - self.qport = qport self.store = Gtk.ListStore(str, str, Pango.Weight) self.view = Gtk.TreeView( @@ -24,13 +40,12 @@ class ServerDetailsDialog(GenericDialog): fixed_height_mode=True, ) self.view.connect("row-activated", self._on_row_activated) + self.view.connect("key-press-event", self._on_keypress) for i, column_title in enumerate(["Item", "Details"]): renderer = Gtk.CellRendererText(xalign=0) if i == 0: - column = Gtk.TreeViewColumn( - column_title, renderer, text=i, weight=2 - ) + column = Gtk.TreeViewColumn(column_title, renderer, text=i, weight=2) else: column = Gtk.TreeViewColumn(column_title, renderer, text=i) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) @@ -43,15 +58,12 @@ class ServerDetailsDialog(GenericDialog): scrollable_tree.add(self.view) scrollable_tree.set_size_request(700, 200) + # TODO: center header text scrollable_message = Gtk.ScrolledWindow() desc = Gtk.Label(label=strings.server_message, valign=Gtk.Align.START) css.add_class(desc, "details-heading") - box = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER - ) - self.description = Gtk.Label( - justify=Gtk.Justification.CENTER, wrap=True - ) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER) + self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True) sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) sep.set_margin_bottom(10) for el in desc, sep, self.description: @@ -61,12 +73,24 @@ class ServerDetailsDialog(GenericDialog): dialog_box.pack_start(scrollable_tree, EXPAND, FILL, NO_PADDING) dialog_box.pack_start(scrollable_message, EXPAND, FILL, NO_PADDING) - self.wait_dialog = GenericDialog(strings.details, Popup.WAIT) - self.wait_dialog.show_all() - thread = threading.Thread( - target=self._background, args=(self.wait_dialog, ip, qport) - ) - thread.start() + for row in details.data: + self.store.append(row + [Pango.Weight.BOLD]) + self.view.set_model(self.store) + text = details.description + text = format_hyperlinks(text) + self.description.set_markup(text) + + self.connect("response", self._on_response) + self.show_all() + + def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> None: + if event.keyval == Gdk.KEY_Escape: + self.destroy() + return True + return False + + def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None: + self.destroy() def _on_row_activated( self, @@ -75,29 +99,3 @@ class ServerDetailsDialog(GenericDialog): col: Gtk.TreeViewColumn, ) -> None: self.destroy() - - def _load(self) -> None: - if self.wait_dialog: - self.wait_dialog.destroy() - if self.success is False: - AppNav.window.spawn_dialog(strings.server_error, Popup.NOTIFY) - return - self.show_all() - self.run() - self.destroy() - - def _background( - self, dialog: "GenericDialog", ip: str, qport: int - ) -> None: - response = Servers.details(self.ip, self.qport) - if response.success: - for row in response.data: - self.store.append(row + [Pango.Weight.BOLD]) - self.view.set_model(self.store) - - text = response.description - text = format_hyperlinks(text) - self.description.set_markup(text) - - self.success = response.success - GLib.idle_add(self._load) diff --git a/dzgui/views/pages/servers.py b/dzgui/views/pages/servers.py index cd85620..61eecbd 100644 --- a/dzgui/views/pages/servers.py +++ b/dzgui/views/pages/servers.py @@ -63,7 +63,6 @@ class ServerNotebook(Gtk.ScrolledWindow): tv = self.get_active_treeview() state = False if tv.get_model() is None else True tv.set_headers_visible(state) - # tv.set_headers_clickable(state) tv.set_loaded(True) tv.grab_focus() @@ -151,9 +150,6 @@ class ServerNotebook(Gtk.ScrolledWindow): tab, col, "_on_col_width_changed", False ) - # def get_tabs(self) -> tuple: - # return (self.browser, self.saved, self.recent, self.lan) - def get_browser(self) -> ServerTreeView: return self.browser diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index e9ba1f8..3e2c380 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -283,7 +283,8 @@ class ServerTreeView(ContextMixin, TreeView): def _parent_row_activated( self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn ) -> None: - print(self.get_value_at_index(0)) + record = self.get_record() + self.controller.connect_by_record(record) def _parent_selection_changed(self, base_class: TreeView, sel: Gtk.TreeSelection): if self.loaded is False: