From a0bb503b27bf25146f372e258f653c4c40b8e5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Cerveau?= Date: Mon, 13 Dec 2021 18:15:48 +0100 Subject: [PATCH] graphview: can now select nodes and links Nodes and links can be selected and deleted --- TODO.md | 6 +- src/app.rs | 14 ++- src/gps.ui | 3 +- src/graphmanager/graphview.css | 30 ++++- src/graphmanager/graphview.rs | 215 +++++++++++++++++++++++++-------- src/graphmanager/link.rs | 68 +++++++++++ src/graphmanager/mod.rs | 2 + src/graphmanager/node.rs | 124 ++++++++++++++++--- src/graphmanager/port.rs | 40 ++++-- 9 files changed, 409 insertions(+), 93 deletions(-) create mode 100644 src/graphmanager/link.rs diff --git a/TODO.md b/TODO.md index e8ab949..136b2e1 100644 --- a/TODO.md +++ b/TODO.md @@ -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. diff --git a/src/app.rs b/src/app.rs index be40fc5..4446af4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -232,6 +232,18 @@ impl GPSApp { 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(); @@ -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(); })); diff --git a/src/gps.ui b/src/gps.ui index 1a65482..be1ff34 100644 --- a/src/gps.ui +++ b/src/gps.ui @@ -40,8 +40,7 @@
_Delete node - app.node.delete - <primary>n + app.delete _Request pad diff --git a/src/graphmanager/graphview.css b/src/graphmanager/graphview.css index bc8c5f2..37d22e6 100644 --- a/src/graphmanager/graphview.css +++ b/src/graphmanager/graphview.css @@ -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 { diff --git a/src/graphmanager/graphview.rs b/src/graphmanager/graphview.rs index c396193..5475c76 100644 --- a/src/graphmanager/graphview.rs +++ b/src/graphmanager/graphview.rs @@ -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>, - pub(super) links: RefCell>, + pub(super) links: RefCell>, pub(super) current_node_id: Cell, pub(super) current_port_id: Cell, pub(super) current_link_id: Cell, @@ -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::().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::() + .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::().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::().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 { + pub fn all_links(&self) -> Vec { 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 { + 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 = None; let mut current_port: Option = None; - let mut current_link: Option = None; + let mut current_link: Option = None; for e in parser { match e { Ok(XMLREvent::StartElement { @@ -841,14 +944,15 @@ impl GraphView { let active: &String = attrs .get::(&String::from("active")) .expect("Unable to find link state"); - current_link = Some(NodeLink { - id: id.parse::().unwrap(), - node_from: node_from.parse::().unwrap(), - node_to: node_to.parse::().unwrap(), - port_from: port_from.parse::().unwrap(), - port_to: port_to.parse::().unwrap(), - active: active.parse::().unwrap(), - }); + current_link = Some(Link::new( + id.parse::().unwrap(), + node_from.parse::().unwrap(), + node_to.parse::().unwrap(), + port_from.parse::().unwrap(), + port_to.parse::().unwrap(), + active.parse::().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; } diff --git a/src/graphmanager/link.rs b/src/graphmanager/link.rs new file mode 100644 index 0000000..b58645a --- /dev/null +++ b/src/graphmanager/link.rs @@ -0,0 +1,68 @@ +// link.rs +// +// Copyright 2021 Tom A. Wagner +// Copyright 2021 Stéphane Cerveau +// +// 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 . +// +// 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, + 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() + } +} diff --git a/src/graphmanager/mod.rs b/src/graphmanager/mod.rs index 910f655..e5b529d 100644 --- a/src/graphmanager/mod.rs +++ b/src/graphmanager/mod.rs @@ -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; diff --git a/src/graphmanager/node.rs b/src/graphmanager/node.rs index e96eddc..3d4cc7d 100644 --- a/src/graphmanager/node.rs +++ b/src/graphmanager/node.rs @@ -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, pub(super) node_type: OnceCell, pub(super) ports: RefCell>, pub(super) num_ports_in: Cell, pub(super) num_ports_out: Cell, pub(super) properties: RefCell>, + pub(super) selected: Cell, } #[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(¢er); + 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) { @@ -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); + } + } } diff --git a/src/graphmanager/port.rs b/src/graphmanager/port.rs index 890d5dc..e268990 100644 --- a/src/graphmanager/port.rs +++ b/src/graphmanager/port.rs @@ -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, pub(super) id: OnceCell, pub(super) direction: OnceCell, + pub(super) selected: Cell, } #[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() + } }