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 is an example implementation for the preferences shown above. Note the addition of these methods which allow programmatic, read-only access to stored preference values (and not the preference values as 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.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
import java.util.logging.*;
import java.util.prefs.*;

import hirondelle.stocks.table.QuoteField;
import hirondelle.stocks.util.Util;
import hirondelle.stocks.util.ui.UiConsts;
import hirondelle.stocks.util.ui.UiUtil;

/**
* Graphical component allows editing of user preferences related to the 
* {@link hirondelle.stocks.table.QuoteTable},
* and programmatic read-only access to these preferences.
*/
public final class QuoteTablePreferencesEditor extends Observable 
  implements PreferencesEditor {

  public JComponent getUI() {
    JPanel content = new JPanel();
    GridBagLayout gridbag = new GridBagLayout();
    content.setLayout(gridbag);
    addSortField(content);
    addHorizontalVerticalLines(content);
    addRowHeight(content);
    addUpdateFrequency(content);
    addColumnOrder(content);
    addRestore(content);
    matchGuiToStoredPrefs();
    return content;
  }

  public String getTitle() {
    return TITLE;
  }

  public int getMnemonic() {
    return MNEMONIC;
  }

  public void savePreferences() {
    fLogger.fine("Updating table preferences.");
    fPrefs.putBoolean(HORIZONTAL_LINES_KEY, fHorizontalLines.isSelected());
    fPrefs.putBoolean(VERTICAL_LINES_KEY, fVerticalLines.isSelected());
    fPrefs.putInt(ROW_HEIGHT_KEY, fRowSizeModel.getNumber().intValue());
    fPrefs.put(SORT_FIELD_KEY, fSortField.getSelectedItem().toString());
    fPrefs.put(COLUMN_ORDER_KEY, fColumnOrderEditor.getItems().toString());
    fPrefs.putInt(UPDATE_FREQ_KEY, fUpdateFreqModel.getNumber().intValue());
    setChanged();
    notifyObservers();
  }

  public void matchGuiToDefaultPreferences() {
    fHorizontalLines.setSelected(HORIZONTAL_LINES_DEFAULT);
    fVerticalLines.setSelected(VERTICAL_LINES_DEFAULT);
    fRowSizeModel.setValue(new Integer(ROW_HEIGHT_DEFAULT));
    fSortField.setSelectedItem(QuoteField.valueFrom(SORT_FIELD_DEFAULT));
    fColumnOrderEditor.setItems(parseRawColumnOrder(COLUMN_ORDER_DEFAULT));
    fUpdateFreqModel.setValue(new Integer(UPDATE_FREQ_DEFAULT));
  }

  /**
  * Return preference for the display of horizontal lines for each row.
  */
  public boolean hasHorizontalLines() {
    return fPrefs.getBoolean(HORIZONTAL_LINES_KEY, HORIZONTAL_LINES_DEFAULT);
  }

  /**
  * Return preference for the display of vertical lines for each column.
  */
  public boolean hasVerticalLines() {
    return fPrefs.getBoolean(VERTICAL_LINES_KEY, VERTICAL_LINES_DEFAULT);
  }

  /**
   * Return the height of each row in pixels, in the range <tt>16..32</tt>
   * (inclusive).
   */
  public int getRowHeight() {
    return fPrefs.getInt(ROW_HEIGHT_KEY, ROW_HEIGHT_DEFAULT);
  }

  /**
   * Return a field identifier, but no ascending-descending indicator.
   */
  public QuoteField getSortField() {
    String fieldName = fPrefs.get(SORT_FIELD_KEY, SORT_FIELD_DEFAULT);
    return QuoteField.valueFrom(fieldName);
  }

  /**
   * Return <tt>Set</tt> of {@link QuoteField} objects, whose
   * iteration order reflects the user's preferred column order.
   */
  public Set<Object> getColumnOrder() {
    return parseRawColumnOrder(fPrefs.get(COLUMN_ORDER_KEY, COLUMN_ORDER_DEFAULT));
  }

  /**
   * Return the number of minutes to wait between periodic updates, in the range
   * <tt>1..60</tt>.
   */
  public int getUpdateFrequency() {
    return fPrefs.getInt(UPDATE_FREQ_KEY, UPDATE_FREQ_DEFAULT);
  }

  // PRIVATE //
  private static final String TITLE = "Quote Table";
  private static final int MNEMONIC = KeyEvent.VK_Q;
  private static final String STOCKS_TABLE_NODE_NAME = 
    "stocksmonitor/ui/prefs/StocksTable"
  ;
  private static final boolean HORIZONTAL_LINES_DEFAULT = true;
  private static final String HORIZONTAL_LINES_KEY = "HorizontalLines";
  private static final boolean VERTICAL_LINES_DEFAULT = true;
  private static final String VERTICAL_LINES_KEY = "VerticalLines";
  private static final int MAX_ROW_HEIGHT = 32;
  private static final int MIN_ROW_HEIGHT = 16;
  private static final int INITIAL_ROW_HEIGHT = MIN_ROW_HEIGHT;
  private static final int STEP_SIZE = 1;
  private static final int ROW_HEIGHT_DEFAULT = MIN_ROW_HEIGHT;
  private static final String ROW_HEIGHT_KEY = "RowHeight";
  private static final String SORT_FIELD_DEFAULT = "Stock";
  private static final String SORT_FIELD_KEY = "SortBy";
  // This preference is unusual in that in needs a bit of parsing
  private static final String COLUMN_ORDER_DEFAULT = 
    "[Stock, Price, Change, %Change, Profit, %Profit]"
  ;
  private static final String COLUMN_ORDER_KEY = "ColumnOrder";
  private static final int MAX_UPDATE_FREQ = 60;
  private static final int MIN_UPDATE_FREQ = 1;
  private static final int UPDATE_FREQ_DEFAULT = 1; // seconds
  private static final String UPDATE_FREQ_KEY = "UpdateFrequency";

  private Preferences fPrefs = Preferences.userRoot().node(STOCKS_TABLE_NODE_NAME);
  private JCheckBox fHorizontalLines;
  private JCheckBox fVerticalLines;
  private SpinnerNumberModel fRowSizeModel;
  private JComboBox fSortField;
  private OrderEditor fColumnOrderEditor;
  private SpinnerNumberModel fUpdateFreqModel;

  private static final Logger fLogger = Util.getLogger(QuoteTablePreferencesEditor.class);

  private void addSortField(JPanel aContent) {
    JLabel sortBy = new JLabel("Sort By:");
    sortBy.setDisplayedMnemonic(KeyEvent.VK_S);
    sortBy.setToolTipText("Always descending order");
    aContent.add(sortBy, getConstraints(0, 0));

    DefaultComboBoxModel sortByModel = new DefaultComboBoxModel(QuoteField.values());
    fSortField = new JComboBox(sortByModel);
    sortBy.setLabelFor(fSortField);
    aContent.add(fSortField, getConstraints(0, 1));
  }

  private void addHorizontalVerticalLines(JPanel aContent) {
    JLabel show = new JLabel("Show:");
    aContent.add(show, getConstraints(1, 0));

    fHorizontalLines = new JCheckBox("Horizontal Lines");
    fHorizontalLines.setMnemonic(KeyEvent.VK_H);
    aContent.add(fHorizontalLines, getConstraints(1, 1));

    fVerticalLines = new JCheckBox("Vertical Lines");
    fVerticalLines.setMnemonic(KeyEvent.VK_V);
    aContent.add(fVerticalLines, getConstraints(1, 2, 2, 1));
  }

  private void addRowHeight(JPanel aContent) {
    JLabel rowHeight = new JLabel("Row Height:");
    rowHeight.setDisplayedMnemonic(KeyEvent.VK_R);
    rowHeight.setToolTipText("Height in pixels of table rows");
    aContent.add(rowHeight, getConstraints(2, 0));
    fRowSizeModel = new SpinnerNumberModel(
      INITIAL_ROW_HEIGHT, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT, STEP_SIZE
    );
    JSpinner rowHeightSelector = new JSpinner(fRowSizeModel);
    rowHeight.setLabelFor(rowHeightSelector);
    aContent.add(rowHeightSelector, getConstraints(2, 1));
  }

  private void addUpdateFrequency(JPanel aContent) {
    JLabel updateFreq = new JLabel("Update Freq:");
    updateFreq.setDisplayedMnemonic(KeyEvent.VK_U);
    updateFreq.setToolTipText("Refresh interval in minutes");
    aContent.add(updateFreq, getConstraints(2, 2));

    int initialFreqValue = MIN_UPDATE_FREQ;
    fUpdateFreqModel = new SpinnerNumberModel(
      initialFreqValue, MIN_UPDATE_FREQ, MAX_UPDATE_FREQ, 1
    );
    JSpinner updateFreqSelector = new JSpinner(fUpdateFreqModel);
    updateFreq.setLabelFor(updateFreqSelector);
    aContent.add(updateFreqSelector, getConstraints(2, 3));
  }

  private void addColumnOrder(JPanel aContent) {
    JLabel columnOrder = new JLabel("Column Order:");
    columnOrder.setDisplayedMnemonic(KeyEvent.VK_C);
    GridBagConstraints columnOrderConstraints = getConstraints(3, 0);
    columnOrderConstraints.anchor = GridBagConstraints.NORTH;
    aContent.add(columnOrder, columnOrderConstraints);

    fColumnOrderEditor = new OrderEditor("Up", "Down");
    // Demonstrates alternate ctor using icons:
    // fColumnOrderEditor = new OrderEditor(
    // UiUtil.getImageIcon("/toolbarButtonGraphics/navigation/Up"),
    // UiUtil.getImageIcon("/toolbarButtonGraphics/navigation/Down"),
    // UiConsts.ONE_SPACE * 15 );
    columnOrder.setLabelFor(fColumnOrderEditor);
    GridBagConstraints orderEditorConstraints = UiUtil.getConstraints(3, 1);
    orderEditorConstraints.insets = new Insets(UiConsts.ONE_SPACE, 0, 0, 0);
    aContent.add(fColumnOrderEditor, orderEditorConstraints);
  }

  private void addRestore(JPanel aContent) {
    JButton restore = new JButton("Restore Defaults");
    restore.setMnemonic(KeyEvent.VK_D);
    restore.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent event) {
        matchGuiToDefaultPreferences();
      }
    });
    GridBagConstraints constraints = UiUtil.getConstraints(3, 2, 2, 1);
    constraints.anchor = GridBagConstraints.SOUTH;
    constraints.insets = new Insets(0, 0, 0, 0);
    aContent.add(restore, constraints);
  }

  /**
   * Return a <tt>Set</tt> of {@link QuoteField} objects, whose
   * iteration order corresponds to the preferred column order.
   */
  private Set<Object> parseRawColumnOrder(String aRawColumnOrderPref) {
    java.util.List<String> columnNames = Util.getListFromString(aRawColumnOrderPref);
    Set<Object> result = new LinkedHashSet<Object>();
    for(String fieldName: columnNames){
      result.add(QuoteField.valueFrom(fieldName));
    }
    assert (result.size() == QuoteField.values().length);
    return result;
  }

  private void matchGuiToStoredPrefs() {
    fHorizontalLines.setSelected(hasHorizontalLines());
    fVerticalLines.setSelected(hasVerticalLines());
    fRowSizeModel.setValue(new Integer(getRowHeight()));
    fSortField.setSelectedItem(getSortField());
    fColumnOrderEditor.setItems(getColumnOrder());
    fUpdateFreqModel.setValue(new Integer(getUpdateFrequency()));
  }

  private GridBagConstraints getConstraints(int aY, int aX) {
    GridBagConstraints result = UiUtil.getConstraints(aY, aX);
    addBottom(result);
    return result;
  }

  private GridBagConstraints getConstraints(int aY, int aX, int aWidth, int aHeight) {
    GridBagConstraints result = UiUtil.getConstraints(aY, aX, aWidth, aHeight);
    addBottom(result);
    return result;
  }

  private void addBottom(GridBagConstraints aConstraints) {
    aConstraints.insets = new Insets(0, 0, UiConsts.ONE_SPACE, UiConsts.ONE_SPACE);
  }

  /**
   * Developer tool for removing old preferences and restarting from the beginning.
   */
  private static void main(String... aArgs) {
    QuoteTablePreferencesEditor thing = new QuoteTablePreferencesEditor();
    try {
      thing.fPrefs.removeNode();
    }
    catch (BackingStoreException ex) {
      fLogger.severe("Cannot access preferences.");
    }
    fLogger.severe("Done.");
  }
}
 


Here is a high level Action class which displays a preferences dialog. It uses a List of PreferencesEditor objects passed to the constructor.

package hirondelle.stocks.preferences;

import java.awt.event.*;
import javax.swing.*;
import java.util.*;
import java.util.logging.*;

import hirondelle.stocks.util.Args;
import hirondelle.stocks.util.ui.StandardEditor;
import hirondelle.stocks.util.ui.UiUtil;
import hirondelle.stocks.preferences.PreferencesEditor;
import hirondelle.stocks.util.Util;

/**
* Present dialog to allow update of user preferences.
*
* <P>Related preferences are grouped together and placed in 
* a single pane of a <tt>JTabbedPane</tt>, which corresponds to an 
* implementation of {@link PreferencesEditor}. Values are pre-populated with 
* current values for preferences.
*
*<P>Most preferences have default values. If so, a  
* <tt>Restore Defaults</tt> button is provided for that set of related 
* preferences.
*
*<P>Preferences are not changed until the <tt>OK</tt> button is pressed. 
* Exception: the logging preferences take effect immediately, without the need 
* for hitting <tt>OK</tt>.
*/
public final class EditUserPreferencesAction extends AbstractAction {

  /**
  * Constructor.
  *  
  * @param aFrame parent window to which this dialog is attached.
  * @param aPrefEditors contains implementations of {@link PreferencesEditor}, 
  * each of which is placed in a pane of a <tt>JTabbedPane</tt>.
  */
  public EditUserPreferencesAction (JFrame aFrame, List<PreferencesEditor> aPrefEditors) {
    super("Preferences...", UiUtil.getEmptyIcon()); 
    Args.checkForNull(aFrame);
    Args.checkForNull(aPrefEditors);
    fFrame = aFrame;
    putValue(SHORT_DESCRIPTION, "Update user preferences");
    putValue(LONG_DESCRIPTION, "Allows user input of preferences.");
    putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_P) );    
    fPrefEditors = aPrefEditors;
  }

  /** Display the user preferences dialog.  */
  public void actionPerformed(ActionEvent event) {
    fLogger.info("Showing user preferences dialog.");
    //lazy construction: fEditor is created only once, when this action 
    //is explicitly invoked
    if ( fEditor == null ) {
      fEditor = new Editor("Edit Preferences", fFrame);
    }
    fEditor.showDialog();
  }
  
  // PRIVATE //
  private JFrame fFrame;
  private java.util.List<PreferencesEditor> fPrefEditors;
  private static final Logger fLogger = Util.getLogger(EditUserPreferencesAction.class);  
  
  /**
  * Specifying this as a field allows for "lazy" creation and use of the GUI, which is 
  * of particular importance for a preferences dialog, since they are usually heavyweight, 
  * and have a large number of components.
  */
  private Editor fEditor;
  
  /**
  * Return GUI for editing all preferences, pre-populated with current 
  * values.
  */
  private JComponent getPrefEditors(){
    JTabbedPane content = new JTabbedPane();
    content.setTabPlacement(JTabbedPane.LEFT);
    int idx = 0;
    for(PreferencesEditor prefEditor: fPrefEditors) {
      JComponent editorGui = prefEditor.getUI();
      editorGui.setBorder(UiUtil.getStandardBorder());
      content.addTab(prefEditor.getTitle() , editorGui);
      content.setMnemonicAt(idx, prefEditor.getMnemonic());
      ++idx;
    }
    return content;
  }
  
  /** Called only when the user hits the OK button.  */
  private void saveSettings(){
    fLogger.fine("User selected OK. Updating table preferences.");
    for(PreferencesEditor prefEditor: fPrefEditors) {
      prefEditor.savePreferences();
    }
  }
  
  /**
  * An example of a nested class which is nested because it is attached only 
  * to the enclosing class, and it cannot act as superclass since multiple 
  * inheritance of implementation is not possible. 
  * 
  * The implementation of this nested class is kept short by calling methods 
  * of the enclosing class.
  */
  private final class Editor extends StandardEditor { 
    
    Editor(String aTitle, JFrame aParent){
      super(aTitle, aParent, StandardEditor.CloseAction.HIDE);
    }

    public JComponent getEditorUI () {
      JPanel content = new JPanel();
      content.setLayout( new BoxLayout(content, BoxLayout.Y_AXIS) );
      content.add( getPrefEditors() );
      //content.setMinimumSize(new Dimension(300,300) );
      return content;
    }
    
    public void okAction() {
      saveSettings();
      dispose();
    }
  }  
}
 



See Also :
Standardized dialogs
Using preferences
Would you use this technique?
Yes   No   Undecided   
© 2010 Hirondelle Systems | Source Code | Contact | License | Quotes | RSS
Individual code snippets can be used under this BSD license - Last updated on June 5, 2010.
Over 150,000 unique IPs last month - Built with WEB4J.
- In Memoriam : Bill Dirani -