From 99ede36a325822cfcf5ed7f10fbc3a91145291b6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:48:23 +0900 Subject: [PATCH] chore: ServerModelManager WIP --- dzgui/controllers/mc.py | 470 +++++++++++++++++------------------ dzgui/model/servers.py | 17 ++ dzgui/views/pages/servers.py | 6 - 3 files changed, 240 insertions(+), 253 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 101117d..8215c51 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -304,6 +304,7 @@ class Controller(GObject.GObject): self.mediator.modtreeview.set_model(model) self.emitter.emit("mod_page_loaded") + # TODO: delegate to threadmanager @call_on_thread(strings.dialog.modlist) def load_mods(self) -> None: model = ModelFactory().make_mod_store() #self.model_man.new_mod_store() @@ -375,165 +376,165 @@ class Controller(GObject.GObject): self.open_page(button.opens) - def dump_favorites(self) -> None: - # TODO: if list is empty, return failure - ips = self.query_config(Preferences.IP_LIST) - if len(ips) == 0: - # FIXME: this is not a failure, just a quiet exit with custom statusbar - # TODO: add custom statusbar parameter - self.cleanup_func = StoredFunc(self.cleanup_on_failure, False) - return - self.dump_ips(ips) + # def dump_favorites(self) -> None: + # # TODO: if list is empty, return failure + # ips = self.query_config(Preferences.IP_LIST) + # if len(ips) == 0: + # # FIXME: this is not a failure, just a quiet exit with custom statusbar + # # TODO: add custom statusbar parameter + # self.cleanup_func = StoredFunc(self.cleanup_on_failure, False) + # return + # self.dump_ips(ips) - def dump_history(self) -> None: - history = self.prefs.paths.history - # TODO: customize statusbar to mention how records are added - try: - 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) - return - if len(rows) == 0: - self.cleanup_func = StoredFunc(self.cleanup_on_failure, False) - return - self.dump_ips(rows) + # def dump_history(self) -> None: + # history = self.prefs.paths.history + # # TODO: customize statusbar to mention how records are added + # try: + # 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) + # return + # if len(rows) == 0: + # self.cleanup_func = StoredFunc(self.cleanup_on_failure, False) + # return + # self.dump_ips(rows) - def dump_ips(self, ips: list) -> None: - # NOTE: block malformed records (TODO: add github issue no.) - ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""] - with ThreadPoolExecutor() as executor: - futures = [ - executor.submit( - Servers.query_direct, - ip.split(":")[0], - int(ip.split(":")[2]), - ) - for ip in ips - ] - # TODO: update dialog in main loop - # wait(futures) - serv = [] - for future in as_completed(futures): - res = future.result() - # NOTE: discard failing entries - if res is None: - continue - serv.append(res) - if len(serv) == 0: - self.cleanup_func = StoredFunc(self.cleanup_on_failure) - return - parsed = Servers.parse_json(serv) - self.push_data_success(parsed, FilterMode.INITIAL) + # def dump_ips(self, ips: list) -> None: + # # NOTE: block malformed records (TODO: add github issue no.) + # ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""] + # with ThreadPoolExecutor() as executor: + # futures = [ + # executor.submit( + # Servers.query_direct, + # ip.split(":")[0], + # int(ip.split(":")[2]), + # ) + # for ip in ips + # ] + # # TODO: update dialog in main loop + # # wait(futures) + # serv = [] + # for future in as_completed(futures): + # res = future.result() + # # NOTE: discard failing entries + # if res is None: + # continue + # serv.append(res) + # if len(serv) == 0: + # self.cleanup_func = StoredFunc(self.cleanup_on_failure) + # return + # parsed = Servers.parse_json(serv) + # self.push_data_success(parsed, FilterMode.INITIAL) # TODO: strings - @call_on_thread("scanning LAN ports") - def dump_lan(self, port: int, early_abort: bool) -> None: - serv = [] - event = threading.Event() - with ThreadPoolExecutor() as executor: - futures = [ - executor.submit(Servers.test_ip, i, port, event) for i in range(1, 256) - ] - for future in as_completed(futures): - try: - res = future.result(timeout=0.5) - if res is not None and early_abort is True: - event.set() - self.cleanup_func = StoredFunc(self.cleanup_on_failure) - return - if res is None: - continue - serv.append(res) - except Exception as e: - logger.critical(e) - self.cleanup_func = StoredFunc(self.cleanup_on_failure) - if len(serv) == 0: - self.cleanup_func = StoredFunc(self.cleanup_on_failure) - return - parsed = Servers.parse_json(serv) - self.push_data_success(parsed, FilterMode.INITIAL) + # @call_on_thread("scanning LAN ports") + # def dump_lan(self, port: int, early_abort: bool) -> None: + # serv = [] + # event = threading.Event() + # with ThreadPoolExecutor() as executor: + # futures = [ + # executor.submit(Servers.test_ip, i, port, event) for i in range(1, 256) + # ] + # for future in as_completed(futures): + # try: + # res = future.result(timeout=0.5) + # if res is not None and early_abort is True: + # event.set() + # self.cleanup_func = StoredFunc(self.cleanup_on_failure) + # return + # if res is None: + # continue + # serv.append(res) + # except Exception as e: + # logger.critical(e) + # self.cleanup_func = StoredFunc(self.cleanup_on_failure) + # if len(serv) == 0: + # self.cleanup_func = StoredFunc(self.cleanup_on_failure) + # return + # parsed = Servers.parse_json(serv) + # self.push_data_success(parsed, FilterMode.INITIAL) - def query_ip_id(self, addr: str) -> None: - # NOTE: Battlemetrics - # TODO: stores full ip, gameport, qport in file as canonical record - if addr.isdigit(): - config = self.prefs.paths.config - resolved = map_id_to_record(config, addr) - res = Servers.query_direct(resolved.ip, resolved.qport) - else: - record = addr.split(":") - ip, qport = record[0], record[1] - res = Servers.query_direct(ip, int(qport)) - return res + # def query_ip_id(self, addr: str) -> None: + # # NOTE: Battlemetrics + # # TODO: stores full ip, gameport, qport in file as canonical record + # if addr.isdigit(): + # config = self.prefs.paths.config + # resolved = map_id_to_record(config, addr) + # res = Servers.query_direct(resolved.ip, resolved.qport) + # else: + # record = addr.split(":") + # ip, qport = record[0], record[1] + # res = Servers.query_direct(ip, int(qport)) + # return res - # TODO: strings - @call_on_thread("querying address") - def connect_by_id_or_ip(self, addr: str) -> None: - res = self.query_ip_id(addr) + # # TODO: strings + # @call_on_thread("querying address") + # def connect_by_id_or_ip(self, addr: str) -> None: + # res = self.query_ip_id(addr) - def cleanup_on_insert(self) -> None: - # FIXME: grab filterman outside of thread - treeview = self.mediator.servers.saved - filter_man = treeview.get_filter_man() - model = filter_man.get_control() - print(len(model)) - # FIXME: refiltration should occur in thread - model.append(self.insert_record[0]) - filtered = filter_man.filter(FilterMode.INITIAL) - treeview.set_model(filter_man.get_proxy_model()) - # TODO: update statusbar - context = self.get_active_context() - # TODO: adding a row may update available maps - # TODO: if all filters are already applied, strange behavior may occur - # need to insert and update all according logic per current filters - self.emitter.emit("servers_loaded", context) - # model = treeview.get_model().append(self.insert_record[0]) + # def cleanup_on_insert(self) -> None: + # # FIXME: grab filterman outside of thread + # treeview = self.mediator.servers.saved + # filter_man = treeview.get_filter_man() + # model = filter_man.get_control() + # print(len(model)) + # # FIXME: refiltration should occur in thread + # model.append(self.insert_record[0]) + # filtered = filter_man.filter(FilterMode.INITIAL) + # treeview.set_model(filter_man.get_proxy_model()) + # # TODO: update statusbar + # context = self.get_active_context() + # # TODO: adding a row may update available maps + # # TODO: if all filters are already applied, strange behavior may occur + # # need to insert and update all according logic per current filters + # self.emitter.emit("servers_loaded", context) + # # model = treeview.get_model().append(self.insert_record[0]) - @call_on_thread("querying address") - def add_by_id_or_ip(self, addr: str) -> None: - res = self.query_ip_id(addr) - self.insert_record = Servers.parse_json([res]) - # if res is None: - # # TODO: set cleanupfunc with dialog - # return - # TODO: add into saved servers file - # TODO: update saved servers model - # if current focus is not save servers, update label - # TODO: saved servers might not be loaded yet, in which case - # should just add to server file - self.set_cleanup_func(StoredFunc(self.cleanup_on_insert)) + # @call_on_thread("querying address") + # def add_by_id_or_ip(self, addr: str) -> None: + # res = self.query_ip_id(addr) + # self.insert_record = Servers.parse_json([res]) + # # if res is None: + # # # TODO: set cleanupfunc with dialog + # # return + # # TODO: add into saved servers file + # # TODO: update saved servers model + # # if current focus is not save servers, update label + # # TODO: saved servers might not be loaded yet, in which case + # # should just add to server file + # self.set_cleanup_func(StoredFunc(self.cleanup_on_insert)) - def dump_api(self) -> None: - key = self.query_config(Preferences.STEAM) - job = Servers.query_api - params = Servers.params - serv = [] - with ThreadPoolExecutor() as executor: - futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params] - for future in as_completed(futures): - try: - GLib.idle_add(lambda: self.wait_dialog.increment()) - res = future.result(timeout=3) - if res.status != 200 or not res.parsed: - self.cleanup_func = StoredFunc(self.cleanup_on_failure) - return - j = res.json - serv += j["response"]["servers"] - except Exception as e: - logger.critical(e) - self.cleanup_func = StoredFunc(self.cleanup_on_failure) - return + # def dump_api(self) -> None: + # key = self.query_config(Preferences.STEAM) + # job = Servers.query_api + # params = Servers.params + # serv = [] + # with ThreadPoolExecutor() as executor: + # futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params] + # for future in as_completed(futures): + # try: + # GLib.idle_add(lambda: self.wait_dialog.increment()) + # res = future.result(timeout=3) + # if res.status != 200 or not res.parsed: + # self.cleanup_func = StoredFunc(self.cleanup_on_failure) + # return + # j = res.json + # serv += j["response"]["servers"] + # except Exception as e: + # logger.critical(e) + # self.cleanup_func = StoredFunc(self.cleanup_on_failure) + # return - # 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"] + # # 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.increment("Unpacking servers")) - parsed = Servers.parse_json(serv) - self.push_data_success(parsed, FilterMode.INITIAL) + # GLib.idle_add(lambda: self.wait_dialog.increment("Unpacking servers")) + # parsed = Servers.parse_json(serv) + # self.push_data_success(parsed, FilterMode.INITIAL) def get_help_row(self) -> str: return self.mediator.menu.get_row_enum() @@ -548,6 +549,7 @@ class Controller(GObject.GObject): remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version) # TODO: strings + # TODO: delegate to LocalModManager @call_on_thread("deleting mod") def delete_single_mod(self, tree_path: Gtk.TreePath) -> None: config = self.prefs.paths.config @@ -671,84 +673,84 @@ class Controller(GObject.GObject): def get_map_man(self) -> "MapManager": 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) + # 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) - # 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")) + # # 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")) - # TODO: signals or other approach to deferring map - # model insertion after thread closes - # cf. servers_loaded signal + # # TODO: signals or other approach to deferring map + # # 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) + # # TODO: servers_loaded vs servers_reloaded + # context = self.get_active_context() + # self.emitter.emit("servers_loaded", context) - # CHORE: this is placeholder logic - if self.first_iteration: - map_man = treeview.get_map_man() - map_man.set_unique_maps(self.new_maps) - self.emitter.emit("servers_loaded_init") - self.first_iteration = False - self.new_maps = None + # # CHORE: this is placeholder logic + # if self.first_iteration: + # map_man = treeview.get_map_man() + # map_man.set_unique_maps(self.new_maps) + # self.emitter.emit("servers_loaded_init") + # self.first_iteration = False + # self.new_maps = None - treeview.grab_focus() + # treeview.grab_focus() - def cleanup_on_failure(self, show_dialog=True) -> None: - treeview = self.get_active_treeview() - treeview.set_loaded(True) - map_man = treeview.get_map_man() + # def cleanup_on_failure(self, show_dialog=True) -> None: + # treeview = self.get_active_treeview() + # treeview.set_loaded(True) + # map_man = treeview.get_map_man() - # TODO: disable map, keyword, and filter widgets if model is None - # -> signal driven (servers_empty) - # 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 + # # TODO: disable map, keyword, and filter widgets if model is None + # # -> signal driven (servers_empty) + # # 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 - treeview.set_model(None) - treeview.grab_focus() + # treeview.set_model(None) + # treeview.grab_focus() - map_man.set_unique_maps(None) - context = self.get_active_context() + # map_man.set_unique_maps(None) + # context = self.get_active_context() - # TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty" - # customize statusbar accordingly - self.emitter.emit("servers_loaded", context) - if show_dialog: - dialog = ExceptionDialog(self, strings.api_warn_msg) - dialog.run() + # # TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty" + # # customize statusbar accordingly + # self.emitter.emit("servers_loaded", context) + # if show_dialog: + # dialog = ExceptionDialog(self, strings.api_warn_msg) + # dialog.run() - # 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: - # FIXME: calls treeview read methods in thread - # treeview = self.get_active_treeview() - # manager = treeview.get_filter_man() - manager = self.get_filter_man() + # # 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: + # # FIXME: calls treeview read methods in thread + # # treeview = self.get_active_treeview() + # # manager = treeview.get_filter_man() + # manager = self.get_filter_man() - if data is None: - self.to_insert = None - else: - if mode == FilterMode.INITIAL: - manager.set_control(data) - manager.filter(mode) - self.to_insert = manager.get_model() - # TODO: pre parse maps - u_maps = set([row[1] for row in data]) - self.new_maps = sorted(u_maps) + # if data is None: + # self.to_insert = None + # else: + # if mode == FilterMode.INITIAL: + # manager.set_control(data) + # manager.filter(mode) + # self.to_insert = manager.get_model() + # # TODO: pre parse maps + # u_maps = set([row[1] for row in data]) + # self.new_maps = sorted(u_maps) - self.cleanup_func = StoredFunc(self.cleanup_on_success) + # self.cleanup_func = StoredFunc(self.cleanup_on_success) def highlight_stale_cleanup(self, stale_mods: list) -> None: """Manipulates attached ListStore in the main event loop""" @@ -848,18 +850,14 @@ class Controller(GObject.GObject): def get_statusbar(self) -> None: return self.mediator.statusbar - @call_on_thread(strings.dialog.fetching) - def run_query_func(self, func: StoredFunc) -> None: - func.call() - - @call_on_thread(strings.dialog.filtering) - def filter_threaded( - self, filter_man: "FilteredModelManager", mode: FilterMode, label: str - ) -> None: - filter_man.filter(mode, label) - self.to_insert = filter_man.get_proxy_model() - print("filtering threaded") - self.cleanup_func = StoredFunc(self.cleanup_on_success) + # @call_on_thread(strings.dialog.filtering) + # def filter_threaded( + # self, filter_man: "FilteredModelManager", mode: FilterMode, label: str + # ) -> None: + # filter_man.filter(mode, label) + # self.to_insert = filter_man.get_proxy_model() + # print("filtering threaded") + # self.cleanup_func = StoredFunc(self.cleanup_on_success) # TODO: call filter_man methods directly def refilter_model(self, mode: FilterMode, label: Optional[str] = None) -> None: @@ -877,30 +875,8 @@ class Controller(GObject.GObject): self.filter_man = filter_man def populate_model(self, tv: Gtk.TreeView) -> None: - # NOTE: prepare GTK objects outside of thread - #if tv.is_loaded() is True: - # self.emitter.emit("servers_loaded", tv.get_enum()) - # return - - #func, jobs = tv.get_query_func() ServerModelManager(self, tv, first_iteration=True) - #if func is None: - # self.emitter.emit("servers_loaded", tv.get_enum()) - # tv.set_model(None) - # return - - # TODO: clear ephemeral model if necessary - # manager = treeview.get_filter_man() - # manager.clear_model() - - # self.first_iteration = True - # self.pending_jobs = jobs - # # TODO: get filter manager a priori and set it so that it can be accessed out of thread - # filter_man = treeview.get_filter_man() - # self.set_filter_man(filter_man) - # self.run_query_func(func) - def get_favorite(self) -> tuple[str, str] | tuple[None, None]: fav = str(self.query_config(Preferences.FAV_LBL)) if len(fav) < 1: diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index 75133df..d0abc37 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -322,3 +322,20 @@ class ServerModelManager: def _get_filter_man(self) -> "FilteredModelManager": return self.filter_man + # TODO: unimplemented + # @call_on_thread(strings.dialog.filtering) + # def filter_threaded( + # self, filter_man: "FilteredModelManager", mode: FilterMode, label: str + # ) -> None: + # filter_man.filter(mode, label) + # self.to_insert = filter_man.get_proxy_model() + # print("filtering threaded") + # self.cleanup_func = StoredFunc(self.cleanup_on_success) + + # TODO: call filter_man methods directly + #def refilter_model(self, mode: FilterMode, label: Optional[str] = None) -> None: + # tv = self.get_active_treeview() + # filter_man = tv.get_filter_man() + # if filter_man.get_control() is None: + # return + # self.filter_threaded(filter_man, mode, label) diff --git a/dzgui/views/pages/servers.py b/dzgui/views/pages/servers.py index f56f955..36d99ca 100644 --- a/dzgui/views/pages/servers.py +++ b/dzgui/views/pages/servers.py @@ -37,12 +37,6 @@ class ServerNotebook(Gtk.ScrolledWindow): ) self.lan = ServerTreeView(controller, ServerTab.LAN, ContextMenuGroup.SCAN_LAN) - # TODO: number of jobs will not be known a priori unless files are read - self.recent.set_query_func(StoredFunc(self.controller.dump_history)) - self.saved.set_query_func(StoredFunc(self.controller.dump_favorites)) - self.lan.set_query_func(StoredFunc(self.controller.dump_test_2)) - self.browser.set_query_func(StoredFunc(self.controller.dump_api), jobs=11) - tabs = [ (self.browser, server_labels.browser), (self.saved, server_labels.saved),