12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142 |
- /********************************************************************
- # Copyright 2014-2022 Daniel 'grindhold' Brendle
- #
- # This file is part of libgtkflow.
- #
- # libgtkflow is free software: you can redistribute it and/or
- # modify it under the terms of the GNU Lesser General Public License
- # as published by the Free Software Foundation, either
- # version 3 of the License, or (at your option) any later
- # version.
- #
- # libgtkflow 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 Lesser General Public License for more details.
- #
- # You should have received a copy of the GNU Lesser General Public
- # License along with libgtkflow.
- # If not, see http://www.gnu.org/licenses/.
- *********************************************************************/
- namespace GtkFlow {
- /**
- * A Gtk Widget that shows nodes and their connections to the user
- * It also lets the user edit said connections.
- */
- public class NodeView : Gtk.Widget {
- private List<Node> nodes = new List<Node>();
- private Gtk.EventControllerMotion motioncontroller;
- private Gtk.GestureClick clickcontroller;
- // The node that is currently being dragged around
- private const int DRAG_THRESHOLD = 3;
- private Node? drag_node = null;
- private bool drag_threshold_fulfilled = false;
- // Coordinates where the drag started
- private double drag_start_x = 0;
- private double drag_start_y = 0;
- // Difference from the chosen drag-point to the
- // upper left corner of the drag_node
- private int drag_diff_x = 0;
- private int drag_diff_y = 0;
- // Remember if a closebutton was pressed
- private bool close_button_pressed = false;
- // Remember if we are resizing a node
- private Node? resize_node = null;
- private int resize_start_x = 0;
- private int resize_start_y = 0;
- // Remember positions of rubberband
- private Gtk.Allocation? rubber_alloc = null;
- private int rubber_start_x = 0;
- private int rubber_start_y = 0;
- // Remember the last dock the mouse hovered over, so we can unhighlight it
- private GFlow.Dock? hovered_dock = null;
- // The dock that we are targeting for dragging a new connector
- private GFlow.Dock? drag_dock = null;
- // The dock that we are targeting to drop a connector on
- private GFlow.Dock? drop_dock = null;
- // The connector that is being used to draw a non-established connection
- private Gtk.Allocation? temp_connector = null;
- /**
- * Connect to this signal if you wish to set custom colors for the
- * connectors depending on what values they transport. Whenever
- * the value of a connected {@link GFlow.Source} changes, this
- * signal will be emitted. Return the color you desire as hex-string
- * similar to those used in css without the preceding hash ('#').
- * Example: red would be "ff0000"
- */
- public virtual signal string color_calculation(GLib.Value v) {
- return this.default_connector_color;
- }
- /**
- * Determines whether docks should be rendered with type-indicators
- */
- public bool show_types {
- get {
- return this._show_types;
- }
- set {
- this._show_types = value;
- this.render_all();
- }
- }
- private bool _show_types = false;
- /**
- * Determines whether the displayed Nodes can be edited by the user
- * e.g. alter their positions by dragging and dropping or drawing
- * new collections or erasing old ones
- */
- public bool editable {get; set; default=true;}
- /**
- * If this property is set to true, the nodeview will not perform
- * any check wheter newly created connections will result in cycles
- * in the graph. It's completely up to the application programmer
- * to make sure that the logic inside the nodes he uses avoids
- * endlessly backpropagated loops
- */
- public bool allow_recursion {get; set; default=false;}
- /**
- * A string that is displayed at the center of the NodeView
- * When no nodes are displayed. You can use it to e.g display
- * a text that encourages the user to spawn nodes.
- */
- public string placeholder {get; set; default="";}
- private Pango.Layout placeholder_layout;
- /**
- * Used to store fg color at iniialization as getting this on the
- * fly has lead to endless-loop-problems.
- */
- private string default_connector_color = "000000";
- /**
- * Creates a new empty {@link NodeView}
- */
- public NodeView() {
- Object();
- this.placeholder_layout = (new Gtk.Label("")).create_pango_layout("");
- this.notify["placeholder"].connect(()=>{
- this.placeholder_layout.set_markup(this.placeholder, -1);
- this.queue_draw();
- });
- this.set_size_request(100,100);
- //this.snapshot.connect((sn)=>{ return this.do_snapshot(sn); });
- this.motioncontroller = new Gtk.EventControllerMotion();
- this.add_controller(this.motioncontroller);
- this.clickcontroller = new Gtk.GestureClick();
- this.add_controller(this.clickcontroller);
- this.motioncontroller.motion.connect((x,y)=>{ this.do_motion_notify_event(x,y); });
- this.clickcontroller.pressed.connect((n,x,y)=>{ this.do_button_press_event(n,x,y); });
- this.clickcontroller.released.connect((n,x,y)=>{ this.do_button_release_event(n,x,y); });
- Gtk.StyleContext sc = this.get_style_context();
- Gdk.RGBA fg = sc.get_color();
- this.default_connector_color = "%2x%2x%2x".printf(col_f2h(fg.red), col_f2h(fg.green), col_f2h(fg.blue));
- }
- public signal void node_added(GFlow.Node n);
- private void add_common(Node n) {
- if (this.nodes.index(n) == -1) {
- this.nodes.insert(n,0);
- n.node_view = this;
- }
- this.queue_draw();
- n.set_parent(this);
- }
- private void render_all() {
- foreach (Node n in this.nodes)
- n.render_all();
- this.queue_draw();
- }
- /**
- * This methods adds a {@link GFlow.Node} to this NodeView
- */
- public void add_node(GFlow.Node gn, Gtk.Widget? title=null) {
- Node n = new Node.with_child(gn, new Gtk.Box(Gtk.Orientation.VERTICAL,0), title);
- n.set_allocation({1,1,0,0});
- this.add_common(n);
- node_added(gn);
- }
- /**
- * This method adds a {@link GFlow.Node} to this nodeview and
- * assigns an arbitrary {@link Gtk.Widget} as its child.
- */
- public void add_with_child(GFlow.Node gn, Gtk.Widget child, Gtk.Widget? title=null) {
- Node n = new Node.with_child(gn, child, title);
- this.add_common(n);
- node_added(gn);
- }
- /**
- * This tells the NodeView to use another {@link NodeRenderer} than
- * the DefaultNodeRenderer for the given {@link GFlow.Node}
- *
- * If you want to use one of libgtkflow's internal node renderers,
- * you can just pass the parameter nrt with one of the values in
- * {@link GtkFlow.NodeRendererType}. If you want to place a custom
- * renderer, set nrt to {@link GtkFlow.NodeRendererType.CUSTOM} and
- * pass the instance of your custom node renderer to the third parameter
- * called nr.
- */
- public void set_node_renderer(GFlow.Node gn, NodeRendererType nrt, NodeRenderer? nr=null) throws GFlow.NodeError, NodeRendererError {
- Node n = this.get_node_from_gflow_node(gn);
- switch (nrt) {
- case NodeRendererType.DOCKLINE:
- n.node_renderer = new DocklineNodeRenderer(n);
- break;
- case NodeRendererType.DEFAULT:
- n.node_renderer = new DefaultNodeRenderer(n);
- break;
- case NodeRendererType.CUSTOM:
- if (nr == null) {
- throw new NodeRendererError.NO_CUSTOM_NODE_RENDERER("You must pass a valid node renderer, not null");
- }
- n.node_renderer = nr;
- break;
- default:
- throw new NodeRendererError.NOT_A_NODE_RENDERER("This is not a valid node renderer");
- }
- this.queue_draw();
- }
- /**
- * Use this method to register childwidgets for custom node
- * renderes to the supplied {@link GFlow.Node}
- */
- public void register_child(GFlow.Node gn, Gtk.Widget child) {
- Node n = this.get_node_from_gflow_node(gn);
- if (n != null) {
- n.add(child);
- //n.show_all();
- }
- }
- /**
- * Use this method to unregister childwidgets from the supplied
- * {@link GFlow.Node}
- */
- public void unregister_child(GFlow.Node gn, Gtk.Widget child) {
- Node n = this.get_node_from_gflow_node(gn);
- if (n != null) {
- n.remove(child);
- }
- }
- internal Node? get_node_from_gflow_node(GFlow.Node gn) {
- foreach (Node n in this.nodes) {
- if (n.gnode == gn) {
- return n;
- }
- }
- return null;
- }
- /**
- * Autolayout this graph
- */
- public void layout(Layout l) {
- var passnodes = this.nodes.copy();
- l.arrange(passnodes);
- }
- /**
- * Used by {@link GtkFlow.Minimap} to get info about nodes.
- */
- internal unowned List<Node> get_nodes(){
- return this.nodes;
- }
- /**
- * Returns nodes that reside inside the given rectangle
- */
- private List<Node> get_nodes_in_rect(Gtk.Allocation alloc) {
- var result = new List<Node>();
- Gdk.Rectangle res;
- Gtk.Allocation node_alloc;
- foreach (Node n in this.nodes) {
- n.get_allocation(out node_alloc);
- node_alloc.union(alloc, out res);
- if (alloc.equal(res)) {
- result.append(n);
- }
- }
- return result;
- }
- /**
- * This signal is being triggered when a node is
- * being removed from this NodeView
- */
- public signal void node_removed(GFlow.Node n);
- /**
- * Remove a {@link GFlow.Node} from this NodeView
- */
- public void remove_node(GFlow.Node n) {
- n.unlink_all();
- Node gn = this.get_node_from_gflow_node(n);
- gn.forall_internal(true, (c)=>{c.destroy();});
- if (this.nodes.index(gn) != -1) {
- this.nodes.remove(gn);
- gn.node_view = null;
- assert (gn is Gtk.Widget);
- ((Gtk.Widget)gn).destroy();
- this.node_removed(n);
- this.queue_draw();
- }
- }
- private Node? get_node_on_position(double x,double y) {
- Gtk.Allocation alloc;
- foreach (Node n in this.nodes) {
- n.get_allocation(out alloc);
- if ( x >= alloc.x && y >= alloc.y &&
- x <= alloc.x + alloc.width && y <= alloc.y + alloc.height ) {
- return n;
- }
- }
- return null;
- }
- private bool do_button_press_event(int n_clicks, double x, double y) {
- uint but = this.clickcontroller.get_current_button();
- message("%u",but);
- /*if ( e.type == Gdk.EventType.@2BUTTON_PRESS
- || e.type == Gdk.EventType.@3BUTTON_PRESS)
- return false;
- */
- if (!this.editable)
- return false;
- Node? n = this.get_node_on_position(x, y);
- GFlow.Dock? targeted_dock = null;
- Point pos = {(int)x,(int)y};
- if (n != null) {
- if (!n.selected) {
- this.unselect_all();
- }
- Gtk.Allocation alloc;
- n.get_allocation(out alloc);
- bool cbp = n.gnode.deletable && n.node_renderer.is_on_closebutton(
- pos, alloc,
- n.border_width
- );
- if (cbp) {
- this.close_button_pressed = true;
- this.unselect_all();
- }
- targeted_dock = n.node_renderer.get_dock_on_position(
- pos, n.get_dock_renderers(),
- n.border_width, alloc, n.title
- );
- if (targeted_dock != null) {
- this.drag_dock = targeted_dock;
- this.drag_dock.active = true;
- int startpos_x = 0, startpos_y = 0;
- if (this.drag_dock is GFlow.Sink && this.drag_dock.is_linked()){
- GFlow.Source s = ((GFlow.Sink)this.drag_dock).sources.last().nth_data(0);
- Node srcnode = this.get_node_from_gflow_node(s.node);
- Gtk.Allocation src_alloc;
- srcnode.get_allocation(out src_alloc);
- if (!srcnode.node_renderer.get_dock_position(
- s, srcnode.get_dock_renderers(),
- (int)srcnode.border_width, src_alloc,
- out startpos_x, out startpos_y,
- srcnode.title )) {
- warning("No dock on position. Aborting drag");
- return false;
- }
- Point startpos = {startpos_x,startpos_y};
- this.temp_connector = {startpos.x, startpos.y,
- (int)x-startpos.x, (int)y-startpos.y};
- } else {
- if (!n.node_renderer.get_dock_position(
- this.drag_dock, n.get_dock_renderers(),
- (int)n.border_width, alloc,
- out startpos_x, out startpos_y,
- n.title )) {
- warning("No dock on position. Aborting drag");
- return false;
- }
- Point startpos = {startpos_x,startpos_y};
- this.temp_connector = {startpos.x, startpos.y, 0, 0};
- }
- this.queue_draw();
- return true;
- }
- }
- // Set a new drag node.
- if (n != null) {
- Gtk.Allocation alloc;
- n.get_allocation(out alloc);
- bool on_resize = n.gnode.resizable && n.node_renderer.is_on_resize_handle(
- pos, alloc,
- n.border_width
- );
- if (on_resize && this.resize_node == null) {
- this.resize_node = n;
- this.resize_node.get_allocation(out alloc);
- if ((this.get_style_context().get_state() & Gtk.StateFlags.DIR_LTR) > 0) {
- this.resize_start_x = alloc.width;
- this.resize_start_y = alloc.height;
- } else {
- this.resize_start_x = 0;
- this.resize_start_y = alloc.height;
- }
- } else if (this.resize_node == null && this.drag_node == null) {
- this.drag_node = n;
- this.drag_node.get_allocation(out alloc);
- } else {
- return false;
- }
- this.drag_start_x = x;
- this.drag_start_y = y;
- this.drag_diff_x = (int)this.drag_start_x - alloc.x;
- this.drag_diff_y = (int)this.drag_start_y - alloc.y;
- } else {
- this.unselect_all();
- this.rubber_alloc = {(int)x, (int)y, 0, 0};
- this.rubber_start_x = (int)x;
- this.rubber_start_y = (int)y;
- }
- return false;
- }
- /**
- * Every currently selected node is being unselected
- */
- public void unselect_all() {
- foreach (Node n in this.nodes) {
- n.selected = false;
- }
- this.queue_draw();
- }
- private List<Node> get_selected_nodes() {
- var result = new List<Node>();
- foreach (Node n in this.nodes) {
- if (n.selected)
- result.append(n);
- }
- return result;
- }
- /**
- * Returns each {@link GFlow.Node} that is currently selected
- */
- public List<GFlow.Node> get_selected() {
- var result = new List<GFlow.Node>();
- foreach (Node n in this.nodes) {
- if (n.selected)
- result.append(n.gnode);
- }
- return result;
- }
- //Empty remove implementation to avoid warning message
- /**
- * Empty default implementation. Do not use. To remove {@link GFlow.Node}s
- * from a NodeView please use {@link NodeView.remove_node}
- */
- public override void remove(Gtk.Widget w) {}
- private bool do_button_release_event(int n, double x, double y) {
- if (!this.editable)
- return false;
- // Determine if this was a closebutton press
- if (this.close_button_pressed) {
- Node? n = this.get_node_on_position(x, y);
- if (n != null) {
- Point pos = {(int)x,(int)y};
- Gtk.Allocation alloc;
- n.get_allocation(out alloc);
- bool cbp = n.node_renderer.is_on_closebutton(
- pos, alloc,
- n.border_width
- );
- if (cbp) {
- this.remove_node(n.gnode);
- this.close_button_pressed = false;
- return true;
- }
- }
- }
- // Try to build a new connection
- if (this.drag_dock != null) {
- try {
- if (this.drag_dock is GFlow.Source && this.drop_dock is GFlow.Sink) {
- ((GFlow.Source)this.drag_dock).link(this.drop_dock as GFlow.Sink);
- }
- else if (this.drag_dock is GFlow.Sink && this.drop_dock is GFlow.Source) {
- ((GFlow.Source)this.drop_dock).link(this.drag_dock as GFlow.Sink);
- }
- else if (this.drag_dock is GFlow.Sink && this.drop_dock is GFlow.Sink) {
- GFlow.Source? src = ((GFlow.Sink)this.drag_dock).sources.last().nth_data(0);
- if (src != null) {
- src.unlink(this.drag_dock as GFlow.Sink);
- src.link(this.drop_dock as GFlow.Sink);
- }
- }
- else if (this.drag_dock is GFlow.Sink && this.drop_dock == null) {
- GFlow.Source? src = ((GFlow.Sink)this.drag_dock).sources.last().nth_data(0);
- if (src != null) {
- src.unlink((GFlow.Sink)this.drag_dock);
- }
- }
- } catch (GLib.Error e) {
- warning(e.message);
- }
- }
- this.stop_dragging();
- this.queue_draw();
- return false;
- }
- private void stop_dragging() {
- this.drag_start_x = 0;
- this.drag_start_y = 0;
- this.drag_diff_x = 0;
- this.drag_diff_y = 0;
- this.rubber_alloc = null;
- this.drag_node = null;
- if (this.drag_dock != null) {
- this.drag_dock.active = false;
- }
- this.drag_dock = null;
- if (this.drop_dock != null) {
- this.drop_dock.active = false;
- }
- this.drop_dock = null;
- this.temp_connector = null;
- this.drag_threshold_fulfilled = false;
- this.resize_node = null;
- this.get_window().set_cursor(null);
- }
- private Gdk.Cursor resize_cursor = null;
- private Gdk.Cursor? get_resize_cursor() {
- if (resize_cursor == null && this.get_realized()) {
- if ((this.get_style_context().get_state() & Gtk.StateFlags.DIR_LTR) > 0 ){
- resize_cursor = new Gdk.Cursor.for_display(
- this.get_window().get_display(),
- Gdk.CursorType.BOTTOM_RIGHT_CORNER
- );
- } else {
- resize_cursor = new Gdk.Cursor.for_display(
- this.get_window().get_display(),
- Gdk.CursorType.BOTTOM_LEFT_CORNER
- );
- }
- }
- return resize_cursor;
- }
- private bool do_motion_notify_event(double x, double y) {
- if (!this.editable)
- return false;
- // Check if we are on a node. If yes, check if we are
- // currently pointing on a dock. if this is true, we
- // Want to draw a new connector instead of dragging the node
- Node? n = this.get_node_on_position(x, y);
- GFlow.Dock? targeted_dock = null;
- if (n != null) {
- Point pos = {(int)x, (int)y};
- Gtk.Allocation alloc;
- n.get_allocation(out alloc);
- // If the close button is pressed, deactivate it when
- // we moved the node
- if (this.close_button_pressed) {
- bool cbp = n.node_renderer.is_on_closebutton(
- pos, alloc,
- n.border_width
- );
- if (!cbp)
- this.close_button_pressed = false;
- }
- // Update cursor if we are on the resize area
- bool on_resize = n.gnode.resizable && n.node_renderer.is_on_resize_handle(
- pos, alloc,
- n.border_width
- );
- // TODO: find replacement for cursor behaviour
- /*if (on_resize)
- this.get_window().set_cursor(this.get_resize_cursor());
- else if (this.resize_node == null)
- this.get_window().set_cursor(null);
- */
- targeted_dock = n.node_renderer.get_dock_on_position(
- pos, n.get_dock_renderers(),
- n.border_width, alloc, n.title
- );
- if (this.drag_dock == null && targeted_dock != this.hovered_dock) {
- this.set_hovered_dock(targeted_dock);
- }
- else if (this.drag_dock != null && targeted_dock != null
- && targeted_dock != this.hovered_dock
- && this.is_suitable_target(this.drag_dock, targeted_dock)) {
- this.set_hovered_dock(targeted_dock);
- }
- } else {
- // If we are leaving the node we will also have to
- // un-highlight the last hovered dock
- if (this.hovered_dock != null)
- this.hovered_dock.highlight = false;
- this.hovered_dock = null;
- this.queue_draw();
- // Update cursor to be default as we are guaranteed not on any
- // resize handle outside of any node.
- // The check for resize node is a cosmetical fix. If there is a
- // Node bing resized in haste, the cursor tends to flicker
- if (this.resize_node == null)
- this.get_window().set_cursor(null);
- }
- // Check if the cursor has been dragged a few pixels (defined by DRAG_THRESHOLD)
- // If yes, actually start dragging
- if ( ( this.drag_node != null || this.drag_dock != null || this.resize_node != null)
- && (Math.fabs(drag_start_x - x) > NodeView.DRAG_THRESHOLD
- || Math.fabs(drag_start_y - y) > NodeView.DRAG_THRESHOLD )) {
- this.drag_threshold_fulfilled = true;
- }
- // Actually something
- if (this.drag_threshold_fulfilled ) {
- Gtk.Allocation alloc;
- if (this.drag_node != null) {
- // Actually move the node(s)
- var nodes_to_drag = this.get_selected_nodes();
- Gtk.Allocation drag_node_alloc;
- this.drag_node.get_allocation(out drag_node_alloc);
- if (nodes_to_drag.length() == 0) {
- nodes_to_drag.append(this.drag_node);
- }
- Gtk.Allocation union = {0,0,0,0};
- bool first = true;
- Point upperleft = {int.MAX,int.MAX};
- foreach (Node node in nodes_to_drag) {
- node.get_allocation(out alloc);
- upperleft.x = (int)Math.fmin(alloc.x, upperleft.x);
- upperleft.y = (int)Math.fmin(alloc.y, upperleft.y);
- alloc.x = (int)Math.fmax(0,(int)x - this.drag_diff_x);
- alloc.y = (int)Math.fmax(0,(int)y - this.drag_diff_y);
- if (first) {
- union = alloc;
- } else {
- Gdk.Rectangle tmp;
- union.union(alloc, out tmp);
- union = (Gtk.Allocation)tmp;
- }
- }
- foreach (Node node in nodes_to_drag) {
- node.get_allocation(out alloc);
- int dn_diff_x = alloc.x - drag_node_alloc.x;
- int dn_diff_y = alloc.y - drag_node_alloc.y;
- int ul_diff_x = alloc.x - upperleft.x;
- int ul_diff_y = alloc.y - upperleft.y;
- alloc.x = (int)Math.fmax(ul_diff_x,(int)x - this.drag_diff_x + dn_diff_x);
- alloc.y = (int)Math.fmax(ul_diff_y,(int)y - this.drag_diff_y + dn_diff_y);
- node.size_allocate(alloc.width, alloc.height, -1);
- }
- }
- if (this.drag_dock != null) {
- // Manipulate the temporary connector
- this.temp_connector.width = (int)x-this.temp_connector.x;
- this.temp_connector.height = (int)y-this.temp_connector.y;
- if (targeted_dock == null) {
- this.set_drop_dock(null);
- }
- else if (this.is_suitable_target(this.drag_dock, targeted_dock))
- this.set_drop_dock(targeted_dock);
- }
- if (this.resize_node != null) {
- // resize the node
- this.resize_node.get_allocation(out alloc);
- if ((this.get_style_context().get_state() & Gtk.StateFlags.DIR_LTR) > 0 ){
- alloc.width = resize_start_x + (int)x - (int)this.drag_start_x;
- alloc.height = resize_start_y + (int)y - (int)this.drag_start_y;
- } else {
- //FIXME: far from perfect. strange behaviour when resizing
- alloc.x = (int)x;
- alloc.width = alloc.width + ((int)this.drag_start_x - (int)x);
- alloc.height = resize_start_y + (int)y - (int)this.drag_start_y;
- }
- this.resize_node.size_allocate(alloc.width, alloc.height, -1);
- }
- this.allocate_minimum();
- this.queue_draw();
- }
- if (this.rubber_alloc != null) {
- if (x >= this.rubber_start_x) {
- this.rubber_alloc.x = (int)this.rubber_start_x;
- this.rubber_alloc.width = (int)x - this.rubber_alloc.x;
- } else {
- this.rubber_alloc.x = (int)x;
- this.rubber_alloc.width = this.rubber_start_x - (int)x;
- }
- if (y >= this.rubber_start_y) {
- this.rubber_alloc.y = (int)this.rubber_start_y;
- this.rubber_alloc.height = (int)y - this.rubber_alloc.y;
- } else {
- this.rubber_alloc.y = (int)y;
- this.rubber_alloc.height = this.rubber_start_y - (int)y;
- }
- var selected_nodes = this.get_nodes_in_rect(this.rubber_alloc);
- foreach (Node node in this.nodes) {
- node.selected = selected_nodes.index(node) != -1;
- }
- this.allocate_minimum();
- this.queue_draw();
- }
- return false;
- }
- /**
- * Allocates the minimum size needed to draw the
- * contained nodes at their respective positions
- * with their respective sizes to this NodeView
- * If the minimunsize is smaller than the containing
- * parent (which ideally should be a scrolledwindow
- * scrolledwindow) the parent's size will be allocated
- * in order to prevent the nodeview from collapsing
- * to a size of 0x0 px thus being inable to receive
- * mouse-events
- */
- private void allocate_minimum() {
- int minwidth = 0, minheight = 0, _ = 0;
- this.get_preferred_width(out minwidth, out _);
- this.get_preferred_height(out minheight, out _);
- this.set_size_request(minwidth, minheight);
- Gtk.Allocation nv_alloc;
- this.get_allocation(out nv_alloc);
- nv_alloc.width = minwidth;
- nv_alloc.height = minheight;
- if (this.parent != null) {
- Gtk.Allocation palloc;
- this.parent.get_allocation(out palloc);
- nv_alloc.width = int.max(minwidth, palloc.width);
- nv_alloc.height = int.max(minheight, palloc.height);
- }
- this.size_allocate(nv_alloc.width, nv_alloc.height, -1);
- }
- /**
- * Calculates the NodeView's minimum and preferred widths
- */
- public new void get_preferred_width(out int minimum_width, out int natural_width) {
- double x_min = 0, x_max = 0;
- Gtk.Allocation alloc;
- foreach (Node n in this.nodes) {
- n.get_allocation(out alloc);
- x_min = Math.fmin(x_min, alloc.x);
- x_max = Math.fmax(x_max, alloc.x+alloc.width);
- }
- if (this.rubber_alloc != null) {
- x_min = Math.fmin(x_min, rubber_alloc.x);
- x_max = Math.fmax(x_max, rubber_alloc.x + rubber_alloc.width);
- }
- if (this.temp_connector != null) {
- x_min = Math.fmin(x_min, temp_connector.x);
- x_max = Math.fmax(x_max, temp_connector.x + temp_connector.width);
- }
- x_min = Math.fmax(0, x_min);
- minimum_width = natural_width = (int)x_max - (int)x_min;
- }
- /**
- * Calculates the NodeView's minimum and preferred heights
- */
- public new void get_preferred_height(out int minimum_height, out int natural_height) {
- double y_min = 0, y_max = 0;
- Gtk.Allocation alloc;
- foreach (Node n in this.nodes) {
- n.get_allocation(out alloc);
- y_min = Math.fmin(y_min, alloc.y);
- y_max = Math.fmax(y_max, alloc.y+alloc.height);
- }
- if (this.rubber_alloc != null) {
- y_min = Math.fmin(y_min, rubber_alloc.y);
- y_max = Math.fmax(y_max, rubber_alloc.y + rubber_alloc.height);
- }
- if (this.temp_connector != null) {
- y_min = Math.fmin(y_min, temp_connector.y);
- y_max = Math.fmax(y_max, temp_connector.y + temp_connector.height);
- }
- y_min = Math.fmax(0, y_min);
- minimum_height = natural_height = (int)y_max - (int)y_min;
- }
- /**
- * Determines wheter one dock can be dropped on another
- */
- private bool is_suitable_target (GFlow.Dock from, GFlow.Dock to) {
- // Check whether the docks have the same type
- if (!from.has_same_type(to))
- return false;
- // Check if the target would lead to a recursion
- // If yes, return the value of allow_recursion. If this
- // value is set to true, it's completely fine to have
- // a recursive graph
- if (to is GFlow.Source && from is GFlow.Sink) {
- if (!this.allow_recursion)
- if (from.node.is_recursive_forward(to.node) ||
- to.node.is_recursive_backward(from.node))
- return false;
- }
- if (to is GFlow.Sink && from is GFlow.Source) {
- if (!this.allow_recursion)
- if (to.node.is_recursive_forward(from.node) ||
- from.node.is_recursive_backward(to.node))
- return false;
- }
- if (to is GFlow.Sink && from is GFlow.Sink) {
- GFlow.Source? s = ((GFlow.Sink)from).sources.last().nth_data(0);
- if (s == null)
- return false;
- if (!this.allow_recursion)
- if (to.node.is_recursive_forward(s.node) ||
- s.node.is_recursive_backward(to.node))
- return false;
- }
- // If the from from-target is a sink, check if the
- // to target is either a source which does not belong to the own node
- // or if the to target is another sink (this is valid as we can
- // move a connection from one sink to another
- if (from is GFlow.Sink
- && ((to is GFlow.Sink
- && to != from)
- || (to is GFlow.Source
- && (!to.node.has_dock(from) || this.allow_recursion)))) {
- return true;
- }
- // Check if the from-target is a source. if yes, make sure the
- // to-target is a sink and it does not belong to the own node
- else if (from is GFlow.Source
- && to is GFlow.Sink
- && (!to.node.has_dock(from) || this.allow_recursion)) {
- return true;
- }
- return false;
- }
- /**
- * Sets the dock that is currently being hovered over to drop
- * a connector on
- */
- private void set_drop_dock(GFlow.Dock? d) {
- if (this.drop_dock != null)
- this.drop_dock.active = false;
- this.drop_dock = d;
- if (this.drop_dock != null)
- this.drop_dock.active = true;
- this.queue_draw();
- }
- /**
- * Sets the dock that is currently being hovered over
- */
- private void set_hovered_dock(GFlow.Dock? d) {
- if (this.hovered_dock != null)
- this.hovered_dock.highlight = false;
- this.hovered_dock = d;
- if (this.hovered_dock != null)
- this.hovered_dock.highlight = true;
- this.queue_draw();
- }
- /**
- * Manually set the position of the given {@link GFlow.Node} on this nodeview
- */
- public void set_node_position(GFlow.Node gn, int x, int y) {
- Node n = this.get_node_from_gflow_node(gn);
- n.set_position(x,y);
- this.allocate_minimum();
- this.queue_draw();
- }
- /**
- * Manually set the allocation of the given {@link GFlow.Node} on this nodeview
- */
- public void set_node_allocation(GFlow.Node gn, Gtk.Allocation alloc) {
- Node n = this.get_node_from_gflow_node(gn);
- n.set_allocation(alloc);
- this.allocate_minimum();
- this.queue_draw();
- }
- /**
- * Return the position of the given {@link GFlow.Node} on this nodeview
- */
- public unowned Point get_node_position(GFlow.Node gn) {
- Node n = this.get_node_from_gflow_node(gn);
- return n.get_position();
- }
- /**
- * Returns the allocation of the given {@link GFlow.Node}
- */
- public unowned Gtk.Allocation get_node_allocation(GFlow.Node gn) {
- Gtk.Allocation alloc;
- Node n = this.get_node_from_gflow_node(gn);
- n.get_allocation(out alloc);
- return alloc;
- }
- private new void snapshot(Gtk.Snapshot sn) {
- Cairo.Context cr = sn.append_cairo();
- Gtk.StyleContext sc = this.get_style_context();
- Gtk.Allocation nv_alloc;
- this.get_allocation(out nv_alloc);
- sc.render_background(cr, 0, 0, nv_alloc.width, nv_alloc.height);
- // Draw nodes
- this.nodes.reverse();
- foreach (Node n in this.nodes) {
- n.current_cairo_ctx = cr;
- Gtk.Allocation alloc;
- n.get_allocation(out alloc);
- var node_properties = NodeProperties();
- node_properties.editable = this.editable;
- node_properties.deletable = n.gnode.deletable;
- node_properties.resizable = n.gnode.resizable;
- node_properties.selected = n.selected;
- n.node_renderer.draw_node(
- this,
- cr,
- alloc,
- n.get_dock_renderers(),
- n.get_childlist(),
- (int)n.border_width,
- node_properties,
- n.title
- );
- if (n.highlight_color != null) {
- var hl = n.highlight_color;
- Gtk.Allocation node_alloc;
- n.get_allocation(out node_alloc);
- cr.save();
- cr.set_source_rgba(hl.red, hl.green, hl.blue, 0.3);
- cr.rectangle(node_alloc.x, node_alloc.y, node_alloc.width, node_alloc.height);
- cr.fill();
- cr.restore();
- cr.save();
- cr.set_source_rgba(hl.red, hl.green, hl.blue, 0.9);
- cr.rectangle(node_alloc.x, node_alloc.y, node_alloc.width, node_alloc.height);
- cr.stroke();
- cr.restore();
- }
- n.current_cairo_ctx = null;
- }
- this.nodes.reverse();
- // Draw connectors
- foreach (Node n in this.nodes) {
- foreach(GFlow.Source source in n.gnode.get_sources()) {
- Gtk.Allocation alloc;
- n.get_allocation(out alloc);
- int source_pos_x = 0, source_pos_y = 0;
- if (!n.node_renderer.get_dock_position(
- source,
- n.get_dock_renderers(),
- (int)n.border_width,
- alloc, out source_pos_x, out source_pos_y,
- n.title)) {
- warning("No dock on position. Ommiting connector");
- continue;
- }
- Point source_pos = {source_pos_x,source_pos_y};
- foreach(GFlow.Sink sink in source.sinks) {
- // Don't draw the connection to a sink if we are dragging it
- if (sink == this.drag_dock && source == sink.sources.last().nth_data(0))
- continue;
- Node? sink_node = this.get_node_from_gflow_node(sink.node);
- sink_node.get_allocation(out alloc);
- int sink_pos_x = 0, sink_pos_y = 0;
- if (!sink_node.node_renderer.get_dock_position(
- sink,
- sink_node.get_dock_renderers(),
- (int)sink_node.border_width,
- alloc, out sink_pos_x, out sink_pos_y,
- sink_node.title )) {
- warning("No dock on position. Ommiting connector");
- continue;
- }
- Point sink_pos = {sink_pos_x,sink_pos_y};
- int w = sink_pos.x - source_pos.x;
- int h = sink_pos.y - source_pos.y;
- cr.save();
- double r=0, g=0, b=0;
- if (source != null && source.get_last_value() != null) {
- string hexcol = color_calculation(source.get_last_value());
- this.hex2col(hexcol ,out r, out g, out b);
- cr.set_source_rgba(r,g,b,1.0);
- } else {
- this.hex2col(default_connector_color ,out r, out g, out b);
- }
- cr.set_source_rgba(r,g,b,1.0);
- cr.move_to(source_pos.x, source_pos.y);
- if (w > 0) {
- cr.rel_curve_to(w/3,0,2*w/3,h,w,h);
- } else {
- cr.rel_curve_to(-w/3,0,1.3*w,h,w,h);
- }
- cr.stroke();
- cr.restore();
- }
- }
- }
- // Draw temporary connector if any
- if (this.temp_connector != null) {
- int w = this.temp_connector.width;
- int h = this.temp_connector.height;
- cr.move_to(this.temp_connector.x, this.temp_connector.y);
- if (w > 0) {
- cr.rel_curve_to(w/3,0,2*w/3,h,w,h);
- } else {
- cr.rel_curve_to(-w/3,0,1.3*w,h,w,h);
- }
- cr.stroke();
- }
- // Draw rubberband
- if (this.rubber_alloc != null) {
- draw_rubberband(this, cr,
- this.rubber_alloc.x, this.rubber_alloc.y,
- Gtk.StateFlags.NORMAL,
- &this.rubber_alloc.width, &this.rubber_alloc.height);
- }
- // Draw placeholder if there are no nodes
- if (this.nodes.length() == 0) {
- sc.save();
- cr.save();
- sc.add_class(Gtk.STYLE_CLASS_BUTTON);
- Gdk.RGBA col = sc.get_color(Gtk.StateFlags.NORMAL);
- cr.set_source_rgba(col.red,col.green,col.blue,col.alpha);
- int placeholder_width, placeholder_height;
- this.placeholder_layout.get_pixel_size(out placeholder_width, out placeholder_height);
- cr.move_to(nv_alloc.width / 2 - placeholder_width / 2,
- nv_alloc.height / 2 - placeholder_height / 2);
- Pango.cairo_show_layout(cr, this.placeholder_layout);
- cr.restore();
- sc.restore();
- }
- return false;
- }
- private void hex2col(string hex, out double r, out double g, out double b) {
- string hexdigits ="0123456789abcdef";
- r = col_h2f(hexdigits.index_of_char(hex[0]) * 16 + hexdigits.index_of_char(hex[1]));
- g = col_h2f(hexdigits.index_of_char(hex[2]) * 16 + hexdigits.index_of_char(hex[3]));
- b = col_h2f(hexdigits.index_of_char(hex[4]) * 16 + hexdigits.index_of_char(hex[5]));
- }
- private double col_h2f(uint col) {
- return col/255.0f;
- }
- private uint col_f2h(double col) {
- return (uint)int.parse( (col*255.0d).to_string() ).abs();
- }
- /**
- * Internal method to initialize this NodeView as a {@link Gtk.Widget}
- */
- public override void realize() {
- /*Gtk.Allocation alloc;
- this.get_allocation(out alloc);
- var attr = Gdk.WindowAttr();
- attr.window_type = Gdk.WindowType.CHILD;
- attr.x = alloc.x;
- attr.y = alloc.y;
- attr.width = alloc.width;
- attr.height = alloc.height;
- attr.visual = this.get_visual();
- attr.event_mask = this.get_events()
- | Gdk.EventMask.POINTER_MOTION_MASK
- | Gdk.EventMask.BUTTON_PRESS_MASK
- | Gdk.EventMask.BUTTON_RELEASE_MASK
- | Gdk.EventMask.LEAVE_NOTIFY_MASK;
- var window = new Gdk.Window(this.get_parent_window(), attr, mask);
- this.set_window(window);
- this.register_window(window);
- this.set_realized(true);*/
- }
- /**
- * Highlight a node in a user-specified color
- *
- * When set to a non-null value, the node will be displayed in the tint
- * of the given color. You can use this to convey status information
- * or to indicate that something with this node is not allright.
- */
- public void set_node_highlight(GFlow.Node gn, Gdk.RGBA? c) {
- Node? n = this.get_node_from_gflow_node(gn);
- if (n == null) {
- warning("Tried to set highlight color to node that does not belong to nodeview.");
- return;
- }
- n.highlight_color = c;
- this.queue_draw();
- }
- }
- public struct Point {
- public int x;
- public int y;
- }
- /**
- * Draw radiobutton.
- * Implemented in drawinghelper.c
- */
- private extern void draw_radio(Gtk.Widget widget,
- Cairo.Context cr,
- int x,
- int y,
- Gtk.StateFlags state,
- int* width,
- int* height);
- /**
- * Draw rubberband selection.
- * Implemented in drawinghelper.c
- */
- private extern void draw_rubberband(Gtk.Widget widget,
- Cairo.Context cr,
- int x,
- int y,
- Gtk.StateFlags state,
- int* width,
- int* height);
- }
|