001: /*
002: * @(#)JideMenu.java
003: *
004: * Copyright 2002 - 2004 JIDE Software Inc. All rights reserved.
005: */
006: package com.jidesoft.swing;
007:
008: import com.jidesoft.plaf.UIDefaultsLookup;
009:
010: import javax.swing.*;
011: import javax.swing.event.MenuEvent;
012: import javax.swing.event.MenuListener;
013: import java.awt.*;
014: import java.awt.event.ActionEvent;
015: import java.awt.event.ActionListener;
016:
017: /**
018: * A special implementation of JMenu. It is used to replace JMenu in order to use with CommandBar.
019: * <br>
020: * It has two special features.
021: * First, it has a PopupMenuCustomizer for lazy menu creation. Instead of creating menu upfront which might be
022: * quite expensive, you can create it using PopupMenuCustomizer. PopupMenuCustomizer is called before
023: * the menu is set visible. Please note, when you use PopupMenuCustomizer, you need to remove the old menu items you added previously using
024: * PopupMenuCustomizer. Otherwise, you will see a menu which gets longer and longer when you show it. See below for an example.
025: * <code><pre>
026: * JideMenu jideMenu = new JideMenu("Dynamic");
027: * jideMenu.setPopupMenuCustomizer(new JideMenu.PopupMenuCustomizer(){
028: * public void customize(JPopupMenu menu) {
029: * menu.add("item 1");
030: * menu.add("item 2");
031: * menu.add("item 3");
032: * menu.add("item 4");
033: * menu.add("item 5");
034: * }
035: * });
036: * </pre></code>
037: * <p/>
038: * Second feature is popup alignment. Usually menu and its popup align to the left side. In our case, we hope
039: * they align to right side. So we added a method call setPreferredPopupHorizontalAlignment(). You can set
040: * to RIGHT if you want to.
041: * <p/>
042: */
043: public class JideMenu extends JMenu implements Alignable {
044:
045: private int _preferredPopupHorizontalAlignment = LEFT;
046:
047: private int _preferredPopupVerticalAlignment = BOTTOM;
048:
049: private MenuCreator _menuCreator;
050:
051: private PopupMenuCustomizer _customizer;
052:
053: public static int DELAY = 400;
054:
055: private int _orientation;
056:
057: public JideMenu() {
058: initMenu();
059: }
060:
061: public JideMenu(String s) {
062: super (s);
063: initMenu();
064: }
065:
066: public JideMenu(Action a) {
067: super (a);
068: initMenu();
069: }
070:
071: public JideMenu(String s, boolean b) {
072: super (s, b);
073: initMenu();
074: }
075:
076: protected void initMenu() {
077: // setDelay(DELAY);
078: addMenuListener(new MenuListener() {
079: public void menuSelected(MenuEvent e) {
080: MenuCreator menuCreator;
081: if ((menuCreator = getMenuCreator()) != null) {
082: menuCreator.createMenu();
083: if (getPopupMenu().getComponentCount() == 0) {
084: return;
085: }
086: }
087:
088: PopupMenuCustomizer customizer;
089: if ((customizer = getPopupMenuCustomizer()) != null) {
090: customizer.customize(getPopupMenu());
091: if (getPopupMenu().getComponentCount() == 0) {
092: return;
093: }
094: }
095: }
096:
097: public void menuDeselected(MenuEvent e) {
098: }
099:
100: public void menuCanceled(MenuEvent e) {
101: }
102: });
103: }
104:
105: /**
106: * Checks if the menu is added to a top level menu container. It will be consider as top level menu when
107: * <br> 1. getParent() equals null, or
108: * <br> 2. getParent() is not an instance of JPopupMenu
109: * <br> Please note, the definition of topLevelMenu is different from that of JMenu.
110: *
111: * @return true if it's top level menu.
112: */
113: @Override
114: public boolean isTopLevelMenu() {
115: return getParent() == null
116: || !(getParent() instanceof JPopupMenu);//TopLevelMenuContainer || getParent() instanceof JMenuBar;
117: }
118:
119: /**
120: * @deprecated The createMenu method of MenuCreator should JPopupMenu as parameter. Since it's a public API
121: * we have to deprecated this one and ask users to use {@link PopupMenuCustomizer} instead.
122: */
123: public interface MenuCreator {
124: void createMenu();
125: }
126:
127: /**
128: * Customizes the popup menu. This method will be called every time before popup menu is set visible.
129: */
130: public interface PopupMenuCustomizer {
131: void customize(JPopupMenu menu);
132: }
133:
134: /**
135: * Gets the MenuCreator.
136: *
137: * @return the MenuCreator.
138: * @deprecated use{@link PopupMenuCustomizer} and {@link #getPopupMenuCustomizer()} instead.
139: */
140: public MenuCreator getMenuCreator() {
141: return _menuCreator;
142: }
143:
144: /**
145: * Sets the MenuCreator. MenuCreator can be used to do lazy menu creation. If you put code
146: * in the MenuCreator, it won't be called until before the menu is set visible.
147: *
148: * @param menuCreator
149: * @deprecated use{@link PopupMenuCustomizer} and {@link #setPopupMenuCustomizer(com.jidesoft.swing.JideMenu.PopupMenuCustomizer)} instead.
150: */
151: public void setMenuCreator(MenuCreator menuCreator) {
152: _menuCreator = menuCreator;
153: }
154:
155: /**
156: * Gets the PopupMenuCustomizer.
157: *
158: * @return the PopupMenuCustomizer.
159: */
160: public PopupMenuCustomizer getPopupMenuCustomizer() {
161: return _customizer;
162: }
163:
164: /**
165: * Sets the PopupMenuCustomizer. PopupMenuCustomizer can be used to do lazy menu creation. If you put code
166: * in the MenuCreator, it won't be called until before the menu is set visible.
167: * <p/>
168: * PopupMenuCustomizer has a customize method. The popup menu of this menu will be passed in.
169: * You can add/remove/change the menu items in customize method. For example, instead of
170: * <code><pre>
171: * JideMenu menu = new JideMenu();
172: * menu.add(new JMenuItem("..."));
173: * menu.add(new JMenuItem("..."));
174: * </pre></code>
175: * You can do
176: * <code><pre>
177: * JideMenu menu = new JideMenu();
178: * menu.setPopupMenuCustomzier(new JideMenu.PopupMenuCustomizer() {
179: * void customize(JPopupMenu popupMenu) {
180: * poupMenu.removeAll();
181: * popupMenu.add(new JMenuItem("..."));
182: * popupMenu.add(new JMenuItem("..."));
183: * }
184: * }
185: * </pre></code>
186: * If the menu is never used, the two add methods will never be called thus improve the performance.
187: *
188: * @param customizer
189: */
190: public void setPopupMenuCustomizer(PopupMenuCustomizer customizer) {
191: _customizer = customizer;
192: }
193:
194: @Override
195: protected Point getPopupMenuOrigin() {
196: int x = 0;
197: int y = 0;
198: JPopupMenu pm = getPopupMenu();
199:
200: // Figure out the sizes needed to caclulate the menu position
201: Dimension s = getSize();
202: Dimension pmSize = pm.getPreferredSize();
203:
204: Point position = getLocationOnScreen();
205: Toolkit toolkit = Toolkit.getDefaultToolkit();
206: GraphicsConfiguration gc = getGraphicsConfiguration();
207: Rectangle screenBounds = new Rectangle(toolkit.getScreenSize());
208: GraphicsEnvironment ge = GraphicsEnvironment
209: .getLocalGraphicsEnvironment();
210: GraphicsDevice[] gd = ge.getScreenDevices();
211: for (int i = 0; i < gd.length; i++) {
212: if (gd[i].getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
213: GraphicsConfiguration dgc = gd[i]
214: .getDefaultConfiguration();
215: if (dgc.getBounds().contains(position)) {
216: gc = dgc;
217: break;
218: }
219: }
220: }
221:
222: if (gc != null) {
223: screenBounds = gc.getBounds();
224: // take screen insets (e.g. taskbar) into account
225: Insets screenInsets = toolkit.getScreenInsets(gc);
226:
227: screenBounds.width -= Math.abs(screenInsets.left
228: + screenInsets.right);
229: screenBounds.height -= Math.abs(screenInsets.top
230: + screenInsets.bottom);
231: position.x -= Math.abs(screenInsets.left);
232: position.y -= Math.abs(screenInsets.top);
233: }
234:
235: Container parent = getParent();
236: if (parent instanceof JPopupMenu) {
237: // We are a submenu (pull-right)
238: int xOffset = UIDefaultsLookup
239: .getInt("Menu.submenuPopupOffsetX");
240: int yOffset = UIDefaultsLookup
241: .getInt("Menu.submenuPopupOffsetY");
242:
243: if (this .getComponentOrientation().isLeftToRight()) {
244: if (JideSwingUtilities.getOrientationOf(this ) == HORIZONTAL) {
245: // First determine x:
246: x = s.width + xOffset; // Prefer placement to the right
247: if (position.x + x + pmSize.width >= screenBounds.width
248: + screenBounds.x
249: &&
250: // popup doesn't fit - place it wherever there's more room
251: screenBounds.width - s.width < 2 * (position.x - screenBounds.x)) {
252:
253: x = 0 - xOffset - pmSize.width;
254: }
255: } else {
256: // First determine x:
257: x = s.width + xOffset; // Prefer placement to the right
258: if (position.x + x + pmSize.width >= screenBounds.width
259: + screenBounds.x
260: &&
261: // popup doesn't fit - place it wherever there's more room
262: screenBounds.width - s.width < 2 * (position.x - screenBounds.x)) {
263:
264: x = 0 - xOffset - pmSize.width;
265: }
266: }
267: } else {
268: // First determine x:
269: x = 0 - xOffset - pmSize.width; // Prefer placement to the left
270: if (position.x + x < screenBounds.x &&
271: // popup doesn't fit - place it wherever there's more room
272: screenBounds.width - s.width > 2 * (position.x - screenBounds.x)) {
273:
274: x = s.width + xOffset;
275: }
276: }
277: // Then the y:
278: y = yOffset; // Prefer dropping down
279: if (position.y + y + pmSize.height >= screenBounds.height
280: + screenBounds.y
281: &&
282: // popup doesn't fit - place it wherever there's more room
283: screenBounds.height - s.height < 2 * (position.y - screenBounds.y)) {
284:
285: y = s.height - yOffset - pmSize.height;
286: }
287: } else {
288: // We are a toplevel menu (pull-down)
289: int xOffset = UIDefaultsLookup
290: .getInt("Menu.menuPopupOffsetX");
291: int yOffset = UIDefaultsLookup
292: .getInt("Menu.menuPopupOffsetY");
293:
294: if (this .getComponentOrientation().isLeftToRight()) {
295: if (JideSwingUtilities.getOrientationOf(this ) == HORIZONTAL) {
296: // First determine the x:
297: if (getPreferredPopupHorizontalAlignment() == LEFT) {
298: x = xOffset; // Extend to the right
299: if (position.x + x + pmSize.width >= screenBounds.width
300: + screenBounds.x
301: &&
302: // popup doesn't fit - place it wherever there's more room
303: screenBounds.width - s.width < 2 * (position.x - screenBounds.x)) {
304:
305: x = s.width - xOffset - pmSize.width;
306: }
307: } else {
308: x = -pmSize.width + xOffset + s.width; // align right
309: if (position.x + x < screenBounds.x) {
310: x = screenBounds.x - position.x;
311: }
312: }
313: } else {
314: // First determine the x:
315: x = -xOffset - pmSize.width; // Extend to the left
316: if (position.x + x < screenBounds.x &&
317: // popup doesn't fit - place it wherever there's more room
318: screenBounds.width - s.width > 2 * (position.x - screenBounds.x)) {
319:
320: x = s.width + xOffset;
321: }
322: }
323: } else {
324: // TODO: when RTL - consider vertical case
325: // First determine the x:
326: x = s.width - xOffset - pmSize.width; // Extend to the left
327: if (position.x + x < screenBounds.x &&
328: // popup doesn't fit - place it wherever there's more room
329: screenBounds.width - s.width > 2 * (position.x - screenBounds.x)) {
330:
331: x = xOffset;
332: }
333: }
334:
335: // Then the y:
336: if (JideSwingUtilities.getOrientationOf(this ) == HORIZONTAL) {
337: y = s.height + yOffset; // Prefer dropping down
338: if (position.y + y + pmSize.height >= screenBounds.height
339: &&
340: // popup doesn't fit - place it wherever there's more room
341: screenBounds.height - s.height < 2 * (position.y - screenBounds.y)) {
342:
343: y = 0 - yOffset - pmSize.height; // Otherwise drop 'up'
344: }
345: } else {
346: y = -yOffset; // Prefer dropping up
347: if (position.y + y + pmSize.height >= screenBounds.height
348: &&
349: // popup doesn't fit - place it wherever there's more room
350: screenBounds.height - s.height < 2 * (position.y - screenBounds.y)) {
351:
352: y = 0 - yOffset - pmSize.height; // Otherwise drop 'up'
353: }
354: }
355: }
356:
357: return new Point(x, y);
358: }
359:
360: /**
361: * Checks if the
362: *
363: * @return false if it's top leve menu. Otherwise, it will return what super.isOpaque().
364: */
365: @Override
366: public boolean isOpaque() {
367: if (isTopLevelMenu()) { // make top level menu opaque
368: return false;
369: } else {
370: return super .isOpaque();
371: }
372: }
373:
374: public boolean originalIsOpaque() {
375: return super .isOpaque();
376: }
377:
378: protected void hideMenu() {
379: MenuSelectionManager msm = MenuSelectionManager
380: .defaultManager();
381: msm.clearSelectedPath();
382: }
383:
384: public int getPreferredPopupHorizontalAlignment() {
385: return _preferredPopupHorizontalAlignment;
386: }
387:
388: public void setPreferredPopupHorizontalAlignment(
389: int preferredPopupHorizontalAlignment) {
390: _preferredPopupHorizontalAlignment = preferredPopupHorizontalAlignment;
391: }
392:
393: public int getPreferredPopupVerticalAlignment() {
394: return _preferredPopupVerticalAlignment;
395: }
396:
397: public void setPreferredPopupVerticalAlignment(
398: int preferredPopupVerticalAlignment) {
399: _preferredPopupVerticalAlignment = preferredPopupVerticalAlignment;
400: }
401:
402: public boolean supportVerticalOrientation() {
403: return true;
404: }
405:
406: public boolean supportHorizontalOrientation() {
407: return true;
408: }
409:
410: public void setOrientation(int orientation) {
411: int old = _orientation;
412: if (old != orientation) {
413: _orientation = orientation;
414: firePropertyChange(PROPERTY_ORIENTATION, old, orientation);
415: }
416: }
417:
418: public int getOrientation() {
419: return _orientation;
420: }
421:
422: private static JideMenu _pendingMenu;
423:
424: private static HideTimer _timer;
425:
426: // use this flag to disable the hide timer as there are quite a few bugs on it that we don't know how to solve.
427: private final static boolean DISABLE_TIMER = true;
428:
429: @Override
430: public void setPopupMenuVisible(boolean b) {
431: if (b && getPopupMenu().getComponentCount() == 0) {
432: return;
433: }
434:
435: if (!DISABLE_TIMER) {
436: if (isTopLevelMenu()) {
437: setPopupMenuVisibleImmediately(b);
438: } else {
439: if (b) {
440: // System.out.println("show new menu");
441: stopTimer();
442: setPopupMenuVisibleImmediately(b);
443: } else {
444: // HACK: check if the calling stack has clearSelectedPath method.
445: StackTraceElement[] stackTraceElements = new Throwable()
446: .getStackTrace();
447: for (int i = 0; i < stackTraceElements.length; i++) {
448: StackTraceElement stackTraceElement = stackTraceElements[i];
449: if (stackTraceElement.getMethodName().equals(
450: "clearSelectedPath")) {
451: setPopupMenuVisibleImmediately(b);
452: return;
453: }
454: }
455: startTimer();
456: }
457: }
458: } else {
459: setPopupMenuVisibleImmediately(b);
460: }
461: }
462:
463: void setPopupMenuVisibleImmediately(boolean b) {
464: super .setPopupMenuVisible(b);
465: }
466:
467: private class HideTimer extends Timer implements ActionListener {
468: public HideTimer() {
469: super (DELAY + 300, null);
470: addActionListener(this );
471: setRepeats(false);
472: }
473:
474: public void actionPerformed(ActionEvent e) {
475: stopTimer();
476: }
477: }
478:
479: private void startTimer() {
480: // System.out.println("timer started");
481: if (_timer != null) {
482: stopTimer();
483: }
484: _pendingMenu = this ;
485: _timer = new HideTimer();
486: _timer.start();
487: }
488:
489: private void stopTimer() {
490: if (_timer != null) {
491: // System.out.println("timer stopped");
492: if (_pendingMenu != null) {
493: // System.out.println("hidding pending menu");
494: _pendingMenu.setPopupMenuVisibleImmediately(false);
495: _pendingMenu = null;
496: }
497: _timer.stop();
498: _timer = null;
499: }
500: }
501:
502: }
|