/*
* Copyright 2011 Luke Usherwood.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.bettyluke.swing;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.Arrays;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.JPopupMenu;
import javax.swing.SwingConstants;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.plaf.basic.BasicArrowButton;
// TODO: Implement correct keyboard cursor behaviour - scrolling past end of view and
// page-up/page-down
public final class ScrollingMenu extends JPopupMenu {
private final BasicArrowButton upButton = createArrow(SwingConstants.NORTH);
private final BasicArrowButton downButton = createArrow(SwingConstants.SOUTH);
private final Controller controller = new Controller();
private final Component[] menuItems;
private final int viewLength;
private int topComp = 0;
/**
* In lieu of a full MVC separation, this class at least separates out the
* behavioural logic from view objects.
*/
private final class Controller extends MouseAdapter implements ActionListener {
private Timer timer = new Timer(66, this);
private float increment = 1;
public void attach() {
upButton.addMouseListener(this);
downButton.addMouseListener(this);
ScrollingMenu.this.addMouseWheelListener(this);
}
@Override
public void actionPerformed(ActionEvent e) {
shiftTo(clampPosition(topComp + (int) increment));
increment *= 1.1;
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
int scrollAmount = e.getScrollType() == MouseWheelEvent.WHEEL_BLOCK_SCROLL ?
viewLength : e.getUnitsToScroll();
shiftTo(clampPosition(topComp + scrollAmount));
e.consume();
}
@Override
public void mouseEntered(MouseEvent e) {
increment = (e.getSource() == upButton) ? -1 : 1;
timer.start();
((AbstractButton) e.getSource()).getModel().setPressed(true);
}
@Override
public void mouseExited(MouseEvent e) {
timer.stop();
((AbstractButton) e.getSource()).getModel().setPressed(false);
}
private int clampPosition(int newTop) {
return Math.max(0, Math.min(newTop, end() - 1));
}
}
/** Constructor which <b>rips</b> all the menu-items out of menu, taking them over. */
public ScrollingMenu(JPopupMenu menu, int visibleLength) {
this.menuItems = Arrays.copyOf(menu.getComponents(), menu.getComponentCount());
menu.removeAll();
this.viewLength = visibleLength - 2;
upButton.setEnabled(false);
add(upButton);
for (int i = 0; i < viewLength; ++i) {
add(menuItems[i]);
}
add(downButton);
setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
controller.attach();
}
/**
* Shifts the menu to display items beginning at the given index, and disables the
* up/down buttons as appropriate.
*/
public void shiftTo(int newTop) {
upButton.setEnabled(newTop > 0);
downButton.setEnabled(newTop < end() - 1);
if (newTop < topComp) {
for (int i = topComp - 1; i >= newTop; --i) {
assert menuItems[i].getParent() == null;
remove(viewLength);
insert(menuItems[i], 1);
}
} else {
for (int i = topComp; i < newTop; ++i) {
Component toAdd = menuItems[i + viewLength];
assert toAdd.getParent() == null;
insert(toAdd, viewLength + 1);
remove(1);
}
}
topComp = newTop;
invalidate();
validate();
repaint();
}
public int end() {
return menuItems.length - viewLength;
}
private static BasicArrowButton createArrow(int dir) {
BasicArrowButton result = new BasicArrowButton(
dir,
new JPopupMenu().getBackground(),
UIManager.getColor("controlShadow"),
Color.DARK_GRAY,
UIManager.getColor("controlLtHighlight")) {
@Override
public Dimension getPreferredSize() {
return new Dimension(24, 24);
}
};
return result;
}
}