From 88ad8cf7c23570f09b9fc28701e1d26883e97731 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:29:38 +0900 Subject: [PATCH 01/10] feat: expand all changelog nodes --- dzgui/views/pages/changelog.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/dzgui/views/pages/changelog.py b/dzgui/views/pages/changelog.py index 4d36637..9548b11 100644 --- a/dzgui/views/pages/changelog.py +++ b/dzgui/views/pages/changelog.py @@ -1,5 +1,6 @@ import logging import re +import textwrap from typing import TYPE_CHECKING from importlib import resources @@ -7,6 +8,7 @@ from importlib import resources from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH from dzgui.util.strings import missing_changelog from dzgui.util.format import format_pango +from dzgui.views.components.box import HBox, VBox from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin from dzgui.views.mixins.scrollable_mixin import ScrollableMixin @@ -31,20 +33,40 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig logger.critical(e) changelog = missing_changelog - # TODO: should long text be wrapped? + # FIXME: wrap long text self.controller = controller self.box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10 ) self.add(self.box) + expand_all = Gtk.Button( + label="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_esc_keypress) changes = self._parse(changelog) self._generate_nodes(changes) + 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 = "Collapse all" if self.expanded else "Expand all" + button.set_label(label) + def grab_content_area(self) -> None: self.grab_focus() @@ -65,9 +87,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig text = "\n".join(changes) formatted = format_pango(text) - container = Gtk.Box( - valign=Gtk.Align.START, 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) @@ -81,6 +101,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig expander.add(container) expander.connect("activate", self._on_expand, container) self.box.add(expander) + self.expanders.append(expander) def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None: """ @@ -94,6 +115,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig container.set_margin_bottom(15) def _parse(self, changelog: str) -> list[tuple[str, list[str]]]: + release = "" releases: list[tuple[str, list[str]]] = [] release_notes: list[str] = [] @@ -107,5 +129,5 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig release = "" release = line continue - release_notes.append(line.rstrip()) + release_notes.append(textwrap.fill(line.rstrip(), width=120)) return releases From eeffdb57f36f7aa6eea51e1cc5501957363b7b7c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:30:10 +0900 Subject: [PATCH 02/10] chore: remove unused imports --- dzgui/views/pages/changelog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dzgui/views/pages/changelog.py b/dzgui/views/pages/changelog.py index 9548b11..35d6e48 100644 --- a/dzgui/views/pages/changelog.py +++ b/dzgui/views/pages/changelog.py @@ -8,7 +8,6 @@ from importlib import resources from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH from dzgui.util.strings import missing_changelog from dzgui.util.format import format_pango -from dzgui.views.components.box import HBox, VBox from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin from dzgui.views.mixins.scrollable_mixin import ScrollableMixin From 456c6e62e8b9be7f6d5aa356b24186b4b7e4333b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:59:13 +0900 Subject: [PATCH 03/10] chore: add test for changelog markdown headers --- tests/test_changelog.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 3eae186..acc6019 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,20 +1,34 @@ +import re import pytest -from importlib import resources -from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH +from pathlib import Path @pytest.fixture -def changelog(): - path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH) - return path +def changelog(request) -> None: + root = request.config.rootpath + changelog = Path(root).joinpath("CHANGELOG.md").read_text() + return changelog -def test_headings(changelog): - with open(changelog, "r") as f: - lines = f.readlines() - for line in lines: - if line.startswith("#"): - pass - # TODO: use regex - pass +def count_hash(line: str) -> int: + cnt = 0 + for c in line: + if c == "#": + cnt += 1 + return cnt + + +@pytest.mark.FOO +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 From 79bbc25714e5fe72b3936d5f68d1b6951a6b40d6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:59:13 +0900 Subject: [PATCH 04/10] chore: add test for changelog markdown headers --- tests/test_changelog.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 3eae186..acc6019 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,20 +1,34 @@ +import re import pytest -from importlib import resources -from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH +from pathlib import Path @pytest.fixture -def changelog(): - path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH) - return path +def changelog(request) -> None: + root = request.config.rootpath + changelog = Path(root).joinpath("CHANGELOG.md").read_text() + return changelog -def test_headings(changelog): - with open(changelog, "r") as f: - lines = f.readlines() - for line in lines: - if line.startswith("#"): - pass - # TODO: use regex - pass +def count_hash(line: str) -> int: + cnt = 0 + for c in line: + if c == "#": + cnt += 1 + return cnt + + +@pytest.mark.FOO +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 From 5536180fecdd5341c09bf781ef90bf645616d8f7 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:03:27 +0900 Subject: [PATCH 05/10] chore: drop placeholder mark --- tests/test_changelog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index acc6019..e897571 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -19,7 +19,6 @@ def count_hash(line: str) -> int: return cnt -@pytest.mark.FOO def test_changelog_prefix(changelog) -> None: r = r".*(\[.*\]).*" lines = changelog.splitlines() From 1e073af27ab548ee8dd2e91e596d71873808dfdb Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:22:14 +0900 Subject: [PATCH 06/10] feat: add notebook to ExceptionDialog --- dzgui/views/dialogs/generic.py | 54 ++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index c9b890c..19e6b44 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -196,17 +196,49 @@ class ExceptionDialog(GenericDialog): 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 + wrap_mode=Gtk.WrapMode.WORD, + editable=False, + left_margin=10, + right_margin=10, + top_margin=15, ) textview.set_buffer(Gtk.TextBuffer(text=self.trace)) - box.pack_start(textview, EXPAND, FILL, 10) - scrollable.add(box) + 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) - # TODO: padding around top of content area when traceback is long content.add(scrollable) copy_button = ClipboardButton(controller, self.get_trace) @@ -219,6 +251,18 @@ class ExceptionDialog(GenericDialog): self.ok.grab_focus() self.connect("response", self._on_response) + def _on_page_changed( + self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int + ) -> None: + if child == self.error_details: + child.set_propagate_natural_height(True) + else: + self.error_details.set_propagate_natural_height(False) + + def set_secondary_text(self, text: str) -> None: + self.details_buffer.set_text(text) + self.error_notebook.set_show_tabs(True) + def get_trace(self) -> str: return self.trace From 0ebc25b3b4b0669be63cf92acaf6e9f9153b550d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:26:42 +0900 Subject: [PATCH 07/10] fix: clear typehinting errors --- dzgui/views/dialogs/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 19e6b44..16d0e8e 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -252,10 +252,10 @@ class ExceptionDialog(GenericDialog): self.connect("response", self._on_response) def _on_page_changed( - self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int + self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int ) -> None: if child == self.error_details: - child.set_propagate_natural_height(True) + self.error_details.set_propagate_natural_height(True) else: self.error_details.set_propagate_natural_height(False) From 8087f3a707b120da95a797908e2e0e7974276ab5 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:38:45 +0900 Subject: [PATCH 08/10] fix: add bottom padding --- dzgui/views/dialogs/generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index 16d0e8e..f3174eb 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -202,6 +202,7 @@ class ExceptionDialog(GenericDialog): left_margin=10, right_margin=10, top_margin=15, + bottom_margin=10, ) textview.set_buffer(Gtk.TextBuffer(text=self.trace)) box.pack_start(textview, EXPAND, FILL, 0) From 0bb8f3206f4048375da2a78e21189c20896eb90d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:40:02 +0900 Subject: [PATCH 09/10] feat: internal knowledeat: add knowledge base text to specific exception dialogs --- dzgui/managers/connection.py | 6 +++++- dzgui/managers/mods.py | 3 ++- dzgui/model/servers.py | 3 ++- dzgui/strings/kb.py | 15 +++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 dzgui/strings/kb.py diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index c96abc2..9baf109 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -45,6 +45,7 @@ from dzgui.strings.dialogs import ( waiting_for_mods, waiting_for_directories, ) +from dzgui.strings import kb from dzgui.strings.server_mods import checkmark, resync from dzgui.util.format import format_mib from dzgui.util.strings import dialog, server_timeout @@ -193,7 +194,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"] + invalid_mods = [ + (mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod" + ] prereqs = Prerequisites( name=info.server_name, @@ -298,6 +301,7 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def _connect_steam(self, menu_only: bool) -> None: diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index da31385..522c340 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -20,7 +20,7 @@ from dzgui.const.constants import ( from dzgui.const.enum import Preferences from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.model.model_factory import FastInsertListStore, ModelFactory -from dzgui.strings import dialogs +from dzgui.strings import dialogs, kb from dzgui.util.format import format_mods from dzgui.util.strings import server_timeout from dzgui.util.symlink import rebuild_symlinks @@ -215,6 +215,7 @@ class ModManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_seconary_text(kb.DZG_006) dialog.run() def select_colorized(self) -> None: diff --git a/dzgui/model/servers.py b/dzgui/model/servers.py index bc5d616..cfb2c0a 100644 --- a/dzgui/model/servers.py +++ b/dzgui/model/servers.py @@ -14,7 +14,7 @@ from dzgui.const.constants import ( ) from dzgui.const.enum import FilterMode, Preferences, ServerTab 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.views.dialogs.generic import ExceptionDialog @@ -420,6 +420,7 @@ class ServerModelManager: # customize statusbar and dialog accordingly if show_dialog: dialog = ExceptionDialog(self.controller, server_timeout) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def _push_data(self, data: list[Any]) -> None: diff --git a/dzgui/strings/kb.py b/dzgui/strings/kb.py new file mode 100644 index 0000000..42f300b --- /dev/null +++ b/dzgui/strings/kb.py @@ -0,0 +1,15 @@ +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, packets received from server responses are expected to be a standard size: MTU (maximum transmission unit) of 1500. + +Deviation from this may cause your router to discard incoming responses from the server.""" From b300b614a09ed7eccd06247b957fbdd96575bdc2 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:40:34 +0900 Subject: [PATCH 10/10] fix: typo --- dzgui/managers/mods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index 522c340..742873a 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -215,7 +215,7 @@ class ModManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) - dialog.set_seconary_text(kb.DZG_006) + dialog.set_secondary_text(kb.DZG_006) dialog.run() def select_colorized(self) -> None: