diff --git a/Cargo.lock b/Cargo.lock index 2688bc2..1e3e5f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,6 +413,8 @@ dependencies = [ "anyhow", "gstreamer", "gtk4", + "log", + "once_cell", ] [[package]] @@ -542,6 +544,15 @@ version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + [[package]] name = "memoffset" version = "0.6.4" diff --git a/Cargo.toml b/Cargo.toml index 45a6a42..fa45240 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,5 @@ edition = "2018" gtk = { version = "0.3", package = "gtk4" } anyhow = "1" gstreamer = "0.16" +log = "0.4.11" +once_cell = "1.7.2" \ No newline at end of file diff --git a/TODO.md b/TODO.md index a70b13b..9cc690a 100644 --- a/TODO.md +++ b/TODO.md @@ -4,12 +4,24 @@ TODO: - [x] Create Element structure with pads and connections - [x] Get a list of GStreamer elements in dialog add plugin - [x] Add plugin details in the element dialog -- [] Draw element with its pad -- [] Be able to move the element on Screen -- [] Create connection between element +- [x] Draw element with its pad +- [x] Be able to move the element on Screen +- [x] Create connection between element +- [] Control the connection between element + - [x] unable to connect in and in out and out + - [] unable to connnec element with incompatible caps. + - [x] unable to connect a port which is already connected +- [] create contextual menu on pad or element +- [] upclass the element +- [] create a crate for graphview/node/port +- [] save/load pipeline - [] Run a pipeline with GStreamer - [] Run the pipeline with GStreamer - [] Control the pipeline with GStreamer - [x] Define the license - [] Connect the logs to the window - [] Create a window for the video output + +## Code cleanup + +[] remove useless code from graphview diff --git a/src/app.rs b/src/app.rs index 5195384..02b8d6a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,28 +16,27 @@ // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-only -use gtk::cairo::Context; use gtk::prelude::*; use gtk::{gio, glib}; use gtk::{ - AboutDialog, Application, ApplicationWindow, Builder, Button, DrawingArea, FileChooserDialog, - ResponseType, Statusbar, Viewport, + AboutDialog, Application, ApplicationWindow, Builder, Button, FileChooserDialog, ResponseType, + Statusbar, Viewport, }; use std::cell::RefCell; use std::rc::{Rc, Weak}; use std::{error, ops}; -use crate::graph::{Element, Graph}; use crate::pipeline::Pipeline; use crate::pluginlist; +use crate::graphmanager::{GraphView, Node}; + #[derive(Debug)] pub struct GPSAppInner { pub window: gtk::ApplicationWindow, - pub drawing_area: DrawingArea, + pub graphview: RefCell, pub builder: Builder, pub pipeline: RefCell, - pub graph: RefCell, } // This represents our main application window. @@ -66,13 +65,6 @@ impl GPSAppWeak { } } -fn draw_elements(elements: &Vec, c: &Context) { - for element in elements { - c.rectangle(element.position.0, element.position.1, 80.0, 45.0); - c.fill().expect("Can not draw into context"); - } -} - impl GPSApp { fn new(application: >k::Application) -> anyhow::Result> { let glade_src = include_str!("gps.ui"); @@ -82,13 +74,11 @@ impl GPSApp { window.set_title(Some("GstPipelineStudio")); window.set_size_request(800, 600); let pipeline = Pipeline::new().expect("Unable to initialize the pipeline"); - let drawing_area = DrawingArea::new(); let app = GPSApp(Rc::new(GPSAppInner { window, - drawing_area, + graphview: RefCell::new(GraphView::new()), builder, pipeline: RefCell::new(pipeline), - graph: RefCell::new(Graph::default()), })); Ok(app) } @@ -125,36 +115,15 @@ impl GPSApp { } pub fn build_ui(&self, application: &Application) { - let app_weak = self.downgrade(); - let drawing_area = gtk::DrawingArea::builder() - .content_height(24) - .content_width(24) - .build(); + //let app_weak = self.downgrade(); - drawing_area.set_draw_func(move |_, c, width, height| { - let app = upgrade_weak!(app_weak); - println!("w: {} h: {} c:{}", width, height, c); - let mut graph = app.graph.borrow_mut(); - let elements = graph.elements(); - draw_elements(&elements, c); - c.paint().expect("Invalid cairo surface state"); - }); let drawing_area_window: Viewport = self .builder .object("drawing_area") .expect("Couldn't get window"); - drawing_area_window.set_child(Some(&drawing_area)); - // let app_weak = self.downgrade(); - // event_box.connect_button_release_event(move |_w, evt| { - // let app = upgrade_weak!(app_weak, gtk::Inhibit(false)); - // let mut element: Element = Default::default(); - // element.position.0 = evt.position().0; - // element.position.1 = evt.position().1; - // app.add_new_element(element); - // app.drawing_area.queue_draw(); - // gtk::Inhibit(false) - // }); + drawing_area_window.set_child(Some(&*self.graphview.borrow())); + let window = &self.window; window.show(); @@ -214,7 +183,6 @@ impl GPSApp { .object("dialog-open-file") .expect("Couldn't get window"); open_button.connect_clicked(glib::clone!(@weak window => move |_| { - // entry.set_text("Clicked!"); open_dialog.connect_response(|dialog, _| dialog.close()); open_dialog.add_buttons(&[ ("Open", ResponseType::Ok), @@ -230,6 +198,50 @@ impl GPSApp { }); open_dialog.show(); })); + + // let add_button: Button = self + // .builder + // .object("button-play") + // .expect("Couldn't get app_button"); + // let app_weak = self.downgrade(); + // add_button.connect_clicked(glib::clone!(@weak window => move |_| { + // // entry.set_text("Clicked!"); + // let app = upgrade_weak!(app_weak); + + // })); + let add_button: Button = self + .builder + .object("button-stop") + .expect("Couldn't get app_button"); + let app_weak = self.downgrade(); + add_button.connect_clicked(glib::clone!(@weak window => move |_| { + let app = upgrade_weak!(app_weak); + let graph_view = app.graphview.borrow_mut(); + graph_view.remove_all_nodes(); + let node_id = graph_view.get_next_node_id(); + let element_name = String::from("appsink"); + let pads = Pipeline::get_pads(&element_name, false); + graph_view.add_node(node_id, Node::new(node_id, &element_name, Pipeline::get_element_type(&element_name)), pads.0, pads.1); + let node_id = graph_view.get_next_node_id(); + let element_name = String::from("videotestsrc"); + let pads = Pipeline::get_pads(&element_name, false); + graph_view.add_node(node_id, Node::new(node_id, &element_name, Pipeline::get_element_type(&element_name)), pads.0, pads.1); + let node_id = graph_view.get_next_node_id(); + let element_name = String::from("videoconvert"); + let pads = Pipeline::get_pads(&element_name, false); + graph_view.add_node(node_id, Node::new(node_id, &element_name, Pipeline::get_element_type(&element_name)), pads.0, pads.1); + + })); + let add_button: Button = self + .builder + .object("button-clear") + .expect("Couldn't get app_button"); + let app_weak = self.downgrade(); + add_button.connect_clicked(glib::clone!(@weak window => move |_| { + let app = upgrade_weak!(app_weak); + let graph_view = app.graphview.borrow_mut(); + graph_view.remove_all_nodes(); + })); } // Downgrade to a weak reference @@ -240,8 +252,19 @@ impl GPSApp { // Called when the application shuts down. We drop our app struct here fn drop(self) {} - pub fn add_new_element(&self, element: Element) { - self.graph.borrow_mut().add_element(element); - self.drawing_area.queue_draw(); + pub fn add_new_element(&self, element_name: String) { + let graph_view = self.graphview.borrow_mut(); + let node_id = graph_view.next_node_id(); + let pads = Pipeline::get_pads(&element_name, false); + graph_view.add_node( + node_id, + Node::new( + node_id, + &element_name, + Pipeline::get_element_type(&element_name), + ), + pads.0, + pads.1, + ); } } diff --git a/src/gps.ui b/src/gps.ui index bbf2c64..2af55fc 100644 --- a/src/gps.ui +++ b/src/gps.ui @@ -133,7 +133,9 @@ - + + 600 + True True True diff --git a/src/graph.rs b/src/graph.rs deleted file mode 100644 index 915430f..0000000 --- a/src/graph.rs +++ /dev/null @@ -1,36 +0,0 @@ -#[derive(Debug, Clone, Default)] -pub struct Element { - pub name: String, - pub position: (f64, f64), - pub size: (f64, f64), -} - -#[derive(Debug)] -pub struct Graph { - elements: Vec, - last_x_position: u32, -} - -impl Default for Graph { - fn default() -> Graph { - Graph { - elements: vec![], - last_x_position: 0, - } - } -} - -impl Graph { - pub fn elements(&mut self) -> &Vec { - &self.elements - } - - pub fn add_element(&mut self, element: Element) { - self.elements.push(element); - } - - pub fn remove_element(&mut self, name: &str) { - let index = self.elements.iter().position(|x| x.name == name).unwrap(); - self.elements.remove(index); - } -} diff --git a/src/graphmanager/graphview.css b/src/graphmanager/graphview.css new file mode 100644 index 0000000..bc8c5f2 --- /dev/null +++ b/src/graphmanager/graphview.css @@ -0,0 +1,13 @@ +@define-color graphview-link #808080; + +node-button { + color: black; + padding: 10px; + border-radius: 5px; + transition: all 250ms ease-in; + border: 1px transparent solid; +} + +graphview { + background: #d0d2d4; +} diff --git a/src/graphmanager/graphview.rs b/src/graphmanager/graphview.rs new file mode 100644 index 0000000..bac05ba --- /dev/null +++ b/src/graphmanager/graphview.rs @@ -0,0 +1,565 @@ +// graphview.rs +// +// Copyright 2021 Tom A. Wagner +// Copyright 2021 Stéphane Cerveau +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-only + +use super::{node::Node, port::Port, port::PortDirection}; + +use gtk::{ + glib::{self, clone}, + graphene, gsk, + prelude::*, + subclass::prelude::*, +}; +use log::{error, warn}; + +use std::cell::RefMut; +use std::{cmp::Ordering, collections::HashMap}; + +#[derive(Debug, Clone)] +pub struct NodeLink { + pub node_from: u32, + pub node_to: u32, + pub port_from: u32, + pub port_to: u32, +} + +static GRAPHVIEW_STYLE: &str = include_str!("graphview.css"); + +mod imp { + use super::*; + + use std::{ + cell::{Cell, RefCell}, + rc::Rc, + }; + + use log::warn; + + #[derive(Default)] + pub struct GraphView { + pub(super) nodes: RefCell>, + pub(super) links: RefCell>, + pub(super) current_node_id: Cell, + pub(super) current_port_id: Cell, + pub(super) current_link_id: Cell, + pub(super) port_selected: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for GraphView { + const NAME: &'static str = "GraphView"; + type Type = super::GraphView; + type ParentType = gtk::Widget; + + fn class_init(klass: &mut Self::Class) { + // The layout manager determines how child widgets are laid out. + klass.set_layout_manager_type::(); + klass.set_css_name("graphview"); + } + } + + impl ObjectImpl for GraphView { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + let drag_state = Rc::new(RefCell::new(None)); + let drag_controller = gtk::GestureDrag::new(); + + drag_controller.connect_drag_begin( + clone!(@strong drag_state => move |drag_controller, x, y| { + let mut drag_state = drag_state.borrow_mut(); + let widget = drag_controller + .widget() + .expect("drag-begin event has no widget") + .dynamic_cast::() + .expect("drag-begin event is not on the GraphView"); + // pick() should at least return the widget itself. + let target = widget.pick(x, y, gtk::PickFlags::DEFAULT).expect("drag-begin pick() did not return a widget"); + *drag_state = if target.ancestor(Port::static_type()).is_some() { + // The user targeted a port, so the dragging should be handled by the Port + // component instead of here. + None + } else if let Some(target) = target.ancestor(Node::static_type()) { + // The user targeted a Node without targeting a specific Port. + // Drag the Node around the screen. + if let Some((x, y)) = widget.node_position(&target) { + Some((target, x, y)) + } else { + error!("Failed to obtain position of dragged node, drag aborted."); + None + } + } else { + None + } + } + )); + drag_controller.connect_drag_update( + clone!(@strong drag_state => move |drag_controller, x, y| { + let widget = drag_controller + .widget() + .expect("drag-update event has no widget") + .dynamic_cast::() + .expect("drag-update event is not on the GraphView"); + let drag_state = drag_state.borrow(); + if let Some((ref node, x1, y1)) = *drag_state { + widget.move_node(node, x1 + x as f32, y1 + y as f32); + } + } + ), + ); + obj.add_controller(&drag_controller); + + let gesture = gtk::GestureClick::new(); + gesture.connect_released(clone!(@weak gesture => move |_gesture, _n_press, x, y| { + let widget = drag_controller + .widget() + .expect("click event has no widget") + .dynamic_cast::() + .expect("click event is not on the GraphView"); + if let Some(target) = widget.pick(x, y, gtk::PickFlags::DEFAULT) { + if let Some(target) = target.ancestor(Port::static_type()) { + let to_port = target.dynamic_cast::().expect("click event is not on the Port"); + if !widget.port_is_linked(&to_port) { + let selected_port = widget.selected_port().to_owned(); + if let Some(from_port) = selected_port { + println!("Port {} is clicked at {}:{}", to_port.id(), x, y); + if widget.ports_compatible(&to_port) { + let from_node = from_port.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::().expect("Unable to cast to Node"); + let to_node = to_port.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::().expect("Unable to cast to Node"); + println!("add link"); + widget.add_link(widget.get_next_link_id(), NodeLink { + node_from: from_node.id(), + node_to: to_node.id(), + port_from: from_port.id(), + port_to: to_port.id() + }, true); + } + widget.set_selected_port(None); + } else { + println!("add selected port id"); + widget.set_selected_port(Some(&to_port)); + } + } + } + } + })); + obj.add_controller(&gesture); + } + + fn dispose(&self, _obj: &Self::Type) { + self.nodes + .borrow() + .values() + .for_each(|node| node.unparent()) + } + } + + impl WidgetImpl for GraphView { + fn snapshot(&self, widget: &Self::Type, snapshot: >k::Snapshot) { + /* FIXME: A lot of hardcoded values in here. + Try to use relative units (em) and colours from the theme as much as possible. */ + + let alloc = widget.allocation(); + + // Draw all children + self.nodes + .borrow() + .values() + .for_each(|node| self.instance().snapshot_child(node, snapshot)); + + // Draw all links + let link_cr = snapshot + .append_cairo(&graphene::Rect::new( + 0.0, + 0.0, + alloc.width as f32, + alloc.height as f32, + )) + .expect("Failed to get cairo context"); + + link_cr.set_line_width(1.5); + + for (link, active) in self.links.borrow().values() { + if let Some((from_x, from_y, to_x, to_y)) = self.get_link_coordinates(link) { + //println!("from_x: {} from_y: {} to_x: {} to_y: {}", from_x, from_y, to_x, to_y); + + // Use dashed line for inactive links, full line otherwise. + if *active { + link_cr.set_dash(&[], 0.0); + } else { + link_cr.set_dash(&[10.0, 5.0], 0.0); + } + + link_cr.move_to(from_x, from_y); + link_cr.line_to(to_x, to_y); + link_cr.set_line_width(2.0); + + if let Err(e) = link_cr.stroke() { + warn!("Failed to draw graphview links: {}", e); + }; + } else { + warn!("Could not get allocation of ports of link: {:?}", link); + } + } + } + } + + impl GraphView { + /// Get coordinates for the drawn link to start at and to end at. + /// + /// # Returns + /// `Some((from_x, from_y, to_x, to_y))` if all objects the links refers to exist as widgets. + fn get_link_coordinates(&self, link: &NodeLink) -> Option<(f64, f64, f64, f64)> { + let nodes = self.nodes.borrow(); + + // For some reason, gtk4::WidgetExt::translate_coordinates gives me incorrect values, + // so we manually calculate the needed offsets here. + + let from_port = &nodes.get(&link.node_from)?.get_port(link.port_from)?; + let gtk::Allocation { + x: mut fx, + y: mut fy, + width: fw, + height: fh, + } = from_port.allocation(); + + let from_node = from_port + .ancestor(Node::static_type()) + .expect("Port is not a child of a node"); + let gtk::Allocation { x: fnx, y: fny, .. } = from_node.allocation(); + fx += fnx + (fw / 2); + fy += fny + (fh / 2); + + let to_port = &nodes.get(&link.node_to)?.get_port(link.port_to)?; + let gtk::Allocation { + x: mut tx, + y: mut ty, + width: tw, + height: th, + .. + } = to_port.allocation(); + let to_node = to_port + .ancestor(Node::static_type()) + .expect("Port is not a child of a node"); + let gtk::Allocation { x: tnx, y: tny, .. } = to_node.allocation(); + tx += tnx + (tw / 2); + ty += tny + (th / 2); + + Some((fx.into(), fy.into(), tx.into(), ty.into())) + } + } +} + +glib::wrapper! { + pub struct GraphView(ObjectSubclass) + @extends gtk::Widget; +} + +impl GraphView { + pub fn new() -> Self { + // Load CSS from the STYLE variable. + let provider = gtk::CssProvider::new(); + provider.load_from_data(GRAPHVIEW_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, + ); + glib::Object::new(&[]).expect("Failed to create GraphView") + } + + pub fn add_node(&self, id: u32, node: Node, input: u32, output: u32) { + let private = imp::GraphView::from_instance(self); + node.set_parent(self); + + // Place widgets in colums of 3, growing down + // let x = if let Some(node_type) = node_type { + // match node_type { + // NodeType::Src => 20.0, + // NodeType::Transform => 420.0, + // NodeType::Sink => 820.0, + // } + // } else { + // 420.0 + // }; + let x = 20.0; + let y = private + .nodes + .borrow() + .values() + .filter_map(|node| { + // Map nodes to locations, discard nodes without location + self.node_position(&node.clone().upcast()) + }) + .filter(|(x2, _)| { + // Only look for other nodes that have a similar x coordinate + (x - x2).abs() < 50.0 + }) + .max_by(|y1, y2| { + // Get max in column + y1.partial_cmp(y2).unwrap_or(Ordering::Equal) + }) + .map_or(20_f32, |(_x, y)| y + 100.0); + + self.move_node(&node.clone().upcast(), x, y); + + private.nodes.borrow_mut().insert(id, node); + let _i = 0; + for _i in 0..input { + let port_id = self.next_port_id(); + let port = Port::new(port_id, "in", PortDirection::Input); + self.add_port(id, port_id, port); + } + + let _i = 0; + for _i in 0..output { + let port_id = self.next_port_id(); + let port = Port::new(port_id, "out", PortDirection::Output); + self.add_port(id, port_id, port); + } + } + + pub fn remove_node(&self, id: u32) { + let private = imp::GraphView::from_instance(self); + let mut nodes = private.nodes.borrow_mut(); + if let Some(node) = nodes.remove(&id) { + node.unparent(); + } else { + warn!("Tried to remove non-existant node (id={}) from graph", id); + } + self.queue_draw(); + } + pub fn all_nodes(&self) -> Vec { + let private = imp::GraphView::from_instance(self); + let nodes = private.nodes.borrow_mut(); + let nodes_list: Vec<_> = nodes.iter().map(|(_, node)| node.clone()).collect(); + nodes_list + } + + pub fn remove_all_nodes(&self) { + let private = imp::GraphView::from_instance(self); + let nodes_list = self.all_nodes(); + for node in nodes_list { + if let Some(link_id) = self.node_is_linked(node.id()) { + let mut links = private.links.borrow_mut(); + links.remove(&link_id); + } + self.remove_node(node.id()); + } + private.current_node_id.set(0); + private.current_port_id.set(0); + private.current_link_id.set(0); + self.queue_draw(); + } + + pub fn add_port(&self, node_id: u32, port_id: u32, port: Port) { + let private = imp::GraphView::from_instance(self); + println!( + "adding a port with port id {} to node id {}", + port_id, node_id + ); + if let Some(node) = private.nodes.borrow_mut().get_mut(&node_id) { + node.add_port(port_id, port); + } else { + error!( + "Node with id {} not found when trying to add port with id {} to graph", + node_id, port_id + ); + } + } + + 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); + } + } + + pub fn add_link(&self, link_id: u32, link: NodeLink, active: bool) { + let private = imp::GraphView::from_instance(self); + if !self.link_exists(&link) { + private.links.borrow_mut().insert(link_id, (link, active)); + self.queue_draw(); + } + } + + pub fn set_link_state(&self, link_id: u32, active: bool) { + let private = imp::GraphView::from_instance(self); + if let Some((_, state)) = private.links.borrow_mut().get_mut(&link_id) { + *state = active; + self.queue_draw(); + } else { + warn!("Link state changed on unknown link (id={})", link_id); + } + } + + pub fn remove_link(&self, id: u32) { + let private = imp::GraphView::from_instance(self); + let mut links = private.links.borrow_mut(); + links.remove(&id); + + self.queue_draw(); + } + + pub fn node_is_linked(&self, node_id: u32) -> Option { + let private = imp::GraphView::from_instance(self); + let links = private.links.borrow_mut(); + for (key, value) in &*links { + if value.0.node_from == node_id || value.0.node_to == node_id { + return Some(*key); + } + } + None + } + + /// Get the position of the specified node inside the graphview. + /// + /// Returns `None` if the node is not in the graphview. + pub(super) fn node_position(&self, node: >k::Widget) -> Option<(f32, f32)> { + let layout_manager = self + .layout_manager() + .expect("Failed to get layout manager") + .dynamic_cast::() + .expect("Failed to cast to FixedLayout"); + + let node = layout_manager + .layout_child(node)? + .dynamic_cast::() + .expect("Could not cast to FixedLayoutChild"); + let transform = node + .transform() + .expect("Failed to obtain transform from layout child"); + Some(transform.to_translate()) + } + + pub(super) fn move_node(&self, node: >k::Widget, x: f32, y: f32) { + let layout_manager = self + .layout_manager() + .expect("Failed to get layout manager") + .dynamic_cast::() + .expect("Failed to cast to FixedLayout"); + + let transform = gsk::Transform::new() + // Nodes should not be able to be dragged out of the view, so we use `max(coordinate, 0.0)` to prevent that. + .translate(&graphene::Point::new(f32::max(x, 0.0), f32::max(y, 0.0))) + .unwrap(); + + layout_manager + .layout_child(node) + .expect("Could not get layout child") + .dynamic_cast::() + .expect("Could not cast to FixedLayoutChild") + .set_transform(&transform); + + // FIXME: If links become proper widgets, + // we don't need to redraw the full graph everytime. + self.queue_draw(); + } + + pub(super) fn link_exists(&self, new_link: &NodeLink) -> bool { + let private = imp::GraphView::from_instance(self); + + for (link, _active) in private.links.borrow().values() { + if (new_link.port_from == link.port_from && new_link.port_to == link.port_to) + || (new_link.port_to == link.port_from && new_link.port_from == link.port_to) + { + println!("link already existing"); + return true; + } + } + false + } + + pub(super) fn port_is_linked(&self, port: &Port) -> bool { + let private = imp::GraphView::from_instance(self); + + for (id, (link, _active)) in private.links.borrow().iter() { + if port.id() == link.port_from || port.id() == link.port_to { + println!("port {} is already linked {}", port.id(), id); + return true; + } + } + return false; + } + + pub(super) fn ports_compatible(&self, to_port: &Port) -> bool { + let current_port = self.selected_port().to_owned(); + if let Some(from_port) = current_port { + let from_node = from_port + .ancestor(Node::static_type()) + .expect("Unable to reach parent") + .dynamic_cast::() + .expect("Unable to cast to Node"); + let to_node = to_port + .ancestor(Node::static_type()) + .expect("Unable to reach parent") + .dynamic_cast::() + .expect("Unable to cast to Node"); + let res = from_port.id() != to_port.id() + && from_port.direction() != to_port.direction() + && from_node.id() != to_node.id(); + if !res { + println!("Unable add the following link"); + } + return res; + } + false + } + + pub fn next_node_id(&self) -> u32 { + let private = imp::GraphView::from_instance(self); + private + .current_node_id + .set(private.current_node_id.get() + 1); + private.current_node_id.get() + } + + pub fn next_port_id(&self) -> u32 { + let private = imp::GraphView::from_instance(self); + private + .current_port_id + .set(private.current_port_id.get() + 1); + private.current_port_id.get() + } + + fn get_next_link_id(&self) -> u32 { + let private = imp::GraphView::from_instance(self); + private + .current_link_id + .set(private.current_link_id.get() + 1); + private.current_link_id.get() + } + + fn set_selected_port(&self, port: Option<&Port>) { + let private = imp::GraphView::from_instance(self); + *private.port_selected.borrow_mut() = port.cloned(); + } + + fn selected_port(&self) -> RefMut> { + let private = imp::GraphView::from_instance(self); + private.port_selected.borrow_mut() + } +} + +impl Default for GraphView { + fn default() -> Self { + Self::new() + } +} diff --git a/src/graphmanager/mod.rs b/src/graphmanager/mod.rs new file mode 100644 index 0000000..910f655 --- /dev/null +++ b/src/graphmanager/mod.rs @@ -0,0 +1,9 @@ +mod graphview; +mod node; +mod port; + +pub use graphview::GraphView; +pub use node::Node; +pub use node::NodeType; +pub use port::Port; +pub use port::PortDirection; diff --git a/src/graphmanager/node.rs b/src/graphmanager/node.rs new file mode 100644 index 0000000..6288e38 --- /dev/null +++ b/src/graphmanager/node.rs @@ -0,0 +1,158 @@ +// node.rs +// +// Copyright 2021 Tom A. Wagner +// Copyright 2021 Stéphane Cerveau +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-only +use gtk::glib; +use gtk::prelude::*; +use gtk::subclass::prelude::*; + +use super::Port; +use super::PortDirection; + +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; + +pub enum NodeType { + Source, + Transform, + Sink, + Unknown, +} + +mod imp { + use super::*; + use once_cell::unsync::OnceCell; + pub struct Node { + pub(super) grid: gtk::Grid, + pub(super) label: gtk::Label, + pub(super) id: OnceCell, + pub(super) node_type: OnceCell, + pub(super) ports: RefCell>, + pub(super) num_ports_in: Cell, + pub(super) num_ports_out: Cell, + } + + #[glib::object_subclass] + impl ObjectSubclass for Node { + const NAME: &'static str = "Node"; + type Type = super::Node; + type ParentType = gtk::Widget; + + fn class_init(klass: &mut Self::Class) { + klass.set_layout_manager_type::(); + klass.set_css_name("button"); + } + + fn new() -> Self { + let grid = gtk::Grid::new(); + let label = gtk::Label::new(None); + + grid.attach(&label, 0, 0, 2, 1); + + // Display a grab cursor when the mouse is over the label so the user knows the node can be dragged. + label.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref()); + + Self { + grid, + label, + id: OnceCell::new(), + node_type: OnceCell::new(), + ports: RefCell::new(HashMap::new()), + num_ports_in: Cell::new(0), + num_ports_out: Cell::new(0), + } + } + } + + impl ObjectImpl for Node { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + self.grid.set_parent(obj); + } + + fn dispose(&self, _obj: &Self::Type) { + self.grid.unparent(); + } + } + + impl WidgetImpl for Node {} +} + +glib::wrapper! { + pub struct Node(ObjectSubclass) + @extends gtk::Widget, gtk::Box; +} + +impl Node { + pub fn new(id: u32, name: &str, node_type: NodeType) -> Self { + let res: Self = glib::Object::new(&[]).expect("Failed to create Node"); + let private = imp::Node::from_instance(&res); + private.id.set(id).expect("Node id already set"); + res.set_name(name); + private.node_type.set(node_type); + res + } + + pub fn set_name(&self, name: &str) { + let self_ = imp::Node::from_instance(self); + self_.label.set_text(name); + println!("{}", name); + } + + pub fn add_port(&mut self, id: u32, port: super::port::Port) { + let private = imp::Node::from_instance(self); + + match port.direction() { + PortDirection::Input => { + private + .grid + .attach(&port, 0, private.num_ports_in.get() + 1, 1, 1); + private.num_ports_in.set(private.num_ports_in.get() + 1); + } + PortDirection::Output => { + private + .grid + .attach(&port, 1, private.num_ports_out.get() + 1, 1, 1); + private.num_ports_out.set(private.num_ports_out.get() + 1); + } + } + + private.ports.borrow_mut().insert(id, port); + } + + pub fn get_port(&self, id: u32) -> Option { + let private = imp::Node::from_instance(self); + private.ports.borrow_mut().get(&id).cloned() + } + + pub fn remove_port(&self, id: u32) { + let private = imp::Node::from_instance(self); + if let Some(port) = private.ports.borrow_mut().remove(&id) { + match port.direction() { + PortDirection::Input => private.num_ports_in.set(private.num_ports_in.get() - 1), + PortDirection::Output => private.num_ports_in.set(private.num_ports_out.get() - 1), + } + + port.unparent(); + } + } + pub fn id(&self) -> u32 { + let private = imp::Node::from_instance(self); + private.id.get().copied().expect("Node id is not set") + } +} diff --git a/src/graphmanager/node.ui b/src/graphmanager/node.ui new file mode 100644 index 0000000..3aaafcb --- /dev/null +++ b/src/graphmanager/node.ui @@ -0,0 +1,15 @@ + + + + diff --git a/src/graphmanager/port.rs b/src/graphmanager/port.rs new file mode 100644 index 0000000..9307508 --- /dev/null +++ b/src/graphmanager/port.rs @@ -0,0 +1,122 @@ +// port.rs +// +// Copyright 2021 Tom A. Wagner +// Copyright 2021 Stéphane Cerveau +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-only +use gtk::{ + glib::{self, subclass::Signal}, + prelude::*, + subclass::prelude::*, +}; + +#[derive(Debug, Clone, PartialEq)] +pub enum PortDirection { + Input, + Output, +} + +mod imp { + use super::*; + use once_cell::{sync::Lazy, unsync::OnceCell}; + + /// Graphical representation of a pipewire port. + #[derive(Default, Clone)] + pub struct Port { + pub(super) label: OnceCell, + pub(super) id: OnceCell, + pub(super) direction: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for Port { + const NAME: &'static str = "Port"; + type Type = super::Port; + type ParentType = gtk::Widget; + + fn class_init(klass: &mut Self::Class) { + klass.set_layout_manager_type::(); + + // Make it look like a GTK button. + klass.set_css_name("button"); + } + } + + impl ObjectImpl for Port { + fn dispose(&self, _obj: &Self::Type) { + if let Some(label) = self.label.get() { + label.unparent() + } + } + + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![Signal::builder( + "port-toggled", + // Provide id of output port and input port to signal handler. + &[::static_type().into(), ::static_type().into()], + // signal handler sends back nothing. + <()>::static_type().into(), + ) + .build()] + }); + + SIGNALS.as_ref() + } + } + impl WidgetImpl for Port {} +} + +glib::wrapper! { + pub struct Port(ObjectSubclass) + @extends gtk::Widget, gtk::Box; +} + +impl Port { + pub fn new(id: u32, name: &str, direction: PortDirection) -> Self { + // Create the widget and initialize needed fields + let res: Self = glib::Object::new(&[]).expect("Failed to create Port"); + + let private = imp::Port::from_instance(&res); + private.id.set(id).expect("Port id already set"); + private + .direction + .set(direction) + .expect("Port direction already set"); + + let label = gtk::Label::new(Some(name)); + label.set_parent(&res); + private + .label + .set(label) + .expect("Port label was already set"); + + // Display a grab cursor when the mouse is over the port so the user knows it can be dragged to another port. + res.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref()); + + res + } + + pub fn id(&self) -> u32 { + let private = imp::Port::from_instance(self); + private.id.get().copied().expect("Port id is not set") + } + + pub fn direction(&self) -> &PortDirection { + let private = imp::Port::from_instance(self); + private.direction.get().expect("Port direction is not set") + } +} diff --git a/src/graphmanager/port.ui b/src/graphmanager/port.ui new file mode 100644 index 0000000..a5c7ebe --- /dev/null +++ b/src/graphmanager/port.ui @@ -0,0 +1,15 @@ + + + + diff --git a/src/main.rs b/src/main.rs index c6301bd..dcdf771 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ #[macro_use] mod macros; mod app; -mod graph; +mod graphmanager; mod pipeline; mod pluginlist; diff --git a/src/pipeline.rs b/src/pipeline.rs index 9e42d82..e050d47 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -16,6 +16,7 @@ // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-only +use crate::graphmanager::NodeType; use gst::prelude::*; use gstreamer as gst; use std::error; @@ -76,10 +77,10 @@ impl Pipeline { elements.sort(); Ok(elements) } - pub fn element_description( + + pub fn element_feature( element_name: &str, - ) -> anyhow::Result> { - let mut desc = String::from(""); + ) -> anyhow::Result> { let registry = gst::Registry::get(); let feature = gst::Registry::find_feature( ®istry, @@ -87,6 +88,14 @@ impl Pipeline { gst::ElementFactory::static_type(), ) .expect("Unable to find the element name"); + Ok(feature) + } + + pub fn element_description( + element_name: &str, + ) -> anyhow::Result> { + let mut desc = String::from(""); + let feature = Pipeline::element_feature(element_name)?; if let Ok(factory) = feature.downcast::() { desc.push_str("Factory details:\n"); @@ -143,4 +152,46 @@ impl Pipeline { } Ok(desc) } + + pub fn get_pads(element_name: &str, include_on_request: bool) -> (u32, u32) { + let feature = Pipeline::element_feature(element_name).expect("Unable to get feature"); + let mut input = 0; + let mut output = 0; + + if let Ok(factory) = feature.downcast::() { + if factory.get_num_pad_templates() > 0 { + let pads = factory.get_static_pad_templates(); + for pad in pads { + if pad.presence() == gst::PadPresence::Always + || (include_on_request + && (pad.presence() == gst::PadPresence::Request + || pad.presence() == gst::PadPresence::Sometimes)) + { + if pad.direction() == gst::PadDirection::Src { + output += 1; + } else if pad.direction() == gst::PadDirection::Sink { + input += 1; + } + } + } + } + } + (input, output) + } + + pub fn get_element_type(element_name: &str) -> NodeType { + let pads = Pipeline::get_pads(element_name, true); + let mut element_type = NodeType::Source; + if pads.0 > 0 { + if pads.1 > 0 { + element_type = NodeType::Transform; + } else { + element_type = NodeType::Sink; + } + } else if pads.1 > 0 { + element_type = NodeType::Source; + } + + element_type + } } diff --git a/src/pluginlist.rs b/src/pluginlist.rs index 522f195..1c0059d 100644 --- a/src/pluginlist.rs +++ b/src/pluginlist.rs @@ -17,12 +17,11 @@ // // SPDX-License-Identifier: GPL-3.0-only use crate::app::GPSApp; -use crate::graph::Element; use crate::pipeline::ElementInfo; use crate::pipeline::Pipeline; +use gtk::glib; use gtk::prelude::*; use gtk::TextBuffer; -use gtk::{gio, glib}; use gtk::{CellRendererText, Dialog, ListStore, TextView, TreeView, TreeViewColumn}; @@ -103,22 +102,13 @@ pub fn display_plugin_list(app: &GPSApp, elements: &[ElementInfo]) { // Now getting back the values from the row corresponding to the // iterator `iter`. // - let element = Element { - name: model - .get(&iter, 1) - .get::() - .expect("Treeview selection, column 1"), - position: (100.0,100.0), - size: (100.0,100.0), - }; let element_name = model .get(&iter, 1) .get::() .expect("Treeview selection, column 1"); - app.add_new_element(element); - println!("{}", element_name); + app.add_new_element(element_name); } }), );