diff options
Diffstat (limited to 'tutorius')
-rw-r--r-- | tutorius/actions.py | 2 | ||||
-rw-r--r-- | tutorius/addon.py | 3 | ||||
-rw-r--r-- | tutorius/creator.py | 273 | ||||
-rw-r--r-- | tutorius/engine.py | 169 | ||||
-rw-r--r-- | tutorius/linear_creator.py | 94 | ||||
-rw-r--r-- | tutorius/properties.py | 16 | ||||
-rw-r--r-- | tutorius/propwidgets.py | 489 | ||||
-rw-r--r-- | tutorius/store.py | 7 | ||||
-rw-r--r-- | tutorius/vault.py | 63 |
9 files changed, 667 insertions, 449 deletions
diff --git a/tutorius/actions.py b/tutorius/actions.py index bb15459..75c9c9b 100644 --- a/tutorius/actions.py +++ b/tutorius/actions.py @@ -43,6 +43,7 @@ class DragWrapper(object): self._handles = [] # event handlers self._dragging = False # whether a drag is in progress self.position = position # position of the widget + self.moved = False self.draggable = draggable @@ -68,6 +69,7 @@ class DragWrapper(object): xparent, yparent = widget.translate_coordinates(widget.parent, xparent, yparent) self.position = (xparent-xrel, yparent-yrel) + self.moved = True self._widget.parent.move(self._eventbox, *self.position) self._widget.parent.move(self._widget, *self.position) self._widget.parent.queue_draw() diff --git a/tutorius/addon.py b/tutorius/addon.py index 30b7a63..6e3d8b9 100644 --- a/tutorius/addon.py +++ b/tutorius/addon.py @@ -28,6 +28,7 @@ __action__ = { "icon" : "hello", "class" : HelloAction, "mandatory_props" : ["text"], + "test" : true, (OPTIONAL) } """ @@ -75,7 +76,7 @@ def list_addons(): global _cache if not _cache: _reload_addons() - return _cache.keys() + return [k for k, v in _cache.items() if 'test' not in v] def get_addon_meta(name): global _cache diff --git a/tutorius/creator.py b/tutorius/creator.py index 906a04e..68c5fa6 100644 --- a/tutorius/creator.py +++ b/tutorius/creator.py @@ -36,6 +36,7 @@ from .services import ObjectStore from .core import State from .tutorial import Tutorial from . import viewer +from .propwidgets import TextInputDialog, StringPropWidget class Creator(object): """ @@ -230,25 +231,7 @@ class Creator(object): meta = addon.get_addon_meta(addonname) for propname in meta['mandatory_props']: prop = getattr(type(event), propname) - if isinstance(prop, properties.TUAMProperty): - selector = WidgetSelector(self._activity) - setattr(event, propname, selector.select()) - elif isinstance(prop, properties.TEventType): - try: - dlg = SignalInputDialog(self._activity, - text="Mandatory property", - field=propname, - addr=event.object_id) - setattr(event, propname, dlg.pop()) - except AttributeError: - pass - elif isinstance(prop, properties.TStringProperty): - dlg = TextInputDialog(self._activity, - text="Mandatory property", - field=propname) - setattr(event, propname, dlg.pop()) - else: - raise NotImplementedError() + prop.widget_class.run_dialog(self._activity, event, propname) event_filters = self._tutorial.get_transition_dict(self._state) @@ -360,6 +343,7 @@ class ToolBox(object): self.tree = gtk.glade.XML(glade_file) self.window = self.tree.get_widget('mainwindow') self._propbox = self.tree.get_widget('propbox') + self._propedits = [] self.window.set_transient_for(parent) @@ -402,74 +386,38 @@ class ToolBox(object): """Refresh property values from the selected action.""" if self._action is None: return - props = self._action._props.keys() - for propnum in xrange(len(props)): - row = self._propbox.get_children()[propnum] - propname = props[propnum] - prop = getattr(type(self._action), propname) - propval = getattr(self._action, propname) - if isinstance(prop, properties.TStringProperty): - propwdg = row.get_children()[1] - propwdg.get_buffer().set_text(propval) - elif isinstance(prop, properties.TUAMProperty): - propwdg = row.get_children()[1] - propwdg.set_label(propval) - elif isinstance(prop, properties.TIntProperty): - propwdg = row.get_children()[1] - propwdg.set_value(propval) - elif isinstance(prop, properties.TArrayProperty): - propwdg = row.get_children()[1] - for i in xrange(len(propval)): - entry = propwdg.get_children()[i] - entry.set_text(str(propval[i])) - else: - propwdg = row.get_children()[1] - propwdg.set_text(str(propval)) + + #Refresh the property editors + for prop in self._propedits: + prop.refresh_widget() def set_action(self, action): """Setter for the action property.""" if self._action is action: self.refresh_properties() return + + #Clear the prop box for old_prop in self._propbox.get_children(): self._propbox.remove(old_prop) + self._propedits = [] + self._action = action if action is None: return for propname in action._props.keys(): row = gtk.HBox() + #Label row.pack_start(gtk.Label(T(propname)), False, False, 10) + + #Value field prop = getattr(type(action), propname) - propval = getattr(action, propname) - if isinstance(prop, properties.TStringProperty): - propwdg = gtk.TextView() - propwdg.get_buffer().set_text(propval) - propwdg.connect_after("focus-out-event", \ - self._str_prop_changed, action, propname) - elif isinstance(prop, properties.TUAMProperty): - propwdg = gtk.Button(propval) - propwdg.connect_after("clicked", \ - self._uam_prop_changed, action, propname) - elif isinstance(prop, properties.TIntProperty): - adjustment = gtk.Adjustment(value=propval, - lower=prop.lower_limit.limit, - upper=prop.upper_limit.limit, - step_incr=1) - propwdg = gtk.SpinButton(adjustment=adjustment) - propwdg.connect_after("focus-out-event", \ - self._int_prop_changed, action, prop) - elif isinstance(prop, properties.TArrayProperty): - propwdg = gtk.HBox() - for i in xrange(len(propval)): - entry = gtk.Entry() - propwdg.pack_start(entry) - entry.connect_after("focus-out-event", \ - self._list_prop_changed, action, propname, i) - else: - propwdg = gtk.Entry() - propwdg.set_text(str(propval)) - row.pack_end(propwdg) + propedit = prop.widget_class(self.__parent, action, propname, self._refresh_action_cb) + self._propedits.append(propedit) + row.pack_end(propedit.widget) + + #Add row self._propbox.pack_start(row, expand=False) self._propbox.show_all() self.refresh_properties() @@ -480,187 +428,10 @@ class ToolBox(object): action = property(fset=set_action, fget=get_action, doc=\ "Action to be edited through introspection.") - def _list_prop_changed(self, widget, evt, action, propname, idx): - try: - #Save props as tuples so that they can be hashed - attr = list(getattr(action, propname)) - attr[idx] = int(widget.get_text()) - setattr(action, propname, tuple(attr)) - except ValueError: - widget.set_text(str(getattr(action, propname)[idx])) - self.__parent._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) - 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) - 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) - - -class WidgetSelector(object): - """ - Allow selecting a widget from within a window without interrupting the - flow of the current call. - - The selector will run on the specified window until either a widget - is selected or abort() gets called. - """ - def __init__(self, window): - super(WidgetSelector, self).__init__() - self.window = window - self._intro_mask = None - self._intro_handle = None - self._select_handle = None - self._prelight = None + def _refresh_action_cb(self): + if self._action is not None: + self.__parent._creator._action_refresh_cb(None, None, self._action) - def select(self): - """ - Starts selecting a widget, by grabbing control of the mouse and - highlighting hovered widgets until one is clicked. - @returns: a widget address or None - """ - if not self._intro_mask: - self._prelight = None - self._intro_mask = overlayer.Mask(catch_events=True) - self._select_handle = self._intro_mask.connect_after( - "button-press-event", self._end_introspect) - self._intro_handle = self._intro_mask.connect_after( - "motion-notify-event", self._intro_cb) - self.window._overlayer.put(self._intro_mask, 0, 0) - self.window._overlayer.queue_draw() - - while bool(self._intro_mask) and not gtk.main_iteration(): - pass - - return gtkutils.raddr_lookup(self._prelight) - - def _end_introspect(self, widget, evt): - if evt.type == gtk.gdk.BUTTON_PRESS and self._prelight: - self._intro_mask.catch_events = False - self._intro_mask.disconnect(self._intro_handle) - self._intro_handle = None - self._intro_mask.disconnect(self._select_handle) - self._select_handle = None - self.window._overlayer.remove(self._intro_mask) - self._intro_mask = None - # for some reason, gtk may not redraw after this unless told to. - self.window.queue_draw() - - def _intro_cb(self, widget, evt): - """ - Callback for capture of widget events, when in introspect mode. - """ - # widget has focus, let's hilight it - win = gtk.gdk.display_get_default().get_window_at_pointer() - if not win: - return - click_wdg = win[0].get_user_data() - if not click_wdg.is_ancestor(self.window._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._prelight = click_wdg - - self.window.queue_draw() - - def abort(self): - """ - Ends the selection. The control will return to the select() caller - with a return value of None, as selection was aborted. - """ - self._intro_mask.catch_events = False - self._intro_mask.disconnect(self._intro_handle) - self._intro_handle = None - self._intro_mask.disconnect(self._select_handle) - self._select_handle = None - self.window._overlayer.remove(self._intro_mask) - self._intro_mask = None - self._prelight = None - -class SignalInputDialog(gtk.MessageDialog): - def __init__(self, parent, text, field, addr): - """ - Create a gtk signal selection dialog. - - @param parent: the parent window this dialog should stay over. - @param text: the title of the dialog. - @param field: the field description of the dialog. - @param addr: the widget address from which to fetch signal list. - """ - gtk.MessageDialog.__init__(self, parent, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_QUESTION, - gtk.BUTTONS_OK, - None) - self.set_markup(text) - self.model = gtk.ListStore(str) - widget = gtkutils.find_widget(parent, addr) - for signal_name in gobject.signal_list_names(widget): - self.model.append(row=(signal_name,)) - self.entry = gtk.ComboBox(self.model) - cell = gtk.CellRendererText() - self.entry.pack_start(cell) - self.entry.add_attribute(cell, 'text', 0) - hbox = gtk.HBox() - lbl = gtk.Label(field) - hbox.pack_start(lbl, False) - hbox.pack_end(self.entry) - self.vbox.pack_end(hbox, True, True) - self.show_all() - - def pop(self): - """ - Show the dialog. It will run in it's own loop and return control - to the caller when a signal has been selected. - - @returns: a signal name or None if no signal was selected - """ - self.run() - self.hide() - iter = self.entry.get_active_iter() - if iter: - text = self.model.get_value(iter, 0) - return text - return None - - def _dialog_done_cb(self, entry, response): - self.response(response) - -class TextInputDialog(gtk.MessageDialog): - def __init__(self, parent, text, field): - gtk.MessageDialog.__init__(self, parent, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_QUESTION, - gtk.BUTTONS_OK, - None) - self.set_markup(text) - self.entry = gtk.Entry() - self.entry.connect("activate", self._dialog_done_cb, gtk.RESPONSE_OK) - hbox = gtk.HBox() - lbl = gtk.Label(field) - hbox.pack_start(lbl, False) - hbox.pack_end(self.entry) - self.vbox.pack_end(hbox, True, True) - self.show_all() - - def pop(self): - self.run() - self.hide() - text = self.entry.get_text() - return text - - 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. diff --git a/tutorius/engine.py b/tutorius/engine.py index be0b935..34616f6 100644 --- a/tutorius/engine.py +++ b/tutorius/engine.py @@ -37,7 +37,6 @@ RUNNER_STATE_SETUP_EVENTS = 2 RUNNER_STATE_AWAITING_NOTIFICATIONS = 3 RUNNER_STATE_UNINSTALLING_ACTIONS = 4 RUNNER_STATE_UNSUBSCRIBING_EVENTS = 5 -RUNNER_STATE_STOPPED = 6 LOGGER = logging.getLogger("sugar.tutorius.engine") @@ -82,7 +81,9 @@ class TutorialRunner(object): #Temp FIX until event, actions have an activity id def setCurrentActivity(self): self._pM.currentActivity = self._activity_id - + + ########################################################################### + # Incoming messages def start(self): self.setCurrentActivity() #Temp Hack until activity in events/actions self.enterState(self._tutorial.INIT) @@ -94,10 +95,78 @@ class TutorialRunner(object): else: self._execute_stop() + def action_installed(self, action_name, action, address): + LOGGER.debug("TutorialRunner :: Action %s received address %s"%(action_name, address)) + self._installed_actions[action_name] = address + # Verify if we just completed the installation of the actions for this state + self._verify_action_install_state() + + def install_error(self, action_name, action, exception): + # TODO : Fix this as it doesn't warn the user about the problem or anything + LOGGER.debug("TutorialRunner :: Action could not be installed %s, exception was : %s"%(str(action), str(exception))) + self._installation_errors[action_name] = exception + self._verify_action_install_state() + + def event_subscribed(self, event_name, event_address): + LOGGER.debug("TutorialRunner :: Event %s was subscribed to, located at address %s"%(event_name, event_address)) + self._subscribed_events[event_name] = event_address + + # Verify if we just completed the subscription of all the events for this state + self._verify_event_install_state() + + def subscribe_error(self, event_name, exception): + # TODO : Do correct error handling here + LOGGER.debug("TutorialRunner :: Could not subscribe to event %s, got exception : %s"%(event_name, str(exception))) + self._subscription_errors[event_name] = exception + + # Verify if we just completed the subscription of all the events for this state + self._verify_event_install_state() + + def all_actions_installed(self): + self._runner_state = RUNNER_STATE_SETUP_EVENTS + # Process the messages that might have been stored + self._process_pending_messages() + + # If we processed a message that changed the runner state, we need to stop + # processing + if self._runner_state != RUNNER_STATE_SETUP_EVENTS: + return + + # Start subscribing to events + transitions = self._tutorial.get_transition_dict(self._state) + + # If there are no transitions, raise the All Events Subscribed message + if len(transitions) == 0: + self.all_events_subscribed() + return + + # Send all the event registration + for (event_name, (event, next_state)) in transitions.items(): + self._pM.subscribe(event_name, event, + save_args(self._handleEvent, next_state), + save_args(self.event_subscribed, event_name), + save_args(self.subscribe_error, event_name)) + + def all_events_subscribed(self): + self._runner_state = RUNNER_STATE_AWAITING_NOTIFICATIONS + self._process_pending_messages() + + def _uninstall_actions(self): + self._runner_state = RUNNER_STATE_UNINSTALLING_ACTIONS + self._remove_installed_actions() + self._execute_stop() + + def _unsubscribe_events(self): + self._runner_state = RUNNER_STATE_UNSUBSCRIBING_EVENTS + self._remove_subscribed_events() + self._uninstall_actions() + + ########################################################################### + # Helper functions def _execute_stop(self): self.setCurrentActivity() #Temp Hack until activity in events/actions self._state = None - self._runner_state = RUNNER_STATE_STOPPED + self._runner_state = RUNNER_STATE_IDLE def _handleEvent(self, next_state, event): # Look if we are actually receiving notifications @@ -136,19 +205,7 @@ class TutorialRunner(object): self._subscribed_events.clear() self._subscription_errors.clear() - def __action_installed(self, action_name, action, address): - LOGGER.debug("TutorialRunner :: Action %s received address %s"%(action_name, address)) - self._installed_actions[action_name] = address - # Verify if we just completed the installation of the actions for this state - self.__verify_action_install_state() - - def __install_error(self, action_name, action, exception): - # TODO : Fix this as it doesn't warn the user about the problem or anything - LOGGER.debug("TutorialRunner :: Action could not be installed %s, exception was : %s"%(str(action), str(exception))) - self._installation_errors[action_name] = exception - self.__verify_action_install_state() - - def __verify_action_install_state(self): + def _verify_action_install_state(self): # Do the check to see if we have finished installing all the actions by either having # received a address for it or an error message install_complete = True @@ -162,24 +219,9 @@ class TutorialRunner(object): if install_complete: LOGGER.debug("TutorialRunner :: All actions installed!") # Raise the All Actions Installed event for the TutorialRunner state - self.__all_actions_installed() + self.all_actions_installed() - def __event_subscribed(self, event_name, event_address): - LOGGER.debug("TutorialRunner :: Event %s was subscribed to, located at address %s"%(event_name, event_address)) - self._subscribed_events[event_name] = event_address - - # Verify if we just completed the subscription of all the events for this state - self.__verify_event_install_state() - - def __subscribe_error(self, event_name, exception): - # TODO : Do correct error handling here - LOGGER.debug("TutorialRunner :: Could not subscribe to event %s, got exception : %s"%(event_name, str(exception))) - self._subscription_errors[event_name] = exception - - # Verify if we just completed the subscription of all the events for this state - self.__verify_event_install_state() - - def __verify_event_install_state(self): + def _verify_event_install_state(self): transitions = self._tutorial.get_transition_dict(self._state) # Check to see if we completed all the event subscriptions @@ -192,48 +234,9 @@ class TutorialRunner(object): if subscribe_complete: LOGGER.debug("TutorialRunner : Subscribed to all events!") - self.__all_events_subscribed() - - def __all_actions_installed(self): - self._runner_state = RUNNER_STATE_SETUP_EVENTS - # Process the messages that might have been stored - self.__process_pending_messages() + self.all_events_subscribed() - # If we processed a message that changed the runner state, we need to stop - # processing - if self._runner_state != RUNNER_STATE_SETUP_EVENTS: - return - - # Start subscribing to events - transitions = self._tutorial.get_transition_dict(self._state) - - # If there are no transitions, raise the All Events Subscribed message - if len(transitions) == 0: - self.__all_events_subscribed() - return - - # Send all the event registration - for (event_name, (event, next_state)) in transitions.items(): - self._pM.subscribe(event_name, event, - save_args(self._handleEvent, next_state), - save_args(self.__event_subscribed, event_name), - save_args(self.__subscribe_error, event_name)) - - def __all_events_subscribed(self): - self._runner_state = RUNNER_STATE_AWAITING_NOTIFICATIONS - self.__process_pending_messages() - - def __uninstall_actions(self): - self._runner_state = RUNNER_STATE_UNINSTALLING_ACTIONS - self._remove_installed_actions() - self._execute_stop() - - def __unsubscribe_events(self): - self._runner_state = RUNNER_STATE_UNSUBSCRIBING_EVENTS - self._remove_subscribed_events() - self.__uninstall_actions() - - def __process_pending_messages(self): + def _process_pending_messages(self): while len(self._message_queue) != 0: (priority, message) = heappop(self._message_queue) @@ -245,9 +248,9 @@ class TutorialRunner(object): # Start removing the installed addons if self._runner_state == RUNNER_STATE_AWAITING_NOTIFICATIONS: # Start uninstalling the events - self.__unsubscribe_events() + self._unsubscribe_events() if self._runner_state == RUNNER_STATE_SETUP_EVENTS: - self.__uninstall_actions() + self._uninstall_actions() elif priority == EVENT_NOTIFICATION_MSG_PRIORITY: LOGGER.debug("TutorialRunner :: Handling stored event notification for next_state %s"%message[0]) self._handle_event(*message) @@ -259,22 +262,22 @@ class TutorialRunner(object): self._actions = self._tutorial.get_action_dict(self._state) if len(self._actions) == 0: - self.__all_actions_installed() + self.all_actions_installed() return for (action_name, action) in self._actions.items(): LOGGER.debug("TutorialRunner :: Installed action %s"%(action_name)) self._pM.install(action, - save_args(self.__action_installed, action_name), - save_args(self.__install_error, action_name)) + save_args(self.action_installed, action_name), + save_args(self.install_error, action_name)) def enterState(self, state_name): """ - Starting from the state_name, the runner execute states until - no automatic transition are found and will wait for an external - event to occur. + Starting from the state_name, the runner execute states from the + tutorial until no automatic transitions are found and will wait + for an external event to occur. - When entering the state, actions and events from the previous + When entering the sate, actions and events from the previous state are respectively uninstalled and unsubscribed and actions and events from the state_name will be installed and subscribed. diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py deleted file mode 100644 index f664c49..0000000 --- a/tutorius/linear_creator.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Greatly influenced by sugar/activity/namingalert.py -# -# 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 - -from copy import deepcopy - -from .core import * -from .actions import * -from .filters import * - -class LinearCreator(object): - """ - This class is used to create a FSM from a linear sequence of orders. The - orders themselves are meant to be either an action or a transition. - """ - - def __init__(self): - self.fsm = FiniteStateMachine("Sample Tutorial") - self.current_actions = [] - self.nb_state = 0 - self.state_name = "INIT" - - def set_name(self, name): - """ - Sets the name of the generated FSM. - """ - self.fsm.name = name - - def action(self, action): - """ - Adds an action to execute in the current state. - """ - self.current_actions.append(action) - - def event(self, event_filter): - """ - Adds a transition to another state. When executing this, all the actions - previously called will be bundled in a single state, with the exit - condition of this state being the transition just added. - - Whatever the name of the next state you inserted in the event, it will - be replaced to point to the next event in the line. - """ - if len(self.current_actions) != 0: - # Set the next state name - there is no way the caller should have - # to deal with that. - next_state_name = "State %d" % (self.nb_state+1) - state = State(self.state_name, action_list=self.current_actions, - event_filter_list=[(event_filter, next_state_name),]) - self.state_name = next_state_name - - self.nb_state += 1 - self.fsm.add_state(state) - - # Clear the actions from the list - self.current_actions = [] - - def generate_fsm(self): - """ - Returns a finite state machine corresponding to the sequence of calls - that were made from this point on. - """ - # Copy the whole FSM that was generated yet - new_fsm = deepcopy(self.fsm) - - # Generate the final state - state = None - if len(self.current_actions) != 0: - state = State("State" + str(self.nb_state), action_list=self.current_actions) - # Don't increment the nb_state here - we would break the linearity - # because we might generate more stuff with this creator later. - # Since we rely on linearity for continuity when generating the - # next state's name on an event filter, we cannot increment here. - else: - state = State("State" + str(self.nb_state)) - - # Insert the state in the copy of the FSM - new_fsm.add_state(state) - - return new_fsm - diff --git a/tutorius/properties.py b/tutorius/properties.py index 6bd16ee..a462782 100644 --- a/tutorius/properties.py +++ b/tutorius/properties.py @@ -27,6 +27,13 @@ from .constraints import Constraint, \ ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint, \ ResourceConstraint +from .propwidgets import PropWidget, \ + StringPropWidget, \ + UAMPropWidget, \ + EventTypePropWidget, \ + IntPropWidget, \ + FloatPropWidget, \ + IntArrayPropWidget class TPropContainer(object): """ @@ -148,6 +155,7 @@ class TutoriusProperty(object): get_contraints() : the constraints inserted on this property. They define what is acceptable or not as values. """ + widget_class = PropWidget def __init__(self): super(TutoriusProperty, self).__init__() self.type = None @@ -192,7 +200,7 @@ class TIntProperty(TutoriusProperty): Represents an integer. Can have an upper value limit and/or a lower value limit. """ - + widget_class = IntPropWidget def __init__(self, value, lower_limit=None, upper_limit=None): TutoriusProperty.__init__(self) self.type = "int" @@ -206,6 +214,7 @@ class TFloatProperty(TutoriusProperty): Represents a floating point number. Can have an upper value limit and/or a lower value limit. """ + widget_class = FloatPropWidget def __init__(self, value, lower_limit=None, upper_limit=None): TutoriusProperty.__init__(self) self.type = "float" @@ -219,6 +228,7 @@ class TStringProperty(TutoriusProperty): """ Represents a string. Can have a maximum size limit. """ + widget_class = StringPropWidget def __init__(self, value, size_limit=None): TutoriusProperty.__init__(self) self.type = "string" @@ -231,6 +241,7 @@ class TArrayProperty(TutoriusProperty): Represents an array of properties. Can have a maximum number of element limit, but there are no constraints on the content of the array. """ + widget_class = IntArrayPropWidget def __init__(self, value, min_size_limit=None, max_size_limit=None): TutoriusProperty.__init__(self) self.type = "array" @@ -250,6 +261,7 @@ class TArrayProperty(TutoriusProperty): min_size_limit=self.min_size_limit.limit, value=self.value, ) + class TColorProperty(TutoriusProperty): """ Represents a RGB color with 3 8-bit integer values. @@ -359,6 +371,7 @@ class TUAMProperty(TutoriusProperty): """ Represents a widget of the interface by storing its UAM. """ + widget_class = UAMPropWidget def __init__(self, value=None): TutoriusProperty.__init__(self) @@ -391,6 +404,7 @@ class TEventType(TutoriusProperty): """ Represents an GUI signal for a widget. """ + widget_class = EventTypePropWidget def __init__(self, value): super(TEventType, self).__init__() self.type = "gtk-signal" diff --git a/tutorius/propwidgets.py b/tutorius/propwidgets.py new file mode 100644 index 0000000..7e78ba4 --- /dev/null +++ b/tutorius/propwidgets.py @@ -0,0 +1,489 @@ +# 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 +""" +Property Widgets. + +Allows displaying properties cleanly. +""" +import gtk +import gobject + +from . import gtkutils, overlayer +########################################################################### +# Dialog classes +########################################################################### +class TextInputDialog(gtk.MessageDialog): + def __init__(self, parent, text, field): + gtk.MessageDialog.__init__(self, parent, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, + gtk.BUTTONS_OK, + None) + self.set_markup(text) + self.entry = gtk.Entry() + self.entry.connect("activate", self._dialog_done_cb, gtk.RESPONSE_OK) + hbox = gtk.HBox() + lbl = gtk.Label(field) + hbox.pack_start(lbl, False) + hbox.pack_end(self.entry) + self.vbox.pack_end(hbox, True, True) + self.show_all() + + def pop(self): + self.run() + self.hide() + text = self.entry.get_text() + return text + + def _dialog_done_cb(self, entry, response): + self.response(response) + +class SignalInputDialog(gtk.MessageDialog): + def __init__(self, parent, text, field, addr): + """ + Create a gtk signal selection dialog. + + @param parent: the parent window this dialog should stay over. + @param text: the title of the dialog. + @param field: the field description of the dialog. + @param addr: the widget address from which to fetch signal list. + """ + gtk.MessageDialog.__init__(self, parent, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, + gtk.BUTTONS_OK, + None) + self.set_markup(text) + self.model = gtk.ListStore(str) + widget = gtkutils.find_widget(parent, addr) + for signal_name in gobject.signal_list_names(widget): + self.model.append(row=(signal_name,)) + self.entry = gtk.ComboBox(self.model) + cell = gtk.CellRendererText() + self.entry.pack_start(cell) + self.entry.add_attribute(cell, 'text', 0) + hbox = gtk.HBox() + lbl = gtk.Label(field) + hbox.pack_start(lbl, False) + hbox.pack_end(self.entry) + self.vbox.pack_end(hbox, True, True) + self.show_all() + + def pop(self): + """ + Show the dialog. It will run in it's own loop and return control + to the caller when a signal has been selected. + + @returns: a signal name or None if no signal was selected + """ + self.run() + self.hide() + iter = self.entry.get_active_iter() + if iter: + text = self.model.get_value(iter, 0) + return text + return None + + def _dialog_done_cb(self, entry, response): + self.response(response) + +class WidgetSelector(object): + """ + Allow selecting a widget from within a window without interrupting the + flow of the current call. + + The selector will run on the specified window until either a widget + is selected or abort() gets called. + """ + def __init__(self, window): + super(WidgetSelector, self).__init__() + self.window = window + self._intro_mask = None + self._intro_handle = None + self._select_handle = None + self._prelight = None + + def select(self): + """ + Starts selecting a widget, by grabbing control of the mouse and + highlighting hovered widgets until one is clicked. + @returns: a widget address or None + """ + if not self._intro_mask: + self._prelight = None + self._intro_mask = overlayer.Mask(catch_events=True) + self._select_handle = self._intro_mask.connect_after( + "button-press-event", self._end_introspect) + self._intro_handle = self._intro_mask.connect_after( + "motion-notify-event", self._intro_cb) + self.window._overlayer.put(self._intro_mask, 0, 0) + self.window._overlayer.queue_draw() + + while bool(self._intro_mask) and not gtk.main_iteration(): + pass + + return gtkutils.raddr_lookup(self._prelight) + + def _end_introspect(self, widget, evt): + if evt.type == gtk.gdk.BUTTON_PRESS and self._prelight: + self._intro_mask.catch_events = False + self._intro_mask.disconnect(self._intro_handle) + self._intro_handle = None + self._intro_mask.disconnect(self._select_handle) + self._select_handle = None + self.window._overlayer.remove(self._intro_mask) + self._intro_mask = None + # for some reason, gtk may not redraw after this unless told to. + self.window.queue_draw() + + def _intro_cb(self, widget, evt): + """ + Callback for capture of widget events, when in introspect mode. + """ + # widget has focus, let's hilight it + win = gtk.gdk.display_get_default().get_window_at_pointer() + if not win: + return + click_wdg = win[0].get_user_data() + if not click_wdg.is_ancestor(self.window._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._prelight = click_wdg + + self.window.queue_draw() + + def abort(self): + """ + Ends the selection. The control will return to the select() caller + with a return value of None, as selection was aborted. + """ + self._intro_mask.catch_events = False + self._intro_mask.disconnect(self._intro_handle) + self._intro_handle = None + self._intro_mask.disconnect(self._select_handle) + self._select_handle = None + self.window._overlayer.remove(self._intro_mask) + self._intro_mask = None + self._prelight = None + +########################################################################### +# Property Widget Classes +########################################################################### +class PropWidget(object): + """ + Base Class for property editing widgets. + Subclasses should implement create_widget, run_dialog and refresh_widget + """ + def __init__(self, parent, edit_object, prop_name, changed_callback=None): + """Constructor + @param parent parent widget + @param edit_object TPropContainer being edited + @param prop_name name of property being edited + @param changed_callback optional callable to call on value changes + """ + self._parent = parent + self._edit_object = edit_object + self._propname = prop_name + self._widget = None + self._changed_cb = changed_callback + + ############################################################ + # Begin Properties + ############################################################ + def set_objprop(self, value): + """Setter for object property value""" + setattr(self._edit_object, self._propname, value) + def get_objprop(self): + """Getter for object property value""" + return getattr(self._edit_object, self._propname) + def _get_widget(self): + """Getter for widget. Creates the widget if necessary""" + if self._widget is None: + self._widget = self.create_widget(self.obj_prop) + return self._widget + def _get_prop_class(self): + """Getter for property type""" + return getattr(type(self._edit_object), self._propname) + def _get_parent(self): + """Getter for parent""" + return self._parent + + obj_prop = property(get_objprop, set_objprop) + widget = property(_get_widget) + prop_class = property(_get_prop_class) + parent = property(_get_parent) + + ############################################################ + # End Properties + ############################################################ + + def notify(self): + """Notify a calling object that the property was changed""" + if self._changed_cb: + self._changed_cb() + + ############################################################ + # Public Interface -- Redefine those function in subclasses + ############################################################ + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + widget = gtk.Entry() + widget.set_text(str(init_value or "")) + return widget + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + raise NotImplementedError() + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + pass + +class StringPropWidget(PropWidget): + """ + Allows editing a str property + """ + @classmethod + def _extract_value(cls, widget): + """ + Class Method + extracts the value from the widget + """ + buf = widget.get_buffer() + return cls._from_text( + buf.get_text(buf.get_start_iter(), buf.get_end_iter()) + ) + + @classmethod + def _from_text(cls, text): + """ + Class Method + transforms the text value into the correct type if required + """ + return text + + def _text_changed(self, widget, evt): + """callback for text change event in the edit box""" + self.obj_prop = self._extract_value(widget) + self.notify() + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + propwdg = gtk.TextView() + propwdg.get_buffer().set_text(init_value or "") + propwdg.connect_after("focus-out-event", \ + self._text_changed) + + return propwdg + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + self.widget.get_buffer().set_text(str(self.obj_prop)) #unicode() ? + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + dlg = TextInputDialog(parent, + text="Mandatory property", + field=propname) + setattr(obj_prop, propname, cls._from_text(dlg.pop())) + +class IntPropWidget(StringPropWidget): + """ + Allows editing an int property with boundaries + """ + @classmethod + def _extract_value(cls, widget): + """ + Class Method + extracts the value from the widget + """ + return widget.get_value_as_int() + + @classmethod + def _from_text(cls, text): + """ + Class Method + transforms the text value into the correct type if required + """ + return int(text) + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + prop = self.prop_class + adjustment = gtk.Adjustment(value=self.obj_prop, + lower=prop.lower_limit.limit, + upper=prop.upper_limit.limit, + step_incr=1) + propwdg = gtk.SpinButton(adjustment=adjustment) + propwdg.connect_after("focus-out-event", \ + self._text_changed) + +class FloatPropWidget(StringPropWidget): + """Allows editing a float property""" + @classmethod + def _from_text(cls, text): + """ + Class Method + transforms the text value into the correct type if required + """ + return float(text) + +class UAMPropWidget(PropWidget): + """Allows editing an UAM property with a widget chooser""" + def _show_uam_chooser(self, widget): + """show the UAM chooser""" + selector = WidgetSelector(self.parent) + self.obj_prop = selector.select() + self.notify() + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + propwdg = gtk.Button(self.obj_prop) + propwdg.connect_after("clicked", self._show_uam_chooser) + return propwdg + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + self.widget.set_label(self.obj_prop) + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + selector = WidgetSelector(parent) + value = selector.select() + setattr(obj_prop, propname, selector.select()) + +class EventTypePropWidget(PropWidget): + """Allows editing an EventType property""" + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + self.widget.set_text(str(self.obj_prop)) + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + try: + dlg = SignalInputDialog(parent, + text="Mandatory property", + field=propname, + addr=obj_prop.object_id) + setattr(obj_prop, propname, dlg.pop()) + except AttributeError: + return + +class IntArrayPropWidget(PropWidget): + """Allows editing an array of ints property""" + def _item_changed(self, widget, evt, idx): + """callback for text changed in one of the entries""" + try: + #Save props as tuples so that they can be hashed + attr = list(self.obj_prop) + attr[idx] = int(widget.get_text()) + self.obj_prop = tuple(attr) + except ValueError: + widget.set_text(str(self.obj_prop[idx])) + self.notify() + + def create_widget(self, init_value=None): + """ + Create the Edit Widget for a property + @param init_value initial value + @return gtk.Widget + """ + value = self.obj_prop + propwdg = gtk.HBox() + for i in xrange(len(value)): + entry = gtk.Entry() + entry.set_text(str(value[i])) + propwdg.pack_start(entry) + entry.connect_after("focus-out-event", \ + self._item_changed, i) + return propwdg + + def refresh_widget(self): + """ + Force the widget to update it's value in case the property has changed + """ + children = self.widget.get_children() + value = self.obj_prop + for i in xrange(len(value)): + children[i].set_text(str(value[i])) + + @classmethod + def run_dialog(cls, parent, obj_prop, propname): + """ + Class Method. + Prompts the user for changing an object's property + @param parent widget + @param obj_prop TPropContainer to edit + @param propname name of property to edit + """ + pass diff --git a/tutorius/store.py b/tutorius/store.py index dc52c82..565295d 100644 --- a/tutorius/store.py +++ b/tutorius/store.py @@ -343,10 +343,10 @@ class StoreProxy(object): xml_response = minidom.parseString(response['body']) - id_node = xml_response.getElementsByTagName("id")[0] + id_node = xml_response.getElementsByTagName("id")[0].firstChild + + id = id_node.nodeValue - id = id_node.getAttribute('value') - return id def unpublish(self, tutorial_store_id): @@ -423,7 +423,6 @@ class StoreProxyHelper(object): @param response The XML response from the server @return True if the response is an error """ - # first look for HTTP errors http_status = response['headers']['status'] diff --git a/tutorius/vault.py b/tutorius/vault.py index dc8c434..1c1e33c 100644 --- a/tutorius/vault.py +++ b/tutorius/vault.py @@ -56,6 +56,7 @@ INI_GUID_PROPERTY = "guid" INI_NAME_PROPERTY = "name" INI_XML_FSM_PROPERTY = "fsm_filename" INI_VERSION_PROPERTY = 'version' +INI_CATEGORY_PROPERTY = 'category' INI_FILENAME = "meta.ini" TUTORIAL_FILENAME = "tutorial.xml" RESOURCES_FOLDER = 'resources' @@ -196,7 +197,6 @@ class Vault(object): @returns a list of Tutorial meta-data (TutorialID, Description, Rating, Category, PublishState, etc...) - TODO : Search for tuto caracterised by the entry : OR between [], and between each The returned dictionnary is of this format : key = property name, value = property value The dictionnary also contain one dictionnary element whose key is the string 'activities' @@ -204,25 +204,25 @@ class Vault(object): value = related activity version number """ - # Temp solution for returning all tutorials metadata - tutorial_list = [] tuto_guid_list = [] ini_file = SafeConfigParser() - if keyword == [] and relatedActivityNames == [] and category == []: - # get all tutorials tuples (name:guid) for all activities and version - tuto_dict = Vault.list_available_tutorials() - for id in tuto_dict.keys(): - tuto_guid_list.append(id) + # get all tutorials tuples (name:guid) for all activities and version + tuto_dict = Vault.list_available_tutorials() + for id in tuto_dict.keys(): + tuto_guid_list.append(id) - # Find .ini metadata files with the guid list + # Find .ini metadata files with the guid list # Get the guid from the tuto tuples for guid in tuto_guid_list: + addition_flag = True # Create a dictionnary containing the metadata and also - # another dictionnary containing the tutorial Related Acttivities, + # another dictionnary containing the tutorial Related Activities, # and add it to a list + ini_file = SafeConfigParser() + # Create a TutorialBundler object from the guid bundler = TutorialBundler(guid) # Find the .ini file path for this guid @@ -233,6 +233,7 @@ class Vault(object): metadata_dictionnary = {} related_act_dictionnary = {} metadata_list = ini_file.options(INI_METADATA_SECTION) + for metadata_name in metadata_list: # Create a dictionnary of tutorial metadata metadata_dictionnary[metadata_name] = ini_file.get(INI_METADATA_SECTION, metadata_name) @@ -245,8 +246,43 @@ class Vault(object): # Add Related Activities dictionnary to metadata dictionnary metadata_dictionnary['activities'] = related_act_dictionnary - # Add this dictionnary to tutorial list - tutorial_list.append(metadata_dictionnary) + # Filter tutorials for keyword (full or partial) + if keyword != []: + addition_flag = False + # Check if at least one keyword of the list is present + for key in keyword: + if key != None: + for value in metadata_dictionnary.values(): + if isinstance(value, str): + if value.lower().count(key.lower()) > 0: + addition_flag = True + # Check one layer of depth in the metadata to find the keyword + # (for exemple, related activites are a dictionnary stocked + # in a value of the main dictionnary) + elif isinstance(value, dict): + for inner_key, inner_value in value.items(): + if isinstance(inner_value, str) and isinstance(inner_key, str) and (inner_value.lower().count(key.lower()) > 0 or inner_key.count(key.lower()) > 0): + addition_flag = True + + # Filter tutorials for related activities + if relatedActivityNames != []: + addition_flag = False + # Check if at least one element of the list is present + for related in relatedActivityNames: + if related != None and related.lower() in related_act_dictionnary.keys(): + addition_flag = True + + # Filter tutorials for categories + if category != []: + addition_flag = False + # Check if at least one element of the list is present + for cat in category: + if cat != None and metadata_dictionnary.has_key(INI_CATEGORY_PROPERTY) and metadata_dictionnary[INI_CATEGORY_PROPERTY].lower() == cat.lower(): + addition_flag = True + + # Add this dictionnary to tutorial list if it has not been filtered + if addition_flag == True: + tutorial_list.append(metadata_dictionnary) # Return tutorial list return tutorial_list @@ -319,9 +355,6 @@ class Vault(object): else: # Error, tutorial already exist return False - - # TODO : wait for Ben input on how to unpublish tuto before coding this function - # For now, no unpublishing will occur. @staticmethod |