package usr.erichschroeter.jpreferences;
import java.awt.BorderLayout;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import usr.erichschroeter.jpreferences.page.CustomPage;
import usr.erichschroeter.jpreferences.page.Page;
import usr.erichschroeter.jpreferences.page.PreferencePage;
/**
* A <code>PreferenceDialog</code> provides a graphical interface for users to
* interact with preferences. The default behavior will display
* {@link Preferences} nodes passed to the constructors.
*
* @author Erich Schroeter, help from
* http://www.roseindia.net/javatutorials/javaapi.shtml
*/
@SuppressWarnings("serial")
public class PreferenceDialog extends JDialog {
/** The preference node tree the user interacts with to view preferences. */
private JTree tree;
/** The table displaying the preferences and their values. */
private CustomPage<?> page;
/**
* A reference to the page that displays a table of {@link Preferences}.
* This is kept as a single reference, as opposed to being added to
* {@link #customNodeMap}, for performance.
*/
private PreferencePage preferencePage;
/** The root preference nodes to be displayed in the {@link #tree}. */
private Preferences[] preferences;
/** The list of custom preference pages. */
private Map<MutableTreeNode, CustomPage<?>> customNodeMap;
/** A mapping of the custom pages to their respective node. */
private Map<CustomPage<?>, MutableTreeNode> customPageMap;
/**
* The left is the {@link #tree}, right is the {@link #page} or
* {@link #preferencePage}.
*/
private JSplitPane splitPane;
/** Whether the search feature is enabled or disabled. */
private boolean searchEnabled;
/** Whether custom pages are allowed to be added. */
private boolean customPagesEnabled;
/** Whether the escape key is bound to close the dialog. */
private boolean escapeToCloseEnabled;
/** Whether the page will be wrapped in a <code>JScrollPane</code>. */
private boolean wrapPageInScrollPaneEnabled;
/** The action that handles closing the dialog. */
private AbstractAction closeAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setVisible(false);
dispose();
}
};
/** A blank page to fall back on. */
class BlankPage extends CustomPage<JPanel> {
public BlankPage() {
super("");
}
@Override
protected void initializePage(JPanel page) {
// keep blank
}
}
// TODO implement search for preferences and values
/**
* Creates a <code>PreferenceDialog</code> calling
* {@link JDialog#JDialog(Dialog, boolean)}.
*
* @param parent
* the <code>Dialog</code> from which the dialog is displayed or
* <code>null</code> if this dialog has no owner
* @param preferences
* the root preference nodes
*/
public PreferenceDialog(Dialog parent, Preferences... preferences) {
super(parent, true);
setPreferences(preferences);
initializeDialog();
}
/**
* Creates a <code>PreferenceDialog</code> calling
* {@link JDialog#JDialog(Window, ModalityType)}.
*
* @param parent
* the <code>Window</code> from which the dialog is displayed or
* <code>null</code> if this dialog has no owner
* @param preferences
* the root preference nodes
*/
public PreferenceDialog(Window parent, Preferences... preferences) {
super(parent, ModalityType.DOCUMENT_MODAL);
setPreferences(preferences);
initializeDialog();
}
/** Initializes the User Interface (UI) for this dialog. */
protected void initializeDialog() {
try {
setIconImage(ImageIO
.read(PreferenceDialog.class
.getClassLoader()
.getResourceAsStream(
"usr/erichschroeter/jpreferences/png/preferences.png")));
} catch (IOException e) {
// let system use default image
}
setLayout(new BorderLayout());
setTitle("Preferences Dialog");
setMinimumSize(new Dimension(400, 400));
customNodeMap = new HashMap<MutableTreeNode, CustomPage<?>>();
customPageMap = new HashMap<CustomPage<?>, MutableTreeNode>();
preferencePage = new PreferencePage(preferences[0]);
setEscapeToCloseEnabled(true);
setSearchEnabled(false);
setCustomPagesEnabled(false);
setWrapPageInScrollPaneEnabled(true);
// a default page
page = new BlankPage();
//
// TreePanel -- panel containing the tree hierarchy
//
JPanel treePanel = new JPanel(new BorderLayout());
if (isSearchEnabled()) {
JTextField searchField = new JTextField("Search...");
treePanel.add(searchField, BorderLayout.NORTH);
}
DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
renderer.setOpenIcon(null);
renderer.setLeafIcon(null);
renderer.setClosedIcon(null);
tree = createTree(true, false, getPreferences());
tree.setCellRenderer(renderer);
treePanel.add(new JScrollPane(tree), BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton closeButton = new JButton("Close");
closeButton.addActionListener(closeAction);
buttonPanel.add(closeButton);
splitPane = new JSplitPane();
splitPane.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
splitPane.setResizeWeight(0.3); // tree doesn't need more than 30%
splitPane.setLeftComponent(treePanel);
splitPane.setRightComponent(new JScrollPane(page.getPage()));
add(splitPane, BorderLayout.CENTER);
add(buttonPanel, BorderLayout.SOUTH);
setLocationRelativeTo(getParent());
pack();
}
/**
* Returns whether the search feature is enabled or disabled. This feature
* displays a search box and filters the preference nodes based on the
* search result.
*
* @see #setSearchEnabled(boolean)
* @return <code>true</code> if the feature is enabled, else
* <code>false</code>
*/
public boolean isSearchEnabled() {
return searchEnabled;
}
/**
* Enables or disables the search feature. This feature displays a search
* box and filters the preference nodes based on the search result.
*
* @see #isSearchEnabled()
* @param enable
* <code>true</code> to enable the feature, <code>false</code> to
* disable
*/
public void setSearchEnabled(boolean enable) {
searchEnabled = enable;
}
/**
* Returns whether the custom pages feature is enabled or disabled. This
* feature enables/disables the ability to add custom preference pages.
*
* @see #setCustomPagesEnabled(boolean)
* @return <code>true</code> if the feature is enabled, else
* <code>false</code>
*/
public boolean isCustomPagesEnabled() {
return customPagesEnabled;
}
/**
* Enables or disables the custom pages feature. This feature
* enables/disables the ability to add custom preference pages.
*
* @see #isCustomPagesEnabled()
* @param enable
* <code>true</code> to enable the feature, <code>false</code> to
* disable
*/
public void setCustomPagesEnabled(boolean enable) {
this.customPagesEnabled = enable;
}
/**
* Returns whether the escape key to close feature is enabled or disabled.
* This feature enables/disables the ability to close the dialog with the
* escape key.
*
* @return <code>true</code> if the feature is enabled, else
* <code>false</code>
*/
public boolean isEscapeToCloseEnabled() {
return escapeToCloseEnabled;
}
/**
* Enables or disables the escape key to close feature. This feature
* enables/disables the ability to close the dialog with the escape key.
*
* @param enable
* <code>true</code> to enable the feature, <code>false</code> to
* disable
*/
public void setEscapeToCloseEnabled(boolean enable) {
this.escapeToCloseEnabled = enable;
KeyStroke escapeStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
if (enable) {
// bind Escape key to close the dialog
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
escapeStroke, escapeStroke.toString());
getRootPane().getActionMap().put(escapeStroke.toString(),
closeAction);
} else {
// unbind Escape key from closing the dialog
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.remove(escapeStroke);
getRootPane().getActionMap().remove(escapeStroke.toString());
}
}
/**
* Returns whether the custom page will be wrapped in a
* <code>JScrollPane</code> feature is enabled or disabled. This feature
* will wrap the selected page in the right component of the
* {@link #splitPane} in a <code>JScrollPane</code> if enabled, or add the
* custom page component directly.
*
* @return <code>true</code> if the feature is enabled, else
* <code>false</code>
*/
public boolean isWrapPageInScrollPaneEnabled() {
return wrapPageInScrollPaneEnabled;
}
/**
* Enables or disables the custom page wrapped in a <code>JScrollPane</code>
* feature. This feature will wrap the selected page in the right component
* of the {@link #splitPane} in a <code>JScrollPane</code> if enabled, or
* add the custom page component directly.
*
* @param enable
* <code>true</code> to enable the feature, <code>false</code> to
* disable
*/
public void setWrapPageInScrollPaneEnabled(boolean enable) {
this.wrapPageInScrollPaneEnabled = enable;
}
protected MutableTreeNode addNodeFor(CustomPage<?> page) {
return addNode(new CustomPageTreeNode(page));
}
protected MutableTreeNode addNodeFor(Page page) {
return addNode(new PageTreeNode(page));
}
protected MutableTreeNode addNode(MutableTreeNode node) {
DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
model.insertNodeInto(node, root, root.getChildCount());
return node;
}
protected void removeNodeFor(CustomPage<?> page) {
removeNode(customPageMap.get(page));
}
protected void removeNode(MutableTreeNode node) {
DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
model.removeNodeFromParent(node);
}
/**
* Adds the specified preference page to the preference dialog. A new
* preference node is created in the preference tree. When selected, the
* <code>page</code>'s UI component is displayed.
* <p>
* If the custom pages feature is disabled, this method does nothing.
*
* @param page
* the preference page
* @return <code>true</code> if <code>page</code> was added, else
* <code>false</code>
*/
public boolean add(CustomPage<?> page) {
boolean added = false;
if (isCustomPagesEnabled()) {
MutableTreeNode node = addNodeFor(page);
customNodeMap.put(node, page);
customPageMap.put(page, node);
added = true;
}
return added;
}
/**
* Removes the specified preference page from the preference dialog.
*
* @param page
* the preference page
*/
public void remove(CustomPage<?> page) {
// TODO handle falling back to last selected node
removeNodeFor(page);
customNodeMap.remove(page);
customPageMap.remove(page);
}
/**
* Sets the page to display to the user. This sets the right component of
* the {@link #splitPane} to <code>page</code>.
* <p>
* <em><b>Note:</b> this performs differently based on the result of
* {@link #isWrapPageInScrollPaneEnabled()}.</em>
*
* @see #isWrapPageInScrollPaneEnabled()
* @see #setWrapPageInScrollPaneEnabled(boolean)
* @param page
* the page to display
*/
public void setPage(CustomPage<?> page) {
this.page = page;
if (isWrapPageInScrollPaneEnabled()) {
splitPane.setRightComponent(new JScrollPane(this.page.getPage()));
} else {
splitPane.setRightComponent(this.page.getPage());
}
}
/**
* Returns the root preference nodes to be displayed in the tree.
*
* @return the root preferences nodes
*/
public Preferences[] getPreferences() {
return preferences;
}
/**
* Sets the root preference nodes to be displayed in the tree.
*
* @param preferences
* the root preferences nodes
*/
public void setPreferences(Preferences... preferences) {
this.preferences = preferences;
}
/**
* Creates and returns a <code>JTree</code> containing tree nodes for each
* <code>Preferences</code> specified.
*
* @param userPreferences
* <code>true</code> to add the nodes for user preferences, or
* <code>false</code> not to
* @param systemPreferences
* <code>true</code> to add the nodes for system preferences, or
* <code>false</code> not to
* @param preferences
* the objects containing preferences to add to the tree
* @see #createTreeSelectionListener()
* @return the preference tree created
*/
protected JTree createTree(boolean userPreferences,
boolean systemPreferences, Preferences... preferences) {
DefaultMutableTreeNode root = new DefaultMutableTreeNode("Preferences");
for (Preferences node : preferences) {
if (userPreferences) {
root.add(createUserRootNode(node));
}
if (systemPreferences) {
root.add(createSystemRootNode(node));
}
}
DefaultTreeModel model = new DefaultTreeModel(root);
JTree tree = new JTree(model);
tree.addTreeSelectionListener(createTreeSelectionListener());
return tree;
}
/**
* Returns the tree listener that listens to the node selected in
* {@link #tree} and handles the page to be displayed.
*
* @see #createTree(boolean, boolean, Preferences...)
* @return the tree selection listener
*/
protected TreeSelectionListener createTreeSelectionListener() {
return new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
Object node = e.getPath().getLastPathComponent();
if (node instanceof PreferenceTreeNode) {
// PreferenceTreeNode node = (PreferenceTreeNode)
// e.getPath().getLastPathComponent();
Preferences pref = ((PreferenceTreeNode) node)
.getPrefObject();
// editTable.setModel(new PreferenceTableModel(pref));
preferencePage.setModel(new PreferenceTableModel(pref));
preferencePage.setPageTitle(pref.name());
setPage(preferencePage);
} else if (node instanceof CustomPageTreeNode) {
setPage(customNodeMap.get(node));
} else {
setPage(new BlankPage());
}
}
};
}
/**
* Creates and returns a <code>MutableTreeNode</code> for the
* <code>preference</code> in the system preferences space.
* <p>
* This is equivalent to <code>createRootNode(preference, false)</code>
*
* @param preference
* the preferences object
* @return a new tree node based on the specified <code>preference</code>
*/
protected MutableTreeNode createSystemRootNode(Preferences preference) {
return createRootNode(preference, false);
}
/**
* Creates and returns a <code>MutableTreeNode</code> for the
* <code>preference</code> in the user preferences space.
* <p>
* This is equivalent to <code>createRootNode(preference, true/code>
*
* @param preference
* the preferences object
* @return a new tree node based on the specified <code>preference</code>
*/
protected MutableTreeNode createUserRootNode(Preferences preference) {
return createRootNode(preference, true);
}
/**
* Creates and returns a <code>MutableTreeNode</code> for the
* <code>preference</code> in the user preferences space.
*
* @param preference
* the preferences object
* @param defaultUser
* <code>true</code> to fall back to
* <code>Preferences.userRoot()</code> or <code>false</code> to
* fall back to <code>Preferences.systemRoot()</code> if
* <code>pref</code> is <code>null</code>
* @return a new tree node based on the specified <code>obj</code>
*/
protected MutableTreeNode createRootNode(Preferences preference,
boolean defaultUser) {
MutableTreeNode node = null;
try {
if (preference == null) {
node = new PreferenceTreeNode(
defaultUser ? Preferences.userRoot()
: Preferences.systemRoot());
} else {
node = new PreferenceTreeNode(preference);
}
} catch (BackingStoreException e) {
e.printStackTrace();
node = new DefaultMutableTreeNode(
defaultUser ? "No User Preferences!"
: "No System Preferences!");
}
return node;
}
}