From 85c1d1862235bf6ebd59e7b0a85d9f19b818b67b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:35:37 +0900 Subject: [PATCH] feat: add standalone managers --- dzgui/model/contextmenu.py | 104 +++++++++++++++++++++++++++++++++++++ dzgui/model/thread_man.py | 77 +++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 dzgui/model/contextmenu.py create mode 100644 dzgui/model/thread_man.py diff --git a/dzgui/model/contextmenu.py b/dzgui/model/contextmenu.py new file mode 100644 index 0000000..728b7b6 --- /dev/null +++ b/dzgui/model/contextmenu.py @@ -0,0 +1,104 @@ +import gi +import logging +from typing import TYPE_CHECKING + +from dzgui.const.enum import ContextMenu, Preferences +from dzgui.model.thread_man import ThreadingManager +from dzgui.util import strings +from dzgui.util.open_links import open_workshop_page + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402 + + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from dzgui.controllers.mc import Controller + + +class ContextMenuManager: + def __init__(self, treeview: Gtk.TreeView, controller: "Controller") -> None: + self.controller = controller + self.treeview = treeview + self.thread_man = ThreadingManager(parent=controller) + + def process(self, action: ContextMenu) -> None: + match action: + # NON THREADED + case ContextMenu.ADD_NOTE: + # spawn edit dialog and update cache, notes file + pass + case ContextMenu.COPY_LOG_CLIPBOARD: + self.copy_log() + case ContextMenu.COPY_SERVER_IP: + self.copy_server_ip() + case ContextMenu.COPY_SERVER_NAME: + self.copy_server_name() + case ContextMenu.DELETE_MOD: + # self.delete_single_mod() + # TODO: connect to emitter automatically + # Gtk.TreeModel, row-inserted/row-deleted + # updates statusbar + # FIXME: signal should instead be emitted off of treeview when rows added/inserted + # self.update_mod_statusbar() + pass + case ContextMenu.OPEN_WORKSHOP: + self.open_mod_page() + case ContextMenu.SET_FAV: + self.controller.set_fav() + + # THREADED + case ContextMenu.ADD_SERVER: + # add to saved servers model verbatim and sort in place + # update tab with ! + # update config file with IP + pass + case ContextMenu.REFRESH_PLAYERS: + # get record + # call a2s on thread + pass + case ContextMenu.REMOVE_HISTORY: + # update history model, update tab label, pop off of queue, write new list into file + # see dq.py + pass + case ContextMenu.REMOVE_SERVER: + # reverse of ADD_SERVER + pass + case ContextMenu.SHOW_DETAILS: + pass + case ContextMenu.SHOW_MODS: + pass + + def copy_server_ip(self) -> None: + record = self.treeview.get_simplified_ip() + self.copy_clipboard(record) + + def copy_clipboard(self, text: str) -> None: + self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + self.clipboard.set_text(text, -1) + + def copy_server_name(self) -> None: + name = self.treeview.get_value_at_index(0) + if name is None: + return + self.copy_clipboard(name) + + def open_mod_page(self) -> None: + mod = self.treeview.get_selected_mod() + cmd = self.controller.query_config(Preferences.CLIENT) + open_workshop_page(mod, cmd) + + def copy_log(self) -> str: + # NOTE: ModTreeView uses Gtk.SelectionMode.MULTIPLE + model, records = self.treeview.get_selection().get_selected_rows() + if len(records) < 1: + return + final = [] + for record in records: + record = self.treeview.get_model()[record] + r = [el for el in record] + concat = strings.delimiter.join(r) + final.append(concat) + text = "\n".join(final) + self.copy_clipboard(text) diff --git a/dzgui/model/thread_man.py b/dzgui/model/thread_man.py new file mode 100644 index 0000000..dc10d28 --- /dev/null +++ b/dzgui/model/thread_man.py @@ -0,0 +1,77 @@ +import inspect +import logging +import threading + +from functools import wraps +from typing import Callable +from dzgui.views.dialogs.generic import WaitDialog + + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402 + +logger = logging.getLogger(__name__) + + +def call_on_thread(dialog_str: str) -> Callable: + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + self = args[0] + stored = StoredFunc(func, *args, **kwargs) + if not hasattr(self, "thread_man"): + raise AttributeError + if type(self.thread_man) is not ThreadingManager: + raise TypeError( + "Attribute 'thread_man' must be of type 'ThreadingManager'" + ) + self.thread_man.call_on_thread(dialog_str, stored) + + return wrapper + + return decorator + + +class StoredFunc: + def __init__(self, func: Callable, *args, **kwargs) -> None: + sig = inspect.signature(func) + self.func = func + self.bindings = sig.bind(*args, **kwargs) + + def call(self) -> None: + self.func(*self.bindings.args, *self.bindings.kwargs) + + +class ThreadingManager: + def __init__(self, parent: Gtk.Window) -> None: + self.parent = parent + self.cleanup_func = None + + def call_on_thread(self, dialog_str: str, func: StoredFunc) -> None: + def callback() -> None: + func.call() + GLib.idle_add(self._destroy_on_idle) + + self.wait_dialog = WaitDialog(self.parent, dialog_str) + self.wait_dialog.show_all() + thread = threading.Thread(target=callback) + thread.start() + + def set_cleanup_func(self, func: StoredFunc) -> None: + if type(func) not in (StoredFunc, type(None)): + msg = f"Callback function '{func}' is not of type StoredFunc or None" + logger.critical(msg) + raise TypeError(msg) + self.cleanup_func = func + + def get_cleanup_func(self) -> StoredFunc: + return self.cleanup_func + + def _destroy_on_idle(self) -> None: + self.wait_dialog.destroy() + func = self.get_cleanup_func() + if func is not None: + func.call() + self.set_cleanup_func(None)