Compare commits

...
Author SHA1 Message Date
aclist 7a6f4a2bb9 feat: immutable decorator 2026-11-11 07:52:05 +09:00
aclist ba92d15085 drop: unused signal logic 2026-11-11 07:50:54 +09:00
aclist 65e04c2fc4 fix: multiple signals being stacked 2026-11-11 07:50:38 +09:00
aclist 0907f5a474 change: immutable dataclass 2026-11-11 07:50:12 +09:00
aclist bfa83c3f94 change: import sort order 2026-11-11 07:15:53 +09:00
aclist aef48e515b fix: failing test 2026-11-11 07:15:28 +09:00
aclist e6ebeaeb5e chore: immutable dataclass 2026-11-11 07:04:25 +09:00
aclist 88c740105a chore: drop unused dataclasses 2026-11-11 07:02:23 +09:00
aclist b98a6f95a6 drop: unused imports 2026-11-11 07:00:17 +09:00
aclist 87d76e83ba chore: update DEVELOPERS.md 2026-11-11 06:57:56 +09:00
aclist a5374d1f1a Merge pull request #526 from aclist/fix/legacy-version-file
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: legacy version file
2026-11-11 05:08:14 +09:00
aclist dcc4cb494b Merge branch 'dzgui7' into fix/legacy-version-file
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-11 05:07:35 +09:00
aclist ff1e150eb1 change: suppress logger message 2026-11-11 05:04:50 +09:00
aclist c7ef91f02e Merge pull request #539 from aclist/fix/remove-from-history
fix: restore missing route
2026-11-11 05:00:34 +09:00
aclist 95bc05eab5 Merge pull request #536 from aclist/change/server-row-dataclass
change: marshal server row data
2026-11-11 04:59:58 +09:00
aclist 89dc6aa8a8 chore: drop comment
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-11 04:52:50 +09:00
aclist 9e8e3730c7 fix: drop unused imports 2026-11-11 04:30:31 +09:00
aclist 52c126ff8c fix: restore missing route 2026-11-11 04:29:36 +09:00
aclist e5c8f2a69c chore: drop stale comment 2026-11-11 04:11:23 +09:00
aclist 63316a01a9 fix: extraneous newlines in log 2026-11-11 03:57:51 +09:00
aclist 0729273503 change: drop Union 2026-11-11 03:51:09 +09:00
aclist 91d164d666 chore: drop unused method 2026-11-11 03:49:43 +09:00
aclist 0d7f022c43 Merge branch 'dzgui7' into change/server-row-dataclass 2026-11-11 03:49:05 +09:00
aclist f76c400697 Merge pull request #535 from aclist/change/enum-dataclasses
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
change: enum dataclasses
2026-11-11 03:44:58 +09:00
aclist 7ae5880890 chore: update union
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-11 03:36:14 +09:00
aclist afbced228d Merge branch 'dzgui7' into change/enum-dataclasses 2026-11-11 03:33:44 +09:00
aclist f5d7bf713d Merge pull request #538 from aclist/fix/clipboard-button-controller
fix: clipboard button controller
2026-11-11 03:32:27 +09:00
aclist 9b948271e1 chore: add test 2026-11-11 00:57:12 +09:00
aclist 7322970c54 fix: define constants outside of FiltrationManager 2026-11-11 00:56:51 +09:00
aclist 504a2a724c fix: typo 2026-11-11 00:48:37 +09:00
aclist c66511b005 change: marshal server row data 2026-11-11 00:41:40 +09:00
aclist c94e4e73db chore: add strings file 2026-11-10 04:34:38 +09:00
aclist 8da264686a chore: clear typehinting errors 2026-11-10 04:33:54 +09:00
aclist 598973104c change: reference dataclass values in enum 2026-11-10 04:30:40 +09:00
aclist 3677b01fdc fix: exception handling 2026-11-09 22:25:13 +09:00
aclist 4f6223f90b fix: strip invalid values from versions file 2026-11-09 22:24:26 +09:00
aclist 441066a1a0 chore: update comments 2026-11-09 22:23:37 +09:00
aclist fa1ac8fa31 chore: update imports 2026-11-09 22:23:27 +09:00
49 changed files with 665 additions and 486 deletions

No files matched your search

+6 -5
View File
@@ -5,7 +5,7 @@
### Fetch submodules ### Fetch submodules
git submodule update --recursive --init git submodule update --recursive --init
### Install with dev dependencies ### Install into editable venv with dev dependencies
uv pip install -e '.[dev]' uv pip install -e '.[dev]'
This includes all dependencies for building, linting, testing, and checking in code. This includes all dependencies for building, linting, testing, and checking in code.
@@ -16,7 +16,7 @@ docs: documentation tools
lint: code linting, fixing, and type checking lint: code linting, fixing, and type checking
test: unit and integration tests test: unit and integration tests
## Miscellaneous ## Build process
### Setting up precommit hooks ### Setting up precommit hooks
pre-commit install pre-commit install
@@ -25,6 +25,7 @@ pre-commit install
sphinx-build -M html source build -a sphinx-build -M html source build -a
### Building standalone release binaries ### Building standalone release binaries
1. Set up the build target: `rustup target add x86_64-unknown-linux-musl` 1. Install the applicable system package containing the 'musl-gcc' wrapper
2. Run `python3 scripts/release.py` 2. Set up the build target: `rustup target add x86_64-unknown-linux-musl`
3. Generate a release and tag against the resulting tarfile generated in `dist` 3. Run `python3 scripts/release.py`
4. Generate a release and tag against the resulting tarfile generated in `dist`
+6 -6
View File
@@ -2,10 +2,9 @@ import hashlib
import logging import logging
import re import re
from concurrent.futures import wait from concurrent.futures import ThreadPoolExecutor, wait
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any
from dzgui.api.steam import get_app_path from dzgui.api.steam import get_app_path
from dzgui.api.servers import get_rules, fqip_to_record from dzgui.api.servers import get_rules, fqip_to_record
@@ -18,8 +17,7 @@ from dzgui.const.constants import (
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.init.migrate import clean_legacy_versions_file
from typing import Any
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -117,10 +115,12 @@ def remove_stale_signatures(
config: Path, versions: Path, ids: list[int] | None = None config: Path, versions: Path, ids: list[int] | None = None
) -> None: ) -> None:
if versions.is_file() is False: if versions.is_file() is False:
logger.warning("Creating new version signatures file") logger.debug("Creating new version signatures file")
versions.touch() versions.touch()
return return
clean_legacy_versions_file(versions)
path = lookup(config, Preferences.DEFAULT) path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path) steam_path = Path(path)
if ids is None: if ids is None:
+3 -2
View File
@@ -13,6 +13,7 @@ from typing import Any, Optional, TYPE_CHECKING, Union
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
from dzgui.const.endpoints import STEAM_SERVERS from dzgui.const.endpoints import STEAM_SERVERS
from dzgui.model.proxy_model import ServerRow
from dzgui.util.format import format_exception from dzgui.util.format import format_exception
from dzgui.util import strings from dzgui.util import strings
@@ -141,7 +142,7 @@ def sanitize(name: str) -> str:
def parse_json( def parse_json(
json: list[dict[str, Any]], json: list[dict[str, Any]],
) -> list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]]: ) -> list[ServerRow]:
""" """
Server metadata is underspecified and server operators Server metadata is underspecified and server operators
tend to insert random garbage in the headers. In case tend to insert random garbage in the headers. In case
@@ -226,7 +227,7 @@ def parse_json(
mapname = row["map"].lower() mapname = row["map"].lower()
players = row["players"] players = row["players"]
max_players = row["max_players"] max_players = row["max_players"]
raw = ( raw = ServerRow(
name, name,
mapname, mapname,
view, view,
+2 -3
View File
@@ -6,7 +6,6 @@ from importlib import resources
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.const.constants import ( from dzgui.const.constants import (
APP_NAME, APP_NAME,
APP_NAME_LOWER, APP_NAME_LOWER,
@@ -18,8 +17,8 @@ from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.init.migrate import ( from dzgui.init.migrate import (
has_new_config,
copy_state_files, copy_state_files,
has_new_config,
migrate_cols_file, migrate_cols_file,
) )
from dzgui.init.prereqs import has_flatpak_client, has_steam_client from dzgui.init.prereqs import has_flatpak_client, has_steam_client
@@ -107,7 +106,7 @@ def load_steam_icon() -> None:
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
try: try:
traversable = resources.files(APP_NAME_LOWER).joinpath(ICONS_PATH) traversable = resources.files(APP_NAME_LOWER).joinpath(ICONS_PATH)
+2 -1
View File
@@ -5,6 +5,7 @@ from dzgui.util._json import read_json
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
class ConfigFileError(Exception): class ConfigFileError(Exception):
pass pass
@@ -39,4 +40,4 @@ def get_favorites(path: Path) -> list[str]:
def enum_to_key(enum: Preferences) -> str: def enum_to_key(enum: Preferences) -> str:
return str(enum.dict["key"]) return str(enum.key)
+2 -1
View File
@@ -6,7 +6,8 @@ if TYPE_CHECKING:
from dzgui.util.ip import Coords from dzgui.util.ip import Coords
# NOTE: mutable dataclass, 'use_miles' key changes on demand # NOTE: mutable dataclass: 'use_miles' key changes on demand
# 'frozen' cannot be True
@dataclass(slots=True) @dataclass(slots=True)
class UserPrefs: class UserPrefs:
is_steam_deck: bool is_steam_deck: bool
+1 -1
View File
@@ -12,7 +12,7 @@ from dzgui.const.constants import (
from dzgui.init.proc import is_sandboxed from dzgui.init.proc import is_sandboxed
@dataclass @dataclass(slots=True, frozen=True)
class Xdg: class Xdg:
config: Path config: Path
columns: Path columns: Path
+224 -206
View File
@@ -1,7 +1,9 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Any, Self
from dzgui.util import strings from dzgui.util import strings
from dzgui.strings import rows
class Port(Enum): class Port(Enum):
@@ -46,125 +48,61 @@ class FilterMode(Enum):
TOGGLE_ON = 3 TOGGLE_ON = 3
class EnumWithAttrs(Enum): @dataclass(slots=True, frozen=True)
def __new__(cls, *args: Any, **kwargs: Any) -> Self: class ModButtonMeta:
value = len(cls.__members__) + 1 label: str
obj = object.__new__(cls) tooltip: str
obj._value_ = value
return obj
def __init__(self, d: dict):
self.dict = d
class Preferences(EnumWithAttrs): @dataclass(slots=True, frozen=True)
STEAM = { class ButtonTypeMeta:
"key": "steam_api", label: str
} tooltip: str
CLIENT = { opens: NotebookPage | None
"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",
}
class NotebookPage(EnumWithAttrs): @dataclass(slots=True, frozen=True)
OFFLINE = {"crumbs": strings.crumbs.offline, "statusbar": True} class ConfigKey:
CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": True} key: str
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}
class RowType(EnumWithAttrs): @dataclass(slots=True, frozen=True)
# TODO: strings class NotebookMeta:
CHANGELOG = { crumbs: str
"label": "View changelog", has_statusbar: bool
"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",
}
# TODO: rename to ContextMenuItem @dataclass(slots=True, frozen=True)
class ContextMenu(EnumWithAttrs): class RowMeta:
ADD_NOTE = {"label": strings.add_note} label: str
ADD_SERVER = {"label": strings.add} tooltip: str
CONNECT = {"label": strings.connect}
COPY_LOG_CLIPBOARD = {"label": strings.copy_log}
COPY_SERVER_IP = {"label": strings.copy_ip} @dataclass(slots=True, frozen=True)
COPY_SERVER_NAME = {"label": strings.copy_name} class ContextMenuMeta:
UNSUB_MOD = {"label": strings.unsub_mod} label: str
VALIDATE_MOD = {"label": strings.validate_mod}
OPEN_WORKSHOP = {"label": strings.open_workshop}
REFRESH_PLAYERS = {"label": strings.refresh_players} class ContextMenuItem(Enum):
REMOVE_HISTORY = {"label": strings.remove_history} ADD_NOTE = ContextMenuMeta(strings.add_note)
REMOVE_SERVER = {"label": strings.remove} ADD_SERVER = ContextMenuMeta(strings.add)
SET_FAV = {"label": strings.add_fav} CONNECT = ContextMenuMeta(strings.connect)
SHOW_DETAILS = {"label": strings.show_details} COPY_LOG_CLIPBOARD = ContextMenuMeta(strings.copy_log)
SHOW_MODS = {"label": strings.show_mods} 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): class ContextMenuGroup(Enum):
@@ -172,113 +110,193 @@ class ContextMenuGroup(Enum):
Groupings of context menu items Groupings of context menu items
""" """
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,) SERVER_MOD = (ContextMenuItem.OPEN_WORKSHOP,)
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD, ContextMenu.VALIDATE_MOD) MOD = (
ContextMenuItem.OPEN_WORKSHOP,
ContextMenuItem.UNSUB_MOD,
ContextMenuItem.VALIDATE_MOD,
)
MOD_OFFLINE = (None,) MOD_OFFLINE = (None,)
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,) LOG = (ContextMenuItem.COPY_LOG_CLIPBOARD,)
SERVER_BROWSER = ( SERVER_BROWSER = (
ContextMenu.CONNECT, ContextMenuItem.CONNECT,
ContextMenu.ADD_SERVER, ContextMenuItem.ADD_SERVER,
ContextMenu.SET_FAV, ContextMenuItem.SET_FAV,
ContextMenu.COPY_SERVER_NAME, ContextMenuItem.COPY_SERVER_NAME,
ContextMenu.COPY_SERVER_IP, ContextMenuItem.COPY_SERVER_IP,
ContextMenu.ADD_NOTE, ContextMenuItem.ADD_NOTE,
ContextMenu.SHOW_MODS, ContextMenuItem.SHOW_MODS,
ContextMenu.SHOW_DETAILS, ContextMenuItem.SHOW_DETAILS,
ContextMenu.REFRESH_PLAYERS, ContextMenuItem.REFRESH_PLAYERS,
) )
SCAN_LAN = ( SCAN_LAN = (
ContextMenu.CONNECT, ContextMenuItem.CONNECT,
ContextMenu.SET_FAV, ContextMenuItem.SET_FAV,
ContextMenu.COPY_SERVER_NAME, ContextMenuItem.COPY_SERVER_NAME,
ContextMenu.COPY_SERVER_IP, ContextMenuItem.COPY_SERVER_IP,
ContextMenu.ADD_NOTE, ContextMenuItem.ADD_NOTE,
ContextMenu.SHOW_MODS, ContextMenuItem.SHOW_MODS,
ContextMenu.SHOW_DETAILS, ContextMenuItem.SHOW_DETAILS,
ContextMenu.REFRESH_PLAYERS, ContextMenuItem.REFRESH_PLAYERS,
) )
SAVED = ( SAVED = (
ContextMenu.CONNECT, ContextMenuItem.CONNECT,
ContextMenu.SET_FAV, ContextMenuItem.SET_FAV,
ContextMenu.REMOVE_SERVER, ContextMenuItem.REMOVE_SERVER,
ContextMenu.COPY_SERVER_NAME, ContextMenuItem.COPY_SERVER_NAME,
ContextMenu.COPY_SERVER_IP, ContextMenuItem.COPY_SERVER_IP,
ContextMenu.ADD_NOTE, ContextMenuItem.ADD_NOTE,
ContextMenu.SHOW_MODS, ContextMenuItem.SHOW_MODS,
ContextMenu.SHOW_DETAILS, ContextMenuItem.SHOW_DETAILS,
ContextMenu.REFRESH_PLAYERS, ContextMenuItem.REFRESH_PLAYERS,
) )
RECENT = ( RECENT = (
ContextMenu.CONNECT, ContextMenuItem.CONNECT,
ContextMenu.ADD_SERVER, ContextMenuItem.ADD_SERVER,
ContextMenu.SET_FAV, ContextMenuItem.SET_FAV,
ContextMenu.REMOVE_HISTORY, ContextMenuItem.REMOVE_HISTORY,
ContextMenu.COPY_SERVER_NAME, ContextMenuItem.COPY_SERVER_NAME,
ContextMenu.COPY_SERVER_IP, ContextMenuItem.COPY_SERVER_IP,
ContextMenu.ADD_NOTE, ContextMenuItem.ADD_NOTE,
ContextMenu.SHOW_MODS, ContextMenuItem.SHOW_MODS,
ContextMenu.SHOW_DETAILS, ContextMenuItem.SHOW_DETAILS,
ContextMenu.REFRESH_PLAYERS, ContextMenuItem.REFRESH_PLAYERS,
) )
class ModButton(EnumWithAttrs): class Preferences(Enum):
SELECT_ALL = { STEAM = ConfigKey("steam_api")
"label": strings.mod_panel.select_all, CLIENT = ConfigKey("client")
"tooltip": strings.mod_panel.select_all_tooltip, WINDOW = ConfigKey("fullscreen")
} DIST = ConfigKey("use_miles")
UNSELECT_ALL = { NAME = ConfigKey("name")
"label": strings.mod_panel.unselect_all, DEFAULT = ConfigKey("default_steam_path")
"tooltip": strings.mod_panel.unselect_all_tooltip, FAV_LBL = ConfigKey("fav_label")
} FAV_SRV = ConfigKey("fav_server")
HIGHLIGHT_STALE = { IP_LIST = ConfigKey("ip_list")
"label": strings.mod_panel.highlight_stale, START_TAB = ConfigKey("start_tab")
"tooltip": strings.mod_panel.highlight_stale_tooltip,
} @property
UNHIGHLIGHT_STALE = { def key(self) -> str:
"label": strings.mod_panel.unhighlight_stale, return self.value.key
"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 ButtonType(EnumWithAttrs): class NotebookPage(Enum):
SERVERS = { OFFLINE = NotebookMeta(strings.crumbs.offline, True)
"label": strings.buttons.servers_label, CHANGELOG = NotebookMeta(strings.crumbs.changelog, True)
"tooltip": strings.buttons.servers_tooltip, DEVELOPERS = NotebookMeta(strings.crumbs.developers, False)
"opens": NotebookPage.SERVERS, HELP = NotebookMeta(strings.crumbs._help, True)
} KEYS = NotebookMeta(strings.crumbs.keys, True)
MODS = { LOG = NotebookMeta(strings.crumbs.log, True)
"label": strings.buttons.mods_label, MODS = NotebookMeta(strings.crumbs.mods, True)
"tooltip": strings.buttons.mods_tooltip, OPTIONS = NotebookMeta(strings.crumbs.options, False)
"opens": NotebookPage.MODS, SERVERS = NotebookMeta(strings.crumbs.servers, True)
} THANKS = NotebookMeta(strings.crumbs.thanks, True)
OPTIONS = { CONNECTION = NotebookMeta(strings.crumbs.connect, True)
"label": strings.buttons.options_label,
"tooltip": strings.buttons.options_tooltip, @property
"opens": NotebookPage.OPTIONS, def crumbs(self) -> str:
} return self.value.crumbs
HELP = {
"label": strings.buttons.help_label, @property
"tooltip": strings.buttons.help_tooltip, def has_statusbar(self) -> bool:
"opens": NotebookPage.HELP, return self.value.has_statusbar
}
EXIT = {
"label": strings.buttons.exit_label, class RowType(Enum):
"tooltip": strings.buttons.exit_tooltip, CHANGELOG = RowMeta(rows.changelog_label, rows.changelog_status)
"opens": None, 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 = ( HELP_MENU_ROWS = (
+8 -12
View File
@@ -1,22 +1,19 @@
import gi
import logging import logging
import threading import threading
from typing import Any, TYPE_CHECKING, Union from typing import Any, TYPE_CHECKING, Union
import dzgui.util._json as JSON # noqa from dzgui.config.userprefs import UserPrefs
from dzgui.const.constants import APP_NAME
from dzgui.const.enum import ( from dzgui.const.enum import (
ButtonType, ButtonType,
ContextMenu, ContextMenuItem,
FilterMode, FilterMode,
NotebookPage, NotebookPage,
Preferences, Preferences,
ServerTab, ServerTab,
) )
from dzgui.config.userprefs import UserPrefs
from dzgui.const.constants import APP_NAME
from dzgui.controllers.emitter import Emitter from dzgui.controllers.emitter import Emitter
from dzgui.managers.config import ConfigManager from dzgui.managers.config import ConfigManager
from dzgui.managers.connection import ConnectionManager from dzgui.managers.connection import ConnectionManager
@@ -29,8 +26,6 @@ from dzgui.util.open_links import open_user_workshop, open_workshop_page
from dzgui.views.dialogs.filepicker import FilePicker from dzgui.views.dialogs.filepicker import FilePicker
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, GObject from gi.repository import Gtk, Gdk, GLib, GObject
@@ -196,7 +191,9 @@ class Controller(GObject.GObject):
def show_developers_page(self) -> None: def show_developers_page(self) -> None:
self.open_page(NotebookPage.DEVELOPERS) 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) self.mediator.grid.notebook.set_page_by_enum(page)
def open_page_by_button(self, button: "ContextualButton") -> None: def open_page_by_button(self, button: "ContextualButton") -> None:
@@ -262,7 +259,7 @@ class Controller(GObject.GObject):
logger.critical(e) logger.critical(e)
return 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 = ContextMenuManager(tree, self)
context_man.process(action) context_man.process(action)
@@ -370,7 +367,6 @@ class Controller(GObject.GObject):
return filter_man.get_filters() return filter_man.get_filters()
# FIXME: wipe maps store when changing tabs if model is none # FIXME: wipe maps store when changing tabs if model is none
def has_server_model(self) -> bool: def has_server_model(self) -> bool:
treeview = self.get_active_treeview() treeview = self.get_active_treeview()
proxy_man = treeview.get_proxy_man() proxy_man = treeview.get_proxy_man()
+22 -1
View File
@@ -1,12 +1,21 @@
import json import json
import logging
import re
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from dzgui.const.constants import LEGACY_CONFIG_PATH, LEGACY_COLS_PATH, LEGACY_IPS_PATH from dzgui.const.constants import (
APP_NAME,
LEGACY_CONFIG_PATH,
LEGACY_COLS_PATH,
LEGACY_IPS_PATH,
)
from dzgui.config.convert import rc2json from dzgui.config.convert import rc2json
from dzgui.util._json import read_json, write_json from dzgui.util._json import read_json, write_json
logger = logging.getLogger(APP_NAME)
def migrate_legacy_conf(config: Path) -> Any: def migrate_legacy_conf(config: Path) -> Any:
old_conf = Path.home() / LEGACY_CONFIG_PATH old_conf = Path.home() / LEGACY_CONFIG_PATH
@@ -70,3 +79,15 @@ def copy_ipdb(ips_path: Path) -> None:
return return
# TODO: ensure that month file is copied # TODO: ensure that month file is copied
shutil.copy(legacy, ips_path) shutil.copy(legacy, ips_path)
def clean_legacy_versions_file(versions: Path) -> None:
# NOTE: handling for #523
try:
pat = r"^(\d+),(\d+)$"
lines = versions.read_text().splitlines()
clean = [line for line in lines if re.match(pat, line)]
concat = "\n".join(clean)
versions.write_text(concat)
except Exception as e:
logger.warning(e)
+10 -1
View File
@@ -46,6 +46,15 @@ class ConfigManager:
ips.append(record) ips.append(record)
self.update_config(Preferences.IP_LIST, ips) 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: def update_history_file(self, fqip: str) -> None:
try: try:
with open(self.prefs.paths.history, "r") as f: with open(self.prefs.paths.history, "r") as f:
@@ -124,7 +133,7 @@ class ConfigManager:
return False return False
def enum_to_key(self, enum: Preferences) -> str: def enum_to_key(self, enum: Preferences) -> str:
return str(enum.dict["key"]) return str(enum.key)
def toggle_config(self, key: Preferences) -> None: def toggle_config(self, key: Preferences) -> None:
try: try:
+18 -18
View File
@@ -2,7 +2,7 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.const.constants import APP_NAME 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.clip import copy_clipboard
from dzgui.util.open_links import open_workshop_page from dzgui.util.open_links import open_workshop_page
@@ -34,7 +34,7 @@ class ContextMenuManager:
self.controller = controller self.controller = controller
self.treeview = treeview self.treeview = treeview
def process(self, action: ContextMenu) -> None: def process(self, action: ContextMenuItem) -> None:
if isinstance(self.treeview, ServerTreeView): if isinstance(self.treeview, ServerTreeView):
record = self.treeview.get_record() record = self.treeview.get_record()
if record is None: if record is None:
@@ -42,14 +42,14 @@ class ContextMenuManager:
match action: match action:
# UNTHREADED # UNTHREADED
case ContextMenu.ADD_NOTE: case ContextMenuItem.ADD_NOTE:
dialog = NoteDialog(self.controller) dialog = NoteDialog(self.controller)
dialog.run() # type: ignore[no-untyped-call] dialog.run() # type: ignore[no-untyped-call]
case ContextMenu.COPY_SERVER_IP: case ContextMenuItem.COPY_SERVER_IP:
self.copy_server_ip() self.copy_server_ip()
case ContextMenu.COPY_SERVER_NAME: case ContextMenuItem.COPY_SERVER_NAME:
self.copy_server_name() self.copy_server_name()
case ContextMenu.SET_FAV: case ContextMenuItem.SET_FAV:
name = self.treeview.get_value_at_index(0) name = self.treeview.get_value_at_index(0)
# NOTE: fully qualified ip # NOTE: fully qualified ip
fqip = self.treeview.get_record_string() fqip = self.treeview.get_record_string()
@@ -58,40 +58,40 @@ class ContextMenuManager:
self.controller.set_fav(name, fqip, simple) self.controller.set_fav(name, fqip, simple)
# THREADED # THREADED
case ContextMenu.ADD_SERVER: case ContextMenuItem.ADD_SERVER:
self.controller.add_by_record(record) self.controller.add_by_record(record)
case ContextMenu.CONNECT: case ContextMenuItem.CONNECT:
self.controller.connect_by_record(record) self.controller.connect_by_record(record)
case ContextMenu.REFRESH_PLAYERS: case ContextMenuItem.REFRESH_PLAYERS:
self.controller.refresh_players(record) self.controller.refresh_players(record)
case ContextMenu.REMOVE_HISTORY: case ContextMenuItem.REMOVE_HISTORY:
self.controller.remove_from_history(record) self.controller.remove_from_history(record)
case ContextMenu.REMOVE_SERVER: case ContextMenuItem.REMOVE_SERVER:
self.controller.remove_by_record(record) self.controller.remove_by_record(record)
case ContextMenu.SHOW_DETAILS: case ContextMenuItem.SHOW_DETAILS:
self.controller.get_details(record) self.controller.get_details(record)
case ContextMenu.SHOW_MODS: case ContextMenuItem.SHOW_MODS:
self.controller.get_modlist(record) self.controller.get_modlist(record)
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)): if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
match action: match action:
case ContextMenu.OPEN_WORKSHOP: case ContextMenuItem.OPEN_WORKSHOP:
self.open_mod_page() self.open_mod_page()
case ContextMenu.UNSUB_MOD: case ContextMenuItem.UNSUB_MOD:
self.controller.unsub_mods(self.treeview) self.controller.unsub_mods(self.treeview)
case ContextMenu.VALIDATE_MOD: case ContextMenuItem.VALIDATE_MOD:
self.controller.validate_mods(self.treeview) self.controller.validate_mods(self.treeview)
case _: case _:
pass pass
if isinstance(self.treeview, LogTreeView): if isinstance(self.treeview, LogTreeView):
match action: match action:
case ContextMenu.COPY_LOG_CLIPBOARD: case ContextMenuItem.COPY_LOG_CLIPBOARD:
self.copy_log() self.copy_log()
if isinstance(self.treeview, ServerModTreeView): if isinstance(self.treeview, ServerModTreeView):
match action: match action:
case ContextMenu.OPEN_WORKSHOP: case ContextMenuItem.OPEN_WORKSHOP:
self.open_mod_page() self.open_mod_page()
def copy_server_ip(self) -> None: def copy_server_ip(self) -> None:
+1 -1
View File
@@ -147,7 +147,7 @@ class ModelFactory:
def make_help_store(self) -> FastInsertListStore: def make_help_store(self) -> FastInsertListStore:
store = self.new_model_from_class(MenuCols) 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) store.extend(rows)
return store return store
+148 -75
View File
@@ -1,7 +1,8 @@
import re import re
from dataclasses import dataclass from dataclasses import dataclass, fields
from typing import Any, TYPE_CHECKING, Union from operator import attrgetter
from typing import TYPE_CHECKING
from dzgui.const.enum import FilterMode from dzgui.const.enum import FilterMode
from dzgui.model.model_factory import ModelFactory from dzgui.model.model_factory import ModelFactory
@@ -33,6 +34,100 @@ class ServerRow:
provider: str provider: str
modded: bool 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: class ProxyModelManager:
""" """
@@ -49,19 +144,15 @@ class ProxyModelManager:
def __init__(self, filter_man: "FilterManager") -> None: def __init__(self, filter_man: "FilterManager") -> None:
self.filter_cache: dict[ self.filter_cache: dict[
tuple[str, ...], tuple[str, ...],
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]], list[ServerRow],
] = {} ] = {}
self.proxy_model: "FastInsertListStore" self.proxy_model: "FastInsertListStore"
self.filter_man = filter_man self.filter_man = filter_man
self.filtration_manager = FiltrationManager()
self.control_model: ( self.control_model: list[ServerRow] | None = None
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]] self.filtered: list[ServerRow] = []
| None
) = None
self.filtered: list[
tuple[str, str, str, str, int, int, int, str, int, int, str, bool]
] = []
self.success = True self.success = True
def has_control_model(self) -> bool: def has_control_model(self) -> bool:
@@ -69,9 +160,7 @@ class ProxyModelManager:
return False return False
return True return True
def append_row_to_control( def append_row_to_control(self, row: ServerRow) -> None:
self, row: tuple[str, str, str, str, int, int, int, str, int, int, str, bool]
) -> None:
if self.control_model is None: if self.control_model is None:
raise AttributeError("Trying to add rows to a non-existent model") raise AttributeError("Trying to add rows to a non-existent model")
self.control_model.append(row) self.control_model.append(row)
@@ -79,21 +168,24 @@ class ProxyModelManager:
def update_playercount(self, playercount: "NewPlayerCount") -> None: def update_playercount(self, playercount: "NewPlayerCount") -> None:
treeiter = playercount.treeiter treeiter = playercount.treeiter
self.proxy_model[treeiter][4] = playercount.players field = [f.name for f in fields(ServerRow)]
self.proxy_model[treeiter][6] = playercount.queue 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
def append_row_to_history( def append_row_to_history(
self, self,
history: tuple[str, str, str, str, int, int, int, str, int, int, str, bool], history: ServerRow,
) -> None: ) -> None:
addr = history[7] addr = history.ip
qport = history[8] qport = history.qport
if self.control_model is None: if self.control_model is None:
raise AttributeError("Trying to append row to empty model") raise AttributeError("Trying to append row to empty model")
found = False found = False
for i, row in enumerate(self.control_model): 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) item = self.control_model.pop(i)
self.control_model.append(item) self.control_model.append(item)
found = True found = True
@@ -102,6 +194,7 @@ class ProxyModelManager:
if found is False: if found is False:
self.control_model.append(history) self.control_model.append(history)
# NOTE: truncate history to 10 records max
if len(self.control_model) == 11: if len(self.control_model) == 11:
del self.control_model[0] del self.control_model[0]
@@ -113,7 +206,7 @@ class ProxyModelManager:
addr = f"{record.ip}:{record.gameport}" addr = f"{record.ip}:{record.gameport}"
qport = record.qport qport = record.qport
for row in self.control_model: 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) self.control_model.remove(row)
break break
self.wipe_cache() self.wipe_cache()
@@ -122,19 +215,22 @@ class ProxyModelManager:
def clear_proxy_model(self) -> None: def clear_proxy_model(self) -> None:
self.proxy_model.clear() self.proxy_model.clear()
def get_proxy_model(self) -> Union["FastInsertListStore", None]: def get_proxy_model(self) -> "FastInsertListStore | None":
if hasattr(self, "proxy_model"): if hasattr(self, "proxy_model"):
return self.proxy_model return self.proxy_model
return None return None
def filter( def filter(
self, mode: FilterMode, skip_cache: bool = False self, mode: FilterMode, skip_cache: bool = False
) -> Union["FastInsertListStore", None]: ) -> "FastInsertListStore | None":
""" """
Native Gtk.TreeView.refilter() method was not performant enough Native Gtk.TreeView.refilter() method was not performant enough
when running in the main loop with 40k+ records when running in the main loop with 40k+ records
skip_cache: used when updating a record on Saved Servers/History and forcing a refilter 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 # TODO: filter cache: return a dataclass object with clearly enumerated map, keyword, and filter values
# instead of just a serial list of strings # instead of just a serial list of strings
@@ -143,8 +239,9 @@ class ProxyModelManager:
if skip_cache is False: if skip_cache is False:
if filters in self.filter_cache: if filters in self.filter_cache:
cached_rows = self.filter_cache[filters] cached_rows = self.filter_cache[filters]
marshalled_rows = ServerRow.marshal_rows(cached_rows)
clone = ModelFactory().make_server_store() clone = ModelFactory().make_server_store()
clone.extend(cached_rows) clone.extend(marshalled_rows)
self.set_proxy_model(clone) self.set_proxy_model(clone)
self.set_filtered(cached_rows) self.set_filtered(cached_rows)
return None return None
@@ -163,17 +260,18 @@ class ProxyModelManager:
clone = ModelFactory().make_server_store() clone = ModelFactory().make_server_store()
if len(rows) > 0: if len(rows) > 0:
rows = self.sort_rows(rows) 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_cache(filters, rows)
self.set_proxy_model(clone) self.set_proxy_model(clone)
return clone return clone
def sort_rows(self, rows: list) -> list: def sort_rows(self, rows: list[ServerRow]) -> list[ServerRow]:
rows.sort(key=lambda x: re.sub(r"[^A-Za-z0-9]+", "", x[0].lower())) rows.sort(key=lambda x: re.sub(r"[^A-Za-z0-9]+", "", x.name.lower()))
return rows 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 Simply culls the control model of any disabled filters
""" """
@@ -182,7 +280,7 @@ class ProxyModelManager:
self.set_filtered(self.filter_toggle_off(filters, f)) self.set_filtered(self.filter_toggle_off(filters, f))
return self.filtered 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 Multi-filtration for any context starts by narrowing by map
""" """
@@ -192,10 +290,10 @@ class ProxyModelManager:
if sel_map == str_filters.all_maps: if sel_map == str_filters.all_maps:
return rows 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 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() keyword = self.filter_man.get_active_keyword()
rows = self.filtered rows = self.filtered
@@ -205,13 +303,15 @@ class ProxyModelManager:
filtered = [ filtered = [
row row
for row in rows for row in rows
if keyword in row[0].lower() if keyword in row.name.lower()
or keyword in row[1].lower() or keyword in row.mapname.lower()
or keyword in row[7].lower() or keyword in row.ip.lower()
] ]
return filtered 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 Sub-filtration of the current model
""" """
@@ -225,43 +325,11 @@ class ProxyModelManager:
self.set_filtered(None) self.set_filtered(None)
return [] return []
rows = self.filtered # TODO: pack filter type enum in filter checkbox, rather than stringwise comparison
match filter_type: rows = self.filtration_manager.filter(filter_type, self.filtered)
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]]
return rows 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""" """Effectively applies all filters"""
self.set_filtered(self.control_model) self.set_filtered(self.control_model)
self.set_filtered(self.filter_map(filters)) self.set_filtered(self.filter_map(filters))
@@ -272,31 +340,36 @@ class ProxyModelManager:
self.set_filtered(self.filter_toggle_off(filters, f)) self.set_filtered(self.filter_toggle_off(filters, f))
return self.filtered 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 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: if rows is None:
self.filtered = [] self.filtered = []
else: else:
self.filtered = rows self.filtered = rows
def get_filtered(self) -> list: def get_filtered(self) -> list[ServerRow]:
return self.filtered return self.filtered
def set_proxy_model(self, model: "FastInsertListStore") -> None: def set_proxy_model(self, model: "FastInsertListStore") -> None:
""" """
FastInsertListStore representation of the raw model after filtration 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 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 Raw data model prior to filtration
""" """
self.control_model = rows self.control_model = rows
def get_control(self) -> list[Any]: def get_control(self) -> list[ServerRow]:
if self.control_model is None: if self.control_model is None:
raise AttributeError( raise AttributeError(
"Expected a populated control model, but it is Nonetype" "Expected a populated control model, but it is Nonetype"
@@ -307,7 +380,7 @@ class ProxyModelManager:
self.filtered = [] self.filtered = []
self.filter_cache = {} self.filter_cache = {}
def push(self, data: list[Any]) -> None: def push(self, data: list[ServerRow]) -> None:
self.wipe_cache() self.wipe_cache()
self.set_control(data) self.set_control(data)
cur_keyword = self.filter_man.get_active_keyword() cur_keyword = self.filter_man.get_active_keyword()
+9 -10
View File
@@ -26,7 +26,7 @@ from gi.repository import Gtk, GLib
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.api.servers import A2SInfo, Record from dzgui.api.servers import A2SInfo, Record
from dzgui.controllers.mc import Controller 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 from dzgui.views.trees.tree_servers import ServerTreeView
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -34,7 +34,7 @@ LAN_TIMEOUT = 0.5
API_TIMEOUT = 3 API_TIMEOUT = 3
@dataclass @dataclass(slots=True, frozen=True)
class NewPlayerCount: class NewPlayerCount:
treeiter: Gtk.TreeIter treeiter: Gtk.TreeIter
players: int players: int
@@ -241,10 +241,7 @@ class ServerModelManager:
def update_history( def update_history(
self, self,
rows: ( rows: list["ServerRow"] | None = None,
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]]
| None
) = None,
) -> None: ) -> None:
proxy_man = self._get_proxy_man() proxy_man = self._get_proxy_man()
if rows is not None: if rows is not None:
@@ -267,7 +264,6 @@ class ServerModelManager:
self.first_iteration = False self.first_iteration = False
self.emitter.emit("load_maps", store) 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: def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
row, record = data row, record = data
proxy_man = self._get_proxy_man() proxy_man = self._get_proxy_man()
@@ -285,6 +281,10 @@ class ServerModelManager:
def remove_from_history(self, record: Servers.Record) -> None: def remove_from_history(self, record: Servers.Record) -> None:
proxy_man = self._get_proxy_man() proxy_man = self._get_proxy_man()
proxy_man.remove_row_from_control(record) 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() self.update_history()
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
@@ -453,13 +453,13 @@ class ServerModelManager:
dialog.show_all() dialog.show_all()
dialog.run() # type: ignore[no-untyped-call] 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._get_proxy_man().push(data)
self._sort_unique_maps(data) self._sort_unique_maps(data)
self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success)) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success))
def _sort_unique_maps(self, data: list[Any]) -> None: def _sort_unique_maps(self, data: list[Any]) -> None:
u_maps = set([row[1] for row in data]) u_maps = set([row.mapname for row in data])
self._set_new_maps(sorted(u_maps)) self._set_new_maps(sorted(u_maps))
def _set_new_maps(self, maps: list[str]) -> None: def _set_new_maps(self, maps: list[str]) -> None:
@@ -473,7 +473,6 @@ class ServerModelManager:
@call_on_thread(dialog.filtering) @call_on_thread(dialog.filtering)
def refilter(self, mode: FilterMode) -> None: def refilter(self, mode: FilterMode) -> None:
# FIXME: causes two wait dialogs when map selection change signal emits after loading servers
self.first_iteration = False self.first_iteration = False
proxy_man = self._get_proxy_man() proxy_man = self._get_proxy_man()
proxy_man.filter(mode) proxy_man.filter(mode)
+29
View File
@@ -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"
+6 -11
View File
@@ -11,6 +11,7 @@ from dzgui.const.enum import Preferences
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.init.prefix import get_version from dzgui.init.prefix import get_version
from dzgui.util.redact import redact_home from dzgui.util.redact import redact_home
from dzgui.util.strings import unknown
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -27,28 +28,22 @@ def get_cpu_model() -> str:
def print_servers(servers: list[str]) -> str: def print_servers(servers: list[str]) -> str:
s = "" return "\n".join(servers)
for server in servers:
s += server + "\n"
return s
def print_mods(mods: list[int]) -> str: def print_mods(mods: list[int]) -> str:
s = "" return "\n".join(map(str, mods))
for mod in mods:
s += str(mod) + "\n"
return s
def write_diagnostic(config: Path, outfile: Path) -> None: def write_diagnostic(config: Path, outfile: Path) -> None:
date = datetime.now().isoformat() date = datetime.now().isoformat()
try: try:
# FIXME: ID_LIKE not available on CachyOS # NOTE: ID_LIKE not available on CachyOS
distro = platform.freedesktop_os_release()["ID_LIKE"] distro = platform.freedesktop_os_release()["ID_LIKE"]
except Exception as e: except Exception as e:
logger.warn(e) logger.warn(e)
distro = "Unknown" distro = unknown
kernel = os.uname().release kernel = os.uname().release
cpu = get_cpu_model() cpu = get_cpu_model()
@@ -68,7 +63,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
servers = lookup(config, Preferences.IP_LIST) servers = lookup(config, Preferences.IP_LIST)
servers_pretty = print_servers(servers) servers_pretty = print_servers(servers)
# FIXME: extraneous newlines in lists of mods
template = f"""\ template = f"""\
{APP_NAME} version {version} {APP_NAME} version {version}
Date: {date} Date: {date}
@@ -82,6 +76,7 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
Mods: Mods:
{mods_pretty} {mods_pretty}
Servers: Servers:
{servers_pretty} {servers_pretty}
""" """
+9
View File
@@ -0,0 +1,9 @@
from dataclasses import dataclass
from typing import Callable
def immutable_dataclass(cls: type | None = None, **kwargs: bool) -> type | Callable[[type], type]:
kwargs["slots"] = True
kwargs["frozen"] = True
if cls is None:
return lambda c: dataclass(c, **kwargs)
return dataclass(cls, **kwargs)
+2 -3
View File
@@ -1,10 +1,10 @@
import a2s import a2s
from ipaddress import ip_address
import requests import requests
import shutil import shutil
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from ipaddress import ip_address
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.api.servers import Record from dzgui.api.servers import Record
@@ -12,12 +12,11 @@ from dzgui.config.ipdb import read_from_table
from dzgui.const.constants import REQUEST_TIMEOUT from dzgui.const.constants import REQUEST_TIMEOUT
from dzgui.const.endpoints import IP_ECHO from dzgui.const.endpoints import IP_ECHO
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@dataclass @dataclass(slots=True, frozen=True)
class Coords: class Coords:
lat: float lat: float
lon: float lon: float
+2
View File
@@ -250,6 +250,7 @@ class Crumbs:
developers: str developers: str
default: str default: str
offline: str offline: str
connect: str
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
@@ -437,6 +438,7 @@ crumbs = Crumbs(
developers="Options > Developers", developers="Options > Developers",
default="Servers > ", default="Servers > ",
offline="Mods > Play offline", offline="Mods > Play offline",
connect="Connect",
) )
no_mods = "No local mods found." no_mods = "No local mods found."
+3 -3
View File
@@ -1,9 +1,9 @@
from typing import Sequence
import gi import gi
from typing import Sequence
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
class GenericBox(Gtk.Box): class GenericBox(Gtk.Box):
+8 -4
View File
@@ -19,7 +19,11 @@ if TYPE_CHECKING:
class ContextualButton(Gtk.Button): class ContextualButton(Gtk.Button):
def __init__( def __init__(
self, label: str, opens: "NotebookPage", tooltip: str, context: ButtonType self,
label: str,
opens: "NotebookPage | None",
tooltip: str,
context: ButtonType,
) -> None: ) -> None:
super().__init__( super().__init__(
label=label, label=label,
@@ -51,9 +55,9 @@ class ButtonBox(Gtk.Box):
for side_button in ButtonType: for side_button in ButtonType:
button = ContextualButton( button = ContextualButton(
label=side_button.dict["label"], label=side_button.label,
opens=side_button.dict["opens"], opens=side_button.opens,
tooltip=side_button.dict["tooltip"], tooltip=side_button.tooltip,
context=side_button, context=side_button,
) )
+1 -1
View File
@@ -1,4 +1,4 @@
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.clip import copy_clipboard
from dzgui.strings import buttons from dzgui.strings import buttons
+1 -1
View File
@@ -43,7 +43,7 @@ class Breadcrumbs(Gtk.Label):
label = page.get_current_tab_text() label = page.get_current_tab_text()
self.set_server_crumbs(label) self.set_server_crumbs(label)
else: else:
crumbs = enum.dict["crumbs"] crumbs = enum.crumbs
self.set_crumbs(crumbs) self.set_crumbs(crumbs)
def set_server_crumbs(self, suffix: str) -> None: def set_server_crumbs(self, suffix: str) -> None:
+5 -4
View File
@@ -1,10 +1,11 @@
from typing import Self
from dzgui.util import css
import gi import gi
from typing import Self
from dzgui.util import css
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
# TODO: subclass Gtk.Frame directly # TODO: subclass Gtk.Frame directly
+2 -3
View File
@@ -1,3 +1,4 @@
import gi
from typing import Self from typing import Self
from warnings import deprecated from warnings import deprecated
@@ -8,10 +9,8 @@ from dzgui.strings import options
from dzgui.views.components.box import HBox from dzgui.views.components.box import HBox
from dzgui.views.components.buttons import Icon from dzgui.views.components.buttons import Icon
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
@deprecated("Client choice is determined at setup time") @deprecated("Client choice is determined at setup time")
+2 -2
View File
@@ -19,8 +19,8 @@ class EnumeratedModButton(Gtk.Button):
super().__init__(margin_start=10, margin_end=10, focus_on_click=False) super().__init__(margin_start=10, margin_end=10, focus_on_click=False)
self.enum = enum self.enum = enum
self.set_label(enum.dict["label"]) self.set_label(enum.label)
self.set_tooltip_text(enum.dict["tooltip"]) self.set_tooltip_text(enum.tooltip)
class ModPanelButton(EnumeratedModButton): class ModPanelButton(EnumeratedModButton):
+1 -1
View File
@@ -1,7 +1,7 @@
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk
class NoOverlayScrolledWindow(Gtk.ScrolledWindow): class NoOverlayScrolledWindow(Gtk.ScrolledWindow):
+2 -2
View File
@@ -56,7 +56,7 @@ class Statusbar(Gtk.Grid):
return return
enum = notebook.get_page_by_enum() enum = notebook.get_page_by_enum()
show_statusbar = enum.dict["statusbar"] show_statusbar = enum.has_statusbar
if show_statusbar is False: if show_statusbar is False:
self.set_by_context(enum, "") self.set_by_context(enum, "")
@@ -148,5 +148,5 @@ class Statusbar(Gtk.Grid):
row = tree.get_value_at_index(1) row = tree.get_value_at_index(1)
if self.controller.loaded is False: if self.controller.loaded is False:
return return
tooltip = row.dict["tooltip"] tooltip = row.tooltip
self.set_by_context(NotebookPage.HELP, tooltip) self.set_by_context(NotebookPage.HELP, tooltip)
+5 -11
View File
@@ -1,13 +1,9 @@
import gi
import sys import sys
from enum import Enum
from typing import Any, Literal, Self, TYPE_CHECKING from typing import Any, Literal, Self, TYPE_CHECKING
# import time
from enum import Enum
from dzgui.views.mixins.colorscheme import ColorAwareApp
# TODO: import dialog titles
from dzgui.api.mods import remove_stale_signatures as remove_stale from dzgui.api.mods import remove_stale_signatures as remove_stale
from dzgui.config.ipdb import get_ipdb from dzgui.config.ipdb import get_ipdb
from dzgui.const.constants import HEX_GREEN, HEX_RED from dzgui.const.constants import HEX_GREEN, HEX_RED
@@ -20,12 +16,10 @@ from dzgui.util.format import format_exception
from dzgui.util.strings import dialog_header from dzgui.util.strings import dialog_header
from dzgui.util.symlink import rebuild_symlinks from dzgui.util.symlink import rebuild_symlinks
from dzgui.views.components.buttons import ClipboardButton from dzgui.views.components.buttons import ClipboardButton
from dzgui.views.mixins.colorscheme import ColorAwareApp
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk # noqa from gi.repository import Gtk, GLib, Gdk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.config.xdg import Xdg from dzgui.config.xdg import Xdg
+1 -1
View File
@@ -15,7 +15,7 @@ from dzgui.views.dialogs.generic import GenericDialog
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, Pango # noqa from gi.repository import Gtk, Gdk, Pango
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.api.servers import Details from dzgui.api.servers import Details
+3 -3
View File
@@ -1,3 +1,5 @@
import gi
from typing import Any from typing import Any
from dzgui.const.constants import ( from dzgui.const.constants import (
@@ -7,10 +9,8 @@ from dzgui.const.constants import (
GIO_SETTINGS_VAL, GIO_SETTINGS_VAL,
) )
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio # noqa from gi.repository import Gtk, Gio
class ColorAwareApp: class ColorAwareApp:
+8 -8
View File
@@ -1,7 +1,7 @@
import logging import logging
from dzgui.const.constants import APP_NAME 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.keys import is_ctrl_mask, is_navkey
from dzgui.util.strings import edit_note from dzgui.util.strings import edit_note
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
@@ -58,17 +58,17 @@ class ContextMixin(TreeView):
self.context_menu.select_first(False) self.context_menu.select_first(False)
return True return True
def _process_dynamic_row(self, row: ContextMenu) -> Gtk.MenuItem: def _process_dynamic_row(self, row: ContextMenuItem) -> Gtk.MenuItem:
if row == ContextMenu.ADD_SERVER and self.is_in_favs(): # type: ignore if row == ContextMenuItem.ADD_SERVER and self.is_in_favs(): # type: ignore
row = ContextMenu.REMOVE_SERVER 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) 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 item.set_sensitive(self.is_modded()) # type: ignore
if row == ContextMenu.ADD_NOTE: if row == ContextMenuItem.ADD_NOTE:
if self.controller.has_note(): if self.controller.has_note():
item.set_label(edit_note) item.set_label(edit_note)
@@ -92,7 +92,7 @@ class ContextMixin(TreeView):
except AttributeError: except AttributeError:
return False 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 self.controller.menu_action(enum, self) # type: ignore
def _on_key(self, menu: Gtk.Menu, event: Gdk.EventKey) -> bool | None: def _on_key(self, menu: Gtk.Menu, event: Gdk.EventKey) -> bool | None:
+1 -1
View File
@@ -1,7 +1,7 @@
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa from gi.repository import Gtk, Gdk
class HelpMenuMixin: class HelpMenuMixin:
+3 -3
View File
@@ -1,10 +1,10 @@
import gi
from dzgui.const.enum import VAdjustment from dzgui.const.enum import VAdjustment
from dzgui.const.constants import SCROLL_INCREMENT from dzgui.const.constants import SCROLL_INCREMENT
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa from gi.repository import Gtk, Gdk
class ScrollableMixin: class ScrollableMixin:
+2 -3
View File
@@ -1,3 +1,4 @@
import gi
from pathlib import Path from pathlib import Path
from typing import Self, TYPE_CHECKING from typing import Self, TYPE_CHECKING
@@ -8,10 +9,8 @@ from dzgui.views.components.buttons import SteamWorkshopButton
from dzgui.views.components.scrollable import NoOverlayScrolledWindow from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import ModTreeView from dzgui.views.trees.tree_mods import ModTreeView
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa from gi.repository import Gtk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
+6 -6
View File
@@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
import gi
from enum import Enum from enum import Enum
from typing import Self, TYPE_CHECKING, Union 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 ( from dzgui.const.constants import (
APPID_DAYZ, APPID_DAYZ,
APPID_DAYZ_EXP, APPID_DAYZ_EXP,
@@ -11,8 +12,10 @@ from dzgui.const.constants import (
APPNAME_DAYZ_EXP_HUMAN, APPNAME_DAYZ_EXP_HUMAN,
) )
from dzgui.const.enum import ContextMenuGroup, NotebookPage from dzgui.const.enum import ContextMenuGroup, NotebookPage
from dzgui.const.icons import ICON_EDIT_DELETE, ICON_FOLDER, ICON_WARNING
from dzgui.managers.offline import OfflineManager from dzgui.managers.offline import OfflineManager
from dzgui.strings import generic, offline from dzgui.strings import generic, offline
from dzgui.util import css
from dzgui.views.components.box import HBox, VBox from dzgui.views.components.box import HBox, VBox
from dzgui.views.components.buttons import Icon, IconTextButton from dzgui.views.components.buttons import Icon, IconTextButton
from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.eventbox import InfoEventBox
@@ -21,11 +24,8 @@ from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.components.misc import ErrorPopover from dzgui.views.components.misc import ErrorPopover
from dzgui.views.trees.tree_mods import OfflineModTreeView from dzgui.views.trees.tree_mods import OfflineModTreeView
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GObject # noqa from gi.repository import Gtk, Gdk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
+14 -17
View File
@@ -1,9 +1,10 @@
import gi
from pathlib import Path from pathlib import Path
from typing import Any, Callable, TYPE_CHECKING from typing import Any, Callable, TYPE_CHECKING
from dzgui.api import pefile as PeFile from dzgui.api import pefile as PeFile
from dzgui.api.steam import get_last_updated from dzgui.api.steam import get_last_updated
from dzgui.config import query from dzgui.config import query
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ, APPID_DAYZ,
@@ -14,12 +15,11 @@ from dzgui.const.constants import (
FLATPAK_SANDBOX, FLATPAK_SANDBOX,
FLATPAK_STEAM_PATH, FLATPAK_STEAM_PATH,
) )
from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL
from dzgui.const.endpoints import STEAM_API_SETUP from dzgui.const.endpoints import STEAM_API_SETUP
from dzgui.const.enum import Preferences, ServerTab from dzgui.const.enum import Preferences, ServerTab
from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL
from dzgui.strings import options from dzgui.strings import options
from dzgui.util import strings, css, open_links from dzgui.util import strings, css, open_links
from dzgui.views.components.box import ShortHBox, VBox from dzgui.views.components.box import ShortHBox, VBox
from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.labels import LeftLabel from dzgui.views.components.labels import LeftLabel
@@ -29,11 +29,8 @@ from dzgui.views.components.misc import ErrorPopover
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa from gi.repository import Gtk, Gdk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
@@ -233,10 +230,19 @@ class ToggleField(Gtk.Box):
self.pack_start(self.radio1, expand=False, fill=False, padding=0) self.pack_start(self.radio1, expand=False, fill=False, padding=0)
self.pack_start(self.radio2, expand=False, fill=False, padding=0) self.pack_start(self.radio2, expand=False, fill=False, padding=0)
self.signal_id: int | None = None
def disconnect_signal(self) -> None:
if self.signal_id is not None:
self.radio1.disconnect(self.signal_id)
def set_suboption_active(self, state: bool) -> None: def set_suboption_active(self, state: bool) -> None:
# NOTE: defer connection of signal until after state is set # NOTE: defer connection of signal until after state is set
self.radio2.set_active(state) self.radio2.set_active(state)
self.radio1.connect("toggled", self._on_radio_toggled, self.context) self.disconnect_signal()
self.signal_id = self.radio1.connect(
"toggled", self._on_radio_toggled, self.context
)
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None: def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
self.controller.toggle_config(context) self.controller.toggle_config(context)
@@ -486,14 +492,5 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
start_tab = self.controller.query_config(Preferences.START_TAB) start_tab = self.controller.query_config(Preferences.START_TAB)
self.start_tab_combo.set_active(start_tab) self.start_tab_combo.set_active(start_tab)
def _suppress_toggles(self, state: bool) -> None:
for toggle in [
self.fullscreen_toggle,
self.distance_toggle,
]:
self.controller.suppress_signal(
self, toggle.get_children()[0], "_on_radio_toggled", state
)
def grab_content_area(self) -> None: def grab_content_area(self) -> None:
self.grab_focus() self.grab_focus()
+3 -20
View File
@@ -1,11 +1,12 @@
from dataclasses import dataclass import gi
from typing import Self, Sequence, TYPE_CHECKING from typing import Self, Sequence, TYPE_CHECKING
from dzgui.const.enum import NotebookPage
from dzgui.const.icons import ( from dzgui.const.icons import (
ICON_ERROR, ICON_ERROR,
ICON_WARNING, ICON_WARNING,
) )
from dzgui.const.enum import NotebookPage
from dzgui.util.css import add_class from dzgui.util.css import add_class
from dzgui.util.keys import is_ctrl_mask from dzgui.util.keys import is_ctrl_mask
from dzgui.util.localize import number from dzgui.util.localize import number
@@ -15,33 +16,15 @@ from dzgui.views.dialogs.generic import DebugDialog
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
from dzgui.views.trees.tree_server_mods import ServerModTreeView from dzgui.views.trees.tree_server_mods import ServerModTreeView
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk from gi.repository import Gdk, Gtk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.managers.connection import Prerequisites from dzgui.managers.connection import Prerequisites
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter from dzgui.controllers.emitter import Emitter
@dataclass
class Warnings:
passworded: bool
dayz_running: bool
no_steam: bool
wrong_version: bool
@dataclass
class Errors:
no_space: bool
no_dayz_exp: bool
no_dayz: bool
class Placeholder(Gtk.Label): class Placeholder(Gtk.Label):
def __init__(self, text: str) -> None: def __init__(self, text: str) -> None:
super().__init__( super().__init__(
+2 -3
View File
@@ -1,3 +1,4 @@
import gi
import logging import logging
from typing import Any, Optional, TYPE_CHECKING from typing import Any, Optional, TYPE_CHECKING
@@ -6,10 +7,8 @@ from dzgui.const.constants import APP_NAME, SEPARATOR
from dzgui.util.keys import is_ctrl_mask, is_navkey from dzgui.util.keys import is_ctrl_mask, is_navkey
from dzgui.views.mixins.cursor_mixin import CursorMixin from dzgui.views.mixins.cursor_mixin import CursorMixin
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk, Gdk, GObject
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
+2 -2
View File
@@ -10,7 +10,7 @@ from dzgui.views.trees.tree_base import TreeView
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -77,4 +77,4 @@ class MenuTreeView(TreeView):
raise ValueError("Trying to call a method on non-existent model") raise ValueError("Trying to call a method on non-existent model")
_iter = self.get_focused_row_iter() _iter = self.get_focused_row_iter()
rowtype = model.get_value(_iter, 1) rowtype = model.get_value(_iter, 1)
return str(rowtype.dict["tooltip"]) return str(rowtype.tooltip)
+3 -3
View File
@@ -1,4 +1,6 @@
import gi
import logging import logging
from typing import Any, TYPE_CHECKING from typing import Any, TYPE_CHECKING
from dzgui.const.constants import APP_NAME, HEX_RED from dzgui.const.constants import APP_NAME, HEX_RED
@@ -9,10 +11,8 @@ from dzgui.views.mixins.context_mixin import ContextMixin
from dzgui.views.mixins.mods_mixin import ModsMixin from dzgui.views.mixins.mods_mixin import ModsMixin
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk, Gdk
if TYPE_CHECKING: if TYPE_CHECKING:
+5 -5
View File
@@ -1,15 +1,15 @@
import gi
from typing import TYPE_CHECKING
from dzgui.const.enum import ContextMenuGroup from dzgui.const.enum import ContextMenuGroup
from dzgui.model.model_factory import ModelFactory from dzgui.model.model_factory import ModelFactory
from dzgui.strings import server_mods from dzgui.strings import server_mods
from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.mixins.context_mixin import ContextMixin
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
from typing import TYPE_CHECKING
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, Pango # noqa from gi.repository import Gtk, Pango
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
+12 -12
View File
@@ -1,30 +1,25 @@
import gi
import logging import logging
import threading import threading
from queue import Queue from queue import Queue
from typing import Any, Self, Union from typing import Any, Literal, Self, TYPE_CHECKING, Union
from warnings import deprecated from warnings import deprecated
from dzgui.api.servers import ping, Record from dzgui.api.servers import ping, Record
from dzgui.const.constants import APP_NAME 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.managers.filter import FilterManager
from dzgui.model.proxy_model import ProxyModelManager from dzgui.model.proxy_model import ProxyModelManager
from dzgui.util import strings from dzgui.util import strings
from dzgui.util.dist import CalcDist from dzgui.util.dist import CalcDist
from dzgui.util.keys import is_ctrl_mask from dzgui.util.keys import is_ctrl_mask
from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.mixins.context_mixin import ContextMixin
from typing import Literal, TYPE_CHECKING
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
import dzgui.util._json as JSON # noqa import dzgui.util._json as JSON # noqa
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk, GLib, Gdk, GObject
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -251,9 +246,9 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
if self.enum is ServerTab.LAN: if self.enum is ServerTab.LAN:
self.emitter.emit("request_custom_port_focus") self.emitter.emit("request_custom_port_focus")
case Gdk.KEY_c: 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: case Gdk.KEY_r:
self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self) self.controller.menu_action(ContextMenuItem.REFRESH_PLAYERS, self)
else: else:
match event.keyval: match event.keyval:
case Gdk.KEY_l | Gdk.KEY_Right: case Gdk.KEY_l | Gdk.KEY_Right:
@@ -413,5 +408,10 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
if row_index >= start[0] <= end[0]: if row_index >= start[0] <= end[0]:
# NOTE: fetch raw data rows # NOTE: fetch raw data rows
real_model = self.proxy_man.get_control() 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)) cell.set_property("text", str(value))
+5 -5
View File
@@ -1,13 +1,13 @@
import gi
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Union
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.model.model_factory import FastInsertListStore from dzgui.model.model_factory import FastInsertListStore
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class SimpleTree(Gtk.ScrolledWindow): class SimpleTree(Gtk.ScrolledWindow):
"""Simple scrollable 1-column tree""" """Simple scrollable 1-column tree"""
+2 -3
View File
@@ -1,13 +1,12 @@
import gi
import pytest import pytest
import subprocess import subprocess
from dzgui.const.constants import GIO_SETTINGS_KEY from dzgui.const.constants import GIO_SETTINGS_KEY
from dzgui.views.mixins.colorscheme import ColorAwareApp from dzgui.views.mixins.colorscheme import ColorAwareApp
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GLib # noqa from gi.repository import Gtk, GLib
class DummyWidget(ColorAwareApp, Gtk.Button): class DummyWidget(ColorAwareApp, Gtk.Button):
+25
View File
@@ -0,0 +1,25 @@
from typing import TYPE_CHECKING
from dzgui.init.migrate import clean_legacy_versions_file
if TYPE_CHECKING:
from pathlib import Path
def test_null_signature_stripping(tmp_path: "Path") -> None:
tmp = tmp_path.joinpath("dzg.versions")
lines = [
"1111,2222",
"3333,4444",
"5555,null",
"foo",
",4555",
"1111",
" ",
",,,",
"-1",
]
expect = ["1111,2222", "3333,4444"]
tmp.write_text("\n".join(lines))
clean_legacy_versions_file(tmp)
assert expect == tmp.read_text().splitlines()
+2 -2
View File
@@ -48,5 +48,5 @@ def test_signatures_with_no_ids(monkeypatch, tmp: str, versions: str) -> None:
monkeypatch.setattr("dzgui.api.mods.lookup", mock_lookup) monkeypatch.setattr("dzgui.api.mods.lookup", mock_lookup)
dzgui.api.mods.remove_stale_signatures(path, path) dzgui.api.mods.remove_stale_signatures(path, path)
with open(tmp, "r") as f: with open(tmp, "r") as f:
lines = f.readlines() lines = [line.rstrip() for line in f.readlines()]
assert lines == ["3410710885,1752864732\n", "3739934289,1780855136\n"] assert lines == ["3410710885,1752864732", "3739934289,1780855136"]
+26
View File
@@ -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]