mirror of
https://github.com/aclist/dztui.git
synced 2026-10-25 12:12:31 +01:00
462 lines
15 KiB
Python
462 lines
15 KiB
Python
from pathlib import Path
|
|
from typing import Callable, TYPE_CHECKING
|
|
|
|
from dzgui.api import pefile as PeFile
|
|
|
|
from dzgui.config import query
|
|
from dzgui.const.constants import (
|
|
APPID_DAYZ,
|
|
APPID_DAYZ_EXP,
|
|
APPNAME_DAYZ,
|
|
APPNAME_DAYZ_EXP_HUMAN,
|
|
NO_EXPAND,
|
|
NO_FILL,
|
|
NO_PADDING,
|
|
VIEW_CONCEAL,
|
|
VIEW_REVEAL,
|
|
)
|
|
from dzgui.const.endpoints import STEAM_API_SETUP
|
|
from dzgui.const.enum import Preferences, ServerTab
|
|
from dzgui.strings import options
|
|
from dzgui.util import strings, css, open_links
|
|
|
|
from dzgui.views.components.box import ShortHBox, VBox
|
|
from dzgui.views.components.eventbox import InfoEventBox
|
|
from dzgui.views.components.labels import LeftLabel
|
|
from dzgui.views.components.buttons import SpinnerButton, WebButton
|
|
from dzgui.views.components.frame import HeadingFrame
|
|
from dzgui.views.components.misc import ErrorPopover
|
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
|
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
|
|
|
|
|
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 SubmitField(Gtk.Box):
|
|
def __init__(
|
|
self,
|
|
controller: "Controller",
|
|
placeholder: str,
|
|
context: Preferences,
|
|
slow: bool = False,
|
|
private: bool = True,
|
|
) -> None:
|
|
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
|
|
|
self.callback: Callable
|
|
self.controller = controller
|
|
self.context = context
|
|
|
|
self.old_text: str
|
|
self.placeholder = placeholder
|
|
|
|
self.entry = Gtk.Entry(placeholder_text=placeholder, hexpand=True)
|
|
|
|
# TODO: audit for all applicable callbacks
|
|
if private:
|
|
self.entry.set_icon_from_icon_name(
|
|
Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL
|
|
)
|
|
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
|
self.entry.connect("icon-release", self._on_icon_release)
|
|
self.entry.set_visibility(False)
|
|
|
|
self.button: SpinnerButton | Gtk.Button
|
|
if slow:
|
|
self.button = SpinnerButton(label=options.save_button)
|
|
else:
|
|
self.button = Gtk.Button(label=options.save_button)
|
|
|
|
self.button.connect("clicked", self._on_save_clicked)
|
|
self.entry.connect("changed", self._on_text_typed)
|
|
self.entry.connect("activate", self._on_field_activated)
|
|
self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted)
|
|
|
|
self.add(self.entry)
|
|
self.add(self.button)
|
|
|
|
def set_text(self, text: str) -> None:
|
|
self.old_text = text
|
|
self.entry.set_text(text)
|
|
if len(text) == 0:
|
|
self.button.set_sensitive(False)
|
|
|
|
def _is_valid_text(self, text: str) -> bool:
|
|
if text.isspace():
|
|
return False
|
|
if len(text) == 0:
|
|
return False
|
|
|
|
if text == self.old_text:
|
|
return False
|
|
return True
|
|
|
|
def _on_text_deleted(
|
|
self,
|
|
buffer: Gtk.EntryBuffer,
|
|
position: int,
|
|
chars: int,
|
|
) -> None:
|
|
|
|
text = buffer.get_text()
|
|
state = self._is_valid_text(text)
|
|
self.button.set_sensitive(state)
|
|
|
|
def _on_text_typed(
|
|
self,
|
|
entry: Gtk.Entry,
|
|
) -> None:
|
|
|
|
text = entry.get_text()
|
|
state = self._is_valid_text(text)
|
|
self.button.set_sensitive(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 _on_field_activated(self, entry: Gtk.Entry) -> None:
|
|
text = entry.get_text()
|
|
if not self._is_valid_text(text):
|
|
return
|
|
self.save_option()
|
|
|
|
def set_callback(self, callback: Callable) -> None:
|
|
self.callback = callback
|
|
|
|
def save_option(self) -> None:
|
|
self.callback()
|
|
self.button.set_sensitive(False)
|
|
|
|
def _on_save_clicked(self, button: Gtk.Button) -> None:
|
|
self.save_option()
|
|
|
|
|
|
class SteamSubmitField(SubmitField):
|
|
def __init__(self, controller: "Controller") -> None:
|
|
super().__init__(
|
|
controller,
|
|
options.steam_placeholder,
|
|
Preferences.STEAM,
|
|
slow=True,
|
|
private=True,
|
|
)
|
|
|
|
self.pop = ErrorPopover(
|
|
position=Gtk.PositionType.BOTTOM, relative_to=self.entry
|
|
)
|
|
self.pop.set_label(options.api_failed)
|
|
self.pop.show_all()
|
|
self.pop.popdown()
|
|
|
|
emitter = controller.get_emitter()
|
|
emitter.connect("api_change_failed", self._on_api_failure)
|
|
emitter.connect("api_change_successful", self._on_api_success)
|
|
|
|
self.set_callback(self.save_setting)
|
|
|
|
def save_setting(self) -> None:
|
|
self.entry.set_sensitive(False)
|
|
text = "".join(self.entry.get_text().split())
|
|
self.controller.update_steam_api_key(text)
|
|
|
|
def _on_api_success(self, emitter: "Emitter") -> None:
|
|
self.old_text = self.entry.get_text()
|
|
if isinstance(self.button, SpinnerButton):
|
|
self.button.stop_spinner()
|
|
self.entry.set_sensitive(True)
|
|
|
|
def _on_api_failure(self, emitter: "Emitter") -> None:
|
|
self.pop.popup()
|
|
if isinstance(self.button, SpinnerButton):
|
|
self.button.stop_spinner()
|
|
self.entry.set_sensitive(True)
|
|
|
|
def block_text_entry(self) -> None:
|
|
self.entry.set_position(-1)
|
|
self.entry.set_can_focus(False)
|
|
|
|
def unblock_text_entry(self) -> None:
|
|
self.entry.set_can_focus(True)
|
|
|
|
|
|
class NameSubmitField(SubmitField):
|
|
def __init__(self, controller: "Controller") -> None:
|
|
super().__init__(
|
|
controller,
|
|
options.name_placeholder,
|
|
Preferences.NAME,
|
|
private=False,
|
|
)
|
|
self.entry.set_width_chars(30) # type: ignore
|
|
self.set_callback(self.save_player_name)
|
|
|
|
def save_player_name(self) -> None:
|
|
value = self.entry.get_text().strip()
|
|
self.old_text = value
|
|
self.controller.update_config(self.context, value)
|
|
|
|
|
|
class ToggleField(Gtk.Box):
|
|
def __init__(
|
|
self,
|
|
controller: "Controller",
|
|
first_option: str,
|
|
second_option: str,
|
|
context: Preferences,
|
|
) -> None:
|
|
super().__init__(spacing=5, halign=Gtk.Align.START)
|
|
|
|
self.controller = controller
|
|
self.context = context
|
|
self.radio1 = Gtk.RadioButton.new_with_label(None, first_option)
|
|
self.radio2 = Gtk.RadioButton.new_from_widget(self.radio1)
|
|
self.radio2.set_label(second_option)
|
|
self.pack_start(self.radio1, NO_EXPAND, NO_FILL, NO_PADDING)
|
|
self.pack_start(self.radio2, NO_EXPAND, NO_FILL, NO_PADDING)
|
|
|
|
def set_suboption_active(self, state: bool) -> None:
|
|
# NOTE: defer connection of signal until after state is set
|
|
self.radio2.set_active(state)
|
|
self.radio1.connect("toggled", self._on_radio_toggled, self.context)
|
|
|
|
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
|
|
self.controller.toggle_config(context)
|
|
|
|
def set_sensitive(self, state: bool) -> None:
|
|
for el in self.radio1, self.radio2:
|
|
el.set_sensitive(state)
|
|
|
|
|
|
class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|
def __init__(self, controller: "Controller"):
|
|
super().__init__(
|
|
margin_start=10,
|
|
margin_end=10,
|
|
)
|
|
|
|
self.controller = controller
|
|
self.controller.register_widget("options", self)
|
|
|
|
self.DEFAULT_WIDTH = 1
|
|
self.DEFAULT_HEIGHT = 1
|
|
|
|
self.steam_entry: Gtk.Entry
|
|
self.pop: Gtk.Popover
|
|
|
|
self.steam = WebButton(label=strings.options.steam_web)
|
|
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
|
|
|
|
self.steam_box = SteamSubmitField(controller)
|
|
api_rows = [
|
|
[LeftLabel(strings.options.steam_placeholder), self.steam_box, self.steam],
|
|
]
|
|
|
|
self.player_box = NameSubmitField(controller)
|
|
self.player_box.set_halign(Gtk.Align.START)
|
|
self.player_box.get_children()[0].set_width_chars(30) # type: ignore
|
|
|
|
self.fullscreen_toggle = ToggleField(
|
|
self.controller,
|
|
strings.options.last_used,
|
|
strings.options.always_fs,
|
|
Preferences.WINDOW,
|
|
)
|
|
|
|
self.distance_toggle = ToggleField(
|
|
self.controller, strings.options.km, strings.options.mi, Preferences.DIST
|
|
)
|
|
|
|
combo_store = Gtk.ListStore(str, object)
|
|
tabs = (
|
|
(options.server_combo, ServerTab.BROWSER),
|
|
(options.saved_combo, ServerTab.SAVED),
|
|
(options.recent_combo, ServerTab.RECENT),
|
|
(options.lan_combo, ServerTab.LAN),
|
|
)
|
|
for tab in tabs:
|
|
combo_store.append(tab)
|
|
self.start_tab_combo = Gtk.ComboBox.new_with_model(combo_store)
|
|
renderer_text = Gtk.CellRendererText()
|
|
self.start_tab_combo.pack_start(renderer_text, True)
|
|
self.start_tab_combo.add_attribute(renderer_text, "text", 0)
|
|
self.start_tab_combo.set_active(0)
|
|
|
|
start_tab_hbox = ShortHBox(self.start_tab_combo)
|
|
self.start_tab_combo.connect("changed", self._on_start_tab_changed)
|
|
|
|
pref_rows = [
|
|
[LeftLabel(strings.options.window_size), self.fullscreen_toggle],
|
|
[LeftLabel(strings.options.distance), self.distance_toggle],
|
|
[LeftLabel(options.start_tab), start_tab_hbox],
|
|
[LeftLabel(strings.options.name), self.player_box],
|
|
]
|
|
|
|
self.dayz_version_label = Gtk.Label(label=strings.null)
|
|
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
|
|
|
|
version_rows = [
|
|
[LeftLabel(APPNAME_DAYZ), self.dayz_version_label],
|
|
[LeftLabel(APPNAME_DAYZ_EXP_HUMAN), self.dayz_exp_version_label],
|
|
]
|
|
|
|
api_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
api_grid = self._make_grid(api_rows)
|
|
api_box.add(api_grid)
|
|
|
|
prefs_grid = self._make_grid(pref_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=options.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 pair in [
|
|
(api_box, strings.options.api_key),
|
|
(prefs_grid, strings.options.prefs),
|
|
(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
|
|
|
|
label = Gtk.Label(label=strings.options.header)
|
|
label.set_halign(Gtk.Align.CENTER)
|
|
css.add_class(label, "page-heading")
|
|
|
|
box = VBox()
|
|
box.add(label)
|
|
box.add(grid)
|
|
self.add(box)
|
|
|
|
self.connect("key-press-event", self._on_keypress)
|
|
|
|
def block_text_entry(self) -> None:
|
|
self.steam_box.block_text_entry()
|
|
|
|
def unblock_text_entry(self) -> None:
|
|
self.steam_box.unblock_text_entry()
|
|
|
|
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_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_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
|
|
_iter = combo.get_active_iter()
|
|
if _iter is None:
|
|
raise ValueError(f"No active iterator set on {combo}")
|
|
enum = combo.get_model()[_iter][1]
|
|
index = enum.value
|
|
self.controller.update_config(Preferences.START_TAB, index)
|
|
|
|
def populate_settings(self) -> None:
|
|
prefs = self.controller.get_prefs()
|
|
# NOTE: re-check in case file was removed by user between runs
|
|
if prefs.paths.config.is_file() is False:
|
|
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
|
dialog.show_all()
|
|
dialog.run()
|
|
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
|
|
|
config = query.get_config(prefs.paths.config)
|
|
|
|
# TODO: use newer config enums
|
|
name = self.controller.query_config(Preferences.NAME)
|
|
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
|
steam = self.controller.query_config(Preferences.STEAM)
|
|
|
|
steam_path = Path(default_steam_path)
|
|
|
|
self.steam_box.set_text(steam)
|
|
self.player_box.set_text(name)
|
|
|
|
fs = config["fullscreen"]
|
|
miles = config["use_miles"]
|
|
|
|
if prefs.is_steam_deck:
|
|
self.fullscreen_toggle.set_sensitive(False)
|
|
eb = InfoEventBox(options.fullscreen_eventbox, self.controller)
|
|
self.fullscreen_toggle.add(eb)
|
|
|
|
fs = True
|
|
|
|
self.fullscreen_toggle.set_suboption_active(fs)
|
|
self.distance_toggle.set_suboption_active(miles)
|
|
|
|
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
|
if dayz_version is None:
|
|
dayz_version = strings.null
|
|
|
|
dayz_exp_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
|
|
if dayz_exp_version is None:
|
|
dayz_exp_version = strings.null
|
|
|
|
self.dayz_version_label.set_text(dayz_version)
|
|
self.dayz_exp_version_label.set_text(dayz_exp_version)
|
|
|
|
start_tab = self.controller.query_config(Preferences.START_TAB)
|
|
self.start_tab_combo.set_active(start_tab)
|
|
|
|
def _suppress_toggles(self, state: bool) -> None:
|
|
for toggle in [
|
|
self.fullscreen_toggle,
|
|
self.distance_toggle,
|
|
]:
|
|
self.controller.suppress_signal(
|
|
self, toggle.get_children()[0], "_on_radio_toggled", state
|
|
)
|
|
|
|
def grab_content_area(self) -> None:
|
|
self.grab_focus()
|