nodeview.vala 47 KB

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