diff options
Diffstat (limited to 'src/de/lmu/ifi/dbs/elki/gui/util')
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/ClassTree.java | 211 | ||||
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/DynamicParameters.java | 8 | ||||
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/LogPane.java | 2 | ||||
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/LogPanel.java | 2 | ||||
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/ParameterTable.java | 461 | ||||
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/ParametersModel.java | 2 | ||||
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/SavedSettingsFile.java | 2 | ||||
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/TreePopup.java | 412 | ||||
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/package-info.java | 2 |
9 files changed, 907 insertions, 195 deletions
diff --git a/src/de/lmu/ifi/dbs/elki/gui/util/ClassTree.java b/src/de/lmu/ifi/dbs/elki/gui/util/ClassTree.java new file mode 100644 index 00000000..3071aa7e --- /dev/null +++ b/src/de/lmu/ifi/dbs/elki/gui/util/ClassTree.java @@ -0,0 +1,211 @@ +package de.lmu.ifi.dbs.elki.gui.util; + +/* + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures + + Copyright (C) 2014 + Ludwig-Maximilians-Universität München + Lehr- und Forschungseinheit für Datenbanksysteme + ELKI Development Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +import java.util.HashMap; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreeNode; + +/** + * Build a tree of available classes for use in Swing UIs. + * + * @author Erich Schubert + * + * @apiviz.has TreeNode + */ +public class ClassTree { + /** + * Build the class tree for a given set of choices. + * + * @param choices Class choices + * @param rootpkg Root package name (to strip / hide) + * @return Root node. + */ + public static TreeNode build(List<Class<?>> choices, String rootpkg) { + MutableTreeNode root = new PackageNode(rootpkg, rootpkg); + HashMap<String, MutableTreeNode> lookup = new HashMap<>(); + if(rootpkg != null) { + lookup.put(rootpkg, root); + } + lookup.put("de.lmu.ifi.dbs.elki", root); + lookup.put("", root); + + // Use the shorthand version of class names. + String prefix = rootpkg != null ? rootpkg + "." : null; + + for(Class<?> impl : choices) { + String name = impl.getName(); + name = (prefix != null && name.startsWith(prefix)) ? name.substring(prefix.length()) : name; + MutableTreeNode c = new ClassNode(impl.getName().substring(impl.getPackage().getName().length() + 1), name); + + MutableTreeNode p = null; + int l = name.lastIndexOf('.'); + while(p == null) { + if(l < 0) { + p = root; + break; + } + String pname = name.substring(0, l); + p = lookup.get(pname); + if(p != null) { + break; + } + l = pname.lastIndexOf('.'); + MutableTreeNode tmp = new PackageNode(l >= 0 ? pname.substring(l + 1) : pname, pname); + tmp.insert(c, 0); + c = tmp; + lookup.put(pname, tmp); + name = pname; + } + p.insert(c, p.getChildCount()); + } + // Simplify tree, except for root node + for(int i = 0; i < root.getChildCount(); i++) { + MutableTreeNode c = (MutableTreeNode) root.getChildAt(i); + MutableTreeNode c2 = simplifyTree(c, null); + if(c != c2) { + root.remove(i); + root.insert(c2, i); + } + } + return root; + } + + /** + * Simplify the tree. + * + * @param cur Current node + * @param prefix Prefix to add + * @return Replacement node + */ + private static MutableTreeNode simplifyTree(MutableTreeNode cur, String prefix) { + if(cur instanceof PackageNode) { + PackageNode node = (PackageNode) cur; + if(node.getChildCount() == 1) { + String newprefix = (prefix != null) ? prefix + "." + (String) node.getUserObject() : (String) node.getUserObject(); + cur = simplifyTree((MutableTreeNode) node.getChildAt(0), newprefix); + } + else { + if(prefix != null) { + node.setUserObject(prefix + "." + (String) node.getUserObject()); + } + for(int i = 0; i < node.getChildCount(); i++) { + MutableTreeNode c = (MutableTreeNode) node.getChildAt(i); + MutableTreeNode c2 = simplifyTree(c, null); + if(c != c2) { + node.remove(i); + node.insert(c2, i); + } + } + } + } + else if(cur instanceof ClassNode) { + ClassNode node = (ClassNode) cur; + if(prefix != null) { + node.setUserObject(prefix + "." + (String) node.getUserObject()); + } + } + return cur; + } + + /** + * Tree node representing a single class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class PackageNode extends DefaultMutableTreeNode { + /** + * Serial version + */ + private static final long serialVersionUID = 1L; + + /** + * Class name. + */ + private String pkgname; + + /** + * Current class name. + * + * @param display Displayed name + * @param pkgname Actual class name + */ + public PackageNode(String display, String pkgname) { + super(display); + this.pkgname = pkgname; + } + + /** + * Return the package name. + * + * @return Package name + */ + public String getPackageName() { + return pkgname; + } + } + + /** + * Tree node representing a single class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class ClassNode extends DefaultMutableTreeNode { + /** + * Serial version + */ + private static final long serialVersionUID = 1L; + + /** + * Class name. + */ + private String clsname; + + /** + * Current class name. + * + * @param display Displayed name + * @param clsname Actual class name + */ + public ClassNode(String display, String clsname) { + super(display); + this.clsname = clsname; + } + + /** + * Return the class name. + * + * @return Class name + */ + public String getClassName() { + return clsname; + } + } +} diff --git a/src/de/lmu/ifi/dbs/elki/gui/util/DynamicParameters.java b/src/de/lmu/ifi/dbs/elki/gui/util/DynamicParameters.java index d974a6d6..2836b33c 100644 --- a/src/de/lmu/ifi/dbs/elki/gui/util/DynamicParameters.java +++ b/src/de/lmu/ifi/dbs/elki/gui/util/DynamicParameters.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.gui.util; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2013 + Copyright (C) 2014 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -28,12 +28,12 @@ import java.util.BitSet; import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID; import de.lmu.ifi.dbs.elki.utilities.optionhandling.ParameterException; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.TrackedParameter; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.SerializedParameterization; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.TrackParameters; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Parameter; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.StringParameter; -import de.lmu.ifi.dbs.elki.utilities.pairs.Pair; /** * Wrapper around a set of parameters for ELKI, that may not yet be complete or @@ -141,8 +141,8 @@ public class DynamicParameters { */ public synchronized void updateFromTrackParameters(TrackParameters track) { parameters.clear(); - for (Pair<Object, Parameter<?>> p : track.getAllParameters()) { - Parameter<?> option = p.getSecond(); + for (TrackedParameter p : track.getAllParameters()) { + Parameter<?> option = p.getParameter(); String value = null; if (option.isDefined()) { if (option.tookDefaultValue()) { diff --git a/src/de/lmu/ifi/dbs/elki/gui/util/LogPane.java b/src/de/lmu/ifi/dbs/elki/gui/util/LogPane.java index 4664b852..e19fd752 100644 --- a/src/de/lmu/ifi/dbs/elki/gui/util/LogPane.java +++ b/src/de/lmu/ifi/dbs/elki/gui/util/LogPane.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.gui.util; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2013 + Copyright (C) 2014 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team diff --git a/src/de/lmu/ifi/dbs/elki/gui/util/LogPanel.java b/src/de/lmu/ifi/dbs/elki/gui/util/LogPanel.java index 11080cc9..99cf50b2 100644 --- a/src/de/lmu/ifi/dbs/elki/gui/util/LogPanel.java +++ b/src/de/lmu/ifi/dbs/elki/gui/util/LogPanel.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.gui.util; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2013 + Copyright (C) 2014 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team diff --git a/src/de/lmu/ifi/dbs/elki/gui/util/ParameterTable.java b/src/de/lmu/ifi/dbs/elki/gui/util/ParameterTable.java index 105242c0..aa94f3ef 100644 --- a/src/de/lmu/ifi/dbs/elki/gui/util/ParameterTable.java +++ b/src/de/lmu/ifi/dbs/elki/gui/util/ParameterTable.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.gui.util; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2013 + Copyright (C) 2014 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -29,18 +29,19 @@ import java.awt.Component; import java.awt.Dimension; import java.awt.FileDialog; import java.awt.Frame; -import java.awt.Insets; -import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; import java.io.File; import java.util.BitSet; import javax.swing.AbstractCellEditor; import javax.swing.Action; import javax.swing.ActionMap; +import javax.swing.BorderFactory; import javax.swing.DefaultCellEditor; +import javax.swing.Icon; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComboBox; @@ -50,11 +51,17 @@ import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; -import javax.swing.plaf.basic.BasicComboPopup; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellEditor; import javax.swing.table.TableColumn; - +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import de.lmu.ifi.dbs.elki.gui.icons.StockIcon; +import de.lmu.ifi.dbs.elki.gui.util.ClassTree.ClassNode; import de.lmu.ifi.dbs.elki.logging.LoggingUtil; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ClassListParameter; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ClassParameter; @@ -134,6 +141,54 @@ public class ParameterTable extends JTable { col1.setPreferredWidth(150); TableColumn col2 = this.getColumnModel().getColumn(1); col2.setPreferredWidth(650); + this.addKeyListener(new Handler()); + + // Increase row height, to make editors usable. + setRowHeight(getRowHeight() + 4); + } + + /** + * Internal key listener. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + protected class Handler implements KeyListener { + @Override + public void keyTyped(KeyEvent e) { + // ignore + } + + @Override + public void keyPressed(KeyEvent e) { + if((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) { + if(e.getKeyCode() == KeyEvent.VK_SPACE // + || e.getKeyCode() == KeyEvent.VK_ENTER // + || e.getKeyCode() == KeyEvent.VK_DOWN // + || e.getKeyCode() == KeyEvent.VK_KP_DOWN) { + final ParameterTable parent = ParameterTable.this; + if(!parent.isEditing()) { + int leadRow = parent.getSelectionModel().getLeadSelectionIndex(); + int leadColumn = parent.getColumnModel().getSelectionModel().getLeadSelectionIndex(); + parent.editCellAt(leadRow, leadColumn); + Component editorComponent = getEditorComponent(); + // This is a hack, to make the content assist open immediately. + if(editorComponent instanceof DispatchingPanel) { + KeyListener[] l = ((DispatchingPanel) editorComponent).component.getKeyListeners(); + for(KeyListener li : l) { + li.keyPressed(e); + } + } + } + } + } + } + + @Override + public void keyReleased(KeyEvent e) { + // ignore + } } /** @@ -156,16 +211,16 @@ public class ParameterTable extends JTable { @Override public void setValue(Object value) { - if (value instanceof String) { + if(value instanceof String) { setText((String) value); setToolTipText(null); return; } - if (value instanceof DynamicParameters.Node) { + if(value instanceof DynamicParameters.Node) { Parameter<?> o = ((DynamicParameters.Node) value).param; // Simulate a tree using indentation - there is no JTreeTable AFAICT StringBuilder buf = new StringBuilder(); - for (int i = 1; i < ((DynamicParameters.Node) value).depth; i++) { + for(int i = 1; i < ((DynamicParameters.Node) value).depth; i++) { buf.append(' '); } buf.append(o.getOptionID().getName()); @@ -180,22 +235,27 @@ public class ParameterTable extends JTable { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - if (!hasFocus) { - if (row < parameters.size()) { + if(!hasFocus) { + if(row < parameters.size()) { BitSet flags = parameters.getNode(row).flags; // TODO: don't hardcode black - maybe mix the other colors, too? c.setForeground(Color.BLACK); - if ((flags.get(DynamicParameters.BIT_INVALID))) { + if((flags.get(DynamicParameters.BIT_INVALID))) { c.setBackground(COLOR_SYNTAX_ERROR); - } else if ((flags.get(DynamicParameters.BIT_SYNTAX_ERROR))) { + } + else if((flags.get(DynamicParameters.BIT_SYNTAX_ERROR))) { c.setBackground(COLOR_SYNTAX_ERROR); - } else if ((flags.get(DynamicParameters.BIT_INCOMPLETE))) { + } + else if((flags.get(DynamicParameters.BIT_INCOMPLETE))) { c.setBackground(COLOR_INCOMPLETE); - } else if ((flags.get(DynamicParameters.BIT_DEFAULT_VALUE))) { + } + else if((flags.get(DynamicParameters.BIT_DEFAULT_VALUE))) { c.setBackground(COLOR_DEFAULT_VALUE); - } else if ((flags.get(DynamicParameters.BIT_OPTIONAL))) { + } + else if((flags.get(DynamicParameters.BIT_OPTIONAL))) { c.setBackground(COLOR_OPTIONAL); - } else { + } + else { c.setBackground(null); } } @@ -209,7 +269,7 @@ public class ParameterTable extends JTable { * * @author Erich Schubert */ - private class DropdownEditor extends DefaultCellEditor { + private class DropdownEditor extends DefaultCellEditor implements KeyListener { /** * Serial Version */ @@ -236,6 +296,32 @@ public class ParameterTable extends JTable { panel = new DispatchingPanel((JComponent) comboBox.getEditor().getEditorComponent()); panel.setLayout(new BorderLayout()); panel.add(comboBox, BorderLayout.CENTER); + comboBox.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3)); + } + + @Override + public void keyTyped(KeyEvent e) { + // Ignore + } + + @Override + public void keyPressed(KeyEvent e) { + if((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) { + if(e.getKeyCode() == KeyEvent.VK_SPACE // + || e.getKeyCode() == KeyEvent.VK_ENTER // + || e.getKeyCode() == KeyEvent.VK_DOWN // + || e.getKeyCode() == KeyEvent.VK_KP_DOWN) { + if(!comboBox.isPopupVisible()) { + comboBox.showPopup(); + e.consume(); + } + } + } + } + + @Override + public void keyReleased(KeyEvent e) { + // Ignore } @Override @@ -244,54 +330,40 @@ public class ParameterTable extends JTable { comboBox.removeAllItems(); // Put the current value in first. Object val = table.getValueAt(row, column); - if (val != null && val instanceof String) { + if(val != null && val instanceof String) { String sval = (String) val; - if (sval.equals(DynamicParameters.STRING_OPTIONAL)) { + if(sval.equals(DynamicParameters.STRING_OPTIONAL)) { sval = ""; } - if (sval.startsWith(DynamicParameters.STRING_USE_DEFAULT)) { + if(sval.startsWith(DynamicParameters.STRING_USE_DEFAULT)) { sval = ""; } - if (sval != "") { + if(sval != "") { comboBox.addItem(sval); comboBox.setSelectedIndex(0); } } - if (row < parameters.size()) { + if(row < parameters.size()) { Parameter<?> option = parameters.getNode(row).param; - // We can do dropdown choices for class parameters - if (option instanceof ClassParameter<?>) { - ClassParameter<?> cp = (ClassParameter<?>) option; - // For parameters with a default value, offer using the default - // For optional parameters, offer not specifying them. - if (cp.hasDefaultValue()) { - comboBox.addItem(DynamicParameters.STRING_USE_DEFAULT + cp.getDefaultValueAsString()); - } else if (cp.isOptional()) { - comboBox.addItem(DynamicParameters.STRING_OPTIONAL); - } - // Offer the shorthand version of class names. - for (Class<?> impl : cp.getKnownImplementations()) { - comboBox.addItem(ClassParameter.canonicalClassName(impl, cp.getRestrictionClass())); - } - } - // and for Flag parameters. - else if (option instanceof Flag) { - if (!Flag.SET.equals(val)) { + // for Flag parameters. + if(option instanceof Flag) { + if(!Flag.SET.equals(val)) { comboBox.addItem(Flag.SET); } - if (!Flag.NOT_SET.equals(val)) { + if(!Flag.NOT_SET.equals(val)) { comboBox.addItem(Flag.NOT_SET); } } // and for Enum parameters. - else if (option instanceof EnumParameter<?>) { + else if(option instanceof EnumParameter<?>) { EnumParameter<?> ep = (EnumParameter<?>) option; - for (String s : ep.getPossibleValues()) { - if (ep.hasDefaultValue() && ep.getDefaultValueAsString().equals(s)) { - if (!(DynamicParameters.STRING_USE_DEFAULT + ep.getDefaultValueAsString()).equals(val)) { + for(String s : ep.getPossibleValues()) { + if(ep.hasDefaultValue() && ep.getDefaultValueAsString().equals(s)) { + if(!(DynamicParameters.STRING_USE_DEFAULT + ep.getDefaultValueAsString()).equals(val)) { comboBox.addItem(DynamicParameters.STRING_USE_DEFAULT + s); } - } else if (!s.equals(val)) { + } + else if(!s.equals(val)) { comboBox.addItem(s); } } @@ -307,7 +379,7 @@ public class ParameterTable extends JTable { * * @author Erich Schubert */ - private class FileNameEditor extends AbstractCellEditor implements TableCellEditor, ActionListener { + private class FileNameEditor extends AbstractCellEditor implements TableCellEditor, ActionListener, KeyListener { /** * Serial version number */ @@ -334,6 +406,11 @@ public class ParameterTable extends JTable { int mode = FileDialog.LOAD; /** + * Default path. + */ + String defaultpath = (new File(".")).getAbsolutePath(); + + /** * Constructor. */ public FileNameEditor() { @@ -342,6 +419,8 @@ public class ParameterTable extends JTable { panel.setLayout(new BorderLayout()); panel.add(textfield, BorderLayout.CENTER); panel.add(button, BorderLayout.EAST); + textfield.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3)); + textfield.addKeyListener(this); } /** @@ -349,38 +428,47 @@ public class ParameterTable extends JTable { */ @Override public void actionPerformed(ActionEvent e) { - final FileDialog fc = new FileDialog(frame); - fc.setDirectory((new File(".")).getAbsolutePath()); + FileDialog fc = new FileDialog(frame); + fc.setDirectory(defaultpath); fc.setMode(mode); final String curr = textfield.getText(); - if (curr != null && curr.length() > 0) { + if(curr != null && curr.length() > 0) { fc.setFile(curr); } fc.setVisible(true); String filename = fc.getFile(); - if (filename != null) { + if(filename != null) { textfield.setText(new File(fc.getDirectory(), filename).getPath()); } + fc.setVisible(false); fc.dispose(); textfield.requestFocus(); - - // Swing file chooser. Currently much worse on Linux/GTK. - // final JFileChooser fc = new JFileChooser(new File(".")); - // final String curr = textfield.getText(); - // if (curr != null && curr.length() > 0) { - // fc.setSelectedFile(new File(curr)); - // } - // int returnVal = fc.showOpenDialog(button); - // - // if(returnVal == JFileChooser.APPROVE_OPTION) { - // textfield.setText(fc.getSelectedFile().getPath()); - // } - // else { - // // Do nothing on cancel. - // } fireEditingStopped(); } + @Override + public void keyTyped(KeyEvent e) { + // Ignore + } + + @Override + public void keyPressed(KeyEvent e) { + if((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) { + if(e.getKeyCode() == KeyEvent.VK_SPACE // + || e.getKeyCode() == KeyEvent.VK_ENTER // + || e.getKeyCode() == KeyEvent.VK_DOWN // + || e.getKeyCode() == KeyEvent.VK_KP_DOWN) { + e.consume(); + actionPerformed(new ActionEvent(e.getSource(), ActionEvent.ACTION_PERFORMED, "assist")); + } + } + } + + @Override + public void keyReleased(KeyEvent e) { + // Ignore + } + /** * Delegate getCellEditorValue to the text field. */ @@ -394,21 +482,16 @@ public class ParameterTable extends JTable { */ @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - if (row < parameters.size()) { + if(row < parameters.size()) { Parameter<?> option = parameters.getNode(row).param; - if (option instanceof FileParameter) { + if(option instanceof FileParameter) { FileParameter fp = (FileParameter) option; File f = null; mode = FileParameter.FileType.INPUT_FILE.equals(fp.getFileType()) ? FileDialog.LOAD : FileDialog.SAVE; - if (fp.isDefined()) { + if(fp.isDefined()) { f = fp.getValue(); } - if (f != null) { - String fn = f.getPath(); - textfield.setText(fn); - } else { - textfield.setText(""); - } + textfield.setText(f != null ? f.getPath() : ""); } } textfield.requestFocus(); @@ -417,11 +500,11 @@ public class ParameterTable extends JTable { } /** - * Editor for selecting input and output file and folders names + * Editor for choosing classes. * * @author Erich Schubert */ - private class ClassListEditor extends AbstractCellEditor implements TableCellEditor, ActionListener { + private class ClassListEditor extends AbstractCellEditor implements TableCellEditor, ActionListener, KeyListener { /** * Serial version number */ @@ -443,30 +526,43 @@ public class ParameterTable extends JTable { final JButton button = new JButton("+"); /** - * The combobox we are abusing to produce the popup + * The popup menu. */ - final JComboBox<String> combo = new JComboBox<>(); + final TreePopup popup; /** - * The popup menu. + * Tree model */ - final SuperPopup popup; + private TreeModel model; + + /** + * Parameter we are currently editing. + */ + private Parameter<?> option; /** * Constructor. */ public ClassListEditor() { + textfield.addKeyListener(this); button.addActionListener(this); - // So the first item doesn't get automatically selected - combo.setEditable(true); - combo.addActionListener(this); - popup = new SuperPopup(combo); + model = new DefaultTreeModel(new DefaultMutableTreeNode()); + popup = new TreePopup(model); + popup.getTree().setRootVisible(false); + popup.addActionListener(this); + + Icon classIcon = StockIcon.getStockIcon(StockIcon.GO_NEXT); + Icon packageIcon = StockIcon.getStockIcon(StockIcon.PACKAGE); + TreePopup.Renderer renderer = (TreePopup.Renderer) popup.getTree().getCellRenderer(); + renderer.setLeafIcon(classIcon); + renderer.setFolderIcon(packageIcon); panel = new DispatchingPanel(textfield); panel.setLayout(new BorderLayout()); panel.add(textfield, BorderLayout.CENTER); panel.add(button, BorderLayout.EAST); + textfield.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3)); } /** @@ -474,80 +570,42 @@ public class ParameterTable extends JTable { */ @Override public void actionPerformed(ActionEvent e) { - if (e.getSource() == button) { + if(e.getSource() == button) { popup.show(panel); - } else if (e.getSource() == combo) { - String newClass = (String) combo.getSelectedItem(); - if (newClass != null && newClass.length() > 0) { - String val = textfield.getText(); - if (val.equals(DynamicParameters.STRING_OPTIONAL)) { - val = ""; - } - if (val.startsWith(DynamicParameters.STRING_USE_DEFAULT)) { - val = ""; - } - if (val.length() > 0) { - val = val + ClassListParameter.LIST_SEP + newClass; - } else { - val = newClass; + return; + } + if(e.getSource() == popup) { + if(e.getActionCommand() == TreePopup.ACTION_CANCELED) { + popup.setVisible(false); + textfield.requestFocus(); + return; + } + TreePath path = popup.getTree().getSelectionPath(); + final Object comp = (path != null) ? path.getLastPathComponent() : null; + if(comp instanceof ClassNode) { + ClassNode sel = (path != null) ? (ClassNode) comp : null; + String newClass = (sel != null) ? sel.getClassName() : null; + if(newClass != null && newClass.length() > 0) { + if(option instanceof ClassListParameter) { + String val = textfield.getText(); + if(val.equals(DynamicParameters.STRING_OPTIONAL) // + || val.startsWith(DynamicParameters.STRING_USE_DEFAULT)) { + val = ""; + } + val = (val.length() > 0) ? val + ClassListParameter.LIST_SEP + newClass : newClass; + textfield.setText(val); + } + else { + textfield.setText(newClass); + } + popup.setVisible(false); + fireEditingStopped(); + textfield.requestFocus(); } - textfield.setText(val); - popup.hide(); } - fireEditingStopped(); - } else { - LoggingUtil.warning("Unrecognized action event in ClassListEditor: " + e); - } - } - - /** - * Modified popup - * - * @author Erich Schubert - * - * @apiviz.exclude - */ - class SuperPopup extends BasicComboPopup { - /** - * Serial version - */ - private static final long serialVersionUID = 1L; - - /** - * Constructor. - * - * @param combo Combo box used for data storage. - */ - public SuperPopup(JComboBox<String> combo) { - super(combo); - } - - /** - * Show the menu on a particular panel. - * - * This code is mostly copied from - * {@link BasicComboPopup#getPopupLocation} - * - * @param parent Parent element to show at. - */ - public void show(JPanel parent) { - Dimension popupSize = parent.getSize(); - Insets insets = getInsets(); - - // reduce the width of the scrollpane by the insets so that the popup - // is the same width as the combo box. - popupSize.setSize(popupSize.width - (insets.right + insets.left), getPopupHeightForRowCount(comboBox.getMaximumRowCount())); - Rectangle popupBounds = computePopupBounds(0, comboBox.getBounds().height, popupSize.width, popupSize.height); - Dimension scrollSize = popupBounds.getSize(); - - scroller.setMaximumSize(scrollSize); - scroller.setPreferredSize(scrollSize); - scroller.setMinimumSize(scrollSize); - - list.revalidate(); - - super.show(parent, 0, parent.getBounds().height); + return; } + LoggingUtil.warning("Unrecognized action event in ClassListEditor: " + e); } /** @@ -558,36 +616,65 @@ public class ParameterTable extends JTable { return textfield.getText(); } + @Override + public void keyTyped(KeyEvent e) { + // Ignore + } + + @Override + public void keyPressed(KeyEvent e) { + if((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) { + if(e.getKeyCode() == KeyEvent.VK_SPACE // + || e.getKeyCode() == KeyEvent.VK_ENTER // + || e.getKeyCode() == KeyEvent.VK_DOWN // + || e.getKeyCode() == KeyEvent.VK_KP_DOWN) { + if(!popup.isVisible()) { + popup.show(ClassListEditor.this.panel); + e.consume(); + } + } + } + } + + @Override + public void keyReleased(KeyEvent e) { + // Ignore + } + /** * Apply the Editor for a selected option. */ @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - combo.removeAllItems(); - if (row < parameters.size()) { - Parameter<?> option = parameters.getNode(row).param; + if(row < parameters.size()) { + this.option = parameters.getNode(row).param; + TreeNode root; // We can do dropdown choices for class parameters - if (option instanceof ClassListParameter<?>) { + if(option instanceof ClassListParameter<?>) { ClassListParameter<?> cp = (ClassListParameter<?>) option; - // Offer the shorthand version of class names. - String prefix = cp.getRestrictionClass().getPackage().getName() + "."; - for (Class<?> impl : cp.getKnownImplementations()) { - String name = impl.getName(); - if (name.startsWith(prefix)) { - name = name.substring(prefix.length()); - } - combo.addItem(name); - } + root = ClassTree.build(cp.getKnownImplementations(), cp.getRestrictionClass().getPackage().getName()); + button.setText("+"); + } + else if(option instanceof ClassParameter<?>) { + ClassParameter<?> cp = (ClassParameter<?>) option; + root = ClassTree.build(cp.getKnownImplementations(), cp.getRestrictionClass().getPackage().getName()); + button.setText("v"); + } + else { + root = new DefaultMutableTreeNode(); } - if (option.isDefined()) { - if (option.tookDefaultValue()) { + if(option.isDefined()) { + if(option.tookDefaultValue()) { textfield.setText(DynamicParameters.STRING_USE_DEFAULT + option.getDefaultValueAsString()); - } else { + } + else { textfield.setText(option.getValueAsString()); } - } else { + } + else { textfield.setText(""); } + popup.getTree().setModel(new DefaultTreeModel(root)); } return panel; } @@ -642,14 +729,16 @@ public class ParameterTable extends JTable { final JComboBox<String> combobox = new JComboBox<>(); combobox.setEditable(true); this.dropdownEditor = new DropdownEditor(combobox); - this.plaintextEditor = new DefaultCellEditor(new JTextField()); + JTextField tf = new JTextField(); + tf.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3)); + this.plaintextEditor = new DefaultCellEditor(tf); this.classListEditor = new ClassListEditor(); this.fileNameEditor = new FileNameEditor(); } @Override public Object getCellEditorValue() { - if (activeEditor == null) { + if(activeEditor == null) { return null; } return activeEditor.getCellEditorValue(); @@ -657,31 +746,31 @@ public class ParameterTable extends JTable { @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - if (value instanceof String) { + if(value instanceof String) { String s = (String) value; - if (s.startsWith(DynamicParameters.STRING_USE_DEFAULT)) { + if(s.startsWith(DynamicParameters.STRING_USE_DEFAULT)) { value = s.substring(DynamicParameters.STRING_USE_DEFAULT.length()); } } - if (row < parameters.size()) { + if(row < parameters.size()) { Parameter<?> option = parameters.getNode(row).param; - if (option instanceof Flag) { + if(option instanceof Flag) { activeEditor = dropdownEditor; return dropdownEditor.getTableCellEditorComponent(table, value, isSelected, row, column); } - if (option instanceof ClassListParameter<?>) { + if(option instanceof ClassListParameter<?>) { activeEditor = classListEditor; return classListEditor.getTableCellEditorComponent(table, value, isSelected, row, column); } - if (option instanceof ClassParameter<?>) { - activeEditor = dropdownEditor; - return dropdownEditor.getTableCellEditorComponent(table, value, isSelected, row, column); + if(option instanceof ClassParameter<?>) { + activeEditor = classListEditor; + return classListEditor.getTableCellEditorComponent(table, value, isSelected, row, column); } - if (option instanceof FileParameter) { + if(option instanceof FileParameter) { activeEditor = fileNameEditor; return fileNameEditor.getTableCellEditorComponent(table, value, isSelected, row, column); } - if (option instanceof EnumParameter<?>) { + if(option instanceof EnumParameter<?>) { activeEditor = dropdownEditor; return dropdownEditor.getTableCellEditorComponent(table, value, isSelected, row, column); } @@ -733,10 +822,10 @@ public class ParameterTable extends JTable { InputMap map = component.getInputMap(condition); ActionMap am = component.getActionMap(); - if (map != null && am != null && isEnabled()) { + if(map != null && am != null && isEnabled()) { Object binding = map.get(ks); Action action = (binding == null) ? null : am.get(binding); - if (action != null) { + if(action != null) { return SwingUtilities.notifyAction(action, ks, e, component, e.getModifiers()); } } diff --git a/src/de/lmu/ifi/dbs/elki/gui/util/ParametersModel.java b/src/de/lmu/ifi/dbs/elki/gui/util/ParametersModel.java index 42f0a1af..82a0ee74 100644 --- a/src/de/lmu/ifi/dbs/elki/gui/util/ParametersModel.java +++ b/src/de/lmu/ifi/dbs/elki/gui/util/ParametersModel.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.gui.util; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2013 + Copyright (C) 2014 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team diff --git a/src/de/lmu/ifi/dbs/elki/gui/util/SavedSettingsFile.java b/src/de/lmu/ifi/dbs/elki/gui/util/SavedSettingsFile.java index a99b7414..8e614b85 100644 --- a/src/de/lmu/ifi/dbs/elki/gui/util/SavedSettingsFile.java +++ b/src/de/lmu/ifi/dbs/elki/gui/util/SavedSettingsFile.java @@ -4,7 +4,7 @@ package de.lmu.ifi.dbs.elki.gui.util; This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures - Copyright (C) 2013 + Copyright (C) 2014 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team diff --git a/src/de/lmu/ifi/dbs/elki/gui/util/TreePopup.java b/src/de/lmu/ifi/dbs/elki/gui/util/TreePopup.java new file mode 100644 index 00000000..60ad0604 --- /dev/null +++ b/src/de/lmu/ifi/dbs/elki/gui/util/TreePopup.java @@ -0,0 +1,412 @@ +package de.lmu.ifi.dbs.elki.gui.util; + +/* + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures + + Copyright (C) 2014 + Ludwig-Maximilians-Universität München + Lehr- und Forschungseinheit für Datenbanksysteme + ELKI Development Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GraphicsConfiguration; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.BoxLayout; +import javax.swing.Icon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.LineBorder; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreeModel; + +/** + * Popup menu that contains a JTree. + * + * @author Erich Schubert + */ +public class TreePopup extends JPopupMenu { + /** + * Serialization version. + */ + private static final long serialVersionUID = 1L; + + /** + * Action string for confirmed operations (enter or click). + */ + public static final String ACTION_SELECTED = "selected"; + + /** + * Action string for canceled operations (escape button pressed). + */ + public static final String ACTION_CANCELED = "canceled"; + + /** + * Tree. + */ + protected JTree tree; + + /** + * Scroll pane, containing the tree. + */ + protected JScrollPane scroller; + + /** + * Tree model. + */ + private TreeModel model; + + /** + * Event handler + */ + private Handler handler = new Handler(); + + /** + * Border of the popup. + */ + private static Border TREE_BORDER = new LineBorder(Color.BLACK, 1); + + /** + * Constructor with an empty tree model. + * + * This needs to also add a root node, and therefore sets + * {@code getTree().setRootVisible(false)}. + */ + public TreePopup() { + this(new DefaultTreeModel(new DefaultMutableTreeNode())); + tree.setRootVisible(false); + } + + /** + * Constructor. + * + * @param model Tree model + */ + public TreePopup(TreeModel model) { + super(); + this.setName("TreePopup.popup"); + this.model = model; + + // UI construction of the popup. + tree = createTree(); + scroller = createScroller(); + configurePopup(); + } + + /** + * Creates the JList used in the popup to display the items in the combo box + * model. This method is called when the UI class is created. + * + * @return a <code>JList</code> used to display the combo box items + */ + protected JTree createTree() { + JTree tree = new JTree(model); + tree.setName("TreePopup.tree"); + tree.setFont(getFont()); + tree.setForeground(getForeground()); + tree.setBackground(getBackground()); + tree.setBorder(null); + tree.setFocusable(true); + tree.addMouseListener(handler); + tree.addKeyListener(handler); + tree.setCellRenderer(new Renderer()); + return tree; + } + + /** + * Configure the popup display. + */ + protected void configurePopup() { + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + setBorderPainted(true); + setBorder(TREE_BORDER); + setOpaque(false); + add(scroller); + setDoubleBuffered(true); + setFocusable(false); + } + + /** + * Creates the scroll pane which houses the scrollable tree. + */ + protected JScrollPane createScroller() { + JScrollPane sp = new JScrollPane(tree, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + sp.setHorizontalScrollBar(null); + sp.setName("TreePopup.scrollPane"); + sp.setFocusable(false); + sp.getVerticalScrollBar().setFocusable(false); + sp.setBorder(null); + return sp; + } + + /** + * Access the tree contained. + * + * @return Tree + */ + public JTree getTree() { + return tree; + } + + /** + * Display the popup, attached to the given component. + * + * @param parent Parent component + */ + public void show(Component parent) { + Dimension parentSize = parent.getSize(); + Insets insets = getInsets(); + + // reduce the width of the scrollpane by the insets so that the popup + // is the same width as the combo box. + parentSize.setSize(parentSize.width - (insets.right + insets.left), 10 * parentSize.height); + Dimension scrollSize = computePopupBounds(parent, 0, getBounds().height, parentSize.width, parentSize.height).getSize(); + + scroller.setMaximumSize(scrollSize); + scroller.setPreferredSize(scrollSize); + scroller.setMinimumSize(scrollSize); + + super.show(parent, 0, parent.getHeight()); + tree.requestFocusInWindow(); + } + + protected Rectangle computePopupBounds(Component parent, int px, int py, int pw, int ph) { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Rectangle screenBounds; + + // Calculate the desktop dimensions relative to the combo box. + GraphicsConfiguration gc = parent.getGraphicsConfiguration(); + Point p = new Point(); + SwingUtilities.convertPointFromScreen(p, parent); + if(gc != null) { + Insets screenInsets = toolkit.getScreenInsets(gc); + screenBounds = gc.getBounds(); + screenBounds.width -= (screenInsets.left + screenInsets.right); + screenBounds.height -= (screenInsets.top + screenInsets.bottom); + screenBounds.x += (p.x + screenInsets.left); + screenBounds.y += (p.y + screenInsets.top); + } + else { + screenBounds = new Rectangle(p, toolkit.getScreenSize()); + } + + Rectangle rect = new Rectangle(px, py, pw, ph); + if(py + ph > screenBounds.y + screenBounds.height && ph < screenBounds.height) { + rect.y = -rect.height; + } + return rect; + } + + /** + * Register an action listener. + * + * @param listener Action listener + */ + public void addActionListener(ActionListener listener) { + listenerList.add(ActionListener.class, listener); + } + + /** + * Unregister an action listener. + * + * @param listener Action listener + */ + public void removeActionListener(ActionListener listener) { + listenerList.remove(ActionListener.class, listener); + } + + /** + * Notify action listeners. + * + * @param event the <code>ActionEvent</code> object + */ + protected void fireActionPerformed(ActionEvent event) { + Object[] listeners = listenerList.getListenerList(); + for(int i = listeners.length - 2; i >= 0; i -= 2) { + if(listeners[i] == ActionListener.class) { + ((ActionListener) listeners[i + 1]).actionPerformed(event); + } + } + } + + /** + * Tree cell render. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public class Renderer extends JPanel implements TreeCellRenderer { + /** + * Serial version + */ + private static final long serialVersionUID = 1L; + + /** + * Label to render + */ + JLabel label; + + /** + * Colors + */ + private Color selbg, defbg, selfg, deffg; + + /** + * Icons + */ + private Icon leafIcon, folderIcon; + + /** + * Constructor. + */ + protected Renderer() { + selbg = UIManager.getColor("Tree.selectionBackground"); + defbg = UIManager.getColor("Tree.textBackground"); + selfg = UIManager.getColor("Tree.selectionForeground"); + deffg = UIManager.getColor("Tree.textForeground"); + + setLayout(new BorderLayout()); + add(label = new JLabel("This should never be rendered.")); + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { + label.setText((String) ((DefaultMutableTreeNode) value).getUserObject()); + setForeground(selected ? selfg : deffg); + setBackground(selected ? selbg : defbg); + label.setIcon(leaf ? leafIcon : folderIcon); + setPreferredSize(new Dimension(1000, label.getPreferredSize().height)); + return this; + } + + /** + * Set the leaf icon + * + * @param leafIcon Leaf icon + */ + public void setLeafIcon(Icon leafIcon) { + this.leafIcon = leafIcon; + } + + /** + * Set the folder icon. + * + * @param folderIcon Folder icon + */ + public void setFolderIcon(Icon folderIcon) { + this.folderIcon = folderIcon; + } + } + + /** + * Event handler class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + protected class Handler implements MouseListener, KeyListener, FocusListener { + @Override + public void keyTyped(KeyEvent e) { + if(e.getKeyChar() == '\n') { + e.consume(); + } + } + + @Override + public void keyPressed(KeyEvent e) { + if(e.getKeyCode() == KeyEvent.VK_ENTER) { + fireActionPerformed(new ActionEvent(TreePopup.this, ActionEvent.ACTION_PERFORMED, ACTION_SELECTED, e.getWhen(), e.getModifiers())); + e.consume(); + return; + } + if(e.getKeyCode() == KeyEvent.VK_ESCAPE) { + fireActionPerformed(new ActionEvent(TreePopup.this, ActionEvent.ACTION_PERFORMED, ACTION_CANCELED, e.getWhen(), e.getModifiers())); + } + } + + @Override + public void keyReleased(KeyEvent e) { + if(e.getKeyCode() == KeyEvent.VK_ENTER) { + e.consume(); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + if(e.getButton() == MouseEvent.BUTTON1) { + fireActionPerformed(new ActionEvent(TreePopup.this, ActionEvent.ACTION_PERFORMED, ACTION_SELECTED, e.getWhen(), e.getModifiers())); + } + // ignore + } + + @Override + public void mousePressed(MouseEvent e) { + // ignore + } + + @Override + public void mouseReleased(MouseEvent e) { + // ignore + } + + @Override + public void mouseEntered(MouseEvent e) { + // ignore + } + + @Override + public void mouseExited(MouseEvent e) { + // ignore + } + + @Override + public void focusGained(FocusEvent e) { + // ignore + } + + @Override + public void focusLost(FocusEvent e) { + fireActionPerformed(new ActionEvent(TreePopup.this, ActionEvent.ACTION_PERFORMED, ACTION_CANCELED)); + } + } +} diff --git a/src/de/lmu/ifi/dbs/elki/gui/util/package-info.java b/src/de/lmu/ifi/dbs/elki/gui/util/package-info.java index 706f587a..55f3d259 100644 --- a/src/de/lmu/ifi/dbs/elki/gui/util/package-info.java +++ b/src/de/lmu/ifi/dbs/elki/gui/util/package-info.java @@ -5,7 +5,7 @@ This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures -Copyright (C) 2013 +Copyright (C) 2014 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team |