From 2b8e0944a4c4139d6a9d0a0528ec5ee0b1eedf68 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:20:38 +0900 Subject: [PATCH 01/14] fix: time regex --- dzgui/model/proxy_model.py | 4 ++-- tests/test_time.py | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/test_time.py diff --git a/dzgui/model/proxy_model.py b/dzgui/model/proxy_model.py index c07a711..997a59f 100644 --- a/dzgui/model/proxy_model.py +++ b/dzgui/model/proxy_model.py @@ -231,10 +231,10 @@ class ProxyModelManager: final.append(row) rows = final case strings.filter_day: - reg = r"([0][0-9]|[1][0-6])" + reg = r"([0][7-9]|[1][0-6])" rows = [row for row in rows if not re.match(reg, row[3])] case strings.filter_night: - reg = r"([0][0-4]|[1][8]|[2][0-3])" + reg = r"([0][0-6]|[1][7-9]|[2][0-3])" rows = [row for row in rows if not re.match(reg, row[3])] case strings.filter_nonascii: rows = [row for row in rows if row[0].isascii()] diff --git a/tests/test_time.py b/tests/test_time.py new file mode 100644 index 0000000..f67906d --- /dev/null +++ b/tests/test_time.py @@ -0,0 +1,26 @@ +import pytest +import re + +day = r"([0][7-9]|[1][0-6])" +night = r"([0][0-6]|[1][7-9]|[2][0-3])" + +pytestmark = pytest.mark.FOO + +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) + + +def test_night() -> None: + for h in range(24): + if 6 < h < 17: + continue + iterate(h, night) From c179e3a9e92eb6235aac81332346e832ee50f7bd Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:37:13 +0900 Subject: [PATCH 02/14] chore: drop unused test mark --- tests/test_time.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_time.py b/tests/test_time.py index f67906d..65257b3 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -1,11 +1,8 @@ -import pytest import re day = r"([0][7-9]|[1][0-6])" night = r"([0][0-6]|[1][7-9]|[2][0-3])" -pytestmark = pytest.mark.FOO - def iterate(h: str, r: str) -> None: for m in range(60): time = f"{h:02}:{m:02}" From 533def5061731b30a4553ec2e5f9feb309eeb328 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:59:04 +0900 Subject: [PATCH 03/14] chore: drop unused vars and classes --- dzgui/controllers/mc.py | 1 - dzgui/views/components/connect_box.py | 68 --------------------------- 2 files changed, 69 deletions(-) delete mode 100644 dzgui/views/components/connect_box.py diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index b2c1a08..2a611e1 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -88,7 +88,6 @@ class Controller(GObject.GObject): # NOTE: suppress requests until entire UI is loaded self.loaded = False - self.pending_jobs = 1 self.exit_event = threading.Event() self.cancel_event = threading.Event() diff --git a/dzgui/views/components/connect_box.py b/dzgui/views/components/connect_box.py deleted file mode 100644 index 3f1200b..0000000 --- a/dzgui/views/components/connect_box.py +++ /dev/null @@ -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 From 8d1d5bde80a4756aeb5d84a3f3d5c4ae44780736 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:43:19 +0900 Subject: [PATCH 04/14] feat: add SpinnerButton --- dzgui/views/components/buttons.py | 36 ++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/dzgui/views/components/buttons.py b/dzgui/views/components/buttons.py index 8071ed7..6697c17 100644 --- a/dzgui/views/components/buttons.py +++ b/dzgui/views/components/buttons.py @@ -64,7 +64,6 @@ class IconButton(Gtk.Button): self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end) self.set_image(self.icon) self.set_image_position(position) - # self.set_image_position(Gtk.PositionType.RIGHT) self.set_focus_on_click(False) def swap_icon(self, icon: str) -> None: @@ -267,3 +266,38 @@ class LoggerAlertsButton(IconTextButton): self.set_halign(Gtk.Align.END) self.set_hexpand(True) self.set_tooltip_text(alert_button_tooltip) + + +class SpinnerButton(Gtk.Button): + def __init__(self, label: str) -> None: + super().__init__() + + 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, a) -> 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() From dfd6850d6ddcecde20d65b0353925b2770548de4 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:44:51 +0900 Subject: [PATCH 05/14] change: move ErrorPopover --- dzgui/views/components/misc.py | 20 +++++++++++++++++++- dzgui/views/pages/offline.py | 24 ++++++------------------ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/dzgui/views/components/misc.py b/dzgui/views/components/misc.py index 7b9fc0f..fa8e29c 100644 --- a/dzgui/views/components/misc.py +++ b/dzgui/views/components/misc.py @@ -1,5 +1,8 @@ +from dzgui.const.constants import ERROR, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD 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 @@ -33,3 +36,18 @@ class ClientCombo(Gtk.ComboBox): self.pack_start(renderer_text, True) self.add_attribute(renderer_text, "text", 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) diff --git a/dzgui/views/pages/offline.py b/dzgui/views/pages/offline.py index f5baa9d..dec7060 100644 --- a/dzgui/views/pages/offline.py +++ b/dzgui/views/pages/offline.py @@ -9,7 +9,6 @@ from dzgui.const.constants import ( APPNAME_DAYZ, APPNAME_DAYZ_EXP_HUMAN, EDIT_DELETE, - ERROR, FOLDER, WARNING, ) @@ -21,6 +20,7 @@ from dzgui.views.components.buttons import Icon, IconTextButton from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.scrollable import NoOverlayScrolledWindow +from dzgui.views.components.misc import ErrorPopover from dzgui.views.trees.tree_mods import OfflineModTreeView @@ -57,17 +57,9 @@ class PageHeading(Gtk.Box): css.add_class(self, "page-heading") -class ErrorPopover(Gtk.Popover): - def __init__(self) -> None: - super().__init__(position=Gtk.PositionType.RIGHT) - - 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() +class OfflineErrorPopover(ErrorPopover): + def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None: + super().__init__(relative_to=relative_to, position=position) def set_label(self, error: FolderError, msg: str) -> None: match error: @@ -115,13 +107,10 @@ class FolderHBox(HBox): [self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button] ) - self.pop = ErrorPopover() - self.pop.set_relative_to(self.button) + self.pop = OfflineErrorPopover(self.button, Gtk.PositionType.RIGHT) self.pop.connect("unmap", lambda _: self.grab_focus()) - self.sidepop = ErrorPopover() - self.sidepop.set_position(Gtk.PositionType.BOTTOM) - self.sidepop.set_relative_to(self.scrolled_label) + self.sidepop = OfflineErrorPopover(self.scrolled_label, Gtk.PositionType.BOTTOM) self.sidepop.connect("unmap", lambda _: self.grab_focus()) self.connect("map", self._on_map) @@ -467,7 +456,6 @@ class OfflineLoader(Gtk.Box): ] ) - # TODO: share ConnectBox class with preconnect dialog? self.button_box = HBox(spacing=5) self.button_box.set_halign(Gtk.Align.END) self.button_box.set_margin_top(15) From 69cba3460d9381cf0d4585ac49c8c54db361cd40 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:45:26 +0900 Subject: [PATCH 06/14] feat: simplify API udpate signals --- dzgui/controllers/emitter.py | 4 ++++ dzgui/controllers/mc.py | 4 ++-- dzgui/managers/config.py | 11 ++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/dzgui/controllers/emitter.py b/dzgui/controllers/emitter.py index ba7ef10..28016ca 100644 --- a/dzgui/controllers/emitter.py +++ b/dzgui/controllers/emitter.py @@ -145,6 +145,10 @@ class Emitter(GObject.GObject): def api_change_failed(self) -> None: 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=()) def already_saved_server(self) -> None: pass diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 2a611e1..b8b69fc 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -301,8 +301,8 @@ class Controller(GObject.GObject): dialog = ExceptionDialog(self, str(e)) dialog.run() - def update_api_key(self, key: Preferences, text: str) -> None: - self.config_man.update_api_key(key, text) + def update_steam_api_key(self, text: str) -> None: + self.config_man.update_steam_api_key(text) def set_resolution(self, window: "OuterWindow") -> None: self.config_man.set_resolution(window) diff --git a/dzgui/managers/config.py b/dzgui/managers/config.py index 2e81f28..ac559b2 100644 --- a/dzgui/managers/config.py +++ b/dzgui/managers/config.py @@ -98,12 +98,13 @@ class ConfigManager: logger.critical(e) raise e - @call_on_thread(dialogs.checking_api) - def update_api_key(self, key: Preferences, text: str) -> None: - if key is Preferences.STEAM: - res = test_steam_api(text) + @call_on_thread(dialogs.checking_api, show_dialog=False) + def update_steam_api_key(self, text: str) -> None: + res = test_steam_api(text) 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: self.thread_man.set_cleanup_func( StoredFunc(lambda: self.emitter.emit("api_change_failed")) From cc485289e3f0d7602c7c6fe46db03bc67f286d82 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:46:16 +0900 Subject: [PATCH 07/14] feat: abstract field creation into SubmitField --- dzgui/strings/options.py | 9 +- dzgui/views/pages/options.py | 342 ++++++++++++++++++++--------------- 2 files changed, 201 insertions(+), 150 deletions(-) diff --git a/dzgui/strings/options.py b/dzgui/strings/options.py index ec28776..fd2aaaf 100644 --- a/dzgui/strings/options.py +++ b/dzgui/strings/options.py @@ -1,12 +1,19 @@ developers = "Developers" start_tab = "Start tab" 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" flatpak_combo = "Flatpak" flatpak_container_combo = "Flatpak (container)" +steam_placeholder = "Enter your Steam API key" +name_placeholder = "Identifies you to other players" + server_combo = "Server browser" saved_combo = "Saved servers" recent_combo = "Recent" lan_combo = "LAN" + +fullscreen_eventbox = "This option is not available on Steam Deck." +save_button = "Save" + +api_failed = "API validation failed" diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index ff97e2e..a0748fc 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import TYPE_CHECKING +from typing import Callable, TYPE_CHECKING from dzgui.api import pefile as PeFile @@ -17,15 +17,15 @@ from dzgui.const.constants import ( ) from dzgui.const.endpoints import STEAM_API_SETUP 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.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.buttons import SpinnerButton, WebButton 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.mixins.scrollable_mixin import ScrollableMixin @@ -40,6 +40,182 @@ if TYPE_CHECKING: from dzgui.controllers.emitter import Emitter +class SubmitField(Gtk.Box): + 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) + + 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() + self.button.stop_spinner() + self.entry.set_sensitive(True) + + def _on_api_failure(self, emitter: "Emitter") -> None: + self.pop.popup() + 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, @@ -80,29 +256,23 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore self.controller = controller 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_HEIGHT = 1 self.steam_entry: Gtk.Entry + self.pop: Gtk.Popover self.steam = WebButton(label=strings.options.steam_web) self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP) - self.steam_box = self._make_submit_field( - strings.options.enter_steam, Preferences.STEAM, True - ) + self.steam_box = SteamSubmitField(controller) api_rows = [ [LeftLabel(strings.options.steam_placeholder), self.steam_box, self.steam], ] - self.player_box = self._make_submit_field( - strings.options.name_placeholder, Preferences.NAME - ) + self.player_box = NameSubmitField(controller) 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.fullscreen_toggle = ToggleField( @@ -112,12 +282,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore 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) @@ -210,11 +374,13 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore return str(model[ind][0]) def block_text_entry(self) -> None: - self.steam_entry.set_position(-1) - self.steam_entry.set_can_focus(False) + self.steam_box.block_text_entry() + # self.steam_entry.set_position(-1) + # self.steam_entry.set_can_focus(False) 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: self.controller.show_developers_page() @@ -222,46 +388,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None: open_links.open_link_by_url(url) - def _make_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: grid = Gtk.Grid( orientation=Gtk.Orientation.VERTICAL, @@ -281,35 +407,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore row += 1 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: _iter = combo.get_active_iter() if _iter is None: @@ -325,49 +422,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore real_cmd = combo.get_model()[_iter][1] self.controller.update_config(Preferences.CLIENT, real_cmd) - 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 populate_settings(self) -> None: prefs = self.controller.get_prefs() # NOTE: re-check in case file was removed by user between runs @@ -385,32 +439,22 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore steam_path = Path(default_steam_path) - self.old_steam = steam - self.old_name = name - - self.steam_entry.set_text(steam) - p = self.player_box.get_children()[0] - if hasattr(p, "set_text"): - p.set_text(name) + self.steam_box.set_text(steam) + self.player_box.set_text(name) fs = config["fullscreen"] miles = config["use_miles"] if prefs.is_steam_deck: self.fullscreen_toggle.set_sensitive(False) + eb = InfoEventBox(options.fullscreen_eventbox, self.controller) + self.fullscreen_toggle.add(eb) + 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 ( - [name, self.player_box], - [steam, self.steam_box], - ): - if field[0] == "": - field[1].get_children()[1].set_sensitive(False) - dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ) if dayz_version is None: dayz_version = strings.null From 4917c0ce2ed08dd6cb86d8084d88a05fd89cd12b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:56:49 +0900 Subject: [PATCH 08/14] chore: clear typehinting errors --- dzgui/views/components/buttons.py | 2 +- dzgui/views/pages/offline.py | 6 +++--- dzgui/views/pages/options.py | 7 +++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/dzgui/views/components/buttons.py b/dzgui/views/components/buttons.py index 6697c17..45df27d 100644 --- a/dzgui/views/components/buttons.py +++ b/dzgui/views/components/buttons.py @@ -288,7 +288,7 @@ class SpinnerButton(Gtk.Button): self.connect("clicked", self._on_button_clicked) self.connect("map", self._on_map) - def _on_map(self, a) -> None: + def _on_map(self, button: Self) -> None: self.spinner.set_visible(False) def _on_button_clicked(self, button: Self) -> None: diff --git a/dzgui/views/pages/offline.py b/dzgui/views/pages/offline.py index dec7060..b0200b0 100644 --- a/dzgui/views/pages/offline.py +++ b/dzgui/views/pages/offline.py @@ -61,7 +61,7 @@ class OfflineErrorPopover(ErrorPopover): def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None: super().__init__(relative_to=relative_to, position=position) - def set_label(self, error: FolderError, msg: str) -> None: + def set_error_label(self, error: FolderError, msg: str) -> None: match error: case FolderError.NO_VALID_MODS: prefix = offline.no_mods @@ -159,13 +159,13 @@ class FolderHBox(HBox): def present_error(self, error: FolderError, msg: str) -> None: if error == FolderError.FOLDER_CHANGED: - self.sidepop.set_label(error, msg) + self.sidepop.set_error_label(error, msg) self.sidepop.popup() return self.folder = "" self.label.set_text("") self.unset_button.hide() - self.pop.set_label(error, msg) + self.pop.set_error_label(error, msg) self.pop.popup() diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index a0748fc..0066a6a 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -69,6 +69,7 @@ class SubmitField(Gtk.Box): 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: @@ -183,12 +184,14 @@ class SteamSubmitField(SubmitField): def _on_api_success(self, emitter: "Emitter") -> None: self.old_text = self.entry.get_text() - self.button.stop_spinner() + 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() - self.button.stop_spinner() + if isinstance(self.button, SpinnerButton): + self.button.stop_spinner() self.entry.set_sensitive(True) def block_text_entry(self) -> None: From 87f167bd538cb6fec0b5ef7e458f52aaa404360e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:58:30 +0900 Subject: [PATCH 09/14] chore: drop duplicate definition --- dzgui/views/pages/options.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 0066a6a..277c13e 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -485,19 +485,5 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore 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: self.grab_focus() From fd7a549dbc3e1750d7f001b736df00ba01004c93 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:06:54 +0900 Subject: [PATCH 10/14] change: point to tip of releases page --- docs/source/dzgui7.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/dzgui7.rst b/docs/source/dzgui7.rst index 5793579..b6a0e30 100644 --- a/docs/source/dzgui7.rst +++ b/docs/source/dzgui7.rst @@ -8,7 +8,7 @@ runtime environment are built-in. Turnkey installer ------------------------ -- Visit the project's `Releases page `_. +- Visit the project's `Releases page `_. - 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 a file explorer. From f5d595eccde744819fe646366a03fbc75f956dbe Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:07:21 +0900 Subject: [PATCH 11/14] chore: drop BM step from setup --- docs/source/setup.rst | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/docs/source/setup.rst b/docs/source/setup.rst index b7204d8..ec1fa07 100644 --- a/docs/source/setup.rst +++ b/docs/source/setup.rst @@ -4,7 +4,7 @@ Setup 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). 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 ^^^^^^^^^^^^^^^^^^^^^^ -**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 `_ (free). - -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``. + 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. Next steps -------------- From 70ccbf29f3ddd6b4a621a8a6ff6aef8f1e464da0 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:07:52 +0900 Subject: [PATCH 12/14] chore: mention inbuilt support in DZGUI 7 --- docs/source/steam.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/steam.rst b/docs/source/steam.rst index c709725..687c75d 100644 --- a/docs/source/steam.rst +++ b/docs/source/steam.rst @@ -1,6 +1,10 @@ Steam integration ==================== +.. note:: + + In :doc:`DZGUI 7 `, 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. 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% :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. 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. From fe8bf611e16144e4269deafe2a63582d41612dd0 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:14:36 +0900 Subject: [PATCH 13/14] chore: expand knowledge base --- docs/source/kb.rst | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/source/kb.rst b/docs/source/kb.rst index 638f189..afaa68f 100644 --- a/docs/source/kb.rst +++ b/docs/source/kb.rst @@ -3,17 +3,22 @@ Knowledge Base .. _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. + +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, packets sent from server responses are expected to be a standard size (see warning below). +Deviation from this may cause your router to discard incoming responses from the server. -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 than their wired counterparts. .. important:: 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. 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 From a97c17f66b8339c20df6b9bd272bc6983b3c2488 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:18:39 +0900 Subject: [PATCH 14/14] docs: update contributing --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5ba222..77d627a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,3 +67,7 @@ It is recommended to follow integrates well with tooling and helps the project review your code. 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.