/* * Copyright 2000-2017 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.application.ApplicationActivationListener; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.VisualPosition; import com.intellij.openapi.editor.event.*; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.ex.EditorSettingsExternalizable; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.util.ProgressIndicatorBase; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.util.Ref; import com.intellij.openapi.wm.IdeFrame; import com.intellij.psi.*; import com.intellij.reference.SoftReference; import com.intellij.ui.popup.PopupFactoryImpl; import com.intellij.util.Alarm; import com.intellij.util.containers.WeakHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.*; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.util.Map; /** * Serves as a facade to the 'show quick doc on mouse over an element' functionality. * <p/> * Not thread-safe. * * @author Denis Zhdanov * @since 7/2/12 9:09 AM */ public class QuickDocOnMouseOverManager { @NotNull private final MyEditorMouseListener 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; @NotNull private final Runnable myHintCloseCallback = new MyCloseDocCallback(); @NotNull private final Map<Document, Boolean> myMonitoredDocuments = new WeakHashMap<>(); private final Map<Editor, Reference<PsiElement> /* PSI element which is located under the current mouse position */> myActiveElements = new WeakHashMap<>(); /** Holds a reference (if any) to the documentation manager used last time to show an 'auto quick doc' popup. */ @Nullable private WeakReference<DocumentationManager> myDocumentationManager; private boolean myEnabled; private boolean myApplicationActive; private MyShowQuickDocRequest myCurrentRequest; // accessed only in EDT public QuickDocOnMouseOverManager(@NotNull Application application) { myAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, application); EditorFactory factory = EditorFactory.getInstance(); if (factory != null) { factory.addEditorFactoryListener(new MyEditorFactoryListener(), application); } ApplicationManager.getApplication().getMessageBus().connect().subscribe( ApplicationActivationListener.TOPIC, new ApplicationActivationListener() { @Override public void applicationActivated(IdeFrame ideFrame) { myApplicationActive = true; } @Override public void applicationDeactivated(IdeFrame ideFrame) { myApplicationActive = false; closeQuickDocIfPossible(); } }); } /** * 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; myApplicationActive = enabled; if (!enabled) { closeQuickDocIfPossible(); myAlarm.cancelAllRequests(); } EditorFactory factory = EditorFactory.getInstance(); if (factory == null) { return; } for (Editor editor : factory.getAllEditors()) { if (enabled) { registerListeners(editor); } else { unRegisterListeners(editor); } } } private void registerListeners(@NotNull Editor editor) { editor.addEditorMouseListener(myMouseListener); editor.addEditorMouseMotionListener(myMouseListener); editor.getScrollingModel().addVisibleAreaListener(myVisibleAreaListener); editor.getCaretModel().addCaretListener(myCaretListener); Document document = editor.getDocument(); if (myMonitoredDocuments.put(document, Boolean.TRUE) == null) { document.addDocumentListener(myDocumentListener); } } private void unRegisterListeners(@NotNull Editor editor) { editor.removeEditorMouseListener(myMouseListener); editor.removeEditorMouseMotionListener(myMouseListener); editor.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener); editor.getCaretModel().removeCaretListener(myCaretListener); Document document = editor.getDocument(); if (myMonitoredDocuments.remove(document) != null) { document.removeDocumentListener(myDocumentListener); } } private void processMouseExited() { myActiveElements.clear(); myAlarm.cancelAllRequests(); } private void processMouseMove(@NotNull EditorMouseEvent e) { if (!myApplicationActive || !myEnabled || e.getArea() != EditorMouseEventArea.EDITING_AREA) { // Skip if the mouse is not at the editing area. closeQuickDocIfPossible(); return; } if (e.getMouseEvent().getModifiers() != 0) { // Don't show the control when any modifier is active (e.g. Ctrl or Alt is hold). There is a common situation that a user // wants to navigate via Ctrl+click or perform quick evaluate by Alt+click. return; } Editor editor = e.getEditor(); if (editor.getComponent().getClientProperty(EditorImpl.IGNORE_MOUSE_TRACKING) != null) { return; } if (editor.isOneLineMode()) { // Don't want auto quick doc to mess at, say, editor used for debugger condition. return; } 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. DocumentationManager manager = getDocManager(); if (manager == null || !manager.isCloseOnSneeze()) { 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) { closeQuickDocIfPossible(); return; } Point point = e.getMouseEvent().getPoint(); if (editor instanceof EditorEx && ((EditorEx)editor).getFoldingModel().getFoldingPlaceholderAt(point) != null) { closeQuickDocIfPossible(); return; } VisualPosition visualPosition = editor.xyToVisualPosition(point); if (editor.getSoftWrapModel().isInsideOrBeforeSoftWrap(visualPosition)) { closeQuickDocIfPossible(); return; } int mouseOffset = editor.logicalPositionToOffset(editor.visualToLogicalPosition(visualPosition)); PsiElement elementUnderMouse = psiFile.findElementAt(mouseOffset); if (elementUnderMouse == null || elementUnderMouse instanceof PsiWhiteSpace || elementUnderMouse instanceof PsiPlainText) { closeQuickDocIfPossible(); return; } if (elementUnderMouse.equals(SoftReference.dereference(myActiveElements.get(editor))) && (!myAlarm.isEmpty() // Request to show documentation for the target component has been already queued. || hint != null)) // Documentation for the target component is being shown. { return; } allowUpdateFromContext(project, false); closeQuickDocIfPossible(); myActiveElements.put(editor, new WeakReference<>(elementUnderMouse)); myAlarm.cancelAllRequests(); if (myCurrentRequest != null) myCurrentRequest.cancel(); myCurrentRequest = new MyShowQuickDocRequest(documentationManager, editor, mouseOffset, elementUnderMouse); myAlarm.addRequest(myCurrentRequest, EditorSettingsExternalizable.getInstance().getQuickDocOnMouseOverElementDelayMillis()); } private void closeQuickDocIfPossible() { myAlarm.cancelAllRequests(); DocumentationManager docManager = getDocManager(); if (docManager == null) { return; } JBPopup hint = docManager.getDocInfoHint(); if (hint == null) { return; } hint.cancel(); myDocumentationManager = null; } private void allowUpdateFromContext(Project project, boolean allow) { DocumentationManager documentationManager = getDocManager(); if (documentationManager != null && documentationManager.getProject(null) == project) { documentationManager.setAllowContentUpdateFromContext(allow); } } @Nullable private DocumentationManager getDocManager() { return SoftReference.dereference(myDocumentationManager); } @Nullable private Editor getEditor() { DocumentationManager manager = getDocManager(); return manager == null ? null : manager.getEditor(); } private class MyShowQuickDocRequest implements Runnable { @NotNull private final DocumentationManager docManager; @NotNull private final Editor editor; private final int offset; @NotNull private final PsiElement originalElement; @NotNull private final ProgressIndicator myProgressIndicator = new ProgressIndicatorBase(); private MyShowQuickDocRequest(@NotNull DocumentationManager docManager, @NotNull Editor editor, int offset, @NotNull PsiElement originalElement) { this.docManager = docManager; this.editor = editor; this.offset = offset; this.originalElement = originalElement; } private void cancel() { myProgressIndicator.cancel(); } @Override public void run() { Ref<PsiElement> targetElementRef = new Ref<>(); QuickDocUtil.runInReadActionWithWriteActionPriorityWithRetries(() -> { if (originalElement.isValid()) { targetElementRef.set(docManager.findTargetElement(editor, offset, originalElement.getContainingFile(), originalElement)); } }, 5000, 100, myProgressIndicator); ApplicationManager.getApplication().invokeLater(() -> { myCurrentRequest = null; if (editor.isDisposed()) return; PsiElement targetElement = targetElementRef.get(); if (targetElement == null) { closeQuickDocIfPossible(); return; } myAlarm.cancelAllRequests(); if (!originalElement.equals(SoftReference.dereference(myActiveElements.get(editor)))) { return; } // Skip the request if there is a control shown as a result of explicit 'show quick doc' (Ctrl + Q) invocation. if (docManager.getDocInfoHint() != null && !docManager.isCloseOnSneeze()) { return; } editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, editor.offsetToVisualPosition(originalElement.getTextRange().getStartOffset())); try { docManager.showJavaDocInfo(editor, targetElement, originalElement, myHintCloseCallback, true); myDocumentationManager = new WeakReference<>(docManager); } finally { editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, null); } }, ApplicationManager.getApplication().getNoneModalityState()); } } private class MyCloseDocCallback implements Runnable { @Override public void run() { myActiveElements.clear(); myDocumentationManager = null; } } private class MyEditorFactoryListener implements EditorFactoryListener { @Override public void editorCreated(@NotNull EditorFactoryEvent event) { if (myEnabled) { registerListeners(event.getEditor()); } } @Override public void editorReleased(@NotNull EditorFactoryEvent event) { if (myEnabled) { // We do this in the 'if' block because editor logs an error on attempt to remove already released listener. unRegisterListeners(event.getEditor()); } } } private class MyEditorMouseListener extends EditorMouseAdapter implements EditorMouseMotionListener { @Override public void mouseExited(EditorMouseEvent e) { processMouseExited(); } @Override public void mouseMoved(EditorMouseEvent e) { processMouseMove(e); } @Override public void mouseDragged(EditorMouseEvent e) {} } private class MyVisibleAreaListener implements VisibleAreaListener { @Override public void visibleAreaChanged(VisibleAreaEvent e) { Editor editor = getEditor(); if (editor == null || editor == e.getEditor()) { closeQuickDocIfPossible(); } } } private class MyCaretListener implements CaretListener { @Override public void caretPositionChanged(CaretEvent e) { Editor editor = getEditor(); if (editor == null || editor == e.getEditor()) { allowUpdateFromContext(e.getEditor().getProject(), true); closeQuickDocIfPossible(); } } } private class MyDocumentListener implements DocumentListener { @Override public void documentChanged(DocumentEvent e) { Editor editor = getEditor(); if (editor == null || editor.getDocument() == e.getDocument()) { closeQuickDocIfPossible(); } } } }