From 2cb155c5ee74f87508c20edc530ff8965d364191 Mon Sep 17 00:00:00 2001 From: "Tom A. Wagner" Date: Tue, 30 Mar 2021 19:57:05 +0200 Subject: [PATCH] view: Refactor view to have a manager `View` struct. The view struct creates, manages and runs the view, and handles all communication with components outside of the view. --- src/controller.rs | 78 +++++++++++--------------- src/main.rs | 64 ++------------------- src/view/graph_view.rs | 14 ++++- src/view/mod.rs | 124 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 108 deletions(-) diff --git a/src/controller.rs b/src/controller.rs index 1ff9de2..9d54d96 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -19,7 +19,7 @@ pub enum MediaType { enum Item { Node { // Keep track of the widget to easily remove ports on it later. - widget: view::Node, + // widget: view::Node, // Keep track of the nodes media type to color ports on it. media_type: Option, }, @@ -43,7 +43,7 @@ enum Item { pub struct Controller { con: Rc>, state: HashMap, - view: view::GraphView, + view: Rc, } impl Controller { @@ -55,7 +55,7 @@ impl Controller { /// 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::GraphView, + view: Rc, con: Rc>, ) -> Rc> { let result = Rc::new(RefCell::new(Controller { @@ -107,21 +107,19 @@ impl Controller { fn add_node(&mut self, node: &GlobalObject) { info!("Adding node to graph: id {}", node.id); - // Update graph to contain the new node. - let node_widget = crate::view::Node::new( - &node - .props - .as_ref() - .map(|dict| { - String::from( - dict.get("node.nick") - .or_else(|| dict.get("node.description")) - .or_else(|| dict.get("node.name")) - .unwrap_or_default(), - ) - }) - .unwrap_or_default(), - ); + // Get the nicest possible name for the node, using a fallback chain of possible name attributes. + let node_name = &node + .props + .as_ref() + .map(|dict| { + String::from( + dict.get("node.nick") + .or_else(|| dict.get("node.description")) + .or_else(|| dict.get("node.name")) + .unwrap_or_default(), + ) + }) + .unwrap_or_default(); // FIXME: This relies on the node being passed to us by the pipwire server before its port. let media_type = node @@ -143,12 +141,12 @@ impl Controller { .flatten() .flatten(); - self.view.add_node(node.id, node_widget.clone()); + self.view.add_node(node.id, node_name); self.state.insert( node.id, Item::Node { - widget: node_widget, + // widget: node_widget, media_type, }, ); @@ -178,7 +176,8 @@ impl Controller { None }; - let new_port = crate::view::port::Port::new( + self.view.add_port( + node_id, port.id, &port_label, if matches!(props.get("port.direction"), Some("in")) { @@ -189,8 +188,6 @@ impl Controller { media_type, ); - self.view.add_port_to_node(node_id, new_port.id, new_port); - // Save node_id so we can delete this port easily. self.state.insert(port.id, Item::Port { node_id }); } @@ -246,9 +243,18 @@ impl Controller { fn global_remove(&mut self, id: u32) { if let Some(item) = self.state.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), + 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!( @@ -257,24 +263,4 @@ impl Controller { ); } } - - fn remove_node(&self, id: u32) { - info!("Removing node from graph: id {}", id); - - self.view.remove_node(id); - } - - fn remove_port(&self, id: u32, node_id: u32) { - info!("Removing port from graph: id {}, node_id: {}", id, node_id); - - if let Some(Item::Node { widget, .. }) = self.state.get(&node_id) { - widget.remove_port(id); - } - } - - fn remove_link(&self, id: u32) { - info!("Removing link from graph: id {}", id); - - self.view.remove_link(id); - } } diff --git a/src/main.rs b/src/main.rs index 780edee..6642b54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,26 +2,9 @@ mod controller; mod pipewire_connection; mod view; -use gtk::glib::{self, clone}; -use gtk::prelude::*; +use std::rc::Rc; -// 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; -} -"; +use gtk::{glib, prelude::*}; #[derive(Debug)] pub struct PipewireLink { @@ -35,10 +18,9 @@ fn main() -> Result<(), Box> { env_logger::init(); gtk::init()?; - let graphview = view::GraphView::new(); - + let view = Rc::new(view::View::new()); let pw_con = pipewire_connection::PipewireConnection::new()?; - let _controller = controller::Controller::new(graphview.clone(), pw_con.clone()); + let _controller = controller::Controller::new(view.clone(), pw_con.clone()); // Do an initial roundtrip before showing the view, // so that the graph is already populated when the window opens. @@ -49,43 +31,7 @@ fn main() -> Result<(), Box> { Continue(true) }); - let app = gtk::Application::new(Some("org.freedesktop.ryuukyu.helvum"), Default::default()) - .expect("Application creation failed"); - - app.connect_startup(|_| { - // 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, - ); - }); - - app.connect_activate(move |app| { - let scrollwindow = gtk::ScrolledWindowBuilder::new().child(&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(); - }); - - 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.run(&std::env::args().collect::>()); + view.run(); Ok(()) } diff --git a/src/view/graph_view.rs b/src/view/graph_view.rs index b107770..d18c456 100644 --- a/src/view/graph_view.rs +++ b/src/view/graph_view.rs @@ -202,7 +202,7 @@ impl GraphView { } } - pub fn add_port_to_node(&self, node_id: u32, port_id: u32, port: crate::view::port::Port) { + pub fn add_port(&self, node_id: u32, port_id: u32, port: crate::view::port::Port) { let private = imp::GraphView::from_instance(self); if let Some(node) = private.nodes.borrow_mut().get_mut(&node_id) { @@ -217,6 +217,14 @@ impl GraphView { } } + pub fn remove_port(&self, id: u32, node_id: u32) { + let private = imp::GraphView::from_instance(self); + let nodes = private.nodes.borrow(); + if let Some(node) = nodes.get(&node_id) { + node.remove_port(id); + } + } + /// Add a link to the graph. /// /// `add_link` takes three arguments: `link_id` is the id of the link as assigned by the pipewire server, @@ -235,11 +243,11 @@ impl GraphView { self.queue_draw(); } - pub fn set_dragged(&self, widget: Option) { + pub(super) fn set_dragged(&self, widget: Option) { *imp::GraphView::from_instance(self).dragged.borrow_mut() = widget; } - pub fn move_node(&self, node: >k::Widget, x: f32, y: f32) { + pub(super) fn move_node(&self, node: >k::Widget, x: f32, y: f32) { let layout_manager = self .get_layout_manager() .expect("Failed to get layout manager") diff --git a/src/view/mod.rs b/src/view/mod.rs index 1635aef..d6bfb57 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1,6 +1,130 @@ +//! The view presented to the user. +//! +//! This module contains gtk widgets and helper struct needed to present the graphical user interface. + mod graph_view; mod node; pub mod port; pub use graph_view::GraphView; pub use node::Node; + +use gtk::{ + glib::{self, clone}, + prelude::*, +}; +use pipewire::port::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; +} +"; + +/// Manager struct of the view. +/// +/// This struct is responsible for setting up the UI, as well as communication with other components outside the view. +pub struct View { + app: gtk::Application, + graphview: GraphView, +} + +impl View { + /// Create the view. + /// This will set up the entire user interface and prepare it for being run. + /// + /// To show and run the interface, its [`run`](`Self::run`) method will need to be called. + pub(super) fn new() -> Self { + let graphview = GraphView::new(); + + let app = gtk::Application::new(Some("org.freedesktop.ryuukyu.helvum"), Default::default()) + .expect("Application creation failed"); + + app.connect_startup(|_| { + // 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, + ); + }); + + app.connect_activate(clone!(@strong graphview => move |app| { + let scrollwindow = gtk::ScrolledWindowBuilder::new().child(&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(); + })); + + 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); + + Self { app, graphview } + } + + /// Run the view. This will enter a gtk event loop and remain in that until the application is quit by the user. + pub(super) fn run(&self) -> i32 { + self.app.run(&std::env::args().collect::>()) + } + + pub fn add_node(&self, id: u32, name: &str) { + let node = crate::view::Node::new(name); + self.graphview.add_node(id, node); + } + + pub fn add_port( + &self, + node_id: u32, + port_id: u32, + port_name: &str, + port_direction: Direction, + port_media_type: Option, + ) { + let port = port::Port::new(port_id, port_name, port_direction, port_media_type); + self.graphview.add_port(node_id, port_id, port) + } + + pub fn add_link(&self, id: u32, link: crate::PipewireLink) { + self.graphview.add_link(id, link); + } + + pub fn remove_node(&self, id: u32) { + self.graphview.remove_node(id); + } + + pub fn remove_port(&self, id: u32, node_id: u32) { + self.graphview.remove_port(id, node_id); + } + + pub fn remove_link(&self, id: u32) { + self.graphview.remove_link(id); + } +}