Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UI freezes after child window closing until first click

I'm writing a drop down component with colors for toolbar. So I've taken ideas from 'Swing hacks' book, changed conception a little bit and added Swing's standard JColorChooser to drop down. The behaviour shold be following: I click a button and a window with color chooser appears; I pick a color and the drop down window closes, and text of the button chages color to the picked one. In whole everything works, but there is one unpleasant bug. After these operations UI freezes and the button even doesn't accept mouse events like 'mouse over'. And this happens until I click. Then the UI behaves as wanted.

Here is the code with conception.

import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.AbstractButton;
import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JWindow;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.colorchooser.AbstractColorChooserPanel;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.metal.MetalComboBoxIcon;

class DropDownComponent2 {
    private JWindow _window;
    private boolean _windowShouldBeShown = false;
    private JComponent _component;
    private AbstractButton _button;
    private JFrame _ownerFrame;

    public DropDownComponent2(JFrame ownerFrame, JComponent component, AbstractButton button) {
        _ownerFrame = ownerFrame;
        _component = component;
        _button = button;
        _button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                _window.setVisible(false);
                Point pt = _button.getLocationOnScreen();
                pt.translate(0, _button.getHeight());
                _window.setLocation(pt);
                showWindow();
                _windowShouldBeShown = true;
            }
        });

        _button.addAncestorListener(new AncestorListener() {
            public void ancestorAdded(AncestorEvent event){
                _window.setVisible(false);
            }
            public void ancestorRemoved(AncestorEvent event){
                _window.setVisible(false);
            }
            public void ancestorMoved(AncestorEvent event){
                if (event.getSource() != _window) {
                    System.out.println("Ansestor moved");
                    _window.setVisible(false);
                }
            }
        });

        Toolkit.getDefaultToolkit().addAWTEventListener(
                new AWTEventListener() { 
                    public void eventDispatched(AWTEvent event) {
                        if (event.getID() == MouseEvent.MOUSE_CLICKED) {
                            if ( !_window.getBounds().contains( MouseInfo.getPointerInfo().getLocation() )) {
                                if (_windowShouldBeShown)
                                    _windowShouldBeShown = false;
                                else {
                                    _window.setVisible(false);
                                }
                            }
                        }
                    }            
                }, AWTEvent.MOUSE_EVENT_MASK);

        _window = new JWindow(_ownerFrame);
        _window.getContentPane().add(component);
        _window.addWindowFocusListener(new WindowAdapter() {
            public void windowLostFocus(WindowEvent evt) {
                System.out.println("window lost focus");
                _window.setVisible(false);
            }
        });
        _window.pack();        
    }

    private Rectangle getScreenRect() {
        return new Rectangle(java.awt.Toolkit.getDefaultToolkit().getScreenSize());
    }

    public void showWindow() {
        Rectangle screenRect = getScreenRect();
        Rectangle windowRect = _window.getBounds();

        int sx1 = screenRect.x;
        int sx2 = screenRect.x + screenRect.width;
        int sy1 = screenRect.y;
        int sy2 = screenRect.y + screenRect.height;

        int wx1 = windowRect.x;
        int wx2 = windowRect.x + windowRect.width;
        int wy1 = windowRect.y;
        int wy2 = windowRect.y + windowRect.height;

        if (wx2 > sx2) {
            _window.setLocation(wx1-(wx2-sx2), _window.getY());
        }
        if (wx1 < sx1) {
            _window.setLocation(0, _window.getY());
        }
        if (wy2 > sy2) {
            _window.setLocation(_window.getX(), wy1-(wy2-wy1));
        }
        if (wy2 < sy1) {
            _window.setLocation(_window.getX(), 0);
        }

        _window.setVisible(true);
    }

    public void hideWindow() {
        _window.setVisible(false);
    }  
}

public class DropDownFrame extends JFrame {
    JButton _button;
    JColorChooser _colorChooser;
    DropDownComponent2 _dropDown;
    JWindow _window;

    public DropDownFrame() {
        _colorChooser = new JColorChooser();
        _colorChooser.setPreviewPanel(new JPanel());
        _colorChooser.setColor(Color.RED);

        // Remove panels other than Swatches
        AbstractColorChooserPanel[] panels = _colorChooser.getChooserPanels();
        for (int i=0; i<panels.length; i++) {
            if (!panels[i].getDisplayName().equals("Swatches"))
                _colorChooser.removeChooserPanel(panels[i]);
        }
        _colorChooser.getSelectionModel().addChangeListener(new ChangeListener() {
            // ### I think the key point is there
            @Override
            public void stateChanged(ChangeEvent e) {
                _dropDown.hideWindow();
                _button.setForeground(_colorChooser.getColor());
            }

        });            

        _button = new JButton("Show JWindow");
        _button.setIcon(new MetalComboBoxIcon());
        _button.setHorizontalTextPosition(SwingConstants.LEFT);
        this.getContentPane().add(_button);

        _dropDown = new DropDownComponent2(DropDownFrame.this, _colorChooser, _button);

        pack();
        setVisible(true);        
    }

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

I'm sure there is something with JColorChooser and selection model. But I can't catch the idea. I tried requestFocus() and requestFocusInWindow(). No success. I tried to use JDialog instead of JWindow. When I press [x] on dialog, everything is as wanted, but when I pick color the UI also freezes!

Another point! If I use a label inside drop down window instead of color chooser and handle a click on the label, everything works fine: window closes, and no freezing!

I was placing _dropDown.hideWindow() inside SwingUtilities.invokeLater(). And without success.

What am I missing?

like image 678
Stanislav Avatar asked Jan 29 '13 05:01

Stanislav


1 Answers

Like the other comments to your question, I have not been able to reproduce the freezing of the UI. I tried your code on Windows 7, Sun JDK 7, and Linux Mint, OpenJDK 7. However, I think your code needs improving. First, it seems quite verbose for what it tries to do. And second, you're using some methods that better be avoided.

In your first paragraph you say that your UI freezes until you click somewhere. That sounds contradictory. If it freezes you shouldn't be able to click to get it to work again. So I assume you're merely having a focus problem? Correct me, if I'm wrong. Indeed, after choosing a color the button loses focus, so you have to click twice to open the color chooser again. So your change listener should look like this:

public void stateChanged(ChangeEvent ce) {
    button.setForeground(colorChooser.getColor());
    DropDownWindow.this.setVisible(false);
    // the drop down window had the focus while being displayed
    // so after it closes, return focus to the button
    button.requestFocus();
}

Secondly, your registering a listener directly on the AWT Event Dispatch Thread to catch some mouse events. I don't see why exactly you do this instead of using ordinary mouse listeners of your UI components. The AWT Event Thread should only be used to oberserve events for purposes such as profiling, testing, and debugging. Never should you use it to alter the state of your UI or push expensive code to it. For UI changes you always use specific event listeners on your UI components, or SwingWorkers for more expensive calculations.

Depending on the platform or Java Runtime implementation that you are using, your use of the AWT Event Thread may cause the UI to become somewhat inresponsive, as you're changing the visiblity state of a window from it.

Also, your use of invokeLater to create your drop down window doesn't change anything because that just places code in the AWT Event Thread, where it would have ended up anyway. The methods invokeLater and its friend invokeAndWait are used to synchronize Java's so-called initial threads (one of which executes the main method, and is therefore called the 'main' thread) with the event dispatch thread. You don't use them to run code asynchronously from the UI thread. For instance, if you run a main method that creates a window or a frame like this

public static void main (String[] args) {
    new JFrame().setVisible(true);
}

you can always think of it as being deferred to the event thread by the Java Runtime Environment. So basically what happens is this:

public static void main (String[] args) {
    // JRE 'starts' Swing by creating an event thread and then
    SwingUtilities.invokeLater(new Runnable() {
    public void run (Runnable r) {
        new JFrame().setVisible(true);
    });
}

So your main method encapsulates that code in another Runnable which basically has the same effect and doesn't solve your problem.

I have rewritten your program and shortened it a bit. I'm not sure it does exactly what you're trying to do. I've tried this code on Windows and Linux without any problems. Whether or not you open the color chooser you can see that mouse events on the button are still being processed.

import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JFrame;
import javax.swing.JWindow;
import javax.swing.colorchooser.AbstractColorChooserPanel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

class DropDownWindow extends JWindow {
    static void setColorChooserPanels (JColorChooser jcc, String name) {
        for (AbstractColorChooserPanel p : jcc.getChooserPanels()) {
            if (!p.getDisplayName().equals(name)) {
                jcc.removeChooserPanel(p);
            }
        }
    }

    final JColorChooser colorChooser;

    DropDownWindow (JFrame ownerFrame, final JButton button) {
        super(ownerFrame);

        colorChooser = new JColorChooser();
        setColorChooserPanels(colorChooser, "Swatches");
        colorChooser.setVisible(true);
        colorChooser.getSelectionModel().addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent ce) {
                button.setForeground(colorChooser.getColor());
                DropDownWindow.this.setVisible(false);
                button.requestFocus();
            }
        });

        add(colorChooser);
        setSize(colorChooser.getPreferredSize());
        pack();

        button.addActionListener(new ActionListener() {
            public void actionPerformed (ActionEvent e) {
                Point pt = button.getLocationOnScreen();
                pt.translate(0, button.getHeight());
                DropDownWindow.this.setLocation(pt);
                DropDownWindow.this.setVisible(true);
            }
        });
    }
}

class MyFrame extends JFrame {
    final JButton button;

    MyFrame () {
        super();

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(300, 100);
        setLocation(500, 300);

        button = new JButton("Choose Color");

        button.addMouseListener(new MouseAdapter() {
            public void mouseEntered (MouseEvent event) {
                System.out.println("mouse entered at: (" + event.getXOnScreen() +
                        ", " + event.getYOnScreen() + ")");
            }
        });

        button.addMouseListener(new MouseAdapter() {
            public void mouseExited (MouseEvent event) {
                System.out.println("mouse exited at: (" + event.getXOnScreen() +
                        ", " + event.getYOnScreen() + ")");
            }
        });

        add(button);

        DropDownWindow ddw = new DropDownWindow(this, button);

        setVisible(true);
    }
}

public class Test {
    public static void main(String[] args) {
        new MyFrame();
    }
}

Please try this code and tell me if your problem goes away. If not, please elaborate a bit further on the effects you experience. Does the UI really freeze? Do you have responsiveness issues? Or is it simply a focus problem that requires you to click more than you want.

like image 128
alexkelbo Avatar answered Oct 16 '22 09:10

alexkelbo