Java tutorial
/* * 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; } } }