Como crear búsquedas personalizadas para Ubuntu de la mano de GNOME Shell

Este es uno de los capítulos del tutorial Crea tu propia extensión para GNOME Shell. Encontrarás los enlaces a todos los de capítulos, al final de este artículo.

Hasta el momento solo hemos visto como crear un indicador de aplicación para situarlo en el panel superior. Sin embargo, GNOME Shell nos pone muy fácil otra característica como son las búsquedas personalizadas.

¿Que es eso de las búsquedas personalizadas?. Las búsquedas personalizadas nos permite crear nuevos elementos de búsqueda, que no tienen porque estar necesariamente en nuestro dispositivo. Al hacer clic en la tecla Super, vemos una caja de texto donde podemos escribir, no solo el nombre de la aplicación que queremos lanzar, sino que podemos escribir cualquier cosa que queramos buscar.

Así, por ejemplo, GNOME Shell trae por defecto la posibilidad de realizar operaciones matemáticas sencillas, y no tanto en esa propia caja de texto. Pero, también podemos instalar otras extensiones que nos permitan realizar búsquedas personalizadas, o incluso, crear nuestras propias búsquedas personalizadas. Búsquedas personalizadas como puede ser la definición de una palabra, un sinónimo,…

En este capítulo del tutorial, vamos a ver como crear nuestra propia búsqueda personalizada. En particular vamos a crear un buscador para que nos muestre la definición de palabras desde WordReference, sin necesidad de recurrir al navegador para ver la definición.

Como crear búsquedas personalizadas para Ubuntu de la mano de GNOME Shell

Búsquedas personalizadas como extensión

Como cualquier extensión, las búsquedas personalizadas también necesitan de como mínimos dos archivos, tal y como vimos en el primer capítulo de este tutorial

  • metadata.json, que es donde se define el nombre, descripción, identificador, etc, de la extensión.
  • extension.js, que es el archivo donde se definen los métodos para inicializar, habilitar y desahabilitar la extensión. Normalmente en este archivo también se define la clase que da sentido a la extensión, pero no es necesario. También se puede definir la clase en un segundo archivo e importarlo, como cualquier otro módulo. Esta segunda opción facilita la claridad en la extensión.

Estructura de la clase de búsquedas personalizadas

La clase de búsquedas personalizadas tiene unos métodos mínimos. Estos métodos son los que proporcionan precisamente la búsqueda. Estos métodos son los siguientes,

  • activateResult. Este método es el que se encarga de abrir la aplicación por defecto al hacer clic sobre el resultado. Es interesante poder definir algunos resultados por defecto sobre los que no se realizará ninguna acción. Por ejemplo, en el caso de que no se haya encontrado nada, se mostrará un mensaje en este sentido, pero no tiene razón de ser abrir una aplicación al hacer clic sobre este mensaje. Lo mismo podemos decir para el caso de que se haya producido un error. En la extensión que he realizado para completar el asunto, abre la página web de la que toma los datos. También se podría haber hecho que copiara el resultado al portapapeles… A lo mejor en una futura versión.
  • getResultMetas. Es el método principal encargado de trabajar con los resultados. Lo que hace es asociar cada identificador con el resultado correspondiente, los empaqueta en un array y llama a la función que se encarga de mostrarlos. Aquí se compara el identificador con nuestros mensajes por defecto, y en el caso de que coincida sube ese mensaje.
  • getInitialResultSet. Este método es el encargado de realizar la búsqueda. Normalmente se le define alguna característica para habilitar la búsqueda. En el ejemplo que he incluido, solo realiza la búsqueda en el caso de que el texto empiece por d:, (d de diccionario).
  • getSubsetResultSearch. Para el caso de que existan búsquedas secundarias. Esto no lo he utilizado.
  • filterResults. Este método tampoco lo he utilizado.

Las funciones principales

Tal y como vimos en un capítulo anterior de este tutorial, en el archivo extension.js, debíamos definir tres funciones, para el correcto funcionamiento de la extensión de GNOME Shell. Evidentemente, esta extensión no podía ser diferente del resto, y tiene esas tres funciones,

  • init. En esta extensión se utiliza para iniciar el módulo de internacionalización.
  • enable. Es la función que se ejecuta al habilitar la extensión.
  • disable. Esta función se ejecuta cuando se deshabilita la extensión.

En particular, enable y disable son tal y como siguen a continuación,

function enable() {
    if (!wordReferenceSearchProvider) {
        wordReferenceSearchProvider = new WordReferenceSearchProvider();
        Main.overview.viewSelector._searchResults._registerProvider(
            wordReferenceSearchProvider
        );
    }
}

function disable() {
    if (wordReferenceSearchProvider){
        Main.overview.viewSelector._searchResults._unregisterProvider(
            wordReferenceSearchProvider
        );
        wordReferenceSearchProvider = null;
    }
}

Como puedes ver, en la función enable se registra un objeto de la clase WordReferenceSearchProvider. Siempre y cuando este objeto no exista ya, claro. Y se registra en el proveedor de resultados de búsqueda.

Por otro lado, en la función disable, se hace justo la operación contraria. Es decir, en el caso de exista nuestro proveedor de búsquedas, se anula el registro y se anula nuestro objeto.

El proveedor de búsquedas

Como hemos comentado anteriormente, nuestra clase proveedora de búsquedas, define una serie de métodos y propiedades mínimas.

Entre las propiedades mínimas hay que destacar las siguientes,

  • this.appInfo nos permite utilizar la aplicación por defecto para abrir enlaces del tipo https.
  • this.appInfo.get_name, se utiliza para definir el nombre de nuestro proveedor de búsquedas.
  • this.appInfo.get_icon, es la propiedad con la que definimos el icono de nuestro proveedor de búsquedas.
  • this._messages, nos permite definir algunos mensajes por defecto, como el mensaje de cargando o el de error.
  • this.resultsMap, es la propiedad donde guardaremos los resultados de la búsqueda. Este objeto Map se encarga de mapear los identificadores de los resultados con su valor.
  • this._timeoutId, es el tiempo que debemos esperar hasta realizar la consulta. Esto deja tiempo para que el usuario termine de escribir la consulta antes de comenzar con la búsqueda. De otra manera, cada vez que pulsamos una tecla se realizaría una búsqueda. Esto terminaría por arrojar un error.

La estructura de los mensajes

Los mensajes tienen una estructura como la que se define a continuación,

'__loading__': {
                id: '__loading__',
                name: 'WordReference',
                description : 'Loading items from WordReference, please wait...',
                createIcon: this.createIcon()
            }

Donde

  • id es el identificador del mensaje.
  • name es el nombre del mensaje.
  • description la descripción del mensaje.
  • createIcon define el icono del mensaje para la búsqueda.

De esta forma, debemos configurar el resultado de las búsquedas que hagamos con nuestra API para que se adapte a esta estructura.

Así, en nuestra clase, hemos definido una función _getResultMeta que precisamente se encarga de mapear nuestros resultados para que cumplan con la estructura definida.

Búsquedas con retraso

Como he comentado en la función getInitialResultSet es donde se realizan las búsquedas. El problema (o no), es que cada vez que el usuario pulsa una tecla se realiza una búsqueda. Esto supone una llamada a la API de la página web. Lo cual supone un consumo de datos innecesario por un lado. Por otro lado, continuamente estaremos viendo el mensaje de error, porque no encontramos el resultado que buscamos.

Para resolver este problema vamos a utilizar las funciones GLib.timeout_add y GLib.source_remove. La primera de las funciones lo que hace es retrasar la ejecución de la búsqueda durante un tiempo determinado. La segunda de las funciones, lo que hace es evitar que se ejecute la función. Es decir, nosotros programamos la búsqueda para que se haga a los 800 ms, si durante ese tiempo, hemos pulsado otra tecla, lo que hace es, detener la primera programación y crear una nueva. Así sucesivamente hasta que transcurran esos 800 ms. Esto lo puedes ver en el siguiente código,

if (this._timeoutId > 0) {
    GLib.source_remove(this._timeoutId);
    this._timeoutId = 0;
}
this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 800, () => {
    // now search
    this._api.get(
        this._getQuery(terms.join(' ')),
        this._getResultSet.bind(this),
        callback,
        this._timeoutId
    );
    return false;
});

Conclusión

La ventaja de las búsquedas personalizadas es que nos permite desde realizar una operación matemática a encontrar la definición de una palabra sin necesidad de lanzar ninguna aplicación. Esto redunda en una mejora de nuestra productividad, puesto que nos permite enfocarnos en nuestro trabajo, y tener el mínimo número de aplicaciones en funcionamiento.

Ahora puedes adaptar tu escritorio a tus necesidades. Por ejemplo si te dedicas a escribir, lo suyo es que tengas un diccionario al alcance de los dedos. Eso, sin necesidad de que tengas una aplicación abierta. Lo mismo te digo para el caso de si te dedicas a realizar cálculos matemáticos, o cualquier otra cosa que se te puede pasar por la cabeza.

Solo he puesto aquella parte del código interesante para la explicación de como crear tu propio proveedor de búsquedas. Si te interesa, puedes consultar todo el código de la aplicación que está disponible en el repositorio de GitHub WordReference Search Provider