From c1ba09814be93b7b77e5938190af5778a8ec1d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Cerveau?= Date: Fri, 7 Jan 2022 17:46:28 +0100 Subject: [PATCH] app: rework menu and actions Use one popover menu parent with the mainwindow Centralize the actions definition Cleanup the code for more readabilty. --- src/app.rs | 523 ++++++++++++++++++++++++++++------------------------- src/gps.ui | 8 +- 2 files changed, 282 insertions(+), 249 deletions(-) diff --git a/src/app.rs b/src/app.rs index ed42193..7bf28ea 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,15 +16,16 @@ // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-only +use glib::SignalHandlerId; use glib::Value; use gtk::gdk::Rectangle; use gtk::prelude::*; use gtk::{ gdk::BUTTON_SECONDARY, AboutDialog, Application, ApplicationWindow, Builder, Button, CellRendererText, FileChooserAction, FileChooserDialog, ListStore, Paned, PopoverMenu, - ResponseType, Statusbar, TreeView, TreeViewColumn, Viewport, + ResponseType, Statusbar, TreeView, TreeViewColumn, Viewport, Widget, }; -use gtk::{gio, glib, graphene}; +use gtk::{gio, gio::SimpleAction, glib, graphene}; use once_cell::unsync::OnceCell; use std::cell::RefCell; use std::collections::HashMap; @@ -46,6 +47,7 @@ pub struct GPSAppInner { pub builder: Builder, pub pipeline: RefCell, pub plugin_list_initialized: OnceCell, + pub menu_signal_handlers: RefCell>, } // This represents our main application window. @@ -101,9 +103,11 @@ impl GPSApp { builder, pipeline: RefCell::new(pipeline), plugin_list_initialized: OnceCell::new(), + menu_signal_handlers: RefCell::new(HashMap::new()), })); 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) { @@ -151,11 +155,108 @@ impl GPSApp { .expect("Couldn't get window"); 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"); + pop_menu.unparent(); + app.drop(); }); } - pub fn get_file_from_dialog(app: &GPSApp, save: bool, f: F) { + fn setup_app_actions(&self, application: >k::Application) { + application.add_action(&gio::SimpleAction::new("open", None)); + application.set_accels_for_action("app.open", &["o"]); + + application.add_action(&gio::SimpleAction::new("save_as", None)); + application.set_accels_for_action("app.save", &["s"]); + + application.add_action(&gio::SimpleAction::new("delete", None)); + application.set_accels_for_action("app.delete", &["d", "Delete"]); + + application.add_action(&gio::SimpleAction::new("quit", None)); + application.set_accels_for_action("app.quit", &["q"]); + + application.add_action(&gio::SimpleAction::new("new-window", None)); + application.set_accels_for_action("app.new-window", &["n"]); + + application.add_action(&gio::SimpleAction::new("about", None)); + application.set_accels_for_action("app.about", &["a"]); + + application.add_action(&gio::SimpleAction::new("favorite.remove", None)); + + application.add_action(&gio::SimpleAction::new("graph.add-plugin", None)); + + application.add_action(&gio::SimpleAction::new("port.delete-link", 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)); + } + + fn app_pop_menu_at_position(&self, widget: &impl IsA, x: f64, y: f64) -> PopoverMenu { + let mainwindow: ApplicationWindow = self + .builder + .object("mainwindow") + .expect("Couldn't get mainwindow"); + + let pop_menu: PopoverMenu = self + .builder + .object("app_pop_menu") + .expect("Couldn't get popover 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(&Rectangle { + x: point.to_vec2().x() as i32, + y: point.to_vec2().y() as i32, + width: 0, + height: 0, + }); + } + pop_menu + } + + fn connect_app_menu_action< + F: Fn(&SimpleAction, std::option::Option<&glib::Variant>) + 'static, + >( + &self, + action_name: &str, + f: F, + ) { + let application = gio::Application::default() + .expect("No default application") + .downcast::() + .expect("Default application has wrong type"); + let action = application + .lookup_action(action_name) + .expect("Unable to find action") + .dynamic_cast::() + .expect("Unable to cast to SimpleAction"); + + if let Some(signal_handler_id) = self.menu_signal_handlers.borrow_mut().remove(action_name) + { + action.disconnect(signal_handler_id); + } + let signal_handler_id = action.connect_activate(f); + self.menu_signal_handlers + .borrow_mut() + .insert(String::from(action_name), signal_handler_id); + } + + fn connect_button_action(&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(app: &GPSApp, save: bool, f: F) { let mut message = "Open file"; let mut ok_button = "Open"; let cancel_button = "Cancel"; @@ -269,7 +370,7 @@ impl GPSApp { } } - fn setup_favorite_list(&self) { + fn setup_favorite_list(&self, application: &Application) { let favorite_list: TreeView = self .builder .object("favorites_list") @@ -300,7 +401,7 @@ impl GPSApp { gesture.set_button(0); let app_weak = self.downgrade(); gesture.connect_pressed( - glib::clone!(@weak favorite_list => move |gesture, _n_press, x, y| { + glib::clone!(@weak favorite_list, @weak application => move |gesture, _n_press, x, y| { let app = upgrade_weak!(app_weak); if gesture.current_button() == BUTTON_SECONDARY { let selection = favorite_list.selection(); @@ -310,38 +411,24 @@ impl GPSApp { .get::() .expect("Treeview selection, column 1"); GPS_DEBUG!("{}", element_name); - let point = graphene::Point::new(x as f32,y as f32); - - let pop_menu: PopoverMenu = app + let pop_menu = app.app_pop_menu_at_position(&favorite_list, x, y); + let menu: gio::MenuModel = app .builder - .object("fav_pop_menu") - .expect("Couldn't get menu model for favorites"); + .object("fav_menu") + .expect("Couldn't get menu model for graph"); + pop_menu.set_menu_model(Some(&menu)); - pop_menu.set_pointing_to(&Rectangle { - x: point.to_vec2().x() as i32, - y: point.to_vec2().y() as i32, - width: 0, - height: 0, - }); - // add an action to delete link - let action = gio::SimpleAction::new("favorite.remove", None); - let app_weak = app.downgrade(); - action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { - let app = upgrade_weak!(app_weak); - Settings::remove_favorite(&element_name); - app.reset_favorite_list(&favorite_list); - pop_menu.unparent(); - })); - let window: ApplicationWindow = app - .builder - .object("mainwindow") - .expect("Couldn't get window"); - if let Some(application) = window.application() { - application.add_action(&action); - } + let app_weak = app.downgrade(); + app.connect_app_menu_action("favorite.remove", + move |_,_| { + let app = upgrade_weak!(app_weak); + Settings::remove_favorite(&element_name); + app.reset_favorite_list(&favorite_list); + } + ); - pop_menu.show(); + pop_menu.show(); } } @@ -389,95 +476,75 @@ 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); - GPSApp::get_file_from_dialog(&app, false, move |app, filename| - { - //logger::print_log(format!("Open file {}", filename)); - app.load_graph(&filename).expect("Unable to open file"); - }); - })); - application.add_action(&action); - application.set_accels_for_action("app.open", &["o"]); + self.setup_app_actions(application); - // 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); - GPSApp::get_file_from_dialog(&app, true, move |app, filename| - { - GPS_DEBUG!("Save file {}", filename); - app.save_graph(&filename).expect("Unable to save file"); - }); - })); - - application.add_action(&action); - application.set_accels_for_action("app.save", &["s"]); - - let action = gio::SimpleAction::new("delete", None); - application.set_accels_for_action("app.delete", &["d", "Delete"]); - let app_weak = self.downgrade(); - action.connect_activate({ - move |_, _| { - let app = upgrade_weak!(app_weak); - let graph_view = app.graphview.borrow(); - graph_view.delete_selected(); - } - }); - application.add_action(&action); - - let action = gio::SimpleAction::new("quit", None); - action.connect_activate({ - let app = application.downgrade(); - move |_, _| { - let app = app.upgrade().unwrap(); - app.quit(); - } - }); - application.add_action(&action); - application.set_accels_for_action("app.quit", &["q"]); - - let action = gio::SimpleAction::new("new-window", None); - let app_weak = self.downgrade(); - action.connect_activate({ - move |_, _| { - let app = upgrade_weak!(app_weak); - app.clear_graph(); - GPS_ERROR!("clear graph"); - } - }); - application.add_action(&action); - application.set_accels_for_action("app.new-window", &["n"]); - - let about_dialog: AboutDialog = self + let pop_menu: PopoverMenu = self .builder - .object("dialog-about") - .expect("Couldn't get window"); - let action = gio::SimpleAction::new("about", None); - action.connect_activate({ - move |_, _| { - about_dialog.show(); - } - }); - application.add_action(&action); - //application.set_accels_for_action("app.about", &["a"]); + .object("app_pop_menu") + .expect("Couldn't get pop over menu for app"); + pop_menu.set_parent(window); - // Create a dialog to select GStreamer elements. - let add_button: Button = self - .builder - .object("button-add-plugin") - .expect("Couldn't get app_button"); let app_weak = self.downgrade(); - add_button.connect_clicked(glib::clone!(@weak window => move |_| { + 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(); + application + .lookup_action("about") + .expect("Unable to find action") + .dynamic_cast::() + .expect("Unable to cast to SimpleAction") + .connect_activate({ + move |_, _| { + let app = upgrade_weak!(app_weak); + let about_dialog: AboutDialog = app + .builder + .object("dialog-about") + .expect("Couldn't get window"); + about_dialog.show(); + } + }); + + let app_weak = self.downgrade(); + self.connect_button_action("button-add-plugin", move |_| { let app = upgrade_weak!(app_weak); GPSApp::display_plugin_list(&app); - })); - + }); let add_button: Button = self .builder .object("button-play") @@ -527,27 +594,21 @@ impl GPSApp { pipeline.set_state(PipelineState::Paused).expect("Unable to change state"); } })); - let add_button: Button = self - .builder - .object("button-stop") - .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-stop", move |_| { let app = upgrade_weak!(app_weak); let pipeline = app.pipeline.borrow(); - pipeline.set_state(PipelineState::Stopped).expect("Unable to change state to STOP"); - })); - let add_button: Button = self - .builder - .object("button-clear") - .expect("Couldn't get app_button"); + let _ = pipeline.set_state(PipelineState::Stopped); + }); let app_weak = self.downgrade(); add_button.connect_clicked(glib::clone!(@weak window => move |_| { let app = upgrade_weak!(app_weak); - app.load_graph("graphs/video.xml").expect("Unable to open file"); + app.load_graph("graphs/compositor.xml").expect("Unable to open file"); })); - let app_weak = self.downgrade(); + // When user clicks on port with right button + let app_weak = self.downgrade(); self.graphview .borrow() .connect_local( @@ -555,77 +616,61 @@ impl GPSApp { false, glib::clone!(@weak application => @default-return None, move |values: &[Value]| { let app = upgrade_weak!(app_weak, None); - let point = values[1].get::().expect("point in args[2]"); - let port_menu: gio::MenuModel = app - .builder - .object("graph_menu") - .expect("Couldn't get menu model for graph"); + 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 get menu model for graph"); + pop_menu.set_menu_model(Some(&menu)); - let pop_menu: PopoverMenu = PopoverMenu::from_model(Some(&port_menu)); - pop_menu.set_parent(&*app.graphview.borrow_mut()); - pop_menu.set_pointing_to(&Rectangle { - x: point.to_vec2().x() as i32, - y: point.to_vec2().y() as i32, - width: 0, - height: 0, - }); - // add an action to delete link - let action = gio::SimpleAction::new("graph.add-plugin", None); - action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { - GPSApp::display_plugin_list(&app); - pop_menu.unparent(); - })); - application.add_action(&action); + let app_weak = app.downgrade(); + app.connect_app_menu_action("graph.add-plugin", + move |_,_| { + let app = upgrade_weak!(app_weak); + GPSApp::display_plugin_list(&app); + } + ); pop_menu.show(); None }), ) .expect("Failed to register graph-right-clicked signal of graphview"); - let app_weak = self.downgrade(); + // When user clicks on port with right button + let app_weak = self.downgrade(); self.graphview .borrow() - .connect_local( - "port-right-clicked", - false, - glib::clone!(@weak application => @default-return None, move |values: &[Value]| { - let app = upgrade_weak!(app_weak, None); + .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 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 port_menu: gio::MenuModel = app - .builder - .object("port_menu") - .expect("Couldn't get menu model for port"); - - let pop_menu: PopoverMenu = PopoverMenu::from_model(Some(&port_menu)); - pop_menu.set_parent(&*app.graphview.borrow_mut()); - pop_menu.set_pointing_to(&Rectangle { - x: point.to_vec2().x() as i32, - y: point.to_vec2().y() as i32, - width: 0, - height: 0, - }); - // add an action to delete link - let action = gio::SimpleAction::new("port.delete-link", None); - action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { - GPS_DEBUG!("port.delete-link port {} node {}", port_id, node_id); - pop_menu.unparent(); - })); - application.add_action(&action); - - pop_menu.show(); - None - }), - ) + 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)); + app.connect_app_menu_action("port.delete-link", move |_, _| { + GPS_DEBUG!("port.delete-link port {} node {}", port_id, node_id); + }); + pop_menu.show(); + None + }) .expect("Failed to register port-right-clicked signal of graphview"); - // When user clicks on node with right button + // When user clicks on node with right button let app_weak = self.downgrade(); self.graphview .borrow() @@ -638,73 +683,65 @@ impl GPSApp { let node_id = values[1].get::().expect("node id args[1]"); let point = values[2].get::().expect("point in args[2]"); - let node_menu: gio::MenuModel = app + 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 pop_menu: PopoverMenu = PopoverMenu::from_model(Some(&node_menu)); - pop_menu.set_parent(&*app.graphview.borrow_mut()); - pop_menu.set_pointing_to(&Rectangle { - x: point.to_vec2().x() as i32, - y: point.to_vec2().y() as i32, - width: 0, - height: 0, - }); - - let action = gio::SimpleAction::new("node.add-to-favorite", None); let app_weak = app.downgrade(); - action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { - let app = upgrade_weak!(app_weak); - GPS_DEBUG!("node.delete {}", node_id); - let node = app.graphview.borrow().node(&node_id).unwrap(); - app.add_to_favorite_list(node.name()); - pop_menu.unparent(); - })); - application.add_action(&action); - let action = gio::SimpleAction::new("node.delete", None); + 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) { + app.add_to_favorite_list(node.name()); + }; + } + ); + let app_weak = app.downgrade(); - action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { - let app = upgrade_weak!(app_weak); - GPS_DEBUG!("node.delete {}", node_id); - app.graphview.borrow_mut().remove_node(node_id); - pop_menu.unparent(); - })); - application.add_action(&action); + 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); + } + ); - let action = gio::SimpleAction::new("node.request-pad-input", None); let app_weak = app.downgrade(); - action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { - let app = upgrade_weak!(app_weak); - GPS_DEBUG!("node.request-pad-input {}", node_id); - let mut node = app.graphview.borrow_mut().node(&node_id).unwrap(); - let port_id = app.graphview.borrow().next_port_id(); - node.add_port(port_id, "in", PortDirection::Input); - pop_menu.unparent(); - })); - application.add_action(&action); + app.connect_app_menu_action("node.request-pad-input", + move |_,_| { + let app = upgrade_weak!(app_weak); + GPS_DEBUG!("node.request-pad-input {}", node_id); + let mut node = app.graphview.borrow_mut().node(&node_id).unwrap(); + let port_id = app.graphview.borrow().next_port_id(); + node.add_port(port_id, "in", PortDirection::Input); + } + ); - let action = gio::SimpleAction::new("node.request-pad-output", None); let app_weak = app.downgrade(); - action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { - let app = upgrade_weak!(app_weak); - GPS_DEBUG!("node.request-pad-output {}", node_id); - let mut node = app.graphview.borrow_mut().node(&node_id).unwrap(); - let port_id = app.graphview.borrow_mut().next_port_id(); - node.add_port(port_id, "out", PortDirection::Output); - pop_menu.unparent(); - })); - application.add_action(&action); + app.connect_app_menu_action("node.request-pad-output", + move |_,_| { + let app = upgrade_weak!(app_weak); + GPS_DEBUG!("node.request-pad-output {}", node_id); + let mut node = app.graphview.borrow_mut().node(&node_id).unwrap(); + let port_id = app.graphview.borrow_mut().next_port_id(); + node.add_port(port_id, "out", PortDirection::Output); - let action = gio::SimpleAction::new("node.properties", None); - action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| { - GPS_DEBUG!("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); + } + ); + 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(); + plugindialogs::display_plugin_properties(&app, &node.name(), node_id); + } + ); pop_menu.show(); None @@ -713,7 +750,7 @@ impl GPSApp { .expect("Failed to register node-right-clicked signal of graphview"); // Setup the favorite list - self.setup_favorite_list(); + self.setup_favorite_list(application); // Setup the logger to get messages into the TreeView let (ready_tx, ready_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); @@ -761,18 +798,18 @@ impl GPSApp { node.update_node_properties(properties); } - pub fn clear_graph(&self) { + fn clear_graph(&self) { let graph_view = self.graphview.borrow_mut(); graph_view.remove_all_nodes(); } - pub fn save_graph(&self, filename: &str) -> anyhow::Result<(), Box> { + fn save_graph(&self, filename: &str) -> anyhow::Result<(), Box> { let graph_view = self.graphview.borrow_mut(); graph_view.render_xml(filename)?; Ok(()) } - pub fn load_graph(&self, filename: &str) -> anyhow::Result<(), Box> { + fn load_graph(&self, filename: &str) -> anyhow::Result<(), Box> { 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 f0adc73..c39f847 100644 --- a/src/gps.ui +++ b/src/gps.ui @@ -83,7 +83,8 @@ - + + GstPipelineStudio Stéphane Cerveau @@ -259,11 +260,6 @@ True - - - fav_menu - -