Como Hacer Una Actividad Sugar

Agregar detalles

Barras de herramientas

Que una Actividad necesita buenas barras de herramientas para ser de primera línea es una verdad universal. En este capítulo aprenderemos a hacerlas. Vamos a guardar las clases de las Toolbars en archivos separados del resto por si queremos que nuestra Actividad soporte tanto el estilo viejo como el nuevo. Si tenemos las clases correspondientes a las toolbars en dos archivos distintos el código puede decidir que archivo utilizar cuando se ejecuta. Por ahora este código soporta el estilo viejo que funciona en todas las versiones de Sugar. El estilo nuevo sólo se utiliza hasta ahora en Sugar on a Stick (SoAS). 

Existe un archivo llamado toolbar.py en el fichero Add_Refinements del repositorio Git que se ve así:

from gettext import gettext as _
import re

import pango
import gobject
import gtk

from sugar.graphics.toolbutton import ToolButton
from sugar.activity import activity

class ReadToolbar(gtk.Toolbar):
    __gtype_name__ = 'ReadToolbar'

    def __init__(self):
        gtk.Toolbar.__init__(self)

        self.back = ToolButton('go-previous')
        self.back.set_tooltip(_('Back'))
        self.back.props.sensitive = False
        self.insert(self.back, -1)
        self.back.show()

        self.forward = ToolButton('go-next')
        self.forward.set_tooltip(_('Forward'))
        self.forward.props.sensitive = False
        self.insert(self.forward, -1)
        self.forward.show()

        num_page_item = gtk.ToolItem()

        self.num_page_entry = gtk.Entry()
        self.num_page_entry.set_text('0')
        self.num_page_entry.set_alignment(1)
        self.num_page_entry.connect('insert-text',
            self.num_page_entry_insert_text_cb)

        self.num_page_entry.set_width_chars(4)

        num_page_item.add(self.num_page_entry)
        self.num_page_entry.show()

        self.insert(num_page_item, -1)
        num_page_item.show()

        total_page_item = gtk.ToolItem()

        self.total_page_label = gtk.Label()

        label_attributes = pango.AttrList()
        label_attributes.insert(pango.AttrSize(
            14000, 0, -1))
        label_attributes.insert(pango.AttrForeground(
            65535, 65535, 65535, 0, -1))
        self.total_page_label.set_attributes(
            label_attributes)

        self.total_page_label.set_text(' / 0')
        total_page_item.add(self.total_page_label)
        self.total_page_label.show()

        self.insert(total_page_item, -1)
        total_page_item.show()

    def num_page_entry_insert_text_cb(self, entry, text,
        length, position):
        if not re.match('[0-9]', text):
            entry.emit_stop_by_name('insert-text')
            return True
        return False

    def update_nav_buttons(self):
        current_page = self.current_page
        self.back.props.sensitive = current_page > 0
        self.forward.props.sensitive = \
            current_page < self.total_pages - 1

        self.num_page_entry.props.text = str(
            current_page + 1)
        self.total_page_label.props.label = \
            ' / ' + str(self.total_pages)

    def set_total_pages(self, pages):
        self.total_pages = pages

    def set_current_page(self, page):
        self.current_page = page
        self.update_nav_buttons()

class ViewToolbar(gtk.Toolbar):
    __gtype_name__ = 'ViewToolbar'

    __gsignals__ = {
        'needs-update-size': (gobject.SIGNAL_RUN_FIRST,
                              gobject.TYPE_NONE,
                              ([])),
        'go-fullscreen': (gobject.SIGNAL_RUN_FIRST,
                          gobject.TYPE_NONE,
                          ([]))
    }

    def __init__(self):
        gtk.Toolbar.__init__(self)
        self.zoom_out = ToolButton('zoom-out')
        self.zoom_out.set_tooltip(_('Zoom out'))
        self.insert(self.zoom_out, -1)
        self.zoom_out.show()

        self.zoom_in = ToolButton('zoom-in')
        self.zoom_in.set_tooltip(_('Zoom in'))
        self.insert(self.zoom_in, -1)
        self.zoom_in.show()

        spacer = gtk.SeparatorToolItem()
        spacer.props.draw = False
        self.insert(spacer, -1)
        spacer.show()

        self.fullscreen = ToolButton('view-fullscreen')
        self.fullscreen.set_tooltip(_('Fullscreen'))
        self.fullscreen.connect('clicked',
            self.fullscreen_cb)
        self.insert(self.fullscreen, -1)
        self.fullscreen.show()

    def fullscreen_cb(self, button):
        self.emit('go-fullscreen')

Otro archivo en el mismo fichero del repositorio Git se llama ReadEtextsActivity2.py. Se ve algo así:

import os
import zipfile
import gtk
import pango
from sugar.activity import activity
from sugar.graphics import style
from toolbar import ReadToolbar, ViewToolbar
from gettext import gettext as _

page=0
PAGE_SIZE = 45
TOOLBAR_READ = 2

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.edit_toolbar = activity.EditToolbar()
        self.edit_toolbar.undo.props.visible = False
        self.edit_toolbar.redo.props.visible = False
        self.edit_toolbar.separator.props.visible = False
        self.edit_toolbar.copy.set_sensitive(False)
        self.edit_toolbar.copy.connect('clicked',
            self.edit_toolbar_copy_cb)
        self.edit_toolbar.paste.props.visible = False
        toolbox.add_toolbar(_('Edit'), self.edit_toolbar)
        self.edit_toolbar.show()

        self.read_toolbar = ReadToolbar()
        toolbox.add_toolbar(_('Read'), self.read_toolbar)
        self.read_toolbar.back.connect('clicked',
            self.go_back_cb)
        self.read_toolbar.forward.connect('clicked',
            self.go_forward_cb)
        self.read_toolbar.num_page_entry.connect('activate',
            self.num_page_entry_activate_cb)
        self.read_toolbar.show()

        self.view_toolbar = ViewToolbar()
        toolbox.add_toolbar(_('View'), self.view_toolbar)
        self.view_toolbar.connect('go-fullscreen',
                self.view_toolbar_go_fullscreen_cb)
        self.view_toolbar.zoom_in.connect('clicked',
            self.zoom_in_cb)
        self.view_toolbar.zoom_out.connect('clicked',
            self.zoom_out_cb)
        self.view_toolbar.show()

        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.clipboard = gtk.Clipboard(
            display=gtk.gdk.display_get_default(),
            selection="CLIPBOARD")
        self.textview.grab_focus()
        self.font_desc = pango.FontDescription("sans %d" %
            style.zoom(10))
        self.textview.modify_font(self.font_desc)

        buffer = self.textview.get_buffer()
        self.markset_id = buffer.connect("mark-set",
            self.mark_set_cb)
        self.toolbox.set_current_toolbar(TOOLBAR_READ)

    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 num_page_entry_activate_cb(self, entry):
        global page
        if entry.props.text:
            new_page = int(entry.props.text) - 1
        else:
            new_page = 0

        if new_page >= self.read_toolbar.total_pages:
            new_page = self.read_toolbar.total_pages - 1
        elif new_page < 0:
            new_page = 0

        self.read_toolbar.current_page = new_page
        self.read_toolbar.set_current_page(new_page)
        self.show_page(new_page)
        entry.props.text = str(new_page + 1)
        self.read_toolbar.update_nav_buttons()
        page = new_page

    def go_back_cb(self, button):
        self.page_previous()

    def go_forward_cb(self, button):
        self.page_next()

    def page_previous(self):
        global page
        page=page-1
        if page < 0: page=0
        self.read_toolbar.set_current_page(page)
        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.read_toolbar.set_current_page(page)
        self.show_page(page)
        v_adjustment = \
            self.scrolled_window.get_vadjustment()
        v_adjustment.value = v_adjustment.lower

    def zoom_in_cb(self,  button):
        self.font_increase()

    def zoom_out_cb(self,  button):
        self.font_decrease()

    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 mark_set_cb(self, textbuffer, iter, textmark):

        if textbuffer.get_has_selection():
            begin, end = textbuffer.get_selection_bounds()
            self.edit_toolbar.copy.set_sensitive(True)
        else:
            self.edit_toolbar.copy.set_sensitive(False)

    def edit_toolbar_copy_cb(self, button):
        textbuffer = self.textview.get_buffer()
        begin, end = textbuffer.get_selection_bounds()
        copy_text = textbuffer.get_text(begin, end)
        self.clipboard.set_text(copy_text)

    def view_toolbar_go_fullscreen_cb(self, view_toolbar):
        self.fullscreen()

    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(),
            'tmp', outfn),  'w')
        try:
            f.write(filebytes)
        finally:
            f.close()

    def get_saved_page_number(self):
        global page
        title = self.metadata.get('title', '')
        if title == '' or not title[len(title)- 1].isdigit():
            page = 0
        else:
            i = len(title) - 1
            newPage = ''
            while (title[i].isdigit() and i > 0):
                newPage = title[i] + newPage
                i = i - 1
            if title[i] == 'P':
                page = int(newPage) - 1
            else:
                # not a page number; maybe a volume number.
                page = 0

    def save_page_number(self):
        global page
        title = self.metadata.get('title', '')
        if title == '' or not title[len(title)-1].isdigit():
            title = title + ' P' +  str(page + 1)
        else:
            i = len(title) - 1
            while (title[i].isdigit() and i > 0):
                i = i - 1
            if title[i] == 'P':
                title = title[0:i] + 'P' + str(page + 1)
            else:
                title = title + ' P' + str(page + 1)
        self.metadata['title'] = title

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

        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 ]
        pagecount = 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
                pagecount = pagecount + 1
        if filename.endswith(".zip"):
            os.remove(currentFileName)
        self.get_saved_page_number()
        self.show_page(page)
        self.read_toolbar.set_total_pages(pagecount + 1)
        self.read_toolbar.set_current_page(page)

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

    def write_file(self, filename):
        "Save meta data for the file."
        self.metadata['activity'] = self.get_bundle_id()
        self.save_page_number()

Este es el activity.info para este ejemplo:

[Activity]
name = Read ETexts II
service_name = net.flossmanuals.ReadEtextsActivity
icon = read-etexts
exec = sugar-activity ReadEtextsActivity2.ReadEtextsActivity
show_launcher = no
activity_version = 1
mime_types = text/plain;application/zip
license = GPLv2+

La linea en negrita es la única que necesita cambiarse. Cuando corramos esta nueva versión esto es lo que veremos:

Hay varias cosas que vale la pena señalar en este código. Miremos primero este import:
from gettext import gettext as _

Usaremos el módulo Python gettext para que nuestra Actividad soporte traducciones a otras lenguas. Lo usaremos en sentencias como esta:

        self.back.set_tooltip(_('Back'))

Por la forma en la que importamos gettext, el guión bajo actuará como esta función. El efecto de esta sentencia será buscar en los archivos especiales de traducciones la palabra o frase que concuerde con la clave “Back” y la remplace por su traducción. Si no hubiera archivo de traducción para el lenguaje deseado simplemente usará la palabra “Back”. Más adelante exploraremos como armar estos archivos de traducción, por ahora es suficiente con asegurarnos de usar gettext para todas las palabras o frases que vayamos a mostrar al usuario de nuestra Actividad.

La segunda cosa que vale la pena destacar es que mientras nuestra Actividad tiene cuatro barras de herramientas, sólo tuvimos que crear dos de ellas. Las barras Activity y Edit son parte de la librería Sugar de Python. Podemos usar esas toolbars como están, esconder controles que no queremos o también ampliar estas barras agregando controles nuevos. En este ejemplo estamos escondiendo los controles Keep (Guardar) y Share (Compartir) de la barra Activity y Undo (Deshacer), Redo (Rehacer), and Paste (Pegar) de de la barra Edit. Estos controles no son necesarios actualmente en tanto no hay soporte para compartir o  modificar libros. Observen también que Activity toolbar es parte de ActivityToolbox. No hay forma de darle a la Actividad una toolbox que no contenga a la Activity toolbar como primera opción.

Otra cosa para señalar es que la clase Actividad no sólo nos provee con una ventana. La ventana tiene una Vbox para contener nuestras barras de herramientas y el cuerpo de nuestra Actividad. Instalaremos el toolbox usando set_toolbox() y el cuerpo de la Actividad utilizando set_canvas().

Las barras Read y View son código PyGtk común pero tienen un botón especial para las barras de Sugar que pueden tener un tooltip asociado, y además la barra View tiene código que nos permite ocultarla y la barra ReadEtextsActivity2 tiene código para des-ocultarla. Esta es una función fácil de agregar a tus propias Actividades y muchos juegos y otro tipo de Actividades pueden beneficiarse con la mayor área de pantalla que se obtiene cuando ocultamos la barra.

 

Metadata y entradas al Journal.

Cada entrada del Diario representa un único archivo y su metadata (información que describe al archivo). Hay entradas estándar de metadata que estarán en cualquier entrada al Diario pero  también puedes crear metadata a tu criterio.

 A diferencia de ReadEtextsActivity, esta versión que analizaremos tiene un método write_file()

    def write_file(self, filename):
        "Save meta data for the file."
        self.metadata['activity'] = self.get_bundle_id()
        self.save_page_number()

No teníamos un método write_file () antes porque no necesitábamos escribir en el archivo que contiene al libro. Sin embargo, para la actualización de los metadatos de la entrada del Diario lo vamos a usar. En concreto, vamos a estar haciendo dos cosas:

  • Guardar el número de página donde el usuario de nuestra Actividad suspendió la lectura para que pueda retomarla cuando vuelva a lanzar la Actividad.

  • Decirle al Jounal que esa entrada pertenece a nuestra Actividad, de modo que en el futuro directamente use el ícono apropiado y lance la actividad al hacer clic en la entrada.

Para que la Actividad Read guarde el número de página usamos una propiedad personalizada de la metadata:

    self.metadata['Read_current_page'] = \
        str(self._document.get_page_cache().get_current_page())

Para almacenar el número de página actual, Read crea una propiedad de metadata llamada Read_current_page. Es tan fácil crear metadata customizada (personalizada) que algunos podrán preguntarse porque no usamos esto para Read Etexts. De hecho en la primer versión de Read Etexts usaba una propiedad customizada, pero en Sugar 0.82 y anteriores había un bug y los metadatos así guardados no sobrevivían al apagado de la computadora. Esto resultaba en que la actividad podía recordar números de páginas solamente mientras el equipo estuviera funcionando.  Actualmente hay algunas laptops XO que no pueden actualizarse a nada más nuevo que .82 y otras que aunque puedan no se actualizan porque esto implica un enorme trabajo para las escuelas. 

Para esquivar este problema escribí este par de métodos:

    def get_saved_page_number(self):
        global page
        title = self.metadata.get('title', '')
        if title == '' or not title[len(title)-1].isdigit():
            page = 0
        else:
            i = len(title) - 1
            newPage = ''
            while (title[i].isdigit() and i > 0):
                newPage = title[i] + newPage
                i = i - 1
            if title[i] == 'P':
                page = int(newPage) - 1
            else:
                # not a page number; maybe a volume number.
                page = 0

    def save_page_number(self):
        global page
        title = self.metadata.get('title', '')
        if title == '' or not title[len(title)-1].isdigit():
            title = title + ' P' +  str(page + 1)
        else:
            i = len(title) - 1
            while (title[i].isdigit() and i > 0):
                i = i - 1
            if title[i] == 'P':
                title = title[0:i] + 'P' + str(page + 1)
            else:
                title = title + ' P' + str(page + 1)
        self.metadata['title'] = title

save_page_number () usa la metadata del título actual y, o bien añade el número de página al final del título o actualiza el número de página que ya está ahí. Como title (título) es parte de la metadata estándar de toda entrada al Journal, el bug no afecta este método.

Estos ejemplos también muestran cómo leer la metadata. 

        title = self.metadata.get('title', '')

Esta línea de código dice: "Obtener la propiedad de metadata llamada title y ponerla en la variable llamada title, si no existiera una propiedad title poner una cadena vacía en title.

Normalmente se guardará metadata en el write_file () y se leerá en el método read_file ().

En una Actividad corriente de las que crean algún tipo de archivo con write_file() esta línea sería innecesaria:
        self.metadata['activity'] = self.get_bundle_id()

Cualquier entrada al Diario creada por otra Actividad tendrá automáticamente seteada esta propiedad. En el caso de Pride and Prejudice nuestra Actividad no creó la entrada. Read Etexts puede leerla porque nuestra Actividad soporta su tipo MIME. Desafortunadamente, este tipo de MIME, application/zip, es utilizado por muchas Actividades. Me resultó muy frustrante querer abrir un libro en Read Etexts y abrirlo sin querer en EToys. Esta línea de código resuelve este problema. Tu solamente precisas usar Start Using... (Empezar a usar) la primera vez que leas un e-book.  Después de ello, el libro usará el ícono de Read Etexts y puede retomarse con un simple clic.

Esto no afecta para nada el tipo MIME de la entrada en el Diario, así que si quieres abrir Pride and Prejudice con Etoys, todavía es posible.

Antes de dejar el tema de la metadata del Journal echemos un vistazo a la metadata estándar que toda Actividad tiene. Aquí vemos un código que crea una entrada al Diario y  actualiza unas cuantas de las propiedades estándar.

    def create_journal_entry(self,  tempfile):
        journal_entry = datastore.create()
        journal_title = self.selected_title
        if self.selected_volume != '':
            journal_title +=  ' ' + _('Volume') + ' ' + \
                self.selected_volume
        if self.selected_author != '':
            journal_title = journal_title  + ', by ' + \
                self.selected_author
        journal_entry.metadata['title'] = journal_title
        journal_entry.metadata['title_set_by_user'] = '1'
        journal_entry.metadata['keep'] = '0'
        format = \
            self._books_toolbar.format_combo.props.value
        if format == '.djvu':
            journal_entry.metadata['mime_type'] = \
                'image/vnd.djvu'
        if format == '.pdf' or format == '_bw.pdf':
            journal_entry.metadata['mime_type'] = \
                'application/pdf'
        journal_entry.metadata['buddies'] = ''
        journal_entry.metadata['preview'] = ''
        journal_entry.metadata['icon-color'] = \
            profile.get_color().to_string()
        textbuffer = self.textview.get_buffer()
        journal_entry.metadata['description'] = \
            textbuffer.get_text(textbuffer.get_start_iter(),
            textbuffer.get_end_iter())
        journal_entry.file_path = tempfile
        datastore.write(journal_entry)
        os.remove(tempfile)
        self._alert(_('Success'), self.selected_title + \
            _(' added to Journal.'))

Este código fue tomado de una Actividad que escribí y que descarga los e-books desde la web y crea para ellos entradas al Diario. Las entradas al diario contienen un título amigable y una descripción completa de cada libro.

Aunque la mayoría de las Actividades manejan al Diario exclusivamente con los métodos read_file() y write_file(), no hay por qué limitarse a esto. En un capítulo posterior voy a mostrar cómo crear y eliminar entradas al Journal, como listar su contenido y mucho más.

En este capítulo tratamos una gran cantidad de información técnica y hay más por venir, pero antes de llegar a ella tenemos que ver algunos temas importantes:

  • Subir tu Actividad a un controlador de versiones.  Esto te permite compartir tu código con el mundo entero y quizás lograr que otras personas decidan trabajar sobre él.

  • Tener tu Actividad traducida a otros idiomas.

  • Distribuir tu Actividad ya terminada.  (O tu Actividad casi-terminada, pero útil igual). 

1 
  1. Traducido Ana Cichero, Uruguay^


your comment:
name :
comment :

If you can't read the word, click here
word :