From a9863ff86bdfc996419df469b01169e54dd3d2da Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 6 May 2026 05:38:29 +0900 Subject: [PATCH] feat: MaskedTree (WIP) --- dzgui/api/mods.py | 3 +- dzgui/data/app.css | 4 ++ dzgui/managers/connection.py | 20 ++++-- dzgui/managers/threading.py | 21 ++++--- dzgui/views/pages/preconnect.py | 107 +++++++++++++++++++++++--------- 5 files changed, 111 insertions(+), 44 deletions(-) diff --git a/dzgui/api/mods.py b/dzgui/api/mods.py index 63015c0..8d43e73 100644 --- a/dzgui/api/mods.py +++ b/dzgui/api/mods.py @@ -116,7 +116,8 @@ def _hash(uid: str) -> str: def remove_stale_signatures(config: Path, versions: Path) -> None: if versions.is_file() is False: - logger.warning("No mod signatures file found") + logger.warning("Creating new version signatures file") + versions.touch() return path = lookup(config, Preferences.DEFAULT) steam_path = Path(path) diff --git a/dzgui/data/app.css b/dzgui/data/app.css index a2346a8..40eb4e6 100644 --- a/dzgui/data/app.css +++ b/dzgui/data/app.css @@ -31,3 +31,7 @@ font-size: 1.2rem; font-weight: 800; } + +.masked-tree { + background-color: transparent; +} diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index f58d815..85df3e8 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -70,18 +70,19 @@ class ConnectionManager: version_file = self.controller.get_prefs().paths.version needs_update = get_needs_update(version_file, hashes) - # TODO: populate version file at boot if nonexistent (after symlinking) + # TODO: store mods that need update in class object for referencing later + # TODO: store remote destination to connect to + # missing mods should be the totality of all mods with no signature # missing = get_missing_mods(local_mod_ids, remote_mod_ids) # print(missing) - - # TODO: when updating, create symlinks of everything - # contains id and signature + # TODO: when downloading mods, create symlinks if missing # TODO: get missing mod sizes, warn if not enough space info = res.source try: dayz_path = PeFile.get_pefile_path(steam_path, info.game_id) + # TODO: handle missing path; do not calculate size if appid is missing total, used, free = shutil.disk_usage(dayz_path) if len(needs_update) > 0: # TODO: generic mib function @@ -97,8 +98,9 @@ class ConnectionManager: self.thread_man.set_cleanup_func(failure_func, destroy_first=True) # TODO: number separator func - # TODO: pack a final PreReq struct with pre-process values + # TODO: pack a final PreReq struct with pre-processed values + # TODO: connection assistant only receives user-facing warnings and list of mods func = StoredFunc(self.controller.open_connection_assistant, res, remote_mods) self.thread_man.set_cleanup_func(func, destroy_first=True) @@ -154,3 +156,11 @@ class ConnectionManager: def _server_timeout(self) -> None: dialog = ExceptionDialog(self.controller, server_timeout) dialog.run() + + def update_mods(self) -> None: + # self.needs_update + pass + + def connect(self) -> None: + # steam api, concat mods + pass diff --git a/dzgui/managers/threading.py b/dzgui/managers/threading.py index 8f85302..f451715 100644 --- a/dzgui/managers/threading.py +++ b/dzgui/managers/threading.py @@ -20,7 +20,7 @@ from gi.repository import Gtk, GLib # noqa E402 logger = logging.getLogger(APP_NAME) -def call_on_thread(dialog_str: str) -> Callable: +def call_on_thread(dialog_str: str, show_dialog: bool = True) -> Callable: def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> None: @@ -32,7 +32,7 @@ def call_on_thread(dialog_str: str) -> Callable: raise TypeError( "Attribute 'thread_man' must be of type 'ThreadingManager'" ) - self.thread_man.call_on_thread(dialog_str, stored) + self.thread_man.call_on_thread(dialog_str, stored, show_dialog) return wrapper @@ -56,13 +56,16 @@ class ThreadingManager: self.cleanup_func: StoredFunc | None = None self.destroy_first = False - def call_on_thread(self, dialog_str: str, func: StoredFunc) -> None: + def call_on_thread( + self, dialog_str: str, func: StoredFunc, show_dialog: bool = True + ) -> None: def callback() -> None: func.call() GLib.idle_add(self._destroy_on_idle) - self.wait_dialog = WaitDialog(self.controller, dialog_str, jobs=self.jobs) - self.wait_dialog.show_all() + if show_dialog: + self.wait_dialog = WaitDialog(self.controller, dialog_str, jobs=self.jobs) + self.wait_dialog.show_all() thread = threading.Thread(target=callback) thread.start() @@ -88,16 +91,20 @@ class ThreadingManager: def get_cleanup_func(self) -> StoredFunc | None: return self.cleanup_func + def destroy_dialog(self) -> None: + if hasattr(self, "wait_dialog"): + self.wait_dialog.destroy() + def _destroy_on_idle(self) -> Literal[False]: if self.destroy_first: - self.wait_dialog.destroy() + self.destroy_dialog() func = self.get_cleanup_func() if func is not None: func.call() self.set_cleanup_func(None) if not self.destroy_first: - self.wait_dialog.destroy() + self.destroy_dialog() return False diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index 2ae6668..9cae71d 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -1,6 +1,6 @@ -from typing import Any, TYPE_CHECKING +from dataclasses import dataclass +from typing import Self, TYPE_CHECKING -from dzgui.api import pefile as PeFile from dzgui.const.constants import ( APPID_DAYZ, APPID_DAYZ_EXP, @@ -13,7 +13,6 @@ from dayzquery import DayzMod from dzgui.util.css import add_class from dzgui.strings import preconnect from dzgui.views.components.frame import HeadingFrame -from dzgui.views.components.labels import IconLabel from dzgui.views.trees.tree_server_mods import ServerModTreeView import gi @@ -27,14 +26,48 @@ if TYPE_CHECKING: from dzgui.controllers.mc import Controller -class WarningLabel(IconLabel): - def __init__(self, text: str) -> None: - super().__init__(text, WARNING) +@dataclass +class Warnings: + passworded: bool + dayz_running: bool + no_steam: bool + wrong_version: bool -class ErrorLabel(IconLabel): - def __init__(self, text: str) -> None: - super().__init__(text, ERROR) +@dataclass +class Errors: + no_space: bool + no_dayz_exp: bool + no_dayz: bool + + +class MaskedTree(Gtk.TreeView): + def __init__(self, icon: str) -> None: + super().__init__(headers_visible=False, can_focus=False) + + self.icon = icon + self.store = Gtk.ListStore(str, str) + self.set_model(self.store) + self.get_selection().set_mode(Gtk.SelectionMode.NONE) + + icon_renderer = Gtk.CellRendererPixbuf() + # NOTE: adjust vertical offset between columns + icon_renderer.set_property("yalign", 0.6) + icon_column = Gtk.TreeViewColumn("", icon_renderer) + icon_column.add_attribute(icon_renderer, "icon-name", 0) + icon_column.set_fixed_width(50) + + text_renderer = Gtk.CellRendererText() + text_column = Gtk.TreeViewColumn("", text_renderer, text=1) + + self.append_column(icon_column) + self.append_column(text_column) + add_class(self, "masked-tree") + + def append(self, items: list[str]) -> None: + self.store.clear() + for item in items: + self.store.append([self.icon, item]) class PreConnectionAssistant(Gtk.ScrolledWindow): @@ -54,7 +87,6 @@ class PreConnectionAssistant(Gtk.ScrolledWindow): self.controller.register_widget("preconnect", self) - # TODO: strings # TODO: dynamic button text if no mods needed self.back = Gtk.Button(label=preconnect.back, halign=Gtk.Align.START) self.cancel = Gtk.Button( @@ -94,28 +126,12 @@ class PreConnectionAssistant(Gtk.ScrolledWindow): self.tree_frame = HeadingFrame(self.tree_box, preconnect.mods) - self.warnings: list[WarningLabel] = [] - # TODO: populate with strings and icons - # set visibility if warnings > 1 - # warning category enums with matching strings - """ - blocking warning types: - - build mismatch - - version mismatch - - not enough drive space - passing warning types: - - dayz is running - - steam is not running - - server has password - """ # TODO: abstract into components - warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - warning_box.add(WarningLabel("Warning 1")) - self.warning_frame = HeadingFrame(warning_box, preconnect.warnings) + self.warning_tree = MaskedTree(WARNING) + self.warning_frame = HeadingFrame(self.warning_tree, preconnect.warnings) - error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - error_box.add(ErrorLabel("Error 1")) - self.error_frame = HeadingFrame(error_box, preconnect.errors) + self.error_tree = MaskedTree(ERROR) + self.error_frame = HeadingFrame(self.error_tree, preconnect.errors) self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.box.add(self.title) @@ -126,6 +142,17 @@ class PreConnectionAssistant(Gtk.ScrolledWindow): self.add(self.box) + # self.connect("key-press-event", self._on_keypress) + self.connect("map", self._on_map) + + def _on_map(self, widget: Self) -> None: + self.tree_frame.set_visible(True) + self.mod_count.set_visible(True) + + def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None: + if event.keyval == Gdk.KEY_Escape: + self.back.emit("clicked") + def _on_ok_clicked(self, button: Gtk.Button) -> None: # TODO: update mod store in place with spinner/toast # TODO: cancel mod downloads @@ -139,6 +166,8 @@ class PreConnectionAssistant(Gtk.ScrolledWindow): self.tree.populate(mods) total = len(mods) + self._set_warnings() + name = res.source.server_name self.title.set_text(name) if total < 1: @@ -148,10 +177,26 @@ class PreConnectionAssistant(Gtk.ScrolledWindow): else: self.tree.set_visible(True) self.mod_count.set_visible(True) + # TODO: print no. of mods that need updating prefix = preconnect.total_mods self.mod_count.set_text(f"{prefix}{str(total)}") - # TODO: reset warning and error dialogs + """ + blocking warning types: + - build mismatch + - version mismatch + - not enough drive space + passing warning types: + - dayz is running + - steam is not running + - server has password + """ + # TODO: if errors > 1, disable buttons + + def _set_warnings(self) -> None: + self.warning_tree.append(["Password protected", "Some other error", "Error 3"]) + self.error_tree.append(["Password protected", "Some other error", "Error 3"]) + pass def download_mods(self) -> None: pass