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

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

There is a newer version: RELEASE240
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.editor;

import java.awt.Dimension;
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 java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.SwingUtilities;
import java.awt.Component;
import java.awt.Container;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JViewport;
import javax.swing.text.BadLocationException;
import org.openide.util.Parameters;


/**
 *  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 static final Logger LOG = Logger.getLogger(PopupManager.class.getName());
    /**
     * Key for a boolean client property that can be set on the popup component to suppress the
     * forwarding of keyboard events into it. Note that popup keyboard actions will still work if
     * the popup receives explicit focus. See NETBEANS-403 and the associated
     * pull request (click
     * "show outdated" to see the original pull request discussion). Make this property private for
     * now to avoid committing to an official API.
     */
    private static final String SUPPRESS_POPUP_KEYBOARD_FORWARDING_CLIENT_PROPERTY_KEY =
        "suppress-popup-keyboard-forwarding";
    
    private JComponent popup = null;
    private final 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 the popup on a fixed point of the view, measured from top-left corner
     */
    public static final Placement FixedPoint = new Placement("TopLeft");
    
    /** 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 final KeyListener keyListener;
    
    private final 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) {
        JComponent oldPopup = this.popup;

        if (oldPopup != null) {
            if (oldPopup.isVisible()) {
                oldPopup.setVisible(false);
            }
            removeFromRootPane(oldPopup);
            this.popup = null;
        }

        if (popup != null && popup != oldPopup) {
            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);
//        }

        if (this.popup != null && this.popup != popup) {
            uninstall(null);
        }

        assert this.popup == null || this.popup == popup : "this.popup=" + this.popup + ", popup=" + popup; //NOI18N

        if (popup != null) {
            if (this.popup == null) {
                this.popup = popup;
                installToRootPane(this.popup);
            } // else this.popup == popup
        
            // Update the bounds of the popup
            Rectangle bounds = computeBounds(this.popup, textComponent,
                cursorBounds, placement, horizontalBounds);

            LOG.log(Level.FINE, "computed-bounds={0}", bounds); //NOI18N
            if (bounds != null){
                // Convert to layered pane's coordinates

                if (horizontalBounds == ScrollBarBounds && placement != FixedPoint){
                    bounds.x = 0;
                }

                JRootPane rp = textComponent.getRootPane();
                if (rp!=null){
                    bounds = SwingUtilities.convertRectangle(textComponent, bounds,
                        rp.getLayeredPane());
                    if (bounds.y < 0) {
                        bounds.y = 0;
                    }
                }

                if (horizontalBounds == ScrollBarBounds){
                    Container parent = textComponent.getParent();
                    if (parent instanceof JLayeredPane) {
                        parent = parent.getParent();
                    }
                    if (parent instanceof JViewport){
                        int shift = parent.getX();
                        Rectangle viewBounds = ((JViewport)parent).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;

                LOG.log(Level.FINE, "setting bounds={0} on {1}", new Object [] { bounds, this.popup }); //NOI18N
                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, horizontalBounds, 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 JLayeredPane) {
            viewParent = viewParent.getParent();
        }
        
        if (viewParent instanceof JViewport) {
            Rectangle viewBounds = ((JViewport)viewParent).getViewRect();

            Rectangle translatedCursorBounds = (Rectangle)cursorBounds.clone();
            if (placement != FixedPoint) {
                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 originalPlacement 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 originalPlacement, HorizontalBounds horizontalBounds) { Parameters.notNull("popup", popup); //NOI18N Parameters.notNull("cursorBounds", cursorBounds); //NOI18N Parameters.notNull("originalPlacement", originalPlacement); //NOI18N // Compute available height above the cursor int aboveCursorHeight = cursorBounds.y; int belowCursorY = cursorBounds.y + cursorBounds.height; int belowCursorHeight = viewHeight - belowCursorY; Dimension prefSize = popup.getPreferredSize(); final int width = Math.min(viewWidth, prefSize.width); popup.setSize(width, Integer.MAX_VALUE); prefSize = popup.getPreferredSize(); Placement placement = determinePlacement(originalPlacement, prefSize, aboveCursorHeight, belowCursorHeight); Rectangle popupBounds = null; for(;;) { // do one or two passes popup.putClientProperty(Placement.class, placement); int maxHeight = (placement == Above || placement == AbovePreferred) ? aboveCursorHeight : belowCursorHeight; int height = Math.min(prefSize.height, maxHeight); popup.setSize(width, 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) { if (placement == FixedPoint) { popupBounds.x = cursorBounds.x; popupBounds.y = cursorBounds.y; } else { //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); } private static Placement determinePlacement(Placement placement, Dimension prefSize, int aboveCursorHeight, int belowCursorHeight) { // Resolve *Preferred placements first if (placement == AbovePreferred) { placement = (prefSize.height <= aboveCursorHeight) ? Above : Largest; } else if (placement == BelowPreferred) { placement = (prefSize.height <= belowCursorHeight) ? Below : Largest; } // Resolve Largest placement if (placement == Largest) { placement = (aboveCursorHeight < belowCursorHeight) ? Below : Above; } return placement; } /** Popup's key filter */ private final class PopupKeyListener implements KeyListener{ public @Override void keyTyped(KeyEvent e) { if (e != null && popup != null && popup.isShowing()) { consumeIfKeyPressInActionMap(e); } } public @Override void keyReleased(KeyEvent e) { if (e != null && popup != null && popup.isShowing()) { consumeIfKeyPressInActionMap(e); } } private boolean shouldPopupReceiveForwardedKeyboardAction(Object actionKey) { /* In NetBeans 8.2, the behavior was to forward all action events except those whose key was "tooltip-no-action" (which, reading through ToolTipSupport, I think applies only to the default action). To avoid breaking anything, keep this behavior except when SUPPRESS_POPUP_KEYBOARD_FORWARDING_CLIENT_PROPERTY_KEY property has been explicitly set. The latter is used to fix NETBEANS-403. */ if (actionKey == null || actionKey.equals("tooltip-no-action")) return false; return popup == null || !Boolean.TRUE.equals( popup.getClientProperty(SUPPRESS_POPUP_KEYBOARD_FORWARDING_CLIENT_PROPERTY_KEY)); } public @Override void keyPressed(KeyEvent e){ if (e != null && popup != null && popup.isShowing()) { // get popup's registered keyboard actions ActionMap am = popup.getActionMap(); InputMap im = popup.getInputMap(); // check whether popup registers keystroke KeyStroke ks = KeyStroke.getKeyStrokeForEvent(e); Object obj = im.get(ks); LOG.log(Level.FINE, "Keystroke for event {0}: {1}; action-map-key={2}", new Object [] { e, ks, obj }); //NOI18N if (shouldPopupReceiveForwardedKeyboardAction(obj)) { // if yes, gets the popup's action for this keystroke, perform it // and consume key event Action action = am.get(obj); LOG.log(Level.FINE, "Popup component''s action: {0}, {1}", new Object [] { action, action != null ? action.getValue(Action.NAME) : null }); //NOI18N /* Make sure to use the popup as the source of the action, since the popup is also providing the event. Not doing this, and instead invoking actionPerformed with a null ActionEvent, was one part of the problem seen in NETBEANS-403. */ if (SwingUtilities.notifyAction(action, ks, e, popup, e.getModifiers())) { e.consume(); return; } } if (e.getKeyCode() != KeyEvent.VK_CONTROL && e.getKeyCode() != KeyEvent.VK_SHIFT && e.getKeyCode() != KeyEvent.VK_ALT && e.getKeyCode() != KeyEvent.VK_ALT_GRAPH && e.getKeyCode() != KeyEvent.VK_META ) { // hide tooltip if any was shown Utilities.getEditorUI(textComponent).getToolTipSupport().setToolTipVisible(false); } } } private void consumeIfKeyPressInActionMap(KeyEvent e) { // get popup's registered keyboard actions ActionMap am = popup.getActionMap(); InputMap im = popup.getInputMap(); // check whether popup registers keystroke // If we consumed key pressed, we need to consume other key events as well: KeyStroke ks = KeyStroke.getKeyStrokeForEvent( new KeyEvent((Component) e.getSource(), KeyEvent.KEY_PRESSED, e.getWhen(), e.getModifiers(), KeyEvent.getExtendedKeyCodeForChar(e.getKeyChar()), e.getKeyChar(), e.getKeyLocation()) ); Object obj = im.get(ks); if (shouldPopupReceiveForwardedKeyboardAction(obj)) { // if yes, if there is a popup's action, consume key event Action action = am.get(obj); if (action != null && action.isEnabled()) { // actionPerformed on key press only. e.consume(); } } } } // End of PopupKeyListener class private final class TextComponentListener extends ComponentAdapter { public @Override void componentHidden(ComponentEvent evt) { install(null); // hide popup } } // End of TextComponentListener class /** Placement of popup panel specification */ public static final class Placement { private final String representation; private Placement(String representation) { this.representation = representation; } public @Override String toString() { return representation; } } // End of Placement class /** Horizontal bounds of popup panel specification */ public static final class HorizontalBounds { private final String representation; private HorizontalBounds(String representation) { this.representation = representation; } public @Override String toString() { return representation; } } // End of HorizontalBounds class }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy