JComboBoxConfigurableLookUp
Componente de tipo lista desplegable, diseñado de modo configurable con el fin de cubrir la mayor parte de necesidades de usabilidad de este tipo de componentes en la Interfaz Gráfica de Usuario.
Introducción
Cuando un desarrollador en Java, diseña la interfaz gráfica de usuario (GUI), puede requerir tener un componente con una lista de ítems, de modo que permita al usuario fácilmente seleccionar el que desee.
Java nos ofrece un componente base: JComboBox, que, de un modo poco flexible, facilita la selección del ítem deseado de entre un conjunto listado.
Cuando el desarrollador compara JComboBox con otros componentes similares existentes en aplicaciones relativamente complejas, se encuentra con una gran limitación en cuanto a sus posibilidades.
Con el fin de superar dichas limitaciones, y manteniendo la filosofía Java para el desarrollo de componentes de GUI según Swing, se decidió crear un componente de tipo Java Bean, siguiendo el patrón de diseño software Modelo-Vista-Controlador (MVC), que, por tanto sería reutilizable, y que, mediante una simple configuración inicial, satisfaga los requisitos de comportamiento necesarios para el desarrollador.
El componente actual es fruto de una evolución durante más de un año, de un componente previo (el antiguo JComboBoxItemsSeekerConfigurable), al que se le ha agregado funcionalidad, y depurado.
Puntos fuertes
- Diseñado para particularizarlo fácilmente a unas necesidades de comportamiento, superando las múltiples limitaciones que posee la versión nativa de Java: JComboBox .
- Diseñado para que sea compatible y fácilmente migrable a partir de la mayoría de las versiones que puedan ya existir en la GUI, de modo que se pueda pasar a utilizar el nuevo componente sin excesivo esfuerzo de programación.
- Compatibilidad con reglas de ordenación de un idioma en particular.
- Por defecto utiliza un agente que ya tiene implementada la búsqueda más común con la que se puede utilizarlo, es una versión de búsqueda binaria.
- Modelo (según el patrón software MVC: Modelo-Vista-Controlador) también configurable, y adaptado para que pueda utilizarlo correctamente el control de este componente.
- 2 tipos de tests:
- Para programadores, donde se puede probar cambiando código.
- Mediante una aplicación con GUI, que permite crear distintos objetos de tipo JComboBoxConfigurableLookUp con distinta configuración y realizar gran cantidad de tests.
- Opcional: posibilidad de crear tu propio agente con una búsqueda personalizada que será la que utilice el modelo del componente para mostrar las coincidencias con el texto escrito.
- Opcional: se puede utilizar un componente de tipo IEditableText en el editor, de modo que tienes soporte para las operaciones más habituales de edición por teclado y por ratón: copiar, cortar, pergar, deshacer, rehacer, seleccionar todo, y borrar.
Descripción
Versión Previa
Adaptar JComboBox implicaba conocer cómo estaba implementado.
El siguiente diagrama muestra una simplificacion de la implementación del componente Swing JComboBox en Java 1.5, según el patrón MVC. Se ha destacado aquello más sensible a cambiar con la nueva versión o que concentra gran cantidad de la lógica del componente:

Diagrama 1: diagrama de clases simplificado de la versión de JComboBox de Java 1.5 . Se ha coloreado las clases que más van a cambiar en JComboBoxConfigurableLookUp.
Teníamos como clase principal: JComboBox, que contiene modelo, vista, y control:
- El modelo estaba claramente separado en la clase DefaultComboBoxModel .
- La vista, tiene un control propio, y, en combinación, la lógica de ambos está repartida en distintos componentes y clases:
- Un ComboEditor con un JTextField interno, que mostrará el ítem seleccionado, y que, en caso que esté en modo editable, permitirá escribir texto.
- Un JButton para mostrar/ocultar un popup con ítems que contiene el componente.
- Un ComboPopup, que es de tipo JPopupMenu, y que contiene una JList (lista) en un JScrollPane, con los ítems renderizados por objetos de tipo ListCellRenderer.
La mayor parte de la lógica de la vista y el control asociado, incluido el de interacción con el usuario está concentrado en BasicComboBoxUI, que en Linux, por defecto es MetalComboBoxUI, mientras que en Windows es WindowsComboBoxUI.
- El control entre vista y modelo está concentrado en gran medida en la clase JComboBox.
Requisitos para la Nueva Versión
- Compatible con JComboBox.
- Que sea un Java Bean y siga el patrón software utilizado en Swing para este tipo de componentes: Modelo - Vista - Controlador.
- Poder listar los ítems que coincidan con el texto escrito, según un criterio determinado. SOLUCIÓN: poder establecer un agente con una búsqueda personalizada.
- La búsqueda por defecto: todos lo ítems del modelo que comiencen por el texto escrito.
- Que sea configurable. SOLUCIÓN: flags para los parámetros de configuración.
- Listar siempre todos lo ítems, o solo aquellos que coincidan con el texto, según el criterio de búsqueda. (Flag del modelo).
- Con o sin distinción mayúsculas / minúsculas en la comparación de ítems. (Flag del modelo).
- Establecer el idioma del que se utilizarán las reglas para ordenar y buscar los ítems del modelo. (Parámetro del modelo).
- Que se pueda habilitar / deshabilitar el que se muestre el popup. Esto es muy útil para evitar la interferencia con algún otro popup de la GUI de la aplicación, que se vea perjudicada cuando se selecciona por código algún ítem del combo.
- Que soporte comportamientos básicos. SOLUCIÓN:
- Texto en rojo cuando no hay ningún ítem que coincida con el texto, según el criterio de búsqueda. (Flag del control).
- Forzar a seleccionar un ítem. (Flag del control).
- Sonido de beep cuando no hay ningún ítem que coincida con el texto, según el criterio de búsqueda. (Flag del control).
- Forzar a que se oculte el popup cuando no haya ningún ítem que coincida con el texto, según el criterio de búsqueda. (Flag del control).
- En caso que se muestren solo las coincidencias, que al pulsar el arrow button, se listen todos los ítems existentes en el modelo. (Flag del control y vista).
- Completar el campo de texto con el ítem seleccionado en el popup vía las teclas up o down. (Flag del control y vista).
- Que se puedan listar los ítems de 3 modos posibles. (Parámetro del modelo) :
- En el mismo orden en que están en el modelo.
- En el mismo orden en que los devuelve el ''agente de búsqueda''.
- Ordenados alfabéticamente (ordena siempre, aunque el ''agente de búsqueda'' ya los haya devuelto ordenados).
Hacia la Nueva Versión
El hecho que el modelo estuviese claramente separdado y concentrado en una clase, facilitó su adaptación. Después de un análisis con algunas pruebas se decició que debería de crearse uno nuevo, dado que costaría menos trabajo y nos evitaríamos posibles bugs por ejecución de código innecesario o incorrecto, en casos particulares. Así, como modelo, teniendo en cuenta el desarrollo existente, y los requisitos de configuración, se creó la clase DefaultComboBoxConfigurableLookUpModel .
Para el caso de vista y control, la adaptación era mucho más compleja. La clase principal, JComboBox implementaba parte del control entre vista y modelo, mientras que el resto de control estaba fragmentado con distinto grano, en múltiples clases relativas a la vista. Esta fragmentación en gran medida controlaba la interacción del usuario con el componente y sus sub-componentes.
Así pues, reimplementar vista y control permitiría tener un componente mejor adaptado, a cambio de un coste de desarrollo muy elevado. Por ello, como solución intermedia se decidió que la clase principal del nuevo componente, JComboBoxConfigurableLookUp, heredara de JComboBox, y actuase como una capa de adaptación entre el modelo, interacción de usuario y de programador, con la implementación existente de JComboBox, teniendo en cuenta la funcionalidad nueva que se quería agregar.
Implementar una capa de adaptación reduce costes temporales, a cambio de aumentar la de complejidad. Aumenta la complejidad debido a que, para realizar cualquier cambio, se debe de conocer la implementación existente, el órden de ejecución de instrucciones, lo que se quiere hacer. En este caso, la dificultad aumenta debido a que, al ser configurable, existen múltiples comportamientos distintos que se deben ofrecer, manteniendo los criterios de interacción (por desarrollador, usuario, u otros componentes o clases de Java) con el componente, en cualquier configuración. Esto no acabará de ser cierto, debido a que ciertas configuraciones, requerirán modos de interacción distintos, aún así, se debe de ofrecer un modo de interacción lo más uniforme y equivalente posible al que ya existía con JComboBox.
Uno de los requisitos más solicitados por los desarrolladores, y que hace que el componente sea muy útil, es el poder desarrollar búsquedas con unos criterios determinados, de modo que los ítems que se muestren o que se seleccionen sean aquellos que cumplan dichos criterios. Estamos pues trabajando en el nivel del modelo. La solución que se adoptó era que se crease una clase que implementase ILookUp, de modo que el modelo invocase a un objeto agente de dicha clase, y trabajase con el subconjunto se ítems que devolviese el agente. Para ello se le indicaba los ítems del modelo, el texto escrito por el usuario, el idioma, y si realiza o no distinción de mayúsculas / minúsculas. Los dos últimos parámetros vienen embebidos en un objeto StringComparator que permite realizar la comparación de ítems según dichos valores.
Nueva Versión
El diagrama 2 muestra los elementos implicados en la adaptación de JComboBox al nuevo componente.

Diagrama 2: diagrama de clases simplificado de la implementación de JComboBoxConfigurableLookUp. Se ha resaltado las clases nuevas, reimplementadas, o aquellas que tienen un peso muy importante en la lógica del nuevo componente, en comparación con JComboBox.
Vista y Control:
Teniendo en cuenta que será editable, para poder agregar control al perder foco, o al cambiar el texto, se agregan 2 listenes especializados (de tipo FocusListener, y KeyListener), al objeto de tipo JTextComponent contenido en el ComboEditor del componente.
Para poder controlar el popup, se agregó un listener especializado de tipo PopupMenuListener, a dicho componente interno.
Para poder controlar la pulsación de arrow-button, sí, el botón situado a la derecha con una flecha indicando hacia abajo, se agregó un MouseListener a dicho componente.
Por último, la parte de control más delicada es la que gestiona el documento donde se escribe texto. Este era uno de los puntos críticos debido a que las tres capas se veían implicadas: vista, control, y modelo. Para que se comportase según la configuración establecida, se creó una clase PlainDocumentTextFormatter, que hereda de la que existía previamente, PlainDocument, y enmascara el control atípico de este nuevo componente, invocando los métodos del padre según una nueva lógica. Será dicho documento el que se encargue además de actualizar el color del texto, y del sonido beep.
Modelo:
Como ya se ha explicado, el modelo ha debido de reimplementarse completamente, de modo que no es compatible con DefaultComboBoxConfigurableLookUpModel.
Se mantiene separado de vista y control, delegando la obtención de la sublista de ítems a un agente de búsquedas especializado, y limitándose a aplicar, según sea su configuración, un control sobre la ordenación de ítems, y el modo en que se le pasan a la parte de control y vista, o, sobre el método del agente a invocar, teniendo en cuenta la distinción o no de minúsculas / mayúsculas.
Internamente, para poder comparar ítems teniendo en cuenta las reglas de ordenación de un idioma en particular, así como la distinción mayúsculas / minúsculas, se utiliza un objeto de tipo StringComparator, con un objeto de tipo Collator, que será en última instancia el que realizará la comparación según las reglas del idioma establecido.
Los ítems dentro del modelo se almacenan según lo haría DefaultComboBoxModel, y, también de un modo ordenado (vía StringComparator), para acelerar el tiempo de respuesta.
Los ítems a los que se accederá, seguirán un criterio de posición según el valor de ItemsOrder y de ShowAllItemsInListBox. En caso que fuese necesario siempre se podrá acceder a todos lo ítems del modelo, por ejemplo: Vector datos = obj_modelo.getData();
El agente que utiliza por defecto realiza una búsqueda de todos los ítems que comienzan por un texto dado, este agente está implementado como StartsWithLookUpAgent. Cabe la posibilidad de cambiar de agente, para ello véase el apartado ¿Cómo crear mi propio agente de búsquedas personalizadas? . Se ha visto la necesidad de permitir habilitar / deshabilitar la notificación de eventos del modelo, para que desde un agente se pueda cambiar los ítems que contiene.
La siguiente tabla lista métodos públicos específicos del modelo DefaultComboBoxConfigurableLookUpModel, que no existen en DefaultComboBoxModel:
| Método | Descripción |
|---|---|
| public Vector<Object> getData(); | Obtención de todos los elementos almacenados en el modelo, según fueron almacenados. |
| public Vector<Object> getDataAccordingItemsOrder(); | Obtención de todos los elementos almacenados en el modelo, ordenados según el criterio del parámetro ItemsOrder. |
| public ILookUp getLookUpAgent(); | Obtención del agente que realiza las búsquedas con los datos del modelo. |
| public void setLookUpAgent(ILookUp agent); | Establecer el agente que realizará las búsquedas con los datos del modelo. |
| public void setTextWritten(String text); | Texto que utilizará el agente para la búsqueda de ítems del modelo. |
| public void setEventNotificationEnabled(boolean b); | Habilitar / desabilitar la notificación de eventos del modelo. |
| public boolean getEventNotificationEnabled(); | Conocer si está habilitada o deshabilitada la notificación de eventos del modelo. |
Parámetros de Configuración
| Parámetro / Flag | Descripción | Valor por defecto | Parte |
|---|---|---|---|
| BeepEnabled | Sonido de beep cuando no hay ningún ítem que coincida con el texto, según el criterio de búsqueda. Posibles valores: true o false. Métodos: public boolean isBeepEnabled(); public void setBeepEnabled(boolean b); |
false | Control |
| CaseSensitive | Con o sin distinción mayúsculas / minúsculas en la comparación de ítems. Posibles valores: CASE_SENSITIVE : con distinción mayúsculas / minúsculas. CASE_INSENSITIVE : sin distinción mayúsculas / minúsculas. Métodos: public boolean isCaseSensitive(); public void setCaseSensitive(boolean caseSensitive); |
CASE_SENSITIVE | Modelo |
| CompleteArrowKeySelection | Completar el campo de texto con el ítem seleccionado en el popup vía las teclas up o down. Posibles valores: true o false. Métodos: public boolean isCompleteArrowKeySelection(); public void setCompleteArrowKeySelection(boolean b); |
false | Control y Vista |
| DisplayAllItemsWithArrowButton | En caso que se muestren solo las coincidencias, que al pulsar el arrow button, se listen todos los ítems existentes en el modelo. Posibles valores: true o false. Métodos: public boolean isDisplayAllItemsWithArrowButton(); public void setDisplayAllItemsWithArrowButton(boolean b); |
true | Control y Vista |
| HidePopupIfThereAreNoItems | Forzar a que se oculte el popup cuando no haya ningún ítem que coincida con el texto, según el criterio de búsqueda. Posibles valores: true o false. Métodos: public boolean isHidePopupIfThereAreNoItems(); public void setHidePopupIfThereAreNoItems(boolean b); |
true | Control |
| ItemsOrder | Modo en que se listarán los ítems: Posibles valores: MAINTAIN_POSITION : en el orden en que están almacenados en el modelo. ALPHABETICAL_ORDERED : en órden alfabético según las reglas del idioma establecido. MAINTAIN_AGENT_POSITIONS : en el orden devuelto por el agente. Métodos: public int getItemsOrder(); public void setItemsOrder(int itemsOrder); |
MAINTAIN_AGENT_POSITIONS | Modelo |
| LanguageRules | Establecer el idioma del que se utilizarán las reglas para ordenar y buscar los ítems del modelo. Posibles valores: ISO_LANGUAGE_CODE _ ISO_COUNTRY_CODE Métodos: public String getLanguageRules(); public void setLanguageRules(String languageRules); |
en_EN | Modelo |
| OnlyOneColorOnText | Texto en rojo cuando no hay ningún ítem que coincida con el texto, según el criterio de búsqueda. Posibles valores: true o false. Métodos: public boolean isOnlyOneColorOnText(); public void setOnlyOneColorOnText(boolean b); |
false | Control |
| ShowAllItemsInListBox | Mostrar siempre todos los ítems del modelo, o solo la sublista que devuelve el agente de búsqueda. Posibles valores: SHOW_ALL_ITEMS : muestra siempre todos los ítems del modelo. SHOW_ONLY_MATCHES : muestra solo la sublista de ítems que devuelve el agente de búsqueda. Métodos: public boolean isShowAllItemsInListBox(); public void setShowAllItemsInListBox(boolean itemsShownInListBox); |
SHOW_ONLY_MATCHES | Modelo |
| ToForceSelectAnItem | Forzar a seleccionar un ítem. Posibles valores: true o false. Métodos: public boolean isToForceSelectAnItem(); public void setToForceSelectAnItem(boolean b); |
true | Control |
¿Cómo crear mi propio agente de búsquedas personalizadas?
DefaultComboBoxConfigurableLookUpModel utiliza por defecto un agente: StartsWithLookUpAgent, que devuelve el subconjunto de los ítems que se le pasan, cuyo valor, utilizando un objeto de tipo StringComparator (para tener en cuenta el idioma y distinción mayúsculas / minúsculas) comienza por un determinado String que tambien se le pasa como parámetro.
En caso que se requiera tener otro tipo de búsqueda, se deberá crear una clase que implemente el interfaz ILookUp.
Dicho interfaz tiene 2 métodos, que serán invocados por el modelo según el valor del flag CaseSensitive.
Según la búsqueda que se quiera ofrecer, se deberá implementar uno u otro, o ambos métodos (recomendado), devolviendo siempre una sub-lista de ítems que cumplen los criterios de búsqueda según determinado texto. Es recomendable que los métodos estén sincronizados, esto puede reducir el rendimiento a cambio de asegurarnos mayor estabilidad.
Una vez implementado el agente, se deberá instanciar, y asignar al modelo, mediante el método setLookUpAgent(ILookUp agent).
A partir de este momento, el modelo utilizará el nuevo agente de búsqueda.
Si en algún momento desde el agente se desea modificar los ítems del modelo, se debe deshabilitar previamente la notificación de eventos de este, para luego restaurarla:
model.setEventNotificationEnabled(false);
// Modify the data of the model
model.setEventNotificationEnabled(true);
Tests
Para desarrolladores
Ubicado en el paquete org.gui.beans.comboboxconfigurablelookup.programmertests del directorio src-test-ui en el proyecto libUIComponent.
Básicamente es una clase, TestJComboBoxConfigurableLookUp, que instancia un objeto de tipo JComboBoxConfigurableLookUp, agregándole ítems, colocándolo en un JFrame, y visualizándolo. En dicho fichero hay abundante código, que una vez descomentado, podemos testear configuración, métodos, cambio componentes, etc.
En el mismo paquete hay un agente de búsquedas de ejemplo, SampleAgent, que devuelve los ítems ubicados en posición par; y un ejemplo de renderer de las celdas del popup, SampleBasicComboBoxRenderer, que cambia el color de fondo de la celda según una fórmula basada en en su posición, y que la pone gris si la celda está seleccionada.
Para usuarios
Ubicado en el paquete org.gui.beans.comboboxconfigurablelookup.usertests del directorio src-test-ui en el proyecto libUIComponent.
Es una pequeña aplicación, JFrameUserTestOfJComboBoxConfigurableLookUp, que permite crear instancias de JComboBoxConfigurableLookUp con distinta configuración, y testear y comparar su comportamiento.
La siguiente captura es de la interfaz de la aplicación:

Captura 1: GUI de la aplicación para testear el componente.
Permite seleccionar el constructor a utilizar para instanciar el componente en un JFrame (visible), definir los parámetros de configuración, rellenar con datos los ejemplos seleccionados, en la pestaña "Log" muestra las operaciones que se van haciendo, y cualquier excepción que se pueda producir, y permite incluso invocar algunos métodos públicos (vía el botón "Test"). Para esto último tiene un parser sencillo para un conjunto de parámetros que se le pueden pasar a los métodos. Con el botón "Ayuda" se muestra la explicación de los parámetros.
Con el botón "Resear" izquierdo restaura la configuración a la que viene por defecto en el componente, mientras que el derecho borra todas las instancias que pudieran haber.
¿Cómo utilizarlo?
El siguiente diagrama puede aclarar los pasos a seguir a hora de utilizar el componente.

Diagrama 3: diagrama de flujo con los caminos de decisión a la hora de incorporar el componente a la GUI. Este diagrama puede ser muy útil cuando el/la desarrollador/a decide por primera vez utilizarlo.
Ejemplos
Para los ejemplos se utilizará como ítems: elefante, oso, leopardo, buitre, gato, Lagarto, ratón, perro, lagartija, Lince.

Captura 2: Captura del componente con la configuración por defecto.

Captura 3: Captura del componente, manteniendo la posición y listando todos los ítems.

Captura 4: Captura del componente, con case-sensitive, manteniendo la posición y buscando.

Captura 5: Captura del componente, con posiciones desordenadas y listando todos los ítems.

Captura 6: Captura del componente, sin case-sensitive, con posiciones desordenadas y buscando.

Captura 7: Captura del componente, buscando, y mostrando solo las coincidencias..

Captura 8: Captura del componente, buscando, y mostrando siempre todos los ítems.

Captura 9: Captura del componente, con case-sensitive, buscando, y mostrando solo coincidencias.

Captura 10: Otra captura del componente, con case-sensitive, buscando, y mostrando solo coincidencias.

Captura 11: Captura del componente, con texto en 2 posibles colores, y búsqueda sin resultados.

Captura 12: Captura del componente, con texto en 1 solo posible color, y búsqueda sin resultados.

Captura 13: Captura del componente, con texto en 2 posibles colores, sin forzar coincidencias, y búsqueda sin resultados.

Captura 14: Captura del componente, mostrando todos los ítems al pulsar el "arrow-button".

Captura 15: Captura del componente, no mostrando todos los ítems al pulsar el "arrow-button".

Captura 16: Captura del componente, no autocompleta el texto seleccionado por las teclas UP o DOWN.

Captura 17: Captura del componente, autocompleta el texto seleccionado por las teclas UP o DOWN.

Captura 18: Captura del componente, con agente de búsqueda personalizado: siempre devuelve ítems en posiciones pares.

Captura 19: con cell-renderer personalizado: cambia color de fondo según posición, o en caso de estar seleccionado, a gris.

Captura 20: Captura del componente, cambiando el editor por uno que contiene un JEditableTextField.
Más información
- Ubicación:
- Proyecto: libUIComponent
- Paquete: org.gvsig.gui.beans.comboboxconfigurablelookup
- Paquete del agente: org.gvsig.gui.beans.comboboxconfigurablelookup.agents
- Tests:
- Para desarrollador: org.gvsig.gui.beans.comboboxconfigurablelookup.programmertests.TestJComboBoxConfigurableLookUp.java
- Para usuario: org.gvsig.gui.beans.comboboxconfigurablelookup.usertests.JFrameUserTestOfJComboBoxConfigurableLookUp.java