From be240231c0a83b4541f281746ea4071221f1ba8f Mon Sep 17 00:00:00 2001 From: "Tom A. Wagner" Date: Sat, 8 May 2021 17:12:04 +0200 Subject: [PATCH] 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. --- docs/architecture.md | 18 +-- src/application.rs | 258 +++++++++++++++++++++++++++++++++++++ src/controller.rs | 170 ------------------------ src/main.rs | 9 +- src/pipewire_connection.rs | 2 +- src/view/mod.rs | 165 +----------------------- src/view/port.rs | 2 +- 7 files changed, 276 insertions(+), 348 deletions(-) create mode 100644 src/application.rs delete mode 100644 src/controller.rs diff --git a/docs/architecture.md b/docs/architecture.md index 2903254..65bd4b1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,6 +10,7 @@ Helvum uses an architecture with the components laid out like this: ``` ┌──────┐ +│ GTK │ │ View │ └────┬─┘ Λ ┆ @@ -20,11 +21,10 @@ Helvum uses an architecture with the components laid out like this: │ ┆ │ ┆ │ V notifies of remote changes -┌┴───────────┐ via messages ┌─────────────────────┐ -│ │<╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ Seperate │ -│ Controller │ │ Pipewire │ -│ ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌>│ Thread │ -└┬───────────┘ Request changes to remote └─────────────────────┘ +┌┴────────────┐ via messages ┌───────────────────┐ +│ Application │<╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ Seperate │ +│ Object ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌>│ Pipewire Thread │ +└┬────────────┘ request changes to remote └───────────────────┘ │ via messages Λ │ ║ │<─── 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 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, where each message sent by one thread will trigger the loop of the other thread to invoke a callback 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. Additionally, a user may also make changes using the view. -For each change, the view notifies the controller by emitting a matching signal. -The controller will then request the pipewire connection to make those changes on the remote. \ +For each change, the view notifies the `Application` by emitting a matching signal. +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. # View Architecture diff --git a/src/application.rs b/src/application.rs new file mode 100644 index 0000000..df063de --- /dev/null +++ b/src/application.rs @@ -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, + }, + 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>, + } + + #[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) + @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) -> Self { + let app: Application = + glib::Object::new(&[("application-id", &"org.freedesktop.ryuukyu.helvum")]) + .expect("Failed to create new Application"); + + // Add 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", &["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) { + 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); + } +} diff --git a/src/controller.rs b/src/controller.rs deleted file mode 100644 index b510585..0000000 --- a/src/controller.rs +++ /dev/null @@ -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, - }, - 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, - 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, - ) -> Rc> { - 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) { - 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 - ); - } - } -} diff --git a/src/main.rs b/src/main.rs index 542edbd..a18147f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ -mod controller; +mod application; mod pipewire_connection; mod view; -use controller::MediaType; +use application::MediaType; use gtk::{ glib::{self, PRIORITY_DEFAULT}, prelude::*, @@ -56,10 +56,9 @@ fn main() -> Result<(), Box> { let pw_thread = std::thread::spawn(move || pipewire_connection::thread_main(gtk_sender, pw_receiver)); - let view = view::View::new(); - let _controller = controller::Controller::new(view.clone(), gtk_receiver); + let app = application::Application::new(gtk_receiver); - view.run(&std::env::args().collect::>()); + app.run(&std::env::args().collect::>()); pw_sender .send(GtkMessage::Terminate) diff --git a/src/pipewire_connection.rs b/src/pipewire_connection.rs index aca8d31..ff5abc9 100644 --- a/src/pipewire_connection.rs +++ b/src/pipewire_connection.rs @@ -7,7 +7,7 @@ use pipewire::{ Context, MainLoop, }; -use crate::{controller::MediaType, GtkMessage, PipewireMessage}; +use crate::{application::MediaType, GtkMessage, PipewireMessage}; /// The "main" function of the pipewire thread. pub(super) fn thread_main( diff --git a/src/view/mod.rs b/src/view/mod.rs index a0252e4..ace5133 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1,170 +1,11 @@ //! 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 node; -pub mod port; +mod port; pub use graph_view::GraphView; pub use node::Node; - -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) - @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 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", &["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, - ) { - 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); - } -} +pub use port::Port; diff --git a/src/view/port.rs b/src/view/port.rs index 3cc3c34..07d7f5c 100644 --- a/src/view/port.rs +++ b/src/view/port.rs @@ -7,7 +7,7 @@ use gtk::{ use log::warn; use pipewire::spa::Direction; -use crate::controller::MediaType; +use crate::application::MediaType; mod imp { use once_cell::{sync::Lazy, unsync::OnceCell};