diff options
author | David Robillard <d@drobilla.net> | 2011-12-06 21:01:38 +0000 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2011-12-06 21:01:38 +0000 |
commit | 0731f12beaa0cfc0de56dc05ca3814143fd394a5 (patch) | |
tree | d8a98ee48badba378172d3a1c46fba2f2e266d37 /src | |
download | ganv-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.cpp | 1953 | ||||
-rw-r--r-- | src/Port.cpp | 52 | ||||
-rw-r--r-- | src/boilerplate.h | 45 | ||||
-rw-r--r-- | src/box.c | 504 | ||||
-rw-r--r-- | src/circle.c | 351 | ||||
-rw-r--r-- | src/color.h | 51 | ||||
-rw-r--r-- | src/edge.c | 658 | ||||
-rw-r--r-- | src/ganv-private.h | 69 | ||||
-rw-r--r-- | src/ganv_bench.cpp | 182 | ||||
-rw-r--r-- | src/gettext.h | 28 | ||||
-rw-r--r-- | src/module.c | 792 | ||||
-rw-r--r-- | src/node.c | 546 | ||||
-rw-r--r-- | src/port.c | 420 | ||||
-rw-r--r-- | src/text.c | 371 |
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; +} |