from pathlib import Path from typing import 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, BM_API_SETUP from dzgui.const.enum import Preferences, ServerTab from dzgui.strings import errors, options from dzgui.util import strings, css, open_links from dzgui.views.components.labels import LeftLabel from dzgui.views.components.buttons import WebButton from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.misc import ClientCombo from dzgui.views.dialogs.generic import ExceptionDialog 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 ShortHBox(Gtk.Box): def __init__(self, widget: Gtk.Widget) -> None: super().__init__(spacing=5, halign=Gtk.Align.START) self.pack_start(widget, NO_EXPAND, NO_FILL, NO_PADDING) class Options(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("options", self) 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 self.bm_entry: Gtk.Entry 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.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 = self.make_binary_radio( strings.options.last_used, strings.options.always_fs, Preferences.WINDOW, ) self.client_combo = ClientCombo() self.client_combo.connect("changed", self._on_client_changed) client_hbox = ShortHBox(self.client_combo) self.distance_toggle = self.make_binary_radio( 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.client), client_hbox], [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) 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) 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_keys), (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 self.scrollable = Gtk.ScrolledWindow(vexpand=True) self.scrollable.add(grid) self.add(self.scrollable) def get_client_name(self) -> str: model = self.client_combo.get_model() ind = self.client_combo.get_active() return str(model[ind][0]) 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(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) else: self.bm_entry.set_text(self.old_bm) pass 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 _on_client_changed(self, combo: Gtk.ComboBoxText) -> None: _iter = combo.get_active_iter() if _iter is None: raise ValueError(f"No active iterator set on {combo}") real_cmd = combo.get_model()[_iter][1] self.controller.update_config(Preferences.CLIENT, real_cmd) 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) 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 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.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) bm = self.controller.query_config(Preferences.BM) steam_path = Path(default_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) p = self.player_box.get_children()[0] if hasattr(p, "set_text"): p.set_text(name) # NOTE: suppress toggle signal until radios are built self._suppress_toggles(True) for el, conf_state in [ (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) 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) # TODO: bicolumn list store with no cell renderer on index 1, use raw command names active_combo = query.get_client_index(config["client"]) self.client_combo.set_active(active_combo) 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 _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