001: /*
002: * Sun Public License Notice
003: *
004: * The contents of this file are subject to the Sun Public License
005: * Version 1.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://www.sun.com/
008: *
009: * The Original Code is NetBeans. The Initial Developer of the Original
010: * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
011: * Microsystems, Inc. All Rights Reserved.
012: *
013: * This is a slightly modified version of PopupManager from the Netbeans project.
014: */
015:
016: package net.sourceforge.squirrel_sql.fw.completion;
017:
018: import java.awt.Rectangle;
019: import java.awt.event.KeyEvent;
020: import java.awt.event.KeyListener;
021:
022: import javax.swing.Action;
023: import javax.swing.ActionMap;
024: import javax.swing.InputMap;
025: import javax.swing.JComponent;
026: import javax.swing.JLayeredPane;
027: import javax.swing.JRootPane;
028: import javax.swing.KeyStroke;
029: import javax.swing.text.JTextComponent;
030:
031: import java.awt.event.ComponentAdapter;
032: import java.awt.event.ComponentEvent;
033: import javax.swing.SwingUtilities;
034: import java.awt.Component;
035: import javax.swing.JViewport;
036: import javax.swing.text.BadLocationException;
037:
038: /**
039: * Popup manager allows to display an arbitrary popup component
040: * over the underlying text component.
041: *
042: * @author Martin Roskanin, Miloslav Metelka
043: * @since 03/2002
044: */
045: public class PopupManager {
046:
047: private JComponent popup = null;
048: private JTextComponent textComponent;
049:
050: /** Place popup always above cursor */
051: public static final Placement Above = new Placement("Above"); //NOI18N
052:
053: /** Place popup always below cursor */
054: public static final Placement Below = new Placement("Below"); //NOI18N
055:
056: /** Place popup to larger area. i.e. if place below cursor is
057: larger than place above, then popup will be placed below cursor. */
058: public static final Placement Largest = new Placement("Largest"); //NOI18N
059:
060: /** Place popup above cursor. If a place above cursor is insufficient,
061: then popup will be placed below cursor. */
062: public static final Placement AbovePreferred = new Placement(
063: "AbovePreferred"); //NOI18N
064:
065: /** Place popup below cursor. If a place below cursor is insufficient,
066: then popup will be placed above cursor. */
067: public static final Placement BelowPreferred = new Placement(
068: "BelowPreferred"); //NOI18N
069:
070: private KeyListener keyListener;
071:
072: private TextComponentListener componentListener;
073:
074: /** Creates a new instance of PopupManager */
075: public PopupManager(JTextComponent textComponent) {
076: this .textComponent = textComponent;
077: keyListener = new PopupKeyListener();
078: textComponent.addKeyListener(keyListener);
079: componentListener = new TextComponentListener();
080: textComponent.addComponentListener(componentListener);
081: }
082:
083: /** Install popup component to textComponent root pane
084: * based on caret coordinates with the <CODE>Largest</CODE> placement.
085: * @param popup popup component to be installed into
086: * root pane of the text component.
087: */
088: public void install(JComponent popup) {
089: int caretPos = textComponent.getCaret().getDot();
090: try {
091: Rectangle caretBounds = textComponent.modelToView(caretPos);
092: install(popup, caretBounds, Largest);
093: } catch (BadLocationException e) {
094: // do not install if the caret position is invalid
095: }
096: }
097:
098: public void install(JComponent popup, Rectangle cursorBounds,
099: Placement placement) {
100: this .popup = popup;
101:
102: // Update the bounds of the popup
103: Rectangle bounds = computeBounds(this .popup, textComponent,
104: cursorBounds, placement);
105:
106: if (bounds != null) {
107: // Convert to layered pane's coordinates
108: bounds = SwingUtilities.convertRectangle(textComponent,
109: bounds, textComponent.getRootPane()
110: .getLayeredPane());
111: this .popup.setBounds(bounds);
112:
113: } else { // can't fit -> hide
114: this .popup.setVisible(false);
115: }
116:
117: /*
118: * CSE: moved this code down here to fix repaint problems on first
119: * display - bounds should be set before install
120: *
121: * Uninstall the old popup from root pane
122: * and install the new one. Even in case
123: * they are the same objects it's necessary
124: * to cover the workspace switches etc.
125: */
126: if (this .popup != null) {
127: removeFromRootPane(this .popup);
128: }
129: if (this .popup != null) {
130: installToRootPane(this .popup);
131: }
132: }
133:
134: /** Returns installed popup panel component */
135: public JComponent get() {
136: return popup;
137: }
138:
139: /** Install popup panel to current textComponent root pane */
140: private void installToRootPane(JComponent c) {
141: JRootPane rp = textComponent.getRootPane();
142: if (rp != null) {
143: rp.getLayeredPane().add(c, JLayeredPane.POPUP_LAYER, 0);
144: }
145: }
146:
147: /** Remove popup panel from previous textComponent root pane */
148: private void removeFromRootPane(JComponent c) {
149: JRootPane rp = c.getRootPane();
150: if (rp != null) {
151: rp.getLayeredPane().remove(c);
152: }
153: }
154:
155: /** Variation of the method for computing the bounds
156: * for the concrete view component. As the component can possibly
157: * be placed in a scroll pane it's first necessary
158: * to translate the cursor bounds and also translate
159: * back the resulting popup bounds.
160: * @param popup popup panel to be displayed
161: * @param view component over which the popup is displayed.
162: * @param cursorBounds the bounds of the caret or mouse cursor
163: * relative to the upper-left corner of the visible view.
164: * @param placement where to place the popup panel according to
165: * the cursor position.
166: * @return bounds of popup panel relative to the upper-left corner
167: * of the underlying view component.
168: * <CODE>null</CODE> if there is no place to display popup.
169: */
170: protected static Rectangle computeBounds(JComponent popup,
171: JComponent view, Rectangle cursorBounds, Placement placement) {
172:
173: Rectangle ret;
174: Component viewParent = view.getParent();
175: if (viewParent instanceof JViewport) {
176: Rectangle viewBounds = ((JViewport) viewParent)
177: .getViewRect();
178: Rectangle translatedCursorBounds = (Rectangle) cursorBounds
179: .clone();
180: translatedCursorBounds.translate(-viewBounds.x,
181: -viewBounds.y);
182:
183: ret = computeBounds(popup, viewBounds.width,
184: viewBounds.height, translatedCursorBounds,
185: placement);
186:
187: if (ret != null) { // valid bounds
188: ret.translate(viewBounds.x, viewBounds.y);
189: }
190:
191: } else { // not in scroll pane
192: ret = computeBounds(popup, view.getWidth(), view
193: .getHeight(), cursorBounds, placement);
194: }
195:
196: return ret;
197: }
198:
199: /** Computes a best-fit bounds of popup panel
200: * according to available space in the underlying view
201: * (visible part of the pane).
202: * The placement is first evaluated and put into the popup's client property
203: * by <CODE>popup.putClientProperty(Placement.class, actual-placement)</CODE>.
204: * The actual placement is <UL>
205: * <LI> <CODE>Above</CODE> if the original placement was <CODE>Above</CODE>.
206: * Or if the original placement was <CODE>AbovePreferred</CODE>
207: * or <CODE>Largest</CODE>
208: * and there is more space above the cursor than below it.
209: * <LI> <CODE>Below</CODE> if the original placement was <CODE>Below</CODE>.
210: * Or if the original placement was <CODE>BelowPreferred</CODE>
211: * or <CODE>Largest</CODE>
212: * and there is more space below the cursor than above it.
213: * <LI> <CODE>AbovePreferred</CODE> if the original placement
214: * was <CODE>AbovePreferred</CODE>
215: * and there is less space above the cursor than below it.
216: * <LI> <CODE>BelowPreferred</CODE> if the original placement
217: * was <CODE>BelowPreferred</CODE>
218: * and there is less space below the cursor than above it.
219: * <P>Once the placement client property is set
220: * the <CODE>popup.setSize()</CODE> is called with the size of the area
221: * above/below the cursor (indicated by the placement).
222: * The popup responds by updating its size to the equal or smaller
223: * size. If it cannot physically fit into the requested area
224: * it can call
225: * <CODE>putClientProperty(Placement.class, null)</CODE>
226: * on itself to indicate that it cannot fit. The method scans
227: * the content of the client property upon return from
228: * <CODE>popup.setSize()</CODE> and if it finds null there it returns
229: * null bounds in that case. The only exception is
230: * if the placement was either <CODE>AbovePreferred</CODE>
231: * or <CODE>BelowPreferred</CODE>. In that case the method
232: * gives it one more try
233: * by attempting to fit the popup into (bigger) complementary
234: * <CODE>Below</CODE> and <CODE>Above</CODE> areas (respectively).
235: * The popup either fits into these (bigger) areas or it again responds
236: * by returning <CODE>null</CODE> in the client property in which case
237: * the method finally gives up and returns null bounds.
238: *
239: * @param popup popup panel to be displayed
240: * @param viewWidth width of the visible view area.
241: * @param viewHeight height of the visible view area.
242: * @param cursorBounds the bounds of the caret or mouse cursor
243: * relative to the upper-left corner of the visible view
244: * @param placement where to place the popup panel according to
245: * the cursor position
246: * @return bounds of popup panel relative to the upper-left corner
247: * of the underlying view.
248: * <CODE>null</CODE> if there is no place to display popup.
249: */
250: protected static Rectangle computeBounds(JComponent popup,
251: int viewWidth, int viewHeight, Rectangle cursorBounds,
252: Placement placement) {
253:
254: if (placement == null) {
255: throw new NullPointerException("placement cannot be null"); // NOI18N
256: }
257:
258: // Compute available height above the cursor
259: int aboveCursorHeight = cursorBounds.y;
260: int belowCursorY = cursorBounds.y + cursorBounds.height;
261: int belowCursorHeight = viewHeight - belowCursorY;
262:
263: // resolve Largest and *Preferred placements if possible
264: if (placement == Largest) {
265: placement = (aboveCursorHeight < belowCursorHeight) ? Below
266: : Above;
267:
268: } else if (placement == AbovePreferred
269: && aboveCursorHeight > belowCursorHeight // more space above
270: ) {
271: placement = Above;
272:
273: } else if (placement == BelowPreferred
274: && belowCursorHeight > aboveCursorHeight // more space below
275: ) {
276: placement = Below;
277: }
278:
279: Rectangle popupBounds = null;
280:
281: while (true) { // do one or two passes
282: popup.putClientProperty(Placement.class, placement);
283:
284: int height = (placement == Above || placement == AbovePreferred) ? aboveCursorHeight
285: : belowCursorHeight;
286:
287: popup.setSize(viewWidth, height);
288: popupBounds = popup.getBounds();
289:
290: Placement updatedPlacement = (Placement) popup
291: .getClientProperty(Placement.class);
292:
293: if (updatedPlacement != placement) { // popup does not fit with the orig placement
294: if (placement == AbovePreferred
295: && updatedPlacement == null) {
296: placement = Below;
297: continue;
298:
299: } else if (placement == BelowPreferred
300: && updatedPlacement == null) {
301: placement = Above;
302: continue;
303: }
304: }
305:
306: if (updatedPlacement == null) {
307: popupBounds = null;
308: }
309:
310: break;
311: }
312:
313: if (popupBounds != null) {
314: //place popup according to caret position and Placement
315: popupBounds.x = Math.min(cursorBounds.x, viewWidth
316: - popupBounds.width);
317:
318: popupBounds.y = (placement == Above || placement == AbovePreferred) ? (aboveCursorHeight - popupBounds.height)
319: : belowCursorY;
320: }
321:
322: return popupBounds;
323: }
324:
325: /** Popup's key filter */
326: private class PopupKeyListener implements KeyListener {
327: public void keyTyped(KeyEvent e) {
328: }
329:
330: public void keyReleased(KeyEvent e) {
331: }
332:
333: public void keyPressed(KeyEvent e) {
334: if (e == null)
335: return;
336: if (popup != null && popup.isShowing()) {
337:
338: // get popup's registered keyboard actions
339: ActionMap am = popup.getActionMap();
340: InputMap im = popup.getInputMap();
341:
342: // check whether popup registers keystroke
343: Object obj = im.get(KeyStroke.getKeyStrokeForEvent(e));
344: if (obj != null) {
345: // if yes, gets the popup's action for this keystroke, perform it
346: // and consume key event
347: Action action = am.get(obj);
348: if (action != null) {
349: action.actionPerformed(null);
350: e.consume();
351: }
352: }
353: }
354: }
355:
356: }
357:
358: private final class TextComponentListener extends ComponentAdapter {
359: public void componentHidden(ComponentEvent evt) {
360: install(null); // hide popup
361: }
362: }
363:
364: /** Placement of popup panel specification */
365: public static final class Placement {
366:
367: private final String representation;
368:
369: private Placement(String representation) {
370: this .representation = representation;
371: }
372:
373: public String toString() {
374: return representation;
375: }
376:
377: }
378: }
|