Preferences dialogs

Settings for user preferences are used in most graphical applications. These settings control the operation and appearance of a program, and need to be available to many classes. On the other hand, the dialogs for user preferences are usually quite extensive, and can take a long time to build and display to the user.

An advantageous design would allow quick programmatic access to preferences, but construct GUIs only if necessary.

Example

A common style is to use a JTabbedPane, with each pane corresponding to a set of related preferences:

Typical Preferences Dialog

Here, each pane is defined as an implementation of an interface - PreferencesEditor:

package hirondelle.stocks.preferences;

import javax.swing.JComponent;

/**
* Allows editing of a set of related user preferences.
*
* <P>Implementations of this interface do not define a "stand-alone" GUI, 
* but rather a component (usually a <tt>JPanel</tt>) which can be used by the 
* caller in any way they want. Typically, a set of <tt>PreferencesEditor</tt> 
* objects are placed in a <tt>JTabbedPane</tt>, one per pane.
*/
public interface PreferencesEditor { 

  /**
  * Return a GUI component which allows the user to edit this set of related 
  * preferences.
  */  
  JComponent getUI();

  /**
  * The name of the tab in which this <tt>PreferencesEditor</tt>
  * will be placed. 
  */
  String getTitle();

  /**
  * The mnemonic to appear in the tab name.
  *
  * <P>Must match a letter appearing in {@link #getTitle}.
  * Use constants defined in <tt>KeyEvent</tt>, for example <tt>KeyEvent.VK_A</tt>.
  */
  int getMnemonic();
  
  /**
  * Store the related preferences as they are currently displayed, overwriting
  * all corresponding settings.
  */
  void savePreferences();

  /**
  * Reset the related preferences to their default values, but only as 
  * presented in the GUI, without affecting stored preference values.
  *
  * <P>This method may not apply in all cases. For example, if the item 
  * represents a config which has no meaningful default value (such as a mail server 
  * name), the desired behavior may be to only allow a manual change. In such a 
  * case, the implementation of this method must be a no-operation. 
  */
  void matchGuiToDefaultPreferences();
}
 

The getUI method of this interface defines the graphical aspect of a PreferencesEditor - implementations do not extend a JComponent, but rather return a JComponent from the getUI method. This allows callers to access preference values from implementations of PreferencesEditor without necessarily constructing a GUI.

Here's an example implementation for the preferences shown above. Note the addition of these methods, specific to this class, which allow programmatic, read-only access to stored preference values (and not the preference values as currently displayed in the GUI):

This implementation extends Observable, such that interested classes may register their interest in changes to this set of preferences:
package hirondelle.stocks.preferences;

import java.util.*;
import java.util.logging.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.prefs.*;
import javax.swing.plaf.metal.MetalTheme;

import hirondelle.stocks.util.ui.Theme;
import hirondelle.stocks.util.ui.UiConsts;
import hirondelle.stocks.util.ui.UiUtil;
import hirondelle.stocks.util.Util;

/**
* Allows editing of user preferences related to the general 
* appearance of the application, such as font size, toolbar icon size, theme, 
* and the like.
*
* <P>Also allows programmatic read-only access to the current stored preferences 
* for these items.
*/
public final class GeneralLookPreferencesEditor extends Observable 
  implements PreferencesEditor {

  @Override public JComponent getUI(){
    JPanel content = new JPanel();
    GridBagLayout gridbag = new GridBagLayout();
    content.setLayout(gridbag);
    addShowToolbarAndLargeIcons(content);
    addTheme(content);
    addRestoreDefaults(content);
    UiUtil.addVerticalGridGlue(content, 4);
    matchGuiToStoredPrefs();
    return content;
  }
  
  @Override public String getTitle() {
    return TITLE;
  }
  
  @Override public int getMnemonic() {
    return MNEMONIC;
  }

  @Override public void savePreferences(){
    fLogger.fine("Updating general preferences.");
    fPrefs.putBoolean(SHOW_TOOL_BAR_KEY, fShowToolBar.isSelected());
    fPrefs.putBoolean(USE_LARGE_ICONS, fLargeIcons.isSelected());
    fPrefs.put(THEME_NAME_KEY, fThemes.getSelectedItem().toString());
    setChanged();
    notifyObservers();
  }

  @Override public void matchGuiToDefaultPreferences(){
    fShowToolBar.setSelected(SHOW_TOOLBAR_DEFAULT);
    fLargeIcons.setSelected(USE_LARGE_ICONS_DEFAULT);
    fThemes.setSelectedItem(Theme.valueOf(THEME_NAME_DEFAULT));
  }

  /**
  * Return the stored user preference for hiding or showing the toolbar.
  */
  public boolean hasShowToolBar(){
    return fPrefs.getBoolean(SHOW_TOOL_BAR_KEY, SHOW_TOOLBAR_DEFAULT);
  }
  
  /**
  * Return the stored user preference for using large icons.
  */
  public boolean hasLargeIcons(){
    return fPrefs.getBoolean(USE_LARGE_ICONS, USE_LARGE_ICONS_DEFAULT);
  }
  
  /**
  * Return the stored user preference for the theme to be applied to the Java 
  * look-and-feel.
  */
  public MetalTheme getTheme(){
    String themeName = fPrefs.get(THEME_NAME_KEY, THEME_NAME_DEFAULT);
    return Theme.valueOf(themeName);
  }
  
  // PRIVATE

  private static final String GENERAL_LOOK_NODE = "stocksmonitor/ui/prefs/GeneralLook";
  private Preferences fPrefs = Preferences.userRoot().node(GENERAL_LOOK_NODE);

  private static final String TITLE = "General Look";
  private static final int MNEMONIC = KeyEvent.VK_G;
  
  private static final boolean SHOW_TOOLBAR_DEFAULT = true;
  private static final String SHOW_TOOL_BAR_KEY = "ShowToolbar";

  private static final boolean USE_LARGE_ICONS_DEFAULT = false;
  private static final String USE_LARGE_ICONS = "UseLargeIcons";

  //Theme name is mapped to Theme using Theme.valueOf
  private static final String THEME_NAME_DEFAULT = "Default";
  private static final String THEME_NAME_KEY = "ThemeName";

  private JCheckBox fShowToolBar;
  private JCheckBox fLargeIcons;
  private JComboBox<MetalTheme> fThemes;
  
  private static final Logger fLogger = Util.getLogger(GeneralLookPreferencesEditor.class);
  
  private void matchGuiToStoredPrefs(){
    fShowToolBar.setSelected(hasShowToolBar());
    fLargeIcons.setSelected(hasLargeIcons());
    fThemes.setSelectedItem(getTheme());
  }
  
  private void addShowToolbarAndLargeIcons(JPanel aContent){
    JLabel toolbar = new JLabel("Show:");
    aContent.add(toolbar, getConstraints(0,0));
    
    fShowToolBar = new JCheckBox("Toolbar");
    fShowToolBar.setMnemonic(KeyEvent.VK_O);
    aContent.add(fShowToolBar, getConstraints(0,1));
    
    JLabel iconSize = new JLabel("Icon Size:");
    aContent.add(iconSize, getConstraints(1,0));

    fLargeIcons = new JCheckBox("Use Large Icons");
    fLargeIcons.setMnemonic(KeyEvent.VK_U);
    iconSize.setLabelFor(fLargeIcons);
    aContent.add(fLargeIcons, getConstraints(1,1));
  }

  private void addTheme(JPanel aContent) {
    JLabel theme = new JLabel("Theme:");
    theme.setDisplayedMnemonic(KeyEvent.VK_T);
    aContent.add(theme, getConstraints(2,0));
    
    DefaultComboBoxModel<MetalTheme> themesModel = new DefaultComboBoxModel<MetalTheme>(
      Theme.VALUES.toArray(new MetalTheme[0])
    );
    fThemes = new JComboBox<MetalTheme>(themesModel);
    theme.setLabelFor(fThemes);
    aContent.add(fThemes, getConstraints(2,1));
  }

  private void addRestoreDefaults(JPanel aContent) {
    JButton restore = new JButton("Restore Defaults");
    restore.setMnemonic(KeyEvent.VK_D);
    restore.addActionListener( new ActionListener() {
      @Override public void actionPerformed(ActionEvent event) {
        matchGuiToDefaultPreferences();
      }
    });
    GridBagConstraints constraints = UiUtil.getConstraints(3,1);
    constraints.insets = new Insets(UiConsts.ONE_SPACE, 0,0,0);
    aContent.add( restore, constraints );
  }
  
  private GridBagConstraints getConstraints(int aY, int aX){
    GridBagConstraints result = UiUtil.getConstraints(aY, aX);
    result.insets = new Insets(0, 0, UiConsts.ONE_SPACE, UiConsts.ONE_SPACE);
    return result;
  }
}
 

See Also :
Standardized dialogs
Using preferences