diff --git a/dzgui/api/bm.py b/dzgui/api/bm.py index 196583b..1f25838 100644 --- a/dzgui/api/bm.py +++ b/dzgui/api/bm.py @@ -1,11 +1,8 @@ import logging import requests -from pathlib import Path from typing import Optional, TYPE_CHECKING -from dzgui.config.query import lookup from dzgui.const.endpoints import BM_SERVERS -from dzgui.const.enum import Preferences logger = logging.getLogger(__name__) diff --git a/dzgui/api/mods.py b/dzgui/api/mods.py index 9010079..5e833ad 100644 --- a/dzgui/api/mods.py +++ b/dzgui/api/mods.py @@ -50,7 +50,7 @@ def get_local_mods(workshop_path: Path) -> list[Path]: return mods -# TODO: TEST: mock bad meta files with fixtures and remove them +# TODO: TEST: mock bad meta files with fixtures and skip them def parse_meta(file: Path) -> ModMeta: mod = file / "meta.cpp" if mod.exists() is False: @@ -67,7 +67,7 @@ def parse_meta(file: Path) -> ModMeta: if tok == "protocol" or tok == "publishedid": ntok = lex.get_token() elif tok == "timestamp": - # some malformed .NET tick conversions result in numbers < 0 + # NOTE: some malformed .NET tick conversions result in numbers < 0 ntok = lex.get_token() if ntok == "-": ntok += str(lex.get_token()) @@ -99,7 +99,8 @@ def get_delimited_mods(steam_path: Path) -> list[Any]: if meta is None: continue size = get_mod_size(mod) - clean.append([meta.name, symlink, mod_dir, size]) + # NOTE: final col is cell renderer highlight toggle + clean.append([meta.name, symlink, mod_dir, size, False]) clean.sort(key=lambda row: row[0]) return clean @@ -147,6 +148,7 @@ def remove_stale_signatures(config: Path, versions: Path) -> None: for line in lines: f.write(line) + def find_stale_mods(config: Path) -> list[int]: def push_record(rec: str) -> list: add = rec.split(":") @@ -163,10 +165,7 @@ def find_stale_mods(config: Path) -> list[int]: remote_mods = [] with ThreadPoolExecutor() as executor: - futures = [ - executor.submit(push_record, record) - for record in records - ] + futures = [executor.submit(push_record, record) for record in records] wait(futures) for future in futures: res = future.result() diff --git a/dzgui/api/servers.py b/dzgui/api/servers.py index 50b2dd6..82d5fd2 100644 --- a/dzgui/api/servers.py +++ b/dzgui/api/servers.py @@ -7,10 +7,9 @@ import requests import socket import subprocess import threading -import typing # noqa from dataclasses import dataclass -from typing import Union +from typing import Optional, Union from dzgui.api.bm import map_id_to_record from dzgui.const.constants import REQUEST_TIMEOUT @@ -461,7 +460,7 @@ def response_to_fq_ip(res: dict) -> str: return f"{ip}:{gameport}:{qport}" -def query_id_or_ip(addr: str, key: str) -> None: +def query_id_or_ip(addr: str, key: str) -> Optional[dict]: # NOTE: Battlemetrics if addr.isdigit(): try: diff --git a/dzgui/controllers/emitter.py b/dzgui/controllers/emitter.py index b0c2284..c84fd70 100644 --- a/dzgui/controllers/emitter.py +++ b/dzgui/controllers/emitter.py @@ -3,7 +3,7 @@ from typing import Union, TYPE_CHECKING import gi gi.require_version("Gtk", "3.0") -from gi.repository.Gtk import ListStore +from gi.repository.Gtk import ListStore # noqa E402 from gi.repository import Gdk, GObject # noqa E402 if TYPE_CHECKING: @@ -98,16 +98,6 @@ class Emitter(GObject.GObject): def check_toggled(self, label: str, state: bool) -> None: pass - @GObject.Signal( - flags=GObject.SignalFlags.RUN_LAST, - arg_types=( - str, - bool, - ), - ) - def check_toggled(self, label: str, state: bool) -> None: - pass - @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,)) def servers_loaded(self, tag: "ServerTab") -> None: pass diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 67830e9..9f7b648 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -6,7 +6,7 @@ import traceback from functools import wraps from pathlib import Path -from typing import Any, Callable, Optional, TYPE_CHECKING +from typing import Any, Callable, TYPE_CHECKING from warnings import deprecated import dzgui.api.pefile as PeFile @@ -49,7 +49,7 @@ from dzgui.util._json import read_json, write_json from dzgui.util.diag import write_diagnostic from dzgui.util.format import format_mods, format_player_count from dzgui.util.localize import number -from dzgui.util.open_links import open_workshop_page, open_user_workshop +from dzgui.util.open_links import open_user_workshop from dzgui.views.dialogs.filepicker import FilePicker from dzgui.views.dialogs.generic import ExceptionDialog, WaitDialog @@ -161,8 +161,8 @@ class Controller(GObject.GObject): def is_auto_install(self) -> bool: return self.query_config(Preferences.INSTALL) - def unblock_signals(self) -> None: - self.block_signals(False) + # def unblock_signals(self) -> None: + # self.block_signals(False) # @deprecated("Currently unused") # def block_signals(self, state: bool = True) -> None: @@ -203,7 +203,7 @@ class Controller(GObject.GObject): # self.config_man().toggle_config(Preferences.DEBUG) self.toggle_config(Preferences.DEBUG) - def get_active_context(self) -> NotebookPage: + def get_active_context(self) -> Gtk.TreeView: tv = self.get_active_treeview() return tv.get_enum() @@ -311,13 +311,9 @@ class Controller(GObject.GObject): # 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() + model = ModelFactory().make_mod_store() path = self.query_config(Preferences.DEFAULT) mods = get_delimited_mods(Path(path)) - - # NOTE: cell renderer highlight toggle - for mod in mods: - mod.append(False) model.extend(mods) self.cleanup_func = StoredFunc(self.load_mods_cleanup, model) @@ -394,7 +390,7 @@ class Controller(GObject.GObject): remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version) # TODO: strings - # TODO: delegate to LocalModManager + # TODO: delegate to LocalModManager or api/mods.py @call_on_thread("deleting mod") def delete_single_mod(self, tree_path: Gtk.TreePath) -> None: config = self.prefs.paths.config @@ -584,7 +580,7 @@ class Controller(GObject.GObject): def refresh_tree(self) -> None: treeview = self.get_active_treeview() treeview.set_loaded(False) - self.populate_model() + ServerModelManager(self, treeview).refresh() def get_player_count(self) -> str: treeview = self.get_active_treeview() @@ -619,7 +615,7 @@ class Controller(GObject.GObject): addr = ip.split(":") return fav, f"{addr[0]}:{addr[2]}" - def get_dist_cache(self) -> dict[str, "Haversine"]: + def get_dist_cache(self) -> dict[str, "Haversine", "ServerTab"]: return self.dist_cache def get_filters(self) -> list: diff --git a/dzgui/managers/contextmenu.py b/dzgui/managers/contextmenu.py index 7b074fe..33c9198 100644 --- a/dzgui/managers/contextmenu.py +++ b/dzgui/managers/contextmenu.py @@ -1,6 +1,6 @@ import gi import logging -from typing import TYPE_CHECKING +from typing import Union, TYPE_CHECKING from dzgui.const.enum import ContextMenu, Preferences from dzgui.managers.connection import ConnectionManager @@ -10,6 +10,9 @@ from dzgui.util import strings from dzgui.util.clip import copy_clipboard from dzgui.util.open_links import open_workshop_page +from dzgui.views.trees.tree_servers import ServerTreeView + + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402 @@ -21,7 +24,11 @@ if TYPE_CHECKING: class ContextMenuManager: - def __init__(self, treeview: Gtk.TreeView, controller: "Controller") -> None: + def __init__( + self, + treeview: Gtk.TreeView, + controller: "Controller", + ) -> None: self.controller = controller self.treeview = treeview self.thread_man = ThreadingManager(parent=controller) @@ -83,6 +90,8 @@ class ContextMenuManager: pass def copy_server_ip(self) -> None: + if not isinstance(self.treeview, ServerTreeView): + return record = self.treeview.get_simplified_ip() copy_clipboard(record) diff --git a/dzgui/managers/thread_man.py b/dzgui/managers/thread_man.py index 117f913..e51e3c9 100644 --- a/dzgui/managers/thread_man.py +++ b/dzgui/managers/thread_man.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) def call_on_thread(dialog_str: str) -> Callable: def decorator(func: Callable) -> Callable: @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Callable: self = args[0] stored = StoredFunc(func, *args, **kwargs) if not hasattr(self, "thread_man"): diff --git a/dzgui/model/filtered_model.py b/dzgui/model/filtered_model.py index 6cefc27..ba3ccd2 100644 --- a/dzgui/model/filtered_model.py +++ b/dzgui/model/filtered_model.py @@ -1,4 +1,3 @@ -import datetime import re from typing import Optional, TYPE_CHECKING @@ -281,5 +280,5 @@ class FilteredModelManager: self.filtered = None self.filter_cache = {} self.ping_cache = {} - if full: - self.control_model = None + # if full: + # self.control_model = None diff --git a/dzgui/model/model_factory.py b/dzgui/model/model_factory.py index ed0f250..0c62ba8 100644 --- a/dzgui/model/model_factory.py +++ b/dzgui/model/model_factory.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any +from typing import Any, Sequence, Union from dzgui.const.enum import HELP_MENU_ROWS from dzgui.util.redact import redact_log @@ -8,7 +8,7 @@ from dzgui.util.strings import delimiter import gi gi.require_version("Gtk", "3.0") -from gi.repository.Gtk import ListStore # noqa E402 +from gi.repository.Gtk import ListStore, TreeIter # noqa E402 from gi.repository import GObject # noqa E402 GTYPE_TO_PYTHON = { @@ -53,27 +53,27 @@ class LogCols: @dataclass(slots=True, frozen=True) class ServerModCols: name: str - uid: GObject.TYPE_INT64 + uid: int # GObject.TYPE_INT64 installed: str @dataclass(slots=True, frozen=True) class MenuCols: - name: GObject.TYPE_STRING # str - hidden: GObject.TYPE_PYOBJECT + name: str + hidden: object # GObject.TYPE_PYOBJECT class FastInsertListStore(ListStore): - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - def _is_same_length(self, lists: list[list[Any]]) -> bool: + def _is_same_length(self, lists: Sequence[Any]) -> bool: first_len = len(lists[0]) if not all(len(sublist) == first_len for sublist in lists): return False return True - def _is_type_homogeneous(self, lists: list[list[Any]]) -> bool: + def _is_type_homogeneous(self, lists: Sequence[Any]) -> bool: transposed = zip(*lists) for column in transposed: @@ -82,7 +82,7 @@ class FastInsertListStore(ListStore): return False return True - def extend(self, rows: list[list[Any]]) -> None: + def extend(self, rows: Sequence[Any]) -> None: """ Compared to calling append() directly, introduces negligible overhead, but guarantees type and length equivalence prior to insertion @@ -103,7 +103,7 @@ class FastInsertListStore(ListStore): for row in rows: self.append(row) - def append(self, row) -> None: + def append(self, row: list[Any] | tuple[Any, ...] | None = ...) -> TreeIter: """ Optimized for speed, but makes no assurances about row homogeneity and may segfault if types and length are not identical to ListStore. @@ -120,7 +120,7 @@ class ModelFactory: def __init__(self) -> None: pass - def new_model_from_logfile(self, path: str) -> None: + def new_model_from_logfile(self, path: str) -> FastInsertListStore: store = self.make_log_store() with open(path, "r") as f: lines = [line.split(delimiter) for line in f.read().splitlines()] @@ -137,17 +137,12 @@ class ModelFactory: return store def make_map_store(self) -> FastInsertListStore: - return ListStore(str) + return FastInsertListStore(str) def make_help_store(self) -> FastInsertListStore: store = self.new_model_from_class(MenuCols) rows = [[row.dict["label"], row] for row in HELP_MENU_ROWS] - # for row in rows:HELP_MENU_ROWS: - # print(type(row) is object) store.extend(rows) - # for row in HELP_MENU_ROWS: - # label = row.dict["label"] - # store.append([label, row]) return store def make_mod_store(self) -> FastInsertListStore: diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index 71a4f73..c8ac7a2 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -42,7 +42,7 @@ class ServerModelManager: self.emitter = controller.get_emitter() self.first_iteration: bool - self.preserve_on_fail: False + self.preserve_on_fail = False self.jobs = 1 # NOTE: store filter man for access inside thread @@ -80,6 +80,8 @@ class ServerModelManager: def refresh(self) -> None: self.preserve_on_fail = True self.load() + # TODO: if refresh is active, callback to button decrement signal + # and do not mark refresh as sensitive @call_on_thread(dialog.fetching) def _dump_api(self) -> None: @@ -109,14 +111,14 @@ class ServerModelManager: ) return - # NOTE: This 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 servers += j["response"]["servers"] parsed = Servers.parse_json(servers) - self._push_data(parsed, FilterMode.INITIAL) + self._push_data(parsed) @call_on_thread(dialog.scanning) def dump_lan(self, port: int, early_abort: bool) -> None: @@ -149,14 +151,16 @@ class ServerModelManager: self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure)) return parsed = Servers.parse_json(servers) - self._push_data(parsed, FilterMode.INITIAL) + self._push_data(parsed) @call_on_thread(dialog.fetching) def _dump_ips(self, ips: list[str]) -> None: # NOTE: block malformed records (TODO: add github issue no.) # TODO: sanitize ip list at config time and drop this + # TODO: make test for this ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""] job = Servers.query_direct + servers = [] with ThreadPoolExecutor() as executor: futures = [ executor.submit( @@ -166,9 +170,8 @@ class ServerModelManager: ) for ip in ips ] - servers = [] for future in as_completed(futures): - res = future.result() + res = future.result(timeout=API_TIMEOUT) self.thread_man.increment_dialog() # NOTE: failing entries are culled if res is None: @@ -181,7 +184,7 @@ class ServerModelManager: return parsed = Servers.parse_json(servers) - self._push_data(parsed, FilterMode.INITIAL) + self._push_data(parsed) @call_on_thread(dialog.querying) def add_by_id_or_ip(self, addr: str) -> None: @@ -199,8 +202,8 @@ class ServerModelManager: config_man = self.controller.get_config_man() config_man.add_saved_server(fqip) - # NOTE: tab was not instantiated yet - if model is None: + # NOTE: if tab was not instantiated yet + if raw_model is None: return raw_model.append(record[0]) @@ -250,8 +253,9 @@ class ServerModelManager: self.tv.set_model(proxy) # TODO: if current tab != self.saved, add label - # TODO: adding a row may update available maps self.emitter.emit("servers_loaded", self.enum) + # NOTE: adding a row may update available maps + # TODO: test this self._update_maps() def _update_maps(self) -> None: @@ -263,8 +267,8 @@ class ServerModelManager: def _cleanup_on_success(self) -> None: self.tv.set_model(self.to_insert) - # inserting a row serializes file on disk, updates control model for that tab, and updates model # TODO: make sure control model len is N + 1 + # 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 @@ -296,21 +300,21 @@ class ServerModelManager: dialog = ExceptionDialog(self.controller, api_warn_msg) dialog.run() - # TODO: break into initial dump and refilter modes, can drop filtermode kwarg - def _push_data(self, data: tuple, mode: Optional[FilterMode]) -> None: + def _push_data(self, data: list) -> None: + # if data is None: + # self.to_insert = None + # else: + # FIXME: filterman calls GTK methods in thread + # if mode == FilterMode.INITIAL: + manager = self._get_filter_man() + manager.wipe_cache() + manager.set_control(data) + manager.filter(FilterMode.INITIAL) + self.to_insert = manager.get_proxy_model() - if data is None: - self.to_insert = None - else: - # FIXME: filterman calls GTK methods in thread - if mode == FilterMode.INITIAL: - manager.set_control(data) - manager.filter(mode) - self.to_insert = manager.get_proxy_model() - - u_maps = set([row[1] for row in data]) - self._set_new_maps(sorted(u_maps)) + u_maps = set([row[1] for row in data]) + self._set_new_maps(sorted(u_maps)) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success)) diff --git a/dzgui/util/dist.py b/dzgui/util/dist.py index d886e54..8a60cc9 100644 --- a/dzgui/util/dist.py +++ b/dzgui/util/dist.py @@ -13,6 +13,7 @@ from gi.repository import Gtk # noqa E402 if TYPE_CHECKING: from dzgui.const.enum import ServerTab from dzgui.controllers.mc import Controller + from queue import Queue logger = logging.getLogger(__name__) @@ -53,9 +54,9 @@ class CalcDist: self.controller = controller self.result_queue = result_queue self.addr = addr - self.ip = self.addr #.split(":")[0] + self.ip = self.addr # .split(":")[0] - #def run(self) -> None: + # def run(self) -> None: cache = self.controller.get_dist_cache() if self.addr in cache: logger.info(f"Address '{self.addr}' already in cache") diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 0abea18..c5194f7 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -273,7 +273,6 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore if widget is None: return - # FIXME: deprecated widget.grab_content_area() def get_page(self) -> Gtk.Widget | None: diff --git a/dzgui/views/components/buttons.py b/dzgui/views/components/buttons.py index 04c88c8..32ee330 100644 --- a/dzgui/views/components/buttons.py +++ b/dzgui/views/components/buttons.py @@ -103,6 +103,7 @@ class RefreshButton(IconTextButton): # FIXME: do this after threaded call fully finishes # FIXME: other signals cause refresh button to become sensitive # after model is loaded + # TODO: put in smm cleanup func, use signal GLib.timeout_add_seconds(1, self.decrement) self.controller.refresh_tree() diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index 3a1cdda..386de3c 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -45,10 +45,6 @@ class RightPanel(Gtk.Box): 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) - # TODO: reference for ping pass # def _on_ping_clicked(self, button: Gtk.Button) -> None: # block_signals() diff --git a/dzgui/views/components/statusbar.py b/dzgui/views/components/statusbar.py index 18716c1..fba3bcc 100644 --- a/dzgui/views/components/statusbar.py +++ b/dzgui/views/components/statusbar.py @@ -150,10 +150,11 @@ class Statusbar(Gtk.Grid): def set_cache(self, string: str) -> None: self.cache = string - def set_text(self, string: str, context: str) -> None: + def set_text(self, string: str, context: str) -> int: meta = self.statusbar.get_context_id(context) self.statusbar.push(meta, string) self.set_cache(string) + return meta def _help_row_changed(self, tree: "TreeView", sel: Gtk.TreeSelection) -> None: row = tree.get_value_at_index(1) diff --git a/dzgui/views/mixins/context_mixin.py b/dzgui/views/mixins/context_mixin.py index e673049..bcb9c72 100644 --- a/dzgui/views/mixins/context_mixin.py +++ b/dzgui/views/mixins/context_mixin.py @@ -55,8 +55,8 @@ class ContextMixin(TreeView): self.context_menu.select_first(False) - def _process_dynamic_row(self, row: ContextMenu) -> None: - if row == ContextMenu.ADD_SERVER and self.is_in_favs(): + def _process_dynamic_row(self, row: ContextMenu) -> Gtk.MenuItem: + if row == ContextMenu.ADD_SERVER and self.is_in_favs(): # type: ignore row = ContextMenu.REMOVE_SERVER item = Gtk.MenuItem(label=row.dict["label"]) @@ -71,7 +71,7 @@ class ContextMixin(TreeView): # if self.get_record_string() in notes_cache: # item.set_label(strings.edit_note) - def _process_button_event(self, event: Gdk.EventButton) -> None: + def _process_button_event(self, event: Gdk.EventButton) -> bool: try: pathinfo = self.get_path_at_pos(int(event.x), int(event.y)) if pathinfo is None: @@ -89,7 +89,7 @@ class ContextMixin(TreeView): return True # FIXME: if selection is not multiple, change cursor except AttributeError: - pass + return False def _on_menu_click(self, widget: Gtk.MenuItem, enum: ContextMenu) -> None: self.controller.menu_action(enum, self)