/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.tools.workbench.test.uitools; import java.awt.BorderLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.text.Collator; import java.util.Comparator; import java.util.Enumeration; import java.util.Iterator; import java.util.prefs.BackingStoreException; import java.util.prefs.NodeChangeEvent; import java.util.prefs.NodeChangeListener; import java.util.prefs.Preferences; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.JTree; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.table.TableModel; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.MutableTreeNode; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.eclipse.persistence.tools.workbench.uitools.app.CollectionValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.ListValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.PropertyValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.SimplePropertyValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.SortedListValueModelAdapter; import org.eclipse.persistence.tools.workbench.uitools.app.adapters.PreferencePropertyValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.adapters.PreferencesCollectionValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.swing.ColumnAdapter; import org.eclipse.persistence.tools.workbench.uitools.app.swing.ListModelAdapter; import org.eclipse.persistence.tools.workbench.uitools.app.swing.ObjectListSelectionModel; import org.eclipse.persistence.tools.workbench.uitools.app.swing.TableModelAdapter; import org.eclipse.persistence.tools.workbench.uitools.cell.SimpleTreeCellRenderer; import org.eclipse.persistence.tools.workbench.utility.CollectionTools; /** * A panel for displaying a tree of preferences. * On the left hand side is a tree of preferences nodes. * On the right hand side is a table of the preferences for * the currently selected node. The preferences values are * displayed and edited as simple strings (as opposed to * using the Preferences API that allows you to get and * set various other primitive types - e.g. int). */ // TODO add popup menu to tree public class PreferencesPanel extends JPanel { /** This is used to sort the nodes and preferences. */ Collator collator; /** This holds the preferences node currently selected in the tree. */ private PropertyValueModel selectedPreferencesHolder; private TreeSelectionModel treeSelectionModel; /** This holds the single preference currently selected in the table. */ private ListValueModel preferencesAdapter; private PropertyValueModel selectedPreferenceHolder; private TableModel tableModel; private ObjectListSelectionModel tableSelectionModel; private JPanel controlPanel; // ********** constructor/initialization ********** /** * Construct a panel to display the "system" * and "current user" preferences. */ public PreferencesPanel() { this(null); } /** * Construct a panel to display the specified preferences. * If the preferences is null, the panel will display the "system" * and "current user" preferences. */ public PreferencesPanel(Preferences preferences) { super(new BorderLayout()); this.initialize(preferences); } private void initialize(Preferences preferences) { // cache a collator for sorting nodes this.collator = Collator.getInstance(); this.selectedPreferencesHolder = new SimplePropertyValueModel(); this.selectedPreferenceHolder = new SimplePropertyValueModel(); this.add(this.buildSplitPane(preferences), BorderLayout.CENTER); this.add(this.buildControlPanel(), BorderLayout.SOUTH); } private JSplitPane buildSplitPane(Preferences preferences) { JSplitPane splitPane = new JSplitPane(); splitPane.setDoubleBuffered(true); splitPane.setLeftComponent(new JScrollPane(this.buildTree(preferences))); splitPane.setRightComponent(new JScrollPane(this.buildTable())); splitPane.setDividerLocation(150); return splitPane; } private JTree buildTree(Preferences preferences) { JTree tree = new JTree(this.buildTreeModel(preferences)); this.treeSelectionModel = tree.getSelectionModel(); this.treeSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); this.treeSelectionModel.addTreeSelectionListener(this.buildTreeSelectionListener()); tree.setCellRenderer(this.buildTreeCellRenderer()); tree.setRootVisible(false); tree.setShowsRootHandles(true); tree.setDoubleBuffered(true); return tree; } private TreeModel buildTreeModel(Preferences preferences) { DefaultTreeModel treeModel = new DefaultTreeModel(null, true); // true = asks allows children treeModel.setRoot(this.buildRootNode(preferences, treeModel)); return treeModel; } private TreeNode buildRootNode(Preferences preferences, DefaultTreeModel treeModel) { try { return this.buildRootNode2(preferences, treeModel); } catch (BackingStoreException ex) { this.handleException(ex); return null; } } private TreeNode buildRootNode2(Preferences preferences, DefaultTreeModel treeModel) throws BackingStoreException { if (preferences == null) { return new DefaultRootNode(treeModel); } return new PreferencesNode(treeModel, preferences); } private TreeSelectionListener buildTreeSelectionListener() { return new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { PreferencesPanel.this.treeSelectionChanged(); } }; } /** * All the nodes in the tree implement LocalNode. */ private TreeCellRenderer buildTreeCellRenderer() { return new SimpleTreeCellRenderer() { protected String buildText(Object value) { return ((LocalNode) value).displayString(); } }; } private JTable buildTable() { this.preferencesAdapter = this.buildSortedPreferencesAdapter(); this.tableModel = new TableModelAdapter(this.preferencesAdapter, this.buildColumnAdapter()); this.tableSelectionModel = this.buildTableSelectionModel(this.preferencesAdapter); JTable table = new JTable(this.tableModel); table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); // see Java bug 5007652 table.setSelectionModel(this.tableSelectionModel); table.setDoubleBuffered(true); table.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN); return table; } private ListValueModel buildSortedPreferencesAdapter() { return new SortedListValueModelAdapter(this.buildPreferencesAdapter(), this.buildPreferenceComparator()); } /** * Override setting the preference value to truncate the string to the * maximum allowed length. */ private CollectionValueModel buildPreferencesAdapter() { return new PreferencesCollectionValueModel(this.selectedPreferencesHolder) { protected PreferencePropertyValueModel buildModel(String key) { return new PreferencePropertyValueModel(this.subjectHolder, key) { protected String convertToString(Object o) { String string = super.convertToString(o); if (string == null) { return string; } if (string.length() > Preferences.MAX_VALUE_LENGTH) { string = string.substring(0, Preferences.MAX_VALUE_LENGTH); } return string; } }; } }; } private Comparator buildPreferenceComparator() { return new Comparator() { public int compare(Object o1, Object o2) { return PreferencesPanel.this.collator.compare(((PreferencePropertyValueModel) o1).getKey(), ((PreferencePropertyValueModel) o2).getKey()); } }; } private ColumnAdapter buildColumnAdapter() { return new PreferenceColumnAdapter(); } private ObjectListSelectionModel buildTableSelectionModel(ListValueModel preferencesHolder) { ObjectListSelectionModel tsm = new ObjectListSelectionModel(new ListModelAdapter(preferencesHolder)); tsm.addListSelectionListener(this.buildTableSelectionListener()); tsm.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); return tsm; } private ListSelectionListener buildTableSelectionListener() { return new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { return; } PreferencesPanel.this.tableSelectionChanged(e); } }; } void tableSelectionChanged(ListSelectionEvent e) { this.selectedPreferenceHolder.setValue(this.tableSelectionModel.getSelectedValue()); } private JPanel buildControlPanel() { this.controlPanel = new JPanel(new GridLayout(2, 4)); this.controlPanel.add(this.buildAddNodeButton()); this.controlPanel.add(this.buildRenameNodeButton()); this.controlPanel.add(this.buildRemoveNodeButton()); this.controlPanel.add(this.buildExportButton()); this.controlPanel.add(this.buildAddPreferenceButton()); this.controlPanel.add(this.buildRenamePreferenceButton()); this.controlPanel.add(this.buildRemovePreferenceButton()); return this.controlPanel; } private JButton buildAddNodeButton() { return new JButton(this.buildAddNodeAction()); } private JButton buildRenameNodeButton() { return new JButton(this.buildRenameNodeAction()); } private JButton buildRemoveNodeButton() { return new JButton(this.buildRemoveNodeAction()); } private JButton buildExportButton() { return new JButton(this.buildExportNodeAction()); } private JButton buildAddPreferenceButton() { return new JButton(this.buildAddPreferenceAction()); } private JButton buildRenamePreferenceButton() { return new JButton(this.buildRenamePreferenceAction()); } private JButton buildRemovePreferenceButton() { return new JButton(this.buildRemovePreferenceAction()); } // ********** public API ********** public void addAction(Action action) { this.controlPanel.add(new JButton(action)); } // ********** accessors ********** Collator getCollator() { return this.collator; } // ********** behavior ********** void treeSelectionChanged() { this.selectedPreferencesHolder.setValue(this.selectedPreferences()); } private void handleException(Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, ex.getMessage(), ex.getClass().getName(), JOptionPane.ERROR_MESSAGE); } private String promptForNodeName() { String name = JOptionPane.showInputDialog("Name"); if (name == null) { return null; } if (name.length() > Preferences.MAX_NAME_LENGTH) { name = name.substring(0, Preferences.MAX_NAME_LENGTH); } if (name.indexOf('/') != -1) { return null; } return name; } private String promptForPreferenceName() { String name = JOptionPane.showInputDialog("Name"); if (name == null) { return null; } if (name.length() > Preferences.MAX_KEY_LENGTH) { name = name.substring(0, Preferences.MAX_KEY_LENGTH); } return name; } // ********** convenience methods ********** private PreferencesNode selectedPreferencesNode() { if (this.treeSelectionModel.getSelectionCount() != 1) { return null; } // the DefaultRootNode should never be visible and selected // so we can safely cast to PreferencesNode return (PreferencesNode) this.treeSelectionModel.getSelectionPath().getLastPathComponent(); } private Preferences selectedPreferences() { PreferencesNode node = this.selectedPreferencesNode(); return (node == null) ? null : node.preferences(); } private PreferencePropertyValueModel selectedPreference() { return (PreferencePropertyValueModel) this.selectedPreferenceHolder.getValue(); } private PreferencePropertyValueModel preference(String key) { for (Iterator stream = (Iterator) this.preferencesAdapter.getValue(); stream.hasNext(); ) { PreferencePropertyValueModel next = (PreferencePropertyValueModel) stream.next(); if (next.getKey().equals(key)) { return next; } } throw new IllegalArgumentException(key); } // ********** add child node ********** private Action buildAddNodeAction() { return new AbstractAction("Add Child Node") { public void actionPerformed(ActionEvent event) { PreferencesPanel.this.addChildToSelectedNode(); } }; } void addChildToSelectedNode() { PreferencesNode selectedNode = this.selectedPreferencesNode(); if (selectedNode == null) { return; } String name = this.promptForNodeName(); if (name == null) { return; } Preferences preferences = selectedNode.preferences(); // this will cause the preferences to be created if it does not already exist Preferences newPreferences = preferences.node(name); try { // flush the change, so an event is fired and the node is // created and added to the tree before we try to select it preferences.flush(); } catch (BackingStoreException ex) { this.handleException(ex); return; // skip the selection of a node that might not be there... } PreferencesNode newNode = selectedNode.childNodeFor(newPreferences); this.treeSelectionModel.setSelectionPath(new TreePath(newNode.getPath())); } // ********** rename node ********** private Action buildRenameNodeAction() { return new AbstractAction("Rename Node") { public void actionPerformed(ActionEvent event) { PreferencesPanel.this.renameSelectedNode(); } }; } /** * we can't simply rename the node - we have to clone * it, then remove it... */ void renameSelectedNode() { PreferencesNode selectedNode = this.selectedPreferencesNode(); if (selectedNode == null || selectedNode.cannotBeRemoved()) { return; } String name = this.promptForNodeName(); if (name == null) { return; } Preferences oldPreferences = selectedNode.preferences(); PreferencesNode parentNode = (PreferencesNode) selectedNode.getParent(); Preferences parentPreferences = parentNode.preferences(); try { if (parentPreferences.nodeExists(name)) { // cannot rename to same name as existing node return; } } catch (BackingStoreException ex) { this.handleException(ex); return; } // clear the selection *before* removing the node, so we are no longer // listening to the node when it gets deleted - it will be in an invalid state... this.treeSelectionModel.clearSelection(); Preferences newPreferences = parentPreferences.node(name); try { this.clone(oldPreferences, newPreferences); oldPreferences.removeNode(); // flush the changes, so events are fired and the tree is updated parentPreferences.flush(); } catch (BackingStoreException ex) { this.handleException(ex); return; // skip the selection of a node that might not be there... } PreferencesNode newNode = parentNode.childNodeFor(newPreferences); this.treeSelectionModel.setSelectionPath(new TreePath(newNode.getPath())); } private void clone(Preferences source, Preferences target) throws BackingStoreException { String[] names = source.childrenNames(); int len = names.length; for (int i = 0; i < len; i++) { String name = names[i]; this.clone(source.node(name), target.node(name)); } names = source.keys(); len = names.length; for (int i = 0; i < len; i++) { String name = names[i]; target.put(name, source.get(name, "")); } } // ********** remove node ********** private Action buildRemoveNodeAction() { return new AbstractAction("Remove Node") { public void actionPerformed(ActionEvent event) { PreferencesPanel.this.removeSelectedNode(); } }; } void removeSelectedNode() { PreferencesNode selectedNode = this.selectedPreferencesNode(); if (selectedNode == null || selectedNode.cannotBeRemoved()) { return; } TreePath postRemoveSelectionPath = this.calculatePostRemoveSelectionPath(selectedNode); // clear the selection *before* removing the node, so we are no longer // listening to the node when it gets deleted - it will be in an invalid state... this.treeSelectionModel.clearSelection(); Preferences preferences = selectedNode.preferences(); try { preferences.removeNode(); // flush the change, so an event is fired and the parent node is updated preferences.flush(); } catch (BackingStoreException ex) { this.handleException(ex); return; // skip the selection of the parent node... } this.treeSelectionModel.setSelectionPath(postRemoveSelectionPath); } /** * Calculate what the selection path should be once the * specified node has been removed from the tree. */ private TreePath calculatePostRemoveSelectionPath(PreferencesNode node) { PreferencesNode parentNode = (PreferencesNode) node.getParent(); int childCount = parentNode.getChildCount(); if (childCount == 1) { // the only child is to be removed - select the parent return new TreePath(parentNode.getPath()); } int nodeIndex = parentNode.getIndex(node); if (nodeIndex == childCount - 1) { // the last child in the list is to be removed - select the previous child return new TreePath(((PreferencesNode) parentNode.getChildAt(nodeIndex - 1)).getPath()); } // by default, select the next child return new TreePath(((PreferencesNode) parentNode.getChildAt(nodeIndex + 1)).getPath()); } // ********** export ********** private Action buildExportNodeAction() { return new AbstractAction("Export Node") { public void actionPerformed(ActionEvent event) { PreferencesPanel.this.exportSelectedNode(); } }; } void exportSelectedNode() { PreferencesNode selectedNode = this.selectedPreferencesNode(); if (selectedNode == null) { return; } JFileChooser fc = new JFileChooser(); int rc = fc.showSaveDialog(this); if (rc != JFileChooser.APPROVE_OPTION) { return; } try { OutputStream stream = new BufferedOutputStream(new FileOutputStream(fc.getSelectedFile()), 2048); selectedNode.preferences().exportSubtree(stream); stream.close(); } catch (IOException ex) { this.handleException(ex); } catch (BackingStoreException ex) { this.handleException(ex); } } // ********** add preference ********** private Action buildAddPreferenceAction() { return new AbstractAction("Add Preference") { public void actionPerformed(ActionEvent event) { PreferencesPanel.this.addPreferenceToSelectedNode(); } }; } void addPreferenceToSelectedNode() { Preferences preferences = this.selectedPreferences(); if (preferences == null) { return; } String name = this.promptForPreferenceName(); if (name == null) { return; } try { if (CollectionTools.contains(preferences.keys(), name)) { return; } } catch (BackingStoreException ex) { this.handleException(ex); return; // skip the selection of a row that might not be there... } preferences.put(name, ""); try { // flush the change, so an event is fired and the preference is // created and added to the table before we try to select it preferences.flush(); } catch (BackingStoreException ex) { this.handleException(ex); return; // skip the selection of a row that might not be there... } this.tableSelectionModel.setSelectedValue(this.preference(name)); } // ********** rename preference ********** private Action buildRenamePreferenceAction() { return new AbstractAction("Rename Preference") { public void actionPerformed(ActionEvent event) { PreferencesPanel.this.renameSelectedPreference(); } }; } void renameSelectedPreference() { PreferencePropertyValueModel preference = this.selectedPreference(); if (preference == null) { return; } String name = this.promptForPreferenceName(); if (name == null) { return; } Preferences preferences = this.selectedPreferences(); try { if (CollectionTools.contains(preferences.keys(), name)) { return; } } catch (BackingStoreException ex) { this.handleException(ex); return; } String value = (String) preference.getValue(); preferences.put(name, value); preferences.remove(preference.getKey()); try { // flush the changes, so events are fired and the table is updated preferences.flush(); } catch (BackingStoreException ex) { this.handleException(ex); return; // skip the selection of a node that might not be there... } this.tableSelectionModel.setSelectedValue(this.preference(name)); } // ********** remove preference ********** private Action buildRemovePreferenceAction() { return new AbstractAction("Remove Preference") { public void actionPerformed(ActionEvent event) { PreferencesPanel.this.removeSelectedPreference(); } }; } void removeSelectedPreference() { PreferencePropertyValueModel preference = this.selectedPreference(); if (preference == null) { return; } Preferences preferences = this.selectedPreferences(); preferences.remove(preference.getKey()); try { // flush the change, so an event is fired and the table is updated preferences.flush(); } catch (BackingStoreException ex) { this.handleException(ex); return; // skip the selection of the parent node... } } // ********** inner classes ********** /** * Define interface used by the tree cell renderer. */ private interface LocalNode { String displayString(); } /** * The implied root node that holds both the system * and current user root nodes. */ private class DefaultRootNode extends DefaultMutableTreeNode implements LocalNode { private static final String NAME = "Root"; DefaultRootNode(DefaultTreeModel treeModel) throws BackingStoreException { super(NAME, true); // true = allows children this.initialize(treeModel); } private void initialize(DefaultTreeModel treeModel) throws BackingStoreException { // the "default root" node has two, hard-coded children this.add(this.buildNode(treeModel, Preferences.systemRoot(), "System")); this.add(this.buildNode(treeModel, Preferences.userRoot(), "User")); } private MutableTreeNode buildNode(DefaultTreeModel treeModel, Preferences preferences, String rootName) throws BackingStoreException { return new RootPreferencesNode(treeModel, preferences, rootName); } /** * Simply display the string "root". * @see org.eclipse.persistence.tools.workbench.test.uitools.PreferencesPanel.LocalNode#displayString() */ public String displayString() { return NAME; } } /** * A node for a Preferences. It will automatically populate itself * with the appropriate child nodes upon construction. Then it * will listen for any child nodes being added or removed and update * itself accordingly. */ private class PreferencesNode extends DefaultMutableTreeNode implements LocalNode, NodeChangeListener, Comparable { private DefaultTreeModel treeModel; // ********** constructor/initialization ********** PreferencesNode(DefaultTreeModel treeModel, Preferences preferences) throws BackingStoreException { super(preferences, true); // true = allows children this.treeModel = treeModel; this.preferences().addNodeChangeListener(this); this.initializeChildNodes(); } /** * Add nodes for the preferences's "children" (which are other preferenceses). */ private void initializeChildNodes() throws BackingStoreException { Preferences preferences = this.preferences(); String[] childrenNames = (String[]) CollectionTools.sort(preferences.childrenNames(), this.collator()); int len = childrenNames.length; for (int i = 0; i < len; i++) { this.add(this.buildNode(preferences.node(childrenNames[i]))); } } private PreferencesNode buildNode(Preferences preferences) throws BackingStoreException { return new PreferencesNode(this.treeModel, preferences); } // ********** LocalNode implementation ********** /** * Display the preferences's name. * @see org.eclipse.persistence.tools.workbench.test.uitools.PreferencesPanel.LocalNode#displayString() */ public String displayString() { return this.name(); } // ********** NodeChangeListener implementation ********** /** * @see java.util.prefs.NodeChangeListener#childAdded(java.util.prefs.NodeChangeEvent) */ public void childAdded(NodeChangeEvent evt) { try { this.addNodeFor(evt.getChild()); } catch (BackingStoreException ex) { throw new RuntimeException(ex); } } private void addNodeFor(Preferences preferences) throws BackingStoreException { PreferencesNode childNode = this.buildNode(preferences); this.treeModel.insertNodeInto(childNode, this, this.insertionIndexOf(childNode)); } private int insertionIndexOf(PreferencesNode childNode) { return (this.children == null) ? 0 : CollectionTools.insertionIndexOf(this.children, childNode); } /** * @see java.util.prefs.NodeChangeListener#childRemoved(java.util.prefs.NodeChangeEvent) */ public void childRemoved(NodeChangeEvent evt) { this.removeChildNodeFor(evt.getChild()); } private void removeChildNodeFor(Preferences preferences) { this.treeModel.removeNodeFromParent(this.childNodeFor(preferences)); } PreferencesNode childNodeFor(Preferences child) { for (Enumeration stream = this.children(); stream.hasMoreElements(); ) { PreferencesNode childNode = (PreferencesNode) stream.nextElement(); if (childNode.preferences() == child) { return childNode; } } throw new IllegalArgumentException(child.toString()); } // ********** Comparable implementation ********** /** * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(Object o) { return this.collator().compare(this.name(), ((PreferencesNode) o).name()); } // ********** other methods ********** Preferences preferences() { return (Preferences) this.getUserObject(); } private Collator collator() { return PreferencesPanel.this.getCollator(); } /** * Return whether the node can be removed. * Most nodes can be removed. */ boolean canBeRemoved() { return true; } /** * Return whether the node cannot be removed. */ boolean cannotBeRemoved() { return ! this.canBeRemoved(); } String name() { return this.preferences().name(); } } /** * A node for a "root" Preferences (system or user). The "root" Preferences * do not have names, so require one to be supplied upon construction. */ private class RootPreferencesNode extends PreferencesNode { private String rootName; RootPreferencesNode(DefaultTreeModel treeModel, Preferences rootPreferences, String rootName) throws BackingStoreException { super(treeModel, rootPreferences); this.rootName = rootName; } public String displayString() { return this.rootName; } /** * Return whether the node can be removed. * Root nodes canNOT be removed. */ boolean canBeRemoved() { return false; } } /** * There are 2 columns: one for the preference key and one for the preference * value. Everything is displayed and edited as strings. */ private static class PreferenceColumnAdapter implements ColumnAdapter { public static final int COLUMN_COUNT = 2; public static final int KEY_COLUMN = 0; public static final int VALUE_COLUMN = 1; private static final String[] COLUMN_NAMES = new String[] {"Key", "Value"}; public int getColumnCount() { return COLUMN_COUNT; } public String getColumnName(int index) { return COLUMN_NAMES[index]; } public Class getColumnClass(int index) { switch (index) { case KEY_COLUMN: return Object.class; case VALUE_COLUMN: return Object.class; default: return Object.class; } } public boolean isColumnEditable(int index) { return index != KEY_COLUMN; } public PropertyValueModel[] cellModels(Object subject) { PreferencePropertyValueModel preference = (PreferencePropertyValueModel) subject; PropertyValueModel[] result = new PropertyValueModel[COLUMN_COUNT]; result[KEY_COLUMN] = new SimplePropertyValueModel(preference.getKey()); result[VALUE_COLUMN] = preference; return result; } } }