# Copyright (C) 2009, Tomeu Vizoso # # This program 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. # # This program 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 more 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 import logging from gettext import gettext as _ import gobject import gtk from gaphas import GtkView, Canvas, Line, geometry from gaphas import tool # We'd rather not depend on sugar in this class from sugar.graphics import style from thoughtview import ThoughtView class MindMapView(GtkView): __gtype_name__ = 'MindMapView' def __init__(self, model=None): self._model = None self._row_changed_sid = None self._row_deleted_sid = None GtkView.__init__(self) self.canvas = Canvas() self.tool = tool.ToolChain(). \ append(NewThoughtTool()). \ append(tool.HoverTool()). \ append(ItemTool()). \ append(tool.TextEditTool()). \ append(tool.RubberbandTool()) if model is not None: self.model = model def set_model(self, new_model): logging.debug('set_model %r' % new_model) if self._model == new_model: return if self._model is not None: self._model.disconnect(self._row_changed_sid) self._model.disconnect(self._row_deleted_sid) self._model = new_model if self._model is not None: self._row_changed_sid = \ self._model.connect('row-changed', self.__row_changed_cb) self._row_deleted_sid = \ self._model.connect('row-deleted', self.__row_deleted_cb) for item in self.canvas.get_all_items(): self.canvas.remove(item) self._populate_from_model(self._model) def _populate_from_model(self, rows): for row in rows: thought_view = ThoughtView(row[0], row[1], row[2], row[3], row[4]) self._populate_from_model(row.iterchildren()) self.add_element(thought_view) def get_model(self): return self._model model = property(get_model, set_model) def _calculate_ports(self, child_view, parent_view): child_rect = child_view.get_rectangle() parent_rect = parent_view.get_rectangle() point_in_child = self._get_closest_intersection(child_rect, parent_rect) point_in_parent = self._get_closest_intersection(parent_rect, child_rect) return point_in_child, point_in_parent def _get_closest_intersection(self, rect1, rect2): center1 = rect1.x + rect1.width / 2, rect1.y + rect1.height / 2 center2 = rect2.x + rect2.width / 2, rect2.y + rect2.height / 2 return self._intersection_rect_line(rect1, (center1, center2)) def _intersection_rect_line(self, rect, line): top_edge = (rect.x, rect.y), (rect.x + rect.width, rect.y) left_edge = (rect.x, rect.y), (rect.x, rect.y + rect.height) bottom_edge = (rect.x, rect.y + rect.height), \ (rect.x + rect.width, rect.y + rect.height) right_edge = (rect.x + rect.width, rect.y + rect.height), \ (rect.x + rect.width, rect.y) for edge in (top_edge, left_edge, bottom_edge, right_edge): point = geometry.intersect_line_line(line[0], line[1], edge[0], edge[1]) if point is not None: return point raise ValueError('Rect %r and line %r dont intersect!' % (rect, line,)) def _update_connection(self, parent_view, thought_view): handle_tool = tool.ConnectHandleTool() if thought_view.line_to_parent is None: line = Line() self.canvas.add(line) thought_view.line_to_parent = line else: line = thought_view.line_to_parent line_handle = line.handles()[0] handle_tool.disconnect(self, line, line_handle) handle_tool.disconnect(self, line, line.opposite(line_handle)) start, end = self._calculate_ports(thought_view, parent_view) line_handle = line.handles()[0] handle_tool.connect(self, line, line_handle, start) handle_tool.connect(self, line, line.opposite(line_handle), end) self.canvas.request_update(line) def __row_changed_cb(self, model, path, iter): row = model[iter] #logging.debug('__row_changed_cb %r' % ((row[0], row[1], row[2], row[3], row[4],),)) thought_view = self._get_thought_by_id(row[0]) if thought_view is None: thought_view = ThoughtView(row[0], row[1], row[2], row[3], row[4]) self.canvas.add(thought_view) else: thought_view.name = row[1] thought_view.set_position(row[2], row[3]) thought_view.color = row[4] gobject.idle_add(self.__update_connections_cb, thought_view, row) def __update_connections_cb(self, thought_view, row): if row.parent is not None: parent_view = self._get_thought_by_id(row.parent[0]) self._update_connection(parent_view, thought_view) for child in row.iterchildren(): child_view = self._get_thought_by_id(child[0]) self._update_connection(thought_view, child_view) def __row_deleted_cb(self, model, path): logging.debug('__row_deleted_cb %r' % path) raise NotImplementedError() thought_view = self._get_thought_by_id(path[0]) self.remove_element(thought_view) def _get_thought_by_id(self, thought_id): for item in self.canvas.get_all_items(): if isinstance(item, ThoughtView) and item.id == thought_id: return item return None class NewThoughtTool(tool.HandleTool): def __init__(self): tool.HandleTool.__init__(self) self._new_connection = None self._parent_thought = None def grab_handle(self, item, handle): logging.debug('NewThoughtTool.grab_handle %r %r' % (item, handle)) tool.HandleTool.grab_handle(self, item, handle) def ungrab_handle(self): logging.debug('NewThoughtTool.ungrab_handle') tool.HandleTool.ungrab_handle(self) def on_button_release(self, context, event): logging.debug('NewThoughtTool.on_button_release') if self._new_connection is not None: context.view.canvas.remove(self._new_connection) self._new_connection = None context.view.model.create_new_thought(x=event.x, y=event.y, parent_id=self._parent_thought.id) tool.HandleTool.on_button_release(self, context, event) def move(self, view, item, handle, pos): #logging.debug('NewThoughtTool.move') if isinstance(item, ThoughtView) and handle == item.new_thought_handle: matrix = view.canvas.get_matrix_i2c(item) offset = matrix[4], matrix[5] if self._new_connection is None: self._new_connection = Line() view.canvas.add(self._new_connection) start_handle = self._new_connection.handles()[0] start_handle.x = item.handles()[4].pos[0] + offset[0] start_handle.y = item.handles()[4].pos[1] + offset[1] #logging.debug('created line starting at %r %r' % item.handles()[4].pos) self._parent_thought = item end_handle = self._new_connection.handles()[1] end_handle.x = pos[0] end_handle.y = pos[1] view.canvas.request_update(self._new_connection) #logging.debug('moved line end to %r %r' % (end_handle.x, end_handle.y)) else: tool.HandleTool.move(self, view, item, handle, pos) def glue(self, view, item, handle, vpos): #logging.debug('NewThoughtTool.glue') tool.HandleTool.glue(self, view, item, handle, vpos) def connect(self, view, item, handle, vpos): logging.debug('NewThoughtTool.connect') tool.HandleTool.connect(self, view, item, handle, vpos) def disconnect(self, view, item, handle): logging.debug('NewThoughtTool.disconnect') tool.HandleTool.disconnect(self, view, item, handle) class ItemTool(tool.ItemTool): def __init__(self): tool.ItemTool.__init__(self) def on_button_release(self, context, event): logging.debug('ItemTool.on_button_release') for item in self._movable_items: if not isinstance(item, ThoughtView): continue x, y = item.get_position() row = context.view.model.find_by_id(item.id) if row[2] != x or row[3] != y: context.view.model.set(row.iter, 2, x, 3, y) tool.ItemTool.on_button_release(self, context, event)