mirror of
https://github.com/aclist/dztui.git
synced 2026-11-05 17:45:10 +01:00
Compare commits
56
Commits
7.0.0b10
...
ad85bb7343
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad85bb7343 | ||
|
|
77df66979a | ||
|
|
d4825390f7 | ||
|
|
d5a857fe3a | ||
|
|
241df72bf3 | ||
|
|
4f51d962e7 | ||
|
|
98b550cec6 | ||
|
|
20c850a977 | ||
|
|
5a8d85ac92 | ||
|
|
450d54d7fd | ||
|
|
65fb07c8da | ||
|
|
8123893a90 | ||
|
|
3c9b5c9ed3 | ||
|
|
f9be6dfae4 | ||
|
|
586f7e6c66 | ||
|
|
49eb3caf0a | ||
|
|
56da5717fa | ||
|
|
9983e91526 | ||
|
|
820deb2955 | ||
|
|
0137bf4610 | ||
|
|
687b1a915e | ||
|
|
401c8625ad | ||
|
|
8503d7e698 | ||
|
|
b35b6148bc | ||
|
|
45fd180dcb | ||
|
|
486da7749a | ||
|
|
606df3423c | ||
|
|
ab439144ea | ||
|
|
db14c5252c | ||
|
|
7d8b3b402f | ||
|
|
4506a0cfda | ||
|
|
978edc5f5f | ||
|
|
2466887014 | ||
|
|
6050a370f8 | ||
|
|
d4a99f8229 | ||
|
|
46fba4eef1 | ||
|
|
bc1118a29f | ||
|
|
775f582ea6 | ||
|
|
2dadd70be3 | ||
|
|
3473f03350 | ||
|
|
e081d0ad16 | ||
|
|
e6026e84f9 | ||
|
|
b17b8c24ff | ||
|
|
4b9687a84d | ||
|
|
0c0f280d15 | ||
|
|
831bd9145d | ||
|
|
0070fe4061 | ||
|
|
9d1443cf65 | ||
|
|
c4cc9f04c4 | ||
|
|
bcd59162b1 | ||
|
|
2f0bac75ac | ||
|
|
9191f43c8e | ||
|
|
2419fedd19 | ||
|
|
83a7996189 | ||
|
|
9d3aa8d9f5 | ||
|
|
0de0dd4a03 |
No files matched your search
@@ -40,6 +40,7 @@
|
||||
- Preboot progress dialog
|
||||
- Choose to jump into splash screen instead of server
|
||||
- Collapsible connection panel
|
||||
- Play offline (load mods directly)
|
||||
|
||||
## Changed
|
||||
- Conform to PEP 440 versioning for beta versions
|
||||
|
||||
+41
-39
@@ -1,6 +1,6 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import shlex
|
||||
import re
|
||||
|
||||
from concurrent.futures import wait
|
||||
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()]
|
||||
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:
|
||||
mod = file / "meta.cpp"
|
||||
if mod.exists() is False:
|
||||
def tokenize(mod: Path) -> dict[Any] | None:
|
||||
file = mod.joinpath("meta.cpp")
|
||||
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
|
||||
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:
|
||||
s = 0
|
||||
@@ -87,33 +80,42 @@ def get_mod_size(path: Path) -> float:
|
||||
return size
|
||||
|
||||
|
||||
def get_delimited_mods(steam_path: Path) -> list[Any]:
|
||||
workshop_path = get_local_mod_path(steam_path)
|
||||
mods = get_local_mods(workshop_path)
|
||||
def get_custom_mods(path: Path) -> list[Any]:
|
||||
mods = get_local_mods(path)
|
||||
# TODO: error handling
|
||||
return parse_mods(mods, use_custom=True)
|
||||
|
||||
|
||||
def parse_mods(mods: list[Path], use_custom: bool = False) -> list[Any]:
|
||||
clean = []
|
||||
for mod in mods:
|
||||
mod_dir = mod.name
|
||||
symlink = _hash(mod_dir)
|
||||
# FIXME: malformed .cpp files could break this
|
||||
# mention that mods may be downloading
|
||||
meta = parse_meta(mod)
|
||||
symlink = _hash(mod_dir, use_custom)
|
||||
meta = tokenize(mod)
|
||||
if meta is None:
|
||||
continue
|
||||
size = get_mod_size(mod)
|
||||
# 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())
|
||||
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:
|
||||
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.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:
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
from dzgui.api.bm import map_id_to_record
|
||||
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
|
||||
from dzgui.const.endpoints import STEAM_SERVERS
|
||||
from dzgui.util.format import format_exception
|
||||
from dzgui.util import strings
|
||||
|
||||
import a2s
|
||||
@@ -272,8 +273,9 @@ def source_info_to_dict(ip: str, qport: int, info: "SourceInfo") -> dict[str, An
|
||||
res["ping"] = ping
|
||||
return res
|
||||
except Exception as e:
|
||||
# TODO: generalized function
|
||||
logger.critical(f"{type(e).__name__}: {e} ({ip}:{qport})")
|
||||
msg = format_exception(e)
|
||||
msg += f" ({ip}:{qport})"
|
||||
logger.critical(msg)
|
||||
raise e
|
||||
|
||||
|
||||
|
||||
+17
-1
@@ -146,7 +146,7 @@ def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> i
|
||||
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"""
|
||||
concat = concat_mods(mods)
|
||||
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])
|
||||
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:
|
||||
resolved_path = path / "config" / "loginusers.vdf"
|
||||
|
||||
@@ -35,6 +35,7 @@ HEX_ORANGE = "#FFAC1C"
|
||||
CARET_DOWN = "go-down-symbolic"
|
||||
CARET_UP = "go-up-symbolic"
|
||||
CLIPBOARD = "edit-copy-symbolic"
|
||||
FOLDER = "folder-symbolic"
|
||||
ERROR = "dialog-error-symbolic"
|
||||
HELP_BUBBLE = "help-about-symbolic"
|
||||
INPUT_KEYBOARD = "input-keyboard-symbolic"
|
||||
|
||||
@@ -103,6 +103,7 @@ class Preferences(EnumWithAttrs):
|
||||
|
||||
|
||||
class NotebookPage(EnumWithAttrs):
|
||||
OFFLINE = {"crumbs": strings.crumbs.offline, "statusbar": True}
|
||||
CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": True}
|
||||
DEVELOPERS = {"crumbs": strings.crumbs.developers, "statusbar": False}
|
||||
HELP = {"crumbs": strings.crumbs._help, "statusbar": True}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository.Gtk import ListStore # noqa E402
|
||||
from gi.repository import Gdk, GObject # noqa E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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
|
||||
@@ -154,3 +154,11 @@ class Emitter(GObject.GObject):
|
||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||
def lan_page_initialized(self) -> None:
|
||||
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
@@ -20,7 +20,6 @@ from dzgui.controllers.emitter import Emitter
|
||||
from dzgui.managers.config import ConfigManager
|
||||
from dzgui.managers.connection import ConnectionManager
|
||||
from dzgui.managers.contextmenu import ContextMenuManager
|
||||
from dzgui.managers.mods import ModManager
|
||||
from dzgui.managers.notes import NoteManager
|
||||
from dzgui.model.servers import ServerModelManager
|
||||
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.right_panel import RightPanel
|
||||
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.preconnect import PreConnectionAssistant
|
||||
from dzgui.views.pages.servers import ServerNotebook
|
||||
from dzgui.views.trees.tree_log import LogTreeView
|
||||
from dzgui.views.trees.tree_menu import MenuTreeView
|
||||
from dzgui.views.trees.tree_mods import ModTreeView
|
||||
from dzgui.views.trees.tree_mods import ModTreeView, OfflineModTreeView
|
||||
from dzgui.views.trees.tree_servers import ServerTreeView
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ class AppNavigation:
|
||||
filters: "FilterPanel"
|
||||
preconnect: "PreConnectionAssistant"
|
||||
options: "Options"
|
||||
offline_loader: "OfflineLoader"
|
||||
|
||||
|
||||
class Controller(GObject.GObject):
|
||||
@@ -218,19 +219,29 @@ class Controller(GObject.GObject):
|
||||
open_user_workshop(uid, client)
|
||||
|
||||
def load_mods(self) -> None:
|
||||
self.mod_man = ModManager(self)
|
||||
self.mediator.modtreeview.load_mods()
|
||||
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
self.mod_man.delete_mods()
|
||||
def 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:
|
||||
return self.mediator.modtreeview.get_model()
|
||||
@@ -257,7 +268,8 @@ class Controller(GObject.GObject):
|
||||
dialog.run()
|
||||
|
||||
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]]:
|
||||
servers = self.get_servers()
|
||||
@@ -533,3 +545,9 @@ class Controller(GObject.GObject):
|
||||
self.clear_cancel_event()
|
||||
return True
|
||||
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
@@ -1,12 +1,10 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.config.query import lookup
|
||||
from dzgui.const.constants import APPID_DAYZ, APP_NAME, LIBRARYFOLDERS_PATH
|
||||
from dzgui.const.enum import Preferences
|
||||
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
||||
|
||||
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)
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
EarlyAlertDialog(str(e))
|
||||
sys.exit(1)
|
||||
raise e
|
||||
@@ -11,7 +11,6 @@ from dzgui.const.enum import Preferences
|
||||
from dzgui.config.query import lookup
|
||||
from dzgui.config.userprefs import UserPrefs
|
||||
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.migrate import (
|
||||
has_new_config,
|
||||
@@ -124,18 +123,9 @@ def main() -> None:
|
||||
if has_steam_client() is False:
|
||||
EarlyAlertDialog(init.requires_steam)
|
||||
|
||||
is_dayz_installed(XDG.config)
|
||||
|
||||
bootwin = BootWindow(XDG, version)
|
||||
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)
|
||||
prefs = UserPrefs(
|
||||
is_steam_deck=_is_steam_deck,
|
||||
|
||||
@@ -238,8 +238,7 @@ class ConnectionManager:
|
||||
def query_modlist_and_present(self, record: Servers.Record) -> None:
|
||||
try:
|
||||
mods = self._query_modlist(record)
|
||||
except Exception as e:
|
||||
print(f"{type(e).__name__}: {e}")
|
||||
except Exception:
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._server_timeout), destroy_first=True
|
||||
)
|
||||
@@ -266,7 +265,7 @@ class ConnectionManager:
|
||||
playername = self.controller.query_config(Preferences.NAME)
|
||||
client = self.controller.query_config(Preferences.CLIENT)
|
||||
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:
|
||||
rc = connect(client, addr, self.appid, playername, self.remote_mod_ids)
|
||||
if rc != 0:
|
||||
|
||||
@@ -8,7 +8,7 @@ from dzgui.util.clip import copy_clipboard
|
||||
from dzgui.util.open_links import open_workshop_page
|
||||
|
||||
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_servers import ServerTreeView
|
||||
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
||||
@@ -27,7 +27,13 @@ if TYPE_CHECKING:
|
||||
class ContextMenuManager:
|
||||
def __init__(
|
||||
self,
|
||||
treeview: LogTreeView | ModTreeView | ServerTreeView | ServerModTreeView,
|
||||
treeview: (
|
||||
LogTreeView
|
||||
| ModTreeView
|
||||
| ServerTreeView
|
||||
| ServerModTreeView
|
||||
| OfflineModTreeView
|
||||
),
|
||||
controller: "Controller",
|
||||
) -> None:
|
||||
self.controller = controller
|
||||
@@ -72,10 +78,10 @@ class ContextMenuManager:
|
||||
case ContextMenu.SHOW_MODS:
|
||||
self.controller.get_modlist(record)
|
||||
|
||||
if isinstance(self.treeview, ModTreeView):
|
||||
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
||||
match action:
|
||||
case ContextMenu.DELETE_MOD:
|
||||
self.controller.delete_mods()
|
||||
self.controller.delete_mods(self.treeview)
|
||||
case ContextMenu.OPEN_WORKSHOP:
|
||||
self.open_mod_page()
|
||||
|
||||
|
||||
+14
-10
@@ -2,7 +2,7 @@ import logging
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.api.mods import (
|
||||
get_delimited_mods,
|
||||
@@ -40,20 +40,20 @@ class ModManager:
|
||||
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.emitter = controller.get_emitter()
|
||||
self.prefs = controller.get_prefs()
|
||||
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._get_mods()
|
||||
|
||||
@call_on_thread(dialogs.fetching_mods)
|
||||
def _get_mods(self) -> None:
|
||||
def load_mods(self) -> None:
|
||||
mods = get_delimited_mods(self.path)
|
||||
if len(mods) < 1:
|
||||
msg = self.format_mod_statusbar()
|
||||
@@ -61,13 +61,15 @@ class ModManager:
|
||||
self.thread_man.set_cleanup_func(func)
|
||||
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.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)
|
||||
if self.store is None:
|
||||
return
|
||||
msg = self.format_mod_statusbar()
|
||||
total_mods = len(self.store)
|
||||
self.emitter.emit("mods_updated", msg, total_mods)
|
||||
@@ -124,6 +126,8 @@ class ModManager:
|
||||
pass
|
||||
|
||||
def _on_mods_deleted(self, iters: list[Gtk.TreeIter]) -> None:
|
||||
if self.store is None:
|
||||
return
|
||||
for _iter in iters:
|
||||
self.store.remove(_iter)
|
||||
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version)
|
||||
|
||||
@@ -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
|
||||
@@ -53,7 +53,7 @@ class UpdateManager:
|
||||
func = StoredFunc(self._on_update_failure, msg)
|
||||
self.thread_man.set_cleanup_func(func, destroy_first=True)
|
||||
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)
|
||||
logger.warning(e)
|
||||
|
||||
|
||||
@@ -165,3 +165,6 @@ class ModelFactory:
|
||||
|
||||
def make_server_store(self) -> FastInsertListStore:
|
||||
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]
|
||||
@@ -9,5 +9,10 @@ load_error_lan = "Failed to find any servers on your network.\nCheck the server
|
||||
fetching_mods = "Fetching mod metadata"
|
||||
deleting_mods = "Deleting mods"
|
||||
scanning_mods = "Scanning mods"
|
||||
parsing_mods = "Parsing mods"
|
||||
|
||||
checking_api = "Checking API key"
|
||||
|
||||
running = "Running"
|
||||
failed = "FAILED"
|
||||
ok = "OK"
|
||||
@@ -3,3 +3,4 @@ api_validation_error = (
|
||||
)
|
||||
|
||||
api_popover = "API key validation failed."
|
||||
dayz_not_found = "Could not find a valid DayZ installation."
|
||||
@@ -0,0 +1 @@
|
||||
selectable_tree = "Ctrl-click to select multiple; Shift-click to select a range."
|
||||
@@ -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"
|
||||
@@ -3,3 +3,4 @@ signatures = "Updating mod signatures"
|
||||
geo = "Checking geolocation records"
|
||||
coords = "Checking local coordinates"
|
||||
updates = "Checking for updates"
|
||||
dayz = "Looking for DayZ"
|
||||
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
|
||||
from dzgui.util.localize import number
|
||||
from dzgui.strings.generic import selectable_tree
|
||||
from dzgui.util.strings import no_mods, no_servers, workshop
|
||||
|
||||
import gi
|
||||
@@ -46,8 +47,7 @@ def format_mods(size: int, mods: int) -> str:
|
||||
return no_mods
|
||||
l_size = number(size)
|
||||
plural = pluralize("mods", mods)
|
||||
# TODO: strings
|
||||
suffix = "Ctrl-click to select multiple; Shift-click to select a range."
|
||||
suffix = selectable_tree
|
||||
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:
|
||||
return f"<b>{text}</b>"
|
||||
|
||||
|
||||
def format_exception(e: Exception) -> str:
|
||||
return f"{type(e).__name__}: {e}"
|
||||
@@ -64,7 +64,7 @@ connect = "Connect"
|
||||
|
||||
# Columns
|
||||
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"]
|
||||
browser_cols = [
|
||||
"Name",
|
||||
@@ -181,7 +181,7 @@ filter_lowpop = "Low pop"
|
||||
filter_nonascii = "Non-ASCII"
|
||||
filter_duplicate = "Duplicate"
|
||||
filter_official = "Official"
|
||||
filter_unofficial = "Unoffic."
|
||||
filter_unofficial = "Unofficial"
|
||||
filter_modded = "Modded"
|
||||
|
||||
# maps
|
||||
@@ -273,6 +273,7 @@ class Crumbs:
|
||||
thanks: str
|
||||
developers: str
|
||||
default: str
|
||||
offline: str
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
@@ -462,6 +463,7 @@ crumbs = Crumbs(
|
||||
thanks="Help > Special thanks",
|
||||
developers="Options > Developers",
|
||||
default="Servers > ",
|
||||
offline="Mods > Play offline"
|
||||
)
|
||||
|
||||
no_mods = "No local mods found."
|
||||
|
||||
@@ -23,6 +23,8 @@ from dzgui.views.pages.help import Help
|
||||
from dzgui.views.pages.keys import Keybindings
|
||||
from dzgui.views.pages.log import Log
|
||||
from dzgui.views.pages.mods import Mods
|
||||
|
||||
from dzgui.views.pages.offline import OfflineLoader
|
||||
from dzgui.views.pages.options import Options
|
||||
from dzgui.views.pages.preconnect import PreConnectionAssistant
|
||||
from dzgui.views.pages.servers import ServerNotebook
|
||||
@@ -110,8 +112,11 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore
|
||||
|
||||
self.connection = PreConnectionAssistant(MainController)
|
||||
|
||||
self.offline = OfflineLoader(MainController)
|
||||
|
||||
self.pages = {
|
||||
self.help: NotebookPage.HELP,
|
||||
self.offline: NotebookPage.OFFLINE,
|
||||
self.clog: NotebookPage.CHANGELOG,
|
||||
self.keys: NotebookPage.KEYS,
|
||||
self.settings: NotebookPage.OPTIONS,
|
||||
|
||||
@@ -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
|
||||
@@ -11,6 +11,7 @@ from dzgui.const.constants import (
|
||||
)
|
||||
from dzgui.const.enum import FilterMode
|
||||
from dzgui.model.servers import ServerModelManager
|
||||
from dzgui.util.format import format_exception
|
||||
from dzgui.util.strings import all_maps
|
||||
from dzgui.views.components.maps_combo import MapsCombo
|
||||
from dzgui.views.components.labels import BoldLabel
|
||||
@@ -333,6 +334,10 @@ class FilterPanel(Gtk.Box):
|
||||
self.emitter.emit("map_selection_changed", name)
|
||||
|
||||
def set_filters(self, filters: dict[str, bool]) -> None:
|
||||
for check in self.button_grid.checks:
|
||||
label = check.get_label()
|
||||
check.set_active(filters[label])
|
||||
try:
|
||||
for check in self.button_grid.checks:
|
||||
label = check.get_label()
|
||||
check.set_active(filters[label])
|
||||
except Exception as e:
|
||||
msg = format_exception(e)
|
||||
logger.warning(msg)
|
||||
@@ -1,3 +1,4 @@
|
||||
from typing import Self
|
||||
from dzgui.util import css
|
||||
|
||||
import gi
|
||||
@@ -6,16 +7,23 @@ gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa
|
||||
|
||||
|
||||
# TODO: subclass Gtk.Frame directly
|
||||
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)
|
||||
|
||||
label = Gtk.Label(label=heading)
|
||||
label.set_halign(Gtk.Align.START)
|
||||
css.add_class(label, "settings-subheading")
|
||||
self.label = Gtk.Label(label=heading)
|
||||
self.label.set_halign(Gtk.Align.START)
|
||||
css.add_class(self.label, "settings-subheading")
|
||||
|
||||
frame = Gtk.Frame(hexpand=True)
|
||||
frame.add(widget)
|
||||
frame.set_label_widget(label)
|
||||
self.frame = Gtk.Frame(hexpand=True)
|
||||
self.frame.set_label_widget(self.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
|
||||
@@ -1,5 +1,4 @@
|
||||
from typing import Self, Union, TYPE_CHECKING
|
||||
from warnings import deprecated
|
||||
|
||||
from dzgui.const.enum import NotebookPage, ServerTab
|
||||
from dzgui.util.strings import esc_to_return, question_to_return
|
||||
@@ -8,7 +7,7 @@ from dzgui.views.components.buttons import LoggerAlertsButton
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GObject # noqa E402
|
||||
from gi.repository import Gtk # noqa E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.const.enum import ServerTab
|
||||
@@ -34,14 +33,6 @@ class Statusbar(Gtk.Grid):
|
||||
self.attach(self.statusbar, 0, 0, 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 = ""
|
||||
|
||||
controller.get_menu().connect("generic_treesel_changed", self._help_row_changed)
|
||||
@@ -78,6 +69,7 @@ class Statusbar(Gtk.Grid):
|
||||
NotebookPage.CHANGELOG,
|
||||
NotebookPage.LOG,
|
||||
NotebookPage.CONNECTION,
|
||||
NotebookPage.OFFLINE,
|
||||
):
|
||||
self.set_by_context(enum, esc_to_return)
|
||||
return
|
||||
@@ -92,12 +84,6 @@ class Statusbar(Gtk.Grid):
|
||||
|
||||
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:
|
||||
self.spinner.start()
|
||||
|
||||
@@ -132,12 +118,6 @@ class Statusbar(Gtk.Grid):
|
||||
cid = self.statusbar.get_context_id(str(context))
|
||||
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(
|
||||
self, context: Union[NotebookPage, "ServerTab"], string: str
|
||||
) -> None:
|
||||
|
||||
+19
-10
@@ -1,19 +1,20 @@
|
||||
import sys
|
||||
from typing import Literal, TYPE_CHECKING
|
||||
from typing import Any, Literal, Self, TYPE_CHECKING
|
||||
|
||||
# import time
|
||||
from enum import Enum
|
||||
from typing import Any, Self
|
||||
|
||||
# TODO: import dialog titles
|
||||
from dzgui.api.mods import remove_stale_signatures as remove_stale
|
||||
from dzgui.config.ipdb import get_ipdb
|
||||
from dzgui.const.constants import HEX_GREEN, HEX_RED
|
||||
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.const.constants import EXPAND, FILL
|
||||
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.symlink import rebuild_symlinks
|
||||
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.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"]):
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
|
||||
@@ -118,6 +122,7 @@ class BootDialog(Gtk.Dialog):
|
||||
self.error_box.hide()
|
||||
|
||||
steps = [
|
||||
(StoredFunc(is_dayz_installed, self.xdg.config), preboot.dayz, False),
|
||||
(StoredFunc(rebuild_symlinks, self.xdg.config), preboot.symlinks, False),
|
||||
(
|
||||
StoredFunc(remove_stale, self.xdg.config, self.xdg.version),
|
||||
@@ -125,6 +130,7 @@ class BootDialog(Gtk.Dialog):
|
||||
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(check_updates, self.version), preboot.updates, True),
|
||||
]
|
||||
@@ -134,6 +140,10 @@ class BootDialog(Gtk.Dialog):
|
||||
|
||||
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]:
|
||||
for row in self.store:
|
||||
if row[2]:
|
||||
@@ -175,16 +185,15 @@ class BootDialog(Gtk.Dialog):
|
||||
self.thread_man.set_cleanup_func(callback)
|
||||
except Exception as e:
|
||||
self.failed = True
|
||||
# TODO: generic exception wrapper
|
||||
self.exception = f"{type(e).__name__}: {e}"
|
||||
self.exception = format_exception(e)
|
||||
callback = StoredFunc(self.update_status, Success.FAIL)
|
||||
self.thread_man.set_cleanup_func(callback)
|
||||
|
||||
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:
|
||||
d = {Success.OK: "OK", Success.FAIL: "FAILED"}
|
||||
d = {Success.OK: dialogs.ok, Success.FAIL: dialogs.failed}
|
||||
msg = d[state]
|
||||
self.store[len(self.store) - 1][1] = msg
|
||||
self.iter_step()
|
||||
@@ -197,7 +206,7 @@ class BootDialog(Gtk.Dialog):
|
||||
it: Gtk.TreeIter,
|
||||
data: Any,
|
||||
) -> None:
|
||||
if model[it][1] == "Running":
|
||||
if model[it][1] == dialogs.running:
|
||||
cell.set_property("visible", True)
|
||||
else:
|
||||
cell.set_property("visible", False)
|
||||
@@ -214,9 +223,9 @@ class BootDialog(Gtk.Dialog):
|
||||
state = model[it][1]
|
||||
if column.get_sort_column_id() != 1:
|
||||
return
|
||||
if state == "OK":
|
||||
if state == dialogs.ok:
|
||||
cell.set_property(prop, HEX_GREEN)
|
||||
elif state == "FAILED":
|
||||
elif state == dialogs.failed:
|
||||
cell.set_property(prop, HEX_RED)
|
||||
else:
|
||||
cell.set_property(prop, None)
|
||||
|
||||
@@ -33,3 +33,28 @@ class FilePicker(Gtk.FileChooserDialog):
|
||||
self.destroy()
|
||||
return Path(file)
|
||||
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
|
||||
@@ -44,6 +44,8 @@ class ContextMixin(TreeView):
|
||||
self.context_menu.connect("key-press-event", self._on_key)
|
||||
|
||||
for row in group.value:
|
||||
if row is None:
|
||||
return False
|
||||
item = self._process_dynamic_row(row)
|
||||
self.context_menu.append(item)
|
||||
|
||||
|
||||
@@ -11,21 +11,36 @@ if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
|
||||
|
||||
|
||||
class Mods(NoOverlayScrolledWindow):
|
||||
class Mods(Gtk.Box):
|
||||
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.emitter = controller.get_emitter()
|
||||
self.tree = ModTreeView(controller)
|
||||
|
||||
self.add(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("unmap", self._on_unmap)
|
||||
|
||||
def _on_offline_clicked(self, button: Gtk.Button) -> None:
|
||||
self.controller.open_offline()
|
||||
|
||||
def _on_unmap(self, widget: Self) -> None:
|
||||
self.emitter.emit("mod_page_toggled", False)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -187,12 +187,14 @@ class Options(Gtk.Box):
|
||||
if prefs.is_debug:
|
||||
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
||||
|
||||
for frame in [
|
||||
HeadingFrame(api_box, strings.options.api_keys),
|
||||
HeadingFrame(prefs_grid, strings.options.prefs),
|
||||
HeadingFrame(mods_grid, strings.options.mods),
|
||||
HeadingFrame(version_grid, strings.options.version),
|
||||
for pair in [
|
||||
(api_box, strings.options.api_keys),
|
||||
(prefs_grid, strings.options.prefs),
|
||||
(mods_grid, strings.options.mods),
|
||||
(version_grid, strings.options.version),
|
||||
]:
|
||||
|
||||
frame = HeadingFrame.new_with_widget_and_label(*pair)
|
||||
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
||||
row += 1
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
self.progress_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
|
||||
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.set_size_request(600, 400)
|
||||
|
||||
@@ -166,7 +166,9 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
self.mods_placeholder = Placeholder("This server has no mods.")
|
||||
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
|
||||
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_box.add(self.warning_tree)
|
||||
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_tree = MaskedTree(ERROR)
|
||||
@@ -183,7 +187,9 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
self.error_placeholder = Placeholder("No errors.")
|
||||
self.error_box.add(self.error_tree)
|
||||
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.add(self.title)
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any, Optional, TYPE_CHECKING
|
||||
from warnings import deprecated
|
||||
|
||||
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
|
||||
|
||||
import gi
|
||||
@@ -85,10 +85,17 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
model, rows = sel.get_selected_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 self.get_model() is None:
|
||||
return
|
||||
return True
|
||||
if self.sel_blocked is False:
|
||||
self.sel_blocked = True
|
||||
self.controller.suppress_signal(
|
||||
@@ -98,7 +105,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
True,
|
||||
)
|
||||
self._vim_nav(event)
|
||||
return
|
||||
return False
|
||||
|
||||
def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None:
|
||||
"""
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import APP_NAME, HEX_RED
|
||||
from dzgui.const.enum import ContextMenuGroup
|
||||
from dzgui.managers.mods import ModManager
|
||||
from dzgui.util import strings, localize
|
||||
from dzgui.views.mixins.context_mixin import ContextMixin
|
||||
from dzgui.views.mixins.mods_mixin import ModsMixin
|
||||
@@ -24,14 +25,15 @@ logger = logging.getLogger(APP_NAME)
|
||||
class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
|
||||
def __init__(self, controller: "Controller") -> None:
|
||||
super().__init__(controller, menu=ContextMenuGroup.MOD)
|
||||
|
||||
self.controller = controller
|
||||
self.mod_man = ModManager(self, controller)
|
||||
|
||||
emitter = self.controller.get_emitter()
|
||||
emitter.connect("mods_updated", self._on_mods_updated)
|
||||
emitter.connect("mods_highlighted", self._on_mods_highlighted)
|
||||
|
||||
self.set_fixed_height_mode(True)
|
||||
self.set_headers_visible(True)
|
||||
|
||||
self.set_model(None)
|
||||
|
||||
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("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:
|
||||
self.get_selection().unselect_all()
|
||||
|
||||
def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None:
|
||||
if mods < 1:
|
||||
return
|
||||
self.set_headers_visible(True)
|
||||
path = Gtk.TreePath.new_from_indices([0])
|
||||
self.set_cursor(path)
|
||||
|
||||
@@ -106,3 +115,13 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
|
||||
val = model[it][3]
|
||||
formatted = localize.number(val)
|
||||
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
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
||||
authors = [
|
||||
{name = "aclist"}
|
||||
]
|
||||
version = "7.0.0b10"
|
||||
version = "7.0.0b12"
|
||||
license = "GPL-3.0-or-later"
|
||||
license-files = ["LICENSE"]
|
||||
readme = "README.md"
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
protocol = 1;
|
||||
publishedid = 1234567890;
|
||||
name = "ModName";
|
||||
timestamp = 638278452000000000;
|
||||
hash = "a1b2c3d4e5f6g7h8i9j0";
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
protocol = 1
|
||||
publishedid = 1234567890
|
||||
name = "ModName"
|
||||
timestamp = 638278452000000000
|
||||
hash = "a1b2c3d4e5f6g7h8i9j0"
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
protocol=1
|
||||
publishedid=1234567890
|
||||
name="ModName"
|
||||
timestamp=638278452000000000
|
||||
hash="a1b2c3d4e5f6g7h8i9j0"
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
protocol=1
|
||||
publishedid=1234567890
|
||||
name=ModName
|
||||
timestamp=638278452000000000
|
||||
hash=a1b2c3d4e5f6g7h8i9j0
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
protocol=1;
|
||||
publishedid=1234567890;
|
||||
name=ModName;
|
||||
timestamp=638278452000000000;
|
||||
hash=a1b2c3d4e5f6g7h8i9j0;
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
protocol=1
|
||||
publishedid=1234567890;
|
||||
name="ModName";
|
||||
timestamp=638278452000000000;
|
||||
hash= a1b2c3d4e5f6g7h8i9j0;
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
from dzgui.api.mods import tokenize
|
||||
|
||||
print(tokenize("meta4.cpp"))
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user