summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Canvas.cpp4184
-rw-r--r--src/Port.cpp57
-rw-r--r--src/boilerplate.h43
-rw-r--r--src/box.c561
-rw-r--r--src/circle.c467
-rw-r--r--src/color.h63
-rw-r--r--src/edge.c786
-rw-r--r--src/fdgl.hpp166
-rw-r--r--src/ganv-marshal.list4
-rw-r--r--src/ganv-private.h403
-rw-r--r--src/ganv_bench.cpp178
-rw-r--r--src/ganv_test.c119
-rwxr-xr-xsrc/ganv_test.py22
-rw-r--r--src/gettext.h26
-rw-r--r--src/group.c446
-rw-r--r--src/item.c707
-rw-r--r--src/module.c859
-rw-r--r--src/node.c897
-rw-r--r--src/port.c735
-rw-r--r--src/text.c381
-rw-r--r--src/widget.c503
21 files changed, 11607 insertions, 0 deletions
diff --git a/src/Canvas.cpp b/src/Canvas.cpp
new file mode 100644
index 0000000..6a18cdc
--- /dev/null
+++ b/src/Canvas.cpp
@@ -0,0 +1,4184 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2016 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Parts based on GnomeCanvas, by Federico Mena <federico@nuclecu.unam.mx>
+ * and Raph Levien <raph@gimp.org>
+ * Copyright 1997-2000 Free Software Foundation
+ */
+
+#define _POSIX_C_SOURCE 200809L // strdup
+#define _XOPEN_SOURCE 600 // isascii on BSD
+
+#include <math.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+#include <iostream>
+#include <map>
+#include <set>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <cairo.h>
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+#include <gtk/gtkstyle.h>
+#include <gtkmm/widget.h>
+
+#include "ganv/Canvas.hpp"
+#include "ganv/Circle.hpp"
+#include "ganv/Edge.hpp"
+#include "ganv/Module.hpp"
+#include "ganv/Port.hpp"
+#include "ganv/box.h"
+#include "ganv/canvas.h"
+#include "ganv/edge.h"
+#include "ganv/group.h"
+#include "ganv/node.h"
+#include "ganv_config.h"
+
+#include "./color.h"
+#include "./ganv-marshal.h"
+#include "./ganv-private.h"
+#include "./gettext.h"
+
+#ifdef HAVE_AGRAPH
+// Deal with graphviz API amateur hour...
+# define _DLL_BLD 0
+# define _dll_import 0
+# define _BLD_cdt 0
+# define _PACKAGE_ast 0
+# include <gvc.h>
+#endif
+#ifdef GANV_FDGL
+# include "./fdgl.hpp"
+#endif
+
+#define CANVAS_IDLE_PRIORITY (GDK_PRIORITY_REDRAW - 5)
+
+static const double GANV_CANVAS_PAD = 8.0;
+
+typedef struct {
+ int x;
+ int y;
+ int width;
+ int height;
+} IRect;
+
+extern "C" {
+static void add_idle(GanvCanvas* canvas);
+static void ganv_canvas_destroy(GtkObject* object);
+static void ganv_canvas_map(GtkWidget* widget);
+static void ganv_canvas_unmap(GtkWidget* widget);
+static void ganv_canvas_realize(GtkWidget* widget);
+static void ganv_canvas_unrealize(GtkWidget* widget);
+static void ganv_canvas_size_allocate(GtkWidget* widget,
+ GtkAllocation* allocation);
+static gint ganv_canvas_button(GtkWidget* widget,
+ GdkEventButton* event);
+static gint ganv_canvas_motion(GtkWidget* widget,
+ GdkEventMotion* event);
+static gint ganv_canvas_expose(GtkWidget* widget,
+ GdkEventExpose* event);
+static gboolean ganv_canvas_key(GtkWidget* widget,
+ GdkEventKey* event);
+static gboolean ganv_canvas_scroll(GtkWidget* widget,
+ GdkEventScroll* event);
+static gint ganv_canvas_crossing(GtkWidget* widget,
+ GdkEventCrossing* event);
+static gint ganv_canvas_focus_in(GtkWidget* widget,
+ GdkEventFocus* event);
+static gint ganv_canvas_focus_out(GtkWidget* widget,
+ GdkEventFocus* event);
+
+static GtkLayoutClass* canvas_parent_class;
+}
+
+static guint signal_connect;
+static guint signal_disconnect;
+
+static GEnumValue dir_values[3];
+
+typedef std::set<GanvNode*> Items;
+
+#define FOREACH_ITEM(items, i) \
+ for (Items::const_iterator i = items.begin(); i != items.end(); ++i)
+
+#define FOREACH_ITEM_MUT(items, i) \
+ for (Items::iterator i = items.begin(); i != items.end(); ++i)
+
+#define FOREACH_EDGE(edges, i) \
+ for (GanvCanvasImpl::Edges::const_iterator i = edges.begin(); \
+ i != edges.end(); \
+ ++i)
+
+#define FOREACH_EDGE_MUT(edges, i) \
+ for (GanvCanvasImpl::Edges::iterator i = edges.begin(); \
+ i != edges.end(); \
+ ++i)
+
+#define FOREACH_SELECTED_EDGE(edges, i) \
+ for (GanvCanvasImpl::SelectedEdges::const_iterator i = edges.begin(); \
+ i != edges.end(); \
+ ++i)
+
+#define FOREACH_SELECTED_PORT(p) \
+ for (SelectedPorts::iterator p = _selected_ports.begin(); \
+ p != _selected_ports.end(); ++p)
+
+#ifdef HAVE_AGRAPH
+class GVNodes : public std::map<GanvNode*, Agnode_t*> {
+public:
+ GVNodes() : gvc(0), G(0) {}
+
+ void cleanup() {
+ gvFreeLayout(gvc, G);
+ agclose (G);
+ gvc = 0;
+ G = 0;
+ }
+
+ GVC_t* gvc;
+ Agraph_t* G;
+};
+#endif
+
+static const uint32_t SELECT_RECT_FILL_COLOUR = 0x2E444577;
+static const uint32_t SELECT_RECT_BORDER_COLOUR = 0x2E4445FF;
+
+/* Order edges by (tail, head) */
+struct TailHeadOrder {
+ inline bool operator()(const GanvEdge* a, const GanvEdge* b) const {
+ return ((a->impl->tail < b->impl->tail)
+ || (a->impl->tail == b->impl->tail
+ && a->impl->head < b->impl->head));
+ }
+};
+
+/* Order edges by (head, tail) */
+struct HeadTailOrder {
+ inline bool operator()(const GanvEdge* a, const GanvEdge* b) const {
+ return ((a->impl->head < b->impl->head)
+ || (a->impl->head == b->impl->head
+ && a->impl->tail < b->impl->tail));
+ }
+};
+
+/* Callback used when the root item of a canvas is destroyed. The user should
+ * never ever do this, so we panic if this happens.
+ */
+static void
+panic_root_destroyed(GtkObject* object, gpointer data)
+{
+ g_error("Eeeek, root item %p of canvas %p was destroyed!", (void*)object, data);
+}
+
+struct GanvCanvasImpl {
+ GanvCanvasImpl(GanvCanvas* canvas)
+ : _gcanvas(canvas)
+ , _wrapper(NULL)
+ , _connect_port(NULL)
+ , _last_selected_port(NULL)
+ , _drag_edge(NULL)
+ , _drag_node(NULL)
+ , _select_rect(NULL)
+ , _select_start_x(0.0)
+ , _select_start_y(0.0)
+ , _drag_state(NOT_DRAGGING)
+ {
+ this->root = GANV_ITEM(g_object_new(ganv_group_get_type(), NULL));
+ this->root->impl->canvas = canvas;
+ g_object_ref_sink(this->root);
+
+ this->direction = GANV_DIRECTION_RIGHT;
+ this->width = 0;
+ this->height = 0;
+
+ this->redraw_region = NULL;
+ this->current_item = NULL;
+ this->new_current_item = NULL;
+ this->grabbed_item = NULL;
+ this->focused_item = NULL;
+ this->pixmap_gc = NULL;
+
+ this->pick_event.type = GDK_LEAVE_NOTIFY;
+ this->pick_event.crossing.x = 0;
+ this->pick_event.crossing.y = 0;
+
+ this->scroll_x1 = 0.0;
+ this->scroll_y1 = 0.0;
+ this->scroll_x2 = canvas->layout.width;
+ this->scroll_y2 = canvas->layout.height;
+
+ this->pixels_per_unit = 1.0;
+ this->font_size = ganv_canvas_get_default_font_size(canvas);
+
+ this->idle_id = 0;
+ this->root_destroy_id = g_signal_connect(
+ this->root, "destroy", G_CALLBACK(panic_root_destroyed), canvas);
+
+ this->redraw_x1 = 0;
+ this->redraw_y1 = 0;
+ this->redraw_x2 = 0;
+ this->redraw_y2 = 0;
+
+ this->draw_xofs = 0;
+ this->draw_yofs = 0;
+ this->zoom_xofs = 0;
+ this->zoom_yofs = 0;
+
+ this->state = 0;
+ this->grabbed_event_mask = 0;
+
+ this->center_scroll_region = FALSE;
+ this->need_update = FALSE;
+ this->need_redraw = FALSE;
+ this->need_repick = TRUE;
+ this->left_grabbed_item = FALSE;
+ this->in_repick = FALSE;
+ this->locked = FALSE;
+ this->exporting = FALSE;
+
+#ifdef GANV_FDGL
+ this->layout_idle_id = 0;
+ this->layout_energy = 0.4;
+ this->sprung_layout = FALSE;
+#endif
+
+ _animate_idle_id = 0;
+
+ _port_order.port_cmp = NULL;
+ _port_order.data = NULL;
+
+ gtk_layout_set_hadjustment(GTK_LAYOUT(canvas), NULL);
+ gtk_layout_set_vadjustment(GTK_LAYOUT(canvas), NULL);
+
+ _move_cursor = gdk_cursor_new(GDK_FLEUR);
+ }
+
+ ~GanvCanvasImpl()
+ {
+ if (_animate_idle_id) {
+ g_source_remove(_animate_idle_id);
+ _animate_idle_id = 0;
+ }
+
+ while (g_idle_remove_by_data(this)) {}
+ ganv_canvas_clear(_gcanvas);
+ gdk_cursor_unref(_move_cursor);
+ }
+
+ static gboolean on_animate_timeout(gpointer impl);
+
+#ifdef GANV_FDGL
+ static gboolean on_layout_timeout(gpointer impl) {
+ return ((GanvCanvasImpl*)impl)->layout_iteration();
+ }
+
+ static void on_layout_done(gpointer impl) {
+ ((GanvCanvasImpl*)impl)->layout_idle_id = 0;
+ }
+
+ gboolean layout_iteration();
+ gboolean layout_calculate(double dur, bool update);
+#endif
+
+ void unselect_ports();
+
+#ifdef HAVE_AGRAPH
+ GVNodes layout_dot(const std::string& filename);
+#endif
+
+ typedef std::set<GanvEdge*, TailHeadOrder> Edges;
+ typedef std::set<GanvEdge*, HeadTailOrder> DstEdges;
+ typedef std::set<GanvEdge*> SelectedEdges;
+ typedef std::set<GanvPort*> SelectedPorts;
+
+ Edges::const_iterator first_edge_from(const GanvNode* src);
+ DstEdges::const_iterator first_edge_to(const GanvNode* dst);
+
+ void select_port(GanvPort* p, bool unique=false);
+ void select_port_toggle(GanvPort* p, int mod_state);
+ void unselect_port(GanvPort* p);
+ void selection_joined_with(GanvPort* port);
+ void join_selection();
+
+ GanvNode* get_node_at(double x, double y);
+
+ bool on_event(GdkEvent* event);
+
+ bool scroll_drag_handler(GdkEvent* event);
+ bool select_drag_handler(GdkEvent* event);
+ bool connect_drag_handler(GdkEvent* event);
+ void end_connect_drag();
+
+ /*
+ Event handler for ports.
+
+ This must be implemented as a Canvas method since port event handling
+ depends on shared data (for selection and connecting). This function
+ should only be used by Port implementations.
+ */
+ bool port_event(GdkEvent* event, GanvPort* port);
+
+ void ports_joined(GanvPort* port1, GanvPort* port2);
+ void port_clicked(GdkEvent* event, GanvPort* port);
+ void highlight_port(GanvPort* port, bool highlight);
+
+ void move_contents_to_internal(double x, double y, double min_x, double min_y);
+
+ GanvCanvas* _gcanvas;
+ Ganv::Canvas* _wrapper;
+
+ Items _items; ///< Items on this canvas
+ Edges _edges; ///< Edges ordered (src, dst)
+ DstEdges _dst_edges; ///< Edges ordered (dst, src)
+ Items _selected_items; ///< Currently selected items
+ SelectedEdges _selected_edges; ///< Currently selected edges
+
+ SelectedPorts _selected_ports; ///< Selected ports (hilited red)
+ GanvPort* _connect_port; ///< Port for which a edge is being made
+ GanvPort* _last_selected_port;
+ GanvEdge* _drag_edge;
+ GanvNode* _drag_node;
+
+ GanvBox* _select_rect; ///< Rectangle for drag selection
+ double _select_start_x; ///< Selection drag start x coordinate
+ double _select_start_y; ///< Selection drag start y coordinate
+
+ enum DragState { NOT_DRAGGING, EDGE, SCROLL, SELECT };
+ DragState _drag_state;
+
+ GdkCursor* _move_cursor;
+ guint _animate_idle_id;
+
+ PortOrderCtx _port_order;
+
+ /* Root canvas item */
+ GanvItem* root;
+
+ /* Flow direction */
+ GanvDirection direction;
+
+ /* Canvas width */
+ double width;
+
+ /* Canvas height */
+ double height;
+
+ /* Region that needs redrawing (list of rectangles) */
+ GSList* redraw_region;
+
+ /* The item containing the mouse pointer, or NULL if none */
+ GanvItem* current_item;
+
+ /* Item that is about to become current (used to track deletions and such) */
+ GanvItem* new_current_item;
+
+ /* Item that holds a pointer grab, or NULL if none */
+ GanvItem* grabbed_item;
+
+ /* If non-NULL, the currently focused item */
+ GanvItem* focused_item;
+
+ /* GC for temporary draw pixmap */
+ GdkGC* pixmap_gc;
+
+ /* Event on which selection of current item is based */
+ GdkEvent pick_event;
+
+ /* Scrolling region */
+ double scroll_x1;
+ double scroll_y1;
+ double scroll_x2;
+ double scroll_y2;
+
+ /* Scaling factor to be used for display */
+ double pixels_per_unit;
+
+ /* Font size in points */
+ double font_size;
+
+ /* Idle handler ID */
+ guint idle_id;
+
+ /* Signal handler ID for destruction of the root item */
+ guint root_destroy_id;
+
+ /* Area that is being redrawn. Contains (x1, y1) but not (x2, y2).
+ * Specified in canvas pixel coordinates.
+ */
+ int redraw_x1;
+ int redraw_y1;
+ int redraw_x2;
+ int redraw_y2;
+
+ /* Offsets of the temprary drawing pixmap */
+ int draw_xofs;
+ int draw_yofs;
+
+ /* Internal pixel offsets when zoomed out */
+ int zoom_xofs;
+ int zoom_yofs;
+
+ /* Last known modifier state, for deferred repick when a button is down */
+ int state;
+
+ /* Event mask specified when grabbing an item */
+ guint grabbed_event_mask;
+
+ /* Whether the canvas should center the scroll region in the middle of
+ * the window if the scroll region is smaller than the window.
+ */
+ gboolean center_scroll_region;
+
+ /* Whether items need update at next idle loop iteration */
+ gboolean need_update;
+
+ /* Whether the canvas needs redrawing at the next idle loop iteration */
+ gboolean need_redraw;
+
+ /* Whether current item will be repicked at next idle loop iteration */
+ gboolean need_repick;
+
+ /* For use by internal pick_current_item() function */
+ gboolean left_grabbed_item;
+
+ /* For use by internal pick_current_item() function */
+ gboolean in_repick;
+
+ /* Disable changes to canvas */
+ gboolean locked;
+
+ /* True if the current draw is an export */
+ gboolean exporting;
+
+#ifdef GANV_FDGL
+ guint layout_idle_id;
+ gdouble layout_energy;
+ gboolean sprung_layout;
+#endif
+};
+
+typedef struct {
+ GanvItem item;
+ GanvEdgePrivate* impl;
+ GanvEdgePrivate impl_data;
+} GanvEdgeKey;
+
+static void
+make_edge_search_key(GanvEdgeKey* key,
+ const GanvNode* tail,
+ const GanvNode* head)
+{
+ memset(key, '\0', sizeof(GanvEdgeKey));
+ key->impl = &key->impl_data;
+ key->impl->tail = const_cast<GanvNode*>(tail);
+ key->impl->head = const_cast<GanvNode*>(head);
+}
+
+GanvCanvasImpl::Edges::const_iterator
+GanvCanvasImpl::first_edge_from(const GanvNode* tail)
+{
+ GanvEdgeKey key;
+ make_edge_search_key(&key, tail, NULL);
+ return _edges.lower_bound((GanvEdge*)&key);
+}
+
+GanvCanvasImpl::DstEdges::const_iterator
+GanvCanvasImpl::first_edge_to(const GanvNode* head)
+{
+ GanvEdgeKey key;
+ make_edge_search_key(&key, NULL, head);
+ return _dst_edges.lower_bound((GanvEdge*)&key);
+}
+
+static void
+select_if_tail_is_selected(GanvEdge* edge, void* data)
+{
+ GanvNode* tail = edge->impl->tail;
+ gboolean selected;
+ g_object_get(tail, "selected", &selected, NULL);
+ if (!selected && GANV_IS_PORT(tail)) {
+ g_object_get(ganv_port_get_module(GANV_PORT(tail)),
+ "selected", &selected, NULL);
+ }
+
+ if (selected) {
+ ganv_edge_select(edge);
+ }
+}
+
+static void
+select_if_head_is_selected(GanvEdge* edge, void* data)
+{
+ GanvNode* head = edge->impl->head;
+ gboolean selected;
+ g_object_get(head, "selected", &selected, NULL);
+ if (!selected && GANV_IS_PORT(head)) {
+ g_object_get(ganv_port_get_module(GANV_PORT(head)),
+ "selected", &selected, NULL);
+ }
+
+ if (selected) {
+ ganv_edge_set_selected(edge, TRUE);
+ }
+}
+
+static void
+select_edges(GanvPort* port, void* data)
+{
+ GanvCanvasImpl* impl = (GanvCanvasImpl*)data;
+ if (port->impl->is_input) {
+ ganv_canvas_for_each_edge_to(impl->_gcanvas,
+ GANV_NODE(port),
+ select_if_tail_is_selected,
+ NULL);
+ } else {
+ ganv_canvas_for_each_edge_from(impl->_gcanvas,
+ GANV_NODE(port),
+ select_if_head_is_selected,
+ NULL);
+ }
+}
+
+#ifdef HAVE_AGRAPH
+static void
+gv_set(void* subject, const char* key, double value)
+{
+ std::ostringstream ss;
+ ss << value;
+ agsafeset(subject, (char*)key, (char*)ss.str().c_str(), (char*)"");
+}
+
+GVNodes
+GanvCanvasImpl::layout_dot(const std::string& filename)
+{
+ GVNodes nodes;
+
+ const double dpi = gdk_screen_get_resolution(gdk_screen_get_default());
+
+ GVC_t* gvc = gvContext();
+
+ Agraph_t* G = agopen((char*)"g", Agdirected, NULL);
+
+ agsafeset(G, (char*)"splines", (char*)"false", (char*)"");
+ agsafeset(G, (char*)"compound", (char*)"true", (char*)"");
+ agsafeset(G, (char*)"remincross", (char*)"true", (char*)"");
+ agsafeset(G, (char*)"overlap", (char*)"scale", (char*)"");
+ agsafeset(G, (char*)"nodesep", (char*)"0.05", (char*)"");
+ gv_set(G, "fontsize", ganv_canvas_get_font_size(_gcanvas));
+ gv_set(G, "dpi", dpi);
+
+ nodes.gvc = gvc;
+ nodes.G = G;
+
+ const bool flow_right = _gcanvas->impl->direction;
+ if (flow_right) {
+ agattr(G, AGRAPH, (char*)"rankdir", (char*)"LR");
+ } else {
+ agattr(G, AGRAPH, (char*)"rankdir", (char*)"TD");
+ }
+
+ unsigned id = 0;
+ std::ostringstream ss;
+ FOREACH_ITEM(_items, i) {
+ ss.str("");
+ ss << "n" << id++;
+ const std::string node_id = ss.str();
+
+ Agnode_t* node = agnode(G, strdup(node_id.c_str()), true);
+ nodes.insert(std::make_pair(*i, node));
+
+ if (GANV_IS_MODULE(*i)) {
+ GanvModule* const m = GANV_MODULE(*i);
+
+ agsafeset(node, (char*)"shape", (char*)"plaintext", (char*)"");
+ gv_set(node, "width", ganv_box_get_width(GANV_BOX(*i)) / dpi);
+ gv_set(node, "height", ganv_box_get_height(GANV_BOX(*i)) / dpi);
+
+ std::string inputs; // Down flow
+ std::string outputs; // Down flow
+ std::string ports; // Right flow
+ unsigned n_inputs = 0;
+ unsigned n_outputs = 0;
+ for (size_t i = 0; i < ganv_module_num_ports(m); ++i) {
+ GanvPort* port = ganv_module_get_port(m, i);
+ ss.str("");
+ ss << port;
+
+ if (port->impl->is_input) {
+ ++n_inputs;
+ } else {
+ ++n_outputs;
+ }
+
+ std::string cell = std::string("<TD PORT=\"") + ss.str() + "\"";
+
+ cell += " FIXEDSIZE=\"TRUE\"";
+ ss.str("");
+ ss << ganv_box_get_width(GANV_BOX(port));// / dpp * 1.3333333;
+ cell += " WIDTH=\"" + ss.str() + "\"";
+
+ ss.str("");
+ ss << ganv_box_get_height(GANV_BOX(port));// / dpp * 1.333333;
+ cell += " HEIGHT=\"" + ss.str() + "\"";
+
+ cell += ">";
+ const char* label = ganv_node_get_label(GANV_NODE(port));
+ if (label && flow_right) {
+ cell += label;
+ }
+ cell += "</TD>";
+
+ if (flow_right) {
+ ports += "<TR>" + cell + "</TR>";
+ } else if (port->impl->is_input) {
+ inputs += cell;
+ } else {
+ outputs += cell;
+ }
+
+ nodes.insert(std::make_pair(GANV_NODE(port), node));
+ }
+
+ const unsigned n_cols = std::max(n_inputs, n_outputs);
+
+ std::string html = "<TABLE CELLPADDING=\"0\" CELLSPACING=\"0\">";
+
+ // Input row (down flow only)
+ if (!inputs.empty()) {
+ for (unsigned i = n_inputs; i < n_cols + 1; ++i) {
+ inputs += "<TD BORDER=\"0\"></TD>";
+ }
+ html += std::string("<TR>") + inputs + "</TR>";
+ }
+
+ // Label row
+ std::stringstream colspan;
+ colspan << (flow_right ? 1 : (n_cols + 1));
+ html += std::string("<TR><TD BORDER=\"0\" CELLPADDING=\"2\" COLSPAN=\"")
+ + colspan.str()
+ + "\">";
+ const char* label = ganv_node_get_label(GANV_NODE(m));
+ if (label) {
+ html += label;
+ }
+ html += "</TD></TR>";
+
+ // Ports rows (right flow only)
+ if (!ports.empty()) {
+ html += ports;
+ }
+
+ // Output row (down flow only)
+ if (!outputs.empty()) {
+ for (unsigned i = n_outputs; i < n_cols + 1; ++i) {
+ outputs += "<TD BORDER=\"0\"></TD>";
+ }
+ html += std::string("<TR>") + outputs + "</TR>";
+ }
+ html += "</TABLE>";
+
+ char* html_label_str = agstrdup_html(G, (char*)html.c_str());
+
+ agsafeset(node, (char*)"label", (char*)html_label_str, (char*)"");
+ } else if (GANV_IS_CIRCLE(*i)) {
+ agsafeset(node, (char*)"shape", (char*)"circle", (char*)"");
+ agsafeset(node, (char*)"fixedsize", (char*)"true", (char*)"");
+ agsafeset(node, (char*)"margin", (char*)"0.0,0.0", (char*)"");
+
+ const double radius = ganv_circle_get_radius(GANV_CIRCLE(*i));
+ const double penwidth = ganv_node_get_border_width(GANV_NODE(*i));
+ const double span = (radius + penwidth) * 2.3 / dpi;
+ gv_set(node, (char*)"width", span);
+ gv_set(node, (char*)"height", span);
+ gv_set(node, (char*)"penwidth", penwidth);
+
+ if (ganv_node_get_dash_length(GANV_NODE(*i)) > 0.0) {
+ agsafeset(node, (char*)"style", (char*)"dashed", (char*)"");
+ }
+
+ const char* label = ganv_node_get_label(GANV_NODE(*i));
+ if (label) {
+ agsafeset(node, (char*)"label", (char*)label, (char*)"");
+ } else {
+ agsafeset(node, (char*)"label", (char*)"", (char*)"");
+ }
+ } else {
+ std::cerr << "Unable to arrange item of unknown type" << std::endl;
+ }
+ }
+
+ FOREACH_EDGE(_edges, i) {
+ const GanvEdge* const edge = *i;
+ GVNodes::iterator tail_i = nodes.find(edge->impl->tail);
+ GVNodes::iterator head_i = nodes.find(edge->impl->head);
+
+ if (tail_i != nodes.end() && head_i != nodes.end()) {
+ Agedge_t* e = agedge(G, tail_i->second, head_i->second, NULL, true);
+ if (GANV_IS_PORT(edge->impl->tail)) {
+ ss.str((char*)"");
+ ss << edge->impl->tail << (flow_right ? ":e" : ":s");
+ agsafeset(e, (char*)"tailport", (char*)ss.str().c_str(), (char*)"");
+ }
+ if (GANV_IS_PORT(edge->impl->head)) {
+ ss.str((char*)"");
+ ss << edge->impl->head << (flow_right ? ":w" : ":n");
+ agsafeset(e, (char*)"headport", (char*)ss.str().c_str(), (char*)"");
+ }
+ if (!ganv_edge_get_constraining(edge)) {
+ agsafeset(e, (char*)"constraint", (char*)"false", (char*)"");
+ }
+ } else {
+ std::cerr << "Unable to find graphviz node" << std::endl;
+ }
+ }
+
+ // Add edges between partners to have them lined up as if connected
+ for (GVNodes::iterator i = nodes.begin(); i != nodes.end(); ++i) {
+ GanvNode* partner = ganv_node_get_partner(i->first);
+ if (partner) {
+ GVNodes::iterator p = nodes.find(partner);
+ if (p != nodes.end()) {
+ Agedge_t* e = agedge(G, i->second, p->second, NULL, true);
+ agsafeset(e, (char*)"style", (char*)"invis", (char*)"");
+ }
+ }
+ }
+
+ gvLayout(gvc, G, (char*)"dot");
+ FILE* tmp = fopen("/dev/null", "w");
+ gvRender(gvc, G, (char*)"dot", tmp);
+ fclose(tmp);
+
+ if (filename != "") {
+ FILE* fd = fopen(filename.c_str(), "w");
+ gvRender(gvc, G, (char*)"dot", fd);
+ fclose(fd);
+ }
+
+ return nodes;
+}
+#endif
+
+inline uint64_t
+get_monotonic_time()
+{
+#if GLIB_CHECK_VERSION(2, 28, 0)
+ return g_get_monotonic_time();
+#else
+ GTimeVal time;
+ g_get_current_time(&time);
+ return time.tv_sec + time.tv_usec;
+#endif
+}
+
+#ifdef GANV_FDGL
+
+inline Region
+get_region(GanvNode* node)
+{
+ GanvItem* item = &node->item;
+
+ double x1, y1, x2, y2;
+ ganv_item_get_bounds(item, &x1, &y1, &x2, &y2);
+
+ Region reg;
+ ganv_item_get_bounds(item, &reg.pos.x, &reg.pos.y, &reg.area.x, &reg.area.y);
+ reg.area.x = x2 - x1;
+ reg.area.y = y2 - y1;
+ reg.pos.x = item->impl->x + (reg.area.x / 2.0);
+ reg.pos.y = item->impl->y + (reg.area.y / 2.0);
+
+ // No need for i2w here since we only care about top-level items
+ return reg;
+}
+
+inline void
+apply_force(GanvNode* a, GanvNode* b, const Vector& f)
+{
+ a->impl->force = vec_add(a->impl->force, f);
+ b->impl->force = vec_sub(b->impl->force, f);
+}
+
+gboolean
+GanvCanvasImpl::layout_iteration()
+{
+ if (_drag_state == EDGE) {
+ return FALSE; // Canvas is locked, halt layout process
+ } else if (!sprung_layout) {
+ return FALSE; // We shouldn't be running at all
+ }
+
+ static const double T_PER_US = .0001; // Sym time per real microsecond
+
+ static uint64_t prev = 0; // Previous iteration time
+
+ const uint64_t now = get_monotonic_time();
+ const double time_to_run = std::min((now - prev) * T_PER_US, 10.0);
+
+ prev = now;
+
+ const double QUANTUM = 0.05;
+ double sym_time = 0.0;
+ while (sym_time + QUANTUM < time_to_run) {
+ if (!layout_calculate(QUANTUM, FALSE)) {
+ break;
+ }
+ sym_time += QUANTUM;
+ }
+
+ return layout_calculate(QUANTUM, TRUE);
+}
+
+gboolean
+GanvCanvasImpl::layout_calculate(double dur, bool update)
+{
+ // A light directional force to push sources to the top left
+ static const double DIR_MAGNITUDE = -1000.0;
+ Vector dir = { 0.0, 0.0 };
+ switch (_gcanvas->impl->direction) {
+ case GANV_DIRECTION_RIGHT: dir.x = DIR_MAGNITUDE; break;
+ case GANV_DIRECTION_DOWN: dir.y = DIR_MAGNITUDE; break;
+ }
+
+ // Calculate attractive spring forces for edges
+ FOREACH_EDGE(_edges, i) {
+ const GanvEdge* const edge = *i;
+ if (!ganv_edge_get_constraining(edge)) {
+ continue;
+ }
+
+ GanvNode* tail = ganv_edge_get_tail(edge);
+ GanvNode* head = ganv_edge_get_head(edge);
+ if (GANV_IS_PORT(tail)) {
+ tail = GANV_NODE(ganv_port_get_module(GANV_PORT(tail)));
+ }
+ if (GANV_IS_PORT(head)) {
+ head = GANV_NODE(ganv_port_get_module(GANV_PORT(head)));
+ }
+ if (tail == head) {
+ continue;
+ }
+
+ head->impl->connected = tail->impl->connected = TRUE;
+
+ GanvEdgeCoords coords;
+ ganv_edge_get_coords(edge, &coords);
+
+ const Vector tpos = { coords.x1, coords.y1 };
+ const Vector hpos = { coords.x2, coords.y2 };
+ apply_force(tail, head, edge_force(dir, hpos, tpos));
+ }
+
+ // Calculate repelling forces between nodes
+ FOREACH_ITEM(_items, i) {
+ if (!GANV_IS_MODULE(*i) && !GANV_IS_CIRCLE(*i)) {
+ continue;
+ }
+
+ GanvNode* const node = *i;
+ GanvNode* partner = ganv_node_get_partner(node);
+ if (!partner && !node->impl->connected) {
+ continue;
+ }
+
+ const Region reg = get_region(node);
+ if (partner) {
+ // Add fake long spring to partner to line up as if connected
+ const Region preg = get_region(partner);
+ apply_force(node, partner, edge_force(dir, preg.pos, reg.pos));
+ }
+
+ /* Add tide force which pulls all objects as if the layout is happening
+ on a flowing river surface. This prevents disconnected components
+ from being ejected, since at some point the tide force will be
+ greater than distant repelling charges. */
+ const Vector mouth = { -100000.0, -100000.0 };
+ node->impl->force = vec_add(
+ node->impl->force,
+ tide_force(mouth, reg.pos, 4000000000000.0));
+
+ // Add slight noise to force to limit oscillation
+ const Vector noise = { rand() / (float)RAND_MAX * 128.0,
+ rand() / (float)RAND_MAX * 128.0 };
+ node->impl->force = vec_add(noise, node->impl->force);
+
+ FOREACH_ITEM(_items, j) {
+ if (i == j || (!GANV_IS_MODULE(*i) && !GANV_IS_CIRCLE(*i))) {
+ continue;
+ }
+ apply_force(node, *j, repel_force(reg, get_region(*j)));
+ }
+ }
+
+ // Update positions based on calculated forces
+ size_t n_moved = 0;
+ FOREACH_ITEM(_items, i) {
+ if (!GANV_IS_MODULE(*i) && !GANV_IS_CIRCLE(*i)) {
+ continue;
+ }
+
+ GanvNode* const node = *i;
+
+ if (node->impl->grabbed ||
+ (!node->impl->connected && !ganv_node_get_partner(node))) {
+ node->impl->vel.x = 0.0;
+ node->impl->vel.y = 0.0;
+ } else {
+ node->impl->vel = vec_add(node->impl->vel,
+ vec_mult(node->impl->force, dur));
+ node->impl->vel = vec_mult(node->impl->vel, layout_energy);
+
+ static const double MAX_VEL = 1000.0;
+ static const double MIN_COORD = 4.0;
+
+ // Clamp velocity
+ const double vel_mag = vec_mag(node->impl->vel);
+ if (vel_mag > MAX_VEL) {
+ node->impl->vel = vec_mult(
+ vec_mult(node->impl->vel, 1.0 / vel_mag),
+ MAX_VEL);
+ }
+
+ // Update position
+ GanvItem* item = &node->item;
+ const double x0 = item->impl->x;
+ const double y0 = item->impl->y;
+ const Vector dpos = vec_mult(node->impl->vel, dur);
+
+ item->impl->x = std::max(MIN_COORD, item->impl->x + dpos.x);
+ item->impl->y = std::max(MIN_COORD, item->impl->y + dpos.y);
+
+ if (update) {
+ ganv_item_request_update(item);
+ item->impl->canvas->impl->need_repick = TRUE;
+ }
+
+ if (lrint(x0) != lrint(item->impl->x) || lrint(y0) != lrint(item->impl->y)) {
+ ++n_moved;
+ }
+ }
+
+ // Reset forces for next time
+ node->impl->force.x = 0.0;
+ node->impl->force.y = 0.0;
+ node->impl->connected = FALSE;
+ }
+
+ if (update) {
+ // Now update edge positions to reflect new node positions
+ FOREACH_EDGE(_edges, i) {
+ GanvEdge* const edge = *i;
+ ganv_edge_update_location(edge);
+ }
+ }
+
+ layout_energy *= 0.999;
+ return n_moved > 0;
+}
+
+#endif // GANV_FDGL
+
+void
+GanvCanvasImpl::select_port(GanvPort* p, bool unique)
+{
+ if (unique) {
+ unselect_ports();
+ }
+ g_object_set(G_OBJECT(p), "selected", TRUE, NULL);
+ _selected_ports.insert(p);
+ _last_selected_port = p;
+}
+
+void
+GanvCanvasImpl::select_port_toggle(GanvPort* port, int mod_state)
+{
+ gboolean selected;
+ g_object_get(G_OBJECT(port), "selected", &selected, NULL);
+ if ((mod_state & GDK_CONTROL_MASK)) {
+ if (selected)
+ unselect_port(port);
+ else
+ select_port(port);
+ } else if ((mod_state & GDK_SHIFT_MASK)) {
+ GanvModule* const m = ganv_port_get_module(port);
+ if (_last_selected_port && m
+ && ganv_port_get_module(_last_selected_port) == m) {
+ // Pivot around _last_selected_port in a single pass over module ports each click
+ GanvPort* old_last_selected = _last_selected_port;
+ GanvPort* first = NULL;
+ bool done = false;
+ for (size_t i = 0; i < ganv_module_num_ports(m); ++i) {
+ GanvPort* const p = ganv_module_get_port(m, i);
+ if (!first && !done && (p == _last_selected_port || p == port)) {
+ first = p;
+ }
+
+ if (first && !done && p->impl->is_input == first->impl->is_input) {
+ select_port(p, false);
+ } else {
+ unselect_port(p);
+ }
+
+ if (p != first && (p == old_last_selected || p == port)) {
+ done = true;
+ }
+ }
+ _last_selected_port = old_last_selected;
+ } else {
+ if (selected) {
+ unselect_port(port);
+ } else {
+ select_port(port);
+ }
+ }
+ } else {
+ if (selected) {
+ unselect_ports();
+ } else {
+ select_port(port, true);
+ }
+ }
+}
+
+void
+GanvCanvasImpl::unselect_port(GanvPort* p)
+{
+ _selected_ports.erase(p);
+ g_object_set(G_OBJECT(p), "selected", FALSE, NULL);
+ if (_last_selected_port == p) {
+ _last_selected_port = NULL;
+ }
+}
+
+void
+GanvCanvasImpl::selection_joined_with(GanvPort* port)
+{
+ FOREACH_SELECTED_PORT(i)
+ ports_joined(*i, port);
+}
+
+void
+GanvCanvasImpl::join_selection()
+{
+ std::vector<GanvPort*> inputs;
+ std::vector<GanvPort*> outputs;
+ FOREACH_SELECTED_PORT(i) {
+ if ((*i)->impl->is_input) {
+ inputs.push_back(*i);
+ } else {
+ outputs.push_back(*i);
+ }
+ }
+
+ if (inputs.size() == 1) { // 1 -> n
+ for (size_t i = 0; i < outputs.size(); ++i)
+ ports_joined(inputs[0], outputs[i]);
+ } else if (outputs.size() == 1) { // n -> 1
+ for (size_t i = 0; i < inputs.size(); ++i)
+ ports_joined(inputs[i], outputs[0]);
+ } else { // n -> m
+ size_t num_to_connect = std::min(inputs.size(), outputs.size());
+ for (size_t i = 0; i < num_to_connect; ++i) {
+ ports_joined(inputs[i], outputs[i]);
+ }
+ }
+}
+
+GanvNode*
+GanvCanvasImpl::get_node_at(double x, double y)
+{
+ GanvItem* item = ganv_canvas_get_item_at(GANV_CANVAS(_gcanvas), x, y);
+ while (item) {
+ if (GANV_IS_NODE(item)) {
+ return GANV_NODE(item);
+ } else {
+ item = item->impl->parent;
+ }
+ }
+
+ return NULL;
+}
+
+bool
+GanvCanvasImpl::on_event(GdkEvent* event)
+{
+ static const int scroll_increment = 10;
+ int scroll_x, scroll_y;
+
+ bool handled = false;
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ handled = true;
+ ganv_canvas_get_scroll_offsets(GANV_CANVAS(_gcanvas), &scroll_x, &scroll_y);
+ switch (event->key.keyval) {
+ case GDK_Up:
+ scroll_y -= scroll_increment;
+ break;
+ case GDK_Down:
+ scroll_y += scroll_increment;
+ break;
+ case GDK_Left:
+ scroll_x -= scroll_increment;
+ break;
+ case GDK_Right:
+ scroll_x += scroll_increment;
+ break;
+ case GDK_Return:
+ if (_selected_ports.size() > 1) {
+ join_selection();
+ ganv_canvas_clear_selection(_gcanvas);
+ }
+ break;
+ default:
+ handled = false;
+ }
+ if (handled) {
+ ganv_canvas_scroll_to(GANV_CANVAS(_gcanvas), scroll_x, scroll_y);
+ return true;
+ }
+ break;
+
+ case GDK_SCROLL:
+ if ((event->scroll.state & GDK_CONTROL_MASK)) {
+ const double zoom = ganv_canvas_get_zoom(_gcanvas);
+ if (event->scroll.direction == GDK_SCROLL_UP) {
+ ganv_canvas_set_zoom(_gcanvas, zoom * 1.25);
+ return true;
+ } else if (event->scroll.direction == GDK_SCROLL_DOWN) {
+ ganv_canvas_set_zoom(_gcanvas, zoom * 0.75);
+ return true;
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return scroll_drag_handler(event)
+ || select_drag_handler(event)
+ || connect_drag_handler(event);
+}
+
+bool
+GanvCanvasImpl::scroll_drag_handler(GdkEvent* event)
+{
+ bool handled = true;
+
+ static int original_scroll_x = 0;
+ static int original_scroll_y = 0;
+ static double origin_x = 0;
+ static double origin_y = 0;
+ static double scroll_offset_x = 0;
+ static double scroll_offset_y = 0;
+ static double last_x = 0;
+ static double last_y = 0;
+
+ GanvItem* root = ganv_canvas_root(_gcanvas);
+
+ if (event->type == GDK_BUTTON_PRESS && event->button.button == 2) {
+ ganv_canvas_grab_item(
+ root,
+ GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK,
+ NULL, event->button.time);
+ ganv_canvas_get_scroll_offsets(GANV_CANVAS(_gcanvas), &original_scroll_x, &original_scroll_y);
+ scroll_offset_x = 0;
+ scroll_offset_y = 0;
+ origin_x = event->button.x_root;
+ origin_y = event->button.y_root;
+ //cerr << "Origin: (" << origin_x << "," << origin_y << ")\n";
+ last_x = origin_x;
+ last_y = origin_y;
+ _drag_state = SCROLL;
+
+ } else if (event->type == GDK_MOTION_NOTIFY && _drag_state == SCROLL) {
+ const double x = event->motion.x_root;
+ const double y = event->motion.y_root;
+ const double x_offset = last_x - x;
+ const double y_offset = last_y - y;
+
+ //cerr << "Coord: (" << x << "," << y << ")\n";
+ //cerr << "Offset: (" << x_offset << "," << y_offset << ")\n";
+
+ scroll_offset_x += x_offset;
+ scroll_offset_y += y_offset;
+ ganv_canvas_scroll_to(GANV_CANVAS(_gcanvas),
+ lrint(original_scroll_x + scroll_offset_x),
+ lrint(original_scroll_y + scroll_offset_y));
+ last_x = x;
+ last_y = y;
+ } else if (event->type == GDK_BUTTON_RELEASE && _drag_state == SCROLL) {
+ ganv_canvas_ungrab_item(root, event->button.time);
+ _drag_state = NOT_DRAGGING;
+ } else {
+ handled = false;
+ }
+
+ return handled;
+}
+
+static void
+get_motion_coords(GdkEventMotion* motion, double* x, double* y)
+{
+ if (motion->is_hint) {
+ gint px;
+ gint py;
+ GdkModifierType state;
+ gdk_window_get_pointer(motion->window, &px, &py, &state);
+ *x = px;
+ *y = py;
+ } else {
+ *x = motion->x;
+ *y = motion->y;
+ }
+}
+
+bool
+GanvCanvasImpl::select_drag_handler(GdkEvent* event)
+{
+ GanvItem* root = ganv_canvas_root(_gcanvas);
+ if (event->type == GDK_BUTTON_PRESS && event->button.button == 1) {
+ assert(_select_rect == NULL);
+ _drag_state = SELECT;
+ if ( !(event->button.state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) )
+ ganv_canvas_clear_selection(_gcanvas);
+ _select_rect = GANV_BOX(
+ ganv_item_new(
+ root,
+ ganv_box_get_type(),
+ "x1", event->button.x,
+ "y1", event->button.y,
+ "x2", event->button.x,
+ "y2", event->button.y,
+ "fill-color", SELECT_RECT_FILL_COLOUR,
+ "border-color", SELECT_RECT_BORDER_COLOUR,
+ NULL));
+ _select_start_x = event->button.x;
+ _select_start_y = event->button.y;
+ ganv_canvas_grab_item(
+ root, GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK,
+ NULL, event->button.time);
+ return true;
+ } else if (event->type == GDK_MOTION_NOTIFY && _drag_state == SELECT) {
+ assert(_select_rect);
+ double x, y;
+ get_motion_coords(&event->motion, &x, &y);
+ _select_rect->impl->coords.x1 = MIN(_select_start_x, x);
+ _select_rect->impl->coords.y1 = MIN(_select_start_y, y);
+ _select_rect->impl->coords.x2 = MAX(_select_start_x, x);
+ _select_rect->impl->coords.y2 = MAX(_select_start_y, y);
+ ganv_item_request_update(&_select_rect->node.item);
+ return true;
+ } else if (event->type == GDK_BUTTON_RELEASE && _drag_state == SELECT) {
+ // Normalize select rect
+ ganv_box_normalize(_select_rect);
+
+ // Select all modules within rect
+ FOREACH_ITEM(_items, i) {
+ GanvNode* node = *i;
+ if ((void*)node != (void*)_select_rect &&
+ ganv_node_is_within(
+ node,
+ ganv_box_get_x1(_select_rect),
+ ganv_box_get_y1(_select_rect),
+ ganv_box_get_x2(_select_rect),
+ ganv_box_get_y2(_select_rect))) {
+ gboolean selected;
+ g_object_get(G_OBJECT(node), "selected", &selected, NULL);
+ if (selected) {
+ ganv_canvas_unselect_node(_gcanvas, node);
+ } else {
+ ganv_canvas_select_node(_gcanvas, node);
+ }
+ }
+ }
+
+ // Select all edges with handles within rect
+ FOREACH_EDGE(_edges, i) {
+ if (ganv_edge_is_within(
+ (*i),
+ ganv_box_get_x1(_select_rect),
+ ganv_box_get_y1(_select_rect),
+ ganv_box_get_x2(_select_rect),
+ ganv_box_get_y2(_select_rect))) {
+ ganv_canvas_select_edge(_gcanvas, *i);
+ }
+ }
+
+ ganv_canvas_ungrab_item(root, event->button.time);
+
+ gtk_object_destroy(GTK_OBJECT(_select_rect));
+ _select_rect = NULL;
+ _drag_state = NOT_DRAGGING;
+ return true;
+ }
+ return false;
+}
+
+bool
+GanvCanvasImpl::connect_drag_handler(GdkEvent* event)
+{
+ static bool snapped = false;
+
+ if (_drag_state != EDGE) {
+ return false;
+ }
+
+ if (event->type == GDK_MOTION_NOTIFY) {
+ double x, y;
+ get_motion_coords(&event->motion, &x, &y);
+
+ if (!_drag_edge) {
+ // Create drag edge
+ assert(!_drag_node);
+ assert(_connect_port);
+
+ _drag_node = GANV_NODE(
+ ganv_item_new(
+ GANV_ITEM(ganv_canvas_root(GANV_CANVAS(_gcanvas))),
+ ganv_node_get_type(),
+ "x", x,
+ "y", y,
+ NULL));
+
+ _drag_edge = ganv_edge_new(
+ _gcanvas,
+ GANV_NODE(_connect_port),
+ _drag_node,
+ "color", GANV_NODE(_connect_port)->impl->fill_color,
+ "curved", TRUE,
+ "ghost", TRUE,
+ NULL);
+ }
+
+ GanvNode* joinee = get_node_at(x, y);
+ if (joinee && ganv_node_can_head(joinee) && joinee != _drag_node) {
+ // Snap to item
+ snapped = true;
+ ganv_item_set(&_drag_edge->item, "head", joinee, NULL);
+ } else if (snapped) {
+ // Unsnap from item
+ snapped = false;
+ ganv_item_set(&_drag_edge->item, "head", _drag_node, NULL);
+ }
+
+ // Update drag edge for pointer position
+ ganv_node_move_to(_drag_node, x, y);
+ ganv_item_request_update(GANV_ITEM(_drag_node));
+ ganv_item_request_update(GANV_ITEM(_drag_edge));
+
+ return true;
+
+ } else if (event->type == GDK_BUTTON_RELEASE) {
+ ganv_canvas_ungrab_item(root, event->button.time);
+
+ double x = event->button.x;
+ double y = event->button.y;
+
+ GanvNode* joinee = get_node_at(x, y);
+
+ if (GANV_IS_PORT(joinee)) {
+ if (joinee == GANV_NODE(_connect_port)) {
+ // Drag ended on the same port it started on, port clicked
+ if (_selected_ports.empty()) {
+ // No selected ports, port clicked
+ select_port(_connect_port);
+ } else {
+ // Connect to selected ports
+ selection_joined_with(_connect_port);
+ _connect_port = NULL;
+ }
+ } else { // drag ended on different port
+ ports_joined(_connect_port, GANV_PORT(joinee));
+ _connect_port = NULL;
+ }
+ }
+
+ end_connect_drag();
+ return true;
+ }
+
+ return false;
+}
+
+void
+GanvCanvasImpl::end_connect_drag()
+{
+ if (_connect_port) {
+ highlight_port(_connect_port, false);
+ }
+ gtk_object_destroy(GTK_OBJECT(_drag_edge));
+ gtk_object_destroy(GTK_OBJECT(_drag_node));
+ _drag_state = NOT_DRAGGING;
+ _connect_port = NULL;
+ _drag_edge = NULL;
+ _drag_node = NULL;
+}
+
+bool
+GanvCanvasImpl::port_event(GdkEvent* event, GanvPort* port)
+{
+ static bool port_pressed = true;
+ static bool port_dragging = false;
+ static bool control_dragging = false;
+ static double control_start_x = 0;
+ static double control_start_y = 0;
+ static float control_start_value = 0;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ GanvModule* const module = ganv_port_get_module(port);
+ double port_x = event->button.x;
+ double port_y = event->button.y;
+ ganv_item_w2i(GANV_ITEM(port), &port_x, &port_y);
+
+ if (_selected_ports.empty() && module && port->impl->control &&
+ (port->impl->is_input ||
+ (port->impl->is_controllable &&
+ port_x < ganv_box_get_width(GANV_BOX(port)) / 2.0))) {
+ if (port->impl->control->is_toggle) {
+ if (port->impl->control->value >= 0.5) {
+ ganv_port_set_control_value_internal(port, 0.0);
+ } else {
+ ganv_port_set_control_value_internal(port, 1.0);
+ }
+ } else {
+ control_dragging = port_pressed = true;
+ control_start_x = event->button.x_root;
+ control_start_y = event->button.y_root;
+ control_start_value = ganv_port_get_control_value(port);
+ ganv_canvas_grab_item(
+ GANV_ITEM(port),
+ GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK,
+ NULL, event->button.time);
+ GANV_NODE(port)->impl->grabbed = TRUE;
+
+ }
+ } else if (!port->impl->is_input) {
+ port_dragging = port_pressed = true;
+ ganv_canvas_grab_item(
+ GANV_ITEM(port),
+ GDK_BUTTON_RELEASE_MASK|GDK_POINTER_MOTION_MASK|
+ GDK_ENTER_NOTIFY_MASK|GDK_LEAVE_NOTIFY_MASK,
+ NULL, event->button.time);
+ } else {
+ port_pressed = true;
+ ganv_canvas_grab_item(GANV_ITEM(port),
+ GDK_BUTTON_RELEASE_MASK,
+ NULL, event->button.time);
+ }
+ return true;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (control_dragging) {
+ const double mouse_x = event->button.x_root;
+ const double mouse_y = event->button.y_root;
+ GdkScreen* screen = gdk_screen_get_default();
+ const int screen_width = gdk_screen_get_width(screen);
+ const int screen_height = gdk_screen_get_height(screen);
+ const double drag_dx = mouse_x - control_start_x;
+ const double drag_dy = mouse_y - control_start_y;
+ const double ythresh = 0.2; // Minimum y fraction for fine
+
+ const double range_x = ((drag_dx > 0)
+ ? (screen_width - control_start_x)
+ : control_start_x) - GANV_CANVAS_PAD;
+
+ const double range_y = ((drag_dy > 0)
+ ? (screen_height - control_start_y)
+ : control_start_y);
+
+ const double dx = drag_dx / range_x;
+ const double dy = fabs(drag_dy / range_y);
+
+ const double value_range = (drag_dx > 0)
+ ? port->impl->control->max - control_start_value
+ : control_start_value - port->impl->control->min;
+
+ const double sens = (dy < ythresh)
+ ? 1.0
+ : 1.0 - fabs(drag_dy / (range_y + ythresh));
+
+ const double dvalue = (dx * value_range) * sens;
+ double value = control_start_value + dvalue;
+ if (value < port->impl->control->min) {
+ value = port->impl->control->min;
+ } else if (value > port->impl->control->max) {
+ value = port->impl->control->max;
+ }
+ ganv_port_set_control_value_internal(port, value);
+ return true;
+ } else if (port_dragging) {
+ return true;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (port_pressed) {
+ ganv_canvas_ungrab_item(GANV_ITEM(port), event->button.time);
+ }
+
+ if (port_dragging) {
+ if (_connect_port) { // dragging
+ ports_joined(port, _connect_port);
+ } else {
+ port_clicked(event, port);
+ }
+ port_dragging = false;
+ } else if (control_dragging) {
+ control_dragging = false;
+ GANV_NODE(port)->impl->grabbed = FALSE;
+ if (event->button.x_root == control_start_x &&
+ event->button.y_root == control_start_y) {
+ select_port_toggle(port, event->button.state);
+ }
+ } else {
+ port_clicked(event, port);
+ }
+ return true;
+
+ case GDK_ENTER_NOTIFY:
+ gboolean selected;
+ g_object_get(G_OBJECT(port), "selected", &selected, NULL);
+ if (!control_dragging && !selected) {
+ highlight_port(port, true);
+ return true;
+ }
+ break;
+
+ case GDK_LEAVE_NOTIFY:
+ if (port_dragging) {
+ _drag_state = GanvCanvasImpl::EDGE;
+ _connect_port = port;
+ port_dragging = false;
+ ganv_canvas_ungrab_item(GANV_ITEM(port), event->crossing.time);
+ ganv_canvas_grab_item(
+ root,
+ GDK_BUTTON_PRESS_MASK|GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK,
+ NULL, event->crossing.time);
+ return true;
+ } else if (!control_dragging) {
+ highlight_port(port, false);
+ return true;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return false;
+}
+
+/* Called when two ports are 'joined' (connected or disconnected) */
+void
+GanvCanvasImpl::ports_joined(GanvPort* port1, GanvPort* port2)
+{
+ if (port1 == port2 || !port1 || !port2 || !port1->impl || !port2->impl) {
+ return;
+ }
+
+ highlight_port(port1, false);
+ highlight_port(port2, false);
+
+ GanvNode* src_node;
+ GanvNode* dst_node;
+
+ if (port2->impl->is_input && !port1->impl->is_input) {
+ src_node = GANV_NODE(port1);
+ dst_node = GANV_NODE(port2);
+ } else if (!port2->impl->is_input && port1->impl->is_input) {
+ src_node = GANV_NODE(port2);
+ dst_node = GANV_NODE(port1);
+ } else {
+ return;
+ }
+
+ if (!ganv_canvas_get_edge(_gcanvas, src_node, dst_node)) {
+ g_signal_emit(_gcanvas, signal_connect, 0,
+ src_node, dst_node, NULL);
+ } else {
+ g_signal_emit(_gcanvas, signal_disconnect, 0,
+ src_node, dst_node, NULL);
+ }
+}
+
+void
+GanvCanvasImpl::port_clicked(GdkEvent* event, GanvPort* port)
+{
+ const bool modded = event->button.state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK);
+ if (!modded && _last_selected_port &&
+ _last_selected_port->impl->is_input != port->impl->is_input) {
+ selection_joined_with(port);
+ } else {
+ select_port_toggle(port, event->button.state);
+ }
+}
+
+void
+GanvCanvasImpl::highlight_port(GanvPort* port, bool highlight)
+{
+ g_object_set(G_OBJECT(port), "highlighted", highlight, NULL);
+ ganv_canvas_for_each_edge_on(_gcanvas,
+ GANV_NODE(port),
+ (highlight
+ ? (GanvEdgeFunc)ganv_edge_highlight
+ : (GanvEdgeFunc)ganv_edge_unhighlight),
+ NULL);
+}
+
+/* Update animated "rubber band" selection effect. */
+gboolean
+GanvCanvasImpl::on_animate_timeout(gpointer data)
+{
+ GanvCanvasImpl* impl = (GanvCanvasImpl*)data;
+ if (!impl->pixmap_gc) {
+ return FALSE; // Unrealized
+ }
+
+ const double seconds = get_monotonic_time() / 1000000.0;
+
+ FOREACH_ITEM(impl->_selected_items, s) {
+ ganv_node_tick(*s, seconds);
+ }
+
+ for (SelectedPorts::iterator p = impl->_selected_ports.begin();
+ p != impl->_selected_ports.end();
+ ++p) {
+ ganv_node_tick(GANV_NODE(*p), seconds);
+ }
+
+ FOREACH_EDGE(impl->_selected_edges, c) {
+ ganv_edge_tick(*c, seconds);
+ }
+
+ return TRUE;
+}
+
+void
+GanvCanvasImpl::move_contents_to_internal(double x, double y, double min_x, double min_y)
+{
+ FOREACH_ITEM(_items, i) {
+ ganv_node_move(*i,
+ x - min_x,
+ y - min_y);
+ }
+}
+
+void
+GanvCanvasImpl::unselect_ports()
+{
+ for (GanvCanvasImpl::SelectedPorts::iterator i = _selected_ports.begin();
+ i != _selected_ports.end(); ++i)
+ g_object_set(G_OBJECT(*i), "selected", FALSE, NULL);
+
+ _selected_ports.clear();
+ _last_selected_port = NULL;
+}
+
+namespace Ganv {
+
+static gboolean
+on_event_after(GanvItem* canvasitem,
+ GdkEvent* ev,
+ void* canvas)
+{
+ return ((Canvas*)canvas)->signal_event.emit(ev);
+}
+
+static void
+on_connect(GanvCanvas* canvas, GanvNode* tail, GanvNode* head, void* data)
+{
+ Canvas* canvasmm = (Canvas*)data;
+ canvasmm->signal_connect.emit(Glib::wrap(tail), Glib::wrap(head));
+}
+
+static void
+on_disconnect(GanvCanvas* canvas, GanvNode* tail, GanvNode* head, void* data)
+{
+ Canvas* canvasmm = (Canvas*)data;
+ canvasmm->signal_disconnect.emit(Glib::wrap(tail), Glib::wrap(head));
+}
+
+Canvas::Canvas(double width, double height)
+ : _gobj(GANV_CANVAS(ganv_canvas_new(width, height)))
+{
+ ganv_canvas_set_wrapper(_gobj, this);
+
+ g_signal_connect_after(ganv_canvas_root(_gobj), "event",
+ G_CALLBACK(on_event_after), this);
+ g_signal_connect(gobj(), "connect",
+ G_CALLBACK(on_connect), this);
+ g_signal_connect(gobj(), "disconnect",
+ G_CALLBACK(on_disconnect), this);
+}
+
+Canvas::~Canvas()
+{
+ delete _gobj->impl;
+}
+
+void
+Canvas::remove_edge_between(Node* item1, Node* item2)
+{
+ GanvEdge* edge = ganv_canvas_get_edge(_gobj, item1->gobj(), item2->gobj());
+ if (edge) {
+ ganv_canvas_remove_edge(_gobj, edge);
+ }
+}
+
+void
+Canvas::remove_edge(Edge* edge)
+{
+ ganv_canvas_remove_edge(_gobj, edge->gobj());
+}
+
+Item*
+Canvas::get_item_at(double x, double y) const
+{
+ GanvItem* item = ganv_canvas_get_item_at(_gobj, x, y);
+ if (item) {
+ return Glib::wrap(item);
+ }
+ return NULL;
+}
+
+Edge*
+Canvas::get_edge(Node* tail, Node* head) const
+{
+ GanvEdge* e = ganv_canvas_get_edge(_gobj, tail->gobj(), head->gobj());
+ if (e) {
+ return Glib::wrap(e);
+ }
+ return NULL;
+}
+
+} // namespace Ganv
+
+extern "C" {
+
+#include "ganv/canvas.h"
+
+#include "./boilerplate.h"
+#include "./color.h"
+#include "./gettext.h"
+
+G_DEFINE_TYPE_WITH_CODE(GanvCanvas, ganv_canvas, GTK_TYPE_LAYOUT,
+ G_ADD_PRIVATE(GanvCanvas))
+
+enum {
+ PROP_0,
+ PROP_WIDTH,
+ PROP_HEIGHT,
+ PROP_DIRECTION,
+ PROP_FONT_SIZE,
+ PROP_LOCKED,
+ PROP_FOCUSED_ITEM
+};
+
+static gboolean
+on_canvas_event(GanvItem* canvasitem,
+ GdkEvent* ev,
+ void* impl)
+{
+ return ((GanvCanvasImpl*)impl)->on_event(ev);
+}
+
+static void
+ganv_canvas_init(GanvCanvas* canvas)
+{
+ GTK_WIDGET_SET_FLAGS(canvas, GTK_CAN_FOCUS);
+
+ canvas->impl = new GanvCanvasImpl(canvas);
+
+ g_signal_connect(G_OBJECT(ganv_canvas_root(canvas)),
+ "event", G_CALLBACK(on_canvas_event), canvas->impl);
+}
+
+static void
+ganv_canvas_set_property(GObject* object,
+ guint prop_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_CANVAS(object));
+
+ GanvCanvas* canvas = GANV_CANVAS(object);
+
+ switch (prop_id) {
+ case PROP_WIDTH:
+ ganv_canvas_resize(canvas, g_value_get_double(value), canvas->impl->height);
+ break;
+ case PROP_HEIGHT:
+ ganv_canvas_resize(canvas, canvas->impl->width, g_value_get_double(value));
+ break;
+ case PROP_DIRECTION:
+ ganv_canvas_set_direction(canvas, (GanvDirection)g_value_get_enum(value));
+ break;
+ case PROP_FONT_SIZE:
+ ganv_canvas_set_font_size(canvas, g_value_get_double(value));
+ break;
+ case PROP_LOCKED:
+ canvas->impl->locked = g_value_get_boolean(value);
+ break;
+ case PROP_FOCUSED_ITEM:
+ canvas->impl->focused_item = GANV_ITEM(g_value_get_object(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_canvas_get_property(GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_CANVAS(object));
+
+ GanvCanvas* canvas = GANV_CANVAS(object);
+
+ switch (prop_id) {
+ GET_CASE(WIDTH, double, canvas->impl->width)
+ GET_CASE(HEIGHT, double, canvas->impl->height)
+ GET_CASE(LOCKED, boolean, canvas->impl->locked);
+ case PROP_FOCUSED_ITEM:
+ g_value_set_object(value, GANV_CANVAS(object)->impl->focused_item);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_canvas_class_init(GanvCanvasClass* klass)
+{
+ GObjectClass* gobject_class = (GObjectClass*)klass;
+ GtkObjectClass* object_class = (GtkObjectClass*)klass;
+ GtkWidgetClass* widget_class = (GtkWidgetClass*)klass;
+
+ canvas_parent_class = GTK_LAYOUT_CLASS(g_type_class_peek_parent(klass));
+
+ gobject_class->set_property = ganv_canvas_set_property;
+ gobject_class->get_property = ganv_canvas_get_property;
+
+ object_class->destroy = ganv_canvas_destroy;
+
+ widget_class->map = ganv_canvas_map;
+ widget_class->unmap = ganv_canvas_unmap;
+ widget_class->realize = ganv_canvas_realize;
+ widget_class->unrealize = ganv_canvas_unrealize;
+ widget_class->size_allocate = ganv_canvas_size_allocate;
+ widget_class->button_press_event = ganv_canvas_button;
+ widget_class->button_release_event = ganv_canvas_button;
+ widget_class->motion_notify_event = ganv_canvas_motion;
+ widget_class->expose_event = ganv_canvas_expose;
+ widget_class->key_press_event = ganv_canvas_key;
+ widget_class->key_release_event = ganv_canvas_key;
+ widget_class->enter_notify_event = ganv_canvas_crossing;
+ widget_class->leave_notify_event = ganv_canvas_crossing;
+ widget_class->focus_in_event = ganv_canvas_focus_in;
+ widget_class->focus_out_event = ganv_canvas_focus_out;
+ widget_class->scroll_event = ganv_canvas_scroll;
+
+ g_object_class_install_property(
+ gobject_class, PROP_FOCUSED_ITEM, g_param_spec_object(
+ "focused-item",
+ _("Focused item"),
+ _("The item that currently has keyboard focus."),
+ GANV_TYPE_ITEM,
+ (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE)));
+
+ g_object_class_install_property(
+ gobject_class, PROP_WIDTH, g_param_spec_double(
+ "width",
+ _("Width"),
+ _("The width of the canvas."),
+ 0.0, G_MAXDOUBLE,
+ 800.0,
+ (GParamFlags)G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_HEIGHT, g_param_spec_double(
+ "height",
+ _("Height"),
+ _("The height of the canvas"),
+ 0.0, G_MAXDOUBLE,
+ 600.0,
+ (GParamFlags)G_PARAM_READWRITE));
+
+ GEnumValue down_dir = { GANV_DIRECTION_DOWN, "down", "down" };
+ GEnumValue right_dir = { GANV_DIRECTION_RIGHT, "right", "right" };
+ GEnumValue null_dir = { 0, 0, 0 };
+ dir_values[0] = down_dir;
+ dir_values[1] = right_dir;
+ dir_values[2] = null_dir;
+ GType dir_type = g_enum_register_static("GanvDirection",
+ dir_values);
+
+ g_object_class_install_property(
+ gobject_class, PROP_DIRECTION, g_param_spec_enum(
+ "direction",
+ _("Direction"),
+ _("The direction of the signal flow on the canvas."),
+ dir_type,
+ GANV_DIRECTION_RIGHT,
+ (GParamFlags)G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_FONT_SIZE, g_param_spec_double(
+ "font-size",
+ _("Font size"),
+ _("The default font size for the canvas"),
+ 0.0, G_MAXDOUBLE,
+ 12.0,
+ (GParamFlags)G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_LOCKED, g_param_spec_boolean(
+ "locked",
+ _("Locked"),
+ _("If true, nodes on the canvas can not be moved by the user."),
+ FALSE,
+ (GParamFlags)G_PARAM_READWRITE));
+
+ signal_connect = g_signal_new("connect",
+ ganv_canvas_get_type(),
+ G_SIGNAL_RUN_FIRST,
+ 0, NULL, NULL,
+ ganv_marshal_VOID__OBJECT_OBJECT,
+ G_TYPE_NONE,
+ 2,
+ ganv_node_get_type(),
+ ganv_node_get_type(),
+ 0);
+
+ signal_disconnect = g_signal_new("disconnect",
+ ganv_canvas_get_type(),
+ G_SIGNAL_RUN_FIRST,
+ 0, NULL, NULL,
+ ganv_marshal_VOID__OBJECT_OBJECT,
+ G_TYPE_NONE,
+ 2,
+ ganv_node_get_type(),
+ ganv_node_get_type(),
+ 0);
+}
+
+void
+ganv_canvas_resize(GanvCanvas* canvas, double width, double height)
+{
+ if (width != canvas->impl->width || height != canvas->impl->height) {
+ canvas->impl->width = width;
+ canvas->impl->height = height;
+ ganv_canvas_set_scroll_region(canvas, 0.0, 0.0, width, height);
+ }
+}
+
+void
+ganv_canvas_contents_changed(GanvCanvas* canvas)
+{
+#ifdef GANV_FDGL
+ if (!canvas->impl->layout_idle_id && canvas->impl->sprung_layout) {
+ canvas->impl->layout_energy = 0.4;
+ canvas->impl->layout_idle_id = g_timeout_add_full(
+ G_PRIORITY_DEFAULT_IDLE,
+ 33,
+ GanvCanvasImpl::on_layout_timeout,
+ canvas->impl,
+ GanvCanvasImpl::on_layout_done);
+ }
+#endif
+}
+
+double
+ganv_canvas_get_default_font_size(const GanvCanvas* canvas)
+{
+ GtkStyle* style = gtk_rc_get_style(GTK_WIDGET(canvas));
+ const PangoFontDescription* font = style->font_desc;
+ return pango_font_description_get_size(font) / (double)PANGO_SCALE;
+}
+
+double
+ganv_canvas_get_font_size(const GanvCanvas* canvas)
+{
+ return canvas->impl->font_size;
+}
+
+void
+ganv_canvas_set_zoom(GanvCanvas* canvas, double zoom)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ zoom = std::max(zoom, 0.01);
+ if (zoom == canvas->impl->pixels_per_unit) {
+ return;
+ }
+
+ const int anchor_x = (canvas->impl->center_scroll_region)
+ ? GTK_WIDGET(canvas)->allocation.width / 2
+ : 0;
+ const int anchor_y = (canvas->impl->center_scroll_region)
+ ? GTK_WIDGET(canvas)->allocation.height / 2
+ : 0;
+
+ /* Find the coordinates of the anchor point in units. */
+ const double ax = (canvas->layout.hadjustment)
+ ? ((canvas->layout.hadjustment->value + anchor_x)
+ / canvas->impl->pixels_per_unit
+ + canvas->impl->scroll_x1 + canvas->impl->zoom_xofs)
+ : ((0.0 + anchor_x) / canvas->impl->pixels_per_unit
+ + canvas->impl->scroll_x1 + canvas->impl->zoom_xofs);
+ const double ay = (canvas->layout.hadjustment)
+ ? ((canvas->layout.vadjustment->value + anchor_y)
+ / canvas->impl->pixels_per_unit
+ + canvas->impl->scroll_y1 + canvas->impl->zoom_yofs)
+ : ((0.0 + anchor_y) / canvas->impl->pixels_per_unit
+ + canvas->impl->scroll_y1 + canvas->impl->zoom_yofs);
+
+ /* Now calculate the new offset of the upper left corner. */
+ const int x1 = ((ax - canvas->impl->scroll_x1) * zoom) - anchor_x;
+ const int y1 = ((ay - canvas->impl->scroll_y1) * zoom) - anchor_y;
+
+ canvas->impl->pixels_per_unit = zoom;
+ ganv_canvas_scroll_to(canvas, x1, y1);
+
+ ganv_canvas_request_update(canvas);
+ gtk_widget_queue_draw(GTK_WIDGET(canvas));
+
+ canvas->impl->need_repick = TRUE;
+}
+
+void
+ganv_canvas_set_font_size(GanvCanvas* canvas, double points)
+{
+ points = std::max(points, 1.0);
+ if (points != canvas->impl->font_size) {
+ canvas->impl->font_size = points;
+ FOREACH_ITEM(canvas->impl->_items, i) {
+ ganv_node_redraw_text(*i);
+ }
+ }
+}
+
+void
+ganv_canvas_zoom_full(GanvCanvas* canvas)
+{
+ if (canvas->impl->_items.empty())
+ return;
+
+ int win_width, win_height;
+ GdkWindow* win = gtk_widget_get_window(
+ GTK_WIDGET(canvas->impl->_gcanvas));
+ gdk_window_get_size(win, &win_width, &win_height);
+
+ // Box containing all canvas items
+ double left = DBL_MAX;
+ double right = DBL_MIN;
+ double top = DBL_MIN;
+ double bottom = DBL_MAX;
+
+ FOREACH_ITEM(canvas->impl->_items, i) {
+ GanvItem* const item = GANV_ITEM(*i);
+ const double x = item->impl->x;
+ const double y = item->impl->y;
+ if (GANV_IS_CIRCLE(*i)) {
+ const double r = GANV_CIRCLE(*i)->impl->coords.radius;
+ left = MIN(left, x - r);
+ right = MAX(right, x + r);
+ bottom = MIN(bottom, y - r);
+ top = MAX(top, y + r);
+ } else {
+ left = MIN(left, x);
+ right = MAX(right, x + ganv_box_get_width(GANV_BOX(*i)));
+ bottom = MIN(bottom, y);
+ top = MAX(top, y + ganv_box_get_height(GANV_BOX(*i)));
+ }
+ }
+
+ static const double pad = GANV_CANVAS_PAD;
+
+ const double new_zoom = std::min(
+ ((double)win_width / (double)(right - left + pad*2.0)),
+ ((double)win_height / (double)(top - bottom + pad*2.0)));
+
+ ganv_canvas_set_zoom(canvas, new_zoom);
+
+ int scroll_x, scroll_y;
+ ganv_canvas_w2c(canvas->impl->_gcanvas,
+ lrintf(left - pad), lrintf(bottom - pad),
+ &scroll_x, &scroll_y);
+
+ ganv_canvas_scroll_to(canvas->impl->_gcanvas,
+ scroll_x, scroll_y);
+}
+
+static void
+set_node_direction(GanvNode* node, void* data)
+{
+ if (GANV_IS_MODULE(node)) {
+ ganv_module_set_direction(GANV_MODULE(node), *(GanvDirection*)data);
+ }
+}
+
+GanvDirection
+ganv_canvas_get_direction(GanvCanvas* canvas)
+{
+ return canvas->impl->direction;
+}
+
+void
+ganv_canvas_set_direction(GanvCanvas* canvas, GanvDirection dir)
+{
+ if (canvas->impl->direction != dir) {
+ canvas->impl->direction = dir;
+ ganv_canvas_for_each_node(canvas, set_node_direction, &dir);
+ ganv_canvas_contents_changed(canvas);
+ }
+}
+
+void
+ganv_canvas_clear_selection(GanvCanvas* canvas)
+{
+ canvas->impl->unselect_ports();
+
+ Items items(canvas->impl->_selected_items);
+ canvas->impl->_selected_items.clear();
+ FOREACH_ITEM(items, i) {
+ ganv_item_set(GANV_ITEM(*i), "selected", FALSE, NULL);
+ }
+
+ GanvCanvasImpl::SelectedEdges edges(canvas->impl->_selected_edges);
+ canvas->impl->_selected_edges.clear();
+ FOREACH_SELECTED_EDGE(edges, c) {
+ ganv_item_set(GANV_ITEM(*c), "selected", FALSE, NULL);
+ }
+}
+
+void
+ganv_canvas_move_selected_items(GanvCanvas* canvas,
+ double dx,
+ double dy)
+{
+ FOREACH_ITEM(canvas->impl->_selected_items, i) {
+ if ((*i)->item.impl->parent == canvas->impl->root) {
+ ganv_node_move(*i, dx, dy);
+ }
+ }
+}
+
+void
+ganv_canvas_selection_move_finished(GanvCanvas* canvas)
+{
+ FOREACH_ITEM(canvas->impl->_selected_items, i) {
+ const double x = GANV_ITEM(*i)->impl->x;
+ const double y = GANV_ITEM(*i)->impl->y;
+ g_signal_emit(*i, signal_moved, 0, x, y, NULL);
+ }
+}
+
+static void
+select_if_ends_are_selected(GanvEdge* edge, void* data)
+{
+ if (ganv_node_is_selected(ganv_edge_get_tail(edge)) &&
+ ganv_node_is_selected(ganv_edge_get_head(edge))) {
+ ganv_edge_set_selected(edge, TRUE);
+ }
+}
+
+static void
+unselect_edges(GanvPort* port, void* data)
+{
+ GanvCanvasImpl* impl = (GanvCanvasImpl*)data;
+ if (port->impl->is_input) {
+ ganv_canvas_for_each_edge_to(impl->_gcanvas,
+ GANV_NODE(port),
+ (GanvEdgeFunc)ganv_edge_unselect,
+ NULL);
+ } else {
+ ganv_canvas_for_each_edge_from(impl->_gcanvas,
+ GANV_NODE(port),
+ (GanvEdgeFunc)ganv_edge_unselect,
+ NULL);
+ }
+}
+
+void
+ganv_canvas_select_node(GanvCanvas* canvas,
+ GanvNode* node)
+{
+ canvas->impl->_selected_items.insert(node);
+
+ // Select any connections to or from this node
+ if (GANV_IS_MODULE(node)) {
+ ganv_module_for_each_port(GANV_MODULE(node), select_edges, canvas->impl);
+ } else {
+ ganv_canvas_for_each_edge_on(
+ canvas, node, select_if_ends_are_selected, canvas->impl);
+ }
+
+ g_object_set(node, "selected", TRUE, NULL);
+}
+
+void
+ganv_canvas_unselect_node(GanvCanvas* canvas,
+ GanvNode* node)
+{
+ // Unselect any connections to or from canvas->impl node
+ if (GANV_IS_MODULE(node)) {
+ ganv_module_for_each_port(GANV_MODULE(node), unselect_edges, canvas->impl);
+ } else {
+ ganv_canvas_for_each_edge_on(
+ canvas, node, (GanvEdgeFunc)ganv_edge_unselect, NULL);
+ }
+
+ // Unselect item
+ canvas->impl->_selected_items.erase(node);
+ g_object_set(node, "selected", FALSE, NULL);
+}
+
+void
+ganv_canvas_add_node(GanvCanvas* canvas,
+ GanvNode* node)
+{
+ GanvItem* item = GANV_ITEM(node);
+ if (item->impl->parent == ganv_canvas_root(canvas)) {
+ canvas->impl->_items.insert(node);
+ }
+}
+
+void
+ganv_canvas_remove_node(GanvCanvas* canvas,
+ GanvNode* node)
+{
+ if (node == (GanvNode*)canvas->impl->_connect_port) {
+ if (canvas->impl->_drag_state == GanvCanvasImpl::EDGE) {
+ ganv_canvas_ungrab_item(ganv_canvas_root(canvas), 0);
+ canvas->impl->end_connect_drag();
+ }
+ canvas->impl->_connect_port = NULL;
+ }
+
+ // Remove from selection
+ canvas->impl->_selected_items.erase(node);
+
+ // Remove children ports from selection if item is a module
+ if (GANV_IS_MODULE(node)) {
+ GanvModule* const module = GANV_MODULE(node);
+ for (unsigned i = 0; i < ganv_module_num_ports(module); ++i) {
+ canvas->impl->unselect_port(ganv_module_get_port(module, i));
+ }
+ }
+
+ // Remove from items
+ canvas->impl->_items.erase(node);
+}
+
+GanvEdge*
+ganv_canvas_get_edge(GanvCanvas* canvas,
+ GanvNode* tail,
+ GanvNode* head)
+{
+ GanvEdgeKey key;
+ make_edge_search_key(&key, tail, head);
+ GanvCanvasImpl::Edges::const_iterator i = canvas->impl->_edges.find((GanvEdge*)&key);
+ return (i != canvas->impl->_edges.end()) ? *i : NULL;
+}
+
+void
+ganv_canvas_remove_edge_between(GanvCanvas* canvas,
+ GanvNode* tail,
+ GanvNode* head)
+{
+ ganv_canvas_remove_edge(canvas, ganv_canvas_get_edge(canvas, tail, head));
+}
+
+void
+ganv_canvas_disconnect_edge(GanvCanvas* canvas,
+ GanvEdge* edge)
+{
+ g_signal_emit(canvas, signal_disconnect, 0,
+ edge->impl->tail, edge->impl->head, NULL);
+}
+
+void
+ganv_canvas_add_edge(GanvCanvas* canvas,
+ GanvEdge* edge)
+{
+ canvas->impl->_edges.insert(edge);
+ canvas->impl->_dst_edges.insert(edge);
+ ganv_canvas_contents_changed(canvas);
+}
+
+void
+ganv_canvas_remove_edge(GanvCanvas* canvas,
+ GanvEdge* edge)
+{
+ if (edge) {
+ canvas->impl->_selected_edges.erase(edge);
+ canvas->impl->_edges.erase(edge);
+ canvas->impl->_dst_edges.erase(edge);
+ ganv_edge_request_redraw(GANV_ITEM(edge), &edge->impl->coords);
+ gtk_object_destroy(GTK_OBJECT(edge));
+ ganv_canvas_contents_changed(canvas);
+ }
+}
+
+void
+ganv_canvas_select_edge(GanvCanvas* canvas,
+ GanvEdge* edge)
+{
+ ganv_item_set(GANV_ITEM(edge), "selected", TRUE, NULL);
+ canvas->impl->_selected_edges.insert(edge);
+}
+
+void
+ganv_canvas_unselect_edge(GanvCanvas* canvas,
+ GanvEdge* edge)
+{
+ ganv_item_set(GANV_ITEM(edge), "selected", FALSE, NULL);
+ canvas->impl->_selected_edges.erase(edge);
+}
+
+void
+ganv_canvas_for_each_node(GanvCanvas* canvas,
+ GanvNodeFunc f,
+ void* data)
+{
+ FOREACH_ITEM(canvas->impl->_items, i) {
+ f(*i, data);
+ }
+}
+
+void
+ganv_canvas_for_each_selected_node(GanvCanvas* canvas,
+ GanvNodeFunc f,
+ void* data)
+{
+ FOREACH_ITEM(canvas->impl->_selected_items, i) {
+ f(*i, data);
+ }
+}
+
+gboolean
+ganv_canvas_empty(const GanvCanvas* canvas)
+{
+ return canvas->impl->_items.empty();
+}
+
+void
+ganv_canvas_for_each_edge(GanvCanvas* canvas,
+ GanvEdgeFunc f,
+ void* data)
+{
+ GanvCanvasImpl* impl = canvas->impl;
+ for (GanvCanvasImpl::Edges::const_iterator i = impl->_edges.begin();
+ i != impl->_edges.end();) {
+ GanvCanvasImpl::Edges::const_iterator next = i;
+ ++next;
+ f((*i), data);
+ i = next;
+ }
+}
+
+void
+ganv_canvas_for_each_edge_from(GanvCanvas* canvas,
+ const GanvNode* tail,
+ GanvEdgeFunc f,
+ void* data)
+{
+ GanvCanvasImpl* impl = canvas->impl;
+ for (GanvCanvasImpl::Edges::const_iterator i = impl->first_edge_from(tail);
+ i != impl->_edges.end() && (*i)->impl->tail == tail;) {
+ GanvCanvasImpl::Edges::const_iterator next = i;
+ ++next;
+ f((*i), data);
+ i = next;
+ }
+}
+
+void
+ganv_canvas_for_each_edge_to(GanvCanvas* canvas,
+ const GanvNode* head,
+ GanvEdgeFunc f,
+ void* data)
+{
+ GanvCanvasImpl* impl = canvas->impl;
+ for (GanvCanvasImpl::Edges::const_iterator i = impl->first_edge_to(head);
+ i != impl->_dst_edges.end() && (*i)->impl->head == head;) {
+ GanvCanvasImpl::Edges::const_iterator next = i;
+ ++next;
+ f((*i), data);
+ i = next;
+ }
+}
+
+void
+ganv_canvas_for_each_edge_on(GanvCanvas* canvas,
+ const GanvNode* node,
+ GanvEdgeFunc f,
+ void* data)
+{
+ ganv_canvas_for_each_edge_from(canvas, node, f, data);
+ ganv_canvas_for_each_edge_to(canvas, node, f, data);
+}
+
+void
+ganv_canvas_for_each_selected_edge(GanvCanvas* canvas,
+ GanvEdgeFunc f,
+ void* data)
+{
+ FOREACH_EDGE(canvas->impl->_selected_edges, i) {
+ f((*i), data);
+ }
+}
+
+GdkCursor*
+ganv_canvas_get_move_cursor(const GanvCanvas* canvas)
+{
+ return canvas->impl->_move_cursor;
+}
+
+gboolean
+ganv_canvas_port_event(GanvCanvas* canvas,
+ GanvPort* port,
+ GdkEvent* event)
+{
+ return canvas->impl->port_event(event, port);
+}
+
+void
+ganv_canvas_clear(GanvCanvas* canvas)
+{
+ canvas->impl->_selected_items.clear();
+ canvas->impl->_selected_edges.clear();
+
+ Items items = canvas->impl->_items; // copy
+ FOREACH_ITEM(items, i) {
+ gtk_object_destroy(GTK_OBJECT(*i));
+ }
+ canvas->impl->_items.clear();
+
+ GanvCanvasImpl::Edges edges = canvas->impl->_edges; // copy
+ FOREACH_EDGE(edges, i) {
+ gtk_object_destroy(GTK_OBJECT(*i));
+ }
+ canvas->impl->_edges.clear();
+ canvas->impl->_dst_edges.clear();
+
+ canvas->impl->_selected_ports.clear();
+ canvas->impl->_connect_port = NULL;
+}
+
+void
+ganv_canvas_select_all(GanvCanvas* canvas)
+{
+ ganv_canvas_clear_selection(canvas);
+ FOREACH_ITEM(canvas->impl->_items, i) {
+ ganv_canvas_select_node(canvas, *i);
+ }
+}
+
+double
+ganv_canvas_get_zoom(const GanvCanvas* canvas)
+{
+ return canvas->impl->pixels_per_unit;
+}
+
+void
+ganv_canvas_move_contents_to(GanvCanvas* canvas, double x, double y)
+{
+ double min_x=HUGE_VAL, min_y=HUGE_VAL;
+ FOREACH_ITEM(canvas->impl->_items, i) {
+ const double x = GANV_ITEM(*i)->impl->x;
+ const double y = GANV_ITEM(*i)->impl->y;
+ min_x = std::min(min_x, x);
+ min_y = std::min(min_y, y);
+ }
+ canvas->impl->move_contents_to_internal(x, y, min_x, min_y);
+}
+
+void
+ganv_canvas_arrange(GanvCanvas* canvas)
+{
+#ifdef HAVE_AGRAPH
+ GVNodes nodes = canvas->impl->layout_dot((char*)"");
+
+ double least_x=HUGE_VAL, least_y=HUGE_VAL, most_x=0, most_y=0;
+
+ // Set numeric locale to POSIX for reading graphviz output with strtod
+ char* locale = strdup(setlocale(LC_NUMERIC, NULL));
+ setlocale(LC_NUMERIC, "POSIX");
+
+ const double dpi = gdk_screen_get_resolution(gdk_screen_get_default());
+ const double dpp = dpi / 72.0;
+
+ // Arrange to graphviz coordinates
+ for (GVNodes::iterator i = nodes.begin(); i != nodes.end(); ++i) {
+ if (GANV_ITEM(i->first)->impl->parent != GANV_ITEM(ganv_canvas_root(canvas))) {
+ continue;
+ }
+ const std::string pos = agget(i->second, (char*)"pos");
+ const std::string x_str = pos.substr(0, pos.find(","));
+ const std::string y_str = pos.substr(pos.find(",") + 1);
+ const double cx = lrint(strtod(x_str.c_str(), NULL) * dpp);
+ const double cy = lrint(strtod(y_str.c_str(), NULL) * dpp);
+
+ double w, h;
+ if (GANV_IS_BOX(i->first)) {
+ w = ganv_box_get_width(GANV_BOX(i->first));
+ h = ganv_box_get_height(GANV_BOX(i->first));
+ } else {
+ w = h = ganv_circle_get_radius(GANV_CIRCLE(i->first)) * 2.3;
+ }
+
+ /* Dot node positions are supposedly node centers, but things only
+ match up if x is interpreted as center and y as top...
+ */
+ double x = cx - (w / 2.0);
+ double y = -cy - (h / 2.0);
+
+ ganv_node_move_to(i->first, x, y);
+
+ if (GANV_IS_CIRCLE(i->first)) {
+ // Offset least x and y to avoid cutting off circles at origin
+ const double r = ganv_circle_get_radius(GANV_CIRCLE(i->first));
+ x -= r;
+ y -= r;
+ }
+
+ least_x = std::min(least_x, x);
+ least_y = std::min(least_y, y);
+ most_x = std::max(most_x, x + w);
+ most_y = std::max(most_y, y + h);
+ }
+
+ // Reset numeric locale to original value
+ setlocale(LC_NUMERIC, locale);
+ free(locale);
+
+ const double graph_width = most_x - least_x;
+ const double graph_height = most_y - least_y;
+
+ //cerr << "CWH: " << _width << ", " << _height << endl;
+ //cerr << "GWH: " << graph_width << ", " << graph_height << endl;
+
+ double old_width, old_height;
+ g_object_get(G_OBJECT(canvas),
+ "width", &old_width,
+ "height", &old_height,
+ NULL);
+
+ const double new_width = std::max(graph_width + 10.0, old_width);
+ const double new_height = std::max(graph_height + 10.0, old_height);
+ if (new_width != old_width || new_height != old_height) {
+ ganv_canvas_resize(canvas, new_width, new_height);
+ }
+ nodes.cleanup();
+
+ static const double border_width = GANV_CANVAS_PAD;
+ canvas->impl->move_contents_to_internal(border_width, border_width, least_x, least_y);
+ ganv_canvas_scroll_to(canvas->impl->_gcanvas, 0, 0);
+
+ FOREACH_ITEM(canvas->impl->_items, i) {
+ const double x = GANV_ITEM(*i)->impl->x;
+ const double y = GANV_ITEM(*i)->impl->y;
+ g_signal_emit(*i, signal_moved, 0, x, y, NULL);
+ }
+#endif
+}
+
+int
+ganv_canvas_export_image(GanvCanvas* canvas,
+ const char* filename,
+ gboolean draw_background)
+{
+ const char* ext = strrchr(filename, '.');
+ if (!ext) {
+ return 1;
+ } else if (!strcmp(ext, ".dot")) {
+ ganv_canvas_export_dot(canvas, filename);
+ return 0;
+ }
+
+ cairo_surface_t* rec_surface = cairo_recording_surface_create(
+ CAIRO_CONTENT_COLOR_ALPHA, NULL);
+
+ // Draw to recording surface
+ cairo_t* cr = cairo_create(rec_surface);
+ canvas->impl->exporting = TRUE;
+ (*GANV_ITEM_GET_CLASS(canvas->impl->root)->draw)(
+ canvas->impl->root, cr,
+ 0, 0, canvas->impl->width, canvas->impl->height);
+ canvas->impl->exporting = FALSE;
+ cairo_destroy(cr);
+
+ // Get draw extent
+ double x, y, w, h;
+ cairo_recording_surface_ink_extents(rec_surface, &x, &y, &w, &h);
+
+ // Create image surface with the appropriate size
+ const double pad = GANV_CANVAS_PAD;
+ const double img_w = w + pad * 2;
+ const double img_h = h + pad * 2;
+ cairo_surface_t* img = NULL;
+ if (!strcmp(ext, ".svg")) {
+ img = cairo_svg_surface_create(filename, img_w, img_h);
+ } else if (!strcmp(ext, ".pdf")) {
+ img = cairo_pdf_surface_create(filename, img_w, img_h);
+ } else if (!strcmp(ext, ".ps")) {
+ img = cairo_ps_surface_create(filename, img_w, img_h);
+ } else {
+ cairo_surface_destroy(rec_surface);
+ return 1;
+ }
+
+ // Draw recording to image surface
+ cr = cairo_create(img);
+ if (draw_background) {
+ double r, g, b, a;
+ color_to_rgba(DEFAULT_BACKGROUND_COLOR, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_rectangle(cr, 0, 0, w + 2 * pad, h + 2 * pad);
+ cairo_fill(cr);
+ }
+ cairo_set_source_surface(cr, rec_surface, -x + pad, -y + pad);
+ cairo_paint(cr);
+ cairo_destroy(cr);
+ cairo_surface_destroy(rec_surface);
+ cairo_surface_destroy(img);
+ return 0;
+}
+
+void
+ganv_canvas_export_dot(GanvCanvas* canvas, const char* filename)
+{
+#ifdef HAVE_AGRAPH
+ GVNodes nodes = canvas->impl->layout_dot(filename);
+ nodes.cleanup();
+#endif
+}
+
+gboolean
+ganv_canvas_supports_sprung_layout(const GanvCanvas* canvas)
+{
+#ifdef GANV_FDGL
+ return TRUE;
+#else
+ return FALSE;
+#endif
+}
+
+gboolean
+ganv_canvas_set_sprung_layout(GanvCanvas* canvas, gboolean sprung_layout)
+{
+#ifndef GANV_FDGL
+ return FALSE;
+#else
+ canvas->impl->sprung_layout = sprung_layout;
+ ganv_canvas_contents_changed(canvas);
+ return TRUE;
+#endif
+}
+
+gboolean
+ganv_canvas_get_locked(const GanvCanvas* canvas)
+{
+ return canvas->impl->locked;
+}
+
+/* Convenience function to remove the idle handler of a canvas */
+static void
+remove_idle(GanvCanvas* canvas)
+{
+ if (canvas->impl->idle_id == 0) {
+ return;
+ }
+
+ g_source_remove(canvas->impl->idle_id);
+ canvas->impl->idle_id = 0;
+}
+
+/* Removes the transient state of the canvas (idle handler, grabs). */
+static void
+shutdown_transients(GanvCanvas* canvas)
+{
+ /* We turn off the need_redraw flag, since if the canvas is mapped again
+ * it will request a redraw anyways. We do not turn off the need_update
+ * flag, though, because updates are not queued when the canvas remaps
+ * itself.
+ */
+ if (canvas->impl->need_redraw) {
+ canvas->impl->need_redraw = FALSE;
+ g_slist_foreach(canvas->impl->redraw_region, (GFunc)g_free, NULL);
+ g_slist_free(canvas->impl->redraw_region);
+ canvas->impl->redraw_region = NULL;
+ canvas->impl->redraw_x1 = 0;
+ canvas->impl->redraw_y1 = 0;
+ canvas->impl->redraw_x2 = 0;
+ canvas->impl->redraw_y2 = 0;
+ }
+
+ if (canvas->impl->grabbed_item) {
+ canvas->impl->grabbed_item = NULL;
+ gdk_pointer_ungrab(GDK_CURRENT_TIME);
+ }
+
+ remove_idle(canvas);
+}
+
+/* Destroy handler for GanvCanvas */
+static void
+ganv_canvas_destroy(GtkObject* object)
+{
+ g_return_if_fail(GANV_IS_CANVAS(object));
+
+ /* remember, destroy can be run multiple times! */
+
+ GanvCanvas* canvas = GANV_CANVAS(object);
+
+ if (canvas->impl->root_destroy_id) {
+ g_signal_handler_disconnect(canvas->impl->root, canvas->impl->root_destroy_id);
+ canvas->impl->root_destroy_id = 0;
+ }
+ if (canvas->impl->root) {
+ gtk_object_destroy(GTK_OBJECT(canvas->impl->root));
+ g_object_unref(G_OBJECT(canvas->impl->root));
+ canvas->impl->root = NULL;
+ }
+
+ shutdown_transients(canvas);
+
+ if (GTK_OBJECT_CLASS(canvas_parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(canvas_parent_class)->destroy)(object);
+ }
+}
+
+GanvCanvas*
+ganv_canvas_new(double width, double height)
+{
+ GanvCanvas* canvas = GANV_CANVAS(
+ g_object_new(ganv_canvas_get_type(),
+ "width", width,
+ "height", height,
+ NULL));
+
+ ganv_canvas_set_scroll_region(canvas, 0.0, 0.0, width, height);
+
+ return canvas;
+}
+
+void
+ganv_canvas_set_wrapper(GanvCanvas* canvas, void* wrapper)
+{
+ canvas->impl->_wrapper = (Ganv::Canvas*)wrapper;
+}
+
+void*
+ganv_canvas_get_wrapper(GanvCanvas* canvas)
+{
+ return canvas->impl->_wrapper;
+}
+
+/* Map handler for the canvas */
+static void
+ganv_canvas_map(GtkWidget* widget)
+{
+ g_return_if_fail(GANV_IS_CANVAS(widget));
+
+ /* Normal widget mapping stuff */
+
+ if (GTK_WIDGET_CLASS(canvas_parent_class)->map) {
+ (*GTK_WIDGET_CLASS(canvas_parent_class)->map)(widget);
+ }
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ if (canvas->impl->need_update) {
+ add_idle(canvas);
+ }
+
+ /* Map items */
+
+ if (GANV_ITEM_GET_CLASS(canvas->impl->root)->map) {
+ (*GANV_ITEM_GET_CLASS(canvas->impl->root)->map)(canvas->impl->root);
+ }
+}
+
+/* Unmap handler for the canvas */
+static void
+ganv_canvas_unmap(GtkWidget* widget)
+{
+ g_return_if_fail(GANV_IS_CANVAS(widget));
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ shutdown_transients(canvas);
+
+ /* Unmap items */
+
+ if (GANV_ITEM_GET_CLASS(canvas->impl->root)->unmap) {
+ (*GANV_ITEM_GET_CLASS(canvas->impl->root)->unmap)(canvas->impl->root);
+ }
+
+ /* Normal widget unmapping stuff */
+
+ if (GTK_WIDGET_CLASS(canvas_parent_class)->unmap) {
+ (*GTK_WIDGET_CLASS(canvas_parent_class)->unmap)(widget);
+ }
+}
+
+/* Realize handler for the canvas */
+static void
+ganv_canvas_realize(GtkWidget* widget)
+{
+ g_return_if_fail(GANV_IS_CANVAS(widget));
+
+ /* Normal widget realization stuff */
+
+ if (GTK_WIDGET_CLASS(canvas_parent_class)->realize) {
+ (*GTK_WIDGET_CLASS(canvas_parent_class)->realize)(widget);
+ }
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ gdk_window_set_events(
+ canvas->layout.bin_window,
+ (GdkEventMask)(gdk_window_get_events(canvas->layout.bin_window)
+ | GDK_EXPOSURE_MASK
+ | GDK_BUTTON_PRESS_MASK
+ | GDK_BUTTON_RELEASE_MASK
+ | GDK_POINTER_MOTION_MASK
+ | GDK_KEY_PRESS_MASK
+ | GDK_KEY_RELEASE_MASK
+ | GDK_ENTER_NOTIFY_MASK
+ | GDK_LEAVE_NOTIFY_MASK
+ | GDK_FOCUS_CHANGE_MASK));
+
+ /* Create our own temporary pixmap gc and realize all the items */
+
+ canvas->impl->pixmap_gc = gdk_gc_new(canvas->layout.bin_window);
+
+ (*GANV_ITEM_GET_CLASS(canvas->impl->root)->realize)(canvas->impl->root);
+
+ canvas->impl->_animate_idle_id = g_timeout_add(
+ 120, GanvCanvasImpl::on_animate_timeout, canvas->impl);
+}
+
+/* Unrealize handler for the canvas */
+static void
+ganv_canvas_unrealize(GtkWidget* widget)
+{
+ g_return_if_fail(GANV_IS_CANVAS(widget));
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ if (canvas->impl->_animate_idle_id) {
+ g_source_remove(canvas->impl->_animate_idle_id);
+ canvas->impl->_animate_idle_id = 0;
+ }
+ while (g_idle_remove_by_data(canvas->impl)) {}
+
+ shutdown_transients(canvas);
+
+ /* Unrealize items and parent widget */
+
+ (*GANV_ITEM_GET_CLASS(canvas->impl->root)->unrealize)(canvas->impl->root);
+
+ g_object_unref(canvas->impl->pixmap_gc);
+ canvas->impl->pixmap_gc = NULL;
+
+ if (GTK_WIDGET_CLASS(canvas_parent_class)->unrealize) {
+ (*GTK_WIDGET_CLASS(canvas_parent_class)->unrealize)(widget);
+ }
+}
+
+/* Handles scrolling of the canvas. Adjusts the scrolling and zooming offset to
+ * keep as much as possible of the canvas scrolling region in view.
+ */
+static void
+scroll_to(GanvCanvas* canvas, int cx, int cy)
+{
+ int scroll_width, scroll_height;
+ int right_limit, bottom_limit;
+ int old_zoom_xofs, old_zoom_yofs;
+ int changed_x = FALSE, changed_y = FALSE;
+ int canvas_width, canvas_height;
+
+ canvas_width = GTK_WIDGET(canvas)->allocation.width;
+ canvas_height = GTK_WIDGET(canvas)->allocation.height;
+
+ scroll_width = floor((canvas->impl->scroll_x2 - canvas->impl->scroll_x1) * canvas->impl->pixels_per_unit
+ + 0.5);
+ scroll_height = floor((canvas->impl->scroll_y2 - canvas->impl->scroll_y1) * canvas->impl->pixels_per_unit
+ + 0.5);
+
+ right_limit = scroll_width - canvas_width;
+ bottom_limit = scroll_height - canvas_height;
+
+ old_zoom_xofs = canvas->impl->zoom_xofs;
+ old_zoom_yofs = canvas->impl->zoom_yofs;
+
+ if (right_limit < 0) {
+ cx = 0;
+
+ if (canvas->impl->center_scroll_region) {
+ canvas->impl->zoom_xofs = (canvas_width - scroll_width) / 2;
+ scroll_width = canvas_width;
+ } else {
+ canvas->impl->zoom_xofs = 0;
+ }
+ } else if (cx < 0) {
+ cx = 0;
+ canvas->impl->zoom_xofs = 0;
+ } else if (cx > right_limit) {
+ cx = right_limit;
+ canvas->impl->zoom_xofs = 0;
+ } else {
+ canvas->impl->zoom_xofs = 0;
+ }
+
+ if (bottom_limit < 0) {
+ cy = 0;
+
+ if (canvas->impl->center_scroll_region) {
+ canvas->impl->zoom_yofs = (canvas_height - scroll_height) / 2;
+ scroll_height = canvas_height;
+ } else {
+ canvas->impl->zoom_yofs = 0;
+ }
+ } else if (cy < 0) {
+ cy = 0;
+ canvas->impl->zoom_yofs = 0;
+ } else if (cy > bottom_limit) {
+ cy = bottom_limit;
+ canvas->impl->zoom_yofs = 0;
+ } else {
+ canvas->impl->zoom_yofs = 0;
+ }
+
+ if ((canvas->impl->zoom_xofs != old_zoom_xofs) || (canvas->impl->zoom_yofs != old_zoom_yofs)) {
+ ganv_canvas_request_update(canvas);
+ gtk_widget_queue_draw(GTK_WIDGET(canvas));
+ }
+
+ if (canvas->layout.hadjustment && ( ((int)canvas->layout.hadjustment->value) != cx) ) {
+ canvas->layout.hadjustment->value = cx;
+ changed_x = TRUE;
+ }
+
+ if (canvas->layout.vadjustment && ( ((int)canvas->layout.vadjustment->value) != cy) ) {
+ canvas->layout.vadjustment->value = cy;
+ changed_y = TRUE;
+ }
+
+ if ((scroll_width != (int)canvas->layout.width)
+ || (scroll_height != (int)canvas->layout.height)) {
+ gtk_layout_set_size(GTK_LAYOUT(canvas), scroll_width, scroll_height);
+ }
+
+ /* Signal GtkLayout that it should do a redraw. */
+
+ if (changed_x) {
+ g_signal_emit_by_name(canvas->layout.hadjustment, "value_changed");
+ }
+
+ if (changed_y) {
+ g_signal_emit_by_name(canvas->layout.vadjustment, "value_changed");
+ }
+}
+
+/* Size allocation handler for the canvas */
+static void
+ganv_canvas_size_allocate(GtkWidget* widget, GtkAllocation* allocation)
+{
+ g_return_if_fail(GANV_IS_CANVAS(widget));
+ g_return_if_fail(allocation != NULL);
+
+ if (GTK_WIDGET_CLASS(canvas_parent_class)->size_allocate) {
+ (*GTK_WIDGET_CLASS(canvas_parent_class)->size_allocate)(widget, allocation);
+ }
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ /* Recenter the view, if appropriate */
+
+ canvas->layout.hadjustment->page_size = allocation->width;
+ canvas->layout.hadjustment->page_increment = allocation->width / 2;
+
+ canvas->layout.vadjustment->page_size = allocation->height;
+ canvas->layout.vadjustment->page_increment = allocation->height / 2;
+
+ scroll_to(canvas,
+ canvas->layout.hadjustment->value,
+ canvas->layout.vadjustment->value);
+
+ g_signal_emit_by_name(canvas->layout.hadjustment, "changed");
+ g_signal_emit_by_name(canvas->layout.vadjustment, "changed");
+}
+
+/* Returns whether the item is an inferior of or is equal to the parent. */
+static gboolean
+is_descendant(GanvItem* item, GanvItem* parent)
+{
+ for (; item; item = item->impl->parent) {
+ if (item == parent) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+
+/* Emits an event for an item in the canvas, be it the current item, grabbed
+ * item, or focused item, as appropriate.
+ */
+int
+ganv_canvas_emit_event(GanvCanvas* canvas, GdkEvent* event)
+{
+ GdkEvent* ev;
+ gint finished;
+ GanvItem* item;
+ GanvItem* parent;
+ guint mask;
+
+ /* Perform checks for grabbed items */
+
+ if (canvas->impl->grabbed_item
+ && !is_descendant(canvas->impl->current_item, canvas->impl->grabbed_item)) {
+ /* I think this warning is annoying and I don't know what it's for
+ * so I'll disable it for now.
+ */
+ /* g_warning ("emit_event() returning FALSE!\n");*/
+ return FALSE;
+ }
+
+ if (canvas->impl->grabbed_item) {
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ mask = GDK_ENTER_NOTIFY_MASK;
+ break;
+
+ case GDK_LEAVE_NOTIFY:
+ mask = GDK_LEAVE_NOTIFY_MASK;
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ mask = GDK_POINTER_MOTION_MASK;
+ break;
+
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ mask = GDK_BUTTON_PRESS_MASK;
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ mask = GDK_BUTTON_RELEASE_MASK;
+ break;
+
+ case GDK_KEY_PRESS:
+ mask = GDK_KEY_PRESS_MASK;
+ break;
+
+ case GDK_KEY_RELEASE:
+ mask = GDK_KEY_RELEASE_MASK;
+ break;
+
+ case GDK_SCROLL:
+ mask = GDK_SCROLL_MASK;
+ break;
+
+ default:
+ mask = 0;
+ break;
+ }
+
+ if (!(mask & canvas->impl->grabbed_event_mask)) {
+ return FALSE;
+ }
+ }
+
+ /* Convert to world coordinates -- we have two cases because of diferent
+ * offsets of the fields in the event structures.
+ */
+
+ ev = gdk_event_copy(event);
+
+ switch (ev->type) {
+ case GDK_ENTER_NOTIFY:
+ case GDK_LEAVE_NOTIFY:
+ ganv_canvas_window_to_world(canvas,
+ ev->crossing.x, ev->crossing.y,
+ &ev->crossing.x, &ev->crossing.y);
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ case GDK_BUTTON_RELEASE:
+ ganv_canvas_window_to_world(canvas,
+ ev->motion.x, ev->motion.y,
+ &ev->motion.x, &ev->motion.y);
+ break;
+
+ default:
+ break;
+ }
+
+ /* Choose where we send the event */
+
+ item = canvas->impl->current_item;
+
+ if (canvas->impl->focused_item
+ && ((event->type == GDK_KEY_PRESS)
+ || (event->type == GDK_KEY_RELEASE)
+ || (event->type == GDK_FOCUS_CHANGE))) {
+ item = canvas->impl->focused_item;
+ }
+
+ /* The event is propagated up the hierarchy (for if someone connected to
+ * a group instead of a leaf event), and emission is stopped if a
+ * handler returns TRUE, just like for GtkWidget events.
+ */
+
+ finished = FALSE;
+
+ while (item && !finished) {
+ g_object_ref(G_OBJECT(item));
+
+ ganv_item_emit_event(item, ev, &finished);
+
+ parent = item->impl->parent;
+ g_object_unref(G_OBJECT(item));
+
+ item = parent;
+ }
+
+ gdk_event_free(ev);
+
+ return finished;
+}
+
+void
+ganv_canvas_set_need_repick(GanvCanvas* canvas)
+{
+ canvas->impl->need_repick = TRUE;
+}
+
+void
+ganv_canvas_forget_item(GanvCanvas* canvas, GanvItem* item)
+{
+ if (canvas->impl && item == canvas->impl->current_item) {
+ canvas->impl->current_item = NULL;
+ canvas->impl->need_repick = TRUE;
+ }
+
+ if (canvas->impl && item == canvas->impl->new_current_item) {
+ canvas->impl->new_current_item = NULL;
+ canvas->impl->need_repick = TRUE;
+ }
+
+ if (canvas->impl && item == canvas->impl->grabbed_item) {
+ canvas->impl->grabbed_item = NULL;
+ gdk_pointer_ungrab(GDK_CURRENT_TIME);
+ }
+
+ if (canvas->impl && item == canvas->impl->focused_item) {
+ canvas->impl->focused_item = NULL;
+ }
+}
+
+void
+ganv_canvas_grab_focus(GanvCanvas* canvas, GanvItem* item)
+{
+ g_return_if_fail(GANV_IS_ITEM(item));
+ g_return_if_fail(GTK_WIDGET_CAN_FOCUS(GTK_WIDGET(canvas)));
+
+ GanvItem* focused_item = canvas->impl->focused_item;
+ GdkEvent ev;
+
+ if (focused_item) {
+ ev.focus_change.type = GDK_FOCUS_CHANGE;
+ ev.focus_change.window = canvas->layout.bin_window;
+ ev.focus_change.send_event = FALSE;
+ ev.focus_change.in = FALSE;
+
+ ganv_canvas_emit_event(canvas, &ev);
+ }
+
+ canvas->impl->focused_item = item;
+ gtk_widget_grab_focus(GTK_WIDGET(canvas));
+
+ if (focused_item) {
+ ev.focus_change.type = GDK_FOCUS_CHANGE;
+ ev.focus_change.window = canvas->layout.bin_window;
+ ev.focus_change.send_event = FALSE;
+ ev.focus_change.in = TRUE;
+
+ ganv_canvas_emit_event(canvas, &ev);
+ }
+}
+
+/**
+ * ganv_canvas_grab_item:
+ * @item: A canvas item.
+ * @event_mask: Mask of events that will be sent to this item.
+ * @cursor: If non-NULL, the cursor that will be used while the grab is active.
+ * @etime: The timestamp required for grabbing the mouse, or GDK_CURRENT_TIME.
+ *
+ * Specifies that all events that match the specified event mask should be sent
+ * to the specified item, and also grabs the mouse by calling
+ * gdk_pointer_grab(). The event mask is also used when grabbing the pointer.
+ * If @cursor is not NULL, then that cursor is used while the grab is active.
+ * The @etime parameter is the timestamp required for grabbing the mouse.
+ *
+ * Return value: If an item was already grabbed, it returns %GDK_GRAB_ALREADY_GRABBED. If
+ * the specified item was hidden by calling ganv_item_hide(), then it
+ * returns %GDK_GRAB_NOT_VIEWABLE. Else, it returns the result of calling
+ * gdk_pointer_grab().
+ **/
+int
+ganv_canvas_grab_item(GanvItem* item, guint event_mask, GdkCursor* cursor, guint32 etime)
+{
+ g_return_val_if_fail(GANV_IS_ITEM(item), GDK_GRAB_NOT_VIEWABLE);
+ g_return_val_if_fail(GTK_WIDGET_MAPPED(item->impl->canvas), GDK_GRAB_NOT_VIEWABLE);
+
+ if (item->impl->canvas->impl->grabbed_item) {
+ return GDK_GRAB_ALREADY_GRABBED;
+ }
+
+ if (!(item->object.flags & GANV_ITEM_VISIBLE)) {
+ return GDK_GRAB_NOT_VIEWABLE;
+ }
+
+ int retval = gdk_pointer_grab(item->impl->canvas->layout.bin_window,
+ FALSE,
+ (GdkEventMask)event_mask,
+ NULL,
+ cursor,
+ etime);
+
+ if (retval != GDK_GRAB_SUCCESS) {
+ return retval;
+ }
+
+ item->impl->canvas->impl->grabbed_item = item;
+ item->impl->canvas->impl->grabbed_event_mask = event_mask;
+ item->impl->canvas->impl->current_item = item; /* So that events go to the grabbed item */
+
+ return retval;
+}
+
+/**
+ * ganv_canvas_ungrab_item:
+ * @item: A canvas item that holds a grab.
+ * @etime: The timestamp for ungrabbing the mouse.
+ *
+ * Ungrabs the item, which must have been grabbed in the canvas, and ungrabs the
+ * mouse.
+ **/
+void
+ganv_canvas_ungrab_item(GanvItem* item, guint32 etime)
+{
+ g_return_if_fail(GANV_IS_ITEM(item));
+
+ if (item->impl->canvas->impl->grabbed_item != item) {
+ return;
+ }
+
+ item->impl->canvas->impl->grabbed_item = NULL;
+
+ gdk_pointer_ungrab(etime);
+}
+
+void
+ganv_canvas_get_zoom_offsets(GanvCanvas* canvas, int* x, int* y)
+{
+ *x = canvas->impl->zoom_xofs;
+ *y = canvas->impl->zoom_yofs;
+}
+
+/* Re-picks the current item in the canvas, based on the event's coordinates.
+ * Also emits enter/leave events for items as appropriate.
+ */
+static int
+pick_current_item(GanvCanvas* canvas, GdkEvent* event)
+{
+ int retval = FALSE;
+
+ /* If a button is down, we'll perform enter and leave events on the
+ * current item, but not enter on any other item. This is more or less
+ * like X pointer grabbing for canvas items.
+ */
+ int button_down = canvas->impl->state & (GDK_BUTTON1_MASK
+ | GDK_BUTTON2_MASK
+ | GDK_BUTTON3_MASK
+ | GDK_BUTTON4_MASK
+ | GDK_BUTTON5_MASK);
+ if (!button_down) {
+ canvas->impl->left_grabbed_item = FALSE;
+ }
+
+ /* Save the event in the canvas. This is used to synthesize enter and
+ * leave events in case the current item changes. It is also used to
+ * re-pick the current item if the current one gets deleted. Also,
+ * synthesize an enter event.
+ */
+ if (event != &canvas->impl->pick_event) {
+ if ((event->type == GDK_MOTION_NOTIFY) || (event->type == GDK_BUTTON_RELEASE)) {
+ /* these fields have the same offsets in both types of events */
+
+ canvas->impl->pick_event.crossing.type = GDK_ENTER_NOTIFY;
+ canvas->impl->pick_event.crossing.window = event->motion.window;
+ canvas->impl->pick_event.crossing.send_event = event->motion.send_event;
+ canvas->impl->pick_event.crossing.subwindow = NULL;
+ canvas->impl->pick_event.crossing.x = event->motion.x;
+ canvas->impl->pick_event.crossing.y = event->motion.y;
+ canvas->impl->pick_event.crossing.mode = GDK_CROSSING_NORMAL;
+ canvas->impl->pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR;
+ canvas->impl->pick_event.crossing.focus = FALSE;
+ canvas->impl->pick_event.crossing.state = event->motion.state;
+
+ /* these fields don't have the same offsets in both types of events */
+
+ if (event->type == GDK_MOTION_NOTIFY) {
+ canvas->impl->pick_event.crossing.x_root = event->motion.x_root;
+ canvas->impl->pick_event.crossing.y_root = event->motion.y_root;
+ } else {
+ canvas->impl->pick_event.crossing.x_root = event->button.x_root;
+ canvas->impl->pick_event.crossing.y_root = event->button.y_root;
+ }
+ } else {
+ canvas->impl->pick_event = *event;
+ }
+ }
+
+ /* Don't do anything else if this is a recursive call */
+
+ if (canvas->impl->in_repick) {
+ return retval;
+ }
+
+ /* LeaveNotify means that there is no current item, so we don't look for one */
+
+ if (canvas->impl->pick_event.type != GDK_LEAVE_NOTIFY) {
+ /* these fields don't have the same offsets in both types of events */
+
+ double x, y;
+ if (canvas->impl->pick_event.type == GDK_ENTER_NOTIFY) {
+ x = canvas->impl->pick_event.crossing.x - canvas->impl->zoom_xofs;
+ y = canvas->impl->pick_event.crossing.y - canvas->impl->zoom_yofs;
+ } else {
+ x = canvas->impl->pick_event.motion.x - canvas->impl->zoom_xofs;
+ y = canvas->impl->pick_event.motion.y - canvas->impl->zoom_yofs;
+ }
+
+ /* world coords */
+
+ x = canvas->impl->scroll_x1 + x / canvas->impl->pixels_per_unit;
+ y = canvas->impl->scroll_y1 + y / canvas->impl->pixels_per_unit;
+
+ /* find the closest item */
+
+ if (canvas->impl->root->object.flags & GANV_ITEM_VISIBLE) {
+ GANV_ITEM_GET_CLASS(canvas->impl->root)->point(
+ canvas->impl->root,
+ x - canvas->impl->root->impl->x, y - canvas->impl->root->impl->y,
+ &canvas->impl->new_current_item);
+ } else {
+ canvas->impl->new_current_item = NULL;
+ }
+ } else {
+ canvas->impl->new_current_item = NULL;
+ }
+
+ if ((canvas->impl->new_current_item == canvas->impl->current_item) && !canvas->impl->left_grabbed_item) {
+ return retval; /* current item did not change */
+
+ }
+ /* Synthesize events for old and new current items */
+
+ if ((canvas->impl->new_current_item != canvas->impl->current_item)
+ && (canvas->impl->current_item != NULL)
+ && !canvas->impl->left_grabbed_item) {
+ GdkEvent new_event;
+
+ new_event = canvas->impl->pick_event;
+ new_event.type = GDK_LEAVE_NOTIFY;
+
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = NULL;
+ canvas->impl->in_repick = TRUE;
+ retval = ganv_canvas_emit_event(canvas, &new_event);
+ canvas->impl->in_repick = FALSE;
+ }
+
+ /* new_current_item may have been set to NULL during the call to ganv_canvas_emit_event() above */
+
+ if ((canvas->impl->new_current_item != canvas->impl->current_item) && button_down) {
+ canvas->impl->left_grabbed_item = TRUE;
+ return retval;
+ }
+
+ /* Handle the rest of cases */
+
+ canvas->impl->left_grabbed_item = FALSE;
+ canvas->impl->current_item = canvas->impl->new_current_item;
+
+ if (canvas->impl->current_item != NULL) {
+ GdkEvent new_event;
+
+ new_event = canvas->impl->pick_event;
+ new_event.type = GDK_ENTER_NOTIFY;
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = NULL;
+ retval = ganv_canvas_emit_event(canvas, &new_event);
+ }
+
+ return retval;
+}
+
+/* Button event handler for the canvas */
+static gint
+ganv_canvas_button(GtkWidget* widget, GdkEventButton* event)
+{
+ int mask;
+ int retval;
+
+ g_return_val_if_fail(GANV_IS_CANVAS(widget), FALSE);
+ g_return_val_if_fail(event != NULL, FALSE);
+
+ retval = FALSE;
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ /*
+ * dispatch normally regardless of the event's window if an item has
+ * has a pointer grab in effect
+ */
+ if (!canvas->impl->grabbed_item && ( event->window != canvas->layout.bin_window) ) {
+ return retval;
+ }
+
+ switch (event->button) {
+ case 1:
+ mask = GDK_BUTTON1_MASK;
+ break;
+ case 2:
+ mask = GDK_BUTTON2_MASK;
+ break;
+ case 3:
+ mask = GDK_BUTTON3_MASK;
+ break;
+ case 4:
+ mask = GDK_BUTTON4_MASK;
+ break;
+ case 5:
+ mask = GDK_BUTTON5_MASK;
+ break;
+ default:
+ mask = 0;
+ }
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ /* Pick the current item as if the button were not pressed, and
+ * then process the event.
+ */
+ canvas->impl->state = event->state;
+ pick_current_item(canvas, (GdkEvent*)event);
+ canvas->impl->state ^= mask;
+ retval = ganv_canvas_emit_event(canvas, (GdkEvent*)event);
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ /* Process the event as if the button were pressed, then repick
+ * after the button has been released
+ */
+ canvas->impl->state = event->state;
+ retval = ganv_canvas_emit_event(canvas, (GdkEvent*)event);
+ event->state ^= mask;
+ canvas->impl->state = event->state;
+ pick_current_item(canvas, (GdkEvent*)event);
+ event->state ^= mask;
+ break;
+
+ default:
+ g_assert_not_reached();
+ }
+
+ return retval;
+}
+
+/* Motion event handler for the canvas */
+static gint
+ganv_canvas_motion(GtkWidget* widget, GdkEventMotion* event)
+{
+ g_return_val_if_fail(GANV_IS_CANVAS(widget), FALSE);
+ g_return_val_if_fail(event != NULL, FALSE);
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ if (event->window != canvas->layout.bin_window) {
+ return FALSE;
+ }
+
+ canvas->impl->state = event->state;
+ pick_current_item(canvas, (GdkEvent*)event);
+ return ganv_canvas_emit_event(canvas, (GdkEvent*)event);
+}
+
+static gboolean
+ganv_canvas_scroll(GtkWidget* widget, GdkEventScroll* event)
+{
+ g_return_val_if_fail(GANV_IS_CANVAS(widget), FALSE);
+ g_return_val_if_fail(event != NULL, FALSE);
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ if (event->window != canvas->layout.bin_window) {
+ return FALSE;
+ }
+
+ canvas->impl->state = event->state;
+ pick_current_item(canvas, (GdkEvent*)event);
+ return ganv_canvas_emit_event(canvas, (GdkEvent*)event);
+}
+
+/* Key event handler for the canvas */
+static gboolean
+ganv_canvas_key(GtkWidget* widget, GdkEventKey* event)
+{
+ g_return_val_if_fail(GANV_IS_CANVAS(widget), FALSE);
+ g_return_val_if_fail(event != NULL, FALSE);
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ if (!ganv_canvas_emit_event(canvas, (GdkEvent*)event)) {
+ GtkWidgetClass* widget_class;
+
+ widget_class = GTK_WIDGET_CLASS(canvas_parent_class);
+
+ if (event->type == GDK_KEY_PRESS) {
+ if (widget_class->key_press_event) {
+ return (*widget_class->key_press_event)(widget, event);
+ }
+ } else if (event->type == GDK_KEY_RELEASE) {
+ if (widget_class->key_release_event) {
+ return (*widget_class->key_release_event)(widget, event);
+ }
+ } else {
+ g_assert_not_reached();
+ }
+
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+}
+
+/* Crossing event handler for the canvas */
+static gint
+ganv_canvas_crossing(GtkWidget* widget, GdkEventCrossing* event)
+{
+ g_return_val_if_fail(GANV_IS_CANVAS(widget), FALSE);
+ g_return_val_if_fail(event != NULL, FALSE);
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ if (event->window != canvas->layout.bin_window) {
+ return FALSE;
+ }
+
+ canvas->impl->state = event->state;
+ return pick_current_item(canvas, (GdkEvent*)event);
+}
+
+/* Focus in handler for the canvas */
+static gint
+ganv_canvas_focus_in(GtkWidget* widget, GdkEventFocus* event)
+{
+ GTK_WIDGET_SET_FLAGS(widget, GTK_HAS_FOCUS);
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ if (canvas->impl->focused_item) {
+ return ganv_canvas_emit_event(canvas, (GdkEvent*)event);
+ } else {
+ return FALSE;
+ }
+}
+
+/* Focus out handler for the canvas */
+static gint
+ganv_canvas_focus_out(GtkWidget* widget, GdkEventFocus* event)
+{
+ GTK_WIDGET_UNSET_FLAGS(widget, GTK_HAS_FOCUS);
+
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+
+ if (canvas->impl->focused_item) {
+ return ganv_canvas_emit_event(canvas, (GdkEvent*)event);
+ } else {
+ return FALSE;
+ }
+}
+
+#define REDRAW_QUANTUM_SIZE 512
+
+static void
+ganv_canvas_paint_rect(GanvCanvas* canvas, gint x0, gint y0, gint x1, gint y1)
+{
+ gint draw_x1, draw_y1;
+ gint draw_x2, draw_y2;
+ gint draw_width, draw_height;
+
+ g_return_if_fail(!canvas->impl->need_update);
+
+ draw_x1 = MAX(x0, canvas->layout.hadjustment->value - canvas->impl->zoom_xofs);
+ draw_y1 = MAX(y0, canvas->layout.vadjustment->value - canvas->impl->zoom_yofs);
+ draw_x2 = MIN(draw_x1 + GTK_WIDGET(canvas)->allocation.width, x1);
+ draw_y2 = MIN(draw_y1 + GTK_WIDGET(canvas)->allocation.height, y1);
+
+ draw_width = draw_x2 - draw_x1;
+ draw_height = draw_y2 - draw_y1;
+
+ if ((draw_width < 1) || (draw_height < 1)) {
+ return;
+ }
+
+ canvas->impl->redraw_x1 = draw_x1;
+ canvas->impl->redraw_y1 = draw_y1;
+ canvas->impl->redraw_x2 = draw_x2;
+ canvas->impl->redraw_y2 = draw_y2;
+ canvas->impl->draw_xofs = draw_x1;
+ canvas->impl->draw_yofs = draw_y1;
+
+ cairo_t* cr = gdk_cairo_create(canvas->layout.bin_window);
+
+ double win_x, win_y;
+ ganv_canvas_window_to_world(canvas, 0, 0, &win_x, &win_y);
+ cairo_translate(cr, -win_x, -win_y);
+ cairo_scale(cr, canvas->impl->pixels_per_unit, canvas->impl->pixels_per_unit);
+
+ if (canvas->impl->root->object.flags & GANV_ITEM_VISIBLE) {
+ double wx1, wy1, ww, wh;
+ ganv_canvas_c2w(canvas, draw_x1, draw_y1, &wx1, &wy1);
+ ganv_canvas_c2w(canvas, draw_width, draw_height, &ww, &wh);
+
+ // Draw background
+ double r, g, b, a;
+ color_to_rgba(DEFAULT_BACKGROUND_COLOR, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_rectangle(cr, wx1, wy1, ww, wh);
+ cairo_fill(cr);
+
+ // Draw root group
+ (*GANV_ITEM_GET_CLASS(canvas->impl->root)->draw)(
+ canvas->impl->root, cr,
+ wx1, wy1, ww, wh);
+ }
+
+ cairo_destroy(cr);
+}
+
+/* Expose handler for the canvas */
+static gint
+ganv_canvas_expose(GtkWidget* widget, GdkEventExpose* event)
+{
+ GanvCanvas* canvas = GANV_CANVAS(widget);
+ if (!GTK_WIDGET_DRAWABLE(widget) ||
+ (event->window != canvas->layout.bin_window)) {
+ return FALSE;
+ }
+
+ /* Find a single bounding rectangle for all rectangles in the region.
+ Since drawing the root group is O(n) and thus very expensive for large
+ canvases, it's much faster to do a single paint than many, even though
+ more area may be painted that way. With a better group implementation,
+ it would likely be better to paint each changed rectangle separately. */
+ GdkRectangle clip;
+ gdk_region_get_clipbox(event->region, &clip);
+
+ const int x2 = clip.x + clip.width;
+ const int y2 = clip.y + clip.height;
+
+ if (canvas->impl->need_update || canvas->impl->need_redraw) {
+ /* Update or drawing is scheduled, so just mark exposed area as dirty */
+ ganv_canvas_request_redraw_c(canvas, clip.x, clip.y, x2, y2);
+ } else {
+ /* No pending updates, draw exposed area immediately */
+ ganv_canvas_paint_rect(canvas, clip.x, clip.y, x2, y2);
+
+ /* And call expose on parent container class */
+ if (GTK_WIDGET_CLASS(canvas_parent_class)->expose_event) {
+ (*GTK_WIDGET_CLASS(canvas_parent_class)->expose_event)(
+ widget, event);
+ }
+ }
+
+ return FALSE;
+}
+
+/* Repaints the areas in the canvas that need it */
+static void
+paint(GanvCanvas* canvas)
+{
+ for (GSList* l = canvas->impl->redraw_region; l; l = l->next) {
+ IRect* rect = (IRect*)l->data;
+
+ const GdkRectangle gdkrect = {
+ rect->x + canvas->impl->zoom_xofs,
+ rect->y + canvas->impl->zoom_yofs,
+ rect->width,
+ rect->height
+ };
+
+ gdk_window_invalidate_rect(canvas->layout.bin_window, &gdkrect, FALSE);
+ g_free(rect);
+ }
+
+ g_slist_free(canvas->impl->redraw_region);
+ canvas->impl->redraw_region = NULL;
+ canvas->impl->need_redraw = FALSE;
+
+ canvas->impl->redraw_x1 = 0;
+ canvas->impl->redraw_y1 = 0;
+ canvas->impl->redraw_x2 = 0;
+ canvas->impl->redraw_y2 = 0;
+}
+
+static void
+do_update(GanvCanvas* canvas)
+{
+ /* Cause the update if necessary */
+
+update_again:
+ if (canvas->impl->need_update) {
+ ganv_item_invoke_update(canvas->impl->root, 0);
+
+ canvas->impl->need_update = FALSE;
+ }
+
+ /* Pick new current item */
+
+ while (canvas->impl->need_repick) {
+ canvas->impl->need_repick = FALSE;
+ pick_current_item(canvas, &canvas->impl->pick_event);
+ }
+
+ /* it is possible that during picking we emitted an event in which
+ the user then called some function which then requested update
+ of something. Without this we'd be left in a state where
+ need_update would have been left TRUE and the canvas would have
+ been left unpainted. */
+ if (canvas->impl->need_update) {
+ goto update_again;
+ }
+
+ /* Paint if able to */
+
+ if (GTK_WIDGET_DRAWABLE(canvas) && canvas->impl->need_redraw) {
+ paint(canvas);
+ }
+}
+
+/* Idle handler for the canvas. It deals with pending updates and redraws. */
+static gboolean
+idle_handler(gpointer data)
+{
+ GDK_THREADS_ENTER();
+
+ GanvCanvas* canvas = GANV_CANVAS(data);
+
+ do_update(canvas);
+
+ /* Reset idle id */
+ canvas->impl->idle_id = 0;
+
+ GDK_THREADS_LEAVE();
+
+ return FALSE;
+}
+
+/* Convenience function to add an idle handler to a canvas */
+static void
+add_idle(GanvCanvas* canvas)
+{
+ g_assert(canvas->impl->need_update || canvas->impl->need_redraw);
+
+ if (!canvas->impl->idle_id) {
+ canvas->impl->idle_id = g_idle_add_full(CANVAS_IDLE_PRIORITY,
+ idle_handler,
+ canvas,
+ NULL);
+ }
+
+ /* canvas->idle_id = gtk_idle_add (idle_handler, canvas); */
+}
+
+GanvItem*
+ganv_canvas_root(GanvCanvas* canvas)
+{
+ g_return_val_if_fail(GANV_IS_CANVAS(canvas), NULL);
+
+ return canvas->impl->root;
+}
+
+void
+ganv_canvas_set_scroll_region(GanvCanvas* canvas,
+ double x1, double y1, double x2, double y2)
+{
+ double wxofs, wyofs;
+ int xofs, yofs;
+
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ /*
+ * Set the new scrolling region. If possible, do not move the visible contents of the
+ * canvas.
+ */
+
+ ganv_canvas_c2w(canvas,
+ GTK_LAYOUT(canvas)->hadjustment->value + canvas->impl->zoom_xofs,
+ GTK_LAYOUT(canvas)->vadjustment->value + canvas->impl->zoom_yofs,
+ /*canvas->impl->zoom_xofs,
+ canvas->impl->zoom_yofs,*/
+ &wxofs, &wyofs);
+
+ canvas->impl->scroll_x1 = x1;
+ canvas->impl->scroll_y1 = y1;
+ canvas->impl->scroll_x2 = x2;
+ canvas->impl->scroll_y2 = y2;
+
+ ganv_canvas_w2c(canvas, wxofs, wyofs, &xofs, &yofs);
+
+ scroll_to(canvas, xofs, yofs);
+
+ canvas->impl->need_repick = TRUE;
+#if 0
+ /* todo: should be requesting update */
+ (*GANV_ITEM_CLASS(canvas->impl->root->object.klass)->update)(
+ canvas->impl->root, NULL, NULL, 0);
+#endif
+}
+
+void
+ganv_canvas_get_scroll_region(GanvCanvas* canvas,
+ double* x1, double* y1, double* x2, double* y2)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ if (x1) {
+ *x1 = canvas->impl->scroll_x1;
+ }
+
+ if (y1) {
+ *y1 = canvas->impl->scroll_y1;
+ }
+
+ if (x2) {
+ *x2 = canvas->impl->scroll_x2;
+ }
+
+ if (y2) {
+ *y2 = canvas->impl->scroll_y2;
+ }
+}
+
+void
+ganv_canvas_set_center_scroll_region(GanvCanvas* canvas, gboolean center_scroll_region)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ canvas->impl->center_scroll_region = center_scroll_region != 0;
+
+ scroll_to(canvas,
+ canvas->layout.hadjustment->value,
+ canvas->layout.vadjustment->value);
+}
+
+gboolean
+ganv_canvas_get_center_scroll_region(const GanvCanvas* canvas)
+{
+ g_return_val_if_fail(GANV_IS_CANVAS(canvas), FALSE);
+
+ return canvas->impl->center_scroll_region ? TRUE : FALSE;
+}
+
+void
+ganv_canvas_scroll_to(GanvCanvas* canvas, int cx, int cy)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ scroll_to(canvas, cx, cy);
+}
+
+void
+ganv_canvas_get_scroll_offsets(const GanvCanvas* canvas, int* cx, int* cy)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ if (cx) {
+ *cx = canvas->layout.hadjustment->value;
+ }
+
+ if (cy) {
+ *cy = canvas->layout.vadjustment->value;
+ }
+}
+
+GanvItem*
+ganv_canvas_get_item_at(GanvCanvas* canvas, double x, double y)
+{
+ g_return_val_if_fail(GANV_IS_CANVAS(canvas), NULL);
+
+ GanvItem* item = NULL;
+ double dist = GANV_ITEM_GET_CLASS(canvas->impl->root)->point(
+ canvas->impl->root,
+ x - canvas->impl->root->impl->x,
+ y - canvas->impl->root->impl->y,
+ &item);
+ if ((int)(dist * canvas->impl->pixels_per_unit + 0.5) <= GANV_CLOSE_ENOUGH) {
+ return item;
+ } else {
+ return NULL;
+ }
+}
+
+void
+ganv_canvas_request_update(GanvCanvas* canvas)
+{
+ if (canvas->impl->need_update) {
+ return;
+ }
+
+ canvas->impl->need_update = TRUE;
+ if (GTK_WIDGET_MAPPED((GtkWidget*)canvas)) {
+ add_idle(canvas);
+ }
+}
+
+static inline gboolean
+rect_overlaps(const IRect* a, const IRect* b)
+{
+ if ((a->x > b->x + b->width) ||
+ (a->y > b->y + b->height) ||
+ (a->x + a->width < b->x) ||
+ (a->y + a->height < b->y)) {
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static inline gboolean
+rect_is_visible(GanvCanvas* canvas, const IRect* r)
+{
+ const IRect rect = {
+ (int)(canvas->layout.hadjustment->value - canvas->impl->zoom_xofs),
+ (int)(canvas->layout.vadjustment->value - canvas->impl->zoom_yofs),
+ GTK_WIDGET(canvas)->allocation.width,
+ GTK_WIDGET(canvas)->allocation.height
+ };
+
+ return rect_overlaps(&rect, r);
+}
+
+void
+ganv_canvas_request_redraw_c(GanvCanvas* canvas,
+ int x1, int y1, int x2, int y2)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ if (!GTK_WIDGET_DRAWABLE(canvas) || (x1 >= x2) || (y1 >= y2)) {
+ return;
+ }
+
+ const IRect rect = { x1, y1, x2 - x1, y2 - y1 };
+
+ if (!rect_is_visible(canvas, &rect)) {
+ return;
+ }
+
+ IRect* r = (IRect*)g_malloc(sizeof(IRect));
+ *r = rect;
+
+ canvas->impl->redraw_region = g_slist_prepend(canvas->impl->redraw_region, r);
+ canvas->impl->need_redraw = TRUE;
+
+ if (canvas->impl->idle_id == 0) {
+ add_idle(canvas);
+ }
+}
+
+/* Request a redraw of the specified rectangle in world coordinates */
+void
+ganv_canvas_request_redraw_w(GanvCanvas* canvas,
+ double x1, double y1, double x2, double y2)
+{
+ int cx1, cx2, cy1, cy2;
+ ganv_canvas_w2c(canvas, x1, y1, &cx1, &cy1);
+ ganv_canvas_w2c(canvas, x2, y2, &cx2, &cy2);
+ ganv_canvas_request_redraw_c(canvas, cx1, cy1, cx2, cy2);
+}
+
+void
+ganv_canvas_w2c_affine(GanvCanvas* canvas, cairo_matrix_t* matrix)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+ g_return_if_fail(matrix != NULL);
+
+ cairo_matrix_init_translate(matrix,
+ -canvas->impl->scroll_x1,
+ -canvas->impl->scroll_y1);
+
+ cairo_matrix_scale(matrix,
+ canvas->impl->pixels_per_unit,
+ canvas->impl->pixels_per_unit);
+}
+
+void
+ganv_canvas_w2c(GanvCanvas* canvas, double wx, double wy, int* cx, int* cy)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ cairo_matrix_t matrix;
+ ganv_canvas_w2c_affine(canvas, &matrix);
+
+ cairo_matrix_transform_point(&matrix, &wx, &wy);
+
+ if (cx) {
+ *cx = floor(wx + 0.5);
+ }
+ if (cy) {
+ *cy = floor(wy + 0.5);
+ }
+}
+
+void
+ganv_canvas_w2c_d(GanvCanvas* canvas, double wx, double wy, double* cx, double* cy)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ cairo_matrix_t matrix;
+ ganv_canvas_w2c_affine(canvas, &matrix);
+
+ cairo_matrix_transform_point(&matrix, &wx, &wy);
+
+ if (cx) {
+ *cx = wx;
+ }
+ if (cy) {
+ *cy = wy;
+ }
+}
+
+void
+ganv_canvas_c2w(GanvCanvas* canvas, int cx, int cy, double* wx, double* wy)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ cairo_matrix_t matrix;
+ ganv_canvas_w2c_affine(canvas, &matrix);
+ cairo_matrix_invert(&matrix);
+
+ double x = cx;
+ double y = cy;
+ cairo_matrix_transform_point(&matrix, &x, &y);
+
+ if (wx) {
+ *wx = x;
+ }
+ if (wy) {
+ *wy = y;
+ }
+}
+
+void
+ganv_canvas_window_to_world(GanvCanvas* canvas, double winx, double winy,
+ double* worldx, double* worldy)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ if (worldx) {
+ *worldx = canvas->impl->scroll_x1 + ((winx - canvas->impl->zoom_xofs)
+ / canvas->impl->pixels_per_unit);
+ }
+
+ if (worldy) {
+ *worldy = canvas->impl->scroll_y1 + ((winy - canvas->impl->zoom_yofs)
+ / canvas->impl->pixels_per_unit);
+ }
+}
+
+void
+ganv_canvas_world_to_window(GanvCanvas* canvas, double worldx, double worldy,
+ double* winx, double* winy)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ if (winx) {
+ *winx = (canvas->impl->pixels_per_unit) * (worldx - canvas->impl->scroll_x1) + canvas->impl->zoom_xofs;
+ }
+
+ if (winy) {
+ *winy = (canvas->impl->pixels_per_unit) * (worldy - canvas->impl->scroll_y1) + canvas->impl->zoom_yofs;
+ }
+}
+
+void
+ganv_canvas_set_port_order(GanvCanvas* canvas,
+ GanvPortOrderFunc port_cmp,
+ void* data)
+{
+ g_return_if_fail(GANV_IS_CANVAS(canvas));
+
+ canvas->impl->_port_order.port_cmp = port_cmp;
+ canvas->impl->_port_order.data = data;
+}
+
+PortOrderCtx
+ganv_canvas_get_port_order(GanvCanvas* canvas)
+{
+ return canvas->impl->_port_order;
+}
+
+gboolean
+ganv_canvas_exporting(GanvCanvas* canvas)
+{
+ return canvas->impl->exporting;
+}
+
+} // extern "C"
diff --git a/src/Port.cpp b/src/Port.cpp
new file mode 100644
index 0000000..3f0d02b
--- /dev/null
+++ b/src/Port.cpp
@@ -0,0 +1,57 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2015 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string>
+
+#include <glib.h>
+
+#include "ganv/Canvas.hpp"
+#include "ganv/Module.hpp"
+#include "ganv/Port.hpp"
+
+#include "./color.h"
+
+namespace Ganv {
+
+static void
+on_value_changed(GanvPort* port, double value, void* portmm)
+{
+ ((Port*)portmm)->signal_value_changed.emit(value);
+}
+
+/* Construct a Port on an existing module. */
+Port::Port(Module& module,
+ const std::string& name,
+ bool is_input,
+ uint32_t color)
+ : Box(module.canvas(),
+ GANV_BOX(ganv_port_new(module.gobj(), is_input,
+ "fill-color", color,
+ "border-color", PORT_BORDER_COLOR(color),
+ "border-width", 2.0,
+ "label", name.c_str(),
+ NULL)))
+{
+ g_signal_connect(gobj(), "value-changed",
+ G_CALLBACK(on_value_changed), this);
+}
+
+Module*
+Port::get_module() const
+{
+ return Glib::wrap(ganv_port_get_module(gobj()));
+}
+
+} // namespace Ganv
diff --git a/src/boilerplate.h b/src/boilerplate.h
new file mode 100644
index 0000000..a419711
--- /dev/null
+++ b/src/boilerplate.h
@@ -0,0 +1,43 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2016 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+typedef gpointer gobject;
+
+/**
+ A case in a switch statement in a set_properties implementation.
+ @prop: Property enumeration ID.
+ @type: Name of the value type, e.g. uint for guint.
+ @field: Field to set to the new value.
+*/
+#define SET_CASE(prop, type, field) \
+ case PROP_##prop: { \
+ const g##type tmp = g_value_get_##type(value); \
+ if ((field) != tmp) { \
+ (field) = tmp; \
+ ganv_item_request_update(GANV_ITEM(object)); \
+ } \
+ break; \
+ }
+
+/**
+ A case in a switch statement in a get_properties implementation.
+ @prop: Property enumeration ID.
+ @type: Name of the value type, e.g. uint for guint.
+ @field: Field to set to the new value.
+*/
+#define GET_CASE(prop, type, field) \
+ case PROP_##prop: \
+ g_value_set_##type(value, field); \
+ break;
diff --git a/src/box.c b/src/box.c
new file mode 100644
index 0000000..e7ecac8
--- /dev/null
+++ b/src/box.c
@@ -0,0 +1,561 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2015 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+#include <string.h>
+
+#include <cairo.h>
+
+#include "ganv/box.h"
+
+#include "./boilerplate.h"
+#include "./color.h"
+#include "./gettext.h"
+#include "./ganv-private.h"
+
+static const double STACKED_OFFSET = 4.0;
+
+G_DEFINE_TYPE_WITH_CODE(GanvBox, ganv_box, GANV_TYPE_NODE,
+ G_ADD_PRIVATE(GanvBox))
+
+static GanvNodeClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_X1,
+ PROP_Y1,
+ PROP_X2,
+ PROP_Y2,
+ PROP_RADIUS_TL,
+ PROP_RADIUS_TR,
+ PROP_RADIUS_BR,
+ PROP_RADIUS_BL,
+ PROP_STACKED,
+ PROP_BEVELED
+};
+
+static void
+ganv_box_init(GanvBox* box)
+{
+ box->impl = ganv_box_get_instance_private(box);
+
+ memset(&box->impl->coords, '\0', sizeof(GanvBoxCoords));
+
+ box->impl->coords.border_width = GANV_NODE(box)->impl->border_width;
+ box->impl->old_coords = box->impl->coords;
+ box->impl->radius_tl = 0.0;
+ box->impl->radius_tr = 0.0;
+ box->impl->radius_br = 0.0;
+ box->impl->radius_bl = 0.0;
+ box->impl->beveled = FALSE;
+}
+
+static void
+ganv_box_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_BOX(object));
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+}
+
+static void
+ganv_box_set_property(GObject* object,
+ guint prop_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_BOX(object));
+
+ GanvBox* box = GANV_BOX(object);
+ GanvBoxPrivate* impl = box->impl;
+ GanvBoxCoords* coords = &impl->coords;
+
+ switch (prop_id) {
+ SET_CASE(X1, double, coords->x1);
+ SET_CASE(Y1, double, coords->y1);
+ SET_CASE(X2, double, coords->x2);
+ SET_CASE(Y2, double, coords->y2);
+ SET_CASE(RADIUS_TL, double, impl->radius_tl);
+ SET_CASE(RADIUS_TR, double, impl->radius_tr);
+ SET_CASE(RADIUS_BR, double, impl->radius_br);
+ SET_CASE(RADIUS_BL, double, impl->radius_bl);
+ SET_CASE(STACKED, boolean, coords->stacked);
+ SET_CASE(BEVELED, boolean, impl->beveled);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_box_get_property(GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_BOX(object));
+
+ GanvBox* box = GANV_BOX(object);
+ GanvBoxPrivate* impl = box->impl;
+ GanvBoxCoords* coords = &impl->coords;
+
+ switch (prop_id) {
+ GET_CASE(X1, double, coords->x1);
+ GET_CASE(X2, double, coords->x2);
+ GET_CASE(Y1, double, coords->y1);
+ GET_CASE(Y2, double, coords->y2);
+ GET_CASE(RADIUS_TL, double, impl->radius_tl);
+ GET_CASE(RADIUS_TR, double, impl->radius_tr);
+ GET_CASE(RADIUS_BR, double, impl->radius_br);
+ GET_CASE(RADIUS_BL, double, impl->radius_bl);
+ GET_CASE(STACKED, boolean, impl->coords.stacked);
+ GET_CASE(BEVELED, boolean, impl->beveled);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_box_bounds_item(const GanvBoxCoords* coords,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ *x1 = coords->x1 - coords->border_width;
+ *y1 = coords->y1 - coords->border_width;
+ *x2 = coords->x2 + coords->border_width + (coords->stacked * STACKED_OFFSET);
+ *y2 = coords->y2 + coords->border_width + (coords->stacked * STACKED_OFFSET);
+}
+
+void
+ganv_box_request_redraw(GanvItem* item,
+ const GanvBoxCoords* coords,
+ gboolean world)
+{
+ double x1, y1, x2, y2;
+ ganv_box_bounds_item(coords, &x1, &y1, &x2, &y2);
+
+ if (!world) {
+ // Convert from item-relative coordinates to world coordinates
+ ganv_item_i2w_pair(item, &x1, &y1, &x2, &y2);
+ }
+
+ ganv_canvas_request_redraw_w(item->impl->canvas, x1, y1, x2, y2);
+}
+
+static void
+coords_i2w(GanvItem* item, GanvBoxCoords* coords)
+{
+ ganv_item_i2w_pair(item, &coords->x1, &coords->y1, &coords->x2, &coords->y2);
+}
+
+static void
+ganv_box_bounds(GanvItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ // Note this will not be correct if children are outside the box bounds
+ GanvBox* box = (GanvBox*)item;
+ ganv_box_bounds_item(&box->impl->coords, x1, y1, x2, y2);
+}
+
+static void
+ganv_box_update(GanvItem* item, int flags)
+{
+ GanvBox* box = GANV_BOX(item);
+ GanvBoxPrivate* impl = box->impl;
+ impl->coords.border_width = box->node.impl->border_width;
+
+ // Request redraw of old location
+ ganv_box_request_redraw(item, &impl->old_coords, TRUE);
+
+ // Store old coordinates in world relative coordinates in case the
+ // group we are in moves between now and the next update
+ impl->old_coords = impl->coords;
+ coords_i2w(item, &impl->old_coords);
+
+ // Call parent class update method, resizing if necessary
+ GANV_ITEM_CLASS(parent_class)->update(item, flags);
+ ganv_box_normalize(box);
+
+ // Update world-relative bounding box
+ ganv_box_bounds(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
+ ganv_item_i2w_pair(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
+
+ // Request redraw of new location
+ ganv_box_request_redraw(item, &impl->coords, FALSE);
+}
+
+void
+ganv_box_path(GanvBox* box,
+ cairo_t* cr, double x1, double y1, double x2, double y2,
+ double dr)
+{
+ static const double degrees = G_PI / 180.0;
+
+ GanvBoxPrivate* impl = box->impl;
+
+ if (impl->radius_tl == 0.0 && impl->radius_tr == 0.0
+ && impl->radius_br == 0.0 && impl->radius_bl == 0.0) {
+ // Simple rectangle
+ cairo_rectangle(cr, x1, y1, x2 - x1, y2 - y1);
+ } else if (impl->beveled) {
+ // Beveled rectangle
+ cairo_new_sub_path(cr);
+ cairo_move_to(cr, x1 + impl->radius_tl, y1);
+ cairo_line_to(cr, x2 - impl->radius_tr, y1);
+ cairo_line_to(cr, x2, y1 + impl->radius_tr);
+ cairo_line_to(cr, x2, y2 - impl->radius_br);
+ cairo_line_to(cr, x2 - impl->radius_br, y2);
+ cairo_line_to(cr, x1 + impl->radius_bl, y2);
+ cairo_line_to(cr, x1, y2 - impl->radius_bl);
+ cairo_line_to(cr, x1, y2 - impl->radius_bl);
+ cairo_line_to(cr, x1, y1 + impl->radius_tl);
+ cairo_close_path(cr);
+ } else {
+ // Rounded rectangle
+ cairo_new_sub_path(cr);
+ cairo_arc(cr,
+ x2 - impl->radius_tr - dr,
+ y1 + impl->radius_tr + dr,
+ impl->radius_tr + dr, -90 * degrees, 0 * degrees);
+ cairo_arc(cr,
+ x2 - impl->radius_br - dr, y2 - impl->radius_br - dr,
+ impl->radius_br + dr, 0 * degrees, 90 * degrees);
+ cairo_arc(cr,
+ x1 + impl->radius_bl + dr, y2 - impl->radius_bl - dr,
+ impl->radius_bl + dr, 90 * degrees, 180 * degrees);
+ cairo_arc(cr,
+ x1 + impl->radius_tl + dr, y1 + impl->radius_tl + dr,
+ impl->radius_tl + dr, 180 * degrees, 270 * degrees);
+ cairo_close_path(cr);
+ }
+}
+
+static void
+ganv_box_draw(GanvItem* item,
+ cairo_t* cr, double cx, double cy, double cw, double ch)
+{
+ GanvBox* box = GANV_BOX(item);
+ GanvBoxPrivate* impl = box->impl;
+
+ double x1 = impl->coords.x1;
+ double y1 = impl->coords.y1;
+ double x2 = impl->coords.x2;
+ double y2 = impl->coords.y2;
+ ganv_item_i2w_pair(item, &x1, &y1, &x2, &y2);
+
+ double dash_length, border_color, fill_color;
+ ganv_node_get_draw_properties(
+ &box->node, &dash_length, &border_color, &fill_color);
+
+ double r, g, b, a;
+
+ for (int i = (impl->coords.stacked ? 1 : 0); i >= 0; --i) {
+ const double x = 0.0 - (STACKED_OFFSET * i);
+ const double y = 0.0 - (STACKED_OFFSET * i);
+
+ // Trace basic box path
+ ganv_box_path(box, cr, x1 - x, y1 - y, x2 - x, y2 - y, 0.0);
+
+ // Fill
+ color_to_rgba(fill_color, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+
+ // Border
+ if (impl->coords.border_width > 0.0) {
+ cairo_fill_preserve(cr);
+ color_to_rgba(border_color, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_set_line_width(cr, impl->coords.border_width);
+ if (dash_length > 0) {
+ cairo_set_dash(cr, &dash_length, 1, box->node.impl->dash_offset);
+ } else {
+ cairo_set_dash(cr, &dash_length, 0, 0);
+ }
+ cairo_stroke(cr);
+ } else {
+ cairo_fill(cr);
+ }
+ }
+
+ GanvItemClass* item_class = GANV_ITEM_CLASS(parent_class);
+ item_class->draw(item, cr, cx, cy, cw, ch);
+}
+
+static double
+ganv_box_point(GanvItem* item, double x, double y, GanvItem** actual_item)
+{
+ GanvBox* box = GANV_BOX(item);
+ GanvBoxPrivate* impl = box->impl;
+
+ *actual_item = NULL;
+
+ double x1, y1, x2, y2;
+ ganv_box_bounds_item(&impl->coords, &x1, &y1, &x2, &y2);
+
+ // Point is inside the box (distance 0)
+ if ((x >= x1) && (y >= y1) && (x <= x2) && (y <= y2)) {
+ *actual_item = item;
+ return 0.0;
+ }
+
+ // Point is outside the box
+ double dx = 0.0;
+ double dy = 0.0;
+
+ // Find horizontal distance to nearest edge
+ if (x < x1) {
+ dx = x1 - x;
+ } else if (x > x2) {
+ dx = x - x2;
+ }
+
+ // Find vertical distance to nearest edge
+ if (y < y1) {
+ dy = y1 - y;
+ } else if (y > y2) {
+ dy = y - y2;
+ }
+
+ return sqrt((dx * dx) + (dy * dy));
+}
+
+static gboolean
+ganv_box_is_within(const GanvNode* self,
+ double x1,
+ double y1,
+ double x2,
+ double y2)
+{
+ double bx1, by1, bx2, by2;
+ g_object_get(G_OBJECT(self),
+ "x1", &bx1,
+ "y1", &by1,
+ "x2", &bx2,
+ "y2", &by2,
+ NULL);
+
+ ganv_item_i2w_pair(GANV_ITEM(self), &bx1, &by1, &bx2, &by2);
+
+ return ( bx1 >= x1
+ && by2 >= y1
+ && bx2 <= x2
+ && by2 <= y2);
+}
+
+static void
+ganv_box_default_set_width(GanvBox* box, double width)
+{
+ box->impl->coords.x2 = ganv_box_get_x1(box) + width;
+ ganv_item_request_update(GANV_ITEM(box));
+}
+
+static void
+ganv_box_default_set_height(GanvBox* box, double height)
+{
+ box->impl->coords.y2 = ganv_box_get_y1(box) + height;
+ ganv_item_request_update(GANV_ITEM(box));
+}
+
+static void
+ganv_box_class_init(GanvBoxClass* klass)
+{
+ GObjectClass* gobject_class = (GObjectClass*)klass;
+ GtkObjectClass* object_class = (GtkObjectClass*)klass;
+ GanvItemClass* item_class = (GanvItemClass*)klass;
+ GanvNodeClass* node_class = (GanvNodeClass*)klass;
+
+ parent_class = GANV_NODE_CLASS(g_type_class_peek_parent(klass));
+
+ gobject_class->set_property = ganv_box_set_property;
+ gobject_class->get_property = ganv_box_get_property;
+
+ g_object_class_install_property(
+ gobject_class, PROP_X1, g_param_spec_double(
+ "x1",
+ _("x1"),
+ _("Top left x coordinate."),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_Y1, g_param_spec_double(
+ "y1",
+ _("y1"),
+ _("Top left y coordinate."),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_X2, g_param_spec_double(
+ "x2",
+ _("x2"),
+ _("Bottom right x coordinate."),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_Y2, g_param_spec_double(
+ "y2",
+ _("y2"),
+ _("Bottom right y coordinate."),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_RADIUS_TL, g_param_spec_double(
+ "radius-tl",
+ _("Top left radius"),
+ _("The radius of the top left corner."),
+ 0.0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_RADIUS_TR, g_param_spec_double(
+ "radius-tr",
+ _("Top right radius"),
+ _("The radius of the top right corner."),
+ 0.0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_RADIUS_BR, g_param_spec_double(
+ "radius-br",
+ _("Bottom right radius"),
+ _("The radius of the bottom right corner."),
+ 0.0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_RADIUS_BL, g_param_spec_double(
+ "radius-bl",
+ _("Bottom left radius"),
+ _("The radius of the bottom left corner."),
+ 0.0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_STACKED, g_param_spec_boolean(
+ "stacked",
+ _("Stacked"),
+ _("Show box with a stacked appearance."),
+ FALSE,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_BEVELED, g_param_spec_boolean(
+ "beveled",
+ _("Beveled"),
+ _("Show radiused corners with a sharp bevel."),
+ FALSE,
+ G_PARAM_READWRITE));
+
+ object_class->destroy = ganv_box_destroy;
+
+ item_class->update = ganv_box_update;
+ item_class->bounds = ganv_box_bounds;
+ item_class->point = ganv_box_point;
+ item_class->draw = ganv_box_draw;
+
+ node_class->is_within = ganv_box_is_within;
+
+ klass->set_width = ganv_box_default_set_width;
+ klass->set_height = ganv_box_default_set_height;
+}
+
+void
+ganv_box_normalize(GanvBox* box)
+{
+ if (box->impl->coords.x2 < box->impl->coords.x1) {
+ const double tmp = box->impl->coords.x1;
+ box->impl->coords.x1 = box->impl->coords.x2;
+ box->impl->coords.x2 = tmp;
+ }
+ if (box->impl->coords.y2 < box->impl->coords.y1) {
+ const double tmp = box->impl->coords.y1;
+ box->impl->coords.y1 = box->impl->coords.y2;
+ box->impl->coords.y2 = tmp;
+ }
+}
+
+double
+ganv_box_get_x1(const GanvBox* box)
+{
+ return box->impl->coords.x1;
+}
+
+double
+ganv_box_get_y1(const GanvBox* box)
+{
+ return box->impl->coords.y1;
+}
+
+double
+ganv_box_get_x2(const GanvBox* box)
+{
+ return box->impl->coords.x2;
+}
+
+double
+ganv_box_get_y2(const GanvBox* box)
+{
+ return box->impl->coords.y2;
+}
+
+double
+ganv_box_get_width(const GanvBox* box)
+{
+ return box->impl->coords.x2 - box->impl->coords.x1;
+}
+
+void
+ganv_box_set_width(GanvBox* box,
+ double width)
+{
+ GANV_BOX_GET_CLASS(box)->set_width(box, width);
+}
+
+double
+ganv_box_get_height(const GanvBox* box)
+{
+ return box->impl->coords.y2 - box->impl->coords.y1;
+}
+
+void
+ganv_box_set_height(GanvBox* box,
+ double height)
+{
+ GANV_BOX_GET_CLASS(box)->set_height(box, height);
+}
+
+double
+ganv_box_get_border_width(const GanvBox* box)
+{
+ return box->impl->coords.border_width;
+}
diff --git a/src/circle.c b/src/circle.c
new file mode 100644
index 0000000..a69c207
--- /dev/null
+++ b/src/circle.c
@@ -0,0 +1,467 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2015 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+#include <string.h>
+
+#include "ganv/canvas.h"
+#include "ganv/circle.h"
+
+#include "./color.h"
+#include "./boilerplate.h"
+#include "./gettext.h"
+#include "./ganv-private.h"
+
+G_DEFINE_TYPE_WITH_CODE(GanvCircle, ganv_circle, GANV_TYPE_NODE,
+ G_ADD_PRIVATE(GanvCircle))
+
+static GanvNodeClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_RADIUS,
+ PROP_RADIUS_EMS,
+ PROP_FIT_LABEL
+};
+
+static void
+ganv_circle_init(GanvCircle* circle)
+{
+ circle->impl = ganv_circle_get_instance_private(circle);
+
+ memset(&circle->impl->coords, '\0', sizeof(GanvCircleCoords));
+ circle->impl->coords.radius = 0.0;
+ circle->impl->coords.radius_ems = 1.0;
+ circle->impl->coords.width = 2.0;
+ circle->impl->old_coords = circle->impl->coords;
+ circle->impl->fit_label = TRUE;
+}
+
+static void
+ganv_circle_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_CIRCLE(object));
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+}
+
+static void
+ganv_circle_set_property(GObject* object,
+ guint prop_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_CIRCLE(object));
+
+ GanvCircle* circle = GANV_CIRCLE(object);
+
+ switch (prop_id) {
+ SET_CASE(RADIUS, double, circle->impl->coords.radius);
+ SET_CASE(RADIUS_EMS, double, circle->impl->coords.radius_ems);
+ SET_CASE(FIT_LABEL, boolean, circle->impl->fit_label);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+
+ if (prop_id == PROP_RADIUS_EMS) {
+ ganv_circle_set_radius_ems(circle, circle->impl->coords.radius_ems);
+ }
+}
+
+static void
+ganv_circle_get_property(GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_CIRCLE(object));
+
+ GanvCircle* circle = GANV_CIRCLE(object);
+
+ switch (prop_id) {
+ GET_CASE(RADIUS, double, circle->impl->coords.radius);
+ GET_CASE(RADIUS_EMS, double, circle->impl->coords.radius_ems);
+ GET_CASE(FIT_LABEL, boolean, circle->impl->fit_label);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_circle_resize(GanvNode* self)
+{
+ GanvNode* node = GANV_NODE(self);
+ GanvCircle* circle = GANV_CIRCLE(self);
+ GanvCanvas* canvas = GANV_CANVAS(GANV_ITEM(node)->impl->canvas);
+
+ if (node->impl->label) {
+ if (node->impl->label->impl->needs_layout) {
+ ganv_text_layout(node->impl->label);
+ }
+
+ const double label_w = node->impl->label->impl->coords.width;
+ const double label_h = node->impl->label->impl->coords.height;
+ if (circle->impl->fit_label) {
+ // Resize to fit text
+ const double radius = MAX(label_w, label_h) / 2.0 + 3.0;
+ if (radius != circle->impl->coords.radius) {
+ ganv_item_set(GANV_ITEM(self),
+ "radius", radius,
+ NULL);
+ }
+ }
+
+ // Center label
+ ganv_item_set(GANV_ITEM(node->impl->label),
+ "x", label_w / -2.0,
+ "y", label_h / -2.0,
+ NULL);
+ }
+
+ if (parent_class->resize) {
+ parent_class->resize(self);
+ }
+
+ ganv_canvas_for_each_edge_on(
+ canvas, node, (GanvEdgeFunc)ganv_edge_update_location, NULL);
+}
+
+static void
+ganv_circle_redraw_text(GanvNode* self)
+{
+ GanvCircle* circle = GANV_CIRCLE(self);
+ if (circle->impl->coords.radius_ems) {
+ ganv_circle_set_radius_ems(circle, circle->impl->coords.radius_ems);
+ }
+
+ if (parent_class->redraw_text) {
+ parent_class->redraw_text(self);
+ }
+}
+
+static gboolean
+ganv_circle_is_within(const GanvNode* self,
+ double x1,
+ double y1,
+ double x2,
+ double y2)
+{
+ const double x = GANV_ITEM(self)->impl->x;
+ const double y = GANV_ITEM(self)->impl->y;
+
+ return x >= x1
+ && x <= x2
+ && y >= y1
+ && y <= y2;
+}
+
+static void
+ganv_circle_vector(const GanvNode* self,
+ const GanvNode* other,
+ double* x,
+ double* y,
+ double* dx,
+ double* dy)
+{
+ GanvCircle* circle = GANV_CIRCLE(self);
+
+ const double cx = GANV_ITEM(self)->impl->x;
+ const double cy = GANV_ITEM(self)->impl->y;
+ const double other_x = GANV_ITEM(other)->impl->x;
+ const double other_y = GANV_ITEM(other)->impl->y;
+
+ const double border = circle->node.impl->border_width;
+ const double xdist = other_x - cx;
+ const double ydist = other_y - cy;
+ const double h = sqrt((xdist * xdist) + (ydist * ydist));
+ const double theta = asin(xdist / (h + DBL_EPSILON));
+ const double y_mod = (cy < other_y) ? 1 : -1;
+ const double ret_h = h - circle->impl->coords.radius - border / 2.0;
+ const double ret_x = other_x - sin(theta) * ret_h;
+ const double ret_y = other_y - cos(theta) * ret_h * y_mod;
+
+ *x = ret_x;
+ *y = ret_y;
+ *dx = 0.0;
+ *dy = 0.0;
+
+ ganv_item_i2w(GANV_ITEM(circle)->impl->parent, x, y);
+}
+
+static void
+request_redraw(GanvItem* item,
+ const GanvCircleCoords* coords,
+ gboolean world)
+{
+ const double w = coords->width;
+
+ double x1 = coords->x - coords->radius - w;
+ double y1 = coords->y - coords->radius - w;
+ double x2 = coords->x + coords->radius + w;
+ double y2 = coords->y + coords->radius + w;
+
+ if (!world) {
+ // Convert from parent-relative coordinates to world coordinates
+ ganv_item_i2w_pair(item, &x1, &y1, &x2, &y2);
+ }
+
+ ganv_canvas_request_redraw_w(item->impl->canvas, x1, y1, x2, y2);
+}
+
+static void
+coords_i2w(GanvItem* item, GanvCircleCoords* coords)
+{
+ ganv_item_i2w(item, &coords->x, &coords->y);
+}
+
+static void
+ganv_circle_bounds_item(GanvItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ const GanvCircle* circle = GANV_CIRCLE(item);
+ const GanvCircleCoords* coords = &circle->impl->coords;
+ *x1 = coords->x - coords->radius - coords->width;
+ *y1 = coords->y - coords->radius - coords->width;
+ *x2 = coords->x + coords->radius + coords->width;
+ *y2 = coords->y + coords->radius + coords->width;
+}
+
+static void
+ganv_circle_bounds(GanvItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ ganv_circle_bounds_item(item, x1, y1, x2, y2);
+}
+
+static void
+ganv_circle_update(GanvItem* item, int flags)
+{
+ GanvCircle* circle = GANV_CIRCLE(item);
+ GanvCirclePrivate* impl = circle->impl;
+ impl->coords.width = circle->node.impl->border_width;
+
+ // Request redraw of old location
+ request_redraw(item, &impl->old_coords, TRUE);
+
+ // Store old coordinates in world relative coordinates in case the
+ // group we are in moves between now and the next update
+ impl->old_coords = impl->coords;
+ coords_i2w(item, &impl->old_coords);
+
+ // Update world-relative bounding box
+ ganv_circle_bounds(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
+ ganv_item_i2w_pair(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
+
+ // Request redraw of new location
+ request_redraw(item, &impl->coords, FALSE);
+
+ GANV_ITEM_CLASS(parent_class)->update(item, flags);
+}
+
+static void
+ganv_circle_draw(GanvItem* item,
+ cairo_t* cr, double cx, double cy, double cw, double ch)
+{
+ GanvNode* node = GANV_NODE(item);
+ GanvCircle* circle = GANV_CIRCLE(item);
+ GanvCirclePrivate* impl = circle->impl;
+
+ double r, g, b, a;
+
+ double x = impl->coords.x;
+ double y = impl->coords.y;
+ ganv_item_i2w(item, &x, &y);
+
+ double dash_length, border_color, fill_color;
+ ganv_node_get_draw_properties(
+ &circle->node, &dash_length, &border_color, &fill_color);
+
+ // Fill
+ cairo_new_path(cr);
+ cairo_arc(cr,
+ x,
+ y,
+ impl->coords.radius + (impl->coords.width / 2.0),
+ 0, 2 * G_PI);
+ color_to_rgba(fill_color, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_fill(cr);
+
+ // Border
+ cairo_arc(cr,
+ x,
+ y,
+ impl->coords.radius,
+ 0, 2 * G_PI);
+ color_to_rgba(border_color, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_set_line_width(cr, impl->coords.width);
+ if (dash_length > 0) {
+ cairo_set_dash(cr, &dash_length, 1, circle->node.impl->dash_offset);
+ } else {
+ cairo_set_dash(cr, &dash_length, 0, 0);
+ }
+ cairo_stroke(cr);
+
+ // Draw label
+ if (node->impl->label) {
+ GanvItem* label_item = GANV_ITEM(node->impl->label);
+
+ if (label_item->object.flags & GANV_ITEM_VISIBLE) {
+ GANV_ITEM_GET_CLASS(label_item)->draw(
+ label_item, cr, cx, cy, cw, ch);
+ }
+ }
+}
+
+static double
+ganv_circle_point(GanvItem* item, double x, double y, GanvItem** actual_item)
+{
+ const GanvCircle* circle = GANV_CIRCLE(item);
+ const GanvCircleCoords* coords = &circle->impl->coords;
+
+ *actual_item = item;
+
+ const double dx = fabs(x - coords->x);
+ const double dy = fabs(y - coords->y);
+ const double d = sqrt((dx * dx) + (dy * dy));
+
+ if (d <= coords->radius + coords->width) {
+ // Point is inside the circle
+ return 0.0;
+ } else {
+ // Distance from the edge of the circle
+ return d - (coords->radius + coords->width);
+ }
+}
+
+static void
+ganv_circle_class_init(GanvCircleClass* klass)
+{
+ GObjectClass* gobject_class = (GObjectClass*)klass;
+ GtkObjectClass* object_class = (GtkObjectClass*)klass;
+ GanvItemClass* item_class = (GanvItemClass*)klass;
+ GanvNodeClass* node_class = (GanvNodeClass*)klass;
+
+ parent_class = GANV_NODE_CLASS(g_type_class_peek_parent(klass));
+
+ gobject_class->set_property = ganv_circle_set_property;
+ gobject_class->get_property = ganv_circle_get_property;
+
+ g_object_class_install_property(
+ gobject_class, PROP_RADIUS, g_param_spec_double(
+ "radius",
+ _("Radius"),
+ _("The radius of the circle."),
+ 0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_RADIUS_EMS, g_param_spec_double(
+ "radius-ems",
+ _("Radius in ems"),
+ _("The radius of the circle in ems."),
+ 0, G_MAXDOUBLE,
+ 1.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_FIT_LABEL, g_param_spec_boolean(
+ "fit-label",
+ _("Fit label"),
+ _("If true, expand circle to fit its label"),
+ TRUE,
+ (GParamFlags)G_PARAM_READWRITE));
+
+ object_class->destroy = ganv_circle_destroy;
+
+ node_class->resize = ganv_circle_resize;
+ node_class->is_within = ganv_circle_is_within;
+ node_class->tail_vector = ganv_circle_vector;
+ node_class->head_vector = ganv_circle_vector;
+ node_class->redraw_text = ganv_circle_redraw_text;
+
+ item_class->update = ganv_circle_update;
+ item_class->bounds = ganv_circle_bounds;
+ item_class->point = ganv_circle_point;
+ item_class->draw = ganv_circle_draw;
+}
+
+GanvCircle*
+ganv_circle_new(GanvCanvas* canvas,
+ const char* first_property_name, ...)
+{
+ GanvCircle* circle = GANV_CIRCLE(
+ g_object_new(ganv_circle_get_type(), "canvas", canvas, NULL));
+
+ va_list args;
+ va_start(args, first_property_name);
+ g_object_set_valist(G_OBJECT(circle), first_property_name, args);
+ va_end(args);
+
+ return circle;
+}
+
+double
+ganv_circle_get_radius(const GanvCircle* circle)
+{
+ return circle->impl->coords.radius;
+}
+
+void
+ganv_circle_set_radius(GanvCircle* circle, double radius)
+{
+ circle->impl->coords.radius = radius;
+ ganv_item_request_update(GANV_ITEM(circle));
+}
+
+double
+ganv_circle_get_radius_ems(const GanvCircle* circle)
+{
+ return circle->impl->coords.radius_ems;
+}
+
+void
+ganv_circle_set_radius_ems(GanvCircle* circle, double ems)
+{
+ GanvCanvas* canvas = GANV_CANVAS(GANV_ITEM(circle)->impl->canvas);
+ const double points = ganv_canvas_get_font_size(canvas);
+ circle->impl->coords.radius_ems = ems;
+ circle->impl->coords.radius = points * ems;
+ ganv_item_request_update(GANV_ITEM(circle));
+}
+
+gboolean
+ganv_circle_get_fit_label(const GanvCircle* circle)
+{
+ return circle->impl->fit_label;
+}
+
+void
+ganv_circle_set_fit_label(GanvCircle* circle, gboolean fit_label)
+{
+ circle->impl->fit_label = fit_label;
+ ganv_item_request_update(GANV_ITEM(circle));
+}
diff --git a/src/color.h b/src/color.h
new file mode 100644
index 0000000..ca52d98
--- /dev/null
+++ b/src/color.h
@@ -0,0 +1,63 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2015 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GANV_UTIL_H
+#define GANV_UTIL_H
+
+#include <glib.h>
+
+#ifdef GANV_USE_LIGHT_THEME
+# define DEFAULT_BACKGROUND_COLOR 0xFFFFFFFF
+# define DEFAULT_TEXT_COLOR 0x000000FF
+# define DIM_TEXT_COLOR 0x333333BB
+# define DEFAULT_FILL_COLOR 0xEEEEEEFF
+# define DEFAULT_BORDER_COLOR 0x000000FF
+# define PORT_BORDER_COLOR(fill) 0x000000FF
+# define EDGE_COLOR(base) highlight_color(tail_color, -48)
+#else
+# define DEFAULT_BACKGROUND_COLOR 0x000000FF
+# define DEFAULT_TEXT_COLOR 0xFFFFFFFF
+# define DIM_TEXT_COLOR 0xCCCCCCBB
+# define DEFAULT_FILL_COLOR 0x1E2224FF
+# define DEFAULT_BORDER_COLOR 0x3E4244FF
+# define PORT_BORDER_COLOR(fill) highlight_color(fill, 0x20)
+# define EDGE_COLOR(base) highlight_color(tail_color, 48)
+#endif
+
+static inline void
+color_to_rgba(guint color, double* r, double* g, double* b, double* a)
+{
+ *r = ((color >> 24) & 0xFF) / 255.0;
+ *g = ((color >> 16) & 0xFF) / 255.0;
+ *b = ((color >> 8) & 0xFF) / 255.0;
+ *a = ((color) & 0xFF) / 255.0;
+}
+
+static inline guint
+highlight_color(guint c, guint delta)
+{
+ const guint max_char = 255;
+ const guint r = MIN((c >> 24) + delta, max_char);
+ const guint g = MIN(((c >> 16) & 0xFF) + delta, max_char);
+ const guint b = MIN(((c >> 8) & 0xFF) + delta, max_char);
+ const guint a = c & 0xFF;
+
+ return ((((guint)(r)) << 24) |
+ (((guint)(g)) << 16) |
+ (((guint)(b)) << 8) |
+ (((guint)(a))));
+}
+
+#endif // GANV_UTIL_H
diff --git a/src/edge.c b/src/edge.c
new file mode 100644
index 0000000..a22bc73
--- /dev/null
+++ b/src/edge.c
@@ -0,0 +1,786 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2016 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+#include <string.h>
+
+#include <cairo.h>
+
+#include "ganv/canvas.h"
+#include "ganv/edge.h"
+#include "ganv/node.h"
+
+#include "./boilerplate.h"
+#include "./color.h"
+#include "./gettext.h"
+
+#include "color.h"
+#include "ganv-private.h"
+
+#define ARROW_DEPTH 32
+#define ARROW_BREADTH 32
+
+// Uncomment to see control point path as straight lines
+//#define GANV_DEBUG_CURVES 1
+
+// Uncomment along with GANV_DEBUG_CURVES to see bounding box (buggy)
+//#define GANV_DEBUG_BOUNDS 1
+
+enum {
+ PROP_0,
+ PROP_TAIL,
+ PROP_HEAD,
+ PROP_WIDTH,
+ PROP_HANDLE_RADIUS,
+ PROP_DASH_LENGTH,
+ PROP_DASH_OFFSET,
+ PROP_COLOR,
+ PROP_CONSTRAINING,
+ PROP_CURVED,
+ PROP_ARROWHEAD,
+ PROP_SELECTED,
+ PROP_HIGHLIGHTED,
+ PROP_GHOST
+};
+
+G_DEFINE_TYPE_WITH_CODE(GanvEdge, ganv_edge, GANV_TYPE_ITEM,
+ G_ADD_PRIVATE(GanvEdge))
+
+static GanvItemClass* parent_class;
+
+static void
+ganv_edge_init(GanvEdge* edge)
+{
+ GanvEdgePrivate* impl = ganv_edge_get_instance_private(edge);
+
+ edge->impl = impl;
+
+ impl->tail = NULL;
+ impl->head = NULL;
+
+ memset(&impl->coords, '\0', sizeof(GanvEdgeCoords));
+ impl->coords.width = 2.0;
+ impl->coords.handle_radius = 4.0;
+ impl->coords.constraining = TRUE;
+ impl->coords.curved = FALSE;
+ impl->coords.arrowhead = FALSE;
+
+ impl->old_coords = impl->coords;
+ impl->dash_length = 0.0;
+ impl->dash_offset = 0.0;
+ impl->color = 0;
+}
+
+static void
+ganv_edge_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_EDGE(object));
+
+ GanvEdge* edge = GANV_EDGE(object);
+ GanvCanvas* canvas = GANV_CANVAS(edge->item.impl->canvas);
+ if (canvas && !edge->impl->ghost) {
+ edge->item.impl->canvas = NULL;
+ }
+ edge->item.impl->parent = NULL;
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+}
+
+static void
+ganv_edge_set_property(GObject* object,
+ guint prop_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_EDGE(object));
+
+ GanvItem* item = GANV_ITEM(object);
+ GanvEdge* edge = GANV_EDGE(object);
+ GanvEdgePrivate* impl = edge->impl;
+ GanvEdgeCoords* coords = &impl->coords;
+
+ switch (prop_id) {
+ SET_CASE(WIDTH, double, coords->width);
+ SET_CASE(HANDLE_RADIUS, double, coords->handle_radius);
+ SET_CASE(DASH_LENGTH, double, impl->dash_length);
+ SET_CASE(DASH_OFFSET, double, impl->dash_offset);
+ SET_CASE(COLOR, uint, impl->color);
+ SET_CASE(CONSTRAINING, boolean, impl->coords.constraining);
+ SET_CASE(CURVED, boolean, impl->coords.curved);
+ SET_CASE(ARROWHEAD, boolean, impl->coords.arrowhead);
+ SET_CASE(SELECTED, boolean, impl->selected);
+ SET_CASE(HIGHLIGHTED, boolean, impl->highlighted);
+ SET_CASE(GHOST, boolean, impl->ghost);
+ case PROP_TAIL: {
+ const gobject tmp = g_value_get_object(value);
+ if (impl->tail != tmp) {
+ impl->tail = GANV_NODE(tmp);
+ ganv_item_request_update(item);
+ }
+ break;
+ }
+ case PROP_HEAD: {
+ const gobject tmp = g_value_get_object(value);
+ if (impl->head != tmp) {
+ impl->head = GANV_NODE(tmp);
+ ganv_item_request_update(item);
+ }
+ break;
+ }
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_edge_get_property(GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_EDGE(object));
+
+ GanvEdge* edge = GANV_EDGE(object);
+ GanvEdgePrivate* impl = edge->impl;
+
+ switch (prop_id) {
+ GET_CASE(TAIL, object, impl->tail);
+ GET_CASE(HEAD, object, impl->head);
+ GET_CASE(WIDTH, double, impl->coords.width);
+ SET_CASE(HANDLE_RADIUS, double, impl->coords.handle_radius);
+ GET_CASE(DASH_LENGTH, double, impl->dash_length);
+ GET_CASE(DASH_OFFSET, double, impl->dash_offset);
+ GET_CASE(COLOR, uint, impl->color);
+ GET_CASE(CONSTRAINING, boolean, impl->coords.constraining);
+ GET_CASE(CURVED, boolean, impl->coords.curved);
+ GET_CASE(ARROWHEAD, boolean, impl->coords.arrowhead);
+ GET_CASE(SELECTED, boolean, impl->selected);
+ GET_CASE(HIGHLIGHTED, boolean, impl->highlighted);
+ SET_CASE(GHOST, boolean, impl->ghost);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+void
+ganv_edge_request_redraw(GanvItem* item,
+ const GanvEdgeCoords* coords)
+{
+ GanvCanvas* canvas = item->impl->canvas;
+ const double w = coords->width;
+ if (coords->curved) {
+ const double src_x = coords->x1;
+ const double src_y = coords->y1;
+ const double dst_x = coords->x2;
+ const double dst_y = coords->y2;
+ const double join_x = (src_x + dst_x) / 2.0;
+ const double join_y = (src_y + dst_y) / 2.0;
+ const double src_x1 = coords->cx1;
+ const double src_y1 = coords->cy1;
+ const double dst_x1 = coords->cx2;
+ const double dst_y1 = coords->cy2;
+
+ const double r1x1 = MIN(MIN(src_x, join_x), src_x1);
+ const double r1y1 = MIN(MIN(src_y, join_y), src_y1);
+ const double r1x2 = MAX(MAX(src_x, join_x), src_x1);
+ const double r1y2 = MAX(MAX(src_y, join_y), src_y1);
+ ganv_canvas_request_redraw_w(canvas,
+ r1x1 - w, r1y1 - w,
+ r1x2 + w, r1y2 + w);
+
+ const double r2x1 = MIN(MIN(dst_x, join_x), dst_x1);
+ const double r2y1 = MIN(MIN(dst_y, join_y), dst_y1);
+ const double r2x2 = MAX(MAX(dst_x, join_x), dst_x1);
+ const double r2y2 = MAX(MAX(dst_y, join_y), dst_y1);
+ ganv_canvas_request_redraw_w(canvas,
+ r2x1 - w, r2y1 - w,
+ r2x2 + w, r2y2 + w);
+
+ } else {
+ const double x1 = MIN(coords->x1, coords->x2);
+ const double y1 = MIN(coords->y1, coords->y2);
+ const double x2 = MAX(coords->x1, coords->x2);
+ const double y2 = MAX(coords->y1, coords->y2);
+
+ ganv_canvas_request_redraw_w(canvas,
+ x1 - w, y1 - w,
+ x2 + w, y2 + w);
+ }
+
+ if (coords->handle_radius > 0.0) {
+ ganv_canvas_request_redraw_w(
+ canvas,
+ coords->handle_x - coords->handle_radius - w,
+ coords->handle_y - coords->handle_radius - w,
+ coords->handle_x + coords->handle_radius + w,
+ coords->handle_y + coords->handle_radius + w);
+ }
+
+ if (coords->arrowhead) {
+ ganv_canvas_request_redraw_w(
+ canvas,
+ coords->x2 - ARROW_DEPTH,
+ coords->y2 - ARROW_BREADTH,
+ coords->x2 + ARROW_DEPTH,
+ coords->y2 + ARROW_BREADTH);
+ }
+}
+
+static void
+ganv_edge_bounds(GanvItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ GanvEdge* edge = GANV_EDGE(item);
+ GanvEdgePrivate* impl = edge->impl;
+ GanvEdgeCoords* coords = &impl->coords;
+ const double w = coords->width;
+
+ if (coords->curved) {
+ *x1 = MIN(coords->x1, MIN(coords->cx1, MIN(coords->x2, coords->cx2))) - w;
+ *y1 = MIN(coords->y1, MIN(coords->cy1, MIN(coords->y2, coords->cy2))) - w;
+ *x2 = MAX(coords->x1, MAX(coords->cx1, MAX(coords->x2, coords->cx2))) + w;
+ *y2 = MAX(coords->y1, MAX(coords->cy1, MAX(coords->y2, coords->cy2))) + w;
+ } else {
+ *x1 = MIN(impl->coords.x1, impl->coords.x2) - w;
+ *y1 = MIN(impl->coords.y1, impl->coords.y2) - w;
+ *x2 = MAX(impl->coords.x1, impl->coords.x2) + w;
+ *y2 = MAX(impl->coords.y1, impl->coords.y2) + w;
+ }
+}
+
+void
+ganv_edge_get_coords(const GanvEdge* edge, GanvEdgeCoords* coords)
+{
+ GanvEdgePrivate* impl = edge->impl;
+
+ GANV_NODE_GET_CLASS(impl->tail)->tail_vector(
+ impl->tail, impl->head,
+ &coords->x1, &coords->y1, &coords->cx1, &coords->cy1);
+ GANV_NODE_GET_CLASS(impl->head)->head_vector(
+ impl->head, impl->tail,
+ &coords->x2, &coords->y2, &coords->cx2, &coords->cy2);
+
+ const double dx = coords->x2 - coords->x1;
+ const double dy = coords->y2 - coords->y1;
+
+ coords->handle_x = coords->x1 + (dx / 2.0);
+ coords->handle_y = coords->y1 + (dy / 2.0);
+
+ const double abs_dx = fabs(dx);
+ const double abs_dy = fabs(dy);
+
+ coords->cx1 = coords->x1 + (coords->cx1 * (abs_dx / 4.0));
+ coords->cy1 = coords->y1 + (coords->cy1 * (abs_dy / 4.0));
+ coords->cx2 = coords->x2 + (coords->cx2 * (abs_dx / 4.0));
+ coords->cy2 = coords->y2 + (coords->cy2 * (abs_dy / 4.0));
+}
+
+static void
+ganv_edge_update(GanvItem* item, int flags)
+{
+ GanvEdge* edge = GANV_EDGE(item);
+ GanvEdgePrivate* impl = edge->impl;
+
+ // Request redraw of old location
+ ganv_edge_request_redraw(item, &impl->old_coords);
+
+ // Calculate new coordinates from tail and head
+ ganv_edge_get_coords(edge, &impl->coords);
+
+ // Update old coordinates
+ impl->old_coords = impl->coords;
+
+ // Get bounding box
+ double x1, x2, y1, y2;
+ ganv_edge_bounds(item, &x1, &y1, &x2, &y2);
+
+ // Ensure bounding box has non-zero area
+ if (x1 == x2) {
+ x2 += 1.0;
+ }
+ if (y1 == y2) {
+ y2 += 1.0;
+ }
+
+ // Update world-relative bounding box
+ item->impl->x1 = x1;
+ item->impl->y1 = y1;
+ item->impl->x2 = x2;
+ item->impl->y2 = y2;
+ ganv_item_i2w_pair(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
+
+ // Request redraw of new location
+ ganv_edge_request_redraw(item, &impl->coords);
+
+ parent_class->update(item, flags);
+}
+
+static void
+ganv_edge_draw(GanvItem* item,
+ cairo_t* cr, double cx, double cy, double cw, double ch)
+{
+ GanvEdge* edge = GANV_EDGE(item);
+ GanvEdgePrivate* impl = edge->impl;
+
+ double src_x = impl->coords.x1;
+ double src_y = impl->coords.y1;
+ double dst_x = impl->coords.x2;
+ double dst_y = impl->coords.y2;
+ double dx = src_x - dst_x;
+ double dy = src_y - dst_y;
+
+ double r, g, b, a;
+ if (impl->highlighted) {
+ color_to_rgba(highlight_color(impl->color, 0x40), &r, &g, &b, &a);
+ } else {
+ color_to_rgba(impl->color, &r, &g, &b, &a);
+ }
+ cairo_set_source_rgba(cr, r, g, b, a);
+
+ cairo_set_line_width(cr, impl->coords.width);
+ cairo_move_to(cr, src_x, src_y);
+
+ const double dash_length = (impl->selected ? 4.0 : impl->dash_length);
+ if (dash_length > 0.0) {
+ double dashed[2] = { dash_length, dash_length };
+ cairo_set_dash(cr, dashed, 2, impl->dash_offset);
+ } else {
+ cairo_set_dash(cr, &dash_length, 0, 0);
+ }
+
+ const double join_x = (src_x + dst_x) / 2.0;
+ const double join_y = (src_y + dst_y) / 2.0;
+
+ if (impl->coords.curved) {
+ // Curved line as 2 paths which join at the middle point
+
+ // Path 1 (src_x, src_y) -> (join_x, join_y)
+ // Control point 1
+ const double src_x1 = impl->coords.cx1;
+ const double src_y1 = impl->coords.cy1;
+ // Control point 2
+ const double src_x2 = (join_x + src_x1) / 2.0;
+ const double src_y2 = (join_y + src_y1) / 2.0;
+
+ // Path 2, (join_x, join_y) -> (dst_x, dst_y)
+ // Control point 1
+ const double dst_x1 = impl->coords.cx2;
+ const double dst_y1 = impl->coords.cy2;
+ // Control point 2
+ const double dst_x2 = (join_x + dst_x1) / 2.0;
+ const double dst_y2 = (join_y + dst_y1) / 2.0;
+
+ cairo_move_to(cr, src_x, src_y);
+ cairo_curve_to(cr, src_x1, src_y1, src_x2, src_y2, join_x, join_y);
+ cairo_curve_to(cr, dst_x2, dst_y2, dst_x1, dst_y1, dst_x, dst_y);
+
+#ifdef GANV_DEBUG_CURVES
+ cairo_stroke(cr);
+ cairo_save(cr);
+ cairo_set_source_rgba(cr, 1.0, 0, 0, 0.5);
+
+ cairo_move_to(cr, src_x, src_y);
+ cairo_line_to(cr, src_x1, src_y1);
+ cairo_stroke(cr);
+
+ cairo_move_to(cr, join_x, join_y);
+ cairo_line_to(cr, src_x2, src_y2);
+ cairo_stroke(cr);
+
+ cairo_move_to(cr, join_x, join_y);
+ cairo_line_to(cr, dst_x2, dst_y2);
+ cairo_stroke(cr);
+
+ cairo_move_to(cr, dst_x, dst_y);
+ cairo_line_to(cr, dst_x1, dst_y1);
+ cairo_stroke(cr);
+
+#ifdef GANV_DEBUG_BOUNDS
+ double bounds_x1, bounds_y1, bounds_x2, bounds_y2;
+ ganv_edge_bounds(item, &bounds_x1, &bounds_y1, &bounds_x2, &bounds_y2);
+ cairo_rectangle(cr,
+ bounds_x1, bounds_y1,
+ bounds_x2 - bounds_x1, bounds_y2 - bounds_y1);
+#endif
+
+ cairo_restore(cr);
+#endif
+
+ cairo_stroke(cr);
+ if (impl->coords.arrowhead) {
+ cairo_move_to(cr, dst_x - 12, dst_y - 4);
+ cairo_line_to(cr, dst_x, dst_y);
+ cairo_line_to(cr, dst_x - 12, dst_y + 4);
+ cairo_close_path(cr);
+ cairo_stroke_preserve(cr);
+ cairo_fill(cr);
+ }
+
+ } else {
+ // Straight line from (x1, y1) to (x2, y2)
+ cairo_move_to(cr, src_x, src_y);
+ cairo_line_to(cr, dst_x, dst_y);
+ cairo_stroke(cr);
+
+ if (impl->coords.arrowhead) {
+ const double ah = sqrt(dx * dx + dy * dy);
+ const double adx = dx / ah * 8.0;
+ const double ady = dy / ah * 8.0;
+
+ cairo_move_to(cr,
+ dst_x + adx - ady/1.5,
+ dst_y + ady + adx/1.5);
+ cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL);
+ cairo_line_to(cr, dst_x, dst_y);
+ cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER);
+ cairo_line_to(cr,
+ dst_x + adx + ady/1.5,
+ dst_y + ady - adx/1.5);
+ cairo_close_path(cr);
+ cairo_stroke_preserve(cr);
+ cairo_fill(cr);
+ }
+ }
+
+ if (!ganv_canvas_exporting(item->impl->canvas) &&
+ impl->coords.handle_radius > 0.0) {
+ cairo_move_to(cr, join_x, join_y);
+ cairo_arc(cr, join_x, join_y, impl->coords.handle_radius, 0, 2 * G_PI);
+ cairo_fill(cr);
+ }
+}
+
+static double
+ganv_edge_point(GanvItem* item, double x, double y, GanvItem** actual_item)
+{
+ const GanvEdge* edge = GANV_EDGE(item);
+ const GanvEdgeCoords* coords = &edge->impl->coords;
+
+ const double dx = fabs(x - coords->handle_x);
+ const double dy = fabs(y - coords->handle_y);
+ const double d = sqrt((dx * dx) + (dy * dy));
+
+ *actual_item = item;
+
+ if (d <= coords->handle_radius) {
+ // Point is inside the handle
+ return 0.0;
+ } else {
+ // Distance from the edge of the handle
+ return d - (coords->handle_radius + coords->width);
+ }
+}
+
+gboolean
+ganv_edge_is_within(const GanvEdge* edge,
+ double x1,
+ double y1,
+ double x2,
+ double y2)
+{
+ const double handle_x = edge->impl->coords.handle_x;
+ const double handle_y = edge->impl->coords.handle_y;
+
+ return handle_x >= x1
+ && handle_x <= x2
+ && handle_y >= y1
+ && handle_y <= y2;
+}
+
+static void
+ganv_edge_class_init(GanvEdgeClass* klass)
+{
+ GObjectClass* gobject_class = (GObjectClass*)klass;
+ GtkObjectClass* object_class = (GtkObjectClass*)klass;
+ GanvItemClass* item_class = (GanvItemClass*)klass;
+
+ parent_class = GANV_ITEM_CLASS(g_type_class_peek_parent(klass));
+
+ gobject_class->set_property = ganv_edge_set_property;
+ gobject_class->get_property = ganv_edge_get_property;
+
+ g_object_class_install_property(
+ gobject_class, PROP_TAIL, g_param_spec_object(
+ "tail",
+ _("Tail"),
+ _("Node this edge starts from."),
+ GANV_TYPE_NODE,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_HEAD, g_param_spec_object(
+ "head",
+ _("Head"),
+ _("Node this edge ends at."),
+ GANV_TYPE_NODE,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_WIDTH, g_param_spec_double(
+ "width",
+ _("Line width"),
+ _("Width of edge line."),
+ 0.0, G_MAXDOUBLE,
+ 2.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_HANDLE_RADIUS, g_param_spec_double(
+ "handle-radius",
+ _("Gandle radius"),
+ _("Radius of handle in canvas units."),
+ 0.0, G_MAXDOUBLE,
+ 4.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_DASH_LENGTH, g_param_spec_double(
+ "dash-length",
+ _("Line dash length"),
+ _("Length of line dashes, or zero for no dashing."),
+ 0.0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_DASH_OFFSET, g_param_spec_double(
+ "dash-offset",
+ _("Line dash offset"),
+ _("Start offset for line dashes, used for selected animation."),
+ 0.0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_COLOR, g_param_spec_uint(
+ "color",
+ _("Color"),
+ _("Line color as an RGBA integer."),
+ 0, G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_CONSTRAINING, g_param_spec_boolean(
+ "constraining",
+ _("Constraining"),
+ _("Whether edge should constrain the layout."),
+ 1,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_CURVED, g_param_spec_boolean(
+ "curved",
+ _("Curved"),
+ _("Whether line should be curved rather than straight."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_ARROWHEAD, g_param_spec_boolean(
+ "arrowhead",
+ _("Arrowhead"),
+ _("Whether to show an arrowhead at the head of this edge."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_SELECTED, g_param_spec_boolean(
+ "selected",
+ _("Selected"),
+ _("Whether this edge is selected."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_HIGHLIGHTED, g_param_spec_boolean(
+ "highlighted",
+ _("Highlighted"),
+ _("Whether to highlight the edge."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_GHOST, g_param_spec_boolean(
+ "ghost",
+ _("Ghost"),
+ _("Whether this edge is a `ghost', which is an edge that is not "
+ "added to the canvas data structures. Ghost edges are used for "
+ "temporary edges that are not considered `real', e.g. the edge "
+ "made while dragging to make a connection."),
+ 0,
+ G_PARAM_READWRITE));
+
+ object_class->destroy = ganv_edge_destroy;
+
+ item_class->update = ganv_edge_update;
+ item_class->bounds = ganv_edge_bounds;
+ item_class->point = ganv_edge_point;
+ item_class->draw = ganv_edge_draw;
+}
+
+GanvEdge*
+ganv_edge_new(GanvCanvas* canvas,
+ GanvNode* tail,
+ GanvNode* head,
+ const char* first_prop_name, ...)
+{
+ GanvEdge* edge = GANV_EDGE(
+ g_object_new(ganv_edge_get_type(), NULL));
+
+ va_list args;
+ va_start(args, first_prop_name);
+ ganv_item_construct(&edge->item,
+ GANV_ITEM(ganv_canvas_root(canvas)),
+ first_prop_name, args);
+ va_end(args);
+
+ edge->impl->tail = tail;
+ edge->impl->head = head;
+
+ if (!edge->impl->color) {
+ const guint tail_color = GANV_NODE(tail)->impl->fill_color;
+ g_object_set(G_OBJECT(edge),
+ "color", EDGE_COLOR(tail_color),
+ NULL);
+ }
+
+ if (!edge->impl->ghost) {
+ ganv_canvas_add_edge(canvas, edge);
+ }
+ return edge;
+}
+
+void
+ganv_edge_update_location(GanvEdge* edge)
+{
+ ganv_item_request_update(GANV_ITEM(edge));
+}
+
+gboolean
+ganv_edge_get_constraining(const GanvEdge* edge)
+{
+ return edge->impl->coords.constraining;
+}
+
+void
+ganv_edge_set_constraining(GanvEdge* edge, gboolean constraining)
+{
+ edge->impl->coords.constraining = constraining;
+ ganv_edge_request_redraw(GANV_ITEM(edge), &edge->impl->coords);
+}
+
+gboolean
+ganv_edge_get_curved(const GanvEdge* edge)
+{
+ return edge->impl->coords.curved;
+}
+
+void
+ganv_edge_set_curved(GanvEdge* edge, gboolean curved)
+{
+ edge->impl->coords.curved = curved;
+ ganv_edge_request_redraw(GANV_ITEM(edge), &edge->impl->coords);
+}
+
+void
+ganv_edge_set_selected(GanvEdge* edge, gboolean selected)
+{
+ GanvCanvas* canvas = GANV_CANVAS(edge->item.impl->canvas);
+ if (selected) {
+ ganv_canvas_select_edge(canvas, edge);
+ } else {
+ ganv_canvas_unselect_edge(canvas, edge);
+ }
+}
+
+void
+ganv_edge_select(GanvEdge* edge)
+{
+ ganv_edge_set_selected(edge, TRUE);
+}
+
+void
+ganv_edge_unselect(GanvEdge* edge)
+{
+ ganv_edge_set_selected(edge, FALSE);
+}
+
+void
+ganv_edge_highlight(GanvEdge* edge)
+{
+ ganv_edge_set_highlighted(edge, TRUE);
+}
+
+void
+ganv_edge_unhighlight(GanvEdge* edge)
+{
+ ganv_edge_set_highlighted(edge, FALSE);
+}
+
+void
+ganv_edge_set_highlighted(GanvEdge* edge, gboolean highlighted)
+{
+ edge->impl->highlighted = highlighted;
+ ganv_edge_request_redraw(GANV_ITEM(edge), &edge->impl->coords);
+}
+
+void
+ganv_edge_tick(GanvEdge* edge, double seconds)
+{
+ ganv_item_set(GANV_ITEM(edge),
+ "dash-offset", seconds * 8.0,
+ NULL);
+}
+
+void
+ganv_edge_disconnect(GanvEdge* edge)
+{
+ if (!edge->impl->ghost) {
+ ganv_canvas_disconnect_edge(
+ GANV_CANVAS(edge->item.impl->canvas),
+ edge);
+ }
+}
+
+void
+ganv_edge_remove(GanvEdge* edge)
+{
+ if (!edge->impl->ghost) {
+ ganv_canvas_remove_edge(
+ GANV_CANVAS(edge->item.impl->canvas),
+ edge);
+ }
+}
+
+GanvNode*
+ganv_edge_get_tail(const GanvEdge* edge)
+{
+ return edge->impl->tail;
+}
+
+GanvNode*
+ganv_edge_get_head(const GanvEdge* edge)
+{
+ return edge->impl->head;
+}
diff --git a/src/fdgl.hpp b/src/fdgl.hpp
new file mode 100644
index 0000000..38fded1
--- /dev/null
+++ b/src/fdgl.hpp
@@ -0,0 +1,166 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2015 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <float.h>
+#include <math.h>
+
+static const double CHARGE_KE = 4000000.0;
+static const double EDGE_K = 16.0;
+static const double EDGE_LEN = 0.1;
+
+struct Region {
+ Vector pos;
+ Vector area;
+};
+
+inline Vector
+vec_add(const Vector& a, const Vector& b)
+{
+ const Vector result = { a.x + b.x, a.y + b.y };
+ return result;
+}
+
+inline Vector
+vec_sub(const Vector& a, const Vector& b)
+{
+ const Vector result = { a.x - b.x, a.y - b.y };
+ return result;
+}
+
+inline Vector
+vec_mult(const Vector& a, double m)
+{
+ const Vector result = { a.x * m, a.y * m };
+ return result;
+}
+
+inline double
+vec_mult(const Vector& a, const Vector& b)
+{
+ return a.x * b.x + a.y * b.y;
+}
+
+/** Magnitude. */
+inline double
+vec_mag(const Vector& vec)
+{
+ return sqrt(vec.x * vec.x + vec.y * vec.y);
+}
+
+/** Reciprocal of magnitude. */
+inline double
+vec_rmag(const Vector& vec)
+{
+ return 1.0 / sqrt(vec.x * vec.x + vec.y * vec.y);
+}
+
+/** Hooke's law */
+inline Vector
+spring_force(const Vector& a, const Vector& b, double length, double k)
+{
+ const Vector vec = vec_sub(b, a);
+ const double mag = vec_mag(vec);
+ const double displacement = length - mag;
+ return vec_mult(vec, k * displacement * 0.5 / mag);
+}
+
+/** Spring force with a directional force to align with flow direction. */
+static const Vector
+edge_force(const Vector& dir, const Vector& hpos, const Vector& tpos)
+{
+ return vec_add(dir, spring_force(hpos, tpos, EDGE_LEN, EDGE_K));
+}
+
+/** Constant tide force, does not vary with distance. */
+inline Vector
+tide_force(const Vector& a, const Vector& b, double power)
+{
+ static const double G = 0.0000000000667;
+ const Vector vec = vec_sub(a, b);
+ const double mag = vec_mag(vec);
+ return vec_mult(vec, G * power / mag);
+}
+
+inline double
+rect_distance(Vector* vec,
+ const double ax1, const double ay1,
+ const double ax2, const double ay2,
+ const double bx1, const double by1,
+ const double bx2, const double by2)
+{
+ vec->x = 0.0;
+ vec->y = 0.0;
+
+ if (ax2 <= bx1) { // A is completely to the left of B
+ vec->x = ax2 - bx1;
+ if (ay2 <= by1) { // Top Left
+ const double dx = bx1 - ax2;
+ const double dy = by1 - ay2;
+ vec->y = ay2 - by1;
+ return sqrt(dx * dx + dy * dy);
+ } else if (ay1 >= by2) { // Bottom left
+ const double dx = bx1 - ax2;
+ const double dy = ay1 - by2;
+ vec->y = ay1 - by2;
+ return sqrt(dx * dx + dy * dy);
+ } else { // Left
+ return bx1 - ax2;
+ }
+ } else if (ax1 >= bx2) { // A is completely to the right of B
+ vec->x = ax1 - bx2;
+ if (ay2 <= by1) { // Top right
+ const double dx = ax1 - bx2;
+ const double dy = by1 - ay2;
+ vec->y = ay2 - by1;
+ return sqrt(dx * dx + dy * dy);
+ } else if (ay1 >= by2) { // Bottom right
+ const double dx = ax1 - bx2;
+ const double dy = ay1 - by2;
+ vec->y = ay1 - by2;
+ return sqrt(dx * dx + dy * dy);
+ } else { // Right
+ return ax1 - bx2;
+ }
+ } else if (ay2 <= by1) { // Top
+ vec->y = ay2 - by1;
+ return by1 - ay2;
+ } else if (ay1 >= by2) { // Bottom
+ vec->y = ay1 - by2;
+ return ay1 - by2;
+ } else { // Overlap
+ return 0.0;
+ }
+}
+
+/** Repelling charge force, ala Coulomb's law. */
+inline Vector
+repel_force(const Region& a, const Region& b)
+{
+ static const double MIN_DIST = 1.0;
+
+ Vector vec;
+ double dist = rect_distance(
+ &vec,
+ a.pos.x - (a.area.x / 2.0), a.pos.y - (a.area.y / 2.0),
+ a.pos.x + (a.area.x / 2.0), a.pos.y + (a.area.y / 2.0),
+ b.pos.x - (b.area.x / 2.0), b.pos.y - (b.area.y / 2.0),
+ b.pos.x + (b.area.x / 2.0), b.pos.y + (b.area.y / 2.0));
+
+ if (dist <= MIN_DIST) {
+ dist = MIN_DIST;
+ vec = vec_sub(a.pos, b.pos);
+ }
+ return vec_mult(vec, (CHARGE_KE * 0.5 / (vec_mag(vec) * dist * dist)));
+}
diff --git a/src/ganv-marshal.list b/src/ganv-marshal.list
new file mode 100644
index 0000000..64e6c6b
--- /dev/null
+++ b/src/ganv-marshal.list
@@ -0,0 +1,4 @@
+BOOLEAN:BOXED
+VOID:DOUBLE,DOUBLE
+VOID:OBJECT,INT,INT,INT,INT
+VOID:OBJECT,OBJECT
diff --git a/src/ganv-private.h b/src/ganv-private.h
new file mode 100644
index 0000000..df611a9
--- /dev/null
+++ b/src/ganv-private.h
@@ -0,0 +1,403 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2015 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GANV_PRIVATE_H
+#define GANV_PRIVATE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <cairo.h>
+
+#include "ganv/canvas.h"
+#include "ganv/text.h"
+#include "ganv/types.h"
+
+#define GANV_CLOSE_ENOUGH 1
+
+extern guint signal_moved;
+
+/* Box */
+
+typedef struct {
+ double x1, y1, x2, y2;
+ double border_width;
+ gboolean stacked;
+} GanvBoxCoords;
+
+struct _GanvBoxPrivate {
+ GanvBoxCoords coords;
+ GanvBoxCoords old_coords;
+ double radius_tl;
+ double radius_tr;
+ double radius_br;
+ double radius_bl;
+ gboolean beveled;
+};
+
+/* Circle */
+
+typedef struct {
+ double x, y, radius, radius_ems;
+ double width;
+} GanvCircleCoords;
+
+struct _GanvCirclePrivate {
+ GanvCircleCoords coords;
+ GanvCircleCoords old_coords;
+ gboolean fit_label;
+};
+
+/* Edge */
+
+typedef struct {
+ double x1, y1, x2, y2;
+ double cx1, cy1, cx2, cy2;
+ double handle_x, handle_y, handle_radius;
+ double width;
+ gboolean constraining;
+ gboolean curved;
+ gboolean arrowhead;
+} GanvEdgeCoords;
+
+struct _GanvEdgePrivate
+{
+ GanvNode* tail;
+ GanvNode* head;
+ GanvEdgeCoords coords;
+ GanvEdgeCoords old_coords;
+ double dash_length;
+ double dash_offset;
+ guint color;
+ gboolean selected;
+ gboolean highlighted;
+ gboolean ghost;
+};
+
+/* Module */
+
+struct _GanvModulePrivate
+{
+ GPtrArray* ports;
+ GanvItem* embed_item;
+ int embed_width;
+ int embed_height;
+ double widest_input;
+ double widest_output;
+ gboolean must_reorder;
+};
+
+/* Node */
+
+#ifdef GANV_FDGL
+typedef struct {
+ double x;
+ double y;
+} Vector;
+#endif
+
+struct _GanvNodePrivate {
+ struct _GanvNode* partner;
+ GanvText* label;
+ double dash_length;
+ double dash_offset;
+ double border_width;
+ guint fill_color;
+ guint border_color;
+ gboolean can_tail;
+ gboolean can_head;
+ gboolean is_source;
+ gboolean selected;
+ gboolean highlighted;
+ gboolean draggable;
+ gboolean show_label;
+ gboolean grabbed;
+ gboolean must_resize;
+#ifdef GANV_FDGL
+ Vector force;
+ Vector vel;
+ gboolean connected;
+#endif
+};
+
+/* Widget */
+
+struct _GanvWidgetPrivate {
+ GtkWidget* widget; /* The child widget */
+
+ double x, y; /* Position at anchor */
+ double width, height; /* Dimensions of widget */
+ GtkAnchorType anchor; /* Anchor side for widget */
+
+ int cx, cy; /* Top-left canvas coordinates for widget */
+ int cwidth, cheight; /* Size of widget in pixels */
+
+ guint destroy_id; /* Signal connection id for destruction of child widget */
+
+ guint size_pixels : 1; /* Is size specified in (unchanging) pixels or units (get scaled)? */
+ guint in_destroy : 1; /* Is child widget being destroyed? */
+};
+
+/* Group */
+struct _GanvGroupPrivate {
+ GList* item_list;
+ GList* item_list_end;
+};
+
+/* Item */
+struct _GanvItemPrivate {
+ /* Parent canvas for this item */
+ struct _GanvCanvas* canvas;
+
+ /* Parent for this item */
+ GanvItem* parent;
+
+ /* Wrapper object for this item, if any */
+ void* wrapper;
+
+ /* Layer (z order), higher values are on top */
+ guint layer;
+
+ /* Position in parent-relative coordinates. */
+ double x, y;
+
+ /* Bounding box for this item (in world coordinates) */
+ double x1, y1, x2, y2;
+
+ /* True if parent manages this item (don't call add/remove) */
+ gboolean managed;
+};
+
+void
+ganv_node_tick(GanvNode* self, double seconds);
+
+void
+ganv_node_tail_vector(const GanvNode* self,
+ const GanvNode* head,
+ double* x1,
+ double* y1,
+ double* x2,
+ double* y2);
+
+void
+ganv_node_head_vector(const GanvNode* self,
+ const GanvNode* tail,
+ double* x1,
+ double* y1,
+ double* x2,
+ double* y2);
+
+/**
+ * ganv_node_get_draw_properties:
+ *
+ * Get the colours that should currently be used for drawing this node. Note
+ * these may not be identical to the property values because of highlighting
+ * and selection.
+ */
+void
+ganv_node_get_draw_properties(const GanvNode* node,
+ double* dash_length,
+ double* border_color,
+ double* fill_color);
+
+/* Port */
+
+typedef struct {
+ GanvBox* rect;
+ float value;
+ float min;
+ float max;
+ gboolean is_toggle;
+ gboolean is_integer;
+} GanvPortControl;
+
+struct _GanvPortPrivate {
+ GanvPortControl* control;
+ GanvText* value_label;
+ gboolean is_input;
+ gboolean is_controllable;
+};
+
+/* Text */
+
+typedef struct
+{
+ double x;
+ double y;
+ double width;
+ double height;
+} GanvTextCoords;
+
+struct _GanvTextPrivate
+{
+ PangoLayout* layout;
+ char* text;
+ GanvTextCoords coords;
+ GanvTextCoords old_coords;
+ double font_size;
+ guint color;
+ gboolean needs_layout;
+};
+
+/* Canvas */
+
+typedef struct {
+ GanvPortOrderFunc port_cmp;
+ void* data;
+} PortOrderCtx;
+
+void
+ganv_canvas_move_selected_items(GanvCanvas* canvas,
+ double dx,
+ double dy);
+
+void
+ganv_canvas_selection_move_finished(GanvCanvas* canvas);
+
+void
+ganv_canvas_add_node(GanvCanvas* canvas,
+ GanvNode* node);
+
+void
+ganv_canvas_remove_node(GanvCanvas* canvas,
+ GanvNode* node);
+
+void
+ganv_canvas_select_node(GanvCanvas* canvas,
+ GanvNode* node);
+
+void
+ganv_canvas_unselect_node(GanvCanvas* canvas,
+ GanvNode* node);
+
+void
+ganv_canvas_add_edge(GanvCanvas* canvas,
+ GanvEdge* edge);
+
+void
+ganv_canvas_select_edge(GanvCanvas* canvas,
+ GanvEdge* edge);
+
+void
+ganv_canvas_unselect_edge(GanvCanvas* canvas,
+ GanvEdge* edge);
+
+void
+ganv_canvas_disconnect_edge(GanvCanvas* canvas,
+ GanvEdge* edge);
+
+gboolean
+ganv_canvas_port_event(GanvCanvas* canvas,
+ GanvPort* port,
+ GdkEvent* event);
+
+void
+ganv_canvas_contents_changed(GanvCanvas* canvas);
+
+void
+ganv_item_i2w_offset(GanvItem* item, double* px, double* py);
+
+void
+ganv_item_i2w_pair(GanvItem* item, double* x1, double* y1, double* x2, double* y2);
+
+void
+ganv_item_invoke_update(GanvItem* item, int flags);
+
+void
+ganv_item_emit_event(GanvItem* item, GdkEvent* event, gint* finished);
+
+void
+ganv_canvas_request_update(GanvCanvas* canvas);
+
+int
+ganv_canvas_emit_event(GanvCanvas* canvas, GdkEvent* event);
+
+void
+ganv_canvas_set_need_repick(GanvCanvas* canvas);
+
+void
+ganv_canvas_forget_item(GanvCanvas* canvas, GanvItem* item);
+
+void
+ganv_canvas_grab_focus(GanvCanvas* canvas, GanvItem* item);
+
+void
+ganv_canvas_get_zoom_offsets(GanvCanvas* canvas, int* x, int* y);
+
+int
+ganv_canvas_grab_item(GanvItem* item, guint event_mask, GdkCursor* cursor, guint32 etime);
+
+void
+ganv_canvas_ungrab_item(GanvItem* item, guint32 etime);
+
+/* Request a redraw of the specified rectangle in canvas coordinates */
+void
+ganv_canvas_request_redraw_c(GanvCanvas* canvas,
+ int x1, int y1, int x2, int y2);
+
+/* Request a redraw of the specified rectangle in world coordinates */
+void
+ganv_canvas_request_redraw_w(GanvCanvas* canvas,
+ double x1, double y1, double x2, double y2);
+
+PortOrderCtx
+ganv_canvas_get_port_order(GanvCanvas* canvas);
+
+gboolean
+ganv_canvas_exporting(GanvCanvas* canvas);
+
+/* Edge */
+
+void
+ganv_edge_update_location(GanvEdge* edge);
+
+void
+ganv_edge_get_coords(const GanvEdge* edge, GanvEdgeCoords* coords);
+
+void
+ganv_edge_request_redraw(GanvItem* item,
+ const GanvEdgeCoords* coords);
+
+void
+ganv_edge_tick(GanvEdge* edge, double seconds);
+
+/* Box */
+
+void
+ganv_box_path(GanvBox* box,
+ cairo_t* cr, double x1, double y1, double x2, double y2,
+ double dr);
+
+void
+ganv_box_request_redraw(GanvItem* item,
+ const GanvBoxCoords* coords,
+ gboolean world);
+
+/* Port */
+
+void
+ganv_port_set_control_value_internal(GanvPort* port,
+ float value);
+
+void
+ganv_port_set_direction(GanvPort* port,
+ GanvDirection direction);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* GANV_PRIVATE_H */
diff --git a/src/ganv_bench.cpp b/src/ganv_bench.cpp
new file mode 100644
index 0000000..b1bdefb
--- /dev/null
+++ b/src/ganv_bench.cpp
@@ -0,0 +1,178 @@
+/* This file is part of Ganv.
+ * Copyright 2013 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <glibmm.h>
+#include <gtkmm/main.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/window.h>
+
+#include "ganv/ganv.hpp"
+
+using namespace std;
+using namespace Ganv;
+
+static const int MAX_NUM_PORTS = 16;
+
+vector<Node*> ins;
+vector<Node*> outs;
+
+static Module*
+make_module(Canvas* canvas)
+{
+ char name[8];
+
+ snprintf(name, 8, "mod%d", rand() % 10000);
+ Module* m(new Module(*canvas, name,
+ rand() % (int)canvas->get_width(),
+ rand() % (int)canvas->get_height(),
+ true));
+
+ int n_ins = rand() % MAX_NUM_PORTS;
+ for (int i = 0; i < n_ins; ++i) {
+ snprintf(name, 8, "in%d", rand() % 10000);
+ Port* p(new Port(*m, name, true,
+ ((rand() % 0xFFFFFF) << 8) | 0xFF));
+ ins.push_back(p);
+ }
+
+ int n_outs = rand() % MAX_NUM_PORTS;
+ for (int i = 0; i < n_outs; ++i) {
+ snprintf(name, 8, "out%d", rand() % 10000);
+ Port* p(new Port(*m, name, false,
+ ((rand() % 0xFFFFFF) << 8) | 0xFF));
+ outs.push_back(p);
+ }
+
+ m->show();
+ return m;
+}
+
+static Circle*
+make_circle(Canvas* canvas)
+{
+ char name[8];
+
+ snprintf(name, 8, "%d", rand() % 10000);
+ Circle* e(new Circle(*canvas, name,
+ rand() % (int)canvas->get_width(),
+ rand() % (int)canvas->get_height()));
+
+ ins.push_back(e);
+ outs.push_back(e);
+
+ return e;
+}
+
+static bool
+quit()
+{
+ Gtk::Main::quit();
+ return true;
+}
+
+static int
+print_usage(const char* name)
+{
+ fprintf(stderr,
+ "USAGE: %s [OPTION]... CANVAS_W CANVAS_H N_MODULES N_CIRCLES N_EDGES\n\n"
+ "Options:\n"
+ " -o Remain open (do not close immediately)\n"
+ " -a Arrange canvas\n"
+ " -s Straight edges\n",
+ name);
+ return 1;
+}
+
+int
+main(int argc, char** argv)
+{
+ if (argc < 5) {
+ return print_usage(argv[0]);
+ }
+
+ int arg = 1;
+
+ bool remain_open = false;
+ bool arrange = false;
+ bool straight = false;
+ for (; arg < argc && argv[arg][0] == '-'; ++arg) {
+ if (argv[arg][1] == 'o') {
+ remain_open = true;
+ } else if (argv[arg][1] == 'a') {
+ arrange = true;
+ } else if (argv[arg][1] == 's') {
+ straight = true;
+ } else {
+ return print_usage(argv[0]);
+ }
+ }
+
+ const int canvas_w = atoi(argv[arg++]);
+ const int canvas_h = atoi(argv[arg++]);
+
+ if (argc - arg < 3) {
+ return print_usage(argv[0]);
+ }
+
+ const int n_modules = atoi(argv[arg++]);
+ const int n_circles = atoi(argv[arg++]);
+ const int n_edges = atoi(argv[arg++]);
+
+ srand(time(NULL));
+
+ Gtk::Main kit(argc, argv);
+
+ Gtk::Window window;
+ Gtk::ScrolledWindow* scroller = Gtk::manage(new Gtk::ScrolledWindow());
+
+ Canvas* canvas = new Canvas(canvas_w, canvas_h);
+ scroller->add(canvas->widget());
+ window.add(*scroller);
+
+ window.show_all();
+
+ for (int i = 0; i < n_modules; ++i) {
+ make_module(canvas);
+ }
+
+ for (int i = 0; i < n_circles; ++i) {
+ make_circle(canvas);
+ }
+
+ for (int i = 0; i < n_edges; ++i) {
+ Node* src = outs[rand() % outs.size()];
+ Node* dst = ins[rand() % ins.size()];
+ Edge* c = new Edge(*canvas, src, dst, 0x808080FF);
+ if (straight) {
+ c->set_curved(false);
+ }
+ }
+
+ if (arrange) {
+ canvas->arrange();
+ }
+
+ if (!remain_open) {
+ Glib::signal_idle().connect(sigc::ptr_fun(quit));
+ }
+
+ Gtk::Main::run(window);
+
+ return 0;
+}
diff --git a/src/ganv_test.c b/src/ganv_test.c
new file mode 100644
index 0000000..ec1b0a8
--- /dev/null
+++ b/src/ganv_test.c
@@ -0,0 +1,119 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2013 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <gtk/gtk.h>
+
+#include "ganv/ganv.h"
+
+static void
+on_window_destroy(GtkWidget* widget, void* data)
+{
+ gtk_main_quit();
+}
+
+static void
+on_connect(GanvCanvas* canvas, GanvNode* tail, GanvNode* head, void* data)
+{
+ ganv_edge_new(canvas, tail, head, "color", 0xFFFFFFFF, NULL);
+}
+
+static void
+on_disconnect(GanvCanvas* canvas, GanvNode* tail, GanvNode* head, void* data)
+{
+ ganv_canvas_remove_edge_between(canvas, tail, head);
+}
+
+static void
+on_value_changed(GanvPort* port, double value, void* data)
+{
+ fprintf(stderr, "Value changed: port %p = %lf\n", (void*)port, value);
+}
+
+int
+main(int argc, char** argv)
+{
+ gtk_init(&argc, &argv);
+
+ GtkWindow* win = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
+ gtk_window_set_title(win, "Ganv Test");
+ g_signal_connect(win, "destroy",
+ G_CALLBACK(on_window_destroy), NULL);
+
+ GanvCanvas* canvas = ganv_canvas_new(1024, 768);
+ gtk_container_add(GTK_CONTAINER(win), GTK_WIDGET(canvas));
+
+ GanvCircle* circle = ganv_circle_new(canvas,
+ "x", 400.0,
+ "y", 400.0,
+ "draggable", TRUE,
+ "label", "state",
+ "radius", 32.0,
+ NULL);
+ ganv_item_show(GANV_ITEM(circle));
+
+ GanvModule* module = ganv_module_new(canvas,
+ "x", 10.0,
+ "y", 10.0,
+ "draggable", TRUE,
+ "label", "test",
+ NULL);
+
+ ganv_port_new(module, FALSE,
+ "label", "Signal",
+ NULL);
+
+ GanvPort* cport = ganv_port_new(module, TRUE,
+ "label", "Control",
+ NULL);
+ ganv_port_show_control(cport);
+ g_signal_connect(cport, "value-changed",
+ G_CALLBACK(on_value_changed), NULL);
+
+ //GtkWidget* entry = gtk_entry_new();
+ //ganv_module_embed(module, entry);
+
+ GanvPort* tport = ganv_port_new(module, TRUE,
+ "label", "Toggle",
+ NULL);
+ ganv_port_show_control(tport);
+ ganv_port_set_control_is_toggle(tport, TRUE);
+
+ ganv_item_show(GANV_ITEM(module));
+
+ GanvModule* module2 = ganv_module_new(canvas,
+ "x", 200.0,
+ "y", 10.0,
+ "draggable", TRUE,
+ "label", "test2",
+ NULL);
+
+ ganv_port_new(module2, TRUE,
+ "label", "Signal",
+ NULL);
+
+ g_signal_connect(canvas, "connect",
+ G_CALLBACK(on_connect), canvas);
+
+ g_signal_connect(canvas, "disconnect",
+ G_CALLBACK(on_disconnect), canvas);
+
+ ganv_item_show(GANV_ITEM(module2));
+
+ gtk_widget_show_all(GTK_WIDGET(win));
+ gtk_window_present(win);
+ gtk_main();
+
+ return 0;
+}
diff --git a/src/ganv_test.py b/src/ganv_test.py
new file mode 100755
index 0000000..1ea53c2
--- /dev/null
+++ b/src/ganv_test.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+from gi.repository import Ganv, Gtk
+
+win = Gtk.Window()
+win.set_title("Ganv Python Test")
+win.connect("destroy", lambda obj: Gtk.main_quit())
+
+canvas = Ganv.Canvas.new(1024, 768)
+module = Ganv.Module(canvas=canvas,
+ label="Test")
+# iport = Ganv.Port(module=module,
+# is_input=True,
+# label="In")
+# oport = Ganv.Port(module=module,
+# is_input=False,
+# label="In")
+
+win.add(canvas)
+win.show_all()
+win.present()
+Gtk.main()
diff --git a/src/gettext.h b/src/gettext.h
new file mode 100644
index 0000000..d32bb70
--- /dev/null
+++ b/src/gettext.h
@@ -0,0 +1,26 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2013 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GANV_GETTEXT_H
+#define GANV_GETTEXT_H
+
+#ifdef ENABLE_NLS
+# include <libintl.h>
+# define _(str) dgettext("ganv", str)
+#else
+# define _(str) str
+#endif
+
+#endif /* GANV_GETTEXT_H */
diff --git a/src/group.c b/src/group.c
new file mode 100644
index 0000000..503e57b
--- /dev/null
+++ b/src/group.c
@@ -0,0 +1,446 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2015 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Based on GnomeCanvasGroup, by Federico Mena <federico@nuclecu.unam.mx>
+ * and Raph Levien <raph@gimp.org>
+ * Copyright 1997-2000 Free Software Foundation
+ */
+
+#include <math.h>
+
+#include "ganv/canvas.h"
+#include "ganv/group.h"
+
+#include "./gettext.h"
+#include "./ganv-private.h"
+
+enum {
+ GROUP_PROP_0
+};
+
+G_DEFINE_TYPE_WITH_CODE(GanvGroup, ganv_group, GANV_TYPE_ITEM,
+ G_ADD_PRIVATE(GanvGroup))
+
+static GanvItemClass* group_parent_class;
+
+static void
+ganv_group_init(GanvGroup* group)
+{
+ GanvGroupPrivate* impl = ganv_group_get_instance_private(group);
+
+ group->impl = impl;
+ group->impl->item_list = NULL;
+ group->impl->item_list_end = NULL;
+}
+
+static void
+ganv_group_set_property(GObject* gobject, guint param_id,
+ const GValue* value, GParamSpec* pspec)
+{
+ g_return_if_fail(GANV_IS_GROUP(gobject));
+
+ switch (param_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, param_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_group_get_property(GObject* gobject, guint param_id,
+ GValue* value, GParamSpec* pspec)
+{
+ g_return_if_fail(GANV_IS_GROUP(gobject));
+
+ switch (param_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, param_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_group_destroy(GtkObject* object)
+{
+ GanvGroup* group;
+
+ g_return_if_fail(GANV_IS_GROUP(object));
+
+ group = GANV_GROUP(object);
+
+ while (group->impl->item_list) {
+ // child is unref'ed by the child's group_remove().
+ gtk_object_destroy(GTK_OBJECT(group->impl->item_list->data));
+ }
+ if (GTK_OBJECT_CLASS(group_parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(group_parent_class)->destroy)(object);
+ }
+}
+
+static void
+ganv_group_update(GanvItem* item, int flags)
+{
+ GanvGroup* group = GANV_GROUP(item);
+
+ double min_x = 0.0;
+ double min_y = 0.0;
+ double max_x = 0.0;
+ double max_y = 0.0;
+
+ for (GList* list = group->impl->item_list; list; list = list->next) {
+ GanvItem* i = (GanvItem*)list->data;
+
+ ganv_item_invoke_update(i, flags);
+
+ min_x = fmin(min_x, fmin(i->impl->x1, i->impl->x2));
+ min_y = fmin(min_y, fmin(i->impl->y1, i->impl->y2));
+ max_x = fmax(max_x, fmax(i->impl->x1, i->impl->x2));
+ max_y = fmax(max_y, fmax(i->impl->y2, i->impl->y2));
+ }
+ item->impl->x1 = min_x;
+ item->impl->y1 = min_y;
+ item->impl->x2 = max_x;
+ item->impl->y2 = max_y;
+
+ (*group_parent_class->update)(item, flags);
+}
+
+static void
+ganv_group_realize(GanvItem* item)
+{
+ GanvGroup* group;
+ GList* list;
+ GanvItem* i;
+
+ group = GANV_GROUP(item);
+
+ for (list = group->impl->item_list; list; list = list->next) {
+ i = (GanvItem*)list->data;
+
+ if (!(i->object.flags & GANV_ITEM_REALIZED)) {
+ (*GANV_ITEM_GET_CLASS(i)->realize)(i);
+ }
+ }
+
+ (*group_parent_class->realize)(item);
+}
+
+static void
+ganv_group_unrealize(GanvItem* item)
+{
+ GanvGroup* group;
+ GList* list;
+ GanvItem* i;
+
+ group = GANV_GROUP(item);
+
+ for (list = group->impl->item_list; list; list = list->next) {
+ i = (GanvItem*)list->data;
+
+ if (i->object.flags & GANV_ITEM_REALIZED) {
+ (*GANV_ITEM_GET_CLASS(i)->unrealize)(i);
+ }
+ }
+
+ (*group_parent_class->unrealize)(item);
+}
+
+static void
+ganv_group_map(GanvItem* item)
+{
+ GanvGroup* group;
+ GList* list;
+ GanvItem* i;
+
+ group = GANV_GROUP(item);
+
+ for (list = group->impl->item_list; list; list = list->next) {
+ i = (GanvItem*)list->data;
+
+ if (!(i->object.flags & GANV_ITEM_MAPPED)) {
+ (*GANV_ITEM_GET_CLASS(i)->map)(i);
+ }
+ }
+
+ (*group_parent_class->map)(item);
+}
+
+static void
+ganv_group_unmap(GanvItem* item)
+{
+ GanvGroup* group;
+ GList* list;
+ GanvItem* i;
+
+ group = GANV_GROUP(item);
+
+ for (list = group->impl->item_list; list; list = list->next) {
+ i = (GanvItem*)list->data;
+
+ if (i->object.flags & GANV_ITEM_MAPPED) {
+ (*GANV_ITEM_GET_CLASS(i)->unmap)(i);
+ }
+ }
+
+ (*group_parent_class->unmap)(item);
+}
+
+static void
+ganv_group_draw(GanvItem* item,
+ cairo_t* cr, double cx, double cy, double cw, double ch)
+{
+ GanvGroup* group = GANV_GROUP(item);
+
+ // TODO: Layered drawing
+
+ for (GList* list = group->impl->item_list; list; list = list->next) {
+ GanvItem* child = (GanvItem*)list->data;
+
+ if (((child->object.flags & GANV_ITEM_VISIBLE)
+ && ((child->impl->x1 < (cx + cw))
+ && (child->impl->y1 < (cy + ch))
+ && (child->impl->x2 > cx)
+ && (child->impl->y2 > cy)))) {
+ if (GANV_ITEM_GET_CLASS(child)->draw) {
+ (*GANV_ITEM_GET_CLASS(child)->draw)(
+ child, cr, cx, cy, cw, ch);
+ }
+ }
+ }
+}
+
+static double
+ganv_group_point(GanvItem* item, double x, double y, GanvItem** actual_item)
+{
+ GanvGroup* group = GANV_GROUP(item);
+
+ const double x1 = x - GANV_CLOSE_ENOUGH;
+ const double y1 = y - GANV_CLOSE_ENOUGH;
+ const double x2 = x + GANV_CLOSE_ENOUGH;
+ const double y2 = y + GANV_CLOSE_ENOUGH;
+
+ double dist = 0.0;
+ double best = 0.0;
+
+ *actual_item = NULL;
+
+ for (GList* list = group->impl->item_list; list; list = list->next) {
+ GanvItem* child = (GanvItem*)list->data;
+ if ((child->impl->x1 > x2) || (child->impl->y1 > y2) || (child->impl->x2 < x1) || (child->impl->y2 < y1)) {
+ continue;
+ }
+
+ GanvItem* point_item = NULL;
+
+ int has_point = FALSE;
+ if ((child->object.flags & GANV_ITEM_VISIBLE)
+ && GANV_ITEM_GET_CLASS(child)->point) {
+ dist = GANV_ITEM_GET_CLASS(child)->point(
+ child,
+ x - child->impl->x, y - child->impl->y,
+ &point_item);
+ has_point = TRUE;
+ }
+
+ if (has_point
+ && point_item
+ && ((int)(dist + 0.5) <= GANV_CLOSE_ENOUGH)) {
+ best = dist;
+ *actual_item = point_item;
+ }
+ }
+
+ if (*actual_item) {
+ return best;
+ } else {
+ *actual_item = item;
+ return 0.0;
+ }
+}
+
+/* Get bounds of child item in group-relative coordinates. */
+static void
+get_child_bounds(GanvItem* child, double* x1, double* y1, double* x2, double* y2)
+{
+ ganv_item_get_bounds(child, x1, y1, x2, y2);
+
+ // Make bounds relative to the item's parent coordinate system
+ *x1 -= child->impl->x;
+ *y1 -= child->impl->y;
+ *x2 -= child->impl->x;
+ *y2 -= child->impl->y;
+}
+
+static void
+ganv_group_bounds(GanvItem* item, double* x1, double* y1, double* x2, double* y2)
+{
+ GanvGroup* group;
+ GanvItem* child;
+ GList* list;
+ double tx1, ty1, tx2, ty2;
+ double minx, miny, maxx, maxy;
+ int set;
+
+ group = GANV_GROUP(item);
+
+ /* Get the bounds of the first visible item */
+
+ child = NULL; /* Unnecessary but eliminates a warning. */
+
+ set = FALSE;
+
+ for (list = group->impl->item_list; list; list = list->next) {
+ child = (GanvItem*)list->data;
+
+ if (child->object.flags & GANV_ITEM_VISIBLE) {
+ set = TRUE;
+ get_child_bounds(child, &minx, &miny, &maxx, &maxy);
+ break;
+ }
+ }
+
+ /* If there were no visible items, return an empty bounding box */
+
+ if (!set) {
+ *x1 = *y1 = *x2 = *y2 = 0.0;
+ return;
+ }
+
+ /* Now we can grow the bounds using the rest of the items */
+
+ list = list->next;
+
+ for (; list; list = list->next) {
+ child = (GanvItem*)list->data;
+
+ if (!(child->object.flags & GANV_ITEM_VISIBLE)) {
+ continue;
+ }
+
+ get_child_bounds(child, &tx1, &ty1, &tx2, &ty2);
+
+ if (tx1 < minx) {
+ minx = tx1;
+ }
+
+ if (ty1 < miny) {
+ miny = ty1;
+ }
+
+ if (tx2 > maxx) {
+ maxx = tx2;
+ }
+
+ if (ty2 > maxy) {
+ maxy = ty2;
+ }
+ }
+
+ *x1 = minx;
+ *y1 = miny;
+ *x2 = maxx;
+ *y2 = maxy;
+}
+
+static void
+ganv_group_add(GanvItem* parent, GanvItem* item)
+{
+ GanvGroup* group = GANV_GROUP(parent);
+ g_object_ref_sink(G_OBJECT(item));
+
+ if (!group->impl->item_list) {
+ group->impl->item_list = g_list_append(group->impl->item_list, item);
+ group->impl->item_list_end = group->impl->item_list;
+ } else {
+ group->impl->item_list_end = g_list_append(group->impl->item_list_end, item)->next;
+ }
+
+ if (group->item.object.flags & GANV_ITEM_REALIZED) {
+ (*GANV_ITEM_GET_CLASS(item)->realize)(item);
+ }
+
+ if (group->item.object.flags & GANV_ITEM_MAPPED) {
+ (*GANV_ITEM_GET_CLASS(item)->map)(item);
+ }
+
+ g_object_notify(G_OBJECT(item), "parent");
+}
+
+static void
+ganv_group_remove(GanvItem* parent, GanvItem* item)
+{
+ GanvGroup* group = GANV_GROUP(parent);
+ GList* children;
+
+ g_return_if_fail(GANV_IS_GROUP(group));
+ g_return_if_fail(GANV_IS_ITEM(item));
+
+ for (children = group->impl->item_list; children; children = children->next) {
+ if (children->data == item) {
+ if (item->object.flags & GANV_ITEM_MAPPED) {
+ (*GANV_ITEM_GET_CLASS(item)->unmap)(item);
+ }
+
+ if (item->object.flags & GANV_ITEM_REALIZED) {
+ (*GANV_ITEM_GET_CLASS(item)->unrealize)(item);
+ }
+
+ /* Unparent the child */
+
+ item->impl->parent = NULL;
+ g_object_unref(G_OBJECT(item));
+
+ /* Remove it from the list */
+
+ if (children == group->impl->item_list_end) {
+ group->impl->item_list_end = children->prev;
+ }
+
+ group->impl->item_list = g_list_remove_link(group->impl->item_list, children);
+ g_list_free(children);
+ break;
+ }
+ }
+}
+
+static void
+ganv_group_class_init(GanvGroupClass* klass)
+{
+ GObjectClass* gobject_class;
+ GtkObjectClass* object_class;
+ GanvItemClass* item_class;
+
+ gobject_class = (GObjectClass*)klass;
+ object_class = (GtkObjectClass*)klass;
+ item_class = (GanvItemClass*)klass;
+
+ group_parent_class = (GanvItemClass*)g_type_class_peek_parent(klass);
+
+ gobject_class->set_property = ganv_group_set_property;
+ gobject_class->get_property = ganv_group_get_property;
+
+ object_class->destroy = ganv_group_destroy;
+
+ item_class->add = ganv_group_add;
+ item_class->remove = ganv_group_remove;
+ item_class->update = ganv_group_update;
+ item_class->realize = ganv_group_realize;
+ item_class->unrealize = ganv_group_unrealize;
+ item_class->map = ganv_group_map;
+ item_class->unmap = ganv_group_unmap;
+ item_class->draw = ganv_group_draw;
+ item_class->point = ganv_group_point;
+ item_class->bounds = ganv_group_bounds;
+}
diff --git a/src/item.c b/src/item.c
new file mode 100644
index 0000000..a458acd
--- /dev/null
+++ b/src/item.c
@@ -0,0 +1,707 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2016 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Based on GnomeCanvas, by Federico Mena <federico@nuclecu.unam.mx>
+ * and Raph Levien <raph@gimp.org>
+ * Copyright 1997-2000 Free Software Foundation
+ */
+
+#include "ganv/canvas.h"
+#include "ganv/node.h"
+
+#include "./boilerplate.h"
+#include "./ganv-marshal.h"
+#include "./ganv-private.h"
+#include "./gettext.h"
+
+/* All canvas items are derived from GanvItem. The only information a GanvItem
+ * contains is its parent canvas, its parent canvas item, its bounding box in
+ * world coordinates, and its current affine transformation.
+ *
+ * Items inside a canvas are organized in a tree, where leaves are items
+ * without any children. Each canvas has a single root item, which can be
+ * obtained with the ganv_canvas_base_get_root() function.
+ *
+ * The abstract GanvItem class does not have any configurable or queryable
+ * attributes.
+ */
+
+/* Update flags for items */
+enum {
+ GANV_CANVAS_UPDATE_REQUESTED = 1 << 0,
+ GANV_CANVAS_UPDATE_AFFINE = 1 << 1,
+ GANV_CANVAS_UPDATE_VISIBILITY = 1 << 2
+};
+
+#define GCI_UPDATE_MASK (GANV_CANVAS_UPDATE_REQUESTED \
+ | GANV_CANVAS_UPDATE_AFFINE \
+ | GANV_CANVAS_UPDATE_VISIBILITY)
+
+enum {
+ ITEM_PROP_0,
+ ITEM_PROP_PARENT,
+ ITEM_PROP_X,
+ ITEM_PROP_Y,
+ ITEM_PROP_MANAGED
+};
+
+enum {
+ ITEM_EVENT,
+ ITEM_LAST_SIGNAL
+};
+
+static guint item_signals[ITEM_LAST_SIGNAL];
+
+G_DEFINE_TYPE_WITH_CODE(GanvItem, ganv_item, GTK_TYPE_OBJECT,
+ G_ADD_PRIVATE(GanvItem))
+
+static GtkObjectClass* item_parent_class;
+
+/* Object initialization function for GanvItem */
+static void
+ganv_item_init(GanvItem* item)
+{
+ GanvItemPrivate* impl = ganv_item_get_instance_private(item);
+
+ item->object.flags |= GANV_ITEM_VISIBLE;
+ item->impl = impl;
+ item->impl->managed = FALSE;
+ item->impl->wrapper = NULL;
+}
+
+/**
+ * ganv_item_new:
+ * @parent: The parent group for the new item.
+ * @type: The object type of the item.
+ * @first_arg_name: A list of object argument name/value pairs, NULL-terminated,
+ * used to configure the item. For example, "fill_color", "black",
+ * "width_units", 5.0, NULL.
+ * @...: first argument value, second argument name, second argument value, ...
+ *
+ * Creates a new canvas item with @parent as its parent group. The item is
+ * created at the top of its parent's stack, and starts up as visible. The item
+ * is of the specified @type, for example, it can be
+ * ganv_canvas_rect_get_type(). The list of object arguments/value pairs is
+ * used to configure the item. If you need to pass construct time parameters, you
+ * should use g_object_new() to pass the parameters and
+ * ganv_item_construct() to set up the canvas item.
+ *
+ * Return value: (transfer full): The newly-created item.
+ **/
+GanvItem*
+ganv_item_new(GanvItem* parent, GType type, const gchar* first_arg_name, ...)
+{
+ g_return_val_if_fail(g_type_is_a(type, ganv_item_get_type()), NULL);
+
+ GanvItem* item = GANV_ITEM(g_object_new(type, NULL));
+
+ va_list args;
+ va_start(args, first_arg_name);
+ ganv_item_construct(item, parent, first_arg_name, args);
+ va_end(args);
+
+ return item;
+}
+
+/* Performs post-creation operations on a canvas item (adding it to its parent
+ * group, etc.)
+ */
+static void
+item_post_create_setup(GanvItem* item)
+{
+ GanvItemClass* parent_class = GANV_ITEM_GET_CLASS(item->impl->parent);
+ if (!item->impl->managed) {
+ if (parent_class->add) {
+ parent_class->add(item->impl->parent, item);
+ } else {
+ g_warning("item added to non-parent item\n");
+ }
+ }
+ ganv_canvas_request_redraw_w(item->impl->canvas,
+ item->impl->x1, item->impl->y1,
+ item->impl->x2 + 1, item->impl->y2 + 1);
+ ganv_canvas_set_need_repick(item->impl->canvas);
+}
+
+static void
+ganv_item_set_property(GObject* object,
+ guint prop_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_ITEM(object));
+
+ GanvItem* item = GANV_ITEM(object);
+
+ switch (prop_id) {
+ case ITEM_PROP_PARENT:
+ if (item->impl->parent != NULL) {
+ g_warning("Cannot set `parent' argument after item has "
+ "already been constructed.");
+ } else if (g_value_get_object(value)) {
+ item->impl->parent = GANV_ITEM(g_value_get_object(value));
+ item->impl->canvas = item->impl->parent->impl->canvas;
+ item_post_create_setup(item);
+ }
+ break;
+ case ITEM_PROP_X:
+ item->impl->x = g_value_get_double(value);
+ ganv_item_request_update(item);
+ break;
+ case ITEM_PROP_Y:
+ item->impl->y = g_value_get_double(value);
+ ganv_item_request_update(item);
+ break;
+ case ITEM_PROP_MANAGED:
+ item->impl->managed = g_value_get_boolean(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_item_get_property(GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_ITEM(object));
+
+ GanvItem* item = GANV_ITEM(object);
+
+ switch (prop_id) {
+ case ITEM_PROP_PARENT:
+ g_value_set_object(value, item->impl->parent);
+ break;
+ case ITEM_PROP_X:
+ g_value_set_double(value, item->impl->x);
+ break;
+ case ITEM_PROP_Y:
+ g_value_set_double(value, item->impl->y);
+ break;
+ case ITEM_PROP_MANAGED:
+ g_value_set_boolean(value, item->impl->managed);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+/**
+ * ganv_item_construct:
+ * @item: An unconstructed canvas item.
+ * @parent: The parent group for the item.
+ * @first_arg_name: The name of the first argument for configuring the item.
+ * @args: The list of arguments used to configure the item.
+ *
+ * Constructs a canvas item; meant for use only by item implementations.
+ **/
+void
+ganv_item_construct(GanvItem* item, GanvItem* parent,
+ const gchar* first_arg_name, va_list args)
+{
+ g_return_if_fail(GANV_IS_ITEM(item));
+
+ item->impl->parent = parent;
+ item->impl->wrapper = NULL;
+ item->impl->canvas = item->impl->parent->impl->canvas;
+ item->impl->layer = 0;
+
+ g_object_set_valist(G_OBJECT(item), first_arg_name, args);
+
+ item_post_create_setup(item);
+}
+
+/* If the item is visible, requests a redraw of it. */
+static void
+redraw_if_visible(GanvItem* item)
+{
+ if (item->object.flags & GANV_ITEM_VISIBLE) {
+ ganv_canvas_request_redraw_w(item->impl->canvas,
+ item->impl->x1, item->impl->y1,
+ item->impl->x2 + 1, item->impl->y2 + 1);
+ }
+}
+
+/* Standard object dispose function for canvas items */
+static void
+ganv_item_dispose(GObject* object)
+{
+ GanvItem* item;
+
+ g_return_if_fail(GANV_IS_ITEM(object));
+
+ item = GANV_ITEM(object);
+
+ if (item->impl->canvas) {
+ redraw_if_visible(item);
+ ganv_canvas_forget_item(item->impl->canvas, item);
+ }
+
+ /* Normal destroy stuff */
+
+ if (item->object.flags & GANV_ITEM_MAPPED) {
+ (*GANV_ITEM_GET_CLASS(item)->unmap)(item);
+ }
+
+ if (item->object.flags & GANV_ITEM_REALIZED) {
+ (*GANV_ITEM_GET_CLASS(item)->unrealize)(item);
+ }
+
+ if (!item->impl->managed && item->impl->parent) {
+ if (GANV_ITEM_GET_CLASS(item->impl->parent)->remove) {
+ GANV_ITEM_GET_CLASS(item->impl->parent)->remove(item->impl->parent, item);
+ } else {
+ fprintf(stderr, "warning: Item parent has no remove method\n");
+ }
+ }
+
+ G_OBJECT_CLASS(item_parent_class)->dispose(object);
+ /* items should remove any reference to item->impl->canvas after the
+ first ::destroy */
+ item->impl->canvas = NULL;
+}
+
+/* Realize handler for canvas items */
+static void
+ganv_item_realize(GanvItem* item)
+{
+ GTK_OBJECT_SET_FLAGS(item, GANV_ITEM_REALIZED);
+
+ ganv_item_request_update(item);
+}
+
+/* Unrealize handler for canvas items */
+static void
+ganv_item_unrealize(GanvItem* item)
+{
+ GTK_OBJECT_UNSET_FLAGS(item, GANV_ITEM_REALIZED);
+}
+
+/* Map handler for canvas items */
+static void
+ganv_item_map(GanvItem* item)
+{
+ GTK_OBJECT_SET_FLAGS(item, GANV_ITEM_MAPPED);
+}
+
+/* Unmap handler for canvas items */
+static void
+ganv_item_unmap(GanvItem* item)
+{
+ GTK_OBJECT_UNSET_FLAGS(item, GANV_ITEM_MAPPED);
+}
+
+/* Update handler for canvas items */
+static void
+ganv_item_update(GanvItem* item, int flags)
+{
+ GTK_OBJECT_UNSET_FLAGS(item, GANV_ITEM_NEED_UPDATE);
+ GTK_OBJECT_UNSET_FLAGS(item, GANV_ITEM_NEED_VIS);
+}
+
+/* Point handler for canvas items */
+static double
+ganv_item_point(GanvItem* item, double x, double y, GanvItem** actual_item)
+{
+ *actual_item = NULL;
+ return G_MAXDOUBLE;
+}
+
+void
+ganv_item_invoke_update(GanvItem* item, int flags)
+{
+ int child_flags = flags;
+
+ /* apply object flags to child flags */
+
+ child_flags &= ~GANV_CANVAS_UPDATE_REQUESTED;
+
+ if (item->object.flags & GANV_ITEM_NEED_UPDATE) {
+ child_flags |= GANV_CANVAS_UPDATE_REQUESTED;
+ }
+
+ if (item->object.flags & GANV_ITEM_NEED_VIS) {
+ child_flags |= GANV_CANVAS_UPDATE_VISIBILITY;
+ }
+
+ if (child_flags & GCI_UPDATE_MASK) {
+ if (GANV_ITEM_GET_CLASS(item)->update) {
+ GANV_ITEM_GET_CLASS(item)->update(item, child_flags);
+ g_assert(!(GTK_OBJECT_FLAGS(item) & GANV_ITEM_NEED_UPDATE));
+ }
+ }
+}
+
+/**
+ * ganv_item_set:
+ * @item: A canvas item.
+ * @first_arg_name: The list of object argument name/value pairs used to configure the item.
+ * @...: first argument value, second argument name, second argument value, ...
+ *
+ * Configures a canvas item. The arguments in the item are set to the specified
+ * values, and the item is repainted as appropriate.
+ **/
+void
+ganv_item_set(GanvItem* item, const gchar* first_arg_name, ...)
+{
+ va_list args;
+
+ va_start(args, first_arg_name);
+ ganv_item_set_valist(item, first_arg_name, args);
+ va_end(args);
+}
+
+/**
+ * ganv_item_set_valist:
+ * @item: A canvas item.
+ * @first_arg_name: The name of the first argument used to configure the item.
+ * @args: The list of object argument name/value pairs used to configure the item.
+ *
+ * Configures a canvas item. The arguments in the item are set to the specified
+ * values, and the item is repainted as appropriate.
+ **/
+void
+ganv_item_set_valist(GanvItem* item, const gchar* first_arg_name, va_list args)
+{
+ g_return_if_fail(GANV_IS_ITEM(item));
+
+ g_object_set_valist(G_OBJECT(item), first_arg_name, args);
+
+ ganv_canvas_set_need_repick(item->impl->canvas);
+}
+
+GanvCanvas*
+ganv_item_get_canvas(GanvItem* item)
+{
+ return item->impl->canvas;
+}
+
+GanvItem*
+ganv_item_get_parent(GanvItem* item)
+{
+ return item->impl->parent;
+}
+
+void
+ganv_item_raise(GanvItem* item)
+{
+ ++item->impl->layer;
+}
+
+void
+ganv_item_lower(GanvItem* item)
+{
+ --item->impl->layer;
+}
+
+/**
+ * ganv_item_move:
+ * @item: A canvas item.
+ * @dx: Horizontal offset.
+ * @dy: Vertical offset.
+ **/
+void
+ganv_item_move(GanvItem* item, double dx, double dy)
+{
+ if (!item || !GANV_IS_ITEM(item)) {
+ return;
+ }
+
+ item->impl->x += dx;
+ item->impl->y += dy;
+
+ ganv_item_request_update(item);
+ ganv_canvas_set_need_repick(item->impl->canvas);
+}
+
+/**
+ * ganv_item_show:
+ * @item: A canvas item.
+ *
+ * Shows a canvas item. If the item was already shown, then no action is taken.
+ **/
+void
+ganv_item_show(GanvItem* item)
+{
+ g_return_if_fail(GANV_IS_ITEM(item));
+
+ if (!(item->object.flags & GANV_ITEM_VISIBLE)) {
+ item->object.flags |= GANV_ITEM_VISIBLE;
+ ganv_canvas_request_redraw_w(item->impl->canvas,
+ item->impl->x1, item->impl->y1,
+ item->impl->x2 + 1, item->impl->y2 + 1);
+ ganv_canvas_set_need_repick(item->impl->canvas);
+ }
+}
+
+/**
+ * ganv_item_hide:
+ * @item: A canvas item.
+ *
+ * Hides a canvas item. If the item was already hidden, then no action is
+ * taken.
+ **/
+void
+ganv_item_hide(GanvItem* item)
+{
+ g_return_if_fail(GANV_IS_ITEM(item));
+
+ if (item->object.flags & GANV_ITEM_VISIBLE) {
+ item->object.flags &= ~GANV_ITEM_VISIBLE;
+ ganv_canvas_request_redraw_w(item->impl->canvas,
+ item->impl->x1, item->impl->y1,
+ item->impl->x2 + 1, item->impl->y2 + 1);
+ ganv_canvas_set_need_repick(item->impl->canvas);
+ }
+}
+
+void
+ganv_item_i2w_offset(GanvItem* item, double* px, double* py)
+{
+ double x = 0.0;
+ double y = 0.0;
+ while (item) {
+ x += item->impl->x;
+ y += item->impl->y;
+ item = item->impl->parent;
+ }
+ *px = x;
+ *py = y;
+}
+
+/**
+ * ganv_item_i2w:
+ * @item: A canvas item.
+ * @x: X coordinate to convert (input/output value).
+ * @y: Y coordinate to convert (input/output value).
+ *
+ * Converts a coordinate pair from item-relative coordinates to world
+ * coordinates.
+ **/
+void
+ganv_item_i2w(GanvItem* item, double* x, double* y)
+{
+ /*g_return_if_fail(GANV_IS_ITEM(item));
+ g_return_if_fail(x != NULL);
+ g_return_if_fail(y != NULL);*/
+
+ double off_x;
+ double off_y;
+ ganv_item_i2w_offset(item, &off_x, &off_y);
+
+ *x += off_x;
+ *y += off_y;
+}
+
+void
+ganv_item_i2w_pair(GanvItem* item, double* x1, double* y1, double* x2, double* y2)
+{
+ double off_x;
+ double off_y;
+ ganv_item_i2w_offset(item, &off_x, &off_y);
+
+ *x1 += off_x;
+ *y1 += off_y;
+ *x2 += off_x;
+ *y2 += off_y;
+}
+
+/**
+ * ganv_item_w2i:
+ * @item: A canvas item.
+ * @x: X coordinate to convert (input/output value).
+ * @y: Y coordinate to convert (input/output value).
+ *
+ * Converts a coordinate pair from world coordinates to item-relative
+ * coordinates.
+ **/
+void
+ganv_item_w2i(GanvItem* item, double* x, double* y)
+{
+ double off_x;
+ double off_y;
+ ganv_item_i2w_offset(item, &off_x, &off_y);
+
+ *x -= off_x;
+ *y -= off_y;
+}
+
+/**
+ * ganv_item_grab_focus:
+ * @item: A canvas item.
+ *
+ * Makes the specified item take the keyboard focus, so all keyboard events will
+ * be sent to it. If the canvas widget itself did not have the focus, it grabs
+ * it as well.
+ **/
+void
+ganv_item_grab_focus(GanvItem* item)
+{
+ ganv_canvas_grab_focus(item->impl->canvas, item);
+}
+
+void
+ganv_item_emit_event(GanvItem* item, GdkEvent* event, gint* finished)
+{
+ g_signal_emit(item, item_signals[ITEM_EVENT], 0, event, finished);
+}
+
+static void
+ganv_item_default_bounds(GanvItem* item, double* x1, double* y1, double* x2, double* y2)
+{
+ *x1 = *y1 = *x2 = *y2 = 0.0;
+}
+
+/**
+ * ganv_item_get_bounds:
+ * @item: A canvas item.
+ * @x1: Leftmost edge of the bounding box (return value).
+ * @y1: Upper edge of the bounding box (return value).
+ * @x2: Rightmost edge of the bounding box (return value).
+ * @y2: Lower edge of the bounding box (return value).
+ *
+ * Queries the bounding box of a canvas item. The bounding box may not be
+ * exactly tight, but the canvas items will do the best they can. The bounds
+ * are returned in the coordinate system of the item's parent.
+ **/
+void
+ganv_item_get_bounds(GanvItem* item, double* x1, double* y1, double* x2, double* y2)
+{
+ GANV_ITEM_GET_CLASS(item)->bounds(item, x1, y1, x2, y2);
+}
+
+/**
+ * ganv_item_request_update:
+ * @item: A canvas item.
+ *
+ * To be used only by item implementations. Requests that the canvas queue an
+ * update for the specified item.
+ **/
+void
+ganv_item_request_update(GanvItem* item)
+{
+ if (!item->impl->canvas) {
+ /* Item is being / has been destroyed, ignore */
+ return;
+ }
+
+ item->object.flags |= GANV_ITEM_NEED_UPDATE;
+
+ if (item->impl->parent != NULL &&
+ !(item->impl->parent->object.flags & GANV_ITEM_NEED_UPDATE)) {
+ /* Recurse up the tree */
+ ganv_item_request_update(item->impl->parent);
+ } else {
+ /* Have reached the top of the tree, make sure the update call gets scheduled. */
+ ganv_canvas_request_update(item->impl->canvas);
+ }
+}
+
+void
+ganv_item_set_wrapper(GanvItem* item, void* wrapper)
+{
+ item->impl->wrapper = wrapper;
+}
+
+void*
+ganv_item_get_wrapper(GanvItem* item)
+{
+ return item->impl->wrapper;
+}
+
+static gboolean
+boolean_handled_accumulator(GSignalInvocationHint* ihint,
+ GValue* return_accu,
+ const GValue* handler_return,
+ gpointer dummy)
+{
+ gboolean continue_emission;
+ gboolean signal_handled;
+
+ signal_handled = g_value_get_boolean(handler_return);
+ g_value_set_boolean(return_accu, signal_handled);
+ continue_emission = !signal_handled;
+
+ return continue_emission;
+}
+
+/* Class initialization function for GanvItemClass */
+static void
+ganv_item_class_init(GanvItemClass* klass)
+{
+ GObjectClass* gobject_class;
+
+ gobject_class = (GObjectClass*)klass;
+
+ item_parent_class = (GtkObjectClass*)g_type_class_peek_parent(klass);
+
+ gobject_class->set_property = ganv_item_set_property;
+ gobject_class->get_property = ganv_item_get_property;
+
+ g_object_class_install_property
+ (gobject_class, ITEM_PROP_PARENT,
+ g_param_spec_object("parent", NULL, NULL,
+ GANV_TYPE_ITEM,
+ (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE)));
+
+ g_object_class_install_property
+ (gobject_class, ITEM_PROP_X,
+ g_param_spec_double("x",
+ _("X"),
+ _("X"),
+ -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
+ (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE)));
+ g_object_class_install_property
+ (gobject_class, ITEM_PROP_Y,
+ g_param_spec_double("y",
+ _("Y"),
+ _("Y"),
+ -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
+ (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE)));
+
+ g_object_class_install_property
+ (gobject_class, ITEM_PROP_MANAGED,
+ g_param_spec_boolean("managed",
+ _("Managed"),
+ _("Whether the item is managed by its parent"),
+ 0,
+ (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE)));
+
+ item_signals[ITEM_EVENT]
+ = g_signal_new("event",
+ G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET(GanvItemClass, event),
+ boolean_handled_accumulator, NULL,
+ ganv_marshal_BOOLEAN__BOXED,
+ G_TYPE_BOOLEAN, 1,
+ GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+ gobject_class->dispose = ganv_item_dispose;
+
+ klass->realize = ganv_item_realize;
+ klass->unrealize = ganv_item_unrealize;
+ klass->map = ganv_item_map;
+ klass->unmap = ganv_item_unmap;
+ klass->update = ganv_item_update;
+ klass->point = ganv_item_point;
+ klass->bounds = ganv_item_default_bounds;
+}
diff --git a/src/module.c b/src/module.c
new file mode 100644
index 0000000..bad930b
--- /dev/null
+++ b/src/module.c
@@ -0,0 +1,859 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2016 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "ganv/canvas.h"
+#include "ganv/module.h"
+#include "ganv/port.h"
+#include "ganv/widget.h"
+
+#include "./color.h"
+#include "./boilerplate.h"
+#include "./gettext.h"
+#include "./ganv-private.h"
+
+#define FOREACH_PORT(ports, i) \
+ for (GanvPort** i = (GanvPort**)ports->pdata; \
+ i != (GanvPort**)ports->pdata + ports->len; ++i)
+
+#define FOREACH_PORT_CONST(ports, i) \
+ for (const GanvPort** i = (const GanvPort**)ports->pdata; \
+ i != (const GanvPort**)ports->pdata + ports->len; ++i)
+
+static const double PAD = 2.0;
+static const double EDGE_PAD = 5.0;
+static const double MODULE_LABEL_PAD = 2.0;
+
+G_DEFINE_TYPE_WITH_CODE(GanvModule, ganv_module, GANV_TYPE_BOX,
+ G_ADD_PRIVATE(GanvModule))
+
+static GanvBoxClass* parent_class;
+
+enum {
+ PROP_0
+};
+
+static void
+ganv_module_init(GanvModule* module)
+{
+ GanvModulePrivate* impl = ganv_module_get_instance_private(module);
+
+ module->impl = impl;
+
+ GANV_NODE(module)->impl->can_head = FALSE;
+ GANV_NODE(module)->impl->can_tail = FALSE;
+
+ impl->ports = g_ptr_array_new();
+ impl->embed_item = NULL;
+ impl->embed_width = 0;
+ impl->embed_height = 0;
+ impl->widest_input = 0.0;
+ impl->widest_output = 0.0;
+ impl->must_reorder = FALSE;
+}
+
+static void
+ganv_module_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_MODULE(object));
+
+ GanvModule* module = GANV_MODULE(object);
+ GanvModulePrivate* impl = module->impl;
+
+ if (impl->ports) {
+ FOREACH_PORT(impl->ports, p) {
+ g_object_unref(GTK_OBJECT(*p));
+ }
+ g_ptr_array_free(impl->ports, TRUE);
+ impl->ports = NULL;
+ }
+
+ if (impl->embed_item) {
+ g_object_unref(GTK_OBJECT(impl->embed_item));
+ impl->embed_item = NULL;
+ }
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+}
+
+static void
+ganv_module_set_property(GObject* object,
+ guint prop_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_MODULE(object));
+
+ switch (prop_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_module_get_property(GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_MODULE(object));
+
+ switch (prop_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+typedef struct {
+ double embed_x;
+ double width;
+ double input_width;
+ double output_width;
+ gboolean horiz;
+ gboolean embed_between;
+} Metrics;
+
+static void
+title_size(GanvModule* module, double* w, double* h)
+{
+ if (module->box.node.impl->label) {
+ g_object_get(G_OBJECT(module->box.node.impl->label),
+ "width", w,
+ "height", h,
+ NULL);
+ } else {
+ *w = *h = 0.0;
+ }
+}
+
+static void
+measure(GanvModule* module, Metrics* m)
+{
+ memset(m, '\0', sizeof(Metrics));
+
+ double title_w, title_h;
+ title_size(module, &title_w, &title_h);
+
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(module));
+ GanvText* canvas_title = GANV_NODE(module)->impl->label;
+ GanvModulePrivate* impl = module->impl;
+
+ if (ganv_canvas_get_direction(canvas) == GANV_DIRECTION_DOWN) {
+ double contents_width = 0.0;
+ if (canvas_title) {
+ contents_width += title_w + (2.0 * PAD);
+ }
+
+ m->embed_x = 0;
+ m->input_width = ganv_module_get_empty_port_breadth(module);
+ m->output_width = ganv_module_get_empty_port_breadth(module);
+
+ // TODO: cache this or merge with resize_right
+ unsigned n_inputs = 0;
+ unsigned n_outputs = 0;
+ FOREACH_PORT(impl->ports, pi) {
+ if ((*pi)->impl->is_input) {
+ ++n_inputs;
+ } else {
+ ++n_outputs;
+ }
+ }
+
+ const unsigned hor_ports = MAX(1, MAX(n_inputs, n_outputs));
+ const double ports_width = (2 * EDGE_PAD) +
+ ((m->input_width) * hor_ports) +
+ ((PAD + 1.0) * (hor_ports - 1));
+
+ m->width = MAX(contents_width, ports_width);
+ m->width = MAX(m->width, impl->embed_width);
+
+ if (impl->embed_item) {
+ m->width = MAX(impl->embed_width + 2.0 * PAD, m->width);
+ m->embed_x = PAD;
+ }
+ return;
+ }
+
+ // The amount of space between a port edge and the module edge (on the
+ // side that the port isn't right on the edge).
+ const double hor_pad = (canvas_title ? 10.0 : 20.0);
+
+ m->width = (canvas_title) ? title_w + 10.0 : 1.0;
+
+ // Title is wide or there is an embedded widget,
+ // put inputs and outputs beside each other
+ m->horiz = (impl->embed_item ||
+ (impl->widest_input + impl->widest_output + 10.0
+ < MAX(m->width, impl->embed_width)));
+
+ // Fit ports to module (or vice-versa)
+ m->input_width = impl->widest_input;
+ m->output_width = impl->widest_output;
+ double expand_w = (m->horiz ? (m->width / 2.0) : m->width) - hor_pad;
+ if (!impl->embed_item) {
+ m->input_width = MAX(impl->widest_input, expand_w);
+ m->output_width = MAX(impl->widest_output, expand_w);
+ }
+
+ const double widest = MAX(m->input_width, m->output_width);
+
+ if (impl->embed_item) {
+ double above_w = MAX(m->width, widest + hor_pad);
+ double between_w = MAX(m->width,
+ (m->input_width
+ + m->output_width
+ + impl->embed_width));
+
+ above_w = MAX(above_w, impl->embed_width);
+
+ // Decide where to place embedded widget if necessary)
+ if (impl->embed_width < impl->embed_height * 2.0) {
+ m->embed_between = TRUE;
+ m->width = between_w;
+ m->embed_x = m->input_width;
+ } else {
+ m->width = above_w;
+ m->embed_x = 2.0;
+ }
+ }
+
+ if (!canvas_title && (impl->widest_input == 0.0
+ || impl->widest_output == 0.0)) {
+ m->width += 10.0;
+ }
+
+ m->width += 4.0;
+ m->width = MAX(m->width, widest + hor_pad);
+}
+
+static void
+place_title(GanvModule* module, GanvDirection dir)
+{
+ GanvBox* box = GANV_BOX(module);
+ GanvText* canvas_title = GANV_NODE(module)->impl->label;
+
+ double title_w, title_h;
+ title_size(module, &title_w, &title_h);
+
+ if (!canvas_title) {
+ return;
+ }
+
+ GanvItem* t = GANV_ITEM(canvas_title);
+ if (dir == GANV_DIRECTION_RIGHT) {
+ t->impl->x = (ganv_box_get_width(box) - title_w) / 2.0;
+ t->impl->y = 1.0;
+ } else {
+ t->impl->x = (ganv_box_get_width(box) - title_w) / 2.0;
+ t->impl->y = ganv_module_get_empty_port_depth(module) + 1.0;
+ }
+}
+
+static void
+resize_right(GanvModule* module)
+{
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(module));
+ GanvModulePrivate* impl = module->impl;
+
+ Metrics m;
+ measure(module, &m);
+
+ double title_w, title_h;
+ title_size(module, &title_w, &title_h);
+
+ // Basic height contains title
+ double header_height = title_h ? (3.0 + title_h) : EDGE_PAD;
+
+ if (impl->embed_item) {
+ ganv_item_set(impl->embed_item,
+ "x", (double)m.embed_x,
+ "y", header_height,
+ NULL);
+ }
+
+ // Actually set width and height
+ ganv_box_set_width(GANV_BOX(module), m.width);
+
+ // Offset ports below embedded widget
+ if (!m.embed_between) {
+ header_height += impl->embed_height;
+ }
+
+ // Move ports to appropriate locations
+ double in_y = header_height;
+ double out_y = header_height;
+ FOREACH_PORT(impl->ports, pi) {
+ GanvPort* const p = (*pi);
+ GanvBox* const pbox = GANV_BOX(p);
+ GanvNode* const pnode = GANV_NODE(p);
+ const double h = ganv_box_get_height(pbox);
+
+ // Offset to shift ports to make borders line up
+ const double border_off = (GANV_NODE(module)->impl->border_width -
+ pnode->impl->border_width) / 2.0;
+
+ if (p->impl->is_input) {
+ ganv_node_move_to(pnode, -border_off, in_y + 1.0);
+ ganv_box_set_width(pbox, m.input_width);
+ in_y += h + pnode->impl->border_width + 1.0;
+
+ ganv_canvas_for_each_edge_to(
+ canvas, pnode,
+ (GanvEdgeFunc)ganv_edge_update_location, NULL);
+ } else {
+ ganv_node_move_to(pnode, m.width - m.output_width + border_off, out_y + 1.0);
+ ganv_box_set_width(pbox, m.output_width);
+ out_y += h + pnode->impl->border_width + 1.0;
+
+ ganv_canvas_for_each_edge_from(
+ canvas, pnode,
+ (GanvEdgeFunc)ganv_edge_update_location, NULL);
+ }
+
+ if (!m.horiz) {
+ in_y = MAX(in_y, out_y);
+ out_y = MAX(in_y, out_y);
+ }
+ }
+
+ double height = MAX(in_y, out_y) + EDGE_PAD;
+ if (impl->embed_item && m.embed_between)
+ height = MAX(height, impl->embed_height + header_height + 2.0);
+
+ ganv_box_set_height(GANV_BOX(module), height);
+
+ place_title(module, GANV_DIRECTION_RIGHT);
+}
+
+static void
+resize_down(GanvModule* module)
+{
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(module));
+ GanvModulePrivate* impl = module->impl;
+
+ Metrics m;
+ measure(module, &m);
+
+ double title_w, title_h;
+ title_size(module, &title_w, &title_h);
+
+ const double port_depth = ganv_module_get_empty_port_depth(module);
+ const double port_breadth = ganv_module_get_empty_port_breadth(module);
+
+ if (impl->embed_item) {
+ ganv_item_set(impl->embed_item,
+ "x", (double)m.embed_x,
+ "y", port_depth + title_h,
+ NULL);
+ }
+
+ const double height = PAD + title_h
+ + impl->embed_height + (port_depth * 2.0);
+
+ // Move ports to appropriate locations
+ guint in_count = 0;
+ guint out_count = 0;
+ double in_x = 0.0;
+ double out_x = 0.0;
+ FOREACH_PORT(impl->ports, pi) {
+ GanvPort* const p = (*pi);
+ GanvBox* const pbox = GANV_BOX(p);
+ GanvNode* const pnode = GANV_NODE(p);
+ ganv_box_set_width(pbox, port_breadth);
+ ganv_box_set_height(pbox, port_depth);
+
+ // Offset to shift ports to make borders line up
+ const double border_off = (GANV_NODE(module)->impl->border_width -
+ pnode->impl->border_width) / 2.0;
+
+ if (p->impl->is_input) {
+ in_x = EDGE_PAD + (in_count++ * (port_breadth + PAD + 1.0));
+ ganv_node_move_to(pnode, in_x, -border_off);
+ ganv_canvas_for_each_edge_to(
+ canvas, pnode,
+ (GanvEdgeFunc)ganv_edge_update_location, NULL);
+ } else {
+ out_x = EDGE_PAD + (out_count++ * (port_breadth + PAD + 1.0));
+ ganv_node_move_to(pnode, out_x, height - port_depth + border_off);
+ ganv_canvas_for_each_edge_from(
+ canvas, pnode,
+ (GanvEdgeFunc)ganv_edge_update_location, NULL);
+ }
+ }
+
+ ganv_box_set_height(GANV_BOX(module), height);
+ ganv_box_set_width(GANV_BOX(module), m.width);
+ place_title(module, GANV_DIRECTION_DOWN);
+}
+
+static void
+measure_ports(GanvModule* module)
+{
+ GanvModulePrivate* impl = module->impl;
+
+ impl->widest_input = 0.0;
+ impl->widest_output = 0.0;
+ FOREACH_PORT_CONST(impl->ports, pi) {
+ const GanvPort* const p = (*pi);
+ const double w = ganv_port_get_natural_width(p);
+ if (p->impl->is_input) {
+ if (w > impl->widest_input) {
+ impl->widest_input = w;
+ }
+ } else {
+ if (w > impl->widest_output) {
+ impl->widest_output = w;
+ }
+ }
+ }
+}
+
+static void
+ganv_module_resize(GanvNode* self)
+{
+ GanvModule* module = GANV_MODULE(self);
+ GanvNode* node = GANV_NODE(self);
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(module));
+
+ double label_w = 0.0;
+ double label_h = 0.0;
+ if (node->impl->label) {
+ g_object_get(node->impl->label,
+ "width", &label_w,
+ "height", &label_h,
+ NULL);
+ }
+
+ measure_ports(module);
+
+ ganv_box_set_width(GANV_BOX(module), label_w + (MODULE_LABEL_PAD * 2.0));
+ ganv_box_set_height(GANV_BOX(module), label_h);
+
+ switch (ganv_canvas_get_direction(canvas)) {
+ case GANV_DIRECTION_RIGHT:
+ resize_right(module);
+ break;
+ case GANV_DIRECTION_DOWN:
+ resize_down(module);
+ break;
+ }
+
+ if (GANV_NODE_CLASS(parent_class)->resize) {
+ GANV_NODE_CLASS(parent_class)->resize(self);
+ }
+}
+
+static void
+ganv_module_redraw_text(GanvNode* self)
+{
+ FOREACH_PORT(GANV_MODULE(self)->impl->ports, p) {
+ ganv_node_redraw_text(GANV_NODE(*p));
+ }
+
+ if (parent_class->parent_class.redraw_text) {
+ parent_class->parent_class.redraw_text(self);
+ }
+}
+
+static void
+ganv_module_add_port(GanvModule* module,
+ GanvPort* port)
+{
+ GanvModulePrivate* impl = module->impl;
+
+ // Update widest input/output measurements if necessary
+ const double width = ganv_port_get_natural_width(port);
+ if (port->impl->is_input && width > impl->widest_input) {
+ impl->widest_input = width;
+ } else if (!port->impl->is_input && width > impl->widest_output) {
+ impl->widest_output = width;
+ }
+
+ // Add to port array
+ g_ptr_array_add(impl->ports, port);
+
+ // Request update with resize and reorder
+ GANV_NODE(module)->impl->must_resize = TRUE;
+ impl->must_reorder = TRUE;
+}
+
+static void
+ganv_module_remove_port(GanvModule* module,
+ GanvPort* port)
+{
+ gboolean removed = g_ptr_array_remove(module->impl->ports, port);
+ if (removed) {
+ const double width = ganv_box_get_width(GANV_BOX(port));
+ // Find new widest input or output, if necessary
+ if (port->impl->is_input && width >= module->impl->widest_input) {
+ module->impl->widest_input = 0;
+ FOREACH_PORT_CONST(module->impl->ports, i) {
+ const GanvPort* const p = (*i);
+ const double w = ganv_box_get_width(GANV_BOX(p));
+ if (p->impl->is_input && w >= module->impl->widest_input) {
+ module->impl->widest_input = w;
+ }
+ }
+ } else if (!port->impl->is_input && width >= module->impl->widest_output) {
+ module->impl->widest_output = 0;
+ FOREACH_PORT_CONST(module->impl->ports, i) {
+ const GanvPort* const p = (*i);
+ const double w = ganv_box_get_width(GANV_BOX(p));
+ if (!p->impl->is_input && w >= module->impl->widest_output) {
+ module->impl->widest_output = w;
+ }
+ }
+ }
+
+ GANV_NODE(module)->impl->must_resize = TRUE;
+ } else {
+ fprintf(stderr, "Failed to find port to remove\n");
+ }
+}
+
+static void
+ganv_module_add(GanvItem* item, GanvItem* child)
+{
+ if (GANV_IS_PORT(child)) {
+ ganv_module_add_port(GANV_MODULE(item), GANV_PORT(child));
+ }
+ ganv_item_request_update(item);
+ if (GANV_ITEM_CLASS(parent_class)->add) {
+ GANV_ITEM_CLASS(parent_class)->add(item, child);
+ }
+}
+
+static void
+ganv_module_remove(GanvItem* item, GanvItem* child)
+{
+ if (GANV_IS_PORT(child)) {
+ ganv_module_remove_port(GANV_MODULE(item), GANV_PORT(child));
+ }
+ ganv_item_request_update(item);
+ if (GANV_ITEM_CLASS(parent_class)->remove) {
+ GANV_ITEM_CLASS(parent_class)->remove(item, child);
+ }
+}
+
+static int
+ptr_sort(const GanvPort** a, const GanvPort** b, const PortOrderCtx* ctx)
+{
+ return ctx->port_cmp(*a, *b, ctx->data);
+}
+
+static void
+ganv_module_update(GanvItem* item, int flags)
+{
+ GanvModule* module = GANV_MODULE(item);
+ GanvCanvas* canvas = ganv_item_get_canvas(item);
+
+ if (module->impl->must_reorder) {
+ // Sort ports array
+ PortOrderCtx ctx = ganv_canvas_get_port_order(canvas);
+ if (ctx.port_cmp) {
+ g_ptr_array_sort_with_data(module->impl->ports,
+ (GCompareDataFunc)ptr_sort,
+
+ &ctx);
+ }
+ module->impl->must_reorder = FALSE;
+ }
+
+ if (module->impl->embed_item) {
+ // Kick the embedded item to update position if we have moved
+ ganv_item_move(GANV_ITEM(module->impl->embed_item), 0.0, 0.0);
+ }
+
+ FOREACH_PORT(module->impl->ports, p) {
+ ganv_item_invoke_update(GANV_ITEM(*p), flags);
+ }
+
+ if (module->impl->embed_item) {
+ ganv_item_invoke_update(GANV_ITEM(module->impl->embed_item), flags);
+ }
+
+ GANV_ITEM_CLASS(parent_class)->update(item, flags);
+}
+
+static void
+ganv_module_draw(GanvItem* item,
+ cairo_t* cr, double cx, double cy, double cw, double ch)
+{
+ GanvNode* node = GANV_NODE(item);
+ GanvModule* module = GANV_MODULE(item);
+
+ // Draw box
+ if (GANV_ITEM_CLASS(parent_class)->draw) {
+ (*GANV_ITEM_CLASS(parent_class)->draw)(item, cr, cx, cy, cw, ch);
+ }
+
+ // Draw label
+ if (node->impl->label) {
+ GanvItem* label_item = GANV_ITEM(node->impl->label);
+ GANV_ITEM_GET_CLASS(label_item)->draw(label_item, cr, cx, cy, cw, ch);
+ }
+
+ // Draw ports
+ FOREACH_PORT(module->impl->ports, p) {
+ GANV_ITEM_GET_CLASS(GANV_ITEM(*p))->draw(
+ GANV_ITEM(*p), cr, cx, cy, cw, ch);
+ }
+
+ // Draw embed item
+ if (module->impl->embed_item) {
+ GANV_ITEM_GET_CLASS(module->impl->embed_item)->draw(
+ module->impl->embed_item, cr, cx, cy, cw, ch);
+ }
+}
+
+static void
+ganv_module_move_to(GanvNode* node,
+ double x,
+ double y)
+{
+ GanvModule* module = GANV_MODULE(node);
+ GANV_NODE_CLASS(parent_class)->move_to(node, x, y);
+ FOREACH_PORT(module->impl->ports, p) {
+ ganv_node_move(GANV_NODE(*p), 0.0, 0.0);
+ }
+ if (module->impl->embed_item) {
+ ganv_item_move(GANV_ITEM(module->impl->embed_item), 0.0, 0.0);
+ }
+}
+
+static void
+ganv_module_move(GanvNode* node,
+ double dx,
+ double dy)
+{
+ GanvModule* module = GANV_MODULE(node);
+ GANV_NODE_CLASS(parent_class)->move(node, dx, dy);
+ FOREACH_PORT(module->impl->ports, p) {
+ ganv_node_move(GANV_NODE(*p), 0.0, 0.0);
+ }
+ if (module->impl->embed_item) {
+ ganv_item_move(GANV_ITEM(module->impl->embed_item), 0.0, 0.0);
+ }
+}
+
+static double
+ganv_module_point(GanvItem* item, double x, double y, GanvItem** actual_item)
+{
+ GanvModule* module = GANV_MODULE(item);
+
+ double d = GANV_ITEM_CLASS(parent_class)->point(item, x, y, actual_item);
+
+ if (!*actual_item) {
+ // Point is not inside module at all, no point in checking children
+ return d;
+ }
+
+ FOREACH_PORT(module->impl->ports, p) {
+ GanvItem* const port = GANV_ITEM(*p);
+
+ *actual_item = NULL;
+ d = GANV_ITEM_GET_CLASS(port)->point(
+ port, x - port->impl->x, y - port->impl->y, actual_item);
+
+ if (*actual_item) {
+ // Point is inside a port
+ return d;
+ }
+ }
+
+ // Point is inside module, but not a child port
+ *actual_item = item;
+ return 0.0;
+}
+
+static void
+ganv_module_class_init(GanvModuleClass* klass)
+{
+ GObjectClass* gobject_class = (GObjectClass*)klass;
+ GtkObjectClass* object_class = (GtkObjectClass*)klass;
+ GanvItemClass* item_class = (GanvItemClass*)klass;
+ GanvNodeClass* node_class = (GanvNodeClass*)klass;
+
+ parent_class = GANV_BOX_CLASS(g_type_class_peek_parent(klass));
+
+ gobject_class->set_property = ganv_module_set_property;
+ gobject_class->get_property = ganv_module_get_property;
+
+ object_class->destroy = ganv_module_destroy;
+
+ item_class->add = ganv_module_add;
+ item_class->remove = ganv_module_remove;
+ item_class->update = ganv_module_update;
+ item_class->draw = ganv_module_draw;
+ item_class->point = ganv_module_point;
+
+ node_class->move = ganv_module_move;
+ node_class->move_to = ganv_module_move_to;
+ node_class->resize = ganv_module_resize;
+ node_class->redraw_text = ganv_module_redraw_text;
+}
+
+GanvModule*
+ganv_module_new(GanvCanvas* canvas,
+ const char* first_property_name, ...)
+{
+ GanvModule* module = GANV_MODULE(
+ g_object_new(ganv_module_get_type(), "canvas", canvas, NULL));
+
+ va_list args;
+ va_start(args, first_property_name);
+ g_object_set_valist(G_OBJECT(module), first_property_name, args);
+ va_end(args);
+
+ return module;
+}
+
+guint
+ganv_module_num_ports(const GanvModule* module)
+{
+ return module->impl->ports ? module->impl->ports->len : 0;
+}
+
+GanvPort*
+ganv_module_get_port(GanvModule* module,
+ guint index)
+{
+ return (GanvPort*)g_ptr_array_index(module->impl->ports, index);
+}
+
+double
+ganv_module_get_empty_port_breadth(const GanvModule* module)
+{
+ return ganv_module_get_empty_port_depth(module) * 2.0;
+}
+
+double
+ganv_module_get_empty_port_depth(const GanvModule* module)
+{
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(module));
+
+ return ganv_canvas_get_font_size(canvas) * 1.1;
+}
+
+static void
+on_embed_size_request(GtkWidget* widget,
+ GtkRequisition* r,
+ void* user_data)
+{
+ GanvModule* module = GANV_MODULE(user_data);
+ GanvModulePrivate* impl = module->impl;
+ if (impl->embed_width == r->width && impl->embed_height == r->height) {
+ return;
+ }
+
+ impl->embed_width = r->width;
+ impl->embed_height = r->height;
+ GANV_NODE(module)->impl->must_resize = TRUE;
+
+ GtkAllocation allocation;
+ allocation.width = r->width;
+ allocation.height = r->width;
+
+ gtk_widget_size_allocate(widget, &allocation);
+ ganv_item_set(impl->embed_item,
+ "width", (double)r->width,
+ "height", (double)r->height,
+ NULL);
+}
+
+void
+ganv_module_embed(GanvModule* module,
+ GtkWidget* widget)
+{
+ GanvModulePrivate* impl = module->impl;
+ if (!widget && !impl->embed_item) {
+ return;
+ }
+
+ if (impl->embed_item) {
+ // Free existing embedded widget
+ gtk_object_destroy(GTK_OBJECT(impl->embed_item));
+ impl->embed_item = NULL;
+ }
+
+ if (!widget) {
+ // Removing an existing embedded widget
+ impl->embed_width = 0;
+ impl->embed_height = 0;
+ GANV_NODE(module)->impl->must_resize = TRUE;
+ ganv_item_request_update(GANV_ITEM(module));
+ return;
+ }
+
+ double title_w, title_h;
+ title_size(module, &title_w, &title_h);
+
+ impl->embed_item = ganv_item_new(
+ GANV_ITEM(module),
+ ganv_widget_get_type(),
+ "x", 2.0,
+ "y", 4.0 + title_h,
+ "widget", widget,
+ NULL);
+
+ GtkRequisition r;
+ gtk_widget_show_all(widget);
+ gtk_widget_size_request(widget, &r);
+ on_embed_size_request(widget, &r, module);
+ ganv_item_show(impl->embed_item);
+
+ g_signal_connect(widget, "size-request",
+ G_CALLBACK(on_embed_size_request), module);
+
+ GANV_NODE(module)->impl->must_resize = TRUE;
+ ganv_item_request_update(GANV_ITEM(module));
+}
+
+void
+ganv_module_set_direction(GanvModule* module,
+ GanvDirection direction)
+{
+ FOREACH_PORT(module->impl->ports, p) {
+ ganv_port_set_direction(*p, direction);
+ }
+ GANV_NODE(module)->impl->must_resize = TRUE;
+ ganv_item_request_update(GANV_ITEM(module));
+}
+
+void
+ganv_module_for_each_port(GanvModule* module,
+ GanvPortFunc f,
+ void* data)
+{
+ GanvModulePrivate* impl = module->impl;
+ const int len = impl->ports->len;
+ GanvPort** copy = (GanvPort**)malloc(sizeof(GanvPort*) * len);
+ memcpy(copy, impl->ports->pdata, sizeof(GanvPort*) * len);
+
+ for (int i = 0; i < len; ++i) {
+ f(copy[i], data);
+ }
+
+ free(copy);
+}
diff --git a/src/node.c b/src/node.c
new file mode 100644
index 0000000..4955f36
--- /dev/null
+++ b/src/node.c
@@ -0,0 +1,897 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2016 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ganv/canvas.h"
+#include "ganv/node.h"
+
+#include "./boilerplate.h"
+#include "./color.h"
+#include "./ganv-marshal.h"
+#include "./ganv-private.h"
+#include "./gettext.h"
+
+guint signal_moved;
+
+G_DEFINE_TYPE_WITH_CODE(GanvNode, ganv_node, GANV_TYPE_ITEM,
+ G_ADD_PRIVATE(GanvNode))
+
+static GanvItemClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_CANVAS,
+ PROP_PARTNER,
+ PROP_LABEL,
+ PROP_SHOW_LABEL,
+ PROP_DASH_LENGTH,
+ PROP_DASH_OFFSET,
+ PROP_BORDER_WIDTH,
+ PROP_FILL_COLOR,
+ PROP_BORDER_COLOR,
+ PROP_CAN_TAIL,
+ PROP_CAN_HEAD,
+ PROP_IS_SOURCE,
+ PROP_SELECTED,
+ PROP_HIGHLIGHTED,
+ PROP_DRAGGABLE,
+ PROP_GRABBED
+};
+
+static void
+ganv_node_init(GanvNode* node)
+{
+ GanvNodePrivate* impl = ganv_node_get_instance_private(node);
+
+ node->impl = impl;
+
+ impl->partner = NULL;
+ impl->label = NULL;
+ impl->dash_length = 0.0;
+ impl->dash_offset = 0.0;
+ impl->border_width = 2.0;
+ impl->fill_color = DEFAULT_FILL_COLOR;
+ impl->border_color = DEFAULT_BORDER_COLOR;
+ impl->can_tail = FALSE;
+ impl->can_head = FALSE;
+ impl->is_source = FALSE;
+ impl->selected = FALSE;
+ impl->highlighted = FALSE;
+ impl->draggable = FALSE;
+ impl->show_label = TRUE;
+ impl->grabbed = FALSE;
+ impl->must_resize = FALSE;
+#ifdef GANV_FDGL
+ impl->force.x = 0.0;
+ impl->force.y = 0.0;
+ impl->vel.x = 0.0;
+ impl->vel.y = 0.0;
+ impl->connected = FALSE;
+#endif
+}
+
+static void
+ganv_node_realize(GanvItem* item)
+{
+ GANV_ITEM_CLASS(parent_class)->realize(item);
+ ganv_canvas_add_node(ganv_item_get_canvas(item), GANV_NODE(item));
+}
+
+static void
+ganv_node_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_NODE(object));
+
+ GanvNode* node = GANV_NODE(object);
+ GanvNodePrivate* impl = node->impl;
+ if (impl->label) {
+ g_object_unref(impl->label);
+ impl->label = NULL;
+ }
+
+ GanvItem* item = GANV_ITEM(object);
+ ganv_node_disconnect(node);
+ if (item->impl->canvas) {
+ ganv_canvas_remove_node(item->impl->canvas, node);
+ }
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+
+ impl->partner = NULL;
+ item->impl->canvas = NULL;
+}
+
+static void
+ganv_node_update(GanvItem* item, int flags)
+{
+ GanvNode* node = GANV_NODE(item);
+ if (node->impl->must_resize) {
+ ganv_node_resize(node);
+ node->impl->must_resize = FALSE;
+ }
+
+ if (node->impl->label) {
+ ganv_item_invoke_update(GANV_ITEM(node->impl->label), flags);
+ }
+
+ GANV_ITEM_CLASS(parent_class)->update(item, flags);
+}
+
+static void
+ganv_node_draw(GanvItem* item,
+ cairo_t* cr, double cx, double cy, double cw, double ch)
+{
+ /* TODO: Label is not drawn here because ports need to draw control
+ rects then the label on top. I can't see a way of solving this since
+ there's no single time parent class draw needs to be called, so perhaps
+ label shouldn't be part of this class... */
+}
+
+static void
+ganv_node_set_property(GObject* object,
+ guint prop_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_NODE(object));
+
+ GanvNode* node = GANV_NODE(object);
+ GanvNodePrivate* impl = node->impl;
+
+ switch (prop_id) {
+ SET_CASE(DASH_LENGTH, double, impl->dash_length);
+ SET_CASE(DASH_OFFSET, double, impl->dash_offset);
+ SET_CASE(BORDER_WIDTH, double, impl->border_width);
+ SET_CASE(FILL_COLOR, uint, impl->fill_color);
+ SET_CASE(BORDER_COLOR, uint, impl->border_color);
+ SET_CASE(CAN_TAIL, boolean, impl->can_tail);
+ SET_CASE(CAN_HEAD, boolean, impl->can_head);
+ SET_CASE(IS_SOURCE, boolean, impl->is_source);
+ SET_CASE(HIGHLIGHTED, boolean, impl->highlighted);
+ SET_CASE(DRAGGABLE, boolean, impl->draggable);
+ SET_CASE(GRABBED, boolean, impl->grabbed);
+ case PROP_PARTNER:
+ impl->partner = (GanvNode*)g_value_get_object(value);
+ break;
+ case PROP_SELECTED:
+ if (impl->selected != g_value_get_boolean(value)) {
+ GanvItem* item = GANV_ITEM(object);
+ impl->selected = g_value_get_boolean(value);
+ if (item->impl->canvas) {
+ if (impl->selected) {
+ ganv_canvas_select_node(ganv_item_get_canvas(item), node);
+ } else {
+ ganv_canvas_unselect_node(ganv_item_get_canvas(item), node);
+ }
+ ganv_item_request_update(item);
+ }
+ }
+ break;
+ case PROP_CANVAS:
+ if (!GANV_ITEM(object)->impl->parent) {
+ GanvCanvas* canvas = GANV_CANVAS(g_value_get_object(value));
+ g_object_set(object, "parent", ganv_canvas_root(canvas), NULL);
+ ganv_canvas_add_node(canvas, node);
+ } else {
+ g_warning("Cannot change `canvas' property after construction");
+ }
+ break;
+ case PROP_LABEL:
+ ganv_node_set_label(node, g_value_get_string(value));
+ break;
+ case PROP_SHOW_LABEL:
+ ganv_node_set_show_label(node, g_value_get_boolean(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_node_get_property(GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_NODE(object));
+
+ GanvNode* node = GANV_NODE(object);
+ GanvNodePrivate* impl = node->impl;
+
+ switch (prop_id) {
+ GET_CASE(PARTNER, object, impl->partner);
+ GET_CASE(LABEL, string, impl->label ? impl->label->impl->text : NULL);
+ GET_CASE(DASH_LENGTH, double, impl->dash_length);
+ GET_CASE(DASH_OFFSET, double, impl->dash_offset);
+ GET_CASE(BORDER_WIDTH, double, impl->border_width);
+ GET_CASE(FILL_COLOR, uint, impl->fill_color);
+ GET_CASE(BORDER_COLOR, uint, impl->border_color);
+ GET_CASE(CAN_TAIL, boolean, impl->can_tail);
+ GET_CASE(CAN_HEAD, boolean, impl->can_head);
+ GET_CASE(IS_SOURCE, boolean, impl->is_source);
+ GET_CASE(SELECTED, boolean, impl->selected);
+ GET_CASE(HIGHLIGHTED, boolean, impl->highlighted);
+ GET_CASE(DRAGGABLE, boolean, impl->draggable);
+ GET_CASE(GRABBED, boolean, impl->grabbed);
+ case PROP_CANVAS:
+ g_value_set_object(value, ganv_item_get_canvas(GANV_ITEM(object)));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_node_default_tail_vector(const GanvNode* self,
+ const GanvNode* head,
+ double* x,
+ double* y,
+ double* dx,
+ double* dy)
+{
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(self));
+
+ *x = GANV_ITEM(self)->impl->x;
+ *y = GANV_ITEM(self)->impl->y;
+
+ switch (ganv_canvas_get_direction(canvas)) {
+ case GANV_DIRECTION_RIGHT:
+ *dx = 1.0;
+ *dy = 0.0;
+ break;
+ case GANV_DIRECTION_DOWN:
+ *dx = 0.0;
+ *dy = 1.0;
+ break;
+ }
+
+ ganv_item_i2w(GANV_ITEM(self)->impl->parent, x, y);
+}
+
+static void
+ganv_node_default_head_vector(const GanvNode* self,
+ const GanvNode* tail,
+ double* x,
+ double* y,
+ double* dx,
+ double* dy)
+{
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(self));
+
+ *x = GANV_ITEM(self)->impl->x;
+ *y = GANV_ITEM(self)->impl->y;
+
+ switch (ganv_canvas_get_direction(canvas)) {
+ case GANV_DIRECTION_RIGHT:
+ *dx = -1.0;
+ *dy = 0.0;
+ break;
+ case GANV_DIRECTION_DOWN:
+ *dx = 0.0;
+ *dy = -1.0;
+ break;
+ }
+
+ ganv_item_i2w(GANV_ITEM(self)->impl->parent, x, y);
+}
+
+void
+ganv_node_get_draw_properties(const GanvNode* node,
+ double* dash_length,
+ double* border_color,
+ double* fill_color)
+{
+ GanvNodePrivate* impl = node->impl;
+
+ *dash_length = impl->dash_length;
+ *border_color = impl->border_color;
+ *fill_color = impl->fill_color;
+
+ if (impl->selected) {
+ *dash_length = 4.0;
+ *border_color = highlight_color(impl->border_color, 0x40);
+ }
+
+ if (impl->highlighted) {
+ *border_color = highlight_color(impl->border_color, 0x40);
+ *fill_color = impl->fill_color;
+ }
+}
+
+void
+ganv_node_set_label(GanvNode* node, const char* str)
+{
+ GanvNodePrivate* impl = node->impl;
+ if (!str || str[0] == '\0') {
+ if (impl->label) {
+ gtk_object_destroy(GTK_OBJECT(impl->label));
+ impl->label = NULL;
+ }
+ } else if (impl->label) {
+ ganv_item_set(GANV_ITEM(impl->label),
+ "text", str,
+ NULL);
+ } else {
+ impl->label = GANV_TEXT(ganv_item_new(GANV_ITEM(node),
+ ganv_text_get_type(),
+ "text", str,
+ "color", DEFAULT_TEXT_COLOR,
+ "managed", TRUE,
+ NULL));
+ }
+
+ impl->must_resize = TRUE;
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+void
+ganv_node_set_show_label(GanvNode* node, gboolean show)
+{
+ if (node->impl->label) {
+ if (show) {
+ ganv_item_show(GANV_ITEM(node->impl->label));
+ } else {
+ ganv_item_hide(GANV_ITEM(node->impl->label));
+ }
+ }
+ node->impl->show_label = show;
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+static void
+ganv_node_default_tick(GanvNode* self,
+ double seconds)
+{
+ GanvNode* node = GANV_NODE(self);
+ node->impl->dash_offset = seconds * 8.0;
+ ganv_item_request_update(GANV_ITEM(self));
+}
+
+static void
+ganv_node_default_disconnect(GanvNode* node)
+{
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(node));
+ if (canvas) {
+ ganv_canvas_for_each_edge_on(
+ canvas, node, (GanvEdgeFunc)ganv_edge_disconnect, NULL);
+ }
+}
+
+static void
+ganv_node_default_move(GanvNode* node,
+ double dx,
+ double dy)
+{
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(node));
+ ganv_item_move(GANV_ITEM(node), dx, dy);
+ ganv_canvas_for_each_edge_on(
+ canvas, node, (GanvEdgeFunc)ganv_edge_update_location, NULL);
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+static void
+ganv_node_default_move_to(GanvNode* node,
+ double x,
+ double y)
+{
+ GanvItem* item = GANV_ITEM(node);
+ GanvCanvas* canvas = ganv_item_get_canvas(item);
+ item->impl->x = x;
+ item->impl->y = y;
+ if (node->impl->can_tail) {
+ ganv_canvas_for_each_edge_from(
+ canvas, node, (GanvEdgeFunc)ganv_edge_update_location, NULL);
+ } else if (node->impl->can_head) {
+ ganv_canvas_for_each_edge_to(
+ canvas, node, (GanvEdgeFunc)ganv_edge_update_location, NULL);
+ }
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+static void
+ganv_node_default_resize(GanvNode* node)
+{
+ GanvItem* item = GANV_ITEM(node);
+ if (GANV_IS_NODE(item->impl->parent)) {
+ ganv_node_resize(GANV_NODE(item->impl->parent));
+ }
+ node->impl->must_resize = FALSE;
+}
+
+static void
+ganv_node_default_redraw_text(GanvNode* node)
+{
+ if (node->impl->label) {
+ ganv_text_layout(node->impl->label);
+ node->impl->must_resize = TRUE;
+ ganv_item_request_update(GANV_ITEM(node));
+ }
+}
+
+static gboolean
+ganv_node_default_event(GanvItem* item,
+ GdkEvent* event)
+{
+ GanvNode* node = GANV_NODE(item);
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(node));
+
+ // FIXME: put these somewhere better
+ static double last_x, last_y;
+ static double drag_start_x, drag_start_y;
+ static gboolean dragging = FALSE;
+
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ ganv_item_raise(GANV_ITEM(node));
+ node->impl->highlighted = TRUE;
+ ganv_item_request_update(item);
+ return TRUE;
+
+ case GDK_LEAVE_NOTIFY:
+ ganv_item_lower(GANV_ITEM(node));
+ node->impl->highlighted = FALSE;
+ ganv_item_request_update(item);
+ return TRUE;
+
+ case GDK_BUTTON_PRESS:
+ drag_start_x = event->button.x;
+ drag_start_y = event->button.y;
+ last_x = event->button.x;
+ last_y = event->button.y;
+ if (!ganv_canvas_get_locked(canvas) && node->impl->draggable && event->button.button == 1) {
+ ganv_canvas_grab_item(
+ GANV_ITEM(node),
+ GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK|GDK_BUTTON_PRESS_MASK,
+ ganv_canvas_get_move_cursor(canvas),
+ event->button.time);
+ node->impl->grabbed = TRUE;
+ dragging = TRUE;
+ return TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (dragging) {
+ gboolean selected;
+ g_object_get(G_OBJECT(node), "selected", &selected, NULL);
+ ganv_canvas_ungrab_item(GANV_ITEM(node), event->button.time);
+ node->impl->grabbed = FALSE;
+ dragging = FALSE;
+ if (event->button.x != drag_start_x || event->button.y != drag_start_y) {
+ ganv_canvas_contents_changed(canvas);
+ if (selected) {
+ ganv_canvas_selection_move_finished(canvas);
+ } else {
+ const double x = GANV_ITEM(node)->impl->x;
+ const double y = GANV_ITEM(node)->impl->y;
+ g_signal_emit(node, signal_moved, 0, x, y, NULL);
+ }
+ } else {
+ // Clicked
+ if (selected) {
+ ganv_canvas_unselect_node(canvas, node);
+ } else {
+ if (!(event->button.state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK))) {
+ ganv_canvas_clear_selection(canvas);
+ }
+ ganv_canvas_select_node(canvas, node);
+ }
+ }
+ return TRUE;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if ((dragging && (event->motion.state & GDK_BUTTON1_MASK))) {
+ gboolean selected;
+ g_object_get(G_OBJECT(node), "selected", &selected, NULL);
+
+ double new_x = event->motion.x;
+ double new_y = event->motion.y;
+
+ if (event->motion.is_hint) {
+ int t_x;
+ int t_y;
+ GdkModifierType state;
+ gdk_window_get_pointer(event->motion.window, &t_x, &t_y, &state);
+ new_x = t_x;
+ new_y = t_y;
+ }
+
+ const double dx = new_x - last_x;
+ const double dy = new_y - last_y;
+ if (selected) {
+ ganv_canvas_move_selected_items(canvas, dx, dy);
+ } else {
+ ganv_node_move(node, dx, dy);
+ }
+
+ last_x = new_x;
+ last_y = new_y;
+ return TRUE;
+ }
+
+ default:
+ break;
+ }
+
+ return FALSE;
+}
+
+static void
+ganv_node_class_init(GanvNodeClass* klass)
+{
+ GObjectClass* gobject_class = (GObjectClass*)klass;
+ GtkObjectClass* object_class = (GtkObjectClass*)klass;
+ GanvItemClass* item_class = (GanvItemClass*)klass;
+
+ parent_class = GANV_ITEM_CLASS(g_type_class_peek_parent(klass));
+
+ gobject_class->set_property = ganv_node_set_property;
+ gobject_class->get_property = ganv_node_get_property;
+
+ g_object_class_install_property(
+ gobject_class, PROP_CANVAS, g_param_spec_object(
+ "canvas",
+ _("Canvas"),
+ _("The canvas this node is on."),
+ GANV_TYPE_CANVAS,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_PARTNER, g_param_spec_object(
+ "partner",
+ _("Partner"),
+ _("Partners are nodes that should be visually aligned to correspond"
+ " to each other, even if they are not necessarily connected (e.g."
+ " for separate modules representing the inputs and outputs of a"
+ " single thing). When the canvas is arranged, the partner will"
+ " be aligned as if there was an edge from this node to its"
+ " partner."),
+ GANV_TYPE_NODE,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_LABEL, g_param_spec_string(
+ "label",
+ _("Label"),
+ _("The text to display as a label on this node."),
+ NULL,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_SHOW_LABEL, g_param_spec_boolean(
+ "show-label",
+ _("Show label"),
+ _("Whether or not to show the label."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_DASH_LENGTH, g_param_spec_double(
+ "dash-length",
+ _("Border dash length"),
+ _("Length of border dashes, or zero for no dashing."),
+ 0.0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_DASH_OFFSET, g_param_spec_double(
+ "dash-offset",
+ _("Border dash offset"),
+ _("Start offset for border dashes, used for selected animation."),
+ 0.0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_BORDER_WIDTH, g_param_spec_double(
+ "border-width",
+ _("Border width"),
+ _("Width of the border line."),
+ 0.0, G_MAXDOUBLE,
+ 2.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_FILL_COLOR, g_param_spec_uint(
+ "fill-color",
+ _("Fill color"),
+ _("Color of internal area."),
+ 0, G_MAXUINT,
+ DEFAULT_FILL_COLOR,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_BORDER_COLOR, g_param_spec_uint(
+ "border-color",
+ _("Border color"),
+ _("Color of border line."),
+ 0, G_MAXUINT,
+ DEFAULT_BORDER_COLOR,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_CAN_TAIL, g_param_spec_boolean(
+ "can-tail",
+ _("Can tail"),
+ _("Whether this node can be the tail of an edge."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_CAN_HEAD, g_param_spec_boolean(
+ "can-head",
+ _("Can head"),
+ _("Whether this object can be the head of an edge."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_IS_SOURCE, g_param_spec_boolean(
+ "is-source",
+ _("Is source"),
+ _("Whether this object should be positioned at the start of signal flow."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_SELECTED, g_param_spec_boolean(
+ "selected",
+ _("Selected"),
+ _("Whether this object is selected."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_HIGHLIGHTED, g_param_spec_boolean(
+ "highlighted",
+ _("Highlighted"),
+ _("Whether this object is highlighted."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_DRAGGABLE, g_param_spec_boolean(
+ "draggable",
+ _("Draggable"),
+ _("Whether this object is draggable."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_GRABBED, g_param_spec_boolean(
+ "grabbed",
+ _("Grabbed"),
+ _("Whether this object is grabbed by the user."),
+ 0,
+ G_PARAM_READWRITE));
+
+ signal_moved = g_signal_new("moved",
+ ganv_node_get_type(),
+ G_SIGNAL_RUN_FIRST,
+ 0, NULL, NULL,
+ ganv_marshal_VOID__DOUBLE_DOUBLE,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_DOUBLE,
+ G_TYPE_DOUBLE,
+ 0);
+
+ object_class->destroy = ganv_node_destroy;
+
+ item_class->realize = ganv_node_realize;
+ item_class->event = ganv_node_default_event;
+ item_class->update = ganv_node_update;
+ item_class->draw = ganv_node_draw;
+
+ klass->disconnect = ganv_node_default_disconnect;
+ klass->move = ganv_node_default_move;
+ klass->move_to = ganv_node_default_move_to;
+ klass->resize = ganv_node_default_resize;
+ klass->redraw_text = ganv_node_default_redraw_text;
+ klass->tick = ganv_node_default_tick;
+ klass->tail_vector = ganv_node_default_tail_vector;
+ klass->head_vector = ganv_node_default_head_vector;
+}
+
+gboolean
+ganv_node_can_tail(const GanvNode* self)
+{
+ return self->impl->can_tail;
+}
+
+gboolean
+ganv_node_can_head(const GanvNode* self)
+{
+ return self->impl->can_head;
+}
+
+void
+ganv_node_set_is_source(const GanvNode* node, gboolean is_source)
+{
+ node->impl->is_source = is_source;
+}
+
+gboolean
+ganv_node_is_within(const GanvNode* node,
+ double x1,
+ double y1,
+ double x2,
+ double y2)
+{
+ return GANV_NODE_GET_CLASS(node)->is_within(node, x1, y1, x2, y2);
+}
+
+void
+ganv_node_tick(GanvNode* node,
+ double seconds)
+{
+ GanvNodeClass* klass = GANV_NODE_GET_CLASS(node);
+ if (klass->tick) {
+ klass->tick(node, seconds);
+ }
+}
+
+void
+ganv_node_tail_vector(const GanvNode* self,
+ const GanvNode* head,
+ double* x1,
+ double* y1,
+ double* x2,
+ double* y2)
+{
+ GANV_NODE_GET_CLASS(self)->tail_vector(
+ self, head, x1, y1, x2, y2);
+}
+
+void
+ganv_node_head_vector(const GanvNode* self,
+ const GanvNode* tail,
+ double* x1,
+ double* y1,
+ double* x2,
+ double* y2)
+{
+ GANV_NODE_GET_CLASS(self)->head_vector(
+ self, tail, x1, y1, x2, y2);
+}
+
+const char*
+ganv_node_get_label(const GanvNode* node)
+{
+ return node->impl->label ? node->impl->label->impl->text : NULL;
+}
+
+double
+ganv_node_get_border_width(const GanvNode* node)
+{
+ return node->impl->border_width;
+}
+
+void
+ganv_node_set_border_width(const GanvNode* node, double border_width)
+{
+ node->impl->border_width = border_width;
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+double
+ganv_node_get_dash_length(const GanvNode* node)
+{
+ return node->impl->dash_length;
+}
+
+void
+ganv_node_set_dash_length(const GanvNode* node, double dash_length)
+{
+ node->impl->dash_length = dash_length;
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+double
+ganv_node_get_dash_offset(const GanvNode* node)
+{
+ return node->impl->dash_offset;
+}
+
+void
+ganv_node_set_dash_offset(const GanvNode* node, double dash_offset)
+{
+ node->impl->dash_offset = dash_offset;
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+guint
+ganv_node_get_fill_color(const GanvNode* node)
+{
+ return node->impl->fill_color;
+}
+
+void
+ganv_node_set_fill_color(const GanvNode* node, guint fill_color)
+{
+ node->impl->fill_color = fill_color;
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+guint
+ganv_node_get_border_color(const GanvNode* node)
+{
+ return node->impl->border_color;
+}
+
+void
+ganv_node_set_border_color(const GanvNode* node, guint border_color)
+{
+ node->impl->border_color = border_color;
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+GanvNode*
+ganv_node_get_partner(const GanvNode* node)
+{
+ return node->impl->partner;
+}
+
+void
+ganv_node_move(GanvNode* node,
+ double dx,
+ double dy)
+{
+ GANV_NODE_GET_CLASS(node)->move(node, dx, dy);
+}
+
+void
+ganv_node_move_to(GanvNode* node,
+ double x,
+ double y)
+{
+ GANV_NODE_GET_CLASS(node)->move_to(node, x, y);
+}
+
+void
+ganv_node_resize(GanvNode* node)
+{
+ GANV_NODE_GET_CLASS(node)->resize(node);
+ node->impl->must_resize = FALSE;
+}
+
+void
+ganv_node_redraw_text(GanvNode* node)
+{
+ GANV_NODE_GET_CLASS(node)->redraw_text(node);
+}
+
+void
+ganv_node_disconnect(GanvNode* node)
+{
+ GANV_NODE_GET_CLASS(node)->disconnect(node);
+}
+
+gboolean
+ganv_node_is_selected(GanvNode* node)
+{
+ gboolean selected = FALSE;
+ g_object_get(node, "selected", &selected, NULL);
+ return selected;
+}
diff --git a/src/port.c b/src/port.c
new file mode 100644
index 0000000..fa76f22
--- /dev/null
+++ b/src/port.c
@@ -0,0 +1,735 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2015 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+#include <stdlib.h>
+
+#include "ganv/canvas.h"
+#include "ganv/port.h"
+#include "ganv/module.h"
+
+#include "./boilerplate.h"
+#include "./color.h"
+#include "./ganv-private.h"
+#include "./gettext.h"
+
+static const double PORT_LABEL_HPAD = 4.0;
+static const double PORT_LABEL_VPAD = 1.0;
+
+static void
+ganv_port_update_control_slider(GanvPort* port, float value, gboolean force);
+
+G_DEFINE_TYPE_WITH_CODE(GanvPort, ganv_port, GANV_TYPE_BOX,
+ G_ADD_PRIVATE(GanvPort))
+
+static GanvBoxClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_IS_INPUT,
+ PROP_IS_CONTROLLABLE
+};
+
+enum {
+ PORT_VALUE_CHANGED,
+ PORT_LAST_SIGNAL
+};
+
+static guint port_signals[PORT_LAST_SIGNAL];
+
+static void
+ganv_port_init(GanvPort* port)
+{
+ port->impl = ganv_port_get_instance_private(port);
+
+ port->impl->control = NULL;
+ port->impl->value_label = NULL;
+ port->impl->is_input = TRUE;
+ port->impl->is_controllable = FALSE;
+}
+
+static void
+ganv_port_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_PORT(object));
+
+ GanvItem* item = GANV_ITEM(object);
+ GanvPort* port = GANV_PORT(object);
+ GanvCanvas* canvas = ganv_item_get_canvas(item);
+ if (canvas) {
+ if (port->impl->is_input) {
+ ganv_canvas_for_each_edge_to(
+ canvas, &port->box.node, (GanvEdgeFunc)ganv_edge_remove, NULL);
+ } else {
+ ganv_canvas_for_each_edge_from(
+ canvas, &port->box.node, (GanvEdgeFunc)ganv_edge_remove, NULL);
+ }
+ }
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+}
+
+static void
+ganv_port_set_property(GObject* object,
+ guint prop_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_PORT(object));
+
+ GanvPort* port = GANV_PORT(object);
+
+ switch (prop_id) {
+ SET_CASE(IS_INPUT, boolean, port->impl->is_input);
+ SET_CASE(IS_CONTROLLABLE, boolean, port->impl->is_controllable);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_port_get_property(GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_PORT(object));
+
+ GanvPort* port = GANV_PORT(object);
+
+ switch (prop_id) {
+ GET_CASE(IS_INPUT, boolean, port->impl->is_input);
+ GET_CASE(IS_CONTROLLABLE, boolean, port->impl->is_controllable);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_port_update(GanvItem* item, int flags)
+{
+ GanvPort* port = GANV_PORT(item);
+ GanvPortPrivate* impl = port->impl;
+
+ if (impl->control) {
+ ganv_item_invoke_update(GANV_ITEM(impl->control->rect), flags);
+ }
+
+ if (impl->value_label) {
+ ganv_item_invoke_update(GANV_ITEM(port->impl->value_label), flags);
+ }
+
+ GanvItemClass* item_class = GANV_ITEM_CLASS(parent_class);
+ item_class->update(item, flags);
+}
+
+static void
+ganv_port_draw(GanvItem* item,
+ cairo_t* cr, double cx, double cy, double cw, double ch)
+{
+ GanvPort* port = GANV_PORT(item);
+ GanvCanvas* canvas = ganv_item_get_canvas(item);
+
+ // Draw Box
+ GanvItemClass* item_class = GANV_ITEM_CLASS(parent_class);
+ item_class->draw(item, cr, cx, cy, cw, ch);
+
+ if (port->impl->control) {
+ // Clip to port boundaries (to stay within radiused borders)
+ cairo_save(cr);
+ const double pad = GANV_NODE(port)->impl->border_width / 2.0;
+ GanvBoxCoords coords = GANV_BOX(port)->impl->coords;
+ ganv_item_i2w_pair(GANV_ITEM(port),
+ &coords.x1, &coords.y1, &coords.x2, &coords.y2);
+ ganv_box_path(GANV_BOX(port), cr,
+ coords.x1 + pad, coords.y1 + pad,
+ coords.x2 - pad, coords.y2 - pad,
+ -pad);
+ cairo_clip(cr);
+
+ GanvItem* const rect = GANV_ITEM(port->impl->control->rect);
+ GANV_ITEM_GET_CLASS(rect)->draw(rect, cr, cx, cy, cw, ch);
+
+ cairo_restore(cr);
+ }
+
+ if (ganv_canvas_get_direction(canvas) == GANV_DIRECTION_DOWN ||
+ !GANV_NODE(port)->impl->show_label) {
+ return;
+ }
+
+ GanvItem* labels[2] = {
+ GANV_ITEM(GANV_NODE(item)->impl->label),
+ port->impl->value_label ? GANV_ITEM(port->impl->value_label) : NULL
+ };
+ for (int i = 0; i < 2; ++i) {
+ if (labels[i] && (labels[i]->object.flags & GANV_ITEM_VISIBLE)) {
+ GANV_ITEM_GET_CLASS(labels[i])->draw(
+ labels[i], cr, cx, cy, cw, ch);
+ }
+ }
+}
+
+static void
+ganv_port_tail_vector(const GanvNode* self,
+ const GanvNode* head,
+ double* x,
+ double* y,
+ double* dx,
+ double* dy)
+{
+ GanvPort* port = GANV_PORT(self);
+ GanvItem* item = &port->box.node.item;
+ GanvCanvas* canvas = ganv_item_get_canvas(item);
+
+ const double px = item->impl->x;
+ const double py = item->impl->y;
+ const double border_width = GANV_NODE(port)->impl->border_width;
+
+ switch (ganv_canvas_get_direction(canvas)) {
+ case GANV_DIRECTION_RIGHT:
+ *x = px + ganv_box_get_width(&port->box) + (border_width / 2.0);
+ *y = py + ganv_box_get_height(&port->box) / 2.0;
+ *dx = 1.0;
+ *dy = 0.0;
+ break;
+ case GANV_DIRECTION_DOWN:
+ *x = px + ganv_box_get_width(&port->box) / 2.0;
+ *y = py + ganv_box_get_height(&port->box) + (border_width / 2.0);
+ *dx = 0.0;
+ *dy = 1.0;
+ break;
+ }
+
+ ganv_item_i2w(item->impl->parent, x, y);
+}
+
+static void
+ganv_port_head_vector(const GanvNode* self,
+ const GanvNode* tail,
+ double* x,
+ double* y,
+ double* dx,
+ double* dy)
+{
+ GanvPort* port = GANV_PORT(self);
+ GanvItem* item = &port->box.node.item;
+ GanvCanvas* canvas = ganv_item_get_canvas(item);
+
+ const double px = item->impl->x;
+ const double py = item->impl->y;
+ const double border_width = GANV_NODE(port)->impl->border_width;
+
+ switch (ganv_canvas_get_direction(canvas)) {
+ case GANV_DIRECTION_RIGHT:
+ *x = px - (border_width / 2.0);
+ *y = py + ganv_box_get_height(&port->box) / 2.0;
+ *dx = -1.0;
+ *dy = 0.0;
+ break;
+ case GANV_DIRECTION_DOWN:
+ *x = px + ganv_box_get_width(&port->box) / 2.0;
+ *y = py - (border_width / 2.0);
+ *dx = 0.0;
+ *dy = -1.0;
+ break;
+ }
+
+ ganv_item_i2w(item->impl->parent, x, y);
+}
+
+static void
+ganv_port_place_labels(GanvPort* port)
+{
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(port));
+ GanvPortPrivate* impl = port->impl;
+ GanvText* label = GANV_NODE(port)->impl->label;
+ const double port_w = ganv_box_get_width(&port->box);
+ const double port_h = ganv_box_get_height(&port->box);
+ double vlabel_w = 0.0;
+ if (impl->value_label) {
+ const double vlabel_h = impl->value_label->impl->coords.height;
+ vlabel_w = impl->value_label->impl->coords.width;
+ if (ganv_canvas_get_direction(canvas) == GANV_DIRECTION_RIGHT) {
+ ganv_item_set(GANV_ITEM(impl->value_label),
+ "x", PORT_LABEL_HPAD,
+ "y", (port_h - vlabel_h) / 2.0 - PORT_LABEL_VPAD,
+ NULL);
+ } else {
+ ganv_item_set(GANV_ITEM(impl->value_label),
+ "x", (port_w - vlabel_w) / 2.0,
+ "y", (port_h - vlabel_h) / 2.0 - PORT_LABEL_VPAD,
+ NULL);
+ }
+ vlabel_w += PORT_LABEL_HPAD;
+ }
+ if (label) {
+ const double label_h = label->impl->coords.height;
+ if (ganv_canvas_get_direction(canvas) == GANV_DIRECTION_RIGHT) {
+ ganv_item_set(GANV_ITEM(label),
+ "x", vlabel_w + PORT_LABEL_HPAD,
+ "y", (port_h - label_h) / 2.0 - PORT_LABEL_VPAD,
+ NULL);
+ }
+ }
+}
+
+static void
+ganv_port_resize(GanvNode* self)
+{
+ GanvPort* port = GANV_PORT(self);
+ GanvNode* node = GANV_NODE(self);
+ GanvText* label = node->impl->label;
+ GanvText* vlabel = port->impl->value_label;
+
+ double label_w = 0.0;
+ double label_h = 0.0;
+ double vlabel_w = 0.0;
+ double vlabel_h = 0.0;
+ if (label && (GANV_ITEM(label)->object.flags & GANV_ITEM_VISIBLE)) {
+ g_object_get(label, "width", &label_w, "height", &label_h, NULL);
+ }
+ if (vlabel && (GANV_ITEM(vlabel)->object.flags & GANV_ITEM_VISIBLE)) {
+ g_object_get(vlabel, "width", &vlabel_w, "height", &vlabel_h, NULL);
+ }
+
+ if (label || vlabel) {
+ double labels_w = label_w + PORT_LABEL_HPAD * 2.0;
+ if (vlabel_w != 0.0) {
+ labels_w += vlabel_w + PORT_LABEL_HPAD;
+ }
+ ganv_box_set_width(&port->box, labels_w);
+ ganv_box_set_height(&port->box,
+ MAX(label_h, vlabel_h) + (PORT_LABEL_VPAD * 2.0));
+
+ ganv_port_place_labels(port);
+ }
+
+ if (GANV_NODE_CLASS(parent_class)->resize) {
+ GANV_NODE_CLASS(parent_class)->resize(self);
+ }
+}
+
+static void
+ganv_port_redraw_text(GanvNode* node)
+{
+ GanvPort* port = GANV_PORT(node);
+ if (port->impl->value_label) {
+ ganv_text_layout(port->impl->value_label);
+ }
+ if (GANV_NODE_CLASS(parent_class)->redraw_text) {
+ (*GANV_NODE_CLASS(parent_class)->redraw_text)(node);
+ }
+ ganv_port_place_labels(port);
+}
+
+static void
+ganv_port_set_width(GanvBox* box,
+ double width)
+{
+ GanvPort* port = GANV_PORT(box);
+ parent_class->set_width(box, width);
+ if (port->impl->control) {
+ ganv_port_update_control_slider(port, port->impl->control->value, TRUE);
+ }
+ ganv_port_place_labels(port);
+}
+
+static void
+ganv_port_set_height(GanvBox* box,
+ double height)
+{
+ GanvPort* port = GANV_PORT(box);
+ parent_class->set_height(box, height);
+ if (port->impl->control) {
+ ganv_item_set(GANV_ITEM(port->impl->control->rect),
+ "y1", box->impl->coords.border_width / 2.0,
+ "y2", height - box->impl->coords.border_width / 2.0,
+ NULL);
+ }
+ ganv_port_place_labels(port);
+}
+
+static gboolean
+ganv_port_event(GanvItem* item, GdkEvent* event)
+{
+ GanvCanvas* canvas = ganv_item_get_canvas(item);
+
+ return ganv_canvas_port_event(canvas, GANV_PORT(item), event);
+}
+
+static void
+ganv_port_class_init(GanvPortClass* klass)
+{
+ GObjectClass* gobject_class = (GObjectClass*)klass;
+ GtkObjectClass* object_class = (GtkObjectClass*)klass;
+ GanvItemClass* item_class = (GanvItemClass*)klass;
+ GanvNodeClass* node_class = (GanvNodeClass*)klass;
+ GanvBoxClass* box_class = (GanvBoxClass*)klass;
+
+ parent_class = GANV_BOX_CLASS(g_type_class_peek_parent(klass));
+
+ gobject_class->set_property = ganv_port_set_property;
+ gobject_class->get_property = ganv_port_get_property;
+
+ g_object_class_install_property(
+ gobject_class, PROP_IS_INPUT, g_param_spec_boolean(
+ "is-input",
+ _("Is input"),
+ _("Whether this port is an input, rather than an output."),
+ 0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_IS_CONTROLLABLE, g_param_spec_boolean(
+ "is-controllable",
+ _("Is controllable"),
+ _("Whether this port can be controlled by the user."),
+ 0,
+ G_PARAM_READWRITE));
+
+ port_signals[PORT_VALUE_CHANGED]
+ = g_signal_new("value-changed",
+ G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE, 1,
+ G_TYPE_DOUBLE);
+
+ object_class->destroy = ganv_port_destroy;
+
+ item_class->update = ganv_port_update;
+ item_class->event = ganv_port_event;
+ item_class->draw = ganv_port_draw;
+
+ node_class->tail_vector = ganv_port_tail_vector;
+ node_class->head_vector = ganv_port_head_vector;
+ node_class->resize = ganv_port_resize;
+ node_class->redraw_text = ganv_port_redraw_text;
+
+ box_class->set_width = ganv_port_set_width;
+ box_class->set_height = ganv_port_set_height;
+}
+
+GanvPort*
+ganv_port_new(GanvModule* module,
+ gboolean is_input,
+ const char* first_prop_name, ...)
+{
+ GanvPort* port = GANV_PORT(g_object_new(ganv_port_get_type(), NULL));
+
+ port->impl->is_input = is_input;
+
+ GanvItem* item = GANV_ITEM(port);
+ va_list args;
+ va_start(args, first_prop_name);
+ ganv_item_construct(item,
+ GANV_ITEM(module),
+ first_prop_name, args);
+ va_end(args);
+
+ GanvBox* box = GANV_BOX(port);
+ box->impl->coords.border_width = 1.0;
+
+ GanvNode* node = GANV_NODE(port);
+ node->impl->can_tail = !is_input;
+ node->impl->can_head = is_input;
+ node->impl->draggable = FALSE;
+ node->impl->border_width = 2.0;
+
+ GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(port));
+ ganv_port_set_direction(port, ganv_canvas_get_direction(canvas));
+
+ return port;
+}
+
+void
+ganv_port_set_direction(GanvPort* port,
+ GanvDirection direction)
+{
+ GanvNode* node = GANV_NODE(port);
+ GanvBox* box = GANV_BOX(port);
+ gboolean is_input = port->impl->is_input;
+ switch (direction) {
+ case GANV_DIRECTION_RIGHT:
+ box->impl->radius_tl = (is_input ? 0.0 : 5.0);
+ box->impl->radius_tr = (is_input ? 5.0 : 0.0);
+ box->impl->radius_br = (is_input ? 5.0 : 0.0);
+ box->impl->radius_bl = (is_input ? 0.0 : 5.0);
+ break;
+ case GANV_DIRECTION_DOWN:
+ box->impl->radius_tl = (is_input ? 0.0 : 5.0);
+ box->impl->radius_tr = (is_input ? 0.0 : 5.0);
+ box->impl->radius_br = (is_input ? 5.0 : 0.0);
+ box->impl->radius_bl = (is_input ? 5.0 : 0.0);
+ break;
+ }
+
+ node->impl->must_resize = TRUE;
+ ganv_item_request_update(GANV_ITEM(node));
+}
+
+void
+ganv_port_show_control(GanvPort* port)
+{
+ if (port->impl->control) {
+ return;
+ }
+
+ const guint color = 0xFFFFFF66;
+ const double border_width = GANV_NODE(port)->impl->border_width;
+
+ GanvPortControl* control = (GanvPortControl*)malloc(sizeof(GanvPortControl));
+ port->impl->control = control;
+
+ control->value = 0.0f;
+ control->min = 0.0f;
+ control->max = 1.0f;
+ control->is_toggle = FALSE;
+ control->is_integer = FALSE;
+ control->rect = GANV_BOX(
+ ganv_item_new(GANV_ITEM(port),
+ ganv_box_get_type(),
+ "x1", border_width / 2.0,
+ "y1", border_width / 2.0,
+ "x2", 0.0,
+ "y2", ganv_box_get_height(&port->box) - border_width / 2.0,
+ "fill-color", color,
+ "border-color", color,
+ "border-width", 0.0,
+ "managed", TRUE,
+ NULL));
+ ganv_item_show(GANV_ITEM(control->rect));
+}
+
+void
+ganv_port_hide_control(GanvPort* port)
+{
+ gtk_object_destroy(GTK_OBJECT(port->impl->control->rect));
+ free(port->impl->control);
+ port->impl->control = NULL;
+}
+
+void
+ganv_port_set_value_label(GanvPort* port,
+ const char* str)
+{
+ GanvPortPrivate* impl = port->impl;
+
+ if (!str || str[0] == '\0') {
+ if (impl->value_label) {
+ gtk_object_destroy(GTK_OBJECT(impl->value_label));
+ impl->value_label = NULL;
+ }
+ } else if (impl->value_label) {
+ ganv_item_set(GANV_ITEM(impl->value_label),
+ "text", str,
+ NULL);
+ } else {
+ impl->value_label = GANV_TEXT(ganv_item_new(GANV_ITEM(port),
+ ganv_text_get_type(),
+ "text", str,
+ "color", DIM_TEXT_COLOR,
+ "managed", TRUE,
+ NULL));
+ }
+}
+
+static void
+ganv_port_update_control_slider(GanvPort* port, float value, gboolean force)
+{
+ GanvPortPrivate* impl = port->impl;
+ if (!impl->control) {
+ return;
+ }
+
+ // Clamp to toggle or integer value if applicable
+ if (impl->control->is_toggle) {
+ if (value != 0.0f) {
+ value = impl->control->max;
+ } else {
+ value = impl->control->min;
+ }
+ } else if (impl->control->is_integer) {
+ value = lrintf(value);
+ }
+
+ // Clamp to range
+ if (value < impl->control->min) {
+ value = impl->control->min;
+ }
+ if (value > impl->control->max) {
+ value = impl->control->max;
+ }
+
+ if (!force && value == impl->control->value) {
+ return; // No change, do nothing
+ }
+
+ const double span = (ganv_box_get_width(&port->box) -
+ GANV_NODE(port)->impl->border_width);
+
+ const double w = (value - impl->control->min)
+ / (impl->control->max - impl->control->min)
+ * span;
+
+ if (isnan(w)) {
+ return; // Shouldn't happen, but ignore crazy values
+ }
+
+ // Redraw port
+ impl->control->value = value;
+ ganv_box_set_width(impl->control->rect, MAX(0.0, w));
+ ganv_box_request_redraw(
+ GANV_ITEM(port), &GANV_BOX(port)->impl->coords, FALSE);
+}
+
+void
+ganv_port_set_control_is_toggle(GanvPort* port,
+ gboolean is_toggle)
+{
+ if (port->impl->control) {
+ port->impl->control->is_toggle = is_toggle;
+ ganv_port_update_control_slider(port, port->impl->control->value, TRUE);
+ }
+}
+
+void
+ganv_port_set_control_is_integer(GanvPort* port,
+ gboolean is_integer)
+{
+ if (port->impl->control) {
+ port->impl->control->is_integer = is_integer;
+ const float rounded = rintf(port->impl->control->value);
+ ganv_port_update_control_slider(port, rounded, TRUE);
+ }
+}
+
+void
+ganv_port_set_control_value(GanvPort* port,
+ float value)
+{
+ ganv_port_update_control_slider(port, value, FALSE);
+}
+
+void
+ganv_port_set_control_value_internal(GanvPort* port,
+ float value)
+{
+ // Update slider
+ ganv_port_set_control_value(port, value);
+
+ // Fire signal to notify user value has changed
+ const double dvalue = port->impl->control->value;
+ g_signal_emit(port, port_signals[PORT_VALUE_CHANGED], 0, dvalue, NULL);
+}
+
+void
+ganv_port_set_control_min(GanvPort* port,
+ float min)
+{
+ if (port->impl->control) {
+ const gboolean force = port->impl->control->min != min;
+ port->impl->control->min = min;
+ if (port->impl->control->max < min) {
+ port->impl->control->max = min;
+ }
+ ganv_port_update_control_slider(port, port->impl->control->value, force);
+ }
+}
+
+void
+ganv_port_set_control_max(GanvPort* port,
+ float max)
+{
+ if (port->impl->control) {
+ const gboolean force = port->impl->control->max != max;
+ port->impl->control->max = max;
+ if (port->impl->control->min > max) {
+ port->impl->control->min = max;
+ }
+ ganv_port_update_control_slider(port, port->impl->control->value, force);
+ }
+}
+
+double
+ganv_port_get_natural_width(const GanvPort* port)
+{
+ GanvCanvas* const canvas = ganv_item_get_canvas(GANV_ITEM(port));
+ GanvText* const label = port->box.node.impl->label;
+ double w = 0.0;
+ if (ganv_canvas_get_direction(canvas) == GANV_DIRECTION_DOWN) {
+ w = ganv_module_get_empty_port_breadth(ganv_port_get_module(port));
+ } else if (label && (GANV_ITEM(label)->object.flags & GANV_ITEM_VISIBLE)) {
+ double label_w;
+ g_object_get(port->box.node.impl->label, "width", &label_w, NULL);
+ w = label_w + (PORT_LABEL_HPAD * 2.0);
+ } else {
+ w = ganv_module_get_empty_port_depth(ganv_port_get_module(port));
+ }
+ if (port->impl->value_label &&
+ (GANV_ITEM(port->impl->value_label)->object.flags
+ & GANV_ITEM_VISIBLE)) {
+ double label_w;
+ g_object_get(port->impl->value_label, "width", &label_w, NULL);
+ w += label_w + PORT_LABEL_HPAD;
+ }
+ return w;
+}
+
+GanvModule*
+ganv_port_get_module(const GanvPort* port)
+{
+ return GANV_MODULE(GANV_ITEM(port)->impl->parent);
+}
+
+float
+ganv_port_get_control_value(const GanvPort* port)
+{
+ return port->impl->control ? port->impl->control->value : 0.0f;
+}
+
+float
+ganv_port_get_control_min(const GanvPort* port)
+{
+ return port->impl->control ? port->impl->control->min : 0.0f;
+}
+
+float
+ganv_port_get_control_max(const GanvPort* port)
+{
+ return port->impl->control ? port->impl->control->max : 0.0f;
+}
+
+gboolean
+ganv_port_is_input(const GanvPort* port)
+{
+ return port->impl->is_input;
+}
+
+gboolean
+ganv_port_is_output(const GanvPort* port)
+{
+ return !port->impl->is_input;
+}
diff --git a/src/text.c b/src/text.c
new file mode 100644
index 0000000..bbac187
--- /dev/null
+++ b/src/text.c
@@ -0,0 +1,381 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2015 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <gtk/gtkstyle.h>
+
+#include "ganv/canvas.h"
+#include "ganv/text.h"
+
+#include "./color.h"
+#include "./boilerplate.h"
+#include "./gettext.h"
+#include "./ganv-private.h"
+
+G_DEFINE_TYPE_WITH_CODE(GanvText, ganv_text, GANV_TYPE_ITEM,
+ G_ADD_PRIVATE(GanvText))
+
+static GanvItemClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_TEXT,
+ PROP_X,
+ PROP_Y,
+ PROP_WIDTH,
+ PROP_HEIGHT,
+ PROP_COLOR,
+ PROP_FONT_SIZE
+};
+
+static void
+ganv_text_init(GanvText* text)
+{
+ GanvTextPrivate* impl = ganv_text_get_instance_private(text);
+
+ text->impl = impl;
+
+ memset(&impl->coords, '\0', sizeof(GanvTextCoords));
+ impl->coords.width = 1.0;
+ impl->coords.height = 1.0;
+ impl->old_coords = impl->coords;
+
+ impl->layout = NULL;
+ impl->text = NULL;
+ impl->font_size = 0.0;
+ impl->color = DEFAULT_TEXT_COLOR;
+ impl->needs_layout = FALSE;
+}
+
+static void
+ganv_text_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_TEXT(object));
+
+ GanvText* text = GANV_TEXT(object);
+ GanvTextPrivate* impl = text->impl;
+
+ if (impl->text) {
+ g_free(impl->text);
+ impl->text = NULL;
+ }
+
+ if (impl->layout) {
+ g_object_unref(impl->layout);
+ impl->layout = NULL;
+ }
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+}
+
+void
+ganv_text_layout(GanvText* text)
+{
+ GanvTextPrivate* impl = text->impl;
+ GanvItem* item = GANV_ITEM(text);
+ GanvCanvas* canvas = ganv_item_get_canvas(item);
+ GtkWidget* widget = GTK_WIDGET(canvas);
+ double points = impl->font_size;
+ GtkStyle* style = gtk_rc_get_style(widget);
+
+ if (impl->font_size == 0.0) {
+ points = ganv_canvas_get_font_size(canvas);
+ }
+
+ if (impl->layout) {
+ g_object_unref(impl->layout);
+ }
+ impl->layout = gtk_widget_create_pango_layout(widget, impl->text);
+
+ PangoFontDescription* font = pango_font_description_copy(style->font_desc);
+ PangoContext* ctx = pango_layout_get_context(impl->layout);
+ cairo_font_options_t* opt = cairo_font_options_copy(
+ pango_cairo_context_get_font_options(ctx));
+
+ pango_font_description_set_size(font, points * (double)PANGO_SCALE);
+ pango_layout_set_font_description(impl->layout, font);
+ pango_cairo_context_set_font_options(ctx, opt);
+ cairo_font_options_destroy(opt);
+ pango_font_description_free(font);
+
+ int width, height;
+ pango_layout_get_pixel_size(impl->layout, &width, &height);
+
+ impl->coords.width = width;
+ impl->coords.height = height;
+ impl->needs_layout = FALSE;
+
+ ganv_item_request_update(GANV_ITEM(text));
+}
+
+static void
+ganv_text_set_property(GObject* object,
+ guint prop_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_TEXT(object));
+
+ GanvText* text = GANV_TEXT(object);
+ GanvTextPrivate* impl = text->impl;
+
+ switch (prop_id) {
+ case PROP_X:
+ impl->coords.x = g_value_get_double(value);
+ break;
+ case PROP_Y:
+ impl->coords.y = g_value_get_double(value);
+ break;
+ case PROP_COLOR:
+ impl->color = g_value_get_uint(value);
+ break;
+ case PROP_FONT_SIZE:
+ impl->font_size = g_value_get_double(value);
+ impl->needs_layout = TRUE;
+ break;
+ case PROP_TEXT:
+ free(impl->text);
+ impl->text = g_value_dup_string(value);
+ impl->needs_layout = TRUE;
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ return;
+ }
+ if (impl->needs_layout) {
+ if (GANV_IS_NODE(GANV_ITEM(text)->impl->parent)) {
+ GANV_NODE(GANV_ITEM(text)->impl->parent)->impl->must_resize = TRUE;
+ }
+ }
+ ganv_item_request_update(GANV_ITEM(text));
+}
+
+static void
+ganv_text_get_property(GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_TEXT(object));
+
+ GanvText* text = GANV_TEXT(object);
+ GanvTextPrivate* impl = text->impl;
+
+ if (impl->needs_layout && (prop_id == PROP_WIDTH
+ || prop_id == PROP_HEIGHT)) {
+ ganv_text_layout(text);
+ }
+
+ switch (prop_id) {
+ GET_CASE(TEXT, string, impl->text);
+ GET_CASE(X, double, impl->coords.x);
+ GET_CASE(Y, double, impl->coords.y);
+ GET_CASE(WIDTH, double, impl->coords.width);
+ GET_CASE(HEIGHT, double, impl->coords.height);
+ GET_CASE(COLOR, uint, impl->color);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_text_bounds_item(GanvItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ GanvText* text = GANV_TEXT(item);
+ GanvTextPrivate* impl = text->impl;
+
+ if (impl->needs_layout) {
+ ganv_text_layout(text);
+ }
+
+ *x1 = impl->coords.x;
+ *y1 = impl->coords.y;
+ *x2 = impl->coords.x + impl->coords.width;
+ *y2 = impl->coords.y + impl->coords.height;
+}
+
+static void
+ganv_text_bounds(GanvItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ ganv_text_bounds_item(item, x1, y1, x2, y2);
+}
+
+static void
+ganv_text_update(GanvItem* item, int flags)
+{
+ // Update world-relative bounding box
+ ganv_text_bounds(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
+ ganv_item_i2w_pair(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
+
+ ganv_canvas_request_redraw_w(
+ item->impl->canvas, item->impl->x1, item->impl->y1, item->impl->x2, item->impl->y2);
+
+ parent_class->update(item, flags);
+}
+
+static double
+ganv_text_point(GanvItem* item, double x, double y, GanvItem** actual_item)
+{
+ *actual_item = NULL;
+
+ double x1, y1, x2, y2;
+ ganv_text_bounds_item(item, &x1, &y1, &x2, &y2);
+ if ((x >= x1) && (y >= y1) && (x <= x2) && (y <= y2)) {
+ return 0.0;
+ }
+
+ // Point is outside the box
+ double dx, dy;
+
+ // Find horizontal distance to nearest edge
+ if (x < x1) {
+ dx = x1 - x;
+ } else if (x > x2) {
+ dx = x - x2;
+ } else {
+ dx = 0.0;
+ }
+
+ // Find vertical distance to nearest edge
+ if (y < y1) {
+ dy = y1 - y;
+ } else if (y > y2) {
+ dy = y - y2;
+ } else {
+ dy = 0.0;
+ }
+
+ return sqrt((dx * dx) + (dy * dy));
+}
+
+static void
+ganv_text_draw(GanvItem* item,
+ cairo_t* cr, double cx, double cy, double cw, double ch)
+{
+ GanvText* text = GANV_TEXT(item);
+ GanvTextPrivate* impl = text->impl;
+
+ double wx = impl->coords.x;
+ double wy = impl->coords.y;
+ ganv_item_i2w(item, &wx, &wy);
+
+ if (impl->needs_layout) {
+ ganv_text_layout(text);
+ }
+
+ double r, g, b, a;
+ color_to_rgba(impl->color, &r, &g, &b, &a);
+
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_move_to(cr, wx, wy);
+ pango_cairo_show_layout(cr, impl->layout);
+}
+
+static void
+ganv_text_class_init(GanvTextClass* klass)
+{
+ GObjectClass* gobject_class = (GObjectClass*)klass;
+ GtkObjectClass* object_class = (GtkObjectClass*)klass;
+ GanvItemClass* item_class = (GanvItemClass*)klass;
+
+ parent_class = GANV_ITEM_CLASS(g_type_class_peek_parent(klass));
+
+ gobject_class->set_property = ganv_text_set_property;
+ gobject_class->get_property = ganv_text_get_property;
+
+ g_object_class_install_property(
+ gobject_class, PROP_TEXT, g_param_spec_string(
+ "text",
+ _("Text"),
+ _("The string to display."),
+ NULL,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_X, g_param_spec_double(
+ "x",
+ _("x"),
+ _("Top left x coordinate."),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_Y, g_param_spec_double(
+ "y",
+ _("y"),
+ _("Top left y coordinate."),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_WIDTH, g_param_spec_double(
+ "width",
+ _("Width"),
+ _("The current width of the text."),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 1.0,
+ G_PARAM_READABLE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_HEIGHT, g_param_spec_double(
+ "height",
+ _("Height"),
+ _("The current height of the text."),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 1.0,
+ G_PARAM_READABLE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_COLOR, g_param_spec_uint(
+ "color",
+ _("Color"),
+ _("The color of the text."),
+ 0, G_MAXUINT,
+ DEFAULT_TEXT_COLOR,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_FONT_SIZE, g_param_spec_double(
+ "font-size",
+ _("Font size"),
+ _("The font size in points."),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+
+ object_class->destroy = ganv_text_destroy;
+
+ item_class->update = ganv_text_update;
+ item_class->bounds = ganv_text_bounds;
+ item_class->point = ganv_text_point;
+ item_class->draw = ganv_text_draw;
+}
diff --git a/src/widget.c b/src/widget.c
new file mode 100644
index 0000000..b08715b
--- /dev/null
+++ b/src/widget.c
@@ -0,0 +1,503 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2016 David Robillard <http://drobilla.net>
+ *
+ * Ganv is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or any later version.
+ *
+ * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with Ganv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Based on GnomeCanvasWidget, by Federico Mena <federico@nuclecu.unam.mx>
+ * Copyright 1997-2000 Free Software Foundation
+ */
+
+#include <math.h>
+
+#include <gtk/gtksignal.h>
+
+#include "ganv/canvas.h"
+#include "ganv/widget.h"
+
+#include "./gettext.h"
+#include "./ganv-private.h"
+
+G_DEFINE_TYPE_WITH_CODE(GanvWidget, ganv_widget, GANV_TYPE_ITEM,
+ G_ADD_PRIVATE(GanvWidget))
+
+static GanvItemClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_WIDGET,
+ PROP_X,
+ PROP_Y,
+ PROP_WIDTH,
+ PROP_HEIGHT,
+ PROP_ANCHOR,
+ PROP_SIZE_PIXELS
+};
+
+static void
+ganv_widget_init(GanvWidget* witem)
+{
+ GanvWidgetPrivate* impl = ganv_widget_get_instance_private(witem);
+
+ witem->impl = impl;
+ witem->impl->x = 0.0;
+ witem->impl->y = 0.0;
+ witem->impl->width = 0.0;
+ witem->impl->height = 0.0;
+ witem->impl->anchor = GTK_ANCHOR_NW;
+ witem->impl->size_pixels = FALSE;
+}
+
+static void
+ganv_widget_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_WIDGET(object));
+
+ GanvWidget* witem = GANV_WIDGET(object);
+
+ if (witem->impl->widget && !witem->impl->in_destroy) {
+ g_signal_handler_disconnect(witem->impl->widget, witem->impl->destroy_id);
+ gtk_widget_destroy(witem->impl->widget);
+ witem->impl->widget = NULL;
+ }
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+}
+
+static void
+recalc_bounds(GanvWidget* witem)
+{
+ GanvItem* item = GANV_ITEM(witem);
+
+ /* Get world coordinates */
+
+ double wx = witem->impl->x;
+ double wy = witem->impl->y;
+ ganv_item_i2w(item, &wx, &wy);
+
+ /* Get canvas pixel coordinates */
+
+ ganv_canvas_w2c(item->impl->canvas, wx, wy, &witem->impl->cx, &witem->impl->cy);
+
+ /* Anchor widget item */
+
+ switch (witem->impl->anchor) {
+ case GTK_ANCHOR_N:
+ case GTK_ANCHOR_CENTER:
+ case GTK_ANCHOR_S:
+ witem->impl->cx -= witem->impl->cwidth / 2;
+ break;
+
+ case GTK_ANCHOR_NE:
+ case GTK_ANCHOR_E:
+ case GTK_ANCHOR_SE:
+ witem->impl->cx -= witem->impl->cwidth;
+ break;
+
+ default:
+ break;
+ }
+
+ switch (witem->impl->anchor) {
+ case GTK_ANCHOR_W:
+ case GTK_ANCHOR_CENTER:
+ case GTK_ANCHOR_E:
+ witem->impl->cy -= witem->impl->cheight / 2;
+ break;
+
+ case GTK_ANCHOR_SW:
+ case GTK_ANCHOR_S:
+ case GTK_ANCHOR_SE:
+ witem->impl->cy -= witem->impl->cheight;
+ break;
+
+ default:
+ break;
+ }
+
+ /* Bounds */
+
+ item->impl->x1 = witem->impl->cx;
+ item->impl->y1 = witem->impl->cy;
+ item->impl->x2 = witem->impl->cx + witem->impl->cwidth;
+ item->impl->y2 = witem->impl->cy + witem->impl->cheight;
+
+ int zoom_xofs, zoom_yofs;
+ ganv_canvas_get_zoom_offsets(item->impl->canvas, &zoom_xofs, &zoom_yofs);
+ if (witem->impl->widget) {
+ gtk_layout_move(GTK_LAYOUT(item->impl->canvas), witem->impl->widget,
+ witem->impl->cx + zoom_xofs,
+ witem->impl->cy + zoom_yofs);
+ }
+}
+
+static void
+do_destroy(GtkObject* object, gpointer data)
+{
+ GanvWidget* witem = GANV_WIDGET(data);
+
+ witem->impl->in_destroy = TRUE;
+ gtk_object_destroy(GTK_OBJECT(data));
+}
+
+static void
+ganv_widget_set_property(GObject* object,
+ guint param_id,
+ const GValue* value,
+ GParamSpec* pspec)
+{
+ GanvItem* item = GANV_ITEM(object);
+ GanvWidget* witem = GANV_WIDGET(object);
+ int update = FALSE;
+ int calc_bounds = FALSE;
+ GObject* obj;
+
+ switch (param_id) {
+ case PROP_WIDGET:
+ if (witem->impl->widget) {
+ g_signal_handler_disconnect(witem->impl->widget, witem->impl->destroy_id);
+ gtk_container_remove(GTK_CONTAINER(item->impl->canvas), witem->impl->widget);
+ }
+
+ obj = (GObject*)g_value_get_object(value);
+ if (obj) {
+ witem->impl->widget = GTK_WIDGET(obj);
+ witem->impl->destroy_id = g_signal_connect(obj, "destroy",
+ G_CALLBACK(do_destroy),
+ witem);
+ int zoom_xofs, zoom_yofs;
+ ganv_canvas_get_zoom_offsets(item->impl->canvas, &zoom_xofs, &zoom_yofs);
+
+ gtk_layout_put(GTK_LAYOUT(item->impl->canvas), witem->impl->widget,
+ witem->impl->cx + zoom_xofs,
+ witem->impl->cy + zoom_yofs);
+ }
+
+ update = TRUE;
+ break;
+
+ case PROP_X:
+ if (witem->impl->x != g_value_get_double(value)) {
+ witem->impl->x = g_value_get_double(value);
+ calc_bounds = TRUE;
+ }
+ break;
+
+ case PROP_Y:
+ if (witem->impl->y != g_value_get_double(value)) {
+ witem->impl->y = g_value_get_double(value);
+ calc_bounds = TRUE;
+ }
+ break;
+
+ case PROP_WIDTH:
+ if (witem->impl->width != fabs(g_value_get_double(value))) {
+ witem->impl->width = fabs(g_value_get_double(value));
+ update = TRUE;
+ }
+ break;
+
+ case PROP_HEIGHT:
+ if (witem->impl->height != fabs(g_value_get_double(value))) {
+ witem->impl->height = fabs(g_value_get_double(value));
+ update = TRUE;
+ }
+ break;
+
+ case PROP_ANCHOR:
+ if (witem->impl->anchor != (GtkAnchorType)g_value_get_enum(value)) {
+ witem->impl->anchor = (GtkAnchorType)g_value_get_enum(value);
+ update = TRUE;
+ }
+ break;
+
+ case PROP_SIZE_PIXELS:
+ if (witem->impl->size_pixels != g_value_get_boolean(value)) {
+ witem->impl->size_pixels = g_value_get_boolean(value);
+ update = TRUE;
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
+ break;
+ }
+
+ if (update) {
+ (*GANV_ITEM_GET_CLASS(item)->update)(item, 0);
+ }
+
+ if (calc_bounds) {
+ recalc_bounds(witem);
+ }
+}
+
+static void
+ganv_widget_get_property(GObject* object,
+ guint param_id,
+ GValue* value,
+ GParamSpec* pspec)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_WIDGET(object));
+
+ GanvWidget* witem = GANV_WIDGET(object);
+
+ switch (param_id) {
+ case PROP_WIDGET:
+ g_value_set_object(value, (GObject*)witem->impl->widget);
+ break;
+
+ case PROP_X:
+ g_value_set_double(value, witem->impl->x);
+ break;
+
+ case PROP_Y:
+ g_value_set_double(value, witem->impl->y);
+ break;
+
+ case PROP_WIDTH:
+ g_value_set_double(value, witem->impl->width);
+ break;
+
+ case PROP_HEIGHT:
+ g_value_set_double(value, witem->impl->height);
+ break;
+
+ case PROP_ANCHOR:
+ g_value_set_enum(value, witem->impl->anchor);
+ break;
+
+ case PROP_SIZE_PIXELS:
+ g_value_set_boolean(value, witem->impl->size_pixels);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_widget_update(GanvItem* item, int flags)
+{
+ GanvWidget* witem = GANV_WIDGET(item);
+
+ if (parent_class->update) {
+ (*parent_class->update)(item, flags);
+ }
+
+ if (witem->impl->widget) {
+ const double pixels_per_unit = ganv_canvas_get_zoom(item->impl->canvas);
+ if (witem->impl->size_pixels) {
+ witem->impl->cwidth = (int)(witem->impl->width + 0.5);
+ witem->impl->cheight = (int)(witem->impl->height + 0.5);
+ } else {
+ witem->impl->cwidth = (int)(witem->impl->width * pixels_per_unit + 0.5);
+ witem->impl->cheight = (int)(witem->impl->height * pixels_per_unit + 0.5);
+ }
+
+ gtk_widget_set_size_request(witem->impl->widget, witem->impl->cwidth, witem->impl->cheight);
+ } else {
+ witem->impl->cwidth = 0.0;
+ witem->impl->cheight = 0.0;
+ }
+
+ recalc_bounds(witem);
+}
+
+static void
+ganv_widget_draw(GanvItem* item,
+ cairo_t* cr, double cx, double cy, double cw, double ch)
+{
+ GanvWidget* witem = GANV_WIDGET(item);
+
+ if (witem->impl->widget) {
+ gtk_widget_queue_draw(witem->impl->widget);
+ }
+}
+
+static double
+ganv_widget_point(GanvItem* item, double x, double y, GanvItem** actual_item)
+{
+ GanvWidget* witem = GANV_WIDGET(item);
+
+ *actual_item = item;
+
+ double x1, y1;
+ ganv_canvas_c2w(item->impl->canvas, witem->impl->cx, witem->impl->cy, &x1, &y1);
+
+ const double pixels_per_unit = ganv_canvas_get_zoom(item->impl->canvas);
+
+ double x2 = x1 + (witem->impl->cwidth - 1) / pixels_per_unit;
+ double y2 = y1 + (witem->impl->cheight - 1) / pixels_per_unit;
+
+ /* Is point inside widget bounds? */
+
+ if ((x >= x1) && (y >= y1) && (x <= x2) && (y <= y2)) {
+ return 0.0;
+ }
+
+ /* Point is outside widget bounds */
+
+ double dx;
+ if (x < x1) {
+ dx = x1 - x;
+ } else if (x > x2) {
+ dx = x - x2;
+ } else {
+ dx = 0.0;
+ }
+
+ double dy;
+ if (y < y1) {
+ dy = y1 - y;
+ } else if (y > y2) {
+ dy = y - y2;
+ } else {
+ dy = 0.0;
+ }
+
+ return sqrt(dx * dx + dy * dy);
+}
+
+static void
+ganv_widget_bounds(GanvItem* item, double* x1, double* y1, double* x2, double* y2)
+{
+ GanvWidget* witem = GANV_WIDGET(item);
+
+ *x1 = witem->impl->x;
+ *y1 = witem->impl->y;
+
+ switch (witem->impl->anchor) {
+ case GTK_ANCHOR_NW:
+ case GTK_ANCHOR_W:
+ case GTK_ANCHOR_SW:
+ break;
+
+ case GTK_ANCHOR_N:
+ case GTK_ANCHOR_CENTER:
+ case GTK_ANCHOR_S:
+ *x1 -= witem->impl->width / 2.0;
+ break;
+
+ case GTK_ANCHOR_NE:
+ case GTK_ANCHOR_E:
+ case GTK_ANCHOR_SE:
+ *x1 -= witem->impl->width;
+ break;
+
+ default:
+ break;
+ }
+
+ switch (witem->impl->anchor) {
+ case GTK_ANCHOR_NW:
+ case GTK_ANCHOR_N:
+ case GTK_ANCHOR_NE:
+ break;
+
+ case GTK_ANCHOR_W:
+ case GTK_ANCHOR_CENTER:
+ case GTK_ANCHOR_E:
+ *y1 -= witem->impl->height / 2.0;
+ break;
+
+ case GTK_ANCHOR_SW:
+ case GTK_ANCHOR_S:
+ case GTK_ANCHOR_SE:
+ *y1 -= witem->impl->height;
+ break;
+
+ default:
+ break;
+ }
+
+ *x2 = *x1 + witem->impl->width;
+ *y2 = *y1 + witem->impl->height;
+}
+
+static void
+ganv_widget_class_init(GanvWidgetClass* klass)
+{
+ GObjectClass* gobject_class = (GObjectClass*)klass;
+ GtkObjectClass* object_class = (GtkObjectClass*)klass;
+ GanvItemClass* item_class = (GanvItemClass*)klass;
+
+ parent_class = (GanvItemClass*)g_type_class_peek_parent(klass);
+
+ gobject_class->set_property = ganv_widget_set_property;
+ gobject_class->get_property = ganv_widget_get_property;
+
+ g_object_class_install_property(
+ gobject_class, PROP_WIDGET, g_param_spec_object(
+ "widget", _("Widget"),
+ _("The widget to embed in this item."),
+ GTK_TYPE_WIDGET,
+ (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE)));
+
+ g_object_class_install_property(
+ gobject_class, PROP_X, g_param_spec_double(
+ "x", _("x"),
+ _("The x coordinate of the anchor"),
+ -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_Y, g_param_spec_double(
+ "y", _("y"),
+ _("The x coordinate of the anchor"),
+ -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_WIDTH, g_param_spec_double(
+ "width", _("Width"),
+ _("The width of the widget."),
+ -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_HEIGHT, g_param_spec_double(
+ "height", _("Height"),
+ _("The height of the widget."),
+ -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_ANCHOR, g_param_spec_enum(
+ "anchor", _("Anchor"),
+ _("The anchor point of the widget."),
+ GTK_TYPE_ANCHOR_TYPE,
+ GTK_ANCHOR_NW,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_SIZE_PIXELS, g_param_spec_boolean(
+ "size-pixels", ("Size is in pixels"),
+ _("Specifies whether the widget size is specified in pixels or"
+ " canvas units. If it is in pixels, then the widget will not"
+ " be scaled when the canvas zoom factor changes. Otherwise,"
+ " it will be scaled."),
+ FALSE,
+ G_PARAM_READWRITE));
+
+ object_class->destroy = ganv_widget_destroy;
+
+ item_class->update = ganv_widget_update;
+ item_class->point = ganv_widget_point;
+ item_class->bounds = ganv_widget_bounds;
+ item_class->draw = ganv_widget_draw;
+}