feat: server threading WIP

This commit is contained in:
aclist 2026-01-19 08:27:10 +09:00
parent 2788abb755
commit fdd6cefe48
11 changed files with 147 additions and 162 deletions

View File

@ -53,7 +53,7 @@ def concat_mods(mods: list[str]) -> str:
# TEST: set config to name=user, use official server and no mods,
# ensure that formatted string is identical to fixture
def connect(addr: str, appid: int, name: str, mods: list):
def connect(addr: str, appid: int, name: str, mods: list) -> None:
# TODO: get name from configs
# TODO: concat_mods(mods):
# @<mod>;@<mod>;

View File

@ -5,7 +5,7 @@ import textwrap
import traceback
from pathlib import Path
from typing import Any, Callable, TYPE_CHECKING
from typing import Any, Callable, Literal, TYPE_CHECKING
import dzgui.api.pefile as PeFile
import dzgui.util._json as JSON # noqa
@ -246,6 +246,7 @@ class Controller:
self.mediator.statusbar.set_text(text)
def set_statusbar_placeholder(self, text: str) -> None:
# TODO: use statusbar stacks instead
self.statusbar_placeholder = text
def set_statusbar_dist(self, haversine: "Haversine") -> None:
@ -263,6 +264,7 @@ class Controller:
dist = str(separated) + " km"
text = self.statusbar_placeholder
self.set_statusbar(f"{text} | Distance: {dist}")
self.mediator.statusbar.spinner.stop()
def delete_multiple_mods(self) -> None:
sel = self.mediator.modtreeview.get_selection()
@ -275,6 +277,7 @@ class Controller:
self.update_mod_statusbar()
def load_mods(self) -> None:
# TODO: threading
model = self.model_manager.get_mod_store()
model.clear()
path = self.query_config(Preferences.DEFAULT)
@ -310,10 +313,6 @@ class Controller:
self.mediator.grid.notebook.settings.populate_settings()
return
# NOTE: disabled for now
#def present_toast(self, text: str) -> None:
# self.mediator.window.toast.set_text_and_fade(text)
def open_keybindings(self) -> None:
notebook = self.mediator.grid.notebook
notebook.toggle_keybindings()
@ -322,23 +321,6 @@ class Controller:
notebook = self.mediator.grid.notebook
notebook.focus_current()
# TODO: deprecated
def spawn_dialog(self, msg: str, mode: Popup) -> bool:
"""
Spawns a GenericDialog transient to the OuterWindow
"""
msg = textwrap.dedent(msg)
dialog = GenericDialog(self, msg, mode)
response = dialog.run()
dialog.destroy()
match response:
case Gtk.ResponseType.OK:
return False
case Gtk.ResponseType.CANCEL | Gtk.ResponseType.DELETE_EVENT:
return True
return False
def set_statusbar_by_row(self, row: "RowType") -> None:
self.mediator.statusbar.refresh(row)
@ -366,7 +348,11 @@ class Controller:
self.save_res_and_quit()
return
case ButtonType.OPTIONS:
self.mediator.grid.notebook.settings.populate_settings()
try:
# TODO: where to put config file check
self.mediator.grid.notebook.settings.populate_settings()
except Exception:
return
case ButtonType.MODS:
self.load_mods()
case ButtonType.HELP:
@ -374,11 +360,10 @@ class Controller:
pass
case ButtonType.SERVERS:
self.mediator.notebook.set_page_by_enum(button.opens)
# TODO: use cache
self.update_server_status()
return
self.mediator.notebook.set_page_by_enum(button.opens)
# TODO: set crumbs by signal
self.set_crumbs(button.get_label())
def open_user_workshop(self, uid: str) -> None:
@ -445,7 +430,9 @@ class Controller:
def update_mod_statusbar(self) -> None:
total_mods, total_size = self.calc_mod_size()
msg = format_mods(total_size, total_mods)
# TODO: combine
self.mediator.statusbar.set_text(msg)
self.mediator.statusbar.spinner.stop()
def calc_mod_size(self) -> tuple[int, int]:
model = self.model_manager.get_mod_store()
@ -508,13 +495,16 @@ class Controller:
log = self.prefs.paths.debug
store = self.model_manager.get_log_store()
store.clear()
# TODO: pop dialog if log is missing
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)
# 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()
self.open_page(NotebookPage.LOG)
def select_colorized(self) -> None:
@ -552,20 +542,51 @@ class Controller:
import time
time.sleep(1)
# TODO: use model managers, etc.
self.data = (["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False])
self.data = (
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False],
)
# TODO: refer to prior implementation--should need to load data into model while in thread
self.get_func_data()
self.destroy_on_idle()
def get_func_data(self) -> tuple:
def test():
# NOTE: do not insert model until main thread is idle
# TODO: grab from model manager and insert entire model
# TODO: do not start refresh button count until load finished
for row in data:
model.append(row)
treeview.set_loaded(True)
self.update_server_status()
treeview.grab_focus()
# TODO: use model manager
treeview = self.get_active_treeview()
model = treeview.get_model()
data = self.data
model.append(data)
treeview.set_loaded(True)
self.update_server_status()
treeview.grab_focus()
GLib.idle_add(test)
def highlight_stale(self) -> None:
self.call_on_thread(self.colorize_mods)
@ -577,14 +598,11 @@ class Controller:
thread = threading.Thread(target=func, args=args)
thread.start()
def destroy_on_idle(self) -> None:
self.wait_dialog.destroy()
# TODO: improve upon this
func = self.callback["func"]
if func is not None:
args = self.callback["args"]
GLib.idle_add(func, *args)
self.set_callback(None, None)
def get_callback(self) -> Callable | None:
return self.callback["func"]
def get_callback_args(self) -> Any:
return self.callback["args"]
def set_callback(self, callback: Callable | None, *args) -> None:
"""
@ -593,6 +611,14 @@ class Controller:
"""
self.callback = { "func": callback, "args": args }
def destroy_on_idle(self) -> None:
self.wait_dialog.destroy()
func = self.get_callback()
if func is not None:
args = self.get_callback_args()
GLib.idle_add(func, *args)
self.set_callback(None, None)
def dump_diagnostics(self) -> None:
picker = FilePicker(self.mediator.window)
file = picker.pick_file()
@ -600,7 +626,8 @@ class Controller:
try:
write_diagnostic(self.prefs.paths.config, file)
except Exception as e:
self.spawn_dialog(str(e), Popup.NOTIFY)
dialog = ExceptionDialog(self, str(e))
dialog.run()
def test_api_response(self, text: str, key: Preferences) -> None:
if key is Preferences.STEAM:
@ -614,7 +641,8 @@ class Controller:
self.destroy_on_idle()
else:
self.destroy_on_idle()
self.spawn_dialog(strings.api_error, Popup.NOTIFY)
dialog = ExceptionDialog(self, strings.api_error)
dialog.run()
def update_api_key(self, text: str, key: Preferences) -> None:
@ -645,13 +673,6 @@ class Controller:
logger.info(f"Using default window size {w},{h}")
window.set_default_size(w, h)
def update_server_status(self) -> None:
treeview = self.mediator.notebook.servers.get_active_treeview()
model = treeview.get_model()
status = format_player_count(model)
self.set_statusbar_placeholder(status)
self.set_statusbar(status + "| Calculating...")
def propagate_column_width(self, col: Gtk.TreeViewColumn) -> None:
GLib.idle_add(self.mediator.servers.update_tab_widths, col)
@ -662,25 +683,31 @@ class Controller:
return self.crumbs_cache
def refresh_tree(self) -> None:
treeview = self.mediator.notebook.servers.get_active_treeview()
treeview = self.get_active_treeview()
treeview.set_loaded(False)
self.populate_model()
def update_server_status(self) -> None:
# TODO: emit signal on statusbar only if page changed
treeview = self.get_active_treeview()
model = treeview.get_model()
status = format_player_count(model)
self.set_statusbar_placeholder(status)
self.set_statusbar(status)
self.mediator.statusbar.spinner.start()
def populate_model(self) -> None:
print("POPULATING MODEL")
# TODO: always use same server model, store in servertreeview class
treeview = self.get_active_treeview()
if treeview.get_loaded() is False:
new_model = self.model_manager.new_model()
# NOTE: set_query_func()
func = treeview.get_query_func()
print(func)
if func is not None:
model = treeview.get_model()
print(model)
# TODO: this may lag?
model.clear()
self.set_callback(None, None)#self.get_func_data)
self.set_callback(None, None)
self.call_on_thread(func)
def focus_button_box(self) -> None:
@ -688,7 +715,7 @@ class Controller:
def present_servers(self) -> None:
# TODO: abstract
self.grab_active_treeview()
# TODO: signal for crumbs
self.update_server_status()
crumbs = self.mediator.servers.get_cached_label()
self.set_crumbs(crumbs)
@ -696,21 +723,26 @@ class Controller:
#tree = self.get_active_treeview()
#tree.emit("on_distcalc_started")
def toggle_check(self, event: Gdk.EventKey) -> None:
keyname = Gdk.keyval_name(event.keyval)
if keyname.isnumeric() and int(keyname) > 0:
digit = int(keyname) - 1
self.mediator.grid.right_panel.filters_vbox.toggle_check(digit)
else:
match event.keyval:
case Gdk.KEY_0:
self.mediator.grid.right_panel.filters_vbox.toggle_check(9)
case Gdk.KEY_minus:
self.mediator.grid.right_panel.filters_vbox.toggle_check(10)
case Gdk.KEY_backslash:
self.mediator.grid.right_panel.filters_vbox.toggle_check(11)
case _:
return False
def toggle_check(self, event: Gdk.EventKey) -> None | Literal[False]:
mappings = {
Gdk.KEY_1: 0,
Gdk.KEY_2: 1,
Gdk.KEY_3: 2,
Gdk.KEY_4: 3,
Gdk.KEY_5: 4,
Gdk.KEY_6: 5,
Gdk.KEY_7: 6,
Gdk.KEY_8: 7,
Gdk.KEY_9: 8,
Gdk.KEY_0: 9,
Gdk.KEY_minus: 10,
Gdk.KEY_backslash: 11,
}
if event.keyval not in mappings:
return False
index = mappings[event.keyval]
# TODO: register filter panel widget
self.mediator.grid.right_panel.filters_vbox.toggle_check(index)
def get_favorite(self) -> tuple[str, str] | tuple[None, None]:
fav = str(self.query_config(Preferences.FAV_LBL))
@ -723,5 +755,6 @@ class Controller:
def get_dist_cache(self) -> dict[str, "Haversine"]:
return self.dist_cache
# TODO: deprecated in favor of map/unmap
def toggle_lan_panel(self, state: bool) -> None:
self.mediator.grid.conpan.set_visible(state)

View File

@ -74,6 +74,7 @@ def get_coords(ips: "Path", ip: str) -> Coords:
if split[2] == upper_digits[2]:
if split[3] > upper_digits[3]:
continue
# TODO: handle last digit equals
if split[2] <= upper_digits[2]:
return Coords(float(fields[-2]), float(fields[-1]))

View File

@ -1,26 +1,16 @@
import json
import locale
import logging
import multiprocessing
import os
import re
#import multiprocessing
import signal
import subprocess
import textwrap
import threading
#import threading
import typing # noqa
import warnings
#from concurrent.futures import wait
#from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Literal
from dzgui.const.enum import NotebookPage, VAdjustment
from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING
from dzgui.const.enum import NotebookPage
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER
from dzgui.controllers.mc import Controller
from dzgui.util import css, strings
@ -40,21 +30,9 @@ from dzgui.views.pages.servers import ServerNotebook
from dzgui.views.pages.thanks import Thanks
from dzgui.views.components.statusbar import Statusbar
from dzgui.views.components.mod_panel import ModSelectionPanel
from dzgui.views.components.right_panel import RightPanel
from dzgui.views.components.toast import Toast
from dzgui.views.dialogs.generic import GenericDialog
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
# TREES
from dzgui.views.trees.tree_menu import MenuTreeView
from dzgui.views.trees.tree_log import LogTreeView
from dzgui.views.trees.tree_mods import ModTreeView
from dzgui.views.trees.tree_servers import ServerTreeView
# TODO: not going to be in base anymore
import dzgui.util._json as JSON # noqa
if TYPE_CHECKING:
from dzgui.config.userprefs import UserPrefs
@ -160,41 +138,6 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
# #return
#
#
#def process_user_input(enum: RowType) -> None:
# prompt = enum.dict["prompt"]
# link_label = enum.dict["link_label"]
# cmd_string = enum.dict["label"]
#
# if enum == RowType.CONN_BY_ID:
# key = MainController.query_config(Preferences.BM)
# if len(key) == 0:
# spawn_dialog(
# "No Battlemetrics API key is set; see Options", Popup.NOTIFY
# )
# return
#
# user_entry = EntryDialog(prompt, Popup.ENTRY, link_label, button_type=enum)
# response = user_entry.get_input()
#
# if response is None:
# logger.info("User aborted entry dialog")
# return
# logger.info(f"User entered: '{response}'")
#
# if enum == RowType.CONN_BY_IP:
# connect_by_ip(enum, response)
# return
#
# if enum == RowType.CONN_BY_ID:
# connect_by_id(enum, response, key)
# return
#
# show_wait_dialog = True
# wait_msg = "Working"
# call_on_thread(
# show_wait_dialog, cmd_string, wait_msg, response, choice=enum
# )
# return
class OuterWindow(Gtk.Window):
def __init__(self) -> None:
@ -216,6 +159,7 @@ class OuterWindow(Gtk.Window):
self.show_all()
css.load_css()
# TODO: first run
MainController.open_page(NotebookPage.SERVERS)
MainController.mediator.grid.conpan.lan.set_visible(False)
@ -344,7 +288,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore
try:
w.focus_first_row()
w.grab_focus()
except Exception as e:
except Exception:
w.grab_focus()
def get_page(self) -> Gtk.Widget | None:
@ -370,12 +314,6 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore
status = enum.dict["statusbar"]
MainController.set_crumbs(crumbs)
#is_mods = True if enum is NotebookPage.MODS else False
#is_servers = True if enum is NotebookPage.SERVERS else False
# TODO: use signals internal to servers.py
#MainController.toggle_mod_panel(is_mods)
#MainController.toggle_server_panels(is_servers)
if status is False:
MainController.set_statusbar("")
if enum is NotebookPage.SERVERS:
@ -409,7 +347,6 @@ class Grid(Gtk.Grid):
self.attach(self.notebook, 0, 0, MAX_COLS, SINGLE_ROW)
els = (
(self.crumb_box, self.notebook, Gtk.PositionType.TOP, MAX_COLS, SINGLE_ROW),
(self.conpan, self.notebook, Gtk.PositionType.BOTTOM, MAX_COLS, SINGLE_ROW),

View File

@ -13,7 +13,8 @@ class ContextualButton(Gtk.Button):
def __init__(self, label, opens, tooltip, context):
super().__init__(
label=label,
tooltip_text=tooltip
tooltip_text=tooltip,
focus_on_click=False,
)
self.context = context

View File

@ -1,7 +1,5 @@
from typing import TYPE_CHECKING
from dzgui.api.servers import validate_ip
from dzgui.util.css import add_class, remove_class
from dzgui.util.strings import connect_panel, fav_panel, lan_panel
from dzgui.views.components.buttons import AddButton, ClipboardButton, SteamConnectButton
from dzgui.views.components.entry import IpEntry, PortEntry
@ -63,7 +61,8 @@ class FavPanel(Gtk.Frame):
user_fav, self.fav_ip = self.controller.get_favorite()
server_name = f"{user_fav} ({self.fav_ip})" if user_fav is not None else connect_panel.no_fav
self.fav_label = Gtk.Label(label=server_name, track_visited_links=False, halign=Gtk.Align.START, hexpand=True)
scrollable_label = Gtk.ScrolledWindow()
# NOTE: disable vscrollbar to prevent layout jumping behavior
scrollable_label = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
scrollable_label.add(self.fav_label)
self.fav_button = SteamConnectButton()

View File

@ -10,32 +10,40 @@ from gi.repository import Gtk # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
class Statusbar(Gtk.Statusbar):
class Statusbar(Gtk.Grid):
def __init__(self, controller: "Controller") -> None:
super().__init__()
super().__init__(orientation=Gtk.Orientation.HORIZONTAL)
self.controller = controller
self.controller.register_widget("statusbar", self)
help_text = strings.statusbar_helptext
self.set_text(help_text)
self.statusbar = Gtk.Statusbar()
self.spinner = Gtk.Spinner()
self.spinner.start()
version = self.controller.get_prefs().version
self.status_right_label = Gtk.Label(label=version)
self.add(self.status_right_label)
self.status_right_label = Gtk.Label(label=version, hexpand=True, halign=Gtk.Align.END)
self.attach(self.statusbar, 0, 0, 3, 1)
self.attach_next_to(self.spinner, self.statusbar, Gtk.PositionType.RIGHT, 3, 1)
self.attach_next_to(self.status_right_label, self.spinner, Gtk.PositionType.RIGHT, 3, 1)
self.set_text(help_text)
self.players = ""
def get_text(self) -> str:
area = self.get_message_area()
area = self.statusbar.get_message_area()
label = area.get_children()[0]
return label.get_text()
def set_text(self, string: str) -> None:
if string is None:
return
meta = self.get_context_id("Statusbar")
self.push(meta, string)
meta = self.statusbar.get_context_id("Statusbar")
self.statusbar.push(meta, string)
def refresh(self, row: "RowType") -> None:
if row is None:

View File

@ -9,9 +9,10 @@ from dzgui.util import strings, css, open_links
from dzgui.views.components.label import LeftLabel
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.buttons import WebButton
from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.dialogs.link_dialog import WorkshopLinkDialog
from dzgui.const.enum import Preferences, Popup
from dzgui.const.enum import Preferences
from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP
from dzgui.const.constants import (
APPID_DAYZ,
@ -427,7 +428,10 @@ class Options(Gtk.Box):
prefs = self.controller.get_prefs()
if prefs.paths.config.is_file() is False:
# NOTE: in case file got deleted locally
self.controller.spawn_dialog(strings.config_not_found, Popup.QUIT)
dialog = ExceptionDialog(self.controller, strings.config_not_found)
dialog.run()
raise Exception
# FIXME: return to main menu
return
config = query.get_config(prefs.paths.config)

View File

@ -51,7 +51,7 @@ class ServerNotebook(Gtk.ScrolledWindow):
pass
else:
pass
# TODO: set model on init of servertreeview
# TODO: set model only on init of servertreeview
# TODO: set model manager for each tab on init
# TODO: add set_ and get_model_manager() methods
tree.set_model(store)

View File

@ -88,8 +88,10 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
) -> None:
if is_navkey(event.keyval):
# TODO: investigate this
# FIXME: will fail on mod list
tv = self.controller.get_active_treeview()
if len(tv.get_model()) < 1:
return
if self.sel_blocked is False:
self.controller.suppress_signal(
tv,

View File

@ -1,6 +1,7 @@
import logging
import multiprocessing
import subprocess
from typing import Self
import gi
gi.require_version("Gtk", "3.0")
@ -145,7 +146,6 @@ class ServerTreeView(TreeView):
self.controller.mediator.grid.conpan.lan.set_visible(True)
def _on_unmap(self, a) -> None:
# TODO: could be done just when changing pages?
if self.get_enum() is ServerTab.LAN:
self.controller.mediator.grid.conpan.lan.set_visible(False)
@ -199,7 +199,7 @@ class ServerTreeView(TreeView):
if self.current_proc and self.current_proc.is_alive():
self.current_proc.terminate()
def _on_distcalc_started(self, treeview):
def _on_distcalc_started(self, treeview: Self):
record = self.get_record()
if record is None:
return