mirror of
https://github.com/aclist/dztui.git
synced 2026-08-28 18:57:12 +02:00
392 lines
13 KiB
Python
392 lines
13 KiB
Python
import logging
|
|
import threading
|
|
|
|
from queue import Queue
|
|
from typing import Any, Optional, Self
|
|
from warnings import deprecated
|
|
|
|
from dzgui.api.servers import ping, Record
|
|
from dzgui.const.constants import APP_NAME
|
|
from dzgui.const.enum import ContextMenu, ContextMenuGroup, ServerTab
|
|
from dzgui.managers.filter import FilterManager
|
|
from dzgui.model.proxy_model import ProxyModelManager
|
|
from dzgui.util import strings
|
|
from dzgui.util.dist import CalcDist
|
|
from dzgui.views.mixins.context_mixin import ContextMixin
|
|
from typing import Literal, TYPE_CHECKING
|
|
|
|
|
|
from dzgui.views.trees.tree_base import TreeView
|
|
import dzgui.util._json as JSON # noqa
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "3.0")
|
|
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
|
|
|
|
|
|
logger = logging.getLogger(APP_NAME)
|
|
|
|
if TYPE_CHECKING:
|
|
from dzgui.controllers.mc import Controller
|
|
from dzgui.controllers.emitter import Emitter
|
|
from dzgui.model.model_factory import FastInsertListStore
|
|
|
|
QUEUE_CHECK_DELAY = 200
|
|
|
|
|
|
class ServerTreeView(ContextMixin, TreeView):
|
|
def __init__(
|
|
self, controller: "Controller", enum: ServerTab, menu: ContextMenuGroup
|
|
) -> None:
|
|
super().__init__(controller, menu=menu)
|
|
|
|
self.controller = controller
|
|
self.emitter = controller.get_emitter()
|
|
self.enum = enum
|
|
|
|
self.loaded = False
|
|
|
|
self.filter_man = FilterManager()
|
|
self.proxy_man = ProxyModelManager(self.filter_man)
|
|
model = self.proxy_man.get_proxy_model()
|
|
self.set_model(model)
|
|
|
|
self.set_fixed_height_mode(True)
|
|
# NOTE: headers become visible on model load
|
|
self.set_headers_visible(False)
|
|
|
|
self.queue_id: int
|
|
self.handler_id: int
|
|
self.queue = Queue()
|
|
|
|
self.seen_cache = []
|
|
|
|
prefs = self.controller.get_prefs()
|
|
columns = prefs.paths.columns
|
|
try:
|
|
data = JSON.read_json(columns)
|
|
valid_json = True
|
|
except Exception as e:
|
|
logger.critical(e)
|
|
valid_json = False
|
|
|
|
width_map = {
|
|
"Name": 800,
|
|
"Map": 300,
|
|
"IP": 240,
|
|
}
|
|
|
|
# TODO: abstract
|
|
# FIXME: resize col width func causes snapping behavior
|
|
browser_cols = strings.browser_cols
|
|
for i, column_title in enumerate(browser_cols):
|
|
renderer = Gtk.CellRendererText()
|
|
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
|
|
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
|
|
column.set_resizable(True)
|
|
column.set_sort_column_id(i)
|
|
|
|
if valid_json:
|
|
try:
|
|
saved_size = data["cols"][column_title]
|
|
except KeyError:
|
|
saved_size = 100
|
|
column.set_fixed_width(saved_size)
|
|
column.set_expand(True)
|
|
else:
|
|
w = width_map[column_title]
|
|
column.set_fixed_width(w)
|
|
if column_title == "Ping":
|
|
column.set_cell_data_func(renderer, self._get_ping)
|
|
|
|
column.connect("notify::fixed-width", self._on_col_width_changed)
|
|
self.append_column(column)
|
|
|
|
self.connect("button-press-event", self.present_menu)
|
|
self.connect("generic_row_activated", self._parent_row_activated)
|
|
self.connect("generic_treesel_changed", self._parent_selection_changed)
|
|
self.connect("key-press-event", self._on_server_keypress)
|
|
self.connect("key-press-event", self.present_menu)
|
|
self.connect("map", self._on_map)
|
|
self.connect("unmap", self._on_unmap)
|
|
|
|
self.set_has_tooltip(True)
|
|
self.connect("query-tooltip", self._on_tooltip)
|
|
|
|
# TODO: why is this being saved?
|
|
# self.thread = None
|
|
|
|
def _on_tooltip(
|
|
self,
|
|
widget: Gtk.Widget,
|
|
x: int,
|
|
y: int,
|
|
keyboard_mode: bool,
|
|
tooltip: Gtk.Tooltip,
|
|
) -> bool:
|
|
"""
|
|
Present record data for the hovered row even if it is unfocused
|
|
"""
|
|
coords = widget.convert_widget_to_bin_window_coords(x, y)
|
|
path = self.get_path_at_pos(coords.bx, coords.by)
|
|
if path is None:
|
|
return False
|
|
|
|
model = self.get_model()
|
|
tree_iter = model.get_iter(path[0])
|
|
ip = model.get_value(tree_iter, 7)
|
|
qport = model.get_value(tree_iter, 8)
|
|
addr = ip + ":" + str(qport)
|
|
note = self.controller.get_note_by_record(addr)
|
|
|
|
if len(note) > 0:
|
|
tooltip.set_text(note)
|
|
self.set_tooltip_row(tooltip, path[0])
|
|
return True
|
|
return False
|
|
|
|
def start_queue_checker(self) -> None:
|
|
self.queue_id = GLib.timeout_add(QUEUE_CHECK_DELAY, self._check_result_queue)
|
|
|
|
def get_filter_man(self) -> FilterManager:
|
|
return self.filter_man
|
|
|
|
def get_proxy_man(self) -> ProxyModelManager:
|
|
return self.proxy_man
|
|
|
|
def get_enum(self) -> ServerTab:
|
|
return self.enum
|
|
|
|
def _on_map(self, widget: Self) -> None:
|
|
# TODO: disable filter panel if current model is None
|
|
if self.get_enum() is ServerTab.LAN:
|
|
self.emitter.emit("lan_tab_toggled", True)
|
|
|
|
store = self.filter_man.get_map_store()
|
|
|
|
# FIXME: if model is none, wipe maps
|
|
# distinguish this signal from changing map combo in-situ
|
|
self.emitter.emit("load_maps", store)
|
|
self.handler_id = self.emitter.connect("statusbar_loaded", self.start_distcalc)
|
|
self.start_queue_checker()
|
|
self.start_distcalc()
|
|
|
|
def _on_unmap(self, tree: Self) -> None:
|
|
# NOTE: removes queue checker for this tab
|
|
GLib.Source.remove(self.queue_id)
|
|
self.emitter.disconnect(self.handler_id)
|
|
if self.get_enum() is ServerTab.LAN:
|
|
self.emitter.emit("lan_tab_toggled", False)
|
|
|
|
def _on_col_width_changed(
|
|
self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt
|
|
) -> None:
|
|
"""
|
|
Propagate width change to other tabs
|
|
"""
|
|
# NOTE: get final width after drag action completes
|
|
GLib.idle_add(self.controller.propagate_column_width, col)
|
|
|
|
def start_distcalc(self, emitter: Optional["Emitter"] = None):
|
|
self.emitter.emit("distcalc_started")
|
|
record = self.get_record()
|
|
if record is None:
|
|
context = self.get_enum()
|
|
self.emitter.emit("distcalc_ended", None, context)
|
|
return
|
|
|
|
cache = self.controller.get_dist_cache()
|
|
|
|
if record.ip in cache:
|
|
haversine = cache[record.ip]
|
|
self.controller.set_statusbar_dist(haversine, self.get_enum())
|
|
return
|
|
|
|
enum = self.get_enum()
|
|
thread = threading.Thread(
|
|
daemon=True,
|
|
target=CalcDist,
|
|
args=(record.ip, enum, self.queue, self.controller, cache),
|
|
)
|
|
thread.start()
|
|
|
|
def _check_result_queue(self) -> Literal[True]:
|
|
latest_result = None
|
|
while not self.queue.empty():
|
|
latest_result = self.queue.get()
|
|
|
|
# FIXME: cache is being checked in two passes
|
|
cache = self.controller.get_dist_cache()
|
|
|
|
if latest_result:
|
|
addr, haversine, tab = latest_result
|
|
if addr not in cache:
|
|
self.controller.set_dist_cache(addr, haversine)
|
|
# TODO: should be emitting a statusbar signal here instead?
|
|
self.controller.set_statusbar_dist(haversine, self.get_enum())
|
|
return True
|
|
|
|
def _on_server_keypress(
|
|
self, treeview: Gtk.TreeView, event: Gdk.EventKey
|
|
) -> None:
|
|
if event.state is Gdk.ModifierType.CONTROL_MASK:
|
|
match event.keyval:
|
|
case Gdk.KEY_f:
|
|
self.emitter.emit("request_keyword_focus")
|
|
case Gdk.KEY_m:
|
|
self.emitter.emit("request_maps_focus")
|
|
case Gdk.KEY_i:
|
|
self.emitter.emit("request_ip_entry_focus")
|
|
case Gdk.KEY_d:
|
|
self.emitter.emit("request_default_port_focus")
|
|
case Gdk.KEY_n:
|
|
if self.enum is ServerTab.LAN:
|
|
self.emitter.emit("request_custom_port_focus")
|
|
case Gdk.KEY_c:
|
|
self.controller.menu_action(ContextMenu.COPY_SERVER_IP, self)
|
|
case Gdk.KEY_r:
|
|
# TODO: unimplemented, needs threading
|
|
self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self)
|
|
case _:
|
|
return False
|
|
return True
|
|
else:
|
|
match event.keyval:
|
|
case Gdk.KEY_l | Gdk.KEY_Right:
|
|
self.emitter.emit("request_button_box_focus")
|
|
return True
|
|
case _:
|
|
self.emitter.emit("check_button_pressed", event.keyval)
|
|
|
|
def is_modded(self) -> bool:
|
|
select = self.get_selection()
|
|
sels = select.get_selected_rows()
|
|
(model, pathlist) = sels
|
|
path = pathlist[0]
|
|
tree_iter = model.get_iter(path)
|
|
has_mods = model.get_value(tree_iter, 11)
|
|
return has_mods
|
|
|
|
def get_selected_row(self) -> Gtk.TreeModelRow:
|
|
sel = self.get_selection()
|
|
sels = sel.get_selected_rows()
|
|
return sels[0]
|
|
|
|
def is_in_favs(self) -> bool:
|
|
record = self.get_record_string()
|
|
if self.controller.get_config_man().is_in_favs(record):
|
|
return True
|
|
return False
|
|
|
|
def _parent_row_activated(
|
|
self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn
|
|
) -> None:
|
|
record = self.get_record()
|
|
self.controller.connect_by_record(record)
|
|
|
|
def _parent_selection_changed(
|
|
self, base_class: TreeView, sel: Gtk.TreeSelection
|
|
) -> None:
|
|
if self.loaded is False:
|
|
return
|
|
self.start_distcalc()
|
|
|
|
def get_name(self) -> str:
|
|
return self.get_value_at_index(0)
|
|
|
|
def get_simplified_ip(self) -> str:
|
|
addr = self.get_value_at_index(7)
|
|
qport = self.get_value_at_index(8)
|
|
ip = addr.split(":")[0]
|
|
return f"{ip}:{qport}"
|
|
|
|
def get_record_string(self) -> str:
|
|
addr = self.get_value_at_index(7)
|
|
qport = self.get_value_at_index(8)
|
|
return f"{addr}:{qport}"
|
|
|
|
def get_record(self) -> Record | None:
|
|
r = self.get_record_string()
|
|
try:
|
|
ip, gameport, qport = r.split(":")
|
|
return Record(ip, int(gameport), int(qport))
|
|
except ValueError as e:
|
|
logger.critical(e)
|
|
return None
|
|
|
|
def is_loaded(self) -> bool:
|
|
return self.loaded
|
|
|
|
def set_loaded(self, status: bool) -> None:
|
|
self.loaded = status
|
|
|
|
def get_model_and_control_model(self) -> tuple["FastInsertListStore", list[Any]]:
|
|
model = self.get_model()
|
|
control = self.proxy_man.get_control()
|
|
return model, control
|
|
|
|
@staticmethod
|
|
def ping_server(
|
|
model, _iter: Gtk.TreeIter, ip: str, qport: int, ping_column: int
|
|
) -> None:
|
|
_ping = ping(ip, qport)
|
|
GLib.idle_add(lambda: model.set(_iter, ping_column, _ping))
|
|
|
|
def _get_ping(
|
|
self,
|
|
column: Gtk.TreeViewColumn,
|
|
cell: Gtk.CellRendererText,
|
|
model: Gtk.TreeModel,
|
|
_iter: Gtk.TreeIter,
|
|
data: Any,
|
|
) -> None:
|
|
|
|
addr_column = 7
|
|
qport_column = 8
|
|
ping_column = 9
|
|
|
|
addr = model.get_value(_iter, addr_column).split(":")
|
|
ip = addr[0]
|
|
gameport = addr[1]
|
|
qport = model.get_value(_iter, qport_column)
|
|
record = f"{addr}:{gameport}:{qport}"
|
|
|
|
if record in self.seen_cache:
|
|
return
|
|
self.seen_cache.append(record)
|
|
|
|
thread = threading.Thread(
|
|
daemon=True,
|
|
target=self.ping_server,
|
|
args=(model, _iter, ip, qport, ping_column),
|
|
)
|
|
thread.start()
|
|
|
|
@deprecated("Currently unused")
|
|
def _lazy_load(
|
|
self,
|
|
column: Gtk.TreeViewColumn,
|
|
cell: Gtk.CellRendererText,
|
|
model: Gtk.TreeModel,
|
|
it: Gtk.TreeIter,
|
|
col_index: int,
|
|
) -> Any:
|
|
"""
|
|
Lazy load contents from model manager into visible CellRenderers on demand
|
|
N.B., all row data types will be stringified and must be fetched
|
|
from separate model manager, not internally from TreeView
|
|
cf. Gtk.TreeViewColumn.set_cell_data_func()
|
|
"""
|
|
path = model.get_path(it)
|
|
row_index = path.get_indices()[0]
|
|
try:
|
|
start, end = self.get_visible_range()
|
|
except Exception:
|
|
return
|
|
if row_index >= start[0] <= end[0]:
|
|
# NOTE: fetch raw data rows
|
|
real_model = self.proxy_man.get_control()
|
|
value = real_model[row_index][col_index]
|
|
cell.set_property("text", str(value))
|