6

I'm pretty new to Swing development, hope my question is not a stupid one.

I've got a following problem. I am tracking the focus using KeyboardFocusManager, listening for property permanentFocusOwner changes. However when the focus changes from one control to another, I get intermediate change of permanentFocusOwner property to null.

My current UI logic is making some changes to the controls when the focus is inside one of the panels or its child panels. However getting intermediate null breaks this logic.

I searched in Google for information about this problem, didn't find anything relevant.

The question is, whether this behaviour is by design, and if there is some way to workaround intermediate nulls.

Here is the minimal application reproducing the said behaviour:

import java.awt.*;
import java.beans.*;
import javax.swing.*;

public class FocusNullTest extends JFrame {

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                FocusNullTest self = new FocusNullTest();
                self.setVisible(true);
            }
        });
    }

    public FocusNullTest() {
        setSize(150, 100);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container contentPane = getContentPane(); 
        contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.X_AXIS));

        contentPane.add(new JButton("1"));
        contentPane.add(new JButton("2"));

        KeyboardFocusManager focusManager =
            KeyboardFocusManager.getCurrentKeyboardFocusManager();
        focusManager.addPropertyChangeListener(
                "permanentFocusOwner",
                new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent e) {
                        System.out.println("permanentFocusOwner changed from: "
                                           + e.getOldValue());
                        System.out.println("permanentFocusOwner changed to  : "
                                           + e.getNewValue());
                    }
                });
    }
}

The log output is:

(program start, focus sets to button 1 automatically)
permanentFocusOwner changed from: null
permanentFocusOwner changed to : javax.swing.JButton[,0,18,41x26, (skipped)]
(clicked on button 2)
permanentFocusOwner changed from: javax.swing.JButton[,0,18,41x26, (skipped)]
permanentFocusOwner changed to : null
permanentFocusOwner changed from: null
permanentFocusOwner changed to : javax.swing.JButton[,41,18,41x26, (skipped)]


(optional part, on the code intention)
My goal is to make something looking like a list view, where the entries expand and display more information when they get focus (and collapse back when they lose it). The expanded view contains some additional buttons.

JList doesn't seem to be the appropriate control, because (1) it doesn't allow clicks on the buttons, and (2) its entries have constant height, whereas I want the entries to expand dynamically on focus. JTable with its edit mode seems to be not an appropriate solution as well, at least because of constant entry size.

So I am using plain JPanel with a vertical box layout as a container, and subscribe to model changes and update the visuals manually. The problem is that when I click on a button, the containing list item loses focus. I could detect that the focus still stays within the list item if the focus wouldn't change to null temporarily.

Vlad
  • 35,022
  • 6
  • 77
  • 199

4 Answers4

3

KeyboardFocusManager is firing two events for most properties (as of beans spec, it shouldn't - never found out the reason, just guessing that the asynchrous nature of focus somehow might be the reason)

   firePropertyChange(someProperty, oldValue, null)
   firePropertyChange(someProperty, null, newValue)

for doing stuff depending on newVaue, wait for the second

kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • 1
    I can't help speculating that this is the result of cross-platform variations in how a window gains focus. – trashgod Sep 10 '11 at 15:03
2

As workaround, store the last "real" previous focus owner as a member in your event handler.

if ((e.getOldValue() != null) && (e.getNewValue() == null))
 prev_owner = e.getOldValue();

Then you'll have a handle to that object when you focus actually lands on the target. Handle the highlighting changes only when a real component actually receives focus (i.e. when getNewValue() is non-null).

(The behavior seems consistent with what is described in The AWT Focus Subsystem, in the sens that the previous component loses its focus first, then the target component gets it. It's not atomic, so there is a period of time where nothing actually has focus. But I'm no expert, so this may vary.)

Mat
  • 202,337
  • 40
  • 393
  • 406
2

My goal is to make something looking like a list view, where the entries expand and display more information when they get focus.

An an alternative, I sometimes use a JSplitPane: on the left, I put the (focusable) expand button in a JTable, Outline or vertical Box of panels; on the right, I put the expanded view.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Thank you for your suggestion! While this would lead to a different UI, it's definitely a nice design. (And I didn't hear about `Outline` before.) – Vlad Sep 10 '11 at 15:14
  • By the way, is there a easy way to bind the JPanel/Box children to a model, the same way like JTable's items can be bound to a TableModel? – Vlad Sep 10 '11 at 15:24
  • I usually give the button an [`Action`](http://download.oracle.com/javase/tutorial/uiswing/misc/action.html) in the `JPanel` constructor. The `actionPerformed()` method queries the application's data model and updates the expanded view. The `Action` can be invoked from a button, menu, toolbar or a focus listener. – trashgod Sep 10 '11 at 16:43
  • @trashgod I some thinking about that, great, excelent suggestion, but invoked me ..., then I ends with oldiest ideas as I know, please see my post, previously upvoted +1, – mKorbel Sep 10 '11 at 17:20
  • @mKorbel: Now that you mention it, the `actionPerformed()` method used the familiar sequence, `p.remove(old); p.add(new); p.revalidate(); p.repaint();` to do the update. Can I impose on you to cite one of your related examples? – trashgod Sep 10 '11 at 17:45
  • @Vlad: There's nothing built-in, but you can use the same [`EventListenerList`](http://download.oracle.com/javase/7/docs/api/javax/swing/event/EventListenerList.html) approach in your `ApplicationModel` as the one implemented in `AbstractTableModel`. Sorry I overlooked this question earlier. – trashgod Sep 10 '11 at 21:00
  • 1
    @trashgod: I am using [`SwingPropertyChangeSupport`](http://download.oracle.com/javase/7/docs/api/javax/swing/event/SwingPropertyChangeSupport.html) at the moment for my model objects. It seems to incapsulate `EventListenersList`. Well, having an abstract model in Swing for _each_ of the controls would be quite good, but I am not against building something manually -- at least this will give me understanding of how the `TableModel` and other existing models work. (I've got experience with .NET/WPF/MVVM, this seems to be roughly the same.) – Vlad Sep 10 '11 at 22:21
2
My goal is to make something looking like a list view, where the entries 
expand and display more information when they get focus (and collapse back 
when they lose it). The expanded view contains some additional buttons.

enter image description here enter image description here

ButtonModel can do that by using JButton, very nice output is by using JToggleButton or still is there original idea with held there JPanel + MouseListener ()

import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class CollapsablePanelTest {

    public static void main(String[] args) {
        CollapsablePanel cp = new CollapsablePanel("test", buildPanel());
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().add(new JScrollPane(cp));
        f.setSize(360, 300);
        f.setLocation(200, 100);
        f.setVisible(true);
    }

    public static JPanel buildPanel() {
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(2, 1, 2, 1);
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        JPanel p1 = new JPanel(new GridBagLayout());
        p1.setBackground(Color.blue);
        gbc.gridwidth = GridBagConstraints.RELATIVE;
        p1.add(new JButton("button 1"), gbc);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        p1.add(new JButton("button 2"), gbc);
        gbc.gridwidth = GridBagConstraints.RELATIVE;
        p1.add(new JButton("button 3"), gbc);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        p1.add(new JButton("button 4"), gbc);
        return p1;
    }

    private CollapsablePanelTest() {
    }
}

class CollapsablePanel extends JPanel {

    private static final long serialVersionUID = 1L;
    private boolean selected;
    private JPanel contentPanel_;
    private HeaderPanel headerPanel_;

    private class HeaderPanel extends JButton /*JToggleButton //implements MouseListener*/ {

        private static final long serialVersionUID = 1L;
        private String __text;
        private Font __font;
        private BufferedImage open, closed;
        private final int OFFSET = 30, PAD = 5;

        public HeaderPanel(String text) {
            //addMouseListener(this);
            __text = text;
            setText(__text);
            __font = new Font("sans-serif", Font.PLAIN, 12);
            // setRequestFocusEnabled(true);
            setPreferredSize(new Dimension(200, 30));
            int w = getWidth();
            int h = getHeight();
            /*try {
            open = ImageIO.read(new File("images/arrow_down_mini.png"));
            closed = ImageIO.read(new File("images/arrow_right_mini.png"));
            } catch (IOException e) {
            e.printStackTrace();
            }*/
            getModel().addChangeListener(new ChangeListener() {

                @Override
                public void stateChanged(ChangeEvent e) {
                    ButtonModel model = (ButtonModel) e.getSource();
                    if (model.isRollover()) {
                        toggleSelection();
                    } else if (model.isPressed()) {
                        toggleSelection();//for JToggleButton
                    }
                }
            });
        }

        /*@Override
        protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        int h = getHeight();
        ///if (selected)
        //g2.drawImage(open, PAD, 0, h, h, this);
        //else
        //g2.drawImage(closed, PAD, 0, h, h, this);
        // Uncomment once you have your own images
        g2.setFont(font);
        FontRenderContext frc = g2.getFontRenderContext();
        LineMetrics lm = font.getLineMetrics(__text, frc);
        float height = lm.getAscent() + lm.getDescent();
        float x = OFFSET;
        float y = (h + height) / 2 - lm.getDescent();
        g2.drawString(__text, x, y);
        }
        @Override
        public void mouseClicked(MouseEvent e) {
        toggleSelection();
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }

        @Override
        public void mousePressed(MouseEvent e) {
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }*/
    }

    public CollapsablePanel(String text, JPanel panel) {
        super(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(1, 3, 0, 3);
        gbc.weightx = 1.0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        selected = false;
        headerPanel_ = new HeaderPanel(text);
        setBackground(Color.orange);
        contentPanel_ = panel;
        add(headerPanel_, gbc);
        add(contentPanel_, gbc);
        contentPanel_.setVisible(false);
        JLabel padding = new JLabel();
        gbc.weighty = 1.0;
        add(padding, gbc);
    }

    public void toggleSelection() {
        selected = !selected;
        if (contentPanel_.isShowing()) {
            contentPanel_.setVisible(false);
        } else {
            contentPanel_.setVisible(true);
        }
        validate();
        headerPanel_.repaint();
    }
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
  • Thanks a lot for your code! However, I see some issues with regard to what I'd like to implement. First, the expansion is toggled by button activation. This is the same as getting focus if the user makes it with the mouse, but if the focus is set focus by keyboard (e.g., by tabbing), the button won't be activated. Second, the panel should collapse as soon as focus leaves it. – Vlad Sep 10 '11 at 19:53
  • if isn't there no MouseListener then ButtonModel works for me with javax.swing.Timer (for delayed showing, or if is there more than one Action, or possible concurency from nearest JComponents) & KeyBinding (not TAB, but F1, F2 or F5) can sovle that, but I still think that would be better to implements JWindow as Container instead of jumping JPanel – mKorbel Sep 10 '11 at 21:00
  • I am not sure that I understand which exactly MouseListener did you mention. For the ButtonModel to work, we need to ensure that the button is in pressed state as long as the focus is on the button or on any item from the expanded panel -- is there an easy way for it? For now, I am just tracking focus like in the example, additionally ignoring changes to null. The UI is defined not by me, so jumping panels seem to be unavoidable. – Vlad Sep 10 '11 at 22:05
  • Just tries once more: your code collapses/uncollapses the content on mouse hover, not on focus. My initial problem is in detecting when the focus leaves the control _and all its child controls_; having cleanly solved this problem I would be able to toggle content the way like you proposed. – Vlad Sep 10 '11 at 22:15
  • `that the button is in pressed state as long as the focus is on the button` hard job - if isRollover, then display container, remove ButtonModel imediatelly and put there MouseMotionListener, then if Cursor moved out of JButton's and Container's getBounds, then to start Timer (delay 500-750msec) for hide Container and adds ButtonModel back, if Cursor returns back just stop Timer, define only Timer#setrepeat(false) – mKorbel Sep 10 '11 at 22:22
  • My approach with `permanentFocusOwner` property change listener seems to be easier, doesn't it? If I would just ignore null focus owners, the problem seems to be more or less solved. – Vlad Sep 10 '11 at 22:25
  • I never use that, nor needed that, but not true that is or isn't useless, maybe will be works... :-) – mKorbel Sep 10 '11 at 22:30
  • Yes, it seems to work. Not sure if there are some hidden problems with it. The approach with timers and setting/removing the models etc. seems somewhat hackish :-) – Vlad Sep 10 '11 at 22:35