nodeview.vala 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142
  1. /********************************************************************
  2. # Copyright 2014-2022 Daniel 'grindhold' Brendle
  3. #
  4. # This file is part of libgtkflow.
  5. #
  6. # libgtkflow is free software: you can redistribute it and/or
  7. # modify it under the terms of the GNU Lesser General Public License
  8. # as published by the Free Software Foundation, either
  9. # version 3 of the License, or (at your option) any later
  10. # version.
  11. #
  12. # libgtkflow is distributed in the hope that it will be
  13. # useful, but WITHOUT ANY WARRANTY; without even the implied
  14. # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
  15. # PURPOSE. See the GNU Lesser General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Lesser General Public
  18. # License along with libgtkflow.
  19. # If not, see http://www.gnu.org/licenses/.
  20. *********************************************************************/
  21. namespace GtkFlow {
  22. /**
  23. * A Gtk Widget that shows nodes and their connections to the user
  24. * It also lets the user edit said connections.
  25. */
  26. public class NodeView : Gtk.Widget {
  27. private List<Node> nodes = new List<Node>();
  28. private Gtk.EventControllerMotion motioncontroller;
  29. private Gtk.GestureClick clickcontroller;
  30. // The node that is currently being dragged around
  31. private const int DRAG_THRESHOLD = 3;
  32. private Node? drag_node = null;
  33. private bool drag_threshold_fulfilled = false;
  34. // Coordinates where the drag started
  35. private double drag_start_x = 0;
  36. private double drag_start_y = 0;
  37. // Difference from the chosen drag-point to the
  38. // upper left corner of the drag_node
  39. private int drag_diff_x = 0;
  40. private int drag_diff_y = 0;
  41. // Remember if a closebutton was pressed
  42. private bool close_button_pressed = false;
  43. // Remember if we are resizing a node
  44. private Node? resize_node = null;
  45. private int resize_start_x = 0;
  46. private int resize_start_y = 0;
  47. // Remember positions of rubberband
  48. private Gtk.Allocation? rubber_alloc = null;
  49. private int rubber_start_x = 0;
  50. private int rubber_start_y = 0;
  51. // Remember the last dock the mouse hovered over, so we can unhighlight it
  52. private GFlow.Dock? hovered_dock = null;
  53. // The dock that we are targeting for dragging a new connector
  54. private GFlow.Dock? drag_dock = null;
  55. // The dock that we are targeting to drop a connector on
  56. private GFlow.Dock? drop_dock = null;
  57. // The connector that is being used to draw a non-established connection
  58. private Gtk.Allocation? temp_connector = null;
  59. /**
  60. * Connect to this signal if you wish to set custom colors for the
  61. * connectors depending on what values they transport. Whenever
  62. * the value of a connected {@link GFlow.Source} changes, this
  63. * signal will be emitted. Return the color you desire as hex-string
  64. * similar to those used in css without the preceding hash ('#').
  65. * Example: red would be "ff0000"
  66. */
  67. public virtual signal string color_calculation(GLib.Value v) {
  68. return this.default_connector_color;
  69. }
  70. /**
  71. * Determines whether docks should be rendered with type-indicators
  72. */
  73. public bool show_types {
  74. get {
  75. return this._show_types;
  76. }
  77. set {
  78. this._show_types = value;
  79. this.render_all();
  80. }
  81. }
  82. private bool _show_types = false;
  83. /**
  84. * Determines whether the displayed Nodes can be edited by the user
  85. * e.g. alter their positions by dragging and dropping or drawing
  86. * new collections or erasing old ones
  87. */
  88. public bool editable {get; set; default=true;}
  89. /**
  90. * If this property is set to true, the nodeview will not perform
  91. * any check wheter newly created connections will result in cycles
  92. * in the graph. It's completely up to the application programmer
  93. * to make sure that the logic inside the nodes he uses avoids
  94. * endlessly backpropagated loops
  95. */
  96. public bool allow_recursion {get; set; default=false;}
  97. /**
  98. * A string that is displayed at the center of the NodeView
  99. * When no nodes are displayed. You can use it to e.g display
  100. * a text that encourages the user to spawn nodes.
  101. */
  102. public string placeholder {get; set; default="";}
  103. private Pango.Layout placeholder_layout;
  104. /**
  105. * Used to store fg color at iniialization as getting this on the
  106. * fly has lead to endless-loop-problems.
  107. */
  108. private string default_connector_color = "000000";
  109. /**
  110. * Creates a new empty {@link NodeView}
  111. */
  112. public NodeView() {
  113. Object();
  114. this.placeholder_layout = (new Gtk.Label("")).create_pango_layout("");
  115. this.notify["placeholder"].connect(()=>{
  116. this.placeholder_layout.set_markup(this.placeholder, -1);
  117. this.queue_draw();
  118. });
  119. this.set_size_request(100,100);
  120. //this.snapshot.connect((sn)=>{ return this.do_snapshot(sn); });
  121. this.motioncontroller = new Gtk.EventControllerMotion();
  122. this.add_controller(this.motioncontroller);
  123. this.clickcontroller = new Gtk.GestureClick();
  124. this.add_controller(this.clickcontroller);
  125. this.motioncontroller.motion.connect((x,y)=>{ this.do_motion_notify_event(x,y); });
  126. this.clickcontroller.pressed.connect((n,x,y)=>{ this.do_button_press_event(n,x,y); });
  127. this.clickcontroller.released.connect((n,x,y)=>{ this.do_button_release_event(n,x,y); });
  128. Gtk.StyleContext sc = this.get_style_context();
  129. Gdk.RGBA fg = sc.get_color();
  130. this.default_connector_color = "%2x%2x%2x".printf(col_f2h(fg.red), col_f2h(fg.green), col_f2h(fg.blue));
  131. }
  132. public signal void node_added(GFlow.Node n);
  133. private void add_common(Node n) {
  134. if (this.nodes.index(n) == -1) {
  135. this.nodes.insert(n,0);
  136. n.node_view = this;
  137. }
  138. this.queue_draw();
  139. n.set_parent(this);
  140. }
  141. private void render_all() {
  142. foreach (Node n in this.nodes)
  143. n.render_all();
  144. this.queue_draw();
  145. }
  146. /**
  147. * This methods adds a {@link GFlow.Node} to this NodeView
  148. */
  149. public void add_node(GFlow.Node gn, Gtk.Widget? title=null) {
  150. Node n = new Node.with_child(gn, new Gtk.Box(Gtk.Orientation.VERTICAL,0), title);
  151. n.set_allocation({1,1,0,0});
  152. this.add_common(n);
  153. node_added(gn);
  154. }
  155. /**
  156. * This method adds a {@link GFlow.Node} to this nodeview and
  157. * assigns an arbitrary {@link Gtk.Widget} as its child.
  158. */
  159. public void add_with_child(GFlow.Node gn, Gtk.Widget child, Gtk.Widget? title=null) {
  160. Node n = new Node.with_child(gn, child, title);
  161. this.add_common(n);
  162. node_added(gn);
  163. }
  164. /**
  165. * This tells the NodeView to use another {@link NodeRenderer} than
  166. * the DefaultNodeRenderer for the given {@link GFlow.Node}
  167. *
  168. * If you want to use one of libgtkflow's internal node renderers,
  169. * you can just pass the parameter nrt with one of the values in
  170. * {@link GtkFlow.NodeRendererType}. If you want to place a custom
  171. * renderer, set nrt to {@link GtkFlow.NodeRendererType.CUSTOM} and
  172. * pass the instance of your custom node renderer to the third parameter
  173. * called nr.
  174. */
  175. public void set_node_renderer(GFlow.Node gn, NodeRendererType nrt, NodeRenderer? nr=null) throws GFlow.NodeError, NodeRendererError {
  176. Node n = this.get_node_from_gflow_node(gn);
  177. switch (nrt) {
  178. case NodeRendererType.DOCKLINE:
  179. n.node_renderer = new DocklineNodeRenderer(n);
  180. break;
  181. case NodeRendererType.DEFAULT:
  182. n.node_renderer = new DefaultNodeRenderer(n);
  183. break;
  184. case NodeRendererType.CUSTOM:
  185. if (nr == null) {
  186. throw new NodeRendererError.NO_CUSTOM_NODE_RENDERER("You must pass a valid node renderer, not null");
  187. }
  188. n.node_renderer = nr;
  189. break;
  190. default:
  191. throw new NodeRendererError.NOT_A_NODE_RENDERER("This is not a valid node renderer");
  192. }
  193. this.queue_draw();
  194. }
  195. /**
  196. * Use this method to register childwidgets for custom node
  197. * renderes to the supplied {@link GFlow.Node}
  198. */
  199. public void register_child(GFlow.Node gn, Gtk.Widget child) {
  200. Node n = this.get_node_from_gflow_node(gn);
  201. if (n != null) {
  202. n.add(child);
  203. //n.show_all();
  204. }
  205. }
  206. /**
  207. * Use this method to unregister childwidgets from the supplied
  208. * {@link GFlow.Node}
  209. */
  210. public void unregister_child(GFlow.Node gn, Gtk.Widget child) {
  211. Node n = this.get_node_from_gflow_node(gn);
  212. if (n != null) {
  213. n.remove(child);
  214. }
  215. }
  216. internal Node? get_node_from_gflow_node(GFlow.Node gn) {
  217. foreach (Node n in this.nodes) {
  218. if (n.gnode == gn) {
  219. return n;
  220. }
  221. }
  222. return null;
  223. }
  224. /**
  225. * Autolayout this graph
  226. */
  227. public void layout(Layout l) {
  228. var passnodes = this.nodes.copy();
  229. l.arrange(passnodes);
  230. }
  231. /**
  232. * Used by {@link GtkFlow.Minimap} to get info about nodes.
  233. */
  234. internal unowned List<Node> get_nodes(){
  235. return this.nodes;
  236. }
  237. /**
  238. * Returns nodes that reside inside the given rectangle
  239. */
  240. private List<Node> get_nodes_in_rect(Gtk.Allocation alloc) {
  241. var result = new List<Node>();
  242. Gdk.Rectangle res;
  243. Gtk.Allocation node_alloc;
  244. foreach (Node n in this.nodes) {
  245. n.get_allocation(out node_alloc);
  246. node_alloc.union(alloc, out res);
  247. if (alloc.equal(res)) {
  248. result.append(n);
  249. }
  250. }
  251. return result;
  252. }
  253. /**
  254. * This signal is being triggered when a node is
  255. * being removed from this NodeView
  256. */
  257. public signal void node_removed(GFlow.Node n);
  258. /**
  259. * Remove a {@link GFlow.Node} from this NodeView
  260. */
  261. public void remove_node(GFlow.Node n) {
  262. n.unlink_all();
  263. Node gn = this.get_node_from_gflow_node(n);
  264. gn.forall_internal(true, (c)=>{c.destroy();});
  265. if (this.nodes.index(gn) != -1) {
  266. this.nodes.remove(gn);
  267. gn.node_view = null;
  268. assert (gn is Gtk.Widget);
  269. ((Gtk.Widget)gn).destroy();
  270. this.node_removed(n);
  271. this.queue_draw();
  272. }
  273. }
  274. private Node? get_node_on_position(double x,double y) {
  275. Gtk.Allocation alloc;
  276. foreach (Node n in this.nodes) {
  277. n.get_allocation(out alloc);
  278. if ( x >= alloc.x && y >= alloc.y &&
  279. x <= alloc.x + alloc.width && y <= alloc.y + alloc.height ) {
  280. return n;
  281. }
  282. }
  283. return null;
  284. }
  285. private bool do_button_press_event(int n_clicks, double x, double y) {
  286. uint but = this.clickcontroller.get_current_button();
  287. message("%u",but);
  288. /*if ( e.type == Gdk.EventType.@2BUTTON_PRESS
  289. || e.type == Gdk.EventType.@3BUTTON_PRESS)
  290. return false;
  291. */
  292. if (!this.editable)
  293. return false;
  294. Node? n = this.get_node_on_position(x, y);
  295. GFlow.Dock? targeted_dock = null;
  296. Point pos = {(int)x,(int)y};
  297. if (n != null) {
  298. if (!n.selected) {
  299. this.unselect_all();
  300. }
  301. Gtk.Allocation alloc;
  302. n.get_allocation(out alloc);
  303. bool cbp = n.gnode.deletable && n.node_renderer.is_on_closebutton(
  304. pos, alloc,
  305. n.border_width
  306. );
  307. if (cbp) {
  308. this.close_button_pressed = true;
  309. this.unselect_all();
  310. }
  311. targeted_dock = n.node_renderer.get_dock_on_position(
  312. pos, n.get_dock_renderers(),
  313. n.border_width, alloc, n.title
  314. );
  315. if (targeted_dock != null) {
  316. this.drag_dock = targeted_dock;
  317. this.drag_dock.active = true;
  318. int startpos_x = 0, startpos_y = 0;
  319. if (this.drag_dock is GFlow.Sink && this.drag_dock.is_linked()){
  320. GFlow.Source s = ((GFlow.Sink)this.drag_dock).sources.last().nth_data(0);
  321. Node srcnode = this.get_node_from_gflow_node(s.node);
  322. Gtk.Allocation src_alloc;
  323. srcnode.get_allocation(out src_alloc);
  324. if (!srcnode.node_renderer.get_dock_position(
  325. s, srcnode.get_dock_renderers(),
  326. (int)srcnode.border_width, src_alloc,
  327. out startpos_x, out startpos_y,
  328. srcnode.title )) {
  329. warning("No dock on position. Aborting drag");
  330. return false;
  331. }
  332. Point startpos = {startpos_x,startpos_y};
  333. this.temp_connector = {startpos.x, startpos.y,
  334. (int)x-startpos.x, (int)y-startpos.y};
  335. } else {
  336. if (!n.node_renderer.get_dock_position(
  337. this.drag_dock, n.get_dock_renderers(),
  338. (int)n.border_width, alloc,
  339. out startpos_x, out startpos_y,
  340. n.title )) {
  341. warning("No dock on position. Aborting drag");
  342. return false;
  343. }
  344. Point startpos = {startpos_x,startpos_y};
  345. this.temp_connector = {startpos.x, startpos.y, 0, 0};
  346. }
  347. this.queue_draw();
  348. return true;
  349. }
  350. }
  351. // Set a new drag node.
  352. if (n != null) {
  353. Gtk.Allocation alloc;
  354. n.get_allocation(out alloc);
  355. bool on_resize = n.gnode.resizable && n.node_renderer.is_on_resize_handle(
  356. pos, alloc,
  357. n.border_width
  358. );
  359. if (on_resize && this.resize_node == null) {
  360. this.resize_node = n;
  361. this.resize_node.get_allocation(out alloc);
  362. if ((this.get_style_context().get_state() & Gtk.StateFlags.DIR_LTR) > 0) {
  363. this.resize_start_x = alloc.width;
  364. this.resize_start_y = alloc.height;
  365. } else {
  366. this.resize_start_x = 0;
  367. this.resize_start_y = alloc.height;
  368. }
  369. } else if (this.resize_node == null && this.drag_node == null) {
  370. this.drag_node = n;
  371. this.drag_node.get_allocation(out alloc);
  372. } else {
  373. return false;
  374. }
  375. this.drag_start_x = x;
  376. this.drag_start_y = y;
  377. this.drag_diff_x = (int)this.drag_start_x - alloc.x;
  378. this.drag_diff_y = (int)this.drag_start_y - alloc.y;
  379. } else {
  380. this.unselect_all();
  381. this.rubber_alloc = {(int)x, (int)y, 0, 0};
  382. this.rubber_start_x = (int)x;
  383. this.rubber_start_y = (int)y;
  384. }
  385. return false;
  386. }
  387. /**
  388. * Every currently selected node is being unselected
  389. */
  390. public void unselect_all() {
  391. foreach (Node n in this.nodes) {
  392. n.selected = false;
  393. }
  394. this.queue_draw();
  395. }
  396. private List<Node> get_selected_nodes() {
  397. var result = new List<Node>();
  398. foreach (Node n in this.nodes) {
  399. if (n.selected)
  400. result.append(n);
  401. }
  402. return result;
  403. }
  404. /**
  405. * Returns each {@link GFlow.Node} that is currently selected
  406. */
  407. public List<GFlow.Node> get_selected() {
  408. var result = new List<GFlow.Node>();
  409. foreach (Node n in this.nodes) {
  410. if (n.selected)
  411. result.append(n.gnode);
  412. }
  413. return result;
  414. }
  415. //Empty remove implementation to avoid warning message
  416. /**
  417. * Empty default implementation. Do not use. To remove {@link GFlow.Node}s
  418. * from a NodeView please use {@link NodeView.remove_node}
  419. */
  420. public override void remove(Gtk.Widget w) {}
  421. private bool do_button_release_event(int n, double x, double y) {
  422. if (!this.editable)
  423. return false;
  424. // Determine if this was a closebutton press
  425. if (this.close_button_pressed) {
  426. Node? n = this.get_node_on_position(x, y);
  427. if (n != null) {
  428. Point pos = {(int)x,(int)y};
  429. Gtk.Allocation alloc;
  430. n.get_allocation(out alloc);
  431. bool cbp = n.node_renderer.is_on_closebutton(
  432. pos, alloc,
  433. n.border_width
  434. );
  435. if (cbp) {
  436. this.remove_node(n.gnode);
  437. this.close_button_pressed = false;
  438. return true;
  439. }
  440. }
  441. }
  442. // Try to build a new connection
  443. if (this.drag_dock != null) {
  444. try {
  445. if (this.drag_dock is GFlow.Source && this.drop_dock is GFlow.Sink) {
  446. ((GFlow.Source)this.drag_dock).link(this.drop_dock as GFlow.Sink);
  447. }
  448. else if (this.drag_dock is GFlow.Sink && this.drop_dock is GFlow.Source) {
  449. ((GFlow.Source)this.drop_dock).link(this.drag_dock as GFlow.Sink);
  450. }
  451. else if (this.drag_dock is GFlow.Sink && this.drop_dock is GFlow.Sink) {
  452. GFlow.Source? src = ((GFlow.Sink)this.drag_dock).sources.last().nth_data(0);
  453. if (src != null) {
  454. src.unlink(this.drag_dock as GFlow.Sink);
  455. src.link(this.drop_dock as GFlow.Sink);
  456. }
  457. }
  458. else if (this.drag_dock is GFlow.Sink && this.drop_dock == null) {
  459. GFlow.Source? src = ((GFlow.Sink)this.drag_dock).sources.last().nth_data(0);
  460. if (src != null) {
  461. src.unlink((GFlow.Sink)this.drag_dock);
  462. }
  463. }
  464. } catch (GLib.Error e) {
  465. warning(e.message);
  466. }
  467. }
  468. this.stop_dragging();
  469. this.queue_draw();
  470. return false;
  471. }
  472. private void stop_dragging() {
  473. this.drag_start_x = 0;
  474. this.drag_start_y = 0;
  475. this.drag_diff_x = 0;
  476. this.drag_diff_y = 0;
  477. this.rubber_alloc = null;
  478. this.drag_node = null;
  479. if (this.drag_dock != null) {
  480. this.drag_dock.active = false;
  481. }
  482. this.drag_dock = null;
  483. if (this.drop_dock != null) {
  484. this.drop_dock.active = false;
  485. }
  486. this.drop_dock = null;
  487. this.temp_connector = null;
  488. this.drag_threshold_fulfilled = false;
  489. this.resize_node = null;
  490. this.get_window().set_cursor(null);
  491. }
  492. private Gdk.Cursor resize_cursor = null;
  493. private Gdk.Cursor? get_resize_cursor() {
  494. if (resize_cursor == null && this.get_realized()) {
  495. if ((this.get_style_context().get_state() & Gtk.StateFlags.DIR_LTR) > 0 ){
  496. resize_cursor = new Gdk.Cursor.for_display(
  497. this.get_window().get_display(),
  498. Gdk.CursorType.BOTTOM_RIGHT_CORNER
  499. );
  500. } else {
  501. resize_cursor = new Gdk.Cursor.for_display(
  502. this.get_window().get_display(),
  503. Gdk.CursorType.BOTTOM_LEFT_CORNER
  504. );
  505. }
  506. }
  507. return resize_cursor;
  508. }
  509. private bool do_motion_notify_event(double x, double y) {
  510. if (!this.editable)
  511. return false;
  512. // Check if we are on a node. If yes, check if we are
  513. // currently pointing on a dock. if this is true, we
  514. // Want to draw a new connector instead of dragging the node
  515. Node? n = this.get_node_on_position(x, y);
  516. GFlow.Dock? targeted_dock = null;
  517. if (n != null) {
  518. Point pos = {(int)x, (int)y};
  519. Gtk.Allocation alloc;
  520. n.get_allocation(out alloc);
  521. // If the close button is pressed, deactivate it when
  522. // we moved the node
  523. if (this.close_button_pressed) {
  524. bool cbp = n.node_renderer.is_on_closebutton(
  525. pos, alloc,
  526. n.border_width
  527. );
  528. if (!cbp)
  529. this.close_button_pressed = false;
  530. }
  531. // Update cursor if we are on the resize area
  532. bool on_resize = n.gnode.resizable && n.node_renderer.is_on_resize_handle(
  533. pos, alloc,
  534. n.border_width
  535. );
  536. // TODO: find replacement for cursor behaviour
  537. /*if (on_resize)
  538. this.get_window().set_cursor(this.get_resize_cursor());
  539. else if (this.resize_node == null)
  540. this.get_window().set_cursor(null);
  541. */
  542. targeted_dock = n.node_renderer.get_dock_on_position(
  543. pos, n.get_dock_renderers(),
  544. n.border_width, alloc, n.title
  545. );
  546. if (this.drag_dock == null && targeted_dock != this.hovered_dock) {
  547. this.set_hovered_dock(targeted_dock);
  548. }
  549. else if (this.drag_dock != null && targeted_dock != null
  550. && targeted_dock != this.hovered_dock
  551. && this.is_suitable_target(this.drag_dock, targeted_dock)) {
  552. this.set_hovered_dock(targeted_dock);
  553. }
  554. } else {
  555. // If we are leaving the node we will also have to
  556. // un-highlight the last hovered dock
  557. if (this.hovered_dock != null)
  558. this.hovered_dock.highlight = false;
  559. this.hovered_dock = null;
  560. this.queue_draw();
  561. // Update cursor to be default as we are guaranteed not on any
  562. // resize handle outside of any node.
  563. // The check for resize node is a cosmetical fix. If there is a
  564. // Node bing resized in haste, the cursor tends to flicker
  565. if (this.resize_node == null)
  566. this.get_window().set_cursor(null);
  567. }
  568. // Check if the cursor has been dragged a few pixels (defined by DRAG_THRESHOLD)
  569. // If yes, actually start dragging
  570. if ( ( this.drag_node != null || this.drag_dock != null || this.resize_node != null)
  571. && (Math.fabs(drag_start_x - x) > NodeView.DRAG_THRESHOLD
  572. || Math.fabs(drag_start_y - y) > NodeView.DRAG_THRESHOLD )) {
  573. this.drag_threshold_fulfilled = true;
  574. }
  575. // Actually something
  576. if (this.drag_threshold_fulfilled ) {
  577. Gtk.Allocation alloc;
  578. if (this.drag_node != null) {
  579. // Actually move the node(s)
  580. var nodes_to_drag = this.get_selected_nodes();
  581. Gtk.Allocation drag_node_alloc;
  582. this.drag_node.get_allocation(out drag_node_alloc);
  583. if (nodes_to_drag.length() == 0) {
  584. nodes_to_drag.append(this.drag_node);
  585. }
  586. Gtk.Allocation union = {0,0,0,0};
  587. bool first = true;
  588. Point upperleft = {int.MAX,int.MAX};
  589. foreach (Node node in nodes_to_drag) {
  590. node.get_allocation(out alloc);
  591. upperleft.x = (int)Math.fmin(alloc.x, upperleft.x);
  592. upperleft.y = (int)Math.fmin(alloc.y, upperleft.y);
  593. alloc.x = (int)Math.fmax(0,(int)x - this.drag_diff_x);
  594. alloc.y = (int)Math.fmax(0,(int)y - this.drag_diff_y);
  595. if (first) {
  596. union = alloc;
  597. } else {
  598. Gdk.Rectangle tmp;
  599. union.union(alloc, out tmp);
  600. union = (Gtk.Allocation)tmp;
  601. }
  602. }
  603. foreach (Node node in nodes_to_drag) {
  604. node.get_allocation(out alloc);
  605. int dn_diff_x = alloc.x - drag_node_alloc.x;
  606. int dn_diff_y = alloc.y - drag_node_alloc.y;
  607. int ul_diff_x = alloc.x - upperleft.x;
  608. int ul_diff_y = alloc.y - upperleft.y;
  609. alloc.x = (int)Math.fmax(ul_diff_x,(int)x - this.drag_diff_x + dn_diff_x);
  610. alloc.y = (int)Math.fmax(ul_diff_y,(int)y - this.drag_diff_y + dn_diff_y);
  611. node.size_allocate(alloc.width, alloc.height, -1);
  612. }
  613. }
  614. if (this.drag_dock != null) {
  615. // Manipulate the temporary connector
  616. this.temp_connector.width = (int)x-this.temp_connector.x;
  617. this.temp_connector.height = (int)y-this.temp_connector.y;
  618. if (targeted_dock == null) {
  619. this.set_drop_dock(null);
  620. }
  621. else if (this.is_suitable_target(this.drag_dock, targeted_dock))
  622. this.set_drop_dock(targeted_dock);
  623. }
  624. if (this.resize_node != null) {
  625. // resize the node
  626. this.resize_node.get_allocation(out alloc);
  627. if ((this.get_style_context().get_state() & Gtk.StateFlags.DIR_LTR) > 0 ){
  628. alloc.width = resize_start_x + (int)x - (int)this.drag_start_x;
  629. alloc.height = resize_start_y + (int)y - (int)this.drag_start_y;
  630. } else {
  631. //FIXME: far from perfect. strange behaviour when resizing
  632. alloc.x = (int)x;
  633. alloc.width = alloc.width + ((int)this.drag_start_x - (int)x);
  634. alloc.height = resize_start_y + (int)y - (int)this.drag_start_y;
  635. }
  636. this.resize_node.size_allocate(alloc.width, alloc.height, -1);
  637. }
  638. this.allocate_minimum();
  639. this.queue_draw();
  640. }
  641. if (this.rubber_alloc != null) {
  642. if (x >= this.rubber_start_x) {
  643. this.rubber_alloc.x = (int)this.rubber_start_x;
  644. this.rubber_alloc.width = (int)x - this.rubber_alloc.x;
  645. } else {
  646. this.rubber_alloc.x = (int)x;
  647. this.rubber_alloc.width = this.rubber_start_x - (int)x;
  648. }
  649. if (y >= this.rubber_start_y) {
  650. this.rubber_alloc.y = (int)this.rubber_start_y;
  651. this.rubber_alloc.height = (int)y - this.rubber_alloc.y;
  652. } else {
  653. this.rubber_alloc.y = (int)y;
  654. this.rubber_alloc.height = this.rubber_start_y - (int)y;
  655. }
  656. var selected_nodes = this.get_nodes_in_rect(this.rubber_alloc);
  657. foreach (Node node in this.nodes) {
  658. node.selected = selected_nodes.index(node) != -1;
  659. }
  660. this.allocate_minimum();
  661. this.queue_draw();
  662. }
  663. return false;
  664. }
  665. /**
  666. * Allocates the minimum size needed to draw the
  667. * contained nodes at their respective positions
  668. * with their respective sizes to this NodeView
  669. * If the minimunsize is smaller than the containing
  670. * parent (which ideally should be a scrolledwindow
  671. * scrolledwindow) the parent's size will be allocated
  672. * in order to prevent the nodeview from collapsing
  673. * to a size of 0x0 px thus being inable to receive
  674. * mouse-events
  675. */
  676. private void allocate_minimum() {
  677. int minwidth = 0, minheight = 0, _ = 0;
  678. this.get_preferred_width(out minwidth, out _);
  679. this.get_preferred_height(out minheight, out _);
  680. this.set_size_request(minwidth, minheight);
  681. Gtk.Allocation nv_alloc;
  682. this.get_allocation(out nv_alloc);
  683. nv_alloc.width = minwidth;
  684. nv_alloc.height = minheight;
  685. if (this.parent != null) {
  686. Gtk.Allocation palloc;
  687. this.parent.get_allocation(out palloc);
  688. nv_alloc.width = int.max(minwidth, palloc.width);
  689. nv_alloc.height = int.max(minheight, palloc.height);
  690. }
  691. this.size_allocate(nv_alloc.width, nv_alloc.height, -1);
  692. }
  693. /**
  694. * Calculates the NodeView's minimum and preferred widths
  695. */
  696. public new void get_preferred_width(out int minimum_width, out int natural_width) {
  697. double x_min = 0, x_max = 0;
  698. Gtk.Allocation alloc;
  699. foreach (Node n in this.nodes) {
  700. n.get_allocation(out alloc);
  701. x_min = Math.fmin(x_min, alloc.x);
  702. x_max = Math.fmax(x_max, alloc.x+alloc.width);
  703. }
  704. if (this.rubber_alloc != null) {
  705. x_min = Math.fmin(x_min, rubber_alloc.x);
  706. x_max = Math.fmax(x_max, rubber_alloc.x + rubber_alloc.width);
  707. }
  708. if (this.temp_connector != null) {
  709. x_min = Math.fmin(x_min, temp_connector.x);
  710. x_max = Math.fmax(x_max, temp_connector.x + temp_connector.width);
  711. }
  712. x_min = Math.fmax(0, x_min);
  713. minimum_width = natural_width = (int)x_max - (int)x_min;
  714. }
  715. /**
  716. * Calculates the NodeView's minimum and preferred heights
  717. */
  718. public new void get_preferred_height(out int minimum_height, out int natural_height) {
  719. double y_min = 0, y_max = 0;
  720. Gtk.Allocation alloc;
  721. foreach (Node n in this.nodes) {
  722. n.get_allocation(out alloc);
  723. y_min = Math.fmin(y_min, alloc.y);
  724. y_max = Math.fmax(y_max, alloc.y+alloc.height);
  725. }
  726. if (this.rubber_alloc != null) {
  727. y_min = Math.fmin(y_min, rubber_alloc.y);
  728. y_max = Math.fmax(y_max, rubber_alloc.y + rubber_alloc.height);
  729. }
  730. if (this.temp_connector != null) {
  731. y_min = Math.fmin(y_min, temp_connector.y);
  732. y_max = Math.fmax(y_max, temp_connector.y + temp_connector.height);
  733. }
  734. y_min = Math.fmax(0, y_min);
  735. minimum_height = natural_height = (int)y_max - (int)y_min;
  736. }
  737. /**
  738. * Determines wheter one dock can be dropped on another
  739. */
  740. private bool is_suitable_target (GFlow.Dock from, GFlow.Dock to) {
  741. // Check whether the docks have the same type
  742. if (!from.has_same_type(to))
  743. return false;
  744. // Check if the target would lead to a recursion
  745. // If yes, return the value of allow_recursion. If this
  746. // value is set to true, it's completely fine to have
  747. // a recursive graph
  748. if (to is GFlow.Source && from is GFlow.Sink) {
  749. if (!this.allow_recursion)
  750. if (from.node.is_recursive_forward(to.node) ||
  751. to.node.is_recursive_backward(from.node))
  752. return false;
  753. }
  754. if (to is GFlow.Sink && from is GFlow.Source) {
  755. if (!this.allow_recursion)
  756. if (to.node.is_recursive_forward(from.node) ||
  757. from.node.is_recursive_backward(to.node))
  758. return false;
  759. }
  760. if (to is GFlow.Sink && from is GFlow.Sink) {
  761. GFlow.Source? s = ((GFlow.Sink)from).sources.last().nth_data(0);
  762. if (s == null)
  763. return false;
  764. if (!this.allow_recursion)
  765. if (to.node.is_recursive_forward(s.node) ||
  766. s.node.is_recursive_backward(to.node))
  767. return false;
  768. }
  769. // If the from from-target is a sink, check if the
  770. // to target is either a source which does not belong to the own node
  771. // or if the to target is another sink (this is valid as we can
  772. // move a connection from one sink to another
  773. if (from is GFlow.Sink
  774. && ((to is GFlow.Sink
  775. && to != from)
  776. || (to is GFlow.Source
  777. && (!to.node.has_dock(from) || this.allow_recursion)))) {
  778. return true;
  779. }
  780. // Check if the from-target is a source. if yes, make sure the
  781. // to-target is a sink and it does not belong to the own node
  782. else if (from is GFlow.Source
  783. && to is GFlow.Sink
  784. && (!to.node.has_dock(from) || this.allow_recursion)) {
  785. return true;
  786. }
  787. return false;
  788. }
  789. /**
  790. * Sets the dock that is currently being hovered over to drop
  791. * a connector on
  792. */
  793. private void set_drop_dock(GFlow.Dock? d) {
  794. if (this.drop_dock != null)
  795. this.drop_dock.active = false;
  796. this.drop_dock = d;
  797. if (this.drop_dock != null)
  798. this.drop_dock.active = true;
  799. this.queue_draw();
  800. }
  801. /**
  802. * Sets the dock that is currently being hovered over
  803. */
  804. private void set_hovered_dock(GFlow.Dock? d) {
  805. if (this.hovered_dock != null)
  806. this.hovered_dock.highlight = false;
  807. this.hovered_dock = d;
  808. if (this.hovered_dock != null)
  809. this.hovered_dock.highlight = true;
  810. this.queue_draw();
  811. }
  812. /**
  813. * Manually set the position of the given {@link GFlow.Node} on this nodeview
  814. */
  815. public void set_node_position(GFlow.Node gn, int x, int y) {
  816. Node n = this.get_node_from_gflow_node(gn);
  817. n.set_position(x,y);
  818. this.allocate_minimum();
  819. this.queue_draw();
  820. }
  821. /**
  822. * Manually set the allocation of the given {@link GFlow.Node} on this nodeview
  823. */
  824. public void set_node_allocation(GFlow.Node gn, Gtk.Allocation alloc) {
  825. Node n = this.get_node_from_gflow_node(gn);
  826. n.set_allocation(alloc);
  827. this.allocate_minimum();
  828. this.queue_draw();
  829. }
  830. /**
  831. * Return the position of the given {@link GFlow.Node} on this nodeview
  832. */
  833. public unowned Point get_node_position(GFlow.Node gn) {
  834. Node n = this.get_node_from_gflow_node(gn);
  835. return n.get_position();
  836. }
  837. /**
  838. * Returns the allocation of the given {@link GFlow.Node}
  839. */
  840. public unowned Gtk.Allocation get_node_allocation(GFlow.Node gn) {
  841. Gtk.Allocation alloc;
  842. Node n = this.get_node_from_gflow_node(gn);
  843. n.get_allocation(out alloc);
  844. return alloc;
  845. }
  846. private new void snapshot(Gtk.Snapshot sn) {
  847. Cairo.Context cr = sn.append_cairo();
  848. Gtk.StyleContext sc = this.get_style_context();
  849. Gtk.Allocation nv_alloc;
  850. this.get_allocation(out nv_alloc);
  851. sc.render_background(cr, 0, 0, nv_alloc.width, nv_alloc.height);
  852. // Draw nodes
  853. this.nodes.reverse();
  854. foreach (Node n in this.nodes) {
  855. n.current_cairo_ctx = cr;
  856. Gtk.Allocation alloc;
  857. n.get_allocation(out alloc);
  858. var node_properties = NodeProperties();
  859. node_properties.editable = this.editable;
  860. node_properties.deletable = n.gnode.deletable;
  861. node_properties.resizable = n.gnode.resizable;
  862. node_properties.selected = n.selected;
  863. n.node_renderer.draw_node(
  864. this,
  865. cr,
  866. alloc,
  867. n.get_dock_renderers(),
  868. n.get_childlist(),
  869. (int)n.border_width,
  870. node_properties,
  871. n.title
  872. );
  873. if (n.highlight_color != null) {
  874. var hl = n.highlight_color;
  875. Gtk.Allocation node_alloc;
  876. n.get_allocation(out node_alloc);
  877. cr.save();
  878. cr.set_source_rgba(hl.red, hl.green, hl.blue, 0.3);
  879. cr.rectangle(node_alloc.x, node_alloc.y, node_alloc.width, node_alloc.height);
  880. cr.fill();
  881. cr.restore();
  882. cr.save();
  883. cr.set_source_rgba(hl.red, hl.green, hl.blue, 0.9);
  884. cr.rectangle(node_alloc.x, node_alloc.y, node_alloc.width, node_alloc.height);
  885. cr.stroke();
  886. cr.restore();
  887. }
  888. n.current_cairo_ctx = null;
  889. }
  890. this.nodes.reverse();
  891. // Draw connectors
  892. foreach (Node n in this.nodes) {
  893. foreach(GFlow.Source source in n.gnode.get_sources()) {
  894. Gtk.Allocation alloc;
  895. n.get_allocation(out alloc);
  896. int source_pos_x = 0, source_pos_y = 0;
  897. if (!n.node_renderer.get_dock_position(
  898. source,
  899. n.get_dock_renderers(),
  900. (int)n.border_width,
  901. alloc, out source_pos_x, out source_pos_y,
  902. n.title)) {
  903. warning("No dock on position. Ommiting connector");
  904. continue;
  905. }
  906. Point source_pos = {source_pos_x,source_pos_y};
  907. foreach(GFlow.Sink sink in source.sinks) {
  908. // Don't draw the connection to a sink if we are dragging it
  909. if (sink == this.drag_dock && source == sink.sources.last().nth_data(0))
  910. continue;
  911. Node? sink_node = this.get_node_from_gflow_node(sink.node);
  912. sink_node.get_allocation(out alloc);
  913. int sink_pos_x = 0, sink_pos_y = 0;
  914. if (!sink_node.node_renderer.get_dock_position(
  915. sink,
  916. sink_node.get_dock_renderers(),
  917. (int)sink_node.border_width,
  918. alloc, out sink_pos_x, out sink_pos_y,
  919. sink_node.title )) {
  920. warning("No dock on position. Ommiting connector");
  921. continue;
  922. }
  923. Point sink_pos = {sink_pos_x,sink_pos_y};
  924. int w = sink_pos.x - source_pos.x;
  925. int h = sink_pos.y - source_pos.y;
  926. cr.save();
  927. double r=0, g=0, b=0;
  928. if (source != null && source.get_last_value() != null) {
  929. string hexcol = color_calculation(source.get_last_value());
  930. this.hex2col(hexcol ,out r, out g, out b);
  931. cr.set_source_rgba(r,g,b,1.0);
  932. } else {
  933. this.hex2col(default_connector_color ,out r, out g, out b);
  934. }
  935. cr.set_source_rgba(r,g,b,1.0);
  936. cr.move_to(source_pos.x, source_pos.y);
  937. if (w > 0) {
  938. cr.rel_curve_to(w/3,0,2*w/3,h,w,h);
  939. } else {
  940. cr.rel_curve_to(-w/3,0,1.3*w,h,w,h);
  941. }
  942. cr.stroke();
  943. cr.restore();
  944. }
  945. }
  946. }
  947. // Draw temporary connector if any
  948. if (this.temp_connector != null) {
  949. int w = this.temp_connector.width;
  950. int h = this.temp_connector.height;
  951. cr.move_to(this.temp_connector.x, this.temp_connector.y);
  952. if (w > 0) {
  953. cr.rel_curve_to(w/3,0,2*w/3,h,w,h);
  954. } else {
  955. cr.rel_curve_to(-w/3,0,1.3*w,h,w,h);
  956. }
  957. cr.stroke();
  958. }
  959. // Draw rubberband
  960. if (this.rubber_alloc != null) {
  961. draw_rubberband(this, cr,
  962. this.rubber_alloc.x, this.rubber_alloc.y,
  963. Gtk.StateFlags.NORMAL,
  964. &this.rubber_alloc.width, &this.rubber_alloc.height);
  965. }
  966. // Draw placeholder if there are no nodes
  967. if (this.nodes.length() == 0) {
  968. sc.save();
  969. cr.save();
  970. sc.add_class(Gtk.STYLE_CLASS_BUTTON);
  971. Gdk.RGBA col = sc.get_color(Gtk.StateFlags.NORMAL);
  972. cr.set_source_rgba(col.red,col.green,col.blue,col.alpha);
  973. int placeholder_width, placeholder_height;
  974. this.placeholder_layout.get_pixel_size(out placeholder_width, out placeholder_height);
  975. cr.move_to(nv_alloc.width / 2 - placeholder_width / 2,
  976. nv_alloc.height / 2 - placeholder_height / 2);
  977. Pango.cairo_show_layout(cr, this.placeholder_layout);
  978. cr.restore();
  979. sc.restore();
  980. }
  981. return false;
  982. }
  983. private void hex2col(string hex, out double r, out double g, out double b) {
  984. string hexdigits ="0123456789abcdef";
  985. r = col_h2f(hexdigits.index_of_char(hex[0]) * 16 + hexdigits.index_of_char(hex[1]));
  986. g = col_h2f(hexdigits.index_of_char(hex[2]) * 16 + hexdigits.index_of_char(hex[3]));
  987. b = col_h2f(hexdigits.index_of_char(hex[4]) * 16 + hexdigits.index_of_char(hex[5]));
  988. }
  989. private double col_h2f(uint col) {
  990. return col/255.0f;
  991. }
  992. private uint col_f2h(double col) {
  993. return (uint)int.parse( (col*255.0d).to_string() ).abs();
  994. }
  995. /**
  996. * Internal method to initialize this NodeView as a {@link Gtk.Widget}
  997. */
  998. public override void realize() {
  999. /*Gtk.Allocation alloc;
  1000. this.get_allocation(out alloc);
  1001. var attr = Gdk.WindowAttr();
  1002. attr.window_type = Gdk.WindowType.CHILD;
  1003. attr.x = alloc.x;
  1004. attr.y = alloc.y;
  1005. attr.width = alloc.width;
  1006. attr.height = alloc.height;
  1007. attr.visual = this.get_visual();
  1008. attr.event_mask = this.get_events()
  1009. | Gdk.EventMask.POINTER_MOTION_MASK
  1010. | Gdk.EventMask.BUTTON_PRESS_MASK
  1011. | Gdk.EventMask.BUTTON_RELEASE_MASK
  1012. | Gdk.EventMask.LEAVE_NOTIFY_MASK;
  1013. var window = new Gdk.Window(this.get_parent_window(), attr, mask);
  1014. this.set_window(window);
  1015. this.register_window(window);
  1016. this.set_realized(true);*/
  1017. }
  1018. /**
  1019. * Highlight a node in a user-specified color
  1020. *
  1021. * When set to a non-null value, the node will be displayed in the tint
  1022. * of the given color. You can use this to convey status information
  1023. * or to indicate that something with this node is not allright.
  1024. */
  1025. public void set_node_highlight(GFlow.Node gn, Gdk.RGBA? c) {
  1026. Node? n = this.get_node_from_gflow_node(gn);
  1027. if (n == null) {
  1028. warning("Tried to set highlight color to node that does not belong to nodeview.");
  1029. return;
  1030. }
  1031. n.highlight_color = c;
  1032. this.queue_draw();
  1033. }
  1034. }
  1035. public struct Point {
  1036. public int x;
  1037. public int y;
  1038. }
  1039. /**
  1040. * Draw radiobutton.
  1041. * Implemented in drawinghelper.c
  1042. */
  1043. private extern void draw_radio(Gtk.Widget widget,
  1044. Cairo.Context cr,
  1045. int x,
  1046. int y,
  1047. Gtk.StateFlags state,
  1048. int* width,
  1049. int* height);
  1050. /**
  1051. * Draw rubberband selection.
  1052. * Implemented in drawinghelper.c
  1053. */
  1054. private extern void draw_rubberband(Gtk.Widget widget,
  1055. Cairo.Context cr,
  1056. int x,
  1057. int y,
  1058. Gtk.StateFlags state,
  1059. int* width,
  1060. int* height);
  1061. }