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 :
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) :
- hasHorizontalLines
- hasVerticalLines
- getRowHeight
- getSortBy
- getColumnOrder
- getUpdateFrequency
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 :
Would you use this technique?
|
|