diff options
Diffstat (limited to 'src/port.c')
-rw-r--r-- | src/port.c | 735 |
1 files changed, 735 insertions, 0 deletions
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; +} |