Error adding placeholder to a text component

2

I'm implementing the component of this topic along with the following instruction:

PromptSupport.setPrompt("Digite..", field); 

This command is from biblioteca swingx-core-1.6.2 and adds a kind of placeholder . But using it together with the FocusListener event of a IconTextField it gives me the following error:

  Exception in thread "AWT-EventQueue-0" java.lang.StackOverflowError
    at javax.swing.text.DefaultCaret$Handler.propertyChange(DefaultCaret.java:1846)
    at java.beans.PropertyChangeSupport.fire(PropertyChangeSupport.java:335)
    at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:327)
    at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:263)
    at java.awt.Component.firePropertyChange(Component.java:8428)
    at javax.swing.JComponent.setBorder(JComponent.java:1796)
    at geral.IconTextField.setBorder(IconTextField.java:45)
    at org.jdesktop.swingx.plaf.BuddyLayoutAndBorder.replaceBorderIfNecessary(BuddyLayoutAndBorder.java:56)
    at org.jdesktop.swingx.plaf.BuddyLayoutAndBorder.propertyChange(BuddyLayoutAndBorder.java:245)
    at java.beans.PropertyChangeSupport.fire(PropertyChangeSupport.java:335)
    at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:328)
    at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:263)
    at java.awt.Component.firePropertyChange(Component.java:8428)
    at javax.swing.JComponent.setBorder(JComponent.java:1796)
    at geral.IconTextField.setBorder(IconTextField.java:45)
    at org.jdesktop.swingx.plaf.BuddyLayoutAndBorder.replaceBorderIfNecessary(BuddyLayoutAndBorder.java:56)
    at org.jdesktop.swingx.plaf.BuddyLayoutAndBorder.propertyChange(BuddyLayoutAndBorder.java:245)
    at java.beans.PropertyChangeSupport.fire(PropertyChangeSupport.java:335)
    at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:328)
    at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:263)
    at java.awt.Component.firePropertyChange(Component.java:8428)
    at javax.swing.JComponent.setBorder(JComponent.java:1796)
    at geral.IconTextField.setBorder(IconTextField.java:45)

What can I do to resolve?

Library Link (scroll down the page, and download the 1st link)

Below I'll leave the classes I'm using:

Main class:

public class JTextFieldDecoratedIcon {

    public void start() throws IOException {

        final JFrame frame = new JFrame();
        frame.setPreferredSize(new Dimension(500, 350));

        JTextField field2 = new JTextField();
        IconTextField field = new IconTextField();

        URL path = new URL("https://i.imgur.com/WKfl8uV.png");
        Image icone = ImageIO.read(path);

        field.setIcon(new ImageIcon(icone));

        frame.add(field, BorderLayout.NORTH);
        field.setPreferredSize(new Dimension(250, 30));

        //bibilioteca swingx-core-1.6.2 ↓
        PromptSupport.setPrompt("Digite..", field);

        frame.add(field2, BorderLayout.SOUTH);
        field2.setPreferredSize(new Dimension(100, 30));

        field.addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                field.setBorder(new LineBorder(new Color(108, 85, 255)));
                field.setBackground(Color.LIGHT_GRAY);
            }

            @Override
            public void focusLost(FocusEvent e) {
                field.setBorder(new LineBorder(Color.GRAY));
                field.setBackground(new Color(255, 255, 255));
            }
        });

        frame.pack();
        frame.setVisible(true);
        frame.setLocationRelativeTo(null);

        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | javax.swing.UnsupportedLookAndFeelException ex) {
            ex.printStackTrace();
        }

        EventQueue.invokeLater(() -> {
            try {
                new JTextFieldDecoratedIcon().start();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        });
    }
}

Class IconTextComponentHelper:

class IconTextComponentHelper {
    private static final int ICON_SPACING = 4;

    private Border mBorder;
    private Icon mIcon;
    private Border mOrigBorder;
    private JTextComponent mTextComponent;

    IconTextComponentHelper(JTextComponent component) {
        mTextComponent = component;
        mOrigBorder = component.getBorder();
        mBorder = mOrigBorder;
    }

    Border getBorder() {
        return mBorder;
    }

    void onPaintComponent(Graphics g) {
        if (mIcon != null) {
            Insets iconInsets = mOrigBorder.getBorderInsets(mTextComponent);
            mIcon.paintIcon(mTextComponent, g, iconInsets.left, iconInsets.top);
        }
    }

    void onSetBorder(Border border) {
        mOrigBorder = border;

        if (mIcon == null) {
            mBorder = border;
        } else {
            Border margin = BorderFactory.createEmptyBorder(0, mIcon.getIconWidth() + ICON_SPACING, 0, 0);
            mBorder = BorderFactory.createCompoundBorder(border, margin);
        }
    }

    void onSetIcon(Icon icon) {
        mIcon = icon;
        resetBorder();
    }

    private void resetBorder() {
        mTextComponent.setBorder(mOrigBorder);
    }
}

IconTextField class:

public class IconTextField extends JTextField {

    private IconTextComponentHelper mHelper = new IconTextComponentHelper(this);

    public IconTextField() {

    super();
}

public IconTextField(int cols) {
    super(cols);
}

private IconTextComponentHelper getHelper() {
    if (mHelper == null) {
        mHelper = new IconTextComponentHelper(this);
    }

    return mHelper;
}

@Override
protected void paintComponent(Graphics graphics) {
    super.paintComponent(graphics);
    getHelper().onPaintComponent(graphics);
}

public void setIcon(Icon icon) {
    getHelper().onSetIcon(icon);
}

public void setIconSpacing(int spacing) {
    //getHelper().onSetIconSpacing(spacing);
}

@Override
    public void setBorder(Border border) {
        getHelper().onSetBorder(border);
        super.setBorder(getHelper().getBorder());
    }
}
    
asked by anonymous 14.07.2017 / 02:15

1 answer

3

After testing several times, I noticed that the class in this lib uses focus listeners to apply the placeholder , and when you try to override the focus methods, it enters an infinite loop because its code changes the field and notifies the listener , which activates the class event to apply the placeholder , and thus keeps repeating infinitely. >

This could be solved if we better understood how the PromptSupport class works until we display the placeholder and change directly in it, something that I find more complicated than creating a class of its own with this functionality.

And to implement a placeholder in JTextfield , you need to monitor the focus of the component. Based on in this answer and on this other in SOEn, I made the class PlaceHolderSupport as an alternative to this lib:

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;

import javax.swing.FocusManager;
import javax.swing.text.JTextComponent;

/**
 * Classe responsável por definir um placeholder a um componente de texto
 * 
 * @author diego
 *
 */
public class PlaceHolderSupport {

    private static JTextComponent textComponent;
    private static String placeHolder = "";

    /**
     * Aplica o texto recebido como placeholder ao componente de texto
     * 
     * @param comp - Componente de texto
     * @param strPlaceHolder - texto do placeholder
     */
    public static void setPlaceHolder(JTextComponent comp, String strPlaceHolder) {

        textComponent = comp;
        placeHolder = strPlaceHolder;
    }

    /**
     * Desenha uma string centralizada no meio do componente representado pelo
     * retangulo
     * 
     * @param g - Instancia de Graphics.
     * @param text - String a ser desenhada.
     * @param rect - Retangulo para centralizar o texto.
     * @param font - Fonte a ser aplicada ao texto
     */
    private static void drawPlaceHolderString(Graphics g, String text, Rectangle rect, Font font) {
        // Obtém as métricas da fonte do texto
        FontMetrics metrics = g.getFontMetrics(font);
        // Determina a coordenada X do texto conforme
        // o tamanho da borda interna esquerda
        int x = textComponent.getBorder().getBorderInsets(textComponent).left;
        // Determina a coordenada Y do texto para que
        // fique centralizado verticalmente
        int y = rect.y + ((rect.height - metrics.getHeight()) / 2) + metrics.getAscent();
        // aplica a fonte
        g.setFont(font);
        // desenha a string
        g.drawString(text, x, y);
    }

    /**
     * Desenha o placeholder no componente de texto
     * 
     * @param g - instancia de Graphics
     */
    public static void onPaintComponent(Graphics g) {

        if (textComponent != null) {
            //verifica se o campo está vazio e se 
            //o foco atual do teclado pertence a ele
            if (textComponent.getText().isEmpty()
                    && !(FocusManager.getCurrentKeyboardFocusManager().getFocusOwner() == textComponent)) {
                Font font = textComponent.getFont().deriveFont(Font.ITALIC);
                g.setColor(Color.gray);
                drawPlaceHolderString(g, placeHolder, textComponent.getBounds(), font);
            } else {
                textComponent.repaint();
            }
        }
    }
}

To use, you need to pass the text of the placeHolder and the instance of the text component in the constructor of this class, using the setPlaceHolder method. In your example of class IconTextField , it looks like this:

public IconTextField() {
    super();
    PlaceHolderSupport.setPlaceHolder(this, "Preencha este campo...");
}

public IconTextField(int cols) {
    super(cols);
    PlaceHolderSupport.setPlaceHolder(this, "Preencha este campo...");
}

Then, in the paintComponent method of your text component, add the following excerpt:

PlaceHolderSupport.onPaintComponent(graphics);

The PlaceHolderSupport.onPaintComponent method checks whether the component is empty and out of focus. If the condition is true, it applies the placeHolder , and when this condition is not true, it forces the component to redraw without the text.

I also suggest that you make a slight modification when adding the focus event on the component as I realized that you try to retrieve the background colors and default borders when losing focus and depending on Look And Feel applied, may not have the expected result. To always get the default background color, you can change as below:

field.addFocusListener(new FocusListener() {

    Color defaultBg = field.getBackground();

    @Override
    public void focusGained(FocusEvent e) {
        field.setBorder(new LineBorder(new Color(108, 85, 255)));
        field.setBackground(Color.LIGHT_GRAY);
    }

    @Override
    public void focusLost(FocusEvent e) {
        field.setBorder(new LineBorder(Color.GRAY));
        field.setBackground(defaultBg);
    }
});

With these changes, your component is already functional with the applied placeholder:

    
15.07.2017 / 17:36