diff --git a/CHANGELOG.md b/CHANGELOG.md index 7916e9c..d65c8c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ - List timestamp of last DayZ client update - Validate mods via Mod page to pull in updates - Support for Nasdara (Badlands) DLC +- Load offline mods ### Changed - Conform to PEP 440 versioning for beta versions @@ -99,16 +100,11 @@ - Current keyword filter not being applied when refreshing table - Steam Flatpak cannot run DZGUI as a non-Steam game - Collisions between native Steam and Steam Flatpak clients - -### Unreleased -- Load offline mods -- Local documentation -- Raw debug command in context menu +- Sticky toggle signal on radio buttons when reloading Options ### Developer-facing -- Add pyproject.toml file -- Show deprecation warnings - Options -> Dev page +- Raw debug command in preconnect menu ## [6.0.5] 2026-05-17 ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77d627a..1e5fe05 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ If you are a developer, you can skip to the end. # Requesting help If you encounter a problem with DZGUI, you can submit tickets on the GitHub -[issue tracker](https://github.com/aclist/dztui/issues) under the +[issue tracker](https://github.com/aclist/dzgui/issues) under the `troubleshooting` tag. # How can I help the project? @@ -24,7 +24,7 @@ accordingly. ## Submitting a ticket -Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues). +Navigate to the GitHub [issue tracker](https://github.com/aclist/dzgui/issues). From there, follow the onscreen prompts. You will be asked questions such as: - What version are you using? @@ -55,7 +55,6 @@ The following naming conventions apply for PRs: - fix/ - patch/hotfix branches - feat/ - feature branches - doc/ - documentation branches -- infra/ - infrastructure branches Implement your changes and test them locally. If they work, you may open a merge request, and then we review your changes. If everything is OK, it diff --git a/DEVELOPERS.md b/DEVELOPERS.md index b201138..abecac9 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -5,7 +5,7 @@ ### Fetch submodules git submodule update --recursive --init -### Install with dev dependencies +### Install into editable venv with dev dependencies uv pip install -e '.[dev]' This includes all dependencies for building, linting, testing, and checking in code. @@ -16,7 +16,7 @@ docs: documentation tools lint: code linting, fixing, and type checking test: unit and integration tests -## Miscellaneous +## Build process ### Setting up precommit hooks pre-commit install @@ -25,6 +25,7 @@ pre-commit install sphinx-build -M html source build -a ### Building standalone release binaries -1. Set up the build target: `rustup target add x86_64-unknown-linux-musl` -2. Run `python3 scripts/release.py` -3. Generate a release and tag against the resulting tarfile generated in `dist` +1. Install the applicable system package containing the 'musl-gcc' wrapper +2. Set up the build target: `rustup target add x86_64-unknown-linux-musl` +3. Run `python3 scripts/release.py` +4. Generate a release and tag against the resulting tarfile generated in `dist` diff --git a/dzgui/api/mods.py b/dzgui/api/mods.py index 234835d..1d59528 100644 --- a/dzgui/api/mods.py +++ b/dzgui/api/mods.py @@ -3,7 +3,6 @@ import logging import re from concurrent.futures import ThreadPoolExecutor, wait -from dataclasses import dataclass from pathlib import Path from typing import Any diff --git a/dzgui/api/servers.py b/dzgui/api/servers.py index 546ec15..5fa219c 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 Any, Optional, TYPE_CHECKING, Union +from typing import Any, TYPE_CHECKING, Union from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT from dzgui.const.endpoints import STEAM_SERVERS @@ -491,7 +491,7 @@ def query_by_record(record: Record, update_gameport: bool = False) -> A2SInfo: return A2SInfo(record, None) -def short_ip_to_record(addr: str) -> Optional[Record]: +def short_ip_to_record(addr: str) -> Record | None: r = addr.split(":") # TODO: raise exceptions instead of Nonetype if len(r) != 2: @@ -499,7 +499,7 @@ def short_ip_to_record(addr: str) -> Optional[Record]: return Record(r[0], 0, int(r[1])) -def fqip_to_record(addr: str) -> Optional[Record]: +def fqip_to_record(addr: str) -> Record | None: r = addr.split(":") if len(r) != 3: return None diff --git a/dzgui/app_init.py b/dzgui/app_init.py index 96ff4d1..30e3a6d 100644 --- a/dzgui/app_init.py +++ b/dzgui/app_init.py @@ -6,7 +6,6 @@ from importlib import resources from pathlib import Path from typing import TYPE_CHECKING - from dzgui.const.constants import ( APP_NAME, APP_NAME_LOWER, @@ -107,7 +106,7 @@ def load_steam_icon() -> None: import gi gi.require_version("Gtk", "3.0") - from gi.repository import Gtk # noqa + from gi.repository import Gtk try: traversable = resources.files(APP_NAME_LOWER).joinpath(ICONS_PATH) diff --git a/dzgui/config/xdg.py b/dzgui/config/xdg.py index 532c73f..eac3dcf 100644 --- a/dzgui/config/xdg.py +++ b/dzgui/config/xdg.py @@ -12,7 +12,7 @@ from dzgui.const.constants import ( from dzgui.init.proc import is_sandboxed -@dataclass +@dataclass(slots=True, frozen=True) class Xdg: config: Path columns: Path diff --git a/dzgui/const/endpoints.py b/dzgui/const/endpoints.py index ed6239d..547cb5d 100644 --- a/dzgui/const/endpoints.py +++ b/dzgui/const/endpoints.py @@ -8,9 +8,9 @@ STEAM_UNSUB = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1 STEAM_APP_DETAILS = "https://store.steampowered.com/api/appdetails?" GITHUB = "https://github.com/aclist" -GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest" +GITHUB_RELEASES = "https://api.github.com/repos/aclist/dzgui/releases/latest" CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest" -GITHUB_USER_RELEASES = "https://github.com/aclist/dztui/releases" +GITHUB_USER_RELEASES = "https://github.com/aclist/dzgui/releases" DB_IP = "https://db-ip.com/db/download/ip-to-city-lite" IP_ECHO = "https://ipecho.net/plain" COORDS_API = "http://ip-api.com/json" diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 58de7d2..169c72a 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -1,11 +1,11 @@ +import gi import logging import threading from typing import Any, TYPE_CHECKING, Union -import dzgui.util._json as JSON # noqa - - +from dzgui.config.userprefs import UserPrefs +from dzgui.const.constants import APP_NAME from dzgui.const.enum import ( ButtonType, ContextMenuItem, @@ -14,9 +14,6 @@ from dzgui.const.enum import ( Preferences, ServerTab, ) - -from dzgui.config.userprefs import UserPrefs -from dzgui.const.constants import APP_NAME from dzgui.controllers.emitter import Emitter from dzgui.managers.config import ConfigManager from dzgui.managers.connection import ConnectionManager @@ -29,8 +26,6 @@ from dzgui.util.open_links import open_user_workshop, open_workshop_page from dzgui.views.dialogs.filepicker import FilePicker from dzgui.views.dialogs.generic import ExceptionDialog -import gi - gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, GLib, GObject @@ -371,7 +366,6 @@ class Controller(GObject.GObject): filter_man = self.get_filter_man() return filter_man.get_filters() - # FIXME: wipe maps store when changing tabs if model is none def has_server_model(self) -> bool: treeview = self.get_active_treeview() proxy_man = treeview.get_proxy_man() diff --git a/dzgui/data/CHANGELOG.md b/dzgui/data/CHANGELOG.md index 4e0eea2..d65c8c8 100644 --- a/dzgui/data/CHANGELOG.md +++ b/dzgui/data/CHANGELOG.md @@ -53,6 +53,11 @@ - Expand/collapse changelog versions - Support local light/dark mode changes on system theme - Server time calculator +- Add "Unmodded" filter to servers +- List timestamp of last DayZ client update +- Validate mods via Mod page to pull in updates +- Support for Nasdara (Badlands) DLC +- Load offline mods ### Changed - Conform to PEP 440 versioning for beta versions @@ -74,6 +79,7 @@ - Enable LAN page Empty/Full filters on first run of app - Propagate subscribed mods to Steam client - Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow +- Use SQLite DB for geolocation records ### Dropped - Battlemetrics API support @@ -92,16 +98,13 @@ - Moved dialogs out of threads - History table not being in lock-step with state file - Current keyword filter not being applied when refreshing table - -### Unreleased -- Load offline mods -- Local documentation -- Raw debug command in context menu +- Steam Flatpak cannot run DZGUI as a non-Steam game +- Collisions between native Steam and Steam Flatpak clients +- Sticky toggle signal on radio buttons when reloading Options ### Developer-facing -- Add pyproject.toml file -- Show deprecation warnings - Options -> Dev page +- Raw debug command in preconnect menu ## [6.0.5] 2026-05-17 ### Changed diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index a57a421..f3ac7e9 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -330,7 +330,6 @@ class ConnectionManager: self.thread_man.update_dialog(waiting_for_launch) while True: - # FIXME: cancel should not be visible here per setting above if self.controller.is_cancel_pending(): # TODO: some facility to also close spawned steam process return diff --git a/dzgui/model/proxy_model.py b/dzgui/model/proxy_model.py index 2cd9b24..1f0028e 100644 --- a/dzgui/model/proxy_model.py +++ b/dzgui/model/proxy_model.py @@ -58,6 +58,11 @@ class ServerRow: getter = cls._to_tuple return [getter(row) for row in rows] + @classmethod + def get_index(cls, field: str) -> int: + _fields = [f.name for f in fields(cls)] + return _fields.index(field) + class FiltrationManager: def __init__(self) -> None: diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index 5e217db..c1e5200 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -34,7 +34,7 @@ LAN_TIMEOUT = 0.5 API_TIMEOUT = 3 -@dataclass +@dataclass(slots=True, frozen=True) class NewPlayerCount: treeiter: Gtk.TreeIter players: int @@ -341,8 +341,8 @@ class ServerModelManager: return config_man.add_saved_server(fqip) - # FIXME: this is valid if saved servers tab is already open, - # but not if app was just booted; causes single row to appear prematurely + # TODO: this is valid if saved servers tab is already open, + # but not if tab is unloaded; causes single row to appear prematurely if proxy_man.has_control_model() is False: proxy_man.push(records) else: diff --git a/dzgui/util/immutable.py b/dzgui/util/immutable.py new file mode 100644 index 0000000..98c0ce4 --- /dev/null +++ b/dzgui/util/immutable.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from typing import Callable + +def immutable_dataclass(cls: type | None = None, **kwargs: bool) -> type | Callable[[type], type]: + kwargs["slots"] = True + kwargs["frozen"] = True + if cls is None: + return lambda c: dataclass(c, **kwargs) + return dataclass(cls, **kwargs) diff --git a/dzgui/util/ip.py b/dzgui/util/ip.py index 168e036..f13f16b 100644 --- a/dzgui/util/ip.py +++ b/dzgui/util/ip.py @@ -1,10 +1,10 @@ import a2s -from ipaddress import ip_address import requests import shutil import subprocess from dataclasses import dataclass +from ipaddress import ip_address from typing import TYPE_CHECKING from dzgui.api.servers import Record @@ -12,12 +12,11 @@ from dzgui.config.ipdb import read_from_table from dzgui.const.constants import REQUEST_TIMEOUT from dzgui.const.endpoints import IP_ECHO - if TYPE_CHECKING: from pathlib import Path -@dataclass +@dataclass(slots=True, frozen=True) class Coords: lat: float lon: float diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 756dae2..4c69bdc 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -1,3 +1,4 @@ +import gi import logging import signal @@ -34,8 +35,6 @@ from dzgui.views.pages.preconnect import PreConnectionAssistant from dzgui.views.pages.servers import ServerNotebook from dzgui.views.pages.thanks import Thanks -import gi - gi.require_version("Gtk", "3.0") gi.require_version("GLibUnix", "2.0") from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore @@ -49,7 +48,10 @@ logger = logging.getLogger(APP_NAME) class OuterWindow(Gtk.ApplicationWindow): def __init__(self, app: "App") -> None: super().__init__( - application=app, title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER + application=app, + title=APP_NAME, + border_width=10, + icon_name=APP_NAME_LOWER, ) self.hb = AppHeaderBar() diff --git a/dzgui/views/components/box.py b/dzgui/views/components/box.py index d581255..b48b29a 100644 --- a/dzgui/views/components/box.py +++ b/dzgui/views/components/box.py @@ -1,9 +1,9 @@ -from typing import Sequence - import gi +from typing import Sequence + gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa +from gi.repository import Gtk class GenericBox(Gtk.Box): diff --git a/dzgui/views/components/frame.py b/dzgui/views/components/frame.py index dab2671..e245dff 100644 --- a/dzgui/views/components/frame.py +++ b/dzgui/views/components/frame.py @@ -1,10 +1,11 @@ -from typing import Self -from dzgui.util import css - import gi +from typing import Self + +from dzgui.util import css + gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa +from gi.repository import Gtk # TODO: subclass Gtk.Frame directly diff --git a/dzgui/views/components/misc.py b/dzgui/views/components/misc.py index 80e694e..dbef1f6 100644 --- a/dzgui/views/components/misc.py +++ b/dzgui/views/components/misc.py @@ -1,3 +1,4 @@ +import gi from typing import Self from warnings import deprecated @@ -8,10 +9,8 @@ from dzgui.strings import options from dzgui.views.components.box import HBox from dzgui.views.components.buttons import Icon -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa +from gi.repository import Gtk @deprecated("Client choice is determined at setup time") diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index c04bba7..078bb49 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -106,14 +106,15 @@ class RightPanel(Gtk.Box): if page.loaded is True: return self.filters_vbox.set_sensitive(False) - # TODO: unless it is lan page - # self.refresh_button.set_sensitive(False) + self.refresh_button.set_sensitive(False) def _on_lan_page_init(self, emitter: "Emitter") -> None: self.filters_vbox.set_sensitive(False) def _on_servers_loaded(self, emitter: "Emitter", context: "ServerTab") -> None: # TODO: similar logic on notebook page change + # TODO: wipe maps store when changing tabs if model is None + state = self.controller.has_server_model() self.filters_vbox.set_sensitive(state) diff --git a/dzgui/views/components/scrollable.py b/dzgui/views/components/scrollable.py index 294ca53..c592549 100644 --- a/dzgui/views/components/scrollable.py +++ b/dzgui/views/components/scrollable.py @@ -1,7 +1,7 @@ import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa +from gi.repository import Gtk class NoOverlayScrolledWindow(Gtk.ScrolledWindow): diff --git a/dzgui/views/components/statusbar.py b/dzgui/views/components/statusbar.py index 43e5ff7..fd1ad81 100644 --- a/dzgui/views/components/statusbar.py +++ b/dzgui/views/components/statusbar.py @@ -101,7 +101,7 @@ class Statusbar(Gtk.Grid): if type(context) is NotebookPage: self.prior_enum = context self.prior_status = "" - # FIXME: CalcDist is being called when table is not loaded + if dist is None: self.set_by_context(context, "") return diff --git a/dzgui/views/dialogs/boot.py b/dzgui/views/dialogs/boot.py index bfc0b1e..5a8a718 100644 --- a/dzgui/views/dialogs/boot.py +++ b/dzgui/views/dialogs/boot.py @@ -1,13 +1,9 @@ +import gi import sys + +from enum import Enum from typing import Any, Literal, Self, TYPE_CHECKING -# import time -from enum import Enum - - -from dzgui.views.mixins.colorscheme import ColorAwareApp - -# TODO: import dialog titles from dzgui.api.mods import remove_stale_signatures as remove_stale from dzgui.config.ipdb import get_ipdb from dzgui.const.constants import HEX_GREEN, HEX_RED @@ -20,12 +16,10 @@ from dzgui.util.format import format_exception from dzgui.util.strings import dialog_header from dzgui.util.symlink import rebuild_symlinks from dzgui.views.components.buttons import ClipboardButton - - -import gi +from dzgui.views.mixins.colorscheme import ColorAwareApp gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk # noqa +from gi.repository import Gtk, GLib, Gdk if TYPE_CHECKING: from dzgui.config.xdg import Xdg diff --git a/dzgui/views/dialogs/filepicker.py b/dzgui/views/dialogs/filepicker.py index 7951ae7..2c0e3cc 100644 --- a/dzgui/views/dialogs/filepicker.py +++ b/dzgui/views/dialogs/filepicker.py @@ -54,6 +54,7 @@ class FolderPicker(Gtk.FileChooserDialog): return None if res == Gtk.ResponseType.OK: folder = self.get_current_folder() + # TODO: this should never be possible if folder is None: try: uri = self.get_uri() @@ -61,7 +62,7 @@ class FolderPicker(Gtk.FileChooserDialog): return None return Path.from_uri(uri) except Exception: - # FIXME: edge cases + # TODO: edge cases return Path("/") self.destroy() return Path(folder) diff --git a/dzgui/views/dialogs/servers.py b/dzgui/views/dialogs/servers.py index b58d352..643d132 100644 --- a/dzgui/views/dialogs/servers.py +++ b/dzgui/views/dialogs/servers.py @@ -15,7 +15,7 @@ from dzgui.views.dialogs.generic import GenericDialog from dzgui.views.trees.tree_base import TreeView gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, Pango # noqa +from gi.repository import Gtk, Gdk, Pango if TYPE_CHECKING: from dzgui.api.servers import Details diff --git a/dzgui/views/mixins/colorscheme.py b/dzgui/views/mixins/colorscheme.py index 2e0be9c..f70a1bd 100644 --- a/dzgui/views/mixins/colorscheme.py +++ b/dzgui/views/mixins/colorscheme.py @@ -1,3 +1,5 @@ +import gi + from typing import Any from dzgui.const.constants import ( @@ -7,10 +9,8 @@ from dzgui.const.constants import ( GIO_SETTINGS_VAL, ) -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gio # noqa +from gi.repository import Gtk, Gio class ColorAwareApp: diff --git a/dzgui/views/mixins/help_menu_mixin.py b/dzgui/views/mixins/help_menu_mixin.py index d287bb7..5ef6fda 100644 --- a/dzgui/views/mixins/help_menu_mixin.py +++ b/dzgui/views/mixins/help_menu_mixin.py @@ -1,7 +1,7 @@ import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk # noqa +from gi.repository import Gtk, Gdk class HelpMenuMixin: diff --git a/dzgui/views/mixins/scrollable_mixin.py b/dzgui/views/mixins/scrollable_mixin.py index 79d83b8..db99c81 100644 --- a/dzgui/views/mixins/scrollable_mixin.py +++ b/dzgui/views/mixins/scrollable_mixin.py @@ -1,10 +1,10 @@ +import gi + from dzgui.const.enum import VAdjustment from dzgui.const.constants import SCROLL_INCREMENT -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk # noqa +from gi.repository import Gtk, Gdk class ScrollableMixin: diff --git a/dzgui/views/pages/mods.py b/dzgui/views/pages/mods.py index 05696a7..5903091 100644 --- a/dzgui/views/pages/mods.py +++ b/dzgui/views/pages/mods.py @@ -1,3 +1,4 @@ +import gi from pathlib import Path from typing import Self, TYPE_CHECKING @@ -8,10 +9,8 @@ from dzgui.views.components.buttons import SteamWorkshopButton from dzgui.views.components.scrollable import NoOverlayScrolledWindow from dzgui.views.trees.tree_mods import ModTreeView -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk # noqa +from gi.repository import Gtk if TYPE_CHECKING: from dzgui.controllers.mc import Controller diff --git a/dzgui/views/pages/offline.py b/dzgui/views/pages/offline.py index ceb5686..6398e4a 100644 --- a/dzgui/views/pages/offline.py +++ b/dzgui/views/pages/offline.py @@ -1,9 +1,10 @@ from __future__ import annotations + +import gi + from enum import Enum from typing import Self, TYPE_CHECKING, Union -from dzgui.util import css -from dzgui.const.icons import ICON_EDIT_DELETE, ICON_FOLDER, ICON_WARNING from dzgui.const.constants import ( APPID_DAYZ, APPID_DAYZ_EXP, @@ -11,8 +12,10 @@ from dzgui.const.constants import ( APPNAME_DAYZ_EXP_HUMAN, ) from dzgui.const.enum import ContextMenuGroup, NotebookPage +from dzgui.const.icons import ICON_EDIT_DELETE, ICON_FOLDER, ICON_WARNING from dzgui.managers.offline import OfflineManager from dzgui.strings import generic, offline +from dzgui.util import css from dzgui.views.components.box import HBox, VBox from dzgui.views.components.buttons import Icon, IconTextButton from dzgui.views.components.eventbox import InfoEventBox @@ -21,11 +24,8 @@ from dzgui.views.components.scrollable import NoOverlayScrolledWindow from dzgui.views.components.misc import ErrorPopover from dzgui.views.trees.tree_mods import OfflineModTreeView - -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GObject # noqa +from gi.repository import Gtk, Gdk if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -136,7 +136,6 @@ class FolderHBox(HBox): self.folder = "" self.label.set_label("") self.unset_button.hide() - # FIXME: rename signal self.emitter.emit("custom_mods_unloaded", self) def get_button(self) -> Gtk.Button: diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 1fa3fbb..8c681f1 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -1,9 +1,10 @@ +import gi + from pathlib import Path from typing import Any, Callable, TYPE_CHECKING from dzgui.api import pefile as PeFile from dzgui.api.steam import get_last_updated - from dzgui.config import query from dzgui.const.constants import ( APPID_DAYZ, @@ -14,12 +15,11 @@ from dzgui.const.constants import ( FLATPAK_SANDBOX, FLATPAK_STEAM_PATH, ) -from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL from dzgui.const.endpoints import STEAM_API_SETUP from dzgui.const.enum import Preferences, ServerTab +from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL from dzgui.strings import options from dzgui.util import strings, css, open_links - from dzgui.views.components.box import ShortHBox, VBox from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.labels import LeftLabel @@ -29,11 +29,8 @@ from dzgui.views.components.misc import ErrorPopover from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.mixins.scrollable_mixin import ScrollableMixin - -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk # noqa +from gi.repository import Gtk, Gdk if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -233,10 +230,19 @@ class ToggleField(Gtk.Box): self.pack_start(self.radio1, expand=False, fill=False, padding=0) self.pack_start(self.radio2, expand=False, fill=False, padding=0) + self.signal_id: int | None = None + + def disconnect_signal(self) -> None: + if self.signal_id is not None: + self.radio1.disconnect(self.signal_id) + def set_suboption_active(self, state: bool) -> None: # NOTE: defer connection of signal until after state is set self.radio2.set_active(state) - self.radio1.connect("toggled", self._on_radio_toggled, self.context) + self.disconnect_signal() + self.signal_id = self.radio1.connect( + "toggled", self._on_radio_toggled, self.context + ) def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None: self.controller.toggle_config(context) @@ -486,14 +492,5 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore start_tab = self.controller.query_config(Preferences.START_TAB) self.start_tab_combo.set_active(start_tab) - def _suppress_toggles(self, state: bool) -> None: - for toggle in [ - self.fullscreen_toggle, - self.distance_toggle, - ]: - self.controller.suppress_signal( - self, toggle.get_children()[0], "_on_radio_toggled", state - ) - def grab_content_area(self) -> None: self.grab_focus() diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index 3f54b31..1ec2e17 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -1,11 +1,12 @@ -from dataclasses import dataclass +import gi + from typing import Self, Sequence, TYPE_CHECKING +from dzgui.const.enum import NotebookPage from dzgui.const.icons import ( ICON_ERROR, ICON_WARNING, ) -from dzgui.const.enum import NotebookPage from dzgui.util.css import add_class from dzgui.util.keys import is_ctrl_mask from dzgui.util.localize import number @@ -15,33 +16,15 @@ from dzgui.views.dialogs.generic import DebugDialog from dzgui.views.components.frame import HeadingFrame from dzgui.views.trees.tree_server_mods import ServerModTreeView -import gi - gi.require_version("Gtk", "3.0") from gi.repository import Gdk, Gtk - if TYPE_CHECKING: from dzgui.managers.connection import Prerequisites from dzgui.controllers.mc import Controller from dzgui.controllers.emitter import Emitter -@dataclass -class Warnings: - passworded: bool - dayz_running: bool - no_steam: bool - wrong_version: bool - - -@dataclass -class Errors: - no_space: bool - no_dayz_exp: bool - no_dayz: bool - - class Placeholder(Gtk.Label): def __init__(self, text: str) -> None: super().__init__( diff --git a/dzgui/views/trees/tree_base.py b/dzgui/views/trees/tree_base.py index 0919b08..2d16df7 100644 --- a/dzgui/views/trees/tree_base.py +++ b/dzgui/views/trees/tree_base.py @@ -1,15 +1,14 @@ +import gi import logging -from typing import Any, Optional, TYPE_CHECKING +from typing import Any, TYPE_CHECKING from dzgui.const.constants import APP_NAME, SEPARATOR from dzgui.util.keys import is_ctrl_mask, is_navkey from dzgui.views.mixins.cursor_mixin import CursorMixin -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa +from gi.repository import Gtk, Gdk, GObject logger = logging.getLogger(APP_NAME) @@ -20,7 +19,7 @@ if TYPE_CHECKING: class TreeView(CursorMixin, Gtk.TreeView): # type: ignore def __init__( - self, controller: "Controller", menu: Optional["ContextMenuGroup"] + self, controller: "Controller", menu: "ContextMenuGroup | None" ) -> None: super().__init__( enable_search=False, @@ -59,7 +58,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore return True return False - def get_current_iter(self) -> Optional[Gtk.TreeIter]: + def get_current_iter(self) -> Gtk.TreeIter | None: it = self.get_selection().get_selected()[1] return it diff --git a/dzgui/views/trees/tree_menu.py b/dzgui/views/trees/tree_menu.py index a0964bc..c64b371 100644 --- a/dzgui/views/trees/tree_menu.py +++ b/dzgui/views/trees/tree_menu.py @@ -10,7 +10,7 @@ from dzgui.views.trees.tree_base import TreeView import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa +from gi.repository import Gtk logger = logging.getLogger(APP_NAME) diff --git a/dzgui/views/trees/tree_mods.py b/dzgui/views/trees/tree_mods.py index 64dd55c..aa48c38 100644 --- a/dzgui/views/trees/tree_mods.py +++ b/dzgui/views/trees/tree_mods.py @@ -1,4 +1,6 @@ +import gi import logging + from typing import Any, TYPE_CHECKING from dzgui.const.constants import APP_NAME, HEX_RED @@ -9,10 +11,8 @@ from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.mixins.mods_mixin import ModsMixin from dzgui.views.trees.tree_base import TreeView -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa +from gi.repository import Gtk, Gdk if TYPE_CHECKING: diff --git a/dzgui/views/trees/tree_server_mods.py b/dzgui/views/trees/tree_server_mods.py index fe1169c..6b6ca07 100644 --- a/dzgui/views/trees/tree_server_mods.py +++ b/dzgui/views/trees/tree_server_mods.py @@ -1,15 +1,15 @@ +import gi + +from typing import TYPE_CHECKING + from dzgui.const.enum import ContextMenuGroup from dzgui.model.model_factory import ModelFactory from dzgui.strings import server_mods from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.trees.tree_base import TreeView -from typing import TYPE_CHECKING - -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, Pango # noqa +from gi.repository import Gtk, Pango if TYPE_CHECKING: from dzgui.controllers.mc import Controller diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index 37210cb..f4f871a 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -1,30 +1,25 @@ +import gi import logging import threading from queue import Queue -from typing import Any, Self, Union +from typing import Any, Literal, Self, TYPE_CHECKING, Union from warnings import deprecated from dzgui.api.servers import ping, Record from dzgui.const.constants import APP_NAME from dzgui.const.enum import ContextMenuItem, ContextMenuGroup, ServerTab from dzgui.managers.filter import FilterManager -from dzgui.model.proxy_model import ProxyModelManager +from dzgui.model.proxy_model import ProxyModelManager, ServerRow from dzgui.util import strings from dzgui.util.dist import CalcDist from dzgui.util.keys import is_ctrl_mask from dzgui.views.mixins.context_mixin import ContextMixin -from typing import Literal, TYPE_CHECKING - - from dzgui.views.trees.tree_base import TreeView import dzgui.util._json as JSON # noqa -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa - +from gi.repository import Gtk, GLib, Gdk, GObject logger = logging.getLogger(APP_NAME) @@ -34,8 +29,15 @@ if TYPE_CHECKING: from dzgui.model.model_factory import FastInsertListStore from dzgui.util.dist import Haversine +# TODO: consts QUEUE_CHECK_DELAY = 200 +IND_NAME = ServerRow.get_index("name") +IND_ADDR = ServerRow.get_index("ip") +IND_QPORT = ServerRow.get_index("qport") +IND_PING = ServerRow.get_index("ping") +IND_MODDED = ServerRow.get_index("modded") + class ServerTreeView(ContextMixin, TreeView): # type: ignore def __init__( @@ -106,7 +108,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore if column_title in width_map: w = width_map[column_title] column.set_fixed_width(w) - if column_title == "Ping": + if i == IND_PING: column.set_cell_data_func(renderer, self._get_ping) column.connect("notify::fixed-width", self._on_col_width_changed) @@ -145,8 +147,8 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore 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) + ip = model.get_value(tree_iter, IND_ADDR) + qport = model.get_value(tree_iter, IND_QPORT) addr = ip + ":" + str(qport) note = self.controller.get_note_by_record(addr) @@ -169,14 +171,12 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore return self.enum def _on_map(self, widget: Self) -> None: - # TODO: disable filter panel if current model is None state = self.get_enum() is ServerTab.LAN self.emitter.emit("lan_tab_toggled", state) store = self.filter_man.get_map_store() - # FIXME: if model is none, wipe maps - # distinguish this signal from changing map combo in-situ + # TODO: distinguish this signal from changing map combo in-situ self.emitter.emit("load_maps", store) self.handler_id = self.emitter.connect("statusbar_loaded", self.start_distcalc) self.start_queue_checker() @@ -221,11 +221,11 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore def _check_result_queue(self) -> Literal[True]: latest_result = None - # FIXME: empty results cause statusbar to be emptied + # TODO: Nonetype results cause statusbar to be blank while not self.queue.empty(): latest_result = self.queue.get() - # FIXME: cache is being checked in two passes + # TODO: cache is being checked in two passes cache = self.controller.get_dist_cache() if latest_result: @@ -267,15 +267,9 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore (model, pathlist) = sels path = pathlist[0] tree_iter = model.get_iter(path) - has_mods = model.get_value(tree_iter, 11) + has_mods = model.get_value(tree_iter, IND_MODDED) return bool(has_mods) - # 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() if self.controller.get_config_man().is_in_favs(record): @@ -304,17 +298,17 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore self.start_distcalc() def get_name(self) -> str: - return str(self.get_value_at_index(0)) + return str(self.get_value_at_index(IND_NAME)) def get_simplified_ip(self) -> str: - addr = self.get_value_at_index(7) - qport = self.get_value_at_index(8) + addr = self.get_value_at_index(IND_ADDR) + qport = self.get_value_at_index(IND_QPORT) ip = addr.split(":")[0] return f"{ip}:{qport}" def get_record_string(self) -> str: - addr = self.get_value_at_index(7) - qport = self.get_value_at_index(8) + addr = self.get_value_at_index(IND_ADDR) + qport = self.get_value_at_index(IND_QPORT) return f"{addr}:{qport}" def get_record(self) -> Record | None: @@ -353,8 +347,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore def _redraw( self, model: "FastInsertListStore", _iter: Gtk.TreeIter, _ping: int ) -> None: - ping_column = 9 - model.set(_iter, ping_column, _ping) + model.set(_iter, IND_PING, _ping) self.queue_draw() def wipe_cache(self) -> None: @@ -369,13 +362,10 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore data: Any, ) -> None: - addr_column = 7 - qport_column = 8 - - addr = model.get_value(_iter, addr_column).split(":") + addr = model.get_value(_iter, IND_ADDR).split(":") ip = addr[0] gameport = int(addr[1]) - qport = model.get_value(_iter, qport_column) + qport = model.get_value(_iter, IND_QPORT) record = Record(ip, gameport, qport) if record in self.seen_cache: @@ -415,6 +405,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore real_model = self.proxy_man.get_control() from dataclasses import fields from dzgui.model.proxy_model import ServerRow + key = fields(ServerRow)[col_index].name value = getattr(real_model[row_index], key) diff --git a/dzgui/views/trees/tree_simple.py b/dzgui/views/trees/tree_simple.py index be96668..8bd9f48 100644 --- a/dzgui/views/trees/tree_simple.py +++ b/dzgui/views/trees/tree_simple.py @@ -1,13 +1,13 @@ +import gi + from typing import TYPE_CHECKING, Union +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + if TYPE_CHECKING: from dzgui.model.model_factory import FastInsertListStore -import gi - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa - class SimpleTree(Gtk.ScrolledWindow): """Simple scrollable 1-column tree""" diff --git a/tests/test_colorscheme.py b/tests/test_colorscheme.py index a8818dc..3462231 100644 --- a/tests/test_colorscheme.py +++ b/tests/test_colorscheme.py @@ -1,13 +1,12 @@ +import gi import pytest import subprocess from dzgui.const.constants import GIO_SETTINGS_KEY from dzgui.views.mixins.colorscheme import ColorAwareApp -import gi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gio, GLib # noqa +from gi.repository import Gtk, GLib class DummyWidget(ColorAwareApp, Gtk.Button): diff --git a/tests/test_mod_signatures.py b/tests/test_mod_signatures.py index 8aa5dd6..49b9ef2 100644 --- a/tests/test_mod_signatures.py +++ b/tests/test_mod_signatures.py @@ -48,5 +48,5 @@ def test_signatures_with_no_ids(monkeypatch, tmp: str, versions: str) -> None: monkeypatch.setattr("dzgui.api.mods.lookup", mock_lookup) dzgui.api.mods.remove_stale_signatures(path, path) with open(tmp, "r") as f: - lines = f.readlines() - assert lines == ["3410710885,1752864732\n", "3739934289,1780855136\n"] + lines = [line.rstrip() for line in f.readlines()] + assert lines == ["3410710885,1752864732", "3739934289,1780855136"] diff --git a/tests/test_pefile.py b/tests/test_pefile.py index ebcfb0e..02892ab 100644 --- a/tests/test_pefile.py +++ b/tests/test_pefile.py @@ -7,7 +7,7 @@ from dzgui.api.steam import ( VDFLoadError, AppNotInstalledError, AppMovedError, - get_app_path + get_app_path, ) from dzgui.config.query import get_config @@ -26,9 +26,8 @@ def default_steam_path(): conf = get_config(xdg.config) return conf["default_steam_path"] +# TODO: additional parsing tests -# TODO: split into separate test file -# FIXME: use fixtures def test_pefile_length(default_steam_path): try: pe_file_path = PeFile.get_pefile_path(Path(default_steam_path), APPID_DAYZ) @@ -47,6 +46,7 @@ def test_invalid_pefile_path(): raise e +# TODO: split into separate test file @pytest.mark.parametrize( "fixture, exception", [ @@ -57,6 +57,7 @@ def test_invalid_pefile_path(): ) def test_not_in_library(monkeypatch, fixture, exception): import dzgui.api.steam as steam + monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", fixture) folder_path = Path(fixture_path("api")) @@ -74,6 +75,7 @@ def second_drive(): def test_on_second_drive(monkeypatch, second_drive): import dzgui.api.steam as steam + monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", second_drive) folder_path = Path(fixture_path("api")) path = get_app_path(folder_path, APPID_DAYZ)