miércoles, 27 de noviembre de 2013

JTextField y saltos de línea

Introducción


Una vez leí la siguiente cita:
Any sufficiently advanced bug is indistinguishable from a feature.
- Rich Kulawiec
(Cualquier fallo suficientemente sofisticado es indistinguible de una característica). 
En la documentación de Java Swing se dice que el componente JTextField no acepta varias líneas y que para ese caso hay otras opciones como JTextArea o JTextPane.

Efectivamente, si se pulsa la tecla ENTER cuando el "focus" lo tiene un JTextField no se produce un salto de línea. Pero, ¿qué ocurre si se establece el texto del JTextField mediante el método JTextField#setText(String) con una String que sí contiene saltos de línea?

Pues he descubierto que depende, tal y como se explica en este "fallo documentado":

JDK-6427290: Possibility to put newlines into a JTextField

Esto ocurre al menos en la versiones 1.6.0_37, 1.7.0_25 y anteriores.
Ummm, ¿es un fallo o es una característica útil? Eso lo dejo a tu criterio.

Demostración


La clave está en instalar sobre el JTextField un DocumentFilter que no altere los saltos de línea presentes en la String (en realidad se instala sobre el Document del JTextField).

Por defecto, los JTextField no tienen ningún DocumentFilter instalado y en tal caso el propio JTextField sustituye los saltos de línea que detecta en la String por espacios en blanco (según JDK-6427290).

Sin embargo, al instalar un DocumentFilter, el JTextField no altera la String y delega completamente esa responsabilidad al DocumentFilter.

El pequeño programa JTextFieldLinesTest crea una ventana con un JTextArea, un JTextField y un JToggleButton:
  • El TextArea muestra información del sistema.
  • El TextField sirve para hacer pruebas con saltos de línea.
  • El botón permite conmutar el comportamiento del TextField respecto a los saltos de línea.
Puedes ver y copiar el código fuente completo en el siguiente enlace:


Las líneas que más interesan del programa están en el ItemListener del botón. Según se invoque a setDocumentFilter(DocumentFilter) con null o con un DocumentFilter válido variará el comportamiento del JTextField respecto a los saltos de línea.

Document document= textField.getDocument();
AbstractDocument abstractDocument= (AbstractDocument) document;

DocumentFilter filter;
if (button.isSelected() == true) {
    filter= new DocumentFilter();
}
else {
    filter= null;
}
                
abstractDocument.setDocumentFilter(filter);

En estas capturas se puede ver el como cambia el comportamiento y el aspecto visual del JTextField.

Monolínea
Monolínea
Multilínea
Multilínea


En ningún caso se pueden teclear saltos de línea (tecla ENTER) sobre el JTextField, pero sí se pueden pegar desde el portapapeles textos que contengan saltos de línea.

Respecto al aspecto visual del JTextField, el que se estire o no a lo alto por la presencia de saltos de línea depende de diversos factores como el LayoutManager que se esté utilizando. Si se muestran saltos de línea sin estirar el JTextField a lo alto el contenido se verá horrible.

Comentar a este respecto que con los tabuladores pasa algo parecido. Al pulsar la tecla TAB normalmente se cede el "focus" al siguiente componente, por lo que no se pueden teclear tabuladores. Sin embargo, si la String indicada en JTextField#setText(String) o el texto pegado desde el portapapeles contienen tabuladores el JTextField los mostrará.

Conclusiones


En mi humilde opinión, mientras no se cambie este comportamiento, es una característica que resulta muy útil para detectar visualmente Strings que contengan saltos de línea en JTextField que se rellenen programáticamente, por ejemplo, con datos leídos desde una base de datos que supuestamente no deberían contener saltos de línea (por eso se visualizan con un JTextField).

Enlaces


Enlaces de interés relacionados con este artículo:


(Actualizado 27/11/2013)



Source code: JTextFieldLinesTest

Código fuente del programa JTextFieldLinesTest

Ver la entrada correspondiente en el siguiente enlace:


JTextField y saltos de línea



package pruebas;

import java.awt.Component;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import javax.swing.text.AbstractDocument;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;

/**
 * @see http://bugs.sun.com/view_bug.do?bug_id=6427290
 */
public class JTextFieldLinesTest {

    public static String makeInfo() {
        String info= String.format(""
            + "%njava.class.version= "            + System.getProperty("java.class.version")
            + "%njava.runtime.version= "          + System.getProperty("java.runtime.version")
            + "%njava.specification.name= "       + System.getProperty("java.specification.name")
            + "%njava.specification.vendor= "     + System.getProperty("java.specification.vendor")
            + "%njava.specification.version= "    + System.getProperty("java.specification.version")
            + "%njava.version= "                  + System.getProperty("java.version")
            + "%njava.vm.info= "                  + System.getProperty("java.vm.info")
            + "%njava.vm.name= "                  + System.getProperty("java.vm.name")
            + "%njava.vm.specification.name= "    + System.getProperty("java.vm.specification.name")
            + "%njava.vm.specification.vendor= "  + System.getProperty("java.vm.specification.vendor")
            + "%njava.vm.specification.version= " + System.getProperty("java.vm.specification.version")
            + "%njava.vm.vendor= "                + System.getProperty("java.vm.vendor")
            + "%njava.vm.version= "               + System.getProperty("java.vm.version")
            + "%nos.arch= "                       + System.getProperty("os.arch")
            + "%nos.name= "                       + System.getProperty("os.name")
            + "%nos.version= "                    + System.getProperty("os.version")
            + "%n"
        );

        return info;
    }
    //--------------------------------------------------------------------------

    public static void selectAll(final JTextField aTextField) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                aTextField.selectAll();
                aTextField.requestFocusInWindow();
            }
        });
    }
    //--------------------------------------------------------------------------

    public static void example() {
        final String DEFAULT_TEXT= String.format("Line1%nLine2%nLine3%nLine4%nLine5");

        JTextArea textArea= new JTextArea();
        textArea.setText( makeInfo() );
        textArea.setEditable(false);

        final JTextField textField= new JTextField();
        textField.setEditable(true);

        String text= String.format(DEFAULT_TEXT);
        textField.setText(text);
        selectAll(textField);

        final JToggleButton button= new JToggleButton("Toogle new lines mode");

        final JPanel panel= new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
        textArea.setAlignmentX(Component.CENTER_ALIGNMENT);
        textField.setAlignmentX(Component.CENTER_ALIGNMENT);
        button.setAlignmentX(Component.CENTER_ALIGNMENT);
        panel.add(textArea);
        panel.add(new JSeparator(JSeparator.HORIZONTAL));
        panel.add(textField);
        panel.add(new JSeparator(JSeparator.HORIZONTAL));
        panel.add(button);

        final JFrame frame= new JFrame("JTextField test");
        frame.setContentPane(panel);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        button.addItemListener(new ItemListener() {
            @Override
            public void itemStateChanged(ItemEvent e) {
                Document document= textField.getDocument();
                AbstractDocument abstractDocument= (AbstractDocument) document;

                DocumentFilter filter;
                if (button.isSelected() == true) {
                    filter= new DocumentFilter();
                }
                else {
                    filter= null;
                }
                abstractDocument.setDocumentFilter(filter);

                String text= DEFAULT_TEXT;
                textField.setText(text);
                selectAll(textField);

                panel.revalidate();
                panel.repaint();
                frame.pack();
            }
        });

        frame.setVisible(true);
    }
    //--------------------------------------------------------------------------

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                example();
            }
        });
    }
    //--------------------------------------------------------------------------

}