mirror of
https://gitlab.freedesktop.org/pipewire/helvum
synced 2026-03-15 19:46:10 +08:00
Merge Controller and View structs into one Application struct.
The `View` sturct was mostly a layer of indirection, and the controller benefitted by absorbing the gtk::Application subclass parts, so now those two are merged into a new gtk::Application subclass.
This commit is contained in:
@@ -10,6 +10,7 @@ Helvum uses an architecture with the components laid out like this:
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌──────┐
|
┌──────┐
|
||||||
|
│ GTK │
|
||||||
│ View │
|
│ View │
|
||||||
└────┬─┘
|
└────┬─┘
|
||||||
Λ ┆
|
Λ ┆
|
||||||
@@ -20,11 +21,10 @@ Helvum uses an architecture with the components laid out like this:
|
|||||||
│ ┆
|
│ ┆
|
||||||
│ ┆
|
│ ┆
|
||||||
│ V notifies of remote changes
|
│ V notifies of remote changes
|
||||||
┌┴───────────┐ via messages ┌─────────────────────┐
|
┌┴────────────┐ via messages ┌───────────────────┐
|
||||||
│ │<╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ Seperate │
|
│ Application │<╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ Seperate │
|
||||||
│ Controller │ │ Pipewire │
|
│ Object ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌>│ Pipewire Thread │
|
||||||
│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌>│ Thread │
|
└┬────────────┘ request changes to remote └───────────────────┘
|
||||||
└┬───────────┘ Request changes to remote └─────────────────────┘
|
|
||||||
│ via messages Λ
|
│ via messages Λ
|
||||||
│ ║
|
│ ║
|
||||||
│<─── updates/reads state ║
|
│<─── updates/reads state ║
|
||||||
@@ -38,17 +38,17 @@ The program is split between two threads, with most stuff happening inside the G
|
|||||||
The GTK thread will sit in a GTK event processing loop, while the pipewire thread will sit in a
|
The GTK thread will sit in a GTK event processing loop, while the pipewire thread will sit in a
|
||||||
pipewire event processing loop.
|
pipewire event processing loop.
|
||||||
|
|
||||||
The `Controller` struct inside the GTK thread is the centerpiece of this architecture.
|
The `Application` object inside the GTK thread is the centerpiece of this architecture.
|
||||||
It communicates with the pipewire thread using two channels,
|
It communicates with the pipewire thread using two channels,
|
||||||
where each message sent by one thread will trigger the loop of the other thread to invoke a callback
|
where each message sent by one thread will trigger the loop of the other thread to invoke a callback
|
||||||
with the received message.
|
with the received message.
|
||||||
|
|
||||||
For each change on the remote pipewire server, the GTK thread is notified by the pipewire thread
|
For each change on the remote pipewire server, the `Application` in the GTK thread is notified by the pipewire thread
|
||||||
and updates the view to reflect those changes, and additionally memorizes anything it might need later in the state.
|
and updates the view to reflect those changes, and additionally memorizes anything it might need later in the state.
|
||||||
|
|
||||||
Additionally, a user may also make changes using the view.
|
Additionally, a user may also make changes using the view.
|
||||||
For each change, the view notifies the controller by emitting a matching signal.
|
For each change, the view notifies the `Application` by emitting a matching signal.
|
||||||
The controller will then request the pipewire connection to make those changes on the remote. \
|
The `Application` will then request the pipewire thread to make those changes on the remote. \
|
||||||
These changes will then be applied to the view like any other remote changes as explained above.
|
These changes will then be applied to the view like any other remote changes as explained above.
|
||||||
|
|
||||||
# View Architecture
|
# View Architecture
|
||||||
|
|||||||
258
src/application.rs
Normal file
258
src/application.rs
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
use std::{cell::RefCell, collections::HashMap};
|
||||||
|
|
||||||
|
use gtk::{
|
||||||
|
gio,
|
||||||
|
glib::{self, clone, Continue, Receiver},
|
||||||
|
prelude::*,
|
||||||
|
subclass::prelude::*,
|
||||||
|
};
|
||||||
|
use log::{info, warn};
|
||||||
|
use pipewire::spa::Direction;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
view::{self},
|
||||||
|
PipewireMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum MediaType {
|
||||||
|
Audio,
|
||||||
|
Video,
|
||||||
|
Midi,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Any pipewire item we need to keep track of.
|
||||||
|
/// These will be saved in the controllers `state` map associated with their id.
|
||||||
|
enum Item {
|
||||||
|
Node {
|
||||||
|
// Keep track of the widget to easily remove ports on it later.
|
||||||
|
// widget: view::Node,
|
||||||
|
// Keep track of the nodes media type to color ports on it.
|
||||||
|
media_type: Option<MediaType>,
|
||||||
|
},
|
||||||
|
Port {
|
||||||
|
// Save the id of the node this is on so we can remove the port from it
|
||||||
|
// when it is deleted.
|
||||||
|
node_id: u32,
|
||||||
|
},
|
||||||
|
// We don't need to memorize anything about links right now, but we need to
|
||||||
|
// be able to find out an id is a link.
|
||||||
|
Link,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This should be in its own .css file.
|
||||||
|
static STYLE: &str = "
|
||||||
|
.audio {
|
||||||
|
background: rgb(50,100,240);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video {
|
||||||
|
background: rgb(200,200,0);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.midi {
|
||||||
|
background: rgb(200,0,50);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
";
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Application {
|
||||||
|
pub(super) graphview: view::GraphView,
|
||||||
|
pub(super) state: RefCell<HashMap<u32, Item>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
|
.get_settings()
|
||||||
|
.set_property_gtk_application_prefer_dark_theme(true);
|
||||||
|
window.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn startup(&self, app: &Self::Type) {
|
||||||
|
self.parent_startup(app);
|
||||||
|
|
||||||
|
// Load CSS from the STYLE variable.
|
||||||
|
let provider = gtk::CssProvider::new();
|
||||||
|
provider.load_from_data(STYLE.as_bytes());
|
||||||
|
gtk::StyleContext::add_provider_for_display(
|
||||||
|
>k::gdk::Display::get_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 {
|
||||||
|
/// Create the view.
|
||||||
|
/// This will set up the entire user interface and prepare it for being run.
|
||||||
|
pub(super) fn new(gtk_receiver: Receiver<PipewireMessage>) -> Self {
|
||||||
|
let app: Application =
|
||||||
|
glib::Object::new(&[("application-id", &"org.freedesktop.ryuukyu.helvum")])
|
||||||
|
.expect("Failed to create new Application");
|
||||||
|
|
||||||
|
// Add <Control-Q> shortcut for quitting the application.
|
||||||
|
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);
|
||||||
|
|
||||||
|
// React to messages received from the pipewire thread.
|
||||||
|
gtk_receiver.attach(
|
||||||
|
None,
|
||||||
|
clone!(
|
||||||
|
@weak app => @default-return Continue(true),
|
||||||
|
move |msg| {
|
||||||
|
match msg {
|
||||||
|
PipewireMessage::NodeAdded {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
media_type,
|
||||||
|
} => app.add_node(id, name, media_type),
|
||||||
|
PipewireMessage::PortAdded {
|
||||||
|
id,
|
||||||
|
node_id,
|
||||||
|
name,
|
||||||
|
direction,
|
||||||
|
} => app.add_port(id, name, node_id, direction),
|
||||||
|
PipewireMessage::LinkAdded { id, link } => app.add_link(id, link),
|
||||||
|
PipewireMessage::ObjectRemoved { id } => app.remove_global(id),
|
||||||
|
};
|
||||||
|
Continue(true)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new node to the view.
|
||||||
|
pub fn add_node(&self, id: u32, name: String, media_type: Option<MediaType>) {
|
||||||
|
info!("Adding node to graph: id {}", id);
|
||||||
|
|
||||||
|
let imp = imp::Application::from_instance(self);
|
||||||
|
|
||||||
|
imp.state.borrow_mut().insert(
|
||||||
|
id,
|
||||||
|
Item::Node {
|
||||||
|
// widget: node_widget,
|
||||||
|
media_type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
imp.graphview.add_node(id, view::Node::new(name.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new port to the view.
|
||||||
|
pub fn add_port(&self, id: u32, name: String, node_id: u32, direction: Direction) {
|
||||||
|
info!("Adding port to graph: id {}", id);
|
||||||
|
|
||||||
|
let imp = imp::Application::from_instance(self);
|
||||||
|
|
||||||
|
// Find out the nodes media type so that the port can be colored.
|
||||||
|
let media_type =
|
||||||
|
if let Some(Item::Node { media_type, .. }) = imp.state.borrow().get(&node_id) {
|
||||||
|
media_type.to_owned()
|
||||||
|
} else {
|
||||||
|
warn!("Node not found for Port {}", id);
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save node_id so we can delete this port easily.
|
||||||
|
imp.state.borrow_mut().insert(id, Item::Port { node_id });
|
||||||
|
|
||||||
|
let port = view::Port::new(id, name.as_str(), direction, media_type);
|
||||||
|
imp.graphview.add_port(node_id, id, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new link to the view.
|
||||||
|
pub fn add_link(&self, id: u32, link: crate::PipewireLink) {
|
||||||
|
info!("Adding link to graph: id {}", id);
|
||||||
|
|
||||||
|
let imp = imp::Application::from_instance(self);
|
||||||
|
|
||||||
|
// FIXME: Links should be colored depending on the data they carry (video, audio, midi) like ports are.
|
||||||
|
|
||||||
|
imp.state.borrow_mut().insert(id, Item::Link);
|
||||||
|
|
||||||
|
// Update graph to contain the new link.
|
||||||
|
imp.graphview.add_link(id, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a global object being removed.
|
||||||
|
pub fn remove_global(&self, id: u32) {
|
||||||
|
let imp = imp::Application::from_instance(self);
|
||||||
|
|
||||||
|
if let Some(item) = imp.state.borrow_mut().remove(&id) {
|
||||||
|
match item {
|
||||||
|
Item::Node { .. } => self.remove_node(id),
|
||||||
|
Item::Port { node_id } => self.remove_port(id, node_id),
|
||||||
|
Item::Link => self.remove_link(id),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Attempted to remove item with id {} that is not saved in state",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the node with the specified id from the view.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the port with the id `id` from the node with the id `node_id`
|
||||||
|
/// from the view.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the link with the specified id from the view.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
|
||||||
|
|
||||||
use gtk::glib::{self, clone, Continue, Receiver};
|
|
||||||
use log::{info, warn};
|
|
||||||
use pipewire::spa::Direction;
|
|
||||||
|
|
||||||
use crate::{view, PipewireLink, PipewireMessage};
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum MediaType {
|
|
||||||
Audio,
|
|
||||||
Video,
|
|
||||||
Midi,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Any pipewire item we need to keep track of.
|
|
||||||
/// These will be saved in the controllers `state` map associated with their id.
|
|
||||||
enum Item {
|
|
||||||
Node {
|
|
||||||
// Keep track of the widget to easily remove ports on it later.
|
|
||||||
// widget: view::Node,
|
|
||||||
// Keep track of the nodes media type to color ports on it.
|
|
||||||
media_type: Option<MediaType>,
|
|
||||||
},
|
|
||||||
Port {
|
|
||||||
// Save the id of the node this is on so we can remove the port from it
|
|
||||||
// when it is deleted.
|
|
||||||
node_id: u32,
|
|
||||||
},
|
|
||||||
// We don't need to memorize anything about links right now, but we need to
|
|
||||||
// be able to find out an id is a link.
|
|
||||||
Link,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mediater between the pipewire connection and the view.
|
|
||||||
///
|
|
||||||
/// The Controller is the central piece of the architecture.
|
|
||||||
/// It manages the view, receives updates from the pipewire connection
|
|
||||||
/// and relays changes the user made to the pipewire connection.
|
|
||||||
///
|
|
||||||
/// It also keeps and manages a state object that contains the current state of objects present on the remote.
|
|
||||||
pub struct Controller {
|
|
||||||
state: HashMap<u32, Item>,
|
|
||||||
view: view::View,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Controller {
|
|
||||||
/// Create a new controller.
|
|
||||||
///
|
|
||||||
/// This function returns an `Rc`, because `Weak` references are needed inside closures the controller
|
|
||||||
/// passes to other components.
|
|
||||||
///
|
|
||||||
/// The returned `Rc` will be the only strong reference kept to the controller, so dropping the `Rc`
|
|
||||||
/// will also drop the controller, unless the `Rc` is cloned outside of this function.
|
|
||||||
pub(super) fn new(
|
|
||||||
view: view::View,
|
|
||||||
gtk_receiver: Receiver<PipewireMessage>,
|
|
||||||
) -> Rc<RefCell<Controller>> {
|
|
||||||
let result = Rc::new(RefCell::new(Controller {
|
|
||||||
view,
|
|
||||||
state: HashMap::new(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// React to messages received from the pipewire thread.
|
|
||||||
gtk_receiver.attach(
|
|
||||||
None,
|
|
||||||
clone!(
|
|
||||||
@weak result as controller => @default-return Continue(true),
|
|
||||||
move |msg| {
|
|
||||||
match msg {
|
|
||||||
PipewireMessage::NodeAdded {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
media_type,
|
|
||||||
} => controller.borrow_mut().add_node(id, name, media_type),
|
|
||||||
PipewireMessage::PortAdded {
|
|
||||||
id,
|
|
||||||
node_id,
|
|
||||||
name,
|
|
||||||
direction,
|
|
||||||
} => controller
|
|
||||||
.borrow_mut()
|
|
||||||
.add_port(id, node_id, name, direction),
|
|
||||||
PipewireMessage::LinkAdded { id, link } => controller.borrow_mut().add_link(id, link),
|
|
||||||
PipewireMessage::ObjectRemoved { id } => controller.borrow_mut().remove_global(id),
|
|
||||||
};
|
|
||||||
Continue(true)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a node object being added.
|
|
||||||
pub(super) fn add_node(&mut self, id: u32, name: String, media_type: Option<MediaType>) {
|
|
||||||
info!("Adding node to graph: id {}", id);
|
|
||||||
|
|
||||||
self.view.add_node(id, name.as_str());
|
|
||||||
|
|
||||||
self.state.insert(
|
|
||||||
id,
|
|
||||||
Item::Node {
|
|
||||||
// widget: node_widget,
|
|
||||||
media_type,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a port object being added.
|
|
||||||
pub(super) fn add_port(&mut self, id: u32, node_id: u32, name: String, direction: Direction) {
|
|
||||||
info!("Adding port to graph: id {}", id);
|
|
||||||
|
|
||||||
// Update graph to contain the new port.
|
|
||||||
|
|
||||||
// Find out the nodes media type so that the port can be colored.
|
|
||||||
let media_type = if let Some(Item::Node { media_type, .. }) = self.state.get(&node_id) {
|
|
||||||
media_type.to_owned()
|
|
||||||
} else {
|
|
||||||
warn!("Node not found for Port {}", id);
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
self.view
|
|
||||||
.add_port(node_id, id, &name, direction, media_type);
|
|
||||||
|
|
||||||
// Save node_id so we can delete this port easily.
|
|
||||||
self.state.insert(id, Item::Port { node_id });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a link object being added.
|
|
||||||
pub(super) fn add_link(&mut self, id: u32, link: PipewireLink) {
|
|
||||||
info!("Adding link to graph: id {}", id);
|
|
||||||
|
|
||||||
// FIXME: Links should be colored depending on the data they carry (video, audio, midi) like ports are.
|
|
||||||
|
|
||||||
self.state.insert(id, Item::Link);
|
|
||||||
|
|
||||||
// Update graph to contain the new link.
|
|
||||||
self.view.add_link(id, link);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a globalobject being removed.
|
|
||||||
/// Relevant objects are removed from the view and/or the state.
|
|
||||||
///
|
|
||||||
/// This is called from the `PipewireConnection` via callback.
|
|
||||||
pub(super) fn remove_global(&mut self, id: u32) {
|
|
||||||
if let Some(item) = self.state.remove(&id) {
|
|
||||||
match item {
|
|
||||||
Item::Node { .. } => {
|
|
||||||
info!("Removing node from graph: id {}", id);
|
|
||||||
self.view.remove_node(id);
|
|
||||||
}
|
|
||||||
Item::Port { node_id } => {
|
|
||||||
info!("Removing port from graph: id {}, node_id: {}", id, node_id);
|
|
||||||
self.view.remove_port(id, node_id);
|
|
||||||
}
|
|
||||||
Item::Link => {
|
|
||||||
info!("Removing link from graph: id {}", id);
|
|
||||||
self.view.remove_link(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"Attempted to remove item with id {} that is not saved in state",
|
|
||||||
id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
mod controller;
|
mod application;
|
||||||
mod pipewire_connection;
|
mod pipewire_connection;
|
||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
use controller::MediaType;
|
use application::MediaType;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
glib::{self, PRIORITY_DEFAULT},
|
glib::{self, PRIORITY_DEFAULT},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
@@ -56,10 +56,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let pw_thread =
|
let pw_thread =
|
||||||
std::thread::spawn(move || pipewire_connection::thread_main(gtk_sender, pw_receiver));
|
std::thread::spawn(move || pipewire_connection::thread_main(gtk_sender, pw_receiver));
|
||||||
|
|
||||||
let view = view::View::new();
|
let app = application::Application::new(gtk_receiver);
|
||||||
let _controller = controller::Controller::new(view.clone(), gtk_receiver);
|
|
||||||
|
|
||||||
view.run(&std::env::args().collect::<Vec<_>>());
|
app.run(&std::env::args().collect::<Vec<_>>());
|
||||||
|
|
||||||
pw_sender
|
pw_sender
|
||||||
.send(GtkMessage::Terminate)
|
.send(GtkMessage::Terminate)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use pipewire::{
|
|||||||
Context, MainLoop,
|
Context, MainLoop,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{controller::MediaType, GtkMessage, PipewireMessage};
|
use crate::{application::MediaType, GtkMessage, PipewireMessage};
|
||||||
|
|
||||||
/// The "main" function of the pipewire thread.
|
/// The "main" function of the pipewire thread.
|
||||||
pub(super) fn thread_main(
|
pub(super) fn thread_main(
|
||||||
|
|||||||
165
src/view/mod.rs
165
src/view/mod.rs
@@ -1,170 +1,11 @@
|
|||||||
//! The view presented to the user.
|
//! The view presented to the user.
|
||||||
//!
|
//!
|
||||||
//! This module contains gtk widgets and helper struct needed to present the graphical user interface.
|
//! This module contains gtk widgets needed to present the graphical user interface.
|
||||||
|
|
||||||
mod graph_view;
|
mod graph_view;
|
||||||
mod node;
|
mod node;
|
||||||
pub mod port;
|
mod port;
|
||||||
|
|
||||||
pub use graph_view::GraphView;
|
pub use graph_view::GraphView;
|
||||||
pub use node::Node;
|
pub use node::Node;
|
||||||
|
pub use port::Port;
|
||||||
use gtk::{
|
|
||||||
gio,
|
|
||||||
glib::{self, clone},
|
|
||||||
prelude::*,
|
|
||||||
subclass::prelude::ObjectSubclassExt,
|
|
||||||
};
|
|
||||||
use pipewire::spa::Direction;
|
|
||||||
|
|
||||||
use crate::controller::MediaType;
|
|
||||||
|
|
||||||
// FIXME: This should be in its own .css file.
|
|
||||||
static STYLE: &str = "
|
|
||||||
.audio {
|
|
||||||
background: rgb(50,100,240);
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video {
|
|
||||||
background: rgb(200,200,0);
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.midi {
|
|
||||||
background: rgb(200,0,50);
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
";
|
|
||||||
|
|
||||||
mod imp {
|
|
||||||
use super::*;
|
|
||||||
use gtk::{glib, subclass::prelude::*};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct View {
|
|
||||||
pub(super) graphview: GraphView,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
|
||||||
impl ObjectSubclass for View {
|
|
||||||
const NAME: &'static str = "HelvumApplication";
|
|
||||||
type Type = super::View;
|
|
||||||
type ParentType = gtk::Application;
|
|
||||||
|
|
||||||
fn new() -> Self {
|
|
||||||
View {
|
|
||||||
graphview: GraphView::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectImpl for View {}
|
|
||||||
impl ApplicationImpl for View {
|
|
||||||
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
|
|
||||||
.get_settings()
|
|
||||||
.set_property_gtk_application_prefer_dark_theme(true);
|
|
||||||
window.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn startup(&self, app: &Self::Type) {
|
|
||||||
self.parent_startup(app);
|
|
||||||
|
|
||||||
// Load CSS from the STYLE variable.
|
|
||||||
let provider = gtk::CssProvider::new();
|
|
||||||
provider.load_from_data(STYLE.as_bytes());
|
|
||||||
gtk::StyleContext::add_provider_for_display(
|
|
||||||
>k::gdk::Display::get_default().expect("Error initializing gtk css provider."),
|
|
||||||
&provider,
|
|
||||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl GtkApplicationImpl for View {}
|
|
||||||
}
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct View(ObjectSubclass<imp::View>)
|
|
||||||
@extends gio::Application, gtk::Application,
|
|
||||||
@implements gio::ActionGroup, gio::ActionMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl View {
|
|
||||||
/// Create the view.
|
|
||||||
/// This will set up the entire user interface and prepare it for being run.
|
|
||||||
pub(super) fn new() -> Self {
|
|
||||||
let app: View = glib::Object::new(&[("application-id", &"org.freedesktop.ryuukyu.helvum")])
|
|
||||||
.expect("Failed to create new Application");
|
|
||||||
|
|
||||||
// Add <Control-Q> shortcut for quitting the application.
|
|
||||||
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);
|
|
||||||
|
|
||||||
app
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new node to the view.
|
|
||||||
pub fn add_node(&self, id: u32, name: &str) {
|
|
||||||
let imp = imp::View::from_instance(self);
|
|
||||||
imp.graphview.add_node(id, crate::view::Node::new(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new port to the view.
|
|
||||||
pub fn add_port(
|
|
||||||
&self,
|
|
||||||
node_id: u32,
|
|
||||||
port_id: u32,
|
|
||||||
port_name: &str,
|
|
||||||
port_direction: Direction,
|
|
||||||
port_media_type: Option<MediaType>,
|
|
||||||
) {
|
|
||||||
let imp = imp::View::from_instance(self);
|
|
||||||
imp.graphview.add_port(
|
|
||||||
node_id,
|
|
||||||
port_id,
|
|
||||||
port::Port::new(port_id, port_name, port_direction, port_media_type),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new link to the view.
|
|
||||||
pub fn add_link(&self, id: u32, link: crate::PipewireLink) {
|
|
||||||
let imp = imp::View::from_instance(self);
|
|
||||||
imp.graphview.add_link(id, link);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the node with the specified id from the view.
|
|
||||||
pub fn remove_node(&self, id: u32) {
|
|
||||||
let imp = imp::View::from_instance(self);
|
|
||||||
imp.graphview.remove_node(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the port with the id `id` from the node with the id `node_id`
|
|
||||||
/// from the view.
|
|
||||||
pub fn remove_port(&self, id: u32, node_id: u32) {
|
|
||||||
let imp = imp::View::from_instance(self);
|
|
||||||
imp.graphview.remove_port(id, node_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the link with the specified id from the view.
|
|
||||||
pub fn remove_link(&self, id: u32) {
|
|
||||||
let imp = imp::View::from_instance(self);
|
|
||||||
imp.graphview.remove_link(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use gtk::{
|
|||||||
use log::warn;
|
use log::warn;
|
||||||
use pipewire::spa::Direction;
|
use pipewire::spa::Direction;
|
||||||
|
|
||||||
use crate::controller::MediaType;
|
use crate::application::MediaType;
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use once_cell::{sync::Lazy, unsync::OnceCell};
|
use once_cell::{sync::Lazy, unsync::OnceCell};
|
||||||
|
|||||||
Reference in New Issue
Block a user