2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* The Original Software is NetBeans. The Initial Developer of the Original
27
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28
* Microsystems, Inc. All Rights Reserved.
30
* If you wish your version of this file to be governed by only the CDDL
31
* or only the GPL Version 2, indicate your decision by adding
32
* "[Contributor] elects to include this software in this distribution
33
* under the [CDDL or GPL Version 2] license." If you do not indicate a
34
* single choice of license, a recipient has the option to distribute
35
* your version of this file under either the CDDL, the GPL Version 2 or
36
* to extend the choice of license to its licensees as provided above.
37
* However, if you add GPL Version 2 code and therefore, elected the GPL
38
* Version 2 license, then the option applies only if the new code is
39
* made subject to such option by the copyright holder.
42
package org.netbeans.editor;
44
import java.awt.Dimension;
45
import java.awt.Point;
46
import java.awt.Rectangle;
47
import java.awt.event.KeyEvent;
48
import java.awt.event.KeyListener;
50
import javax.swing.Action;
51
import javax.swing.ActionMap;
52
import javax.swing.InputMap;
53
import javax.swing.JComponent;
54
import javax.swing.JLayeredPane;
55
import javax.swing.JRootPane;
56
import javax.swing.KeyStroke;
57
import javax.swing.text.JTextComponent;
59
import org.netbeans.editor.EditorUI;
60
import org.netbeans.editor.Utilities;
61
import java.awt.event.ComponentAdapter;
62
import java.awt.event.ComponentEvent;
63
import javax.swing.SwingUtilities;
64
import java.awt.Component;
65
import javax.swing.JViewport;
66
import javax.swing.text.BadLocationException;
70
* Popup manager allows to display an arbitrary popup component
71
* over the underlying text component.
73
* @author Martin Roskanin, Miloslav Metelka
76
public class PopupManager {
78
private JComponent popup = null;
79
private JTextComponent textComponent;
81
/** Place popup always above cursor */
82
public static final Placement Above = new Placement("Above"); //NOI18N
84
/** Place popup always below cursor */
85
public static final Placement Below = new Placement("Below"); //NOI18N
87
/** Place popup to larger area. i.e. if place below cursor is
88
larger than place above, then popup will be placed below cursor. */
89
public static final Placement Largest = new Placement("Largest"); //NOI18N
91
/** Place popup above cursor. If a place above cursor is insufficient,
92
then popup will be placed below cursor. */
93
public static final Placement AbovePreferred = new Placement("AbovePreferred"); //NOI18N
95
/** Place popup below cursor. If a place below cursor is insufficient,
96
then popup will be placed above cursor. */
97
public static final Placement BelowPreferred = new Placement("BelowPreferred"); //NOI18N
99
/** Place popup inside the scrollbar's viewport */
100
public static final HorizontalBounds ViewPortBounds = new HorizontalBounds("ViewPort"); //NOI18N
102
/** Place popup inside the whole scrollbar */
103
public static final HorizontalBounds ScrollBarBounds = new HorizontalBounds("ScrollBar"); //NOI18N
105
private KeyListener keyListener;
107
private TextComponentListener componentListener;
109
/** Creates a new instance of PopupManager */
110
public PopupManager(JTextComponent textComponent) {
111
this.textComponent = textComponent;
112
keyListener = new PopupKeyListener();
113
textComponent.addKeyListener(keyListener);
114
componentListener = new TextComponentListener();
115
textComponent.addComponentListener(componentListener);
118
/** Install popup component to textComponent root pane
119
* based on caret coordinates with the <CODE>Largest</CODE> placement.
120
* Note: Make sure the component is properly uninstalled later,
121
* if it is not necessary. See issue #35325 for details.
122
* @param popup popup component to be installed into
123
* root pane of the text component.
125
public void install(JComponent popup) {
126
if (textComponent == null) return;
127
int caretPos = textComponent.getCaret().getDot();
129
Rectangle caretBounds = textComponent.modelToView(caretPos);
130
install(popup, caretBounds, Largest);
131
} catch (BadLocationException e) {
132
// do not install if the caret position is invalid
136
/** Removes popup component from textComponent root pane
137
* @param popup popup component to be removed from
138
* root pane of the text component.
140
public void uninstall(JComponent popup){
141
if (this.popup != null){
142
if (this.popup.isVisible()) this.popup.setVisible(false);
143
removeFromRootPane(this.popup);
146
if (popup!=this.popup && popup!= null){
147
if (popup.isVisible()) popup.setVisible(false);
148
removeFromRootPane(popup);
152
public void install(JComponent popup, Rectangle cursorBounds,
153
Placement placement, HorizontalBounds horizontalBounds, int horizontalAdjustment, int verticalAdjustment){
154
/* Uninstall the old popup from root pane
155
* and install the new one. Even in case
156
* they are the same objects it's necessary
157
* to cover the workspace switches etc.
159
if (this.popup != null) {
160
// if i.e. completion is visible and tooltip is being installed,
161
// completion popup should be closed.
162
if (this.popup.isVisible() && this.popup!=popup) this.popup.setVisible(false);
163
removeFromRootPane(this.popup);
168
if (this.popup != null) {
169
installToRootPane(this.popup);
172
// Update the bounds of the popup
173
Rectangle bounds = computeBounds(this.popup, textComponent,
174
cursorBounds, placement, horizontalBounds);
177
// Convert to layered pane's coordinates
179
if (horizontalBounds == ScrollBarBounds){
183
JRootPane rp = textComponent.getRootPane();
185
bounds = SwingUtilities.convertRectangle(textComponent, bounds,
186
rp.getLayeredPane());
189
if (horizontalBounds == ScrollBarBounds){
190
if (textComponent.getParent() instanceof JViewport){
191
int shift = textComponent.getParent().getX();
192
Rectangle viewBounds = ((JViewport)textComponent.getParent()).getViewRect();
193
bounds.x += viewBounds.x;
195
bounds.width += shift;
199
bounds.x = bounds.x + horizontalAdjustment;
200
bounds.y = bounds.y + verticalAdjustment;
201
bounds.width = bounds.width - horizontalAdjustment;
202
bounds.height = bounds.height - verticalAdjustment;
203
this.popup.setBounds(bounds);
205
} else { // can't fit -> hide
206
this.popup.setVisible(false);
210
public void install(JComponent popup, Rectangle cursorBounds,
211
Placement placement, HorizontalBounds horizontalBounds){
212
install(popup, cursorBounds, placement, ViewPortBounds, 0, 0);
216
public void install(JComponent popup, Rectangle cursorBounds,
217
Placement placement){
218
install(popup, cursorBounds, placement, ViewPortBounds);
221
/** Returns installed popup panel component */
222
public JComponent get(){
227
/** Install popup panel to current textComponent root pane */
228
private void installToRootPane(JComponent c) {
229
JRootPane rp = textComponent.getRootPane();
231
rp.getLayeredPane().add(c, JLayeredPane.POPUP_LAYER, 0);
235
/** Remove popup panel from previous textComponent root pane */
236
private void removeFromRootPane(JComponent c) {
237
JRootPane rp = c.getRootPane();
239
rp.getLayeredPane().remove(c);
243
/** Variation of the method for computing the bounds
244
* for the concrete view component. As the component can possibly
245
* be placed in a scroll pane it's first necessary
246
* to translate the cursor bounds and also translate
247
* back the resulting popup bounds.
248
* @param popup popup panel to be displayed
249
* @param view component over which the popup is displayed.
250
* @param cursorBounds the bounds of the caret or mouse cursor
251
* relative to the upper-left corner of the visible view.
252
* @param placement where to place the popup panel according to
253
* the cursor position.
254
* @return bounds of popup panel relative to the upper-left corner
255
* of the underlying view component.
256
* <CODE>null</CODE> if there is no place to display popup.
258
protected static Rectangle computeBounds(JComponent popup,
259
JComponent view, Rectangle cursorBounds, Placement placement, HorizontalBounds horizontalBounds) {
261
if (horizontalBounds == null) horizontalBounds = ViewPortBounds;
264
Component viewParent = view.getParent();
266
if (viewParent instanceof JViewport) {
267
Rectangle viewBounds = ((JViewport)viewParent).getViewRect();
269
Rectangle translatedCursorBounds = (Rectangle)cursorBounds.clone();
270
translatedCursorBounds.translate(-viewBounds.x, -viewBounds.y);
272
ret = computeBounds(popup, viewBounds.width, viewBounds.height,
273
translatedCursorBounds, placement, horizontalBounds);
275
if (ret != null) { // valid bounds
276
ret.translate(viewBounds.x, viewBounds.y);
279
} else { // not in scroll pane
280
ret = computeBounds(popup, view.getWidth(), view.getHeight(),
281
cursorBounds, placement);
287
protected static Rectangle computeBounds(JComponent popup,
288
JComponent view, Rectangle cursorBounds, Placement placement) {
289
return computeBounds(popup, view, cursorBounds, placement, ViewPortBounds);
292
/** Computes a best-fit bounds of popup panel
293
* according to available space in the underlying view
294
* (visible part of the pane).
295
* The placement is first evaluated and put into the popup's client property
296
* by <CODE>popup.putClientProperty(Placement.class, actual-placement)</CODE>.
297
* The actual placement is <UL>
298
* <LI> <CODE>Above</CODE> if the original placement was <CODE>Above</CODE>.
299
* Or if the original placement was <CODE>AbovePreferred</CODE>
300
* or <CODE>Largest</CODE>
301
* and there is more space above the cursor than below it.
302
* <LI> <CODE>Below</CODE> if the original placement was <CODE>Below</CODE>.
303
* Or if the original placement was <CODE>BelowPreferred</CODE>
304
* or <CODE>Largest</CODE>
305
* and there is more space below the cursor than above it.
306
* <LI> <CODE>AbovePreferred</CODE> if the original placement
307
* was <CODE>AbovePreferred</CODE>
308
* and there is less space above the cursor than below it.
309
* <LI> <CODE>BelowPreferred</CODE> if the original placement
310
* was <CODE>BelowPreferred</CODE>
311
* and there is less space below the cursor than above it.
312
* <P>Once the placement client property is set
313
* the <CODE>popup.setSize()</CODE> is called with the size of the area
314
* above/below the cursor (indicated by the placement).
315
* The popup responds by updating its size to the equal or smaller
316
* size. If it cannot physically fit into the requested area
318
* <CODE>putClientProperty(Placement.class, null)</CODE>
319
* on itself to indicate that it cannot fit. The method scans
320
* the content of the client property upon return from
321
* <CODE>popup.setSize()</CODE> and if it finds null there it returns
322
* null bounds in that case. The only exception is
323
* if the placement was either <CODE>AbovePreferred</CODE>
324
* or <CODE>BelowPreferred</CODE>. In that case the method
325
* gives it one more try
326
* by attempting to fit the popup into (bigger) complementary
327
* <CODE>Below</CODE> and <CODE>Above</CODE> areas (respectively).
328
* The popup either fits into these (bigger) areas or it again responds
329
* by returning <CODE>null</CODE> in the client property in which case
330
* the method finally gives up and returns null bounds.
332
* @param popup popup panel to be displayed
333
* @param viewWidth width of the visible view area.
334
* @param viewHeight height of the visible view area.
335
* @param cursorBounds the bounds of the caret or mouse cursor
336
* relative to the upper-left corner of the visible view
337
* @param placement where to place the popup panel according to
338
* the cursor position
339
* @return bounds of popup panel relative to the upper-left corner
340
* of the underlying view.
341
* <CODE>null</CODE> if there is no place to display popup.
343
protected static Rectangle computeBounds(JComponent popup,
344
int viewWidth, int viewHeight, Rectangle cursorBounds, Placement placement, HorizontalBounds horizontalBounds) {
346
if (placement == null) {
347
throw new NullPointerException("placement cannot be null"); // NOI18N
350
// Compute available height above the cursor
351
int aboveCursorHeight = cursorBounds.y;
352
int belowCursorY = cursorBounds.y + cursorBounds.height;
353
int belowCursorHeight = viewHeight - belowCursorY;
355
// Resolve *Preferred placements first
356
if (placement == AbovePreferred || placement == BelowPreferred) {
357
int prefHeight = popup.getPreferredSize().height;
358
if (placement == AbovePreferred) {
359
placement = (prefHeight <= aboveCursorHeight) ? Above : Largest;
360
} else { // BelowPreferred
361
placement = (prefHeight <= belowCursorHeight) ? Below : Largest;
365
// Resolve Largest placement
366
if (placement == Largest) {
367
placement = (aboveCursorHeight < belowCursorHeight)
372
Rectangle popupBounds = null;
374
while (true) { // do one or two passes
375
popup.putClientProperty(Placement.class, placement);
377
int height = (placement == Above || placement == AbovePreferred)
381
popup.setSize(viewWidth, height);
382
popupBounds = popup.getBounds();
384
Placement updatedPlacement = (Placement)popup.getClientProperty(Placement.class);
386
if (updatedPlacement != placement) { // popup does not fit with the orig placement
387
if (placement == AbovePreferred && updatedPlacement == null) {
391
} else if (placement == BelowPreferred && updatedPlacement == null) {
397
if (updatedPlacement == null) {
404
if (popupBounds != null) {
405
//place popup according to caret position and Placement
406
popupBounds.x = Math.min(cursorBounds.x, viewWidth - popupBounds.width);
408
popupBounds.y = (placement == Above || placement == AbovePreferred)
409
? (aboveCursorHeight - popupBounds.height)
416
protected static Rectangle computeBounds(JComponent popup,
417
int viewWidth, int viewHeight, Rectangle cursorBounds, Placement placement) {
418
return computeBounds(popup, viewWidth, viewHeight, cursorBounds, placement, ViewPortBounds);
421
/** Popup's key filter */
422
private class PopupKeyListener implements KeyListener{
424
public void keyTyped(KeyEvent e){}
425
public void keyReleased(KeyEvent e){}
427
public void keyPressed(KeyEvent e){
428
if (e == null) return;
429
if (popup != null && popup.isShowing()){
431
// get popup's registered keyboard actions
432
ActionMap am = popup.getActionMap();
433
InputMap im = popup.getInputMap();
435
// check whether popup registers keystroke
436
Object obj = im.get(KeyStroke.getKeyStrokeForEvent(e));
438
// if yes, gets the popup's action for this keystroke, perform it
439
// and consume key event
440
Action action = am.get(obj);
441
if (action != null) {
442
action.actionPerformed(null);
451
private final class TextComponentListener extends ComponentAdapter {
453
public void componentHidden(ComponentEvent evt) {
454
install(null); // hide popup
459
/** Placement of popup panel specification */
460
public static final class Placement {
462
private final String representation;
464
private Placement(String representation) {
465
this.representation = representation;
468
public String toString() {
469
return representation;
474
/** Horizontal bounds of popup panel specification */
475
public static final class HorizontalBounds {
477
private final String representation;
479
private HorizontalBounds(String representation) {
480
this.representation = representation;
483
public String toString() {
484
return representation;