851694db809e3420629ed19c64f8e1e0e1a17e83
[fedora-idea.git] / platform / lang-impl / src / com / intellij / codeInsight / intention / impl / IntentionHintComponent.java
blob851694db809e3420629ed19c64f8e1e0e1a17e83
1 /*
2 * Copyright 2000-2009 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.HintManagerImpl;
23 import com.intellij.codeInsight.hint.HintUtil;
24 import com.intellij.codeInsight.hint.PriorityQuestionAction;
25 import com.intellij.codeInsight.hint.ScrollAwareHint;
26 import com.intellij.codeInsight.intention.IntentionAction;
27 import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings;
28 import com.intellij.openapi.Disposable;
29 import com.intellij.openapi.actionSystem.ActionManager;
30 import com.intellij.openapi.actionSystem.IdeActions;
31 import com.intellij.openapi.application.ApplicationManager;
32 import com.intellij.openapi.diagnostic.Logger;
33 import com.intellij.openapi.editor.Editor;
34 import com.intellij.openapi.editor.LogicalPosition;
35 import com.intellij.openapi.keymap.KeymapUtil;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.ui.popup.JBPopupFactory;
38 import com.intellij.openapi.ui.popup.JBPopupListener;
39 import com.intellij.openapi.ui.popup.LightweightWindowEvent;
40 import com.intellij.openapi.ui.popup.ListPopup;
41 import com.intellij.openapi.util.Disposer;
42 import com.intellij.openapi.util.IconLoader;
43 import com.intellij.psi.PsiFile;
44 import com.intellij.ui.LightweightHint;
45 import com.intellij.ui.RowIcon;
46 import com.intellij.ui.awt.RelativePoint;
47 import com.intellij.util.Alarm;
48 import com.intellij.util.IncorrectOperationException;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.TestOnly;
52 import javax.swing.*;
53 import javax.swing.border.Border;
54 import java.awt.*;
55 import java.awt.event.ActionEvent;
56 import java.awt.event.ActionListener;
57 import java.awt.event.MouseAdapter;
58 import java.awt.event.MouseEvent;
60 /**
61 * @author max
62 * @author Mike
63 * @author Valentin
64 * @author Eugene Belyaev
65 * @author Konstantin Bulenkov
66 * @author and me too (Chinee?)
68 public class IntentionHintComponent extends JPanel implements Disposable, ScrollAwareHint {
69 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.intention.impl.IntentionHintComponent.ListPopupRunnable");
71 static final Icon ourIntentionIcon = IconLoader.getIcon("/actions/realIntentionBulb.png");
72 static final Icon ourBulbIcon = IconLoader.getIcon("/actions/intentionBulb.png");
73 static final Icon ourQuickFixIcon = IconLoader.getIcon("/actions/quickfixBulb.png");
74 static final Icon ourIntentionOffIcon = IconLoader.getIcon("/actions/realIntentionOffBulb.png");
75 static final Icon ourQuickFixOffIcon = IconLoader.getIcon("/actions/quickfixOffBulb.png");
76 static final Icon ourArrowIcon = IconLoader.getIcon("/general/arrowDown.png");
77 private static final Border INACTIVE_BORDER = null;
78 private static final Insets INACTIVE_MARGIN = new Insets(0, 0, 0, 0);
79 private static final Insets ACTIVE_MARGIN = new Insets(0, 0, 0, 0);
81 private final Editor myEditor;
83 private static final Alarm myAlarm = new Alarm();
85 private final RowIcon myHighlightedIcon;
86 private final JButton myButton;
88 private final Icon mySmartTagIcon;
90 private static final int DELAY = 500;
91 private final MyComponentHint myComponentHint;
92 private static final Color BACKGROUND_COLOR = new Color(255, 255, 255, 0);
93 private boolean myPopupShown = false;
94 private boolean myDisposed = false;
95 private ListPopup myPopup;
96 private final PsiFile myFile;
98 public static IntentionHintComponent showIntentionHint(Project project, final PsiFile file, Editor editor, ShowIntentionsPass.IntentionsInfo intentions,
99 boolean showExpanded) {
100 final Point position = getHintPosition(editor);
101 return showIntentionHint(project, file, editor, intentions, showExpanded, position);
104 public static IntentionHintComponent showIntentionHint(Project project, final PsiFile file, Editor editor,
105 ShowIntentionsPass.IntentionsInfo intentions,
106 boolean showExpanded,
107 final Point position) {
108 final IntentionHintComponent component = new IntentionHintComponent(project, file, editor, intentions);
110 if (showExpanded) {
111 component.showIntentionHintImpl(false, position);
112 ApplicationManager.getApplication().invokeLater(new Runnable() {
113 public void run() {
114 component.showPopup();
118 else {
119 component.showIntentionHintImpl(true, position);
121 Disposer.register(project, component);
123 return component;
126 @TestOnly
127 public boolean isDisposed() {
128 return myDisposed;
131 public void dispose() {
132 myDisposed = true;
133 myComponentHint.hide();
134 super.hide();
137 public void editorScrolled() {
138 closePopup();
141 //true if actions updated, there is nothing to do
142 //false if has to recreate popup, no need to reshow
143 //null if has to reshow
144 public synchronized Boolean updateActions(ShowIntentionsPass.IntentionsInfo intentions) {
145 if (myPopup.isDisposed()) return null;
146 if (!myFile.isValid()) return null;
147 IntentionListStep step = (IntentionListStep)myPopup.getListStep();
148 if (!step.updateActions(intentions)) {
149 return Boolean.TRUE;
151 if (!myPopupShown) {
152 return Boolean.FALSE;
154 return null;
157 public synchronized void recreate() {
158 IntentionListStep step = (IntentionListStep)myPopup.getListStep();
159 recreateMyPopup(step);
162 private void showIntentionHintImpl(final boolean delay, final Point position) {
163 final int offset = myEditor.getCaretModel().getOffset();
165 myComponentHint.setShouldDelay(delay);
167 HintManagerImpl hintManager = HintManagerImpl.getInstanceImpl();
169 PriorityQuestionAction action = new PriorityQuestionAction() {
170 public boolean execute() {
171 showPopup();
172 return true;
175 public int getPriority() {
176 return -10;
179 if (hintManager.canShowQuestionAction(action)) {
180 hintManager.showQuestionHint(myEditor, position, offset, offset, myComponentHint, action);
184 private static Point getHintPosition(Editor editor) {
185 if (ApplicationManager.getApplication().isUnitTestMode()) return new Point();
186 final int offset = editor.getCaretModel().getOffset();
187 final LogicalPosition pos = editor.offsetToLogicalPosition(offset);
188 int line = pos.line;
190 final Point position = editor.logicalPositionToXY(new LogicalPosition(line, 0));
191 final int yShift = (ourIntentionIcon.getIconHeight() - editor.getLineHeight() - 1) / 2 - 1;
192 final int xShift = ourIntentionIcon.getIconWidth();
194 LOG.assertTrue(editor.getComponent().isDisplayable());
195 Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
196 Point realPoint = new Point(Math.max(0,visibleArea.x - xShift), position.y + yShift-20);
197 Point location = SwingUtilities.convertPoint(editor.getContentComponent(), realPoint, editor.getComponent().getRootPane().getLayeredPane());
199 return new Point(location.x, location.y);
202 private IntentionHintComponent(@NotNull Project project, @NotNull PsiFile file, @NotNull Editor editor, ShowIntentionsPass.IntentionsInfo intentions) {
203 ApplicationManager.getApplication().assertReadAccessAllowed();
204 myFile = file;
205 myEditor = editor;
207 setLayout(new BorderLayout());
208 setOpaque(false);
210 boolean showFix = false;
211 for (final HighlightInfo.IntentionActionDescriptor pairs : intentions.errorFixesToShow) {
212 IntentionAction fix = pairs.getAction();
213 if (IntentionManagerSettings.getInstance().isShowLightBulb(fix)) {
214 showFix = true;
215 break;
218 mySmartTagIcon = showFix ? ourQuickFixIcon : ourBulbIcon;
220 myHighlightedIcon = new RowIcon(2);
221 myHighlightedIcon.setIcon(mySmartTagIcon, 0);
222 myHighlightedIcon.setIcon(ourArrowIcon, 1);
224 myButton = new JButton(mySmartTagIcon);
225 myButton.setFocusable(false);
226 myButton.setMargin(INACTIVE_MARGIN);
227 myButton.setBorderPainted(false);
228 myButton.setContentAreaFilled(false);
230 add(myButton, BorderLayout.CENTER);
231 setBorder(INACTIVE_BORDER);
233 myButton.addActionListener(new ActionListener() {
234 public void actionPerformed(ActionEvent e) {
235 showPopup();
239 myButton.addMouseListener(new MouseAdapter() {
240 public void mouseEntered(MouseEvent e) {
241 onMouseEnter();
244 public void mouseExited(MouseEvent e) {
245 onMouseExit();
249 myComponentHint = new MyComponentHint(this);
250 IntentionListStep step = new IntentionListStep(this, intentions, myEditor, myFile, project);
251 recreateMyPopup(step);
254 public void hide() {
255 Disposer.dispose(this);
258 private void onMouseExit() {
259 Window ancestor = SwingUtilities.getWindowAncestor(myPopup.getContent());
260 if (ancestor == null) {
261 myButton.setBackground(BACKGROUND_COLOR);
262 myButton.setIcon(mySmartTagIcon);
263 setBorder(INACTIVE_BORDER);
264 myButton.setMargin(INACTIVE_MARGIN);
265 updateComponentHintSize();
269 private void onMouseEnter() {
270 myButton.setBackground(HintUtil.QUESTION_COLOR);
271 myButton.setIcon(myHighlightedIcon);
272 setBorder(BorderFactory.createLineBorder(Color.black));
273 myButton.setMargin(ACTIVE_MARGIN);
274 updateComponentHintSize();
276 String acceleratorsText = KeymapUtil.getFirstKeyboardShortcutText(
277 ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS));
278 if (acceleratorsText.length() > 0) {
279 myButton.setToolTipText(CodeInsightBundle.message("lightbulb.tooltip", acceleratorsText));
283 private void updateComponentHintSize() {
284 Component component = myComponentHint.getComponent();
285 component.setSize(getPreferredSize().width, getHeight());
288 @TestOnly
289 public LightweightHint getComponentHint() {
290 return myComponentHint;
293 private void closePopup() {
294 myPopup.cancel();
295 myPopupShown = false;
298 private void showPopup() {
299 if (myPopup == null || myPopup.isDisposed()) return;
301 if (isShowing()) {
302 myPopup.show(RelativePoint.getSouthWestOf(this));
304 else {
305 myPopup.showInBestPositionFor(myEditor);
308 myPopupShown = true;
311 private void recreateMyPopup(IntentionListStep step) {
312 if (myPopup != null) {
313 Disposer.dispose(myPopup);
315 myPopup = JBPopupFactory.getInstance().createListPopup(step);
316 myPopup.addListener(new JBPopupListener.Adapter() {
317 @Override
318 public void onClosed(LightweightWindowEvent event) {
319 myPopupShown = false;
322 Disposer.register(this, myPopup);
323 Disposer.register(myPopup, new Disposable() {
324 public void dispose() {
325 ApplicationManager.getApplication().assertIsDispatchThread();
330 void canceled(IntentionListStep intentionListStep) {
331 if (myPopup.getListStep() != intentionListStep || myDisposed) {
332 return;
334 // Root canceled. Create new popup. This one cannot be reused.
335 recreateMyPopup(intentionListStep);
338 private class MyComponentHint extends LightweightHint {
339 private boolean myVisible = false;
340 private boolean myShouldDelay;
342 private MyComponentHint(JComponent component) {
343 super(component);
346 public void show(@NotNull final JComponent parentComponent, final int x, final int y, final JComponent focusBackComponent) {
347 myVisible = true;
348 if (myShouldDelay) {
349 myAlarm.cancelAllRequests();
350 myAlarm.addRequest(new Runnable() {
351 public void run() {
352 showImpl(parentComponent, x, y, focusBackComponent);
354 }, DELAY);
356 else {
357 showImpl(parentComponent, x, y, focusBackComponent);
361 private void showImpl(JComponent parentComponent, int x, int y, JComponent focusBackComponent) {
362 if (!parentComponent.isShowing()) return;
363 super.show(parentComponent, x, y, focusBackComponent);
366 public void hide() {
367 super.hide();
368 myVisible = false;
369 myAlarm.cancelAllRequests();
372 public boolean isVisible() {
373 return myVisible || super.isVisible();
376 public void setShouldDelay(boolean shouldDelay) {
377 myShouldDelay = shouldDelay;
381 public static class EnableDisableIntentionAction implements IntentionAction{
382 private final String myActionFamilyName;
383 private final IntentionManagerSettings mySettings = IntentionManagerSettings.getInstance();
384 private final IntentionAction myAction;
386 public EnableDisableIntentionAction(IntentionAction action) {
387 myActionFamilyName = action.getFamilyName();
388 myAction = action;
389 // needed for checking errors in user written actions
390 //noinspection ConstantConditions
391 LOG.assertTrue(myActionFamilyName != null, "action "+action.getClass()+" family returned null");
394 @NotNull
395 public String getText() {
396 return mySettings.isEnabled(myAction) ?
397 CodeInsightBundle.message("disable.intention.action", myActionFamilyName) :
398 CodeInsightBundle.message("enable.intention.action", myActionFamilyName);
401 @NotNull
402 public String getFamilyName() {
403 return getText();
406 public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
407 return true;
410 public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
411 mySettings.setEnabled(myAction, !mySettings.isEnabled(myAction));
414 public boolean startInWriteAction() {
415 return false;
418 @Override
419 public String toString() {
420 return getText();