GPS: introduce the graphmanager

Introduce the first version of a graph manager
with:
- Graphview
- Node
- Port
This commit is contained in:
Stéphane Cerveau 2021-11-25 16:13:40 +01:00
parent 25b2d1f8bf
commit 5f91fbaef7
16 changed files with 1052 additions and 100 deletions

11
Cargo.lock generated
View file

@ -413,6 +413,8 @@ dependencies = [
"anyhow", "anyhow",
"gstreamer", "gstreamer",
"gtk4", "gtk4",
"log",
"once_cell",
] ]
[[package]] [[package]]
@ -542,6 +544,15 @@ version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" 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]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.4" version = "0.6.4"

View file

@ -9,3 +9,5 @@ edition = "2018"
gtk = { version = "0.3", package = "gtk4" } gtk = { version = "0.3", package = "gtk4" }
anyhow = "1" anyhow = "1"
gstreamer = "0.16" gstreamer = "0.16"
log = "0.4.11"
once_cell = "1.7.2"

18
TODO.md
View file

@ -4,12 +4,24 @@ TODO:
- [x] Create Element structure with pads and connections - [x] Create Element structure with pads and connections
- [x] Get a list of GStreamer elements in dialog add plugin - [x] Get a list of GStreamer elements in dialog add plugin
- [x] Add plugin details in the element dialog - [x] Add plugin details in the element dialog
- [] Draw element with its pad - [x] Draw element with its pad
- [] Be able to move the element on Screen - [x] Be able to move the element on Screen
- [] Create connection between element - [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 a pipeline with GStreamer
- [] Run the pipeline with GStreamer - [] Run the pipeline with GStreamer
- [] Control the pipeline with GStreamer - [] Control the pipeline with GStreamer
- [x] Define the license - [x] Define the license
- [] Connect the logs to the window - [] Connect the logs to the window
- [] Create a window for the video output - [] Create a window for the video output
## Code cleanup
[] remove useless code from graphview

View file

@ -16,28 +16,27 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// //
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use gtk::cairo::Context;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{gio, glib}; use gtk::{gio, glib};
use gtk::{ use gtk::{
AboutDialog, Application, ApplicationWindow, Builder, Button, DrawingArea, FileChooserDialog, AboutDialog, Application, ApplicationWindow, Builder, Button, FileChooserDialog, ResponseType,
ResponseType, Statusbar, Viewport, Statusbar, Viewport,
}; };
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::{Rc, Weak}; use std::rc::{Rc, Weak};
use std::{error, ops}; use std::{error, ops};
use crate::graph::{Element, Graph};
use crate::pipeline::Pipeline; use crate::pipeline::Pipeline;
use crate::pluginlist; use crate::pluginlist;
use crate::graphmanager::{GraphView, Node};
#[derive(Debug)] #[derive(Debug)]
pub struct GPSAppInner { pub struct GPSAppInner {
pub window: gtk::ApplicationWindow, pub window: gtk::ApplicationWindow,
pub drawing_area: DrawingArea, pub graphview: RefCell<GraphView>,
pub builder: Builder, pub builder: Builder,
pub pipeline: RefCell<Pipeline>, pub pipeline: RefCell<Pipeline>,
pub graph: RefCell<Graph>,
} }
// This represents our main application window. // This represents our main application window.
@ -66,13 +65,6 @@ impl GPSAppWeak {
} }
} }
fn draw_elements(elements: &Vec<Element>, 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 { impl GPSApp {
fn new(application: &gtk::Application) -> anyhow::Result<GPSApp, Box<dyn error::Error>> { fn new(application: &gtk::Application) -> anyhow::Result<GPSApp, Box<dyn error::Error>> {
let glade_src = include_str!("gps.ui"); let glade_src = include_str!("gps.ui");
@ -82,13 +74,11 @@ impl GPSApp {
window.set_title(Some("GstPipelineStudio")); window.set_title(Some("GstPipelineStudio"));
window.set_size_request(800, 600); window.set_size_request(800, 600);
let pipeline = Pipeline::new().expect("Unable to initialize the pipeline"); let pipeline = Pipeline::new().expect("Unable to initialize the pipeline");
let drawing_area = DrawingArea::new();
let app = GPSApp(Rc::new(GPSAppInner { let app = GPSApp(Rc::new(GPSAppInner {
window, window,
drawing_area, graphview: RefCell::new(GraphView::new()),
builder, builder,
pipeline: RefCell::new(pipeline), pipeline: RefCell::new(pipeline),
graph: RefCell::new(Graph::default()),
})); }));
Ok(app) Ok(app)
} }
@ -125,36 +115,15 @@ impl GPSApp {
} }
pub fn build_ui(&self, application: &Application) { pub fn build_ui(&self, application: &Application) {
let app_weak = self.downgrade(); //let app_weak = self.downgrade();
let drawing_area = gtk::DrawingArea::builder()
.content_height(24)
.content_width(24)
.build();
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 let drawing_area_window: Viewport = self
.builder .builder
.object("drawing_area") .object("drawing_area")
.expect("Couldn't get window"); .expect("Couldn't get window");
drawing_area_window.set_child(Some(&drawing_area));
// let app_weak = self.downgrade(); drawing_area_window.set_child(Some(&*self.graphview.borrow()));
// 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)
// });
let window = &self.window; let window = &self.window;
window.show(); window.show();
@ -214,7 +183,6 @@ impl GPSApp {
.object("dialog-open-file") .object("dialog-open-file")
.expect("Couldn't get window"); .expect("Couldn't get window");
open_button.connect_clicked(glib::clone!(@weak window => move |_| { open_button.connect_clicked(glib::clone!(@weak window => move |_| {
// entry.set_text("Clicked!");
open_dialog.connect_response(|dialog, _| dialog.close()); open_dialog.connect_response(|dialog, _| dialog.close());
open_dialog.add_buttons(&[ open_dialog.add_buttons(&[
("Open", ResponseType::Ok), ("Open", ResponseType::Ok),
@ -230,6 +198,50 @@ impl GPSApp {
}); });
open_dialog.show(); 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 // Downgrade to a weak reference
@ -240,8 +252,19 @@ impl GPSApp {
// Called when the application shuts down. We drop our app struct here // Called when the application shuts down. We drop our app struct here
fn drop(self) {} fn drop(self) {}
pub fn add_new_element(&self, element: Element) { pub fn add_new_element(&self, element_name: String) {
self.graph.borrow_mut().add_element(element); let graph_view = self.graphview.borrow_mut();
self.drawing_area.queue_draw(); 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,
);
} }
} }

View file

@ -133,7 +133,9 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkPaned">
<property name="position">600</property>
<property name="position-set">True</property>
<property name="hexpand">True</property> <property name="hexpand">True</property>
<property name="vexpand">True</property> <property name="vexpand">True</property>
<child> <child>

View file

@ -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<Element>,
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<Element> {
&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);
}
}

View file

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

View file

@ -0,0 +1,565 @@
// graphview.rs
//
// Copyright 2021 Tom A. Wagner <tom.a.wagner@protonmail.com>
// Copyright 2021 Stéphane Cerveau <scerveau@collabora.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// 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<HashMap<u32, Node>>,
pub(super) links: RefCell<HashMap<u32, (NodeLink, bool)>>,
pub(super) current_node_id: Cell<u32>,
pub(super) current_port_id: Cell<u32>,
pub(super) current_link_id: Cell<u32>,
pub(super) port_selected: RefCell<Option<Port>>,
}
#[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::<gtk::FixedLayout>();
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::<Self::Type>()
.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::<Self::Type>()
.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::<Self::Type>()
.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::<Port>().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::<Node>().expect("Unable to cast to Node");
let to_node = to_port.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::<Node>().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: &gtk::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<imp::GraphView>)
@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(
&gtk::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<Node> {
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<u32> {
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: &gtk::Widget) -> Option<(f32, f32)> {
let layout_manager = self
.layout_manager()
.expect("Failed to get layout manager")
.dynamic_cast::<gtk::FixedLayout>()
.expect("Failed to cast to FixedLayout");
let node = layout_manager
.layout_child(node)?
.dynamic_cast::<gtk::FixedLayoutChild>()
.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: &gtk::Widget, x: f32, y: f32) {
let layout_manager = self
.layout_manager()
.expect("Failed to get layout manager")
.dynamic_cast::<gtk::FixedLayout>()
.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::<gtk::FixedLayoutChild>()
.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::<Node>()
.expect("Unable to cast to Node");
let to_node = to_port
.ancestor(Node::static_type())
.expect("Unable to reach parent")
.dynamic_cast::<Node>()
.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<Option<Port>> {
let private = imp::GraphView::from_instance(self);
private.port_selected.borrow_mut()
}
}
impl Default for GraphView {
fn default() -> Self {
Self::new()
}
}

9
src/graphmanager/mod.rs Normal file
View file

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

158
src/graphmanager/node.rs Normal file
View file

@ -0,0 +1,158 @@
// node.rs
//
// Copyright 2021 Tom A. Wagner <tom.a.wagner@protonmail.com>
// Copyright 2021 Stéphane Cerveau <scerveau@collabora.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// 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<u32>,
pub(super) node_type: OnceCell<NodeType>,
pub(super) ports: RefCell<HashMap<u32, Port>>,
pub(super) num_ports_in: Cell<i32>,
pub(super) num_ports_out: Cell<i32>,
}
#[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::<gtk::BinLayout>();
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<imp::Node>)
@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<super::port::Port> {
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")
}
}

15
src/graphmanager/node.ui Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="Node">
<child>
<object class="GtkBox" id="box_">
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="label">
<property name="name">Node</property>
</object>
</child>
</object>
</child>
</template>
</interface>

122
src/graphmanager/port.rs Normal file
View file

@ -0,0 +1,122 @@
// port.rs
//
// Copyright 2021 Tom A. Wagner <tom.a.wagner@protonmail.com>
// Copyright 2021 Stéphane Cerveau <scerveau@collabora.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// 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<gtk::Label>,
pub(super) id: OnceCell<u32>,
pub(super) direction: OnceCell<PortDirection>,
}
#[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::<gtk::BinLayout>();
// 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<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder(
"port-toggled",
// Provide id of output port and input port to signal handler.
&[<u32>::static_type().into(), <u32>::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<imp::Port>)
@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")
}
}

15
src/graphmanager/port.ui Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="Port">
<child>
<object class="GtkBox" id="box_">
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="name">Some Text</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -20,7 +20,7 @@
#[macro_use] #[macro_use]
mod macros; mod macros;
mod app; mod app;
mod graph; mod graphmanager;
mod pipeline; mod pipeline;
mod pluginlist; mod pluginlist;

View file

@ -16,6 +16,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// //
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use crate::graphmanager::NodeType;
use gst::prelude::*; use gst::prelude::*;
use gstreamer as gst; use gstreamer as gst;
use std::error; use std::error;
@ -76,10 +77,10 @@ impl Pipeline {
elements.sort(); elements.sort();
Ok(elements) Ok(elements)
} }
pub fn element_description(
pub fn element_feature(
element_name: &str, element_name: &str,
) -> anyhow::Result<String, Box<dyn error::Error>> { ) -> anyhow::Result<gst::PluginFeature, Box<dyn error::Error>> {
let mut desc = String::from("");
let registry = gst::Registry::get(); let registry = gst::Registry::get();
let feature = gst::Registry::find_feature( let feature = gst::Registry::find_feature(
&registry, &registry,
@ -87,6 +88,14 @@ impl Pipeline {
gst::ElementFactory::static_type(), gst::ElementFactory::static_type(),
) )
.expect("Unable to find the element name"); .expect("Unable to find the element name");
Ok(feature)
}
pub fn element_description(
element_name: &str,
) -> anyhow::Result<String, Box<dyn error::Error>> {
let mut desc = String::from("");
let feature = Pipeline::element_feature(element_name)?;
if let Ok(factory) = feature.downcast::<gst::ElementFactory>() { if let Ok(factory) = feature.downcast::<gst::ElementFactory>() {
desc.push_str("<b>Factory details:</b>\n"); desc.push_str("<b>Factory details:</b>\n");
@ -143,4 +152,46 @@ impl Pipeline {
} }
Ok(desc) 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::<gst::ElementFactory>() {
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
}
} }

View file

@ -17,12 +17,11 @@
// //
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use crate::app::GPSApp; use crate::app::GPSApp;
use crate::graph::Element;
use crate::pipeline::ElementInfo; use crate::pipeline::ElementInfo;
use crate::pipeline::Pipeline; use crate::pipeline::Pipeline;
use gtk::glib;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::TextBuffer; use gtk::TextBuffer;
use gtk::{gio, glib};
use gtk::{CellRendererText, Dialog, ListStore, TextView, TreeView, TreeViewColumn}; 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 // Now getting back the values from the row corresponding to the
// iterator `iter`. // iterator `iter`.
// //
let element = Element {
name: model
.get(&iter, 1)
.get::<String>()
.expect("Treeview selection, column 1"),
position: (100.0,100.0),
size: (100.0,100.0),
};
let element_name = model let element_name = model
.get(&iter, 1) .get(&iter, 1)
.get::<String>() .get::<String>()
.expect("Treeview selection, column 1"); .expect("Treeview selection, column 1");
app.add_new_element(element);
println!("{}", element_name); println!("{}", element_name);
app.add_new_element(element_name);
} }
}), }),
); );