diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..63a5323 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,124 @@ +# History of the project +This file collects notable milestones and events related to DZGUI, mainly around DZGUI as a framework and the history of development. +A chronological summary of features can be found in CHANGELOG.md. + +## 2021 +### 2021-12-03: DayZ adds support for Battleye over Proton +DayZ was among a handful of games with whitelisted anti-cheat support under Valve's emerging WINE-based Proton compatibility layer. +While adding anti-cheat compatibility at a technical level, this change did not actually allow the client to connect to servers running complex modlists. + +Development of the precursor to DZGUI began at around this time. + +### 2021-12-12: initial version of DZTUI is created +DZTUI, a Bash script and terminal user interface, was the precursor to DZGUI. +This was an initial attempt at bootstrapping the Steam client to handle mods correctly when launching DayZ. +Due to user interest, features continued to be added. This early tool is archived under the 'dztui' branch. + +## 2022 +### 2022-05-09: addition of zenity GUI and project name change (DZGUI) +With the introduction of the Steam Deck, more users wanted a graphical user interface +to interact with DZTUI. Being that DZTUI was purely a Bash script, a method of +drawing menus was needed, so the "zenity" package was used to create prefabricated dialogs. +While this met the minimum requirement to show prompts and selectable options, no rich customization was possible. + +The introduction of zenity as a dependency marked the shift from "DZTUI" to "DZGUI". + +Throughout 2022, the limits of zenity for DZGUI's purposes were tested, particularly in the context of SteamOS. +This led to the discovery of a number of bugs in zenity, GTK, and SteamOS, which were reported up to their respective channels. +While this was improving the SteamOS user experience, it started to become clear that a fully custom UI was needed. + +This barebones version of DZGUI sufficed for some time, with the Bash script and some standalone Python modules doing the heavy lifting, +and this period served to surface a number of bugs and interesting interactions with the Steam client. + +## 2023 +### 2023-11-23: Python 3.10 dependency +Prior to this time, DZGUI open-endedly allowed you to use any Python version for its modules, without any significant handling for +the idiosyncrasies of different versions. A minimum requirement of Python 3.10 was thus instituted to normalize expected behavior. + +Throughout 2023, various features related to server browsing were added, such as filters and more granular search parameters. + +There were still a lot of unknowns at this time, so things were being resolved through trial and error. + +## 2024 +### 2024-01-31: DZGUI 5 API introduces GTK UI +Using zenity as a simple GUI had exposed some bugs and limitations in that package, particularly when trying to push huge lists of server records +into tables and sorting them in a performant manner. + +Around this time it became clear that a more robust UI framework was needed. While keeping the original Bash script and Python modules for +background processing, PyGObject (GTK) was added to the project in order to ship a custom UI that could be designed from the ground up. + +However, it should be noted that at this point, the DZGUI logic itself was still a (increasingly large) Bash script to which a GTK UI had been stitched on. +These were totally separate components, with UI actions on the GTK side triggering calls to Bash functions in an overly complex helper file. +The UI was basically a thin client around the old Bash logic, but this was still a far cry from an integrated application with proper signals and dedicated components. + +Effectively, DZGUI 5 through 6 were a stepping stone that set the groundwork to go from DZGUI 4 (pure Bash script) to +DZGUI 7 (standalone Python application). Today, little of DZGUI 5's original logic remains outside of the basic UI grid. + +### 2024-10-15: Frostline DLC released, widespread parsing problems +DayZ's first DLC introduced a new format for server responses that packed extra metadata into the A2S_RULES format. +Upon its release, all third-party DayZ launchers and server listing web sites were unable to parse the mod information being returned from Frostline servers. + +For a period of two weeks, this remained unsolved and various attempts were made to understand the updated specification. + +### 2024-10-30: DZGUI hotfix adds support for Frostline servers +DZGUI was the first third-party tool to fix parsing for Frostline servers and restore functionality. +This was a small milestone, because it meant DZGUI was evolving from a "dumb" launcher to a featureful tool +touching upon and improving all aspects of the end-to-end DayZ experience. + +Particularly around this time, the sheer diversity of needs of modders, server operators, and players started to become clear. +Users had highly specific and diverse use cases for setting up their game environment, and requested a number of features to support that on +the DZGUI side. + +## 2025 +### 2025-01-17: DZGUI 6 API +With DZGUI 6, the difficult decision to enforce a Python 3.13 dependency was made. +While the previous dependency was Python 3.10+, it was deemed better to develop against and target identical versions. + +However, due to the vagaries of managing Python environments on end-user systems, this change added unnecessary complexity and frustration. +Users had to properly set up virtual environments, install the relevant dependencies, and create a wrapper script to run this version, whereas +previous versions had essentially been point and click. + +### 2025-10-23: DZGUI 7 API and the "Ship of Theseus" +It had become clear that the situation above was untenable and the legacy Bash script was unmaintainable given the project's enlarged scope. +The lack of a robust testing or typehinting framework made it difficult to diagnose rare bugs. + +Shipping a standalone, turnkey application with zero or minimal dependencies was the most urgent priority. +DZGUI was still carrying around small bits and pieces of the old Bash script logic, so the decision was made to +refactor the application into a pure Python + GTK tool. This would effectively be a total rewrite of all logic, +only preserving the most minimal skeleton of the original UI layout as a reference. + +Over the course of a year, a wholesale "Ship of Theseus" rewrite took place. +Nearly every component of DZGUI 6, both API and UI side, was taken apart and replaced. + +A soft alpha and beta phase were used to iterate on this concept further. DZGUI 7 was increasingly recommended to users +as the preferred choice. This allowed for keeping the established userbase on the (relatively) stable DZGUI 6 spec, while building up +the new spec and making it available to more users as features were elaborated. + +Through late 2025, several LLM-generated applications that recycled significant parts of DZGUI's code without the applicable GPL license were found. +The maintainers of those projects were advised to excise the offending segments or apply the necessary GPL licenses. + +## 2026 +### 2026-05-17: Switch to standalone releases +The DZGUI 7 beta starts shipping standalone glibc builds with an integrated setup wizard and installer. + +DZGUI 6 had relied on "over the air" updates delivered via either a "stable" or "testing" branch +(archived respectively under the "dzgui" and "testing" branches). Effectively, the script would bootstrap itself and +fetch raw files directly from the repository. This was prone to problems because updates were monolithic, not giving +an easy escape hatch if the user needed to rollback in the event of a regression. + +This was changed to atomic, tagged releases. In addition to updating directly through DZGUI, these prebuilt releases can be +downloaded manually as needed, and rollbacks are available for all prior versions under the DZGUI 7 specification. + +### 2026-10-27: 500 tickets closed +The milestone of 500 tickets/PRs being closed was reached. + +2026 was by far DZGUI's most intensive year by commits, perhaps suggesting the scope of the rewrite: + +- 2021: 16 +- 2022: 434 +- 2023: 322 +- 2024: 347 +- 2025: 426 +- 2026: 1300 + +DZGUI 7 is scheduled to release as a stable version within the year. diff --git a/README.md b/README.md index c3079a8..d062444 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ Users wishing to review the licenses to these components can inspect the `LICENS Steam and the Steam logo are trademarks and/or registered trademarks of Valve Corporation in the U.S. and/or other countries. +DayZ is a trademark and brand of Bohemia Interactive a.s. + ## Disclaimer DZGUI is beta-quality software and is provided as-is. diff --git a/dzgui/api/acf.py b/dzgui/api/acf.py index ce243a4..4b1dc71 100644 --- a/dzgui/api/acf.py +++ b/dzgui/api/acf.py @@ -111,7 +111,8 @@ class WorkshopACF(ACF): def __init__(self, file: Path) -> None: super().__init__(file) - def unpack(self, d: dict, lines: list[Any] = []) -> str: + def unpack(self, d: dict[str, Any], lines: list[Any] = []) -> str: + self.indent: int t1 = "AppWorkshop" t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails") for k, v in d.items(): diff --git a/dzgui/api/mods.py b/dzgui/api/mods.py index 170e7c4..234835d 100644 --- a/dzgui/api/mods.py +++ b/dzgui/api/mods.py @@ -23,14 +23,6 @@ from dzgui.init.migrate import clean_legacy_versions_file logger = logging.getLogger(APP_NAME) -@dataclass -class ModMeta: - protocol: str - published_id: str - name: str - timestamp: str - - def get_local_mod_ids(steam_path: Path) -> list[int]: workshop_path = get_local_mod_path(steam_path) mods = get_local_mods(workshop_path) @@ -113,10 +105,6 @@ def get_delimited_mods(steam_path: Path) -> list[Any]: return parse_mods(mods) -def get_missing_mods(local: list, remote: list) -> list: - return [mod for mod in remote if mod not in local] - - def _hash(uid: str, use_custom: bool = False) -> str: md5 = hashlib.md5() md5.update(uid.encode("ascii")) @@ -153,7 +141,7 @@ def remove_stale_signatures( def find_stale_mods(config: Path) -> list[int]: - def push_record(rec: str) -> list: + def push_record(rec: str) -> list[int]: record = fqip_to_record(rec) if record is None: return [] diff --git a/dzgui/api/servers.py b/dzgui/api/servers.py index 8c331e4..546ec15 100644 --- a/dzgui/api/servers.py +++ b/dzgui/api/servers.py @@ -13,6 +13,7 @@ from typing import Any, Optional, TYPE_CHECKING, Union from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT from dzgui.const.endpoints import STEAM_SERVERS +from dzgui.model.proxy_model import ServerRow from dzgui.util.format import format_exception from dzgui.util import strings @@ -111,7 +112,7 @@ def get_netmask() -> str: # TODO: rename -def test_ip(suffix: int, port: int, event: threading.Event) -> dict | None: +def test_ip(suffix: int, port: int, event: threading.Event) -> dict[str, Any] | None: if event.is_set(): return None netmask = get_netmask() @@ -139,7 +140,9 @@ def sanitize(name: str) -> str: return name -def parse_json(json: list) -> list: +def parse_json( + json: list[dict[str, Any]], +) -> list[ServerRow]: """ Server metadata is underspecified and server operators tend to insert random garbage in the headers. In case @@ -224,7 +227,7 @@ def parse_json(json: list) -> list: mapname = row["map"].lower() players = row["players"] max_players = row["max_players"] - raw = ( + raw = ServerRow( name, mapname, view, @@ -507,14 +510,14 @@ def record_to_fqip(record: Record) -> str: return f"{record.ip}:{record.gameport}:{record.qport}" -def response_to_fqip(res: dict) -> str: +def response_to_fqip(res: dict[str, Any]) -> str: ip = res["addr"].split(":")[0] gameport = res["gameport"] qport = res["addr"].split(":")[1] return f"{ip}:{gameport}:{qport}" -def response_to_record(res: dict) -> Record: +def response_to_record(res: dict[str, Any]) -> Record: ip = res["addr"].split(":")[0] gameport = res["gameport"] qport = res["addr"].split(":")[1] diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 4a7d1e8..23357f2 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -31,10 +31,10 @@ from dzgui.const.constants import ( VDF_PATH, ) from dzgui.const.endpoints import ( - APP_DETAILS, - SUB_ENDPOINT, + STEAM_APP_DETAILS, STEAM_PUBLISHED_FILES, - UNSUB_ENDPOINT, + STEAM_SUB, + STEAM_UNSUB, ) from dzgui.strings import wizard from dzgui.strings.errors import api_validation_error, workshop_endpoint_error @@ -310,11 +310,11 @@ def update_workshop(key: str, mod: int, endpoint: str) -> None: def subscribe(key: str, mod: int) -> None: - update_workshop(key, mod, SUB_ENDPOINT) + update_workshop(key, mod, STEAM_SUB) def unsubscribe(key: str, mod: int) -> None: - update_workshop(key, mod, UNSUB_ENDPOINT) + update_workshop(key, mod, STEAM_UNSUB) def validate_mod(client: str, mod: str) -> None: @@ -512,7 +512,7 @@ def get_app_path(folders_path: Path, appid: int) -> Path: def get_app_name(appid: int) -> str | None: payload = {"appids": [appid]} - res = requests.get(APP_DETAILS, params=payload) + res = requests.get(STEAM_APP_DETAILS, params=payload) if res.status_code != 200: return None try: diff --git a/dzgui/app_init.py b/dzgui/app_init.py index b112450..96ff4d1 100644 --- a/dzgui/app_init.py +++ b/dzgui/app_init.py @@ -22,7 +22,6 @@ from dzgui.init.migrate import ( has_new_config, migrate_cols_file, ) -from dzgui.init.flock import lock_acquire from dzgui.init.prereqs import has_flatpak_client, has_steam_client from dzgui.strings import boot @@ -122,8 +121,6 @@ def load_steam_icon() -> None: def build_gui(version: str, is_debug: bool) -> App | None: - lock = lock_acquire() # noqa - set_locale() # NOTE: consider aborting this check if steam deck xdg_paths = get_xdg_paths() @@ -158,9 +155,6 @@ def build_gui(version: str, is_debug: bool) -> App | None: f.truncate(0) if _is_steam_deck is False: - # TODO: sudo escalation dialog - # count = get_map_count() - # TODO: move into module if FLATPAK_ROOT_PATH in lookup(XDG.config, Preferences.DEFAULT): has_client = has_flatpak_client() else: diff --git a/dzgui/config/query.py b/dzgui/config/query.py index 1af14a5..e9d8a1f 100644 --- a/dzgui/config/query.py +++ b/dzgui/config/query.py @@ -5,6 +5,7 @@ from dzgui.util._json import read_json from dzgui.const.enum import Preferences + class ConfigFileError(Exception): pass @@ -39,4 +40,4 @@ def get_favorites(path: Path) -> list[str]: def enum_to_key(enum: Preferences) -> str: - return str(enum.dict["key"]) + return str(enum.key) diff --git a/dzgui/config/xdg.py b/dzgui/config/xdg.py index b72ea7b..532c73f 100644 --- a/dzgui/config/xdg.py +++ b/dzgui/config/xdg.py @@ -51,7 +51,7 @@ def is_writeable(path_str: str) -> bool: return True -def get_xdg_paths() -> dict: +def get_xdg_paths() -> dict[str, Path]: home = Path.home() xdg_paths = { "XDG_CONFIG_HOME": home / ".config", @@ -73,7 +73,7 @@ def get_xdg_paths() -> dict: return resolved_paths -def parse_filepaths(xdg: dict) -> Xdg: +def parse_filepaths(xdg: dict[str, Path]) -> Xdg: config = xdg["XDG_CONFIG_HOME"] state = xdg["XDG_STATE_HOME"] share = xdg["XDG_DATA_HOME"] diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index aba522f..daed17e 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -37,37 +37,8 @@ HEX_GREEN = "#32CD32" HEX_RED = "#FF0000" HEX_ORANGE = "#FFAC1C" -# TODO: normalize icon names -CALCULATOR = "accessories-calculator-symbolic" -CARET_DOWN = "go-down-symbolic" -CARET_UP = "go-up-symbolic" -CLIPBOARD = "edit-copy-symbolic" -CLOSE = "window-close-symbolic" -FOLDER = "folder-symbolic" -EDIT_DELETE = "edit-delete-symbolic" -ERROR = "dialog-error-symbolic" -HELP_BUBBLE = "help-about-symbolic" -INPUT_KEYBOARD = "input-keyboard-symbolic" -LIST_ADD = "list-add-symbolic" -REFRESH_ICON = "view-refresh-symbolic" -SEARCH_ICON = "system-search-symbolic" -STEAM_ICON = "steam-tray-symbolic" -VIEW_CONCEAL = "view-conceal-symbolic" -VIEW_REVEAL = "view-reveal-symbolic" -WARNING = "dialog-warning-symbolic" -WEB_BROWSER = "web-browser-symbolic" -DAY_ICON = "weather-clear-symbolic" -NIGHT_ICON = "weather-clear-night-symbolic" - SEPARATOR = "SEPARATOR" -# TODO: drop -NO_PADDING = 0 -NO_EXPAND = False -NO_FILL = False -EXPAND = True -FILL = True - SCROLL_INCREMENT = 50 WINDOW_DEFAULT_X = 1400 WINDOW_DEFAULT_Y = 800 diff --git a/dzgui/const/endpoints.py b/dzgui/const/endpoints.py index 7616ff0..ed6239d 100644 --- a/dzgui/const/endpoints.py +++ b/dzgui/const/endpoints.py @@ -3,9 +3,9 @@ STEAM_PUBLISHED_FILES = ( "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1" ) STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1" -SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1" -UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1" -APP_DETAILS = "https://store.steampowered.com/api/appdetails?" +STEAM_SUB = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1" +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" diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index b1728f9..d7c8911 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -1,7 +1,9 @@ +from __future__ import annotations +from dataclasses import dataclass from enum import Enum -from typing import Any, Self from dzgui.util import strings +from dzgui.strings import rows class Port(Enum): @@ -46,125 +48,61 @@ class FilterMode(Enum): TOGGLE_ON = 3 -class EnumWithAttrs(Enum): - def __new__(cls, *args: Any, **kwargs: Any) -> Self: - value = len(cls.__members__) + 1 - obj = object.__new__(cls) - obj._value_ = value - return obj - - def __init__(self, d: dict): - self.dict = d +@dataclass(slots=True, frozen=True) +class ModButtonMeta: + label: str + tooltip: str -class Preferences(EnumWithAttrs): - STEAM = { - "key": "steam_api", - } - CLIENT = { - "key": "client", - } - WINDOW = { - "key": "fullscreen", - } - DIST = { - "key": "use_miles", - } - NAME = { - "key": "name", - } - DEFAULT = { - "key": "default_steam_path", - } - FAV_LBL = { - "key": "fav_label", - } - FAV_SRV = { - "key": "fav_server", - } - IP_LIST = { - "key": "ip_list", - } - START_TAB = { - "key": "start_tab", - } +@dataclass(slots=True, frozen=True) +class ButtonTypeMeta: + label: str + tooltip: str + opens: NotebookPage | None -class NotebookPage(EnumWithAttrs): - OFFLINE = {"crumbs": strings.crumbs.offline, "statusbar": True} - CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": True} - DEVELOPERS = {"crumbs": strings.crumbs.developers, "statusbar": False} - HELP = {"crumbs": strings.crumbs._help, "statusbar": True} - KEYS = {"crumbs": strings.crumbs.keys, "statusbar": True} - LOG = {"crumbs": strings.crumbs.log, "statusbar": True} - MODS = {"crumbs": strings.crumbs.mods, "statusbar": True} - OPTIONS = {"crumbs": strings.crumbs.options, "statusbar": False} - SERVERS = {"crumbs": strings.crumbs.servers, "statusbar": True} - THANKS = {"crumbs": strings.crumbs.thanks, "statusbar": True} - CONNECTION = {"crumbs": "Connect", "statusbar": True} +@dataclass(slots=True, frozen=True) +class ConfigKey: + key: str -class RowType(EnumWithAttrs): - # TODO: strings - CHANGELOG = { - "label": "View changelog", - "tooltip": "Opens the DZGUI changelog", - } - SHOW_LOG = { - "label": "Show debug log", - "tooltip": "Read the DZGUI log generated since startup", - } - SEPARATOR = { - "label": "SEPARATOR", - "tooltip": "", - } - DUMP_LOG = { - "label": "Output system info to log file", - "tooltip": "Dump diagnostic data for troubleshooting", - } - DOCS = { - "label": "Documentation/help files (GitHub) ⧉", - "tooltip": "Open the DZGUI documentation in a browser", - } - DOCS_FALLBACK = { - "label": "Documentation/help files (Codeberg mirror) ⧉", - "tooltip": "Open the DZGUI documentation in a browser", - } - BUGS = { - "label": "Report a bug (GitHub) ⧉", - "tooltip": "Open the DZGUI issue tracker in a browser", - } - FORUM = { - "label": "DZGUI Subreddit ⧉", - "tooltip": "Open the DZGUI discussion forum in a browser", - } - SPONSOR = { - "label": "Sponsor (GitHub) ⧉", - "tooltip": "Sponsor development of the DZGUI project", - } - THANKS = { - "label": "Special thanks", - "tooltip": "A list of contributors, testers, and sponsors", - } +@dataclass(slots=True, frozen=True) +class NotebookMeta: + crumbs: str + has_statusbar: bool -# TODO: rename to ContextMenuItem -class ContextMenu(EnumWithAttrs): - ADD_NOTE = {"label": strings.add_note} - ADD_SERVER = {"label": strings.add} - CONNECT = {"label": strings.connect} - COPY_LOG_CLIPBOARD = {"label": strings.copy_log} - COPY_SERVER_IP = {"label": strings.copy_ip} - COPY_SERVER_NAME = {"label": strings.copy_name} - UNSUB_MOD = {"label": strings.unsub_mod} - VALIDATE_MOD = {"label": strings.validate_mod} - OPEN_WORKSHOP = {"label": strings.open_workshop} - REFRESH_PLAYERS = {"label": strings.refresh_players} - REMOVE_HISTORY = {"label": strings.remove_history} - REMOVE_SERVER = {"label": strings.remove} - SET_FAV = {"label": strings.add_fav} - SHOW_DETAILS = {"label": strings.show_details} - SHOW_MODS = {"label": strings.show_mods} +@dataclass(slots=True, frozen=True) +class RowMeta: + label: str + tooltip: str + + +@dataclass(slots=True, frozen=True) +class ContextMenuMeta: + label: str + + +class ContextMenuItem(Enum): + ADD_NOTE = ContextMenuMeta(strings.add_note) + ADD_SERVER = ContextMenuMeta(strings.add) + CONNECT = ContextMenuMeta(strings.connect) + COPY_LOG_CLIPBOARD = ContextMenuMeta(strings.copy_log) + COPY_SERVER_IP = ContextMenuMeta(strings.copy_ip) + COPY_SERVER_NAME = ContextMenuMeta(strings.copy_name) + UNSUB_MOD = ContextMenuMeta(strings.unsub_mod) + VALIDATE_MOD = ContextMenuMeta(strings.validate_mod) + OPEN_WORKSHOP = ContextMenuMeta(strings.open_workshop) + REFRESH_PLAYERS = ContextMenuMeta(strings.refresh_players) + REMOVE_HISTORY = ContextMenuMeta(strings.remove_history) + REMOVE_SERVER = ContextMenuMeta(strings.remove) + SET_FAV = ContextMenuMeta(strings.add_fav) + SHOW_DETAILS = ContextMenuMeta(strings.show_details) + SHOW_MODS = ContextMenuMeta(strings.show_mods) + + @property + def label(self) -> str: + return self.value.label class ContextMenuGroup(Enum): @@ -172,113 +110,193 @@ class ContextMenuGroup(Enum): Groupings of context menu items """ - SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,) - MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD, ContextMenu.VALIDATE_MOD) + SERVER_MOD = (ContextMenuItem.OPEN_WORKSHOP,) + MOD = ( + ContextMenuItem.OPEN_WORKSHOP, + ContextMenuItem.UNSUB_MOD, + ContextMenuItem.VALIDATE_MOD, + ) MOD_OFFLINE = (None,) - LOG = (ContextMenu.COPY_LOG_CLIPBOARD,) + LOG = (ContextMenuItem.COPY_LOG_CLIPBOARD,) SERVER_BROWSER = ( - ContextMenu.CONNECT, - ContextMenu.ADD_SERVER, - ContextMenu.SET_FAV, - ContextMenu.COPY_SERVER_NAME, - ContextMenu.COPY_SERVER_IP, - ContextMenu.ADD_NOTE, - ContextMenu.SHOW_MODS, - ContextMenu.SHOW_DETAILS, - ContextMenu.REFRESH_PLAYERS, + ContextMenuItem.CONNECT, + ContextMenuItem.ADD_SERVER, + ContextMenuItem.SET_FAV, + ContextMenuItem.COPY_SERVER_NAME, + ContextMenuItem.COPY_SERVER_IP, + ContextMenuItem.ADD_NOTE, + ContextMenuItem.SHOW_MODS, + ContextMenuItem.SHOW_DETAILS, + ContextMenuItem.REFRESH_PLAYERS, ) SCAN_LAN = ( - ContextMenu.CONNECT, - ContextMenu.SET_FAV, - ContextMenu.COPY_SERVER_NAME, - ContextMenu.COPY_SERVER_IP, - ContextMenu.ADD_NOTE, - ContextMenu.SHOW_MODS, - ContextMenu.SHOW_DETAILS, - ContextMenu.REFRESH_PLAYERS, + ContextMenuItem.CONNECT, + ContextMenuItem.SET_FAV, + ContextMenuItem.COPY_SERVER_NAME, + ContextMenuItem.COPY_SERVER_IP, + ContextMenuItem.ADD_NOTE, + ContextMenuItem.SHOW_MODS, + ContextMenuItem.SHOW_DETAILS, + ContextMenuItem.REFRESH_PLAYERS, ) SAVED = ( - ContextMenu.CONNECT, - ContextMenu.SET_FAV, - ContextMenu.REMOVE_SERVER, - ContextMenu.COPY_SERVER_NAME, - ContextMenu.COPY_SERVER_IP, - ContextMenu.ADD_NOTE, - ContextMenu.SHOW_MODS, - ContextMenu.SHOW_DETAILS, - ContextMenu.REFRESH_PLAYERS, + ContextMenuItem.CONNECT, + ContextMenuItem.SET_FAV, + ContextMenuItem.REMOVE_SERVER, + ContextMenuItem.COPY_SERVER_NAME, + ContextMenuItem.COPY_SERVER_IP, + ContextMenuItem.ADD_NOTE, + ContextMenuItem.SHOW_MODS, + ContextMenuItem.SHOW_DETAILS, + ContextMenuItem.REFRESH_PLAYERS, ) RECENT = ( - ContextMenu.CONNECT, - ContextMenu.ADD_SERVER, - ContextMenu.SET_FAV, - ContextMenu.REMOVE_HISTORY, - ContextMenu.COPY_SERVER_NAME, - ContextMenu.COPY_SERVER_IP, - ContextMenu.ADD_NOTE, - ContextMenu.SHOW_MODS, - ContextMenu.SHOW_DETAILS, - ContextMenu.REFRESH_PLAYERS, + ContextMenuItem.CONNECT, + ContextMenuItem.ADD_SERVER, + ContextMenuItem.SET_FAV, + ContextMenuItem.REMOVE_HISTORY, + ContextMenuItem.COPY_SERVER_NAME, + ContextMenuItem.COPY_SERVER_IP, + ContextMenuItem.ADD_NOTE, + ContextMenuItem.SHOW_MODS, + ContextMenuItem.SHOW_DETAILS, + ContextMenuItem.REFRESH_PLAYERS, ) -class ModButton(EnumWithAttrs): - SELECT_ALL = { - "label": strings.mod_panel.select_all, - "tooltip": strings.mod_panel.select_all_tooltip, - } - UNSELECT_ALL = { - "label": strings.mod_panel.unselect_all, - "tooltip": strings.mod_panel.unselect_all_tooltip, - } - HIGHLIGHT_STALE = { - "label": strings.mod_panel.highlight_stale, - "tooltip": strings.mod_panel.highlight_stale_tooltip, - } - UNHIGHLIGHT_STALE = { - "label": strings.mod_panel.unhighlight_stale, - "tooltip": strings.mod_panel.unhighlight_stale_tooltip, - } - UNSUB_SELECTED = { - "label": strings.mod_panel.unsub_selected, - "tooltip": strings.mod_panel.unsub_selected_tooltip, - } - SELECT_STALE = { - "label": strings.mod_panel.select_stale, - "tooltip": strings.mod_panel.select_stale_tooltip, - } - VALIDATE = { - "label": strings.mod_panel.validate, - "tooltip": strings.mod_panel.validate_tooltip, - } +class Preferences(Enum): + STEAM = ConfigKey("steam_api") + CLIENT = ConfigKey("client") + WINDOW = ConfigKey("fullscreen") + DIST = ConfigKey("use_miles") + NAME = ConfigKey("name") + DEFAULT = ConfigKey("default_steam_path") + FAV_LBL = ConfigKey("fav_label") + FAV_SRV = ConfigKey("fav_server") + IP_LIST = ConfigKey("ip_list") + START_TAB = ConfigKey("start_tab") + + @property + def key(self) -> str: + return self.value.key -class ButtonType(EnumWithAttrs): - SERVERS = { - "label": strings.buttons.servers_label, - "tooltip": strings.buttons.servers_tooltip, - "opens": NotebookPage.SERVERS, - } - MODS = { - "label": strings.buttons.mods_label, - "tooltip": strings.buttons.mods_tooltip, - "opens": NotebookPage.MODS, - } - OPTIONS = { - "label": strings.buttons.options_label, - "tooltip": strings.buttons.options_tooltip, - "opens": NotebookPage.OPTIONS, - } - HELP = { - "label": strings.buttons.help_label, - "tooltip": strings.buttons.help_tooltip, - "opens": NotebookPage.HELP, - } - EXIT = { - "label": strings.buttons.exit_label, - "tooltip": strings.buttons.exit_tooltip, - "opens": None, - } +class NotebookPage(Enum): + OFFLINE = NotebookMeta(strings.crumbs.offline, True) + CHANGELOG = NotebookMeta(strings.crumbs.changelog, True) + DEVELOPERS = NotebookMeta(strings.crumbs.developers, False) + HELP = NotebookMeta(strings.crumbs._help, True) + KEYS = NotebookMeta(strings.crumbs.keys, True) + LOG = NotebookMeta(strings.crumbs.log, True) + MODS = NotebookMeta(strings.crumbs.mods, True) + OPTIONS = NotebookMeta(strings.crumbs.options, False) + SERVERS = NotebookMeta(strings.crumbs.servers, True) + THANKS = NotebookMeta(strings.crumbs.thanks, True) + CONNECTION = NotebookMeta(strings.crumbs.connect, True) + + @property + def crumbs(self) -> str: + return self.value.crumbs + + @property + def has_statusbar(self) -> bool: + return self.value.has_statusbar + + +class RowType(Enum): + CHANGELOG = RowMeta(rows.changelog_label, rows.changelog_status) + SHOW_LOG = RowMeta(rows.log_label, rows.log_status) + SEPARATOR = RowMeta(rows.separator_label, rows.separator_status) + DUMP_LOG = RowMeta(rows.system_log_label, rows.system_log_status) + DOCS = RowMeta(rows.docs_label, rows.docs_status) + DOCS_FALLBACK = RowMeta(rows.docs_fallback_label, rows.docs_fallback_status) + BUGS = RowMeta(rows.bugs_label, rows.bugs_status) + FORUM = RowMeta(rows.forum_label, rows.forum_status) + SPONSOR = RowMeta(rows.sponsor_label, rows.sponsor_status) + THANKS = RowMeta(rows.thanks_label, rows.thanks_status) + + @property + def label(self) -> str: + return self.value.label + + @property + def tooltip(self) -> str: + return self.value.tooltip + + +class ModButton(Enum): + SELECT_ALL = ModButtonMeta( + strings.mod_panel.select_all, strings.mod_panel.select_all_tooltip + ) + UNSELECT_ALL = ModButtonMeta( + strings.mod_panel.unselect_all, strings.mod_panel.unselect_all_tooltip + ) + HIGHLIGHT_STALE = ModButtonMeta( + strings.mod_panel.highlight_stale, strings.mod_panel.highlight_stale_tooltip + ) + UNHIGHLIGHT_STALE = ModButtonMeta( + strings.mod_panel.unhighlight_stale, strings.mod_panel.unhighlight_stale_tooltip + ) + UNSUB_SELECTED = ModButtonMeta( + strings.mod_panel.unsub_selected, + strings.mod_panel.unsub_selected_tooltip, + ) + SELECT_STALE = ModButtonMeta( + strings.mod_panel.select_stale, + strings.mod_panel.select_stale_tooltip, + ) + VALIDATE = ModButtonMeta( + strings.mod_panel.validate, + strings.mod_panel.validate_tooltip, + ) + + @property + def label(self) -> str: + return self.value.label + + @property + def tooltip(self) -> str: + return self.value.tooltip + + +class ButtonType(Enum): + SERVERS = ButtonTypeMeta( + strings.buttons.servers_label, + strings.buttons.servers_tooltip, + NotebookPage.SERVERS, + ) + MODS = ButtonTypeMeta( + strings.buttons.mods_label, + strings.buttons.mods_tooltip, + NotebookPage.MODS, + ) + OPTIONS = ButtonTypeMeta( + strings.buttons.options_label, + strings.buttons.options_tooltip, + NotebookPage.OPTIONS, + ) + HELP = ButtonTypeMeta( + strings.buttons.help_label, + strings.buttons.help_tooltip, + NotebookPage.HELP, + ) + EXIT = ButtonTypeMeta( + strings.buttons.exit_label, + strings.buttons.exit_tooltip, + None, + ) + + @property + def label(self) -> str: + return self.value.label + + @property + def tooltip(self) -> str: + return self.value.tooltip + + @property + def opens(self) -> NotebookPage | None: + return self.value.opens HELP_MENU_ROWS = ( diff --git a/dzgui/const/icons.py b/dzgui/const/icons.py new file mode 100644 index 0000000..176e73e --- /dev/null +++ b/dzgui/const/icons.py @@ -0,0 +1,20 @@ +ICON_CALCULATOR = "accessories-calculator-symbolic" +ICON_CARET_DOWN = "go-down-symbolic" +ICON_CARET_UP = "go-up-symbolic" +ICON_CLIPBOARD = "edit-copy-symbolic" +ICON_CLOSE = "window-close-symbolic" +ICON_DAY = "weather-clear-symbolic" +ICON_EDIT_DELETE = "edit-delete-symbolic" +ICON_ERROR = "dialog-error-symbolic" +ICON_FOLDER = "folder-symbolic" +ICON_HELP_BUBBLE = "help-about-symbolic" +ICON_INPUT_KEYBOARD = "input-keyboard-symbolic" +ICON_LIST_ADD = "list-add-symbolic" +ICON_NIGHT = "weather-clear-night-symbolic" +ICON_REFRESH = "view-refresh-symbolic" +ICON_SEARCH = "system-search-symbolic" +ICON_STEAM = "steam-tray-symbolic" +ICON_VIEW_CONCEAL = "view-conceal-symbolic" +ICON_VIEW_REVEAL = "view-reveal-symbolic" +ICON_WARNING = "dialog-warning-symbolic" +ICON_WEB_BROWSER = "web-browser-symbolic" diff --git a/dzgui/controllers/emitter.py b/dzgui/controllers/emitter.py index 82b0e91..e21ac18 100644 --- a/dzgui/controllers/emitter.py +++ b/dzgui/controllers/emitter.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING import gi gi.require_version("Gtk", "3.0") -from gi.repository.Gtk import ListStore # noqa E402 -from gi.repository import Gdk, GObject # noqa E402 +from gi.repository.Gtk import ListStore +from gi.repository import GObject if TYPE_CHECKING: from dzgui.const.enum import NotebookPage, ServerTab diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 1a05955..58de7d2 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -8,7 +8,7 @@ import dzgui.util._json as JSON # noqa from dzgui.const.enum import ( ButtonType, - ContextMenu, + ContextMenuItem, FilterMode, NotebookPage, Preferences, @@ -32,7 +32,7 @@ from dzgui.views.dialogs.generic import ExceptionDialog import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402 +from gi.repository import Gtk, Gdk, GLib, GObject logger = logging.getLogger(APP_NAME) @@ -77,7 +77,7 @@ class AppNavigation: class Controller(GObject.GObject): def __init__(self) -> None: - self.dist_cache: dict[str, "Haversine"] = {} + self.dist_cache: dict[str, "Haversine" | None] = {} self.mediator = AppNavigation() self.prefs: UserPrefs @@ -196,7 +196,9 @@ class Controller(GObject.GObject): def show_developers_page(self) -> None: self.open_page(NotebookPage.DEVELOPERS) - def open_page(self, page: NotebookPage) -> None: + def open_page(self, page: NotebookPage | None) -> None: + if page is None: + return self.mediator.grid.notebook.set_page_by_enum(page) def open_page_by_button(self, button: "ContextualButton") -> None: @@ -262,7 +264,7 @@ class Controller(GObject.GObject): logger.critical(e) return - def menu_action(self, action: ContextMenu, tree: "ServerTreeView") -> None: + def menu_action(self, action: ContextMenuItem, tree: "ServerTreeView") -> None: context_man = ContextMenuManager(tree, self) context_man.process(action) @@ -274,7 +276,7 @@ class Controller(GObject.GObject): except Exception as e: dialog = ExceptionDialog(self, str(e)) dialog.show_all() - dialog.run() + dialog.run() # type: ignore[no-untyped-call] def select_colorized(self) -> None: mod_man = self.mediator.modtreeview.get_mod_man() @@ -308,7 +310,7 @@ class Controller(GObject.GObject): except Exception as e: dialog = ExceptionDialog(self, str(e)) dialog.show_all() - dialog.run() + dialog.run() # type: ignore[no-untyped-call] def update_steam_api_key(self, text: str) -> None: self.config_man.update_steam_api_key(text) @@ -355,22 +357,21 @@ class Controller(GObject.GObject): filter_man.set_filter(filt, state) self.mediator.filters.set_filters(filters) - def get_dist_cache(self) -> dict[str, "Haversine"]: + def get_dist_cache(self) -> dict[str, Union["Haversine", None]]: return self.dist_cache - def set_dist_cache(self, addr: str, haversine: "Haversine") -> None: + def set_dist_cache(self, addr: str, haversine: Union["Haversine", None]) -> None: self.dist_cache[addr] = haversine def get_map_store(self) -> Gtk.ListStore: filter_man = self.get_filter_man() return filter_man.get_map_store() - def get_enabled_filters(self) -> dict: + def get_enabled_filters(self) -> dict[str, bool]: 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/main.py b/dzgui/main.py index 5e660cf..a32d021 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -43,6 +43,9 @@ def main() -> None: # NOTE: only import ligbirepository modules after the above check from dzgui.app_init import build_gui + from dzgui.init.flock import lock_acquire + + lock = lock_acquire() # noqa app = build_gui(version, args.debug) if app is None: diff --git a/dzgui/managers/config.py b/dzgui/managers/config.py index 7490b15..e02eaa2b 100644 --- a/dzgui/managers/config.py +++ b/dzgui/managers/config.py @@ -46,6 +46,15 @@ class ConfigManager: ips.append(record) self.update_config(Preferences.IP_LIST, ips) + def remove_from_history_file(self, fqip: str) -> None: + try: + history = self.prefs.paths.history + ips = [line.rstrip() for line in history.read_text().splitlines()] + ips.remove(fqip) + history.write_text("\n".join(ips)) + except Exception as e: + logger.critical(e) + def update_history_file(self, fqip: str) -> None: try: with open(self.prefs.paths.history, "r") as f: @@ -124,7 +133,7 @@ class ConfigManager: return False def enum_to_key(self, enum: Preferences) -> str: - return str(enum.dict["key"]) + return str(enum.key) def toggle_config(self, key: Preferences) -> None: try: @@ -162,7 +171,7 @@ class ConfigManager: trace = traceback.format_exc() dialog = ExceptionDialog(self.controller, trace) dialog.show_all() - dialog.run() + dialog.run() # type: ignore[no-untyped-call] def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None: columns = tv.get_columns() diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 89c35f0..a57a421 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -48,18 +48,13 @@ from dzgui.strings.dialogs import ( waiting_for_directories, ) from dzgui.strings import kb -from dzgui.strings.server_mods import checkmark, resync +from dzgui.strings.server_mods import checkmark, resync, invalid_mod from dzgui.util.format import format_exception, format_mib from dzgui.util.strings import dialog, server_timeout from dzgui.util.symlink import rebuild_symlinks from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.servers import ServerDetailsDialog, ServerModDialog -import gi - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib # noqa E402 - if TYPE_CHECKING: from dzgui.api.servers import A2SInfo, Record from dzgui.controllers.mc import Controller @@ -197,9 +192,8 @@ class ConnectionManager: is_last = self.is_last_server() - # TODO: strings invalid_mods = [ - (mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod" + (mod[0], mod[1]) for mod in remote_mods if mod[2] == invalid_mod ] prereqs = Prerequisites( @@ -277,8 +271,7 @@ class ConnectionManager: for mod in alpha_mods: # NOTE: if the mod is neither synched or out of date, it is a malformed mod if mod[2] == "": - # TODO: strings - mod[2] = "Invalid mod" + mod[2] = invalid_mod return alpha_mods, missing_mods @@ -298,22 +291,22 @@ class ConnectionManager: def _present_modlist_dialog(self, mods: list[list[str]]) -> None: dialog = ServerModDialog(self.controller, mods) - dialog.run() + dialog.run() # type: ignore[no-untyped-call] def _present_details_dialog(self, details: Servers.Details) -> None: dialog = ServerDetailsDialog(self.controller, details) - dialog.run() + dialog.run() # type: ignore[no-untyped-call] def _workshop_timeout(self, msg: str) -> None: dialog = ExceptionDialog(self.controller, msg) dialog.show_all() - dialog.run() + dialog.run() # type: ignore[no-untyped-call] def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) dialog.set_details_buffer(kb.DZG_006) dialog.show_all() - dialog.run() + dialog.run() # type: ignore[no-untyped-call] def get_debug_args(self) -> str: addr = f"{self.record.ip}:{self.record.gameport}" diff --git a/dzgui/managers/contextmenu.py b/dzgui/managers/contextmenu.py index 016ce7b..ded03ec 100644 --- a/dzgui/managers/contextmenu.py +++ b/dzgui/managers/contextmenu.py @@ -2,7 +2,7 @@ import logging from typing import TYPE_CHECKING from dzgui.const.constants import APP_NAME -from dzgui.const.enum import ContextMenu, Preferences +from dzgui.const.enum import ContextMenuItem, Preferences from dzgui.util.clip import copy_clipboard from dzgui.util.open_links import open_workshop_page @@ -34,7 +34,7 @@ class ContextMenuManager: self.controller = controller self.treeview = treeview - def process(self, action: ContextMenu) -> None: + def process(self, action: ContextMenuItem) -> None: if isinstance(self.treeview, ServerTreeView): record = self.treeview.get_record() if record is None: @@ -42,14 +42,14 @@ class ContextMenuManager: match action: # UNTHREADED - case ContextMenu.ADD_NOTE: + case ContextMenuItem.ADD_NOTE: dialog = NoteDialog(self.controller) - dialog.run() - case ContextMenu.COPY_SERVER_IP: + dialog.run() # type: ignore[no-untyped-call] + case ContextMenuItem.COPY_SERVER_IP: self.copy_server_ip() - case ContextMenu.COPY_SERVER_NAME: + case ContextMenuItem.COPY_SERVER_NAME: self.copy_server_name() - case ContextMenu.SET_FAV: + case ContextMenuItem.SET_FAV: name = self.treeview.get_value_at_index(0) # NOTE: fully qualified ip fqip = self.treeview.get_record_string() @@ -58,40 +58,40 @@ class ContextMenuManager: self.controller.set_fav(name, fqip, simple) # THREADED - case ContextMenu.ADD_SERVER: + case ContextMenuItem.ADD_SERVER: self.controller.add_by_record(record) - case ContextMenu.CONNECT: + case ContextMenuItem.CONNECT: self.controller.connect_by_record(record) - case ContextMenu.REFRESH_PLAYERS: + case ContextMenuItem.REFRESH_PLAYERS: self.controller.refresh_players(record) - case ContextMenu.REMOVE_HISTORY: + case ContextMenuItem.REMOVE_HISTORY: self.controller.remove_from_history(record) - case ContextMenu.REMOVE_SERVER: + case ContextMenuItem.REMOVE_SERVER: self.controller.remove_by_record(record) - case ContextMenu.SHOW_DETAILS: + case ContextMenuItem.SHOW_DETAILS: self.controller.get_details(record) - case ContextMenu.SHOW_MODS: + case ContextMenuItem.SHOW_MODS: self.controller.get_modlist(record) if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)): match action: - case ContextMenu.OPEN_WORKSHOP: + case ContextMenuItem.OPEN_WORKSHOP: self.open_mod_page() - case ContextMenu.UNSUB_MOD: + case ContextMenuItem.UNSUB_MOD: self.controller.unsub_mods(self.treeview) - case ContextMenu.VALIDATE_MOD: + case ContextMenuItem.VALIDATE_MOD: self.controller.validate_mods(self.treeview) case _: pass if isinstance(self.treeview, LogTreeView): match action: - case ContextMenu.COPY_LOG_CLIPBOARD: + case ContextMenuItem.COPY_LOG_CLIPBOARD: self.copy_log() if isinstance(self.treeview, ServerModTreeView): match action: - case ContextMenu.OPEN_WORKSHOP: + case ContextMenuItem.OPEN_WORKSHOP: self.open_mod_page() def copy_server_ip(self) -> None: diff --git a/dzgui/managers/filter.py b/dzgui/managers/filter.py index 4151bc5..38aaa7d 100644 --- a/dzgui/managers/filter.py +++ b/dzgui/managers/filter.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING from dzgui.model.model_factory import ModelFactory from dzgui.util import strings -from dzgui.util.strings import all_maps +from dzgui.strings.filters import all_maps if TYPE_CHECKING: from dzgui.model.model_factory import FastInsertListStore @@ -51,7 +51,6 @@ class FilterManager: self.active_map = (0, all_maps) self.prior_map = all_maps - self.filters: list self.enabled_filters = dict(self.default_filters) def get_tooltips(self) -> dict[str, str]: @@ -75,11 +74,11 @@ class FilterManager: def set_active_keyword(self, word: str) -> None: self.active_keyword = word - def get_default_filters(self) -> dict: + def get_default_filters(self) -> dict[str, bool]: """Deep copy of defaults""" return dict(self.default_filters) - def get_filters(self) -> dict: + def get_filters(self) -> dict[str, bool]: return self.enabled_filters def set_filter(self, label: str, state: bool) -> None: @@ -106,17 +105,11 @@ class FilterManager: self.reinit_map_store() maps.sort() - for m in maps: - # TODO: strings - if m == "All maps": - continue + clean = [m for m in maps if m != all_maps] + for m in clean: self.append_map([m]) - # TODO: currently unused - def get_unique_maps(self) -> list[str]: - return [row[0] for row in self.map_store if row[0] != "All maps"] - - def get_all_filters(self) -> tuple: + def get_all_filters(self) -> tuple[str, ...]: map_name = self.get_active_map_name() enabled = self.get_filters() kw = self.get_active_keyword() diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 6a98e02..3bb2ace 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -30,7 +30,7 @@ import dzgui.api.pefile as PeFile import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -221,7 +221,7 @@ class ModManager: total_size += mod[3] return total_mods, total_size - def _on_stale_mods_found(self, stale_mods: list) -> None: + def _on_stale_mods_found(self, stale_mods: list[int]) -> None: """Manipulates attached ListStore in the main event loop""" model = self.treeview.get_model() if model is None: diff --git a/dzgui/managers/offline.py b/dzgui/managers/offline.py index 645a682..d16acaa 100644 --- a/dzgui/managers/offline.py +++ b/dzgui/managers/offline.py @@ -52,7 +52,7 @@ class OfflineManager: is_valid = is_mission(folder) self.emitter.emit("custom_mission_loaded", str(folder), is_valid) - def find_custom_mods(self, callback: Callable) -> None: + def find_custom_mods(self, callback: Callable[[], None]) -> None: folder = self.open_folderpicker(dialogs.custom_mod_dialog) window = self.controller.get_window() window.set_sensitive(False) diff --git a/dzgui/managers/threading.py b/dzgui/managers/threading.py index c20f324..0344103 100644 --- a/dzgui/managers/threading.py +++ b/dzgui/managers/threading.py @@ -15,15 +15,15 @@ if TYPE_CHECKING: import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib # noqa E402 +from gi.repository import GLib logger = logging.getLogger(APP_NAME) def call_on_thread( dialog_str: str, show_dialog: bool = True, show_cancel: bool = False -) -> Callable: - def decorator(func: Callable) -> Callable: +) -> Callable[..., Any]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> None: self = args[0] @@ -42,7 +42,7 @@ def call_on_thread( class StoredFunc: - def __init__(self, func: Callable, *args: Any, **kwargs: Any) -> None: + def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None: sig = inspect.signature(func) self.func = func self.bindings = sig.bind(*args, **kwargs) diff --git a/dzgui/managers/update.py b/dzgui/managers/update.py index ff64fb4..f08b279 100644 --- a/dzgui/managers/update.py +++ b/dzgui/managers/update.py @@ -15,11 +15,6 @@ from dzgui.strings import dialogs from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog -import gi - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib # noqa E402 - if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -60,9 +55,9 @@ class UpdateManager: def _on_update_success(self) -> None: msg = dialogs.update_success dialog = QuitDialog(self.controller, msg) - dialog.run() + dialog.run() # type: ignore[no-untyped-call] def _on_update_failure(self, msg: str) -> None: dialog = ExceptionDialog(self.controller, msg) dialog.show_all() - dialog.run() + dialog.run() # type: ignore[no-untyped-call] diff --git a/dzgui/model/model_factory.py b/dzgui/model/model_factory.py index 7dfffbf..a929245 100644 --- a/dzgui/model/model_factory.py +++ b/dzgui/model/model_factory.py @@ -7,11 +7,11 @@ from dzgui.util.strings import delimiter import gi gi.require_version("Gtk", "3.0") -from gi.repository.Gtk import ListStore, TreeIter # noqa E402 -from gi.repository import GObject # noqa E402 +from gi.repository.Gtk import ListStore, TreeIter +from gi.repository import GObject GTYPE_TO_PYTHON = { - GObject.type_from_name(GObject.type_name(ptype)): ptype + GObject.type_from_name(GObject.type_name(ptype)): ptype # type: ignore[no-untyped-call] for ptype in (int, float, str, bool, object) } @@ -147,7 +147,7 @@ class ModelFactory: def make_help_store(self) -> FastInsertListStore: store = self.new_model_from_class(MenuCols) - rows = [[row.dict["label"], row] for row in HELP_MENU_ROWS] + rows = [[row.label, row] for row in HELP_MENU_ROWS] store.extend(rows) return store diff --git a/dzgui/model/proxy_model.py b/dzgui/model/proxy_model.py index c892110..2cd9b24 100644 --- a/dzgui/model/proxy_model.py +++ b/dzgui/model/proxy_model.py @@ -1,10 +1,13 @@ import re -from typing import Any, TYPE_CHECKING, Union +from dataclasses import dataclass, fields +from operator import attrgetter +from typing import TYPE_CHECKING from dzgui.const.enum import FilterMode from dzgui.model.model_factory import ModelFactory from dzgui.util import strings +from dzgui.strings import filters as str_filters if TYPE_CHECKING: from dzgui.managers.filter import FilterManager @@ -16,6 +19,116 @@ DAY_REG = r"([0][6-9]|[1][0-7])" NIGHT_REG = r"([0][0-5]|[1][8-9]|[2][0-3])" +@dataclass(slots=True, frozen=True) +class ServerRow: + name: str + mapname: str + view: str + time: str + players: int + max_players: int + queue: int + ip: str + qport: int + ping: str + provider: str + modded: bool + + # NOTE: construct getter once for fast lookup in lieu of field introspection + _to_tuple = attrgetter( + "name", + "mapname", + "view", + "time", + "players", + "max_players", + "queue", + "ip", + "qport", + "ping", + "provider", + "modded", + ) + + @classmethod + def marshal_rows( + cls, rows: list["ServerRow"] + ) -> list[tuple[str, str, str, str, int, int, int, str, int, str, str, bool]]: + """Marshals data before passing into FastInsertListStore's extend() method""" + getter = cls._to_tuple + return [getter(row) for row in rows] + + +class FiltrationManager: + def __init__(self) -> None: + + self.filter_map = { + strings.filter_3pp: self.filter_3pp, + strings.filter_1pp: self.filter_1pp, + strings.filter_official: self.filter_official, + strings.filter_unofficial: self.filter_unofficial, + strings.filter_empty: self.filter_empty, + strings.filter_full: self.filter_full, + strings.filter_duplicate: self.filter_duplicate, + strings.filter_day: self.filter_day, + strings.filter_night: self.filter_night, + strings.filter_nonascii: self.filter_nonascii, + strings.filter_lowpop: self.filter_lowpop, + strings.filter_modded: self.filter_modded, + strings.filter_unmodded: self.filter_unmodded, + } + + def filter_3pp(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if row.view != strings.filter_3pp] + + def filter_1pp(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if row.view != strings.filter_1pp] + + def filter_official(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if row.provider != strings.filter_official] + + def filter_unofficial(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if row.provider != strings.filter_unofficial] + + def filter_empty(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if row.players != 0] + + def filter_full(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if row.players != row.max_players] + + def filter_duplicate(self, rows: list[ServerRow]) -> list[ServerRow]: + seen = set() + final = [] + for row in rows: + if row.name not in seen: + seen.add(row.name) + final.append(row) + rows = final + return rows + + def filter_day(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if not re.match(DAY_REG, row.time)] + + def filter_night(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if not re.match(NIGHT_REG, row.time)] + + def filter_nonascii(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if row.name.isascii()] + + def filter_lowpop(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if (row.players / row.max_players * 100) > 30] + + def filter_modded(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if not row.modded] + + def filter_unmodded(self, rows: list[ServerRow]) -> list[ServerRow]: + return [row for row in rows if row.modded] + + def filter(self, filter_type: str, rows: list[ServerRow]) -> list[ServerRow]: + func = self.filter_map[filter_type] + return func(rows) + + class ProxyModelManager: """ Manages access to cached FastInsertListStore resources and @@ -30,20 +143,16 @@ class ProxyModelManager: def __init__(self, filter_man: "FilterManager") -> None: self.filter_cache: dict[ - tuple[str], - list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]], + tuple[str, ...], + list[ServerRow], ] = {} self.proxy_model: "FastInsertListStore" self.filter_man = filter_man + self.filtration_manager = FiltrationManager() - self.control_model: ( - list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]] - | None - ) = None - self.filtered: list[ - tuple[str, str, str, str, int, int, int, str, int, int, str, bool] - ] = [] + self.control_model: list[ServerRow] | None = None + self.filtered: list[ServerRow] = [] self.success = True def has_control_model(self) -> bool: @@ -51,12 +160,7 @@ class ProxyModelManager: return False return True - def append_row(self, row: list) -> None: - self.proxy_model.append(row) - - def append_row_to_control( - self, row: tuple[str, str, str, str, int, int, int, str, int, int, str, bool] - ) -> None: + def append_row_to_control(self, row: ServerRow) -> None: if self.control_model is None: raise AttributeError("Trying to add rows to a non-existent model") self.control_model.append(row) @@ -64,22 +168,24 @@ class ProxyModelManager: def update_playercount(self, playercount: "NewPlayerCount") -> None: treeiter = playercount.treeiter - self.proxy_model[treeiter][4] = playercount.players - self.proxy_model[treeiter][6] = playercount.queue + field = [f.name for f in fields(ServerRow)] + index_players = field.index("players") + index_queue = field.index("queue") + self.proxy_model[treeiter][index_players] = playercount.players + self.proxy_model[treeiter][index_queue] = playercount.queue - # TODO: use dataclass for record rows def append_row_to_history( self, - history: tuple[str, str, str, str, int, int, int, str, int, int, str, bool], + history: ServerRow, ) -> None: - addr = history[7] - qport = history[8] + addr = history.ip + qport = history.qport if self.control_model is None: raise AttributeError("Trying to append row to empty model") found = False for i, row in enumerate(self.control_model): - if addr == row[7] and qport == row[8]: + if addr == row.ip and qport == row.qport: item = self.control_model.pop(i) self.control_model.append(item) found = True @@ -88,6 +194,7 @@ class ProxyModelManager: if found is False: self.control_model.append(history) + # NOTE: truncate history to 10 records max if len(self.control_model) == 11: del self.control_model[0] @@ -99,7 +206,7 @@ class ProxyModelManager: addr = f"{record.ip}:{record.gameport}" qport = record.qport for row in self.control_model: - if addr == row[7] and qport == row[8]: + if addr == row.ip and qport == row.qport: self.control_model.remove(row) break self.wipe_cache() @@ -108,19 +215,22 @@ class ProxyModelManager: def clear_proxy_model(self) -> None: self.proxy_model.clear() - def get_proxy_model(self) -> Union["FastInsertListStore", None]: + def get_proxy_model(self) -> "FastInsertListStore | None": if hasattr(self, "proxy_model"): return self.proxy_model return None def filter( self, mode: FilterMode, skip_cache: bool = False - ) -> Union["FastInsertListStore", None]: + ) -> "FastInsertListStore | None": """ Native Gtk.TreeView.refilter() method was not performant enough when running in the main loop with 40k+ records skip_cache: used when updating a record on Saved Servers/History and forcing a refilter + + NOTE: data must be marshalled from ServerRow format before constructing list stores + The "clone" is the proxy model (list of tuples) actually fed into Gtk.TreeView """ # TODO: filter cache: return a dataclass object with clearly enumerated map, keyword, and filter values # instead of just a serial list of strings @@ -129,8 +239,9 @@ class ProxyModelManager: if skip_cache is False: if filters in self.filter_cache: cached_rows = self.filter_cache[filters] + marshalled_rows = ServerRow.marshal_rows(cached_rows) clone = ModelFactory().make_server_store() - clone.extend(cached_rows) + clone.extend(marshalled_rows) self.set_proxy_model(clone) self.set_filtered(cached_rows) return None @@ -149,17 +260,18 @@ class ProxyModelManager: clone = ModelFactory().make_server_store() if len(rows) > 0: rows = self.sort_rows(rows) - clone.extend(rows) + marshalled_rows = ServerRow.marshal_rows(rows) + clone.extend(marshalled_rows) self.set_cache(filters, rows) self.set_proxy_model(clone) return clone - def sort_rows(self, rows: list) -> list: - rows.sort(key=lambda x: re.sub(r"[^A-Za-z0-9]+", "", x[0].lower())) + def sort_rows(self, rows: list[ServerRow]) -> list[ServerRow]: + rows.sort(key=lambda x: re.sub(r"[^A-Za-z0-9]+", "", x.name.lower())) return rows - def filter_initial(self, filters: tuple) -> list: + def filter_initial(self, filters: tuple[str, ...]) -> list[ServerRow]: """ Simply culls the control model of any disabled filters """ @@ -168,20 +280,20 @@ class ProxyModelManager: self.set_filtered(self.filter_toggle_off(filters, f)) return self.filtered - def filter_map(self, filters: tuple) -> list: + def filter_map(self, filters: tuple[str, ...]) -> list[ServerRow]: """ Multi-filtration for any context starts by narrowing by map """ rows = self.filtered sel_map = self.filter_man.get_active_map_name() - if sel_map == strings.all_maps: + if sel_map == str_filters.all_maps: return rows - rows = [row for row in rows if row[1] == sel_map] + rows = [row for row in rows if row.mapname == sel_map] return rows - def filter_keyword(self, filters: tuple) -> list: + def filter_keyword(self, filters: tuple[str, ...]) -> list[ServerRow]: keyword = self.filter_man.get_active_keyword() rows = self.filtered @@ -191,13 +303,15 @@ class ProxyModelManager: filtered = [ row for row in rows - if keyword in row[0].lower() - or keyword in row[1].lower() - or keyword in row[7].lower() + if keyword in row.name.lower() + or keyword in row.mapname.lower() + or keyword in row.ip.lower() ] return filtered - def filter_toggle_off(self, filters: tuple, filter_type: str) -> list: + def filter_toggle_off( + self, filters: tuple[str, ...], filter_type: str + ) -> list[ServerRow]: """ Sub-filtration of the current model """ @@ -211,43 +325,11 @@ class ProxyModelManager: self.set_filtered(None) return [] - rows = self.filtered - match filter_type: - case strings.filter_3pp: - rows = [row for row in rows if row[2] != strings.filter_3pp] - case strings.filter_1pp: - rows = [row for row in rows if row[2] != strings.filter_1pp] - case strings.filter_official: - rows = [row for row in rows if row[10] != strings.filter_official] - case strings.filter_unofficial: - rows = [row for row in rows if row[10] != strings.filter_unofficial] - case strings.filter_empty: - rows = [row for row in rows if row[4] != 0] - case strings.filter_full: - rows = [row for row in rows if row[4] != row[5]] - case strings.filter_duplicate: - seen = set() - final = [] - for row in rows: - if row[0] not in seen: - seen.add(row[0]) - final.append(row) - rows = final - case strings.filter_day: - rows = [row for row in rows if not re.match(DAY_REG, row[3])] - case strings.filter_night: - rows = [row for row in rows if not re.match(NIGHT_REG, row[3])] - case strings.filter_nonascii: - rows = [row for row in rows if row[0].isascii()] - case strings.filter_lowpop: - rows = [row for row in rows if (row[4] / row[5] * 100) > 30] - case strings.filter_modded: - rows = [row for row in rows if not row[11]] - case strings.filter_unmodded: - rows = [row for row in rows if row[11]] + # TODO: pack filter type enum in filter checkbox, rather than stringwise comparison + rows = self.filtration_manager.filter(filter_type, self.filtered) return rows - def filter_toggle_on(self, filters: tuple[str]) -> list: + def filter_toggle_on(self, filters: tuple[str, ...]) -> list[ServerRow]: """Effectively applies all filters""" self.set_filtered(self.control_model) self.set_filtered(self.filter_map(filters)) @@ -258,31 +340,36 @@ class ProxyModelManager: self.set_filtered(self.filter_toggle_off(filters, f)) return self.filtered - def set_cache(self, filters: tuple[str], filtered_rows: list[tuple]) -> None: + def set_cache( + self, filters: tuple[str, ...], filtered_rows: list[ServerRow] + ) -> None: self.filter_cache[filters] = filtered_rows - def set_filtered(self, rows: list | None) -> None: + def set_filtered(self, rows: list[ServerRow] | None) -> None: if rows is None: self.filtered = [] else: self.filtered = rows - def get_filtered(self) -> list: + def get_filtered(self) -> list[ServerRow]: return self.filtered def set_proxy_model(self, model: "FastInsertListStore") -> None: """ FastInsertListStore representation of the raw model after filtration + + This attribute is saved from within a thread so that it later be retrieved + and safely packed into the TreeView in the main loop, cf. get_proxy_model() """ self.proxy_model = model - def set_control(self, rows: list) -> None: + def set_control(self, rows: list[ServerRow]) -> None: """ Raw data model prior to filtration """ self.control_model = rows - def get_control(self) -> list[Any]: + def get_control(self) -> list[ServerRow]: if self.control_model is None: raise AttributeError( "Expected a populated control model, but it is Nonetype" @@ -293,7 +380,7 @@ class ProxyModelManager: self.filtered = [] self.filter_cache = {} - def push(self, data: list[Any]) -> None: + def push(self, data: list[ServerRow]) -> None: self.wipe_cache() self.set_control(data) cur_keyword = self.filter_man.get_active_keyword() diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index a1d3082..5e217db 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -21,12 +21,12 @@ from dzgui.views.dialogs.generic import ExceptionDialog import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib # noqa E402 +from gi.repository import Gtk, GLib if TYPE_CHECKING: from dzgui.api.servers import A2SInfo, Record from dzgui.controllers.mc import Controller - from dzgui.model.proxy_model import ProxyModelManager + from dzgui.model.proxy_model import ProxyModelManager, ServerRow from dzgui.views.trees.tree_servers import ServerTreeView logger = logging.getLogger(APP_NAME) @@ -241,10 +241,7 @@ class ServerModelManager: def update_history( self, - rows: ( - list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]] - | None - ) = None, + rows: list["ServerRow"] | None = None, ) -> None: proxy_man = self._get_proxy_man() if rows is not None: @@ -267,7 +264,6 @@ class ServerModelManager: self.first_iteration = False self.emitter.emit("load_maps", store) - # TODO: dataclass for record rows; check for other dict annotations def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None: row, record = data proxy_man = self._get_proxy_man() @@ -285,6 +281,10 @@ class ServerModelManager: def remove_from_history(self, record: Servers.Record) -> None: proxy_man = self._get_proxy_man() proxy_man.remove_row_from_control(record) + + fqip = Servers.record_to_fqip(record) + config_man = self.controller.get_config_man() + config_man.remove_from_history_file(fqip) self.update_history() @call_on_thread(dialog.querying) @@ -432,7 +432,7 @@ class ServerModelManager: if show_dialog: dialog = ExceptionDialog(self.controller, dialogs.load_error_lan) dialog.show_all() - dialog.run() + dialog.run() # type: ignore[no-untyped-call] def _cleanup_on_failure(self, show_dialog: bool = True) -> None: # TODO: disable map, keyword, and filter widgets if model is None @@ -451,15 +451,15 @@ class ServerModelManager: dialog = ExceptionDialog(self.controller, server_timeout) dialog.set_details_buffer(kb.DZG_006) dialog.show_all() - dialog.run() + dialog.run() # type: ignore[no-untyped-call] - def _push_data(self, data: list[Any]) -> None: + def _push_data(self, data: list["ServerRow"]) -> None: self._get_proxy_man().push(data) self._sort_unique_maps(data) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success)) - def _sort_unique_maps(self, data: list) -> None: - u_maps = set([row[1] for row in data]) + def _sort_unique_maps(self, data: list[Any]) -> None: + u_maps = set([row.mapname for row in data]) self._set_new_maps(sorted(u_maps)) def _set_new_maps(self, maps: list[str]) -> None: @@ -473,7 +473,6 @@ class ServerModelManager: @call_on_thread(dialog.filtering) def refilter(self, mode: FilterMode) -> None: - # FIXME: causes two wait dialogs when map selection change signal emits after loading servers self.first_iteration = False proxy_man = self._get_proxy_man() proxy_man.filter(mode) diff --git a/dzgui/strings/buttons.py b/dzgui/strings/buttons.py index faa00a0..539b7a0 100644 --- a/dzgui/strings/buttons.py +++ b/dzgui/strings/buttons.py @@ -1,2 +1,16 @@ +clipboard_button = "Copy to clipboard" +clipboard_ip_button = "Copy IP to clipboard" + +dock_button_collapse = "Collapse the connection panel" +dock_button_expand = "Expand the connection panel" + workshop = "Steam Workshop" workshop_tooltip = "Open your Workshop subscriptions" + +version_notifier = "Copied!" +version_tooltip = "Click to copy to clipboard" + +update_button = "Updates available" + +unset_button_label = "Unset" +unset_button_tooltip = "Unset this server as favorite" diff --git a/dzgui/strings/dialogs.py b/dzgui/strings/dialogs.py index b895ba0..b31f58a 100644 --- a/dzgui/strings/dialogs.py +++ b/dzgui/strings/dialogs.py @@ -29,3 +29,5 @@ toggle_calc = "Toggle calculator" server_details = "Server details" server_message = "Server message" + +caught_cancel_signal = "Caught cancel signal, cleaning up" diff --git a/dzgui/strings/filters.py b/dzgui/strings/filters.py new file mode 100644 index 0000000..b6e6bc6 --- /dev/null +++ b/dzgui/strings/filters.py @@ -0,0 +1,4 @@ +all_maps = "All maps" +filters_heading = "Filters" +keyword_placeholder = "Filter by keyword" +map_placeholder = "Filter by map" diff --git a/dzgui/strings/generic.py b/dzgui/strings/generic.py index 1aff78a..68b9cd9 100644 --- a/dzgui/strings/generic.py +++ b/dzgui/strings/generic.py @@ -1 +1,2 @@ +tab_changed_label = "*" selectable_tree = "Ctrl-click to select multiple; Shift-click to select a range." diff --git a/dzgui/strings/notes.py b/dzgui/strings/notes.py new file mode 100644 index 0000000..77d53da --- /dev/null +++ b/dzgui/strings/notes.py @@ -0,0 +1,5 @@ +dialog_title = "Add a short note/reminder" +dialog_subtitle = "Limit 30 chars" +delete_button = "Delete note" +ok_button = "Update note" +current_note = "Current note: '{0}'" diff --git a/dzgui/strings/options.py b/dzgui/strings/options.py index a105f07..fd505f2 100644 --- a/dzgui/strings/options.py +++ b/dzgui/strings/options.py @@ -16,7 +16,9 @@ lan_combo = "LAN" fullscreen_eventbox = "This option is not available on Steam Deck." save_button = "Save" +api_placeholder = "Enter API key" api_failed = "API validation failed" +api_validate = "Validate" flatpak_label = "Flatpak client" flatpak_standard = "Standard" diff --git a/dzgui/strings/rows.py b/dzgui/strings/rows.py new file mode 100644 index 0000000..636dd7f --- /dev/null +++ b/dzgui/strings/rows.py @@ -0,0 +1,29 @@ +changelog_label = "View changelog" +changelog_status = "Opens the DZGUI changelog" + +log_label = "Show debug log" +log_status = "Read the DZGUI log generated since startup" + +separator_label = "SEPARATOR" +separator_status = "" + +system_log_label = "Output system info to log file" +system_log_status = "Dump diagnostic data for troubleshooting" + +docs_label = "Documentation/help files (GitHub) ⧉" +docs_status = "Open the DZGUI documentation in a browser" + +docs_fallback_label = "Documentation/help files (Codeberg mirror) ⧉" +docs_fallback_status = "Open the DZGUI documentation in a browser" + +bugs_label = "Report a bug (GitHub) ⧉" +bugs_status = "Open the DZGUI issue tracker in a browser" + +forum_label = "DZGUI Subreddit ⧉" +forum_status = "Open the DZGUI discussion forum in a browser" + +sponsor_label = "Sponsor (GitHub) ⧉" +sponsor_status = "Sponsor development of the DZGUI project" + +thanks_label = "Special thanks" +thanks_status = "A list of contributors, testers, and sponsors" diff --git a/dzgui/strings/server_mods.py b/dzgui/strings/server_mods.py index c41b443..d5a2e42 100644 --- a/dzgui/strings/server_mods.py +++ b/dzgui/strings/server_mods.py @@ -3,4 +3,5 @@ mod_id = "ID" up_to_date = "Up to date" modlist = "Modlist" checkmark = "✓" -resync="⟳" +resync = "⟳" +invalid_mod = "Invalid mod" diff --git a/dzgui/util/clip.py b/dzgui/util/clip.py index 971515c..991346d 100644 --- a/dzgui/util/clip.py +++ b/dzgui/util/clip.py @@ -1,8 +1,7 @@ import gi gi.require_version("Gtk", "3.0") -gi.require_version("Gdk", "3.0") -from gi.repository import Gtk, Gdk # noqa E402 +from gi.repository import Gtk, Gdk def copy_clipboard(text: str) -> None: diff --git a/dzgui/util/css.py b/dzgui/util/css.py index 44bf7cd..2350be7 100644 --- a/dzgui/util/css.py +++ b/dzgui/util/css.py @@ -5,7 +5,7 @@ from dzgui.const.constants import APP_NAME_LOWER, CSS_PATH import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk # noqa E402 +from gi.repository import Gtk, Gdk def add_class(widget: Gtk.Widget, label: str) -> None: diff --git a/dzgui/util/diag.py b/dzgui/util/diag.py index 97423da..56ce725 100644 --- a/dzgui/util/diag.py +++ b/dzgui/util/diag.py @@ -11,6 +11,7 @@ from dzgui.const.enum import Preferences from dzgui.config.query import lookup from dzgui.init.prefix import get_version from dzgui.util.redact import redact_home +from dzgui.util.strings import unknown logger = logging.getLogger(APP_NAME) @@ -27,28 +28,22 @@ def get_cpu_model() -> str: def print_servers(servers: list[str]) -> str: - s = "" - for server in servers: - s += server + "\n" - return s + return "\n".join(servers) def print_mods(mods: list[int]) -> str: - s = "" - for mod in mods: - s += str(mod) + "\n" - return s + return "\n".join(map(str, mods)) def write_diagnostic(config: Path, outfile: Path) -> None: date = datetime.now().isoformat() try: - # FIXME: ID_LIKE not available on CachyOS + # NOTE: ID_LIKE not available on CachyOS distro = platform.freedesktop_os_release()["ID_LIKE"] except Exception as e: logger.warn(e) - distro = "Unknown" + distro = unknown kernel = os.uname().release cpu = get_cpu_model() @@ -68,7 +63,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None: servers = lookup(config, Preferences.IP_LIST) servers_pretty = print_servers(servers) - # FIXME: extraneous newlines in lists of mods template = f"""\ {APP_NAME} version {version} Date: {date} @@ -82,6 +76,7 @@ def write_diagnostic(config: Path, outfile: Path) -> None: Mods: {mods_pretty} + Servers: {servers_pretty} """ diff --git a/dzgui/util/dist.py b/dzgui/util/dist.py index 46c2d44..94ec811 100644 --- a/dzgui/util/dist.py +++ b/dzgui/util/dist.py @@ -7,11 +7,6 @@ from dzgui.const.constants import APP_NAME from dzgui.util.ip import get_coords from dzgui.util.localize import number -import gi - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 - if TYPE_CHECKING: from queue import Queue from dzgui.const.enum import ServerTab @@ -59,7 +54,7 @@ class CalcDist: self, addr: str, enum: "ServerTab", - result_queue: "Queue", + result_queue: "Queue[tuple[str, Haversine | None, ServerTab]]", controller: "Controller", cache: dict[str, Haversine], ) -> None: @@ -72,7 +67,7 @@ class CalcDist: self.ip = self.addr dist = self.compare(self.ip) - self.result_queue.put([self.addr, dist, self.enum]) + self.result_queue.put((self.addr, dist, self.enum)) def compare(self, remote_ip: str) -> Haversine | None: prefs = self.controller.get_prefs() diff --git a/dzgui/util/flatpak.py b/dzgui/util/flatpak.py index 7372fd6..d86663d 100644 --- a/dzgui/util/flatpak.py +++ b/dzgui/util/flatpak.py @@ -1,12 +1,16 @@ import logging import subprocess +from typing import TYPE_CHECKING from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID, TMP_PATH logger = logging.getLogger(APP_NAME) +if TYPE_CHECKING: + from pathlib import Path -def apply_overrides(xdg: dict[str, str]) -> None: + +def apply_overrides(xdg: dict[str, "Path"]) -> None: try: # NOTE: D-Bus registration required for sandbox args = [ @@ -19,7 +23,7 @@ def apply_overrides(xdg: dict[str, str]) -> None: subprocess.run([*args], capture_output=True) paths: list[str] = [] - paths.extend([*xdg.values()]) + paths.extend([str(path) for path in xdg.values()]) paths.append(TMP_PATH) for path in paths: diff --git a/dzgui/util/format.py b/dzgui/util/format.py index e7b4d07..38894f8 100644 --- a/dzgui/util/format.py +++ b/dzgui/util/format.py @@ -1,4 +1,5 @@ import re +from typing import Any from dzgui.util.localize import number from dzgui.strings.generic import selectable_tree @@ -7,7 +8,7 @@ from dzgui.util.strings import no_mods, no_servers, workshop import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk def format_pango(text: str) -> str: @@ -60,7 +61,7 @@ def format_server_mods(mods: int) -> str: return f"Found {mods:n} {plural}. {workshop}" -def format_player_count(model: Gtk.TreeModel | None, control: list) -> str: +def format_player_count(model: Gtk.TreeModel | None, control: list[Any]) -> str: players = 0 hits: int status: str diff --git a/dzgui/util/ip.py b/dzgui/util/ip.py index d9a2c12..168e036 100644 --- a/dzgui/util/ip.py +++ b/dzgui/util/ip.py @@ -31,7 +31,7 @@ class ReservedIPError(Exception): pass -def ip2list(ip: str) -> list: +def ip2list(ip: str) -> list[int]: return [int(digit) for digit in ip.split(".")] diff --git a/dzgui/util/keys.py b/dzgui/util/keys.py index 04f291d..5293d21 100644 --- a/dzgui/util/keys.py +++ b/dzgui/util/keys.py @@ -1,6 +1,8 @@ import gi + gi.require_version("Gtk", "3.0") -from gi.repository import Gdk # noqa E402 +from gi.repository import Gdk + def is_navkey(key: int) -> bool: nav_keys = [ @@ -18,6 +20,7 @@ def is_navkey(key: int) -> bool: return True return False + def is_ctrl_mask(event: Gdk.EventKey) -> bool: if event.state is Gdk.ModifierType.CONTROL_MASK: return True diff --git a/dzgui/util/margins.py b/dzgui/util/margins.py index f4f8883..82cef96 100644 --- a/dzgui/util/margins.py +++ b/dzgui/util/margins.py @@ -1,6 +1,8 @@ import gi + gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk + def set_surrounding_margins(widget: Gtk.Widget, margin: int) -> None: """ diff --git a/dzgui/util/save_screenshot.py b/dzgui/util/save_screenshot.py index 8c106bc..ffa769f 100644 --- a/dzgui/util/save_screenshot.py +++ b/dzgui/util/save_screenshot.py @@ -4,7 +4,7 @@ from typing import cast, TYPE_CHECKING import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GLib # noqa E402 +from gi.repository import Gdk, GLib if TYPE_CHECKING: from dzgui.controllers.mc import Controller diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 931ca88..7951298 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -118,6 +118,7 @@ servers = { "0": "toggle filter 10", "Minus": "toggle filter 11", "Backslash": "toggle filter 12", + "Apostrophe": "toggle filter 13", } vim = { "j": "Move down a row/scroll up", @@ -159,9 +160,6 @@ filter_unofficial = "Unofficial" filter_modded = "Modded" filter_unmodded = "Unmodded" -# maps -all_maps = "All maps" - # state unknown = "Unknown" none_provided = "None provided" @@ -252,6 +250,7 @@ class Crumbs: developers: str default: str offline: str + connect: str @dataclass(slots=True, frozen=True) @@ -439,6 +438,7 @@ crumbs = Crumbs( developers="Options > Developers", default="Servers > ", offline="Mods > Play offline", + connect="Connect", ) no_mods = "No local mods found." diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 79fde50..756dae2 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -38,7 +38,7 @@ import gi gi.require_version("Gtk", "3.0") gi.require_version("GLibUnix", "2.0") -from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore # noqa E402 +from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore if TYPE_CHECKING: from dzgui.config.userprefs import UserPrefs diff --git a/dzgui/views/components/buttonbox.py b/dzgui/views/components/buttonbox.py index 496f237..ea79c45 100644 --- a/dzgui/views/components/buttonbox.py +++ b/dzgui/views/components/buttonbox.py @@ -1,13 +1,13 @@ import logging from typing import TYPE_CHECKING -from dzgui.const.constants import APP_NAME, NO_EXPAND, NO_FILL, NO_PADDING +from dzgui.const.constants import APP_NAME from dzgui.const.enum import ButtonType import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk # noqa E402 +from gi.repository import Gtk, Gdk logger = logging.getLogger(APP_NAME) @@ -19,7 +19,11 @@ if TYPE_CHECKING: class ContextualButton(Gtk.Button): def __init__( - self, label: str, opens: "NotebookPage", tooltip: str, context: ButtonType + self, + label: str, + opens: "NotebookPage | None", + tooltip: str, + context: ButtonType, ) -> None: super().__init__( label=label, @@ -51,9 +55,9 @@ class ButtonBox(Gtk.Box): for side_button in ButtonType: button = ContextualButton( - label=side_button.dict["label"], - opens=side_button.dict["opens"], - tooltip=side_button.dict["tooltip"], + label=side_button.label, + opens=side_button.opens, + tooltip=side_button.tooltip, context=side_button, ) @@ -63,7 +67,7 @@ class ButtonBox(Gtk.Box): self.buttons.append(button) button.connect("clicked", self._on_selection_button_clicked) - self.pack_start(button, NO_EXPAND, NO_FILL, NO_PADDING) + self.pack_start(button, expand=False, fill=False, padding=0) def _focus_first_button(self, emitter: "Emitter") -> None: self.buttons[0].grab_focus() diff --git a/dzgui/views/components/buttons.py b/dzgui/views/components/buttons.py index bb3d45d..7fc45b4 100644 --- a/dzgui/views/components/buttons.py +++ b/dzgui/views/components/buttons.py @@ -1,28 +1,25 @@ -from typing import Callable, Literal, Self, TYPE_CHECKING, Union +from typing import Callable, Literal, Self, TYPE_CHECKING from dzgui.util.clip import copy_clipboard -from dzgui.util.format import pluralize from dzgui.strings import buttons from dzgui.util.strings import ( - alert_button_tooltip, atomic_buttons, ) -from dzgui.const.constants import ( - CLIPBOARD, - CLOSE, - INPUT_KEYBOARD, - LIST_ADD, - REFRESH_ICON, - STEAM_ICON, - WARNING, - WEB_BROWSER, +from dzgui.const.icons import ( + ICON_CLIPBOARD, + ICON_CLOSE, + ICON_INPUT_KEYBOARD, + ICON_LIST_ADD, + ICON_REFRESH, + ICON_STEAM, + ICON_WEB_BROWSER, ) from dzgui.strings import connect_panel import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GLib # noqa E402 +from gi.repository import Gtk, GLib if TYPE_CHECKING: from gi.repository import GLib @@ -64,7 +61,7 @@ class IconButton(Gtk.Button): self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end) self.set_image(self.icon) self.set_image_position(position) - self.set_focus_on_click(False) + self.set_focus_on_click(False) # type: ignore[no-untyped-call] self.set_always_show_image(True) def swap_icon(self, icon: str) -> None: @@ -101,15 +98,13 @@ class LargeIconTextButton(IconButton): class ClipboardButton(IconTextButton): - def __init__(self, controller: Union["Controller", None], func: Callable) -> None: - super().__init__(CLIPBOARD, atomic_buttons.copy) - self.controller = controller + def __init__(self, func: Callable[[], str]) -> None: + super().__init__(ICON_CLIPBOARD, atomic_buttons.copy) self.connect("clicked", self._on_button_clicked, func) - # TODO: strings - self.set_tooltip_text("Copy to clipboard") + self.set_tooltip_text(buttons.clipboard_button) - def _on_button_clicked(self, button: Self, func: Callable) -> None: + def _on_button_clicked(self, button: Self, func: Callable[[], str]) -> None: def revert() -> Literal[False]: self.revert_icon() return False @@ -120,33 +115,30 @@ class ClipboardButton(IconTextButton): GLib.timeout_add(600, revert) -# TODO: determine when controller would be passed to this button or drop class CopyIpButton(ClipboardButton): - def __init__(self, controller: Union["Controller", None], func: Callable) -> None: - super().__init__(controller, func) + def __init__(self, func: Callable[[], str]) -> None: + super().__init__(func) - # TODO: strings - self.set_tooltip_text("Copy IP to clipboard") + self.set_tooltip_text(buttons.clipboard_ip_button) class CloseButton(IconTextButton): def __init__(self, label: str) -> None: - super().__init__(icon=CLOSE, label=label) + super().__init__(icon=ICON_CLOSE, label=label) - # TODO: strings - self.set_tooltip_text("Unset this server as favorite") + self.set_tooltip_text(buttons.unset_button_tooltip) class WebButton(IconTextButton): def __init__(self, label: str) -> None: - super().__init__(icon=WEB_BROWSER, label=label) + super().__init__(icon=ICON_WEB_BROWSER, label=label) pass class RefreshButton(IconTextButton): def __init__(self, controller: "Controller") -> None: super().__init__( - icon=REFRESH_ICON, + icon=ICON_REFRESH, label=atomic_buttons.refresh, ) @@ -211,7 +203,7 @@ class RefreshButton(IconTextButton): class KeysButton(IconTextButton): def __init__(self, controller: "Controller") -> None: - super().__init__(icon=INPUT_KEYBOARD, label=atomic_buttons.keys) + super().__init__(icon=ICON_INPUT_KEYBOARD, label=atomic_buttons.keys) self.controller = controller self.set_margin_top(10) @@ -228,7 +220,7 @@ class KeysButton(IconTextButton): class SteamConnectButton(LargeIconTextButton): def __init__(self) -> None: - super().__init__(icon=STEAM_ICON, label=connect_panel.connect_button) + super().__init__(icon=ICON_STEAM, label=connect_panel.connect_button) self.set_tooltip_text(connect_panel.connect_tooltip) @@ -248,33 +240,10 @@ class SteamWorkshopButton(SteamTextButton): class AddButton(IconTextButton): def __init__(self) -> None: - super().__init__(icon=LIST_ADD, label=connect_panel.add_button) + super().__init__(icon=ICON_LIST_ADD, label=connect_panel.add_button) self.set_tooltip_text(connect_panel.add_tooltip) -class LoggerAlertsButton(IconTextButton): - def __init__(self, warnings: int, errors: int) -> None: - warnings_text = "" - errors_text = "" - separator = "" - if warnings > 0: - warnings_plural = pluralize("warnings", warnings) - warnings_text = f"{warnings} {warnings_plural}" - if errors > 0: - errors_plural = pluralize("errors", errors) - errors_text = f" {errors} {errors_plural}" - if warnings + errors > 1: - separator = "," - concat = warnings_text + separator + errors_text - super().__init__( - icon=WARNING, - label=f"Loaded with{concat}", - ) - self.set_halign(Gtk.Align.END) - self.set_hexpand(True) - self.set_tooltip_text(alert_button_tooltip) - - class SpinnerButton(Gtk.Button): def __init__(self, label: str) -> None: super().__init__() diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index 17d0a71..7ea9a34 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -1,8 +1,14 @@ from typing import Self, TYPE_CHECKING -from dzgui.const.constants import CARET_DOWN, CARET_UP, UDP_PORT +from dzgui.const.icons import ICON_CARET_DOWN, ICON_CARET_UP +from dzgui.const.constants import UDP_PORT from dzgui.model.servers import ServerModelManager from dzgui.strings import connect_panel +from dzgui.strings.buttons import ( + dock_button_collapse, + dock_button_expand, + unset_button_label, +) from dzgui.util.keys import is_ctrl_mask from dzgui.views.components.buttons import ( AddButton, @@ -18,7 +24,7 @@ from dzgui.views.components.labels import BoldLabel import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk # noqa E402 +from gi.repository import Gtk, Gdk if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -30,27 +36,24 @@ ROWS = 1 class DockButton(IconButton): def __init__(self, child: Gtk.Widget) -> None: - super().__init__(CARET_DOWN) + super().__init__(ICON_CARET_DOWN) self.expanded = True self.child = child self.connect("clicked", self._on_click) - self.collapse_text = "Collapse the connection panel" - self.expand_text = "Expand the connection panel" - self.set_margin_top(10) - self.set_tooltip_text(self.collapse_text) + self.set_tooltip_text(dock_button_collapse) def collapse(self) -> None: - self.set_image(Icon(CARET_UP)) + self.set_image(Icon(ICON_CARET_UP)) self.child.hide() - self.set_tooltip_text(self.expand_text) + self.set_tooltip_text(dock_button_expand) def expand(self) -> None: - self.set_image(Icon(CARET_DOWN)) + self.set_image(Icon(ICON_CARET_DOWN)) self.child.show() - self.set_tooltip_text(self.collapse_text) + self.set_tooltip_text(dock_button_collapse) def _on_click(self, button: Self) -> None: if self.expanded: @@ -81,7 +84,6 @@ class LanPanel(Gtk.Frame): self.scan = Gtk.Button(label=connect_panel.lan_scan_button) self.scan.connect("clicked", self._on_scan_clicked) - # TODO: strings self.early_abort = Gtk.CheckButton(label=connect_panel.lan_checkbox) self.early_abort.set_active(True) self.early_abort.set_tooltip_text(connect_panel.lan_abort_tooltip) @@ -187,10 +189,9 @@ class FavPanel(Gtk.Frame): self.fav_button = SteamConnectButton() self.fav_button.connect("clicked", self._on_connect_clicked) - self.copy_button = CopyIpButton(self.controller, self.get_fav_ip) + self.copy_button = CopyIpButton(self.get_fav_ip) - # TODO: strings - self.unset_button = CloseButton("Unset") + self.unset_button = CloseButton(unset_button_label) self.unset_button.connect("clicked", self._on_unset_clicked) if favorite is None: diff --git a/dzgui/views/components/crumbs.py b/dzgui/views/components/crumbs.py index cd57426..22b48f5 100644 --- a/dzgui/views/components/crumbs.py +++ b/dzgui/views/components/crumbs.py @@ -7,7 +7,7 @@ from dzgui.util.format import embolden import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402 +from gi.repository import Gtk if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -43,7 +43,7 @@ class Breadcrumbs(Gtk.Label): label = page.get_current_tab_text() self.set_server_crumbs(label) else: - crumbs = enum.dict["crumbs"] + crumbs = enum.crumbs self.set_crumbs(crumbs) def set_server_crumbs(self, suffix: str) -> None: diff --git a/dzgui/views/components/entry.py b/dzgui/views/components/entry.py index e1b3e7e..db3fb94 100644 --- a/dzgui/views/components/entry.py +++ b/dzgui/views/components/entry.py @@ -1,15 +1,16 @@ from typing import Any, Callable, TYPE_CHECKING from dzgui.api.servers import validate_ip -from dzgui.const.constants import VIEW_CONCEAL, VIEW_REVEAL +from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL from dzgui.util.css import add_class, remove_class from dzgui.strings import connect_panel +from dzgui.strings.options import api_placeholder, api_validate from dzgui.strings.errors import api_popover import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GObject # noqa E402 +from gi.repository import Gtk, Gdk, GObject if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -38,7 +39,7 @@ class ValidatedEntry(Gtk.Entry): def __init__( self, controller: "Controller", - func: Callable, + func: Callable[[str], bool], placeholder_text: str = "", tooltip_text: str = "", ) -> None: @@ -46,7 +47,6 @@ class ValidatedEntry(Gtk.Entry): hexpand=True, placeholder_text=placeholder_text, tooltip_text=tooltip_text ) - self.func: Callable self.set_validation_func(func) self.controller = controller @@ -72,7 +72,7 @@ class ValidatedEntry(Gtk.Entry): self.emit("string_validated", False) remove_class(self, self.classname) - def set_validation_func(self, func: Callable) -> None: + def set_validation_func(self, func: Callable[[str], bool]) -> None: self.func = func def insert_icon(self) -> None: @@ -141,21 +141,19 @@ class APIEntry(Gtk.Box): def __init__(self) -> None: super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) - self.func: Callable | None = None + self.func: Callable[[str], None] | None = None - # TODO: strings self.entry = Gtk.Entry( - width_chars=60, hexpand=True, placeholder_text="Enter API key" + width_chars=60, hexpand=True, placeholder_text=api_placeholder ) - self.entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL) + self.entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, ICON_VIEW_REVEAL) self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True) self.entry.set_visibility(False) self.entry.connect("changed", self._on_text_changed) self.entry.connect("icon-release", self._on_icon_release) self.entry.connect("activate", self._on_field_activated) - # TODO: strings - self.submit = Gtk.Button(label="Validate") + self.submit = Gtk.Button(label=api_validate) self.submit.set_sensitive(False) self.submit.connect("clicked", self._on_submit) @@ -206,9 +204,9 @@ class APIEntry(Gtk.Box): ) -> None: visible = widget.get_visibility() if visible: - icon, state = VIEW_REVEAL, False + icon, state = ICON_VIEW_REVEAL, False else: - icon, state = VIEW_CONCEAL, True + icon, state = ICON_VIEW_CONCEAL, True widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) widget.set_visibility(state) @@ -222,7 +220,7 @@ class APIEntry(Gtk.Box): res = self.func(text) return res - def set_validation_func(self, func: Callable | None) -> None: + def set_validation_func(self, func: Callable[[str], None] | None) -> None: self.func = func def disable_button(self) -> None: diff --git a/dzgui/views/components/eventbox.py b/dzgui/views/components/eventbox.py index 5e06127..92c97ca 100644 --- a/dzgui/views/components/eventbox.py +++ b/dzgui/views/components/eventbox.py @@ -1,12 +1,12 @@ from typing import TYPE_CHECKING from dzgui.const.enum import NotebookPage -from dzgui.const.constants import HELP_BUBBLE +from dzgui.const.icons import ICON_HELP_BUBBLE import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk # noqa E402 +from gi.repository import Gtk, Gdk if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -20,7 +20,7 @@ class InfoEventBox(Gtk.EventBox): self.text = text self.icon = Gtk.Image.new_from_icon_name( - HELP_BUBBLE, Gtk.IconSize.LARGE_TOOLBAR + ICON_HELP_BUBBLE, Gtk.IconSize.LARGE_TOOLBAR ) self.icon.set_opacity(0.8) box = Gtk.Box() diff --git a/dzgui/views/components/filter_panel.py b/dzgui/views/components/filter_panel.py index 75c51e9..b3bc584 100644 --- a/dzgui/views/components/filter_panel.py +++ b/dzgui/views/components/filter_panel.py @@ -3,21 +3,22 @@ from typing import Literal, TYPE_CHECKING from dzgui.const.constants import ( APP_NAME, - EXPAND, - NO_EXPAND, - NO_FILL, - NO_PADDING, - SEARCH_ICON, ) +from dzgui.const.icons import ICON_SEARCH from dzgui.util.format import format_exception -from dzgui.util.strings import all_maps +from dzgui.strings.filters import ( + all_maps, + filters_heading, + keyword_placeholder, + map_placeholder, +) from dzgui.views.components.maps_combo import MapsCombo from dzgui.views.components.labels import BoldLabel import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, Pango, GLib # noqa E402 +from gi.repository import Gtk, Gdk, Pango, GLib logger = logging.getLogger(APP_NAME) @@ -30,7 +31,7 @@ class ButtonGrid(Gtk.Grid): def __init__( self, controller: "Controller", - defaults: dict[str, str], + defaults: dict[str, bool], tooltips: dict[str, str], ) -> None: super().__init__( @@ -42,7 +43,10 @@ class ButtonGrid(Gtk.Grid): self.checks: list[Gtk.CheckButton] = [] flowbox = Gtk.FlowBox( - halign=Gtk.Align.CENTER, min_children_per_line=3, max_children_per_line=3, selection_mode=Gtk.SelectionMode.NONE + halign=Gtk.Align.CENTER, + min_children_per_line=3, + max_children_per_line=3, + selection_mode=Gtk.SelectionMode.NONE, ) for check in defaults.keys(): @@ -93,14 +97,13 @@ class ButtonGrid(Gtk.Grid): logger.info(f"User toggled button '{label}' to {state}") self.emitter.emit("check_toggled", label, state) - def get_checkboxes(self) -> list: + def get_checkboxes(self) -> list[Gtk.CheckButton]: return self.checks class KeywordEntry(Gtk.Entry): def __init__(self, controller: "Controller") -> None: - # TODO :strings - super().__init__(placeholder_text="Filter by keyword") + super().__init__(placeholder_text=keyword_placeholder) self.keyword = "" self.controller = controller @@ -109,7 +112,7 @@ class KeywordEntry(Gtk.Entry): self.connect("key-press-event", self._on_keypress) self.connect("icon-release", lambda *args: self.activate()) - self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, SEARCH_ICON) + self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, ICON_SEARCH) self.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True) self.emitter.connect("load_maps", self._on_maps_loaded) @@ -174,8 +177,7 @@ class FilterPanel(Gtk.Box): self.keyword_entry = KeywordEntry(self.controller) self.button_grid = ButtonGrid(self.controller, defaults, tooltips) - # TODO: strings - self.filters_label = BoldLabel("Filters") + self.filters_label = BoldLabel(filters_heading) self.emitter.connect( "request_keyword_focus", lambda _: self.keyword_entry.grab_focus() @@ -195,13 +197,12 @@ class FilterPanel(Gtk.Box): self.maps_entry: Gtk.Entry = self.maps_combo.get_child() # type: ignore self.maps_entry.set_completion(completion) - # TODO: strings - self.maps_entry.set_placeholder_text("Filter by map") + self.maps_entry.set_placeholder_text(map_placeholder) self.maps_entry.connect("changed", self._on_map_completion, True) self.maps_entry.connect("key-press-event", self._on_map_entry_keypress) self.maps_entry.connect("activate", self._on_maps_activated) - self.maps_combo.pack_start(renderer_text, EXPAND) + self.maps_combo.pack_start(renderer_text, expand=True) self.maps_combo.connect("changed", self._on_map_changed) self.maps_combo.connect("key-press-event", self._on_combo_keypress) @@ -211,7 +212,7 @@ class FilterPanel(Gtk.Box): self.maps_combo, self.button_grid, ): - self.pack_start(el, NO_EXPAND, NO_FILL, NO_PADDING) + self.pack_start(el, expand=False, fill=False, padding=0) def _on_maps_activated(self, entry: Gtk.Entry) -> None: text = entry.get_text() @@ -305,6 +306,7 @@ class FilterPanel(Gtk.Box): Gdk.KEY_0: 9, Gdk.KEY_minus: 10, Gdk.KEY_backslash: 11, + Gdk.KEY_apostrophe: 12, } if keyval not in mappings: return False diff --git a/dzgui/views/components/icon.py b/dzgui/views/components/icon.py index 2cb222d..54b88d9 100644 --- a/dzgui/views/components/icon.py +++ b/dzgui/views/components/icon.py @@ -1,7 +1,7 @@ import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk class Icon(Gtk.Image): diff --git a/dzgui/views/components/labels.py b/dzgui/views/components/labels.py index 9ecc52a..a9c7db0 100644 --- a/dzgui/views/components/labels.py +++ b/dzgui/views/components/labels.py @@ -3,7 +3,7 @@ from dzgui.util.format import embolden import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk class BoldLabel(Gtk.Label): diff --git a/dzgui/views/components/maps_combo.py b/dzgui/views/components/maps_combo.py index 0eea10f..821b831 100644 --- a/dzgui/views/components/maps_combo.py +++ b/dzgui/views/components/maps_combo.py @@ -1,7 +1,7 @@ import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib # noqa E402 +from gi.repository import Gtk class MapsCombo(Gtk.EntryCompletion): diff --git a/dzgui/views/components/misc.py b/dzgui/views/components/misc.py index cdc3f49..80e694e 100644 --- a/dzgui/views/components/misc.py +++ b/dzgui/views/components/misc.py @@ -1,7 +1,8 @@ from typing import Self from warnings import deprecated -from dzgui.const.constants import ERROR, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD +from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD +from dzgui.const.icons import ICON_ERROR from dzgui.init.prereqs import has_steam_client, has_flatpak_client from dzgui.strings import options from dzgui.views.components.box import HBox @@ -73,7 +74,7 @@ class ErrorPopover(Gtk.Popover): self.hbox = HBox() self.label = Gtk.Label(label="", margin_start=10, margin_end=10) - error_icon = Icon(ERROR, margin_start=10) + error_icon = Icon(ICON_ERROR, margin_start=10) self.hbox.extend([error_icon, self.label]) self.add(self.hbox) self.show_all() diff --git a/dzgui/views/components/mod_panel.py b/dzgui/views/components/mod_panel.py index 7c913c4..7f7d7ed 100644 --- a/dzgui/views/components/mod_panel.py +++ b/dzgui/views/components/mod_panel.py @@ -1,14 +1,13 @@ from typing import Self, TYPE_CHECKING from dzgui.const.enum import ModButton -from dzgui.const.constants import NO_EXPAND, FILL, NO_PADDING from dzgui.util.strings import mod_panel from dzgui.views.components.labels import BoldLabel import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -20,8 +19,8 @@ class EnumeratedModButton(Gtk.Button): super().__init__(margin_start=10, margin_end=10, focus_on_click=False) self.enum = enum - self.set_label(enum.dict["label"]) - self.set_tooltip_text(enum.dict["tooltip"]) + self.set_label(enum.label) + self.set_tooltip_text(enum.tooltip) class ModPanelButton(EnumeratedModButton): @@ -55,7 +54,7 @@ class ModSelectionPanel(Gtk.Box): for button in buttons: b = EnumeratedModButton(button) b.connect("clicked", self._on_button_clicked) - self.main_panel.pack_start(b, NO_EXPAND, FILL, NO_PADDING) + self.main_panel.pack_start(b, expand=False, fill=True, padding=0) self.stale_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) self.highlight_stale_button = ModPanelButton(ModButton.HIGHLIGHT_STALE) @@ -67,11 +66,11 @@ class ModSelectionPanel(Gtk.Box): self.unhighlight_stale_button, self.select_stale_button, ): - self.stale_panel.pack_start(b, NO_EXPAND, FILL, NO_PADDING) + self.stale_panel.pack_start(b, expand=False, fill=True, padding=0) b.connect("clicked", self._on_button_clicked) for el in header, self.main_panel, self.stale_panel: - self.pack_start(el, NO_EXPAND, FILL, NO_PADDING) + self.pack_start(el, expand=False, fill=True, padding=0) self.connect("map", self._reinit_button) diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index f38def4..c04bba7 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -6,13 +6,10 @@ from typing import Literal, TYPE_CHECKING from dzgui.const.constants import ( APP_NAME, - NO_EXPAND, - NO_FILL, - FILL, - NO_PADDING, ) from dzgui.const.enum import ServerTab from dzgui.managers.update import UpdateManager +from dzgui.strings.buttons import update_button, version_notifier, version_tooltip from dzgui.util.clip import copy_clipboard from dzgui.util.file import is_writeable from dzgui.views.components.buttonbox import ButtonBox @@ -23,7 +20,7 @@ from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysBu import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GLib # noqa E402 +from gi.repository import Gtk, Gdk, GLib if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -65,20 +62,19 @@ class RightPanel(Gtk.Box): vexpand=True, halign=Gtk.Align.END, valign=Gtk.Align.END, - # TODO: strings - tooltip_text="Click to copy to clipboard", + tooltip_text=version_tooltip, ) eb = Gtk.EventBox(halign=Gtk.Align.END, valign=Gtk.Align.END) eb.add(self.version_label) eb.connect("button-press-event", self._on_version_clicked) for el in self.button_vbox, self.keys, self.filters_vbox, self.refresh_button: - self.pack_start(el, NO_EXPAND, FILL, NO_PADDING) + self.pack_start(el, expand=False, fill=True, padding=0) - self.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING) + self.pack_start(self.sel_panel, expand=False, fill=False, padding=0) self.update_button = IconTextButton( - "dialog-information-symbolic", label="Updates available" + "dialog-information-symbolic", label=update_button ) self.gutter_box = Gtk.Box( @@ -96,7 +92,7 @@ class RightPanel(Gtk.Box): "clicked", self._on_update_button_clicked, exe_path, update ) self.gutter_box.add(eb) - self.pack_start(self.gutter_box, NO_EXPAND, FILL, NO_PADDING) + self.pack_start(self.gutter_box, expand=False, fill=True, padding=0) def _on_update_button_clicked( self, button: Gtk.Button, exe_path: str, url: str @@ -131,7 +127,6 @@ class RightPanel(Gtk.Box): return self.copying = True version = self.version_label.get_text() - # TODO: strings - self.version_label.set_text("Copied!") + self.version_label.set_text(version_notifier) copy_clipboard(version) GLib.timeout_add_seconds(1, revert) diff --git a/dzgui/views/components/statusbar.py b/dzgui/views/components/statusbar.py index 7eb75a3..43e5ff7 100644 --- a/dzgui/views/components/statusbar.py +++ b/dzgui/views/components/statusbar.py @@ -2,12 +2,11 @@ from typing import Self, Union, TYPE_CHECKING from dzgui.const.enum import NotebookPage, ServerTab from dzgui.util.strings import esc_to_return, question_to_return -from dzgui.views.components.buttons import LoggerAlertsButton import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk if TYPE_CHECKING: from dzgui.const.enum import ServerTab @@ -47,9 +46,6 @@ class Statusbar(Gtk.Grid): self.emitter.connect("servers_loaded", self._on_servers_loaded) self.emitter.connect("mods_updated", self._on_mods_updated) - def _on_alerts_clicked(self, button: LoggerAlertsButton) -> None: - self.controller.populate_log() - def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None: self.set_by_context(NotebookPage.MODS, msg) @@ -60,7 +56,7 @@ class Statusbar(Gtk.Grid): return enum = notebook.get_page_by_enum() - show_statusbar = enum.dict["statusbar"] + show_statusbar = enum.has_statusbar if show_statusbar is False: self.set_by_context(enum, "") @@ -152,5 +148,5 @@ class Statusbar(Gtk.Grid): row = tree.get_value_at_index(1) if self.controller.loaded is False: return - tooltip = row.dict["tooltip"] + tooltip = row.tooltip self.set_by_context(NotebookPage.HELP, tooltip) diff --git a/dzgui/views/components/toast.py b/dzgui/views/components/toast.py index d6fd577..95e663f 100644 --- a/dzgui/views/components/toast.py +++ b/dzgui/views/components/toast.py @@ -2,8 +2,9 @@ from typing import Literal from dzgui.util import css import gi + gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib # noqa E402 +from gi.repository import Gtk, GLib class Toast(Gtk.EventBox): diff --git a/dzgui/views/dialogs/boot.py b/dzgui/views/dialogs/boot.py index d4aa557..bfc0b1e 100644 --- a/dzgui/views/dialogs/boot.py +++ b/dzgui/views/dialogs/boot.py @@ -14,7 +14,6 @@ from dzgui.const.constants import HEX_GREEN, HEX_RED from dzgui.init.coords import get_local_coords from dzgui.init.dayz import is_dayz_installed from dzgui.init.update import check_updates -from dzgui.const.constants import EXPAND, FILL from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.strings import preboot, dialogs from dzgui.util.format import format_exception @@ -105,7 +104,7 @@ class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore self.error_label = Gtk.Label(margin_start=15, margin_end=15) # TODO: abstract class - self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text()) + self.copy_button = ClipboardButton(self.error_label.get_text) self.button_hbox = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.CENTER, spacing=10 ) @@ -118,8 +117,8 @@ class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore self.error_box.add(self.button_hbox) self.content = self.get_content_area() - self.content.pack_start(self.scrollable_tree, EXPAND, FILL, 0) - self.content.pack_start(self.error_box, EXPAND, FILL, 0) + self.content.pack_start(self.scrollable_tree, expand=True, fill=True, padding=0) + self.content.pack_start(self.error_box, expand=True, fill=True, padding=0) self.show_all() self.error_box.hide() diff --git a/dzgui/views/dialogs/calc.py b/dzgui/views/dialogs/calc.py index 979cb0a..cf7e3be 100644 --- a/dzgui/views/dialogs/calc.py +++ b/dzgui/views/dialogs/calc.py @@ -3,14 +3,14 @@ from dataclasses import dataclass from datetime import datetime, time, timedelta from typing import Literal -from dzgui.const.constants import DAY_ICON, NIGHT_ICON +from dzgui.const.icons import ICON_DAY, ICON_NIGHT from dzgui.strings import calc from dzgui.views.components.icon import Icon from dzgui.views.components.labels import BoldLabel from dzgui.views.components.box import VBox, HBox gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, GObject # type: ignore # noqa E402 +from gi.repository import Gtk, GLib, GObject DAY_START_TIME = time(hour=6, minute=0, second=0) DAY_END_TIME = time(hour=18, minute=0, second=0) @@ -385,7 +385,7 @@ class TimePickerFrame(Gtk.Frame): h1 = BoldLabel(calc.heading_server_time) h2 = BoldLabel(calc.heading_target_time) - self.icon_indicator = Icon(DAY_ICON) + self.icon_indicator = Icon(ICON_DAY) self.update_server_icon() server_time_box = HBox(10) @@ -411,7 +411,7 @@ class TimePickerFrame(Gtk.Frame): self.add(flowbox) def update_server_icon(self) -> None: - icon = DAY_ICON if self.server_time.is_day() else NIGHT_ICON + icon = ICON_DAY if self.server_time.is_day() else ICON_NIGHT self.icon_indicator.set_icon_name(icon) def _on_server_clock_increment(self, emitter: Emitter, time_now: datetime) -> None: diff --git a/dzgui/views/dialogs/early_alert.py b/dzgui/views/dialogs/early_alert.py index b7032a0..c861725 100644 --- a/dzgui/views/dialogs/early_alert.py +++ b/dzgui/views/dialogs/early_alert.py @@ -8,7 +8,7 @@ from dzgui.views.mixins.colorscheme import ColorAwareApp import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk class AbortDialog(ColorAwareApp, Gtk.MessageDialog): # type: ignore @@ -46,7 +46,7 @@ class AbortDialog(ColorAwareApp, Gtk.MessageDialog): # type: ignore self.connect("response", self._on_response) - self.run() + self.run() # type: ignore[no-untyped-call] self.destroy() def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None: diff --git a/dzgui/views/dialogs/filepicker.py b/dzgui/views/dialogs/filepicker.py index 2048598..7951ae7 100644 --- a/dzgui/views/dialogs/filepicker.py +++ b/dzgui/views/dialogs/filepicker.py @@ -2,10 +2,10 @@ from pathlib import Path from dzgui.util.strings import picker -import gi # noqa E402 +import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk class FilePicker(Gtk.FileChooserDialog): @@ -16,14 +16,14 @@ class FilePicker(Gtk.FileChooserDialog): transient_for=parent, resizable=True, ) - self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) - self.add_buttons("_Save", Gtk.ResponseType.OK) + self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) # type: ignore[no-untyped-call] + self.add_buttons("_Save", Gtk.ResponseType.OK) # type: ignore[no-untyped-call] self.set_default_response(Gtk.ResponseType.OK) self.set_current_name(picker.placeholder) def pick_file(self) -> Path | None: - res = self.run() + res = self.run() # type: ignore[no-untyped-call] if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT): self.destroy() return None @@ -43,12 +43,12 @@ class FolderPicker(Gtk.FileChooserDialog): parent=parent, resizable=True, ) - self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) - self.add_buttons("_OK", Gtk.ResponseType.OK) + self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) # type: ignore[no-untyped-call] + self.add_buttons("_OK", Gtk.ResponseType.OK) # type: ignore[no-untyped-call] self.set_default_response(Gtk.ResponseType.OK) def pick_folder(self) -> Path | None: - res = self.run() + res = self.run() # type: ignore[no-untyped-call] if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT): self.destroy() return None diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index cff1e2a..da46346 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -1,6 +1,5 @@ from typing import Literal, Self, TYPE_CHECKING -from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL from dzgui.strings import dialogs from dzgui.util import strings from dzgui.views.components.buttons import ClipboardButton @@ -8,7 +7,7 @@ from dzgui.views.components.buttons import ClipboardButton import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402 +from gi.repository import Gtk, GLib if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -46,7 +45,7 @@ class GenericDialog(Gtk.MessageDialog): def pack(self, widget: Gtk.Widget) -> None: content = self.get_content_area() - content.pack_start(widget, EXPAND, FILL, NO_PADDING) + content.pack_start(widget, expand=True, fill=True, padding=0) class ConfirmationDialog(GenericDialog): @@ -121,13 +120,13 @@ class WaitDialog(GenericDialog): spinner_box.add(spinner) if self.jobs > 1: - content.pack_start(self.prog, NO_EXPAND, NO_FILL, 0) + content.pack_start(self.prog, expand=False, fill=False, padding=0) else: - content.pack_start(spinner_box, NO_EXPAND, NO_FILL, 0) + content.pack_start(spinner_box, expand=False, fill=False, padding=0) spinner.start() if show_cancel is True: - content.pack_start(self.cancel, NO_EXPAND, NO_FILL, 0) + content.pack_start(self.cancel, expand=False, fill=False, padding=0) def update_text(self, msg: str) -> None: self.format_secondary_text(msg) @@ -140,8 +139,7 @@ class WaitDialog(GenericDialog): self.cur_job += 1 def _on_cancel_clicked(self, button: Gtk.Button) -> None: - # TODO: strings - GLib.idle_add(self.update_text, "Caught cancel signal, cleaning up") + GLib.idle_add(self.update_text, dialogs.caught_cancel_signal) self.controller.set_cancel_event() def show_cancel(self, state: bool) -> None: @@ -187,7 +185,7 @@ class TextBufferDialog(GenericDialog): secondary=secondary, ) self.text = text - copy_button = ClipboardButton(controller, self.get_text) + copy_button = ClipboardButton(self.get_text) self.add_action_widget(copy_button, Gtk.ResponseType.NONE) self.add_button("OK", Gtk.ResponseType.OK) self.connect("response", self._on_response) @@ -272,7 +270,7 @@ class ExceptionDialog(TextBufferDialog): bottom_margin=10, ) textview.set_buffer(Gtk.TextBuffer(text=trace)) - box.pack_start(textview, EXPAND, FILL, 0) + box.pack_start(textview, expand=True, fill=True, padding=0) self.error_details = Gtk.ScrolledWindow( overlay_scrolling=False, @@ -296,7 +294,7 @@ class ExceptionDialog(TextBufferDialog): ) details_textview.set_buffer(self.details_buffer) - details_box.pack_start(details_textview, EXPAND, FILL, 10) + details_box.pack_start(details_textview, expand=True, fill=True, padding=10) self.error_details.add(details_box) self.error_notebook = Gtk.Notebook(show_tabs=False, margin_bottom=15) diff --git a/dzgui/views/dialogs/link_dialog.py b/dzgui/views/dialogs/link_dialog.py index 294765c..32c39ec 100644 --- a/dzgui/views/dialogs/link_dialog.py +++ b/dzgui/views/dialogs/link_dialog.py @@ -1,7 +1,6 @@ import textwrap from typing import Self, TYPE_CHECKING -from dzgui.const.constants import NO_EXPAND, NO_FILL from dzgui.util.strings import notice from dzgui.views.dialogs.generic import GenericDialog from dzgui.views.components.buttons import SteamTextButton @@ -9,7 +8,7 @@ from dzgui.views.components.buttons import SteamTextButton import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402 +from gi.repository import Gtk if TYPE_CHECKING: @@ -42,7 +41,7 @@ class WorkshopLinkDialog(GenericDialog): button.set_margin_start(60) button.set_margin_end(60) button.connect("clicked", self._on_button_clicked, uid) - self.dialogBox.pack_end(button, NO_EXPAND, NO_FILL, 0) + self.dialogBox.pack_end(button, expand=False, fill=False, padding=0) self.show_all() self.connect("response", self._on_dialog_response) diff --git a/dzgui/views/dialogs/note.py b/dzgui/views/dialogs/note.py index b595e90..362f1d8 100644 --- a/dzgui/views/dialogs/note.py +++ b/dzgui/views/dialogs/note.py @@ -1,12 +1,13 @@ from typing import Self, TYPE_CHECKING +from dzgui.strings import notes from dzgui.views.components.entry import ValidatedEntry from dzgui.views.dialogs.generic import GenericDialog -import gi # noqa E402 +import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gdk, Gtk # noqa E402 +from gi.repository import Gtk if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -14,23 +15,20 @@ if TYPE_CHECKING: class NoteDialog(GenericDialog): def __init__(self, controller: "Controller"): - # TODO: add to strings - title = "Add a short note/reminder." - secondary = "Limit 30 chars" super().__init__( controller=controller, - text=title, + text=notes.dialog_title, buttons=Gtk.ButtonsType.NONE, mtype=Gtk.MessageType.INFO, - secondary=secondary, + secondary=notes.dialog_subtitle, ) self.controller = controller - delete_button = Gtk.Button(label="Delete note") + delete_button = Gtk.Button(label=notes.delete_button) delete_button.set_sensitive(False) self.add_action_widget(delete_button, Gtk.ResponseType.CANCEL) - self.ok = self.add_button("Update note", Gtk.ResponseType.OK) + self.ok = self.add_button(notes.ok_button, Gtk.ResponseType.OK) self.ok.grab_default() self.entry = ValidatedEntry(controller, self.validate) @@ -42,7 +40,7 @@ class NoteDialog(GenericDialog): self.entry.set_text(note) if len(note) > 0: delete_button.set_sensitive(True) - self.format_secondary_text(f"Current note: '{note}'") + self.format_secondary_text(notes.current_note.format(note)) elif len(note) == 0: self.ok.set_sensitive(False) @@ -55,9 +53,6 @@ class NoteDialog(GenericDialog): self.entry.connect("changed", self._on_text_changed) def _on_text_changed(self, entry: Gtk.Entry) -> None: - """ - Called in conjunction with signal callback in the parent class - """ if len(entry.get_text()) == 0: self.ok.set_sensitive(False) @@ -71,14 +66,11 @@ class NoteDialog(GenericDialog): def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None: match response: - case Gtk.ResponseType.DELETE_EVENT: - self.destroy() case Gtk.ResponseType.OK: note = self.entry.get_text() self.controller.add_note(note) - self.destroy() case Gtk.ResponseType.CANCEL: self.controller.delete_note() - self.destroy() - case Gtk.ResponseType.CANCEL | Gtk.ResponseType.CLOSE: - self.destroy() + case _: + pass + self.destroy() diff --git a/dzgui/views/dialogs/servers.py b/dzgui/views/dialogs/servers.py index 6166aee..b58d352 100644 --- a/dzgui/views/dialogs/servers.py +++ b/dzgui/views/dialogs/servers.py @@ -2,7 +2,7 @@ import gi import html from typing import Self, TYPE_CHECKING -from dzgui.const.constants import CALCULATOR +from dzgui.const.icons import ICON_CALCULATOR from dzgui.const.enum import ContextMenuGroup from dzgui.model.model_factory import ModelFactory from dzgui.util import css @@ -110,7 +110,7 @@ class ServerDetailsDialog(ServerDialog): text = format_hyperlinks(text) self.description.set_markup(html.escape(text)) - calc_button = IconTextButton(CALCULATOR, dialogs.toggle_calc) + calc_button = IconTextButton(ICON_CALCULATOR, dialogs.toggle_calc) calc_button.connect("clicked", self._on_calc_pressed) calc_button.set_halign(Gtk.Align.END) calc_button.set_valign(Gtk.Align.START) diff --git a/dzgui/views/dialogs/uninstall.py b/dzgui/views/dialogs/uninstall.py index 3e7d2e7..8b25510 100644 --- a/dzgui/views/dialogs/uninstall.py +++ b/dzgui/views/dialogs/uninstall.py @@ -18,11 +18,11 @@ from dzgui.views.mixins.colorscheme import ColorAwareApp import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Pango # noqa E402 +from gi.repository import Gtk, GLib, Pango class Assistant(Gtk.Assistant): - def __init__(self, is_deck: bool, paths: dict[str, str]): + def __init__(self, is_deck: bool, paths: dict[str, Path]): super().__init__() if is_deck: # NOTE: deemed to be "safe" dimensions that exclude taskbar size @@ -63,7 +63,7 @@ class Assistant(Gtk.Assistant): class Window(Gtk.Window): - def __init__(self, is_deck: bool, paths: dict[str, str]) -> None: + def __init__(self, is_deck: bool, paths: dict[str, Path]) -> None: super().__init__(title=APP_NAME, icon_name=APP_NAME) self.assistant = Assistant(is_deck, paths) @@ -121,7 +121,7 @@ class CheckboxWithPath(CheckboxWithLabel): class UninstallPage(ScrolledWizardPage): - def __init__(self, paths: dict[str, str], pyapp: str | None): + def __init__(self, paths: dict[str, Path], pyapp: str | None): super().__init__(heading=uninstall.pg1_heading, description=uninstall.pg1_blurb) self.pyapp = pyapp @@ -219,7 +219,7 @@ class UninstallPage(ScrolledWizardPage): class UninstallWizard(ColorAwareApp, Gtk.Application): # type: ignore - def __init__(self, is_deck: bool, paths: dict[str, str]) -> None: + def __init__(self, is_deck: bool, paths: dict[str, Path]) -> None: super().__init__() config = Path(paths["XDG_CONFIG_HOME"]) if not config.is_dir() or not config.joinpath("config.json").is_file(): diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index 5dbcdb4..82d84f0 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -14,7 +14,7 @@ from dzgui.const.constants import ( HERO_PATH, FLATPAK_ROOT_PATH, LEGACY_CONFIG_PATH, - TMP_PATH + TMP_PATH, ) from dzgui.const.boilerplate import config_boilerplate from dzgui.const.endpoints import STEAM_API_SETUP @@ -35,7 +35,7 @@ from dzgui.views.trees.tree_simple import SimpleTree import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402 +from gi.repository import Gtk, GLib, GObject, GdkPixbuf if TYPE_CHECKING: from dzgui.config.xdg import Xdg @@ -168,7 +168,12 @@ class NotificationFrame(Gtk.Frame): class APIValidationPage(EnumeratedWizardPage): def __init__( - self, enum: PageNum, heading: str, description: str, link: str, func: Callable + self, + enum: PageNum, + heading: str, + description: str, + link: str, + func: Callable[[str], None], ) -> None: super().__init__( enum=enum, @@ -421,7 +426,7 @@ class PreferencesPage(EnumeratedWizardPage): class FlatpakOverridePage(EnumeratedWizardPage): - def __init__(self, xdg_paths: dict[str, str]) -> None: + def __init__(self, xdg_paths: dict[str, Path]) -> None: super().__init__( enum=PageNum.FLATPAK, heading=wizard.heading_flatpak, @@ -471,7 +476,7 @@ class CompletionPage(EnumeratedWizardPage): class Assistant(Gtk.Assistant): - def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]): + def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, Path]): super().__init__() if is_deck: # NOTE: deemed to be "safe" dimensions that exclude taskbar size @@ -776,7 +781,7 @@ class SteamPathPage(EnumeratedWizardPage): class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore - def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None: + def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, Path]) -> None: super().__init__() GLib.set_prgname(APP_NAME) self.win = Window(is_deck, XDG, xdg_paths) @@ -787,7 +792,7 @@ class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore class Window(Gtk.Window): - def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None: + def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, Path]) -> None: super().__init__(title=APP_NAME, icon_name=APP_NAME) self.assistant = Assistant(is_deck, XDG, xdg_paths) diff --git a/dzgui/views/mixins/context_mixin.py b/dzgui/views/mixins/context_mixin.py index 5befa2c..3334364 100644 --- a/dzgui/views/mixins/context_mixin.py +++ b/dzgui/views/mixins/context_mixin.py @@ -1,7 +1,7 @@ import logging from dzgui.const.constants import APP_NAME -from dzgui.const.enum import ContextMenu +from dzgui.const.enum import ContextMenuItem from dzgui.util.keys import is_ctrl_mask, is_navkey from dzgui.util.strings import edit_note from dzgui.views.trees.tree_base import TreeView @@ -9,15 +9,13 @@ 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 E402 +from gi.repository import Gtk, Gdk logger = logging.getLogger(APP_NAME) class ContextMixin(TreeView): - def present_menu( - self, widget: Gtk.Widget, event: Gdk.Event - ) -> bool: + def present_menu(self, widget: Gtk.Widget, event: Gdk.Event) -> bool: if self.menu is None: return False @@ -26,7 +24,7 @@ class ContextMixin(TreeView): match event.type: case Gdk.EventType.BUTTON_PRESS: - if event.button != 3: + if event.button != Gdk.BUTTON_SECONDARY: # type: ignore[comparison-overlap] return False self._process_button_event(event) # type: ignore case Gdk.EventType.KEY_PRESS: @@ -60,17 +58,17 @@ class ContextMixin(TreeView): self.context_menu.select_first(False) return True - 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 + def _process_dynamic_row(self, row: ContextMenuItem) -> Gtk.MenuItem: + if row == ContextMenuItem.ADD_SERVER and self.is_in_favs(): # type: ignore + row = ContextMenuItem.REMOVE_SERVER - item = Gtk.MenuItem(label=row.dict["label"]) + item = Gtk.MenuItem(label=row.label) item.connect("activate", self._on_menu_click, row) - if row == ContextMenu.SHOW_MODS: + if row == ContextMenuItem.SHOW_MODS: item.set_sensitive(self.is_modded()) # type: ignore - if row == ContextMenu.ADD_NOTE: + if row == ContextMenuItem.ADD_NOTE: if self.controller.has_note(): item.set_label(edit_note) @@ -94,7 +92,7 @@ class ContextMixin(TreeView): except AttributeError: return False - def _on_menu_click(self, widget: Gtk.MenuItem, enum: ContextMenu) -> None: + def _on_menu_click(self, widget: Gtk.MenuItem, enum: ContextMenuItem) -> None: self.controller.menu_action(enum, self) # type: ignore def _on_key(self, menu: Gtk.Menu, event: Gdk.EventKey) -> bool | None: diff --git a/dzgui/views/mixins/cursor_mixin.py b/dzgui/views/mixins/cursor_mixin.py index 71d4851..0690e19 100644 --- a/dzgui/views/mixins/cursor_mixin.py +++ b/dzgui/views/mixins/cursor_mixin.py @@ -5,7 +5,7 @@ from dzgui.util.keys import is_ctrl_mask import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402 +from gi.repository import Gtk, Gdk class CursorMixin: diff --git a/dzgui/views/mixins/mods_mixin.py b/dzgui/views/mixins/mods_mixin.py index 6cc7da8..1b76321 100644 --- a/dzgui/views/mixins/mods_mixin.py +++ b/dzgui/views/mixins/mods_mixin.py @@ -3,7 +3,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 E402 +from gi.repository import Gtk class ModsMixin: diff --git a/dzgui/views/pages/changelog.py b/dzgui/views/pages/changelog.py index 108912c..86a7d32 100644 --- a/dzgui/views/pages/changelog.py +++ b/dzgui/views/pages/changelog.py @@ -15,7 +15,7 @@ from dzgui.views.mixins.scrollable_mixin import ScrollableMixin import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk logger = logging.getLogger(APP_NAME) diff --git a/dzgui/views/pages/devs.py b/dzgui/views/pages/devs.py index 7c95255..3eccae8 100644 --- a/dzgui/views/pages/devs.py +++ b/dzgui/views/pages/devs.py @@ -7,10 +7,10 @@ from dzgui.util.strings import developers from dzgui.views.components.labels import BoldLabel from dzgui.views.trees.tree_base import TreeView -import gi # noqa E402 +import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk if TYPE_CHECKING: from dzgui.controllers.mc import Controller diff --git a/dzgui/views/pages/help.py b/dzgui/views/pages/help.py index bcfbfd8..e4b1f17 100644 --- a/dzgui/views/pages/help.py +++ b/dzgui/views/pages/help.py @@ -4,7 +4,7 @@ from dzgui.views.trees.tree_menu import MenuTreeView import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk if TYPE_CHECKING: from dzgui.controllers.mc import Controller diff --git a/dzgui/views/pages/keys.py b/dzgui/views/pages/keys.py index ea12476..ddd630e 100644 --- a/dzgui/views/pages/keys.py +++ b/dzgui/views/pages/keys.py @@ -7,12 +7,13 @@ from dzgui.util.css import add_class import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk if TYPE_CHECKING: from dzgui.controllers.mc import Controller +# TODO: refactor into autopopulated ListStores class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore """ Notebook page holding a prearranged grid @@ -35,7 +36,7 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore self.connect("key-press-event", self._on_keypress) - def build_keys(self, items: list) -> Gtk.Grid: + def build_keys(self, items: list[dict[str, str]]) -> Gtk.Grid: grid = Gtk.Grid(row_spacing=10, column_homogeneous=True) grid.set_halign(Gtk.Align.START) row, col = 1, 0 @@ -72,7 +73,7 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore row += 1 return grid - def build_sidebar(self, categories: list) -> Gtk.Grid: + def build_sidebar(self, categories: list[str]) -> Gtk.Grid: row, col = 0, 0 w, h = 1, 1 sidebar = Gtk.Grid(row_homogeneous=True, orientation=Gtk.Orientation.VERTICAL) @@ -83,7 +84,7 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore sidebar.attach(label, col, row, w, h) return sidebar - def build_grid(self, items: list) -> Gtk.Grid: + def build_grid(self, items: list[dict[str, str]]) -> Gtk.Grid: grid = Gtk.Grid( row_spacing=20, halign=Gtk.Align.CENTER, diff --git a/dzgui/views/pages/log.py b/dzgui/views/pages/log.py index a9e1bd5..aad06a6 100644 --- a/dzgui/views/pages/log.py +++ b/dzgui/views/pages/log.py @@ -9,7 +9,7 @@ from dzgui.views.trees.tree_log import LogTreeView import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +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 b0200b0..ceb5686 100644 --- a/dzgui/views/pages/offline.py +++ b/dzgui/views/pages/offline.py @@ -3,14 +3,12 @@ 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, APPNAME_DAYZ, APPNAME_DAYZ_EXP_HUMAN, - EDIT_DELETE, - FOLDER, - WARNING, ) from dzgui.const.enum import ContextMenuGroup, NotebookPage from dzgui.managers.offline import OfflineManager @@ -87,7 +85,7 @@ class FolderHBox(HBox): self.emitter = controller.get_emitter() self.eb = InfoEventBox(eb_text, controller) - self.button = IconTextButton(FOLDER, btn_label, Gtk.PositionType.LEFT) + self.button = IconTextButton(ICON_FOLDER, btn_label, Gtk.PositionType.LEFT) self.button.set_halign(Gtk.Align.START) self.button.set_image_position(Gtk.PositionType.LEFT) @@ -99,7 +97,7 @@ class FolderHBox(HBox): self.label = Gtk.Label() self.scrolled_label.add(self.label) - self.unset_button = IconTextButton(EDIT_DELETE, offline.unset_button) + self.unset_button = IconTextButton(ICON_EDIT_DELETE, offline.unset_button) self.unset_button.connect("clicked", self._on_unset_clicked) self.spinner = Gtk.Spinner() @@ -195,7 +193,7 @@ class ModFrame(HeadingFrame): self.tree_vbox.extend([self.scrolled, self.status]) self.no_mods = HBox(spacing=10) - no_mods_icon = Icon(WARNING, margin_start=10) + no_mods_icon = Icon(ICON_WARNING, margin_start=10) no_mods_label = Gtk.Label(label=offline.no_local_mods, halign=Gtk.Align.START) self.no_mods.extend([no_mods_icon, no_mods_label]) diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index b5633ca..1fa3fbb 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Callable, TYPE_CHECKING +from typing import Any, Callable, TYPE_CHECKING from dzgui.api import pefile as PeFile from dzgui.api.steam import get_last_updated @@ -13,12 +13,8 @@ from dzgui.const.constants import ( FLATPAK_RUN_CMD, FLATPAK_SANDBOX, FLATPAK_STEAM_PATH, - NO_EXPAND, - NO_FILL, - NO_PADDING, - VIEW_CONCEAL, - VIEW_REVEAL, ) +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.strings import options @@ -55,7 +51,7 @@ class SubmitField(Gtk.Box): ) -> None: super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) - self.callback: Callable + self.callback: Callable[[], None] self.controller = controller self.context = context @@ -67,7 +63,7 @@ class SubmitField(Gtk.Box): # TODO: audit for all applicable callbacks if private: self.entry.set_icon_from_icon_name( - Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL + Gtk.EntryIconPosition.SECONDARY, ICON_VIEW_REVEAL ) self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True) self.entry.connect("icon-release", self._on_icon_release) @@ -131,9 +127,9 @@ class SubmitField(Gtk.Box): ) -> None: visible = widget.get_visibility() if visible: - icon, state = VIEW_REVEAL, False + icon, state = ICON_VIEW_REVEAL, False else: - icon, state = VIEW_CONCEAL, True + icon, state = ICON_VIEW_CONCEAL, True widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) widget.set_visibility(state) @@ -143,7 +139,7 @@ class SubmitField(Gtk.Box): return self.save_option() - def set_callback(self, callback: Callable) -> None: + def set_callback(self, callback: Callable[[], None]) -> None: self.callback = callback def save_option(self) -> None: @@ -210,7 +206,7 @@ class NameSubmitField(SubmitField): Preferences.NAME, private=False, ) - self.entry.set_width_chars(30) # type: ignore + self.entry.set_width_chars(30) self.set_callback(self.save_player_name) def save_player_name(self) -> None: @@ -234,8 +230,8 @@ class ToggleField(Gtk.Box): self.radio1 = Gtk.RadioButton.new_with_label(None, first_option) self.radio2 = Gtk.RadioButton.new_from_widget(self.radio1) self.radio2.set_label(second_option) - self.pack_start(self.radio1, NO_EXPAND, NO_FILL, NO_PADDING) - self.pack_start(self.radio2, NO_EXPAND, NO_FILL, NO_PADDING) + self.pack_start(self.radio1, expand=False, fill=False, padding=0) + self.pack_start(self.radio2, expand=False, fill=False, padding=0) def set_suboption_active(self, state: bool) -> None: # NOTE: defer connection of signal until after state is set @@ -406,7 +402,8 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None: open_links.open_link_by_url(url) - def _make_grid(self, rows: list) -> Gtk.Grid: + # TODO: use FlowBox + def _make_grid(self, rows: list[list[Any]]) -> Gtk.Grid: grid = Gtk.Grid( orientation=Gtk.Orientation.VERTICAL, column_spacing=10, @@ -439,7 +436,7 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore if prefs.paths.config.is_file() is False: dialog = ExceptionDialog(self.controller, strings.config_not_found) dialog.show_all() - dialog.run() + dialog.run() # type: ignore[no-untyped-call] raise OSError(f"Config file '{prefs.paths.config}' not found") config = query.get_config(prefs.paths.config) diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index b737ff0..3f54b31 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -1,9 +1,9 @@ from dataclasses import dataclass from typing import Self, Sequence, TYPE_CHECKING -from dzgui.const.constants import ( - ERROR, - WARNING, +from dzgui.const.icons import ( + ICON_ERROR, + ICON_WARNING, ) from dzgui.const.enum import NotebookPage from dzgui.util.css import add_class @@ -18,7 +18,7 @@ from dzgui.views.trees.tree_server_mods import ServerModTreeView import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gdk, Gtk # noqa E402 +from gi.repository import Gdk, Gtk if TYPE_CHECKING: @@ -166,7 +166,7 @@ class PreConnectionAssistant(Gtk.Box): # TODO: abstract into components self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.warning_tree = MaskedTree(WARNING) + self.warning_tree = MaskedTree(ICON_WARNING) self.warning_placeholder = Placeholder(preconnect.placeholder_no_warnings) self.warning_box.add(self.warning_tree) self.warning_box.add(self.warning_placeholder) @@ -175,7 +175,7 @@ class PreConnectionAssistant(Gtk.Box): ) self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.error_tree = MaskedTree(ERROR) + self.error_tree = MaskedTree(ICON_ERROR) self.error_placeholder = Placeholder(preconnect.no_errors) self.error_box.add(self.error_tree) self.error_box.add(self.error_placeholder) @@ -231,7 +231,7 @@ class PreConnectionAssistant(Gtk.Box): args = self.controller.get_debug_args() d = DebugDialog(self.controller, args) d.show_all() - d.run() + d.run() # type: ignore[no-untyped-call] def _on_ok_clicked(self, button: Gtk.Button) -> None: self.controller.update_and_connect() diff --git a/dzgui/views/pages/servers.py b/dzgui/views/pages/servers.py index caefe21..92090ba 100644 --- a/dzgui/views/pages/servers.py +++ b/dzgui/views/pages/servers.py @@ -5,12 +5,13 @@ from typing import Self, TYPE_CHECKING from dzgui.const.constants import APP_NAME from dzgui.const.enum import ContextMenuGroup, ServerTab from dzgui.views.trees.tree_servers import ServerTreeView +from dzgui.strings.generic import tab_changed_label from dzgui.util.strings import server_labels import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, GLib # noqa E402 +from gi.repository import Gtk, Gdk logger = logging.getLogger(APP_NAME) @@ -147,8 +148,7 @@ class ServerNotebook(Gtk.ScrolledWindow): label = self.notebook.get_tab_label_text(child) if label is not None: - # TODO: strings - text = label.strip("*") + text = label.strip(tab_changed_label) self.notebook.set_tab_label_text(child, text) tree = child.get_tree() diff --git a/dzgui/views/pages/thanks.py b/dzgui/views/pages/thanks.py index 4adc39c..88e4256 100644 --- a/dzgui/views/pages/thanks.py +++ b/dzgui/views/pages/thanks.py @@ -8,7 +8,7 @@ from dzgui.views.mixins.scrollable_mixin import ScrollableMixin import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk # TODO: wrap and center long values if TYPE_CHECKING: diff --git a/dzgui/views/trees/tree_base.py b/dzgui/views/trees/tree_base.py index 5d7b6a2..0919b08 100644 --- a/dzgui/views/trees/tree_base.py +++ b/dzgui/views/trees/tree_base.py @@ -79,11 +79,6 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore index = path.get_indices()[0] return index - def get_selected_records(self) -> list: - sel = self.get_selection() - model, rows = sel.get_selected_rows() - return [model[row] for row in rows] - def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> bool: from dzgui.views.trees.tree_servers import ServerTreeView @@ -145,9 +140,11 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore self.set_cursor(path) def get_value_at_index(self, index: int) -> Any: - (model, pathlist) = self.get_model_and_pathlist() + model, pathlist = self.get_model_and_pathlist() if len(pathlist) < 1: return "" + if model is None: + return "" path = pathlist[0] tree_iter = model.get_iter(path) value = model.get_value(tree_iter, index) @@ -161,20 +158,16 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore sel = self.get_selection() self._on_tree_selection_changed(sel) - def get_model_and_pathlist(self) -> tuple: + def get_model_and_pathlist( + self, + ) -> tuple[Gtk.TreeModel | None, list[Gtk.TreePath]]: select = self.get_selection() sels = select.get_selected_rows() - (model, pathlist) = sels + model: Gtk.TreeModel | None + pathlist: list[Gtk.TreePath] + model, pathlist = sels return (model, pathlist) - # @deprecated("Currently unused") - # def get_mpath(self) -> Optional[Gtk.TreePath]: - # (model, pathlist) = self.get_model_and_pathlist() - # if len(pathlist) < 1: - # return None - # path = pathlist[0] - # return path - def _on_row_activated( self, treeview: Gtk.TreeView, @@ -189,15 +182,6 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore return True return False - # @deprecated("unused") - # def get_selected_row(self) -> Optional[Gtk.TreeModelRow]: - # ind = self.get_selected_row_index() - # model = self.get_model() - # if model is None: - # return None - # row = model[ind] - # return row - def get_col_value_by_path_index(self, path: Gtk.TreePath, index: int) -> Any: model = self.get_model() if model is None: diff --git a/dzgui/views/trees/tree_log.py b/dzgui/views/trees/tree_log.py index 7f141ef..fd4279d 100644 --- a/dzgui/views/trees/tree_log.py +++ b/dzgui/views/trees/tree_log.py @@ -12,7 +12,7 @@ from dzgui.views.mixins.context_mixin import ContextMixin import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402 +from gi.repository import Gtk, Gdk if TYPE_CHECKING: from pathlib import Path diff --git a/dzgui/views/trees/tree_menu.py b/dzgui/views/trees/tree_menu.py index 04a17f2..a0964bc 100644 --- a/dzgui/views/trees/tree_menu.py +++ b/dzgui/views/trees/tree_menu.py @@ -77,4 +77,4 @@ class MenuTreeView(TreeView): raise ValueError("Trying to call a method on non-existent model") _iter = self.get_focused_row_iter() rowtype = model.get_value(_iter, 1) - return str(rowtype.dict["tooltip"]) + return str(rowtype.tooltip) diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index 3143c6d..37210cb 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -7,7 +7,7 @@ from warnings import deprecated from dzgui.api.servers import ping, Record from dzgui.const.constants import APP_NAME -from dzgui.const.enum import ContextMenu, ContextMenuGroup, ServerTab +from dzgui.const.enum import ContextMenuItem, ContextMenuGroup, ServerTab from dzgui.managers.filter import FilterManager from dzgui.model.proxy_model import ProxyModelManager from dzgui.util import strings @@ -32,6 +32,7 @@ if TYPE_CHECKING: from dzgui.controllers.mc import Controller from dzgui.controllers.emitter import Emitter from dzgui.model.model_factory import FastInsertListStore + from dzgui.util.dist import Haversine QUEUE_CHECK_DELAY = 200 @@ -64,7 +65,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore self.queue_id: int self.handler_id: int - self.queue: Queue = Queue() + self.queue: Queue[tuple[str, "Haversine" | None, ServerTab]] = Queue() self.seen_cache: list[Record] = [] @@ -250,9 +251,9 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore if self.enum is ServerTab.LAN: self.emitter.emit("request_custom_port_focus") case Gdk.KEY_c: - self.controller.menu_action(ContextMenu.COPY_SERVER_IP, self) + self.controller.menu_action(ContextMenuItem.COPY_SERVER_IP, self) case Gdk.KEY_r: - self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self) + self.controller.menu_action(ContextMenuItem.REFRESH_PLAYERS, self) else: match event.keyval: case Gdk.KEY_l | Gdk.KEY_Right: @@ -406,11 +407,15 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore path = model.get_path(it) row_index = path.get_indices()[0] try: - start, end = self.get_visible_range() + start, end = self.get_visible_range() # type: ignore[no-untyped-call] except Exception: return if row_index >= start[0] <= end[0]: # NOTE: fetch raw data rows real_model = self.proxy_man.get_control() - value = real_model[row_index][col_index] + from dataclasses import fields + from dzgui.model.proxy_model import ServerRow + key = fields(ServerRow)[col_index].name + + value = getattr(real_model[row_index], key) cell.set_property("text", str(value)) diff --git a/pyproject.toml b/pyproject.toml index 7d97ce6..4283cec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ line-length = 88 indent-width = 4 extend-exclude = ["*lib/a2s"] lint.unfixable = ["F401", "I001"] +lint.ignore = ["E402"] [tool.ruff.format] quote-style = "double" @@ -104,7 +105,7 @@ python-a2s = { path = "./dzgui/lib/a2s", editable = true } dayzquery = { path = "./dzgui/lib/dayzquery", editable = true } [tool.uv] -config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } } +config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3" } } [tool.pytest.ini_options] markers = [ diff --git a/scripts/concat_licenses.py b/scripts/concat_licenses.py index 56f80a6..c1f149c 100644 --- a/scripts/concat_licenses.py +++ b/scripts/concat_licenses.py @@ -1,4 +1,4 @@ -from pathlib import Path +from shared import get_git_root BOILERPLATE = """ This file includes the combined licenses to the following components: @@ -40,7 +40,7 @@ class CombinedLicense: def concat_license() -> str: combined = CombinedLicense() - root = Path(__file__).resolve().parents[1] + root = get_git_root() licenses = root.joinpath("docs/licenses") main_license = root.joinpath(LICENSE_FILE) diff --git a/scripts/copy_changelog.py b/scripts/copy_changelog.py index c32a4cb..e900469 100644 --- a/scripts/copy_changelog.py +++ b/scripts/copy_changelog.py @@ -1,10 +1,11 @@ import shutil import subprocess -from pathlib import Path -par = Path(__file__).parent.parent -clog = par / "CHANGELOG.md" -target = par / "dzgui/data/CHANGELOG.md" +from shared import get_git_root + +root = get_git_root() +clog = root.joinpath("CHANGELOG.md") +target = root.joinpath("dzgui/data/CHANGELOG.md") shutil.copy(clog, target) diff --git a/scripts/parse_toml.py b/scripts/parse_toml.py index dc0afe0..e4b5210 100644 --- a/scripts/parse_toml.py +++ b/scripts/parse_toml.py @@ -1,10 +1,9 @@ import tomllib -from pathlib import Path - -root = Path(__file__).resolve().parents[1] +from shared import get_git_root def get_entrypoint() -> None: + root = get_git_root() p = root.joinpath("pyproject.toml") with open(p, "rb") as f: t = tomllib.load(f) diff --git a/scripts/prebuild.py b/scripts/prebuild.py index 8700b62..143bc56 100644 --- a/scripts/prebuild.py +++ b/scripts/prebuild.py @@ -3,14 +3,15 @@ import requests import subprocess import tarfile -from pathlib import Path from urllib.parse import unquote +from shared import get_git_root + BUILD = "3.13.9" DATE = "20251014" CPYTHON_BUILD = f"cpython-{BUILD}" -root = Path(__file__).resolve().parents[1] +root = get_git_root() build_dir = root.joinpath("build") cpython_dir = build_dir.joinpath(CPYTHON_BUILD) diff --git a/scripts/release.py b/scripts/release.py index 2eb8f02..2afaf47 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -9,6 +9,7 @@ from pathlib import Path from concat_licenses import concat_license from parse_toml import get_entrypoint from prebuild import get_pyapp, rebuild_cpython +from shared import get_git_root def update_uname(info: tarfile.TarInfo) -> tarfile.TarInfo: @@ -17,7 +18,7 @@ def update_uname(info: tarfile.TarInfo) -> tarfile.TarInfo: return info -root = Path(__file__).resolve().parents[1] +root = get_git_root() dist_dir = root.joinpath("dist") build_dir = root.joinpath("build") diff --git a/scripts/scrot.py b/scripts/scrot.py index 9dbf04e..3800197 100644 --- a/scripts/scrot.py +++ b/scripts/scrot.py @@ -1,16 +1,7 @@ -import subprocess from pathlib import Path from dzgui.app_init import build_gui - - -def get_git_root() -> Path: - proc = subprocess.run( - ["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True - ) - if proc.returncode != 0: - raise OSError("Failed to parse git root") - return Path(proc.stdout.rstrip()) +from shared import get_git_root def get_image_path() -> Path: diff --git a/scripts/shared/__init__.py b/scripts/shared/__init__.py new file mode 100644 index 0000000..f638b7b --- /dev/null +++ b/scripts/shared/__init__.py @@ -0,0 +1,11 @@ +import subprocess +from pathlib import Path + + +def get_git_root() -> Path: + proc = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True + ) + if proc.returncode != 0: + raise OSError("Failed to parse git root") + return Path(proc.stdout.rstrip()) diff --git a/scripts/unused_imports.py b/scripts/unused_imports.py deleted file mode 100644 index 83498ad..0000000 --- a/scripts/unused_imports.py +++ /dev/null @@ -1,38 +0,0 @@ -from pathlib import Path - - -def iterate(search_str: str) -> None: - # TODO: exclude files from .gitignore - print() - print(f"Searching for '{search_str}'") - print() - files = Path(".").rglob("*") - ignore = [ - "scripts/", - ".git", - "egg", - "CHANGELOG", - ".mypy", - "build/", - "test.py", - "bugs", - "pyproject.toml", - ] - for file in files: - if file.is_dir(): - continue - if any(s in str(file) for s in ignore): - continue - try: - text = file.read_text() - if search_str in text: - c = text.count(search_str) - if 0 < c < 2: - print(file, c) - except Exception: - continue - - -imports = ["Pango", "GObject", "GLib", "Gdk", "Gtk"] -for i in imports: - iterate(i) diff --git a/tests/test_html_escaping.py b/tests/test_html_escaping.py index 904e5b2..2c3c03c 100644 --- a/tests/test_html_escaping.py +++ b/tests/test_html_escaping.py @@ -6,7 +6,7 @@ from dzgui.views.dialogs.servers import ServerDetailsDialog import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk class MockController: diff --git a/tests/test_paused_downloads.py b/tests/test_paused_downloads.py index 86bbb70..1c45ca2 100644 --- a/tests/test_paused_downloads.py +++ b/tests/test_paused_downloads.py @@ -16,7 +16,7 @@ from dzgui.api.steam import is_dayz_downloading (["state_221100_1111"], False), ], ) -def test_dayz_patch_pending(monkeypatch, file_list, expect) -> None: +def test_dayz_patch_pending(monkeypatch, file_list: list[str], expect: bool) -> None: def mock_get_pending_downloads(steam_path: Path, appid: int) -> None: files = [Path(file) for file in file_list] return files diff --git a/tests/test_row_marshalling.py b/tests/test_row_marshalling.py new file mode 100644 index 0000000..7e59080 --- /dev/null +++ b/tests/test_row_marshalling.py @@ -0,0 +1,26 @@ +from dzgui.model.model_factory import ModelFactory +from dzgui.model.proxy_model import ServerRow + + +def test_row_conversion() -> None: + """Converted FastInsertListStore is expected to have identical elements""" + data = [ + "Test Server", + "chernarusplus", + "1PP", + "05:00", + 10, + 10, + 0, + "127.0.0.0:27016", + 27017, + 99, + "Unofficial", + False, + ] + rows = [ServerRow(*data)] + marshalled = ServerRow.marshal_rows(rows) + store = ModelFactory().make_server_store() + store.extend(marshalled) + converted = [[el for el in row] for row in store] + assert data == converted[0]