El widget de cabeceras HTTP por fin está hecho

Como dicen que una imagen vale más que mil palabras, os quiero enseñar una cosa.

Efectivamente, el widget de cabeceras HTTP por fin está programado, lo que significa que por fin se puede completar toda la información necesaria para tirar una petición HTTP mínima con Cartero. Dejad que os cuente cómo funciona por dentro.

Cuando planteé el primer prototipo de la interfaz de usuario, anticipé que este iba a ser el widget más complicado y el que más lata iba a dar. Definitivamente no por cantidad de elementos que hay que sostener en la pantalla, que no son tantos, sino por la complejidad del modelo de widgets.

Primer sobre Gtk.ListView

El componente para editar las cabeceras de una petición HTTP es una lista. Concretamente, una Gtk.ListView. Este widget sirve para presentar una lista de elementos genérica, sea del tipo que sea.

Me gusta poner el ejemplo de una pantalla de opciones donde seleccionas la red wifi a la que te quieres conectar. Cada red wifi se corresponde con una fila de la lista. Sin embargo, a diferencia de una lista aburrida, Gtk.ListView te permite personalizar cómo quieres que se vea cada elemento de la lista.

Una ListView tiene tres partes. Lamentablemente es un poco confuso porque cada cosa está relacionada entre sí:

  • Un modelo de lista, que representa la parte interna con los datos que van asociados a la lista de verdad. En el caso de unas opciones de wifi, el modelo de lista podría ser una lista de objetos (GObject), donde cada objeto tiene sus propiedades (nombre de la red, intensidad de señal, protección…). Esa lista es de tipo Gio.ListStore porque ya está programada y tiene métodos para insertar y quitar elementos de la lista.
  • Un modelo de selección, que le dice a la ListView cómo seleccionar cosas. Es una interfaz un poco confusa y antipática, pero las claves a entender son:
    • Sirve para que la ListView pueda mantener y cambiar una selección. Por ejemplo, que cuando se haga clic sobre un elemento de la lista, se seleccione completamente.
    • Generalmente usamos uno de los modelos de selección ya disponibles en función de cuántos elementos queremos que se puedan seleccionar en simultáneo: Gtk.NoSelection, Gtk.SingleSelection o Gtk.MultiSelection. Instanciar y asignar.
    • Es otra lista (también implementa la interfaz Gio.ListModel), y de hecho el modelo de selección es quien le explica a la ListView qué elementos hay. Así que os he mentido un poco: no es el modelo de selección, es el modelo (a secas), y una ListView tiene dos partes, no tres. Cuando la lista tenga que representar cosas (como las wifis disponibles), se lo va a preguntar a este modelo. De todos modos, las implementaciones (NoSelection, SingleSelection y MultiSelection) a su vez envuelven el otro modelo de lista, así que preguntarle al modelo de selección cuántos elementos tiene la lista o cuál es el elemento en la posición N simplemente redirige la llamada al otro modelo de lista.
  • Una factoría que sirve para explicarle a la ListView cómo pintar cada uno de los elementos de la lista. Todo lo que tiene que hacer es asociar cada elemento del modelo (cada red wifi) con un widget, que es lo que la ListView pintará en la fila. Por ejemplo, con una Label que pone el nombre de la red wifi, o un Gtk.Box más desarrollado que muestre iconos y botones.

Verdaderamente no es un widget para gente recién llegada, pero si alguien está buscando más información sobre cómo agregar una ListView a una aplicación basada en GTK, puedo recomendar los siguientes recursos:

Unidireccionalmente todo parece funcionar bien

En principio, si sólo quisiese mostrar información de una lista, con todo esto podría tirar.

Mi implementación en Cartero se basa en una clase de datos pura (un GObject) llamado Header (o CarteroHeader), que tiene tres propiedades con la información sobre una cabecera HTTP.

Modelo UML de la clase Header. Tiene tres campos: active, header-name y header-value

Existe un widget llamado HeaderRow, que es una Box que pinta los campos de texto y el checkbox para introducir información sobre una cabecera y pintarla a nivel visual.

Para hacer mi lista de headers, voy a necesitar instanciar una ListStore a la que le pueda hacer append de las distintas cabeceras a insertar, y remove cuando se pulse el botón Eliminar para descartar esa cabecera. Esa ListStore va a estar envuelta en una NoSelection, porque no quiero que se pueda seleccionar cada fila para dar una sensación de unidad entre la lista. Sin embargo, tengo que guardar mi propia referencia a la ListStore de dentro1.

Mi lista va a tener una factory que le explique a la ListView que cada fila de la lista es un HeaderRow, y le va a ligar la información de la Header para mostrar ese texto en la lista y también para, idealmente, permitir que quien use la aplicación pueda editar esa cabecera.

Inicialmente, esto funciona bien para presentar datos. Es decir, tienes una ListModel ya existente con datos de cabeceras. Le asocias una factory, y la ListView mapea correctamente cada cabecera en un widget, le rellena los datos, y lo expone por pantalla. Buena parte de lo que se hizo en el VOD número 6 va de esto. Lamentablemente, lo que no pude hacer en el VOD número 6 fue agregarle reactividad correctamente, de tal forma que modificar en la interfaz de usuario un campo de texto actualizase la variable interna de la Header en la lista.

GTK es reactivo…

Antes de continuar, aclarar que GTK es reactivo. Igual que tu framework JavaScript favorito, en GTK existe el concepto de los bindings, para hacer que dos propiedades, por ejemplo, la propiedad header-name de un Header, y la propiedad text de un Entry, estén sincronizados.

De este modo, puedes hacer que cambiar el valor de header-name (nuestra «variable») también actualice el texto que se muestra en el Entry. Simultáneamente, escribir sobre ese Entry actualizará el valor de la variable.

La clase Header y el widget HeaderRow están perfectamente alineadas. Para mí sería muy fácil establecer un bind entre las tres propiedades de cada clase y ligarlas de tal manera que la HeaderRow muestre inicialmente los valores que hay originalmente en mi Header, pero tocar los inputs de la interfaz actualicen el valor interno de la Header.

…pero tienes que saber cuándo ser reactivo

Sin embargo, la razón por la que he tardado tanto tiempo en tener este componente es que he forzado esa reactividad a ir demasiado más allá. En particular, hay dos cosas que no van a funcionar bien.

En primer lugar, es mejor hacer binding de propiedades pequeñas y fáciles de serializar, como una cadena de caracteres o un boolean, antes que hacer un binding de toda una clase personalizada. Los bindings tampoco son magia, es simplemente una consecuencia de que GObject use getters y setters para las propiedades de una clase, que se puede hacer que ya que haces un set, se notifique a un par de watchers que estén pendientes de una variable para propagar ese cambio a otros objetos.

Si asocio una variable completa de tipo Header en vez de cada primitiva, estoy metiendo demasiadas cosas que pueden fallar. Por ejemplo, que una parte del binding no se dé cuenta de que ha cambiado la otra parte porque la «variable» de tipo Header sea la misma referencia y por lo tanto el mismo objeto que antes. (Quien haya usado React sabrá a qué me refiero.)

Otro problema es que inicialmente quise hacer reactiva toda la ListView. En otras palabras, pretendía tal cual pasarle a la ListView todo mi ListStore con mis headers y esperar que de algún modo supiese cómo transformar ese cambio en un input en actualizar una variable dentro de la lista conceptual de headers.

Todo el rato, GTK me decía que no era posible crear un binding bidireccional (es decir, que pudiese luego traerse cambios de la UI al modelo) dentro de una lista. Y después de recapacitar en frío, tal vez tenga razón. Generalmente las listas se usa para elegir información de un rango de opciones ya determinado, pero permitir modificar directamente una lista no es un caso de uso muy extendido.

Por lo tanto, llegué a la conclusión hace unos días de que simplemente no iba a ser posible hacer reactiva de golpe toda una lista y esperar que GTK hiciese su magia.

Dividiendo el problema en subproblemas

Esa factoría que he dicho antes que le explica a una ListView cómo pintar cada elemento de su lista tiene dos implementaciones alternativas: Gtk.BuilderListItemFactory y Gtk.SignalListItemFactory.

La Builder es más automática, y es la que inicialmente tomé. En la Builder, todo lo que se hace es pasarle un recurso de interfaz (o sea, un XML de Builder) con la plantilla, y lo que hace esta factory es usar la plantilla para crear una copia del widget para cada elemento de la ListModel que hay detrás y ponerlo en pantalla. Mediante lookups (o sea, un binding unidireccional), se puede traer una propiedad de ese objeto de la ListModel y así pintarlo en una label, o en un checkbox.

La Signal, por otra parte, es más manual. Como da pista su nombre, hay cuatro señales (o sea, cuatro callbacks) que hay que implementar para crear el comportamiento de la factory. Las funciones se ejecutan en momentos puntuales y sirven para crear y destruir las filas de la lista y asociarles un objeto del ListModel.

Esta es la solución que implementé ayer después de estudiarla y la que parece que está funcionando correctamente. He dividido el problema de «cómo mostrar esta lista de cabeceras» en dos partes, con dos sistemas de reactividad independientes, que funcionan mejor por separado que tratando de hacer uno único global.

Por un lado, la lista es reactiva por sí misma, en el sentido de que si agregas o quitas un elemento de la lista, esto va a provocar que dinámicamente se pongan o quiten filas de la lista. Esto ocurre ahora mismo cuando pulsas el botón Agregar, o el botón Quitar. Provoca que se llame al método append / remove correspondiente de la lista, disparando las señales que causan que se meta una fila nueva o se borre una ya existente.

Por otro lado, la SignalListItemBuilder me pide en una de las señales que implemente el código que quiero que se ejecute cuando la ListView se dispone a mostrar una de esas filas, para que lo asocie con el elemento del modelo que le haya tocado.

Este proceso lo tengo que hacer a mano, e incluye:

  • Poner los valores iniciales en los inputs del widget a partir del valor que tenga la header en este momento. (Ahora que lo pienso, este paso no debería ser necesario porque para eso tengo mis binds…)
  • Definir los binds para que a partir de ese instante, modificar los campos de texto actualice de vuelta el valor en la header que hay dentro de la lista.
  • Conectar el event listener del botón Eliminar, para que si se pulsa, pueda recibir adecuadamente el evento de borrado y así eliminar esa fila.

Una cosa importante es que igual que fabrico el código que se ejecuta cuando se va a pintar una fila para ponerle esa información, también tengo que ponerle la señal que desconecta el widget de la cabecera. GTK va a intentar reciclar los widgets todo lo que pueda. Eso significa que si borro una fila, probablemente no descarte del todo mi widget, sino que lo deje en un pool de filas sin usar para reciclarla más adelante. Si no borro las señales y los binds previamente establecidos, ocurriría que al pulsar un botón o cambiar un campo de texto se dispararían varias señales y empezaría a modificarse información de otras filas.

¿Ahora qué?

Con este widget por fin implementado, tengo toda la información necesaria para fabricar la petición HTTP. En mi próxima sesión de programación, que es la que voy a hacer hoy a través de mi canal de YouTube y el de Twitch, terminaré de implementar el cliente HTTP, con la información de las cabeceras, y empezaré con el panel de respuesta. Hay que mostrar tanto las cabeceras de la respuesta como el cuerpo. Por lo menos esa información es de solo lectura, así que será más fácil de elaborar.

Notas al pie

  1. Por una serie de cuestiones, la interfaz ListModel sólo ofrece funciones de lectura, como la función que te dice cuántos elementos tiene la lista, y la que te da el elemento en la posición N. La idea es centrarse en lo que une a una lista de sólo lectura con una lista de lectura y escritura. ListStore implementa ListModel y la extiende agregando los métodos de modificación (append / remove), pero hay que tener en cuenta que todo el framework de listas trabaja con la interfaz ListModel, así que necesitamos castear todo el tiempo a ListStore o conservar una referencia rápida para poder agregar y quitar elementos de la lista. ↩︎

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *