Compare commits

...
60 Commits
Author SHA1 Message Date
aclist ad85bb7343 drop collision check for custom mods
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
Mod hashes are taken from the directory names, which cannot be the same
at the filesystem level. Custom mod hashes are also unique from local
mods because they are given a different namespace.
2026-06-11 16:17:02 +09:00
aclist 77df66979a chore: cleanup 2026-06-11 15:40:09 +09:00
aclist d4825390f7 chore: update strings 2026-06-11 15:39:52 +09:00
aclist d5a857fe3a fix: parse tree iters correctly 2026-06-11 15:31:49 +09:00
aclist 241df72bf3 fix: pass custom flag 2026-06-11 15:31:17 +09:00
aclist 4f51d962e7 fix: parse filepath correctly 2026-06-11 15:15:43 +09:00
aclist 98b550cec6 chore: dedicated mission frame 2026-06-11 15:13:09 +09:00
aclist 20c850a977 chore: drop print statement 2026-06-11 15:12:13 +09:00
aclist 5a8d85ac92 fix: less brittle meta parsing 2026-06-11 15:11:38 +09:00
aclist 450d54d7fd fix: swap logic 2026-06-11 12:37:27 +09:00
aclist 65fb07c8da chore: clear type annotation errors 2026-06-11 12:10:37 +09:00
aclist 8123893a90 chore: clean up offline mod manager 2026-06-11 12:03:44 +09:00
aclist 3c9b5c9ed3 chore: update strings 2026-06-11 12:03:22 +09:00
aclist f9be6dfae4 chore: drop unused method 2026-06-11 12:02:44 +09:00
aclist 586f7e6c66 feat: parse mission and mods 2026-06-11 12:02:28 +09:00
aclist 49eb3caf0a fix: handle empty store 2026-06-11 12:02:05 +09:00
aclist 56da5717fa feat: add emitter signal 2026-06-11 12:01:32 +09:00
aclist 9983e91526 feat: test mission integrity 2026-06-11 12:01:17 +09:00
aclist 820deb2955 fix: incorrect error state 2026-06-11 10:52:49 +09:00
aclist 0137bf4610 fix: process escape key 2026-06-11 10:47:32 +09:00
aclist 687b1a915e feat: capture escape signal in BootDialog 2026-06-11 10:43:52 +09:00
aclist 401c8625ad chore: add placeholder notes
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-11 08:07:58 +09:00
aclist 8503d7e698 feat: namespace mod hashes 2026-06-11 08:02:27 +09:00
aclist b35b6148bc change: tweak expansion properties 2026-06-11 07:51:16 +09:00
aclist 45fd180dcb fix: accept emitter as arg
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-10 19:23:14 +09:00
aclist 486da7749a fix: emitter arguments 2026-06-10 18:37:31 +09:00
aclist 606df3423c change: delegate early checks to BootWindow 2026-06-10 15:59:44 +09:00
aclist ab439144ea fix: use StoredFunc
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-10 15:21:04 +09:00
aclist db14c5252c change: clearer startup error 2026-06-10 15:18:42 +09:00
aclist 7d8b3b402f feat: use emitter 2026-06-10 14:30:50 +09:00
aclist 4506a0cfda chore: revert IconButton alignment changes 2026-06-10 08:27:24 +09:00
aclist 978edc5f5f chore: add format strings 2026-06-10 08:26:50 +09:00
aclist 2466887014 chore: drop print statement
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-10 06:45:51 +09:00
aclist 6050a370f8 feat: folder icons 2026-06-10 06:21:12 +09:00
aclist d4a99f8229 feat: prepare mods in manager (WIP) 2026-06-10 06:15:27 +09:00
aclist 46fba4eef1 chore: clear typehinting errors 2026-06-10 05:45:08 +09:00
aclist bc1118a29f feat: OfflineManager 2026-06-10 05:27:57 +09:00
aclist 775f582ea6 fix: drop extraneous arg 2026-06-10 05:27:39 +09:00
aclist 2dadd70be3 fix: pack tree and statusbar in vbox 2026-06-10 05:05:22 +09:00
aclist 3473f03350 fix: pad inner scrollbar 2026-06-10 04:51:05 +09:00
aclist e081d0ad16 feat: collect appid on toggle 2026-06-09 03:09:34 +09:00
aclist e6026e84f9 feat: launch offline (WIP) 2026-06-09 02:37:50 +09:00
aclist b17b8c24ff fix: fill model inside thread 2026-06-08 19:27:40 +09:00
aclist 4b9687a84d chore: clear typehinting errors 2026-06-07 20:22:32 +09:00
aclist 0c0f280d15 docs: update changelog 2026-06-07 20:18:27 +09:00
aclist 831bd9145d feat: launch offline (WIP)) 2026-06-07 20:17:58 +09:00
aclist 0070fe4061 fix: suppress headers if no model is loaded 2026-06-07 20:17:32 +09:00
aclist 9d1443cf65 feat: launch offline (WIP) 2026-06-07 20:15:30 +09:00
aclist c4cc9f04c4 Merge pull request #347 from aclist/chore/clean-statusbar
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
chore: drop unused code from statusbar
2026-06-06 17:56:03 +09:00
aclist bcd59162b1 chore: drop unused code from statusbar 2026-06-06 17:55:05 +09:00
aclist 2f0bac75ac Merge pull request #346 from aclist/fix/permit-filters
fix: permit legacy filters
2026-06-06 17:53:19 +09:00
aclist 9191f43c8e chore: drop comment 2026-06-06 17:50:53 +09:00
aclist 2419fedd19 chore: format exceptions with type 2026-06-06 17:50:32 +09:00
aclist 83a7996189 The filter panel previously did not pack filter labels homogeneously, so
the width of the "Unofficial" label was being manually constrained by
abbreviating it. Labels auto ellipsize and adjust now, so the full name
is used. The filter file check is also allowed to fail if the user's
filter preferences file contains old key names.
2026-06-06 17:39:49 +09:00
aclist 9d3aa8d9f5 Merge pull request #344 from aclist/fix/stringify-exceptions
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: stringify Exception when passing to dialog
2026-06-06 02:31:09 +09:00
aclist 0de0dd4a03 fix: stringify Exception when passing to dialog 2026-06-06 02:29:09 +09:00
aclist aeefc830cd Merge pull request #343 from aclist/prelease/7.0.0b10
chore: bump version
2026-06-06 02:05:26 +09:00
aclist 647f75cde3 chore: bump version 2026-06-06 02:03:45 +09:00
aclist 7e8b8eb7f7 Merge pull request #341 from aclist/fix/7.0.0b10-hotfix
fix: extract tarfile correctly
2026-06-06 02:02:51 +09:00
aclist 79af78a7e0 fix: extract tarfile correctly 2026-06-06 02:00:15 +09:00
46 changed files with 909 additions and 156 deletions

No files matched your search

+1
View File
@@ -40,6 +40,7 @@
- Preboot progress dialog - Preboot progress dialog
- Choose to jump into splash screen instead of server - Choose to jump into splash screen instead of server
- Collapsible connection panel - Collapsible connection panel
- Play offline (load mods directly)
## Changed ## Changed
- Conform to PEP 440 versioning for beta versions - Conform to PEP 440 versioning for beta versions
+41 -39
View File
@@ -1,6 +1,6 @@
import hashlib import hashlib
import logging import logging
import shlex import re
from concurrent.futures import wait from concurrent.futures import wait
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@@ -48,36 +48,29 @@ def get_local_mods(workshop_path: Path) -> list[Path]:
mods = [file for file in workshop_path.iterdir() if file.is_dir()] mods = [file for file in workshop_path.iterdir() if file.is_dir()]
return mods return mods
def is_mission(path: Path) -> bool:
# TODO: parse integrity of other files
file = path / "init.c"
return file.exists()
def parse_meta(file: Path) -> ModMeta | None: def tokenize(mod: Path) -> dict[Any] | None:
mod = file / "meta.cpp" file = mod.joinpath("meta.cpp")
if mod.exists() is False: delimiter=r"\s*=\s*"
modmeta = {}
try:
with open(file, "r", encoding="utf-8") as file:
for line in file:
line = line.strip().rstrip(";")
if not line:
continue
els = re.split(delimiter, line, maxsplit=1)
if len(els) == 2:
key, value = els
modmeta[key.strip()] = value.strip('"')
return modmeta
except Exception as e:
logger.critical(e)
return None return None
with open(file / "meta.cpp", "r") as f:
st = f.read()
lex = shlex.shlex(st)
lex.whitespace += "=;"
v = []
while True:
tok = lex.get_token()
if not tok:
break
if tok == "protocol" or tok == "publishedid":
ntok = lex.get_token()
elif tok == "timestamp":
# NOTE: some malformed .NET tick conversions result in numbers < 0
ntok = lex.get_token()
if ntok == "-":
ntok += str(lex.get_token())
elif tok == "name":
ntok = lex.get_token()
if ntok is not None:
ntok = ntok.split('"')[1]
if ntok is not None:
v.append(ntok)
meta = ModMeta(*v)
return meta
def get_mod_size(path: Path) -> float: def get_mod_size(path: Path) -> float:
s = 0 s = 0
@@ -87,33 +80,42 @@ def get_mod_size(path: Path) -> float:
return size return size
def get_delimited_mods(steam_path: Path) -> list[Any]: def get_custom_mods(path: Path) -> list[Any]:
workshop_path = get_local_mod_path(steam_path) mods = get_local_mods(path)
mods = get_local_mods(workshop_path) # TODO: error handling
return parse_mods(mods, use_custom=True)
def parse_mods(mods: list[Path], use_custom: bool = False) -> list[Any]:
clean = [] clean = []
for mod in mods: for mod in mods:
mod_dir = mod.name mod_dir = mod.name
symlink = _hash(mod_dir) symlink = _hash(mod_dir, use_custom)
# FIXME: malformed .cpp files could break this meta = tokenize(mod)
# mention that mods may be downloading
meta = parse_meta(mod)
if meta is None: if meta is None:
continue continue
size = get_mod_size(mod) size = get_mod_size(mod)
# NOTE: final col is cell renderer highlight toggle # NOTE: final col is cell renderer highlight toggle
clean.append([meta.name, symlink, mod_dir, size, False]) clean.append([meta["name"], symlink, mod_dir, size, False])
clean.sort(key=lambda row: str(row[0]).casefold()) clean.sort(key=lambda row: str(row[0]).casefold())
return clean return clean
def get_delimited_mods(steam_path: Path) -> list[Any]:
workshop_path = get_local_mod_path(steam_path)
mods = get_local_mods(workshop_path)
return parse_mods(mods)
def get_missing_mods(local: list, remote: list) -> list: def get_missing_mods(local: list, remote: list) -> list:
return [mod for mod in remote if mod not in local] return [mod for mod in remote if mod not in local]
def _hash(uid: str) -> str: def _hash(uid: str, use_custom: bool = False) -> str:
md5 = hashlib.md5() md5 = hashlib.md5()
md5.update(uid.encode("ascii")) md5.update(uid.encode("ascii"))
return "@" + md5.hexdigest()[:8] prefix = "@C" if use_custom else "@"
return prefix + md5.hexdigest()[:8]
def remove_stale_signatures(config: Path, versions: Path) -> None: def remove_stale_signatures(config: Path, versions: Path) -> None:
+4 -2
View File
@@ -14,6 +14,7 @@ from typing import Any, Optional, TYPE_CHECKING, Union
from dzgui.api.bm import map_id_to_record from dzgui.api.bm import map_id_to_record
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
from dzgui.const.endpoints import STEAM_SERVERS from dzgui.const.endpoints import STEAM_SERVERS
from dzgui.util.format import format_exception
from dzgui.util import strings from dzgui.util import strings
import a2s import a2s
@@ -272,8 +273,9 @@ def source_info_to_dict(ip: str, qport: int, info: "SourceInfo") -> dict[str, An
res["ping"] = ping res["ping"] = ping
return res return res
except Exception as e: except Exception as e:
# TODO: generalized function msg = format_exception(e)
logger.critical(f"{type(e).__name__}: {e} ({ip}:{qport})") msg += f" ({ip}:{qport})"
logger.critical(msg)
raise e raise e
+17 -1
View File
@@ -146,7 +146,7 @@ def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> i
return proc.returncode return proc.returncode
def load_to_menu(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int: def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int:
"""Loads to the menu screen with the selected mods; used for AFK/pre-joining""" """Loads to the menu screen with the selected mods; used for AFK/pre-joining"""
concat = concat_mods(mods) concat = concat_mods(mods)
client_args = concat_bash_args(client) client_args = concat_bash_args(client)
@@ -162,6 +162,22 @@ def load_to_menu(client: str, addr: str, appid: int, name: str, mods: list[str])
proc = subprocess.run([*client_args, *params]) proc = subprocess.run([*client_args, *params])
return proc.returncode return proc.returncode
def launch_offline(client: str, appid: int, name: str, mods: list[str], mission: str) -> int:
"""Launch offline with specific mods/missions"""
concat = concat_mods(mods)
client_args = concat_bash_args(client)
params = [
"-applaunch",
str(appid),
"-nolauncher",
"-nosplash",
"-skipintro",
f"-name={name}",
f"-mod={concat}",
f"-mission={mission}"
]
proc = subprocess.run([*client_args, *params])
return proc.returncode
def find_user_id(path: Path) -> str | None: def find_user_id(path: Path) -> str | None:
resolved_path = path / "config" / "loginusers.vdf" resolved_path = path / "config" / "loginusers.vdf"
+2 -1
View File
@@ -35,6 +35,7 @@ HEX_ORANGE = "#FFAC1C"
CARET_DOWN = "go-down-symbolic" CARET_DOWN = "go-down-symbolic"
CARET_UP = "go-up-symbolic" CARET_UP = "go-up-symbolic"
CLIPBOARD = "edit-copy-symbolic" CLIPBOARD = "edit-copy-symbolic"
FOLDER = "folder-symbolic"
ERROR = "dialog-error-symbolic" ERROR = "dialog-error-symbolic"
HELP_BUBBLE = "help-about-symbolic" HELP_BUBBLE = "help-about-symbolic"
INPUT_KEYBOARD = "input-keyboard-symbolic" INPUT_KEYBOARD = "input-keyboard-symbolic"
@@ -79,4 +80,4 @@ LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
TMP_PATH = "/tmp" TMP_PATH = "/tmp"
TMP_TARBALL = "/tmp/dzgui.tar.gz" TMP_TARBALL = "/tmp/dzgui.tar.gz"
TMP_EXE = "/tmp/dzgui" TMP_EXE = "/tmp/dzgui/dzgui"
+1
View File
@@ -103,6 +103,7 @@ class Preferences(EnumWithAttrs):
class NotebookPage(EnumWithAttrs): class NotebookPage(EnumWithAttrs):
OFFLINE = {"crumbs": strings.crumbs.offline, "statusbar": True}
CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": True} CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": True}
DEVELOPERS = {"crumbs": strings.crumbs.developers, "statusbar": False} DEVELOPERS = {"crumbs": strings.crumbs.developers, "statusbar": False}
HELP = {"crumbs": strings.crumbs._help, "statusbar": True} HELP = {"crumbs": strings.crumbs._help, "statusbar": True}
+9 -1
View File
@@ -1,13 +1,13 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository.Gtk import ListStore # noqa E402 from gi.repository.Gtk import ListStore # noqa E402
from gi.repository import Gdk, GObject # noqa E402 from gi.repository import Gdk, GObject # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.const.enum import NotebookPage, ServerTab from dzgui.const.enum import NotebookPage, ServerTab
from dzgui.model.model_factory import FastInsertListStore
# TODO: if servers fail to load, may leave dangling widgets waiting for a signal # TODO: if servers fail to load, may leave dangling widgets waiting for a signal
@@ -154,3 +154,11 @@ class Emitter(GObject.GObject):
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=()) @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def lan_page_initialized(self) -> None: def lan_page_initialized(self) -> None:
pass pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object, str, bool))
def custom_mods_loaded(self, store: "FastInsertListStore", folder: str) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str, bool))
def custom_mission_loaded(self, folder: str, is_valid: bool) -> None:
pass
+27 -9
View File
@@ -20,7 +20,6 @@ from dzgui.controllers.emitter import Emitter
from dzgui.managers.config import ConfigManager from dzgui.managers.config import ConfigManager
from dzgui.managers.connection import ConnectionManager from dzgui.managers.connection import ConnectionManager
from dzgui.managers.contextmenu import ContextMenuManager from dzgui.managers.contextmenu import ContextMenuManager
from dzgui.managers.mods import ModManager
from dzgui.managers.notes import NoteManager from dzgui.managers.notes import NoteManager
from dzgui.model.servers import ServerModelManager from dzgui.model.servers import ServerModelManager
from dzgui.util.diag import write_diagnostic from dzgui.util.diag import write_diagnostic
@@ -47,12 +46,13 @@ if TYPE_CHECKING:
from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.right_panel import RightPanel from dzgui.views.components.right_panel import RightPanel
from dzgui.views.components.statusbar import Statusbar from dzgui.views.components.statusbar import Statusbar
from dzgui.views.pages.offline import OfflineLoader
from dzgui.views.pages.options import Options from dzgui.views.pages.options import Options
from dzgui.views.pages.preconnect import PreConnectionAssistant from dzgui.views.pages.preconnect import PreConnectionAssistant
from dzgui.views.pages.servers import ServerNotebook from dzgui.views.pages.servers import ServerNotebook
from dzgui.views.trees.tree_log import LogTreeView from dzgui.views.trees.tree_log import LogTreeView
from dzgui.views.trees.tree_menu import MenuTreeView from dzgui.views.trees.tree_menu import MenuTreeView
from dzgui.views.trees.tree_mods import ModTreeView from dzgui.views.trees.tree_mods import ModTreeView, OfflineModTreeView
from dzgui.views.trees.tree_servers import ServerTreeView from dzgui.views.trees.tree_servers import ServerTreeView
@@ -72,6 +72,7 @@ class AppNavigation:
filters: "FilterPanel" filters: "FilterPanel"
preconnect: "PreConnectionAssistant" preconnect: "PreConnectionAssistant"
options: "Options" options: "Options"
offline_loader: "OfflineLoader"
class Controller(GObject.GObject): class Controller(GObject.GObject):
@@ -218,19 +219,29 @@ class Controller(GObject.GObject):
open_user_workshop(uid, client) open_user_workshop(uid, client)
def load_mods(self) -> None: def load_mods(self) -> None:
self.mod_man = ModManager(self) self.mediator.modtreeview.load_mods()
def uncolorize_mods(self) -> None: def uncolorize_mods(self) -> None:
self.mod_man.uncolorize_mods() mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.uncolorize_mods()
def highlight_stale(self) -> None: def highlight_stale(self) -> None:
self.mod_man.highlight_stale() mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.highlight_stale()
def toggle_mod_selection(self, state: bool) -> None: def toggle_mod_selection(self, state: bool) -> None:
self.mod_man.toggle_mod_selection(state) mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.toggle_mod_selection(state)
def delete_mods(self) -> None: def delete_mods(
self.mod_man.delete_mods() self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
) -> None:
if treeview is None:
view = self.mediator.modtreeview
else:
view = treeview
mod_man = view.get_mod_man()
mod_man.delete_mods()
def get_mod_store(self) -> Gtk.TreeModel | None: def get_mod_store(self) -> Gtk.TreeModel | None:
return self.mediator.modtreeview.get_model() return self.mediator.modtreeview.get_model()
@@ -257,7 +268,8 @@ class Controller(GObject.GObject):
dialog.run() dialog.run()
def select_colorized(self) -> None: def select_colorized(self) -> None:
self.mod_man.select_colorized() mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.select_colorized()
def get_all_tree_filters(self) -> dict[str, dict[str, bool]]: def get_all_tree_filters(self) -> dict[str, dict[str, bool]]:
servers = self.get_servers() servers = self.get_servers()
@@ -533,3 +545,9 @@ class Controller(GObject.GObject):
self.clear_cancel_event() self.clear_cancel_event()
return True return True
return False return False
def open_offline(self) -> None:
self.open_page(NotebookPage.OFFLINE)
mod_man = self.mediator.modtreeview.get_mod_man()
store = mod_man.store
self.mediator.offline_loader.populate(store)
+1 -4
View File
@@ -1,12 +1,10 @@
import logging import logging
import sys
from pathlib import Path from pathlib import Path
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.const.constants import APPID_DAYZ, APP_NAME, LIBRARYFOLDERS_PATH from dzgui.const.constants import APPID_DAYZ, APP_NAME, LIBRARYFOLDERS_PATH
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
import dzgui.api.pefile as PeFile import dzgui.api.pefile as PeFile
@@ -19,5 +17,4 @@ def is_dayz_installed(config: Path) -> None:
PeFile.get_app_path(Path(path) / LIBRARYFOLDERS_PATH, APPID_DAYZ) PeFile.get_app_path(Path(path) / LIBRARYFOLDERS_PATH, APPID_DAYZ)
except Exception as e: except Exception as e:
logger.critical(e) logger.critical(e)
EarlyAlertDialog(str(e)) raise e
sys.exit(1)
-10
View File
@@ -11,7 +11,6 @@ from dzgui.const.enum import Preferences
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.init.dayz import is_dayz_installed
from dzgui.init.flock import lock_acquire from dzgui.init.flock import lock_acquire
from dzgui.init.migrate import ( from dzgui.init.migrate import (
has_new_config, has_new_config,
@@ -124,18 +123,9 @@ def main() -> None:
if has_steam_client() is False: if has_steam_client() is False:
EarlyAlertDialog(init.requires_steam) EarlyAlertDialog(init.requires_steam)
is_dayz_installed(XDG.config)
bootwin = BootWindow(XDG, version) bootwin = BootWindow(XDG, version)
local_coords, latest_release = bootwin.get_results() local_coords, latest_release = bootwin.get_results()
# rebuild_symlinks(XDG.config)
# remove_stale_signatures(XDG.config, XDG.version)
## TODO: handle IP DB failure and use coords fallback
# local_coords = get_local_coords(XDG.ips)
# latest_release = check_updates(version)
use_miles = lookup(XDG.config, Preferences.DIST) use_miles = lookup(XDG.config, Preferences.DIST)
prefs = UserPrefs( prefs = UserPrefs(
is_steam_deck=_is_steam_deck, is_steam_deck=_is_steam_deck,
+2 -3
View File
@@ -238,8 +238,7 @@ class ConnectionManager:
def query_modlist_and_present(self, record: Servers.Record) -> None: def query_modlist_and_present(self, record: Servers.Record) -> None:
try: try:
mods = self._query_modlist(record) mods = self._query_modlist(record)
except Exception as e: except Exception:
print(f"{type(e).__name__}: {e}")
self.thread_man.set_cleanup_func( self.thread_man.set_cleanup_func(
StoredFunc(self._server_timeout), destroy_first=True StoredFunc(self._server_timeout), destroy_first=True
) )
@@ -266,7 +265,7 @@ class ConnectionManager:
playername = self.controller.query_config(Preferences.NAME) playername = self.controller.query_config(Preferences.NAME)
client = self.controller.query_config(Preferences.CLIENT) client = self.controller.query_config(Preferences.CLIENT)
if menu_only: if menu_only:
rc = load_to_menu(client, addr, self.appid, playername, self.remote_mod_ids) rc = load_to_menu(client, self.appid, playername, self.remote_mod_ids)
else: else:
rc = connect(client, addr, self.appid, playername, self.remote_mod_ids) rc = connect(client, addr, self.appid, playername, self.remote_mod_ids)
if rc != 0: if rc != 0:
+10 -4
View File
@@ -8,7 +8,7 @@ from dzgui.util.clip import copy_clipboard
from dzgui.util.open_links import open_workshop_page from dzgui.util.open_links import open_workshop_page
from dzgui.views.dialogs.note import NoteDialog from dzgui.views.dialogs.note import NoteDialog
from dzgui.views.trees.tree_mods import ModTreeView from dzgui.views.trees.tree_mods import ModTreeView, OfflineModTreeView
from dzgui.views.trees.tree_log import LogTreeView from dzgui.views.trees.tree_log import LogTreeView
from dzgui.views.trees.tree_servers import ServerTreeView from dzgui.views.trees.tree_servers import ServerTreeView
from dzgui.views.trees.tree_server_mods import ServerModTreeView from dzgui.views.trees.tree_server_mods import ServerModTreeView
@@ -27,7 +27,13 @@ if TYPE_CHECKING:
class ContextMenuManager: class ContextMenuManager:
def __init__( def __init__(
self, self,
treeview: LogTreeView | ModTreeView | ServerTreeView | ServerModTreeView, treeview: (
LogTreeView
| ModTreeView
| ServerTreeView
| ServerModTreeView
| OfflineModTreeView
),
controller: "Controller", controller: "Controller",
) -> None: ) -> None:
self.controller = controller self.controller = controller
@@ -72,10 +78,10 @@ class ContextMenuManager:
case ContextMenu.SHOW_MODS: case ContextMenu.SHOW_MODS:
self.controller.get_modlist(record) self.controller.get_modlist(record)
if isinstance(self.treeview, ModTreeView): if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
match action: match action:
case ContextMenu.DELETE_MOD: case ContextMenu.DELETE_MOD:
self.controller.delete_mods() self.controller.delete_mods(self.treeview)
case ContextMenu.OPEN_WORKSHOP: case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page() self.open_mod_page()
+14 -10
View File
@@ -2,7 +2,7 @@ import logging
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Any, TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.api.mods import ( from dzgui.api.mods import (
get_delimited_mods, get_delimited_mods,
@@ -40,20 +40,20 @@ class ModManager:
this manager is instantiated each time the Mods page is opened this manager is instantiated each time the Mods page is opened
""" """
def __init__(self, controller: "Controller") -> None: def __init__(self, tree: Gtk.TreeView, controller: "Controller") -> None:
self.treeview = tree
self.controller = controller self.controller = controller
self.emitter = controller.get_emitter() self.emitter = controller.get_emitter()
self.prefs = controller.get_prefs() self.prefs = controller.get_prefs()
self.path = controller.query_config(Preferences.DEFAULT) self.path = controller.query_config(Preferences.DEFAULT)
self.treeview = self.controller.get_modtreeview()
self.store: FastInsertListStore self.store: FastInsertListStore | None = None
self.thread_man = ThreadingManager(controller) self.thread_man = ThreadingManager(controller)
self._get_mods()
@call_on_thread(dialogs.fetching_mods) @call_on_thread(dialogs.fetching_mods)
def _get_mods(self) -> None: def load_mods(self) -> None:
mods = get_delimited_mods(self.path) mods = get_delimited_mods(self.path)
if len(mods) < 1: if len(mods) < 1:
msg = self.format_mod_statusbar() msg = self.format_mod_statusbar()
@@ -61,13 +61,15 @@ class ModManager:
self.thread_man.set_cleanup_func(func) self.thread_man.set_cleanup_func(func)
return return
func = StoredFunc(self._on_mods_loaded, mods)
self.thread_man.set_cleanup_func(func)
def _on_mods_loaded(self, mods: list[list[Any]]) -> None:
self.store = ModelFactory().make_mod_store() self.store = ModelFactory().make_mod_store()
self.store.extend(mods) self.store.extend(mods)
func = StoredFunc(self._on_mods_loaded)
self.thread_man.set_cleanup_func(func)
def _on_mods_loaded(self) -> None:
self.treeview.set_model(self.store) self.treeview.set_model(self.store)
if self.store is None:
return
msg = self.format_mod_statusbar() msg = self.format_mod_statusbar()
total_mods = len(self.store) total_mods = len(self.store)
self.emitter.emit("mods_updated", msg, total_mods) self.emitter.emit("mods_updated", msg, total_mods)
@@ -124,6 +126,8 @@ class ModManager:
pass pass
def _on_mods_deleted(self, iters: list[Gtk.TreeIter]) -> None: def _on_mods_deleted(self, iters: list[Gtk.TreeIter]) -> None:
if self.store is None:
return
for _iter in iters: for _iter in iters:
self.store.remove(_iter) self.store.remove(_iter)
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version) remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version)
+98
View File
@@ -0,0 +1,98 @@
from pathlib import Path
from typing import TYPE_CHECKING, Union
from dzgui.api.mods import is_mission, get_custom_mods
from dzgui.api.steam import launch_offline
from dzgui.const.enum import Preferences
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.model.model_factory import ModelFactory
from dzgui.strings import dialogs
from dzgui.util.symlink import clone_symlinks
from dzgui.views.dialogs.filepicker import FolderPicker
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
class OfflineManager:
def __init__(
self,
controller: "Controller",
) -> None:
super().__init__()
self.controller = controller
self.emitter = controller.get_emitter()
self.thread_man = ThreadingManager(controller)
self.appid: int
self.mission_folder: str
self.local_mods: list[str] | None
self.custom_mods: list[str] | None
def get_mission(self) -> None:
folder = self.open_folderpicker()
if folder is None:
return
is_valid = is_mission(folder)
# TODO: stop storing values in here
if is_valid:
# TODO: str
self.mission_folder = str(folder)
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
def get_custom_mods(self, local_mods: list[str]) -> None:
folder = self.open_folderpicker()
if folder is None:
return
self.parse_custom_mods(local_mods, folder)
@call_on_thread(dialogs.parsing_mods)
def parse_custom_mods(self, local_mods: list[str], folder: str) -> None:
mods = get_custom_mods(Path(folder))
store = ModelFactory().make_mod_store()
store.extend(mods)
func = StoredFunc(lambda: self.emitter.emit("custom_mods_loaded", store, folder))
self.thread_man.set_cleanup_func(func)
def setup(
self,
appid: int,
mission: str = "",
local_mods: list[str] | None = None,
custom_mods: list[str] | None = None,
) -> None:
self.appid = appid
self.local_mods = local_mods
self.custom_mods = custom_mods
self.launch()
@call_on_thread(dialogs.waiting_for_launch)
def launch(self) -> None:
client = self.controller.query_config(Preferences.CLIENT)
name = self.controller.query_config(Preferences.NAME)
steam_path = self.controller.query_config(Preferences.DEFAULT)
new_symlinks: list[str] = []
combined_mods: list[str] = []
if self.local_mods is not None:
combined_mods.extend(self.local_mods)
if self.custom_mods is not None:
# TODO: new function that creates symlinks in game path
# based on selected mods
clone_symlinks(Path(steam_path))
combined_mods.extend(new_symlinks)
launch_offline(client, self.appid, name, combined_mods, self.mission_folder)
def open_folderpicker(self) -> Union["Path", None]:
picker = FolderPicker(self.controller.get_window())
folder = picker.pick_folder()
picker.destroy()
return folder
+2 -2
View File
@@ -42,7 +42,7 @@ class UpdateManager:
with tarfile.open(TMP_TARBALL) as tar: with tarfile.open(TMP_TARBALL) as tar:
tar.extractall(TMP_PATH) tar.extractall(TMP_PATH)
shutil.move(TMP_EXE, exe_path) shutil.copy(TMP_EXE, exe_path)
proc = subprocess.run([exe_path, "self", "restore"]) proc = subprocess.run([exe_path, "self", "restore"])
if proc.returncode == 0: if proc.returncode == 0:
@@ -53,7 +53,7 @@ class UpdateManager:
func = StoredFunc(self._on_update_failure, msg) func = StoredFunc(self._on_update_failure, msg)
self.thread_man.set_cleanup_func(func, destroy_first=True) self.thread_man.set_cleanup_func(func, destroy_first=True)
except Exception as e: except Exception as e:
func = StoredFunc(self._on_update_failure, e) func = StoredFunc(self._on_update_failure, str(e))
self.thread_man.set_cleanup_func(func, destroy_first=True) self.thread_man.set_cleanup_func(func, destroy_first=True)
logger.warning(e) logger.warning(e)
+3
View File
@@ -165,3 +165,6 @@ class ModelFactory:
def make_server_store(self) -> FastInsertListStore: def make_server_store(self) -> FastInsertListStore:
return self.new_model_from_class(ServerCols) return self.new_model_from_class(ServerCols)
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
# return [[el for el in row] for row in model]
+5
View File
@@ -9,5 +9,10 @@ load_error_lan = "Failed to find any servers on your network.\nCheck the server
fetching_mods = "Fetching mod metadata" fetching_mods = "Fetching mod metadata"
deleting_mods = "Deleting mods" deleting_mods = "Deleting mods"
scanning_mods = "Scanning mods" scanning_mods = "Scanning mods"
parsing_mods = "Parsing mods"
checking_api = "Checking API key" checking_api = "Checking API key"
running = "Running"
failed = "FAILED"
ok = "OK"
+1
View File
@@ -3,3 +3,4 @@ api_validation_error = (
) )
api_popover = "API key validation failed." api_popover = "API key validation failed."
dayz_not_found = "Could not find a valid DayZ installation."
+1
View File
@@ -0,0 +1 @@
selectable_tree = "Ctrl-click to select multiple; Shift-click to select a range."
+14
View File
@@ -0,0 +1,14 @@
heading = "Play offline"
mission_button = "Select mission folder"
mission_frame = "Mission"
version = "DayZ Version"
custom_frame = "Custom mods"
custom_button = "Set custom mod folder"
local_frame = "Installed mods"
no_mods = "No valid mods found"
no_mission = "Not a valid mission"
+1
View File
@@ -3,3 +3,4 @@ signatures = "Updating mod signatures"
geo = "Checking geolocation records" geo = "Checking geolocation records"
coords = "Checking local coordinates" coords = "Checking local coordinates"
updates = "Checking for updates" updates = "Checking for updates"
dayz = "Looking for DayZ"
+6 -2
View File
@@ -1,6 +1,7 @@
import re import re
from dzgui.util.localize import number from dzgui.util.localize import number
from dzgui.strings.generic import selectable_tree
from dzgui.util.strings import no_mods, no_servers, workshop from dzgui.util.strings import no_mods, no_servers, workshop
import gi import gi
@@ -46,8 +47,7 @@ def format_mods(size: int, mods: int) -> str:
return no_mods return no_mods
l_size = number(size) l_size = number(size)
plural = pluralize("mods", mods) plural = pluralize("mods", mods)
# TODO: strings suffix = selectable_tree
suffix = "Ctrl-click to select multiple; Shift-click to select a range."
return f"Found {mods:n} {plural} taking up {l_size} MiB. {suffix}" return f"Found {mods:n} {plural} taking up {l_size} MiB. {suffix}"
@@ -83,3 +83,7 @@ def format_player_count(model: Gtk.TreeModel | None, control: list) -> str:
def embolden(text: str) -> str: def embolden(text: str) -> str:
return f"<b>{text}</b>" return f"<b>{text}</b>"
def format_exception(e: Exception) -> str:
return f"{type(e).__name__}: {e}"
+4 -2
View File
@@ -64,7 +64,7 @@ connect = "Connect"
# Columns # Columns
server_mod_cols = ["Mod", "ID", "Installed"] server_mod_cols = ["Mod", "ID", "Installed"]
mod_cols = ["Mod", "Symlink", "Dir", "Size (MiB)", "Color"] mod_cols = ["Mod", "Symlink", "ID", "Size (MiB)", "Color"]
log_cols = ["Timestamp", "Flag", "Traceback", "Message"] log_cols = ["Timestamp", "Flag", "Traceback", "Message"]
browser_cols = [ browser_cols = [
"Name", "Name",
@@ -181,7 +181,7 @@ filter_lowpop = "Low pop"
filter_nonascii = "Non-ASCII" filter_nonascii = "Non-ASCII"
filter_duplicate = "Duplicate" filter_duplicate = "Duplicate"
filter_official = "Official" filter_official = "Official"
filter_unofficial = "Unoffic." filter_unofficial = "Unofficial"
filter_modded = "Modded" filter_modded = "Modded"
# maps # maps
@@ -273,6 +273,7 @@ class Crumbs:
thanks: str thanks: str
developers: str developers: str
default: str default: str
offline: str
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
@@ -462,6 +463,7 @@ crumbs = Crumbs(
thanks="Help > Special thanks", thanks="Help > Special thanks",
developers="Options > Developers", developers="Options > Developers",
default="Servers > ", default="Servers > ",
offline="Mods > Play offline"
) )
no_mods = "No local mods found." no_mods = "No local mods found."
+5
View File
@@ -23,6 +23,8 @@ from dzgui.views.pages.help import Help
from dzgui.views.pages.keys import Keybindings from dzgui.views.pages.keys import Keybindings
from dzgui.views.pages.log import Log from dzgui.views.pages.log import Log
from dzgui.views.pages.mods import Mods from dzgui.views.pages.mods import Mods
from dzgui.views.pages.offline import OfflineLoader
from dzgui.views.pages.options import Options from dzgui.views.pages.options import Options
from dzgui.views.pages.preconnect import PreConnectionAssistant from dzgui.views.pages.preconnect import PreConnectionAssistant
from dzgui.views.pages.servers import ServerNotebook from dzgui.views.pages.servers import ServerNotebook
@@ -110,8 +112,11 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore
self.connection = PreConnectionAssistant(MainController) self.connection = PreConnectionAssistant(MainController)
self.offline = OfflineLoader(MainController)
self.pages = { self.pages = {
self.help: NotebookPage.HELP, self.help: NotebookPage.HELP,
self.offline: NotebookPage.OFFLINE,
self.clog: NotebookPage.CHANGELOG, self.clog: NotebookPage.CHANGELOG,
self.keys: NotebookPage.KEYS, self.keys: NotebookPage.KEYS,
self.settings: NotebookPage.OPTIONS, self.settings: NotebookPage.OPTIONS,
+68
View File
@@ -0,0 +1,68 @@
from dzgui.strings import preconnect
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk # noqa E402
class ConnectBox(Gtk.Box):
def __init__(self) -> None:
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
self.back = Gtk.Button(
label=preconnect.back, halign=Gtk.Align.END, hexpand=True
)
# TODO: dynamically update this
self.ok = Gtk.Button(label="Launch", halign=Gtk.Align.END)
for button in self.back, self.ok:
self.add(button)
def get_back_button(self) -> Gtk.Button:
return self.back
def get_ok_button(self) -> Gtk.Button:
return self.ok
class PreconnectBox(Gtk.Box):
def __init__(self) -> None:
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
valign=Gtk.Align.END,
vexpand=True,
spacing=5,
)
self.button_hbox = ConnectBox()
self.ok = self.button_hbox.get_ok_button()
self.ok.set_label(preconnect.update_mods)
self.connect_last = Gtk.Button(
label=preconnect.connect_last,
halign=Gtk.Align.END,
tooltip_text=preconnect.connect_last_tooltip,
)
self.button_hbox.add(self.connect_last)
self.raise_window = Gtk.CheckButton(
# TODO: strings
label="Foreground DZGUI while downloading",
halign=Gtk.Align.END,
hexpand=True,
valign=Gtk.Align.END,
visible=False,
has_tooltip=True,
sensitive=False,
tooltip_text="Foreground the DZGUI window after mod downloads are queued",
active=True,
)
self.add(self.button_hbox)
self.add(self.raise_window)
def get_back_button(self) -> Gtk.Button:
return self.button_hbox.get_back_button()
def get_ok_button(self) -> Gtk.Button:
return self.ok
+8 -3
View File
@@ -11,6 +11,7 @@ from dzgui.const.constants import (
) )
from dzgui.const.enum import FilterMode from dzgui.const.enum import FilterMode
from dzgui.model.servers import ServerModelManager from dzgui.model.servers import ServerModelManager
from dzgui.util.format import format_exception
from dzgui.util.strings import all_maps from dzgui.util.strings import all_maps
from dzgui.views.components.maps_combo import MapsCombo from dzgui.views.components.maps_combo import MapsCombo
from dzgui.views.components.labels import BoldLabel from dzgui.views.components.labels import BoldLabel
@@ -333,6 +334,10 @@ class FilterPanel(Gtk.Box):
self.emitter.emit("map_selection_changed", name) self.emitter.emit("map_selection_changed", name)
def set_filters(self, filters: dict[str, bool]) -> None: def set_filters(self, filters: dict[str, bool]) -> None:
for check in self.button_grid.checks: try:
label = check.get_label() for check in self.button_grid.checks:
check.set_active(filters[label]) label = check.get_label()
check.set_active(filters[label])
except Exception as e:
msg = format_exception(e)
logger.warning(msg)
+16 -8
View File
@@ -1,3 +1,4 @@
from typing import Self
from dzgui.util import css from dzgui.util import css
import gi import gi
@@ -6,16 +7,23 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa from gi.repository import Gtk # noqa
# TODO: subclass Gtk.Frame directly
class HeadingFrame(Gtk.Box): class HeadingFrame(Gtk.Box):
def __init__(self, widget: Gtk.Widget, heading: str) -> None: def __init__(self, heading: str = "") -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL) super().__init__(orientation=Gtk.Orientation.VERTICAL)
label = Gtk.Label(label=heading) self.label = Gtk.Label(label=heading)
label.set_halign(Gtk.Align.START) self.label.set_halign(Gtk.Align.START)
css.add_class(label, "settings-subheading") css.add_class(self.label, "settings-subheading")
frame = Gtk.Frame(hexpand=True) self.frame = Gtk.Frame(hexpand=True)
frame.add(widget) self.frame.set_label_widget(self.label)
frame.set_label_widget(label)
self.add(frame) self.add(self.frame)
@classmethod
def new_with_widget_and_label(cls, widget: Gtk.Widget, label: str) -> Self:
n = cls()
n.frame.add(widget)
n.label.set_label(label)
return n
+2 -22
View File
@@ -1,5 +1,4 @@
from typing import Self, Union, TYPE_CHECKING from typing import Self, Union, TYPE_CHECKING
from warnings import deprecated
from dzgui.const.enum import NotebookPage, ServerTab from dzgui.const.enum import NotebookPage, ServerTab
from dzgui.util.strings import esc_to_return, question_to_return from dzgui.util.strings import esc_to_return, question_to_return
@@ -8,7 +7,7 @@ from dzgui.views.components.buttons import LoggerAlertsButton
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GObject # noqa E402 from gi.repository import Gtk # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.const.enum import ServerTab from dzgui.const.enum import ServerTab
@@ -34,14 +33,6 @@ class Statusbar(Gtk.Grid):
self.attach(self.statusbar, 0, 0, 3, 1) self.attach(self.statusbar, 0, 0, 3, 1)
self.attach_next_to(self.spinner, self.statusbar, Gtk.PositionType.RIGHT, 3, 1) self.attach_next_to(self.spinner, self.statusbar, Gtk.PositionType.RIGHT, 3, 1)
# TODO: pack version event box in right panel into hbox with update button
# TODO: spawns a modal or just jumps right to install page
# from dzgui.views.components.buttons import IconTextButton
# b = IconTextButton("dialog-information-symbolic", label="Updates available")
# b.set_halign(Gtk.Align.END)
# b.set_hexpand(True)
# self.attach_next_to(b, self.spinner, Gtk.PositionType.RIGHT, 3, 1)
self.players = "" self.players = ""
controller.get_menu().connect("generic_treesel_changed", self._help_row_changed) controller.get_menu().connect("generic_treesel_changed", self._help_row_changed)
@@ -78,6 +69,7 @@ class Statusbar(Gtk.Grid):
NotebookPage.CHANGELOG, NotebookPage.CHANGELOG,
NotebookPage.LOG, NotebookPage.LOG,
NotebookPage.CONNECTION, NotebookPage.CONNECTION,
NotebookPage.OFFLINE,
): ):
self.set_by_context(enum, esc_to_return) self.set_by_context(enum, esc_to_return)
return return
@@ -92,12 +84,6 @@ class Statusbar(Gtk.Grid):
self.set_by_context(enum, bar) self.set_by_context(enum, bar)
# TODO: unused
def _on_notebook_page_returned(
self, statusbar: Self, prior_context: NotebookPage
) -> None:
self.pop(prior_context)
def _on_server_row_changed(self, statusbar: Self) -> None: def _on_server_row_changed(self, statusbar: Self) -> None:
self.spinner.start() self.spinner.start()
@@ -132,12 +118,6 @@ class Statusbar(Gtk.Grid):
cid = self.statusbar.get_context_id(str(context)) cid = self.statusbar.get_context_id(str(context))
self.statusbar.pop(cid) self.statusbar.pop(cid)
@deprecated("currently unused")
# def get_text(self) -> str:
# area = self.statusbar.get_message_area()
# label = area.get_children()[0]
# return str(label.get_text())
def set_by_context( def set_by_context(
self, context: Union[NotebookPage, "ServerTab"], string: str self, context: Union[NotebookPage, "ServerTab"], string: str
) -> None: ) -> None:
+19 -10
View File
@@ -1,19 +1,20 @@
import sys import sys
from typing import Literal, TYPE_CHECKING from typing import Any, Literal, Self, TYPE_CHECKING
# import time # import time
from enum import Enum from enum import Enum
from typing import Any, Self
# TODO: import dialog titles # TODO: import dialog titles
from dzgui.api.mods import remove_stale_signatures as remove_stale from dzgui.api.mods import remove_stale_signatures as remove_stale
from dzgui.config.ipdb import get_ipdb from dzgui.config.ipdb import get_ipdb
from dzgui.const.constants import HEX_GREEN, HEX_RED from dzgui.const.constants import HEX_GREEN, HEX_RED
from dzgui.init.coords import get_local_coords from dzgui.init.coords import get_local_coords
from dzgui.init.dayz import is_dayz_installed
from dzgui.init.update import check_updates from dzgui.init.update import check_updates
from dzgui.const.constants import EXPAND, FILL from dzgui.const.constants import EXPAND, FILL
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import preboot from dzgui.strings import preboot, dialogs
from dzgui.util.format import format_exception
from dzgui.util.strings import dialog_header from dzgui.util.strings import dialog_header
from dzgui.util.symlink import rebuild_symlinks from dzgui.util.symlink import rebuild_symlinks
from dzgui.views.components.buttons import ClipboardButton from dzgui.views.components.buttons import ClipboardButton
@@ -57,6 +58,9 @@ class BootDialog(Gtk.Dialog):
self.view = Gtk.TreeView(enable_search=False, headers_visible=False) self.view = Gtk.TreeView(enable_search=False, headers_visible=False)
self.view.set_model(self.store) self.view.set_model(self.store)
# TODO: capture sigint in early dialogs
self.connect("key-press-event", self._on_keypress)
for i, column_title in enumerate(["Task", "State"]): for i, column_title in enumerate(["Task", "State"]):
renderer = Gtk.CellRendererText() renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(column_title, renderer, text=i) column = Gtk.TreeViewColumn(column_title, renderer, text=i)
@@ -118,6 +122,7 @@ class BootDialog(Gtk.Dialog):
self.error_box.hide() self.error_box.hide()
steps = [ steps = [
(StoredFunc(is_dayz_installed, self.xdg.config), preboot.dayz, False),
(StoredFunc(rebuild_symlinks, self.xdg.config), preboot.symlinks, False), (StoredFunc(rebuild_symlinks, self.xdg.config), preboot.symlinks, False),
( (
StoredFunc(remove_stale, self.xdg.config, self.xdg.version), StoredFunc(remove_stale, self.xdg.config, self.xdg.version),
@@ -125,6 +130,7 @@ class BootDialog(Gtk.Dialog):
False, False,
), ),
(StoredFunc(get_ipdb, self.xdg.ips), preboot.geo, False), (StoredFunc(get_ipdb, self.xdg.ips), preboot.geo, False),
# TODO: handle IP DB failure and use coords fallback
(StoredFunc(get_local_coords, self.xdg.ips), preboot.coords, True), (StoredFunc(get_local_coords, self.xdg.ips), preboot.coords, True),
(StoredFunc(check_updates, self.version), preboot.updates, True), (StoredFunc(check_updates, self.version), preboot.updates, True),
] ]
@@ -134,6 +140,10 @@ class BootDialog(Gtk.Dialog):
GLib.timeout_add(100, self.pulse_spinner) GLib.timeout_add(100, self.pulse_spinner)
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
if event.keyval == Gdk.KEY_Escape:
self.exit_button.emit("clicked")
def pulse_spinner(self) -> Literal[True]: def pulse_spinner(self) -> Literal[True]:
for row in self.store: for row in self.store:
if row[2]: if row[2]:
@@ -175,16 +185,15 @@ class BootDialog(Gtk.Dialog):
self.thread_man.set_cleanup_func(callback) self.thread_man.set_cleanup_func(callback)
except Exception as e: except Exception as e:
self.failed = True self.failed = True
# TODO: generic exception wrapper self.exception = format_exception(e)
self.exception = f"{type(e).__name__}: {e}"
callback = StoredFunc(self.update_status, Success.FAIL) callback = StoredFunc(self.update_status, Success.FAIL)
self.thread_man.set_cleanup_func(callback) self.thread_man.set_cleanup_func(callback)
def update_task(self, task: str) -> None: def update_task(self, task: str) -> None:
self.store.append((task, "Running", True, 0)) self.store.append((task, dialogs.running, True, 0))
def update_status(self, state: Success) -> None: def update_status(self, state: Success) -> None:
d = {Success.OK: "OK", Success.FAIL: "FAILED"} d = {Success.OK: dialogs.ok, Success.FAIL: dialogs.failed}
msg = d[state] msg = d[state]
self.store[len(self.store) - 1][1] = msg self.store[len(self.store) - 1][1] = msg
self.iter_step() self.iter_step()
@@ -197,7 +206,7 @@ class BootDialog(Gtk.Dialog):
it: Gtk.TreeIter, it: Gtk.TreeIter,
data: Any, data: Any,
) -> None: ) -> None:
if model[it][1] == "Running": if model[it][1] == dialogs.running:
cell.set_property("visible", True) cell.set_property("visible", True)
else: else:
cell.set_property("visible", False) cell.set_property("visible", False)
@@ -214,9 +223,9 @@ class BootDialog(Gtk.Dialog):
state = model[it][1] state = model[it][1]
if column.get_sort_column_id() != 1: if column.get_sort_column_id() != 1:
return return
if state == "OK": if state == dialogs.ok:
cell.set_property(prop, HEX_GREEN) cell.set_property(prop, HEX_GREEN)
elif state == "FAILED": elif state == dialogs.failed:
cell.set_property(prop, HEX_RED) cell.set_property(prop, HEX_RED)
else: else:
cell.set_property(prop, None) cell.set_property(prop, None)
+25
View File
@@ -33,3 +33,28 @@ class FilePicker(Gtk.FileChooserDialog):
self.destroy() self.destroy()
return Path(file) return Path(file)
return None return None
class FolderPicker(Gtk.FileChooserDialog):
def __init__(self, parent: Gtk.Window) -> None:
super().__init__(
title=picker.title,
action=Gtk.FileChooserAction.SELECT_FOLDER,
parent=parent,
resizable=True,
)
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
self.add_buttons("_OK", Gtk.ResponseType.OK)
self.set_default_response(Gtk.ResponseType.OK)
def pick_folder(self) -> Path | None:
res = self.run()
if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
self.destroy()
return None
if res == Gtk.ResponseType.OK:
folder = self.get_current_folder()
if folder is not None:
self.destroy()
return Path(folder)
return None
+2
View File
@@ -44,6 +44,8 @@ class ContextMixin(TreeView):
self.context_menu.connect("key-press-event", self._on_key) self.context_menu.connect("key-press-event", self._on_key)
for row in group.value: for row in group.value:
if row is None:
return False
item = self._process_dynamic_row(row) item = self._process_dynamic_row(row)
self.context_menu.append(item) self.context_menu.append(item)
+22 -7
View File
@@ -11,21 +11,36 @@ if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
class Mods(Gtk.Box):
class Mods(NoOverlayScrolledWindow):
def __init__(self, controller: "Controller") -> None: def __init__(self, controller: "Controller") -> None:
super().__init__() super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.box = NoOverlayScrolledWindow()
self.tree = ModTreeView(controller)
self.tree.set_vexpand(True)
self.box.add(self.tree)
self.controller = controller self.controller = controller
self.emitter = controller.get_emitter()
self.tree = ModTreeView(controller)
self.add(self.tree)
self.controller.register_widget("modtreeview", self.tree) self.controller.register_widget("modtreeview", self.tree)
self.emitter = controller.get_emitter()
self.offline_button = Gtk.Button(
label="Play offline",
halign=Gtk.Align.START,
margin_top=10,
margin_bottom=10,
)
self.offline_button.connect("clicked", self._on_offline_clicked)
self.add(self.offline_button)
self.add(self.box)
self.connect("map", self._on_map) self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap) self.connect("unmap", self._on_unmap)
def _on_offline_clicked(self, button: Gtk.Button) -> None:
self.controller.open_offline()
def _on_unmap(self, widget: Self) -> None: def _on_unmap(self, widget: Self) -> None:
self.emitter.emit("mod_page_toggled", False) self.emitter.emit("mod_page_toggled", False)
+378
View File
@@ -0,0 +1,378 @@
from __future__ import annotations
from pathlib import Path
from typing import Self, Sequence, TYPE_CHECKING, Union
from dzgui.util import css
import dzgui.api.pefile as PeFile
from dzgui.const.constants import (
APPID_DAYZ,
APPID_DAYZ_EXP,
APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN,
FOLDER,
)
from dzgui.const.enum import NotebookPage, Preferences
from dzgui.managers.offline import OfflineManager
from dzgui.strings import generic, offline
from dzgui.views.components.buttons import IconTextButton
from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import OfflineModTreeView
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
from dzgui.model.model_factory import FastInsertListStore
class GenericBox(Gtk.Box):
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
super().__init__(orientation=orientation, spacing=spacing)
def extend(self, els: Sequence[Gtk.Widget]) -> None:
for el in els:
self.add(el)
class HBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=spacing)
class VBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
class PageHeading(Gtk.Label):
def __init__(self, label: str) -> None:
super().__init__(label=label, halign=Gtk.Align.CENTER)
css.add_class(self, "page-heading")
class FolderHBox(HBox):
def __init__(self, btn_label: str) -> None:
super().__init__(spacing=10)
self.set_margin_start(10)
self.set_margin_end(10)
self.set_margin_bottom(5)
# TODO: alternate class for left-aligned icons
self.button = IconTextButton(FOLDER, btn_label)
self.button.set_halign(Gtk.Align.START)
self.button.set_image_position(Gtk.PositionType.LEFT)
self.scrolled_label = Gtk.ScrolledWindow(propagate_natural_width=True, halign=Gtk.Align.START)
self.label = Gtk.Label()
self.scrolled_label.add(self.label)
self.extend([self.button, self.scrolled_label])
def get_button(self) -> Gtk.Button:
return self.button
def set_label(self, label: str) -> None:
# TODO: strings
prefix = "Current folder: "
self.label.set_label(prefix + label)
def hide_label(self) -> None:
self.label.hide()
class ModFrame(HeadingFrame):
def __init__(self, parent: OfflineLoader, controller: "Controller", label: str) -> None:
super().__init__(heading=label)
self.parent = parent
self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.tree = OfflineModTreeView(controller)
self.tree.set_model(None)
self.tree.set_valign(Gtk.Align.FILL)
self.scrolled = NoOverlayScrolledWindow()
self.scrolled.set_margin_end(5)
self.scrolled.set_vexpand(True)
self.scrolled.add(self.tree)
self.status = Gtk.Label(
halign=Gtk.Align.START, margin_start=5, margin_top=3, margin_bottom=3
)
self.tree_vbox = VBox()
self.tree_vbox.extend([self.scrolled, self.status])
self.vbox.pack_end(self.tree_vbox, expand=True, fill=True, padding=3)
self.frame.add(self.vbox)
sel = self.tree.get_selection()
sel.connect("changed", self._on_selection_changed)
# TODO: inherit icons from warnings area in preconnect dialog
# TODO: strings
self.error_label = Gtk.Label(label="No mods found", halign=Gtk.Align.START, margin_start=10, margin_bottom=5)
self.vbox.pack_end(self.error_label, expand=True, fill=True, padding=3)
def set_error(self, msg: str) -> None:
self.error_label.set_label(msg)
self.error_label.show()
def hide_errors(self) -> None:
self.error_label.hide()
def start_empty(self) -> None:
self.error_label.show()
self.collapse_tree()
def hide_all(self) -> None:
self.error_label.hide()
self.collapse_tree()
# TODO: warnings area
# name collision within custom mods
def get_mods(self) -> list[str]:
model, treeiters = self.tree.get_selection().get_selected_rows()
if model is None:
return []
return [model[_iter][1] for _iter in treeiters]
def collapse_tree(self) -> None:
self.tree_vbox.hide()
def get_tree(self) -> OfflineModTreeView:
return self.tree
def pack(self, widget: Gtk.Widget) -> None:
self.vbox.pack_start(widget, expand=False, fill=False, padding=5)
def set_model(self, model: Union["FastInsertListStore", None]) -> None:
self.tree.set_model(model)
self.tree.mod_man.store = model
self.set_cursor()
def _on_selection_changed(self, sel: Gtk.TreeSelection) -> None:
model, rows = sel.get_selected_rows()
if len(rows) == 0:
status = generic.selectable_tree
else:
status = f"Mods selected: {len(rows)}"
self.status.set_label(status)
self.parent.check_button()
def set_cursor(self) -> None:
path = Gtk.TreePath.new_from_indices([0])
self.tree.set_cursor(path)
self.tree.get_selection().unselect_all()
class CustomModFrame(ModFrame):
def __init__(
self, parent: OfflineLoader, controller: "Controller", heading: str
) -> None:
super().__init__(parent, controller, heading)
self.parent = parent
self.controller = controller
self.emitter = controller.get_emitter()
# TODO: descriptive text here explaining how this area works
self.custom_hbox = FolderHBox(offline.custom_button)
self.custom_hbox.get_button().connect("clicked", self._on_custom_button_clicked)
self.pack(self.custom_hbox)
self.emitter.connect("custom_mods_loaded", self._on_custom_mods_loaded)
def hide_tree(self) -> None:
# TODO: no mods message is different from collision message
self.set_error(offline.no_mods)
self.custom_hbox.hide_label()
self.tree_vbox.hide()
def present_tree(self, folder: str, store: "FastInsertListStore") -> None:
self.custom_hbox.set_label(folder)
self.tree.set_model(store)
self.tree_vbox.show()
self.hide_errors()
def _on_custom_mods_loaded(
self,
emitter: "Emitter",
store: "FastInsertListStore",
folder: str,
) -> None:
if len(store) == 0:
self.hide_tree()
else:
self.present_tree()
def _on_custom_button_clicked(self, button: Gtk.Button) -> None:
local_mods = self.get_mods()
self.parent.offline_man.get_custom_mods(local_mods)
class MissionFrame(HeadingFrame):
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
super().__init__(heading=offline.mission_frame)
self.parent = parent
self.controller = controller
self.emitter = controller.get_emitter()
self.mission_hbox = FolderHBox(offline.mission_button)
self.mission_button = self.mission_hbox.get_button()
self.mission_button.connect("clicked", self._on_mission_button_clicked)
self.warning = Gtk.Label(halign=Gtk.Align.START, margin_start=10, margin_bottom=5)
self.vbox = VBox()
self.vbox.extend([self.mission_hbox, self.warning])
self.frame.add(self.vbox)
self.emitter.connect("custom_mission_loaded", self._on_mission_loaded)
def _on_mission_loaded(self, emitter: "Emitter", folder: str, is_valid: bool) -> None:
self.mission_hbox.set_label(folder)
if not is_valid:
self.warning.show()
self.warning.set_label(offline.no_mission)
else:
self.warning.hide()
def _on_mission_button_clicked(self, button: Gtk.Button) -> None:
self.parent.offline_man.get_mission()
class RadioFrame(HeadingFrame):
def __init__(self, controller: "Controller") -> None:
super().__init__(heading=offline.version)
self.controller = controller
self.id_map = {APPNAME_DAYZ: APPID_DAYZ, APPNAME_DAYZ_EXP_HUMAN: APPID_DAYZ_EXP}
self.appid = APPID_DAYZ
self.dayz = Gtk.RadioButton.new_with_label(None, APPNAME_DAYZ)
self.dayz_exp = Gtk.RadioButton.new_with_label_from_widget(
self.dayz, APPNAME_DAYZ_EXP_HUMAN
)
self.radio_box = HBox()
self.radio_box.extend([self.dayz, self.dayz_exp])
self.radio_box.set_margin_start(10)
self.radio_box.set_margin_bottom(5)
self.frame.add(self.radio_box)
# TODO: abstract out of here
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
steam_path = Path(default_steam_path)
dayz_exp = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
self.dayz.connect("toggled", self._on_radio_toggled)
if dayz_exp is None:
self.dayz_exp.set_sensitive(False)
def _on_radio_toggled(self, radio: Gtk.RadioButton) -> None:
for el in self.dayz, self.dayz_exp:
if el.get_active():
label = el.get_label()
self.appid = self.id_map[label]
def get_appid(self) -> int:
return self.appid
class OfflineLoader(Gtk.Box):
def __init__(self, controller: "Controller"):
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
margin_start=10,
margin_end=10,
)
self.controller = controller
self.controller.register_widget("offline_loader", self)
self.offline_man = OfflineManager(controller)
self.add(PageHeading(offline.heading))
self.local_frame = ModFrame(self, controller, offline.local_frame)
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
self.custom_tree = OfflineModTreeView(controller)
self.mission_frame = MissionFrame(self, controller)
self.radio_frame = RadioFrame(controller)
self.scrollable = Gtk.ScrolledWindow(
vexpand=True, propagate_natural_height=True
)
self.content_box = VBox(spacing=5)
self.content_box.extend(
[
self.local_frame,
self.custom_frame,
self.mission_frame,
self.radio_frame,
]
)
# TODO: share ConnectBox class with preconnect dialog?
self.button_box = HBox(spacing=5)
self.button_box.set_halign(Gtk.Align.END)
self.button_box.set_margin_top(5)
self.back = Gtk.Button(label="Back")
self.ok = Gtk.Button(label="Launch", sensitive=False)
self.back.connect("clicked", self._on_back_clicked)
self.ok.connect("clicked", self._on_ok_clicked)
self.connect("key-press-event", self._on_keypress)
self.button_box.extend([self.back, self.ok])
self.scrollable.add(self.content_box)
self.add(self.scrollable)
self.add(self.button_box)
def check_button(self) -> None:
local_mods = self.local_frame.get_mods()
custom_mods = self.custom_frame.get_mods()
if len(local_mods) == 0 and len(custom_mods) == 0:
self.ok.set_sensitive(False)
else:
self.ok.set_sensitive(True)
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
if event.keyval == Gdk.KEY_Escape:
self.back.emit("clicked")
def populate(self, store: Union["FastInsertListStore", None]) -> None:
self.local_frame.set_model(store)
if store is None:
self.local_frame.start_empty()
else:
self.local_frame.hide_errors()
# NOTE: suppress custom tree until explicitly loaded
self.custom_frame.hide_all()
def _on_back_clicked(self, button: Gtk.Button) -> None:
self.controller.open_page(NotebookPage.MODS)
def _on_ok_clicked(self, button: Gtk.Button) -> None:
# TODO: grab all values in one pass
# appid = self.radio_frame.get_appid()
# mission = self.mission_frame.get_mission()
# local_mods = self.local_frame.get_mods()
# custom_mods = self.custom_frame.get_mods()
# cf. api.mods._hash(uid, use_custom=True)
# TODO: set up all symlinks prior to launch, including custom ones
# self.offline_man.setup(appid, mission, local_mods, custom_mods)
pass
+7 -5
View File
@@ -187,12 +187,14 @@ class Options(Gtk.Box):
if prefs.is_debug: if prefs.is_debug:
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT) grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
for frame in [ for pair in [
HeadingFrame(api_box, strings.options.api_keys), (api_box, strings.options.api_keys),
HeadingFrame(prefs_grid, strings.options.prefs), (prefs_grid, strings.options.prefs),
HeadingFrame(mods_grid, strings.options.mods), (mods_grid, strings.options.mods),
HeadingFrame(version_grid, strings.options.version), (version_grid, strings.options.version),
]: ]:
frame = HeadingFrame.new_with_widget_and_label(*pair)
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT) grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
row += 1 row += 1
+10 -4
View File
@@ -154,7 +154,7 @@ class PreConnectionAssistant(Gtk.Box):
self.progress_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15) self.progress_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
self.progress_box.add(self.mod_count) self.progress_box.add(self.mod_count)
self.scrolled = Gtk.ScrolledWindow() self.scrolled = Gtk.ScrolledWindow(margin_end=10)
self.scrolled.add(self.tree) self.scrolled.add(self.tree)
self.scrolled.set_size_request(600, 400) self.scrolled.set_size_request(600, 400)
@@ -166,7 +166,9 @@ class PreConnectionAssistant(Gtk.Box):
self.mods_placeholder = Placeholder("This server has no mods.") self.mods_placeholder = Placeholder("This server has no mods.")
self.tree_box.add(self.mods_placeholder) self.tree_box.add(self.mods_placeholder)
self.tree_frame = HeadingFrame(self.tree_box, preconnect.mods) self.tree_frame = HeadingFrame.new_with_widget_and_label(
self.tree_box, preconnect.mods
)
# TODO: abstract into components # TODO: abstract into components
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
@@ -175,7 +177,9 @@ class PreConnectionAssistant(Gtk.Box):
self.warning_placeholder = Placeholder("No warnings.") self.warning_placeholder = Placeholder("No warnings.")
self.warning_box.add(self.warning_tree) self.warning_box.add(self.warning_tree)
self.warning_box.add(self.warning_placeholder) self.warning_box.add(self.warning_placeholder)
self.warning_frame = HeadingFrame(self.warning_box, preconnect.warnings) self.warning_frame = HeadingFrame.new_with_widget_and_label(
self.warning_box, preconnect.warnings
)
self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.error_tree = MaskedTree(ERROR) self.error_tree = MaskedTree(ERROR)
@@ -183,7 +187,9 @@ class PreConnectionAssistant(Gtk.Box):
self.error_placeholder = Placeholder("No errors.") self.error_placeholder = Placeholder("No errors.")
self.error_box.add(self.error_tree) self.error_box.add(self.error_tree)
self.error_box.add(self.error_placeholder) self.error_box.add(self.error_placeholder)
self.error_frame = HeadingFrame(self.error_box, preconnect.errors) self.error_frame = HeadingFrame.new_with_widget_and_label(
self.error_box, preconnect.errors
)
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
self.box.add(self.title) self.box.add(self.title)
+11 -4
View File
@@ -4,7 +4,7 @@ from typing import Any, Optional, TYPE_CHECKING
from warnings import deprecated from warnings import deprecated
from dzgui.const.constants import APP_NAME, SEPARATOR from dzgui.const.constants import APP_NAME, SEPARATOR
from dzgui.util.keys import is_navkey from dzgui.util.keys import is_ctrl_mask, is_navkey
from dzgui.views.mixins.cursor_mixin import CursorMixin from dzgui.views.mixins.cursor_mixin import CursorMixin
import gi import gi
@@ -85,10 +85,17 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
model, rows = sel.get_selected_rows() model, rows = sel.get_selected_rows()
return [model[row] for row in rows] return [model[row] for row in rows]
def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None: def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> bool:
from dzgui.views.trees.tree_servers import ServerTreeView
# NOTE: suppress popup search and delegate binding to server view (cf. request_keyword_focus)
if not isinstance(treeview, ServerTreeView):
if is_ctrl_mask(event) and event.keyval == Gdk.KEY_f:
return True
if is_navkey(event.keyval): if is_navkey(event.keyval):
if self.get_model() is None: if self.get_model() is None:
return return True
if self.sel_blocked is False: if self.sel_blocked is False:
self.sel_blocked = True self.sel_blocked = True
self.controller.suppress_signal( self.controller.suppress_signal(
@@ -98,7 +105,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
True, True,
) )
self._vim_nav(event) self._vim_nav(event)
return return False
def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None: def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None:
""" """
+21 -2
View File
@@ -3,6 +3,7 @@ from typing import Any, TYPE_CHECKING
from dzgui.const.constants import APP_NAME, HEX_RED from dzgui.const.constants import APP_NAME, HEX_RED
from dzgui.const.enum import ContextMenuGroup from dzgui.const.enum import ContextMenuGroup
from dzgui.managers.mods import ModManager
from dzgui.util import strings, localize from dzgui.util import strings, localize
from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.mixins.context_mixin import ContextMixin
from dzgui.views.mixins.mods_mixin import ModsMixin from dzgui.views.mixins.mods_mixin import ModsMixin
@@ -24,14 +25,15 @@ logger = logging.getLogger(APP_NAME)
class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
def __init__(self, controller: "Controller") -> None: def __init__(self, controller: "Controller") -> None:
super().__init__(controller, menu=ContextMenuGroup.MOD) super().__init__(controller, menu=ContextMenuGroup.MOD)
self.controller = controller self.controller = controller
self.mod_man = ModManager(self, controller)
emitter = self.controller.get_emitter() emitter = self.controller.get_emitter()
emitter.connect("mods_updated", self._on_mods_updated) emitter.connect("mods_updated", self._on_mods_updated)
emitter.connect("mods_highlighted", self._on_mods_highlighted) emitter.connect("mods_highlighted", self._on_mods_highlighted)
self.set_fixed_height_mode(True) self.set_fixed_height_mode(True)
self.set_headers_visible(True)
self.set_model(None) self.set_model(None)
for i, column_title in enumerate(strings.mod_cols): for i, column_title in enumerate(strings.mod_cols):
@@ -58,12 +60,19 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
self.connect("button-press-event", self.present_menu) self.connect("button-press-event", self.present_menu)
self.connect("key-press-event", self.present_menu) self.connect("key-press-event", self.present_menu)
def get_mod_man(self) -> ModManager:
return self.mod_man
def load_mods(self) -> None:
self.mod_man.load_mods()
def _on_mods_highlighted(self, emitter: "Emitter") -> None: def _on_mods_highlighted(self, emitter: "Emitter") -> None:
self.get_selection().unselect_all() self.get_selection().unselect_all()
def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None: def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None:
if mods < 1: if mods < 1:
return return
self.set_headers_visible(True)
path = Gtk.TreePath.new_from_indices([0]) path = Gtk.TreePath.new_from_indices([0])
self.set_cursor(path) self.set_cursor(path)
@@ -106,3 +115,13 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
val = model[it][3] val = model[it][3]
formatted = localize.number(val) formatted = localize.number(val)
cell.set_property("text", formatted) cell.set_property("text", formatted)
# NOTE: reapply color property after float conversion
self._format_color(column, cell, model, it, data)
def set_menu(self, context: ContextMenuGroup) -> None:
self.menu = context
class OfflineModTreeView(ModTreeView):
def __init__(self, controller: "Controller") -> None:
super().__init__(controller)
+1 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [ authors = [
{name = "aclist"} {name = "aclist"}
] ]
version = "7.0.0b9" version = "7.0.0b12"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
+5
View File
@@ -0,0 +1,5 @@
protocol = 1;
publishedid = 1234567890;
name = "ModName";
timestamp = 638278452000000000;
hash = "a1b2c3d4e5f6g7h8i9j0";
+5
View File
@@ -0,0 +1,5 @@
protocol = 1
publishedid = 1234567890
name = "ModName"
timestamp = 638278452000000000
hash = "a1b2c3d4e5f6g7h8i9j0"
+5
View File
@@ -0,0 +1,5 @@
protocol=1
publishedid=1234567890
name="ModName"
timestamp=638278452000000000
hash="a1b2c3d4e5f6g7h8i9j0"
+5
View File
@@ -0,0 +1,5 @@
protocol=1
publishedid=1234567890
name=ModName
timestamp=638278452000000000
hash=a1b2c3d4e5f6g7h8i9j0
+5
View File
@@ -0,0 +1,5 @@
protocol=1;
publishedid=1234567890;
name=ModName;
timestamp=638278452000000000;
hash=a1b2c3d4e5f6g7h8i9j0;
+5
View File
@@ -0,0 +1,5 @@
protocol=1
publishedid=1234567890;
name="ModName";
timestamp=638278452000000000;
hash= a1b2c3d4e5f6g7h8i9j0;
+3
View File
@@ -0,0 +1,3 @@
from dzgui.api.mods import tokenize
print(tokenize("meta4.cpp"))
+17
View File
@@ -0,0 +1,17 @@
import pytest
from dzgui.api.mods import tokenize
from tests.fixtures import fixture_path
@pytest.mark.mods
@pytest.mark.parametrize("fixture", [
fixture_path("cpp/meta1.cpp"),
fixture_path("cpp/meta2.cpp"),
fixture_path("cpp/meta3.cpp"),
fixture_path("cpp/meta4.cpp"),
fixture_path("cpp/meta5.cpp"),
fixture_path("cpp/meta6.cpp"),
]
)
def test_modmeta(fixture: str) -> None:
meta = tokenize(fixture)
assert meta["name"] == "ModName"