Documentation - rewrite activity section

- rewrite of activity section, bundle section, graphics.alert, and
  graphics.window.
This commit is contained in:
James Cameron 2017-07-19 17:31:09 +10:00
parent 4652b7ca2a
commit 5750773dda
5 changed files with 663 additions and 382 deletions

View File

@ -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. <http://www.collabora.co.uk/>
@ -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)

View File

@ -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:
<p>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.</p>
<p>To help in researching, Browse offers many features:</p>
<ul>
<li>Bookmark (save) good pages you find - never loose good resources or forget to add them to your bibliography</li>
<li>Bookmark pages with collaborators in real time - great for researching as a group or teachers showing pages to their class</li>
<li>Comment on your bookmarked pages - a great tool for making curated collections</li>
</ul>
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
'''

View File

@ -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<version>\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

View File

@ -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):

View File

@ -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.
"""