JLabel editable

24 02 2010

Los JLabel son elementos no editables. Lo cual tiene sentido. Están pensadas para mostrar información, posiblemente, etiquetando otro elemento.

En la aplicación que estoy construyendo queremos que unos componentes personalizados sirvan para mostrar información pero a su vez permitan editarla y queremos diferenciar claramente si el componente está en modo edición o no. En definitiva queremos tener etiquetas que bajo según qué circunstancias se puedan editar. Puesto que el JLabel no nos brinda esta opción, hemos tenido que trabajar un poco más y crear un componente personalizado.

El truco consistente en tener un componente, por ejemplo un JPanel, con un layout del tipo CardLayout con dos ‘cartas’ una de las cuales muestra una JLabel de toda la vida y otra un JTextField. El componente aporta la lógica para cambiar la carta en respuesta a ciertas interacciones del usuario. Si mantenemos el contenido de los dos componentes de forma coherente, tenemos la ilusión que la etiqueta se convierte en un campo de texto cuando entramos en modo edición y al revés al salir de este modo.

El siguiente código muestra una versión incipiente (pero funcional) de este ‘etiqueta editable’. Para entrar en modo edición hacemos doble click en la etiqueta y para salir del estado de edición pulsamos RETURN para confirmar los cambios o ESCAPE para cancelarlos.

Tomadlo como una prueba de concepto (que es lo que es) y no como una clase terminada con calidad industrial.

public class EditableLabel extends JPanel {
	/** The user is editing the component (JTextField is shown at the moment) */
	private boolean editing;

	/** The confirm edition key is Return */
	private static final int confirmKeyCode = KeyEvent.VK_ENTER;

	/** The cancel edition key is Return */
	private static final int cancelKeyCode = KeyEvent.VK_ESCAPE;

	/** The value which holds this component */
	private String value;

	// graphical components

	private CardLayout cl;
	private JPanel pnlCards;
	private static final String TEXT_FIELD = "text field";
	private JTextField textField;
	private static final String LABEL = "label";
	private JLabel label;

	public EditableLabel(String initialText) {
		this.value = initialText;

		cl = new CardLayout();
		pnlCards = new JPanel(cl);
		textField = new JTextField();
		label = new JLabel(initialText);
		pnlCards.add(textField, TEXT_FIELD);
		pnlCards.add(label, LABEL);
		cl.show(pnlCards, LABEL);
		add(pnlCards);

		// register the listeners
		label.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				// if double click, set edition mode
				if (e.getClickCount() == 2) {
					startEdition();
				}
			}
		});

		textField.addKeyListener(new KeyAdapter() {
			@Override
			public void keyReleased(KeyEvent e) {
				if (e.getKeyCode() == confirmKeyCode) {
					/* confirmation key pressed, so changing to non-edition
					 * and confirm changes */
					confirmEdition();
				} else if (e.getKeyCode() == cancelKeyCode) {
					/* cancel key pressed, so changing to non edition and
					 * cancel the changes */
					cancelEdition();
				}
			}
		});
	}

	private void startEdition() {
		cl.show(pnlCards, TEXT_FIELD);
		textField.setText(value);
		textField.requestFocus();
		textField.selectAll();
	}

	private void cancelEdition() {
		textField.setText(value);
		cl.show(pnlCards, LABEL);
	}

	private void confirmEdition() {
		value = textField.getText();
		label.setText(value);
		cl.show(pnlCards, LABEL);
	}

	/**
	 * @return the value
	 */
	public String getValue() {
		return value;
	}

	/**
	 * @param value the value to set
	 */
	public void setValue(String value) {
		this.value = value;
	}

	/**
	 * @return returns true if the component is currently showing the text
	 *  field or false otherwise.
	 */
	public boolean isEditing() {
		return editing;
	}
}
Anuncios




Trasteando con las JToolTip en Swing

23 02 2010

En esta entrada no voy a descubrir nada nuevo porque no soy un experto en Swing, pero son un par de detallitos que me han hecho perder algunos minutos.

La aplicación en la que estoy trabajando es un cliente Swing con componentes personalizados para construir una vista similar a diagramas de flujo. Estos componentes heredan de JPanel son más o menos complejos y sobrescriben los métodos de pintado como paintComponent. A su vez tienen componentes anidados y áreas ‘blancas’ que forman parte del componente Swing pero que en realidad para el usuario no forman parte del mismo. El siguiente diagrama lo ilustra un poco:

Mostrar el tooltip únicamente cuando el ratón está en determinadas partes del componente

Como sabemos la clase JComponent define el método setToolTipText(String tooltip) que permite establecer la tooltip, el ‘problema’ es que esta se establece para todo el componente. En mi ejemplo, yo sólo quiero que se muestre si el usuario posa el ratón en el diamante, ya que para él el resto del componente forma parte del fondo. Si no hacemos nada más, al poner el ratón en cualquier parte del componente se mostrará el tooltip.

La solución consiste en sobrescribir el método getToolTipText(MouseEvent event) para que devuelva null cuando las coordenadas del ratón (en el espacio de coordenadas del componente) están fuera del área para la que queremos mostrar el tooltip. El siguiendo snipet de código muestra la sobrescritura del método en mi componente personalizado: si estamos dentro del área definida por el rombo (calculado en un método auxiliar) entonces se muestra el tooltip establecido previamente con setToolTipText sino, devolvemos un null y por tanto no se renderizará el tooltip.

/**
* If the mouse is outside the diamond, don't show the tooltip.
*/
@Override
public String getToolTipText(MouseEvent event) {
    if (isInDiamond(event.getX(), event.getY())) {
        return getToolTipText();
    } else {
        return null;
    }
}

Tooltip multilínea

Aunque parezca sorprendente, la implementación por defecto del tooltip no soporta directamente multilíneas, así pues, si le pasamos un String que contenga retornos de carro en el método setToolTipText, éstos serán ignorados vilmente y se pintarán todas las líneas seguidas. Para solucionar ésto hay dos soluciones: una pesada y elegante y otra inmediata pero algo fea.

Empecemos por la segunda. Muchos componentes Swing tienen soporte para HTML. El tooltip también. Si asignamos un String que empiece por <html> el componente asumirá que le estamos pasando código HTML y lo renderizará como tal. El truco consiste pues, en sustituir todos los caracteres ‘\n’ por el String <br/>. Por ejemplo:

String strMultiLinea = ...
setToolTipText("<html>" + strMultiLinea.replaceAll("\n", "<br/>") + "</html>");

Rápido y funciona, pero es feo. La solución elegante consiste en implementar nuestro propio tooltip que soporte esta característica. Esto debemos hacerlo en dos pasos:

  1. Implementar nuestra clase tooltip personalizada heredando de javax.swing.JToolTip,
  2. Diciéndole al componente que debe usar nuestra propia implementación sobrescribiendo el método createToolTip para que devuelva una instancia de nuestra clase.




Capturar excepciones en el AWT Thread de una aplicación Swing

20 02 2010

En java podemos clasificar las excepciones en dos tipos, las “checked” y las que no son “not-checked” (perdonad, pero no sé cómo traducirlo satisfactoriamente).

Los métodos que lanzan excepciones del primer tipo ( cheched ) fuerzan al cliente de dicho métodos a tratarlas con un bloque try/catch o a lanzarlas para que sea un antecesor en la pilla de llamadas quien las trate. En última instancia la excepción alcanza el método principal main, que si no la gestiona la ‘lanza’ al sistema operativo y la aplicación muere con el volcado de pila del lugar donde se produjo.

Las excepciones not-checked no tienen esta restricción: ni es obligatorio capturarlas, ni el método cliente tiene que declarar explícitamente su lanzamiento mediante una cláusula throws en su signatura.

Las excepciones not-checked son todas aquellas que heredan de la clase RuntimeException . Así pues, si un método puede lanzar NullPointerException o IllegalArgumentException (entre otras), no está obligado a declararlo en su signatura ni el cliente a gestionar la excepción. De todos modos, si se produce, su propagación sigue la misma política: van escalando la pila de llamadas hasta que alguien las trate.

A la hora de programar una aplicación somos muy conscientes de las excepciones checked porque su no gestión es un error en tiempo de compilación, sin embargo para las excepciones not checked no tenemos más remedio que prever su posibilidad y hacer la gestión correspondiente donde corresponda. No obstante es posible que mientras estamos desarrollando o depurando no hayamos previsto una situación que desencadene el lanzamiento de una excepción de este tipo (típicamente se nos puede escapar alguna NullPointerException ). En estos casos (y siempre en general) es útil tener un bloque catch que capture el ‘resto’ de excepciones que el programador no previó e intentar hacer algo útil y más elegante que acabar la aplicación abruptamente (como por ejemplo guardar información en los logs que nos permita depurar o hacer un diagnóstico del problema).

¿Cómo y dónde ponemos este bloque “ captura el resto de excepciones”? En aplicaciones con un solo hilo de ejecución es muy fácil: en el main como ilustra el siguiente ejemplo.

public static void main(String[] args) {
try {
	// mi super-programa
} catch (Throwable e) {
	logger.debug("Unexpected exception!", e);
}

¿Pero qué pasa en una aplicación multi-hilo? En este caso el main creará varios hilos que tendrán su propia pila de llamadas por lo que cualquier excepción producida y no capturada en cualquiera de esos hilos no podrá ser capturada por el catch del ejemplo anterior.

En particular, las aplicaciones Swing son siempre multi-hilo. El código de programación de todos los listeners de gestión de eventos producidos por los diferentes elementos de la GUI, se ejecuta dentro del event dispatch thread (el hilo de gestión de eventos) también conocido como “AWT” Thread y que no es el mismo en el que se ejecuta el main del programa.

¿Qué pasa si se produce una excepción not-checked en este hilo? Pues que si no lo hemos previsto en el código y no hay ningún catch que la capture, se pierde irremediablemente. Afortunadamente existe una solución que expondré a continuación y que también está explicada en este post.

La API estándar de Java ofrece la clase ThreadGroup . Cuando creamos un thread podemos hacer que pertenezca a una instancia de esta clase. Dicha clase tiene un método con la siguiente signatura:

void uncaughtException(Thread t, Throwable e)

que es llamado por la máquina virtual de forma automática cuando cualquiera de los threads que forman parte de ese grupo lanza una excepción que nadie captura. Cuando un hilo se crea dentro de un grupo, todos los hilos que pueda crear ese hilo también pertenecen al mismo grupo.

Ahora ya tenemos todos los elementos que solucionan nuestro problema:

  1. Creamos una subclase de ThreadGroup que sobrescriba el método uncaughtException para que haga algo útil en nuestro programa.
  2. En el main construimos una instancia de la clase que acabamos de construir.
  3. Creamos un thread que se encarga de montar toda la GUI que forma parte del grupo recién instanciado.

Con esto conseguimos que todos los threads interesantes de nuestro programa pertenezcan al grupo que es capaz de gestionar las excepciones no capturadas.

Nuestro ThreadGroup podría tener el siguiente aspecto:

class HandleExceptionGroup extends ThreadGroup {
	private static Logger logger = Logger.getLogger(ExceptionGroup.class);

	public HandleExceptionGroup() {
		super("ExceptionGroup");
	}

	@Override
	public void uncaughtException(Thread t, Throwable e) {
		String msg =
			"GUI produced an unexpected exception (thread: " + t.getName() + ") ";

		logger.warn(msg, e);
	}
}

y el main de la clase principal:

public static void main(String[] args) {
        HandleExceptionGroup threadGroup = new HandleExceptionGroup();
	new Thread(threadGroup, "Main Thread"){
		@Override
		public void run() {
			// aquí construyo la GUI
		}
	}.start();
}