// graphbook.rs // // Copyright 2021 Stéphane Cerveau // // This file is part of GstPipelineStudio // // SPDX-License-Identifier: GPL-3.0-only use crate::app::{GPSApp, GPSAppWeak}; use crate::gps as GPS; use crate::graphmanager as GM; use crate::graphmanager::PropertyExt; use crate::logger; use crate::settings::Settings; use crate::ui as GPSUI; use crate::{GPS_DEBUG, GPS_TRACE, GPS_WARN}; use glib::Value; use gtk::prelude::*; use gtk::{gio, glib, graphene}; use std::cell::{Cell, Ref, RefCell}; use std::path::Path; #[derive(Debug, Clone, Copy, PartialEq)] enum TabState { Undefined = 0, Modified, Saved, } #[derive(Debug, Clone)] pub struct GraphTab { graphview: RefCell, player: RefCell, id: Cell, name: gtk::Label, filename: RefCell, state: Cell, } impl GraphTab { pub fn new(app: GPSAppWeak, id: u32, filename: &str) -> Self { let label = gtk::Label::new(Some("Untitled*")); let graphtab = GraphTab { id: Cell::new(id), graphview: RefCell::new(GM::GraphView::new()), player: RefCell::new( GPS::Player::new().expect("Unable to initialize GStreamer subsystem"), ), name: label, filename: RefCell::new(filename.to_string()), state: Cell::new(TabState::Undefined), }; graphtab .graphview .borrow() .set_id(graphbook_get_new_graphview_id(&app)); graphtab.player.borrow().set_app(app); graphtab } pub fn id(&self) -> u32 { self.id.get() } pub fn widget_label(&self) -> >k::Label { &self.name } pub fn graphview(&self) -> Ref { self.graphview.borrow() } pub fn player(&self) -> Ref { self.player.borrow() } pub fn set_name(&self, name: &str) { self.name.set_text(name); } pub fn basename(&self) -> String { Path::new(&self.filename.borrow().as_str()) .file_name() .unwrap() .to_str() .unwrap() .to_string() } pub fn set_filename(&self, filename: &str) { self.filename.replace(filename.to_string()); self.set_name(self.basename().as_str()); self.set_modified(false); } pub fn filename(&self) -> String { self.filename.borrow().clone() } pub fn set_modified(&self, modified: bool) { let name = self.basename(); if modified { self.set_name(&(name + "*")); self.state.set(TabState::Modified); } else { self.set_name(name.as_str()); self.state.set(TabState::Saved); } } pub fn undefined(&self) -> bool { self.state.get() == TabState::Undefined } pub fn modified(&self) -> bool { self.state.get() == TabState::Modified } } pub fn graphtab(app: &GPSApp, id: u32) -> GraphTab { app.graphbook .borrow() .get(&id) .cloned() .expect("the current graphtab should be ok") } pub fn graphbook_get_new_graphview_id(app_weak: &GPSAppWeak) -> u32 { let app = app_weak.upgrade(); let mut graphview_id: u32 = 0; for tab in app.unwrap().graphbook.borrow().values() { if tab.graphview().id() > graphview_id { graphview_id = tab.graphview().id() } } graphview_id + 1 } pub fn graphbook_get_new_graphtab_id(app: &GPSApp) -> u32 { let mut graphtab_id: u32 = 0; for tab in app.graphbook.borrow().values() { if tab.id() > graphtab_id { graphtab_id = tab.id() } } graphtab_id + 1 } pub fn current_graphtab(app: &GPSApp) -> GraphTab { graphtab(app, app.current_graphtab.get()) } pub fn current_graphtab_set_filename(app: &GPSApp, filename: &str) { app.graphbook .borrow() .get(&app.current_graphtab.get()) .expect("the graphtab is available") .set_filename(filename); } pub fn current_graphtab_set_modified(app: &GPSApp, modified: bool) { app.graphbook .borrow() .get(&app.current_graphtab.get()) .expect("the graphtab is available") .set_modified(modified); } pub fn setup_graphbook(app: &GPSApp) { let graphbook: gtk::Notebook = app .builder .object("graphbook") .expect("Couldn't get the graphbook"); let app_weak = app.downgrade(); graphbook.connect_switch_page(move |_book, widget, page| { let graphview = widget .first_child() .expect("Unable to get the child from the graphbook, ie the graphview"); if let Ok(graphview) = graphview.dynamic_cast::() { let app = upgrade_weak!(app_weak); GPS_TRACE!("graphview.id() {} graphbook page {}", graphview.id(), page); app.current_graphtab.set(page); } }); } pub fn create_graphtab(app: &GPSApp, id: u32, name: Option<&str>) { let graph_tab = GraphTab::new(app.downgrade(), id, name.unwrap_or("Untitled")); let gt = graph_tab.clone(); app.graphbook.borrow_mut().insert(id, graph_tab); let graphbook: gtk::Notebook = app .builder .object("graphbook") .expect("Couldn't get graphbook"); let drawing_area_window: gtk::Viewport = gtk::Viewport::builder().build(); drawing_area_window.set_child(Some(&*graphtab(app, id).graphview())); let tab_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); let label = gt.widget_label(); tab_box.append(label); let icon = gtk::Image::from_icon_name("window-close-symbolic"); let close_button = gtk::Button::builder().build(); close_button.set_child(Some(&icon)); close_button.add_css_class("small-button"); close_button.add_css_class("image-button"); close_button.add_css_class("flat"); let app_weak = app.downgrade(); close_button.connect_clicked(glib::clone!(@weak graphbook => move |_| { let app = upgrade_weak!(app_weak); graphbook.remove_page(Some(current_graphtab(&app).id())); })); tab_box.append(&close_button); graphbook.append_page(&drawing_area_window, Some(&tab_box)); graphbook.set_tab_reorderable(&drawing_area_window, true); let app_weak = app.downgrade(); gt.graphview().connect_local( "graph-updated", false, glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| { let app = upgrade_weak!(app_weak, None); let id = values[1].get::().expect("id in args[1]"); GPS_DEBUG!("Graph updated id={}", id); let _ = app .save_graph( Settings::graph_file_path() .to_str() .expect("Unable to convert to string"), ) .map_err(|e| GPS_WARN!("Unable to save file {}", e)); current_graphtab_set_modified(&app, true); None }), ); let app_weak = app.downgrade(); gt.graphview().connect_local( "node-added", false, glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| { let app = upgrade_weak!(app_weak, None); let graph_id = values[1].get::().expect("graph id in args[1]"); let node_id = values[2].get::().expect("node id in args[2]"); GPS_DEBUG!("Node added node id={} in graph id={}", node_id, graph_id); if let Some(node) = current_graphtab(&app).graphview().node(node_id) { let description = GPS::ElementInfo::element_description(&node.name()).ok(); node.set_tooltip_markup(description.as_deref()); if !GPS::ElementInfo::element_factory_exists(&node.name()) { node.set_light(true); } for port in node.all_ports(GM::PortDirection::All) { let caps = PropertyExt::property(&port,"_caps"); GPS_DEBUG!("caps={} for port id {}", caps.clone().unwrap_or_else(|| "caps unknown".to_string()), port.id()); port.set_tooltip_markup(caps.as_deref()); } } None }), ); let app_weak = app.downgrade(); gt.graphview().connect_local( "port-added", false, glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| { let app = upgrade_weak!(app_weak, None); let graph_id = values[1].get::().expect("graph id in args[1]"); let node_id = values[2].get::().expect("node id in args[2]"); let port_id = values[3].get::().expect("port id in args[3]"); GPS_DEBUG!("Port added port id={} to node id={} in graph id={}", port_id, node_id, graph_id); if let Some(node) = current_graphtab(&app).graphview().node(node_id) { if let Some(port) = node.port(port_id) { let caps = PropertyExt::property(&port, "_caps"); GPS_DEBUG!("caps={} for port id {}", caps.clone().unwrap_or_else(|| "caps unknown".to_string()), port.id()); port.set_tooltip_markup(caps.as_deref()); } } None }), ); // When user clicks on port with right button let app_weak = app.downgrade(); gt.graphview() .connect_local( "graph-right-clicked", false, glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| { let app = upgrade_weak!(app_weak, None); let point = values[1].get::().expect("point in args[2]"); let pop_menu = app.app_pop_menu_at_position(&*current_graphtab(&app).graphview(), point.to_vec2().x() as f64, point.to_vec2().y() as f64); let menu: gio::MenuModel = app .builder .object("graph_menu") .expect("Couldn't graph_menu"); pop_menu.set_menu_model(Some(&menu)); let app_weak = app.downgrade(); app.connect_app_menu_action("graph.clear", move |_,_| { let app = upgrade_weak!(app_weak); current_graphtab(&app).graphview().clear(); } ); let app_weak = app.downgrade(); app.connect_app_menu_action("graph.check", move |_,_| { let app = upgrade_weak!(app_weak); let render_parse_launch = current_graphtab(&app).player().pipeline_description_from_graphview(¤t_graphtab(&app).graphview()); if current_graphtab(&app).player().create_pipeline(&render_parse_launch).is_ok() { GPSUI::message::display_message_dialog(&render_parse_launch,gtk::MessageType::Info, |_| {}); } else { GPSUI::message::display_error_dialog(false, &format!("Unable to render:\n\n{render_parse_launch}")); } } ); let app_weak = app.downgrade(); app.connect_app_menu_action("graph.pipeline_details", move |_,_| { let app = upgrade_weak!(app_weak); GPSUI::properties::display_pipeline_details(&app); } ); pop_menu.show(); None }), ); // When user clicks on port with right button let app_weak = app.downgrade(); gt.graphview() .connect_local("port-right-clicked", false, move |values: &[Value]| { let app = upgrade_weak!(app_weak, None); let port_id = values[1].get::().expect("port id args[1]"); let node_id = values[2].get::().expect("node id args[2]"); let point = values[3] .get::() .expect("point in args[3]"); let pop_menu = app.app_pop_menu_at_position( &*current_graphtab(&app).graphview(), point.to_vec2().x() as f64, point.to_vec2().y() as f64, ); let menu: gio::MenuModel = app .builder .object("port_menu") .expect("Couldn't get menu model for port"); pop_menu.set_menu_model(Some(&menu)); if current_graphtab(&app) .graphview() .can_remove_port(node_id, port_id) { let app_weak = app.downgrade(); app.connect_app_menu_action("port.delete", move |_, _| { let app = upgrade_weak!(app_weak); GPS_DEBUG!("port.delete-link port id {} node id {}", port_id, node_id); current_graphtab(&app) .graphview() .remove_port(node_id, port_id); }); } else { app.disconnect_app_menu_action("port.delete"); } let app_weak = app.downgrade(); app.connect_app_menu_action("port.properties", move |_, _| { let app = upgrade_weak!(app_weak); GPS_DEBUG!("port.properties port id {} node id {}", port_id, node_id); let node = app.node(node_id); let port = app.port(node_id, port_id); GPSUI::properties::display_pad_properties( &app, &node.name(), &port.name(), node_id, port_id, ); }); pop_menu.show(); None }); // When user clicks on node with right button let app_weak = app.downgrade(); gt.graphview() .connect_local( "node-right-clicked", false, glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| { let app = upgrade_weak!(app_weak, None); let node_id = values[1].get::().expect("node id args[1]"); let node = current_graphtab(&app).graphview().node(node_id).unwrap(); let element_exists = GPS::ElementInfo::element_factory_exists(&node.name()); let point = values[2].get::().expect("point in args[2]"); let pop_menu = app.app_pop_menu_at_position(&*current_graphtab(&app).graphview(), point.to_vec2().x() as f64, point.to_vec2().y() as f64); let menu: gio::MenuModel = app .builder .object("node_menu") .expect("Couldn't get menu model for node"); pop_menu.set_menu_model(Some(&menu)); let app_weak = app.downgrade(); app.connect_app_menu_action("node.delete", move |_,_| { let app = upgrade_weak!(app_weak); GPS_DEBUG!("node.delete id: {}", node_id); current_graphtab(&app).graphview().remove_node(node_id); } ); if element_exists { let app_weak = app.downgrade(); app.connect_app_menu_action("node.add-to-favorite", move |_,_| { let app = upgrade_weak!(app_weak); GPS_DEBUG!("node.add-to-favorite id: {}", node_id); if let Some(node) = current_graphtab(&app).graphview().node(node_id) { GPSUI::elements::add_to_favorite_list(&app, node.name()); }; } ); let node = app.node(node_id); if let Some(input) = GPS::ElementInfo::element_supports_new_pad_request(&node.name(), GM::PortDirection::Input) { let app_weak = app.downgrade(); app.connect_app_menu_action("node.request-pad-input", move |_,_| { let app = upgrade_weak!(app_weak); GPS_DEBUG!("node.request-pad-input id: {}", node_id); app.create_port_with_caps(node_id, GM::PortDirection::Input, GM::PortPresence::Sometimes, input.caps().to_string()); } ); } else { app.disconnect_app_menu_action("node.request-pad-input"); } let node = app.node(node_id); if let Some(output) = GPS::ElementInfo::element_supports_new_pad_request(&node.name(), GM::PortDirection::Output) { let app_weak = app.downgrade(); app.connect_app_menu_action("node.request-pad-output", move |_,_| { let app = upgrade_weak!(app_weak); GPS_DEBUG!("node.request-pad-output id: {}", node_id); app.create_port_with_caps(node_id, GM::PortDirection::Output, GM::PortPresence::Sometimes, output.caps().to_string()); } ); } else { app.disconnect_app_menu_action("node.request-pad-output"); } let app_weak = app.downgrade(); app.connect_app_menu_action("node.properties", move |_,_| { let app = upgrade_weak!(app_weak); GPS_DEBUG!("node.properties id {}", node_id); let node = current_graphtab(&app).graphview().node(node_id).unwrap(); GPSUI::properties::display_plugin_properties(&app, &node.name(), node_id); } ); } pop_menu.show(); None }), ); let app_weak = app.downgrade(); gt.graphview().connect_local( "node-double-clicked", false, glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| { let app = upgrade_weak!(app_weak, None); let node_id = values[1].get::().expect("node id args[1]"); GPS_TRACE!("Node double clicked id={}", node_id); let node = current_graphtab(&app).graphview().node(node_id).unwrap(); if GPS::ElementInfo::element_factory_exists(&node.name()) { GPSUI::properties::display_plugin_properties(&app, &node.name(), node_id); } None }), ); let app_weak = app.downgrade(); gt.graphview().connect_local( "link-double-clicked", false, glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| { let app = upgrade_weak!(app_weak, None); let link_id = values[1].get::().expect("link id args[1]"); GPS_TRACE!("link double clicked id={}", link_id); let link = current_graphtab(&app).graphview().link(link_id).unwrap(); GPSUI::dialog::create_input_dialog( "Enter caps filter description", "description", &link.name(), &app, move |app, link_desc| { current_graphtab(&app).graphview().set_link_name(link.id(), link_desc.as_str()); GPS_DEBUG!("link double clicked id={} name={}", link.id(), link.name()); }, ); None }), ); }