# Copyright (C) 2009, Tutorius.org # # 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 """ This module defines Actions that can be done and undone on a state """ import gtk import logging from gettext import gettext as _ from sugar.graphics import icon from . import addon from .services import ObjectStore from .properties import * from .constants import * import pickle import logging LOGGER = logging.getLogger("actions") class DragWrapper(object): """Wrapper to allow gtk widgets to be dragged around""" fromImage = [ ( WIDGET_ID, 0, TARGET_TYPE_WIDGET ) ] LOGGER = logging.getLogger("sugar.tutorius.actions.DragWrapper") def __init__(self, widget, position, update_action_cb, draggable=False): """ Creates a wrapper to allow gtk widgets to be mouse dragged, if the parent container supports the move() method, like a gtk.Layout. @param widget the widget to enhance with drag capability @param position the widget's position. Will translate the widget if needed @param update_action_cb The callback to trigger @param draggable wether to enable the drag functionality now """ self._widget = widget self._eventbox = None self._drag_on = False # whether dragging is enabled self._rel_pos = (0,0) # mouse pos relative to widget self._handles = [] # event handlers self.position = position # position of the widget self.moved = False self.update_action_cb = update_action_cb self.draggable = draggable def _drag_begin(self, widget, drag_context, *args): """Callback for initialisation of drag and drop""" # Setup the drag icon to what the widget is rendering self.LOGGER.debug("%s drag-begin"%(str(widget))) width = self._widget.allocation.width height = self._widget.allocation.height x = self._widget.allocation.x y = self._widget.allocation.y depth = 24 # Should be set dynamically pxbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height) pxbuf.fill(0xFFFFFFFF) px = gtk.gdk.Pixmap(None, width, height, depth) # source, size, colors px.set_colormap(gtk.gdk.colormap_get_system()) ctxt = px.cairo_create() ctxt.set_source_pixbuf(pxbuf,0,0) ctxt.paint() # Compensate when drawing the icon for the context # translation done to the position occupied by the widget ctxt.translate(-x, -y) self._widget.draw_with_context(ctxt) pxbuf.get_from_drawable(px,gtk.gdk.colormap_get_system(), 0, 0, 0, 0, -1, -1) drag_context.set_icon_pixbuf(pxbuf,0,0) def _drag_fail(self, *args): """ Callback to prevent the animation that brings back the dragged item to its initial position. """ LOGGER.debug("DragWrapper :: Drag fail event was raised") return True def _drag_end(self, widget, context, *args): """Callback for end of drag (stolen focus).""" xrel, yrel = self._rel_pos rootwin = widget.get_screen().get_root_window() xparent,yparent,mods = rootwin.get_pointer() self.position = (int(xparent-xrel), int(yparent-yrel)) self.LOGGER.debug("%s drag-end pos: %s"%(str(widget),str(self.position))) self._widget.parent.move(self._eventbox, *self.position) self._widget.parent.move(self._widget, *self.position) self._widget.parent.queue_draw() self.update_action_cb('position', self.position) def set_draggable(self, value): """Setter for the draggable property""" if bool(value) ^ bool(self._drag_on): if value: self._eventbox = gtk.EventBox() self._eventbox.show() self._eventbox.set_visible_window(False) size = self._widget.size_request() self._eventbox.set_size_request(*size) self._widget.parent.put(self._eventbox, *self.position) # Prepare the widget for drag and drop self._eventbox.drag_source_set(gtk.gdk.BUTTON1_MASK, self.fromImage, gtk.gdk.ACTION_MOVE) self._handles.append(self._eventbox.connect( "drag-begin", self._drag_begin)) self._handles.append(self._eventbox.connect( "drag-failed", self._drag_fail)) self._handles.append(self._eventbox.connect( "drag-end", self._drag_end)) else: while len(self._handles): handle = self._handles.pop() self._eventbox.disconnect(handle) self._eventbox.parent.remove(self._eventbox) self._eventbox.destroy() self._eventbox = None self._drag_on = value def get_draggable(self): """Getter for the draggable property""" return self._drag_on draggable = property(fset=set_draggable, fget=get_draggable, \ doc="Property to enable the draggable behaviour of the widget") def set_widget(self, widget): """Setter for the widget property""" if self._dragging or self._drag_on: raise Exception("Can't change widget while dragging is enabled.") assert hasattr(widget, "parent"), "wrapped widget should have a parent" parent = widget.parent assert hasattr(parent, "move"), "container of widget need move method" self._widget = widget def get_widget(self): """Getter for the widget property""" return self._widget widget = property(fset=set_widget, fget=get_widget) class Action(TPropContainer): """Base class for Actions""" def __init__(self): TPropContainer.__init__(self) self.position = (0,0) self._drag = None # The callback that will be triggered when the action is requested # to notify all its changes self._properties_updated_cb = None def do(self, **kwargs): """ Perform the action """ raise NotImplementedError("Not implemented") def undo(self): """ Revert anything the action has changed """ pass #Should raise NotImplemented? def set_notification_cb(self, notif_cb): LOGGER.debug("Action :: Setting notification callback for creator...") self._properties_updated_cb = notif_cb def update_property(self, name, value): """ Callback used in the wrapper to send a new value to an action. """ LOGGER.debug("Action :: update_property on %s with value '%s'"%(name, str(value))) # Set the property itself - this will modify the diff dict and we will # be able to notify the owner with the new value self.__setattr__(name, value) # Send the notification to the creator self.notify() def notify(self): LOGGER.debug("Action :: Notifying creator with new values in dict : %s"%(str(self._diff_dict))) # If a notification callback was registered if self._properties_updated_cb: # Propagate it self._properties_updated_cb(self._diff_dict) # Empty the diff dict as we just synchronized with the creator self._diff_dict.clear() def enter_editmode(self, **kwargs): """ Enters edit mode. The action should display itself in some way, without affecting the currently running application. The default is a small box with the action icon. """ meta = addon.get_addon_meta(type(self).__name__) actionicon = icon.Icon(icon_name=meta['icon'], icon_size=gtk.ICON_SIZE_LARGE_TOOLBAR) # Eventbox create a visible window for the icon, so it clips correctly self.__edit_img = gtk.EventBox() self.__edit_img.set_visible_window(True) self.__edit_img.add(actionicon) x, y = self.position ObjectStore().activity._overlayer.put(self.__edit_img, x, y) self.__edit_img.show_all() self._drag = DragWrapper(self.__edit_img, self.position, update_action_cb=self.update_property, draggable=True) def exit_editmode(self, **kwargs): x, y = self._drag.position self.position = [int(x), int(y)] self.__edit_img.destroy()