diff --git a/TODO.md b/TODO.md index 3302626..99e1c55 100644 --- a/TODO.md +++ b/TODO.md @@ -25,6 +25,15 @@ TODO: - [x] Connect the logs to the window - [] Create a window for the video output - [] Add multiple graphviews with tabs. +- [] Property window in the main window +- [] Connect the GPS status to GST status +- [] Implement graph dot render/load +- [] Implement a command line parser to graph +- [] Unable to create a pad in an element without the template +- [] Remove a pad from the graph +- [] Implement graphview unit test +- [] Implement pipeline unit test +- [] Save node position in XML ## bugs @@ -35,5 +44,5 @@ TODO: ## Code cleanup [] remove useless code from graphview -[] Move render to a specific module +[X] Move render to a specific module [x] Move GST render to a specific module diff --git a/src/app.rs b/src/app.rs index c66f376..6575d98 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,6 +16,7 @@ // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-only + use glib::SignalHandlerId; use glib::Value; use gtk::gdk::Rectangle; @@ -29,8 +30,8 @@ use gtk::{gio, gio::SimpleAction, glib, graphene}; use once_cell::unsync::OnceCell; use std::cell::RefCell; use std::collections::HashMap; +use std::ops; use std::rc::{Rc, Weak}; -use std::{error, ops}; use crate::about; use crate::logger; @@ -78,26 +79,29 @@ impl GPSAppWeak { } impl GPSApp { - fn new(application: >k::Application) -> anyhow::Result> { + fn new(application: >k::Application) -> anyhow::Result { let glade_src = include_str!("gps.ui"); let builder = Builder::from_string(glade_src); - let window: ApplicationWindow = builder.object("mainwindow").expect("Couldn't get window"); + let window: ApplicationWindow = builder + .object("mainwindow") + .expect("Couldn't get the main window"); window.set_application(Some(application)); window.set_title(Some("GstPipelineStudio")); + let settings = Settings::load_settings(); window.set_size_request(settings.app_width, settings.app_height); let paned: Paned = builder .object("graph_logs-paned") - .expect("Couldn't get window"); + .expect("Couldn't get graph_logs-paned"); paned.set_position(settings.app_graph_logs_paned_pos); let paned: Paned = builder .object("graph_favorites-paned") - .expect("Couldn't get window"); + .expect("Couldn't get graph_favorites-paned"); paned.set_position(settings.app_graph_favorites_paned_pos); if settings.app_maximized { window.maximize(); } - let pipeline = Pipeline::new().expect("Unable to initialize the pipeline"); + let pipeline = Pipeline::new().expect("Unable to initialize GStreamer subsystem"); let app = GPSApp(Rc::new(GPSAppInner { window, graphview: RefCell::new(GraphView::new()), @@ -113,11 +117,8 @@ impl GPSApp { // Create application and error out if that fails for whatever reason let app = match GPSApp::new(application) { Ok(app) => app, - Err(_err) => { - /* utils::show_error_dialog( - true, - format!("Error creating application: {}", err).as_str(), - ); */ + Err(err) => { + println!("Error creating application: {}", err); return; } }; @@ -125,7 +126,6 @@ impl GPSApp { // When the application is activated show the UI. This happens when the first process is // started, and in the first process whenever a second process is started let app_weak = app.downgrade(); - application.connect_activate(glib::clone!(@weak application => move |_| { let app = upgrade_weak!(app_weak); app.build_ui(&application); @@ -140,7 +140,7 @@ impl GPSApp { let window: ApplicationWindow = app .builder .object("mainwindow") - .expect("Couldn't get window"); + .expect("Couldn't get the main window"); let mut settings = Settings::load_settings(); settings.app_maximized = window.is_maximized(); settings.app_width = window.width(); @@ -148,19 +148,19 @@ impl GPSApp { let paned: Paned = app .builder .object("graph_logs-paned") - .expect("Couldn't get window"); + .expect("Couldn't get graph_logs-paned"); settings.app_graph_logs_paned_pos = paned.position(); let paned: Paned = app .builder .object("graph_favorites-paned") - .expect("Couldn't get window"); + .expect("Couldn't get graph_favorites-paned"); settings.app_graph_favorites_paned_pos = paned.position(); Settings::save_settings(&settings); let pop_menu: PopoverMenu = app .builder .object("app_pop_menu") - .expect("Couldn't get pop over menu for app"); + .expect("Couldn't get app_pop_menu"); pop_menu.unparent(); app.drop(); @@ -203,12 +203,12 @@ impl GPSApp { let mainwindow: ApplicationWindow = self .builder .object("mainwindow") - .expect("Couldn't get mainwindow"); + .expect("Couldn't get the main window"); let pop_menu: PopoverMenu = self .builder .object("app_pop_menu") - .expect("Couldn't get popover menu"); + .expect("Couldn't get app_pop_menu"); if let Some((x, y)) = widget.translate_coordinates(&mainwindow, x, y) { let point = graphene::Point::new(x as f32, y as f32); @@ -232,12 +232,12 @@ impl GPSApp { let application = gio::Application::default() .expect("No default application") .downcast::() - .expect("Default application has wrong type"); + .expect("Unable to downcast default application"); let action = application .lookup_action(action_name) - .expect("Unable to find action") + .unwrap_or_else(|| panic!("Unable to find action {}", action_name)) .dynamic_cast::() - .expect("Unable to cast to SimpleAction"); + .expect("Unable to dynamic cast to SimpleAction"); if let Some(signal_handler_id) = self.menu_signal_handlers.borrow_mut().remove(action_name) { @@ -270,7 +270,7 @@ impl GPSApp { let window: ApplicationWindow = app .builder .object("mainwindow") - .expect("Couldn't get window"); + .expect("Couldn't get main window"); let file_chooser: FileChooserDialog = FileChooserDialog::new( Some(message), Some(&window), @@ -289,7 +289,7 @@ impl GPSApp { file.path() .expect("Couldn't get file path") .to_str() - .expect("unable to convert to string"), + .expect("Unable to convert to string"), ); f(app, filename); } @@ -336,8 +336,8 @@ impl GPSApp { fn setup_logger_list(&self) { let logger_list: TreeView = self .builder - .object("logger_list") - .expect("Couldn't get window"); + .object("treeview-logger") + .expect("Couldn't get treeview-logger"); let column = TreeViewColumn::new(); let cell = CellRendererText::new(); column.pack_start(&cell, true); @@ -358,8 +358,8 @@ impl GPSApp { fn add_to_logger_list(&self, log_entry: String) { let logger_list: TreeView = self .builder - .object("logger_list") - .expect("Couldn't get window"); + .object("treeview-logger") + .expect("Couldn't get treeview-logger"); if let Some(model) = logger_list.model() { let list_store = model .dynamic_cast::() @@ -382,8 +382,8 @@ impl GPSApp { fn setup_favorite_list(&self, application: &Application) { let favorite_list: TreeView = self .builder - .object("favorites_list") - .expect("Couldn't get window"); + .object("treeview-favorites") + .expect("Couldn't get treeview-favorites"); let column = TreeViewColumn::new(); let cell = CellRendererText::new(); @@ -402,7 +402,7 @@ impl GPSApp { .get(&iter, 0) .get::() .expect("Treeview selection, column 1"); - GPS_DEBUG!("{}", element_name); + GPS_DEBUG!("{} selected", element_name); app.add_new_element(&element_name); } }); @@ -418,14 +418,14 @@ impl GPSApp { let element_name = model .get(&iter, 0) .get::() - .expect("Treeview selection, column 1"); - GPS_DEBUG!("{}", element_name); + .expect("Treeview selection, column 0"); + GPS_DEBUG!("Element {} selected", element_name); let pop_menu = app.app_pop_menu_at_position(&favorite_list, x, y); let menu: gio::MenuModel = app .builder .object("fav_menu") - .expect("Couldn't get menu model for graph"); + .expect("Couldn't get fav_menu model"); pop_menu.set_menu_model(Some(&menu)); let app_weak = app.downgrade(); @@ -451,8 +451,8 @@ impl GPSApp { if !favorites.contains(&element_name) { let favorite_list: TreeView = self .builder - .object("favorites_list") - .expect("Couldn't get window"); + .object("treeview-favorites") + .expect("Couldn't get treeview-favorites"); if let Some(model) = favorite_list.model() { let list_store = model .dynamic_cast::() @@ -472,7 +472,7 @@ impl GPSApp { let drawing_area_window: Viewport = self .builder .object("drawing_area") - .expect("Couldn't get window"); + .expect("Couldn't get drawing_area"); drawing_area_window.set_child(Some(&*self.graphview.borrow())); @@ -482,7 +482,7 @@ impl GPSApp { let status_bar: Statusbar = self .builder .object("status_bar") - .expect("Couldn't get window"); + .expect("Couldn't get status_bar"); status_bar.push(status_bar.context_id("Description"), "GPS is ready"); self.setup_app_actions(application); @@ -490,7 +490,7 @@ impl GPSApp { let pop_menu: PopoverMenu = self .builder .object("app_pop_menu") - .expect("Couldn't get pop over menu for app"); + .expect("Couldn't get app_pop_menu"); pop_menu.set_parent(window); let app_weak = self.downgrade(); @@ -543,48 +543,26 @@ impl GPSApp { let app = upgrade_weak!(app_weak); GPSApp::display_plugin_list(&app); }); - 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 graph_view = app.graphview.borrow(); - let pipeline = app.pipeline.borrow(); - if pipeline.state() == PipelineState::Stopped { - if let Err(err) = pipeline.create_pipeline(&pipeline.render_gst_launch(&graph_view)) { - GPS_ERROR!("Unable to start a pipeline: {}", err); - } - pipeline.set_state(PipelineState::Playing).expect("Unable to change state"); - } else if pipeline.state() == PipelineState::Paused { - pipeline.set_state(PipelineState::Playing).expect("Unable to change state"); - } else { - pipeline.set_state(PipelineState::Paused).expect("Unable to change state"); - } - })); - let add_button: Button = self - .builder - .object("button-pause") - .expect("Couldn't get app_button"); let app_weak = self.downgrade(); - add_button.connect_clicked(glib::clone!(@weak window => move |_| { + self.connect_button_action("button-play", move |_| { let app = upgrade_weak!(app_weak); let graph_view = app.graphview.borrow(); - let pipeline = app.pipeline.borrow(); - if pipeline.state() == PipelineState::Stopped { - if let Err(err) = pipeline.create_pipeline(&pipeline.render_gst_launch(&graph_view)) { - GPS_ERROR!("Unable to start a pipeline: {}", err); - } - pipeline.set_state(PipelineState::Paused).expect("Unable to change state"); - } else if pipeline.state() == PipelineState::Paused { - pipeline.set_state(PipelineState::Playing).expect("Unable to change state"); - } else { - pipeline.set_state(PipelineState::Paused).expect("Unable to change state"); - } - })); + let _ = app + .pipeline + .borrow() + .start_pipeline(&graph_view, PipelineState::Playing); + }); + + let app_weak = self.downgrade(); + self.connect_button_action("button-pause", move |_| { + let app = upgrade_weak!(app_weak); + let graph_view = app.graphview.borrow(); + let _ = app + .pipeline + .borrow() + .start_pipeline(&graph_view, PipelineState::Paused); + }); let app_weak = self.downgrade(); self.connect_button_action("button-stop", move |_| { @@ -592,6 +570,7 @@ impl GPSApp { let pipeline = app.pipeline.borrow(); let _ = pipeline.set_state(PipelineState::Stopped); }); + let app_weak = self.downgrade(); self.connect_button_action("button-clear", move |_| { let app = upgrade_weak!(app_weak); @@ -614,7 +593,7 @@ impl GPSApp { let menu: gio::MenuModel = app .builder .object("graph_menu") - .expect("Couldn't get menu model for graph"); + .expect("Couldn't graph_menu"); pop_menu.set_menu_model(Some(&menu)); let app_weak = app.downgrade(); @@ -624,7 +603,6 @@ impl GPSApp { GPSApp::display_plugin_list(&app); } ); - pop_menu.show(); None }), @@ -795,13 +773,13 @@ impl GPSApp { graph_view.remove_all_nodes(); } - fn save_graph(&self, filename: &str) -> anyhow::Result<(), Box> { + fn save_graph(&self, filename: &str) -> anyhow::Result<()> { let graph_view = self.graphview.borrow_mut(); graph_view.render_xml(filename)?; Ok(()) } - fn load_graph(&self, filename: &str) -> anyhow::Result<(), Box> { + fn load_graph(&self, filename: &str) -> anyhow::Result<()> { self.clear_graph(); let graph_view = self.graphview.borrow_mut(); graph_view.load_xml(filename)?; diff --git a/src/gps.ui b/src/gps.ui index 201fc66..2498107 100644 --- a/src/gps.ui +++ b/src/gps.ui @@ -139,7 +139,7 @@ - + end 1 1 @@ -254,7 +254,7 @@ True True - + @@ -264,7 +264,7 @@ - + diff --git a/src/pipeline.rs b/src/pipeline.rs index d10e938..f1e35b3 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -25,7 +25,6 @@ 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; use std::rc::{Rc, Weak}; @@ -88,7 +87,7 @@ pub struct PipelineInner { } impl Pipeline { - pub fn new() -> Result> { + pub fn new() -> anyhow::Result { let pipeline = Pipeline(Rc::new(PipelineInner { pipeline: RefCell::new(None), current_state: Cell::new(PipelineState::Stopped), @@ -97,36 +96,58 @@ impl Pipeline { Ok(pipeline) } - pub fn create_pipeline(&self, description: &str) -> Result<(), Box> { + pub fn create_pipeline(&self, description: &str) -> anyhow::Result<()> { GPS_INFO!("Creating pipeline {}", description); - /* create playbin */ - + // Create pipeline from the description let pipeline = gst::parse_launch(&description.to_string())?; if let Ok(pipeline) = pipeline.downcast::() { - //pipeline.set_property_message_forward(true); - let bus = pipeline.bus().expect("Pipeline had no bus"); let pipeline_weak = self.downgrade(); bus.add_watch_local(move |_bus, msg| { let pipeline = upgrade_weak!(pipeline_weak, glib::Continue(false)); - pipeline.on_pipeline_message(msg); - glib::Continue(true) })?; *self.pipeline.borrow_mut() = Some(pipeline); /* start playing */ } else { - GPS_ERROR!("Couldn't downcast pipeline") + GPS_ERROR!("Can not create a proper pipeline from gstreamer parse_launch"); } Ok(()) } - pub fn set_state(&self, state: PipelineState) -> Result<(), Box> { + pub fn start_pipeline( + &self, + graphview: &GraphView, + new_state: PipelineState, + ) -> anyhow::Result { + if self.state() == PipelineState::Stopped { + self.create_pipeline(&self.render_gst_launch(graphview)) + .map_err(|err| { + GPS_ERROR!("Unable to start a pipeline: {}", err); + }) + .unwrap(); + self.set_state(new_state) + .map_err(|_| GPS_ERROR!("Unable to change state")) + .unwrap(); + } else if self.state() == PipelineState::Paused { + self.set_state(PipelineState::Playing) + .map_err(|_| GPS_ERROR!("Unable to change state")) + .unwrap(); + } else { + self.set_state(PipelineState::Paused) + .map_err(|_| GPS_ERROR!("Unable to change state")) + .unwrap(); + } + + Ok(self.state()) + } + + pub fn set_state(&self, new_state: PipelineState) -> anyhow::Result { if let Some(pipeline) = self.pipeline.borrow().to_owned() { - match state { + match new_state { PipelineState::Playing => pipeline.set_state(gst::State::Playing)?, PipelineState::Paused => pipeline.set_state(gst::State::Paused)?, PipelineState::Stopped => { @@ -134,9 +155,9 @@ impl Pipeline { gst::StateChangeSuccess::Success } }; - self.current_state.set(state); + self.current_state.set(new_state); } - Ok(()) + Ok(new_state) } pub fn state(&self) -> PipelineState { @@ -181,7 +202,7 @@ impl Pipeline { }; } - pub fn elements_list() -> Result, Box> { + pub fn elements_list() -> anyhow::Result> { let registry = gst::Registry::get(); let mut elements: Vec = Vec::new(); let plugins = gst::Registry::plugin_list(®istry); @@ -204,24 +225,15 @@ impl Pipeline { Ok(elements) } - pub fn element_feature( - element_name: &str, - ) -> anyhow::Result> { + fn element_feature(element_name: &str) -> Option { let registry = gst::Registry::get(); - let feature = gst::Registry::find_feature( - ®istry, - element_name, - gst::ElementFactory::static_type(), - ) - .expect("Unable to find the element name"); - Ok(feature) + gst::Registry::find_feature(®istry, element_name, gst::ElementFactory::static_type()) } - pub fn element_description( - element_name: &str, - ) -> anyhow::Result> { + pub fn element_description(element_name: &str) -> anyhow::Result { let mut desc = String::from(""); - let feature = Pipeline::element_feature(element_name)?; + let feature = Pipeline::element_feature(element_name) + .ok_or_else(|| glib::bool_error!("Failed get element feature"))?; if let Ok(factory) = feature.downcast::() { desc.push_str("Factory details:\n"); @@ -330,15 +342,13 @@ impl Pipeline { } } - pub fn element_properties( - element_name: &str, - ) -> anyhow::Result, Box> { + pub fn element_properties(element_name: &str) -> anyhow::Result> { 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"); + .expect("Unable to get the factory from the feature"); let element = factory.create(None)?; let params = element.class().list_properties(); @@ -364,8 +374,10 @@ impl Pipeline { let factory = feature .downcast::() - .expect("Factory not found"); - let element = factory.create(None).expect("Unable to get factory"); + .expect("Unable to get the factory from the feature"); + let element = factory + .create(None) + .expect("Unable to create an element from the feature"); match element.dynamic_cast::() { Ok(uri_handler) => uri_handler.uri_type() == gst::URIType::Src, Err(_e) => false, @@ -373,7 +385,7 @@ impl Pipeline { } // Render graph methods - pub fn process_node( + fn process_gst_node( &self, graphview: &GraphView, node: &Node, @@ -401,7 +413,7 @@ impl Pipeline { description.push_str(&format!("{}. ", node.unique_name())); } else { description = - self.process_node(graphview, &node, elements, description.clone()); + self.process_gst_node(graphview, &node, elements, description.clone()); } } } @@ -415,7 +427,7 @@ impl Pipeline { let mut description = String::from(""); for source_node in source_nodes { description = - self.process_node(graphview, &source_node, &mut elements, description.clone()); + self.process_gst_node(graphview, &source_node, &mut elements, description.clone()); } description } diff --git a/src/plugindialogs.rs b/src/plugindialogs.rs index 36b4f9e..a1fd1eb 100644 --- a/src/plugindialogs.rs +++ b/src/plugindialogs.rs @@ -61,7 +61,7 @@ pub fn display_plugin_list(app: &GPSApp, elements: &[ElementInfo]) { let dialog: Dialog = app .builder .object("dialog-plugin-list") - .expect("Couldn't get window"); + .expect("Couldn't get the dialog-plugin-list window"); if app.plugin_list_initialized.get().is_none() { dialog.set_title(Some("Plugin list")); @@ -70,13 +70,13 @@ pub fn display_plugin_list(app: &GPSApp, elements: &[ElementInfo]) { let text_view: TextView = app .builder .object("textview-plugin-list") - .expect("Couldn't get window"); + .expect("Couldn't get textview-plugin-list window"); let text_buffer: TextBuffer = text_view.buffer(); let tree: TreeView = app .builder .object("treeview-plugin-list") - .expect("Couldn't get window"); + .expect("Couldn't get treeview-plugin-list window"); if tree.n_columns() < 2 { append_column(&tree, 0); append_column(&tree, 1); @@ -94,8 +94,8 @@ pub fn display_plugin_list(app: &GPSApp, elements: &[ElementInfo]) { let element_name = model .get(&iter, 1) .get::() - .expect("Treeview selection, column 1"); - let description = Pipeline::element_description(&element_name).expect("Unable to get element list from GStreamer"); + .expect("Unable to get the treeview selection, column 1"); + let description = Pipeline::element_description(&element_name).expect("Unable to get element description from GStreamer"); text_buffer.set_text(""); text_buffer.insert_markup(&mut text_buffer.end_iter(), &description); } @@ -110,14 +110,12 @@ pub fn display_plugin_list(app: &GPSApp, elements: &[ElementInfo]) { let element_name = model .get(&iter, 1) .get::() - .expect("Treeview selection, column 1"); + .expect("Unable to get the treeview selection, column 1"); app.add_new_element(&element_name); } }), ); - app.plugin_list_initialized - .set(true) - .expect("Should never happen"); + app.plugin_list_initialized.set(true).unwrap(); } dialog.show(); @@ -127,7 +125,7 @@ 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"); + .expect("Couldn't get dialog-plugin-properties"); dialog.set_title(Some(&format!("{} properties", element_name))); dialog.set_default_size(640, 480); @@ -136,7 +134,7 @@ pub fn display_plugin_properties(app: &GPSApp, element_name: &str, node_id: u32) let properties_box: Box = app .builder .object("box-plugin-properties") - .expect("Couldn't get window"); + .expect("Couldn't get box-plugin-properties"); let update_properties: Rc>> = Rc::new(RefCell::new(HashMap::new())); let properties = Pipeline::element_properties(element_name).unwrap(); @@ -163,8 +161,8 @@ pub fn display_plugin_properties(app: &GPSApp, element_name: &str, node_id: u32) } let properties_apply_btn: Button = app .builder - .object("apply-plugin-properties") - .expect("Couldn't get window"); + .object("button-apply-plugin-properties") + .expect("Couldn't get button-apply-plugin-properties"); let app_weak = app.downgrade(); properties_apply_btn.connect_clicked(