From 49ab39870266196f6267f83dd6951f839ebde773 Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 04 Dec 2009 02:21:14 +0000 Subject: Merge ../../simpoirs-clone into remote_integration Conflicts: src/extensions/tutoriusremote.py --- diff --git a/addons/bubblemessage.py b/addons/bubblemessage.py index aaf086c..7e91d00 100644 --- a/addons/bubblemessage.py +++ b/addons/bubblemessage.py @@ -93,7 +93,7 @@ class BubbleMessage(Action): self.overlay.put(self._bubble, x, y) self._bubble.show() - self._drag = DragWrapper(self._bubble, self.position, True) + self._drag = DragWrapper(self._bubble, self.position, update_action_cb=self.update_property, draggable=True) def exit_editmode(self, *args): if self._drag.moved: diff --git a/addons/bubblemessagewimg.py b/addons/bubblemessagewimg.py index 9c3dfc1..0ad444f 100644 --- a/addons/bubblemessagewimg.py +++ b/addons/bubblemessagewimg.py @@ -96,7 +96,7 @@ class BubbleMessageWImg(Action): self.overlay.put(self._bubble, x, y) self._bubble.show() - self._drag = DragWrapper(self._bubble, self.position, True) + self._drag = DragWrapper(self._bubble, self.position, update_action_cb=self.update_property, draggable=True) def exit_editmode(self, *args): x,y = self._drag.position diff --git a/addons/gtkwidgeteventfilter.py b/addons/gtkwidgeteventfilter.py index b5ce9ae..ac14399 100644 --- a/addons/gtkwidgeteventfilter.py +++ b/addons/gtkwidgeteventfilter.py @@ -65,6 +65,5 @@ __event__ = { "icon" : "player_play", "class" : GtkWidgetEventFilter, "mandatory_props" : ["object_id", "event_name"], - "test" : True } diff --git a/addons/gtkwidgettypefilter.py b/addons/gtkwidgettypefilter.py index 4ffecb5..8faf172 100644 --- a/addons/gtkwidgettypefilter.py +++ b/addons/gtkwidgettypefilter.py @@ -96,5 +96,6 @@ __event__ = { 'display_name' : 'Widget Filter', 'icon' : '', 'class' : GtkWidgetTypeFilter, - 'mandatory_props' : ['next_state', 'object_id'] + 'mandatory_props' : ['next_state', 'object_id'], + "test" : True, } diff --git a/data/ui/creator.glade b/data/ui/creator.glade index 1c9669d..aeba19c 100644 --- a/data/ui/creator.glade +++ b/data/ui/creator.glade @@ -1,16 +1,19 @@ - + + + - - 300 500 + GTK_WINDOW_TOPLEVEL Toolbox False - center-on-parent + False + GTK_WIN_POS_CENTER_ON_PARENT 200 500 True + GDK_WINDOW_TYPE_HINT_UTILITY True True False @@ -19,35 +22,37 @@ True - vertical + GTK_ORIENTATION_VERTICAL 5 + GTK_ORIENTATION_VERTICAL True 5 - start + GTK_BUTTONBOX_START - gtk-save True True True + gtk-save True + 0 False False - 0 - gtk-quit True True True + gtk-quit True + 0 @@ -60,24 +65,24 @@ False False - 0 True True - never - automatic - in + GTK_POLICY_NEVER + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN True - queue + GTK_RESIZE_QUEUE True - vertical + GTK_ORIENTATION_VERTICAL + GTK_ORIENTATION_VERTICAL True @@ -90,7 +95,6 @@ 2 0 0 - 0 @@ -106,7 +110,6 @@ False - 0 @@ -121,7 +124,6 @@ 2 0 0 - 0 @@ -153,8 +155,9 @@ True - vertical + GTK_ORIENTATION_VERTICAL 10 + GTK_ORIENTATION_VERTICAL @@ -169,26 +172,27 @@ True 5 - start + GTK_BUTTONBOX_START - gtk-media-record True True + gtk-media-record True + 0 False False - 0 - gtk-media-stop True True + gtk-media-stop True + 0 False diff --git a/src/extensions/tutoriusremote.py b/src/extensions/tutoriusremote.py index 918f3da..fc93659 100755 --- a/src/extensions/tutoriusremote.py +++ b/src/extensions/tutoriusremote.py @@ -36,7 +36,7 @@ from sugar.graphics.combobox import ComboBox from jarabe.frame.frameinvoker import FrameWidgetInvoker from jarabe.model.shell import get_model -#from sugar.tutorius.creator import default_creator +from sugar.tutorius.creator import default_creator from sugar.tutorius.vault import Vault @@ -45,12 +45,7 @@ _ICON_NAME = 'tutortool' LOGGER = logging.getLogger('remote') class TutoriusRemote(TrayIcon): - - FRAME_POSITION_RELATIVE = 102 - - def __init__(self):#, creator): - #self._creator = creator - + def __init__(self): client = gconf.client_get_default() self._color = XoColor(client.get_string('/desktop/sugar/user/color')) @@ -67,69 +62,74 @@ class TPalette(Palette): def __init__(self, primary_text): super(TPalette, self).__init__(primary_text) - #self._creator_item = gtk.MenuItem(_('Create a tutorial')) - #self._creator_item.connect('activate', self._start_creator) - #self._creator_item.show() + self._creator_item = gtk.MenuItem(_('Create a tutorial')) + self._creator_item.connect('activate', self._toggle_creator) + self._creator_item.show() self._tut_list_item = gtk.MenuItem(_('Show tutorials')) self._tut_list_item.connect('activate', self._list_tutorials) self._tut_list_item.show() - #self.menu.append(self._creator_item) + self.menu.append(self._creator_item) self.menu.append(self._tut_list_item) self.set_content(None) - #def _start_creator(self, widget): - # default_creator().start_authoring(tutorial=None) + def _toggle_creator(self, widget): + creator = default_creator() + + if creator.is_authoring == False: + # Replace the start creator button by the stop creator + # Allocate a white color for the text + self._creator_item.props.label = _("Stop this tutorial") + creator.start_authoring(tutorial=None) + + else: + # Attempt to close the creator - this will popup a confirmation + # dialog if the user has unsaved changes + creator._cleanup_cb() + + # If the creator was not actually closed - (in case cancel + # is implemented one day) + if creator.is_authoring == True: + return + # Switch back to start creator entry + self._creator_item.props.label = _("Create a tutorial") def _list_tutorials(self, widget): - # Create the selection dialog dlg = gtk.Dialog('Run a tutorial', None, gtk.DIALOG_MODAL, - (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, - gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - # Write the user prompt + (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, + gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT)) dlg.vbox.pack_start(gtk.Label(_('Which tutorial do you want to run?\n'))) - # Fetchthe current activity name activity = get_model().get_active_activity() - act_name = activity.get_activity_name() - # Query the vault to get the tutorials related to this activity + act_name = activity.get_activity_name() tutorial_dict = Vault.list_available_tutorials(act_name) # Build the combo box combo = ComboBox() - - # Insert all the related tutorials for (tuto_name, tuto_guid) in tutorial_dict.items(): combo.append_item(tuto_name, tuto_guid) dlg.vbox.pack_end(combo) dlg.show_all() - # Show the dialog to the user result = dlg.run() + dlg.destroy() - # If the user cliked OK if result == gtk.RESPONSE_ACCEPT: - # Get the current active item in the row = combo.get_active_item() if row: - # Fetch the name and Guid guid = row[0] name = row[1] - LOGGER.debug("TPalette :: Got message to launch tutorial %s with guid %s"%(str(name), str(guid))) - # Send a DBus message to the Engine to run this tutorial from sugar.tutorius.service import ServiceProxy service = ServiceProxy() + service.launch(guid) - - # Close the dialog - dlg.destroy() def setup(tray): - tray.add_device(TutoriusRemote())#default_creator())) + tray.add_device(TutoriusRemote()) diff --git a/tests/probetests.py b/tests/probetests.py index 37748d8..17c6afc 100644 --- a/tests/probetests.py +++ b/tests/probetests.py @@ -96,11 +96,13 @@ class MockProbeProxy(object): def isAlive(self): return self.MockAlive - def install(self, action, action_installed_cb, error_cb): + def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None): self.MockAction = action self.MockAddressCallback_install = action_installed_cb self.MockInstallErrorCallback = error_cb self.MockActionUpdate = None + self.MockIsEditing = is_editing + self.MockEditCb = editing_cb return None def update(self, action_address, newaction, block=False): @@ -108,9 +110,10 @@ class MockProbeProxy(object): self.MockActionUpdate = newaction return None - def uninstall(self, action_address): + def uninstall(self, action_address, is_editing=False): self.MockAction = None self.MockActionUpdate = None + self.MockIsEditing = None return None def subscribe(self, event, notif_cb, subscribe_cb, error_cb): @@ -223,35 +226,35 @@ class ProbeTest(unittest.TestCase): assert message_box is None, "Message box should still be empty" #install 1 - address = self.probe.install(pickle.dumps(action)) + address = self.probe.install(pickle.dumps(action), False) assert type(address) == str, "install should return a string" assert message_box == (5, "woot"), "message box should have (i, s)" #install 2 action.i, action.s = (10, "ahhah!") - address2 = self.probe.install(pickle.dumps(action)) + address2 = self.probe.install(pickle.dumps(action), False) assert message_box == (10, "ahhah!"), "message box should have changed" assert address != address2, "action addresses should be different" #uninstall 2 - self.probe.uninstall(address2) + self.probe.uninstall(address2, False) assert message_box is None, "undo should clear the message box" #update action 1 with action 2 props - self.probe.update(address, pickle.dumps(action._props)) + self.probe.update(address, pickle.dumps(action._props), False) assert message_box == (10, "ahhah!"), "message box should have changed(i, s)" #ErrorCase: Update with bad address #try to update 2, should fail - self.assertRaises(KeyError, self.probe.update, address2, pickle.dumps(action._props)) + self.assertRaises(KeyError, self.probe.update, address2, pickle.dumps(action._props), False) - self.probe.uninstall(address) + self.probe.uninstall(address, False) assert message_box is None, "undo should clear the message box" message_box = "Test" #ErrorCase: Uninstall bad address (currently silent fail) #Uninstall twice should do nothing - self.probe.uninstall(address) + self.probe.uninstall(address, False) assert message_box == "Test", "undo should not have happened again" def test_events(self): @@ -458,10 +461,10 @@ class ProbeProxyTest(unittest.TestCase): #ErrorCase: Uninstall on not installed action (silent fail) #Test the uninstall - self.probeProxy.uninstall(action2_address) + self.probeProxy.uninstall(action2_address, False) assert not "uninstall" in self.mockObj.MockCall, "Uninstall should not be called if action is not installed" - self.probeProxy.uninstall(address) + self.probeProxy.uninstall(address, False) assert self.mockObj.MockCall["uninstall"]["args"][0] == address, "1 argument, the action address" def test_events(self): diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index d0cc6e1..c0eedee 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -20,16 +20,16 @@ import os import gobject -import dbus import dbus.service import cPickle as pickle +from functools import partial from . import addon +from . import properties from .services import ObjectStore -from .properties import TPropContainer -from .dbustools import save_args, ignore, logError +from .dbustools import remote_call, save_args, ignore, logError import copy """ @@ -127,11 +127,12 @@ class TProbe(dbus.service.Object): # ------------------ Action handling -------------------------------------- @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='s', out_signature='s') - def install(self, pickled_action): + in_signature='sb', out_signature='s') + def install(self, pickled_action, is_editing): """ Install an action on the Activity @param pickled_action string pickled action + @param is_editing whether this action comes from the editor @return string address of installed action """ loaded_action = pickle.loads(str(pickled_action)) @@ -144,17 +145,22 @@ class TProbe(dbus.service.Object): if action._props: action._props.update(loaded_action._props) - action.do(activity=self._activity) - + if not is_editing: + action.do(activity=self._activity) + else: + action.enter_editmode() + action.set_notification_cb(partial(self.update_action, address)) + return address @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='ss', out_signature='') - def update(self, address, action_props): + in_signature='ssb', out_signature='') + def update(self, address, action_props, is_editing): """ Update an already registered action @param address string address returned by install() @param action_props pickled action properties + @param is_editing whether this action comes from the editor @return None """ action = self._installedActions[address] @@ -162,26 +168,47 @@ class TProbe(dbus.service.Object): if action._props: props = pickle.loads(str(action_props)) action._props.update(props) - action.undo() - action.do() + if not is_editing: + action.undo() + action.do() + else: + action.exit_editmode() + action.enter_editmode() @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='s', out_signature='') - def uninstall(self, address): + in_signature='sb', out_signature='') + def uninstall(self, address, is_editing): """ Uninstall an action @param address string address returned by install() + @param is_editing whether this action comes from the editor @return None """ if self._installedActions.has_key(address): action = self._installedActions[address] - action.undo() + if not is_editing: + action.undo() + else: + action.exit_editmode() self._installedActions.pop(address) # ------------------ Event handling --------------------------------------- @dbus.service.method("org.tutorius.ProbeInterface", in_signature='s', out_signature='s') + def create_event(self, addon_name): + # avoid recursive imports + event = addon.create(addon_name) + addonname = type(event).__name__ + meta = addon.get_addon_meta(addonname) + for propname in meta['mandatory_props']: + prop = getattr(type(event), propname) + prop.widget_class.run_dialog(self._activity, event, propname) + + return pickle.dumps(event) + + @dbus.service.method("org.tutorius.ProbeInterface", + in_signature='s', out_signature='s') def subscribe(self, pickled_event): """ Subscribe to an Event @@ -214,7 +241,6 @@ class TProbe(dbus.service.Object): @param address string adress returned by subscribe() @return None """ - if self._subscribedEvents.has_key(address): eventfilter = self._subscribedEvents[address] eventfilter.remove_handlers() @@ -235,6 +261,21 @@ class TProbe(dbus.service.Object): else: raise RuntimeWarning("Attempted to raise an unregistered event") + @dbus.service.signal("org.tutorius.ProbeInterface") + def addonUpdated(self, addon_address, pickled_diff_dict): + # Don't do any added processing, the signal will be sent + # when the method exits + pass + + def update_action(self, addon_address, diff_dict): + LOGGER.debug("TProbe :: Trying to update action %s with new property dict %s"%(addon_address, str(diff_dict))) + # Check that this action is installed + if addon_address in self._installedActions.keys(): + LOGGER.debug("TProbe :: Updating action %s"%(addon_address)) + self.addonUpdated(addon_address, pickle.dumps(diff_dict)) + else: + raise RuntimeWarning("Attempted to updated an action that wasn't installed") + # Return a unique name for this action def _generate_action_reference(self, action): # TODO elavoie 2009-07-25 Should return a universal address @@ -282,6 +323,7 @@ class ProbeProxy: self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface") self._actions = {} + self._edition_callbacks = {} # We keep those two data structures to be able to have multiple callbacks # for the same event and be able to remove them independently # _subscribedEvents holds a list of callback addresses's for each event @@ -290,6 +332,17 @@ class ProbeProxy: self._registeredCallbacks = {} self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface") + self._object.connect_to_signal("addonUpdated", self._handle_update_signal, dbus_interface="org.tutorius.ProbeInterface") + + def _handle_update_signal(self, addon_address, pickled_diff_dict): + address = str(addon_address) + diff_dict = pickle.loads(str(pickled_diff_dict)) + LOGGER.debug("ProbeProxy :: Received update property for action %s"%(address)) + # Launch the callback to warn the upper layers of a modification of the addon + # from a widget inside the activity + if self._edition_callbacks.has_key(address): + LOGGER.debug("ProbeProxy :: Executing update callback...") + self._edition_callbacks[address](address, diff_dict) def _handle_signal(self, pickled_event): event = pickle.loads(str(pickled_event)) @@ -310,33 +363,47 @@ class ProbeProxy: except: return False - def __update_action(self, action, callback, address): + def __update_action(self, action, callback, editing_cb, address): LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address)) + address = str(address) + # Store the action self._actions[address] = action + # Store the edition callback + if editing_cb: + self._edition_callbacks[address] = editing_cb + # Propagate the action installed callback upwards in the stack callback(address) def __clear_action(self, address): + # Remove the action installed at this address self._actions.pop(address, None) + # Remove the edition callback + self._edition_callbacks.pop(address, None) - def install(self, action, action_installed_cb, error_cb): + def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None): """ Install an action on the TProbe's activity @param action Action to install @param action_installed_cb The callback function to call once the action is installed @param error_cb The callback function to call when an error happens + @param is_editing whether this action comes from the editor + @param editing_cb The function to execute when the action is updated + (this is only done in edition mode) @return None """ - self._probe.install(pickle.dumps(action), - reply_handler=save_args(self.__update_action, action, action_installed_cb), - error_handler=save_args(error_cb, action)) + self._probe.install(pickle.dumps(action), + is_editing, + reply_handler=save_args(self.__update_action, action, action_installed_cb, editing_cb), + error_handler=save_args(error_cb, action)) - def update(self, action_address, newaction): + def update(self, action_address, newaction, is_editing=False): """ Update an already installed action's properties and run it again @param action_address The address of the action to update. This is provided by the install callback method. @param newaction Action to update it with @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor @return None """ #TODO review how to make this work well @@ -344,19 +411,20 @@ class ProbeProxy: raise RuntimeWarning("Action not installed") #TODO Check error handling return self._probe.update(action_address, pickle.dumps(newaction._props), + is_editing, reply_handler=ignore, error_handler=logError) - def uninstall(self, action_address): + def uninstall(self, action_address, is_editing): """ Uninstall an installed action @param action_address The address of the action to uninstall. This address was given on action installation - @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor """ if action_address in self._actions: self._actions.pop(action_address, None) - self._probe.uninstall(action_address, reply_handler=ignore, error_handler=logError) + self._probe.uninstall(action_address, is_editing, reply_handler=ignore, error_handler=logError) def __update_event(self, event, callback, event_subscribed_cb, address): LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address)) @@ -407,7 +475,18 @@ class ProbeProxy: else: LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address) + def create_event(self, addon_name): + """ + Create an event on the app side and request the user to fill the + properties before returning it. + + @param addon_name: the add-on name of the event + @returns: an eventfilter instance + """ + return pickle.loads(str(self._probe.create_event(addon_name))) + def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): + """ Register an event listener @param event Event to listen for @@ -427,7 +506,7 @@ class ProbeProxy: reply_handler=save_args(self.__update_event, event, notification_cb, event_subscribed_cb), error_handler=save_args(error_cb, event)) - def unsubscribe(self, address, block=True): + def unsubscribe(self, address): """ Unregister an event listener @param address identifier given by subscribe() @@ -449,12 +528,13 @@ class ProbeProxy: subscribed events should be removed. """ for action_addr in self._actions.keys(): - self.uninstall(action_addr) + # TODO : Make sure there is a way for each action to be properly + # uninstalled according to its right edition mode + self.uninstall(action_addr, True) for address in self._subscribedEvents.keys(): self.unsubscribe(address) - class ProbeManager(object): """ The ProbeManager provides multiplexing across multiple activity ProbeProxies @@ -464,6 +544,8 @@ class ProbeManager(object): """ _LOGGER = logging.getLogger("sugar.tutorius.ProbeManager") + default_instance = None + def __init__(self, proxy_class=ProbeProxy): """Constructor @param proxy_class Class to use for creating Proxies to activities. @@ -478,6 +560,8 @@ class ProbeManager(object): ProbeManager._LOGGER.debug("__init__()") + ProbeManager.default_instance = self + def setCurrentActivity(self, activity_id): if not activity_id in self._probes: raise RuntimeError("Activity not attached, id : %s"%activity_id) @@ -488,41 +572,64 @@ class ProbeManager(object): currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) - def install(self, action, action_installed_cb, error_cb): + def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None): """ Install an action on the current activity @param action Action to install @param action_installed_cb The callback to call once the action is installed @param error_cb The callback that will be called if there is an error during installation @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor + @param editing_cb The function to execute when propagating changes on + this action (only used when is_editing is true) @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).install(action, action_installed_cb, error_cb) + return self._first_proxy(self.currentActivity).install( + action=action, + is_editing=is_editing, + action_installed_cb=action_installed_cb, + error_cb=error_cb, + editing_cb=editing_cb) else: raise RuntimeWarning("No activity attached") - def update(self, action_address, newaction): + def update(self, action_address, newaction, is_editing=False): """ Update an already installed action's properties and run it again @param action_address Action to update @param newaction Action to update it with @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).update(action_address, newaction) + return self._first_proxy(self.currentActivity).update(action_address, newaction, is_editing) else: raise RuntimeWarning("No activity attached") - def uninstall(self, action_address): + def uninstall(self, action_address, is_editing=False): """ Uninstall an installed action - @param action Action to uninstall + @param action_address Action to uninstall @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor """ if self.currentActivity: - return self._first_proxy(self.currentActivity).uninstall(action_address) + return self._first_proxy(self.currentActivity).uninstall(action_address, is_editing) + else: + raise RuntimeWarning("No activity attached") + + def create_event(self, addon_name): + """ + Create an event on the app side and request the user to fill the + properties before returning it. + + @param addon_name: the add-on name of the event + @returns: an eventfilter instance + """ + if self.currentActivity: + return self._first_proxy(self.currentActivity).create_event(addon_name) else: raise RuntimeWarning("No activity attached") @@ -599,8 +706,6 @@ class ProbeManager(object): return self._probes[process_name] else: return [] - - def _first_proxy(self, process_name): """ diff --git a/tutorius/actions.py b/tutorius/actions.py index 75c9c9b..a32138f 100644 --- a/tutorius/actions.py +++ b/tutorius/actions.py @@ -26,14 +26,21 @@ from . import addon from .services import ObjectStore from .properties import * +import pickle + +import logging + +LOGGER = logging.getLogger("actions") + class DragWrapper(object): """Wrapper to allow gtk widgets to be dragged around""" - def __init__(self, widget, position, draggable=False): + 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 @@ -45,6 +52,7 @@ class DragWrapper(object): self.position = position # position of the widget self.moved = False + self.update_action_cb = update_action_cb self.draggable = draggable def _pressed_cb(self, widget, evt): @@ -79,10 +87,13 @@ class DragWrapper(object): self._eventbox.grab_remove() self._dragging = False + LOGGER.debug("DragWrapper :: Sending update notification...") + self.update_action_cb('position', self.position) + 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): @@ -139,6 +150,12 @@ class Action(TPropContainer): TPropContainer.__init__(self) self.position = (0,0) self._drag = None + # The differences dictionary. This is a structure that holds all the + # modifications that were made to the properties since the action + # was last installed or the last moment the notification was executed. + # Every property change will be logged inside it and it will be sent + # to the creator to update its action edition dialog. + self._property_update_cb = None def do(self, **kwargs): """ @@ -152,7 +169,35 @@ class Action(TPropContainer): """ pass #Should raise NotImplemented? - def enter_editmode(self, **kwargs): + + def set_notification_cb(self, notif_cb): + LOGGER.debug("Action :: Setting notification callback for creator...") + self._property_update_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))) + #property = getattr(self, name) + #property = value + + #self._props[name] = 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._property_update_cb: + # Propagate it + self._property_update_cb(self._diff_dict) + # Empty the diff dict as we just synchronized with the creator + self._diff_dict.clear() + + def enter_editmode(self, *args, **kwargs): """ Enters edit mode. The action should display itself in some way, without affecting the currently running application. The default is @@ -171,7 +216,7 @@ class Action(TPropContainer): ObjectStore().activity._overlayer.put(self.__edit_img, x, y) self.__edit_img.show_all() - self._drag = DragWrapper(self.__edit_img, self.position, True) + 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 diff --git a/tutorius/creator.py b/tutorius/creator.py index 68c5fa6..0d3ac3c 100644 --- a/tutorius/creator.py +++ b/tutorius/creator.py @@ -21,67 +21,113 @@ the activity itself. # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -import gtk.gdk import gtk.glade import gobject from gettext import gettext as T +import pickle + import uuid import os from sugar.graphics import icon, style +import jarabe.frame -from . import overlayer, gtkutils, actions, vault, properties, addon -from . import filters +from . import overlayer, gtkutils, vault, addon from .services import ObjectStore -from .core import State from .tutorial import Tutorial from . import viewer -from .propwidgets import TextInputDialog, StringPropWidget +from .propwidgets import TextInputDialog +from . import TProbe + +from functools import partial + +from dbus import SessionBus +from dbus.service import method, Object, BusName +from .dbustools import ignore + +import logging + +LOGGER = logging.getLogger("creator") + +BUS_PATH = "/org/tutorius/Creator" +BUS_NAME = "org.tutorius.Creator" + +def default_creator(): + """ + The Creator class is a singleton. There can never be more than one creator + at a time. This method returns a new instance only if none + already exists. Else, the existing instance is returned. + """ + Creator._instance = Creator._instance or Creator() + return Creator._instance -class Creator(object): +def get_creator_proxy(): """ - Class acting as a bridge between the creator, serialization and core - classes. This contains most of the UI part of the editor. + Returns a Creator dbus proxy for inter-process events. """ - def __init__(self, activity, tutorial=None): + bus = SessionBus() + proxy = bus.get_object(BUS_NAME, BUS_PATH) + return proxy + +class Creator(Object): + """ + Class acting as a controller for the tutorial edition. + """ + + _instance = None + + def __init__(self): + bus_name = BusName(BUS_NAME, bus=SessionBus()) + Object.__init__(self, bus_name, BUS_PATH) + + self.tuto = None + self.is_authoring = False + Creator._instance = self + self._probe_mgr = TProbe.ProbeManager.default_instance + self._installed_actions = list() + + def start_authoring(self, tutorial=None): """ - Instanciate a tutorial creator for the activity. + Start authoring a tutorial. - @param activity to bind the creator to - @param tutorial an existing tutorial to edit, or None to create one + @type tutorial: str or None + @param tutorial: the unique identifier to an existing tutorial to + modify, or None to create a new one. """ - self._activity = activity + if self.is_authoring: + raise Exception("Already authoring") + + self.is_authoring = True + if not tutorial: self._tutorial = Tutorial('Untitled') self._state = self._tutorial.add_state() self._tutorial.update_transition( transition_name=self._tutorial.INITIAL_TRANSITION_NAME, new_state=self._state) + final_event = addon.create( + name='MessageButtonNext', + message=T('This is the end of this tutorial.') + ) + self._tutorial.add_transition( + state_name=self._state, + transition=(final_event, self._tutorial.END), + ) else: self._tutorial = tutorial # TODO load existing tutorial; unused yet self._action_panel = None self._current_filter = None - self._intro_mask = None - self._intro_handle = None - allocation = self._activity.get_allocation() - self._width = allocation.width - self._height = allocation.height self._selected_widget = None self._eventmenu = None self.tuto = None self._guid = None + self.metadata = None - self._hlmask = overlayer.Rectangle(None, (1.0, 0.0, 0.0, 0.5)) - self._activity._overlayer.put(self._hlmask, 0, 0) + frame = jarabe.frame.get_view() - dlg_width = 300 - dlg_height = 70 - sw = gtk.gdk.screen_width() - sh = gtk.gdk.screen_height() - - self._propedit = ToolBox(self._activity) + self._propedit = ToolBox(None) self._propedit.tree.signal_autoconnect({ 'on_quit_clicked': self._cleanup_cb, 'on_save_clicked': self.save, @@ -89,18 +135,39 @@ class Creator(object): 'on_event_activate': self._add_event_cb, }) self._propedit.window.move( - gtk.gdk.screen_width()-self._propedit.window.get_allocation().width, - 100) - + gtk.gdk.screen_width()-self._propedit.window.get_allocation().width\ + -style.GRID_CELL_SIZE, + style.GRID_CELL_SIZE) + self._propedit.window.connect('enter-notify-event', + frame._enter_notify_cb) + self._propedit.window.connect('leave-notify-event', + frame._leave_notify_cb) self._overview = viewer.Viewer(self._tutorial, self) - self._overview.win.set_transient_for(self._activity) + self._overview.win.set_transient_for(frame._bottom_panel) + self._overview.win.connect('enter-notify-event', + frame._enter_notify_cb) + self._overview.win.connect('leave-notify-event', + frame._leave_notify_cb) - self._overview.win.move(0, gtk.gdk.screen_height()- \ - self._overview.win.get_allocation().height) + self._overview.win.move(style.GRID_CELL_SIZE, + gtk.gdk.screen_height()-style.GRID_CELL_SIZE \ + -self._overview.win.get_allocation().height) self._transitions = dict() + # FIXME : remove when probemgr completed + #self._probe_mgr.attach('org.laptop.Calculate') + self._probe_mgr._current_activity = 'org.laptop.Calculate' + + def _tool_enter_notify_cb(self, window, event): + frame = jarabe.frame.get_view() + frame._bottom_panel.hover = True + + def _tool_leave_notify_cb(self, window, event): + frame = jarabe.frame.get_view() + frame._bottom_panel.hover = False + def _update_next_state(self, state, event, next_state): self._transitions[event] = next_state @@ -120,7 +187,8 @@ class Creator(object): .get(action, None) if not action_obj: return False - action_obj.exit_editmode() + + self._probe_mgr.uninstall(action_obj.address) self._tutorial.delete_action(action) self._overview.win.queue_draw() return True @@ -133,7 +201,9 @@ class Creator(object): @returns: True if successful, otherwise False. """ - if self._state in (self._tutorial.INIT, self._tutorial.END): + if self._state in (self._tutorial.INIT, self._tutorial.END) \ + or self._tutorial.END in \ + self._tutorial.get_following_states_dict(self._state): # last state cannot be removed return False @@ -144,103 +214,105 @@ class Creator(object): return bool(self._tutorial.delete_state(remove_state)) def get_insertion_point(self): + """ + @returns: the current tutorial insertion point. + """ return self._state def set_insertion_point(self, state_name): - for action in self._tutorial.get_action_dict(self._state).values(): - action.exit_editmode() + """ + Set the tutorial modification point to the specified state. + Actions of the state will enter the edit mode. + New actions will be inserted to that state and new transisions will + shift the current transision to the next state. + + @param state_name: the name of the state to use as insertion point + """ + # first is not modifiable, as the auto transition would make changes + # pointless. The end state is also pointless to modify, as the tutorial + # gets detached. + if state_name == self._tutorial.INIT \ + or state_name == self._tutorial.END: + return + + for action in self._installed_actions: + self._probe_mgr.uninstall(action.address, + is_editing=True) + self._installed_actions = [] self._state = state_name state_actions = self._tutorial.get_action_dict(self._state).values() + for action in state_actions: - action.enter_editmode() - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True, + editing_cb=self.update_addon_property) if state_actions: + # I'm really lazy right now and to keep things simple I simply + # always select the first action when + # we change state. we should really select the clicked block + # in the overview instead. FIXME self._propedit.action = state_actions[0] else: self._propedit.action = None self._overview.win.queue_draw() - - def _evfilt_cb(self, menuitem, event): - """ - This will get called once the user has selected a menu item from the - event filter popup menu. This should add the correct event filter - to the FSM and increment states. - """ - # undo actions so they don't persist through step editing - for action in self._state.get_action_list(): - action.exit_editmode() - self._hlmask.covered = None - self._propedit.action = None - self._activity.queue_draw() - - def _intro_cb(self, widget, evt): - """ - Callback for capture of widget events, when in introspect mode. - """ - if evt.type == gtk.gdk.BUTTON_PRESS: - # widget has focus, let's hilight it - win = gtk.gdk.display_get_default().get_window_at_pointer() - click_wdg = win[0].get_user_data() - if not click_wdg.is_ancestor(self._activity._overlayer): - # as popups are not (yet) supported, it would break - # badly if we were to play with a widget not in the - # hierarchy. - return - for hole in self._intro_mask.pass_thru: - self._intro_mask.mask(hole) - self._intro_mask.unmask(click_wdg) - self._selected_widget = gtkutils.raddr_lookup(click_wdg) - - if self._eventmenu: - self._eventmenu.destroy() - self._eventmenu = gtk.Menu() - menuitem = gtk.MenuItem(label=type(click_wdg).__name__) - menuitem.set_sensitive(False) - self._eventmenu.append(menuitem) - self._eventmenu.append(gtk.MenuItem()) - - for item in gobject.signal_list_names(click_wdg): - menuitem = gtk.MenuItem(label=item) - menuitem.connect("activate", self._evfilt_cb, item) - self._eventmenu.append(menuitem) - self._eventmenu.show_all() - self._eventmenu.popup(None, None, None, evt.button, evt.time) - self._activity.queue_draw() - def _add_action_cb(self, widget, path): """Callback for the action creation toolbar tool""" action_type = self._propedit.actions_list[path][ToolBox.ICON_NAME] + LOGGER.debug("Creator :: Adding an action = %s"%(action_type)) action = addon.create(action_type) - action.enter_editmode() + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True, + editing_cb=self.update_addon_property) self._tutorial.add_action(self._state, action) - # FIXME: replace following with event catching - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + self._propedit.action = action self._overview.win.queue_draw() def _add_event_cb(self, widget, path): - """Callback for the event creation toolbar tool""" + """ + Callback for the event creation toolbar tool. + + The behaviour of event addition is to push the transition of the current + state to the next (newly created state). + + | + v + .--------. .-------. .--------. + | action |---->| event |---->| action | + '--------' '-------' '--------' + | + .--------. .-----------. v .-------. .--------. + | action |--->| new event |-->| event |---->| action | + '--------' '-----------' '-------' '--------' + The cursor always selects a state (between the action and transition) + The result is what the user expects: inserting before an action will + effectively shift the next transition to the next state. + + """ event_type = self._propedit.events_list[path][ToolBox.ICON_NAME] - event = addon.create(event_type) - addonname = type(event).__name__ - meta = addon.get_addon_meta(addonname) - for propname in meta['mandatory_props']: - prop = getattr(type(event), propname) - prop.widget_class.run_dialog(self._activity, event, propname) + event = self._probe_mgr.create_event(event_type) event_filters = self._tutorial.get_transition_dict(self._state) # if not at the end of tutorial if event_filters: - old_transition = event_filters.keys()[0] - new_state = self._tutorial.add_state(event_filters[old_transition]) - self._tutorial.update_transition(transition_name=old_transition, - new_state=new_state) + old_name = event_filters.keys()[0] + old_transition = self._tutorial.delete_transition(old_name) + new_state = self._tutorial.add_state( + transition_list=(old_transition,) + ) + self._tutorial.add_transition(state_name=self._state, + transition=(event, new_state), + ) else: # append empty state only if edit inserting at end of linearized @@ -252,17 +324,29 @@ class Creator(object): self.set_insertion_point(new_state) + def properties_changed(self, action, properties): + LOGGER.debug("Creator :: properties_changed for action at address %s to %s"%(action.address, str(properties))) + address = action.address + self._probe_mgr.update(address, + action, + is_editing=True) + + def _update_error(self, exception): + pass + def _action_refresh_cb(self, widget, evt, action): """ Callback for refreshing properties values and notifying the property dialog of the new values. """ - action.exit_editmode() - action.enter_editmode() - self._activity.queue_draw() - # TODO: replace following with event catching - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + self._probe_mgr.uninstall(action.address, + is_editing=True) + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True, + editing_cb=self.update_addon_property) self._propedit.action = action self._overview.win.queue_draw() @@ -275,61 +359,116 @@ class Creator(object): """ # undo actions so they don't persist through step editing for action in self._tutorial.get_action_dict(self._state).values(): - action.exit_editmode() + self._probe_mgr.uninstall(action.address, + is_editing=True) + # TODO : Support quit cancellation - right now,every time we execute this, + # we will forcibly end edition afterwards. It would be nice to keep creating if kwargs.get('force', False): dialog = gtk.MessageDialog( - parent=self._activity, + parent=self._overview.win, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, - message_format=T('Do you want to save before stopping edition?')) + message_format=T( + 'Do you want to save before stopping edition?')) do_save = dialog.run() dialog.destroy() if do_save == gtk.RESPONSE_YES: self.save() # remove UI remains - self._hlmask.covered = None - self._activity._overlayer.remove(self._hlmask) - self._hlmask.destroy() - self._hlmask = None self._propedit.destroy() self._overview.destroy() - self._activity.queue_draw() - del self._activity._creator + self.is_authoring = False def save(self, widget=None): + """ + Save the currently edited tutorial to bundle, prompting for + a name as needed. + """ if not self._guid: self._guid = str(uuid.uuid1()) dlg = TextInputDialog(parent=self._overview.win, text=T("Enter a tutorial title."), field=T("Title")) - tutorialName = "" - while not tutorialName: tutorialName = dlg.pop() + tutorial_name = "" + while not tutorial_name: + tutorial_name = dlg.pop() dlg.destroy() self._metadata = { vault.INI_GUID_PROPERTY: self._guid, - vault.INI_NAME_PROPERTY: tutorialName, + vault.INI_NAME_PROPERTY: tutorial_name, vault.INI_VERSION_PROPERTY: '1', - 'activities':{os.environ['SUGAR_BUNDLE_NAME']: - os.environ['SUGAR_BUNDLE_VERSION'] - }, } + # FIXME : The environment does not dispose of the appropriate + # variables to inform the creator at this point. We will + # need to iterate inside all the actions and remember + # their sources. + + # FIXME : I insist. This is a hack. + related_activities_dict = {} + related_activities_dict['calculate'] = '27' + + self._metadata['activities'] = dict(related_activities_dict) vault.Vault.saveTutorial(self._tutorial, self._metadata) + def launch(self, *args): + assert False, "REMOVE THIS CALL!!!" + launch = staticmethod(launch) - def launch(*args, **kwargs): + def _action_installed_cb(self, action, address): """ - Launch and attach a creator to the currently running activity. + This is a callback intented to be use to receive actions addresses + after they are installed. + @param address: the address of the newly installed action """ - activity = ObjectStore().activity - if not hasattr(activity, "_creator"): - activity._creator = Creator(activity) - launch = staticmethod(launch) + action.address = address + self._installed_actions.append(action) + + def _dbus_exception(self, event, exception): + """ + This is a callback intented to be use to receive exceptions on remote + DBUS calls. + @param exception: the exception thrown by the remote process + """ + LOGGER.debug("Creator :: Got exception -> %s"%(str(exception))) + + @method(BUS_NAME, + in_signature='', + out_signature='b') + def get_authoring_state(self): + """ + @returns True if the creator is being executed right now, False otherwise. + """ + return self.is_authoring + + def update_addon_property(self, addon_address, diff_dict): + """ + Updates the properties on an addon. + @param addon_address The address of the addon that has the property + @param diff_dict The updates to apply to the property dict. + This is treated as a partial update to the addon's + dictionary and contains at least one property value pair + @returns True if the property was updated, False otherwise + """ + # Look up the registered addresses inside the installed actions + for action in self._installed_actions: + # If this is the correct action + if action.address == addon_address: + # Update its property with the new value + action._props.update(diff_dict) + # Update the property edition dialog with it + self._propedit.action = action + return True + class ToolBox(object): + """ + Palette window for edition tools, including the actions, states and + the editable property list of selected actions. + """ ICON_LABEL = 0 ICON_IMAGE = 1 ICON_NAME = 2 @@ -337,21 +476,26 @@ class ToolBox(object): def __init__(self, parent): super(ToolBox, self).__init__() self.__parent = parent - sugar_prefix = os.getenv("SUGAR_PREFIX",default="/usr") + sugar_prefix = os.getenv("SUGAR_PREFIX", default="/usr") glade_file = os.path.join(sugar_prefix, 'share', 'tutorius', 'ui', 'creator.glade') self.tree = gtk.glade.XML(glade_file) self.window = self.tree.get_widget('mainwindow') + self.window.modify_bg(gtk.STATE_NORMAL, + style.COLOR_TOOLBAR_GREY.get_gdk_color()) self._propbox = self.tree.get_widget('propbox') self._propedits = [] self.window.set_transient_for(parent) + self.window.set_keep_above(True) self._action = None self.actions_list = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str) - self.actions_list.set_sort_column_id(self.ICON_LABEL, gtk.SORT_ASCENDING) + self.actions_list.set_sort_column_id(self.ICON_LABEL, + gtk.SORT_ASCENDING) self.events_list = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str) - self.events_list.set_sort_column_id(self.ICON_LABEL, gtk.SORT_ASCENDING) + self.events_list.set_sort_column_id(self.ICON_LABEL, + gtk.SORT_ASCENDING) for toolname in addon.list_addons(): meta = addon.get_addon_meta(toolname) @@ -361,9 +505,13 @@ class ToolBox(object): label = format_multiline(meta['display_name']) if meta['type'] == addon.TYPE_ACTION: - self.actions_list.append((label, img, toolname, meta['display_name'])) + self.actions_list.append( + (label, img, toolname, meta['display_name']) + ) else: - self.events_list.append((label, img, toolname, meta['display_name'])) + self.events_list.append( + (label, img, toolname, meta['display_name']) + ) iconview_action = self.tree.get_widget('iconview1') iconview_action.set_model(self.actions_list) @@ -413,7 +561,8 @@ class ToolBox(object): #Value field prop = getattr(type(action), propname) - propedit = prop.widget_class(self.__parent, action, propname, self._refresh_action_cb) + propedit = prop.widget_class(self.__parent, action, propname, + self._refresh_action_cb) self._propedits.append(propedit) row.pack_end(propedit.widget) @@ -430,8 +579,8 @@ class ToolBox(object): def _refresh_action_cb(self): if self._action is not None: - self.__parent._creator._action_refresh_cb(None, None, self._action) - + default_creator().properties_changed(self._action, self._action._props) + #self.__parent._creator._action_refresh_cb(None, None, self._action) # The purpose of this function is to reformat text, as current IconView # implentation does not insert carriage returns on long lines. diff --git a/tutorius/properties.py b/tutorius/properties.py index a462782..85e8aa5 100644 --- a/tutorius/properties.py +++ b/tutorius/properties.py @@ -19,6 +19,7 @@ TutoriusProperties have the same behaviour as python properties (assuming you also use the TPropContainer), with the added benefit of having builtin dialog prompts and constraint validation. """ +import uuid from copy import copy, deepcopy from .constraints import Constraint, \ @@ -35,6 +36,9 @@ from .propwidgets import PropWidget, \ FloatPropWidget, \ IntArrayPropWidget +import logging +LOGGER = logging.getLogger("properties") + class TPropContainer(object): """ A class containing properties. This does the attribute wrapping between @@ -60,6 +64,14 @@ class TPropContainer(object): self._props[attr_name] = propinstance.validate( copy(propinstance.default)) + self.__id = hash(uuid.uuid4()) + # The differences dictionary. This is a structure that holds all the + # modifications that were made to the properties since the action + # was last installed or the last moment the notification was executed. + # Every property change will be logged inside it and it will be sent + # to the creator to update its action edition dialog. + self._diff_dict = {} + def __getattribute__(self, name): """ Process the 'fake' read of properties in the appropriate instance @@ -93,8 +105,11 @@ class TPropContainer(object): try: # We attempt to get the property object with __getattribute__ # to work through inheritance and benefit of the MRO. - return props.__setitem__(name, + real_value = props.__setitem__(name, object.__getattribute__(self, name).validate(value)) + LOGGER.debug("Action :: caching %s = %s in diff dict"%(name, str(value))) + self._diff_dict[name] = value + return real_value except AttributeError: return object.__setattr__(self, name, value) @@ -128,21 +143,23 @@ class TPropContainer(object): # Providing the hash methods necessary to use TPropContainers # in a dictionary, according to their properties def __hash__(self): - #Return a hash of properties (key, value) sorted by key - #We need to transform the list of property key, value lists into - # a tuple of key, value tuples - return hash(tuple(map(tuple,sorted(self._props.items(), cmp=lambda x, y: cmp(x[0], y[0]))))) + # many places we use containers as keys to store additional data. + # Since containers are mutable, there is a need for a hash function + # where the result is constant, so we can still lookup old instances. + return self.__id def __eq__(self, e2): - return isinstance(e2, type(self)) and self._props == e2._props + return self.__id == e2.__id or \ + (isinstance(e2, type(self)) and self._props == e2._props) # Adding methods for pickling and unpickling an object with # properties def __getstate__(self): - return self._props.copy() + return dict(id=self.__id, props=self._props.copy()) def __setstate__(self, dict): - self._props.update(dict) + self.__id = dict['id'] + self._props.update(dict['props']) class TutoriusProperty(object): """ diff --git a/tutorius/translator.py b/tutorius/translator.py index 4f29078..bd24f8f 100644 --- a/tutorius/translator.py +++ b/tutorius/translator.py @@ -177,7 +177,7 @@ class ResourceTranslator(object): install_error_cb(old_action, exception) # Decorated functions - def install(self, action, action_installed_cb, error_cb): + def install(self, action, action_installed_cb, error_cb, is_editing=False, editing_cb=None): # Make a new copy of the action that we want to install, # because translate() changes the action and we # don't want to modify the caller's action representation @@ -187,7 +187,9 @@ class ResourceTranslator(object): # Send the new action to the probe manager self._probe_manager.install(new_action, save_args(self.action_installed, action_installed_cb), - save_args(self.action_install_error, error_cb, new_action)) + save_args(self.action_install_error, error_cb, new_action), + is_editing=is_editing, + editing_cb=editing_cb) def update(self, action_address, newaction): translated_new_action = copy_module.deepcopy(newaction) diff --git a/tutorius/viewer.py b/tutorius/viewer.py index 56428e1..8041162 100644 --- a/tutorius/viewer.py +++ b/tutorius/viewer.py @@ -65,7 +65,8 @@ class Viewer(object): self.drag_pos = None self.selection = set() - self.win = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.win = gtk.Window(gtk.WINDOW_POPUP) + self.win.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY) self.win.set_size_request(400, 200) self.win.set_gravity(gtk.gdk.GRAVITY_SOUTH_WEST) self.win.show() -- cgit v0.9.1