feat: reimplement details dialog

This commit is contained in:
aclist committed 2026-03-30 21:42:19 +09:00
1 parent ae291eff41
commit 4022b4c6b6
11 files changed
+125 -88

No files matched your search

+10 -6
View File
@@ -234,6 +234,7 @@ class Ping:
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class Details: class Details:
name: Union[str, None]
data: Union[list, None] data: Union[list, None]
description: str description: str
success: bool success: bool
@@ -272,22 +273,25 @@ def get_prereqs(ip: str, qport: int) -> Prereqs:
return Prereqs(is_password, gameport, appid, version) return Prereqs(is_password, gameport, appid, version)
def details(ip: str, qport: int) -> Details: def details(record: Record) -> Details:
ip = record.ip
qport = record.qport
default_str = strings.none_provided default_str = strings.none_provided
try: try:
info = a2s.info((ip, qport)) info = a2s.info((ip, qport))
name = info.server_name
except TimeoutError: except TimeoutError:
return Details(None, default_str, False) return Details(None, None, default_str, False)
try: try:
rules = dayzquery.dayz_rules((ip, int(qport))) rules = dayzquery.dayz_rules((ip, qport))
except TimeoutError: except TimeoutError:
return Details(None, default_str, False) return Details(None, None, default_str, False)
try: try:
keywords = info.keywords.split(",") keywords = info.keywords.split(",")
except AttributeError: except AttributeError:
return Details(None, default_str, False) return Details(None, None, default_str, False)
battleye = strings.disabled battleye = strings.disabled
if "battleye" in keywords: if "battleye" in keywords:
@@ -362,7 +366,7 @@ def details(ip: str, qport: int) -> Details:
["Version", version], ["Version", version],
] ]
return Details(rows, description, True) return Details(name, rows, description, True)
def ping(iteration: int, addr: list, qport: int, ping: int) -> Ping: def ping(iteration: int, addr: list, qport: int, ping: int) -> Ping:
+5
View File
@@ -449,6 +449,7 @@ class Controller(GObject.GObject):
model[path][4] = True model[path][4] = True
self.emitter.emit("mods_highlighted") self.emitter.emit("mods_highlighted")
# TODO: delegate to mod manager
@call_on_thread(strings.dialog.working) @call_on_thread(strings.dialog.working)
def highlight_stale(self) -> None: def highlight_stale(self) -> None:
# TODO: set progress bar for number of mods # TODO: set progress bar for number of mods
@@ -609,4 +610,8 @@ class Controller(GObject.GObject):
ConnectionManager(self).connect_by_ip(addr) ConnectionManager(self).connect_by_ip(addr)
def connect_by_record(self, record: "Record") -> None: def connect_by_record(self, record: "Record") -> None:
# NOTE: calls on thread
ConnectionManager(self).connect_by_record(record) ConnectionManager(self).connect_by_record(record)
def get_details(self, record: "Record") -> None:
ConnectionManager(self).query_details(record)
+37 -8
View File
@@ -2,9 +2,9 @@ from typing import TYPE_CHECKING
import dzgui.api.servers as Servers import dzgui.api.servers as Servers
from dzgui.managers.thread_man import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.thread_man import call_on_thread, StoredFunc, ThreadingManager
from dzgui.util.strings import api_warn_msg, dialog from dzgui.util.strings import api_warn_msg, dialog, server_timeout
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.dialogs.server_details import ServerDetailsDialog
import gi import gi
@@ -25,28 +25,57 @@ class ConnectionManager:
def connect_by_id(self, addr: str, key: str) -> None: def connect_by_id(self, addr: str, key: str) -> None:
res = Servers.query_by_id(addr, key) res = Servers.query_by_id(addr, key)
if res is None: if res is None:
self.thread_man.set_cleanup_func(StoredFunc(self._connection_failure)) self.thread_man.set_cleanup_func(
StoredFunc(self._server_timeout), destroy_first=True
)
print("DEBUG")
print(res) print(res)
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
def connect_by_ip(self, addr: str) -> None: def connect_by_ip(self, addr: str) -> None:
res = Servers.query_by_ip(addr) res = Servers.query_by_ip(addr)
if res is None: if res is None:
self.thread_man.set_cleanup_func(StoredFunc(self._connection_failure)) self.thread_man.set_cleanup_func(
StoredFunc(self._server_timeout), destroy_first=True
)
print("DEBUG")
print(res) print(res)
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
def connect_by_record(self, record: Servers.Record) -> None: def connect_by_record(self, record: Servers.Record) -> None:
res = Servers.query_by_record(record) res = Servers.query_by_record(record)
if res is None: if res is None:
self.thread_man.set_cleanup_func(StoredFunc(self._connection_failure)) self.thread_man.set_cleanup_func(
StoredFunc(self._server_timeout), destroy_first=True
)
# TODO: add to history if successful # TODO: add to history if successful
print("DEBUG")
print(res) print(res)
def _connection_failure(self) -> None: @call_on_thread(dialog.querying)
# TODO: more explicit warning message, not necessarily API failure? def query_details(self, record: Servers.Record) -> None:
dialog = ExceptionDialog(self.controller, api_warn_msg) details = Servers.details(record)
if details.success is False:
self.thread_man.set_cleanup_func(
StoredFunc(self._server_timeout), destroy_first=True
)
return
self.thread_man.set_cleanup_func(
StoredFunc(self._present_details_dialog, details), destroy_first=True
)
def _present_details_dialog(self, details: Servers.Details) -> None:
dialog = ServerDetailsDialog(self.controller, details)
dialog.run() dialog.run()
def _server_timeout(self) -> None:
dialog = ExceptionDialog(self.controller, server_timeout)
dialog.run()
# def _connection_failure(self) -> None:
# # FIXME: returns api warning, but this is more likely a localized server issue
# dialog = ExceptionDialog(self.controller, api_warn_msg)
# dialog.run()
+2 -4
View File
@@ -3,7 +3,6 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.const.enum import ContextMenu, Preferences from dzgui.const.enum import ContextMenu, Preferences
from dzgui.managers.thread_man import ThreadingManager
from dzgui.util import strings from dzgui.util import strings
from dzgui.util.clip import copy_clipboard from dzgui.util.clip import copy_clipboard
from dzgui.util.open_links import open_workshop_page from dzgui.util.open_links import open_workshop_page
@@ -29,7 +28,6 @@ class ContextMenuManager:
) -> None: ) -> None:
self.controller = controller self.controller = controller
self.treeview = treeview self.treeview = treeview
self.thread_man = ThreadingManager(parent=controller)
def process(self, action: ContextMenu) -> None: def process(self, action: ContextMenu) -> None:
match action: match action:
@@ -80,8 +78,8 @@ class ContextMenuManager:
# reverse of ADD_SERVER # reverse of ADD_SERVER
pass pass
case ContextMenu.SHOW_DETAILS: case ContextMenu.SHOW_DETAILS:
print("DEBUG: unimplemented") record = self.treeview.get_record()
pass self.controller.get_details(record)
case ContextMenu.SHOW_MODS: case ContextMenu.SHOW_MODS:
print("DEBUG: unimplemented") print("DEBUG: unimplemented")
pass pass
+20 -9
View File
@@ -50,7 +50,9 @@ 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 self.destroy_first = False
# 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:
@@ -72,30 +74,39 @@ class ThreadingManager:
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, set in cleanup func # TODO: this should not be delegated here, set in cleanup func
def set_alternate_statusbar(self, msg: str) -> None: # def set_alternate_statusbar(self, msg: str) -> None:
self.alternate_statusbar = msg # self.alternate_statusbar = msg
def get_alternate_statusbar(self) -> Optional[str]: # def get_alternate_statusbar(self) -> Optional[str]:
return self.alternate_statusbar # return self.alternate_statusbar
def set_cleanup_func(self, func: StoredFunc) -> None: def set_cleanup_func(self, func: StoredFunc, destroy_first: bool = False) -> 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"
logger.critical(msg) logger.critical(msg)
raise TypeError(msg) raise TypeError(msg)
self.destroy_first = destroy_first
self.cleanup_func = func self.cleanup_func = func
def get_cleanup_func(self) -> StoredFunc: def get_cleanup_func(self) -> StoredFunc:
return self.cleanup_func return self.cleanup_func
def _d(self) -> None: # def _d(self) -> None:
self.wait_dialog.hide() # self.wait_dialog.hide()
return False # return False
def _destroy_on_idle(self) -> GLib.SOURCE_REMOVE: def _destroy_on_idle(self) -> GLib.SOURCE_REMOVE:
if self.destroy_first:
self.wait_dialog.destroy()
func = self.get_cleanup_func() func = self.get_cleanup_func()
if func is not None: if func is not None:
func.call() func.call()
self.set_cleanup_func(None) self.set_cleanup_func(None)
if not self.destroy_first:
self.wait_dialog.destroy() self.wait_dialog.destroy()
return GLib.SOURCE_REMOVE return GLib.SOURCE_REMOVE
def get_wait_dialog(self) -> WaitDialog:
return self.wait_dialog
+3 -1
View File
@@ -320,7 +320,7 @@ class ServerModelManager:
# 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, servers_failed_to_load) # -> signal driven (servers_empty, servers_failed_to_load)
# NOTE: used by refresh action # NOTE: used by refresh button action
if not self.preserve_on_fail: if not self.preserve_on_fail:
self.tv.set_model(None) self.tv.set_model(None)
filter_man = self.tv.get_filter_man() filter_man = self.tv.get_filter_man()
@@ -330,6 +330,8 @@ class ServerModelManager:
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty" # TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
# customize statusbar and dialog accordingly # customize statusbar and dialog accordingly
self.emitter.emit("servers_loaded", self.enum) self.emitter.emit("servers_loaded", self.enum)
# TODO: destroy wait dialog first
# see threadman.set_cleanup_func(_, destroy_first=True)
if show_dialog: if show_dialog:
dialog = ExceptionDialog(self.controller, api_warn_msg) dialog = ExceptionDialog(self.controller, api_warn_msg)
dialog.run() dialog.run()
+1 -1
View File
@@ -102,7 +102,7 @@ build_corrupted = (
api_warn_msg = """No servers returned. Please wait and try again. api_warn_msg = """No servers returned. Please wait and try again.
If this issue persists, your API key may be defunct or your network is blocking requests. If this issue persists, your API key may be defunct or your network is blocking requests.
""" """
server_timeout = "Timed out when querying server, check IP or try again later" server_timeout = "Timed out when querying server, check IP or try again later."
server_error = ( server_error = (
"Error while contacting server, possibly timed out. Please wait and try again." "Error while contacting server, possibly timed out. Please wait and try again."
) )
-7
View File
@@ -19,13 +19,6 @@ if TYPE_CHECKING:
# dialog_type = Gtk.MessageType.INFO # dialog_type = Gtk.MessageType.INFO
# button_type = Gtk.ButtonsType.OK # button_type = Gtk.ButtonsType.OK
# header_text = strings.modlist # header_text = strings.modlist
# case Popup.DETAILS:
# dialog_type = Gtk.MessageType.INFO
# button_type = Gtk.ButtonsType.OK
# header_text = strings.server_details
# TODO: unused
# def update_label(self, text: str) -> None:
# self.format_secondary_text(text)
class GenericDialog(Gtk.MessageDialog): class GenericDialog(Gtk.MessageDialog):
+45 -47
View File
@@ -1,20 +1,36 @@
import gi import gi
import dzgui.api.servers as Servers
from dzgui.const.constants import EXPAND, FILL, NO_PADDING
from dzgui.const.enum import Popup
from dzgui.util import css
from dzgui.util import strings
from dzgui.util.format import format_hyperlinks
from dzgui.views.dialogs.generic import GenericDialog
from typing import Self, TYPE_CHECKING
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
from dzgui.const.constants import EXPAND, FILL, NO_PADDING if TYPE_CHECKING:
from dzgui.const.enum import Popup from dzgui.api.servers import Details
class ServerDetailsDialog(GenericDialog): class ServerDetailsDialog(GenericDialog):
def __init__(self, server_name: str, ip: str, qport: int): def __init__(self, controller, details: "Details"):
super().__init__(server_name, Popup.DETAILS) super().__init__(
controller=controller,
text=details.name,
buttons=Gtk.ButtonsType.OK,
mtype=Gtk.MessageType.INFO,
secondary=strings.server_details,
)
dialog_box = self.get_content_area() dialog_box = self.get_content_area()
self.set_default_response(Gtk.ResponseType.OK) self.set_default_response(Gtk.ResponseType.OK)
self.set_size_request(800, 700) self.set_size_request(800, 700)
self.ip = ip.split(":")[0]
self.qport = qport
self.store = Gtk.ListStore(str, str, Pango.Weight) self.store = Gtk.ListStore(str, str, Pango.Weight)
self.view = Gtk.TreeView( self.view = Gtk.TreeView(
@@ -24,13 +40,12 @@ class ServerDetailsDialog(GenericDialog):
fixed_height_mode=True, fixed_height_mode=True,
) )
self.view.connect("row-activated", self._on_row_activated) self.view.connect("row-activated", self._on_row_activated)
self.view.connect("key-press-event", self._on_keypress)
for i, column_title in enumerate(["Item", "Details"]): for i, column_title in enumerate(["Item", "Details"]):
renderer = Gtk.CellRendererText(xalign=0) renderer = Gtk.CellRendererText(xalign=0)
if i == 0: if i == 0:
column = Gtk.TreeViewColumn( column = Gtk.TreeViewColumn(column_title, renderer, text=i, weight=2)
column_title, renderer, text=i, weight=2
)
else: else:
column = Gtk.TreeViewColumn(column_title, renderer, text=i) column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
@@ -43,15 +58,12 @@ class ServerDetailsDialog(GenericDialog):
scrollable_tree.add(self.view) scrollable_tree.add(self.view)
scrollable_tree.set_size_request(700, 200) scrollable_tree.set_size_request(700, 200)
# TODO: center header text
scrollable_message = Gtk.ScrolledWindow() scrollable_message = Gtk.ScrolledWindow()
desc = Gtk.Label(label=strings.server_message, valign=Gtk.Align.START) desc = Gtk.Label(label=strings.server_message, valign=Gtk.Align.START)
css.add_class(desc, "details-heading") css.add_class(desc, "details-heading")
box = Gtk.Box( box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER)
orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True)
)
self.description = Gtk.Label(
justify=Gtk.Justification.CENTER, wrap=True
)
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
sep.set_margin_bottom(10) sep.set_margin_bottom(10)
for el in desc, sep, self.description: for el in desc, sep, self.description:
@@ -61,12 +73,24 @@ class ServerDetailsDialog(GenericDialog):
dialog_box.pack_start(scrollable_tree, EXPAND, FILL, NO_PADDING) dialog_box.pack_start(scrollable_tree, EXPAND, FILL, NO_PADDING)
dialog_box.pack_start(scrollable_message, EXPAND, FILL, NO_PADDING) dialog_box.pack_start(scrollable_message, EXPAND, FILL, NO_PADDING)
self.wait_dialog = GenericDialog(strings.details, Popup.WAIT) for row in details.data:
self.wait_dialog.show_all() self.store.append(row + [Pango.Weight.BOLD])
thread = threading.Thread( self.view.set_model(self.store)
target=self._background, args=(self.wait_dialog, ip, qport) text = details.description
) text = format_hyperlinks(text)
thread.start() self.description.set_markup(text)
self.connect("response", self._on_response)
self.show_all()
def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> None:
if event.keyval == Gdk.KEY_Escape:
self.destroy()
return True
return False
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
self.destroy()
def _on_row_activated( def _on_row_activated(
self, self,
@@ -75,29 +99,3 @@ class ServerDetailsDialog(GenericDialog):
col: Gtk.TreeViewColumn, col: Gtk.TreeViewColumn,
) -> None: ) -> None:
self.destroy() self.destroy()
def _load(self) -> None:
if self.wait_dialog:
self.wait_dialog.destroy()
if self.success is False:
AppNav.window.spawn_dialog(strings.server_error, Popup.NOTIFY)
return
self.show_all()
self.run()
self.destroy()
def _background(
self, dialog: "GenericDialog", ip: str, qport: int
) -> None:
response = Servers.details(self.ip, self.qport)
if response.success:
for row in response.data:
self.store.append(row + [Pango.Weight.BOLD])
self.view.set_model(self.store)
text = response.description
text = format_hyperlinks(text)
self.description.set_markup(text)
self.success = response.success
GLib.idle_add(self._load)
-4
View File
@@ -63,7 +63,6 @@ class ServerNotebook(Gtk.ScrolledWindow):
tv = self.get_active_treeview() tv = self.get_active_treeview()
state = False if tv.get_model() is None else True state = False if tv.get_model() is None else True
tv.set_headers_visible(state) tv.set_headers_visible(state)
# tv.set_headers_clickable(state)
tv.set_loaded(True) tv.set_loaded(True)
tv.grab_focus() tv.grab_focus()
@@ -151,9 +150,6 @@ class ServerNotebook(Gtk.ScrolledWindow):
tab, col, "_on_col_width_changed", False tab, col, "_on_col_width_changed", False
) )
# def get_tabs(self) -> tuple:
# return (self.browser, self.saved, self.recent, self.lan)
def get_browser(self) -> ServerTreeView: def get_browser(self) -> ServerTreeView:
return self.browser return self.browser
+2 -1
View File
@@ -283,7 +283,8 @@ class ServerTreeView(ContextMixin, TreeView):
def _parent_row_activated( def _parent_row_activated(
self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn
) -> None: ) -> None:
print(self.get_value_at_index(0)) record = self.get_record()
self.controller.connect_by_record(record)
def _parent_selection_changed(self, base_class: TreeView, sel: Gtk.TreeSelection): def _parent_selection_changed(self, base_class: TreeView, sel: Gtk.TreeSelection):
if self.loaded is False: if self.loaded is False: