summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2011-12-06 21:01:38 +0000
committerDavid Robillard <d@drobilla.net>2011-12-06 21:01:38 +0000
commit0731f12beaa0cfc0de56dc05ca3814143fd394a5 (patch)
treed8a98ee48badba378172d3a1c46fba2f2e266d37 /src
downloadganv-0731f12beaa0cfc0de56dc05ca3814143fd394a5.tar.gz
ganv-0731f12beaa0cfc0de56dc05ca3814143fd394a5.tar.bz2
ganv-0731f12beaa0cfc0de56dc05ca3814143fd394a5.zip
FlowCanvas's successor is hereby dubbed Ganv.
git-svn-id: http://svn.drobilla.net/lad/trunk/ganv@3820 a436a847-0d15-0410-975c-d299462d15a1
Diffstat (limited to 'src')
-rw-r--r--src/Canvas.cpp1953
-rw-r--r--src/Port.cpp52
-rw-r--r--src/boilerplate.h45
-rw-r--r--src/box.c504
-rw-r--r--src/circle.c351
-rw-r--r--src/color.h51
-rw-r--r--src/edge.c658
-rw-r--r--src/ganv-private.h69
-rw-r--r--src/ganv_bench.cpp182
-rw-r--r--src/gettext.h28
-rw-r--r--src/module.c792
-rw-r--r--src/node.c546
-rw-r--r--src/port.c420
-rw-r--r--src/text.c371
14 files changed, 6022 insertions, 0 deletions
diff --git a/src/Canvas.cpp b/src/Canvas.cpp
new file mode 100644
index 0000000..a401283
--- /dev/null
+++ b/src/Canvas.cpp
@@ -0,0 +1,1953 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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/>.
+ */
+
+#define _POSIX_C_SOURCE 200809L // strdup
+
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+#include <iostream>
+#include <list>
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+#include <set>
+
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtkstyle.h>
+
+#include <gtkmm/widget.h>
+
+#include <libgnomecanvas/gnome-canvas-pixbuf.h>
+
+#include "ganv-config.h"
+#include "ganv/Canvas.hpp"
+#include "ganv/Edge.hpp"
+#include "ganv/Circle.hpp"
+#include "ganv/Module.hpp"
+#include "ganv/Port.hpp"
+#include "ganv/box.h"
+#include "ganv/canvas.h"
+#include "ganv/edge.h"
+#include "ganv/node.h"
+
+#include "./color.h"
+
+#ifdef HAVE_AGRAPH
+# include <gvc.h>
+#endif
+
+using std::cerr;
+using std::endl;
+using std::list;
+using std::string;
+using std::vector;
+
+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 = 0x0E2425FF;
+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->tail < b->tail)
+ || (a->tail == b->tail && a->head < b->head));
+ }
+};
+
+/** Order edges by (head, tail) */
+struct HeadTailOrder {
+ inline bool operator()(const GanvEdge* a, const GanvEdge* b) const {
+ return ((a->head < b->head)
+ || (a->head == b->head && a->tail < b->tail));
+ }
+};
+
+static gboolean
+on_canvas_event(GnomeCanvasItem* canvasitem,
+ GdkEvent* ev,
+ void* canvas)
+{
+ return ((Ganv::Canvas*)canvas)->on_event(ev);
+}
+
+struct GanvCanvasImpl {
+ GanvCanvasImpl(Ganv::Canvas& canvas, GanvCanvas* gobj, double width, double height)
+ : _canvas(canvas)
+ , _gcanvas(gobj)
+ , _layout(Glib::wrap(GTK_LAYOUT(_gcanvas)))
+ , _connect_port(NULL)
+ , _last_selected_port(NULL)
+ , _base_rect(gnome_canvas_item_new(
+ gnome_canvas_root(GNOME_CANVAS(_gcanvas)),
+ gnome_canvas_rect_get_type(),
+ "x1", 0.0,
+ "y1", 0.0,
+ "x2", width,
+ "y2", height,
+ "fill-color-rgba", 0x000000FF,
+ NULL))
+ , _select_rect(NULL)
+ , _zoom(1.0)
+ , _font_size(0.0)
+ , _width(width)
+ , _height(height)
+ , _drag_state(NOT_DRAGGING)
+ , _direction(Ganv::Canvas::HORIZONTAL)
+ {
+ _wrapper_key = g_quark_from_string("ganvmm");
+ _move_cursor = gdk_cursor_new(GDK_FLEUR);
+
+ gnome_canvas_set_scroll_region(GNOME_CANVAS(_gcanvas), 0.0, 0.0, width, height);
+
+ g_signal_connect(G_OBJECT(gnome_canvas_root(GNOME_CANVAS(_gcanvas))),
+ "event", G_CALLBACK(on_canvas_event), &_canvas);
+ }
+
+ ~GanvCanvasImpl()
+ {
+ gdk_cursor_unref(_move_cursor);
+ }
+
+ void for_each_edge_from(const GanvNode* tail, GanvEdgeFunction f);
+ void for_each_edge_to(const GanvNode* head, GanvEdgeFunction f);
+ void for_each_edge_on(const GanvNode* node, GanvEdgeFunction f);
+
+ void add_item(GanvNode* i);
+ bool remove_item(GanvNode* i);
+
+ void select_edge(GanvEdge* edge);
+ void unselect_edge(GanvEdge* edge);
+ void select_item(GanvNode* item);
+ void unselect_item(GanvNode* item);
+
+ void move_selected_items(double dx, double dy);
+ void selection_move_finished();
+
+#ifdef HAVE_AGRAPH
+ GVNodes layout_dot(bool use_length_hints, const std::string& filename);
+#endif
+
+ void remove_edge(GanvEdge* c);
+ bool are_connected(const GanvNode* tail,
+ const GanvNode* head);
+
+ typedef std::set<GanvEdge*, TailHeadOrder> Edges;
+ typedef std::set<GanvEdge*, HeadTailOrder> DstEdges;
+ typedef std::set<GanvEdge*> SelectedEdges;
+ typedef std::list<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 scroll_drag_handler(GdkEvent* event);
+ bool select_drag_handler(GdkEvent* event);
+ bool connect_drag_handler(GdkEvent* event);
+
+ /**
+ 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);
+ bool animate_selected();
+
+ void move_contents_to_internal(double x, double y, double min_x, double min_y);
+
+ Ganv::Canvas& _canvas;
+ GanvCanvas* _gcanvas;
+ Gtk::Layout* _layout;
+
+ 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;
+
+ GnomeCanvasItem* _base_rect; ///< Background
+ GanvBox* _select_rect; ///< Rectangle for drag selection
+
+ double _zoom; ///< Current zoom level
+ double _font_size; ///< Current font size in points
+ double _width;
+ double _height;
+
+ enum DragState { NOT_DRAGGING, EDGE, SCROLL, SELECT };
+ DragState _drag_state;
+
+ GQuark _wrapper_key;
+ GdkCursor* _move_cursor;
+
+ Ganv::Canvas::FlowDirection _direction;
+};
+
+static GanvEdge
+make_edge_search_key(const GanvNode* tail, const GanvNode* head)
+{
+ GanvEdge key;
+ memset(&key, '\0', sizeof(GanvEdge));
+ key.tail = const_cast<GanvNode*>(tail);
+ key.head = const_cast<GanvNode*>(head);
+ return key;
+}
+
+GanvCanvasImpl::Edges::const_iterator
+GanvCanvasImpl::first_edge_from(const GanvNode* tail)
+{
+ GanvEdge key = make_edge_search_key(tail, NULL);
+ return _edges.lower_bound(&key);
+}
+
+GanvCanvasImpl::DstEdges::const_iterator
+GanvCanvasImpl::first_edge_to(const GanvNode* head)
+{
+ GanvEdge key = make_edge_search_key(NULL, head);
+ return _dst_edges.lower_bound(&key);
+}
+
+void
+GanvCanvasImpl::select_edge(GanvEdge* edge)
+{
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(edge), "selected", TRUE, NULL);
+ _selected_edges.insert(edge);
+}
+
+void
+GanvCanvasImpl::unselect_edge(GanvEdge* edge)
+{
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(edge), "selected", FALSE, NULL);
+ _selected_edges.erase(edge);
+}
+
+void
+GanvCanvasImpl::move_selected_items(double dx, double dy)
+{
+ FOREACH_ITEM(_selected_items, i) {
+ ganv_node_move(*i, dx, dy);
+ }
+}
+
+void
+GanvCanvasImpl::selection_move_finished()
+{
+ FOREACH_ITEM(_selected_items, i) {
+ std::cerr << "FIXME: selection move finished" << std::endl;
+ Glib::wrap(*i)->signal_moved.emit();
+ }
+}
+
+static void
+select_if_tail_is_selected(GanvEdge* edge)
+{
+ gboolean selected;
+ g_object_get(edge->tail, "selected", &selected, NULL);
+ if (!selected && GANV_IS_PORT(edge->tail)) {
+ g_object_get(ganv_port_get_module(GANV_PORT(edge->tail)),
+ "selected", &selected, NULL);
+ }
+
+ if (selected) {
+ ganv_edge_select(edge);
+ }
+}
+
+static void
+select_if_head_is_selected(GanvEdge* edge)
+{
+ gboolean selected;
+ g_object_get(edge->head, "selected", &selected, NULL);
+ if (!selected && GANV_IS_PORT(edge->head)) {
+ g_object_get(ganv_port_get_module(GANV_PORT(edge->head)),
+ "selected", &selected, NULL);
+ }
+
+ if (selected) {
+ ganv_edge_select(edge);
+ }
+}
+
+static void
+select_edges(GanvPort* port, void* data)
+{
+ GanvCanvasImpl* impl = (GanvCanvasImpl*)data;
+ if (port->is_input) {
+ impl->for_each_edge_to(GANV_NODE(port),
+ select_if_tail_is_selected);
+ } else {
+ impl->for_each_edge_from(GANV_NODE(port),
+ select_if_head_is_selected);
+ }
+}
+
+void
+GanvCanvasImpl::add_item(GanvNode* n)
+{
+ if (GNOME_CANVAS_ITEM(n)->parent == GNOME_CANVAS_ITEM(_canvas.root())) {
+ _items.insert(n);
+ }
+}
+
+/** Remove an item from the canvas, cutting all references.
+ * Returns true if item was found (and removed).
+ */
+bool
+GanvCanvasImpl::remove_item(GanvNode* item)
+{
+ bool ret = false;
+
+ // Remove from selection
+ _selected_items.erase(item);
+
+ // Remove children ports from selection if item is a module
+ if (GANV_IS_MODULE(item)) {
+ GanvModule* const module = GANV_MODULE(item);
+ for (unsigned i = 0; i < module->ports->len; ++i) {
+ unselect_port((GanvPort*)g_ptr_array_index(
+ module->ports, i));
+ }
+ }
+
+ // Remove from items
+ _items.erase(item);
+
+ return ret;
+}
+
+void
+GanvCanvasImpl::select_item(GanvNode* m)
+{
+ _selected_items.insert(m);
+
+ // Select any connections to or from this node
+ if (GANV_IS_MODULE(m)) {
+ Ganv::Module* module = Glib::wrap(GANV_MODULE(m));
+ module->for_each_port(select_edges, this);
+ } else {
+ for_each_edge_on(m, ganv_edge_select);
+ }
+
+ g_object_set(m, "selected", TRUE, NULL);
+}
+
+static void
+unselect_edges(GanvPort* port, void* data)
+{
+ GanvCanvasImpl* impl = (GanvCanvasImpl*)data;
+ if (port->is_input) {
+ impl->for_each_edge_to(GANV_NODE(port),
+ ganv_edge_unselect);
+ } else {
+ impl->for_each_edge_from(GANV_NODE(port),
+ ganv_edge_unselect);
+ }
+}
+
+void
+GanvCanvasImpl::unselect_item(GanvNode* m)
+{
+ // Unselect any connections to or from this node
+ if (GANV_IS_MODULE(m)) {
+ Ganv::Module* module = Glib::wrap(GANV_MODULE(m));
+ module->for_each_port(unselect_edges, this);
+ } else {
+ for_each_edge_on(m, ganv_edge_unselect);
+ }
+
+ // Unselect item
+ _selected_items.erase(m);
+ g_object_set(m, "selected", FALSE, 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(), NULL);
+}
+
+GVNodes
+GanvCanvasImpl::layout_dot(bool use_length_hints, 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", AGDIGRAPH);
+ agsafeset(G, (char*)"splines", (char*)"false", NULL);
+ agsafeset(G, (char*)"compound", (char*)"true", NULL);
+ agsafeset(G, (char*)"remincross", (char*)"true", NULL);
+ agsafeset(G, (char*)"overlap", (char*)"scale", NULL);
+ agsafeset(G, (char*)"nodesep", (char*)"0.0", NULL);
+ gv_set(G, "fontsize", ganv_canvas_get_font_size(_canvas.gobj()));
+ gv_set(G, "dpi", dpi);
+
+ nodes.gvc = gvc;
+ nodes.G = G;
+
+ if (_direction == Ganv::Canvas::HORIZONTAL) {
+ agraphattr(G, (char*)"rankdir", (char*)"LR");
+ } else {
+ agraphattr(G, (char*)"rankdir", (char*)"TD");
+ }
+
+ unsigned id = 0;
+ std::ostringstream ss;
+ FOREACH_ITEM(_items, i) {
+ if (GANV_IS_MODULE(*i)) {
+ // Create a subgraph for this node
+ ss.str("");
+ ss << "cluster" << id++;
+ Agraph_t* subg = agsubg(G, (char*)ss.str().c_str());
+ agsafeset(subg, (char*)"shape", (char*)"box", NULL);
+ agsafeset(subg, (char*)"pad", (char*)"0", NULL);
+ gv_set(subg, "width",
+ ganv_box_get_width(GANV_BOX(*i)) / dpi);
+ gv_set(subg, "height",
+ ganv_box_get_height(GANV_BOX(*i)) / dpi);
+
+ // Make a borderless node for the title (instead of simply setting
+ // a title on the subgraph, so partners can be connected)
+ ss.str("");
+ ss << "n" << id++;
+ Agnode_t* node = agnode(subg, strdup(ss.str().c_str()));
+ agsafeset(node, (char*)"pos", (char*)"0,0", NULL);
+ agsafeset(node, (char*)"shape", (char*)"box", NULL);
+ agsafeset(node, (char*)"penwidth", (char*)"0", NULL);
+ agsafeset(node, (char*)"margin", (char*)"0", NULL);
+ agsafeset(node, (char*)"pad", (char*)"0", NULL);
+ const char* label = ganv_node_get_label(GANV_NODE(*i));
+ if (label) {
+ agsafeset(node, (char*)"label", (char*)label, NULL);
+ double width, height;
+ g_object_get((*i)->label, "width", &width, "height", &height, NULL);
+ gv_set(node, "width", width / dpi);
+ gv_set(node, "height", height / dpi);
+ }
+ nodes.insert(std::make_pair(*i, (Agnode_t*)node));
+
+ // Make a node in the subgraph for each port on this module
+ GanvModule* const m = GANV_MODULE(*i);
+ for (size_t i = 0; i < m->ports->len; ++i) {
+ GanvPort* port = (GanvPort*)g_ptr_array_index(m->ports, i);
+ ss.str("");
+ ss << "p" << id++;
+ Agnode_t* pnode = agnode(subg, (char*)ss.str().c_str());
+ agsafeset(pnode, (char*)"shape", (char*)"box", NULL);
+ agsafeset(pnode, (char*)"margin", (char*)"0", NULL);
+ agsafeset(pnode, (char*)"pin", (char*)"true", NULL);
+ agsafeset(pnode, (char*)"label",
+ (char*)ganv_node_get_label(GANV_NODE(port)),
+ NULL);
+
+ // Fix position (we don't want ports to be arranged)
+ // (TODO: I don't think dot actually supports this...)
+ double x, y;
+ g_object_get(G_OBJECT(port), "x", &x, "y", &y, NULL);
+ ss.str("");
+ ss << (x / dpi) << "," << (-y / dpi);
+ agsafeset(pnode, (char*)"pos", (char*)ss.str().c_str(), NULL);
+ gv_set(pnode, "width",
+ ganv_box_get_width(GANV_BOX(port)) / dpi);
+ gv_set(pnode, "height",
+ ganv_box_get_height(GANV_BOX(port)) / dpi);
+ nodes.insert(std::make_pair(GANV_NODE(port), pnode));
+ }
+ } else if (GANV_IS_CIRCLE(*i)) {
+ ss.str("");
+ ss << "n" << id++;
+ Agnode_t* node = agnode(G, strdup(ss.str().c_str()));
+ agsafeset(node, (char*)"width", (char*)"1.0", NULL);
+ agsafeset(node, (char*)"height", (char*)"1.0", NULL);
+ agsafeset(node, (char*)"shape", (char*)"ellipse", NULL);
+ agsafeset(node, (char*)"label",
+ (char*)ganv_node_get_label(GANV_NODE(*i)),
+ NULL);
+ nodes.insert(std::make_pair(*i, node));
+ } 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->tail);
+ GVNodes::iterator head_i = nodes.find(edge->head);
+
+ if (tail_i != nodes.end() && head_i != nodes.end()) {
+ agedge(G, tail_i->second, head_i->second);
+ } else {
+ std::cerr << "Unable to find graphviz node" << std::endl;
+ }
+ }
+
+ // Add edges between partners to have them lined up as if they are
+ // 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);
+ agsafeset(e, (char*)"style", (char*)"dotted", NULL);
+ }
+ }
+ }
+
+ 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
+
+void
+GanvCanvasImpl::remove_edge(GanvEdge* edge)
+{
+ if (edge) {
+ _selected_edges.erase(edge);
+ _edges.erase(edge);
+ _dst_edges.erase(edge);
+ gtk_object_destroy(GTK_OBJECT(edge));
+ }
+}
+
+/** Return whether there is a edge between item1 and item2.
+ *
+ * Note that edges are directed, so this may return false when there
+ * is a edge between the two items (in the opposite direction).
+ */
+bool
+GanvCanvasImpl::are_connected(const GanvNode* tail,
+ const GanvNode* head)
+{
+ GanvEdge key = make_edge_search_key(tail, head);
+ return (_edges.find(&key) != _edges.end());
+}
+
+void
+GanvCanvasImpl::select_port(GanvPort* p, bool unique)
+{
+ if (unique)
+ _canvas.unselect_ports();
+ g_object_set(G_OBJECT(p), "selected", TRUE, NULL);
+ SelectedPorts::iterator i = find(_selected_ports.begin(), _selected_ports.end(), p);
+ if (i == _selected_ports.end())
+ _selected_ports.push_back(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 < m->ports->len; ++i) {
+ GanvPort* const p = (GanvPort*)g_ptr_array_index(m->ports, i);
+ if (!first && !done && (p == _last_selected_port || p == port)) {
+ first = p;
+ }
+
+ if (first && !done && p->is_input == first->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) {
+ _canvas.unselect_ports();
+ } else {
+ select_port(port, true);
+ }
+ }
+}
+
+void
+GanvCanvasImpl::unselect_port(GanvPort* p)
+{
+ SelectedPorts::iterator i = find(_selected_ports.begin(), _selected_ports.end(), p);
+ if (i != _selected_ports.end())
+ _selected_ports.erase(i);
+ 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()
+{
+ vector<GanvPort*> inputs;
+ vector<GanvPort*> outputs;
+ FOREACH_SELECTED_PORT(i) {
+ if ((*i)->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)
+{
+ GnomeCanvasItem* item = gnome_canvas_get_item_at(GNOME_CANVAS(_gcanvas), x, y);
+ while (item) {
+ if (GANV_IS_NODE(item)) {
+ return GANV_NODE(item);
+ } else {
+ item = item->parent;
+ }
+ }
+
+ return NULL;
+}
+
+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;
+
+ if (event->type == GDK_BUTTON_PRESS && event->button.button == 2) {
+ gnome_canvas_item_grab(
+ GNOME_CANVAS_ITEM(_base_rect),
+ GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK,
+ NULL, event->button.time);
+ gnome_canvas_get_scroll_offsets(GNOME_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;
+ gnome_canvas_scroll_to(GNOME_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) {
+ gnome_canvas_item_ungrab(GNOME_CANVAS_ITEM(_base_rect), 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)
+{
+ 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)) )
+ _canvas.clear_selection();
+ _select_rect = GANV_BOX(
+ gnome_canvas_item_new(
+ _canvas.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));
+ gnome_canvas_item_lower_to_bottom(GNOME_CANVAS_ITEM(_select_rect));
+ gnome_canvas_item_lower_to_bottom(GNOME_CANVAS_ITEM(_base_rect));
+ gnome_canvas_item_grab(
+ GNOME_CANVAS_ITEM(_base_rect), 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);
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(_select_rect),
+ "x2", x,
+ "y2", y,
+ NULL);
+ 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 (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) {
+ unselect_item(node);
+ } else {
+ select_item(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))) {
+ select_edge(*i);
+ }
+ }
+
+ gnome_canvas_item_ungrab(GNOME_CANVAS_ITEM(_base_rect), 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 GanvEdge* drag_edge = NULL;
+ static GanvNode* drag_node = NULL;
+ 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);
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(_base_rect), &x, &y);
+
+ if (!drag_edge) {
+ // Create drag edge
+ assert(!drag_node);
+ assert(_connect_port);
+
+ drag_node = GANV_NODE(
+ gnome_canvas_item_new(gnome_canvas_root(GNOME_CANVAS(_gcanvas)),
+ ganv_node_get_type(),
+ "x", x,
+ "y", y,
+ NULL));
+
+ drag_edge = ganv_edge_new(
+ _canvas.gobj(),
+ GANV_NODE(_connect_port),
+ drag_node,
+ "color", GANV_NODE(_connect_port)->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;
+ gnome_canvas_item_set(&drag_edge->item,
+ "head", joinee,
+ NULL);
+ } else if (snapped) {
+ // Unsnap from item
+ snapped = false;
+ gnome_canvas_item_set(&drag_edge->item,
+ "head", drag_node,
+ NULL);
+ }
+
+ // Update drag edge for pointer position
+ ganv_node_move_to(drag_node, x, y);
+ gnome_canvas_item_request_update(&drag_node->group.item);
+ gnome_canvas_item_request_update(&drag_edge->item);
+
+ return true;
+
+ } else if (event->type == GDK_BUTTON_RELEASE) {
+ gnome_canvas_item_ungrab(GNOME_CANVAS_ITEM(_base_rect), event->button.time);
+
+ double x = event->button.x;
+ double y = event->button.y;
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(_base_rect), &x, &y);
+
+ GanvNode* joinee = get_node_at(x, y);
+
+ if (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);
+ _canvas.unselect_ports();
+ _connect_port = NULL;
+ }
+ } else { // drag ended on different port
+ ports_joined(_connect_port, GANV_PORT(joinee));
+ _canvas.unselect_ports();
+ _connect_port = NULL;
+ }
+ }
+
+ // Clean up dragging stuff
+
+ if (_connect_port) {
+ g_object_set(G_OBJECT(_connect_port), "highlighted", FALSE, NULL);
+ }
+
+ _canvas.unselect_ports();
+ _drag_state = NOT_DRAGGING;
+ _connect_port = NULL;
+
+ gtk_object_destroy(GTK_OBJECT(drag_edge));
+ gtk_object_destroy(GTK_OBJECT(drag_node));
+ drag_edge = NULL;
+ drag_node = NULL;
+
+ return true;
+ }
+
+ return false;
+}
+
+bool
+GanvCanvasImpl::port_event(GdkEvent* event, GanvPort* port)
+{
+ static bool port_dragging = false;
+ static bool control_dragging = false;
+ static bool ignore_button_release = false;
+
+ bool handled = true;
+
+ switch (event->type) {
+
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ GanvModule* const module = ganv_port_get_module(port);
+ if (module && _canvas.get_locked() && port->is_input) {
+#if 0
+ if (port->is_toggled()) {
+ port->toggle();
+ ignore_button_release = true;
+ } else {
+ control_dragging = true;
+ const double port_x = module->get_x() + port->get_x();
+ float new_control = ((event->button.x - port_x) / (double)port->get_width());
+ if (new_control < 0.0)
+ new_control = 0.0;
+ else if (new_control > 1.0)
+ new_control = 1.0;
+
+ new_control *= (port->control_max() - port->control_min());
+ new_control += port->control_min();
+ if (new_control < port->control_min())
+ new_control = port->control_min();
+ if (new_control > port->control_max())
+ new_control = port->control_max();
+ if (new_control != port->control_value())
+ port->set_control(new_control);
+ }
+#endif
+ } else {
+ port_dragging = true;
+ }
+ } else {
+ // FIXME
+ //return Glib::wrap(port)->on_click(&event->button);
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ if (control_dragging) {
+ GanvModule* const module = ganv_port_get_module(port);
+ if (module) {
+ const double port_x = ganv_box_get_x1(GANV_BOX(module))
+ + ganv_box_get_x1(GANV_BOX(port));
+ float new_control = ((event->button.x - port_x)
+ / ganv_box_get_width(GANV_BOX(port)));
+ if (new_control < 0.0)
+ new_control = 0.0;
+ else if (new_control > 1.0)
+ new_control = 1.0;
+
+ new_control *= (port->control->max - port->control->min);
+ new_control += port->control->min;
+ assert(new_control >= port->control->min);
+ assert(new_control <= port->control->max);
+
+ if (new_control != port->control->value) {
+ ganv_port_set_control_value(port, new_control);
+ }
+ }
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (port_dragging) {
+ if (_connect_port) { // dragging
+ ports_joined(port, _connect_port);
+ _canvas.unselect_ports();
+ } else {
+ bool modded = event->button.state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK);
+ if (!modded && _last_selected_port && _last_selected_port->is_input != port->is_input) {
+ selection_joined_with(port);
+ _canvas.unselect_ports();
+ } else {
+ select_port_toggle(port, event->button.state);
+ }
+ }
+ port_dragging = false;
+ } else if (control_dragging) {
+ control_dragging = false;
+ } else if (ignore_button_release) {
+ ignore_button_release = false;
+ } else {
+ handled = false;
+ }
+ break;
+
+ case GDK_ENTER_NOTIFY:
+ //signal_item_entered.emit(port);
+ gboolean selected;
+ g_object_get(G_OBJECT(port), "selected", &selected, NULL);
+ if (!control_dragging && !selected) {
+ g_object_set(G_OBJECT(port), "highlighted", TRUE, NULL);
+ return true;
+ }
+ break;
+
+ case GDK_LEAVE_NOTIFY:
+ if (port_dragging) {
+ _drag_state = GanvCanvasImpl::EDGE;
+ _connect_port = port;
+ port_dragging = false;
+ gnome_canvas_item_grab(
+ GNOME_CANVAS_ITEM(_base_rect),
+ GDK_BUTTON_PRESS_MASK|GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK,
+ NULL, event->crossing.time);
+ } else if (!control_dragging) {
+ g_object_set(G_OBJECT(port), "highlighted", FALSE, NULL);
+ }
+ //signal_item_left.emit(port);
+ break;
+
+ default:
+ handled = false;
+ }
+
+ return handled;
+}
+
+/** Called when two ports are 'toggled' (connected or disconnected)
+ */
+void
+GanvCanvasImpl::ports_joined(GanvPort* port1, GanvPort* port2)
+{
+ if (port1 == port2 || !port1 || !port2) {
+ return;
+ }
+
+ g_object_set(G_OBJECT(port1), "highlighted", FALSE, NULL);
+ g_object_set(G_OBJECT(port2), "highlighted", FALSE, NULL);
+
+ GanvNode* src_node;
+ GanvNode* dst_node;
+
+ if (port2->is_input && !port1->is_input) {
+ src_node = GANV_NODE(port1);
+ dst_node = GANV_NODE(port2);
+ } else if (!port2->is_input && port1->is_input) {
+ src_node = GANV_NODE(port2);
+ dst_node = GANV_NODE(port1);
+ } else {
+ return;
+ }
+
+ if (are_connected(GANV_NODE(src_node), GANV_NODE(dst_node)))
+ _canvas.disconnect(Glib::wrap(src_node), Glib::wrap(dst_node));
+ else
+ _canvas.connect(Glib::wrap(src_node), Glib::wrap(dst_node));
+}
+
+/** Update animated "rubber band" selection effect. */
+bool
+GanvCanvasImpl::animate_selected()
+{
+ const double seconds = g_get_monotonic_time() / 1000000.0;
+
+ FOREACH_ITEM(_selected_items, s) {
+ ganv_node_tick(*s, seconds);
+ }
+
+ FOREACH_SELECTED_PORT(p) {
+ ganv_node_tick(GANV_NODE(*p), seconds);
+ }
+
+ FOREACH_EDGE(_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::for_each_edge_from(const GanvNode* tail,
+ GanvEdgeFunction f)
+{
+ for (GanvCanvasImpl::Edges::const_iterator i = first_edge_from(tail);
+ i != _edges.end() && (*i)->tail == tail;) {
+ GanvCanvasImpl::Edges::const_iterator next = i;
+ ++next;
+ f((*i));
+ i = next;
+ }
+}
+
+void
+GanvCanvasImpl::for_each_edge_to(const GanvNode* head,
+ GanvEdgeFunction f)
+{
+ for (GanvCanvasImpl::Edges::const_iterator i = first_edge_to(head);
+ i != _dst_edges.end() && (*i)->head == head;) {
+ GanvCanvasImpl::Edges::const_iterator next = i;
+ ++next;
+ f((*i));
+ i = next;
+ }
+}
+
+void
+GanvCanvasImpl::for_each_edge_on(const GanvNode* node,
+ GanvEdgeFunction f)
+{
+ for_each_edge_from(node, f);
+ for_each_edge_to(node, f);
+}
+
+namespace Ganv {
+
+Canvas::Canvas(double width, double height)
+ : _gobj(GANV_CANVAS(ganv_canvas_new()))
+{
+ _gobj->impl = new GanvCanvasImpl(*this, _gobj, width, height);
+
+ impl()->_font_size = get_default_font_size();
+
+ g_object_set_qdata(G_OBJECT(impl()->_gcanvas), wrapper_key(), this);
+
+ Glib::signal_timeout().connect(
+ sigc::mem_fun(impl(), &GanvCanvasImpl::animate_selected), 120);
+}
+
+Canvas::~Canvas()
+{
+ destroy();
+ delete impl();
+}
+
+void
+Canvas::set_zoom(double pix_per_unit)
+{
+ set_zoom_and_font_size(pix_per_unit, impl()->_font_size);
+}
+
+void
+Canvas::set_font_size(double points)
+{
+ set_zoom_and_font_size(impl()->_zoom, points);
+}
+
+void
+Canvas::set_zoom_and_font_size(double zoom, double points)
+{
+ points = std::max(points, 1.0);
+ zoom = std::max(zoom, 0.01);
+
+ if (zoom == impl()->_zoom && points == impl()->_font_size)
+ return;
+
+ if (zoom != impl()->_zoom) {
+ impl()->_zoom = zoom;
+ gnome_canvas_set_pixels_per_unit(GNOME_CANVAS(impl()->_gcanvas), zoom);
+ }
+
+ impl()->_font_size = points;
+
+#if 0
+ FOREACH_ITEM(impl()->_items, i) {
+ (*i)->redraw_text();
+ }
+#endif
+}
+
+void
+Canvas::zoom_full()
+{
+#if 0
+ if (impl()->_items.empty())
+ return;
+
+ int win_width, win_height;
+ Glib::RefPtr<Gdk::Window> win = GNOME_CANVAS(impl()->_gcanvas)->get_window();
+ win->get_size(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(impl()->_items, i) {
+ if ((*i)->get_x() < left)
+ left = (*i)->get_x();
+ if ((*i)->get_x() + (*i)->width() > right)
+ right = (*i)->get_x() + (*i)->width();
+ if ((*i)->get_y() < bottom)
+ bottom = (*i)->get_y();
+ if ((*i)->get_y() + (*i)->height() > top)
+ top = (*i)->get_y() + (*i)->height();
+ }
+
+ static const double pad = 8.0;
+
+ const double new_zoom = std::min(
+ ((double)win_width / (double)(right - left + pad*2.0)),
+ ((double)win_height / (double)(top - bottom + pad*2.0)));
+
+ set_zoom(new_zoom);
+
+ int scroll_x, scroll_y;
+ GNOME_CANVAS(impl()->_gcanvas)->w2c(lrintf(left - pad), lrintf(bottom - pad), scroll_x, scroll_y);
+
+ GNOME_CANVAS(impl()->_gcanvas)->scroll_to(scroll_x, scroll_y);
+#endif
+}
+
+void
+Canvas::clear_selection()
+{
+ unselect_ports();
+
+ FOREACH_ITEM(impl()->_selected_items, i) {
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(*i), "selected", FALSE, NULL);
+ }
+
+ FOREACH_SELECTED_EDGE(impl()->_selected_edges, c) {
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(*c), "selected", FALSE, NULL);
+ }
+
+ impl()->_selected_items.clear();
+ impl()->_selected_edges.clear();
+}
+
+void
+Canvas::select_edge(Edge* edge)
+{
+ impl()->select_edge(edge->gobj());
+}
+
+void
+Canvas::unselect_edge(Edge* edge)
+{
+ impl()->unselect_edge(edge->gobj());
+}
+
+double
+Canvas::get_zoom()
+{
+ return impl()->_zoom;
+}
+
+double
+Canvas::width() const
+{
+ return impl()->_width;
+}
+
+double
+Canvas::height() const
+{
+ return impl()->_height;
+}
+
+void
+Canvas::set_direction(FlowDirection d)
+{
+ impl()->_direction = d;
+ std::cerr << "FIXME: set direction" << std::endl;
+#if 0
+ FOREACH_ITEM(impl()->_items, i) {
+ Module* mod = dynamic_cast<Module*>(*i);
+ if (mod) {
+ mod->set_show_port_labels(d == Canvas::HORIZONTAL);
+ }
+ }
+#endif
+}
+
+Canvas::FlowDirection
+Canvas::direction() const
+{
+ return impl()->_direction;
+}
+
+void
+Canvas::select_all()
+{
+ clear_selection();
+ FOREACH_ITEM(impl()->_items, m) {
+ impl()->select_item(*m);
+ }
+}
+
+void
+Canvas::select_item(Ganv::Node* m)
+{
+ impl()->select_item(m->gobj());
+}
+
+void
+Canvas::unselect_item(Ganv::Node* m)
+{
+ impl()->unselect_item(m->gobj());
+}
+
+void
+Canvas::destroy()
+{
+ impl()->_selected_items.clear();
+ impl()->_selected_edges.clear();
+
+ Items items = impl()->_items; // copy
+ FOREACH_ITEM(items, i) {
+ gtk_object_destroy(GTK_OBJECT(*i));
+ }
+
+ impl()->_edges.clear();
+
+ impl()->_selected_ports.clear();
+ impl()->_connect_port = NULL;
+
+ impl()->_items.clear();
+}
+
+void
+Canvas::unselect_ports()
+{
+ for (GanvCanvasImpl::SelectedPorts::iterator i = impl()->_selected_ports.begin();
+ i != impl()->_selected_ports.end(); ++i)
+ g_object_set(G_OBJECT(*i), "selected", FALSE, NULL);
+
+ impl()->_selected_ports.clear();
+ impl()->_last_selected_port = NULL;
+}
+
+void
+Canvas::set_default_placement(Node* i)
+{
+ // Simple cascade. This will get more clever in the future.
+ double x = ((impl()->_width / 2.0) + (impl()->_items.size() * 25));
+ double y = ((impl()->_height / 2.0) + (impl()->_items.size() * 25));
+
+ i->move_to(x, y);
+}
+
+void
+Canvas::remove_edge(Node* item1,
+ Node* item2)
+{
+ Edge* edge = get_edge(item1, item2);
+ if (edge) {
+ impl()->remove_edge(edge->gobj());
+ }
+}
+
+/** Get the edge between two items.
+ *
+ * Note that edges are directed.
+ * This will only return a edge from @a tail to @a head.
+ */
+Edge*
+Canvas::get_edge(Node* tail, Node* head) const
+{
+ FOREACH_EDGE(impl()->_edges, i) {
+ const GanvNode* const t = (*i)->tail;
+ const GanvNode* const h = (*i)->head;
+
+ if (t == tail->gobj() && h == head->gobj())
+ return Glib::wrap(*i);
+ }
+
+ return NULL;
+}
+
+bool
+Canvas::on_event(GdkEvent* event)
+{
+ static const int scroll_increment = 10;
+ int scroll_x, scroll_y;
+ gnome_canvas_get_scroll_offsets(GNOME_CANVAS(impl()->_gcanvas), &scroll_x, &scroll_y);
+
+ switch (event->type) {
+ case GDK_KEY_PRESS:
+ 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 (impl()->_selected_ports.size() > 1) {
+ impl()->join_selection();
+ clear_selection();
+ }
+ return true;
+ default:
+ return false;
+ }
+ gnome_canvas_scroll_to(GNOME_CANVAS(impl()->_gcanvas), scroll_x, scroll_y);
+ return true;
+
+ case GDK_SCROLL:
+ if ((event->scroll.state & GDK_CONTROL_MASK)) {
+ if (event->scroll.direction == GDK_SCROLL_UP) {
+ set_font_size(get_font_size() * 1.25);
+ return true;
+ } else if (event->scroll.direction == GDK_SCROLL_DOWN) {
+ set_font_size(get_font_size() * 0.75);
+ return true;
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return impl()->scroll_drag_handler(event)
+ || impl()->select_drag_handler(event)
+ || impl()->connect_drag_handler(event);
+}
+
+void
+Canvas::render_to_dot(const string& dot_output_filename)
+{
+#ifdef HAVE_AGRAPH
+ GVNodes nodes = impl()->layout_dot(false, dot_output_filename);
+ nodes.cleanup();
+#endif
+}
+
+void
+Canvas::arrange(bool use_length_hints)
+{
+#ifdef HAVE_AGRAPH
+ GVNodes nodes = impl()->layout_dot(use_length_hints, "");
+
+ 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");
+
+ // Arrange to graphviz coordinates
+ for (GVNodes::iterator i = nodes.begin(); i != nodes.end(); ++i) {
+ if (GNOME_CANVAS_ITEM(i->first)->parent != GNOME_CANVAS_ITEM(root())) {
+ continue;
+ }
+ const string pos = agget(i->second, (char*)"pos");
+ const string x_str = pos.substr(0, pos.find(","));
+ const string y_str = pos.substr(pos.find(",") + 1);
+ const double cx = strtod(x_str.c_str(), NULL) * 1.1;
+ const double cy = strtod(y_str.c_str(), NULL) * 1.1;
+ const double w = ganv_box_get_width(GANV_BOX(i->first));
+
+ /* Dot node positions are supposedly node centers, but things only
+ match up if x is interpreted as center and y as top...
+ */
+ const double x = cx - (w / 2.0);
+ const double y = -cy;
+
+ ganv_node_move_to(i->first, x, y);
+
+ least_x = std::min(least_x, x);
+ least_y = std::min(least_y, y);
+ most_x = std::max(most_x, x);
+ most_y = std::max(most_y, y);
+ }
+
+ // 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;
+
+ if (graph_width + 10 > impl()->_width)
+ resize(graph_width + 10, impl()->_height);
+
+ if (graph_height + 10 > impl()->_height)
+ resize(impl()->_width, graph_height + 10);
+
+ nodes.cleanup();
+
+ static const double border_width = impl()->_font_size;
+ impl()->move_contents_to_internal(border_width, border_width, least_x, least_y);
+ gnome_canvas_scroll_to(GNOME_CANVAS(impl()->_gcanvas), 0, 0);
+
+ FOREACH_ITEM(impl()->_items, i) {
+ std::cerr << "FIXME: arrange moved" << std::endl;
+ Glib::wrap((*i))->signal_moved.emit();
+ }
+#endif
+}
+
+void
+Canvas::move_contents_to(double x, double y)
+{
+ double min_x=HUGE_VAL, min_y=HUGE_VAL;
+ FOREACH_ITEM(impl()->_items, i) {
+ double x, y;
+ g_object_get(*i, "x", &x, "y", &y, NULL);
+ min_x = std::min(min_x, x);
+ min_y = std::min(min_y, y);
+ }
+ impl()->move_contents_to_internal(x, y, min_x, min_y);
+}
+
+void
+Canvas::resize(double width, double height)
+{
+ if (width != impl()->_width || height != impl()->_height) {
+ gnome_canvas_item_set(
+ GNOME_CANVAS_ITEM(impl()->_base_rect),
+ "x2", width,
+ "y2", height,
+ NULL);
+ impl()->_width = width;
+ impl()->_height = height;
+ gnome_canvas_set_scroll_region(GNOME_CANVAS(impl()->_gcanvas), 0.0, 0.0, width, height);
+ }
+}
+
+void
+Canvas::for_each_node(NodeFunction f, void* data)
+{
+ FOREACH_ITEM(impl()->_items, i) {
+ f(*i, data);
+ }
+}
+
+void
+Canvas::for_each_selected_node(NodeFunction f, void* data)
+{
+ FOREACH_ITEM(impl()->_selected_items, i) {
+ f(*i, data);
+ }
+}
+
+void
+Canvas::for_each_edge(EdgePtrFunction f, void* data)
+{
+ FOREACH_EDGE(impl()->_edges, i) {
+ f((*i), data);
+ }
+}
+
+void
+Canvas::for_each_selected_edge(EdgePtrFunction f, void* data)
+{
+ FOREACH_EDGE(impl()->_selected_edges, i) {
+ f((*i), data);
+ }
+}
+
+GnomeCanvasGroup*
+Canvas::root()
+{
+ return gnome_canvas_root(GNOME_CANVAS(impl()->_gcanvas));
+}
+
+Gtk::Layout&
+Canvas::widget()
+{
+ return *impl()->_layout;
+}
+
+void
+Canvas::get_scroll_offsets(int& cx, int& cy) const
+{
+ gnome_canvas_get_scroll_offsets(GNOME_CANVAS(impl()->_gcanvas), &cx, &cy);
+}
+
+void
+Canvas::scroll_to(int x, int y)
+{
+ gnome_canvas_scroll_to(GNOME_CANVAS(impl()->_gcanvas), x, y);
+}
+
+GQuark
+Canvas::wrapper_key()
+{
+ return impl()->_wrapper_key;
+}
+
+GdkCursor*
+Canvas::move_cursor()
+{
+ return impl()->_move_cursor;
+}
+
+GanvCanvas*
+Canvas::gobj()
+{
+ return impl()->_gcanvas;
+}
+
+const GanvCanvas*
+Canvas::gobj() const
+{
+ return impl()->_gcanvas;
+}
+
+} // namespace Ganv
+
+
+extern "C" {
+
+#include <libgnomecanvas/libgnomecanvas.h>
+
+#include "ganv/canvas.h"
+
+#include "./boilerplate.h"
+#include "./color.h"
+#include "./gettext.h"
+
+G_DEFINE_TYPE(GanvCanvas, ganv_canvas, GNOME_TYPE_CANVAS)
+
+static GnomeCanvasClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_LOCKED
+};
+
+static void
+ganv_canvas_init(GanvCanvas* canvas)
+{
+ canvas->direction = GANV_HORIZONTAL;
+}
+
+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_LOCKED: {
+ const gboolean tmp = g_value_get_boolean(value);
+ if (canvas->locked != tmp) {
+ GnomeCanvasItem* base = GNOME_CANVAS_ITEM(canvas->impl->_base_rect);
+ gnome_canvas_item_set(base,
+ "fill-color", tmp ? 0x131415FF : 0x000000FF,
+ NULL);
+ canvas->locked = tmp;
+ gnome_canvas_item_request_update(base);
+ }
+ 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(LOCKED, boolean, canvas->locked);
+ 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;
+
+ parent_class = GNOME_CANVAS_CLASS(g_type_class_peek_parent(klass));
+
+ gobject_class->set_property = ganv_canvas_set_property;
+ gobject_class->get_property = ganv_canvas_get_property;
+
+ g_object_class_install_property(
+ gobject_class, PROP_LOCKED, g_param_spec_boolean(
+ "locked",
+ _("locked"),
+ _("whether canvas items are movable"),
+ FALSE,
+ (GParamFlags)G_PARAM_READWRITE));
+}
+
+GanvCanvas*
+ganv_canvas_new(void)
+{
+ return (GanvCanvas*)g_object_new(ganv_canvas_get_type(), NULL);
+}
+
+GnomeCanvasGroup*
+ganv_canvas_get_root(const GanvCanvas* canvas)
+{
+ return gnome_canvas_root(GNOME_CANVAS(canvas->impl->_gcanvas));
+}
+
+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 ganv_canvas_get_default_font_size(canvas);
+}
+
+void
+ganv_canvas_clear_selection(GanvCanvas* canvas)
+{
+ canvas->impl->_canvas.clear_selection();
+}
+
+void
+ganv_canvas_move_selected_items(GanvCanvas* canvas,
+ double dx,
+ double dy)
+{
+ return canvas->impl->move_selected_items(dx, dy);
+}
+
+void
+ganv_canvas_selection_move_finished(GanvCanvas* canvas)
+{
+ return canvas->impl->selection_move_finished();
+}
+
+void
+ganv_canvas_select_node(GanvCanvas* canvas,
+ GanvNode* node)
+{
+ canvas->impl->select_item(node);
+}
+
+void
+ganv_canvas_unselect_node(GanvCanvas* canvas,
+ GanvNode* node)
+{
+ canvas->impl->unselect_item(node);
+}
+
+void
+ganv_canvas_add_node(GanvCanvas* canvas,
+ GanvNode* node)
+{
+ canvas->impl->add_item(node);
+}
+
+void
+ganv_canvas_remove_node(GanvCanvas* canvas,
+ GanvNode* node)
+{
+ canvas->impl->remove_item(node);
+}
+
+void
+ganv_canvas_add_edge(GanvCanvas* canvas,
+ GanvEdge* edge)
+{
+ canvas->impl->_edges.insert(edge);
+ canvas->impl->_dst_edges.insert(edge);
+}
+
+void
+ganv_canvas_remove_edge(GanvCanvas* canvas,
+ GanvEdge* edge)
+{
+ canvas->impl->remove_edge(edge);
+}
+
+void
+ganv_canvas_select_edge(GanvCanvas* canvas,
+ GanvEdge* edge)
+{
+ canvas->impl->select_edge(edge);
+}
+
+void
+ganv_canvas_unselect_edge(GanvCanvas* canvas,
+ GanvEdge* edge)
+{
+ canvas->impl->unselect_edge(edge);
+}
+
+void
+ganv_canvas_for_each_edge_from(GanvCanvas* canvas,
+ const GanvNode* tail,
+ GanvEdgeFunction f)
+{
+ canvas->impl->for_each_edge_from(tail, f);
+}
+
+void
+ganv_canvas_for_each_edge_to(GanvCanvas* canvas,
+ const GanvNode* head,
+ GanvEdgeFunction f)
+{
+ canvas->impl->for_each_edge_to(head, f);
+}
+
+void
+ganv_canvas_for_each_edge_on(GanvCanvas* canvas,
+ const GanvNode* node,
+ GanvEdgeFunction f)
+{
+ canvas->impl->for_each_edge_on(node, f);
+}
+
+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);
+}
+
+} // extern "C"
diff --git a/src/Port.cpp b/src/Port.cpp
new file mode 100644
index 0000000..1a48740
--- /dev/null
+++ b/src/Port.cpp
@@ -0,0 +1,52 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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 {
+
+/** Contruct 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", highlight_color(color, 0x20),
+ "label", name.c_str(),
+ NULL)))
+{
+}
+
+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..413c48d
--- /dev/null
+++ b/src/boilerplate.h
@@ -0,0 +1,45 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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; \
+ gnome_canvas_item_request_update(GNOME_CANVAS_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..2a542bf
--- /dev/null
+++ b/src/box.c
@@ -0,0 +1,504 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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"
+
+static const double STACKED_OFFSET = 4.0;
+
+G_DEFINE_TYPE(GanvBox, ganv_box, GANV_TYPE_NODE)
+
+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
+};
+
+static void
+ganv_box_init(GanvBox* box)
+{
+ memset(&box->coords, '\0', sizeof(GanvBoxCoords));
+ box->coords.border_width = box->node.border_width;
+
+ box->old_coords = box->coords;
+ box->radius_tl = 0.0;
+ box->radius_tr = 0.0;
+ box->radius_br = 0.0;
+ box->radius_bl = 0.0;
+}
+
+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);
+ GanvBoxCoords* coords = &box->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, box->radius_tl);
+ SET_CASE(RADIUS_TR, double, box->radius_tr);
+ SET_CASE(RADIUS_BR, double, box->radius_br);
+ SET_CASE(RADIUS_BL, double, box->radius_bl);
+ SET_CASE(STACKED, boolean, coords->stacked);
+ 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);
+
+ switch (prop_id) {
+ GET_CASE(X1, double, box->coords.x1);
+ GET_CASE(X2, double, box->coords.x2);
+ GET_CASE(Y1, double, box->coords.y1);
+ GET_CASE(Y2, double, box->coords.y2);
+ GET_CASE(RADIUS_TL, double, box->radius_tl);
+ GET_CASE(RADIUS_TR, double, box->radius_tr);
+ GET_CASE(RADIUS_BR, double, box->radius_br);
+ GET_CASE(RADIUS_BL, double, box->radius_bl);
+ GET_CASE(STACKED, boolean, box->coords.stacked);
+ 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 = MIN(coords->x1, coords->x2) - coords->border_width;
+ *y1 = MIN(coords->y1, coords->y2) - coords->border_width;
+ *x2 = MAX(coords->x1, coords->x2) + coords->border_width;
+ *y2 = MAX(coords->y1, coords->y2) + coords->border_width;
+
+ if (coords->stacked) {
+ *x2 += STACKED_OFFSET;
+ *y2 += STACKED_OFFSET;
+ }
+}
+
+
+static void
+request_redraw(GnomeCanvasItem* item,
+ const GanvBoxCoords* coords,
+ gboolean world)
+{
+ double x1, y1, x2, y2;
+ ganv_box_bounds_item(coords, &x1, &y1, &x2, &y2);
+
+ if (!world) {
+ // Convert from parent-relative coordinates to world coordinates
+ gnome_canvas_item_i2w(item, &x1, &y1);
+ gnome_canvas_item_i2w(item, &x2, &y2);
+ }
+
+ gnome_canvas_request_redraw(item->canvas, x1, y1, x2, y2);
+}
+
+static void
+coords_i2w(GnomeCanvasItem* item, GanvBoxCoords* coords)
+{
+ gnome_canvas_item_i2w(item, &coords->x1, &coords->y1);
+ gnome_canvas_item_i2w(item, &coords->x2, &coords->y2);
+}
+
+static void
+ganv_box_bounds(GnomeCanvasItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ // Note this will not be correct if children are outside the box bounds
+ GanvBox* box = GANV_BOX(item);
+ ganv_box_bounds_item(&box->coords, x1, y1, x2, y2);
+ gnome_canvas_item_i2w(item, x1, y1);
+ gnome_canvas_item_i2w(item, x2, y2);
+}
+
+static void
+ganv_box_update(GnomeCanvasItem* item,
+ double* affine,
+ ArtSVP* clip_path,
+ int flags)
+{
+ GanvBox* box = GANV_BOX(item);
+ box->coords.border_width = box->node.border_width;
+
+ // Request redraw of old location
+ request_redraw(item, &box->old_coords, TRUE);
+
+ GnomeCanvasItemClass* item_class = GNOME_CANVAS_ITEM_CLASS(parent_class);
+ item_class->update(item, affine, clip_path, flags);
+
+ // Store old coordinates in world relative coordinates in case the
+ // group we are in moves between now and the next update
+ box->old_coords = box->coords;
+ coords_i2w(item, &box->old_coords);
+
+ // Get bounding box
+ double x1, x2, y1, y2;
+ ganv_box_bounds(item, &x1, &y1, &x2, &y2);
+
+ // Update item canvas coordinates
+ gnome_canvas_w2c_d(GNOME_CANVAS(item->canvas), x1, y1, &item->x1, &item->y1);
+ gnome_canvas_w2c_d(GNOME_CANVAS(item->canvas), x2, y2, &item->x2, &item->y2);
+
+ // Request redraw of new location
+ request_redraw(item, &box->coords, FALSE);
+
+}
+
+static void
+ganv_box_render(GnomeCanvasItem* item,
+ GnomeCanvasBuf* buf)
+{
+ // Not implemented
+}
+
+static void
+ganv_box_draw(GnomeCanvasItem* item,
+ GdkDrawable* drawable,
+ int cx, int cy,
+ int width, int height)
+{
+ GanvBox* me = GANV_BOX(item);
+ cairo_t* cr = gdk_cairo_create(drawable);
+
+ double x1 = me->coords.x1;
+ double y1 = me->coords.y1;
+ double x2 = me->coords.x2;
+ double y2 = me->coords.y2;
+ gnome_canvas_item_i2w(item, &x1, &y1);
+ gnome_canvas_item_i2w(item, &x2, &y2);
+
+ double dash_length, border_color, fill_color;
+ ganv_node_get_draw_properties(
+ &me->node, &dash_length, &border_color, &fill_color);
+
+ double r, g, b, a;
+
+ double degrees = M_PI / 180.0;
+
+ for (int i = (me->coords.stacked ? 1 : 0); i >= 0; --i) {
+ const int x = cx - (STACKED_OFFSET * i);
+ const int y = cy - (STACKED_OFFSET * i);
+
+ if (me->radius_tl == 0.0 && me->radius_tr == 0.0
+ && me->radius_br == 0.0 && me->radius_bl == 0.0) {
+ // Simple rectangle
+ cairo_rectangle(cr, x1 - x, y1 - y, x2 - x1, y2 - y1);
+ } else {
+ // Rounded rectangle
+ cairo_new_sub_path(cr);
+ cairo_arc(cr,
+ x2 - x - me->radius_tr,
+ y1 - y + me->radius_tr,
+ me->radius_tr, -90 * degrees, 0 * degrees);
+ cairo_arc(cr,
+ x2 - x - me->radius_br, y2 - y - me->radius_br,
+ me->radius_br, 0 * degrees, 90 * degrees);
+ cairo_arc(cr,
+ x1 - x + me->radius_bl, y2 - y - me->radius_bl,
+ me->radius_bl, 90 * degrees, 180 * degrees);
+ cairo_arc(cr,
+ x1 - x + me->radius_tl, y1 - y + me->radius_tl,
+ me->radius_tl, 180 * degrees, 270 * degrees);
+ cairo_close_path(cr);
+ }
+
+ // Fill
+ color_to_rgba(fill_color, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_fill_preserve(cr);
+
+ // Border
+ if (me->coords.border_width > 0.0) {
+ color_to_rgba(border_color, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_set_line_width(cr, me->coords.border_width);
+ if (dash_length > 0) {
+ cairo_set_dash(cr, &dash_length, 1, me->node.dash_offset);
+ }
+ }
+ cairo_stroke(cr);
+ }
+
+ GnomeCanvasItemClass* item_class = GNOME_CANVAS_ITEM_CLASS(parent_class);
+ item_class->draw(item, drawable, cx, cy, width, height);
+
+ cairo_destroy(cr);
+}
+
+static double
+ganv_box_point(GnomeCanvasItem* item,
+ double x, double y,
+ int cx, int cy,
+ GnomeCanvasItem** actual_item)
+{
+ GanvBox* box = GANV_BOX(item);
+
+ *actual_item = NULL;
+
+ double x1, y1, x2, y2;
+ ganv_box_bounds_item(&box->coords, &x1, &y1, &x2, &y2);
+
+ // Point is inside the box (distance 0)
+ if ((x >= x1) && (y >= y1) && (x <= x2) && (y <= y2)) {
+ GnomeCanvasItemClass* item_class = GNOME_CANVAS_ITEM_CLASS(parent_class);
+ double d = item_class->point(item, x, y, cx, cy, actual_item);
+ if (*actual_item) {
+ return d;
+ } else {
+ *actual_item = item;
+ 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 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);
+
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(self), &bx1, &by1);
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(self), &bx2, &by2);
+
+ return ( bx1 >= x1
+ && by2 >= y1
+ && bx2 <= x2
+ && by2 <= y2);
+}
+
+static void
+ganv_box_default_set_width(GanvBox* box,
+ double width)
+{
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(box),
+ "x2", ganv_box_get_x1(box) + width,
+ NULL);
+}
+
+static void
+ganv_box_default_set_height(GanvBox* box,
+ double height)
+{
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(box),
+ "y2", ganv_box_get_y1(box) + height,
+ NULL);
+}
+
+static void
+ganv_box_class_init(GanvBoxClass* class)
+{
+ GObjectClass* gobject_class = (GObjectClass*)class;
+ GtkObjectClass* object_class = (GtkObjectClass*)class;
+ GnomeCanvasItemClass* item_class = (GnomeCanvasItemClass*)class;
+ GanvNodeClass* node_class = (GanvNodeClass*)class;
+
+ parent_class = GANV_NODE_CLASS(g_type_class_peek_parent(class));
+
+ 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 corner radius"),
+ _("top left corner radius"),
+ 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 corner radius"),
+ _("top right corner radius"),
+ 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 corner radius"),
+ _("bottom right corner radius"),
+ 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 corner radius"),
+ _("bottom left corner radius"),
+ 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 the box with a stacked appearance."),
+ 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->render = ganv_box_render;
+ item_class->draw = ganv_box_draw;
+
+ node_class->is_within = ganv_box_is_within;
+
+ class->set_width = ganv_box_default_set_width;
+ class->set_height = ganv_box_default_set_height;
+}
+
+void
+ganv_box_normalize(GanvBox* box)
+{
+ const double x1 = box->coords.x1;
+ const double y1 = box->coords.y1;
+ const double x2 = box->coords.x2;
+ const double y2 = box->coords.y2;
+
+ box->coords.x1 = MIN(x1, x2);
+ box->coords.y1 = MIN(y1, y2);
+ box->coords.x2 = MAX(x1, x2);
+ box->coords.y2 = MAX(y1, y2);
+}
diff --git a/src/circle.c b/src/circle.c
new file mode 100644
index 0000000..5452417
--- /dev/null
+++ b/src/circle.c
@@ -0,0 +1,351 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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/circle.h"
+
+#include "./color.h"
+#include "./boilerplate.h"
+#include "./gettext.h"
+
+G_DEFINE_TYPE(GanvCircle, ganv_circle, GANV_TYPE_NODE)
+
+static GanvNodeClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_RADIUS
+};
+
+static void
+ganv_circle_init(GanvCircle* circle)
+{
+ memset(&circle->coords, '\0', sizeof(GanvCircleCoords));
+ circle->coords.radius = 8.0;
+ circle->coords.width = 2.0;
+ circle->old_coords = circle->coords;
+}
+
+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->coords.radius);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+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->coords.radius);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static gboolean
+ganv_circle_is_within(const GanvNode* self,
+ double x1,
+ double y1,
+ double x2,
+ double y2)
+{
+ double x, y;
+ g_object_get(G_OBJECT(self), "x", &x, "y", &y, NULL);
+
+ return x >= x1
+ && x <= x2
+ && y >= y1
+ && y <= y2;
+}
+
+static void
+ganv_circle_tail_vector(const GanvNode* self,
+ const GanvNode* head,
+ double* x,
+ double* y,
+ double* dx,
+ double* dy)
+{
+ g_object_get(G_OBJECT(self), "x", x, "y", y, NULL);
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(self)->parent, x, y);
+ *dx = 0.0;
+ *dy = 0.0;
+}
+
+static void
+ganv_circle_head_vector(const GanvNode* self,
+ const GanvNode* tail,
+ double* x,
+ double* y,
+ double* dx,
+ double* dy)
+{
+ GanvCircle* circle = GANV_CIRCLE(self);
+
+ double cx, cy;
+ g_object_get(G_OBJECT(self), "x", &cx, "y", &cy, NULL);
+
+ // FIXME: Not quite right, should be the connect point
+ double tail_x, tail_y;
+ g_object_get(G_OBJECT(tail),
+ "x", &tail_x,
+ "y", &tail_y,
+ NULL);
+
+ const double xdist = tail_x - cx;
+ const double ydist = tail_y - cy;
+ const double h = sqrt((xdist * xdist) + (ydist * ydist));
+ const double theta = asin(xdist / (h + DBL_EPSILON));
+ const double y_mod = (cy < tail_y) ? 1 : -1;
+ const double ret_h = h - circle->coords.radius;
+ const double ret_x = tail_x - sin(theta) * ret_h;
+ const double ret_y = tail_y - cos(theta) * ret_h * y_mod;
+
+ *x = ret_x;
+ *y = ret_y;
+ *dx = 0.0;
+ *dy = 0.0;
+
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(self)->parent, x, y);
+}
+
+static void
+request_redraw(GnomeCanvasItem* 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
+ gnome_canvas_item_i2w(item, &x1, &y1);
+ gnome_canvas_item_i2w(item, &x2, &y2);
+ }
+
+ gnome_canvas_request_redraw(item->canvas, x1, y1, x2, y2);
+ gnome_canvas_request_redraw(item->canvas, 0, 0, 10000, 10000);
+}
+
+static void
+coords_i2w(GnomeCanvasItem* item, GanvCircleCoords* coords)
+{
+ gnome_canvas_item_i2w(item, &coords->x, &coords->y);
+}
+
+static void
+ganv_circle_bounds_item(GnomeCanvasItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ const GanvCircle* circle = GANV_CIRCLE(item);
+ const GanvCircleCoords* coords = &circle->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(GnomeCanvasItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ ganv_circle_bounds_item(item, x1, y1, x2, y2);
+ gnome_canvas_item_i2w(item, x1, y1);
+ gnome_canvas_item_i2w(item, x2, y2);
+}
+
+static void
+ganv_circle_update(GnomeCanvasItem* item,
+ double* affine,
+ ArtSVP* clip_path,
+ int flags)
+{
+ GanvCircle* circle = GANV_CIRCLE(item);
+
+ GnomeCanvasItemClass* item_class = GNOME_CANVAS_ITEM_CLASS(parent_class);
+ if (item_class->update) {
+ (*item_class->update)(item, affine, clip_path, flags);
+ }
+
+ // Request redraw of old location
+ request_redraw(item, &circle->old_coords, TRUE);
+
+ // Store old coordinates in world relative coordinates in case the
+ // group we are in moves between now and the next update
+ circle->old_coords = circle->coords;
+ coords_i2w(item, &circle->old_coords);
+
+ // Get bounding circle
+ double x1, x2, y1, y2;
+ ganv_circle_bounds(item, &x1, &y1, &x2, &y2);
+
+ // Update item canvas coordinates
+ gnome_canvas_w2c_d(GNOME_CANVAS(item->canvas), x1, y1, &item->x1, &item->y1);
+ gnome_canvas_w2c_d(GNOME_CANVAS(item->canvas), x2, y2, &item->x2, &item->y2);
+
+ // Request redraw of new location
+ request_redraw(item, &circle->coords, FALSE);
+}
+
+static void
+ganv_circle_render(GnomeCanvasItem* item,
+ GnomeCanvasBuf* buf)
+{
+ // Not implemented
+}
+
+static void
+ganv_circle_draw(GnomeCanvasItem* item,
+ GdkDrawable* drawable,
+ int x, int y,
+ int width, int height)
+{
+ GanvCircle* me = GANV_CIRCLE(item);
+ cairo_t* cr = gdk_cairo_create(drawable);
+
+ double r, g, b, a;
+
+ double cx = me->coords.x;
+ double cy = me->coords.y;
+ gnome_canvas_item_i2w(item, &cx, &cy);
+
+ double dash_length, border_color, fill_color;
+ ganv_node_get_draw_properties(
+ &me->node, &dash_length, &border_color, &fill_color);
+
+ cairo_arc(cr,
+ cx - x,
+ cy - y,
+ me->coords.radius,
+ 0, 2 * M_PI);
+
+ // Fill
+ color_to_rgba(fill_color, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_fill_preserve(cr);
+
+ // Border
+ color_to_rgba(border_color, &r, &g, &b, &a);
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_set_line_width(cr, me->coords.width);
+ if (dash_length > 0) {
+ cairo_set_dash(cr, &dash_length, 1, me->node.dash_offset);
+ }
+ cairo_stroke(cr);
+
+ cairo_destroy(cr);
+}
+
+static double
+ganv_circle_point(GnomeCanvasItem* item,
+ double x, double y,
+ int cx, int cy,
+ GnomeCanvasItem** actual_item)
+{
+ const GanvCircle* circle = GANV_CIRCLE(item);
+ const GanvCircleCoords* coords = &circle->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* class)
+{
+ GObjectClass* gobject_class = (GObjectClass*)class;
+ GtkObjectClass* object_class = (GtkObjectClass*)class;
+ GnomeCanvasItemClass* item_class = (GnomeCanvasItemClass*)class;
+ GanvNodeClass* node_class = (GanvNodeClass*)class;
+
+ parent_class = GANV_NODE_CLASS(g_type_class_peek_parent(class));
+
+ 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"),
+ _("radius of circle"),
+ 0, G_MAXDOUBLE,
+ 0.0,
+ G_PARAM_READWRITE));
+
+ object_class->destroy = ganv_circle_destroy;
+
+ node_class->is_within = ganv_circle_is_within;
+ node_class->tail_vector = ganv_circle_tail_vector;
+ node_class->head_vector = ganv_circle_head_vector;
+
+ item_class->update = ganv_circle_update;
+ item_class->bounds = ganv_circle_bounds;
+ item_class->point = ganv_circle_point;
+ item_class->render = ganv_circle_render;
+ item_class->draw = ganv_circle_draw;
+}
diff --git a/src/color.h b/src/color.h
new file mode 100644
index 0000000..03a390b
--- /dev/null
+++ b/src/color.h
@@ -0,0 +1,51 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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/gmacros.h>
+
+#define DEFAULT_TEXT_COLOR 0xFFFFFFFF
+#define DEFAULT_FILL_COLOR 0x1E2224FF
+#define DEFAULT_BORDER_COLOR 0x3E4244FF
+
+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..7d5b48c
--- /dev/null
+++ b/src/edge.c
@@ -0,0 +1,658 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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 <libgnomecanvas/libgnomecanvas.h>
+
+#include "ganv/canvas.h"
+#include "ganv/edge.h"
+#include "ganv/node.h"
+
+#include "./boilerplate.h"
+#include "./color.h"
+#include "./gettext.h"
+
+#include "ganv-private.h"
+
+// TODO: Very sloppy clipping for arrow heads
+#define ARROW_DEPTH 32
+#define ARROW_BREADTH 32
+
+// Uncomment to see control point path as straight lines
+//#define GANV_DEBUG_CURVES 1
+
+enum {
+ PROP_0,
+ PROP_TAIL,
+ PROP_HEAD,
+ PROP_WIDTH,
+ PROP_HANDLE_RADIUS,
+ PROP_DASH_LENGTH,
+ PROP_DASH_OFFSET,
+ PROP_COLOR,
+ PROP_CURVED,
+ PROP_ARROWHEAD,
+ PROP_SELECTED,
+ PROP_HIGHLIGHTED,
+ PROP_GHOST
+};
+
+G_DEFINE_TYPE(GanvEdge, ganv_edge, GNOME_TYPE_CANVAS_ITEM)
+
+static GnomeCanvasItemClass* parent_class;
+
+static void
+ganv_edge_init(GanvEdge* edge)
+{
+ edge->tail = NULL;
+ edge->head = NULL;
+
+ memset(&edge->coords, '\0', sizeof(GanvEdgeCoords));
+ edge->coords.width = 2.0;
+ edge->coords.handle_radius = 4.0;
+ edge->coords.curved = FALSE;
+ edge->coords.arrowhead = FALSE;
+
+ edge->old_coords = edge->coords;
+ edge->dash_length = 0.0;
+ edge->dash_offset = 0.0;
+ edge->color = 0xA0A0A0FF;
+}
+
+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.canvas);
+ if (canvas && !edge->ghost) {
+ ganv_canvas_remove_edge(canvas, edge);
+ edge->item.canvas = NULL;
+ }
+ edge->item.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));
+
+ GanvEdge* edge = GANV_EDGE(object);
+ GanvEdgeCoords* coords = &edge->coords;
+
+ switch (prop_id) {
+ SET_CASE(TAIL, object, edge->tail);
+ SET_CASE(HEAD, object, edge->head);
+ SET_CASE(WIDTH, double, coords->width);
+ SET_CASE(HANDLE_RADIUS, double, coords->handle_radius);
+ SET_CASE(DASH_LENGTH, double, edge->dash_length);
+ SET_CASE(DASH_OFFSET, double, edge->dash_offset);
+ SET_CASE(COLOR, uint, edge->color);
+ SET_CASE(CURVED, boolean, edge->coords.curved);
+ SET_CASE(ARROWHEAD, boolean, edge->coords.arrowhead);
+ SET_CASE(SELECTED, boolean, edge->selected);
+ SET_CASE(HIGHLIGHTED, boolean, edge->highlighted);
+ SET_CASE(GHOST, boolean, edge->ghost);
+ 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);
+
+ switch (prop_id) {
+ GET_CASE(TAIL, object, edge->tail);
+ GET_CASE(HEAD, object, edge->head);
+ GET_CASE(WIDTH, double, edge->coords.width);
+ SET_CASE(HANDLE_RADIUS, double, edge->coords.handle_radius);
+ GET_CASE(DASH_LENGTH, double, edge->dash_length);
+ GET_CASE(DASH_OFFSET, double, edge->dash_offset);
+ GET_CASE(COLOR, uint, edge->color);
+ GET_CASE(CURVED, boolean, edge->coords.curved);
+ GET_CASE(ARROWHEAD, boolean, edge->coords.arrowhead);
+ GET_CASE(SELECTED, boolean, edge->selected);
+ GET_CASE(HIGHLIGHTED, boolean, edge->highlighted);
+ SET_CASE(GHOST, boolean, edge->ghost);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+request_redraw(GnomeCanvas* canvas,
+ const GanvEdgeCoords* coords)
+{
+ 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);
+ gnome_canvas_request_redraw(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);
+ gnome_canvas_request_redraw(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);
+
+ gnome_canvas_request_redraw(canvas,
+ x1 - w, y1 - w,
+ x2 + w, y2 + w);
+ }
+
+ gnome_canvas_request_redraw(canvas,
+ coords->handle_x - coords->handle_radius,
+ coords->handle_y - coords->handle_radius,
+ coords->handle_x + coords->handle_radius,
+ coords->handle_y + coords->handle_radius);
+
+ if (coords->arrowhead) {
+ gnome_canvas_request_redraw(
+ canvas,
+ coords->x2 - ARROW_DEPTH,
+ coords->y2 - ARROW_BREADTH,
+ coords->x2 + ARROW_DEPTH,
+ coords->y2 + ARROW_BREADTH);
+ }
+}
+
+static void
+ganv_edge_bounds(GnomeCanvasItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ GanvEdge* edge = GANV_EDGE(item);
+
+ // TODO: This is not correct for curved edges
+ *x1 = MIN(edge->coords.x1, edge->coords.x2);
+ *y1 = MIN(edge->coords.y1, edge->coords.y2);
+ *x2 = MAX(edge->coords.x1, edge->coords.x2);
+ *y2 = MAX(edge->coords.y1, edge->coords.y2);
+}
+
+static void
+ganv_edge_update(GnomeCanvasItem* item,
+ double* affine,
+ ArtSVP* clip_path,
+ int flags)
+{
+ GanvEdge* edge = GANV_EDGE(item);
+
+ if (parent_class->update) {
+ (*parent_class->update)(item, affine, clip_path, flags);
+ }
+
+ // Request redraw of old location
+ request_redraw(item->canvas, &edge->old_coords);
+
+ // Calculate new coordinates from tail and head
+ GanvEdgeCoords* coords = &edge->coords;
+ GANV_NODE_GET_CLASS(edge->tail)->tail_vector(
+ edge->tail, edge->head,
+ &coords->x1, &coords->y1, &coords->cx1, &coords->cy1);
+ GANV_NODE_GET_CLASS(edge->head)->head_vector(
+ edge->head, edge->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);
+
+ coords->cx1 = coords->x1 + (coords->cx1 * (ceilf(fabs(dx)) / 4.0));
+ coords->cy1 += coords->y1;
+ coords->cx2 = coords->x2 + (coords->cx2 * (ceilf(fabs(dx)) / 4.0));
+ coords->cy2 += coords->y2;
+
+ // Update old coordinates
+ edge->old_coords = edge->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 item canvas coordinates
+ gnome_canvas_w2c_d(GNOME_CANVAS(item->canvas), x1, y1, &item->x1, &item->y1);
+ gnome_canvas_w2c_d(GNOME_CANVAS(item->canvas), x2, y2, &item->x2, &item->y2);
+
+ // Request redraw of new location
+ request_redraw(item->canvas, &edge->coords);
+}
+
+static void
+ganv_edge_render(GnomeCanvasItem* item,
+ GnomeCanvasBuf* buf)
+{
+ // Not implemented
+}
+
+static void
+ganv_edge_draw(GnomeCanvasItem* item,
+ GdkDrawable* drawable,
+ int x, int y,
+ int width, int height)
+{
+ GanvEdge* me = GANV_EDGE(item);
+ cairo_t* cr = gdk_cairo_create(drawable);
+
+ double src_x = me->coords.x1 - x;
+ double src_y = me->coords.y1 - y;
+ //double src_cx = me->coords.cx1 - x;
+ //double src_cy = me->coords.cy1 - y;
+ double dst_x = me->coords.x2 - x;
+ double dst_y = me->coords.y2 - y;
+ //double dst_cx = me->coords.cx2 - x;
+ //double dst_cy = me->coords.cy2 - y;
+
+ double dx = src_x - dst_x;
+ double dy = src_y - dst_y;
+
+ double r, g, b, a;
+ if (me->highlighted) {
+ color_to_rgba(highlight_color(me->color, 0x20), &r, &g, &b, &a);
+ } else {
+ color_to_rgba(me->color, &r, &g, &b, &a);
+ }
+ cairo_set_source_rgba(cr, r, g, b, a);
+
+ cairo_set_line_width(cr, me->coords.width);
+ cairo_move_to(cr, src_x, src_y);
+
+ const double dash_length = (me->selected ? 4.0 : me->dash_length);
+ if (dash_length > 0.0) {
+ double dashed[2] = { dash_length, dash_length };
+ cairo_set_dash(cr, dashed, 2, me->dash_offset);
+ }
+
+ const double join_x = (src_x + dst_x) / 2.0;
+ const double join_y = (src_y + dst_y) / 2.0;
+
+ if (me->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 = me->coords.cx1 - x;
+ const double src_y1 = me->coords.cy1 - y;
+ // 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 = me->coords.cx2 - x;
+ const double dst_y1 = me->coords.cy2 - y;
+ // 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);
+
+ if (me->coords.arrowhead) {
+ cairo_line_to(cr, dst_x - 12, dst_y - 4);
+ cairo_move_to(cr, dst_x, dst_y);
+ cairo_line_to(cr, dst_x - 12, dst_y + 4);
+ }
+
+#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);
+ cairo_restore(cr);
+#endif
+
+ } 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);
+
+ if (me->coords.arrowhead) {
+ const double ah = sqrt(dx * dx + dy * dy);
+ const double adx = dx / ah * 10.0;
+ const double ady = dy / ah * 10.0;
+
+ cairo_line_to(cr,
+ dst_x + adx - ady/1.5,
+ dst_y + ady + adx/1.5);
+ cairo_move_to(cr, dst_x, dst_y);
+ cairo_line_to(cr,
+ dst_x + adx + ady/1.5,
+ dst_y + ady - adx/1.5);
+ }
+ }
+
+ cairo_stroke(cr);
+
+ cairo_move_to(cr, join_x, join_y);
+ cairo_arc(cr, join_x, join_y, me->coords.handle_radius, 0, 2 * M_PI);
+ cairo_fill(cr);
+
+ cairo_destroy(cr);
+}
+
+static double
+ganv_edge_point(GnomeCanvasItem* item,
+ double x, double y,
+ int cx, int cy,
+ GnomeCanvasItem** actual_item)
+{
+ const GanvEdge* edge = GANV_EDGE(item);
+ const GanvEdgeCoords* coords = &edge->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->coords.handle_x;
+ const double handle_y = edge->coords.handle_y;
+
+ return handle_x >= x1
+ && handle_x <= x2
+ && handle_y >= y1
+ && handle_y <= y2;
+}
+
+static void
+ganv_edge_class_init(GanvEdgeClass* class)
+{
+ GObjectClass* gobject_class = (GObjectClass*)class;
+ GtkObjectClass* object_class = (GtkObjectClass*)class;
+ GnomeCanvasItemClass* item_class = (GnomeCanvasItemClass*)class;
+
+ parent_class = GNOME_CANVAS_ITEM_CLASS(g_type_class_peek_parent(class));
+
+ 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 line in canvas units"),
+ 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",
+ _("handle 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 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"),
+ _("offset for dashes (useful for 'rubber band' 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"),
+ _("color as an RGBA integer"),
+ 0, G_MAXUINT,
+ 0xA0A0A0FF,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_CURVED, g_param_spec_boolean(
+ "curved",
+ _("curved"),
+ _("whether line should be curved, not 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 end point"),
+ 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 to highlight the edge"),
+ 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->render = ganv_edge_render;
+ 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);
+ gnome_canvas_item_construct(&edge->item,
+ ganv_canvas_get_root(canvas),
+ first_prop_name, args);
+ va_end(args);
+
+ edge->tail = tail;
+ edge->head = head;
+
+ if (!edge->ghost) {
+ ganv_canvas_add_edge(canvas, edge);
+ }
+ return edge;
+}
+
+void
+ganv_edge_update_location(GanvEdge* edge)
+{
+ gnome_canvas_item_request_update(GNOME_CANVAS_ITEM(edge));
+}
+
+void
+ganv_edge_select(GanvEdge* edge)
+{
+ GanvCanvas* canvas = GANV_CANVAS(edge->item.canvas);
+ ganv_canvas_select_edge(canvas, edge);
+}
+
+void
+ganv_edge_unselect(GanvEdge* edge)
+{
+ GanvCanvas* canvas = GANV_CANVAS(edge->item.canvas);
+ ganv_canvas_unselect_edge(canvas, edge);
+}
+
+void
+ganv_edge_highlight(GanvEdge* edge)
+{
+ edge->highlighted = TRUE;
+ gnome_canvas_item_raise_to_top(GNOME_CANVAS_ITEM(edge));
+ gnome_canvas_item_request_update(GNOME_CANVAS_ITEM(edge));
+}
+
+void
+ganv_edge_unhighlight(GanvEdge* edge)
+{
+ edge->highlighted = FALSE;
+ gnome_canvas_item_request_update(GNOME_CANVAS_ITEM(edge));
+}
+
+void
+ganv_edge_tick(GanvEdge* edge,
+ double seconds)
+{
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(edge),
+ "dash-offset", seconds * 8.0,
+ NULL);
+}
+
+void
+ganv_edge_remove(GanvEdge* edge)
+{
+ if (!edge->ghost) {
+ ganv_canvas_remove_edge(
+ GANV_CANVAS(edge->item.canvas),
+ edge);
+ }
+}
diff --git a/src/ganv-private.h b/src/ganv-private.h
new file mode 100644
index 0000000..d44dc02
--- /dev/null
+++ b/src/ganv-private.h
@@ -0,0 +1,69 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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
+
+void
+ganv_canvas_add_node(GanvCanvas* canvas,
+ GanvNode* node);
+
+void
+ganv_canvas_remove_node(GanvCanvas* canvas,
+ GanvNode* node);
+
+void
+ganv_canvas_move_selected_items(GanvCanvas* canvas,
+ double dx,
+ double dy);
+
+void
+ganv_canvas_selection_move_finished(GanvCanvas* canvas);
+
+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_remove_edge(GanvCanvas* canvas,
+ GanvEdge* edge);
+
+void
+ganv_canvas_select_edge(GanvCanvas* canvas,
+ GanvEdge* edge);
+
+void
+ganv_canvas_unselect_edge(GanvCanvas* canvas,
+ GanvEdge* edge);
+
+GdkCursor*
+ganv_canvas_get_move_cursor(const GanvCanvas* canvas);
+
+gboolean
+ganv_canvas_port_event(GanvCanvas* canvas,
+ GanvPort* port,
+ GdkEvent* event);
+
+#endif /* GANV_PRIVATE_H */
diff --git a/src/ganv_bench.cpp b/src/ganv_bench.cpp
new file mode 100644
index 0000000..693882d
--- /dev/null
+++ b/src/ganv_bench.cpp
@@ -0,0 +1,182 @@
+/* This file is part of Ganv.
+ * Copyright 2011 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 (at your option)
+ * 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 <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;
+
+Module*
+make_module(Canvas* canvas)
+{
+ char name[8];
+
+ snprintf(name, 8, "mod%d", rand() % 10000);
+ Module* m(new Module(*canvas, name,
+ rand() % (int)canvas->width(),
+ rand() % (int)canvas->height(),
+ true,
+ 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;
+}
+
+Circle*
+make_circle(Canvas* canvas)
+{
+ char name[8];
+
+ snprintf(name, 8, "%d", rand() % 10000);
+ Circle* e(new Circle(*canvas, name,
+ rand() % (int)canvas->width(),
+ rand() % (int)canvas->height(),
+ 32.0,
+ 32.0));
+
+ ins.push_back(e);
+ outs.push_back(e);
+
+ return e;
+}
+
+bool
+quit()
+{
+ Gtk::Main::quit();
+ return true;
+}
+
+int
+print_usage(const char* name)
+{
+ fprintf(stderr,
+ "USAGE: %s [OPTIONS] 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/gettext.h b/src/gettext.h
new file mode 100644
index 0000000..ff66f1f
--- /dev/null
+++ b/src/gettext.h
@@ -0,0 +1,28 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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/module.c b/src/module.c
new file mode 100644
index 0000000..66eb486
--- /dev/null
+++ b/src/module.c
@@ -0,0 +1,792 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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.h>
+
+#include "ganv/canvas.h"
+#include "ganv/module.h"
+#include "ganv/port.h"
+
+#include "./color.h"
+#include "./boilerplate.h"
+#include "./gettext.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 MODULE_LABEL_PAD = 2.0;
+static const double MODULE_ICON_SIZE = 16;
+
+G_DEFINE_TYPE(GanvModule, ganv_module, GANV_TYPE_BOX)
+
+static GanvBoxClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_SHOW_PORT_LABELS
+};
+
+static void
+ganv_module_init(GanvModule* module)
+{
+ GANV_NODE(module)->can_head = FALSE;
+ GANV_NODE(module)->can_tail = FALSE;
+
+ module->ports = g_ptr_array_new();
+ module->icon_box = NULL;
+ module->embed_item = NULL;
+ module->embed_width = 0;
+ module->embed_height = 0;
+ module->widest_input = 0.0;
+ module->widest_output = 0.0;
+ module->show_port_labels = FALSE;
+ module->must_resize = FALSE;
+ module->port_size_changed = FALSE;
+}
+
+static void
+ganv_module_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_MODULE(object));
+
+ g_ptr_array_free(GANV_MODULE(object)->ports, TRUE);
+
+ GanvModule* module = GANV_MODULE(object);
+ FOREACH_PORT(module->ports, p) {
+ gtk_object_destroy(GTK_OBJECT(*p));
+ }
+
+ 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));
+
+ GanvModule* module = GANV_MODULE(object);
+
+ switch (prop_id) {
+ case PROP_SHOW_PORT_LABELS: {
+ const gboolean tmp = g_value_get_boolean(value);
+ if (module->show_port_labels != tmp) {
+ module->show_port_labels = tmp;
+ module->port_size_changed = TRUE;
+ module->must_resize = TRUE;
+ /* FIXME
+ FOREACH_PORT_CONST(gobj()->ports, p) {
+ (*p)->show_label(b);
+ }*/
+ gnome_canvas_item_request_update(GNOME_CANVAS_ITEM(object));
+ }
+ break;
+ }
+ 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));
+
+ GanvModule* module = GANV_MODULE(object);
+
+ switch (prop_id) {
+ GET_CASE(SHOW_PORT_LABELS, boolean, module->show_port_labels);
+ 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.label) {
+ g_object_get(G_OBJECT(module->box.node.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_CANVAS(GNOME_CANVAS_ITEM(module)->canvas);
+ GanvText* canvas_title = module->box.node.label;
+
+ GanvDirection direction = canvas->direction;
+
+ if (direction == GANV_VERTICAL) {
+ static const double PAD = 2.0;
+
+ double contents_width = PAD;
+ if (canvas_title) {
+ contents_width += title_w;
+ }
+ if (module->icon_box) {
+ contents_width += MODULE_ICON_SIZE + 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);
+
+ const double ports_width = PAD + ((m->input_width + PAD) * module->ports->len);
+
+ m->width = MAX(contents_width, ports_width);
+ m->width = MAX(m->width, module->embed_width);
+ 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;
+
+ if (module->icon_box)
+ m->width += MODULE_ICON_SIZE + 2;
+
+ // Title is wide, put inputs and outputs beside each other
+ m->horiz = (module->widest_input + module->widest_output + 10.0
+ < MAX(m->width, module->embed_width));
+
+ // Fit ports to module (or vice-versa)
+ m->input_width = module->widest_input;
+ m->output_width = module->widest_output;
+ double expand_w = (m->horiz ? (m->width / 2.0) : m->width) - hor_pad;
+ if (module->show_port_labels && !module->embed_item) {
+ m->input_width = MAX(module->widest_input, expand_w);
+ m->output_width = MAX(module->widest_output, expand_w);
+ }
+
+ const double widest = MAX(m->input_width, m->output_width);
+
+ if (module->embed_item) {
+ double above_w = MAX(m->width, widest + hor_pad);
+ double between_w = MAX(m->width,
+ (m->input_width
+ + m->output_width
+ + module->embed_width));
+
+ above_w = MAX(above_w, module->embed_width);
+
+ // Decide where to place embedded widget if necessary)
+ if (module->embed_width < module->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 && (module->widest_input == 0.0
+ || module->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);
+
+ double title_w, title_h;
+ title_size(module, &title_w, &title_h);
+
+ GanvText* canvas_title = module->box.node.label;
+
+ if (!canvas_title) {
+ return;
+ } else if (dir == GANV_HORIZONTAL) {
+ if (module->icon_box) {
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(canvas_title),
+ "x", MODULE_ICON_SIZE + 1.0,
+ NULL);
+ } else {
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(canvas_title),
+ "x", ((ganv_box_get_width(box) / 2.0)
+ - (title_w / 2.0)),
+ NULL);
+ }
+ } else {
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(canvas_title),
+ "x", ((ganv_box_get_width(box) / 2.0)
+ - (title_w / 2.0)),
+ "y", ganv_module_get_empty_port_depth(module) + 2.0,
+ NULL);
+ }
+}
+
+static void
+resize_horiz(GanvModule* module)
+{
+ GanvCanvas* canvas = GANV_CANVAS(GNOME_CANVAS_ITEM(module)->canvas);
+
+ Metrics m;
+ measure(module, &m);
+
+ double title_w, title_h;
+ title_size(module, &title_w, &title_h);
+
+ // Basic height contains title, icon
+ double header_height = 2.0 + title_h;
+
+ double height = header_height;
+
+ if (module->embed_item) {
+ gnome_canvas_item_set(module->embed_item,
+ "x", (double)m.embed_x,
+ 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 += module->embed_height;
+ }
+
+ // Move ports to appropriate locations
+ int i = 0;
+ gboolean last_was_input = FALSE;
+ double y = 0.0;
+ double h = 0.0;
+ FOREACH_PORT(module->ports, pi) {
+ GanvPort* const p = (*pi);
+ GanvBox* const pbox = GANV_BOX(p);
+ GanvNode* const pnode = GANV_NODE(p);
+ h = ganv_box_get_height(pbox);
+
+ if (p->is_input) {
+ y = header_height + (i * (h + 1.0));
+ ++i;
+ ganv_box_set_width(pbox, m.input_width);
+ ganv_node_move_to(pnode, 0.0, y);
+ last_was_input = TRUE;
+
+ ganv_canvas_for_each_edge_to(
+ canvas, pnode, ganv_edge_update_location);
+ } else {
+ if (!m.horiz || !last_was_input) {
+ y = header_height + (i * (h + 1.0));
+ ++i;
+ }
+ ganv_box_set_width(pbox, m.output_width);
+ ganv_node_move_to(pnode,
+ m.width - ganv_box_get_width(pbox),
+ y);
+ last_was_input = FALSE;
+
+ ganv_canvas_for_each_edge_from(
+ canvas, pnode, ganv_edge_update_location);
+ }
+ }
+
+ if (module->ports->len == 0) {
+ h += header_height;
+ }
+
+ height = y + h + 4.0;
+ if (module->embed_item && m.embed_between)
+ height = MAX(height, module->embed_height + header_height + 2.0);
+
+ ganv_box_set_height(&module->box, height);
+
+ place_title(module, GANV_HORIZONTAL);
+}
+
+static void
+resize_vert(GanvModule* module)
+{
+ GanvCanvas* canvas = GANV_CANVAS(GNOME_CANVAS_ITEM(module)->canvas);
+
+ Metrics m;
+ measure(module, &m);
+
+ double title_w, title_h;
+ title_size(module, &title_w, &title_h);
+
+ static const double PAD = 2.0;
+
+ const double port_depth = ganv_module_get_empty_port_depth(module);
+ const double port_breadth = ganv_module_get_empty_port_breadth(module);
+
+ if (module->embed_item) {
+ gnome_canvas_item_set(module->embed_item,
+ "x", (double)m.embed_x,
+ "y", port_depth + title_h,
+ NULL);
+ }
+
+ const double height = PAD + title_h
+ + module->embed_height + (port_depth * 2.0);
+
+ // Move ports to appropriate locations
+ int i = 0;
+ gboolean last_was_input = FALSE;
+ double x = 0.0;
+ FOREACH_PORT(module->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);
+ if (p->is_input) {
+ x = PAD + (i * (port_breadth + PAD));
+ ++i;
+ ganv_node_move_to(pnode, x, 0);
+ last_was_input = TRUE;
+
+ ganv_canvas_for_each_edge_to(canvas, pnode,
+ ganv_edge_update_location);
+ } else {
+ if (!last_was_input) {
+ x = PAD + (i * (port_breadth + PAD));
+ ++i;
+ }
+ ganv_node_move_to(pnode,
+ x,
+ height - ganv_box_get_height(pbox));
+ last_was_input = FALSE;
+
+ ganv_canvas_for_each_edge_from(canvas, pnode,
+ ganv_edge_update_location);
+ }
+ }
+
+ ganv_box_set_width(GANV_BOX(module), m.width);
+ ganv_box_set_height(GANV_BOX(module), height);
+
+ place_title(module, GANV_VERTICAL);
+}
+
+static void
+measure_ports(GanvModule* module)
+{
+ module->widest_input = 0.0;
+ module->widest_output = 0.0;
+ FOREACH_PORT_CONST(module->ports, pi) {
+ const GanvPort* const p = (*pi);
+ const double w = ganv_port_get_natural_width(p);
+ if (p->is_input) {
+ if (w > module->widest_input) {
+ module->widest_input = w;
+ }
+ } else {
+ if (w > module->widest_output) {
+ module->widest_output = w;
+ }
+ }
+ }
+}
+
+static void
+layout(GanvNode* self)
+{
+ GanvModule* module = GANV_MODULE(self);
+ GanvNode* node = GANV_NODE(self);
+ GanvCanvas* canvas = GANV_CANVAS(GNOME_CANVAS_ITEM(module)->canvas);
+
+ double label_w = 0.0;
+ double label_h = 0.0;
+ if (node->label) {
+ g_object_get(node->label,
+ "width", &label_w,
+ "height", &label_h,
+ NULL);
+ }
+
+ ganv_box_set_width(&module->box, label_w + (MODULE_LABEL_PAD * 2.0));
+ ganv_box_set_height(&module->box, label_h);
+
+ if (module->port_size_changed) {
+ measure_ports(module);
+ module->port_size_changed = FALSE;
+ }
+
+ switch (canvas->direction) {
+ case GANV_HORIZONTAL:
+ resize_horiz(module);
+ break;
+ case GANV_VERTICAL:
+ resize_vert(module);
+ break;
+ }
+
+ module->must_resize = FALSE;
+}
+
+static void
+ganv_module_resize(GanvNode* self)
+{
+ GanvModule* module = GANV_MODULE(self);
+
+ if (module->must_resize) {
+ layout(self);
+ }
+
+ if (parent_class->parent_class.resize) {
+ parent_class->parent_class.resize(self);
+ }
+}
+
+static void
+ganv_module_update(GnomeCanvasItem* item,
+ double* affine,
+ ArtSVP* clip_path,
+ int flags)
+{
+ GanvNode* node = GANV_NODE(item);
+ GanvModule* module = GANV_MODULE(item);
+ if (module->must_resize) {
+ layout(node);
+ }
+
+ GnomeCanvasItemClass* item_class = GNOME_CANVAS_ITEM_CLASS(parent_class);
+ item_class->update(item, affine, clip_path, flags);
+}
+
+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->ports, p) {
+ ganv_node_move(GANV_NODE(*p), 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->ports, p) {
+ ganv_node_move(GANV_NODE(*p), 0.0, 0.0);
+ }
+}
+
+static void
+ganv_module_class_init(GanvModuleClass* class)
+{
+ GObjectClass* gobject_class = (GObjectClass*)class;
+ GtkObjectClass* object_class = (GtkObjectClass*)class;
+ GnomeCanvasItemClass* item_class = (GnomeCanvasItemClass*)class;
+ GanvNodeClass* node_class = (GanvNodeClass*)class;
+
+ parent_class = GANV_BOX_CLASS(g_type_class_peek_parent(class));
+
+ gobject_class->set_property = ganv_module_set_property;
+ gobject_class->get_property = ganv_module_get_property;
+
+ object_class->destroy = ganv_module_destroy;
+
+ item_class->update = ganv_module_update;
+
+ node_class->move = ganv_module_move;
+ node_class->move_to = ganv_module_move_to;
+ node_class->resize = ganv_module_resize;
+}
+
+void
+ganv_module_add_port(GanvModule* module,
+ GanvPort* port)
+{
+ const double width = ganv_port_get_natural_width(port);
+ if (port->is_input && width > module->widest_input) {
+ module->widest_input = width;
+ module->must_resize = TRUE;
+ } else if (!port->is_input && width > module->widest_output) {
+ module->widest_output = width;
+ module->must_resize = TRUE;
+ }
+
+#if 0
+
+ double port_x, port_y;
+
+ // Place vertically
+ if (canvas()->direction() == Canvas::HORIZONTAL) {
+ if (gobj()->ports->len != 0) {
+ const Port* const last_port = *back();
+ port_y = last_port->get_y() + last_port->get_height() + 1;
+ } else {
+ port_y = 2.0 + title_height();
+ }
+ } else {
+ if (p->is_input()) {
+ port_y = 0.0;
+ } else {
+ port_y = get_height() - get_empty_port_depth();
+ }
+ }
+
+ // Place horizontally
+ Metrics m;
+ calculate_metrics(m);
+
+ set_width(m.width);
+ if (p->is_input()) {
+ p->set_width(m.input_width);
+ port_x = 0;
+ } else {
+ p->set_width(m.output_width);
+ port_x = m.width - p->get_width();
+ }
+
+ p->move_to(port_x, port_y);
+
+ g_ptr_array_add(gobj()->ports, p->gobj());
+ if (canvas()->direction() == Canvas::HORIZONTAL) {
+ set_height(p->get_y() + p->get_height() + 1);
+ }
+
+ place_title();
+#endif
+ module->must_resize = TRUE;
+
+ g_ptr_array_add(module->ports, port);
+ //if (canvas()->direction() == Canvas::HORIZONTAL) {
+ // set_height(p->get_y() + p->get_height() + 1);
+ //}
+
+ GanvCanvas* canvas = GANV_CANVAS(
+ GNOME_CANVAS_ITEM(module)->canvas);
+
+ place_title(module, canvas->direction);
+
+}
+
+void
+ganv_module_remove_port(GanvModule* module,
+ GanvPort* port)
+{
+#if 0
+ gboolean removed = g_ptr_array_remove(gobj()->ports, port->gobj());
+ if (removed) {
+ // Find new widest input or output, if necessary
+ if (port->is_input() && port->get_width() >= _widest_input) {
+ _widest_input = 0;
+ FOREACH_PORT_CONST(gobj()->ports, i) {
+ const Port* const p = (*i);
+ if (p->is_input() && p->get_width() >= _widest_input) {
+ _widest_input = p->get_width();
+ }
+ }
+ } else if (port->is_output() && port->get_width() >= _widest_output) {
+ _widest_output = 0;
+ FOREACH_PORT_CONST(gobj()->ports, i) {
+ const Port* const p = (*i);
+ if (p->is_output() && p->get_width() >= _widest_output) {
+ _widest_output = p->get_width();
+ }
+ }
+ }
+
+ _must_resize = true;
+ gnome_canvas_item_request_update(GNOME_CANVAS_ITEM(_gobj));
+ } else {
+ std::cerr << "Failed to find port to remove" << std::endl;
+ }
+#endif
+}
+
+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_CANVAS(
+ GNOME_CANVAS_ITEM(module)->canvas);
+
+ return ganv_canvas_get_font_size(canvas);
+}
+
+void
+ganv_module_set_icon(GanvModule* module,
+ GdkPixbuf* icon)
+{
+ if (module->icon_box) {
+ gtk_object_destroy(GTK_OBJECT(module->icon_box));
+ module->icon_box = NULL;
+ }
+
+ if (icon) {
+ module->icon_box = gnome_canvas_item_new(
+ GNOME_CANVAS_GROUP(module),
+ gnome_canvas_pixbuf_get_type(),
+ "x", 8.0,
+ "y", 10.0,
+ "pixbuf", icon,
+ NULL);
+
+ const double icon_w = gdk_pixbuf_get_width(icon);
+ const double icon_h = gdk_pixbuf_get_height(icon);
+ const double scale = MODULE_ICON_SIZE / ((icon_w > icon_h)
+ ? icon_w : icon_h);
+ const double scale_trans[6] = {
+ scale, 0.0, 0.0,
+ scale, 0.0, 0.0
+ };
+
+ gnome_canvas_item_affine_relative(module->icon_box, scale_trans);
+ gnome_canvas_item_raise_to_top(module->icon_box);
+ gnome_canvas_item_show(module->icon_box);
+ }
+ module->must_resize = TRUE;
+}
+
+static void
+on_embed_size_request(GtkWidget* widget,
+ GtkRequisition* r,
+ void* user_data)
+{
+ GanvModule* module = GANV_MODULE(user_data);
+ if (module->embed_width == r->width && module->embed_height == r->height) {
+ return;
+ }
+
+ module->embed_width = r->width;
+ module->embed_height = r->height;
+
+ module->must_resize = TRUE;
+
+ GtkAllocation allocation;
+ allocation.width = r->width;
+ allocation.height = r->width;
+
+ gtk_widget_size_allocate(widget, &allocation);
+ gnome_canvas_item_set(module->embed_item,
+ "width", (double)r->width,
+ "height", (double)r->height,
+ NULL);
+}
+
+void
+ganv_module_embed(GanvModule* module,
+ GtkWidget* widget)
+{
+ if (module->embed_item) {
+ gtk_object_destroy(GTK_OBJECT(module->embed_item));
+ module->embed_item = NULL;
+ }
+
+ if (!widget) {
+ module->embed_width = 0;
+ module->embed_height = 0;
+ module->must_resize = TRUE;
+ return;
+ }
+
+ double title_w, title_h;
+ title_size(module, &title_w, &title_h);
+
+ const double y = 4.0 + title_h;
+ module->embed_item = gnome_canvas_item_new(
+ GNOME_CANVAS_GROUP(module),
+ gnome_canvas_widget_get_type(),
+ "x", 2.0,
+ "y", y,
+ "widget", widget,
+ NULL);
+
+ gtk_widget_show_all(widget);
+
+ GtkRequisition r;
+ gtk_widget_size_request(widget, &r);
+ on_embed_size_request(widget, &r, module);
+
+ gnome_canvas_item_show(module->embed_item);
+ gnome_canvas_item_raise_to_top(module->embed_item);
+
+ g_signal_connect(widget, "size-request",
+ G_CALLBACK(on_embed_size_request), module);
+
+ layout(GANV_NODE(module));
+ gnome_canvas_item_request_update(GNOME_CANVAS_ITEM(module));
+}
+
+void
+ganv_module_for_each_port(GanvModule* module,
+ GanvPortFunction f,
+ void* data)
+{
+ const int len = module->ports->len;
+ GanvPort** copy = (GanvPort**)malloc(sizeof(GanvPort*) * len);
+ memcpy(copy, module->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..e7c3931
--- /dev/null
+++ b/src/node.c
@@ -0,0 +1,546 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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 <libgnomecanvas/libgnomecanvas.h>
+
+#include "ganv/canvas.h"
+#include "ganv/node.h"
+
+#include "./boilerplate.h"
+#include "./color.h"
+#include "./ganv-private.h"
+#include "./gettext.h"
+
+G_DEFINE_TYPE(GanvNode, ganv_node, GNOME_TYPE_CANVAS_GROUP)
+
+static GnomeCanvasGroupClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_PARTNER,
+ PROP_LABEL,
+ PROP_DASH_LENGTH,
+ PROP_DASH_OFFSET,
+ PROP_BORDER_WIDTH,
+ PROP_FILL_COLOR,
+ PROP_BORDER_COLOR,
+ PROP_CAN_TAIL,
+ PROP_CAN_HEAD,
+ PROP_SELECTED,
+ PROP_HIGHLIGHTED,
+ PROP_DRAGGABLE
+};
+
+static gboolean
+on_event(GanvNode* node, GdkEvent* event)
+{
+ return GANV_NODE_GET_CLASS(node)->on_event(node, event);
+}
+
+static void
+ganv_node_init(GanvNode* node)
+{
+ node->partner = NULL;
+ node->label = NULL;
+ node->dash_length = 0.0;
+ node->dash_offset = 0.0;
+ node->border_width = 2.0;
+ node->fill_color = DEFAULT_FILL_COLOR;
+ node->border_color = DEFAULT_BORDER_COLOR;
+ node->can_tail = FALSE;
+ node->can_head = FALSE;
+ node->selected = FALSE;
+ node->highlighted = FALSE;
+ node->draggable = FALSE;
+
+ g_signal_connect(G_OBJECT(node),
+ "event", G_CALLBACK(on_event), node);
+}
+
+static void
+ganv_node_realize(GnomeCanvasItem* item)
+{
+ GNOME_CANVAS_ITEM_CLASS(parent_class)->realize(item);
+ ganv_canvas_add_node(GANV_CANVAS(item->canvas),
+ 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);
+ if (node->label) {
+ gtk_object_destroy(GTK_OBJECT(node->label));
+ node->label = NULL;
+ }
+
+ GnomeCanvasItem* item = GNOME_CANVAS_ITEM(object);
+ ganv_node_disconnect(node);
+ if (item->canvas) {
+ ganv_canvas_remove_node(GANV_CANVAS(item->canvas), node);
+ }
+
+ node->partner = NULL;
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+}
+
+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);
+
+ switch (prop_id) {
+ SET_CASE(PARTNER, object, node->partner);
+ SET_CASE(DASH_LENGTH, double, node->dash_length);
+ SET_CASE(DASH_OFFSET, double, node->dash_offset);
+ SET_CASE(BORDER_WIDTH, double, node->border_width);
+ SET_CASE(FILL_COLOR, uint, node->fill_color);
+ SET_CASE(BORDER_COLOR, uint, node->border_color);
+ SET_CASE(CAN_TAIL, boolean, node->can_tail)
+ SET_CASE(CAN_HEAD, boolean, node->can_head)
+ SET_CASE(SELECTED, boolean, node->selected)
+ SET_CASE(HIGHLIGHTED, boolean, node->highlighted)
+ SET_CASE(DRAGGABLE, boolean, node->draggable)
+ case PROP_LABEL:
+ ganv_node_set_label(node, g_value_get_string(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);
+
+ typedef char* gstring;
+
+ switch (prop_id) {
+ GET_CASE(PARTNER, object, node->partner);
+ GET_CASE(LABEL, string, node->label->text);
+ GET_CASE(DASH_LENGTH, double, node->dash_length);
+ GET_CASE(DASH_OFFSET, double, node->dash_offset);
+ GET_CASE(BORDER_WIDTH, double, node->border_width);
+ GET_CASE(FILL_COLOR, uint, node->fill_color);
+ GET_CASE(BORDER_COLOR, uint, node->border_color);
+ GET_CASE(CAN_TAIL, boolean, node->can_tail)
+ GET_CASE(CAN_HEAD, boolean, node->can_head)
+ GET_CASE(SELECTED, boolean, node->selected)
+ GET_CASE(HIGHLIGHTED, boolean, node->highlighted)
+ GET_CASE(DRAGGABLE, boolean, node->draggable)
+ 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)
+{
+ g_object_get(G_OBJECT(self),
+ "x", x,
+ "y", y,
+ NULL);
+
+ *dx = 1.0;
+ *dy = 0.0;
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(self)->parent, x, y);
+}
+
+static void
+ganv_node_default_head_vector(const GanvNode* self,
+ const GanvNode* tail,
+ double* x,
+ double* y,
+ double* dx,
+ double* dy)
+{
+ g_object_get(G_OBJECT(self),
+ "x", x,
+ "y", y,
+ NULL);
+
+ *dx = -1.0;
+ *dy = 0.0;
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(self)->parent, x, y);
+}
+
+void
+ganv_node_get_draw_properties(const GanvNode* node,
+ double* dash_length,
+ double* border_color,
+ double* fill_color)
+{
+ *dash_length = node->dash_length;
+ *border_color = node->border_color;
+ *fill_color = node->fill_color;
+
+ if (node->selected) {
+ *dash_length = 4.0;
+ *border_color = highlight_color(node->border_color, 0x20);
+ }
+
+ if (node->highlighted) {
+ *fill_color = highlight_color(node->fill_color, 0x20);
+ *border_color = highlight_color(node->border_color, 0x20);
+ }
+}
+
+const char*
+ganv_node_get_label(const GanvNode* node)
+{
+ return node->label ? node->label->text : NULL;
+}
+
+void
+ganv_node_set_label(GanvNode* node, const char* str)
+{
+ if (str[0] == '\0' || !str) {
+ if (node->label) {
+ gtk_object_destroy(GTK_OBJECT(node->label));
+ node->label = NULL;
+ }
+ } else if (node->label) {
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(node->label),
+ "text", str,
+ NULL);
+ } else {
+ node->label = GANV_TEXT(gnome_canvas_item_new(
+ GNOME_CANVAS_GROUP(node),
+ ganv_text_get_type(),
+ "text", str,
+ "color", 0xFFFFFFFF,
+ NULL));
+ }
+
+ GanvNodeClass* klass = GANV_NODE_GET_CLASS(node);
+ if (klass->resize) {
+ klass->resize(node);
+ }
+}
+
+static void
+ganv_node_default_tick(GanvNode* self,
+ double seconds)
+{
+ GanvNode* node = GANV_NODE(self);
+ node->dash_offset = seconds * 8.0;
+ gnome_canvas_item_request_update(GNOME_CANVAS_ITEM(self));
+}
+
+static void
+ganv_node_default_disconnect(GanvNode* node)
+{
+ GanvCanvas* canvas = GANV_CANVAS(GNOME_CANVAS_ITEM(node)->canvas);
+ if (canvas) {
+ ganv_canvas_for_each_edge_on(canvas, node, ganv_edge_remove);
+ }
+}
+
+static void
+ganv_node_default_move(GanvNode* node,
+ double dx,
+ double dy)
+{
+ GanvCanvas* canvas = GANV_CANVAS(GNOME_CANVAS_ITEM(node)->canvas);
+ gnome_canvas_item_move(GNOME_CANVAS_ITEM(node), dx, dy);
+ ganv_canvas_for_each_edge_on(canvas, node,
+ ganv_edge_update_location);
+
+}
+
+static void
+ganv_node_default_move_to(GanvNode* node,
+ double x,
+ double y)
+{
+ GanvCanvas* canvas = GANV_CANVAS(GNOME_CANVAS_ITEM(node)->canvas);
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(node),
+ "x", x,
+ "y", y,
+ NULL);
+ if (node->can_tail) {
+ ganv_canvas_for_each_edge_from(
+ canvas, node, ganv_edge_update_location);
+ } else if (node->can_head) {
+ ganv_canvas_for_each_edge_to(
+ canvas, node, ganv_edge_update_location);
+ }
+}
+
+static gboolean
+ganv_node_default_on_event(GanvNode* node,
+ GdkEvent* event)
+{
+ GanvCanvas* canvas = GANV_CANVAS(GNOME_CANVAS_ITEM(node)->canvas);
+
+ // FIXME: put these somewhere better
+ static double last_x, last_y;
+ static double drag_start_x, drag_start_y;
+ static gboolean dragging = FALSE;
+
+ /*
+ double click_x,
+ gnome_canvas_item_w2i(GNOME_CANVAS_ITEM(GNOME_CANVAS_ITEM(node)->parent),
+ &click_x, &click_y);
+ */
+
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(node),
+ "highlighted", TRUE, NULL);
+ return TRUE;
+
+ case GDK_LEAVE_NOTIFY:
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(node),
+ "highlighted", FALSE, NULL);
+ 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 (!canvas->locked && node->draggable && event->button.button == 1) {
+ gnome_canvas_item_grab(
+ GNOME_CANVAS_ITEM(node),
+ GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK|GDK_BUTTON_PRESS_MASK,
+ ganv_canvas_get_move_cursor(canvas),
+ event->button.time);
+ dragging = TRUE;
+ return TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ if (dragging) {
+ gboolean selected;
+ g_object_get(G_OBJECT(node), "selected", &selected, NULL);
+ gnome_canvas_item_ungrab(GNOME_CANVAS_ITEM(node), event->button.time);
+ dragging = FALSE;
+ if (event->button.x != drag_start_x || event->button.y != drag_start_y) {
+ // Dragged
+ // FIXME: emit moved signal
+ ganv_canvas_selection_move_finished(canvas);
+ } 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* class)
+{
+ GObjectClass* gobject_class = (GObjectClass*)class;
+ GtkObjectClass* object_class = (GtkObjectClass*)class;
+ GnomeCanvasItemClass* item_class = (GnomeCanvasItemClass*)class;
+
+ parent_class = GNOME_CANVAS_GROUP_CLASS(g_type_class_peek_parent(class));
+
+ gobject_class->set_property = ganv_node_set_property;
+ gobject_class->get_property = ganv_node_get_property;
+
+ 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"),
+ NULL,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_DASH_LENGTH, g_param_spec_double(
+ "dash-length",
+ _("border dash length"),
+ _("length of 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"),
+ _("offset for dashes (useful for 'rubber band' 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 around this node."),
+ 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 area"),
+ 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 object 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_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));
+
+ object_class->destroy = ganv_node_destroy;
+
+ item_class->realize = ganv_node_realize;
+
+ class->disconnect = ganv_node_default_disconnect;
+ class->move = ganv_node_default_move;
+ class->move_to = ganv_node_default_move_to;
+ class->tick = ganv_node_default_tick;
+ class->tail_vector = ganv_node_default_tail_vector;
+ class->head_vector = ganv_node_default_head_vector;
+ class->on_event = ganv_node_default_on_event;
+}
diff --git a/src/port.c b/src/port.c
new file mode 100644
index 0000000..7316846
--- /dev/null
+++ b/src/port.c
@@ -0,0 +1,420 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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 "ganv/canvas.h"
+#include "ganv/port.h"
+#include "ganv/module.h"
+
+#include "./boilerplate.h"
+#include "./ganv-private.h"
+#include "./gettext.h"
+
+static const double PORT_LABEL_HPAD = 4.0;
+static const double PORT_LABEL_VPAD = 1.0;
+
+G_DEFINE_TYPE(GanvPort, ganv_port, GANV_TYPE_BOX)
+
+static GanvBoxClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_IS_INPUT
+};
+
+static void
+ganv_port_init(GanvPort* port)
+{
+ port->is_input = TRUE;
+}
+
+static void
+ganv_port_destroy(GtkObject* object)
+{
+ g_return_if_fail(object != NULL);
+ g_return_if_fail(GANV_IS_PORT(object));
+
+ GnomeCanvasItem* item = GNOME_CANVAS_ITEM(object);
+ GanvPort* port = GANV_PORT(object);
+ GanvCanvas* canvas = GANV_CANVAS(item->canvas);
+ if (port->is_input) {
+ ganv_canvas_for_each_edge_to(canvas,
+ &port->box.node,
+ ganv_edge_remove);
+ } else {
+ ganv_canvas_for_each_edge_from(canvas,
+ &port->box.node,
+ ganv_edge_remove);
+ }
+
+ 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->is_input);
+ 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->is_input);
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+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);
+
+ double px, py;
+ g_object_get(G_OBJECT(self), "x", &px, "y", &py, NULL);
+
+ *x = px + ganv_box_get_width(&port->box);
+ *y = py + ganv_box_get_height(&port->box) / 2.0;
+ *dx = 1.0;
+ *dy = 0.0;
+
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(self)->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);
+
+ double px, py;
+ g_object_get(G_OBJECT(self), "x", &px, "y", &py, NULL);
+
+ *x = px;
+ *y = py + ganv_box_get_height(&port->box) / 2.0;
+ *dx = -1.0;
+ *dy = 0.0;
+
+ gnome_canvas_item_i2w(GNOME_CANVAS_ITEM(self)->parent, x, y);
+}
+
+static void
+ganv_port_resize(GanvNode* self)
+{
+ GanvPort* port = GANV_PORT(self);
+ GanvNode* node = GANV_NODE(self);
+
+ double label_w, label_h;
+ g_object_get(node->label,
+ "width", &label_w,
+ "height", &label_h,
+ NULL);
+
+ ganv_box_set_width(&port->box, label_w + (PORT_LABEL_HPAD * 2.0));
+ ganv_box_set_height(&port->box, label_h + (PORT_LABEL_VPAD * 2.0));
+
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(node->label),
+ "x", PORT_LABEL_HPAD,
+ "y", PORT_LABEL_VPAD,
+ NULL);
+
+ if (parent_class->parent_class.resize) {
+ parent_class->parent_class.resize(self);
+ }
+}
+
+static void
+ganv_port_set_width(GanvBox* box,
+ double width)
+{
+ GanvPort* port = GANV_PORT(box);
+ parent_class->set_width(box, width);
+ if (port->control) {
+ ganv_port_set_control_value(port, port->control->value);
+ }
+}
+
+static void
+ganv_port_set_height(GanvBox* box,
+ double height)
+{
+ GanvPort* port = GANV_PORT(box);
+ parent_class->set_height(box, height);
+ if (port->control) {
+ double control_y1;
+ g_object_get(port->control->rect, "y1", &control_y1, NULL);
+ gnome_canvas_item_set(GNOME_CANVAS_ITEM(port->control->rect),
+ "y2", control_y1 + height,
+ NULL);
+ }
+}
+
+static gboolean
+on_event(GanvNode* node, GdkEvent* event)
+{
+ GnomeCanvasItem* item = GNOME_CANVAS_ITEM(node);
+ GanvCanvas* canvas = GANV_CANVAS(item->canvas);
+
+ return ganv_canvas_port_event(canvas, GANV_PORT(node), event);
+}
+
+static void
+ganv_port_class_init(GanvPortClass* class)
+{
+ GObjectClass* gobject_class = (GObjectClass*)class;
+ GtkObjectClass* object_class = (GtkObjectClass*)class;
+ GanvNodeClass* node_class = (GanvNodeClass*)class;
+ GanvBoxClass* box_class = (GanvBoxClass*)class;
+
+ parent_class = GANV_BOX_CLASS(g_type_class_peek_parent(class));
+
+ 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 (or an output, if false)"),
+ 0,
+ G_PARAM_READWRITE));
+
+ object_class->destroy = ganv_port_destroy;
+
+ node_class->on_event = on_event;
+ node_class->tail_vector = ganv_port_tail_vector;
+ node_class->head_vector = ganv_port_head_vector;
+ node_class->resize = ganv_port_resize;
+
+ 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));
+
+ GnomeCanvasItem* item = GNOME_CANVAS_ITEM(port);
+ va_list args;
+ va_start(args, first_prop_name);
+ gnome_canvas_item_construct(item,
+ GNOME_CANVAS_GROUP(module),
+ first_prop_name, args);
+ va_end(args);
+
+ port->is_input = is_input;
+
+ GanvBox* box = GANV_BOX(port);
+ box->radius_tl = (is_input ? 0.0 : 4.0);
+ box->radius_tr = (is_input ? 4.0 : 0.0);
+ box->radius_br = (is_input ? 4.0 : 0.0);
+ box->radius_bl = (is_input ? 0.0 : 4.0);
+
+ GanvNode* node = GANV_NODE(port);
+ node->can_tail = !is_input;
+ node->can_head = is_input;
+ node->draggable = FALSE;
+
+ GanvCanvas* canvas = GANV_CANVAS(item->canvas);
+ if (!node->label) {
+ const double depth = ganv_module_get_empty_port_depth(module);
+ const double breadth = ganv_module_get_empty_port_breadth(module);
+ if (canvas->direction == GANV_HORIZONTAL) {
+ ganv_box_set_width(box, depth);
+ ganv_box_set_height(box, breadth);
+ } else {
+ ganv_box_set_width(box, breadth);
+ ganv_box_set_height(box, depth);
+ }
+ }
+
+ ganv_module_add_port(module, port);
+ return port;
+}
+
+void
+ganv_port_show_control(GanvPort* port)
+{
+ port->control = (GanvPortControl*)malloc(sizeof(GanvPortControl));
+ port->control->value = 0.0f;
+ port->control->min = 0.0f;
+ port->control->max = 0.0f;
+ port->control->is_toggle = FALSE;
+ port->control->rect = GANV_BOX(gnome_canvas_item_new(
+ GNOME_CANVAS_GROUP(port),
+ ganv_box_get_type(),
+ "x1", 0.0,
+ "y1", 0.0,
+ "x2", 0.0,
+ "y2", ganv_box_get_height(&port->box),
+ "fill-color", 0xFFFFFF80,
+ "border-width", 0.0,
+ NULL));
+ gnome_canvas_item_show(GNOME_CANVAS_ITEM(port->control->rect));
+}
+
+void
+ganv_port_hide_control(GanvPort* port)
+{
+ gtk_object_destroy(GTK_OBJECT(port->control->rect));
+ free(port->control);
+ port->control = NULL;
+}
+
+void
+ganv_port_set_control_is_toggle(GanvPort* port,
+ gboolean is_toggle)
+{
+ if (port->control) {
+ port->control->is_toggle = is_toggle;
+ ganv_port_set_control_value(port, port->control->value);
+ }
+}
+
+void
+ganv_port_set_control_value(GanvPort* port,
+ float value)
+{
+ if (!port->control) {
+ return;
+ }
+
+ if (port->control->is_toggle) {
+ if (value != 0.0) {
+ value = port->control->max;
+ } else {
+ value = port->control->min;
+ }
+ }
+
+ if (value < port->control->min) {
+ port->control->min = value;
+ }
+ if (value > port->control->max) {
+ port->control->max = value;
+ }
+
+ if (port->control->max == port->control->min) {
+ port->control->max = port->control->min + 1.0;
+ }
+
+ const int inf = isinf(value);
+ if (inf == -1) {
+ value = port->control->min;
+ } else if (inf == 1) {
+ value = port->control->max;
+ }
+
+ const double w = (value - port->control->min)
+ / (port->control->max - port->control->min)
+ * ganv_box_get_width(&port->box);
+
+ if (isnan(w)) {
+ return;
+ }
+
+ ganv_box_set_width(port->control->rect, MAX(0.0, w - 1.0));
+#if 0
+ if (signal && _control->value == value)
+ signal = false;
+#endif
+
+ port->control->value = value;
+}
+
+void
+ganv_port_set_control_min(GanvPort* port,
+ float min)
+{
+ if (port->control) {
+ port->control->min = min;
+ ganv_port_set_control_value(port, port->control->value);
+ }
+}
+
+void
+ganv_port_set_control_max(GanvPort* port,
+ float max)
+{
+ if (port->control) {
+ port->control->max = max;
+ ganv_port_set_control_value(port, port->control->value);
+ }
+}
+
+double
+ganv_port_get_natural_width(const GanvPort* port)
+{
+ /*
+ Canvas* const canvas = _module->canvas();
+ if (canvas->direction() == Canvas::VERTICAL) {
+ return _module->empty_port_breadth();
+ } else*/
+ if (port->box.node.label) {
+ double label_w;
+ g_object_get(port->box.node.label, "width", &label_w, NULL);
+ return label_w + (PORT_LABEL_HPAD * 2.0);
+ } else {
+ //return _module->empty_port_depth();
+ return 4.0;
+ }
+}
+
+GanvModule*
+ganv_port_get_module(const GanvPort* port)
+{
+ return GANV_MODULE(GNOME_CANVAS_ITEM(port)->parent);
+}
diff --git a/src/text.c b/src/text.c
new file mode 100644
index 0000000..b239775
--- /dev/null
+++ b/src/text.c
@@ -0,0 +1,371 @@
+/* This file is part of Ganv.
+ * Copyright 2007-2011 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 (at your option)
+ * 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 <stdint.h>
+#include <string.h>
+#include <math.h>
+
+#include <gtk/gtkstyle.h>
+#include <libgnomecanvas/libgnomecanvas.h>
+
+#include "ganv/canvas.h"
+#include "ganv/text.h"
+
+#include "./color.h"
+#include "./boilerplate.h"
+#include "./gettext.h"
+
+G_DEFINE_TYPE(GanvText, ganv_text, GNOME_TYPE_CANVAS_ITEM)
+
+static GnomeCanvasItemClass* parent_class;
+
+enum {
+ PROP_0,
+ PROP_TEXT,
+ PROP_X,
+ PROP_Y,
+ PROP_WIDTH,
+ PROP_HEIGHT,
+ PROP_COLOR
+};
+
+static void
+ganv_text_init(GanvText* text)
+{
+ memset(&text->coords, '\0', sizeof(GanvTextCoords));
+ text->coords.width = 1.0;
+ text->coords.height = 1.0;
+ text->old_coords = text->coords;
+
+ text->surface = NULL;
+ text->text = NULL;
+ text->color = 0xFFFFFFFF;
+}
+
+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);
+
+ if (text->text) {
+ g_free(text->text);
+ text->text = NULL;
+ }
+
+ if (text->surface) {
+ cairo_surface_destroy(text->surface);
+ text->surface = NULL;
+ }
+
+ if (GTK_OBJECT_CLASS(parent_class)->destroy) {
+ (*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
+ }
+}
+
+static void
+ganv_text_layout(GanvText* text)
+{
+ GnomeCanvasItem* item = GNOME_CANVAS_ITEM(text);
+ GanvCanvas* canvas = GANV_CANVAS(item->canvas);
+ GtkWidget* widget = GTK_WIDGET(canvas);
+ double font_size = ganv_canvas_get_font_size(canvas);
+ guint color = 0xFFFFFFFF;
+
+ GtkStyle* style = gtk_rc_get_style(widget);
+ PangoFontDescription* font = pango_font_description_copy(style->font_desc);
+ PangoLayout* layout = gtk_widget_create_pango_layout(widget, text->text);
+ PangoContext* context = pango_layout_get_context(layout);
+ cairo_font_options_t* options = cairo_font_options_copy(
+ pango_cairo_context_get_font_options(context));
+
+ pango_font_description_set_size(font, font_size * (double)PANGO_SCALE);
+ pango_layout_set_font_description(layout, font);
+
+ if (cairo_font_options_get_antialias(options) == CAIRO_ANTIALIAS_SUBPIXEL) {
+ cairo_font_options_set_antialias(options, CAIRO_ANTIALIAS_GRAY);
+ }
+
+ pango_cairo_context_set_font_options(context, options);
+ cairo_font_options_destroy(options);
+
+ int width, height;
+ pango_layout_get_pixel_size(layout, &width, &height);
+
+ text->coords.width = width;
+ text->coords.height = height;
+
+ if (text->surface) {
+ cairo_surface_destroy(text->surface);
+ }
+
+ text->surface = cairo_image_surface_create(
+ CAIRO_FORMAT_ARGB32, width, height);
+
+ cairo_t* cr = cairo_create(text->surface);
+
+ double r, g, b, a;
+ color_to_rgba(color, &r, &g, &b, &a);
+
+ cairo_set_source_rgba(cr, r, g, b, a);
+ cairo_move_to(cr, 0, 0);
+ pango_cairo_show_layout(cr, layout);
+
+ cairo_destroy(cr);
+ g_object_unref(layout);
+ pango_font_description_free(font);
+
+ gnome_canvas_item_request_update(GNOME_CANVAS_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);
+
+ switch (prop_id) {
+ SET_CASE(X, double, text->coords.x);
+ SET_CASE(Y, double, text->coords.y);
+ SET_CASE(WIDTH, double, text->coords.width);
+ SET_CASE(HEIGHT, double, text->coords.height);
+ SET_CASE(COLOR, uint, text->color)
+ case PROP_TEXT:
+ free(text->text);
+ text->text = g_value_dup_string(value);
+ ganv_text_layout(text);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+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);
+
+ switch (prop_id) {
+ GET_CASE(TEXT, string, text->text)
+ GET_CASE(X, double, text->coords.x);
+ GET_CASE(Y, double, text->coords.y);
+ GET_CASE(WIDTH, double, text->coords.width);
+ GET_CASE(HEIGHT, double, text->coords.height);
+ GET_CASE(COLOR, uint, text->color)
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+ganv_text_bounds_item(GnomeCanvasItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ GanvText* text = GANV_TEXT(item);
+
+ *x1 = MIN(text->coords.x, text->coords.x + text->coords.width);
+ *y1 = MIN(text->coords.y, text->coords.y + text->coords.height);
+ *x2 = MAX(text->coords.x, text->coords.x + text->coords.width);
+ *y2 = MAX(text->coords.y, text->coords.y + text->coords.height);
+}
+
+static void
+ganv_text_bounds(GnomeCanvasItem* item,
+ double* x1, double* y1,
+ double* x2, double* y2)
+{
+ ganv_text_bounds_item(item, x1, y1, x2, y2);
+ gnome_canvas_item_i2w(item->parent, x1, y1);
+ gnome_canvas_item_i2w(item->parent, x2, y2);
+}
+
+static void
+ganv_text_update(GnomeCanvasItem* item,
+ double* affine,
+ ArtSVP* clip_path,
+ int flags)
+{
+ double x1, y1, x2, y2;
+ ganv_text_bounds(item, &x1, &y1, &x2, &y2);
+ gnome_canvas_request_redraw(item->canvas, x1, y1, x2, y2);
+
+ // I have no idea why this is necessary
+ item->x1 = x1;
+ item->y1 = y1;
+ item->x2 = x2;
+ item->y2 = y2;
+
+ parent_class->update(item, affine, clip_path, flags);
+}
+
+static double
+ganv_text_point(GnomeCanvasItem* item,
+ double x, double y,
+ int cx, int cy,
+ GnomeCanvasItem** 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_render(GnomeCanvasItem* item,
+ GnomeCanvasBuf* buf)
+{
+ // Not implemented
+}
+
+static void
+ganv_text_draw(GnomeCanvasItem* item,
+ GdkDrawable* drawable,
+ int x, int y,
+ int width, int height)
+{
+ GanvText* text = GANV_TEXT(item);
+ cairo_t* cr = gdk_cairo_create(drawable);
+
+ double wx = text->coords.x;
+ double wy = text->coords.y;
+ gnome_canvas_item_i2w(item, &wx, &wy);
+
+ // Round to the nearest pixel so text isn't blurry
+ wx = lrint(wx - x);
+ wy = lrint(wy - y);
+
+ cairo_set_source_surface(cr, text->surface, wx, wy);
+ cairo_paint(cr);
+
+ cairo_destroy(cr);
+}
+
+static void
+ganv_text_class_init(GanvTextClass* class)
+{
+ GObjectClass* gobject_class = (GObjectClass*)class;
+ GtkObjectClass* object_class = (GtkObjectClass*)class;
+ GnomeCanvasItemClass* item_class = (GnomeCanvasItemClass*)class;
+
+ parent_class = GNOME_CANVAS_ITEM_CLASS(g_type_class_peek_parent(class));
+
+ 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 text to display"),
+ NULL,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_X, g_param_spec_double(
+ "x",
+ _("x"),
+ _("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"),
+ _("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"),
+ _("width"),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 1.0,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(
+ gobject_class, PROP_HEIGHT, g_param_spec_double(
+ "height",
+ _("height"),
+ _("height"),
+ -G_MAXDOUBLE, G_MAXDOUBLE,
+ 1.0,
+ G_PARAM_READWRITE));
+
+ 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));
+
+ 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->render = ganv_text_render;
+ item_class->draw = ganv_text_draw;
+}