diff options
Diffstat (limited to 'src/de/lmu/ifi/dbs/elki/gui/util/TreePopup.java')
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/gui/util/TreePopup.java | 412 |
1 files changed, 412 insertions, 0 deletions
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)); + } + } +} |