/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * 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 com.rapidminer.gui.tools; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.FocusTraversalPolicy; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import javax.swing.AbstractAction; import javax.swing.BoxLayout; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import com.rapidminer.gui.look.Colors; /** * This {@link JPopupMenu} extension displays its contents in a scrollpane. The maximum height can * be set as well as a custom width of this popup menu. Furthermore, focus traversal via TAB works * for any {@link Component} added to this popupmenu, not only for {@link JMenuItem}s. * * @author Marco Boeck * */ public class ScrollableJPopupMenu extends JPopupMenu { private static final long serialVersionUID = 7440641917394853639L; private static final int INSETS = 5; public static final int SIZE_TINY = 100; public static final int SIZE_SMALL = 200; public static final int SIZE_NORMAL = 400; public static final int SIZE_LARGE = 600; public static final int SIZE_HUGE = 800; /** the scrollpane which allows scrolling */ private JScrollPane scrollPane; /** the inner panel which houses all components */ private JPanel innerPanel; /** the title text for the menu, displayed above scrollpane */ private String title; /** the max height in pixel for the scrollpane */ private int maxHeight; /** the max width in pixel for the scrollpane */ private int maxWidth; /** if not null, will be used to determine the width of the scrollpane */ private Integer customWidth; /** * Creates a new {@link ScrollJPopupMenu} instance with the default max height. * */ public ScrollableJPopupMenu() { this(null, SIZE_NORMAL); } /** * Creates a new {@link ScrollJPopupMenu} instance with the specified max height. * * @param maxHeight */ public ScrollableJPopupMenu(int maxHeight) { this(null, maxHeight); } /** * Creates a new {@link ScrollJPopupMenu} instance with the specified title. * * @param title */ public ScrollableJPopupMenu(String title) { this(title, SIZE_NORMAL); } /** * Creates a new {@link ScrollJPopupMenu} instance with the specified max height and title. * * @param title * @param maxHeight */ public ScrollableJPopupMenu(String title, int maxHeight) { super(); if (maxHeight < SIZE_TINY) { throw new IllegalArgumentException("size must not be smaller than " + SIZE_TINY); } this.title = title; this.maxHeight = maxHeight; this.maxWidth = 800; initGUI(); } /** * Initializes the GUI. */ private void initGUI() { innerPanel = new JPanel(); innerPanel.setBackground(Colors.MENU_ITEM_BACKGROUND); innerPanel.setLayout(new BoxLayout(this.innerPanel, BoxLayout.Y_AXIS)); scrollPane = new ExtendedJScrollPane(innerPanel); scrollPane.setBorder(null); scrollPane.setMaximumSize(new Dimension(maxWidth, maxHeight)); // allows closing of popup via Escape (which consumes the event) getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "close"); getActionMap().put("close", new AbstractAction() { private static final long serialVersionUID = 6744591919263755414L; @Override public void actionPerformed(ActionEvent e) { ScrollableJPopupMenu.this.setVisible(false); } }); // add optional title if (title != null) { super.add(new JLabel(title)); } // add scrollpane super.add(scrollPane); // set focus policy so we only cycle through components in our scrollpane setFocusTraversalPolicyProvider(true); setFocusTraversalPolicy(new FocusTraversalPolicy() { @Override public Component getLastComponent(Container aContainer) { if (innerPanel.getComponentCount() <= 0) { return null; } return innerPanel.getComponent(innerPanel.getComponentCount() - 1); } @Override public Component getFirstComponent(Container aContainer) { if (innerPanel.getComponentCount() <= 0) { return null; } return innerPanel.getComponent(0); } @Override public Component getDefaultComponent(Container aContainer) { return getFirstComponent(aContainer); } @Override public Component getComponentBefore(Container aContainer, Component aComponent) { if (innerPanel.getComponentCount() <= 0) { return null; } for (int i = 0; i < innerPanel.getComponentCount(); i++) { if (innerPanel.getComponent(i) == aComponent) { if (i == 0) { return getLastComponent(aContainer); } Component previousComp = innerPanel.getComponent(i - 1); // needed so focus cyling does not stop at a separator if (previousComp instanceof Separator) { return getComponentBefore(aContainer, previousComp); } else { return previousComp; } } } return getFirstComponent(aContainer); } @Override public Component getComponentAfter(Container aContainer, Component aComponent) { if (innerPanel.getComponentCount() <= 0) { return null; } for (int i = 0; i < innerPanel.getComponentCount(); i++) { if (innerPanel.getComponent(i) == aComponent) { if (i == innerPanel.getComponentCount() - 1) { return getFirstComponent(aContainer); } Component nextComp = innerPanel.getComponent(i + 1); // needed so focus cyling does not stop at a separator if (nextComp instanceof Separator) { return getComponentAfter(aContainer, nextComp); } else { return nextComp; } } } return getLastComponent(aContainer); } }); } @Override public Component add(final Component comp) { innerPanel.add(comp); resizeScrollPane(); return comp; } @Override public void remove(Component comp) { innerPanel.remove(comp); resizeScrollPane(); } /** * Updates the preferred size of the scrollpane depending on the components of this popup menu. */ private void resizeScrollPane() { int width = customWidth == null ? innerPanel.getPreferredSize().width + scrollPane.getVerticalScrollBar().getPreferredSize().width + INSETS : customWidth - INSETS; scrollPane .setPreferredSize(new Dimension(width, Math.min(maxHeight, innerPanel.getPreferredSize().height + INSETS))); } /** * Sets the fixed custom width. If set to <code>null</code>, will not use a fixed width. * * @param customWidth */ public void setCustomWidth(Integer customWidth) { this.customWidth = customWidth; resizeScrollPane(); } /** * Returns all {@link Component}s inside the scrollpane. * * @return */ public Component[] getComponentsInsideScrollpane() { return innerPanel.getComponents(); } /** * Requets the focus on the first component inside the scrollpane. Does nothing if no components * exist. * * @return {@link Component#requestFocusInWindow()} */ private boolean requestFocusForFirstComponent() { if (innerPanel.getComponentCount() > 0) { return innerPanel.getComponent(0).requestFocusInWindow(); } return false; } @Override public boolean requestFocusInWindow() { return requestFocusForFirstComponent(); } }