From 595063de62e664b5add435996d1b2306add05321 Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Tue, 3 Jul 2012 15:33:53 +0400 Subject: [PATCH] IDEA-66333 Quick documentation lookup on mouse hover Make it easier to hide 'auto quick doc' control --- .../documentation/DocumentationManager.java | 1847 +++++++++-------- .../QuickDocOnMouseOverManager.java | 535 +++-- 2 files changed, 1192 insertions(+), 1190 deletions(-) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java index 2c990c8661be..fa7dbdd11e07 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java @@ -1,921 +1,926 @@ -/* - * Copyright 2000-2012 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.documentation; - -import com.intellij.codeInsight.CodeInsightBundle; -import com.intellij.codeInsight.TargetElementUtilBase; -import com.intellij.codeInsight.hint.HintManagerImpl; -import com.intellij.codeInsight.hint.ParameterInfoController; -import com.intellij.codeInsight.lookup.Lookup; -import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.codeInsight.lookup.LookupManager; -import com.intellij.ide.BrowserUtil; -import com.intellij.ide.DataManager; -import com.intellij.ide.util.PropertiesComponent; -import com.intellij.ide.util.gotoByName.ChooseByNameBase; -import com.intellij.lang.Language; -import com.intellij.lang.LanguageDocumentation; -import com.intellij.lang.documentation.CompositeDocumentationProvider; -import com.intellij.lang.documentation.DocumentationProvider; -import com.intellij.lang.documentation.ExternalDocumentationHandler; -import com.intellij.lang.documentation.ExternalDocumentationProvider; -import com.intellij.openapi.actionSystem.*; -import com.intellij.openapi.actionSystem.ex.ActionManagerEx; -import com.intellij.openapi.actionSystem.ex.AnActionListener; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.components.ServiceManager; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.IndexNotReadyException; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.roots.OrderEntry; -import com.intellij.openapi.roots.libraries.LibraryUtil; -import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService; -import com.intellij.openapi.ui.popup.JBPopup; -import com.intellij.openapi.ui.popup.JBPopupFactory; -import com.intellij.openapi.util.*; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.wm.IdeFocusManager; -import com.intellij.openapi.wm.ToolWindowId; -import com.intellij.openapi.wm.ex.WindowManagerEx; -import com.intellij.psi.*; -import com.intellij.psi.presentation.java.SymbolPresentationUtil; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.ui.content.Content; -import com.intellij.ui.popup.AbstractPopup; -import com.intellij.ui.popup.NotLookupOrSearchCondition; -import com.intellij.ui.popup.PopupPositionManager; -import com.intellij.ui.popup.PopupUpdateProcessor; -import com.intellij.util.Alarm; -import com.intellij.util.Consumer; -import com.intellij.util.Processor; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.lang.ref.WeakReference; -import java.util.*; -import java.util.List; - -public class DocumentationManager extends DockablePopupManager { - - @NonNls public static final String JAVADOC_LOCATION_AND_SIZE = "javadoc.popup"; - public static final DataKey SELECTED_QUICK_DOC_TEXT = DataKey.create("QUICK_DOC.SELECTED_TEXT"); - - private static final Logger LOG = Logger.getInstance("#" + DocumentationManager.class.getName()); - private static final String SHOW_DOCUMENTATION_IN_TOOL_WINDOW = "ShowDocumentationInToolWindow"; - private static final String DOCUMENTATION_AUTO_UPDATE_ENABLED = "DocumentationAutoUpdateEnabled"; - - private Editor myEditor = null; - private ParameterInfoController myParameterInfoController; - private final Alarm myUpdateDocAlarm; - private WeakReference myDocInfoHintRef; - private Component myPreviouslyFocused = null; - public static final Key ORIGINAL_ELEMENT_KEY = Key.create("Original element"); - @NonNls public static final String PSI_ELEMENT_PROTOCOL = "psi_element://"; - @NonNls private static final String DOC_ELEMENT_PROTOCOL = "doc_element://"; - - private final ActionManagerEx myActionManagerEx; - - private static final int ourFlagsForTargetElements = TargetElementUtilBase.getInstance().getAllAccepted(); - - protected String getToolwindowId() { - return ToolWindowId.DOCUMENTATION; - } - - protected DocumentationComponent createComponent() { - return new DocumentationComponent(this, createActions()); - } - - protected String getRestorePopupDescription() { - return "Restore documentation popup behavior"; - } - - protected String getAutoUpdateDescription() { - return "Show documentation for current element automatically"; - } - - protected String getAutoUpdateTitle() { - return "Auto Show Documentation for Selected Element"; - } - - public static DocumentationManager getInstance(Project project) { - return ServiceManager.getService(project, DocumentationManager.class); - } - - public DocumentationManager(Project project, ActionManagerEx managerEx) { - super(project); - myActionManagerEx = managerEx; - final AnActionListener actionListener = new AnActionListener() { - public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { - final JBPopup hint = getDocInfoHint(); - if (hint != null) { - if (action instanceof HintManagerImpl.ActionToIgnore) return; - if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN)) return; - if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_UP)) return; - if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_PAGE_DOWN)) return; - if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_PAGE_UP)) return; - if (action == ActionManagerEx.getInstanceEx().getAction(IdeActions.ACTION_EDITOR_ESCAPE)) return; - if (ActionPlaces.JAVADOC_INPLACE_SETTINGS.equals(event.getPlace())) return; - hint.cancel(); - } - } - - public void beforeEditorTyping(char c, DataContext dataContext) { - final JBPopup hint = getDocInfoHint(); - if (hint != null) { - hint.cancel(); - } - } - - - public void afterActionPerformed(final AnAction action, final DataContext dataContext, AnActionEvent event) { - } - }; - myActionManagerEx.addAnActionListener(actionListener, project); - myUpdateDocAlarm = new Alarm(Alarm.ThreadToUse.OWN_THREAD,myProject); - } - - public void showJavaDocInfo(@NotNull Editor editor, - @NotNull final PsiElement element, - @NotNull final PsiElement original, - @Nullable Runnable closeCallback) - { - myEditor = editor; - showJavaDocInfo(element, original, closeCallback); - } - - public void showJavaDocInfo(@NotNull final PsiElement element, final PsiElement original) { - showJavaDocInfo(element, original, null); - } - - public void showJavaDocInfo(@NotNull final PsiElement element, final PsiElement original, @Nullable Runnable closeCallback) { - PopupUpdateProcessor updateProcessor = new PopupUpdateProcessor(element.getProject()) { - public void updatePopup(Object lookupItemObject) { - if (lookupItemObject instanceof PsiElement) { - doShowJavaDocInfo((PsiElement)lookupItemObject, false, this, original, false); - } - } - }; - - doShowJavaDocInfo(element, false, updateProcessor, original, false, closeCallback); - } - - public void showJavaDocInfo(final Editor editor, @Nullable final PsiFile file, boolean requestFocus) { - showJavaDocInfo(editor, file, requestFocus, false); - } - - private void showJavaDocInfo(final Editor editor, @Nullable final PsiFile file, boolean requestFocus, final boolean autoupdate) { - myEditor = editor; - final Project project = getProject(file); - PsiDocumentManager.getInstance(project).commitAllDocuments(); - - final PsiElement list = - ParameterInfoController.findArgumentList(file, editor.getCaretModel().getOffset(), -1); - if (list != null) { - myParameterInfoController = ParameterInfoController.findControllerAtOffset(editor, list.getTextRange().getStartOffset()); - } - - final PsiElement originalElement = file != null ? file.findElementAt(editor.getCaretModel().getOffset()) : null; - PsiElement element = findTargetElement(editor, file, originalElement); - assertSameProject(element); - - if (element == null && myParameterInfoController != null) { - final Object[] objects = myParameterInfoController.getSelectedElements(); - - if (objects != null && objects.length > 0) { - if (objects[0] instanceof PsiElement) { - element = (PsiElement)objects[0]; - assertSameProject(element); - } - } - } - - if (element == null && file == null) return; //file == null for text field editor - - if (element == null) { // look if we are within a javadoc comment - element = originalElement; - assertSameProject(element); - if (element == null) return; - - PsiComment comment = PsiTreeUtil.getParentOfType(element, PsiComment.class); - if (comment == null) return; - - element = comment instanceof PsiDocCommentBase ? ((PsiDocCommentBase)comment).getOwner() : comment.getParent(); - if (element == null) return; - //if (!(element instanceof PsiDocCommentOwner)) return null; - } - - storeOriginalElement(project, originalElement, element); - - final PopupUpdateProcessor updateProcessor = new PopupUpdateProcessor(project) { - public void updatePopup(Object lookupIteObject) { - if (lookupIteObject == null) { - return; - } - if (lookupIteObject instanceof PsiElement) { - doShowJavaDocInfo((PsiElement)lookupIteObject, false, this, originalElement, autoupdate); - return; - } - - DocumentationProvider documentationProvider = getProviderFromElement(file); - - PsiElement element = documentationProvider.getDocumentationElementForLookupItem( - PsiManager.getInstance(myProject), - lookupIteObject, - originalElement - ); - - if (element == null) return; - - if (myEditor != null) { - final PsiFile file = element.getContainingFile(); - if (file != null) { - Editor editor = myEditor; - showJavaDocInfo(myEditor, file, false); - myEditor = editor; - } - } - else { - doShowJavaDocInfo(element, false, this, originalElement, autoupdate); - } - } - }; - - doShowJavaDocInfo(element, requestFocus, updateProcessor, originalElement, autoupdate); - } - - private void doShowJavaDocInfo(final PsiElement element, boolean requestFocus, PopupUpdateProcessor updateProcessor, - final PsiElement originalElement, final boolean autoupdate) - { - doShowJavaDocInfo(element, requestFocus, updateProcessor, originalElement, autoupdate, null); - } - - private void doShowJavaDocInfo(final PsiElement element, - boolean requestFocus, - PopupUpdateProcessor updateProcessor, - final PsiElement originalElement, - final boolean autoupdate, - @Nullable final Runnable closeCallback) - { - Project project = getProject(element); - - if (myToolWindow == null && PropertiesComponent.getInstance().isTrueValue(SHOW_DOCUMENTATION_IN_TOOL_WINDOW)) { - createToolWindow(element, originalElement); - return; - } - else if (myToolWindow != null) { - final Content content = myToolWindow.getContentManager().getSelectedContent(); - if (content != null) { - final DocumentationComponent component = (DocumentationComponent)content.getComponent(); - if (component.getElement() != element) { - content.setDisplayName(getTitle(element, true)); - fetchDocInfo(getDefaultCollector(element, originalElement), component, true); - if (!myToolWindow.isVisible()) myToolWindow.show(null); - return; - } - else { - if (element != null && !autoupdate) { - restorePopupBehavior(); - } - else { - return; - } - } - } - } - - final JBPopup _oldHint = getDocInfoHint(); - if (_oldHint != null && _oldHint.isVisible() && _oldHint instanceof AbstractPopup) { - final DocumentationComponent oldComponent = (DocumentationComponent)((AbstractPopup)_oldHint).getComponent(); - fetchDocInfo(getDefaultCollector(element, originalElement), oldComponent); - return; - } - - final DocumentationComponent component = new DocumentationComponent(this); - component.setNavigateCallback(new Consumer() { - @Override - public void consume(PsiElement psiElement) { - final AbstractPopup jbPopup = (AbstractPopup)getDocInfoHint(); - if (jbPopup != null) { - final String title = getTitle(psiElement, false); - jbPopup.setCaption(title); - } - } - }); - Processor pinCallback = new Processor() { - public boolean process(JBPopup popup) { - createToolWindow(element, originalElement); - popup.cancel(); - return false; - } - }; - - final KeyboardShortcut keyboardShortcut = ActionManagerEx.getInstanceEx().getKeyboardShortcut("QuickJavaDoc"); - final List> actions = - Collections.singletonList(Pair.create(new ActionListener() { - public void actionPerformed(ActionEvent e) { - createToolWindow(element, originalElement); - final JBPopup hint = getDocInfoHint(); - if (hint != null && hint.isVisible()) hint.cancel(); - } - }, keyboardShortcut != null ? keyboardShortcut.getFirstKeyStroke() : null)); // Null keyStroke is ok here - - boolean hasLookup = LookupManager.getActiveLookup(myEditor) != null; - final JBPopup hint = JBPopupFactory.getInstance().createComponentPopupBuilder(component, component) - .setRequestFocusCondition(project, NotLookupOrSearchCondition.INSTANCE) - .setProject(project) - .addListener(updateProcessor) - .addUserData(updateProcessor) - .setKeyboardActions(actions) - .setDimensionServiceKey(myProject, JAVADOC_LOCATION_AND_SIZE, false) - .setResizable(true) - .setMovable(true) - .setRequestFocus(requestFocus) - .setCancelOnClickOutside(!hasLookup) // otherwise selecting lookup items by mouse would close the doc - .setTitle(getTitle(element, false)) - .setCouldPin(pinCallback) - .setCancelCallback(new Computable() { - public Boolean compute() { - if (closeCallback != null) { - closeCallback.run(); - } - if (fromQuickSearch()) { - ((ChooseByNameBase.JPanelProvider)myPreviouslyFocused.getParent()).unregisterHint(); - } - - Disposer.dispose(component); - myEditor = null; - myPreviouslyFocused = null; - myParameterInfoController = null; - return Boolean.TRUE; - } - }) - .createPopup(); - - - AbstractPopup oldHint = (AbstractPopup)getDocInfoHint(); - if (oldHint != null) { - DocumentationComponent oldComponent = (DocumentationComponent)oldHint.getComponent(); - PsiElement element1 = oldComponent.getElement(); - if (Comparing.equal(element, element1)) { - if (requestFocus) { - component.getComponent().requestFocus(); - } - return; - } - oldHint.cancel(); - } - - component.setHint(hint); - - if (myEditor == null) { - // subsequent invocation of javadoc popup from completion will have myEditor == null because of cancel invoked, - // so reevaluate the editor for proper popup placement - Lookup lookup = LookupManager.getInstance(myProject).getActiveLookup(); - myEditor = lookup != null ? lookup.getEditor() : null; - } - fetchDocInfo(getDefaultCollector(element, originalElement), component); - - myDocInfoHintRef = new WeakReference(hint); - myPreviouslyFocused = WindowManagerEx.getInstanceEx().getFocusedComponent(project); - - if (fromQuickSearch()) { - ((ChooseByNameBase.JPanelProvider)myPreviouslyFocused.getParent()).registerHint(hint); - } - } - - private static String getTitle(@NotNull final PsiElement element, final boolean _short) { - final String title = SymbolPresentationUtil.getSymbolPresentableText(element); - return _short ? title != null ? title : element.getText() : CodeInsightBundle.message("javadoc.info.title", title != null ? title : element.getText()); - } - - public static void storeOriginalElement(final Project project, final PsiElement originalElement, final PsiElement element) { - if (element == null) return; - try { - element.putUserData( - ORIGINAL_ELEMENT_KEY, - SmartPointerManager.getInstance(project).createSmartPsiElementPointer(originalElement) - ); - } catch (RuntimeException ex) { - // PsiPackage does not allow putUserData - } - } - - @Nullable - public PsiElement findTargetElement(final Editor editor, @Nullable final PsiFile file, PsiElement contextElement) { - return findTargetElement(editor, editor.getCaretModel().getOffset(), file, contextElement); - } - - @Nullable - public PsiElement findTargetElement(final Editor editor, int offset, @Nullable final PsiFile file, PsiElement contextElement) { - TargetElementUtilBase util = TargetElementUtilBase.getInstance(); - PsiElement element = editor != null ? util.findTargetElement(editor, ourFlagsForTargetElements, offset) - : null; - assertSameProject(element); - - // Allow context doc over xml tag content - if (element != null || contextElement != null) { - final PsiElement adjusted = util.adjustElement(editor, ourFlagsForTargetElements, element, contextElement); - if (adjusted != null) { - element = adjusted; - assertSameProject(element); - } - } - - if (element == null && editor != null) { - element = getElementFromLookup(editor, file); - assertSameProject(element); - - if (element == null) { - final PsiReference ref = util.findReference(editor, offset); - - if (ref != null) { - element = util.adjustReference(ref); - assertSameProject(element); - if (element == null && ref instanceof PsiPolyVariantReference) { - element = ref.getElement(); - assertSameProject(element); - } - } - } - } - - storeOriginalElement(myProject, contextElement, element); - - return element; - } - - @Nullable - public PsiElement getElementFromLookup(final Editor editor, @Nullable final PsiFile file) { - - final Lookup activeLookup = LookupManager.getInstance(myProject).getActiveLookup(); - - if (activeLookup != null) { - LookupElement item = activeLookup.getCurrentItem(); - if (item != null) { - - - int offset = editor.getCaretModel().getOffset(); - if (offset > 0 && offset == editor.getDocument().getTextLength()) offset--; - final PsiElement contextElement = file == null? null : file.findElementAt(offset); - final PsiReference ref = TargetElementUtilBase.findReference(editor, offset); - - final DocumentationProvider documentationProvider = getProviderFromElement(file); - - return documentationProvider.getDocumentationElementForLookupItem( - PsiManager.getInstance(myProject), - item.getObject(), - ref != null ? ref.getElement():contextElement - ); - } - } - return null; - } - - private boolean fromQuickSearch() { - return myPreviouslyFocused != null && myPreviouslyFocused.getParent() instanceof ChooseByNameBase.JPanelProvider; - } - - private DocumentationCollector getDefaultCollector(@NotNull final PsiElement element, @Nullable final PsiElement originalElement) { - return new DocumentationCollector() { - - @Nullable - public String getDocumentation() throws Exception { - final DocumentationProvider provider = ApplicationManager.getApplication().runReadAction( - new Computable() { - public DocumentationProvider compute() { - return getProviderFromElement(element, originalElement); - } - } - ); - if (myParameterInfoController != null) { - final String doc = ApplicationManager.getApplication().runReadAction( - new Computable() { - public String compute() { - return generateParameterInfoDocumentation(provider); - } - } - ); - if (doc != null) return doc; - } - if (provider instanceof ExternalDocumentationProvider) { - final List urls = ApplicationManager.getApplication().runReadAction( - new NullableComputable>() { - public List compute() { - final SmartPsiElementPointer originalElementPtr = element.getUserData(ORIGINAL_ELEMENT_KEY); - final PsiElement originalElement = originalElementPtr != null ? originalElementPtr.getElement() : null; - if (((ExternalDocumentationProvider)provider).hasDocumentationFor(element, originalElement)) { - return provider.getUrlFor(element, originalElement); - } - return null; - } - } - ); - if (urls != null) { - final String doc = ((ExternalDocumentationProvider)provider).fetchExternalDocumentation(myProject, element, urls); - if (doc != null) return doc; - } - } - return ApplicationManager.getApplication().runReadAction( - new Computable() { - @Nullable - public String compute() { - final SmartPsiElementPointer originalElement = element.getUserData(ORIGINAL_ELEMENT_KEY); - return provider.generateDoc(element, originalElement != null ? originalElement.getElement() : null); - } - } - ); - } - - private String generateParameterInfoDocumentation(DocumentationProvider provider) { - final Object[] objects = myParameterInfoController.getSelectedElements(); - - if (objects.length > 0) { - @NonNls StringBuffer sb = null; - - for (Object o : objects) { - PsiElement parameter = null; - if (o instanceof PsiElement) { - parameter = (PsiElement)o; - } - - if (parameter != null) { - final SmartPsiElementPointer originalElement = parameter.getUserData(ORIGINAL_ELEMENT_KEY); - final String str2 = provider.generateDoc(parameter, originalElement != null ? originalElement.getElement() : null); - if (str2 == null) continue; - if (sb == null) sb = new StringBuffer(); - sb.append(str2); - sb.append("
"); - } - else { - sb = null; - break; - } - } - - if (sb != null) return sb.toString(); - } - return null; - } - - @Nullable - public PsiElement getElement() { - return element.isValid() ? element : null; - } - }; - } - - @Nullable - public JBPopup getDocInfoHint() { - if (myDocInfoHintRef == null) return null; - JBPopup hint = myDocInfoHintRef.get(); - if (hint == null || !hint.isVisible()) { - myDocInfoHintRef = null; - return null; - } - return hint; - } - - public void fetchDocInfo(final DocumentationCollector provider, final DocumentationComponent component) { - doFetchDocInfo(component, provider, true, false); - } - - public void fetchDocInfo(final DocumentationCollector provider, final DocumentationComponent component, final boolean clearHistory) { - doFetchDocInfo(component, provider, true, clearHistory); - } - - public void fetchDocInfo(final PsiElement element, final DocumentationComponent component) { - doFetchDocInfo(component, getDefaultCollector(element, null), true, false); - } - - public ActionCallback queueFetchDocInfo(final DocumentationCollector provider, final DocumentationComponent component, final boolean clearHistory) { - return doFetchDocInfo(component, provider, false, clearHistory); - } - - public ActionCallback queueFetchDocInfo(final PsiElement element, final DocumentationComponent component) { - return queueFetchDocInfo(getDefaultCollector(element, null), component, false); - } - - private ActionCallback doFetchDocInfo(final DocumentationComponent component, final DocumentationCollector provider, final boolean cancelRequests, final boolean clearHistory) { - final ActionCallback callback = new ActionCallback(); - component.startWait(); - if (cancelRequests) { - myUpdateDocAlarm.cancelAllRequests(); - } - if (component.isEmpty()) { - component.setText(CodeInsightBundle.message("javadoc.fetching.progress"), null, clearHistory); - } - - myUpdateDocAlarm.addRequest(new Runnable() { - public void run() { - if (myProject.isDisposed()) return; - final Throwable[] ex = new Throwable[1]; - String text = null; - try { - text = provider.getDocumentation(); - } - catch (Throwable e) { - LOG.info(e); - ex[0] = e; - } - - if (ex[0] != null) { - //noinspection SSBasedInspection - SwingUtilities.invokeLater(new Runnable() { - public void run() { - String message = ex[0] instanceof IndexNotReadyException - ? "Documentation is not available until indices are built." - : CodeInsightBundle.message("javadoc.external.fetch.error.message", ex[0].getLocalizedMessage()); - component.setText(message, null, true); - callback.setDone(); - } - }); - return; - } - - final PsiElement element = ApplicationManager.getApplication().runReadAction(new Computable() { - @Nullable - public PsiElement compute() { - return provider.getElement(); - } - }); - if (element == null) { - return; - } - final String documentationText = text; - //noinspection SSBasedInspection - SwingUtilities.invokeLater(new Runnable() { - public void run() { - PsiDocumentManager.getInstance(myProject).commitAllDocuments(); - - if (!element.isValid()) { - callback.setDone(); - return; - } - - if (documentationText == null) { - component.setText(CodeInsightBundle.message("no.documentation.found"), element, true); - } - else if (documentationText.length() == 0) { - component.setText(component.getText(), element, true, clearHistory); - } - else { - component.setData(element, documentationText, clearHistory); - } - - final AbstractPopup jbPopup = (AbstractPopup)getDocInfoHint(); - if(jbPopup==null){ - callback.setDone(); - return; - } - jbPopup.setCaption(getTitle(element, false)); - callback.setDone(); - } - }); - } - }, 10); - return callback; - } - - @NotNull - public static DocumentationProvider getProviderFromElement(final PsiElement element) { - return getProviderFromElement(element, null); - } - - @NotNull - public static DocumentationProvider getProviderFromElement(@Nullable PsiElement element, @Nullable PsiElement originalElement) { - if (element != null && !element.isValid()) { - element = null; - } - if (originalElement != null && !originalElement.isValid()) { - originalElement = null; - } - - if (originalElement == null) { - originalElement = getOriginalElement(element); - } - - PsiFile containingFile = - originalElement != null ? originalElement.getContainingFile() : element != null ? element.getContainingFile() : null; - Set result = new LinkedHashSet(); - - final Language containingFileLanguage = containingFile != null ? containingFile.getLanguage() : null; - DocumentationProvider originalProvider = - containingFile != null ? LanguageDocumentation.INSTANCE.forLanguage(containingFileLanguage) : null; - - final Language elementLanguage = element != null ? element.getLanguage() : null; - DocumentationProvider elementProvider = - element == null || elementLanguage.is(containingFileLanguage) ? null : LanguageDocumentation.INSTANCE.forLanguage(elementLanguage); - - result.add(elementProvider); - result.add(originalProvider); - - if (containingFile != null) { - final Language baseLanguage = containingFile.getViewProvider().getBaseLanguage(); - if (!baseLanguage.is(containingFileLanguage)) { - result.add(LanguageDocumentation.INSTANCE.forLanguage(baseLanguage)); - } - } - else if (element instanceof PsiDirectory) { - final Set langs = new HashSet(); - - for (PsiFile file : ((PsiDirectory)element).getFiles()) { - final Language baseLanguage = file.getViewProvider().getBaseLanguage(); - if (!langs.contains(baseLanguage)) { - langs.add(baseLanguage); - result.add(LanguageDocumentation.INSTANCE.forLanguage(baseLanguage)); - } - } - } - return CompositeDocumentationProvider.wrapProviders(result); - } - - @Nullable - public static PsiElement getOriginalElement(final PsiElement element) { - SmartPsiElementPointer originalElementPointer = element!=null ? element.getUserData(ORIGINAL_ELEMENT_KEY):null; - return originalElementPointer != null ? originalElementPointer.getElement() : null; - } - - void navigateByLink(final DocumentationComponent component, final String url) { - component.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - final PsiElement psiElement = component.getElement(); - final PsiManager manager = PsiManager.getInstance(getProject(psiElement)); - if (url.startsWith("open")) { - final PsiFile containingFile = psiElement.getContainingFile(); - if (containingFile != null) { - final VirtualFile virtualFile = containingFile.getVirtualFile(); - final OrderEntry libraryEntry = LibraryUtil.findLibraryEntry(virtualFile, myProject); - if (libraryEntry != null) { - ProjectSettingsService.getInstance(myProject).openLibraryOrSdkSettings(libraryEntry); - } - } - } else if (url.startsWith(PSI_ELEMENT_PROTOCOL)) { - final String refText = url.substring(PSI_ELEMENT_PROTOCOL.length()); - DocumentationProvider provider = getProviderFromElement(psiElement); - final PsiElement targetElement = provider.getDocumentationElementForLink(manager, refText, psiElement); - if (targetElement != null) { - fetchDocInfo(getDefaultCollector(targetElement, null), component); - } - } - else { - final DocumentationProvider provider = getProviderFromElement(psiElement); - boolean processed = false; - if (provider instanceof CompositeDocumentationProvider) { - for (DocumentationProvider documentationProvider : ((CompositeDocumentationProvider)provider).getProviders()) { - if (documentationProvider instanceof ExternalDocumentationHandler) { - final ExternalDocumentationHandler externalDocumentationHandler = (ExternalDocumentationHandler)documentationProvider; - if (externalDocumentationHandler.canFetchDocumentationLink(url)) { - fetchDocInfo(new DocumentationCollector() { - @Override - public String getDocumentation() throws Exception { - return externalDocumentationHandler.fetchExternalDocumentation(url, myProject); - } - - @Override - public PsiElement getElement() { - return psiElement; - } - }, component); - processed = true; - } - else if (externalDocumentationHandler.handleExternalLink(manager, url, psiElement)) { - processed = true; - break; - } - } - } - } - - if (!processed) { - final String docUrl = url; - - fetchDocInfo - (new DocumentationCollector() { - public String getDocumentation() throws Exception { - if (docUrl.startsWith(DOC_ELEMENT_PROTOCOL)) { - final List urls = ApplicationManager.getApplication().runReadAction( - new Computable>() { - public List compute() { - final DocumentationProvider provider = getProviderFromElement(psiElement); - return provider.getUrlFor(psiElement, getOriginalElement(psiElement)); - } - } - ); - BrowserUtil.launchBrowser(urls != null && !urls.isEmpty() ? urls.get(0) : docUrl); - } - else { - BrowserUtil.launchBrowser(docUrl); - } - return ""; - } - - public PsiElement getElement() { - //String loc = getElementLocator(docUrl); - // - //if (loc != null) { - // PsiElement context = component.getElement(); - // return JavaDocUtil.findReferenceTarget(context.getManager(), loc, context); - //} - - return psiElement; - } - }, component); - } - } - - component.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - } - - void showHint(final JBPopup hint) { - final Component focusOwner = IdeFocusManager.getInstance(myProject).getFocusOwner(); - DataContext dataContext = DataManager.getInstance().getDataContext(focusOwner); - PopupPositionManager.positionPopupInBestPosition(hint, myEditor, dataContext); - } - - public void requestFocus() { - if (fromQuickSearch()) { - myPreviouslyFocused.getParent().requestFocus(); - } - } - - public Project getProject(@Nullable final PsiElement element) { - assertSameProject(element); - return myProject; - } - - private void assertSameProject(@Nullable PsiElement element) { - if (element != null && element.isValid() && myProject != element.getProject()) { - throw new AssertionError(myProject + "!=" + element.getProject() + "; element=" + element); - } - } - - @SuppressWarnings({"HardCodedStringLiteral"}) - public static void createHyperlink(StringBuilder buffer, String refText,String label,boolean plainLink) { - buffer.append(""); - if (!plainLink) { - buffer.append(""); - } - buffer.append(label); - if (!plainLink) { - buffer.append(""); - } - buffer.append(""); - } - - @Override - public String getShowInToolWindowProperty() { - return SHOW_DOCUMENTATION_IN_TOOL_WINDOW; - } - - @Override - public String getAutoUpdateEnabledProperty() { - return DOCUMENTATION_AUTO_UPDATE_ENABLED; - } - - protected void doUpdateComponent(PsiElement element, PsiElement originalElement, DocumentationComponent component) { - fetchDocInfo(getDefaultCollector(element, originalElement), component); - } - - protected void doUpdateComponent(Editor editor, PsiFile psiFile) { - showJavaDocInfo(editor, psiFile, false, true); - } - - @Override - protected void doUpdateComponent(@NotNull PsiElement element) { - showJavaDocInfo(element, element); - } - - @Override - protected String getTitle(PsiElement element) { - return getTitle(element, true); - } - - private interface DocumentationCollector { - @Nullable - String getDocumentation() throws Exception; - @Nullable - PsiElement getElement(); - } -} +/* + * Copyright 2000-2012 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.documentation; + +import com.intellij.codeInsight.CodeInsightBundle; +import com.intellij.codeInsight.TargetElementUtilBase; +import com.intellij.codeInsight.hint.HintManagerImpl; +import com.intellij.codeInsight.hint.ParameterInfoController; +import com.intellij.codeInsight.lookup.Lookup; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupManager; +import com.intellij.ide.BrowserUtil; +import com.intellij.ide.DataManager; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.ide.util.gotoByName.ChooseByNameBase; +import com.intellij.lang.Language; +import com.intellij.lang.LanguageDocumentation; +import com.intellij.lang.documentation.CompositeDocumentationProvider; +import com.intellij.lang.documentation.DocumentationProvider; +import com.intellij.lang.documentation.ExternalDocumentationHandler; +import com.intellij.lang.documentation.ExternalDocumentationProvider; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.actionSystem.ex.AnActionListener; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.IndexNotReadyException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.OrderEntry; +import com.intellij.openapi.roots.libraries.LibraryUtil; +import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService; +import com.intellij.openapi.ui.popup.JBPopup; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.util.*; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.IdeFocusManager; +import com.intellij.openapi.wm.ToolWindowId; +import com.intellij.openapi.wm.ex.WindowManagerEx; +import com.intellij.psi.*; +import com.intellij.psi.presentation.java.SymbolPresentationUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.ui.content.Content; +import com.intellij.ui.popup.AbstractPopup; +import com.intellij.ui.popup.NotLookupOrSearchCondition; +import com.intellij.ui.popup.PopupPositionManager; +import com.intellij.ui.popup.PopupUpdateProcessor; +import com.intellij.util.Alarm; +import com.intellij.util.Consumer; +import com.intellij.util.Processor; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.ref.WeakReference; +import java.util.*; +import java.util.List; + +public class DocumentationManager extends DockablePopupManager { + + @NonNls public static final String JAVADOC_LOCATION_AND_SIZE = "javadoc.popup"; + public static final DataKey SELECTED_QUICK_DOC_TEXT = DataKey.create("QUICK_DOC.SELECTED_TEXT"); + + private static final Logger LOG = Logger.getInstance("#" + DocumentationManager.class.getName()); + private static final String SHOW_DOCUMENTATION_IN_TOOL_WINDOW = "ShowDocumentationInToolWindow"; + private static final String DOCUMENTATION_AUTO_UPDATE_ENABLED = "DocumentationAutoUpdateEnabled"; + + private Editor myEditor = null; + private ParameterInfoController myParameterInfoController; + private final Alarm myUpdateDocAlarm; + private WeakReference myDocInfoHintRef; + private Component myPreviouslyFocused = null; + public static final Key ORIGINAL_ELEMENT_KEY = Key.create("Original element"); + @NonNls public static final String PSI_ELEMENT_PROTOCOL = "psi_element://"; + @NonNls private static final String DOC_ELEMENT_PROTOCOL = "doc_element://"; + + private final ActionManagerEx myActionManagerEx; + + private static final int ourFlagsForTargetElements = TargetElementUtilBase.getInstance().getAllAccepted(); + + protected String getToolwindowId() { + return ToolWindowId.DOCUMENTATION; + } + + protected DocumentationComponent createComponent() { + return new DocumentationComponent(this, createActions()); + } + + protected String getRestorePopupDescription() { + return "Restore documentation popup behavior"; + } + + protected String getAutoUpdateDescription() { + return "Show documentation for current element automatically"; + } + + protected String getAutoUpdateTitle() { + return "Auto Show Documentation for Selected Element"; + } + + public static DocumentationManager getInstance(Project project) { + return ServiceManager.getService(project, DocumentationManager.class); + } + + public DocumentationManager(final Project project, ActionManagerEx managerEx) { + super(project); + myActionManagerEx = managerEx; + final AnActionListener actionListener = new AnActionListener() { + public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { + final JBPopup hint = getDocInfoHint(); + if (hint != null) { + if (action instanceof HintManagerImpl.ActionToIgnore) return; + if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN)) return; + if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_UP)) return; + if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_PAGE_DOWN)) return; + if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_PAGE_UP)) return; + if (action == ActionManagerEx.getInstanceEx().getAction(IdeActions.ACTION_EDITOR_ESCAPE)) return; + if (ActionPlaces.JAVADOC_INPLACE_SETTINGS.equals(event.getPlace())) return; + Component toFocus = myPreviouslyFocused; + hint.cancel(); + if (toFocus != null) { + IdeFocusManager.getInstance(project).requestFocus(toFocus, true); + } + } + } + + public void beforeEditorTyping(char c, DataContext dataContext) { + final JBPopup hint = getDocInfoHint(); + if (hint != null) { + hint.cancel(); + } + } + + + public void afterActionPerformed(final AnAction action, final DataContext dataContext, AnActionEvent event) { + } + }; + myActionManagerEx.addAnActionListener(actionListener, project); + myUpdateDocAlarm = new Alarm(Alarm.ThreadToUse.OWN_THREAD,myProject); + } + + public void showJavaDocInfo(@NotNull Editor editor, + @NotNull final PsiElement element, + @NotNull final PsiElement original, + @Nullable Runnable closeCallback) + { + myEditor = editor; + showJavaDocInfo(element, original, closeCallback); + } + + public void showJavaDocInfo(@NotNull final PsiElement element, final PsiElement original) { + showJavaDocInfo(element, original, null); + } + + public void showJavaDocInfo(@NotNull final PsiElement element, final PsiElement original, @Nullable Runnable closeCallback) { + PopupUpdateProcessor updateProcessor = new PopupUpdateProcessor(element.getProject()) { + public void updatePopup(Object lookupItemObject) { + if (lookupItemObject instanceof PsiElement) { + doShowJavaDocInfo((PsiElement)lookupItemObject, false, this, original, false); + } + } + }; + + doShowJavaDocInfo(element, false, updateProcessor, original, false, closeCallback); + } + + public void showJavaDocInfo(final Editor editor, @Nullable final PsiFile file, boolean requestFocus) { + showJavaDocInfo(editor, file, requestFocus, false); + } + + private void showJavaDocInfo(final Editor editor, @Nullable final PsiFile file, boolean requestFocus, final boolean autoupdate) { + myEditor = editor; + final Project project = getProject(file); + PsiDocumentManager.getInstance(project).commitAllDocuments(); + + final PsiElement list = + ParameterInfoController.findArgumentList(file, editor.getCaretModel().getOffset(), -1); + if (list != null) { + myParameterInfoController = ParameterInfoController.findControllerAtOffset(editor, list.getTextRange().getStartOffset()); + } + + final PsiElement originalElement = file != null ? file.findElementAt(editor.getCaretModel().getOffset()) : null; + PsiElement element = findTargetElement(editor, file, originalElement); + assertSameProject(element); + + if (element == null && myParameterInfoController != null) { + final Object[] objects = myParameterInfoController.getSelectedElements(); + + if (objects != null && objects.length > 0) { + if (objects[0] instanceof PsiElement) { + element = (PsiElement)objects[0]; + assertSameProject(element); + } + } + } + + if (element == null && file == null) return; //file == null for text field editor + + if (element == null) { // look if we are within a javadoc comment + element = originalElement; + assertSameProject(element); + if (element == null) return; + + PsiComment comment = PsiTreeUtil.getParentOfType(element, PsiComment.class); + if (comment == null) return; + + element = comment instanceof PsiDocCommentBase ? ((PsiDocCommentBase)comment).getOwner() : comment.getParent(); + if (element == null) return; + //if (!(element instanceof PsiDocCommentOwner)) return null; + } + + storeOriginalElement(project, originalElement, element); + + final PopupUpdateProcessor updateProcessor = new PopupUpdateProcessor(project) { + public void updatePopup(Object lookupIteObject) { + if (lookupIteObject == null) { + return; + } + if (lookupIteObject instanceof PsiElement) { + doShowJavaDocInfo((PsiElement)lookupIteObject, false, this, originalElement, autoupdate); + return; + } + + DocumentationProvider documentationProvider = getProviderFromElement(file); + + PsiElement element = documentationProvider.getDocumentationElementForLookupItem( + PsiManager.getInstance(myProject), + lookupIteObject, + originalElement + ); + + if (element == null) return; + + if (myEditor != null) { + final PsiFile file = element.getContainingFile(); + if (file != null) { + Editor editor = myEditor; + showJavaDocInfo(myEditor, file, false); + myEditor = editor; + } + } + else { + doShowJavaDocInfo(element, false, this, originalElement, autoupdate); + } + } + }; + + doShowJavaDocInfo(element, requestFocus, updateProcessor, originalElement, autoupdate); + } + + private void doShowJavaDocInfo(final PsiElement element, boolean requestFocus, PopupUpdateProcessor updateProcessor, + final PsiElement originalElement, final boolean autoupdate) + { + doShowJavaDocInfo(element, requestFocus, updateProcessor, originalElement, autoupdate, null); + } + + private void doShowJavaDocInfo(final PsiElement element, + boolean requestFocus, + PopupUpdateProcessor updateProcessor, + final PsiElement originalElement, + final boolean autoupdate, + @Nullable final Runnable closeCallback) + { + Project project = getProject(element); + + if (myToolWindow == null && PropertiesComponent.getInstance().isTrueValue(SHOW_DOCUMENTATION_IN_TOOL_WINDOW)) { + createToolWindow(element, originalElement); + return; + } + else if (myToolWindow != null) { + final Content content = myToolWindow.getContentManager().getSelectedContent(); + if (content != null) { + final DocumentationComponent component = (DocumentationComponent)content.getComponent(); + if (component.getElement() != element) { + content.setDisplayName(getTitle(element, true)); + fetchDocInfo(getDefaultCollector(element, originalElement), component, true); + if (!myToolWindow.isVisible()) myToolWindow.show(null); + return; + } + else { + if (element != null && !autoupdate) { + restorePopupBehavior(); + } + else { + return; + } + } + } + } + + final JBPopup _oldHint = getDocInfoHint(); + if (_oldHint != null && _oldHint.isVisible() && _oldHint instanceof AbstractPopup) { + final DocumentationComponent oldComponent = (DocumentationComponent)((AbstractPopup)_oldHint).getComponent(); + fetchDocInfo(getDefaultCollector(element, originalElement), oldComponent); + return; + } + + final DocumentationComponent component = new DocumentationComponent(this); + component.setNavigateCallback(new Consumer() { + @Override + public void consume(PsiElement psiElement) { + final AbstractPopup jbPopup = (AbstractPopup)getDocInfoHint(); + if (jbPopup != null) { + final String title = getTitle(psiElement, false); + jbPopup.setCaption(title); + } + } + }); + Processor pinCallback = new Processor() { + public boolean process(JBPopup popup) { + createToolWindow(element, originalElement); + popup.cancel(); + return false; + } + }; + + final KeyboardShortcut keyboardShortcut = ActionManagerEx.getInstanceEx().getKeyboardShortcut("QuickJavaDoc"); + final List> actions = + Collections.singletonList(Pair.create(new ActionListener() { + public void actionPerformed(ActionEvent e) { + createToolWindow(element, originalElement); + final JBPopup hint = getDocInfoHint(); + if (hint != null && hint.isVisible()) hint.cancel(); + } + }, keyboardShortcut != null ? keyboardShortcut.getFirstKeyStroke() : null)); // Null keyStroke is ok here + + boolean hasLookup = LookupManager.getActiveLookup(myEditor) != null; + final JBPopup hint = JBPopupFactory.getInstance().createComponentPopupBuilder(component, component) + .setRequestFocusCondition(project, NotLookupOrSearchCondition.INSTANCE) + .setProject(project) + .addListener(updateProcessor) + .addUserData(updateProcessor) + .setKeyboardActions(actions) + .setDimensionServiceKey(myProject, JAVADOC_LOCATION_AND_SIZE, false) + .setResizable(true) + .setMovable(true) + .setRequestFocus(requestFocus) + .setCancelOnClickOutside(!hasLookup) // otherwise selecting lookup items by mouse would close the doc + .setTitle(getTitle(element, false)) + .setCouldPin(pinCallback) + .setModalContext(false) + .setCancelCallback(new Computable() { + public Boolean compute() { + if (closeCallback != null) { + closeCallback.run(); + } + if (fromQuickSearch()) { + ((ChooseByNameBase.JPanelProvider)myPreviouslyFocused.getParent()).unregisterHint(); + } + + Disposer.dispose(component); + myEditor = null; + myPreviouslyFocused = null; + myParameterInfoController = null; + return Boolean.TRUE; + } + }) + .createPopup(); + + + AbstractPopup oldHint = (AbstractPopup)getDocInfoHint(); + if (oldHint != null) { + DocumentationComponent oldComponent = (DocumentationComponent)oldHint.getComponent(); + PsiElement element1 = oldComponent.getElement(); + if (Comparing.equal(element, element1)) { + if (requestFocus) { + component.getComponent().requestFocus(); + } + return; + } + oldHint.cancel(); + } + + component.setHint(hint); + + if (myEditor == null) { + // subsequent invocation of javadoc popup from completion will have myEditor == null because of cancel invoked, + // so reevaluate the editor for proper popup placement + Lookup lookup = LookupManager.getInstance(myProject).getActiveLookup(); + myEditor = lookup != null ? lookup.getEditor() : null; + } + fetchDocInfo(getDefaultCollector(element, originalElement), component); + + myDocInfoHintRef = new WeakReference(hint); + myPreviouslyFocused = WindowManagerEx.getInstanceEx().getFocusedComponent(project); + + if (fromQuickSearch()) { + ((ChooseByNameBase.JPanelProvider)myPreviouslyFocused.getParent()).registerHint(hint); + } + } + + private static String getTitle(@NotNull final PsiElement element, final boolean _short) { + final String title = SymbolPresentationUtil.getSymbolPresentableText(element); + return _short ? title != null ? title : element.getText() : CodeInsightBundle.message("javadoc.info.title", title != null ? title : element.getText()); + } + + public static void storeOriginalElement(final Project project, final PsiElement originalElement, final PsiElement element) { + if (element == null) return; + try { + element.putUserData( + ORIGINAL_ELEMENT_KEY, + SmartPointerManager.getInstance(project).createSmartPsiElementPointer(originalElement) + ); + } catch (RuntimeException ex) { + // PsiPackage does not allow putUserData + } + } + + @Nullable + public PsiElement findTargetElement(final Editor editor, @Nullable final PsiFile file, PsiElement contextElement) { + return findTargetElement(editor, editor.getCaretModel().getOffset(), file, contextElement); + } + + @Nullable + public PsiElement findTargetElement(final Editor editor, int offset, @Nullable final PsiFile file, PsiElement contextElement) { + TargetElementUtilBase util = TargetElementUtilBase.getInstance(); + PsiElement element = editor != null ? util.findTargetElement(editor, ourFlagsForTargetElements, offset) + : null; + assertSameProject(element); + + // Allow context doc over xml tag content + if (element != null || contextElement != null) { + final PsiElement adjusted = util.adjustElement(editor, ourFlagsForTargetElements, element, contextElement); + if (adjusted != null) { + element = adjusted; + assertSameProject(element); + } + } + + if (element == null && editor != null) { + element = getElementFromLookup(editor, file); + assertSameProject(element); + + if (element == null) { + final PsiReference ref = util.findReference(editor, offset); + + if (ref != null) { + element = util.adjustReference(ref); + assertSameProject(element); + if (element == null && ref instanceof PsiPolyVariantReference) { + element = ref.getElement(); + assertSameProject(element); + } + } + } + } + + storeOriginalElement(myProject, contextElement, element); + + return element; + } + + @Nullable + public PsiElement getElementFromLookup(final Editor editor, @Nullable final PsiFile file) { + + final Lookup activeLookup = LookupManager.getInstance(myProject).getActiveLookup(); + + if (activeLookup != null) { + LookupElement item = activeLookup.getCurrentItem(); + if (item != null) { + + + int offset = editor.getCaretModel().getOffset(); + if (offset > 0 && offset == editor.getDocument().getTextLength()) offset--; + final PsiElement contextElement = file == null? null : file.findElementAt(offset); + final PsiReference ref = TargetElementUtilBase.findReference(editor, offset); + + final DocumentationProvider documentationProvider = getProviderFromElement(file); + + return documentationProvider.getDocumentationElementForLookupItem( + PsiManager.getInstance(myProject), + item.getObject(), + ref != null ? ref.getElement():contextElement + ); + } + } + return null; + } + + private boolean fromQuickSearch() { + return myPreviouslyFocused != null && myPreviouslyFocused.getParent() instanceof ChooseByNameBase.JPanelProvider; + } + + private DocumentationCollector getDefaultCollector(@NotNull final PsiElement element, @Nullable final PsiElement originalElement) { + return new DocumentationCollector() { + + @Nullable + public String getDocumentation() throws Exception { + final DocumentationProvider provider = ApplicationManager.getApplication().runReadAction( + new Computable() { + public DocumentationProvider compute() { + return getProviderFromElement(element, originalElement); + } + } + ); + if (myParameterInfoController != null) { + final String doc = ApplicationManager.getApplication().runReadAction( + new Computable() { + public String compute() { + return generateParameterInfoDocumentation(provider); + } + } + ); + if (doc != null) return doc; + } + if (provider instanceof ExternalDocumentationProvider) { + final List urls = ApplicationManager.getApplication().runReadAction( + new NullableComputable>() { + public List compute() { + final SmartPsiElementPointer originalElementPtr = element.getUserData(ORIGINAL_ELEMENT_KEY); + final PsiElement originalElement = originalElementPtr != null ? originalElementPtr.getElement() : null; + if (((ExternalDocumentationProvider)provider).hasDocumentationFor(element, originalElement)) { + return provider.getUrlFor(element, originalElement); + } + return null; + } + } + ); + if (urls != null) { + final String doc = ((ExternalDocumentationProvider)provider).fetchExternalDocumentation(myProject, element, urls); + if (doc != null) return doc; + } + } + return ApplicationManager.getApplication().runReadAction( + new Computable() { + @Nullable + public String compute() { + final SmartPsiElementPointer originalElement = element.getUserData(ORIGINAL_ELEMENT_KEY); + return provider.generateDoc(element, originalElement != null ? originalElement.getElement() : null); + } + } + ); + } + + private String generateParameterInfoDocumentation(DocumentationProvider provider) { + final Object[] objects = myParameterInfoController.getSelectedElements(); + + if (objects.length > 0) { + @NonNls StringBuffer sb = null; + + for (Object o : objects) { + PsiElement parameter = null; + if (o instanceof PsiElement) { + parameter = (PsiElement)o; + } + + if (parameter != null) { + final SmartPsiElementPointer originalElement = parameter.getUserData(ORIGINAL_ELEMENT_KEY); + final String str2 = provider.generateDoc(parameter, originalElement != null ? originalElement.getElement() : null); + if (str2 == null) continue; + if (sb == null) sb = new StringBuffer(); + sb.append(str2); + sb.append("
"); + } + else { + sb = null; + break; + } + } + + if (sb != null) return sb.toString(); + } + return null; + } + + @Nullable + public PsiElement getElement() { + return element.isValid() ? element : null; + } + }; + } + + @Nullable + public JBPopup getDocInfoHint() { + if (myDocInfoHintRef == null) return null; + JBPopup hint = myDocInfoHintRef.get(); + if (hint == null || !hint.isVisible()) { + myDocInfoHintRef = null; + return null; + } + return hint; + } + + public void fetchDocInfo(final DocumentationCollector provider, final DocumentationComponent component) { + doFetchDocInfo(component, provider, true, false); + } + + public void fetchDocInfo(final DocumentationCollector provider, final DocumentationComponent component, final boolean clearHistory) { + doFetchDocInfo(component, provider, true, clearHistory); + } + + public void fetchDocInfo(final PsiElement element, final DocumentationComponent component) { + doFetchDocInfo(component, getDefaultCollector(element, null), true, false); + } + + public ActionCallback queueFetchDocInfo(final DocumentationCollector provider, final DocumentationComponent component, final boolean clearHistory) { + return doFetchDocInfo(component, provider, false, clearHistory); + } + + public ActionCallback queueFetchDocInfo(final PsiElement element, final DocumentationComponent component) { + return queueFetchDocInfo(getDefaultCollector(element, null), component, false); + } + + private ActionCallback doFetchDocInfo(final DocumentationComponent component, final DocumentationCollector provider, final boolean cancelRequests, final boolean clearHistory) { + final ActionCallback callback = new ActionCallback(); + component.startWait(); + if (cancelRequests) { + myUpdateDocAlarm.cancelAllRequests(); + } + if (component.isEmpty()) { + component.setText(CodeInsightBundle.message("javadoc.fetching.progress"), null, clearHistory); + } + + myUpdateDocAlarm.addRequest(new Runnable() { + public void run() { + if (myProject.isDisposed()) return; + final Throwable[] ex = new Throwable[1]; + String text = null; + try { + text = provider.getDocumentation(); + } + catch (Throwable e) { + LOG.info(e); + ex[0] = e; + } + + if (ex[0] != null) { + //noinspection SSBasedInspection + SwingUtilities.invokeLater(new Runnable() { + public void run() { + String message = ex[0] instanceof IndexNotReadyException + ? "Documentation is not available until indices are built." + : CodeInsightBundle.message("javadoc.external.fetch.error.message", ex[0].getLocalizedMessage()); + component.setText(message, null, true); + callback.setDone(); + } + }); + return; + } + + final PsiElement element = ApplicationManager.getApplication().runReadAction(new Computable() { + @Nullable + public PsiElement compute() { + return provider.getElement(); + } + }); + if (element == null) { + return; + } + final String documentationText = text; + //noinspection SSBasedInspection + SwingUtilities.invokeLater(new Runnable() { + public void run() { + PsiDocumentManager.getInstance(myProject).commitAllDocuments(); + + if (!element.isValid()) { + callback.setDone(); + return; + } + + if (documentationText == null) { + component.setText(CodeInsightBundle.message("no.documentation.found"), element, true); + } + else if (documentationText.length() == 0) { + component.setText(component.getText(), element, true, clearHistory); + } + else { + component.setData(element, documentationText, clearHistory); + } + + final AbstractPopup jbPopup = (AbstractPopup)getDocInfoHint(); + if(jbPopup==null){ + callback.setDone(); + return; + } + jbPopup.setCaption(getTitle(element, false)); + callback.setDone(); + } + }); + } + }, 10); + return callback; + } + + @NotNull + public static DocumentationProvider getProviderFromElement(final PsiElement element) { + return getProviderFromElement(element, null); + } + + @NotNull + public static DocumentationProvider getProviderFromElement(@Nullable PsiElement element, @Nullable PsiElement originalElement) { + if (element != null && !element.isValid()) { + element = null; + } + if (originalElement != null && !originalElement.isValid()) { + originalElement = null; + } + + if (originalElement == null) { + originalElement = getOriginalElement(element); + } + + PsiFile containingFile = + originalElement != null ? originalElement.getContainingFile() : element != null ? element.getContainingFile() : null; + Set result = new LinkedHashSet(); + + final Language containingFileLanguage = containingFile != null ? containingFile.getLanguage() : null; + DocumentationProvider originalProvider = + containingFile != null ? LanguageDocumentation.INSTANCE.forLanguage(containingFileLanguage) : null; + + final Language elementLanguage = element != null ? element.getLanguage() : null; + DocumentationProvider elementProvider = + element == null || elementLanguage.is(containingFileLanguage) ? null : LanguageDocumentation.INSTANCE.forLanguage(elementLanguage); + + result.add(elementProvider); + result.add(originalProvider); + + if (containingFile != null) { + final Language baseLanguage = containingFile.getViewProvider().getBaseLanguage(); + if (!baseLanguage.is(containingFileLanguage)) { + result.add(LanguageDocumentation.INSTANCE.forLanguage(baseLanguage)); + } + } + else if (element instanceof PsiDirectory) { + final Set langs = new HashSet(); + + for (PsiFile file : ((PsiDirectory)element).getFiles()) { + final Language baseLanguage = file.getViewProvider().getBaseLanguage(); + if (!langs.contains(baseLanguage)) { + langs.add(baseLanguage); + result.add(LanguageDocumentation.INSTANCE.forLanguage(baseLanguage)); + } + } + } + return CompositeDocumentationProvider.wrapProviders(result); + } + + @Nullable + public static PsiElement getOriginalElement(final PsiElement element) { + SmartPsiElementPointer originalElementPointer = element!=null ? element.getUserData(ORIGINAL_ELEMENT_KEY):null; + return originalElementPointer != null ? originalElementPointer.getElement() : null; + } + + void navigateByLink(final DocumentationComponent component, final String url) { + component.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + final PsiElement psiElement = component.getElement(); + final PsiManager manager = PsiManager.getInstance(getProject(psiElement)); + if (url.startsWith("open")) { + final PsiFile containingFile = psiElement.getContainingFile(); + if (containingFile != null) { + final VirtualFile virtualFile = containingFile.getVirtualFile(); + final OrderEntry libraryEntry = LibraryUtil.findLibraryEntry(virtualFile, myProject); + if (libraryEntry != null) { + ProjectSettingsService.getInstance(myProject).openLibraryOrSdkSettings(libraryEntry); + } + } + } else if (url.startsWith(PSI_ELEMENT_PROTOCOL)) { + final String refText = url.substring(PSI_ELEMENT_PROTOCOL.length()); + DocumentationProvider provider = getProviderFromElement(psiElement); + final PsiElement targetElement = provider.getDocumentationElementForLink(manager, refText, psiElement); + if (targetElement != null) { + fetchDocInfo(getDefaultCollector(targetElement, null), component); + } + } + else { + final DocumentationProvider provider = getProviderFromElement(psiElement); + boolean processed = false; + if (provider instanceof CompositeDocumentationProvider) { + for (DocumentationProvider documentationProvider : ((CompositeDocumentationProvider)provider).getProviders()) { + if (documentationProvider instanceof ExternalDocumentationHandler) { + final ExternalDocumentationHandler externalDocumentationHandler = (ExternalDocumentationHandler)documentationProvider; + if (externalDocumentationHandler.canFetchDocumentationLink(url)) { + fetchDocInfo(new DocumentationCollector() { + @Override + public String getDocumentation() throws Exception { + return externalDocumentationHandler.fetchExternalDocumentation(url, myProject); + } + + @Override + public PsiElement getElement() { + return psiElement; + } + }, component); + processed = true; + } + else if (externalDocumentationHandler.handleExternalLink(manager, url, psiElement)) { + processed = true; + break; + } + } + } + } + + if (!processed) { + final String docUrl = url; + + fetchDocInfo + (new DocumentationCollector() { + public String getDocumentation() throws Exception { + if (docUrl.startsWith(DOC_ELEMENT_PROTOCOL)) { + final List urls = ApplicationManager.getApplication().runReadAction( + new Computable>() { + public List compute() { + final DocumentationProvider provider = getProviderFromElement(psiElement); + return provider.getUrlFor(psiElement, getOriginalElement(psiElement)); + } + } + ); + BrowserUtil.launchBrowser(urls != null && !urls.isEmpty() ? urls.get(0) : docUrl); + } + else { + BrowserUtil.launchBrowser(docUrl); + } + return ""; + } + + public PsiElement getElement() { + //String loc = getElementLocator(docUrl); + // + //if (loc != null) { + // PsiElement context = component.getElement(); + // return JavaDocUtil.findReferenceTarget(context.getManager(), loc, context); + //} + + return psiElement; + } + }, component); + } + } + + component.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + + void showHint(final JBPopup hint) { + final Component focusOwner = IdeFocusManager.getInstance(myProject).getFocusOwner(); + DataContext dataContext = DataManager.getInstance().getDataContext(focusOwner); + PopupPositionManager.positionPopupInBestPosition(hint, myEditor, dataContext); + } + + public void requestFocus() { + if (fromQuickSearch()) { + myPreviouslyFocused.getParent().requestFocus(); + } + } + + public Project getProject(@Nullable final PsiElement element) { + assertSameProject(element); + return myProject; + } + + private void assertSameProject(@Nullable PsiElement element) { + if (element != null && element.isValid() && myProject != element.getProject()) { + throw new AssertionError(myProject + "!=" + element.getProject() + "; element=" + element); + } + } + + @SuppressWarnings({"HardCodedStringLiteral"}) + public static void createHyperlink(StringBuilder buffer, String refText,String label,boolean plainLink) { + buffer.append(""); + if (!plainLink) { + buffer.append(""); + } + buffer.append(label); + if (!plainLink) { + buffer.append(""); + } + buffer.append(""); + } + + @Override + public String getShowInToolWindowProperty() { + return SHOW_DOCUMENTATION_IN_TOOL_WINDOW; + } + + @Override + public String getAutoUpdateEnabledProperty() { + return DOCUMENTATION_AUTO_UPDATE_ENABLED; + } + + protected void doUpdateComponent(PsiElement element, PsiElement originalElement, DocumentationComponent component) { + fetchDocInfo(getDefaultCollector(element, originalElement), component); + } + + protected void doUpdateComponent(Editor editor, PsiFile psiFile) { + showJavaDocInfo(editor, psiFile, false, true); + } + + @Override + protected void doUpdateComponent(@NotNull PsiElement element) { + showJavaDocInfo(element, element); + } + + @Override + protected String getTitle(PsiElement element) { + return getTitle(element, true); + } + + private interface DocumentationCollector { + @Nullable + String getDocumentation() throws Exception; + @Nullable + PsiElement getElement(); + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/QuickDocOnMouseOverManager.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/QuickDocOnMouseOverManager.java index 71955959cd84..90bdcb7c93e9 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/QuickDocOnMouseOverManager.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/QuickDocOnMouseOverManager.java @@ -1,269 +1,266 @@ -/* - * Copyright 2000-2012 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.documentation; - -import com.intellij.openapi.application.Application; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.EditorFactory; -import com.intellij.openapi.editor.event.*; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.popup.JBPopup; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiWhiteSpace; -import com.intellij.ui.popup.PopupFactoryImpl; -import com.intellij.util.Alarm; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.awt.*; -import java.lang.ref.WeakReference; -import java.util.HashMap; -import java.util.Map; - -/** - * Serves as a facade to the 'show quick doc on mouse over an element' functionality. - *

- * Not thread-safe. - * - * @author Denis Zhdanov - * @since 7/2/12 9:09 AM - */ -public class QuickDocOnMouseOverManager { - - private static final long QUICK_DOC_DELAY_MILLIS; - static { - long delay = 500; - String property = System.getProperty("editor.auto.quick.doc.delay.ms"); - if (property != null) { - try { - long parsed = Long.parseLong(property); - if (parsed > 0) { - delay = parsed; - } - } - catch (Exception e) { - // Ignore. - } - } - QUICK_DOC_DELAY_MILLIS = delay; - } - - @NotNull private final EditorMouseMotionListener myEditorListener = new MyEditorMouseListener(); - @NotNull private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); - @NotNull private final Runnable myRequest = new MyShowQuickDocRequest(); - @NotNull private final Runnable myHintCloseCallback = new Runnable() { - @Override - public void run() { - myActiveElements.clear(); - myDocumentationManager = null; - } - }; - - private final Map myActiveElements - = new HashMap(); - - /** Holds a reference (if any) to the documentation manager used last time to show an 'auto quick doc' popup. */ - @Nullable private WeakReference myDocumentationManager; - - @Nullable private DelayedQuickDocInfo myDelayedQuickDocInfo; - private boolean myEnabled; - - public QuickDocOnMouseOverManager(@NotNull Application application) { - EditorFactory factory = EditorFactory.getInstance(); - if (factory != null) { - factory.addEditorFactoryListener(new MyEditorFactoryListener(), application); - } - } - - /** - * Instructs the manager to enable or disable 'show quick doc automatically when the mouse goes over an editor element' mode. - * - * @param enabled flag that identifies if quick doc should be automatically shown - */ - public void setEnabled(boolean enabled) { - myEnabled = enabled; - if (!enabled) { - closeAutoQuickDocComponentIfNecessary(); - myAlarm.cancelAllRequests(); - } - EditorFactory factory = EditorFactory.getInstance(); - if (factory == null) { - return; - } - for (Editor editor : factory.getAllEditors()) { - if (enabled) { - editor.addEditorMouseMotionListener(myEditorListener); - } - else { - editor.removeEditorMouseMotionListener(myEditorListener); - } - } - } - - private void processMouseMove(@NotNull EditorMouseEvent e) { - if (e.getArea() != EditorMouseEventArea.EDITING_AREA) { - // Skip if the mouse is not at the editing area. - closeAutoQuickDocComponentIfNecessary(); - return; - } - - Editor editor = e.getEditor(); - Project project = editor.getProject(); - if (project == null) { - return; - } - - DocumentationManager documentationManager = DocumentationManager.getInstance(project); - JBPopup hint = documentationManager.getDocInfoHint(); - if (hint != null) { - - // Skip the event if the control is shown because of explicit 'show quick doc' action call. - WeakReference ref = myDocumentationManager; - if (ref == null || ref.get() == null) { - return; - } - - // Skip the event if the mouse is under the opened quick doc control. - Point hintLocation = hint.getLocationOnScreen(); - Dimension hintSize = hint.getSize(); - int mouseX = e.getMouseEvent().getXOnScreen(); - int mouseY = e.getMouseEvent().getYOnScreen(); - if (mouseX >= hintLocation.x && mouseX <= hintLocation.x + hintSize.width && mouseY >= hintLocation.y - && mouseY <= hintLocation.y + hintSize.height) - { - return; - } - } - - PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); - if (psiFile == null) { - closeAutoQuickDocComponentIfNecessary(); - return; - } - - int mouseOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(e.getMouseEvent().getPoint())); - PsiElement elementUnderMouse = psiFile.findElementAt(mouseOffset); - if (elementUnderMouse == null || elementUnderMouse instanceof PsiWhiteSpace) { - closeAutoQuickDocComponentIfNecessary(); - return; - } - - PsiElement targetElementUnderMouse = documentationManager.findTargetElement(editor, mouseOffset, psiFile, elementUnderMouse); - if (targetElementUnderMouse == null) { - // No PSI element is located under the current mouse position - close quick doc if any. - closeAutoQuickDocComponentIfNecessary(); - return; - } - - PsiElement activeElement = myActiveElements.get(editor); - if (targetElementUnderMouse.equals(activeElement) - && (myAlarm.getActiveRequestCount() > 0 // Request to show documentation for the target component has been already queued. - || hint != null)) // Documentation for the target component is being shown. - { - return; - } - closeAutoQuickDocComponentIfNecessary(); - myActiveElements.put(editor, targetElementUnderMouse); - myDelayedQuickDocInfo = new DelayedQuickDocInfo(documentationManager, editor, targetElementUnderMouse, elementUnderMouse); - - myAlarm.cancelAllRequests(); - myAlarm.addRequest(myRequest, QUICK_DOC_DELAY_MILLIS); - } - - private void closeAutoQuickDocComponentIfNecessary() { - myAlarm.cancelAllRequests(); - WeakReference ref = myDocumentationManager; - if (ref == null) { - return; - } - - DocumentationManager docManager = ref.get(); - if (docManager == null) { - return; - } - - JBPopup hint = docManager.getDocInfoHint(); - if (hint == null) { - return; - } - - hint.cancel(); - } - - private static class DelayedQuickDocInfo { - - @NotNull public final DocumentationManager docManager; - @NotNull public final Editor editor; - @NotNull public final PsiElement targetElement; - @NotNull public final PsiElement originalElement; - - private DelayedQuickDocInfo(@NotNull DocumentationManager docManager, - @NotNull Editor editor, @NotNull PsiElement targetElement, - @NotNull PsiElement originalElement) - { - this.docManager = docManager; - this.editor = editor; - this.targetElement = targetElement; - this.originalElement = originalElement; - } - } - - private class MyShowQuickDocRequest implements Runnable { - @Override - public void run() { - myAlarm.cancelAllRequests(); - - DelayedQuickDocInfo info = myDelayedQuickDocInfo; - if (info == null || !info.targetElement.equals(myActiveElements.get(info.editor))) { - return; - } - - info.editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, - info.editor.offsetToVisualPosition(info.originalElement.getTextRange().getStartOffset())); - try { - info.docManager.showJavaDocInfo(info.editor, info.targetElement, info.originalElement, myHintCloseCallback); - myDocumentationManager = new WeakReference(info.docManager); - } - finally { - info.editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, null); - } - } - } - - private class MyEditorFactoryListener implements EditorFactoryListener { - @Override - public void editorCreated(@NotNull EditorFactoryEvent event) { - if (myEnabled) { - event.getEditor().addEditorMouseMotionListener(myEditorListener); - } - } - - @Override - public void editorReleased(@NotNull EditorFactoryEvent event) { - event.getEditor().removeEditorMouseMotionListener(myEditorListener); - } - } - - private class MyEditorMouseListener extends EditorMouseMotionAdapter { - - @Override - public void mouseMoved(EditorMouseEvent e) { - processMouseMove(e); - } - } -} +/* + * Copyright 2000-2012 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.documentation; + +import com.intellij.openapi.application.Application; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.JBPopup; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.ui.popup.PopupFactoryImpl; +import com.intellij.util.Alarm; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; + +/** + * Serves as a facade to the 'show quick doc on mouse over an element' functionality. + *

+ * Not thread-safe. + * + * @author Denis Zhdanov + * @since 7/2/12 9:09 AM + */ +public class QuickDocOnMouseOverManager { + + private static final long QUICK_DOC_DELAY_MILLIS; + static { + long delay = 500; + String property = System.getProperty("editor.auto.quick.doc.delay.ms"); + if (property != null) { + try { + long parsed = Long.parseLong(property); + if (parsed > 0) { + delay = parsed; + } + } + catch (Exception e) { + // Ignore. + } + } + QUICK_DOC_DELAY_MILLIS = delay; + } + + @NotNull private final EditorMouseMotionListener myMouseListener = new MyEditorMouseListener(); + @NotNull private final VisibleAreaListener myVisibleAreaListener = new MyVisibleAreaListener(); + @NotNull private final CaretListener myCaretListener = new MyCaretListener(); + @NotNull private final DocumentListener myDocumentListener = new MyDocumentListener(); + @NotNull private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); + @NotNull private final Runnable myRequest = new MyShowQuickDocRequest(); + @NotNull private final Runnable myHintCloseCallback = new MyCloseDocCallback(); + + private final Map myActiveElements + = new HashMap(); + + /** Holds a reference (if any) to the documentation manager used last time to show an 'auto quick doc' popup. */ + @Nullable private WeakReference myDocumentationManager; + + @Nullable private DelayedQuickDocInfo myDelayedQuickDocInfo; + private boolean myEnabled; + + public QuickDocOnMouseOverManager(@NotNull Application application) { + EditorFactory factory = EditorFactory.getInstance(); + if (factory != null) { + factory.addEditorFactoryListener(new MyEditorFactoryListener(), application); + } + } + + /** + * Instructs the manager to enable or disable 'show quick doc automatically when the mouse goes over an editor element' mode. + * + * @param enabled flag that identifies if quick doc should be automatically shown + */ + public void setEnabled(boolean enabled) { + myEnabled = enabled; + if (!enabled) { + closeAutoQuickDocComponentIfNecessary(); + myAlarm.cancelAllRequests(); + } + EditorFactory factory = EditorFactory.getInstance(); + if (factory == null) { + return; + } + for (Editor editor : factory.getAllEditors()) { + if (enabled) { + editor.addEditorMouseMotionListener(myEditorListener); + } + else { + editor.removeEditorMouseMotionListener(myEditorListener); + } + } + } + + private void processMouseMove(@NotNull EditorMouseEvent e) { + if (e.getArea() != EditorMouseEventArea.EDITING_AREA) { + // Skip if the mouse is not at the editing area. + closeAutoQuickDocComponentIfNecessary(); + return; + } + + Editor editor = e.getEditor(); + Project project = editor.getProject(); + if (project == null) { + return; + } + + DocumentationManager documentationManager = DocumentationManager.getInstance(project); + JBPopup hint = documentationManager.getDocInfoHint(); + if (hint != null) { + + // Skip the event if the control is shown because of explicit 'show quick doc' action call. + WeakReference ref = myDocumentationManager; + if (ref == null || ref.get() == null) { + return; + } + + // Skip the event if the mouse is under the opened quick doc control. + Point hintLocation = hint.getLocationOnScreen(); + Dimension hintSize = hint.getSize(); + int mouseX = e.getMouseEvent().getXOnScreen(); + int mouseY = e.getMouseEvent().getYOnScreen(); + if (mouseX >= hintLocation.x && mouseX <= hintLocation.x + hintSize.width && mouseY >= hintLocation.y + && mouseY <= hintLocation.y + hintSize.height) + { + return; + } + } + + PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); + if (psiFile == null) { + closeAutoQuickDocComponentIfNecessary(); + return; + } + + int mouseOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(e.getMouseEvent().getPoint())); + PsiElement elementUnderMouse = psiFile.findElementAt(mouseOffset); + if (elementUnderMouse == null || elementUnderMouse instanceof PsiWhiteSpace) { + closeAutoQuickDocComponentIfNecessary(); + return; + } + + PsiElement targetElementUnderMouse = documentationManager.findTargetElement(editor, mouseOffset, psiFile, elementUnderMouse); + if (targetElementUnderMouse == null) { + // No PSI element is located under the current mouse position - close quick doc if any. + closeAutoQuickDocComponentIfNecessary(); + return; + } + + PsiElement activeElement = myActiveElements.get(editor); + if (targetElementUnderMouse.equals(activeElement) + && (myAlarm.getActiveRequestCount() > 0 // Request to show documentation for the target component has been already queued. + || hint != null)) // Documentation for the target component is being shown. + { + return; + } + closeAutoQuickDocComponentIfNecessary(); + myActiveElements.put(editor, targetElementUnderMouse); + myDelayedQuickDocInfo = new DelayedQuickDocInfo(documentationManager, editor, targetElementUnderMouse, elementUnderMouse); + + myAlarm.cancelAllRequests(); + myAlarm.addRequest(myRequest, QUICK_DOC_DELAY_MILLIS); + } + + private void closeAutoQuickDocComponentIfNecessary() { + myAlarm.cancelAllRequests(); + WeakReference ref = myDocumentationManager; + if (ref == null) { + return; + } + + DocumentationManager docManager = ref.get(); + if (docManager == null) { + return; + } + + JBPopup hint = docManager.getDocInfoHint(); + if (hint == null) { + return; + } + + hint.cancel(); + } + + private static class DelayedQuickDocInfo { + + @NotNull public final DocumentationManager docManager; + @NotNull public final Editor editor; + @NotNull public final PsiElement targetElement; + @NotNull public final PsiElement originalElement; + + private DelayedQuickDocInfo(@NotNull DocumentationManager docManager, + @NotNull Editor editor, @NotNull PsiElement targetElement, + @NotNull PsiElement originalElement) + { + this.docManager = docManager; + this.editor = editor; + this.targetElement = targetElement; + this.originalElement = originalElement; + } + } + + private class MyShowQuickDocRequest implements Runnable { + @Override + public void run() { + myAlarm.cancelAllRequests(); + + DelayedQuickDocInfo info = myDelayedQuickDocInfo; + if (info == null || !info.targetElement.equals(myActiveElements.get(info.editor))) { + return; + } + + info.editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, + info.editor.offsetToVisualPosition(info.originalElement.getTextRange().getStartOffset())); + try { + info.docManager.showJavaDocInfo(info.editor, info.targetElement, info.originalElement, myHintCloseCallback); + myDocumentationManager = new WeakReference(info.docManager); + } + finally { + info.editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, null); + } + } + } + + private class MyEditorFactoryListener implements EditorFactoryListener { + @Override + public void editorCreated(@NotNull EditorFactoryEvent event) { + if (myEnabled) { + event.getEditor().addEditorMouseMotionListener(myEditorListener); + } + } + + @Override + public void editorReleased(@NotNull EditorFactoryEvent event) { + event.getEditor().removeEditorMouseMotionListener(myEditorListener); + } + } + + private class MyEditorMouseListener extends EditorMouseMotionAdapter { + + @Override + public void mouseMoved(EditorMouseEvent e) { + processMouseMove(e); + } + } +}