0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041:
0042: package org.openide.explorer.view;
0043:
0044: import java.awt.Dimension;
0045: import java.awt.Insets;
0046: import java.awt.Point;
0047: import java.awt.Rectangle;
0048: import java.awt.Toolkit;
0049: import java.awt.dnd.Autoscroll;
0050: import java.awt.dnd.DnDConstants;
0051: import java.awt.event.ActionEvent;
0052: import java.awt.event.ActionListener;
0053: import java.awt.event.FocusEvent;
0054: import java.awt.event.FocusListener;
0055: import java.awt.event.InputEvent;
0056: import java.awt.event.KeyAdapter;
0057: import java.awt.event.KeyEvent;
0058: import java.awt.event.KeyListener;
0059: import java.awt.event.MouseEvent;
0060: import java.beans.PropertyChangeEvent;
0061: import java.beans.PropertyChangeListener;
0062: import java.beans.PropertyVetoException;
0063: import java.beans.VetoableChangeListener;
0064: import java.io.Externalizable;
0065: import java.io.IOException;
0066: import java.io.ObjectInput;
0067: import java.io.ObjectOutput;
0068: import java.util.ArrayList;
0069: import java.util.List;
0070: import javax.accessibility.AccessibleContext;
0071: import javax.swing.AbstractAction;
0072: import javax.swing.Action;
0073: import javax.swing.BorderFactory;
0074: import javax.swing.BoxLayout;
0075: import javax.swing.JComponent;
0076: import javax.swing.JLabel;
0077: import javax.swing.JList;
0078: import javax.swing.JPanel;
0079: import javax.swing.JPopupMenu;
0080: import javax.swing.JScrollPane;
0081: import javax.swing.JTextField;
0082: import javax.swing.JViewport;
0083: import javax.swing.KeyStroke;
0084: import javax.swing.ListSelectionModel;
0085: import javax.swing.SwingUtilities;
0086: import javax.swing.ToolTipManager;
0087: import javax.swing.event.DocumentEvent;
0088: import javax.swing.event.DocumentListener;
0089: import javax.swing.event.ListDataEvent;
0090: import javax.swing.event.ListDataListener;
0091: import javax.swing.event.ListSelectionEvent;
0092: import javax.swing.event.ListSelectionListener;
0093: import javax.swing.text.Position;
0094: import org.openide.awt.MouseUtils;
0095: import org.openide.explorer.ExplorerManager;
0096: import org.openide.explorer.ExplorerManager.Provider;
0097: import org.openide.nodes.Node;
0098: import org.openide.nodes.Node.Property;
0099: import org.openide.nodes.NodeOp;
0100: import org.openide.util.ContextAwareAction;
0101: import org.openide.util.NbBundle;
0102: import org.openide.util.Utilities;
0103: import org.openide.util.WeakListeners;
0104: import org.openide.util.actions.CallbackSystemAction;
0105:
0106: /** Explorer view to display items in a list.
0107: * <p>
0108: * This class is a <q>view</q>
0109: * to use it properly you need to add it into a component which implements
0110: * {@link Provider}. Good examples of that can be found
0111: * in {@link org.openide.explorer.ExplorerUtils}. Then just use
0112: * {@link Provider#getExplorerManager} call to get the {@link ExplorerManager}
0113: * and control its state.
0114: * </p>
0115: * <p>
0116: * There can be multiple <q>views</q> under one container implementing {@link Provider}. Select from
0117: * range of predefined ones or write your own:
0118: * </p>
0119: * <ul>
0120: * <li>{@link org.openide.explorer.view.BeanTreeView} - shows a tree of nodes</li>
0121: * <li>{@link org.openide.explorer.view.ContextTreeView} - shows a tree of nodes without leaf nodes</li>
0122: * <li>{@link org.openide.explorer.view.ListView} - shows a list of nodes</li>
0123: * <li>{@link org.openide.explorer.view.IconView} - shows a rows of nodes with bigger icons</li>
0124: * <li>{@link org.openide.explorer.view.ChoiceView} - creates a combo box based on the explored nodes</li>
0125: * <li>{@link org.openide.explorer.view.TreeTableView} - shows tree of nodes together with a set of their {@link Property}</li>
0126: * <li>{@link org.openide.explorer.view.MenuView} - can create a {@link javax.swing.JMenu} structure based on structure of {@link Node}s</li>
0127: * </ul>
0128: * <p>
0129: * All of these views use {@link ExplorerManager#find} to walk up the AWT hierarchy and locate the
0130: * {@link ExplorerManager} to use as a controler. They attach as listeners to
0131: * it and also call its setter methods to update the shared state based on the
0132: * user action. Not all views make sence together, but for example
0133: * {@link org.openide.explorer.view.ContextTreeView} and {@link org.openide.explorer.view.ListView} were designed to complement
0134: * themselves and behaves like windows explorer. The {@link org.openide.explorer.propertysheet.PropertySheetView}
0135: * for example should be able to work with any other view.
0136: * </p>
0137: * @author Ian Formanek, Jan Jancura, Jaroslav Tulach
0138: */
0139: public class ListView extends JScrollPane implements Externalizable {
0140: /** generated Serialized Version UID */
0141: static final long serialVersionUID = -7540940974042262975L;
0142:
0143: /** Explorer manager to work with. Is not null only if the component is showing
0144: * in components hierarchy
0145: */
0146: private transient ExplorerManager manager;
0147:
0148: /** The actual JList list */
0149: transient protected JList list;
0150:
0151: /** model to use */
0152: transient protected NodeListModel model;
0153:
0154: //
0155: // listeners
0156: //
0157:
0158: /** Listener to nearly everything */
0159: transient Listener managerListener;
0160:
0161: /** weak variation of the listener for property change on the explorer manager */
0162: transient PropertyChangeListener wlpc;
0163:
0164: /** weak variation of the listener for vetoable change on the explorer manager */
0165: transient VetoableChangeListener wlvc;
0166:
0167: /** popup */
0168: transient PopupSupport popupSupport;
0169:
0170: //
0171: // properties
0172: //
0173:
0174: /** if true, the icon view displays a popup on right mouse click, if false, the popup is not displayed */
0175: private boolean popupAllowed = true;
0176:
0177: /** if true, the hierarchy traversal is allowed, if false, it is disabled */
0178: private boolean traversalAllowed = true;
0179:
0180: /** action preformer */
0181: private ActionListener defaultProcessor;
0182:
0183: //
0184: // Dnd
0185: //
0186:
0187: /** true if drag support is active */
0188: transient boolean dragActive = false;
0189:
0190: /** true if drop support is active */
0191: transient boolean dropActive = false;
0192:
0193: /** Drag support */
0194: transient ListViewDragSupport dragSupport;
0195:
0196: /** Drop support */
0197: transient ListViewDropSupport dropSupport;
0198:
0199: // default DnD actions
0200: transient private int allowedDragActions = DnDConstants.ACTION_COPY_OR_MOVE
0201: | DnDConstants.ACTION_REFERENCE;
0202: transient private int allowedDropActions = DnDConstants.ACTION_COPY_OR_MOVE
0203: | DnDConstants.ACTION_REFERENCE;
0204:
0205: /** True, if the selection listener is attached. */
0206: transient boolean listenerActive;
0207:
0208: // init .................................................................................
0209:
0210: /** Default constructor.
0211: */
0212: public ListView() {
0213: initializeList();
0214:
0215: // activation of drop target
0216: setDropTarget(DragDropUtilities.dragAndDropEnabled);
0217:
0218: // no border, window system manages outer border itself
0219: setBorder(BorderFactory.createEmptyBorder());
0220: setViewportBorder(BorderFactory.createEmptyBorder());
0221: }
0222:
0223: /** Initializes the tree & model.
0224: */
0225: private void initializeList() {
0226: // initilizes the JTree
0227: model = createModel();
0228: list = createList();
0229: list.setModel(model);
0230:
0231: setViewportView(list);
0232:
0233: {
0234: AbstractAction action = new GoUpAction();
0235: KeyStroke key = KeyStroke.getKeyStroke(
0236: KeyEvent.VK_BACK_SPACE, 0);
0237: list.registerKeyboardAction(action, key,
0238: JComponent.WHEN_FOCUSED);
0239: }
0240:
0241: {
0242: AbstractAction action = new EnterAction();
0243: KeyStroke key = KeyStroke
0244: .getKeyStroke(KeyEvent.VK_ENTER, 0);
0245: list.registerKeyboardAction(action, key,
0246: JComponent.WHEN_FOCUSED);
0247: }
0248:
0249: managerListener = new Listener();
0250: popupSupport = new PopupSupport();
0251: list.getActionMap().put("org.openide.actions.PopupAction",
0252: popupSupport); // NOI18N
0253:
0254: list.getSelectionModel().setSelectionMode(
0255: ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
0256:
0257: ToolTipManager.sharedInstance().registerComponent(list);
0258: }
0259:
0260: /*
0261: * Write view's state to output stream.
0262: */
0263: public void writeExternal(ObjectOutput out) throws IOException {
0264: out.writeObject(popupAllowed ? Boolean.TRUE : Boolean.FALSE);
0265: out
0266: .writeObject(traversalAllowed ? Boolean.TRUE
0267: : Boolean.FALSE);
0268: out.writeObject(new Integer(getSelectionMode()));
0269: }
0270:
0271: /*
0272: * Reads view's state form output stream.
0273: */
0274: public void readExternal(ObjectInput in) throws IOException,
0275: ClassNotFoundException {
0276: popupAllowed = ((Boolean) in.readObject()).booleanValue();
0277: traversalAllowed = ((Boolean) in.readObject()).booleanValue();
0278: setSelectionMode(((Integer) in.readObject()).intValue());
0279: }
0280:
0281: // properties ...........................................................................
0282:
0283: /** Test whether display of a popup menu is enabled.
0284: * @return <code>true</code> if so */
0285: public boolean isPopupAllowed() {
0286: return popupAllowed;
0287: }
0288:
0289: /** Enable/disable displaying popup menus on list view items. Default is enabled.
0290: * @param value <code>true</code> to enable
0291: */
0292: public void setPopupAllowed(boolean value) {
0293: popupAllowed = value;
0294: }
0295:
0296: /** Test whether hierarchy traversal shortcuts are permitted.
0297: * @return <code>true</code> if so */
0298: public boolean isTraversalAllowed() {
0299: return traversalAllowed;
0300: }
0301:
0302: /** Enable/disable hierarchy traversal using <code>CTRL+click</code> (down) and <code>Backspace</code> (up), default is enabled.
0303: * @param value <code>true</code> to enable
0304: */
0305: public void setTraversalAllowed(boolean value) {
0306: traversalAllowed = value;
0307: }
0308:
0309: /** Get the current processor for default actions.
0310: * If not <code>null</code>, double-clicks or pressing Enter on
0311: * items in the view will not perform the default action on the selected node; rather the processor
0312: * will be notified about the event.
0313: * @return the current default-action processor, or <code>null</code>
0314: */
0315: public ActionListener getDefaultProcessor() {
0316: return defaultProcessor;
0317: }
0318:
0319: /** Set a new processor for default actions.
0320: * @param value the new default-action processor, or <code>null</code> to restore use of the selected node's declared default action
0321: * @see #getDefaultProcessor
0322: */
0323: public void setDefaultProcessor(ActionListener value) {
0324: defaultProcessor = value;
0325: }
0326:
0327: /**
0328: * Set whether single-item or multiple-item
0329: * selections are allowed.
0330: * @param selectionMode one of {@link ListSelectionModel#SINGLE_SELECTION}, {@link ListSelectionModel#SINGLE_INTERVAL_SELECTION}, or {@link ListSelectionModel#MULTIPLE_INTERVAL_SELECTION}
0331: * @see ListSelectionModel#setSelectionMode
0332: */
0333: public void setSelectionMode(int selectionMode) {
0334: list.setSelectionMode(selectionMode);
0335: }
0336:
0337: /** Get the selection mode.
0338: * @return the mode
0339: * @see #setSelectionMode
0340: */
0341: public int getSelectionMode() {
0342: return list.getSelectionMode();
0343: }
0344:
0345: /********** Support for the Drag & Drop operations *********/
0346: /** @return true if dragging from the view is enabled, false
0347: * otherwise.<br>
0348: * Drag support is disabled by default.
0349: */
0350: public boolean isDragSource() {
0351: return dragActive;
0352: }
0353:
0354: /** Enables/disables dragging support.
0355: * @param state true enables dragging support, false disables it.
0356: */
0357: public void setDragSource(boolean state) {
0358: if (state == dragActive) {
0359: return;
0360: }
0361:
0362: dragActive = state;
0363:
0364: // create drag support if needed
0365: if (dragActive && (dragSupport == null)) {
0366: dragSupport = new ListViewDragSupport(this , list);
0367: }
0368:
0369: // activate / deactivate support according to the state
0370: dragSupport.activate(dragActive);
0371: }
0372:
0373: /** @return true if dropping to the view is enabled, false
0374: * otherwise<br>
0375: * Drop support is disabled by default.
0376: */
0377: public boolean isDropTarget() {
0378: return dropActive;
0379: }
0380:
0381: /** Enables/disables dropping support.
0382: * @param state true means drops into view are allowed,
0383: * false forbids any drops into this view.
0384: */
0385: public void setDropTarget(boolean state) {
0386: if (state == dropActive) {
0387: return;
0388: }
0389:
0390: dropActive = state;
0391:
0392: // create drop support if needed
0393: if (dropActive && (dropSupport == null)) {
0394: dropSupport = new ListViewDropSupport(this , list);
0395: }
0396:
0397: // activate / deactivate support according to the state
0398: dropSupport.activate(dropActive);
0399: }
0400:
0401: /** Actions constants comes from {@link java.awt.dnd.DnDConstants}.
0402: * All actions (copy, move, link) are allowed by default.
0403: * @return int representing set of actions which are allowed when dragging from
0404: * asociated component.
0405: */
0406: public int getAllowedDragActions() {
0407: return allowedDragActions;
0408: }
0409:
0410: /** Sets allowed actions for dragging
0411: * @param actions new drag actions, using {@link java.awt.dnd.DnDConstants}
0412: */
0413: public void setAllowedDragActions(int actions) {
0414: // PENDING check parameters
0415: allowedDragActions = actions;
0416: }
0417:
0418: /** Actions constants comes from {@link java.awt.dnd.DnDConstants}.
0419: * All actions are allowed by default.
0420: * @return int representing set of actions which are allowed when dropping
0421: * into the asociated component.
0422: */
0423: public int getAllowedDropActions() {
0424: return allowedDropActions;
0425: }
0426:
0427: /** Sets allowed actions for dropping.
0428: * @param actions new allowed drop actions, using {@link java.awt.dnd.DnDConstants}
0429: */
0430: public void setAllowedDropActions(int actions) {
0431: // PENDING check parameters
0432: allowedDropActions = actions;
0433: }
0434:
0435: //
0436: // Methods to override
0437: //
0438:
0439: /** Creates the list that will display the data.
0440: */
0441: protected JList createList() {
0442: JList list = new NbList();
0443: list.setCellRenderer(new NodeRenderer());
0444:
0445: return list;
0446: }
0447:
0448: /** Allows subclasses to change the default model used for
0449: * the list.
0450: */
0451: protected NodeListModel createModel() {
0452: return new NodeListModel();
0453: }
0454:
0455: /** Called when the list changed selection and the explorer manager
0456: * should be updated.
0457: * @param nodes list of nodes that should be selected
0458: * @param em explorer manager
0459: * @exception PropertyVetoException if the manager does not allow the
0460: * selection
0461: */
0462: protected void selectionChanged(Node[] nodes, ExplorerManager em)
0463: throws PropertyVetoException {
0464: em.setSelectedNodes(nodes);
0465: }
0466:
0467: /** Called when explorer manager is about to change the current selection.
0468: * The view can forbid the change if it is not able to display such
0469: * selection.
0470: *
0471: * @param nodes the nodes to select
0472: * @return false if the view is not able to change the selection
0473: */
0474: protected boolean selectionAccept(Node[] nodes) {
0475: // if the selection is just the root context, confirm the selection
0476: if ((nodes.length == 1)
0477: && manager.getRootContext().equals(nodes[0])) {
0478: // XXX shouldn't this be exploredContext?
0479: return true;
0480: }
0481:
0482: // we do not allow selection in other than the exploredContext
0483: for (int i = 0; i < nodes.length; i++) {
0484: VisualizerNode v = VisualizerNode.getVisualizer(null,
0485: nodes[i]);
0486:
0487: if (model.getIndex(v) == -1) {
0488: return false;
0489: }
0490: }
0491:
0492: return true;
0493: }
0494:
0495: /** Shows selection.
0496: * @param indexes indexes of objects to select
0497: */
0498: protected void showSelection(int[] indexes) {
0499: list.setSelectedIndices(indexes);
0500: }
0501:
0502: //
0503: // Working methods
0504: //
0505:
0506: /* Initilizes the view.
0507: */
0508: public void addNotify() {
0509: super .addNotify();
0510:
0511: // run under mutex
0512: ExplorerManager em = ExplorerManager.find(this );
0513:
0514: if (em != manager) {
0515: if (manager != null) {
0516: manager.removeVetoableChangeListener(wlvc);
0517: manager.removePropertyChangeListener(wlpc);
0518: }
0519:
0520: manager = em;
0521:
0522: manager.addVetoableChangeListener(wlvc = WeakListeners
0523: .vetoableChange(managerListener, manager));
0524: manager.addPropertyChangeListener(wlpc = WeakListeners
0525: .propertyChange(managerListener, manager));
0526:
0527: model.setNode(manager.getExploredContext());
0528:
0529: updateSelection();
0530: } else {
0531: // bugfix #23509, the listener were removed --> add it again
0532: if (!listenerActive && (manager != null)) {
0533: manager.addVetoableChangeListener(wlvc = WeakListeners
0534: .vetoableChange(managerListener, manager));
0535: manager.addPropertyChangeListener(wlpc = WeakListeners
0536: .propertyChange(managerListener, manager));
0537: }
0538: }
0539:
0540: if (!listenerActive) {
0541: listenerActive = true;
0542: list.getSelectionModel().addListSelectionListener(
0543: managerListener);
0544: model.addListDataListener(managerListener);
0545:
0546: // bugfix #23974, model doesn't reflect an explorer context change
0547: // because any listener was not active
0548: model.setNode(manager.getExploredContext());
0549: list.addMouseListener(popupSupport);
0550: }
0551: }
0552:
0553: /** Removes listeners.
0554: */
0555: public void removeNotify() {
0556: super .removeNotify();
0557: listenerActive = false;
0558: list.getSelectionModel().removeListSelectionListener(
0559: managerListener);
0560:
0561: // bugfix #23509, remove useless listeners
0562: if (manager != null) {
0563: manager.removeVetoableChangeListener(wlvc);
0564: manager.removePropertyChangeListener(wlpc);
0565: }
0566:
0567: model.removeListDataListener(managerListener);
0568: list.removeMouseListener(popupSupport);
0569:
0570: // #112536: [dafe] I wasn't able to find out real reason for #112536
0571: // Following delaying works, but is hacky a bit and we have to check
0572: // if ListView is reused - addNotify called again, which we check through isDisplayable
0573: SwingUtilities.invokeLater(new Runnable() {
0574: public void run() {
0575: if (!isDisplayable()) {
0576: // #109123: clear the model, as it may become invalid, because we stopped
0577: // tracking ExplorerManager changes through listeners
0578: model.setNode(Node.EMPTY);
0579: }
0580: }
0581: });
0582: }
0583:
0584: /* Requests focus for the list component. Overrides superclass method. */
0585: public void requestFocus() {
0586: list.requestFocus();
0587: }
0588:
0589: /* Requests focus for the list component. Overrides superclass method. */
0590: public boolean requestFocusInWindow() {
0591: return list.requestFocusInWindow();
0592: }
0593:
0594: /** This method is called when user double-clicks on some object or
0595: * presses Enter key.
0596: * @param index Index of object in current explored context
0597: */
0598: final void performObjectAt(int index, int modifiers) {
0599: if ((index < 0) || (index >= model.getSize())) {
0600: return;
0601: }
0602:
0603: VisualizerNode v = (VisualizerNode) model.getElementAt(index);
0604: Node node = v.node;
0605:
0606: // if DefaultProcessor is set, the default action is notified to it overriding the default action on nodes
0607: if (defaultProcessor != null) {
0608: defaultProcessor.actionPerformed(new ActionEvent(node, 0,
0609: null, modifiers));
0610:
0611: return;
0612: }
0613:
0614: // on double click - invoke default action, if there is any
0615: // (unless user holds CTRL key what means that we should always dive into the context)
0616: Action a = node.getPreferredAction();
0617:
0618: if ((a != null) && ((modifiers & InputEvent.CTRL_MASK) == 0)) {
0619: if (a instanceof ContextAwareAction) {
0620: a = ((ContextAwareAction) a)
0621: .createContextAwareInstance(node.getLookup());
0622: }
0623:
0624: if (a.isEnabled()) {
0625: a.actionPerformed(new ActionEvent(node,
0626: ActionEvent.ACTION_PERFORMED, "")); // NOI18N
0627: } else {
0628: Toolkit.getDefaultToolkit().beep();
0629: }
0630: }
0631: // otherwise dive into the context
0632: else if (traversalAllowed && (!node.isLeaf())) {
0633: manager
0634: .setExploredContext(node, manager
0635: .getSelectedNodes());
0636: }
0637: }
0638:
0639: /** Called when selection has been changed. Make selection visible (at least partly).
0640: */
0641: private void updateSelection() {
0642: Node[] sel = manager.getSelectedNodes();
0643: int[] indices = new int[sel.length];
0644:
0645: // bugfix #27094, make sure a selection is visible
0646: int firstVisible = list.getFirstVisibleIndex();
0647: int lastVisible = list.getLastVisibleIndex();
0648: boolean ensureVisible = indices.length > 0;
0649:
0650: for (int i = 0; i < sel.length; i++) {
0651: VisualizerNode v = VisualizerNode.getVisualizer(null,
0652: sel[i]);
0653: indices[i] = model.getIndex(v);
0654: ensureVisible = ensureVisible
0655: && ((indices[i] < firstVisible) || (indices[i] > lastVisible));
0656: }
0657:
0658: // going to change list because of E.M.'s order -- temp disable the
0659: // listener
0660: if (listenerActive) {
0661: list.getSelectionModel().removeListSelectionListener(
0662: managerListener);
0663: }
0664:
0665: try {
0666: showSelection(indices);
0667:
0668: if (ensureVisible) {
0669: list.ensureIndexIsVisible(indices[0]);
0670: }
0671: } finally {
0672: if (listenerActive) {
0673: list.getSelectionModel().addListSelectionListener(
0674: managerListener);
0675: }
0676: }
0677: }
0678:
0679: void createPopup(int xpos, int ypos, boolean contextMenu) {
0680: if (manager == null) {
0681: return;
0682: }
0683:
0684: if (!popupAllowed) {
0685: return;
0686: }
0687:
0688: JPopupMenu popup;
0689:
0690: if (contextMenu) {
0691: popup = Utilities.actionsToPopup(manager
0692: .getExploredContext().getActions(true), this );
0693: } else {
0694: Action[] actions = NodeOp.findActions(manager
0695: .getSelectedNodes());
0696: popup = Utilities.actionsToPopup(actions, this );
0697: }
0698:
0699: if ((popup != null) && (popup.getSubElements().length > 0)) {
0700: Point p = getViewport().getViewPosition();
0701: p.x = xpos - p.x;
0702: p.y = ypos - p.y;
0703:
0704: SwingUtilities.convertPointToScreen(p, ListView.this );
0705:
0706: Dimension popupSize = popup.getPreferredSize();
0707: Rectangle screenBounds = Utilities
0708: .getUsableScreenBounds(getGraphicsConfiguration());
0709:
0710: if ((p.x + popupSize.width) > (screenBounds.x + screenBounds.width)) {
0711: p.x = (screenBounds.x + screenBounds.width)
0712: - popupSize.width;
0713: }
0714:
0715: if ((p.y + popupSize.height) > (screenBounds.y + screenBounds.height)) {
0716: p.y = (screenBounds.y + screenBounds.height)
0717: - popupSize.height;
0718: }
0719:
0720: SwingUtilities.convertPointFromScreen(p, ListView.this );
0721: popup.show(this , p.x, p.y);
0722: }
0723: }
0724:
0725: // innerclasses .........................................................................
0726:
0727: /**
0728: * Enhancement of standard JList.
0729: * Provides access to the Node's ToolTips, Accessibility and Autoscrolling.
0730: */
0731: final class NbList extends JList implements Autoscroll {
0732: static final long serialVersionUID = -7571829536335024077L;
0733:
0734: /** The worker for the scrolling */
0735: AutoscrollSupport support;
0736:
0737: // navigator
0738: int SEARCH_FIELD_PREFERRED_SIZE = 160;
0739: int SEARCH_FIELD_SPACE = 3;
0740: private String maxPrefix;
0741:
0742: // searchTextField manages focus because it handles VK_TAB key
0743: private JTextField searchTextField = new JTextField() {
0744: public boolean isManagingFocus() {
0745: return true;
0746: }
0747:
0748: public void processKeyEvent(KeyEvent ke) {
0749: //override the default handling so that
0750: //the parent will never receive the escape key and
0751: //close a modal dialog
0752: if (ke.getKeyCode() == ke.VK_ESCAPE) {
0753: removeSearchField();
0754: ke.consume(); // #44394
0755:
0756: // bugfix #32909, reqest focus when search field is removed
0757: NbList.this .requestFocus();
0758: } else {
0759: super .processKeyEvent(ke);
0760: }
0761: }
0762: };
0763:
0764: final private int heightOfTextField = searchTextField
0765: .getPreferredSize().height;
0766: private int originalScrollMode;
0767: private JPanel searchpanel = null;
0768:
0769: NbList() {
0770: super ();
0771:
0772: // fix for #18292
0773: // default action map for JList defines these shortcuts
0774: // but we use our own mechanism for handling them
0775: // following lines disable default L&F handling (if it is
0776: // defined on Ctrl-c, Ctrl-v and Ctrl-x)
0777: getInputMap().put(KeyStroke.getKeyStroke("control C"),
0778: "none"); // NOI18N
0779: getInputMap().put(KeyStroke.getKeyStroke("control V"),
0780: "none"); // NOI18N
0781: getInputMap().put(KeyStroke.getKeyStroke("control X"),
0782: "none"); // NOI18N
0783: setupSearch();
0784: }
0785:
0786: public void addNotify() {
0787: super .addNotify();
0788: ViewTooltips.register(this );
0789: }
0790:
0791: public void removeNotify() {
0792: super .removeNotify();
0793: ViewTooltips.unregister(this );
0794: }
0795:
0796: protected void processFocusEvent(FocusEvent fe) {
0797: super .processFocusEvent(fe);
0798: repaintSelection();
0799: }
0800:
0801: private void repaintSelection() {
0802: int[] idx = getSelectedIndices();
0803:
0804: if (idx.length == 0) {
0805: return;
0806: }
0807:
0808: for (int i = 0; i < idx.length; i++) {
0809: Rectangle r = getCellBounds(idx[i], idx[i]);
0810: repaint(r.x, r.y, r.width, r.height);
0811: }
0812: }
0813:
0814: // ToolTips:
0815:
0816: /**
0817: * Overrides JComponent's getToolTipText method in order to allow
0818: * Node's tips to be used if they are useful.
0819: *
0820: * @param event the MouseEvent that initiated the ToolTip display
0821: */
0822: public String getToolTipText(MouseEvent event) {
0823: if (event != null) {
0824: Point p = event.getPoint();
0825: int row = locationToIndex(p);
0826:
0827: if (row >= 0) {
0828: VisualizerNode v = (VisualizerNode) model
0829: .getElementAt(row);
0830: String tooltip = v.getShortDescription();
0831: String displayName = v.getDisplayName();
0832:
0833: if ((tooltip != null)
0834: && !tooltip.equals(displayName)) {
0835: return tooltip;
0836: }
0837: }
0838: }
0839:
0840: return null;
0841: }
0842:
0843: // Autoscroll:
0844:
0845: /** notify the Component to autoscroll */
0846: public void autoscroll(Point cursorLoc) {
0847: getSupport().autoscroll(cursorLoc);
0848: }
0849:
0850: /** @return the Insets describing the autoscrolling region or border
0851: * relative to the geometry of the implementing Component.
0852: */
0853: public Insets getAutoscrollInsets() {
0854: return getSupport().getAutoscrollInsets();
0855: }
0856:
0857: /** Safe getter for autoscroll support. */
0858: AutoscrollSupport getSupport() {
0859: if (support == null) {
0860: support = new AutoscrollSupport(this , new Insets(15,
0861: 10, 15, 10));
0862: }
0863:
0864: return support;
0865: }
0866:
0867: // Accessibility:
0868: public AccessibleContext getAccessibleContext() {
0869: if (accessibleContext == null) {
0870: accessibleContext = new AccessibleExplorerList();
0871: }
0872:
0873: return accessibleContext;
0874: }
0875:
0876: private void setupSearch() {
0877: // Remove the default key listeners
0878: KeyListener[] keyListeners = (KeyListener[]) (getListeners(KeyListener.class));
0879:
0880: for (int i = 0; i < keyListeners.length; i++) {
0881: removeKeyListener(keyListeners[i]);
0882: }
0883:
0884: // Add new key listeners
0885: addKeyListener(new KeyAdapter() {
0886: public void keyPressed(KeyEvent e) {
0887: int modifiers = e.getModifiers();
0888: int keyCode = e.getKeyCode();
0889:
0890: if (((modifiers > 0) && (modifiers != KeyEvent.SHIFT_MASK))
0891: || e.isActionKey()) {
0892: return;
0893: }
0894:
0895: char c = e.getKeyChar();
0896:
0897: if (!Character.isISOControl(c)
0898: && (keyCode != KeyEvent.VK_SHIFT)) {
0899: searchTextField.setText(String.valueOf(c));
0900: displaySearchField();
0901: }
0902: }
0903: });
0904:
0905: // Create a the "multi-event" listener for the text field. Instead of
0906: // adding separate instances of each needed listener, we're using a
0907: // class which implements them all. This approach is used in order
0908: // to avoid the creation of 4 instances which takes some time
0909: SearchFieldListener searchFieldListener = new SearchFieldListener();
0910: searchTextField.addKeyListener(searchFieldListener);
0911: searchTextField.addFocusListener(searchFieldListener);
0912: searchTextField.getDocument().addDocumentListener(
0913: searchFieldListener);
0914: }
0915:
0916: private List<Integer> doSearch(String prefix) {
0917: List<Integer> results = new ArrayList<Integer>();
0918:
0919: // do search forward the selected index
0920: int startIndex = (getSelectedIndex() == -1) ? 0
0921: : getSelectedIndex();
0922: int size = getModel().getSize();
0923: if (size == 0)
0924: return results; // nothing to search
0925:
0926: while (true) {
0927: startIndex = startIndex % size;
0928: startIndex = getNextMatch(prefix, startIndex,
0929: Position.Bias.Forward);
0930:
0931: if ((startIndex != -1)
0932: && !results.contains(new Integer(startIndex))) {
0933: results.add(Integer.valueOf(startIndex));
0934:
0935: String elementName = getModel().getElementAt(
0936: startIndex).toString();
0937:
0938: // initialize prefix
0939: if (maxPrefix == null) {
0940: maxPrefix = elementName;
0941: }
0942:
0943: maxPrefix = findMaxPrefix(maxPrefix, elementName);
0944:
0945: // try next element
0946: startIndex++;
0947: } else {
0948: break;
0949: }
0950: }
0951:
0952: return results;
0953: }
0954:
0955: private String findMaxPrefix(String str1, String str2) {
0956: String res = null;
0957:
0958: for (int i = 0; str1.regionMatches(true, 0, str2, 0, i); i++) {
0959: res = str1.substring(0, i);
0960: }
0961:
0962: return res;
0963: }
0964:
0965: private void prepareSearchPanel() {
0966: if (searchpanel == null) {
0967: searchpanel = new JPanel();
0968:
0969: JLabel lbl = new JLabel(NbBundle.getMessage(
0970: TreeView.class, "LBL_QUICKSEARCH")); //NOI18N
0971: searchpanel.setLayout(new BoxLayout(searchpanel,
0972: BoxLayout.X_AXIS));
0973: searchpanel.add(lbl);
0974: searchpanel.add(searchTextField);
0975: lbl.setLabelFor(searchTextField);
0976: searchpanel.setBorder(BorderFactory
0977: .createRaisedBevelBorder());
0978: lbl.setBorder(BorderFactory.createEmptyBorder(0, 0, 0,
0979: 5));
0980: }
0981: }
0982:
0983: /**
0984: * Adds the search field to the tree.
0985: */
0986: private void displaySearchField() {
0987: if ((getModel().getSize() > 0)
0988: && !searchTextField.isDisplayable()) {
0989: JViewport viewport = ListView.this .getViewport();
0990: originalScrollMode = viewport.getScrollMode();
0991: viewport.setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
0992:
0993: //Rectangle visibleTreeRect = getVisibleRect();
0994: prepareSearchPanel();
0995: add(searchpanel);
0996: revalidate();
0997: repaint();
0998:
0999: // bugfix #28501, avoid the chars duplicated on jdk1.3
1000: SwingUtilities.invokeLater(new Runnable() {
1001: public void run() {
1002: searchTextField.requestFocus();
1003: }
1004: });
1005: }
1006: }
1007:
1008: public void doLayout() {
1009: super .doLayout();
1010:
1011: if ((searchpanel != null) && searchpanel.isDisplayable()) {
1012: Rectangle visibleRect = getVisibleRect();
1013: int width = Math.min(visibleRect.width
1014: - (SEARCH_FIELD_SPACE * 2),
1015: SEARCH_FIELD_PREFERRED_SIZE
1016: - SEARCH_FIELD_SPACE);
1017:
1018: searchpanel
1019: .setBounds(Math.max(SEARCH_FIELD_SPACE,
1020: (visibleRect.x + visibleRect.width)
1021: - width), visibleRect.y
1022: + SEARCH_FIELD_SPACE, Math.min(
1023: visibleRect.width, width)
1024: - SEARCH_FIELD_SPACE, heightOfTextField);
1025: //System.err.println("Laid out search field: " + searchpanel.getBounds());
1026: }
1027: }
1028:
1029: /**
1030: * Removes the search field from the tree.
1031: */
1032: private void removeSearchField() {
1033: if ((searchpanel != null) && searchpanel.isDisplayable()) {
1034: remove(searchpanel);
1035: ListView.this .getViewport().setScrollMode(
1036: originalScrollMode);
1037: this .repaint(searchpanel.getBounds());
1038: requestFocus();
1039: }
1040: }
1041:
1042: private class AccessibleExplorerList extends AccessibleJList {
1043: AccessibleExplorerList() {
1044: }
1045:
1046: public String getAccessibleName() {
1047: return ListView.this .getAccessibleContext()
1048: .getAccessibleName();
1049: }
1050:
1051: public String getAccessibleDescription() {
1052: return ListView.this .getAccessibleContext()
1053: .getAccessibleDescription();
1054: }
1055: }
1056:
1057: private class SearchFieldListener extends KeyAdapter implements
1058: DocumentListener, FocusListener {
1059: /** The last search results */
1060: private List results = new ArrayList();
1061:
1062: /** The last selected index from the search results. */
1063: private int currentSelectionIndex;
1064:
1065: SearchFieldListener() {
1066: }
1067:
1068: public void changedUpdate(DocumentEvent e) {
1069: searchForNode();
1070: }
1071:
1072: public void insertUpdate(DocumentEvent e) {
1073: searchForNode();
1074: }
1075:
1076: public void removeUpdate(DocumentEvent e) {
1077: searchForNode();
1078: }
1079:
1080: public void keyPressed(KeyEvent e) {
1081: int keyCode = e.getKeyCode();
1082:
1083: if (keyCode == KeyEvent.VK_ESCAPE) {
1084: removeSearchField();
1085: NbList.this .requestFocus();
1086: } else if (keyCode == KeyEvent.VK_UP) {
1087: currentSelectionIndex--;
1088: displaySearchResult();
1089:
1090: // Stop processing the event here. Otherwise it's dispatched
1091: // to the tree too (which scrolls)
1092: e.consume();
1093: } else if (keyCode == KeyEvent.VK_DOWN) {
1094: currentSelectionIndex++;
1095: displaySearchResult();
1096:
1097: // Stop processing the event here. Otherwise it's dispatched
1098: // to the tree too (which scrolls)
1099: e.consume();
1100: } else if (keyCode == KeyEvent.VK_TAB) {
1101: if (maxPrefix != null) {
1102: searchTextField.setText(maxPrefix);
1103: }
1104:
1105: e.consume();
1106: } else if (keyCode == KeyEvent.VK_ENTER) {
1107: removeSearchField();
1108: NbList.this .requestFocus();
1109: NbList.this .dispatchEvent(e);
1110: }
1111: }
1112:
1113: /** Searches for a node in the tree. */
1114: private void searchForNode() {
1115: currentSelectionIndex = 0;
1116: results.clear();
1117: maxPrefix = null;
1118:
1119: String text = searchTextField.getText();
1120:
1121: if (text.length() > 0) {
1122: results = doSearch(text);
1123: displaySearchResult();
1124: }
1125: }
1126:
1127: private void displaySearchResult() {
1128: int sz = results.size();
1129:
1130: if (sz > 0) {
1131: if (currentSelectionIndex < 0) {
1132: currentSelectionIndex = sz - 1;
1133: } else if (currentSelectionIndex >= sz) {
1134: currentSelectionIndex = 0;
1135: }
1136:
1137: Integer index = (Integer) results
1138: .get(currentSelectionIndex);
1139: list.setSelectedIndex(index.intValue());
1140: list.ensureIndexIsVisible(index.intValue());
1141: } else {
1142: list.clearSelection();
1143: }
1144: }
1145:
1146: public void focusGained(FocusEvent e) {
1147: // Do nothing
1148: }
1149:
1150: public void focusLost(FocusEvent e) {
1151: removeSearchField();
1152: }
1153: }
1154:
1155: // end of navigator
1156: }
1157:
1158: private final class PopupSupport extends
1159: MouseUtils.PopupMouseAdapter implements Action, Runnable {
1160:
1161: CallbackSystemAction csa;
1162:
1163: public PopupSupport() {
1164: }
1165:
1166: public void mouseClicked(MouseEvent e) {
1167: if (MouseUtils.isDoubleClick(e)) {
1168: int index = list.locationToIndex(e.getPoint());
1169: performObjectAt(index, e.getModifiers());
1170: }
1171: }
1172:
1173: protected void showPopup(MouseEvent e) {
1174: Point p = new Point(e.getX(), e.getY());
1175: int i = list.locationToIndex(p);
1176:
1177: if (!list.isSelectedIndex(i)) {
1178: list.setSelectedIndex(i);
1179: }
1180:
1181: // the area of selected
1182: Rectangle r = list.getCellBounds(i, i);
1183: boolean contextMenu = (r == null) || !r.contains(p);
1184:
1185: createPopup(e.getX(), e.getY(), contextMenu);
1186: }
1187:
1188: public void actionPerformed(ActionEvent e) {
1189: // XXX why later?
1190: SwingUtilities.invokeLater(this );
1191: }
1192:
1193: public void run() {
1194: boolean multisel = (list.getSelectionMode() != ListSelectionModel.SINGLE_SELECTION);
1195: int i = (multisel ? list.getLeadSelectionIndex() : list
1196: .getSelectedIndex());
1197:
1198: if (i < 0) {
1199: return;
1200: }
1201:
1202: Point p = list.indexToLocation(i);
1203:
1204: if (p == null) {
1205: return;
1206: }
1207:
1208: createPopup(p.x, p.y, false);
1209: }
1210:
1211: public Object getValue(String key) {
1212: return null;
1213: }
1214:
1215: public void putValue(String key, Object value) {
1216: }
1217:
1218: public void setEnabled(boolean b) {
1219: }
1220:
1221: public boolean isEnabled() {
1222: // XXX should maybe use logic in {@link #run}?
1223: return true;
1224: }
1225:
1226: public void addPropertyChangeListener(
1227: PropertyChangeListener listener) {
1228: }
1229:
1230: public void removePropertyChangeListener(
1231: PropertyChangeListener listener) {
1232: }
1233:
1234: }
1235:
1236: private final class Listener implements ListDataListener,
1237: ListSelectionListener, PropertyChangeListener,
1238: VetoableChangeListener {
1239: Listener() {
1240: }
1241:
1242: /** Implements <code>ListDataListener</code> interface. */
1243: public void intervalAdded(ListDataEvent evt) {
1244: updateSelection();
1245: }
1246:
1247: /** Implements <code>ListDataListener</code>. */
1248: public void intervalRemoved(ListDataEvent evt) {
1249: updateSelection();
1250: }
1251:
1252: /** Implemetns <code>ListDataListener</code>. */
1253: public void contentsChanged(ListDataEvent evt) {
1254: updateSelection();
1255: }
1256:
1257: public void vetoableChange(PropertyChangeEvent evt)
1258: throws PropertyVetoException {
1259: if (manager.PROP_SELECTED_NODES.equals(evt
1260: .getPropertyName())) {
1261: Node[] newNodes = (Node[]) evt.getNewValue();
1262:
1263: if (!selectionAccept(newNodes)) {
1264: throw new PropertyVetoException("", evt); // NOI18N
1265: }
1266: }
1267: }
1268:
1269: public void propertyChange(PropertyChangeEvent evt) {
1270: if (manager.PROP_SELECTED_NODES.equals(evt
1271: .getPropertyName())) {
1272: updateSelection();
1273:
1274: return;
1275: }
1276:
1277: if (ExplorerManager.PROP_EXPLORED_CONTEXT.equals(evt
1278: .getPropertyName())) {
1279: model.setNode(manager.getExploredContext());
1280:
1281: //System.out.println("Children: " + java.util.Arrays.asList (list.getValues ())); // NOI18N
1282: return;
1283: }
1284: }
1285:
1286: public void valueChanged(ListSelectionEvent e) {
1287: int curSize = model.getSize();
1288: int[] indices = list.getSelectedIndices();
1289:
1290: // bugfix #24193, check if the nodes in selection are in the view's root context
1291: List<Node> ll = new ArrayList<Node>(indices.length);
1292:
1293: for (int i = 0; i < indices.length; i++) {
1294: if (indices[i] < curSize) {
1295: Node n = Visualizer.findNode(model
1296: .getElementAt(indices[i]));
1297:
1298: if ((n == manager.getRootContext())
1299: || (n.getParentNode() != null)) {
1300: ll.add(n);
1301: }
1302: } else {
1303: // something went wrong?
1304: updateSelection();
1305:
1306: return;
1307: }
1308: }
1309:
1310: Node[] nodes = ll.toArray(new Node[ll.size()]);
1311:
1312: // forwarding TO E.M., so we won't listen to its cries for a while
1313: manager.removePropertyChangeListener(wlpc);
1314: manager.removeVetoableChangeListener(wlvc);
1315:
1316: try {
1317: selectionChanged(nodes, manager);
1318: } catch (PropertyVetoException ex) {
1319: // selection vetoed - restore previous selection
1320: updateSelection();
1321: } finally {
1322: manager.addPropertyChangeListener(wlpc);
1323: manager.addVetoableChangeListener(wlvc);
1324: }
1325: }
1326: }
1327:
1328: // Backspace jumps to parent folder of explored context
1329: private final class GoUpAction extends AbstractAction {
1330: static final long serialVersionUID = 1599999335583246715L;
1331:
1332: public GoUpAction() {
1333: super ("GoUpAction"); // NOI18N
1334: }
1335:
1336: public void actionPerformed(ActionEvent e) {
1337: if (traversalAllowed) {
1338: Node pan = manager.getExploredContext();
1339: pan = pan.getParentNode();
1340:
1341: if (pan != null) {
1342: manager.setExploredContext(pan, manager
1343: .getSelectedNodes());
1344: }
1345: }
1346: }
1347:
1348: public boolean isEnabled() {
1349: return true;
1350: }
1351: }
1352:
1353: //Enter key performObjectAt selected index.
1354: private final class EnterAction extends AbstractAction {
1355: static final long serialVersionUID = -239805141416294016L;
1356:
1357: public EnterAction() {
1358: super ("Enter"); // NOI18N
1359: }
1360:
1361: public void actionPerformed(ActionEvent e) {
1362: int index = list.getSelectedIndex();
1363: performObjectAt(index, e.getModifiers());
1364: }
1365:
1366: public boolean isEnabled() {
1367: return true;
1368: }
1369: }
1370: }
|