diff --git a/src/sugar3/activity/activity.py b/src/sugar3/activity/activity.py index 7b7d21ed..5953ec64 100644 --- a/src/sugar3/activity/activity.py +++ b/src/sugar3/activity/activity.py @@ -1,40 +1,3 @@ -''' -Base class for activities written in Python -=========================================== - -This is currently the only definitive reference for what an -activity must do to participate in the Sugar desktop. - -A Basic Activity ----------------- - -All activities must implement a class derived from 'Activity' in this class. -The convention is to call it ActivitynameActivity, but this is not required as -the activity.info file associated with your activity will tell the sugar-shell -which class to start. - -For example the most minimal Activity: - -.. code-block:: python - - from sugar3.activity import activity - - class ReadActivity(activity.Activity): - pass - -To get a real, working activity, you will at least have to implement: - -__init__(), :func:`sugar3.activity.activity.Activity.read_file()` and -:func:`sugar3.activity.activity.Activity.write_file()` - -Aditionally, you will probably need a at least a Toolbar so you can have some -interesting buttons for the user, like for example 'exit activity' - -See the methods of the Activity class below for more information on what you -will need for a real activity. - -.. note:: This API is STABLE. -''' # Copyright (C) 2006-2007 Red Hat, Inc. # Copyright (C) 2007-2009 One Laptop Per Child # Copyright (C) 2010 Collabora Ltd. @@ -54,6 +17,147 @@ will need for a real activity. # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. +''' +Activity +======== + +A definitive reference for what a Sugar Python activity must do to +participate in the Sugar desktop. + +.. note:: This API is STABLE. + +The :class:`Activity` class is used to derive all Sugar Python +activities. This is where your activity starts. + +**Derive from the class** + + .. code-block:: python + + from sugar3.activity.activity import Activity + + class MyActivity(Activity): + def __init__(self, handle): + Activity.__init__(self, handle) + + An activity must implement a new class derived from + :class:`Activity`. + + Name the new class `MyActivity`, where `My` is the name of your + activity. Use bundle metadata to tell Sugar to instantiate this + class. See :class:`~sugar3.bundle` for bundle metadata. + +**Create a ToolbarBox** + + In your :func:`__init__` method create a + :class:`~sugar3.graphics.toolbarbox.ToolbarBox`, with an + :class:`~sugar3.activity.widgets.ActivityToolbarButton`, a + :class:`~sugar3.activity.widgets.StopButton`, and then call + :func:`~sugar3.graphics.window.Window.set_toolbar_box`. + + .. code-block:: python + :emphasize-lines: 2-4,10- + + from sugar3.activity.activity import Activity + from sugar3.graphics.toolbarbox import ToolbarBox + from sugar3.activity.widgets import ActivityToolbarButton + from sugar3.activity.widgets import StopButton + + class MyActivity(Activity): + def __init__(self, handle): + Activity.__init__(self, handle) + + toolbar_box = ToolbarBox() + activity_button = ActivityToolbarButton(self) + toolbar_box.toolbar.insert(activity_button, 0) + activity_button.show() + + separator = Gtk.SeparatorToolItem(draw=False) + separator.set_expand(True) + toolbar_box.toolbar.insert(separator, -1) + separator.show() + + stop_button = StopButton(self) + toolbar_box.toolbar.insert(stop_button, -1) + stop_button.show() + + self.set_toolbar_box(toolbar_box) + toolbar_box.show() + +**Journal methods** + + In your activity class, code + :func:`~sugar3.activity.activity.Activity.read_file()` and + :func:`~sugar3.activity.activity.Activity.write_file()` methods. + + Most activities create and resume journal objects. For example, + the Write activity saves the document as a journal object, and + reads it from the journal object when resumed. + + :func:`~sugar3.activity.activity.Activity.read_file()` and + :func:`~sugar3.activity.activity.Activity.write_file()` will be + called by the toolkit to tell your activity that it must load or + save the data the user is working on. + +**Activity toolbars** + + Add any activity toolbars before the last separator in the + :class:`~sugar3.graphics.toolbarbox.ToolbarBox`, so that the + :class:`~sugar3.activity.widgets.StopButton` is aligned to the + right. + + There are a number of standard Toolbars. + + You may need the :class:`~sugar3.activity.widgets.EditToolbar`. + This has copy and paste buttons. You may derive your own + class from + :class:`~sugar3.activity.widgets.EditToolbar`: + + .. code-block:: python + + from sugar3.activity.widgets import EditToolbar + + class MyEditToolbar(EditToolbar): + ... + + See :class:`~sugar3.activity.widgets.EditToolbar` for the + methods you should implement in your class. + + You may need some activity specific buttons and options which + you can create as toolbars by deriving a class from + :class:`Gtk.Toolbar`: + + .. code-block:: python + + class MySpecialToolbar(Gtk.Toolbar): + ... + +**Sharing** + + An activity can be shared across the network with other users. Near + the end of your :func:`__init__`, test if the activity is shared, + and connect to signals to detect sharing. + + .. code-block:: python + + if self.shared_activity: + # we are joining the activity + self.connect('joined', self._joined_cb) + if self.get_shared(): + # we have already joined + self._joined_cb() + else: + # we are creating the activity + self.connect('shared', self._shared_cb) + + Add methods to handle the signals. + +Read through the methods of the :class:`Activity` class below, to learn +more about how to make an activity work. + +Hint: A good and simple activity to learn from is the Read activity. +You may copy it and use it as a template. +''' + import gettext import logging import os @@ -117,6 +221,9 @@ N_IFACE_NAME = 'org.freedesktop.Notifications' CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' PREVIEW_SIZE = style.zoom(300), style.zoom(225) +""" +Size of a preview image for journal object metadata. +""" class _ActivitySession(GObject.GObject): @@ -170,118 +277,31 @@ class _ActivitySession(GObject.GObject): class Activity(Window, Gtk.Container): - ''' - This is the base Activity class that all other Activities derive from. - This is where your activity starts. + """ + Initialise an Activity. - To get a working Activity: - 0. Derive your Activity from this class: + Args: + handle (:class:`~sugar3.activity.activityhandle.ActivityHandle`): instance providing the activity id and access to the presence service which *may* provide sharing for this application + create_jobject (boolean): DEPRECATED: define if it should create a journal object if we are not resuming. The parameter is ignored, and always will be created a object in the Journal. - .. code-block:: python + **Signals:** + * **shared** - the activity has been shared on a network in order that other users may join, + * **joined** - the activity has joined with other instances of the activity to create a shared network activity. - class MyActivity(activity.Activity): - ... + Side effects: - 1. implement an __init__() method for your Activity class. + * sets the gdk screen DPI setting (resolution) to the Sugar screen resolution. - Use your init method to create your own ToolbarBox. - This is the code to make a basic toolbar with the activity - toolbar and a stop button. + * connects our "destroy" message to our _destroy_cb method. - .. code-block:: python + * creates a base Gtk.Window within this window. - from sugar3.graphics.toolbarbox import ToolbarBox - from sugar3.activity.widgets import ActivityToolbarButton - from sugar3.activity.widgets import StopButton + * creates an ActivityService (self._bus) servicing this application. - def __init__(self, handle): - activity.Activity.__init__(self, handle) - - toolbar_box = ToolbarBox() - activity_button = ActivityToolbarButton(self) - toolbar_box.toolbar.insert(activity_button, 0) - activity_button.show() - - ... Your toolbars ... - - separator = Gtk.SeparatorToolItem(draw=False) - separator.set_expand(True) - toolbar_box.toolbar.insert(separator, -1) - separator.show() - - stop_button = StopButton(self) - toolbar_box.toolbar.insert(stop_button, -1) - stop_button.show() - - self.set_toolbar_box(toolbar_box) - toolbar_box.show() - - Add extra Toolbars to your toolbox. - - You should setup Activity sharing here too. - - Finaly, your Activity may need some resources which you can claim - here too. - - The __init__() method is also used to make the distinction between - being resumed from the Journal, or starting with a blank document. - - 2. Implement :func:`sugar3.activity.activity.Activity.read_file()` and - :func:`sugar3.activity.activity.Activity.write_file()` - Most activities revolve around creating and storing Journal entries. - For example, Write: You create a document, it is saved to the - Journal and then later you resume working on the document. - - :func:`sugar3.activity.activity.Activity.read_file()` and - :func:`sugar3.activity.activity.Activity.write_file()` - will be called by sugar to tell your - Activity that it should load or save the document the user is - working on. - - 3. Implement our Activity Toolbars. - - The Toolbars are added to your Activity in step 1 (the toolbox), but - you need to implement them somewhere. Now is a good time. - - There are a number of standard Toolbars. The most basic one, the one - your almost absolutely MUST have is the ActivityToolbar. Without - this, you're not really making a proper Sugar Activity (which may be - okay, but you should really stop and think about why not!) You do - this with the ActivityToolbox(self) call in step 1. - - Usually, you will also need the standard EditToolbar. This is the - one which has the standard copy and paste buttons. You need to - derive your own EditToolbar class from - :class:`sugar3.activity.widgets.EditToolbar`: - - .. code-block:: python - - from sugar3.activity.widgets import EditToolbar - - class MyEditToolbar(EditToolbar): - ... - - See EditToolbar for the methods you should implement in your class. - - Finaly, your Activity will very likely need some activity specific - buttons and options you can create your own toolbars by deriving a - class from :class:`Gtk.Toolbar`: - - .. code-block:: python - - class MySpecialToolbar(Gtk.Toolbar): - ... - - 4. Use your creativity. Make your Activity something special and share - it with your friends! - - Read through the methods of the Activity class below, to learn more - about how to make an Activity work. - - Hint: A good and simple Activity to learn from is the Read activity. - To create your own activity, you may want to copy it and use it as a - template. - ''' + When your activity implements :func:`__init__`, it must call the + :class:`Activity` class :func:`__init__` before any + :class:`Activity` specific code. + """ __gtype_name__ = 'SugarActivity' @@ -294,39 +314,6 @@ class Activity(Window, Gtk.Container): } def __init__(self, handle, create_jobject=True): - ''' - Initialise the Activity - - Args: - - handle (sugar3.activity.activityhandle.ActivityHandle) - instance providing the activity id and access to the - presence service which *may* provide sharing for this - application - create_jobject (boolean) - DEPRECATED: define if it should create a journal object if we are - not resuming. The parameter is ignored, and always will - be created a object in the Journal. - - Side effects: - - Sets the gdk screen DPI setting (resolution) to the - Sugar screen resolution. - - Connects our "destroy" message to our _destroy_cb - method. - - Creates a base Gtk.Window within this window. - - Creates an ActivityService (self._bus) servicing - this application. - - Usage: - If your Activity implements __init__(), it should call - the base class __init()__ before doing Activity specific things. - - ''' - # Stuff that needs to be done early icons_path = os.path.join(get_bundle_path(), 'icons') Gtk.IconTheme.get_default().append_search_path(icons_path) @@ -464,6 +451,13 @@ class Activity(Window, Gtk.Container): self._original_title = self._jobject.metadata['title'] def add_stop_button(self, button): + """ + Register an extra stop button. Normally not required. Use only + when an activity has more than the default stop button. + + Args: + button (:class:`Gtk.Button`): a stop button + """ self._stop_buttons.append(button) def run_main_loop(self): @@ -548,6 +542,14 @@ class Activity(Window, Gtk.Container): wait_loop.quit() def get_active(self): + ''' + Get whether the activity is active. An activity may be made + inactive by the shell as a result of another activity being + active. An active activity accumulates usage metrics. + + Returns: + boolean: if the activity is active. + ''' return self._active def _update_spent_time(self): @@ -562,6 +564,14 @@ class Activity(Window, Gtk.Container): self._active_time = current def set_active(self, active): + ''' + Set whether the activity is active. An activity may declare + itself active or inactive, as can the shell. An active activity + accumulates usage metrics. + + Args: + active (boolean): if the activity is active. + ''' if self._active != active: self._active = active self._update_spent_time() @@ -570,12 +580,22 @@ class Activity(Window, Gtk.Container): active = GObject.property( type=bool, default=False, getter=get_active, setter=set_active) + ''' + Whether an activity is active. + ''' def get_max_participants(self): ''' + Get the maximum number of users that can share a instance + of this activity. Should be configured in the activity.info + file. When not configured, it will be zero. + Returns: - int: the max number of users than can share a instance of the - activity. Should be configured in the activity.info file. + int: the maximum number of participants + + See also + :func:`~sugar3.bundle.activitybundle.ActivityBundle.get_max_participants` + in :class:`~sugar3.bundle.activitybundle.ActivityBundle`. ''' # If max_participants has not been set in the activity, get it # from the bundle. @@ -585,6 +605,16 @@ class Activity(Window, Gtk.Container): return self._max_participants def set_max_participants(self, participants): + ''' + Set the maximum number of users that can share a instance of + this activity. An activity may use this method instead of or + as well as configuring the activity.info file. When both are + used, this method takes precedence over the activity.info + file. + + Args: + participants (int): the maximum number of participants + ''' self._max_participants = participants max_participants = GObject.property( @@ -593,15 +623,18 @@ class Activity(Window, Gtk.Container): def get_id(self): ''' + Get the activity id, a likely-unique identifier for the + instance of an activity, randomly assigned when a new instance + is started, or read from the journal object metadata when a + saved instance is resumed. + Returns: - int: the activity id of the current instance of your activity. + str: the activity id - The activity id is sort-of-like the unix process id (PID). However, - unlike PIDs it is only different for each new instance - and stays the same everytime a user - resumes an activity. This is also the identity of your Activity to - other XOs for use when sharing. + See also + :meth:`~sugar3.activity.activityfactory.create_activity_id` + and :meth:`~sugar3.util.unique_id`. ''' return self._activity_id @@ -614,6 +647,8 @@ class Activity(Window, Gtk.Container): def get_canvas(self): ''' + Get the :attr:`canvas`. + Returns: :class:`Gtk.Widget`: the widget used as canvas ''' @@ -621,9 +656,7 @@ class Activity(Window, Gtk.Container): def set_canvas(self, canvas): ''' - Sets the 'work area' of your activity with the canvas of your choice. - - One commonly used canvas is Gtk.ScrolledWindow + Set the :attr:`canvas`. Args: canvas (:class:`Gtk.Widget`): the widget used as canvas @@ -634,6 +667,10 @@ class Activity(Window, Gtk.Container): canvas.connect('map', self.__canvas_map_cb) canvas = property(get_canvas, set_canvas) + ''' + The :class:`Gtk.Widget` used as canvas, or work area of your + activity. A common canvas is :class:`Gtk.ScrolledWindow`. + ''' def __screen_size_changed_cb(self, screen): self._adapt_window_to_screen() @@ -709,8 +746,10 @@ class Activity(Window, Gtk.Container): Subclasses implement this method if they support resuming objects from the journal. 'file_path' is the file to read from. - You should immediately open the file from the file_path, because the - file_name will be deleted immediately after returning from read_file(). + You should immediately open the file from the file_path, + because the file_name will be deleted immediately after + returning from :meth:`read_file`. + Once the file has been opened, you do not have to read it immediately: After you have opened it, the file will only be really gone when you close it. @@ -722,7 +761,7 @@ class Activity(Window, Gtk.Container): originals. Args: - str: the file path to read + file_path (str): the file path to read ''' raise NotImplementedError @@ -738,10 +777,10 @@ class Activity(Window, Gtk.Container): activity. For example, the Read activity saves the current page and zoom level, so it can display the page. - Note: Currently, the file_path *WILL* be different from the one you - received in file_read(). Even if you kept the file_path from - file_read() open until now, you must still write the entire file to - this file_path. + Note: Currently, the file_path *WILL* be different from the + one you received in :meth:`read_file`. Even if you kept the + file_path from :meth:`read_file` open until now, you must + still write the entire file to this file_path. Args: file_path (str): complete path of the file to write @@ -794,17 +833,20 @@ class Activity(Window, Gtk.Container): def get_preview(self): ''' + Get a preview image from the :attr:`canvas`, for use as + metadata for the journal object. This should be what the user + is seeing at the time. + Returns: - str: with data ready to save with an image representing the state - of the activity. Generally this is what the user is seeing in - this moment. + str: image data in PNG format - Activities can override this method, which should return a str with the - binary content of a png image with a width of PREVIEW_SIZE pixels. + Activities may override this method, and return a string with + image data in PNG format with a width and height of + :attr:`~sugar3.activity.activity.PREVIEW_SIZE` pixels. - The method does create a cairo surface similar to that of the canvas' - window and draws on that. Then we create a cairo image surface with - the desired preview size and scale the canvas surface on that. + The method creates a Cairo surface similar to that of the + :ref:`Gdk.Window` of the :meth:`canvas` widget, draws on it, + then resizes to a surface with the preview size. ''' if self.canvas is None or not hasattr(self.canvas, 'get_window'): return None @@ -864,12 +906,13 @@ class Activity(Window, Gtk.Container): def save(self): ''' - Request that the activity is saved to the Journal. + Save to the journal. - This method is called by the close() method below. In general, - activities should not override this method. This method is part of the - public API of an Activity, and should behave in standard ways. Use your - own implementation of write_file() to save your Activity specific data. + This may be called by the :meth:`close` method. + + Activities should not override this method. This method is part of the + public API of an activity, and should behave in standard ways. Use your + own implementation of write_file() to save your activity specific data. ''' if self._jobject is None: @@ -931,11 +974,15 @@ class Activity(Window, Gtk.Container): def copy(self): ''' - Request that the activity 'Keep in Journal' the current state - of the activity. + Make a copy of the journal object. - Activities should not override this method. Instead, like save() do any - copy work that needs to be done in write_file() + Activities may use this to 'Keep in Journal' the current state + of the activity. A new journal object will be created for the + running activity. + + Activities should not override this method. Instead, like + :meth:`save` do any copy work that needs to be done in + :meth:`write_file`. ''' logging.debug('Activity.copy: %r' % self._jobject.object_id) self.save() @@ -968,17 +1015,22 @@ class Activity(Window, Gtk.Container): def get_shared_activity(self): ''' - Returns: - an instance of the shared Activity or None + Get the shared activity of type + :class:`sugar3.presence.activity.Activity`, or None if the + activity is not shared, or is shared and not yet joined. - The shared activity is of type sugar3.presence.activity.Activity + Returns: + :class:`sugar3.presence.activity.Activity`: instance of + the shared activity or None ''' return self.shared_activity def get_shared(self): ''' + Get whether the activity is shared. + Returns: - bool: True if the activity is shared on the mesh. + bool: the activity is shared. ''' if not self.shared_activity: return False @@ -1025,14 +1077,14 @@ class Activity(Window, Gtk.Container): def invite(self, account_path, contact_id): ''' - Invite a buddy to join this Activity. + Invite a buddy to join this activity. Args: account_path contact_id - Side Effects: - Calls self.share(True) to privately share the activity if it wasn't + **Side Effects:** + Calls :meth:`share` to privately share the activity if it wasn't shared before. ''' self._invites_queue.append((account_path, contact_id)) @@ -1049,10 +1101,11 @@ class Activity(Window, Gtk.Container): Args: private (bool): True to share by invitation only, - False to advertise as shared to everyone. + False to advertise as shared to everyone. - Once the activity is shared, its privacy can be changed by setting - its 'private' property. + Once the activity is shared, its privacy can be changed by + setting the :attr:`private` property of the + :attr:`sugar3.presence.activity.Activity` class. ''' if self.shared_activity and self.shared_activity.props.joined: raise RuntimeError('Activity %s already shared.' % @@ -1097,8 +1150,14 @@ class Activity(Window, Gtk.Container): def can_close(self): ''' - Activities should override this function if they want to perform - extra checks before actually closing. + Return whether :func:`close` is permitted. + + An activity may override this function to code extra checks + before closing. + + Returns: + bool: whether :func:`close` is permitted by activity, + default True. ''' return True @@ -1229,14 +1288,18 @@ class Activity(Window, Gtk.Container): def close(self, skip_save=False): ''' - Request that the activity be stopped and saved to the Journal + Save to the journal and stop the activity. - Activities should not override this method, but should implement - write_file() to do any state saving instead. If the application wants - to control wether it can close, it should override can_close(). + Activities should not override this method, but should + implement :meth:`write_file` to do any state saving + instead. If the activity wants to control wether it can close, + it should override :meth:`can_close`. Args: - skip_save (bool) + skip_save (bool): avoid last-chance save; but does not prevent + a journal object, as an object is created when the activity + starts. Use this when an activity calls :meth:`save` just + prior to :meth:`close`. ''' if not self.can_close(): return @@ -1267,9 +1330,11 @@ class Activity(Window, Gtk.Container): def get_metadata(self): ''' + Get the journal object metadata. + Returns: - dict: the jobject metadata or None if there is no jobject. + dict: the journal object metadata, or None if there is no object. Activities can set metadata in write_file() using: @@ -1283,8 +1348,9 @@ class Activity(Window, Gtk.Container): self.metadata.get('MyKey', 'aDefaultValue') - Note: Make sure your activity works properly if one or more of the - metadata items is missing. Never assume they will all be present. + Make sure your activity works properly if one or more of the + metadata items is missing. Never assume they will all be + present. ''' if self._jobject: return self._jobject.metadata @@ -1295,27 +1361,32 @@ class Activity(Window, Gtk.Container): def handle_view_source(self): ''' - A developer can impleement this method to show aditional information - in the View Source window. Example implementations are available - on activities Browse or TurtleArt. + An activity may override this method to show aditional + information in the View Source window. Examples can be seen in + Browse and TurtleArt. + + Raises: + :exc:`NotImplementedError` ''' raise NotImplementedError def get_document_path(self, async_cb, async_err_cb): + ''' + Not implemented. + ''' async_err_cb(NotImplementedError()) def busy(self): ''' Show that the activity is busy. If used, must be called once - before a lengthy operation, and unbusy must be called after - the operation completes. + before a lengthy operation, and :meth:`unbusy` must be called + after the operation completes. .. code-block:: python self.busy() self.long_operation() self.unbusy() - ''' if self._busy_count == 0: self._old_cursor = self.get_window().get_cursor() @@ -1324,12 +1395,12 @@ class Activity(Window, Gtk.Container): def unbusy(self): ''' - Returns: - - int: a count of further calls to unbusy expected - Show that the activity is not busy. An equal number of calls - to unbusy are required to balance the calls to busy. + to :meth:`unbusy` are required to balance the calls to + :meth:`busy`. + + Returns: + int: a count of further calls to :meth:`unbusy` expected ''' self._busy_count -= 1 if self._busy_count == 0: @@ -1434,6 +1505,12 @@ def get_activity_root(): def show_object_in_journal(object_id): + ''' + Raise the journal activity and show a journal object. + + Args: + object_id (object): journal object + ''' bus = dbus.SessionBus() obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) journal = dbus.Interface(obj, J_DBUS_INTERFACE) @@ -1441,6 +1518,13 @@ def show_object_in_journal(object_id): def launch_bundle(bundle_id='', object_id=''): + ''' + Launch an activity for a journal object, or an activity. + + Args: + bundle_id (str): activity bundle id, optional + object_id (object): journal object + ''' bus = dbus.SessionBus() obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) bundle_launcher = dbus.Interface(obj, J_DBUS_INTERFACE) @@ -1448,6 +1532,13 @@ def launch_bundle(bundle_id='', object_id=''): def get_bundle(bundle_id='', object_id=''): + ''' + Get the bundle id of an activity that can open a journal object. + + Args: + bundle_id (str): activity bundle id, optional + object_id (object): journal object + ''' bus = dbus.SessionBus() obj = bus.get_object(J_DBUS_SERVICE, J_DBUS_PATH) journal = dbus.Interface(obj, J_DBUS_INTERFACE) diff --git a/src/sugar3/bundle/__init__.py b/src/sugar3/bundle/__init__.py index 85ebcede..e2c7ce4b 100644 --- a/src/sugar3/bundle/__init__.py +++ b/src/sugar3/bundle/__init__.py @@ -14,3 +14,100 @@ # License along with this library; if not, write to the # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. + +''' + +Activity Metadata +================= + +Your `activity/activity.info` file must have these metadata keys after +an `[Activity]` header: + +* `name` - the name of the activity, shown by Sugar in the list of + installed activities, e.g. Browse, + +* `activity_version` - the version of the activity, e.g. 1, 1.2, + 1.2.3, 1.2.3-country, or 1.2.3~developer, + +* `bundle_id` - the activity bundle identifier, using Java package + naming conventions, usually an organisation or individual domain + name in reverse order, e.g. `org.sugarlabs.Name`, + +* `license` - an identifier for the software license of the bundle, + either a `Fedora License Short Name`_, (e.g. GPLv3+) or an `SPDX + License Identifier`_, with an optional `or later version` suffix, + e.g. `GPL-3.0+`, + +* `icon` - the icon file for the activity, shown by Sugar in the list + of installed activities, + +* `exec` - how to execute the activity, e.g. `sugar-activity module.Class`, + +Optional metadata keys are; + +* `mime_types` - list of MIME types supported by the activity, + separated by semicolons. Your `read_file` method must be able to read + files of these MIME types. Used to offer your activity when opening a + downloaded file or a journal object. + +* `url` - link to the home page for the activity, + +* `repository` - link to repository for activity code, + +.. _SPDX License Identifier: http://spdx.org/licenses/ +.. _Fedora License Short Name: https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses + +AppStream Metadata +================== + +AppStream is a standard, distribution independent package metadata. +For Sugar activities, the AppStream metadata is automatically exported +from the activity.info file by the bundlebuilder during the install +step. + +In order to be compliant with AppStream, activities must have the +following metadata fields under the [Activity] header (of the +`activity.info` file): + +* `metadata_license` - license for screenshots and description. AppStream + requests only using one of the following: `CC0-1.0`, `CC-BY-3.0`, + `CC-BY-SA-3.0` or `GFDL-1.3` + +* `description` - a long (multi paragraph) description of your application. + This must be written in a subset of HTML. Only the p, ol, ul and li tags + are supported. + +Optional metadata key: + +* `screenshots` - a space separated list of screenshot URLs. PNG or JPEG files + are supported. + +Example `activity.info` +----------------------- + +.. code-block:: ini + :emphasize-lines: 10-12,20-21 + + [Activity] + name = Browse + bundle_id = org.laptop.WebActivity + exec = sugar-activity webactivity.WebActivity + activity_version = 200 + icon = activity-web + max_participants = 100 + summary = Surf the world! + + license = GPL-3.0+ + metadata_license = CC0-1.0 + description: +

Surf the world! Here you can do research, watch educational videos, take online courses, find books, connect with friends and more. Browse is powered by the WebKit2 rendering engine with the Faster Than Light javascript interpreter - allowing you to view the full beauty of the web.

+

To help in researching, Browse offers many features:

+ + url = https://github.com/sugarlabs/browse-activity + screenshots = https://people.sugarlabs.org/sam/activity-ss/browse-1-1.png https://people.sugarlabs.org/sam/activity-ss/browse-1-2.png + +''' diff --git a/src/sugar3/bundle/bundleversion.py b/src/sugar3/bundle/bundleversion.py index 31b71b62..91df406a 100644 --- a/src/sugar3/bundle/bundleversion.py +++ b/src/sugar3/bundle/bundleversion.py @@ -15,18 +15,40 @@ # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. -# -# Based on the implementation of PEP 386, but adapted to our -# numeration schema. -# + +""" +Validation and normalization of bundle versions. + +Instances of :class:`NormalizedVersion` can be directly compared; + + >>> from sugar3.bundle.bundleversion import NormalizedVersion + >>> a = NormalizedVersion('157.3') + >>> b = NormalizedVersion('201.2') + >>> a > b + False + >>> b > a + True + +Invalid versions will raise :exc:`InvalidVersionError`. + +Valid versions are `1`, `1.2`, `1.2.3`, `1.2.3-peru`, and +`1.2.3~dfsg`. + +Invalid versions are: + * `1.2peru` (because the suffix must be preceded with a dash or tilde), + * `1.2.` (because a version can't end with a period), or + * `1.02.5` (because a version can't have a leading zero). + +Based on the implementation of :pep:`386`, but adapted to our +numeration schema. + +Attributes: + VERSION_RE (RegexObject): regular expression for versions, deprecated, as it is insufficient by itself. +""" import re -class InvalidVersionError(Exception): - """The passed activity version can not be normalized.""" - pass - VERSION_RE = re.compile(r''' ^ (?P\d+) # minimum 'N' @@ -37,30 +59,30 @@ VERSION_RE = re.compile(r''' $''', re.VERBOSE) -class NormalizedVersion(object): - """A normalized version. - - Good: - 1 - 1.2 - 1.2.3 - 1.2.3-peru - 1.2.3~dfsg - - Bad: - 1.2peru # must be separated with - - 1.2. # can't end with '.' - 1.02.5 # can't have a leading zero - +class InvalidVersionError(Exception): """ + A version cannot be normalized, because: + * the object is not a string, + * the string does not match the regular expression, or + * the string has a leading zero in a version part. + """ + pass + +class NormalizedVersion(object): + """ + Normalize a version string. + + Args: + activity_version (str): the version string + + Raises: + :exc:`InvalidVersionError` + + Attributes: + parts (list): the numeric parts of the version after normalization. + """ def __init__(self, activity_version): - """Create a NormalizedVersion instance from a version string. - - Keyword arguments: - activity_version -- The version string - - """ self._activity_version = activity_version self.parts = [] self._local = None diff --git a/src/sugar3/graphics/alert.py b/src/sugar3/graphics/alert.py index a561ef93..54210428 100644 --- a/src/sugar3/graphics/alert.py +++ b/src/sugar3/graphics/alert.py @@ -1,11 +1,16 @@ """ -Alerts appear at the top of the body of your activity. +Alerts appear in an activity below the toolbox and above the canvas. -At a high level, Alert and its different variations (TimeoutAlert, -ConfirmationAlert, etc.) have a title, an alert message and then several -buttons that the user can click. The Alert class will pass "response" events -to your activity when any of these buttons are clicked, along with a -response_id to help you identify what button was clicked. +:class:`Alert` and the derived :class:`TimeoutAlert`, +:class:`ConfirmationAlert`, :class:`ErrorAlert`, and +:class:`NotifyAlert`, each have a title, a message and optional +buttons. + +:class:`Alert` will emit a `response` signal when a button is +clicked. + +The :class:`TimeoutAlert` and :class:`NotifyAlert` display a countdown +and will emit a `response` signal when a timeout occurs. Example: Create a simple alert message. @@ -16,11 +21,12 @@ Example: # Create a new simple alert alert = Alert() - # Populate the title and text body of the alert. + + # Set the title and text body of the alert alert.props.title = _('Title of Alert Goes Here') alert.props.msg = _('Text message of alert goes here') - # Call the add_alert() method (inherited via the sugar3.graphics.Window - # superclass of Activity) to add this alert to the activity window. + + # Add the alert to the activity self.add_alert(alert) alert.show() @@ -60,13 +66,17 @@ _ = lambda msg: gettext.dgettext('sugar-toolkit-gtk3', msg) class Alert(Gtk.EventBox): """ - UI interface for Alerts + Alerts are inside the activity window instead of being a + separate popup window. They do not hide the canvas. - Alerts are used inside the activity window instead of being a - separate popup window. They do not hide canvas content. You can - use `add_alert()` and `remove_alert()` inside your activity - to add and remove the alert. The position of the alert is below the - toolbox or top in fullscreen mode. + Use :func:`~sugar3.graphics.window.Window.add_alert` and + :func:`~sugar3.graphics.window.Window.remove_alert` to add and + remove an alert. These methods are inherited by an + :class:`~sugar3.activity.activity.Activity` via superclass + :class:`~sugar3.graphics.window.Window`. + + The alert is placed between the canvas and the toolbox, or above + the canvas in fullscreen mode. Args: title (str): the title of the alert @@ -159,10 +169,16 @@ class Alert(Gtk.EventBox): def add_entry(self): """ - Add an entry, after the title and before the buttons. + Create an entry and add it to the alert. + + The entry is placed after the title and before the buttons. + + Caller is responsible for capturing the entry text in the + `response` signal handler or a :class:`Gtk.Entry` signal + handler. Returns: - Gtk.Entry: the entry added to the alert + :class:`Gtk.Entry`: the entry added to the alert """ entry = Gtk.Entry() self._hbox.pack_start(entry, True, True, 0) @@ -175,19 +191,26 @@ class Alert(Gtk.EventBox): def add_button(self, response_id, label, icon=None, position=-1): """ - Add a button to the alert + Create a button and add it to the alert. + + The button is added to the end of the alert. + + When the button is clicked, the `response` signal will be + emitted, along with a response identifier. Args: - response_id (int): will be emitted with the response signal a - response ID should one of the pre-defined GTK Response Type - Constants or a positive number - label (str): that will occure right to the buttom - icon (:class:`sugar3.graphics.icon.Icon` or :class:`Gtk.Image`, optional): - icon for the button, placed before the text - postion (int, optional): the position of the button in the box + response_id (int): the response identifier, a + :class:`Gtk.ResponseType` constant or any positive + integer, + label (str): a label for the button + icon (:class:`~sugar3.graphics.icon.Icon` or \ + :class:`Gtk.Image`, optional): + an icon for the button + position (int, optional): the position of the button in + the box of buttons, Returns: - Gtk.Button: the button added to the alert + :class:`Gtk.Button`: the button added to the alert """ button = Gtk.Button() @@ -204,10 +227,13 @@ class Alert(Gtk.EventBox): def remove_button(self, response_id): """ - Remove a button from the alert by the given response id + Remove a button from the alert. + + The button is selected for removal using the response + identifier that was passed to :func:`add_button`. Args: - response_id (int): the same response id passed to add_button + response_id (int): the response identifier Returns: None @@ -233,23 +259,22 @@ if hasattr(Alert, 'set_css_name'): class ConfirmationAlert(Alert): """ - This is a ready-made two button (Cancel, Ok) alert. + An alert with two buttons; Ok and Cancel. - A confirmation alert is a nice shortcut from a standard Alert because it - comes with 'OK' and 'Cancel' buttons already built-in. When clicked, the - 'OK' button will emit a response with a response_id of - :class:`Gtk.ResponseType.OK`, while the 'Cancel' button will emit + When a button is clicked, the :class:`ConfirmationAlert` will emit + a `response` signal with a response identifier. For the Ok + button, the response identifier will be + :class:`Gtk.ResponseType.OK`. For the Cancel button, :class:`Gtk.ResponseType.CANCEL`. Args: - **kwargs: options for :class:`sugar3.graphics.alert.Alert` + **kwargs: parameters for :class:`~sugar3.graphics.alert.Alert` .. code-block:: python from sugar3.graphics.alert import ConfirmationAlert - # Create a Confirmation alert (with ok and cancel buttons standard) - # then add it to the UI. + # Create a Confirmation alert and add it to the UI. def _alert_confirmation(self): alert = ConfirmationAlert() alert.props.title=_('Title of Alert Goes Here') @@ -257,15 +282,14 @@ class ConfirmationAlert(Alert): alert.connect('response', self._alert_response_cb) self.add_alert(alert) - # Called when an alert object throws a response event. + # Called when an alert object sends a response signal. def _alert_response_cb(self, alert, response_id): - # Remove the alert from the screen, since either a response button - # was clicked or there was a timeout + # Remove the alert self.remove_alert(alert) - # Do any work that is specific to the type of button clicked. + # Check the response identifier. if response_id is Gtk.ResponseType.OK: - print 'Ok Button was clicked. Do any work upon ok here ...' + print 'Ok Button was clicked.' elif response_id is Gtk.ResponseType.CANCEL: print 'Cancel Button was clicked.' """ @@ -284,22 +308,20 @@ class ConfirmationAlert(Alert): class ErrorAlert(Alert): """ - This is a ready-made one button (Ok) alert. + An alert with one button; Ok. - An error alert is a nice shortcut from a standard Alert because it - comes with the 'OK' button already built-in. When clicked, the - 'OK' button will emit a response with a response_id of + When the button is clicked, the :class:`ErrorAlert` will + emit a `response` signal with a response identifier :class:`Gtk.ResponseType.OK`. Args: - **kwargs: options for :class:`sugar3.graphics.alert.Alert` + **kwargs: parameters for :class:`~sugar3.graphics.alert.Alert` .. code-block:: python from sugar3.graphics.alert import ErrorAlert - # Create a Error alert (with ok button standard) - # and add it to the UI. + # Create a Error alert and add it to the UI. def _alert_error(self): alert = ErrorAlert() alert.props.title=_('Title of Alert Goes Here') @@ -309,13 +331,12 @@ class ErrorAlert(Alert): # called when an alert object throws a response event. def _alert_response_cb(self, alert, response_id): - # Remove the alert from the screen, since either a response button - # was clicked or there was a timeout + # Remove the alert self.remove_alert(alert) - # Do any work that is specific to the response_id. + # Check the response identifier. if response_id is Gtk.ResponseType.OK: - print 'Ok Button was clicked. Do any work upon ok here' + print 'Ok Button was clicked.' """ def __init__(self, **kwargs): @@ -396,40 +417,43 @@ class _TimeoutAlert(Alert): class TimeoutAlert(_TimeoutAlert): """ - This is a ready-made two button (Continue, Cancel) alert. The continue - button contains a visual countdown indicating the time remaining to the - user. If the user does not select a button before the timeout, the - response callback is called and the alert is usually removed. + A timed alert with two buttons; Continue and Cancel. The Continue + button contains a countdown of seconds remaining. + + When a button is clicked, the :class:`TimeoutAlert` will emit + a `response` signal with a response identifier. For the Continue + button, the response identifier will be + :class:`Gtk.ResponseType.OK`. For the Cancel button, + :class:`Gtk.ResponseType.CANCEL`. + + If the countdown reaches zero before a button is clicked, the + :class:`TimeoutAlert` will emit a `response` signal with a + response identifier of -1. Args: - timeout (int, optional): the length in seconds for the timeout to - last, defaults to 5 seconds - **kwargs: options for :class:`sugar3.graphics.alert.Alert` + timeout (int, optional): time in seconds, default 5 + **kwargs: parameters for :class:`~sugar3.graphics.alert.Alert` .. code-block:: python from sugar3.graphics.alert import TimeoutAlert - # Create a Timeout alert (with ok and cancel buttons standard) then - # add it to the UI + # Create a Timeout alert and add it to the UI def _alert_timeout(self): - # Notice that for a TimeoutAlert, you pass the number of seconds - # in which to timeout. By default, this is 5. - alert = TimeoutAlert(10) + alert = TimeoutAlert(timeout=10) alert.props.title = _('Title of Alert Goes Here') - alert.props.msg = _('Text message of timeout alert goes here') + alert.props.msg = _('Text message of alert goes here') alert.connect('response', self.__alert_response_cb) self.add_alert(alert) # Called when an alert object throws a response event. def __alert_response_cb(self, alert, response_id): - # Remove the alert from the screen, since either a response button - # was clicked or there was a timeout + # Remove the alert self.remove_alert(alert) - # Do any work that is specific to the type of button clicked. + # Check the response identifier. if response_id is Gtk.ResponseType.OK: - print 'Ok Button was clicked. Do any work upon ok here ...' + print 'Continue Button was clicked.' elif response_id is Gtk.ResponseType.CANCEL: print 'Cancel Button was clicked.' elif response_id == -1: @@ -446,32 +470,42 @@ class TimeoutAlert(_TimeoutAlert): class NotifyAlert(_TimeoutAlert): """ - Timeout alert with only an "OK" button. This should be used just for - notifications and not for user interaction. The alert will timeout after - a given length, similar to a :class:`sugar3.graphics.alert.TimeoutAlert`. + A timed alert with one button; Ok. The button contains a + countdown of seconds remaining. + + When the button is clicked, the :class:`NotifyAlert` will + emit a `response` signal with a response identifier + :class:`Gtk.ResponseType.OK`. + + If the countdown reaches zero before the button is clicked, the + :class:`NotifyAlert` will emit a `response` signal with a + response identifier of -1. Args: - timeout (int, optional): the length in seconds for the timeout to - last, defaults to 5 seconds - **kwargs: options for :class:`sugar3.graphics.alert.Alert` + timeout (int, optional): time in seconds, default 5 + **kwargs: parameters for :class:`~sugar3.graphics.alert.Alert` .. code-block:: python from sugar3.graphics.alert import NotifyAlert - # create a Notify alert (with only an 'OK' button) then show it + # create a Notify alert then show it def _alert_notify(self): alert = NotifyAlert() alert.props.title = _('Title of Alert Goes Here') - alert.props.msg = _('Text message of notify alert goes here') + alert.props.msg = _('Text message of alert goes here') alert.connect('response', self._alert_response_cb) self.add_alert(alert) def __alert_response_cb(self, alert, response_id): - # Hide the alert from the user + # Remove the alert self.remove_alert(alert) - assert response_id == Gtk.ResponseType.OK + # Check the response identifier. + if response_id is Gtk.ResponseType.OK: + print 'Ok Button was clicked.' + elif response_id == -1: + print 'Timeout occurred' """ def __init__(self, timeout=5, **kwargs): diff --git a/src/sugar3/graphics/window.py b/src/sugar3/graphics/window.py index 510949b7..17f796f8 100644 --- a/src/sugar3/graphics/window.py +++ b/src/sugar3/graphics/window.py @@ -41,8 +41,8 @@ class UnfullscreenButton(Gtk.Window): """ A ready-made "Unfullscreen" button. - The type of button used by :class:`sugar3.graphics.window.Window` to exit - fullscreen mode when in fullscreen mode. + Used by :class:`~sugar3.graphics.window.Window` to exit fullscreen + mode. """ def __init__(self): @@ -94,13 +94,28 @@ class UnfullscreenButton(Gtk.Window): class Window(Gtk.Window): """ - UI interface for activity Windows + An activity window. + + Used as a container to display things that happen in an activity. + A window must contain a canvas widget, and a toolbar box widget. + A window may also contain alert message widgets and a tray widget. + + Widgets are kept in a vertical box in this order; + * toolbar box, + * alerts, + * canvas, + * tray. + + A window may be in fullscreen or non-fullscreen mode. In fullscreen + mode, the toolbar and tray are hidden. + + Motion events are tracked, and an unfullscreen button is shown when + the mouse is moved into the top right corner of the canvas. + + Key press events are tracked; + * :kbd:`escape` will cancel fullscreen mode, + * :kbd:`Alt+space` will toggle tray visibility. - Windows are used as a container to display things that happen in an - activity. They contain canvas content, alerts messages, a tray and a - toolbar. Windows can either be in fullscreen or non-fullscreen mode. - Note that the toolbar is hidden in fullscreen mode, while it is visible in - non-fullscreen mode. """ def __init__(self, **args): @@ -152,9 +167,9 @@ class Window(Gtk.Window): """ Make window active. - Brings the window to the top and makes it acive, even after invoking on - response to non-gtk events (in contrast to present()). - See bug #1423 + Brings the window to the top and makes it active, even after + invoking on response to non-GTK events (in contrast to + present()). See bug #1423 """ window = self.get_window() if window is None: @@ -167,16 +182,18 @@ class Window(Gtk.Window): def is_fullscreen(self): """ - Check whether the window is fullscreen or not. + Check if the window is fullscreen. Returns: - bool: true if window is fullscreen, false otherwise + bool: window is fullscreen """ return self._is_fullscreen def fullscreen(self): """ - Make the window fullscreen and hide the toolbar. + Make the window fullscreen. The toolbar and tray will be + hidden, and the :class:`UnfullscreenButton` will be shown for + a short time. """ palettegroup.popdown_all() if self._toolbar_box is not None: @@ -200,7 +217,9 @@ class Window(Gtk.Window): def unfullscreen(self): """ - Put the window in non-fullscreen mode and make the toolbar visible. + Restore the window to non-fullscreen mode. The + :class:`UnfullscreenButton` will be hidden, and the toolbar + and tray will be shown. """ if self._toolbar_box is not None: self._toolbar_box.show() @@ -218,10 +237,10 @@ class Window(Gtk.Window): def set_canvas(self, canvas): """ - Set current canvas of the window. + Set canvas widget. Args: - canvas (:class:`Gtk.Widget`): the canvas to set as current + canvas (:class:`Gtk.Widget`): the canvas to set """ if self._canvas: self.__hbox.remove(self._canvas) @@ -234,30 +253,36 @@ class Window(Gtk.Window): def get_canvas(self): """ - Get current canvas content of the window. + Get canvas widget. Returns: - Gtk.Widget: the current canvas of the window + :class:`Gtk.Widget`: the canvas """ return self._canvas canvas = property(get_canvas, set_canvas) + """ + Property: the :class:`Gtk.Widget` to be shown as the canvas, below + the toolbar and alerts, and above the tray. + """ def get_toolbar_box(self): """ - Return window's current toolbar. + Get :class:`~sugar3.graphics.toolbarbox.ToolbarBox` widget. Returns: - sugar3.graphics.toolbar_box.ToolbarBox: the current toolbar box of the window + :class:`~sugar3.graphics.toolbarbox.ToolbarBox`: the + current toolbar box of the window """ return self._toolbar_box def set_toolbar_box(self, toolbar_box): """ - Set window's current toolbar. + Set :class:`~sugar3.graphics.toolbarbox.ToolbarBox` widget. Args: - toolbar_box (:class:`sugar3.graphics.toolbarbox.ToolbarBox`): the toolbar box to set as current + toolbar_box (:class:`~sugar3.graphics.toolbarbox.ToolbarBox`): + the toolbar box to set as current """ if self._toolbar_box: self.__vbox.remove(self._toolbar_box) @@ -269,14 +294,19 @@ class Window(Gtk.Window): self._toolbar_box = toolbar_box toolbar_box = property(get_toolbar_box, set_toolbar_box) + """ + Property: the :class:`~sugar3.graphics.toolbarbox.ToolbarBox` to + be shown above the alerts and canvas. + """ def set_tray(self, tray, position): """ - Set the window's current tray. + Set the tray. Args: - tray (:class:`sugar3.graphics.tray.HTray` or :class:`sugar3.graphics.tray.VTray`): the tray to set - position (Gtk.PositionType constant): the edge to set the tray at + tray (:class:`~sugar3.graphics.tray.HTray` \ + or :class:`~sugar3.graphics.tray.VTray`): the tray to set + position (:class:`Gtk.PositionType`): the edge to set the tray at """ if self.tray: box = self.tray.get_parent() @@ -293,12 +323,14 @@ class Window(Gtk.Window): def add_alert(self, alert): """ - Add an alert message to the window. - Add an alert to the window. Note that you do need to .show the alert + Add an alert to the window. + + You must call :class:`Gtk.Widget`. :func:`show` on the alert to make it visible. Args: - alert (:class:`sugar3.graphics.alert.Alert`): the alert to add + alert (:class:`~sugar3.graphics.alert.Alert`): the alert + to add """ self._alerts.append(alert) if len(self._alerts) == 1: @@ -313,7 +345,8 @@ class Window(Gtk.Window): Remove an alert message from the window. Args: - alert (:class:`sugar3.graphics.alert.Alert`): the alert to remove + alert (:class:`~sugar3.graphics.alert.Alert`): the alert + to remove """ if alert in self._alerts: self._alerts.remove(alert) @@ -397,19 +430,19 @@ class Window(Gtk.Window): def set_enable_fullscreen_mode(self, enable_fullscreen_mode): """ - Set whether the window is allowed to enter fullscreen mode. + Set enable fullscreen mode. Args: - enable_fullscreen_mode (bool): the boolean to set `_enable_fullscreen_mode` to + enable_fullscreen_mode (bool): enable fullscreen mode """ self._enable_fullscreen_mode = enable_fullscreen_mode def get_enable_fullscreen_mode(self): """ - Return whether the window is allowed to enter fullscreen mode. + Get enable fullscreen mode. Returns: - bool: true if window is allowed to be fullscreen, false otherwise + bool: enable fullscreen mode """ return self._enable_fullscreen_mode @@ -417,3 +450,7 @@ class Window(Gtk.Window): type=object, setter=set_enable_fullscreen_mode, getter=get_enable_fullscreen_mode) + """ + Property: (bool) whether the window is allowed to enter fullscreen + mode, default True. + """