# 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 """ from gettext import gettext as _ from sugar.tutorius import gtkutils from dialog import TutoriusDialog import overlayer from sugar.tutorius.editor import WidgetIdentifier from sugar.tutorius.services import ObjectStore from sugar.tutorius.properties import * import gtk.gdk class DragWrapper(object): """Wrapper to allow gtk widgets to be dragged around""" def __init__(self, widget, position, 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 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._dragging = False # whether a drag is in progress self.position = position # position of the widget self.draggable = draggable def _pressed_cb(self, widget, evt): """Callback for start of drag event""" self._eventbox.grab_add() self._dragging = True self._rel_pos = evt.get_coords() def _moved_cb(self, widget, evt): """Callback for mouse drag events""" if not self._dragging: return # Focus on a widget before dragging another would # create addititonal move event, making the widget jump unexpectedly. # Solution found was to process those focus events before dragging. if gtk.events_pending(): return xrel, yrel = self._rel_pos xparent, yparent = evt.get_coords() xparent, yparent = widget.translate_coordinates(widget.parent, xparent, yparent) self.position = (xparent-xrel, yparent-yrel) self._widget.parent.move(self._eventbox, *self.position) self._widget.parent.move(self._widget, *self.position) self._widget.parent.queue_draw() def _released_cb(self, *args): """Callback for end of drag (mouse release).""" self._eventbox.grab_remove() self._dragging = False def _drag_end(self, *args): """Callback for end of drag (stolen focus).""" self._dragging = False 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) self._handles.append(self._eventbox.connect( "button-press-event", self._pressed_cb)) self._handles.append(self._eventbox.connect( "button-release-event", self._released_cb)) self._handles.append(self._eventbox.connect( "motion-notify-event", self._moved_cb)) self._handles.append(self._eventbox.connect( "grab-broken-event", 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(object): """Base class for Actions""" def __init__(self): object.__init__(self) self.properties = 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 get_properties(self): """ Fills self.property with a dict of TutoriusProperty and return the list of property names. get_properties has to be called before accessing self.property """ if self.properties is None: self.properties = {} for i in dir(self): if isinstance(getattr(self,i), TutoriusProperty): self.properties[i] = getattr(self,i) return self.properties.keys() def enter_editmode(self, **kwargs): """ Enters edit mode. The action should display itself in some way, without affecting the currently running application. """ raise NotImplementedError("Not implemented") class OnceWrapper(object): """ Wraps a class to perform an action once only This ConcreteActions's do() method will only be called on the first do() and the undo() will be callable after do() has been called """ def __init__(self, action): self._action = action self._called = False self._need_undo = False def do(self): """ Do the action only on the first time """ if not self._called: self._called = True self._action.do() self._need_undo = True def undo(self): """ Undo the action if it's been done """ if self._need_undo: self._action.undo() self._need_undo = False def get_properties(self): return self._action.get_properties() class DialogMessage(Action): """ Shows a dialog with a given text, at the given position on the screen. @param message A string to display to the user @param pos A list of the form [x, y] """ def __init__(self, message, pos=None): super(DialogMessage, self).__init__() self._dialog = None self.message = TStringProperty(message) self.position = TArrayProperty(pos or [0, 0], 2, 2) def do(self): """ Show the dialog """ self._dialog = TutoriusDialog(self.message.value) self._dialog.set_button_clicked_cb(self._dialog.close_self) self._dialog.set_modal(False) self._dialog.move(self.position.value[0], self.position.value[1]) self._dialog.show() def undo(self): """ Destroy the dialog """ if self._dialog: self._dialog.destroy() self._dialog = None class BubbleMessage(Action): """ Shows a dialog with a given text, at the given position on the screen. @param message A string to display to the user @param pos A list of the form [x, y] @param speaker treeish representation of the speaking widget @param tailpos The position of the tail of the bubble; useful to point to specific elements of the interface """ def __init__(self, message, pos=None, speaker=None, tailpos=None): Action.__init__(self) self.message = TStringProperty(message) # Create the position as an array of fixed-size 2 self.position = TArrayProperty(pos or [0,0], 2, 2) # Do the same for the tail position self.tail_pos = TArrayProperty(tailpos or [0,0], 2, 2) self.overlay = None self._bubble = None self._speaker = None self.__drag = None def do(self): """ Show the dialog """ # get or inject overlayer self.overlay = ObjectStore().activity._overlayer # FIXME: subwindows, are left to overlap this. This behaviour is # undesirable. subwindows (i.e. child of top level windows) should be # handled either by rendering over them, or by finding different way to # draw the overlay. if not self.overlay: self.overlay = ObjectStore().activity._overlayer if not self._bubble: x, y = self.position.value # TODO: tails are relative to tailpos. They should be relative to # the speaking widget. Same of the bubble position. self._bubble = overlayer.TextBubble(text=self.message.value, tailpos=self.tail_pos.value) self._bubble.show() self.overlay.put(self._bubble, x, y) self.overlay.queue_draw() def undo(self): """ Destroy the dialog """ if self._bubble: self._bubble.destroy() self._bubble = None def enter_editmode(self, *args): """ Enters edit mode. The action should display itself in some way, without affecting the currently running application. """ if not self.overlay: self.overlay = ObjectStore().activity._overlayer assert not self.__drag, "bubble action set to editmode twice" x, y = self.position.value self._bubble = overlayer.TextBubble(text=self.message.value, tailpos=self.tail_pos.value) self.overlay.put(self._bubble, x, y) self._bubble.show() self.__drag = DragWrapper(self._bubble, self.position.value, True) def exit_editmode(self, *args): x,y = self.__drag.position self.position.set([int(x), int(y)]) if self.__drag: self.__drag.draggable = False self.__drag = None if self._bubble: self.overlay.remove(self._bubble) self._bubble = None self.overlay = None class WidgetIdentifyAction(Action): def __init__(self): self.activity = None self._dialog = None def do(self): os = ObjectStore() if os.activity: self.activity = os.activity self._dialog = WidgetIdentifier(self.activity) self._dialog.show() def undo(self): if self._dialog: self._dialog.destroy() class ChainAction(Action): """Utility class to allow executing actions in a specific order""" def __init__(self, *actions): """ChainAction(action1, ... ) builds a chain of actions""" self._actions = actions def do(self,**kwargs): """do() each action in the chain""" for act in self._actions: act.do(**kwargs) def undo(self): """undo() each action in the chain, starting with the last""" for act in reversed(self._actions): act.undo() class DisableWidgetAction(Action): def __init__(self, target): """Constructor @param target target treeish """ Action.__init__(self) self._target = target self._widget = None def do(self): """Action do""" os = ObjectStore() if os.activity: self._widget = gtkutils.find_widget(os.activity, self._target) if self._widget: self._widget.set_sensitive(False) def undo(self): """Action undo""" if self._widget: self._widget.set_sensitive(True) class TypeTextAction(Action): """ Simulate a user typing text in a widget Work on any widget that implements a insert_text method @param widget The treehish representation of the widget @param text the text that is typed """ def __init__(self, widget, text): Action.__init__(self) self._widget = widget self._text = text def do(self, **kwargs): """ Type the text """ widget = gtkutils.find_widget(ObjectStore().activity, self._widget) if hasattr(widget, "insert_text"): widget.insert_text(self._text, -1) def undo(self): """ no undo """ pass class ClickAction(Action): """ Action that simulate a click on a widget Work on any widget that implements a clicked() method @param widget The threehish representation of the widget """ def __init__(self, widget): Action.__init__(self) self._widget = widget def do(self): """ click the widget """ widget = gtkutils.find_widget(ObjectStore().activity, self._widget) if hasattr(widget, "clicked"): widget.clicked() def undo(self): """ No undo """ pass