Swing Application Framework incorpora la ejecución de acciones como tareas en segundo plano. Estas tareas se pueden definir para que bloqueen la interfaz gráfica del programa. Cuando se bloquea la interfaz gráfica es cuando entran en juego los Input Blockers y de ellos, y de como personalizarlos, trata este artículo.

El Input Blocker por defecto

El Input Blocker por defecto es DefaultInputBlocker, este extiende de la clase abstracta Task.InputBlocker. Cualquier InputBlocker debe tener un comportamiento diferente según el tipo de bloqueo que hayamos especificado. Existen cuatro tipos de bloqueo que puede ejercer una acción:

  • ACTION: Bloquea la acción ejecutada, normalmente deshabilitándola durante la ejecución de esta
  • COMPONENT: Bloquea el componente origen de la acción, deshabilitándolo también.
  • WINDOW: Bloquea la ventana en la que se ha ejecutado la acción.
  • APPLICATION: Bloquea todas las ventanas de la aplicación.

Para este artículo vamos nos interesan los tipos de bloqueo WINDOW y APPLICATION que en el DefaultInputBlocker tienen el mismo tratamiento y que muestran un diálogo de progreso de la acción.

El DefaultInputBlocker asigna un nuevo GlassPane a la ventana principal de la aplicación. Este se encarga de bloquear los eventos de ratón y teclado. Además muestra un JDialog con información de progreso y con capacidad de cancelar la ejecución de la tarea si no se ha deshabilitado mediante setUserCanCancel(false)

Personalizando el diálogo de progreso

El JDialog de bloqueo está compuesto por los siguientes componentes y sus respectivos nombres para referirse a ellos en los archivos de recursos:

Nombre Componente Descripción
BlockingDialog javax.swing.JDialog El diálogo de bloqueo
BlockingDialog.optionPane javax.swing.JOptionPane El JOptionPane
BlockingDialog.cancelButton javax.swing.JButton El botón de cancelar (si se permite)
BlockingDialog.progressBar javax.swing.JProgressBar La barra de progreso

Este JDialog puede ser modificado mediante la inyección de recursos. NetBeans ofrece desde su editor gráfico modificar alguno de estos aspectos pero no nos da control total sobre ellos.

A la hora de definir las propiedades de estos componentes podemos hacerlo de una forma global, con lo que los cambios se aplicarán a todos los DefaultInputBlocker de la aplicación, o para una acción en concreto.

Para definir propiedades globalmente debemos definirlas en el fichero de recursos de la aplicación. En este fichero y usando el nombre de cada componente podemos especificar sus propiedades. Así, por ejemplo, para la aplicación Ejemplo cuya clase que deriva de org.jdesktop.application.Application es EjemploApp existirá un fichero de recursos en la carpeta resources llamado EjemploApp.properties. En este fichero podemos indicar propiedades generales como:

BlockingDialog.cancelButton.text = &Cancelar

Esto hará que todos nuestros diálogos de progreso tengan el texto “Cancelar” en vez del el “Cancel” por defecto. El & indica el mnemonic a usar para el botón (al estilo vb).

Si luego queremos cambiar un recurso para una acción en concreto podemos hacerlo en el fichero de recursos de la clase donde se encuentra la acción. Por ejemplo, imaginemos que la clase de la vista de la aplicación se llama EjemploView, tendremos un fichero de recursos llamado EjemploView.properties. Ahora tenemos una acción llamada mover y queremos que el texto del botón de cancelar sea “Detener”. Para ello en el fichero de recursos EjemploView.properties añadiremos la linea:

mover.BlockingDialog.cancelButton.text = &Detener

Esto es aplicable para todas las propiedades que se quieran inyectar.

¿Que propiedades de cada componente puedo cambiar?

La inyección de recursos no funciona con todas las propiedades de un objeto. ¿Como sabemos que propiedades se pueden asignar?.

La propiedad a inyectar debe ser accesible y debe ser de un tipo gestionable por algún conversor de recursos. La clase ResourceConverter proporciona conversores para:

  • Boolean
  • Integer
  • Float
  • Double
  • Long
  • Short
  • Byte
  • MessageFormat
  • URL
  • URI

La clase ResourceMap añade los siguientes conversores:

  • java.awt.Color: En los formatos soportados por Color.decode()
  • java.awt.Dimension: En formato “x,y”
  • javax.swing.border.EmptyBorder: En formato “top,left,bottom,right”
  • java.awt.Font: En los formatos soportados por Font.decode()
  • javax.swing.Icon: Indicando la ruta al archivo de imagen
  • java.awt.Image: Indicando la ruta al archivo de imagen
  • java.awt.Insets: En formato “top,left,bottom,right”
  • javax.swing.KeyStroke: En el formato soportado por KeyStroke.getKeyStroke(s)
  • java.awt.Point: En formato “x,y”
  • java.awt.Rectangle: En formato “x,y,width,height”

Si queremos podemos añadir nuevos conversores mediante la función estática:

ResourceConverter.register(nuestroConversor);

Crear un conversor de recursos es muy simple, un ejemplo, el del conversor para Dimension, vale más que mil palabras:

class DimensionStringConverter extends ResourceConverter {
	DimensionStringConverter() {
	    super(Dimension.class);
	}
	@Override 
        public Object parseString(String s, ResourceMap ignore) 
                 throws ResourceConverterException {
            List<Double> xy = parseDoubles(s, 2, "invalid x,y Dimension string");
            Dimension d = new Dimension();
            d.setSize(xy.get(0), xy.get(1));
            return d;
	}
    }

Decidiendo cuando debe aparecer nuestro diálogo de progreso

Existe una variable de configuración especial para decidir cuando debe aparecer nuestra ventana de progreso. Este retraso es muy util para evitar ventanas que aparecen y desaparecen en décimas de segundos.

Mediante la propiedad BlockingDialogTimer.delay podremos especificar que tiempo, en milisegundos, debe pasar antes de que aparezca el diálogo de progreso. El valor por defecto para esta propiedad es 250, valor muy bajo a mi parecer que genera apariciones fantasma de ventanas.

Texto de la barra de progreso

El valor por defecto para BlockingDialog.progressBar.string es “%02d:%02d, %02d:%02d remaining”. ¿Porqué este valor por defecto?.

El DefaultInputBlocker formatea la cadena de la barra de progreso pasándole una serie de variables que gestiona el mismo. Estas variables son (pasadas como long y en este orden):

  • minutos de ejecución de la acción transcurridos
  • segundos de ejecución de la acción transcurridos
  • minutos de ejecución de la acción restantes estimados
  • segundos de ejecución de la acción restantes estimados

Podemos, por lo tanto, modificar esta cadena siguiendo las reglas de formateo de la clase Formatter teniendo en cuenta las variables que se le van a pasar. Así un ejemplo de formato que mostraría primero el tiempo restante y después el transcurrido sería:

Quedan %3$02d:%4$02d (%1$02d:%2$02d transcurridos)

Para que la estimación funcione deberemos ir asignando el progreso de la acción mediante los métodos setProgress(...). De esta forma el DefaultInputBlocker basándose en el tiempo transcurrido calculará el tiempo restante estimado.

Hay que tener en cuenta que la llamada al formateo de la cadena se hace cuando se asigna algo de progreso en la acción. De forma que si no queremos que aparezca la ventana de progreso mostrando el texto de formato (con todos sus %02d…) en vez de el texto formateado deberemos asignar algo de progreso antes de que se muestre el diálogo.

Mostrando ventanas sobre el diálogo de progreso

Durante la ejecución de la tarea se pueden mostrar todo tipo de ventanas que se situarán sobre el diálogo de progeso, pero hay que tener muy en cuenta que si se muestra algún diálogo antes de que aparezca la ventana de progreso esta puede bloquear el acceso al diálogo. Por ejemplo:

@Action(block = Task.BlockingScope.APPLICATION)
    public Task prueba() {
        return new PruebaTask(getApplication());
    }
 
    private class PruebaTask extends org.jdesktop.application.Task<Object, Void> {
 
        PruebaTask(org.jdesktop.application.Application app) {
            super(app);
            setProgress(1);
        }
 
        @Override
        protected Object doInBackground() {
            //Este diálogo bloqueará toda la acción
            JOptionPane.showMessageDialog(getFrame(), "Soy un malvado diálogo bloqueador de acciones");
            for (int i = 1; i < 100; i++) {
                try {
                    setProgress(i);
                    Thread.sleep(1000);
                } catch (InterruptedException ex) {
                    [...]
                }
            }
            return null;
        }
 
        @Override
        protected void succeeded(Object result) {
        }
    }

En este ejemplo se muestra una ventana nada más empezar la acción, a menos que seamos muy rápidos, aparecerá el diálogo de progreso sobre ella de forma que no podremos pulsar aceptar. Pero como la ejecución está bloqueada en espera del retorno del JOptionPane la acción nunca terminará. Si hemos deshabilitado la posibilidad de cancelar la acción ya tenemos nuestro programa bloqueado y a nuestro usuario mosqueado.

Propiedades más comunes

A modo de tabla de referencia las propiedades más comunes para ser modificadas en el DefaultInputBlocker:

  • BlockingDialog.title
  • BlockingDialog.optionPane.icon
  • BlockingDialog.optionPane.message
  • BlockingDialog.cancelButton.text
  • BlockingDialog.cancelButton.icon
  • BlockingDialog.progressBar.indeterminate
  • BlockingDialog.progressBar.stringPainted
  • BlockingDialog.progressBar.string
  • BlockingDialogTimer.delay