From 2d613e3933fef99c67d595ccc4bb4a26aa8205a0 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 17 Jan 2024 12:11:49 +0000 Subject: [PATCH] Gtk4 migration (#693) Co-authored-by: Qubasa Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/693 Co-authored-by: Johannes Kirschbauer Co-committed-by: Johannes Kirschbauer --- pkgs/clan-cli/clan_cli/__init__.py | 5 +- pkgs/clan-vm-manager/clan_vm_manager/app.py | 177 +++------- .../clan_vm_manager/errors/show_error.py | 14 +- .../clan_vm_manager/model/__init__.py | 0 .../clan_vm_manager/model/use_views.py | 37 ++ .../clan_vm_manager/model/use_vms.py | 53 +++ .../clan-vm-manager/clan_vm_manager/models.py | 133 ++++--- .../clan-vm-manager/clan_vm_manager/style.css | 16 + .../clan_vm_manager/ui/clan_join_page.py | 26 -- .../clan_vm_manager/ui/clan_select_list.py | 328 ------------------ .../clan_vm_manager/ui/context_menu.py | 39 --- .../clan_vm_manager/views/__init__.py | 0 .../clan_vm_manager/views/list.py | 80 +++++ .../clan_vm_manager/views/trust_join.py | 139 ++++++++ .../clan_vm_manager/windows/flash.py | 65 ---- .../clan_vm_manager/windows/join.py | 211 ----------- .../clan_vm_manager/windows/overview.py | 95 ----- pkgs/clan-vm-manager/default.nix | 7 +- pkgs/clan-vm-manager/shell.nix | 3 +- 19 files changed, 458 insertions(+), 970 deletions(-) create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/model/__init__.py create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/model/use_views.py create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/model/use_vms.py create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/style.css delete mode 100644 pkgs/clan-vm-manager/clan_vm_manager/ui/clan_join_page.py delete mode 100644 pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py delete mode 100644 pkgs/clan-vm-manager/clan_vm_manager/ui/context_menu.py create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/views/__init__.py create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/views/list.py create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/views/trust_join.py delete mode 100644 pkgs/clan-vm-manager/clan_vm_manager/windows/flash.py delete mode 100644 pkgs/clan-vm-manager/clan_vm_manager/windows/join.py delete mode 100644 pkgs/clan-vm-manager/clan_vm_manager/windows/overview.py diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 085b96fd2..5689a82c3 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -1,7 +1,6 @@ import argparse import logging import sys -import traceback from collections.abc import Sequence from pathlib import Path from types import ModuleType @@ -136,13 +135,15 @@ def main() -> None: args.func(args) except ClanError as e: if args.debug: - traceback.print_exc() + log.exception(e) sys.exit(1) if isinstance(e, ClanCmdError): if e.cmd.msg: print(e.cmd.msg, file=sys.stderr) else: print(e, file=sys.stderr) + elif isinstance(e, ClanError): + print(e, file=sys.stderr) sys.exit(1) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/app.py b/pkgs/clan-vm-manager/clan_vm_manager/app.py index e781df0d0..ff07b1bc9 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/app.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/app.py @@ -4,151 +4,91 @@ from dataclasses import dataclass from pathlib import Path import gi -from clan_cli import vms -from clan_vm_manager.windows.flash import FlashUSBWindow +from clan_vm_manager.interfaces import InitialJoinValues +from clan_vm_manager.model.use_views import Views +from clan_vm_manager.views.list import ClanList +from clan_vm_manager.views.trust_join import Trust -gi.require_version("Gtk", "3.0") - -import multiprocessing as mp +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from clan_cli.clan_uri import ClanURI -from gi.repository import Gio, Gtk +from gi.repository import Adw, Gdk, Gio, Gtk 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 .model.use_vms import VMS @dataclass class ClanConfig: - initial_window: str + initial_view: str url: ClanURI | None -# Will be executed in the context of the child process -def on_except(error: Exception, proc: mp.process.BaseProcess) -> None: - show_error_dialog(str(error)) +class MainWindow(Adw.ApplicationWindow): + def __init__(self, config: ClanConfig) -> None: + super().__init__() + self.set_title("cLAN Manager") + self.set_default_size(980, 650) + + view = Adw.ToolbarView() + self.set_content(view) + + header = Adw.HeaderBar() + view.add_top_bar(header) + + # Initialize all views + stack_view = Views.use().view + stack_view.add_named(ClanList(), "list") + stack_view.add_named( + Trust(initial_values=InitialJoinValues(url=config.url)), "join.trust" + ) + + stack_view.set_visible_child_name(config.initial_view) + + clamp = Adw.Clamp() + clamp.set_child(stack_view) + clamp.set_maximum_size(1000) + + view.set_content(clamp) + + # Push the first page to the navigation view -class Application(Gtk.Application): - def __init__(self, windows: ClanWindows, config: ClanConfig) -> None: +class Application(Adw.Application): + def __init__(self, config: ClanConfig) -> None: super().__init__( application_id=constants["APPID"], flags=Gio.ApplicationFlags.FLAGS_NONE ) - self.init_style() - self.windows = windows - self.proc_manager = ProcessManager() - initial = windows.__dict__[config.initial_window] - self.cbs = Callbacks( - show_list=self.show_list, - show_join=self.show_join, - show_flash=self.show_flash, - spawn_vm=self.spawn_vm, - stop_vm=self.stop_vm, - running_vms=self.running_vms, - ) - if issubclass(initial, JoinWindow): - # see JoinWindow constructor - self.window = initial( - initial_values=InitialJoinValues(url=config.url or ""), - cbs=self.cbs, - ) - - if issubclass(initial, OverviewWindow): - # see OverviewWindow constructor - self.window = initial(cbs=self.cbs) - - # Connect to the shutdown signal + self.config = config self.connect("shutdown", self.on_shutdown) def on_shutdown(self, app: Gtk.Application) -> None: print("Shutting down") - self.proc_manager.kill_all() - - def spawn_vm(self, url: str, attr: str) -> None: - print(f"spawn_vm {url}") - - # TODO: We should use VMConfig from the history file - vm = vms.run.inspect_vm(flake_url=url, flake_attr=attr) - log_path = Path(".") - - # TODO: We only use the url as the ident. This is not unique as the flake_attr is missing. - # when we migrate everything to use the ClanURI class we can use the full url as the ident - self.proc_manager.spawn( - ident=url, - on_except=on_except, - log_path=log_path, - func=vms.run.run_vm, - vm=vm, - ) - - def stop_vm(self, url: str, attr: str) -> None: - self.proc_manager.kill(url) - - def running_vms(self) -> list[str]: - return self.proc_manager.running_procs() - - def show_list(self) -> None: - prev = self.window - self.window = self.windows.__dict__["overview"](cbs=self.cbs) - self.window.set_application(self) - prev.hide() - - def show_join(self) -> None: - prev = self.window - self.window = self.windows.__dict__["join"]( - cbs=self.cbs, initial_values=InitialJoinValues(url=None) - ) - self.window.set_application(self) - prev.hide() - - def show_flash(self) -> None: - prev = self.window - self.window = self.windows.__dict__["flash_usb"]( - cbs=self.cbs, initial_values=InitialFlashValues(None) - ) - self.window.set_application(self) - prev.hide() - - def do_startup(self) -> None: - Gtk.Application.do_startup(self) - Gtk.init() + VMS.use().kill_all() 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(config=self.config) + window.set_application(self) + 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: - print(f"Joining clan {args.clan_uri}") app = Application( - windows=ClanWindows( - join=JoinWindow, overview=OverviewWindow, flash_usb=FlashUSBWindow - ), - config=ClanConfig(url=args.clan_uri, initial_window="join"), + config=ClanConfig(url=args.clan_uri, initial_view="join.trust"), ) return app.run() @@ -160,17 +100,10 @@ def register_join_parser(parser: argparse.ArgumentParser) -> None: def show_overview(args: argparse.Namespace) -> None: app = Application( - windows=ClanWindows( - join=JoinWindow, overview=OverviewWindow, flash_usb=FlashUSBWindow - ), - config=ClanConfig(url=None, initial_window="overview"), + config=ClanConfig(url=None, initial_view="list"), ) return app.run() def register_overview_parser(parser: argparse.ArgumentParser) -> None: parser.set_defaults(func=show_overview) - - -# def register_run_parser(parser: argparse.ArgumentParser) -> None: -# parser.set_defaults(func=show_run_vm) 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 2cc15cb60..f5ad364a0 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 @@ -4,19 +4,17 @@ from typing import Literal import gi -gi.require_version("Gtk", "3.0") +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from clan_cli.errors import ClanError -from gi.repository import Gtk +from gi.repository import Adw Severity = Literal["Error"] | Literal["Warning"] | Literal["Info"] | str def show_error_dialog(error: ClanError, severity: Severity | None = "Error") -> None: message = str(error) - dialog = Gtk.MessageDialog( - None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, severity - ) + dialog = Adw.MessageDialog(parent=None, heading=severity, body=message) print("error:", message) - dialog.format_secondary_text(message) - dialog.run() - dialog.destroy() + dialog.add_response("ok", "ok") + dialog.choose() diff --git a/pkgs/clan-vm-manager/clan_vm_manager/model/__init__.py b/pkgs/clan-vm-manager/clan_vm_manager/model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pkgs/clan-vm-manager/clan_vm_manager/model/use_views.py b/pkgs/clan-vm-manager/clan_vm_manager/model/use_views.py new file mode 100644 index 000000000..6407da27b --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/model/use_views.py @@ -0,0 +1,37 @@ +from typing import Any + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw + + +class Views: + """ + This is a singleton. + It is initialized with the first call of use() + + Usage: + + Views.use().set_visible() + + Views.use() can also be called before the data is needed. e.g. to eliminate/reduce waiting time. + + """ + + _instance: "None | Views" = None + view: Adw.ViewStack + + # Make sure the VMS class is used as a singleton + def __init__(self) -> None: + raise RuntimeError("Call use() instead") + + @classmethod + def use(cls: Any) -> "Views": + if cls._instance is None: + print("Creating new instance") + cls._instance = cls.__new__(cls) + cls.view = Adw.ViewStack() + + return cls._instance diff --git a/pkgs/clan-vm-manager/clan_vm_manager/model/use_vms.py b/pkgs/clan-vm-manager/clan_vm_manager/model/use_vms.py new file mode 100644 index 000000000..9403125d9 --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/model/use_vms.py @@ -0,0 +1,53 @@ +import multiprocessing as mp +from typing import Any + +from clan_cli.errors import ClanError +from gi.repository import Gio + +from clan_vm_manager.errors.show_error import show_error_dialog +from clan_vm_manager.models import VM, get_initial_vms + + +# https://amolenaar.pages.gitlab.gnome.org/pygobject-docs/Adw-1/class-ToolbarView.html +# Will be executed in the context of the child process +def on_except(error: Exception, proc: mp.process.BaseProcess) -> None: + show_error_dialog(ClanError(str(error))) + + +class VMS: + """ + This is a singleton. + It is initialized with the first call of use() + + Usage: + + VMS.use().get_running_vms() + + VMS.use() can also be called before the data is needed. e.g. to eliminate/reduce waiting time. + + """ + + list_store: Gio.ListStore + _instance: "None | VMS" = None + + # Make sure the VMS class is used as a singleton + def __init__(self) -> None: + raise RuntimeError("Call use() instead") + + @classmethod + def use(cls: Any) -> "VMS": + if cls._instance is None: + print("Creating new instance") + cls._instance = cls.__new__(cls) + cls.list_store = Gio.ListStore.new(VM) + + for vm in get_initial_vms(): + cls.list_store.append(vm) + return cls._instance + + def get_running_vms(self) -> list[VM]: + return list(filter(lambda vm: vm.is_running(), self.list_store)) + + def kill_all(self) -> None: + for vm in self.get_running_vms(): + vm.stop() diff --git a/pkgs/clan-vm-manager/clan_vm_manager/models.py b/pkgs/clan-vm-manager/clan_vm_manager/models.py index ef3ad3175..802abf071 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/models.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/models.py @@ -1,100 +1,95 @@ -from collections import OrderedDict -from dataclasses import dataclass +import multiprocessing as mp +import sys +import weakref from enum import StrEnum from pathlib import Path -from typing import Any - -import gi -from clan_cli.history.list import list_history - -from .errors.show_error import show_error_dialog - -gi.require_version("GdkPixbuf", "2.0") +from clan_cli import vms from clan_cli.errors import ClanError -from gi.repository import GdkPixbuf +from clan_cli.history.add import HistoryEntry +from clan_cli.history.list import list_history +from gi.repository import GObject from clan_vm_manager import assets +from .errors.show_error import show_error_dialog +from .executor import MPProcess, spawn + class VMStatus(StrEnum): RUNNING = "Running" STOPPED = "Stopped" -@dataclass(frozen=True) -class VMBase: - icon: Path | GdkPixbuf.Pixbuf - name: str - url: str - status: VMStatus - _flake_attr: str +def on_except(error: Exception, proc: mp.process.BaseProcess) -> None: + show_error_dialog(ClanError(str(error))) - @staticmethod - def name_to_type_map() -> OrderedDict[str, type]: - return OrderedDict( - { - "Icon": GdkPixbuf.Pixbuf, - "Name": str, - "URL": str, - "Status": str, - "_FlakeAttr": str, - } + +class VM(GObject.Object): + def __init__( + self, + icon: Path, + status: VMStatus, + data: HistoryEntry, + process: MPProcess | None = None, + ) -> None: + super().__init__() + self.data = data + self.process = process + self.status = status + self._finalizer = weakref.finalize(self, self.stop) + + def start(self) -> None: + if self.process is not None: + show_error_dialog(ClanError("VM is already running")) + return + vm = vms.run.inspect_vm( + flake_url=self.data.flake.flake_url, flake_attr=self.data.flake.flake_attr + ) + log_path = Path(".") + + self.process = spawn( + on_except=on_except, + log_path=log_path, + func=vms.run.run_vm, + vm=vm, ) - @staticmethod - def to_idx(name: str) -> int: - return list(VMBase.name_to_type_map().keys()).index(name) + def is_running(self) -> bool: + if self.process is not None: + return self.process.proc.is_alive() + return False - def list_data(self) -> OrderedDict[str, Any]: - return OrderedDict( - { - "Icon": str(self.icon), - "Name": self.name, - "URL": self.url, - "Status": self.status, - "_FlakeAttr": self._flake_attr, - } - ) + def get_id(self) -> str: + return self.data.flake.flake_url + self.data.flake.flake_attr + + def stop(self) -> None: + if self.process is None: + print("VM is already stopped", file=sys.stderr) + return + + self.process.kill_group() + self.process = None -@dataclass(frozen=True) -class VM: - # Inheritance is bad. Lets use composition - # Added attributes are separated from base attributes. - base: VMBase - autostart: bool = False - description: str | None = None - - -# TODO: How to handle incompatible / corrupted history file. Delete it? -# start/end indexes can be used optionally for pagination -def get_initial_vms( - running_vms: list[str], start: int = 0, end: int | None = None -) -> list[VM]: +def get_initial_vms() -> list[VM]: vm_list = [] try: # Execute `clan flakes add ` to democlan for this to work for entry in list_history(): - icon = assets.loc / "placeholder.jpeg" - if entry.flake.icon is not None: + if entry.flake.icon is None: + icon = assets.loc / "placeholder.jpeg" + else: icon = entry.flake.icon - status = VMStatus.STOPPED - if entry.flake.flake_url in running_vms: - status = VMStatus.RUNNING - - base = VMBase( + base = VM( icon=icon, - name=entry.flake.clan_name, - url=entry.flake.flake_url, - status=status, - _flake_attr=entry.flake.flake_attr, + status=VMStatus.STOPPED, + data=entry, ) - vm_list.append(VM(base=base)) + vm_list.append(base) except ClanError as e: show_error_dialog(e) - # start/end slices can be used for pagination - return vm_list[start:end] + return vm_list 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 000000000..e91505303 --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/style.css @@ -0,0 +1,16 @@ +/* Insert custom styles here */ + +navigation-view { + padding: 5px; + /* padding-left: 5px; + padding-right: 5px; + padding-bottom: 5px; */ +} + +avatar { + margin: 2px; +} + +.trust { + padding: 25px; +} \ No newline at end of file diff --git a/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_join_page.py b/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_join_page.py deleted file mode 100644 index 9b96decaa..000000000 --- a/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_join_page.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 - - -import gi - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk - - -class ClanJoinPage(Gtk.Box): - def __init__(self, *, stack: Gtk.Stack) -> None: - super().__init__() - self.page = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, spacing=6, expand=True - ) - self.set_border_width(10) - self.stack = stack - - button = Gtk.Button(label="Back to list", margin_left=10) - button.connect("clicked", self.switch) - self.add(button) - - self.add(Gtk.Label("Join cLan")) - - def switch(self, widget: Gtk.Widget) -> None: - self.stack.set_visible_child_name("list") diff --git a/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py b/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py deleted file mode 100644 index acdacb1f4..000000000 --- a/pkgs/clan-vm-manager/clan_vm_manager/ui/clan_select_list.py +++ /dev/null @@ -1,328 +0,0 @@ -from collections.abc import Callable - -from gi.repository import Gdk, GdkPixbuf, Gtk - -from ..interfaces import Callbacks -from ..models import VMBase, VMStatus -from .context_menu import VmMenu - - -class ClanEditForm(Gtk.ListBox): - def __init__(self, *, selected: VMBase | None) -> None: - super().__init__() - self.page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, expand=True) - self.set_border_width(10) - self.selected = selected - self.set_selection_mode(0) - - if self.selected: - row = Gtk.ListBoxRow() - row.add(Gtk.Label(f"\n {self.selected.name}")) - self.add(row) - - # ---------- row 1 -------- - row = Gtk.ListBoxRow() - row_layout = Gtk.Box(spacing=6, expand=True) - - # Doc: pack_start/end takes alignment params Expand, Fill, Padding - row_layout.pack_start(Gtk.Label("Memory Size in MiB"), False, False, 5) - row_layout.pack_start( - Gtk.SpinButton.new_with_range(512, 4096, 256), True, True, 0 - ) - - row.add(row_layout) - self.add(row) - - # ----------- row 2 ------- - - row = Gtk.ListBoxRow() - row_layout = Gtk.Box(spacing=6, expand=True) - - row_layout.pack_start(Gtk.Label("CPU Count"), False, False, 5) - row_layout.pack_end(Gtk.SpinButton.new_with_range(1, 5, 1), True, True, 0) - - row.add(row_layout) - self.add(row) - - def switch(self, widget: Gtk.Widget) -> None: - self.show_list() - - -class ClanEdit(Gtk.Box): - def __init__( - self, *, remount_list: Callable[[], None], selected_vm: VMBase | None - ) -> None: - super().__init__(orientation=Gtk.Orientation.VERTICAL, expand=True) - - self.show_list = remount_list - self.selected = selected_vm - - self.toolbar = ClanEditToolbar(on_save_clicked=self.on_save) - self.add(self.toolbar) - self.add(ClanEditForm(selected=self.selected)) - - def on_save(self, widget: Gtk.Widget) -> None: - print("Save clicked saving values") - self.show_list() - - -class ClanList(Gtk.Box): - """ - The ClanList - Is the composition of - the ClanListToolbar - the clanListView - # ------------------------ # - # - Tools < Edit> # - # ------------------------ # - # - List Items - # - <...> - # ------------------------# - """ - - 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], - ) -> None: - super().__init__(orientation=Gtk.Orientation.VERTICAL, expand=True) - - 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 - - self.selected_vm: VMBase | None = selected_vm - - 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) - - self.add( - 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 - - -class ClanListToolbar(Gtk.Toolbar): - 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) - - self.start_button = Gtk.ToolButton(label="Start") - self.start_button.connect("clicked", on_start_clicked) - self.add(self.start_button) - - self.stop_button = Gtk.ToolButton(label="Stop") - self.stop_button.connect("clicked", on_stop_clicked) - self.add(self.stop_button) - - self.edit_button = Gtk.ToolButton(label="Edit") - self.edit_button.connect("clicked", on_edit_clicked) - self.add(self.edit_button) - - self.join_clan_button = Gtk.ToolButton(label="Join Clan") - self.join_clan_button.connect("clicked", on_join_clan_clicked) - self.add(self.join_clan_button) - - self.flash_button = Gtk.ToolButton(label="Write to USB") - self.flash_button.connect("clicked", on_flash_clicked) - self.add(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) - - -class ClanEditToolbar(Gtk.Toolbar): - def __init__( - self, - *, - on_save_clicked: Callable[[Gtk.Widget], None], - ) -> None: - super().__init__(orientation=Gtk.Orientation.HORIZONTAL) - - # 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.add(self.save_button) - - -class ClanListView(Gtk.Box): - def __init__( - self, - *, - on_select_row: Callable[[VMBase], None], - selected_vm: VMBase | None, - vms: list[VMBase], - on_double_click: Callable[[VMBase], None], - ) -> None: - super().__init__(expand=True) - self.vms: list[VMBase] = vms - self.on_select_row = on_select_row - self.on_double_click = on_double_click - self.context_menu: VmMenu | None = None - - store_types = VMBase.name_to_type_map().values() - - self.list_store = Gtk.ListStore(*store_types) - self.tree_view = Gtk.TreeView(self.list_store, expand=True) - for vm in self.vms: - self.insertVM(vm) - - setColRenderers(self.tree_view) - - self.set_selected_vm(selected_vm) - selection = self.tree_view.get_selection() - selection.connect("changed", self._on_select_row) - self.tree_view.connect("row-activated", self._on_double_click) - self.tree_view.connect("button-press-event", self._on_button_pressed) - - self.set_border_width(10) - self.add(self.tree_view) - - def find_vm(self, vm: VMBase) -> int: - for idx, row in enumerate(self.list_store): - if row[VMBase.to_idx("Name")] == vm.name: # TODO: Change to path - return idx - return -1 - - def set_selected_vm(self, vm: VMBase | None) -> None: - if vm is None: - return - selection = self.tree_view.get_selection() - idx = self.find_vm(vm) - selection.select_path(idx) - - def insertVM(self, vm: VMBase) -> None: - values = list(vm.list_data().values()) - icon_idx = VMBase.to_idx("Icon") - values[icon_idx] = GdkPixbuf.Pixbuf.new_from_file_at_scale( - filename=values[icon_idx], width=64, height=64, preserve_aspect_ratio=True - ) - self.list_store.append(values) - - def _on_select_row(self, selection: Gtk.TreeSelection) -> None: - model, row = selection.get_selected() - if row is not None: - vm = VMBase(*model[row]) - self.on_select_row(vm) - - def _on_button_pressed( - self, tree_view: Gtk.TreeView, event: Gdk.EventButton - ) -> None: - if self.context_menu: - self.context_menu.destroy() - self.context_menu = None - - if event.button == 3: - path, column, x, y = tree_view.get_path_at_pos(event.x, event.y) - if path is not None: - vm = VMBase(*self.list_store[path[0]]) - print(event) - print(f"Right click on {vm.url}") - self.context_menu = VmMenu(vm) - self.context_menu.popup_at_pointer(event) - - def _on_double_click( - self, tree_view: Gtk.TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn - ) -> None: - # Get the selection object of the tree view - selection = tree_view.get_selection() - model, row = selection.get_selected() - - if row is not None: - vm = VMBase(*model[row]) - self.on_double_click(vm) - - -def setColRenderers(tree_view: Gtk.TreeView) -> None: - for idx, (key, gtype) in enumerate(VMBase.name_to_type_map().items()): - col: Gtk.TreeViewColumn = None - - if key.startswith("_"): - continue - - if issubclass(gtype, GdkPixbuf.Pixbuf): - renderer = Gtk.CellRendererPixbuf() - col = Gtk.TreeViewColumn(key, renderer, pixbuf=idx) - elif issubclass(gtype, bool): - renderer = Gtk.CellRendererToggle() - col = Gtk.TreeViewColumn(key, renderer, active=idx) - elif issubclass(gtype, str): - renderer = Gtk.CellRendererText() - col = Gtk.TreeViewColumn(key, renderer, text=idx) - else: - raise Exception(f"Unknown type: {gtype}") - - # CommonSetup for all columns - if col: - col.set_resizable(True) - col.set_expand(True) - col.set_property("sizing", Gtk.TreeViewColumnSizing.AUTOSIZE) - col.set_property("alignment", 0.5) - col.set_sort_column_id(idx) - tree_view.append_column(col) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/ui/context_menu.py b/pkgs/clan-vm-manager/clan_vm_manager/ui/context_menu.py deleted file mode 100644 index fa288274b..000000000 --- a/pkgs/clan-vm-manager/clan_vm_manager/ui/context_menu.py +++ /dev/null @@ -1,39 +0,0 @@ -import gi - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk - -from ..models import VMBase - - -class VmMenu(Gtk.Menu): - def __init__(self, vm: VMBase) -> None: - super().__init__() - self.vm = vm - self.menu_items = [ - ("Start", self.start_vm), - ("Stop", self.stop_vm), - ("Edit", self.edit_vm), - ("Remove", self.remove_vm), - ("Write to USB", self.write_to_usb), - ] - for item in self.menu_items: - menu_item = Gtk.MenuItem(label=item[0]) - menu_item.connect("activate", item[1]) - self.append(menu_item) - self.show_all() - - def start_vm(self, widget: Gtk.Widget) -> None: - print("start_vm") - - def stop_vm(self, widget: Gtk.Widget) -> None: - print("stop_vm") - - def edit_vm(self, widget: Gtk.Widget) -> None: - print("edit_vm") - - def remove_vm(self, widget: Gtk.Widget) -> None: - print("remove_vm") - - def write_to_usb(self, widget: Gtk.Widget) -> None: - print("write_to_usb") diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/__init__.py b/pkgs/clan-vm-manager/clan_vm_manager/views/__init__.py new file mode 100644 index 000000000..e69de29bb 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 000000000..ab511f044 --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -0,0 +1,80 @@ +from functools import partial + +import gi + +from ..model.use_vms import VMS + +gi.require_version("Adw", "1") +from gi.repository import Adw, Gdk, Gtk + +from ..models import VM + + +class ClanList(Gtk.Box): + """ + The ClanList + Is the composition of + the ClanListToolbar + the clanListView + # ------------------------ # + # - Tools < Edit> # + # ------------------------ # + # - List Items + # - <...> + # ------------------------# + """ + + def __init__(self) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + + boxed_list = Gtk.ListBox() + boxed_list.set_selection_mode(Gtk.SelectionMode.NONE) + boxed_list.add_css_class("boxed-list") + + def create_widget(item: VM) -> Gtk.Widget: + flake = item.data.flake + row = Adw.ActionRow() + + print("Creating", item.data.flake.flake_attr) + # Title + row.set_title(flake.clan_name) + row.set_title_lines(1) + row.set_title_selectable(True) + + # Subtitle + row.set_subtitle(flake.flake_attr) + row.set_subtitle_lines(1) + + # Avatar + avatar = Adw.Avatar() + avatar.set_custom_image(Gdk.Texture.new_from_filename(flake.icon)) + avatar.set_text(flake.clan_name + " " + flake.flake_attr) + avatar.set_show_initials(True) + avatar.set_size(50) + row.add_prefix(avatar) + + # Switch + switch = Gtk.Switch() + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + box.set_valign(Gtk.Align.CENTER) + box.append(switch) + + switch.connect("notify::active", partial(self.on_row_toggle, item)) + row.add_suffix(box) + + return row + + list_store = VMS.use().list_store + + boxed_list.bind_model(list_store, create_widget_func=create_widget) + + self.append(boxed_list) + + def on_row_toggle(self, vm: VM, row: Adw.SwitchRow, state: bool) -> None: + print("Toggled", vm.data.flake.flake_attr, "active:", row.get_active()) + + if row.get_active(): + vm.start() + + if not row.get_active(): + vm.stop() diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/trust_join.py b/pkgs/clan-vm-manager/clan_vm_manager/views/trust_join.py new file mode 100644 index 000000000..ee545a3a2 --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/trust_join.py @@ -0,0 +1,139 @@ +from functools import partial + +import gi +from clan_cli.clan_uri import ClanURI +from clan_cli.errors import ClanError +from clan_cli.history.add import add_history + +from clan_vm_manager.errors.show_error import show_error_dialog + +from ..interfaces import InitialJoinValues + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + + +from gi.repository import Adw, Gio, GObject, Gtk + + +class TrustValues(GObject.Object): + data: InitialJoinValues + + def __init__(self, data: InitialJoinValues) -> None: + super().__init__() + print("TrustValues", data) + self.data = data + + +class Trust(Gtk.Box): + def __init__( + self, + *, + initial_values: InitialJoinValues, + ) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + + # self.on_trust = on_trust + self.url: ClanURI | None = initial_values.url + + def render(item: TrustValues) -> Gtk.Widget: + row = Adw.ActionRow() + row.set_title(str(item.data.url)) + row.add_css_class("trust") + + avatar = Adw.Avatar() + avatar.set_text(str(item.data.url)) + avatar.set_show_initials(True) + avatar.set_size(50) + row.add_prefix(avatar) + + cancel_button = Gtk.Button(label="Cancel") + cancel_button.add_css_class("error") + + trust_button = Gtk.Button(label="Trust") + trust_button.add_css_class("success") + trust_button.connect("clicked", partial(self.on_trust_clicked, item.data)) + + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + box.set_valign(Gtk.Align.CENTER) + box.append(cancel_button) + box.append(trust_button) + + # switch.connect("notify::active", partial(self.on_row_toggle, item.data)) + row.add_suffix(box) + + return row + + boxed_list = Gtk.ListBox() + boxed_list.set_selection_mode(Gtk.SelectionMode.NONE) + boxed_list.add_css_class("boxed-list") + + list_store = Gio.ListStore.new(TrustValues) + list_store.append(TrustValues(data=initial_values)) + + # icon = Gtk.Image.new_from_pixbuf( + # GdkPixbuf.Pixbuf.new_from_file_at_scale( + # filename=str(assets.loc / "placeholder.jpeg"), + # width=256, + # height=256, + # preserve_aspect_ratio=True, + # ) + # ) + + boxed_list.bind_model(list_store, create_widget_func=render) + + self.append(boxed_list) + + # layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + # # layout.set_border_width(20) + # layout.set_spacing(20) + + # if self.url is not None: + # self.entry = Gtk.Label(label=str(self.url)) + # layout.append(icon) + # layout.append(Gtk.Label(label="Clan URL")) + # else: + # layout.append(Gtk.Label(label="Enter Clan URL")) + # self.entry = Gtk.Entry() + # # Autocomplete + # # TODO: provide intelligent suggestions + # completion_list = Gtk.ListStore(str) + # completion_list.append(["clan://"]) + # completion = Gtk.EntryCompletion() + # completion.set_model(completion_list) + # completion.set_text_column(0) + # completion.set_popup_completion(False) + # completion.set_inline_completion(True) + + # self.entry.set_completion(completion) + # self.entry.set_placeholder_text("clan://") + + # layout.append(self.entry) + + # if self.url is None: + # trust_button = Gtk.Button(label="Load cLAN-URL") + # else: + # trust_button = Gtk.Button(label="Trust cLAN-URL") + + # trust_button.connect("clicked", self.on_trust_clicked) + # layout.append(trust_button) + + def on_trust_clicked(self, item: InitialJoinValues, widget: Gtk.Widget) -> None: + try: + uri = item.url + # or ClanURI(self.entry.get_text()) + print(f"trusted: {uri}") + if uri: + add_history(uri) + # history = list_history() + + # found = filter( + # lambda item: item.flake.flake_url == uri.get_internal(), history + # ) + # if found: + # [item] = found + # self.on_trust(uri.get_internal(), item.flake) + + except ClanError as e: + pass + show_error_dialog(e) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/flash.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/flash.py deleted file mode 100644 index 5ef517065..000000000 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/flash.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import Any - -import gi -from clan_cli.errors import ClanError - -from clan_vm_manager.errors.show_error import show_error_dialog - -from ..interfaces import Callbacks, InitialFlashValues - -gi.require_version("Gtk", "3.0") - -from gi.repository import Gio, Gtk - - -class Details(Gtk.Box): - def __init__(self, initial: InitialFlashValues, stack: Gtk.Stack) -> None: - super().__init__() - - def on_confirm(self, widget: Gtk.Widget) -> None: - show_error_dialog(ClanError("Feature not ready yet."), "Info") - - def on_cancel(self, widget: Gtk.Widget) -> None: - show_error_dialog(ClanError("Feature not ready yet."), "Info") - - -class FlashUSBWindow(Gtk.ApplicationWindow): - def __init__(self, cbs: Callbacks, initial_values: InitialFlashValues) -> None: - super().__init__() - # Initialize the main wincbsdow - # self.cbs = cbs - self.set_title("cLAN Manager") - self.connect("delete-event", self.on_quit) - self.set_default_size(800, 600) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, expand=True) - self.add(vbox) - - button = Gtk.ToolButton() - button.set_icon_name("go-previous") - button.connect("clicked", self.switch) - - toolbar = Gtk.Toolbar(orientation=Gtk.Orientation.HORIZONTAL) - toolbar.add(button) - vbox.add(toolbar) - - self.stack = Gtk.Stack() - - print("initial_values", initial_values) - self.stack.add_titled( - Details(initial_values, stack=self.stack), - "details", - "Details", - ) - - vbox.add(self.stack) - - # Must be called AFTER all components were added - self.show_all() - - def switch(self, widget: Gtk.Widget) -> None: - pass - # self.cbs.show_list() - - def on_quit(self, *args: Any) -> None: - Gio.Application.quit(self.get_application()) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/join.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/join.py deleted file mode 100644 index d315820d1..000000000 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/join.py +++ /dev/null @@ -1,211 +0,0 @@ -from collections.abc import Callable -from typing import Any - -import gi -from clan_cli.clan_uri import ClanURI -from clan_cli.errors import ClanError -from clan_cli.flakes.inspect import FlakeConfig -from clan_cli.history.add import add_history, list_history - -from clan_vm_manager import assets -from clan_vm_manager.errors.show_error import show_error_dialog - -from ..interfaces import Callbacks, InitialJoinValues - -gi.require_version("Gtk", "3.0") - -from gi.repository import GdkPixbuf, Gio, Gtk - - -class Trust(Gtk.Box): - def __init__( - self, - initial_values: InitialJoinValues, - on_trust: Callable[[str, FlakeConfig], None], - ) -> None: - super().__init__() - - self.on_trust = on_trust - self.url: ClanURI | None = initial_values.url - - icon = Gtk.Image.new_from_pixbuf( - GdkPixbuf.Pixbuf.new_from_file_at_scale( - filename=str(assets.loc / "placeholder.jpeg"), - width=256, - height=256, - preserve_aspect_ratio=True, - ) - ) - layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, expand=True) - layout.set_border_width(20) - layout.set_spacing(20) - - if self.url is not None: - self.entry = Gtk.Label(label=str(self.url)) - layout.add(icon) - layout.add(Gtk.Label(label="Clan URL")) - else: - layout.add(Gtk.Label(label="Enter Clan URL")) - self.entry = Gtk.Entry() - # Autocomplete - # TODO: provide intelligent suggestions - completion_list = Gtk.ListStore(str) - completion_list.append(["clan://"]) - completion = Gtk.EntryCompletion() - completion.set_model(completion_list) - completion.set_text_column(0) - completion.set_popup_completion(False) - completion.set_inline_completion(True) - - self.entry.set_completion(completion) - self.entry.set_placeholder_text("clan://") - - layout.add(self.entry) - - if self.url is None: - trust_button = Gtk.Button(label="Load cLAN-URL") - else: - trust_button = Gtk.Button(label="Trust cLAN-URL") - - trust_button.connect("clicked", self.on_trust_clicked) - layout.add(trust_button) - - self.set_center_widget(layout) - - def on_trust_clicked(self, widget: Gtk.Widget) -> None: - try: - uri = self.url or ClanURI(self.entry.get_text()) - print(f"trusted: {uri}") - add_history(uri) - history = list_history() - found = filter( - lambda item: item.flake.flake_url == uri.get_internal(), history - ) - if found: - [item] = found - self.on_trust(uri.get_internal(), item.flake) - - except ClanError as e: - show_error_dialog(e) - - -class Details(Gtk.Box): - def __init__(self, url: str, flake: FlakeConfig) -> None: - super().__init__() - - self.flake = flake - - icon = Gtk.Image.new_from_pixbuf( - GdkPixbuf.Pixbuf.new_from_file_at_scale( - filename=str(flake.icon), - width=256, - height=256, - preserve_aspect_ratio=True, - ) - ) - layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, expand=True) - layout.set_border_width(20) - - upper = Gtk.Box(orientation="vertical") - upper.set_spacing(20) - upper.add(Gtk.Label(label="Clan URL")) - upper.add(icon) - - label = Gtk.Label(label=str(url)) - - upper.add(label) - - description_label = Gtk.Label(label=flake.description) - upper.add(description_label) - - lower = Gtk.Box(orientation="horizontal", expand=True) - lower.set_spacing(20) - - join_button = Gtk.Button(label="Join") - join_button.connect("clicked", self.on_join) - join_action_area = Gtk.Box(orientation="horizontal", expand=False) - join_button_area = Gtk.Box(orientation="vertical", expand=False) - join_action_area.pack_end(join_button_area, expand=False, fill=False, padding=0) - join_button_area.pack_end(join_button, expand=False, fill=False, padding=0) - join_details = Gtk.Label(label="Info") - - join_details_area = Gtk.Box(orientation="horizontal", expand=False) - join_label_area = Gtk.Box(orientation="vertical", expand=False) - - for info in [ - f"Memory: {flake.clan_name}", - "CPU: 2 Cores", - "Storage: 64 GiB", - ]: - details_label = Gtk.Label(label=info) - details_label.set_justify(Gtk.Justification.LEFT) - join_label_area.pack_end(details_label, expand=False, fill=False, padding=0) - - join_label_area.pack_end(join_details, expand=False, fill=False, padding=0) - join_details_area.pack_start( - join_label_area, expand=False, fill=False, padding=0 - ) - - lower.pack_start(join_details_area, expand=True, fill=True, padding=0) - lower.pack_end(join_action_area, expand=True, fill=True, padding=0) - layout.pack_start(upper, expand=False, fill=False, padding=0) - layout.add(lower) - - self.add(layout) - - def on_join(self, widget: Gtk.Widget) -> None: - # TODO: @Qubasa - - show_error_dialog(ClanError("Feature not ready yet."), "Info") - - -class JoinWindow(Gtk.ApplicationWindow): - def __init__(self, initial_values: InitialJoinValues, cbs: Callbacks) -> None: - super().__init__() - # Initialize the main wincbsdow - self.cbs = cbs - self.set_title("cLAN Manager") - self.connect("delete-event", self.on_quit) - self.set_default_size(800, 600) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, expand=True) - self.add(vbox) - - button = Gtk.ToolButton() - button.set_icon_name("go-previous") - button.connect("clicked", self.switch) - - toolbar = Gtk.Toolbar(orientation=Gtk.Orientation.HORIZONTAL) - toolbar.add(button) - vbox.add(toolbar) - - self.stack = Gtk.Stack() - - print("initial_values", initial_values) - self.stack.add_titled( - Trust(initial_values, on_trust=self.on_trust), - "trust", - "Trust", - ) - - vbox.add(self.stack) - - # vbox.add(Gtk.Entry(text=str(initial_values.url))) - - # Must be called AFTER all components were added - self.show_all() - - def on_trust(self, url: str, flake: FlakeConfig) -> None: - self.stack.add_titled( - Details(url=url, flake=flake), - "details", - "Details", - ) - self.show_all() - self.stack.set_visible_child_name("details") - - def switch(self, widget: Gtk.Widget) -> None: - self.cbs.show_list() - - 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 507a556fe..000000000 --- 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", "3.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, expand=True) - self.add(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.add(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/default.nix b/pkgs/clan-vm-manager/default.nix index dbd74ba15..0b6e01106 100644 --- a/pkgs/clan-vm-manager/default.nix +++ b/pkgs/clan-vm-manager/default.nix @@ -4,13 +4,12 @@ , copyDesktopItems , pygobject3 , wrapGAppsHook -, gtk3 -, spice-gtk +, gtk4 , gnome , gobject-introspection , clan-cli , makeDesktopItem -, ipdb +, libadwaita }: let source = ./.; @@ -33,7 +32,7 @@ python3.pkgs.buildPythonApplication { gobject-introspection ]; - buildInputs = [ spice-gtk gtk3 gnome.adwaita-icon-theme ]; + buildInputs = [ gtk4 libadwaita gnome.adwaita-icon-theme ]; propagatedBuildInputs = [ pygobject3 clan-cli ]; # also re-expose dependencies so we test them in CI diff --git a/pkgs/clan-vm-manager/shell.nix b/pkgs/clan-vm-manager/shell.nix index 884e0ee3f..2a683d5da 100644 --- a/pkgs/clan-vm-manager/shell.nix +++ b/pkgs/clan-vm-manager/shell.nix @@ -1,4 +1,4 @@ -{ clan-vm-manager, clan-cli, mkShell, ruff, desktop-file-utils, xdg-utils, mypy, python3Packages }: +{ clan-vm-manager, libadwaita, clan-cli, mkShell, ruff, desktop-file-utils, xdg-utils, mypy, python3Packages }: mkShell { inherit (clan-vm-manager) propagatedBuildInputs buildInputs; nativeBuildInputs = [ @@ -7,6 +7,7 @@ mkShell { xdg-utils mypy python3Packages.ipdb + libadwaita.devdoc # has the demo called 'adwaita-1-demo' ] ++ clan-vm-manager.nativeBuildInputs; PYTHONBREAKPOINT = "ipdb.set_trace";