All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.editor.PopupManager Maven / Gradle / Ivy

/*
 *                 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 java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JRootPane;
import javax.swing.KeyStroke;
import javax.swing.text.JTextComponent;

import org.netbeans.editor.EditorUI;
import org.netbeans.editor.Utilities;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.SwingUtilities;
import java.awt.Component;
import javax.swing.JViewport;
import javax.swing.text.BadLocationException;


/**
 *  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
    
    /** Place popup inside the scrollbar's viewport */
    public static final HorizontalBounds ViewPortBounds = new HorizontalBounds("ViewPort"); //NOI18N
    
    /** Place popup inside the whole scrollbar */
    public static final HorizontalBounds ScrollBarBounds = new HorizontalBounds("ScrollBar"); //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 Largest placement.
     *  Note: Make sure the component is properly uninstalled later,
     *  if it is not necessary. See issue #35325 for details.
     *  @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
        }
    }

    /** Removes popup component from textComponent root pane
     *  @param popup popup component to be removed from
     *  root pane of the text component.
     */
    public void uninstall(JComponent popup){
        if (this.popup != null){
            if (this.popup.isVisible()) this.popup.setVisible(false);
            removeFromRootPane(this.popup);
        }
        
        if (popup!=this.popup && popup!= null){
            if (popup.isVisible()) popup.setVisible(false);
            removeFromRootPane(popup);
        }
    }

    public void install(JComponent popup, Rectangle cursorBounds,
    Placement placement, HorizontalBounds horizontalBounds, int horizontalAdjustment, int verticalAdjustment){
        /* 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) {
            // if i.e. completion is visible and tooltip is being installed, 
            // completion popup should be closed.
            if (this.popup.isVisible() && this.popup!=popup) this.popup.setVisible(false);
            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, horizontalBounds);

        if (bounds != null){
            // Convert to layered pane's coordinates
            
            if (horizontalBounds == ScrollBarBounds){
                bounds.x = 0;
            }
            
            JRootPane rp = textComponent.getRootPane();
            if (rp!=null){
                bounds = SwingUtilities.convertRectangle(textComponent, bounds,
                    rp.getLayeredPane());
            }
            
            if (horizontalBounds == ScrollBarBounds){
                if (textComponent.getParent() instanceof JViewport){
                    int shift = textComponent.getParent().getX();
                    Rectangle viewBounds = ((JViewport)textComponent.getParent()).getViewRect();
                    bounds.x += viewBounds.x;
                    bounds.x -= shift;
                    bounds.width += shift;
                }
            }
            
            bounds.x = bounds.x + horizontalAdjustment;
            bounds.y = bounds.y + verticalAdjustment;
            bounds.width = bounds.width - horizontalAdjustment;
            bounds.height = bounds.height - verticalAdjustment;
            this.popup.setBounds(bounds);

        } else { // can't fit -> hide
            this.popup.setVisible(false);
        }
    }
    
    public void install(JComponent popup, Rectangle cursorBounds,
    Placement placement, HorizontalBounds horizontalBounds){
        install(popup, cursorBounds, placement, ViewPortBounds, 0, 0);
    }
    
    
    public void install(JComponent popup, Rectangle cursorBounds,
    Placement placement){
        install(popup, cursorBounds, placement, ViewPortBounds);
    }
    
    /** 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.
     *    null if there is no place to display popup.
     */
    protected static Rectangle computeBounds(JComponent popup,
    JComponent view, Rectangle cursorBounds, Placement placement, HorizontalBounds horizontalBounds) {
        
        if (horizontalBounds == null) horizontalBounds = ViewPortBounds;
        
        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, horizontalBounds);
            
            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;
    }

    protected static Rectangle computeBounds(JComponent popup,
    JComponent view, Rectangle cursorBounds, Placement placement) {
        return computeBounds(popup, view, cursorBounds, placement, ViewPortBounds);
    }    
    
    /** 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 popup.putClientProperty(Placement.class, actual-placement).
     *  The actual placement is 
    *
  • Above if the original placement was Above. * Or if the original placement was AbovePreferred * or Largest * and there is more space above the cursor than below it. *
  • Below if the original placement was Below. * Or if the original placement was BelowPreferred * or Largest * and there is more space below the cursor than above it. *
  • AbovePreferred if the original placement * was AbovePreferred * and there is less space above the cursor than below it. *
  • BelowPreferred if the original placement * was BelowPreferred * and there is less space below the cursor than above it. *

    Once the placement client property is set * the popup.setSize() 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 * putClientProperty(Placement.class, null) * on itself to indicate that it cannot fit. The method scans * the content of the client property upon return from * popup.setSize() and if it finds null there it returns * null bounds in that case. The only exception is * if the placement was either AbovePreferred * or BelowPreferred. In that case the method * gives it one more try * by attempting to fit the popup into (bigger) complementary * Below and Above areas (respectively). * The popup either fits into these (bigger) areas or it again responds * by returning null 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. * null if there is no place to display popup. */ protected static Rectangle computeBounds(JComponent popup, int viewWidth, int viewHeight, Rectangle cursorBounds, Placement placement, HorizontalBounds horizontalBounds) { 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; } protected static Rectangle computeBounds(JComponent popup, int viewWidth, int viewHeight, Rectangle cursorBounds, Placement placement) { return computeBounds(popup, viewWidth, viewHeight, cursorBounds, placement, ViewPortBounds); } /** 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; } } /** Horizontal bounds of popup panel specification */ public static final class HorizontalBounds { private final String representation; private HorizontalBounds(String representation) { this.representation = representation; } public String toString() { return representation; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy