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:
Tom A. Wagner
2021-05-08 17:12:04 +02:00
parent 2cc684d57c
commit be240231c0
7 changed files with 276 additions and 348 deletions

258
src/application.rs Normal file
View 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(
&gtk::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);
}
}

View File

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

View File

@@ -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<dyn std::error::Error>> {
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::<Vec<_>>());
app.run(&std::env::args().collect::<Vec<_>>());
pw_sender
.send(GtkMessage::Terminate)

View File

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

View File

@@ -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(
&gtk::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);
}
}
pub use port::Port;

View File

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