graphview: can now select nodes and links

Nodes and links can be selected and deleted
This commit is contained in:
Stéphane Cerveau 2021-12-13 18:15:48 +01:00
parent 7d08abaca8
commit a0bb503b27
9 changed files with 409 additions and 93 deletions

View file

@ -19,9 +19,9 @@ TODO:
- [x] Run the pipeline with GStreamer
- [x] Control the pipeline with GStreamer
- [x] Define the license
- [] check that that a node accept to create a port on request (input/output)
- [] select nodes/links with a Trait Selectable
- [] be able to remove a link by selecting it
- [] check that a node accept to create a port on request (input/output)
- [x] select nodes/links with a Trait Selectable
- [x] be able to remove a link by selecting it
- [] Connect the logs to the window
- [] Create a window for the video output
- [] Add multiple graphviews with tabs.

View file

@ -232,6 +232,18 @@ impl GPSApp {
application.add_action(&action);
application.set_accels_for_action("app.save", &["<primary>s"]);
let action = gio::SimpleAction::new("delete", None);
application.set_accels_for_action("app.delete", &["<primary>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();
@ -437,7 +449,7 @@ impl GPSApp {
let app = upgrade_weak!(app_weak);
println!("node.request-pad-output {}", node_id);
let mut node = app.graphview.borrow_mut().node(&node_id).unwrap();
let port_id = app.graphview.borrow().next_port_id();
let port_id = app.graphview.borrow_mut().next_port_id();
node.add_port(port_id, "out", PortDirection::Output);
pop_menu.unparent();
}));

View file

@ -40,8 +40,7 @@
<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>
<attribute name="action">app.delete</attribute>
</item>
<submenu>
<attribute name="label" translatable="yes" comments="Node menu entry request pad">_Request pad</attribute>

View file

@ -1,11 +1,29 @@
@define-color graphview-link #808080;
node-button {
color: black;
padding: 10px;
border-radius: 5px;
transition: all 250ms ease-in;
border: 1px transparent solid;
button.node {
color: rgb(0, 0, 255);
background: rgb(170, 255, 170);
}
button.node-selected {
border-color: rgb(255, 0, 0);
background: rgb(170, 255, 170);
}
button.port {
color: rgb(0, 0, 255);
}
button.port-selected {
border-color: rgb(255, 0, 0);
}
button.port-out {
background: rgb(255, 170, 170);
}
button.port-in {
background: rgb(170, 170, 255);
}
graphview {

View file

@ -24,7 +24,7 @@ use xml::reader::XmlEvent as XMLREvent;
use xml::writer::EmitterConfig;
use xml::writer::XmlEvent as XMLWEvent;
use super::{node::Node, node::NodeType, port::Port, port::PortDirection};
use super::{link::Link, node::Node, node::NodeType, port::Port, port::PortDirection};
use glib::subclass::Signal;
use once_cell::sync::Lazy;
use std::fs::File;
@ -41,15 +41,6 @@ use log::{error, warn};
use std::cell::RefMut;
use std::{cmp::Ordering, collections::HashMap, error};
#[derive(Debug, Clone)]
pub struct NodeLink {
pub id: u32,
pub node_from: u32,
pub node_to: u32,
pub port_from: u32,
pub port_to: u32,
pub active: bool,
}
static GRAPHVIEW_STYLE: &str = include_str!("graphview.css");
@ -66,7 +57,7 @@ mod imp {
#[derive(Default)]
pub struct GraphView {
pub(super) nodes: RefCell<HashMap<u32, Node>>,
pub(super) links: RefCell<HashMap<u32, NodeLink>>,
pub(super) links: RefCell<HashMap<u32, Link>>,
pub(super) current_node_id: Cell<u32>,
pub(super) current_port_id: Cell<u32>,
pub(super) current_link_id: Cell<u32>,
@ -152,8 +143,27 @@ mod imp {
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");
widget.unselect_all();
node.set_selected(true);
obj.emit_by_name("node-right-clicked", &[&node.id(), &graphene::Point::new(x as f32,y as f32)]).expect("unable to send signal");
}
} else 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");
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 Node");
widget.unselect_all();
port.toggle_selected();
} else if let Some(target) = target.ancestor(Node::static_type()) {
let node = target.dynamic_cast::<Node>().expect("click event is not on the Node");
widget.unselect_all();
node.toggle_selected();
}
else {
widget.point_on_link(&graphene::Point::new(x.floor() as f32,y.floor() as f32));
}
}
}),
);
@ -181,14 +191,15 @@ mod imp {
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(Link::new(
widget.next_link_id(),
node_from.id(),
node_to.id(),
port_from.id(),
port_to.id(),
true,
false,
));
}
widget.set_selected_port(None);
} else {
@ -261,18 +272,21 @@ mod imp {
))
.expect("Failed to get cairo context");
link_cr.set_line_width(1.5);
for link in self.links.borrow().values() {
if let Some((from_x, from_y, to_x, to_y)) = self.link_coordinates(link) {
//println!("from_x: {} from_y: {} to_x: {} to_y: {}", from_x, from_y, to_x, to_y);
link_cr.set_line_width(link.thickness as f64);
// Use dashed line for inactive links, full line otherwise.
if link.active {
link_cr.set_dash(&[], 0.0);
} else {
link_cr.set_dash(&[10.0, 5.0], 0.0);
}
if link.selected() {
link_cr.set_source_rgb(1.0, 0.18, 0.18);
} else {
link_cr.set_source_rgb(0.0, 0.0, 0.0);
}
link_cr.move_to(from_x, from_y);
link_cr.line_to(to_x, to_y);
@ -293,13 +307,12 @@ mod imp {
///
/// # Returns
/// `Some((from_x, from_y, to_x, to_y))` if all objects the links refers to exist as widgets.
fn link_coordinates(&self, link: &NodeLink) -> Option<(f64, f64, f64, f64)> {
pub fn link_coordinates(&self, link: &Link) -> Option<(f64, f64, f64, f64)> {
let nodes = self.nodes.borrow();
// For some reason, gtk4::WidgetExt::translate_coordinates gives me incorrect values,
// so we manually calculate the needed offsets here.
let from_node = nodes.get(&link.node_from)?;
let from_port = from_node.port(&link.port_from)?;
let from_port = &nodes.get(&link.node_from)?.port(&link.port_from)?;
let gtk::Allocation {
x: mut fx,
y: mut fy,
@ -307,28 +320,29 @@ mod imp {
height: fh,
} = from_port.allocation();
let from_node = from_port
.ancestor(Node::static_type())
.expect("Port is not a child of a node");
let gtk::Allocation { x: fnx, y: fny, .. } = from_node.allocation();
fx += fnx + (fw / 2);
fy += fny + (fh / 2);
let to_port = &nodes.get(&link.node_to)?.port(&link.port_to)?;
if let Some((port_x, port_y)) = from_port.translate_coordinates(from_node, 0.0, 0.0) {
fx += fnx + fw + port_x as i32;
fy = fny + (fh / 2) + port_y as i32;
}
let to_node = nodes.get(&link.node_to)?;
let to_port = to_node.port(&link.port_to)?;
let gtk::Allocation {
x: mut tx,
y: mut ty,
width: tw,
width: _tw,
height: th,
..
} = to_port.allocation();
let to_node = to_port
.ancestor(Node::static_type())
.expect("Port is not a child of a node");
let gtk::Allocation { x: tnx, y: tny, .. } = to_node.allocation();
tx += tnx + (tw / 2);
ty += tny + (th / 2);
let gtk::Allocation { x: tnx, y: tny, .. } = to_node.allocation();
if let Some((port_x, port_y)) = to_port.translate_coordinates(to_node, 0.0, 0.0) {
tx += tnx + port_x as i32;
ty = tny + (th / 2) + port_y as i32;
}
//println!("{} {} -> {} {}", fx, fy, tx, ty);
Some((fx.into(), fy.into(), tx.into(), ty.into()))
}
}
@ -460,6 +474,14 @@ impl GraphView {
None
}
pub fn unselect_nodes(&self) {
let private = imp::GraphView::from_instance(self);
for node in private.nodes.borrow_mut().values() {
node.set_selected(false);
node.unselect_all_ports();
}
}
// Port related methods
pub fn add_port(
&self,
@ -502,14 +524,14 @@ impl GraphView {
}
// Link related methods
pub fn all_links(&self) -> Vec<NodeLink> {
pub fn all_links(&self) -> Vec<Link> {
let private = imp::GraphView::from_instance(self);
let links = private.links.borrow();
let links_list: Vec<_> = links.iter().map(|(_, link)| link.clone()).collect();
links_list
}
pub fn add_link(&self, link: NodeLink) {
pub fn add_link(&self, link: Link) {
let private = imp::GraphView::from_instance(self);
if !self.link_exists(&link) {
private.links.borrow_mut().insert(link.id, link);
@ -535,6 +557,35 @@ impl GraphView {
self.queue_draw();
}
pub fn unselect_links(&self) {
let private = imp::GraphView::from_instance(self);
for link in private.links.borrow_mut().values() {
link.set_selected(false);
}
}
pub fn point_on_link(&self, point: &graphene::Point) -> Option<Link> {
let private = imp::GraphView::from_instance(self);
self.unselect_all();
for link in private.links.borrow_mut().values() {
if let Some((from_x, from_y, to_x, to_y)) = private.link_coordinates(link) {
let quad = graphene::Quad::new(
&graphene::Point::new(from_x as f32, from_y as f32 - link.thickness as f32),
&graphene::Point::new(to_x as f32, to_y as f32 - link.thickness as f32),
&graphene::Point::new(to_x as f32, to_y as f32 + link.thickness as f32),
&graphene::Point::new(from_x as f32, from_y as f32 + link.thickness as f32),
);
if quad.contains(point) {
link.toggle_selected();
self.queue_draw();
return Some(link.clone());
}
}
}
self.queue_draw();
None
}
/// Get the position of the specified node inside the graphview.
///
/// Returns `None` if the node is not in the graphview.
@ -579,7 +630,7 @@ impl GraphView {
self.queue_draw();
}
pub(super) fn link_exists(&self, new_link: &NodeLink) -> bool {
pub(super) fn link_exists(&self, new_link: &Link) -> bool {
let private = imp::GraphView::from_instance(self);
for link in private.links.borrow().values() {
@ -625,6 +676,13 @@ impl GraphView {
private.current_node_id.get()
}
pub fn update_current_node_id(&self, node_id: u32) {
let private = imp::GraphView::from_instance(self);
if node_id > private.current_node_id.get() {
private.current_node_id.set(node_id);
}
}
pub fn next_port_id(&self) -> u32 {
let private = imp::GraphView::from_instance(self);
private
@ -633,6 +691,13 @@ impl GraphView {
private.current_port_id.get()
}
pub fn update_current_port_id(&self, port_id: u32) {
let private = imp::GraphView::from_instance(self);
if port_id > private.current_port_id.get() {
private.current_port_id.set(port_id);
}
}
fn next_link_id(&self) -> u32 {
let private = imp::GraphView::from_instance(self);
private
@ -641,7 +706,15 @@ impl GraphView {
private.current_link_id.get()
}
pub fn update_current_link_id(&self, link_id: u32) {
let private = imp::GraphView::from_instance(self);
if link_id > private.current_link_id.get() {
private.current_link_id.set(link_id);
}
}
fn set_selected_port(&self, port: Option<&Port>) {
self.unselect_all();
let private = imp::GraphView::from_instance(self);
*private.port_selected.borrow_mut() = port.cloned();
}
@ -689,6 +762,36 @@ impl GraphView {
}
description
}
pub fn unselect_all(&self) {
self.unselect_nodes();
self.unselect_links();
self.queue_draw();
}
pub fn delete_selected(&self) {
let private = imp::GraphView::from_instance(self);
let mut link_id = None;
let mut node_id = None;
for link in private.links.borrow_mut().values() {
if link.selected() {
link_id = Some(link.id);
}
}
for node in private.nodes.borrow_mut().values() {
if node.selected() {
node_id = Some(node.id());
}
}
if let Some(id) = link_id {
self.remove_link(id);
}
if let Some(id) = node_id {
self.remove_node(id);
}
self.queue_draw();
}
//TO BE MOVED
pub fn render_gst(&self) -> String {
let nodes = self.all_nodes(NodeType::Source);
@ -761,7 +864,7 @@ impl GraphView {
let mut current_node: Option<Node> = None;
let mut current_port: Option<Port> = None;
let mut current_link: Option<NodeLink> = None;
let mut current_link: Option<Link> = None;
for e in parser {
match e {
Ok(XMLREvent::StartElement {
@ -841,14 +944,15 @@ impl GraphView {
let active: &String = attrs
.get::<String>(&String::from("active"))
.expect("Unable to find link state");
current_link = Some(NodeLink {
id: id.parse::<u32>().unwrap(),
node_from: node_from.parse::<u32>().unwrap(),
node_to: node_to.parse::<u32>().unwrap(),
port_from: port_from.parse::<u32>().unwrap(),
port_to: port_to.parse::<u32>().unwrap(),
active: active.parse::<bool>().unwrap(),
});
current_link = Some(Link::new(
id.parse::<u32>().unwrap(),
node_from.parse::<u32>().unwrap(),
node_to.parse::<u32>().unwrap(),
port_from.parse::<u32>().unwrap(),
port_to.parse::<u32>().unwrap(),
active.parse::<bool>().unwrap(),
false,
));
}
_ => println!("name unknown: {}", name),
}
@ -857,12 +961,13 @@ impl GraphView {
println!("closing {}", name);
match name.to_string().as_str() {
"Graph" => {
println!("Graph ended");
println!("Graph ended with success");
}
"Node" => {
if let Some(node) = current_node {
let id = node.id();
self.add_node(id, node);
self.update_current_node_id(id);
}
current_node = None;
}
@ -870,17 +975,21 @@ impl GraphView {
"Port" => {
if let Some(port) = current_port {
let node = current_node.clone();
let id = port.id();
node.expect("No current node, error...").add_port(
port.id(),
id,
&port.name(),
port.direction(),
);
self.update_current_port_id(id);
}
current_port = None;
}
"Link" => {
if let Some(link) = current_link {
let id = link.id;
self.add_link(link);
self.update_current_link_id(id);
}
current_link = None;
}

68
src/graphmanager/link.rs Normal file
View file

@ -0,0 +1,68 @@
// link.rs
//
// Copyright 2021 Tom A. Wagner <tom.a.wagner@protonmail.com>
// 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 std::cell::Cell;
#[derive(Debug, Clone)]
pub struct Link {
pub id: u32,
pub node_from: u32,
pub node_to: u32,
pub port_from: u32,
pub port_to: u32,
pub active: bool,
pub selected: Cell<bool>,
pub thickness: u32,
}
impl Link {
pub fn new(
id: u32,
node_from: u32,
node_to: u32,
port_from: u32,
port_to: u32,
active: bool,
selected: bool,
) -> Self {
Self {
id,
node_from,
node_to,
port_from,
port_to,
active,
selected: Cell::new(selected),
thickness: 4,
}
}
pub fn toggle_selected(&self) {
self.set_selected(!self.selected.get());
}
pub fn set_selected(&self, selected: bool) {
self.selected.set(selected);
}
pub fn selected(&self) -> bool {
self.selected.get()
}
}

View file

@ -1,8 +1,10 @@
mod graphview;
mod link;
mod node;
mod port;
pub use graphview::GraphView;
pub use link::Link;
pub use node::Node;
pub use node::NodeType;
pub use port::Port;

View file

@ -58,16 +58,21 @@ impl NodeType {
mod imp {
use super::*;
use gtk::Orientation;
use once_cell::unsync::OnceCell;
pub struct Node {
pub(super) grid: gtk::Grid,
pub(super) label: gtk::Label,
pub(super) layoutbox: gtk::Box,
pub(super) inputs: gtk::Box,
pub(super) outputs: gtk::Box,
pub(super) name: gtk::Label,
pub(super) description: gtk::Label,
pub(super) id: OnceCell<u32>,
pub(super) node_type: OnceCell<NodeType>,
pub(super) ports: RefCell<HashMap<u32, Port>>,
pub(super) num_ports_in: Cell<i32>,
pub(super) num_ports_out: Cell<i32>,
pub(super) properties: RefCell<HashMap<String, String>>,
pub(super) selected: Cell<bool>,
}
#[glib::object_subclass]
@ -82,23 +87,62 @@ mod imp {
}
fn new() -> Self {
let grid = gtk::Grid::new();
let label = gtk::Label::new(None);
let layoutbox = gtk::Box::new(Orientation::Vertical, 6);
let name_desc = gtk::Box::new(Orientation::Vertical, 6);
layoutbox.append(&name_desc);
let ports = gtk::Box::builder()
.orientation(Orientation::Horizontal)
.halign(gtk::Align::Start)
.spacing(10)
.margin_bottom(10)
.margin_top(10)
.build();
grid.attach(&label, 0, 0, 2, 1);
layoutbox.append(&ports);
let inputs = gtk::Box::builder()
.orientation(Orientation::Vertical)
.halign(gtk::Align::Start)
.spacing(10)
.build();
// Display a grab cursor when the mouse is over the label so the user knows the node can be dragged.
label.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
ports.append(&inputs);
let center = gtk::Box::builder()
.orientation(Orientation::Vertical)
.halign(gtk::Align::Center)
.hexpand(true)
.margin_start(20)
.margin_end(20)
.build();
ports.append(&center);
let outputs = gtk::Box::builder()
.orientation(Orientation::Vertical)
.halign(gtk::Align::End)
.spacing(10)
.build();
ports.append(&outputs);
let name = gtk::Label::new(None);
name_desc.append(&name);
let description = gtk::Label::new(None);
name_desc.append(&description);
// Display a grab cursor when the mouse is over the name so the user knows the node can be dragged.
name.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
Self {
grid,
label,
layoutbox,
inputs,
outputs,
name,
description,
id: OnceCell::new(),
node_type: OnceCell::new(),
ports: RefCell::new(HashMap::new()),
num_ports_in: Cell::new(0),
num_ports_out: Cell::new(0),
properties: RefCell::new(HashMap::new()),
selected: Cell::new(false),
}
}
}
@ -106,11 +150,11 @@ mod imp {
impl ObjectImpl for Node {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.grid.set_parent(obj);
self.layoutbox.set_parent(obj);
}
fn dispose(&self, _obj: &Self::Type) {
self.grid.unparent();
self.layoutbox.unparent();
}
}
@ -128,6 +172,7 @@ impl Node {
let private = imp::Node::from_instance(&res);
private.id.set(id).expect("Node id already set");
res.set_name(name);
res.add_css_class("node");
private
.node_type
.set(node_type)
@ -137,24 +182,36 @@ impl Node {
fn set_name(&self, name: &str) {
let self_ = imp::Node::from_instance(self);
self_.label.set_text(name);
self_.name.set_text(name);
println!("{}", name);
}
fn set_description(&self, description: &str) {
let self_ = imp::Node::from_instance(self);
self_.description.set_text(description);
println!("{}", description);
}
fn update_description(&self) {
let self_ = imp::Node::from_instance(self);
let mut description = String::from("");
for (name, value) in self_.properties.borrow().iter() {
description.push_str(&format!("{}:{}", name, value));
description.push('\n');
}
self.set_description(&description);
}
pub fn add_port(&mut self, id: u32, name: &str, direction: PortDirection) {
let private = imp::Node::from_instance(self);
let port = Port::new(id, name, direction);
match port.direction() {
PortDirection::Input => {
private
.grid
.attach(&port, 0, private.num_ports_in.get() + 1, 1, 1);
private.inputs.append(&port);
private.num_ports_in.set(private.num_ports_in.get() + 1);
}
PortDirection::Output => {
private
.grid
.attach(&port, 1, private.num_ports_out.get() + 1, 1, 1);
private.outputs.append(&port);
private.num_ports_out.set(private.num_ports_out.get() + 1);
}
_ => panic!("Port without direction"),
@ -202,12 +259,12 @@ impl Node {
pub fn name(&self) -> String {
let private = imp::Node::from_instance(self);
private.label.text().to_string()
private.name.text().to_string()
}
pub fn unique_name(&self) -> String {
let private = imp::Node::from_instance(self);
let mut unique_name = private.label.text().to_string();
let mut unique_name = private.name.text().to_string();
unique_name.push_str(&self.id().to_string());
unique_name
}
@ -221,6 +278,7 @@ impl Node {
let private = imp::Node::from_instance(self);
println!("{} {} updated", name, value);
private.properties.borrow_mut().insert(name, value);
self.update_description();
}
pub fn update_node_properties(&self, new_properties: &HashMap<String, String>) {
@ -233,4 +291,30 @@ impl Node {
let private = imp::Node::from_instance(self);
private.properties.borrow()
}
pub fn toggle_selected(&self) {
self.set_selected(!self.selected());
}
pub fn set_selected(&self, selected: bool) {
let private = imp::Node::from_instance(self);
private.selected.set(selected);
if selected {
self.add_css_class("node-selected");
} else {
self.remove_css_class("node-selected");
}
}
pub fn selected(&self) -> bool {
let private = imp::Node::from_instance(self);
private.selected.get()
}
pub fn unselect_all_ports(&self) {
let private = imp::Node::from_instance(self);
for port in private.ports.borrow_mut().values() {
port.set_selected(false);
}
}
}

View file

@ -22,6 +22,7 @@ use gtk::{
prelude::*,
subclass::prelude::*,
};
use std::cell::Cell;
use std::{borrow::Borrow, fmt};
#[derive(Debug, Clone, PartialEq, Copy)]
@ -61,6 +62,7 @@ mod imp {
pub(super) label: OnceCell<gtk::Label>,
pub(super) id: OnceCell<u32>,
pub(super) direction: OnceCell<PortDirection>,
pub(super) selected: Cell<bool>,
}
#[glib::object_subclass]
@ -110,26 +112,29 @@ glib::wrapper! {
impl Port {
pub fn new(id: u32, name: &str, direction: PortDirection) -> Self {
// Create the widget and initialize needed fields
let res: Self = glib::Object::new(&[]).expect("Failed to create Port");
let private = imp::Port::from_instance(&res);
let port: Self = glib::Object::new(&[]).expect("Failed to create Port");
port.add_css_class("port");
let private = imp::Port::from_instance(&port);
private.id.set(id).expect("Port id already set");
private.selected.set(false);
private
.direction
.set(direction)
.expect("Port direction already set");
if direction == PortDirection::Input {
port.add_css_class("port-in");
} else {
port.add_css_class("port-out");
}
let label = gtk::Label::new(Some(name));
label.set_parent(&res);
label.set_parent(&port);
private
.label
.set(label)
.expect("Port label was already set");
// Display a grab cursor when the mouse is over the port so the user knows it can be dragged to another port.
res.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
res
port
}
pub fn id(&self) -> u32 {
@ -147,4 +152,23 @@ impl Port {
let label = private.label.borrow().get().unwrap();
label.text().to_string()
}
pub fn toggle_selected(&self) {
self.set_selected(!self.selected());
}
pub fn set_selected(&self, selected: bool) {
let private = imp::Port::from_instance(self);
private.selected.set(selected);
if selected {
self.add_css_class("port-selected");
} else {
self.remove_css_class("port-selected");
}
}
pub fn selected(&self) -> bool {
let private = imp::Port::from_instance(self);
private.selected.get()
}
}