use std::cell::RefCell;
use gtk::{
gio,
glib::{self, clone, Continue, Receiver},
prelude::*,
subclass::prelude::*,
};
use log::{info, warn};
use pipewire::{channel::Sender, spa::Direction};
use crate::{
view::{self},
GtkMessage, MediaType, PipewireLink, PipewireMessage,
};
static STYLE: &str = include_str!("style.css");
mod imp {
use super::*;
use once_cell::unsync::OnceCell;
#[derive(Default)]
pub struct Application {
pub(super) graphview: view::GraphView,
pub(super) pw_sender: OnceCell<RefCell<Sender<GtkMessage>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for Application {
const NAME: &'static str = "HelvumApplication";
type Type = super::Application;
type ParentType = gtk::Application;
}
impl ObjectImpl for Application {}
impl ApplicationImpl for Application {
fn activate(&self, app: &Self::Type) {
let scrollwindow = gtk::ScrolledWindowBuilder::new()
.child(&self.graphview)
.build();
let window = gtk::ApplicationWindowBuilder::new()
.application(app)
.default_width(1280)
.default_height(720)
.title("Helvum - Pipewire Patchbay")
.child(&scrollwindow)
.build();
window
.settings()
.set_gtk_application_prefer_dark_theme(true);
window.show();
}
fn startup(&self, app: &Self::Type) {
self.parent_startup(app);
let provider = gtk::CssProvider::new();
provider.load_from_data(STYLE.as_bytes());
gtk::StyleContext::add_provider_for_display(
>k::gdk::Display::default().expect("Error initializing gtk css provider."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
}
impl GtkApplicationImpl for Application {}
}
glib::wrapper! {
pub struct Application(ObjectSubclass<imp::Application>)
@extends gio::Application, gtk::Application,
@implements gio::ActionGroup, gio::ActionMap;
}
impl Application {
pub(super) fn new(
gtk_receiver: Receiver<PipewireMessage>,
pw_sender: Sender<GtkMessage>,
) -> Self {
let app: Application =
glib::Object::new(&[("application-id", &"org.freedesktop.ryuukyu.helvum")])
.expect("Failed to create new Application");
let imp = imp::Application::from_instance(&app);
imp.pw_sender
.set(RefCell::new(pw_sender))
.map_err(|_| ())
.expect("pw_sender field was already set");
let quit = gtk::gio::SimpleAction::new("quit", None);
quit.connect_activate(clone!(@weak app => move |_, _| {
app.quit();
}));
app.set_accels_for_action("app.quit", &["<Control>Q"]);
app.add_action(&quit);
gtk_receiver.attach(
None,
clone!(
@weak app => @default-return Continue(true),
move |msg| {
match msg {
PipewireMessage::NodeAdded{ id, name } => app.add_node(id,name),
PipewireMessage::PortAdded{ id, node_id, name, direction, media_type} => app.add_port(id,name,node_id,direction,media_type),
PipewireMessage::LinkAdded{ id, node_from, port_from, node_to, port_to} => app.add_link(id,node_from,port_from,node_to,port_to),
PipewireMessage::NodeRemoved { id } => app.remove_node(id),
PipewireMessage::PortRemoved { id, node_id } => app.remove_port(id, node_id),
PipewireMessage::LinkRemoved { id } => app.remove_link(id)
};
Continue(true)
}
),
);
app
}
pub fn add_node(&self, id: u32, name: String) {
info!("Adding node to graph: id {}", id);
imp::Application::from_instance(self)
.graphview
.add_node(id, view::Node::new(name.as_str()));
}
pub fn add_port(
&self,
id: u32,
name: String,
node_id: u32,
direction: Direction,
media_type: Option<MediaType>,
) {
info!("Adding port to graph: id {}", id);
let imp = imp::Application::from_instance(self);
let port = view::Port::new(id, name.as_str(), direction, media_type);
if let Err(e) = port.connect_local(
"port_toggled",
false,
clone!(@weak self as app => @default-return None, move |args| {
let port_from = args[1].get::<u32>().unwrap();
let port_to = args[2].get::<u32>().unwrap();
app.toggle_link(port_from, port_to);
None
}),
) {
warn!("Failed to connect to \"port-toggled\" signal: {}", e);
}
imp.graphview.add_port(node_id, id, port);
}
pub fn add_link(&self, id: u32, node_from: u32, port_from: u32, node_to: u32, port_to: u32) {
info!("Adding link to graph: id {}", id);
imp::Application::from_instance(self).graphview.add_link(
id,
PipewireLink {
node_from,
port_from,
node_to,
port_to,
},
);
}
fn toggle_link(&self, port_from: u32, port_to: u32) {
let imp = imp::Application::from_instance(self);
let sender = imp.pw_sender.get().expect("pw_sender not set").borrow_mut();
sender
.send(GtkMessage::ToggleLink { port_from, port_to })
.expect("Failed to send message");
}
fn remove_node(&self, id: u32) {
info!("Removing node from graph: id {}", id);
let imp = imp::Application::from_instance(self);
imp.graphview.remove_node(id);
}
fn remove_port(&self, id: u32, node_id: u32) {
info!("Removing port from graph: id {}, node_id: {}", id, node_id);
let imp = imp::Application::from_instance(self);
imp.graphview.remove_port(id, node_id);
}
fn remove_link(&self, id: u32) {
info!("Removing link from graph: id {}", id);
let imp = imp::Application::from_instance(self);
imp.graphview.remove_link(id);
}
}