Como Hacer Una Actividad Sugar

Heredar una Actividad desde sugar.activity.Activity

Python orientado a objetos

Python permite dos estilos de programación: procedural y orientada a objetos. La programación procedural es cuando se tienen datos de entrada, se procesan, y se produce una salida. Si quieres calcular todos los números primos menores a cien o convertir un archivo de Word a texto plano, probablemente uses el estilo procedural.

Los programas orientados a objetos están construidos a partir de unas unidades llamadas objetos. Un objeto se describe como una colección de campos o atributos que contienen datos y métodos para hacer cosas con datos. Además de ejecutar trabajo y guardar datos, los objetos pueden mandarse mensajes entre sí.

Considera un programa que procese palabras. No tiene sólo una entrada, algunos procesos y una salida. Puede recibir datos del teclado, de los botones del mouse, del movimiento del mouse, de la impresora, etc. Un procesador de palabras también puede editar varios documentos a la vez. Cualquier programa con una interfaz gráfica puede naturalmente ser programado mediante orientación a objetos.

Los objetos están descriptos por clases.  Cuando creas un objeto, estás creando una instancia de una clase.

Hay otra cosa que una clase puede hacer, que es heredar métodos y atributos de otra clase. Cuando defines una clase, puedes decir que extiende otra clase, y al hacer eso tu clase tiene toda la funcionalidad de la otra clase más su propia funcionalidad. La clase extendida se vuelve su padre.

Todas las Actividades Sugar extienden una clase de Python llamada sugar.activity.Activity. Esta clase provee métodos que todas las Actividades precisan. Además de eso, hay métodos que puedes sobrescribir en tu clase, que la clase padre llamará cuando precise. Para el escritor principiante de Actividades tres métodos son importantes: 

__init__()

Este método se llama cuando la actividad se inicia. Aquí es donde creas la interfaz para tu Actividad, incluyendo las barras de herramientas.

 

read_file(self, file_path)

Este método se llama cuando retomas una Actividad guardada en el Diario. Se llama luego de llamar al método  __init__(). El parámetro file_path contiene el nombre de una copia temporal del archivo en el Diario. Este archivo se elimina al finalizar el método, pero como Sugar funciona sobre Linux, si abres un archivo para leer, tu programa  puede continuar leyéndolo aún después de ser eliminado, el archivo no se desaparece hasta que lo cierres.

write_file(self, file_path)

Este método es llamado cuando una Actividad actualiza la entrada en el Diario. Al igual que read_file() tu Actividad no trabaja directamente con el Diario. En su lugar abre el archivo nombrado en el file_path para salida y escribe en él. Ese archivo a su vez es copiado al Diario. 

Hay tres motivos que pueden causar que  write_file() se ejecute:

  • Tu Actividad cierra

  • Alguien presiona el botón Keep en la barra de herramientas de la Actividad

  • Tu Actividad deja de ser la Actividad activa, o alguien la mueve a otra vista.

Además de actualizar el archivo en el Diario, los metodos read_file() y write_file() son usados para leer y actualizar los metadatos del archivo en el Diario.

Cuando convertimos nuestro programa de Python en una Actividad, sacamos mucho del código que escribimos y lo remplazaremos con código heredado de la clase sugar.activity.Activity.

Extendiendo la clase Actividad

Aquí hay una versión de nuestro programa que extiende la Actividad. Puede ser encontrada en el repositorio Git en el directorio Inherit_From_sugar.activity.Activity con el nombre 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[0])
            currentFileName = os.path.join(
                self.get_activity_root(),
                'instance', 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]


Este programa tiene algunas diferencias con la versión independiente. Para empezar, se ha quitado la línea:

#! /usr/bin/env python

Ya no estamos ejecutando el programa directamente desde el intérprete de Python. Ahora Sugar lo está ejecutando como una Actividad. Casi todo lo que estaba dentro del método main() fue movido al método __init__() y se ha quitado main().

Nota también que ha cambiado la declaración de la clase:

class ReadEtextsActivity(activity.Activity)

Esta línea nos dice que la clase ReadEtextsActivity extiende la clase sugar.activity.Activity y como resultado hereda el código de esa clase. Por lo tanto no necesitamos definir una función, ni en bucle principal de GTK, el código de la clase que extendemos hará todo eso.

Aunque ganamos mucho de esta herencia, también perdemos algo: una barra de título para la aplicación principal. En un ambiente grafico, un software llamado gestor de ventanas es responsable de ponerle bordes a las ventanas, permitir que cambien de tamaño, reducirlas a iconos, maximizarlas, etc. Sugar utiliza un gestor de ventanas llamado Matchbox que hace que cada ventana ocupe el espacio completo y no le pone borde, barra de título, ni ningún otro tipo de decoración de ventana. Como resultado, no podemos cerrar nuestra aplicación haciendo click en la "X". Para compensar  esto tenemos que tener una barra de herramientas que contenga un botón para cerrar. Es así que cada Actividad tiene una barra de herramientas de Actividad que contiene algunos botones y controles estándar. Si te fijas en el código verás que estoy escondiendo algunos controles para los cuales no tenemos uso todavía.

El método read_file() no es llamado más desde el método main() y no parece ser llamado desde ningún lugar del programa. Sin embargo es llamado por parte del código que heredamos de la clase padre. Similarmente los métodos __init__() y write_file() (en caso de tenerlo) son llamados por la clase padre.

El lector especialmente observador podrá notar otro cambio. Nuestro programa original  creaba un archivo temporal cuando necesitaba extraer algo de un archivo ZIP. Ponía ese archivo en un directorio llamado /tmp. Nuestra nueva Actividad todavía crea el archivo pero lo pone en un directorio diferente, uno especifico de la Actividad.

Toda escritura al sistema de archivos está restringido a subdirectorios de la dirección dada  por self.get_activity_root(). Este método dará un directorio que pertenece sólo a tu Actividad. Contendrá tres subdirectorios con distintas políticas.

data

Este subdirectorio es usado para datos como los archivos de configuración. Los archivos guardados acá sobrevivirán reinicios y actualizaciones del OS.
tmp
Este directorio es similar al directorio /tmp, siendo respaldado por RAM. Puede ser tan pequeño como 1 MB. Este directorio es eliminado cuando la Actividad se cierra.
instance
Este directorio es similar al directorio tmp, siendo respaldado por el disco duro en vez de la RAM. Es único por instancia. Es usado para transferencias con el Diario. Este directorio es eliminado cuando la Actividad se cierra.

Hacer estos cambios al código no es suficiente para hacer que nuestro programa sea una Actividad.  Tenemos que hacer un trabajo de empaquetamiento y configurarlo para que sea ejecutado por el emulador de Sugar. También necesitamos aprender cómo ejecutar el emulador de Sugar. ¡Esto viene a continuación!

Heredar una Actividad desde sugar.activity.Activity

Python orientado a objetos

Python permite dos estilos de programación: procedural y orientada a objetos. La programación procedural es cuando se tienen datos de entrada, se procesan, y se produce una salida. Si quieres calcular todos los números primos menores a cien o convertir un archivo de Word a texto plano, probablemente uses el estilo procedural.

Los programas orientados a objetos están construidos a partir de unas unidades llamadas objetos. Un objeto se describe como una colección de campos o atributos que contienen datos y métodos para hacer cosas con datos. Además de ejecutar trabajo y guardar datos, los objetos pueden mandarse mensajes entre sí.

Considera un programa que procese palabras. No tiene sólo una entrada, algunos procesos y una salida. Puede recibir datos del teclado, de los botones del mouse, del movimiento del mouse, de la impresora, etc. Un procesador de palabras también puede editar varios documentos a la vez. Cualquier programa con una interfaz gráfica puede naturalmente ser programado mediante orientación a objetos.

Los objetos están descriptos por clases.  Cuando creas un objeto, estás creando una instancia de una clase.

Hay otra cosa que una clase puede hacer, que es heredar métodos y atributos de otra clase. Cuando defines una clase, puedes decir que extiende otra clase, y al hacer eso tu clase tiene toda la funcionalidad de la otra clase más su propia funcionalidad. La clase extendida se vuelve su padre.

Todas las Actividades Sugar extienden una clase de Python llamada sugar.activity.Activity. Esta clase provee métodos que todas las Actividades precisan. Además de eso, hay métodos que puedes sobrescribir en tu clase, que la clase padre llamará cuando precise. Para el escritor principiante de Actividades tres métodos son importantes: 

__init__()

Este método se llama cuando la actividad se inicia. Aquí es donde creas la interfaz para tu Actividad, incluyendo las barras de herramientas.

 

read_file(self, file_path)

Este método se llama cuando retomas una Actividad guardada en el Diario. Se llama luego de llamar al método  __init__(). El parámetro file_path contiene el nombre de una copia temporal del archivo en el Diario. Este archivo se elimina al finalizar el método, pero como Sugar funciona sobre Linux, si abres un archivo para leer, tu programa  puede continuar leyéndolo aún después de ser eliminado, el archivo no se desaparece hasta que lo cierres.

write_file(self, file_path)

Este método es llamado cuando una Actividad actualiza la entrada en el Diario. Al igual que read_file() tu Actividad no trabaja directamente con el Diario. En su lugar abre el archivo nombrado en el file_path para salida y escribe en él. Ese archivo a su vez es copiado al Diario. 

Hay tres motivos que pueden causar que  write_file() se ejecute:

  • Tu Actividad cierra

  • Alguien presiona el botón Keep en la barra de herramientas de la Actividad

  • Tu Actividad deja de ser la Actividad activa, o alguien la mueve a otra vista.

Además de actualizar el archivo en el Diario, los metodos read_file() y write_file() son usados para leer y actualizar los metadatos del archivo en el Diario.

Cuando convertimos nuestro programa de Python en una Actividad, sacamos mucho del código que escribimos y lo remplazaremos con código heredado de la clase sugar.activity.Activity.

Extendiendo la clase Actividad

Aquí hay una versión de nuestro programa que extiende la Actividad. Puede ser encontrada en el repositorio Git en el directorio Inherit_From_sugar.activity.Activity con el nombre 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[0])
            currentFileName = os.path.join(
                self.get_activity_root(),
                'instance', 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]


Este programa tiene algunas diferencias con la versión independiente. Para empezar, se ha quitado la línea:

#! /usr/bin/env python

Ya no estamos ejecutando el programa directamente desde el intérprete de Python. Ahora Sugar lo está ejecutando como una Actividad. Casi todo lo que estaba dentro del método main() fue movido al método __init__() y se ha quitado main().

Nota también que ha cambiado la declaración de la clase:

class ReadEtextsActivity(activity.Activity)

Esta línea nos dice que la clase ReadEtextsActivity extiende la clase sugar.activity.Activity y como resultado hereda el código de esa clase. Por lo tanto no necesitamos definir una función, ni un bucle principal de GTK, el código de la clase que extendemos hará todo eso.

Aunque ganamos mucho de esta herencia, también perdemos algo: una barra de título para la aplicación principal. En un ambiente gráfico, un software llamado gestor de ventanas es responsable de ponerle bordes a las ventanas, permitir que cambien de tamaño, reducirlas a íconos, maximizarlas, etc. Sugar utiliza un gestor de ventanas llamado Matchbox que hace que cada ventana ocupe el espacio completo y no le pone borde, barra de título, ni ningún otro tipo de decoración de ventana. Como resultado, no podemos cerrar nuestra aplicación haciendo click en la "X". Para compensar  esto tenemos que tener una barra de herramientas que contenga un botón para cerrar. Es así que cada Actividad tiene una barra de herramientas de Actividad que contiene algunos botones y controles estándar. Si te fijas en el código verás que estoy escondiendo algunos controles para los cuales no tenemos uso todavía.

El método read_file() no es llamado más desde el método main() y no parece ser llamado desde ningún lugar del programa. Sin embargo es llamado por parte del código que heredamos de la clase padre. Similarmente los métodos __init__() y write_file() (en caso de tenerlo) son llamados por la clase padre.

El lector especialmente observador podrá notar otro cambio. Nuestro programa original  creaba un archivo temporal cuando necesitaba extraer algo de un archivo ZIP. Ponía ese archivo en un directorio llamado /tmp. Nuestra nueva Actividad todavía crea el archivo pero lo pone en un directorio diferente, uno específico de la Actividad.

Toda escritura al sistema de archivos está restringida a subdirectorios de la dirección dada  por self.get_activity_root(). Este método dará un directorio que pertenece sólo a tu Actividad. Contendrá tres subdirectorios con distintas políticas.

data
Este subdirectorio es usado para datos como los archivos de configuración. Los archivos guardados acá sobrevivirán reinicios y actualizaciones del OS.
tmp
Este directorio es similar al directorio /tmp, siendo respaldado por RAM. Puede ser tan pequeño como 1 MB. Este directorio es eliminado cuando la Actividad se cierra.
instance
Este directorio es similar al directorio tmp, siendo respaldado por el disco duro en vez de la RAM. Es único por instancia. Es usado para transferencias con el Diario. Este directorio es eliminado cuando la Actividad se cierra.

Hacer estos cambios al código no es suficiente para hacer que nuestro programa sea una Actividad.  Tenemos que hacer un trabajo de empaquetamiento y configurarlo para que sea ejecutado por el emulador de Sugar. También necesitamos aprender cómo ejecutar el emulador de Sugar. ¡Esto viene a continuación!

1  
  1. Traducido Juan Michelini, Uruguay^