001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.openide.explorer.view;
042:
043: import org.openide.explorer.*;
044: import org.openide.explorer.ExplorerManager.Provider;
045: import org.openide.nodes.*;
046: import org.openide.nodes.Node.Property;
047: import org.openide.util.HelpCtx;
048: import org.openide.util.Mutex;
049: import org.openide.util.NbBundle;
050: import org.openide.util.Utilities;
051:
052: import java.awt.*;
053: import java.awt.event.*;
054:
055: import java.beans.*;
056:
057: import java.io.*;
058:
059: import java.net.URL;
060:
061: import java.util.*;
062:
063: import javax.swing.*;
064: import javax.swing.event.*;
065:
066: /** An explorer view that shows the context hierarchy in
067: * a popup menu. Initially, it shows a left button which opens a popup
068: * menu from the root context and a right button which opens a popup menu from the currently
069: * explored context.
070: *
071: * <p>
072: * This class is a <q>view</q>
073: * to use it properly you need to add it into a component which implements
074: * {@link Provider}. Good examples of that can be found
075: * in {@link ExplorerUtils}. Then just use
076: * {@link Provider#getExplorerManager} call to get the {@link ExplorerManager}
077: * and control its state.
078: * </p>
079: * <p>
080: * There can be multiple <q>views</q> under one container implementing {@link Provider}. Select from
081: * range of predefined ones or write your own:
082: * </p>
083: * <ul>
084: * <li>{@link org.openide.explorer.view.BeanTreeView} - shows a tree of nodes</li>
085: * <li>{@link org.openide.explorer.view.ContextTreeView} - shows a tree of nodes without leaf nodes</li>
086: * <li>{@link org.openide.explorer.view.ListView} - shows a list of nodes</li>
087: * <li>{@link org.openide.explorer.view.IconView} - shows a rows of nodes with bigger icons</li>
088: * <li>{@link org.openide.explorer.view.ChoiceView} - creates a combo box based on the explored nodes</li>
089: * <li>{@link org.openide.explorer.view.TreeTableView} - shows tree of nodes together with a set of their {@link Property}</li>
090: * <li>{@link org.openide.explorer.view.MenuView} - can create a {@link JMenu} structure based on structure of {@link Node}s</li>
091: * </ul>
092: * <p>
093: * All of these views use {@link ExplorerManager#find} to walk up the AWT hierarchy and locate the
094: * {@link ExplorerManager} to use as a controler. They attach as listeners to
095: * it and also call its setter methods to update the shared state based on the
096: * user action. Not all views make sence together, but for example
097: * {@link org.openide.explorer.view.ContextTreeView} and {@link org.openide.explorer.view.ListView} were designed to complement
098: * themselves and behaves like windows explorer. The {@link org.openide.explorer.propertysheet.PropertySheetView}
099: * for example should be able to work with any other view.
100: * </p>
101: *
102: *
103: * @author Ian Formanek, Jaroslav Tulach
104: */
105: public class MenuView extends JPanel {
106: /** generated Serialized Version UID */
107: static final long serialVersionUID = -4970665063421766904L;
108:
109: /** default listener that opens explorer */
110: static final NodeAcceptor DEFAULT_LISTENER = new NodeAcceptor() {
111: public boolean acceptNodes(Node[] nodes) {
112: // don't allow multiple selections
113: if ((nodes == null) || (nodes.length != 1)) {
114: return false;
115: }
116:
117: Node n = nodes[0];
118: Action a = n.getPreferredAction();
119:
120: if ((a != null) && a.isEnabled()) {
121: a.actionPerformed(new ActionEvent(n, 0, "")); // NOI18N
122:
123: return true;
124: }
125:
126: return false;
127: }
128: };
129:
130: /** The explorerManager that manages this view */
131: transient private ExplorerManager explorerManager;
132:
133: /** button to open root view */
134: private JButton root;
135:
136: /** button to open view from current node */
137: private JButton current;
138:
139: /** property change listener */
140: transient private Listener listener;
141:
142: /* This is the constructor implementation
143: * recommended by ExplorerView class that only calls the inherited
144: * constructor and leaves the initialization for method initialize().
145: * @see #initialize */
146:
147: /** Construct a new menu view.
148: */
149: public MenuView() {
150: setLayout(new java.awt.FlowLayout());
151:
152: root = new JButton(NbBundle.getBundle(MenuView.class)
153: .getString("MenuViewStartFromRoot"));
154: add(root);
155:
156: current = new JButton(NbBundle.getBundle(MenuView.class)
157: .getString("MenuViewStartFromCurrent"));
158: add(current);
159:
160: init();
161: }
162:
163: /** Initializes listeners */
164: private void init() {
165: root.addMouseListener(listener = new Listener(true));
166: current.addMouseListener(new Listener(false));
167: }
168:
169: private void readObject(ObjectInputStream ois) throws IOException,
170: ClassNotFoundException {
171: ois.defaultReadObject();
172: init();
173: }
174:
175: /* Initializes view.
176: */
177: public void addNotify() {
178: super .addNotify();
179: explorerManager = ExplorerManager.find(this );
180: explorerManager.addPropertyChangeListener(listener);
181: doChecks();
182: }
183:
184: /* Deinitializes view.
185: */
186: public void removeNotify() {
187: super .removeNotify();
188: explorerManager.removePropertyChangeListener(listener);
189: explorerManager = null;
190: }
191:
192: /** Does some checks */
193: private void doChecks() {
194: current
195: .setEnabled(explorerManager.getSelectedNodes().length == 1);
196: }
197:
198: /** Acceptor that can be passed to constructor of {@link MenuView.Menu}.
199: * It permits determination of which nodes should be accepted upon a click.
200: *
201: * @deprecated This interface is almost the same as {@link NodeAcceptor}
202: * so it is redundant and obsoleted. Use {@link NodeAcceptor}
203: * interface instead.
204: */
205: public static @Deprecated
206: interface Acceptor {
207: /** Test whether to accept the node or not. Can also perform some actions (such as opening the node, etc.).
208: * @param n the node
209: * @return true if the <code>menu</code> should close
210: * @deprecated whole interface is obsoleted, use {@link NodeAcceptor#acceptNodes} instead.
211: */
212: public @Deprecated
213: boolean accept(Node n);
214: }
215:
216: /** Listener that opens the menu and listens to its actions
217: */
218: private class Listener extends MouseAdapter implements
219: NodeAcceptor, PropertyChangeListener {
220: /** from root */
221: private boolean root;
222:
223: public Listener(boolean root) {
224: this .root = root;
225: }
226:
227: public void mousePressed(MouseEvent e) {
228: if (e.getComponent().isEnabled()) {
229: // open the popup menu
230: Node context = null;
231:
232: if (!root) {
233: Node[] sel = explorerManager.getSelectedNodes();
234:
235: if (sel.length > 0) {
236: context = sel[0];
237: }
238: }
239:
240: if (context == null) {
241: context = explorerManager.getRootContext();
242: }
243:
244: Menu menu = new Menu(context, listener);
245:
246: JPopupMenu popupMenu = menu.getPopupMenu();
247: java.awt.Point p = new java.awt.Point(e.getX(), e
248: .getY());
249: p.x = e.getX() - p.x;
250: p.y = e.getY() - p.y;
251: SwingUtilities
252: .convertPointToScreen(p, e.getComponent());
253:
254: Dimension popupSize = popupMenu.getPreferredSize();
255: Rectangle screenBounds = Utilities
256: .getUsableScreenBounds(getGraphicsConfiguration());
257:
258: if ((p.x + popupSize.width) > (screenBounds.x + screenBounds.width)) {
259: p.x = (screenBounds.x + screenBounds.width)
260: - popupSize.width;
261: }
262:
263: if ((p.y + popupSize.height) > (screenBounds.y + screenBounds.height)) {
264: p.y = (screenBounds.y + screenBounds.height)
265: - popupSize.height;
266: }
267:
268: SwingUtilities.convertPointFromScreen(p, e
269: .getComponent());
270: popupMenu.show(e.getComponent(), p.x, p.y);
271: }
272: }
273:
274: /** Is the set of nodes acceptable?
275: * @param nodes the nodes to consider
276: * @return <CODE>true</CODE> if so
277: */
278: public boolean acceptNodes(Node[] nodes) {
279: // don't allow multiple selections
280: if ((nodes == null) || (nodes.length != 1)) {
281: return false;
282: }
283:
284: Node n = nodes[0];
285: Node parent = n.getParentNode();
286:
287: if (parent != null) {
288: explorerManager.setExploredContext(parent,
289: new Node[] { n });
290: }
291:
292: return true;
293: }
294:
295: public void propertyChange(PropertyChangeEvent ev) {
296: if (ExplorerManager.PROP_SELECTED_NODES.equals(ev
297: .getPropertyName())) {
298: doChecks();
299: }
300: }
301: }
302:
303: /** Menu item representing a node (with children) in a menu hierarchy.
304: * One can attach an acceptor to the menu that will be informed
305: * each time a user selects an item whether
306: * to close the menu or not.
307: * The submenu content is taken as a blocking snapshot of the Node's
308: * Children at the time of the submenu popup and does not reflect
309: * subsequent changes to Node's Children until next popup.
310: * This means that the Node's Children should properly implement
311: * blocking getNodes(true) in order to show proper content in MenuView.
312: */
313: public static class Menu extends org.openide.awt.JMenuPlus {
314: static final long serialVersionUID = -1505289666675423991L;
315:
316: /** The node represented. */
317: protected Node node;
318:
319: /** Action listener to attach to all menu items. */
320: protected NodeAcceptor action;
321:
322: /** A boolean flag that notes the content was already created */
323: private boolean filled = false;
324:
325: /** Constructor that assigns the node a default action.
326: * For example, open the Explorer or a property sheet.
327: * @param node node to represent
328: */
329: public Menu(Node node) {
330: this (node, DEFAULT_LISTENER);
331: }
332:
333: /** Constructor that permits specification of the action on the node.
334: *
335: * @param node node to represent
336: * @param action action called when node is selected
337: */
338: public Menu(Node node, NodeAcceptor action) {
339: this (node, action, true);
340: }
341:
342: /** @deprecated use {@link MenuView.Menu#MenuView.Menu(Node, NodeAcceptor)}
343: */
344: public @Deprecated
345: Menu(Node node, Acceptor action) {
346: this (node, new AcceptorProxy(action), true);
347: }
348:
349: /** @deprecated use {@link MenuView.Menu#MenuView.Menu(Node, NodeAcceptor, boolean)}
350: */
351: public @Deprecated
352: Menu(Node node, Acceptor action, boolean setName) {
353: this (node, new AcceptorProxy(action), setName);
354: }
355:
356: /** Constructor that permits specification of the action on the node,
357: * and permits overriding the name and icon of the menu.
358: *
359: * @param node node to represent
360: * @param action action called when node selected
361: * @param setName <code>true</code> to automatically set the name and icon of the item
362: */
363: public Menu(final Node node, NodeAcceptor action,
364: boolean setName) {
365: this .node = node;
366: this .action = action;
367:
368: if (setName) {
369: MenuItem.initialize(this , node);
370:
371: HelpCtx help = node.getHelpCtx();
372:
373: if ((help != null)
374: && !help.equals(HelpCtx.DEFAULT_HELP)
375: && (help.getHelpID() != null)) {
376: HelpCtx.setHelpIDString(this , help.getHelpID());
377: }
378: }
379: }
380:
381: /** Overriden to fill the submenu with the real content lazily */
382: public JPopupMenu getPopupMenu() {
383: final JPopupMenu popup = super .getPopupMenu();
384: fillSubmenu(popup);
385:
386: return popup;
387: }
388:
389: private void fillSubmenu(JPopupMenu popup) {
390: if (!filled) {
391: filled = true;
392:
393: Helper h = new Helper(popup);
394:
395: Node[] nodes = node.getChildren().getNodes(true);
396:
397: // Fill in the popup.
398: removeAll();
399:
400: for (int i = 0; i < nodes.length; i++)
401: add(createMenuItem(nodes[i]));
402:
403: // also work with empty element
404: if (getMenuComponentCount() == 0) {
405: add(createEmptyMenuItem());
406: }
407: }
408: }
409:
410: /** Checks for {@link MouseEvent#isPopupTrigger right click} to ask the acceptor whether
411: * to accept the selection.
412: * @param e the mouse event
413: * @param path used by the superclass
414: * @param manager used by the superclass
415: */
416: public void processMouseEvent(MouseEvent e, MenuElement[] path,
417: MenuSelectionManager manager) {
418: super .processMouseEvent(e, path, manager);
419:
420: if (e.isPopupTrigger()
421: && action.acceptNodes(new Node[] { node })) {
422: MenuSelectionManager.defaultManager()
423: .clearSelectedPath();
424: }
425: }
426:
427: /** Helper method. Creates empty menu item. */
428: private static JMenuItem createEmptyMenuItem() {
429: JMenuItem empty = new JMenuItem(NbBundle.getMessage(
430: MenuView.class, "EmptySubMenu"));
431:
432: empty.setEnabled(false);
433:
434: return empty;
435: }
436:
437: /** Create a menu element for a node. The default implementation creates
438: * {@link MenuView.MenuItem}s for leafs and <code>Menu</code> for other nodes.
439: *
440: * @param n node to create element for
441: * @return the created node
442: */
443: protected JMenuItem createMenuItem(Node n) {
444: return n.isLeaf() ? (JMenuItem) new MenuItem(n, action)
445: : (JMenuItem) new Menu(n, action);
446: }
447:
448: /** Little class that will reset our status on menu hide */
449: private class Helper implements PopupMenuListener {
450: private JPopupMenu popup;
451:
452: Helper(JPopupMenu master) {
453: popup = master;
454: popup.addPopupMenuListener(this );
455: }
456:
457: public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
458: filled = false; // clear the status and stop listening
459: popup.removePopupMenuListener(this );
460: }
461:
462: public void popupMenuCanceled(PopupMenuEvent e) {
463: }
464:
465: public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
466: }
467: }
468: }
469:
470: // End of class Menu.
471:
472: /** Proxy that allows to use now deprecated MenuView.Acceptor interface
473: * on places where NodeAcceptor is requested.
474: * This class can be deleted together with MenuView.Acceptor deletion.
475: */
476: private static final class AcceptorProxy implements NodeAcceptor {
477: private Acceptor original;
478:
479: AcceptorProxy(Acceptor original) {
480: this .original = original;
481: }
482:
483: public boolean acceptNodes(Node[] nodes) {
484: // don't allow multiple selections
485: if ((nodes == null) || (nodes.length != 1)) {
486: return false;
487: }
488:
489: return original.accept(nodes[0]);
490: }
491: }
492:
493: // end of AcceptorProxy inner class
494:
495: /** Menu item that can represent one node in the tree. */
496: public static class MenuItem extends JMenuItem implements
497: HelpCtx.Provider {
498: /** generated Serialized Version UID */
499: static final long serialVersionUID = -918973978614344429L;
500:
501: /** The node represented. */
502: protected Node node;
503:
504: /** The action listener to attach to all menu items. */
505: protected NodeAcceptor action;
506:
507: /** Construct item for given node with the node's default action.
508: * @param node the node to represent
509: */
510: public MenuItem(Node node) {
511: this (node, DEFAULT_LISTENER);
512: }
513:
514: /** Construct item for given node, specifying an action.
515: * @param node the node to represent
516: * @param l the acceptor to decide whether to accept this node or not
517: */
518: public MenuItem(Node node, NodeAcceptor l) {
519: this (node, l, true);
520: }
521:
522: /** @deprecated Use proper constructor with (@link NodeAcceptor). */
523: public @Deprecated
524: MenuItem(Node node, Acceptor action) {
525: this (node, new AcceptorProxy(action), true);
526: }
527:
528: /** @deprecated Use proper constructor with (@link NodeAcceptor). */
529: public @Deprecated
530: MenuItem(Node node, Acceptor action, boolean setName) {
531: this (node, new AcceptorProxy(action), setName);
532: }
533:
534: /** Construct item for given node, specifying the action and whether to create the icon and name automatically.
535: * @param node the node to represent
536: * @param l the acceptor to decide whether to accept this node or not
537: * @param setName <code>false</code> if the name and icon should not be set
538: */
539: public MenuItem(Node node, NodeAcceptor l, boolean setName) {
540: super ();
541:
542: this .node = node;
543: this .action = l;
544:
545: if (setName) {
546: initialize(this , node);
547: }
548:
549: // [pnejedly] HelpCtx is now provided through HelpCtx.Provider
550: // HelpCtx help = node.getHelpCtx ();
551: // if (help != null && ! help.equals (HelpCtx.DEFAULT_HELP) && help.getHelpID () != null)
552: // HelpCtx.setHelpIDString (this, help.getHelpID ());
553: }
554:
555: /**
556: * @return HelpCtx of the underlying node.
557: * @since 3.38
558: */
559: public HelpCtx getHelpCtx() {
560: return node.getHelpCtx();
561: }
562:
563: /** Inform the acceptor.
564: * @param time see superclass
565: */
566: public void doClick(int time) {
567: action.acceptNodes(new Node[] { node });
568: }
569:
570: /** Initialize an item for a node. */
571: static void initialize(final JMenuItem item, final Node node) {
572: final class NI implements Runnable, NodeListener,
573: ItemListener {
574: public void run() {
575: item
576: .setIcon(new ImageIcon(
577: node
578: .getIcon(java.beans.BeanInfo.ICON_COLOR_16x16)));
579: item.setText(node.getDisplayName());
580:
581: /*
582: item.setMargin(new java.awt.Insets(0, 0, 0, 0));
583: item.setHorizontalTextPosition(RIGHT);
584: item.setHorizontalAlignment(LEFT);
585: */
586: }
587:
588: public void childrenAdded(NodeMemberEvent ev) {
589: }
590:
591: public void childrenRemoved(NodeMemberEvent ev) {
592: }
593:
594: public void childrenReordered(NodeReorderEvent ev) {
595: }
596:
597: public void nodeDestroyed(NodeEvent ev) {
598: }
599:
600: /** Update a visualizer (change of name, icon, description, etc.)
601: */
602: public void propertyChange(PropertyChangeEvent ev) {
603: if (Node.PROP_ICON.equals(ev.getPropertyName())) {
604: Mutex.EVENT.readAccess(this );
605:
606: return;
607: }
608:
609: if (Node.PROP_DISPLAY_NAME.equals(ev
610: .getPropertyName())) {
611: Mutex.EVENT.readAccess(this );
612:
613: return;
614: }
615: }
616:
617: public void itemStateChanged(ItemEvent ev) {
618: }
619: }
620:
621: NI ni = new NI();
622:
623: // update this immediatelly
624: ni.run();
625:
626: // attach the listener to the menu item, to prevent it from garbage
627: // collection until the menu item exists
628: item.addItemListener(ni);
629:
630: // listen to changes in node, but weakly, to allow garbage collection
631: // event the node exists
632: node.addNodeListener(NodeOp.weakNodeListener(ni, node));
633: }
634: }
635: }
|