From 52c83b1aab72721a42134a19e5db3acc6c235a39 Mon Sep 17 00:00:00 2001 From: Simon Poirier Date: Thu, 05 Nov 2009 13:13:43 +0000 Subject: half overlayer/half creator --- diff --git a/addons/bubblemessage.py b/addons/bubblemessage.py index 6572a6a..d76dc1c 100644 --- a/addons/bubblemessage.py +++ b/addons/bubblemessage.py @@ -13,7 +13,7 @@ # 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 -from ..actions import Action, DragWrapper +from ..actions import Action from ..properties import TStringProperty, TArrayProperty from .. import overlayer from ..services import ObjectStore @@ -86,21 +86,17 @@ class BubbleMessage(Action): """ if not self.overlay: self.overlay = ObjectStore().activity._overlayer - assert not self._drag, "bubble action set to editmode twice" x, y = self.position self._bubble = overlayer.TextBubble(text=self.message, tailpos=self.tail_pos) - self.overlay.put(self._bubble, x, y) + self._handle = self.overlay.put( + self._bubble, + x, y, + draggable=True, + position_cb=self._update_position) self._bubble.show() - self._drag = DragWrapper(self._bubble, self.position, True) - def exit_editmode(self, *args): - x,y = self._drag.position - self.position = (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 diff --git a/data/ui/creator.glade b/data/ui/creator.glade index 1c9669d..35214df 100644 --- a/data/ui/creator.glade +++ b/data/ui/creator.glade @@ -1,16 +1,18 @@ - + + + - - 300 500 + GTK_WINDOW_POPUP Toolbox False - center-on-parent + GTK_WIN_POS_CENTER_ON_PARENT 200 500 True + GDK_WINDOW_TYPE_HINT_UTILITY True True False @@ -19,35 +21,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 +64,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 +94,6 @@ 2 0 0 - 0 @@ -106,7 +109,6 @@ False - 0 @@ -121,7 +123,6 @@ 2 0 0 - 0 @@ -153,8 +154,9 @@ True - vertical + GTK_ORIENTATION_VERTICAL 10 + GTK_ORIENTATION_VERTICAL @@ -169,26 +171,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/setup.py b/setup.py index a994937..b46c08f 100755 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ class TestCommand(Command): sources) coverage.report(sources) coverage.erase() - + def _listsources(self, arg, dirname, fnames): fnames = filter(lambda x:x.endswith('.py'), fnames) for name in fnames: @@ -83,25 +83,27 @@ class TestCommand(Command): setup(name='Tutorius', version='0.0', description='Interactive tutor and Tutorial creator', - maintainer='Simon Poirier', + maintainer='Simon Poirier', maintainer_email='simpoir@gmail.com', author='Tutorius team', author_email='sugar-narratives@googlegroups.com', url='http://tutorius.org', license='GPLv3', packages=[ - 'sugar.tutorius', - 'sugar.tutorius.uam', - 'sugar.tutorius.addons', - ], + 'sugar.tutorius', + 'sugar.tutorius.uam', + 'sugar.tutorius.addons', + ], package_dir={ - 'sugar.tutorius': 'tutorius', - 'sugar.tutorius.addons': 'addons', - }, + 'sugar.tutorius': 'src/tutorius', + 'sugar.tutorius.addons': 'addons', + }, cmdclass = {'test': TestCommand}, data_files=[('share/icons/sugar/scalable/actions', glob.glob('data/icons/*.svg')), + ('share/icons/sugar/scalable/device', ['data/icons/tutortool.svg']), ('share/tutorius/ui', glob.glob('data/ui/*.glade')), + ('share/sugar/extensions/deviceicon', glob.glob('src/extensions/*')), ] ) -# vim: set et sw=4 sts=4 ts=4: +# vim: set et sw=4 sts=4 ts=4: diff --git a/src/extensions/tutoriusremote.py b/src/extensions/tutoriusremote.py new file mode 100755 index 0000000..4ae8ac8 --- /dev/null +++ b/src/extensions/tutoriusremote.py @@ -0,0 +1,74 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Simon Poirier +# +# +# 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 modules regroups the UI elements that drives the tutorial and tutorial +creator from the Sugar frame. +""" + +import gtk +from gettext import gettext as _ +import gconf +import dbus + +from sugar.graphics.tray import TrayIcon +from sugar.graphics.palette import Palette +from sugar.graphics.xocolor import XoColor + +from jarabe.frame.frameinvoker import FrameWidgetInvoker + +from sugar.tutorius.creator import Creator + +_ICON_NAME = 'tutortool' + +class TutoriusRemote(TrayIcon): + + FRAME_POSITION_RELATIVE = 102 + + def __init__(self, creator): + self._creator = creator + + client = gconf.client_get_default() + self._color = XoColor(client.get_string('/desktop/sugar/user/color')) + + super(TutoriusRemote, self).__init__(icon_name=_ICON_NAME, + xo_color=self._color) + + self.set_palette_invoker(FrameWidgetInvoker(self)) + + self.palette = TPalette(_('Tutorius')) + self.palette.set_group_id('frame') + + +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.menu.append(self._creator_item) + + self.set_content(None) + + def _start_creator(self, widget): + Creator().start_authoring(tutorial=None) + + + +def setup(tray): + tray.add_device(TutoriusRemote(Creator())) diff --git a/tutorius/TProbe.py b/src/tutorius/TProbe.py index f55547c..1e94894 100644 --- a/tutorius/TProbe.py +++ b/src/tutorius/TProbe.py @@ -93,11 +93,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='ss', 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)) @@ -110,17 +111,21 @@ class TProbe(dbus.service.Object): if action._props: action._props.update(loaded_action._props) - action.do() - + if not pickle.loads(str(is_editing)): + action.do() + else: + action.enter_editmode() + return address @dbus.service.method("org.tutorius.ProbeInterface", - in_signature='ss', out_signature='') - def update(self, address, action_props): + in_signature='sss', 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] @@ -128,20 +133,28 @@ class TProbe(dbus.service.Object): if action._props: props = pickle.loads(str(action_props)) action._props.update(props) - action.undo() - action.do() + if not pickle.loads(str(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='ss', 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 pickle.loads(str(is_editing)): + action.undo() + else: + action.exit_editmode() self._installedActions.pop(address) @@ -283,39 +296,46 @@ class ProbeProxy: def __clear_action(self, action): self._actions.pop(action, None) - def install(self, action, block=False): + def install(self, action, block=False, is_editing=False): """ Install an action on the TProbe's activity @param action Action to install @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor @return None """ - return remote_call(self._probe.install, (pickle.dumps(action),), + return remote_call(self._probe.install, (pickle.dumps(action), + pickle.dumps(is_editing)), save_args(self.__update_action, action), block=block) - def update(self, action, newaction, block=False): + def update(self, action, newaction, block=False, is_editing=False): """ Update an already installed action's properties and run it again @param action 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 """ #TODO review how to make this work well if not action in self._actions: raise RuntimeWarning("Action not installed") #TODO Check error handling - return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block) + return remote_call(self._probe.update, (self._actions[action], + pickle.dumps(newaction._props), + pickle.dumps(is_editing)), + block=block) - def uninstall(self, action, block=False): + def uninstall(self, action, block=False, is_editing=False): """ Uninstall an installed action @param action Action to uninstall @param block Force a synchroneous dbus call if True + @param is_editing whether this action comes from the editor """ if action in self._actions: - remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block) + remote_call(self._probe.uninstall,(self._actions.pop(action), is_editing), block=block) def __update_event(self, event, callback, address): LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address)) @@ -455,39 +475,42 @@ class ProbeManager(object): if self._current_activity == activity_id: self._current_activity = None - def install(self, action, block=False): + def install(self, action, block=False, is_editing=False): """ Install an action on the current activity @param action Action to install @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._probes[self.currentActivity].install(action, block) + return self._probes[self.currentActivity].install(action, block, is_editing) else: raise RuntimeWarning("No activity attached") - def update(self, action, newaction, block=False): + def update(self, action, newaction, block=False, is_editing=False): """ Update an already installed action's properties and run it again @param action 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._probes[self.currentActivity].update(action, newaction, block) + return self._probes[self.currentActivity].update(action, newaction, block, is_editing) else: raise RuntimeWarning("No activity attached") - def uninstall(self, action, block=False): + def uninstall(self, action, block=False, is_editing=False): """ Uninstall an installed action @param action 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._probes[self.currentActivity].uninstall(action, block) + return self._probes[self.currentActivity].uninstall(action, block, is_editing) else: raise RuntimeWarning("No activity attached") diff --git a/tutorius/__init__.py b/src/tutorius/__init__.py index e69de29..e69de29 100644 --- a/tutorius/__init__.py +++ b/src/tutorius/__init__.py diff --git a/src/tutorius/actions.py b/src/tutorius/actions.py new file mode 100644 index 0000000..b52886d --- /dev/null +++ b/src/tutorius/actions.py @@ -0,0 +1,78 @@ +# 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 + +from gettext import gettext as _ + +from sugar.graphics import icon + +from . import addon +from .services import ObjectStore +from .properties import * + +class Action(TPropContainer): + """Base class for Actions""" + def __init__(self): + TPropContainer.__init__(self) + self.position = (0,0) + + 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 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__) + + self.__edit_img = 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 + + self._handle = ObjectStore().activity._overlayer.put( + self.__edit_img, + x, y, + draggable=True, + position_cb=self._update_position) + + self.__edit_img.show_all() + + def _update_position(self, x, y): + self.position = [int(x), int(y)] + + def exit_editmode(self, **kwargs): + ObjectStore().activity._overlayer.remove(self._handle) + self.__edit_img.destroy() + diff --git a/tutorius/addon.py b/src/tutorius/addon.py index 7ac68f7..7ac68f7 100644 --- a/tutorius/addon.py +++ b/src/tutorius/addon.py diff --git a/tutorius/constraints.py b/src/tutorius/constraints.py index 519bce8..519bce8 100644 --- a/tutorius/constraints.py +++ b/src/tutorius/constraints.py diff --git a/tutorius/core.py b/src/tutorius/core.py index bfbe07b..bfbe07b 100644 --- a/tutorius/core.py +++ b/src/tutorius/core.py diff --git a/tutorius/creator.py b/src/tutorius/creator.py index c477056..c6934b7 100644 --- a/tutorius/creator.py +++ b/src/tutorius/creator.py @@ -27,28 +27,55 @@ import gobject from gettext import gettext as T import os -from sugar.graphics import icon -import copy +from sugar.graphics import icon, style +import jarabe.frame from . import overlayer, gtkutils, actions, vault, properties, addon from . import filters from .services import ObjectStore from .core import Tutorial, FiniteStateMachine, State from . import viewer +from . import TProbe class Creator(object): """ Class acting as a bridge between the creator, serialization and core classes. This contains most of the UI part of the editor. """ - def __init__(self, activity, tutorial=None): + + _instance = None + + def __new__(typ): + """ + This class is a singleton. There can never be more than one creator + at a time. This method returns a new instance of typ only if none + already exists. + """ + return typ._instance or object.__new__(typ) + + def __init__(self): + if Creator._instance: + return + + super(Creator, self).__init__() + self.tuto = None + self.is_authoring = False + Creator._instance = self + self._probe_mgr = TProbe.ProbeManager() + + 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 = FiniteStateMachine('Untitled') self._state = State(name='INIT') @@ -60,25 +87,14 @@ class Creator(object): 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._hlmask = overlayer.Rectangle(None, (1.0, 0.0, 0.0, 0.5)) - self._activity._overlayer.put(self._hlmask, 0, 0) - - dlg_width = 300 - dlg_height = 70 - sw = gtk.gdk.screen_width() - sh = gtk.gdk.screen_height() + frame = jarabe.frame.get_view() - 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, @@ -86,18 +102,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 @@ -121,7 +158,8 @@ class Creator(object): if fsm_action is action: state.clear_actions() if state is self._state: - fsm_action.exit_editmode() + self._probe_mgr.uninstall(fsmaction, is_editing=True) + #fsm_action.exit_editmode() state_actions.remove(fsm_action) self.set_insertion_point(state.name) for keep_action in state_actions: @@ -182,14 +220,16 @@ class Creator(object): def set_insertion_point(self, state_name): for action in self._state.get_action_list(): - action.exit_editmode() + self._probe_mgr.uninstall(action, is_editing=True) + #action.exit_editmode() self._state = self._tutorial.get_state_by_name(state_name) self._overview.win.queue_draw() state_actions = self._state.get_action_list() for action in state_actions: - action.enter_editmode() - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + self._probe_mgr.install(action, is_editing=True) + #action.enter_editmode() + #action._drag._eventbox.connect_after( + # "button-release-event", self._action_refresh_cb, action) if state_actions: self._propedit.action = state_actions[0] @@ -205,54 +245,21 @@ class Creator(object): """ # 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._probe_mgr.uninstall(action, is_editing=True) + #action.exit_editmode() 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] action = addon.create(action_type) - action.enter_editmode() + self._probe_mgr.install(action, is_editing=True) + #action.enter_editmode() self._state.add_action(action) # FIXME: replace following with event catching - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + #action._drag._eventbox.connect_after( + # "button-release-event", self._action_refresh_cb, action) self._overview.win.queue_draw() def _add_event_cb(self, widget, path): @@ -314,12 +321,14 @@ class Creator(object): Callback for refreshing properties values and notifying the property dialog of the new values. """ - action.exit_editmode() - action.enter_editmode() + self._probe_mgr.uninstall(action, is_editing=True) + #action.exit_editmode() + self._probe_mgr.install(action, is_editing=True) + #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) + #action._drag._eventbox.connect_after( + # "button-release-event", self._action_refresh_cb, action) self._propedit.action = action self._overview.win.queue_draw() @@ -330,10 +339,10 @@ class Creator(object): """ # undo actions so they don't persist through step editing for action in self._state.get_action_list(): - action.exit_editmode() + self._probe_mgr.uninstall(action, is_editing=True) + #action.exit_editmode() dialog = gtk.MessageDialog( - parent=self._activity, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, @@ -344,19 +353,14 @@ class Creator(object): 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): if not self.tuto: - dlg = TextInputDialog(self._activity, - text=T("Enter a tutorial title."), + dlg = TextInputDialog(text=T("Enter a tutorial title."), field=T("Title")) tutorialName = "" while not tutorialName: tutorialName = dlg.pop() @@ -369,16 +373,11 @@ class Creator(object): bundle.write_metadata_file(self.tuto) bundle.write_fsm(self._tutorial) - - def launch(*args, **kwargs): - """ - Launch and attach a creator to the currently running activity. - """ - activity = ObjectStore().activity - if not hasattr(activity, "_creator"): - activity._creator = Creator(activity) + def launch(self, *args): + assert False, "REMOVE THIS CALL!!!" launch = staticmethod(launch) + class ToolBox(object): ICON_LABEL = 0 ICON_IMAGE = 1 @@ -392,6 +391,8 @@ class ToolBox(object): '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.window.set_transient_for(parent) @@ -521,19 +522,19 @@ class ToolBox(object): setattr(action, propname, tuple(attr)) except ValueError: widget.set_text(str(getattr(action, propname)[idx])) - self.__parent._creator._action_refresh_cb(None, None, action) + Creator()._action_refresh_cb(None, None, action) def _uam_prop_changed(self, widget, action, propname): selector = WidgetSelector(self.__parent) selection = selector.select() setattr(action, propname, selection) - self.__parent._creator._action_refresh_cb(None, None, action) + Creator()._action_refresh_cb(None, None, action) def _str_prop_changed(self, widget, evt, action, propname): buf = widget.get_buffer() setattr(action, propname, buf.get_text(buf.get_start_iter(), buf.get_end_iter())) - self.__parent._creator._action_refresh_cb(None, None, action) + Creator()._action_refresh_cb(None, None, action) def _int_prop_changed(self, widget, evt, action, prop): setattr(action, propname, widget.get_value_as_int()) - self.__parent._creator._action_refresh_cb(None, None, action) + Creator()._action_refresh_cb(None, None, action) class WidgetSelector(object): @@ -695,6 +696,7 @@ class TextInputDialog(gtk.MessageDialog): def _dialog_done_cb(self, entry, response): self.response(response) + # The purpose of this function is to reformat text, as current IconView # implentation does not insert carriage returns on long lines. # To preserve layout, this call reformat text to fit in small space under an diff --git a/tutorius/dbustools.py b/src/tutorius/dbustools.py index 5d70d7b..5d70d7b 100644 --- a/tutorius/dbustools.py +++ b/src/tutorius/dbustools.py diff --git a/tutorius/dialog.py b/src/tutorius/dialog.py index be51a0e..be51a0e 100644 --- a/tutorius/dialog.py +++ b/src/tutorius/dialog.py diff --git a/tutorius/editor.py b/src/tutorius/editor.py index 9d2effe..9d2effe 100644 --- a/tutorius/editor.py +++ b/src/tutorius/editor.py diff --git a/tutorius/engine.py b/src/tutorius/engine.py index e77a018..e77a018 100644 --- a/tutorius/engine.py +++ b/src/tutorius/engine.py diff --git a/tutorius/filters.py b/src/tutorius/filters.py index 38cf86b..38cf86b 100644 --- a/tutorius/filters.py +++ b/src/tutorius/filters.py diff --git a/tutorius/gtkutils.py b/src/tutorius/gtkutils.py index 1a9cb0f..1a9cb0f 100644 --- a/tutorius/gtkutils.py +++ b/src/tutorius/gtkutils.py diff --git a/tutorius/linear_creator.py b/src/tutorius/linear_creator.py index f664c49..f664c49 100644 --- a/tutorius/linear_creator.py +++ b/src/tutorius/linear_creator.py diff --git a/tutorius/overlayer.py b/src/tutorius/overlayer.py index b967739..f0d251e 100644 --- a/tutorius/overlayer.py +++ b/src/tutorius/overlayer.py @@ -18,6 +18,9 @@ drawing management (Overlayer) and basic overlayable widgets are defined here. # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import pygtk +pygtk.require('2.0') + import gobject import gtk import cairo @@ -57,20 +60,72 @@ class Overlayer(gtk.Layout): @param overlayed widget to be overlayed. Will be resized to full size. """ + + _instance = None + def __new__(klass, *args): + """ + As the overlay runs in a separate window, bound to the activity's main + window, it doesn't make sense to have multiple instances. + For this reason, __new__ will enforce the singleton pattern and + return the same instance. + + @param klass: the type to instanciate + @returns: the instance of Overlayer for this process. + """ + return Overlayer._instance or gtk.Layout.__new__(klass) + def __init__(self, overlayed=None): + if Overlayer._instance: + return + Overlayer._instance = self super(Overlayer, self).__init__() - self._overlayed = overlayed - if overlayed: - self.put(overlayed, 0, 0) + _set_wm_compositing(True) - self.__realizer = self.connect_after("realize", self.__init_realized) - self.connect("size-allocate", self.__size_allocate) - self.show() + self._overlayed = overlayed + self._win = gtk.Window(gtk.WINDOW_POPUP) + self._win.set_property('skip-taskbar-hint', True) + self._win.set_property('skip-pager-hint', True) + self._win.set_accept_focus(False) + self._win.set_decorated(False) + self._win.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_NOTIFICATION) + self._win.set_keep_above(True) + self._win.set_app_paintable(True) + + self._win.connect("screen-changed", self.__init_realized) + self._win.connect("expose-event", self.__expose_overlay) + self.__visible = True + gobject.timeout_add_seconds(5, self.__keep_visible) + + # ensure colormap is set before realization, so the window gets + # realized with transparency enabled. + self.__init_realized(self._win) + self._win.show_all() self.__render_handle = None - def put(self, child, x, y): + def __keep_visible(self, *args): + """ + This is a hack to ensure the overlay window is always visible. + Ideally, it would be bound to the window, manager raising itself + with its parent window. Right now, it still is in the same process + as the activity, with the side-effect of disappearing along with any + popup that pops down, including menus and tooltips. + + set self.__visible to false to stop this behaviour. + """ + # TODO: use Activity WM tracking to ensure overlayers don't "fight" + # for the focus. We should also evaluate the benefit of completely + # extracting the overlayer from the activities and putting it as part of + # the tutorius service + # standalone service. + if self.__visible: + self._win.present() + gobject.timeout_add_seconds(5, self.__keep_visible) + + + + def put(self, child, x, y, draggable=False, position_cb=None): """ Adds a child widget to be overlayed. This can be, overlay widgets or normal GTK widgets (though normal widgets will alwas appear under @@ -79,15 +134,33 @@ class Overlayer(gtk.Layout): @param child the child to add @param x the horizontal coordinate for positionning @param y the vertical coordinate for positionning + @param draggable: whether or not this widget's should have the + mouse-drag functionality enabled. + @param position_cb: a callback for obtaining position changes + if draggable was activated. Callbacks signature should be + cb(x, y). + @returns: a handle, used when calling Overlayer.remove(handle) """ if hasattr(child, "draw_with_context"): # if the widget has the CanvasDrawable protocol, use it. child.no_expose = True - super(Overlayer, self).put(child, x, y) + + wrapper = DragWrapper(child, draggable, position_cb) + super(Overlayer, self).put(wrapper.container, x, y) + self.show_all() # be sure to redraw or the overlay may not show self.queue_draw() + return wrapper + + def remove(self, handle): + """ + Remove the widget with handle from the overlay. + + @param handle: the handle of the widget, as returned by put() + """ + super(Overlayer, self).remove(handle.container) def __init_realized(self, widget): """ @@ -96,61 +169,63 @@ class Overlayer(gtk.Layout): callback for the first expose run. It should also be called after beign reparented to ensure the window used for drawing is set up. """ - assert hasattr(self.window, "set_composited"), \ - "compositing not supported or widget not realized." - self.disconnect(self.__realizer) - del self.__realizer + screen = widget.get_screen() + colormap = screen.get_rgba_colormap() + if not colormap: + colormap = screen.get_rgb_colormap() + + widget.set_colormap(colormap) - self.parent.set_app_paintable(True) + self._win.set_size_request(screen.get_width(), screen.get_height()) - # the parent is composited, so we can access gtk's rendered buffer - # and overlay over. If we don't composite, we won't be able to read - # pixels and background will be black. - self.window.set_composited(True) - self.__render_handle = self.parent.connect_after("expose-event", \ - self.__expose_overlay) + return True def __expose_overlay(self, widget, event): """expose event handler to draw the thing.""" - #get our child (in this case, the event box) - child = widget.get_child() + screen = widget.get_screen() + colormap = screen.get_rgba_colormap() + widget.set_colormap(colormap) + width, height = widget.get_size() - #create a cairo context to draw to the window ctx = widget.window.cairo_create() - - #the source data is the (composited) event box - ctx.set_source_pixmap(child.window, - child.allocation.x, - child.allocation.y) - - #draw no more than our expose event intersects our child - region = gtk.gdk.region_rectangle(child.allocation) - rect = gtk.gdk.region_rectangle(event.area) - region.intersect(rect) - ctx.region (region) - ctx.clip() + # clear the window + ctx.set_source_rgba(1.0, 1.0, 1.0, 0.0) + ctx.set_operator(cairo.OPERATOR_SOURCE) + ctx.paint() + + ctx.set_source_rgba(1.0, 0.2, 0.2, 0.7) + ctx.rectangle(10.0, 10.0, 200.0, 150.0) + ctx.fill() + ctx.move_to(20,20) + ctx.set_source_rgba(0.0, 0.2, 0.2, 0.7) + layout = ctx.create_layout() + layout.set_markup(self._overlayed.get_title()) + ctx.show_layout(layout) + + + ##draw no more than our expose event intersects our child + #region = gtk.gdk.region_rectangle(child.allocation) + #rect = gtk.gdk.region_rectangle(event.area) + #region.intersect(rect) + #ctx.region (region) + #ctx.clip() ctx.set_operator(cairo.OPERATOR_OVER) - # has to be blended and a 1.0 alpha would not make it blend - ctx.paint_with_alpha(0.99) #draw overlay - for drawn_child in self.get_children()[1:]: + for child in self.get_children(): + drawn_child = child.get_children()[0] if hasattr(drawn_child, "draw_with_context"): drawn_child.draw_with_context(ctx) - def __size_allocate(self, widget, allocation): - """ - Set size allocation (actual gtk widget size) and propagate it to - overlayed child - """ - self.allocation = allocation - # One may wonder why using size_request instead of size_allocate; - # Since widget is laid out in a Layout box, the Layout will honor the - # requested size. Using size_allocate could make a nasty nested loop in - # some cases. - self._overlayed.set_size_request(allocation.width, allocation.height) + # let events pass through + mask = gtk.gdk.Pixmap(None, width, height, 1) + mask_ctx = mask.cairo_create() + mask_ctx.set_operator(cairo.OPERATOR_CLEAR) + mask_ctx.paint() + self._win.input_shape_combine_mask(mask, 0, 0) + return False class TextBubble(gtk.Widget): """ @@ -181,6 +256,7 @@ class TextBubble(gtk.Widget): self._no_expose = False self.__exposer = None + self._text_layout = None def draw_with_context(self, context): """ @@ -499,5 +575,133 @@ class Mask(gtk.EventBox): gobject.type_register(Mask) +class DragWrapper(object): + """Wrapper to allow gtk widgets to be dragged around""" + def __init__(self, widget, draggable=False, callback=None): + """ + Creates a wrapper to allow gtk widgets to be mouse dragged, if the + parent container supports the move() method, like a gtk.Layout. + + Note: initial position attribute is none, until the widget is moved. + + @param widget the widget to enhance with drag capability + @param draggable wether to enable the drag functionality now + @param callback: a callback for obtaining position changes + if draggable was activated. Callbacks signature should be + cb(x, y). + """ + self._widget = widget + self.container = gtk.EventBox() + self.container.show() + self.container.set_visible_window(False) + self.container.add(widget) + + 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 = None # position of the widget + self._callback = callback + + self.draggable = draggable + + def _pressed_cb(self, widget, evt): + """Callback for start of drag event""" + self.container.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.container, *self.position) + self._widget.parent.queue_draw() + + def _released_cb(self, *args): + """Callback for end of drag (mouse release).""" + self.container.grab_remove() + self._dragging = False + if self._callback: + callback(*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): + if value: + size = self._widget.size_request() + self.container.set_size_request(*size) + self._handles.append(self.container.connect( + "button-press-event", self._pressed_cb)) + self._handles.append(self.container.connect( + "button-release-event", self._released_cb)) + self._handles.append(self.container.connect( + "motion-notify-event", self._moved_cb)) + self._handles.append(self.container.connect( + "grab-broken-event", self._drag_end)) + else: + while self._handles: + handle = self._handles.pop() + self.container.disconnect(handle) + 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) + + +GCONF_COMPOSITING_KEY = '/apps/metacity/general/compositing_manager' + +def _set_wm_compositing(compositing): + """ + Utility method to reset metacity's compostiting. + Compositing allows windows to blend together and avoids flickers + when widgets redraw (like the frame animation). + + @type compositing: bool + @param compositing: Whether to enable metacity compositing feature. + @returns: the new value for compositing + """ + import gconf + client = gconf.client_get_default() + if not client.get_bool(GCONF_COMPOSITING_KEY): + client.set_bool(GCONF_COMPOSITING_KEY, compositing) + return client.get_bool(GCONF_COMPOSITING_KEY) + # vim:set ts=4 sts=4 sw=4 et: diff --git a/tutorius/properties.py b/src/tutorius/properties.py index 5422532..5422532 100644 --- a/tutorius/properties.py +++ b/src/tutorius/properties.py diff --git a/tutorius/service.py b/src/tutorius/service.py index eb246a1..eb246a1 100644 --- a/tutorius/service.py +++ b/src/tutorius/service.py diff --git a/tutorius/services.py b/src/tutorius/services.py index e7b17d8..e7b17d8 100644 --- a/tutorius/services.py +++ b/src/tutorius/services.py diff --git a/tutorius/store.py b/src/tutorius/store.py index 9c8bdff..9c8bdff 100644 --- a/tutorius/store.py +++ b/src/tutorius/store.py diff --git a/tutorius/testwin.py b/src/tutorius/testwin.py index ef92b7f..ef92b7f 100644 --- a/tutorius/testwin.py +++ b/src/tutorius/testwin.py diff --git a/tutorius/textbubble.py b/src/tutorius/textbubble.py index e09b298..e09b298 100644 --- a/tutorius/textbubble.py +++ b/src/tutorius/textbubble.py diff --git a/tutorius/uam/__init__.py b/src/tutorius/uam/__init__.py index bcd67e1..bcd67e1 100644 --- a/tutorius/uam/__init__.py +++ b/src/tutorius/uam/__init__.py diff --git a/tutorius/uam/gobjectparser.py b/src/tutorius/uam/gobjectparser.py index c1fba3d..c1fba3d 100644 --- a/tutorius/uam/gobjectparser.py +++ b/src/tutorius/uam/gobjectparser.py diff --git a/tutorius/uam/gtkparser.py b/src/tutorius/uam/gtkparser.py index ede2f03..ede2f03 100644 --- a/tutorius/uam/gtkparser.py +++ b/src/tutorius/uam/gtkparser.py diff --git a/tutorius/vault.py b/src/tutorius/vault.py index b455a52..b455a52 100644 --- a/tutorius/vault.py +++ b/src/tutorius/vault.py diff --git a/tutorius/viewer.py b/src/tutorius/viewer.py index 272558e..fc86d44 100644 --- a/tutorius/viewer.py +++ b/src/tutorius/viewer.py @@ -68,7 +68,8 @@ class Viewer(object): self.drag_pos = None self.selection = [] - 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() @@ -378,6 +379,7 @@ class Viewer(object): ctx.clip() ctx.paint() + ctx.translate(20, 0) ctx.translate(BLOCK_PADDING, BLOCK_PADDING) painter = self._paint_state(ctx, states) diff --git a/tutorius/actions.py b/tutorius/actions.py deleted file mode 100644 index bb15459..0000000 --- a/tutorius/actions.py +++ /dev/null @@ -1,178 +0,0 @@ -# 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 - -from gettext import gettext as _ - -from sugar.graphics import icon - -from . import addon -from .services import ObjectStore -from .properties import * - -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(TPropContainer): - """Base class for Actions""" - def __init__(self): - TPropContainer.__init__(self) - self.position = (0,0) - self._drag = 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 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, True) - - def exit_editmode(self, **kwargs): - x, y = self._drag.position - self.position = [int(x), int(y)] - self.__edit_img.destroy() - -- cgit v0.9.1