
Por defecto, cada Actividad genera y lee una entrada en el Journal (Diario). La mayoría de las Actividades no hace nada más que esto en relación al Diario, si tu Actividad es de este tipo no necesitarás leer el contenido de este capítulo. Sin embargo, en pro del día en que hagas Actividades más elaboradas, te conviene seguir leyendo.
Primero repasemos que es el Journal. El Journal, es una colección de archivos con cierta metadata (data por encima de la data) asociada a ellos. La metadata, está guardada como cadenas de texto y incluye cosas como Título, Descripción, Etiquetas (Tags), MIME Types y una captura de pantalla del último acceso a la Actividad.
Estos archivos de metadata no son leídos directamente por tu Actividad. Sugar provee una Interfaz de Programación de Aplicaciones ( API Application Programming Interface). Esta API proporciona métodos para agregar, borrar y modificar entradas del Journal, así como métodos de búsqueda de entradas y de listado de entradas que coincidan con algún criterio.
En el paquete datastore se encuentra la API que usaremos. Después de la versión .82 esta API fue reescrita así que deberemos aprender como hacer para que nuestra Actividad tenga soporte para ambas versiones.
Si has venido leyendo este libro hasta ahora, ya habrás notado más de un caso donde Sugar comienza con un proceder inicial que luego cambia para incluir mejoras, pero siempre brinda la opción de que las Actividades elijan trabajar con los métodos viejos. Si te preguntas si es normal para un proyecto proceder de esta forma; te digo, como programador profesional que los trucos para conservar la retro-compatibilidad son archi-comunes, y que Sugar no hace más trucos que los habituales. Cuando Herman Hollerith tabuló el censo de 18901 con tarjetas perforadas, tomó decisiones con las que los programadores de hoy día deben lidiar aun.
Aunque soy un gran fan del concepto del Journal, no soy muy amigo de la Actividad Journal que Sugar usa para navegar el Journal y para mantenerlo. Mi mayor queja es que representa el contenido de los dispositivos de memoria como pendrives y tarjetas SD, como si estos ficheros estuvieran en el Journal. Mi postura es que los archivos y ficheros son una cosa, y que el Journal es otra cosa, por lo que la interfaz de usuario debería distinguir esto bien.
La Actividad Journal no es una Actividad en el sentido estricto. Hereda código de la clase Activity como cualquier otra Actividad, está escrita en Python y usa la misma datastore API que todas las actividades. Sin embargo, se ejecuta de una forma particular que deriva en permisos y habilidades que están más allá que los de una Actividad común. En particular hace dos cosas:
Si quisiera escribir una Actividad Journal que hiciera lo mismo que la original pero con una interfaz de usuario más a mi gusto, el modelo de seguridad de Sugar no me lo permitiría. Una versión más moderada podría ser útil igual. Así como cuando Kal-El, de vez en cuando, elige ser Clark Kent en vez Superman, mientras no se necesiten super poderes mi Actividad Journal será una alternativa valiosa frente a la Actividad Journal incorporada.
Mi Actividad, a la que llamo Sugar Commander, tiene dos pestañas. Una representa al Journal y se ve así:
En esta pestaña se puede navegar sobre el contenido del Diario, ordenarlo por Título o por Tipo MIME, seleccionar entradas, ver detalles, editar Título, Descripción o Etiquetas, y borrar entradas no deseadas. La otra pestaña muestra archivos y carpetas y se ve así:
Esta pestaña permite navegar archivos y directorios del sistema de archivos regular, incluyendo pendrives y tarjetas SD. Permite también, seleccionar un archivo y convertirlo en una entrada de Diario apretando el botón en el pié de la pantalla.
Esta Actividad tiene muy poquito código y sin embargo logra hacer todo lo que otra Actividad en relación al Diario. Con este comando puedes descargarla desde el repositorio Git.
git clone git://git.sugarlabs.org/sugar-commander/\
mainline.git
Hay solo un archivo fuente, sugarcommander.py:
import logging
import os
import gtk
import pango
import zipfile
from sugar import mime
from sugar.activity import activity
from sugar.datastore import datastore
from sugar.graphics.alert import NotifyAlert
from sugar.graphics import style
from gettext import gettext as _
import gobject
import dbus
COLUMN_TITLE = 0
COLUMN_MIME = 1
COLUMN_JOBJECT = 2
DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
_logger = logging.getLogger('sugar-commander')
class SugarCommander(activity.Activity):
def __init__(self, handle, create_jobject=True):
"The entry point to the Activity"
activity.Activity.__init__(self, handle, False)
self.selected_journal_entry = None
self.selected_path = None
canvas = gtk.Notebook()
canvas.props.show_border = True
canvas.props.show_tabs = True
canvas.show()
self.ls_journal = gtk.ListStore(
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_PYOBJECT)
self.tv_journal = gtk.TreeView(self.ls_journal)
self.tv_journal.set_rules_hint(True)
self.tv_journal.set_search_column(COLUMN_TITLE)
self.selection_journal = \
self.tv_journal.get_selection()
self.selection_journal.set_mode(
gtk.SELECTION_SINGLE)
self.selection_journal.connect("changed",
self.selection_journal_cb)
renderer = gtk.CellRendererText()
renderer.set_property('wrap-mode', gtk.WRAP_WORD)
renderer.set_property('wrap-width', 500)
renderer.set_property('width', 500)
self.col_journal = gtk.TreeViewColumn(_('Title'),
renderer, text=COLUMN_TITLE)
self.col_journal.set_sort_column_id(COLUMN_TITLE)
self.tv_journal.append_column(self.col_journal)
mime_renderer = gtk.CellRendererText()
mime_renderer.set_property('width', 500)
self.col_mime = gtk.TreeViewColumn(_('MIME'),
mime_renderer, text=COLUMN_MIME)
self.col_mime.set_sort_column_id(COLUMN_MIME)
self.tv_journal.append_column(self.col_mime)
self.list_scroller_journal = gtk.ScrolledWindow(
hadjustment=None, vadjustment=None)
self.list_scroller_journal.set_policy(
gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
self.list_scroller_journal.add(self.tv_journal)
label_attributes = pango.AttrList()
label_attributes.insert(pango.AttrSize(
14000, 0, -1))
label_attributes.insert(pango.AttrForeground(
65535, 65535, 65535, 0, -1))
tab1_label = gtk.Label(_("Journal"))
tab1_label.set_attributes(label_attributes)
tab1_label.show()
self.tv_journal.show()
self.list_scroller_journal.show()
column_table = gtk.Table(rows=1, columns=2,
homogeneous = False)
image_table = gtk.Table(rows=2, columns=2,
homogeneous=False)
self.image = gtk.Image()
image_table.attach(self.image, 0, 2, 0, 1,
xoptions=gtk.FILL|gtk.SHRINK,
yoptions=gtk.FILL|gtk.SHRINK,
xpadding=10,
ypadding=10)
self.btn_save = gtk.Button(_("Save"))
self.btn_save.connect('button_press_event',
self.save_button_press_event_cb)
image_table.attach(self.btn_save, 0, 1, 1, 2,
xoptions=gtk.SHRINK,
yoptions=gtk.SHRINK, xpadding=10,
ypadding=10)
self.btn_save.props.sensitive = False
self.btn_save.show()
self.btn_delete = gtk.Button(_("Delete"))
self.btn_delete.connect('button_press_event',
self.delete_button_press_event_cb)
image_table.attach(self.btn_delete, 1, 2, 1, 2,
xoptions=gtk.SHRINK,
yoptions=gtk.SHRINK, xpadding=10,
ypadding=10)
self.btn_delete.props.sensitive = False
self.btn_delete.show()
column_table.attach(image_table, 0, 1, 0, 1,
xoptions=gtk.FILL|gtk.SHRINK,
yoptions=gtk.SHRINK, xpadding=10,
ypadding=10)
entry_table = gtk.Table(rows=3, columns=2,
homogeneous=False)
title_label = gtk.Label(_("Title"))
entry_table.attach(title_label, 0, 1, 0, 1,
xoptions=gtk.SHRINK,
yoptions=gtk.SHRINK,
xpadding=10, ypadding=10)
title_label.show()
self.title_entry = gtk.Entry(max=0)
entry_table.attach(self.title_entry, 1, 2, 0, 1,
xoptions=gtk.FILL|gtk.SHRINK,
yoptions=gtk.SHRINK, xpadding=10, ypadding=10)
self.title_entry.connect('key_press_event',
self.key_press_event_cb)
self.title_entry.show()
description_label = gtk.Label(_("Description"))
entry_table.attach(description_label, 0, 1, 1, 2,
xoptions=gtk.SHRINK,
yoptions=gtk.SHRINK,
xpadding=10, ypadding=10)
description_label.show()
self.description_textview = gtk.TextView()
self.description_textview.set_wrap_mode(
gtk.WRAP_WORD)
entry_table.attach(self.description_textview,
1, 2, 1, 2,
xoptions=gtk.EXPAND|gtk.FILL|gtk.SHRINK,
yoptions=gtk.EXPAND|gtk.FILL|gtk.SHRINK,
xpadding=10, ypadding=10)
self.description_textview.props.accepts_tab = False
self.description_textview.connect('key_press_event',
self.key_press_event_cb)
self.description_textview.show()
tags_label = gtk.Label(_("Tags"))
entry_table.attach(tags_label, 0, 1, 2, 3,
xoptions=gtk.SHRINK,
yoptions=gtk.SHRINK,
xpadding=10, ypadding=10)
tags_label.show()
self.tags_textview = gtk.TextView()
self.tags_textview.set_wrap_mode(gtk.WRAP_WORD)
entry_table.attach(self.tags_textview, 1, 2, 2, 3,
xoptions=gtk.FILL,
yoptions=gtk.EXPAND|gtk.FILL,
xpadding=10, ypadding=10)
self.tags_textview.props.accepts_tab = False
self.tags_textview.connect('key_press_event',
self.key_press_event_cb)
self.tags_textview.show()
entry_table.show()
self.scroller_entry = gtk.ScrolledWindow(
hadjustment=None, vadjustment=None)
self.scroller_entry.set_policy(gtk.POLICY_NEVER,
gtk.POLICY_AUTOMATIC)
self.scroller_entry.add_with_viewport(entry_table)
self.scroller_entry.show()
column_table.attach(self.scroller_entry,
1, 2, 0, 1,
xoptions=gtk.FILL|gtk.EXPAND|gtk.SHRINK,
yoptions=gtk.FILL|gtk.EXPAND|gtk.SHRINK,
xpadding=10, ypadding=10)
image_table.show()
column_table.show()
vbox = gtk.VBox(homogeneous=True, spacing=5)
vbox.pack_start(column_table)
vbox.pack_end(self.list_scroller_journal)
canvas.append_page(vbox, tab1_label)
self._filechooser = gtk.FileChooserWidget(
action=gtk.FILE_CHOOSER_ACTION_OPEN,
backend=None)
self._filechooser.set_current_folder("/media")
self.copy_button = gtk.Button(
_("Copy File To The Journal"))
self.copy_button.connect('clicked',
self.create_journal_entry)
self.copy_button.show()
self._filechooser.set_extra_widget(self.copy_button)
preview = gtk.Image()
self._filechooser.set_preview_widget(preview)
self._filechooser.connect("update-preview",
self.update_preview_cb, preview)
tab2_label = gtk.Label(_("Files"))
tab2_label.set_attributes(label_attributes)
tab2_label.show()
canvas.append_page(self._filechooser, tab2_label)
self.set_canvas(canvas)
self.show_all()
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.load_journal_table()
bus = dbus.SessionBus()
remote_object = bus.get_object(
DS_DBUS_SERVICE, DS_DBUS_PATH)
_datastore = dbus.Interface(remote_object,
DS_DBUS_INTERFACE)
_datastore.connect_to_signal('Created',
self.datastore_created_cb)
_datastore.connect_to_signal('Updated',
self.datastore_updated_cb)
_datastore.connect_to_signal('Deleted',
self.datastore_deleted_cb)
self.selected_journal_entry = None
def update_preview_cb(self, file_chooser, preview):
filename = file_chooser.get_preview_filename()
try:
file_mimetype = mime.get_for_file(filename)
if file_mimetype.startswith('image/'):
pixbuf = \
gtk.gdk.pixbuf_new_from_file_at_size(
filename,
style.zoom(320), style.zoom(240))
preview.set_from_pixbuf(pixbuf)
have_preview = True
elif file_mimetype == 'application/x-cbz':
fname = self.extract_image(filename)
pixbuf = \
gtk.gdk.pixbuf_new_from_file_at_size(
fname,
style.zoom(320), style.zoom(240))
preview.set_from_pixbuf(pixbuf)
have_preview = True
os.remove(fname)
else:
have_preview = False
except:
have_preview = False
file_chooser.set_preview_widget_active(
have_preview)
return
def key_press_event_cb(self, entry, event):
self.btn_save.props.sensitive = True
def save_button_press_event_cb(self, entry, event):
self.update_entry()
def delete_button_press_event_cb(self, entry, event):
datastore.delete(
self.selected_journal_entry.object_id)
def datastore_created_cb(self, uid):
new_jobject = datastore.get(uid)
iter = self.ls_journal.append()
title = new_jobject.metadata['title']
self.ls_journal.set(iter, COLUMN_TITLE, title)
mime = new_jobject.metadata['mime_type']
self.ls_journal.set(iter, COLUMN_MIME, mime)
self.ls_journal.set(iter, COLUMN_JOBJECT,
new_jobject)
def datastore_updated_cb(self, uid):
new_jobject = datastore.get(uid)
iter = self.ls_journal.get_iter_first()
for row in self.ls_journal:
jobject = row[COLUMN_JOBJECT]
if jobject.object_id == uid:
title = new_jobject.metadata['title']
self.ls_journal.set_value(iter,
COLUMN_TITLE, title)
break
iter = self.ls_journal.iter_next(iter)
object_id = self.selected_journal_entry.object_id
if object_id == uid:
self.set_form_fields(new_jobject)
def datastore_deleted_cb(self, uid):
save_path = self.selected_path
iter = self.ls_journal.get_iter_first()
for row in self.ls_journal:
jobject = row[COLUMN_JOBJECT]
if jobject.object_id == uid:
self.ls_journal.remove(iter)
break
iter = self.ls_journal.iter_next(iter)
try:
self.selection_journal.select_path(save_path)
self.tv_journal.grab_focus()
except:
self.title_entry.set_text('')
description_textbuffer = \
self.description_textview.get_buffer()
description_textbuffer.set_text('')
tags_textbuffer = \
self.tags_textview.get_buffer()
tags_textbuffer.set_text('')
self.btn_save.props.sensitive = False
self.btn_delete.props.sensitive = False
self.image.clear()
self.image.show()
def update_entry(self):
needs_update = False
if self.selected_journal_entry is None:
return
object_id = self.selected_journal_entry.object_id
jobject = datastore.get(object_id)
old_title = jobject.metadata.get('title', None)
if old_title != self.title_entry.props.text:
jobject.metadata['title'] = \
self.title_entry.props.text
jobject.metadata['title_set_by_user'] = '1'
needs_update = True
old_tags = jobject.metadata.get('tags', None)
new_tags = \
self.tags_textview.props.buffer.props.text
if old_tags != new_tags:
jobject.metadata['tags'] = new_tags
needs_update = True
old_description = jobject.metadata.get(
'description', None)
new_description = \
self.description_textview.props.buffer.props.text
if old_description != new_description:
jobject.metadata['description'] = new_description
needs_update = True
if needs_update:
datastore.write(jobject, update_mtime=False,
reply_handler=self.datastore_write_cb,
error_handler=self.datastore_write_error_cb)
self.btn_save.props.sensitive = False
def datastore_write_cb(self):
pass
def datastore_write_error_cb(self, error):
logging.error(
'sugarcommander.datastore_write_error_cb:'
' %r' % error)
def close(self, skip_save=False):
"Override the close method so we don't try to
create a Journal entry."
activity.Activity.close(self, True)
def selection_journal_cb(self, selection):
self.btn_delete.props.sensitive = True
tv = selection.get_tree_view()
model = tv.get_model()
sel = selection.get_selected()
if sel:
model, iter = sel
jobject = model.get_value(iter,COLUMN_JOBJECT)
jobject = datastore.get(jobject.object_id)
self.selected_journal_entry = jobject
self.set_form_fields(jobject)
self.selected_path = model.get_path(iter)
def set_form_fields(self, jobject):
self.title_entry.set_text(jobject.metadata['title'])
description_textbuffer = \
self.description_textview.get_buffer()
if jobject.metadata.has_key('description'):
description_textbuffer.set_text(
jobject.metadata['description'])
else:
description_textbuffer.set_text('')
tags_textbuffer = self.tags_textview.get_buffer()
if jobject.metadata.has_key('tags'):
tags_textbuffer.set_text(jobject.metadata['tags'])
else:
tags_textbuffer.set_text('')
self.create_preview(jobject.object_id)
def create_preview(self, object_id):
jobject = datastore.get(object_id)
if jobject.metadata.has_key('preview'):
preview = jobject.metadata['preview']
if preview is None or preview == '' \
or preview == 'None':
if jobject.metadata['mime_type'].startswith(
'image/'):
filename = jobject.get_file_path()
self.show_image(filename)
return
if jobject.metadata['mime_type'] == \
'application/x-cbz':
filename = jobject.get_file_path()
fname = self.extract_image(filename)
self.show_image(fname)
os.remove(fname)
return
if jobject.metadata.has_key('preview') and \
len(jobject.metadata['preview']) > 4:
if jobject.metadata['preview'][1:4] == 'PNG':
preview_data = jobject.metadata['preview']
else:
import base64
preview_data = \
base64.b64decode(
jobject.metadata['preview'])
loader = gtk.gdk.PixbufLoader()
loader.write(preview_data)
scaled_buf = loader.get_pixbuf()
loader.close()
self.image.set_from_pixbuf(scaled_buf)
self.image.show()
else:
self.image.clear()
self.image.show()
def load_journal_table(self):
self.btn_save.props.sensitive = False
self.btn_delete.props.sensitive = False
ds_mounts = datastore.mounts()
mountpoint_id = None
if len(ds_mounts) == 1 and \
ds_mounts[0]['id'] == 1:
pass
else:
for mountpoint in ds_mounts:
id = mountpoint['id']
uri = mountpoint['uri']
if uri.startswith('/home'):
mountpoint_id = id
query = {}
if mountpoint_id is not None:
query['mountpoints'] = [ mountpoint_id ]
ds_objects, num_objects = \
datastore.find(query, properties=['uid',
'title', 'mime_type'])
self.ls_journal.clear()
for i in xrange (0, num_objects, 1):
iter = self.ls_journal.append()
title = ds_objects[i].metadata['title']
self.ls_journal.set(iter, COLUMN_TITLE, title)
mime = ds_objects[i].metadata['mime_type']
self.ls_journal.set(iter, COLUMN_MIME, mime)
self.ls_journal.set(iter, COLUMN_JOBJECT,
ds_objects[i])
if not self.selected_journal_entry is None and \
self.selected_journal_entry.object_id == \
ds_objects[i].object_id:
self.selection_journal.select_iter(iter)
self.ls_journal.set_sort_column_id(COLUMN_TITLE,
gtk.SORT_ASCENDING)
v_adjustment = \
self.list_scroller_journal.get_vadjustment()
v_adjustment.value = 0
return ds_objects[0]
def create_journal_entry(self, widget, data=None):
filename = self._filechooser.get_filename()
journal_entry = datastore.create()
journal_entry.metadata['title'] = \
self.make_new_filename(filename)
journal_entry.metadata['title_set_by_user'] = '1'
journal_entry.metadata['keep'] = '0'
file_mimetype = mime.get_for_file(filename)
if not file_mimetype is None:
journal_entry.metadata['mime_type'] = \
file_mimetype
journal_entry.metadata['buddies'] = ''
if file_mimetype.startswith('image/'):
preview = \
self.create_preview_metadata(filename)
elif file_mimetype == 'application/x-cbz':
fname = self.extract_image(filename)
preview = self.create_preview_metadata(fname)
os.remove(fname)
else:
preview = ''
if not preview == '':
journal_entry.metadata['preview'] = \
dbus.ByteArray(preview)
else:
journal_entry.metadata['preview'] = ''
journal_entry.file_path = filename
datastore.write(journal_entry)
self.alert(_('Success'), _('%s added to Journal.')
% self.make_new_filename(filename))
def alert(self, title, text=None):
alert = NotifyAlert(timeout=20)
alert.props.title = title
alert.props.msg = text
self.add_alert(alert)
alert.connect('response', self.alert_cancel_cb)
alert.show()
def alert_cancel_cb(self, alert, response_id):
self.remove_alert(alert)
def show_image(self, filename):
"display a resized image in a preview"
scaled_buf = gtk.gdk.pixbuf_new_from_file_at_size(
filename,
style.zoom(320), style.zoom(240))
self.image.set_from_pixbuf(scaled_buf)
self.image.show()
def extract_image(self, filename):
zf = zipfile.ZipFile(filename, 'r')
image_files = zf.namelist()
image_files.sort()
file_to_extract = image_files[0]
extract_new_filename = self.make_new_filename(
file_to_extract)
if extract_new_filename is None or \
extract_new_filename == '':
# skip over directory name if the images
# are in a subdirectory.
file_to_extract = image_files[1]
extract_new_filename = self.make_new_filename(
file_to_extract)
if len(image_files) > 0:
if self.save_extracted_file(zf, file_to_extract):
fname = os.path.join(self.get_activity_root(),
'instance',
extract_new_filename)
return fname
def save_extracted_file(self, zipfile, filename):
"Extract the file to a temp directory for viewing"
try:
filebytes = zipfile.read(filename)
except zipfile.BadZipfile, err:
print 'Error opening the zip file: %s' % (err)
return False
except KeyError, err:
self.alert('Key Error', 'Zipfile key not found: '
+ str(filename))
return
outfn = self.make_new_filename(filename)
if (outfn == ''):
return False
fname = os.path.join(self.get_activity_root(),
'instance', outfn)
f = open(fname, 'w')
try:
f.write(filebytes)
finally:
f.close()
return True
def make_new_filename(self, filename):
partition_tuple = filename.rpartition('/')
return partition_tuple[2]
def create_preview_metadata(self, filename):
file_mimetype = mime.get_for_file(filename)
if not file_mimetype.startswith('image/'):
return ''
scaled_pixbuf = \
gtk.gdk.pixbuf_new_from_file_at_size(
filename,
style.zoom(320), style.zoom(240))
preview_data = []
def save_func(buf, data):
data.append(buf)
scaled_pixbuf.save_to_callback(save_func,
'png',
user_data=preview_data)
preview_data = ''.join(preview_data)
return preview_data
Miremos este código analizando los métodos de a uno.
Cuando alguien pulsa un botón del gtk.FileChooser se agrega una entrada al Journal. Este es el código que se ejecuta.
def create_journal_entry(self, widget, data=None):
filename = self._filechooser.get_filename()
journal_entry = datastore.create()
journal_entry.metadata['title'] = \
self.make_new_filename(
filename)
journal_entry.metadata['title_set_by_user'] = '1'
journal_entry.metadata['keep'] = '0'
file_mimetype = mime.get_for_file(filename)
if not file_mimetype is None:
journal_entry.metadata['mime_type'] = \
file_mimetype
journal_entry.metadata['buddies'] = ''
if file_mimetype.startswith('image/'):
preview = self.create_preview_metadata(filename)
elif file_mimetype == 'application/x-cbz':
fname = self.extract_image(filename)
preview = self.create_preview_metadata(fname)
os.remove(fname)
else:
preview = ''
if not preview == '':
journal_entry.metadata['preview'] = \
dbus.ByteArray(preview)
else:
journal_entry.metadata['preview'] = ''
journal_entry.file_path = filename
datastore.write(journal_entry)
La metadata es lo único que vale la pena comentar de esto, title (título) es lo que se indica como #3 en la imagen acá debajo. title_set_by_user (título elegido por el autor) se setea en 1 para que la Actividad no pida al usuario cambiar el título cuando se cierre. keep refiere a las estrellitas que aparecen al inicio de la entrad del Journal (ver #1 en la imagen debajo), se encienden con keep seteado en 1 y se apagan en 0. buddies es la lista de usuarios que colaboraron en esta entrada del Journal, no hay ninguno en este ejemplo pero aparecen en #4 en la imagen debajo.
preview es una imagen en formato PNG que muestra la captura de pantalla de la Actividad en uso. Esta es creada por la propia Actividad cuando se ejecuta de modo que no es necesario crearla al agregar una entrada al diario. Simplemente se deja el string vacio ('') para esta propiedad.
Como en el Sugar Commander, las preview son mucho más visibles que en la Actividad Journal normal, decidí que Sugar Commander creara una imagen de preview para todo archivo de imagen o libros que se agregara al Journal. Para esto hice un pixbuf de la imagen para que se ajuste a la dimensiones escaladas de 320x240 pixels y luego un dbus.ByteArray desde ahí, porque este es el formato que el Journal usa para guardar las imágenes de preview.
El mime_type describe el formato del archivo y generalmente se asigna sobre la base del sujijo del nombre de archivo. Por ejemplo, archivos terminados en .html tienen tipo MIME 'text/html'. Python tiene un paquete llamado mimetypes que a partir del nombre del archivo deduce de que tipo MIME se trata, pero Sugar tiene su propio paquete para hacer la misma cosa. Para la mayoría de los archivos es indistinto usar uno u otro pero como Sugar tiene sus propios MIME para cosas como los "bundles" (empaquetados) de las Actividades, es mejor utilizar el paquete de mime tipos de Sugar. Puedes importarlo de esta forma:
from sugar import mime
El resto de la metadata ( ícono, hora de modificación) se crea automáticamente.
Las Actividades Sugar crean por defecto la entrada al Diario usando el método write_file(). Pero hay algunas Actividades que no se beneficiarían al hacer esto. Por ejemplo, 'Get Internet Archive Books' (Descargar libros de Internet) descarga los e-books al Diario, pero no tiene una entrada de diario propia. Lo mismo ocurre con el propio Sugar Commander.
Si se desea que un juego registre los mejores puntajes y guarde estos puntajes en una entrada del Journal se requiere que que los jugadores retomen el juego desde el Journal y no desde el anillo inicial de Actividades. En otro caso, se pueden guardar estos registros en un directorio data y no es en lo absoluto necesario agregar una entrada al Journal.
Sugar te da un procedimiento para esto. Primero hay que especificar un argumento extra en método __init__() de tu Actividad de esta forma:
class SugarCommander(activity.Activity):
def __init__(self, handle, create_jobject=True):
"The entry point to the Activity"
activity.Activity.__init__(self, handle, False)
En segundo lugar hay que editar el método close() de esta manera:
def close(self, skip_save=False):
"Override the close method so we don't try to
create a Journal entry."
activity.Activity.close(self, True)
Esto es todo lo necesario para evitar la entrada en el Journal.
Si se quiere una lista de las entradas del Journal, se puede usar el método find() de datastore. El método find usa un argumento que contiene el criterio de búsqueda. Si quisieras buscar archivos de imagen podrías filtrar por mime-type usando sentencias como esta:
ds_objects, num_objects = datastore.find(
{'mime_type':['image/jpeg',
'image/gif', 'image/tiff', 'image/png']},
properties=['uid',
'title', 'mime_type']))
Podemos usar cualquier atributo de metadata como criterio de búsqueda. Para listar todo en el diario usamos un criterio vacío como este:
ds_objects, num_objects = datastore.find({},
properties=['uid',
'title', 'mime_type']))
The properties argument specifies what metadata to return for each object in the list. You should limit these to what you plan to use, but always include uid. One thing you should never include in a list is preview. This is an image file showing what the Activity for the Journal object looked like when it was last used. If for some reason you need this there is a simple way to get it for an individual Journal object, but you never want to include it in a list because it will slow down your Activity enormously.
El argumento "properties" selecciona que metadata se pide para cada objeto de la lista. Aunque conviene limitar esta selección siempre se debe incluir el uid. A la vez nunca se debe incluir en un listado el preview. Este es un archivo de imagen con la vista de la Actividad tal y como se veía al usarse por última vez. Hay formas simples de pedir esta imagen para una entrada puntual del diario pero nunca es conveniente incluir este pedido para una lista porque enlentecería enormemente el funcionamiento.
Listing out what is in the Journal is complicated because of the datastore rewrite done for Sugar .84. Before .84 the datastore.find() method listed out both Journal entries and files on external media like thumb drives and SD cards and you need to figure out which is which. In .84 and later it only lists out Journal entries. Fortunately it is possible to write code that supports either behavior. Here is code in Sugar Commander that only lists Journal entries:
Obtener un listado completo del Journal es complicado dada la reescritura que se hizo del datastore para Sugar .84. Antes de esto el método datastore.find() listaba simultaneamente las entradas al Diario y los archivos sobre medios externos, como tarjetas SD y pendrives. En .84 o posteriores sólo lista entradas de diario. Afortunadamente es posible escribir código que soporte el comportamiento anterior. Acá el código que en Sugar Commander lista exclusivamente entradas de Diario.
def load_journal_table(self):
self.btn_save.props.sensitive = False
self.btn_delete.props.sensitive = False
ds_mounts = datastore.mounts()
mountpoint_id = None
if len(ds_mounts) == 1 and ds_mounts[0]['id'] == 1:
pass
else:
for mountpoint in ds_mounts:
id = mountpoint['id']
uri = mountpoint['uri']
if uri.startswith('/home'):
mountpoint_id = id
query = {}
if mountpoint_id is not None:
query['mountpoints'] = [ mountpoint_id ]
ds_objects, num_objects = datastore.find(
query, properties=['uid',
'title', 'mime_type'])
self.ls_journal.clear()
for i in xrange (0, num_objects, 1):
iter = self.ls_journal.append()
title = ds_objects[i].metadata['title']
self.ls_journal.set(iter,
COLUMN_TITLE, title)
mime = ds_objects[i].metadata['mime_type']
self.ls_journal.set(iter, COLUMN_MIME, mime)
self.ls_journal.set(iter, COLUMN_JOBJECT,
ds_objects[i])
if not self.selected_journal_entry is None and \
self.selected_journal_entry.object_id == \
ds_objects[i].object_id:
self.selection_journal.select_iter(iter)
self.ls_journal.set_sort_column_id(COLUMN_TITLE,
gtk.SORT_ASCENDING)
v_adjustment = \
self.list_scroller_journal.get_vadjustment()
v_adjustment.value = 0
return ds_objects[0]
We need to use the datastore.mounts() method for two purposes:
Necesitamos usar el método datastore.mounts() con doble propósito:
What if you want the Sugar .82 behavior, listing both Journal entries and USB files as Journal objects, in both .82 and .84 and up? I wanted to do that for View Slides and ended up using this code:
¿Qué hacer si queremos el comportamiento de Sugar .82, o sea listar como objetos del diario tanto a las entradas como a los archivos de un USB? Al escribir View Slides opté por hacer esto y usé este código:
def load_journal_table(self):
ds_objects, num_objects = datastore.find(
{'mime_type':['image/jpeg',
'image/gif', 'image/tiff', 'image/png']},
properties=['uid', 'title', 'mime_type'])
self.ls_right.clear()
for i in xrange (0, num_objects, 1):
iter = self.ls_right.append()
title = ds_objects[i].metadata['title']
mime_type = ds_objects[i].metadata['mime_type']
if mime_type == 'image/jpeg' \
and not title.endswith('.jpg') \
and not title.endswith('.jpeg') \
and not title.endswith('.JPG') \
and not title.endswith('.JPEG') :
title = title + '.jpg'
if mime_type == 'image/png' \
and not title.endswith('.png') \
and not title.endswith('.PNG'):
title = title + '.png'
if mime_type == 'image/gif' \
and not title.endswith('.gif')\
and not title.endswith('.GIF'):
title = title + '.gif'
if mime_type == 'image/tiff' \
and not title.endswith('.tiff')\
and not title.endswith('.TIFF'):
title = title + '.tiff'
self.ls_right.set(iter, COLUMN_IMAGE, title)
jobject_wrapper = JobjectWrapper()
jobject_wrapper.set_jobject(ds_objects[i])
self.ls_right.set(iter, COLUMN_PATH,
jobject_wrapper)
valid_endings = ('.jpg', '.jpeg', '.JPEG',
'.JPG', '.gif', '.GIF', '.tiff',
'.TIFF', '.png', '.PNG')
ds_mounts = datastore.mounts()
if len(ds_mounts) == 1 and ds_mounts[0]['id'] == 1:
# datastore.mounts() is stubbed out,
# we're running .84 or better
for dirname, dirnames, filenames in os.walk(
'/media'):
if '.olpc.store' in dirnames:
dirnames.remove('.olpc.store')
# don't visit .olpc.store directories
for filename in filenames:
if filename.endswith(valid_endings):
iter = self.ls_right.append()
jobject_wrapper = JobjectWrapper()
jobject_wrapper.set_file_path(
os.path.join(dirname, filename))
self.ls_right.set(iter, COLUMN_IMAGE,
filename)
self.ls_right.set(iter, COLUMN_PATH,
jobject_wrapper)
self.ls_right.set_sort_column_id(COLUMN_IMAGE,
gtk.SORT_ASCENDING)
In this case I use the datastore.mounts() method to figure out what version of the datastore I have and then if I'm running .84 and later I use os.walk() to create a flat list of all files in all directories found under the directory /media (which is where USB and SD cards are always mounted). I can't make these files into directories, but what I can do is make a wrapper class that can contain either a Journal object or a file and use those objects where I would normally use Journal objects. The wrapper class looks like this:
En este caso utilicé el método datastore.mounts() para descubir que versión del datastore estaba en uso y entonces si se trataba de .84 o posterior usar os.walk() para crear una lista plana de todos los archivos encontrados bajo el directorio /media (que es donde se montan los USB y las SD). No puedo transformar estos archivos en directorios, pero sí hacer una clase wrapper que abarque tanto a entradas del diario como a archivos y usar estos objetos como normalmente utilizaría objetos del diario. Esta clase wrapper se vería asi:
class JobjectWrapper():
def __init__(self):
self.__jobject = None
self.__file_path = None
def set_jobject(self, jobject):
self.__jobject = jobject
def set_file_path(self, file_path):
self.__file_path = file_path
def get_file_path(self):
if self.__jobject != None:
return self.__jobject.get_file_path()
else:
return self.__file_path
When you're ready to read a file stored in a Journal object you can use the get_file_path() method of the Journal object to get a file path and open it for reading, like this:
Cuando se quiere leer una archivo guardado como objeto de Journal, se puede usar el método get_file_path() de un objeto Journal para obtener la ruta del archivo y abrirlo para lectura:
fname = jobject.get_file_path()
One word of caution: be aware that this path does not exist until you call get_file_path() and will not exist long after. With the Journal you work with copies of files in the Journal, not the originals. For that reason you don't want to store the return value of get_file_path() for later use because later it may not be valid. Instead, store the Journal object itself and call the method right before you need the path.
Una palabra de advertencia: esta ruta no existe hasta que no se llama al método get_file_path() y no existirá después. Con el Diario se trabaja sobre copias de los archivos del Diario y no sobre el original. Esta es la razón por la que no vale guardar para uso posterior la ruta obtenida mediante get_file_path() y en cambio si hay que guardar el objeto Journal y llamar al método cuando se necesite la ruta.
Metadata entries for Journal objects generally contain strings and work the way you would expect, with one exception, which is the preview.
Las entradas de metadata del diario son en general cadenas y trabajan de formas esperables con la excepción de preview:
def create_preview(self, object_id):
jobject = datastore.get(object_id)
if jobject.metadata.has_key('preview'):
preview = jobject.metadata['preview']
if preview is None or preview == '' or
preview == 'None':
if jobject.metadata['mime_type'].startswith(
'image/'):
filename = jobject.get_file_path()
self.show_image(filename)
return
if jobject.metadata['mime_type'] == \
'application/x-cbz':
filename = jobject.get_file_path()
fname = self.extract_image(filename)
self.show_image(fname)
os.remove(fname)
return
if jobject.metadata.has_key('preview') and \
len(jobject.metadata['preview']) > 4:
if jobject.metadata['preview'][1:4] == 'PNG':
preview_data = jobject.metadata['preview']
else:
import base64
preview_data = base64.b64decode(
jobject.metadata['preview'])
loader = gtk.gdk.PixbufLoader()
loader.write(preview_data)
scaled_buf = loader.get_pixbuf()
loader.close()
self.image.set_from_pixbuf(scaled_buf)
self.image.show()
else:
self.image.clear()
self.image.show()
The preview metadata attribute is different in two ways:
El atributo preview difiere de otros de la metadata en dos maneras:
The code you would use to get a complete copy of a Journal object looks like this:
El código a usar para obtener una copia entera de un objeto del Diario se ve así:
object_id = jobject.object_id
jobject = datastore.get(object_id)
Para explicar que es codificar en base 64, digamos que seguramente escuchaste que las computadoras utilizan el sistema de numeración en base dos, donde los únicos dígitos son 1 y 0. Una unidad de almacenamiento de datos que puede contener sólo un cero o un uno se llama bit. Las computadoras precisan almacenar otra información distinta de números y para esto se agrupan (generalmente) los bits de a 8 y se llama byte a esta agrupación. Si usamos 7 de los 8 bits en un byte podemos guardar un carácter del alfabeto romano, un signo de puntuación, un dígito o cosas como los caracteres que marcan tabulación o avances de línea. Todo archivo que pueda crearse usando solamente 7 de los 8 bits será un archivo de texto. Todo lo que necesite usar los 8 bits de cada byte, incluyendo programas, vídeos, música o fotos de Jessica Alba se llamará binario. En versiones anteriores a Sugar .82 la metadata de un objeto del Journal solo podía almacenar cadenas de texto y de alguna forma había que representar 8 bits usando 7 bits. Esto se resolvió agrupando los bytes en paquetes más grandes y luego partiéndolos de nuevo en grupos de 7 bits. Python tiene el módulo base64 para hacer esto.
La codificación base 64 es actualmente una técnica muy común. Si alguna vez enviaste un adjunto en un email, este viajó codificado en base 64.
El código mostrado debajo muestra un par de formas de crear la imagen de preview. Si la metadata de preview contiene una imagen PNG esta se cargará sobre un pixbuf y se desplegará. Si el tipo MIME es el de un archivo de imagen o de un zip de imágenes, como los que usan los comics, crearemos el preview desde la misma entrada de Journal.
El código verifica el primero de los tres caracteres en la metadata preview para ver si son 'PNG'. Si es así, el archivo es un Portable Network Graphics que se guarda como binario y no necesita conversión a base 64 pero en otro caso la conversión es necesaria.
El código a utilizar para actualizar un objeto del Journal se ve así:
def update_entry(self):
needs_update = False
if self.selected_journal_entry is None:
return
object_id = self.selected_journal_entry.object_id
jobject = datastore.get(object_id)
old_title = jobject.metadata.get('title', None)
if old_title != self.title_entry.props.text:
jobject.metadata['title'] = \
self.title_entry.props.text
jobject.metadata['title_set_by_user'] = '1'
needs_update = True
old_tags = jobject.metadata.get('tags', None)
new_tags = \
self.tags_textview.props.buffer.props.text
if old_tags != new_tags:
jobject.metadata['tags'] = new_tags
needs_update = True
old_description = \
jobject.metadata.get('description', None)
new_description = \
self.description_textview.props.buffer.props.text
if old_description != new_description:
jobject.metadata['description'] = \
new_description
needs_update = True
if needs_update:
datastore.write(jobject, update_mtime=False,
reply_handler=self.datastore_write_cb,
error_handler=self.datastore_write_error_cb)
self.btn_save.props.sensitive = False
def datastore_write_cb(self):
pass
def datastore_write_error_cb(self, error):
logging.error(
'sugarcommander.datastore_write_error_cb:'
' %r' % error)
El código para borrar una entrada del Diario es este:
def delete_button_press_event_cb(self, entry, event):
datastore.delete(
self.selected_journal_entry.object_id)
En el capítulo Making Shared Activities /nombre a designar Vladimir vimos como llamadas de D-Bus enviadas sobre Telepathy podían usarse para mandar mensajes desde una actividad que se ejecuta en una computadora a la misma actividad en una computadora distinta. Normalmente no se usa D-bus de esta forma sino para enviar mensajes entre programas que se ejecutan en la misma máquina.
Por ejemplo, al trabajar con el Diario se obtienen retro-llamadas cada vez que el Diario se actualiza. También se generan retro-llamadas cuando la propia actividad es la que se refresca. Si es importante para tu Actividad saber si el diario se actualizó o no, deberás obtener estas retro-llamadas.
Lo primero que debes hacer es definir algunas constantes e importar el paquete dbus:
DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore' DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore' DS_DBUS_PATH = '/org/laptop/sugar/DataStore' import dbus
Next, in your __init__() method put code to connect to the signals and do the callbacks:
bus = dbus.SessionBus()
remote_object = bus.get_object(
DS_DBUS_SERVICE, DS_DBUS_PATH)
_datastore = dbus.Interface(remote_object,
DS_DBUS_INTERFACE)
_datastore.connect_to_signal('Created',
self._datastore_created_cb)
_datastore.connect_to_signal('Updated',
self._datastore_updated_cb)
_datastore.connect_to_signal('Deleted',
self._datastore_deleted_cb)
The methods being run by the callbacks might look something like this:
def datastore_created_cb(self, uid):
new_jobject = datastore.get(uid)
iter = self.ls_journal.append()
title = new_jobject.metadata['title']
self.ls_journal.set(iter,
COLUMN_TITLE, title)
mime = new_jobject.metadata['mime_type']
self.ls_journal.set(iter,
COLUMN_MIME, mime)
self.ls_journal.set(iter,
COLUMN_JOBJECT, new_jobject)
def datastore_updated_cb(self, uid):
new_jobject = datastore.get(uid)
iter = self.ls_journal.get_iter_first()
for row in self.ls_journal:
jobject = row[COLUMN_JOBJECT]
if jobject.object_id == uid:
title = new_jobject.metadata['title']
self.ls_journal.set_value(iter,
COLUMN_TITLE, title)
break
iter = self.ls_journal.iter_next(iter)
object_id = \
self.selected_journal_entry.object_id
if object_id == uid:
self.set_form_fields(new_jobject)
def datastore_deleted_cb(self, uid):
save_path = self.selected_path
iter = self.ls_journal.get_iter_first()
for row in self.ls_journal:
jobject = row[COLUMN_JOBJECT]
if jobject.object_id == uid:
self.ls_journal.remove(iter)
break
iter = self.ls_journal.iter_next(iter)
try:
self.selection_journal.select_path(
save_path)
self.tv_journal.grab_focus()
except:
self.title_entry.set_text('')
description_textbuffer = \
self.description_textview.get_buffer()
description_textbuffer.set_text('')
tags_textbuffer = \
self.tags_textview.get_buffer()
tags_textbuffer.set_text('')
self.btn_save.props.sensitive = False
self.btn_delete.props.sensitive = False
self.image.clear()
self.image.show()
El uid que se asigna a cada retro-llamada es el id del objeto del Diario -object id - que fue agregado, actualizado o borrado. Si se agrega una entrada al Diario obtenemos el objeto Journal desde su uid, y luego lo agregamos al gtk.ListStore para armar el gtk.TreeModel donde listamos las entradas según el modelo árbol. Necesitamos llevar control cuando una entrada se actualiza o se borra para esto usamos el uid para descubrir cual renglón de la lista gtk.ListStore es necesario borrar o modificar. Para esto se itera sobre las entradas en la gtk.ListStore buscando coincidencias.
Ahora ya sabes todo lo que puedes necesitar para trabajar con el Journal.