From e281345323dd4a2232558e63d392defc25c6a608 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 20 Dec 2025 23:06:47 +0900 Subject: [PATCH 01/49] feat: server notebook prototype --- dzgui/controllers/mc.py | 2 +- dzgui/util/strings.py | 4 ++-- dzgui/views/base.py | 12 +++++++----- dzgui/views/trees/tree_servers.py | 11 +++++------ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 0c7547a..b4f158b 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -99,7 +99,7 @@ class Controller: def terminate_process(self) -> None: # TODO: only used by server table multiprocessing queue - self.mediator.notebook.servers.terminate_process() + self.mediator.notebook.servers.browser.terminate_process() def get_prefs(self) -> UserPrefs: return self.prefs diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 2129746..3bab098 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -63,10 +63,10 @@ log_cols = ["Timestamp", "Flag", "Traceback", "Message"] browser_cols = [ "Name", "Map", - "Perspective", + "View", "Gametime", "Players", - "Maximum", + "Max", "Queue", "IP", "Qport", diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 24f23f2..2847449 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -247,7 +247,7 @@ class OuterWindow(Gtk.Window): AppNav.treeview = self.grid.notebook.scrollable_treelist.treeview css.load_css() - AppNav.grid.notebook.set_page_by_enum(NotebookPage.MAIN) + AppNav.grid.notebook.set_page_by_enum(NotebookPage.SERVERS) def _on_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: if event.state is Gdk.ModifierType.CONTROL_MASK \ @@ -344,9 +344,11 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # add all treeviews as page and register them to AppNav and self.indexes # when switching to a treeview, update relevant view and just pop that page # instead of loading/unloading the model each time - self.ssu = Gtk.ScrolledWindow() - self.servers = ServerTreeView(MainController) - self.ssu.add(self.servers) + from dzgui.views.pages.servers import ServerNotebook + self.servers = ServerNotebook(MainController) + #self.ssu = Gtk.ScrolledWindow() + #self.servers = ServerTreeView(MainController) + #self.ssu.add(self.servers) self.quad = Gtk.ScrolledWindow() @@ -373,7 +375,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.clog: NotebookPage.CHANGELOG, self.keys: NotebookPage.KEYS, self.settings: NotebookPage.OPTIONS, - self.ssu: NotebookPage.SERVERS, + self.servers: NotebookPage.SERVERS, self.scroll_mod: NotebookPage.MODS, self.scroll_log: NotebookPage.LOG, self.thanks: NotebookPage.THANKS, diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index e6436f0..53f643c 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -81,13 +81,12 @@ class ServerTreeView(TreeView): self.append_column(column) # TODO: do not load model on initial init - # TODO: row_store = self.controller.get_row_store() # TODO: test values: see below - r = Gtk.ListStore( - str, str, str, str, int, int, int, str, int, int, str, bool - ) - r.append(["TEST", "a", "a", "a", 0, 0, 0, "a", 0, 0, "a", False]) - self.set_model(r) + #r = Gtk.ListStore( + # str, str, str, str, int, int, int, str, int, int, str, bool + #) + #r.append(["TEST", "a", "a", "a", 0, 0, 0, "a", 0, 0, "a", False]) + #self.set_model(r) self.connect("on_distcalc_started", self._on_calclat_started) From 8b3d8c94fd0c0d9d070869f42b78f1544d9c692c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 21 Dec 2025 03:01:55 +0900 Subject: [PATCH 02/49] fix: connect signals to new notebook tabs --- dzgui/const/enum.py | 105 ++---------------------- dzgui/controllers/mc.py | 47 +++++++++-- dzgui/controllers/model.py | 14 ++-- dzgui/util/strings.py | 10 +-- dzgui/views/base.py | 80 +++++++++--------- dzgui/views/components/buttonbox.py | 5 +- dzgui/views/components/connect_panel.py | 25 ++++-- dzgui/views/components/web_button.py | 2 +- dzgui/views/trees/tree_base.py | 9 +- dzgui/views/trees/tree_servers.py | 37 +++------ 10 files changed, 137 insertions(+), 197 deletions(-) diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 4cc0ea1..b9e032f 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -361,20 +361,6 @@ class ContextMenuGroup(Enum): ) -MAIN_MENU_ROWS = ( - RowType.SERVER_BROWSER, - RowType.SAVED_SERVERS, - RowType.RECENT_SERVERS, - RowType.SCAN_LAN, - RowType.SEPARATOR, - RowType.QUICK_CONNECT, - RowType.CONN_BY_IP, - RowType.CONN_BY_ID, - RowType.SEPARATOR, - RowType.ADD_BY_IP, - RowType.ADD_BY_ID, - RowType.CHNG_FAV, -) HELP_MENU_ROWS = ( RowType.CHANGELOG, RowType.SHOW_LOG, @@ -389,87 +375,6 @@ HELP_MENU_ROWS = ( RowType.THANKS, ) -#class WindowContext(EnumWithAttrs): -# # TODO: deprecated -# @classmethod -# def row2con(cls, row: RowType) -> "WindowContext": -# m = WindowContext.MAIN_MENU -# for member in cls: -# if row in member.dict["rows"]: -# m = member -# elif row in member.dict["called_by"]: -# m = member -# else: -# continue -# return m -# -# # outer menu pages -# # TODO: some deprecated keys in here -# #MAIN_MENU = { -# # RowType.SERVER_BROWSER, -# # RowType.SAVED_SERVERS, -# # RowType.RECENT_SERVERS, -# # RowType.SCAN_LAN, -# # RowType.SEPARATOR, -# # RowType.QUICK_CONNECT, -# # RowType.CONN_BY_IP, -# # RowType.CONN_BY_ID, -# # RowType.SEPARATOR, -# # RowType.ADD_BY_IP, -# # RowType.ADD_BY_ID, -# # RowType.CHNG_FAV, -# # } -# #MODS = { -# # "label": "Mods", -# # "rows": [], -# # "called_by": [], -# #} -# HELP = { -# "label": "Help", -# "rows": [ -# RowType.CHANGELOG, -# RowType.SHOW_LOG, -# RowType.DUMP_LOG, -# RowType.SEPARATOR, -# RowType.DOCS, -# RowType.DOCS_FALLBACK, -# RowType.BUGS, -# RowType.FORUM, -# RowType.SPONSOR, -# RowType.SEPARATOR, -# RowType.THANKS, -# ], -# "called_by": [], -# } -# -# ## inner server contexts -# #TABLE_API = { -# # "label": "", -# # "rows": [], -# # "called_by": [RowType.SERVER_BROWSER], -# #} -# #TABLE_SERVER = { -# # "label": "", -# # "rows": [], -# # "called_by": [ -# # RowType.SAVED_SERVERS, -# # RowType.RECENT_SERVERS, -# # RowType.SCAN_LAN, -# # ], -# #} -# #TABLE_MODS = { -# # "label": "", -# # "rows": [], -# # "called_by": [ -# # RowType.LIST_MODS, -# # ], -# #} -# #TABLE_LOG = { -# # "label": "", -# # "rows": [], -# # "called_by": [RowType.SHOW_LOG], -# #} - class ModButton(EnumWithAttrs): SELECT_ALL = { @@ -499,10 +404,10 @@ class ModButton(EnumWithAttrs): class ButtonType(EnumWithAttrs): - MAIN_MENU = { - "label": strings.buttons.main_label, - "tooltip": strings.buttons.main_tooltip, - "opens": NotebookPage.MAIN, + SERVERS = { + "label": strings.buttons.servers_label, + "tooltip": strings.buttons.servers_tooltip, + "opens": NotebookPage.SERVERS, } MODS = { "label": strings.buttons.mods_label, @@ -517,7 +422,7 @@ class ButtonType(EnumWithAttrs): HELP = { "label": strings.buttons.help_label, "tooltip": strings.buttons.help_tooltip, - "opens": NotebookPage.MAIN, + "opens": NotebookPage.HELP, } EXIT = { "label": strings.buttons.exit_label, diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index b4f158b..f71cc55 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -79,8 +79,8 @@ class Controller: def get_crumbs(self) -> str: return self.mediator.grid.get_breadcrumbs() - def get_row_store(self) -> Gtk.ListStore: - return self.model_manager.get_row_store() + #def get_row_store(self) -> Gtk.ListStore: + # return self.model_manager.get_row_store() def get_help_store(self) -> Gtk.ListStore: return self.model_manager.get_help_store() @@ -160,7 +160,8 @@ class Controller: widget.handler_block_by_func(func) else: widget.handler_unblock_by_func(func) - self.mediator.treeview.sel_blocked = state + # TODO + #self.mediator.treeview.sel_blocked = state def toggle_debug_mode(self) -> None: self.toggle_config(Preferences.DEBUG) @@ -277,6 +278,7 @@ class Controller: self.mediator.grid.notebook.set_page_by_enum(page) def open_page_by_button(self, button: "ContextualButton") -> None: + print(button.context) match button.context: case ButtonType.EXIT: logger.info("Normal user exit") @@ -288,13 +290,16 @@ class Controller: self.load_mods() case ButtonType.HELP: help_store = self.model_manager.get_help_store() + # TODO: simply set this model once and open the page self.mediator.treeview.set_model(help_store) - case ButtonType.MAIN_MENU: - # TODO: going to be deprecated after server notebook is added - row_store = self.model_manager.get_row_store() - self.mediator.treeview.set_model(row_store) + case ButtonType.SERVERS: + self.mediator.notebook.set_page_by_enum(button.opens) + # TODO: shorten this + treeview = self.mediator.notebook.servers.get_active_treeview() + treeview.grab_focus() + return - self.mediator.grid.notebook.set_page_by_enum(button.opens) + self.mediator.notebook.set_page_by_enum(button.opens) self.set_crumbs(button.get_label()) # TODO: deprecated? @@ -544,3 +549,29 @@ class Controller: def get_developer_mode(self) -> bool: return self.is_developer + + def set_server_statusbar(self) -> None: + # TODO: different models + model = self.model_manager.get_server_store() + model = AppNav.treeview.get_model() + if model is None: + players = 0 + hits = 0 + else: + hits = len(model) + players = 0 + for row in model: + players += row[4] + + # TODO: move to util.format + players_pretty = pluralize("players", players) + hits_pretty = pluralize("matches", hits) + formatted = ( + f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}" + ) + suffix = "| Distance: calculating..." + + if players == 0: + suffix = "" + self.set_text(formatted + suffix) + self.players = formatted diff --git a/dzgui/controllers/model.py b/dzgui/controllers/model.py index d8ebc7a..f401c67 100644 --- a/dzgui/controllers/model.py +++ b/dzgui/controllers/model.py @@ -2,7 +2,7 @@ import re from dataclasses import dataclass from typing import Self -from dzgui.const.enum import FilterMode, MAIN_MENU_ROWS, HELP_MENU_ROWS +from dzgui.const.enum import FilterMode, HELP_MENU_ROWS from dzgui.util import strings import gi @@ -69,16 +69,16 @@ class ModelManager: self.ping_cache: dict[str, int] = {} self.map_store = ListStore(str) - self.row_store = self.new_model_from_class(MenuCols) + #self.row_store = self.new_model_from_class(MenuCols) self.help_store = self.new_model_from_class(MenuCols) self.mod_store = self.new_model_from_class(ModCols) self.log_store = self.new_model_from_class(LogCols) self.modlist_store = self.new_model_from_class(ServerModCols) - for row in MAIN_MENU_ROWS: - label = row.dict["label"] - self.row_store.append([label, row]) + #for row in MAIN_MENU_ROWS: + # label = row.dict["label"] + # self.row_store.append([label, row]) for row in HELP_MENU_ROWS: label = row.dict["label"] @@ -100,8 +100,8 @@ class ModelManager: def get_map_store(self) -> ListStore: return self.map_store - def get_row_store(self) -> ListStore: - return self.row_store + #def get_row_store(self) -> ListStore: + # return self.row_store def get_help_store(self) -> ListStore: return self.help_store diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 3bab098..8a92998 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -240,8 +240,8 @@ class Init: @dataclass(slots=True, frozen=True) class Button: - main_label: str - main_tooltip: str + servers_label: str + servers_tooltip: str mods_label: str mods_tooltip: str options_label: str @@ -426,8 +426,8 @@ dialog = Dialog( ) buttons = Button( - main_label="Main menu", - main_tooltip="Search for and connect to servers", + servers_label="Servers", + servers_tooltip="Search for and connect to servers", mods_label="Mods", mods_tooltip="Manage local mods", options_label="Options", @@ -448,7 +448,7 @@ crumbs = Crumbs( mods="Mods", my_servers="Main menu > Saved servers", options="Options", - servers="Main menu > Server browser", + servers="Servers", thanks="Help > Special thanks", developers="Options > Developers", ) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 2847449..c4418c6 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -11,13 +11,12 @@ import threading import typing # noqa import warnings -from collections.abc import Callable -from concurrent.futures import wait -from concurrent.futures import ThreadPoolExecutor +#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 Literal, Any # TODO: most likely very little of this will be retained from dzgui.const.enum import ( @@ -26,7 +25,6 @@ from dzgui.const.enum import ( RowType, NotebookPage, VAdjustment, - MAIN_MENU_ROWS, ) from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, WINDOW_DEFAULT_X, WINDOW_DEFAULT_Y @@ -34,26 +32,25 @@ from dzgui.controllers.mc import Controller from dzgui.util import css, dist, localize, strings, ip, deck, open_links from dzgui.util.format import pluralize -# GRID ITEMS +# NOTEBOOK ITEMS # TODO: import notebook only and add components there? from dzgui.views.pages.changelog import Changelog from dzgui.views.pages.keys import Keybindings -from dzgui.views.pages.thanks import Thanks 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.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_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 -from dzgui.views.dialogs.generic import GenericDialog - # TODO: not going to be in base anymore import dzgui.util._json as JSON # noqa @@ -233,6 +230,7 @@ class OuterWindow(Gtk.Window): self.toast = Toast() self.overlay = Gtk.Overlay() self.overlay.add_overlay(self.grid) + # TODO: toast is deprecated self.overlay.add_overlay(self.toast) self.add(self.overlay) @@ -244,7 +242,7 @@ class OuterWindow(Gtk.Window): self.grid.sel_panel.set_visible(False) # TODO: register from treeview or notebook - AppNav.treeview = self.grid.notebook.scrollable_treelist.treeview + #AppNav.treeview = self.grid.notebook.scrollable_treelist.treeview css.load_css() AppNav.grid.notebook.set_page_by_enum(NotebookPage.SERVERS) @@ -268,27 +266,15 @@ class OuterWindow(Gtk.Window): MainController.save_res_and_quit() -# TODO: consolidate -# wrap base tree class in this -class ScrollableTree(Gtk.ScrolledWindow): - def __init__(self): - super().__init__() - - self.treeview = MenuTreeView(MainController) - self.add(self.treeview) - - def _set_adjustment(self, adjustment: VAdjustment) -> None: - pass - - class AppHeaderBar(Gtk.HeaderBar): - def __init__(self): + def __init__(self) -> None: super().__init__() self.props.title = APP_NAME self.set_decoration_layout(":minimize,maximize,close") self.set_show_close_button(True) +# TODO: deprecated class ScrollableNote(ScrollableMixin, Gtk.Box): def __init__(self, content_box: Gtk.Box, back_button=False): super().__init__(orientation=Gtk.Orientation.VERTICAL) @@ -322,11 +308,11 @@ class Notebook(ScrollableMixin, Gtk.Notebook): AppNav.notebook = self self.prior_page: int - # TODO: set internally - self.scrollable_treelist = ScrollableTree() - self.scrollable_treelist.set_hexpand(False) - self.scrollable_treelist.set_vexpand(True) - self.scrollable_treelist.type = None + # TODO: set static help page + #self.scrollable_treelist = ScrollableTree() + #self.scrollable_treelist.set_hexpand(False) + #self.scrollable_treelist.set_vexpand(True) + #self.scrollable_treelist.type = None self.change = Changelog(MainController) self.clog = ScrollableNote(self.change, back_button=False) @@ -344,11 +330,8 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # add all treeviews as page and register them to AppNav and self.indexes # when switching to a treeview, update relevant view and just pop that page # instead of loading/unloading the model each time - from dzgui.views.pages.servers import ServerNotebook self.servers = ServerNotebook(MainController) - #self.ssu = Gtk.ScrolledWindow() - #self.servers = ServerTreeView(MainController) - #self.ssu.add(self.servers) + AppNav.servers = self.servers self.quad = Gtk.ScrolledWindow() @@ -369,9 +352,9 @@ class Notebook(ScrollableMixin, Gtk.Notebook): from dzgui.views.pages.devs import Developers developers = Developers(MainController) self.developers = ScrollableNote(developers) - + self.pages = { - self.scrollable_treelist: NotebookPage.MAIN, + #self.scrollable_treelist: NotebookPage.MAIN, self.clog: NotebookPage.CHANGELOG, self.keys: NotebookPage.KEYS, self.settings: NotebookPage.OPTIONS, @@ -491,6 +474,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.focus_current() # TODO: should be an internal property of those pages blank = [ + NotebookPage.SERVERS, NotebookPage.OPTIONS, NotebookPage.THANKS, NotebookPage.CHANGELOG, @@ -510,13 +494,11 @@ class Notebook(ScrollableMixin, Gtk.Notebook): crumbs = enum.dict["crumbs"] MainController.set_crumbs(crumbs) - # TODO: going to be deprecated after server notebook is added - if enum is not NotebookPage.MAIN: - return - - MainController.set_help_menu_crumbs() + # TODO: + # MainController.set_help_menu_crumbs() +# TODO: move to components class Statusbar(Gtk.Statusbar): def __init__(self) -> None: super().__init__() @@ -603,6 +585,14 @@ class Grid(Gtk.Grid): self.attach_next_to( self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1 ) + + #from dzgui.views.components.connect_panel import ConnectPanel + #self.conpan = ConnectPanel() + #self.attach_next_to( + # self.conpan, self.notebook, Gtk.PositionType.BOTTOM, 3, 1 + #) + + self.attach_next_to( self.statusbar, self.notebook, Gtk.PositionType.BOTTOM, 3, 1 ) @@ -615,6 +605,7 @@ class Grid(Gtk.Grid): return self.breadcrumbs.get_text() def set_breadcrumbs(self, text: str) -> None: + # TODO: embolden func self.breadcrumbs.set_markup(f"{text}") @@ -658,7 +649,12 @@ class AppNavigation: window: OuterWindow notebook: Notebook right_panel: RightPanel - treeview: MenuTreeView + #treeview: MenuTreeView + servers: Gtk.ScrolledWindow + browser: ServerTreeView + saved: ServerTreeView + recent: ServerTreeView + lan: ServerTreeView modtreeview: ModTreeView logtreeview: LogTreeView # TODO: add tree_log and tree_server views here diff --git a/dzgui/views/components/buttonbox.py b/dzgui/views/components/buttonbox.py index 6cf6976..fefce37 100644 --- a/dzgui/views/components/buttonbox.py +++ b/dzgui/views/components/buttonbox.py @@ -29,7 +29,6 @@ class ButtonBox(Gtk.Box): orientation=Gtk.Orientation.VERTICAL, ) - self.prior_button = ButtonType.MAIN_MENU self.controller = controller self.buttons = list() self.connect("key-press-event", self._on_keypress) @@ -56,6 +55,10 @@ class ButtonBox(Gtk.Box): def _on_selection_button_clicked(self, button: Gtk.Button) -> None: self.controller.open_page_by_button(button) + button.set_sensitive(False) + for b in self.buttons: + if b != button: + b.set_sensitive(True) def _walk_buttons(self, increment: int) -> None: for i, button in enumerate(self.buttons): diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index f600828..90bdd69 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -2,16 +2,31 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 +from dzgui.views.components.web_button import WebButton + class ConnectPanel(Gtk.Grid): def __init__(self) -> None: - super().__init__() + super().__init__(vexpand=False, margin=20) self.label = Gtk.Label(label="Connect by IP/ID") - self.label2 = Gtk.Label(label="Save server by IP/ID") - self.entry1 = Gtk.Entry() + self.label2 = Gtk.Label(label="Favorite server") + # TODO: embold function + self.label2.set_markup("Favorite server") + sep = Gtk.Separator() + self.entry1 = Gtk.Entry(placeholder_text="Enter IP or Battlemetrics ID", hexpand=False) self.entry2 = Gtk.Entry() + self.fav = Gtk.Label("MY favorite server") + self.favedit = Gtk.Button("Edit") + self.edit = WebButton(label="EDIT") + self.favbutton = Gtk.Button(label="Connect") self.attach(self.label, 0, 0, 3, 1) + button = Gtk.Button(label="Connect") self.attach_next_to(self.entry1, self.label, Gtk.PositionType.BOTTOM, 3, 1) - self.attach_next_to(self.label2, self.entry1, Gtk.PositionType.BOTTOM, 3, 1) - self.attach_next_to(self.entry2, self.label2, Gtk.PositionType.BOTTOM, 3, 1) + self.attach_next_to(button, self.entry1, Gtk.PositionType.RIGHT, 3, 1) + + self.attach_next_to(sep, self.entry1, Gtk.PositionType.BOTTOM, 3, 1) + self.attach_next_to(self.label2, sep, Gtk.PositionType.BOTTOM, 3, 1) + self.attach_next_to(self.fav, self.label2, Gtk.PositionType.BOTTOM, 3, 1) + self.attach_next_to(self.edit, self.fav, Gtk.PositionType.RIGHT, 3, 1) + self.attach_next_to(self.favbutton, self.edit, Gtk.PositionType.RIGHT, 3, 1) diff --git a/dzgui/views/components/web_button.py b/dzgui/views/components/web_button.py index 4d8f0ef..d0a301e 100644 --- a/dzgui/views/components/web_button.py +++ b/dzgui/views/components/web_button.py @@ -5,7 +5,7 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 - +# TODO: abstract to support other icons class WebButton(Gtk.Button): def __init__(self, label: str): super().__init__(label=label) diff --git a/dzgui/views/trees/tree_base.py b/dzgui/views/trees/tree_base.py index 7e4ce7c..1b50607 100644 --- a/dzgui/views/trees/tree_base.py +++ b/dzgui/views/trees/tree_base.py @@ -84,10 +84,15 @@ class TreeView(CursorMixin, Gtk.TreeView): if is_navkey(event.keyval): # TODO: investigate this + # TODO: get active tree if server view is open + servers = self.controller.mediator.notebook.servers + tv = servers.get_active_treeview() if self.sel_blocked is False: self.controller.suppress_signal( - self.controller.mediator.treeview, - self.controller.mediator.treeview.selected_row, + tv, + tv.selected_row, + #self.controller.mediator.treeview, + #self.controller.mediator.treeview.selected_row, "_on_tree_selection_changed", True, ) diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index 53f643c..672c053 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -80,14 +80,6 @@ class ServerTreeView(TreeView): self.append_column(column) - # TODO: do not load model on initial init - # TODO: test values: see below - #r = Gtk.ListStore( - # str, str, str, str, int, int, int, str, int, int, str, bool - #) - #r.append(["TEST", "a", "a", "a", 0, 0, 0, "a", 0, 0, "a", False]) - #self.set_model(r) - self.connect("on_distcalc_started", self._on_calclat_started) @@ -117,7 +109,7 @@ class ServerTreeView(TreeView): addr = latest_result[0] km = latest_result[1] cache[addr] = km - # TODO: fixme + # FIXME self.statusbar.append_distance(km) return True @@ -137,9 +129,6 @@ class ServerTreeView(TreeView): return True AppNav.right_panel.filters_vbox.keyword_entry.grab_focus() case Gdk.KEY_m: - # FIXME: should no longer be relevant - if AppNav.treeview.view == WindowContext.TABLE_MODS: - return True AppNav.right_panel.filters_vbox.maps_entry.grab_focus() else: keyname = Gdk.keyval_name(event.keyval) @@ -164,6 +153,7 @@ class ServerTreeView(TreeView): def _on_server_button_release( self, widget: Gtk.Widget, event: Gdk.EventButton ) -> None: + # TODO: use ContextMixin if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3: return @@ -181,6 +171,7 @@ class ServerTreeView(TreeView): self.menu = Gtk.Menu() mod_context_items = [ContextMenu.OPEN_WORKSHOP, ContextMenu.DELETE_MOD] # TODO: reimplement server context enums + # TODO: inherit from tree_servers.py server_context_items = { RowType.SERVER_BROWSER: [ ContextMenu.ADD_SERVER, @@ -221,12 +212,7 @@ class ServerTreeView(TreeView): } # TODO: how to get current server context - if self.view == WindowContext.TABLE_MODS: - items = mod_context_items - elif self.subpage in server_context_items: - items = server_context_items[self.subpage] - else: - return + items = server_context_items[self.subpage] for row in items: if row == ContextMenu.ADD_SERVER: @@ -265,22 +251,21 @@ class ServerTreeView(TreeView): print(self.get_value_at_index(0)) def _parent_selection_changed(self, base_class: TreeView, sel: Gtk.TreeSelection): - print(self.get_value_at_index(0)) self.terminate_process() record = self.get_record() - print(record) - # TODO: ? - if not record: - grid.statusbar.update_server_meta() + if record is None: + # TODO: move to controller + #grid.statusbar.update_server_meta() return ip = record.ip - self.emit("on_distcalc_started") - self.current_proc = CalcDist(self, record.ip, self.queue, cache) - self.current_proc.start() + # TODO + #self.emit("on_distcalc_started") + #self.current_proc = CalcDist(self, record.ip, self.queue, cache) + #self.current_proc.start() def get_record_string(self) -> str: addr = self.get_value_at_index(7) From 4a3d407e5f804ae41dadd757f1eef151b8b14374 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 21 Dec 2025 03:04:53 +0900 Subject: [PATCH 03/49] fix: remove main menu enum --- dzgui/const/enum.py | 2 +- dzgui/util/strings.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index b9e032f..798b855 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -82,7 +82,7 @@ class NotebookPage(EnumWithAttrs): KEYS = {"crumbs": strings.crumbs.keys,} LAN = {"crumbs": strings.crumbs.lan,} LOG = {"crumbs": strings.crumbs.log,} - MAIN = {"crumbs": strings.crumbs.main,} + HELP = {"crumbs": strings.crumbs._help,} MODS = {"crumbs": strings.crumbs.mods,} MY_SERVERS = {"crumbs": strings.crumbs.my_servers,} OPTIONS = {"crumbs": strings.crumbs.options,} diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 8a92998..de3f55c 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -258,7 +258,7 @@ class Crumbs: keys: str lan: str log: str - main: str + _help: str mods: str my_servers: str options: str @@ -444,7 +444,7 @@ crumbs = Crumbs( keys="Keybindings", lan="Main menu > Local servers", log="Help > Debug log", - main="Main menu", + _help="Help", mods="Mods", my_servers="Main menu > Saved servers", options="Options", From 37dab3e83be77eb58650005e0e810b7a383c5aca Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:40:29 +0900 Subject: [PATCH 04/49] feat: populate ServerTreeViews --- dzgui/const/enum.py | 36 +++++----- dzgui/controllers/mc.py | 28 +++++--- dzgui/controllers/model.py | 19 +++++- dzgui/util/keys.py | 5 +- dzgui/util/strings.py | 17 ++++- dzgui/views/base.py | 49 ++++++------- dzgui/views/components/buttonbox.py | 10 +-- dzgui/views/trees/tree_menu.py | 4 +- dzgui/views/trees/tree_servers.py | 102 ++++++++++------------------ 9 files changed, 140 insertions(+), 130 deletions(-) diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 798b855..fffb8b2 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -311,6 +311,7 @@ class ContextMenu(EnumWithAttrs): OPEN_WORKSHOP = {"label": strings.open_workshop} DELETE_MOD = {"label": strings.delete_mod} COPY_LOG_CLIPBOARD = {"label": strings.copy_log} + CONNECT = {"label": strings.connect} class ContextMenuGroup(Enum): @@ -324,6 +325,7 @@ class ContextMenuGroup(Enum): ContextMenu.COPY_LOG_CLIPBOARD, ) SERVER_BROWSER = ( + ContextMenu.CONNECT, ContextMenu.ADD_SERVER, ContextMenu.COPY_NAME, ContextMenu.COPY_CLIPBOARD, @@ -333,6 +335,7 @@ class ContextMenuGroup(Enum): ContextMenu.REFRESH_PLAYERS, ) SCAN_LAN = ( + ContextMenu.CONNECT, ContextMenu.COPY_NAME, ContextMenu.COPY_CLIPBOARD, ContextMenu.ADD_NOTE, @@ -341,6 +344,7 @@ class ContextMenuGroup(Enum): ContextMenu.REFRESH_PLAYERS, ) SAVED = ( + ContextMenu.CONNECT, ContextMenu.REMOVE_SERVER, ContextMenu.COPY_NAME, ContextMenu.COPY_CLIPBOARD, @@ -350,6 +354,7 @@ class ContextMenuGroup(Enum): ContextMenu.REFRESH_PLAYERS, ) RECENT = ( + ContextMenu.CONNECT, ContextMenu.ADD_SERVER, ContextMenu.REMOVE_HISTORY, ContextMenu.COPY_NAME, @@ -360,22 +365,6 @@ class ContextMenuGroup(Enum): ContextMenu.REFRESH_PLAYERS, ) - -HELP_MENU_ROWS = ( - RowType.CHANGELOG, - RowType.SHOW_LOG, - RowType.DUMP_LOG, - RowType.SEPARATOR, - RowType.DOCS, - RowType.DOCS_FALLBACK, - RowType.BUGS, - RowType.FORUM, - RowType.SPONSOR, - RowType.SEPARATOR, - RowType.THANKS, -) - - class ModButton(EnumWithAttrs): SELECT_ALL = { "label": strings.mod_panel.select_all, @@ -429,3 +418,18 @@ class ButtonType(EnumWithAttrs): "tooltip": strings.buttons.exit_tooltip, "opens": None, } + + +HELP_MENU_ROWS = ( + RowType.CHANGELOG, + RowType.SHOW_LOG, + RowType.DUMP_LOG, + RowType.SEPARATOR, + RowType.DOCS, + RowType.DOCS_FALLBACK, + RowType.BUGS, + RowType.FORUM, + RowType.SPONSOR, + RowType.SEPARATOR, + RowType.THANKS, +) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index f71cc55..ae1174d 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -68,7 +68,7 @@ class Controller: def set_help_menu_crumbs(self) -> None: # TODO: going to be deprecated after server notebook is added - tip = self.mediator.treeview.get_model()[0][0] + tip = self.mediator.menu.get_model()[0][0] store = self.model_manager.get_help_store()[0][0] if tip == store: self.set_crumbs("Help") @@ -79,8 +79,17 @@ class Controller: def get_crumbs(self) -> str: return self.mediator.grid.get_breadcrumbs() - #def get_row_store(self) -> Gtk.ListStore: - # return self.model_manager.get_row_store() + def get_server_store(self) -> Gtk.ListStore: + return self.model_manager.get_server_store() + + def get_saved_store(self) -> Gtk.ListStore: + return self.model_manager.get_saved_store() + + def get_recent_store(self) -> Gtk.ListStore: + return self.model_manager.get_recent_store() + + def get_lan_store(self) -> Gtk.ListStore: + return self.model_manager.get_lan_store() def get_help_store(self) -> Gtk.ListStore: return self.model_manager.get_help_store() @@ -137,12 +146,12 @@ class Controller: state, ) self.suppress_signal( - self.mediator.treeview, - self.mediator.treeview.selected_row, + self.mediator.menu, + self.mediator.menu.selected_row, "_on_tree_selection_changed", state, ) - self.suppress_signal(self.mediator.treeview, self.mediator.treeview, "_on_keypress", state) + self.suppress_signal(self.mediator.menu, self.mediator.menu, "_on_keypress", state) for check in self.mediator.grid.right_panel.filters_vbox.checks: self.suppress_signal( self.mediator.grid.right_panel.filters_vbox, @@ -161,7 +170,7 @@ class Controller: else: widget.handler_unblock_by_func(func) # TODO - #self.mediator.treeview.sel_blocked = state + #self.mediator.menu.sel_blocked = state def toggle_debug_mode(self) -> None: self.toggle_config(Preferences.DEBUG) @@ -278,7 +287,6 @@ class Controller: self.mediator.grid.notebook.set_page_by_enum(page) def open_page_by_button(self, button: "ContextualButton") -> None: - print(button.context) match button.context: case ButtonType.EXIT: logger.info("Normal user exit") @@ -289,9 +297,7 @@ class Controller: case ButtonType.MODS: self.load_mods() case ButtonType.HELP: - help_store = self.model_manager.get_help_store() - # TODO: simply set this model once and open the page - self.mediator.treeview.set_model(help_store) + pass case ButtonType.SERVERS: self.mediator.notebook.set_page_by_enum(button.opens) # TODO: shorten this diff --git a/dzgui/controllers/model.py b/dzgui/controllers/model.py index f401c67..7550a1c 100644 --- a/dzgui/controllers/model.py +++ b/dzgui/controllers/model.py @@ -76,9 +76,10 @@ class ModelManager: self.log_store = self.new_model_from_class(LogCols) self.modlist_store = self.new_model_from_class(ServerModCols) - #for row in MAIN_MENU_ROWS: - # label = row.dict["label"] - # self.row_store.append([label, row]) + self.server_store = self.new_model() + self.saved_store = self.new_model() + self.recent_store = self.new_model() + self.lan_store = self.new_model() for row in HELP_MENU_ROWS: label = row.dict["label"] @@ -93,6 +94,18 @@ class ModelManager: cls.instance = super(ModelManager, cls).__new__(cls) return cls.instance + def get_recent_store(self) -> ListStore: + return self.recent_store + + def get_lan_store(self) -> ListStore: + return self.lan_store + + def get_saved_store(self) -> ListStore: + return self.saved_store + + def get_server_store(self) -> ListStore: + return self.server_store + def new_model_from_class(self, cls: type) -> ListStore: store = ListStore(*[ftype for field, ftype in cls.__annotations__.items()]) return store diff --git a/dzgui/util/keys.py b/dzgui/util/keys.py index bf8c363..04f291d 100644 --- a/dzgui/util/keys.py +++ b/dzgui/util/keys.py @@ -19,8 +19,7 @@ def is_navkey(key: int) -> bool: return False def is_ctrl_mask(event: Gdk.EventKey) -> bool: - if event.keyval is Gdk.KEY_l \ - and event.state is Gdk.ModifierType.CONTROL_MASK: - return True + if event.state is Gdk.ModifierType.CONTROL_MASK: + return True else: return False diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index de3f55c..c1eefc3 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -53,8 +53,9 @@ copy_name = "Copy name to clipboard" copy_ip = "Copy IP to clipboard" copy_log = "Copy record(s) to clipboard" add = "Add to my servers" -remove = "Remove from my servers" +remove = "Remove from Saved Servers" remove_history = "Remove from history" +connect = "Connect" # Columns server_mod_cols = ["Mod", "ID", "Installed"] @@ -496,3 +497,17 @@ developers = DevelopersPage( prefs_label="Boot preferences", columns=["Key", "Value"] ) + +@dataclass(slots=True, frozen=True) +class ServerLabels: + browser: str + saved: str + recent: str + lan: str + +server_labels = ServerLabels( + browser="Server Browser", + saved="Saved Servers", + recent="Recent", + lan="LAN", +) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index c4418c6..a3c5f08 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -47,6 +47,7 @@ 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 @@ -308,21 +309,17 @@ class Notebook(ScrollableMixin, Gtk.Notebook): AppNav.notebook = self self.prior_page: int - # TODO: set static help page - #self.scrollable_treelist = ScrollableTree() - #self.scrollable_treelist.set_hexpand(False) - #self.scrollable_treelist.set_vexpand(True) - #self.scrollable_treelist.type = None + from dzgui.views.pages.help import Help + self.help = Help(MainController) + view = self.help.get_treeview() + AppNav.menu = view self.change = Changelog(MainController) self.clog = ScrollableNote(self.change, back_button=False) self.clog.scrollable.set_propagate_natural_width(False) - self.clog.type = RowType.CHANGELOG # TODO: scrollable internally self.keys = ScrollableNote(Keybindings()) - self.keys.type = RowType.KEYBINDINGS - self.settings = Options(MainController) # TODO: make all treeviews internally scrollable in base class @@ -354,6 +351,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.developers = ScrollableNote(developers) self.pages = { + self.help: NotebookPage.HELP, #self.scrollable_treelist: NotebookPage.MAIN, self.clog: NotebookPage.CHANGELOG, self.keys: NotebookPage.KEYS, @@ -452,14 +450,21 @@ class Notebook(ScrollableMixin, Gtk.Notebook): def focus_current(self) -> None: widget = self.get_page() - if widget: - w = widget.get_children()[0] - # NOTE: if item contains treeview, focus first cell - try: - w.focus_first_row() - w.grab_focus() - except Exception as e: - w.grab_focus() + if widget is None: + return + + if widget is self.servers: + view = self.servers.get_active_treeview() + view.grab_focus() + return + + w = widget.get_children()[0] + # NOTE: if item contains treeview, focus first cell + try: + w.focus_first_row() + w.grab_focus() + except Exception as e: + w.grab_focus() def get_page(self) -> Gtk.Widget | None: ind = self.get_current_page() @@ -491,13 +496,10 @@ class Notebook(ScrollableMixin, Gtk.Notebook): is_mods = True if enum is NotebookPage.MODS else False MainController.toggle_mod_panel(is_mods) + # TODO: crumbs = enum.dict["crumbs"] MainController.set_crumbs(crumbs) - # TODO: - # MainController.set_help_menu_crumbs() - - # TODO: move to components class Statusbar(Gtk.Statusbar): def __init__(self) -> None: @@ -570,6 +572,9 @@ class Grid(Gtk.Grid): AppNav.grid = self + self.breadcrumbs = Gtk.Label(halign=Gtk.Align.START) + self.set_breadcrumbs(strings.label_main_menu) + # FIXME: do not pass AppNav to right panel self.right_panel = RightPanel(AppNav, MainController) self.sel_panel = ModSelectionPanel(MainController) @@ -578,8 +583,6 @@ class Grid(Gtk.Grid): self.notebook = Notebook() self.statusbar = Statusbar() - self.breadcrumbs = Gtk.Label(halign=Gtk.Align.START) - self.set_breadcrumbs(strings.label_main_menu) self.attach(self.notebook, 0, 0, 3, 1) self.attach_next_to( @@ -649,7 +652,7 @@ class AppNavigation: window: OuterWindow notebook: Notebook right_panel: RightPanel - #treeview: MenuTreeView + menu: MenuTreeView servers: Gtk.ScrolledWindow browser: ServerTreeView saved: ServerTreeView diff --git a/dzgui/views/components/buttonbox.py b/dzgui/views/components/buttonbox.py index fefce37..65931c9 100644 --- a/dzgui/views/components/buttonbox.py +++ b/dzgui/views/components/buttonbox.py @@ -54,11 +54,13 @@ class ButtonBox(Gtk.Box): def _on_selection_button_clicked(self, button: Gtk.Button) -> None: + # # TODO: maybe drop this and just ensure buttons respond correctly self.controller.open_page_by_button(button) - button.set_sensitive(False) - for b in self.buttons: - if b != button: - b.set_sensitive(True) + # if button.context != ButtonType.HELP: + # button.set_sensitive(False) + # for b in self.buttons: + # if b != button: + # b.set_sensitive(True) def _walk_buttons(self, increment: int) -> None: for i, button in enumerate(self.buttons): diff --git a/dzgui/views/trees/tree_menu.py b/dzgui/views/trees/tree_menu.py index 0b91ff3..4ca8774 100644 --- a/dzgui/views/trees/tree_menu.py +++ b/dzgui/views/trees/tree_menu.py @@ -29,8 +29,8 @@ class MenuTreeView(TreeView): column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) self.append_column(column) - row_store = self.controller.get_row_store() - self.set_model(row_store) + help_store = self.controller.get_help_store() + self.set_model(help_store) self.selected_row = self.get_selection() diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index 672c053..98dcaa7 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -8,6 +8,7 @@ from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from dzgui.const.enum import ( ContextMenu, + ContextMenuGroup, RowType, ) from dzgui.const.constants import UDP_PORT @@ -29,6 +30,12 @@ if TYPE_CHECKING: # TODO: add multiprocessing queue # TODO: fix cache +class EnumeratedMenuItem(Gtk.MenuItem): + def __init__(self, enum: ContextMenu): + super().__init__(label=enum.dict["label"]) + self.enum = enum + + class ServerTreeView(TreeView): __gsignals__ = { "on_distcalc_started": (GObject.SignalFlags.RUN_FIRST, None, ()) @@ -36,6 +43,7 @@ class ServerTreeView(TreeView): def __init__(self, controller: "Controller") -> None: super().__init__(controller) + self.menu = Gtk.Menu() self.controller = controller self.set_fixed_height_mode(True) @@ -62,9 +70,6 @@ class ServerTreeView(TreeView): column.set_resizable(True) column.set_sort_column_id(i) - # TODO: use index of column instead of name - # so literal name won't matter - # needs conversion logic for old configs if valid_json: try: saved_size = data["cols"][column_title] @@ -81,11 +86,8 @@ class ServerTreeView(TreeView): self.append_column(column) self.connect("on_distcalc_started", self._on_calclat_started) - - self.connect("button-release-event", self._on_server_button_release) self.connect("key-press-event", self._on_server_keypress) - self.connect("generic_row_activated", self._parent_row_activated) self.connect("generic_treesel_changed", self._parent_selection_changed) @@ -150,6 +152,18 @@ class ServerTreeView(TreeView): case _: return False + def set_context_menu(self, items: ContextMenuGroup) -> None: + # TODO: if debug is on, add raw command + for item in items.value: + menu_item = EnumeratedMenuItem(item) + menu_item.connect("activate", self._on_menu_click) + self.menu.append(menu_item) + self.menu.show_all() + + def _on_menu_click(self, item) -> None: + print(item.enum) + pass + def _on_server_button_release( self, widget: Gtk.Widget, event: Gdk.EventButton ) -> None: @@ -168,69 +182,23 @@ class ServerTreeView(TreeView): except AttributeError: pass - self.menu = Gtk.Menu() mod_context_items = [ContextMenu.OPEN_WORKSHOP, ContextMenu.DELETE_MOD] - # TODO: reimplement server context enums - # TODO: inherit from tree_servers.py - server_context_items = { - RowType.SERVER_BROWSER: [ - ContextMenu.ADD_SERVER, - ContextMenu.COPY_NAME, - ContextMenu.COPY_CLIPBOARD, - ContextMenu.ADD_NOTE, - ContextMenu.SHOW_MODS, - ContextMenu.SHOW_DETAILS, - ContextMenu.REFRESH_PLAYERS, - ], - RowType.SCAN_LAN: [ - ContextMenu.COPY_NAME, - ContextMenu.COPY_CLIPBOARD, - ContextMenu.ADD_NOTE, - ContextMenu.SHOW_MODS, - ContextMenu.SHOW_DETAILS, - ContextMenu.REFRESH_PLAYERS, - ], - RowType.SAVED_SERVERS: [ - ContextMenu.REMOVE_SERVER, - ContextMenu.COPY_NAME, - ContextMenu.COPY_CLIPBOARD, - ContextMenu.ADD_NOTE, - ContextMenu.SHOW_MODS, - ContextMenu.SHOW_DETAILS, - ContextMenu.REFRESH_PLAYERS, - ], - RowType.RECENT_SERVERS: [ - ContextMenu.ADD_SERVER, - ContextMenu.REMOVE_HISTORY, - ContextMenu.COPY_NAME, - ContextMenu.ADD_NOTE, - ContextMenu.COPY_CLIPBOARD, - ContextMenu.SHOW_MODS, - ContextMenu.SHOW_DETAILS, - ContextMenu.REFRESH_PLAYERS, - ], - } - # TODO: how to get current server context - items = server_context_items[self.subpage] - - for row in items: - if row == ContextMenu.ADD_SERVER: - if self.is_in_favs(): - row = ContextMenu.REMOVE_SERVER - item = Gtk.MenuItem(label=row.dict["label"]) - item.type = row - item.action = row.dict["action"] - item.connect("activate", self._on_menu_click) - self.menu.append(item) - if row == ContextMenu.SHOW_MODS: - if not self.has_mods(): - item.set_sensitive(False) - if row == ContextMenu.ADD_NOTE: - if self.get_record_string() in notes_cache: - item.set_label(strings.edit_note) - - self.menu.show_all() + # TODO: dynamic menu entries + #for row in items: + # if row == ContextMenu.ADD_SERVER: + # if self.is_in_favs(): + # row = ContextMenu.REMOVE_SERVER + # item = Gtk.MenuItem(label=row.dict["label"]) + # item.type = row + # item.action = row.dict["action"] + # self.menu.append(item) + # if row == ContextMenu.SHOW_MODS: + # if not self.has_mods(): + # item.set_sensitive(False) + # if row == ContextMenu.ADD_NOTE: + # if self.get_record_string() in notes_cache: + # item.set_label(strings.edit_note) if event.type is Gdk.EventType.KEY_PRESS and event.keyval is Gdk.KEY_l: if self.is_selection_empty(): From 248978f993c3f278b8ed09357f440af22bafd8f0 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:03:44 +0900 Subject: [PATCH 05/49] fix: update crumbs when paginating --- dzgui/const/constants.py | 1 + dzgui/const/enum.py | 3 - dzgui/controllers/mc.py | 13 ++-- dzgui/util/css.py | 2 + dzgui/util/strings.py | 8 +- dzgui/views/base.py | 111 ++++----------------------- dzgui/views/components/web_button.py | 26 +++++-- dzgui/views/trees/tree_menu.py | 1 - dzgui/views/trees/tree_servers.py | 11 ++- 9 files changed, 54 insertions(+), 122 deletions(-) diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index a004505..9782bab 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -34,6 +34,7 @@ VIEW_CONCEAL = "view-conceal-symbolic" VIEW_REVEAL = "view-reveal-symbolic" INPUT_KEYBOARD = "input-keyboard-symbolic" HELP_BUBBLE = "help-about-symbolic" +REFRESH_ICON = "view-refresh-symbolic" WEB_BROWSER = "web-browser-symbolic" SEPARATOR = "SEPARATOR" diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index fffb8b2..111a5fc 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -78,13 +78,10 @@ class Preferences(EnumWithAttrs): class NotebookPage(EnumWithAttrs): CHANGELOG = {"crumbs": strings.crumbs.changelog,} - HISTORY = {"crumbs": strings.crumbs.history,} KEYS = {"crumbs": strings.crumbs.keys,} - LAN = {"crumbs": strings.crumbs.lan,} LOG = {"crumbs": strings.crumbs.log,} HELP = {"crumbs": strings.crumbs._help,} MODS = {"crumbs": strings.crumbs.mods,} - MY_SERVERS = {"crumbs": strings.crumbs.my_servers,} OPTIONS = {"crumbs": strings.crumbs.options,} SERVERS = {"crumbs": strings.crumbs.servers,} THANKS = {"crumbs": strings.crumbs.thanks,} diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index ae1174d..b058c40 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -556,10 +556,10 @@ class Controller: def get_developer_mode(self) -> bool: return self.is_developer - def set_server_statusbar(self) -> None: - # TODO: different models - model = self.model_manager.get_server_store() - model = AppNav.treeview.get_model() + def update_server_status(self) -> None: + from dzgui.util.format import pluralize + treeview = self.mediator.notebook.servers.get_active_treeview() + model = treeview.get_model() if model is None: players = 0 hits = 0 @@ -568,7 +568,6 @@ class Controller: players = 0 for row in model: players += row[4] - # TODO: move to util.format players_pretty = pluralize("players", players) hits_pretty = pluralize("matches", hits) @@ -579,5 +578,5 @@ class Controller: if players == 0: suffix = "" - self.set_text(formatted + suffix) - self.players = formatted + self.mediator.statusbar.set_text(formatted + suffix) + #self.players = formatted diff --git a/dzgui/util/css.py b/dzgui/util/css.py index d53188d..90411ad 100644 --- a/dzgui/util/css.py +++ b/dzgui/util/css.py @@ -11,6 +11,8 @@ def add_class(widget: Gtk.Widget, label: str) -> None: context = widget.get_style_context() context.add_class(label) +# TODO: def remove_class() + def load_css() -> None: # TODO: consider storing this in a data file css = """ diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index c1eefc3..88975cb 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -255,13 +255,10 @@ class Button: @dataclass(slots=True, frozen=True) class Crumbs: changelog: str - history: str keys: str - lan: str log: str _help: str mods: str - my_servers: str options: str servers: str thanks: str @@ -441,15 +438,12 @@ buttons = Button( crumbs = Crumbs( changelog="Help > Changelog", - history="Main menu > Recent servers", keys="Keybindings", - lan="Main menu > Local servers", log="Help > Debug log", _help="Help", mods="Mods", - my_servers="Main menu > Saved servers", options="Options", - servers="Servers", + servers="Servers > Server browser", thanks="Help > Special thanks", developers="Options > Developers", ) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index a3c5f08..5904433 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -40,6 +40,7 @@ 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.mod_panel import ModSelectionPanel from dzgui.views.components.right_panel import RightPanel from dzgui.views.components.toast import Toast @@ -65,19 +66,7 @@ logger = logging.getLogger(__name__) warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) # TODO: move to controller -cache: dict[str, int] = {} - -# TODO: move to strings.py -def format_metadata(row: RowType) -> str: - prefix = row.dict["tooltip"] - - if row == RowType.QUICK_CONNECT or row == RowType.CHNG_FAV: - label = MainController.query_config(Preferences.FAV_LBL) - if len(label) < 1: - label = "unset" - return f"{prefix} ({label})" - else: - return prefix +#cache: dict[str, int] = {} ## TODO: move to configs/servers #def query_history() -> list | None: @@ -228,23 +217,14 @@ class OuterWindow(Gtk.Window): MainController.set_resolution(self) self.grid = Grid() - self.toast = Toast() - self.overlay = Gtk.Overlay() - self.overlay.add_overlay(self.grid) - # TODO: toast is deprecated - self.overlay.add_overlay(self.toast) - self.add(self.overlay) + self.add(self.grid) self.show_all() - self.toast.set_visible(False) self.grid.right_panel.filters_vbox.set_visible(False) self.grid.right_panel.enable_ping_button(False) self.grid.sel_panel.set_visible(False) - # TODO: register from treeview or notebook - #AppNav.treeview = self.grid.notebook.scrollable_treelist.treeview - css.load_css() AppNav.grid.notebook.set_page_by_enum(NotebookPage.SERVERS) @@ -416,6 +396,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): def return_prior(self) -> None: page = self.get_nth_page(self.prior_page) # FIXME: check enum for NotebookPage.OPTIONS + # TODO: can be dropped? if hasattr(page, "steam_entry"): """ Gtk.Notebook focuses the first input field when changing pages; @@ -479,7 +460,6 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.focus_current() # TODO: should be an internal property of those pages blank = [ - NotebookPage.SERVERS, NotebookPage.OPTIONS, NotebookPage.THANKS, NotebookPage.CHANGELOG, @@ -496,99 +476,35 @@ class Notebook(ScrollableMixin, Gtk.Notebook): is_mods = True if enum is NotebookPage.MODS else False MainController.toggle_mod_panel(is_mods) - # TODO: + # TODO: crumbs = enum.dict["crumbs"] MainController.set_crumbs(crumbs) -# TODO: move to components -class Statusbar(Gtk.Statusbar): - def __init__(self) -> None: - super().__init__() - - help_text = strings.statusbar_helptext - self.set_text(help_text) - - version = MainController.get_prefs().version - self.status_right_label = Gtk.Label(label=version) - self.add(self.status_right_label) - - self.players = "" - - def get_text(self) -> str: - area = self.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) - - def refresh(self, row: RowType) -> None: - if row is None: - formatted = "" - else: - formatted = format_metadata(row) - self.set_text(formatted) - - def append_distance(self, dist: str) -> None: - # TODO: process strings in controller - if dist == strings.unknown: - dist = f"| Distance: {dist}" - else: - dist = f"| Distance: {dist}" - self.set_text(self.players + dist) - - def update_server_meta(self) -> None: - model = AppNav.treeview.get_model() - if model is None: - players = 0 - hits = 0 - else: - hits = len(model) - players = 0 - for row in model: - players += row[4] - - # TODO: move to util.format - players_pretty = pluralize("players", players) - hits_pretty = pluralize("matches", hits) - formatted = ( - f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}" - ) - suffix = "| Distance: calculating..." - - if players == 0: - suffix = "" - self.set_text(formatted + suffix) - self.players = formatted - class Grid(Gtk.Grid): def __init__(self) -> None: super().__init__() self.set_column_homogeneous(True) - AppNav.grid = self - + self.statusbar = Statusbar(MainController) self.breadcrumbs = Gtk.Label(halign=Gtk.Align.START) self.set_breadcrumbs(strings.label_main_menu) + AppNav.statusbar = self.statusbar + AppNav.grid = self + # FIXME: do not pass AppNav to right panel self.right_panel = RightPanel(AppNav, MainController) self.sel_panel = ModSelectionPanel(MainController) self.right_panel.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, 0) self.notebook = Notebook() - self.statusbar = Statusbar() - self.attach(self.notebook, 0, 0, 3, 1) self.attach_next_to( self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1 ) - + #from dzgui.views.components.connect_panel import ConnectPanel #self.conpan = ConnectPanel() #self.attach_next_to( @@ -629,6 +545,11 @@ class App(Gtk.Application): ) self.win.add_accel_group(accel) + # FIXME: hacky + AppNav.notebook.servers.notebook.next_page() + AppNav.notebook.servers.notebook.prev_page() + MainController.focus_notebook() + GLib.unix_signal_add( GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint ) @@ -652,6 +573,7 @@ class AppNavigation: window: OuterWindow notebook: Notebook right_panel: RightPanel + statusbar: Statusbar menu: MenuTreeView servers: Gtk.ScrolledWindow browser: ServerTreeView @@ -661,6 +583,7 @@ class AppNavigation: modtreeview: ModTreeView logtreeview: LogTreeView # TODO: add tree_log and tree_server views here + # or obtain via get_treeview() AppNav = AppNavigation() MainController = Controller() diff --git a/dzgui/views/components/web_button.py b/dzgui/views/components/web_button.py index d0a301e..dc0b71d 100644 --- a/dzgui/views/components/web_button.py +++ b/dzgui/views/components/web_button.py @@ -5,11 +5,25 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 -# TODO: abstract to support other icons -class WebButton(Gtk.Button): - def __init__(self, label: str): - super().__init__(label=label) - icon = Icon(WEB_BROWSER, l_margin=5) - self.set_image(icon) +class IconButton(Gtk.Button): + def __init__(self, icon: str, margin: int = 0): + super().__init__() + + i = Icon(icon, l_margin=margin) + self.set_image(i) self.set_image_position(Gtk.PositionType.RIGHT) + + +class IconTextButton(IconButton): + def __init__(self, icon: str, label: str): + super().__init__(icon, margin=5) + + self.set_label(label) + + +class WebButton(IconTextButton): + def __init__(self, label: str): + super().__init__(icon=WEB_BROWSER, label=label) + + pass diff --git a/dzgui/views/trees/tree_menu.py b/dzgui/views/trees/tree_menu.py index 4ca8774..da9cd9e 100644 --- a/dzgui/views/trees/tree_menu.py +++ b/dzgui/views/trees/tree_menu.py @@ -44,7 +44,6 @@ class MenuTreeView(TreeView): column: Gtk.TreeViewColumn ) -> None: row_type = self.get_value_at_index(1) - print(row_type) match row_type: case RowType.THANKS: diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index 98dcaa7..c3b52a6 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -216,17 +216,20 @@ class ServerTreeView(TreeView): column: Gtk.TreeViewColumn ) -> None: # TODO: process server connection + # TODO: get record print(self.get_value_at_index(0)) def _parent_selection_changed(self, base_class: TreeView, sel: Gtk.TreeSelection): self.terminate_process() record = self.get_record() + #TODO + return - if record is None: - # TODO: move to controller - #grid.statusbar.update_server_meta() - return + #model = self.get_model() + #if record is None: + # self.controller.update_server_status(model) + # return ip = record.ip From c0df63b9d893ddf1f365161e440b708182a326cd Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 27 Dec 2025 19:40:08 +0900 Subject: [PATCH 06/49] chore: move import --- dzgui/views/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 5904433..0276a03 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -35,6 +35,7 @@ from dzgui.util.format import pluralize # NOTEBOOK ITEMS # TODO: import notebook only and add components there? from dzgui.views.pages.changelog import Changelog +from dzgui.views.pages.devs import Developers from dzgui.views.pages.keys import Keybindings from dzgui.views.pages.options import Options from dzgui.views.pages.servers import ServerNotebook @@ -326,7 +327,6 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.thanks = ScrollableNote(Thanks(), back_button=False) - from dzgui.views.pages.devs import Developers developers = Developers(MainController) self.developers = ScrollableNote(developers) From 21b6da84713a735f20f3de34e929b31a44e78164 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 27 Dec 2025 19:40:48 +0900 Subject: [PATCH 07/49] feat: col width WIP --- dzgui/controllers/mc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index b058c40..61e071a 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -56,6 +56,7 @@ if TYPE_CHECKING: from dzgui.views.base import AppNavigation from dzgui.const.enum import ContextMenu, RowType from dzgui.views.base import OuterWindow + from dzgui.views.trees.tree_servers import ServerTreeView class Controller: def __init__(self) -> None: @@ -580,3 +581,6 @@ class Controller: suffix = "" self.mediator.statusbar.set_text(formatted + suffix) #self.players = formatted + + def update_column_width(self, title: str, width: int) -> None: + self.mediator.servers.update_tab_widths(title, width) From ef2742443f24a653dc41c693ffaaaade44185b03 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 27 Dec 2025 19:41:20 +0900 Subject: [PATCH 08/49] feat: col width WIP --- dzgui/views/trees/tree_servers.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index c3b52a6..857ce1c 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -46,6 +46,8 @@ class ServerTreeView(TreeView): self.menu = Gtk.Menu() self.controller = controller + self.resizable_cols: list[Gtk.TreeViewColumn] = [] + self.set_fixed_height_mode(True) self.set_headers_visible(True) @@ -83,6 +85,8 @@ class ServerTreeView(TreeView): if column_title == "Map": column.set_fixed_width(300) + self.resizable_cols.append(column) + column.connect("notify::fixed-width", self._on_col_width_changed) self.append_column(column) self.connect("on_distcalc_started", self._on_calclat_started) @@ -93,6 +97,28 @@ class ServerTreeView(TreeView): GLib.timeout_add(200, self._check_result_queue) + def _on_col_width_changed( + self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt + ) -> None: + title = col.get_title() + size = col.get_width() + + prefs = self.controller.get_prefs() + columns = prefs.paths.columns + try: + data = JSON.read_json(columns) + data["cols"][title] = size + except Exception as e: + logger.critical(e) + data = {"cols": {title: size}} + + try: + JSON.write_json(data, columns) + except Exception as e: + logger.critical(e) + + #self.controller.update_column_width(title, size) + def terminate_process(self) -> None: if self.current_proc and self.current_proc.is_alive(): self.current_proc.terminate() From ad6bdb2e3c46a1675376bfd477d1080b1a13d91e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:14:35 +0900 Subject: [PATCH 09/49] feat: server connection panel WIP --- dzgui/const/enum.py | 5 ++++ dzgui/controllers/mc.py | 22 ++++++++++---- dzgui/util/strings.py | 3 +- dzgui/views/base.py | 12 ++++---- dzgui/views/components/connect_panel.py | 39 ++++++++++++++++--------- dzgui/views/trees/tree_servers.py | 37 ++++++++++++++++++++++- 6 files changed, 91 insertions(+), 27 deletions(-) diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 111a5fc..224a5b9 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -297,6 +297,7 @@ class RowType(EnumWithAttrs): # TODO: rename to ContextItem class ContextMenu(EnumWithAttrs): ADD_SERVER = {"label": strings.add} + ADD_FAV = {"label": strings.add_fav} REMOVE_SERVER = {"label": strings.remove} COPY_NAME = {"label": strings.copy_name} COPY_CLIPBOARD = {"label": strings.copy_ip} @@ -324,6 +325,7 @@ class ContextMenuGroup(Enum): SERVER_BROWSER = ( ContextMenu.CONNECT, ContextMenu.ADD_SERVER, + ContextMenu.ADD_FAV, ContextMenu.COPY_NAME, ContextMenu.COPY_CLIPBOARD, ContextMenu.ADD_NOTE, @@ -333,6 +335,7 @@ class ContextMenuGroup(Enum): ) SCAN_LAN = ( ContextMenu.CONNECT, + ContextMenu.ADD_FAV, ContextMenu.COPY_NAME, ContextMenu.COPY_CLIPBOARD, ContextMenu.ADD_NOTE, @@ -342,6 +345,7 @@ class ContextMenuGroup(Enum): ) SAVED = ( ContextMenu.CONNECT, + ContextMenu.ADD_FAV, ContextMenu.REMOVE_SERVER, ContextMenu.COPY_NAME, ContextMenu.COPY_CLIPBOARD, @@ -353,6 +357,7 @@ class ContextMenuGroup(Enum): RECENT = ( ContextMenu.CONNECT, ContextMenu.ADD_SERVER, + ContextMenu.ADD_FAV, ContextMenu.REMOVE_HISTORY, ContextMenu.COPY_NAME, ContextMenu.COPY_CLIPBOARD, diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 61e071a..8fe3989 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -39,7 +39,7 @@ from dzgui.util.diag import write_diagnostic from dzgui.util import cooldown, strings from dzgui.util._json import read_json, write_json from dzgui.util.open_links import open_workshop_page -from dzgui.util.format import format_mods +from dzgui.util.format import format_mods, pluralize from dzgui.util.redact import redact_log from dzgui.views.dialogs.filepicker import FilePicker @@ -56,7 +56,6 @@ if TYPE_CHECKING: from dzgui.views.base import AppNavigation from dzgui.const.enum import ContextMenu, RowType from dzgui.views.base import OuterWindow - from dzgui.views.trees.tree_servers import ServerTreeView class Controller: def __init__(self) -> None: @@ -558,7 +557,6 @@ class Controller: return self.is_developer def update_server_status(self) -> None: - from dzgui.util.format import pluralize treeview = self.mediator.notebook.servers.get_active_treeview() model = treeview.get_model() if model is None: @@ -575,12 +573,24 @@ class Controller: formatted = ( f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}" ) - suffix = "| Distance: calculating..." + suffix = " | Distance: calculating..." if players == 0: suffix = "" self.mediator.statusbar.set_text(formatted + suffix) #self.players = formatted - def update_column_width(self, title: str, width: int) -> None: - self.mediator.servers.update_tab_widths(title, width) + def propagate_column_width(self, col: Gtk.TreeViewColumn) -> None: + GLib.idle_add(self.mediator.servers.update_tab_widths, col) +# tabs = self.mediator.servers.get_tabs() +# tree = self.mediator.servers.get_active_treeview() +# width = col.get_width() +# title = col.get_title() +# for tab in tabs: +# if tab == tree: +# continue +# for col in tab.get_columns(): +# if col.get_title() == title: +# self.suppress_signal(tab, col, "_on_col_width_changed", True) +# col.set_fixed_width(width) +# self.suppress_signal(tab, col, "_on_col_width_changed", False) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 88975cb..07690b4 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -53,7 +53,8 @@ copy_name = "Copy name to clipboard" copy_ip = "Copy IP to clipboard" copy_log = "Copy record(s) to clipboard" add = "Add to my servers" -remove = "Remove from Saved Servers" +add_fav = "Set as favorite" +remove = "Remove from my servers" remove_history = "Remove from history" connect = "Connect" diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 0276a03..f5834cb 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -505,15 +505,15 @@ class Grid(Gtk.Grid): self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1 ) - #from dzgui.views.components.connect_panel import ConnectPanel - #self.conpan = ConnectPanel() - #self.attach_next_to( - # self.conpan, self.notebook, Gtk.PositionType.BOTTOM, 3, 1 - #) + from dzgui.views.components.connect_panel import ConnectPanel + self.conpan = ConnectPanel() + self.attach_next_to( + self.conpan, self.notebook, Gtk.PositionType.BOTTOM, 3, 1 + ) self.attach_next_to( - self.statusbar, self.notebook, Gtk.PositionType.BOTTOM, 3, 1 + self.statusbar, self.conpan, Gtk.PositionType.BOTTOM, 3, 1 ) self.attach_next_to( self.right_panel, self.notebook, Gtk.PositionType.RIGHT, 1, 1 diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index 90bdd69..5cdc55c 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -4,29 +4,42 @@ from gi.repository import Gtk # noqa E402 from dzgui.views.components.web_button import WebButton -class ConnectPanel(Gtk.Grid): +class ConnectPanel(Gtk.Frame): def __init__(self) -> None: - super().__init__(vexpand=False, margin=20) + super().__init__(margin_top=10, margin_bottom=5) - self.label = Gtk.Label(label="Connect by IP/ID") + self.label = Gtk.Label(label="Add/connect") self.label2 = Gtk.Label(label="Favorite server") # TODO: embold function + #self.label2.set_markup("Favorite server") self.label2.set_markup("Favorite server") sep = Gtk.Separator() self.entry1 = Gtk.Entry(placeholder_text="Enter IP or Battlemetrics ID", hexpand=False) self.entry2 = Gtk.Entry() - self.fav = Gtk.Label("MY favorite server") + self.fav = Gtk.Label(label="MY favorite server very long title") self.favedit = Gtk.Button("Edit") self.edit = WebButton(label="EDIT") self.favbutton = Gtk.Button(label="Connect") - self.attach(self.label, 0, 0, 3, 1) - button = Gtk.Button(label="Connect") - self.attach_next_to(self.entry1, self.label, Gtk.PositionType.BOTTOM, 3, 1) - self.attach_next_to(button, self.entry1, Gtk.PositionType.RIGHT, 3, 1) + # TODO: dedent + long = """IP: Format as IP:Query port, e.g.\n192.168.1.1:27016\n + Battlemetrics: numeric server ID + """ - self.attach_next_to(sep, self.entry1, Gtk.PositionType.BOTTOM, 3, 1) - self.attach_next_to(self.label2, sep, Gtk.PositionType.BOTTOM, 3, 1) - self.attach_next_to(self.fav, self.label2, Gtk.PositionType.BOTTOM, 3, 1) - self.attach_next_to(self.edit, self.fav, Gtk.PositionType.RIGHT, 3, 1) - self.attach_next_to(self.favbutton, self.edit, Gtk.PositionType.RIGHT, 3, 1) + # TODO: add tooltips to all buttons + self.entry1.set_tooltip_text(long) + + self.con = Gtk.Button(label="Connect") + self.addb = Gtk.Button(label="Add") + + self.grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5) + self.grid.attach(self.label2, 0, 0, 3, 1) + button = Gtk.Button(label="Connect") + self.grid.attach_next_to(self.fav, self.label2, Gtk.PositionType.RIGHT, 3, 1) + self.grid.attach_next_to(self.favbutton, self.fav, Gtk.PositionType.RIGHT, 3, 1) + self.grid.attach_next_to(self.label, self.label2, Gtk.PositionType.BOTTOM, 3, 1) + self.grid.attach_next_to(self.entry1, self.label, Gtk.PositionType.RIGHT, 3, 1) + self.grid.attach_next_to(self.addb, self.entry1, Gtk.PositionType.RIGHT, 3, 1) + self.grid.attach_next_to(self.con, self.addb, Gtk.PositionType.RIGHT, 3, 1) + + self.add(self.grid) diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index 857ce1c..e428b0d 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -44,6 +44,7 @@ class ServerTreeView(TreeView): super().__init__(controller) self.menu = Gtk.Menu() + self.menu.connect("key-press-event", self._on_key) self.controller = controller self.resizable_cols: list[Gtk.TreeViewColumn] = [] @@ -85,6 +86,7 @@ class ServerTreeView(TreeView): if column_title == "Map": column.set_fixed_width(300) + self.resizable_cols.append(column) column.connect("notify::fixed-width", self._on_col_width_changed) self.append_column(column) @@ -97,9 +99,41 @@ class ServerTreeView(TreeView): GLib.timeout_add(200, self._check_result_queue) + def _on_key(self, menu: Gtk.Menu, event: Gdk.EventKey) -> bool | None: + if not is_navkey(event.keyval): + return False + sel = menu.get_selected_item() + children = menu.get_children() + for i, child in enumerate(children): + if sel is child: + ind = i + break + + match event.keyval: + case Gdk.KEY_j: + if ind == len(children) - 1: + return True + menu.select_item(children[ind+1]) + case Gdk.KEY_k: + if ind - 1 < 0: + return True + menu.select_item(children[ind-1]) + case Gdk.KEY_g: + menu.select_item(children[0]) + case Gdk.KEY_G: + ind = len(children) - 1 + menu.select_item(children[ind]) + case _: + return False + return True + def _on_col_width_changed( self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt ) -> None: + """ + Propagate width change to other tabs + """ + # TODO: only update res file on quit title = col.get_title() size = col.get_width() @@ -117,7 +151,8 @@ class ServerTreeView(TreeView): except Exception as e: logger.critical(e) - #self.controller.update_column_width(title, size) + # NOTE: get final width after drag action completes + GLib.idle_add(self.controller.propagate_column_width, col) def terminate_process(self) -> None: if self.current_proc and self.current_proc.is_alive(): From a5140d7761d300d5dbce7295e390b0f96dc3eb08 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:34:34 +0900 Subject: [PATCH 10/49] fix: hide connect panel contextually --- dzgui/views/base.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index f5834cb..e8dbb28 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -35,6 +35,7 @@ from dzgui.util.format import pluralize # 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.pages.devs import Developers from dzgui.views.pages.keys import Keybindings from dzgui.views.pages.options import Options @@ -458,7 +459,13 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.prior_page = self.get_current_page() self.set_current_page(self.indexes[enum]) self.focus_current() + # TODO: should be an internal property of those pages + if enum is NotebookPage.SERVERS: + AppNav.grid.show_connect_panel() + else: + AppNav.grid.hide_connect_panel() + blank = [ NotebookPage.OPTIONS, NotebookPage.THANKS, @@ -505,13 +512,11 @@ class Grid(Gtk.Grid): self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1 ) - from dzgui.views.components.connect_panel import ConnectPanel self.conpan = ConnectPanel() self.attach_next_to( self.conpan, self.notebook, Gtk.PositionType.BOTTOM, 3, 1 ) - self.attach_next_to( self.statusbar, self.conpan, Gtk.PositionType.BOTTOM, 3, 1 ) @@ -520,6 +525,12 @@ class Grid(Gtk.Grid): ) self.show_all() + def hide_connect_panel(self) -> None: + self.conpan.set_visible(False) + + def show_connect_panel(self) -> None: + self.conpan.set_visible(True) + def get_breadcrumbs(self) -> str: return self.breadcrumbs.get_text() From 218462291809f563eb4fa49498c60a9176e8ca25 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:35:05 +0900 Subject: [PATCH 11/49] fix: handle remote repo timeout --- dzgui/init/update.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dzgui/init/update.py b/dzgui/init/update.py index 222374c..22fb189 100644 --- a/dzgui/init/update.py +++ b/dzgui/init/update.py @@ -13,10 +13,14 @@ from dzgui.init.prefix import is_prefix_writeable def get_latest_release() -> str | None: tag = None for url in [GITHUB_RELEASES, CODEBERG_RELEASES]: - res = requests.get(url, timeout=REQUEST_TIMEOUT) - if res.status_code == 200: - tag = res.json()["tag_name"] - break + try: + res = requests.get(url, timeout=REQUEST_TIMEOUT) + if res.status_code == 200: + tag = res.json()["tag_name"] + break + except Exception as e: + # TODO: log exception + continue return tag def allow_updates(allow: bool) -> bool: From 5a76c37839f4cf9210f76f207937a9ca0ff2e611 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:39:47 +0900 Subject: [PATCH 12/49] chore: move is_developer to UserPrefs --- dzgui/config/userprefs.py | 1 + dzgui/controllers/mc.py | 12 +++--------- dzgui/main.py | 3 ++- dzgui/views/base.py | 3 +-- dzgui/views/pages/options.py | 5 ++++- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/dzgui/config/userprefs.py b/dzgui/config/userprefs.py index 5576544..d7c09f9 100644 --- a/dzgui/config/userprefs.py +++ b/dzgui/config/userprefs.py @@ -6,6 +6,7 @@ from dzgui.util.ip import Coords class UserPrefs: is_steam_deck: bool is_game_mode: bool + is_developer: bool coords: Coords | None version: str allow_updates: bool diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 8fe3989..c520817 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -59,7 +59,6 @@ if TYPE_CHECKING: class Controller: def __init__(self) -> None: - self.is_developer: bool self.mediator: AppNavigation self.prefs: UserPrefs self.cooldown = 0 @@ -375,7 +374,6 @@ class Controller: msg = format_mods(total_size, total_mods) self.mediator.grid.statusbar.set_text(msg) - def calc_mod_size(self) -> tuple[int, int]: model = self.model_manager.get_mod_store() total_mods = len(model) @@ -387,10 +385,12 @@ class Controller: def menu_action(self, action: ContextMenu, path: Gtk.TreePath) -> None: match action: - # NOTE: manipulates server store + # NOTE: manipulates server stores # TODO: unimplemented case ContextMenu.ADD_SERVER: pass + case ContextMenu.ADD_FAV: + pass case ContextMenu.ADD_NOTE: pass case ContextMenu.COPY_CLIPBOARD: @@ -550,12 +550,6 @@ class Controller: logger.info(f"Using default window size {w},{h}") window.set_default_size(w, h) - def set_developer_mode(self, mode: bool) -> None: - self.is_developer = mode - - def get_developer_mode(self) -> bool: - return self.is_developer - def update_server_status(self) -> None: treeview = self.mediator.notebook.servers.get_active_treeview() model = treeview.get_model() diff --git a/dzgui/main.py b/dzgui/main.py index 166f70e..c9b692b 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -111,10 +111,11 @@ def main() -> None: prefs = UserPrefs( _is_steam_deck, _is_game_mode, + args.developers, local_coords, version, allow, XDG ) print("All OK. Loading UI...") - App(prefs, args.developers) + App(prefs) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index e8dbb28..091e78b 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -540,10 +540,9 @@ class Grid(Gtk.Grid): class App(Gtk.Application): - def __init__(self, prefs, is_developer) -> None: + def __init__(self, prefs) -> None: GLib.set_prgname(APP_NAME) MainController.set_prefs(prefs) - MainController.set_developer_mode(is_developer) self.win = OuterWindow() diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 4dc05ec..95fcb37 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -167,7 +167,10 @@ class Options(Gtk.Box): developers=Gtk.Button(label="Developers", halign=Gtk.Align.START) developers.connect("clicked", self._on_developers_clicked) - if self.controller.get_developer_mode(): + + prefs = self.controller.get_prefs() + is_developer = prefs.is_developer + if is_developer: grid.attach( developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT ) From b0565bc7f1c5e1a2c6faaca41c61e1a33ecf98f4 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:56:44 +0900 Subject: [PATCH 13/49] feat: log timeout error when attempting update --- dzgui/init/update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dzgui/init/update.py b/dzgui/init/update.py index 22fb189..7449a61 100644 --- a/dzgui/init/update.py +++ b/dzgui/init/update.py @@ -1,3 +1,4 @@ +import logging import requests import subprocess import sys @@ -9,6 +10,7 @@ from dzgui.const.constants import APP_NAME_LOWER, REQUEST_TIMEOUT from dzgui.const.endpoints import GITHUB_RELEASES, CODEBERG_RELEASES from dzgui.init.prefix import is_prefix_writeable +logger = logging.getLogger(__name__) def get_latest_release() -> str | None: tag = None @@ -19,7 +21,7 @@ def get_latest_release() -> str | None: tag = res.json()["tag_name"] break except Exception as e: - # TODO: log exception + logger.critical(e) continue return tag From 029bc1edbd18a803c1b660c30887353c04171016 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:57:06 +0900 Subject: [PATCH 14/49] chore: add annotations --- dzgui/views/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 091e78b..ecb8588 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -259,7 +259,7 @@ class AppHeaderBar(Gtk.HeaderBar): # TODO: deprecated class ScrollableNote(ScrollableMixin, Gtk.Box): - def __init__(self, content_box: Gtk.Box, back_button=False): + def __init__(self, content_box: Gtk.Box, back_button: bool = False): super().__init__(orientation=Gtk.Orientation.VERTICAL) self.scrollable = Gtk.ScrolledWindow() @@ -330,10 +330,9 @@ class Notebook(ScrollableMixin, Gtk.Notebook): developers = Developers(MainController) self.developers = ScrollableNote(developers) - + self.pages = { self.help: NotebookPage.HELP, - #self.scrollable_treelist: NotebookPage.MAIN, self.clog: NotebookPage.CHANGELOG, self.keys: NotebookPage.KEYS, self.settings: NotebookPage.OPTIONS, From 167935b96328c1dde526babf64d84e3ee783b7b6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 30 Dec 2025 21:27:30 +0900 Subject: [PATCH 15/49] fix: update statusbars/crumbs correctly --- dzgui/controllers/mc.py | 21 ++++++++---- dzgui/views/base.py | 66 ++++++++++++++++-------------------- dzgui/views/pages/options.py | 9 +++++ 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index c520817..536d581 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -21,6 +21,7 @@ from dzgui.const.enum import ( NotebookPage, ButtonType, ContextMenu, + RowType ) from dzgui.const.constants import ( @@ -54,11 +55,11 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from dzgui.views.components.buttonbox import ContextualButton from dzgui.views.base import AppNavigation - from dzgui.const.enum import ContextMenu, RowType from dzgui.views.base import OuterWindow class Controller: def __init__(self) -> None: + self.crumbs_cache = "" self.mediator: AppNavigation self.prefs: UserPrefs self.cooldown = 0 @@ -72,8 +73,8 @@ class Controller: if tip == store: self.set_crumbs("Help") - def set_crumbs(self, crumbs: str) -> None: - self.mediator.grid.set_breadcrumbs(crumbs) + def set_crumbs(self, text: str) -> None: + self.mediator.grid.set_breadcrumbs(text) def get_crumbs(self) -> str: return self.mediator.grid.get_breadcrumbs() @@ -293,15 +294,16 @@ class Controller: return case ButtonType.OPTIONS: self.mediator.grid.notebook.settings.populate_settings() + self.mediator.grid.statusbar.refresh(None) case ButtonType.MODS: self.load_mods() case ButtonType.HELP: + self.mediator.grid.statusbar.refresh(RowType.CHANGELOG) pass case ButtonType.SERVERS: self.mediator.notebook.set_page_by_enum(button.opens) - # TODO: shorten this - treeview = self.mediator.notebook.servers.get_active_treeview() - treeview.grab_focus() + # TODO: use cache + self.update_server_status() return self.mediator.notebook.set_page_by_enum(button.opens) @@ -588,3 +590,10 @@ class Controller: # self.suppress_signal(tab, col, "_on_col_width_changed", True) # col.set_fixed_width(width) # self.suppress_signal(tab, col, "_on_col_width_changed", False) + + def set_crumbs_cache(self,text: str) -> None: + self.crumbs_cache = text + + def get_crumbs_cache(self) -> None: + return self.crumbs_cache + self.crumbs_cache = text diff --git a/dzgui/views/base.py b/dzgui/views/base.py index ecb8588..f8f6544 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -289,7 +289,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): super().__init__(show_tabs=False, show_border=False) AppNav.notebook = self - self.prior_page: int + self.prior_page: NotebookPage from dzgui.views.pages.help import Help self.help = Help(MainController) @@ -394,26 +394,20 @@ class Notebook(ScrollableMixin, Gtk.Notebook): page._set_adjustment(VAdjustment.BOTTOM) def return_prior(self) -> None: - page = self.get_nth_page(self.prior_page) - # FIXME: check enum for NotebookPage.OPTIONS - # TODO: can be dropped? - if hasattr(page, "steam_entry"): - """ - 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 + """ + 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 to keybindings dialog - """ - entries = page.steam_entry, page.bm_entry - for entry in entries: - entry.set_position(-1) - entry.set_can_focus(False) - self.set_current_page(self.prior_page) - for entry in entries: - entry.set_can_focus(True) - self.set_current_page(self.prior_page) + Used when switching back from NotebookPage.KEYS + """ + 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(): @@ -426,7 +420,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): if cur_page == NotebookPage.KEYS: self.return_prior() else: - self.prior_page = self.get_current_page() + #self.prior_page = cur_page #self.get_current_page() self.set_page_by_enum(NotebookPage.KEYS) def focus_current(self) -> None: @@ -440,7 +434,6 @@ class Notebook(ScrollableMixin, Gtk.Notebook): return w = widget.get_children()[0] - # NOTE: if item contains treeview, focus first cell try: w.focus_first_row() w.grab_focus() @@ -455,25 +448,24 @@ class Notebook(ScrollableMixin, Gtk.Notebook): return widget def set_page_by_enum(self, enum: NotebookPage) -> None: - self.prior_page = self.get_current_page() + self.prior_page = self.get_page_by_enum() self.set_current_page(self.indexes[enum]) self.focus_current() # TODO: should be an internal property of those pages - if enum is NotebookPage.SERVERS: - AppNav.grid.show_connect_panel() - else: - AppNav.grid.hide_connect_panel() - - blank = [ - NotebookPage.OPTIONS, - NotebookPage.THANKS, - NotebookPage.CHANGELOG, - NotebookPage.LOG - ] - if enum in blank: - # TODO: delegate to controller - AppNav.grid.statusbar.refresh(None) + match enum: + case NotebookPage.KEYS: + MainController.set_statusbar("") + AppNav.grid.hide_connect_panel() + case NotebookPage.SERVERS: + # TODO: consolidate in mc.py + AppNav.grid.show_connect_panel() + self.servers.get_active_treeview().grab_focus() + MainController.update_server_status() + crumbs = self.servers.get_cached_label() + MainController.set_crumbs(crumbs) + case _: + AppNav.grid.hide_connect_panel() def _on_page_changed( self, notebook: "Notebook", page: Gtk.Widget, page_num: int diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 95fcb37..f7a81a3 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -190,6 +190,15 @@ class Options(Gtk.Box): self.scrollable.add(grid) self.add(self.scrollable) + def block_text_entry(self) -> None: + for entry in self.steam_entry, self.bm_entry: + entry.set_position(-1) + entry.set_can_focus(False) + + def unblock_text_entry(self) -> None: + for entry in self.steam_entry, self.bm_entry: + entry.set_can_focus(True) + def _on_developers_clicked(self, button: Gtk.Button) -> None: self.controller.show_developers_page() From 618ee47e534eff0ddafcd5eb45e05378d8ff86db Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 30 Dec 2025 21:36:39 +0900 Subject: [PATCH 16/49] fix: only save column width on quit --- dzgui/controllers/mc.py | 22 ++++++++++++++++++++++ dzgui/views/trees/tree_servers.py | 15 --------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 536d581..65015c4 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -7,6 +7,8 @@ from pathlib import Path from typing import Any, Callable, TYPE_CHECKING import dzgui.api.pefile as PeFile +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, @@ -176,6 +178,26 @@ class Controller: self.toggle_config(Preferences.DEBUG) def save_res_and_quit(self, *args: Any) -> None: + treeview = self.mediator.notebook.servers.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) + if self.mediator.window.props.is_maximized: Gtk.main_quit() return diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index e428b0d..3f98170 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -133,24 +133,9 @@ class ServerTreeView(TreeView): """ Propagate width change to other tabs """ - # TODO: only update res file on quit title = col.get_title() size = col.get_width() - prefs = self.controller.get_prefs() - columns = prefs.paths.columns - try: - data = JSON.read_json(columns) - data["cols"][title] = size - except Exception as e: - logger.critical(e) - data = {"cols": {title: size}} - - try: - JSON.write_json(data, columns) - except Exception as e: - logger.critical(e) - # NOTE: get final width after drag action completes GLib.idle_add(self.controller.propagate_column_width, col) From 2fc2eddf8754fb57614463d6eb0c7d52733ff61f Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:23:36 +0900 Subject: [PATCH 17/49] chore: move statusbar format method to format.py --- dzgui/controllers/mc.py | 50 ++++++++++------------------------------- dzgui/util/format.py | 23 ++++++++++++++++++- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 65015c4..dcff5ee 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -42,7 +42,7 @@ from dzgui.util.diag import write_diagnostic from dzgui.util import cooldown, strings from dzgui.util._json import read_json, write_json from dzgui.util.open_links import open_workshop_page -from dzgui.util.format import format_mods, pluralize +from dzgui.util.format import format_mods, format_player_count, pluralize from dzgui.util.redact import redact_log from dzgui.views.dialogs.filepicker import FilePicker @@ -171,7 +171,7 @@ class Controller: widget.handler_block_by_func(func) else: widget.handler_unblock_by_func(func) - # TODO + # TODO: deprecated? #self.mediator.menu.sel_blocked = state def toggle_debug_mode(self) -> None: @@ -213,13 +213,16 @@ class Controller: Gtk.main_quit() + def get_statusbar(self) -> str: + return self.mediator.grid.statusbar.get_text() + def set_statusbar(self, text: str) -> None: self.mediator.grid.statusbar.set_text(text) def delete_multiple_mods(self) -> None: sel = self.mediator.modtreeview.get_selection() model, pathlist = sel.get_selected_rows() - # NOTE: reverse when multiple + # NOTE: reverse when multiple selection for path in reversed(pathlist): self.delete_single_mod(path) @@ -254,7 +257,7 @@ class Controller: logger.critical(e) self.spawn_dialog(strings.something_wrong, Popup.NOTIFY) # TODO: suppress signals - # then reenable (or it spawns twice) + # then reenable (or it spawns dialog twice) self.mediator.grid.notebook.settings.populate_settings() return @@ -309,6 +312,7 @@ class Controller: 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: logger.info("Normal user exit") @@ -316,7 +320,7 @@ class Controller: return case ButtonType.OPTIONS: self.mediator.grid.notebook.settings.populate_settings() - self.mediator.grid.statusbar.refresh(None) + self.set_statusbar("") case ButtonType.MODS: self.load_mods() case ButtonType.HELP: @@ -577,42 +581,12 @@ class Controller: def update_server_status(self) -> None: treeview = self.mediator.notebook.servers.get_active_treeview() model = treeview.get_model() - if model is None: - players = 0 - hits = 0 - else: - hits = len(model) - players = 0 - for row in model: - players += row[4] - # TODO: move to util.format - players_pretty = pluralize("players", players) - hits_pretty = pluralize("matches", hits) - formatted = ( - f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}" - ) - suffix = " | Distance: calculating..." - - if players == 0: - suffix = "" - self.mediator.statusbar.set_text(formatted + suffix) - #self.players = formatted + status = format_player_count(model) + self.mediator.statusbar.set_text(status) def propagate_column_width(self, col: Gtk.TreeViewColumn) -> None: GLib.idle_add(self.mediator.servers.update_tab_widths, col) -# tabs = self.mediator.servers.get_tabs() -# tree = self.mediator.servers.get_active_treeview() -# width = col.get_width() -# title = col.get_title() -# for tab in tabs: -# if tab == tree: -# continue -# for col in tab.get_columns(): -# if col.get_title() == title: -# self.suppress_signal(tab, col, "_on_col_width_changed", True) -# col.set_fixed_width(width) -# self.suppress_signal(tab, col, "_on_col_width_changed", False) - + def set_crumbs_cache(self,text: str) -> None: self.crumbs_cache = text diff --git a/dzgui/util/format.py b/dzgui/util/format.py index cb51065..549fb01 100644 --- a/dzgui/util/format.py +++ b/dzgui/util/format.py @@ -1,7 +1,11 @@ import re from dzgui.util.localize import number -from dzgui.util.strings import no_mods +from dzgui.util.strings import no_mods, no_servers, distance_suffix + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk # noqa E402 def format_pango(text: str) -> str: @@ -42,3 +46,20 @@ def format_mods(size: int, mods: int) -> str: l_size = number(size) plural = pluralize("mods", mods) return f"Found {mods:n} {plural} taking up {l_size} MiB" + + +def format_player_count(model: Gtk.ListStore | None) -> str: + players = 0 + hits: int + status: str + if model is None or len(model) == 0: + return no_servers + else: + hits = len(model) + for row in model: + players += row[4] + players_pretty = pluralize("players", players) + hits_pretty = pluralize("matches", hits) + suffix = distance_suffix + status = f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}" + return f"{status} | {suffix}" From 2c19d999fff93b380c72bc158c107a7860e99fca Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:24:06 +0900 Subject: [PATCH 18/49] chore: update strings --- dzgui/util/strings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 07690b4..b27ce88 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -451,6 +451,7 @@ crumbs = Crumbs( checkmark = "✓" no_mods = "No local mods found." +no_servers = "No server metadata to list." @dataclass(slots=True, frozen=True) class Flags: @@ -506,3 +507,5 @@ server_labels = ServerLabels( recent="Recent", lan="LAN", ) + +distance_suffix = "Distance: calculating..." From 29e13db5a22631ee7d452132414e5d56ed5e630a Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:24:36 +0900 Subject: [PATCH 19/49] fix: handle invalid filepath --- dzgui/init/migrate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dzgui/init/migrate.py b/dzgui/init/migrate.py index e7594b9..7ee5f0c 100644 --- a/dzgui/init/migrate.py +++ b/dzgui/init/migrate.py @@ -50,4 +50,6 @@ def copy_ipdb(ips_path: Path) -> None: legacy = home / LEGACY_IPS_PATH if ips_path == legacy: return + if legacy.is_file() is False: + return shutil.copy(legacy, ips_path) From 64384d1ac6fa6a4a07c56a7f3730a19e4980dadf Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:25:02 +0900 Subject: [PATCH 20/49] fix: cache statusbar content --- dzgui/views/base.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index f8f6544..aa79ee3 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -18,25 +18,18 @@ from dataclasses import dataclass from enum import Enum from pathlib import Path -# TODO: most likely very little of this will be retained -from dzgui.const.enum import ( - Preferences, - Popup, - RowType, - NotebookPage, - VAdjustment, -) -from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL -from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, WINDOW_DEFAULT_X, WINDOW_DEFAULT_Y +from dzgui.const.enum import NotebookPage, VAdjustment +from dzgui.const.constants import NO_EXPAND, NO_FILL +from dzgui.const.constants import APP_NAME, APP_NAME_LOWER from dzgui.controllers.mc import Controller -from dzgui.util import css, dist, localize, strings, ip, deck, open_links -from dzgui.util.format import pluralize +from dzgui.util import css, strings # 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.pages.devs import Developers +from dzgui.views.pages.help import Help from dzgui.views.pages.keys import Keybindings from dzgui.views.pages.options import Options from dzgui.views.pages.servers import ServerNotebook @@ -62,7 +55,6 @@ 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) @@ -290,8 +282,8 @@ class Notebook(ScrollableMixin, Gtk.Notebook): AppNav.notebook = self self.prior_page: NotebookPage + self.prior_status: str - from dzgui.views.pages.help import Help self.help = Help(MainController) view = self.help.get_treeview() AppNav.menu = view @@ -408,6 +400,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.settings.unblock_text_entry() return self.set_page_by_enum(self.prior_page) + MainController.set_statusbar(self.prior_status) def get_page_by_enum(self) -> NotebookPage | None: for k, v in self.indexes.items(): @@ -420,7 +413,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): if cur_page == NotebookPage.KEYS: self.return_prior() else: - #self.prior_page = cur_page #self.get_current_page() + self.prior_status = MainController.get_statusbar() self.set_page_by_enum(NotebookPage.KEYS) def focus_current(self) -> None: From c4b918d61e594315b4ed62122d253e0149caee3e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:25:52 +0900 Subject: [PATCH 21/49] chore: update comments and imports --- dzgui/main.py | 2 +- dzgui/views/components/connect_panel.py | 6 ++++-- dzgui/views/components/right_panel.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dzgui/main.py b/dzgui/main.py index c9b692b..d91c187 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -104,7 +104,7 @@ def main() -> None: rebuild_symlinks(XDG.config) remove_stale_signatures(XDG.config, XDG.version) - # TODO: handle IP DB failure + # TODO: handle IP DB failure and use coords fallback get_ipdb(XDG.ips) local_coords = get_local_coords(XDG.ips) diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index 5cdc55c..a462a08 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -1,8 +1,9 @@ +from dzgui.views.components.web_button import WebButton + import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 -from dzgui.views.components.web_button import WebButton class ConnectPanel(Gtk.Frame): def __init__(self) -> None: @@ -10,10 +11,11 @@ class ConnectPanel(Gtk.Frame): self.label = Gtk.Label(label="Add/connect") self.label2 = Gtk.Label(label="Favorite server") + # TODO: embold function #self.label2.set_markup("Favorite server") + self.label2.set_markup("Favorite server") - sep = Gtk.Separator() self.entry1 = Gtk.Entry(placeholder_text="Enter IP or Battlemetrics ID", hexpand=False) self.entry2 = Gtk.Entry() self.fav = Gtk.Label(label="MY favorite server very long title") diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index b7fe336..730e3cc 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -34,6 +34,7 @@ class RightPanel(Gtk.Box): ) self.ping.connect("clicked", self._on_ping_clicked) + # TODO: drop after adding context menu row self.debug_toggle = Gtk.ToggleButton( label=strings.debug_mode, margin_top=10, From 72c1081b4872559fce64e5cfee266f6fd3df3f23 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 01:58:26 +0900 Subject: [PATCH 22/49] feat: embolden func --- dzgui/util/format.py | 4 ++++ dzgui/views/base.py | 5 +++-- dzgui/views/components/connect_panel.py | 8 ++++---- dzgui/views/components/mod_panel.py | 6 ++++-- dzgui/views/pages/devs.py | 12 ++++++++---- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/dzgui/util/format.py b/dzgui/util/format.py index 549fb01..ff24f9b 100644 --- a/dzgui/util/format.py +++ b/dzgui/util/format.py @@ -63,3 +63,7 @@ def format_player_count(model: Gtk.ListStore | None) -> str: suffix = distance_suffix status = f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}" return f"{status} | {suffix}" + + +def embolden(text: str) -> str: + return f"{text}" diff --git a/dzgui/views/base.py b/dzgui/views/base.py index aa79ee3..4f3c0c7 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -23,6 +23,7 @@ from dzgui.const.constants import NO_EXPAND, NO_FILL 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? @@ -519,8 +520,8 @@ class Grid(Gtk.Grid): return self.breadcrumbs.get_text() def set_breadcrumbs(self, text: str) -> None: - # TODO: embolden func - self.breadcrumbs.set_markup(f"{text}") + crumbs = embolden(text) + self.breadcrumbs.set_markup(crumbs) class App(Gtk.Application): diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index a462a08..660dc67 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -1,3 +1,4 @@ +from dzgui.util.format import embolden from dzgui.views.components.web_button import WebButton import gi @@ -12,10 +13,9 @@ class ConnectPanel(Gtk.Frame): self.label = Gtk.Label(label="Add/connect") self.label2 = Gtk.Label(label="Favorite server") - # TODO: embold function - #self.label2.set_markup("Favorite server") - - self.label2.set_markup("Favorite server") + # TODO: strings + text = embolden("Favorite server") + self.label2.set_markup(text) self.entry1 = Gtk.Entry(placeholder_text="Enter IP or Battlemetrics ID", hexpand=False) self.entry2 = Gtk.Entry() self.fav = Gtk.Label(label="MY favorite server very long title") diff --git a/dzgui/views/components/mod_panel.py b/dzgui/views/components/mod_panel.py index 599daba..bff00da 100644 --- a/dzgui/views/components/mod_panel.py +++ b/dzgui/views/components/mod_panel.py @@ -2,7 +2,8 @@ from typing import TYPE_CHECKING from dzgui.const.enum import ModButton from dzgui.const.constants import NO_EXPAND, FILL -from dzgui.util import strings +from dzgui.util.strings import mod_panel +from dzgui.util.format import embolden import gi gi.require_version("Gtk", "3.0") @@ -34,7 +35,8 @@ class ModSelectionPanel(Gtk.Box): ) self.header = Gtk.Label() - self.header.set_markup(f"{strings.mod_panel.header}") + text = embolden(mod_panel.header) + self.header.set_markup(text) self.main_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) for button in buttons: diff --git a/dzgui/views/pages/devs.py b/dzgui/views/pages/devs.py index 7938178..40ff46b 100644 --- a/dzgui/views/pages/devs.py +++ b/dzgui/views/pages/devs.py @@ -2,8 +2,9 @@ from dataclasses import fields from typing import Union, TYPE_CHECKING from dzgui.const.enum import NotebookPage -from dzgui.util.strings import developers from dzgui.util.css import add_class +from dzgui.util.format import embolden +from dzgui.util.strings import developers import gi # noqa E402 gi.require_version("Gtk", "3.0") @@ -35,10 +36,12 @@ class Developers(Gtk.Box): back_button = Gtk.Button(label="Back", halign=Gtk.Align.START) back_button.connect("clicked", self._on_back_clicked) paths_label = Gtk.Label() - paths_label.set_markup(f"{developers.paths_label}") + text = embolden(developers.paths_label) + paths_label.set_markup(text) prefs_label = Gtk.Label() - prefs_label.set_markup(f"{developers.prefs_label}") + text = embolden(developers.prefs_label) + prefs_label.set_markup(text) paths_tree = self._make_tree(self.controller.prefs.paths) prefs_tree = self._make_tree(self.controller.prefs) @@ -64,7 +67,8 @@ class Developers(Gtk.Box): self.controller.open_page(NotebookPage.OPTIONS) def _make_tree(self, prefs: Union["Xdg", "UserPrefs"]) -> Gtk.TreeView: - view = Gtk.TreeView() + from dzgui.views.trees.tree_base import TreeView + view = TreeView(self.controller) renderer = Gtk.CellRendererText() for i, col in enumerate(developers.columns): column = Gtk.TreeViewColumn(col, renderer, text=i) From e7735e04725c192c6ebdcee950b4f03b2d98b58c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 02:41:05 +0900 Subject: [PATCH 23/49] feat: lazy refresh model on page change (WIP) --- dzgui/controllers/mc.py | 20 ++++++++++++++++++++ dzgui/controllers/model.py | 8 ++++---- dzgui/views/components/right_panel.py | 17 ++++++++++++++++- dzgui/views/components/web_button.py | 9 +++++++-- dzgui/views/trees/tree_servers.py | 16 ++++++++++++++++ 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index dcff5ee..4aa2d91 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -307,6 +307,7 @@ class Controller: def show_developers_page(self) -> None: self.open_page(NotebookPage.DEVELOPERS) + # TODO: focus first row def open_page(self, page: NotebookPage) -> None: self.mediator.grid.notebook.set_page_by_enum(page) @@ -593,3 +594,22 @@ class Controller: def get_crumbs_cache(self) -> None: return self.crumbs_cache self.crumbs_cache = text + + def refresh_tree(self) -> None: + treeview = self.mediator.notebook.servers.get_active_treeview() + treeview.set_loaded(False) + self.populate_model() + + def populate_model(self) -> None: + # TODO: always use same server model, store in servertreeview class + treeview = self.mediator.notebook.servers.get_active_treeview() + if treeview.get_loaded() is False: + new_model = self.model_manager.new_model() + func = treeview.get_query_func() + if func is not None: + model = treeview.get_model() + model.clear() + data = func() + model.append(data) + treeview.set_loaded(True) + self.update_server_status() diff --git a/dzgui/controllers/model.py b/dzgui/controllers/model.py index 7550a1c..2b75b76 100644 --- a/dzgui/controllers/model.py +++ b/dzgui/controllers/model.py @@ -89,10 +89,10 @@ class ModelManager: self.filtered = None self.success = True - def __new__(cls) -> Self: - if not hasattr(cls, "instance"): - cls.instance = super(ModelManager, cls).__new__(cls) - return cls.instance + #def __new__(cls) -> Self: + # if not hasattr(cls, "instance"): + # cls.instance = super(ModelManager, cls).__new__(cls) + # return cls.instance def get_recent_store(self) -> ListStore: return self.recent_store diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index 730e3cc..2ee5704 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -6,6 +6,8 @@ from dzgui.const.enum import Preferences from dzgui.views.components.buttonbox import ButtonBox from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.icon import Icon +from dzgui.views.components.web_button import RefreshButton +# TODO: rename web_button from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL, INPUT_KEYBOARD from dzgui.util import strings @@ -25,6 +27,7 @@ class RightPanel(Gtk.Box): padding = 0 self.pack_start(el, NO_EXPAND, NO_FILL, padding) + # TODO: more custom button classes self.ping = Gtk.Button( label=strings.ping_servers, margin_top=10, @@ -43,10 +46,18 @@ class RightPanel(Gtk.Box): tooltip_text=strings.debug_tooltip, ) + # TODO: icon + self.refresh_button = RefreshButton("Refresh") + self.refresh_button.set_margin_top(10) + self.refresh_button.set_margin_start(80) + self.refresh_button.set_margin_end(80) + self.refresh_button.connect("clicked", self._on_refresh_clicked) + if controller.query_config(Preferences.DEBUG) == True: self.debug_toggle.set_active(True) self.debug_toggle.connect("toggled", self._on_debug_toggled) + # TODO: make button class i = Icon(INPUT_KEYBOARD) i.set_margin_start(5) self.question = Gtk.Button( @@ -59,7 +70,7 @@ class RightPanel(Gtk.Box): self.question.set_image_position(Gtk.PositionType.RIGHT) self.question.connect("clicked", self._on_question_clicked) - for el in self.ping, self.debug_toggle, self.question: + for el in self.ping, self.debug_toggle, self.question, self.refresh_button: padding = 0 self.pack_start(el, NO_EXPAND, FILL, padding) @@ -85,6 +96,10 @@ class RightPanel(Gtk.Box): grid = self.AppNav.grid self.controller.toggle_debug_mode() + def _on_refresh_clicked(self, button: RefreshButton) -> None: + print("button clicked") + self.controller.refresh_tree() + def _on_ping_clicked(self, button: Gtk.Button) -> None: # TODO block_signals() diff --git a/dzgui/views/components/web_button.py b/dzgui/views/components/web_button.py index dc0b71d..1089de8 100644 --- a/dzgui/views/components/web_button.py +++ b/dzgui/views/components/web_button.py @@ -1,5 +1,5 @@ from dzgui.views.components.icon import Icon -from dzgui.const.constants import WEB_BROWSER +from dzgui.const.constants import REFRESH_ICON, WEB_BROWSER import gi gi.require_version("Gtk", "3.0") @@ -25,5 +25,10 @@ class IconTextButton(IconButton): class WebButton(IconTextButton): def __init__(self, label: str): super().__init__(icon=WEB_BROWSER, label=label) - + pass + + +class RefreshButton(IconTextButton): + def __init__(self, label: str): + super().__init__(icon=REFRESH_ICON, label=label) pass diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index 3f98170..b5bd183 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -43,6 +43,9 @@ class ServerTreeView(TreeView): def __init__(self, controller: "Controller") -> None: super().__init__(controller) + self.loaded = False + self.query_func: Callable = None + self.menu = Gtk.Menu() self.menu.connect("key-press-event", self._on_key) self.controller = controller @@ -127,6 +130,12 @@ class ServerTreeView(TreeView): return False return True + def set_query_func(self, func: Callable) -> None: + self.query_func = func + + def get_query_func(self) -> Callable | None: + return self.query_func + def _on_col_width_changed( self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt ) -> None: @@ -304,3 +313,10 @@ class ServerTreeView(TreeView): ip = addr.split(":")[0] gameport = int(addr.split(":")[1]) return Record(ip, gameport, qport) + + def get_loaded(self) -> bool: + return self.loaded + + def set_loaded(self, status: bool) -> None: + self.loaded = status + From 580ccf0e83584d70f527ffbea036dd7ead0ca99b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 02:41:47 +0900 Subject: [PATCH 24/49] chore: drop deprecated method --- dzgui/controllers/mc.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 4aa2d91..f18466a 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -68,13 +68,6 @@ class Controller: self.model_manager = ModelManager() - def set_help_menu_crumbs(self) -> None: - # TODO: going to be deprecated after server notebook is added - tip = self.mediator.menu.get_model()[0][0] - store = self.model_manager.get_help_store()[0][0] - if tip == store: - self.set_crumbs("Help") - def set_crumbs(self, text: str) -> None: self.mediator.grid.set_breadcrumbs(text) From b6210e450f8e09f5b1929cfb7de02e6213881255 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 02:45:08 +0900 Subject: [PATCH 25/49] fix: grab treeview focus on refresh --- dzgui/controllers/mc.py | 1 + dzgui/views/components/right_panel.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index f18466a..30fb6e4 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -606,3 +606,4 @@ class Controller: model.append(data) treeview.set_loaded(True) self.update_server_status() + treeview.grab_focus() diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index 2ee5704..4172265 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -97,7 +97,6 @@ class RightPanel(Gtk.Box): self.controller.toggle_debug_mode() def _on_refresh_clicked(self, button: RefreshButton) -> None: - print("button clicked") self.controller.refresh_tree() def _on_ping_clicked(self, button: Gtk.Button) -> None: From 46ade773b38dbcd068da264191e5e0f1c224caf3 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 05:50:25 +0900 Subject: [PATCH 26/49] fix: allocate correct grid rows to right panel --- dzgui/const/constants.py | 1 + dzgui/views/base.py | 62 +++++++++++++------------- dzgui/views/components/buttonbox.py | 13 ++---- dzgui/views/components/filter_panel.py | 17 ++++--- dzgui/views/components/mod_panel.py | 12 ++--- dzgui/views/components/right_panel.py | 11 ++--- dzgui/views/dialogs/entry.py | 5 ++- dzgui/views/dialogs/lan.py | 3 +- dzgui/views/dialogs/server_details.py | 5 ++- dzgui/views/pages/options.py | 7 +-- 10 files changed, 67 insertions(+), 69 deletions(-) diff --git a/dzgui/const/constants.py b/dzgui/const/constants.py index 9782bab..c3b586f 100644 --- a/dzgui/const/constants.py +++ b/dzgui/const/constants.py @@ -39,6 +39,7 @@ WEB_BROWSER = "web-browser-symbolic" SEPARATOR = "SEPARATOR" +NO_PADDING = 0 NO_EXPAND = False NO_FILL = False EXPAND = True diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 4f3c0c7..511b1dc 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -19,7 +19,7 @@ from enum import Enum from pathlib import Path from dzgui.const.enum import NotebookPage, VAdjustment -from dzgui.const.constants import NO_EXPAND, NO_FILL +from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING from dzgui.const.constants import APP_NAME, APP_NAME_LOWER from dzgui.controllers.mc import Controller from dzgui.util import css, strings @@ -209,16 +209,16 @@ class OuterWindow(Gtk.Window): self.connect("delete-event", self._on_delete_event) self.connect("key-press-event", self._on_keypress) - MainController.set_resolution(self) self.grid = Grid() self.add(self.grid) + MainController.set_resolution(self) self.show_all() - self.grid.right_panel.filters_vbox.set_visible(False) - self.grid.right_panel.enable_ping_button(False) self.grid.sel_panel.set_visible(False) + # TODO: + #self.grid.right_panel.enable_ping_button(False) css.load_css() AppNav.grid.notebook.set_page_by_enum(NotebookPage.SERVERS) @@ -279,7 +279,7 @@ class ScrollableNote(ScrollableMixin, Gtk.Box): class Notebook(ScrollableMixin, Gtk.Notebook): def __init__(self) -> None: - super().__init__(show_tabs=False, show_border=False) + super().__init__(show_tabs=False) AppNav.notebook = self self.prior_page: NotebookPage @@ -298,15 +298,13 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.settings = Options(MainController) # TODO: make all treeviews internally scrollable in base class - # server and quad tables should have hexpand property set to True + # NOTE: server and quad tables should have hexpand property set to True # add all treeviews as page and register them to AppNav and self.indexes # when switching to a treeview, update relevant view and just pop that page # instead of loading/unloading the model each time self.servers = ServerNotebook(MainController) AppNav.servers = self.servers - self.quad = Gtk.ScrolledWindow() - self.scroll_mod = Gtk.ScrolledWindow() # TODO: register this table self.quad_table = ModTreeView(MainController) @@ -454,12 +452,14 @@ class Notebook(ScrollableMixin, Gtk.Notebook): case NotebookPage.SERVERS: # TODO: consolidate in mc.py AppNav.grid.show_connect_panel() + AppNav.grid.show_filter_panel() self.servers.get_active_treeview().grab_focus() MainController.update_server_status() crumbs = self.servers.get_cached_label() MainController.set_crumbs(crumbs) case _: AppNav.grid.hide_connect_panel() + AppNav.grid.hide_filter_panel() def _on_page_changed( self, notebook: "Notebook", page: Gtk.Widget, page_num: int @@ -475,8 +475,12 @@ class Notebook(ScrollableMixin, Gtk.Notebook): class Grid(Gtk.Grid): def __init__(self) -> None: - super().__init__() - self.set_column_homogeneous(True) + super().__init__(column_homogeneous=True) + + MAX_ROWS = 3 + MAX_COLS = 3 + SINGLE_ROW = 1 + SINGLE_COL = 1 self.statusbar = Statusbar(MainController) self.breadcrumbs = Gtk.Label(halign=Gtk.Align.START) @@ -487,29 +491,32 @@ class Grid(Gtk.Grid): # FIXME: do not pass AppNav to right panel self.right_panel = RightPanel(AppNav, MainController) + # TODO: move into right panel self.sel_panel = ModSelectionPanel(MainController) - self.right_panel.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, 0) + self.right_panel.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING) self.notebook = Notebook() - - self.attach(self.notebook, 0, 0, 3, 1) - self.attach_next_to( - self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1 - ) - self.conpan = ConnectPanel() - self.attach_next_to( - self.conpan, self.notebook, Gtk.PositionType.BOTTOM, 3, 1 - ) - self.attach_next_to( - self.statusbar, self.conpan, Gtk.PositionType.BOTTOM, 3, 1 - ) - self.attach_next_to( - self.right_panel, self.notebook, Gtk.PositionType.RIGHT, 1, 1 + self.attach(self.notebook, 0, 0, MAX_COLS, 1) + + els = ( + (self.breadcrumbs, 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 hide_filter_panel(self) -> None: + self.right_panel.filters_vbox.set_visible(False) + + def show_filter_panel(self) -> None: + self.right_panel.filters_vbox.set_visible(True) + def hide_connect_panel(self) -> None: self.conpan.set_visible(False) @@ -540,11 +547,6 @@ class App(Gtk.Application): ) self.win.add_accel_group(accel) - # FIXME: hacky - AppNav.notebook.servers.notebook.next_page() - AppNav.notebook.servers.notebook.prev_page() - MainController.focus_notebook() - GLib.unix_signal_add( GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint ) diff --git a/dzgui/views/components/buttonbox.py b/dzgui/views/components/buttonbox.py index 65931c9..8020698 100644 --- a/dzgui/views/components/buttonbox.py +++ b/dzgui/views/components/buttonbox.py @@ -1,7 +1,7 @@ import logging from dzgui.const.enum import ButtonType -from dzgui.const.constants import NO_EXPAND, NO_FILL +from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING import gi gi.require_version("Gtk", "3.0") @@ -20,7 +20,7 @@ class ContextualButton(Gtk.Button): self.opens = opens class ButtonBox(Gtk.Box): - def __init__(self, controller): + def __init__(self, controller) -> None: super().__init__( spacing=6, margin_top=0, @@ -43,24 +43,17 @@ class ButtonBox(Gtk.Box): ) # FIXME: if debug log fails to load, still opens table - size = (10, 10) if prefs.is_steam_deck else (50, 50) x, y = size button.set_size_request(x, y) self.buttons.append(button) button.connect("clicked", self._on_selection_button_clicked) - self.pack_start(button, NO_EXPAND, NO_FILL, 0) + self.pack_start(button, NO_EXPAND, NO_FILL, NO_PADDING) def _on_selection_button_clicked(self, button: Gtk.Button) -> None: - # # TODO: maybe drop this and just ensure buttons respond correctly self.controller.open_page_by_button(button) - # if button.context != ButtonType.HELP: - # button.set_sensitive(False) - # for b in self.buttons: - # if b != button: - # b.set_sensitive(True) def _walk_buttons(self, increment: int) -> None: for i, button in enumerate(self.buttons): diff --git a/dzgui/views/components/filter_panel.py b/dzgui/views/components/filter_panel.py index fc7fe30..fc99b81 100644 --- a/dzgui/views/components/filter_panel.py +++ b/dzgui/views/components/filter_panel.py @@ -8,13 +8,13 @@ from typing import Literal from dzgui.util import strings from dzgui.util.margins import set_surrounding_margins from dzgui.const.enum import FilterMode -from dzgui.const.constants import NO_EXPAND, NO_FILL +from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING logger = logging.getLogger(__name__) class FilterPanel(Gtk.Box): def __init__(self, appnav, controller): - super().__init__(spacing=6) + super().__init__(spacing=6, vexpand=False) # TODO: set strings in constants self.default_filters = { @@ -71,6 +71,8 @@ class FilterPanel(Gtk.Box): set_surrounding_margins(self, 10) self.set_margin_top(1) + # TODO: strings + # TODO: embolden self.filters_label = Gtk.Label(label="Filters") self.keyword_entry = Gtk.Entry() @@ -96,15 +98,16 @@ class FilterPanel(Gtk.Box): self.maps_entry.connect("key-press-event", self._on_map_entry_keypress) # FIXME: only giving two params to pack_start + # cf. EXPAND self.maps_combo.pack_start(renderer_text, True) self.maps_combo.connect("changed", self._on_map_changed) self.maps_combo.connect("key-press-event", self._on_combo_keypress) - self.pack_start(self.filters_label, NO_EXPAND, NO_FILL, 0) - self.pack_start(self.keyword_entry, NO_EXPAND, NO_FILL, 0) - self.pack_start(self.maps_combo, NO_EXPAND, NO_FILL, 0) - - self.pack_start(button_grid, NO_EXPAND, NO_FILL, 0) + # TODO: consolidate + self.pack_start(self.filters_label, NO_EXPAND, NO_FILL, NO_PADDING) + self.pack_start(self.keyword_entry, NO_EXPAND, NO_FILL, NO_PADDING) + self.pack_start(self.maps_combo, NO_EXPAND, NO_FILL, NO_PADDING) + self.pack_start(button_grid, NO_EXPAND, NO_FILL, NO_PADDING) def set_unique_maps(self, maps: list) -> None: if len(maps) < 1: diff --git a/dzgui/views/components/mod_panel.py b/dzgui/views/components/mod_panel.py index bff00da..ee8f18d 100644 --- a/dzgui/views/components/mod_panel.py +++ b/dzgui/views/components/mod_panel.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from dzgui.const.enum import ModButton -from dzgui.const.constants import NO_EXPAND, FILL +from dzgui.const.constants import NO_EXPAND, FILL, NO_PADDING from dzgui.util.strings import mod_panel from dzgui.util.format import embolden @@ -42,18 +42,18 @@ class ModSelectionPanel(Gtk.Box): for button in buttons: b = EnumeratedModButton(button) b.connect("clicked", self._on_button_clicked) - self.main_panel.pack_start(b, NO_EXPAND, FILL, 0) + self.main_panel.pack_start(b, NO_EXPAND, FILL, NO_PADDING) self.extra_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) for button in (ModButton.SELECT_STALE, ModButton.UNHIGHLIGHT_STALE): b = EnumeratedModButton(button) b.connect("clicked", self._on_button_clicked) b.set_sensitive(False) - self.extra_panel.pack_start(b, NO_EXPAND, FILL, 0) + self.extra_panel.pack_start(b, NO_EXPAND, FILL, NO_PADDING) - self.pack_start(self.header, NO_EXPAND, FILL, 0) - self.pack_start(self.main_panel, NO_EXPAND, FILL, 0) - self.pack_start(self.extra_panel, NO_EXPAND, FILL, 0) + self.pack_start(self.header, NO_EXPAND, FILL, NO_PADDING) + self.pack_start(self.main_panel, NO_EXPAND, FILL, NO_PADDING) + self.pack_start(self.extra_panel, NO_EXPAND, FILL, NO_PADDING) def after_colorize(self) -> None: self.controller.unselect_all_mods() diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index 4172265..c653c77 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -8,7 +8,7 @@ from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.icon import Icon from dzgui.views.components.web_button import RefreshButton # TODO: rename web_button -from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL, INPUT_KEYBOARD +from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL, INPUT_KEYBOARD, NO_PADDING from dzgui.util import strings # TODO: refactor depends on ServerTreeView @@ -23,10 +23,6 @@ class RightPanel(Gtk.Box): self.button_vbox = ButtonBox(controller) self.filters_vbox = FilterPanel(appnav, controller) - for el in self.button_vbox, self.filters_vbox: - padding = 0 - self.pack_start(el, NO_EXPAND, NO_FILL, padding) - # TODO: more custom button classes self.ping = Gtk.Button( label=strings.ping_servers, @@ -70,9 +66,8 @@ class RightPanel(Gtk.Box): self.question.set_image_position(Gtk.PositionType.RIGHT) self.question.connect("clicked", self._on_question_clicked) - for el in self.ping, self.debug_toggle, self.question, self.refresh_button: - padding = 0 - self.pack_start(el, NO_EXPAND, FILL, padding) + for el in self.button_vbox, self.question, self.filters_vbox, self.refresh_button: + self.pack_start(el, NO_EXPAND, FILL, NO_PADDING) def enable_ping_button(self, state: bool) -> None: self.ping.set_visible(state) diff --git a/dzgui/views/dialogs/entry.py b/dzgui/views/dialogs/entry.py index 68e8538..4a520d1 100644 --- a/dzgui/views/dialogs/entry.py +++ b/dzgui/views/dialogs/entry.py @@ -2,6 +2,7 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa +from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING from dzgui.const.enum import Popup class EntryDialog(GenericDialog): @@ -24,14 +25,14 @@ class EntryDialog(GenericDialog): self.user_entry.set_margin_top(0) self.user_entry.set_size_request(250, 0) self.user_entry.set_activates_default(True) - self.dialogBox.pack_start(self.user_entry, NO_EXPAND, NO_FILL, 0) + self.dialogBox.pack_start(self.user_entry, NO_EXPAND, NO_FILL, NO_PADDING) if link: button = Gtk.Button(label=link) button.set_margin_start(60) button.set_margin_end(60) button.connect("clicked", self._on_button_clicked, button_type) - self.dialogBox.pack_end(button, NO_EXPAND, NO_FILL, 0) + self.dialogBox.pack_end(button, NO_EXPAND, NO_FILL, NO_PADDING) self.ok = self.dialog.action_area.get_children()[1] self.ok.set_sensitive(False) diff --git a/dzgui/views/dialogs/lan.py b/dzgui/views/dialogs/lan.py index f05eae9..dd061e1 100644 --- a/dzgui/views/dialogs/lan.py +++ b/dzgui/views/dialogs/lan.py @@ -4,6 +4,7 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa +from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING from dzgui.util.import strings class LanDialog(Gtk.MessageDialog): @@ -55,7 +56,7 @@ class LanDialog(Gtk.MessageDialog): self.button_box.add(self.warn_label) content = self.get_content_area() - content.pack_start(self.button_box, NO_EXPAND, NO_FILL, 0) + content.pack_start(self.button_box, NO_EXPAND, NO_FILL, NO_PADDING) content.set_margin_start(30) content.set_margin_end(30) content.show_all() diff --git a/dzgui/views/dialogs/server_details.py b/dzgui/views/dialogs/server_details.py index ddf0c4e..18aa3d1 100644 --- a/dzgui/views/dialogs/server_details.py +++ b/dzgui/views/dialogs/server_details.py @@ -2,6 +2,7 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa +from dzgui.const.constants import EXPAND, FILL, NO_PADDING from dzgui.const.enum import Popup class ServerDetailsDialog(GenericDialog): @@ -57,8 +58,8 @@ class ServerDetailsDialog(GenericDialog): box.add(el) scrollable_message.add(box) - dialog_box.pack_start(scrollable_tree, EXPAND, FILL, 0) - dialog_box.pack_start(scrollable_message, EXPAND, FILL, 0) + dialog_box.pack_start(scrollable_tree, EXPAND, FILL, NO_PADDING) + dialog_box.pack_start(scrollable_message, EXPAND, FILL, NO_PADDING) self.wait_dialog = GenericDialog(strings.details, Popup.WAIT) self.wait_dialog.show_all() diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index f7a81a3..a8967e2 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -23,6 +23,7 @@ from dzgui.const.constants import ( FLATPAK_SANDBOX, NO_EXPAND, NO_FILL, + NO_PADDING, STEAM_CMD, VIEW_CONCEAL, VIEW_REVEAL, @@ -91,7 +92,7 @@ class Options(Gtk.Box): self.client_combo.set_active(0) self.client_combo.connect("changed", self._on_client_changed) hbox = Gtk.Box(spacing=5, halign=Gtk.Align.START) - hbox.pack_start(self.client_combo, NO_EXPAND, NO_FILL, 0) + hbox.pack_start(self.client_combo, NO_EXPAND, NO_FILL, NO_PADDING) self.distance_toggle = self.make_binary_radio( strings.options.km, strings.options.mi, Preferences.DIST @@ -402,8 +403,8 @@ class Options(Gtk.Box): radio2 = Gtk.RadioButton.new_from_widget(radio1) radio2.set_label(second_option) radio1.connect("toggled", self._on_radio_toggled, context) - hbox.pack_start(radio1, NO_EXPAND, NO_FILL, 0) - hbox.pack_start(radio2, NO_EXPAND, NO_FILL, 0) + hbox.pack_start(radio1, NO_EXPAND, NO_FILL, NO_PADDING) + hbox.pack_start(radio2, NO_EXPAND, NO_FILL, NO_PADDING) return hbox From dc310593d06ae0964933be860418792fb8853506 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 06:04:24 +0900 Subject: [PATCH 27/49] chore: clear typehinting errors --- dzgui/controllers/mc.py | 9 ++++----- dzgui/util/format.py | 2 +- dzgui/views/base.py | 18 +++++++++++++----- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 30fb6e4..3e123a4 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -255,8 +255,8 @@ class Controller: return # NOTE: disabled for now - def present_toast(self, text: str) -> None: - self.mediator.window.toast.set_text_and_fade(text) + #def present_toast(self, text: str) -> None: + # self.mediator.window.toast.set_text_and_fade(text) def start_cooldown(self) -> None: self.cooldown = cooldown.get_time() @@ -581,12 +581,11 @@ class Controller: def propagate_column_width(self, col: Gtk.TreeViewColumn) -> None: GLib.idle_add(self.mediator.servers.update_tab_widths, col) - def set_crumbs_cache(self,text: str) -> None: + def set_crumbs_cache(self, text: str) -> None: self.crumbs_cache = text - def get_crumbs_cache(self) -> None: + def get_crumbs_cache(self) -> str: return self.crumbs_cache - self.crumbs_cache = text def refresh_tree(self) -> None: treeview = self.mediator.notebook.servers.get_active_treeview() diff --git a/dzgui/util/format.py b/dzgui/util/format.py index ff24f9b..40224b3 100644 --- a/dzgui/util/format.py +++ b/dzgui/util/format.py @@ -48,7 +48,7 @@ def format_mods(size: int, mods: int) -> str: return f"Found {mods:n} {plural} taking up {l_size} MiB" -def format_player_count(model: Gtk.ListStore | None) -> str: +def format_player_count(model: Gtk.TreeModel | None) -> str: players = 0 hits: int status: str diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 511b1dc..dae181d 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -17,6 +17,7 @@ import warnings 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 @@ -52,6 +53,9 @@ 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 + import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402 @@ -202,14 +206,13 @@ class OuterWindow(Gtk.Window): MainController.set_mediator(AppNav) AppNav.window = self - # steam deck taskbar may occlude elements + # 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.connect("key-press-event", self._on_keypress) - self.grid = Grid() self.add(self.grid) @@ -217,12 +220,15 @@ class OuterWindow(Gtk.Window): self.show_all() self.grid.sel_panel.set_visible(False) + # TODO: #self.grid.right_panel.enable_ping_button(False) css.load_css() + # TODO: call controller directly AppNav.grid.notebook.set_page_by_enum(NotebookPage.SERVERS) + # TODO: deprecated def _on_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: if event.state is Gdk.ModifierType.CONTROL_MASK \ and event.keyval is Gdk.KEY_d: @@ -391,7 +397,8 @@ class Notebook(ScrollableMixin, Gtk.Notebook): fields unfocusable prior to the page 'switch-page' signal, then makes them focusable again - Used when switching back from NotebookPage.KEYS + 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() @@ -532,7 +539,7 @@ class Grid(Gtk.Grid): class App(Gtk.Application): - def __init__(self, prefs) -> None: + def __init__(self, prefs: "UserPrefs") -> None: GLib.set_prgname(APP_NAME) MainController.set_prefs(prefs) @@ -552,8 +559,9 @@ class App(Gtk.Application): ) Gtk.main() - def _catch_sigint(self) -> None: + def _catch_sigint(self) -> Literal[True]: self.win.halt_proc_and_quit() + return True def _halt_window_subprocess( self, From 645682ee06ab3c5701f62a9f770378fcd6a637fb Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:22:35 +0900 Subject: [PATCH 28/49] fix: only unlink stale links --- dzgui/util/symlink.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/dzgui/util/symlink.py b/dzgui/util/symlink.py index 0c847e0..59528bd 100644 --- a/dzgui/util/symlink.py +++ b/dzgui/util/symlink.py @@ -15,14 +15,18 @@ def rebuild_symlinks(config: Path) -> None: steam_path = Path(path) dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ) + # NOTE: unlink stale symlinks for file in dayz_path.iterdir(): - if file.is_symlink(): + if file.is_symlink() and file.exists() is False: file.unlink() workshop = get_local_mod_path(steam_path) - for uid in get_local_mod_ids(steam_path): - uid = str(uid) + # NOTE: create symlinks for missing mods + for mod_id in get_local_mod_ids(steam_path): + uid = str(mod_id) md5sum = _hash(str(uid)) - Path(dayz_path / md5sum).symlink_to(workshop / uid) + source = Path(dayz_path / md5sum) + if source.exists() is False: + source.symlink_to(workshop / uid) def clone_symlinks(config: Path) -> None: @@ -39,6 +43,7 @@ def clone_symlinks(config: Path) -> None: return # TODO: test these two operations + # use less time intensive logic like the above for file in exp_path.iterdir(): if file.is_symlink(): file.unlink() From d74060c5181ca5132d07d13806e409267865f856 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:22:55 +0900 Subject: [PATCH 29/49] chore: reword tooltip --- dzgui/util/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index b27ce88..1fcab97 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -154,7 +154,7 @@ label_main_menu = "Main menu" ping_servers = "Ping servers" debug_mode = "Debug mode" keys_button = "Keys" -keys_tooltip = "Opens the keybindings dialog" +keys_tooltip = "Toggles the keybindings dialog" debug_tooltip = ( "Used to perform a dry run without\n" "actually connecting to a server" From 69cde11e8e6d6bdfba92fd77f72cc011ec3dcc00 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:24:44 +0900 Subject: [PATCH 30/49] chore: consolidate buttons --- dzgui/views/components/mod_panel.py | 31 ++++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/dzgui/views/components/mod_panel.py b/dzgui/views/components/mod_panel.py index ee8f18d..185e542 100644 --- a/dzgui/views/components/mod_panel.py +++ b/dzgui/views/components/mod_panel.py @@ -27,33 +27,36 @@ class ModSelectionPanel(Gtk.Box): self.controller = controller + header = Gtk.Label() + text = embolden(mod_panel.header) + header.set_markup(text) + + self.main_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) buttons = ( ModButton.SELECT_ALL, ModButton.UNSELECT_ALL, - ModButton.HIGHLIGHT_STALE, ModButton.DELETE_SELECTED, ) - - self.header = Gtk.Label() - text = embolden(mod_panel.header) - self.header.set_markup(text) - - self.main_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) for button in buttons: b = EnumeratedModButton(button) b.connect("clicked", self._on_button_clicked) self.main_panel.pack_start(b, NO_EXPAND, FILL, NO_PADDING) - self.extra_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - for button in (ModButton.SELECT_STALE, ModButton.UNHIGHLIGHT_STALE): + self.stale_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + buttons = ( + ModButton.HIGHLIGHT_STALE, + ModButton.SELECT_STALE, + ModButton.UNHIGHLIGHT_STALE, + ) + for button in buttons: b = EnumeratedModButton(button) b.connect("clicked", self._on_button_clicked) - b.set_sensitive(False) - self.extra_panel.pack_start(b, NO_EXPAND, FILL, NO_PADDING) + if button is not ModButton.HIGHLIGHT_STALE: + b.set_sensitive(False) + self.stale_panel.pack_start(b, NO_EXPAND, FILL, NO_PADDING) - self.pack_start(self.header, NO_EXPAND, FILL, NO_PADDING) - self.pack_start(self.main_panel, NO_EXPAND, FILL, NO_PADDING) - self.pack_start(self.extra_panel, NO_EXPAND, FILL, NO_PADDING) + for el in header, self.main_panel, self.stale_panel: + self.pack_start(el, NO_EXPAND, FILL, NO_PADDING) def after_colorize(self) -> None: self.controller.unselect_all_mods() From aba00f732a0c6fbeb290441aef7baa97cf646ded Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 21:31:33 +0900 Subject: [PATCH 31/49] feat: mediator rewrite (WIP) --- dzgui/controllers/mc.py | 68 ++++++++-- dzgui/util/format.py | 4 +- dzgui/views/base.py | 176 ++++++++----------------- dzgui/views/components/filter_panel.py | 24 ++-- dzgui/views/components/right_panel.py | 50 ++++--- dzgui/views/trees/tree_log.py | 1 + dzgui/views/trees/tree_servers.py | 20 ++- 7 files changed, 163 insertions(+), 180 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 3e123a4..06e1a51 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -56,18 +56,49 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from dzgui.views.components.buttonbox import ContextualButton - from dzgui.views.base import AppNavigation from dzgui.views.base import OuterWindow + from dzgui.views.trees.tree_base import TreeView + from dzgui.views.trees.tree_mods import ModTreeView + from dzgui.views.trees.tree_servers import ServerTreeView + from dzgui.views.trees.tree_log import LogTreeView + from dzgui.views.base import Notebook, Grid, OuterWindow + from dzgui.views.components.statusbar import Statusbar + from dzgui.views.components.right_panel import RightPanel + from dzgui.views.pages.servers import ServerNotebook + +class AppNavigation: + window: "OuterWindow" + grid: "Grid" + right_panel: "RightPanel" + statusbar: "Statusbar" + notebook: "Notebook" + modtreeview: "ModTreeView" + menu: "MenuTreeView" + servers: "ServerNotebook" + browser: "ServerTreeView" + saved: "ServerTreeView" + recent: "ServerTreeView" + lan: "ServerTreeView" + modtreeview: "ModTreeView" + logtreeview: "LogTreeView" + +# TODO: most server contexts can be dropped from this struct class Controller: def __init__(self) -> None: self.crumbs_cache = "" - self.mediator: AppNavigation + self.mediator = AppNavigation() self.prefs: UserPrefs self.cooldown = 0 self.model_manager = ModelManager() + 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 set_crumbs(self, text: str) -> None: self.mediator.grid.set_breadcrumbs(text) @@ -124,11 +155,12 @@ class Controller: def append_map(self, map_row: list) -> None: self.model_manager.append_map(map_row) - def set_mediator(self, mediator: "AppNavigation") -> None: - self.mediator = mediator + # TODO: drop + # def set_mediator(self, mediator: "AppNavigation") -> None: + # self.mediator = mediator - def get_mediator(self) -> "AppNavigation": - return self.mediator + # def get_mediator(self) -> "AppNavigation": + # return self.mediator def unblock_signals(self) -> None: self.block_signals(False) @@ -170,8 +202,14 @@ class Controller: def toggle_debug_mode(self) -> None: self.toggle_config(Preferences.DEBUG) + def get_active_treeview(self) -> "TreeView": + 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.mediator.notebook.servers.get_active_treeview() + treeview = self.get_active_treeview() columns = treeview.get_columns() columns_file = self.prefs.paths.columns @@ -295,8 +333,12 @@ class Controller: def set_statusbar_by_row(self, row: "RowType") -> None: self.mediator.grid.statusbar.refresh(row) + def toggle_server_panels(self, state: bool) -> None: + self.mediator.grid.toggle_filter_panel(state) + self.mediator.grid.toggle_connect_panel(state) + def toggle_mod_panel(self, state: bool) -> None: - self.mediator.grid.sel_panel.set_visible(state) + self.mediator.grid.right_panel.sel_panel.set_visible(state) def show_developers_page(self) -> None: self.open_page(NotebookPage.DEVELOPERS) @@ -314,7 +356,6 @@ class Controller: return case ButtonType.OPTIONS: self.mediator.grid.notebook.settings.populate_settings() - self.set_statusbar("") case ButtonType.MODS: self.load_mods() case ButtonType.HELP: @@ -606,3 +647,12 @@ class Controller: treeview.set_loaded(True) self.update_server_status() treeview.grab_focus() + + def focus_button_box(self) -> None: + self.mediator.right_panel.focus_button_box() + + def present_servers(self) -> None: + self.grab_active_treeview() + self.update_server_status() + crumbs = self.mediator.servers.get_cached_label() + self.set_crumbs(crumbs) diff --git a/dzgui/util/format.py b/dzgui/util/format.py index 40224b3..49695d0 100644 --- a/dzgui/util/format.py +++ b/dzgui/util/format.py @@ -45,7 +45,9 @@ def format_mods(size: int, mods: int) -> str: return no_mods l_size = number(size) plural = pluralize("mods", mods) - return f"Found {mods:n} {plural} taking up {l_size} MiB" + # TODO: strings + suffix = "Ctrl-click to select multiple." + return f"Found {mods:n} {plural} taking up {l_size} MiB. {suffix}" def format_player_count(model: Gtk.TreeModel | None) -> str: diff --git a/dzgui/views/base.py b/dzgui/views/base.py index dae181d..0376152 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -33,6 +33,7 @@ from dzgui.views.components.connect_panel import ConnectPanel 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.mods import Mods from dzgui.views.pages.options import Options from dzgui.views.pages.servers import ServerNotebook from dzgui.views.pages.thanks import Thanks @@ -203,15 +204,14 @@ class OuterWindow(Gtk.Window): super().__init__(title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER) self.hb = AppHeaderBar() - MainController.set_mediator(AppNav) - AppNav.window = self + 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.connect("key-press-event", self._on_keypress) + #self.connect("key-press-event", self._on_keypress) self.grid = Grid() self.add(self.grid) @@ -219,24 +219,8 @@ class OuterWindow(Gtk.Window): MainController.set_resolution(self) self.show_all() - self.grid.sel_panel.set_visible(False) - - # TODO: - #self.grid.right_panel.enable_ping_button(False) - css.load_css() - # TODO: call controller directly - AppNav.grid.notebook.set_page_by_enum(NotebookPage.SERVERS) - - # TODO: deprecated - def _on_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: - if event.state is Gdk.ModifierType.CONTROL_MASK \ - and event.keyval is Gdk.KEY_d: - if AppNav.right_panel.filters_vbox.keyword_entry.is_focus(): - return - if AppNav.right_panel.filters_vbox.maps_entry.is_focus(): - return - AppNav.right_panel.toggle_debug() + MainController.open_page(NotebookPage.SERVERS) def _on_delete_event( self, window: "OuterWindow", event: Gdk.EventKey @@ -280,47 +264,35 @@ class ScrollableNote(ScrollableMixin, Gtk.Box): self.add(self.gutter) def _on_back_clicked(self, button: Gtk.Button) -> None: - AppNav.notebook.return_prior() + pass class Notebook(ScrollableMixin, Gtk.Notebook): def __init__(self) -> None: super().__init__(show_tabs=False) - AppNav.notebook = self self.prior_page: NotebookPage self.prior_status: str - self.help = Help(MainController) - view = self.help.get_treeview() - AppNav.menu = view + MainController.register_widget("notebook", self) + self.help = Help(MainController) self.change = Changelog(MainController) self.clog = ScrollableNote(self.change, back_button=False) self.clog.scrollable.set_propagate_natural_width(False) # TODO: scrollable internally + # TODO: make all treeviews internally scrollable in base class self.keys = ScrollableNote(Keybindings()) self.settings = Options(MainController) - # TODO: make all treeviews internally scrollable in base class # NOTE: server and quad tables should have hexpand property set to True - # add all treeviews as page and register them to AppNav and self.indexes - # when switching to a treeview, update relevant view and just pop that page - # instead of loading/unloading the model each time self.servers = ServerNotebook(MainController) - AppNav.servers = self.servers - - self.scroll_mod = Gtk.ScrolledWindow() - # TODO: register this table - self.quad_table = ModTreeView(MainController) - AppNav.modtreeview = self.quad_table - self.scroll_mod.add(self.quad_table) + self.mods = Mods(MainController) self.scroll_log = Gtk.ScrolledWindow() self.scroll_log.set_hexpand(True) self.log_table = LogTreeView(MainController) - AppNav.logtreeview = self.log_table self.scroll_log.add(self.log_table) self.thanks = ScrollableNote(Thanks(), back_button=False) @@ -334,7 +306,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.keys: NotebookPage.KEYS, self.settings: NotebookPage.OPTIONS, self.servers: NotebookPage.SERVERS, - self.scroll_mod: NotebookPage.MODS, + self.mods: NotebookPage.MODS, self.scroll_log: NotebookPage.LOG, self.thanks: NotebookPage.THANKS, self.developers: NotebookPage.DEVELOPERS, @@ -350,45 +322,38 @@ class Notebook(ScrollableMixin, Gtk.Notebook): page.show_all() index = self.append_page(page) enum = self.pages[page] - crumbs = enum.dict["crumbs"] self.indexes[enum] = index self.connect_after("switch-page", self._on_page_changed) self.connect("key-press-event", self._on_keypress) - def _on_mods_focus_change(self, widget: ModTreeView, event: Gdk.EventFocus) -> None: - MainController.toggle_mod_panel(event.in_) - def _on_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: page = self.get_page() match event.keyval: - case Gdk.KEY_Return: - page = self.get_page() - if page: - page.back_button.clicked() case Gdk.KEY_Right | Gdk.KEY_l: if event.state is Gdk.ModifierType.CONTROL_MASK: return - AppNav.right_panel.focus_button_box() + MainController.focus_button_box() case Gdk.KEY_question: self.toggle_keybindings() # NOTE: abort on non scrollable pages - # TODO: this should be an internal property of those pages - allowed = (NotebookPage.KEYS, NotebookPage.CHANGELOG, NotebookPage.THANKS) - if self.pages[page] not in allowed: - return + # TODO: deprecated + #allowed = (NotebookPage.KEYS, NotebookPage.CHANGELOG, NotebookPage.THANKS) + #if self.pages[page] not in allowed: + # return - match event.keyval: - case Gdk.KEY_k | Gdk.KEY_Up: - page._set_adjustment(VAdjustment.UP) - case Gdk.KEY_Down | Gdk.KEY_j: - page._set_adjustment(VAdjustment.DOWN) - case Gdk.KEY_g: - page._set_adjustment(VAdjustment.TOP) - case Gdk.KEY_G: - page._set_adjustment(VAdjustment.BOTTOM) + # FIXME: may already be delegated to some pages (e.g. keys) + #match event.keyval: + # case Gdk.KEY_k | Gdk.KEY_Up: + # page._set_adjustment(VAdjustment.UP) + # case Gdk.KEY_Down | Gdk.KEY_j: + # page._set_adjustment(VAdjustment.DOWN) + # case Gdk.KEY_g: + # page._set_adjustment(VAdjustment.TOP) + # case Gdk.KEY_G: + # page._set_adjustment(VAdjustment.BOTTOM) def return_prior(self) -> None: """ @@ -428,8 +393,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): return if widget is self.servers: - view = self.servers.get_active_treeview() - view.grab_focus() + MainController.grab_active_treeview() return w = widget.get_children()[0] @@ -447,37 +411,33 @@ class Notebook(ScrollableMixin, Gtk.Notebook): return widget def set_page_by_enum(self, enum: NotebookPage) -> None: - self.prior_page = self.get_page_by_enum() + 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() - # TODO: should be an internal property of those pages - match enum: - case NotebookPage.KEYS: - MainController.set_statusbar("") - AppNav.grid.hide_connect_panel() - case NotebookPage.SERVERS: - # TODO: consolidate in mc.py - AppNav.grid.show_connect_panel() - AppNav.grid.show_filter_panel() - self.servers.get_active_treeview().grab_focus() - MainController.update_server_status() - crumbs = self.servers.get_cached_label() - MainController.set_crumbs(crumbs) - case _: - AppNav.grid.hide_connect_panel() - AppNav.grid.hide_filter_panel() - def _on_page_changed( self, notebook: "Notebook", page: Gtk.Widget, page_num: int ) -> None: enum = self.get_page_by_enum() - is_mods = True if enum is NotebookPage.MODS else False - MainController.toggle_mod_panel(is_mods) + if enum is not None: + crumbs = enum.dict["crumbs"] + MainController.set_crumbs(crumbs) - # TODO: - crumbs = enum.dict["crumbs"] - MainController.set_crumbs(crumbs) + is_mods = True if enum is NotebookPage.MODS else False + is_servers = True if enum is NotebookPage.SERVERS else False + + MainController.toggle_mod_panel(is_mods) + MainController.toggle_server_panels(is_servers) + + match enum: + case NotebookPage.KEYS | NotebookPage.OPTIONS: + MainController.set_statusbar("") + case NotebookPage.SERVERS: + MainController.present_servers() + case _: + pass class Grid(Gtk.Grid): @@ -489,22 +449,17 @@ class Grid(Gtk.Grid): SINGLE_ROW = 1 SINGLE_COL = 1 + MainController.register_widget("grid", self) + self.statusbar = Statusbar(MainController) + self.right_panel = RightPanel(MainController) self.breadcrumbs = Gtk.Label(halign=Gtk.Align.START) self.set_breadcrumbs(strings.label_main_menu) - AppNav.statusbar = self.statusbar - AppNav.grid = self - - # FIXME: do not pass AppNav to right panel - self.right_panel = RightPanel(AppNav, MainController) - # TODO: move into right panel - self.sel_panel = ModSelectionPanel(MainController) - self.right_panel.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING) - self.notebook = Notebook() self.conpan = ConnectPanel() + self.attach(self.notebook, 0, 0, MAX_COLS, 1) els = ( @@ -518,17 +473,11 @@ class Grid(Gtk.Grid): self.show_all() - def hide_filter_panel(self) -> None: - self.right_panel.filters_vbox.set_visible(False) + def toggle_filter_panel(self, state: bool) -> None: + self.right_panel.filters_vbox.set_visible(state) - def show_filter_panel(self) -> None: - self.right_panel.filters_vbox.set_visible(True) - - def hide_connect_panel(self) -> None: - self.conpan.set_visible(False) - - def show_connect_panel(self) -> None: - self.conpan.set_visible(True) + def toggle_connect_panel(self, state: bool) -> None: + self.conpan.set_visible(state) def get_breadcrumbs(self) -> str: return self.breadcrumbs.get_text() @@ -572,23 +521,4 @@ class App(Gtk.Application): ) -> None: self.win.halt_proc_and_quit() - -class AppNavigation: - grid: Grid - window: OuterWindow - notebook: Notebook - right_panel: RightPanel - statusbar: Statusbar - menu: MenuTreeView - servers: Gtk.ScrolledWindow - browser: ServerTreeView - saved: ServerTreeView - recent: ServerTreeView - lan: ServerTreeView - modtreeview: ModTreeView - logtreeview: LogTreeView - # TODO: add tree_log and tree_server views here - # or obtain via get_treeview() - -AppNav = AppNavigation() MainController = Controller() diff --git a/dzgui/views/components/filter_panel.py b/dzgui/views/components/filter_panel.py index fc99b81..623c7a7 100644 --- a/dzgui/views/components/filter_panel.py +++ b/dzgui/views/components/filter_panel.py @@ -13,7 +13,7 @@ from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING logger = logging.getLogger(__name__) class FilterPanel(Gtk.Box): - def __init__(self, appnav, controller): + def __init__(self, controller): super().__init__(spacing=6, vexpand=False) # TODO: set strings in constants @@ -32,7 +32,6 @@ class FilterPanel(Gtk.Box): strings.filter_modded: True, } - self.AppNav = appnav self.controller = controller map_store = self.controller.get_map_store() @@ -140,7 +139,8 @@ class FilterPanel(Gtk.Box): self.keyword_filter = "" self.reinit_filters() self.set_visible(False) - sel_panel = self.AppNav.grid.sel_panel + # TODO: + sel_panel = self.controller.mediator.grid.sel_panel if sel_panel.is_visible(): sel_panel.set_visible(False) @@ -194,7 +194,8 @@ class FilterPanel(Gtk.Box): completion.set_model(map_store) def restore_focus_to_treeview(self) -> Literal[False]: - self.AppNav.treeview.grab_focus() + view = self.controller.get_active_treeview() + view.grab_focus() return False def _on_keyword_keypress( @@ -233,7 +234,8 @@ class FilterPanel(Gtk.Box): return self.keyword_filter def _on_keyword_enter(self, entry: Gtk.Entry) -> None: - self.AppNav.window.set_keep_below(False) + # TODO: + self.controller.mediator.window.set_keep_below(False) keyword = entry.get_text().lower() if keyword == self.keyword_filter: return @@ -241,7 +243,8 @@ class FilterPanel(Gtk.Box): return logger.info(f"User filtered by keyword '{keyword}'") self.keyword_filter = keyword - self.AppNav.treeview.filter(FilterMode.KEYWORD, keyword) + treeview = self.controller.get_active_treeview() + treeview.filter(FilterMode.KEYWORD, keyword) def _on_button_release(self, window, button) -> Literal[True]: return True @@ -258,7 +261,9 @@ class FilterPanel(Gtk.Box): check.set_active(not state) def _on_check_toggled(self, button: Gtk.CheckButton) -> None: - if not self.AppNav.treeview.is_server_context(self.AppNav.treeview.view): + treeview = self.controller.get_active_treeview() + # TODO: drop + if not treeview.is_server_context(self.AppNav.treeview.view): return label = button.get_label() state = button.get_active() @@ -269,9 +274,10 @@ class FilterPanel(Gtk.Box): mode = FilterMode.TOGGLE_OFF self.enabled_filters[label] = state - self.AppNav.treeview.filter(mode, label) + treeview.filter(mode, label) def _on_map_changed(self, combo: Gtk.ComboBox) -> None: + treeview = self.controller.get_active_treeview() old_sel = self.selected_map model = combo.get_model() tree_iter = combo.get_active_iter() @@ -286,4 +292,4 @@ class FilterPanel(Gtk.Box): self.prior_map = self.selected_map self.selected_map = selection self.maps_entry.set_text(selection) - self.AppNav.treeview.filter(FilterMode.MAP) + treeview.filter(FilterMode.MAP) diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index c653c77..879b8c7 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -5,6 +5,7 @@ from gi.repository import Gtk, Gdk from dzgui.const.enum import Preferences from dzgui.views.components.buttonbox import ButtonBox from dzgui.views.components.filter_panel import FilterPanel +from dzgui.views.components.mod_panel import ModSelectionPanel from dzgui.views.components.icon import Icon from dzgui.views.components.web_button import RefreshButton # TODO: rename web_button @@ -13,15 +14,20 @@ from dzgui.util import strings # TODO: refactor depends on ServerTreeView class RightPanel(Gtk.Box): - def __init__(self, appnav, controller): + def __init__(self, controller): super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL) - self.AppNav = appnav - self.AppNav.right_panel = self self.controller = controller - self.button_vbox = ButtonBox(controller) - self.filters_vbox = FilterPanel(appnav, controller) + + + self.filters_vbox = FilterPanel(controller) + self.sel_panel = ModSelectionPanel(controller) + + self.controller.register_widget("right_panel", self) + + self.sel_panel.set_visible(False) + self.filters_vbox.set_visible(False) # TODO: more custom button classes self.ping = Gtk.Button( @@ -49,26 +55,30 @@ class RightPanel(Gtk.Box): self.refresh_button.set_margin_end(80) self.refresh_button.connect("clicked", self._on_refresh_clicked) - if controller.query_config(Preferences.DEBUG) == True: - self.debug_toggle.set_active(True) - self.debug_toggle.connect("toggled", self._on_debug_toggled) + #if controller.query_config(Preferences.DEBUG) == True: + # self.debug_toggle.set_active(True) + #self.debug_toggle.connect("toggled", self._on_debug_toggled) # TODO: make button class i = Icon(INPUT_KEYBOARD) i.set_margin_start(5) - self.question = Gtk.Button( + self.keys = Gtk.Button( label=strings.keys_button, margin_start=80, margin_end=80, tooltip_text=strings.keys_tooltip, image = i, ) - self.question.set_image_position(Gtk.PositionType.RIGHT) - self.question.connect("clicked", self._on_question_clicked) + self.keys.set_image_position(Gtk.PositionType.RIGHT) + self.keys.connect("clicked", self._on_question_clicked) - for el in self.button_vbox, self.question, self.filters_vbox, self.refresh_button: + for el in self.button_vbox, self.filters_vbox, self.refresh_button: self.pack_start(el, NO_EXPAND, FILL, NO_PADDING) + self.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING) + self.pack_start(self.keys, NO_EXPAND, FILL, NO_PADDING) + + def enable_ping_button(self, state: bool) -> None: self.ping.set_visible(state) @@ -80,17 +90,6 @@ class RightPanel(Gtk.Box): self.selected = "All maps" self.filters_vbox.set_unique_maps(rows) - def toggle_debug(self) -> None: - if type(self.AppNav.window.get_focus()) is Gtk.Entry: - return - state = self.debug_toggle.get_active() - self.debug_toggle.set_active(not state) - - def _on_debug_toggled(self, button: Gtk.Button) -> None: - state = button.get_active() - grid = self.AppNav.grid - self.controller.toggle_debug_mode() - def _on_refresh_clicked(self, button: RefreshButton) -> None: self.controller.refresh_tree() @@ -112,11 +111,10 @@ class RightPanel(Gtk.Box): path = Gtk.TreePath.new_from_indices([res.iteration]) temp_model[path][9] = res.ping ModelManager.ping_cache[res.addr] = res.ping + # TODO: drop/rewrite self.AppNav.treeview.set_model(temp_model) self.AppNav.treeview.wait_dialog.destroy() - self.AppNav.treeview.enable_ping_column(True) self.AppNav.treeview.grab_focus() - self.AppNav.right_panel.ping.set_sensitive(False) # TODO: unblock_signals() @@ -129,7 +127,7 @@ class RightPanel(Gtk.Box): thread.start() def _on_question_clicked(self, button: Gtk.Button) -> None: - self.AppNav.grid.notebook.toggle_keybindings() + self.controller.open_keybindings() def focus_button_box(self) -> None: self.button_vbox.buttons[0].grab_focus() diff --git a/dzgui/views/trees/tree_log.py b/dzgui/views/trees/tree_log.py index b85c9a8..2d891f3 100644 --- a/dzgui/views/trees/tree_log.py +++ b/dzgui/views/trees/tree_log.py @@ -21,6 +21,7 @@ class LogTreeView(ContextMixin, TreeView): super().__init__(controller, menu=ContextMenuGroup.LOG) self.controller = controller + self.controller.register_widget("logtreeview", self) # TODO: maybe put this in init self.set_fixed_height_mode(True) diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index b5bd183..de04c6f 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -43,6 +43,8 @@ class ServerTreeView(TreeView): def __init__(self, controller: "Controller") -> None: super().__init__(controller) + QUEUE_CHECK_DELAY = 200 + self.loaded = False self.query_func: Callable = None @@ -50,8 +52,6 @@ class ServerTreeView(TreeView): self.menu.connect("key-press-event", self._on_key) self.controller = controller - self.resizable_cols: list[Gtk.TreeViewColumn] = [] - self.set_fixed_height_mode(True) self.set_headers_visible(True) @@ -89,8 +89,6 @@ class ServerTreeView(TreeView): if column_title == "Map": column.set_fixed_width(300) - - self.resizable_cols.append(column) column.connect("notify::fixed-width", self._on_col_width_changed) self.append_column(column) @@ -100,7 +98,7 @@ class ServerTreeView(TreeView): self.connect("generic_row_activated", self._parent_row_activated) self.connect("generic_treesel_changed", self._parent_selection_changed) - GLib.timeout_add(200, self._check_result_queue) + GLib.timeout_add(QUEUE_CHECK_DELAY, self._check_result_queue) def _on_key(self, menu: Gtk.Menu, event: Gdk.EventKey) -> bool | None: if not is_navkey(event.keyval): @@ -173,6 +171,7 @@ class ServerTreeView(TreeView): def _on_server_keypress( self, treeview: Gtk.TreeView, event: Gdk.EventKey ) -> bool | None: + # TODO: rewrite AppNav # TODO: use mixins # CONTROL_MASK + KEY_l if event.state is Gdk.ModifierType.CONTROL_MASK: @@ -182,8 +181,6 @@ class ServerTreeView(TreeView): case Gdk.KEY_r: self.refresh_player_count() case Gdk.KEY_f: - if not AppNav.treeview.is_server_context(AppNav.treeview.view): - return True AppNav.right_panel.filters_vbox.keyword_entry.grab_focus() case Gdk.KEY_m: AppNav.right_panel.filters_vbox.maps_entry.grab_focus() @@ -216,7 +213,6 @@ class ServerTreeView(TreeView): self.menu.show_all() def _on_menu_click(self, item) -> None: - print(item.enum) pass def _on_server_button_release( @@ -278,8 +274,9 @@ class ServerTreeView(TreeView): self.terminate_process() record = self.get_record() - #TODO - return + + if record is None: + return #model = self.get_model() #if record is None: @@ -287,7 +284,6 @@ class ServerTreeView(TreeView): # return ip = record.ip - # TODO #self.emit("on_distcalc_started") #self.current_proc = CalcDist(self, record.ip, self.queue, cache) @@ -299,6 +295,7 @@ class ServerTreeView(TreeView): return f"{addr}:{qport}" def get_record(self) -> dict | None: + # TODO: delegate to controller select = self.get_selection() sels = select.get_selected_rows() (model, pathlist) = sels @@ -319,4 +316,3 @@ class ServerTreeView(TreeView): def set_loaded(self, status: bool) -> None: self.loaded = status - From 5f0c8f76548aae13f02df2bccb6503220f51df2f Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 1 Jan 2026 23:49:16 +0900 Subject: [PATCH 32/49] feat: apply scrollable mixin to pages --- dzgui/const/enum.py | 18 +++++------ dzgui/controllers/mc.py | 24 ++++++++++----- dzgui/util/keys.py | 19 ++++++++++++ dzgui/views/base.py | 42 ++++++-------------------- dzgui/views/components/filter_panel.py | 11 +++---- dzgui/views/mixins/scrollable_mixin.py | 17 +++++++++-- dzgui/views/pages/changelog.py | 11 +++++-- dzgui/views/pages/keys.py | 16 +++++++--- dzgui/views/trees/tree_base.py | 16 +++++++--- dzgui/views/trees/tree_mods.py | 9 +++++- dzgui/views/trees/tree_servers.py | 20 ++---------- 11 files changed, 115 insertions(+), 88 deletions(-) diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 224a5b9..aa0dc9c 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -77,15 +77,15 @@ class Preferences(EnumWithAttrs): class NotebookPage(EnumWithAttrs): - CHANGELOG = {"crumbs": strings.crumbs.changelog,} - KEYS = {"crumbs": strings.crumbs.keys,} - LOG = {"crumbs": strings.crumbs.log,} - HELP = {"crumbs": strings.crumbs._help,} - MODS = {"crumbs": strings.crumbs.mods,} - OPTIONS = {"crumbs": strings.crumbs.options,} - SERVERS = {"crumbs": strings.crumbs.servers,} - THANKS = {"crumbs": strings.crumbs.thanks,} - DEVELOPERS = {"crumbs": strings.crumbs.developers,} + CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": False} + KEYS = {"crumbs": strings.crumbs.keys, "statusbar": False} + LOG = {"crumbs": strings.crumbs.log, "statusbar": False} + HELP = {"crumbs": strings.crumbs._help, "statusbar": True} + MODS = {"crumbs": strings.crumbs.mods, "statusbar": True} + OPTIONS = {"crumbs": strings.crumbs.options, "statusbar": False} + SERVERS = {"crumbs": strings.crumbs.servers, "statusbar": True} + THANKS = {"crumbs": strings.crumbs.thanks, "statusbar": False} + DEVELOPERS = {"crumbs": strings.crumbs.developers, "statusbar": False} class RowType(EnumWithAttrs): diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 06e1a51..2d747f0 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -155,13 +155,6 @@ class Controller: def append_map(self, map_row: list) -> None: self.model_manager.append_map(map_row) - # TODO: drop - # def set_mediator(self, mediator: "AppNavigation") -> None: - # self.mediator = mediator - - # def get_mediator(self) -> "AppNavigation": - # return self.mediator - def unblock_signals(self) -> None: self.block_signals(False) @@ -643,6 +636,7 @@ class Controller: model = treeview.get_model() model.clear() data = func() + # TODO: threading model.append(data) treeview.set_loaded(True) self.update_server_status() @@ -656,3 +650,19 @@ class Controller: self.update_server_status() crumbs = self.mediator.servers.get_cached_label() self.set_crumbs(crumbs) + + 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 diff --git a/dzgui/util/keys.py b/dzgui/util/keys.py index 04f291d..4f20038 100644 --- a/dzgui/util/keys.py +++ b/dzgui/util/keys.py @@ -23,3 +23,22 @@ def is_ctrl_mask(event: Gdk.EventKey) -> bool: return True else: return False + +def is_filterkey(key: int) -> bool: + keys = ( + Gdk.KEY_0, + Gdk.KEY_1, + Gdk.KEY_2, + Gdk.KEY_3, + Gdk.KEY_4, + Gdk.KEY_5, + Gdk.KEY_6, + Gdk.KEY_7, + Gdk.KEY_8, + Gdk.KEY_9, + Gdk.KEY_backslash, + Gdk.KEY_minus, + ) + if key in keys: + return True + return False diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 0376152..41919e4 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -277,27 +277,27 @@ class Notebook(ScrollableMixin, Gtk.Notebook): MainController.register_widget("notebook", self) self.help = Help(MainController) - self.change = Changelog(MainController) - self.clog = ScrollableNote(self.change, back_button=False) - self.clog.scrollable.set_propagate_natural_width(False) + self.clog = Changelog(MainController) - # TODO: scrollable internally # TODO: make all treeviews internally scrollable in base class - self.keys = ScrollableNote(Keybindings()) + self.keys = Keybindings() self.settings = Options(MainController) # NOTE: server and quad tables should have hexpand property set to True self.servers = ServerNotebook(MainController) self.mods = Mods(MainController) + # TODO: change this class to scrolledwindow self.scroll_log = Gtk.ScrolledWindow() self.scroll_log.set_hexpand(True) self.log_table = LogTreeView(MainController) self.scroll_log.add(self.log_table) + # TODO: change this class to scrolledwindow self.thanks = ScrollableNote(Thanks(), back_button=False) developers = Developers(MainController) + # TODO: change this class to scrolledwindow self.developers = ScrollableNote(developers) self.pages = { @@ -328,8 +328,6 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.connect("key-press-event", self._on_keypress) def _on_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: - page = self.get_page() - match event.keyval: case Gdk.KEY_Right | Gdk.KEY_l: if event.state is Gdk.ModifierType.CONTROL_MASK: @@ -338,23 +336,6 @@ class Notebook(ScrollableMixin, Gtk.Notebook): case Gdk.KEY_question: self.toggle_keybindings() - # NOTE: abort on non scrollable pages - # TODO: deprecated - #allowed = (NotebookPage.KEYS, NotebookPage.CHANGELOG, NotebookPage.THANKS) - #if self.pages[page] not in allowed: - # return - - # FIXME: may already be delegated to some pages (e.g. keys) - #match event.keyval: - # case Gdk.KEY_k | Gdk.KEY_Up: - # page._set_adjustment(VAdjustment.UP) - # case Gdk.KEY_Down | Gdk.KEY_j: - # page._set_adjustment(VAdjustment.DOWN) - # case Gdk.KEY_g: - # page._set_adjustment(VAdjustment.TOP) - # case Gdk.KEY_G: - # page._set_adjustment(VAdjustment.BOTTOM) - def return_prior(self) -> None: """ Gtk.Notebook focuses the first input field when changing pages; @@ -423,6 +404,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): enum = self.get_page_by_enum() if enum is not None: crumbs = enum.dict["crumbs"] + status = enum.dict["statusbar"] MainController.set_crumbs(crumbs) is_mods = True if enum is NotebookPage.MODS else False @@ -431,13 +413,10 @@ class Notebook(ScrollableMixin, Gtk.Notebook): MainController.toggle_mod_panel(is_mods) MainController.toggle_server_panels(is_servers) - match enum: - case NotebookPage.KEYS | NotebookPage.OPTIONS: - MainController.set_statusbar("") - case NotebookPage.SERVERS: - MainController.present_servers() - case _: - pass + if status is False: + MainController.set_statusbar("") + if enum is NotebookPage.SERVERS: + MainController.present_servers() class Grid(Gtk.Grid): @@ -459,7 +438,6 @@ class Grid(Gtk.Grid): self.notebook = Notebook() self.conpan = ConnectPanel() - self.attach(self.notebook, 0, 0, MAX_COLS, 1) els = ( diff --git a/dzgui/views/components/filter_panel.py b/dzgui/views/components/filter_panel.py index 623c7a7..ad51ac7 100644 --- a/dzgui/views/components/filter_panel.py +++ b/dzgui/views/components/filter_panel.py @@ -2,10 +2,11 @@ import logging import gi # noqa E402 gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk, Pango +from gi.repository import Gtk, Gdk, Pango, GLib from typing import Literal from dzgui.util import strings +from dzgui.util.format import embolden from dzgui.util.margins import set_surrounding_margins from dzgui.const.enum import FilterMode from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING @@ -71,8 +72,9 @@ class FilterPanel(Gtk.Box): self.set_margin_top(1) # TODO: strings - # TODO: embolden - self.filters_label = Gtk.Label(label="Filters") + text = embolden("Filters") + self.filters_label = Gtk.Label() + self.filters_label.set_markup(text) self.keyword_entry = Gtk.Entry() self.keyword_entry.set_placeholder_text("Filter by keyword") @@ -262,9 +264,6 @@ class FilterPanel(Gtk.Box): def _on_check_toggled(self, button: Gtk.CheckButton) -> None: treeview = self.controller.get_active_treeview() - # TODO: drop - if not treeview.is_server_context(self.AppNav.treeview.view): - return label = button.get_label() state = button.get_active() logger.info(f"User toggled button '{label}' to {state}") diff --git a/dzgui/views/mixins/scrollable_mixin.py b/dzgui/views/mixins/scrollable_mixin.py index 735c061..4d9e406 100644 --- a/dzgui/views/mixins/scrollable_mixin.py +++ b/dzgui/views/mixins/scrollable_mixin.py @@ -3,11 +3,22 @@ from dzgui.const.constants import SCROLL_INCREMENT import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gdk # noqa +from gi.repository import Gtk, Gdk # noqa class ScrollableMixin: - def _set_adjustment(self, adjustment = VAdjustment) -> None: - vadj = self.scrollable.get_vadjustment() + def _on_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: + match event.keyval: + case Gdk.KEY_k | Gdk.KEY_Up: + self._set_adjustment(VAdjustment.UP) + case Gdk.KEY_Down | Gdk.KEY_j: + self._set_adjustment(VAdjustment.DOWN) + case Gdk.KEY_g: + self._set_adjustment(VAdjustment.TOP) + case Gdk.KEY_G: + self._set_adjustment(VAdjustment.BOTTOM) + + def _set_adjustment(self, adjustment: VAdjustment) -> None: + vadj = self.get_vadjustment() match adjustment: case VAdjustment.TOP: adj = vadj.get_lower() diff --git a/dzgui/views/pages/changelog.py b/dzgui/views/pages/changelog.py index 2caa366..d145a50 100644 --- a/dzgui/views/pages/changelog.py +++ b/dzgui/views/pages/changelog.py @@ -5,6 +5,7 @@ from importlib import resources from dzgui.const.constants import APP_NAME_LOWER from dzgui.util.format import format_pango +from dzgui.views.mixins.scrollable_mixin import ScrollableMixin import gi gi.require_version("Gtk", "3.0") @@ -16,9 +17,9 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from dzgui.controllers.mc import Controller -class Changelog(Gtk.Box): +class Changelog(ScrollableMixin, Gtk.ScrolledWindow): def __init__(self, controller: "Controller"): - super().__init__() + super().__init__(propagate_natural_width=False) try: changelog = resources.read_text(APP_NAME_LOWER, "data/CHANGELOG.md") @@ -30,4 +31,8 @@ class Changelog(Gtk.Box): formatted = format_pango(changelog) self.changelog_label = Gtk.Label(valign=Gtk.Align.START, margin=15) self.changelog_label.set_markup(formatted) - self.add(self.changelog_label) + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.box.add(self.changelog_label) + self.add(self.box) + + self.connect("key-press-event", self._on_keypress) diff --git a/dzgui/views/pages/keys.py b/dzgui/views/pages/keys.py index 2b12f62..9fa357b 100644 --- a/dzgui/views/pages/keys.py +++ b/dzgui/views/pages/keys.py @@ -1,25 +1,31 @@ +from dzgui.views.mixins.scrollable_mixin import ScrollableMixin from dzgui.util.strings import navigation, servers, vim, key_header, key_contexts from dzgui.util.css import add_class import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk, Gdk # noqa E402 -class Keybindings(Gtk.Box): +class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): """ Notebook page holding a prearranged grid of keybindings and their descriptions """ def __init__(self) -> None: - super().__init__(orientation=Gtk.Orientation.VERTICAL) + super().__init__() + + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) label = Gtk.Label(label=key_header) add_class(label, "page-heading") - self.add(label) grid = self.build_grid([servers, navigation, vim]) - self.add(grid) + self.box.add(label) + self.box.add(grid) + self.add(self.box) + + self.connect("key-press-event", self._on_keypress) def build_keys(self, items: list) -> Gtk.Grid: grid = Gtk.Grid(row_spacing=10, column_homogeneous=True) diff --git a/dzgui/views/trees/tree_base.py b/dzgui/views/trees/tree_base.py index 1b50607..1e8c9ce 100644 --- a/dzgui/views/trees/tree_base.py +++ b/dzgui/views/trees/tree_base.py @@ -67,6 +67,11 @@ class TreeView(CursorMixin, Gtk.TreeView): it = self.get_selection().get_selected()[1] return it + def get_focused_row_iter(self) -> Gtk.TreeIter: + path = self.get_focused_row_path() + model = self.get_model() + return model.get_iter(path) + def get_focused_row_path(self) -> Gtk.TreePath: return self.get_cursor().path @@ -84,15 +89,11 @@ class TreeView(CursorMixin, Gtk.TreeView): if is_navkey(event.keyval): # TODO: investigate this - # TODO: get active tree if server view is open - servers = self.controller.mediator.notebook.servers - tv = servers.get_active_treeview() + tv = self.controller.get_active_treeview() if self.sel_blocked is False: self.controller.suppress_signal( tv, tv.selected_row, - #self.controller.mediator.treeview, - #self.controller.mediator.treeview.selected_row, "_on_tree_selection_changed", True, ) @@ -106,6 +107,11 @@ class TreeView(CursorMixin, Gtk.TreeView): """ Suppresses spamming on keydown """ + #if event.keyval is Gdk.KEY_space: + # it = self.get_focused_row_iter() + # self.get_selection().select_iter(it) + # return True + if is_navkey(event.keyval): if self.sel_blocked is True: self.controller.suppress_signal( diff --git a/dzgui/views/trees/tree_mods.py b/dzgui/views/trees/tree_mods.py index b7417d4..b7a39f0 100644 --- a/dzgui/views/trees/tree_mods.py +++ b/dzgui/views/trees/tree_mods.py @@ -43,7 +43,7 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): column.set_fixed_width(500) else: column.set_fixed_width(150) - # hidden color property column + # NOTE: hidden color property column column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) column.set_sort_column_id(i) if i != 4: @@ -61,6 +61,13 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): self.s = self.get_selection().get_selected_rows() def _on_mods_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: + #if event.keyval is Gdk.KEY_space: + # it = self.get_focused_row_iter() + # self.get_selection().select_iter(it) + # path = self.get_focused_row_path() + # self.set_cursor(path) + # return False + # TODO: parse keys self.present_menu(widget, event) def _on_mods_button_press(self, diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index de04c6f..b24afff 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -171,7 +171,6 @@ class ServerTreeView(TreeView): def _on_server_keypress( self, treeview: Gtk.TreeView, event: Gdk.EventKey ) -> bool | None: - # TODO: rewrite AppNav # TODO: use mixins # CONTROL_MASK + KEY_l if event.state is Gdk.ModifierType.CONTROL_MASK: @@ -181,28 +180,15 @@ class ServerTreeView(TreeView): case Gdk.KEY_r: self.refresh_player_count() case Gdk.KEY_f: - AppNav.right_panel.filters_vbox.keyword_entry.grab_focus() + self.controller.mediator.grid.right_panel.filters_vbox.keyword_entry.grab_focus() case Gdk.KEY_m: - AppNav.right_panel.filters_vbox.maps_entry.grab_focus() + self.controller.mediator.grid.right_panel.filters_vbox.maps_entry.grab_focus() else: - keyname = Gdk.keyval_name(event.keyval) - if keyname.isnumeric() and int(keyname) > 0: - digit = int(keyname) - 1 - AppNav.grid.right_panel.filters_vbox.toggle_check(digit) - return False match event.keyval: case Gdk.KEY_l | Gdk.KEY_Right: - #if event.state is Gdk.ModifierType.CONTROL_MASK: - # return self.controller.mediator.right_panel.focus_button_box() - case Gdk.KEY_0: - grid.right_panel.filters_vbox.toggle_check(9) - case Gdk.KEY_minus: - grid.right_panel.filters_vbox.toggle_check(10) - case Gdk.KEY_backslash: - grid.right_panel.filters_vbox.toggle_check(11) case _: - return False + self.controller.toggle_check(event) def set_context_menu(self, items: ContextMenuGroup) -> None: # TODO: if debug is on, add raw command From 06e6065236cc8a865774b3fe651b3b0e5eede674 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:25:00 +0900 Subject: [PATCH 33/49] feat: dynamic connect panel (WIP) --- dzgui/controllers/mc.py | 13 +- dzgui/controllers/model.py | 2 - dzgui/util/keys.py | 19 --- dzgui/util/strings.py | 29 +++++ dzgui/views/base.py | 16 ++- .../components/{web_button.py => buttons.py} | 16 ++- dzgui/views/components/connect_panel.py | 85 +++++++----- dzgui/views/components/filter_panel.py | 29 ++--- dzgui/views/components/mod_panel.py | 6 +- dzgui/views/components/right_panel.py | 123 ++++++------------ dzgui/views/dialogs/link_dialog.py | 7 +- dzgui/views/pages/devs.py | 10 +- dzgui/views/pages/options.py | 6 +- dzgui/views/trees/tree_servers.py | 4 +- 14 files changed, 185 insertions(+), 180 deletions(-) rename dzgui/views/components/{web_button.py => buttons.py} (66%) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 2d747f0..ec4b289 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -329,13 +329,15 @@ class Controller: def toggle_server_panels(self, state: bool) -> None: self.mediator.grid.toggle_filter_panel(state) self.mediator.grid.toggle_connect_panel(state) + self.mediator.grid.toggle_refresh_button(state) def toggle_mod_panel(self, state: bool) -> None: self.mediator.grid.right_panel.sel_panel.set_visible(state) def show_developers_page(self) -> None: self.open_page(NotebookPage.DEVELOPERS) - # TODO: focus first row + # TODO: put cursor on first row + #self.mediator.developers.focus_first_row() def open_page(self, page: NotebookPage) -> None: self.mediator.grid.notebook.set_page_by_enum(page) @@ -363,8 +365,7 @@ class Controller: self.mediator.notebook.set_page_by_enum(button.opens) self.set_crumbs(button.get_label()) - # TODO: deprecated? - def open_self_workshop(self, uid: str) -> None: + 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_workshop_page(uid, client) @@ -666,3 +667,9 @@ class Controller: self.mediator.grid.right_panel.filters_vbox.toggle_check(11) case _: return False + + def get_favorite_label(self) -> str | None: + fav = str(self.query_config(Preferences.FAV_LBL)) + if len(fav) < 1: + return None + return fav diff --git a/dzgui/controllers/model.py b/dzgui/controllers/model.py index 2b75b76..3372bf5 100644 --- a/dzgui/controllers/model.py +++ b/dzgui/controllers/model.py @@ -69,9 +69,7 @@ class ModelManager: self.ping_cache: dict[str, int] = {} self.map_store = ListStore(str) - #self.row_store = self.new_model_from_class(MenuCols) self.help_store = self.new_model_from_class(MenuCols) - self.mod_store = self.new_model_from_class(ModCols) self.log_store = self.new_model_from_class(LogCols) self.modlist_store = self.new_model_from_class(ServerModCols) diff --git a/dzgui/util/keys.py b/dzgui/util/keys.py index 4f20038..04f291d 100644 --- a/dzgui/util/keys.py +++ b/dzgui/util/keys.py @@ -23,22 +23,3 @@ def is_ctrl_mask(event: Gdk.EventKey) -> bool: return True else: return False - -def is_filterkey(key: int) -> bool: - keys = ( - Gdk.KEY_0, - Gdk.KEY_1, - Gdk.KEY_2, - Gdk.KEY_3, - Gdk.KEY_4, - Gdk.KEY_5, - Gdk.KEY_6, - Gdk.KEY_7, - Gdk.KEY_8, - Gdk.KEY_9, - Gdk.KEY_backslash, - Gdk.KEY_minus, - ) - if key in keys: - return True - return False diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 1fcab97..1529a79 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -508,4 +508,33 @@ server_labels = ServerLabels( lan="LAN", ) +@dataclass(slots=True, frozen=True) +class ConnectPanel: + connect: str + add: str + edit: str + favorite: str + add_con: str + placeholder: str + entry_tooltip: str + add_tooltip: str + connect_tooltip: str + no_fav: str + +connect_panel = ConnectPanel( + connect="Connect", + add="Add", + edit="Edit", + favorite="Favorite server", + add_con="Add/connect", + placeholder="Enter IP or Battlemetrics ID", + entry_tooltip=( + "- IP: Format as IP:Query port\ne.g. 192.168.1.1:27016\n" + "- Battlemetrics: numeric server ID\ne.g. 123456" + ), + add_tooltip="Add to my servers", + connect_tooltip="Connect to this server", + no_fav="None set. Right click a server and select 'Set favorite' to set.", +) + distance_suffix = "Distance: calculating..." diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 41919e4..12d3301 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -66,6 +66,7 @@ logger = logging.getLogger(__name__) 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 @@ -283,7 +284,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.keys = Keybindings() self.settings = Options(MainController) - # NOTE: server and quad tables should have hexpand property set to True + # NOTE: server, mod, and log tables should have hexpand property set to True self.servers = ServerNotebook(MainController) self.mods = Mods(MainController) @@ -295,9 +296,8 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # TODO: change this class to scrolledwindow self.thanks = ScrollableNote(Thanks(), back_button=False) - - developers = Developers(MainController) # TODO: change this class to scrolledwindow + developers = Developers(MainController) self.developers = ScrollableNote(developers) self.pages = { @@ -314,7 +314,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.indexes = {} """ - Note that due to historical reasons, GtkNotebook refuses to switch to a page + 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. """ @@ -436,9 +436,9 @@ class Grid(Gtk.Grid): self.set_breadcrumbs(strings.label_main_menu) self.notebook = Notebook() - self.conpan = ConnectPanel() + self.conpan = ConnectPanel(MainController) - self.attach(self.notebook, 0, 0, MAX_COLS, 1) + self.attach(self.notebook, 0, 0, MAX_COLS, SINGLE_ROW) els = ( (self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, MAX_COLS, SINGLE_ROW), @@ -457,6 +457,10 @@ class Grid(Gtk.Grid): 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) + + # TODO make this method internal to Statusbar def get_breadcrumbs(self) -> str: return self.breadcrumbs.get_text() diff --git a/dzgui/views/components/web_button.py b/dzgui/views/components/buttons.py similarity index 66% rename from dzgui/views/components/web_button.py rename to dzgui/views/components/buttons.py index 1089de8..62f56c4 100644 --- a/dzgui/views/components/web_button.py +++ b/dzgui/views/components/buttons.py @@ -1,5 +1,5 @@ from dzgui.views.components.icon import Icon -from dzgui.const.constants import REFRESH_ICON, WEB_BROWSER +from dzgui.const.constants import REFRESH_ICON, WEB_BROWSER, INPUT_KEYBOARD import gi gi.require_version("Gtk", "3.0") @@ -9,7 +9,6 @@ from gi.repository import Gtk # noqa E402 class IconButton(Gtk.Button): def __init__(self, icon: str, margin: int = 0): super().__init__() - i = Icon(icon, l_margin=margin) self.set_image(i) self.set_image_position(Gtk.PositionType.RIGHT) @@ -18,7 +17,6 @@ class IconButton(Gtk.Button): class IconTextButton(IconButton): def __init__(self, icon: str, label: str): super().__init__(icon, margin=5) - self.set_label(label) @@ -31,4 +29,14 @@ class WebButton(IconTextButton): class RefreshButton(IconTextButton): def __init__(self, label: str): super().__init__(icon=REFRESH_ICON, label=label) - pass + self.set_margin_top(10) + self.set_margin_start(80) + self.set_margin_end(80) + + +class KeysButton(IconTextButton): + def __init__(self, label: str): + super().__init__(icon=INPUT_KEYBOARD, label=label) + self.set_margin_top(10) + self.set_margin_start(80) + self.set_margin_end(80) diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index 660dc67..97f132a 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -1,47 +1,72 @@ -from dzgui.util.format import embolden -from dzgui.views.components.web_button import WebButton +from typing import TYPE_CHECKING + +from dzgui.util.strings import connect_panel +from dzgui.views.components.buttons import WebButton +from dzgui.views.components.labels import BoldLabel import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 +if TYPE_CHECKING: + from dzgui.controllers.mc import Controller class ConnectPanel(Gtk.Frame): - def __init__(self) -> None: + def __init__(self, controller: "Controller") -> None: super().__init__(margin_top=10, margin_bottom=5) - self.label = Gtk.Label(label="Add/connect") - self.label2 = Gtk.Label(label="Favorite server") + COLS = 1 + ROWS = 1 + self.controller = controller - # TODO: strings - text = embolden("Favorite server") - self.label2.set_markup(text) - self.entry1 = Gtk.Entry(placeholder_text="Enter IP or Battlemetrics ID", hexpand=False) - self.entry2 = Gtk.Entry() - self.fav = Gtk.Label(label="MY favorite server very long title") - self.favedit = Gtk.Button("Edit") - self.edit = WebButton(label="EDIT") - self.favbutton = Gtk.Button(label="Connect") + self.entry = Gtk.Entry( + placeholder_text=connect_panel.placeholder, + hexpand=True, + tooltip_text=connect_panel.entry_tooltip + ) - # TODO: dedent - long = """IP: Format as IP:Query port, e.g.\n192.168.1.1:27016\n - Battlemetrics: numeric server ID - """ + # TODO: get ip as well? + user_fav = self.controller.get_favorite_label() - # TODO: add tooltips to all buttons - self.entry1.set_tooltip_text(long) + server_name = user_fav if user_fav is not None else connect_panel.no_fav + scrollable_label = Gtk.ScrolledWindow() + label = Gtk.Label(label=server_name, halign=Gtk.Align.START) + scrollable_label.add(label) - self.con = Gtk.Button(label="Connect") - self.addb = Gtk.Button(label="Add") + self.fav_button = Gtk.Button(label=connect_panel.connect, + tooltip_text=connect_panel.connect_tooltip + ) + self.fav_edit = Gtk.Button(label=connect_panel.edit) + self.edit_server = WebButton(label="EDIT") + + self.conn_server= Gtk.Button(label=connect_panel.connect, + tooltip_text=connect_panel.connect_tooltip + ) + self.add_server = Gtk.Button(label=connect_panel.add, + tooltip_text=connect_panel.add_tooltip + ) + + self.conn_server.set_sensitive(False) + self.add_server.set_sensitive(False) + if server_name is None: + self.fav_button.set_sensitive(False) + + add_label = BoldLabel(connect_panel.add_con) + conn_label = BoldLabel(connect_panel.favorite) self.grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5) - self.grid.attach(self.label2, 0, 0, 3, 1) - button = Gtk.Button(label="Connect") - self.grid.attach_next_to(self.fav, self.label2, Gtk.PositionType.RIGHT, 3, 1) - self.grid.attach_next_to(self.favbutton, self.fav, Gtk.PositionType.RIGHT, 3, 1) - self.grid.attach_next_to(self.label, self.label2, Gtk.PositionType.BOTTOM, 3, 1) - self.grid.attach_next_to(self.entry1, self.label, Gtk.PositionType.RIGHT, 3, 1) - self.grid.attach_next_to(self.addb, self.entry1, Gtk.PositionType.RIGHT, 3, 1) - self.grid.attach_next_to(self.con, self.addb, Gtk.PositionType.RIGHT, 3, 1) + self.grid.attach(conn_label, 0, 0, COLS, ROWS) + + els = ( + (scrollable_label, conn_label, Gtk.PositionType.RIGHT, 3, ROWS), + (self.fav_button, scrollable_label, Gtk.PositionType.RIGHT, COLS, ROWS), + (add_label, conn_label, Gtk.PositionType.BOTTOM, COLS, ROWS), + (self.entry, scrollable_label, Gtk.PositionType.BOTTOM, COLS, ROWS), + (self.add_server, self.fav_button, Gtk.PositionType.BOTTOM, COLS, ROWS), + (self.conn_server, self.add_server, Gtk.PositionType.RIGHT, COLS, ROWS), + ) + + for el, sibling, pos, h_span, v_span in els: + self.grid.attach_next_to(el, sibling, pos, h_span, v_span) self.add(self.grid) diff --git a/dzgui/views/components/filter_panel.py b/dzgui/views/components/filter_panel.py index ad51ac7..8afa0d1 100644 --- a/dzgui/views/components/filter_panel.py +++ b/dzgui/views/components/filter_panel.py @@ -1,15 +1,15 @@ import logging +from typing import Literal import gi # noqa E402 gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, Pango, GLib -from typing import Literal -from dzgui.util import strings -from dzgui.util.format import embolden -from dzgui.util.margins import set_surrounding_margins from dzgui.const.enum import FilterMode from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING +from dzgui.util import strings +from dzgui.util.margins import set_surrounding_margins +from dzgui.views.components.labels import BoldLabel logger = logging.getLogger(__name__) @@ -17,7 +17,6 @@ class FilterPanel(Gtk.Box): def __init__(self, controller): super().__init__(spacing=6, vexpand=False) - # TODO: set strings in constants self.default_filters = { strings.filter_1pp: True, strings.filter_day: True, @@ -39,10 +38,11 @@ class FilterPanel(Gtk.Box): self.checks = [] self.maps_hr = [] + self.enabled_filters = dict(self.default_filters) - self.keyword_filter = "" - self.selected_map = strings.all_maps - self.prior_map = strings.all_maps + self.keyword_filter: str + self.selected_map: str = strings.all_maps + self.prior_map: str = strings.all_maps button_grid = Gtk.Grid( halign=Gtk.Align.CENTER, column_spacing=5, column_homogeneous=True @@ -72,9 +72,7 @@ class FilterPanel(Gtk.Box): self.set_margin_top(1) # TODO: strings - text = embolden("Filters") - self.filters_label = Gtk.Label() - self.filters_label.set_markup(text) + self.filters_label = BoldLabel("Filters") self.keyword_entry = Gtk.Entry() self.keyword_entry.set_placeholder_text("Filter by keyword") @@ -99,16 +97,13 @@ class FilterPanel(Gtk.Box): self.maps_entry.connect("key-press-event", self._on_map_entry_keypress) # FIXME: only giving two params to pack_start - # cf. EXPAND + # cf. EXPAND, NO_EXPAND self.maps_combo.pack_start(renderer_text, True) self.maps_combo.connect("changed", self._on_map_changed) self.maps_combo.connect("key-press-event", self._on_combo_keypress) - # TODO: consolidate - self.pack_start(self.filters_label, NO_EXPAND, NO_FILL, NO_PADDING) - self.pack_start(self.keyword_entry, NO_EXPAND, NO_FILL, NO_PADDING) - self.pack_start(self.maps_combo, NO_EXPAND, NO_FILL, NO_PADDING) - self.pack_start(button_grid, NO_EXPAND, NO_FILL, NO_PADDING) + for el in self.filters_label, self.keyword_entry, self.maps_combo, button_grid: + self.pack_start(el, NO_EXPAND, NO_FILL, NO_PADDING) def set_unique_maps(self, maps: list) -> None: if len(maps) < 1: diff --git a/dzgui/views/components/mod_panel.py b/dzgui/views/components/mod_panel.py index 185e542..109969a 100644 --- a/dzgui/views/components/mod_panel.py +++ b/dzgui/views/components/mod_panel.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from dzgui.const.enum import ModButton from dzgui.const.constants import NO_EXPAND, FILL, NO_PADDING from dzgui.util.strings import mod_panel -from dzgui.util.format import embolden +from dzgui.views.components.labels import BoldLabel import gi gi.require_version("Gtk", "3.0") @@ -27,9 +27,7 @@ class ModSelectionPanel(Gtk.Box): self.controller = controller - header = Gtk.Label() - text = embolden(mod_panel.header) - header.set_markup(text) + header = BoldLabel(mod_panel.header) self.main_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) buttons = ( diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index 879b8c7..fb72e42 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -7,8 +7,7 @@ from dzgui.views.components.buttonbox import ButtonBox from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.mod_panel import ModSelectionPanel from dzgui.views.components.icon import Icon -from dzgui.views.components.web_button import RefreshButton -# TODO: rename web_button +from dzgui.views.components.buttons import RefreshButton, KeysButton from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL, INPUT_KEYBOARD, NO_PADDING from dzgui.util import strings @@ -17,71 +16,31 @@ class RightPanel(Gtk.Box): def __init__(self, controller): super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL) + self.controller = controller + self.controller.register_widget("right_panel", self) + self.button_vbox = ButtonBox(controller) - - self.filters_vbox = FilterPanel(controller) self.sel_panel = ModSelectionPanel(controller) - self.controller.register_widget("right_panel", self) - - self.sel_panel.set_visible(False) - self.filters_vbox.set_visible(False) - - # TODO: more custom button classes - self.ping = Gtk.Button( - label=strings.ping_servers, - margin_top=10, - margin_start=80, - margin_end=80, - tooltip_text=strings.ping_tooltip, - ) - self.ping.connect("clicked", self._on_ping_clicked) - - # TODO: drop after adding context menu row - self.debug_toggle = Gtk.ToggleButton( - label=strings.debug_mode, - margin_top=10, - margin_start=80, - margin_end=80, - tooltip_text=strings.debug_tooltip, - ) - - # TODO: icon + # TODO: strings, move strings into buttons.py self.refresh_button = RefreshButton("Refresh") - self.refresh_button.set_margin_top(10) - self.refresh_button.set_margin_start(80) - self.refresh_button.set_margin_end(80) + # TODO: tooltip, strings + # TODO: update refresh button sensitivity on cooldown + self.refresh_button.set_tooltip_text("Refresh server data") self.refresh_button.connect("clicked", self._on_refresh_clicked) - #if controller.query_config(Preferences.DEBUG) == True: - # self.debug_toggle.set_active(True) - #self.debug_toggle.connect("toggled", self._on_debug_toggled) - - # TODO: make button class - i = Icon(INPUT_KEYBOARD) - i.set_margin_start(5) - self.keys = Gtk.Button( - label=strings.keys_button, - margin_start=80, - margin_end=80, - tooltip_text=strings.keys_tooltip, - image = i, - ) - self.keys.set_image_position(Gtk.PositionType.RIGHT) + self.keys = KeysButton(strings.keys_button) + self.keys.set_tooltip_text(strings.keys_tooltip) self.keys.connect("clicked", self._on_question_clicked) - for el in self.button_vbox, self.filters_vbox, self.refresh_button: + for el in self.button_vbox, self.keys, self.filters_vbox, self.refresh_button: self.pack_start(el, NO_EXPAND, FILL, NO_PADDING) self.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING) - self.pack_start(self.keys, NO_EXPAND, FILL, NO_PADDING) - - - def enable_ping_button(self, state: bool) -> None: - self.ping.set_visible(state) + # TODO: move to filter panel def reinit_maps(self, rows: list) -> None: self.controller.reinit_map_store() # TODO: communicate with controller @@ -93,38 +52,38 @@ class RightPanel(Gtk.Box): def _on_refresh_clicked(self, button: RefreshButton) -> None: self.controller.refresh_tree() - def _on_ping_clicked(self, button: Gtk.Button) -> None: - # TODO - block_signals() + #def _on_ping_clicked(self, button: Gtk.Button) -> None: + # # TODO + # block_signals() - def _update_pings(): - # TODO - rows = ModelManager.get_filtered() - with ThreadPoolExecutor(100) as executor: - futures = [ - executor.submit(Servers.ping, i, row) - for i, row in enumerate(rows) - ] - wait(futures) - for future in futures: - res = future.result() - path = Gtk.TreePath.new_from_indices([res.iteration]) - temp_model[path][9] = res.ping - ModelManager.ping_cache[res.addr] = res.ping - # TODO: drop/rewrite - self.AppNav.treeview.set_model(temp_model) - self.AppNav.treeview.wait_dialog.destroy() - self.AppNav.treeview.grab_focus() + # def _update_pings(): + # # TODO + # rows = ModelManager.get_filtered() + # with ThreadPoolExecutor(100) as executor: + # futures = [ + # executor.submit(Servers.ping, i, row) + # for i, row in enumerate(rows) + # ] + # wait(futures) + # for future in futures: + # res = future.result() + # path = Gtk.TreePath.new_from_indices([res.iteration]) + # temp_model[path][9] = res.ping + # ModelManager.ping_cache[res.addr] = res.ping + # # TODO: drop/rewrite + # self.AppNav.treeview.set_model(temp_model) + # self.AppNav.treeview.wait_dialog.destroy() + # self.AppNav.treeview.grab_focus() - # TODO: - unblock_signals() + # # TODO: + # unblock_signals() - temp_model = self.AppNav.treeview.get_model() - self.AppNav.treeview.set_model(None) - self.AppNav.treeview.wait_dialog = GenericDialog("Pinging servers", Popup.WAIT) - self.AppNav.treeview.wait_dialog.show_all() - thread = threading.Thread(target=_update_pings, args=()) - thread.start() + # temp_model = self.AppNav.treeview.get_model() + # self.AppNav.treeview.set_model(None) + # self.AppNav.treeview.wait_dialog = GenericDialog("Pinging servers", Popup.WAIT) + # self.AppNav.treeview.wait_dialog.show_all() + # thread = threading.Thread(target=_update_pings, args=()) + # thread.start() def _on_question_clicked(self, button: Gtk.Button) -> None: self.controller.open_keybindings() diff --git a/dzgui/views/dialogs/link_dialog.py b/dzgui/views/dialogs/link_dialog.py index ed98f59..b5db94f 100644 --- a/dzgui/views/dialogs/link_dialog.py +++ b/dzgui/views/dialogs/link_dialog.py @@ -5,7 +5,7 @@ from dzgui.const.enum import Popup, Preferences from dzgui.const.constants import NO_EXPAND, NO_FILL from dzgui.util.open_links import open_user_workshop from dzgui.views.dialogs.generic import GenericDialog -from dzgui.views.components.web_button import WebButton +from dzgui.views.components.buttons import WebButton import gi gi.require_version("Gtk", "3.0") @@ -41,8 +41,9 @@ class WorkshopLinkDialog(GenericDialog): def _on_button_clicked(self, button: Gtk.Button, uid: str) -> None: # TODO: currently checks user id on instantiation - client = self.controller.query_config(Preferences.CLIENT) - open_user_workshop(uid, client) + self.controller.open_user_workshop(uid) + #client = self.controller.query_config(Preferences.CLIENT) + #open_user_workshop(uid, client) def _on_dialog_response( self, dialog: Self, resp: Gtk.ResponseType diff --git a/dzgui/views/pages/devs.py b/dzgui/views/pages/devs.py index 40ff46b..80b01fe 100644 --- a/dzgui/views/pages/devs.py +++ b/dzgui/views/pages/devs.py @@ -3,8 +3,8 @@ from typing import Union, TYPE_CHECKING from dzgui.const.enum import NotebookPage from dzgui.util.css import add_class -from dzgui.util.format import embolden from dzgui.util.strings import developers +from dzgui.views.components.labels import BoldLabel import gi # noqa E402 gi.require_version("Gtk", "3.0") @@ -35,13 +35,9 @@ class Developers(Gtk.Box): back_button = Gtk.Button(label="Back", halign=Gtk.Align.START) back_button.connect("clicked", self._on_back_clicked) - paths_label = Gtk.Label() - text = embolden(developers.paths_label) - paths_label.set_markup(text) - prefs_label = Gtk.Label() - text = embolden(developers.prefs_label) - prefs_label.set_markup(text) + paths_label = BoldLabel(developers.paths_label) + prefs_label = BoldLabel(developers.prefs_label) paths_tree = self._make_tree(self.controller.prefs.paths) prefs_tree = self._make_tree(self.controller.prefs) diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index a8967e2..8d6fe55 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -8,7 +8,7 @@ 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.web_button import WebButton +from dzgui.views.components.buttons import WebButton from dzgui.views.dialogs.link_dialog import WorkshopLinkDialog from dzgui.const.enum import Preferences, Popup @@ -303,6 +303,7 @@ class Options(Gtk.Box): #call_on_thread(show_wait_dialog, cmd, wait_msg, "") def _on_client_changed(self, combo: Gtk.ComboBoxText) -> None: + # TODO: use two columns or constants here, not strings client = combo.get_active_text() match client: case "Steam": @@ -423,9 +424,10 @@ class Options(Gtk.Box): return box def populate_settings(self) -> None: + # TODO: controller.get_config() prefs = self.controller.get_prefs() if prefs.paths.config.is_file() is False: - # in case file got deleted locally + # NOTE: in case file got deleted locally self.controller.spawn_dialog(strings.config_not_found, Popup.QUIT) return diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index b24afff..a686165 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -180,6 +180,7 @@ class ServerTreeView(TreeView): case Gdk.KEY_r: self.refresh_player_count() case Gdk.KEY_f: + # TODO: register filter panel instead of mediating thru right panel self.controller.mediator.grid.right_panel.filters_vbox.keyword_entry.grab_focus() case Gdk.KEY_m: self.controller.mediator.grid.right_panel.filters_vbox.maps_entry.grab_focus() @@ -191,7 +192,7 @@ class ServerTreeView(TreeView): self.controller.toggle_check(event) def set_context_menu(self, items: ContextMenuGroup) -> None: - # TODO: if debug is on, add raw command + # TODO: if debug is on, add raw command copy to context menu for item in items.value: menu_item = EnumeratedMenuItem(item) menu_item.connect("activate", self._on_menu_click) @@ -199,6 +200,7 @@ class ServerTreeView(TreeView): self.menu.show_all() def _on_menu_click(self, item) -> None: + print(f"UNIMPLEMENTED: {item.enum}") pass def _on_server_button_release( From 8a1c5bea6ce50f04fd8b48e52a145d47033071df Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:29:15 +0900 Subject: [PATCH 34/49] fix: open user workshop --- dzgui/controllers/mc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index ec4b289..78b2e2d 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -41,7 +41,7 @@ from dzgui.controllers.model import ModelManager from dzgui.util.diag import write_diagnostic from dzgui.util import cooldown, strings from dzgui.util._json import read_json, write_json -from dzgui.util.open_links import open_workshop_page +from dzgui.util.open_links import open_workshop_page, open_user_workshop from dzgui.util.format import format_mods, format_player_count, pluralize from dzgui.util.redact import redact_log @@ -368,7 +368,7 @@ class Controller: 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_workshop_page(uid, client) + open_user_workshop(uid, client) def copy_log(self, paths: list[Gtk.TreePath]) -> str: if len(paths) < 1: From 3c82b71f76635cde38e984c962f9eedf2d044d4c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:32:32 +0900 Subject: [PATCH 35/49] feat: live input validation --- dzgui/controllers/model.py | 3 -- dzgui/util/css.py | 9 ++++- dzgui/views/base.py | 17 ++++----- dzgui/views/components/connect_panel.py | 47 +++++++++++++++++++++++-- dzgui/views/components/filter_panel.py | 12 ++----- dzgui/views/components/right_panel.py | 15 ++++---- dzgui/views/trees/tree_server_mods.py | 4 +-- dzgui/views/trees/tree_servers.py | 2 ++ 8 files changed, 71 insertions(+), 38 deletions(-) diff --git a/dzgui/controllers/model.py b/dzgui/controllers/model.py index 3372bf5..8dae7e4 100644 --- a/dzgui/controllers/model.py +++ b/dzgui/controllers/model.py @@ -151,11 +151,9 @@ class ModelManager: if prior_map == "All maps": rows = self.filter_map(filters) else: - AppNav.right_panel.ping.set_sensitive(True) rows = self.filter_toggle_on(filters, *args) case FilterMode.KEYWORD: - AppNav.right_panel.ping.set_sensitive(True) rows = self.filter_toggle_on(filters, *args) case FilterMode.TOGGLE_OFF: @@ -164,7 +162,6 @@ class ModelManager: rows = self.filtered case FilterMode.TOGGLE_ON: - AppNav.right_panel.ping.set_sensitive(True) rows = self.filter_toggle_on(filters, *args) if mode is not FilterMode.INITIAL: diff --git a/dzgui/util/css.py b/dzgui/util/css.py index 90411ad..3c18249 100644 --- a/dzgui/util/css.py +++ b/dzgui/util/css.py @@ -11,11 +11,18 @@ def add_class(widget: Gtk.Widget, label: str) -> None: context = widget.get_style_context() context.add_class(label) -# TODO: def remove_class() + +def remove_class(widget: Gtk.Widget, label: str) -> None: + context = widget.get_style_context() + context.remove_class(label) + def load_css() -> None: # TODO: consider storing this in a data file css = """ + .invalid-entry { + border-color: red; + } .frame { border: 0px; } diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 12d3301..5ef5a4c 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -80,14 +80,9 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) # return rows # #def process_tree_option(choice: RowType) -> None: -# context = AppNav.treeview.view -# command = choice -# cmd_string = command.dict["label"] -# logger.info(f"Parsing tree option '{command}' for the context '{context}'") -# # # server tables # if command == RowType.RESOLVE_IP: -# record = AppNav.treeview.get_record() +# record = treeview.get_record() # wait_msg = command.dict["wait_msg"] # show_wait_dialog = True # @@ -103,12 +98,12 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) # if command == RowType.QUICK_CONNECT: # record = MainController.query_config(Preferences.FAV_SRV) # if record == "": -# AppNav.window.spawn_dialog("No favorite server currently set", Popup.NOTIFY) +# spawn_dialog("No favorite server currently set", Popup.NOTIFY) # return # # record = str_to_record(record) # thread_new_with_dialog( -# AppNav.treeview.prepare_connection, +# treeview.prepare_connection, # parse_shell_output, # "Querying server", # command, @@ -131,7 +126,7 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) #def connect_by_ip(enum: RowType, response: str) -> None: # def _prep(response: str) -> None: # record = Servers.validate_ip(response) -# proc = AppNav.treeview.prepare_connection(record) +# proc = treeview.prepare_connection(record) # return proc # # thread_new_with_dialog( @@ -155,7 +150,7 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) # logger.critical(e) # # raise dialog # return -# # proc = AppNav.treeview.prepare_connection(record) +# # proc = treeview.prepare_connection(record) # # return proc # # #thread_new_with_dialog( @@ -172,7 +167,7 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) # if enum == RowType.CONN_BY_ID: # key = MainController.query_config(Preferences.BM) # if len(key) == 0: -# AppNav.window.spawn_dialog( +# spawn_dialog( # "No Battlemetrics API key is set; see Options", Popup.NOTIFY # ) # return diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index 97f132a..0ccdd1a 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -1,12 +1,14 @@ 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 from dzgui.views.components.buttons import WebButton from dzgui.views.components.labels import BoldLabel import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk, Gdk # noqa E402 if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -17,6 +19,7 @@ class ConnectPanel(Gtk.Frame): COLS = 1 ROWS = 1 + self.classname = "invalid-entry" self.controller = controller self.entry = Gtk.Entry( @@ -24,14 +27,16 @@ class ConnectPanel(Gtk.Frame): hexpand=True, tooltip_text=connect_panel.entry_tooltip ) + self.entry.connect("key-press-event", self._on_entry_keypress) + self.entry.connect("changed", self._on_text_changed) # TODO: get ip as well? user_fav = self.controller.get_favorite_label() server_name = user_fav if user_fav is not None else connect_panel.no_fav + self.fav_label = Gtk.Label(label=server_name, halign=Gtk.Align.START) scrollable_label = Gtk.ScrolledWindow() - label = Gtk.Label(label=server_name, halign=Gtk.Align.START) - scrollable_label.add(label) + scrollable_label.add(self.fav_label) self.fav_button = Gtk.Button(label=connect_panel.connect, tooltip_text=connect_panel.connect_tooltip @@ -70,3 +75,39 @@ class ConnectPanel(Gtk.Frame): self.grid.attach_next_to(el, sibling, pos, h_span, v_span) self.add(self.grid) + + def mark_valid(self) -> None: + self.conn_server.set_sensitive(True) + self.add_server.set_sensitive(True) + remove_class(self.entry, self.classname) + + def mark_invalid(self) -> None: + self.conn_server.set_sensitive(False) + self.add_server.set_sensitive(False) + add_class(self.entry, self.classname) + + def _on_text_changed(self, entry: Gtk.Entry) -> None: + text = entry.get_text() + if len(text) < 1: + self.conn_server.set_sensitive(False) + self.add_server.set_sensitive(False) + remove_class(entry, self.classname) + return + try: + validate_ip(text) + self.mark_valid() + except Exception as e: + if text.isdigit(): + self.mark_valid() + else: + self.mark_invalid() + + def set_fav_label(self, text: str) -> None: + # TODO: called by controller when changing fav + self.fav_label.set_text(text) + + def _on_entry_keypress(self, entry: Gtk.Entry, event: Gdk.EventKey) -> None: + if event.keyval == Gdk.KEY_Escape: + # NOTE: unselect text + entry.select_region(0, 0) + self.controller.grab_active_treeview() diff --git a/dzgui/views/components/filter_panel.py b/dzgui/views/components/filter_panel.py index 8afa0d1..ed6c1ef 100644 --- a/dzgui/views/components/filter_panel.py +++ b/dzgui/views/components/filter_panel.py @@ -108,9 +108,8 @@ class FilterPanel(Gtk.Box): def set_unique_maps(self, maps: list) -> None: if len(maps) < 1: return - # FIXME: clear typehints - u_maps = set([row[1] for row in maps]) # type: ignore - u_maps = sorted(u_maps) # type: ignore + u_maps = set([row[1] for row in maps]) + u_maps = sorted(u_maps) for m in u_maps: self.controller.append_map([m]) self.maps_hr.append(m) @@ -124,13 +123,6 @@ class FilterPanel(Gtk.Box): filters.append(k) return tuple(filters) - # used on personal/local server lists - def enable_all_filters(self) -> None: - for check in self.checks: - check.set_active(True) - for k in self.enabled_filters: - self.enabled_filters[k] = True - def reinit_panel(self) -> None: self.keyword_entry.set_text("") self.keyword_filter = "" diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index fb72e42..e5601f6 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -52,10 +52,9 @@ class RightPanel(Gtk.Box): def _on_refresh_clicked(self, button: RefreshButton) -> None: self.controller.refresh_tree() + # TODO: reference #def _on_ping_clicked(self, button: Gtk.Button) -> None: - # # TODO # block_signals() - # def _update_pings(): # # TODO # rows = ModelManager.get_filtered() @@ -71,17 +70,17 @@ class RightPanel(Gtk.Box): # temp_model[path][9] = res.ping # ModelManager.ping_cache[res.addr] = res.ping # # TODO: drop/rewrite - # self.AppNav.treeview.set_model(temp_model) - # self.AppNav.treeview.wait_dialog.destroy() - # self.AppNav.treeview.grab_focus() + # treeview.set_model(temp_model) + # treeview.wait_dialog.destroy() + # treeview.grab_focus() # # TODO: # unblock_signals() # temp_model = self.AppNav.treeview.get_model() - # self.AppNav.treeview.set_model(None) - # self.AppNav.treeview.wait_dialog = GenericDialog("Pinging servers", Popup.WAIT) - # self.AppNav.treeview.wait_dialog.show_all() + # treeview.set_model(None) + # treeview.wait_dialog = GenericDialog("Pinging servers", Popup.WAIT) + # treeview.wait_dialog.show_all() # thread = threading.Thread(target=_update_pings, args=()) # thread.start() diff --git a/dzgui/views/trees/tree_server_mods.py b/dzgui/views/trees/tree_server_mods.py index e84f0fc..df44134 100644 --- a/dzgui/views/trees/tree_server_mods.py +++ b/dzgui/views/trees/tree_server_mods.py @@ -76,14 +76,14 @@ class ModDialog(GenericDialog): # dialog.destroy() # # TODO: natively implemented # #if data.returncode == 1: - # # AppNav.window.spawn_dialog(strings.server_error, Popup.NOTIFY) + # # spawn_dialog(strings.server_error, Popup.NOTIFY) # # return # self.show_all() # self.set_markup(f"Modlist ({mod_count} mods)") # self.run() # self.destroy() - # record = AppNav.treeview.get_record() + # record = treeview.get_record() # if not record: # return diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index a686165..47edc6a 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -184,6 +184,8 @@ class ServerTreeView(TreeView): self.controller.mediator.grid.right_panel.filters_vbox.keyword_entry.grab_focus() case Gdk.KEY_m: self.controller.mediator.grid.right_panel.filters_vbox.maps_entry.grab_focus() + case Gdk.KEY_i: + self.controller.mediator.grid.conpan.entry.grab_focus() else: match event.keyval: case Gdk.KEY_l | Gdk.KEY_Right: From 86147db18ed2bfb3cb1611062e66a313ef057799 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:32:17 +0900 Subject: [PATCH 36/49] feat: add BoldLabel class --- dzgui/views/components/labels.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 dzgui/views/components/labels.py diff --git a/dzgui/views/components/labels.py b/dzgui/views/components/labels.py new file mode 100644 index 0000000..df63009 --- /dev/null +++ b/dzgui/views/components/labels.py @@ -0,0 +1,12 @@ +from dzgui.util.format import embolden + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk # noqa E402 + +class BoldLabel(Gtk.Label): + def __init__(self, text: str): + super().__init__() + + label = embolden(text) + self.set_markup(label) From 271ed76c3882490a1a8fe499ef9f717fd9621f27 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:33:35 +0900 Subject: [PATCH 37/49] feat: add Help class --- dzgui/views/pages/help.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 dzgui/views/pages/help.py diff --git a/dzgui/views/pages/help.py b/dzgui/views/pages/help.py new file mode 100644 index 0000000..9fa4aa9 --- /dev/null +++ b/dzgui/views/pages/help.py @@ -0,0 +1,21 @@ +from typing import TYPE_CHECKING +from dzgui.views.trees.tree_menu import MenuTreeView + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk # noqa E402 + +if TYPE_CHECKING: + from dzgui.controllers.mc import Controller + +class Help(Gtk.ScrolledWindow): + def __init__(self, controller: "Controller") -> None: + super().__init__() + self.treeview = MenuTreeView(controller) + self.add(self.treeview) + + self.controller = controller + self.controller.register_widget("menu", self.treeview) + + def get_treeview(self) -> MenuTreeView: + return self.treeview From 3b7b131cf5e3a775c3211800a643d3cb5aa48f72 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:34:02 +0900 Subject: [PATCH 38/49] feat: add Mods class --- dzgui/views/pages/mods.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 dzgui/views/pages/mods.py diff --git a/dzgui/views/pages/mods.py b/dzgui/views/pages/mods.py new file mode 100644 index 0000000..20e381f --- /dev/null +++ b/dzgui/views/pages/mods.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING +from dzgui.views.trees.tree_mods import ModTreeView + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk # noqa + +if TYPE_CHECKING: + from dzgui.controllers.mc import Controller + +class Mods(Gtk.ScrolledWindow): + def __init__(self, controller: "Controller") -> None: + super().__init__() + + self.controller = controller + self.tree = ModTreeView(controller) + + self.add(self.tree) + self.controller.register_widget("modtreeview", self.tree) From 058cbc39b133e34c3e13efc3c8b31f6313d4644d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:34:25 +0900 Subject: [PATCH 39/49] feat: add Servers class --- dzgui/views/pages/servers.py | 157 +++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 dzgui/views/pages/servers.py diff --git a/dzgui/views/pages/servers.py b/dzgui/views/pages/servers.py new file mode 100644 index 0000000..9e8fc1a --- /dev/null +++ b/dzgui/views/pages/servers.py @@ -0,0 +1,157 @@ +import logging + +from typing import Self, TYPE_CHECKING + +from dzgui.const.enum import ContextMenuGroup +from dzgui.views.trees.tree_servers import ServerTreeView +from dzgui.util.strings import server_labels + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gdk, GLib # noqa E402 + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from dzgui.controllers.mc import Controller + +class ServerNotebook(Gtk.ScrolledWindow): + def __init__(self, controller: "Controller"): + super().__init__() + + self.tab_cache = "" + self.controller = controller + self.controller.register_widget("servers", self) + self.notebook = Gtk.Notebook(show_tabs=True) + + self.browser = ServerTreeView(controller) + self.saved = ServerTreeView(controller) + self.recent = ServerTreeView(controller) + self.lan = ServerTreeView(controller) + + self.lan.set_query_func(self.query_test) + # TODO: set context menu on init + self.browser.set_context_menu(ContextMenuGroup.SERVER_BROWSER) + self.saved.set_context_menu(ContextMenuGroup.SAVED) + self.recent.set_context_menu(ContextMenuGroup.RECENT) + self.lan.set_context_menu(ContextMenuGroup.SCAN_LAN) + + # TODO: set model manager for each tab on init + # TODO: add set_ and get_model_manager() methods + + tabs = [ + (self.browser, server_labels.browser, self.controller.get_server_store), + (self.saved, server_labels.saved, self.controller.get_saved_store), + (self.recent, server_labels.recent, self.controller.get_recent_store), + (self.lan, server_labels.lan, self.controller.get_lan_store), + ] + + for tree, label, func in tabs: + store = func() + if label == "LAN": + pass + #store.append(["BAR", "a", "a", "a", 1, 1, 1, "1:1", 0, 0, "a", False]) + else: + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + store.append(["BAR", "a", "a", "a", 0, 0, 0, "1:1", 0, 0, "a", False]) + tree.loaded = True + # TODO: set model on init of servertreeview + tree.set_model(store) + + scrolled = Gtk.ScrolledWindow() + scrolled.add(tree) + self.notebook.append_page(scrolled, Gtk.Label(label=label)) + + self.add(self.notebook) + self.notebook.connect_after("switch-page", self._on_page_changed) + self.connect("key-press-event", self._on_keypress) + + def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None: + + match event.keyval: + case Gdk.KEY_n: + self.notebook.next_page() + case Gdk.KEY_p: + self.notebook.prev_page() + case _: + return + self.get_active_treeview().grab_focus() + + + def _on_page_changed(self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int) -> None: + # TODO: abstract + # TODO: load on first run + label = self.notebook.get_tab_label_text(child) + if label is None: + return + # TODO: strings + text = label.strip("*") + self.notebook.set_tab_label_text(child, text) + + # TODO: strings + string = f"Servers > {text}" + self.controller.set_crumbs(string) + self.tab_cache = string + + child.grab_focus() + + # FIXME: doesn't fire on first run + self.controller.update_server_status() + self.controller.populate_model() + + # TODO: put in controller + def query_test(self) -> None: + data = (["BAR", "a", "a", "a", 1, 1, 1, "1:1", 0, 0, "a", False]) + return data + + def get_cached_label(self) -> str: + return self.tab_cache + + def get_active_treeview(self) -> ServerTreeView: + index = self.notebook.get_current_page() + scrollable = self.notebook.get_nth_page(index) + treeview = scrollable.get_children()[0] + return treeview + + def add_notification(self) -> None: + saved = self.notebook.get_nth_page(1) + if saved is None: + return + text = self.notebook.get_tab_label_text(saved) + if text is None: + return + # TODO: strings + if "*" in text: + return + text += "*" + self.notebook.set_tab_label_text(saved, text) + + def update_tab_widths(self, col: Gtk.TreeViewColumn) -> None: + # TODO: may cause pixel offsets when application is maximized + width = col.get_width() + title = col.get_title() + for tab in (self.browser, self.saved, self.recent, self.lan): + if tab == self.get_active_treeview(): + continue + for col in tab.get_columns(): + if col.get_title() == title: + self.controller.suppress_signal(tab, col, "_on_col_width_changed", True) + col.set_fixed_width(width) + self.controller.suppress_signal(tab, col, "_on_col_width_changed", False) + + def get_tabs(self) -> tuple: + return (self.browser, self.saved, self.recent, self.lan) From 42e9cf6378cfb03a9baf6ddc71ccf58ab337a45c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 7 Jan 2026 23:35:22 +0900 Subject: [PATCH 40/49] feat: add Statusbar class --- dzgui/views/components/statusbar.py | 64 +++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 dzgui/views/components/statusbar.py diff --git a/dzgui/views/components/statusbar.py b/dzgui/views/components/statusbar.py new file mode 100644 index 0000000..4e817ca --- /dev/null +++ b/dzgui/views/components/statusbar.py @@ -0,0 +1,64 @@ +from typing import TYPE_CHECKING + +from dzgui.const.enum import RowType, Preferences +from dzgui.util import strings + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk # noqa E402 + +if TYPE_CHECKING: + from dzgui.controllers.mc import Controller + +class Statusbar(Gtk.Statusbar): + def __init__(self, controller: "Controller") -> None: + super().__init__() + + self.controller = controller + self.controller.register_widget("statusbar", self) + + help_text = strings.statusbar_helptext + self.set_text(help_text) + + version = self.controller.get_prefs().version + self.status_right_label = Gtk.Label(label=version) + self.add(self.status_right_label) + + self.players = "" + + def get_text(self) -> str: + area = self.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) + + def refresh(self, row: "RowType") -> None: + if row is None: + formatted = "" + else: + formatted = self.format_metadata(row) + self.set_text(formatted) + + def append_distance(self, dist: str) -> None: + # TODO: process strings in controller + if dist == strings.unknown: + dist = f"| Distance: {dist}" + else: + dist = f"| Distance: {dist}" + self.set_text(self.players + dist) + + def format_metadata(self, row: "RowType") -> str: + prefix = row.dict["tooltip"] + + if row == RowType.QUICK_CONNECT or row == RowType.CHNG_FAV: + label = self.controller.query_config(Preferences.FAV_LBL) + if len(label) < 1: + label = "unset" + return f"{prefix} ({label})" + else: + return prefix From f83eca7c1841e057e01e7b962e3799c1cc4bdb41 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:15:27 +0900 Subject: [PATCH 41/49] fix: ensure log path exists --- dzgui/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dzgui/main.py b/dzgui/main.py index d91c187..1ba0c1d 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -64,6 +64,10 @@ def main() -> None: xdg_paths = get_xdg_paths() XDG = parse_filepaths(xdg_paths) + # NOTE: required for logs to be written + if XDG.debug.is_file() is False: + XDG.debug.parent.mkdir(parents=True, exist_ok=True) + if has_new_config(XDG.config) is False: migrate_legacy_conf(XDG.config) migrate_cols_file(XDG.columns) From 7fbc75bea97329ced58586c2af4d8dafd9c8a515 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 9 Jan 2026 07:01:25 +0900 Subject: [PATCH 42/49] chore: linting errors --- dzgui/views/components/connect_panel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index 0ccdd1a..7019ea0 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -87,6 +87,7 @@ class ConnectPanel(Gtk.Frame): add_class(self.entry, self.classname) def _on_text_changed(self, entry: Gtk.Entry) -> None: + # TODO: on submission, strip whitespace and newlines text = entry.get_text() if len(text) < 1: self.conn_server.set_sensitive(False) @@ -96,7 +97,7 @@ class ConnectPanel(Gtk.Frame): try: validate_ip(text) self.mark_valid() - except Exception as e: + except Exception: if text.isdigit(): self.mark_valid() else: From 3ae06f0bfb749b624e06d6dba7dac466fe0ee4b4 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 9 Jan 2026 07:01:55 +0900 Subject: [PATCH 43/49] feat: add ip field binding --- dzgui/util/strings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 1529a79..23ecc9d 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -128,6 +128,7 @@ servers = { "Ctrl-f": "jump to keyword search field", "Ctrl-m": "jump to maps field", "Ctrl-d": "toggle dry run (debug) mode", + "Ctrl-i": "jump to IP insert field", "ESC": "return to table", "1-9": "toggle filter 1-9 on/off", "0": "toggle filter 10", From 4e0cd02afa1a5bc36c25425fb1270932e1f24182 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 9 Jan 2026 07:02:19 +0900 Subject: [PATCH 44/49] chore: add comments --- dzgui/main.py | 2 ++ dzgui/views/base.py | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/dzgui/main.py b/dzgui/main.py index d91c187..86a6ee1 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -63,6 +63,7 @@ def main() -> None: # NOTE: consider aborting this check if steam deck xdg_paths = get_xdg_paths() XDG = parse_filepaths(xdg_paths) + # TODO: ensure state path is created if has_new_config(XDG.config) is False: migrate_legacy_conf(XDG.config) @@ -93,6 +94,7 @@ def main() -> None: if _is_steam_deck is False: # TODO: sudo escalation dialog count = get_map_count() + # TODO: move into module if has_steam_client() is False: EarlyAlertDialog(init.requires_steam) sys.exit(1) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 5ef5a4c..293617d 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -237,7 +237,7 @@ class AppHeaderBar(Gtk.HeaderBar): # TODO: deprecated -class ScrollableNote(ScrollableMixin, Gtk.Box): +class ScrollableNote(ScrollableMixin, Gtk.Box): # type: ignore def __init__(self, content_box: Gtk.Box, back_button: bool = False): super().__init__(orientation=Gtk.Orientation.VERTICAL) @@ -263,7 +263,7 @@ class ScrollableNote(ScrollableMixin, Gtk.Box): pass -class Notebook(ScrollableMixin, Gtk.Notebook): +class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore def __init__(self) -> None: super().__init__(show_tabs=False) @@ -275,7 +275,6 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.help = Help(MainController) self.clog = Changelog(MainController) - # TODO: make all treeviews internally scrollable in base class self.keys = Keybindings() self.settings = Options(MainController) @@ -283,13 +282,13 @@ class Notebook(ScrollableMixin, Gtk.Notebook): self.servers = ServerNotebook(MainController) self.mods = Mods(MainController) - # TODO: change this class to scrolledwindow + # TODO: make all treeviews internally scrollable in base class self.scroll_log = Gtk.ScrolledWindow() self.scroll_log.set_hexpand(True) self.log_table = LogTreeView(MainController) self.scroll_log.add(self.log_table) - # TODO: change this class to scrolledwindow + # TODO: make all treeviews internally scrollable in base class self.thanks = ScrollableNote(Thanks(), back_button=False) # TODO: change this class to scrolledwindow developers = Developers(MainController) From 31d289048f0269f3148d6fe0c676208e9f4fd85b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:58:41 +0900 Subject: [PATCH 45/49] fix: fetch ipdb if missing --- dzgui/config/ipdb.py | 10 +++++----- dzgui/main.py | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/dzgui/config/ipdb.py b/dzgui/config/ipdb.py index e72f014..4d64264 100644 --- a/dzgui/config/ipdb.py +++ b/dzgui/config/ipdb.py @@ -27,11 +27,11 @@ def get_ipdb(ips_path: Path) -> None: date = find_date(url) month_file = ips_path.parent / ".month" - if month_file.exists() is True: - old_date = month_file.read_text().rstrip("\n") - if old_date == date: - logger.info(f"IP DB date matches: {date}") - return + if ips_path.exists() and month_file.exists(): + old_date = month_file.read_text().rstrip("\n") + if old_date == date: + logger.info(f"IP DB date matches: {date}") + return # TODO: log additional output logger.info(f"Fetching IPDB for {date} from {url}") diff --git a/dzgui/main.py b/dzgui/main.py index cbf691b..b2c5fe0 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -19,7 +19,6 @@ from dzgui.init.migrate import ( migrate_cols_file, migrate_legacy_conf, copy_state_files, - copy_ipdb, ) from dzgui.init.prefix import get_version from dzgui.init.prereqs import has_steam_client @@ -73,7 +72,6 @@ def main() -> None: migrate_legacy_conf(XDG.config) migrate_cols_file(XDG.columns) copy_state_files(xdg_paths["XDG_STATE_HOME"]) - copy_ipdb(XDG.ips) _format = "%(asctime)s␞%(levelname)s␞%(filename)s::%(funcName)s::%(lineno)s␞%(message)s" logging.basicConfig(filename=XDG.debug, format=_format, level=logging.DEBUG) From 922ff7b3b2a14de05b99f4f4abf523f0b51f8bc6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:09:55 +0900 Subject: [PATCH 46/49] chore: abstraction --- dzgui/main.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/dzgui/main.py b/dzgui/main.py index b2c5fe0..87b8410 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -42,7 +42,23 @@ parser.add_argument("-u", "--uninstall", action="store_true", help=flags.uninsta parser.add_argument("-d", "--developers", action="store_true", help=flags.developers) args = parser.parse_args() +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + # TODO: profile load time +def make_parents(path: "Path") -> None: + path.parent.mkdir(parents=True, exist_ok=True) + +def uninstall() -> None: + # TODO: uninstall data files (-u) + # -u removes state, log, freedesktop + # XDG_SHARE_HOME/dzgui + # XDG_STATE_HOME/dzgui + # XDG_DATA_HOME/dzgui + pass + def main() -> None: lock = lock_acquire() # noqa @@ -50,11 +66,9 @@ def main() -> None: print(get_version()) sys.exit(0) - # TODO: uninstall data files (-u) - # -u removes state, log, freedesktop - # XDG_SHARE_HOME/dzgui - # XDG_STATE_HOME/dzgui - # XDG_DATA_HOME/dzgui + if args.uninstall is True: + uninstall() + sys.exit(0) version = get_version() set_locale() @@ -62,11 +76,11 @@ def main() -> None: # NOTE: consider aborting this check if steam deck xdg_paths = get_xdg_paths() XDG = parse_filepaths(xdg_paths) - # TODO: ensure state path is created - # NOTE: required for logs to be written + if XDG.resolution.parent.is_dir() is False: + make_parents(XDG.resolution) if XDG.debug.is_file() is False: - XDG.debug.parent.mkdir(parents=True, exist_ok=True) + make_parents(XDG.debug) if has_new_config(XDG.config) is False: migrate_legacy_conf(XDG.config) @@ -78,17 +92,13 @@ def main() -> None: with open(XDG.debug, "w") as f: f.truncate(0) - _is_steam_deck = is_steam_deck() - _is_game_mode = False - if _is_steam_deck: - _is_game_mode = is_game_mode() + _is_game_mode = is_game_mode() if _is_steam_deck else False if _is_game_mode: # NOTE: this may no longer be necessary on newer versions of SteamOS del os.environ["GTK_IM_MODULE"] # TODO: test spamming timeout - allow = False allow = allow_updates(ALLOW_UPDATES) if allow is True: check_updates(version) From 91425cbcaa97441880d844b18987a453f0ded427 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:08:59 +0900 Subject: [PATCH 47/49] feat: pack pages in ScrolledWindow --- dzgui/views/base.py | 46 ++++------------------------------ dzgui/views/pages/changelog.py | 2 +- dzgui/views/pages/devs.py | 24 ++++++++---------- dzgui/views/pages/log.py | 21 ++++++++++++++++ dzgui/views/pages/thanks.py | 28 ++++++++++----------- dzgui/views/trees/tree_base.py | 2 +- dzgui/views/trees/tree_log.py | 2 +- 7 files changed, 54 insertions(+), 71 deletions(-) create mode 100644 dzgui/views/pages/log.py diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 293617d..a1b5a13 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -33,6 +33,7 @@ from dzgui.views.components.connect_panel import ConnectPanel 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 @@ -207,7 +208,6 @@ class OuterWindow(Gtk.Window): self.set_titlebar(self.hb) self.connect("delete-event", self._on_delete_event) - #self.connect("key-press-event", self._on_keypress) self.grid = Grid() self.add(self.grid) @@ -236,33 +236,6 @@ class AppHeaderBar(Gtk.HeaderBar): self.set_show_close_button(True) -# TODO: deprecated -class ScrollableNote(ScrollableMixin, Gtk.Box): # type: ignore - def __init__(self, content_box: Gtk.Box, back_button: bool = False): - super().__init__(orientation=Gtk.Orientation.VERTICAL) - - self.scrollable = Gtk.ScrolledWindow() - self.scrollable.set_vexpand(True) - - self.back_button = Gtk.Button( - label="Back", hexpand=True, halign=Gtk.Align.CENTER - ) - - self.gutter = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, valign=Gtk.Align.END - ) - if back_button: - self.gutter.add(self.back_button) - self.back_button.connect("clicked", self._on_back_clicked) - - self.scrollable.add(content_box) - self.add(self.scrollable) - self.add(self.gutter) - - def _on_back_clicked(self, button: Gtk.Button) -> None: - pass - - class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore def __init__(self) -> None: super().__init__(show_tabs=False) @@ -278,21 +251,12 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore self.keys = Keybindings() self.settings = Options(MainController) - # NOTE: server, mod, and log tables should have hexpand property set to True self.servers = ServerNotebook(MainController) self.mods = Mods(MainController) - # TODO: make all treeviews internally scrollable in base class - self.scroll_log = Gtk.ScrolledWindow() - self.scroll_log.set_hexpand(True) - self.log_table = LogTreeView(MainController) - self.scroll_log.add(self.log_table) - - # TODO: make all treeviews internally scrollable in base class - self.thanks = ScrollableNote(Thanks(), back_button=False) - # TODO: change this class to scrolledwindow - developers = Developers(MainController) - self.developers = ScrollableNote(developers) + self.thanks = Thanks() + self.log = Log(MainController) + self.developers = Developers(MainController) self.pages = { self.help: NotebookPage.HELP, @@ -301,7 +265,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore self.settings: NotebookPage.OPTIONS, self.servers: NotebookPage.SERVERS, self.mods: NotebookPage.MODS, - self.scroll_log: NotebookPage.LOG, + self.log: NotebookPage.LOG, self.thanks: NotebookPage.THANKS, self.developers: NotebookPage.DEVELOPERS, } diff --git a/dzgui/views/pages/changelog.py b/dzgui/views/pages/changelog.py index d145a50..4ba2437 100644 --- a/dzgui/views/pages/changelog.py +++ b/dzgui/views/pages/changelog.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from dzgui.controllers.mc import Controller -class Changelog(ScrollableMixin, Gtk.ScrolledWindow): +class Changelog(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore def __init__(self, controller: "Controller"): super().__init__(propagate_natural_width=False) diff --git a/dzgui/views/pages/devs.py b/dzgui/views/pages/devs.py index 80b01fe..cd4a1ea 100644 --- a/dzgui/views/pages/devs.py +++ b/dzgui/views/pages/devs.py @@ -15,19 +15,21 @@ if TYPE_CHECKING: from dzgui.config.userprefs import UserPrefs from dzgui.config.xdg import Xdg -class Developers(Gtk.Box): +class Developers(Gtk.ScrolledWindow): """ Shows TreeViews displaying contents of parsed XDG paths and user preferences """ def __init__(self, controller: "Controller") -> None: super().__init__( - orientation=Gtk.Orientation.VERTICAL, - margin_start=10, - margin_end=10, ) self.controller = controller + self.box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + margin_start=10, + margin_end=10 + ) heading = Gtk.Label(label=developers.header) heading.set_halign(Gtk.Align.CENTER) @@ -47,17 +49,13 @@ class Developers(Gtk.Box): paths_tree.set_cursor(path) prefs_tree.set_cursor(path) - for el in [ - paths_label, - paths_tree, - prefs_label, - prefs_tree - ]: + for el in [paths_label, paths_tree, prefs_label, prefs_tree]: trees_box.add(el) - self.add(heading) - self.add(back_button) - self.add(trees_box) + for el in [heading, back_button, trees_box]: + self.box.add(el) + + self.add(self.box) def _on_back_clicked(self, button: Gtk.Button) -> None: self.controller.open_page(NotebookPage.OPTIONS) diff --git a/dzgui/views/pages/log.py b/dzgui/views/pages/log.py new file mode 100644 index 0000000..057ab23 --- /dev/null +++ b/dzgui/views/pages/log.py @@ -0,0 +1,21 @@ +from typing import TYPE_CHECKING +from dzgui.views.trees.tree_log import LogTreeView + +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk # noqa E402 + +if TYPE_CHECKING: + from dzgui.controllers.mc import Controller + +class Log(Gtk.ScrolledWindow): + def __init__(self, controller: "Controller") -> None: + super().__init__() + self.treeview = LogTreeView(controller) + self.add(self.treeview) + + self.controller = controller + self.controller.register_widget("logtreeview", self.treeview) + + def get_treeview(self) -> LogTreeView: + return self.treeview diff --git a/dzgui/views/pages/thanks.py b/dzgui/views/pages/thanks.py index 130cf42..e59b807 100644 --- a/dzgui/views/pages/thanks.py +++ b/dzgui/views/pages/thanks.py @@ -1,34 +1,34 @@ from dzgui.util.format import format_pango from dzgui.util.strings import thanks +from dzgui.views.mixins.scrollable_mixin import ScrollableMixin import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 -""" -Special thanks page recognizing contributors to the project in alpha-order -""" +# TODO: wrap and center long values -# TODO: make scrollable, test long values/wrapping - -class Thanks(Gtk.Box): +class Thanks(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore + """ + Special thanks page recognizing contributors to the project in alpha-order + """ def __init__(self) -> None: - super().__init__( - orientation=Gtk.Orientation.VERTICAL, - spacing=10 - ) - + super().__init__() + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) label = format_pango(thanks.header) header = Gtk.Label() header.set_markup(label) - self.add(header) description = Gtk.Label(label=thanks.description, justify=Gtk.Justification.CENTER) - self.add(description) users = [f"- {user}" for user in sorted(thanks.users, key=str.lower)] pretty_users = "\n".join(users) body = Gtk.Label() body.set_markup(pretty_users) - self.add(body) + + for el in header, description, body: + self.box.add(el) + self.add(self.box) + + self.connect("key-press-event", self._on_keypress) diff --git a/dzgui/views/trees/tree_base.py b/dzgui/views/trees/tree_base.py index 1e8c9ce..2886990 100644 --- a/dzgui/views/trees/tree_base.py +++ b/dzgui/views/trees/tree_base.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from dzgui.controllers.mc import Controller from dzgui.const.enum import ContextMenuGroup -class TreeView(CursorMixin, Gtk.TreeView): +class TreeView(CursorMixin, Gtk.TreeView): # type: ignore def __init__(self, controller: "Controller", menu: "ContextMenuGroup" = None) -> None: super().__init__( enable_search=False, diff --git a/dzgui/views/trees/tree_log.py b/dzgui/views/trees/tree_log.py index 2d891f3..535ae97 100644 --- a/dzgui/views/trees/tree_log.py +++ b/dzgui/views/trees/tree_log.py @@ -24,8 +24,8 @@ class LogTreeView(ContextMixin, TreeView): self.controller.register_widget("logtreeview", self) # TODO: maybe put this in init - self.set_fixed_height_mode(True) self.set_headers_visible(True) + self.set_fixed_height_mode(True) self.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) model = self.controller.get_log_store() From 12d367fa7edbfed2df576317d89365ae040be0ce Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:03:22 +0900 Subject: [PATCH 48/49] feat: button cooldown --- dzgui/controllers/mc.py | 3 +++ dzgui/main.py | 1 + dzgui/util/strings.py | 3 +++ dzgui/views/components/buttons.py | 15 ++++++------ dzgui/views/components/right_panel.py | 34 ++++++++++++++++++++------- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 78b2e2d..50495c3 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -292,6 +292,9 @@ class Controller: def start_cooldown(self) -> None: self.cooldown = cooldown.get_time() + def get_cooldown(self) -> None: + return self.cooldown + def manage_cooldown(self) -> bool: if cooldown.is_elapsed(self.cooldown): self.cooldown = cooldown.get_time() diff --git a/dzgui/main.py b/dzgui/main.py index 87b8410..a01a86c 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -79,6 +79,7 @@ def main() -> None: if XDG.resolution.parent.is_dir() is False: make_parents(XDG.resolution) + # TODO: test if XDG.debug.is_file() is False: make_parents(XDG.debug) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 23ecc9d..037b8e6 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -539,3 +539,6 @@ connect_panel = ConnectPanel( ) distance_suffix = "Distance: calculating..." + +refresh = "Refresh" +refresh_tooltip = "Refresh server data" diff --git a/dzgui/views/components/buttons.py b/dzgui/views/components/buttons.py index 62f56c4..2bcbe7e 100644 --- a/dzgui/views/components/buttons.py +++ b/dzgui/views/components/buttons.py @@ -1,3 +1,4 @@ +from dzgui.util.strings import refresh from dzgui.views.components.icon import Icon from dzgui.const.constants import REFRESH_ICON, WEB_BROWSER, INPUT_KEYBOARD @@ -7,35 +8,33 @@ from gi.repository import Gtk # noqa E402 class IconButton(Gtk.Button): - def __init__(self, icon: str, margin: int = 0): + def __init__(self, icon: str, margin: int = 0) -> None: super().__init__() i = Icon(icon, l_margin=margin) self.set_image(i) self.set_image_position(Gtk.PositionType.RIGHT) - class IconTextButton(IconButton): - def __init__(self, icon: str, label: str): + def __init__(self, icon: str, label: str) -> None: super().__init__(icon, margin=5) self.set_label(label) class WebButton(IconTextButton): - def __init__(self, label: str): + def __init__(self, label: str) -> None: super().__init__(icon=WEB_BROWSER, label=label) pass class RefreshButton(IconTextButton): - def __init__(self, label: str): - super().__init__(icon=REFRESH_ICON, label=label) + def __init__(self) -> None: + super().__init__(icon=REFRESH_ICON, label=refresh) self.set_margin_top(10) self.set_margin_start(80) self.set_margin_end(80) - class KeysButton(IconTextButton): - def __init__(self, label: str): + def __init__(self, label: str) -> None: super().__init__(icon=INPUT_KEYBOARD, label=label) self.set_margin_top(10) self.set_margin_start(80) diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index e5601f6..4f81e7e 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -1,6 +1,8 @@ +from typing import Literal, TYPE_CHECKING + import gi # noqa E402 gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, Gdk +from gi.repository import Gtk, Gdk, GLib from dzgui.const.enum import Preferences from dzgui.views.components.buttonbox import ButtonBox @@ -9,13 +11,17 @@ from dzgui.views.components.mod_panel import ModSelectionPanel from dzgui.views.components.icon import Icon from dzgui.views.components.buttons import RefreshButton, KeysButton from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL, INPUT_KEYBOARD, NO_PADDING -from dzgui.util import strings +from dzgui.util.strings import refresh_tooltip, refresh, keys_button, keys_tooltip + +if TYPE_CHECKING: + from GLib import SOURCE_REMOVE # TODO: refactor depends on ServerTreeView class RightPanel(Gtk.Box): def __init__(self, controller): super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL) + self.time = 30 self.controller = controller self.controller.register_widget("right_panel", self) @@ -24,15 +30,12 @@ class RightPanel(Gtk.Box): self.filters_vbox = FilterPanel(controller) self.sel_panel = ModSelectionPanel(controller) - # TODO: strings, move strings into buttons.py - self.refresh_button = RefreshButton("Refresh") - # TODO: tooltip, strings - # TODO: update refresh button sensitivity on cooldown - self.refresh_button.set_tooltip_text("Refresh server data") + self.refresh_button = RefreshButton() + self.refresh_button.set_tooltip_text(refresh_tooltip) self.refresh_button.connect("clicked", self._on_refresh_clicked) - self.keys = KeysButton(strings.keys_button) - self.keys.set_tooltip_text(strings.keys_tooltip) + self.keys = KeysButton(keys_button) + self.keys.set_tooltip_text(keys_tooltip) self.keys.connect("clicked", self._on_question_clicked) for el in self.button_vbox, self.keys, self.filters_vbox, self.refresh_button: @@ -49,7 +52,20 @@ class RightPanel(Gtk.Box): self.selected = "All maps" self.filters_vbox.set_unique_maps(rows) + def decrement(self) -> "SOURCE_REMOVE" | Literal[True]: + self.time -= 1 + if self.time == 0: + self.time = 30 + self.refresh_button.set_label(refresh) + self.refresh_button.set_sensitive(True) + return GLib.SOURCE_REMOVE + self.refresh_button.set_label(f"{refresh} ({str(self.time)})") + return True + def _on_refresh_clicked(self, button: RefreshButton) -> None: + self.refresh_button.set_sensitive(False) + self.refresh_button.set_label(f"{refresh} ({str(self.time)})") + GLib.timeout_add_seconds(1, self.decrement) self.controller.refresh_tree() # TODO: reference From 786232cb4a73e7b8a59a78b7df9865d5ad605ddf Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:10:37 +0900 Subject: [PATCH 49/49] chore: update notes --- CHANGELOG.md | 2 +- dzgui/controllers/mc.py | 1 + dzgui/data/CHANGELOG.md | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc386a..52eec94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Open filepicker when generating system log - Developers page (and -d flag) - Redact API key in log table +- Integrate add/connect widgets into main menu ## Changed - Reduce padding on keys button @@ -23,4 +24,3 @@ ## Unreleased - Setup wizard -- Integrate add/connect widgets into main menu diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 50495c3..c3380eb 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -635,6 +635,7 @@ class Controller: treeview = self.mediator.notebook.servers.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() if func is not None: model = treeview.get_model() diff --git a/dzgui/data/CHANGELOG.md b/dzgui/data/CHANGELOG.md index 2fc386a..52eec94 100644 --- a/dzgui/data/CHANGELOG.md +++ b/dzgui/data/CHANGELOG.md @@ -14,6 +14,7 @@ - Open filepicker when generating system log - Developers page (and -d flag) - Redact API key in log table +- Integrate add/connect widgets into main menu ## Changed - Reduce padding on keys button @@ -23,4 +24,3 @@ ## Unreleased - Setup wizard -- Integrate add/connect widgets into main menu