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:
042: package org.netbeans.editor;
043:
044: import java.awt.Dimension;
045: import java.awt.Point;
046: import java.awt.Rectangle;
047: import java.awt.event.KeyEvent;
048: import java.awt.event.KeyListener;
049:
050: import javax.swing.Action;
051: import javax.swing.ActionMap;
052: import javax.swing.InputMap;
053: import javax.swing.JComponent;
054: import javax.swing.JLayeredPane;
055: import javax.swing.JRootPane;
056: import javax.swing.KeyStroke;
057: import javax.swing.text.JTextComponent;
058:
059: import org.netbeans.editor.EditorUI;
060: import org.netbeans.editor.Utilities;
061: import java.awt.event.ComponentAdapter;
062: import java.awt.event.ComponentEvent;
063: import javax.swing.SwingUtilities;
064: import java.awt.Component;
065: import javax.swing.JViewport;
066: import javax.swing.text.BadLocationException;
067:
068: /**
069: * Popup manager allows to display an arbitrary popup component
070: * over the underlying text component.
071: *
072: * @author Martin Roskanin, Miloslav Metelka
073: * @since 03/2002
074: */
075: public class PopupManager {
076:
077: private JComponent popup = null;
078: private JTextComponent textComponent;
079:
080: /** Place popup always above cursor */
081: public static final Placement Above = new Placement("Above"); //NOI18N
082:
083: /** Place popup always below cursor */
084: public static final Placement Below = new Placement("Below"); //NOI18N
085:
086: /** Place popup to larger area. i.e. if place below cursor is
087: larger than place above, then popup will be placed below cursor. */
088: public static final Placement Largest = new Placement("Largest"); //NOI18N
089:
090: /** Place popup above cursor. If a place above cursor is insufficient,
091: then popup will be placed below cursor. */
092: public static final Placement AbovePreferred = new Placement(
093: "AbovePreferred"); //NOI18N
094:
095: /** Place popup below cursor. If a place below cursor is insufficient,
096: then popup will be placed above cursor. */
097: public static final Placement BelowPreferred = new Placement(
098: "BelowPreferred"); //NOI18N
099:
100: /** Place popup inside the scrollbar's viewport */
101: public static final HorizontalBounds ViewPortBounds = new HorizontalBounds(
102: "ViewPort"); //NOI18N
103:
104: /** Place popup inside the whole scrollbar */
105: public static final HorizontalBounds ScrollBarBounds = new HorizontalBounds(
106: "ScrollBar"); //NOI18N
107:
108: private KeyListener keyListener;
109:
110: private TextComponentListener componentListener;
111:
112: /** Creates a new instance of PopupManager */
113: public PopupManager(JTextComponent textComponent) {
114: this .textComponent = textComponent;
115: keyListener = new PopupKeyListener();
116: textComponent.addKeyListener(keyListener);
117: componentListener = new TextComponentListener();
118: textComponent.addComponentListener(componentListener);
119: }
120:
121: /** Install popup component to textComponent root pane
122: * based on caret coordinates with the <CODE>Largest</CODE> placement.
123: * Note: Make sure the component is properly uninstalled later,
124: * if it is not necessary. See issue #35325 for details.
125: * @param popup popup component to be installed into
126: * root pane of the text component.
127: */
128: public void install(JComponent popup) {
129: if (textComponent == null)
130: return;
131: int caretPos = textComponent.getCaret().getDot();
132: try {
133: Rectangle caretBounds = textComponent.modelToView(caretPos);
134: install(popup, caretBounds, Largest);
135: } catch (BadLocationException e) {
136: // do not install if the caret position is invalid
137: }
138: }
139:
140: /** Removes popup component from textComponent root pane
141: * @param popup popup component to be removed from
142: * root pane of the text component.
143: */
144: public void uninstall(JComponent popup) {
145: if (this .popup != null) {
146: if (this .popup.isVisible())
147: this .popup.setVisible(false);
148: removeFromRootPane(this .popup);
149: }
150:
151: if (popup != this .popup && popup != null) {
152: if (popup.isVisible())
153: popup.setVisible(false);
154: removeFromRootPane(popup);
155: }
156: }
157:
158: public void install(JComponent popup, Rectangle cursorBounds,
159: Placement placement, HorizontalBounds horizontalBounds,
160: int horizontalAdjustment, int verticalAdjustment) {
161: /* Uninstall the old popup from root pane
162: * and install the new one. Even in case
163: * they are the same objects it's necessary
164: * to cover the workspace switches etc.
165: */
166: if (this .popup != null) {
167: // if i.e. completion is visible and tooltip is being installed,
168: // completion popup should be closed.
169: if (this .popup.isVisible() && this .popup != popup)
170: this .popup.setVisible(false);
171: removeFromRootPane(this .popup);
172: }
173:
174: this .popup = popup;
175:
176: if (this .popup != null) {
177: installToRootPane(this .popup);
178: }
179:
180: // Update the bounds of the popup
181: Rectangle bounds = computeBounds(this .popup, textComponent,
182: cursorBounds, placement, horizontalBounds);
183:
184: if (bounds != null) {
185: // Convert to layered pane's coordinates
186:
187: if (horizontalBounds == ScrollBarBounds) {
188: bounds.x = 0;
189: }
190:
191: JRootPane rp = textComponent.getRootPane();
192: if (rp != null) {
193: bounds = SwingUtilities.convertRectangle(textComponent,
194: bounds, rp.getLayeredPane());
195: }
196:
197: if (horizontalBounds == ScrollBarBounds) {
198: if (textComponent.getParent() instanceof JViewport) {
199: int shift = textComponent.getParent().getX();
200: Rectangle viewBounds = ((JViewport) textComponent
201: .getParent()).getViewRect();
202: bounds.x += viewBounds.x;
203: bounds.x -= shift;
204: bounds.width += shift;
205: }
206: }
207:
208: bounds.x = bounds.x + horizontalAdjustment;
209: bounds.y = bounds.y + verticalAdjustment;
210: bounds.width = bounds.width - horizontalAdjustment;
211: bounds.height = bounds.height - verticalAdjustment;
212: this .popup.setBounds(bounds);
213:
214: } else { // can't fit -> hide
215: this .popup.setVisible(false);
216: }
217: }
218:
219: public void install(JComponent popup, Rectangle cursorBounds,
220: Placement placement, HorizontalBounds horizontalBounds) {
221: install(popup, cursorBounds, placement, ViewPortBounds, 0, 0);
222: }
223:
224: public void install(JComponent popup, Rectangle cursorBounds,
225: Placement placement) {
226: install(popup, cursorBounds, placement, ViewPortBounds);
227: }
228:
229: /** Returns installed popup panel component */
230: public JComponent get() {
231: return popup;
232: }
233:
234: /** Install popup panel to current textComponent root pane */
235: private void installToRootPane(JComponent c) {
236: JRootPane rp = textComponent.getRootPane();
237: if (rp != null) {
238: rp.getLayeredPane().add(c, JLayeredPane.POPUP_LAYER, 0);
239: }
240: }
241:
242: /** Remove popup panel from previous textComponent root pane */
243: private void removeFromRootPane(JComponent c) {
244: JRootPane rp = c.getRootPane();
245: if (rp != null) {
246: rp.getLayeredPane().remove(c);
247: }
248: }
249:
250: /** Variation of the method for computing the bounds
251: * for the concrete view component. As the component can possibly
252: * be placed in a scroll pane it's first necessary
253: * to translate the cursor bounds and also translate
254: * back the resulting popup bounds.
255: * @param popup popup panel to be displayed
256: * @param view component over which the popup is displayed.
257: * @param cursorBounds the bounds of the caret or mouse cursor
258: * relative to the upper-left corner of the visible view.
259: * @param placement where to place the popup panel according to
260: * the cursor position.
261: * @return bounds of popup panel relative to the upper-left corner
262: * of the underlying view component.
263: * <CODE>null</CODE> if there is no place to display popup.
264: */
265: protected static Rectangle computeBounds(JComponent popup,
266: JComponent view, Rectangle cursorBounds,
267: Placement placement, HorizontalBounds horizontalBounds) {
268:
269: if (horizontalBounds == null)
270: horizontalBounds = ViewPortBounds;
271:
272: Rectangle ret;
273: Component viewParent = view.getParent();
274:
275: if (viewParent instanceof JViewport) {
276: Rectangle viewBounds = ((JViewport) viewParent)
277: .getViewRect();
278:
279: Rectangle translatedCursorBounds = (Rectangle) cursorBounds
280: .clone();
281: translatedCursorBounds.translate(-viewBounds.x,
282: -viewBounds.y);
283:
284: ret = computeBounds(popup, viewBounds.width,
285: viewBounds.height, translatedCursorBounds,
286: placement, horizontalBounds);
287:
288: if (ret != null) { // valid bounds
289: ret.translate(viewBounds.x, viewBounds.y);
290: }
291:
292: } else { // not in scroll pane
293: ret = computeBounds(popup, view.getWidth(), view
294: .getHeight(), cursorBounds, placement);
295: }
296:
297: return ret;
298: }
299:
300: protected static Rectangle computeBounds(JComponent popup,
301: JComponent view, Rectangle cursorBounds, Placement placement) {
302: return computeBounds(popup, view, cursorBounds, placement,
303: ViewPortBounds);
304: }
305:
306: /** Computes a best-fit bounds of popup panel
307: * according to available space in the underlying view
308: * (visible part of the pane).
309: * The placement is first evaluated and put into the popup's client property
310: * by <CODE>popup.putClientProperty(Placement.class, actual-placement)</CODE>.
311: * The actual placement is <UL>
312: * <LI> <CODE>Above</CODE> if the original placement was <CODE>Above</CODE>.
313: * Or if the original placement was <CODE>AbovePreferred</CODE>
314: * or <CODE>Largest</CODE>
315: * and there is more space above the cursor than below it.
316: * <LI> <CODE>Below</CODE> if the original placement was <CODE>Below</CODE>.
317: * Or if the original placement was <CODE>BelowPreferred</CODE>
318: * or <CODE>Largest</CODE>
319: * and there is more space below the cursor than above it.
320: * <LI> <CODE>AbovePreferred</CODE> if the original placement
321: * was <CODE>AbovePreferred</CODE>
322: * and there is less space above the cursor than below it.
323: * <LI> <CODE>BelowPreferred</CODE> if the original placement
324: * was <CODE>BelowPreferred</CODE>
325: * and there is less space below the cursor than above it.
326: * <P>Once the placement client property is set
327: * the <CODE>popup.setSize()</CODE> is called with the size of the area
328: * above/below the cursor (indicated by the placement).
329: * The popup responds by updating its size to the equal or smaller
330: * size. If it cannot physically fit into the requested area
331: * it can call
332: * <CODE>putClientProperty(Placement.class, null)</CODE>
333: * on itself to indicate that it cannot fit. The method scans
334: * the content of the client property upon return from
335: * <CODE>popup.setSize()</CODE> and if it finds null there it returns
336: * null bounds in that case. The only exception is
337: * if the placement was either <CODE>AbovePreferred</CODE>
338: * or <CODE>BelowPreferred</CODE>. In that case the method
339: * gives it one more try
340: * by attempting to fit the popup into (bigger) complementary
341: * <CODE>Below</CODE> and <CODE>Above</CODE> areas (respectively).
342: * The popup either fits into these (bigger) areas or it again responds
343: * by returning <CODE>null</CODE> in the client property in which case
344: * the method finally gives up and returns null bounds.
345: *
346: * @param popup popup panel to be displayed
347: * @param viewWidth width of the visible view area.
348: * @param viewHeight height of the visible view area.
349: * @param cursorBounds the bounds of the caret or mouse cursor
350: * relative to the upper-left corner of the visible view
351: * @param placement where to place the popup panel according to
352: * the cursor position
353: * @return bounds of popup panel relative to the upper-left corner
354: * of the underlying view.
355: * <CODE>null</CODE> if there is no place to display popup.
356: */
357: protected static Rectangle computeBounds(JComponent popup,
358: int viewWidth, int viewHeight, Rectangle cursorBounds,
359: Placement placement, HorizontalBounds horizontalBounds) {
360:
361: if (placement == null) {
362: throw new NullPointerException("placement cannot be null"); // NOI18N
363: }
364:
365: // Compute available height above the cursor
366: int aboveCursorHeight = cursorBounds.y;
367: int belowCursorY = cursorBounds.y + cursorBounds.height;
368: int belowCursorHeight = viewHeight - belowCursorY;
369:
370: // Resolve *Preferred placements first
371: if (placement == AbovePreferred || placement == BelowPreferred) {
372: int prefHeight = popup.getPreferredSize().height;
373: if (placement == AbovePreferred) {
374: placement = (prefHeight <= aboveCursorHeight) ? Above
375: : Largest;
376: } else { // BelowPreferred
377: placement = (prefHeight <= belowCursorHeight) ? Below
378: : Largest;
379: }
380: }
381:
382: // Resolve Largest placement
383: if (placement == Largest) {
384: placement = (aboveCursorHeight < belowCursorHeight) ? Below
385: : Above;
386: }
387:
388: Rectangle popupBounds = null;
389:
390: while (true) { // do one or two passes
391: popup.putClientProperty(Placement.class, placement);
392:
393: int height = (placement == Above || placement == AbovePreferred) ? aboveCursorHeight
394: : belowCursorHeight;
395:
396: popup.setSize(viewWidth, height);
397: popupBounds = popup.getBounds();
398:
399: Placement updatedPlacement = (Placement) popup
400: .getClientProperty(Placement.class);
401:
402: if (updatedPlacement != placement) { // popup does not fit with the orig placement
403: if (placement == AbovePreferred
404: && updatedPlacement == null) {
405: placement = Below;
406: continue;
407:
408: } else if (placement == BelowPreferred
409: && updatedPlacement == null) {
410: placement = Above;
411: continue;
412: }
413: }
414:
415: if (updatedPlacement == null) {
416: popupBounds = null;
417: }
418:
419: break;
420: }
421:
422: if (popupBounds != null) {
423: //place popup according to caret position and Placement
424: popupBounds.x = Math.min(cursorBounds.x, viewWidth
425: - popupBounds.width);
426:
427: popupBounds.y = (placement == Above || placement == AbovePreferred) ? (aboveCursorHeight - popupBounds.height)
428: : belowCursorY;
429: }
430:
431: return popupBounds;
432: }
433:
434: protected static Rectangle computeBounds(JComponent popup,
435: int viewWidth, int viewHeight, Rectangle cursorBounds,
436: Placement placement) {
437: return computeBounds(popup, viewWidth, viewHeight,
438: cursorBounds, placement, ViewPortBounds);
439: }
440:
441: /** Popup's key filter */
442: private class PopupKeyListener implements KeyListener {
443:
444: public void keyTyped(KeyEvent e) {
445: }
446:
447: public void keyReleased(KeyEvent e) {
448: }
449:
450: public void keyPressed(KeyEvent e) {
451: if (e == null)
452: return;
453: if (popup != null && popup.isShowing()) {
454:
455: // get popup's registered keyboard actions
456: ActionMap am = popup.getActionMap();
457: InputMap im = popup.getInputMap();
458:
459: // check whether popup registers keystroke
460: Object obj = im.get(KeyStroke.getKeyStrokeForEvent(e));
461: if (obj != null) {
462: // if yes, gets the popup's action for this keystroke, perform it
463: // and consume key event
464: Action action = am.get(obj);
465: if (action != null) {
466: action.actionPerformed(null);
467: e.consume();
468: }
469: }
470: }
471: }
472:
473: }
474:
475: private final class TextComponentListener extends ComponentAdapter {
476:
477: public void componentHidden(ComponentEvent evt) {
478: install(null); // hide popup
479: }
480:
481: }
482:
483: /** Placement of popup panel specification */
484: public static final class Placement {
485:
486: private final String representation;
487:
488: private Placement(String representation) {
489: this .representation = representation;
490: }
491:
492: public String toString() {
493: return representation;
494: }
495:
496: }
497:
498: /** Horizontal bounds of popup panel specification */
499: public static final class HorizontalBounds {
500:
501: private final String representation;
502:
503: private HorizontalBounds(String representation) {
504: this .representation = representation;
505: }
506:
507: public String toString() {
508: return representation;
509: }
510:
511: }
512:
513: }
|