diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ff240a..053e26e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Reduce padding on keys button - Boldface breadcrumbs - Bold labels inside frames +- Unsticky keys when clicking sidebar ## Unreleased - Setup wizard diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 2c2ceed..147bfad 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -14,7 +14,6 @@ class Popup(Enum): NOTIFY = 2 CONFIRM = 3 ENTRY = 4 - RETURN = 5 MODLIST = 6 DETAILS = 7 QUIT = 8 diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 15d5bdf..515f315 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -2,12 +2,14 @@ import logging import shutil import threading import textwrap +import traceback from pathlib import Path from typing import Any, Callable, TYPE_CHECKING import dzgui.api.pefile as PeFile import dzgui.util._json as JSON # noqa +from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.api.probe import test_steam_api, test_bm_api from dzgui.api.mods import ( @@ -38,7 +40,7 @@ from dzgui.config import update from dzgui.config.query import lookup from dzgui.config.userprefs import UserPrefs from dzgui.controllers.model import ModelManager -from dzgui.util import cooldown, strings +from dzgui.util import strings from dzgui.util.diag import write_diagnostic from dzgui.util._json import read_json, write_json from dzgui.util.localize import number @@ -89,7 +91,6 @@ class Controller: self.crumbs_cache = "" self.mediator = AppNavigation() self.prefs: UserPrefs - self.cooldown = 0 self.model_manager = ModelManager() @@ -291,14 +292,18 @@ class Controller: update.toggle_config(config, context) except Exception as e: logger.critical(e) - self.spawn_dialog(strings.something_wrong, Popup.NOTIFY) + trace = traceback.format_exc() + dialog = ExceptionDialog(self, trace) + dialog.run() def update_config(self, key: Preferences, value: str) -> None: try: update.write_config(self.prefs.paths.config, key, value) except Exception as e: logger.critical(e) - self.spawn_dialog(strings.something_wrong, Popup.NOTIFY) + trace = traceback.format_exc() + dialog = ExceptionDialog(self, trace) + dialog.run() # TODO: suppress signals # then reenable (or it spawns dialog twice) self.mediator.grid.notebook.settings.populate_settings() @@ -308,19 +313,6 @@ class Controller: #def present_toast(self, text: str) -> None: # self.mediator.window.toast.set_text_and_fade(text) - def start_cooldown(self) -> None: - self.cooldown = cooldown.get_time() - - def get_cooldown(self) -> None: - return self.cooldown - - def manage_cooldown(self) -> bool: - if cooldown.is_elapsed(self.cooldown): - self.cooldown = cooldown.get_time() - return True - else: - return False - def open_keybindings(self) -> None: notebook = self.mediator.grid.notebook notebook.toggle_keybindings() @@ -329,6 +321,7 @@ class Controller: notebook = self.mediator.grid.notebook notebook.focus_current() + # TODO: deprecated def spawn_dialog(self, msg: str, mode: Popup) -> bool: """ Spawns a GenericDialog transient to the OuterWindow diff --git a/dzgui/data/CHANGELOG.md b/dzgui/data/CHANGELOG.md index 6ff240a..053e26e 100644 --- a/dzgui/data/CHANGELOG.md +++ b/dzgui/data/CHANGELOG.md @@ -26,6 +26,7 @@ - Reduce padding on keys button - Boldface breadcrumbs - Bold labels inside frames +- Unsticky keys when clicking sidebar ## Unreleased - Setup wizard diff --git a/dzgui/main.py b/dzgui/main.py index a01a86c..a2bcd57 100644 --- a/dzgui/main.py +++ b/dzgui/main.py @@ -3,14 +3,13 @@ import logging import os import sys -from dzgui.api.mods import remove_stale_signatures +from typing import TYPE_CHECKING +from dzgui.api.mods import remove_stale_signatures from dzgui.config.ipdb import get_ipdb from dzgui.config.userprefs import UserPrefs from dzgui.config.xdg import get_xdg_paths, parse_filepaths - from dzgui.const.update import ALLOW_UPDATES - from dzgui.init.coords import get_local_coords from dzgui.init.dayz import is_dayz_installed from dzgui.init.flock import lock_acquire @@ -42,8 +41,6 @@ parser.add_argument("-u", "--uninstall", action="store_true", help=flags.uninsta parser.add_argument("-d", "--developers", action="store_true", help=flags.developers) args = parser.parse_args() -from typing import TYPE_CHECKING - if TYPE_CHECKING: from pathlib import Path @@ -110,7 +107,6 @@ def main() -> None: # TODO: move into module if has_steam_client() is False: EarlyAlertDialog(init.requires_steam) - sys.exit(1) is_dayz_installed(XDG.config) is_dayz_running() diff --git a/dzgui/util/cooldown.py b/dzgui/util/cooldown.py deleted file mode 100644 index 413a4d0..0000000 --- a/dzgui/util/cooldown.py +++ /dev/null @@ -1,16 +0,0 @@ -from datetime import datetime - -def get_time() -> int: - return int(datetime.now().timestamp()) - -# TODO: deprecated -#def write_time(path: Path) -> None: -# time = get_time() -# Path.write_text(str(time)) - -def is_elapsed(time_before: int) -> bool: - time_now = get_time() - if time_now - time_before <= 30: - return False - else: - return True diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index d4da3c4..7594aac 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -86,9 +86,10 @@ scan_servers = "Scan LAN servers" select_port = "Select the query port" # Errors +error_heading = "Error" config_not_found = "DZGUI configuration file not found. Please exit and restart." cannot_acquire_lock = "DZGUI is already open." -something_wrong = "Something went wrong." +something_wrong = "Something went wrong. See the detailed error below." steam_key_missing = "No Steam API key is set." malformed_mods = "Found mods on system, but was unable to parse results." steam_missing = "Local Steam installation is not set, possibly malformed config file." @@ -154,8 +155,6 @@ label_main_menu = "Main menu" # buttons ping_servers = "Ping servers" debug_mode = "Debug mode" -keys_button = "Keys" -keys_tooltip = "Toggles the keybindings dialog" debug_tooltip = ( "Used to perform a dry run without\n" "actually connecting to a server" @@ -539,6 +538,20 @@ connect_panel = ConnectPanel( ) distance_suffix = "Distance: calculating..." +dialog_error = "ERROR" -refresh = "Refresh" -refresh_tooltip = "Refresh server data" +@dataclass(slots=True, frozen=True) +class AtomicButton: + refresh: str + refresh_tooltip: str + copy: str + keys: str + keys_tooltip: str + +atomic_buttons = AtomicButton( + refresh="Refresh", + refresh_tooltip="Refresh server data", + copy="Copy", + keys="Keys", + keys_tooltip="Toggles the keybindings dialog", +) diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 19a6094..3982d20 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -8,7 +8,6 @@ import signal import subprocess import textwrap import threading -import traceback import typing # noqa import warnings @@ -466,3 +465,8 @@ class App(Gtk.Application): self.win.halt_proc_and_quit() MainController = Controller() + +# TODO: icon sanity check, log result +#theme = Gtk.IconTheme.get_default() +#icons = theme.list_icons(None) +#print("steam_tray_mono" in icons) diff --git a/dzgui/views/components/buttons.py b/dzgui/views/components/buttons.py index 8517a11..76064ad 100644 --- a/dzgui/views/components/buttons.py +++ b/dzgui/views/components/buttons.py @@ -1,6 +1,6 @@ -from typing import Callable, Self, TYPE_CHECKING +from typing import Self, TYPE_CHECKING -from dzgui.util.strings import refresh, connect_panel +from dzgui.util.strings import atomic_buttons, connect_panel from dzgui.const.constants import ( CLIPBOARD, INPUT_KEYBOARD, @@ -47,6 +47,8 @@ class IconTextButton(IconButton): super().__init__(icon, margin=5) self.set_label(label) + self.set_focus_on_click(False) + class LargeIconTextButton(IconButton): def __init__(self, icon: str, label: str) -> None: @@ -56,12 +58,14 @@ class LargeIconTextButton(IconButton): self.set_image(LargeIcon(icon)) -class ClipboardButton(IconButton): +class ClipboardButton(IconTextButton): def __init__(self, controller: "Controller", data: str) -> None: - super().__init__(CLIPBOARD) + super().__init__(CLIPBOARD, atomic_buttons.copy) self.controller = controller self.connect("clicked", self._on_button_clicked, data) + self.set_tooltip_text("Copy IP to clipboard") + def _on_button_clicked(self, button: Self, data: str) -> None: self.controller.copy_clipboard(data) @@ -74,19 +78,21 @@ class WebButton(IconTextButton): class RefreshButton(IconTextButton): def __init__(self) -> None: - super().__init__(icon=REFRESH_ICON, label=refresh) + super().__init__(icon=REFRESH_ICON, label=atomic_buttons.refresh) self.set_margin_top(10) self.set_margin_start(80) self.set_margin_end(80) class KeysButton(IconTextButton): - def __init__(self, label: str) -> None: - super().__init__(icon=INPUT_KEYBOARD, label=label) + def __init__(self) -> None: + super().__init__(icon=INPUT_KEYBOARD, label=atomic_buttons.keys) self.set_margin_top(10) self.set_margin_start(80) self.set_margin_end(80) + self.set_tooltip_text(atomic_buttons.keys_tooltip) + class SteamConnectButton(LargeIconTextButton): def __init__(self) -> None: diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index 03055c3..136ac38 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -1,7 +1,6 @@ from typing import TYPE_CHECKING from dzgui.api.servers import validate_ip -from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING from dzgui.util.css import add_class, remove_class from dzgui.util.strings import connect_panel from dzgui.views.components.buttons import AddButton, ClipboardButton, SteamConnectButton @@ -14,6 +13,9 @@ from gi.repository import Gtk, Gdk # noqa E402 if TYPE_CHECKING: from dzgui.controllers.mc import Controller +COLS = 1 +ROWS = 1 + class LanPanel(Gtk.Frame): def __init__(self, controller: "Controller") -> None: super().__init__(margin_top=10, margin_bottom=5) @@ -24,26 +26,24 @@ class LanPanel(Gtk.Frame): self.set_label_widget(label) radio1 = Gtk.RadioButton.new_with_label(None, "Default port (27016)") - radio2 = Gtk.RadioButton.new_from_widget(radio1) - # TODO: new_with_label_from_widget - radio2.set_label("Custom port") + radio2 = Gtk.RadioButton.new_with_label_from_widget(radio1, "Custom port") - self.entry = Gtk.Entry(placeholder_text="Query port", sensitive=False) + self.entry = Gtk.Entry(placeholder_text="Enter the query port (1-65535)", sensitive=False, hexpand=True) self.button = Gtk.Button(label="Scan") radio1.connect("toggled", self._on_radio_toggled) - # TODO: use grid, more column spacing - hbox = Gtk.Box(margin=10, spacing=5, halign=Gtk.Align.START) - hbox.pack_start(radio1, NO_EXPAND, NO_FILL, NO_PADDING) - hbox.pack_start(radio2, NO_EXPAND, NO_FILL, NO_PADDING) - hbox.pack_start(self.entry, NO_EXPAND, NO_FILL, NO_PADDING) - hbox.pack_start(self.button, NO_EXPAND, NO_FILL, NO_PADDING) + self.grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5) + self.grid.attach(radio1, 0, 0, COLS, ROWS) + # TODO: expression to attach in a line + self.grid.attach_next_to(radio2, radio1, Gtk.PositionType.RIGHT, COLS, ROWS) + self.grid.attach_next_to(self.entry, radio2, Gtk.PositionType.RIGHT, COLS, ROWS) + self.grid.attach_next_to(self.button, self.entry, Gtk.PositionType.RIGHT, COLS, ROWS) - self.add(hbox) + self.add(self.grid) def _on_radio_toggled(self, button: Gtk.RadioButton) -> None: - for el in self.entry, self.button: - el.set_sensitive(not button.get_active()) + self.entry.set_sensitive(not button.get_active()) + # TODO: validate custom port entry class AddPanel(Gtk.Frame): def __init__(self, controller: "Controller") -> None: @@ -52,14 +52,10 @@ class AddPanel(Gtk.Frame): label = BoldLabel("Connect") self.set_label_widget(label) -class ConnectPanel(Gtk.Box): - def __init__(self, controller: "Controller") -> None: - super().__init__(orientation=Gtk.Orientation.VERTICAL) - - COLS = 1 - ROWS = 1 - self.classname = "invalid-entry" - self.controller = controller + self.add_server = AddButton() + self.conn_server = SteamConnectButton() + self.conn_server.set_sensitive(False) + self.add_server.set_sensitive(False) self.entry = Gtk.Entry( placeholder_text=connect_panel.placeholder, @@ -69,24 +65,6 @@ class ConnectPanel(Gtk.Box): self.entry.connect("key-press-event", self._on_entry_keypress) self.entry.connect("changed", self._on_text_changed) - user_fav, self.fav_ip = self.controller.get_favorite() - - server_name = f"{user_fav} ({self.fav_ip})" if user_fav is not None else connect_panel.no_fav - self.fav_label = Gtk.Label(label=server_name, track_visited_links=False, halign=Gtk.Align.START, hexpand=True) - scrollable_label = Gtk.ScrolledWindow() - scrollable_label.add(self.fav_label) - - self.fav_button = SteamConnectButton() - self.fav_edit = Gtk.Button(label=connect_panel.edit) - - self.add_server = AddButton() - self.conn_server = SteamConnectButton() - - self.conn_server.set_sensitive(False) - self.add_server.set_sensitive(False) - if server_name is None: - self.fav_button.set_sensitive(False) - self.grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5) self.grid.attach(self.entry, 0, 0, COLS, ROWS) @@ -98,32 +76,7 @@ class ConnectPanel(Gtk.Box): for el, sibling, pos, h_span, v_span in els: self.grid.attach_next_to(el, sibling, pos, h_span, v_span) - self.lan = LanPanel(self.controller) - self.add(self.lan) - - label = BoldLabel("Favorite server") - frame = Gtk.Frame(margin_top=10, margin_bottom=5, label_widget=label) - b = ClipboardButton(self.controller, self.get_fav_ip()) - b.set_focus_on_click(False) - b.set_tooltip_text("Copy IP to clipboard") - - # FIXME: height of connect buttons is not equivalent - grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5) - grid.attach(scrollable_label, 0, 0, 3, ROWS) - grid.attach_next_to(b, scrollable_label, Gtk.PositionType.RIGHT, COLS, ROWS) - grid.attach_next_to(self.fav_button, b, Gtk.PositionType.RIGHT, COLS, ROWS) - frame.add(grid) - self.add(frame) - - label = BoldLabel("Connect") - frame = Gtk.Frame(margin_top=10, margin_bottom=5, label_widget=label) - frame.add(self.grid) - self.add(frame) - - #self.lan.set_visible(False) - - def get_fav_ip(self) -> str: - return self.fav_ip + self.add(self.grid) def mark_valid(self) -> None: self.conn_server.set_sensitive(True) @@ -152,12 +105,53 @@ class ConnectPanel(Gtk.Box): else: self.mark_invalid() - def set_fav_label(self, text: str) -> None: - # TODO: called by controller when changing fav - self.fav_label.set_text(text) - def _on_entry_keypress(self, entry: Gtk.Entry, event: Gdk.EventKey) -> None: if event.keyval == Gdk.KEY_Escape: # NOTE: unselect text entry.select_region(0, 0) self.controller.grab_active_treeview() + + +class ConnectPanel(Gtk.Box): + def __init__(self, controller: "Controller") -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + + self.classname = "invalid-entry" + self.controller = controller + + user_fav, self.fav_ip = self.controller.get_favorite() + + server_name = f"{user_fav} ({self.fav_ip})" if user_fav is not None else connect_panel.no_fav + self.fav_label = Gtk.Label(label=server_name, track_visited_links=False, halign=Gtk.Align.START, hexpand=True) + scrollable_label = Gtk.ScrolledWindow() + scrollable_label.add(self.fav_label) + + self.fav_button = SteamConnectButton() + self.fav_edit = Gtk.Button(label=connect_panel.edit) + if server_name is None: + self.fav_button.set_sensitive(False) + + self.lan = LanPanel(self.controller) + self.add_panel = AddPanel(self.controller) + + label = BoldLabel("Favorite server") + frame = Gtk.Frame(margin_top=10, margin_bottom=5, label_widget=label) + b = ClipboardButton(self.controller, self.get_fav_ip()) + + # FIXME: height of connect buttons is not equivalent + grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5) + grid.attach(scrollable_label, 0, 0, 3, ROWS) + grid.attach_next_to(b, scrollable_label, Gtk.PositionType.RIGHT, COLS, ROWS) + grid.attach_next_to(self.fav_button, b, Gtk.PositionType.RIGHT, COLS, ROWS) + frame.add(grid) + + for el in self.lan, frame, self.add_panel: + self.add(el) + + def get_fav_ip(self) -> str: + return self.fav_ip + + def set_fav_label(self, text: str) -> None: + # TODO: called by controller when changing fav + self.fav_label.set_text(text) + diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index c657c8a..b994ee0 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -10,7 +10,7 @@ from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.mod_panel import ModSelectionPanel from dzgui.views.components.buttons import RefreshButton, KeysButton from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL, INPUT_KEYBOARD, NO_PADDING -from dzgui.util.strings import refresh_tooltip, refresh, keys_button, keys_tooltip +from dzgui.util.strings import atomic_buttons if TYPE_CHECKING: from GLib import SOURCE_REMOVE @@ -31,14 +31,13 @@ class RightPanel(Gtk.Box): self.refresh_button = RefreshButton() # TODO: consolidate into class - self.refresh_button.set_tooltip_text(refresh_tooltip) + #self.refresh_button.set_tooltip_text(refresh_tooltip) self.refresh_button.set_focus_on_click(False) self.refresh_button.connect("clicked", self._on_refresh_clicked) - self.keys = KeysButton(keys_button) - self.keys.set_focus_on_click(False) - self.keys.set_tooltip_text(keys_tooltip) + self.keys = KeysButton() self.keys.connect("clicked", self._on_keys_clicked) + #self.keys.set_focus_on_click(False) for el in self.button_vbox, self.keys, self.filters_vbox, self.refresh_button: self.pack_start(el, NO_EXPAND, FILL, NO_PADDING) @@ -61,13 +60,14 @@ class RightPanel(Gtk.Box): self.refresh_button.set_label(refresh) self.refresh_button.set_sensitive(True) return GLib.SOURCE_REMOVE - self.refresh_button.set_label(f"{refresh} ({str(self.time)})") + # TODO: abstract this formatting logic + self.refresh_button.set_label(f"{atomic_buttons.refresh} ({str(self.time)})") return True def _on_refresh_clicked(self, button: RefreshButton) -> None: # TODO: could be internal to refresh button class self.refresh_button.set_sensitive(False) - self.refresh_button.set_label(f"{refresh} ({str(self.time)})") + self.refresh_button.set_label(f"{atomic_buttons.refresh} ({str(self.time)})") GLib.timeout_add_seconds(1, self.decrement) self.controller.refresh_tree() diff --git a/dzgui/views/dialogs/early_alert.py b/dzgui/views/dialogs/early_alert.py index 03b70ff..f4de721 100644 --- a/dzgui/views/dialogs/early_alert.py +++ b/dzgui/views/dialogs/early_alert.py @@ -1,22 +1,21 @@ import textwrap +import sys + +from typing import Self +from dzgui.util.strings import dialog_error, dialog_header import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 class EarlyAlertDialog(Gtk.MessageDialog): - def __init__(self, string): - # TODO: strings + def __init__(self, string: str) -> None: super().__init__( - title="DZGUI - Dialog", - flags=0, - text="ERROR", + title=dialog_header, + text=dialog_error, transient_for=None, buttons=Gtk.ButtonsType.OK ) - ok = self.action_area.get_children()[0] - ok.set_label("OK") - ok.connect("clicked", Gtk.main_quit) msg = textwrap.fill(string, 50) self.format_secondary_text(msg) @@ -28,8 +27,10 @@ class EarlyAlertDialog(Gtk.MessageDialog): self.set_default_size(250, 100) - self.connect("delete-event", Gtk.main_quit) - self.connect("destroy", Gtk.main_quit) + self.connect("response", self._on_response) - self.show_all() - Gtk.main() + self.run() + self.destroy() + + def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None: + sys.exit(1) diff --git a/dzgui/views/dialogs/generic.py b/dzgui/views/dialogs/generic.py index e834e85..423caad 100644 --- a/dzgui/views/dialogs/generic.py +++ b/dzgui/views/dialogs/generic.py @@ -4,147 +4,103 @@ from typing import Any, Literal, Self, TYPE_CHECKING from dzgui.const.constants import NO_EXPAND, NO_FILL, EXPAND, FILL from dzgui.const.enum import Popup, ButtonType, NotebookPage from dzgui.util import strings +from dzgui.views.components.buttons import ClipboardButton import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402 - if TYPE_CHECKING: from dzgui.controllers.mc import Controller -class ExceptionDialog(Gtk.MessageDialog): - def __init__(self, controller: "Controller", trace: str): - super().__init__( - transient_for=controller.mediator.window, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text="Error", - secondary_text="Something went wrong. See the detailed error below.", - title=strings.dialog_header, - modal=True, - ) - - # TODO: strings - self.set_size_request(550, 250) - - from dzgui.views.components.buttons import ClipboardButton - content = self.get_content_area() - content.set_spacing(0) - - box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL) - textview = Gtk.TextView(wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10) - textview.set_buffer(Gtk.TextBuffer(text=trace)) - # NOTE: box expands to end of content area - box.pack_start(textview, EXPAND, FILL, 10) - - action_area = self.get_action_area() - action_area.set_spacing(10) - action_area.set_margin_bottom(10) - action_area.set_layout(Gtk.ButtonBoxStyle.CENTER) - - but = ClipboardButton(controller, trace) - # TODO: flag to set button with text, or other class - but.set_label("Copy") - action_area.pack_start(but, True, True, 10) - # NOTE: reverse button order after insertion - action_area.set_direction(Gtk.TextDirection.RTL) - content.add(box) - - self.set_default_response(Gtk.ResponseType.OK) - self.connect("response", self._on_response) - self.show_all() - """ - usage: - from dzgui.views.dialogs.generic import ExceptionDialog - try: - a.banana() - except Exception as e: - trace = traceback.format_exc() - dialog = ExceptionDialog(MainController, trace) - dialog.run() - """ - - - def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None: - self.destroy() +# TODO: reimplement as standalone dialogs +# NOTE: steam deck prints <2> if dialog title is same as window title +# case Popup.MODLIST: +# dialog_type = Gtk.MessageType.INFO +# button_type = Gtk.ButtonsType.OK +# header_text = strings.modlist +# case Popup.DETAILS: +# dialog_type = Gtk.MessageType.INFO +# button_type = Gtk.ButtonsType.OK +# header_text = strings.server_details +# TODO: unused +# def update_label(self, text: str) -> None: +# self.format_secondary_text(text) class GenericDialog(Gtk.MessageDialog): - def __init__(self, controller: "Controller", text: str, mode: Popup): - match mode: - case Popup.WAIT: - dialog_type = Gtk.MessageType.INFO - button_type = Gtk.ButtonsType.NONE - header_text = strings.wait - case Popup.NOTIFY | Popup.RETURN | Popup.QUIT: - dialog_type = Gtk.MessageType.INFO - button_type = Gtk.ButtonsType.OK - header_text = strings.notice - case Popup.CONFIRM: - dialog_type = Gtk.MessageType.QUESTION - button_type = Gtk.ButtonsType.OK_CANCEL - header_text = strings.confirmation - case Popup.ENTRY: - dialog_type = Gtk.MessageType.QUESTION - button_type = Gtk.ButtonsType.OK_CANCEL - header_text = strings.input_required - case Popup.MODLIST: - dialog_type = Gtk.MessageType.INFO - button_type = Gtk.ButtonsType.OK - header_text = strings.modlist - case Popup.DETAILS: - dialog_type = Gtk.MessageType.INFO - button_type = Gtk.ButtonsType.OK - header_text = strings.server_details - - Gtk.MessageDialog.__init__( - self, + def __init__(self, + controller: "Controller", + text: str, + mtype: Gtk.MessageType, + buttons: Gtk.ButtonsType, + secondary: str + ) -> None: + super().__init__( transient_for=controller.mediator.window, - message_type=dialog_type, - buttons=button_type, - text=header_text, - secondary_text=textwrap.fill(text, 50), - # NOTE: steam deck prints <2> if dialog title is same as window title + message_type=mtype, + buttons=buttons, + text=text, + secondary_text=secondary, title=strings.dialog_header, modal=True, ) - self.controller = controller - - if mode == Popup.WAIT: - dialogBox = self.get_content_area() - spinner = Gtk.Spinner() - dialogBox.pack_end(spinner, NO_EXPAND, NO_FILL, 0) - spinner.start() - self.connect("delete-event", self._on_dialog_delete) - - if mode == Popup.RETURN: - button_label = strings.main_menu - ok = self.action_area.get_children()[0] - ok.set_label(button_label) - ok.connect("clicked", self._return_to_main_menu) - self.connect("delete-event", self._return_to_main_menu) - - if mode == Popup.QUIT: - button_label = strings.exit_app - ok = self.action_area.get_children()[0] - ok.set_label(button_label) - ok.connect("clicked", self._quit) - self.connect("delete-event", self._quit) - - self.set_default_response(Gtk.ResponseType.OK) self.set_size_request(500, 0) self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.set_default_response(Gtk.ResponseType.OK) + self.connect("response", self._on_response) + self.action_area.set_layout(Gtk.ButtonBoxStyle.CENTER) self.action_area.set_margin_bottom(20) self.outer = self.get_content_area() self.outer.set_margin_start(30) self.outer.set_margin_end(30) - def _quit(self, *args: Any) -> None: - self.controller.save_res_and_quit() + def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None: + self.destroy() + + +class ConfirmationDialog(GenericDialog): + def __init__(self, controller: "Controller", secondary: str): + super().__init__( + controller=controller, + text=strings.confirm, + mtype=Gtk.MessageType.QUESTION, + buttons=Gtk.ButtonsType.OK_CANCEL, + secondary=secondary, + ) + + +class NotifyDialog(GenericDialog): + def __init__(self, controller: "Controller", secondary: str): + super().__init__( + controller=controller, + text=strings.notice, + mtype=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + secondary=secondary, + ) + + +class WaitDialog(GenericDialog): + def __init__(self, controller: "Controller", secondary: str): + super().__init__( + controller=controller, + text=strings.wait, + mtype=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.NONE, + secondary=secondary, + ) + + self.connect("delete-event", self._on_dialog_delete) + content = self.get_content_area() + spinner = Gtk.Spinner() + content.pack_end(spinner, NO_EXPAND, NO_FILL, 0) + + spinner.start() + self.show_all() def _on_dialog_delete( self, response_id: Gtk.ResponseType, event: Gdk.Event @@ -154,8 +110,81 @@ class GenericDialog(Gtk.MessageDialog): """ return True - def _return_to_main_menu(self, widget: Gtk.Widget) -> None: - self.controller.open_page(NotebookPage.MAIN) - def update_label(self, text: str) -> None: - self.format_secondary_text(text) +class QuitDialog(GenericDialog): + def __init__(self, controller: "Controller", secondary: str): + super().__init__( + controller=controller, + text=strings.wait, + mtype=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.NONE, + secondary=secondary, + ) + + self.controller = controller + self.add_button(strings.exit_app, Gtk.ResponseType.OK) + + self.connect("response", self._on_response) + self.connect("delete-event", self._on_response) + + def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None: + self.controller.save_res_and_quit() + + +class ExceptionDialog(GenericDialog): + """ + Error dialog with rich traceback. + Usage: + try: + foo() + except Exception: + trace = traceback.format_exc() + dialog = ExceptionDialog(Controller, trace) + dialog.run() + """ + def __init__(self, controller: "Controller", trace: str): + super().__init__( + controller=controller, + text=strings.error_heading, + mtype=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.NONE, + secondary=strings.something_wrong, + ) + + # NOTE: box expands to end of content area + scrollable = Gtk.ScrolledWindow( + propagate_natural_height=True, + max_content_height=500 + ) + box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL) + textview = Gtk.TextView( + wrap_mode=Gtk.WrapMode.WORD, + editable=False, + left_margin=10, + right_margin=10 + ) + textview.set_buffer(Gtk.TextBuffer(text=trace)) + box.pack_start(textview, EXPAND, FILL, 10) + scrollable.add(box) + + content = self.get_content_area() + content.set_spacing(0) + # FIXME: padding around top of content area when traceback is long + content.add(scrollable) + + copy_button = ClipboardButton(controller, trace) + self.add_action_widget(copy_button, Gtk.ResponseType.NONE) + self.add_button("OK", Gtk.ResponseType.OK) + action_area = self.get_action_area() + self.show_all() + + self.connect("response", self._on_response) + + def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None: + match response: + case Gtk.ResponseType.OK: + self.destroy() + case Gtk.ResponseType.NONE: + return True # type: ignore + case Gtk.ResponseType.DELETE_EVENT: + self.destroy() diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index 8cf053c..efcaf90 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -424,7 +424,6 @@ class Options(Gtk.Box): return box def populate_settings(self) -> None: - # TODO: controller.get_config() prefs = self.controller.get_prefs() if prefs.paths.config.is_file() is False: # NOTE: in case file got deleted locally diff --git a/tests/test_cooldown.py b/tests/test_cooldown.py deleted file mode 100644 index 32ce40c..0000000 --- a/tests/test_cooldown.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest -import time - -from dzgui.util import cooldown - - -def test_time_under(): - time_before = cooldown.get_time() - time.sleep(1) - result = cooldown.is_elapsed(time_before) - assert result is False - - -@pytest.mark.slow -def test_time_over(): - time_before = cooldown.get_time() - time.sleep(31) - result = cooldown.is_elapsed(time_before) - assert result is True