diff --git a/TODO.md b/TODO.md index c112bc3..bb7045d 100644 --- a/TODO.md +++ b/TODO.md @@ -26,7 +26,9 @@ TODO: ## bugs - [] crash with x11 on contextual menu +- [] check that element exists before creating it on file load. ## Code cleanup [] remove useless code from graphview +[] Move render to a specific module diff --git a/src/app.rs b/src/app.rs index 1b2ed49..bd5cd59 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,11 +25,12 @@ use gtk::{ FileChooserDialog, PopoverMenu, ResponseType, Statusbar, Viewport, }; use std::cell::RefCell; +use std::collections::HashMap; use std::rc::{Rc, Weak}; use std::{error, ops}; use crate::pipeline::{Pipeline, PipelineState}; -use crate::pluginlist; +use crate::plugindialogs; use crate::graphmanager::{GraphView, Node}; @@ -115,6 +116,47 @@ impl GPSApp { app.drop(); }); } + + pub fn get_file_from_dialog(app: &GPSApp, save: bool, f: F) { + let mut message = "Open file"; + let mut ok_button = "Open"; + let cancel_button = "Cancel"; + let mut action = FileChooserAction::Open; + if save { + message = "Save file"; + ok_button = "Save"; + action = FileChooserAction::Save; + } + + let file_chooser: FileChooserDialog = FileChooserDialog::new( + Some(message), + Some(&app.window), + action, + &[ + (ok_button, ResponseType::Ok), + (cancel_button, ResponseType::Cancel), + ], + ); + let app_weak = app.downgrade(); + file_chooser.connect_response(move |d: &FileChooserDialog, response: ResponseType| { + let app = upgrade_weak!(app_weak); + 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"), + ); + f(app, filename); + } + + d.close(); + }); + + file_chooser.show(); + } + pub fn show_error_dialog(fatal: bool, message: &str) { let app = gio::Application::default() .expect("No default application") @@ -165,25 +207,11 @@ impl GPSApp { // 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")); + GPSApp::get_file_from_dialog(&app, false, move |app, filename| + { 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", &["o"]); @@ -192,27 +220,14 @@ impl GPSApp { 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(); - - })); + let app = upgrade_weak!(app_weak); + GPSApp::get_file_from_dialog(&app, true, move |app, filename| + { + println!("Save file {}", filename); + app.save_graph(&filename).expect("Unable to save file"); + }); + })); application.add_action(&action); application.set_accels_for_action("app.save", &["s"]); @@ -255,7 +270,7 @@ impl GPSApp { add_button.connect_clicked(glib::clone!(@weak window => move |_| { let app = upgrade_weak!(app_weak); let elements = Pipeline::elements_list().expect("Unable to obtain element's list"); - pluginlist::display_plugin_list(&app, &elements); + plugindialogs::display_plugin_list(&app, &elements); })); let add_button: Button = self @@ -319,7 +334,7 @@ impl GPSApp { node_id, Node::new( node_id, - "videotestsrc", + "filesrc", Pipeline::element_type("videotestsrc"), ), 0, @@ -395,8 +410,11 @@ impl GPSApp { height: 0, }); let action = gio::SimpleAction::new("node.delete", None); + let app_weak = app.downgrade(); action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { + let app = upgrade_weak!(app_weak); println!("node.delete {}", node_id); + app.graphview.borrow_mut().remove_node(node_id); pop_menu.unparent(); })); application.add_action(&action); @@ -406,15 +424,18 @@ impl GPSApp { println!("node.request-pad {}", node_id); pop_menu.unparent(); })); + application.add_action(&action); let action = gio::SimpleAction::new("node.properties", None); action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { println!("node.properties {}", node_id); - + let node = app.graphview.borrow().node(&node_id).unwrap(); + plugindialogs::display_plugin_properties(&app, &node.name(), node_id); pop_menu.unparent(); })); application.add_action(&action); + pop_menu.show(); None }), @@ -434,6 +455,15 @@ impl GPSApp { let graph_view = self.graphview.borrow_mut(); let node_id = graph_view.next_node_id(); let pads = Pipeline::pads(&element_name, false); + if Pipeline::element_is_uri_src_handler(&element_name) { + GPSApp::get_file_from_dialog(self, false, move |app, filename| { + println!("Open file {}", filename); + let node = app.graphview.borrow().node(&node_id).unwrap(); + let mut properties: HashMap = HashMap::new(); + properties.insert(String::from("location"), filename); + node.update_node_properties(&properties); + }); + } graph_view.add_node_with_port( node_id, Node::new( @@ -446,6 +476,11 @@ impl GPSApp { ); } + pub fn update_element_properties(&self, node_id: u32, properties: &HashMap) { + let node = self.graphview.borrow().node(&node_id).unwrap(); + node.update_node_properties(properties); + } + pub fn save_graph(&self, filename: &str) -> anyhow::Result<(), Box> { let graph_view = self.graphview.borrow_mut(); graph_view.render_xml(filename)?; diff --git a/src/gps.ui b/src/gps.ui index 8547984..677210b 100644 --- a/src/gps.ui +++ b/src/gps.ui @@ -68,8 +68,8 @@ 260 True - - horizontal + + 200s True @@ -80,7 +80,7 @@ - + True True @@ -91,6 +91,36 @@ + + mainwindow + 320 + 260 + True + + + vertical + + + True + True + + + vertical + + + + + + + end + 1 + 1 + apply + + + + + GstPipelineStudio 800 @@ -153,7 +183,7 @@ 1 1 - clear + _clear diff --git a/src/graphmanager/graphview.rs b/src/graphmanager/graphview.rs index d99b5a2..09ba670 100644 --- a/src/graphmanager/graphview.rs +++ b/src/graphmanager/graphview.rs @@ -412,6 +412,10 @@ impl GraphView { let private = imp::GraphView::from_instance(self); let mut nodes = private.nodes.borrow_mut(); if let Some(node) = nodes.remove(&id) { + if let Some(link_id) = self.node_is_linked(node.id()) { + let mut links = private.links.borrow_mut(); + links.remove(&link_id); + } node.unparent(); } else { warn!("Tried to remove non-existant node (id={}) from graph", id); @@ -431,14 +435,15 @@ impl GraphView { nodes_list } + pub fn node(&self, id: &u32) -> Option { + let private = imp::GraphView::from_instance(self); + private.nodes.borrow().get(id).cloned() + } + pub fn remove_all_nodes(&self) { let private = imp::GraphView::from_instance(self); let nodes_list = self.all_nodes(NodeType::All); 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); @@ -493,7 +498,6 @@ impl GraphView { } // Link related methods - pub fn all_links(&self) -> Vec { let private = imp::GraphView::from_instance(self); let links = private.links.borrow(); @@ -668,6 +672,10 @@ impl GraphView { description.push_str(" ! "); } + for (name, value) in node.properties().iter() { + description.push_str(&format!(" {}={}", name, value)); + } + println!("{}", description); for port in ports { if let Some((_port_to, node_to)) = self.port_connected_to(port.id()) { @@ -714,6 +722,15 @@ impl GraphView { )?; writer.write(XMLWEvent::end_element())?; } + + for (name, value) in node.properties().iter() { + writer.write( + XMLWEvent::start_element("Property") + .attr("name", name) + .attr("value", value), + )?; + writer.write(XMLWEvent::end_element())?; + } writer.write(XMLWEvent::end_element())?; } //Get the link and write it. @@ -775,6 +792,17 @@ impl GraphView { NodeType::from_str(node_type.as_str()), )); } + "Property" => { + let name = attrs + .get::(&String::from("name")) + .expect("Unable to find property name"); + let value: &String = attrs + .get::(&String::from("value")) + .expect("Unable to find property value"); + let node = current_node.clone(); + node.expect("current node does not exist") + .add_property(name.clone(), value.clone()); + } "Port" => { let id = attrs .get::(&String::from("id")) @@ -835,6 +863,7 @@ impl GraphView { } current_node = None; } + "Property" => {} "Port" => { if let Some(port) = current_port { let node = current_node.clone(); diff --git a/src/graphmanager/node.rs b/src/graphmanager/node.rs index caa0804..afb2d15 100644 --- a/src/graphmanager/node.rs +++ b/src/graphmanager/node.rs @@ -67,6 +67,7 @@ mod imp { pub(super) ports: RefCell>, pub(super) num_ports_in: Cell, pub(super) num_ports_out: Cell, + pub(super) properties: RefCell>, } #[glib::object_subclass] @@ -97,6 +98,7 @@ mod imp { ports: RefCell::new(HashMap::new()), num_ports_in: Cell::new(0), num_ports_out: Cell::new(0), + properties: RefCell::new(HashMap::new()), } } } @@ -214,4 +216,21 @@ impl Node { let private = imp::Node::from_instance(self); private.node_type.get() } + + pub fn add_property(&self, name: String, value: String) { + let private = imp::Node::from_instance(self); + println!("{} {} updated", name, value); + private.properties.borrow_mut().insert(name, value); + } + + pub fn update_node_properties(&self, new_properties: &HashMap) { + for (key, value) in new_properties { + self.add_property(key.clone(), value.clone()); + } + } + + pub fn properties(&self) -> Ref> { + let private = imp::Node::from_instance(self); + private.properties.borrow() + } } diff --git a/src/macros.rs b/src/macros.rs index ca8bc11..859c264 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -12,3 +12,22 @@ macro_rules! upgrade_weak { upgrade_weak!($x, ()) }; } + +macro_rules! str_some_value ( + ($value:expr, $t:ty) => ( + { + let value = $value.get::<$t>().expect("ser_some_value macro"); + value + } + ); +); +macro_rules! str_opt_value ( + ($value:expr, $t:ty) => ( + { + match $value.get::<$t>() { + Ok(v) => Some(v), + Err(_e) => None + } + } + ); +); diff --git a/src/main.rs b/src/main.rs index e4fa405..2df081b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ mod app; mod common; mod graphmanager; mod pipeline; -mod pluginlist; +mod plugindialogs; use gtk::prelude::*; use crate::app::GPSApp; diff --git a/src/pipeline.rs b/src/pipeline.rs index 90ef7c7..13744d3 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -21,6 +21,7 @@ use crate::graphmanager::NodeType; use gst::prelude::*; use gstreamer as gst; use std::cell::{Cell, RefCell}; +use std::collections::HashMap; use std::error; use std::fmt; use std::ops; @@ -304,6 +305,64 @@ impl Pipeline { element_type } + + fn value_as_str(v: &glib::Value) -> Option { + match v.type_() { + glib::Type::I8 => Some(str_some_value!(v, i8).to_string()), + glib::Type::U8 => Some(str_some_value!(v, u8).to_string()), + glib::Type::BOOL => Some(str_some_value!(v, bool).to_string()), + glib::Type::I32 => Some(str_some_value!(v, i32).to_string()), + glib::Type::U32 => Some(str_some_value!(v, u32).to_string()), + glib::Type::I64 => Some(str_some_value!(v, i64).to_string()), + glib::Type::U64 => Some(str_some_value!(v, u64).to_string()), + glib::Type::F32 => Some(str_some_value!(v, f32).to_string()), + glib::Type::F64 => Some(str_some_value!(v, f64).to_string()), + glib::Type::STRING => str_opt_value!(v, String), + _ => None, + } + } + + pub fn element_properties( + element_name: &str, + ) -> anyhow::Result, Box> { + let mut properties_list = HashMap::new(); + let feature = Pipeline::element_feature(element_name).expect("Unable to get feature"); + + let factory = feature + .downcast::() + .expect("Factory not found"); + let element = factory.create(None)?; + let params = element.class().list_properties(); + + for param in params { + println!("Property_name {}", param.name()); + if (param.flags() & glib::ParamFlags::READABLE) == glib::ParamFlags::READABLE + || (param.flags() & glib::ParamFlags::READWRITE) == glib::ParamFlags::READWRITE + { + let value = Pipeline::value_as_str(&element.property(param.name()).unwrap()) + .unwrap_or_else(|| String::from("")); + properties_list.insert(String::from(param.name()), value); + } else if let Some(value) = Pipeline::value_as_str(param.default_value()) { + properties_list.insert(String::from(param.name()), value); + } else { + println!("Unable to add property_name {}", param.name()); + } + } + Ok(properties_list) + } + + pub fn element_is_uri_src_handler(element_name: &str) -> bool { + let feature = Pipeline::element_feature(element_name).expect("Unable to get feature"); + + let factory = feature + .downcast::() + .expect("Factory not found"); + let element = factory.create(None).expect("Unable to get factory"); + match element.dynamic_cast::() { + Ok(uri_handler) => uri_handler.uri_type() == gst::URIType::Src, + Err(_e) => false, + } + } } impl Drop for PipelineInner { diff --git a/src/pluginlist.rs b/src/plugindialogs.rs similarity index 62% rename from src/pluginlist.rs rename to src/plugindialogs.rs index 1c0059d..13c2de4 100644 --- a/src/pluginlist.rs +++ b/src/plugindialogs.rs @@ -1,4 +1,4 @@ -// pluginlist.rs +// plugindialogs.rs // // Copyright 2021 Stéphane Cerveau // @@ -22,8 +22,14 @@ use crate::pipeline::Pipeline; use gtk::glib; use gtk::prelude::*; use gtk::TextBuffer; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; -use gtk::{CellRendererText, Dialog, ListStore, TextView, TreeView, TreeViewColumn}; +use gtk::{ + Box, Button, CellRendererText, Dialog, Entry, Label, ListStore, TextView, TreeView, + TreeViewColumn, +}; fn create_and_fill_model(elements: &[ElementInfo]) -> ListStore { // Creation of a model with two rows. @@ -115,3 +121,61 @@ pub fn display_plugin_list(app: &GPSApp, elements: &[ElementInfo]) { dialog.show(); } + +pub fn display_plugin_properties(app: &GPSApp, element_name: &str, node_id: u32) { + let dialog: Dialog = app + .builder + .object("dialog-plugin-properties") + .expect("Couldn't get window"); + + dialog.set_title(Some(&format!("{} properties", element_name))); + dialog.set_default_size(640, 480); + dialog.set_modal(true); + + let properties_box: Box = app + .builder + .object("box-plugin-properties") + .expect("Couldn't get window"); + let update_properties: Rc>> = + Rc::new(RefCell::new(HashMap::new())); + let properties = Pipeline::element_properties(element_name).unwrap(); + for (name, value) in properties { + let entry_box = Box::new(gtk::Orientation::Horizontal, 6); + let label = Label::new(Some(&name)); + label.set_hexpand(true); + label.set_halign(gtk::Align::Start); + label.set_margin_start(4); + entry_box.append(&label); + let entry: Entry = Entry::new(); + entry.set_text(&value); + entry.set_hexpand(true); + entry.set_halign(gtk::Align::Start); + entry.set_widget_name(&name); + entry.connect_changed( + glib::clone!(@weak entry, @strong update_properties => move |_| { + println!("{}:{}", entry.widget_name(), entry.text()); + update_properties.borrow_mut().insert(entry.widget_name().to_string(), entry.text().to_string()); + }), + ); + entry_box.append(&entry); + properties_box.append(&entry_box); + } + let properties_apply_btn: Button = app + .builder + .object("apply-plugin-properties") + .expect("Couldn't get window"); + + let app_weak = app.downgrade(); + properties_apply_btn.connect_clicked( + glib::clone!(@strong update_properties, @weak dialog => move |_| { + let app = upgrade_weak!(app_weak); + app.update_element_properties(node_id, &update_properties.borrow()); + dialog.close(); + }), + ); + + dialog.show(); + for p in update_properties.borrow().values() { + println!("updated properties {}", p); + } +}