Merge pull request #30 from aclist/feat/server-notebook

feat/server notebook
This commit is contained in:
aclist 2026-01-09 20:13:32 +09:00 committed by GitHub
commit 775ca2a8d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1348 additions and 929 deletions

View File

@ -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

View File

@ -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}")

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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;
}

View File

@ -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"<b>{text}</b>"

View File

@ -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

View File

@ -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"

View File

@ -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()

View File

@ -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"<b>{text}</b>")
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()

View File

@ -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:

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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"<b>{strings.mod_panel.header}</b>")
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()

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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"<b>{developers.paths_label}</b>")
prefs_label = Gtk.Label()
prefs_label.set_markup(f"<b>{developers.prefs_label}</b>")
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)

21
dzgui/views/pages/help.py Normal file
View File

@ -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

View File

@ -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)

21
dzgui/views/pages/log.py Normal file
View File

@ -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

19
dzgui/views/pages/mods.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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(

View File

@ -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()

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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