mirror of
https://github.com/aclist/dztui.git
synced 2026-08-25 17:32:36 +02:00
Compare commits
166 Commits
28cd6b8c45
...
04e9e5dcf0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04e9e5dcf0 | ||
|
|
1b06fcae42 | ||
|
|
9759c39fd7 | ||
|
|
4451eee849 | ||
|
|
3f587f64a0 | ||
|
|
b00523661f | ||
|
|
f24b3574b8 | ||
|
|
fad8d2f43b | ||
|
|
e1e034fffd | ||
|
|
ea2ca684d6 | ||
|
|
2e2d3484c4 | ||
|
|
1254044ef8 | ||
|
|
7d651e9653 | ||
|
|
d9f392f8c9 | ||
|
|
6281ed37b3 | ||
|
|
0bb2ae2266 | ||
|
|
89b4e2ec98 | ||
|
|
b6d3f8d385 | ||
|
|
1b6ebac3a3 | ||
|
|
629c974b9d | ||
|
|
371eaa34ac | ||
|
|
c29346e134 | ||
|
|
79fd96e834 | ||
|
|
cc1948d225 | ||
|
|
e32c7fcacb | ||
|
|
76b3186ed7 | ||
|
|
493818b770 | ||
|
|
e2a6fa0c7e | ||
|
|
0a310f0b70 | ||
|
|
3f1aff7cc6 | ||
|
|
c348a499b0 | ||
|
|
5ff936f266 | ||
|
|
dfeafc3d28 | ||
|
|
96b91fd87c | ||
|
|
71d760dc56 | ||
|
|
aeffce2579 | ||
|
|
93d870b5bf | ||
|
|
63c2b5acd4 | ||
|
|
0d5e190b75 | ||
|
|
e9d064820e | ||
|
|
17b1a75745 | ||
|
|
e7ff025605 | ||
|
|
ec004aad5d | ||
|
|
bd022ea1d8 | ||
|
|
52b18f710f | ||
|
|
9ec3fa09e3 | ||
|
|
cb9ffb5b99 | ||
|
|
37c3937aee | ||
|
|
cf850c8f34 | ||
|
|
1332026c8c | ||
|
|
4eaedd0cdd | ||
|
|
b1f189bff8 | ||
|
|
b153ab875e | ||
|
|
de4e74daa1 | ||
|
|
0dfcc5a387 | ||
|
|
c64631cdca | ||
|
|
97ddbcdf0a | ||
|
|
ddc18e5705 | ||
|
|
6fb83b5c31 | ||
|
|
b5105ca9f9 | ||
|
|
05fab5774e | ||
|
|
203cf0da7f | ||
|
|
7dfdb4209a | ||
|
|
c03aa8f166 | ||
|
|
c34fa2fe03 | ||
|
|
0f4d611345 | ||
|
|
ca1958b0a6 | ||
|
|
c7bd6f635c | ||
|
|
2b394553b3 | ||
|
|
5b83a26f6a | ||
|
|
6ded9a3eec | ||
|
|
1a3154712f | ||
|
|
9f8b760c79 | ||
|
|
0f59d92bc3 | ||
|
|
cca1006efb | ||
|
|
027e6c0781 | ||
|
|
3932a3aed4 | ||
|
|
22c9be36a6 | ||
|
|
a2938c17f5 | ||
|
|
48b0660ebd | ||
|
|
5eb89a172d | ||
|
|
c037cf9c40 | ||
|
|
7cc8076df1 | ||
|
|
b300b614a0 | ||
|
|
0bb8f3206f | ||
|
|
8087f3a707 | ||
|
|
0ebc25b3b4 | ||
|
|
1e073af27a | ||
|
|
250da7a635 | ||
|
|
8098c4b68c | ||
|
|
1e8a423ec3 | ||
|
|
067b77bf92 | ||
|
|
d0ccc3bf4c | ||
|
|
38cc55530f | ||
|
|
5536180fec | ||
|
|
79bbc25714 | ||
|
|
456c6e62e8 | ||
|
|
eeffdb57f3 | ||
|
|
88ad8cf7c2 | ||
|
|
9f9a88d2ef | ||
|
|
f7e0b44379 | ||
|
|
ee33ce6662 | ||
|
|
a421d4c70b | ||
|
|
080b99cda8 | ||
|
|
afca9e5e6a | ||
|
|
a97c17f66b | ||
|
|
fe8bf611e1 | ||
|
|
70ccbf29f3 | ||
|
|
f5d595eccd | ||
|
|
fd7a549dbc | ||
|
|
6c810b28a9 | ||
|
|
1a1145c79c | ||
|
|
510719e3ec | ||
|
|
e06775c3c8 | ||
|
|
f5c71200a8 | ||
|
|
557e221993 | ||
|
|
dd30fa33d1 | ||
|
|
1753a40b94 | ||
|
|
a72d692c68 | ||
|
|
ce1cad6dcf | ||
|
|
9b24928f74 | ||
|
|
87f167bd53 | ||
|
|
4917c0ce2e | ||
|
|
cc485289e3 | ||
|
|
69cba3460d | ||
|
|
dfd6850d6d | ||
|
|
8d1d5bde80 | ||
|
|
533def5061 | ||
|
|
c179e3a9e9 | ||
|
|
2b8e0944a4 | ||
|
|
cd4bc7ff4e | ||
|
|
b209c9912d | ||
|
|
bfb0ffc9ad | ||
|
|
0eb47a0427 | ||
|
|
ea785960d8 | ||
|
|
7102c21f5a | ||
|
|
22294d7125 | ||
|
|
e9ce292368 | ||
|
|
5532e4e267 | ||
|
|
120703e702 | ||
|
|
b31896387a | ||
|
|
e5bb25cd60 | ||
|
|
e59b7e01fd | ||
|
|
7994bf988c | ||
|
|
90b1c875df | ||
|
|
b61e806009 | ||
|
|
b46d0614c9 | ||
|
|
c73864d545 | ||
|
|
314fed559c | ||
|
|
1c346a6c3c | ||
|
|
65e11ddd03 | ||
|
|
a1c0592c7d | ||
|
|
22c82677cb | ||
|
|
c5b41346ca | ||
|
|
c2fc9b2cca | ||
|
|
ba9d6e1d3b | ||
|
|
be6ab2c9cb | ||
|
|
2413dd1f14 | ||
|
|
9d8d37e110 | ||
|
|
fc28d419bf | ||
|
|
095af395f9 | ||
|
|
4ada06d305 | ||
|
|
5a0ab92985 | ||
|
|
f2501871ee | ||
|
|
cf70182733 | ||
|
|
42feeae8d0 |
16
CHANGELOG.md
16
CHANGELOG.md
@ -3,6 +3,8 @@
|
|||||||
## [7.0.0] Unreleased
|
## [7.0.0] Unreleased
|
||||||
### Added
|
### Added
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
|
- Uninstall wizard
|
||||||
|
- Descriptive details panel to certain error dialogs
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
- Documentation ships with source
|
- Documentation ships with source
|
||||||
@ -24,6 +26,7 @@
|
|||||||
- Set favorite server from tables
|
- Set favorite server from tables
|
||||||
- Detailed/copyable trace in critical error dialogs
|
- Detailed/copyable trace in critical error dialogs
|
||||||
- Visual icons
|
- Visual icons
|
||||||
|
- Unset favorite server button
|
||||||
- Integrated server notebook
|
- Integrated server notebook
|
||||||
- Propagate width changes to all tables
|
- Propagate width changes to all tables
|
||||||
- Remember tree position in menus
|
- Remember tree position in menus
|
||||||
@ -45,11 +48,18 @@
|
|||||||
- Generate Steam shortcuts and cover art
|
- Generate Steam shortcuts and cover art
|
||||||
- Warn user if background downloads are disabled
|
- Warn user if background downloads are disabled
|
||||||
- Dynamic copy button
|
- Dynamic copy button
|
||||||
|
- Update map count (-u flag)
|
||||||
|
- Filter button tooltips
|
||||||
|
- Expand/collapse changelog versions
|
||||||
|
- Support local light/dark mode changes on system theme
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
|
- Enforce fullscreen mode on Steam Deck
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
|
- Changed some dialog error messages from popups to popovers
|
||||||
|
- Changed how filter buttons are packed in sidebar
|
||||||
- Bold labels inside frames
|
- Bold labels inside frames
|
||||||
- Sidebar buttons do not steal focus
|
- Sidebar buttons do not steal focus
|
||||||
- Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method
|
- Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method
|
||||||
@ -62,16 +72,20 @@
|
|||||||
- Reduce size of geolocation DB on disk (~100MB)
|
- Reduce size of geolocation DB on disk (~100MB)
|
||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
- Propagate subscribed mods to Steam client
|
- Propagate subscribed mods to Steam client
|
||||||
|
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
||||||
|
|
||||||
### Dropped
|
### Dropped
|
||||||
- Battlemetrics API support
|
- Battlemetrics API support
|
||||||
- Debug mode
|
- Unused metadata from system log files
|
||||||
- Branch switching
|
- Branch switching
|
||||||
- Manual mod install mode (describe rationale)
|
- Manual mod install mode (describe rationale)
|
||||||
- Force update mods
|
- Force update mods
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
||||||
|
- In-game time field missing certain ranges of time at night
|
||||||
|
- Wrap long entries in changelog
|
||||||
|
- Certain server descriptions with special characters not rendering correctly
|
||||||
- Center server title text on server dialogs
|
- Center server title text on server dialogs
|
||||||
- Rare segfaults when changing maps (threading)
|
- Rare segfaults when changing maps (threading)
|
||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
|
|||||||
@ -67,3 +67,7 @@ It is recommended to follow
|
|||||||
integrates well with tooling and helps the project review your code.
|
integrates well with tooling and helps the project review your code.
|
||||||
|
|
||||||
Please see DEVELOPERS.md for further details.
|
Please see DEVELOPERS.md for further details.
|
||||||
|
|
||||||
|
## License applicability to contributions
|
||||||
|
|
||||||
|
Code contributed to this repository becomes subject to the terms of the GPL license defined in the LICENSE file at the project root.
|
||||||
|
|||||||
@ -8,7 +8,7 @@ runtime environment are built-in.
|
|||||||
Turnkey installer
|
Turnkey installer
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
- Visit the project's `Releases page <https://github.com/aclist/dztui/releases>`_.
|
- Visit the project's `Releases page <https://github.com/aclist/dztui/releases/latest>`_.
|
||||||
- Extract the DZGUI tarball from the top of the **Assets** list.
|
- Extract the DZGUI tarball from the top of the **Assets** list.
|
||||||
- From a terminal, run the command ``./dzgui`` or double click on the extracted file in
|
- From a terminal, run the command ``./dzgui`` or double click on the extracted file in
|
||||||
a file explorer.
|
a file explorer.
|
||||||
|
|||||||
@ -3,17 +3,22 @@ Knowledge Base
|
|||||||
|
|
||||||
.. _DZG-001:
|
.. _DZG-001:
|
||||||
|
|
||||||
DZG-001: Periodically getting dropped from servers, or servers time out in DZGUI
|
DZG-001: Timeouts occur while trying to query a specific server
|
||||||
------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------
|
||||||
DayZ opens a large number of connections while querying/connected to servers.
|
The leading cause of specific servers periodically timing out is local network configuration.
|
||||||
|
|
||||||
In addition, packets sent from server responses are expected to be a standard size (see warning below).
|
Many third-party DayZ servers use a server rental/hosting provider with DDoS protection.
|
||||||
|
This can cause responses from servers to originate from a server other than the one originally queried.
|
||||||
|
|
||||||
If your network does not have enough headroom or has settings departing from defaults, this may lead to getting dropped from servers,
|
Consumer-grade routers are likely to drop this traffic as invalid due to how they handle NAT (network address translation).
|
||||||
unresponsiveness, or a timeout.
|
By contrast, wireless routers and enterprise-grade routers may be less likely to have this issue.
|
||||||
|
|
||||||
|
If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for
|
||||||
|
the server's query port.
|
||||||
|
|
||||||
|
In addition, a uniform packet size is expected. If your router's MTU setting differs from the default of 1,500,
|
||||||
|
it could lead to incoming responses being discarded (see warning below).
|
||||||
|
|
||||||
If you are on Wi-Fi, try switching to a wired connection and see if the problem resolves itself. Consumer Wi-Fi routers
|
|
||||||
tend to have less headroom than their wired counterparts.
|
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
Ensure that MTU (maximum tranmission unit) on your network is set to the standard size of 1,500 bytes.
|
Ensure that MTU (maximum tranmission unit) on your network is set to the standard size of 1,500 bytes.
|
||||||
@ -80,3 +85,28 @@ There is some misconception that a Steam Web API key could be used to gain infor
|
|||||||
A Steam Web API key is the most strict way of getting authentic, reliable, and consistent server information in a zero-trust model.
|
A Steam Web API key is the most strict way of getting authentic, reliable, and consistent server information in a zero-trust model.
|
||||||
|
|
||||||
You are responsible for the creation, storage, management, and revocation of your Web API key.
|
You are responsible for the creation, storage, management, and revocation of your Web API key.
|
||||||
|
|
||||||
|
DZG-008: Periodically getting dropped from servers while connected
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
In some cases, DayZ opens a large number of connections while connected to servers.
|
||||||
|
|
||||||
|
If your network does not have enough headroom or has settings departing from defaults, this may lead to getting dropped from servers,
|
||||||
|
unresponsiveness, or a timeout.
|
||||||
|
|
||||||
|
If you are on Wi-Fi, try switching to a wired connection and see if the problem resolves itself. Consumer Wi-Fi routers
|
||||||
|
tend to have less headroom for simultaneous connections than their wired counterparts.
|
||||||
|
|
||||||
|
DZG-009: Floating dialogs appear maximized on tiling window managers
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
|
||||||
|
The main DZGUI window and its child dialogs are expected to be rendered as floating by your window manager.
|
||||||
|
DZGUI sends window manager hints to this effect, but tiling window managers designed to bisect the screen into quadrants (e.g., i3 window manager)
|
||||||
|
may try to always launch applications in fullscreen.
|
||||||
|
|
||||||
|
To resolve this, set specific exclusions or window hints in your WM's configuration file.
|
||||||
|
For example, for i3, add the following to your `XDG_CONFIG_HOME/i3/config` file (defaults to `$HOME/.config/i3/config`):
|
||||||
|
|
||||||
|
.. code:: console
|
||||||
|
|
||||||
|
for_window [instance="DZGUI"] floating enable, move position center
|
||||||
|
for_window [instance="DZGUI - Dialog"] floating enable, move position center
|
||||||
|
|||||||
@ -4,7 +4,7 @@ Setup
|
|||||||
DayZ license
|
DayZ license
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
Prepare a Steam account with a DayZ license (i.e., own the game). From Steam's right-click menu for the game options, under ``Compatibility``, enable a Proton version ≥ ``6.8`` (or use Proton
|
Prepare a Steam account with a DayZ license (i.e., own the game). From Steam's right-click menu for the game options, under ``Compatibility``, enable a Proton version ≥ ``6.8`` (or use Proton
|
||||||
Experimental for the latest version).
|
Experimental for the latest version).
|
||||||
|
|
||||||
As of this writing, any recent version of Proton should work, and it is encouraged to use the most recent one.
|
As of this writing, any recent version of Proton should work, and it is encouraged to use the most recent one.
|
||||||
@ -33,20 +33,10 @@ Once configured, you can insert this key in the app when launching it for the fi
|
|||||||
|
|
||||||
Battlemetrics API key
|
Battlemetrics API key
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
**This step is optional, but recommended**. Using this key lets you also connect to and query servers by their shorthand ID instead of by IP.
|
.. warning::
|
||||||
|
|
||||||
Register for an API key at `BattleMetrics <https://www.battlemetrics.com/account/register?after=%2Fdevelopers>`_ (free).
|
This feature is no longer supported in DZGUI. Battlemetrics has switched to a paid service.
|
||||||
|
Battlemetrics queries are expected to fail in DZGUI 6. In DZGUI 7, this feature has been removed entirely.
|
||||||
From the ``Personal Access Tokens area``, select ``New Token``.
|
|
||||||
|
|
||||||
Give the token any name in the field at the top.
|
|
||||||
|
|
||||||
Leave **all options unchecked** and scroll to the bottom. Select ``Create Token``.
|
|
||||||
|
|
||||||
Once configured, you can insert this key in the app when launching it for the first time (optional), or later on when using the connect/query by ID methods in the app.
|
|
||||||
|
|
||||||
.. tip::
|
|
||||||
Each server has a unique ID. This is the string of numbers at the end of the URL. For example, in the URL ``https://www.battlemetrics.com/servers/dayz/8039514``, the ID is ``8039514``.
|
|
||||||
|
|
||||||
Next steps
|
Next steps
|
||||||
--------------
|
--------------
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
Steam integration
|
Steam integration
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
In :doc:`DZGUI 7 <dzgui7>`, Steam shortcut creation and cover art setup are handled automatically during setup. You can skip this step unless you are using DZGUI 6.
|
||||||
|
|
||||||
DZGUI can be added to Steam as a "non-Steam game" in order to facilitate integration with Steam Deck or desktop environments.
|
DZGUI can be added to Steam as a "non-Steam game" in order to facilitate integration with Steam Deck or desktop environments.
|
||||||
|
|
||||||
First, launch Steam in the **Large** (default) view.
|
First, launch Steam in the **Large** (default) view.
|
||||||
@ -74,7 +78,7 @@ The final result will create box art looking like the above.
|
|||||||
:scale: 100%
|
:scale: 100%
|
||||||
:alt: Adding a non-Steam game
|
:alt: Adding a non-Steam game
|
||||||
|
|
||||||
Next, right-click the DZGUI entry in your Library and select **Properties** to open the properties dialog. Next to the **Shortcut** field, you will see a small square box which represents the game’s
|
Next, right-click the DZGUI entry in your Library and select **Properties** to open the properties dialog. Next to the **Shortcut** field, you will see a small square box which represents the game’s
|
||||||
icon. Click this to open a file explorer, navigate to ``$HOME/.local/share/dzgui``, and select ``icon.png``. This will add a small icon to the list view.
|
icon. Click this to open a file explorer, navigate to ``$HOME/.local/share/dzgui``, and select ``icon.png``. This will add a small icon to the list view.
|
||||||
|
|
||||||
Finally, after you launch DZGUI for the first time, you should quit the application and return to the Library view. Select the **Recent Games** dropdown on the right-hand side.
|
Finally, after you launch DZGUI for the first time, you should quit the application and return to the Library view. Select the **Recent Games** dropdown on the right-hand side.
|
||||||
|
|||||||
@ -160,6 +160,14 @@ class Shortcuts:
|
|||||||
NEW_ENTRY["tags"] = {}
|
NEW_ENTRY["tags"] = {}
|
||||||
return NEW_ENTRY
|
return NEW_ENTRY
|
||||||
|
|
||||||
|
def delete_shortcut(self, start_path: Path) -> None:
|
||||||
|
shortcuts = self.shortcuts["shortcuts"]
|
||||||
|
for s in shortcuts:
|
||||||
|
if shortcuts[s]["StartDir"] == str(start_path):
|
||||||
|
del shortcuts[s]
|
||||||
|
break
|
||||||
|
self.save_shortcuts()
|
||||||
|
|
||||||
def save_shortcuts(self) -> None:
|
def save_shortcuts(self) -> None:
|
||||||
try:
|
try:
|
||||||
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from dzgui.const.constants import (
|
|||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
DAYZ_BINARY,
|
DAYZ_BINARY,
|
||||||
|
DAYZ_LAUNCHER,
|
||||||
DEBIAN_STEAM_PATH,
|
DEBIAN_STEAM_PATH,
|
||||||
DEFAULT_STEAM_PATH,
|
DEFAULT_STEAM_PATH,
|
||||||
FLATPAK_STEAM_PATH,
|
FLATPAK_STEAM_PATH,
|
||||||
@ -150,6 +151,22 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
|
|||||||
return hashes
|
return hashes
|
||||||
|
|
||||||
|
|
||||||
|
def connect_debug(client: str, addr: str, appid: int, name: str, mods: list[str]) -> str:
|
||||||
|
concat = concat_mods(mods)
|
||||||
|
client_args = concat_bash_args(client)
|
||||||
|
params = [
|
||||||
|
"-applaunch",
|
||||||
|
str(appid),
|
||||||
|
f"-connect={addr}",
|
||||||
|
"-nolauncher",
|
||||||
|
"-nosplash",
|
||||||
|
"-skipintro",
|
||||||
|
f"-name={name}",
|
||||||
|
f"-mod={concat}",
|
||||||
|
]
|
||||||
|
client_args.extend(params)
|
||||||
|
return " ".join(client_args)
|
||||||
|
|
||||||
# TODO: set config to name=user, use official server and no mods,
|
# TODO: set config to name=user, use official server and no mods,
|
||||||
# ensure that formatted string is identical to fixture with same hash
|
# ensure that formatted string is identical to fixture with same hash
|
||||||
def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int:
|
def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int:
|
||||||
@ -382,14 +399,14 @@ def get_client_allows_downloads(path: Path) -> bool:
|
|||||||
def is_dayz_running() -> bool:
|
def is_dayz_running() -> bool:
|
||||||
"""Subprocesses spawned from Steam will not show up in regular process tree"""
|
"""Subprocesses spawned from Steam will not show up in regular process tree"""
|
||||||
procs = []
|
procs = []
|
||||||
substring = DAYZ_BINARY
|
substring = [DAYZ_BINARY, DAYZ_LAUNCHER]
|
||||||
for proc in psutil.process_iter():
|
for proc in psutil.process_iter():
|
||||||
try:
|
try:
|
||||||
procs.append(proc.cmdline())
|
procs.append(proc.cmdline())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
continue
|
continue
|
||||||
return any(substring in item for sublist in procs for item in sublist)
|
return any(s in item for sublist in procs for item in sublist for s in substring)
|
||||||
|
|
||||||
|
|
||||||
def get_app_path(folders_path: Path, appid: int) -> Path:
|
def get_app_path(folders_path: Path, appid: int) -> Path:
|
||||||
|
|||||||
@ -18,10 +18,10 @@ from dzgui.init.flock import lock_acquire
|
|||||||
from dzgui.init.prereqs import has_steam_client
|
from dzgui.init.prereqs import has_steam_client
|
||||||
from dzgui.strings import boot
|
from dzgui.strings import boot
|
||||||
|
|
||||||
# from dzgui.util.map_count import get_map_count
|
|
||||||
from dzgui.util.deck import is_steam_deck, is_game_mode
|
from dzgui.util.deck import is_steam_deck, is_game_mode
|
||||||
from dzgui.util.dirs import make_parents
|
from dzgui.util.dirs import make_parents
|
||||||
from dzgui.util.localize import set_locale
|
from dzgui.util.localize import set_locale
|
||||||
|
from dzgui.util.map_count import test_map_count
|
||||||
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
||||||
from dzgui.util.strings import init
|
from dzgui.util.strings import init
|
||||||
|
|
||||||
@ -119,6 +119,7 @@ def load_gui(version: str, is_debug: bool) -> None:
|
|||||||
del os.environ["GTK_IM_MODULE"]
|
del os.environ["GTK_IM_MODULE"]
|
||||||
|
|
||||||
if has_new_config(XDG.config) is False:
|
if has_new_config(XDG.config) is False:
|
||||||
|
test_map_count()
|
||||||
migrate_cols_file(XDG.columns)
|
migrate_cols_file(XDG.columns)
|
||||||
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
||||||
# TODO: add logging inside wizard
|
# TODO: add logging inside wizard
|
||||||
@ -153,4 +154,5 @@ def load_gui(version: str, is_debug: bool) -> None:
|
|||||||
use_miles=use_miles,
|
use_miles=use_miles,
|
||||||
)
|
)
|
||||||
print(boot.all_ok)
|
print(boot.all_ok)
|
||||||
App(prefs)
|
app = App(prefs)
|
||||||
|
app.run(None)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ APPNAME_DAYZ = "DayZ"
|
|||||||
APPNAME_DAYZ_EXP = "DayZ Exp"
|
APPNAME_DAYZ_EXP = "DayZ Exp"
|
||||||
APPNAME_DAYZ_EXP_HUMAN = "DayZ Experimental"
|
APPNAME_DAYZ_EXP_HUMAN = "DayZ Experimental"
|
||||||
DAYZ_BINARY = "DayZ_x64.exe"
|
DAYZ_BINARY = "DayZ_x64.exe"
|
||||||
|
DAYZ_LAUNCHER = "DayZLauncher.exe"
|
||||||
|
|
||||||
LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf"
|
LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf"
|
||||||
WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ)
|
WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ)
|
||||||
@ -29,6 +30,7 @@ BETA_REPO = "testing"
|
|||||||
APP_NAME = "DZGUI"
|
APP_NAME = "DZGUI"
|
||||||
APP_NAME_LOWER = "dzgui"
|
APP_NAME_LOWER = "dzgui"
|
||||||
APP_NAME_ABBR = "dzg"
|
APP_NAME_ABBR = "dzg"
|
||||||
|
GTK_APP_ID = "io.github.aclist.dzgui"
|
||||||
|
|
||||||
HEX_GREEN = "#32CD32"
|
HEX_GREEN = "#32CD32"
|
||||||
HEX_RED = "#FF0000"
|
HEX_RED = "#FF0000"
|
||||||
@ -38,6 +40,7 @@ HEX_ORANGE = "#FFAC1C"
|
|||||||
CARET_DOWN = "go-down-symbolic"
|
CARET_DOWN = "go-down-symbolic"
|
||||||
CARET_UP = "go-up-symbolic"
|
CARET_UP = "go-up-symbolic"
|
||||||
CLIPBOARD = "edit-copy-symbolic"
|
CLIPBOARD = "edit-copy-symbolic"
|
||||||
|
CLOSE = "window-close-symbolic"
|
||||||
FOLDER = "folder-symbolic"
|
FOLDER = "folder-symbolic"
|
||||||
EDIT_DELETE = "edit-delete-symbolic"
|
EDIT_DELETE = "edit-delete-symbolic"
|
||||||
ERROR = "dialog-error-symbolic"
|
ERROR = "dialog-error-symbolic"
|
||||||
@ -89,3 +92,8 @@ TMP_TARBALL = "/tmp/dzgui.tar.gz"
|
|||||||
TMP_EXE = "/tmp/dzgui/dzgui"
|
TMP_EXE = "/tmp/dzgui/dzgui"
|
||||||
|
|
||||||
DAYZ_COMMUNITY_ROOT = "Missions"
|
DAYZ_COMMUNITY_ROOT = "Missions"
|
||||||
|
|
||||||
|
GIO_SETTINGS_INTERFACE = "org.gnome.desktop.interface"
|
||||||
|
GIO_SETTINGS_PROP = "color-scheme"
|
||||||
|
GIO_SETTINGS_KEY = "gtk-application-prefer-dark-theme"
|
||||||
|
GIO_SETTINGS_VAL = "prefer-dark"
|
||||||
|
|||||||
@ -73,12 +73,6 @@ class Preferences(EnumWithAttrs):
|
|||||||
NAME = {
|
NAME = {
|
||||||
"key": "name",
|
"key": "name",
|
||||||
}
|
}
|
||||||
INSTALL = {
|
|
||||||
"key": "auto_install",
|
|
||||||
}
|
|
||||||
DEBUG = {
|
|
||||||
"key": "debug",
|
|
||||||
}
|
|
||||||
DEFAULT = {
|
DEFAULT = {
|
||||||
"key": "default_steam_path",
|
"key": "default_steam_path",
|
||||||
}
|
}
|
||||||
@ -91,9 +85,6 @@ class Preferences(EnumWithAttrs):
|
|||||||
IP_LIST = {
|
IP_LIST = {
|
||||||
"key": "ip_list",
|
"key": "ip_list",
|
||||||
}
|
}
|
||||||
BRANCH = {
|
|
||||||
"key": "branch",
|
|
||||||
}
|
|
||||||
START_TAB = {
|
START_TAB = {
|
||||||
"key": "start_tab",
|
"key": "start_tab",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,6 +145,10 @@ class Emitter(GObject.GObject):
|
|||||||
def api_change_failed(self) -> None:
|
def api_change_failed(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
|
def api_change_successful(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
def already_saved_server(self) -> None:
|
def already_saved_server(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -88,7 +88,6 @@ class Controller(GObject.GObject):
|
|||||||
|
|
||||||
# NOTE: suppress requests until entire UI is loaded
|
# NOTE: suppress requests until entire UI is loaded
|
||||||
self.loaded = False
|
self.loaded = False
|
||||||
self.pending_jobs = 1
|
|
||||||
|
|
||||||
self.exit_event = threading.Event()
|
self.exit_event = threading.Event()
|
||||||
self.cancel_event = threading.Event()
|
self.cancel_event = threading.Event()
|
||||||
@ -115,9 +114,6 @@ class Controller(GObject.GObject):
|
|||||||
def query_config(self, key: Preferences) -> Any:
|
def query_config(self, key: Preferences) -> Any:
|
||||||
return self.config_man.lookup(key)
|
return self.config_man.lookup(key)
|
||||||
|
|
||||||
def is_auto_install(self) -> bool:
|
|
||||||
return bool(self.query_config(Preferences.INSTALL))
|
|
||||||
|
|
||||||
def suppress_signal(
|
def suppress_signal(
|
||||||
self, owner: Any, child: Any, func_name: str, state: bool
|
self, owner: Any, child: Any, func_name: str, state: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -267,6 +263,7 @@ class Controller(GObject.GObject):
|
|||||||
self.open_page(NotebookPage.LOG)
|
self.open_page(NotebookPage.LOG)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dialog = ExceptionDialog(self, str(e))
|
dialog = ExceptionDialog(self, str(e))
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def select_colorized(self) -> None:
|
def select_colorized(self) -> None:
|
||||||
@ -300,10 +297,11 @@ class Controller(GObject.GObject):
|
|||||||
write_diagnostic(self.prefs.paths.config, file)
|
write_diagnostic(self.prefs.paths.config, file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dialog = ExceptionDialog(self, str(e))
|
dialog = ExceptionDialog(self, str(e))
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def update_api_key(self, key: Preferences, text: str) -> None:
|
def update_steam_api_key(self, text: str) -> None:
|
||||||
self.config_man.update_api_key(key, text)
|
self.config_man.update_steam_api_key(text)
|
||||||
|
|
||||||
def set_resolution(self, window: "OuterWindow") -> None:
|
def set_resolution(self, window: "OuterWindow") -> None:
|
||||||
self.config_man.set_resolution(window)
|
self.config_man.set_resolution(window)
|
||||||
@ -402,6 +400,9 @@ class Controller(GObject.GObject):
|
|||||||
def get_menu(self) -> "MenuTreeView":
|
def get_menu(self) -> "MenuTreeView":
|
||||||
return self.mediator.menu
|
return self.mediator.menu
|
||||||
|
|
||||||
|
def unset_fav(self) -> None:
|
||||||
|
self.config_man.unset_fav()
|
||||||
|
|
||||||
def has_favorites(self) -> bool:
|
def has_favorites(self) -> bool:
|
||||||
favs = self.config_man.get_favorites()
|
favs = self.config_man.get_favorites()
|
||||||
if len(favs) < 1:
|
if len(favs) < 1:
|
||||||
@ -459,6 +460,9 @@ class Controller(GObject.GObject):
|
|||||||
open_workshop_page(mod, cmd)
|
open_workshop_page(mod, cmd)
|
||||||
|
|
||||||
def has_note(self) -> bool:
|
def has_note(self) -> bool:
|
||||||
|
# TODO: get record string, store in memory
|
||||||
|
# get note by record, get is in favs, etc. store as a block
|
||||||
|
# this is only used by context mixin
|
||||||
note = self.get_note()
|
note = self.get_note()
|
||||||
if len(note) > 0:
|
if len(note) > 0:
|
||||||
return True
|
return True
|
||||||
@ -473,11 +477,13 @@ class Controller(GObject.GObject):
|
|||||||
return self.notes_man.get_note(record)
|
return self.notes_man.get_note(record)
|
||||||
|
|
||||||
def add_note(self, note: str) -> None:
|
def add_note(self, note: str) -> None:
|
||||||
|
# TODO: record should be cached upon creation of dialog
|
||||||
tv = self.get_active_treeview()
|
tv = self.get_active_treeview()
|
||||||
record = tv.get_record_string()
|
record = tv.get_record_string()
|
||||||
self.notes_man.add_note(record, note)
|
self.notes_man.add_note(record, note)
|
||||||
|
|
||||||
def delete_note(self) -> None:
|
def delete_note(self) -> None:
|
||||||
|
# TODO: record should be cached upon creation of dialog
|
||||||
tv = self.get_active_treeview()
|
tv = self.get_active_treeview()
|
||||||
record = tv.get_record_string()
|
record = tv.get_record_string()
|
||||||
self.notes_man.delete_note(record)
|
self.notes_man.delete_note(record)
|
||||||
@ -519,6 +525,9 @@ class Controller(GObject.GObject):
|
|||||||
ind = self.config_man.get_start_tab()
|
ind = self.config_man.get_start_tab()
|
||||||
self.get_servers().notebook.set_current_page(ind)
|
self.get_servers().notebook.set_current_page(ind)
|
||||||
|
|
||||||
|
def get_debug_args(self) -> str:
|
||||||
|
return self.connection_man.get_debug_args()
|
||||||
|
|
||||||
def update_and_load_to_menu(self) -> None:
|
def update_and_load_to_menu(self) -> None:
|
||||||
self.connection_man.update_and_connect(menu_only=True)
|
self.connection_man.update_and_connect(menu_only=True)
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
## [7.0.0] Unreleased
|
## [7.0.0] Unreleased
|
||||||
### Added
|
### Added
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
|
- Uninstall wizard
|
||||||
|
- Descriptive details panel to certain error dialogs
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
- Documentation ships with source
|
- Documentation ships with source
|
||||||
@ -24,6 +26,7 @@
|
|||||||
- Set favorite server from tables
|
- Set favorite server from tables
|
||||||
- Detailed/copyable trace in critical error dialogs
|
- Detailed/copyable trace in critical error dialogs
|
||||||
- Visual icons
|
- Visual icons
|
||||||
|
- Unset favorite server button
|
||||||
- Integrated server notebook
|
- Integrated server notebook
|
||||||
- Propagate width changes to all tables
|
- Propagate width changes to all tables
|
||||||
- Remember tree position in menus
|
- Remember tree position in menus
|
||||||
@ -44,11 +47,19 @@
|
|||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
- Generate Steam shortcuts and cover art
|
- Generate Steam shortcuts and cover art
|
||||||
- Warn user if background downloads are disabled
|
- Warn user if background downloads are disabled
|
||||||
|
- Dynamic copy button
|
||||||
|
- Update map count (-u flag)
|
||||||
|
- Filter button tooltips
|
||||||
|
- Expand/collapse changelog versions
|
||||||
|
- Support local light/dark mode changes on system theme
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
|
- Enforce fullscreen mode on Steam Deck
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
|
- Changed some dialog error messages from popups to popovers
|
||||||
|
- Changed how filter buttons are packed in sidebar
|
||||||
- Bold labels inside frames
|
- Bold labels inside frames
|
||||||
- Sidebar buttons do not steal focus
|
- Sidebar buttons do not steal focus
|
||||||
- Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method
|
- Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method
|
||||||
@ -61,18 +72,24 @@
|
|||||||
- Reduce size of geolocation DB on disk (~100MB)
|
- Reduce size of geolocation DB on disk (~100MB)
|
||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
- Propagate subscribed mods to Steam client
|
- Propagate subscribed mods to Steam client
|
||||||
|
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
||||||
|
|
||||||
### Dropped
|
### Dropped
|
||||||
- Debug mode
|
- Battlemetrics API support
|
||||||
|
- Unused metadata from system log files
|
||||||
- Branch switching
|
- Branch switching
|
||||||
- Manual mod install mode (describe rationale)
|
- Manual mod install mode (describe rationale)
|
||||||
- Force update mods
|
- Force update mods
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
||||||
|
- In-game time field missing certain ranges of time at night
|
||||||
|
- Wrap long entries in changelog
|
||||||
|
- Certain server descriptions with special characters not rendering correctly
|
||||||
- Center server title text on server dialogs
|
- Center server title text on server dialogs
|
||||||
- Rare segfaults when changing maps (threading)
|
- Rare segfaults when changing maps (threading)
|
||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
|
- History table not being in lock-step with state file
|
||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
|
|||||||
@ -5,33 +5,36 @@ import warnings
|
|||||||
from dzgui.const.constants import APP_NAME
|
from dzgui.const.constants import APP_NAME
|
||||||
from dzgui.init.libgi import test_libgi_missing
|
from dzgui.init.libgi import test_libgi_missing
|
||||||
from dzgui.init.prefix import get_version
|
from dzgui.init.prefix import get_version
|
||||||
|
from dzgui.util.deck import is_steam_deck
|
||||||
|
from dzgui.util.map_count import set_map_count
|
||||||
from dzgui.util.strings import flags
|
from dzgui.util.strings import flags
|
||||||
|
|
||||||
|
from dzgui.views.dialogs.uninstall import UninstallWizard
|
||||||
|
from dzgui.config.xdg import get_xdg_paths
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description=flags.description)
|
parser = argparse.ArgumentParser(description=flags.description)
|
||||||
parser.add_argument("-v", "--version", action="store_true", help=flags.version)
|
group = parser.add_mutually_exclusive_group()
|
||||||
parser.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall)
|
group.add_argument("-d", "--debug", action="store_true", help=flags.debug)
|
||||||
parser.add_argument("-d", "--debug", action="store_true", help=flags.debug)
|
group.add_argument("-m", "--map", action="store_true", help=flags.map_count)
|
||||||
|
group.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall)
|
||||||
|
group.add_argument("-v", "--version", action="store_true", help=flags.version)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def uninstall() -> None:
|
|
||||||
# TODO: uninstall data files (-u)
|
|
||||||
# -u removes state, log, freedesktop
|
|
||||||
# XDG_SHARE_HOME/dzgui
|
|
||||||
# XDG_STATE_HOME/dzgui
|
|
||||||
# XDG_DATA_HOME/dzgui
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if args.version is True:
|
if args.version is True:
|
||||||
print(get_version())
|
print(get_version())
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if args.uninstall is True:
|
if args.uninstall is True:
|
||||||
uninstall()
|
paths = get_xdg_paths()
|
||||||
|
_is_steam_deck = is_steam_deck()
|
||||||
|
UninstallWizard(_is_steam_deck, paths)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if args.debug is True:
|
if args.debug is True:
|
||||||
warnings.filterwarnings("default", category=DeprecationWarning)
|
warnings.filterwarnings("default", category=DeprecationWarning)
|
||||||
|
if args.map is True:
|
||||||
|
set_map_count()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
version = get_version()
|
version = get_version()
|
||||||
|
|
||||||
|
|||||||
@ -19,11 +19,6 @@ from dzgui.views.dialogs.generic import ExceptionDialog
|
|||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util._json import read_json, write_json
|
from dzgui.util._json import read_json, write_json
|
||||||
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository.Gtk import main_quit # noqa E402
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.config.userprefs import UserPrefs
|
from dzgui.config.userprefs import UserPrefs
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@ -98,12 +93,13 @@ class ConfigManager:
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@call_on_thread(dialogs.checking_api)
|
@call_on_thread(dialogs.checking_api, show_dialog=False)
|
||||||
def update_api_key(self, key: Preferences, text: str) -> None:
|
def update_steam_api_key(self, text: str) -> None:
|
||||||
if key is Preferences.STEAM:
|
res = test_steam_api(text)
|
||||||
res = test_steam_api(text)
|
|
||||||
if res is True:
|
if res is True:
|
||||||
self.update_config(key, text)
|
self.update_config(Preferences.STEAM, text)
|
||||||
|
func = StoredFunc(lambda: self.emitter.emit("api_change_successful"))
|
||||||
|
self.thread_man.set_cleanup_func(func)
|
||||||
else:
|
else:
|
||||||
self.thread_man.set_cleanup_func(
|
self.thread_man.set_cleanup_func(
|
||||||
StoredFunc(lambda: self.emitter.emit("api_change_failed"))
|
StoredFunc(lambda: self.emitter.emit("api_change_failed"))
|
||||||
@ -160,6 +156,13 @@ class ConfigManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def unset_fav(self) -> None:
|
||||||
|
try:
|
||||||
|
self.write_config(Preferences.FAV_LBL, "")
|
||||||
|
self.write_config(Preferences.FAV_SRV, "")
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
def write_config(self, key: Preferences, value: str) -> None:
|
def write_config(self, key: Preferences, value: str) -> None:
|
||||||
try:
|
try:
|
||||||
real_key = self.enum_to_key(key)
|
real_key = self.enum_to_key(key)
|
||||||
@ -170,8 +173,8 @@ class ConfigManager:
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
trace = traceback.format_exc()
|
trace = traceback.format_exc()
|
||||||
dialog = ExceptionDialog(self.controller, trace)
|
dialog = ExceptionDialog(self.controller, trace)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
raise e
|
|
||||||
|
|
||||||
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
|
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
|
||||||
columns = tv.get_columns()
|
columns = tv.get_columns()
|
||||||
@ -198,7 +201,10 @@ class ConfigManager:
|
|||||||
|
|
||||||
logger.info("Normal user exit")
|
logger.info("Normal user exit")
|
||||||
if window.props.is_maximized:
|
if window.props.is_maximized:
|
||||||
main_quit()
|
app = window.get_application()
|
||||||
|
if app is None:
|
||||||
|
return
|
||||||
|
app.quit()
|
||||||
return
|
return
|
||||||
|
|
||||||
w, h = window.get_size()
|
w, h = window.get_size()
|
||||||
@ -210,7 +216,10 @@ class ConfigManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
|
|
||||||
main_quit()
|
app = window.get_application()
|
||||||
|
if app is None:
|
||||||
|
return
|
||||||
|
app.quit()
|
||||||
|
|
||||||
def set_resolution(self, window: "OuterWindow") -> None:
|
def set_resolution(self, window: "OuterWindow") -> None:
|
||||||
if self.prefs.is_game_mode:
|
if self.prefs.is_game_mode:
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from dzgui.api.shortcuts import Shortcuts
|
|||||||
|
|
||||||
from dzgui.api.steam import (
|
from dzgui.api.steam import (
|
||||||
connect,
|
connect,
|
||||||
|
connect_debug,
|
||||||
get_app_allows_downloads,
|
get_app_allows_downloads,
|
||||||
get_app_name,
|
get_app_name,
|
||||||
get_needs_update,
|
get_needs_update,
|
||||||
@ -45,6 +46,7 @@ from dzgui.strings.dialogs import (
|
|||||||
waiting_for_mods,
|
waiting_for_mods,
|
||||||
waiting_for_directories,
|
waiting_for_directories,
|
||||||
)
|
)
|
||||||
|
from dzgui.strings import kb
|
||||||
from dzgui.strings.server_mods import checkmark, resync
|
from dzgui.strings.server_mods import checkmark, resync
|
||||||
from dzgui.util.format import format_mib
|
from dzgui.util.format import format_mib
|
||||||
from dzgui.util.strings import dialog, server_timeout
|
from dzgui.util.strings import dialog, server_timeout
|
||||||
@ -193,7 +195,9 @@ class ConnectionManager:
|
|||||||
is_last = self.is_last_server()
|
is_last = self.is_last_server()
|
||||||
|
|
||||||
# TODO: strings
|
# TODO: strings
|
||||||
invalid_mods = [(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"]
|
invalid_mods = [
|
||||||
|
(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"
|
||||||
|
]
|
||||||
|
|
||||||
prereqs = Prerequisites(
|
prereqs = Prerequisites(
|
||||||
name=info.server_name,
|
name=info.server_name,
|
||||||
@ -298,8 +302,17 @@ class ConnectionManager:
|
|||||||
|
|
||||||
def _server_timeout(self) -> None:
|
def _server_timeout(self) -> None:
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
|
dialog.set_details_buffer(kb.DZG_006)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
|
def get_debug_args(self) -> str:
|
||||||
|
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||||
|
playername = self.controller.query_config(Preferences.NAME)
|
||||||
|
return connect_debug(
|
||||||
|
self.client, addr, self.appid, playername, self.remote_mod_ids
|
||||||
|
)
|
||||||
|
|
||||||
def _connect_steam(self, menu_only: bool) -> None:
|
def _connect_steam(self, menu_only: bool) -> None:
|
||||||
addr = f"{self.record.ip}:{self.record.gameport}"
|
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||||
playername = self.controller.query_config(Preferences.NAME)
|
playername = self.controller.query_config(Preferences.NAME)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import gi
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@ -14,10 +13,6 @@ from dzgui.views.trees.tree_servers import ServerTreeView
|
|||||||
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
||||||
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@ -21,14 +21,30 @@ class FilterManager:
|
|||||||
strings.filter_3pp: True,
|
strings.filter_3pp: True,
|
||||||
strings.filter_night: True,
|
strings.filter_night: True,
|
||||||
strings.filter_full: show_full,
|
strings.filter_full: show_full,
|
||||||
strings.filter_lowpop: True,
|
|
||||||
strings.filter_nonascii: False,
|
|
||||||
strings.filter_duplicate: False,
|
|
||||||
strings.filter_official: True,
|
strings.filter_official: True,
|
||||||
|
strings.filter_nonascii: False,
|
||||||
|
strings.filter_lowpop: True,
|
||||||
strings.filter_unofficial: True,
|
strings.filter_unofficial: True,
|
||||||
|
strings.filter_duplicate: False,
|
||||||
strings.filter_modded: True,
|
strings.filter_modded: True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
|
self.tooltips = {
|
||||||
|
strings.filter_1pp: "First-person perspective",
|
||||||
|
strings.filter_day: "In-game time between 0700 and 1659",
|
||||||
|
strings.filter_empty: "Servers with no players",
|
||||||
|
strings.filter_3pp: "Third-person perspective",
|
||||||
|
strings.filter_night: "In-game time between 1700 and 0659",
|
||||||
|
strings.filter_full: "Servers with no open slots",
|
||||||
|
strings.filter_official: "Bohemia official servers",
|
||||||
|
strings.filter_nonascii: "Server names using non-standard, complex glyphs",
|
||||||
|
strings.filter_lowpop: "Current population is under 30% of total",
|
||||||
|
strings.filter_unofficial: "Third-party servers",
|
||||||
|
strings.filter_duplicate: "Duplicates of existing servers (usually spoofed)",
|
||||||
|
strings.filter_modded: "Servers with one or more mods",
|
||||||
|
}
|
||||||
|
|
||||||
self.active_keyword = ""
|
self.active_keyword = ""
|
||||||
self.active_map = (0, all_maps)
|
self.active_map = (0, all_maps)
|
||||||
self.prior_map = all_maps
|
self.prior_map = all_maps
|
||||||
@ -36,6 +52,9 @@ class FilterManager:
|
|||||||
self.filters: list
|
self.filters: list
|
||||||
self.enabled_filters = dict(self.default_filters)
|
self.enabled_filters = dict(self.default_filters)
|
||||||
|
|
||||||
|
def get_tooltips(self) -> dict[str, str]:
|
||||||
|
return self.tooltips
|
||||||
|
|
||||||
def set_prior_map(self, name: str) -> None:
|
def set_prior_map(self, name: str) -> None:
|
||||||
self.prior_map = name
|
self.prior_map = name
|
||||||
|
|
||||||
@ -86,6 +105,7 @@ class FilterManager:
|
|||||||
self.reinit_map_store()
|
self.reinit_map_store()
|
||||||
maps.sort()
|
maps.sort()
|
||||||
for m in maps:
|
for m in maps:
|
||||||
|
# TODO: strings
|
||||||
if m == "All maps":
|
if m == "All maps":
|
||||||
continue
|
continue
|
||||||
self.append_map([m])
|
self.append_map([m])
|
||||||
|
|||||||
@ -22,9 +22,7 @@ from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManage
|
|||||||
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util.format import format_mods
|
from dzgui.util.format import format_mods
|
||||||
from dzgui.util.strings import server_timeout
|
|
||||||
from dzgui.util.symlink import rebuild_symlinks
|
from dzgui.util.symlink import rebuild_symlinks
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
|
||||||
|
|
||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
@ -213,10 +211,6 @@ class ModManager:
|
|||||||
func = StoredFunc(self._on_stale_mods_found, stale)
|
func = StoredFunc(self._on_stale_mods_found, stale)
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
def _server_timeout(self) -> None:
|
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
|
||||||
dialog.run()
|
|
||||||
|
|
||||||
def select_colorized(self) -> None:
|
def select_colorized(self) -> None:
|
||||||
model = self.treeview.get_model()
|
model = self.treeview.get_model()
|
||||||
if model is None:
|
if model is None:
|
||||||
|
|||||||
@ -64,4 +64,5 @@ class UpdateManager:
|
|||||||
|
|
||||||
def _on_update_failure(self, msg: str) -> None:
|
def _on_update_failure(self, msg: str) -> None:
|
||||||
dialog = ExceptionDialog(self.controller, msg)
|
dialog = ExceptionDialog(self.controller, msg)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|||||||
@ -12,6 +12,9 @@ if TYPE_CHECKING:
|
|||||||
from dzgui.api.servers import Record
|
from dzgui.api.servers import Record
|
||||||
from dzgui.model.servers import NewPlayerCount
|
from dzgui.model.servers import NewPlayerCount
|
||||||
|
|
||||||
|
DAY_REG = r"([0][7-9]|[1][0-6])"
|
||||||
|
NIGHT_REG = r"([0][0-6]|[1][7-9]|[2][0-3])"
|
||||||
|
|
||||||
|
|
||||||
class ProxyModelManager:
|
class ProxyModelManager:
|
||||||
"""
|
"""
|
||||||
@ -231,11 +234,9 @@ class ProxyModelManager:
|
|||||||
final.append(row)
|
final.append(row)
|
||||||
rows = final
|
rows = final
|
||||||
case strings.filter_day:
|
case strings.filter_day:
|
||||||
reg = r"([0][0-9]|[1][0-6])"
|
rows = [row for row in rows if not re.match(DAY_REG, row[3])]
|
||||||
rows = [row for row in rows if not re.match(reg, row[3])]
|
|
||||||
case strings.filter_night:
|
case strings.filter_night:
|
||||||
reg = r"([0][0-4]|[1][8]|[2][0-3])"
|
rows = [row for row in rows if not re.match(NIGHT_REG, row[3])]
|
||||||
rows = [row for row in rows if not re.match(reg, row[3])]
|
|
||||||
case strings.filter_nonascii:
|
case strings.filter_nonascii:
|
||||||
rows = [row for row in rows if row[0].isascii()]
|
rows = [row for row in rows if row[0].isascii()]
|
||||||
case strings.filter_lowpop:
|
case strings.filter_lowpop:
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from dzgui.const.constants import (
|
|||||||
)
|
)
|
||||||
from dzgui.const.enum import FilterMode, Preferences, ServerTab
|
from dzgui.const.enum import FilterMode, Preferences, ServerTab
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs, kb
|
||||||
from dzgui.util.strings import server_timeout, dialog
|
from dzgui.util.strings import server_timeout, dialog
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
|
|
||||||
@ -405,6 +405,7 @@ class ServerModelManager:
|
|||||||
|
|
||||||
if show_dialog:
|
if show_dialog:
|
||||||
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
|
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
||||||
@ -420,6 +421,8 @@ class ServerModelManager:
|
|||||||
# customize statusbar and dialog accordingly
|
# customize statusbar and dialog accordingly
|
||||||
if show_dialog:
|
if show_dialog:
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
|
dialog.set_details_buffer(kb.DZG_006)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def _push_data(self, data: list[Any]) -> None:
|
def _push_data(self, data: list[Any]) -> None:
|
||||||
|
|||||||
2
dzgui/strings/changelog.py
Normal file
2
dzgui/strings/changelog.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
collapse_all = "Collapse all"
|
||||||
|
expand_all = "Expand all"
|
||||||
@ -4,10 +4,7 @@ add_popover = "This address is already in your Saved Servers"
|
|||||||
add_tooltip="Add to Saved Servers"
|
add_tooltip="Add to Saved Servers"
|
||||||
connect_button="Connect"
|
connect_button="Connect"
|
||||||
connect_tooltip="Connect to this server"
|
connect_tooltip="Connect to this server"
|
||||||
connect_entry_tooltip=(
|
connect_entry_placeholder="Enter IP:Query port (e.g., 192.168.1.1:27016)"
|
||||||
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016"
|
|
||||||
)
|
|
||||||
connect_entry_placeholder="Enter IP (IP:Query port)"
|
|
||||||
|
|
||||||
fav_heading="Favorite server"
|
fav_heading="Favorite server"
|
||||||
favs_empty="None set. Right click a server and select 'Set favorite' to set."
|
favs_empty="None set. Right click a server and select 'Set favorite' to set."
|
||||||
@ -18,6 +15,6 @@ lan_custom_button="Custom port"
|
|||||||
lan_scan_button="Scan"
|
lan_scan_button="Scan"
|
||||||
lan_abort_tooltip = "Unless you have multiple DayZ servers on your LAN,\nleave this checked to get results faster"
|
lan_abort_tooltip = "Unless you have multiple DayZ servers on your LAN,\nleave this checked to get results faster"
|
||||||
lan_checkbox = "Stop scanning on first hit"
|
lan_checkbox = "Stop scanning on first hit"
|
||||||
lan_placeholder="Enter the query port (1-65535)"
|
lan_entry_placeholder="Enter the query port (1-65535)"
|
||||||
lan_entry_tooltip="Specify the port to search for DayZ servers on the local network"
|
lan_entry_tooltip="Specify the port to search for DayZ servers on the local network"
|
||||||
lan_scan_tooltip="Scan for servers"
|
lan_scan_tooltip="Scan for servers"
|
||||||
|
|||||||
@ -20,3 +20,6 @@ ok = "OK"
|
|||||||
|
|
||||||
mission_dialog = "Set a mission folder location"
|
mission_dialog = "Set a mission folder location"
|
||||||
custom_mod_dialog = "Set location to a custom mods folder"
|
custom_mod_dialog = "Set location to a custom mods folder"
|
||||||
|
|
||||||
|
debug_heading = "Debug"
|
||||||
|
debug_secondary = "Launch arguments"
|
||||||
|
|||||||
13
dzgui/strings/kb.py
Normal file
13
dzgui/strings/kb.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
DZG_006 = """The leading cause of specific servers periodically timing out is local network configuration.
|
||||||
|
|
||||||
|
Many third-party DayZ servers use a server rental/hosting provider with DDoS protection.
|
||||||
|
|
||||||
|
This can cause responses from servers to originate from a server other than the one originally queried.
|
||||||
|
|
||||||
|
Consumer-grade routers are likely to drop this traffic as invalid due to how they handle NAT (network address translation).
|
||||||
|
|
||||||
|
By contrast, wireless routers and enterprise-grade routers may be less likely to have this issue.
|
||||||
|
|
||||||
|
If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port.
|
||||||
|
|
||||||
|
In addition, a uniform packet size is expected. If your router's MTU (maximum transmission unit) setting differs from the default of 1500, it could lead to incoming responses being discarded."""
|
||||||
16
dzgui/strings/map_count.py
Normal file
16
dzgui/strings/map_count.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
exit_msg = (
|
||||||
|
"System map count is not high enough to run DayZ. "
|
||||||
|
"Please exit and run 'dzgui -m' to update map count."
|
||||||
|
)
|
||||||
|
failed_to_parse = (
|
||||||
|
"Failed to parse system map count.\n"
|
||||||
|
"This usually indicates that systemd is not installed."
|
||||||
|
)
|
||||||
|
failed_to_update = (
|
||||||
|
"Failed to update system map count.\n"
|
||||||
|
"Please report the issue upstream and provide the following traceback.\n\n"
|
||||||
|
"{0}"
|
||||||
|
)
|
||||||
|
meets_minimum = "System map count of {0} already meets the minimum."
|
||||||
|
prompt = "Updated map count will be written to the file {0}."
|
||||||
|
user_exit = "User exit"
|
||||||
@ -1,12 +1,19 @@
|
|||||||
developers = "Developers"
|
developers = "Developers"
|
||||||
start_tab = "Start tab"
|
start_tab = "Start tab"
|
||||||
workshop_label = "Subscribed mods"
|
workshop_label = "Subscribed mods"
|
||||||
workshop_eventbox = "If you manually subscribed to mods on Steam prior to using DZGUI, you can unsubscribe via this link."
|
|
||||||
steam_combo = "Steam"
|
steam_combo = "Steam"
|
||||||
flatpak_combo = "Flatpak"
|
flatpak_combo = "Flatpak"
|
||||||
flatpak_container_combo = "Flatpak (container)"
|
flatpak_container_combo = "Flatpak (container)"
|
||||||
|
|
||||||
|
steam_placeholder = "Enter your Steam API key"
|
||||||
|
name_placeholder = "Identifies you to other players"
|
||||||
|
|
||||||
server_combo = "Server browser"
|
server_combo = "Server browser"
|
||||||
saved_combo = "Saved servers"
|
saved_combo = "Saved servers"
|
||||||
recent_combo = "Recent"
|
recent_combo = "Recent"
|
||||||
lan_combo = "LAN"
|
lan_combo = "LAN"
|
||||||
|
|
||||||
|
fullscreen_eventbox = "This option is not available on Steam Deck."
|
||||||
|
save_button = "Save"
|
||||||
|
|
||||||
|
api_failed = "API validation failed"
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
update_mods = "Update mods and connect"
|
update_mods = "Update mods and connect"
|
||||||
connect = "Connect"
|
connect = "Connect"
|
||||||
connect_last = "Load to menu screen"
|
connect_last = "Load to menu screen"
|
||||||
connect_last_tooltip = (
|
connect_last_tooltip = "Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
|
||||||
"Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
|
debug = "Debug"
|
||||||
)
|
|
||||||
back = "Back"
|
back = "Back"
|
||||||
cancel = "Cancel"
|
cancel = "Cancel"
|
||||||
warnings = "Warnings"
|
warnings = "Warnings"
|
||||||
@ -14,3 +13,38 @@ up_to_date = "All mods are up to date."
|
|||||||
all_updated = "All mods updated."
|
all_updated = "All mods updated."
|
||||||
"If you recently installed {build} or moved it to a different drive, "
|
"If you recently installed {build} or moved it to a different drive, "
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
"restart Steam to allow these changes to synchronize, then try again."
|
||||||
|
required_space = " Need to download {0} MiB of mod updates."
|
||||||
|
|
||||||
|
placeholder_no_mods = "This server has no mods."
|
||||||
|
placeholder_no_warnings = "No warnings."
|
||||||
|
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
invalid_mods = (
|
||||||
|
"Server has invalid mods that are not recognized by Steam.\n"
|
||||||
|
"Contact the server owner and include these mod IDs in your report:\n"
|
||||||
|
"{0}"
|
||||||
|
)
|
||||||
|
not_enough_space = (
|
||||||
|
"Need to update {0} MiB of mods, but installation path only has {1} MiB."
|
||||||
|
)
|
||||||
|
resync = (
|
||||||
|
"If you recently installed {0} or moved it to a different drive,\n"
|
||||||
|
"restart Steam to allow these changes to synchronize, then try again."
|
||||||
|
)
|
||||||
|
steam_not_running = "'{0}' is set as the default Steam client, but it is either not installed or not running."
|
||||||
|
use_desktop_mode = "Use Desktop Mode to download mods on Steam Deck."
|
||||||
|
version_mismatch = "Local client version '{0}' does not match remote version '{1}'."
|
||||||
|
version_missing = "Remote server is running the build '{0}', but it is not installed."
|
||||||
|
|
||||||
|
|
||||||
|
# Warnings
|
||||||
|
protected_server = (
|
||||||
|
"Protected: you will be prompted for a password when connecting to this server."
|
||||||
|
)
|
||||||
|
dayz_running = "It looks like DayZ is already running in the background. Exit DayZ before connecting."
|
||||||
|
running_app = (
|
||||||
|
"The app '{0}' is currently running in Steam, but background downloads are not enabled.\n"
|
||||||
|
"Either stop the game first, or update your global Steam settings or the game's local settings.\n"
|
||||||
|
"Otherwise, mods may be queued for download but never update."
|
||||||
|
)
|
||||||
|
|||||||
38
dzgui/strings/uninstall.py
Normal file
38
dzgui/strings/uninstall.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
pg1_title = "Choose uninstall targets"
|
||||||
|
pg1_heading = "Choose uninstall targets"
|
||||||
|
pg1_blurb = "Here you can choose which DZGUI configuration/state files to remove. "
|
||||||
|
|
||||||
|
pg2_title = "Uninstall complete"
|
||||||
|
pg2_heading = "Uninstall complete"
|
||||||
|
pg2_blurb = "Finished removing the selected files. See the console for any errors."
|
||||||
|
|
||||||
|
config_label = "Configuration files"
|
||||||
|
config_details = (
|
||||||
|
"User preferences and API key. "
|
||||||
|
"Keep this file if you want to import it again later."
|
||||||
|
)
|
||||||
|
desktop_label = "Desktop shortcut"
|
||||||
|
desktop_details = "Shortcut set on the desktop, if applicable."
|
||||||
|
|
||||||
|
start_menu_label = "Start menu shortcut"
|
||||||
|
start_menu_details = (
|
||||||
|
"Shortcut set under the Games category of the start menu, if applicable."
|
||||||
|
)
|
||||||
|
state_label = "Long-term state files"
|
||||||
|
state_details = (
|
||||||
|
"UI size, filter preferences, column size, server notes, and mod signatures."
|
||||||
|
)
|
||||||
|
steam_shortcut = "Steam shortcut"
|
||||||
|
steam_details = "Shortcut to DZGUI installed within Steam, if applicable. Requires Steam to be restarted to reflect the change."
|
||||||
|
|
||||||
|
|
||||||
|
path_remove_prefix = "The following path will be removed:"
|
||||||
|
not_installed = "It looks like DZGUI was not previously set up; nothing to uninstall."
|
||||||
|
standalone_uninstall = (
|
||||||
|
"It looks like you are running the standalone version of DZGUI.\n"
|
||||||
|
"To finalize the uninstall process, you can remove the 'dzgui' file."
|
||||||
|
)
|
||||||
|
system_uninstall = (
|
||||||
|
"It looks like DZGUI was installed via your system package manager or directly from source code.\n"
|
||||||
|
"To finalize the uninstall process, follow the standard removal procedure for that context."
|
||||||
|
)
|
||||||
@ -54,8 +54,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
|||||||
cpu = get_cpu_model()
|
cpu = get_cpu_model()
|
||||||
version = get_version()
|
version = get_version()
|
||||||
|
|
||||||
debug = lookup(config, Preferences.DEBUG)
|
|
||||||
install = lookup(config, Preferences.INSTALL)
|
|
||||||
default = lookup(config, Preferences.DEFAULT)
|
default = lookup(config, Preferences.DEFAULT)
|
||||||
|
|
||||||
steam_path = Path(default)
|
steam_path = Path(default)
|
||||||
@ -79,8 +77,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
|||||||
Kernel: {kernel}
|
Kernel: {kernel}
|
||||||
CPU: {cpu}
|
CPU: {cpu}
|
||||||
|
|
||||||
Debug: {debug}
|
|
||||||
Auto-install: {install}
|
|
||||||
Steam path: {steam_redacted}
|
Steam path: {steam_redacted}
|
||||||
Workshop path: {workshop_redacted}
|
Workshop path: {workshop_redacted}
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,71 @@
|
|||||||
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import traceback
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dzgui.const.constants import VM_FILE, MIN_COUNT
|
from dzgui.const.constants import APP_NAME, VM_FILE, MIN_COUNT
|
||||||
|
from dzgui.util.bash import concat_bash_args
|
||||||
|
from dzgui.strings import map_count
|
||||||
|
from dzgui.views.dialogs.early_alert import EarlyIgnoreDialog
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def is_map_count_valid(count: int | None) -> bool:
|
||||||
|
if count is None:
|
||||||
|
# NOTE: permit if count was unreadable
|
||||||
|
return True
|
||||||
|
return count >= MIN_COUNT
|
||||||
|
|
||||||
|
|
||||||
def get_map_count() -> int | None:
|
def get_map_count() -> int | None:
|
||||||
path = Path(VM_FILE)
|
path = Path(VM_FILE)
|
||||||
if path.is_file() is False:
|
try:
|
||||||
|
count = int(path.read_text())
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
return None
|
return None
|
||||||
count = int(path.read_text())
|
|
||||||
return count
|
return count
|
||||||
|
|
||||||
# TODO: unfinished
|
|
||||||
#if count < MIN_COUNT:
|
|
||||||
# print("needs sudo escalation")
|
|
||||||
# # pop prompt
|
|
||||||
# # get response
|
|
||||||
# set_map_count()
|
|
||||||
#return count
|
|
||||||
|
|
||||||
# TODO: unfinished
|
def test_map_count() -> None:
|
||||||
|
count = get_map_count()
|
||||||
|
if is_map_count_valid(count):
|
||||||
|
return
|
||||||
|
msg = map_count.exit_msg
|
||||||
|
EarlyIgnoreDialog(msg)
|
||||||
|
|
||||||
|
|
||||||
def set_map_count() -> None:
|
def set_map_count() -> None:
|
||||||
|
count = get_map_count()
|
||||||
|
if count is None:
|
||||||
|
print(map_count.failed_to_parse)
|
||||||
|
return
|
||||||
|
if is_map_count_valid(count):
|
||||||
|
msg = map_count.meets_minimum.format(count)
|
||||||
|
print(msg)
|
||||||
|
return
|
||||||
|
|
||||||
conf = "/etc/sysctl.d/dayz.conf"
|
conf = "/etc/sysctl.d/dayz.conf"
|
||||||
count = f"vm.max_map_count={MIN_COUNT}"
|
value = f"vm.max_map_count={MIN_COUNT}"
|
||||||
with open(conf, "w") as f:
|
try:
|
||||||
f.write(count)
|
msg = map_count.prompt.format(conf)
|
||||||
# TODO: use concat_bash_args()
|
print(msg)
|
||||||
subprocess.run(["/usr/bin/sudo", "sysctl", "-p", conf])
|
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||||
|
tmp = f.name
|
||||||
|
Path(tmp).write_text(value)
|
||||||
|
|
||||||
|
mv_cmd = f"sudo mv {tmp} {conf}"
|
||||||
|
reload_cmd = f"sudo sysctl -p {conf}"
|
||||||
|
for cmd in mv_cmd, reload_cmd:
|
||||||
|
args = concat_bash_args(cmd)
|
||||||
|
subprocess.run([*args])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
trace = traceback.format_exc()
|
||||||
|
print(map_count.failed_to_update.format(trace))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(map_count.user_exit)
|
||||||
|
sys.exit(0)
|
||||||
|
|||||||
@ -1,27 +1,6 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from dzgui.const.constants import SYSTEM_LOG
|
from dzgui.const.constants import SYSTEM_LOG
|
||||||
|
|
||||||
# TODO: move to util.format.py
|
|
||||||
|
|
||||||
|
|
||||||
def build_missing(build: str) -> str:
|
|
||||||
msg = (
|
|
||||||
f"This server is running {build}. You can install "
|
|
||||||
f"{build} by searching for it in your Steam library. "
|
|
||||||
f"If you recently installed {build} or moved it to a different drive, "
|
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
|
||||||
)
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
def build_path_invalid(build: str) -> str:
|
|
||||||
msg = (
|
|
||||||
f"Steam is reporting that {build} is installed at a non-existent location. "
|
|
||||||
f"If you recently installed {build} or moved it to a different drive, "
|
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
|
||||||
)
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
# General
|
# General
|
||||||
dialog_header = "DZGUI - Dialog"
|
dialog_header = "DZGUI - Dialog"
|
||||||
@ -467,13 +446,15 @@ class Flags:
|
|||||||
version: str
|
version: str
|
||||||
uninstall: str
|
uninstall: str
|
||||||
debug: str
|
debug: str
|
||||||
|
map_count: str
|
||||||
|
|
||||||
|
|
||||||
flags = Flags(
|
flags = Flags(
|
||||||
description="DayZ server browser and mod manager",
|
description="DayZ server browser and mod manager",
|
||||||
version="Print version information",
|
version="print version information",
|
||||||
uninstall="Uninstall data files (use prior to 'pip uninstall')",
|
uninstall="clean up state/config files",
|
||||||
debug="Enables developer debugging features",
|
debug="enable developer debugging features",
|
||||||
|
map_count="check and update system map count value",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -525,63 +506,6 @@ server_labels = ServerLabels(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class ConnectPanel:
|
|
||||||
connect: str
|
|
||||||
add: str
|
|
||||||
add_con: str
|
|
||||||
placeholder: str
|
|
||||||
entry_tooltip: str
|
|
||||||
add_tooltip: str
|
|
||||||
connect_tooltip: str
|
|
||||||
|
|
||||||
|
|
||||||
connect_panel = ConnectPanel(
|
|
||||||
connect="Connect",
|
|
||||||
add="Add",
|
|
||||||
add_con="Add/connect",
|
|
||||||
placeholder="Enter IP (IP:Query port)",
|
|
||||||
entry_tooltip=(
|
|
||||||
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016"
|
|
||||||
),
|
|
||||||
add_tooltip="Add to Saved Servers",
|
|
||||||
connect_tooltip="Connect to this server",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class FavPanel:
|
|
||||||
heading: str
|
|
||||||
no_fav: str
|
|
||||||
|
|
||||||
|
|
||||||
fav_panel = FavPanel(
|
|
||||||
heading="Favorite server",
|
|
||||||
no_fav="None set. Right click a server and select 'Set favorite' to set.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class LanPanel:
|
|
||||||
heading: str
|
|
||||||
default_button: str
|
|
||||||
custom_button: str
|
|
||||||
scan_button: str
|
|
||||||
placeholder: str
|
|
||||||
entry_tooltip: str
|
|
||||||
scan_tooltip: str
|
|
||||||
|
|
||||||
|
|
||||||
lan_panel = LanPanel(
|
|
||||||
heading="LAN query port",
|
|
||||||
default_button="Default port (27016)",
|
|
||||||
custom_button="Custom port",
|
|
||||||
scan_button="Scan",
|
|
||||||
placeholder="Enter the query port (1-65535)",
|
|
||||||
entry_tooltip="Specify the port to search for DayZ servers on the local network",
|
|
||||||
scan_tooltip="Scan for servers",
|
|
||||||
)
|
|
||||||
|
|
||||||
distance_suffix = "Distance: calculating..."
|
distance_suffix = "Distance: calculating..."
|
||||||
dialog_error = "ERROR"
|
dialog_error = "ERROR"
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,14 @@ import logging
|
|||||||
import signal
|
import signal
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Literal
|
from typing import TYPE_CHECKING, Literal, Self
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, STEAM_ICON
|
from dzgui.const.constants import (
|
||||||
|
APP_NAME,
|
||||||
|
APP_NAME_LOWER,
|
||||||
|
GTK_APP_ID,
|
||||||
|
STEAM_ICON,
|
||||||
|
)
|
||||||
from dzgui.const.enum import NotebookPage
|
from dzgui.const.enum import NotebookPage
|
||||||
from dzgui.controllers.emitter import Emitter
|
from dzgui.controllers.emitter import Emitter
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@ -14,6 +19,7 @@ from dzgui.views.components.connect_panel import ConnectPanel
|
|||||||
from dzgui.views.components.crumbs import Breadcrumbs
|
from dzgui.views.components.crumbs import Breadcrumbs
|
||||||
from dzgui.views.components.right_panel import RightPanel
|
from dzgui.views.components.right_panel import RightPanel
|
||||||
from dzgui.views.components.statusbar import Statusbar
|
from dzgui.views.components.statusbar import Statusbar
|
||||||
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
||||||
|
|
||||||
# TODO: import notebook only and add components there?
|
# TODO: import notebook only and add components there?
|
||||||
@ -46,9 +52,11 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
|
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
|
||||||
|
|
||||||
|
|
||||||
class OuterWindow(Gtk.Window):
|
class OuterWindow(Gtk.ApplicationWindow):
|
||||||
def __init__(self) -> None:
|
def __init__(self, app: Gtk.Application) -> None:
|
||||||
super().__init__(title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER)
|
super().__init__(
|
||||||
|
application=app, title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER
|
||||||
|
)
|
||||||
|
|
||||||
self.hb = AppHeaderBar()
|
self.hb = AppHeaderBar()
|
||||||
MainController.register_widget("window", self)
|
MainController.register_widget("window", self)
|
||||||
@ -69,8 +77,8 @@ class OuterWindow(Gtk.Window):
|
|||||||
self.show_all()
|
self.show_all()
|
||||||
css.load_css()
|
css.load_css()
|
||||||
|
|
||||||
MainController.open_page(NotebookPage.SERVERS)
|
|
||||||
MainController.set_start_tab()
|
MainController.set_start_tab()
|
||||||
|
MainController.open_page(NotebookPage.SERVERS)
|
||||||
self.grid.hide_widgets_on_init()
|
self.grid.hide_widgets_on_init()
|
||||||
|
|
||||||
MainController.loaded = True
|
MainController.loaded = True
|
||||||
@ -279,13 +287,18 @@ class Grid(Gtk.Grid):
|
|||||||
self.right_panel.refresh_button.set_visible(state)
|
self.right_panel.refresh_button.set_visible(state)
|
||||||
|
|
||||||
|
|
||||||
class App(Gtk.Application):
|
class App(ColorAwareApp, Gtk.Application): # type: ignore
|
||||||
def __init__(self, prefs: "UserPrefs") -> None:
|
def __init__(self, prefs: "UserPrefs") -> None:
|
||||||
|
super().__init__(application_id=GTK_APP_ID)
|
||||||
|
|
||||||
GLib.set_prgname(APP_NAME)
|
GLib.set_prgname(APP_NAME)
|
||||||
MainController.set_prefs(prefs)
|
MainController.set_prefs(prefs)
|
||||||
|
|
||||||
self.win = OuterWindow()
|
self._setup_signals()
|
||||||
|
self.connect("activate", self._on_activate)
|
||||||
|
|
||||||
|
def _on_activate(self, app: Self) -> None:
|
||||||
|
self.win = OuterWindow(app)
|
||||||
|
|
||||||
accel = Gtk.AccelGroup()
|
accel = Gtk.AccelGroup()
|
||||||
accel.connect(
|
accel.connect(
|
||||||
@ -296,9 +309,6 @@ class App(Gtk.Application):
|
|||||||
)
|
)
|
||||||
self.win.add_accel_group(accel)
|
self.win.add_accel_group(accel)
|
||||||
|
|
||||||
self._setup_signals()
|
|
||||||
Gtk.main()
|
|
||||||
|
|
||||||
def _setup_signals(self) -> None:
|
def _setup_signals(self) -> None:
|
||||||
SIGNAL_ADD = "signal_add"
|
SIGNAL_ADD = "signal_add"
|
||||||
SIGNAL_ADD_FULL = "signal_add_full"
|
SIGNAL_ADD_FULL = "signal_add_full"
|
||||||
|
|||||||
@ -23,3 +23,10 @@ class HBox(GenericBox):
|
|||||||
class VBox(GenericBox):
|
class VBox(GenericBox):
|
||||||
def __init__(self, spacing: int = 0) -> None:
|
def __init__(self, spacing: int = 0) -> None:
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
|
||||||
|
|
||||||
|
class ShortHBox(HBox):
|
||||||
|
def __init__(self, widget: Gtk.Widget) -> None:
|
||||||
|
super().__init__(spacing=5)
|
||||||
|
|
||||||
|
self.set_halign(Gtk.Align.START)
|
||||||
|
self.pack_start(widget, expand=False, fill=False, padding=0)
|
||||||
|
|||||||
@ -6,10 +6,10 @@ from dzgui.strings import buttons
|
|||||||
from dzgui.util.strings import (
|
from dzgui.util.strings import (
|
||||||
alert_button_tooltip,
|
alert_button_tooltip,
|
||||||
atomic_buttons,
|
atomic_buttons,
|
||||||
connect_panel,
|
|
||||||
)
|
)
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
CLIPBOARD,
|
CLIPBOARD,
|
||||||
|
CLOSE,
|
||||||
INPUT_KEYBOARD,
|
INPUT_KEYBOARD,
|
||||||
LIST_ADD,
|
LIST_ADD,
|
||||||
REFRESH_ICON,
|
REFRESH_ICON,
|
||||||
@ -17,6 +17,7 @@ from dzgui.const.constants import (
|
|||||||
WARNING,
|
WARNING,
|
||||||
WEB_BROWSER,
|
WEB_BROWSER,
|
||||||
)
|
)
|
||||||
|
from dzgui.strings import connect_panel
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@ -63,7 +64,6 @@ class IconButton(Gtk.Button):
|
|||||||
self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end)
|
self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end)
|
||||||
self.set_image(self.icon)
|
self.set_image(self.icon)
|
||||||
self.set_image_position(position)
|
self.set_image_position(position)
|
||||||
# self.set_image_position(Gtk.PositionType.RIGHT)
|
|
||||||
self.set_focus_on_click(False)
|
self.set_focus_on_click(False)
|
||||||
|
|
||||||
def swap_icon(self, icon: str) -> None:
|
def swap_icon(self, icon: str) -> None:
|
||||||
@ -128,6 +128,14 @@ class CopyIpButton(ClipboardButton):
|
|||||||
self.set_tooltip_text("Copy IP to clipboard")
|
self.set_tooltip_text("Copy IP to clipboard")
|
||||||
|
|
||||||
|
|
||||||
|
class CloseButton(IconTextButton):
|
||||||
|
def __init__(self, label: str) -> None:
|
||||||
|
super().__init__(icon=CLOSE, label=label)
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
|
self.set_tooltip_text("Unset this server as favorite")
|
||||||
|
|
||||||
|
|
||||||
class WebButton(IconTextButton):
|
class WebButton(IconTextButton):
|
||||||
def __init__(self, label: str) -> None:
|
def __init__(self, label: str) -> None:
|
||||||
super().__init__(icon=WEB_BROWSER, label=label)
|
super().__init__(icon=WEB_BROWSER, label=label)
|
||||||
@ -213,7 +221,7 @@ class KeysButton(IconTextButton):
|
|||||||
|
|
||||||
class SteamConnectButton(LargeIconTextButton):
|
class SteamConnectButton(LargeIconTextButton):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(icon=STEAM_ICON, label=connect_panel.connect)
|
super().__init__(icon=STEAM_ICON, label=connect_panel.connect_button)
|
||||||
self.set_tooltip_text(connect_panel.connect_tooltip)
|
self.set_tooltip_text(connect_panel.connect_tooltip)
|
||||||
|
|
||||||
|
|
||||||
@ -233,7 +241,7 @@ class SteamWorkshopButton(SteamTextButton):
|
|||||||
|
|
||||||
class AddButton(IconTextButton):
|
class AddButton(IconTextButton):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(icon=LIST_ADD, label=connect_panel.add)
|
super().__init__(icon=LIST_ADD, label=connect_panel.add_button)
|
||||||
self.set_tooltip_text(connect_panel.add_tooltip)
|
self.set_tooltip_text(connect_panel.add_tooltip)
|
||||||
|
|
||||||
|
|
||||||
@ -258,3 +266,38 @@ class LoggerAlertsButton(IconTextButton):
|
|||||||
self.set_halign(Gtk.Align.END)
|
self.set_halign(Gtk.Align.END)
|
||||||
self.set_hexpand(True)
|
self.set_hexpand(True)
|
||||||
self.set_tooltip_text(alert_button_tooltip)
|
self.set_tooltip_text(alert_button_tooltip)
|
||||||
|
|
||||||
|
|
||||||
|
class SpinnerButton(Gtk.Button):
|
||||||
|
def __init__(self, label: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.text = label
|
||||||
|
self.label = Gtk.Label(label=label, halign=Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
self.spinner = Gtk.Spinner()
|
||||||
|
self.spinner.set_halign(Gtk.Align.END)
|
||||||
|
self.spinner.set_sensitive(False)
|
||||||
|
|
||||||
|
grid = Gtk.Grid(column_spacing=10)
|
||||||
|
for el in self.label, self.spinner:
|
||||||
|
grid.add(el)
|
||||||
|
|
||||||
|
self.add(grid)
|
||||||
|
|
||||||
|
self.connect("clicked", self._on_button_clicked)
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
def _on_map(self, button: Self) -> None:
|
||||||
|
self.spinner.set_visible(False)
|
||||||
|
|
||||||
|
def _on_button_clicked(self, button: Self) -> None:
|
||||||
|
self.start_spinner()
|
||||||
|
|
||||||
|
def stop_spinner(self) -> None:
|
||||||
|
self.spinner.set_visible(False)
|
||||||
|
self.spinner.stop()
|
||||||
|
|
||||||
|
def start_spinner(self) -> None:
|
||||||
|
self.spinner.set_visible(True)
|
||||||
|
self.spinner.start()
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
from dzgui.strings import preconnect
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gdk, Gtk # noqa E402
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectBox(Gtk.Box):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
|
||||||
|
|
||||||
self.back = Gtk.Button(
|
|
||||||
label=preconnect.back, halign=Gtk.Align.END, hexpand=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: dynamically update this
|
|
||||||
self.ok = Gtk.Button(label="Launch", halign=Gtk.Align.END)
|
|
||||||
|
|
||||||
for button in self.back, self.ok:
|
|
||||||
self.add(button)
|
|
||||||
|
|
||||||
def get_back_button(self) -> Gtk.Button:
|
|
||||||
return self.back
|
|
||||||
|
|
||||||
def get_ok_button(self) -> Gtk.Button:
|
|
||||||
return self.ok
|
|
||||||
|
|
||||||
|
|
||||||
class PreconnectBox(Gtk.Box):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__(
|
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
|
||||||
valign=Gtk.Align.END,
|
|
||||||
vexpand=True,
|
|
||||||
spacing=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.button_hbox = ConnectBox()
|
|
||||||
self.ok = self.button_hbox.get_ok_button()
|
|
||||||
self.ok.set_label(preconnect.update_mods)
|
|
||||||
|
|
||||||
self.connect_last = Gtk.Button(
|
|
||||||
label=preconnect.connect_last,
|
|
||||||
halign=Gtk.Align.END,
|
|
||||||
tooltip_text=preconnect.connect_last_tooltip,
|
|
||||||
)
|
|
||||||
self.button_hbox.add(self.connect_last)
|
|
||||||
|
|
||||||
self.raise_window = Gtk.CheckButton(
|
|
||||||
# TODO: strings
|
|
||||||
label="Foreground DZGUI while downloading",
|
|
||||||
halign=Gtk.Align.END,
|
|
||||||
hexpand=True,
|
|
||||||
valign=Gtk.Align.END,
|
|
||||||
visible=False,
|
|
||||||
has_tooltip=True,
|
|
||||||
sensitive=False,
|
|
||||||
tooltip_text="Foreground the DZGUI window after mod downloads are queued",
|
|
||||||
active=True,
|
|
||||||
)
|
|
||||||
self.add(self.button_hbox)
|
|
||||||
self.add(self.raise_window)
|
|
||||||
|
|
||||||
def get_back_button(self) -> Gtk.Button:
|
|
||||||
return self.button_hbox.get_back_button()
|
|
||||||
|
|
||||||
def get_ok_button(self) -> Gtk.Button:
|
|
||||||
return self.ok
|
|
||||||
@ -6,6 +6,7 @@ from dzgui.strings import connect_panel
|
|||||||
from dzgui.util.keys import is_ctrl_mask
|
from dzgui.util.keys import is_ctrl_mask
|
||||||
from dzgui.views.components.buttons import (
|
from dzgui.views.components.buttons import (
|
||||||
AddButton,
|
AddButton,
|
||||||
|
CloseButton,
|
||||||
CopyIpButton,
|
CopyIpButton,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
@ -187,17 +188,27 @@ class FavPanel(Gtk.Frame):
|
|||||||
self.fav_button = SteamConnectButton()
|
self.fav_button = SteamConnectButton()
|
||||||
self.fav_button.connect("clicked", self._on_connect_clicked)
|
self.fav_button.connect("clicked", self._on_connect_clicked)
|
||||||
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip)
|
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip)
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
|
self.unset_button = CloseButton("Unset")
|
||||||
|
self.unset_button.connect("clicked", self._on_unset_clicked)
|
||||||
|
|
||||||
if favorite is None:
|
if favorite is None:
|
||||||
self.toggle_buttons(False)
|
self.toggle_buttons(False)
|
||||||
|
|
||||||
# NOTE: disable vscrollbar to prevent layout jumping behavior
|
# NOTE: disable vscrollbar to prevent layout jumping behavior
|
||||||
scrollable_label = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
|
self.scrollable_label = Gtk.ScrolledWindow(
|
||||||
scrollable_label.add(self.fav_label)
|
vscrollbar_policy=Gtk.PolicyType.NEVER, overlay_scrolling=False
|
||||||
|
)
|
||||||
|
self.scrollable_label.add(self.fav_label)
|
||||||
|
|
||||||
grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5)
|
grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5)
|
||||||
grid.attach(scrollable_label, 0, 0, 3, ROWS)
|
grid.attach(self.scrollable_label, 0, 0, 3, ROWS)
|
||||||
grid.attach_next_to(
|
grid.attach_next_to(
|
||||||
self.copy_button, scrollable_label, Gtk.PositionType.RIGHT, COLS, ROWS
|
self.unset_button, self.scrollable_label, Gtk.PositionType.RIGHT, COLS, ROWS
|
||||||
|
)
|
||||||
|
grid.attach_next_to(
|
||||||
|
self.copy_button, self.unset_button, Gtk.PositionType.RIGHT, COLS, ROWS
|
||||||
)
|
)
|
||||||
grid.attach_next_to(
|
grid.attach_next_to(
|
||||||
self.fav_button, self.copy_button, Gtk.PositionType.RIGHT, COLS, ROWS
|
self.fav_button, self.copy_button, Gtk.PositionType.RIGHT, COLS, ROWS
|
||||||
@ -205,8 +216,16 @@ class FavPanel(Gtk.Frame):
|
|||||||
|
|
||||||
self.add(grid)
|
self.add(grid)
|
||||||
|
|
||||||
|
def _on_unset_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
try:
|
||||||
|
self.controller.unset_fav()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
self.fav_label.set_text(connect_panel.favs_empty)
|
||||||
|
self.toggle_buttons(False)
|
||||||
|
|
||||||
def toggle_buttons(self, state: bool) -> None:
|
def toggle_buttons(self, state: bool) -> None:
|
||||||
for button in self.fav_button, self.copy_button:
|
for button in self.fav_button, self.copy_button, self.unset_button:
|
||||||
button.set_sensitive(state)
|
button.set_sensitive(state)
|
||||||
|
|
||||||
def _on_connect_clicked(self, button: Gtk.Button) -> None:
|
def _on_connect_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from typing import Any, Callable, TYPE_CHECKING
|
|||||||
from dzgui.api.servers import validate_ip
|
from dzgui.api.servers import validate_ip
|
||||||
from dzgui.const.constants import VIEW_CONCEAL, VIEW_REVEAL
|
from dzgui.const.constants import VIEW_CONCEAL, VIEW_REVEAL
|
||||||
from dzgui.util.css import add_class, remove_class
|
from dzgui.util.css import add_class, remove_class
|
||||||
from dzgui.util.strings import connect_panel, lan_panel
|
from dzgui.strings import connect_panel
|
||||||
from dzgui.strings.errors import api_popover
|
from dzgui.strings.errors import api_popover
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@ -122,11 +122,8 @@ class IpEntry(ValidatedEntry):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
controller,
|
controller,
|
||||||
func=validate_ip_truthy,
|
func=validate_ip_truthy,
|
||||||
placeholder_text=connect_panel.placeholder,
|
placeholder_text=connect_panel.connect_entry_placeholder,
|
||||||
tooltip_text=connect_panel.entry_tooltip,
|
|
||||||
)
|
)
|
||||||
self.set_placeholder_text(connect_panel.placeholder)
|
|
||||||
self.set_tooltip_text(connect_panel.entry_tooltip)
|
|
||||||
|
|
||||||
|
|
||||||
class PortEntry(ValidatedEntry):
|
class PortEntry(ValidatedEntry):
|
||||||
@ -134,11 +131,9 @@ class PortEntry(ValidatedEntry):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
controller,
|
controller,
|
||||||
func=validate_port,
|
func=validate_port,
|
||||||
placeholder_text=connect_panel.placeholder,
|
placeholder_text=connect_panel.lan_entry_placeholder,
|
||||||
tooltip_text=connect_panel.entry_tooltip,
|
tooltip_text=connect_panel.lan_entry_tooltip,
|
||||||
)
|
)
|
||||||
self.set_placeholder_text(lan_panel.placeholder)
|
|
||||||
self.set_tooltip_text(lan_panel.entry_tooltip)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: backport to Options page
|
# TODO: backport to Options page
|
||||||
|
|||||||
@ -29,21 +29,33 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class ButtonGrid(Gtk.Grid):
|
class ButtonGrid(Gtk.Grid):
|
||||||
def __init__(self, controller: "Controller", defaults: dict) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller: "Controller",
|
||||||
|
defaults: dict[str, str],
|
||||||
|
tooltips: dict[str, str],
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
halign=Gtk.Align.CENTER, column_spacing=5, column_homogeneous=True
|
halign=Gtk.Align.CENTER, column_spacing=5, column_homogeneous=True
|
||||||
)
|
)
|
||||||
row = 1
|
|
||||||
col = 0
|
|
||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
self.checks: list[Gtk.CheckButton] = []
|
self.checks: list[Gtk.CheckButton] = []
|
||||||
|
|
||||||
# TODO: use enumerated checks
|
flowbox = Gtk.FlowBox(
|
||||||
|
halign=Gtk.Align.CENTER, min_children_per_line=3, max_children_per_line=3, selection_mode=Gtk.SelectionMode.NONE
|
||||||
|
)
|
||||||
|
|
||||||
for check in defaults.keys():
|
for check in defaults.keys():
|
||||||
checkbox = Gtk.CheckButton(label=check)
|
checkbox = Gtk.CheckButton(label=check)
|
||||||
|
try:
|
||||||
|
tt = tooltips[check]
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
tt = ""
|
||||||
|
|
||||||
|
checkbox.set_tooltip_text(tt)
|
||||||
label = checkbox.get_child()
|
label = checkbox.get_child()
|
||||||
if label is not None:
|
if label is not None:
|
||||||
label.set_ellipsize(Pango.EllipsizeMode.END) # type: ignore
|
label.set_ellipsize(Pango.EllipsizeMode.END) # type: ignore
|
||||||
@ -51,14 +63,12 @@ class ButtonGrid(Gtk.Grid):
|
|||||||
if defaults[check]:
|
if defaults[check]:
|
||||||
checkbox.set_active(True)
|
checkbox.set_active(True)
|
||||||
|
|
||||||
col = col + 1
|
|
||||||
if col > 3:
|
|
||||||
row += 1
|
|
||||||
col = 1
|
|
||||||
self.attach(checkbox, col, row, 1, 1)
|
|
||||||
checkbox.connect("toggled", self._on_check_toggled)
|
checkbox.connect("toggled", self._on_check_toggled)
|
||||||
|
flowbox.add(checkbox)
|
||||||
self.checks.append(checkbox)
|
self.checks.append(checkbox)
|
||||||
|
|
||||||
|
self.add(flowbox)
|
||||||
|
|
||||||
def block_toggles(self, state: bool) -> None:
|
def block_toggles(self, state: bool) -> None:
|
||||||
for check in self.checks:
|
for check in self.checks:
|
||||||
self.controller.suppress_signal(
|
self.controller.suppress_signal(
|
||||||
@ -163,11 +173,12 @@ class FilterPanel(Gtk.Box):
|
|||||||
|
|
||||||
filter_man = self.controller.get_filter_man()
|
filter_man = self.controller.get_filter_man()
|
||||||
defaults = filter_man.get_default_filters()
|
defaults = filter_man.get_default_filters()
|
||||||
|
tooltips = filter_man.get_tooltips()
|
||||||
self.map_store = filter_man.get_map_store()
|
self.map_store = filter_man.get_map_store()
|
||||||
self.enabled_filters = defaults
|
self.enabled_filters = defaults
|
||||||
|
|
||||||
self.keyword_entry = KeywordEntry(self.controller)
|
self.keyword_entry = KeywordEntry(self.controller)
|
||||||
self.button_grid = ButtonGrid(self.controller, defaults)
|
self.button_grid = ButtonGrid(self.controller, defaults, tooltips)
|
||||||
|
|
||||||
# TODO: strings
|
# TODO: strings
|
||||||
self.filters_label = BoldLabel("Filters")
|
self.filters_label = BoldLabel("Filters")
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
from dzgui.const.constants import ERROR, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
|
||||||
from dzgui.strings import options
|
from dzgui.strings import options
|
||||||
from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
|
from dzgui.views.components.box import HBox
|
||||||
|
from dzgui.views.components.buttons import Icon
|
||||||
|
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@ -33,3 +36,18 @@ class ClientCombo(Gtk.ComboBox):
|
|||||||
self.pack_start(renderer_text, True)
|
self.pack_start(renderer_text, True)
|
||||||
self.add_attribute(renderer_text, "text", 0)
|
self.add_attribute(renderer_text, "text", 0)
|
||||||
self.set_active(0)
|
self.set_active(0)
|
||||||
|
|
||||||
|
class ErrorPopover(Gtk.Popover):
|
||||||
|
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
|
||||||
|
super().__init__(relative_to=relative_to, position=position)
|
||||||
|
|
||||||
|
self.hbox = HBox()
|
||||||
|
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
||||||
|
error_icon = Icon(ERROR, margin_start=10)
|
||||||
|
self.hbox.extend([error_icon, self.label])
|
||||||
|
self.add(self.hbox)
|
||||||
|
self.show_all()
|
||||||
|
self.popdown()
|
||||||
|
|
||||||
|
def set_label(self, label: str) -> None:
|
||||||
|
self.label.set_label(label)
|
||||||
|
|||||||
@ -4,6 +4,9 @@ from typing import Any, Literal, Self, TYPE_CHECKING
|
|||||||
# import time
|
# import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
|
|
||||||
# TODO: import dialog titles
|
# TODO: import dialog titles
|
||||||
from dzgui.api.mods import remove_stale_signatures as remove_stale
|
from dzgui.api.mods import remove_stale_signatures as remove_stale
|
||||||
from dzgui.config.ipdb import get_ipdb
|
from dzgui.config.ipdb import get_ipdb
|
||||||
@ -37,7 +40,7 @@ class Success(Enum):
|
|||||||
|
|
||||||
# TODO: strings for "Running", "Failed", etc.
|
# TODO: strings for "Running", "Failed", etc.
|
||||||
# TODO: add margins to tree
|
# TODO: add margins to tree
|
||||||
class BootDialog(Gtk.Dialog):
|
class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore
|
||||||
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
|
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
title=dialog_header,
|
title=dialog_header,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import sys
|
|||||||
|
|
||||||
from typing import Self
|
from typing import Self
|
||||||
from dzgui.util.strings import dialog_error, dialog_header
|
from dzgui.util.strings import dialog_error, dialog_header
|
||||||
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@ -10,21 +11,26 @@ gi.require_version("Gtk", "3.0")
|
|||||||
from gi.repository import Gtk # noqa E402
|
from gi.repository import Gtk # noqa E402
|
||||||
|
|
||||||
|
|
||||||
class EarlyAlertDialog(Gtk.MessageDialog):
|
class AbortDialog(ColorAwareApp, Gtk.MessageDialog): # type: ignore
|
||||||
def __init__(self, string: str) -> None:
|
def __init__(self, string: str, buttons: Gtk.ButtonsType) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
title=dialog_header,
|
title=dialog_header,
|
||||||
text=dialog_error,
|
text=dialog_error,
|
||||||
transient_for=None,
|
transient_for=None,
|
||||||
buttons=Gtk.ButtonsType.OK,
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = textwrap.fill(string, 50)
|
msg = textwrap.fill(string, 50)
|
||||||
self.format_secondary_text(msg)
|
self.format_secondary_text(msg)
|
||||||
|
|
||||||
|
ma = self.get_message_area()
|
||||||
|
label = ma.get_children()[1] # type: ignore
|
||||||
|
label.set_justify(Gtk.Justification.CENTER)
|
||||||
|
|
||||||
aa = self.get_action_area()
|
aa = self.get_action_area()
|
||||||
aa.set_margin_bottom(20)
|
aa.set_margin_bottom(20)
|
||||||
# self.action_area.set_margin_bottom(20)
|
aa.set_layout(Gtk.ButtonBoxStyle.CENTER) # type: ignore
|
||||||
|
|
||||||
self.outer = self.get_content_area()
|
self.outer = self.get_content_area()
|
||||||
self.outer.set_margin_start(30)
|
self.outer.set_margin_start(30)
|
||||||
self.outer.set_margin_end(30)
|
self.outer.set_margin_end(30)
|
||||||
@ -32,26 +38,30 @@ class EarlyAlertDialog(Gtk.MessageDialog):
|
|||||||
self.set_default_size(250, 100)
|
self.set_default_size(250, 100)
|
||||||
|
|
||||||
abort = self.get_widget_for_response(Gtk.ResponseType.OK)
|
abort = self.get_widget_for_response(Gtk.ResponseType.OK)
|
||||||
|
ignore = self.get_widget_for_response(Gtk.ResponseType.CANCEL)
|
||||||
if abort is not None and hasattr(abort, "set_label"):
|
if abort is not None and hasattr(abort, "set_label"):
|
||||||
abort.set_label("Exit")
|
abort.set_label("Exit")
|
||||||
|
if ignore is not None and hasattr(ignore, "set_label"):
|
||||||
|
ignore.set_label("Ignore")
|
||||||
|
|
||||||
self.connect("response", self._on_response)
|
self.connect("response", self._on_response)
|
||||||
|
|
||||||
self.run()
|
self.run()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
||||||
match response:
|
match response:
|
||||||
case Gtk.ResponseType.OK:
|
case Gtk.ResponseType.OK | Gtk.ResponseType.DELETE_EVENT:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
case Gtk.ResponseType.CANCEL:
|
case Gtk.ResponseType.CANCEL:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class EarlyIgnoreDialog(EarlyAlertDialog):
|
class EarlyAlertDialog(AbortDialog):
|
||||||
def __init__(self, string: str) -> None:
|
def __init__(self, string: str) -> None:
|
||||||
super().__init__(string=string)
|
super().__init__(string=string, buttons=Gtk.ButtonsType.OK)
|
||||||
|
|
||||||
# TODO: reverse order
|
|
||||||
self.add_button("Ignore", Gtk.ResponseType.CANCEL)
|
|
||||||
|
|
||||||
# TODO: if exit, sys.exit(1), else pass
|
class EarlyIgnoreDialog(AbortDialog):
|
||||||
|
def __init__(self, string: str) -> None:
|
||||||
|
super().__init__(string=string, buttons=Gtk.ButtonsType.OK_CANCEL)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from typing import Literal, Self, TYPE_CHECKING
|
from typing import Literal, Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL
|
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.util import strings
|
||||||
from dzgui.views.components.buttons import ClipboardButton
|
from dzgui.views.components.buttons import ClipboardButton
|
||||||
|
|
||||||
@ -107,7 +108,7 @@ class WaitDialog(GenericDialog):
|
|||||||
self.cur_job = 1
|
self.cur_job = 1
|
||||||
|
|
||||||
self.cancel = Gtk.Button(label="Cancel", halign=Gtk.Align.CENTER)
|
self.cancel = Gtk.Button(label="Cancel", halign=Gtk.Align.CENTER)
|
||||||
self.cancel.connect("clicked", lambda _: self.controller.set_cancel_event())
|
self.cancel.connect("clicked", self._on_cancel_clicked)
|
||||||
|
|
||||||
self.connect("delete-event", lambda widget, event: True)
|
self.connect("delete-event", lambda widget, event: True)
|
||||||
content = self.get_content_area()
|
content = self.get_content_area()
|
||||||
@ -138,6 +139,11 @@ class WaitDialog(GenericDialog):
|
|||||||
self.prog.set_fraction(fraction)
|
self.prog.set_fraction(fraction)
|
||||||
self.cur_job += 1
|
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")
|
||||||
|
self.controller.set_cancel_event()
|
||||||
|
|
||||||
def show_cancel(self, state: bool) -> None:
|
def show_cancel(self, state: bool) -> None:
|
||||||
self.cancel.set_visible(state)
|
self.cancel.set_visible(state)
|
||||||
|
|
||||||
@ -156,6 +162,7 @@ class QuitDialog(GenericDialog):
|
|||||||
self.add_button(strings.exit_app, Gtk.ResponseType.OK)
|
self.add_button(strings.exit_app, Gtk.ResponseType.OK)
|
||||||
|
|
||||||
self.connect("response", self._on_response)
|
self.connect("response", self._on_response)
|
||||||
|
# TODO: superfluous with the above
|
||||||
self.connect("delete-event", self._on_response)
|
self.connect("delete-event", self._on_response)
|
||||||
|
|
||||||
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
||||||
@ -163,58 +170,30 @@ class QuitDialog(GenericDialog):
|
|||||||
self.controller.save_res_and_quit()
|
self.controller.save_res_and_quit()
|
||||||
|
|
||||||
|
|
||||||
class ExceptionDialog(GenericDialog):
|
class TextBufferDialog(GenericDialog):
|
||||||
"""
|
def __init__(
|
||||||
Error dialog with rich traceback.
|
self,
|
||||||
Usage:
|
controller: "Controller",
|
||||||
try:
|
mtype: Gtk.MessageType,
|
||||||
foo()
|
heading: str,
|
||||||
except Exception:
|
secondary: str,
|
||||||
trace = traceback.format_exc()
|
text: str,
|
||||||
dialog = ExceptionDialog(Controller, trace)
|
):
|
||||||
dialog.run()
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, controller: "Controller", trace: str):
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
controller=controller,
|
controller=controller,
|
||||||
text=strings.error_heading,
|
text=heading,
|
||||||
mtype=Gtk.MessageType.ERROR,
|
mtype=mtype,
|
||||||
buttons=Gtk.ButtonsType.NONE,
|
buttons=Gtk.ButtonsType.NONE,
|
||||||
secondary=strings.something_wrong,
|
secondary=secondary,
|
||||||
)
|
)
|
||||||
|
self.text = text
|
||||||
self.trace = trace
|
copy_button = ClipboardButton(controller, self.get_text)
|
||||||
# NOTE: box expands to end of content area
|
|
||||||
scrollable = Gtk.ScrolledWindow(
|
|
||||||
propagate_natural_height=True, max_content_height=500
|
|
||||||
)
|
|
||||||
box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL)
|
|
||||||
# TODO: wrap/truncate long messages
|
|
||||||
textview = Gtk.TextView(
|
|
||||||
wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10
|
|
||||||
)
|
|
||||||
textview.set_buffer(Gtk.TextBuffer(text=self.trace))
|
|
||||||
box.pack_start(textview, EXPAND, FILL, 10)
|
|
||||||
scrollable.add(box)
|
|
||||||
|
|
||||||
content = self.get_content_area()
|
|
||||||
content.set_spacing(0)
|
|
||||||
# TODO: padding around top of content area when traceback is long
|
|
||||||
content.add(scrollable)
|
|
||||||
|
|
||||||
copy_button = ClipboardButton(controller, self.get_trace)
|
|
||||||
self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
|
self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
|
||||||
self.add_button("OK", Gtk.ResponseType.OK)
|
self.add_button("OK", Gtk.ResponseType.OK)
|
||||||
|
|
||||||
self.show_all()
|
|
||||||
self.ok = self.get_widget_for_response(Gtk.ResponseType.OK)
|
|
||||||
if self.ok is not None:
|
|
||||||
self.ok.grab_focus()
|
|
||||||
self.connect("response", self._on_response)
|
self.connect("response", self._on_response)
|
||||||
|
|
||||||
def get_trace(self) -> str:
|
def get_text(self) -> str:
|
||||||
return self.trace
|
return self.text
|
||||||
|
|
||||||
def _on_response(
|
def _on_response(
|
||||||
self, dialog: Self, response: Gtk.ResponseType
|
self, dialog: Self, response: Gtk.ResponseType
|
||||||
@ -230,3 +209,114 @@ class ExceptionDialog(GenericDialog):
|
|||||||
return None
|
return None
|
||||||
case _:
|
case _:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DebugDialog(TextBufferDialog):
|
||||||
|
def __init__(self, controller: "Controller", debug: str):
|
||||||
|
super().__init__(
|
||||||
|
controller=controller,
|
||||||
|
mtype=Gtk.MessageType.INFO,
|
||||||
|
heading=dialogs.debug_heading,
|
||||||
|
secondary=dialogs.debug_secondary,
|
||||||
|
text=debug,
|
||||||
|
)
|
||||||
|
scrollable = Gtk.ScrolledWindow(
|
||||||
|
propagate_natural_height=False,
|
||||||
|
min_content_height=200,
|
||||||
|
max_content_height=200,
|
||||||
|
margin_bottom=10,
|
||||||
|
)
|
||||||
|
textview = Gtk.TextView(
|
||||||
|
wrap_mode=Gtk.WrapMode.CHAR, editable=False, left_margin=10, right_margin=10
|
||||||
|
)
|
||||||
|
textview.set_buffer(Gtk.TextBuffer(text=debug))
|
||||||
|
scrollable.add(textview)
|
||||||
|
content = self.get_content_area()
|
||||||
|
content.set_spacing(0)
|
||||||
|
content.add(scrollable)
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionDialog(TextBufferDialog):
|
||||||
|
"""
|
||||||
|
Error dialog with rich traceback.
|
||||||
|
Usage:
|
||||||
|
try:
|
||||||
|
foo()
|
||||||
|
except Exception:
|
||||||
|
trace = traceback.format_exc()
|
||||||
|
dialog = ExceptionDialog(Controller, trace)
|
||||||
|
dialog.show_all()
|
||||||
|
dialog.run()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, controller: "Controller", trace: str):
|
||||||
|
super().__init__(
|
||||||
|
controller=controller,
|
||||||
|
heading=strings.error_heading,
|
||||||
|
secondary=strings.something_wrong,
|
||||||
|
text=trace,
|
||||||
|
mtype=Gtk.MessageType.ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE: box expands to end of content area
|
||||||
|
scrollable = Gtk.ScrolledWindow(
|
||||||
|
propagate_natural_height=True, max_content_height=500
|
||||||
|
)
|
||||||
|
box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
textview = Gtk.TextView(
|
||||||
|
wrap_mode=Gtk.WrapMode.WORD,
|
||||||
|
editable=False,
|
||||||
|
left_margin=10,
|
||||||
|
right_margin=10,
|
||||||
|
top_margin=15,
|
||||||
|
bottom_margin=10,
|
||||||
|
)
|
||||||
|
textview.set_buffer(Gtk.TextBuffer(text=trace))
|
||||||
|
box.pack_start(textview, EXPAND, FILL, 0)
|
||||||
|
|
||||||
|
self.error_details = Gtk.ScrolledWindow(
|
||||||
|
overlay_scrolling=False,
|
||||||
|
max_content_height=150,
|
||||||
|
propagate_natural_height=False,
|
||||||
|
)
|
||||||
|
details_box = Gtk.Box(
|
||||||
|
hexpand=True,
|
||||||
|
vexpand=True,
|
||||||
|
margin_right=5,
|
||||||
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.details_buffer = Gtk.TextBuffer()
|
||||||
|
details_textview = Gtk.TextView(
|
||||||
|
wrap_mode=Gtk.WrapMode.WORD_CHAR,
|
||||||
|
editable=False,
|
||||||
|
left_margin=10,
|
||||||
|
right_margin=10,
|
||||||
|
top_margin=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
details_textview.set_buffer(self.details_buffer)
|
||||||
|
details_box.pack_start(details_textview, EXPAND, FILL, 10)
|
||||||
|
self.error_details.add(details_box)
|
||||||
|
|
||||||
|
self.error_notebook = Gtk.Notebook(show_tabs=False, margin_bottom=15)
|
||||||
|
self.error_notebook.append_page(box, Gtk.Label(label="Error"))
|
||||||
|
self.error_notebook.append_page(self.error_details, Gtk.Label(label="Details"))
|
||||||
|
self.error_notebook.connect("switch-page", self._on_page_changed)
|
||||||
|
scrollable.add(self.error_notebook)
|
||||||
|
|
||||||
|
content = self.get_content_area()
|
||||||
|
content.set_spacing(0)
|
||||||
|
content.add(scrollable)
|
||||||
|
|
||||||
|
def _on_page_changed(
|
||||||
|
self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int
|
||||||
|
) -> None:
|
||||||
|
if child == self.error_details:
|
||||||
|
GLib.idle_add(self.error_details.set_propagate_natural_height, True)
|
||||||
|
else:
|
||||||
|
GLib.idle_add(self.error_details.set_propagate_natural_height, False)
|
||||||
|
|
||||||
|
def set_details_buffer(self, text: str) -> None:
|
||||||
|
self.details_buffer.set_text(text)
|
||||||
|
self.error_notebook.set_show_tabs(True)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import html
|
||||||
from typing import Self, TYPE_CHECKING
|
from typing import Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import EXPAND, FILL
|
from dzgui.const.constants import EXPAND, FILL
|
||||||
@ -65,6 +66,7 @@ class ServerDialog(GenericDialog):
|
|||||||
|
|
||||||
class ServerDetailsDialog(ServerDialog):
|
class ServerDetailsDialog(ServerDialog):
|
||||||
def __init__(self, controller: "Controller", details: "Details"):
|
def __init__(self, controller: "Controller", details: "Details"):
|
||||||
|
# TODO: server name should also be packed in details struct
|
||||||
name = controller.get_server_name()
|
name = controller.get_server_name()
|
||||||
super().__init__(controller, strings.server_details, name, menu=None)
|
super().__init__(controller, strings.server_details, name, menu=None)
|
||||||
|
|
||||||
@ -109,7 +111,7 @@ class ServerDetailsDialog(ServerDialog):
|
|||||||
self.view.set_model(self.store)
|
self.view.set_model(self.store)
|
||||||
text = details.description
|
text = details.description
|
||||||
text = format_hyperlinks(text)
|
text = format_hyperlinks(text)
|
||||||
self.description.set_markup(text)
|
self.description.set_markup(html.escape(text))
|
||||||
|
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
|
|||||||
230
dzgui/views/dialogs/uninstall.py
Normal file
230
dzgui/views/dialogs/uninstall.py
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from dzgui.api.shortcuts import Shortcuts
|
||||||
|
from dzgui.const.constants import APP_NAME
|
||||||
|
from dzgui.strings import uninstall
|
||||||
|
from dzgui.util._json import read_json
|
||||||
|
from dzgui.util.css import load_css
|
||||||
|
from dzgui.util.format import format_exception
|
||||||
|
from dzgui.views.components.box import HBox
|
||||||
|
from dzgui.views.dialogs.wizard import ScrolledWizardPage, CheckboxWithLabel
|
||||||
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk, GLib, Pango # noqa E402
|
||||||
|
|
||||||
|
|
||||||
|
class Assistant(Gtk.Assistant):
|
||||||
|
def __init__(self, is_deck: bool, paths: dict[str, str]):
|
||||||
|
super().__init__()
|
||||||
|
if is_deck:
|
||||||
|
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||||
|
self.set_default_size(1085, 670)
|
||||||
|
else:
|
||||||
|
self.set_default_size(1500, 900)
|
||||||
|
|
||||||
|
self.set_forward_page_func(self._advance_page)
|
||||||
|
|
||||||
|
pyapp = os.getenv("PYAPP")
|
||||||
|
|
||||||
|
self.page1 = UninstallPage(paths, pyapp)
|
||||||
|
self.page2 = CompletionPage(pyapp)
|
||||||
|
|
||||||
|
self.append_page(self.page1)
|
||||||
|
self.set_page_type(self.page1, Gtk.AssistantPageType.INTRO)
|
||||||
|
self.set_page_title(self.page1, uninstall.pg1_title)
|
||||||
|
self.set_page_complete(self.page1, True)
|
||||||
|
|
||||||
|
self.append_page(self.page2)
|
||||||
|
self.set_page_title(self.page2, uninstall.pg2_title)
|
||||||
|
self.set_page_type(self.page2, Gtk.AssistantPageType.SUMMARY)
|
||||||
|
|
||||||
|
self.connect("cancel", self.destroy_and_quit)
|
||||||
|
self.connect("close", self.destroy_and_quit)
|
||||||
|
self.show_all()
|
||||||
|
load_css()
|
||||||
|
|
||||||
|
def _advance_page(self, index: int) -> int:
|
||||||
|
cur_page = self.get_nth_page(index)
|
||||||
|
if cur_page == self.page1:
|
||||||
|
self.page1.uninstall()
|
||||||
|
return index + 1
|
||||||
|
|
||||||
|
def destroy_and_quit(self, widget: Self) -> None:
|
||||||
|
self.destroy()
|
||||||
|
Gtk.main_quit()
|
||||||
|
|
||||||
|
|
||||||
|
class Window(Gtk.Window):
|
||||||
|
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||||
|
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||||
|
self.assistant = Assistant(is_deck, paths)
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionPage(ScrolledWizardPage):
|
||||||
|
def __init__(self, pyapp: str | None):
|
||||||
|
super().__init__(heading=uninstall.pg2_heading, description=uninstall.pg2_blurb)
|
||||||
|
|
||||||
|
text = (
|
||||||
|
uninstall.standalone_uninstall
|
||||||
|
if pyapp is not None
|
||||||
|
else uninstall.system_uninstall
|
||||||
|
)
|
||||||
|
|
||||||
|
label = Gtk.Label(
|
||||||
|
label=text,
|
||||||
|
wrap_mode=Pango.WrapMode.WORD,
|
||||||
|
margin=10,
|
||||||
|
max_width_chars=80,
|
||||||
|
justify=Gtk.Justification.CENTER,
|
||||||
|
)
|
||||||
|
frame = Gtk.Frame()
|
||||||
|
frame.add(label)
|
||||||
|
self.add_start(frame)
|
||||||
|
|
||||||
|
|
||||||
|
class FileLabel(HBox):
|
||||||
|
def __init__(self, path: Path) -> None:
|
||||||
|
super().__init__(spacing=10)
|
||||||
|
|
||||||
|
label = Gtk.Label(label=uninstall.path_remove_prefix, halign=Gtk.Align.START)
|
||||||
|
textview = Gtk.TextView(
|
||||||
|
editable=False,
|
||||||
|
halign=Gtk.Align.START,
|
||||||
|
left_margin=10,
|
||||||
|
right_margin=10,
|
||||||
|
)
|
||||||
|
textview.set_buffer(Gtk.TextBuffer(text=str(path)))
|
||||||
|
self.extend([label, textview])
|
||||||
|
|
||||||
|
|
||||||
|
class CheckboxWithPath(CheckboxWithLabel):
|
||||||
|
def __init__(
|
||||||
|
self, label: str, details: str, path: Path, active: bool = True
|
||||||
|
) -> None:
|
||||||
|
super().__init__(text=label, blurb_text=details)
|
||||||
|
|
||||||
|
self.conf_path = path
|
||||||
|
self.set_active(active)
|
||||||
|
fl = FileLabel(path)
|
||||||
|
self.indent_below(fl)
|
||||||
|
|
||||||
|
def get_conf_path(self) -> Path:
|
||||||
|
return self.conf_path
|
||||||
|
|
||||||
|
|
||||||
|
class UninstallPage(ScrolledWizardPage):
|
||||||
|
def __init__(self, paths: dict[str, str], pyapp: str | None):
|
||||||
|
super().__init__(heading=uninstall.pg1_heading, description=uninstall.pg1_blurb)
|
||||||
|
|
||||||
|
self.pyapp = pyapp
|
||||||
|
|
||||||
|
config = Path(paths["XDG_CONFIG_HOME"])
|
||||||
|
state = Path(paths["XDG_STATE_HOME"])
|
||||||
|
self.share = Path(paths["XDG_DATA_HOME"])
|
||||||
|
shortcut = self.share.parent.joinpath("applications/dzgui.desktop")
|
||||||
|
desktop = config.parent.parent.joinpath("Desktop/dzgui.desktop")
|
||||||
|
self.steam_path = self.parse_steam_path(config, self.share)
|
||||||
|
|
||||||
|
self.boxes: list[CheckboxWithPath] = []
|
||||||
|
self.checks_area = Gtk.Box(
|
||||||
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
|
halign=Gtk.Align.CENTER,
|
||||||
|
spacing=20,
|
||||||
|
margin_top=20,
|
||||||
|
margin_bottom=20,
|
||||||
|
)
|
||||||
|
self.config_box = CheckboxWithPath(
|
||||||
|
uninstall.config_label, uninstall.config_details, config, active=False
|
||||||
|
)
|
||||||
|
self.desktop_box = CheckboxWithPath(
|
||||||
|
uninstall.desktop_label, uninstall.desktop_details, desktop
|
||||||
|
)
|
||||||
|
self.start_menu_box = CheckboxWithPath(
|
||||||
|
uninstall.start_menu_label, uninstall.start_menu_details, shortcut
|
||||||
|
)
|
||||||
|
self.state_box = CheckboxWithPath(
|
||||||
|
uninstall.state_label, uninstall.state_details, state
|
||||||
|
)
|
||||||
|
self.steam_box = CheckboxWithLabel(
|
||||||
|
uninstall.steam_shortcut, uninstall.steam_details
|
||||||
|
)
|
||||||
|
|
||||||
|
for box in (
|
||||||
|
self.config_box,
|
||||||
|
self.state_box,
|
||||||
|
self.desktop_box,
|
||||||
|
self.start_menu_box,
|
||||||
|
):
|
||||||
|
self.checks_area.add(box)
|
||||||
|
self.boxes.append(box)
|
||||||
|
|
||||||
|
self.checks_area.add(self.steam_box)
|
||||||
|
|
||||||
|
self.add_start(self.checks_area)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
def parse_steam_path(self, config: Path, steam: Path) -> Path | None:
|
||||||
|
try:
|
||||||
|
file = config.joinpath("config.json")
|
||||||
|
conf = read_json(file)
|
||||||
|
steam = conf["default_steam_path"]
|
||||||
|
self.steam_path = Path(steam)
|
||||||
|
return Path(steam)
|
||||||
|
except Exception as e:
|
||||||
|
print(format_exception(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def wipe_conf_file(self, box: CheckboxWithPath) -> None:
|
||||||
|
if box.get_active():
|
||||||
|
path = box.get_conf_path()
|
||||||
|
try:
|
||||||
|
if path.is_dir():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
else:
|
||||||
|
path.unlink()
|
||||||
|
print(f"Deleted '{path}'")
|
||||||
|
except Exception as e:
|
||||||
|
print(format_exception(e))
|
||||||
|
|
||||||
|
def wipe_steam_shortcut(self) -> None:
|
||||||
|
if self.steam_path is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
shortcuts = Shortcuts(self.steam_path)
|
||||||
|
shortcuts.delete_shortcut(self.share)
|
||||||
|
shutil.rmtree(self.share)
|
||||||
|
except Exception as e:
|
||||||
|
print(format_exception(e))
|
||||||
|
|
||||||
|
def wipe_pyapp(self) -> None:
|
||||||
|
if self.pyapp is None:
|
||||||
|
return
|
||||||
|
subprocess.run([self.pyapp, "self", "remove"])
|
||||||
|
|
||||||
|
def uninstall(self) -> None:
|
||||||
|
for box in self.boxes:
|
||||||
|
self.wipe_conf_file(box)
|
||||||
|
if self.steam_box.get_active():
|
||||||
|
self.wipe_steam_shortcut()
|
||||||
|
self.wipe_pyapp()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UninstallWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||||
|
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||||
|
super().__init__()
|
||||||
|
config = Path(paths["XDG_CONFIG_HOME"])
|
||||||
|
if not config.is_dir() or not config.joinpath("config.json").is_file():
|
||||||
|
print(uninstall.not_installed)
|
||||||
|
return
|
||||||
|
GLib.set_prgname(APP_NAME)
|
||||||
|
self.win = Window(is_deck, paths)
|
||||||
|
Gtk.main()
|
||||||
@ -29,6 +29,7 @@ from dzgui.util.css import add_class, load_css
|
|||||||
from dzgui.views.components.buttons import WebButton
|
from dzgui.views.components.buttons import WebButton
|
||||||
from dzgui.views.components.entry import APIEntry
|
from dzgui.views.components.entry import APIEntry
|
||||||
from dzgui.views.components.misc import ClientCombo
|
from dzgui.views.components.misc import ClientCombo
|
||||||
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ class PageNum(Enum):
|
|||||||
class OptionalPageMixin:
|
class OptionalPageMixin:
|
||||||
"""Marks optional pages as advanceable"""
|
"""Marks optional pages as advanceable"""
|
||||||
|
|
||||||
def _on_map(self, page: "ScrolledWizardPage") -> None:
|
def _on_map(self, page: "EnumeratedWizardPage") -> None:
|
||||||
EMITTER.emit("step_complete")
|
EMITTER.emit("step_complete")
|
||||||
|
|
||||||
|
|
||||||
@ -72,10 +73,9 @@ class Progress(Gtk.ProgressBar):
|
|||||||
|
|
||||||
|
|
||||||
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
||||||
def __init__(self, enum: PageNum, heading: str, description: str):
|
def __init__(self, heading: str, description: str):
|
||||||
super().__init__(overlay_scrolling=False)
|
super().__init__(overlay_scrolling=False)
|
||||||
|
|
||||||
self.enum = enum
|
|
||||||
self.page_type: Gtk.AssistantPageType
|
self.page_type: Gtk.AssistantPageType
|
||||||
self.title = heading
|
self.title = heading
|
||||||
self.heading = Heading(heading)
|
self.heading = Heading(heading)
|
||||||
@ -88,7 +88,6 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
|||||||
height=600,
|
height=600,
|
||||||
preserve_aspect_ratio=True,
|
preserve_aspect_ratio=True,
|
||||||
)
|
)
|
||||||
image = Gtk.Image.new_from_pixbuf(pixbuf)
|
|
||||||
|
|
||||||
self.box = Gtk.Box(
|
self.box = Gtk.Box(
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
@ -97,22 +96,13 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
|||||||
margin_top=50,
|
margin_top=50,
|
||||||
spacing=20,
|
spacing=20,
|
||||||
)
|
)
|
||||||
|
image = Gtk.Image.new_from_pixbuf(pixbuf)
|
||||||
|
self.box.pack_start(image, expand=False, fill=True, padding=0)
|
||||||
|
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
self.prog = Progress()
|
|
||||||
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
|
||||||
self.box.pack_start(image, expand=False, fill=True, padding=0)
|
|
||||||
self.box.pack_start(self.heading, expand=False, fill=True, padding=0)
|
self.box.pack_start(self.heading, expand=False, fill=True, padding=0)
|
||||||
self.box.pack_start(self.description, expand=False, fill=True, padding=0)
|
self.box.pack_start(self.description, expand=False, fill=True, padding=0)
|
||||||
|
|
||||||
self.connect("map", self._on_map)
|
|
||||||
|
|
||||||
def get_enum(self) -> PageNum:
|
|
||||||
return self.enum
|
|
||||||
|
|
||||||
def get_progress_bar(self) -> Progress:
|
|
||||||
return self.prog
|
|
||||||
|
|
||||||
def get_page_type(self) -> Gtk.AssistantPageType:
|
def get_page_type(self) -> Gtk.AssistantPageType:
|
||||||
return self.page_type
|
return self.page_type
|
||||||
|
|
||||||
@ -131,8 +121,20 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
|||||||
def get_box(self) -> Gtk.Box:
|
def get_box(self) -> Gtk.Box:
|
||||||
return self.box
|
return self.box
|
||||||
|
|
||||||
def _on_map(self, page: "ScrolledWizardPage") -> None:
|
|
||||||
pass
|
class EnumeratedWizardPage(ScrolledWizardPage):
|
||||||
|
def __init__(self, enum: PageNum, heading: str, description: str) -> None:
|
||||||
|
super().__init__(heading=heading, description=description)
|
||||||
|
|
||||||
|
self.enum = enum
|
||||||
|
self.prog = Progress()
|
||||||
|
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
||||||
|
|
||||||
|
def get_enum(self) -> PageNum:
|
||||||
|
return self.enum
|
||||||
|
|
||||||
|
def get_progress_bar(self) -> Progress:
|
||||||
|
return self.prog
|
||||||
|
|
||||||
|
|
||||||
class NotificationFrame(Gtk.Frame):
|
class NotificationFrame(Gtk.Frame):
|
||||||
@ -162,7 +164,7 @@ class NotificationFrame(Gtk.Frame):
|
|||||||
self.label.set_markup(wrapped)
|
self.label.set_markup(wrapped)
|
||||||
|
|
||||||
|
|
||||||
class APIValidationPage(ScrolledWizardPage):
|
class APIValidationPage(EnumeratedWizardPage):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, enum: PageNum, heading: str, description: str, link: str, func: Callable
|
self, enum: PageNum, heading: str, description: str, link: str, func: Callable
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -239,7 +241,7 @@ class SteamValidationPage(APIValidationPage):
|
|||||||
self.thread_man.set_cleanup_func(cleanup)
|
self.thread_man.set_cleanup_func(cleanup)
|
||||||
|
|
||||||
|
|
||||||
class IntroductionPage(ScrolledWizardPage):
|
class IntroductionPage(EnumeratedWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.INTRO,
|
enum=PageNum.INTRO,
|
||||||
@ -294,7 +296,7 @@ class RadioFrame(Gtk.Frame):
|
|||||||
return self.button
|
return self.button
|
||||||
|
|
||||||
|
|
||||||
class ConfigMigrationPage(ScrolledWizardPage):
|
class ConfigMigrationPage(EnumeratedWizardPage):
|
||||||
def __init__(self, config: Path) -> None:
|
def __init__(self, config: Path) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.HAS_CONFIG,
|
enum=PageNum.HAS_CONFIG,
|
||||||
@ -354,7 +356,7 @@ class ConfigMigrationPage(ScrolledWizardPage):
|
|||||||
EMITTER.emit("config", True)
|
EMITTER.emit("config", True)
|
||||||
|
|
||||||
|
|
||||||
class PreferencesPage(ScrolledWizardPage):
|
class PreferencesPage(EnumeratedWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.USER_PREFS,
|
enum=PageNum.USER_PREFS,
|
||||||
@ -414,7 +416,7 @@ class PreferencesPage(ScrolledWizardPage):
|
|||||||
EMITTER.emit("step_complete")
|
EMITTER.emit("step_complete")
|
||||||
|
|
||||||
|
|
||||||
class CompletionPage(ScrolledWizardPage):
|
class CompletionPage(EnumeratedWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.FINAL,
|
enum=PageNum.FINAL,
|
||||||
@ -539,7 +541,9 @@ class Assistant(Gtk.Assistant):
|
|||||||
return
|
return
|
||||||
self.set_page_complete(page, True)
|
self.set_page_complete(page, True)
|
||||||
|
|
||||||
def _add_page(self, page: ScrolledWizardPage, ptype: Gtk.AssistantPageType) -> None:
|
def _add_page(
|
||||||
|
self, page: EnumeratedWizardPage, ptype: Gtk.AssistantPageType
|
||||||
|
) -> None:
|
||||||
self.append_page(page)
|
self.append_page(page)
|
||||||
self.set_page_type(page, ptype)
|
self.set_page_type(page, ptype)
|
||||||
self.set_page_title(page, page.get_title())
|
self.set_page_title(page, page.get_title())
|
||||||
@ -548,7 +552,7 @@ class Assistant(Gtk.Assistant):
|
|||||||
def _set_config_state(self, emitter: "Emitter", state: bool) -> None:
|
def _set_config_state(self, emitter: "Emitter", state: bool) -> None:
|
||||||
self.config = state
|
self.config = state
|
||||||
|
|
||||||
def _on_page_prepare(self: Self, wizard: Self, page: ScrolledWizardPage) -> None:
|
def _on_page_prepare(self: Self, wizard: Self, page: EnumeratedWizardPage) -> None:
|
||||||
page_num = self.get_current_page() + 1
|
page_num = self.get_current_page() + 1
|
||||||
total = self.get_n_pages()
|
total = self.get_n_pages()
|
||||||
fraction = page_num / total
|
fraction = page_num / total
|
||||||
@ -565,15 +569,21 @@ class CheckboxWithLabel(Gtk.Box):
|
|||||||
def __init__(self, text: str, blurb_text: str) -> None:
|
def __init__(self, text: str, blurb_text: str) -> None:
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||||
|
|
||||||
|
self.indent = 20
|
||||||
self.button = Gtk.CheckButton(label=text)
|
self.button = Gtk.CheckButton(label=text)
|
||||||
self.button.set_active(True)
|
self.button.set_active(True)
|
||||||
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=20)
|
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=self.indent)
|
||||||
|
|
||||||
wrapped = textwrap.fill(blurb_text, width=100)
|
wrapped = textwrap.fill(blurb_text, width=100)
|
||||||
label.set_markup(f"- {wrapped}")
|
label.set_markup(f"- {wrapped}")
|
||||||
|
|
||||||
for el in self.button, label:
|
for el in self.button, label:
|
||||||
self.add(el)
|
self.add(el)
|
||||||
|
|
||||||
|
def indent_below(self, widget: Gtk.Widget) -> None:
|
||||||
|
widget.set_margin_start(self.indent)
|
||||||
|
self.add(widget)
|
||||||
|
|
||||||
def get_checkbox(self) -> Gtk.CheckButton:
|
def get_checkbox(self) -> Gtk.CheckButton:
|
||||||
return self.button
|
return self.button
|
||||||
|
|
||||||
@ -584,7 +594,7 @@ class CheckboxWithLabel(Gtk.Box):
|
|||||||
self.button.set_active(state)
|
self.button.set_active(state)
|
||||||
|
|
||||||
|
|
||||||
class ShortcutCreationPage(OptionalPageMixin, ScrolledWizardPage): # type: ignore
|
class ShortcutCreationPage(OptionalPageMixin, EnumeratedWizardPage): # type: ignore
|
||||||
def __init__(self, shortcut: Path) -> None:
|
def __init__(self, shortcut: Path) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.SHORTCUTS,
|
enum=PageNum.SHORTCUTS,
|
||||||
@ -642,7 +652,7 @@ class ShortcutCreationPage(OptionalPageMixin, ScrolledWizardPage): # type: igno
|
|||||||
freedesktop.write_desktop_shortcut(desktop_file)
|
freedesktop.write_desktop_shortcut(desktop_file)
|
||||||
|
|
||||||
|
|
||||||
class SteamPathPage(ScrolledWizardPage):
|
class SteamPathPage(EnumeratedWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.USER_PREFS,
|
enum=PageNum.USER_PREFS,
|
||||||
@ -697,7 +707,7 @@ class SteamPathPage(ScrolledWizardPage):
|
|||||||
self.err_box.set_visible(False)
|
self.err_box.set_visible(False)
|
||||||
|
|
||||||
|
|
||||||
class SetupWizard(Gtk.Application):
|
class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||||
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
GLib.set_prgname(APP_NAME)
|
GLib.set_prgname(APP_NAME)
|
||||||
|
|||||||
37
dzgui/views/mixins/colorscheme.py
Normal file
37
dzgui/views/mixins/colorscheme.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from dzgui.const.constants import (
|
||||||
|
GIO_SETTINGS_KEY,
|
||||||
|
GIO_SETTINGS_INTERFACE,
|
||||||
|
GIO_SETTINGS_PROP,
|
||||||
|
GIO_SETTINGS_VAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk, Gio # noqa
|
||||||
|
|
||||||
|
|
||||||
|
class ColorAwareApp:
|
||||||
|
"""
|
||||||
|
Polls the local theme's color scheme settings.
|
||||||
|
Intended to be consumed by Gtk.Applications (or ephemeral widgets like EarlyAlertDialogs)
|
||||||
|
on initialization, prior to being realized. Applying scheme changes too late in the chain
|
||||||
|
may cause a blinking effect.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.interface_settings = Gio.Settings.new(GIO_SETTINGS_INTERFACE)
|
||||||
|
self.interface_settings.connect(
|
||||||
|
"changed::color-scheme", self._on_color_scheme_changed
|
||||||
|
)
|
||||||
|
self.default_settings = Gtk.Settings.get_default()
|
||||||
|
self._on_color_scheme_changed(self.interface_settings, GIO_SETTINGS_PROP)
|
||||||
|
|
||||||
|
def _on_color_scheme_changed(self, settings: Gio.Settings, prop: str) -> None:
|
||||||
|
state = self.interface_settings.get_string(prop) == GIO_SETTINGS_VAL
|
||||||
|
if self.default_settings is None:
|
||||||
|
return
|
||||||
|
self.default_settings.set_property(GIO_SETTINGS_KEY, state)
|
||||||
@ -104,20 +104,11 @@ class ContextMixin(TreeView):
|
|||||||
return False
|
return False
|
||||||
menu = self.context_menu
|
menu = self.context_menu
|
||||||
children = menu.get_children()
|
children = menu.get_children()
|
||||||
sel = menu.get_selected_item()
|
|
||||||
for i, child in enumerate(children):
|
|
||||||
if sel is child:
|
|
||||||
ind = i
|
|
||||||
break
|
|
||||||
|
|
||||||
match event.keyval:
|
match event.keyval:
|
||||||
case Gdk.KEY_j:
|
case Gdk.KEY_j:
|
||||||
if ind + 1 > len(children) - 1:
|
|
||||||
return False
|
|
||||||
menu.emit("move-current", Gtk.MenuDirectionType.NEXT)
|
menu.emit("move-current", Gtk.MenuDirectionType.NEXT)
|
||||||
case Gdk.KEY_k:
|
case Gdk.KEY_k:
|
||||||
if ind - 1 < 0:
|
|
||||||
return False
|
|
||||||
menu.emit("move-current", Gtk.MenuDirectionType.PREV)
|
menu.emit("move-current", Gtk.MenuDirectionType.PREV)
|
||||||
case Gdk.KEY_g:
|
case Gdk.KEY_g:
|
||||||
menu.select_item(children[0])
|
menu.select_item(children[0])
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH
|
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH
|
||||||
|
from dzgui.strings import changelog as clog
|
||||||
from dzgui.util.strings import missing_changelog
|
from dzgui.util.strings import missing_changelog
|
||||||
from dzgui.util.format import format_pango
|
from dzgui.util.format import format_pango
|
||||||
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
|
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
|
||||||
@ -31,20 +33,39 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
changelog = missing_changelog
|
changelog = missing_changelog
|
||||||
|
|
||||||
# TODO: should long text be wrapped?
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.box = Gtk.Box(
|
self.box = Gtk.Box(
|
||||||
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
|
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
|
||||||
)
|
)
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
|
|
||||||
|
expand_all = Gtk.Button(
|
||||||
|
label=clog.expand_all, halign=Gtk.Align.START, margin_start=20
|
||||||
|
)
|
||||||
|
expand_all.connect("clicked", self._on_expand_all_clicked)
|
||||||
|
self.box.add(expand_all)
|
||||||
|
|
||||||
|
self.expanded = False
|
||||||
|
self.expanders: list[Gtk.Expander] = []
|
||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
self.connect("key-press-event", self._on_esc_keypress)
|
self.connect("key-press-event", self._on_esc_keypress)
|
||||||
|
|
||||||
changes = self._parse(changelog)
|
changes = self._parse(changelog)
|
||||||
self._generate_nodes(changes)
|
self._generate_nodes(changes)
|
||||||
|
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
|
def _on_expand_all_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
self.expanded = not self.expanded
|
||||||
|
for expander in self.expanders:
|
||||||
|
# NOTE: simply setting set_expanded() does not trigger activate() signal,
|
||||||
|
# so margins are not applied
|
||||||
|
if expander.get_expanded() == self.expanded:
|
||||||
|
continue
|
||||||
|
expander.activate()
|
||||||
|
label = clog.collapse_all if self.expanded else clog.expand_all
|
||||||
|
button.set_label(label)
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
self.grab_focus()
|
self.grab_focus()
|
||||||
|
|
||||||
@ -65,9 +86,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
text = "\n".join(changes)
|
text = "\n".join(changes)
|
||||||
formatted = format_pango(text)
|
formatted = format_pango(text)
|
||||||
|
|
||||||
container = Gtk.Box(
|
container = Gtk.Box(valign=Gtk.Align.START, halign=Gtk.Align.START)
|
||||||
valign=Gtk.Align.START, halign=Gtk.Align.START
|
|
||||||
)
|
|
||||||
label = Gtk.Label()
|
label = Gtk.Label()
|
||||||
label.set_markup(formatted)
|
label.set_markup(formatted)
|
||||||
container.add(label)
|
container.add(label)
|
||||||
@ -81,6 +100,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
expander.add(container)
|
expander.add(container)
|
||||||
expander.connect("activate", self._on_expand, container)
|
expander.connect("activate", self._on_expand, container)
|
||||||
self.box.add(expander)
|
self.box.add(expander)
|
||||||
|
self.expanders.append(expander)
|
||||||
|
|
||||||
def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None:
|
def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None:
|
||||||
"""
|
"""
|
||||||
@ -94,6 +114,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
container.set_margin_bottom(15)
|
container.set_margin_bottom(15)
|
||||||
|
|
||||||
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
|
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
|
||||||
|
|
||||||
release = ""
|
release = ""
|
||||||
releases: list[tuple[str, list[str]]] = []
|
releases: list[tuple[str, list[str]]] = []
|
||||||
release_notes: list[str] = []
|
release_notes: list[str] = []
|
||||||
@ -107,5 +128,5 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
release = ""
|
release = ""
|
||||||
release = line
|
release = line
|
||||||
continue
|
continue
|
||||||
release_notes.append(line.rstrip())
|
release_notes.append(textwrap.fill(line.rstrip(), width=120))
|
||||||
return releases
|
return releases
|
||||||
|
|||||||
@ -9,7 +9,6 @@ from dzgui.const.constants import (
|
|||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
EDIT_DELETE,
|
EDIT_DELETE,
|
||||||
ERROR,
|
|
||||||
FOLDER,
|
FOLDER,
|
||||||
WARNING,
|
WARNING,
|
||||||
)
|
)
|
||||||
@ -21,6 +20,7 @@ from dzgui.views.components.buttons import Icon, IconTextButton
|
|||||||
from dzgui.views.components.eventbox import InfoEventBox
|
from dzgui.views.components.eventbox import InfoEventBox
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
||||||
|
from dzgui.views.components.misc import ErrorPopover
|
||||||
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
||||||
|
|
||||||
|
|
||||||
@ -57,19 +57,11 @@ class PageHeading(Gtk.Box):
|
|||||||
css.add_class(self, "page-heading")
|
css.add_class(self, "page-heading")
|
||||||
|
|
||||||
|
|
||||||
class ErrorPopover(Gtk.Popover):
|
class OfflineErrorPopover(ErrorPopover):
|
||||||
def __init__(self) -> None:
|
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
|
||||||
super().__init__(position=Gtk.PositionType.RIGHT)
|
super().__init__(relative_to=relative_to, position=position)
|
||||||
|
|
||||||
self.hbox = HBox()
|
def set_error_label(self, error: FolderError, msg: str) -> None:
|
||||||
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
|
||||||
error_icon = Icon(ERROR, margin_start=10)
|
|
||||||
self.hbox.extend([error_icon, self.label])
|
|
||||||
self.add(self.hbox)
|
|
||||||
self.show_all()
|
|
||||||
self.popdown()
|
|
||||||
|
|
||||||
def set_label(self, error: FolderError, msg: str) -> None:
|
|
||||||
match error:
|
match error:
|
||||||
case FolderError.NO_VALID_MODS:
|
case FolderError.NO_VALID_MODS:
|
||||||
prefix = offline.no_mods
|
prefix = offline.no_mods
|
||||||
@ -115,13 +107,10 @@ class FolderHBox(HBox):
|
|||||||
[self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button]
|
[self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.pop = ErrorPopover()
|
self.pop = OfflineErrorPopover(self.button, Gtk.PositionType.RIGHT)
|
||||||
self.pop.set_relative_to(self.button)
|
|
||||||
self.pop.connect("unmap", lambda _: self.grab_focus())
|
self.pop.connect("unmap", lambda _: self.grab_focus())
|
||||||
|
|
||||||
self.sidepop = ErrorPopover()
|
self.sidepop = OfflineErrorPopover(self.scrolled_label, Gtk.PositionType.BOTTOM)
|
||||||
self.sidepop.set_position(Gtk.PositionType.BOTTOM)
|
|
||||||
self.sidepop.set_relative_to(self.scrolled_label)
|
|
||||||
self.sidepop.connect("unmap", lambda _: self.grab_focus())
|
self.sidepop.connect("unmap", lambda _: self.grab_focus())
|
||||||
|
|
||||||
self.connect("map", self._on_map)
|
self.connect("map", self._on_map)
|
||||||
@ -170,13 +159,13 @@ class FolderHBox(HBox):
|
|||||||
|
|
||||||
def present_error(self, error: FolderError, msg: str) -> None:
|
def present_error(self, error: FolderError, msg: str) -> None:
|
||||||
if error == FolderError.FOLDER_CHANGED:
|
if error == FolderError.FOLDER_CHANGED:
|
||||||
self.sidepop.set_label(error, msg)
|
self.sidepop.set_error_label(error, msg)
|
||||||
self.sidepop.popup()
|
self.sidepop.popup()
|
||||||
return
|
return
|
||||||
self.folder = ""
|
self.folder = ""
|
||||||
self.label.set_text("")
|
self.label.set_text("")
|
||||||
self.unset_button.hide()
|
self.unset_button.hide()
|
||||||
self.pop.set_label(error, msg)
|
self.pop.set_error_label(error, msg)
|
||||||
self.pop.popup()
|
self.pop.popup()
|
||||||
|
|
||||||
|
|
||||||
@ -467,7 +456,6 @@ class OfflineLoader(Gtk.Box):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: share ConnectBox class with preconnect dialog?
|
|
||||||
self.button_box = HBox(spacing=5)
|
self.button_box = HBox(spacing=5)
|
||||||
self.button_box.set_halign(Gtk.Align.END)
|
self.button_box.set_halign(Gtk.Align.END)
|
||||||
self.button_box.set_margin_top(15)
|
self.button_box.set_margin_top(15)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import Callable, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api import pefile as PeFile
|
from dzgui.api import pefile as PeFile
|
||||||
|
|
||||||
@ -17,14 +17,15 @@ from dzgui.const.constants import (
|
|||||||
)
|
)
|
||||||
from dzgui.const.endpoints import STEAM_API_SETUP
|
from dzgui.const.endpoints import STEAM_API_SETUP
|
||||||
from dzgui.const.enum import Preferences, ServerTab
|
from dzgui.const.enum import Preferences, ServerTab
|
||||||
from dzgui.strings import errors, options
|
from dzgui.strings import options
|
||||||
from dzgui.util import strings, css, open_links
|
from dzgui.util import strings, css, open_links
|
||||||
|
|
||||||
from dzgui.views.components.box import VBox
|
from dzgui.views.components.box import ShortHBox, VBox
|
||||||
|
from dzgui.views.components.eventbox import InfoEventBox
|
||||||
from dzgui.views.components.labels import LeftLabel
|
from dzgui.views.components.labels import LeftLabel
|
||||||
from dzgui.views.components.buttons import WebButton
|
from dzgui.views.components.buttons import SpinnerButton, WebButton
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
from dzgui.views.components.misc import ClientCombo
|
from dzgui.views.components.misc import ClientCombo, ErrorPopover
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
||||||
|
|
||||||
@ -39,11 +40,214 @@ if TYPE_CHECKING:
|
|||||||
from dzgui.controllers.emitter import Emitter
|
from dzgui.controllers.emitter import Emitter
|
||||||
|
|
||||||
|
|
||||||
class ShortHBox(Gtk.Box):
|
class SubmitField(Gtk.Box):
|
||||||
def __init__(self, widget: Gtk.Widget) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller: "Controller",
|
||||||
|
placeholder: str,
|
||||||
|
context: Preferences,
|
||||||
|
slow: bool = False,
|
||||||
|
private: bool = True,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||||
|
|
||||||
|
self.callback: Callable
|
||||||
|
self.controller = controller
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
self.old_text: str
|
||||||
|
self.placeholder = placeholder
|
||||||
|
|
||||||
|
self.entry = Gtk.Entry(placeholder_text=placeholder, hexpand=True)
|
||||||
|
|
||||||
|
# TODO: audit for all applicable callbacks
|
||||||
|
if private:
|
||||||
|
self.entry.set_icon_from_icon_name(
|
||||||
|
Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL
|
||||||
|
)
|
||||||
|
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
||||||
|
self.entry.connect("icon-release", self._on_icon_release)
|
||||||
|
self.entry.set_visibility(False)
|
||||||
|
|
||||||
|
self.button: SpinnerButton | Gtk.Button
|
||||||
|
if slow:
|
||||||
|
self.button = SpinnerButton(label=options.save_button)
|
||||||
|
else:
|
||||||
|
self.button = Gtk.Button(label=options.save_button)
|
||||||
|
|
||||||
|
self.button.connect("clicked", self._on_save_clicked)
|
||||||
|
self.entry.connect("insert-text", self._on_text_typed)
|
||||||
|
self.entry.connect("activate", self._on_field_activated)
|
||||||
|
self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted)
|
||||||
|
|
||||||
|
self.add(self.entry)
|
||||||
|
self.add(self.button)
|
||||||
|
|
||||||
|
def set_text(self, text: str) -> None:
|
||||||
|
self.old_text = text
|
||||||
|
self.entry.set_text(text)
|
||||||
|
if len(text) == 0:
|
||||||
|
self.button.set_sensitive(False)
|
||||||
|
|
||||||
|
def _is_valid_text(self, text: str) -> bool:
|
||||||
|
if text.isspace():
|
||||||
|
return False
|
||||||
|
if len(text) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if text == self.old_text:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _on_text_deleted(
|
||||||
|
self,
|
||||||
|
buffer: Gtk.EntryBuffer,
|
||||||
|
position: int,
|
||||||
|
chars: int,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
text = buffer.get_text()
|
||||||
|
state = self._is_valid_text(text)
|
||||||
|
self.button.set_sensitive(state)
|
||||||
|
|
||||||
|
def _on_text_typed(
|
||||||
|
self,
|
||||||
|
entry: Gtk.Entry,
|
||||||
|
text: str,
|
||||||
|
length: int,
|
||||||
|
pos: int,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
buffer = entry.get_property("buffer")
|
||||||
|
text = buffer.get_text() + text
|
||||||
|
state = self._is_valid_text(text)
|
||||||
|
self.button.set_sensitive(state)
|
||||||
|
|
||||||
|
def _on_icon_release(
|
||||||
|
self,
|
||||||
|
widget: Gtk.Entry,
|
||||||
|
icon_pos: Gtk.EntryIconPosition,
|
||||||
|
event: Gdk.Event,
|
||||||
|
) -> None:
|
||||||
|
visible = widget.get_visibility()
|
||||||
|
if visible:
|
||||||
|
icon, state = VIEW_REVEAL, False
|
||||||
|
else:
|
||||||
|
icon, state = VIEW_CONCEAL, True
|
||||||
|
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||||
|
widget.set_visibility(state)
|
||||||
|
|
||||||
|
def _on_field_activated(self, entry: Gtk.Entry) -> None:
|
||||||
|
text = entry.get_text()
|
||||||
|
if not self._is_valid_text(text):
|
||||||
|
return
|
||||||
|
self.save_option()
|
||||||
|
|
||||||
|
def set_callback(self, callback: Callable) -> None:
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
def save_option(self) -> None:
|
||||||
|
self.callback()
|
||||||
|
self.button.set_sensitive(False)
|
||||||
|
|
||||||
|
def _on_save_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
self.save_option()
|
||||||
|
|
||||||
|
|
||||||
|
class SteamSubmitField(SubmitField):
|
||||||
|
def __init__(self, controller: "Controller") -> None:
|
||||||
|
super().__init__(
|
||||||
|
controller,
|
||||||
|
options.steam_placeholder,
|
||||||
|
Preferences.STEAM,
|
||||||
|
slow=True,
|
||||||
|
private=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.pop = ErrorPopover(
|
||||||
|
position=Gtk.PositionType.BOTTOM, relative_to=self.entry
|
||||||
|
)
|
||||||
|
self.pop.set_label(options.api_failed)
|
||||||
|
self.pop.show_all()
|
||||||
|
self.pop.popdown()
|
||||||
|
|
||||||
|
emitter = controller.get_emitter()
|
||||||
|
emitter.connect("api_change_failed", self._on_api_failure)
|
||||||
|
emitter.connect("api_change_successful", self._on_api_success)
|
||||||
|
|
||||||
|
self.set_callback(self.save_setting)
|
||||||
|
|
||||||
|
def save_setting(self) -> None:
|
||||||
|
self.entry.set_sensitive(False)
|
||||||
|
text = "".join(self.entry.get_text().split())
|
||||||
|
self.controller.update_steam_api_key(text)
|
||||||
|
|
||||||
|
def _on_api_success(self, emitter: "Emitter") -> None:
|
||||||
|
self.old_text = self.entry.get_text()
|
||||||
|
if isinstance(self.button, SpinnerButton):
|
||||||
|
self.button.stop_spinner()
|
||||||
|
self.entry.set_sensitive(True)
|
||||||
|
|
||||||
|
def _on_api_failure(self, emitter: "Emitter") -> None:
|
||||||
|
self.pop.popup()
|
||||||
|
if isinstance(self.button, SpinnerButton):
|
||||||
|
self.button.stop_spinner()
|
||||||
|
self.entry.set_sensitive(True)
|
||||||
|
|
||||||
|
def block_text_entry(self) -> None:
|
||||||
|
self.entry.set_position(-1)
|
||||||
|
self.entry.set_can_focus(False)
|
||||||
|
|
||||||
|
def unblock_text_entry(self) -> None:
|
||||||
|
self.entry.set_can_focus(True)
|
||||||
|
|
||||||
|
|
||||||
|
class NameSubmitField(SubmitField):
|
||||||
|
def __init__(self, controller: "Controller") -> None:
|
||||||
|
super().__init__(
|
||||||
|
controller,
|
||||||
|
options.name_placeholder,
|
||||||
|
Preferences.NAME,
|
||||||
|
private=False,
|
||||||
|
)
|
||||||
|
self.entry.set_width_chars(30) # type: ignore
|
||||||
|
self.set_callback(self.save_player_name)
|
||||||
|
|
||||||
|
def save_player_name(self) -> None:
|
||||||
|
value = self.entry.get_text().strip()
|
||||||
|
self.old_text = value
|
||||||
|
self.controller.update_config(self.context, value)
|
||||||
|
|
||||||
|
|
||||||
|
class ToggleField(Gtk.Box):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller: "Controller",
|
||||||
|
first_option: str,
|
||||||
|
second_option: str,
|
||||||
|
context: Preferences,
|
||||||
|
) -> None:
|
||||||
super().__init__(spacing=5, halign=Gtk.Align.START)
|
super().__init__(spacing=5, halign=Gtk.Align.START)
|
||||||
|
|
||||||
self.pack_start(widget, NO_EXPAND, NO_FILL, NO_PADDING)
|
self.controller = controller
|
||||||
|
self.context = context
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
|
||||||
|
self.controller.toggle_config(context)
|
||||||
|
|
||||||
|
def set_sensitive(self, state: bool) -> None:
|
||||||
|
for el in self.radio1, self.radio2:
|
||||||
|
el.set_sensitive(state)
|
||||||
|
|
||||||
|
|
||||||
class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||||
@ -55,32 +259,27 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.controller.register_widget("options", self)
|
self.controller.register_widget("options", self)
|
||||||
emitter = controller.get_emitter()
|
|
||||||
emitter.connect("api_change_failed", self._on_api_change_failed)
|
|
||||||
|
|
||||||
self.DEFAULT_WIDTH = 1
|
self.DEFAULT_WIDTH = 1
|
||||||
self.DEFAULT_HEIGHT = 1
|
self.DEFAULT_HEIGHT = 1
|
||||||
|
|
||||||
self.steam_entry: Gtk.Entry
|
self.steam_entry: Gtk.Entry
|
||||||
|
self.pop: Gtk.Popover
|
||||||
|
|
||||||
self.steam = WebButton(label=strings.options.steam_web)
|
self.steam = WebButton(label=strings.options.steam_web)
|
||||||
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
|
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
|
||||||
|
|
||||||
self.steam_box = self._make_submit_field(
|
self.steam_box = SteamSubmitField(controller)
|
||||||
strings.options.enter_steam, Preferences.STEAM, True
|
|
||||||
)
|
|
||||||
api_rows = [
|
api_rows = [
|
||||||
[LeftLabel(strings.options.steam_placeholder), self.steam_box, self.steam],
|
[LeftLabel(strings.options.steam_placeholder), self.steam_box, self.steam],
|
||||||
]
|
]
|
||||||
|
|
||||||
self.player_box = self._make_submit_field(
|
self.player_box = NameSubmitField(controller)
|
||||||
strings.options.name_placeholder, Preferences.NAME
|
|
||||||
)
|
|
||||||
self.player_box.set_halign(Gtk.Align.START)
|
self.player_box.set_halign(Gtk.Align.START)
|
||||||
# TODO: make submit field a standalone class
|
|
||||||
self.player_box.get_children()[0].set_width_chars(30) # type: ignore
|
self.player_box.get_children()[0].set_width_chars(30) # type: ignore
|
||||||
|
|
||||||
self.fullscreen_toggle = self.make_binary_radio(
|
self.fullscreen_toggle = ToggleField(
|
||||||
|
self.controller,
|
||||||
strings.options.last_used,
|
strings.options.last_used,
|
||||||
strings.options.always_fs,
|
strings.options.always_fs,
|
||||||
Preferences.WINDOW,
|
Preferences.WINDOW,
|
||||||
@ -91,8 +290,8 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
|
|
||||||
client_hbox = ShortHBox(self.client_combo)
|
client_hbox = ShortHBox(self.client_combo)
|
||||||
|
|
||||||
self.distance_toggle = self.make_binary_radio(
|
self.distance_toggle = ToggleField(
|
||||||
strings.options.km, strings.options.mi, Preferences.DIST
|
self.controller, strings.options.km, strings.options.mi, Preferences.DIST
|
||||||
)
|
)
|
||||||
|
|
||||||
combo_store = Gtk.ListStore(str, object)
|
combo_store = Gtk.ListStore(str, object)
|
||||||
@ -178,11 +377,13 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
return str(model[ind][0])
|
return str(model[ind][0])
|
||||||
|
|
||||||
def block_text_entry(self) -> None:
|
def block_text_entry(self) -> None:
|
||||||
self.steam_entry.set_position(-1)
|
self.steam_box.block_text_entry()
|
||||||
self.steam_entry.set_can_focus(False)
|
# self.steam_entry.set_position(-1)
|
||||||
|
# self.steam_entry.set_can_focus(False)
|
||||||
|
|
||||||
def unblock_text_entry(self) -> None:
|
def unblock_text_entry(self) -> None:
|
||||||
self.steam_entry.set_can_focus(True)
|
self.steam_box.unblock_text_entry()
|
||||||
|
# self.steam_entry.set_can_focus(True)
|
||||||
|
|
||||||
def _on_developers_clicked(self, button: Gtk.Button) -> None:
|
def _on_developers_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.show_developers_page()
|
self.controller.show_developers_page()
|
||||||
@ -190,46 +391,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None:
|
def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None:
|
||||||
open_links.open_link_by_url(url)
|
open_links.open_link_by_url(url)
|
||||||
|
|
||||||
def _make_submit_field(
|
|
||||||
self,
|
|
||||||
placeholder: str,
|
|
||||||
context: Preferences,
|
|
||||||
private: bool = False,
|
|
||||||
) -> Gtk.Box:
|
|
||||||
|
|
||||||
entry = Gtk.Entry(placeholder_text=placeholder, hexpand=True)
|
|
||||||
button = Gtk.Button(label="Save")
|
|
||||||
|
|
||||||
button.connect("clicked", self._on_save_clicked, entry, context)
|
|
||||||
entry.connect("insert-text", self._on_text_typed, context, button)
|
|
||||||
entry.connect("activate", self._on_field_activated, context, button)
|
|
||||||
entry.get_property("buffer").connect(
|
|
||||||
"deleted-text", self._on_text_deleted, context, button
|
|
||||||
)
|
|
||||||
|
|
||||||
if private:
|
|
||||||
entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL)
|
|
||||||
entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
|
||||||
entry.connect("icon-release", self._on_icon_release)
|
|
||||||
entry.set_visibility(False)
|
|
||||||
|
|
||||||
if context == Preferences.STEAM:
|
|
||||||
self.steam_entry = entry
|
|
||||||
|
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
|
||||||
box.add(entry)
|
|
||||||
box.add(button)
|
|
||||||
|
|
||||||
return box
|
|
||||||
|
|
||||||
def _on_field_activated(
|
|
||||||
self, entry: Gtk.Entry, context: Preferences, button: Gtk.Button
|
|
||||||
) -> None:
|
|
||||||
text = entry.get_text()
|
|
||||||
if not self._is_valid_text(text, context):
|
|
||||||
return
|
|
||||||
self._on_save_clicked(button, entry, context)
|
|
||||||
|
|
||||||
def _make_grid(self, rows: list) -> Gtk.Grid:
|
def _make_grid(self, rows: list) -> Gtk.Grid:
|
||||||
grid = Gtk.Grid(
|
grid = Gtk.Grid(
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
@ -249,35 +410,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
row += 1
|
row += 1
|
||||||
return grid
|
return grid
|
||||||
|
|
||||||
def _on_save_clicked(
|
|
||||||
self, button: Gtk.Button, entry: Gtk.Entry, enum: Preferences
|
|
||||||
) -> None:
|
|
||||||
old_text = self.controller.query_config(enum)
|
|
||||||
self.old_text = old_text
|
|
||||||
self.old_entry = entry
|
|
||||||
|
|
||||||
button.set_sensitive(False)
|
|
||||||
match enum:
|
|
||||||
case Preferences.NAME:
|
|
||||||
value = entry.get_text().strip()
|
|
||||||
self.controller.update_config(enum, value)
|
|
||||||
case Preferences.STEAM:
|
|
||||||
text = "".join(entry.get_text().split())
|
|
||||||
self.controller.update_api_key(enum, text)
|
|
||||||
|
|
||||||
def _on_api_change_failed(self, emitter: "Emitter") -> None:
|
|
||||||
self.old_entry.set_text(self.old_text)
|
|
||||||
# TODO: use popover
|
|
||||||
dialog = ExceptionDialog(self.controller, errors.api_validation_error)
|
|
||||||
dialog.run()
|
|
||||||
|
|
||||||
def restore_api_text(self, text: str, entry: Gtk.Entry) -> None:
|
|
||||||
entry.set_text(text)
|
|
||||||
|
|
||||||
def revert(self, mode: Preferences) -> None:
|
|
||||||
if mode == Preferences.STEAM:
|
|
||||||
self.steam_entry.set_text(self.old_steam)
|
|
||||||
|
|
||||||
def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
|
def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
|
||||||
_iter = combo.get_active_iter()
|
_iter = combo.get_active_iter()
|
||||||
if _iter is None:
|
if _iter is None:
|
||||||
@ -293,79 +425,12 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
real_cmd = combo.get_model()[_iter][1]
|
real_cmd = combo.get_model()[_iter][1]
|
||||||
self.controller.update_config(Preferences.CLIENT, real_cmd)
|
self.controller.update_config(Preferences.CLIENT, real_cmd)
|
||||||
|
|
||||||
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
|
|
||||||
try:
|
|
||||||
self.controller.toggle_config(context)
|
|
||||||
except Exception:
|
|
||||||
button.handler_block_by_func(self._on_radio_toggled)
|
|
||||||
self.populate_settings()
|
|
||||||
button.handler_unblock_by_func(self._on_radio_toggled)
|
|
||||||
|
|
||||||
def _is_valid_text(self, text: str, context: Preferences) -> bool:
|
|
||||||
if text.isspace():
|
|
||||||
return False
|
|
||||||
if len(text) == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
match context:
|
|
||||||
case Preferences.NAME:
|
|
||||||
old = self.old_name
|
|
||||||
case Preferences.STEAM:
|
|
||||||
old = self.old_steam
|
|
||||||
if text == old:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _on_text_deleted(
|
|
||||||
self,
|
|
||||||
buffer: Gtk.EntryBuffer,
|
|
||||||
position: int,
|
|
||||||
chars: int,
|
|
||||||
context: Preferences,
|
|
||||||
button: Gtk.Button,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
text = buffer.get_text()
|
|
||||||
state = self._is_valid_text(text, context)
|
|
||||||
button.set_sensitive(state)
|
|
||||||
|
|
||||||
def _on_text_typed(
|
|
||||||
self,
|
|
||||||
entry: Gtk.Entry,
|
|
||||||
text: str,
|
|
||||||
length: int,
|
|
||||||
pos: int,
|
|
||||||
context: Preferences,
|
|
||||||
button: Gtk.Button,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
buffer = entry.get_property("buffer")
|
|
||||||
text = buffer.get_text() + text
|
|
||||||
state = self._is_valid_text(text, context)
|
|
||||||
button.set_sensitive(state)
|
|
||||||
|
|
||||||
def make_binary_radio(
|
|
||||||
self,
|
|
||||||
first_option: str,
|
|
||||||
second_option: str,
|
|
||||||
context: Preferences,
|
|
||||||
) -> Gtk.Box:
|
|
||||||
|
|
||||||
hbox = Gtk.Box(spacing=5, halign=Gtk.Align.START)
|
|
||||||
radio1 = Gtk.RadioButton.new_with_label(None, first_option)
|
|
||||||
radio2 = Gtk.RadioButton.new_from_widget(radio1)
|
|
||||||
radio2.set_label(second_option)
|
|
||||||
radio1.connect("toggled", self._on_radio_toggled, context)
|
|
||||||
hbox.pack_start(radio1, NO_EXPAND, NO_FILL, NO_PADDING)
|
|
||||||
hbox.pack_start(radio2, NO_EXPAND, NO_FILL, NO_PADDING)
|
|
||||||
|
|
||||||
return hbox
|
|
||||||
|
|
||||||
def populate_settings(self) -> None:
|
def populate_settings(self) -> None:
|
||||||
prefs = self.controller.get_prefs()
|
prefs = self.controller.get_prefs()
|
||||||
# NOTE: re-check in case file was removed by user between runs
|
# NOTE: re-check in case file was removed by user between runs
|
||||||
if prefs.paths.config.is_file() is False:
|
if prefs.paths.config.is_file() is False:
|
||||||
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
||||||
|
|
||||||
@ -378,30 +443,21 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
|
|
||||||
steam_path = Path(default_steam_path)
|
steam_path = Path(default_steam_path)
|
||||||
|
|
||||||
self.old_steam = steam
|
self.steam_box.set_text(steam)
|
||||||
self.old_name = name
|
self.player_box.set_text(name)
|
||||||
|
|
||||||
self.steam_entry.set_text(steam)
|
fs = config["fullscreen"]
|
||||||
p = self.player_box.get_children()[0]
|
miles = config["use_miles"]
|
||||||
if hasattr(p, "set_text"):
|
|
||||||
p.set_text(name)
|
|
||||||
|
|
||||||
# NOTE: suppress toggle signal until radios are built
|
if prefs.is_steam_deck:
|
||||||
self._suppress_toggles(True)
|
self.fullscreen_toggle.set_sensitive(False)
|
||||||
for el, conf_state in [
|
eb = InfoEventBox(options.fullscreen_eventbox, self.controller)
|
||||||
(self.fullscreen_toggle, config["fullscreen"]),
|
self.fullscreen_toggle.add(eb)
|
||||||
(self.distance_toggle, config["use_miles"]),
|
|
||||||
]:
|
|
||||||
el.get_children()[conf_state].set_active(True)
|
|
||||||
self._suppress_toggles(False)
|
|
||||||
|
|
||||||
# NOTE: disable buttons if no text is set
|
fs = True
|
||||||
for field in (
|
|
||||||
[name, self.player_box],
|
self.fullscreen_toggle.set_suboption_active(fs)
|
||||||
[steam, self.steam_box],
|
self.distance_toggle.set_suboption_active(miles)
|
||||||
):
|
|
||||||
if field[0] == "":
|
|
||||||
field[1].get_children()[1].set_sensitive(False)
|
|
||||||
|
|
||||||
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
||||||
if dayz_version is None:
|
if dayz_version is None:
|
||||||
@ -430,19 +486,5 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
self, toggle.get_children()[0], "_on_radio_toggled", state
|
self, toggle.get_children()[0], "_on_radio_toggled", state
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_icon_release(
|
|
||||||
self,
|
|
||||||
widget: Gtk.Entry,
|
|
||||||
icon_pos: Gtk.EntryIconPosition,
|
|
||||||
event: Gdk.Event,
|
|
||||||
) -> None:
|
|
||||||
visible = widget.get_visibility()
|
|
||||||
if visible:
|
|
||||||
icon, state = VIEW_REVEAL, False
|
|
||||||
else:
|
|
||||||
icon, state = VIEW_CONCEAL, True
|
|
||||||
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
|
||||||
widget.set_visibility(state)
|
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
self.grab_focus()
|
self.grab_focus()
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from dzgui.util.keys import is_ctrl_mask
|
|||||||
from dzgui.util.localize import number
|
from dzgui.util.localize import number
|
||||||
from dzgui.strings.server_mods import checkmark
|
from dzgui.strings.server_mods import checkmark
|
||||||
from dzgui.strings import preconnect
|
from dzgui.strings import preconnect
|
||||||
|
from dzgui.views.dialogs.generic import DebugDialog
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
||||||
|
|
||||||
@ -120,6 +121,12 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
spacing=5,
|
spacing=5,
|
||||||
)
|
)
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||||
|
|
||||||
|
# NOTE: -d flag
|
||||||
|
if self.controller.get_prefs().is_debug:
|
||||||
|
debug = Gtk.Button(label=preconnect.debug, halign=Gtk.Align.START)
|
||||||
|
debug.connect("clicked", self._on_debug_clicked)
|
||||||
|
box.add(debug)
|
||||||
for button in self.back, self.ok, self.connect_last:
|
for button in self.back, self.ok, self.connect_last:
|
||||||
box.add(button)
|
box.add(button)
|
||||||
self.button_box.add(box)
|
self.button_box.add(box)
|
||||||
@ -150,8 +157,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
self.tree_box.add(self.scrolled)
|
self.tree_box.add(self.scrolled)
|
||||||
self.tree_box.add(self.progress_box)
|
self.tree_box.add(self.progress_box)
|
||||||
|
|
||||||
# TODO: strings
|
self.mods_placeholder = Placeholder(preconnect.placeholder_no_mods)
|
||||||
self.mods_placeholder = Placeholder("This server has no mods.")
|
|
||||||
self.tree_box.add(self.mods_placeholder)
|
self.tree_box.add(self.mods_placeholder)
|
||||||
|
|
||||||
self.tree_frame = HeadingFrame.new_with_widget_and_label(
|
self.tree_frame = HeadingFrame.new_with_widget_and_label(
|
||||||
@ -161,8 +167,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
# TODO: abstract into components
|
# TODO: abstract into components
|
||||||
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
self.warning_tree = MaskedTree(WARNING)
|
self.warning_tree = MaskedTree(WARNING)
|
||||||
# TODO: strings
|
self.warning_placeholder = Placeholder(preconnect.placeholder_no_warnings)
|
||||||
self.warning_placeholder = Placeholder("No warnings.")
|
|
||||||
self.warning_box.add(self.warning_tree)
|
self.warning_box.add(self.warning_tree)
|
||||||
self.warning_box.add(self.warning_placeholder)
|
self.warning_box.add(self.warning_placeholder)
|
||||||
self.warning_frame = HeadingFrame.new_with_widget_and_label(
|
self.warning_frame = HeadingFrame.new_with_widget_and_label(
|
||||||
@ -223,6 +228,12 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
|
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.update_and_load_to_menu()
|
self.controller.update_and_load_to_menu()
|
||||||
|
|
||||||
|
def _on_debug_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
args = self.controller.get_debug_args()
|
||||||
|
d = DebugDialog(self.controller, args)
|
||||||
|
d.show_all()
|
||||||
|
d.run()
|
||||||
|
|
||||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.update_and_connect()
|
self.controller.update_and_connect()
|
||||||
|
|
||||||
@ -233,61 +244,46 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
|
|
||||||
resync_msg = (
|
resync_msg = preconnect.resync.format(prereqs.build)
|
||||||
f"If you recently installed {prereqs.build} or moved it to a different drive,\n"
|
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
"""Errors"""
|
"""Errors"""
|
||||||
if len(prereqs.invalid_mods) > 0:
|
if len(prereqs.invalid_mods) > 0:
|
||||||
pairs = [": ".join(sub) for sub in prereqs.invalid_mods]
|
pairs = [": ".join(sub) for sub in prereqs.invalid_mods]
|
||||||
lines = "\n".join(pairs)
|
lines = "\n".join(pairs)
|
||||||
msg = (
|
msg = preconnect.invalid_mods.format(lines)
|
||||||
"Server has invalid mods that are not recognized by Steam.\n"
|
|
||||||
"Contact the server owner and include these mod IDs in your report:\n"
|
|
||||||
f"{lines}"
|
|
||||||
)
|
|
||||||
errors.append(msg)
|
errors.append(msg)
|
||||||
if prereqs.binary_missing:
|
if prereqs.binary_missing:
|
||||||
errors.append(
|
msg = preconnect.version_missing.format(prereqs.build)
|
||||||
f"Remote server is running the build '{prereqs.build}', but it is not installed.\n{resync_msg}"
|
msg += f"\n{resync_msg}"
|
||||||
)
|
errors.append(msg)
|
||||||
elif prereqs.local_version != prereqs.remote_version:
|
elif prereqs.local_version != prereqs.remote_version:
|
||||||
errors.append(
|
msg = preconnect.version_mismatch.format(
|
||||||
f"Local client version '{prereqs.local_version}' does not match remote version '{prereqs.remote_version}'.\n{resync_msg}"
|
prereqs.local_version, prereqs.remote_version
|
||||||
)
|
)
|
||||||
|
msg += f"\n{resync_msg}"
|
||||||
|
errors.append(msg)
|
||||||
if prereqs.required_space > prereqs.available_space:
|
if prereqs.required_space > prereqs.available_space:
|
||||||
required_pretty = number(prereqs.required_space)
|
required_pretty = number(prereqs.required_space)
|
||||||
available_pretty = number(prereqs.available_space)
|
available_pretty = number(prereqs.available_space)
|
||||||
errors.append(
|
msg = preconnect.not_enough_space.format(required_pretty, available_pretty)
|
||||||
f"Need to update {required_pretty} MiB of mods, but installation path only has {available_pretty} MiB."
|
errors.append(msg)
|
||||||
)
|
|
||||||
if len(prereqs.mods) > 0 and prereqs.game_mode:
|
if len(prereqs.mods) > 0 and prereqs.game_mode:
|
||||||
errors.append("Use Desktop Mode to download mods on Steam Deck")
|
errors.append(preconnect.use_desktop_mode)
|
||||||
|
|
||||||
if prereqs.steam_proc.is_running is False:
|
if prereqs.steam_proc.is_running is False:
|
||||||
client = prereqs.steam_proc.name
|
client = prereqs.steam_proc.name
|
||||||
errors.append(
|
msg = preconnect.steam_not_running.format(client)
|
||||||
f"'{client}' is set as the default Steam client, but it is either not installed or not running."
|
errors.append(msg)
|
||||||
)
|
|
||||||
|
|
||||||
"""Warnings"""
|
"""Warnings"""
|
||||||
if prereqs.passworded:
|
if prereqs.passworded:
|
||||||
warnings.append(
|
warnings.append(preconnect.protected_server)
|
||||||
"Protected: you will be prompted for a password when connecting to this server."
|
|
||||||
)
|
|
||||||
if prereqs.dayz_running is True:
|
if prereqs.dayz_running is True:
|
||||||
warnings.append(
|
warnings.append(preconnect.dayz_running)
|
||||||
"It looks like DayZ is already running in the background. Exit DayZ before connecting."
|
|
||||||
)
|
|
||||||
|
|
||||||
allows_dl, running_app = prereqs.allows_downloads
|
allows_dl, running_app = prereqs.allows_downloads
|
||||||
if len(prereqs.mods) > 0 and allows_dl is False:
|
if len(prereqs.mods) > 0 and allows_dl is False:
|
||||||
msg = (
|
msg = preconnect.running_app.format(running_app)
|
||||||
f"The app '{running_app}' is currently running in Steam, but background downloads are not enabled.\n"
|
|
||||||
"Either stop the game first, or update your global Steam settings or the game's local settings.\n"
|
|
||||||
"Otherwise, mods may be queued for download but never update."
|
|
||||||
)
|
|
||||||
warnings.append(msg)
|
warnings.append(msg)
|
||||||
|
|
||||||
self.add_warnings(warnings)
|
self.add_warnings(warnings)
|
||||||
@ -326,7 +322,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
|
|
||||||
if prereqs.required_space != 0:
|
if prereqs.required_space != 0:
|
||||||
pretty = number(prereqs.required_space)
|
pretty = number(prereqs.required_space)
|
||||||
suffix = f" Need to download {pretty} MiB of mod updates."
|
suffix = preconnect.required_space.format(pretty)
|
||||||
prefix = preconnect.total_mods
|
prefix = preconnect.total_mods
|
||||||
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
|
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -295,6 +295,12 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
) -> None:
|
) -> None:
|
||||||
if self.loaded is False:
|
if self.loaded is False:
|
||||||
return
|
return
|
||||||
|
# NOTE: signal triggers twice on right-click events
|
||||||
|
# due to deselect-then-select behavior in GTK
|
||||||
|
sel = self.get_selection()
|
||||||
|
model, row = sel.get_selected()
|
||||||
|
if row is None:
|
||||||
|
return
|
||||||
self.start_distcalc()
|
self.start_distcalc()
|
||||||
|
|
||||||
def get_name(self) -> str:
|
def get_name(self) -> str:
|
||||||
|
|||||||
@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b21"
|
version = "7.0.0b22"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@ -27,7 +27,7 @@ def fetch(url: str, destination: str) -> None:
|
|||||||
|
|
||||||
def get_cpython() -> None:
|
def get_cpython() -> None:
|
||||||
source = f"https://github.com/astral-sh/python-build-standalone/releases/download/{DATE}/"
|
source = f"https://github.com/astral-sh/python-build-standalone/releases/download/{DATE}/"
|
||||||
build = f"{CPYTHON_BUILD}%2B{DATE}-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz"
|
build = f"{CPYTHON_BUILD}%2B{DATE}-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
url = f"{source}{build}"
|
url = f"{source}{build}"
|
||||||
outfile = unquote(build)
|
outfile = unquote(build)
|
||||||
fetch(url, outfile)
|
fetch(url, outfile)
|
||||||
|
|||||||
38
scripts/unused_imports.py
Normal file
38
scripts/unused_imports.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def iterate(search_str: str) -> None:
|
||||||
|
# TODO: exclude files from .gitignore
|
||||||
|
print()
|
||||||
|
print(f"Searching for '{search_str}'")
|
||||||
|
print()
|
||||||
|
files = Path(".").rglob("*")
|
||||||
|
ignore = [
|
||||||
|
"scripts/",
|
||||||
|
".git",
|
||||||
|
"egg",
|
||||||
|
"CHANGELOG",
|
||||||
|
".mypy",
|
||||||
|
"build/",
|
||||||
|
"test.py",
|
||||||
|
"bugs",
|
||||||
|
"pyproject.toml",
|
||||||
|
]
|
||||||
|
for file in files:
|
||||||
|
if file.is_dir():
|
||||||
|
continue
|
||||||
|
if any(s in str(file) for s in ignore):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
text = file.read_text()
|
||||||
|
if search_str in text:
|
||||||
|
c = text.count(search_str)
|
||||||
|
if 0 < c < 2:
|
||||||
|
print(file, c)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
imports = ["Pango", "GObject", "GLib", "Gdk", "Gtk"]
|
||||||
|
for i in imports:
|
||||||
|
iterate(i)
|
||||||
@ -1,20 +1,33 @@
|
|||||||
|
import re
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from importlib import resources
|
from pathlib import Path
|
||||||
from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def changelog():
|
def changelog(request) -> None:
|
||||||
path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH)
|
root = request.config.rootpath
|
||||||
return path
|
changelog = Path(root).joinpath("CHANGELOG.md").read_text()
|
||||||
|
return changelog
|
||||||
|
|
||||||
|
|
||||||
def test_headings(changelog):
|
def count_hash(line: str) -> int:
|
||||||
with open(changelog, "r") as f:
|
cnt = 0
|
||||||
lines = f.readlines()
|
for c in line:
|
||||||
for line in lines:
|
if c == "#":
|
||||||
if line.startswith("#"):
|
cnt += 1
|
||||||
pass
|
return cnt
|
||||||
# TODO: use regex
|
|
||||||
pass
|
|
||||||
|
def test_changelog_prefix(changelog) -> None:
|
||||||
|
r = r".*(\[.*\]).*"
|
||||||
|
lines = changelog.splitlines()
|
||||||
|
sort = sorted(lines)
|
||||||
|
match = [line for line in sort if line.startswith("#")]
|
||||||
|
for m in match:
|
||||||
|
if "Changelog" in m:
|
||||||
|
assert count_hash(m) == 1
|
||||||
|
elif re.match(r, m) is not None:
|
||||||
|
assert count_hash(m) == 2
|
||||||
|
else:
|
||||||
|
assert count_hash(m) == 3
|
||||||
|
|||||||
59
tests/test_colorscheme.py
Normal file
59
tests/test_colorscheme.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class TestWidget(ColorAwareApp, Gtk.Button):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.state: bool
|
||||||
|
|
||||||
|
def check_theme(self) -> None:
|
||||||
|
self.state = self.get_settings().get_property(GIO_SETTINGS_KEY)
|
||||||
|
Gtk.main_quit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def widget() -> TestWidget:
|
||||||
|
return TestWidget()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)])
|
||||||
|
def test_light_mode(widget, key: str, expect: bool) -> None:
|
||||||
|
local_pref = subprocess.run(
|
||||||
|
["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
).stdout
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"gsettings",
|
||||||
|
"set",
|
||||||
|
"org.gnome.desktop.interface",
|
||||||
|
"color-scheme",
|
||||||
|
key,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE: allow changes to propagate
|
||||||
|
GLib.idle_add(widget.check_theme)
|
||||||
|
Gtk.main()
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"gsettings",
|
||||||
|
"set",
|
||||||
|
"org.gnome.desktop.interface",
|
||||||
|
"color-scheme",
|
||||||
|
local_pref,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert widget.state is expect
|
||||||
30
tests/test_html_escaping.py
Normal file
30
tests/test_html_escaping.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from dzgui.api.servers import Details
|
||||||
|
from dzgui.views.dialogs.servers import ServerDetailsDialog
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk # noqa E402
|
||||||
|
|
||||||
|
|
||||||
|
class MockController:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.window = Gtk.Window()
|
||||||
|
|
||||||
|
def get_window(self) -> Gtk.Window:
|
||||||
|
return self.window
|
||||||
|
|
||||||
|
def get_server_name(self) -> str:
|
||||||
|
return "My server"
|
||||||
|
|
||||||
|
def get_emitter(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def test_html_escaping() -> None:
|
||||||
|
text = "<Server description> Game & mods"
|
||||||
|
details = Details([["0", "1"]], text, True)
|
||||||
|
controller = MockController()
|
||||||
|
dialog = ServerDetailsDialog(controller, details)
|
||||||
|
|
||||||
|
assert dialog.description.get_text() == text
|
||||||
23
tests/test_time.py
Normal file
23
tests/test_time.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from dzgui.model.proxy_model import DAY_REG, NIGHT_REG
|
||||||
|
|
||||||
|
|
||||||
|
def iterate(h: str, r: str) -> None:
|
||||||
|
for m in range(60):
|
||||||
|
time = f"{h:02}:{m:02}"
|
||||||
|
assert re.match(r, time) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_day() -> None:
|
||||||
|
for h in range(17):
|
||||||
|
if h < 7:
|
||||||
|
continue
|
||||||
|
iterate(h, DAY_REG)
|
||||||
|
|
||||||
|
|
||||||
|
def test_night() -> None:
|
||||||
|
for h in range(24):
|
||||||
|
if 6 < h < 17:
|
||||||
|
continue
|
||||||
|
iterate(h, NIGHT_REG)
|
||||||
Loading…
Reference in New Issue
Block a user