dzgui/dzgui/controllers/mc.py
aclist 9fa2840d1b
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
chore: abstract cancel check function to controller
2026-06-05 22:39:13 +09:00

538 lines
18 KiB
Python

import logging
import threading
from typing import Any, TYPE_CHECKING, Union
import dzgui.util._json as JSON # noqa
from dzgui.const.enum import (
FilterMode,
Preferences,
NotebookPage,
ButtonType,
ContextMenu,
)
from dzgui.config.userprefs import UserPrefs
from dzgui.const.constants import APP_NAME
from dzgui.controllers.emitter import Emitter
from dzgui.managers.config import ConfigManager
from dzgui.managers.connection import ConnectionManager
from dzgui.managers.contextmenu import ContextMenuManager
from dzgui.managers.mods import ModManager
from dzgui.managers.notes import NoteManager
from dzgui.model.servers import ServerModelManager
from dzgui.util.diag import write_diagnostic
from dzgui.util.format import format_player_count
from dzgui.util.open_links import open_user_workshop, open_workshop_page
from dzgui.views.dialogs.filepicker import FilePicker
from dzgui.views.dialogs.generic import ExceptionDialog
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402
logger = logging.getLogger(APP_NAME)
if TYPE_CHECKING:
from dzgui.api.servers import Record
from dzgui.const.enum import ServerTab
from dzgui.managers.connection import Prerequisites
from dzgui.managers.filter import FilterManager
from dzgui.util.dist import Haversine
from dzgui.views.base import Notebook, Grid, OuterWindow
from dzgui.views.components.buttonbox import ContextualButton
from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.right_panel import RightPanel
from dzgui.views.components.statusbar import Statusbar
from dzgui.views.pages.options import Options
from dzgui.views.pages.preconnect import PreConnectionAssistant
from dzgui.views.pages.servers import ServerNotebook
from dzgui.views.trees.tree_log import LogTreeView
from dzgui.views.trees.tree_menu import MenuTreeView
from dzgui.views.trees.tree_mods import ModTreeView
from dzgui.views.trees.tree_servers import ServerTreeView
class AppNavigation:
window: "OuterWindow"
grid: "Grid"
right_panel: "RightPanel"
statusbar: "Statusbar"
notebook: "Notebook"
modtreeview: "ModTreeView"
menu: "MenuTreeView"
servers: "ServerNotebook"
saved: "ServerTreeView"
recent: "ServerTreeView"
lan: "ServerTreeView"
logtreeview: "LogTreeView"
filters: "FilterPanel"
preconnect: "PreConnectionAssistant"
options: "Options"
class Controller(GObject.GObject):
def __init__(self) -> None:
self.dist_cache: dict[str, "Haversine"] = {}
self.mediator = AppNavigation()
self.prefs: UserPrefs
self.emitter = Emitter()
self.emitter.connect("map_selection_changed", self._on_map_selection_changed)
self.emitter.connect("check_toggled", self._on_check_toggled)
# NOTE: suppress requests until entire UI is loaded
self.loaded = False
self.pending_jobs = 1
self.exit_event = threading.Event()
self.cancel_event = threading.Event()
self.connection_man: ConnectionManager
def get_emitter(self) -> Emitter:
return self.emitter
def register_widget(self, attr: str, widget: Gtk.Widget) -> None:
try:
setattr(self.mediator, attr, widget)
except AttributeError:
logger.critical(f"{attr} is not a valid AppNavigation attribute.")
def get_prefs(self) -> UserPrefs:
return self.prefs
def set_prefs(self, prefs: UserPrefs) -> None:
self.config_man = ConfigManager(prefs, self)
self.notes_man = NoteManager(self, prefs.paths.notes)
self.prefs = prefs
def query_config(self, key: Preferences) -> Any:
return self.config_man.lookup(key)
def is_auto_install(self) -> bool:
return bool(self.query_config(Preferences.INSTALL))
def suppress_signal(
self, owner: Any, child: Any, func_name: str, state: bool
) -> None:
func = getattr(owner, func_name)
if state:
child.handler_block_by_func(func)
else:
child.handler_unblock_by_func(func)
def get_active_context(self) -> "ServerTab":
return self.get_active_treeview().get_enum()
def get_active_treeview(self) -> "ServerTreeView":
return self.mediator.notebook.servers.get_active_treeview()
def grab_active_treeview(self) -> None:
self.get_active_treeview().grab_focus()
def save_res_and_quit(self, *args: Any) -> None:
treeview = self.get_active_treeview()
window = self.get_window()
self.config_man.save_res_and_quit(treeview, window)
def set_statusbar(self, enum: "NotebookPage | ServerTab", text: str) -> None:
self.mediator.statusbar.set_by_context(enum, text)
def remove_statusbar(self, context: "NotebookPage | ServerTab") -> None:
self.mediator.statusbar.pop(context)
def set_statusbar_dist(
self, haversine: Union["Haversine", None], enum: "ServerTab"
) -> None:
"""
NOTE: prevents race condition when server tab changed,
but allows caching the distance in the background
"""
context = self.get_active_context()
page = self.mediator.notebook.get_page_by_enum()
if page != NotebookPage.SERVERS:
self.emitter.emit("distcalc_ended", None, context)
return
if enum != context:
self.emitter.emit("distcalc_ended", None, context)
return
if haversine is None:
self.emitter.emit("distcalc_ended", None, context)
return
dist = haversine.get_rounded(self.prefs.use_miles)
self.emitter.emit("distcalc_ended", dist, context)
def toggle_config(self, key: Preferences) -> None:
# NOTE: Preferences.DIST is dynamic
if key == Preferences.DIST:
self.prefs.use_miles = not self.prefs.use_miles
self.config_man.toggle_config(key)
def update_config(self, key: Preferences, value: str) -> None:
self.config_man.update_config(key, value)
def open_keybindings(self) -> None:
notebook = self.mediator.grid.notebook
notebook.toggle_keybindings()
def focus_notebook(self) -> None:
notebook = self.mediator.grid.notebook
notebook.focus_current()
def show_developers_page(self) -> None:
self.open_page(NotebookPage.DEVELOPERS)
def open_page(self, page: NotebookPage) -> None:
self.mediator.grid.notebook.set_page_by_enum(page)
def open_page_by_button(self, button: "ContextualButton") -> None:
# TODO: consolidate methods with set_page_by_enum
match button.context:
case ButtonType.EXIT:
self.save_res_and_quit()
return
case ButtonType.OPTIONS:
try:
# TODO: where to put config file check
self.mediator.grid.notebook.settings.populate_settings()
except Exception:
return
case ButtonType.MODS:
self.load_mods()
self.open_page(button.opens)
def get_help_row(self) -> str:
return self.mediator.menu.get_row_enum()
def open_user_workshop(self, uid: str) -> None:
# NOTE: uid may contain leading zeroes, not a real integer
client = self.query_config(Preferences.CLIENT)
open_user_workshop(uid, client)
def load_mods(self) -> None:
self.mod_man = ModManager(self)
def uncolorize_mods(self) -> None:
self.mod_man.uncolorize_mods()
def highlight_stale(self) -> None:
self.mod_man.highlight_stale()
def toggle_mod_selection(self, state: bool) -> None:
self.mod_man.toggle_mod_selection(state)
def delete_mods(self) -> None:
self.mod_man.delete_mods()
def get_mod_store(self) -> Gtk.TreeModel | None:
return self.mediator.modtreeview.get_model()
def set_fav(self, name: str, record: str, simple_ip: str) -> None:
try:
self.config_man.set_fav(name, record)
self.emitter.emit("fav_server_changed", name, simple_ip)
except Exception as e:
logger.critical(e)
return
def menu_action(self, action: ContextMenu, tree: "ServerTreeView") -> None:
context_man = ContextMenuManager(tree, self)
context_man.process(action)
def populate_log(self) -> None:
log = self.prefs.paths.debug
try:
self.mediator.logtreeview.populate_log(log)
self.open_page(NotebookPage.LOG)
except Exception as e:
dialog = ExceptionDialog(self, str(e))
dialog.run()
def select_colorized(self) -> None:
self.mod_man.select_colorized()
def get_all_tree_filters(self) -> dict[str, dict[str, bool]]:
servers = self.get_servers()
all_filters: dict[str, dict[str, bool]] = {}
trees = (
servers.get_browser(),
servers.get_saved(),
servers.get_recent(),
servers.get_lan(),
)
for tree in trees:
enum = str(tree.get_enum())
filters = tree.get_filter_man().get_filters()
all_filters[enum] = filters
return all_filters
def get_filter_man(self) -> "FilterManager":
"""Each ServerTreeView has an atomic FilterManager"""
return self.get_active_treeview().get_filter_man()
def dump_diagnostics(self) -> None:
picker = FilePicker(self.mediator.window)
file = picker.pick_file()
if file is not None:
try:
write_diagnostic(self.prefs.paths.config, file)
except Exception as e:
dialog = ExceptionDialog(self, str(e))
dialog.run()
def update_api_key(self, key: Preferences, text: str) -> None:
self.config_man.update_api_key(key, text)
def set_resolution(self, window: "OuterWindow") -> None:
self.config_man.set_resolution(window)
def propagate_column_width(self, col: Gtk.TreeViewColumn) -> None:
GLib.idle_add(self.mediator.servers.update_tab_widths, col)
def refresh_tree(self) -> None:
treeview = self.get_active_treeview()
treeview.set_loaded(False)
ServerModelManager(self, treeview).refresh()
def get_player_count(self) -> str:
treeview = self.get_active_treeview()
model, control = treeview.get_model_and_control_model()
count = format_player_count(model, control)
return count
def get_statusbar(self) -> "Statusbar":
return self.mediator.statusbar
def populate_model(self, tv: "ServerTreeView") -> None:
# NOTE: skip on previously loaded tabs
if tv.is_loaded():
self.emitter.emit("servers_loaded", tv.get_enum())
return
self.mediator.statusbar.set_by_context(tv.get_enum(), "")
self.mediator.filters.button_grid.block_toggles(True)
self.populate_filter_prefs()
self.mediator.filters.button_grid.block_toggles(False)
self.emitter.emit("lan_page_initialized")
ServerModelManager(self, tv).load()
def populate_filter_prefs(self) -> None:
tv = self.get_active_treeview()
filters = self.config_man.get_filters(tv)
if filters is None:
return
filter_man = self.get_filter_man()
for filt, state in filters.items():
filter_man.set_filter(filt, state)
self.mediator.filters.set_filters(filters)
def get_dist_cache(self) -> dict[str, "Haversine"]:
return self.dist_cache
def set_dist_cache(self, addr: str, haversine: "Haversine") -> None:
self.dist_cache[addr] = haversine
def get_map_store(self) -> Gtk.ListStore:
filter_man = self.get_filter_man()
return filter_man.get_map_store()
def get_enabled_filters(self) -> dict:
filter_man = self.get_filter_man()
return filter_man.get_filters()
# FIXME: wipe maps store when changing tabs if model is none
def has_server_model(self) -> bool:
treeview = self.get_active_treeview()
proxy_man = treeview.get_proxy_man()
control = proxy_man.get_control()
if control is None:
return False
if len(control) < 1:
return False
return True
def _on_check_toggled(self, emitter: Emitter, label: str, state: bool) -> None:
filter_man = self.get_filter_man()
filter_man.set_filter(label, state)
mode = FilterMode.TOGGLE_ON if state else FilterMode.TOGGLE_OFF
ServerModelManager(self, self.get_active_treeview()).refilter(mode)
def _on_map_selection_changed(self, emitter: Emitter, selection: str) -> None:
smm = ServerModelManager(self, self.get_active_treeview())
smm.refilter(FilterMode.TOGGLE_ON)
def get_notebook(self) -> "Notebook":
return self.mediator.notebook
def get_prior_page(self) -> NotebookPage:
return self.mediator.notebook.get_prior_page()
def get_servers(self) -> "ServerNotebook":
return self.mediator.servers
def get_server_notebook(self) -> Gtk.Notebook:
return self.mediator.servers.notebook
def get_window(self) -> "OuterWindow":
return self.mediator.window
def get_menu(self) -> "MenuTreeView":
return self.mediator.menu
def has_favorites(self) -> bool:
favs = self.config_man.get_favorites()
if len(favs) < 1:
return False
return True
def get_config_man(self) -> ConfigManager:
return self.config_man
def add_by_str(self, addr: str) -> None:
saved_tree = self.get_servers().get_saved()
ServerModelManager(self, saved_tree).add_by_str(addr)
def add_by_record(self, record: "Record") -> None:
saved_tree = self.get_servers().get_saved()
ServerModelManager(self, saved_tree).add_by_record(record)
def remove_by_record(self, record: "Record") -> None:
saved_tree = self.get_servers().get_saved()
ServerModelManager(self, saved_tree).remove_by_record(record)
def add_to_history(self, row: dict[str, Any], record: "Record") -> None:
tv = self.get_servers().get_recent()
ServerModelManager(self, tv).add_to_history((row, record))
# def append_to_history(self, record: "Record") -> None:
# self.config_man.append_to_history_file(record)
def remove_from_history(self, record: "Record") -> None:
# NOTE: remove action is only possible from history tree context menu,
# i.e., the active tree
tv = self.get_active_treeview()
ServerModelManager(self, tv).remove_from_history(record)
def connect_by_str(self, addr: str) -> None:
if addr.isdigit():
config_man = self.get_config_man()
key = config_man.lookup(Preferences.BM)
self.connection_man = ConnectionManager(self)
self.connection_man.connect_by_id(int(addr), key)
else:
self.connection_man = ConnectionManager(self)
self.connection_man.connect_by_ip(addr)
def connect_by_record(self, record: "Record") -> None:
self.connection_man = ConnectionManager(self)
self.connection_man.connect_by_record(record)
def get_details(self, record: "Record") -> None:
ConnectionManager(self).query_details(record)
def get_modlist(self, record: "Record") -> None:
ConnectionManager(self).query_modlist_and_present(record)
def get_server_name(self) -> str:
tv = self.get_active_treeview()
return tv.get_name()
def open_workshop_page(self, mod: str) -> None:
cmd = self.query_config(Preferences.CLIENT)
open_workshop_page(mod, cmd)
def has_note(self) -> bool:
note = self.get_note()
if len(note) > 0:
return True
return False
def get_note_by_record(self, record: str) -> str:
return self.notes_man.get_note(record)
def get_note(self) -> str:
tv = self.get_active_treeview()
record = tv.get_record_string()
return self.notes_man.get_note(record)
def add_note(self, note: str) -> None:
tv = self.get_active_treeview()
record = tv.get_record_string()
self.notes_man.add_note(record, note)
def delete_note(self) -> None:
tv = self.get_active_treeview()
record = tv.get_record_string()
self.notes_man.delete_note(record)
def refresh_players(self, record: "Record") -> None:
treeview = self.get_active_treeview()
model, treeiter = treeview.get_selection().get_selected()
ServerModelManager(self, treeview).update_playercount(treeiter, record)
def get_active_keyword(self) -> str:
return self.get_filter_man().get_active_keyword()
def set_active_keyword(self, keyword: str) -> None:
self.get_filter_man().set_active_keyword(keyword)
def get_modtreeview(self) -> "ModTreeView":
return self.mediator.modtreeview
def get_exit_event(self) -> threading.Event:
return self.exit_event
def set_exit_event(self) -> None:
self.exit_event.set()
def set_cancel_event(self) -> None:
self.cancel_event.set()
def get_cancel_event(self) -> threading.Event:
return self.cancel_event
def clear_cancel_event(self) -> None:
self.cancel_event.clear()
def open_connection_assistant(self, prereqs: "Prerequisites") -> None:
self.open_page(NotebookPage.CONNECTION)
self.mediator.preconnect.populate(prereqs)
def set_start_tab(self) -> None:
ind = self.config_man.get_start_tab()
self.get_servers().notebook.set_current_page(ind)
def update_and_load_to_menu(self, raise_window: bool) -> None:
self.connection_man.update_and_connect(raise_window, menu_only=True)
def update_and_connect(self, raise_window: bool) -> None:
self.connection_man.update_and_connect(raise_window)
def update_status(self) -> None:
self.mediator.preconnect.mark_finished()
def get_steam_client_name(self) -> str:
return self.mediator.options.get_client_name()
def present_window(self) -> None:
self.mediator.window.present_with_time(Gdk.CURRENT_TIME)
def is_cancel_pending(self) -> bool:
if self.get_exit_event().is_set():
return True
if self.get_cancel_event().is_set():
self.clear_cancel_event()
return True
return False