clan-vm-manager: add log view #927
@ -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.
|
@ -17,6 +17,7 @@ avatar {
|
||||
}
|
||||
|
||||
.join-list {
|
||||
margin-top: 1px;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
|
||||
@ -56,3 +57,10 @@ avatar {
|
||||
searchbar {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
|
||||
.log-view {
|
||||
margin-top: 12px;
|
||||
font-family: monospace;
|
||||
padding: 8px;
|
||||
}
|
||||
|
69
pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py
Normal file
69
pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py
Normal file
@ -0,0 +1,69 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "4.0")
|
||||
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
|
||||
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) -> None:
|
||||
super().__init__()
|
||||
self.toast = Adw.Toast.new(f"Error: {message}")
|
||||
self.toast.set_priority(Adw.ToastPriority.HIGH)
|
||||
|
||||
self.toast.set_button_label("details")
|
||||
|
||||
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"),
|
||||
)
|
@ -4,11 +4,11 @@ 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
|
||||
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
|
||||
|
||||
@ -56,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(
|
||||
@ -86,7 +85,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 +94,16 @@ 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 +212,12 @@ 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"
|
||||
)
|
||||
|
69
pkgs/clan-vm-manager/clan_vm_manager/views/logs.py
Normal file
69
pkgs/clan-vm-manager/clan_vm_manager/views/logs.py
Normal file
@ -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)
|
@ -5,10 +5,12 @@ 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
|
||||
from clan_vm_manager.views.list import ClanList
|
||||
from clan_vm_manager.views.logs import Logs
|
||||
|
||||
gi.require_version("Adw", "1")
|
||||
|
||||
@ -25,8 +27,11 @@ class MainWindow(Adw.ApplicationWindow):
|
||||
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)
|
||||
@ -48,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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user