dzgui/dzgui/controllers/mc.py
2026-02-15 03:05:32 +09:00

1102 lines
35 KiB
Python

import inspect
import logging
import shutil
import threading
import traceback
from typing import Optional
from warnings import deprecated
from concurrent.futures import wait, as_completed
from concurrent.futures import ThreadPoolExecutor
from functools import wraps
from pathlib import Path
from typing import Any, Callable, TYPE_CHECKING
import dzgui.api.pefile as PeFile
import dzgui.api.servers as Servers
import dzgui.util._json as JSON # noqa
from dzgui.api.probe import test_steam_api, test_bm_api
from dzgui.api.mods import (
get_delimited_mods,
get_local_mod_path,
find_stale_mods,
_hash,
remove_stale_signatures,
)
from dzgui.const.enum import (
FilterMode,
Preferences,
NotebookPage,
ButtonType,
ContextMenu,
)
from dzgui.const.constants import (
APPID_DAYZ,
APPID_DAYZ_EXP,
HEX_RED,
WINDOW_DEFAULT_X,
WINDOW_DEFAULT_Y,
)
from dzgui.config import update
from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs
from dzgui.controllers.emitter import Emitter
from dzgui.model.misc_model import ModelManager
from dzgui.util import strings
from dzgui.util.diag import write_diagnostic
from dzgui.util._json import read_json, write_json
from dzgui.util.localize import number
from dzgui.util.open_links import open_workshop_page, open_user_workshop
from dzgui.util.format import format_mods, format_player_count
from dzgui.util.redact import redact_log
from dzgui.views.dialogs.generic import ExceptionDialog, WaitDialog
from dzgui.views.dialogs.filepicker import FilePicker
import gi
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.model.map_model import MapManager
from dzgui.const.enum import ServerTab
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.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"
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()
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
def get_emitter(self) -> Emitter:
return self.emitter
def call_on_thread(dialog_str: str) -> Callable:
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, True)
self.wait_dialog.show_all()
thread = threading.Thread(target=callback)
thread.start()
return wrapper
return decorator
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 terminate_process(self) -> None:
# TODO: only used by server table multiprocessing queue
self.get_active_treeview().terminate_process()
def get_prefs(self) -> UserPrefs:
return self.prefs
def set_prefs(self, prefs: UserPrefs) -> None:
self.prefs = prefs
def query_config(self, key: Preferences) -> str | bool | list:
config = self.prefs.paths.config
return lookup(config, key)
def is_auto_install(self) -> bool:
return self.query_config(Preferences.INSTALL)
def reinit_map_store(self) -> None:
self.model_man.set_all_maps()
def append_map(self, map_row: list) -> None:
self.model_man.append_map(map_row)
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,
self.mediator.filters.maps_combo,
"_on_map_changed",
state,
)
self.suppress_signal(
self.mediator.menu,
self.mediator.menu.selected_row,
"_on_tree_selection_changed",
state,
)
self.suppress_signal(
self.mediator.menu, self.mediator.menu, "_on_keypress", state
)
for check in self.mediator.filters.checks:
self.suppress_signal(
self.mediator.filters,
check,
"_on_check_toggled",
state,
)
def suppress_signal(
self, owner: Gtk.Widget, widget: Gtk.Widget, func_name: str, state: bool
) -> None:
func = getattr(owner, func_name)
if state:
widget.handler_block_by_func(func)
else:
widget.handler_unblock_by_func(func)
def toggle_debug_mode(self) -> None:
self.toggle_config(Preferences.DEBUG)
def get_active_context(self) -> NotebookPage:
tv = self.get_active_treeview()
return tv.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()
columns = treeview.get_columns()
columns_file = self.prefs.paths.columns
try:
data = JSON.read_json(columns_file)
except Exception as e:
logger.critical(e)
data = {"cols": {}}
for column in columns:
title = column.get_title()
size = column.get_width()
data["cols"][title] = size
try:
JSON.write_json(data, columns_file)
except Exception as e:
logger.critical(e)
logger.info("Normal user exit")
if self.mediator.window.props.is_maximized:
Gtk.main_quit()
return
w, h = self.mediator.window.get_size()
data = {"res": {"width": w, "height": h}}
res_path = self.prefs.paths.resolution
try:
write_json(data, res_path)
except Exception as e:
logger.critical(e)
Gtk.main_quit()
@deprecated("use statusbar internal contexts")
def remove_statusbar(self, context: str) -> None:
c = self.mediator.statusbar.statusbar.get_context_id(context)
self.mediator.statusbar.statusbar.pop(c)
# TODO: refactor any modules using this
@deprecated("use set_by_context")
def set_statusbar(self, text: str, context: str) -> int:
msg_id = self.mediator.statusbar.set_text(text, context)
return msg_id
def set_statusbar_dist(self, haversine: "Haversine", 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
dist: str
if haversine is None:
dist = "Unknown"
else:
if self.prefs.use_miles:
raw = round(haversine.as_miles())
separated = number(raw)
dist = str(separated) + " mi"
else:
raw = round(haversine.as_kilometers())
separated = number(raw)
dist = str(separated) + " km"
self.emitter.emit("distcalc_ended", dist, context)
def delete_multiple_mods(self) -> None:
sel = self.mediator.modtreeview.get_selection()
model, pathlist = sel.get_selected_rows()
# NOTE: reverse when multiple selection
for path in reversed(pathlist):
self.delete_single_mod(path)
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:
model = self.model_man.new_mod_store()
path = self.query_config(Preferences.DEFAULT)
mods = get_delimited_mods(Path(path))
for mod in mods:
# NOTE: holds color column
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:
update.toggle_config(config, context)
# NOTE: 'use_miles' key is updated dynamically for statusbar unit
if context == Preferences.DIST:
self.prefs.use_miles = not self.prefs.use_miles
except Exception as e:
logger.critical(e)
trace = traceback.format_exc()
dialog = ExceptionDialog(self, trace)
dialog.run()
def update_config(self, key: Preferences, value: str) -> None:
try:
update.write_config(self.prefs.paths.config, key, value)
except Exception as e:
logger.critical(e)
trace = traceback.format_exc()
dialog = ExceptionDialog(self, trace)
dialog.run()
# TODO: suppress signals
# then reenable (or it spawns dialog twice)
self.mediator.grid.notebook.settings.populate_settings()
return
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:
# TODO: reload using refresh button, rather than on demand?
self.load_mods()
self.open_page(button.opens)
def dump_api(self) -> None:
key = self.query_config(Preferences.STEAM)
job = Servers.query_api
params = Servers.params
serv = []
i = 0
total = 10
with ThreadPoolExecutor() as executor:
futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params]
# wait(futures)
for future in as_completed(futures):
try:
i += 1
GLib.idle_add(lambda: self.wait_dialog.prog.set_fraction(i / total))
res = future.result(timeout=3)
if res.status != 200 or not res.parsed:
# TODO: pop warning dialog, create enum around various failure states
# set failure type here
self.cleanup_func = CleanupFunc(self.cleanup_on_failure)
return
j = res.json
serv += j["response"]["servers"]
except Exception as e:
# TODO: save exception text
print(e)
self.cleanup_func = CleanupFunc(self.cleanup_on_failure)
"""
TODO: e.g. class method like 'total'
total of 10 params for this call
@call_on_thread(msg, jobs=10)
if total > 1, pop progress bar
GLib.idle_add(lambda: self.wait_dialog.format_secondary_text("Pass 2"))
GLib.idle_add(lambda: self.wait_dialog.prog.set_fraction(0.5))
subclass or wrap add fraction and increment, then calculate off of total
This next step is allowed to fail, since this metadata is incidental
"""
res = Servers.query_api(key, APPID_DAYZ_EXP, "")
if res.status == 200 and res.parsed is True:
j = res.json
serv += j["response"]["servers"]
GLib.idle_add(
lambda: self.wait_dialog.format_secondary_text("Unpacking servers")
)
GLib.idle_add(lambda: self.wait_dialog.prog.set_fraction(10 / total))
# TODO: additional ping column pass, collated
parsed = Servers.parse_json(serv)
self.push_data_success(parsed, FilterMode.INITIAL)
def get_help_row(self) -> str:
tv = self.mediator.menu
model = self.get_help_store()
tree_iter = tv.get_focused_row_iter()
value = model.get_value(tree_iter, 1)
return value.dict["tooltip"]
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 copy_log(self, paths: list[Gtk.TreePath]) -> str:
if len(paths) < 1:
return ""
final = []
for path in paths:
record = self.log_store[path]
r = [el for el in record]
concat = strings.delimiter.join(r)
final.append(concat)
text = "\n".join(final)
return text
def get_col_value_by_path_index(self, path: Gtk.TreePath, index: int) -> Any:
treeview = self.get_active_treeview()
model = treeview.get_model()
if model is None:
return None
value = model[path][index]
return value
def copy_name(self, path: Gtk.TreePath) -> None:
# TODO: column values are deterministic, perhaps use a col name to index map
name = self.get_col_value_by_path_index(path, 0)
if name is None:
return
self.copy_clipboard(name)
def copy_ip(self, path: Gtk.TreePath) -> None:
treeview = self.get_active_treeview()
record = treeview.get_record()
self.copy_clipboard(f"{record.ip}:{record.qport}")
def copy_clipboard(self, text: str) -> None:
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.clipboard.set_text(text, -1)
def open_mod_page(self, path: Gtk.TreePath) -> None:
mod, it = self.model_man.get_mod_from_tree_path(path)
cmd = self.query_config(Preferences.CLIENT)
open_workshop_page(mod, cmd)
# TODO: put in model manager (dedicated manager for mod store)
# def get_mod_from_tree_path(
# self, tree_path: Gtk.TreePath
# ) -> tuple[str, Gtk.TreeIter]:
# model = self.model_man.get_mod_store()
# tree_iter = model.get_iter(tree_path)
# mod = model.get(tree_iter, 2)[0]
# return mod, tree_iter
def get_mod_from_tree_path(
self, tree_path: Gtk.TreePath
) -> tuple[str, Gtk.TreeIter]:
model = self.get_mod_store()
tree_iter = model.get_iter(tree_path)
mod = model.get(tree_iter, 2)[0]
return mod, tree_iter
def delete_single_mod_cleanup(self, _iter: Gtk.TreeIter) -> None:
self.get_mod_store().remove(_iter)
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version)
# TODO: strings
@call_on_thread("deleting mod")
def delete_single_mod(self, tree_path: Gtk.TreePath) -> None:
config = self.prefs.paths.config
mod, _iter = self.get_mod_from_tree_path(tree_path)
path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path)
mods_path = get_local_mod_path(steam_path)
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
md5 = _hash(mod)
symlink = app_path / md5
symlink.unlink()
shutil.rmtree(mods_path / mod)
# NOTE: second pass to unlink DAYZ_EXP mods
# TODO: test this with working APPID_DAYZ_EXP installation
try:
app_path_exp = PeFile.get_nested_app_path(steam_path, APPID_DAYZ_EXP)
symlink = app_path_exp / md5
symlink.unlink()
except PeFile.AppNotInstalledError:
pass
self.cleanup_func = CleanupFunc(self.delete_single_mod_cleanup, _iter)
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.get_mod_store()
if model is None:
return 0, 0
total_mods = len(model)
total_size = 0
for mod in model:
total_size += mod[3]
return total_mods, total_size
def set_fav(self) -> None:
treeview = self.get_active_treeview()
name = treeview.get_value_at_index(0)
record = treeview.get_record_string()
try:
self.update_config(Preferences.FAV_LBL, name)
self.update_config(Preferences.FAV_SRV, record)
except Exception:
# TODO: add a failure dialog here
return
simple_ip = treeview.get_simplified_ip()
self.emitter.emit("fav_server_changed", name, simple_ip)
def menu_action(self, action: ContextMenu, path: Gtk.TreePath) -> None:
match action:
# NOTE: manipulates server stores and caches
# add to saved servers model verbatim and sort in place
# update tab with !
# update config file with IP
# TODO: needs threading
case ContextMenu.ADD_SERVER:
pass
# spawn edit dialog and update cache, notes file
case ContextMenu.ADD_NOTE:
pass
case ContextMenu.COPY_CLIPBOARD:
self.copy_ip(path)
case ContextMenu.COPY_NAME:
self.copy_name(path)
case ContextMenu.DELETE_MOD:
self.delete_single_mod(path)
# 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()
# call a2s on thread and update ephemeral model in situ
case ContextMenu.REFRESH_PLAYERS:
pass
# update history model, update tab label, pop off of queue, write new list into file
# see dq.py
case ContextMenu.REMOVE_HISTORY:
pass
# reverse of ADD_SERVER
case ContextMenu.REMOVE_SERVER:
pass
case ContextMenu.SET_FAV:
self.set_fav()
# NOTE: spawns dedicated dialogs
# TODO: needs threading
case ContextMenu.SHOW_DETAILS:
pass
# TODO: needs threading
case ContextMenu.SHOW_MODS:
pass
case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page(path)
def toggle_mod_selection(self, state: bool) -> None:
sel = self.mediator.modtreeview.get_selection()
if state:
sel.select_all()
else:
sel.unselect_all()
self.mediator.modtreeview.grab_focus()
def populate_log(self) -> None:
log = self.prefs.paths.debug
store = self.model_man.get_log_store()
store.clear()
# NOTE: this model is reloaded each time as log changes
try:
with open(log, "r") as f:
lines = [
line.split(strings.delimiter) for line in f.read().splitlines()
]
for record in lines:
clean = redact_log(record)
store.append(clean)
except Exception as e:
dialog = ExceptionDialog(self, str(e))
dialog.run()
return
self.open_page(NotebookPage.LOG)
def select_colorized(self) -> None:
model = self.get_mod_store()
sel = self.mediator.modtreeview.get_selection()
for mod in model:
it = mod.iter
path = model.get_path(it)
if mod[4] == HEX_RED:
sel.select_path(path)
# TODO: make as method of tree?
# FIXME: move cursor when finished
def uncolorize_mods(self) -> None:
model = self.get_mod_store()
for mod in model:
it = mod.iter
path = model.get_path(it)
model[path][4] = None
def dump_test_2(self) -> None:
import time
time.sleep(1)
data = (
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
1,
1,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"172.111.51.156:2302",
1,
1,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
1,
1,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
1,
1,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
0,
0,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
0,
0,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
0,
0,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
0,
0,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
0,
0,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
0,
0,
"a",
False,
],
[
"BAR",
"chernarusplus",
"a",
"a",
1,
1,
1,
"185.207.214.16:2302",
0,
0,
"a",
False,
],
)
self.push_data_success(data, FilterMode.INITIAL)
def get_map_man(self) -> "MapManager":
return self.get_active_treeview().get_map_man()
def cleanup_on_success(self) -> None:
treeview = self.get_active_treeview()
treeview.set_loaded(True)
treeview.set_model(self.to_insert)
map_man = treeview.get_map_man()
# TODO: signals or other approach to deferring map
# model insertion after thread closes
# cf. servers_loaded signal
context = self.get_active_context()
self.emitter.emit("servers_loaded", context)
# CHORE: this is placeholder logic
if self.first_iteration:
map_man.set_unique_maps(self.new_maps)
self.emitter.emit("servers_loaded_init")
self.first_iteration = False
self.new_maps = None
treeview.grab_focus()
def cleanup_on_failure(self) -> None:
treeview = self.get_active_treeview()
treeview.set_model(None)
treeview.set_loaded(True)
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)
map_man.set_unique_maps(None)
context = self.get_active_context()
# TODO: distinguish signals, e.g. "servers_failed_to_load"
self.emitter.emit("servers_loaded", context)
treeview.grab_focus()
dialog = ExceptionDialog(self, "API TIMEOUT")
dialog.run()
# def push_data_failure(self) -> None:
# #treeview = self.get_active_treeview()
# #treeview.set_loaded(True)
# # TODO: wipe control model on failure or keep old results?
# # manager = treeview.get_filter_man()
# self.cleanup_func = CleanupFunc(self.cleanup_on_failure)
def push_data_success(self, data: tuple, mode: Optional[FilterMode]) -> None:
# FIXME: set outside of thread
treeview = self.get_active_treeview()
manager = treeview.get_filter_man()
self.to_insert = None
if data is None:
self.to_insert = None
else:
# TODO: consolidate into filter manager
if mode == FilterMode.INITIAL:
manager.set_control(data)
manager.filter(mode)
self.to_insert = manager.get_model()
# TODO: pre parse maps
u_maps = set([row[1] for row in data])
self.new_maps = sorted(u_maps)
treeview.set_loaded(True)
self.cleanup_func = CleanupFunc(self.cleanup_on_success)
def highlight_stale_cleanup(self, stale_mods: list) -> None:
model = self.get_mod_store()
for mod in model:
it = mod.iter
path = model.get_path(it)
if int(mod[2]) in stale_mods:
model[path][4] = HEX_RED
self.emitter.emit("mods_highlighted")
@call_on_thread(strings.dialog.working)
def highlight_stale(self) -> None:
stale = find_stale_mods(self.prefs.paths.config)
self.cleanup_func = CleanupFunc(self.highlight_stale_cleanup, stale)
def get_cleanup_func(self) -> CleanupFunc:
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.cleanup_func = None
self.mediator.window.set_sensitive(True)
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()
@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:
res = test_bm_api(text)
if res is True:
self.update_config(key, text)
else:
self.cleanup_func = CleanupFunc(
lambda: self.emitter.emit("api_change_failed")
)
def set_resolution(self, window: "OuterWindow") -> None:
if self.prefs.is_game_mode:
window.fullscreen()
return
elif self.query_config(Preferences.WINDOW) is True:
window.fullscreen()
try:
data = read_json(self.prefs.paths.resolution)
valid_json = True
except Exception as e:
valid_json = False
logger.critical(e)
if valid_json:
res = data["res"]
w, h = res["width"], res["height"]
logger.info(f"Restoring window size to {w},{h}")
window.set_default_size(w, h)
else:
w = WINDOW_DEFAULT_X
h = WINDOW_DEFAULT_Y
logger.info(f"Using default window size {w},{h}")
window.set_default_size(w, h)
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)
self.populate_model()
def get_player_count(self) -> str:
treeview = self.get_active_treeview()
model = treeview.get_model()
filter_man = treeview.get_filter_man()
control_model = filter_man.get_control()
count = format_player_count(model, control_model)
return count
def get_statusbar(self) -> None:
return self.mediator.statusbar
@call_on_thread(strings.dialog.fetching)
def run_query_func(self, func: Callable) -> None:
func()
@call_on_thread(strings.dialog.filtering)
def filter_threaded(self, mode: FilterMode, label: str) -> None:
# FIXME: call outside of thread
tv = self.get_active_treeview()
filter_man = tv.get_filter_man()
filter_man.filter(mode, label)
# TODO: why pushing empty data?
self.push_data_success("", mode)
# TODO: consolidate with method above
# TODO: call filter_man methods directly
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
self.filter_threaded(mode, label)
def populate_model(self) -> None:
# NOTE: prepare GTK objects outside of thread
treeview = self.get_active_treeview()
if treeview.is_loaded() is True:
self.emitter.emit("servers_loaded", treeview.get_enum())
return
func = treeview.get_query_func()
if func is None:
self.emitter.emit("servers_loaded", treeview.get_enum())
treeview.set_model(None)
return
# TODO: clear ephemeral model if necessary
# manager = treeview.get_filter_man()
# manager.clear_model()
self.first_iteration = True
self.run_query_func(func)
def get_favorite(self) -> tuple[str, str] | tuple[None, None]:
fav = str(self.query_config(Preferences.FAV_LBL))
if len(fav) < 1:
return None, None
ip = str(self.query_config(Preferences.FAV_SRV))
addr = ip.split(":")
return fav, f"{addr[0]}:{addr[2]}"
def get_dist_cache(self) -> dict[str, "Haversine"]:
return self.dist_cache
def get_help_store(self) -> Gtk.ListStore:
return self.model_man.get_help_store()
def get_modlist_store(self) -> Gtk.ListStore:
return self.model_man.get_modlist_store()
def get_log_store(self) -> Gtk.ListStore:
return self.model_man.get_log_store()
def get_filters(self) -> list:
return self.mediator.filters.get_filters()
# TODO: filterman calls back to here, gets convoluted
def get_keyword(self) -> str:
tv = self.get_active_treeview()
filter_man = tv.get_filter_man()
return filter_man.get_keyword_filter()
def get_map_store(self) -> Gtk.ListStore:
map_man = self.get_map_man()
return map_man.get_map_store()
def get_selected_map(self) -> str:
map_man = self.get_map_man()
return map_man.get_selected_map()
def get_enabled_filters(self) -> dict:
map_man = self.get_map_man()
return map_man.get_filters()
def get_prior_map(self) -> str:
map_man = self.get_map_man()
return map_man.get_prior_map()
def _on_check_toggled(self, emitter: Emitter, label: str, state: bool) -> None:
map_man = self.get_map_man()
map_man.set_filter(label, state)
if state:
mode = FilterMode.TOGGLE_ON
else:
mode = FilterMode.TOGGLE_OFF
self.refilter_model(mode, label)
# TODO: map man should be consolidated into filter man
def _on_map_selection_changed(self, emitter: Emitter, selection: str) -> None:
map_man = self.get_map_man()
map_man.set_selected_map(selection)
self.refilter_model(FilterMode.MAP)
def has_server_model(self) -> bool:
treeview = self.get_active_treeview()
return treeview.get_model() is not None