diff --git a/dzgui/api/servers.py b/dzgui/api/servers.py index f2a3aaf..728bbda 100644 --- a/dzgui/api/servers.py +++ b/dzgui/api/servers.py @@ -361,14 +361,14 @@ def details(ip: str, qport: int) -> Details: return Details(rows, description, True) -def ping(iteration: int, row: list) -> Ping: - addr = row[7] - qport = row[8] +def ping(iteration: int, addr: list, qport: int, ping: int) -> Ping: + # addr = row[7] + # qport = row[8] res = None - if row[9] != 9999: - return Ping(addr, iteration, row[9]) + if ping != 9999: + return Ping(addr, iteration, ping) try: ip = addr.split(":")[0] diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index f9fdfe8..501e392 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -115,9 +115,12 @@ class Controller(GObject.GObject): self.emitter = Emitter() self.emitter.connect("map_selection_changed", self._on_map_selection_changed) self.emitter.connect("check_toggled", self._on_check_toggled) + self.emitter.connect("servers_loaded", self._on_servers_loaded) + self.emitter.connect("servers_loaded_init", self._on_servers_loaded_init) # NOTE: suppress requests until entire UI is loaded self.loaded = False + self.pending_jobs = 1 def get_emitter(self) -> Emitter: return self.emitter @@ -131,7 +134,7 @@ class Controller(GObject.GObject): GLib.idle_add(self._destroy_on_idle) self = args[0] - self.wait_dialog = WaitDialog(self, dialog_str, True) + self.wait_dialog = WaitDialog(self, dialog_str, jobs=self.pending_jobs) self.wait_dialog.show_all() thread = threading.Thread(target=callback) thread.start() @@ -388,48 +391,29 @@ class Controller(GObject.GObject): params = Servers.params serv = [] i = 0 - total = 10 with ThreadPoolExecutor() as executor: futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params] - # wait(futures) for future in as_completed(futures): try: i += 1 - GLib.idle_add(lambda: self.wait_dialog.prog.set_fraction(i / total)) + GLib.idle_add(lambda: self.wait_dialog.increment()) res = future.result(timeout=3) if res.status != 200 or not res.parsed: - # TODO: pop warning dialog, create enum around various failure states - # set failure type here self.cleanup_func = CleanupFunc(self.cleanup_on_failure) return j = res.json serv += j["response"]["servers"] except Exception as e: - # TODO: save exception text - print(e) + logger.critical(e) self.cleanup_func = CleanupFunc(self.cleanup_on_failure) - """ - TODO: e.g. class method like 'total' - total of 10 params for this call - @call_on_thread(msg, jobs=10) - if total > 1, pop progress bar - GLib.idle_add(lambda: self.wait_dialog.format_secondary_text("Pass 2")) - GLib.idle_add(lambda: self.wait_dialog.prog.set_fraction(0.5)) - subclass or wrap add fraction and increment, then calculate off of total - This next step is allowed to fail, since this metadata is incidental - """ + # NOTE: This step is allowed to fail, since this metadata is incidental res = Servers.query_api(key, APPID_DAYZ_EXP, "") if res.status == 200 and res.parsed is True: j = res.json serv += j["response"]["servers"] - GLib.idle_add( - lambda: self.wait_dialog.format_secondary_text("Unpacking servers") - ) - GLib.idle_add(lambda: self.wait_dialog.prog.set_fraction(10 / total)) - - # TODO: additional ping column pass, collated + GLib.idle_add(lambda: self.wait_dialog.increment("Unpacking servers")) parsed = Servers.parse_json(serv) self.push_data_success(parsed, FilterMode.INITIAL) @@ -466,7 +450,6 @@ class Controller(GObject.GObject): return value def copy_name(self, path: Gtk.TreePath) -> None: - # TODO: column values are deterministic, perhaps use a col name to index map name = self.get_col_value_by_path_index(path, 0) if name is None: return @@ -659,13 +642,13 @@ class Controller(GObject.GObject): sel.select_path(path) # TODO: make as method of tree? - # FIXME: move cursor when finished def uncolorize_mods(self) -> None: model = self.get_mod_store() for mod in model: it = mod.iter path = model.get_path(it) model[path][4] = None + self.mediator.modtreeview.set_cursor(0) def dump_test_2(self) -> None: import time @@ -833,8 +816,10 @@ class Controller(GObject.GObject): return self.get_active_treeview().get_map_man() def cleanup_on_success(self) -> None: + self.pending_jobs = 1 treeview = self.get_active_treeview() treeview.set_loaded(True) + treeview.set_model(self.to_insert) map_man = treeview.get_map_man() @@ -842,6 +827,7 @@ class Controller(GObject.GObject): # model insertion after thread closes # cf. servers_loaded signal + # TODO: servers_loaded vs servers_reloaded context = self.get_active_context() self.emitter.emit("servers_loaded", context) @@ -856,27 +842,29 @@ class Controller(GObject.GObject): def cleanup_on_failure(self) -> None: treeview = self.get_active_treeview() - treeview.set_model(None) treeview.set_loaded(True) map_man = treeview.get_map_man() - # TODO: what if refresh action occurred, and the old model is still valid? + # TODO: disable map, keyword, and filter widgets if model is None # -> signal driven (servers_empty) - map_man.set_unique_maps(None) + # TODO: what if refresh action occurred and failed, and the old model is still valid? + # skip the step below if refresh action failed + # do not wipe control model in this case + # e.g. if treeview.is_refresh(): + # revert old model + # wipe refresh state to False - context = self.get_active_context() - # TODO: distinguish signals, e.g. "servers_failed_to_load" - self.emitter.emit("servers_loaded", context) + treeview.set_model(None) treeview.grab_focus() - dialog = ExceptionDialog(self, "API TIMEOUT") - dialog.run() - # def push_data_failure(self) -> None: - # #treeview = self.get_active_treeview() - # #treeview.set_loaded(True) - # # TODO: wipe control model on failure or keep old results? - # # manager = treeview.get_filter_man() - # self.cleanup_func = CleanupFunc(self.cleanup_on_failure) + map_man.set_unique_maps(None) + context = self.get_active_context() + + # TODO: distinguish signals, e.g. "servers_failed_to_load" + # may trigger different behavior + self.emitter.emit("servers_loaded", context) + dialog = ExceptionDialog(self, strings.api_warn_msg) + dialog.run() def push_data_success(self, data: tuple, mode: Optional[FilterMode]) -> None: # FIXME: set outside of thread @@ -901,6 +889,7 @@ class Controller(GObject.GObject): self.cleanup_func = CleanupFunc(self.cleanup_on_success) def highlight_stale_cleanup(self, stale_mods: list) -> None: + """Manipulates attached ListStore in the main event loop""" model = self.get_mod_store() for mod in model: it = mod.iter @@ -911,6 +900,7 @@ class Controller(GObject.GObject): @call_on_thread(strings.dialog.working) def highlight_stale(self) -> None: + # TODO: set progress bar for number of mods stale = find_stale_mods(self.prefs.paths.config) self.cleanup_func = CleanupFunc(self.highlight_stale_cleanup, stale) @@ -995,6 +985,7 @@ class Controller(GObject.GObject): @call_on_thread(strings.dialog.fetching) def run_query_func(self, func: Callable) -> None: + # TODO: use CleanupFunc -> StoredFunc func() @call_on_thread(strings.dialog.filtering) @@ -1010,6 +1001,7 @@ class Controller(GObject.GObject): # TODO: call filter_man methods directly def refilter_model(self, mode: FilterMode, label: Optional[str] = None) -> None: tv = self.get_active_treeview() + #tv.freeze_child_notify() filter_man = tv.get_filter_man() if filter_man.get_control() is None: return @@ -1022,7 +1014,7 @@ class Controller(GObject.GObject): self.emitter.emit("servers_loaded", treeview.get_enum()) return - func = treeview.get_query_func() + func, jobs = treeview.get_query_func() if func is None: self.emitter.emit("servers_loaded", treeview.get_enum()) treeview.set_model(None) @@ -1031,8 +1023,10 @@ class Controller(GObject.GObject): # TODO: clear ephemeral model if necessary # manager = treeview.get_filter_man() # manager.clear_model() + #treeview.fancy_col.set_cell_data_func(treeview.fancy_rend, None) self.first_iteration = True + self.pending_jobs = jobs self.run_query_func(func) def get_favorite(self) -> tuple[str, str] | tuple[None, None]: @@ -1080,6 +1074,27 @@ class Controller(GObject.GObject): map_man = self.get_map_man() return map_man.get_prior_map() + def get_active_map(self) -> None: + return self.get_map_man().get_active_map() + + def set_active_map(self, ind: int) -> None: + self.get_map_man().set_active_map(ind) + + def _on_servers_loaded_init(self, emitter: "Emitter") -> None: + # FIXME: wipe maps store when changing tabs if model is none + tv = self.get_active_treeview() + if tv.loaded is False: + return + store = self.get_map_store() + self.emitter.emit("load_maps", store) + + def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None: + # NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None + state = self.has_server_model() + tv = self.get_active_treeview() + tv.set_headers_visible(state) + tv.set_headers_clickable(state) + def _on_check_toggled(self, emitter: Emitter, label: str, state: bool) -> None: map_man = self.get_map_man() map_man.set_filter(label, state) diff --git a/dzgui/model/filtered_model.py b/dzgui/model/filtered_model.py index dffd952..18bd750 100644 --- a/dzgui/model/filtered_model.py +++ b/dzgui/model/filtered_model.py @@ -1,6 +1,7 @@ +import datetime import re from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from warnings import deprecated from dzgui.const.enum import FilterMode @@ -114,16 +115,18 @@ class FilteredModelManager: case FilterMode.TOGGLE_ON: rows = self.filter_toggle_on(filters, *args) - if mode is not FilterMode.INITIAL: - for row in rows: - if row[7] in self.ping_cache: - row[9] = self.ping_cache[row[7]] + # if mode is not FilterMode.INITIAL: + # for row in rows: + # if row[7] in self.ping_cache: + # row[9] = self.ping_cache[row[7]] clone = self.new_model_from_class(ServerColumns) if len(rows) > 0: + # TODO: considering exposing an option to not sort rows rows = self.sort_rows(rows) for row in rows: clone.append(row) + # else: # print("nothing to filter, sending none") # clone = None @@ -256,7 +259,7 @@ class FilteredModelManager: def convert_model_to_list(self, model: ListStore) -> list: return [[el for el in row] for row in model] - def set_filtered(self, rows: list | None) -> None: + def set_filtered(self, rows: Optional[list]) -> None: if rows is None: rows = [] self.filtered = rows diff --git a/dzgui/model/map_model.py b/dzgui/model/map_model.py index fe08b1f..9af925b 100644 --- a/dzgui/model/map_model.py +++ b/dzgui/model/map_model.py @@ -31,12 +31,19 @@ class MapManager: strings.filter_unofficial: True, strings.filter_modded: True, } + self.active_map = 0 self.filters: list self.enabled_filters = dict(self.default_filters) def reinit_filters(self) -> None: self.enabled_filters = dict(self.default_filters) + def get_active_map(self) -> int: + return self.active_map + + def set_active_map(self, ind: int) -> None: + self.active_map = ind + def get_default_filters(self) -> dict: """Deep copy of defaults""" return dict(self.default_filters) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 0d37986..5a39119 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -99,10 +99,8 @@ steam_missing = "Local Steam installation is not set, possibly malformed config build_corrupted = ( "Steam settings or DayZ installation may be corrupted. Try restarting Steam." ) -api_warn_msg = """No servers returned. Possible causes: -no servers in favorites/history, local network issue, or API key on cooldown. -Return to the main menu, wait 30s, and try again. -If this issue persists, your API key may be defunct. +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_error = ( @@ -167,7 +165,7 @@ ping_tooltip = ( ) # statusbar_helptext = "Select a row to see its detailed description" -#statusbar_helptext = "No server metadata to list." +# statusbar_helptext = "No server metadata to list." # use e.g. filters.1pp filter_1pp = "1PP" diff --git a/dzgui/views/components/filter_panel.py b/dzgui/views/components/filter_panel.py index 86ed56f..a8ea9bb 100644 --- a/dzgui/views/components/filter_panel.py +++ b/dzgui/views/components/filter_panel.py @@ -153,7 +153,6 @@ class FilterPanel(Gtk.Box): "request_maps_focus", lambda _: self.maps_entry.grab_focus() ) self.emitter.connect("check_button_pressed", self.toggle_check_by_key) - # self.emitter.connect("servers_loaded", self._on_servers_loaded) self.emitter.connect("load_maps", self._on_maps_loaded) # TODO: break into MapsCombo class @@ -177,6 +176,9 @@ class FilterPanel(Gtk.Box): self.maps_combo.connect("changed", self._on_map_changed) self.maps_combo.connect("key-press-event", self._on_combo_keypress) + # FIXME: should be a property of treeview's meta manager + self.active_map = 0 + for el in ( self.filters_label, self.keyword_entry, @@ -194,10 +196,12 @@ class FilterPanel(Gtk.Box): if text == row[0]: self.maps_combo.set_active(i) self._on_map_changed(self.maps_combo) + self.controller.set_active_map(i) def _on_maps_loaded(self, emitter: "Emitter", store: Gtk.ListStore) -> None: self.maps_combo.set_model(store) - self.maps_combo.set_active(0) + ind = self.controller.get_active_map() + self.maps_combo.set_active(ind) self.button_grid.reload_filters() # TODO: move into metamanager @@ -259,6 +263,8 @@ class FilterPanel(Gtk.Box): store = self.controller.get_map_store() if len(text) >= completion.get_minimum_key_length(): completion.set_model(store) + ind = self.get_active_combo() + self.controller.set_active_map(ind) def restore_focus_to_treeview(self) -> Literal[False]: view = self.controller.get_active_treeview() diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index a8f7232..fc26b61 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -44,9 +44,10 @@ class RightPanel(Gtk.Box): state = self.controller.has_server_model() for widget in (self.refresh_button, self.filters_vbox): widget.set_sensitive(state) + # FIXME: may wish to reload these in place to show new data - if context in (ServerTab.RECENT, ServerTab.SAVED): - self.refresh_button.set_sensitive(False) + # if context in (ServerTab.RECENT, ServerTab.SAVED): + # self.refresh_button.set_sensitive(False) # TODO: reference for ping pass # def _on_ping_clicked(self, button: Gtk.Button) -> None: diff --git a/dzgui/views/dialogs/entry.py b/dzgui/views/dialogs/entry.py deleted file mode 100644 index 64d57a8..0000000 --- a/dzgui/views/dialogs/entry.py +++ /dev/null @@ -1,83 +0,0 @@ -import gi - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa - -from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING -from dzgui.const.enum import Popup - - -class EntryDialog(GenericDialog): - def __init__(self, text: str, mode: Popup, link: str, button_type=None): - super().__init__(text, mode) - - """ - Wraps Gtk.Entry in a dialog and provides basic response handling. - Returns user input as a string or None. - The Entry widget itself can be manipulated via the get_entry() method. - """ - - self.dialog = GenericDialog(text, mode) - self.dialogBox = self.dialog.get_content_area() - self.dialog.set_default_response(Gtk.ResponseType.OK) - self.dialog.set_size_request(500, 0) - - self.user_entry = Gtk.Entry() - set_surrounding_margins(self.user_entry, 20) - self.user_entry.set_margin_top(0) - self.user_entry.set_size_request(250, 0) - self.user_entry.set_activates_default(True) - self.dialogBox.pack_start(self.user_entry, NO_EXPAND, NO_FILL, NO_PADDING) - - if link: - button = Gtk.Button(label=link) - button.set_margin_start(60) - button.set_margin_end(60) - button.connect("clicked", self._on_button_clicked, button_type) - self.dialogBox.pack_end(button, NO_EXPAND, NO_FILL, NO_PADDING) - - self.ok = self.dialog.action_area.get_children()[1] - self.ok.set_sensitive(False) - self.user_entry.connect("insert-text", self._on_text_typed) - self.user_entry.get_property("buffer").connect( - "deleted-text", self._on_text_deleted - ) - - def _is_valid_text(self, text: str) -> bool: - if text.isspace(): - return False - if len(text) == 0: - return False - return True - - def _on_text_deleted( - self, buffer: Gtk.EntryBuffer, position: int, chars: int - ) -> None: - text = buffer.get_text() - state = self._is_valid_text(text) - self.ok.set_sensitive(state) - - def _on_text_typed( - self, entry: Gtk.Entry, text: str, length: int, pos: int - ) -> None: - state = self._is_valid_text(text) - self.ok.set_sensitive(state) - - def _on_button_clicked(self, button: Gtk.Button, enum: RowType) -> None: - result = open_links.open_link_by_rowtype(enum) - if result is False: - AppNav.window.spawn_dialog(strings.something_wrong, Popup.NOTIFY) - - def get_entry(self) -> Gtk.Entry: - return self.user_entry - - def get_input(self) -> str | None: - self.dialog.show_all() - - response = self.dialog.run() - text = self.user_entry.get_text() - self.dialog.destroy() - if (response == Gtk.ResponseType.OK) and (text != ""): - return text - else: - return None diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 738fdd3..fd41306 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -101,9 +101,7 @@ class NotifyDialog(GenericDialog): class WaitDialog(GenericDialog): - def __init__( - self, controller: "Controller", secondary: str, show_progress: bool = False - ): + def __init__(self, controller: "Controller", secondary: str, jobs: int = 1): super().__init__( controller=controller, text=strings.wait, @@ -112,16 +110,26 @@ class WaitDialog(GenericDialog): secondary=secondary, ) + self.jobs = jobs + self.cur_job = 1 + self.connect("delete-event", self._on_dialog_delete) content = self.get_content_area() spinner = Gtk.Spinner() self.prog = Gtk.ProgressBar() content.pack_end(spinner, NO_EXPAND, NO_FILL, 0) - if show_progress: + if self.jobs > 1: content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0) else: spinner.start() + def increment(self, msg: str = "") -> None: + if msg != "": + self.format_secondary_text(msg) + fraction = self.cur_job / self.jobs + self.prog.set_fraction(fraction) + self.cur_job += 1 + def _on_dialog_delete( self, response_id: Gtk.ResponseType, event: Gdk.Event ) -> Literal[True]: diff --git a/dzgui/views/dialogs/link_dialog.py b/dzgui/views/dialogs/link_dialog.py index 57e6c09..fa36460 100644 --- a/dzgui/views/dialogs/link_dialog.py +++ b/dzgui/views/dialogs/link_dialog.py @@ -1,14 +1,13 @@ import textwrap from typing import Self, TYPE_CHECKING -from dzgui.const.enum import Popup, Preferences from dzgui.const.constants import NO_EXPAND, NO_FILL -from dzgui.util.open_links import open_user_workshop from dzgui.util.strings import notice from dzgui.views.dialogs.generic import GenericDialog -from dzgui.views.components.buttons import SteamTextButton, WebButton +from dzgui.views.components.buttons import SteamTextButton import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402 @@ -18,9 +17,17 @@ if TYPE_CHECKING: class WorkshopLinkDialog(GenericDialog): - def __init__(self, controller: "Controller", text: str, button_label: str, uid: str): + def __init__( + self, controller: "Controller", text: str, button_label: str, uid: str + ): text = textwrap.dedent(text).replace("\n", " ") - super().__init__(controller, text=notice, mtype=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.OK, secondary=text) + super().__init__( + controller, + text=notice, + mtype=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + secondary=text, + ) self.controller = controller self.dialogBox = self.get_content_area() self.set_default_response(Gtk.ResponseType.OK) @@ -43,12 +50,10 @@ class WorkshopLinkDialog(GenericDialog): def _on_button_clicked(self, button: Gtk.Button, uid: str) -> None: # TODO: currently checks user id on instantiation self.controller.open_user_workshop(uid) - #client = self.controller.query_config(Preferences.CLIENT) - #open_user_workshop(uid, client) + # client = self.controller.query_config(Preferences.CLIENT) + # open_user_workshop(uid, client) - def _on_dialog_response( - self, dialog: Self, resp: Gtk.ResponseType - ) -> None: + def _on_dialog_response(self, dialog: Self, resp: Gtk.ResponseType) -> None: match resp: case Gtk.ResponseType.DELETE_EVENT: return diff --git a/dzgui/views/mixins/context_mixin.py b/dzgui/views/mixins/context_mixin.py index b75515c..753e2b3 100644 --- a/dzgui/views/mixins/context_mixin.py +++ b/dzgui/views/mixins/context_mixin.py @@ -20,27 +20,21 @@ class ContextMixin(TreeView): ) -> None: # FIXME: double click causes issue # split into present by click and present by key + if self.is_selection_empty(): return False - if event.type is Gdk.EventType.BUTTON_PRESS: - if event.button != 3: - return False - try: - pathinfo = self.get_path_at_pos(int(event.x), int(event.y)) - if pathinfo is None: - return True - (path, col, cellx, celly) = pathinfo - if path is None: - return True - self.set_cursor(path, col, False) - except AttributeError: - pass - - if event.type is Gdk.EventType.KEY_PRESS: - if event.state is not Gdk.ModifierType.CONTROL_MASK: - return False - if event.keyval is not Gdk.KEY_l: + match event.type: + case Gdk.EventType.BUTTON_PRESS: + if event.button != 3: + return False + self._process_button_event(event) + case Gdk.EventType.KEY_PRESS: + if event.state is not Gdk.ModifierType.CONTROL_MASK: + return False + if event.keyval is not Gdk.KEY_l: + return False + case _: return False group = self.menu @@ -64,6 +58,18 @@ class ContextMixin(TreeView): self.context_menu.select_first(False) + def _process_button_event(self, event: Gdk.EventButton) -> None: + try: + pathinfo = self.get_path_at_pos(int(event.x), int(event.y)) + if pathinfo is None: + return True + (path, col, cellx, celly) = pathinfo + if path is None: + return True + self.set_cursor(path, col, False) + except AttributeError: + pass + def _on_menu_click(self, widget: Gtk.MenuItem, enum: ContextMenu) -> None: """ Local mods page allows multi selection, so ensure that only focused row diff --git a/dzgui/views/pages/servers.py b/dzgui/views/pages/servers.py index 5cdfeaf..f891f67 100644 --- a/dzgui/views/pages/servers.py +++ b/dzgui/views/pages/servers.py @@ -37,8 +37,9 @@ class ServerNotebook(Gtk.ScrolledWindow): self.lan = ServerTreeView(controller, ServerTab.LAN, ContextMenuGroup.SCAN_LAN) self.recent.set_query_func(self.controller.dump_test_2) + # TODO: number of servers will not be known a priori until reading file during query phase self.lan.set_query_func(self.controller.dump_test_2) - self.browser.set_query_func(self.controller.dump_api) + self.browser.set_query_func(self.controller.dump_api, jobs=11) tabs = [ (self.browser, server_labels.browser), diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index fdcaf6b..4ccf9de 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -7,12 +7,14 @@ from warnings import deprecated from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.const.enum import ContextMenuGroup, ServerTab from dzgui.api.servers import Record +from dzgui.api.servers import ping as Ping from dzgui.model.map_model import MapManager from dzgui.model.filtered_model import FilteredModelManager from dzgui.util.dist import CalcDist from dzgui.util import strings from typing import Callable, Literal, TYPE_CHECKING + from dzgui.views.trees.tree_base import TreeView import dzgui.util._json as JSON # noqa @@ -56,6 +58,9 @@ class ServerTreeView(ContextMixin, TreeView): self.queue_id: int self.handler_id: int + self.query_func_jobs = 1 + + self.seen_cache = [] self.current_proc = None self.queue = multiprocessing.Queue() @@ -95,17 +100,21 @@ class ServerTreeView(ContextMixin, TreeView): else: w = width_map[column_title] column.set_fixed_width(w) - #if column_title == "Name": + #if column_title == "Ping": + #self.fancy_col = column + #self.fancy_rend = renderer + #column.set_cell_data_func(renderer, self._get_ping) + # if column_title == "Name": # column.set_fixed_width(800) - #if column_title == "Map": + # if column_title == "Map": # column.set_fixed_width(300) # TODO: standardize widths based on column title and longest content - #if column_title == "Name": + # if column_title == "Name": # column.set_fixed_width(500) - #if column_title == "Map": + # if column_title == "Map": # column.set_fixed_width(200) - #if column_title == "IP": + # if column_title == "IP": # column.set_fixed_width(240) column.connect("notify::fixed-width", self._on_col_width_changed) @@ -119,55 +128,47 @@ class ServerTreeView(ContextMixin, TreeView): self.connect("map", self._on_map) self.connect("unmap", self._on_unmap) - self.emitter.connect("servers_loaded_init", self._on_servers_loaded_init) - self.emitter.connect("servers_loaded", self._on_servers_loaded) + def _get_ping( + self, + column: Gtk.TreeViewColumn, + cell: Gtk.CellRendererText, + model: Gtk.TreeModel, + it: Gtk.TreeIter, + data: Any, + ): + def ping_server(model, _iter, ip: str, qport: int, ping: int): + #res = Ping(0, model[_iter]) + res = Ping(0, ip, qport, ping) + ping = res.ping + print(res) + GLib.idle_add(lambda: model.set(_iter, ping_column, ping)) - def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None: - # FIXME: gets called on every tab, should be localized only to that context - # hook signal from main controller - state = self.controller.has_server_model() - # NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None - self.set_headers_visible(state) - self.set_headers_clickable(state) + addr_column = 7 + qport_column = 8 + ping_column = 9 + # TODO: unpack model before entering thread + # test 40k servers with unpacked model + addr = model.get_value(it, addr_column).split(":")[0] + qport = model.get_value(it, qport_column) + ping = model.get_value(it, ping_column) + ip = f"{addr}:{qport}" - def _on_servers_loaded_init(self, emitter: "Emitter") -> None: - if self.loaded is False: + if ip in self.seen_cache: return - store = self.map_man.get_map_store() - self.emitter.emit("load_maps", store) + self.seen_cache.append(ip) - #def _get_ping( - # self, - # column: Gtk.TreeViewColumn, - # cell: Gtk.CellRendererText, - # model: Gtk.TreeModel, - # it: Gtk.TreeIter, - # data: Any, - #): - # def ping(model, it, ip: str): - # # TODO: use a2s to ping server - # # a2s.info -> "ping" key - # # Servers.ping() -> this accepts a whole row - # # try, if failure just abort - # self.res = ip.replace(":", "%") - # num = 11111 - # GLib.idle_add(lambda: model.set(it, 9, num)) - - # addr = model.get_value(it, 7).split(":")[0] - # qport = model.get_value(it, 8) - # ip = f"{addr}:{qport}" - - # thread = threading.Thread( - # daemon=True, - # target=ping, - # args=( - # model, - # it, - # ip, - # ), - # ) - # thread.start() - # pass + thread = threading.Thread( + daemon=True, + target=ping_server, + args=( + model, + it, + ip, + qport, + ping + ), + ) + thread.start() def start_timeout(self) -> None: self.queue_id = GLib.timeout_add(QUEUE_CHECK_DELAY, self._check_result_queue) @@ -178,7 +179,7 @@ class ServerTreeView(ContextMixin, TreeView): def get_filter_man(self) -> FilteredModelManager: return self.filter_man - #def shrink_to_fit(self) -> None: + # def shrink_to_fit(self) -> None: # cols = self.get_columns() # # TODO: run on only one treeview and propagate results # # TODO: does not shrink name, map, ip fields to fit @@ -208,8 +209,8 @@ class ServerTreeView(ContextMixin, TreeView): if self.get_enum() is ServerTab.LAN: self.emitter.emit("lan_tab_toggled", True) - # FIXME: only if the tab is active store = self.map_man.get_map_store() + # FIXME: if model is none, wipe maps self.emitter.emit("load_maps", store) self.handler_id = self.emitter.connect("statusbar_loaded", self.start_distcalc) self.start_timeout() @@ -222,11 +223,12 @@ class ServerTreeView(ContextMixin, TreeView): if self.get_enum() is ServerTab.LAN: self.emitter.emit("lan_tab_toggled", False) - def set_query_func(self, func: Callable) -> None: + def set_query_func(self, func: Callable, jobs: int = 1) -> None: self.query_func = func + self.query_func_jobs = jobs def get_query_func(self) -> Callable | None: - return self.query_func + return self.query_func, self.query_func_jobs def _on_col_width_changed( self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt