diff options
Diffstat (limited to 'view.py')
-rw-r--r-- | view.py | 268 |
1 files changed, 268 insertions, 0 deletions
@@ -0,0 +1,268 @@ +# 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 + +# We'd rather not depend on sugar in this class +from sugar.graphics import style + +class MindMapView(gtk.DrawingArea): + + def __init__(self, model=None): + self._model = None + self._row_changed_sid = None + self._row_deleted_sid = None + self._row_inserted_sid = None + self._thought_views = None + + gobject.GObject.__init__(self) + + self.connect('expose-event', self.__expose_event_cb) + + if model is not None: + self.model = model + + # DND stuff + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._last_clicked_thought = None + self.drag_source_set(0, [], 0) + self.add_events(gtk.gdk.BUTTON_PRESS_MASK | + gtk.gdk.POINTER_MOTION_HINT_MASK) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + self.connect('button-press-event', self.__button_press_event_cb) + self.connect('drag-end', self.__drag_end_cb) + self.connect('drag-data-get', self.__drag_data_get_cb) + self.connect('drag-data-received', self.__drag_data_received_cb) + + self.drag_dest_set(gtk.DEST_DEFAULT_MOTION | + gtk.DEST_DEFAULT_HIGHLIGHT | + gtk.DEST_DEFAULT_DROP, + [('application/x-thought', 0, 0)], + gtk.gdk.ACTION_MOVE) + + 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.disconnect(self._row_inserted_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) + self._row_inserted_sid = \ + self._model.connect('row-inserted', self.__row_inserted_cb) + + self._thought_views = [] + for thought_model in self._model.get_thoughts(): + though_view = ThoughtView(thought_model) + self._thought_views.append(though_view) + + def get_model(self): + return self._model + + model = property(get_model, set_model) + + def __expose_event_cb(self, widget, event): + logging.debug('expose %r' % self._model) + if self._model is None or not self._model: + return + + context = self.window.cairo_create() + + rect = self.get_allocation() + rect.x = 0 + rect.y = 0 + logging.debug('do_expose_event %r' % ((rect.x, rect.y, rect.width, rect.height),)) + + context.rectangle(event.area.x, event.area.y, + event.area.width, event.area.height) + context.clip() + + context.set_source_rgb(1.0, 1.0, 1.0) + context.paint() + + for thought_view in self._thought_views: + if thought_view.overlaps(event.area): + context.save() + thought_view.draw(context) + context.restore() + + return False + + def _invalidate_whole_area(self): + alloc = self.get_allocation() + rect = gtk.gdk.Rectangle(0, 0, alloc.width, alloc.height) + self.window.invalidate_rect(rect, True) + self.window.process_updates(True) + + def __row_changed_cb(self, model, path, iter): + logging.debug('__row_changed_cb %r' % path) + self._invalidate_whole_area() + + def __row_inserted_cb(self, model, path, iter): + logging.debug('__row_inserted_cb %r' % path) + + thought_model = model.get_thought(path[0]) + though_view = ThoughtView(thought_model) + self._thought_views.append(though_view) + + self._invalidate_whole_area() + + def __row_deleted_cb(self, model, path): + logging.debug('__row_deleted_cb %r' % path) + + for thought_view in self._thought_views: + if thought_view.model.id == path[0]: + self._thought_views.remove(thought_view) + break + + self._invalidate_whole_area() + + # DND methods + def __motion_notify_event_cb(self, widget, event): + if not self._pressed_button: + return True + + # if the mouse button is not pressed, no drag should occurr + if not event.state & gtk.gdk.BUTTON1_MASK: + self._pressed_button = None + return True + + logging.debug('motion_notify_event_cb') + + if event.is_hint: + x, y, state_ = event.window.get_pointer() + else: + x = event.x + y = event.y + + if widget.drag_check_threshold(int(self._press_start_x), + int(self._press_start_y), + int(x), + int(y)): + context_ = widget.drag_begin([('application/x-thought', 0, 0)], + gtk.gdk.ACTION_MOVE, + 1, + event) + return True + + def __drag_end_cb(self, widget, drag_context): + logging.debug('drag_end_cb') + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._last_clicked_thought = None + + def __drag_data_get_cb(self, widget, context, selection, target_type, + event_time): + logging.debug('drag_data_get_cb: requested target ' + selection.target) + + thought_id = self._last_clicked_thought.model.id + selection.set(selection.target, 8, str(thought_id)) + + def __button_press_event_cb(self, widget, event): + logging.debug('button_press_event_cb') + + if event.button == 1 and event.type == gtk.gdk.BUTTON_PRESS: + self._last_clicked_thought = \ + self._get_thought_at_coords(event.x, event.y) + if self._last_clicked_thought is not None: + self._pressed_button = event.button + self._press_start_x = event.x + self._press_start_y = event.y + + return False + + def _get_thought_at_coords(self, x, y): + for thought in self._thought_views: + thought_x = thought.model.x + thought_y = thought.model.y + width, height = thought.get_size() + + if x >= thought_x and (x <= thought_x + width) and \ + y >= thought_y and (y <= thought_y + height): + return thought + return None + + def __drag_data_received_cb(self, widget, drag_context, x, y, + selection_data, info, time): + thought_id = int(selection_data.data) + for thought in self._thought_views: + if thought.model.id == thought_id: + thought.model.x = x + thought.model.y = y + break + +class ThoughtView(object): + + _PADDING = style.zoom(10) + + def __init__(self, model): + self.model = model + self._last_width = 0 + self._last_height = 0 + + def draw(self, context): + if self.model.name is None or not self.model.name: + name = _('Unnamed') + else: + name = self.model.name + + context.save() + context.set_source_rgb(0, 0, 0) + layout = context.create_layout() + layout.set_text(name) + + width, height = layout.get_pixel_size() + + x, y = self.model.x + self._PADDING, self.model.y + self._PADDING + context.translate(x, y) + context.show_layout(layout) + context.restore() + + width += self._PADDING * 2 + height += self._PADDING * 2 + + context.save() + context.set_line_width(1) + context.set_source_rgb(0.2, 0.2, 0.2) + context.rectangle(self.model.x, self.model.y, width, height) + context.stroke() + context.restore() + + self._last_width = width + self._last_height = height + + def overlaps(self, rect): + return True + + def get_size(self, context=None): + return self._last_width, self._last_height + |