Gtk4 migration (#693)
Co-authored-by: Qubasa <consulting@qube.email> Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/693 Co-authored-by: Johannes Kirschbauer <hsjobeki@gmail.com> Co-committed-by: Johannes Kirschbauer <hsjobeki@gmail.com>
This commit is contained in:
committed by
Luis Hebendanz
parent
ae1745289d
commit
2d613e3933
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
37
pkgs/clan-vm-manager/clan_vm_manager/model/use_views.py
Normal file
37
pkgs/clan-vm-manager/clan_vm_manager/model/use_views.py
Normal file
@@ -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
|
||||
53
pkgs/clan-vm-manager/clan_vm_manager/model/use_vms.py
Normal file
53
pkgs/clan-vm-manager/clan_vm_manager/model/use_vms.py
Normal file
@@ -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()
|
||||
@@ -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 <path>` 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
|
||||
|
||||
16
pkgs/clan-vm-manager/clan_vm_manager/style.css
Normal file
16
pkgs/clan-vm-manager/clan_vm_manager/style.css
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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 <Start> <Stop> < 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)
|
||||
@@ -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")
|
||||
80
pkgs/clan-vm-manager/clan_vm_manager/views/list.py
Normal file
80
pkgs/clan-vm-manager/clan_vm_manager/views/list.py
Normal file
@@ -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 <Start> <Stop> < 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()
|
||||
139
pkgs/clan-vm-manager/clan_vm_manager/views/trust_join.py
Normal file
139
pkgs/clan-vm-manager/clan_vm_manager/views/trust_join.py
Normal file
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user