blob: 5486a07f6f18719590062d528f3f5c98f25fdbb9 [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.navigation;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.TargetElementUtilBase;
import com.intellij.codeInsight.documentation.DocumentationManager;
import com.intellij.codeInsight.documentation.DocumentationManagerProtocol;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.hint.HintUtil;
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
import com.intellij.codeInsight.navigation.actions.GotoTypeDeclarationAction;
import com.intellij.ide.IdeTooltipManager;
import com.intellij.ide.util.EditSourceUtil;
import com.intellij.lang.documentation.DocumentationProvider;
import com.intellij.navigation.ItemPresentation;
import com.intellij.navigation.NavigationItem;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.actionSystem.MouseShortcut;
import com.intellij.openapi.actionSystem.Shortcut;
import com.intellij.openapi.actionSystem.impl.ActionButton;
import com.intellij.openapi.actionSystem.impl.PresentationFactory;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.AbstractProjectComponent;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorManagerAdapter;
import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.util.ProgressIndicatorBase;
import com.intellij.openapi.progress.util.ProgressIndicatorUtils;
import com.intellij.openapi.progress.util.ReadTask;
import com.intellij.openapi.project.DumbAwareRunnable;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.Navigatable;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.search.searches.DefinitionsScopedSearch;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.ui.HintListener;
import com.intellij.ui.LightweightHint;
import com.intellij.ui.ScreenUtil;
import com.intellij.ui.components.JBLayeredPane;
import com.intellij.usageView.UsageViewShortNameLocation;
import com.intellij.usageView.UsageViewTypeLocation;
import com.intellij.util.Alarm;
import com.intellij.util.Consumer;
import com.intellij.util.Processor;
import com.intellij.util.ui.UIUtil;
import gnu.trove.TIntArrayList;
import org.intellij.lang.annotations.JdkConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventObject;
import java.util.List;
public class CtrlMouseHandler extends AbstractProjectComponent {
private static final AbstractDocumentationTooltipAction[] ourTooltipActions = {new ShowQuickDocAtPinnedWindowFromTooltipAction()};
private final EditorColorsManager myEditorColorsManager;
private HighlightersSet myHighlighter;
@JdkConstants.InputEventMask private int myStoredModifiers = 0;
private TooltipProvider myTooltipProvider = null;
private final FileEditorManager myFileEditorManager;
private final DocumentationManager myDocumentationManager;
@Nullable private Point myPrevMouseLocation;
private LightweightHint myHint;
private enum BrowseMode {None, Declaration, TypeDeclaration, Implementation}
private final KeyListener myEditorKeyListener = new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent e) {
handleKey(e);
}
@Override
public void keyReleased(final KeyEvent e) {
handleKey(e);
}
private void handleKey(final KeyEvent e) {
int modifiers = e.getModifiers();
if (modifiers == myStoredModifiers) {
return;
}
BrowseMode browseMode = getBrowseMode(modifiers);
if (browseMode == BrowseMode.None) {
disposeHighlighter();
cancelPreviousTooltip();
}
else {
TooltipProvider tooltipProvider = myTooltipProvider;
if (tooltipProvider != null) {
if (browseMode != tooltipProvider.getBrowseMode()) {
disposeHighlighter();
}
myStoredModifiers = modifiers;
cancelPreviousTooltip();
myTooltipProvider = new TooltipProvider(tooltipProvider.myEditor, tooltipProvider.myPosition);
myTooltipProvider.execute(browseMode);
}
}
}
};
private final FileEditorManagerListener myFileEditorManagerListener = new FileEditorManagerAdapter() {
@Override
public void selectionChanged(@NotNull FileEditorManagerEvent e) {
disposeHighlighter();
cancelPreviousTooltip();
}
};
private final VisibleAreaListener myVisibleAreaListener = new VisibleAreaListener() {
@Override
public void visibleAreaChanged(VisibleAreaEvent e) {
disposeHighlighter();
cancelPreviousTooltip();
}
};
private final EditorMouseAdapter myEditorMouseAdapter = new EditorMouseAdapter() {
@Override
public void mouseReleased(EditorMouseEvent e) {
disposeHighlighter();
cancelPreviousTooltip();
}
};
private final EditorMouseMotionListener myEditorMouseMotionListener = new EditorMouseMotionAdapter() {
@Override
public void mouseMoved(final EditorMouseEvent e) {
if (e.isConsumed() || !myProject.isInitialized() || myProject.isDisposed()) {
return;
}
MouseEvent mouseEvent = e.getMouseEvent();
if (isMouseOverTooltip(mouseEvent.getLocationOnScreen())
|| ScreenUtil.isMovementTowards(myPrevMouseLocation, mouseEvent.getLocationOnScreen(), getHintBounds())) {
myPrevMouseLocation = mouseEvent.getLocationOnScreen();
return;
}
myPrevMouseLocation = mouseEvent.getLocationOnScreen();
Editor editor = e.getEditor();
if (editor.getProject() != null && editor.getProject() != myProject) return;
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
PsiFile psiFile = documentManager.getPsiFile(editor.getDocument());
Point point = new Point(mouseEvent.getPoint());
if (documentManager.isCommitted(editor.getDocument())) {
// when document is committed, try to check injected stuff - it's fast
int offset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(point));
editor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(editor, psiFile, offset);
}
LogicalPosition pos = editor.xyToLogicalPosition(point);
int offset = editor.logicalPositionToOffset(pos);
int selStart = editor.getSelectionModel().getSelectionStart();
int selEnd = editor.getSelectionModel().getSelectionEnd();
myStoredModifiers = mouseEvent.getModifiers();
BrowseMode browseMode = getBrowseMode(myStoredModifiers);
cancelPreviousTooltip();
if (browseMode == BrowseMode.None || offset >= selStart && offset < selEnd) {
disposeHighlighter();
return;
}
myTooltipProvider = new TooltipProvider(editor, pos);
myTooltipProvider.execute(browseMode);
}
};
private void cancelPreviousTooltip() {
if (myTooltipProvider != null) {
myTooltipProvider.dispose();
myTooltipProvider = null;
}
}
@NotNull private final Alarm myDocAlarm;
public CtrlMouseHandler(final Project project,
StartupManager startupManager,
EditorColorsManager colorsManager,
FileEditorManager fileEditorManager,
@NotNull DocumentationManager documentationManager,
@NotNull final EditorFactory editorFactory) {
super(project);
myEditorColorsManager = colorsManager;
startupManager.registerPostStartupActivity(new DumbAwareRunnable() {
@Override
public void run() {
EditorEventMulticaster eventMulticaster = editorFactory.getEventMulticaster();
eventMulticaster.addEditorMouseListener(myEditorMouseAdapter, project);
eventMulticaster.addEditorMouseMotionListener(myEditorMouseMotionListener, project);
eventMulticaster.addCaretListener(new CaretAdapter() {
@Override
public void caretPositionChanged(CaretEvent e) {
if (myHint != null) {
myDocumentationManager.updateToolwindowContext();
}
}
}, project);
}
});
myFileEditorManager = fileEditorManager;
myDocumentationManager = documentationManager;
myDocAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, myProject);
}
@Override
@NotNull
public String getComponentName() {
return "CtrlMouseHandler";
}
private boolean isMouseOverTooltip(@NotNull Point mouseLocationOnScreen) {
Rectangle bounds = getHintBounds();
return bounds != null && bounds.contains(mouseLocationOnScreen);
}
@Nullable
private Rectangle getHintBounds() {
LightweightHint hint = myHint;
if (hint == null) {
return null;
}
JComponent hintComponent = hint.getComponent();
if (!hintComponent.isShowing()) {
return null;
}
return new Rectangle(hintComponent.getLocationOnScreen(), hintComponent.getSize());
}
@NotNull
private static BrowseMode getBrowseMode(@JdkConstants.InputEventMask int modifiers) {
if (modifiers != 0) {
final Keymap activeKeymap = KeymapManager.getInstance().getActiveKeymap();
if (matchMouseShortcut(activeKeymap, modifiers, IdeActions.ACTION_GOTO_DECLARATION)) return BrowseMode.Declaration;
if (matchMouseShortcut(activeKeymap, modifiers, IdeActions.ACTION_GOTO_TYPE_DECLARATION)) return BrowseMode.TypeDeclaration;
if (matchMouseShortcut(activeKeymap, modifiers, IdeActions.ACTION_GOTO_IMPLEMENTATION)) return BrowseMode.Implementation;
if (modifiers == InputEvent.CTRL_MASK || modifiers == InputEvent.META_MASK) return BrowseMode.Declaration;
}
return BrowseMode.None;
}
private static boolean matchMouseShortcut(final Keymap activeKeymap, @JdkConstants.InputEventMask int modifiers, final String actionId) {
final MouseShortcut syntheticShortcut = new MouseShortcut(MouseEvent.BUTTON1, modifiers, 1);
for (Shortcut shortcut : activeKeymap.getShortcuts(actionId)) {
if (shortcut instanceof MouseShortcut) {
final MouseShortcut mouseShortcut = (MouseShortcut)shortcut;
if (mouseShortcut.getModifiers() == syntheticShortcut.getModifiers()) {
return true;
}
}
}
return false;
}
@Nullable
@TestOnly
public static String getInfo(PsiElement element, PsiElement atPointer) {
return generateInfo(element, atPointer).text;
}
@NotNull
private static DocInfo generateInfo(PsiElement element, PsiElement atPointer) {
final DocumentationProvider documentationProvider = DocumentationManager.getProviderFromElement(element, atPointer);
String result = doGenerateInfo(element, atPointer, documentationProvider);
return result == null ? DocInfo.EMPTY : new DocInfo(result, documentationProvider, element);
}
@Nullable
private static String doGenerateInfo(@NotNull PsiElement element,
@NotNull PsiElement atPointer,
@NotNull DocumentationProvider documentationProvider)
{
String info = documentationProvider.getQuickNavigateInfo(element, atPointer);
if (info != null) {
return info;
}
if (element instanceof PsiFile) {
final VirtualFile virtualFile = ((PsiFile)element).getVirtualFile();
if (virtualFile != null) {
return virtualFile.getPresentableUrl();
}
}
info = getQuickNavigateInfo(element);
if (info != null) {
return info;
}
if (element instanceof NavigationItem) {
final ItemPresentation presentation = ((NavigationItem)element).getPresentation();
if (presentation != null) {
return presentation.getPresentableText();
}
}
return null;
}
@Nullable
private static String getQuickNavigateInfo(PsiElement element) {
final String name = ElementDescriptionUtil.getElementDescription(element, UsageViewShortNameLocation.INSTANCE);
if (StringUtil.isEmpty(name)) return null;
final String typeName = ElementDescriptionUtil.getElementDescription(element, UsageViewTypeLocation.INSTANCE);
final PsiFile file = element.getContainingFile();
final StringBuilder sb = new StringBuilder();
if (StringUtil.isNotEmpty(typeName)) sb.append(typeName).append(" ");
sb.append("\"").append(name).append("\"");
if (file != null && file.isPhysical()) {
sb.append(" [").append(file.getName()).append("]");
}
return sb.toString();
}
private abstract static class Info {
@NotNull protected final PsiElement myElementAtPointer;
@NotNull private final List<TextRange> myRanges;
public Info(@NotNull PsiElement elementAtPointer, @NotNull List<TextRange> ranges) {
myElementAtPointer = elementAtPointer;
myRanges = ranges;
}
public Info(@NotNull PsiElement elementAtPointer) {
this(elementAtPointer, Collections.singletonList(new TextRange(elementAtPointer.getTextOffset(),
elementAtPointer.getTextOffset() + elementAtPointer.getTextLength())));
}
boolean isSimilarTo(@NotNull Info that) {
return Comparing.equal(myElementAtPointer, that.myElementAtPointer) && myRanges.equals(that.myRanges);
}
@NotNull
public List<TextRange> getRanges() {
return myRanges;
}
@NotNull
public abstract DocInfo getInfo();
public abstract boolean isValid(@NotNull Document document);
public abstract void showDocInfo(@NotNull DocumentationManager docManager);
protected boolean rangesAreCorrect(@NotNull Document document) {
final TextRange docRange = new TextRange(0, document.getTextLength());
for (TextRange range : getRanges()) {
if (!docRange.contains(range)) return false;
}
return true;
}
}
private static void showDumbModeNotification(@NotNull Project project) {
DumbService.getInstance(project).showDumbModeNotification("Element information is not available during index update");
}
private static class InfoSingle extends Info {
@NotNull private final PsiElement myTargetElement;
public InfoSingle(@NotNull PsiElement elementAtPointer, @NotNull PsiElement targetElement) {
super(elementAtPointer);
myTargetElement = targetElement;
}
public InfoSingle(@NotNull PsiReference ref, @NotNull final PsiElement targetElement) {
super(ref.getElement(), ReferenceRange.getAbsoluteRanges(ref));
myTargetElement = targetElement;
}
@Override
@NotNull
public DocInfo getInfo() {
return ApplicationManager.getApplication().runReadAction(new Computable<DocInfo>() {
@Override
public DocInfo compute() {
try {
return generateInfo(myTargetElement, myElementAtPointer);
}
catch (IndexNotReadyException e) {
showDumbModeNotification(myTargetElement.getProject());
return DocInfo.EMPTY;
}
}
});
}
@Override
public boolean isValid(@NotNull Document document) {
if (!myTargetElement.isValid()) return false;
if (!myElementAtPointer.isValid()) return false;
if (myTargetElement == myElementAtPointer) return false;
return rangesAreCorrect(document);
}
@Override
public void showDocInfo(@NotNull DocumentationManager docManager) {
docManager.showJavaDocInfo(myTargetElement, myElementAtPointer, null);
docManager.setAllowContentUpdateFromContext(false);
}
}
private static class InfoMultiple extends Info {
public InfoMultiple(@NotNull final PsiElement elementAtPointer) {
super(elementAtPointer);
}
public InfoMultiple(@NotNull final PsiElement elementAtPointer, @NotNull PsiReference ref) {
super(elementAtPointer, ReferenceRange.getAbsoluteRanges(ref));
}
@Override
@NotNull
public DocInfo getInfo() {
return new DocInfo(CodeInsightBundle.message("multiple.implementations.tooltip"), null, null);
}
@Override
public boolean isValid(@NotNull Document document) {
return rangesAreCorrect(document);
}
@Override
public void showDocInfo(@NotNull DocumentationManager docManager) {
// Do nothing
}
}
@Nullable
private Info getInfoAt(@NotNull Editor editor, @NotNull PsiFile file, int offset, @NotNull BrowseMode browseMode) {
PsiElement targetElement = null;
if (browseMode == BrowseMode.TypeDeclaration) {
try {
targetElement = GotoTypeDeclarationAction.findSymbolType(editor, offset);
}
catch (IndexNotReadyException e) {
showDumbModeNotification(myProject);
}
}
else if (browseMode == BrowseMode.Declaration) {
final PsiReference ref = TargetElementUtilBase.findReference(editor, offset);
final List<PsiElement> resolvedElements = ref == null ? Collections.<PsiElement>emptyList() : resolve(ref);
final PsiElement resolvedElement = resolvedElements.size() == 1 ? resolvedElements.get(0) : null;
final PsiElement[] targetElements = GotoDeclarationAction.findTargetElementsNoVS(myProject, editor, offset, false);
final PsiElement elementAtPointer = file.findElementAt(TargetElementUtilBase.adjustOffset(file, editor.getDocument(), offset));
if (targetElements != null) {
if (targetElements.length == 0) {
return null;
}
else if (targetElements.length == 1) {
if (targetElements[0] != resolvedElement && elementAtPointer != null && targetElements[0].isPhysical()) {
return ref != null ? new InfoSingle(ref, targetElements[0]) : new InfoSingle(elementAtPointer, targetElements[0]);
}
}
else {
return elementAtPointer != null ? new InfoMultiple(elementAtPointer) : null;
}
}
if (resolvedElements.size() == 1) {
return new InfoSingle(ref, resolvedElements.get(0));
}
else if (resolvedElements.size() > 1) {
return elementAtPointer != null ? new InfoMultiple(elementAtPointer, ref) : null;
}
}
else if (browseMode == BrowseMode.Implementation) {
final PsiElement element = TargetElementUtilBase.getInstance().findTargetElement(editor, ImplementationSearcher.getFlags(), offset);
PsiElement[] targetElements = new ImplementationSearcher() {
@Override
@NotNull
protected PsiElement[] searchDefinitions(final PsiElement element, Editor editor) {
final List<PsiElement> found = new ArrayList<PsiElement>(2);
DefinitionsScopedSearch.search(element, getSearchScope(element, editor)).forEach(new Processor<PsiElement>() {
@Override
public boolean process(final PsiElement psiElement) {
found.add(psiElement);
return found.size() != 2;
}
});
return PsiUtilCore.toPsiElementArray(found);
}
}.searchImplementations(editor, element, offset);
if (targetElements.length > 1) {
PsiElement elementAtPointer = file.findElementAt(offset);
if (elementAtPointer != null) {
return new InfoMultiple(elementAtPointer);
}
return null;
}
if (targetElements.length == 1) {
Navigatable descriptor = EditSourceUtil.getDescriptor(targetElements[0]);
if (descriptor == null || !descriptor.canNavigate()) {
return null;
}
targetElement = targetElements[0];
}
}
if (targetElement != null && targetElement.isPhysical()) {
PsiElement elementAtPointer = file.findElementAt(offset);
if (elementAtPointer != null) {
return new InfoSingle(elementAtPointer, targetElement);
}
}
return null;
}
@NotNull
private static List<PsiElement> resolve(@NotNull PsiReference ref) {
// IDEA-56727 try resolve first as in GotoDeclarationAction
PsiElement resolvedElement = ref.resolve();
if (resolvedElement == null && ref instanceof PsiPolyVariantReference) {
List<PsiElement> result = new ArrayList<PsiElement>();
final ResolveResult[] psiElements = ((PsiPolyVariantReference)ref).multiResolve(false);
for (ResolveResult resolveResult : psiElements) {
if (resolveResult.getElement() != null) {
result.add(resolveResult.getElement());
}
}
return result;
}
return resolvedElement == null ? Collections.<PsiElement>emptyList() : Collections.singletonList(resolvedElement);
}
private void disposeHighlighter() {
if (myHighlighter != null) {
myHighlighter.uninstall();
HintManager.getInstance().hideAllHints();
myHighlighter = null;
}
}
private void fulfillDocInfo(@NotNull final String header,
@NotNull final DocumentationProvider provider,
@NotNull final PsiElement originalElement,
@NotNull final PsiElement anchorElement,
@NotNull final Consumer<String> newTextConsumer,
@NotNull final LightweightHint hint)
{
myDocAlarm.cancelAllRequests();
myDocAlarm.addRequest(new Runnable() {
@Override
public void run() {
final Ref<String> fullTextRef = new Ref<String>();
final Ref<String> qualifiedNameRef = new Ref<String>();
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
try {
fullTextRef.set(provider.generateDoc(anchorElement, originalElement));
}
catch (IndexNotReadyException e) {
fullTextRef.set("Documentation is not available while indexing is in progress");
}
if (anchorElement instanceof PsiQualifiedNamedElement) {
qualifiedNameRef.set(((PsiQualifiedNamedElement)anchorElement).getQualifiedName());
}
}
});
String fullText = fullTextRef.get();
if (fullText == null) {
return;
}
final String updatedText = DocPreviewUtil.buildPreview(header, qualifiedNameRef.get(), fullText);
final String newHtml = HintUtil.prepareHintText(updatedText, HintUtil.getInformationHint());
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
// There is a possible case that quick doc control width is changed, e.g. it contained text
// like 'public final class String implements java.io.Serializable, java.lang.Comparable<java.lang.String>' and
// new text replaces fully-qualified class names by hyperlinks with short name.
// That's why we might need to update the control size. We assume that the hint component is located at the
// layered pane, so, the algorithm is to find an ancestor layered pane and apply new size for the target component.
JComponent component = hint.getComponent();
Dimension oldSize = component.getPreferredSize();
newTextConsumer.consume(newHtml);
final int widthIncrease;
if (component instanceof QuickDocInfoPane) {
int buttonWidth = ((QuickDocInfoPane)component).getButtonWidth();
widthIncrease = calculateWidthIncrease(buttonWidth, updatedText);
}
else {
widthIncrease = 0;
}
if (oldSize == null) {
return;
}
Dimension newSize = component.getPreferredSize();
if (newSize.width + widthIncrease == oldSize.width) {
return;
}
component.setPreferredSize(new Dimension(newSize.width + widthIncrease, newSize.height));
// We're assuming here that there are two possible hint representation modes: popup and layered pane.
if (hint.isRealPopup()) {
TooltipProvider tooltipProvider = myTooltipProvider;
if (tooltipProvider != null) {
// There is a possible case that 'raw' control was rather wide but the 'rich' one is narrower. That's why we try to
// re-show the hint here. Benefits: there is a possible case that we'll be able to show nice layered pane-based balloon;
// the popup will be re-positioned according to the new width.
hint.hide();
tooltipProvider.showHint(new LightweightHint(component));
}
else {
component.setPreferredSize(new Dimension(newSize.width + widthIncrease, oldSize.height));
hint.pack();
}
return;
}
Container topLevelLayeredPaneChild = null;
boolean adjustBounds = false;
for (Container current = component.getParent(); current != null; current = current.getParent()) {
if (current instanceof JLayeredPane) {
adjustBounds = true;
break;
}
else {
topLevelLayeredPaneChild = current;
}
}
if (adjustBounds && topLevelLayeredPaneChild != null) {
Rectangle bounds = topLevelLayeredPaneChild.getBounds();
topLevelLayeredPaneChild.setBounds(bounds.x, bounds.y, bounds.width + newSize.width + widthIncrease - oldSize.width, bounds.height);
}
}
});
}
}, 0);
}
/**
* It's possible that we need to expand quick doc control's width in order to provide better visual representation
* (see http://youtrack.jetbrains.com/issue/IDEA-101425). This method calculates that width expand.
*
* @param buttonWidth icon button's width
* @param updatedText text which will be should at the quick doc control
* @return width increase to apply to the target quick doc control (zero if no additional width increase is required)
*/
private static int calculateWidthIncrease(int buttonWidth, String updatedText) {
int maxLineWidth = 0;
TIntArrayList lineWidths = new TIntArrayList();
for (String lineText : StringUtil.split(updatedText, "<br/>")) {
String html = HintUtil.prepareHintText(lineText, HintUtil.getInformationHint());
int width = new JLabel(html).getPreferredSize().width;
maxLineWidth = Math.max(maxLineWidth, width);
lineWidths.add(width);
}
if (!lineWidths.isEmpty()) {
int firstLineAvailableTrailingWidth = maxLineWidth - lineWidths.get(0);
if (firstLineAvailableTrailingWidth >= buttonWidth) {
return 0;
}
else {
return buttonWidth - firstLineAvailableTrailingWidth;
}
}
return 0;
}
private class TooltipProvider {
@NotNull private final Editor myEditor;
@NotNull private final LogicalPosition myPosition;
private BrowseMode myBrowseMode;
private boolean myDisposed;
private final ProgressIndicator myProgress = new ProgressIndicatorBase();
TooltipProvider(@NotNull Editor editor, @NotNull LogicalPosition pos) {
myEditor = editor;
myPosition = pos;
}
void dispose() {
myDisposed = true;
myProgress.cancel();
}
public BrowseMode getBrowseMode() {
return myBrowseMode;
}
void execute(@NotNull BrowseMode browseMode) {
myBrowseMode = browseMode;
Document document = myEditor.getDocument();
final PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
if (file == null) return;
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
if (EditorUtil.inVirtualSpace(myEditor, myPosition)) {
return;
}
final int offset = myEditor.logicalPositionToOffset(myPosition);
int selStart = myEditor.getSelectionModel().getSelectionStart();
int selEnd = myEditor.getSelectionModel().getSelectionEnd();
if (offset >= selStart && offset < selEnd) return;
ProgressIndicatorUtils.scheduleWithWriteActionPriority(myProgress, new ReadTask() {
@Override
public void computeInReadAction(@NotNull ProgressIndicator indicator) {
doExecute(file, offset);
}
@Override
public void onCanceled(@NotNull ProgressIndicator indicator) {
}
});
}
private void doExecute(@NotNull PsiFile file, int offset) {
final Info info;
try {
info = getInfoAt(myEditor, file, offset, myBrowseMode);
if (info == null) return;
}
catch (IndexNotReadyException e) {
showDumbModeNotification(myProject);
return;
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
if (myDisposed || myEditor.isDisposed() || !myEditor.getComponent().isShowing()) return;
showHint(info);
}
});
}
private void showHint(@NotNull Info info) {
if (myDisposed || myEditor.isDisposed()) return;
Component internalComponent = myEditor.getContentComponent();
if (myHighlighter != null) {
if (!info.isSimilarTo(myHighlighter.getStoredInfo())) {
disposeHighlighter();
}
else {
// highlighter already set
internalComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return;
}
}
if (!info.isValid(myEditor.getDocument())) {
return;
}
myHighlighter = installHighlighterSet(info, myEditor);
DocInfo docInfo = info.getInfo();
if (docInfo.text == null) return;
if (myDocumentationManager.hasActiveDockedDocWindow()) {
info.showDocInfo(myDocumentationManager);
}
HyperlinkListener hyperlinkListener = docInfo.docProvider == null
? null
: new QuickDocHyperlinkListener(docInfo.docProvider, info.myElementAtPointer);
final Ref<QuickDocInfoPane> quickDocPaneRef = new Ref<QuickDocInfoPane>();
MouseListener mouseListener = new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
QuickDocInfoPane pane = quickDocPaneRef.get();
if (pane != null) {
pane.mouseEntered(e);
}
}
@Override
public void mouseExited(MouseEvent e) {
QuickDocInfoPane pane = quickDocPaneRef.get();
if (pane != null) {
pane.mouseExited(e);
}
}
@Override
public void mouseClicked(MouseEvent e) {
}
};
Ref<Consumer<String>> newTextConsumerRef = new Ref<Consumer<String>>();
JComponent label = HintUtil.createInformationLabel(docInfo.text, hyperlinkListener, mouseListener, newTextConsumerRef);
Consumer<String> newTextConsumer = newTextConsumerRef.get();
QuickDocInfoPane quickDocPane = null;
if (docInfo.documentationAnchor != null) {
quickDocPane = new QuickDocInfoPane(docInfo.documentationAnchor, info.myElementAtPointer, label);
quickDocPaneRef.set(quickDocPane);
}
JComponent hintContent = quickDocPane == null ? label : quickDocPane;
final LightweightHint hint = new LightweightHint(hintContent);
myHint = hint;
hint.addHintListener(new HintListener() {
@Override
public void hintHidden(EventObject event) {
myHint = null;
}
});
myDocAlarm.cancelAllRequests();
if (newTextConsumer != null && docInfo.docProvider != null && docInfo.documentationAnchor != null) {
fulfillDocInfo(docInfo.text, docInfo.docProvider, info.myElementAtPointer, docInfo.documentationAnchor, newTextConsumer, hint);
}
showHint(hint);
}
public void showHint(@NotNull LightweightHint hint) {
final HintManagerImpl hintManager = HintManagerImpl.getInstanceImpl();
Point p = HintManagerImpl.getHintPosition(hint, myEditor, myPosition, HintManager.ABOVE);
hintManager.showEditorHint(hint, myEditor, p,
HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING,
0, false, HintManagerImpl.createHintHint(myEditor, p, hint, HintManager.ABOVE).setContentActive(false));
}
}
@NotNull
private HighlightersSet installHighlighterSet(@NotNull Info info, @NotNull Editor editor) {
final JComponent internalComponent = editor.getContentComponent();
internalComponent.addKeyListener(myEditorKeyListener);
editor.getScrollingModel().addVisibleAreaListener(myVisibleAreaListener);
final Cursor cursor = internalComponent.getCursor();
internalComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
myFileEditorManager.addFileEditorManagerListener(myFileEditorManagerListener);
List<RangeHighlighter> highlighters = new ArrayList<RangeHighlighter>();
TextAttributes attributes = myEditorColorsManager.getGlobalScheme().getAttributes(EditorColors.REFERENCE_HYPERLINK_COLOR);
for (TextRange range : info.getRanges()) {
TextAttributes attr = NavigationUtil.patchAttributesColor(attributes, range, editor);
final RangeHighlighter highlighter = editor.getMarkupModel().addRangeHighlighter(range.getStartOffset(), range.getEndOffset(),
HighlighterLayer.SELECTION + 1,
attr,
HighlighterTargetArea.EXACT_RANGE);
highlighters.add(highlighter);
}
return new HighlightersSet(highlighters, editor, cursor, info);
}
private class HighlightersSet {
@NotNull private final List<RangeHighlighter> myHighlighters;
@NotNull private final Editor myHighlighterView;
@NotNull private final Cursor myStoredCursor;
@NotNull private final Info myStoredInfo;
private HighlightersSet(@NotNull List<RangeHighlighter> highlighters,
@NotNull Editor highlighterView,
@NotNull Cursor storedCursor,
@NotNull Info storedInfo) {
myHighlighters = highlighters;
myHighlighterView = highlighterView;
myStoredCursor = storedCursor;
myStoredInfo = storedInfo;
}
public void uninstall() {
for (RangeHighlighter highlighter : myHighlighters) {
highlighter.dispose();
}
Component internalComponent = myHighlighterView.getContentComponent();
internalComponent.setCursor(myStoredCursor);
internalComponent.removeKeyListener(myEditorKeyListener);
myHighlighterView.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener);
myFileEditorManager.removeFileEditorManagerListener(myFileEditorManagerListener);
}
@NotNull
public Info getStoredInfo() {
return myStoredInfo;
}
}
private static class DocInfo {
public static final DocInfo EMPTY = new DocInfo(null, null, null);
@Nullable public final String text;
@Nullable public final DocumentationProvider docProvider;
@Nullable public final PsiElement documentationAnchor;
DocInfo(@Nullable String text, @Nullable DocumentationProvider provider, @Nullable PsiElement documentationAnchor) {
this.text = text;
docProvider = provider;
this.documentationAnchor = documentationAnchor;
}
}
private class QuickDocInfoPane extends JBLayeredPane {
private static final int BUTTON_HGAP = 5;
@NotNull private final List<JComponent> myButtons = new ArrayList<JComponent>();
@NotNull private final JComponent myBaseDocControl;
private final int myMinWidth;
private final int myMinHeight;
private final int myButtonWidth;
QuickDocInfoPane(@NotNull PsiElement documentationAnchor,
@NotNull PsiElement elementUnderMouse,
@NotNull JComponent baseDocControl) {
myBaseDocControl = baseDocControl;
PresentationFactory presentationFactory = new PresentationFactory();
for (AbstractDocumentationTooltipAction action : ourTooltipActions) {
Icon icon = action.getTemplatePresentation().getIcon();
Dimension minSize = new Dimension(icon.getIconWidth(), icon.getIconHeight());
myButtons.add(new ActionButton(action, presentationFactory.getPresentation(action), IdeTooltipManager.IDE_TOOLTIP_PLACE, minSize));
action.setDocInfo(documentationAnchor, elementUnderMouse);
}
Collections.reverse(myButtons);
setPreferredSize(baseDocControl.getPreferredSize());
setMaximumSize(baseDocControl.getMaximumSize());
setMinimumSize(baseDocControl.getMinimumSize());
setBackground(baseDocControl.getBackground());
add(baseDocControl, Integer.valueOf(0));
int minWidth = 0;
int minHeight = 0;
int buttonWidth = 0;
for (JComponent button : myButtons) {
button.setBorder(null);
button.setBackground(baseDocControl.getBackground());
add(button, Integer.valueOf(1));
button.setVisible(false);
Dimension preferredSize = button.getPreferredSize();
minWidth += preferredSize.width;
minHeight = Math.max(minHeight, preferredSize.height);
buttonWidth = Math.max(buttonWidth, preferredSize.width);
}
myButtonWidth = buttonWidth;
int margin = 2;
myMinWidth = minWidth + margin * 2 + (myButtons.size() - 1) * BUTTON_HGAP;
myMinHeight = minHeight + margin * 2;
}
public int getButtonWidth() {
return myButtonWidth;
}
@Override
public Dimension getPreferredSize() {
return expandIfNecessary(myBaseDocControl.getPreferredSize());
}
@Override
public void setPreferredSize(Dimension preferredSize) {
super.setPreferredSize(preferredSize);
myBaseDocControl.setPreferredSize(preferredSize);
}
@Override
public Dimension getMinimumSize() {
return expandIfNecessary(myBaseDocControl.getMinimumSize());
}
@Override
public Dimension getMaximumSize() {
return expandIfNecessary(myBaseDocControl.getMaximumSize());
}
@NotNull
private Dimension expandIfNecessary(@NotNull Dimension base) {
if (base.width >= myMinWidth && base.height >= myMinHeight) {
return base;
}
return new Dimension(Math.max(myMinWidth, base.width), Math.max(myMinHeight, base.height));
}
@Override
public void doLayout() {
Rectangle bounds = getBounds();
myBaseDocControl.setBounds(new Rectangle(0, 0, bounds.width, bounds.height));
int x = bounds.width;
for (JComponent button : myButtons) {
Dimension buttonSize = button.getPreferredSize();
x -= buttonSize.width;
button.setBounds(x, 0, buttonSize.width, buttonSize.height);
x -= BUTTON_HGAP;
}
}
public void mouseEntered(@NotNull MouseEvent e) {
processStateChangeIfNecessary(e.getLocationOnScreen(), true);
}
public void mouseExited(@NotNull MouseEvent e) {
processStateChangeIfNecessary(e.getLocationOnScreen(), false);
}
private void processStateChangeIfNecessary(@NotNull Point mouseScreenLocation, boolean mouseEntered) {
// Don't show 'view quick doc' buttons if docked quick doc control is already active.
if (myDocumentationManager.hasActiveDockedDocWindow()) {
return;
}
// Skip event triggered when mouse leaves action button area.
if (!mouseEntered && new Rectangle(getLocationOnScreen(), getSize()).contains(mouseScreenLocation)) {
return;
}
for (JComponent button : myButtons) {
button.setVisible(mouseEntered);
}
}
}
private class QuickDocHyperlinkListener implements HyperlinkListener {
@NotNull private final DocumentationProvider myProvider;
@NotNull private final PsiElement myContext;
QuickDocHyperlinkListener(@NotNull DocumentationProvider provider, @NotNull PsiElement context) {
myProvider = provider;
myContext = context;
}
@Override
public void hyperlinkUpdate(@NotNull HyperlinkEvent e) {
if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) {
return;
}
String description = e.getDescription();
if (StringUtil.isEmpty(description) || !description.startsWith(DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL)) {
return;
}
String elementName = e.getDescription().substring(DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL.length());
final PsiElement targetElement = myProvider.getDocumentationElementForLink(PsiManager.getInstance(myProject), elementName, myContext);
if (targetElement != null) {
LightweightHint hint = myHint;
if (hint != null) {
hint.hide(true);
}
myDocumentationManager.showJavaDocInfo(targetElement, myContext, null);
}
}
}
}