Combos y listas con Gtk y JavaScript

Este es uno de los capítulos del tutorial Aplicaciones nativas en JavaScript con GJS. Encontrarás los enlaces a todos los de capítulos, al final de este artículo.

Si has llegado hasta este capítulo del tutorial, ya tienes en tu caja de herramientas para implementar aplicaciones, una buena cantidad de clases con las que ofrecer al usuario una buena experiencia de uso. Sin embargo, a pesar de ello, todavía hay opciones disponibles para mejorar la usabilidad de tu aplicación. En concreto en este capítulo del tutorial quiero centrarme en las listas y las listas desplegables, es decir combos y listas con Gtk y JavaScript. Estas dos clases te van a permitir preguntar al usuario y ofrecerle las distintas alternativas, con lo que, de nuevo le facilitas al usuario la vida.

Tanto las listas, como las listas o cajas desplegables, no son mas que una evolución de la clase Gtk.Entry que ya pudiste revisar en el capítulo sobre Editar test con GTK y JavaScript. Y me refiero a una evolución en el sentido de que permiten centrar el tiro, a la hora de preguntar al usuario.

Combos y listas con Gtk y JavaScript

Combos y listas con Gtk y JavaScript

Antes de introducirte con las clases que te van a permitir el uso de combos y listas en Gtk y JavaScript, advertirte que esto no es tan sencillo como te podrías imaginar inicialmente. Vamos, que es algo mas complicado. Aquí, para esto de los combos y listas intervienen varias clases, que te permiten, por un lado organizar la información que vas a mostrar y por el otro pintarla.

En este capítulo, al contrario de lo que he venido haciendo en capítulos anteriores, voy a indicarte que clases intervendrán y cual es su función, para posteriormente, ver su aplicación práctica en el ejemplo.

Árboles y listas

Las clases Gtk.TreeStore y Gtk.ListStore, son las encargadas de guardar y gestionar los datos que se mostrarán posteriormente en otros elementos, en las vistas. La primera de las clases organiza los datos en forma de árbol, de forma que cada fila de datos, puede tener un padre y a su vez, puede o no, tener varios hijos. Por otro lado, la clase Gtk.ListStore, almacena los datos en filas únicamente, una encima o debajo de otra.

Gtk.TreeStore

Esta primera clase te permite almacenar las filas en forma de árbol, por lo que tienes que indicar en que rama del árbol se encuentra. Si, no le indicas, nada, simplemente podrá una rama encima de la anterior, o debajo, según lo veas. A continuación declaro el objeto, lo defino y le añado algunos valores, y a continuación te explicaré que es cada cosa.

let model = new Gtk.TreeStore();

model.set_column_types([
    GObject.TYPE_STRING,
    GObject.TYPE_FLOAT
]);

let iter0 = model.append(null);
model.set(iter0, [0, 1], ["Especial", 0.9]);

iter1 = model.insert(iter0, null);
model.set(iter1, [0, 1], ["Melones", 2.5]);

model.set(model.append(null), [0, 1], ["Peras", 2.0]);
model.set(model.append(null), [0, 1], ["Melocotones", 1.3]);

Esto en funcionamiento, es lo que puedes ver en la siguiente captura de pantalla,

Un arbol con Gtk y JavaScript

¿Pero que es cada cosa? Vamos allá,

En la primera línea let model = new Gtk.TreeStore(), declaras el objeto que va a contener la información. En las cuatro líneas siguientes defines tanto el número de columnas, como el tipo de dato que contendrá nuestro objeto. En este caso, serán dos columnas. La primera de la columna será texto, definido por GObject.TYPE_STRING, mientras que la segunda será un número, definido por GObject.TYPE_FLOAT.

Además de estos dos, tienes muchos otros tipos como GObject.TYPE_INT, GObject.TYPE_BOOLEAN, etc. La verdad, es que el único sitio donde he encontrado un listado de estos tipos es en la página de desarrollo de GNOME.

Ahora solo queda añadir las ramas al árbol. La forma de hacerlo, es crear primero un Gtk.Iter. El Gtk.Iter es un objeto que apunta a una rama del árbol. Así, para crear nuestro Gtk.Iter, tenemos dos opciones, la primera es directamente utilizando model.append(null), que añadirá un nuevo puntero al final del árbol. La segunda de las opciones, es insertar la rama en otra. ESto lo hacemos con model.insert(iter0, null). Donde indicamos como primer parámetro de que rama cuelga.

Hecho todo esto, queda definir lo que hay en la rama… Esto lo vas a hacer con model.set, donde le indicas el puntero, las columnas que añades, y el contenido. Por ejemplo,

model.set(iter0, [0, 1], ["Especial", 0.9]);

Curioso ¿no te parece?

Gtk.ListStore

La siguiente de las clases que puedes utilizar para guardar información es Gtk.ListStore. En este caso, la definición es exactamente igual que en el caso anterior, pero se diferencia a la hora de añadir, que es realmente mucho mas sencillo, porque no te tienes que preocupar de en que rama tienes que ponerlo. Aquí, simplemente es una debajo de la otra. Por ejemplo,

let model = new Gtk.ListStore();

model.set_column_types([
    GObject.TYPE_STRING,
    GObject.TYPE_FLOAT
]);

model.set(model.append(), [0, 1], ["Peras", 2.0]);
model.set(model.append(), [0, 1], ["Melones", 3.4]);
model.set(model.append(), [0, 1], ["Melocotones", 1.5]);

En lugar de crear primero el puntero con let iter = model.append(), directamente lo pongo en la misma línea en la que añado el contenido, dado que en este caso, no te tienes que preocupar de si va en una rama u otra, simplemente una debajo de otra.

Gtk.TreeView

El siguiente elemento es la clase que te va a permitir mostrar la información, los datos, que has cargado con alguna de las clases anteriores, Gtk.TreeStore o Gtk.ListStore. Como ves, está perfectamente diferenciado la vista, que es lo que vas a ver aquí, del modelo, que viste, en los apartados anteriores.

Con el Gtk.TreeView, también hay bastante trabajo para hacer. No solo está en definir la vista, también hay que crear cada una de las columnas, e indicar, el texto que contendrán. Pero no solo, esto, también les tienes que indicar, como se visualizarán. Así, que vamos allá,

let bold = new Gtk.CellRendererText({
    weight: Pango.Weight.BOLD
});
let normal = new Gtk.CellRendererText({
    weight: Pango.Weight.LIGHT
});

let column0 = new Gtk.TreeViewColumn({
    title: "Fruta"
});
let column1 = new Gtk.TreeViewColumn({
    title: "Precio"
});

column0.pack_start(bold, true);
column0.add_attribute(bold, "text", 0);
column1.pack_start(normal, true);
column1.add_attribute(normal, "text", 1);

let treeView = new Gtk.TreeView({
    model: model
});
treeView.insert_column(column0, 0);
treeView.insert_column(column1, 1);

layout.attach(treeView, 1, 0, 1, 1);

Tienes que ir construyendo desde los cimientos hasta el tejado. Empezando lo primero por los formatos que vas a utilizar para mostrar la información. En este caso, he utilizado diferentes pesos para la tipografía. Esto, es lo que puedes ver en las primeras líneas,

let bold = new Gtk.CellRendererText({
    weight: Pango.Weight.BOLD
});

El siguiente paso es construir las columnas. Las columnas, son de la clase Gtk.TreeViewColumn. Estas clase tiene una gran cantidad de propiedades y métodos que te van a permitir personalizarlo según tus necesidades. De la misma forma, también tiene algunas señales interesantes, que te permite conocer cuando el usuario ha realizado algún tipo de acción sobre la vista. Para construir la vista simplemente tienes que utilizar la siguiente sintaxis,

let column0 = new Gtk.TreeViewColumn({
    title: "Fruta"
});

Ahora que ya tienes todas las piezas solo te queda ensamblarlo. Tienes que asignar a la columna el elemento que se encarga de renderizar, Gtk.CellRendererText, y definir el atributo. Esto lo haces con las siguientes líneas,

column0.pack_start(bold, true);
column0.add_attribute(bold, "text", 0);

Y el último paso, es añadir esas columnas a la vista, utilizando,

treeView.insert_column(column0, 0);

Y con esto queda todo completado para el caso del Gtk.TreeView.

Gtk.ComboBox

Otra clase que puedes utilizar para mostrar la información es Gtk.ComboBox. Se trata de las típicas cajas de desplegables, o combos. De nuevo, esta clase es significativamente mas sencilla de utilizar que la anterior, aunque tiene muchas similitudes con ella. Resumiendo, y dejando aparte las clases de renderizado, que ya están definidas en el apartado anterior, esto se resumiría en lo siguiente,

let combo = new Gtk.ComboBox({
    model: model
});
combo.pack_start(bold, true);
combo.add_attribute(bold, "text", 0);
combo.pack_start(normal, true);
combo.add_attribute(normal, "text", 1);

Esto último te suena claramente del apartado anterior. Ahora solo te queda ver el resultado, que tiene que ser muy similar al que te muestro en la siguiente captura de pantalla,

Un combo con Gtk y JavaScript

Vaya, no se muestra ningún dato preseleccionado. Parece que falta algo. Pero ¿como seleccionar un dato por código?. Tienes dos opciones, o bien lo indicas en la propia declaración del combo, o bien posteriormente. En el primer caso, sería de la siguiente forma,

let combo = new Gtk.ComboBox({
    model: model,
    active: 0
});

La siguiente opción sería utilizar el método set_active como te muestro a continuación,

combo.set_active(0);

Por supuesto, que el siguiente paso, sería preguntar cual ha sido el elemento seleccionado por el usuario. Esto, lo puedes hacer de la siguiente forma,

let dialog = new Dialog();
if (dialog.run() == Gtk.ResponseType.OK){
    let model = dialog.combo.get_model();
    let selected = dialog.combo.get_active_iter()[1];
    log(model.get_value(selected, 0));
}

Para poder llamar al objeto combo, lo he tenido que declarar como un atributo de la clase, es decir,

this.combo = new Gtk.ComboBox({
    model: model,
    active: 0
});

Si te fijas, para saber el elemento activo utilizo dialog.combo.get_active_iter()[1]. La razón del [1], es porque este método devuelve dos valores, un booleano, y el puntero al elemento seleccionado. En el caso de que haya uno seleccionado devolverá true, pero en otro caso devolverá false.

En el caso del Gtk.TreeView, como te puedes imaginar, se complica ligeramente,

let selected = dialog.treeView.get_selection();
let [isSelected, model, iter] = selected.get_selected();
log(model.get_value(iter, 0));

La razón para esta complicación reside en el hecho de que con esta clase es posible, según lo hayas definido seleccionar varios elementos, en lugar de uno solo.

El código

Lo único que me queda pendiente es mostrarte el código completo para que lo puedas ejecutar en tu propia máquina. En este caso, he puesto el código correspondiente al Gtk.TreeView que es el mas complicado. Para el caso del Gtk.ComboBox, simplemente tienes que ir reemplazando las piezas que te he ido indicando a lo largo de este capítulo,

#!/usr/bin/env gjs

imports.gi.versions.Gtk = '3.0'
const {Gtk, GObject, GdkPixbuf, Pango} = imports.gi;

Gtk.init(null);

var Dialog = GObject.registerClass(
    class Dialog extends Gtk.Dialog{
        _init(){
            super._init({
                defaultWidth: 200,
                defaultHeight: 200
            });
            let layout = new Gtk.Grid({
                margin: 10,
                rowSpacing: 5,
                columnSpacing: 5
            });
            this.add_button("Aceptar", Gtk.ResponseType.OK);
            this.add_button("Cancelar", Gtk.ResponseType.CANCEL);
            this.get_content_area().add(layout);
            let label = new Gtk.Label({
                label: "Elige"
            });
            layout.attach(label, 0, 0, 1, 1);
            let model = new Gtk.TreeStore();
            model.set_column_types([
                GObject.TYPE_STRING,
                GObject.TYPE_FLOAT
            ]);
            let iter = model.append(null);
            model.set(iter, [0, 1], ["Especial", 0.9]);
            model.set(model.append(null), [0, 1], ["Peras", 2.0]);
            model.set(model.insert(iter, null), [0, 1], ["Melones", 2.5]);
            model.set(model.append(null), [0, 1], ["Melocotones", 1.3]);

            let bold = new Gtk.CellRendererText({
                weight: Pango.Weight.BOLD
            });
            let normal = new Gtk.CellRendererText({
                weight: Pango.Weight.LIGHT
            });

            let column0 = new Gtk.TreeViewColumn({
                title: "Fruta"
            });
            let column1 = new Gtk.TreeViewColumn({
                title: "Precio"
            });
            column0.pack_start(bold, true);
            column0.add_attribute(bold, "text", 0);
            column1.pack_start(normal, true);
            column1.add_attribute(normal, "text", 1);

            this.treeView = new Gtk.TreeView({
                model: model
            });
            this.treeView.insert_column(column0, 0);
            this.treeView.insert_column(column1, 1);

            layout.attach(this.treeView, 1, 0, 1, 1);
            this.show_all();
        }
    }
);

let dialog = new Dialog();
if (dialog.run() == Gtk.ResponseType.OK){
    let selected = dialog.treeView.get_selection();
    let [isSelected, model, iter] = selected.get_selected();
    log(model.get_value(iter, 0));
}

Conclusión

Esto de dar opciones al usuario, para que elija una, podía parecer algo trivial, pero como tu mismo has podido comprobar, de trivial no tiene nada. Tampoco es que sea excesivamente complicado, pero es necesario conocer las distintas piezas que lo componen, para poder hacerlo de forma efectiva. Esta es la razón de dedicar un capítulo del tutorial, precisamente a, combos y listas con Gtk y JavaScript.

Esto, no es mas que la punta del iceberg de las posibilidades que te permiten estas clases. Ten en cuenta que aquí se puede desde mostrar imágenes, combinando con lo que viste en el capítulo anterior, hasta editar en el propio Gtk.TreeView.


Más información,

Imagen de portada de Glenn Carstens-Peters en Unsplash

Deja una respuesta

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