/* This file is part of Ganv. * Copyright 2007-2016 David Robillard * * 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 . */ #include "boilerplate.h" #include "color.h" #include "ganv-marshal.h" #include "ganv-private.h" #include "gettext.h" #include #include #include #include #include #include #include #include #include #include #include #include #include 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 = (GanvNodePrivate*)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; } /// Expands the canvas to contain a moved/resized node if necessary static void ganv_node_expand_canvas(GanvNode* node) { GanvItem* item = GANV_ITEM(node); GanvCanvas* canvas = ganv_item_get_canvas(item); if (item->impl->parent == ganv_canvas_root(canvas)) { const double pad = 10.0; double x1 = 0.0; double y1 = 0.0; double x2 = 0.0; double y2 = 0.0; ganv_item_get_bounds(item, &x1, &y1, &x2, &y2); ganv_item_i2w(item, &x1, &y1); ganv_item_i2w(item, &x2, &y2); double canvas_w = 0.0; double canvas_h = 0.0; ganv_canvas_get_size(canvas, &canvas_w, &canvas_h); if (x2 + pad > canvas_w || y2 + pad > canvas_h) { ganv_canvas_resize(canvas, MAX(x1 + pad, canvas_w), MAX(y2 + pad, canvas_h)); } } } 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); ganv_node_expand_canvas(node); } static void ganv_node_draw(GanvItem* item, cairo_t* cr, double cx, double cy, double cw, double ch) { (void)item; (void)cr; (void)cx; (void)cy; (void)cw; (void)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) { (void)head; 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) { (void)tail; 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 = NAN; static double last_y = NAN; static double drag_start_x = NAN; static double drag_start_y = NAN; 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 = FALSE; 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 = FALSE; 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 = 0; int t_y = 0; GdkModifierType state = (GdkModifierType)0; 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; }