Tooltip en PyGTK para un IconView (insertado en un ScrolledWindow)

jueves, diciembre 03, 2009

Luego de pasar toda una tarde reventándome la cabeza y tomando psicotrópicos leyendo la documentación oficial de PyGTK y haciendo pruebas, logré conseguir mi objetivo: Mostrar un miserable tooltip sensible en un IconView usando la nueva API para Tooltips de PyGTK >= 2.12.

La forma antigua y tradicional de hacer un Tooltip era más rudimentaria; incluso habían formas que involucraban conectarse al evento 'motion-notify-event' del widget, obtener la celda en cuestión en base a la posición del ratón y crear la ventana a pie para luego mostrarla. Sin mencionar que era necesario manejar el tiempo de aparición del tooltip usando un timer y la desaparición del tooltip actual antes de mostrar uno nuevo. Nada trivial en realidad, pero así lo implementé en varias ocasiones.

La nueva API reduce todo el proceso a 3 simples pasos:
  • Establecer la propiedad has-tooltip del widget a True para que GTK pueda monitorear los eventos de movimiento relacionados con el tooltip
  • Conectarse a la señal 'query-tooltip' del widget. Esta señal será emitida cuando el tooltip deba ser mostrado. Uno de los argumentos pasados a la señal es un objeto Tooltip correspondiente al widget. Solo queda de nuestra parte modificarlo
  • Retornar True desde el callback que maneja la señal 'query-tooltip' para mostrar el tooltip o False para que no se muestre

Sencillo, ¿eh?. La API es realmente buena y facilita un montón la creación de un Tooltip, de hecho es tan sencillo que parace increible xD. Pero... se les pasó un pequeño detalle... las ventanas de desplazamiento (ScrolledWindow).

Cuando se usa un widget que es o será más grande que el espacio disponible para dibujarlo (TreeView, TextView, IconView, etc) se debe emplear un ventana con scroll (ScrolledWindow) e insertar el widget dentro de ella. La ScrolledWindow se encargará de manejar eso de los scrollbars, el viewport, etc, etc, etc (si no entiendes de que estoy hablando te recomiendo leer este apartado del tutorial de PyGTK). Muy bonito todo... hasta ahora.

El problema aparece cuando queremos mostrar un tooltip sensible para un IconView/TreeView (es decir, que el tooltip mostrará información diferente para cada elemento del contenedor) pues la famosa señal 'query-tooltip' dentro de sus argumentos pasa la posición RELATIVA del cursor mediante x e y:

def callback(widget, x, y, keyboard_mode, tooltip, user_param1, ...)

¿Qué significa la 'posición relativa del cursor'? Pues, en un widget sin scroll es la posición exácta del cursor sobre ese widget, pero en un widget con scroll ocurre lo siguiente:



La señal 'query-tooltip' nos devuelve lo que en la imagen se ve como X e Y, es decir la posición del elemento relativa al ScrolledWindow. Bien.

Ahora, para saber a cual elemento del IconView estamos haciendo referencia hace falta conocer las coordenadas del elemento (relativas al IconView) y ubicar en el modelo el registro correspondiente.

¿Cómo demonios vamos a obtener el elemento del IconView si la señal nos devuelve unas coordenadas que no nos sirven? ¿Cómo rayos obtenemos los valores de Z y W para referenciar al objeto correctamente?

Pues he aquí la solución, y es más fácil de lo que parece. El código se explica con los comentarios.

# -*- coding: utf-8 -*-

import gtk

class PruebaTooltip:
    def __init__(self):
        # Creamos nuestro modelo con 2 campos, uno para la imagen y otro para 
        # la descripción
        self.model = gtk.ListStore(gtk.gdk.Pixbuf, str)
        
        # Creamos el IconView
        self.iconview = gtk.IconView(self.model)
        # Le decimos que la imagen la sacará de la primera columna
        self.iconview.set_pixbuf_column(0)
        # Habilitamos el nuevo soporte de la API para tooltips
        self.iconview.set_has_tooltip(True)
        self.iconview.set_orientation(gtk.ORIENTATION_VERTICAL)
        self.iconview.set_selection_mode(gtk.SELECTION_SINGLE)
        self.iconview.set_column_spacing(10)
        self.iconview.set_columns(6)
        self.iconview.set_item_width(50)
        # Nos conectamos a la señal 'query-tooltip'
        self.iconview.connect("query-tooltip", self.show_tooltip)
        
        # Creamos el ScrolledWindow y le insertamos el IconView
        self.scrollwin = gtk.ScrolledWindow()
        self.scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.scrollwin.set_shadow_type(gtk.SHADOW_IN)
        self.scrollwin.add(self.iconview)
        
        vbox = gtk.VBox(False, 5)
        vbox.pack_start(self.scrollwin, True, True, 0)
        
        # Creamos una ventana simple y le agregamos la caja que contiene la
        # ScrolledWindow y todo lo demás
        self.window = gtk.Window()
        self.window.set_title('Tooltip de in IconView como debe ser')
        self.window.set_default_size(300, 300)
        self.window.set_position(gtk.WIN_POS_CENTER)
        self.window.connect('destroy', gtk.main_quit)
        self.window.add(vbox)
        self.window.show_all()
        
        # Creamos unos cuantos elementos dentro del modelo (esto es solo con 
        # fines ilustrativos, pues en teoría debería llenarse desde otra parte)
        for i in range(30):
            label = 'Tooltip del Elemento %i' % (i + 1)
            pix = self.window.render_icon(gtk.STOCK_ABOUT, gtk.ICON_SIZE_DIALOG)
            self.model.append([pix, label])
        del pix
    
    # Esta es la parte ruda xD
    # Nuestro callback para la señal 'query-tooltip'
    def show_tooltip(self, widget, x, y, keyboard_mode, tooltip):
        # Calculamos el offset (w y x), es decir la diferencia entre el origen 
        # del ScrolledWindow y el IconView. Para eso usamos el valor de cada uno
        # de los scrollbar. Simple ¿no?. Pues después de los psicotrópicos lo
        # ví muy sencillo :P
        w = self.scrollwin.get_property('hadjustment').value
        z = self.scrollwin.get_property('vadjustment').value
        
        # Ubicamos la ruta del elemento según la posición 'exácta' del cursor
        # sobre el IconView
        path = widget.get_path_at_pos(int(x + w), int(y + z))
        if path is None: return False
        
        model = widget.get_model()
        
        # Obtenemos el elemento mediante el modelo y la ruta
        iter = model.get_iter(path)
        
        # Obtenemos la imagen y la descripción guardada en el modelo
        pix = model.get_value(iter, 0)
        desc = model.get_value(iter, 1)
        # Establecemos la imagen del tooltip
        tooltip.set_icon(pix)
        # Establecemos el texto del tooltip (con soporte para marcado pango :D)
        tooltip.set_markup(desc)
        # Borramos la imagen para no dejar basura regada
        del pix
        
        # Devolvemos True para que se muestre el Tooltip y seamos felices weee!
        return True
        
if __name__ == "__main__":
    PruebaTooltip()
    gtk.main()

Al final veremos algo así:



Pues sí, eso es todo... una simple suma. Lo que me reventó el coco fue saber de donde diablos sacar los valores de W y Z (los offset). Espero que hayan podido leer este post antes de pensar en implementar los Tooltips a la Old-Fashion Way e incluso antes de pensar en el suicidio xD.

Cambio y fuera

1 comentarios:

Anónimo dijo...

Probaste la función gtk_icon_view_convert_widget_to_bin_window_coords()?

No estoy seguro, pero tengo la impresión que es justo lo que necesitas para poder convertir los (x, y) que vienen con el callback show_tooltip() a unas coordenadas (x', y') que funcionen correctamente en los casos que mencionas (scrolled windows).

A mi al menos me ha funcionado bastante bien :-)