mirror of
https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio.git
synced 2024-11-25 02:21:00 +00:00
GPS: Add xml save/load in graphview
Can now save and load in XML graphs/nodes/ports/links
This commit is contained in:
parent
eb771a74f4
commit
fe93db3458
8 changed files with 465 additions and 131 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -415,6 +415,7 @@ dependencies = [
|
|||
"gtk4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -965,3 +966,9 @@ name = "winapi-x86_64-pc-windows-gnu"
|
|||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
||||
|
|
|
@ -10,4 +10,5 @@ gtk = { version = "0.3", package = "gtk4" }
|
|||
anyhow = "1"
|
||||
gstreamer = "0.16"
|
||||
log = "0.4.11"
|
||||
once_cell = "1.7.2"
|
||||
once_cell = "1.7.2"
|
||||
xml-rs = "0.8.4"
|
3
TODO.md
3
TODO.md
|
@ -14,13 +14,14 @@ TODO:
|
|||
- [] create contextual menu on pad or element
|
||||
- [] upclass the element
|
||||
- [] create a crate for graphview/node/port
|
||||
- [] save/load pipeline
|
||||
- [x] save/load pipeline
|
||||
- [] Run a pipeline with GStreamer
|
||||
- [] Run the pipeline with GStreamer
|
||||
- [] Control the pipeline with GStreamer
|
||||
- [x] Define the license
|
||||
- [] Connect the logs to the window
|
||||
- [] Create a window for the video output
|
||||
- [] Add multiple graphviews with tabs.
|
||||
|
||||
## Code cleanup
|
||||
|
||||
|
|
123
src/app.rs
123
src/app.rs
|
@ -19,8 +19,8 @@
|
|||
use gtk::prelude::*;
|
||||
use gtk::{gio, glib};
|
||||
use gtk::{
|
||||
AboutDialog, Application, ApplicationWindow, Builder, Button, FileChooserDialog, ResponseType,
|
||||
Statusbar, Viewport,
|
||||
AboutDialog, Application, ApplicationWindow, Builder, Button, FileChooserAction,
|
||||
FileChooserDialog, ResponseType, Statusbar, Viewport,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::{Rc, Weak};
|
||||
|
@ -115,8 +115,6 @@ impl GPSApp {
|
|||
}
|
||||
|
||||
pub fn build_ui(&self, application: &Application) {
|
||||
//let app_weak = self.downgrade();
|
||||
|
||||
let drawing_area_window: Viewport = self
|
||||
.builder
|
||||
.object("drawing_area")
|
||||
|
@ -133,6 +131,63 @@ impl GPSApp {
|
|||
.expect("Couldn't get window");
|
||||
status_bar.push(status_bar.context_id("Description"), "GPS is ready");
|
||||
|
||||
let action = gio::SimpleAction::new("open", None);
|
||||
let app_weak = self.downgrade();
|
||||
// Add a dialog to open the graph
|
||||
action.connect_activate(glib::clone!(@weak window => move |_, _| {
|
||||
let app = upgrade_weak!(app_weak);
|
||||
let file_chooser = FileChooserDialog::new(
|
||||
Some("Open File"),
|
||||
Some(&window),
|
||||
FileChooserAction::Open,
|
||||
&[("Open", ResponseType::Ok), ("Cancel", ResponseType::Cancel)],
|
||||
);
|
||||
file_chooser.connect_response(move |d: &FileChooserDialog, response: ResponseType| {
|
||||
if response == ResponseType::Ok {
|
||||
let file = d.file().expect("Couldn't get file");
|
||||
let filename = String::from(file.path().expect("Couldn't get file path").to_str().expect("unable to convert to string"));
|
||||
println!("Open file {}", filename);
|
||||
app.load_graph(&filename).expect("Unable to open file");
|
||||
}
|
||||
|
||||
d.close();
|
||||
});
|
||||
|
||||
file_chooser.show();
|
||||
|
||||
}));
|
||||
application.add_action(&action);
|
||||
application.set_accels_for_action("app.open", &["<primary>o"]);
|
||||
|
||||
// Add a dialog to save the graph
|
||||
let action = gio::SimpleAction::new("save_as", None);
|
||||
let app_weak = self.downgrade();
|
||||
action.connect_activate(glib::clone!(@weak window => move |_, _| {
|
||||
let app = upgrade_weak!(app_weak);
|
||||
let file_chooser = FileChooserDialog::new(
|
||||
Some("Save File"),
|
||||
Some(&window),
|
||||
FileChooserAction::Open,
|
||||
&[("Save", ResponseType::Ok), ("Cancel", ResponseType::Cancel)],
|
||||
);
|
||||
file_chooser.connect_response(move |d: &FileChooserDialog, response: ResponseType| {
|
||||
if response == ResponseType::Ok {
|
||||
let file = d.file().expect("Couldn't get file");
|
||||
let filename = String::from(file.path().expect("Couldn't get file path").to_str().expect("unable to convert to string"));
|
||||
println!("Save file {}", filename);
|
||||
app.save_graph(&filename).expect("Unable to save file");
|
||||
}
|
||||
|
||||
d.close();
|
||||
});
|
||||
|
||||
file_chooser.show();
|
||||
|
||||
}));
|
||||
|
||||
application.add_action(&action);
|
||||
application.set_accels_for_action("app.save", &["<primary>s"]);
|
||||
|
||||
let action = gio::SimpleAction::new("quit", None);
|
||||
action.connect_activate({
|
||||
let app = application.downgrade();
|
||||
|
@ -173,42 +228,17 @@ impl GPSApp {
|
|||
let elements = Pipeline::elements_list().expect("Unable to obtain element's list");
|
||||
pluginlist::display_plugin_list(&app, &elements);
|
||||
}));
|
||||
// Create a dialog to open a file
|
||||
let open_button: Button = self
|
||||
|
||||
let add_button: Button = self
|
||||
.builder
|
||||
.object("button-open-file")
|
||||
.object("button-play")
|
||||
.expect("Couldn't get app_button");
|
||||
let open_dialog: FileChooserDialog = self
|
||||
.builder
|
||||
.object("dialog-open-file")
|
||||
.expect("Couldn't get window");
|
||||
open_button.connect_clicked(glib::clone!(@weak window => move |_| {
|
||||
open_dialog.connect_response(|dialog, _| dialog.close());
|
||||
open_dialog.add_buttons(&[
|
||||
("Open", ResponseType::Ok),
|
||||
("Cancel", ResponseType::Cancel)
|
||||
]);
|
||||
let app_weak = self.downgrade();
|
||||
add_button.connect_clicked(glib::clone!(@weak window => move |_| {
|
||||
// entry.set_text("Clicked!");
|
||||
let _app = upgrade_weak!(app_weak);
|
||||
|
||||
open_dialog.connect_response(|open_dialog, response| {
|
||||
if response == ResponseType::Ok {
|
||||
let file = open_dialog.file().expect("Couldn't get file");
|
||||
println!("Files: {:?}", file);
|
||||
}
|
||||
open_dialog.close();
|
||||
});
|
||||
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")
|
||||
|
@ -221,15 +251,15 @@ impl GPSApp {
|
|||
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);
|
||||
graph_view.add_node_with_port(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);
|
||||
graph_view.add_node_with_port(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);
|
||||
graph_view.add_node_with_port(node_id, Node::new(node_id, &element_name, Pipeline::get_element_type(&element_name)), pads.0, pads.1);
|
||||
|
||||
}));
|
||||
let add_button: Button = self
|
||||
|
@ -256,7 +286,7 @@ impl GPSApp {
|
|||
let graph_view = self.graphview.borrow_mut();
|
||||
let node_id = graph_view.next_node_id();
|
||||
let pads = Pipeline::get_pads(&element_name, false);
|
||||
graph_view.add_node(
|
||||
graph_view.add_node_with_port(
|
||||
node_id,
|
||||
Node::new(
|
||||
node_id,
|
||||
|
@ -267,4 +297,17 @@ impl GPSApp {
|
|||
pads.1,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn save_graph(&self, filename: &str) -> anyhow::Result<(), Box<dyn error::Error>> {
|
||||
let graph_view = self.graphview.borrow_mut();
|
||||
graph_view.render_xml(filename)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_graph(&self, filename: &str) -> anyhow::Result<(), Box<dyn error::Error>> {
|
||||
let graph_view = self.graphview.borrow_mut();
|
||||
graph_view.remove_all_nodes();
|
||||
graph_view.load_xml(filename)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
34
src/gps.ui
34
src/gps.ui
|
@ -8,6 +8,16 @@
|
|||
<attribute name="action">app.new-window</attribute>
|
||||
<attribute name="accel"><primary>n</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes" comments="Primary menu entry that opens the graph">_Open</attribute>
|
||||
<attribute name="action">app.open</attribute>
|
||||
<attribute name="accel"><primary>n</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes" comments="Primary menu entry that saves the graph">_Save As</attribute>
|
||||
<attribute name="action">app.save_as</attribute>
|
||||
<attribute name="accel"><primary>n</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes" comments="Primary menu entry that opens the About dialog.">_About GstPipelineStudio</attribute>
|
||||
<attribute name="action">app.about</attribute>
|
||||
|
@ -59,13 +69,17 @@
|
|||
<property name="default-height">600</property>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar" id="header-bar">
|
||||
<property name="hexpand">0</property>
|
||||
<property name="show-title-buttons">True</property>
|
||||
<child type="start">
|
||||
<object class="GtkButton">
|
||||
<property name="action-name">win.open</property>
|
||||
<property name="label">Open</property>
|
||||
</object>
|
||||
<object class="GtkMenuButton" id="gear_menu_button">
|
||||
<property name="valign">3</property>
|
||||
<property name="focus-on-click">0</property>
|
||||
<property name="popover">
|
||||
<object class="GtkPopoverMenu" id="gear_menu">
|
||||
<property name="menu-model">primary_menu</property>
|
||||
</object>
|
||||
</property>
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
@ -86,14 +100,6 @@
|
|||
<property name="icon-name">list-add</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="button-open-file">
|
||||
<property name="hexpand">1</property>
|
||||
<property name="label">gtk-open</property>
|
||||
<property name="receives-default">1</property>
|
||||
<property name="icon-name">document-open-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="button-play">
|
||||
<property name="hexpand">1</property>
|
||||
|
|
|
@ -18,7 +18,15 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::{node::Node, port::Port, port::PortDirection};
|
||||
extern crate xml;
|
||||
use xml::reader::EventReader;
|
||||
use xml::reader::XmlEvent as XMLREvent;
|
||||
use xml::writer::EmitterConfig;
|
||||
use xml::writer::XmlEvent as XMLWEvent;
|
||||
|
||||
use super::{node::Node, node::NodeType, port::Port, port::PortDirection};
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
use gtk::{
|
||||
glib::{self, clone},
|
||||
|
@ -29,14 +37,15 @@ use gtk::{
|
|||
use log::{error, warn};
|
||||
|
||||
use std::cell::RefMut;
|
||||
use std::{cmp::Ordering, collections::HashMap};
|
||||
|
||||
use std::{cmp::Ordering, collections::HashMap, error};
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NodeLink {
|
||||
pub id: u32,
|
||||
pub node_from: u32,
|
||||
pub node_to: u32,
|
||||
pub port_from: u32,
|
||||
pub port_to: u32,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
static GRAPHVIEW_STYLE: &str = include_str!("graphview.css");
|
||||
|
@ -54,7 +63,7 @@ mod imp {
|
|||
#[derive(Default)]
|
||||
pub struct GraphView {
|
||||
pub(super) nodes: RefCell<HashMap<u32, Node>>,
|
||||
pub(super) links: RefCell<HashMap<u32, (NodeLink, bool)>>,
|
||||
pub(super) links: RefCell<HashMap<u32, NodeLink>>,
|
||||
pub(super) current_node_id: Cell<u32>,
|
||||
pub(super) current_port_id: Cell<u32>,
|
||||
pub(super) current_link_id: Cell<u32>,
|
||||
|
@ -132,24 +141,27 @@ mod imp {
|
|||
.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);
|
||||
}
|
||||
|
||||
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 let None = widget.port_is_linked(to_port.id()) {
|
||||
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(NodeLink {
|
||||
id: widget.next_link_id(),
|
||||
node_from: from_node.id(),
|
||||
node_to: to_node.id(),
|
||||
port_from: from_port.id(),
|
||||
port_to: to_port.id(),
|
||||
active: true
|
||||
} );
|
||||
}
|
||||
widget.set_selected_port(None);
|
||||
} else {
|
||||
println!("add selected port id");
|
||||
|
@ -195,12 +207,12 @@ mod imp {
|
|||
|
||||
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) {
|
||||
for link in self.links.borrow().values() {
|
||||
if let Some((from_x, from_y, to_x, to_y)) = self.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 {
|
||||
if link.active {
|
||||
link_cr.set_dash(&[], 0.0);
|
||||
} else {
|
||||
link_cr.set_dash(&[10.0, 5.0], 0.0);
|
||||
|
@ -225,13 +237,13 @@ mod imp {
|
|||
///
|
||||
/// # 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)> {
|
||||
fn 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 from_port = &nodes.get(&link.node_from)?.port(&link.port_from)?;
|
||||
let gtk::Allocation {
|
||||
x: mut fx,
|
||||
y: mut fy,
|
||||
|
@ -246,7 +258,7 @@ mod imp {
|
|||
fx += fnx + (fw / 2);
|
||||
fy += fny + (fh / 2);
|
||||
|
||||
let to_port = &nodes.get(&link.node_to)?.get_port(link.port_to)?;
|
||||
let to_port = &nodes.get(&link.node_to)?.port(&link.port_to)?;
|
||||
let gtk::Allocation {
|
||||
x: mut tx,
|
||||
y: mut ty,
|
||||
|
@ -284,21 +296,22 @@ impl GraphView {
|
|||
glib::Object::new(&[]).expect("Failed to create GraphView")
|
||||
}
|
||||
|
||||
pub fn add_node(&self, id: u32, node: Node, input: u32, output: u32) {
|
||||
pub fn add_node_with_port(&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 x = if let Some(node_type) = node.node_type() {
|
||||
match node_type {
|
||||
NodeType::Source => 20.0,
|
||||
NodeType::Transform => 320.0,
|
||||
NodeType::Sink => 620.0,
|
||||
_ => 20.0,
|
||||
}
|
||||
} else {
|
||||
420.0
|
||||
};
|
||||
|
||||
let y = private
|
||||
.nodes
|
||||
.borrow()
|
||||
|
@ -335,6 +348,10 @@ impl GraphView {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_node(&self, id: u32, node: Node) {
|
||||
self.add_node_with_port(id, node, 0, 0);
|
||||
}
|
||||
|
||||
pub fn remove_node(&self, id: u32) {
|
||||
let private = imp::GraphView::from_instance(self);
|
||||
let mut nodes = private.nodes.borrow_mut();
|
||||
|
@ -347,7 +364,7 @@ impl GraphView {
|
|||
}
|
||||
pub fn all_nodes(&self) -> Vec<Node> {
|
||||
let private = imp::GraphView::from_instance(self);
|
||||
let nodes = private.nodes.borrow_mut();
|
||||
let nodes = private.nodes.borrow();
|
||||
let nodes_list: Vec<_> = nodes.iter().map(|(_, node)| node.clone()).collect();
|
||||
nodes_list
|
||||
}
|
||||
|
@ -368,6 +385,17 @@ impl GraphView {
|
|||
self.queue_draw();
|
||||
}
|
||||
|
||||
pub fn node_is_linked(&self, node_id: u32) -> Option<u32> {
|
||||
let private = imp::GraphView::from_instance(self);
|
||||
for (key, link) in private.links.borrow().iter() {
|
||||
if link.node_from == node_id || link.node_to == node_id {
|
||||
return Some(*key);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Port related methods
|
||||
pub fn add_port(&self, node_id: u32, port_id: u32, port: Port) {
|
||||
let private = imp::GraphView::from_instance(self);
|
||||
println!(
|
||||
|
@ -392,18 +420,37 @@ impl GraphView {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_link(&self, link_id: u32, link: NodeLink, active: bool) {
|
||||
pub fn port_is_linked(&self, port_id: u32) -> Option<(u32, u32, u32)> {
|
||||
let private = imp::GraphView::from_instance(self);
|
||||
for (key, link) in private.links.borrow().iter() {
|
||||
if link.port_from == port_id || link.port_to == port_id {
|
||||
return Some((*key, link.node_from, link.port_from));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Link related methods
|
||||
|
||||
pub fn all_links(&self) -> Vec<NodeLink> {
|
||||
let private = imp::GraphView::from_instance(self);
|
||||
let links = private.links.borrow();
|
||||
let links_list: Vec<_> = links.iter().map(|(_, link)| link.clone()).collect();
|
||||
links_list
|
||||
}
|
||||
|
||||
pub fn add_link(&self, link: NodeLink) {
|
||||
let private = imp::GraphView::from_instance(self);
|
||||
if !self.link_exists(&link) {
|
||||
private.links.borrow_mut().insert(link_id, (link, active));
|
||||
private.links.borrow_mut().insert(link.id, link);
|
||||
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;
|
||||
if let Some(link) = private.links.borrow_mut().get_mut(&link_id) {
|
||||
link.active = active;
|
||||
self.queue_draw();
|
||||
} else {
|
||||
warn!("Link state changed on unknown link (id={})", link_id);
|
||||
|
@ -418,17 +465,6 @@ impl GraphView {
|
|||
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.
|
||||
|
@ -476,7 +512,7 @@ impl GraphView {
|
|||
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() {
|
||||
for link 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)
|
||||
{
|
||||
|
@ -487,18 +523,6 @@ impl GraphView {
|
|||
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 {
|
||||
|
@ -539,7 +563,7 @@ impl GraphView {
|
|||
private.current_port_id.get()
|
||||
}
|
||||
|
||||
fn get_next_link_id(&self) -> u32 {
|
||||
fn next_link_id(&self) -> u32 {
|
||||
let private = imp::GraphView::from_instance(self);
|
||||
private
|
||||
.current_link_id
|
||||
|
@ -556,6 +580,181 @@ impl GraphView {
|
|||
let private = imp::GraphView::from_instance(self);
|
||||
private.port_selected.borrow_mut()
|
||||
}
|
||||
|
||||
// Render graph methods
|
||||
pub fn render_xml(&self, filename: &str) -> anyhow::Result<(), Box<dyn error::Error>> {
|
||||
let mut file = File::create(filename).unwrap();
|
||||
let mut writer = EmitterConfig::new()
|
||||
.perform_indent(true)
|
||||
.create_writer(&mut file);
|
||||
|
||||
writer.write(XMLWEvent::start_element("Graph"))?;
|
||||
|
||||
//Get the nodes
|
||||
let nodes = self.all_nodes();
|
||||
for node in nodes {
|
||||
writer.write(
|
||||
XMLWEvent::start_element("Node")
|
||||
.attr("name", &node.name())
|
||||
.attr("id", &node.id().to_string())
|
||||
.attr("type", &node.node_type().unwrap().to_string()),
|
||||
)?;
|
||||
for port in node.ports().values() {
|
||||
writer.write(
|
||||
XMLWEvent::start_element("Port")
|
||||
.attr("name", &port.name())
|
||||
.attr("id", &port.id().to_string())
|
||||
.attr("direction", &port.direction().to_string()),
|
||||
)?;
|
||||
writer.write(XMLWEvent::end_element())?;
|
||||
}
|
||||
writer.write(XMLWEvent::end_element())?;
|
||||
}
|
||||
//Get the link and write it.
|
||||
for link in self.all_links() {
|
||||
writer.write(
|
||||
XMLWEvent::start_element("Link")
|
||||
.attr("id", &link.id.to_string())
|
||||
.attr("node_from", &link.node_from.to_string())
|
||||
.attr("node_to", &link.node_to.to_string())
|
||||
.attr("port_from", &link.port_from.to_string())
|
||||
.attr("port_to", &link.port_to.to_string())
|
||||
.attr("active", &link.active.to_string()),
|
||||
)?;
|
||||
writer.write(XMLWEvent::end_element())?;
|
||||
}
|
||||
writer.write(XMLWEvent::end_element())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_xml(&self, filename: &str) -> anyhow::Result<(), Box<dyn error::Error>> {
|
||||
let file = File::open(filename).unwrap();
|
||||
let file = BufReader::new(file);
|
||||
|
||||
let parser = EventReader::new(file);
|
||||
|
||||
let mut current_node: Option<Node> = None;
|
||||
let mut current_port: Option<Port> = None;
|
||||
let mut current_link: Option<NodeLink> = None;
|
||||
for e in parser {
|
||||
match e {
|
||||
Ok(XMLREvent::StartElement {
|
||||
ref name,
|
||||
ref attributes,
|
||||
..
|
||||
}) => {
|
||||
println!("{}", name);
|
||||
let mut attrs = HashMap::new();
|
||||
attributes.iter().for_each(|a| {
|
||||
attrs.insert(a.name.to_string(), a.value.to_string());
|
||||
});
|
||||
match name.to_string().as_str() {
|
||||
"Graph" => {
|
||||
println!("New graph detected");
|
||||
}
|
||||
"Node" => {
|
||||
let id = attrs
|
||||
.get::<String>(&String::from("id"))
|
||||
.expect("Unable to find node id");
|
||||
let name = attrs
|
||||
.get::<String>(&String::from("name"))
|
||||
.expect("Unable to find node name");
|
||||
let node_type: &String = attrs
|
||||
.get::<String>(&String::from("type"))
|
||||
.expect("Unable to find node type");
|
||||
|
||||
current_node = Some(Node::new(
|
||||
id.parse::<u32>().unwrap(),
|
||||
name,
|
||||
NodeType::from_str(node_type.as_str()),
|
||||
));
|
||||
}
|
||||
"Port" => {
|
||||
let id = attrs
|
||||
.get::<String>(&String::from("id"))
|
||||
.expect("Unable to find port id");
|
||||
let name = attrs
|
||||
.get::<String>(&String::from("name"))
|
||||
.expect("Unable to find port name");
|
||||
let direction: &String = attrs
|
||||
.get::<String>(&String::from("direction"))
|
||||
.expect("Unable to find port direction");
|
||||
current_port = Some(Port::new(
|
||||
id.parse::<u32>().unwrap(),
|
||||
name,
|
||||
PortDirection::from_str(direction),
|
||||
));
|
||||
}
|
||||
"Link" => {
|
||||
let id = attrs
|
||||
.get::<String>(&String::from("id"))
|
||||
.expect("Unable to find link id");
|
||||
let node_from = attrs
|
||||
.get::<String>(&String::from("node_from"))
|
||||
.expect("Unable to find link node_from");
|
||||
let node_to = attrs
|
||||
.get::<String>(&String::from("node_to"))
|
||||
.expect("Unable to find link node_to");
|
||||
let port_from = attrs
|
||||
.get::<String>(&String::from("port_from"))
|
||||
.expect("Unable to find link port_from");
|
||||
let port_to = attrs
|
||||
.get::<String>(&String::from("port_to"))
|
||||
.expect("Unable to find link port_to");
|
||||
let active: &String = attrs
|
||||
.get::<String>(&String::from("active"))
|
||||
.expect("Unable to find link state");
|
||||
current_link = Some(NodeLink {
|
||||
id: id.parse::<u32>().unwrap(),
|
||||
node_from: node_from.parse::<u32>().unwrap(),
|
||||
node_to: node_to.parse::<u32>().unwrap(),
|
||||
port_from: port_from.parse::<u32>().unwrap(),
|
||||
port_to: port_to.parse::<u32>().unwrap(),
|
||||
active: active.parse::<bool>().unwrap(),
|
||||
});
|
||||
}
|
||||
_ => println!("name unknown: {}", name),
|
||||
}
|
||||
}
|
||||
Ok(XMLREvent::EndElement { name }) => {
|
||||
println!("closing {}", name);
|
||||
match name.to_string().as_str() {
|
||||
"Graph" => {
|
||||
println!("Graph ended");
|
||||
}
|
||||
"Node" => {
|
||||
if let Some(node) = current_node {
|
||||
let id = node.id();
|
||||
self.add_node(id, node);
|
||||
}
|
||||
current_node = None;
|
||||
}
|
||||
"Port" => {
|
||||
if let Some(port) = current_port {
|
||||
let node = current_node.clone();
|
||||
node.expect("No current node, error...")
|
||||
.add_port(port.id(), port);
|
||||
}
|
||||
current_port = None;
|
||||
}
|
||||
"Link" => {
|
||||
if let Some(link) = current_link {
|
||||
self.add_link(link);
|
||||
}
|
||||
current_link = None;
|
||||
}
|
||||
_ => println!("name unknown: {}", name),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GraphView {
|
||||
|
|
|
@ -24,16 +24,38 @@ use gtk::subclass::prelude::*;
|
|||
use super::Port;
|
||||
use super::PortDirection;
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cell::{Cell, Ref, RefCell};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum NodeType {
|
||||
Source,
|
||||
Transform,
|
||||
Sink,
|
||||
All,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl fmt::Display for NodeType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeType {
|
||||
pub fn from_str(node_type_name: &str) -> NodeType {
|
||||
match node_type_name {
|
||||
"Source" => NodeType::Source,
|
||||
"Transform" => NodeType::Transform,
|
||||
"Sink" => NodeType::Sink,
|
||||
"All" => NodeType::All,
|
||||
_ => NodeType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
use once_cell::unsync::OnceCell;
|
||||
|
@ -104,11 +126,14 @@ impl 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);
|
||||
private
|
||||
.node_type
|
||||
.set(node_type)
|
||||
.expect("Node type is already set");
|
||||
res
|
||||
}
|
||||
|
||||
pub fn set_name(&self, name: &str) {
|
||||
fn set_name(&self, name: &str) {
|
||||
let self_ = imp::Node::from_instance(self);
|
||||
self_.label.set_text(name);
|
||||
println!("{}", name);
|
||||
|
@ -130,14 +155,19 @@ impl Node {
|
|||
.attach(&port, 1, private.num_ports_out.get() + 1, 1, 1);
|
||||
private.num_ports_out.set(private.num_ports_out.get() + 1);
|
||||
}
|
||||
_ => panic!("Port without direction"),
|
||||
}
|
||||
|
||||
private.ports.borrow_mut().insert(id, port);
|
||||
}
|
||||
|
||||
pub fn get_port(&self, id: u32) -> Option<super::port::Port> {
|
||||
pub fn ports(&self) -> Ref<HashMap<u32, Port>> {
|
||||
let private = imp::Node::from_instance(self);
|
||||
private.ports.borrow_mut().get(&id).cloned()
|
||||
private.ports.borrow()
|
||||
}
|
||||
pub fn port(&self, id: &u32) -> Option<super::port::Port> {
|
||||
let private = imp::Node::from_instance(self);
|
||||
private.ports.borrow().get(id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove_port(&self, id: u32) {
|
||||
|
@ -146,13 +176,31 @@ impl Node {
|
|||
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),
|
||||
_ => panic!("Port without direction"),
|
||||
}
|
||||
|
||||
port.unparent();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
let private = imp::Node::from_instance(self);
|
||||
private.id.get().copied().expect("Node id is not set")
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
let private = imp::Node::from_instance(self);
|
||||
private.label.text().to_string()
|
||||
}
|
||||
|
||||
pub fn unique_name(&self) -> String {
|
||||
let private = imp::Node::from_instance(self);
|
||||
let mut unique_name = private.label.text().to_string();
|
||||
unique_name.push_str(&self.id().to_string());
|
||||
unique_name
|
||||
}
|
||||
|
||||
pub fn node_type(&self) -> Option<&NodeType> {
|
||||
let private = imp::Node::from_instance(self);
|
||||
private.node_type.get()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,31 @@ use gtk::{
|
|||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PortDirection {
|
||||
Input,
|
||||
Output,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl fmt::Display for PortDirection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
// or, alternatively:
|
||||
// fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PortDirection {
|
||||
pub fn from_str(port_direction_name: &str) -> PortDirection {
|
||||
match port_direction_name {
|
||||
"Input" => PortDirection::Input,
|
||||
"Output" => PortDirection::Output,
|
||||
_ => PortDirection::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
|
@ -119,4 +139,13 @@ impl Port {
|
|||
let private = imp::Port::from_instance(self);
|
||||
private.direction.get().expect("Port direction is not set")
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
let private = imp::Port::from_instance(self);
|
||||
private
|
||||
.direction
|
||||
.get()
|
||||
.expect("direction is not set")
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue