2 * Copyright 2000-2015 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.intellij.codeInsight.intention.impl;
19 import com.intellij.codeInsight.CodeInsightBundle;
20 import com.intellij.codeInsight.daemon.impl.HighlightInfo;
21 import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass;
22 import com.intellij.codeInsight.hint.HintManager;
23 import com.intellij.codeInsight.hint.HintManagerImpl;
24 import com.intellij.codeInsight.hint.PriorityQuestionAction;
25 import com.intellij.codeInsight.hint.ScrollAwareHint;
26 import com.intellij.codeInsight.intention.HighPriorityAction;
27 import com.intellij.codeInsight.intention.IntentionAction;
28 import com.intellij.codeInsight.intention.impl.config.IntentionActionWrapper;
29 import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings;
30 import com.intellij.codeInsight.intention.impl.config.IntentionSettingsConfigurable;
31 import com.intellij.codeInsight.unwrap.ScopeHighlighter;
32 import com.intellij.codeInspection.SuppressIntentionActionFromFix;
33 import com.intellij.icons.AllIcons;
34 import com.intellij.openapi.Disposable;
35 import com.intellij.openapi.actionSystem.ActionManager;
36 import com.intellij.openapi.actionSystem.DataProvider;
37 import com.intellij.openapi.actionSystem.IdeActions;
38 import com.intellij.openapi.actionSystem.PlatformDataKeys;
39 import com.intellij.openapi.application.ApplicationManager;
40 import com.intellij.openapi.diagnostic.Logger;
41 import com.intellij.openapi.editor.Editor;
42 import com.intellij.openapi.editor.EditorFactory;
43 import com.intellij.openapi.editor.VisualPosition;
44 import com.intellij.openapi.editor.actions.EditorActionUtil;
45 import com.intellij.openapi.editor.colors.EditorColors;
46 import com.intellij.openapi.editor.colors.EditorColorsManager;
47 import com.intellij.openapi.editor.event.EditorFactoryAdapter;
48 import com.intellij.openapi.editor.event.EditorFactoryEvent;
49 import com.intellij.openapi.keymap.KeymapUtil;
50 import com.intellij.openapi.options.ShowSettingsUtil;
51 import com.intellij.openapi.project.Project;
52 import com.intellij.openapi.ui.popup.JBPopupFactory;
53 import com.intellij.openapi.ui.popup.JBPopupListener;
54 import com.intellij.openapi.ui.popup.LightweightWindowEvent;
55 import com.intellij.openapi.ui.popup.ListPopup;
56 import com.intellij.openapi.util.Comparing;
57 import com.intellij.openapi.util.Condition;
58 import com.intellij.openapi.util.Disposer;
59 import com.intellij.psi.PsiElement;
60 import com.intellij.psi.PsiFile;
61 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
62 import com.intellij.refactoring.BaseRefactoringIntentionAction;
63 import com.intellij.ui.HintHint;
64 import com.intellij.ui.LightweightHint;
65 import com.intellij.ui.PopupMenuListenerAdapter;
66 import com.intellij.ui.RowIcon;
67 import com.intellij.ui.awt.RelativePoint;
68 import com.intellij.util.Alarm;
69 import com.intellij.util.IncorrectOperationException;
70 import com.intellij.util.ThreeState;
71 import com.intellij.util.containers.ContainerUtil;
72 import com.intellij.util.ui.EmptyIcon;
73 import org.jetbrains.annotations.NotNull;
74 import org.jetbrains.annotations.Nullable;
75 import org.jetbrains.annotations.TestOnly;
78 import javax.swing.border.Border;
79 import javax.swing.event.ListSelectionEvent;
80 import javax.swing.event.ListSelectionListener;
81 import javax.swing.event.PopupMenuEvent;
82 import javax.swing.event.PopupMenuListener;
84 import java.awt.event.MouseAdapter;
85 import java.awt.event.MouseEvent;
86 import java.util.Collections;
87 import java.util.List;
93 * @author Eugene Belyaev
94 * @author Konstantin Bulenkov
95 * @author and me too (Chinee?)
97 public class IntentionHintComponent extends JPanel implements Disposable, ScrollAwareHint {
98 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.intention.impl.IntentionHintComponent.ListPopupRunnable");
100 static final Icon ourInactiveArrowIcon = new EmptyIcon(AllIcons.General.ArrowDown.getIconWidth(), AllIcons.General.ArrowDown.getIconHeight());
102 private static final int NORMAL_BORDER_SIZE = 6;
103 private static final int SMALL_BORDER_SIZE = 4;
105 private static final Border INACTIVE_BORDER = BorderFactory.createEmptyBorder(NORMAL_BORDER_SIZE, NORMAL_BORDER_SIZE, NORMAL_BORDER_SIZE, NORMAL_BORDER_SIZE);
106 private static final Border INACTIVE_BORDER_SMALL = BorderFactory.createEmptyBorder(SMALL_BORDER_SIZE, SMALL_BORDER_SIZE, SMALL_BORDER_SIZE, SMALL_BORDER_SIZE);
108 private static Border createActiveBorder() {
109 return BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(getBorderColor(), 1), BorderFactory.createEmptyBorder(NORMAL_BORDER_SIZE - 1, NORMAL_BORDER_SIZE-1, NORMAL_BORDER_SIZE-1, NORMAL_BORDER_SIZE-1));
112 private static Border createActiveBorderSmall() {
113 return BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(getBorderColor(), 1), BorderFactory.createEmptyBorder(SMALL_BORDER_SIZE-1, SMALL_BORDER_SIZE-1, SMALL_BORDER_SIZE-1, SMALL_BORDER_SIZE-1));
116 private static Color getBorderColor() {
117 return EditorColorsManager.getInstance().getGlobalScheme().getColor(EditorColors.SELECTED_TEARLINE_COLOR);
120 private final Editor myEditor;
122 private static final Alarm myAlarm = new Alarm();
124 private final RowIcon myHighlightedIcon;
125 private final JLabel myIconLabel;
127 private final RowIcon myInactiveIcon;
129 private static final int DELAY = 500;
130 private final MyComponentHint myComponentHint;
131 private volatile boolean myPopupShown = false;
132 private boolean myDisposed = false;
133 private volatile ListPopup myPopup;
134 private final PsiFile myFile;
136 private PopupMenuListener myOuterComboboxPopupListener;
139 public static IntentionHintComponent showIntentionHint(@NotNull Project project,
140 @NotNull PsiFile file,
141 @NotNull Editor editor,
142 @NotNull ShowIntentionsPass.IntentionsInfo intentions,
143 boolean showExpanded) {
144 ApplicationManager.getApplication().assertIsDispatchThread();
145 final Point position = getHintPosition(editor);
146 return showIntentionHint(project, file, editor, intentions, showExpanded, position);
150 public static IntentionHintComponent showIntentionHint(@NotNull final Project project,
151 @NotNull PsiFile file,
152 @NotNull final Editor editor,
153 @NotNull ShowIntentionsPass.IntentionsInfo intentions,
154 boolean showExpanded,
155 @NotNull Point position) {
156 ApplicationManager.getApplication().assertIsDispatchThread();
157 final IntentionHintComponent component = new IntentionHintComponent(project, file, editor, intentions);
159 component.showIntentionHintImpl(!showExpanded, position);
160 Disposer.register(project, component);
162 ApplicationManager.getApplication().invokeLater(new Runnable() {
165 if (!editor.isDisposed() && editor.getComponent().isShowing()) {
166 component.showPopup(false);
169 }, project.getDisposed());
176 public boolean isDisposed() {
181 public void dispose() {
182 ApplicationManager.getApplication().assertIsDispatchThread();
184 myComponentHint.hide();
187 if (myOuterComboboxPopupListener != null) {
188 final Container ancestor = SwingUtilities.getAncestorOfClass(JComboBox.class, myEditor.getContentComponent());
189 if (ancestor != null) {
190 ((JComboBox)ancestor).removePopupMenuListener(myOuterComboboxPopupListener);
193 myOuterComboboxPopupListener = null;
198 public void editorScrolled() {
202 //true if actions updated, there is nothing to do
203 //false if has to recreate popup, no need to reshow
204 //null if has to reshow
205 public Boolean updateActions(@NotNull ShowIntentionsPass.IntentionsInfo intentions) {
206 if (myPopup.isDisposed()) return null;
207 if (!myFile.isValid()) return null;
208 IntentionListStep step = (IntentionListStep)myPopup.getListStep();
209 if (!step.updateActions(intentions)) {
213 return Boolean.FALSE;
218 // for using in tests !
220 public IntentionAction getAction(int index) {
221 if (myPopup == null || myPopup.isDisposed()) {
224 IntentionListStep listStep = (IntentionListStep)myPopup.getListStep();
225 List<IntentionActionWithTextCaching> values = listStep.getValues();
226 if (values.size() <= index) {
229 return values.get(index).getAction();
232 public void recreate() {
233 ApplicationManager.getApplication().assertIsDispatchThread();
234 IntentionListStep step = (IntentionListStep)myPopup.getListStep();
235 recreateMyPopup(step);
238 private void showIntentionHintImpl(final boolean delay, final Point position) {
239 final int offset = myEditor.getCaretModel().getOffset();
241 myComponentHint.setShouldDelay(delay);
243 HintManagerImpl hintManager = HintManagerImpl.getInstanceImpl();
245 PriorityQuestionAction action = new PriorityQuestionAction() {
247 public boolean execute() {
253 public int getPriority() {
257 if (hintManager.canShowQuestionAction(action)) {
258 hintManager.showQuestionHint(myEditor, position, offset, offset, myComponentHint, action, HintManager.ABOVE);
263 private static Point getHintPosition(Editor editor) {
264 if (ApplicationManager.getApplication().isUnitTestMode()) return new Point();
265 final int offset = editor.getCaretModel().getOffset();
266 final VisualPosition pos = editor.offsetToVisualPosition(offset);
269 final Point position = editor.visualPositionToXY(new VisualPosition(line, 0));
270 LOG.assertTrue(editor.getComponent().isDisplayable());
272 JComponent convertComponent = editor.getContentComponent();
275 final boolean oneLineEditor = editor.isOneLineMode();
277 // place bulb at the corner of the surrounding component
278 final JComponent contentComponent = editor.getContentComponent();
279 Container ancestorOfClass = SwingUtilities.getAncestorOfClass(JComboBox.class, contentComponent);
281 if (ancestorOfClass != null) {
282 convertComponent = (JComponent) ancestorOfClass;
284 ancestorOfClass = SwingUtilities.getAncestorOfClass(JTextField.class, contentComponent);
285 if (ancestorOfClass != null) {
286 convertComponent = (JComponent) ancestorOfClass;
290 realPoint = new Point(- (AllIcons.Actions.RealIntentionBulb.getIconWidth() / 2) - 4, - (AllIcons.Actions.RealIntentionBulb
291 .getIconHeight() / 2));
293 // try to place bulb on the same line
294 final int borderHeight = NORMAL_BORDER_SIZE;
296 int yShift = -(NORMAL_BORDER_SIZE + AllIcons.Actions.RealIntentionBulb.getIconHeight());
297 if (canPlaceBulbOnTheSameLine(editor)) {
298 yShift = -(borderHeight + (AllIcons.Actions.RealIntentionBulb.getIconHeight() - editor.getLineHeight()) /2 + 3);
301 final int xShift = AllIcons.Actions.RealIntentionBulb.getIconWidth();
303 Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
304 realPoint = new Point(Math.max(0,visibleArea.x - xShift), position.y + yShift);
307 Point location = SwingUtilities.convertPoint(convertComponent, realPoint, editor.getComponent().getRootPane().getLayeredPane());
308 return new Point(location.x, location.y);
311 private static boolean canPlaceBulbOnTheSameLine(Editor editor) {
312 if (ApplicationManager.getApplication().isUnitTestMode() || editor.isOneLineMode()) return false;
313 final int offset = editor.getCaretModel().getOffset();
314 final VisualPosition pos = editor.offsetToVisualPosition(offset);
317 final int firstNonSpaceColumnOnTheLine = EditorActionUtil.findFirstNonSpaceColumnOnTheLine(editor, line);
318 if (firstNonSpaceColumnOnTheLine == -1) return false;
319 final Point point = editor.visualPositionToXY(new VisualPosition(line, firstNonSpaceColumnOnTheLine));
320 return point.x > AllIcons.Actions.RealIntentionBulb.getIconWidth() + (editor.isOneLineMode() ? SMALL_BORDER_SIZE : NORMAL_BORDER_SIZE) * 2;
323 private IntentionHintComponent(@NotNull Project project,
324 @NotNull PsiFile file,
325 @NotNull final Editor editor,
326 @NotNull ShowIntentionsPass.IntentionsInfo intentions) {
327 ApplicationManager.getApplication().assertIsDispatchThread();
331 setLayout(new BorderLayout());
334 boolean showRefactoringsBulb = ContainerUtil.exists(intentions.inspectionFixesToShow, new Condition<HighlightInfo.IntentionActionDescriptor>() {
336 public boolean value(HighlightInfo.IntentionActionDescriptor descriptor) {
337 return descriptor.getAction() instanceof BaseRefactoringIntentionAction;
340 boolean showFix = !showRefactoringsBulb && ContainerUtil.exists(intentions.errorFixesToShow, new Condition<HighlightInfo.IntentionActionDescriptor>() {
342 public boolean value(HighlightInfo.IntentionActionDescriptor descriptor) {
343 return IntentionManagerSettings.getInstance().isShowLightBulb(descriptor.getAction());
347 Icon smartTagIcon = showRefactoringsBulb ? AllIcons.Actions.RefactoringBulb : showFix ? AllIcons.Actions.QuickfixBulb : AllIcons.Actions.IntentionBulb;
349 myHighlightedIcon = new RowIcon(2);
350 myHighlightedIcon.setIcon(smartTagIcon, 0);
351 myHighlightedIcon.setIcon(AllIcons.General.ArrowDown, 1);
353 myInactiveIcon = new RowIcon(2);
354 myInactiveIcon.setIcon(smartTagIcon, 0);
355 myInactiveIcon.setIcon(ourInactiveArrowIcon, 1);
357 myIconLabel = new JLabel(myInactiveIcon);
358 myIconLabel.setOpaque(false);
360 add(myIconLabel, BorderLayout.CENTER);
362 setBorder(editor.isOneLineMode() ? INACTIVE_BORDER_SMALL : INACTIVE_BORDER);
364 myIconLabel.addMouseListener(new MouseAdapter() {
366 public void mousePressed(MouseEvent e) {
367 if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
373 public void mouseEntered(MouseEvent e) {
374 onMouseEnter(editor.isOneLineMode());
378 public void mouseExited(MouseEvent e) {
379 onMouseExit(editor.isOneLineMode());
383 myComponentHint = new MyComponentHint(this);
384 IntentionListStep step = new IntentionListStep(this, intentions, myEditor, myFile, project);
385 recreateMyPopup(step);
386 // dispose myself when editor closed
387 EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryAdapter() {
389 public void editorReleased(@NotNull EditorFactoryEvent event) {
390 if (event.getEditor() == myEditor) {
399 Disposer.dispose(this);
402 private void onMouseExit(final boolean small) {
403 Window ancestor = SwingUtilities.getWindowAncestor(myPopup.getContent());
404 if (ancestor == null) {
405 myIconLabel.setIcon(myInactiveIcon);
406 setBorder(small ? INACTIVE_BORDER_SMALL : INACTIVE_BORDER);
410 private void onMouseEnter(final boolean small) {
411 myIconLabel.setIcon(myHighlightedIcon);
412 setBorder(small ? createActiveBorderSmall() : createActiveBorder());
414 String acceleratorsText = KeymapUtil.getFirstKeyboardShortcutText(
415 ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS));
416 if (!acceleratorsText.isEmpty()) {
417 myIconLabel.setToolTipText(CodeInsightBundle.message("lightbulb.tooltip", acceleratorsText));
422 public LightweightHint getComponentHint() {
423 return myComponentHint;
426 private void closePopup() {
427 ApplicationManager.getApplication().assertIsDispatchThread();
429 myPopupShown = false;
432 private void showPopup(boolean mouseClick) {
433 ApplicationManager.getApplication().assertIsDispatchThread();
434 if (myPopup == null || myPopup.isDisposed()) return;
436 if (mouseClick && isShowing()) {
437 final RelativePoint swCorner = RelativePoint.getSouthWestOf(this);
438 final int yOffset = canPlaceBulbOnTheSameLine(myEditor) ? 0 : myEditor.getLineHeight() - (myEditor.isOneLineMode() ? SMALL_BORDER_SIZE : NORMAL_BORDER_SIZE);
439 myPopup.show(new RelativePoint(swCorner.getComponent(), new Point(swCorner.getPoint().x, swCorner.getPoint().y + yOffset)));
442 myPopup.showInBestPositionFor(myEditor);
448 private void recreateMyPopup(@NotNull IntentionListStep step) {
449 ApplicationManager.getApplication().assertIsDispatchThread();
450 if (myPopup != null) {
451 Disposer.dispose(myPopup);
453 myPopup = JBPopupFactory.getInstance().createListPopup(step);
455 final PsiFile injectedFile = InjectedLanguageUtil.findInjectedPsiNoCommit(myFile, myEditor.getCaretModel().getOffset());
456 final Editor injectedEditor = InjectedLanguageUtil.getInjectedEditorForInjectedFile(myEditor, injectedFile);
458 final ScopeHighlighter highlighter = new ScopeHighlighter(myEditor);
459 final ScopeHighlighter injectionHighlighter = new ScopeHighlighter(injectedEditor);
461 myPopup.addListener(new JBPopupListener.Adapter() {
463 public void onClosed(LightweightWindowEvent event) {
464 highlighter.dropHighlight();
465 injectionHighlighter.dropHighlight();
466 myPopupShown = false;
469 myPopup.addListSelectionListener(new ListSelectionListener() {
471 public void valueChanged(ListSelectionEvent e) {
472 final Object source = e.getSource();
473 highlighter.dropHighlight();
474 injectionHighlighter.dropHighlight();
476 if (source instanceof DataProvider) {
477 final Object selectedItem = PlatformDataKeys.SELECTED_ITEM.getData((DataProvider)source);
478 if (selectedItem instanceof IntentionActionWithTextCaching) {
479 final IntentionAction action = ((IntentionActionWithTextCaching)selectedItem).getAction();
480 if (action instanceof SuppressIntentionActionFromFix) {
481 if (injectedFile != null && ((SuppressIntentionActionFromFix)action).isShouldBeAppliedToInjectionHost() == ThreeState.NO) {
482 final PsiElement at = injectedFile.findElementAt(injectedEditor.getCaretModel().getOffset());
483 final PsiElement container = ((SuppressIntentionActionFromFix)action).getContainer(at);
484 if (container != null) {
485 injectionHighlighter.highlight(container, Collections.singletonList(container));
489 final PsiElement at = myFile.findElementAt(myEditor.getCaretModel().getOffset());
490 final PsiElement container = ((SuppressIntentionActionFromFix)action).getContainer(at);
491 if (container != null) {
492 highlighter.highlight(container, Collections.singletonList(container));
501 if (myEditor.isOneLineMode()) {
502 // hide popup on combobox popup show
503 final Container ancestor = SwingUtilities.getAncestorOfClass(JComboBox.class, myEditor.getContentComponent());
504 if (ancestor != null) {
505 final JComboBox comboBox = (JComboBox)ancestor;
506 myOuterComboboxPopupListener = new PopupMenuListenerAdapter() {
508 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
513 comboBox.addPopupMenuListener(myOuterComboboxPopupListener);
517 Disposer.register(this, myPopup);
518 Disposer.register(myPopup, new Disposable() {
520 public void dispose() {
521 ApplicationManager.getApplication().assertIsDispatchThread();
526 void canceled(@NotNull IntentionListStep intentionListStep) {
527 if (myPopup.getListStep() != intentionListStep || myDisposed) {
530 // Root canceled. Create new popup. This one cannot be reused.
531 recreateMyPopup(intentionListStep);
534 private static class MyComponentHint extends LightweightHint {
535 private boolean myVisible = false;
536 private boolean myShouldDelay;
538 private MyComponentHint(JComponent component) {
543 public void show(@NotNull final JComponent parentComponent,
546 final JComponent focusBackComponent,
547 @NotNull HintHint hintHint) {
550 myAlarm.cancelAllRequests();
551 myAlarm.addRequest(new Runnable() {
554 showImpl(parentComponent, x, y, focusBackComponent);
559 showImpl(parentComponent, x, y, focusBackComponent);
563 private void showImpl(JComponent parentComponent, int x, int y, JComponent focusBackComponent) {
564 if (!parentComponent.isShowing()) return;
565 super.show(parentComponent, x, y, focusBackComponent, new HintHint(parentComponent, new Point(x, y)));
572 myAlarm.cancelAllRequests();
576 public boolean isVisible() {
577 return myVisible || super.isVisible();
580 public void setShouldDelay(boolean shouldDelay) {
581 myShouldDelay = shouldDelay;
585 public static class EnableDisableIntentionAction extends AbstractEditIntentionSettingsAction {
586 private final IntentionManagerSettings mySettings = IntentionManagerSettings.getInstance();
587 private final IntentionAction myAction;
589 public EnableDisableIntentionAction(IntentionAction action) {
592 // needed for checking errors in user written actions
593 //noinspection ConstantConditions
594 LOG.assertTrue(myFamilyName != null, "action "+action.getClass()+" family returned null");
599 public String getText() {
600 return mySettings.isEnabled(myAction) ?
601 CodeInsightBundle.message("disable.intention.action", myFamilyName) :
602 CodeInsightBundle.message("enable.intention.action", myFamilyName);
606 public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
607 mySettings.setEnabled(myAction, !mySettings.isEnabled(myAction));
611 public String toString() {
616 public static class EditIntentionSettingsAction extends AbstractEditIntentionSettingsAction implements HighPriorityAction {
617 public EditIntentionSettingsAction(IntentionAction action) {
623 public String getText() {
624 return "Edit intention settings";
628 public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
629 final IntentionSettingsConfigurable configurable = new IntentionSettingsConfigurable();
630 ShowSettingsUtil.getInstance().editConfigurable(project, configurable, new Runnable() {
633 SwingUtilities.invokeLater(new Runnable() {
636 configurable.selectIntention(myFamilyName);
644 private static abstract class AbstractEditIntentionSettingsAction implements IntentionAction {
645 protected final String myFamilyName;
646 private final boolean myDisabled;
648 public AbstractEditIntentionSettingsAction(IntentionAction action) {
649 myFamilyName = action.getFamilyName();
650 myDisabled = action instanceof IntentionActionWrapper &&
651 Comparing.equal(action.getFamilyName(), ((IntentionActionWrapper)action).getFullFamilyName());
656 public String getFamilyName() {
661 public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
666 public boolean startInWriteAction() {