nodeview.vala 23 KB

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