// Charles A. Loomis, Jr., and University of California, Santa Cruz,
// Copyright (c) 2000
package org.freehep.swing.graphics;
import java.awt.BasicStroke;
import java.awt.KeyboardFocusManager;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.border.Border;
import javax.swing.event.EventListenerList;
/**
* The primary superclass of all graphical selection panels. These
* panels are expected to handle all of the interaction with the user,
* and generate a GraphicalSelectionEvent when a selection has been
* made.
*
* Note that GraphicalSelectionPanels use the information about the
* size of the component to send back meaningful zoom transformation
* and the like. To keep these calculations simple, Borders are not
* allowed on these components. If a Border is desired, then embed
* the selection panel within a container and put the Border on the
* container.
*
* @author Charles Loomis
* @version $Id: GraphicalSelectionPanel.java 8584 2006-08-10 23:06:37Z duns $ */
public class GraphicalSelectionPanel
extends JPanel
implements MouseListener,
KeyListener,
MouseMotionListener,
java.io.Serializable {
/**
* A private ActionEvent which is used when a selection is made. Since
* the sender never changes, this event is simply reused. */
private ActionEvent actionEvent;
/**
* The list of selection actions. */
protected LinkedList selectionActions = new LinkedList();
/**
* The hash map which maps keys to actions. */
protected ActionMap actionMap = new ActionMap();
/**
* The "Leave" action in the popup menu. Created here as an
* Action so that subclasses can more easily enable and disable
* this item. */
protected SelectionAction defaultModeAction =
new SelectionAction("Default Mode",
GraphicalSelectionEvent.DEFAULT_MODE,
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0));
/**
* The "Next" action in the popup menu. Created here as an
* Action so that subclasses can more easily enable and disable
* this item. */
protected SelectionAction nextAction =
new SelectionAction("Next Mode",
GraphicalSelectionEvent.NEXT_MODE,
KeyStroke.getKeyStroke(KeyEvent.VK_N,0));
/**
* The "Previous" action in the popup menu. Created here as an
* Action so that subclasses can more easily enable and disable
* this item. */
protected SelectionAction previousAction =
new SelectionAction("Previous Mode",
GraphicalSelectionEvent.PREVIOUS_MODE,
KeyStroke.getKeyStroke(KeyEvent.VK_P,0));
/**
* Thin stroke for the white part of the selection box. Set the
* miter limit so that the drawing doesn't extend outside of the
* bounding box. */
final protected static Stroke thinStroke =
new BasicStroke(1.f,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER,
3.f);
/**
* Thick stroke for the black part of the selection box. Set the
* miter limit so that the drawing doesn't extend outside of the
* bounding box. */
final protected static Stroke thickStroke =
new BasicStroke(3.f,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER,
5.f);
/**
* An empty popup menu is available for subclasses of this
* object. */
private JPopupMenu popup;
/**
* A flag to indicate whether a popup menu is currently being
* processed or not. */
private boolean processingPopup;
/**
* Keeps track of all of the event listeners. */
private EventListenerList listenerList;
/**
* Error string when user attempts to set a non-null border. */
private final static String NON_NULL_BORDER_ERROR =
"GraphicalSelectionPanel does not support borders.";
/**
* Creates a selection panel which is transparent. */
public GraphicalSelectionPanel() {
// make sure that tab and shift-tab can be used to cycle between components.
Set forwardKeys = new TreeSet();
forwardKeys.add(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0));
setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, forwardKeys);
Set backwardKeys = new TreeSet();
backwardKeys.add(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_MASK));
setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, backwardKeys);
setOpaque(false);
listenerList = new EventListenerList();
actionEvent = new ActionEvent(this,0,"KeyAction");
addMouseListener(this);
addMouseMotionListener(this);
addKeyListener(this);
setRequestFocusEnabled(true);
// Make the necessary selection actions.
makeSelectionActions();
// Make the popup menu.
popup = new JPopupMenu();
processingPopup = false;
// Get the popup menu.
JPopupMenu popup = getPopupMenu();
// Add items to this popup.
JMenuItem item;
// Add all of the selection actions.
Iterator i = selectionActions.iterator();
while (i.hasNext()) {
Action action = (Action) i.next();
item = new JMenuItem(action);
KeyStroke accelerator =
(KeyStroke) action.getValue(Action.ACCELERATOR_KEY);
addActionEntry(accelerator,action);
item.setAccelerator(accelerator);
popup.add(item);
}
popup.addSeparator();
item = new JMenuItem(nextAction);
KeyStroke accelerator =
(KeyStroke) nextAction.getValue(Action.ACCELERATOR_KEY);
addActionEntry(accelerator,nextAction);
item.setAccelerator(accelerator);
popup.add(item);
item = new JMenuItem(previousAction);
accelerator =
(KeyStroke) previousAction.getValue(Action.ACCELERATOR_KEY);
addActionEntry(accelerator,previousAction);
item.setAccelerator(accelerator);
popup.add(item);
item = new JMenuItem(defaultModeAction);
accelerator =
(KeyStroke) defaultModeAction.getValue(Action.ACCELERATOR_KEY);
addActionEntry(accelerator,defaultModeAction);
item.setAccelerator(accelerator);
popup.add(item);
}
/**
* This makes all of the selection actions and binds them to specific
* keys. */
private void makeSelectionActions() {
// Make the zoom actions.
Action action =
new SelectionAction("Zoom",GraphicalSelectionEvent.ZOOM,
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0));
selectionActions.add(action);
addActionEntry(KeyEvent.VK_ENTER,action);
action =
new SelectionAction("Zoom (new view)",
GraphicalSelectionEvent.ZOOM_NEW_VIEW,
KeyStroke.getKeyStroke(KeyEvent.VK_V,0));
selectionActions.add(action);
addActionEntry(KeyEvent.VK_N,action);
// Make all of the picking actions.
action =
new SelectionAction("Pick",
GraphicalSelectionEvent.PICK,
KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,0));
selectionActions.add(action);
addActionEntry(KeyEvent.VK_SPACE,action);
action =
new SelectionAction("Pick (add)",
GraphicalSelectionEvent.PICK_ADD,
KeyStroke.getKeyStroke(KeyEvent.VK_PLUS,0));
selectionActions.add(action);
addActionEntry(KeyEvent.VK_EQUALS,action);
addActionEntry(KeyEvent.VK_PLUS,action);
action =
new SelectionAction("Un-Pick",
GraphicalSelectionEvent.UNPICK,
KeyStroke.getKeyStroke(KeyEvent.VK_MINUS,0));
selectionActions.add(action);
addActionEntry(KeyEvent.VK_UNDERSCORE,action);
addActionEntry(KeyEvent.VK_MINUS,action);
action =
new ClearAction(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0));
selectionActions.add(action);
addActionEntry(KeyEvent.VK_ESCAPE,action);
addActionEntry(KeyEvent.VK_BACK_SPACE,action);
addActionEntry(KeyEvent.VK_DELETE,action);
}
/**
* Activate or inactivate all of the selection actions. */
protected void setSelectionActionsEnabled(boolean enable) {
Iterator i = selectionActions.iterator();
while (i.hasNext()) {
Action action = (Action) i.next();
action.setEnabled(enable);
}
}
/**
* The default implementation of this method does nothing.
* Subclasses should provide the needed functionality to ensure
* that the selection is no longer visible after this method is
* called. */
public void resetSelection() {
}
/**
* This method returns the popup menu for this component. This
* may be modified by subclasses of this event to provide needed
* menu items.
*
* @return the component's popup menu */
public JPopupMenu getPopupMenu() {
return popup;
}
/**
* This resets a flag which indicates when a popup menu is being
* processed. This should be called by subclasses when the
* ActionEvent from a popup menu is received. */
protected void cancelPopupProcessing() {
processingPopup = false;
}
/**
* This method determines whether or not a popup menu is being
* processed. If it is then this will return true and the mouse
* event should be ignored.
*
* @param e MouseEvent passed into mouse handling routine
*
* @return a boolean indicating whether or not to ignore the mouse
* event */
public boolean isProcessingPopup(MouseEvent e) {
int id = e.getID();
boolean flag = processingPopup;
if (id==MouseEvent.MOUSE_PRESSED) {
if (!processingPopup && testPopupTrigger(e)) {
getPopupMenu().show(e.getComponent(),
e.getX(),e.getY());
processingPopup = true;
flag = true;
}
} else if (id==MouseEvent.MOUSE_RELEASED) {
if (!processingPopup && testPopupTrigger(e)) {
getPopupMenu().show(e.getComponent(),
e.getX(),e.getY());
processingPopup = true;
flag = true;
} else if (processingPopup &&
!getPopupMenu().isVisible()) {
processingPopup = false;
flag = true;
}
}
return flag;
}
/**
* This component does not support borders. If this method is
* called with any non-null argument, then an
* IllegalArgumentException is thrown. If a border is desired,
* then this component should be embedded within container which
* has one.
*
* @param border must be null */
public final void setBorder(Border border) {
if (border!=null)
throw new IllegalArgumentException(NON_NULL_BORDER_ERROR);
}
/**
* This component does not support borders. Null is always
* returned by this method.
*
* @return null */
public final Border getBorder() {
return null;
}
/**
* Moves and resizes this component. This is overridden so that
* the selection can be reset if the size changes.
*
* @param x x-coordinate of component
* @param y y-coordinate of component
* @param width width of the component
* @param height height of the component */
public void setBounds(int x, int y, int width, int height) {
resetSelection();
super.setBounds(x,y,width,height);
}
/**
* Add a GraphicalSelectionListener.
*
* @param listener the GraphicalSelectionListener to add */
public void
addGraphicalSelectionListener(GraphicalSelectionListener
listener) {
listenerList.add(GraphicalSelectionListener.class, listener);
}
/**
* Remove a GraphicalSelectionListener.
*
* @param listener the GraphicalSelectionListener to remove */
public void
removeGraphicalSelectionListener(GraphicalSelectionListener
listener) {
listenerList.remove(GraphicalSelectionListener.class,
listener);
}
/**
* Send the GraphicalSelectionMade event to all currently
* registered GraphicalSelectionListeners.
*
* @param gsEvent the GraphicalSelectionEvent which is sent to all
* currently registered GraphicalSelectionListeners */
protected void fireGraphicalSelectionMade(GraphicalSelectionEvent gsEvent){
Object[] listeners = listenerList.getListenerList();
for (int i=listeners.length-2; i>=0; i-=2) {
if (listeners[i]==GraphicalSelectionListener.class) {
((GraphicalSelectionListener)listeners[i+1]).
graphicalSelectionMade(gsEvent);
}
}
}
/**
* Invoked when the mouse has been clicked on a component. This
* is an empty method which subclasses should override if
* necessary.
*
* @param e MouseEvent describing action */
public void mouseClicked(MouseEvent e) {}
/**
* Invoked when the mouse enters a component. This method just
* requests the keyboard focus. Subclasses which override this
* method should also request the keyboard focus with a call to
* requestFocus().
*
* @param e MouseEvent describing action */
public void mouseEntered(MouseEvent e) {
requestFocus();
}
/**
* Invoked when the mouse exits a component. This is an empty
* method which subclasses should override if necessary.
*
* @param e MouseEvent describing action */
public void mouseExited(MouseEvent e) {}
/**
* Invoked when the mouse button has been pressed on a component.
* This is an empty method which subclasses should override if
* necessary.
*
* @param e MouseEvent describing action */
public void mousePressed(MouseEvent e) {}
/**
* Invoked when a mouse button has been released on a component.
* This is an empty method which subclasses should override if
* necessary.
*
* @param e MouseEvent describing action */
public void mouseReleased(MouseEvent e) {}
/**
* Invoked when a mouse button is pressed on a component and then
* dragged. This is an empty method which subclasses should
* override if necessary.
*
* @param e MouseEvent describing action */
public void mouseDragged(MouseEvent e) {}
/**
* Invoked when the mouse button has been moved on a component
* (with no buttons down). This is an empty method which
* subclasses should override if necessary.
*
* @param e MouseEvent describing action */
public void mouseMoved(MouseEvent e) {}
/**
* Invoked when a key has been pressed. This is an empty method
* which subclasses should override if necessary.
*
* @param e KeyEvent describing key which has been pressed. */
public void keyPressed(KeyEvent e) {}
/**
* Process key-released events. This defines and uses the following key
* bindings:
*
* Subclasses may override this method to provide additional
* key-bindings. However if the subclass doesn't handle a particular
* key event, this method should be called.
*
* @param e KeyEvent describing the key which has been released */
public void keyReleased(KeyEvent e) {
// Get the keystroke. Ignore the modifiers for all keys except the
// arrow keys.
int keyCode = e.getKeyCode();
int modifiers = 0;
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode,modifiers);
InputMap inputMap = getInputMap();
Object actionKey = inputMap.get(keyStroke);
if (actionKey!=null) {
Action action = (Action) actionMap.get(actionKey);
if (action!=null) {
action.actionPerformed(actionEvent);
}
}
}
/**
* Invoked when a key has been typed. This is an empty method
* which subclasses should override if necessary.
*
* @param e KeyEvent describing key which has been typed.
*/
public void keyTyped(KeyEvent e) {}
/**
* A utility function which creates an appropriate selection event
* when the user accepts the current selection and sends it to all
* listeners. */
protected void makeSelectionEvent(int type) {}
/**
* A utility method which tests to see if the given mouse event
* should trigger the popup menu. Normally, Java itself has an
* isPopupTrigger() method, but this doesn't work reliably under
* Windows. This method will return true if the mouse click is on
* the right button. */
protected boolean testPopupTrigger(MouseEvent e) {
int modifiers = e.getModifiers();
return ((modifiers & InputEvent.BUTTON3_MASK)!=0);
}
/**
* This class defines the Select action. This causes a
* GraphicalSelectionEvent to be generated and sent to all
* listeners. */
class SelectionAction
extends AbstractAction {
private int actionCode;
public SelectionAction(String name, int actionCode) {
super(name);
this.actionCode = actionCode;
}
public SelectionAction(String name,
int actionCode,
KeyStroke keyStroke) {
this(name,actionCode);
putValue(ACCELERATOR_KEY,keyStroke);
}
public void actionPerformed(ActionEvent e) {
cancelPopupProcessing();
if (isEnabled()) {
makeSelectionEvent(actionCode);
}
}
}
/**
* This class defines the Clear action. This causes the current
* selection to be cleared. */
class ClearAction
extends AbstractAction {
public ClearAction(KeyStroke keyStroke) {
super("Clear");
putValue(ACCELERATOR_KEY,keyStroke);
}
public void actionPerformed(ActionEvent e) {
cancelPopupProcessing();
resetSelection();
}
}
/**
* This utility method binds an action to a particular key. The
* associated action will be done when the given key is typed. */
protected void addActionEntry(int keyCode, Action action) {
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode,0);
addActionEntry(keyStroke,action);
}
/**
* This utility method binds an action to a KeyStroke. The associated
* action will be done when the given KeyStroke is encountered. */
protected void addActionEntry(KeyStroke keyStroke, Action action) {
Object actionMapKey = action.getValue(Action.NAME);
actionMap.put(actionMapKey,action);
InputMap inputMap = getInputMap();
inputMap.put(keyStroke,actionMapKey);
}
}