diff --git a/pkgs/clan-vm-manager/clan_vm_manager/app.py b/pkgs/clan-vm-manager/clan_vm_manager/app.py index 28d58fd1..04abdeb1 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/app.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/app.py @@ -2,11 +2,14 @@ import argparse from dataclasses import dataclass from pathlib import Path +import os import gi from clan_cli import vms -from clan_vm_manager.windows.flash import FlashUSBWindow +from clan_vm_manager.views.list import ClanList + +# from clan_vm_manager.windows.flash import FlashUSBWindow gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") @@ -14,26 +17,19 @@ gi.require_version("Adw", "1") import multiprocessing as mp from clan_cli.clan_uri import ClanURI -from gi.repository import Gio, Gtk, Adw +from gi.repository import Adw, Gio, Gtk, Gdk from .constants import constants from .errors.show_error import show_error_dialog from .executor import ProcessManager -from .interfaces import Callbacks, InitialFlashValues, InitialJoinValues -from .windows.join import JoinWindow -from .windows.overview import OverviewWindow - -@dataclass -class ClanWindows: - join: type[JoinWindow] - overview: type[OverviewWindow] - flash_usb: type[FlashUSBWindow] +# from .windows.join import JoinWindow +# from .windows.overview import OverviewWindow @dataclass class ClanConfig: - initial_window: str + initial_view: str url: ClanURI | None @@ -43,30 +39,53 @@ def on_except(error: Exception, proc: mp.process.BaseProcess) -> None: class MainWindow(Adw.ApplicationWindow): - def __init__(self, config: ClanConfig) -> None: - super().__init__() - self.set_title("Clan Manager") - view = Adw.ToolbarView() - header = Adw.HeaderBar() - view.add_top_bar(header) + def __init__(self, app: Gtk.Application, config: ClanConfig) -> None: + super().__init__() + self.set_application(app) - label = Gtk.Label.new("testlabel") - view.set_content(label) + self.set_title("Clan Manager") + + # ToolbarView is the root layout. + # A Widget containing a page, as well as top and/or bottom bars. + # See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.ToolbarView.html + self.view = Adw.ToolbarView() + self.header = Adw.HeaderBar() + self.view.add_top_bar(self.header) + - self.set_content(view) + clan_list = ClanList( + app = app + ) + + self.stack = Adw.ViewStack() + self.stack.add_titled( + clan_list, "list", "list" + + ) -class Application(Gtk.Application): + self.view.set_content(self.stack) + + self.set_content(self.view) + + +# AdwApplication +# - handles library initialization by calling adw_init() in the default GApplication::startup signal handle +# - automatically loads stylesheets (style.css, style-dark.css, ...) +# - ... +# More see: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Application.html +class Application(Adw.Application): def __init__(self, config: ClanConfig) -> None: super().__init__( application_id=constants["APPID"], flags=Gio.ApplicationFlags.FLAGS_NONE ) - # TODO: - # self.init_style() - - self.window = MainWindow(config) + self.config = config self.proc_manager = ProcessManager() + # self.connect("activate", self.do_activate) self.connect("shutdown", self.on_shutdown) + # constants + # breakpoint() + def on_shutdown(self, app: Gtk.Application) -> None: print("Shutting down") @@ -115,25 +134,22 @@ class Application(Gtk.Application): def do_activate(self) -> None: - win = self.props.active_window - if not win: - win = self.window - win.set_application(self) - win.present() - + self.init_style() + window = MainWindow(app=self,config=self.config) + window.set_default_size(980,650) + window.present() + # TODO: For css styling def init_style(self) -> None: - pass - # css_provider = Gtk.CssProvider() - # css_provider.load_from_resource(constants['RESOURCEID'] + '/style.css') - # screen = Gdk.Screen.get_default() - # style_context = Gtk.StyleContext() - # style_context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + resource_path = Path(__file__).parent / "style.css" + css_provider = Gtk.CssProvider() + css_provider.load_from_path(str(resource_path)) + Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def show_join(args: argparse.Namespace) -> None: app = Application( - config=ClanConfig(url=args.clan_uri, initial_window="join"), + config=ClanConfig(url=args.clan_uri, initial_view="join"), ) return app.run() @@ -145,7 +161,7 @@ def register_join_parser(parser: argparse.ArgumentParser) -> None: def show_overview(args: argparse.Namespace) -> None: app = Application( - config=ClanConfig(url=None, initial_window="overview"), + config=ClanConfig(url=None, initial_view="list"), ) return app.run() diff --git a/pkgs/clan-vm-manager/clan_vm_manager/errors/show_error.py b/pkgs/clan-vm-manager/clan_vm_manager/errors/show_error.py index a75d88cc..d6b193f3 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/errors/show_error.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/errors/show_error.py @@ -7,7 +7,7 @@ import gi gi.require_version("Gtk", "4.0") gi.require_version('Adw', '1') from clan_cli.errors import ClanError -from gi.repository import Gtk, Adw +from gi.repository import Adw Severity = Literal["Error"] | Literal["Warning"] | Literal["Info"] | str diff --git a/pkgs/clan-vm-manager/clan_vm_manager/style.css b/pkgs/clan-vm-manager/clan_vm_manager/style.css new file mode 100644 index 00000000..47a982bd --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/style.css @@ -0,0 +1,27 @@ +window { + /* Set consistent border radius */ + border-radius: 10px; +} + +.title { + margin-right: 20px; +} + +.subtitle { + font-size: 12px; + font-weight: 300; + margin-right: 20px; +} + +row { + padding-right: 10px; + padding-top: 10px; + padding-bottom: 10px; +} + +stack { + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; + padding-bottom: 10px; +} \ No newline at end of file diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/__init__.py b/pkgs/clan-vm-manager/clan_vm_manager/views/__init__.py similarity index 100% rename from pkgs/clan-vm-manager/clan_vm_manager/windows/__init__.py rename to pkgs/clan-vm-manager/clan_vm_manager/views/__init__.py diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/flash.py b/pkgs/clan-vm-manager/clan_vm_manager/views/flash.py similarity index 100% rename from pkgs/clan-vm-manager/clan_vm_manager/windows/flash.py rename to pkgs/clan-vm-manager/clan_vm_manager/views/flash.py diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/join.py b/pkgs/clan-vm-manager/clan_vm_manager/views/join.py similarity index 99% rename from pkgs/clan-vm-manager/clan_vm_manager/windows/join.py rename to pkgs/clan-vm-manager/clan_vm_manager/views/join.py index 344f0712..ab5fe2ec 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/join.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/join.py @@ -16,7 +16,7 @@ gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") -from gi.repository import GdkPixbuf, Gio, Gtk, Adw +from gi.repository import GdkPixbuf, Gio, Gtk class Trust(Gtk.Box): diff --git a/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py similarity index 59% rename from pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py rename to pkgs/clan-vm-manager/clan_vm_manager/views/list.py index 20783b4c..b9f0ceed 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -1,9 +1,13 @@ -from typing import Callable, Any +from collections.abc import Callable -from gi.repository import Gdk, GdkPixbuf, Gtk, Adw -from ..interfaces import Callbacks -from ..models import VMBase, VMStatus +import gi +gi.require_version("Adw", "1") +from gi.repository import Adw, GdkPixbuf, Gio, GObject, Gtk +from pathlib import Path + +from ..models import VMBase, get_initial_vms + # from .context_menu import VmMenu @@ -66,6 +70,21 @@ class ClanEdit(Gtk.Box): self.show_list() +class VMListItem(GObject.Object): + def __init__(self, data: VMBase) -> None: + super().__init__() + self.data = data + +class ClanIcon(Gtk.Box): + def __init__(self, icon_path: Path) -> None: + super().__init__() + self.append( + GdkPixbuf.Pixbuf.new_from_file_at_scale( + filename=icon_path, width=64, height=64, preserve_aspect_ratio=True + ) + ) + + class ClanList(Gtk.Box): """ The ClanList @@ -83,132 +102,67 @@ class ClanList(Gtk.Box): def __init__( self, *, - remount_list: Callable[[], None], - remount_edit: Callable[[], None], - set_selected: Callable[[VMBase | None], None], - cbs: Callbacks, - selected_vm: VMBase | None, - vms: list[VMBase], + app: Adw.Application ) -> None: - super().__init__(orientation=Gtk.Orientation.VERTICAL, ) + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.application = app - self.remount_edit_view = remount_edit - self.remount_list_view = remount_list - self.set_selected = set_selected - self.cbs = cbs - self.show_join = cbs.show_join + boxed_list = Gtk.ListBox() + boxed_list.set_selection_mode(Gtk.SelectionMode.NONE) + boxed_list.add_css_class("boxed-list") - self.selected_vm: VMBase | None = selected_vm + def create_widget(item: VMListItem) -> Gtk.Widget: + print("Creating", item.data) + vm = item.data + # Not displayed; Can be used as id. + row = Adw.SwitchRow() + row.set_name(vm.url) + + row.set_title(vm.name) + row.set_title_lines(1) - # self.toolbar = ClanListToolbar( - # selected_vm=selected_vm, - # on_start_clicked=self.on_start_clicked, - # on_stop_clicked=self.on_stop_clicked, - # on_edit_clicked=self.on_edit_clicked, - # on_join_clan_clicked=self.on_join_clan_clicked, - # on_flash_clicked=self.on_flash_clicked, - # ) - # self.add(self.toolbar) + row.set_subtitle(vm.url) + row.set_subtitle_lines(1) - self.append( - ClanListView( - vms=vms, - on_select_row=self.on_select_vm, - selected_vm=selected_vm, - on_double_click=self.on_double_click, - ) - ) - - def on_flash_clicked(self, widget: Gtk.Widget) -> None: - self.cbs.show_flash() - - def on_double_click(self, vm: VMBase) -> None: - self.on_start_clicked(self) - - def on_start_clicked(self, widget: Gtk.Widget) -> None: - if self.selected_vm: - self.cbs.spawn_vm(self.selected_vm.url, self.selected_vm._flake_attr) - # Call this to reload - self.remount_list_view() - - def on_stop_clicked(self, widget: Gtk.Widget) -> None: - if self.selected_vm: - self.cbs.stop_vm(self.selected_vm.url, self.selected_vm._flake_attr) - self.remount_list_view() - - def on_join_clan_clicked(self, widget: Gtk.Widget) -> None: - self.show_join() - - def on_edit_clicked(self, widget: Gtk.Widget) -> None: - self.remount_edit_view() - - def on_select_vm(self, vm: VMBase) -> None: - # self.toolbar.set_selected_vm(vm) - - self.set_selected(vm) - self.selected_vm = vm + avatar = Adw.Avatar() + avatar.set_text(vm.name) + avatar.set_show_initials(True) + avatar.set_size(50) + # (GdkPixbuf.Pixbuf.new_from_file_at_scale( + # filename=vm.icon, + # width=512, + # height=512, + # preserve_aspect_ratio=True, + # )) + + # Gtk.Image.new_from_pixbuf( + + # ) + row.add_prefix(avatar) -class ClanListToolbar(Gtk.Box): - def __init__( - self, - *, - selected_vm: VMBase | None, - on_start_clicked: Callable[[Gtk.Widget], None], - on_stop_clicked: Callable[[Gtk.Widget], None], - on_edit_clicked: Callable[[Gtk.Widget], None], - on_join_clan_clicked: Callable[[Gtk.Widget], None], - on_flash_clicked: Callable[[Gtk.Widget], None], - ) -> None: - super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + row.connect("notify::active", self.on_row_toggle) - self.start_button = Gtk.ToolButton(label="Start") - self.start_button.connect("clicked", on_start_clicked) - self.append(self.start_button) + return row + - self.stop_button = Gtk.ToolButton(label="Stop") - self.stop_button.connect("clicked", on_stop_clicked) - self.append(self.stop_button) + list_store = Gio.ListStore() + print(list_store) - self.edit_button = Gtk.ToolButton(label="Edit") - self.edit_button.connect("clicked", on_edit_clicked) - self.append(self.edit_button) - - self.join_clan_button = Gtk.ToolButton(label="Join Clan") - self.join_clan_button.connect("clicked", on_join_clan_clicked) - self.append(self.join_clan_button) - - self.flash_button = Gtk.ToolButton(label="Write to USB") - self.flash_button.connect("clicked", on_flash_clicked) - self.append(self.flash_button) - - self.set_selected_vm(selected_vm) - - def set_selected_vm(self, vm: VMBase | None) -> None: - if vm: - self.edit_button.set_sensitive(True) - self.start_button.set_sensitive(vm.status == VMStatus.STOPPED) - self.stop_button.set_sensitive(vm.status == VMStatus.RUNNING) - else: - self.edit_button.set_sensitive(False) - self.start_button.set_sensitive(False) - self.stop_button.set_sensitive(False) + for vm in get_initial_vms(app.running_vms()): + list_store.append(VMListItem(data=vm.base)) -class ClanEditToolbar(Gtk.Box): - def __init__( - self, - *, - on_save_clicked: Callable[[Gtk.Widget], None], - ) -> None: - super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + boxed_list.bind_model(list_store, create_widget_func=create_widget) - # Icons See: https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html - # Could not find a suitable one - self.save_button = Gtk.ToolButton(label="Save") - self.save_button.connect("clicked", on_save_clicked) + + self.append(boxed_list) - self.append(self.save_button) + def on_row_toggle(self, row: Adw.SwitchRow, state: bool) -> None: + print("Toggled", row.get_name(), "active:", row.get_active()) + # TODO: start VM here + # question: Should we disable the switch + # for the time until we got a response for this VM? class ClanListView(Gtk.Box): diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/~list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/~list.py new file mode 100644 index 00000000..89376987 --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/~list.py @@ -0,0 +1,79 @@ + +import gi + +gi.require_version("Gtk", "4.0") + + +# class List(): +# def __init__(self, cbs: Callbacks) -> None: +# super().__init__() +# self.set_title("cLAN Manager") +# # self.connect("delete-event", self.on_quit) +# self.set_default_size(800, 600) +# self.cbs = cbs + +# vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, ) +# self.set_child(vbox) +# self.stack = Gtk.Stack() + + + +# # Add named stacks +# self.stack.add_titled(clan_list, "list", "List") +# self.stack.add_titled( +# ClanJoinPage(stack=self.remount_list_view), "join", "Join" +# ) +# self.stack.add_titled( +# ClanEdit(remount_list=self.remount_list_view, selected_vm=None), +# "edit", +# "Edit", +# ) + +# vbox.append(self.stack) + +# # Must be called AFTER all components were added +# # self.show_all() + +# def set_selected(self, sel: VMBase | None) -> None: +# self.selected_vm = sel + +# def remount_list_view(self) -> None: +# widget = self.stack.get_child_by_name("list") +# if widget: +# widget.destroy() +# vms = [] + +# for vm in get_initial_vms(self.cbs.running_vms()): +# vms.append(vm.base) +# # FIXME: It feels very odd that we have to re-fetch the selected VM. +# # The model should be just updated in-place. +# if self.selected_vm and vm.base.url == self.selected_vm.url: +# self.selected_vm = vm.base + +# clan_list = ClanList( +# vms=vms, +# cbs=self.cbs, +# remount_list=self.remount_list_view, +# remount_edit=self.remount_edit_view, +# set_selected=self.set_selected, +# selected_vm=self.selected_vm, +# ) +# self.stack.add_titled(clan_list, "list", "List") +# # self.show_all() +# self.stack.set_visible_child_name("list") + +# def remount_edit_view(self) -> None: +# widget = self.stack.get_child_by_name("edit") +# if widget: +# widget.destroy() + +# self.stack.add_titled( +# ClanEdit(remount_list=self.remount_list_view, selected_vm=self.selected_vm), +# "edit", +# "Edit", +# ) +# # self.show_all() +# self.stack.set_visible_child_name("edit") + +# def on_quit(self, *args: Any) -> None: +# Gio.Application.quit(self.get_application()) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/overview.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/overview.py deleted file mode 100644 index b0a7d792..00000000 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/overview.py +++ /dev/null @@ -1,95 +0,0 @@ -from typing import Any - -import gi - -from ..models import VMBase, get_initial_vms - -gi.require_version("Gtk", "4.0") - -from gi.repository import Gio, Gtk - -from ..interfaces import Callbacks -from ..ui.clan_join_page import ClanJoinPage -from ..ui.clan_select_list import ClanEdit, ClanList - - -class OverviewWindow(Gtk.ApplicationWindow): - def __init__(self, cbs: Callbacks) -> None: - super().__init__() - self.set_title("cLAN Manager") - # self.connect("delete-event", self.on_quit) - self.set_default_size(800, 600) - self.cbs = cbs - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, ) - self.set_child(vbox) - self.stack = Gtk.Stack() - - clan_list = ClanList( - vms=[vm.base for vm in get_initial_vms(self.cbs.running_vms())], - cbs=self.cbs, - remount_list=self.remount_list_view, - remount_edit=self.remount_edit_view, - set_selected=self.set_selected, - selected_vm=None, - ) - - # Add named stacks - self.stack.add_titled(clan_list, "list", "List") - self.stack.add_titled( - ClanJoinPage(stack=self.remount_list_view), "join", "Join" - ) - self.stack.add_titled( - ClanEdit(remount_list=self.remount_list_view, selected_vm=None), - "edit", - "Edit", - ) - - vbox.append(self.stack) - - # Must be called AFTER all components were added - # self.show_all() - - def set_selected(self, sel: VMBase | None) -> None: - self.selected_vm = sel - - def remount_list_view(self) -> None: - widget = self.stack.get_child_by_name("list") - if widget: - widget.destroy() - vms = [] - - for vm in get_initial_vms(self.cbs.running_vms()): - vms.append(vm.base) - # FIXME: It feels very odd that we have to re-fetch the selected VM. - # The model should be just updated in-place. - if self.selected_vm and vm.base.url == self.selected_vm.url: - self.selected_vm = vm.base - - clan_list = ClanList( - vms=vms, - cbs=self.cbs, - remount_list=self.remount_list_view, - remount_edit=self.remount_edit_view, - set_selected=self.set_selected, - selected_vm=self.selected_vm, - ) - self.stack.add_titled(clan_list, "list", "List") - # self.show_all() - self.stack.set_visible_child_name("list") - - def remount_edit_view(self) -> None: - widget = self.stack.get_child_by_name("edit") - if widget: - widget.destroy() - - self.stack.add_titled( - ClanEdit(remount_list=self.remount_list_view, selected_vm=self.selected_vm), - "edit", - "Edit", - ) - # self.show_all() - self.stack.set_visible_child_name("edit") - - def on_quit(self, *args: Any) -> None: - Gio.Application.quit(self.get_application())