/* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* * GenericObjectEditor.java * Copyright (C) 2002-2010 University of Waikato, Hamilton, New Zealand * */ package weka.gui; import weka.core.Capabilities; import weka.core.CapabilitiesHandler; import weka.core.ClassDiscovery; import weka.core.OptionHandler; import weka.core.WekaPackageManager; import weka.core.SerializedObject; import weka.core.Utils; import weka.core.Capabilities.Capability; import weka.gui.CheckBoxList.CheckBoxListModel; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.FontMetrics; import java.awt.Frame; import java.awt.GridLayout; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.beans.PropertyEditor; import java.beans.PropertyEditorManager; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Array; import java.util.Enumeration; import java.util.Hashtable; import java.util.Properties; import java.util.StringTokenizer; import java.util.Vector; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import weka.core.Capabilities; import weka.core.CapabilitiesHandler; import weka.core.ClassDiscovery; import weka.core.CustomDisplayStringProvider; import weka.core.OptionHandler; import weka.core.SerializedObject; import weka.core.Utils; import weka.core.Capabilities.Capability; import weka.gui.CheckBoxList.CheckBoxListModel; /** * A PropertyEditor for objects. It can be used either in a static or a dynamic * way. <br> * <br> * In the <b>static</b> way (<code>USE_DYNAMIC</code> is <code>false</code>) the * objects have been defined as editable in the GenericObjectEditor * configuration file, which lists possible values that can be selected from, * and themselves configured. The configuration file is called * "GenericObjectEditor.props" and may live in either the location given by * "user.home" or the current directory (this last will take precedence), and a * default properties file is read from the Weka distribution. For speed, the * properties file is read only once when the class is first loaded -- this may * need to be changed if we ever end up running in a Java OS ;-). <br> * <br> * If it is used in a <b>dynamic</b> way (the <code>UseDynamic</code> property * of the GenericPropertiesCreator props file is set to <code>true</code>) * then the classes to list are discovered by the * <code>GenericPropertiesCreator</code> class (it checks the complete classpath). * * @see GenericPropertiesCreator * @see GenericPropertiesCreator#useDynamic() * @see GenericPropertiesCreator#CREATOR_FILE * @see weka.core.ClassDiscovery * * @author Len Trigg (trigg@cs.waikato.ac.nz) * @author Xin Xu (xx5@cs.waikato.ac.nz) * @author Richard Kirkby (rkirkby@cs.waikato.ac.nz) * @author FracPete (fracpete at waikato dot ac dot nz) * @version $Revision: 6858 $ */ public class GenericObjectEditor implements PropertyEditor, CustomPanelSupplier { /** The object being configured. */ protected Object m_Object; /** Holds a copy of the current object that can be reverted to if the user decides to cancel. */ protected Object m_Backup; /** Handles property change notification. */ protected PropertyChangeSupport m_Support = new PropertyChangeSupport(this); /** The Class of objects being edited. */ protected Class m_ClassType; /** The model containing the list of names to select from. */ protected Hashtable m_ObjectNames; /** The GUI component for editing values, created when needed. */ protected GOEPanel m_EditorComponent; /** True if the GUI component is needed. */ protected boolean m_Enabled = true; /** The name of the properties file. */ protected static String PROPERTY_FILE = "weka/gui/GenericObjectEditor.props"; /** Contains the editor properties. */ protected static Properties EDITOR_PROPERTIES; /** the properties files containing the class/editor mappings. */ public static final String GUIEDITORS_PROPERTY_FILE = "weka/gui/GUIEditors.props"; /** The tree node of the current object so we can re-select it for the user. */ protected GOETreeNode m_treeNodeOfCurrentObject; /** The property panel created for the objects. */ protected PropertyPanel m_ObjectPropertyPanel; /** whether the class can be changed. */ protected boolean m_canChangeClassInDialog; /** whether the Weka Editors were already registered. */ protected static boolean m_EditorsRegistered; /** for filtering the tree based on the Capabilities of the leaves. */ protected Capabilities m_CapabilitiesFilter = null; public static void determineClasses() { try { // make sure we load all packages first!!! WekaPackageManager.loadPackages(false); EDITOR_PROPERTIES = GenericPropertiesCreator.getGlobalOutputProperties(); if (EDITOR_PROPERTIES == null) { // try creating a new one from scratch GenericPropertiesCreator creator = new GenericPropertiesCreator(); // dynamic approach? if (creator.useDynamic()) { try { creator.execute(false); EDITOR_PROPERTIES = creator.getOutputProperties(); } catch (Exception e) { JOptionPane.showMessageDialog( null, "Could not determine the properties for the generic object\n" + "editor. This exception was produced:\n" + e.toString(), "GenericObjectEditor", JOptionPane.ERROR_MESSAGE); } } else { // Allow a properties file in the current directory to override try { EDITOR_PROPERTIES = Utils.readProperties(PROPERTY_FILE); java.util.Enumeration keys = (java.util.Enumeration)EDITOR_PROPERTIES.propertyNames(); if (!keys.hasMoreElements()) { throw new Exception("Failed to read a property file for the " +"generic object editor"); } } catch (Exception ex) { JOptionPane.showMessageDialog( null, "Could not read a configuration file for the generic object\n" +"editor. An example file is included with the Weka distribution.\n" +"This file should be named \"" + PROPERTY_FILE + "\" and\n" +"should be placed either in your user home (which is set\n" + "to \"" + System.getProperties().getProperty("user.home") + "\")\n" + "or the directory that java was started from\n", "GenericObjectEditor", JOptionPane.ERROR_MESSAGE); } } } if (EDITOR_PROPERTIES == null) { JOptionPane.showMessageDialog( null, "Could not initialize the GenericPropertiesCreator. ", "GenericObjectEditor", JOptionPane.ERROR_MESSAGE); } } catch (Exception e) { JOptionPane.showMessageDialog( null, "Could not initialize the GenericPropertiesCreator. " + "This exception was produced:\n" + e.toString(), "GenericObjectEditor", JOptionPane.ERROR_MESSAGE); } } /** * Loads the configuration property file (USE_DYNAMIC is FALSE) or determines * the classes dynamically (USE_DYNAMIC is TRUE) * @see #USE_DYNAMIC * @see GenericPropertiesCreator */ static { determineClasses(); } /** * A specialized TreeNode for supporting filtering via Capabilities. */ public class GOETreeNode extends DefaultMutableTreeNode { /** for serialization. */ static final long serialVersionUID = -1707872446682150133L; /** color for "no support". */ public final static String NO_SUPPORT = "silver"; /** color for "maybe support". */ public final static String MAYBE_SUPPORT = "blue"; /** the Capabilities object to use for filtering. */ protected Capabilities m_Capabilities = null; /** * Creates a tree node that has no parent and no children, but which * allows children. */ public GOETreeNode() { super(); } /** * Creates a tree node with no parent, no children, but which allows * children, and initializes it with the specified user object. * * @param userObject an Object provided by the user that constitutes * the node's data */ public GOETreeNode(Object userObject) { super(userObject); } /** * Creates a tree node with no parent, no children, initialized with the * specified user object, and that allows children only if specified. * @param userObject an Object provided by the user that constitutes * the node's data * @param allowsChildren if true, the node is allowed to have child nodes * -- otherwise, it is always a leaf node */ public GOETreeNode(Object userObject, boolean allowsChildren) { super(userObject, allowsChildren); } /** * generates if necessary a Capabilities object for the given leaf. */ protected void initCapabilities() { String classname; Class cls; Object obj; if (m_Capabilities != null) return; if (!isLeaf()) return; classname = getClassnameFromPath(new TreePath(getPath())); try { cls = Class.forName(classname); if (!ClassDiscovery.hasInterface(CapabilitiesHandler.class, cls)) return; obj = cls.newInstance(); m_Capabilities = ((CapabilitiesHandler) obj).getCapabilities(); } catch (Exception e) { // ignore it } } /** * returns a string representation of this treenode. * * @return the text to display */ public String toString() { String result; result = super.toString(); if (m_CapabilitiesFilter != null) { initCapabilities(); if (m_Capabilities != null) { if (m_Capabilities.supportsMaybe(m_CapabilitiesFilter) && !m_Capabilities.supports(m_CapabilitiesFilter)) result = "<html><font color=\"" + MAYBE_SUPPORT + "\">" + result + "</font></i><html>"; else if (!m_Capabilities.supports(m_CapabilitiesFilter)) result = "<html><font color=\"" + NO_SUPPORT + "\">" + result + "</font></i><html>"; } } return result; } } /** * A dialog for selecting Capabilities to look for in the GOE tree. */ public class CapabilitiesFilterDialog extends JDialog { /** for serialization. */ static final long serialVersionUID = -7845503345689646266L; /** the dialog itself. */ protected JDialog m_Self; /** the popup to display again. */ protected JPopupMenu m_Popup = null; /** the capabilities used for initializing the dialog. */ protected Capabilities m_Capabilities = new Capabilities(null); /** the label, listing the name of the superclass. */ protected JLabel m_InfoLabel = new JLabel(); /** the list with all the capabilities. */ protected CheckBoxList m_List = new CheckBoxList(); /** the OK button. */ protected JButton m_OkButton = new JButton("OK"); /** the Cancel button. */ protected JButton m_CancelButton = new JButton("Cancel"); /** * creates a dialog to choose Capabilities from. */ public CapabilitiesFilterDialog() { super(); m_Self = this; initGUI(); } /** * sets up the GUI. */ protected void initGUI() { JPanel panel; CheckBoxListModel model; setTitle("Filtering Capabilities..."); setLayout(new BorderLayout()); panel = new JPanel(new BorderLayout()); panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); getContentPane().add(panel, BorderLayout.NORTH); m_InfoLabel.setText( "<html>" + m_ClassType.getName().replaceAll(".*\\.", "") + "s" + " have to support <i>at least</i> the following capabilities <br>" + "(the ones highlighted <font color=\"" + GOETreeNode.NO_SUPPORT + "\">" + GOETreeNode.NO_SUPPORT + "</font> don't meet these requirements <br>" + "the ones highlighted <font color=\"" + GOETreeNode.MAYBE_SUPPORT + "\">" + GOETreeNode.MAYBE_SUPPORT + "</font> possibly meet them):" + "</html>"); panel.add(m_InfoLabel, BorderLayout.CENTER); // list getContentPane().add(new JScrollPane(m_List), BorderLayout.CENTER); model = (CheckBoxListModel) m_List.getModel(); for (Capability cap: Capability.values()) model.addElement(cap); // buttons panel = new JPanel(new FlowLayout(FlowLayout.CENTER)); getContentPane().add(panel, BorderLayout.SOUTH); m_OkButton.setMnemonic('O'); m_OkButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { updateCapabilities(); if (m_CapabilitiesFilter == null) m_CapabilitiesFilter = new Capabilities(null); m_CapabilitiesFilter.assign(m_Capabilities); m_Self.setVisible(false); showPopup(); } }); panel.add(m_OkButton); m_CancelButton.setMnemonic('C'); m_CancelButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { m_Self.setVisible(false); showPopup(); } }); panel.add(m_CancelButton); pack(); } /** * transfers the Capabilities object to the JList. * * @see #m_Capabilities * @see #m_List */ protected void updateList() { CheckBoxListModel model; model = (CheckBoxListModel) m_List.getModel(); for (Capability cap: Capability.values()) model.setChecked(model.indexOf(cap), m_Capabilities.handles(cap)); } /** * transfers the selected Capabilities from the JList to the * Capabilities object. * * @see #m_Capabilities * @see #m_List */ protected void updateCapabilities() { CheckBoxListModel model; model = (CheckBoxListModel) m_List.getModel(); for (Capability cap: Capability.values()) { if (model.getChecked(model.indexOf(cap))) m_Capabilities.enable(cap); else m_Capabilities.disable(cap); } } /** * sets the initial capabilities. * * @param value the capabilities to use */ public void setCapabilities(Capabilities value) { if (value != null) m_Capabilities.assign(value); else m_Capabilities = new Capabilities(null); updateList(); } /** * returns the currently selected capabilities. * * @return the currently selected capabilities */ public Capabilities getCapabilities() { return m_Capabilities; } /** * sets the JPopupMenu to display again after closing the dialog. * * @param value the JPopupMenu to display again */ public void setPopup(JPopupMenu value) { m_Popup = value; } /** * returns the currently set JPopupMenu. * * @return the current JPopupMenu */ public JPopupMenu getPopup() { return m_Popup; } /** * if a JPopupMenu is set, it is displayed again. Displaying this dialog * closes any JPopupMenu automatically. */ public void showPopup() { if (getPopup() != null) getPopup().setVisible(true); } } /** * Creates a popup menu containing a tree that is aware * of the screen dimensions. */ public class JTreePopupMenu extends JPopupMenu { /** for serialization. */ static final long serialVersionUID = -3404546329655057387L; /** the popup itself. */ private JPopupMenu m_Self; /** The tree. */ private JTree m_tree; /** The scroller. */ private JScrollPane m_scroller; /** The filter button in case of CapabilitiesHandlers. */ private JButton m_FilterButton = new JButton("Filter..."); /** The remove filter button in case of CapabilitiesHandlers. */ private JButton m_RemoveFilterButton = new JButton("Remove filter"); /** The button for closing the popup again. */ private JButton m_CloseButton = new JButton("Close"); /** * Constructs a new popup menu. * * @param tree the tree to put in the menu */ public JTreePopupMenu(JTree tree) { m_Self = this; setLayout(new BorderLayout()); JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); add(panel, BorderLayout.SOUTH); if (ClassDiscovery.hasInterface(CapabilitiesHandler.class, m_ClassType)) { // filter m_FilterButton.setMnemonic('F'); m_FilterButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (e.getSource() == m_FilterButton) { CapabilitiesFilterDialog dialog = new CapabilitiesFilterDialog(); dialog.setCapabilities(m_CapabilitiesFilter); dialog.setPopup(m_Self); dialog.setVisible(true); m_Support.firePropertyChange("", null, null); repaint(); } } }); panel.add(m_FilterButton); // remove m_RemoveFilterButton.setMnemonic('R'); m_RemoveFilterButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (e.getSource() == m_RemoveFilterButton) { m_CapabilitiesFilter = null; m_Support.firePropertyChange("", null, null); repaint(); } } }); panel.add(m_RemoveFilterButton); } // close m_CloseButton.setMnemonic('C'); m_CloseButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (e.getSource() == m_CloseButton) { m_Self.setVisible(false); } } }); panel.add(m_CloseButton); m_tree = tree; JPanel treeView = new JPanel(); treeView.setLayout(new BorderLayout()); treeView.add(m_tree, BorderLayout.NORTH); // make backgrounds look the same treeView.setBackground(m_tree.getBackground()); m_scroller = new JScrollPane(treeView); m_scroller.setPreferredSize(new Dimension(300, 400)); m_scroller.getVerticalScrollBar().setUnitIncrement(20); add(m_scroller); } /** * Displays the menu, making sure it will fit on the screen. * * @param invoker the component thast invoked the menu * @param x the x location of the popup * @param y the y location of the popup */ public void show(Component invoker, int x, int y) { super.show(invoker, x, y); // calculate available screen area for popup java.awt.Point location = getLocationOnScreen(); java.awt.Dimension screenSize = getToolkit().getScreenSize(); int maxWidth = (int) (screenSize.getWidth() - location.getX()); int maxHeight = (int) (screenSize.getHeight() - location.getY()); // if the part of the popup goes off the screen then resize it Dimension scrollerSize = m_scroller.getPreferredSize(); int height = (int) scrollerSize.getHeight(); int width = (int) scrollerSize.getWidth(); if (width > maxWidth) width = maxWidth; if (height > maxHeight) height = maxHeight; // commit any size changes m_scroller.setPreferredSize(new Dimension(width, height)); revalidate(); pack(); } } /** * Handles the GUI side of editing values. */ public class GOEPanel extends JPanel { /** for serialization. */ static final long serialVersionUID = 3656028520876011335L; /** The component that performs classifier customization. */ protected PropertySheetPanel m_ChildPropertySheet; /** The name of the current class. */ protected JLabel m_ClassNameLabel; /** Open object from disk. */ protected JButton m_OpenBut; /** Save object to disk. */ protected JButton m_SaveBut; /** ok button. */ protected JButton m_okBut; /** cancel button. */ protected JButton m_cancelBut; /** The filechooser for opening and saving object files. */ protected JFileChooser m_FileChooser; /** Creates the GUI editor component. */ public GOEPanel() { m_Backup = copyObject(m_Object); m_ClassNameLabel = new JLabel("None"); m_ClassNameLabel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); m_ChildPropertySheet = new PropertySheetPanel(); m_ChildPropertySheet.addPropertyChangeListener (new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { m_Support.firePropertyChange("", null, null); } }); m_OpenBut = new JButton("Open..."); m_OpenBut.setToolTipText("Load a configured object"); m_OpenBut.setEnabled(true); m_OpenBut.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Object object = openObject(); if (object != null) { // setValue takes care of: Making sure obj is of right type, // and firing property change. setValue(object); // Need a second setValue to get property values filled in OK. // Not sure why. setValue(object); } } }); m_SaveBut = new JButton("Save..."); m_SaveBut.setToolTipText("Save the current configured object"); m_SaveBut.setEnabled(true); m_SaveBut.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { saveObject(m_Object); } }); m_okBut = new JButton("OK"); m_okBut.setEnabled(true); m_okBut.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { m_Backup = copyObject(m_Object); if ((getTopLevelAncestor() != null) && (getTopLevelAncestor() instanceof Window)) { Window w = (Window) getTopLevelAncestor(); w.dispose(); } } }); m_cancelBut = new JButton("Cancel"); m_cancelBut.setEnabled(true); m_cancelBut.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (m_Backup != null) { m_Object = copyObject(m_Backup); // To fire property change m_Support.firePropertyChange("", null, null); m_ObjectNames = getClassesFromProperties(); updateObjectNames(); updateChildPropertySheet(); } if ((getTopLevelAncestor() != null) && (getTopLevelAncestor() instanceof Window)) { Window w = (Window) getTopLevelAncestor(); w.dispose(); } } }); setLayout(new BorderLayout()); if (m_canChangeClassInDialog) { JButton chooseButton = createChooseClassButton(); JPanel top = new JPanel(); top.setLayout(new BorderLayout()); top.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); top.add(chooseButton, BorderLayout.WEST); top.add(m_ClassNameLabel, BorderLayout.CENTER); add(top, BorderLayout.NORTH); } else { add(m_ClassNameLabel, BorderLayout.NORTH); } add(m_ChildPropertySheet, BorderLayout.CENTER); // Since we resize to the size of the property sheet, a scrollpane isn't // typically needed // add(new JScrollPane(m_ChildPropertySheet), BorderLayout.CENTER); JPanel okcButs = new JPanel(); okcButs.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); okcButs.setLayout(new GridLayout(1, 4, 5, 5)); okcButs.add(m_OpenBut); okcButs.add(m_SaveBut); okcButs.add(m_okBut); okcButs.add(m_cancelBut); add(okcButs, BorderLayout.SOUTH); if (m_ClassType != null) { m_ObjectNames = getClassesFromProperties(); if (m_Object != null) { updateObjectNames(); updateChildPropertySheet(); } } } /** * Enables/disables the cancel button. * * @param flag true to enable cancel button, false * to disable it */ protected void setCancelButton(boolean flag) { if(m_cancelBut != null) m_cancelBut.setEnabled(flag); } /** * Opens an object from a file selected by the user. * * @return the loaded object, or null if the operation was cancelled */ protected Object openObject() { if (m_FileChooser == null) { createFileChooser(); } int returnVal = m_FileChooser.showOpenDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { File selected = m_FileChooser.getSelectedFile(); try { ObjectInputStream oi = new ObjectInputStream(new BufferedInputStream(new FileInputStream(selected))); Object obj = oi.readObject(); oi.close(); if (!m_ClassType.isAssignableFrom(obj.getClass())) { throw new Exception("Object not of type: " + m_ClassType.getName()); } return obj; } catch (Exception ex) { JOptionPane.showMessageDialog(this, "Couldn't read object: " + selected.getName() + "\n" + ex.getMessage(), "Open object file", JOptionPane.ERROR_MESSAGE); } } return null; } /** * Saves an object to a file selected by the user. * * @param object the object to save */ protected void saveObject(Object object) { if (m_FileChooser == null) { createFileChooser(); } int returnVal = m_FileChooser.showSaveDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { File sFile = m_FileChooser.getSelectedFile(); try { ObjectOutputStream oo = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(sFile))); oo.writeObject(object); oo.close(); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "Couldn't write to file: " + sFile.getName() + "\n" + ex.getMessage(), "Save object", JOptionPane.ERROR_MESSAGE); } } } /** * Creates the file chooser the user will use to save/load files with. */ protected void createFileChooser() { m_FileChooser = new JFileChooser(new File(System.getProperty("user.dir"))); m_FileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); } /** * Makes a copy of an object using serialization. * * @param source the object to copy * @return a copy of the source object */ protected Object copyObject(Object source) { Object result = null; try { result = GenericObjectEditor.this.makeCopy(source); setCancelButton(true); } catch (Exception ex) { setCancelButton(false); System.err.println("GenericObjectEditor: Problem making backup object"); System.err.println(ex); } return result; } /** * Allows customization of the action label on the dialog. * * @param newLabel the new string for the ok button */ public void setOkButtonText(String newLabel) { m_okBut.setText(newLabel); } /** * This is used to hook an action listener to the ok button. * * @param a The action listener. */ public void addOkListener(ActionListener a) { m_okBut.addActionListener(a); } /** * This is used to hook an action listener to the cancel button. * * @param a The action listener. */ public void addCancelListener(ActionListener a) { m_cancelBut.addActionListener(a); } /** * This is used to remove an action listener from the ok button. * * @param a The action listener */ public void removeOkListener(ActionListener a) { m_okBut.removeActionListener(a); } /** * This is used to remove an action listener from the cancel button. * * @param a The action listener */ public void removeCancelListener(ActionListener a) { m_cancelBut.removeActionListener(a); } /** * Updates the child property sheet, and creates if needed. */ public void updateChildPropertySheet() { // Update the object name displayed String className = "None"; if (m_Object != null) { className = m_Object.getClass().getName(); } m_ClassNameLabel.setText(className); // Set the object as the target of the propertysheet m_ChildPropertySheet.setTarget(m_Object); // Adjust size of containing window if possible if ((getTopLevelAncestor() != null) && (getTopLevelAncestor() instanceof Window)) { ((Window) getTopLevelAncestor()).pack(); } } } /** * Default constructor. */ public GenericObjectEditor() { this(false); } /** * Constructor that allows specifying whether it is possible * to change the class within the editor dialog. * * @param canChangeClassInDialog whether the user can change the class */ public GenericObjectEditor(boolean canChangeClassInDialog) { m_canChangeClassInDialog = canChangeClassInDialog; } /** * registers all the editors in Weka. */ public static void registerEditors() { Properties props; Enumeration enm; String name; String value; Class baseCls; Class cls; if (m_EditorsRegistered) return; System.err.println("---Registering Weka Editors---"); m_EditorsRegistered = true; // load properties try { props = Utils.readProperties(GUIEDITORS_PROPERTY_FILE); } catch (Exception e) { props = new Properties(); e.printStackTrace(); } enm = props.propertyNames(); while (enm.hasMoreElements()) { name = enm.nextElement().toString(); value = props.getProperty(name, ""); registerEditor(name, value); } } public static void registerEditor(String name, String value) { Class baseCls; Class cls; try { // array class? if (name.endsWith("[]")) { baseCls = Class.forName(name.substring(0, name.indexOf("[]"))); cls = Array.newInstance(baseCls, 1).getClass(); } else { cls = Class.forName(name); } // register PropertyEditorManager.registerEditor(cls, Class.forName(value)); } catch (Exception e) { System.err.println("Problem registering " + name + "/" + value + ": " + e); } } /** * Sets whether the user can change the class in the dialog. * * @param value if true then the user can change the class */ public void setCanChangeClassInDialog(boolean value) { m_canChangeClassInDialog = value; } /** * Returns whether the user can change the class in the dialog. * * @return true if the user can change the class */ public boolean getCanChangeClassInDialog() { return m_canChangeClassInDialog; } /** * Returns the backup object (may be null if there is no * backup. * * @return the backup object */ public Object getBackup() { return m_Backup; } /** * returns the name of the root element of the given class name, * <code>null</code> if it doesn't contain the separator. * * @param clsname the full classname * @param separator the separator * @return string the root element */ protected static String getRootFromClass(String clsname, String separator) { if (clsname.indexOf(separator) > -1) return clsname.substring(0, clsname.indexOf(separator)); else return null; } /** * parses the given string of classes separated by ", " and returns the * a hashtable with as many entries as there are different root elements in * the class names (the key is the root element). E.g. if there's only * "weka." as the prefix for all classes the a hashtable of size 1 is returned. * if NULL is the input, then NULL is also returned. * * @param classes the classnames to work on * @return for each distinct root element in the classnames, one entry in * the hashtable (with the root element as key) */ public static Hashtable sortClassesByRoot(String classes) { Hashtable roots; Hashtable result; Enumeration enm; int i; StringTokenizer tok; String clsname; Vector list; HierarchyPropertyParser hpp; String separator; String root; String tmpStr; if (classes == null) return null; roots = new Hashtable(); hpp = new HierarchyPropertyParser(); separator = hpp.getSeperator(); // go over all classnames and store them in the hashtable, with the // root element as the key tok = new StringTokenizer(classes, ", "); while (tok.hasMoreElements()) { clsname = tok.nextToken(); root = getRootFromClass(clsname, separator); if (root == null) continue; // already stored? if (!roots.containsKey(root)) { list = new Vector(); roots.put(root, list); } else { list = (Vector) roots.get(root); } list.add(clsname); } // build result result = new Hashtable(); enm = roots.keys(); while (enm.hasMoreElements()) { root = (String) enm.nextElement(); list = (Vector) roots.get(root); tmpStr = ""; for (i = 0; i < list.size(); i++) { if (i > 0) tmpStr += ","; tmpStr += (String) list.get(i); } result.put(root, tmpStr); } return result; } /** * Called when the class of object being edited changes. * * @return the hashtable containing the HierarchyPropertyParsers for the root * elements */ protected Hashtable getClassesFromProperties() { Hashtable hpps = new Hashtable(); String className = m_ClassType.getName(); Hashtable typeOptions = sortClassesByRoot(EDITOR_PROPERTIES.getProperty(className)); if (typeOptions == null) { /* System.err.println("Warning: No configuration property found in\n" + PROPERTY_FILE + "\n" + "for " + className); */ } else { try { Enumeration enm = typeOptions.keys(); while (enm.hasMoreElements()) { String root = (String) enm.nextElement(); String typeOption = (String) typeOptions.get(root); HierarchyPropertyParser hpp = new HierarchyPropertyParser(); hpp.build(typeOption, ", "); hpps.put(root, hpp); } } catch (Exception ex) { System.err.println("Invalid property: " + typeOptions); } } return hpps; } /** * Updates the list of selectable object names, adding any new names to the list. */ protected void updateObjectNames() { if (m_ObjectNames == null) { m_ObjectNames = getClassesFromProperties(); } if (m_Object != null) { String className = m_Object.getClass().getName(); String root = getRootFromClass(className, new HierarchyPropertyParser().getSeperator()); HierarchyPropertyParser hpp = (HierarchyPropertyParser) m_ObjectNames.get(root); if (hpp != null) { if(!hpp.contains(className)){ hpp.add(className); } } } } /** * Sets whether the editor is "enabled", meaning that the current * values will be painted. * * @param newVal a value of type 'boolean' */ public void setEnabled(boolean newVal) { if (newVal != m_Enabled) { m_Enabled = newVal; } } /** * Sets the class of values that can be edited. * * @param type a value of type 'Class' */ public void setClassType(Class type) { m_ClassType = type; m_ObjectNames = getClassesFromProperties(); } /** * Sets the current object to be the default, taken as the first item in * the chooser. */ public void setDefaultValue() { if (m_ClassType == null) { System.err.println("No ClassType set up for GenericObjectEditor!!"); return; } Hashtable hpps = getClassesFromProperties(); HierarchyPropertyParser hpp = null; Enumeration enm = hpps.elements(); try{ while (enm.hasMoreElements()) { hpp = (HierarchyPropertyParser) enm.nextElement(); if(hpp.depth() > 0) { hpp.goToRoot(); while(!hpp.isLeafReached()) hpp.goToChild(0); String defaultValue = hpp.fullValue(); setValue(Class.forName(defaultValue).newInstance()); } } }catch(Exception ex){ System.err.println("Problem loading the first class: "+ hpp.fullValue()); ex.printStackTrace(); } } /** * Sets the current Object. If the Object is in the * Object chooser, this becomes the selected item (and added * to the chooser if necessary). * * @param o an object that must be a Object. */ public void setValue(Object o) { if (m_ClassType == null) { System.err.println("No ClassType set up for GenericObjectEditor!!"); return; } if (!m_ClassType.isAssignableFrom(o.getClass())) { System.err.println("setValue object not of correct type!"); return; } setObject(o); if (m_EditorComponent != null) m_EditorComponent.repaint(); updateObjectNames(); } /** * Sets the current Object. * * @param c a value of type 'Object' */ protected void setObject(Object c) { // This should really call equals() for comparison. boolean trueChange ; if (getValue() != null) { trueChange = (!c.equals(getValue())); } else trueChange = true; m_Backup = m_Object; m_Object = c; if (m_EditorComponent != null) { m_EditorComponent.updateChildPropertySheet(); } if (trueChange) { m_Support.firePropertyChange("", null, null); } } /** * Gets the current Object. * * @return the current Object */ public Object getValue() { Object result = null; try { result = makeCopy(m_Object); } catch (Exception ex) { ex.printStackTrace(); } return result; } /** * Supposedly returns an initialization string to create a Object * identical to the current one, including it's state, but this doesn't * appear possible given that the initialization string isn't supposed to * contain multiple statements. * * @return the java source code initialisation string */ public String getJavaInitializationString() { return "new " + m_Object.getClass().getName() + "()"; } /** * Returns true to indicate that we can paint a representation of the * Object. * * @return true */ public boolean isPaintable() { return true; } /** * Paints a representation of the current Object. * * @param gfx the graphics context to use * @param box the area we are allowed to paint into */ public void paintValue(java.awt.Graphics gfx, java.awt.Rectangle box) { if (m_Enabled) { String rep; if (m_Object != null) { if (m_Object instanceof CustomDisplayStringProvider) { rep = ((CustomDisplayStringProvider) m_Object).toDisplay(); } else { rep = m_Object.getClass().getName(); int dotPos = rep.lastIndexOf('.'); if (dotPos != -1) { rep = rep.substring(dotPos + 1); } } } else { rep = "None"; } java.awt.Font originalFont = gfx.getFont(); gfx.setFont(originalFont.deriveFont(java.awt.Font.BOLD)); FontMetrics fm = gfx.getFontMetrics(); int vpad = (box.height - fm.getHeight()); gfx.drawString(rep, 2, fm.getAscent() + vpad); int repwidth = fm.stringWidth(rep); gfx.setFont(originalFont); if ((m_Object instanceof OptionHandler) && !(m_Object instanceof CustomDisplayStringProvider)) { gfx.drawString(" " + Utils.joinOptions(((OptionHandler)m_Object).getOptions()), repwidth + 2, fm.getAscent() + vpad); } } } /** * Returns null as we don't support getting/setting values as text. * * @return null */ public String getAsText() { return null; } /** * Returns null as we don't support getting/setting values as text. * * @param text the text value * @throws IllegalArgumentException as we don't support * getting/setting values as text. */ public void setAsText(String text) { throw new IllegalArgumentException(text); } /** * Returns null as we don't support getting values as tags. * * @return null */ public String[] getTags() { return null; } /** * Returns true because we do support a custom editor. * * @return true */ public boolean supportsCustomEditor() { return true; } /** * Returns the array editing component. * * @return a value of type 'java.awt.Component' */ public java.awt.Component getCustomEditor() { if (m_EditorComponent == null) { m_EditorComponent = new GOEPanel(); } return m_EditorComponent; } /** * Adds a PropertyChangeListener who will be notified of value changes. * * @param l a value of type 'PropertyChangeListener' */ public void addPropertyChangeListener(PropertyChangeListener l) { m_Support.addPropertyChangeListener(l); } /** * Removes a PropertyChangeListener. * * @param l a value of type 'PropertyChangeListener' */ public void removePropertyChangeListener(PropertyChangeListener l) { m_Support.removePropertyChangeListener(l); } /** * Gets the custom panel used for editing the object. * * @return the panel */ public JPanel getCustomPanel() { final JButton chooseButton = createChooseClassButton(); m_ObjectPropertyPanel = new PropertyPanel(this, true); JPanel customPanel = new JPanel() { public void setEnabled(boolean enabled) { super.setEnabled(enabled); chooseButton.setEnabled(enabled); } }; customPanel.setLayout(new BorderLayout()); customPanel.add(chooseButton, BorderLayout.WEST); customPanel.add(m_ObjectPropertyPanel, BorderLayout.CENTER); return customPanel; } /** * Creates a button that when clicked will enable the user to change * the class of the object being edited. * * @return the choose button */ protected JButton createChooseClassButton() { JButton setButton = new JButton("Choose"); // anonymous action listener shows a JTree popup and allows the user // to choose the class they want setButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JPopupMenu popup = getChooseClassPopupMenu(); // show the popup where the source component is if (e.getSource() instanceof Component) { Component comp = (Component) e.getSource(); popup.show(comp, comp.getX(), comp.getY()); popup.pack(); popup.repaint(); } } }); return setButton; } /** * creates a classname from the given path. * * @param path the path to generate the classname from * @return the generated classname */ protected String getClassnameFromPath(TreePath path) { StringBuffer classname = new StringBuffer(); // recreate class name from path int start = 0; if (m_ObjectNames.size() > 1) start = 1; for (int i = start; i < path.getPathCount(); i++) { if (i>start) classname.append("."); classname.append( (String) ((GOETreeNode) path.getPathComponent(i)).getUserObject()); } return classname.toString(); } /** * Returns a popup menu that allows the user to change * the class of object. * * @return a JPopupMenu that when shown will let the user choose the class */ public JPopupMenu getChooseClassPopupMenu() { updateObjectNames(); // create the tree, and find the path to the current class m_treeNodeOfCurrentObject = null; final JTree tree = createTree(m_ObjectNames); if (m_treeNodeOfCurrentObject != null) { tree.setSelectionPath(new TreePath(m_treeNodeOfCurrentObject.getPath())); } tree.getSelectionModel().setSelectionMode (TreeSelectionModel.SINGLE_TREE_SELECTION); // create the popup final JPopupMenu popup = new JTreePopupMenu(tree); // respond when the user chooses a class tree.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { GOETreeNode node = (GOETreeNode) tree.getLastSelectedPathComponent(); if (node == null) return; if (node.isLeaf()) { classSelected(getClassnameFromPath(tree.getSelectionPath())); popup.setVisible(false); } } }); return popup; } /** * Creates a JTree from an object heirarchy. * * @param hpps the hierarchy of objects to mirror in the tree * @return a JTree representation of the hierarchy */ protected JTree createTree(Hashtable hpps) { GOETreeNode superRoot; Enumeration enm; HierarchyPropertyParser hpp; if (hpps.size() > 1) superRoot = new GOETreeNode("root"); else superRoot = null; enm = hpps.elements(); while (enm.hasMoreElements()) { hpp = (HierarchyPropertyParser) enm.nextElement(); hpp.goToRoot(); GOETreeNode root = new GOETreeNode(hpp.getValue()); addChildrenToTree(root, hpp); if (superRoot == null) superRoot = root; else superRoot.add(root); } JTree tree = new JTree(superRoot); return tree; } /** * Recursively builds a JTree from an object heirarchy. * Also updates m_treeNodeOfCurrentObject if the current object * is discovered during creation. * * @param tree the root of the tree to add children to * @param hpp the hierarchy of objects to mirror in the tree */ protected void addChildrenToTree(GOETreeNode tree, HierarchyPropertyParser hpp) { try { for (int i=0; i<hpp.numChildren(); i++) { hpp.goToChild(i); GOETreeNode child = new GOETreeNode(hpp.getValue()); if ((m_Object != null) && m_Object.getClass().getName().equals(hpp.fullValue())) { m_treeNodeOfCurrentObject = child; } tree.add(child); addChildrenToTree(child, hpp); hpp.goToParent(); } } catch (Exception e) { e.printStackTrace(); } } /** * Called when the user selects an class type to change to. * * @param className the name of the class that was selected */ protected void classSelected(String className) { try { if ((m_Object != null) && m_Object.getClass().getName().equals(className)) { return; } setValue(Class.forName(className).newInstance()); //m_ObjectPropertyPanel.showPropertyDialog(); if (m_EditorComponent != null) { m_EditorComponent.updateChildPropertySheet(); } } catch (Exception ex) { JOptionPane.showMessageDialog(null, "Could not create an example of\n" + className + "\n" + "from the current classpath", "Class load failed", JOptionPane.ERROR_MESSAGE); ex.printStackTrace(); try { if(m_Backup != null) setValue(m_Backup); else setDefaultValue(); } catch(Exception e) { System.err.println(ex.getMessage()); ex.printStackTrace(); } } } /** * Sets the capabilities to use for filtering. * * @param value the object to get the filter capabilities from */ public void setCapabilitiesFilter(Capabilities value) { m_CapabilitiesFilter = new Capabilities(null); m_CapabilitiesFilter.assign(value); } /** * Returns the current Capabilities filter, can be null. * * @return the current Capabiliities used for filtering */ public Capabilities getCapabilitiesFilter() { return m_CapabilitiesFilter; } /** * Removes the current Capabilities filter. */ public void removeCapabilitiesFilter() { m_CapabilitiesFilter = null; } /** * Makes a copy of an object using serialization. * * @param source the object to copy * @return a copy of the source object * @exception Exception if the copy fails */ public static Object makeCopy(Object source) throws Exception { SerializedObject so = new SerializedObject(source); Object result = so.getObject(); return result; } /** * Returns the available classnames for a certain property in the * props file. * * @param property the property to get the classnames for * @return the classnames */ public static Vector<String> getClassnames(String property) { Vector<String> result; String value; String[] items; int i; result = new Vector<String>(); value = EDITOR_PROPERTIES.getProperty(property, "").replaceAll(" ", "").trim(); if (value.length() > 0) { items = value.split(","); for (i = 0; i < items.length; i++) result.add(items[i]); } return result; } /** * Tests out the Object editor from the command line. * * @param args may contain the class name of a Object to edit */ public static void main(String [] args) { try { GenericObjectEditor.registerEditors(); GenericObjectEditor ce = new GenericObjectEditor(true); ce.setClassType(weka.classifiers.Classifier.class); Object initial = new weka.classifiers.rules.ZeroR(); if (args.length > 0){ ce.setClassType(Class.forName(args[0])); if(args.length > 1){ initial = (Object)Class.forName(args[1]).newInstance(); ce.setValue(initial); } else ce.setDefaultValue(); } else ce.setValue(initial); PropertyDialog pd = new PropertyDialog((Frame) null, ce, 100, 100); pd.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { PropertyEditor pe = ((PropertyDialog)e.getSource()).getEditor(); Object c = (Object)pe.getValue(); String options = ""; if (c instanceof OptionHandler) { options = Utils.joinOptions(((OptionHandler)c).getOptions()); } System.out.println(c.getClass().getName() + " " + options); System.exit(0); } }); pd.setVisible(true); } catch (Exception ex) { ex.printStackTrace(); System.err.println(ex.getMessage()); } } }