Compare commits

..
Author SHA1 Message Date
aclist c267b1bbc7 docs: update changelog
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-12 02:12:40 +09:00
aclist d0b3a723c6 drop: unused comment 2026-11-12 02:06:36 +09:00
aclist 535bc505f7 chore: update comments 2026-11-12 02:06:20 +09:00
aclist f2375feeec feat: add get_index() method to ServerRow 2026-11-12 02:04:55 +09:00
aclist 1b9448735d chore: drop old comments 2026-11-12 02:03:59 +09:00
aclist d5693b07fb chore: update comments 2026-11-12 02:03:31 +09:00
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 c1630adee1 chore: update return value 2026-11-11 03:31:17 +09:00
aclist 88f6b84bea fix: restore dropped typehint 2026-11-11 03:29:23 +09:00
aclist 134dcb6f0b fix: unused param blocking boot 2026-11-11 03:27:12 +09:00
aclist cc0354c881 chore: drop unused imports 2026-11-11 03:21:31 +09:00
aclist f2d21be347 Merge pull request #534 from aclist/chore/strict-typehinting
chore: clear strict typehinting errors
2026-11-11 03:19:29 +09:00
aclist f65fa153cc Merge branch 'chore/strict-typehinting' of github.com:aclist/dzgui into chore/strict-typehinting
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-11 03:18:50 +09:00
aclist 8aa7328d8f chore: drop unused import 2026-11-11 03:18:40 +09:00
aclist bb04854cd7 chore: add keybinding to strings 2026-11-11 03:17:29 +09:00
aclist 5014b2a467 Merge branch 'dzgui7' into chore/strict-typehinting 2026-11-11 03:15:26 +09:00
aclist d020295aa3 Merge pull request #537 from aclist/fix/cannot-acquire-lock
fix: cannot acquire lock
2026-11-11 03:12:31 +09:00
aclist 1879347831 chore: add typehints 2026-11-11 03:09:39 +09:00
aclist 5a1c098af7 fix: acquire lock prior to calling build_gui() 2026-11-11 03:09:14 +09:00
aclist 3d346dfb07 Merge pull request #531 from aclist/chore/update-strings-pass-1
chore: update strings pass 1
2026-11-11 03:06:43 +09:00
aclist cb03db6032 Merge branch 'dzgui7' into chore/strict-typehinting 2026-11-11 02:47:40 +09:00
aclist c69022f74c Merge pull request #532 from aclist/drop/no-e402
chore: drop unused Gtk imports
2026-11-11 02:44:40 +09:00
aclist 7f0bccad64 Merge pull request #528 from aclist/drop/abstract-constructor-params
drop: abstracted constructor params
2026-11-11 02:43:30 +09:00
aclist d6b5c7bbb4 chore: drop unused imports
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-11 01:45:00 +09:00
aclist f0a07cbbba Merge branch 'dzgui7' into drop/abstract-constructor-params 2026-11-11 01:41:58 +09:00
aclist 30fa762780 fix: update bool 2026-11-11 01:35:12 +09:00
aclist 807a530550 Merge pull request #529 from aclist/change/normalize-icon-names
change: normalize icon names
2026-11-11 01:31:08 +09:00
aclist f092cfdf4a Merge pull request #530 from aclist/change/normalize-endpoint-names
change: normalize endpoint names
2026-11-11 01:22:59 +09:00
aclist d5c597ab03 Merge pull request #527 from aclist/docs/history-file-2
docs: add history file
2026-11-11 01:15:33 +09:00
aclist 06c85fdfd8 chore: add remarks about release structure
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-11-11 01:12:51 +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 0c4026daf8 chore: clear typehinting errors 2026-11-10 10:02:41 +09:00
aclist e03c6905c6 chore: clear typehinting errors 2026-11-10 06:30:12 +09:00
aclist 8fb3462f35 chore: add placeholder for row struct 2026-11-10 05:58:22 +09:00
aclist bfe9b9d147 chore: clear typehinting errors 2026-11-10 05:58:02 +09:00
aclist 9f36569fd4 drop: LoggerAlertsButton 2026-11-10 05:10:56 +09:00
aclist da18b41136 chore: drop Soup2 stubs 2026-11-10 05:09:02 +09:00
aclist f092207c61 chore: clear strict typehinting errors 2026-11-10 04:37:51 +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 15d4b71350 Merge pull request #533 from aclist/docs/more-attribution
Mirror to Codeberg / mirror-to-codeberg (push) Canceled after 0s
chore: add remarks to README
2026-11-10 03:08:12 +09:00
aclist 16d3df618a chore: add remarks to README 2026-11-10 03:07:30 +09:00
aclist 4e5fadf928 chore: drop old comments 2026-11-10 03:04:44 +09:00
aclist fa2f86b03e chore: abstract string literals 2026-11-10 02:41:24 +09:00
aclist c8fe6ec056 chore: update strings files 2026-11-10 02:37:22 +09:00
aclist 6901e5456e drop: unused dataclass 2026-11-10 02:36:18 +09:00
aclist 49298d9943 change: normalize endpoint names 2026-11-09 23:51:19 +09:00
aclist 72e3d95808 change: normalize icon names 2026-11-09 23:48:10 +09:00
aclist a42c9d4389 drop: abstracted constructor params 2026-11-09 23:28:50 +09:00
aclist cc708c64b2 docs: add history file 2026-11-09 23:19:32 +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
93 changed files with 1148 additions and 898 deletions

No files matched your search

+3 -7
View File
@@ -57,6 +57,7 @@
- List timestamp of last DayZ client update
- Validate mods via Mod page to pull in updates
- Support for Nasdara (Badlands) DLC
- Load offline mods
### Changed
- Conform to PEP 440 versioning for beta versions
@@ -99,16 +100,11 @@
- Current keyword filter not being applied when refreshing table
- Steam Flatpak cannot run DZGUI as a non-Steam game
- Collisions between native Steam and Steam Flatpak clients
### Unreleased
- Load offline mods
- Local documentation
- Raw debug command in context menu
- Sticky toggle signal on radio buttons when reloading Options
### Developer-facing
- Add pyproject.toml file
- Show deprecation warnings
- Options -> Dev page
- Raw debug command in preconnect menu
## [6.0.5] 2026-05-17
### Changed
+6 -5
View File
@@ -5,7 +5,7 @@
### Fetch submodules
git submodule update --recursive --init
### Install with dev dependencies
### Install into editable venv with dev dependencies
uv pip install -e '.[dev]'
This includes all dependencies for building, linting, testing, and checking in code.
@@ -16,7 +16,7 @@ docs: documentation tools
lint: code linting, fixing, and type checking
test: unit and integration tests
## Miscellaneous
## Build process
### Setting up precommit hooks
pre-commit install
@@ -25,6 +25,7 @@ pre-commit install
sphinx-build -M html source build -a
### Building standalone release binaries
1. Set up the build target: `rustup target add x86_64-unknown-linux-musl`
2. Run `python3 scripts/release.py`
3. Generate a release and tag against the resulting tarfile generated in `dist`
1. Install the applicable system package containing the 'musl-gcc' wrapper
2. Set up the build target: `rustup target add x86_64-unknown-linux-musl`
3. Run `python3 scripts/release.py`
4. Generate a release and tag against the resulting tarfile generated in `dist`
+124
View File
@@ -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.
+2
View File
@@ -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.
+2 -1
View File
@@ -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():
+7 -19
View File
@@ -2,10 +2,9 @@ import hashlib
import logging
import re
from concurrent.futures import wait
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from concurrent.futures import ThreadPoolExecutor, wait
from pathlib import Path
from typing import Any
from dzgui.api.steam import get_app_path
from dzgui.api.servers import get_rules, fqip_to_record
@@ -18,20 +17,11 @@ from dzgui.const.constants import (
from dzgui.config.query import lookup
from dzgui.const.enum import Preferences
from typing import Any
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)
@@ -114,10 +104,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"))
@@ -129,10 +115,12 @@ def remove_stale_signatures(
config: Path, versions: Path, ids: list[int] | None = None
) -> None:
if versions.is_file() is False:
logger.warning("Creating new version signatures file")
logger.debug("Creating new version signatures file")
versions.touch()
return
clean_legacy_versions_file(versions)
path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path)
if ids is None:
@@ -152,7 +140,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 []
+8 -5
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.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]
+6 -6
View File
@@ -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:
+2 -9
View File
@@ -6,7 +6,6 @@ from importlib import resources
from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.const.constants import (
APP_NAME,
APP_NAME_LOWER,
@@ -18,11 +17,10 @@ from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.init.migrate import (
has_new_config,
copy_state_files,
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
@@ -108,7 +106,7 @@ def load_steam_icon() -> None:
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
from gi.repository import Gtk
try:
traversable = resources.files(APP_NAME_LOWER).joinpath(ICONS_PATH)
@@ -122,8 +120,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 +154,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:
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -6,7 +6,8 @@ if TYPE_CHECKING:
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)
class UserPrefs:
is_steam_deck: bool
+3 -3
View File
@@ -12,7 +12,7 @@ from dzgui.const.constants import (
from dzgui.init.proc import is_sandboxed
@dataclass
@dataclass(slots=True, frozen=True)
class Xdg:
config: Path
columns: Path
@@ -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"]
-29
View File
@@ -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
+3 -3
View File
@@ -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"
+224 -206
View File
@@ -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 = (
+20
View File
@@ -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"
+14 -19
View File
@@ -1,22 +1,19 @@
import gi
import logging
import threading
from typing import Any, TYPE_CHECKING, Union
import dzgui.util._json as JSON # noqa
from dzgui.config.userprefs import UserPrefs
from dzgui.const.constants import APP_NAME
from dzgui.const.enum import (
ButtonType,
ContextMenu,
ContextMenuItem,
FilterMode,
NotebookPage,
Preferences,
ServerTab,
)
from dzgui.config.userprefs import UserPrefs
from dzgui.const.constants import APP_NAME
from dzgui.controllers.emitter import Emitter
from dzgui.managers.config import ConfigManager
from dzgui.managers.connection import ConnectionManager
@@ -29,8 +26,6 @@ from dzgui.util.open_links import open_user_workshop, open_workshop_page
from dzgui.views.dialogs.filepicker import FilePicker
from dzgui.views.dialogs.generic import ExceptionDialog
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, GObject
@@ -77,7 +72,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 +191,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 +259,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 +271,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 +305,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 +352,20 @@ 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()
+10 -7
View File
@@ -53,6 +53,11 @@
- Expand/collapse changelog versions
- Support local light/dark mode changes on system theme
- Server time calculator
- Add "Unmodded" filter to servers
- List timestamp of last DayZ client update
- Validate mods via Mod page to pull in updates
- Support for Nasdara (Badlands) DLC
- Load offline mods
### Changed
- Conform to PEP 440 versioning for beta versions
@@ -74,6 +79,7 @@
- Enable LAN page Empty/Full filters on first run of app
- Propagate subscribed mods to Steam client
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
- Use SQLite DB for geolocation records
### Dropped
- Battlemetrics API support
@@ -92,16 +98,13 @@
- Moved dialogs out of threads
- History table not being in lock-step with state file
- Current keyword filter not being applied when refreshing table
### Unreleased
- Load offline mods
- Local documentation
- Raw debug command in context menu
- Steam Flatpak cannot run DZGUI as a non-Steam game
- Collisions between native Steam and Steam Flatpak clients
- Sticky toggle signal on radio buttons when reloading Options
### Developer-facing
- Add pyproject.toml file
- Show deprecation warnings
- Options -> Dev page
- Raw debug command in preconnect menu
## [6.0.5] 2026-05-17
### Changed
+22 -1
View File
@@ -1,12 +1,21 @@
import json
import logging
import re
import shutil
from pathlib import Path
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.util._json import read_json, write_json
logger = logging.getLogger(APP_NAME)
def migrate_legacy_conf(config: Path) -> Any:
old_conf = Path.home() / LEGACY_CONFIG_PATH
@@ -70,3 +79,15 @@ def copy_ipdb(ips_path: Path) -> None:
return
# TODO: ensure that month file is copied
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)
+3
View File
@@ -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:
+11 -2
View File
@@ -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()
+7 -10
View File
@@ -48,7 +48,7 @@ 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
@@ -192,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(
@@ -272,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
@@ -293,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}"
@@ -332,7 +330,6 @@ class ConnectionManager:
self.thread_man.update_dialog(waiting_for_launch)
while True:
# FIXME: cancel should not be visible here per setting above
if self.controller.is_cancel_pending():
# TODO: some facility to also close spawned steam process
return
+19 -19
View File
@@ -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:
+6 -13
View File
@@ -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()
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -22,8 +22,8 @@ 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)
+2 -2
View File
@@ -55,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]
+2 -2
View File
@@ -11,7 +11,7 @@ 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
+172 -80
View File
@@ -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,121 @@ 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]
@classmethod
def get_index(cls, field: str) -> int:
_fields = [f.name for f in fields(cls)]
return _fields.index(field)
class FiltrationManager:
def __init__(self) -> None:
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 +148,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 +165,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 +173,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 +199,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 +211,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 +220,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 +244,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 +265,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 +285,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 +308,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 +330,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 +345,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 +385,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()
+14 -15
View File
@@ -26,7 +26,7 @@ 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)
@@ -34,7 +34,7 @@ LAN_TIMEOUT = 0.5
API_TIMEOUT = 3
@dataclass
@dataclass(slots=True, frozen=True)
class NewPlayerCount:
treeiter: Gtk.TreeIter
players: int
@@ -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)
@@ -341,8 +341,8 @@ class ServerModelManager:
return
config_man.add_saved_server(fqip)
# FIXME: this is valid if saved servers tab is already open,
# but not if app was just booted; causes single row to appear prematurely
# TODO: this is valid if saved servers tab is already open,
# but not if tab is unloaded; causes single row to appear prematurely
if proxy_man.has_control_model() is False:
proxy_man.push(records)
else:
@@ -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)
+14
View File
@@ -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"
+2
View File
@@ -29,3 +29,5 @@ toggle_calc = "Toggle calculator"
server_details = "Server details"
server_message = "Server message"
caught_cancel_signal = "Caught cancel signal, cleaning up"
+4
View File
@@ -0,0 +1,4 @@
all_maps = "All maps"
filters_heading = "Filters"
keyword_placeholder = "Filter by keyword"
map_placeholder = "Filter by map"
+1
View File
@@ -1 +1,2 @@
tab_changed_label = "*"
selectable_tree = "Ctrl-click to select multiple; Shift-click to select a range."
+5
View File
@@ -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}'"
+2
View File
@@ -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"
+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"
+2 -1
View File
@@ -3,4 +3,5 @@ mod_id = "ID"
up_to_date = "Up to date"
modlist = "Modlist"
checkmark = "✓"
resync="⟳"
resync = "⟳"
invalid_mod = "Invalid mod"
+6 -11
View File
@@ -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}
"""
+2 -2
View File
@@ -54,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:
@@ -67,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()
+6 -2
View File
@@ -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:
+2 -1
View File
@@ -1,4 +1,5 @@
import re
from typing import Any
from dzgui.util.localize import number
from dzgui.strings.generic import selectable_tree
@@ -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
+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)
+3 -4
View File
@@ -1,10 +1,10 @@
import a2s
from ipaddress import ip_address
import requests
import shutil
import subprocess
from dataclasses import dataclass
from ipaddress import ip_address
from typing import TYPE_CHECKING
from dzgui.api.servers import Record
@@ -12,12 +12,11 @@ from dzgui.config.ipdb import read_from_table
from dzgui.const.constants import REQUEST_TIMEOUT
from dzgui.const.endpoints import IP_ECHO
if TYPE_CHECKING:
from pathlib import Path
@dataclass
@dataclass(slots=True, frozen=True)
class Coords:
lat: float
lon: float
@@ -31,7 +30,7 @@ class ReservedIPError(Exception):
pass
def ip2list(ip: str) -> list:
def ip2list(ip: str) -> list[int]:
return [int(digit) for digit in ip.split(".")]
+3 -3
View File
@@ -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."
+3 -3
View File
@@ -1,9 +1,9 @@
from typing import Sequence
import gi
from typing import Sequence
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
from gi.repository import Gtk
class GenericBox(Gtk.Box):
+10 -6
View File
@@ -1,7 +1,7 @@
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
@@ -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()
+24 -55
View File
@@ -1,21 +1,18 @@
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
@@ -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__()
+15 -14
View File
@@ -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,
@@ -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:
+1 -1
View File
@@ -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:
+11 -13
View File
@@ -1,9 +1,10 @@
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
@@ -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:
+2 -2
View File
@@ -1,7 +1,7 @@
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
@@ -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()
+16 -17
View File
@@ -3,14 +3,15 @@ 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
@@ -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__(
@@ -96,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
@@ -112,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)
@@ -177,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()
@@ -198,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)
@@ -214,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()
@@ -308,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
+5 -4
View File
@@ -1,10 +1,11 @@
from typing import Self
from dzgui.util import css
import gi
from typing import Self
from dzgui.util import css
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
from gi.repository import Gtk
# TODO: subclass Gtk.Frame directly
+5 -5
View File
@@ -1,16 +1,16 @@
import gi
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
from dzgui.views.components.buttons import Icon
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
from gi.repository import Gtk
@deprecated("Client choice is determined at setup time")
@@ -73,7 +73,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()
+5 -6
View File
@@ -1,7 +1,6 @@
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
@@ -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)
+10 -14
View File
@@ -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
@@ -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
@@ -110,14 +106,15 @@ class RightPanel(Gtk.Box):
if page.loaded is True:
return
self.filters_vbox.set_sensitive(False)
# TODO: unless it is lan page
# self.refresh_button.set_sensitive(False)
self.refresh_button.set_sensitive(False)
def _on_lan_page_init(self, emitter: "Emitter") -> None:
self.filters_vbox.set_sensitive(False)
def _on_servers_loaded(self, emitter: "Emitter", context: "ServerTab") -> None:
# TODO: similar logic on notebook page change
# TODO: wipe maps store when changing tabs if model is None
state = self.controller.has_server_model()
self.filters_vbox.set_sensitive(state)
@@ -131,7 +128,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)
+1 -1
View File
@@ -1,7 +1,7 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
from gi.repository import Gtk
class NoOverlayScrolledWindow(Gtk.ScrolledWindow):
+3 -7
View File
@@ -2,7 +2,6 @@ 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
@@ -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, "")
@@ -105,7 +101,7 @@ class Statusbar(Gtk.Grid):
if type(context) is NotebookPage:
self.prior_enum = context
self.prior_status = ""
# FIXME: CalcDist is being called when table is not loaded
if dist is None:
self.set_by_context(context, "")
return
@@ -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)
+8 -15
View File
@@ -1,32 +1,25 @@
import gi
import sys
from enum import Enum
from typing import Any, Literal, Self, TYPE_CHECKING
# import time
from enum import Enum
from dzgui.views.mixins.colorscheme import ColorAwareApp
# TODO: import dialog titles
from dzgui.api.mods import remove_stale_signatures as remove_stale
from dzgui.config.ipdb import get_ipdb
from dzgui.const.constants import HEX_GREEN, HEX_RED
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
from dzgui.util.strings import dialog_header
from dzgui.util.symlink import rebuild_symlinks
from dzgui.views.components.buttons import ClipboardButton
import gi
from dzgui.views.mixins.colorscheme import ColorAwareApp
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk # noqa
from gi.repository import Gtk, GLib, Gdk
if TYPE_CHECKING:
from dzgui.config.xdg import Xdg
@@ -105,7 +98,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 +111,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()
+3 -3
View File
@@ -3,7 +3,7 @@ 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
@@ -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:
+1 -1
View File
@@ -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:
+7 -6
View File
@@ -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,17 +43,18 @@ 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
if res == Gtk.ResponseType.OK:
folder = self.get_current_folder()
# TODO: this should never be possible
if folder is None:
try:
uri = self.get_uri()
+8 -10
View File
@@ -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
@@ -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)
+1 -2
View File
@@ -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
@@ -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)
+9 -17
View File
@@ -1,5 +1,6 @@
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
@@ -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()
+3 -3
View File
@@ -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
@@ -15,7 +15,7 @@ from dzgui.views.dialogs.generic import GenericDialog
from dzgui.views.trees.tree_base import TreeView
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, Pango # noqa
from gi.repository import Gtk, Gdk, Pango
if TYPE_CHECKING:
from dzgui.api.servers import Details
@@ -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)
+4 -4
View File
@@ -22,7 +22,7 @@ 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():
+10 -5
View File
@@ -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)
+3 -3
View File
@@ -1,3 +1,5 @@
import gi
from typing import Any
from dzgui.const.constants import (
@@ -7,10 +9,8 @@ from dzgui.const.constants import (
GIO_SETTINGS_VAL,
)
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio # noqa
from gi.repository import Gtk, Gio
class ColorAwareApp:
+10 -12
View File
@@ -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
@@ -15,9 +15,7 @@ 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:
+1 -1
View File
@@ -1,7 +1,7 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa
from gi.repository import Gtk, Gdk
class HelpMenuMixin:
+3 -3
View File
@@ -1,10 +1,10 @@
import gi
from dzgui.const.enum import VAdjustment
from dzgui.const.constants import SCROLL_INCREMENT
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa
from gi.repository import Gtk, Gdk
class ScrollableMixin:
+4 -3
View File
@@ -13,6 +13,7 @@ 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,
+2 -3
View File
@@ -1,3 +1,4 @@
import gi
from pathlib import Path
from typing import Self, TYPE_CHECKING
@@ -8,10 +9,8 @@ from dzgui.views.components.buttons import SteamWorkshopButton
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import ModTreeView
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa
from gi.repository import Gtk
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
+9 -12
View File
@@ -1,20 +1,21 @@
from __future__ import annotations
import gi
from enum import Enum
from typing import Self, TYPE_CHECKING, Union
from dzgui.util import css
from dzgui.const.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.const.icons import ICON_EDIT_DELETE, ICON_FOLDER, ICON_WARNING
from dzgui.managers.offline import OfflineManager
from dzgui.strings import generic, offline
from dzgui.util import css
from dzgui.views.components.box import HBox, VBox
from dzgui.views.components.buttons import Icon, IconTextButton
from dzgui.views.components.eventbox import InfoEventBox
@@ -23,11 +24,8 @@ from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.components.misc import ErrorPopover
from dzgui.views.trees.tree_mods import OfflineModTreeView
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GObject # noqa
from gi.repository import Gtk, Gdk
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
@@ -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()
@@ -138,7 +136,6 @@ class FolderHBox(HBox):
self.folder = ""
self.label.set_label("")
self.unset_button.hide()
# FIXME: rename signal
self.emitter.emit("custom_mods_unloaded", self)
def get_button(self) -> Gtk.Button:
@@ -195,7 +192,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])
+26 -32
View File
@@ -1,9 +1,10 @@
import gi
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
from dzgui.config import query
from dzgui.const.constants import (
APPID_DAYZ,
@@ -13,17 +14,12 @@ 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.endpoints import STEAM_API_SETUP
from dzgui.const.enum import Preferences, ServerTab
from dzgui.const.icons import ICON_VIEW_CONCEAL, ICON_VIEW_REVEAL
from dzgui.strings import options
from dzgui.util import strings, css, open_links
from dzgui.views.components.box import ShortHBox, VBox
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.labels import LeftLabel
@@ -33,11 +29,8 @@ from dzgui.views.components.misc import ErrorPopover
from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa
from gi.repository import Gtk, Gdk
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
@@ -55,7 +48,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 +60,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 +124,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 +136,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 +203,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,13 +227,22 @@ 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)
self.signal_id: int | None = None
def disconnect_signal(self) -> None:
if self.signal_id is not None:
self.radio1.disconnect(self.signal_id)
def set_suboption_active(self, state: bool) -> None:
# NOTE: defer connection of signal until after state is set
self.radio2.set_active(state)
self.radio1.connect("toggled", self._on_radio_toggled, self.context)
self.disconnect_signal()
self.signal_id = self.radio1.connect(
"toggled", self._on_radio_toggled, self.context
)
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
self.controller.toggle_config(context)
@@ -406,7 +408,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 +442,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)
@@ -489,14 +492,5 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
start_tab = self.controller.query_config(Preferences.START_TAB)
self.start_tab_combo.set_active(start_tab)
def _suppress_toggles(self, state: bool) -> None:
for toggle in [
self.fullscreen_toggle,
self.distance_toggle,
]:
self.controller.suppress_signal(
self, toggle.get_children()[0], "_on_radio_toggled", state
)
def grab_content_area(self) -> None:
self.grab_focus()
+9 -26
View File
@@ -1,11 +1,12 @@
from dataclasses import dataclass
import gi
from typing import Self, Sequence, TYPE_CHECKING
from dzgui.const.constants import (
ERROR,
WARNING,
)
from dzgui.const.enum import NotebookPage
from dzgui.const.icons import (
ICON_ERROR,
ICON_WARNING,
)
from dzgui.util.css import add_class
from dzgui.util.keys import is_ctrl_mask
from dzgui.util.localize import number
@@ -15,33 +16,15 @@ from dzgui.views.dialogs.generic import DebugDialog
from dzgui.views.components.frame import HeadingFrame
from dzgui.views.trees.tree_server_mods import ServerModTreeView
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk
if TYPE_CHECKING:
from dzgui.managers.connection import Prerequisites
from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
@dataclass
class Warnings:
passworded: bool
dayz_running: bool
no_steam: bool
wrong_version: bool
@dataclass
class Errors:
no_space: bool
no_dayz_exp: bool
no_dayz: bool
class Placeholder(Gtk.Label):
def __init__(self, text: str) -> None:
super().__init__(
@@ -166,7 +149,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 +158,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 +214,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()
+2 -2
View File
@@ -5,6 +5,7 @@ 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
@@ -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()
+11 -28
View File
@@ -1,3 +1,4 @@
import gi
import logging
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.views.mixins.cursor_mixin import CursorMixin
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
from gi.repository import Gtk, Gdk, GObject
logger = logging.getLogger(APP_NAME)
@@ -79,11 +78,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 +139,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 +157,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 +181,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:
+2 -2
View File
@@ -10,7 +10,7 @@ from dzgui.views.trees.tree_base import TreeView
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
from gi.repository import Gtk
logger = logging.getLogger(APP_NAME)
@@ -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)
+3 -3
View File
@@ -1,4 +1,6 @@
import gi
import logging
from typing import Any, TYPE_CHECKING
from dzgui.const.constants import APP_NAME, HEX_RED
@@ -9,10 +11,8 @@ from dzgui.views.mixins.context_mixin import ContextMixin
from dzgui.views.mixins.mods_mixin import ModsMixin
from dzgui.views.trees.tree_base import TreeView
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
from gi.repository import Gtk, Gdk
if TYPE_CHECKING:
+5 -5
View File
@@ -1,15 +1,15 @@
import gi
from typing import TYPE_CHECKING
from dzgui.const.enum import ContextMenuGroup
from dzgui.model.model_factory import ModelFactory
from dzgui.strings import server_mods
from dzgui.views.mixins.context_mixin import ContextMixin
from dzgui.views.trees.tree_base import TreeView
from typing import TYPE_CHECKING
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, Pango # noqa
from gi.repository import Gtk, Pango
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
+35 -40
View File
@@ -1,30 +1,25 @@
import gi
import logging
import threading
from queue import Queue
from typing import Any, Self, Union
from typing import Any, Literal, Self, TYPE_CHECKING, Union
from warnings import deprecated
from dzgui.api.servers import ping, Record
from dzgui.const.constants import APP_NAME
from dzgui.const.enum import 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.model.proxy_model import ProxyModelManager, ServerRow
from dzgui.util import strings
from dzgui.util.dist import CalcDist
from dzgui.util.keys import is_ctrl_mask
from dzgui.views.mixins.context_mixin import ContextMixin
from typing import Literal, TYPE_CHECKING
from dzgui.views.trees.tree_base import TreeView
import dzgui.util._json as JSON # noqa
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
from gi.repository import Gtk, GLib, Gdk, GObject
logger = logging.getLogger(APP_NAME)
@@ -32,9 +27,16 @@ 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
# TODO: consts
QUEUE_CHECK_DELAY = 200
IND_NAME = ServerRow.get_index("name")
IND_ADDR = ServerRow.get_index("ip")
IND_QPORT = ServerRow.get_index("qport")
IND_PING = ServerRow.get_index("ping")
IND_MODDED = ServerRow.get_index("modded")
class ServerTreeView(ContextMixin, TreeView): # type: ignore
def __init__(
@@ -64,7 +66,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] = []
@@ -144,8 +146,8 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
if model is None:
return False
tree_iter = model.get_iter(path[0])
ip = model.get_value(tree_iter, 7)
qport = model.get_value(tree_iter, 8)
ip = model.get_value(tree_iter, IND_ADDR)
qport = model.get_value(tree_iter, IND_QPORT)
addr = ip + ":" + str(qport)
note = self.controller.get_note_by_record(addr)
@@ -168,14 +170,12 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
return self.enum
def _on_map(self, widget: Self) -> None:
# TODO: disable filter panel if current model is None
state = self.get_enum() is ServerTab.LAN
self.emitter.emit("lan_tab_toggled", state)
store = self.filter_man.get_map_store()
# FIXME: if model is none, wipe maps
# distinguish this signal from changing map combo in-situ
# TODO: distinguish this signal from changing map combo in-situ
self.emitter.emit("load_maps", store)
self.handler_id = self.emitter.connect("statusbar_loaded", self.start_distcalc)
self.start_queue_checker()
@@ -220,7 +220,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
def _check_result_queue(self) -> Literal[True]:
latest_result = None
# FIXME: empty results cause statusbar to be emptied
# TODO: Nonetype results cause statusbar to be blank
while not self.queue.empty():
latest_result = self.queue.get()
@@ -250,9 +250,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:
@@ -266,15 +266,9 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
(model, pathlist) = sels
path = pathlist[0]
tree_iter = model.get_iter(path)
has_mods = model.get_value(tree_iter, 11)
has_mods = model.get_value(tree_iter, IND_MODDED)
return bool(has_mods)
# def get_selected_row(self) -> Gtk.TreeModelRow:
# sel = self.get_selection()
# sels = sel.get_selected_rows()
# print(type(sels[0]))
# return sels[0]
def is_in_favs(self) -> bool:
record = self.get_record_string()
if self.controller.get_config_man().is_in_favs(record):
@@ -303,17 +297,17 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
self.start_distcalc()
def get_name(self) -> str:
return str(self.get_value_at_index(0))
return str(self.get_value_at_index(IND_NAME))
def get_simplified_ip(self) -> str:
addr = self.get_value_at_index(7)
qport = self.get_value_at_index(8)
addr = self.get_value_at_index(IND_ADDR)
qport = self.get_value_at_index(IND_QPORT)
ip = addr.split(":")[0]
return f"{ip}:{qport}"
def get_record_string(self) -> str:
addr = self.get_value_at_index(7)
qport = self.get_value_at_index(8)
addr = self.get_value_at_index(IND_ADDR)
qport = self.get_value_at_index(IND_QPORT)
return f"{addr}:{qport}"
def get_record(self) -> Record | None:
@@ -352,8 +346,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
def _redraw(
self, model: "FastInsertListStore", _iter: Gtk.TreeIter, _ping: int
) -> None:
ping_column = 9
model.set(_iter, ping_column, _ping)
model.set(_iter, IND_PING, _ping)
self.queue_draw()
def wipe_cache(self) -> None:
@@ -368,13 +361,10 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
data: Any,
) -> None:
addr_column = 7
qport_column = 8
addr = model.get_value(_iter, addr_column).split(":")
addr = model.get_value(_iter, IND_ADDR).split(":")
ip = addr[0]
gameport = int(addr[1])
qport = model.get_value(_iter, qport_column)
qport = model.get_value(_iter, IND_QPORT)
record = Record(ip, gameport, qport)
if record in self.seen_cache:
@@ -406,11 +396,16 @@ 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))
+5 -5
View File
@@ -1,13 +1,13 @@
import gi
from typing import TYPE_CHECKING, Union
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
if TYPE_CHECKING:
from dzgui.model.model_factory import FastInsertListStore
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class SimpleTree(Gtk.ScrolledWindow):
"""Simple scrollable 1-column tree"""
+1 -1
View File
@@ -105,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 = [
+2 -3
View File
@@ -1,13 +1,12 @@
import gi
import pytest
import subprocess
from dzgui.const.constants import GIO_SETTINGS_KEY
from dzgui.views.mixins.colorscheme import ColorAwareApp
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GLib # noqa
from gi.repository import Gtk, GLib
class DummyWidget(ColorAwareApp, Gtk.Button):
+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)
dzgui.api.mods.remove_stale_signatures(path, path)
with open(tmp, "r") as f:
lines = f.readlines()
assert lines == ["3410710885,1752864732\n", "3739934289,1780855136\n"]
lines = [line.rstrip() for line in f.readlines()]
assert lines == ["3410710885,1752864732", "3739934289,1780855136"]
+1 -1
View File
@@ -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
+5 -3
View File
@@ -7,7 +7,7 @@ from dzgui.api.steam import (
VDFLoadError,
AppNotInstalledError,
AppMovedError,
get_app_path
get_app_path,
)
from dzgui.config.query import get_config
@@ -26,9 +26,8 @@ def default_steam_path():
conf = get_config(xdg.config)
return conf["default_steam_path"]
# TODO: additional parsing tests
# TODO: split into separate test file
# FIXME: use fixtures
def test_pefile_length(default_steam_path):
try:
pe_file_path = PeFile.get_pefile_path(Path(default_steam_path), APPID_DAYZ)
@@ -47,6 +46,7 @@ def test_invalid_pefile_path():
raise e
# TODO: split into separate test file
@pytest.mark.parametrize(
"fixture, exception",
[
@@ -57,6 +57,7 @@ def test_invalid_pefile_path():
)
def test_not_in_library(monkeypatch, fixture, exception):
import dzgui.api.steam as steam
monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", fixture)
folder_path = Path(fixture_path("api"))
@@ -74,6 +75,7 @@ def second_drive():
def test_on_second_drive(monkeypatch, second_drive):
import dzgui.api.steam as steam
monkeypatch.setattr(steam, "LIBRARYFOLDERS_PATH", second_drive)
folder_path = Path(fixture_path("api"))
path = get_app_path(folder_path, APPID_DAYZ)
+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]