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/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/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/const/constants.py b/dzgui/const/constants.py
index a004505..c3b586f 100644
--- a/dzgui/const/constants.py
+++ b/dzgui/const/constants.py
@@ -34,10 +34,12 @@ 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"
+NO_PADDING = 0
NO_EXPAND = False
NO_FILL = False
EXPAND = True
diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py
index 4cc0ea1..aa0dc9c 100644
--- a/dzgui/const/enum.py
+++ b/dzgui/const/enum.py
@@ -77,18 +77,15 @@ 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,}
- MAIN = {"crumbs": strings.crumbs.main,}
- 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,}
- 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):
@@ -300,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}
@@ -311,6 +309,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,7 +323,9 @@ class ContextMenuGroup(Enum):
ContextMenu.COPY_LOG_CLIPBOARD,
)
SERVER_BROWSER = (
+ ContextMenu.CONNECT,
ContextMenu.ADD_SERVER,
+ ContextMenu.ADD_FAV,
ContextMenu.COPY_NAME,
ContextMenu.COPY_CLIPBOARD,
ContextMenu.ADD_NOTE,
@@ -333,6 +334,8 @@ class ContextMenuGroup(Enum):
ContextMenu.REFRESH_PLAYERS,
)
SCAN_LAN = (
+ ContextMenu.CONNECT,
+ ContextMenu.ADD_FAV,
ContextMenu.COPY_NAME,
ContextMenu.COPY_CLIPBOARD,
ContextMenu.ADD_NOTE,
@@ -341,6 +344,8 @@ class ContextMenuGroup(Enum):
ContextMenu.REFRESH_PLAYERS,
)
SAVED = (
+ ContextMenu.CONNECT,
+ ContextMenu.ADD_FAV,
ContextMenu.REMOVE_SERVER,
ContextMenu.COPY_NAME,
ContextMenu.COPY_CLIPBOARD,
@@ -350,7 +355,9 @@ class ContextMenuGroup(Enum):
ContextMenu.REFRESH_PLAYERS,
)
RECENT = (
+ ContextMenu.CONNECT,
ContextMenu.ADD_SERVER,
+ ContextMenu.ADD_FAV,
ContextMenu.REMOVE_HISTORY,
ContextMenu.COPY_NAME,
ContextMenu.COPY_CLIPBOARD,
@@ -360,117 +367,6 @@ class ContextMenuGroup(Enum):
ContextMenu.REFRESH_PLAYERS,
)
-
-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,
- RowType.DUMP_LOG,
- RowType.SEPARATOR,
- RowType.DOCS,
- RowType.DOCS_FALLBACK,
- RowType.BUGS,
- RowType.FORUM,
- RowType.SPONSOR,
- RowType.SEPARATOR,
- 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 = {
"label": strings.mod_panel.select_all,
@@ -499,10 +395,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,10 +413,25 @@ 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,
"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 0c7547a..c3380eb 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,
@@ -21,6 +23,7 @@ from dzgui.const.enum import (
NotebookPage,
ButtonType,
ContextMenu,
+ RowType
)
from dzgui.const.constants import (
@@ -38,8 +41,8 @@ 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.format import format_mods
+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
from dzgui.views.dialogs.filepicker import FilePicker
@@ -53,34 +56,66 @@ 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
+ 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.is_developer: bool
- self.mediator: AppNavigation
+ self.crumbs_cache = ""
+ self.mediator = AppNavigation()
self.prefs: UserPrefs
self.cooldown = 0
self.model_manager = ModelManager()
- 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]
- store = self.model_manager.get_help_store()[0][0]
- if tip == store:
- self.set_crumbs("Help")
+ 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, 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()
- 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()
@@ -99,7 +134,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
@@ -120,12 +155,6 @@ 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
-
- def get_mediator(self) -> "AppNavigation":
- return self.mediator
-
def unblock_signals(self) -> None:
self.block_signals(False)
@@ -137,12 +166,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,
@@ -160,12 +189,39 @@ class Controller:
widget.handler_block_by_func(func)
else:
widget.handler_unblock_by_func(func)
- self.mediator.treeview.sel_blocked = state
+ # TODO: deprecated?
+ #self.mediator.menu.sel_blocked = state
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.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
@@ -181,13 +237,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)
@@ -222,17 +281,20 @@ 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
# 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()
+ def get_cooldown(self) -> None:
+ return self.cooldown
+
def manage_cooldown(self) -> bool:
if cooldown.is_elapsed(self.cooldown):
self.cooldown = cooldown.get_time()
@@ -267,16 +329,24 @@ 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)
+ self.mediator.grid.toggle_refresh_button(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)
+ # 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)
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")
@@ -287,21 +357,21 @@ class Controller:
case ButtonType.MODS:
self.load_mods()
case ButtonType.HELP:
- help_store = self.model_manager.get_help_store()
- 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)
+ self.mediator.grid.statusbar.refresh(RowType.CHANGELOG)
+ pass
+ case ButtonType.SERVERS:
+ self.mediator.notebook.set_page_by_enum(button.opens)
+ # TODO: use cache
+ self.update_server_status()
+ 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?
- 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)
+ open_user_workshop(uid, client)
def copy_log(self, paths: list[Gtk.TreePath]) -> str:
if len(paths) < 1:
@@ -364,7 +434,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)
@@ -376,10 +445,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:
@@ -539,8 +610,70 @@ 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 update_server_status(self) -> None:
+ treeview = self.mediator.notebook.servers.get_active_treeview()
+ model = treeview.get_model()
+ status = format_player_count(model)
+ self.mediator.statusbar.set_text(status)
- def get_developer_mode(self) -> bool:
- return self.is_developer
+ 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:
+ self.crumbs_cache = text
+
+ def get_crumbs_cache(self) -> str:
+ return self.crumbs_cache
+
+ 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()
+ # NOTE: set_query_func()
+ func = treeview.get_query_func()
+ if func is not None:
+ model = treeview.get_model()
+ model.clear()
+ data = func()
+ # TODO: threading
+ model.append(data)
+ 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)
+
+ def toggle_check(self, event: Gdk.EventKey) -> None:
+ keyname = Gdk.keyval_name(event.keyval)
+ if keyname.isnumeric() and int(keyname) > 0:
+ digit = int(keyname) - 1
+ self.mediator.grid.right_panel.filters_vbox.toggle_check(digit)
+ else:
+ match event.keyval:
+ case Gdk.KEY_0:
+ self.mediator.grid.right_panel.filters_vbox.toggle_check(9)
+ case Gdk.KEY_minus:
+ self.mediator.grid.right_panel.filters_vbox.toggle_check(10)
+ case Gdk.KEY_backslash:
+ self.mediator.grid.right_panel.filters_vbox.toggle_check(11)
+ case _:
+ return False
+
+ def 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 d8ebc7a..8dae7e4 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,15 @@ 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)
- 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"]
@@ -88,10 +87,22 @@ 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
+
+ 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()])
@@ -100,8 +111,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
@@ -140,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:
@@ -153,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/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
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)
diff --git a/dzgui/init/update.py b/dzgui/init/update.py
index 222374c..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,14 +10,19 @@ 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
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:
+ logger.critical(e)
+ continue
return tag
def allow_updates(allow: bool) -> bool:
diff --git a/dzgui/main.py b/dzgui/main.py
index 166f70e..a01a86c 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
@@ -43,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
@@ -51,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()
@@ -64,28 +77,29 @@ def main() -> None:
xdg_paths = get_xdg_paths()
XDG = parse_filepaths(xdg_paths)
+ 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)
+
if has_new_config(XDG.config) is False:
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)
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)
@@ -93,6 +107,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)
@@ -104,17 +119,18 @@ 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)
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/util/css.py b/dzgui/util/css.py
index d53188d..3c18249 100644
--- a/dzgui/util/css.py
+++ b/dzgui/util/css.py
@@ -11,9 +11,18 @@ def add_class(widget: Gtk.Widget, label: str) -> None:
context = widget.get_style_context()
context.add_class(label)
+
+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/util/format.py b/dzgui/util/format.py
index cb51065..49695d0 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:
@@ -41,4 +45,27 @@ 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:
+ 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}"
+
+
+def embolden(text: str) -> str:
+ return f"{text}"
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 2129746..037b8e6 100644
--- a/dzgui/util/strings.py
+++ b/dzgui/util/strings.py
@@ -53,8 +53,10 @@ copy_name = "Copy name to clipboard"
copy_ip = "Copy IP to clipboard"
copy_log = "Copy record(s) to clipboard"
add = "Add to my servers"
+add_fav = "Set as favorite"
remove = "Remove from my servers"
remove_history = "Remove from history"
+connect = "Connect"
# Columns
server_mod_cols = ["Mod", "ID", "Installed"]
@@ -63,10 +65,10 @@ log_cols = ["Timestamp", "Flag", "Traceback", "Message"]
browser_cols = [
"Name",
"Map",
- "Perspective",
+ "View",
"Gametime",
"Players",
- "Maximum",
+ "Max",
"Queue",
"IP",
"Qport",
@@ -126,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",
@@ -152,7 +155,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"
@@ -240,8 +243,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
@@ -254,13 +257,10 @@ class Button:
@dataclass(slots=True, frozen=True)
class Crumbs:
changelog: str
- history: str
keys: str
- lan: str
log: str
- main: str
+ _help: str
mods: str
- my_servers: str
options: str
servers: str
thanks: 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",
@@ -440,21 +440,19 @@ buttons = Button(
crumbs = Crumbs(
changelog="Help > Changelog",
- history="Main menu > Recent servers",
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",
- servers="Main menu > Server browser",
+ servers="Servers > Server browser",
thanks="Help > Special thanks",
developers="Options > Developers",
)
checkmark = "ā"
no_mods = "No local mods found."
+no_servers = "No server metadata to list."
@dataclass(slots=True, frozen=True)
class Flags:
@@ -496,3 +494,51 @@ 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",
+)
+
+@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..."
+
+refresh = "Refresh"
+refresh_tooltip = "Refresh server data"
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()
diff --git a/dzgui/views/base.py b/dzgui/views/base.py
index 24f23f2..a1b5a13 100644
--- a/dzgui/views/base.py
+++ b/dzgui/views/base.py
@@ -11,75 +11,64 @@ 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
+from typing import TYPE_CHECKING, Literal
-# TODO: most likely very little of this will be retained
-from dzgui.const.enum import (
- Preferences,
- Popup,
- 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
+from dzgui.const.enum import NotebookPage, VAdjustment
+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, dist, localize, strings, ip, deck, open_links
-from dzgui.util.format import pluralize
+from dzgui.util import css, strings
+from dzgui.util.format import embolden
-# GRID ITEMS
+# 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.thanks import Thanks
+from dzgui.views.pages.log import Log
+from dzgui.views.pages.mods import Mods
from dzgui.views.pages.options import Options
+from dzgui.views.pages.servers import ServerNotebook
+from dzgui.views.pages.thanks import Thanks
+from dzgui.views.components.statusbar import Statusbar
from dzgui.views.components.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_log import LogTreeView
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
+if TYPE_CHECKING:
+ from dzgui.config.userprefs import UserPrefs
+
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
-
logger = logging.getLogger(__name__)
# https://bugzilla.gnome.org/show_bug.cgi?id=708676
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
# TODO: move to controller
-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
+# kilometer cache; note, user may change measurement it partway through, flush cache
+#cache: dict[str, int] = {}
## TODO: move to configs/servers
#def query_history() -> list | None:
@@ -92,14 +81,9 @@ def format_metadata(row: RowType) -> str:
# 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
#
@@ -115,12 +99,12 @@ def format_metadata(row: RowType) -> str:
# 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,
@@ -143,7 +127,7 @@ def format_metadata(row: RowType) -> str:
#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(
@@ -167,7 +151,7 @@ def format_metadata(row: RowType) -> str:
# logger.critical(e)
# # raise dialog
# return
-# # proc = AppNav.treeview.prepare_connection(record)
+# # proc = treeview.prepare_connection(record)
# # return proc
#
# #thread_new_with_dialog(
@@ -184,7 +168,7 @@ def format_metadata(row: RowType) -> str:
# 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
@@ -217,46 +201,22 @@ 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)
- # 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)
-
- MainController.set_resolution(self)
self.grid = Grid()
- self.toast = Toast()
- self.overlay = Gtk.Overlay()
- self.overlay.add_overlay(self.grid)
- self.overlay.add_overlay(self.toast)
- self.add(self.overlay)
+ self.add(self.grid)
+ MainController.set_resolution(self)
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.MAIN)
-
- 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
@@ -268,121 +228,51 @@ 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)
-class ScrollableNote(ScrollableMixin, Gtk.Box):
- def __init__(self, content_box: Gtk.Box, back_button=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:
- AppNav.notebook.return_prior()
-
-
-class Notebook(ScrollableMixin, Gtk.Notebook):
+class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore
def __init__(self) -> None:
- super().__init__(show_tabs=False, show_border=False)
+ super().__init__(show_tabs=False)
- AppNav.notebook = self
- self.prior_page: int
+ self.prior_page: NotebookPage
+ self.prior_status: str
- # TODO: set internally
- self.scrollable_treelist = ScrollableTree()
- self.scrollable_treelist.set_hexpand(False)
- self.scrollable_treelist.set_vexpand(True)
- self.scrollable_treelist.type = None
+ MainController.register_widget("notebook", self)
- 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.help = Help(MainController)
+ self.clog = Changelog(MainController)
+ self.keys = Keybindings()
self.settings = Options(MainController)
- # TODO: make all treeviews internally scrollable in base class
- # 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.ssu = Gtk.ScrolledWindow()
- self.servers = ServerTreeView(MainController)
- self.ssu.add(self.servers)
+ self.servers = ServerNotebook(MainController)
+ self.mods = Mods(MainController)
- self.quad = Gtk.ScrolledWindow()
-
- 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.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)
-
- from dzgui.views.pages.devs import Developers
- developers = Developers(MainController)
- self.developers = ScrollableNote(developers)
+ self.thanks = Thanks()
+ self.log = Log(MainController)
+ self.developers = Developers(MainController)
self.pages = {
- self.scrollable_treelist: NotebookPage.MAIN,
+ self.help: NotebookPage.HELP,
self.clog: NotebookPage.CHANGELOG,
self.keys: NotebookPage.KEYS,
self.settings: NotebookPage.OPTIONS,
- self.ssu: NotebookPage.SERVERS,
- self.scroll_mod: NotebookPage.MODS,
- self.scroll_log: NotebookPage.LOG,
+ self.servers: NotebookPage.SERVERS,
+ self.mods: NotebookPage.MODS,
+ self.log: NotebookPage.LOG,
self.thanks: NotebookPage.THANKS,
self.developers: NotebookPage.DEVELOPERS,
}
self.indexes = {}
"""
- Note that due to historical reasons, 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.
"""
@@ -390,66 +280,37 @@ 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
-
- 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:
- page = self.get_nth_page(self.prior_page)
- # FIXME: check enum for NotebookPage.OPTIONS
- 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 to avoid cursor
+ getting stuck inside text entry fields
+ """
+ if self.prior_page is NotebookPage.OPTIONS:
+ self.settings.block_text_entry()
+ self.set_page_by_enum(self.prior_page)
+ self.settings.unblock_text_entry()
+ return
+ self.set_page_by_enum(self.prior_page)
+ MainController.set_statusbar(self.prior_status)
def get_page_by_enum(self) -> NotebookPage | None:
for k, v in self.indexes.items():
@@ -462,19 +323,24 @@ class Notebook(ScrollableMixin, Gtk.Notebook):
if cur_page == NotebookPage.KEYS:
self.return_prior()
else:
- self.prior_page = self.get_current_page()
+ self.prior_status = MainController.get_statusbar()
self.set_page_by_enum(NotebookPage.KEYS)
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:
+ MainController.grab_active_treeview()
+ return
+
+ w = widget.get_children()[0]
+ 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()
@@ -484,143 +350,87 @@ class Notebook(ScrollableMixin, Gtk.Notebook):
return widget
def set_page_by_enum(self, enum: NotebookPage) -> None:
- self.prior_page = self.get_current_page()
+ 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
- blank = [
- NotebookPage.OPTIONS,
- NotebookPage.THANKS,
- NotebookPage.CHANGELOG,
- NotebookPage.LOG
- ]
- if enum in blank:
- # TODO: delegate to controller
- AppNav.grid.statusbar.refresh(None)
def _on_page_changed(
self, notebook: "Notebook", page: Gtk.Widget, page_num: int
) -> None:
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
+ is_servers = True if enum is NotebookPage.SERVERS else False
+
MainController.toggle_mod_panel(is_mods)
+ MainController.toggle_server_panels(is_servers)
- 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()
-
-
-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
+ if status is False:
+ MainController.set_statusbar("")
+ if enum is NotebookPage.SERVERS:
+ MainController.present_servers()
class Grid(Gtk.Grid):
def __init__(self) -> None:
- super().__init__()
- self.set_column_homogeneous(True)
+ super().__init__(column_homogeneous=True)
- AppNav.grid = self
+ MAX_ROWS = 3
+ MAX_COLS = 3
+ SINGLE_ROW = 1
+ SINGLE_COL = 1
- # 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()
+ 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)
- self.attach(self.notebook, 0, 0, 3, 1)
- self.attach_next_to(
- self.breadcrumbs, self.notebook, Gtk.PositionType.TOP, 3, 1
- )
- self.attach_next_to(
- self.statusbar, self.notebook, Gtk.PositionType.BOTTOM, 3, 1
- )
- self.attach_next_to(
- self.right_panel, self.notebook, Gtk.PositionType.RIGHT, 1, 1
+ self.notebook = Notebook()
+ self.conpan = ConnectPanel(MainController)
+
+ self.attach(self.notebook, 0, 0, MAX_COLS, SINGLE_ROW)
+
+ 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 toggle_filter_panel(self, state: bool) -> None:
+ self.right_panel.filters_vbox.set_visible(state)
+
+ def toggle_connect_panel(self, state: bool) -> None:
+ self.conpan.set_visible(state)
+
+ def toggle_refresh_button(self, state: bool) -> None:
+ self.right_panel.refresh_button.set_visible(state)
+
+ # TODO make this method internal to Statusbar
def get_breadcrumbs(self) -> str:
return self.breadcrumbs.get_text()
def set_breadcrumbs(self, text: str) -> None:
- self.breadcrumbs.set_markup(f"{text}")
+ crumbs = embolden(text)
+ self.breadcrumbs.set_markup(crumbs)
class App(Gtk.Application):
- def __init__(self, prefs, is_developer) -> None:
+ def __init__(self, prefs: "UserPrefs") -> None:
GLib.set_prgname(APP_NAME)
MainController.set_prefs(prefs)
- MainController.set_developer_mode(is_developer)
self.win = OuterWindow()
@@ -638,8 +448,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,
@@ -650,16 +461,4 @@ class App(Gtk.Application):
) -> None:
self.win.halt_proc_and_quit()
-
-class AppNavigation:
- grid: Grid
- window: OuterWindow
- notebook: Notebook
- right_panel: RightPanel
- treeview: MenuTreeView
- modtreeview: ModTreeView
- logtreeview: LogTreeView
- # TODO: add tree_log and tree_server views here
-
-AppNav = AppNavigation()
MainController = Controller()
diff --git a/dzgui/views/components/buttonbox.py b/dzgui/views/components/buttonbox.py
index 6cf6976..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,
@@ -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)
@@ -44,14 +43,13 @@ 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:
diff --git a/dzgui/views/components/buttons.py b/dzgui/views/components/buttons.py
new file mode 100644
index 0000000..2bcbe7e
--- /dev/null
+++ b/dzgui/views/components/buttons.py
@@ -0,0 +1,41 @@
+from dzgui.util.strings import refresh
+from dzgui.views.components.icon import Icon
+from dzgui.const.constants import REFRESH_ICON, WEB_BROWSER, INPUT_KEYBOARD
+
+import gi
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk # noqa E402
+
+
+class IconButton(Gtk.Button):
+ 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) -> None:
+ super().__init__(icon, margin=5)
+ self.set_label(label)
+
+
+class WebButton(IconTextButton):
+ def __init__(self, label: str) -> None:
+ super().__init__(icon=WEB_BROWSER, label=label)
+ pass
+
+
+class RefreshButton(IconTextButton):
+ 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) -> None:
+ 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 f600828..7019ea0 100644
--- a/dzgui/views/components/connect_panel.py
+++ b/dzgui/views/components/connect_panel.py
@@ -1,17 +1,114 @@
+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
-class ConnectPanel(Gtk.Grid):
- def __init__(self) -> None:
- super().__init__()
+if TYPE_CHECKING:
+ from dzgui.controllers.mc import Controller
- self.label = Gtk.Label(label="Connect by IP/ID")
- self.label2 = Gtk.Label(label="Save server by IP/ID")
- self.entry1 = Gtk.Entry()
- self.entry2 = Gtk.Entry()
+class ConnectPanel(Gtk.Frame):
+ def __init__(self, controller: "Controller") -> None:
+ super().__init__(margin_top=10, margin_bottom=5)
- self.attach(self.label, 0, 0, 3, 1)
- 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)
+ COLS = 1
+ ROWS = 1
+ self.classname = "invalid-entry"
+ self.controller = controller
+
+ self.entry = Gtk.Entry(
+ placeholder_text=connect_panel.placeholder,
+ 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()
+ scrollable_label.add(self.fav_label)
+
+ 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(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)
+
+ 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:
+ # TODO: on submission, strip whitespace and newlines
+ 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:
+ 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 fc7fe30..ed6c1ef 100644
--- a/dzgui/views/components/filter_panel.py
+++ b/dzgui/views/components/filter_panel.py
@@ -1,22 +1,22 @@
import logging
+from typing import Literal
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.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.const.enum import FilterMode
-from dzgui.const.constants import NO_EXPAND, NO_FILL
+from dzgui.views.components.labels import BoldLabel
logger = logging.getLogger(__name__)
class FilterPanel(Gtk.Box):
- def __init__(self, appnav, controller):
- super().__init__(spacing=6)
+ 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,
@@ -32,17 +32,17 @@ class FilterPanel(Gtk.Box):
strings.filter_modded: True,
}
- self.AppNav = appnav
self.controller = controller
map_store = self.controller.get_map_store()
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
@@ -71,7 +71,8 @@ class FilterPanel(Gtk.Box):
set_surrounding_margins(self, 10)
self.set_margin_top(1)
- self.filters_label = Gtk.Label(label="Filters")
+ # TODO: strings
+ self.filters_label = BoldLabel("Filters")
self.keyword_entry = Gtk.Entry()
self.keyword_entry.set_placeholder_text("Filter by keyword")
@@ -96,22 +97,19 @@ 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, 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)
- 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)
+ 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:
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)
@@ -125,19 +123,13 @@ 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 = ""
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)
@@ -191,7 +183,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(
@@ -230,7 +223,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
@@ -238,7 +232,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
@@ -255,8 +250,7 @@ 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):
- return
+ treeview = self.controller.get_active_treeview()
label = button.get_label()
state = button.get_active()
logger.info(f"User toggled button '{label}' to {state}")
@@ -266,9 +260,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()
@@ -283,4 +278,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/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)
diff --git a/dzgui/views/components/mod_panel.py b/dzgui/views/components/mod_panel.py
index 599daba..109969a 100644
--- a/dzgui/views/components/mod_panel.py
+++ b/dzgui/views/components/mod_panel.py
@@ -1,8 +1,9 @@
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.const.constants import NO_EXPAND, FILL, NO_PADDING
+from dzgui.util.strings import mod_panel
+from dzgui.views.components.labels import BoldLabel
import gi
gi.require_version("Gtk", "3.0")
@@ -26,32 +27,34 @@ class ModSelectionPanel(Gtk.Box):
self.controller = controller
+ header = BoldLabel(mod_panel.header)
+
+ 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()
- self.header.set_markup(f"{strings.mod_panel.header}")
-
- 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, 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):
+ 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, 0)
+ 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, 0)
- self.pack_start(self.main_panel, NO_EXPAND, FILL, 0)
- self.pack_start(self.extra_panel, NO_EXPAND, FILL, 0)
+ 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()
diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py
index b7fe336..4f81e7e 100644
--- a/dzgui/views/components/right_panel.py
+++ b/dzgui/views/components/right_panel.py
@@ -1,70 +1,49 @@
+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
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.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL, INPUT_KEYBOARD
-from dzgui.util import strings
+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.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, appnav, controller):
+ def __init__(self, controller):
super().__init__(spacing=6, orientation=Gtk.Orientation.VERTICAL)
- self.AppNav = appnav
- self.AppNav.right_panel = self
+ self.time = 30
+
self.controller = controller
+ self.controller.register_widget("right_panel", self)
self.button_vbox = ButtonBox(controller)
- self.filters_vbox = FilterPanel(appnav, controller)
+ self.filters_vbox = FilterPanel(controller)
+ self.sel_panel = ModSelectionPanel(controller)
- for el in self.button_vbox, self.filters_vbox:
- padding = 0
- self.pack_start(el, NO_EXPAND, NO_FILL, padding)
+ self.refresh_button = RefreshButton()
+ self.refresh_button.set_tooltip_text(refresh_tooltip)
+ self.refresh_button.connect("clicked", self._on_refresh_clicked)
- 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)
+ self.keys = KeysButton(keys_button)
+ self.keys.set_tooltip_text(keys_tooltip)
+ self.keys.connect("clicked", self._on_question_clicked)
- self.debug_toggle = Gtk.ToggleButton(
- label=strings.debug_mode,
- margin_top=10,
- margin_start=80,
- margin_end=80,
- tooltip_text=strings.debug_tooltip,
- )
+ for el in self.button_vbox, self.keys, self.filters_vbox, self.refresh_button:
+ self.pack_start(el, NO_EXPAND, FILL, NO_PADDING)
- if controller.query_config(Preferences.DEBUG) == True:
- self.debug_toggle.set_active(True)
- self.debug_toggle.connect("toggled", self._on_debug_toggled)
-
- i = Icon(INPUT_KEYBOARD)
- i.set_margin_start(5)
- self.question = 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)
-
- for el in self.ping, self.debug_toggle, self.question:
- padding = 0
- self.pack_start(el, NO_EXPAND, FILL, padding)
-
- def enable_ping_button(self, state: bool) -> None:
- self.ping.set_visible(state)
+ self.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING)
+ # TODO: move to filter panel
def reinit_maps(self, rows: list) -> None:
self.controller.reinit_map_store()
# TODO: communicate with controller
@@ -73,53 +52,56 @@ 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 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_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.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()
- def _on_ping_clicked(self, button: Gtk.Button) -> None:
- # TODO
- block_signals()
+ # TODO: reference
+ #def _on_ping_clicked(self, button: Gtk.Button) -> None:
+ # 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
+ # treeview.set_model(temp_model)
+ # treeview.wait_dialog.destroy()
+ # 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
- 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()
- # 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()
+ # 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()
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/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
diff --git a/dzgui/views/components/web_button.py b/dzgui/views/components/web_button.py
deleted file mode 100644
index 4d8f0ef..0000000
--- a/dzgui/views/components/web_button.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from dzgui.views.components.icon import Icon
-from dzgui.const.constants import WEB_BROWSER
-
-import gi
-gi.require_version("Gtk", "3.0")
-from gi.repository import Gtk # noqa E402
-
-
-class WebButton(Gtk.Button):
- def __init__(self, label: str):
- super().__init__(label=label)
-
- icon = Icon(WEB_BROWSER, l_margin=5)
- self.set_image(icon)
- self.set_image_position(Gtk.PositionType.RIGHT)
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/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/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/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..4ba2437 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): # type: ignore
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/devs.py b/dzgui/views/pages/devs.py
index 7938178..cd4a1ea 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.strings import developers
+from dzgui.views.components.labels import BoldLabel
import gi # noqa E402
gi.require_version("Gtk", "3.0")
@@ -14,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)
@@ -34,11 +37,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()
- paths_label.set_markup(f"{developers.paths_label}")
- prefs_label = Gtk.Label()
- prefs_label.set_markup(f"{developers.prefs_label}")
+ 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)
@@ -48,23 +49,20 @@ 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)
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)
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
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/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/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)
diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py
index 4dc05ec..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
@@ -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
@@ -167,7 +168,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
)
@@ -187,6 +191,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()
@@ -290,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":
@@ -390,8 +404,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
@@ -410,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/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)
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 7e4ce7c..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,
@@ -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,10 +89,11 @@ class TreeView(CursorMixin, Gtk.TreeView):
if is_navkey(event.keyval):
# TODO: investigate this
+ tv = self.controller.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,
"_on_tree_selection_changed",
True,
)
@@ -101,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_log.py b/dzgui/views/trees/tree_log.py
index b85c9a8..535ae97 100644
--- a/dzgui/views/trees/tree_log.py
+++ b/dzgui/views/trees/tree_log.py
@@ -21,10 +21,11 @@ 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)
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()
diff --git a/dzgui/views/trees/tree_menu.py b/dzgui/views/trees/tree_menu.py
index 0b91ff3..da9cd9e 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()
@@ -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_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_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 e6436f0..47edc6a 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,13 @@ class ServerTreeView(TreeView):
def __init__(self, controller: "Controller") -> None:
super().__init__(controller)
+ QUEUE_CHECK_DELAY = 200
+
+ self.loaded = False
+ self.query_func: Callable = None
+
+ self.menu = Gtk.Menu()
+ self.menu.connect("key-press-event", self._on_key)
self.controller = controller
self.set_fixed_height_mode(True)
@@ -62,9 +76,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]
@@ -78,27 +89,62 @@ class ServerTreeView(TreeView):
if column_title == "Map":
column.set_fixed_width(300)
+ column.connect("notify::fixed-width", self._on_col_width_changed)
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)
-
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)
- 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):
+ 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 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:
+ """
+ Propagate width change to other tabs
+ """
+ title = col.get_title()
+ size = col.get_width()
+
+ # 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():
@@ -118,7 +164,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
@@ -134,37 +180,35 @@ 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()
+ # 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:
- # FIXME: should no longer be relevant
- if AppNav.treeview.view == WindowContext.TABLE_MODS:
- return True
- AppNav.right_panel.filters_vbox.maps_entry.grab_focus()
+ 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:
- 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 copy to context menu
+ 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(f"UNIMPLEMENTED: {item.enum}")
+ pass
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
@@ -179,73 +223,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
- 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
- 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
-
- 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():
@@ -263,25 +257,27 @@ 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):
- 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:
return
- ip = record.ip
+ #model = self.get_model()
+ #if record is None:
+ # self.controller.update_server_status(model)
+ # return
- self.emit("on_distcalc_started")
- self.current_proc = CalcDist(self, record.ip, self.queue, cache)
- self.current_proc.start()
+ ip = record.ip
+ # 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)
@@ -289,6 +285,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
@@ -303,3 +300,9 @@ 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