Philippp
Philippp

Reputation: 857

Detect focuslost with many subcomponents

I want to create an expandable panel (to accomodate several UI elements for specific tasks). The panel can be expanded by user action, and should stay expanded as long as any subcomponent in the panel has focus. When the user puts focus somewhere outside the panel, then it should close.

Is there an easy way

Or would I need to register/check for focus(-events) on every subcomponent? (Since this should be a generically usable panel, I need a generic approach which is independent from the specific hierarchy of subcomponents.)

Upvotes: 1

Views: 124

Answers (1)

gthanop
gthanop

Reputation: 3321

A recommended way of grouping focus listeners on multiple Components, according to the official tutorials, is to install a single PropertyChangeListener on the current KeyboardFocusManager. For example something like:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.KeyboardFocusManager;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class Main {
    
    private static void createAndShowGUI() {
        
        final int rows = 5, cols = 5;
        final JPanel focusables = new JPanel(new GridLayout(0, cols, 10, 10));
        for (int row = 0; row < rows; ++row)
            for (int col = 0; col < cols; ++col)
                focusables.add(new JTextField("Focusable", 10));
        
        final JPanel nonFocusable = new JPanel(); //FlowLayout.
        nonFocusable.add(new JTextField("Click here to change focus"));
         
        final JPanel contents = new JPanel(new BorderLayout());
        contents.add(focusables, BorderLayout.CENTER);
        contents.add(nonFocusable, BorderLayout.LINE_END);
        
        KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("focusOwner", new PropertyChangeListener() {
            @Override
            public void propertyChange(final PropertyChangeEvent evt) {
                final Object newValue = evt.getNewValue();
                if (newValue instanceof Component
                        && SwingUtilities.isDescendingFrom((Component) newValue, focusables)) //If 'focusables' is a parent of newValue...
                    focusables.setBackground(Color.GREEN); //Then focus is back on our components...
                else
                    focusables.setBackground(Color.RED); //Else the focus is not on our components (it may even be another application).
            }
        });
        
        final JFrame frame = new JFrame("Multi focus lost");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(contents);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
    
    public static void main(final String[] args) {
        SwingUtilities.invokeLater(Main::createAndShowGUI);
    }
}

According to the tutorial:

The property change listener is notified of every change involving the focus...

How to use the above example:

  1. Run it, and you can click in each JTextField to give it focus.
  2. If the currently focused JTextField has for its parent a Component that we specify (which is the parent JPanel of the desired JTextFields in this case, named focusables) then the background color of that parent changes to green, otherwise red. But you can obviously implement your logic instead of changing the background color.

How the above example works:

  1. We install a PropertyChangeListener on the current KeyboardFocusManager for the property focusOwner, which will let us get notified only when any Component in the application changes its focus-owner property (ie changing the focused Component triggers such an event). We can install a PropertyChangeListener such that we get notified for all sorts of events, but the focusOwner is what we are only intersted in, so we let the system know. For a list of all properties you can track by focus events, scroll to the bottom of the tutorial.
  2. Inside the PropertyChangeListener implementation we only need to check that the new focus owner has the focusables panel for its parent. We can check this by traversing the Component hierarchy upwards from the focus owner, or just call SwingUtilities#isDescendingFrom which is a convenience method for this purpose. If we were listening to all the focus events, then we would also have to filter manually the property focusOwner (by checking that PropertyChangeEvent#getPropertyName returns it).

Note that the focus from our desired Components is lost when we are leaving the application (but gets back when we return).

Also note that the hierarchy of Components in the above example is only one level deep for simplicity (ie the desired focusable Components are direct children of the focusables panel), but we can change this to any level deep we want, as long as the desired Components all have the same parent along their path (which has to be different from the parent of the non-desired ones).


The only exception that I can think of is if you generally have mixed parent Containers, ie JPanels for example that have as direct children both a desired Component and a non-desired one. In this case, you will have to check for each Component individually in the PorpertyChangeListener or implement a FocusListener for each desired Component. The official tutorial on how to implement a FocusListener can be found here.

Upvotes: 1

Related Questions