/* * This file is part of ELKI: * Environment for Developing KDD-Applications Supported by Index-Structures * * Copyright (C) 2017 * 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/>. */ package de.lmu.ifi.dbs.elki.gui.util; 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 * @since 0.7.0 */ 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)); } } }