The Source for Java Technology Collaboration
User: Password:



Alexander Potochkin

Alexander Potochkin's Blog

Enabling/Disabling Swing Containers

Posted by alexfromsun on June 26, 2007 at 08:58 AM | Comments (23)

The fact that disabling a Swing container doesn't mean disabling its child component has always been surprising for Swing beginners
for example see this thread
moreover sometimes it is surprising for me as well :-)

Consider the following code:

JPanel panel = new JPanel(); 
panel.add(new JButton("Hello"));       
frame.add(panel); 
         
panel.setEnabled(false);

When the panel is disabled its child button remains perfectly accessible.

In this blog we'll examine how to disable a container with all its children
and let's start with the most well-known and, at the same time, the most limited technique:

Blocking GlassPane

The GlassPane technique can hardly be used to disable a particular container,
it's always used to disable a whole frame, which can be desirable during a time consuming operation

Amost a year ago I wrote A well-behaved GlassPane blog, where I described how to properly create "dynamic" GlassPanes,
which respond to MouseEvents and don't break the "mouseEvents transparency"

However in some cases GlassPane can deliberately consume all mouseEvents to make the frame looks "frozen"

I didn't cover this case in the previous blog because it looked quite trivial
you just set you own GlassPane with fancy animation, add a mouseListener to it to break the "mouseEvents transparency" and that's it

public class InitialGlassPane extends JPanel { 
    public InitialGlassPane() { 
        setOpaque(false); 
 
        // This is breaking the "mouseEvents transparency" 
        // see also  
        // http://weblogs.java.net/blog/alexfromsun/archive/2006/09/index.html 
        // http://weblogs.java.net/blog/alexfromsun/archive/2005/10/index.html 
        addMouseListener(new MouseAdapter() { 
        }); 
    } 
 
    protected void paintComponent(Graphics g) { 
        Graphics2D g2 = (Graphics2D) g.create(); 
        // fill the component with the translucent color 
        g2.setColor(new Color(0, 0, 128, 128)); 
        g2.fillRect(0, 0, getWidth(), getHeight()); 
        g2.dispose(); 
    } 
}

at least this component looks like a good candidate to be a blocking GlassPane

Check it out:

For this example I created a frame to test the InitialGlassPane

  1. Select the "Show GlassPane" menuItem from the Option menu or press Ctrl-D for the same result
    (letter "D" means that we are gonna "disable" the frame)
  2. Click to the frame's components with the mouse
  3. They don't react - wow, that's what we need !

But... let' try to use keyboard

  1. If the first checkbox is focused, press Space
  2. The checkbox gets selected !
  3. Press Tab and the focus will move to the next component
  4. You can even type in the textFields
  5. This is hardly how disabled components should behave
Initial blocking GlassPane


InitialGlassPane blocks mouseEvents but doesn't block events from the keyboard

I've seen several blocking glassPanes which can show very nice animation but failed to block the keyboard
so it was possible to change the state of the components behind them

To solve this problem we should request focus to our GlassPane and make it to keep the focus until the GlassPane is visible
The InputVerifier class will help us to do that

public class BetterGlassPane extends JPanel { 
    public BetterGlassPane() { 
        setOpaque(false); 
 
        // This is breaking the "mouseEvents transparency" 
        // see also  
        // http://weblogs.java.net/blog/alexfromsun/archive/2006/09/index.html 
        // http://weblogs.java.net/blog/alexfromsun/archive/2005/10/index.html 
        addMouseListener(new MouseAdapter() { 
        }); 
         
        // This component keeps the focus until is made hidden         
        setInputVerifier(new InputVerifier() { 
            public boolean verify(JComponent input) { 
                return !isVisible(); 
            } 
        }); 
    } 
 
    protected void paintComponent(Graphics g) { 
        Graphics2D g2 = (Graphics2D) g.create(); 
        // fill the component with another translucent color 
        g2.setColor(new Color(0, 128, 128, 128)); 
        g2.fillRect(0, 0, getWidth(), getHeight()); 
        g2.dispose(); 
    } 
} 

  1. Restart the GlassPane test from the previous link
  2. Select the "BetterGlassPane" from the option menu
  3. Press Ctrl-D
  4. Now it is impossible to change any component's state with the keyboard as well as with the mouse
  5. The frame is truly disabled now

Here is the source code for the GlassPane example

Why blocking GlassPane is *not* a good solution ?

The BetterGlassPane solves the keyboard issue and nevertheless it still has a big problem:

All good applications never block the GUI, so a user can always interact with it

A GlassPane blocks all components in the frame, including the MenuBar, so if you use it during a time-consuming operation,
user simply has no way to cancel this operation

GlassPanes don't solve the main problem -
How to disable a particular container with all its children ?

=== The way to go ===


The JXLayer is a great choice if you want to enrich existing components and it also solves our main problem.

Wrap your container with the JXLayer and call JXLayer.setLocked(true) after that - all components inside will be disabled


Note: Initialy JXLayer overrode setEnabled() method to provide the same functionality, but this led to big problems with mouse cursor support,
so the old setEnabled() method was rewritten as setLocked(boolean) and LockedLayerDemo was accordingly updated.

Seeing is believing:

  1. Select the checkbox at the righ hand side of the frame
  2. The container will become disabled
  3. Try to click on it with the mouse and see how the focus is traversed when you press the Tab key
JXLayer is even more capable, it remembers its last focused child component:
  1. Enable the container, focus a textField and start typing
  2. Press Alt-D to disable a container
  3. Don't move focus with the mouse or the Tab key to another component
  4. press Alt-D one more time to enable the layer again
  5. Notice that your textField is still focused and you can continue typing

The powerful painters' support makes it easy to modify the visual appearance of a container when it is disabled:

  1. Disable a container
  2. Switch between three painters with help of the menu or the radioButton from the right hand side
  3. Notice how the disabled component can be modified
  4. Change the Look and Feel with the menu, resize the frame - the disabled container will be accordingly repainted
JXLayer demo


JXLayer is extremely easy to use, check out the source code of this demo.

By the way, two of my painters use the beautiful open-sourced image filters from www.jhlabs.com,
don't miss this very useful resource

The JXLayer is an open source project, all files with the demos included you can find on the SwingHelper project

I am happy to have so much feedback from everyone who uses JXLayer in their projects
more blogs are coming


Note: Check out the Disabling Swing Containers, the final solution blog

Thanks
alexp


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • Hi Alex, 
    
    JXLayer is simple and good thing. I suppose that such component
    has to be included into JDK. Some time ago I also did 
    same or similar wheel invention :) and probably many other 
    developers too.
    
    BTW how it will work if I set a hourglass cursor to JXLayer in 
    disabled state? Will the cursor appear only over JXLayer? 
    As I remember there were problems with setting cursors to 
    disabled components.
    
    Maxim

    Posted by: maxz1 on June 26, 2007 at 11:47 PM

  • Here's a link to a discussion (in SwingLabs forum) trying to shine light on the darker issues with container-wide enable/disable:

    http://forums.java.net/jive/thread.jspa?threadID=13758 Congrats if JXLayer can cope with them ;-)

    Jeanette

    Posted by: kleopatra on June 27, 2007 at 06:26 AM

  • Hello Maxim

    Initially disabled JXLayer supported only default mouse cursor
    as well as any other disabled Swing components do

    But when I read your comment I realized that being able to
    set a custom cursor for disabled layer is a useful feature

    With help of some AWT magic the request is completed
    Check out the updated demo from this blog

    I added a new checkbox and you can set the wait cursor for disabled layer

    I set the cursor to the layer's GlassPane
    since it covers the whole layer and all its child components

    layer.getGlassPane().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

    it does the trick

    Thank you for your valuable comment !
    alexp

    Posted by: alexfromsun on June 27, 2007 at 06:44 AM

  • Hello Jeanette

    Thanks for the link
    By the way, JXLayer doesn't use all that spooky techniques like
    "disable everything recursively"
    :-)

    The solution is clear and elegant -
    I use the layer's GlassPane to block the mouseEvents
    and use a custom FocusTraversalPolicy to prevent child components from receiving focus

    together with keeping the recent focus owner, mouse cursors
    and complete painters' support it makes a really good solution

    I extensively tested this functionlaity and now
    I am sure that puzzle is solved

    Your feedback is very welcome

    Thanks
    alexp

    Posted by: alexfromsun on June 27, 2007 at 07:02 AM

  • This is very cool. One thing though, if you call isEnabled() on any of the child components they will return true right? So while this is just as effective as disabling, it's not really disabling the children. It's more like blocking or restricting them. I built something similar for our application. Ours is used for long calls to a server and optionally displays a message, icon, and infinite progress bar. We don't have the nifty blur or emboss effects.

    Posted by: aberrant on June 27, 2007 at 08:15 AM

  • Alex, somehow I knew I should have looked at your code before posting Will do soon, thanks!

    Jeanette

    Posted by: kleopatra on June 27, 2007 at 08:22 AM

  • BTW, could you all please not use those funky scrollpanes in the messages? It makes hard reading because a) I have to left scroll and b) the font size inside is increased differently from the font size outside it (yeah, my eyes are not the youngest, so they prefer a bigger font :-) Thanks!

    Posted by: kleopatra on June 27, 2007 at 08:28 AM

  • Hello Aberrant

    That's correct - I don't use recursive disabling
    because I don't think it's a good solution

    It's nice to hear that you've used similar approach
    I hope you'll try JXLayer in your next project
    please check the other JXLayer demos from SwingHelper.dev.java.net
    you may find them useful

    Thanks
    alexp

    Posted by: alexfromsun on June 27, 2007 at 08:51 AM

  • Hello Jeanette

    I reformatted my comments
    Hope it looks better now

    Thanks
    alexp

    Posted by: alexfromsun on June 27, 2007 at 08:53 AM

  • Hi Alex and again thanks a lot for the elegant JXLayer solution !

    I have one question/problem that I hope you can help with.

    I use the JXLayer for my Hourglass disable/enable solution. The only problem is that when I disable/enable the JXLayer from for example a click of a button, the focus of the button is not retained after the JXLayer is enabled again.

    Do you have any idea what could be the matter, or of a possible workaround?

    You can see this by changing your demo just a little bit.
    To save you a minute, here's a code snip:

    private JComponent createLayerPanel() { JComponent panel = new JPanel(); panel.add(new JCheckBox("JCheckBox")); panel.add(new JRadioButton("JRadioButton")); panel.add(new JTextField(15)); JButton btn = new JButton("Have a nice day"); btn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { disableEnable(); disableEnable(); } }); panel.add(btn); panel.add(new JTextField(15)); panel.add(new JCheckBox("JCheckBox")); panel.add(new JRadioButton("JRadioButton")); panel.add(new JTextField(15)); panel.add(new JCheckBox("JCheckBox")); panel.add(new JRadioButton("JRadioButton")); panel.setBorder(BorderFactory.createEtchedBorder()); return panel; }

    private void disableEnable() { layer.setEnabled(!layer.isEnabled()); if (layer.isEnabled()) { // Reset cursor for enabled layer layer.getGlassPane().setCursor(null); } else if (waitCursorItem.isSelected()) { layer.getGlassPane().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); } waitCursorItem.setEnabled(!layer.isEnabled()); }

    Best regards, Gath

    Posted by: gath55 on August 01, 2007 at 05:37 AM

  • Hello Gath

    I just fixed this problem,
    please test the latest version
    and let me know if everything is ok

    Thanks for using JXLayer
    alexp

    Posted by: alexfromsun on August 01, 2007 at 11:50 AM

  • Hi Alex, thank you for the quick response.

    The problem in the demo is fixed. I still have a slight problem, for example if I make a button open a dialog (modal), when I close the dialog, the focus still disappears. That is if I'm doing it like this:

    disableLayer()
    showModalDialog()
    enableLayer()

    But now with your fix I can work around that. Please let me know if my current problem is something you could fix as well, because otherwise I have to go through a lot of code :-/ :-)

    Best regards, Gath

    Posted by: gath55 on August 01, 2007 at 02:04 PM

  • Hello Gath

    I see no reason why to disable a layer when modal dialog is shown
    because in this case you can't do anything with any component from a blocked window.
    Could you give more details, why you need it ?

    Thanks
    alexp

    Posted by: alexfromsun on August 01, 2007 at 03:07 PM

  • Hello Alex,

    I agree with you and I can fix this now, the reason I asked was just that I have a lot of code which is like this:

    try
    {
    showHourglassAndDisableLayer();
    openModalDialog();
    if(dialogNotCanceled())
    {
    doStuffBasedOnInput();
    }
    }
    finally
    {
    hideHourglassAndEnableLayer();
    }


    Before when I used a not as good disabling technique it worked fine because when the dialog was closed the Focus would return to, for example, the button which was clicked to generate the action that opens the dialog.

    I don't know the details of how the Java Focus system works and why it doesn't work the same way with JXLayer, but if this is not a reasonable thing for you to fix, don't worry, I will just go through the code and make the changes.

    Thanks for the help,

    Gath

    Posted by: gath55 on August 02, 2007 at 03:58 AM

  • Hello Gath

    It doesn't work the same way for JXLayer,
    because it is a special component and I made some efforts
    to change the way how a disabled layer works with focus

    When you disable a layer and the focus inside it,
    layer requests the focus to its glassPane
    and that's why it allows it to efficiently disable all its children
    so the solution is -
    if you want JXLayer to work with focus exaclty like any other container don't disable it

    I am not sure that you really need to disable it for your case
    modal dialog blocks all components from layer's window
    and the only thing you need for your case is to show Hourglass cursor
    you don't need to disable your layer to do that

    layer.getGlassPane().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

    I would also set the cursor back before doStuffBasedOnInput and skip the finally clause

    Thanks
    alexp

    Posted by: alexfromsun on August 02, 2007 at 05:10 AM

  • Hi Alex, yes I agree, I just need to clean up the code a little bit.

    Thanks again !!

    Gath

    Posted by: gath55 on August 02, 2007 at 12:47 PM

  • Hello Everybody

    It turned out that it was impossible to correctly support mouse cursor for disabled layer,
    so the JXLayer.setEnabled() method was rewritten as JXLayer.setLocked(boolean)
    I am sorry for any inconvenience it may cause

    the LockedLayerDemo was accordingly updated,
    now everything works just perfect

    Thanks
    alexp

    Posted by: alexfromsun on August 09, 2007 at 08:05 AM

  • I am going to check out JXLayer, but the InputVerifier approach caught my eye because it is far more elegantly succinct than my convoluted prior approach to add/remove KeyEventDispatcher listeners on the global KeyboardFocusManager based on the glass pane's visibility. Alex clearly describes the InputVerifier technique:
    To solve this problem we should request focus to our GlassPane and make it to keep the focus until the GlassPane is visible...
    The code snip above shows how to set the InputVerifier in the ctor, but it took me a bit of a while to see that he was requesting focus of the glass pane in his MainFrame.java test code. Simply overriding the glass pane's setVisible() method would automate that step:
    @Override
    public void setVisible(boolean aFlag) {
            super.setVisible(aFlag);
            if (aFlag) {
                requestFocusInWindow();            
            }
        }
    

    Posted by: gkedge on August 27, 2007 at 01:16 PM

  • Hello everybody
    In the previous example: is it possible to lock the JXLayer (i.e. disable container) and add some JButton on top of the JXLayer (eg. with name 'Disable container') that re-enables container when is clicked? Of course, JButton is invisible when JXLayer isn't locked.
    Thanks,
    Danijel

    Posted by: danijelbasic on December 20, 2007 at 02:26 AM

  • Hello Danijel

    JXLayer has its own GlassPane, see JXLayer.getGlassPane() method
    you can set any layout and add any components to it
    so it is quite easy to implement what you described

    Thanks> alexp

    Posted by: alexfromsun on December 20, 2007 at 05:00 AM

  • Hello everybody

    There seem to be a minor bug in the blocking in the "Locked/Unlocked layer demo". It do not block for mnemonic keystrokes.

    I added a mnemonic to the "Have a nice day" button and it sadly get activated despite the locking.

    A slightly more complicated variant: I moved the JMenuBar to inside the locked area and added a M mnemonic to 'Menu' Now the menu bar looks locked when I lock it, but the menu is still working, I can make it drop down and change look and feel despite it was intended to be locked.

    In my application I have one critical operation, and while it is running I will not like my user to be able to do anything else

    How do I accomplish that?

    Lars Worsaae

    Posted by: worsaae on January 04, 2008 at 04:47 AM

  • Hello Lars

    Mnemics are not blocked inside the locked layer,
    it is sad but true

    I didn't find an acceptable solution for this problem yet
    will let you know when I have any news

    As for menuBar, it is not designed to be added to arbitrary components,
    frame.setJMenuBar() is the only recommeded usage
    if you need to disable it, try menuBar.setEnabled(false)

    Thanks
    alexp

    Posted by: alexfromsun on January 11, 2008 at 07:43 AM

  • Hello. I found your blog looking for good solution for completly broken InputVerifier functionality. In short InputVerifier works only in very limited way. It does not prevent from:
    -switching JTabbedPanePane
    -selecting combobox value with mouse
    -selecting value in JTree
    -firing JButton actionPerformed
    Case is described here

    There are many attempts to make it work but most promising is using GlassPane as described here Unfortunetly this is not well behaved glassPane. Maybe it would be good to construct it with your approach to make it finally working.

    Best regards,
    Marek Mosiewicz
    http://www.jotel.com.pl

    Posted by: marekmosiewicz on April 02, 2008 at 04:48 AM



Only logged in users may post comments. Login Here.


Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds