move Ctrl-mouse calculation to background thread (RUBY-5727)
[fedora-idea.git] / platform / lang-impl / src / com / intellij / codeInsight / navigation / CtrlMouseHandler.java
blob165567b14e2c19d57f747b51ff53c27314005abb
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.navigation;
19 import com.intellij.codeInsight.CodeInsightBundle;
20 import com.intellij.codeInsight.TargetElementUtilBase;
21 import com.intellij.codeInsight.documentation.DocumentationManager;
22 import com.intellij.codeInsight.hint.HintManager;
23 import com.intellij.codeInsight.hint.HintManagerImpl;
24 import com.intellij.codeInsight.hint.HintUtil;
25 import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
26 import com.intellij.codeInsight.navigation.actions.GotoTypeDeclarationAction;
27 import com.intellij.ide.util.EditSourceUtil;
28 import com.intellij.lang.documentation.DocumentationProvider;
29 import com.intellij.navigation.ItemPresentation;
30 import com.intellij.navigation.NavigationItem;
31 import com.intellij.openapi.actionSystem.IdeActions;
32 import com.intellij.openapi.actionSystem.MouseShortcut;
33 import com.intellij.openapi.actionSystem.Shortcut;
34 import com.intellij.openapi.application.ApplicationManager;
35 import com.intellij.openapi.components.AbstractProjectComponent;
36 import com.intellij.openapi.editor.Document;
37 import com.intellij.openapi.editor.Editor;
38 import com.intellij.openapi.editor.EditorFactory;
39 import com.intellij.openapi.editor.LogicalPosition;
40 import com.intellij.openapi.editor.colors.EditorColorsManager;
41 import com.intellij.openapi.editor.colors.TextAttributesKey;
42 import com.intellij.openapi.editor.event.*;
43 import com.intellij.openapi.editor.markup.*;
44 import com.intellij.openapi.fileEditor.FileEditorManager;
45 import com.intellij.openapi.fileEditor.FileEditorManagerAdapter;
46 import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
47 import com.intellij.openapi.fileEditor.FileEditorManagerListener;
48 import com.intellij.openapi.keymap.Keymap;
49 import com.intellij.openapi.keymap.KeymapManager;
50 import com.intellij.openapi.project.DumbAwareRunnable;
51 import com.intellij.openapi.project.DumbService;
52 import com.intellij.openapi.project.IndexNotReadyException;
53 import com.intellij.openapi.project.Project;
54 import com.intellij.openapi.startup.StartupManager;
55 import com.intellij.openapi.ui.MultiLineLabelUI;
56 import com.intellij.openapi.util.Comparing;
57 import com.intellij.openapi.util.TextRange;
58 import com.intellij.openapi.vfs.VirtualFile;
59 import com.intellij.pom.Navigatable;
60 import com.intellij.psi.*;
61 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
62 import com.intellij.psi.search.searches.DefinitionsSearch;
63 import com.intellij.ui.LightweightHint;
64 import com.intellij.util.Processor;
65 import com.intellij.util.ui.UIUtil;
66 import org.jetbrains.annotations.NotNull;
67 import org.jetbrains.annotations.Nullable;
69 import javax.swing.*;
70 import java.awt.*;
71 import java.awt.event.*;
72 import java.util.ArrayList;
73 import java.util.List;
75 public class CtrlMouseHandler extends AbstractProjectComponent {
76 private final TextAttributes ourReferenceAttributes;
77 private RangeHighlighter myHighlighter;
78 private Editor myHighlighterView;
79 private Cursor myStoredCursor;
80 private Info myStoredInfo;
81 private int myStoredModifiers = 0;
82 private TooltipProvider myTooltipProvider = null;
83 private final FileEditorManager myFileEditorManager;
85 private enum BrowseMode { None, Declaration, TypeDeclaration, Implementation }
87 private final KeyListener myEditorKeyListener = new KeyAdapter() {
88 public void keyPressed(final KeyEvent e) {
89 handleKey(e);
92 public void keyReleased(final KeyEvent e) {
93 handleKey(e);
96 private void handleKey(final KeyEvent e) {
97 int modifiers = e.getModifiers();
98 if ( modifiers == myStoredModifiers) {
99 return;
102 BrowseMode browseMode = getBrowseMode(modifiers);
104 if (browseMode != BrowseMode.None) {
105 if (myTooltipProvider != null) {
106 if (browseMode != myTooltipProvider.getBrowseMode()) {
107 disposeHighlighter();
109 myStoredModifiers = modifiers;
110 myTooltipProvider.execute(browseMode);
112 } else {
113 disposeHighlighter();
114 myTooltipProvider = null;
119 private final FileEditorManagerListener myFileEditorManagerListener = new FileEditorManagerAdapter() {
120 public void selectionChanged(FileEditorManagerEvent e) {
121 disposeHighlighter();
122 myTooltipProvider = null;
126 private final VisibleAreaListener myVisibleAreaListener = new VisibleAreaListener() {
127 public void visibleAreaChanged(VisibleAreaEvent e) {
128 disposeHighlighter();
129 myTooltipProvider = null;
133 private final EditorMouseAdapter myEditorMouseAdapter = new EditorMouseAdapter() {
134 public void mouseReleased(EditorMouseEvent e) {
135 disposeHighlighter();
136 myTooltipProvider = null;
140 private final EditorMouseMotionListener myEditorMouseMotionListener = new EditorMouseMotionAdapter() {
141 public void mouseMoved(final EditorMouseEvent e) {
142 if (e.isConsumed() || myProject.isDisposed()) {
143 return;
145 MouseEvent mouseEvent = e.getMouseEvent();
147 Editor editor = e.getEditor();
148 if (editor.getProject() != null && editor.getProject() != myProject) return;
149 PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(editor.getDocument());
150 Point point = new Point(mouseEvent.getPoint());
151 if (!PsiDocumentManager.getInstance(myProject).isUncommited(editor.getDocument())) {
152 // when document is committed, try to check injected stuff - it's fast
153 editor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(editor, psiFile, editor.logicalPositionToOffset(editor.xyToLogicalPosition(point)));
156 LogicalPosition pos = editor.xyToLogicalPosition(point);
157 int offset = editor.logicalPositionToOffset(pos);
158 int selStart = editor.getSelectionModel().getSelectionStart();
159 int selEnd = editor.getSelectionModel().getSelectionEnd();
161 myStoredModifiers = mouseEvent.getModifiers();
162 BrowseMode browseMode = getBrowseMode(myStoredModifiers);
164 if (myTooltipProvider != null) {
165 myTooltipProvider.dispose();
168 if (browseMode == BrowseMode.None || offset >= selStart && offset < selEnd) {
169 disposeHighlighter();
170 myTooltipProvider = null;
171 return;
174 myTooltipProvider = new TooltipProvider(editor, pos);
175 myTooltipProvider.execute(browseMode);
179 private static final TextAttributesKey CTRL_CLICKABLE_ATTRIBUTES_KEY =
180 TextAttributesKey.createTextAttributesKey("CTRL_CLICKABLE", new TextAttributes(Color.blue, null, Color.blue, EffectType.LINE_UNDERSCORE, 0));
182 public CtrlMouseHandler(final Project project, StartupManager startupManager, EditorColorsManager colorsManager,
183 FileEditorManager fileEditorManager) {
184 super(project);
185 startupManager.registerPostStartupActivity(new DumbAwareRunnable(){
186 public void run() {
187 EditorEventMulticaster eventMulticaster = EditorFactory.getInstance().getEventMulticaster();
188 eventMulticaster.addEditorMouseListener(myEditorMouseAdapter, project);
189 eventMulticaster.addEditorMouseMotionListener(myEditorMouseMotionListener, project);
192 ourReferenceAttributes = colorsManager.getGlobalScheme().getAttributes(CTRL_CLICKABLE_ATTRIBUTES_KEY);
193 myFileEditorManager = fileEditorManager;
196 @NotNull
197 public String getComponentName() {
198 return "CtrlMouseHandler";
201 private static BrowseMode getBrowseMode(final int modifiers) {
202 if ( modifiers != 0 ) {
203 final Keymap activeKeymap = KeymapManager.getInstance().getActiveKeymap();
204 if (matchMouseShourtcut(activeKeymap, modifiers, IdeActions.ACTION_GOTO_DECLARATION)) return BrowseMode.Declaration;
205 if (matchMouseShourtcut(activeKeymap, modifiers, IdeActions.ACTION_GOTO_TYPE_DECLARATION)) return BrowseMode.TypeDeclaration;
206 if (matchMouseShourtcut(activeKeymap, modifiers, IdeActions.ACTION_GOTO_IMPLEMENTATION)) return BrowseMode.Implementation;
208 return BrowseMode.None;
211 private static boolean matchMouseShourtcut(final Keymap activeKeymap, final int modifiers, final String actionId) {
212 final MouseShortcut syntheticShortcat = new MouseShortcut(MouseEvent.BUTTON1, modifiers, 1);
213 for ( Shortcut shortcut : activeKeymap.getShortcuts(actionId)) {
214 if ( shortcut instanceof MouseShortcut) {
215 final MouseShortcut mouseShortcut = (MouseShortcut)shortcut;
216 if ( mouseShortcut.getModifiers() == syntheticShortcat.getModifiers() ) {
217 return true;
221 return false;
224 @Nullable
225 private static String generateInfo(PsiElement element) {
226 final DocumentationProvider documentationProvider = DocumentationManager.getProviderFromElement(element);
228 String info = documentationProvider.getQuickNavigateInfo(element);
229 if (info != null) {
230 return info;
233 if (element instanceof PsiFile) {
234 final VirtualFile virtualFile = ((PsiFile)element).getVirtualFile();
235 if (virtualFile != null) {
236 return virtualFile.getPresentableUrl();
240 if (element instanceof NavigationItem) {
241 final ItemPresentation presentation = ((NavigationItem)element).getPresentation();
242 if (presentation != null) {
243 return presentation.getPresentableText();
247 return null;
250 private abstract static class Info {
251 @NotNull protected final PsiElement myElementAtPointer;
252 public final int myStartOffset;
253 public final int myEndOffset;
255 public Info(@NotNull PsiElement elementAtPointer, int startOffset, int endOffset ) {
256 myElementAtPointer = elementAtPointer;
257 myStartOffset = startOffset;
258 myEndOffset = endOffset;
261 public Info(@NotNull PsiElement elementAtPointer ) {
262 this(elementAtPointer, elementAtPointer.getTextOffset(), elementAtPointer.getTextOffset() + elementAtPointer.getTextLength());
265 boolean isSimilarTo(final Info that) {
266 return Comparing.equal(myElementAtPointer, that.myElementAtPointer) &&
267 myStartOffset == that.myStartOffset &&
268 myEndOffset == that.myEndOffset;
271 @Nullable
272 public abstract String getInfo();
274 public abstract boolean isValid(Document document);
277 private static void showDumbModeNotification(final Project project) {
278 DumbService.getInstance(project).showDumbModeNotification("Element information is not available during index update");
281 private static class InfoSingle extends Info {
282 @NotNull private final PsiElement myTargetElement;
284 public InfoSingle(@NotNull PsiElement elementAtPointer, @NotNull PsiElement targetElement) {
285 super(elementAtPointer);
286 myTargetElement = targetElement;
289 public InfoSingle(final PsiReference ref, @NotNull final PsiElement targetElement) {
290 super(ref.getElement(), ref.getElement().getTextRange().getStartOffset() + ref.getRangeInElement().getStartOffset(),
291 ref.getElement().getTextRange().getStartOffset() + ref.getRangeInElement().getEndOffset());
292 myTargetElement = targetElement;
295 @Nullable
296 public String getInfo() {
297 try {
298 return generateInfo(myTargetElement);
300 catch (IndexNotReadyException e) {
301 showDumbModeNotification(myTargetElement.getProject());
302 return null;
306 public boolean isValid(Document document) {
307 return myTargetElement.isValid() &&
308 myTargetElement != myElementAtPointer &&
309 myTargetElement != myElementAtPointer.getParent() &&
310 new TextRange(0, document.getTextLength()).containsRange(myStartOffset, myEndOffset)
311 /* && targetNavigateable(myTargetElement)*/;
315 private static class InfoMultiple extends Info {
317 public InfoMultiple(@NotNull final PsiElement elementAtPointer) {
318 super(elementAtPointer);
321 public String getInfo() {
322 return CodeInsightBundle.message("multiple.implementations.tooltip");
325 public boolean isValid(Document document) {
326 return new TextRange(0, document.getTextLength()).containsRange(myStartOffset, myEndOffset);
330 @Nullable
331 private Info getInfoAt(final Editor editor, LogicalPosition pos, BrowseMode browseMode) {
332 Document document = editor.getDocument();
333 PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
334 if (file == null) return null;
335 PsiDocumentManager.getInstance(myProject).commitAllDocuments();
337 if (TargetElementUtilBase.inVirtualSpace(editor, pos)) {
338 return null;
341 final int offset = editor.logicalPositionToOffset(pos);
343 int selStart = editor.getSelectionModel().getSelectionStart();
344 int selEnd = editor.getSelectionModel().getSelectionEnd();
346 if (offset >= selStart && offset < selEnd) return null;
348 PsiElement targetElement = null;
350 if (browseMode == BrowseMode.TypeDeclaration) {
351 try {
352 targetElement = GotoTypeDeclarationAction.findSymbolType(editor, offset);
354 catch (IndexNotReadyException e) {
355 showDumbModeNotification(myProject);
358 else if (browseMode == BrowseMode.Declaration) {
359 PsiReference ref = TargetElementUtilBase.findReference(editor, offset);
360 if (ref != null) {
361 PsiElement resolvedElement = resolve(ref);
362 if (resolvedElement != null) {
363 return new InfoSingle (ref, resolvedElement);
366 targetElement = GotoDeclarationAction.findTargetElement(myProject, editor, offset);
367 } else if ( browseMode == BrowseMode.Implementation ) {
368 final PsiElement element = TargetElementUtilBase.getInstance().findTargetElement(editor, ImplementationSearcher.getFlags(), offset);
369 PsiElement[] targetElements = new ImplementationSearcher() {
370 @NotNull
371 protected PsiElement[] searchDefinitions(final PsiElement element) {
372 final List<PsiElement> found = new ArrayList<PsiElement>(2);
373 DefinitionsSearch.search(element).forEach( new Processor<PsiElement>() {
374 public boolean process(final PsiElement psiElement) {
375 found.add ( psiElement );
376 return found.size() != 2;
379 return found.toArray(new PsiElement[found.size()]);
381 }.searchImplementations(editor, element, offset);
382 if ( targetElements.length > 1) {
383 PsiElement elementAtPointer = file.findElementAt(offset);
384 if (elementAtPointer != null) {
385 return new InfoMultiple(elementAtPointer);
387 return null;
389 if (targetElements.length == 1) {
390 Navigatable descriptor = EditSourceUtil.getDescriptor(targetElements[0]);
391 if (descriptor == null || !descriptor.canNavigate()) {
392 return null;
394 targetElement = targetElements [ 0 ];
398 if (targetElement != null && targetElement.isPhysical() ) {
399 PsiElement elementAtPointer = file.findElementAt(offset);
400 if ( elementAtPointer != null ) {
401 return new InfoSingle(elementAtPointer, targetElement);
405 return null;
408 @Nullable
409 private static PsiElement resolve(final PsiReference ref) {
410 PsiElement resolvedElement = null;
412 if (ref instanceof PsiPolyVariantReference) {
413 final ResolveResult[] psiElements = ((PsiPolyVariantReference)ref).multiResolve(false);
414 if (psiElements.length > 0) {
415 final ResolveResult resolveResult = psiElements[0];
416 if (resolveResult != null) {
417 resolvedElement = resolveResult.getElement();
421 else {
422 resolvedElement = ref.resolve();
424 return resolvedElement;
427 private void disposeHighlighter() {
428 if (myHighlighter != null) {
429 myHighlighterView.getMarkupModel().removeHighlighter(myHighlighter);
430 Component internalComponent = myHighlighterView.getContentComponent();
431 internalComponent.setCursor(myStoredCursor);
432 internalComponent.removeKeyListener(myEditorKeyListener);
433 myHighlighterView.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener);
434 myFileEditorManager.removeFileEditorManagerListener(myFileEditorManagerListener);
435 HintManager.getInstance().hideAllHints();
436 myHighlighter = null;
437 myHighlighterView = null;
438 myStoredCursor = null;
440 myStoredInfo = null;
443 private class TooltipProvider {
444 private final Editor myEditor;
445 private final LogicalPosition myPosition;
446 private BrowseMode myBrowseMode;
447 private boolean myDisposed;
449 public TooltipProvider(Editor editor, LogicalPosition pos) {
450 myEditor = editor;
451 myPosition = pos;
454 public void dispose() {
455 myDisposed = true;
458 public BrowseMode getBrowseMode() {
459 return myBrowseMode;
462 public void execute(BrowseMode browseMode) {
463 myBrowseMode = browseMode;
465 ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
466 public void run() {
467 ApplicationManager.getApplication().runReadAction(new Runnable() {
468 public void run() {
469 doExecute();
476 private void doExecute() {
477 final Info info;
478 try {
479 info = getInfoAt(myEditor, myPosition, myBrowseMode);
481 catch (IndexNotReadyException e) {
482 showDumbModeNotification(myProject);
483 return;
485 if (info == null) return;
487 SwingUtilities.invokeLater(new Runnable() {
488 public void run() {
489 showHint(info);
494 private void showHint(Info info) {
495 if (myDisposed) return;
496 Component internalComponent = myEditor.getContentComponent();
497 if (myHighlighter != null) {
498 if (!info.isSimilarTo(myStoredInfo)) {
499 disposeHighlighter();
500 } else {
501 // highlighter already set
502 internalComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
503 return;
507 if (info.isValid(myEditor.getDocument())) {
508 installLinkHighlighter(info);
510 internalComponent.addKeyListener(myEditorKeyListener);
511 myEditor.getScrollingModel().addVisibleAreaListener(myVisibleAreaListener);
512 myStoredCursor = internalComponent.getCursor();
513 myStoredInfo = info;
514 internalComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
515 myFileEditorManager.addFileEditorManagerListener(myFileEditorManagerListener);
517 String text = info.getInfo();
519 if (text == null) return;
521 JLabel label = HintUtil.createInformationLabel(text);
522 label.setUI(new MultiLineLabelUI());
523 Font FONT = UIUtil.getLabelFont();
524 label.setFont(FONT);
525 final LightweightHint hint = new LightweightHint(label);
526 final HintManagerImpl hintManager = HintManagerImpl.getInstanceImpl();
527 label.addMouseMotionListener(new MouseMotionAdapter() {
528 public void mouseMoved(MouseEvent e) {
529 hintManager.hideAllHints();
532 Point p = HintManagerImpl.getHintPosition(hint, myEditor, myPosition, HintManager.ABOVE);
533 hintManager.showEditorHint(hint, myEditor, p,
534 HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING,
535 0, false);
539 private void installLinkHighlighter(Info info) {
540 int startOffset = info.myStartOffset;
541 int endOffset = info.myEndOffset;
542 myHighlighter =
543 myEditor.getMarkupModel().addRangeHighlighter(startOffset, endOffset, HighlighterLayer.SELECTION + 1,
544 ourReferenceAttributes, HighlighterTargetArea.EXACT_RANGE);
545 myHighlighterView = myEditor;