/* This file is part of FlowCanvas. * Copyright (C) 2007-2009 David Robillard * * FlowCanvas 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 2 of the License, or (at your option) any later * version. * * FlowCanvas 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 this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "Canvas.hpp" #include "Module.hpp" #include "Port.hpp" #ifdef HAVE_AGRAPH #include #endif using std::cerr; using std::endl; using std::list; using std::string; using std::vector; namespace FlowCanvas { sigc::signal Canvas::signal_item_entered; sigc::signal Canvas::signal_item_left; Canvas::Canvas(double width, double height) : _base_rect(*root(), 0, 0, width, height) , _select_rect(NULL) , _select_dash(NULL) , _zoom(1.0) , _width(width) , _height(height) , _drag_state(NOT_DRAGGING) , _direction(HORIZONTAL) , _remove_objects(true) , _locked(false) { set_scroll_region(0.0, 0.0, width, height); set_center_scroll_region(true); _base_rect.property_fill_color_rgba() = 0x000000FF; //_base_rect.show(); _base_rect.signal_event().connect(sigc::mem_fun(this, &Canvas::scroll_drag_handler)); _base_rect.signal_event().connect(sigc::mem_fun(this, &Canvas::canvas_event)); _base_rect.signal_event().connect(sigc::mem_fun(this, &Canvas::select_drag_handler)); _base_rect.signal_event().connect(sigc::mem_fun(this, &Canvas::connection_drag_handler)); set_dither(Gdk::RGB_DITHER_NORMAL); // NONE or NORMAL or MAX // Dash style for selected modules and selection box _select_dash = new ArtVpathDash(); _select_dash->n_dash = 2; _select_dash->dash = art_new(double, 2); _select_dash->dash[0] = 5; _select_dash->dash[1] = 5; Glib::signal_timeout().connect( sigc::mem_fun(this, &Canvas::animate_selected), 300); } Canvas::~Canvas() { destroy(); art_free(_select_dash->dash); delete _select_dash; } void Canvas::lock(bool l) { _locked = l; if (l) _base_rect.property_fill_color_rgba() = 0x131415FF; else _base_rect.property_fill_color_rgba() = 0x000000FF; } void Canvas::set_zoom(double pix_per_unit) { if (_zoom == pix_per_unit) return; _zoom = pix_per_unit; set_pixels_per_unit(_zoom); for (ItemList::iterator m = _items.begin(); m != _items.end(); ++m) (*m)->zoom(_zoom); for (list >::iterator c = _connections.begin(); c != _connections.end(); ++c) (*c)->zoom(_zoom); } void Canvas::scroll_to_center() { int win_width = 0; int win_height = 0; Glib::RefPtr win = get_window(); if (win) win->get_size(win_width, win_height); scroll_to((int)((_width - win_width) / 2.0), (int)((_height - win_height) / 2.0)); } void Canvas::zoom_full() { if (_items.empty()) return; int win_width, win_height; Glib::RefPtr win = get_window(); win->get_size(win_width, win_height); // Box containing all canvas items double left = DBL_MAX; double right = DBL_MIN; double top = DBL_MIN; double bottom = DBL_MAX; for (ItemList::iterator m = _items.begin(); m != _items.end(); ++m) { const boost::shared_ptr mod = (*m); if (!mod) continue; if (mod->property_x() < left) left = mod->property_x(); if (mod->property_x() + mod->width() > right) right = mod->property_x() + mod->width(); if (mod->property_y() < bottom) bottom = mod->property_y(); if (mod->property_y() + mod->height() > top) top = mod->property_y() + mod->height(); } static const double pad = 8.0; const double new_zoom = std::min( ((double)win_width / (double)(right - left + pad*2.0)), ((double)win_height / (double)(top - bottom + pad*2.0))); set_zoom(new_zoom); int scroll_x, scroll_y; w2c(lrintf(left - pad), lrintf(bottom - pad), scroll_x, scroll_y); scroll_to(scroll_x, scroll_y); } void Canvas::clear_selection() { unselect_ports(); for (list >::iterator m = _selected_items.begin(); m != _selected_items.end(); ++m) (*m)->set_selected(false); for (list >::iterator c = _selected_connections.begin(); c != _selected_connections.end(); ++c) (*c)->set_selected(false); _selected_items.clear(); _selected_connections.clear(); } void Canvas::unselect_connection(Connection* connection) { for (ConnectionList::iterator i = _selected_connections.begin(); i != _selected_connections.end();) { if ((*i).get() == connection) { i = _selected_connections.erase(i); } else { ++i; } } connection->set_selected(false); } /** Add a module to the current selection, and automagically select any connections * between selected modules */ void Canvas::select_item(boost::shared_ptr m) { assert(! m->selected()); _selected_items.push_back(m); for (ConnectionList::iterator i = _connections.begin(); i != _connections.end(); ++i) { const boost::shared_ptr c = (*i); const boost::shared_ptr src = c->source().lock(); const boost::shared_ptr dst = c->dest().lock(); if (!src || !dst) continue; const boost::shared_ptr src_port = boost::dynamic_pointer_cast(src); const boost::shared_ptr dst_port = boost::dynamic_pointer_cast(dst); if (!src_port || !dst_port) continue; const boost::shared_ptr src_module = src_port->module().lock(); const boost::shared_ptr dst_module = dst_port->module().lock(); if (!src_module || !dst_module) continue; if ( !c->selected()) { if (src_module == m && dst_module->selected()) { c->set_selected(true); _selected_connections.push_back(c); } else if (dst_module == m && src_module->selected()) { c->set_selected(true); _selected_connections.push_back(c); } } } m->set_selected(true); } void Canvas::unselect_item(boost::shared_ptr m) { // Remove any connections that aren't selected anymore because this module isn't boost::shared_ptr c; for (ConnectionList::iterator i = _selected_connections.begin(); i != _selected_connections.end() ; ) { c = (*i); if (boost::dynamic_pointer_cast(c->source().lock()) == m || boost::dynamic_pointer_cast(c->dest().lock()) == m) { c->set_selected(false); i = _selected_connections.erase(i); continue; } boost::shared_ptr src_port = boost::dynamic_pointer_cast(c->source().lock()); if (src_port && src_port->module().lock() == m) { c->set_selected(false); i = _selected_connections.erase(i); continue; } boost::shared_ptr dst_port = boost::dynamic_pointer_cast(c->dest().lock()); if (dst_port && dst_port->module().lock() == m) { c->set_selected(false); i = _selected_connections.erase(i); continue; } ++i; } // Remove the module for (list >::iterator i = _selected_items.begin(); i != _selected_items.end(); ++i) { if ((*i) == m) { _selected_items.erase(i); break; } } m->set_selected(false); } /** Removes all ports and connections and modules. */ void Canvas::destroy() { _remove_objects = false; _selected_items.clear(); _selected_connections.clear(); _connections.clear(); _selected_ports.clear(); _connect_port.reset(); _items.clear(); _remove_objects = true; } void Canvas::unselect_ports() { for (SelectedPorts::iterator i = _selected_ports.begin(); i != _selected_ports.end(); ++i) (*i)->set_selected(false); _selected_ports.clear(); _last_selected_port.reset(); } void Canvas::select_port(boost::shared_ptr p, bool unique) { if (unique) unselect_ports(); p->set_selected(true); SelectedPorts::iterator i = find(_selected_ports.begin(), _selected_ports.end(), p); if (i == _selected_ports.end()) _selected_ports.push_back(p); _last_selected_port = p; } void Canvas::unselect_port(boost::shared_ptr p) { SelectedPorts::iterator i = find(_selected_ports.begin(), _selected_ports.end(), p); if (i != _selected_ports.end()) _selected_ports.erase(i); p->set_selected(false); if (_last_selected_port == p) _last_selected_port.reset(); } void Canvas::select_port_toggle(boost::shared_ptr p, int mod_state) { if ((mod_state & GDK_CONTROL_MASK)) { if (p->selected()) unselect_port(p); else select_port(p); } else if ((mod_state & GDK_SHIFT_MASK)) { boost::shared_ptr m = p->module().lock(); if (_last_selected_port && m && _last_selected_port->module().lock() == m) { // Pivot around _last_selected_port in a single pass over module ports each click boost::shared_ptr old_last_selected = _last_selected_port; boost::shared_ptr first; bool done = false; const PortVector& ports = m->ports(); for (size_t i = 0; i < ports.size(); ++i) { if (!first && !done && (ports[i] == _last_selected_port || ports[i] == p)) first = ports[i]; if (first && !done && ports[i]->is_input() == first->is_input()) select_port(ports[i], false); else unselect_port(ports[i]); if (ports[i] != first && (ports[i] == old_last_selected || ports[i] == p)) done = true; } _last_selected_port = old_last_selected; } else { if (p->selected()) unselect_port(p); else select_port(p); } } else { if (p->selected()) unselect_ports(); else select_port(p, true); } } /** Sets the passed module's location to a reasonable default. */ void Canvas::set_default_placement(boost::shared_ptr m) { assert(m); // Simple cascade. This will get more clever in the future. double x = ((_width / 2.0) + (_items.size() * 25)); double y = ((_height / 2.0) + (_items.size() * 25)); m->move_to(x, y); } void Canvas::add_item(boost::shared_ptr m) { if (m) _items.push_back(m); } /** Remove an item from the canvas, cutting all references. * Returns true if item was found (and removed). */ bool Canvas::remove_item(boost::shared_ptr item) { bool ret = false; // Remove from selection for (list >::iterator i = _selected_items.begin(); i != _selected_items.end(); ++i) { if ((*i) == item) { _selected_items.erase(i); break; } } // Remove children ports from selection if item is a module boost::shared_ptr module = boost::dynamic_pointer_cast(item); if (module) { for (PortVector::iterator i = module->ports().begin(); i != module->ports().end(); ++i) { unselect_port(*i); } } // Remove from items for (ItemList::iterator i = _items.begin(); i != _items.end(); ++i) { if (*i == item) { ret = true; _items.erase(i); break; } } // Remove any connections adjacent to this item boost::shared_ptr c; for (ConnectionList::iterator i = _connections.begin(); i != _connections.end() ; ) { c = (*i); ConnectionList::iterator next = i; ++next; const boost::shared_ptr src_port = boost::dynamic_pointer_cast(c->source().lock()); const boost::shared_ptr dst_port = boost::dynamic_pointer_cast(c->dest().lock()); if (boost::dynamic_pointer_cast(c->source().lock()) == item || boost::dynamic_pointer_cast(c->dest().lock()) == item || (src_port && src_port->module().lock() == item) || (dst_port && dst_port->module().lock() == item)) remove_connection(c); i = next; } return ret; } boost::shared_ptr Canvas::remove_connection(boost::shared_ptr item1, boost::shared_ptr item2) { boost::shared_ptr ret; if (!_remove_objects) return ret; assert(item1); assert(item2); boost::shared_ptr c = get_connection(item1, item2); if (!c) { cerr << "Couldn't find connection." << endl; return ret; } else { remove_connection(c); return c; } } /** Return whether there is a connection between item1 and item2. * * Note that connections are directed, so this may return false when there * is a connection between the two items (in the opposite direction). */ bool Canvas::are_connected(boost::shared_ptr tail, boost::shared_ptr head) { for (ConnectionList::const_iterator c = _connections.begin(); c != _connections.end(); ++c) { const boost::shared_ptr src = (*c)->source().lock(); const boost::shared_ptr dst = (*c)->dest().lock(); if (src == tail && dst == head) return true; } return false; } /** Get the connection between two items. * * Note that connections are directed. * This will only return a connection from @a tail to @a head. */ boost::shared_ptr Canvas::get_connection(boost::shared_ptr tail, boost::shared_ptr head) const { for (ConnectionList::const_iterator i = _connections.begin(); i != _connections.end(); ++i) { const boost::shared_ptr src = (*i)->source().lock(); const boost::shared_ptr dst = (*i)->dest().lock(); if (src == tail && dst == head) return *i; } return boost::shared_ptr(); } bool Canvas::add_connection(boost::shared_ptr src, boost::shared_ptr dst, uint32_t color) { // Create (graphical) connection object boost::shared_ptr c(new Connection(shared_from_this(), src, dst, color)); src->add_connection(c); dst->add_connection(c); _connections.push_back(c); return true; } bool Canvas::add_connection(boost::shared_ptr c) { const boost::shared_ptr src = c->source().lock(); const boost::shared_ptr dst = c->dest().lock(); if (src && dst) { src->add_connection(c); dst->add_connection(c); _connections.push_back(c); return true; } else { return false; } } void Canvas::remove_connection(boost::shared_ptr connection) { if (!_remove_objects) return; unselect_connection(connection.get()); ConnectionList::iterator i = find(_connections.begin(), _connections.end(), connection); if (i != _connections.end()) { const boost::shared_ptr c = *i; const boost::shared_ptr src = c->source().lock(); const boost::shared_ptr dst = c->dest().lock(); if (src) src->remove_connection(c); if (dst) dst->remove_connection(c); _connections.erase(i); } } void Canvas::selection_joined_with(boost::shared_ptr port) { for (SelectedPorts::iterator i = _selected_ports.begin(); i != _selected_ports.end(); ++i) ports_joined(*i, port); } void Canvas::join_selection() { vector< boost::shared_ptr > inputs; vector< boost::shared_ptr > outputs; for (SelectedPorts::iterator i = _selected_ports.begin(); i != _selected_ports.end(); ++i) { if ((*i)->is_input()) inputs.push_back(*i); else outputs.push_back(*i); } if (inputs.size() == 1) { // 1 -> n for (size_t i = 0; i < outputs.size(); ++i) ports_joined(inputs[0], outputs[i]); } else if (outputs.size() == 1) { // n -> 1 for (size_t i = 0; i < inputs.size(); ++i) ports_joined(inputs[i], outputs[0]); } else { // n -> m size_t num_to_connect = std::min(inputs.size(), outputs.size()); for (size_t i = 0; i < num_to_connect; ++i) { ports_joined(inputs[i], outputs[i]); } } } /** Called when two ports are 'toggled' (connected or disconnected) */ void Canvas::ports_joined(boost::shared_ptr port1, boost::shared_ptr port2) { if (port1 == port2) return; assert(port1); assert(port2); port1->set_highlighted(false); port2->set_highlighted(false); string src_mod_name, dst_mod_name, src_port_name, dst_port_name; boost::shared_ptr src_port; boost::shared_ptr dst_port; if (port2->is_input() && ! port1->is_input()) { src_port = port1; dst_port = port2; } else if ( ! port2->is_input() && port1->is_input()) { src_port = port2; dst_port = port1; } else { return; } if (are_connected(src_port, dst_port)) disconnect(src_port, dst_port); else connect(src_port, dst_port); } /** Event handler for ports. * * These events can't be handled in the Port class because they have to do with * connections etc. which deal with multiple ports (ie _selected_port). Ports * pass their events on to this function to get around this. */ bool Canvas::port_event(GdkEvent* event, boost::weak_ptr weak_port) { boost::shared_ptr port = weak_port.lock(); if (!port) return false; static bool port_dragging = false; static bool control_dragging = false; static bool ignore_button_release = false; bool handled = true; switch (event->type) { case GDK_BUTTON_PRESS: if (event->button.button == 1) { boost::shared_ptr module = port->module().lock(); if (module && _locked && port->is_input()) { if (port->is_toggled()) { port->toggle(); ignore_button_release = true; } else { control_dragging = true; const double port_x = module->property_x() + port->property_x(); float new_control = ((event->button.x - port_x) / (double)port->width()); if (new_control < 0.0) new_control = 0.0; else if (new_control > 1.0) new_control = 1.0; new_control *= (port->control_max() - port->control_min()); new_control += port->control_min(); if (new_control < port->control_min()) new_control = port->control_min(); if (new_control > port->control_max()) new_control = port->control_max(); if (new_control != port->control_value()) port->set_control(new_control); } } else { port_dragging = true; } } else if (event->button.button == 3) { port->popup_menu(event->button.button, event->button.time); } else { handled = false; } break; case GDK_MOTION_NOTIFY: if (control_dragging) { boost::shared_ptr module = port->module().lock(); if (module) { const double port_x = module->property_x() + port->property_x(); float new_control = ((event->button.x - port_x) / (double)port->width()); if (new_control < 0.0) new_control = 0.0; else if (new_control > 1.0) new_control = 1.0; new_control *= (port->control_max() - port->control_min()); new_control += port->control_min(); assert(new_control >= port->control_min()); assert(new_control <= port->control_max()); if (new_control != port->control_value()) port->set_control(new_control); } } break; case GDK_BUTTON_RELEASE: if (port_dragging) { if (_connect_port) { // dragging ports_joined(port, _connect_port); unselect_ports(); } else { bool modded = event->button.state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK); if (!modded && _last_selected_port && _last_selected_port->is_input() != port->is_input()) { selection_joined_with(port); unselect_ports(); } else { select_port_toggle(port, event->button.state); } } port_dragging = false; } else if (control_dragging) { control_dragging = false; } else if (ignore_button_release) { ignore_button_release = false; } else { handled = false; } break; case GDK_ENTER_NOTIFY: signal_item_entered.emit(port.get()); if (!control_dragging && !port->selected()) { port->set_highlighted(true); return true; } break; case GDK_LEAVE_NOTIFY: if (port_dragging) { _drag_state = CONNECTION; _connect_port = port; port_dragging = false; _base_rect.grab( GDK_BUTTON_PRESS_MASK|GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK, Gdk::Cursor(Gdk::CROSSHAIR), event->crossing.time); } else if (!control_dragging) { port->set_highlighted(false); } signal_item_left.emit(port.get()); break; default: handled = false; } return handled; } bool Canvas::canvas_event(GdkEvent* event) { static const int scroll_increment = 10; int scroll_x, scroll_y; get_scroll_offsets(scroll_x, scroll_y); switch (event->type) { case GDK_KEY_PRESS: switch (event->key.keyval) { case GDK_Up: scroll_y -= scroll_increment; break; case GDK_Down: scroll_y += scroll_increment; break; case GDK_Left: scroll_x -= scroll_increment; break; case GDK_Right: scroll_x += scroll_increment; break; case GDK_Return: if (_selected_ports.size() > 1) { join_selection(); clear_selection(); } break; default: return false; } scroll_to(scroll_x, scroll_y); return true; default: return false; } } void Canvas::on_parent_changed(Gtk::Widget* old_parent) { _parent_event_connection.disconnect(); if (get_parent()) _parent_event_connection = get_parent()->signal_event().connect( sigc::mem_fun(*this, &Canvas::frame_event)); } bool Canvas::frame_event(GdkEvent* ev) { bool handled = false; // Zoom if (ev->type == GDK_SCROLL && (ev->scroll.state & GDK_CONTROL_MASK)) { if (ev->scroll.direction == GDK_SCROLL_UP) { set_zoom(_zoom * 1.25); handled = true; } else if (ev->scroll.direction == GDK_SCROLL_DOWN) { set_zoom(_zoom * 0.75); handled = true; } } return handled; } bool Canvas::scroll_drag_handler(GdkEvent* event) { bool handled = true; static int original_scroll_x = 0; static int original_scroll_y = 0; static double origin_x = 0; static double origin_y = 0; static double scroll_offset_x = 0; static double scroll_offset_y = 0; static double last_x = 0; static double last_y = 0; if (event->type == GDK_BUTTON_PRESS && event->button.button == 2) { _base_rect.grab(GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK, Gdk::Cursor(Gdk::FLEUR), event->button.time); get_scroll_offsets(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; scroll_to(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) { _base_rect.ungrab(event->button.time); _drag_state = NOT_DRAGGING; } else { handled = false; } return handled; } bool Canvas::select_drag_handler(GdkEvent* event) { boost::shared_ptr module; 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)) ) clear_selection(); _select_rect = new Gnome::Canvas::Rect(*root(), event->button.x, event->button.y, event->button.x, event->button.y); _select_rect->property_fill_color_rgba() = 0x273344FF; _select_rect->property_outline_color_rgba() = 0xEEEEFFFF; _select_rect->property_width_units() = 0.5; _select_rect->lower_to_bottom(); _base_rect.lower_to_bottom(); _base_rect.grab(GDK_POINTER_MOTION_MASK|GDK_BUTTON_RELEASE_MASK, Gdk::Cursor(Gdk::ARROW), event->button.time); return true; } else if (event->type == GDK_MOTION_NOTIFY && _drag_state == SELECT) { assert(_select_rect); double x = event->button.x, y = event->button.y; if (event->motion.is_hint) { gint t_x; gint t_y; GdkModifierType state; gdk_window_get_pointer(event->motion.window, &t_x, &t_y, &state); x = t_x; y = t_y; } _select_rect->property_x2() = x; _select_rect->property_y2() = y; return true; } else if (event->type == GDK_BUTTON_RELEASE && _drag_state == SELECT) { // Select all modules within rect for (ItemList::iterator i = _items.begin(); i != _items.end(); ++i) { module = (*i); if (module->is_within(*_select_rect)) { if (module->selected()) unselect_item(module); else select_item(module); } } _base_rect.ungrab(event->button.time); delete _select_rect; _select_rect = NULL; _drag_state = NOT_DRAGGING; return true; } return false; } /** Updates _select_dash for rotation effect, and updates any * selected item's borders (and the selection rectangle). */ bool Canvas::animate_selected() { static int i = 0; i = (i+1) % 10; _select_dash->offset = i; for (ItemList::iterator m = _selected_items.begin(); m != _selected_items.end(); ++m) (*m)->select_tick(); for (ConnectionList::iterator c = _selected_connections.begin(); c != _selected_connections.end(); ++c) (*c)->select_tick(); return true; } bool Canvas::connection_drag_handler(GdkEvent* event) { bool handled = true; // These are invisible, just used for making connections (while dragging) static boost::shared_ptr drag_module; static boost::shared_ptr drag_port; static boost::shared_ptr drag_connection; static boost::shared_ptr snapped_port; static bool snapped = false; /*if (event->type == GDK_BUTTON_PRESS && event->button.button == 2) { _drag_state = SCROLL; } else */if (event->type == GDK_MOTION_NOTIFY && _drag_state == CONNECTION) { double x = event->button.x, y = event->button.y; if (event->motion.is_hint) { gint t_x; gint t_y; GdkModifierType state; gdk_window_get_pointer(event->motion.window, &t_x, &t_y, &state); x = t_x; y = t_y; } root()->w2i(x, y); if (!drag_connection) { // Havn't created the connection yet assert(drag_port == NULL); assert(_connect_port); drag_module = boost::shared_ptr(new Module(shared_from_this(), "", x, y)); bool drag_port_is_input = true; if (_connect_port->is_input()) drag_port_is_input = false; drag_port = boost::shared_ptr(new Port(drag_module, "", drag_port_is_input, _connect_port->color())); //drag_port->hide(); drag_module->hide(); drag_module->move_to(x, y); drag_port->property_x() = 0; drag_port->property_y() = 0; drag_port->_rect->property_x2() = 1; drag_port->_rect->property_y2() = 1; if (drag_port_is_input) drag_connection = boost::shared_ptr(new Connection( shared_from_this(), _connect_port, drag_port, _connect_port->color() + 0x22222200)); else drag_connection = boost::shared_ptr(new Connection( shared_from_this(), drag_port, _connect_port, _connect_port->color() + 0x22222200)); drag_connection->update_location(); } if (snapped) { if (drag_connection) drag_connection->hide(); boost::shared_ptr p = get_port_at(x, y); if (drag_connection) drag_connection->show(); if (p) { boost::shared_ptr m = p->module().lock(); if (m) { if (!p->selected()) { if (snapped_port) snapped_port->set_highlighted(false); p->set_highlighted(true); snapped_port = p; } drag_module->property_x() = m->property_x().get_value(); drag_module->_module_box.property_x2() = m->_module_box.property_x2().get_value(); drag_module->property_y() = m->property_y().get_value(); drag_module->_module_box.property_y2() = m->_module_box.property_y2().get_value(); drag_port->property_x() = p->property_x().get_value(); drag_port->property_y() = p->property_y().get_value(); } } else { // off the port now, unsnap if (snapped_port) snapped_port->set_highlighted(false); snapped_port.reset(); snapped = false; drag_module->property_x() = x; drag_module->property_y() = y; drag_port->property_x() = 0; drag_port->property_y() = 0; drag_port->_rect->property_x2() = 1; drag_port->_rect->property_y2() = 1; } drag_connection->update_location(); } else { // not snapped to a port assert(drag_module); assert(drag_port); assert(_connect_port); // "Snap" to port, if we're on a port and it's the right direction if (drag_connection) drag_connection->hide(); boost::shared_ptr p = get_port_at(x, y); if (drag_connection) drag_connection->show(); if (p && p->is_input() != _connect_port->is_input()) { boost::shared_ptr m = p->module().lock(); if (m) { p->set_highlighted(true); snapped_port = p; snapped = true; // Make drag module and port exactly the same size/loc as the snapped drag_module->move_to(m->property_x().get_value(), m->property_y().get_value()); drag_module->set_width(m->width()); drag_module->set_height(m->height()); drag_port->property_x() = p->property_x().get_value(); drag_port->property_y() = p->property_y().get_value(); // Make the drag port as wide as the snapped port // so the connection coords are the same drag_port->_rect->property_x2() = p->_rect->property_x2().get_value(); drag_port->_rect->property_y2() = p->_rect->property_y2().get_value(); } } else { drag_module->property_x() = x; drag_module->property_y() = y - 7; // FIXME: s#7#cursor_height/2# } drag_connection->update_location(); } } else if (event->type == GDK_BUTTON_RELEASE && _drag_state == CONNECTION) { _base_rect.ungrab(event->button.time); double x = event->button.x; double y = event->button.y; _base_rect.i2w(x, y); if (drag_connection) drag_connection->hide(); boost::shared_ptr p = get_port_at(x, y); if (drag_connection) drag_connection->show(); if (p) { if (p == _connect_port) { // drag ended on same port it started on if (_selected_ports.empty()) { // no active port, just activate (hilite) it select_port(_connect_port); } else { // there is already an active port, connect it with this one selection_joined_with(_connect_port); unselect_ports(); _connect_port.reset(); snapped_port.reset(); } } else { // drag ended on different port //p->set_highlighted(false); ports_joined(_connect_port, p); unselect_ports(); _connect_port.reset(); snapped_port.reset(); } } // Clean up dragging stuff if (_connect_port) _connect_port->set_highlighted(false); _drag_state = NOT_DRAGGING; drag_connection.reset(); drag_module.reset(); drag_port.reset(); unselect_ports(); snapped_port.reset(); _connect_port.reset(); } else { handled = false; } return handled; } boost::shared_ptr Canvas::get_port_at(double x, double y) { // Loop through every port and see if the item at these coordinates is that port // (if you're thinking this is slow, stupid, and disgusting, you're right) for (ItemList::const_iterator i = _items.begin(); i != _items.end(); ++i) { const boost::shared_ptr m = boost::dynamic_pointer_cast(*i); if (m && m->point_is_within(x, y)) return m->port_at(x, y); } return boost::shared_ptr(); } #ifdef HAVE_AGRAPH class GVNodes : public std::map, 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; }; #else class GVNodes : public std::map, void*> {}; #endif GVNodes Canvas::layout_dot(bool use_length_hints, const std::string& filename) { GVNodes nodes; #ifdef HAVE_AGRAPH /* FIXME: Are the strdup's here a leak? * GraphViz documentation disagrees with function prototypes. */ GVC_t* gvc = gvContext(); Agraph_t* G = agopen((char*)"g", AGDIGRAPH); nodes.gvc = gvc; nodes.G = G; if (_direction == HORIZONTAL) agraphattr(G, (char*)"rankdir", (char*)"LR"); else agraphattr(G, (char*)"rankdir", (char*)"TD"); unsigned id = 0; for (ItemList::const_iterator i = _items.begin(); i != _items.end(); ++i) { std::ostringstream ss; ss << "n" << id++; Agnode_t* node = agnode(G, strdup(ss.str().c_str())); if (boost::dynamic_pointer_cast(*i)) { ss.str(""); ss << (*i)->width() / 96.0; agsafeset(node, (char*)"width", strdup(ss.str().c_str()), (char*)""); ss.str(""); ss << (*i)->height() / 96.0; agsafeset(node, (char*)"height", strdup(ss.str().c_str()), (char*)""); agsafeset(node, (char*)"shape", (char*)"box", (char*)""); agsafeset(node, (char*)"label", (char*)(*i)->name().c_str(), (char*)""); } else { agsafeset(node, (char*)"width", (char*)"1.0", (char*)""); agsafeset(node, (char*)"height", (char*)"1.0", (char*)""); agsafeset(node, (char*)"shape", (char*)"ellipse", (char*)""); agsafeset(node, (char*)"label", (char*)(*i)->name().c_str(), (char*)""); } assert(node); nodes.insert(std::make_pair(*i, node)); } for (ConnectionList::iterator i = _connections.begin(); i != _connections.end(); ++i) { const boost::shared_ptr c = *i; boost::shared_ptr src_port = boost::dynamic_pointer_cast(c->source().lock()); boost::shared_ptr dst_port = boost::dynamic_pointer_cast(c->dest().lock()); boost::shared_ptr src_item = boost::dynamic_pointer_cast(c->source().lock()); boost::shared_ptr dst_item = boost::dynamic_pointer_cast(c->dest().lock()); GVNodes::iterator src_i = src_port ? nodes.find(src_port->module().lock()) : nodes.find(src_item); GVNodes::iterator dst_i = dst_port ? nodes.find(dst_port->module().lock()) : nodes.find(dst_item); assert(src_i != nodes.end() && dst_i != nodes.end()); Agnode_t* src_node = src_i->second; Agnode_t* dst_node = dst_i->second; assert(src_node && dst_node); Agedge_t* edge = agedge(G, src_node, dst_node); if (use_length_hints && c->length_hint() != 0) { std::ostringstream len_ss; len_ss << c->length_hint(); agsafeset(edge, (char*)"minlen", strdup(len_ss.str().c_str()), (char*)"1.0"); } } // Add edges between partners to have them lined up as if they are connected for (GVNodes::iterator i = nodes.begin(); i != nodes.end(); ++i) { boost::shared_ptr partner = i->first->partner().lock(); if (partner) { GVNodes::iterator p = nodes.find(partner); if (p != nodes.end()) agedge(G, i->second, p->second); } } gvLayout(gvc, G, (char*)"dot"); gvRender(gvc, G, (char*)"dot", fopen("/dev/null", "w")); if (filename != "") { FILE* fd = fopen(filename.c_str(), "w"); gvRender(gvc, G, (char*)"dot", fd); fclose(fd); } #endif return nodes; } void Canvas::render_to_dot(const string& dot_output_filename) { #ifdef HAVE_AGRAPH GVNodes nodes = layout_dot(false, dot_output_filename); nodes.cleanup(); #endif } void Canvas::arrange(bool use_length_hints, bool center) { #ifdef HAVE_AGRAPH GVNodes nodes = layout_dot(use_length_hints, ""); double least_x=HUGE_VAL, least_y=HUGE_VAL, most_x=0, most_y=0; // Set numeric locale to POSIX for reading graphviz output with strtod char* locale = strdup(setlocale(LC_NUMERIC, NULL)); setlocale(LC_NUMERIC, "POSIX"); // Arrange to graphviz coordinates for (GVNodes::iterator i = nodes.begin(); i != nodes.end(); ++i) { const string pos = agget(i->second, (char*)"pos"); const string x_str = pos.substr(0, pos.find(",")); const string y_str = pos.substr(pos.find(",")+1); const double x = strtod(x_str.c_str(), NULL) * 1.25; const double y = -strtod(y_str.c_str(), NULL) * 1.25; i->first->property_x() = x - i->first->width()/2.0; i->first->property_y() = y - i->first->height()/2.0; least_x = std::min(least_x, x); least_y = std::min(least_y, y); most_x = std::max(most_x, x); most_y = std::max(most_y, y); } // Reset numeric locale to original value setlocale(LC_NUMERIC, locale); free(locale); const double graph_width = most_x - least_x; const double graph_height = most_y - least_y; //cerr << "CWH: " << _width << ", " << _height << endl; //cerr << "GWH: " << graph_width << ", " << graph_height << endl; if (graph_width + 10 > _width) resize(graph_width + 10, _height); if (graph_height + 10 > _height) resize(_width, graph_height + 10); nodes.cleanup(); if (center) { move_contents_to_internal( _width / 2.0 - (graph_width / 2.0), _height / 2.0 - (graph_height / 2.0), least_x, least_y); scroll_to_center(); } else { static const double border_width = 64.0; move_contents_to_internal(border_width, border_width, least_x, least_y); scroll_to(0, 0); } for (ItemList::const_iterator i = _items.begin(); i != _items.end(); ++i) (*i)->store_location(); #endif } void Canvas::move_contents_to(double x, double y) { double min_x=HUGE_VAL, min_y=HUGE_VAL; for (ItemList::const_iterator i = _items.begin(); i != _items.end(); ++i) { min_x = std::min(min_x, double((*i)->property_x())); min_y = std::min(min_y, double((*i)->property_y())); } move_contents_to_internal(x, y, min_x, min_y); } void Canvas::move_contents_to_internal(double x, double y, double min_x, double min_y) { for (ItemList::const_iterator i = _items.begin(); i != _items.end(); ++i) (*i)->move(x - min_x, y - min_y); } void Canvas::resize(double width, double height) { if (width != _width || height != _height) { _base_rect.property_x2() = _base_rect.property_x1() + width; _base_rect.property_y2() = _base_rect.property_y1() + height; _width = width; _height = height; set_scroll_region(0.0, 0.0, width, height); } } void Canvas::resize_all_items() { for (ItemList::const_iterator i = _items.begin(); i != _items.end(); ++i) (*i)->resize(); } } // namespace FlowCanvas