nodeview.vala 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  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. private errordomain InternalError {
  23. DOCKS_NOT_SUITABLE
  24. }
  25. private interface MotionQueuedNodeOperation : Object {
  26. public abstract void do_on_nodeview(NodeView nv);
  27. }
  28. private class RemoveNodeOperation : MotionQueuedNodeOperation, Object {
  29. private NodeRenderer n;
  30. public RemoveNodeOperation(NodeRenderer n) {
  31. this.n = n;
  32. }
  33. public void do_on_nodeview(NodeView nv) {
  34. n.n.unlink_all();
  35. var child = nv.get_first_child ();
  36. while (child != null) {
  37. if (child == n) {
  38. child.unparent ();
  39. child = null;
  40. this.n = null;
  41. return;
  42. }
  43. child = child.get_next_sibling();
  44. }
  45. warning("Tried to remove a node that is not a child of nodeview");
  46. }
  47. }
  48. private class NodeViewLayoutManager : Gtk.LayoutManager {
  49. protected override Gtk.SizeRequestMode get_request_mode (Gtk.Widget widget) {
  50. return Gtk.SizeRequestMode.CONSTANT_SIZE;
  51. }
  52. protected override void measure(Gtk.Widget w, Gtk.Orientation o, int for_size, out int min, out int pref, out int min_base, out int pref_base) {
  53. int lower_bound = 0;
  54. int upper_bound = 0;
  55. var c = w.get_first_child();
  56. while (c != null) {
  57. var lc = (NodeViewLayoutChild)this.get_layout_child(c);
  58. switch (o) {
  59. case Gtk.Orientation.HORIZONTAL:
  60. if (lc.x < 0) {
  61. lower_bound = int.min(lc.x, lower_bound);
  62. } else {
  63. upper_bound = int.max(lc.x + c.get_width(), upper_bound);
  64. }
  65. break;
  66. case Gtk.Orientation.VERTICAL:
  67. if (lc.y < 0) {
  68. lower_bound = int.min(lc.y, lower_bound);
  69. } else {
  70. upper_bound = int.max(lc.y + c.get_height(), upper_bound);
  71. }
  72. break;
  73. }
  74. c = c.get_next_sibling();
  75. }
  76. min = upper_bound - lower_bound;
  77. pref = upper_bound - lower_bound;
  78. min_base = -1;
  79. pref_base = -1;
  80. }
  81. protected override void allocate(Gtk.Widget w, int height, int width, int baseline) {
  82. var c = w.get_first_child();
  83. while (c != null) {
  84. int cwidth, cheight, _;
  85. c.measure(Gtk.Orientation.HORIZONTAL, -1, out cwidth, out _, out _, out _);
  86. c.measure(Gtk.Orientation.VERTICAL, -1, out cheight, out _, out _, out _);
  87. var lc = (NodeViewLayoutChild)this.get_layout_child(c);
  88. c.queue_allocate();
  89. c.allocate_size({lc.x,lc.y, cwidth, cheight}, -1);
  90. c = c.get_next_sibling();
  91. }
  92. }
  93. public override Gtk.LayoutChild create_layout_child (Gtk.Widget widget, Gtk.Widget for_child) {
  94. return new NodeViewLayoutChild(for_child, this);
  95. }
  96. }
  97. private class NodeViewLayoutChild : Gtk.LayoutChild {
  98. public int x = 0;
  99. public int y = 0;
  100. public NodeViewLayoutChild(Gtk.Widget w, Gtk.LayoutManager lm) {
  101. Object(child_widget: w, layout_manager: lm);
  102. }
  103. }
  104. /**
  105. * A widget that displays flowgraphs expressed through {@link GFlow} objects
  106. *
  107. * This allows you to add {@link GFlow.Node}s to it in order to display
  108. * A graph of these nodes and their connections.
  109. */
  110. public class NodeView : Gtk.Widget {
  111. construct {
  112. set_css_name("gtkflow_nodeview");
  113. }
  114. /**
  115. * If this property is set to true, the nodeview will not perform
  116. * any check wheter newly created connections will result in cycles
  117. * in the graph. It's completely up to the application programmer
  118. * to make sure that the logic inside the nodes he uses avoids
  119. * endlessly backpropagated loops
  120. */
  121. public bool allow_recursion {get; set; default=false;}
  122. /**
  123. * The eventcontrollers to receive events
  124. */
  125. private Gtk.EventControllerMotion ctr_motion;
  126. private Gtk.GestureClick ctr_click;
  127. /**
  128. * The current extents of the temporary connector
  129. * if null, there is no temporary connector drawn at the moment
  130. */
  131. private Gdk.Rectangle? temp_connector = null;
  132. /**
  133. * The dock that the temporary connector will be attched to
  134. */
  135. private Dock? temp_connected_dock = null;
  136. /**
  137. * The dock that was clicked to invoke the temporary connector
  138. */
  139. private Dock? clicked_dock = null;
  140. /**
  141. * The node that is being moved right now via mouse drag.
  142. * The node that receives the button press event registers
  143. * itself with this property
  144. */
  145. internal NodeRenderer? move_node {get; set; default=null;}
  146. internal NodeRenderer? resize_node {get; set; default=null;}
  147. /**
  148. * A rectangle detailing the extents of a rubber marking
  149. */
  150. private Gdk.Rectangle? mark_rubberband = null;
  151. /**
  152. * Holds a Queue of node operations to be done after motion is done.
  153. */
  154. private Queue<MotionQueuedNodeOperation> queued_operations = new Queue<MotionQueuedNodeOperation>();
  155. /**
  156. * Instantiate a new NodeView
  157. */
  158. public NodeView (){
  159. this.set_layout_manager(new NodeViewLayoutManager());
  160. this.set_size_request(100,100);
  161. this.ctr_motion = new Gtk.EventControllerMotion();
  162. this.add_controller(this.ctr_motion);
  163. this.ctr_motion.motion.connect((x,y)=> { this.process_motion(x,y); });
  164. this.ctr_click = new Gtk.GestureClick();
  165. this.add_controller(this.ctr_click);
  166. this.ctr_click.pressed.connect((n,x,y) => { this.start_marking(n,x,y); });
  167. this.ctr_click.released.connect((n,x,y) => { this.end_temp_connector(n,x,y); });
  168. }
  169. /**
  170. * {@inheritDoc}
  171. */
  172. public override void dispose() {
  173. var nodewidget = this.get_first_child();
  174. while (nodewidget != null) {
  175. var delnode = nodewidget;
  176. nodewidget = nodewidget.get_next_sibling();
  177. delnode.unparent();
  178. }
  179. base.dispose();
  180. }
  181. internal List<unowned NodeRenderer> get_marked_nodes() {
  182. var result = new List<unowned NodeRenderer>();
  183. var nodewidget = this.get_first_child();
  184. while (nodewidget != null) {
  185. var node = (NodeRenderer)nodewidget;
  186. if (node.marked) {
  187. result.append(node);
  188. }
  189. nodewidget = nodewidget.get_next_sibling();
  190. }
  191. return result;
  192. }
  193. private void process_motion(double x, double y) {
  194. if (this.move_node != null && this.layout_manager != null) {
  195. var lc = (NodeViewLayoutChild) this.layout_manager.get_layout_child(this.move_node);
  196. int old_x = lc.x;
  197. int old_y = lc.y;
  198. lc.x = (int)(x-this.move_node.click_offset_x);
  199. lc.y = (int)(y-this.move_node.click_offset_y);
  200. if (this.move_node.marked) {
  201. foreach (NodeRenderer n in this.get_marked_nodes()) {
  202. if (n == this.move_node) continue;
  203. var mlc = (NodeViewLayoutChild) this.layout_manager.get_layout_child(n);
  204. mlc.x -= old_x - lc.x;
  205. mlc.y -= old_y - lc.y;
  206. }
  207. }
  208. }
  209. if (this.resize_node != null) {
  210. int d_x, d_y;
  211. Gtk.Allocation node_alloc;
  212. this.resize_node.get_allocation(out node_alloc);
  213. d_x = (int)(x-this.resize_node.click_offset_x-node_alloc.x);
  214. d_y = (int)(y-this.resize_node.click_offset_y-node_alloc.y);
  215. int new_width = (int)this.resize_node.resize_start_width+d_x;
  216. int new_height = (int)this.resize_node.resize_start_height+d_y;
  217. this.resize_node.set_size_request(new_width, new_height);
  218. }
  219. if (this.temp_connector != null) {
  220. var n = (NodeRenderer)this.retrieve_node(this.temp_connected_dock.d.node);
  221. this.temp_connector.width = (int)(x - this.temp_connector.x-n.get_margin());
  222. this.temp_connector.height = (int)(y - this.temp_connector.y-n.get_margin());
  223. }
  224. if (this.mark_rubberband != null) {
  225. this.mark_rubberband.width = (int)(x - this.mark_rubberband.x);
  226. this.mark_rubberband.height = (int)(y - this.mark_rubberband.y);
  227. var nodewidget = this.get_first_child();
  228. Gtk.Allocation node_alloc;
  229. Gdk.Rectangle absolute_marked = this.mark_rubberband;
  230. if (absolute_marked.width < 0) {
  231. absolute_marked.width *= -1;
  232. absolute_marked.x -= absolute_marked.width;
  233. }
  234. if (absolute_marked.height < 0) {
  235. absolute_marked.height *= -1;
  236. absolute_marked.y -= absolute_marked.height;
  237. }
  238. Gdk.Rectangle result;
  239. while (nodewidget != null) {
  240. var node = (NodeRenderer)nodewidget;
  241. node.get_allocation(out node_alloc);
  242. node_alloc.intersect(absolute_marked, out result);
  243. node.marked = result == node_alloc;
  244. nodewidget = node.get_next_sibling();
  245. }
  246. }
  247. this.queue_allocate();
  248. var item = queued_operations.pop_head();
  249. if (item != null) {
  250. item.do_on_nodeview(this);
  251. while ((item = queued_operations.pop_head ()) != null) {
  252. item.do_on_nodeview(this);
  253. }
  254. }
  255. }
  256. private void start_marking(int n_clicks, double x, double y) {
  257. if (this.pick(x,y, Gtk.PickFlags.DEFAULT) == this)
  258. this.mark_rubberband = {(int)x,(int)y,0,0};
  259. }
  260. internal void start_temp_connector(Dock d) {
  261. this.clicked_dock = d;
  262. if (d.d is GFlow.Sink && d.d.is_linked()) {
  263. var sink = (GFlow.Sink)d.d;
  264. this.temp_connected_dock = this.retrieve_dock(sink.sources.last().nth_data(0));
  265. } else {
  266. this.temp_connected_dock = d;
  267. }
  268. var node = this.retrieve_node(this.temp_connected_dock.d.node);
  269. Gtk.Allocation node_alloc, dock_alloc;
  270. node.get_allocation(out node_alloc);
  271. this.temp_connected_dock.get_allocation(out dock_alloc);
  272. var x = node_alloc.x + dock_alloc.x + 8;
  273. var y = node_alloc.y + dock_alloc.y + 8;
  274. this.temp_connector = {x, y, 0, 0};
  275. }
  276. internal void end_temp_connector(int n_clicks, double x, double y) {
  277. if (this.temp_connector != null) {
  278. var w = this.pick(x,y,Gtk.PickFlags.DEFAULT);
  279. if (w is Dock) {
  280. var pd = (Dock)w;
  281. if (pd.d is GFlow.Source && this.temp_connected_dock.d is GFlow.Sink
  282. || pd.d is GFlow.Sink && this.temp_connected_dock.d is GFlow.Source) {
  283. try {
  284. if (!this.is_suitable_target(pd.d, this.temp_connected_dock.d)) {
  285. throw new InternalError.DOCKS_NOT_SUITABLE("Can't link because is no good");
  286. }
  287. pd.d.link(this.temp_connected_dock.d);
  288. } catch (Error e) {
  289. warning("Could not link: "+e.message);
  290. }
  291. }
  292. else if (pd.d is GFlow.Sink && this.clicked_dock != null
  293. && this.clicked_dock.d is GFlow.Sink
  294. && this.temp_connected_dock is GFlow.Source) {
  295. try {
  296. if (!this.is_suitable_target(pd.d, this.temp_connected_dock.d)) {
  297. throw new InternalError.DOCKS_NOT_SUITABLE("Can't link because is no good");
  298. }
  299. this.clicked_dock.d.unlink(this.temp_connected_dock.d);
  300. pd.d.link(this.temp_connected_dock.d);
  301. } catch (Error e) {
  302. warning("Could not edit links: "+e.message);
  303. }
  304. }
  305. pd.queue_draw();
  306. } else {
  307. if (this.temp_connected_dock.d is GFlow.Source
  308. && this.clicked_dock != null
  309. && this.clicked_dock.d is GFlow.Sink) {
  310. try {
  311. this.clicked_dock.d.unlink(this.temp_connected_dock.d);
  312. } catch (Error e) {
  313. warning("Could not unlink: "+e.message);
  314. }
  315. }
  316. }
  317. this.queue_draw();
  318. this.temp_connected_dock.queue_draw();
  319. if (this.clicked_dock != null) {
  320. this.clicked_dock.queue_draw();
  321. }
  322. this.clicked_dock = null;
  323. this.temp_connected_dock = null;
  324. this.temp_connector = null;
  325. }
  326. this.update_extents();
  327. this.queue_resize();
  328. this.mark_rubberband = null;
  329. this.queue_allocate();
  330. }
  331. private void update_extents() {
  332. int min_x=0, min_y = 0;
  333. NodeViewLayoutChild lc;
  334. var child = this.get_first_child();
  335. while (child != null) {
  336. lc = (NodeViewLayoutChild)this.layout_manager.get_layout_child(child);
  337. min_x = int.min(min_x, lc.x);
  338. min_y = int.min(min_y, lc.y);
  339. child = child.get_next_sibling();
  340. }
  341. if (min_x >= 0 && min_y >= 0) {
  342. return;
  343. }
  344. child = this.get_first_child();
  345. while (child != null) {
  346. lc = (NodeViewLayoutChild)this.layout_manager.get_layout_child(child);
  347. if (min_x < 0)
  348. lc.x += -min_x;
  349. if (min_y < 0)
  350. lc.y += -min_y;
  351. child = child.get_next_sibling();
  352. }
  353. var parent = this.get_parent();
  354. if (parent!=null && parent is Gtk.Viewport) {
  355. var scrollwidget = parent.get_parent();
  356. if (parent != null && parent is Gtk.ScrolledWindow) {
  357. var sw = (Gtk.ScrolledWindow)scrollwidget;
  358. sw.hadjustment.value += (double)(-min_x);
  359. sw.vadjustment.value += (double)(-min_y);
  360. }
  361. }
  362. }
  363. /**
  364. * Add a node to this nodeview
  365. */
  366. public void add(NodeRenderer n) {
  367. n.set_parent (this);
  368. }
  369. /**
  370. * Remove a node from this nodeview
  371. */
  372. public void remove(NodeRenderer n) {
  373. queued_operations.push_tail(new RemoveNodeOperation(n));
  374. }
  375. /**
  376. * Retrieve a Node-Widget from this node.
  377. *
  378. * Gives you the {@link GtkFlow.Node}-object that corresponds to the given
  379. * {@link GFlow.Node}. Returns null if the searched Node is not associated
  380. * with any of the Node-Widgets in this nodeview.
  381. */
  382. public NodeRenderer? retrieve_node (GFlow.Node n) {
  383. var c = (NodeRenderer)this.get_first_child();
  384. while (c != null) {
  385. if (!(c is NodeRenderer )) continue;
  386. if (c.n == n) return c;
  387. c = (NodeRenderer)c.get_next_sibling();
  388. }
  389. return null;
  390. }
  391. /**
  392. * Retrieve a Dock-Widget from this nodeview.
  393. *
  394. * Gives you a {@link Dock}-object that corresponds to the given
  395. * {@link GFlow.Dock}. Returns null if the given Dock is not
  396. * associated with any of the Dock-Widgets in this nodeview.
  397. */
  398. public Dock? retrieve_dock (GFlow.Dock d) {
  399. var c = (NodeRenderer)this.get_first_child();
  400. Dock? found = null;
  401. while (c != null) {
  402. if (!(c is NodeRenderer )) {
  403. c = (NodeRenderer)c.get_next_sibling();
  404. continue;
  405. }
  406. found = c.retrieve_dock(d);
  407. if (found != null) return found;
  408. c = (NodeRenderer)c.get_next_sibling();
  409. }
  410. return null;
  411. }
  412. /**
  413. * Determines wheter one dock can be dropped on another
  414. */
  415. private bool is_suitable_target (GFlow.Dock from, GFlow.Dock to) {
  416. // Check whether the docks have the same type
  417. if (!from.has_same_type(to))
  418. return false;
  419. // Check if the target would lead to a recursion
  420. // If yes, return the value of allow_recursion. If this
  421. // value is set to true, it's completely fine to have
  422. // a recursive graph
  423. if (to is GFlow.Source && from is GFlow.Sink) {
  424. if (!this.allow_recursion)
  425. if (from.node.is_recursive_forward(to.node) ||
  426. to.node.is_recursive_backward(from.node))
  427. return false;
  428. }
  429. if (to is GFlow.Sink && from is GFlow.Source) {
  430. if (!this.allow_recursion)
  431. if (to.node.is_recursive_forward(from.node) ||
  432. from.node.is_recursive_backward(to.node))
  433. return false;
  434. }
  435. if (to is GFlow.Sink && from is GFlow.Sink) {
  436. GFlow.Source? s = ((GFlow.Sink)from).sources.last().nth_data(0);
  437. if (s == null)
  438. return false;
  439. if (!this.allow_recursion)
  440. if (to.node.is_recursive_forward(s.node) ||
  441. s.node.is_recursive_backward(to.node))
  442. return false;
  443. }
  444. // If the from from-target is a sink, check if the
  445. // to target is either a source which does not belong to the own node
  446. // or if the to target is another sink (this is valid as we can
  447. // move a connection from one sink to another
  448. if (from is GFlow.Sink
  449. && ((to is GFlow.Sink
  450. && to != from)
  451. || (to is GFlow.Source
  452. && (!to.node.has_dock(from) || this.allow_recursion)))) {
  453. return true;
  454. }
  455. // Check if the from-target is a source. if yes, make sure the
  456. // to-target is a sink and it does not belong to the own node
  457. else if (from is GFlow.Source
  458. && to is GFlow.Sink
  459. && (!to.node.has_dock(from) || this.allow_recursion)) {
  460. return true;
  461. }
  462. return false;
  463. }
  464. internal signal void draw_minimap();
  465. protected override void snapshot (Gtk.Snapshot sn) {
  466. base.snapshot(sn);
  467. var rect = Graphene.Rect().init(0,0,(float)this.get_width(), (float)this.get_height());
  468. var cr = sn.append_cairo(rect);
  469. Gdk.RGBA color = {0.0f,0.0f,0.0f,1.0f};
  470. var c = this.get_first_child();
  471. while (c != null) {
  472. var nr = (NodeRenderer)c;
  473. int tgt_x, tgt_y, src_x, src_y, w, h;
  474. foreach (GFlow.Sink snk in nr.n.get_sinks()) {
  475. var target_dock = this.retrieve_dock(snk);
  476. Gtk.Allocation tgt_alloc, tgt_node_alloc;
  477. target_dock.get_allocation(out tgt_alloc);
  478. nr.get_allocation(out tgt_node_alloc);
  479. foreach (GFlow.Source src in snk.sources) {
  480. if (this.temp_connected_dock != null && src == this.temp_connected_dock.d
  481. && this.clicked_dock != null && snk == this.clicked_dock.d) {
  482. continue;
  483. }
  484. var source_dock = this.retrieve_dock(src);
  485. var source_node = this.retrieve_node(src.node);
  486. Gtk.Allocation src_dock_alloc, src_node_alloc;
  487. source_dock.get_allocation(out src_dock_alloc);
  488. source_node.get_allocation(out src_node_alloc);
  489. src_x = src_dock_alloc.x+src_node_alloc.x+source_node.get_margin() + 8;
  490. src_y = src_dock_alloc.y+src_node_alloc.y+source_node.get_margin() + 8;
  491. tgt_x = tgt_alloc.x+tgt_node_alloc.x+nr.get_margin() + 8;
  492. tgt_y = tgt_alloc.y+tgt_node_alloc.y+nr.get_margin() + 8;
  493. w = tgt_x - src_x;
  494. h = tgt_y - src_y;
  495. var sourcedock = this.retrieve_dock(src);
  496. if (sourcedock != null) {
  497. color = sourcedock.resolve_color(sourcedock, sourcedock.last_value);
  498. }
  499. cr.save();
  500. cr.set_source_rgba(color.red, color.green, color.blue, color.alpha);
  501. cr.move_to(src_x, src_y);
  502. if (w > 0) {
  503. cr.rel_curve_to(w/3,0,2*w/3,h,w,h);
  504. } else {
  505. cr.rel_curve_to(-w/3,0,1.3*w,h,w,h);
  506. }
  507. cr.stroke();
  508. cr.restore();
  509. }
  510. }
  511. c = c.get_next_sibling();
  512. }
  513. this.draw_minimap();
  514. if (this.temp_connector != null) {
  515. color = this.temp_connected_dock.resolve_color(
  516. this.temp_connected_dock, this.temp_connected_dock.last_value
  517. );
  518. var nr = this.retrieve_node(this.temp_connected_dock.d.node);
  519. cr.save();
  520. cr.set_source_rgba(color.red, color.green, color.blue, color.alpha);
  521. cr.move_to(this.temp_connector.x+nr.get_margin(), this.temp_connector.y+nr.get_margin());
  522. cr.rel_curve_to(
  523. this.temp_connector.width/3,
  524. 0,
  525. 2*this.temp_connector.width/3,
  526. this.temp_connector.height,
  527. this.temp_connector.width,
  528. this.temp_connector.height
  529. );
  530. cr.stroke();
  531. cr.restore();
  532. }
  533. if (this.mark_rubberband != null) {
  534. cr.save();
  535. cr.set_source_rgba(0.0, 0.2, 0.9, 0.4);
  536. cr.rectangle(
  537. this.mark_rubberband.x, this.mark_rubberband.y,
  538. this.mark_rubberband.width, this.mark_rubberband.height
  539. );
  540. cr.fill();
  541. cr.set_source_rgba(0.0, 0.2, 1.0, 1.0);
  542. cr.stroke();
  543. }
  544. }
  545. }
  546. }