Compare commits

...
60 Commits
Author SHA1 Message Date
aclist 5532e4e267 chore: simplify control flow
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-08-13 13:42:51 +09:00
aclist 120703e702 chore: move map count test to pre-config 2026-08-13 13:37:47 +09:00
aclist b31896387a chore: simplify strings 2026-08-13 13:37:21 +09:00
aclist e5bb25cd60 chore: drop forced exception test 2026-08-13 13:32:08 +09:00
aclist e59b7e01fd chore: drop debug message 2026-08-13 13:31:05 +09:00
aclist 7994bf988c chore: clear typehinting errors 2026-08-13 13:27:40 +09:00
aclist 90b1c875df fix: center label in message area 2026-08-13 13:24:48 +09:00
aclist b61e806009 chore: lowercase launch flags for conformity with parser 2026-08-13 13:24:26 +09:00
aclist b46d0614c9 chore: add strings file 2026-08-13 13:24:10 +09:00
aclist c73864d545 fix: unreachable code 2026-08-13 13:23:29 +09:00
aclist 314fed559c chore: drop unused log file metadata 2026-08-13 13:22:41 +09:00
aclist 1c346a6c3c chore: change sort order of CLI args 2026-08-13 13:22:05 +09:00
aclist 65e11ddd03 chore: clear typehinting errors 2026-08-12 05:07:01 +09:00
aclist a1c0592c7d chore: drop unused import 2026-08-12 05:05:28 +09:00
aclist 22c82677cb chore: drop unused import 2026-08-12 05:05:18 +09:00
aclist c5b41346ca chore: drop unused import 2026-08-12 05:04:47 +09:00
aclist c2fc9b2cca feat: test map count at boot 2026-08-12 05:04:28 +09:00
aclist ba9d6e1d3b chore: update strings 2026-08-12 05:04:16 +09:00
aclist be6ab2c9cb feat: test and set map count 2026-08-12 05:04:06 +09:00
aclist 2413dd1f14 feat: dedicated ignore dialog 2026-08-12 05:03:36 +09:00
aclist 9d8d37e110 chore: move ShortHBox to generics
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-08-12 00:54:14 +09:00
aclist fc28d419bf fix: change bool state
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-08-11 22:23:49 +09:00
aclist 095af395f9 feat: dynamic radio sensitivity on steam deck 2026-08-11 22:23:18 +09:00
aclist 4ada06d305 chore: test html escaping in descriptions 2026-08-11 21:44:47 +09:00
aclist 5a0ab92985 chore: add tech debt comment 2026-08-11 21:44:08 +09:00
aclist f2501871ee feat: update dialog on cancel action 2026-08-11 21:43:53 +09:00
aclist cf70182733 feat: unset fav server by button
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-08-09 23:48:17 +09:00
aclist 42feeae8d0 fix: html escape server desc 2026-08-09 21:07:37 +09:00
aclist 28cd6b8c45 Merge pull request #436 from aclist/chore/restore-maximize-logic
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: restore maximize logic
2026-08-05 19:14:27 +09:00
aclist 81e327fb35 chore: drop unused import 2026-08-05 19:13:31 +09:00
aclist 37e247d1d9 chore: restore maximize logic following changelog margin hotfix 2026-08-05 19:13:11 +09:00
aclist 286e3f2d80 Merge pull request #435 from aclist/fix/margins-after-expansion
fix: set margins after expansion
2026-08-05 19:08:35 +09:00
aclist 2ef27a55d8 fix: set margins only after widget expansion 2026-08-05 19:05:57 +09:00
aclist d0ac09ffe5 change: add line breaks to tooltip 2026-08-05 19:05:10 +09:00
aclist c0015c05ec Merge pull request #434 from aclist/fix/maximize-after-realize
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: test maximizing after widget and children are fully realized
2026-08-05 00:24:08 +09:00
aclist 682b5bb4c0 chore: rename method 2026-08-05 00:23:10 +09:00
aclist 3551877192 chore: test maximizing after widget and children are fully realized 2026-08-05 00:22:12 +09:00
aclist b22aba8593 Merge pull request #433 from aclist/fix/set-default-before-maximize
chore: test setting default size before maximizing
2026-08-05 00:06:09 +09:00
aclist f85f1ef2a5 chore: test setting default size before maximizing 2026-08-05 00:04:21 +09:00
aclist 010d7984b6 Merge pull request #432 from aclist/fix/set-default-size
chore: test using default dimensions defined in wizard
2026-08-04 23:57:50 +09:00
aclist 70e1e9e740 Merge branch 'fix/set-default-size' of github.com:aclist/dztui into fix/set-default-size 2026-08-04 23:57:06 +09:00
aclist 734af7cf22 chore: test using default dimensions defined in wizard 2026-08-04 23:56:56 +09:00
aclist d8f4977ffe chore: use default dimensiontest usingefined in wizard 2026-08-04 23:55:54 +09:00
aclist c9fd00f938 Merge pull request #431 from aclist/fix/defer-maximize
chore: test deferring maximize call until after widget is realized
2026-08-04 23:38:30 +09:00
aclist f4db1484d8 chore: test deferring maximize call until after widget is realized 2026-08-04 23:36:08 +09:00
aclist 99ef9a31b9 Merge pull request #430 from aclist/fix/steam-deck-fs
fix: maximize UI on Steam Deck
2026-08-04 23:16:44 +09:00
aclist 4c392dc571 chore: bump version 2026-08-04 23:15:22 +09:00
aclist 155ffea15d chore: more idiomatic syntax 2026-08-04 23:15:01 +09:00
aclist 57d231331d fix: trigger maximize on steam deck (#426) 2026-08-04 23:14:20 +09:00
aclist d5fe3c35dc Merge pull request #429 from aclist/fix/scrollable-key-nav
fix: repack certain pages to support keybindings
2026-08-04 23:12:07 +09:00
aclist 3cf547e903 chore: backport changes from dzgui7 branch
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-08-04 23:09:54 +09:00
aclist bd0bfcb03d Merge pull request #428 from aclist/fix/invalid-mod-manifest
fix: handle invalid mods on server
2026-08-04 23:06:05 +09:00
aclist 4f6c583294 chore: add comment
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-08-04 23:05:12 +09:00
aclist 5e5a6b4eaf chore: more generic warning message 2026-08-04 23:04:47 +09:00
aclist ff59d6dc35 chore: clear typehinting errors 2026-08-04 18:04:22 +09:00
aclist 1c154a709d fix: repack certain pages to support mixins 2026-08-04 18:03:14 +09:00
aclist fb47564618 chore: trigger checks
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-31 23:13:03 +09:00
aclist 30e309442b chore: trigger checks 2026-07-30 20:45:58 +09:00
aclist 3e4ed95908 chore: trigger checks 2026-07-30 20:37:14 +09:00
aclist 93edbac5b0 fix: handle invalid mods on server 2026-07-30 20:35:23 +09:00
28 changed files with 339 additions and 121 deletions

No files matched your search

+5 -1
View File
@@ -139,6 +139,9 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
j = r.json()
rows = j["response"]["publishedfiledetails"]
for row in rows:
# NOTE: not a valid published file
if row["result"] == 9:
continue
title = str(row["title"])
_id = str(row["publishedfileid"])
time = int(row["time_updated"])
@@ -205,9 +208,11 @@ def launch_offline(
proc = subprocess.run([*client_args, *params])
return proc.returncode
def find_loginusers(path: Path) -> Path:
return path / "config" / "loginusers.vdf"
def find_user_id(path: Path) -> str | None:
resolved_path = find_loginusers(path)
try:
@@ -226,7 +231,6 @@ def find_user_id(path: Path) -> str | None:
return None
def find_user_id_32(path: Path) -> int:
uid = find_user_id(path)
if uid is None:
+2 -1
View File
@@ -18,10 +18,10 @@ from dzgui.init.flock import lock_acquire
from dzgui.init.prereqs import has_steam_client
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.dirs import make_parents
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.strings import init
@@ -119,6 +119,7 @@ def load_gui(version: str, is_debug: bool) -> None:
del os.environ["GTK_IM_MODULE"]
if has_new_config(XDG.config) is False:
test_map_count()
migrate_cols_file(XDG.columns)
copy_state_files(xdg_paths["XDG_STATE_HOME"])
# TODO: add logging inside wizard
+1
View File
@@ -38,6 +38,7 @@ HEX_ORANGE = "#FFAC1C"
CARET_DOWN = "go-down-symbolic"
CARET_UP = "go-up-symbolic"
CLIPBOARD = "edit-copy-symbolic"
CLOSE = "window-close-symbolic"
FOLDER = "folder-symbolic"
EDIT_DELETE = "edit-delete-symbolic"
ERROR = "dialog-error-symbolic"
+8
View File
@@ -402,6 +402,9 @@ class Controller(GObject.GObject):
def get_menu(self) -> "MenuTreeView":
return self.mediator.menu
def unset_fav(self) -> None:
self.config_man.unset_fav()
def has_favorites(self) -> bool:
favs = self.config_man.get_favorites()
if len(favs) < 1:
@@ -459,6 +462,9 @@ class Controller(GObject.GObject):
open_workshop_page(mod, cmd)
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()
if len(note) > 0:
return True
@@ -473,11 +479,13 @@ class Controller(GObject.GObject):
return self.notes_man.get_note(record)
def add_note(self, note: str) -> None:
# TODO: record should be cached upon creation of dialog
tv = self.get_active_treeview()
record = tv.get_record_string()
self.notes_man.add_note(record, note)
def delete_note(self) -> None:
# TODO: record should be cached upon creation of dialog
tv = self.get_active_treeview()
record = tv.get_record_string()
self.notes_man.delete_note(record)
+8 -2
View File
@@ -5,12 +5,14 @@ import warnings
from dzgui.const.constants import APP_NAME
from dzgui.init.libgi import test_libgi_missing
from dzgui.init.prefix import get_version
from dzgui.util.map_count import set_map_count
from dzgui.util.strings import flags
parser = argparse.ArgumentParser(description=flags.description)
parser.add_argument("-v", "--version", action="store_true", help=flags.version)
parser.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall)
parser.add_argument("-d", "--debug", action="store_true", help=flags.debug)
parser.add_argument("-m", "--map", action="store_true", help=flags.map_count)
parser.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall)
parser.add_argument("-v", "--version", action="store_true", help=flags.version)
args = parser.parse_args()
@@ -24,6 +26,7 @@ def uninstall() -> None:
def main() -> None:
# TODO: isolate single flags
if args.version is True:
print(get_version())
sys.exit(0)
@@ -32,6 +35,9 @@ def main() -> None:
sys.exit(0)
if args.debug is True:
warnings.filterwarnings("default", category=DeprecationWarning)
if args.map is True:
set_map_count()
sys.exit(0)
version = get_version()
+7
View File
@@ -160,6 +160,13 @@ class ConfigManager:
except Exception:
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:
try:
real_key = self.enum_to_key(key)
+10
View File
@@ -86,6 +86,7 @@ class Prerequisites:
mods: list[list[str]]
game_mode: bool
is_last_server: bool
invalid_mods: list[tuple[str, str]]
allows_downloads: tuple[bool, str]
@@ -191,6 +192,9 @@ class ConnectionManager:
is_last = self.is_last_server()
# TODO: strings
invalid_mods = [(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"]
prereqs = Prerequisites(
name=info.server_name,
appid=info.game_id,
@@ -206,6 +210,7 @@ class ConnectionManager:
mods=remote_mods,
game_mode=game_mode,
is_last_server=is_last,
invalid_mods=invalid_mods,
allows_downloads=allows_downloads,
)
@@ -261,6 +266,11 @@ class ConnectionManager:
for mod in alpha_mods:
if any(mod[1] in tuple for tuple in missing_mods):
mod[2] = resync
for mod in alpha_mods:
# NOTE: if the mod is neither synched or out of date, it is a malformed mod
if mod[2] == "":
# TODO: strings
mod[2] = "Invalid mod"
return alpha_mods, missing_mods
+3 -3
View File
@@ -21,11 +21,11 @@ class FilterManager:
strings.filter_3pp: True,
strings.filter_night: True,
strings.filter_full: show_full,
strings.filter_lowpop: True,
strings.filter_nonascii: False,
strings.filter_duplicate: False,
strings.filter_official: True,
strings.filter_nonascii: False,
strings.filter_lowpop: True,
strings.filter_unofficial: True,
strings.filter_duplicate: False,
strings.filter_modded: True,
}
+16
View 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"
-4
View File
@@ -54,8 +54,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
cpu = get_cpu_model()
version = get_version()
debug = lookup(config, Preferences.DEBUG)
install = lookup(config, Preferences.INSTALL)
default = lookup(config, Preferences.DEFAULT)
steam_path = Path(default)
@@ -79,8 +77,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
Kernel: {kernel}
CPU: {cpu}
Debug: {debug}
Auto-install: {install}
Steam path: {steam_redacted}
Workshop path: {workshop_redacted}
+59 -16
View File
@@ -1,28 +1,71 @@
import logging
import subprocess
import sys
import tempfile
import traceback
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:
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
count = int(path.read_text())
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:
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"
count = f"vm.max_map_count={MIN_COUNT}"
with open(conf, "w") as f:
f.write(count)
# TODO: use concat_bash_args()
subprocess.run(["/usr/bin/sudo", "sysctl", "-p", conf])
value = f"vm.max_map_count={MIN_COUNT}"
try:
msg = map_count.prompt.format(conf)
print(msg)
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)
+8 -8
View File
@@ -327,8 +327,8 @@ mod_panel = ModPanelStrings(
unhighlight_stale_tooltip="Clears highlight from stale mods",
highlight_stale="Highlight stale",
highlight_stale_tooltip=(
"Shows locally-installed mods which are not used by any server "
"in your Saved Servers"
"Shows locally-installed mods which are not \n"
"used by any server in your Saved Servers"
),
unsub_selected="Unsubscribe selected",
unsub_selected_tooltip="Unsubscribes from selected mods",
@@ -467,13 +467,15 @@ class Flags:
version: str
uninstall: str
debug: str
map_count: str
flags = Flags(
description="DayZ server browser and mod manager",
version="Print version information",
uninstall="Uninstall data files (use prior to 'pip uninstall')",
debug="Enables developer debugging features",
version="print version information",
uninstall="clean up state/config files",
debug="enable developer debugging features",
map_count="check and update system map count value",
)
@@ -541,9 +543,7 @@ connect_panel = ConnectPanel(
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"
),
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",
)
+4 -1
View File
@@ -54,7 +54,10 @@ class OuterWindow(Gtk.Window):
MainController.register_widget("window", self)
# NOTE: steam deck taskbar may occlude elements
if MainController.get_prefs().is_steam_deck is False:
is_deck = MainController.get_prefs().is_steam_deck
if is_deck:
self.maximize()
else:
self.set_titlebar(self.hb)
self.connect("delete-event", self._on_delete_event)
+7
View File
@@ -23,3 +23,10 @@ class HBox(GenericBox):
class VBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
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)
+9
View File
@@ -10,6 +10,7 @@ from dzgui.util.strings import (
)
from dzgui.const.constants import (
CLIPBOARD,
CLOSE,
INPUT_KEYBOARD,
LIST_ADD,
REFRESH_ICON,
@@ -128,6 +129,14 @@ class CopyIpButton(ClipboardButton):
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):
def __init__(self, label: str) -> None:
super().__init__(icon=WEB_BROWSER, label=label)
+24 -5
View File
@@ -6,6 +6,7 @@ from dzgui.strings import connect_panel
from dzgui.util.keys import is_ctrl_mask
from dzgui.views.components.buttons import (
AddButton,
CloseButton,
CopyIpButton,
Icon,
IconButton,
@@ -187,17 +188,27 @@ class FavPanel(Gtk.Frame):
self.fav_button = SteamConnectButton()
self.fav_button.connect("clicked", self._on_connect_clicked)
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip)
# TODO: strings
self.unset_button = CloseButton("Unset")
self.unset_button.connect("clicked", self._on_unset_clicked)
if favorite is None:
self.toggle_buttons(False)
# NOTE: disable vscrollbar to prevent layout jumping behavior
scrollable_label = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
scrollable_label.add(self.fav_label)
self.scrollable_label = Gtk.ScrolledWindow(
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.attach(scrollable_label, 0, 0, 3, ROWS)
grid.attach(self.scrollable_label, 0, 0, 3, ROWS)
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(
self.fav_button, self.copy_button, Gtk.PositionType.RIGHT, COLS, ROWS
@@ -205,8 +216,16 @@ class FavPanel(Gtk.Frame):
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:
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)
def _on_connect_clicked(self, button: Gtk.Button) -> None:
+7 -9
View File
@@ -33,15 +33,15 @@ class ButtonGrid(Gtk.Grid):
super().__init__(
halign=Gtk.Align.CENTER, column_spacing=5, column_homogeneous=True
)
row = 1
col = 0
self.controller = controller
self.emitter = controller.get_emitter()
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
)
for check in defaults.keys():
checkbox = Gtk.CheckButton(label=check)
label = checkbox.get_child()
@@ -51,14 +51,12 @@ class ButtonGrid(Gtk.Grid):
if defaults[check]:
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)
flowbox.add(checkbox)
self.checks.append(checkbox)
self.add(flowbox)
def block_toggles(self, state: bool) -> None:
for check in self.checks:
self.controller.suppress_signal(
+19 -10
View File
@@ -10,21 +10,26 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402
class EarlyAlertDialog(Gtk.MessageDialog):
def __init__(self, string: str) -> None:
class AbortDialog(Gtk.MessageDialog):
def __init__(self, string: str, buttons: Gtk.ButtonsType) -> None:
super().__init__(
title=dialog_header,
text=dialog_error,
transient_for=None,
buttons=Gtk.ButtonsType.OK,
buttons=buttons,
)
msg = textwrap.fill(string, 50)
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.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.set_margin_start(30)
self.outer.set_margin_end(30)
@@ -32,26 +37,30 @@ class EarlyAlertDialog(Gtk.MessageDialog):
self.set_default_size(250, 100)
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"):
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.run()
self.destroy()
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
match response:
case Gtk.ResponseType.OK:
case Gtk.ResponseType.OK | Gtk.ResponseType.DELETE_EVENT:
sys.exit(1)
case Gtk.ResponseType.CANCEL:
return
class EarlyIgnoreDialog(EarlyAlertDialog):
class EarlyAlertDialog(AbortDialog):
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)
+7 -1
View File
@@ -107,7 +107,7 @@ class WaitDialog(GenericDialog):
self.cur_job = 1
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)
content = self.get_content_area()
@@ -138,6 +138,11 @@ class WaitDialog(GenericDialog):
self.prog.set_fraction(fraction)
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:
self.cancel.set_visible(state)
@@ -156,6 +161,7 @@ class QuitDialog(GenericDialog):
self.add_button(strings.exit_app, Gtk.ResponseType.OK)
self.connect("response", self._on_response)
# TODO: superfluous with the above
self.connect("delete-event", self._on_response)
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
+3 -1
View File
@@ -1,3 +1,4 @@
import html
from typing import Self, TYPE_CHECKING
from dzgui.const.constants import EXPAND, FILL
@@ -65,6 +66,7 @@ class ServerDialog(GenericDialog):
class ServerDetailsDialog(ServerDialog):
def __init__(self, controller: "Controller", details: "Details"):
# TODO: server name should also be packed in details struct
name = controller.get_server_name()
super().__init__(controller, strings.server_details, name, menu=None)
@@ -109,7 +111,7 @@ class ServerDetailsDialog(ServerDialog):
self.view.set_model(self.store)
text = details.description
text = format_hyperlinks(text)
self.description.set_markup(text)
self.description.set_markup(html.escape(text))
self.show_all()
+5 -1
View File
@@ -37,7 +37,11 @@ class CursorMixin:
end = len(model) - 1
else:
return False
cur_row = self.get_focused_row_index() # type: ignore
try:
cur_row = self.get_focused_row_index() # type: ignore
except Exception:
return False
if position == CursorPosition.DOWN:
if cur_row == end:
+20 -4
View File
@@ -33,7 +33,9 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
# TODO: should long text be wrapped?
self.controller = controller
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10)
self.box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
)
self.add(self.box)
self.connect("key-press-event", self._on_keypress)
@@ -55,7 +57,6 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
return version, date
raise ValueError("Changelog parse error")
def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None:
for header, changes in releases:
for ind in (0, -1):
@@ -64,8 +65,12 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
text = "\n".join(changes)
formatted = format_pango(text)
label = Gtk.Label(valign=Gtk.Align.START, margin=15, halign=Gtk.Align.START)
container = Gtk.Box(
valign=Gtk.Align.START, halign=Gtk.Align.START
)
label = Gtk.Label()
label.set_markup(formatted)
container.add(label)
version, date = self._parse_version(header)
button_text = f"{version} ({date})"
@@ -73,9 +78,20 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
expander = Gtk.Expander()
expander.set_label_widget(button)
expander.add(label)
expander.add(container)
expander.connect("activate", self._on_expand, container)
self.box.add(expander)
def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None:
"""
Wayland/some themes do not expect widgets to have a size allocation until expanded.
Trying to set margins a priori may cause negative allocation,
as the margins are subtracted from zero. In effect, the widget is not realized
until it is expanded for the first time.
"""
container.set_margin_start(15)
container.set_margin_top(15)
container.set_margin_bottom(15)
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
release = ""
+1 -1
View File
@@ -103,4 +103,4 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
return grid
def grab_content_area(self) -> None:
self.box.grab_focus()
self.grab_focus()
+3
View File
@@ -521,3 +521,6 @@ class OfflineLoader(Gtk.Box):
self.mission_frame.present_folder_changed(mission)
return
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
def grab_content_area(self) -> None:
self.local_frame.tree.grab_focus()
+61 -50
View File
@@ -20,11 +20,14 @@ from dzgui.const.enum import Preferences, ServerTab
from dzgui.strings import errors, options
from dzgui.util import strings, css, open_links
from dzgui.views.components.box import ShortHBox, VBox
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.labels import LeftLabel
from dzgui.views.components.buttons import WebButton
from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.misc import ClientCombo
from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
import gi
@@ -37,17 +40,40 @@ if TYPE_CHECKING:
from dzgui.controllers.emitter import Emitter
class ShortHBox(Gtk.Box):
def __init__(self, widget: Gtk.Widget) -> None:
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)
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(Gtk.Box):
class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
def __init__(self, controller: "Controller"):
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
margin_start=10,
margin_end=10,
)
@@ -60,11 +86,6 @@ class Options(Gtk.Box):
self.DEFAULT_WIDTH = 1
self.DEFAULT_HEIGHT = 1
label = Gtk.Label(label=strings.options.header)
label.set_halign(Gtk.Align.CENTER)
css.add_class(label, "page-heading")
self.add(label)
self.steam_entry: Gtk.Entry
self.steam = WebButton(label=strings.options.steam_web)
@@ -84,19 +105,26 @@ class Options(Gtk.Box):
# TODO: make submit field a standalone class
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.always_fs,
Preferences.WINDOW,
)
# TODO: strings
eb = InfoEventBox(
"This option is not available on Steam Deck.", self.controller
)
self.fullscreen_toggle.add(eb)
self.client_combo = ClientCombo()
self.client_combo.connect("changed", self._on_client_changed)
client_hbox = ShortHBox(self.client_combo)
self.distance_toggle = self.make_binary_radio(
strings.options.km, strings.options.mi, Preferences.DIST
self.distance_toggle = ToggleField(
self.controller, strings.options.km, strings.options.mi, Preferences.DIST
)
combo_store = Gtk.ListStore(str, object)
@@ -165,9 +193,16 @@ class Options(Gtk.Box):
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
row += 1
self.scrollable = Gtk.ScrolledWindow(vexpand=True)
self.scrollable.add(grid)
self.add(self.scrollable)
label = Gtk.Label(label=strings.options.header)
label.set_halign(Gtk.Align.CENTER)
css.add_class(label, "page-heading")
box = VBox()
box.add(label)
box.add(grid)
self.add(box)
self.connect("key-press-event", self._on_keypress)
def get_client_name(self) -> str:
model = self.client_combo.get_model()
@@ -290,14 +325,6 @@ class Options(Gtk.Box):
real_cmd = combo.get_model()[_iter][1]
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
@@ -341,23 +368,6 @@ class Options(Gtk.Box):
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:
prefs = self.controller.get_prefs()
# NOTE: re-check in case file was removed by user between runs
@@ -383,14 +393,15 @@ class Options(Gtk.Box):
if hasattr(p, "set_text"):
p.set_text(name)
# NOTE: suppress toggle signal until radios are built
self._suppress_toggles(True)
for el, conf_state in [
(self.fullscreen_toggle, config["fullscreen"]),
(self.distance_toggle, config["use_miles"]),
]:
el.get_children()[conf_state].set_active(True)
self._suppress_toggles(False)
fs = config["fullscreen"]
miles = config["use_miles"]
if prefs.is_steam_deck:
self.fullscreen_toggle.set_sensitive(False)
fs = True
self.fullscreen_toggle.set_suboption_active(fs)
self.distance_toggle.set_suboption_active(miles)
# NOTE: disable buttons if no text is set
for field in (
@@ -442,4 +453,4 @@ class Options(Gtk.Box):
widget.set_visibility(state)
def grab_content_area(self) -> None:
return
self.grab_focus()
+11 -2
View File
@@ -239,6 +239,15 @@ class PreConnectionAssistant(Gtk.Box):
)
"""Errors"""
if len(prereqs.invalid_mods) > 0:
pairs = [": ".join(sub) for sub in prereqs.invalid_mods]
lines = "\n".join(pairs)
msg = (
"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)
if prereqs.binary_missing:
errors.append(
f"Remote server is running the build '{prereqs.build}', but it is not installed.\n{resync_msg}"
@@ -275,10 +284,10 @@ class PreConnectionAssistant(Gtk.Box):
allows_dl, running_app = prereqs.allows_downloads
if len(prereqs.mods) > 0 and allows_dl is False:
msg = (
f"The game '{running_app}' is currently running in Steam, but background downloads are not enabled.\n"
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)
self.add_warnings(warnings)
+1 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [
{name = "aclist"}
]
version = "7.0.0b20"
version = "7.0.0b21"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"
+30
View 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