com.intellij.codeInsight.intention.impl.IntentionHintComponent.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.codeInsight.intention.impl.IntentionHintComponent.java

Source

/*
 * Copyright 2000-2009 JetBrains s.r.o.
 *
 * Licensed 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 com.intellij.codeInsight.intention.impl;

import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.hint.PriorityQuestionAction;
import com.intellij.codeInsight.hint.ScrollAwareHint;
import com.intellij.codeInsight.intention.HighPriorityAction;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings;
import com.intellij.codeInsight.intention.impl.config.IntentionSettingsConfigurable;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.VisualPosition;
import com.intellij.openapi.editor.actions.EditorActionUtil;
import com.intellij.openapi.editor.event.EditorFactoryAdapter;
import com.intellij.openapi.editor.event.EditorFactoryEvent;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.JBPopupListener;
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.util.Disposer;
import com.intellij.psi.PsiFile;
import com.intellij.refactoring.BaseRefactoringIntentionAction;
import com.intellij.ui.HintHint;
import com.intellij.ui.LightweightHint;
import com.intellij.ui.RowIcon;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.Alarm;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ui.EmptyIcon;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;

/**
 * @author max
 * @author Mike
 * @author Valentin
 * @author Eugene Belyaev
 * @author Konstantin Bulenkov
 * @author and me too (Chinee?)
 */
public class IntentionHintComponent extends JPanel implements Disposable, ScrollAwareHint {
    private static final Logger LOG = Logger
            .getInstance("#com.intellij.codeInsight.intention.impl.IntentionHintComponent.ListPopupRunnable");

    static final Icon ourInactiveArrowIcon = new EmptyIcon(AllIcons.General.ArrowDown.getIconWidth(),
            AllIcons.General.ArrowDown.getIconHeight());

    private static final int NORMAL_BORDER_SIZE = 6;
    private static final int SMALL_BORDER_SIZE = 4;

    private static final Border INACTIVE_BORDER = BorderFactory.createEmptyBorder(NORMAL_BORDER_SIZE,
            NORMAL_BORDER_SIZE, NORMAL_BORDER_SIZE, NORMAL_BORDER_SIZE);
    private static final Border ACTIVE_BORDER = BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(Color.BLACK, 1), BorderFactory.createEmptyBorder(NORMAL_BORDER_SIZE - 1,
                    NORMAL_BORDER_SIZE - 1, NORMAL_BORDER_SIZE - 1, NORMAL_BORDER_SIZE - 1));

    private static final Border INACTIVE_BORDER_SMALL = BorderFactory.createEmptyBorder(SMALL_BORDER_SIZE,
            SMALL_BORDER_SIZE, SMALL_BORDER_SIZE, SMALL_BORDER_SIZE);
    private static final Border ACTIVE_BORDER_SMALL = BorderFactory.createCompoundBorder(
            BorderFactory.createLineBorder(Color.BLACK, 1), BorderFactory.createEmptyBorder(SMALL_BORDER_SIZE - 1,
                    SMALL_BORDER_SIZE - 1, SMALL_BORDER_SIZE - 1, SMALL_BORDER_SIZE - 1));

    private final Editor myEditor;

    private static final Alarm myAlarm = new Alarm();

    private final RowIcon myHighlightedIcon;
    private final JLabel myIconLabel;

    private final RowIcon myInactiveIcon;

    private static final int DELAY = 500;
    private final MyComponentHint myComponentHint;
    private boolean myPopupShown = false;
    private boolean myDisposed = false;
    private ListPopup myPopup;
    private final PsiFile myFile;
    private static final int LIGHTBULB_OFFSET = 20;

    private PopupMenuListener myOuterComboboxPopupListener;

    @NotNull
    public static IntentionHintComponent showIntentionHint(@NotNull Project project, @NotNull PsiFile file,
            @NotNull Editor editor, @NotNull ShowIntentionsPass.IntentionsInfo intentions, boolean showExpanded) {
        final Point position = getHintPosition(editor);
        return showIntentionHint(project, file, editor, intentions, showExpanded, position);
    }

    @NotNull
    public static IntentionHintComponent showIntentionHint(@NotNull final Project project, @NotNull PsiFile file,
            @NotNull final Editor editor, @NotNull ShowIntentionsPass.IntentionsInfo intentions,
            boolean showExpanded, @NotNull Point position) {
        final IntentionHintComponent component = new IntentionHintComponent(project, file, editor, intentions);

        component.showIntentionHintImpl(!showExpanded, position);
        Disposer.register(project, component);
        if (showExpanded) {
            ApplicationManager.getApplication().invokeLater(new Runnable() {
                @Override
                public void run() {
                    if (!editor.isDisposed() && editor.getComponent().isShowing()) {
                        component.showPopup();
                    }
                }
            }, project.getDisposed());
        }

        return component;
    }

    @TestOnly
    public boolean isDisposed() {
        return myDisposed;
    }

    @Override
    public void dispose() {
        ApplicationManager.getApplication().assertIsDispatchThread();
        myDisposed = true;
        myComponentHint.hide();
        super.hide();

        if (myOuterComboboxPopupListener != null) {
            final Container ancestor = SwingUtilities.getAncestorOfClass(JComboBox.class,
                    myEditor.getContentComponent());
            if (ancestor != null) {
                ((JComboBox) ancestor).removePopupMenuListener(myOuterComboboxPopupListener);
            }

            myOuterComboboxPopupListener = null;
        }
    }

    @Override
    public void editorScrolled() {
        closePopup();
    }

    //true if actions updated, there is nothing to do
    //false if has to recreate popup, no need to reshow
    //null if has to reshow
    public synchronized Boolean updateActions(ShowIntentionsPass.IntentionsInfo intentions) {
        if (myPopup.isDisposed())
            return null;
        if (!myFile.isValid())
            return null;
        IntentionListStep step = (IntentionListStep) myPopup.getListStep();
        if (!step.updateActions(intentions)) {
            return Boolean.TRUE;
        }
        if (!myPopupShown) {
            return Boolean.FALSE;
        }
        return null;
    }

    // for using in tests !
    @Nullable
    public IntentionAction getAction(int index) {
        if (myPopup == null || myPopup.isDisposed()) {
            return null;
        }
        IntentionListStep listStep = (IntentionListStep) myPopup.getListStep();
        List<IntentionActionWithTextCaching> values = listStep.getValues();
        if (values.size() <= index) {
            return null;
        }
        return values.get(index).getAction();
    }

    public synchronized void recreate() {
        IntentionListStep step = (IntentionListStep) myPopup.getListStep();
        recreateMyPopup(step);
    }

    private void showIntentionHintImpl(final boolean delay, final Point position) {
        final int offset = myEditor.getCaretModel().getOffset();

        myComponentHint.setShouldDelay(delay);

        HintManagerImpl hintManager = HintManagerImpl.getInstanceImpl();

        PriorityQuestionAction action = new PriorityQuestionAction() {
            @Override
            public boolean execute() {
                showPopup();
                return true;
            }

            @Override
            public int getPriority() {
                return -10;
            }
        };
        if (hintManager.canShowQuestionAction(action)) {
            hintManager.showQuestionHint(myEditor, position, offset, offset, myComponentHint, action,
                    HintManager.ABOVE);
        }
    }

    @NotNull
    private static Point getHintPosition(Editor editor) {
        if (ApplicationManager.getApplication().isUnitTestMode())
            return new Point();
        final int offset = editor.getCaretModel().getOffset();
        final VisualPosition pos = editor.offsetToVisualPosition(offset);
        int line = pos.line;

        final Point position = editor.visualPositionToXY(new VisualPosition(line, 0));
        LOG.assertTrue(editor.getComponent().isDisplayable());

        JComponent convertComponent = editor.getContentComponent();

        Point realPoint;
        final boolean oneLineEditor = editor.isOneLineMode();
        if (oneLineEditor) {
            // place bulb at the corner of the surrounding component
            final JComponent contentComponent = editor.getContentComponent();
            Container ancestorOfClass = SwingUtilities.getAncestorOfClass(JComboBox.class, contentComponent);

            if (ancestorOfClass != null) {
                convertComponent = (JComponent) ancestorOfClass;
            } else {
                ancestorOfClass = SwingUtilities.getAncestorOfClass(JTextField.class, contentComponent);
                if (ancestorOfClass != null) {
                    convertComponent = (JComponent) ancestorOfClass;
                }
            }

            realPoint = new Point(-(AllIcons.Actions.RealIntentionBulb.getIconWidth() / 2) - 4,
                    -(AllIcons.Actions.RealIntentionBulb.getIconHeight() / 2));
        } else {
            // try to place bulb on the same line
            final int borderHeight = NORMAL_BORDER_SIZE;

            int yShift = -(NORMAL_BORDER_SIZE + AllIcons.Actions.RealIntentionBulb.getIconHeight());
            if (canPlaceBulbOnTheSameLine(editor)) {
                yShift = -(borderHeight
                        + ((AllIcons.Actions.RealIntentionBulb.getIconHeight() - editor.getLineHeight()) / 2) + 3);
            }

            final int xShift = AllIcons.Actions.RealIntentionBulb.getIconWidth();

            Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
            realPoint = new Point(Math.max(0, visibleArea.x - xShift), position.y + yShift);
        }

        Point location = SwingUtilities.convertPoint(convertComponent, realPoint,
                editor.getComponent().getRootPane().getLayeredPane());
        return new Point(location.x, location.y);
    }

    private static boolean canPlaceBulbOnTheSameLine(Editor editor) {
        if (ApplicationManager.getApplication().isUnitTestMode() || editor.isOneLineMode())
            return false;
        final int offset = editor.getCaretModel().getOffset();
        final VisualPosition pos = editor.offsetToVisualPosition(offset);
        int line = pos.line;

        final int firstNonSpaceColumnOnTheLine = EditorActionUtil.findFirstNonSpaceColumnOnTheLine(editor, line);
        if (firstNonSpaceColumnOnTheLine == -1)
            return false;
        final Point point = editor.visualPositionToXY(new VisualPosition(line, firstNonSpaceColumnOnTheLine));
        return point.x > (AllIcons.Actions.RealIntentionBulb.getIconWidth()
                + (editor.isOneLineMode() ? SMALL_BORDER_SIZE : NORMAL_BORDER_SIZE) * 2);
    }

    private IntentionHintComponent(@NotNull Project project, @NotNull PsiFile file, @NotNull final Editor editor,
            @NotNull ShowIntentionsPass.IntentionsInfo intentions) {
        ApplicationManager.getApplication().assertReadAccessAllowed();
        myFile = file;
        myEditor = editor;

        setLayout(new BorderLayout());
        setOpaque(false);

        boolean showRefactoringsBulb = false;
        for (HighlightInfo.IntentionActionDescriptor descriptor : intentions.inspectionFixesToShow) {
            if (descriptor.getAction() instanceof BaseRefactoringIntentionAction) {
                showRefactoringsBulb = true;
                break;
            }
        }
        boolean showFix = false;
        if (!showRefactoringsBulb) {
            showFix = false;
            for (final HighlightInfo.IntentionActionDescriptor pairs : intentions.errorFixesToShow) {
                IntentionAction fix = pairs.getAction();
                if (IntentionManagerSettings.getInstance().isShowLightBulb(fix)) {
                    showFix = true;
                    break;
                }
            }
        }

        Icon smartTagIcon = showRefactoringsBulb ? AllIcons.Actions.RefactoringBulb
                : showFix ? AllIcons.Actions.QuickfixBulb : AllIcons.Actions.IntentionBulb;

        myHighlightedIcon = new RowIcon(2);
        myHighlightedIcon.setIcon(smartTagIcon, 0);
        myHighlightedIcon.setIcon(AllIcons.General.ArrowDown, 1);

        myInactiveIcon = new RowIcon(2);
        myInactiveIcon.setIcon(smartTagIcon, 0);
        myInactiveIcon.setIcon(ourInactiveArrowIcon, 1);

        myIconLabel = new JLabel(myInactiveIcon);
        myIconLabel.setOpaque(false);

        add(myIconLabel, BorderLayout.CENTER);

        setBorder(editor.isOneLineMode() ? INACTIVE_BORDER_SMALL : INACTIVE_BORDER);

        myIconLabel.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
                    showPopup();
                }
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                onMouseEnter(editor.isOneLineMode());
            }

            @Override
            public void mouseExited(MouseEvent e) {
                onMouseExit(editor.isOneLineMode());
            }
        });

        myComponentHint = new MyComponentHint(this);
        IntentionListStep step = new IntentionListStep(this, intentions, myEditor, myFile, project);
        recreateMyPopup(step);
        // dispose myself when editor closed
        EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryAdapter() {
            @Override
            public void editorReleased(@NotNull EditorFactoryEvent event) {
                if (event.getEditor() == myEditor) {
                    hide();
                }
            }
        }, this);
    }

    @Override
    public void hide() {
        Disposer.dispose(this);
    }

    private void onMouseExit(final boolean small) {
        Window ancestor = SwingUtilities.getWindowAncestor(myPopup.getContent());
        if (ancestor == null) {
            myIconLabel.setIcon(myInactiveIcon);
            setBorder(small ? INACTIVE_BORDER_SMALL : INACTIVE_BORDER);
        }
    }

    private void onMouseEnter(final boolean small) {
        myIconLabel.setIcon(myHighlightedIcon);
        setBorder(small ? ACTIVE_BORDER_SMALL : ACTIVE_BORDER);

        String acceleratorsText = KeymapUtil.getFirstKeyboardShortcutText(
                ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS));
        if (!acceleratorsText.isEmpty()) {
            myIconLabel.setToolTipText(CodeInsightBundle.message("lightbulb.tooltip", acceleratorsText));
        }
    }

    @TestOnly
    public LightweightHint getComponentHint() {
        return myComponentHint;
    }

    private void closePopup() {
        myPopup.cancel();
        myPopupShown = false;
    }

    private void showPopup() {
        if (myPopup == null || myPopup.isDisposed())
            return;

        if (isShowing()) {
            final RelativePoint swCorner = RelativePoint.getSouthWestOf(this);
            final int yOffset = canPlaceBulbOnTheSameLine(myEditor) ? 0
                    : myEditor.getLineHeight()
                            - (myEditor.isOneLineMode() ? SMALL_BORDER_SIZE : NORMAL_BORDER_SIZE);
            myPopup.show(new RelativePoint(swCorner.getComponent(),
                    new Point(swCorner.getPoint().x, swCorner.getPoint().y + yOffset)));
        } else {
            myPopup.showInBestPositionFor(myEditor);
        }

        myPopupShown = true;
    }

    private void recreateMyPopup(@NotNull IntentionListStep step) {
        if (myPopup != null) {
            Disposer.dispose(myPopup);
        }
        myPopup = JBPopupFactory.getInstance().createListPopup(step);
        myPopup.addListener(new JBPopupListener.Adapter() {
            @Override
            public void onClosed(LightweightWindowEvent event) {
                myPopupShown = false;
            }
        });

        if (myEditor.isOneLineMode()) {
            // hide popup on combobox popup show
            final Container ancestor = SwingUtilities.getAncestorOfClass(JComboBox.class,
                    myEditor.getContentComponent());
            if (ancestor != null) {
                final JComboBox comboBox = (JComboBox) ancestor;
                myOuterComboboxPopupListener = new PopupMenuListener() {
                    @Override
                    public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                        hide();
                    }

                    @Override
                    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                    }

                    @Override
                    public void popupMenuCanceled(PopupMenuEvent e) {
                    }
                };

                comboBox.addPopupMenuListener(myOuterComboboxPopupListener);
            }
        }

        Disposer.register(this, myPopup);
        Disposer.register(myPopup, new Disposable() {
            @Override
            public void dispose() {
                ApplicationManager.getApplication().assertIsDispatchThread();
            }
        });
    }

    void canceled(IntentionListStep intentionListStep) {
        if (myPopup.getListStep() != intentionListStep || myDisposed) {
            return;
        }
        // Root canceled. Create new popup. This one cannot be reused.
        recreateMyPopup(intentionListStep);
    }

    private class MyComponentHint extends LightweightHint {
        private boolean myVisible = false;
        private boolean myShouldDelay;

        private MyComponentHint(JComponent component) {
            super(component);
        }

        @Override
        public void show(@NotNull final JComponent parentComponent, final int x, final int y,
                final JComponent focusBackComponent, @NotNull HintHint hintHint) {
            myVisible = true;
            if (myShouldDelay) {
                myAlarm.cancelAllRequests();
                myAlarm.addRequest(new Runnable() {
                    @Override
                    public void run() {
                        showImpl(parentComponent, x, y, focusBackComponent);
                    }
                }, DELAY);
            } else {
                showImpl(parentComponent, x, y, focusBackComponent);
            }
        }

        private void showImpl(JComponent parentComponent, int x, int y, JComponent focusBackComponent) {
            if (!parentComponent.isShowing())
                return;
            super.show(parentComponent, x, y, focusBackComponent, new HintHint(parentComponent, new Point(x, y)));
        }

        @Override
        public void hide() {
            super.hide();
            myVisible = false;
            myAlarm.cancelAllRequests();
        }

        @Override
        public boolean isVisible() {
            return myVisible || super.isVisible();
        }

        public void setShouldDelay(boolean shouldDelay) {
            myShouldDelay = shouldDelay;
        }
    }

    public static class EnableDisableIntentionAction implements IntentionAction {
        private final String myActionFamilyName;
        private final IntentionManagerSettings mySettings = IntentionManagerSettings.getInstance();
        private final IntentionAction myAction;

        public EnableDisableIntentionAction(IntentionAction action) {
            myActionFamilyName = action.getFamilyName();
            myAction = action;
            // needed for checking errors in user written actions
            //noinspection ConstantConditions
            LOG.assertTrue(myActionFamilyName != null, "action " + action.getClass() + " family returned null");
        }

        @Override
        @NotNull
        public String getText() {
            return mySettings.isEnabled(myAction)
                    ? CodeInsightBundle.message("disable.intention.action", myActionFamilyName)
                    : CodeInsightBundle.message("enable.intention.action", myActionFamilyName);
        }

        @Override
        @NotNull
        public String getFamilyName() {
            return getText();
        }

        @Override
        public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
            return true;
        }

        @Override
        public void invoke(@NotNull Project project, Editor editor, PsiFile file)
                throws IncorrectOperationException {
            mySettings.setEnabled(myAction, !mySettings.isEnabled(myAction));
        }

        @Override
        public boolean startInWriteAction() {
            return false;
        }

        @Override
        public String toString() {
            return getText();
        }
    }

    public static class EditIntentionSettingsAction implements IntentionAction, HighPriorityAction {
        private String myFamilyName;

        public EditIntentionSettingsAction(IntentionAction action) {
            myFamilyName = action.getFamilyName();
        }

        @NotNull
        @Override
        public String getText() {
            return "Edit intention settings";
        }

        @NotNull
        @Override
        public String getFamilyName() {
            return getText();
        }

        @Override
        public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
            return true;
        }

        @Override
        public void invoke(@NotNull Project project, Editor editor, PsiFile file)
                throws IncorrectOperationException {
            final IntentionSettingsConfigurable configurable = new IntentionSettingsConfigurable();
            ShowSettingsUtil.getInstance().editConfigurable(project, configurable, new Runnable() {
                @Override
                public void run() {
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            configurable.selectIntention(myFamilyName);
                        }
                    });
                }
            });
        }

        @Override
        public boolean startInWriteAction() {
            return false;
        }
    }
}