# 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): GtkView.__init__(self) self.canvas = Canvas() self.tool = tool.ToolChain(). \ append(NewThoughtTool()). \ append(tool.HoverTool()). \ append(ItemTool()). \ append(TextEditTool(model)). \ append(tool.RubberbandTool()) self.model = model self.model.connect('row-changed', self.__row_changed_cb) self.model.connect('row-deleted', self.__row_deleted_cb) 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 _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 = Connection() 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) class Connection(Line): def __init__(self): super(Connection, self).__init__() def draw_head(self, context): cr = context.cairo cr.move_to(2, 0) cr.line_to(12, 10) cr.stroke() cr.move_to(2, 0) cr.line_to(12, -10) cr.stroke() super(Connection, self).draw_head(context) def draw(self, context): cr = context.cairo cr.set_source_rgb(0, 0, .8) super(Connection, self).draw(context) class TextEditTool(tool.Tool): def __init__(self, model): super(tool.Tool, self).__init__() self._clicked_item = None self._model = model def on_double_click(self, context, event): view = context.view self._clicked_item = view.get_item_at_point((event.x, event.y)) if not isinstance(self._clicked_item, ThoughtView): return False window = gtk.Window() window.set_parent_window(context.view.window) window.size_allocate(gtk.gdk.Rectangle(int(event.x), int(event.y), 50, 50)) x, y = self._clicked_item.get_position() origin_x, origin_y = context.view.window.get_origin() window.move(x + origin_x, y + origin_y) window.connect('focus-out-event', self.__focus_out_event_cb) window.connect('realize', self.__realize_cb) text_view = gtk.TextView() buf = text_view.get_buffer() buf.set_text(self._clicked_item.name) buf.select_range(buf.get_start_iter(), buf.get_end_iter()) text_view.connect('key-press-event', self.__key_press_event_cb) text_view.show() window.add(text_view) window.show() return True def __realize_cb(self, widget): widget.set_decorated(False) widget.set_resize_mode(gtk.RESIZE_IMMEDIATE) widget.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) def __key_press_event_cb(self, widget, event): if event.keyval == gtk.keysyms.Return: buf = widget.get_buffer() name = buf.get_text(buf.get_start_iter(), buf.get_end_iter()) row = self._model.find_by_id(self._clicked_item.id) self._model.set(row.iter, 1, name) widget.get_toplevel().destroy() return True elif event.keyval == gtk.keysyms.Escape: widget.get_toplevel().destroy() return True else: return False def __focus_out_event_cb(self, widget, event): widget.destroy()