diff --git a/dzgui/api/mods.py b/dzgui/api/mods.py index bcbd1c6..4953ceb 100644 --- a/dzgui/api/mods.py +++ b/dzgui/api/mods.py @@ -101,8 +101,8 @@ def get_delimited_mods(steam_path: Path) -> list[Any]: continue size = get_mod_size(mod) # NOTE: final col is cell renderer highlight toggle - clean.append([meta.name, symlink, mod_dir, size, False]) - clean.sort(key=lambda row: row[0].casefold()) + clean.append([meta.name.casefold(), symlink, mod_dir, size, False]) + clean.sort(key=lambda row: row[0]) return clean diff --git a/dzgui/api/servers.py b/dzgui/api/servers.py index ce07b89..29b23b4 100644 --- a/dzgui/api/servers.py +++ b/dzgui/api/servers.py @@ -9,7 +9,7 @@ import subprocess import threading from dataclasses import dataclass -from typing import Optional, TYPE_CHECKING, Union +from typing import Any, Optional, TYPE_CHECKING, Union from dzgui.api.bm import map_id_to_record from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT @@ -228,7 +228,7 @@ def query_direct(ip: str, qport: int, timeout: float = 3.0) -> dict | None: class Res: status: int parsed: bool - json: Union[str, None] + json: Union[dict[str, Any], None] @dataclass(slots=True, frozen=True) @@ -393,13 +393,13 @@ def query_api(key: str, appid: int, param: str) -> Res: res.raise_for_status() parsed = True status = 200 - data = res.json() + json = res.json() except Exception: parsed = False - data = None + json = None status = 403 finally: - return Res(status, parsed, data) + return Res(status, parsed, json) def validate_ip(addr: str) -> Record: @@ -453,7 +453,7 @@ def query_by_id(addr: str, key: str) -> Optional[dict]: return None -def query_playercount(record: Record) -> Optional[tuple[int]]: +def query_playercount(record: Record) -> tuple[int, int] | None: try: res = query_direct(record.ip, record.qport) players = int(res["players"]) @@ -462,7 +462,7 @@ def query_playercount(record: Record) -> Optional[tuple[int]]: queue = int(r[1].split(",")[0]) except IndexError: queue = 0 - return (players, queue) + return players, queue except Exception as e: logger.critical(e) return None diff --git a/dzgui/managers/config.py b/dzgui/managers/config.py index 561fa4d..93cd012 100644 --- a/dzgui/managers/config.py +++ b/dzgui/managers/config.py @@ -72,7 +72,7 @@ class ConfigManager: # TODO: do this on demand if/when in options page # self.mediator.grid.notebook.settings.populate_settings() - def get_config(self) -> dict: + def get_config(self) -> Any: # TODO: is this being called twice? try: return read_json(self.config) diff --git a/dzgui/managers/contextmenu.py b/dzgui/managers/contextmenu.py index 412a03c..51a158a 100644 --- a/dzgui/managers/contextmenu.py +++ b/dzgui/managers/contextmenu.py @@ -33,61 +33,55 @@ class ContextMenuManager: self.treeview = treeview def process(self, action: ContextMenu) -> None: - match action: - # UNTHREADED - case ContextMenu.ADD_NOTE: - dialog = NoteDialog(self.controller) - dialog.run() - case ContextMenu.COPY_LOG_CLIPBOARD: - self.copy_log() - case ContextMenu.COPY_SERVER_IP: - self.copy_server_ip() - case ContextMenu.COPY_SERVER_NAME: - self.copy_server_name() - case ContextMenu.DELETE_MOD: - self.controller.delete_mods() - case ContextMenu.OPEN_WORKSHOP: - self.open_mod_page() - case ContextMenu.SET_FAV: - name = self.treeview.get_value_at_index(0) - record_str = self.treeview.get_record_string() - simple = self.treeview.get_simplified_ip() - self.controller.set_fav(name, record_str, simple) + if isinstance(self.treeview, ServerTreeView): + record = self.treeview.get_record() + if record is None: + return - # THREADED - case ContextMenu.ADD_SERVER: - record = self.treeview.get_record() - if record is None: - return - self.controller.add_by_record(record) - case ContextMenu.CONNECT: - record = self.treeview.get_record() - if record is None: - return - self.controller.connect_by_record(record) - case ContextMenu.REFRESH_PLAYERS: - record = self.treeview.get_record() - if record is None: - return - self.controller.refresh_players(record) - case ContextMenu.REMOVE_HISTORY: - record = self.treeview.get_record() - if record is None: - return - self.controller.remove_from_history(record) - case ContextMenu.REMOVE_SERVER: - record = self.treeview.get_record() - if record is None: - return - self.controller.remove_by_record(record) - case ContextMenu.SHOW_DETAILS: - record = self.treeview.get_record() - if record is None: - return - self.controller.get_details(record) - case ContextMenu.SHOW_MODS: - record = self.treeview.get_record() - self.controller.get_modlist(record) + match action: + # UNTHREADED + case ContextMenu.ADD_NOTE: + dialog = NoteDialog(self.controller) + dialog.run() + case ContextMenu.COPY_SERVER_IP: + self.copy_server_ip() + case ContextMenu.COPY_SERVER_NAME: + self.copy_server_name() + case ContextMenu.SET_FAV: + name = self.treeview.get_value_at_index(0) + # NOTE: fully qualified ip + fqip = self.treeview.get_record_string() + # NOTE: short ip for display purposes + simple = self.treeview.get_simplified_ip() + self.controller.set_fav(name, fqip, simple) + + # THREADED + case ContextMenu.ADD_SERVER: + self.controller.add_by_record(record) + case ContextMenu.CONNECT: + self.controller.connect_by_record(record) + case ContextMenu.REFRESH_PLAYERS: + self.controller.refresh_players(record) + case ContextMenu.REMOVE_HISTORY: + self.controller.remove_from_history(record) + case ContextMenu.REMOVE_SERVER: + self.controller.remove_by_record(record) + case ContextMenu.SHOW_DETAILS: + self.controller.get_details(record) + case ContextMenu.SHOW_MODS: + self.controller.get_modlist(record) + + if isinstance(self.treeview, ModTreeView): + match action: + case ContextMenu.DELETE_MOD: + self.controller.delete_mods() + case ContextMenu.OPEN_WORKSHOP: + self.open_mod_page() + + if isinstance(self.treeview, LogTreeView): + match action: + case ContextMenu.COPY_LOG_CLIPBOARD: + self.copy_log() def copy_server_ip(self) -> None: if not isinstance(self.treeview, ServerTreeView): diff --git a/dzgui/managers/filter.py b/dzgui/managers/filter.py index b6f7a6d..52ef6a0 100644 --- a/dzgui/managers/filter.py +++ b/dzgui/managers/filter.py @@ -76,7 +76,7 @@ class FilterManager: def append_map(self, row: list[str]) -> None: self.map_store.append(row) - def set_unique_maps(self, maps: list) -> None: + def set_unique_maps(self, maps: list[str]) -> None: if maps is None: return if len(maps) < 1: diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 9e09abf..f0ed79f 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -2,7 +2,7 @@ import logging import shutil from pathlib import Path -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING from dzgui.api.mods import ( get_delimited_mods, @@ -60,7 +60,7 @@ class ModManager: func = StoredFunc(self._on_mods_loaded, mods) self.thread_man.set_cleanup_func(func) - def _on_mods_loaded(self, mods: list[list[str, str, str, float, bool]]) -> None: + def _on_mods_loaded(self, mods: list[list[Any]]) -> None: self.store = ModelFactory().make_mod_store() self.store.extend(mods) self.treeview.set_model(self.store) @@ -83,14 +83,20 @@ class ModManager: def get_mod_from_tree_path( self, tree_path: Gtk.TreePath - ) -> tuple[str, Gtk.TreeIter]: + ) -> tuple[str, Gtk.TreeIter] | None: model = self.treeview.get_model() + if model is None: + return None tree_iter = model.get_iter(tree_path) - mod = model.get(tree_iter, 2)[0] + mod = model.get_value(tree_iter, 2)[0] + print(mod) return mod, tree_iter - def delete_single_mod(self, tree_path: Gtk.TreePath) -> Gtk.TreeIter: - mod, _iter = self.get_mod_from_tree_path(tree_path) + def delete_single_mod(self, tree_path: Gtk.TreePath) -> Gtk.TreeIter | None: + res = self.get_mod_from_tree_path(tree_path) + if res is None: + return None + mod, _iter = res steam_path = Path(self.path) mods_path = get_local_mod_path(steam_path) @@ -118,12 +124,17 @@ class ModManager: #if model: # model.remove(_iter) # FIXME: do not remove from model interface directly remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version) - mods = len(self.treeview.get_model()) + model = self.treeview.get_model() + if model is None: + return + mods = len(model) msg = self.format_mod_statusbar() self.emitter.emit("mods_updated", msg, mods) def uncolorize_mods(self) -> None: model = self.treeview.get_model() + if model is None: + return for mod in model: _iter = mod.iter path = model.get_path(_iter) @@ -175,6 +186,8 @@ class ModManager: def select_colorized(self) -> None: model = self.treeview.get_model() + if model is None: + return sel = self.treeview.get_selection() for mod in model: it = mod.iter diff --git a/dzgui/model/proxy_model.py b/dzgui/model/proxy_model.py index 9cbc7da..472bf1a 100644 --- a/dzgui/model/proxy_model.py +++ b/dzgui/model/proxy_model.py @@ -32,8 +32,9 @@ class ProxyModelManager: self.proxy_model: "FastInsertListStore" = None self.filter_man = filter_man - self.control_model: list = None - self.filtered: list = None + # TODO: list typehints + self.control_model: list | None = None + self.filtered: list | None = None self.success = True def has_control_model(self) -> bool: @@ -61,9 +62,9 @@ class ProxyModelManager: def remove_row_from_control(self, record: "Record") -> None: addr = f"{record.ip}:{record.gameport}" qport = record.qport - for record in self.control_model: - if addr == record[7] and qport == record[8]: - self.control_model.remove(record) + for row in self.control_model: + if addr == row[7] and qport == row[8]: + self.control_model.remove(row) break self.wipe_cache() self.filter(FilterMode.INITIAL, skip_cache=True) diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index 351e5c4..ec93aa5 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -86,7 +86,7 @@ class ServerModelManager: key = config_man.lookup(Preferences.STEAM) job = Servers.query_api params = Servers.params - servers: list[dict[Any, Any]] = [] + servers = [] with ThreadPoolExecutor() as executor: futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params] for future in as_completed(futures): @@ -105,7 +105,8 @@ class ServerModelManager: ) return j = res.json - servers += j["response"]["servers"] + if j is not None: + servers.extend(j["response"]["servers"]) except Exception as e: logger.critical(e) self.thread_man.set_cleanup_func( @@ -117,7 +118,8 @@ class ServerModelManager: res = Servers.query_api(key, APPID_DAYZ_EXP, "") if res.status == 200 and res.parsed is True: j = res.json - servers += j["response"]["servers"] + if j is not None: + servers.extend(j["response"]["servers"]) parsed = Servers.parse_json(servers) self._push_data(parsed) @@ -364,14 +366,15 @@ class ServerModelManager: if self.first_iteration: self._update_maps() - def _cleanup_on_failure(self, show_dialog=True) -> None: + def _cleanup_on_failure(self, show_dialog: bool = True) -> None: # TODO: disable map, keyword, and filter widgets if model is None # -> signal driven (servers_empty, servers_failed_to_load) if self.preserve_on_fail is False: + # CHORE: test if maps are cleared on failure self.tv.set_model(None) - filter_man = self.tv.get_filter_man() - filter_man.set_unique_maps(None) + #filter_man = self.tv.get_filter_man() + #filter_man.set_unique_maps([]) # TODO: emit signal to not disable widget sensitivity # TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty" diff --git a/dzgui/strings/connect_panel.py b/dzgui/strings/connect_panel.py index 4fc831a..abfcfc8 100644 --- a/dzgui/strings/connect_panel.py +++ b/dzgui/strings/connect_panel.py @@ -13,7 +13,7 @@ connect_entry_placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numeric fav_heading="Favorite server" favs_empty="None set. Right click a server and select 'Set favorite' to set." -lan_heading="LAN query port", +lan_heading="LAN query port" lan_default_button="Default port (27016)" lan_custom_button="Custom port" lan_scan_button="Scan" diff --git a/dzgui/views/dialogs/servers.py b/dzgui/views/dialogs/servers.py index 70dd7f5..3ed4178 100644 --- a/dzgui/views/dialogs/servers.py +++ b/dzgui/views/dialogs/servers.py @@ -59,7 +59,7 @@ class ServerDialog(GenericDialog): class ServerDetailsDialog(ServerDialog): - def __init__(self, controller, details: "Details"): + def __init__(self, controller: "Controller", details: "Details"): name = controller.get_server_name() super().__init__(controller, strings.server_details, name) @@ -96,6 +96,9 @@ class ServerDetailsDialog(ServerDialog): self.pack(scrollable_message) + if details.data is None: + return + for row in details.data: self.store.append(row + [Pango.Weight.BOLD]) self.view.set_model(self.store) @@ -115,7 +118,7 @@ class ServerDetailsDialog(ServerDialog): class ServerModDialog(ServerDialog): - def __init__(self, controller, mods: list[str]): + def __init__(self, controller: "Controller", mods: list[list[str]]): name = controller.get_server_name() super().__init__(controller, strings.modlist, name) diff --git a/dzgui/views/pages/servers.py b/dzgui/views/pages/servers.py index f3fb435..63fb091 100644 --- a/dzgui/views/pages/servers.py +++ b/dzgui/views/pages/servers.py @@ -76,15 +76,20 @@ class ServerNotebook(Gtk.ScrolledWindow): if cur_page == saved: return page = self.notebook.get_nth_page(saved) + if not page: + return label = self.notebook.get_tab_label(page) if label is None: return - label.set_text(f"{server_labels.saved}*") + if hasattr(label, "set_text"): + label.set_text(f"{server_labels.saved}*") 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() + if tv is None: + return state = False if tv.get_model() is None else True tv.set_headers_visible(state) tv.set_loaded(True) @@ -114,14 +119,19 @@ class ServerNotebook(Gtk.ScrolledWindow): return False def grab_content_area(self) -> None: - self.get_active_treeview().grab_focus() + tv = self.get_active_treeview() + if tv is not None: + tv.grab_focus() def get_current_tab_text(self) -> str: ind = self.notebook.get_current_page() child = self.notebook.get_nth_page(ind) if child is None: return "" - return self.notebook.get_tab_label_text(child) + text = self.notebook.get_tab_label_text(child) + if text is None: + return "" + return text def _on_page_changed( self, notebook: Gtk.Notebook, child: ScrollableTree, index: int @@ -130,21 +140,27 @@ class ServerNotebook(Gtk.ScrolledWindow): return label = self.notebook.get_tab_label_text(child) - # TODO: strings - text = label.strip("*") - self.notebook.set_tab_label_text(child, text) + if label is not None: + # TODO: strings + text = label.strip("*") + self.notebook.set_tab_label_text(child, text) tree = child.get_tree() self.emitter.emit("server_page_changed", tree) # NOTE: spawns a thread - self.controller.populate_model(self.get_active_treeview()) + tv = self.get_active_treeview() + if tv is not None: + self.controller.populate_model(tv) - def get_active_treeview(self) -> ServerTreeView: + def get_active_treeview(self) -> ServerTreeView | None: index = self.notebook.get_current_page() scrollable = self.notebook.get_nth_page(index) - treeview = scrollable.get_children()[0] - return treeview + if scrollable is not None and hasattr(scrollable, "get_children"): + tv = scrollable.get_children()[0] + if isinstance(tv, ServerTreeView): + return tv + return None def add_notification(self) -> None: saved = self.notebook.get_nth_page(1) diff --git a/dzgui/views/trees/tree_log.py b/dzgui/views/trees/tree_log.py index 2340d59..1d7dae4 100644 --- a/dzgui/views/trees/tree_log.py +++ b/dzgui/views/trees/tree_log.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: logger = logging.getLogger(APP_NAME) -class LogTreeView(ContextMixin, TreeView): +class LogTreeView(ContextMixin, TreeView): # type: ignore def __init__(self, controller: "Controller") -> None: super().__init__(controller, menu=ContextMenuGroup.LOG) @@ -56,7 +56,8 @@ class LogTreeView(ContextMixin, TreeView): sortable = Gtk.TreeModelSort(_filter) self.set_model(sortable) _filter.refilter() - self.set_cursor(0) + path = Gtk.TreePath.new_from_indices([0]) + self.set_cursor(path) def toggle_filter(self, _filter: str) -> None: if _filter in self.filters: @@ -64,19 +65,20 @@ class LogTreeView(ContextMixin, TreeView): else: self.filters.append(_filter) # NOTE: unwrap TreeModelSort and TreeModelFilter - self.get_model().get_model().refilter() + self.get_model().get_model().refilter() # type: ignore def _filter_rows( - self, model: Gtk.ListStore, _iter: Gtk.TreeIter, data: Any - ) -> None: + self, filter_model: Gtk.TreeModelFilter, _iter: Gtk.TreeIter, data: Any + ) -> bool: if len(self.filters) < 1: return False - return model[_iter][1] in self.filters + return filter_model[_iter][1] in self.filters - def _on_log_buttonpress(self, widget: Gtk.Widget, event: Gdk.EventButton) -> None: + def _on_log_buttonpress(self, widget: Gtk.Widget, event: Gdk.EventButton) -> bool: if event.button == Gdk.BUTTON_SECONDARY: self.present_menu(widget, event) return True + return False def _on_log_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: self.present_menu(widget, event) diff --git a/dzgui/views/trees/tree_mods.py b/dzgui/views/trees/tree_mods.py index 8067343..8e9d165 100644 --- a/dzgui/views/trees/tree_mods.py +++ b/dzgui/views/trees/tree_mods.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: logger = logging.getLogger(APP_NAME) -class ModTreeView(ModsMixin, ContextMixin, TreeView): +class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore def __init__(self, controller: "Controller") -> None: super().__init__(controller, menu=ContextMenuGroup.MOD) self.controller = controller @@ -64,31 +64,17 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None: if mods < 1: return - self.set_cursor(0) + path = Gtk.TreePath.new_from_indices([0]) + self.set_cursor(path) def get_selected_mod(self) -> str: path = self.get_focused_row_path() model = self.get_model() tree_iter = model.get_iter(path) mod = model.get(tree_iter, 2)[0] - return mod - # return mod, tree_iter + return str(mod) - # def _on_mods_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: - # # TODO: multiselect - # # if event.keyval is Gdk.KEY_space: - # # it = self.get_focused_row_iter() - # # self.get_selection().select_iter(it) - # # path = self.get_focused_row_path() - # # self.set_cursor(path) - # # return False - # self.present_menu(widget, event) - - # def _on_mods_button_press(self, widget: Gtk.Widget, event: Gdk.EventButton) -> None: - # if event.button == 3: - # self.present_menu(widget, event) - - def _parent_selection_changed(self, base_class: TreeView, sel: Gtk.TreeSelection): + def _parent_selection_changed(self, base_class: TreeView, sel: Gtk.TreeSelection) -> None: pass def _format_color( diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index 3866126..9980671 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -35,7 +35,7 @@ if TYPE_CHECKING: QUEUE_CHECK_DELAY = 200 -class ServerTreeView(ContextMixin, TreeView): +class ServerTreeView(ContextMixin, TreeView): # type: ignore def __init__( self, controller: "Controller", enum: ServerTab, menu: ContextMenuGroup ) -> None: @@ -58,9 +58,9 @@ class ServerTreeView(ContextMixin, TreeView): self.queue_id: int self.handler_id: int - self.queue = Queue() + self.queue: Queue = Queue() - self.seen_cache = [] + self.seen_cache: list[str] = [] prefs = self.controller.get_prefs() columns = prefs.paths.columns @@ -119,7 +119,7 @@ class ServerTreeView(ContextMixin, TreeView): def _on_tooltip( self, - widget: Gtk.Widget, + treeview: Self, x: int, y: int, keyboard_mode: bool, @@ -128,12 +128,13 @@ class ServerTreeView(ContextMixin, TreeView): """ Present record data for the hovered row even if it is unfocused """ - coords = widget.convert_widget_to_bin_window_coords(x, y) - path = self.get_path_at_pos(coords.bx, coords.by) - if path is None: + coords_x, coords_y = treeview.convert_widget_to_bin_window_coords(x, y) + path = self.get_path_at_pos(coords_x, coords_y) + if path is None or path[0] is None: return False - model = self.get_model() + if model is None: + return False tree_iter = model.get_iter(path[0]) ip = model.get_value(tree_iter, 7) qport = model.get_value(tree_iter, 8) @@ -188,7 +189,7 @@ class ServerTreeView(ContextMixin, TreeView): # NOTE: get final width after drag action completes GLib.idle_add(self.controller.propagate_column_width, col) - def start_distcalc(self, emitter: Optional["Emitter"] = None): + def start_distcalc(self, emitter: Union["Emitter", None] = None) -> None: self.emitter.emit("distcalc_started") record = self.get_record() if record is None: @@ -248,14 +249,10 @@ class ServerTreeView(ContextMixin, TreeView): case Gdk.KEY_r: # TODO: unimplemented, needs threading self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self) - case _: - return False - return True else: match event.keyval: case Gdk.KEY_l | Gdk.KEY_Right: self.emitter.emit("request_button_box_focus") - return True case _: self.emitter.emit("check_button_pressed", event.keyval) @@ -266,12 +263,13 @@ class ServerTreeView(ContextMixin, TreeView): path = pathlist[0] tree_iter = model.get_iter(path) has_mods = model.get_value(tree_iter, 11) - return has_mods + return bool(has_mods) - def get_selected_row(self) -> Gtk.TreeModelRow: - sel = self.get_selection() - sels = sel.get_selected_rows() - return sels[0] + #def get_selected_row(self) -> Gtk.TreeModelRow: + # sel = self.get_selection() + # sels = sel.get_selected_rows() + # print(type(sels[0])) + # return sels[0] def is_in_favs(self) -> bool: record = self.get_record_string() @@ -283,6 +281,8 @@ class ServerTreeView(ContextMixin, TreeView): self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn ) -> None: record = self.get_record() + if record is None: + return self.controller.connect_by_record(record) def _parent_selection_changed( @@ -321,14 +321,14 @@ class ServerTreeView(ContextMixin, TreeView): def set_loaded(self, status: bool) -> None: self.loaded = status - def get_model_and_control_model(self) -> tuple[Union["FastInsertListStore", None], list[Any]]: + def get_model_and_control_model(self) -> tuple[Union[Gtk.TreeModel, None], list[Any]]: model = self.get_model() control = self.proxy_man.get_control() return model, control @staticmethod def ping_server( - model, _iter: Gtk.TreeIter, ip: str, qport: int, ping_column: int + model: "FastInsertListStore", _iter: Gtk.TreeIter, ip: str, qport: int, ping_column: int ) -> None: _ping = ping(ip, qport) GLib.idle_add(lambda: model.set(_iter, ping_column, _ping))