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 sugar.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:
This is called when your Activity is started up. This is where you will set up the user interface for your Activity, including toolbars.
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.
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.
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 under the name ReadEtextsActivity.py:
import sys import os import zipfile import pygtk import gtk import pango from sugar.activity import activity from sugar.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 = activity.ActivityToolbox(self) activity_toolbar = toolbox.get_activity_toolbar() activity_toolbar.keep.props.visible = False activity_toolbar.share.props.visible = False self.set_toolbox(toolbox) toolbox.show() self.scrolled_window = gtk.ScrolledWindow() self.scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) self.scrolled_window.props.shadow_type = \ gtk.SHADOW_NONE 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 = gtk.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.value = v_adjustment.upper -\ v_adjustment.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.value = v_adjustment.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.value == v_adjustment.upper - \ v_adjustment.page_size: self.page_next() return if v_adjustment.value < v_adjustment.upper -\ v_adjustment.page_size: new_value = v_adjustment.value +\ v_adjustment.step_increment if new_value > v_adjustment.upper -\ v_adjustment.page_size: new_value = v_adjustment.upper -\ v_adjustment.page_size v_adjustment.value = new_value def scroll_up(self): v_adjustment = \ self.scrolled_window.get_vadjustment() if v_adjustment.value == v_adjustment.lower: self.page_previous() return if v_adjustment.value > v_adjustment.lower: new_value = v_adjustment.value - \ v_adjustment.step_increment if new_value < v_adjustment.lower: new_value = v_adjustment.lower v_adjustment.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(), 'instance', 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) currentFileName = os.path.join( self.get_activity_root(), 'instance', self.book_files) 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
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:
This statement now tells us that class ReadEtextsActivity extends the class sugar.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:
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!