diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 8215c51..ebdcdc3 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -112,6 +112,8 @@ class Controller(GObject.GObject): self.prefs: UserPrefs self.cleanup_func: StoredFunc = None + #self.config_man = ConfigManager() + self.emitter = Emitter() self.emitter.connect("map_selection_changed", self._on_map_selection_changed) self.emitter.connect("check_toggled", self._on_check_toggled) @@ -150,12 +152,15 @@ class Controller(GObject.GObject): logger.critical(f"{attr} is not a valid AppNavigation attribute.") def get_prefs(self) -> UserPrefs: + # return self.config_man.get_prefs() return self.prefs def set_prefs(self, prefs: UserPrefs) -> None: + # self.config_man.set_prefs() self.prefs = prefs def query_config(self, key: Preferences) -> str | bool | list: + # return self.config_man.lookup(key) config = self.prefs.paths.config return lookup(config, key) @@ -201,6 +206,7 @@ class Controller(GObject.GObject): widget.handler_unblock_by_func(func) def toggle_debug_mode(self) -> None: + # self.config_man().toggle_config(Preferences.DEBUG) self.toggle_config(Preferences.DEBUG) def get_active_context(self) -> NotebookPage: @@ -217,6 +223,7 @@ class Controller(GObject.GObject): treeview = self.get_active_treeview() columns = treeview.get_columns() + #columns_file = self.config_man.get_columns() columns_file = self.prefs.paths.columns try: data = JSON.read_json(columns_file) @@ -242,6 +249,7 @@ class Controller(GObject.GObject): w, h = self.mediator.window.get_size() data = {"res": {"width": w, "height": h}} + #res_path = self.config_man.get_resolution() res_path = self.prefs.paths.resolution try: write_json(data, res_path) @@ -279,6 +287,7 @@ class Controller(GObject.GObject): if haversine is None: dist = "Unknown" else: + # self.config_man.use_miles() if self.prefs.use_miles: raw = round(haversine.as_miles()) separated = number(raw) @@ -651,25 +660,6 @@ class Controller(GObject.GObject): model[path][4] = None self.mediator.modtreeview.set_cursor(0) - def dump_test_2(self) -> None: - data = ( - [ - "BAR", - "chernarusplus", - "a", - "a", - 1, - 1, - 1, - "185.207.214.16:2302", - 1, - 1, - "a", - False, - ], - ) - self.push_data_success(data, FilterMode.INITIAL) - def get_map_man(self) -> "MapManager": return self.get_active_treeview().get_map_man() @@ -928,6 +918,7 @@ class Controller(GObject.GObject): self.emitter.emit("load_maps", store) def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None: + return # NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None # TODO: this should be internal to servers page state = self.has_server_model() diff --git a/dzgui/managers/thread_man.py b/dzgui/managers/thread_man.py index 18e0d3d..7eafcf6 100644 --- a/dzgui/managers/thread_man.py +++ b/dzgui/managers/thread_man.py @@ -3,7 +3,7 @@ import logging import threading from functools import wraps -from typing import Callable +from typing import Callable, Optional from dzgui.views.dialogs.generic import WaitDialog @@ -49,6 +49,7 @@ class ThreadingManager: self.parent = parent self.jobs = 1 self.cleanup_func = None + self.alternate_statusbar = None def call_on_thread(self, dialog_str: str, func: StoredFunc) -> None: def callback() -> None: @@ -69,6 +70,13 @@ class ThreadingManager: def increment_dialog_with_str(self, text: str) -> None: GLib.idle_add(lambda: self.wait_dialog.increment(text)) + # TODO: this should not be delegated here + def set_alternate_statusbar(self, msg: str) -> None: + self.alternate_statusbar = msg + + def get_alternate_statusbar(self) -> Optional[str]: + return self.alternate_statusbar + def set_cleanup_func(self, func: StoredFunc) -> None: if type(func) not in (StoredFunc, type(None)): msg = f"Callback function '{func}' is not of type StoredFunc or None" diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index d0abc37..bfceaca 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -1,5 +1,6 @@ from datetime import datetime import logging +import threading from concurrent.futures import wait, as_completed from concurrent.futures import ThreadPoolExecutor @@ -21,6 +22,8 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib # noqa E402 +LAN_TIMEOUT = 0.5 +API_TIMEOUT = 3 if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -29,6 +32,10 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) + +# TODO: failure: spawns error dialog +# TODO: non failure with empty model: updates statusbar + class ServerModelManager: def __init__(self, controller: "Controller", tv: Gtk.TreeView, first_iteration=False) -> None: @@ -37,6 +44,7 @@ class ServerModelManager: self.emitter = controller.get_emitter() if tv.is_loaded(): self.emitter.emit("servers_loaded", enum) + return self.tv = tv self.jobs = 1 @@ -61,7 +69,8 @@ class ServerModelManager: # TODO: get row count self._dump_history() case ServerTab.LAN: - self._dump_lan() + return + #self._dump_lan() @call_on_thread(dialog.fetching) def _dump_api(self) -> None: @@ -74,7 +83,7 @@ class ServerModelManager: for future in as_completed(futures): try: self.thread_man.increment_dialog() - res = future.result(timeout=3) + res = future.result(timeout=API_TIMEOUT) if res.status != 200 or not res.parsed: self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure)) return @@ -92,15 +101,12 @@ class ServerModelManager: j = res.json servers += j["response"]["servers"] - # TODO: strings - #self.thread_man.increment_dialog_with_str("Unpacking servers") - # TODO: try/except + # TODO: try/except when parsing parsed = Servers.parse_json(servers) - self._push_data_success(parsed, FilterMode.INITIAL) + self._push_data(parsed, FilterMode.INITIAL) - # TODO: strings - @call_on_thread("scanning LAN ports") - def _dump_lan(self, port: int, early_abort: bool) -> None: + @call_on_thread(dialog.scanning) + def dump_lan(self, port: int, early_abort: bool) -> None: servers = [] ports = range(1, 256) @@ -111,24 +117,24 @@ class ServerModelManager: ] for future in as_completed(futures): try: - res = future.result(timeout=0.5) + res = future.result(timeout=LAN_TIMEOUT) if res is not None and early_abort is True: # NOTE: first non-empty hit, flag pending threads to close event.set() servers.append(res) - self.cleanup_func = StoredFunc(self._cleanup_on_success) + self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success)) return if res is None: continue servers.append(res) except Exception as e: logger.critical(e) - self.cleanup_func = StoredFunc(self._cleanup_on_failure) + self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure)) if len(servers) == 0: - self.cleanup_func = StoredFunc(self._cleanup_on_failure) + self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure)) return parsed = Servers.parse_json(servers) - self._push_data_success(parsed, FilterMode.INITIAL) + self._push_data(parsed, FilterMode.INITIAL) # TODO: strings @call_on_thread("dumping ips") @@ -136,10 +142,11 @@ class ServerModelManager: # NOTE: block malformed records (TODO: add github issue no.) # TODO: sanitize ip list at config time, drop this ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""] + job = Servers.query_direct with ThreadPoolExecutor() as executor: futures = [ executor.submit( - Servers.query_direct, + job, ip.split(":")[0], int(ip.split(":")[2]), ) @@ -154,12 +161,11 @@ class ServerModelManager: continue servers.append(res) if len(servers) == 0: - self.cleanup_func = StoredFunc(self._cleanup_on_failure) + self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure)) return - # NOTE: 1 extra progress bar pass for parsing parsed = Servers.parse_json(servers) - self._push_data_success(parsed, FilterMode.INITIAL) + self._push_data(parsed, FilterMode.INITIAL) def _query_ip_id(self, addr: str) -> None: # NOTE: Battlemetrics @@ -177,7 +183,8 @@ class ServerModelManager: # TODO: strings @call_on_thread("querying address") def _connect_by_id_or_ip(self, addr: str) -> None: - res = self.query_ip_id(addr) + res = self._query_ip_id(addr) + # TODO: unimplemented @call_on_thread("querying address") def _add_by_id_or_ip(self, addr: str) -> None: @@ -193,7 +200,8 @@ class ServerModelManager: # NOTE: this can be called from other tabs--if current focus is not ServerTab.SAVED, update label only # TODO: saved servers might not be loaded yet, in which case should just update local file only # TODO: perform simple equality comparison of self.tv.get_enum() == ServerTab.SAVED - # FIXME: filter man is saved on a per tab basis, so this will mismatch + # FIXME: filter man is saved on a per tab basis, so this will mismatch + # check if servers.get_active_treeview() is same as self.tv self.set_cleanup_func(StoredFunc(self._cleanup_on_insert)) def _dump_history(self) -> None: @@ -203,10 +211,10 @@ class ServerModelManager: with open(history, "r") as f: rows = [row.rstrip("\n") for row in f] except OSError: - self.cleanup_func = StoredFunc(self._cleanup_on_failure, False) + self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure, False)) return if len(rows) == 0: - self.cleanup_func = StoredFunc(self._cleanup_on_failure, False) + self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure, False)) return self.thread_man.set_job_count(len(rows)) self._dump_ips(rows) @@ -218,7 +226,7 @@ class ServerModelManager: if len(ips) == 0: # FIXME: this is not a failure, just a quiet exit with custom statusbar # TODO: add custom statusbar parameters - self.cleanup_func = StoredFunc(self._cleanup_on_failure, False) + self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure, False)) return self._dump_ips(ips) @@ -243,17 +251,10 @@ class ServerModelManager: self.emitter.emit("servers_loaded", context) def _cleanup_on_success(self) -> None: - self.pending_jobs = 1 - self.tv.set_loaded(True) self.tv.set_model(self.to_insert) - # TODO: this will allow history and saved tab to emit signals to statusbar - # CHORE: test if treeview's sort method inserts row at the correct index - # inserting a row serializes file on disk, updates control model for that tab, and - # reapplies filters to ephemeral model; since filters are applied, in-situ insertion might not be necessary - # TODO: will be inserted out of order - #self.to_insert.connect("row-inserted", lambda *args: print("row inserted into model")) - + # inserting a row serializes file on disk, updates control model for that tab, and updates model + # NOTE: when inserting new rows, the entire control model is wiped and rebuilt, then proxy model is swapped in # TODO: signals or other approach to deferring map # model insertion after thread closes # cf. servers_loaded signal @@ -262,7 +263,8 @@ class ServerModelManager: context = self.tv.get_enum() self.emitter.emit("servers_loaded", context) - # CHORE: this is placeholder logic + # CHORE: this is placeholder logic, + # refills map combo with new maps if self.first_iteration: map_man = self.tv.get_map_man() map_man.set_unique_maps(self.new_maps) @@ -270,14 +272,11 @@ class ServerModelManager: self.first_iteration = False self.new_maps = None - self.tv.grab_focus() - def _cleanup_on_failure(self, show_dialog=True) -> None: - self.treeview.set_loaded(True) map_man = self.treeview.get_map_man() # TODO: disable map, keyword, and filter widgets if model is None - # -> signal driven (servers_empty) + # -> signal driven (servers_empty, servers_failed_to_load) # 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 @@ -286,8 +285,6 @@ class ServerModelManager: # wipe refresh state to False self.treeview.set_model(None) - self.treeview.grab_focus() - map_man.set_unique_maps(None) context = self.treeview.get_enum() @@ -300,7 +297,7 @@ class ServerModelManager: # TODO: break into initial dump and refilter modes, can drop filtermode kwarg # and stop pushing empty data - def _push_data_success(self, data: tuple, mode: Optional[FilterMode]) -> None: + def _push_data(self, data: tuple, mode: Optional[FilterMode]) -> None: # FIXME: calls treeview read methods in thread # treeview = self.get_active_treeview() # manager = treeview.get_filter_man() @@ -313,7 +310,7 @@ class ServerModelManager: manager.set_control(data) manager.filter(mode) self.to_insert = manager.get_proxy_model() - # TODO: pre parse maps + u_maps = set([row[1] for row in data]) self.new_maps = sorted(u_maps) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index febd83d..cba5d78 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -234,6 +234,7 @@ class Dialog: modlist: str working: str updating_mods: str + scanning: str @dataclass(slots=True, frozen=True) @@ -429,6 +430,7 @@ dialog = Dialog( modlist="Fetching modlist", working="Working", updating_mods="Updating mods", + scanning="Scanning LAN ports", ) buttons = Button( diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index 93d3b59..2e38462 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING +from dzgui.model.servers import ServerModelManager from dzgui.util.strings import connect_panel, fav_panel, lan_panel from dzgui.views.components.buttons import ( AddButton, @@ -75,7 +76,9 @@ class LanPanel(Gtk.Frame): else: port = self.entry.get_text() abort = self.early_abort.get_active() - self.controller.dump_lan(int(port), abort) + smm = ServerModelManager(self.controller, self.controller.get_active_treeview()) + smm.dump_lan(int(port), abort) + #self.controller.dump_lan(int(port), abort) def _on_radio_toggled(self, button: Gtk.RadioButton) -> None: state = button.get_active() diff --git a/dzgui/views/pages/servers.py b/dzgui/views/pages/servers.py index 36d99ca..907473f 100644 --- a/dzgui/views/pages/servers.py +++ b/dzgui/views/pages/servers.py @@ -55,6 +55,18 @@ class ServerNotebook(Gtk.ScrolledWindow): self.connect("map", self._on_map) self.connect("unmap", self._on_unmap) + self.emitter.connect("servers_loaded", self._on_servers_loaded) + + 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 + # TODO: this should be internal to servers page + 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() + def _on_map(self, widget: Self) -> None: self.emitter.emit("server_page_toggled", True) # FIXME: only applies to server notebook, not atomic page @@ -137,5 +149,17 @@ 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_tabs(self) -> tuple: + # return (self.browser, self.saved, self.recent, self.lan) + + def get_browser(self) -> ServerTreeView: + return self.browser + + def get_saved(self) -> ServerTreeView: + return self.saved + + def get_recent(self) -> ServerTreeView: + return self.recent + + def get_lan(self) -> ServerTreeView: + return self.lan diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index dcdf05a..f05d542 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -45,7 +45,6 @@ class ServerTreeView(ContextMixin, TreeView): self.enum = enum self.loaded = False - self.query_func: Callable = None self.filter_man = FilteredModelManager(controller) model = self.filter_man.get_proxy_model() @@ -59,12 +58,7 @@ class ServerTreeView(ContextMixin, TreeView): self.queue_id: int self.handler_id: int - self.query_func_jobs = 1 - - self.current_proc = None - self.queue = Queue() - # self.queue = multiprocessing.Queue() prefs = self.controller.get_prefs() columns = prefs.paths.columns @@ -129,6 +123,7 @@ class ServerTreeView(ContextMixin, TreeView): self.connect("map", self._on_map) self.connect("unmap", self._on_unmap) + # TODO: why is this being saved? self.thread = None def start_timeout(self) -> None: @@ -184,14 +179,6 @@ class ServerTreeView(ContextMixin, TreeView): if self.get_enum() is ServerTab.LAN: self.emitter.emit("lan_tab_toggled", False) - def set_query_func(self, func: "StoredFunc", jobs: int = 1) -> None: - self.query_func = func - self.query_func_jobs = jobs - - # TODO: possibly split this up - def get_query_func(self) -> tuple["StoredFunc", int]: - return self.query_func, self.query_func_jobs - def _on_col_width_changed( self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt ) -> None: