Wednesday, March 14, 2007

Java password dialog

Maybe you're already familiar with Swing's JOptionPane for simple user interaction. showInputDialog(…) is great for prompting the user for input in a short amount of code, but it's not suitable for passwords, as everything typed is echoed in plain text.

Most methods on JOptionPane accept a "message" as an Object. In summary, this can be either an array of objects (Object[]) to display, a Component, an Icon, or any other type of object which will be converted to a String by calling its toString method. This allows us to pass in a JPasswordField to take properly mask the input for password entry:

JPasswordField jpf = new JPasswordField();
JOptionPane.showConfirmDialog(null, jpf,
  "Password:", JOptionPane.OK_CANCEL_OPTION);

By using an array of objects, an additional label can still be placed above the JPasswordField:

JLabel label = new JLabel("Please enter your password:");
JPasswordField jpf = new JPasswordField();
JOptionPane.showConfirmDialog(null,
  new Object[]{label, jpf}, "Password:",
  JOptionPane.OK_CANCEL_OPTION);

However, both of these examples have the same UI issue - the "OK" button, not the password field, is focused by default. This is an existing issue in Sun's bug database: bug 5018574 - "Unable to set focus to another component in JOptionPane". The only real work-around is to set focus on the password field after the option pane is displayed. Unfortunately, all of the show*Dialog static methods, when called, immediately display and then block until the user provides a response - leaving no opportunity to call for a focus change.

A work-around is to instantiate the option pane directly, as described under "Direct use:" in the JOptionPane API docs. A ComponentListener can then be added to change the focus once the dialog is shown:

import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;

import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JPasswordField;
import javax.swing.SwingUtilities;

…

final JPasswordField jpf = new JPasswordField();
JOptionPane jop = new JOptionPane(jpf,
  JOptionPane.QUESTION_MESSAGE,
  JOptionPane.OK_CANCEL_OPTION);
JDialog dialog = jop.createDialog("Password:");
dialog.addComponentListener(new ComponentAdapter(){
  @Override
  public void componentShown(ComponentEvent e){
    SwingUtilities.invokeLater(new Runnable(){
      @Override
      public void run(){
        jpf.requestFocusInWindow();
      }
    });
  }
});
dialog.setVisible(true);
int result = (Integer)jop.getValue();
dialog.dispose();
char[] password = null;
if(result == JOptionPane.OK_OPTION){
  password = jpf.getPassword();
}

(See the "Input focus" thread on Sun's Developer Forums for some additional background.)

An alternative would be make your own JFrame and JPanel, and add the buttons, icons, event handling, and everything else yourself - things already provided by the JOptionPane class.

10 comments:

Bill la Forge said...

Very handy. Thanks! I've just added it to my RemoteLoginAction module, with attribution of course.
http://agilewiki.wiki.sourceforge.net/

Adam said...

I like this solution the most of everything that's been offered up online, but I haven't been able to get this (or the AncestorListener trick) to work using jdk 1.6.0_13 on linux.

Has anyone else had success with these hacks in later releases of Swing?

Mark A. Ziesemer said...

Adam - I had never tested this under Linux until now, and was able to confirm your issue. What I did notice is that requestFocusInWindow() returns a boolean giving an indication to the success. Under Linux, it was returning false, meaning "the focus change request is guaranteed to fail".

I just updated the example in the post to include the call to requestFocusInWindow() to be executed by SwingUtilities.invokeLater, which currently resolves the issue. I believe this has something to do with the order in which the components are made visible, which may be implemented differently under Linux. The component and all parent components are required to be visible before the requestFocus calls will work.

Thomas said...

Hi Mark.

It seems the ComponentListener is not the best choice, as it can be system-dependant in what order the components are painted, and therefor it is called to early. (at elast on my Vista platform)

If you use a WindowListener instead, it worked fine on all my platforms, since the Window seems usually activated when it is painted completely.

Not sure if it works everywhere, but it can be an alternate solution if the ComponentListener does not do the trick.

--------------

dialog.addWindowListener(new WindowAdapter(){
@Override
public void windowActivated(WindowEvent e){
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run(){
jpf.requestFocusInWindow();
}
});
}
});

--------------

Anonymous said...

I came up with this approach also, but I used a WindowFocusListener (the windowGainedFocus) event, so that when the window gets the focus then the password field requests the focus within the window.

I haven't tested this on my platforms yet to see if it always works (I didn't realize it was an issue until I read this post just now), but it's something to try.

dialog.addWindowFocusListener(new WindowAdapter() {
public void windowGainedFocus(WindowEvent e){
jpf.requestFocusInWindow();
}
});

Akerbos said...

The solution using windowGainedFocus almost worked. I could _see_ the password field having focus before it was moved to the OK button again. I came up with this and it works (at least on my Ubuntu machine running Sun JVM):

wrapper.addWindowListener(new java.awt.event.WindowAdapter(){
@Override
public void windowGainedFocus(java.awt.event.WindowEvent e){
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run(){
jpf.requestFocusInWindow();
}
});
}
});

jpf.addFocusListener(new java.awt.event.FocusListener() {
public void focusGained( FocusEvent e ) {
jpf.selectAll();
}

public void focusLost( FocusEvent e ) {
if ( jpf.getPassword().length == 0 ) {
jpf.requestFocusInWindow();
}
}
});

Anonymous said...

windowOpened worked for me under Vista.

dialog.addWindowListener(new WindowAdapter()
{
public void windowOpened(WindowEvent e)
{
jpf.requestFocusInWindow();
}
});

My problem is that when I close the dialog using Alt+F4, my app goes to lala land since its still waiting for me to press OK/Cancel on a dialog that's already closed.

Anonymous said...

jop.getValue() returns null as soon as the dialog is closed with Alt+F4.

Mark A. Ziesemer said...

All - see the new alternative workaround recently posted by Sun in the evaluation section of the reported bug, 5018574. I've not tested it yet, but it uses a HierarchyListener instead.

Olivier Bertrand said...

Reading all comments it occurs to me that trying to use a buggy dialog and fighting to overcome it is complicated, unnatural, and useless because one never knows if it works on all platforms.

The true solution, of course, would be that Sun people provide a JOptionPane.showPasswordDialog that would do exactly what an InputDialog does with a JPasswordField instead of a JTextField.

Meanwhile, I found much simpler an natural to use a plain dialog, for instance:

public class PasswordDlg extends JDialog implements ActionListener {
boolean id = false;
JButton ok,can;
JPasswordField password;

PasswordDlg(JFrame frame) {
// Set the dialog owner, title and make it modal
super(frame, "Password Dialog", true);

// Make the password field
pwd = new JPasswordField(14);
password.requestFocus();

// Make the text panel
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
panel.add(new JLabel("Please enter your password:"));
panel.add(password);

// Make the button panel
JPanel p = new JPanel();
p.setLayout(new FlowLayout());
p.add(ok = new JButton("OK"));
ok.addActionListener(this);
p.add(can = new JButton("Cancel"));
can.addActionListener(this);
p.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));

//Put everything together, using the content pane's BorderLayout.
Container contentPane = getContentPane();
contentPane.add(panel, BorderLayout.CENTER);
contentPane.add(p, BorderLayout.PAGE_END);
pack();

// Make it visible
setVisible(true);
} // end of PasswordDlg

public void actionPerformed(ActionEvent ae){
id = (ae.getSource() == ok);
setVisible(false);
} // end of actionPerformed

} // end of class PasswordDlg

I can provide on demand a complete PasswordDemo application.