Cairo, PyWebKit y PyGTK: Semana de Pruebas

jueves, octubre 22, 2009

Esta ha sido una semana de pruebas, pruebas y más pruebas. Estoy trabajando en eso de mejorar las interfaces en PyGTK y pretendo apoyarme en Cairo y WebKit. He investigado un poco de ambos; los resultados han sido alentadores.

Primera prueba de la semana: Cairo

Cairo es una librería que permite dibujar sobre un widget (canvas o lienzo). Y cuando digo dibujar, me refiero a dibujar. Trazar líneas, rectángulos, arcos y cosas por el estilo.

Lo primero que se me ocurrió hacer para probar Cairo fue un medidor. Para esto solo me haría falta un slider vertical y el canvas para dibujar. La idea es que el medidor se llene o se vacíe según el deslizamiento de la barra.

El código del medidor quedó así (se explica con los comentarios):
#!/usr/bin/python

# Ejemplo de widget con Cairo
#
# Author: Wil Alvarez (aka Satanas)
# Oct 19, 2009

import gtk
import cairo

# Creamos una clase que herede de gtk.DrawingArea para usarla como canvas
class Cpu(gtk.DrawingArea):
    def __init__(self, parent):
        self.par = parent
        gtk.DrawingArea.__init__(self)
        # Nos conectamos al evento expose, pues allí es donde ocurre toda 
        # la diversión
        self.connect('expose-event', self.expose)
        self.set_size_request(130, 200)
    
    # Este evento se ejecuta cada vez que la aplicación necesita redibujarse
    # o cuando cambiamos un valor y mandamos a redibujarla. Aquí se pintará
    # y se le dará forma al widget
    def expose(self, widget, event):
        # Aquí obtenemos el contexto de cairo
        cr = widget.window.cairo_create()
        cr.set_line_width(0.8)
        
        # Definimos un rectángulo para limitar el proceso de dibujado y así
        # optimizar la operación
        cr.rectangle(event.area.x, event.area.y, 
            event.area.width, event.area.height)
        cr.clip()
        
        cr.rectangle(0,0,130,200)
        cr.set_source_rgb(0, 0, 0)  # Establecemos el color de la brocha/pincel
        cr.fill()
        
        # Obtenemos el valor actual del slider
        x = (self.par.cur_value * 34) / 100
        
        # Dibujamos 34 barritas para el medidor y según el valor de 'x'
        # decidimos si está 'encendida' o no
        for i in range(34):
            if (i < 34 - x):
                cr.set_source_rgb(0.53, 0, 0)
            else:
                cr.set_source_rgb(1, 0, 0)
            
            h = 15 + (i*5)
            cr.rectangle(15,h,49,4)
            cr.fill()
            
            cr.rectangle(67,h,49,4)
            cr.fill()

# Creamos una ventana sencilla en PyGTK con el slider y el canvas
class PyApp(gtk.Window):
    def __init__(self):
        gtk.Window.__init__(self)
        
        self.set_title('CPU Meter')
        self.set_size_request(200, 200)
        self.set_position(gtk.WIN_POS_CENTER)
        self.connect('destroy', gtk.main_quit)

        self.cur_value = 10
       
        vbox = gtk.VBox(False, 2)
        
        scale = gtk.VScale()
        scale.set_range(0, 100)
        scale.set_digits(0)
        scale.set_size_request(35, 160)
        scale.set_value(self.cur_value)
        scale.set_inverted(True)
        scale.connect('value-changed', self.on_changed)
        
        self.cpu = Cpu(self)
        
        hbox = gtk.HBox(False)
        hbox.pack_start(self.cpu)
        hbox.pack_start(scale)
        
        vbox.pack_start(hbox, True, True, 2)

        self.add(vbox)
        self.show_all()
        
    # Programamos el evento 'value-changed' de la barra para que con cada
    # cambio mande a redibujar al widget del medidor
    def on_changed(self, widget):
        self.cur_value = widget.get_value()
        self.cpu.queue_draw()


    def get_cur_value(self):
        return self.cur_value

PyApp()
gtk.main()
Al final la aplicación quedó así:



Bastante aceptable para mi gusto. El código pueden descargarlo aquí


Segunda prueba de la semana: PyWebKit

La segunda prueba fue con PyWebKit. Esta prueba me ha frustrado un poco por la dificultad de conseguir documentación o referencias sobre la API de PyWebKit. Fue una labor árdua. Tuve que descargar varios códigos fuentes; entre ellos el de Gwibber (cliente de Twitter) y hasta el del mismísimo PyWebKit para lograr acercame a algo vagamente funcional.

El único ejemplo que traía PyWebKit era el de un navegador que soporta pestañas y otro centenar de características, por lo que no era para nada sencillo comprender su código (¿a quién se le ocurriría poner ese ejemplo? BIG FAIL! Es solo un demo, ¡rayos!... No el próximo Firefox, Opera o Safari).

El código de gwibber era de lejos más comprensible, aunque no menos complejo (se lo justifico por ser una aplicación de verdad no un ejemplo). Después de mucho leer, digerir implementaciones de controles GTK, fumarme unas cuantas lumpias y otros esoterismos, pude dar con el método que permite insertar código HTML directamente sobre el widget... el famoso load_string.

La cuestión con PyWebKit es relativamente simple, porque después que dominamos el load_string lo demás es carpintería HTML y CSS.

El código de la prueba a continuación:

#!/usr/bin/python

# Ejemplo de widget con WebKit
#
# Author: Wil Alvarez (aka Satanas)
# Oct 20, 2009

import gtk
import webkit
import gobject
gobject.threads_init()

# Codigo HTML que insertaremos al control para que lo muestre
ABOUT_PAGE = """
<html><head><title>PyWebKitGtk</title></head><body>
<h1>Mi primera prueba con PyWebKit</h1>
<p><a href="http://code.google.com/p/pywebkitgtk/">http://code.google.com/p/pywebkitgtk/</a><br/>
</p>
<div style="border: 1px solid #000; width:300px; height: 100px; background-color:#aaa;">
  zOMG! This is fucking awesome<br/><br/>
  No se que más poner en este div con estilos css  XDDD
</div>
</body></html>
"""

# Clase donde sobreecribimos el widget WebView de WebKit para implementar
# nuestro código y hacer uso del load_string para inyectar HTML directamente
# sobre el control (sin usar URI o algo similar)
class MessageStreamView(webkit.WebView):
    def __init__(self):
        webkit.WebView.__init__(self)
        self.connect("navigation-requested", self.on_click_link)
        
        self.settings = webkit.WebSettings()
        self.set_settings(self.settings)
        
        # Recibe como parámetros el código HTML, el mime-type de la página,
        # la codificación y un URI
        self.load_string(ABOUT_PAGE, "text/html", "iso-8859-15", "about")
        
    def on_click_link(self, view, frame, req):
        uri = req.get_uri()
        print uri
        return True

# Creamos una ventana simple en PyGTK con el control que acabamos de crear y 
# voilá! Tenemos nuestro widget que renderiza páginas web con el motor WebKit
class Simulador(gtk.Window):
    def __init__(self):
        gtk.Window.__init__(self)
        self.set_title('Pruebas de Gwibber, Webkit y otras shits')
        self.set_default_size(400, 400)
        self.set_position(gtk.WIN_POS_CENTER)
        self.connect('destroy', gtk.main_quit)
        
        messages = MessageStreamView()
        
        vbox = gtk.VBox(False, 5)
        vbox.pack_start(messages, True, True, 0)
        
        self.add(vbox)
        self.show_all()
    
Simulador()
gtk.main()
La ventana luce así:



Y el código pueden descargarlo aquí.

Aún no estoy muy satisfecho con la información que tengo de PyWebKit, lo mejor que he conseguido es una lista con los nombres de los métodos de la API y más nada, así que seguiré buscando.


Conslusiones

Cada librería tiene un ámbito diferente, por lo que la idea no es establecer una comparación uno a uno entre ellas, sino destacar los pro y los contra para forjar una idea de hasta donde podemos llegar con ellas.

PyWebKit

La mayor desventaja de PyWebKit es la falta de documentación. No me importaría tener una documentación vulgar, chapucera y hasta con errores ortográficos, con tal de al menos tener una! Sin embargo, para compensar eso podemos decir que PyWebKit nos otorga todo el poder y la flexibilidad de un motor de renderizado como WebKit. El límite lo pone nuestro manejo de HTML/CSS y el nivel de implementación de PyWebKit.

Cairo

Cairo tampoco tiene documentación ofical (o no la he encontrado aún) pero al menos hay unos cuantos tutoriales en la red que te dan luces acerca de su funcionamiento y sus métodos básicos. La principal ventaja de Cairo es que nos da la oportunidad de ser artistas sobre un widget xD, incluso creo que se puede usar OpenGL para acelerar el renderizado y aplicar efectos 3D... así que el límite lo pone nuestra imaginación. Pero como diría el abuelo de Peter Parker:

Un gran poder trae consigo una gran responsabilidad

Y esto es porque Cairo se utiliza en controles GTK que hacen las veces de lienzo; podemos imaginarlo como un control más de GTK pero que podemos dibujar a nuestro antojo. Esto trae una consecuencia, pensar en una aplicación desarrollada completamente con Cairo requiere una inversión de trabajo enorme (y quizás injustificada e innecesaria) pues Cairo como tal no tiene widgets, así que nos tocaría implementar desde cero cada control que queramos utilizar; entiéndase cajas de texto, etiquetas, botones y ni hablar de las listas y elementos con scroll... sería una verdadera pesadilla.

Ambas librerías tienen un enorme potencial, cada una en su ámbito y creo que la fórmula ganadora está en una buena combinación de ambas. Ni más ni menos.

Para la próxima entrega postearé las segundas pruebas con Cairo y las pruebas con DBus y el sistema de notificaciones de Ubuntu, NotifyOSD.

Referencias
[1] http://www.zetcode.com/tutorials/pygtktutorial/customwidget/
[2] http://zetcode.com/tutorials/cairographicstutorial/customgtkwidget/
[3] http://www.pygtk.org/articles/cairo-pygtk-widgets/cairo-pygtk-widgets.htm
[4] http://www.tortall.net/mu/wiki/CairoTutorial
[5] http://jackvalmadre.wordpress.com/2008/09/21/resizable-image-control/
[6] http://www.pygtk.org/articles/cairo-pygtk-widget-signals-es/cairo-pygtk-widget-signals.html
[7] http://trac.webkit.org/attachment/wiki/HackingGtk/webkit.api

Blog Action Day 2009 - Cambio Climático

jueves, octubre 15, 2009

Hoy es un día especial. Es un día en el que los blogeros de todo el mundo nos unimos a una sola voz por un fin común. Hoy 15 de Octubre de 2009 nos unimos para enfrentar uno de los problemas más urgentes que ataca a nuestro planeta... El cambio climático. Es nuestra oportunidad de llegar a cientos de miles de personas y cambiar el curso de la historia. Escribe en tu blog sobre el cambio climático. Cualquier aporte es bueno. Eso es el Blog Action Day 2009 y yo estoy dentro. Tú... ¿Qué esperas?

Cierro con una premisa muy conocida en el mundo del Software Libre y un video:

No es lo mismo adaptarse a los cambios que provocarlos




Error con Ruby 1.8 y Rails 1.2.6 en Debian Squeeze

lunes, octubre 05, 2009

Estaba yo una tarde de lo más tranquilo, programando mi sistema de compras de café verde mientras tarareaba una canciónde Bloodbath xD. En una de esas me dispongo a correr las migraciones de mi proyecto para actualizar la base de datos y para mi sorpresa ¡la migración falla!. El error (nada explícito) a continuación:

$ rake db:migrate
(in /home/satanas/proyectos/cvacafe/sicca)
rake aborted!
{:root=>"(as the the label for a named route) will become a shortcut for map.connect '', so find another name"} is not a symbol

(See full trace by running task with --trace)


Al ver la verbosidad del mensaje invoco el comando pero con la opción --trace tal como me lo sugiere la advertencia y obtengo esto:

$ rake db:migrate --trace
(in /home/satanas/proyectos/cvacafe/sicca)
** Invoke db:migrate (first_time)
** Invoke environment (first_time)
** Execute environment
rake aborted!
{:root=>"(as the the label for a named route) will become a shortcut for map.connect '', so find another name"} is not a symbol
/usr/lib/ruby/1.8/deprecated.rb:176:in `instance_method'
/usr/lib/ruby/1.8/deprecated.rb:176:in `deprecate'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.6/lib/action_controller/routing.rb:994
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:342:in `new_constants_in'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.6/lib/action_controller/base.rb:4
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:342:in `new_constants_in'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.6/lib/action_controller.rb:37
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:342:in `new_constants_in'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.6/lib/initializer.rb:166:in `require_frameworks'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.6/lib/initializer.rb:166:in `each'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.6/lib/initializer.rb:166:in `require_frameworks'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.6/lib/initializer.rb:87:in `process'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.6/lib/initializer.rb:47:in `send'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.6/lib/initializer.rb:47:in `run'
/home/satanas/proyectos/cvacafe/sicca/config/../config/environment.rb:13
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:342:in `new_constants_in'
/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.4/lib/active_support/dependencies.rb:495:in `require'
/usr/lib/ruby/gems/1.8/gems/rails-1.2.6/lib/tasks/misc.rake:3
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:617:in `call'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:617:in `execute'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:612:in `each'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:612:in `execute'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:578:in `invoke_with_call_chain'
/usr/lib/ruby/1.8/monitor.rb:242:in `synchronize'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:571:in `invoke_with_call_chain'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:588:in `invoke_prerequisites'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:585:in `each'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:585:in `invoke_prerequisites'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:577:in `invoke_with_call_chain'
/usr/lib/ruby/1.8/monitor.rb:242:in `synchronize'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:571:in `invoke_with_call_chain'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:564:in `invoke'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:2019:in `invoke_task'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:1997:in `top_level'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:1997:in `each'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:1997:in `top_level'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:2036:in `standard_exception_handling'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:1991:in `top_level'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:1970:in `run'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:2036:in `standard_exception_handling'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:1967:in `run'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/bin/rake:31
/usr/bin/rake:19:in `load'
/usr/bin/rake:19


Pues al igual que ustedes, quedé un poco chino con el error (a no ser que alguno de ustedes sea un Ruby developer :P), pero me puse a revisar unos archivos claves: /usr/lib/ruby/1.8/deprecated.rb en la línea 176 y /usr/lib/ruby/gems/1.8/gems/actionpack-1.13.6/lib/action_controller/routing.rb en la línea 994 (béndito sea el Software Libre xD) y descubrí que el problema era con la llamada a una tal función deprecate que esperaba un símbolo y no sé que diablos estaba recibiendo.

Le pregunté a un amigo (que programa conmigo el mismo sistema y no tenía el problema) por sus versiones de Ruby, Rails y las gemas. Teníamos todas las versiones exactamente iguales excepto la de Ruby; yo tenía la 1.8.7.174 y él tenía la 1.8.7.72.

Sí, aunque ustedes no lo crean yo aún uso Rails 1.2.6 con Ruby 1.8 y resulta que Debian Squeeze (testing al momento de escribir esto) me actualizó el paquete de Ruby estropeando la compatibilidad de Ruby y Rails. Sí, también sé que debo migrar a Rails 2 ¡¡¡cuanto antes!!!

Bueno la cuestión la solucioné de una manera relativamente fácil, purge mi instalación de ruby:

# aptitude purge ruby ruby1.8-dev ri ri1.8 rdoc rdoc1.8 irb irb1.8 ruby1.8-examples libreadline-ruby libopenssl-ruby libdbi-ruby libdbd-mysql-ruby libdbd-pg-ruby libdbd-odbc-ruby libdbd-sqlite3-ruby libpgsql-ruby libmysql-ruby

Cambié mis repositorios a los de Debian Lenny (estable para la fecha) (/etc/apt/sources.list):

deb http://ftp.us.debian.org/debian lenny main contrib non-free
deb-src http://ftp.us.debian.org/debian lenny main contrib non-free


Actualicé la lista de paquetes:

# aptitude update

Instalé nuevamente Ruby (pero la versión de Lenny):

# aptitude install ruby ruby1.8-dev ri ri1.8 rdoc rdoc1.8 irb irb1.8 ruby1.8-examples libreadline-ruby libopenssl-ruby libdbi-ruby libdbd-mysql-ruby libdbd-pg-ruby libdbd-odbc-ruby libdbd-sqlite3-ruby libpgsql-ruby libmysql-ruby

Apliqué retención a todos los paquetes anteriores (para que en una futura actualización no pase lo mismo):

# aptitude hold ruby ruby1.8-dev ri ri1.8 rdoc rdoc1.8 irb irb1.8 ruby1.8-examples libreadline-ruby libopenssl-ruby libdbi-ruby libdbd-mysql-ruby libdbd-pg-ruby libdbd-odbc-ruby libdbd-sqlite3-ruby libpgsql-ruby libmysql-ruby

Y volví a poner mis repos de Squeeze:

deb http://ftp.us.debian.org/debian testing main contrib non-free
deb-src http://ftp.us.debian.org/debian testing main contrib non-free


Ahora tengo mi obsoleto y anticuado Ruby 1.8.7.72 pero que me funciona de maravillas con Rails 1.2.6 xD

Cambio y fuera