mirror of
https://github.com/aclist/dztui.git
synced 2026-08-27 18:27:07 +02:00
407 lines
12 KiB
Python
407 lines
12 KiB
Python
import logging
|
|
import signal
|
|
import warnings
|
|
|
|
from typing import TYPE_CHECKING, Literal
|
|
|
|
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
|
|
from dzgui.util.format import embolden
|
|
|
|
# NOTEBOOK ITEMS
|
|
# TODO: import notebook only and add components there?
|
|
from dzgui.views.pages.changelog import Changelog
|
|
from dzgui.views.components.connect_panel import ConnectPanel
|
|
from dzgui.views.components.crumbs import Breadcrumbs
|
|
from dzgui.views.pages.devs import Developers
|
|
from dzgui.views.pages.help import Help
|
|
from dzgui.views.pages.keys import Keybindings
|
|
from dzgui.views.pages.log import Log
|
|
from dzgui.views.pages.mods import Mods
|
|
from dzgui.views.pages.options import Options
|
|
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.right_panel import RightPanel
|
|
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
|
|
|
if TYPE_CHECKING:
|
|
from dzgui.config.userprefs import UserPrefs
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "3.0")
|
|
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
|
|
|
logger = logging.getLogger(__name__)
|
|
# https://bugzilla.gnome.org/show_bug.cgi?id=708676
|
|
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
|
|
|
|
# TODO: move to controller
|
|
# kilometer cache; note, user may change measurement it partway through, flush cache
|
|
# cache: dict[str, int] = {}
|
|
|
|
## TODO: move to configs/servers
|
|
# def query_history() -> list | None:
|
|
# history_file = MainController.get_prefs().paths.history
|
|
# try:
|
|
# with open(history_file, "r") as f:
|
|
# rows = [row.rstrip("\n") for row in f]
|
|
# except OSError:
|
|
# rows = None
|
|
# return rows
|
|
#
|
|
# def process_tree_option(choice: RowType) -> None:
|
|
# # server tables
|
|
# if command == RowType.RESOLVE_IP:
|
|
# record = treeview.get_record()
|
|
# wait_msg = command.dict["wait_msg"]
|
|
# show_wait_dialog = True
|
|
#
|
|
# # TODO: needs threading, this is a slow process
|
|
# # TODO: needs saving into config
|
|
# # TODO: used by add/remove servers
|
|
# real_ip = ip.resolve_ip(record)
|
|
# #call_on_thread(
|
|
# # show_wait_dialog, cmd_string, wait_msg, record, choice=choice
|
|
# #)
|
|
# return
|
|
#
|
|
# if command == RowType.QUICK_CONNECT:
|
|
# record = MainController.query_config(Preferences.FAV_SRV)
|
|
# if record == "":
|
|
# spawn_dialog("No favorite server currently set", Popup.NOTIFY)
|
|
# return
|
|
#
|
|
# record = str_to_record(record)
|
|
# thread_new_with_dialog(
|
|
# treeview.prepare_connection,
|
|
# parse_shell_output,
|
|
# "Querying server",
|
|
# command,
|
|
# [record],
|
|
# )
|
|
# return
|
|
#
|
|
## TODO: belongs in model
|
|
# def str_to_record(record: str) -> Record | None:
|
|
# r = record.split(":")
|
|
# if len(r) != 3:
|
|
# return None
|
|
# return Record(r[0], int(r[1]), int(r[2]))
|
|
#
|
|
## TODO: ibid
|
|
# def record_to_str(record: Record) -> str:
|
|
# return f"{record.ip}:{record.gameport}:{record.qport}"
|
|
#
|
|
#
|
|
# def connect_by_ip(enum: RowType, response: str) -> None:
|
|
# def _prep(response: str) -> None:
|
|
# record = Servers.validate_ip(response)
|
|
# proc = treeview.prepare_connection(record)
|
|
# return proc
|
|
#
|
|
# thread_new_with_dialog(
|
|
# _prep, parse_shell_output, "Querying IP", enum, [response]
|
|
# )
|
|
# return
|
|
#
|
|
#
|
|
# def connect_by_id(enum: RowType, uid: str, key: str) -> None:
|
|
# def _prep(key: str, response: str) -> None:
|
|
# # TODO: if response is non numeric, raise dialog
|
|
# if response.isnumeric() is False:
|
|
# pass
|
|
# # raise error
|
|
# # raise BmIdError("ID must be numeric only")
|
|
# # return
|
|
# from dzgui.api.bm import map_id_to_record
|
|
# try:
|
|
# record = map_id_to_record(config, uid)
|
|
# except Exception as e:
|
|
# logger.critical(e)
|
|
# # raise dialog
|
|
# return
|
|
# # proc = treeview.prepare_connection(record)
|
|
# # return proc
|
|
#
|
|
# #thread_new_with_dialog(
|
|
# # _prep, parse_shell_output, "Querying API", enum, [key, response]
|
|
# #)
|
|
# #return
|
|
#
|
|
#
|
|
|
|
|
|
class OuterWindow(Gtk.Window):
|
|
def __init__(self) -> None:
|
|
super().__init__(title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER)
|
|
|
|
self.hb = AppHeaderBar()
|
|
MainController.register_widget("window", self)
|
|
|
|
# NOTE: steam deck taskbar may occlude elements
|
|
if MainController.get_prefs().is_steam_deck is False:
|
|
self.set_titlebar(self.hb)
|
|
|
|
self.connect("delete-event", self._on_delete_event)
|
|
|
|
self.grid = Grid()
|
|
self.add(self.grid)
|
|
|
|
MainController.set_resolution(self)
|
|
self.show_all()
|
|
|
|
css.load_css()
|
|
|
|
# TODO: first run
|
|
MainController.open_page(NotebookPage.SERVERS)
|
|
MainController.mediator.grid.conpan.lan.set_visible(False)
|
|
self.grid.right_panel.sel_panel.hide()
|
|
# TODO: POC, trigger page change here
|
|
MainController.loaded = True
|
|
MainController.populate_model()
|
|
|
|
def _on_delete_event(self, window: "OuterWindow", event: Gdk.EventKey) -> None:
|
|
self.halt_proc_and_quit()
|
|
|
|
def halt_proc_and_quit(self) -> None:
|
|
MainController.terminate_process()
|
|
MainController.save_res_and_quit()
|
|
|
|
|
|
class AppHeaderBar(Gtk.HeaderBar):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.props.title = APP_NAME
|
|
self.set_decoration_layout(":minimize,maximize,close")
|
|
self.set_show_close_button(True)
|
|
|
|
|
|
class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore
|
|
def __init__(self) -> None:
|
|
super().__init__(show_tabs=False)
|
|
|
|
self.prior_page: NotebookPage
|
|
|
|
MainController.register_widget("notebook", self)
|
|
|
|
self.help = Help(MainController)
|
|
self.clog = Changelog(MainController)
|
|
|
|
self.keys = Keybindings()
|
|
self.settings = Options(MainController)
|
|
|
|
self.servers = ServerNotebook(MainController)
|
|
self.mods = Mods(MainController)
|
|
|
|
self.thanks = Thanks()
|
|
self.log = Log(MainController)
|
|
self.developers = Developers(MainController)
|
|
|
|
self.pages = {
|
|
self.help: NotebookPage.HELP,
|
|
self.clog: NotebookPage.CHANGELOG,
|
|
self.keys: NotebookPage.KEYS,
|
|
self.settings: NotebookPage.OPTIONS,
|
|
self.servers: NotebookPage.SERVERS,
|
|
self.mods: NotebookPage.MODS,
|
|
self.log: NotebookPage.LOG,
|
|
self.thanks: NotebookPage.THANKS,
|
|
self.developers: NotebookPage.DEVELOPERS,
|
|
}
|
|
self.indexes = {}
|
|
|
|
"""
|
|
Note that due to historical reasons, Gtk.Notebook refuses to switch to a page
|
|
unless the child widget is visible. Therefore, it is recommended to show child
|
|
widgets before adding them to a notebook.
|
|
"""
|
|
for page in self.pages:
|
|
page.show_all()
|
|
index = self.append_page(page)
|
|
enum = self.pages[page]
|
|
self.indexes[enum] = index
|
|
|
|
self.connect_after("switch-page", self._on_page_changed)
|
|
self.connect("key-press-event", self._on_keypress)
|
|
|
|
def _on_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None:
|
|
match event.keyval:
|
|
case Gdk.KEY_Right | Gdk.KEY_l:
|
|
if event.state is Gdk.ModifierType.CONTROL_MASK:
|
|
return
|
|
MainController.focus_button_box()
|
|
case Gdk.KEY_question:
|
|
self.toggle_keybindings()
|
|
|
|
def return_prior(self) -> None:
|
|
"""
|
|
Gtk.Notebook focuses the first input field when changing pages;
|
|
this workaround unhighlights the selected region and makes entry
|
|
fields unfocusable prior to the page 'switch-page' signal,
|
|
then makes them focusable again
|
|
|
|
Used when switching back from NotebookPage.KEYS to avoid cursor
|
|
getting stuck inside text entry fields
|
|
"""
|
|
if self.prior_page is NotebookPage.OPTIONS:
|
|
self.settings.block_text_entry()
|
|
self.set_page_by_enum(self.prior_page)
|
|
self.settings.unblock_text_entry()
|
|
return
|
|
self.set_page_by_enum(self.prior_page)
|
|
|
|
def get_page_by_enum(self) -> NotebookPage | None:
|
|
for k, v in self.indexes.items():
|
|
if v == self.get_current_page():
|
|
return k
|
|
return None
|
|
|
|
def toggle_keybindings(self) -> None:
|
|
cur_page = self.get_page_by_enum()
|
|
if cur_page == NotebookPage.KEYS:
|
|
self.return_prior()
|
|
else:
|
|
self.set_page_by_enum(NotebookPage.KEYS)
|
|
|
|
def focus_current(self) -> None:
|
|
widget = self.get_page()
|
|
if widget is None:
|
|
return
|
|
|
|
if widget is self.servers:
|
|
MainController.grab_active_treeview()
|
|
return
|
|
|
|
w = widget.get_children()[0]
|
|
w.grab_focus()
|
|
|
|
def get_page(self) -> Gtk.Widget | None:
|
|
ind = self.get_current_page()
|
|
widget = self.get_nth_page(ind)
|
|
if not widget:
|
|
return None
|
|
return widget
|
|
|
|
def set_page_by_enum(self, enum: NotebookPage) -> None:
|
|
prior = self.get_page_by_enum()
|
|
if prior is not None:
|
|
self.prior_page = prior
|
|
self.set_current_page(self.indexes[enum])
|
|
self.focus_current()
|
|
|
|
def _on_page_changed(
|
|
self, notebook: "Notebook", page: Gtk.Widget, page_num: int
|
|
) -> None:
|
|
pass
|
|
|
|
|
|
class Grid(Gtk.Grid):
|
|
def __init__(self) -> None:
|
|
super().__init__(column_homogeneous=True)
|
|
|
|
MAX_ROWS = 3
|
|
MAX_COLS = 3
|
|
SINGLE_ROW = 1
|
|
SINGLE_COL = 1
|
|
|
|
MainController.register_widget("grid", self)
|
|
|
|
self.statusbar = Statusbar(MainController)
|
|
self.right_panel = RightPanel(MainController)
|
|
|
|
# self.bu = Gtk.Button(label="Shrink to fit", halign=Gtk.Align.END)
|
|
# self.bu.connect("clicked", self._shrink)
|
|
# self.crumb_box.add(self.bu)
|
|
|
|
self.notebook = Notebook()
|
|
self.conpan = ConnectPanel(MainController)
|
|
|
|
self.breadcrumbs = Breadcrumbs(MainController)
|
|
self.crumb_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
self.crumb_box.add(self.breadcrumbs)
|
|
|
|
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),
|
|
(
|
|
self.statusbar,
|
|
self.conpan,
|
|
Gtk.PositionType.BOTTOM,
|
|
MAX_COLS,
|
|
SINGLE_ROW,
|
|
),
|
|
(
|
|
self.right_panel,
|
|
self.notebook,
|
|
Gtk.PositionType.RIGHT,
|
|
SINGLE_COL,
|
|
MAX_ROWS,
|
|
),
|
|
)
|
|
for el, sibling, pos, h_span, v_span in els:
|
|
self.attach_next_to(el, sibling, pos, h_span, v_span)
|
|
|
|
self.show_all()
|
|
|
|
def _shrink(self, button: Gtk.Button) -> None:
|
|
tv = MainController.get_active_treeview()
|
|
tv.shrink_to_fit()
|
|
|
|
def toggle_filter_panel(self, state: bool) -> None:
|
|
self.right_panel.filters_vbox.set_visible(state)
|
|
|
|
def toggle_connect_panel(self, state: bool) -> None:
|
|
self.conpan.set_visible(state)
|
|
|
|
def toggle_refresh_button(self, state: bool) -> None:
|
|
self.right_panel.refresh_button.set_visible(state)
|
|
|
|
|
|
class App(Gtk.Application):
|
|
def __init__(self, prefs: "UserPrefs") -> None:
|
|
GLib.set_prgname(APP_NAME)
|
|
MainController.set_prefs(prefs)
|
|
|
|
self.win = OuterWindow()
|
|
|
|
accel = Gtk.AccelGroup()
|
|
accel.connect(
|
|
Gdk.KEY_q,
|
|
Gdk.ModifierType.CONTROL_MASK,
|
|
Gtk.AccelFlags.VISIBLE,
|
|
self._halt_window_subprocess,
|
|
)
|
|
self.win.add_accel_group(accel)
|
|
|
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint)
|
|
Gtk.main()
|
|
|
|
def _catch_sigint(self) -> Literal[True]:
|
|
self.win.halt_proc_and_quit()
|
|
return True
|
|
|
|
def _halt_window_subprocess(
|
|
self,
|
|
accel_group: Gtk.AccelGroup,
|
|
window: "OuterWindow",
|
|
code: Gdk.EventKey,
|
|
flag: Gdk.ModifierType,
|
|
) -> None:
|
|
self.win.halt_proc_and_quit()
|
|
|
|
|
|
MainController = Controller()
|
|
|
|
# TODO: icon sanity check, log result
|
|
# theme = Gtk.IconTheme.get_default()
|
|
# icons = theme.list_icons(None)
|
|
# print("steam_tray_mono" in icons)
|
|
# FIXME:
|