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