// graphview.rs // // Copyright 2021 Tom A. Wagner // Copyright 2021 Stéphane Cerveau // // This file is part of GraphManager // // SPDX-License-Identifier: GPL-3.0-only extern crate xml; use xml::reader::EventReader; use xml::reader::XmlEvent as XMLREvent; use xml::writer::EmitterConfig; use xml::writer::XmlEvent as XMLWEvent; use super::{ link::*, node::{Node, NodeType}, port::{Port, PortDirection, PortPresence}, property::PropertyExt, selection::SelectionExt, }; use once_cell::sync::Lazy; use std::io::Cursor; use gtk::{ gdk, glib::{self, clone, subclass::Signal}, graphene, gsk, prelude::*, subclass::prelude::*, }; use log::{debug, error, info, trace, warn}; use std::cell::RefMut; use std::{cmp::Ordering, collections::HashMap}; static GRAPHVIEW_STYLE: &str = include_str!("graphview.css"); pub static GRAPHVIEW_XML_VERSION: &str = "0.1"; const CANVAS_SIZE: f64 = 5000.0; mod imp { use super::*; use std::cell::{Cell, RefCell}; use log::warn; pub struct DragState { node: glib::WeakRef, /// This stores the offset of the pointer to the origin of the node, /// so that we can keep the pointer over the same position when moving the node /// /// The offset is normalized to the default zoom-level of 1.0. offset: graphene::Point, } #[derive(Default)] pub struct GraphView { pub(super) id: Cell, pub(super) nodes: RefCell>, pub(super) links: RefCell>, pub(super) current_node_id: Cell, pub(super) current_port_id: Cell, pub(super) current_link_id: Cell, pub(super) port_selected: RefCell>, pub(super) mouse_position: Cell<(f64, f64)>, pub dragged_node: RefCell>, pub hadjustment: RefCell>, pub vadjustment: RefCell>, pub zoom_factor: Cell, } #[glib::object_subclass] impl ObjectSubclass for GraphView { const NAME: &'static str = "GraphView"; type Type = super::GraphView; type ParentType = gtk::Widget; type Interfaces = (gtk::Scrollable,); fn class_init(klass: &mut Self::Class) { // The layout manager determines how child widgets are laid out. //klass.set_layout_manager_type::(); klass.set_css_name("graphview"); } } impl ObjectImpl for GraphView { fn constructed(&self) { let obj = self.obj(); self.parent_constructed(); self.obj().set_overflow(gtk::Overflow::Hidden); let drag_controller = gtk::GestureDrag::new(); drag_controller.connect_drag_begin(|drag_controller, x, y| { let widget = drag_controller .widget() .dynamic_cast::() .expect("drag-begin event is not on the GraphView"); let mut dragged_node = widget.imp().dragged_node.borrow_mut(); // pick() should at least return the widget itself. let target = widget .pick(x, y, gtk::PickFlags::DEFAULT) .expect("drag-begin pick() did not return a widget"); *dragged_node = if target.ancestor(Port::static_type()).is_some() { // The user targeted a port, so the dragging should be handled by the Port // component instead of here. None } else if let Some(target) = target.ancestor(Node::static_type()) { // The user targeted a Node without targeting a specific Port. // Drag the Node around the screen. let node = target.dynamic_cast_ref::().unwrap(); let Some(canvas_node_pos) = widget.node_position(node) else { return; }; let canvas_cursor_pos = widget .imp() .screen_space_to_canvas_space_transform() .transform_point(&graphene::Point::new(x as f32, y as f32)); Some(DragState { node: node.clone().downgrade(), offset: graphene::Point::new( canvas_cursor_pos.x() - canvas_node_pos.x(), canvas_cursor_pos.y() - canvas_node_pos.y(), ), }) } else { None } }); drag_controller.connect_drag_update(|drag_controller, x, y| { let widget = drag_controller .widget() .dynamic_cast::() .expect("drag-update event is not on the GraphView"); let dragged_node = widget.imp().dragged_node.borrow(); let Some(DragState { node, offset }) = dragged_node.as_ref() else { return; }; let Some(node) = node.upgrade() else { return }; let (start_x, start_y) = drag_controller .start_point() .expect("Drag has no start point"); let onscreen_node_origin = graphene::Point::new((start_x + x) as f32, (start_y + y) as f32); let transform = widget.imp().screen_space_to_canvas_space_transform(); let canvas_node_origin = transform.transform_point(&onscreen_node_origin); widget.move_node( &node, &graphene::Point::new( canvas_node_origin.x() - offset.x(), canvas_node_origin.y() - offset.y(), ), ); }); let gesture = gtk::GestureClick::new(); gesture.set_button(0); gesture.connect_pressed( clone!(@weak obj, @weak drag_controller => move |gesture, _n_press, x, y| { if gesture.current_button() == gdk::BUTTON_SECONDARY { let widget = drag_controller.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 Port"); let node = port.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::().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)]); } 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)]); } else { widget.unselect_all(); obj.emit_by_name::<()>("graph-right-clicked", &[&graphene::Point::new(x as f32,y as f32)]); } } else if gesture.current_button() == gdk::BUTTON_PRIMARY { let widget = drag_controller.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)); } } }), ); gesture.connect_released(clone!(@weak gesture, @weak obj, @weak drag_controller => move |_gesture, _n_press, x, y| { if gesture.current_button() == gdk::BUTTON_PRIMARY { let widget = drag_controller .widget() .dynamic_cast::() .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 port_clicked = target.dynamic_cast::().expect("click event is not on the Port"); if widget.port_is_linked(port_clicked.id()).is_none() { let selected_port = widget.selected_port().to_owned(); if let Some(mut port_from) = selected_port { debug!("Port {} is clicked at {}:{}", port_clicked.id(), x, y); let mut port_to = port_clicked; if widget.ports_compatible(&port_to) { let mut node_from = port_from.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::().expect("Unable to cast to Node"); let mut node_to = port_to.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::().expect("Unable to cast to Node"); info!("add link from port {} to {} ", port_from.id(), port_to.id()); if port_to.direction() == PortDirection::Output { debug!("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(widget.create_link( node_from.id(), node_to.id(), port_from.id(), port_to.id(), )); } widget.set_selected_port(None); } else { info!("add selected port id {}", port_clicked.id()); widget.set_selected_port(Some(&port_clicked)); } } else { // click to a linked port widget.set_selected_port(None); } } else { if let Some(target) = target.ancestor(Node::static_type()) { let node = target.dynamic_cast::().expect("click event is not on the Node"); info!(" node id {}", node.id()); if _n_press % 2 == 0 { info!("double clicked node id {}", node.id()); obj.emit_by_name::<()>("node-double-clicked", &[&node.id(), &graphene::Point::new(x as f32,y as f32)]); } } else if _n_press % 2 == 0 { if let Some(link) = widget.point_on_link(&graphene::Point::new(x.floor() as f32,y.floor() as f32)) { info!("double clicked link id {}", link.id()); obj.emit_by_name::<()>("link-double-clicked", &[&link.id(), &graphene::Point::new(x as f32,y as f32)]); } } else { info!("double click {}",widget.width()); } // Click to something else than a port widget.set_selected_port(None); } } } })); obj.add_controller(drag_controller); obj.add_controller(gesture); let event_motion = gtk::EventControllerMotion::new(); event_motion.connect_motion(glib::clone!(@weak obj => move |_e, x, y| { let graphview = obj; if graphview.selected_port().is_some() { graphview.set_mouse_position(x,y); graphview.queue_allocate(); } })); obj.add_controller(event_motion); let scroll_controller = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::BOTH_AXES); scroll_controller.connect_scroll(|eventcontroller, _, delta_y| { let event = eventcontroller.current_event().unwrap(); // We are inside the event handler, so it must have an event if event .modifier_state() .contains(gdk::ModifierType::CONTROL_MASK) { let widget = eventcontroller .widget() .downcast::() .unwrap(); widget.set_zoom_factor(widget.zoom_factor() + (0.1 * -delta_y), None); glib::Propagation::Stop } else { glib::Propagation::Proceed } }); self.obj().add_controller(scroll_controller); } fn dispose(&self) { self.nodes .borrow() .values() .for_each(|(node, _)| node.unparent()) } fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| { vec![ Signal::builder("port-right-clicked") .param_types([ u32::static_type(), u32::static_type(), graphene::Point::static_type(), ]) .build(), Signal::builder("node-right-clicked") .param_types([u32::static_type(), graphene::Point::static_type()]) .build(), Signal::builder("node-double-clicked") .param_types([u32::static_type(), graphene::Point::static_type()]) .build(), Signal::builder("graph-right-clicked") .param_types([graphene::Point::static_type()]) .build(), Signal::builder("graph-updated") .param_types( // returns graph ID [u32::static_type()], ) .build(), Signal::builder("node-added") .param_types( // returns graph ID and Node ID [u32::static_type(), u32::static_type()], ) .build(), Signal::builder("port-added") .param_types([u32::static_type(), u32::static_type(), u32::static_type()]) .build(), Signal::builder("link-double-clicked") .param_types([u32::static_type(), graphene::Point::static_type()]) .build(), ] }); SIGNALS.as_ref() } fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ glib::ParamSpecOverride::for_interface::("hadjustment"), glib::ParamSpecOverride::for_interface::("vadjustment"), glib::ParamSpecOverride::for_interface::("hscroll-policy"), glib::ParamSpecOverride::for_interface::("vscroll-policy"), glib::ParamSpecDouble::builder("zoom-factor") .minimum(0.3) .maximum(4.0) .default_value(1.0) .flags(glib::ParamFlags::CONSTRUCT | glib::ParamFlags::READWRITE) .build(), ] }); PROPERTIES.as_ref() } fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { "hadjustment" => self.hadjustment.borrow().to_value(), "vadjustment" => self.vadjustment.borrow().to_value(), "hscroll-policy" | "vscroll-policy" => gtk::ScrollablePolicy::Natural.to_value(), "zoom-factor" => self.zoom_factor.get().to_value(), _ => unimplemented!(), } } fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { let obj = self.obj(); match pspec.name() { "hadjustment" => { obj.set_adjustment(&obj, value.get().ok(), gtk::Orientation::Horizontal) } "vadjustment" => { obj.set_adjustment(&obj, value.get().ok(), gtk::Orientation::Vertical) } "hscroll-policy" | "vscroll-policy" => {} "zoom-factor" => { self.zoom_factor.set(value.get().unwrap()); obj.queue_allocate(); } _ => unimplemented!(), } } } impl WidgetImpl for GraphView { fn size_allocate(&self, _width: i32, _height: i32, baseline: i32) { let widget = &*self.obj(); let zoom_factor = self.zoom_factor.get(); for (node, point) in self.nodes.borrow().values() { let (_, natural_size) = node.preferred_size(); let transform = self .canvas_space_to_screen_space_transform() .translate(point); node.allocate( (natural_size.width() as f64 * zoom_factor).ceil() as i32, (natural_size.height() as f64 * zoom_factor).ceil() as i32, baseline, Some(transform), ); } if let Some(ref hadjustment) = *self.hadjustment.borrow() { widget.set_adjustment_values(widget, hadjustment, gtk::Orientation::Horizontal); } if let Some(ref vadjustment) = *self.vadjustment.borrow() { widget.set_adjustment_values(widget, vadjustment, gtk::Orientation::Vertical); } } fn snapshot(&self, snapshot: >k::Snapshot) { /* FIXME: A lot of hardcoded values in here. Try to use relative units (em) and colours from the theme as much as possible. */ let widget = &*self.obj(); let alloc = widget.allocation(); // Draw all children // Draw all visible children self.nodes .borrow() .values() // Cull nodes from rendering when they are outside the visible canvas area .filter(|(node, _)| alloc.intersect(&node.allocation()).is_some()) .for_each(|(node, _)| widget.snapshot_child(node, snapshot)); for link in self.links.borrow().values() { if let Some((from_x, from_y, to_x, to_y)) = self.link_coordinates(link) { self.draw_link( snapshot, link.active(), link.selected(), link.name().as_str(), link.thickness as f64, &graphene::Point::new(from_x as f32, from_y as f32), &graphene::Point::new(to_x as f32, to_y as f32), ); } else { warn!("Could not get link coordinates: {:?}", link); } } if self.port_selected.borrow().is_some() { let port = self.port_selected.borrow(); let port = port.as_ref().unwrap(); let node = port .ancestor(Node::static_type()) .expect("Unable to reach parent") .dynamic_cast::() .expect("Unable to cast to Node"); let (from_x, from_y) = self.link_from_coordinates(node.id(), port.id()); let (to_x, to_y) = self.mouse_position.get(); self.draw_link( snapshot, false, false, "", 2.0, &graphene::Point::new(from_x as f32, from_y as f32), &graphene::Point::new(to_x as f32, to_y as f32), ); } } } impl ScrollableImpl for GraphView {} impl GraphView { /// Returns a [`gsk::Transform`] matrix that can translate from canvas space to screen space. /// /// Canvas space is non-zoomed, and (0, 0) is fixed at the middle of the graph. \ /// Screen space is zoomed and adjusted for scrolling, (0, 0) is at the top-left corner of the window. /// /// This is the inverted form of [`Self::screen_space_to_canvas_space_transform()`]. fn canvas_space_to_screen_space_transform(&self) -> gsk::Transform { let hadj = self.hadjustment.borrow().as_ref().unwrap().value(); let vadj = self.vadjustment.borrow().as_ref().unwrap().value(); let zoom_factor = self.zoom_factor.get(); gsk::Transform::new() .translate(&graphene::Point::new(-hadj as f32, -vadj as f32)) .scale(zoom_factor as f32, zoom_factor as f32) } /// Returns a [`gsk::Transform`] matrix that can translate from screen space to canvas space. /// /// This is the inverted form of [`Self::canvas_space_to_screen_space_transform()`], see that function for a more detailed explanation. fn screen_space_to_canvas_space_transform(&self) -> gsk::Transform { self.canvas_space_to_screen_space_transform() .invert() .unwrap() } fn link_from_coordinates(&self, node_from: u32, port_from: u32) -> (f64, f64) { let nodes = self.nodes.borrow(); let widget = &*self.obj(); let from_node = nodes .get(&node_from) .unwrap_or_else(|| (panic!("Unable to get node from {}", node_from))); let from_port = from_node .0 .port(port_from) .unwrap_or_else(|| panic!("Unable to get port from {}", port_from)); let (x, y) = from_port .translate_coordinates( widget, (from_port.width() / 2) as f64, (from_port.height() / 2) as f64, ) .unwrap(); (x, y) } fn link_to_coordinates(&self, node_to: u32, port_to: u32) -> (f64, f64) { let nodes = self.nodes.borrow(); let widget = &*self.obj(); let to_node = nodes .get(&node_to) .unwrap_or_else(|| panic!("Unable to get node to {}", node_to)); let to_port = to_node .0 .port(port_to) .unwrap_or_else(|| panic!("Unable to get port to {}", port_to)); let (x, y) = to_port .translate_coordinates( widget, (to_port.width() / 2) as f64, (to_port.height() / 2) as f64, ) .unwrap(); (x, y) } /// Retrieves coordinates for the drawn link to start at and to end at. /// /// # Returns /// `Some((from_x, from_y, to_x, to_y))` if all objects the links refers to exist as widgets. pub fn link_coordinates(&self, link: &Link) -> Option<(f64, f64, f64, f64)> { let (from_x, from_y) = self.link_from_coordinates(link.node_from, link.port_from); let (to_x, to_y) = self.link_to_coordinates(link.node_to, link.port_to); Some((from_x, from_y, to_x, to_y)) } #[allow(clippy::too_many_arguments)] fn draw_link( &self, snapshot: >k::Snapshot, active: bool, selected: bool, name: &str, thickness: f64, point_from: &graphene::Point, point_to: &graphene::Point, ) { let alloc = self.obj().allocation(); let link_cr = snapshot.append_cairo(&graphene::Rect::new( 0.0, 0.0, alloc.width() as f32, alloc.height() as f32, )); link_cr.set_line_width(thickness); // Use dashed line for inactive links, full line otherwise. if active { link_cr.set_dash(&[], 0.0); } else { link_cr.set_dash(&[10.0, 5.0], 0.0); } if 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(point_from.x() as f64, point_from.y() as f64); link_cr.line_to(point_to.x() as f64, point_to.y() as f64); link_cr.set_line_width(2.0); if let Err(e) = link_cr.stroke() { warn!("Failed to draw graphview links: {}", e); }; trace!("the link name is {}", name); if !name.is_empty() { let x = (point_from.x() + point_to.x()) / 2.0 + 20.0; let y = (point_from.y() + point_to.y()) / 2.0 + 20.0; link_cr.move_to(x as f64, y as f64); let _ = link_cr.show_text(name); } } } } glib::wrapper! { pub struct GraphView(ObjectSubclass) @extends gtk::Widget; } impl GraphView { pub const ZOOM_MIN: f64 = 0.3; pub const ZOOM_MAX: f64 = 4.0; /// Create a new graphview /// /// # Returns /// Graphview object pub fn new() -> Self { // Load CSS from the STYLE variable. let provider = gtk::CssProvider::new(); provider.load_from_data(GRAPHVIEW_STYLE); gtk::style_context_add_provider_for_display( >k::gdk::Display::default().expect("Error initializing gtk css provider."), &provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, ); glib::Object::new::() } /// Set graphview id /// pub fn set_id(&self, id: u32) { let private = imp::GraphView::from_obj(self); private.id.set(id) } /// Retrieves the graphview id /// pub fn id(&self) -> u32 { let private = imp::GraphView::from_obj(self); private.id.get() } /// Clear the graphview /// pub fn clear(&self) { self.remove_all_nodes(); } pub fn zoom_factor(&self) -> f64 { self.property("zoom-factor") } pub fn set_zoom_factor(&self, zoom_factor: f64, anchor: Option<(f64, f64)>) { let private = imp::GraphView::from_obj(self); let zoom_factor = zoom_factor.clamp(Self::ZOOM_MIN, Self::ZOOM_MAX); let (anchor_x_screen, anchor_y_screen) = anchor.unwrap_or_else(|| { ( self.allocation().width() as f64 / 2.0, self.allocation().height() as f64 / 2.0, ) }); let old_zoom = private.zoom_factor.get(); let hadjustment_ref = private.hadjustment.borrow(); let vadjustment_ref = private.vadjustment.borrow(); let hadjustment = hadjustment_ref.as_ref().unwrap(); let vadjustment = vadjustment_ref.as_ref().unwrap(); let x_total = (anchor_x_screen + hadjustment.value()) / old_zoom; let y_total = (anchor_y_screen + vadjustment.value()) / old_zoom; let new_hadjustment = x_total * zoom_factor - anchor_x_screen; let new_vadjustment = y_total * zoom_factor - anchor_y_screen; hadjustment.set_value(new_hadjustment); vadjustment.set_value(new_vadjustment); self.set_property("zoom-factor", zoom_factor); info!("zoom factor {}", zoom_factor); } // Node /// Create a new node with a new id /// pub fn create_node(&self, name: &str, node_type: NodeType) -> Node { let id = self.next_node_id(); self.create_node_with_id(id, name, node_type) } /// Create a new node and add it to the graphview with input/output port number. /// pub fn create_node_with_port( &self, name: &str, node_type: NodeType, input: u32, output: u32, ) -> Node { let mut node = self.create_node(name, node_type); let _i = 0; for _i in 0..input { let port = self.create_port("in", PortDirection::Input, PortPresence::Always); self.add_port_to_node(&mut node, port); } let _i = 0; for _i in 0..output { let port = self.create_port("out", PortDirection::Output, PortPresence::Always); self.add_port_to_node(&mut node, port); } node } /// Add node to the graphview without port /// pub fn add_node(&self, node: Node) { let private = imp::GraphView::from_obj(self); node.set_parent(self); // Place widgets in columns of 3, growing down let x = if let Some(node_type) = node.node_type() { match node_type { NodeType::Source => 20.0, NodeType::Transform => 320.0, NodeType::Sink => 620.0, _ => 20.0, } } else { 420.0 }; let y = private .nodes .borrow() .values() .map(|node| { // Map nodes to their locations let point = self.node_position(&node.0.clone().upcast()).unwrap(); (point.x(), point.y()) }) .filter(|(x2, _)| { // Only look for other nodes that have a similar x coordinate (x - x2).abs() < 50.0 }) .max_by(|y1, y2| { // Get max in column y1.partial_cmp(y2).unwrap_or(Ordering::Equal) }) .map_or(20_f32, |(_x, y)| y + 120.0); let node_id = node.id(); private .nodes .borrow_mut() .insert(node.id(), (node, graphene::Point::new(x, y))); self.emit_by_name::<()>("node-added", &[&private.id.get(), &node_id]); self.graph_updated(); } /// Remove node from the graphview /// pub fn remove_node(&self, id: u32) { let private = imp::GraphView::from_obj(self); if let Some(node) = private.nodes.borrow_mut().remove(&id) { while let Some(link_id) = self.node_is_linked(node.0.id()) { info!("Remove link id {}", link_id); private.links.borrow_mut().remove(&link_id); } node.0.unparent(); } else { warn!("Tried to remove non-existent node (id={}) from graph", id); } } /// Select all nodes according to the NodeType /// /// Returns a vector of nodes pub fn all_nodes(&self, node_type: NodeType) -> Vec { let private = imp::GraphView::from_obj(self); let nodes = private.nodes.borrow(); let nodes_list: Vec<_> = nodes .iter() .filter(|(_, node)| { *node.0.node_type().unwrap() == node_type || node_type == NodeType::All }) .map(|(_, node)| node.0.clone()) .collect(); nodes_list } /// Get the node with the specified node id inside the graphview. /// /// Returns `None` if the node is not in the graphview. pub fn node(&self, id: u32) -> Option { let private = imp::GraphView::from_obj(self); if let Some(node) = private.nodes.borrow().get(&id).cloned() { Some(node.0) } else { None } } /// Get the node with the specified node name inside the graphview. /// /// Returns `None` if the node is not in the graphview. pub fn node_by_unique_name(&self, unique_name: &str) -> Option { let private = imp::GraphView::from_obj(self); for node in private.nodes.borrow().values() { if node.0.unique_name() == unique_name { return Some(node.0.clone()); } } None } /// Remove all the nodes from the graphview /// pub fn remove_all_nodes(&self) { let private = imp::GraphView::from_obj(self); let nodes_list = self.all_nodes(NodeType::All); for node in nodes_list { self.remove_node(node.id()); } private.current_node_id.set(0); private.current_port_id.set(0); private.current_link_id.set(0); } /// Check if the node is linked /// /// Returns Some(link id) or `None` if the node is not linked. pub fn node_is_linked(&self, node_id: u32) -> Option { let private = imp::GraphView::from_obj(self); for (key, link) in private.links.borrow().iter() { if link.node_from == node_id || link.node_to == node_id { return Some(*key); } } None } /// Get the position of the specified node inside the graphview. /// /// Returns `None` if the node is not in the graphview. pub(super) fn node_position(&self, node: &Node) -> Option { self.imp() .nodes .borrow() .get(&node.id()) .map(|(_, point)| *point) } // Port /// Create a new port with a new id /// pub fn create_port( &self, name: &str, direction: PortDirection, presence: PortPresence, ) -> Port { let id = self.next_port_id(); info!("Create a port with port id {}", id); self.create_port_with_id(id, name, direction, presence) } /// Add the port with id from node with id. /// pub fn add_port_to_node(&self, node: &mut Node, port: Port) { let private = imp::GraphView::from_obj(self); let port_id = port.id(); node.add_port(port); self.emit_by_name::<()>("port-added", &[&private.id.get(), &node.id(), &port_id]); } /// Check if the port with id from node with id can be removed. /// /// Return true if the port presence is not always. pub fn can_remove_port(&self, node_id: u32, port_id: u32) -> bool { let private = imp::GraphView::from_obj(self); if let Some(node) = private.nodes.borrow().get(&node_id) { return node.0.can_remove_port(port_id); } warn!("Unable to find a node with the id {}", node_id); false } /// Remove the port with id from node with id. /// pub fn remove_port(&self, node_id: u32, port_id: u32) { let private = imp::GraphView::from_obj(self); if let Some(node) = private.nodes.borrow().get(&node_id) { if let Some(link_id) = self.port_is_linked(port_id) { self.remove_link(link_id); } node.0.remove_port(port_id); } } /// Check if the port is linked /// /// Returns Some(link id) or `None` if the port is not linked. pub fn port_is_linked(&self, port_id: u32) -> Option { let private = imp::GraphView::from_obj(self); for (key, link) in private.links.borrow().iter() { if link.port_from == port_id || link.port_to == port_id { return Some(*key); } } None } // Link /// Create a new link with a new id /// pub fn create_link( &self, node_from_id: u32, node_to_id: u32, port_from_id: u32, port_to_id: u32, ) -> Link { self.create_link_with_id( self.next_link_id(), node_from_id, node_to_id, port_from_id, port_to_id, ) } /// Add a link to the graphView /// pub fn add_link(&self, link: Link) { let private = imp::GraphView::from_obj(self); if !self.link_exists(&link) { private.links.borrow_mut().insert(link.id, link); self.graph_updated(); } } /// Set the link state with ink id and link state (boolean) /// pub fn set_link_state(&self, link_id: u32, active: bool) { let private = imp::GraphView::from_obj(self); if let Some(link) = private.links.borrow_mut().get_mut(&link_id) { link.set_active(active); self.queue_draw(); } else { warn!("Link state changed on unknown link (id={})", link_id); } } /// Select all nodes according to the NodeType /// /// Returns a vector of links pub fn all_links(&self, link_state: bool) -> Vec { let private = imp::GraphView::from_obj(self); let links = private.links.borrow(); let links_list: Vec<_> = links .iter() .filter(|(_, link)| link.active() == link_state) .map(|(_, node)| node.clone()) .collect(); links_list } /// Get the link with the specified link id inside the graphview. /// /// Returns `None` if the link is not in the graphview. pub fn link(&self, id: u32) -> Option { let private = imp::GraphView::from_obj(self); private.links.borrow().get(&id).cloned() } /// Set the link state with ink id and link state (boolean) /// pub fn set_link_name(&self, link_id: u32, name: &str) { let private = imp::GraphView::from_obj(self); let mut updated = false; if let Some(link) = private.links.borrow_mut().get_mut(&link_id) { link.set_name(name); self.queue_draw(); updated = true; } else { warn!("Link name changed on unknown link (id={})", link_id); } if updated { self.graph_updated(); } } /// Retrieves the node/port id connected to the input port id /// pub fn port_connected_to(&self, port_id: u32) -> Option<(u32, u32)> { let private = imp::GraphView::from_obj(self); for (_id, link) in private.links.borrow().iter() { if port_id == link.port_from { return Some((link.port_to, link.node_to)); } } None } /// Retrieves the link connected to the port id /// pub fn port_link(&self, port_id: u32) -> Option { let private = imp::GraphView::from_obj(self); for (_id, link) in private.links.borrow().iter() { if port_id == link.port_from { return Some(link.clone()); } } None } /// Delete the selected element (link, node, port) /// pub fn delete_selected(&self) { let private = imp::GraphView::from_obj(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.0.selected() { node_id = Some(node.0.id()); } } if let Some(id) = link_id { self.remove_link(id); } if let Some(id) = node_id { self.remove_node(id); } self.graph_updated(); } /// Render the graph with XML format in a buffer /// pub fn render_xml(&self) -> anyhow::Result> { let private = imp::GraphView::from_obj(self); let mut buffer = Vec::new(); let mut writer = EmitterConfig::new() .perform_indent(true) .create_writer(&mut buffer); writer.write( XMLWEvent::start_element("Graph") .attr("id", &private.id.get().to_string()) .attr("version", GRAPHVIEW_XML_VERSION), )?; //Get the nodes for node in self.all_nodes(NodeType::All) { writer.write( XMLWEvent::start_element("Node") .attr("name", &node.name()) .attr("id", &node.id().to_string()) .attr("type", &node.node_type().unwrap().to_string()) .attr("pos_x", &node.position().0.to_string()) .attr("pos_y", &node.position().1.to_string()) .attr("light", &node.light().to_string()), )?; for port in node.ports().values() { writer.write( XMLWEvent::start_element("Port") .attr("name", &port.name()) .attr("id", &port.id().to_string()) .attr("direction", &port.direction().to_string()) .attr("presence", &port.presence().to_string()), )?; for (name, value) in port.properties().iter() { writer.write( XMLWEvent::start_element("Property") .attr("name", name) .attr("value", value), )?; writer.write(XMLWEvent::end_element())?; } writer.write(XMLWEvent::end_element())?; } for (name, value) in node.properties().iter() { writer.write( XMLWEvent::start_element("Property") .attr("name", name) .attr("value", value), )?; writer.write(XMLWEvent::end_element())?; } writer.write(XMLWEvent::end_element())?; } //Get the link and write it. for (_id, link) in private.links.borrow().iter() { writer.write( XMLWEvent::start_element("Link") .attr("id", &link.id.to_string()) .attr("node_from", &link.node_from.to_string()) .attr("node_to", &link.node_to.to_string()) .attr("port_from", &link.port_from.to_string()) .attr("port_to", &link.port_to.to_string()) .attr("name", &link.name()) .attr("active", &link.active().to_string()), )?; writer.write(XMLWEvent::end_element())?; } writer.write(XMLWEvent::end_element())?; Ok(buffer) } /// Load the graph from a file with XML format /// pub fn load_from_xml(&self, buffer: Vec) -> anyhow::Result<()> { self.clear(); let file = Cursor::new(buffer); let parser = EventReader::new(file); let mut current_node: Option = None; let mut current_node_properties: HashMap = HashMap::new(); let mut current_port: Option = None; let mut current_port_properties: HashMap = HashMap::new(); let mut current_link: Option = None; for e in parser { match e { Ok(XMLREvent::StartElement { ref name, ref attributes, .. }) => { trace!("Found XLM element={}", name); let mut attrs = HashMap::new(); attributes.iter().for_each(|a| { attrs.insert(a.name.to_string(), a.value.to_string()); }); match name.to_string().as_str() { "Graph" => { trace!("New graph detected"); if let Some(id) = attrs.get::(&String::from("id")) { self.set_id(id.parse::().expect("id should be an u32")); } if let Some(version) = attrs.get::(&"version".to_string()) { info!("Found file format version: {}", version); } else { warn!("No file format version found"); } } "Node" => { let id = attrs .get::(&String::from("id")) .expect("Unable to find node id"); let name = attrs .get::(&String::from("name")) .expect("Unable to find node name"); let node_type: &String = attrs .get::(&String::from("type")) .expect("Unable to find node type"); let default_value = String::from("0"); let pos_x: &String = attrs .get::(&String::from("pos_x")) .unwrap_or(&default_value); let pos_y: &String = attrs .get::(&String::from("pos_y")) .unwrap_or(&default_value); let default_value = String::from("false"); let light: &String = attrs .get::(&String::from("light")) .unwrap_or(&default_value); let node = self.create_node_with_id( id.parse::().unwrap(), name, NodeType::from_str(node_type.as_str()), ); node.set_position( pos_x.parse::().unwrap(), pos_y.parse::().unwrap(), ); node.set_light(light.parse::().unwrap()); current_node = Some(node); } "Property" => { let name = attrs .get::(&String::from("name")) .expect("Unable to find property name"); let value: &String = attrs .get::(&String::from("value")) .expect("Unable to find property value"); if current_port.is_some() { current_port_properties.insert(name.to_string(), value.to_string()); } else if current_node.is_some() { info!("add property to node {}={}", name, value); current_node_properties.insert(name.to_string(), value.to_string()); } } "Port" => { let id = attrs .get::(&String::from("id")) .expect("Unable to find port id"); let name = attrs .get::(&String::from("name")) .expect("Unable to find port name"); let direction: &String = attrs .get::(&String::from("direction")) .expect("Unable to find port direction"); let default_value = PortPresence::Always.to_string(); let presence: &String = attrs .get::(&String::from("presence")) .unwrap_or(&default_value); current_port = Some(self.create_port_with_id( id.parse::().unwrap(), name, PortDirection::from_str(direction), PortPresence::from_str(presence), )); } "Link" => { let id = attrs .get::(&String::from("id")) .expect("Unable to find link id"); let node_from = attrs .get::(&String::from("node_from")) .expect("Unable to find link node_from"); let node_to = attrs .get::(&String::from("node_to")) .expect("Unable to find link node_to"); let port_from = attrs .get::(&String::from("port_from")) .expect("Unable to find link port_from"); let port_to = attrs .get::(&String::from("port_to")) .expect("Unable to find link port_to"); let active: &String = attrs .get::(&String::from("active")) .expect("Unable to find link state"); let default_value = String::from(""); let name: &String = attrs .get::(&String::from("name")) .unwrap_or(&default_value); let link = self.create_link_with_id( id.parse::().unwrap(), node_from.parse::().unwrap(), node_to.parse::().unwrap(), port_from.parse::().unwrap(), port_to.parse::().unwrap(), ); link.set_active(active.parse::().unwrap()); link.set_name(name.parse::().unwrap().as_str()); current_link = Some(link); } _ => warn!("name unknown: {}", name), } } Ok(XMLREvent::EndElement { name }) => { trace!("closing {}", name); match name.to_string().as_str() { "Graph" => { trace!("Graph ended with success"); } "Node" => { if let Some(node) = current_node { let id = node.id(); let position = graphene::Point::new(node.position().0, node.position().1); node.update_properties(¤t_node_properties); current_node_properties.clear(); self.add_node(node); if let Some(node) = self.node(id) { self.move_node(&node, &position); } self.update_current_node_id(id); } current_node = None; } "Property" => {} "Port" => { if let Some(port) = current_port { if let Some(mut node) = current_node.clone() { let id = port.id(); port.update_properties(¤t_port_properties); self.add_port_to_node(&mut node, port); current_port_properties.clear(); 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; } _ => warn!("name unknown: {}", name), } } Err(e) => { error!("Error: {}", e); break; } _ => {} } } Ok(()) } //Private fn create_node_with_id(&self, id: u32, name: &str, node_type: NodeType) -> Node { Node::new(id, name, node_type) } fn create_port_with_id( &self, id: u32, name: &str, direction: PortDirection, presence: PortPresence, ) -> Port { Port::new(id, name, direction, presence) } fn create_link_with_id( &self, link_id: u32, node_from_id: u32, node_to_id: u32, port_from_id: u32, port_to_id: u32, ) -> Link { Link::new(link_id, node_from_id, node_to_id, port_from_id, port_to_id) } fn remove_link(&self, id: u32) { let private = imp::GraphView::from_obj(self); let mut links = private.links.borrow_mut(); links.remove(&id); self.queue_draw(); } fn update_current_link_id(&self, link_id: u32) { let private = imp::GraphView::from_obj(self); if link_id > private.current_link_id.get() { private.current_link_id.set(link_id); } } fn link_exists(&self, new_link: &Link) -> bool { let private = imp::GraphView::from_obj(self); for link in private.links.borrow().values() { if (new_link.port_from == link.port_from && new_link.port_to == link.port_to) || (new_link.port_to == link.port_from && new_link.port_from == link.port_to) { warn!("link already existing"); return true; } } false } fn move_node(&self, widget: &Node, point: &graphene::Point) { let mut nodes = self.imp().nodes.borrow_mut(); let node = nodes .get_mut(&widget.id()) .expect("Node is not on the graph"); node.1 = graphene::Point::new( point.x().clamp( -(CANVAS_SIZE / 2.0) as f32, (CANVAS_SIZE / 2.0) as f32 - widget.width() as f32, ), point.y().clamp( -(CANVAS_SIZE / 2.0) as f32, (CANVAS_SIZE / 2.0) as f32 - widget.height() as f32, ), ); // we don't need to redraw the full graph everytime. self.queue_allocate(); } fn unselect_nodes(&self) { let private = imp::GraphView::from_obj(self); for node in private.nodes.borrow_mut().values() { node.0.set_selected(false); node.0.unselect_all_ports(); } } fn update_current_node_id(&self, node_id: u32) { let private = imp::GraphView::from_obj(self); if node_id > private.current_node_id.get() { private.current_node_id.set(node_id); } } fn unselect_links(&self) { let private = imp::GraphView::from_obj(self); for link in private.links.borrow_mut().values() { link.set_selected(false); } } fn unselect_all(&self) { self.unselect_nodes(); self.unselect_links(); self.queue_draw(); } fn point_on_link(&self, point: &graphene::Point) -> Option { let private = imp::GraphView::from_obj(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 } fn graph_updated(&self) { let private = imp::GraphView::from_obj(self); self.queue_allocate(); self.emit_by_name::<()>("graph-updated", &[&private.id.get()]); } fn next_node_id(&self) -> u32 { let private = imp::GraphView::from_obj(self); private .current_node_id .set(private.current_node_id.get() + 1); private.current_node_id.get() } fn next_port_id(&self) -> u32 { let private = imp::GraphView::from_obj(self); private .current_port_id .set(private.current_port_id.get() + 1); private.current_port_id.get() } fn next_link_id(&self) -> u32 { let private = imp::GraphView::from_obj(self); private .current_link_id .set(private.current_link_id.get() + 1); private.current_link_id.get() } fn set_selected_port(&self, port: Option<&Port>) { if port.is_some() { self.unselect_all(); } let private = imp::GraphView::from_obj(self); *private.port_selected.borrow_mut() = port.cloned(); } fn selected_port(&self) -> RefMut> { let private = imp::GraphView::from_obj(self); private.port_selected.borrow_mut() } fn set_mouse_position(&self, x: f64, y: f64) { let private = imp::GraphView::from_obj(self); private.mouse_position.set((x, y)); } fn ports_compatible(&self, to_port: &Port) -> bool { let current_port = self.selected_port().to_owned(); if let Some(from_port) = current_port { let from_node = from_port .ancestor(Node::static_type()) .expect("Unable to reach parent") .dynamic_cast::() .expect("Unable to cast to Node"); let to_node = to_port .ancestor(Node::static_type()) .expect("Unable to reach parent") .dynamic_cast::() .expect("Unable to cast to Node"); let res = from_port.id() != to_port.id() && from_port.direction() != to_port.direction() && from_node.id() != to_node.id(); if !res { warn!("Unable add the following link"); } return res; } false } fn update_current_port_id(&self, port_id: u32) { let private = imp::GraphView::from_obj(self); if port_id > private.current_port_id.get() { private.current_port_id.set(port_id); } } fn set_adjustment( &self, obj: &super::GraphView, adjustment: Option<>k::Adjustment>, orientation: gtk::Orientation, ) { let private = imp::GraphView::from_obj(self); match orientation { gtk::Orientation::Horizontal => *private.hadjustment.borrow_mut() = adjustment.cloned(), gtk::Orientation::Vertical => *private.vadjustment.borrow_mut() = adjustment.cloned(), _ => unimplemented!(), } if let Some(adjustment) = adjustment { adjustment.connect_value_changed(clone!(@weak obj => move |_| obj.queue_allocate() )); } } fn set_adjustment_values( &self, obj: &super::GraphView, adjustment: >k::Adjustment, orientation: gtk::Orientation, ) { let private = imp::GraphView::from_obj(self); let size = match orientation { gtk::Orientation::Horizontal => obj.width(), gtk::Orientation::Vertical => obj.height(), _ => unimplemented!(), }; let zoom_factor = private.zoom_factor.get(); adjustment.configure( adjustment.value(), -(CANVAS_SIZE / 2.0) * zoom_factor, (CANVAS_SIZE / 2.0) * zoom_factor, (f64::from(size) * 0.1) * zoom_factor, (f64::from(size) * 0.9) * zoom_factor, f64::from(size) * zoom_factor, ); } } impl Default for GraphView { fn default() -> Self { Self::new() } }