dzgui/dzgui/views/pages/options.py
2026-04-25 19:23:53 +09:00

535 lines
18 KiB
Python

from pathlib import Path
from typing import TYPE_CHECKING
from dzgui.api import pefile as PeFile
from dzgui.api.steam import find_user_id
from dzgui.config import query
from dzgui.util import strings, css, open_links
from dzgui.views.components.labels import LeftLabel
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.buttons import WebButton
from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.dialogs.link_dialog import WorkshopLinkDialog
from dzgui.const.enum import Preferences
from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP
from dzgui.const.constants import (
APPID_DAYZ,
APPID_DAYZ_EXP,
APPNAME_DAYZ,
APPNAME_DAYZ_EXP,
BETA_REPO,
FLATPAK_RUN_CMD,
FLATPAK_SANDBOX,
NO_EXPAND,
NO_FILL,
NO_PADDING,
STEAM_CMD,
VIEW_CONCEAL,
VIEW_REVEAL,
)
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
class Options(Gtk.Box):
def __init__(self, controller: "Controller"):
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
margin_start=10,
margin_end=10,
)
self.controller = controller
emitter = controller.get_emitter()
emitter.connect("api_change_failed", self._on_api_change_failed)
self.DEFAULT_WIDTH = 1
self.DEFAULT_HEIGHT = 1
label = Gtk.Label(label=strings.options.header)
label.set_halign(Gtk.Align.CENTER)
css.add_class(label, "page-heading")
self.add(label)
self.steam_entry: Gtk.Entry = None
self.bm_entry: Gtk.Entry = None
self.steam = WebButton(label=strings.options.steam_web)
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
self.bm = WebButton(label=strings.options.bm_web)
self.bm.connect("clicked", self._on_link_button_clicked, BM_API_SETUP)
self.steam_box = self._make_submit_field(
strings.options.enter_steam, Preferences.STEAM, True
)
self.bm_box = self._make_submit_field(
strings.options.enter_bm, Preferences.BM, True
)
api_rows = [
[LeftLabel(strings.options.steam_placeholder), self.steam_box],
[LeftLabel(strings.options.bm_placeholder), self.bm_box],
]
self.player_box = self._make_submit_field(
strings.options.name_placeholder, Preferences.NAME
)
self.fullscreen_toggle = self.make_binary_radio(
strings.options.last_used,
strings.options.always_fs,
Preferences.WINDOW,
)
self.client_combo = Gtk.ComboBoxText()
# TODO: strings
self.client_combo.append_text("Steam")
self.client_combo.append_text("Flatpak")
self.client_combo.append_text("Flatpak (container)")
self.client_combo.set_active(0)
self.client_combo.connect("changed", self._on_client_changed)
hbox = Gtk.Box(spacing=5, halign=Gtk.Align.START)
hbox.pack_start(self.client_combo, NO_EXPAND, NO_FILL, NO_PADDING)
self.distance_toggle = self.make_binary_radio(
strings.options.km, strings.options.mi, Preferences.DIST
)
pref_rows = [
[LeftLabel(strings.options.client), hbox],
[LeftLabel(strings.options.window_size), self.fullscreen_toggle],
[LeftLabel(strings.options.distance), self.distance_toggle],
[LeftLabel(strings.options.name), self.player_box],
]
self.mod_install_toggle = self.make_binary_radio(
strings.options.manual_dl, strings.options.auto_dl, Preferences.INSTALL
)
self.force_button = Gtk.Button(label=strings.options.update)
self.force_button.connect("clicked", self._on_force_update_clicked)
# NOTE: sensitivity state is updated after config file is loaded
self.force_button.set_sensitive(False)
eb = InfoEventBox(strings.options.dl_eventbox, controller)
eb2 = InfoEventBox(strings.options.force_eventbox, controller)
mod_rows = [
[LeftLabel(strings.options.install_mode), self.mod_install_toggle, eb],
[LeftLabel(strings.options.force_update), self.force_button, eb2],
]
self.dayz_version_label = Gtk.Label(label=strings.null)
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
self.branch_combo = Gtk.ComboBoxText()
self.branch_combo.append_text(strings.options.stable)
self.branch_combo.append_text(strings.options.testing)
self.branch_combo.set_active(0)
self.branch_combo.connect("changed", self._on_branch_changed)
self.branch_eb = InfoEventBox("", controller)
version_rows = [
[LeftLabel(APPNAME_DAYZ), self.dayz_version_label],
[LeftLabel(APPNAME_DAYZ_EXP), self.dayz_exp_version_label],
[LeftLabel(strings.options.branch), self.branch_combo, self.branch_eb],
]
api_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
api_grid = self._make_grid(api_rows)
api_box.add(api_grid)
api_links_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
margin_start=5,
margin_end=5,
margin_top=5,
margin_bottom=10,
homogeneous=True,
spacing=10,
)
api_links_box.add(self.steam)
api_links_box.add(self.bm)
api_box.add(api_links_box)
prefs_grid = self._make_grid(pref_rows)
mods_grid = self._make_grid(mod_rows)
version_grid = self._make_grid(version_rows)
col = 1
row = 1
grid = Gtk.Grid(
orientation=Gtk.Orientation.VERTICAL,
row_spacing=30,
hexpand=True,
)
developers = Gtk.Button(label="Developers", halign=Gtk.Align.START)
developers.connect("clicked", self._on_developers_clicked)
prefs = self.controller.get_prefs()
if prefs.is_debug:
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
for frame in [
self.make_frame(api_box, strings.options.api_keys),
self.make_frame(prefs_grid, strings.options.prefs),
self.make_frame(mods_grid, strings.options.mods),
self.make_frame(version_grid, strings.options.version),
]:
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
row += 1
self.scrollable = Gtk.ScrolledWindow(vexpand=True)
self.scrollable.add(grid)
self.add(self.scrollable)
def block_text_entry(self) -> None:
for entry in self.steam_entry, self.bm_entry:
entry.set_position(-1)
entry.set_can_focus(False)
def unblock_text_entry(self) -> None:
for entry in self.steam_entry, self.bm_entry:
entry.set_can_focus(True)
def _on_developers_clicked(self, button: Gtk.Button) -> None:
self.controller.show_developers_page()
def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None:
open_links.open_link_by_url(url)
def _make_submit_field(
self,
placeholder: str,
context: Preferences,
private: bool = False,
) -> Gtk.Box:
entry = Gtk.Entry(placeholder_text=placeholder, hexpand=True)
button = Gtk.Button(label="Save")
button.connect("clicked", self._on_save_clicked, entry, context)
entry.connect("insert-text", self._on_text_typed, context, button)
entry.connect("activate", self._on_field_activated, context, button)
entry.get_property("buffer").connect(
"deleted-text", self._on_text_deleted, context, button
)
if private:
entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL)
entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
entry.connect("icon-release", self._on_icon_release)
entry.set_visibility(False)
if context == Preferences.STEAM:
self.steam_entry = entry
else:
self.bm_entry = entry
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
box.add(entry)
box.add(button)
return box
def _on_field_activated(
self, entry: Gtk.Entry, context: Preferences, button: Gtk.Button
) -> None:
text = entry.get_text()
if not self._is_valid_text(text, context):
return
self._on_save_clicked(button, entry, context)
def _make_grid(self, rows: list) -> Gtk.Grid:
grid = Gtk.Grid(
orientation=Gtk.Orientation.VERTICAL,
column_spacing=10,
row_spacing=5,
margin_start=5,
margin_end=5,
margin_top=10,
margin_bottom=10,
)
row = 1
for record in rows:
col = 1
for el in record:
grid.attach(el, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
col += 1
row += 1
return grid
def _on_save_clicked(
self, button: Gtk.Button, entry: Gtk.Entry, enum: Preferences
) -> None:
old_text = self.controller.query_config(enum)
self.old_text = old_text
self.old_entry = entry
button.set_sensitive(False)
match enum:
case Preferences.NAME:
value = entry.get_text().strip()
self.controller.update_config(enum, value)
case Preferences.BM | Preferences.STEAM:
text = "".join(entry.get_text().split())
self.controller.update_api_key(text, enum)
def _on_api_change_failed(self, emitter: "Emitter") -> None:
self.old_entry.set_text(self.old_text)
dialog = ExceptionDialog(self.controller, strings.api_error)
dialog.run()
def restore_api_text(self, text: str, entry: Gtk.Entry) -> None:
entry.set_text(text)
def revert(self, mode: Preferences) -> None:
if mode == Preferences.STEAM:
self.steam_entry.set_text(self.old_steam)
else:
self.bm_entry.set_text(self.old_bm)
pass
def _on_force_update_clicked(self, button: Gtk.Button) -> None:
# TODO: unimplemented
print("UNIMPLEMENTED")
# wait_msg = strings.dialog.updating_mods
def _on_client_changed(self, combo: Gtk.ComboBoxText) -> None:
# TODO: use two columns or constants here, not strings
client = combo.get_active_text()
match client:
case "Steam":
value = STEAM_CMD
case "Flatpak":
value = FLATPAK_RUN_CMD
case "Flatpak (container)":
value = FLATPAK_SANDBOX
self.controller.update_config(Preferences.CLIENT, value)
def _on_branch_changed(self, combo: Gtk.ComboBoxText) -> None:
branch = combo.get_active_text()
print("UNIMPLEMENTED: ", branch)
## TODO: needs to trigger download process
# self.controller.toggle_branch(branch)
# branch = combo.get_active_text().lower()
# self.controller.update_config("branch", branch)
# scripts/update
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
try:
self.controller.toggle_config(context)
except Exception:
button.handler_block_by_func(self._on_radio_toggled)
self.populate_settings()
button.handler_unblock_by_func(self._on_radio_toggled)
if context == Preferences.INSTALL:
if self.controller.is_auto_install():
self.force_button.set_sensitive(True)
WorkshopLinkDialog(
self.controller,
strings.options.manual_sub_msg,
strings.self_workshop,
self.uid,
)
else:
self.force_button.set_sensitive(False)
def _is_valid_text(self, text: str, context: Preferences) -> bool:
if text.isspace():
return False
if len(text) == 0:
return False
match context:
case Preferences.NAME:
old = self.old_name
case Preferences.STEAM:
old = self.old_steam
case Preferences.BM:
old = self.old_bm
if text == old:
return False
return True
def _on_text_deleted(
self,
buffer: Gtk.EntryBuffer,
position: int,
chars: int,
context: Preferences,
button: Gtk.Button,
) -> None:
text = buffer.get_text()
state = self._is_valid_text(text, context)
button.set_sensitive(state)
def _on_text_typed(
self,
entry: Gtk.Entry,
text: str,
length: int,
pos: int,
context: Preferences,
button: Gtk.Button,
) -> None:
buffer = entry.get_property("buffer")
text = buffer.get_text() + text
state = self._is_valid_text(text, context)
button.set_sensitive(state)
def make_binary_radio(
self,
first_option: str,
second_option: str,
context: Preferences,
) -> Gtk.Box:
hbox = Gtk.Box(spacing=5, halign=Gtk.Align.START)
radio1 = Gtk.RadioButton.new_with_label(None, first_option)
radio2 = Gtk.RadioButton.new_from_widget(radio1)
radio2.set_label(second_option)
radio1.connect("toggled", self._on_radio_toggled, context)
hbox.pack_start(radio1, NO_EXPAND, NO_FILL, NO_PADDING)
hbox.pack_start(radio2, NO_EXPAND, NO_FILL, NO_PADDING)
return hbox
def make_frame(self, widget: Gtk.Widget, text: str) -> Gtk.Box:
label = Gtk.Label(label=text)
label.set_halign(Gtk.Align.START)
css.add_class(label, "settings-subheading")
frame = Gtk.Frame(hexpand=True)
frame.add(widget)
frame.set_label_widget(label)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.add(frame)
return box
def populate_settings(self) -> None:
prefs = self.controller.get_prefs()
if prefs.paths.config.is_file() is False:
# NOTE: in case file got deleted locally
dialog = ExceptionDialog(self.controller, strings.config_not_found)
dialog.run()
raise Exception
# FIXME: return to main menu
return
config = query.get_config(prefs.paths.config)
name = config["name"]
default_steam_path = config["default_steam_path"]
steam = config["steam_api"]
bm = config["bm_api"]
install = config["auto_install"]
steam_path = Path(default_steam_path)
self.uid = find_user_id(steam_path)
self.old_steam = steam
self.old_bm = bm
self.old_name = name
self.steam_entry.set_text(steam)
self.bm_entry.set_text(bm)
self.player_box.get_children()[0].set_text(name)
# NOTE: suppress toggle signal until radios are built
self._suppress_toggles(True)
self.force_button.set_sensitive(install)
for el, conf_state in [
(self.mod_install_toggle, install),
(self.fullscreen_toggle, config["fullscreen"]),
(self.distance_toggle, config["use_miles"]),
]:
el.get_children()[conf_state].set_active(True)
self._suppress_toggles(False)
# NOTE: disable buttons if no text is set
for field in (
[name, self.player_box],
[steam, self.steam_box],
[bm, self.bm_box],
):
if field[0] == "":
field[1].get_children()[1].set_sensitive(False)
try:
pe_file_path = PeFile.get_pefile_path(steam_path, APPID_DAYZ)
vers = PeFile.get_dayz_version(pe_file_path)
dayz_version = PeFile.dayz_version_to_str(vers)
except Exception:
dayz_version = strings.null
try:
exp_file_path = PeFile.get_pefile_path(steam_path, APPID_DAYZ_EXP)
vers = PeFile.get_dayz_version(exp_file_path)
dayz_exp_version = PeFile.dayz_version_to_str(vers)
except Exception:
dayz_exp_version = strings.null
self.dayz_version_label.set_text(dayz_version)
self.dayz_exp_version_label.set_text(dayz_exp_version)
# TODO: not happy with this
active_combo = query.get_client_index(config["client"])
self.client_combo.set_active(active_combo)
active_combo = 1 if config["branch"] == BETA_REPO else 0
self.controller.suppress_signal(
self, self.branch_combo, "_on_branch_changed", True
)
self.branch_combo.set_active(active_combo)
self.branch_combo.set_sensitive(prefs.allow_updates)
self.controller.suppress_signal(
self, self.branch_combo, "_on_branch_changed", False
)
if prefs.allow_updates is True:
msg = strings.options.self_update
else:
msg = strings.options.no_self_update
self.branch_eb.set_text(msg)
def _suppress_toggles(self, state: bool) -> None:
for toggle in [
self.mod_install_toggle,
self.fullscreen_toggle,
self.distance_toggle,
]:
self.controller.suppress_signal(
self, toggle.get_children()[0], "_on_radio_toggled", state
)
def _on_icon_release(
self,
widget: Gtk.Entry,
icon_pos: Gtk.EntryIconPosition,
event: Gdk.Event,
) -> None:
visible = widget.get_visibility()
if visible:
icon, state = VIEW_REVEAL, False
else:
icon, state = VIEW_CONCEAL, True
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
widget.set_visibility(state)
def grab_content_area(self) -> None:
return