mirror of
https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio.git
synced 2024-06-06 09:49:21 +00:00
9b768b7d56
The user can now see the position and duration of the playback The slider can now seek to the given position.
865 lines
35 KiB
Rust
865 lines
35 KiB
Rust
// app.rs
|
|
//
|
|
// 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 glib::SignalHandlerId;
|
|
use glib::Value;
|
|
use gtk::gdk;
|
|
use gtk::prelude::*;
|
|
use gtk::{gio, gio::SimpleAction, glib, graphene};
|
|
use gtk::{
|
|
Application, ApplicationWindow, Builder, Button, FileChooserAction, FileChooserDialog, Paned,
|
|
PopoverMenu, ResponseType, Statusbar, Viewport, Widget,
|
|
};
|
|
use log::error;
|
|
use once_cell::unsync::OnceCell;
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::fs::File;
|
|
use std::io::{Read, Write};
|
|
use std::ops;
|
|
use std::rc::{Rc, Weak};
|
|
|
|
use crate::gps as GPS;
|
|
use crate::logger;
|
|
use crate::settings::Settings;
|
|
use crate::ui as GPSUI;
|
|
|
|
use crate::{GPS_DEBUG, GPS_ERROR, GPS_INFO, GPS_TRACE, GPS_WARN};
|
|
|
|
use crate::graphmanager as GM;
|
|
use crate::graphmanager::PropertyExt;
|
|
use std::fmt;
|
|
#[derive(Debug)]
|
|
pub struct GPSAppInner {
|
|
pub window: gtk::ApplicationWindow,
|
|
pub graphview: RefCell<GM::GraphView>,
|
|
pub builder: Builder,
|
|
pub pipeline: RefCell<GPS::Pipeline>,
|
|
pub plugin_list_initialized: OnceCell<bool>,
|
|
pub signal_handlers: RefCell<HashMap<String, SignalHandlerId>>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum AppState {
|
|
Ready,
|
|
Playing,
|
|
Paused,
|
|
Stopped,
|
|
Error,
|
|
}
|
|
|
|
impl fmt::Display for AppState {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "{:?}", self)
|
|
}
|
|
}
|
|
|
|
// This represents our main application window.
|
|
#[derive(Debug, Clone)]
|
|
pub struct GPSApp(Rc<GPSAppInner>);
|
|
|
|
// Deref into the contained struct to make usage a bit more ergonomic
|
|
impl ops::Deref for GPSApp {
|
|
type Target = GPSAppInner;
|
|
|
|
fn deref(&self) -> &GPSAppInner {
|
|
&*self.0
|
|
}
|
|
}
|
|
|
|
// Weak reference to our application struct
|
|
//
|
|
// Weak references are important to prevent reference cycles. Reference cycles are cases where
|
|
// struct A references directly or indirectly struct B, and struct B references struct A again
|
|
// while both are using reference counting.
|
|
pub struct GPSAppWeak(Weak<GPSAppInner>);
|
|
impl GPSAppWeak {
|
|
// Upgrade to a strong reference if it still exists
|
|
pub fn upgrade(&self) -> Option<GPSApp> {
|
|
self.0.upgrade().map(GPSApp)
|
|
}
|
|
}
|
|
|
|
impl GPSApp {
|
|
fn new(application: >k::Application) -> anyhow::Result<GPSApp> {
|
|
let glade_src = include_str!("gps.ui");
|
|
let builder = Builder::from_string(glade_src);
|
|
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_dashboard-paned")
|
|
.expect("Couldn't get graph_dashboard-paned");
|
|
paned.set_position(settings.app_graph_dashboard_paned_pos);
|
|
let paned: Paned = builder
|
|
.object("graph_logs-paned")
|
|
.expect("Couldn't get graph_logs-paned");
|
|
paned.set_position(settings.app_graph_logs_paned_pos);
|
|
let paned: Paned = builder
|
|
.object("elements_preview-paned")
|
|
.expect("Couldn't get elements_preview-paned");
|
|
paned.set_position(settings.app_elements_preview_paned_pos);
|
|
let paned: Paned = builder
|
|
.object("elements_properties-paned")
|
|
.expect("Couldn't get elements_properties-paned");
|
|
paned.set_position(settings.app_elements_properties_paned_pos);
|
|
|
|
if settings.app_maximized {
|
|
window.maximize();
|
|
}
|
|
let pipeline = GPS::Pipeline::new().expect("Unable to initialize GStreamer subsystem");
|
|
let app = GPSApp(Rc::new(GPSAppInner {
|
|
window,
|
|
graphview: RefCell::new(GM::GraphView::new()),
|
|
builder,
|
|
pipeline: RefCell::new(pipeline),
|
|
plugin_list_initialized: OnceCell::new(),
|
|
signal_handlers: RefCell::new(HashMap::new()),
|
|
}));
|
|
let app_weak = app.downgrade();
|
|
app.pipeline.borrow().set_app(app_weak);
|
|
app.graphview.borrow_mut().set_id(0);
|
|
Ok(app)
|
|
}
|
|
|
|
pub fn on_startup(application: >k::Application) {
|
|
// Create application and error out if that fails for whatever reason
|
|
let app = match GPSApp::new(application) {
|
|
Ok(app) => app,
|
|
Err(err) => {
|
|
error!("Error creating application: {}", err);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// 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);
|
|
}));
|
|
|
|
let app_weak = app.downgrade();
|
|
let slider: gtk::Scale = app
|
|
.builder
|
|
.object("scale-position")
|
|
.expect("Couldn't get status_bar");
|
|
let slider_update_signal_id = slider.connect_value_changed(move |slider| {
|
|
let app = upgrade_weak!(app_weak);
|
|
let pipeline = app.pipeline.borrow();
|
|
let value = slider.value() as u64;
|
|
GPS_TRACE!("Seeking to {} s", value);
|
|
if pipeline.set_position(value).is_err() {
|
|
GPS_ERROR!("Seeking to {} failed", value);
|
|
}
|
|
});
|
|
let app_weak = app.downgrade();
|
|
let timeout_id =
|
|
glib::timeout_add_local(std::time::Duration::from_millis(500), move || {
|
|
let app = upgrade_weak!(app_weak, glib::Continue(false));
|
|
let pipeline = app.pipeline.borrow();
|
|
|
|
let label: gtk::Label = app
|
|
.builder
|
|
.object("label-position")
|
|
.expect("Couldn't get status_bar");
|
|
let slider: gtk::Scale = app
|
|
.builder
|
|
.object("scale-position")
|
|
.expect("Couldn't get status_bar");
|
|
let position = pipeline.position();
|
|
let duration = pipeline.duration();
|
|
slider.set_range(0.0, duration as f64 / 1000_f64);
|
|
slider.block_signal(&slider_update_signal_id);
|
|
slider.set_value(position as f64 / 1000_f64);
|
|
slider.unblock_signal(&slider_update_signal_id);
|
|
|
|
// Query the current playing position from the underlying pipeline.
|
|
let position_desc = pipeline.position_description();
|
|
// Display the playing position in the gui.
|
|
label.set_text(&position_desc);
|
|
// Tell the callback to continue calling this closure.
|
|
glib::Continue(true)
|
|
});
|
|
|
|
let timeout_id = RefCell::new(Some(timeout_id));
|
|
let app_container = RefCell::new(Some(app));
|
|
|
|
application.connect_shutdown(move |_| {
|
|
let app = app_container
|
|
.borrow_mut()
|
|
.take()
|
|
.expect("Shutdown called multiple times");
|
|
let window: ApplicationWindow = app
|
|
.builder
|
|
.object("mainwindow")
|
|
.expect("Couldn't get the main window");
|
|
let mut settings = Settings::load_settings();
|
|
settings.app_maximized = window.is_maximized();
|
|
settings.app_width = window.width();
|
|
settings.app_height = window.height();
|
|
let paned: Paned = app
|
|
.builder
|
|
.object("graph_dashboard-paned")
|
|
.expect("Couldn't get graph_dashboard-paned");
|
|
settings.app_graph_dashboard_paned_pos = paned.position();
|
|
let paned: Paned = app
|
|
.builder
|
|
.object("graph_logs-paned")
|
|
.expect("Couldn't get graph_logs-paned");
|
|
settings.app_graph_logs_paned_pos = paned.position();
|
|
let paned: Paned = app
|
|
.builder
|
|
.object("elements_preview-paned")
|
|
.expect("Couldn't get elements_preview-paned");
|
|
settings.app_elements_preview_paned_pos = paned.position();
|
|
let paned: Paned = app
|
|
.builder
|
|
.object("elements_properties-paned")
|
|
.expect("Couldn't get elements_properties-paned");
|
|
settings.app_elements_properties_paned_pos = paned.position();
|
|
Settings::save_settings(&settings);
|
|
|
|
let pop_menu: PopoverMenu = app
|
|
.builder
|
|
.object("app_pop_menu")
|
|
.expect("Couldn't get app_pop_menu");
|
|
pop_menu.unparent();
|
|
if let Some(timeout_id) = timeout_id.borrow_mut().take() {
|
|
timeout_id.remove();
|
|
}
|
|
|
|
app.drop();
|
|
});
|
|
}
|
|
|
|
fn setup_app_actions(&self, application: >k::Application) {
|
|
application.add_action(&gio::SimpleAction::new("open", None));
|
|
application.set_accels_for_action("app.open", &["<primary>o"]);
|
|
|
|
application.add_action(&gio::SimpleAction::new("save_as", None));
|
|
application.set_accels_for_action("app.save", &["<primary>s"]);
|
|
|
|
application.add_action(&gio::SimpleAction::new("delete", None));
|
|
application.set_accels_for_action("app.delete", &["<primary>d", "Delete"]);
|
|
|
|
application.add_action(&gio::SimpleAction::new("quit", None));
|
|
application.set_accels_for_action("app.quit", &["<primary>q"]);
|
|
|
|
application.add_action(&gio::SimpleAction::new("new-window", None));
|
|
application.set_accels_for_action("app.new-window", &["<primary>n"]);
|
|
|
|
application.add_action(&gio::SimpleAction::new("about", None));
|
|
application.set_accels_for_action("app.about", &["<primary>a"]);
|
|
|
|
application.add_action(&gio::SimpleAction::new("favorite.remove", None));
|
|
|
|
application.add_action(&gio::SimpleAction::new("graph.check", None));
|
|
|
|
application.add_action(&gio::SimpleAction::new("port.delete", None));
|
|
|
|
application.add_action(&gio::SimpleAction::new("node.add-to-favorite", None));
|
|
application.add_action(&gio::SimpleAction::new("node.delete", None));
|
|
application.add_action(&gio::SimpleAction::new("node.request-pad-input", None));
|
|
application.add_action(&gio::SimpleAction::new("node.request-pad-output", None));
|
|
application.add_action(&gio::SimpleAction::new("node.properties", None));
|
|
}
|
|
|
|
pub fn app_pop_menu_at_position(
|
|
&self,
|
|
widget: &impl IsA<Widget>,
|
|
x: f64,
|
|
y: f64,
|
|
) -> PopoverMenu {
|
|
let mainwindow: ApplicationWindow = self
|
|
.builder
|
|
.object("mainwindow")
|
|
.expect("Couldn't get the main window");
|
|
|
|
let pop_menu: PopoverMenu = self
|
|
.builder
|
|
.object("app_pop_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);
|
|
pop_menu.set_pointing_to(Some(&gdk::Rectangle::new(
|
|
point.to_vec2().x() as i32,
|
|
point.to_vec2().y() as i32,
|
|
0,
|
|
0,
|
|
)));
|
|
}
|
|
pop_menu
|
|
}
|
|
|
|
fn app_menu_action(&self, action_name: &str) -> SimpleAction {
|
|
let application = gio::Application::default()
|
|
.expect("No default application")
|
|
.downcast::<gtk::Application>()
|
|
.expect("Unable to downcast default application");
|
|
|
|
application
|
|
.lookup_action(action_name)
|
|
.unwrap_or_else(|| panic!("Unable to find action {}", action_name))
|
|
.dynamic_cast::<SimpleAction>()
|
|
.expect("Unable to dynamic cast to SimpleAction")
|
|
}
|
|
|
|
fn disconnect_app_menu_action(&self, action_name: &str) {
|
|
let action = self.app_menu_action(action_name);
|
|
|
|
if let Some(signal_handler_id) = self.signal_handlers.borrow_mut().remove(action_name) {
|
|
action.disconnect(signal_handler_id);
|
|
}
|
|
}
|
|
|
|
pub fn connect_app_menu_action<
|
|
F: Fn(&SimpleAction, std::option::Option<&glib::Variant>) + 'static,
|
|
>(
|
|
&self,
|
|
action_name: &str,
|
|
f: F,
|
|
) {
|
|
let action = self.app_menu_action(action_name);
|
|
self.disconnect_app_menu_action(action_name);
|
|
let signal_handler_id = action.connect_activate(f);
|
|
self.signal_handlers
|
|
.borrow_mut()
|
|
.insert(String::from(action_name), signal_handler_id);
|
|
}
|
|
|
|
fn connect_button_action<F: Fn(&Button) + 'static>(&self, button_name: &str, f: F) {
|
|
let button: Button = self
|
|
.builder
|
|
.object(button_name)
|
|
.unwrap_or_else(|| panic!("Couldn't get app_button {}", button_name));
|
|
button.connect_clicked(f);
|
|
}
|
|
|
|
fn get_file_from_dialog<F: Fn(GPSApp, String) + 'static>(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 window: ApplicationWindow = app
|
|
.builder
|
|
.object("mainwindow")
|
|
.expect("Couldn't get main window");
|
|
let file_chooser: FileChooserDialog = FileChooserDialog::new(
|
|
Some(message),
|
|
Some(&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 set_app_state(&self, state: AppState) {
|
|
let status_bar: Statusbar = self
|
|
.builder
|
|
.object("status_bar")
|
|
.expect("Couldn't get status_bar");
|
|
status_bar.push(status_bar.context_id("Description"), &state.to_string());
|
|
}
|
|
|
|
pub fn set_app_preview(&self, paintable: &gdk::Paintable) {
|
|
let picture = gtk::Picture::new();
|
|
picture.set_paintable(Some(paintable));
|
|
let box_preview: gtk::Box = self
|
|
.builder
|
|
.object("box-preview")
|
|
.expect("Couldn't get box_preview");
|
|
while let Some(child) = box_preview.first_child() {
|
|
box_preview.remove(&child);
|
|
}
|
|
box_preview.append(&picture);
|
|
}
|
|
|
|
pub fn build_ui(&self, application: &Application) {
|
|
let drawing_area_window: Viewport = self
|
|
.builder
|
|
.object("drawing_area")
|
|
.expect("Couldn't get drawing_area");
|
|
|
|
drawing_area_window.set_child(Some(&*self.graphview.borrow()));
|
|
|
|
// Setup the logger to get messages into the TreeView
|
|
let (ready_tx, ready_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
|
let app_weak = self.downgrade();
|
|
logger::init_logger(
|
|
ready_tx,
|
|
Settings::default_log_file_path()
|
|
.to_str()
|
|
.expect("Unable to convert log file path to a string"),
|
|
);
|
|
GPSUI::logger::setup_logger_list(self);
|
|
let _ = ready_rx.attach(None, move |msg: String| {
|
|
let app = upgrade_weak!(app_weak, glib::Continue(false));
|
|
GPSUI::logger::add_to_logger_list(&app, &msg);
|
|
glib::Continue(true)
|
|
});
|
|
|
|
let window = &self.window;
|
|
|
|
window.show();
|
|
self.set_app_state(AppState::Ready);
|
|
self.setup_app_actions(application);
|
|
|
|
let pop_menu: PopoverMenu = self
|
|
.builder
|
|
.object("app_pop_menu")
|
|
.expect("Couldn't get app_pop_menu");
|
|
pop_menu.set_parent(window);
|
|
|
|
let app_weak = self.downgrade();
|
|
self.connect_app_menu_action("new-window", move |_, _| {
|
|
let app = upgrade_weak!(app_weak);
|
|
app.clear_graph();
|
|
GPS_ERROR!("clear graph");
|
|
});
|
|
|
|
let app_weak = self.downgrade();
|
|
self.connect_app_menu_action("open", move |_, _| {
|
|
let app = upgrade_weak!(app_weak);
|
|
GPSApp::get_file_from_dialog(&app, false, move |app, filename| {
|
|
app.load_graph(&filename)
|
|
.unwrap_or_else(|_| GPS_ERROR!("Unable to open file {}", filename));
|
|
});
|
|
});
|
|
|
|
let app_weak = self.downgrade();
|
|
self.connect_app_menu_action("save_as", move |_, _| {
|
|
let app = upgrade_weak!(app_weak);
|
|
GPSApp::get_file_from_dialog(&app, true, move |app, filename| {
|
|
GPS_DEBUG!("Save file {}", filename);
|
|
app.save_graph(&filename)
|
|
.unwrap_or_else(|_| GPS_ERROR!("Unable to save file to {}", filename));
|
|
});
|
|
});
|
|
|
|
let app_weak = self.downgrade();
|
|
self.connect_app_menu_action("delete", move |_, _| {
|
|
let app = upgrade_weak!(app_weak);
|
|
let graph_view = app.graphview.borrow();
|
|
graph_view.delete_selected();
|
|
});
|
|
|
|
let app = application.downgrade();
|
|
self.connect_app_menu_action("quit", move |_, _| {
|
|
let app = app.upgrade().unwrap();
|
|
app.quit();
|
|
});
|
|
|
|
let app_weak = self.downgrade();
|
|
self.connect_app_menu_action("about", move |_, _| {
|
|
let app = upgrade_weak!(app_weak);
|
|
GPSUI::about::display_about_dialog(&app);
|
|
});
|
|
|
|
let app_weak = self.downgrade();
|
|
self.connect_button_action("button-play", move |_| {
|
|
let app = upgrade_weak!(app_weak);
|
|
let graph_view = app.graphview.borrow();
|
|
let _ = app
|
|
.pipeline
|
|
.borrow()
|
|
.start_pipeline(&graph_view, GPS::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, GPS::PipelineState::Paused);
|
|
});
|
|
|
|
let app_weak = self.downgrade();
|
|
self.connect_button_action("button-stop", move |_| {
|
|
let app = upgrade_weak!(app_weak);
|
|
let pipeline = app.pipeline.borrow();
|
|
let _ = pipeline.set_state(GPS::PipelineState::Stopped);
|
|
});
|
|
|
|
let app_weak = self.downgrade();
|
|
self.connect_button_action("button-clear", move |_| {
|
|
let app = upgrade_weak!(app_weak);
|
|
app.clear_graph();
|
|
});
|
|
let app_weak = self.downgrade();
|
|
self.graphview.borrow().connect_local(
|
|
"node-added",
|
|
false,
|
|
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
|
|
let app = upgrade_weak!(app_weak, None);
|
|
let graph_id = values[1].get::<u32>().expect("graph id in args[1]");
|
|
let node_id = values[2].get::<u32>().expect("node id in args[2]");
|
|
GPS_INFO!("Node added node id={} in graph id={}", node_id, graph_id);
|
|
if let Some(node) = app.graphview.borrow().node(node_id) {
|
|
let description = GPS::ElementInfo::element_description(&node.name()).ok();
|
|
node.set_tooltip_markup(description.as_deref());
|
|
for port in node.all_ports(GM::PortDirection::All) {
|
|
let caps = PropertyExt::property(&port, "caps");
|
|
GPS_TRACE!("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 = self.downgrade();
|
|
self.graphview.borrow().connect_local(
|
|
"port-added",
|
|
false,
|
|
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
|
|
let app = upgrade_weak!(app_weak, None);
|
|
let graph_id = values[1].get::<u32>().expect("graph id in args[1]");
|
|
let node_id = values[2].get::<u32>().expect("node id in args[2]");
|
|
let port_id = values[3].get::<u32>().expect("port id in args[3]");
|
|
GPS_INFO!("Port added port id={} to node id={} in graph id={}", port_id, node_id, graph_id);
|
|
if let Some(node) = app.graphview.borrow().node(node_id) {
|
|
if let Some(port) = node.port(port_id) {
|
|
let caps = PropertyExt::property(&port, "caps");
|
|
GPS_TRACE!("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 = self.downgrade();
|
|
self.graphview.borrow().connect_local(
|
|
"graph-updated",
|
|
false,
|
|
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
|
|
let app = upgrade_weak!(app_weak, None);
|
|
let id = values[1].get::<u32>().expect("id in args[1]");
|
|
GPS_TRACE!("Graph updated id={}", id);
|
|
let _ = app
|
|
.save_graph(
|
|
Settings::default_graph_file_path()
|
|
.to_str()
|
|
.expect("Unable to convert to string"),
|
|
)
|
|
.map_err(|e| GPS_WARN!("Unable to save file {}", e));
|
|
None
|
|
}),
|
|
);
|
|
// When user clicks on port with right button
|
|
let app_weak = self.downgrade();
|
|
self.graphview
|
|
.borrow()
|
|
.connect_local(
|
|
"graph-right-clicked",
|
|
false,
|
|
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
|
|
let app = upgrade_weak!(app_weak, None);
|
|
let point = values[1].get::<graphene::Point>().expect("point in args[2]");
|
|
|
|
let pop_menu = app.app_pop_menu_at_position(&*app.graphview.borrow(), 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.check",
|
|
move |_,_| {
|
|
let app = upgrade_weak!(app_weak);
|
|
let render_parse_launch = app.pipeline.borrow().render_gst_launch(&app.graphview.borrow());
|
|
if app.pipeline.borrow().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));
|
|
}
|
|
}
|
|
);
|
|
pop_menu.show();
|
|
None
|
|
}),
|
|
);
|
|
|
|
// When user clicks on port with right button
|
|
let app_weak = self.downgrade();
|
|
self.graphview.borrow().connect_local(
|
|
"port-right-clicked",
|
|
false,
|
|
move |values: &[Value]| {
|
|
let app = upgrade_weak!(app_weak, None);
|
|
|
|
let port_id = values[1].get::<u32>().expect("port id args[1]");
|
|
let node_id = values[2].get::<u32>().expect("node id args[2]");
|
|
let point = values[3]
|
|
.get::<graphene::Point>()
|
|
.expect("point in args[3]");
|
|
|
|
let pop_menu = app.app_pop_menu_at_position(
|
|
&*app.graphview.borrow(),
|
|
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 app.graphview.borrow().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 {} node {}", port_id, node_id);
|
|
app.graphview.borrow().remove_port(node_id, port_id);
|
|
});
|
|
} else {
|
|
app.disconnect_app_menu_action("port.delete");
|
|
}
|
|
|
|
pop_menu.show();
|
|
None
|
|
},
|
|
);
|
|
|
|
// When user clicks on node with right button
|
|
let app_weak = self.downgrade();
|
|
self.graphview
|
|
.borrow()
|
|
.connect_local(
|
|
"node-right-clicked",
|
|
false,
|
|
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
|
|
let app = upgrade_weak!(app_weak, None);
|
|
|
|
let node_id = values[1].get::<u32>().expect("node id args[1]");
|
|
let node = app.graphview.borrow().node(node_id).expect("Unable to find node with this ID");
|
|
let point = values[2].get::<graphene::Point>().expect("point in args[2]");
|
|
|
|
let pop_menu = app.app_pop_menu_at_position(&*app.graphview.borrow(), 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.add-to-favorite",
|
|
move |_,_| {
|
|
let app = upgrade_weak!(app_weak);
|
|
GPS_DEBUG!("node.add-to-favorite {}", node_id);
|
|
if let Some(node) = app.graphview.borrow().node(node_id) {
|
|
GPSUI::elements::add_to_favorite_list(&app, node.name());
|
|
};
|
|
}
|
|
);
|
|
|
|
let app_weak = app.downgrade();
|
|
app.connect_app_menu_action("node.delete",
|
|
move |_,_| {
|
|
let app = upgrade_weak!(app_weak);
|
|
GPS_DEBUG!("node.delete {}", node_id);
|
|
app.graphview.borrow_mut().remove_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);
|
|
let graphview = app.graphview.borrow();
|
|
GPS_DEBUG!("node.request-pad-input {}", node_id);
|
|
let properties: HashMap<String, String> = HashMap::from([
|
|
("caps".to_string(), input.caps().to_string())]);
|
|
let port = graphview.create_port("in", GM::PortDirection::Input, GM::PortPresence::Sometimes);
|
|
port.update_properties(&properties);
|
|
if let Some(mut node) = graphview.node(node_id) {
|
|
graphview.add_port_to_node(&mut node, port);
|
|
}
|
|
}
|
|
);
|
|
} else {
|
|
app.disconnect_app_menu_action("node.request-pad-input");
|
|
}
|
|
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);
|
|
let graphview = app.graphview.borrow();
|
|
GPS_DEBUG!("node.request-pad-output {}", node_id);
|
|
let properties: HashMap<String, String> = HashMap::from([
|
|
("caps".to_string(), output.caps().to_string())]);
|
|
let port = graphview.create_port("out", GM::PortDirection::Output, GM::PortPresence::Sometimes);
|
|
port.update_properties(&properties);
|
|
if let Some(mut node) = graphview.node(node_id) {
|
|
graphview.add_port_to_node(&mut node, port);
|
|
}
|
|
|
|
}
|
|
);
|
|
} 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 {}", node_id);
|
|
let node = app.graphview.borrow().node(node_id).unwrap();
|
|
GPSUI::properties::display_plugin_properties(&app, &node.name(), node_id);
|
|
}
|
|
);
|
|
|
|
pop_menu.show();
|
|
None
|
|
}),
|
|
);
|
|
|
|
// Setup the favorite list
|
|
GPSUI::elements::setup_favorite_list(self);
|
|
// Setup the favorite list
|
|
GPSUI::elements::setup_elements_list(self);
|
|
|
|
let _ = self
|
|
.load_graph(
|
|
Settings::default_graph_file_path()
|
|
.to_str()
|
|
.expect("Unable to convert to string"),
|
|
)
|
|
.map_err(|_e| {
|
|
GPS_WARN!("Unable to load default graph");
|
|
});
|
|
}
|
|
|
|
// Downgrade to a weak reference
|
|
pub fn downgrade(&self) -> GPSAppWeak {
|
|
GPSAppWeak(Rc::downgrade(&self.0))
|
|
}
|
|
|
|
// Called when the application shuts down. We drop our app struct here
|
|
fn drop(self) {}
|
|
|
|
pub fn add_new_element(&self, element_name: &str) {
|
|
let graphview = self.graphview.borrow();
|
|
let (inputs, outputs) = GPS::PadInfo::pads(element_name, false);
|
|
let mut node =
|
|
graphview.create_node(element_name, GPS::ElementInfo::element_type(element_name));
|
|
let node_id = node.id();
|
|
if GPS::ElementInfo::element_is_uri_src_handler(element_name) {
|
|
GPSApp::get_file_from_dialog(self, false, move |app, filename| {
|
|
GPS_DEBUG!("Open file {}", filename);
|
|
let graphview = app.graphview.borrow();
|
|
let mut properties: HashMap<String, String> = HashMap::new();
|
|
properties.insert(String::from("location"), filename);
|
|
if let Some(node) = graphview.node(node_id) {
|
|
node.update_properties(&properties);
|
|
}
|
|
});
|
|
}
|
|
|
|
for input in inputs {
|
|
let properties: HashMap<String, String> =
|
|
HashMap::from([("caps".to_string(), input.caps().to_string())]);
|
|
let port =
|
|
graphview.create_port("in", GM::PortDirection::Input, GM::PortPresence::Always);
|
|
port.update_properties(&properties);
|
|
graphview.add_port_to_node(&mut node, port);
|
|
}
|
|
for output in outputs {
|
|
let properties: HashMap<String, String> =
|
|
HashMap::from([("caps".to_string(), output.caps().to_string())]);
|
|
let port =
|
|
graphview.create_port("out", GM::PortDirection::Output, GM::PortPresence::Always);
|
|
port.update_properties(&properties);
|
|
graphview.add_port_to_node(&mut node, port);
|
|
}
|
|
graphview.add_node(node);
|
|
}
|
|
|
|
pub fn update_element_properties(&self, node_id: u32, properties: &HashMap<String, String>) {
|
|
let node = self.graphview.borrow().node(node_id).unwrap();
|
|
node.update_properties(properties);
|
|
}
|
|
|
|
pub fn element_property(&self, node_id: u32, property_name: &str) -> Option<String> {
|
|
let node = self.graphview.borrow().node(node_id).unwrap();
|
|
PropertyExt::property(&node, property_name)
|
|
}
|
|
|
|
fn clear_graph(&self) {
|
|
let graph_view = self.graphview.borrow();
|
|
graph_view.clear();
|
|
}
|
|
|
|
fn save_graph(&self, filename: &str) -> anyhow::Result<()> {
|
|
let graph_view = self.graphview.borrow();
|
|
let mut file = File::create(filename)?;
|
|
let buffer = graph_view.render_xml()?;
|
|
file.write_all(&buffer)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn load_graph(&self, filename: &str) -> anyhow::Result<()> {
|
|
let graph_view = self.graphview.borrow();
|
|
let mut file = File::open(filename)?;
|
|
let mut buffer = Vec::new();
|
|
file.read_to_end(&mut buffer).expect("buffer overflow");
|
|
graph_view.load_from_xml(buffer)?;
|
|
Ok(())
|
|
}
|
|
}
|