Make your own sugar activities

Inherit From sugar3.activity.Activity

Object Oriented Python

Python supports two styles of programming: procedural and object oriented. Procedural programming is when you have some input data, do some processing on it, and produce an output. If you want to calculate all the prime numbers under a hundred or convert a Word document into a plain text file you'll probably use the procedural style to do that.

Object oriented programs are built up from units called objects. An object is described as a collection of fields or attributes containing data along with methods for doing things with that data. In addition to doing work and storing data objects can send messages to one another.

Consider a word processing program. It doesn't have just one input, some process, and one output. It can receive input from the keyboard, from the mouse buttons, from the mouse traveling over something, from the clipboard, etc. It can send output to the screen, to a file, to a printer, to the clipboard, etc. A word processor can edit several documents at the same time too. Any program with a GUI is a natural fit for the object oriented style of programming.

Objects are described by classes. When you create an object you are creating an instance of a class.

There's one other thing that a class can do, which is to inherit methods and attributes from another class. When you define a class you can say it extends some class, and by doing that in effect your class has the functionality of the other class plus its own functionality. The extended class becomes its parent.

All Sugar Activities extend a Python class called sugar3.activity.Activity. This class provides methods that all Activities need. In addition to that, there are methods that you can override in your own class that the parent class will call when it needs to. For the beginning Activity writer three methods are important:

__init__()

This is called when your Activity is started up. This is where you will set up the user interface for your Activity, including toolbars.

read_file(self, file_path)

This is called when you resume an Activity from a Journal entry. It is called after the __init__() method is called. The file_path parameter contains the name of a temporary file that is a copy of the file in the Journal entry. The file is deleted as soon as this method finishes, but because Sugar runs on Linux if you open the file for reading your program can continue to read it even after it is deleted and it the file will not actually go away until you close it.

write_file(self, file_path)

This is called when the Activity updates the Journal entry. Just like with read_file() your Activity does not work with the Journal directly. Instead it opens the file named in file_path for output and writes to it. That file in turn is copied to the Journal entry.

There are three things that can cause write_file() to be executed:

  • Your Activity closes.
  • Someone presses the Keep button in the Activity toolbar.
  • Your Activity ceases to be the active Activity, or someone moves from the Activity View to some other View.

In addition to updating the file in the Journal entry the read_file() and write_file() methods are used to read and update the metadata in the Journal entry.

When we convert our standalone Python program to an Activity we'll take out much of the code we wrote and replace it with code inherited from the sugar.activity.Activity  class.

Extending The Activity Class

Here's a version of our program that extends Activity.  You'll find it in the Git repository in the directory Inherit_From_sugar.activity.Activity_gtk3 under the name ReadEtextsActivity.py:

import os
import zipfile
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Pango
from sugar3.activity import widgets
from sugar3.activity.widgets import StopButton
from sugar3.activity import activity
from sugar3.graphics import style

page=0
PAGE_SIZE = 45

class ReadEtextsActivity(activity.Activity):
    def __init__(self, handle):
        "The entry point to the Activity"
        global page
        activity.Activity.__init__(self, handle)

        toolbox = widgets.ActivityToolbar(self)
        toolbox.share.props.visible = False

        stop_button = StopButton(self)
        stop_button.show()
        toolbox.insert(stop_button, -1)

        self.set_toolbar_box(toolbox)

        toolbox.show()
        self.scrolled_window = Gtk.ScrolledWindow()
        self.scrolled_window.set_policy(Gtk.PolicyType.NEVER, \
            Gtk.PolicyType.AUTOMATIC)

        self.textview = Gtk.TextView()
        self.textview.set_editable(False)
        self.textview.set_cursor_visible(False)
        self.textview.set_left_margin(50)
        self.textview.connect("key_press_event", self.keypress_cb)

        self.scrolled_window.add(self.textview)
        self.set_canvas(self.scrolled_window)
        self.textview.show()
        self.scrolled_window.show()
        page = 0
        self.textview.grab_focus()
        self.font_desc = Pango.FontDescription("sans %d" % style.zoom(10))
        self.textview.modify_font(self.font_desc)

    def keypress_cb(self, widget, event):
        "Respond when the user presses one of the arrow keys"
        keyname = Gdk.keyval_name(event.keyval)
        print keyname
        if keyname == 'plus':
            self.font_increase()
            return True
        if keyname == 'minus':
            self.font_decrease()
            return True
        if keyname == 'Page_Up' :
            self.page_previous()
            return True
        if keyname == 'Page_Down':
            self.page_next()
            return True
        if keyname == 'Up' or keyname == 'KP_Up' \
                or keyname == 'KP_Left':
            self.scroll_up()
            return True
        if keyname == 'Down' or keyname == 'KP_Down' \
                or keyname == 'KP_Right':
            self.scroll_down()
            return True
        return False

    def page_previous(self):
        global page
        page=page-1
        if page < 0: page=0
        self.show_page(page)
        v_adjustment = self.scrolled_window.get_vadjustment()
        v_adjustment.set_value(v_adjustment.get_upper() - \
            v_adjustment.get_page_size())

    def page_next(self):
        global page
        page=page+1
        if page >= len(self.page_index): page=0
        self.show_page(page)
        v_adjustment = self.scrolled_window.get_vadjustment()
        v_adjustment.set_value(v_adjustment.get_lower())

    def font_decrease(self):
        font_size = self.font_desc.get_size() / 1024
        font_size = font_size - 1
        if font_size < 1:
            font_size = 1
        self.font_desc.set_size(font_size * 1024)
        self.textview.modify_font(self.font_desc)

    def font_increase(self):
        font_size = self.font_desc.get_size() / 1024
        font_size = font_size + 1
        self.font_desc.set_size(font_size * 1024)
        self.textview.modify_font(self.font_desc)

    def scroll_down(self):
        v_adjustment = self.scrolled_window.get_vadjustment()
        if v_adjustment.get_value() == v_adjustment.get_upper() - \
                v_adjustment.get_page_size():
            self.page_next()
            return
        if v_adjustment.get_value() < v_adjustment.get_upper() - \
            v_adjustment.get_page_size():
            new_value = v_adjustment.get_value() + \
                v_adjustment.step_increment
            if new_value > v_adjustment.get_upper() - \
                v_adjustment.get_page_size():
                new_value = v_adjustment.get_upper() - \
                    v_adjustment.get_page_size()
            v_adjustment.set_value(new_value)

    def scroll_up(self):
        v_adjustment = self.scrolled_window.get_vadjustment()
        if v_adjustment.get_value() == v_adjustment.get_lower():
            self.page_previous()
            return
        if v_adjustment.get_value() > v_adjustment.get_lower():
            new_value = v_adjustment.get_value() - \
                v_adjustment.step_increment
            if new_value < v_adjustment.get_lower():
                new_value = v_adjustment.get_lower()
            v_adjustment.set_value(new_value)

    def show_page(self, page_number):
        global PAGE_SIZE, current_word
        position = self.page_index[page_number]
        self.etext_file.seek(position)
        linecount = 0
        label_text = '\n\n\n'
        textbuffer = self.textview.get_buffer()
        while linecount < PAGE_SIZE:
            line = self.etext_file.readline()
            label_text = label_text + unicode(line, 'iso-8859-1')
            linecount = linecount + 1
        label_text = label_text + '\n\n\n'
        textbuffer.set_text(label_text)
        self.textview.set_buffer(textbuffer)

    def save_extracted_file(self, zipfile, filename):
        "Extract the file to a temp directory for viewing"
        filebytes = zipfile.read(filename)
        outfn = self.make_new_filename(filename)
        if (outfn == ''):
            return False
        f = open(os.path.join(self.get_activity_root(), 'tmp',  outfn),  'w')
        try:
            f.write(filebytes)
        finally:
            f.close()

    def read_file(self, filename):
        "Read the Etext file"
        global PAGE_SIZE

        if zipfile.is_zipfile(filename):
            self.zf = zipfile.ZipFile(filename, 'r')
            self.book_files = self.zf.namelist()
            self.save_extracted_file(self.zf, self.book_files[0])
            currentFileName = os.path.join(self.get_activity_root(), 'tmp', \
                self.book_files[0])
        else:
            currentFileName = filename

        self.etext_file = open(currentFileName,"r")
        self.page_index = [ 0 ]
        linecount = 0
        while self.etext_file:
            line = self.etext_file.readline()
            if not line:
                break
            linecount = linecount + 1
            if linecount >= PAGE_SIZE:
                position = self.etext_file.tell()
                self.page_index.append(position)
                linecount = 0
        if filename.endswith(".zip"):
            os.remove(currentFileName)
        self.show_page(0)

    def make_new_filename(self, filename):
        partition_tuple = filename.rpartition('/')
        return partition_tuple[2]

This program has some significant differences from the standalone version.  First, note that this line:

#! /usr/bin/env python

has been removed.  We are no longer running the program directly from the Python interpreter.  Now Sugar is running it as an Activity.  Notice that much (but not all) of what was in the main() method has been moved to the __init__() method and the main() method has been removed.

Notice too that the class statement has changed:

class ReadEtextsActivity(activity.Activity)

This statement now tells us that class ReadEtextsActivity extends the class sugar3.activity.Activity.   As a result it inherits the code that is in that class.  Therefore we no longer need a GTK main loop, or to define a window.  The code in this class we extend will do that for us.

While we gain much from this inheritance, we lose something too: a title bar for the main window.  In a graphical operating environment a piece of software called a window manager is responsible for putting borders on windows, making them resizeable, reducing them to icons, maximizing them, etc.  Sugar uses a window manager named Matchbox which makes each window fill the whole screen and puts no border, title bar, or any other window decorations on the windows.   As a result of that we can't close our application by clicking on the "X" in the title bar as before.  To make up for this we need to have a toolbar that contains a Close button.  Thus every Activity has an Activity toolbar that contains some standard controls and buttons.  If you look at the code you'll see I'm hiding a couple of controls which we have no use for yet.

The read_file() method is no longer called from the main() method and doesn't seem to be called from anywhere in the program.  Of course it does get called, by some of the Activity code we inherited from our new parent class.  Similarly the __init__() and write_file() methods (if we had a write_file() method) get called by the parent Activity class.

If you're especially observant you might have noticed another change.  Our original standalone program created a temporary file when it needed to extract something from a Zip file.  It put that file in a directory called /tmp.  Our new Activity still creates the file but puts it in a different directory, one specific to the Activity.

All writing to the file system is restricted to subdirectories of the path given by self.get_activity_root().  This method will give you a directory that belongs to your Activity alone.  It will contain three subdirectories with different policies:
data
This directory is used for data such as configuration files.  Files stored here will survive reboots and OS upgrades.
tmp
This directory is used similar to the /tmp directory, being backed by RAM. It may be as small as 1 MB. This directory is deleted when the activity exits.
instance
This directory is similar to the tmp directory, being backed by the computer's drive rather than by RAM. It is unique per instance. It is used for transfer to and from the Journal. This directory is deleted when the activity exits.

Making these changes to the code is not enough to make our program an Activity.  We have to do some packaging work and get it set up to run from the Sugar emulator.  We also need to learn how to run the Sugar emulator.  That comes next!