chore: move signal emission into ServerTreeView

This commit is contained in:
aclist committed 2026-02-22 04:04:37 +09:00
1 parent 99ede36a32
commit 68cd567249
7 files changed
+90 -78

No files matched your search

+10 -19
View File
@@ -112,6 +112,8 @@ class Controller(GObject.GObject):
self.prefs: UserPrefs self.prefs: UserPrefs
self.cleanup_func: StoredFunc = None self.cleanup_func: StoredFunc = None
#self.config_man = ConfigManager()
self.emitter = Emitter() self.emitter = Emitter()
self.emitter.connect("map_selection_changed", self._on_map_selection_changed) self.emitter.connect("map_selection_changed", self._on_map_selection_changed)
self.emitter.connect("check_toggled", self._on_check_toggled) self.emitter.connect("check_toggled", self._on_check_toggled)
@@ -150,12 +152,15 @@ class Controller(GObject.GObject):
logger.critical(f"{attr} is not a valid AppNavigation attribute.") logger.critical(f"{attr} is not a valid AppNavigation attribute.")
def get_prefs(self) -> UserPrefs: def get_prefs(self) -> UserPrefs:
# return self.config_man.get_prefs()
return self.prefs return self.prefs
def set_prefs(self, prefs: UserPrefs) -> None: def set_prefs(self, prefs: UserPrefs) -> None:
# self.config_man.set_prefs()
self.prefs = prefs self.prefs = prefs
def query_config(self, key: Preferences) -> str | bool | list: def query_config(self, key: Preferences) -> str | bool | list:
# return self.config_man.lookup(key)
config = self.prefs.paths.config config = self.prefs.paths.config
return lookup(config, key) return lookup(config, key)
@@ -201,6 +206,7 @@ class Controller(GObject.GObject):
widget.handler_unblock_by_func(func) widget.handler_unblock_by_func(func)
def toggle_debug_mode(self) -> None: def toggle_debug_mode(self) -> None:
# self.config_man().toggle_config(Preferences.DEBUG)
self.toggle_config(Preferences.DEBUG) self.toggle_config(Preferences.DEBUG)
def get_active_context(self) -> NotebookPage: def get_active_context(self) -> NotebookPage:
@@ -217,6 +223,7 @@ class Controller(GObject.GObject):
treeview = self.get_active_treeview() treeview = self.get_active_treeview()
columns = treeview.get_columns() columns = treeview.get_columns()
#columns_file = self.config_man.get_columns()
columns_file = self.prefs.paths.columns columns_file = self.prefs.paths.columns
try: try:
data = JSON.read_json(columns_file) data = JSON.read_json(columns_file)
@@ -242,6 +249,7 @@ class Controller(GObject.GObject):
w, h = self.mediator.window.get_size() w, h = self.mediator.window.get_size()
data = {"res": {"width": w, "height": h}} data = {"res": {"width": w, "height": h}}
#res_path = self.config_man.get_resolution()
res_path = self.prefs.paths.resolution res_path = self.prefs.paths.resolution
try: try:
write_json(data, res_path) write_json(data, res_path)
@@ -279,6 +287,7 @@ class Controller(GObject.GObject):
if haversine is None: if haversine is None:
dist = "Unknown" dist = "Unknown"
else: else:
# self.config_man.use_miles()
if self.prefs.use_miles: if self.prefs.use_miles:
raw = round(haversine.as_miles()) raw = round(haversine.as_miles())
separated = number(raw) separated = number(raw)
@@ -651,25 +660,6 @@ class Controller(GObject.GObject):
model[path][4] = None model[path][4] = None
self.mediator.modtreeview.set_cursor(0) self.mediator.modtreeview.set_cursor(0)
def dump_test_2(self) -> None:
data = (
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
1,
1,
"a",
False,
],
)
self.push_data_success(data, FilterMode.INITIAL)
def get_map_man(self) -> "MapManager": def get_map_man(self) -> "MapManager":
return self.get_active_treeview().get_map_man() return self.get_active_treeview().get_map_man()
@@ -928,6 +918,7 @@ class Controller(GObject.GObject):
self.emitter.emit("load_maps", store) self.emitter.emit("load_maps", store)
def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None: def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None:
return
# NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None # NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None
# TODO: this should be internal to servers page # TODO: this should be internal to servers page
state = self.has_server_model() state = self.has_server_model()
+9 -1
View File
@@ -3,7 +3,7 @@ import logging
import threading import threading
from functools import wraps from functools import wraps
from typing import Callable from typing import Callable, Optional
from dzgui.views.dialogs.generic import WaitDialog from dzgui.views.dialogs.generic import WaitDialog
@@ -49,6 +49,7 @@ class ThreadingManager:
self.parent = parent self.parent = parent
self.jobs = 1 self.jobs = 1
self.cleanup_func = None self.cleanup_func = None
self.alternate_statusbar = None
def call_on_thread(self, dialog_str: str, func: StoredFunc) -> None: def call_on_thread(self, dialog_str: str, func: StoredFunc) -> None:
def callback() -> None: def callback() -> None:
@@ -69,6 +70,13 @@ class ThreadingManager:
def increment_dialog_with_str(self, text: str) -> None: def increment_dialog_with_str(self, text: str) -> None:
GLib.idle_add(lambda: self.wait_dialog.increment(text)) GLib.idle_add(lambda: self.wait_dialog.increment(text))
# TODO: this should not be delegated here
def set_alternate_statusbar(self, msg: str) -> None:
self.alternate_statusbar = msg
def get_alternate_statusbar(self) -> Optional[str]:
return self.alternate_statusbar
def set_cleanup_func(self, func: StoredFunc) -> None: def set_cleanup_func(self, func: StoredFunc) -> None:
if type(func) not in (StoredFunc, type(None)): if type(func) not in (StoredFunc, type(None)):
msg = f"Callback function '{func}' is not of type StoredFunc or None" msg = f"Callback function '{func}' is not of type StoredFunc or None"
+38 -41
View File
@@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
import logging import logging
import threading
from concurrent.futures import wait, as_completed from concurrent.futures import wait, as_completed
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@@ -21,6 +22,8 @@ import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib # noqa E402 from gi.repository import Gtk, GLib # noqa E402
LAN_TIMEOUT = 0.5
API_TIMEOUT = 3
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
@@ -29,6 +32,10 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# TODO: failure: spawns error dialog
# TODO: non failure with empty model: updates statusbar
class ServerModelManager: class ServerModelManager:
def __init__(self, controller: "Controller", tv: Gtk.TreeView, first_iteration=False) -> None: def __init__(self, controller: "Controller", tv: Gtk.TreeView, first_iteration=False) -> None:
@@ -37,6 +44,7 @@ class ServerModelManager:
self.emitter = controller.get_emitter() self.emitter = controller.get_emitter()
if tv.is_loaded(): if tv.is_loaded():
self.emitter.emit("servers_loaded", enum) self.emitter.emit("servers_loaded", enum)
return
self.tv = tv self.tv = tv
self.jobs = 1 self.jobs = 1
@@ -61,7 +69,8 @@ class ServerModelManager:
# TODO: get row count # TODO: get row count
self._dump_history() self._dump_history()
case ServerTab.LAN: case ServerTab.LAN:
self._dump_lan() return
#self._dump_lan()
@call_on_thread(dialog.fetching) @call_on_thread(dialog.fetching)
def _dump_api(self) -> None: def _dump_api(self) -> None:
@@ -74,7 +83,7 @@ class ServerModelManager:
for future in as_completed(futures): for future in as_completed(futures):
try: try:
self.thread_man.increment_dialog() self.thread_man.increment_dialog()
res = future.result(timeout=3) res = future.result(timeout=API_TIMEOUT)
if res.status != 200 or not res.parsed: if res.status != 200 or not res.parsed:
self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure)) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure))
return return
@@ -92,15 +101,12 @@ class ServerModelManager:
j = res.json j = res.json
servers += j["response"]["servers"] servers += j["response"]["servers"]
# TODO: strings # TODO: try/except when parsing
#self.thread_man.increment_dialog_with_str("Unpacking servers")
# TODO: try/except
parsed = Servers.parse_json(servers) parsed = Servers.parse_json(servers)
self._push_data_success(parsed, FilterMode.INITIAL) self._push_data(parsed, FilterMode.INITIAL)
# TODO: strings @call_on_thread(dialog.scanning)
@call_on_thread("scanning LAN ports") def dump_lan(self, port: int, early_abort: bool) -> None:
def _dump_lan(self, port: int, early_abort: bool) -> None:
servers = [] servers = []
ports = range(1, 256) ports = range(1, 256)
@@ -111,24 +117,24 @@ class ServerModelManager:
] ]
for future in as_completed(futures): for future in as_completed(futures):
try: try:
res = future.result(timeout=0.5) res = future.result(timeout=LAN_TIMEOUT)
if res is not None and early_abort is True: if res is not None and early_abort is True:
# NOTE: first non-empty hit, flag pending threads to close # NOTE: first non-empty hit, flag pending threads to close
event.set() event.set()
servers.append(res) servers.append(res)
self.cleanup_func = StoredFunc(self._cleanup_on_success) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success))
return return
if res is None: if res is None:
continue continue
servers.append(res) servers.append(res)
except Exception as e: except Exception as e:
logger.critical(e) logger.critical(e)
self.cleanup_func = StoredFunc(self._cleanup_on_failure) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure))
if len(servers) == 0: if len(servers) == 0:
self.cleanup_func = StoredFunc(self._cleanup_on_failure) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure))
return return
parsed = Servers.parse_json(servers) parsed = Servers.parse_json(servers)
self._push_data_success(parsed, FilterMode.INITIAL) self._push_data(parsed, FilterMode.INITIAL)
# TODO: strings # TODO: strings
@call_on_thread("dumping ips") @call_on_thread("dumping ips")
@@ -136,10 +142,11 @@ class ServerModelManager:
# NOTE: block malformed records (TODO: add github issue no.) # NOTE: block malformed records (TODO: add github issue no.)
# TODO: sanitize ip list at config time, drop this # TODO: sanitize ip list at config time, drop this
ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""] ips = [ip for ip in ips if len(ip.split(":")) == 3 and ip.split(":")[2] != ""]
job = Servers.query_direct
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
futures = [ futures = [
executor.submit( executor.submit(
Servers.query_direct, job,
ip.split(":")[0], ip.split(":")[0],
int(ip.split(":")[2]), int(ip.split(":")[2]),
) )
@@ -154,12 +161,11 @@ class ServerModelManager:
continue continue
servers.append(res) servers.append(res)
if len(servers) == 0: if len(servers) == 0:
self.cleanup_func = StoredFunc(self._cleanup_on_failure) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure))
return return
# NOTE: 1 extra progress bar pass for parsing
parsed = Servers.parse_json(servers) parsed = Servers.parse_json(servers)
self._push_data_success(parsed, FilterMode.INITIAL) self._push_data(parsed, FilterMode.INITIAL)
def _query_ip_id(self, addr: str) -> None: def _query_ip_id(self, addr: str) -> None:
# NOTE: Battlemetrics # NOTE: Battlemetrics
@@ -177,7 +183,8 @@ class ServerModelManager:
# TODO: strings # TODO: strings
@call_on_thread("querying address") @call_on_thread("querying address")
def _connect_by_id_or_ip(self, addr: str) -> None: def _connect_by_id_or_ip(self, addr: str) -> None:
res = self.query_ip_id(addr) res = self._query_ip_id(addr)
# TODO: unimplemented
@call_on_thread("querying address") @call_on_thread("querying address")
def _add_by_id_or_ip(self, addr: str) -> None: def _add_by_id_or_ip(self, addr: str) -> None:
@@ -193,7 +200,8 @@ class ServerModelManager:
# NOTE: this can be called from other tabs--if current focus is not ServerTab.SAVED, update label only # NOTE: this can be called from other tabs--if current focus is not ServerTab.SAVED, update label only
# TODO: saved servers might not be loaded yet, in which case should just update local file only # TODO: saved servers might not be loaded yet, in which case should just update local file only
# TODO: perform simple equality comparison of self.tv.get_enum() == ServerTab.SAVED # TODO: perform simple equality comparison of self.tv.get_enum() == ServerTab.SAVED
# FIXME: filter man is saved on a per tab basis, so this will mismatch # FIXME: filter man is saved on a per tab basis, so this will mismatch
# check if servers.get_active_treeview() is same as self.tv
self.set_cleanup_func(StoredFunc(self._cleanup_on_insert)) self.set_cleanup_func(StoredFunc(self._cleanup_on_insert))
def _dump_history(self) -> None: def _dump_history(self) -> None:
@@ -203,10 +211,10 @@ class ServerModelManager:
with open(history, "r") as f: with open(history, "r") as f:
rows = [row.rstrip("\n") for row in f] rows = [row.rstrip("\n") for row in f]
except OSError: except OSError:
self.cleanup_func = StoredFunc(self._cleanup_on_failure, False) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure, False))
return return
if len(rows) == 0: if len(rows) == 0:
self.cleanup_func = StoredFunc(self._cleanup_on_failure, False) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure, False))
return return
self.thread_man.set_job_count(len(rows)) self.thread_man.set_job_count(len(rows))
self._dump_ips(rows) self._dump_ips(rows)
@@ -218,7 +226,7 @@ class ServerModelManager:
if len(ips) == 0: if len(ips) == 0:
# FIXME: this is not a failure, just a quiet exit with custom statusbar # FIXME: this is not a failure, just a quiet exit with custom statusbar
# TODO: add custom statusbar parameters # TODO: add custom statusbar parameters
self.cleanup_func = StoredFunc(self._cleanup_on_failure, False) self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_failure, False))
return return
self._dump_ips(ips) self._dump_ips(ips)
@@ -243,17 +251,10 @@ class ServerModelManager:
self.emitter.emit("servers_loaded", context) self.emitter.emit("servers_loaded", context)
def _cleanup_on_success(self) -> None: def _cleanup_on_success(self) -> None:
self.pending_jobs = 1
self.tv.set_loaded(True)
self.tv.set_model(self.to_insert) self.tv.set_model(self.to_insert)
# TODO: this will allow history and saved tab to emit signals to statusbar # inserting a row serializes file on disk, updates control model for that tab, and updates model
# CHORE: test if treeview's sort method inserts row at the correct index # NOTE: when inserting new rows, the entire control model is wiped and rebuilt, then proxy model is swapped in
# inserting a row serializes file on disk, updates control model for that tab, and
# reapplies filters to ephemeral model; since filters are applied, in-situ insertion might not be necessary
# TODO: will be inserted out of order
#self.to_insert.connect("row-inserted", lambda *args: print("row inserted into model"))
# TODO: signals or other approach to deferring map # TODO: signals or other approach to deferring map
# model insertion after thread closes # model insertion after thread closes
# cf. servers_loaded signal # cf. servers_loaded signal
@@ -262,7 +263,8 @@ class ServerModelManager:
context = self.tv.get_enum() context = self.tv.get_enum()
self.emitter.emit("servers_loaded", context) self.emitter.emit("servers_loaded", context)
# CHORE: this is placeholder logic # CHORE: this is placeholder logic,
# refills map combo with new maps
if self.first_iteration: if self.first_iteration:
map_man = self.tv.get_map_man() map_man = self.tv.get_map_man()
map_man.set_unique_maps(self.new_maps) map_man.set_unique_maps(self.new_maps)
@@ -270,14 +272,11 @@ class ServerModelManager:
self.first_iteration = False self.first_iteration = False
self.new_maps = None self.new_maps = None
self.tv.grab_focus()
def _cleanup_on_failure(self, show_dialog=True) -> None: def _cleanup_on_failure(self, show_dialog=True) -> None:
self.treeview.set_loaded(True)
map_man = self.treeview.get_map_man() map_man = self.treeview.get_map_man()
# TODO: disable map, keyword, and filter widgets if model is None # TODO: disable map, keyword, and filter widgets if model is None
# -> signal driven (servers_empty) # -> signal driven (servers_empty, servers_failed_to_load)
# TODO: what if refresh action occurred and failed, and the old model is still valid? # TODO: what if refresh action occurred and failed, and the old model is still valid?
# skip the step below if refresh action failed # skip the step below if refresh action failed
# do not wipe control model in this case # do not wipe control model in this case
@@ -286,8 +285,6 @@ class ServerModelManager:
# wipe refresh state to False # wipe refresh state to False
self.treeview.set_model(None) self.treeview.set_model(None)
self.treeview.grab_focus()
map_man.set_unique_maps(None) map_man.set_unique_maps(None)
context = self.treeview.get_enum() context = self.treeview.get_enum()
@@ -300,7 +297,7 @@ class ServerModelManager:
# TODO: break into initial dump and refilter modes, can drop filtermode kwarg # TODO: break into initial dump and refilter modes, can drop filtermode kwarg
# and stop pushing empty data # and stop pushing empty data
def _push_data_success(self, data: tuple, mode: Optional[FilterMode]) -> None: def _push_data(self, data: tuple, mode: Optional[FilterMode]) -> None:
# FIXME: calls treeview read methods in thread # FIXME: calls treeview read methods in thread
# treeview = self.get_active_treeview() # treeview = self.get_active_treeview()
# manager = treeview.get_filter_man() # manager = treeview.get_filter_man()
@@ -313,7 +310,7 @@ class ServerModelManager:
manager.set_control(data) manager.set_control(data)
manager.filter(mode) manager.filter(mode)
self.to_insert = manager.get_proxy_model() self.to_insert = manager.get_proxy_model()
# TODO: pre parse maps
u_maps = set([row[1] for row in data]) u_maps = set([row[1] for row in data])
self.new_maps = sorted(u_maps) self.new_maps = sorted(u_maps)
+2
View File
@@ -234,6 +234,7 @@ class Dialog:
modlist: str modlist: str
working: str working: str
updating_mods: str updating_mods: str
scanning: str
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
@@ -429,6 +430,7 @@ dialog = Dialog(
modlist="Fetching modlist", modlist="Fetching modlist",
working="Working", working="Working",
updating_mods="Updating mods", updating_mods="Updating mods",
scanning="Scanning LAN ports",
) )
buttons = Button( buttons = Button(
+4 -1
View File
@@ -1,5 +1,6 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.model.servers import ServerModelManager
from dzgui.util.strings import connect_panel, fav_panel, lan_panel from dzgui.util.strings import connect_panel, fav_panel, lan_panel
from dzgui.views.components.buttons import ( from dzgui.views.components.buttons import (
AddButton, AddButton,
@@ -75,7 +76,9 @@ class LanPanel(Gtk.Frame):
else: else:
port = self.entry.get_text() port = self.entry.get_text()
abort = self.early_abort.get_active() abort = self.early_abort.get_active()
self.controller.dump_lan(int(port), abort) smm = ServerModelManager(self.controller, self.controller.get_active_treeview())
smm.dump_lan(int(port), abort)
#self.controller.dump_lan(int(port), abort)
def _on_radio_toggled(self, button: Gtk.RadioButton) -> None: def _on_radio_toggled(self, button: Gtk.RadioButton) -> None:
state = button.get_active() state = button.get_active()
+26 -2
View File
@@ -55,6 +55,18 @@ class ServerNotebook(Gtk.ScrolledWindow):
self.connect("map", self._on_map) self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap) self.connect("unmap", self._on_unmap)
self.emitter.connect("servers_loaded", self._on_servers_loaded)
def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None:
# NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None
# TODO: this should be internal to servers page
tv = self.get_active_treeview()
state = False if tv.get_model() is None else True
tv.set_headers_visible(state)
tv.set_headers_clickable(state)
tv.set_loaded(True)
tv.grab_focus()
def _on_map(self, widget: Self) -> None: def _on_map(self, widget: Self) -> None:
self.emitter.emit("server_page_toggled", True) self.emitter.emit("server_page_toggled", True)
# FIXME: only applies to server notebook, not atomic page # FIXME: only applies to server notebook, not atomic page
@@ -137,5 +149,17 @@ class ServerNotebook(Gtk.ScrolledWindow):
tab, col, "_on_col_width_changed", False tab, col, "_on_col_width_changed", False
) )
def get_tabs(self) -> tuple: # def get_tabs(self) -> tuple:
return (self.browser, self.saved, self.recent, self.lan) # return (self.browser, self.saved, self.recent, self.lan)
def get_browser(self) -> ServerTreeView:
return self.browser
def get_saved(self) -> ServerTreeView:
return self.saved
def get_recent(self) -> ServerTreeView:
return self.recent
def get_lan(self) -> ServerTreeView:
return self.lan
+1 -14
View File
@@ -45,7 +45,6 @@ class ServerTreeView(ContextMixin, TreeView):
self.enum = enum self.enum = enum
self.loaded = False self.loaded = False
self.query_func: Callable = None
self.filter_man = FilteredModelManager(controller) self.filter_man = FilteredModelManager(controller)
model = self.filter_man.get_proxy_model() model = self.filter_man.get_proxy_model()
@@ -59,12 +58,7 @@ class ServerTreeView(ContextMixin, TreeView):
self.queue_id: int self.queue_id: int
self.handler_id: int self.handler_id: int
self.query_func_jobs = 1
self.current_proc = None
self.queue = Queue() self.queue = Queue()
# self.queue = multiprocessing.Queue()
prefs = self.controller.get_prefs() prefs = self.controller.get_prefs()
columns = prefs.paths.columns columns = prefs.paths.columns
@@ -129,6 +123,7 @@ class ServerTreeView(ContextMixin, TreeView):
self.connect("map", self._on_map) self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap) self.connect("unmap", self._on_unmap)
# TODO: why is this being saved?
self.thread = None self.thread = None
def start_timeout(self) -> None: def start_timeout(self) -> None:
@@ -184,14 +179,6 @@ class ServerTreeView(ContextMixin, TreeView):
if self.get_enum() is ServerTab.LAN: if self.get_enum() is ServerTab.LAN:
self.emitter.emit("lan_tab_toggled", False) self.emitter.emit("lan_tab_toggled", False)
def set_query_func(self, func: "StoredFunc", jobs: int = 1) -> None:
self.query_func = func
self.query_func_jobs = jobs
# TODO: possibly split this up
def get_query_func(self) -> tuple["StoredFunc", int]:
return self.query_func, self.query_func_jobs
def _on_col_width_changed( def _on_col_width_changed(
self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt
) -> None: ) -> None: