blob: 0cd18020ad54e458db301146c125d482bca9c4b4 [file] [log] [blame]
/*
* Copyright 2000-2014 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.hint.HintManager;
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.EditorSettingsExternalizable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
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.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 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();
@NotNull private final Map<Document, Boolean> myMonitoredDocuments = new WeakHashMap<Document, Boolean>();
private final Map<Editor, PsiElement /** PSI element which is located under the current mouse position */> myActiveElements
= new WeakHashMap<Editor, PsiElement>();
/** Holds a reference (if any) to the documentation manager used last time to show an 'auto quick doc' popup. */
@Nullable private WeakReference<DocumentationManager> myDocumentationManager;
@Nullable private DelayedQuickDocInfo myDelayedQuickDocInfo;
private boolean myEnabled;
private boolean myApplicationActive;
public QuickDocOnMouseOverManager(@NotNull Application 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;
}
});
}
/**
* 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.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.removeEditorMouseMotionListener(myMouseListener);
editor.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener);
editor.getCaretModel().removeCaretListener(myCaretListener);
Document document = editor.getDocument();
if (myMonitoredDocuments.remove(document) != null) {
document.removeDocumentListener(myDocumentListener);
}
}
private void processMouseMove(@NotNull EditorMouseEvent e) {
if (!myApplicationActive || 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.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;
}
VisualPosition visualPosition = editor.xyToVisualPosition(e.getMouseEvent().getPoint());
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;
}
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.
closeQuickDocIfPossible();
return;
}
PsiElement activeElement = myActiveElements.get(editor);
if (targetElementUnderMouse.equals(activeElement)
&& (!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, targetElementUnderMouse);
myDelayedQuickDocInfo = new DelayedQuickDocInfo(documentationManager, editor, targetElementUnderMouse, elementUnderMouse);
myAlarm.cancelAllRequests();
myAlarm.addRequest(myRequest, 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);
}
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 {
private final HintManager myHintManager = HintManager.getInstance();
@Override
public void run() {
myAlarm.cancelAllRequests();
// Skip the request if it's outdated (the mouse is moved other another element).
DelayedQuickDocInfo info = myDelayedQuickDocInfo;
if (info == null || !info.targetElement.equals(myActiveElements.get(info.editor))) {
return;
}
// Skip the request if there is a control shown as a result of explicit 'show quick doc' (Ctrl + Q) invocation.
if (info.docManager.getDocInfoHint() != null && !info.docManager.isCloseOnSneeze()) {
return;
}
// We don't want to show a quick doc control if there is an active hint (e.g. the mouse is under an invalid element
// and corresponding error info is shown).
if (!info.docManager.hasActiveDockedDocWindow() && myHintManager.hasShownHintsThatWillHideByOtherHint(false)) {
myAlarm.addRequest(this, EditorSettingsExternalizable.getInstance().getQuickDocOnMouseOverElementDelayMillis());
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, true);
myDocumentationManager = new WeakReference<DocumentationManager>(info.docManager);
}
finally {
info.editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, null);
}
}
}
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 EditorMouseMotionAdapter {
@Override
public void mouseMoved(EditorMouseEvent e) {
processMouseMove(e);
}
}
private class MyVisibleAreaListener implements VisibleAreaListener {
@Override
public void visibleAreaChanged(VisibleAreaEvent e) {
closeQuickDocIfPossible();
}
}
private class MyCaretListener extends CaretAdapter {
@Override
public void caretPositionChanged(CaretEvent e) {
allowUpdateFromContext(e.getEditor().getProject(), true);
closeQuickDocIfPossible();
}
}
private class MyDocumentListener extends DocumentAdapter {
@Override
public void documentChanged(DocumentEvent e) {
closeQuickDocIfPossible();
}
}
}