feat: wrap callback function

This commit is contained in:
aclist 2026-02-14 20:16:28 +09:00
parent 2c8e722d8a
commit 96244a4560
8 changed files with 150 additions and 108 deletions

View File

@ -83,7 +83,23 @@ class Emitter(GObject.GObject):
) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str,bool,))
@GObject.Signal(
flags=GObject.SignalFlags.RUN_LAST,
arg_types=(
str,
bool,
),
)
def check_toggled(self, label: str, state: bool) -> None:
pass
@GObject.Signal(
flags=GObject.SignalFlags.RUN_LAST,
arg_types=(
str,
bool,
),
)
def check_toggled(self, label: str, state: bool) -> None:
pass
@ -102,3 +118,15 @@ class Emitter(GObject.GObject):
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
def load_maps(self, store: ListStore) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def mod_page_loaded(self) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def mods_highlighted(self) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def api_change_failed(self) -> None:
pass

View File

@ -1,3 +1,4 @@
import inspect
import logging
import shutil
import threading
@ -93,11 +94,22 @@ class AppNavigation:
filters: "FilterPanel"
class CleanupFunc:
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 Controller(GObject.GObject):
def __init__(self) -> None:
self.dist_cache: dict[str, "Haversine", "ServerTab"] = {}
self.mediator = AppNavigation()
self.prefs: UserPrefs
self.cleanup_func: CleanupFunc = None
self.model_man = ModelManager()
self.emitter = Emitter()
@ -114,10 +126,14 @@ class Controller(GObject.GObject):
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
def callback() -> None:
func(*args, **kwargs)
GLib.idle_add(self._destroy_on_idle)
self = args[0]
self.wait_dialog = WaitDialog(self, dialog_str)
self.wait_dialog.show_all()
thread = threading.Thread(target=func, args=args)
thread = threading.Thread(target=callback)
thread.start()
return wrapper
@ -156,6 +172,7 @@ class Controller(GObject.GObject):
def unblock_signals(self) -> None:
self.block_signals(False)
@deprecated("Currently unused")
def block_signals(self, state: bool = True) -> None:
self.suppress_signal(
self.mediator.filters,
@ -280,6 +297,7 @@ class Controller(GObject.GObject):
self.emitter.emit("distcalc_ended", dist, context)
# TODO: threading
def delete_multiple_mods(self) -> None:
sel = self.mediator.modtreeview.get_selection()
model, pathlist = sel.get_selected_rows()
@ -290,10 +308,13 @@ class Controller(GObject.GObject):
total_mods, total_size = self.calc_mod_size()
self.update_mod_statusbar()
def load_mods_cleanup(self, model: Gtk.ListStore) -> None:
self.mediator.modtreeview.set_model(model)
self.emitter.emit("mod_page_loaded")
@call_on_thread(strings.dialog.modlist)
def load_mods(self) -> None:
# TODO: threading; could get slow with 100s of mods
model = self.model_man.get_mod_store()
model.clear()
model = self.model_man.new_mod_store()
path = self.query_config(Preferences.DEFAULT)
mods = get_delimited_mods(Path(path))
@ -302,6 +323,8 @@ class Controller(GObject.GObject):
mod.append(None)
model.append(mod)
self.cleanup_func = CleanupFunc(self.load_mods_cleanup, model)
def toggle_config(self, context: Preferences) -> None:
config = self.prefs.paths.config
try:
@ -484,16 +507,21 @@ class Controller(GObject.GObject):
except PeFile.AppNotInstalledError:
pass
model = self.model_man.get_mod_store()
model = self.get_mod_store()
model.remove(it)
def get_mod_store(self) -> Gtk.ListStore:
return self.mediator.modtreeview.get_model()
def format_mod_statusbar(self) -> None:
total_mods, total_size = self.calc_mod_size()
msg = format_mods(total_size, total_mods)
return msg
def calc_mod_size(self) -> tuple[int, int]:
model = self.model_man.get_mod_store()
model = self.get_mod_store()
if model is None:
return 0, 0
total_mods = len(model)
total_size = 0
for mod in model:
@ -590,7 +618,7 @@ class Controller(GObject.GObject):
self.open_page(NotebookPage.LOG)
def select_colorized(self) -> None:
model = self.model_man.get_mod_store()
model = self.get_mod_store()
sel = self.mediator.modtreeview.get_selection()
for mod in model:
it = mod.iter
@ -598,29 +626,14 @@ class Controller(GObject.GObject):
if mod[4] == HEX_RED:
sel.select_path(path)
# TODO: make as method of tree?
def uncolorize_mods(self) -> None:
model = self.model_man.get_mod_store()
model = self.get_mod_store()
for mod in model:
it = mod.iter
path = model.get_path(it)
model[path][4] = None
def colorize_mods(self) -> None:
model = self.model_man.get_mod_store()
stale = find_stale_mods(self.prefs.paths.config)
for mod in model:
it = mod.iter
path = model.get_path(it)
# TODO: consider storing in ListStore as int
# TODO: clone existing model and set outside of threai
if int(mod[2]) in stale:
model[path][4] = HEX_RED
self.destroy_on_idle()
def unselect_all_mods(self) -> None:
self.mediator.modtreeview.get_selection().unselect_all()
def dump_test_2(self) -> None:
import time
@ -810,7 +823,6 @@ class Controller(GObject.GObject):
self.new_maps = None
treeview.grab_focus()
self.destroy_on_idle()
# if self.success is False:
# # TODO: different dialogs for server tab contexts, e.g. lan timeout
# # TODO: if history/favorites is empty, don't even trigger a call to dump data
@ -818,10 +830,10 @@ class Controller(GObject.GObject):
# dialog.run()
def cleanup_on_failure(self) -> None:
# TODO: what if refresh action occurred, and the old model is valid?
treeview = self.get_active_treeview()
treeview.set_model(None)
map_man = treeview.get_map_man()
# TODO: what if refresh action occurred, and the old model is still valid?
# TODO: disable map, keyword, and filter widgets if model is None?
# -> signal driven (servers_empty)
# would have to make those unsensitive when changing server tabs
@ -832,7 +844,6 @@ class Controller(GObject.GObject):
# TODO: distinguish signals, e.g. "servers_failed_to_load"
self.emitter.emit("servers_loaded", context)
treeview.grab_focus()
self.destroy_on_idle()
dialog = ExceptionDialog(self, "API TIMEOUT")
dialog.run()
@ -841,7 +852,7 @@ class Controller(GObject.GObject):
treeview.set_loaded(True)
# TODO: wipe control model on failure or keep old results?
# manager = treeview.get_filter_man()
GLib.idle_add(self.cleanup_on_failure)
self.cleanup_func = CleanupFunc(self.cleanup_on_failure)
def push_data(self, data: tuple, mode: Optional[FilterMode], success: bool) -> None:
@ -866,38 +877,31 @@ class Controller(GObject.GObject):
self.new_maps = sorted(u_maps)
treeview.set_loaded(True)
GLib.idle_add(self.cleanup)
self.cleanup_func = CleanupFunc(self.cleanup)
@call_on_thread(strings.dialog.working)
def highlight_stale(self) -> None:
self.colorize_mods()
model = self.get_mod_store()
stale = find_stale_mods(self.prefs.paths.config)
for mod in model:
it = mod.iter
path = model.get_path(it)
# TODO: consider storing in ListStore as int
# FIXME: deep copy existing model and set outside of thread
if int(mod[2]) in stale:
model[path][4] = HEX_RED
self.cleanup_func = CleanupFunc(lambda: self.emitter.emit("mods_highlighted"))
def get_callback(self) -> Callable | None:
return self.callback["func"]
def get_cleanup_func(self) -> CleanupFunc:
return self.cleanup_func
def get_callback_args(self) -> Any:
return self.callback["args"]
def set_callback(self, callback: Callable | None, *args) -> None:
"""
Lets calling widgets register a callback function
when spawning a threaded process
"""
self.callback = {"func": callback, "args": args}
def destroy_on_idle(self) -> None:
"""
TODO: chiefly responsible for removing spinner dialog
"""
def _destroy_on_idle(self) -> None:
self.wait_dialog.destroy()
func = self.get_callback()
self.mediator.window.set_sensitive(True)
# TODO: spawn error dialog if API crawl failed
# most likely going to drop callback method
func = self.get_cleanup_func()
if func is not None:
args = self.get_callback_args()
GLib.idle_add(func, *args)
self.set_callback(None, None)
func.call()
self.cleanup_func = None
self.mediator.window.set_sensitive(True)
def dump_diagnostics(self) -> None:
picker = FilePicker(self.mediator.window)
@ -909,7 +913,8 @@ class Controller(GObject.GObject):
dialog = ExceptionDialog(self, str(e))
dialog.run()
def test_api_response(self, text: str, key: Preferences) -> None:
@call_on_thread(strings.dialog.working)
def update_api_key(self, text: str, key: Preferences) -> None:
if key is Preferences.STEAM:
res = test_steam_api(text)
else:
@ -917,17 +922,9 @@ class Controller(GObject.GObject):
if res is True:
self.update_config(key, text)
self.set_callback(None, None)
self.destroy_on_idle()
else:
self.destroy_on_idle()
dialog = ExceptionDialog(self, strings.api_error)
dialog.run()
@call_on_thread(strings.dialog.working)
# FIXME: entire process is behind thread, including destroy_on_idle()
def update_api_key(self, text: str, key: Preferences) -> None:
self.test_api_response(text, key)
# FIXME: dialog spawning in thread
self.cleanup_func = CleanupFunc(self.emitter.emit("api_change_failed"))
def set_resolution(self, window: "OuterWindow") -> None:
if self.prefs.is_game_mode:
@ -985,14 +982,12 @@ class Controller(GObject.GObject):
self.push_data("", mode, success=True)
# FIXME: optional label/map/keyword parameter
# FIXME: drop set_callback
# TODO: consolidate with method above
def refilter_model(self, mode: FilterMode, label: Optional[str] = None) -> None:
tv = self.get_active_treeview()
filter_man = tv.get_filter_man()
if filter_man.get_control() is None:
return
# TODO: deprecated in this context?
self.set_callback(None, None)
self.filter_threaded(mode, label)
def populate_model(self) -> None:
@ -1001,18 +996,15 @@ class Controller(GObject.GObject):
self.emitter.emit("servers_loaded", treeview.get_enum())
return
# TODO: maybe set tree to none afterwards (swap models)
# treeview.set_model(None)
func = treeview.get_query_func()
if func is None:
self.emitter.emit("servers_loaded", treeview.get_enum())
treeview.set_model(None)
return
# TODO:
# TODO: clear ephemeral model if necessary
# manager = treeview.get_filter_man()
# manager.clear_model()
self.first_iteration = True
self.set_callback(None, None)
self.run_query_func(func)
def get_favorite(self) -> tuple[str, str] | tuple[None, None]:
@ -1034,9 +1026,6 @@ class Controller(GObject.GObject):
def get_modlist_store(self) -> Gtk.ListStore:
return self.model_man.get_modlist_store()
def get_mod_store(self) -> Gtk.ListStore:
return self.model_man.get_mod_store()
def get_log_store(self) -> Gtk.ListStore:
return self.model_man.get_log_store()

View File

@ -47,8 +47,8 @@ class ModelManager:
self.map_store = ListStore(str)
self.help_store = self.new_model_from_class(MenuCols)
self.mod_store = self.new_model_from_class(ModCols)
self.log_store = self.new_model_from_class(LogCols)
# FIXME: needs to be generated on demand
self.modlist_store = self.new_model_from_class(ServerModCols)
for row in HELP_MENU_ROWS:
@ -65,8 +65,8 @@ class ModelManager:
def get_help_store(self) -> ListStore:
return self.help_store
def get_mod_store(self) -> ListStore:
return self.mod_store
def new_mod_store(self) -> ListStore:
return self.new_model_from_class(ModCols)
def get_log_store(self) -> ListStore:
return self.log_store

View File

@ -16,7 +16,7 @@ if TYPE_CHECKING:
class EnumeratedModButton(Gtk.Button):
def __init__(self, enum: ModButton) -> None:
super().__init__(margin_start=10, margin_end=10)
super().__init__(margin_start=10, margin_end=10, focus_on_click=False)
self.enum = enum
self.set_label(enum.dict["label"])
@ -30,6 +30,7 @@ class ModSelectionPanel(Gtk.Box):
self.controller = controller
emitter = controller.get_emitter()
emitter.connect("mod_page_toggled", self._on_mod_page_toggled)
emitter.connect("mods_highlighted", self._on_mods_highlighted)
header = BoldLabel(mod_panel.header)
@ -60,14 +61,17 @@ class ModSelectionPanel(Gtk.Box):
for el in header, self.main_panel, self.stale_panel:
self.pack_start(el, NO_EXPAND, FILL, NO_PADDING)
def _on_mods_highlighted(self, emitter: "Emitter") -> None:
self.swap_sensitive(True)
def _on_mod_page_toggled(self, emitter: "Emitter", state: bool) -> None:
self.set_visible(state)
def after_colorize(self) -> None:
# TODO: split into signal that touches two widgets,
# modtreeview and this widget
self.controller.unselect_all_mods()
self.swap_sensitive(True)
#def after_colorize(self) -> None:
# # TODO: split into signal that touches two widgets,
# # modtreeview and this widget
# #self.controller.unselect_all_mods()
# self.swap_sensitive(True)
def swap_sensitive(self, state: bool) -> None:
for child in self.stale_panel.get_children():
@ -79,15 +83,12 @@ class ModSelectionPanel(Gtk.Box):
def _on_button_clicked(self, button: EnumeratedModButton) -> None:
match button.enum:
case ModButton.SELECT_ALL:
# TODO: signals
self.controller.toggle_mod_selection(True)
case ModButton.UNSELECT_ALL:
self.controller.toggle_mod_selection(False)
case ModButton.DELETE_SELECTED:
self.controller.delete_multiple_mods()
case ModButton.HIGHLIGHT_STALE:
self.controller.set_callback(self.after_colorize)
self.controller.highlight_stale()
case ModButton.UNHIGHLIGHT_STALE:
self.controller.uncolorize_mods()

View File

@ -44,6 +44,7 @@ class RightPanel(Gtk.Box):
state = self.controller.has_server_model()
for widget in (self.refresh_button, self.filters_vbox):
widget.set_sensitive(state)
# FIXME: may wish to reload these in place to show new data
if context in (ServerTab.RECENT, ServerTab.SAVED):
self.refresh_button.set_sensitive(False)

View File

@ -51,6 +51,11 @@ class Statusbar(Gtk.Grid):
self.emitter.connect("distcalc_started", self._on_distcalc_started)
self.emitter.connect("distcalc_ended", self._on_distcalc_ended)
self.emitter.connect("servers_loaded", self._on_servers_loaded)
self.emitter.connect("mod_page_loaded", self._on_mod_page_loaded)
def _on_mod_page_loaded(self, emitter: "Emitter") -> None:
msg = self.controller.format_mod_statusbar()
self.set_by_context(NotebookPage.MODS, msg)
def _on_notebook_page_changed(
self, notebook: "Notebook", child: Gtk.Widget, index: int
@ -75,12 +80,15 @@ class Statusbar(Gtk.Grid):
match enum:
case NotebookPage.MODS:
bar = self.controller.format_mod_statusbar()
return
# FIXME: format on callback after mods load
#bar = self.controller.format_mod_statusbar()
case NotebookPage.HELP:
bar = self.controller.get_help_row()
case NotebookPage.SERVERS:
context = self.controller.get_active_context()
self.emitter.emit("servers_loaded", context)
# FIXME: may be superfluous
#self.emitter.emit("servers_loaded", context)
return
case NotebookPage.KEYS:
bar = question_to_return

View File

@ -37,6 +37,7 @@ from gi.repository import Gtk, Gdk # noqa
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
class Options(Gtk.Box):
@ -48,6 +49,8 @@ class Options(Gtk.Box):
)
self.controller = controller
emitter = controller.get_emitter()
emitter.connect("api_change_failed", self._on_api_change_failed)
self.DEFAULT_WIDTH = 1
self.DEFAULT_HEIGHT = 1
@ -270,6 +273,9 @@ class Options(Gtk.Box):
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:
@ -277,9 +283,14 @@ class Options(Gtk.Box):
self.controller.update_config(enum, value)
case Preferences.BM | Preferences.STEAM:
text = "".join(entry.get_text().split())
self.controller.set_callback(self.restore_api_text, old_text, entry)
# self.controller.set_callback(self.restore_api_text, old_text, entry)
self.controller.update_api_key(text, enum)
def _on_api_change_failed(self, emitter: "Emitter") -> None:
self.old_entry.set_text(self.old_text)
dialog = ExceptionDialog(self.controller, strings.api_error)
dialog.run()
def restore_api_text(self, text: str, entry: Gtk.Entry) -> None:
entry.set_text(text)

View File

@ -15,6 +15,7 @@ from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
logger = logging.getLogger(__name__)
@ -23,12 +24,14 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView):
def __init__(self, controller: "Controller") -> None:
super().__init__(controller, menu=ContextMenuGroup.MOD)
self.controller = controller
emitter = self.controller.get_emitter()
emitter.connect("mod_page_loaded", self._on_mod_page_loaded)
emitter.connect("mods_highlighted", self._on_mods_highlighted)
self.set_fixed_height_mode(True)
self.set_headers_visible(True)
mod_store = self.controller.get_mod_store()
self.set_model(mod_store)
self.set_model(None)
for i, column_title in enumerate(strings.mod_cols):
renderer = Gtk.CellRendererText()
@ -52,26 +55,27 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView):
self.connect("generic_treesel_changed", self._parent_selection_changed)
self.connect("button-press-event", self.present_menu)
self.connect("key-press-event", self.present_menu)
self.connect("map", self._on_map)
def _on_map(self, tree: Self) -> None:
# FIXME: placeholder logic
# identify root cause of row unfocus on init
def _on_mods_highlighted(self, emitter: "Emitter") -> None:
self.get_selection().unselect_all()
# TODO: test loading with no mods
def _on_mod_page_loaded(self, emitter: "Emitter") -> None:
self.set_cursor(0)
# def _on_mods_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None:
# # TODO: multiselect
# # if event.keyval is Gdk.KEY_space:
# # it = self.get_focused_row_iter()
# # self.get_selection().select_iter(it)
# # path = self.get_focused_row_path()
# # self.set_cursor(path)
# # return False
# self.present_menu(widget, event)
# def _on_mods_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None:
# # TODO: multiselect
# # if event.keyval is Gdk.KEY_space:
# # it = self.get_focused_row_iter()
# # self.get_selection().select_iter(it)
# # path = self.get_focused_row_path()
# # self.set_cursor(path)
# # return False
# self.present_menu(widget, event)
# def _on_mods_button_press(self, widget: Gtk.Widget, event: Gdk.EventButton) -> None:
# if event.button == 3:
# self.present_menu(widget, event)
# def _on_mods_button_press(self, widget: Gtk.Widget, event: Gdk.EventButton) -> None:
# if event.button == 3:
# self.present_menu(widget, event)
def _parent_selection_changed(self, base_class: TreeView, sel: Gtk.TreeSelection):
pass