/******************************************************************************** * * * (c) Copyright 2010 Verizon Communications USA and The Open University UK * * * * This software is freely distributed in accordance with * * the GNU Lesser General Public (LGPL) license, version 3 or later * * as published by the Free Software Foundation. * * For details see LGPL: http://www.fsf.org/licensing/licenses/lgpl.html * * and GPL: http://www.fsf.org/licensing/licenses/gpl-3.0.html * * * * This software is provided by the copyright holders and contributors "as is" * * and any express or implied warranties, including, but not limited to, the * * implied warranties of merchantability and fitness for a particular purpose * * are disclaimed. In no event shall the copyright owner or contributors be * * liable for any direct, indirect, incidental, special, exemplary, or * * consequential damages (including, but not limited to, procurement of * * substitute goods or services; loss of use, data, or profits; or business * * interruption) however caused and on any theory of liability, whether in * * contract, strict liability, or tort (including negligence or otherwise) * * arising in any way out of the use of this software, even if advised of the * * possibility of such damage. * * * ********************************************************************************/ package com.compendium.ui; import java.awt.BorderLayout; import java.awt.Component; import java.awt.ComponentOrientation; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Point; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionAdapter; import java.util.TimerTask; import java.util.Vector; import javax.swing.Action; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JViewport; import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.ToolTipManager; import javax.swing.border.EmptyBorder; import com.compendium.LanguageProperties; /** * This class implements a scrollable menu * * @author Michelle Bachler */ public class UIScrollableMenu extends JMenu { /** The default length of the menu before using scrolling*/ private static final int MENU_LENGTH = 15; /** All the menu items in the menu.*/ private Vector menuitems = null; /** The menu item in the scrollable part of the menu.*/ private Vector listitems = null; /** A count of the separators adding to this menu.*/ private Vector vtSeparators = null; /** Holds the length the menu can be before implementing scrolling.*/ private int menuLength = 15; /** Holds the position of the item from which scrolling should start.*/ private int scrollStart = 0; /** The visual delay when scrolling.*/ private int scrollDelay = 300; /** Are we scrolling at present?*/ private boolean scroll = false; /** Have sufficient items been added to implement scrolling?*/ private boolean listAdded = false; /** Holds the up arrow.*/ private JPanel upArrow = null; /** Holds the down arrow.*/ private JPanel downArrow = null; /** Holds the up arrow icon.*/ private JLabel up = null; /** holds the down arrow item.*/ private JLabel down = null; /** The list that is the scrollable section of the menu.*/ private JList list = null; /** The viewport of the scrollable section of the menu.*/ private JViewport view = null; /** The scollpane for the menu.*/ private JScrollPane scrollpane = null; /** Used for autoscrolling when dragged nodes hit the viewport edge.*/ //private java.util.Timer timer = null; /** * Constructor. Initializes the menu. */ public UIScrollableMenu () { super(); init(); } /** * Constructs a new <code>UIScrollableMenu</code> with the supplied string as its text. * * @param s the text for the menu label. * @param b can the menu be torn off (not yet implemented). */ public UIScrollableMenu(String s, boolean tearOff) { this(s, 0, MENU_LENGTH, tearOff); } /** * Constructs a new <code>UIScrollableMenu</code> with the supplied string as its text. * * @param s the text for the menu label. * @param scrollStart, the position to start the scroll from. */ public UIScrollableMenu(String s, int scrollStart) { this(s, scrollStart, MENU_LENGTH, false); } /** * Constructs a new <code>UIScrollableMenu</code> with the supplied string as its text. * * @param s the text for the menu label. * @param scrollStart, the position to start the scroll from. * @param b can the menu be torn off (not yet implemented). */ public UIScrollableMenu(String s, int scrollStart, boolean tearOff) { this(s, scrollStart, MENU_LENGTH, tearOff); } /** * Constructs a new <code>UIScrollableMenu</code> with the supplied string as its text. * * @param s the text for the menu label. * @param scrollStart, the position to start the scroll from. * @param menuLength, the lenght of the visible menu before you start using scrolling. */ public UIScrollableMenu(String s, int scrollStart, int menuLength) { this(s, scrollStart, menuLength, false); } /** * Constructs a new <code>UIScrollableMenu</code> with the supplied string as its text * * @param s the text for the menu label. * @param scrollStart, the position to start the scroll from. * @param menuLength, the lenght of the visible menu before you start using scrolling. * @param b can the menu be torn off (not yet implemented) */ public UIScrollableMenu(String s, int scrollStart, int menuLength, boolean tearOff) { super(s, tearOff); if (menuLength > 2) { this.menuLength = menuLength; } if (scrollStart > 0 && scrollStart < menuLength) this.scrollStart = scrollStart; init(); } /** * Constructs a menu whose properties are taken from the <code>Action</code> supplied. * @param a an <code>Action</code> */ public UIScrollableMenu(Action a) { super(a); init(); } /** * Initialize and draw the Menu. */ private void init() { menuitems = new Vector(20); listitems = new Vector(20); vtSeparators = new Vector(); this.setDoubleBuffered(true); JMenuItem test = new JMenuItem(); upArrow = new JPanel(new BorderLayout()); upArrow.setBorder(new EmptyBorder(4,4,4,4)); upArrow.setBackground(test.getBackground()); up = new JLabel(UIImages.get(IUIConstants.UP_ARROW_ICON)); up.addMouseListener(createMouseAdapter()); up.setHorizontalAlignment(SwingConstants.CENTER); up.setEnabled(false); upArrow.add(up, BorderLayout.CENTER); downArrow = new JPanel(new BorderLayout()); downArrow.setBorder(new EmptyBorder(4,4,4,4)); downArrow.setBackground(test.getBackground()); down = new JLabel(UIImages.get(IUIConstants.DOWN_ARROW_ICON)); down.addMouseListener(createMouseAdapter()); down.setHorizontalAlignment(SwingConstants.CENTER); downArrow.add(down,BorderLayout.CENTER); list = new JList(); list.setVisibleRowCount(menuLength); list.setBackground(test.getBackground()); list.setCellRenderer(new ScrollListCellRenderer()); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); ToolTipManager.sharedInstance().registerComponent(list); list.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent evt) { JList lst = (JList)evt.getSource(); JMenuItem item = (JMenuItem)lst.getSelectedValue(); if (item != null) { item.doClick(); setPopupMenuVisible(false); } } public void mouseExited(MouseEvent evt) { list.clearSelection(); } }); list.addMouseMotionListener(new MouseMotionAdapter() { public void mouseMoved(MouseEvent evt) { final MouseEvent event = evt; Thread thread = new Thread("UIScrollableMenu.list mouse event") { //$NON-NLS-1$ public void run() { JList lst = (JList)event.getSource(); int x = event.getX(); int y = event.getY(); int index = lst.locationToIndex(new Point(x, y)); list.setSelectedIndex(index); } }; thread.start(); } }); view = new JViewport(); view.setView(list); scrollpane = new JScrollPane(); scrollpane.setViewport(view); scrollpane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); scrollpane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scrollpane.setBorder(null); } /** * Create and return the <code>MouseListener</code> for this menu. * This controls the scrolling. * @return MouseListener, the <code>MouseListener</code> for this menu. */ private MouseListener createMouseAdapter() { MouseListener mouseAdapter = new MouseAdapter() { public void mouseClicked(MouseEvent evt) { JLabel item = (JLabel)evt.getSource(); if (item.equals(up)) { scrollUp(); } else { scrollDown(); } } // CAUSED FLICKER IN MAPS AFTER MENU CLOSED /*public void mouseEntered(MouseEvent evt) { JLabel item = (JLabel)evt.getSource(); timer = new java.util.Timer(); ScrollMenu task = new ScrollMenu(item.equals(up)); timer.schedule(task, new Date(), scrollDelay); } public void mouseExited(MouseEvent evt) { if (timer != null) { timer.cancel(); } }*/ }; return mouseAdapter; } /** * This inner class is used to perform the autoscrolling of nodes when they hit the edge of the view. */ private class ScrollMenu extends TimerTask { boolean scrollUp = false; public ScrollMenu(boolean scroll) { scrollUp = scroll; } public void run() { if (scrollUp) { scrollUp(); } else { scrollDown(); } } } private void scrollDown() { try { int top = list.getFirstVisibleIndex(); int bottom = list.getLastVisibleIndex(); int items = listitems.size()-1; // IF WE ARE SCROLLING DOWN FROM 0, ENABLE UP ARROW if (top == 0) { up.setEnabled(true); } // IF WE ARE DRAWING THE BOTTOM ITEM, DISABLE DOWN ARROW if (bottom == items) { down.setEnabled(false); } if (bottom < items) { try { list.ensureIndexIsVisible(bottom+1); list.repaint(); } catch(Exception e) {} } } catch(Exception io) { io.printStackTrace(); } } private void scrollUp() { try { int top = list.getFirstVisibleIndex(); int bottom = list.getLastVisibleIndex(); int items = listitems.size()-1; top = list.getFirstVisibleIndex(); bottom = list.getLastVisibleIndex(); // IF WE ARE SCROLLING UP FROM BOTTOM, ENABLE DOWN ARROW if (!down.isEnabled() && bottom < items) { down.setEnabled(true); } // IF WE ARE DRAWING THE TOPITEM, DISABLE UP ARROW if (top == 1 && up.isEnabled()) { up.setEnabled(false); } if (top > 0) { try { list.ensureIndexIsVisible(top-1); list.repaint(); } catch(Exception e) {} } } catch(Exception io) { io.printStackTrace(); } } /* * Set the time delay by which the scroll action waits before scrolling to the next menu item * int delay, the delay between scrolling between menu items, in milliseconds */ public void setScrollDelay(int delay) { scrollDelay =delay; } // ADD METHODS /** * Appends a new separator to the end of the menu. */ public void addSeparator() { JSeparator sep = new JSeparator(); if (menuitems.size() < scrollStart) { super.add(sep); menuitems.addElement(sep); } else { listitems.addElement(sep); vtSeparators.add(sep); if (!listAdded) { super.add(upArrow); super.add(scrollpane); super.add(downArrow); listAdded = true; } } } /** * Appends a menu item to the end of this menu. * Returns the menu item added. * * @param menuItem the <code>JMenuitem</code> to be added * @return the <code>JMenuItem</code> added */ public JMenuItem add(JMenuItem menuItem) { if (menuitems.size() < scrollStart) { super.add(menuItem); menuitems.addElement(menuItem); } else { listitems.addElement(menuItem); if (!listAdded) { super.add(upArrow); super.add(scrollpane); super.add(downArrow); listAdded = true; } } return menuItem; } /** * Creates a new menu item attached to the specified * <code>Action</code> object and appends it to the end of this menu. * As of 1.3, this is no longer the preferred method for adding * <code>Actions</code> to * a container. Instead it is recommended to configure a control with * an action using <code>setAction</code>, * and then add that control directly * to the <code>Container</code>. * * @param a the <code>Action</code> for the menu item to be added * @see Action */ public JMenuItem add(Action a) { JMenuItem mi = createActionComponent(a); mi.setAction(a); add(mi); return mi; } /** * Creates a new menu item with the specified text and appends * it to the end of this menu. * * @param s the string for the menu item to be added */ public JMenuItem add(String s) { return add(new JMenuItem(s)); } /** * Adds the specified component to this container at the given * position. If <code>index</code> equals -1, the component will * be appended to the end. * @param c the <code>Component</code> to add * @param index the position at which to insert the component * @return the <code>Component</code> added * @see #remove * @see java.awt.Container#add(Component, int) */ public Component add(Component c, int index) throws IllegalArgumentException { if (index < 0) { throw new IllegalArgumentException(LanguageProperties.getString(LanguageProperties.UI_GENERAL_BUNDLE, "UIScrollableMenu.indexLessThanZero")); //$NON-NLS-1$ } if (index < scrollStart) { super.add(c, index); menuitems.insertElementAt(c, index); // IF THIS ITEM GOES BEYOND THE SCROLL START POSITION, MOVE THE EXTRA ITEMS if (menuitems.size() >= scrollStart) { int insertIndex = 0; for (int i=scrollStart-1; i<menuitems.size(); i++) { listitems.insertElementAt(menuitems.elementAt(i), insertIndex); insertIndex++; } for (int j=scrollStart-1; j<menuitems.size(); j++) { menuitems.removeElementAt(j); j--; } } } else { listitems.insertElementAt(c, index); if (!listAdded) { super.add(upArrow); super.add(scrollpane); super.add(downArrow); listAdded = true; } } return c; } /** * Appends a component to the end of this menu. * Returns the component added. * * @param c the <code>Component</code> to add * @return the <code>Component</code> added */ public Component add(Component c) { if (menuitems.size() < scrollStart) { super.add(c); menuitems.addElement(c); } else { listitems.addElement(c); if (!listAdded) { super.add(upArrow); super.add(scrollpane); super.add(downArrow); listAdded = true; } } return c; } // INSERT METHODS /** * Inserts a new menu item with the specified text at a * given position. * * @param s the text for the menu item to add * @param pos an integer specifying the position at which to add the * new menu item * @exception IllegalArgumentException if the value of * <code>pos</code> < 0 */ public void insert(String s, int pos) throws IllegalArgumentException { add(new JMenuItem(s), pos); } /** * Inserts the specified <code>JMenuitem</code> at a given position. * * @param mi the <code>JMenuitem</code> to add * @param pos an integer specifying the position at which to add the * new <code>JMenuitem</code> * @exception IllegalArgumentException if the value of * <code>pos</code> < 0 * @return the new menu item */ public JMenuItem insert(JMenuItem mi, int pos) throws IllegalArgumentException { return (JMenuItem)add(mi, pos); } /** * Inserts a new menu item attached to the specified <code>Action</code> * object at a given position. * * @param a the <code>Action</code> object for the menu item to add * @param pos an integer specifying the position at which to add the * new menu item * @exception IllegalArgumentException if the value of * <code>pos</code> < 0 */ public JMenuItem insert(Action a, int pos) throws IllegalArgumentException { JMenuItem mi = createActionComponent(a); mi.setAction(a); return (JMenuItem)add(mi, pos); } /** * Inserts a separator at the specified position. * * @param pos an integer specifying the position at which to * insert the menu separator * @exception IllegalArgumentException if the value of * <code>pos</code> < 0 */ public void insertSeparator(int pos) throws IllegalArgumentException { JSeparator sep = new JSeparator(); add(sep, pos); } // REMOVE METHODS /** * Removes the specified menu item from this menu. If there is no * popup menu, this method will have no effect. * * @param item the <code>JMenuItem</code> to be removed from the menu */ public void remove(JMenuItem item) { int count = 0; if (item != null) { if (!listitems.remove(item)) { menuitems.remove(item); super.remove(item); } } } /** * Removes the menu item at the specified index from this menu. * * @param pos the position of the item to be removed * @exception IllegalArgumentException if the value of * <code>pos</code> < 0, or if <code>pos</code> * is greater than the number of menu items */ public void remove(int pos) { if (pos < scrollStart) { if (pos < menuitems.size() && pos >= 0) { menuitems.removeElementAt(pos); super.remove(pos); } } else { pos = pos-scrollStart; if (pos < listitems.size() && pos >= 0) { listitems.removeElementAt(pos); } } } /** * Removes the component <code>c</code> from this menu. * * @param c the component to be removed */ public void remove(Component c) { int count = 0; if (!listitems.remove(c)) { menuitems.remove(c); super.remove(c); //ADD IN LOWER ITEM TO MAKE COUNT BACK UP TO SCROLLSTART } else { vtSeparators.remove(c); } } /** * Removes all menu items from this menu. */ public void removeAll() { super.removeAll(); menuitems.removeAllElements(); listitems.removeAllElements(); vtSeparators.removeAllElements(); listAdded = false; } // OTHER METHODS /** * Returns the number of components on the menu. * * @return an integer containing the number of components on the menu */ public int getMenuComponentCount() { int componentCount = 0; componentCount = listitems.size() + menuitems.size(); return componentCount; } /** * Returns the component at position <code>n</code>. * * @param n the position of the component to be returned * @return the component requested, or <code>null</code> * if there is no popup menu * */ public Component getMenuComponent(int n) { if (n < scrollStart) return (Component) menuitems.elementAt(n); else return (Component) listitems.elementAt(n-scrollStart); } /** * Returns an array of <code>Component</code>s of the menu's * subcomponents. Note that this returns all <code>Component</code>s * in the popup menu, including separators. * * @return an array of <code>Component</code>s or an empty array * if there is no popup menu */ public Component[] getMenuComponents() { Component[] comps = new Component[menuitems.size()+listitems.size()]; int i=0; for (i=0; i<menuitems.size(); i++) comps[i] = (Component)menuitems.elementAt(i); for (i=0; i<listitems.size(); i++) comps[i+scrollStart] = (Component)listitems.elementAt(i); return comps; } /** * Returns true if the specified component exists in the * submenu hierarchy. * * @param c the <code>Component</code> to be tested * @return true if the <code>Component</code> exists, false otherwise */ public boolean isMenuComponent(Component c) { if (!super.isMenuComponent(c)) { // Are we in the scrolllist int count = listitems.size(); for (int i = 0 ; i < count; i++) { Component comp = (Component)listitems.elementAt(i); if (comp == c) return true; if (comp instanceof JMenu) { JMenu subMenu = (JMenu) comp; if (subMenu.isMenuComponent(c)) return true; } } } return false; } /** * CURRENTLY DOES NOTHING. */ public void applyComponentOrientation(ComponentOrientation o) { //super.applyComponentOrientation(o); //if ( popupMenu != null ) { // int ncomponents = getMenuComponentCount(); // for (int i = 0 ; i < ncomponents ; ++i) { // getMenuComponent(i).applyComponentOrientation(o); // } // popupMenu.setComponentOrientation(o); //} } /** * CURRENTLY DOES NOTHING. */ public void setComponentOrientation(ComponentOrientation o) { //super.setComponentOrientation(o); //if ( popupMenu != null ) { // popupMenu.setComponentOrientation(o); //} } /** * Sets the visibility of the menu's popup. If the menu is * not enabled, this method will have no effect. * * @param vis, a boolean value -- true to make the menu visible, false to hide it. */ public void setPopupMenuVisible(boolean vis) { if (!vis) { list.ensureIndexIsVisible(0); up.setEnabled(false); down.setEnabled(true); //if (timer != null) { // timer.cancel(); //} } else { // DON'T NEED THE SCROLL ARROWS IF MENU NOT LONG ENOUGH if (listitems.size() <= menuLength) { super.remove(upArrow); super.remove(downArrow); list.setVisibleRowCount(listitems.size() - vtSeparators.size()); } list.setListData(listitems); list.clearSelection(); list.validate(); Dimension dim = list.getPreferredScrollableViewportSize(); view.setExtentSize(dim); } getPopupMenu().pack(); super.setPopupMenuVisible(vis); } /** * This class implements the list renderer for the scrollable list part of the menu. */ private class ScrollListCellRenderer implements ListCellRenderer { /** For getting the background colour.*/ private JMenuItem test = new JMenuItem(); public ScrollListCellRenderer() {} public Component getListCellRendererComponent(JList list, Object value, // value to display int index, // cell index boolean isSelected, // is the cell selected boolean cellHasFocus ) { // the list and the cell have the focus FlowLayout flow = new FlowLayout(FlowLayout.LEFT); flow.setHgap(0); flow.setVgap(0); JPanel panel = new JPanel(flow); if (value instanceof JMenuItem) { JMenuItem item = (JMenuItem)value; item.setSelected(isSelected); if (isSelected) { item.setForeground(list.getSelectionForeground()); item.setBackground(list.getSelectionBackground()); panel.setBackground(list.getSelectionBackground()); } else { item.setForeground(test.getForeground()); item.setBackground(test.getBackground()); panel.setBackground(test.getBackground()); } panel.add(item); panel.setToolTipText(item.getToolTipText()); } else if (value instanceof JSeparator) { JSeparator sep = (JSeparator)value; panel.add(sep); } else { JLabel label = new JLabel(); label.setText((value == null) ? "" : value.toString()); //$NON-NLS-1$ panel.add(label); } panel.setBorder(null); //ToolTipManager.sharedInstance().registerComponent(panel); return panel; } } }