1
0
forked from clan/clan-core

Compare commits

...

3 Commits

13 changed files with 554 additions and 579 deletions

View File

@ -2,37 +2,34 @@
import argparse
from dataclasses import dataclass
from pathlib import Path
import os
import gi
from clan_cli import vms
from clan_vm_manager.windows.flash import FlashUSBWindow
from clan_vm_manager.views.list import ClanList
gi.require_version("Gtk", "3.0")
# from clan_vm_manager.windows.flash import FlashUSBWindow
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
import multiprocessing as mp
from clan_cli.clan_uri import ClanURI
from gi.repository import Gio, Gtk
from gi.repository import Adw, Gio, Gtk, Gdk
from .constants import constants
from .errors.show_error import show_error_dialog
from .executor import ProcessManager
from .interfaces import Callbacks, InitialFlashValues, InitialJoinValues
from .windows.join import JoinWindow
from .windows.overview import OverviewWindow
@dataclass
class ClanWindows:
join: type[JoinWindow]
overview: type[OverviewWindow]
flash_usb: type[FlashUSBWindow]
# from .windows.join import JoinWindow
# from .windows.overview import OverviewWindow
@dataclass
class ClanConfig:
initial_window: str
initial_view: str
url: ClanURI | None
@ -41,36 +38,54 @@ def on_except(error: Exception, proc: mp.process.BaseProcess) -> None:
show_error_dialog(str(error))
class Application(Gtk.Application):
def __init__(self, windows: ClanWindows, config: ClanConfig) -> None:
class MainWindow(Adw.ApplicationWindow):
def __init__(self, app: Gtk.Application, config: ClanConfig) -> None:
super().__init__()
self.set_application(app)
self.set_title("Clan Manager")
# ToolbarView is the root layout.
# A Widget containing a page, as well as top and/or bottom bars.
# See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.ToolbarView.html
self.view = Adw.ToolbarView()
self.header = Adw.HeaderBar()
self.view.add_top_bar(self.header)
clan_list = ClanList(
app = app
)
self.stack = Adw.ViewStack()
self.stack.add_titled(
clan_list, "list", "list"
)
self.view.set_content(self.stack)
self.set_content(self.view)
# AdwApplication
# - handles library initialization by calling adw_init() in the default GApplication::startup signal handle
# - automatically loads stylesheets (style.css, style-dark.css, ...)
# - ...
# More see: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Application.html
class Application(Adw.Application):
def __init__(self, config: ClanConfig) -> None:
super().__init__(
application_id=constants["APPID"], flags=Gio.ApplicationFlags.FLAGS_NONE
)
self.init_style()
self.windows = windows
self.config = config
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.connect("activate", self.do_activate)
self.connect("shutdown", self.on_shutdown)
# constants
# breakpoint()
def on_shutdown(self, app: Gtk.Application) -> None:
print("Shutting down")
@ -99,56 +114,42 @@ class Application(Gtk.Application):
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()
Gio.Application.do_startup(self)
menu = Gio.Menu.new()
file_menu = Gio.Menu.new()
item = Gio.MenuItem.new("Menu Item", "app.menu_item")
file_menu.append_item(item)
menu.append_submenu("File", file_menu)
# TODO: add application menu
self.set_menubar(menu)
def do_activate(self) -> None:
win = self.props.active_window
if not win:
win = self.window
win.set_application(self)
win.present()
self.init_style()
window = MainWindow(app=self,config=self.config)
window.set_default_size(980,650)
window.present()
# TODO: For css styling
def init_style(self) -> None:
pass
# css_provider = Gtk.CssProvider()
# css_provider.load_from_resource(constants['RESOURCEID'] + '/style.css')
# screen = Gdk.Screen.get_default()
# style_context = Gtk.StyleContext()
# style_context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
resource_path = Path(__file__).parent / "style.css"
css_provider = Gtk.CssProvider()
css_provider.load_from_path(str(resource_path))
Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
def show_join(args: argparse.Namespace) -> None:
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"),
)
return app.run()
@ -160,17 +161,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)
parser.set_defaults(func=show_overview)

View File

@ -4,19 +4,19 @@ 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()

View File

@ -0,0 +1,27 @@
window {
/* Set consistent border radius */
border-radius: 10px;
}
.title {
margin-right: 20px;
}
.subtitle {
font-size: 12px;
font-weight: 300;
margin-right: 20px;
}
row {
padding-right: 10px;
padding-top: 10px;
padding-bottom: 10px;
}
stack {
padding-left: 10px;
padding-right: 10px;
padding-top: 10px;
padding-bottom: 10px;
}

View File

@ -3,7 +3,7 @@
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
@ -11,16 +11,17 @@ 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
orientation=Gtk.Orientation.VERTICAL, spacing=6
)
self.set_border_width(10)
# self.set_border_width(10)
self.stack = stack
button = Gtk.Button(label="Back to list", margin_left=10)
button = Gtk.Button(label="Back to list")
button.connect("clicked", self.switch)
self.add(button)
self.append(button)
self.add(Gtk.Label("Join cLan"))
label = Gtk.Label.new("Join cLan")
self.append(label)
def switch(self, widget: Gtk.Widget) -> None:
self.stack.set_visible_child_name("list")

View File

@ -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)

View File

@ -1,6 +1,6 @@
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
from ..models import VMBase
@ -21,7 +21,7 @@ class VmMenu(Gtk.Menu):
menu_item = Gtk.MenuItem(label=item[0])
menu_item.connect("activate", item[1])
self.append(menu_item)
self.show_all()
# self.show_all()
def start_vm(self, widget: Gtk.Widget) -> None:
print("start_vm")

View File

@ -7,7 +7,7 @@ from clan_vm_manager.errors.show_error import show_error_dialog
from ..interfaces import Callbacks, InitialFlashValues
gi.require_version("Gtk", "3.0")
gi.require_version("Gtk", "4.0")
from gi.repository import Gio, Gtk
@ -29,19 +29,19 @@ class FlashUSBWindow(Gtk.ApplicationWindow):
# Initialize the main wincbsdow
# self.cbs = cbs
self.set_title("cLAN Manager")
self.connect("delete-event", self.on_quit)
# 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)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, )
self.append(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)
# toolbar = Gtk.Toolbar(orientation=Gtk.Orientation.HORIZONTAL)
# toolbar.add(button)
# vbox.add(toolbar)
self.stack = Gtk.Stack()
@ -52,10 +52,10 @@ class FlashUSBWindow(Gtk.ApplicationWindow):
"Details",
)
vbox.add(self.stack)
vbox.append(self.stack)
# Must be called AFTER all components were added
self.show_all()
# self.show_all()
def switch(self, widget: Gtk.Widget) -> None:
pass

View File

@ -12,7 +12,9 @@ from clan_vm_manager.errors.show_error import show_error_dialog
from ..interfaces import Callbacks, InitialJoinValues
gi.require_version("Gtk", "3.0")
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import GdkPixbuf, Gio, Gtk
@ -36,16 +38,16 @@ class Trust(Gtk.Box):
preserve_aspect_ratio=True,
)
)
layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, expand=True)
layout.set_border_width(20)
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.add(icon)
layout.add(Gtk.Label(label="Clan URL"))
layout.append(icon)
layout.append(Gtk.Label(label="Clan URL"))
else:
layout.add(Gtk.Label(label="Enter Clan URL"))
layout.append(Gtk.Label(label="Enter Clan URL"))
self.entry = Gtk.Entry()
# Autocomplete
# TODO: provide intelligent suggestions
@ -60,7 +62,7 @@ class Trust(Gtk.Box):
self.entry.set_completion(completion)
self.entry.set_placeholder_text("clan://")
layout.add(self.entry)
layout.append(self.entry)
if self.url is None:
trust_button = Gtk.Button(label="Load cLAN-URL")
@ -68,9 +70,9 @@ class Trust(Gtk.Box):
trust_button = Gtk.Button(label="Trust cLAN-URL")
trust_button.connect("clicked", self.on_trust_clicked)
layout.add(trust_button)
layout.append(trust_button)
self.set_center_widget(layout)
self.append(layout)
def on_trust_clicked(self, widget: Gtk.Widget) -> None:
try:
@ -86,6 +88,7 @@ class Trust(Gtk.Box):
self.on_trust(uri.get_internal(), item.flake)
except ClanError as e:
pass
show_error_dialog(e)
@ -103,34 +106,34 @@ class Details(Gtk.Box):
preserve_aspect_ratio=True,
)
)
layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, expand=True)
layout.set_border_width(20)
layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, )
# layout.set_border_width(20)
upper = Gtk.Box(orientation="vertical")
upper.set_spacing(20)
upper.add(Gtk.Label(label="Clan URL"))
upper.add(icon)
upper.append(Gtk.Label(label="Clan URL"))
upper.append(icon)
label = Gtk.Label(label=str(url))
upper.add(label)
upper.append(label)
description_label = Gtk.Label(label=flake.description)
upper.add(description_label)
upper.append(description_label)
lower = Gtk.Box(orientation="horizontal", expand=True)
lower = Gtk.Box(orientation="horizontal", )
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_action_area = Gtk.Box(orientation="horizontal", )
join_button_area = Gtk.Box(orientation="vertical", )
join_action_area.append(join_button_area)
join_button_area.append(join_button)
join_details = Gtk.Label(label="Info")
join_details_area = Gtk.Box(orientation="horizontal", expand=False)
join_label_area = Gtk.Box(orientation="vertical", expand=False)
join_details_area = Gtk.Box(orientation="horizontal", )
join_label_area = Gtk.Box(orientation="vertical", )
for info in [
f"Memory: {flake.clan_name}",
@ -139,19 +142,19 @@ class Details(Gtk.Box):
]:
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.append(details_label)
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
join_label_area.append(join_details)
join_details_area.append(
join_label_area
)
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)
lower.append(join_details_area)
lower.append(join_action_area)
layout.append(upper)
layout.append(lower)
self.add(layout)
self.append(layout)
def on_join(self, widget: Gtk.Widget) -> None:
# TODO: @Qubasa
@ -163,21 +166,34 @@ 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.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)
# menu = Gtk.Menu()
# menu_bar = Gtk.MenuBar()
button = Gtk.ToolButton()
button.set_icon_name("go-previous")
button.connect("clicked", self.switch)
# main_menu = Gtk.MenuItem("Menu")
# menu_item = Gtk.MenuItem("Start", "Start the selected clan")
toolbar = Gtk.Toolbar(orientation=Gtk.Orientation.HORIZONTAL)
toolbar.add(button)
vbox.add(toolbar)
# menu_bar.append(main_menu)
# main_menu.set_submenu(menu)
# menu.append(menu_item)
# vbox.add(menu_bar)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.set_child(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()
@ -188,12 +204,12 @@ class JoinWindow(Gtk.ApplicationWindow):
"Trust",
)
vbox.add(self.stack)
vbox.append(self.stack)
# vbox.add(Gtk.Entry(text=str(initial_values.url)))
# Must be called AFTER all components were added
self.show_all()
# self.show_all()
def on_trust(self, url: str, flake: FlakeConfig) -> None:
self.stack.add_titled(
@ -201,7 +217,7 @@ class JoinWindow(Gtk.ApplicationWindow):
"details",
"Details",
)
self.show_all()
# self.show_all()
self.stack.set_visible_child_name("details")
def switch(self, widget: Gtk.Widget) -> None:

View File

@ -0,0 +1,282 @@
from collections.abc import Callable
import gi
gi.require_version("Adw", "1")
from gi.repository import Adw, GdkPixbuf, Gio, GObject, Gtk
from pathlib import Path
from ..models import VMBase, get_initial_vms
# from .context_menu import VmMenu
class ClanEditForm(Gtk.Box):
def __init__(self, *, selected: VMBase | None) -> None:
super().__init__()
self.page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
# self.set_border_width(10)
self.selected = selected
# self.set_selection_mode(0)
if self.selected:
label = Gtk.Box()
label.append(Gtk.Label.new(f"\n {self.selected.name}"))
self.append(label)
# ---------- row 1 --------
# row = Gtk.ListBoxRow()
# row_layout = Gtk.Box(spacing=6)
# # Doc: pack_start/end takes alignment params Expand, Fill, Padding
# row_layout.append(Gtk.Label.new("Memory Size in MiB"))
# row_layout.append(
# Gtk.SpinButton.new_with_range(512, 4096, 256)
# )
# row.append(row_layout)
# self.append(row)
# # ----------- row 2 -------
# row = Gtk.ListBoxRow()
# row_layout = Gtk.Box(spacing=6)
# row_layout.append(Gtk.Label("CPU Count"))
# row_layout.append(Gtk.SpinButton.new_with_range(1, 5, 1))
# row.append(row_layout)
# self.append(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, )
self.show_list = remount_list
self.selected = selected_vm
# self.toolbar = ClanEditToolbar(on_save_clicked=self.on_save)
# self.add(self.toolbar)
self.append(ClanEditForm(selected=self.selected))
def on_save(self, widget: Gtk.Widget) -> None:
print("Save clicked saving values")
self.show_list()
class VMListItem(GObject.Object):
def __init__(self, data: VMBase) -> None:
super().__init__()
self.data = data
class ClanIcon(Gtk.Box):
def __init__(self, icon_path: Path) -> None:
super().__init__()
self.append(
GdkPixbuf.Pixbuf.new_from_file_at_scale(
filename=icon_path, width=64, height=64, preserve_aspect_ratio=True
)
)
class ClanList(Gtk.Box):
"""
The ClanList
Is the composition of
the ClanListToolbar
the clanListView
# ------------------------ #
# - Tools <Start> <Stop> < Edit> #
# ------------------------ #
# - List Items
# - <...>
# ------------------------#
"""
def __init__(
self,
*,
app: Adw.Application
) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.application = app
boxed_list = Gtk.ListBox()
boxed_list.set_selection_mode(Gtk.SelectionMode.NONE)
boxed_list.add_css_class("boxed-list")
def create_widget(item: VMListItem) -> Gtk.Widget:
print("Creating", item.data)
vm = item.data
# Not displayed; Can be used as id.
row = Adw.SwitchRow()
row.set_name(vm.url)
row.set_title(vm.name)
row.set_title_lines(1)
row.set_subtitle(vm.url)
row.set_subtitle_lines(1)
avatar = Adw.Avatar()
avatar.set_text(vm.name)
avatar.set_show_initials(True)
avatar.set_size(50)
# (GdkPixbuf.Pixbuf.new_from_file_at_scale(
# filename=vm.icon,
# width=512,
# height=512,
# preserve_aspect_ratio=True,
# ))
# Gtk.Image.new_from_pixbuf(
# )
row.add_prefix(avatar)
row.connect("notify::active", self.on_row_toggle)
return row
list_store = Gio.ListStore()
print(list_store)
for vm in get_initial_vms(app.running_vms()):
list_store.append(VMListItem(data=vm.base))
boxed_list.bind_model(list_store, create_widget_func=create_widget)
self.append(boxed_list)
def on_row_toggle(self, row: Adw.SwitchRow, state: bool) -> None:
print("Toggled", row.get_name(), "active:", row.get_active())
# TODO: start VM here
# question: Should we disable the switch
# for the time until we got a response for this VM?
class ClanListView(Gtk.Box):
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__()
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)
# 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.append(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: Any
# ) -> 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)

View File

@ -0,0 +1,79 @@
import gi
gi.require_version("Gtk", "4.0")
# class List():
# def __init__(self, cbs: Callbacks) -> None:
# super().__init__()
# self.set_title("cLAN Manager")
# # self.connect("delete-event", self.on_quit)
# self.set_default_size(800, 600)
# self.cbs = cbs
# vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, )
# self.set_child(vbox)
# self.stack = Gtk.Stack()
# # Add named stacks
# self.stack.add_titled(clan_list, "list", "List")
# self.stack.add_titled(
# ClanJoinPage(stack=self.remount_list_view), "join", "Join"
# )
# self.stack.add_titled(
# ClanEdit(remount_list=self.remount_list_view, selected_vm=None),
# "edit",
# "Edit",
# )
# vbox.append(self.stack)
# # Must be called AFTER all components were added
# # self.show_all()
# def set_selected(self, sel: VMBase | None) -> None:
# self.selected_vm = sel
# def remount_list_view(self) -> None:
# widget = self.stack.get_child_by_name("list")
# if widget:
# widget.destroy()
# vms = []
# for vm in get_initial_vms(self.cbs.running_vms()):
# vms.append(vm.base)
# # FIXME: It feels very odd that we have to re-fetch the selected VM.
# # The model should be just updated in-place.
# if self.selected_vm and vm.base.url == self.selected_vm.url:
# self.selected_vm = vm.base
# clan_list = ClanList(
# vms=vms,
# cbs=self.cbs,
# remount_list=self.remount_list_view,
# remount_edit=self.remount_edit_view,
# set_selected=self.set_selected,
# selected_vm=self.selected_vm,
# )
# self.stack.add_titled(clan_list, "list", "List")
# # self.show_all()
# self.stack.set_visible_child_name("list")
# def remount_edit_view(self) -> None:
# widget = self.stack.get_child_by_name("edit")
# if widget:
# widget.destroy()
# self.stack.add_titled(
# ClanEdit(remount_list=self.remount_list_view, selected_vm=self.selected_vm),
# "edit",
# "Edit",
# )
# # self.show_all()
# self.stack.set_visible_child_name("edit")
# def on_quit(self, *args: Any) -> None:
# Gio.Application.quit(self.get_application())

View File

@ -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())

View File

@ -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