graphview: display contextual menu on right click

display a contextual menu if user clicks a node or port with a right
click.
This commit is contained in:
Stéphane Cerveau 2021-12-07 12:20:21 +01:00
parent bc7f4424ca
commit 6c7dccd450
4 changed files with 229 additions and 38 deletions

12
TODO.md
View file

@ -11,18 +11,22 @@ TODO:
- [x] unable to connect in and in out and out
- [] unable to connnec element with incompatible caps.
- [x] unable to connect a port which is already connected
- [] create contextual menu on pad or element
- [x] create contextual menu on pad or element
- [] upclass the element
- [] create a crate for graphview/node/port
- [x] save/load pipeline
- [] Run a pipeline with GStreamer
- [] Run the pipeline with GStreamer
- [] Control the pipeline with GStreamer
- [x] Run a pipeline with GStreamer
- [x] Run the pipeline with GStreamer
- [x] Control the pipeline with GStreamer
- [x] Define the license
- [] Connect the logs to the window
- [] Create a window for the video output
- [] Add multiple graphviews with tabs.
## bugs
- [] crash with x11 on contextual menu
## Code cleanup
[] remove useless code from graphview

View file

@ -16,11 +16,13 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-only
use glib::Value;
use gtk::gdk::Rectangle;
use gtk::prelude::*;
use gtk::{gio, glib};
use gtk::{gio, glib, graphene};
use gtk::{
AboutDialog, Application, ApplicationWindow, Builder, Button, FileChooserAction,
FileChooserDialog, ResponseType, Statusbar, Viewport,
FileChooserDialog, PopoverMenu, ResponseType, Statusbar, Viewport,
};
use std::cell::RefCell;
use std::rc::{Rc, Weak};
@ -242,7 +244,7 @@ impl GPSApp {
}
});
application.add_action(&action);
application.set_accels_for_action("app.about", &["<primary>a"]);
//application.set_accels_for_action("app.about", &["<primary>a"]);
// Create a dialog to select GStreamer elements.
let add_button: Button = self
@ -312,7 +314,112 @@ impl GPSApp {
let app = upgrade_weak!(app_weak);
let graph_view = app.graphview.borrow_mut();
graph_view.remove_all_nodes();
let node_id = graph_view.next_node_id();
graph_view.add_node_with_port(
node_id,
Node::new(
node_id,
"videotestsrc",
Pipeline::element_type("videotestsrc"),
),
0,
1,
);
}));
let app_weak = self.downgrade();
// When user clicks on port with right button
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);
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 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 |_,_| {
println!("port.delete-link port {} node {}", port_id, node_id);
pop_menu.unparent();
}));
application.add_action(&action);
pop_menu.show();
None
}),
)
.expect("Failed to register port-right-clicked signal of graphview");
// 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 point = values[2].get::<graphene::Point>().expect("point in args[2]");
let node_menu: gio::MenuModel = app
.builder
.object("node_menu")
.expect("Couldn't get menu model for node");
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.delete", None);
action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| {
println!("node.delete {}", node_id);
pop_menu.unparent();
}));
application.add_action(&action);
let action = gio::SimpleAction::new("node.request-pad", None);
action.connect_activate(glib::clone!(@weak pop_menu => move |_,_| {
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);
pop_menu.unparent();
}));
application.add_action(&action);
pop_menu.show();
None
}),
)
.expect("Failed to register node-right-clicked signal of graphview");
}
// Downgrade to a weak reference

View file

@ -28,6 +28,34 @@
</item>
</section>
</menu>
<menu id="port_menu">
<section>
<item>
<attribute name="label" translatable="yes" comments="port menu entry delete the link">_Delete link</attribute>
<attribute name="action">app.port.delete-link</attribute>
</item>
</section>
</menu>
<menu id="node_menu">
<section>
<item>
<attribute name="label" translatable="yes" comments="Node menu entry delete the element">_Delete node</attribute>
<attribute name="action">app.node.delete</attribute>
<attribute name="accel">&lt;primary&gt;n</attribute>
</item>
<item>
<attribute name="label" translatable="yes" comments="Node menu entry request pad">_Request pad</attribute>
<attribute name="action">app.node.request-pad</attribute>
<attribute name="accel">&lt;primary&gt;n</attribute>
</item>
<item>
<attribute name="label" translatable="yes" comments="Node menu entry request pad">_Properties</attribute>
<attribute name="action">app.node.properties</attribute>
<attribute name="accel">&lt;primary&gt;n</attribute>
</item>
</section>
</menu>
<object class="GtkAboutDialog" id="dialog-about">
<property name="program-name">GstPipelineStudio</property>
<property name="authors">Stéphane Cerveau</property>

View file

@ -25,10 +25,13 @@ use xml::writer::EmitterConfig;
use xml::writer::XmlEvent as XMLWEvent;
use super::{node::Node, node::NodeType, port::Port, port::PortDirection};
use glib::subclass::Signal;
use once_cell::sync::Lazy;
use std::fs::File;
use std::io::BufReader;
use gtk::{
gdk::{BUTTON_PRIMARY, BUTTON_SECONDARY},
glib::{self, clone},
graphene, gsk,
prelude::*,
@ -135,41 +138,63 @@ mod imp {
obj.add_controller(&drag_controller);
let gesture = gtk::GestureClick::new();
gesture.connect_released(clone!(@weak gesture => move |_gesture, _n_press, x, y| {
let widget = drag_controller
.widget()
.expect("click event has no widget")
gesture.set_button(0);
gesture.connect_pressed(
clone!(@weak obj, @weak drag_controller => move |gesture, _n_press, x, y| {
if gesture.current_button() == BUTTON_SECONDARY {
let widget = drag_controller.widget().expect("click event has no widget")
.dynamic_cast::<Self::Type>()
.expect("click event is not on the GraphView");
if let Some(target) = widget.pick(x, y, gtk::PickFlags::DEFAULT) {
if let Some(target) = target.ancestor(Port::static_type()) {
let mut port_to = target.dynamic_cast::<Port>().expect("click event is not on the Port");
if widget.port_is_linked(port_to.id()).is_none() {
let selected_port = widget.selected_port().to_owned();
if let Some(mut port_from) = selected_port {
println!("Port {} is clicked at {}:{}", port_to.id(), x, y);
if widget.ports_compatible(&port_to) {
let mut node_from = port_from.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::<Node>().expect("Unable to cast to Node");
let mut node_to = port_to.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::<Node>().expect("Unable to cast to Node");
println!("add link");
if *port_to.direction() == PortDirection::Output {
println!("swap ports and nodes to create the link");
std::mem::swap(&mut node_from, &mut node_to);
std::mem::swap(&mut port_from, &mut port_to);
let target = widget.pick(x, y, gtk::PickFlags::DEFAULT).expect("port pick() did not return a widget");
if let Some(target) = target.ancestor(Port::static_type()) {
let port = target.dynamic_cast::<Port>().expect("click event is not on the Port");
let node = port.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::<Node>().expect("Unable to cast to Node");
obj.emit_by_name("port-right-clicked", &[&port.id(), &node.id(), &graphene::Point::new(x as f32,y as f32)]).expect("unable to send signal");
} else if let Some(target) = target.ancestor(Node::static_type()) {
let node = target.dynamic_cast::<Node>().expect("click event is not on the Node");
obj.emit_by_name("node-right-clicked", &[&node.id(), &graphene::Point::new(x as f32,y as f32)]).expect("unable to send signal");
}
}
}),
);
gesture.connect_released(clone!(@weak gesture, @weak drag_controller => move |_gesture, _n_press, x, y| {
if gesture.current_button() == BUTTON_PRIMARY {
let widget = drag_controller
.widget()
.expect("click event has no widget")
.dynamic_cast::<Self::Type>()
.expect("click event is not on the GraphView");
if let Some(target) = widget.pick(x, y, gtk::PickFlags::DEFAULT) {
if let Some(target) = target.ancestor(Port::static_type()) {
let mut port_to = target.dynamic_cast::<Port>().expect("click event is not on the Port");
if widget.port_is_linked(port_to.id()).is_none() {
let selected_port = widget.selected_port().to_owned();
if let Some(mut port_from) = selected_port {
println!("Port {} is clicked at {}:{}", port_to.id(), x, y);
if widget.ports_compatible(&port_to) {
let mut node_from = port_from.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::<Node>().expect("Unable to cast to Node");
let mut node_to = port_to.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::<Node>().expect("Unable to cast to Node");
println!("add link");
if *port_to.direction() == PortDirection::Output {
println!("swap ports and nodes to create the link");
std::mem::swap(&mut node_from, &mut node_to);
std::mem::swap(&mut port_from, &mut port_to);
}
widget.add_link(NodeLink {
id: widget.next_link_id(),
node_from: node_from.id(),
node_to: node_to.id(),
port_from: port_from.id(),
port_to: port_to.id(),
active: true
} );
}
widget.add_link(NodeLink {
id: widget.next_link_id(),
node_from: node_from.id(),
node_to: node_to.id(),
port_from: port_from.id(),
port_to: port_to.id(),
active: true
} );
widget.set_selected_port(None);
} else {
println!("add selected port id");
widget.set_selected_port(Some(&port_to));
}
widget.set_selected_port(None);
} else {
println!("add selected port id");
widget.set_selected_port(Some(&port_to));
}
}
}
@ -184,6 +209,33 @@ mod imp {
.values()
.for_each(|node| node.unparent())
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder(
"port-right-clicked",
&[
u32::static_type().into(),
u32::static_type().into(),
graphene::Point::static_type().into(),
],
<()>::static_type().into(),
)
.build(),
Signal::builder(
"node-right-clicked",
&[
u32::static_type().into(),
graphene::Point::static_type().into(),
],
<()>::static_type().into(),
)
.build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for GraphView {