From ee8fa1da0ac1752e2a92449b308bb1efe90d799c Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 9 Mar 2024 16:45:54 +0100 Subject: [PATCH 1/2] vm-manager: add toast overlay to main window --- pkgs/clan-vm-manager/README.md | 6 +++ .../clan_vm_manager/assets/style.css | 1 + .../clan_vm_manager/singletons/toast.py | 54 +++++++++++++++++++ .../clan_vm_manager/views/list.py | 22 ++++++-- .../clan_vm_manager/windows/main_window.py | 8 ++- 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py diff --git a/pkgs/clan-vm-manager/README.md b/pkgs/clan-vm-manager/README.md index 02a1366ce..8a31c44eb 100644 --- a/pkgs/clan-vm-manager/README.md +++ b/pkgs/clan-vm-manager/README.md @@ -86,3 +86,9 @@ Here are some important documentation links related to the Clan VM Manager: - [Python + GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/textview.html): Although the Clan VM Manager uses GTK4, this tutorial for GTK3 can still be useful as it covers the basics of building GTK-based applications with Python. It includes examples and explanations for various GTK widgets, including text views. - [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/): This link provides the GNOME Human Interface Guidelines, which offer design and usability recommendations for creating GNOME applications. It covers topics such as layout, navigation, and interaction patterns. + +## Error handling + +> Error dialogs should be avoided where possible, since they are disruptive. +> +> For simple non-critical errors, toasts can be a good alternative. \ No newline at end of file diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css index 1284730e2..5799ba2ab 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css +++ b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css @@ -17,6 +17,7 @@ avatar { } .join-list { + margin-top: 1px; margin-left: 2px; margin-right: 2px; diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py new file mode 100644 index 000000000..41acfc616 --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py @@ -0,0 +1,54 @@ +import logging +from typing import Any + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + +from gi.repository import Adw + +log = logging.getLogger(__name__) + +class ToastOverlay: + """ + The ToastOverlay is a class that manages the display of toasts + It should be used as a singleton in your application to prevent duplicate toasts + Usage + """ + # For some reason, the adw toast overlay cannot be subclassed + # Thats why it is added as a class property + overlay: Adw.ToastOverlay + active_toasts: set[str] + + _instance: "None | ToastOverlay" = None + + def __init__(self) -> None: + raise RuntimeError("Call use() instead") + + @classmethod + def use(cls: Any) -> "ToastOverlay": + if cls._instance is None: + cls._instance = cls.__new__(cls) + cls.overlay = Adw.ToastOverlay() + cls.active_toasts = set() + + return cls._instance + + def add_toast_unique(self, toast: Adw.Toast, key: str) -> None: + if key not in self.active_toasts: + self.active_toasts.add(key) + self.overlay.add_toast(toast) + toast.connect("dismissed", lambda toast: self.active_toasts.remove(key)) + + +class ErrorToast: + toast: Adw.Toast + + def __init__(self, message: str): + super().__init__() + self.toast = Adw.Toast.new(f"Error: {message}") + self.toast.set_priority(Adw.ToastPriority.HIGH) + + + \ No newline at end of file diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index 438e2452d..c89cf8785 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -9,6 +9,7 @@ from clan_cli.clan_uri import ClanURI from clan_vm_manager.components.interfaces import ClanConfig from clan_vm_manager.components.vmobj import VMObject +from clan_vm_manager.singletons.toast import ErrorToast, ToastOverlay from clan_vm_manager.singletons.use_join import JoinList, JoinValue from clan_vm_manager.singletons.use_vms import ClanStore, VMStore @@ -86,7 +87,7 @@ class ClanList(Gtk.Box): assert app is not None app.add_action(add_action) - menu_model = Gio.Menu() + # menu_model = Gio.Menu() # TODO: Make this lazy, blocks UI startup for too long # for vm in machines.list.list_machines(flake_url=vm.data.flake.flake_url): # if vm not in vm_store: @@ -95,10 +96,18 @@ class ClanList(Gtk.Box): box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) box.set_valign(Gtk.Align.CENTER) - add_button = Gtk.MenuButton() - add_button.set_has_frame(False) - add_button.set_menu_model(menu_model) - add_button.set_label("Add machine") + + add_button = Gtk.Button() + add_button_content = Adw.ButtonContent.new() + add_button_content.set_label("Add machine") + add_button_content.set_icon_name("list-add-symbolic") + add_button.add_css_class("flat") + add_button.set_child(add_button_content) + + + # add_button.set_has_frame(False) + # add_button.set_menu_model(menu_model) + # add_button.set_label("Add machine") box.append(add_button) grp.set_header_suffix(box) @@ -207,6 +216,9 @@ class ClanList(Gtk.Box): if vm is not None: sub = row.get_subtitle() assert sub is not None + + ToastOverlay.use().add_toast_unique(ErrorToast("Already exists. Joining again will update it").toast, "warning.duplicate.join") + row.set_subtitle( sub + "\nClan already exists. Joining again will update it" ) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index 895acc808..a7b2cda3a 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -5,6 +5,7 @@ import gi from clan_cli.history.list import list_history from clan_vm_manager.components.interfaces import ClanConfig +from clan_vm_manager.singletons.toast import ToastOverlay from clan_vm_manager.singletons.use_views import ViewStack from clan_vm_manager.singletons.use_vms import ClanStore from clan_vm_manager.views.details import Details @@ -24,9 +25,12 @@ class MainWindow(Adw.ApplicationWindow): super().__init__() self.set_title("cLAN Manager") self.set_default_size(980, 650) - + + overlay = ToastOverlay.use().overlay view = Adw.ToolbarView() - self.set_content(view) + overlay.set_child(view) + + self.set_content(overlay) header = Adw.HeaderBar() view.add_top_bar(header) From 4687c816ab76ec39566cb35b7f3fe6582a2c4809 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 10 Mar 2024 13:18:01 +0100 Subject: [PATCH 2/2] clan-vm-manager: add log view --- .../clan_vm_manager/assets/style.css | 7 ++ .../clan_vm_manager/singletons/toast.py | 21 +++++- .../clan_vm_manager/views/list.py | 9 ++- .../clan_vm_manager/views/logs.py | 69 +++++++++++++++++++ .../clan_vm_manager/windows/main_window.py | 4 +- 5 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/views/logs.py diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css index 5799ba2ab..c179744dd 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css +++ b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css @@ -57,3 +57,10 @@ avatar { searchbar { margin-bottom: 25px; } + + +.log-view { + margin-top: 12px; + font-family: monospace; + padding: 8px; +} diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py index 41acfc616..941fc5d93 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py @@ -8,16 +8,21 @@ gi.require_version("Adw", "1") from gi.repository import Adw +from clan_vm_manager.singletons.use_views import ViewStack +from clan_vm_manager.views.logs import Logs + log = logging.getLogger(__name__) + class ToastOverlay: """ The ToastOverlay is a class that manages the display of toasts It should be used as a singleton in your application to prevent duplicate toasts Usage """ + # For some reason, the adw toast overlay cannot be subclassed - # Thats why it is added as a class property + # Thats why it is added as a class property overlay: Adw.ToastOverlay active_toasts: set[str] @@ -45,10 +50,20 @@ class ToastOverlay: class ErrorToast: toast: Adw.Toast - def __init__(self, message: str): + def __init__(self, message: str) -> None: super().__init__() self.toast = Adw.Toast.new(f"Error: {message}") self.toast.set_priority(Adw.ToastPriority.HIGH) + self.toast.set_button_label("details") - \ No newline at end of file + views = ViewStack.use().view + + # we cannot check this type, python is not smart enough + logs_view: Logs = views.get_child_by_name("logs") # type: ignore + logs_view.set_message(message) + + self.toast.connect( + "button-clicked", + lambda _: views.set_visible_child_name("logs"), + ) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index c89cf8785..c9e30da0b 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -4,7 +4,6 @@ from functools import partial from typing import Any, TypeVar import gi -from clan_cli import history from clan_cli.clan_uri import ClanURI from clan_vm_manager.components.interfaces import ClanConfig @@ -57,7 +56,6 @@ class ClanList(Gtk.Box): app.connect("join_request", self.on_join_request) self.log_label: Gtk.Label = Gtk.Label() - self.__init_machines = history.add.list_history() # Add join list self.join_boxed_list = create_boxed_list( @@ -96,7 +94,6 @@ class ClanList(Gtk.Box): box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) box.set_valign(Gtk.Align.CENTER) - add_button = Gtk.Button() add_button_content = Adw.ButtonContent.new() add_button_content.set_label("Add machine") @@ -104,7 +101,6 @@ class ClanList(Gtk.Box): add_button.add_css_class("flat") add_button.set_child(add_button_content) - # add_button.set_has_frame(False) # add_button.set_menu_model(menu_model) # add_button.set_label("Add machine") @@ -217,7 +213,10 @@ class ClanList(Gtk.Box): sub = row.get_subtitle() assert sub is not None - ToastOverlay.use().add_toast_unique(ErrorToast("Already exists. Joining again will update it").toast, "warning.duplicate.join") + ToastOverlay.use().add_toast_unique( + ErrorToast("Already exists. Joining again will update it").toast, + "warning.duplicate.join", + ) row.set_subtitle( sub + "\nClan already exists. Joining again will update it" diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py b/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py new file mode 100644 index 000000000..2374ba7f8 --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py @@ -0,0 +1,69 @@ +import logging + +import gi + +gi.require_version("Adw", "1") +from gi.repository import Adw, Gio, Gtk + +from clan_vm_manager.singletons.use_views import ViewStack + +log = logging.getLogger(__name__) + + +class Logs(Gtk.Box): + """ + Simple log view + This includes a banner and a text view and a button to close the log and navigate back to the overview + """ + + def __init__(self) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + + app = Gio.Application.get_default() + assert app is not None + + self.banner = Adw.Banner.new("Error details") + self.banner.set_revealed(True) + + close_button = Gtk.Button() + button_content = Adw.ButtonContent.new() + button_content.set_label("Back") + button_content.set_icon_name("go-previous-symbolic") + close_button.add_css_class("flat") + close_button.set_child(button_content) + close_button.connect( + "clicked", + lambda _: ViewStack.use().view.set_visible_child_name("list"), + ) + + self.close_button = close_button + + self.text_view = Gtk.TextView() + self.text_view.set_editable(False) + self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) + self.text_view.add_css_class("log-view") + + self.append(self.close_button) + self.append(self.banner) + self.append(self.text_view) + + def set_message(self, message: str) -> None: + """ + Set the log message. This will delete any previous message + """ + buffer = self.text_view.get_buffer() + buffer.set_text(message) + + mark = buffer.create_mark(None, buffer.get_end_iter(), False) # type: ignore + self.text_view.scroll_to_mark(mark, 0.05, True, 0.0, 1.0) + + def append_message(self, message: str) -> None: + """ + Append to the end of a potentially existent log message + """ + buffer = self.text_view.get_buffer() + end_iter = buffer.get_end_iter() + buffer.insert(end_iter, message) # type: ignore + + mark = buffer.create_mark(None, buffer.get_end_iter(), False) # type: ignore + self.text_view.scroll_to_mark(mark, 0.05, True, 0.0, 1.0) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index a7b2cda3a..77bd4e870 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -10,6 +10,7 @@ from clan_vm_manager.singletons.use_views import ViewStack from clan_vm_manager.singletons.use_vms import ClanStore from clan_vm_manager.views.details import Details from clan_vm_manager.views.list import ClanList +from clan_vm_manager.views.logs import Logs gi.require_version("Adw", "1") @@ -25,7 +26,7 @@ class MainWindow(Adw.ApplicationWindow): super().__init__() self.set_title("cLAN Manager") self.set_default_size(980, 650) - + overlay = ToastOverlay.use().overlay view = Adw.ToolbarView() overlay.set_child(view) @@ -52,6 +53,7 @@ class MainWindow(Adw.ApplicationWindow): stack_view.add_named(scroll, "list") stack_view.add_named(Details(), "details") + stack_view.add_named(Logs(), "logs") stack_view.set_visible_child_name(config.initial_view)