mirror of
https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio.git
synced 2024-11-22 09:00:59 +00:00
GPS: introduce the graphmanager
Introduce the first version of a graph manager with: - Graphview - Node - Port
This commit is contained in:
parent
25b2d1f8bf
commit
5f91fbaef7
16 changed files with 1052 additions and 100 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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
18
TODO.md
|
@ -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
|
||||||
|
|
111
src/app.rs
111
src/app.rs
|
@ -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: >k::Application) -> anyhow::Result<GPSApp, Box<dyn error::Error>> {
|
fn new(application: >k::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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
36
src/graph.rs
36
src/graph.rs
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
13
src/graphmanager/graphview.css
Normal file
13
src/graphmanager/graphview.css
Normal 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;
|
||||||
|
}
|
565
src/graphmanager/graphview.rs
Normal file
565
src/graphmanager/graphview.rs
Normal 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: >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<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(
|
||||||
|
>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<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: >k::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: >k::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
9
src/graphmanager/mod.rs
Normal 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
158
src/graphmanager/node.rs
Normal 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
15
src/graphmanager/node.ui
Normal 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
122
src/graphmanager/port.rs
Normal 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
15
src/graphmanager/port.ui
Normal 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>
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
®istry,
|
®istry,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue