feat: abstract field creation into SubmitField

This commit is contained in:
aclist 2026-08-16 12:46:16 +09:00
parent 69cba3460d
commit cc485289e3
2 changed files with 201 additions and 150 deletions

View File

@ -1,12 +1,19 @@
developers = "Developers"
start_tab = "Start tab"
workshop_label = "Subscribed mods"
workshop_eventbox = "If you manually subscribed to mods on Steam prior to using DZGUI, you can unsubscribe via this link."
steam_combo = "Steam"
flatpak_combo = "Flatpak"
flatpak_container_combo = "Flatpak (container)"
steam_placeholder = "Enter your Steam API key"
name_placeholder = "Identifies you to other players"
server_combo = "Server browser"
saved_combo = "Saved servers"
recent_combo = "Recent"
lan_combo = "LAN"
fullscreen_eventbox = "This option is not available on Steam Deck."
save_button = "Save"
api_failed = "API validation failed"

View File

@ -1,5 +1,5 @@
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Callable, TYPE_CHECKING
from dzgui.api import pefile as PeFile
@ -17,15 +17,15 @@ from dzgui.const.constants import (
)
from dzgui.const.endpoints import STEAM_API_SETUP
from dzgui.const.enum import Preferences, ServerTab
from dzgui.strings import errors, options
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 WebButton
from dzgui.views.components.buttons import SpinnerButton, WebButton
from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.misc import ClientCombo
from dzgui.views.components.misc import ClientCombo, ErrorPopover
from dzgui.views.dialogs.generic import ExceptionDialog
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
@ -40,6 +40,182 @@ if TYPE_CHECKING:
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)
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("insert-text", 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,
text: str,
length: int,
pos: int,
) -> None:
buffer = entry.get_property("buffer")
text = buffer.get_text() + 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()
self.button.stop_spinner()
self.entry.set_sensitive(True)
def _on_api_failure(self, emitter: "Emitter") -> None:
self.pop.popup()
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,
@ -80,29 +256,23 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
self.controller = controller
self.controller.register_widget("options", self)
emitter = controller.get_emitter()
emitter.connect("api_change_failed", self._on_api_change_failed)
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 = self._make_submit_field(
strings.options.enter_steam, Preferences.STEAM, True
)
self.steam_box = SteamSubmitField(controller)
api_rows = [
[LeftLabel(strings.options.steam_placeholder), self.steam_box, self.steam],
]
self.player_box = self._make_submit_field(
strings.options.name_placeholder, Preferences.NAME
)
self.player_box = NameSubmitField(controller)
self.player_box.set_halign(Gtk.Align.START)
# TODO: make submit field a standalone class
self.player_box.get_children()[0].set_width_chars(30) # type: ignore
self.fullscreen_toggle = ToggleField(
@ -112,12 +282,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
Preferences.WINDOW,
)
# TODO: strings
eb = InfoEventBox(
"This option is not available on Steam Deck.", self.controller
)
self.fullscreen_toggle.add(eb)
self.client_combo = ClientCombo()
self.client_combo.connect("changed", self._on_client_changed)
@ -210,11 +374,13 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
return str(model[ind][0])
def block_text_entry(self) -> None:
self.steam_entry.set_position(-1)
self.steam_entry.set_can_focus(False)
self.steam_box.block_text_entry()
# self.steam_entry.set_position(-1)
# self.steam_entry.set_can_focus(False)
def unblock_text_entry(self) -> None:
self.steam_entry.set_can_focus(True)
self.steam_box.unblock_text_entry()
# self.steam_entry.set_can_focus(True)
def _on_developers_clicked(self, button: Gtk.Button) -> None:
self.controller.show_developers_page()
@ -222,46 +388,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
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
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,
@ -281,35 +407,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
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.STEAM:
text = "".join(entry.get_text().split())
self.controller.update_api_key(enum, text)
def _on_api_change_failed(self, emitter: "Emitter") -> None:
self.old_entry.set_text(self.old_text)
# TODO: use popover
dialog = ExceptionDialog(self.controller, errors.api_validation_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)
def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
_iter = combo.get_active_iter()
if _iter is None:
@ -325,49 +422,6 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
real_cmd = combo.get_model()[_iter][1]
self.controller.update_config(Preferences.CLIENT, real_cmd)
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
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 populate_settings(self) -> None:
prefs = self.controller.get_prefs()
# NOTE: re-check in case file was removed by user between runs
@ -385,32 +439,22 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
steam_path = Path(default_steam_path)
self.old_steam = steam
self.old_name = name
self.steam_entry.set_text(steam)
p = self.player_box.get_children()[0]
if hasattr(p, "set_text"):
p.set_text(name)
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)
# NOTE: disable buttons if no text is set
for field in (
[name, self.player_box],
[steam, self.steam_box],
):
if field[0] == "":
field[1].get_children()[1].set_sensitive(False)
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
if dayz_version is None:
dayz_version = strings.null