ladish/gui/flowcanvas/Canvas.cpp

1460 lines
37 KiB
C++

/* This file is part of FlowCanvas.
* Copyright (C) 2007-2009 David Robillard <http://drobilla.net>
*
* 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 <algorithm>
#include <cassert>
#include <cmath>
#include <iostream>
#include <list>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include <boost/enable_shared_from_this.hpp>
#include "config.h"
#include "Canvas.hpp"
#include "Module.hpp"
#include "Port.hpp"
#ifdef HAVE_AGRAPH
#include <gvc.h>
#endif
using std::cerr;
using std::endl;
using std::list;
using std::string;
using std::vector;
namespace FlowCanvas {
sigc::signal<void, Gnome::Canvas::Item*> Canvas::signal_item_entered;
sigc::signal<void, Gnome::Canvas::Item*> 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<boost::shared_ptr<Connection> >::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<Gdk::Window> 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<Gdk::Window> 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<Item> 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<boost::shared_ptr<Item> >::iterator m = _selected_items.begin(); m != _selected_items.end(); ++m)
(*m)->set_selected(false);
for (list<boost::shared_ptr<Connection> >::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<Item> m)
{
assert(! m->selected());
_selected_items.push_back(m);
for (ConnectionList::iterator i = _connections.begin(); i != _connections.end(); ++i) {
const boost::shared_ptr<Connection> c = (*i);
const boost::shared_ptr<Connectable> src = c->source().lock();
const boost::shared_ptr<Connectable> dst = c->dest().lock();
if (!src || !dst)
continue;
const boost::shared_ptr<Port> src_port
= boost::dynamic_pointer_cast<Port>(src);
const boost::shared_ptr<Port> dst_port
= boost::dynamic_pointer_cast<Port>(dst);
if (!src_port || !dst_port)
continue;
const boost::shared_ptr<Module> src_module = src_port->module().lock();
const boost::shared_ptr<Module> 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<Item> m)
{
// Remove any connections that aren't selected anymore because this module isn't
boost::shared_ptr<Connection> c;
for (ConnectionList::iterator i = _selected_connections.begin(); i != _selected_connections.end() ; ) {
c = (*i);
if (boost::dynamic_pointer_cast<Item>(c->source().lock()) == m
|| boost::dynamic_pointer_cast<Item>(c->dest().lock()) == m) {
c->set_selected(false);
i = _selected_connections.erase(i);
continue;
}
boost::shared_ptr<Port> src_port = boost::dynamic_pointer_cast<Port>(c->source().lock());
if (src_port && src_port->module().lock() == m) {
c->set_selected(false);
i = _selected_connections.erase(i);
continue;
}
boost::shared_ptr<Port> dst_port = boost::dynamic_pointer_cast<Port>(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<boost::shared_ptr<Item> >::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<Port> 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<Port> 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<Port> 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<Module> 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<Port> old_last_selected = _last_selected_port;
boost::shared_ptr<Port> 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<Module> 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<Item> 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> item)
{
bool ret = false;
// Remove from selection
for (list<boost::shared_ptr<Item> >::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> module = boost::dynamic_pointer_cast<Module>(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<Connection> c;
for (ConnectionList::iterator i = _connections.begin(); i != _connections.end() ; ) {
c = (*i);
ConnectionList::iterator next = i;
++next;
const boost::shared_ptr<Port> src_port
= boost::dynamic_pointer_cast<Port>(c->source().lock());
const boost::shared_ptr<Port> dst_port
= boost::dynamic_pointer_cast<Port>(c->dest().lock());
if (boost::dynamic_pointer_cast<Item>(c->source().lock()) == item
|| boost::dynamic_pointer_cast<Item>(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<Connection>
Canvas::remove_connection(boost::shared_ptr<Connectable> item1,
boost::shared_ptr<Connectable> item2)
{
boost::shared_ptr<Connection> ret;
if (!_remove_objects)
return ret;
assert(item1);
assert(item2);
boost::shared_ptr<Connection> 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<const Connectable> tail,
boost::shared_ptr<const Connectable> head)
{
for (ConnectionList::const_iterator c = _connections.begin(); c != _connections.end(); ++c) {
const boost::shared_ptr<Connectable> src = (*c)->source().lock();
const boost::shared_ptr<Connectable> 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<Connection>
Canvas::get_connection(boost::shared_ptr<Connectable> tail,
boost::shared_ptr<Connectable> head) const
{
for (ConnectionList::const_iterator i = _connections.begin(); i != _connections.end(); ++i) {
const boost::shared_ptr<Connectable> src = (*i)->source().lock();
const boost::shared_ptr<Connectable> dst = (*i)->dest().lock();
if (src == tail && dst == head)
return *i;
}
return boost::shared_ptr<Connection>();
}
bool
Canvas::add_connection(boost::shared_ptr<Connectable> src,
boost::shared_ptr<Connectable> dst,
uint32_t color)
{
// Create (graphical) connection object
boost::shared_ptr<Connection> 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<Connection> c)
{
const boost::shared_ptr<Connectable> src = c->source().lock();
const boost::shared_ptr<Connectable> 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> 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<Connection> c = *i;
const boost::shared_ptr<Connectable> src = c->source().lock();
const boost::shared_ptr<Connectable> 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> 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<Port> > inputs;
vector< boost::shared_ptr<Port> > 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<Port> port1, boost::shared_ptr<Port> 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<Port> src_port;
boost::shared_ptr<Port> 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<Port> weak_port)
{
boost::shared_ptr<Port> 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> 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> 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<Item> 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<Module> drag_module;
static boost::shared_ptr<Port> drag_port;
static boost::shared_ptr<Connection> drag_connection;
static boost::shared_ptr<Port> 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<Module>(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<Port>(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<Connection>(new Connection(
shared_from_this(), _connect_port, drag_port,
_connect_port->color() + 0x22222200));
else
drag_connection = boost::shared_ptr<Connection>(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<Port> p = get_port_at(x, y);
if (drag_connection)
drag_connection->show();
if (p) {
boost::shared_ptr<Module> 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<Port> p = get_port_at(x, y);
if (drag_connection)
drag_connection->show();
if (p && p->is_input() != _connect_port->is_input()) {
boost::shared_ptr<Module> 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<Port> 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<Port>
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<Module> m
= boost::dynamic_pointer_cast<Module>(*i);
if (m && m->point_is_within(x, y))
return m->port_at(x, y);
}
return boost::shared_ptr<Port>();
}
#ifdef HAVE_AGRAPH
class GVNodes : public std::map<boost::shared_ptr<Item>, 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<boost::shared_ptr<Item>, 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<Module>(*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<Connection> c = *i;
boost::shared_ptr<Port> src_port = boost::dynamic_pointer_cast<Port>(c->source().lock());
boost::shared_ptr<Port> dst_port = boost::dynamic_pointer_cast<Port>(c->dest().lock());
boost::shared_ptr<Item> src_item = boost::dynamic_pointer_cast<Item>(c->source().lock());
boost::shared_ptr<Item> dst_item = boost::dynamic_pointer_cast<Item>(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<Item> 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