/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun * Microsystems, Inc. All Rights Reserved. */ package org.netbeans.editor; import javax.swing.*; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; /** * Popup manager allows to display an arbitrary popup component * over the underlying text component. * * @author Martin Roskanin, Miloslav Metelka * @since 03/2002 */ public class PopupManager { private JComponent popup = null; private JTextComponent textComponent; /** Place popup always above cursor */ public static final Placement Above = new Placement("Above"); //NOI18N /** Place popup always below cursor */ public static final Placement Below = new Placement("Below"); //NOI18N /** Place popup to larger area. i.e. if place below cursor is larger than place above, then popup will be placed below cursor. */ public static final Placement Largest = new Placement("Largest"); //NOI18N /** Place popup above cursor. If a place above cursor is insufficient, then popup will be placed below cursor. */ public static final Placement AbovePreferred = new Placement("AbovePreferred"); //NOI18N /** Place popup below cursor. If a place below cursor is insufficient, then popup will be placed above cursor. */ public static final Placement BelowPreferred = new Placement("BelowPreferred"); //NOI18N private KeyListener keyListener; private TextComponentListener componentListener; /** Creates a new instance of PopupManager */ public PopupManager(JTextComponent textComponent) { this.textComponent = textComponent; keyListener = new PopupKeyListener(); textComponent.addKeyListener(keyListener); componentListener = new TextComponentListener(); textComponent.addComponentListener(componentListener); } /** Install popup component to textComponent root pane * based on caret coordinates with the <CODE>Largest</CODE> placement. * @param popup popup component to be installed into * root pane of the text component. */ public void install(JComponent popup) { if (textComponent == null) return; int caretPos = textComponent.getCaret().getDot(); try { Rectangle caretBounds = textComponent.modelToView(caretPos); install(popup, caretBounds, Largest); } catch (BadLocationException e) { // do not install if the caret position is invalid } } public void install(JComponent popup, Rectangle cursorBounds, Placement placement){ /* Uninstall the old popup from root pane * and install the new one. Even in case * they are the same objects it's necessary * to cover the workspace switches etc. */ if (this.popup != null) { removeFromRootPane(this.popup); } this.popup = popup; if (this.popup != null) { installToRootPane(this.popup); } // Update the bounds of the popup Rectangle bounds = computeBounds(this.popup, textComponent, cursorBounds, placement); if (bounds != null){ // Convert to layered pane's coordinates bounds = SwingUtilities.convertRectangle(textComponent, bounds, textComponent.getRootPane().getLayeredPane()); this.popup.setBounds(bounds); } else { // can't fit -> hide this.popup.setVisible(false); } } /** Returns installed popup panel component */ public JComponent get(){ return popup; } /** Install popup panel to current textComponent root pane */ private void installToRootPane(JComponent c) { JRootPane rp = textComponent.getRootPane(); if (rp != null) { rp.getLayeredPane().add(c, JLayeredPane.POPUP_LAYER, 0); } } /** Remove popup panel from previous textComponent root pane */ private void removeFromRootPane(JComponent c) { JRootPane rp = c.getRootPane(); if (rp != null) { rp.getLayeredPane().remove(c); } } /** Variation of the method for computing the bounds * for the concrete view component. As the component can possibly * be placed in a scroll pane it's first necessary * to translate the cursor bounds and also translate * back the resulting popup bounds. * @param popup popup panel to be displayed * @param view component over which the popup is displayed. * @param cursorBounds the bounds of the caret or mouse cursor * relative to the upper-left corner of the visible view. * @param placement where to place the popup panel according to * the cursor position. * @return bounds of popup panel relative to the upper-left corner * of the underlying view component. * <CODE>null</CODE> if there is no place to display popup. */ protected static Rectangle computeBounds(JComponent popup, JComponent view, Rectangle cursorBounds, Placement placement) { Rectangle ret; Component viewParent = view.getParent(); if (viewParent instanceof JViewport) { Rectangle viewBounds = ((JViewport)viewParent).getViewRect(); Rectangle translatedCursorBounds = (Rectangle)cursorBounds.clone(); translatedCursorBounds.translate(-viewBounds.x, -viewBounds.y); ret = computeBounds(popup, viewBounds.width, viewBounds.height, translatedCursorBounds, placement); if (ret != null) { // valid bounds ret.translate(viewBounds.x, viewBounds.y); } } else { // not in scroll pane ret = computeBounds(popup, view.getWidth(), view.getHeight(), cursorBounds, placement); } return ret; } /** Computes a best-fit bounds of popup panel * according to available space in the underlying view * (visible part of the pane). * The placement is first evaluated and put into the popup's client property * by <CODE>popup.putClientProperty(Placement.class, actual-placement)</CODE>. * The actual placement is <UL> * <LI> <CODE>Above</CODE> if the original placement was <CODE>Above</CODE>. * Or if the original placement was <CODE>AbovePreferred</CODE> * or <CODE>Largest</CODE> * and there is more space above the cursor than below it. * <LI> <CODE>Below</CODE> if the original placement was <CODE>Below</CODE>. * Or if the original placement was <CODE>BelowPreferred</CODE> * or <CODE>Largest</CODE> * and there is more space below the cursor than above it. * <LI> <CODE>AbovePreferred</CODE> if the original placement * was <CODE>AbovePreferred</CODE> * and there is less space above the cursor than below it. * <LI> <CODE>BelowPreferred</CODE> if the original placement * was <CODE>BelowPreferred</CODE> * and there is less space below the cursor than above it. * <P>Once the placement client property is set * the <CODE>popup.setSize()</CODE> is called with the size of the area * above/below the cursor (indicated by the placement). * The popup responds by updating its size to the equal or smaller * size. If it cannot physically fit into the requested area * it can call * <CODE>putClientProperty(Placement.class, null)</CODE> * on itself to indicate that it cannot fit. The method scans * the content of the client property upon return from * <CODE>popup.setSize()</CODE> and if it finds null there it returns * null bounds in that case. The only exception is * if the placement was either <CODE>AbovePreferred</CODE> * or <CODE>BelowPreferred</CODE>. In that case the method * gives it one more try * by attempting to fit the popup into (bigger) complementary * <CODE>Below</CODE> and <CODE>Above</CODE> areas (respectively). * The popup either fits into these (bigger) areas or it again responds * by returning <CODE>null</CODE> in the client property in which case * the method finally gives up and returns null bounds. * * @param popup popup panel to be displayed * @param viewWidth width of the visible view area. * @param viewHeight height of the visible view area. * @param cursorBounds the bounds of the caret or mouse cursor * relative to the upper-left corner of the visible view * @param placement where to place the popup panel according to * the cursor position * @return bounds of popup panel relative to the upper-left corner * of the underlying view. * <CODE>null</CODE> if there is no place to display popup. */ protected static Rectangle computeBounds(JComponent popup, int viewWidth, int viewHeight, Rectangle cursorBounds, Placement placement) { if (placement == null) { throw new NullPointerException("placement cannot be null"); // NOI18N } // Compute available height above the cursor int aboveCursorHeight = cursorBounds.y; int belowCursorY = cursorBounds.y + cursorBounds.height; int belowCursorHeight = viewHeight - belowCursorY; // resolve Largest and *Preferred placements if possible if (placement == Largest) { placement = (aboveCursorHeight < belowCursorHeight) ? Below : Above; } else if (placement == AbovePreferred && aboveCursorHeight > belowCursorHeight // more space above ) { placement = Above; } else if (placement == BelowPreferred && belowCursorHeight > aboveCursorHeight // more space below ) { placement = Below; } Rectangle popupBounds = null; while (true) { // do one or two passes popup.putClientProperty(Placement.class, placement); int height = (placement == Above || placement == AbovePreferred) ? aboveCursorHeight : belowCursorHeight; popup.setSize(viewWidth, height); popupBounds = popup.getBounds(); Placement updatedPlacement = (Placement)popup.getClientProperty(Placement.class); if (updatedPlacement != placement) { // popup does not fit with the orig placement if (placement == AbovePreferred && updatedPlacement == null) { placement = Below; continue; } else if (placement == BelowPreferred && updatedPlacement == null) { placement = Above; continue; } } if (updatedPlacement == null) { popupBounds = null; } break; } if (popupBounds != null) { //place popup according to caret position and Placement popupBounds.x = Math.min(cursorBounds.x, viewWidth - popupBounds.width); popupBounds.y = (placement == Above || placement == AbovePreferred) ? (aboveCursorHeight - popupBounds.height) : belowCursorY; } return popupBounds; } /** Popup's key filter */ private class PopupKeyListener implements KeyListener{ public void keyTyped(KeyEvent e){} public void keyReleased(KeyEvent e){} public void keyPressed(KeyEvent e){ if (e == null) return; if (popup != null && popup.isShowing()){ // get popup's registered keyboard actions ActionMap am = popup.getActionMap(); InputMap im = popup.getInputMap(); // check whether popup registers keystroke Object obj = im.get(KeyStroke.getKeyStrokeForEvent(e)); if (obj!=null){ // if yes, gets the popup's action for this keystroke, perform it // and consume key event Action action = am.get(obj); if (action != null) { action.actionPerformed(null); e.consume(); } } } } } private final class TextComponentListener extends ComponentAdapter { public void componentHidden(ComponentEvent evt) { install(null); // hide popup } } /** Placement of popup panel specification */ public static final class Placement { private final String representation; private Placement(String representation) { this.representation = representation; } public String toString() { return representation; } } }