From b3e2037e6b04890c427d5b69fa1bc6e265410c87 Mon Sep 17 00:00:00 2001 From: Tomeu Vizoso Date: Tue, 20 Jan 2009 16:58:45 +0000 Subject: Refactor canvas stuff to another module --- diff --git a/canvas.py b/canvas.py new file mode 100644 index 0000000..d137f78 --- /dev/null +++ b/canvas.py @@ -0,0 +1,187 @@ +# 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 + +import gobject +import gtk + +from sugar import dispatch + +class Canvas(gtk.DrawingArea): + + __gtype_name__ = 'MindMapCanvas' + + def __init__(self): + self._elements = [] + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._last_clicked_element = None + self._dragging = False + + gobject.GObject.__init__(self) + + self.dragging_started = dispatch.Signal() + self.dragging_finished = dispatch.Signal() + + self.add_events(gtk.gdk.BUTTON_PRESS_MASK | + gtk.gdk.BUTTON_RELEASE_MASK | + gtk.gdk.POINTER_MOTION_MASK | + gtk.gdk.POINTER_MOTION_HINT_MASK) + + self.connect('expose-event', self.__expose_event_cb) + self.connect('button-press-event', self.__button_press_event_cb) + self.connect('button-release-event', self.__button_release_event_cb) + self.connect('motion-notify-event', self.__motion_notify_event_cb) + + def __expose_event_cb(self, widget, event): + context = self.window.cairo_create() + + rect = self.get_allocation() + rect.x = 0 + rect.y = 0 + + 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 element in self._elements: + rect = element.get_rect() + if rect.intersect(event.area): + context.save() + + context.rectangle(rect.x, rect.y, rect.width, rect.height) + context.clip() + + element.draw(context) + + context.restore() + + return False + + def get_element_at(self, x, y): + for element in self._elements: + rect = element.get_rect() + if (x >= rect.x and x <= rect.x + rect.width) and \ + (y >= rect.y and y <= rect.y + rect.height): + return element + return None + + def _invalidate(self, x, y, width, height): + if self.window is None: + return + rect = gtk.gdk.Rectangle(x, y, width, height) + self.window.invalidate_rect(rect, True) + self.window.process_updates(True) + + def add_element(self, element): + element.invalidated.connect(self.__element_invalidated_cb) + self._elements.append(element) + rect = element.get_rect() + self._invalidate(rect.x, rect.y, rect.width, rect.height) + + def remove_all_elements(self): + rect = gtk.gdk.Rectangle(0, 0, 0, 0) + for element in self._elements: + rect.union(element.get_rect()) + + self._invalidate(rect.x, rect.y, rect.width, rect.height) + + self._elements = [] + + def __element_invalidated_cb(self, **kwargs): + element = kwargs['sender'] + + rect = element.previous_rect + self._invalidate(rect.x, rect.y, rect.width, rect.height) + + rect = element.get_rect() + self._invalidate(rect.x, rect.y, rect.width, rect.height) + + 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_element = self.get_element_at(event.x, event.y) + logging.debug('button_press_event_cb %r' % self._last_clicked_element) + if self._last_clicked_element is not None: + self._pressed_button = event.button + self._press_start_x = event.x + self._press_start_y = event.y + + return False + + def __button_release_event_cb(self, widget, event): + logging.debug('button_release_event_cb') + if self._dragging: + self.dragging_finished.send(self, position=(event.x, event.y), + element=self._last_clicked_element) + self._cleanup_drag() + + return False + + def _cleanup_drag(self): + self._last_clicked_element = None + self._pressed_button = None + self._press_start_x = None + self._press_start_y = None + self._dragging = False + + def __motion_notify_event_cb(self, widget, event): + if self._dragging or self._last_clicked_element is None: + return True + + # if the mouse button is not pressed, no drag should occurr + if not event.state & gtk.gdk.BUTTON1_MASK: + self._cleanup_drag() + 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 self.drag_check_threshold(int(self._press_start_x), + int(self._press_start_y), + int(x), + int(y)): + self._dragging = True + self.dragging_started.send(self, position=(x, y), + element=self._last_clicked_element) + + return True + +class CanvasElement(object): + def __init__(self): + self.invalidated = dispatch.Signal() + self.previous_rect = (0, 0, 0, 0) + + def draw(self, context): + self.previous_rect = self.get_rect() + + def invalidate(self): + self.invalidated.send(self) + + def get_rect(self): + pass + diff --git a/view.py b/view.py index b7b1914..66e29c7 100644 --- a/view.py +++ b/view.py @@ -23,41 +23,38 @@ import gtk # We'd rather not depend on sugar in this class from sugar.graphics import style -class MindMapView(gtk.DrawingArea): - +from canvas import Canvas, CanvasElement + +class MindMapView(Canvas): + + __gtype_name__ = 'MindMapView' + 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) + Canvas.__init__(self) - self.connect('expose-event', self.__expose_event_cb) + self.dragging_started.connect(self.__dragging_started_cb) + self.dragging_finished.connect(self.__dragging_finished_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 __dragging_started_cb(self, **kwargs): + x, y = kwargs['position'] + logging.debug('__dragging_started_cb %r %r' % (x, y)) + thought = kwargs['element'] + thought.dragging = True + + def __dragging_finished_cb(self, **kwargs): + x, y = kwargs['position'] + thought = kwargs['element'] + logging.debug('__dragging_finished_cb %r %r' % (x, y)) + thought.dragging = False + thought.model.x = x + thought.model.y = y def set_model(self, new_model): logging.debug('set_model %r' % new_model) @@ -65,204 +62,132 @@ class MindMapView(gtk.DrawingArea): 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 = [] + self.remove_all_elements() for thought_model in self._model.get_thoughts(): though_view = ThoughtView(thought_model) - self._thought_views.append(though_view) + self.add_element(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() + self.add_element(though_view) def __row_deleted_cb(self, model, path): logging.debug('__row_deleted_cb %r' % path) - for thought_view in self._thought_views: + though_view = self._get_though_by_id(path[0]) + self.remove_element(though_view) + + def _get_though_by_id(self, thought_id): + for thought_view in self.get_elements(): if thought_view.model.id == path[0]: - self._thought_views.remove(thought_view) - break + return thought_view + return None - self._invalidate_whole_area() +class ThoughtView(CanvasElement): - # 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 + _PADDING = style.zoom(10) - logging.debug('motion_notify_event_cb') + def __init__(self, model): + CanvasElement.__init__(self) - 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 + self.model = model + self._last_width = -1 + self._last_height = -1 + self._dragging = False + + self.model.changed.connect(self.__model_changed_cb) - 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 + def set_dragging(self, dragging): + if self._dragging != dragging: + self._dragging = dragging + self.invalidate() -class ThoughtView(object): + def get_dragging(self): + return self._dragging - _PADDING = style.zoom(10) + dragging = property(get_dragging, set_dragging) - def __init__(self, model): - self.model = model - self._last_width = 0 - self._last_height = 0 + def _get_name_layout(self, context=None): + if context is None: + visual = gtk.gdk.screen_get_default().get_system_visual() + pixmap = gtk.gdk.Pixmap(None, 1, 1, visual.depth) + context = pixmap.cairo_create() - 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) + return layout + + def draw(self, context): + if self._dragging: + context.set_source_rgb(0.8, 0.8, 0.8) + else: + context.set_source_rgb(0, 0, 0) + + context.save() + layout = self._get_name_layout(context) + width, height = layout.get_pixel_size() + width += self._PADDING * 2 + height += self._PADDING * 2 - x, y = self.model.x + self._PADDING, self.model.y + self._PADDING + x = self.model.x + self._PADDING + y = 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.set_line_width(4) + context.rectangle(self.model.x, self.model.y, width, height) context.stroke() context.restore() self._last_width = width self._last_height = height + + CanvasElement.draw(self, context) - def overlaps(self, rect): - return True - - def get_size(self, context=None): + def get_size(self): return self._last_width, self._last_height + def __model_changed_cb(self, **kwargs): + self.x = self.model.x + self.y = self.model.y + self.invalidate() + + def get_rect(self): + if -1 in (self._last_width, self._last_height): + layout = self._get_name_layout() + width, height = layout.get_pixel_size() + self._last_width = width + self._PADDING * 2 + self._last_height = height + self._PADDING * 2 + + return gtk.gdk.Rectangle(self.model.x, self.model.y, + self._last_width, self._last_height) + -- cgit v0.9.1