Filtrado de modelos Qt en Minirok
Introducción
La búsquedas en Minirok, tanto en la vista en árbol como en la lista de reproducción, se realizan limitando las filas visibles a aquéllas que contengan la cadena introducida en la caja de búsqueda. Es decir, se produce un filtrado. Para esto, Qt proporciona la clase QSortFilterProxyModel.
La idea de esta clase, como su nombre indica, es convertirse en un proxy entre el modelo fuente y la vista, de manera que la vista sólo se comunica con el proxy, el cual recibe peticiones de la vista y las remite al modelo fuente tras realizar las modificaciones pertinentes.
Por ejemplo, si el modelo fuente tiene 10 filas, pero hay un filtro en efecto que hace que sólo deben ser visibles la 4, la 5 y la 8, el proxy le dirá a la vista que hay 3 filas (y no 10), y cuando la vista solicite el texto de la fila 1 para mostrarlo, el proxy le devolverá el texto de la fila 4 en el modelo fuente (y lo mismo para las filas 2/5, y 3/8).
Cada vez que hay un cambio en el patrón del proxy, éste notifica a la vista de que deberá solicitar de nuevo las filas, pues éstas han cambiado (puede que ahora haya 4 filas visibles, por ejemplo, o 2).
Patrones multi-palabra
En Minirok, el comportamiento de las búsquedas consiste en mostrar las filas que contengan todas las palabras de la caja de búsqueda, en cualquier orden (por ejemplo, la búsqueda “como ana” debe incluir el fichero 09_Ana Torroja - Como sueñan las sirenas.mp3).
La clase QSortFilterProxyModel realiza el filtrado mediante expresiones regulares, las cuales son insuficientes (excepto mediante contorsiones muy desagradables) para realizar matching de todas las palabras sin importar el orden.
La solución a esto es proporcionar una subclase de QSortFilterProxyModel que proporcione una implementación alternativa del método virtual filterAcceptsRow() que sea capaz de buscar todas las palabras en cualquier orden. Para los curiosos, es la clase Model del fichero proxy.py.
Mapeado de métodos
Como he mencionado arriba, la vista se comunica única y exclusivamente con el proxy, y no con el modelo fuente. QSortFilterProxyModel proporciona reimplementaciones de, por ejemplo, data(); pero ¿qué pasa con otros métodos del modelo fuente?
La respuesta es que el modelo proxy ha de proporcionar wrappers de los métodos no estándar del modelo fuente. Si, por ejemplo, el modelo fuente tiene un método toggle_enqueued(index), el proxy deberá proporcionar un método con la misma signatura, que simplemente llame al método del modelo fuente previa traducción del índice del proxy al índice fuente mediante el método mapToSource() (éste es el método que convertiría la fila 1 a la fila 4 en el ejemplo de arriba).
Esto puede ser un poco tedioso de escribir y Python, al ser un lenguaje dinámico, tiene varios mecanismos para hacerlo más fácil. Uno de ellos es utilizar decorators, así:
@map
def toggle_enqueued(self, index):
pass
Donde map es un decorador tal que:
def map(method):
"""Decorator to invoke a method in sourceModel(), mapping one index."""
def wrapper(self, index):
index = self.mapToSource(index)
return getattr(self.sourceModel(), method.func_name)(index)
return wrapper
Cabe añadir que para que estos wrappers funcionen, los métodos adicionales en el modelo fuente deben tomar índices y no números de fila, tal y como era el caso en la versión original de mi clase Playlist. Antes de poder implantar el modelo proxy, tuve que cambiar varios métodos de tomar filas a tomar índices en la revisión 620. Así mismo, eliminé varios métodos query en favor de roles de usuario en data().
Problemas al filtrar un árbol
Una vez tuve el proxy para la lista de reproducción funcionando, me puse con la vista en árbol. Aquí, sin embargo, me encontré de inmediato con un problema: el caso en el que un fichero contiene todas las palabras de la búsqueda, pero los directorios padre no. En ese caso, el comportamiento de QSortFilterProxyModel es no mostrar a los padres ni, por tanto, al hijo que contenía las palabras!
La clase KListViewSearchLine de KDE3 hacía esto bien: con la función setKeepParentsVisible(), era posible mostrar los padres aunque no contuvieran las palabras, si algún hijo de ellos las contenía. No hay nada similar en Qt 4, pero escribí a Trolltech comentando el problema y lo han aceptado como sugerencia.
Mientras tanto, no obstante, hay que buscarse la vida. Básicamente lo que he hecho es delegar el filtrado al propio modelo fuente, de manera que cuando se le pregunta si un determinado índice es visible o no, primero visita (mediante llamadas recursivas) todos sus hijos para ver si son visibles (cacheando el resultado), y si alguno de ellos lo es, marca el índice original como visible. Cuando se le pregunte si algún hijo es visible, él cálculo ya estará realizado y devolverá el valor almacenado.
Ordenación
Por último, QSortFilterProxyModel también es capaz de proporcionar a la vista una visión ordenada del modelo original. Sin embargo, no he hecho uso de esa capacidad, ya que me era necesario tener los elementos ya ordenados en el modelo fuente, para que por ejemplo al usar drag & drop, la lista de ficheros generada esté en el orden correcto.