/*
* 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.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.hint.ElementLocationUtil;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.hint.HintUtil;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.ide.actions.BaseNavigateToSourceAction;
import com.intellij.ide.actions.ExternalJavaDocAction;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.lang.documentation.DocumentationProvider;
import com.intellij.lang.documentation.ExternalDocumentationProvider;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.impl.ActionButton;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.colors.ColorKey;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.options.FontSize;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.ex.WindowManagerEx;
import com.intellij.pom.Navigatable;
import com.intellij.psi.PsiElement;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.JBColor;
import com.intellij.ui.SideBorder;
import com.intellij.ui.components.JBLayeredPane;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.Consumer;
import com.intellij.util.containers.HashMap;
import com.intellij.util.ui.GraphicsUtil;
import com.intellij.util.ui.JBDimension;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.accessibility.ScreenReader;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.*;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import java.awt.*;
import java.awt.event.*;
import java.net.URL;
import java.util.*;
import java.util.List;
public class DocumentationComponent extends JPanel implements Disposable, DataProvider {
private static final Logger LOG = Logger.getInstance(DocumentationComponent.class);
private static final Color DOCUMENTATION_COLOR = new JBColor(new Color(0xf6f6f6), new Color(0x4d4f51));
public static final ColorKey COLOR_KEY = ColorKey.createColorKey("DOCUMENTATION_COLOR", DOCUMENTATION_COLOR);
private static final Highlighter.HighlightPainter LINK_HIGHLIGHTER = new LinkHighlighter();
@NonNls private static final String DOCUMENTATION_TOPIC_ID = "reference.toolWindows.Documentation";
private static final int PREFERRED_WIDTH_EM = 37;
private static final int PREFERRED_HEIGHT_MIN_EM = 7;
private static final int PREFERRED_HEIGHT_MAX_EM = 20;
private DocumentationManager myManager;
private SmartPsiElementPointer myElement;
private long myModificationCount;
private static final String QUICK_DOC_FONT_SIZE_PROPERTY = "quick.doc.font.size";
private final Stack<Context> myBackStack = new Stack<>();
private final Stack<Context> myForwardStack = new Stack<>();
private final ActionToolbar myToolBar;
private volatile boolean myIsEmpty;
private boolean myIsShown;
private final JLabel myElementLabel;
private final MutableAttributeSet myFontSizeStyle = new SimpleAttributeSet();
private JSlider myFontSizeSlider;
private final JComponent mySettingsPanel;
private final MyShowSettingsButton myShowSettingsButton;
private boolean myIgnoreFontSizeSliderChange;
private String myEffectiveExternalUrl;
private final MyDictionary<String, Image> myImageProvider = new MyDictionary<String, Image>() {
@Override
public Image get(Object key) {
if (myManager == null || key == null) return null;
PsiElement element = getElement();
if (element == null) return null;
URL url = (URL)key;
Image inMemory = myManager.getElementImage(element, url.toExternalForm());
if (inMemory != null) {
return inMemory;
}
/*Url parsedUrl = Urls.parseEncoded(url.toExternalForm());
BuiltInServerManager builtInServerManager = BuiltInServerManager.getInstance();
if (parsedUrl != null && builtInServerManager.isOnBuiltInWebServer(parsedUrl)) {
try {
url = new URL(builtInServerManager.addAuthToken(parsedUrl).toExternalForm());
}
catch (MalformedURLException e) {
LOG.warn(e);
}
} */
return Toolkit.getDefaultToolkit().createImage(url);
}
};
private static class Context {
private final SmartPsiElementPointer element;
private final String text;
private final String externalUrl;
private final Rectangle viewRect;
private final int highlightedLink;
public Context(SmartPsiElementPointer element, String text, String externalUrl, Rectangle viewRect, int highlightedLink) {
this.element = element;
this.text = text;
this.externalUrl = externalUrl;
this.viewRect = viewRect;
this.highlightedLink = highlightedLink;
}
}
private final JScrollPane myScrollPane;
private final JEditorPane myEditorPane;
private String myText; // myEditorPane.getText() surprisingly crashes.., let's cache the text
private final JPanel myControlPanel;
private boolean myControlPanelVisible;
private final ExternalDocAction myExternalDocAction;
private Consumer<PsiElement> myNavigateCallback;
private int myHighlightedLink = -1;
private Object myHighlightingTag;
private JBPopup myHint;
private final Map<KeyStroke, ActionListener> myKeyboardActions = new HashMap<>();
@Override
public boolean requestFocusInWindow() {
// With a screen reader active, set the focus directly to the editor because
// it makes it easier for users to read/navigate the documentation contents.
if (ScreenReader.isActive())
return myEditorPane.requestFocusInWindow();
else
return myScrollPane.requestFocusInWindow();
}
@Override
public void requestFocus() {
// With a screen reader active, set the focus directly to the editor because
// it makes it easier for users to read/navigate the documentation contents.
IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> {
if (ScreenReader.isActive()) {
IdeFocusManager.getGlobalInstance().requestFocus(myEditorPane, true);
}
else {
IdeFocusManager.getGlobalInstance().requestFocus(myScrollPane, true);
}
});
}
public DocumentationComponent(final DocumentationManager manager, final AnAction[] additionalActions) {
myManager = manager;
myIsEmpty = true;
myIsShown = false;
myEditorPane = new JEditorPane(UIUtil.HTML_MIME, "") {
@Override
public Dimension getPreferredScrollableViewportSize() {
int em = myEditorPane.getFont().getSize();
int prefWidth = PREFERRED_WIDTH_EM * em;
int prefHeightMin = PREFERRED_HEIGHT_MIN_EM * em;
int prefHeightMax = PREFERRED_HEIGHT_MAX_EM * em;
if (getWidth() == 0 || getHeight() == 0) {
setSize(prefWidth, prefHeightMax);
}
Insets ins = myEditorPane.getInsets();
View rootView = myEditorPane.getUI().getRootView(myEditorPane);
rootView.setSize(prefWidth, prefHeightMax); // Necessary! Without this line, the size won't increase when the content does
int prefHeight = (int)rootView.getPreferredSpan(View.Y_AXIS) + ins.bottom + ins.top +
myScrollPane.getHorizontalScrollBar().getMaximumSize().height;
prefHeight = Math.max(prefHeightMin, Math.min(prefHeightMax, prefHeight));
return new Dimension(prefWidth, prefHeight);
}
{
enableEvents(AWTEvent.KEY_EVENT_MASK);
}
@Override
protected void processKeyEvent(KeyEvent e) {
KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
ActionListener listener = myKeyboardActions.get(keyStroke);
if (listener != null) {
listener.actionPerformed(new ActionEvent(DocumentationComponent.this, 0, ""));
e.consume();
return;
}
super.processKeyEvent(e);
}
@Override
protected void paintComponent(Graphics g) {
GraphicsUtil.setupAntialiasing(g);
super.paintComponent(g);
}
@Override
public void setDocument(Document doc) {
super.setDocument(doc);
if (doc instanceof StyledDocument) {
doc.putProperty("imageCache", myImageProvider);
}
}
};
DataProvider helpDataProvider = new DataProvider() {
@Override
public Object getData(@NonNls String dataId) {
return PlatformDataKeys.HELP_ID.is(dataId) ? DOCUMENTATION_TOPIC_ID : null;
}
};
myEditorPane.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, helpDataProvider);
myText = "";
myEditorPane.setEditable(false);
if (ScreenReader.isActive()) {
// Note: Making the caret visible is merely for convenience
myEditorPane.getCaret().setVisible(true);
}
myEditorPane.setBackground(HintUtil.INFORMATION_COLOR);
myEditorPane.setEditorKit(UIUtil.getHTMLEditorKit(false));
myScrollPane = new JBScrollPane(myEditorPane) {
@Override
protected void processMouseWheelEvent(MouseWheelEvent e) {
if (!EditorSettingsExternalizable.getInstance().isWheelFontChangeEnabled() || !EditorUtil.isChangeFontSize(e)) {
super.processMouseWheelEvent(e);
return;
}
int rotation = e.getWheelRotation();
if (rotation == 0) return;
int change = Math.abs(rotation);
boolean increase = rotation <= 0;
FontSize newFontSize = getQuickDocFontSize();
for (; change > 0; change--) {
if (increase) {
newFontSize = newFontSize.larger();
}
else {
newFontSize = newFontSize.smaller();
}
}
if (newFontSize == getQuickDocFontSize()) {
return;
}
setQuickDocFontSize(newFontSize);
applyFontSize();
setFontSizeSliderSize(newFontSize);
}
};
myScrollPane.setBorder(null);
myScrollPane.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, helpDataProvider);
final MouseListener mouseAdapter = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
myManager.requestFocus();
myShowSettingsButton.hideSettings();
}
};
myEditorPane.addMouseListener(mouseAdapter);
Disposer.register(this, new Disposable() {
@Override
public void dispose() {
myEditorPane.removeMouseListener(mouseAdapter);
}
});
final FocusListener focusAdapter = new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
Component previouslyFocused = WindowManagerEx.getInstanceEx().getFocusedComponent(manager.getProject(getElement()));
if (previouslyFocused != myEditorPane) {
if (myHint != null && !myHint.isDisposed()) myHint.cancel();
}
}
};
myEditorPane.addFocusListener(focusAdapter);
Disposer.register(this, new Disposable() {
@Override
public void dispose() {
myEditorPane.removeFocusListener(focusAdapter);
}
});
setLayout(new BorderLayout());
JLayeredPane layeredPane = new JBLayeredPane() {
@Override
public void doLayout() {
final Rectangle r = getBounds();
for (Component component : getComponents()) {
if (component instanceof JScrollPane) {
component.setBounds(0, 0, r.width, r.height);
}
else {
int insets = 2;
Dimension d = component.getPreferredSize();
component.setBounds(r.width - d.width - insets, insets, d.width, d.height);
}
}
}
@Override
public Dimension getPreferredSize() {
Dimension editorPaneSize = myEditorPane.getPreferredScrollableViewportSize();
Dimension controlPanelSize = myControlPanel.getPreferredSize();
return getSize(editorPaneSize, controlPanelSize);
}
@Override
public Dimension getMinimumSize() {
Dimension editorPaneSize = new JBDimension(20, 20);
Dimension controlPanelSize = myControlPanel.getMinimumSize();
return getSize(editorPaneSize, controlPanelSize);
}
private Dimension getSize(Dimension editorPaneSize, Dimension controlPanelSize) {
return new Dimension(Math.max(editorPaneSize.width, controlPanelSize.width), editorPaneSize.height + controlPanelSize.height);
}
};
layeredPane.add(myScrollPane);
layeredPane.setLayer(myScrollPane, 0);
mySettingsPanel = createSettingsPanel();
layeredPane.add(mySettingsPanel);
layeredPane.setLayer(mySettingsPanel, JLayeredPane.POPUP_LAYER);
add(layeredPane, BorderLayout.CENTER);
setOpaque(true);
myScrollPane.setViewportBorder(JBScrollPane.createIndentBorder());
final DefaultActionGroup actions = new DefaultActionGroup();
final BackAction back = new BackAction();
final ForwardAction forward = new ForwardAction();
EditDocumentationSourceAction edit = new EditDocumentationSourceAction();
actions.add(back);
actions.add(forward);
actions.add(myExternalDocAction = new ExternalDocAction());
actions.add(edit);
try {
String backKey = ScreenReader.isActive() ? "alt LEFT" : "LEFT";
CustomShortcutSet backShortcutSet = new CustomShortcutSet(KeyboardShortcut.fromString(backKey),
KeymapUtil.parseMouseShortcut("button4"));
String forwardKey = ScreenReader.isActive() ? "alt RIGHT" : "RIGHT";
CustomShortcutSet forwardShortcutSet = new CustomShortcutSet(KeyboardShortcut.fromString(forwardKey),
KeymapUtil.parseMouseShortcut("button5"));
back.registerCustomShortcutSet(backShortcutSet, this);
forward.registerCustomShortcutSet(forwardShortcutSet, this);
// mouse actions are checked only for exact component over which click was performed,
// so we need to register shortcuts for myEditorPane as well
back.registerCustomShortcutSet(backShortcutSet, myEditorPane);
forward.registerCustomShortcutSet(forwardShortcutSet, myEditorPane);
}
catch (InvalidDataException e) {
LOG.error(e);
}
myExternalDocAction.registerCustomShortcutSet(CustomShortcutSet.fromString("UP"), this);
edit.registerCustomShortcutSet(CommonShortcuts.getEditSource(), this);
if (additionalActions != null) {
for (final AnAction action : additionalActions) {
actions.add(action);
ShortcutSet shortcutSet = action.getShortcutSet();
if (shortcutSet != null) {
action.registerCustomShortcutSet(shortcutSet, this);
}
}
}
new NextLinkAction().registerCustomShortcutSet(CustomShortcutSet.fromString("TAB"), this);
new PreviousLinkAction().registerCustomShortcutSet(CustomShortcutSet.fromString("shift TAB"), this);
new ActivateLinkAction().registerCustomShortcutSet(CustomShortcutSet.fromString("ENTER"), this);
myToolBar = ActionManager.getInstance().createActionToolbar(ActionPlaces.JAVADOC_TOOLBAR, actions, true);
myControlPanel = new JPanel(new BorderLayout(5, 5));
myControlPanel.setBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM));
myElementLabel = new JLabel();
myElementLabel.setMinimumSize(new Dimension(100, 0)); // do not recalculate size according to the text
myControlPanel.add(myToolBar.getComponent(), BorderLayout.WEST);
myControlPanel.add(myElementLabel, BorderLayout.CENTER);
myControlPanel.add(myShowSettingsButton = new MyShowSettingsButton(), BorderLayout.EAST);
myControlPanelVisible = false;
final HyperlinkListener hyperlinkListener = new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
HyperlinkEvent.EventType type = e.getEventType();
if (type == HyperlinkEvent.EventType.ACTIVATED) {
manager.navigateByLink(DocumentationComponent.this, e.getDescription());
}
}
};
myEditorPane.addHyperlinkListener(hyperlinkListener);
Disposer.register(this, new Disposable() {
@Override
public void dispose() {
myEditorPane.removeHyperlinkListener(hyperlinkListener);
}
});
registerActions();
updateControlState();
}
public DocumentationComponent(final DocumentationManager manager) {
this(manager, null);
}
@Override
public Object getData(@NonNls String dataId) {
if (DocumentationManager.SELECTED_QUICK_DOC_TEXT.getName().equals(dataId)) {
// Javadocs often contain symbols (non-breakable white space). We don't want to copy them as is and replace
// with raw white spaces. See IDEA-86633 for more details.
String selectedText = myEditorPane.getSelectedText();
return selectedText == null? null : selectedText.replace((char)160, ' ');
}
return null;
}
private JComponent createSettingsPanel() {
JPanel result = new JPanel(new FlowLayout(FlowLayout.RIGHT, 3, 0));
result.add(new JLabel(ApplicationBundle.message("label.font.size")));
myFontSizeSlider = new JSlider(SwingConstants.HORIZONTAL, 0, FontSize.values().length - 1, 3);
myFontSizeSlider.setMinorTickSpacing(1);
myFontSizeSlider.setPaintTicks(true);
myFontSizeSlider.setPaintTrack(true);
myFontSizeSlider.setSnapToTicks(true);
UIUtil.setSliderIsFilled(myFontSizeSlider, true);
result.add(myFontSizeSlider);
result.setBorder(BorderFactory.createLineBorder(JBColor.border(), 1));
myFontSizeSlider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (myIgnoreFontSizeSliderChange) {
return;
}
setQuickDocFontSize(FontSize.values()[myFontSizeSlider.getValue()]);
applyFontSize();
}
});
String tooltipText = ApplicationBundle.message("quickdoc.tooltip.font.size.by.wheel");
result.setToolTipText(tooltipText);
myFontSizeSlider.setToolTipText(tooltipText);
result.setVisible(false);
result.setOpaque(true);
myFontSizeSlider.setOpaque(true);
return result;
}
@NotNull
public static FontSize getQuickDocFontSize() {
String strValue = PropertiesComponent.getInstance().getValue(QUICK_DOC_FONT_SIZE_PROPERTY);
if (strValue != null) {
try {
return FontSize.valueOf(strValue);
}
catch (IllegalArgumentException iae) {
// ignore, fall back to default font.
}
}
return FontSize.SMALL;
}
public void setQuickDocFontSize(@NotNull FontSize fontSize) {
PropertiesComponent.getInstance().setValue(QUICK_DOC_FONT_SIZE_PROPERTY, fontSize.toString());
}
private void setFontSizeSliderSize(FontSize fontSize) {
myIgnoreFontSizeSliderChange = true;
try {
FontSize[] sizes = FontSize.values();
for (int i = 0; i < sizes.length; i++) {
if (fontSize == sizes[i]) {
myFontSizeSlider.setValue(i);
break;
}
}
}
finally {
myIgnoreFontSizeSliderChange = false;
}
}
public boolean isEmpty() {
return myIsEmpty;
}
public void startWait() {
myIsEmpty = true;
}
private void setControlPanelVisible(boolean visible) {
if (visible == myControlPanelVisible) return;
if (visible) {
add(myControlPanel, BorderLayout.NORTH);
}
else {
remove(myControlPanel);
}
myControlPanelVisible = visible;
}
public void setHint(JBPopup hint) {
myHint = hint;
}
public JBPopup getHint() {
return myHint;
}
public JComponent getComponent() {
return myEditorPane;
}
@Nullable
public PsiElement getElement() {
return myElement != null ? myElement.getElement() : null;
}
private void setElement(SmartPsiElementPointer element) {
myElement = element;
myModificationCount = getCurrentModificationCount();
}
public boolean isUpToDate() {
return getElement() != null && myModificationCount == getCurrentModificationCount();
}
private long getCurrentModificationCount() {
return myElement != null ? PsiModificationTracker.SERVICE.getInstance(myElement.getProject()).getModificationCount() : -1;
}
public void setNavigateCallback(Consumer<PsiElement> navigateCallback) {
myNavigateCallback = navigateCallback;
}
public void setText(String text, @Nullable PsiElement element, boolean clearHistory) {
setText(text, element, false, clearHistory);
}
public void setText(String text, PsiElement element, boolean clean, boolean clearHistory) {
if (clean && myElement != null) {
myBackStack.push(saveContext());
myForwardStack.clear();
}
updateControlState();
setData(element, text, clearHistory, null);
if (clean) {
myIsEmpty = false;
}
if (clearHistory) clearHistory();
}
public void replaceText(String text, PsiElement element) {
PsiElement current = getElement();
if (current == null || !current.getManager().areElementsEquivalent(current, element)) return;
setText(text, element, false);
if (!myBackStack.empty()) myBackStack.pop();
}
private void clearHistory() {
myForwardStack.clear();
myBackStack.clear();
}
public void setData(PsiElement _element, String text, final boolean clearHistory, String effectiveExternalUrl) {
setData(_element, text, clearHistory, effectiveExternalUrl, null);
}
public void setData(PsiElement _element, String text, final boolean clearHistory, String effectiveExternalUrl, String ref) {
if (myElement != null) {
myBackStack.push(saveContext());
myForwardStack.clear();
}
myEffectiveExternalUrl = effectiveExternalUrl;
final SmartPsiElementPointer element = _element != null && _element.isValid()
? SmartPointerManager.getInstance(_element.getProject()).createSmartPsiElementPointer(_element)
: null;
if (element != null) {
setElement(element);
}
myIsEmpty = false;
updateControlState();
setDataInternal(element, text, new Rectangle(0, 0), ref);
if (clearHistory) clearHistory();
}
private void setDataInternal(SmartPsiElementPointer element, String text, final Rectangle viewRect, final String ref) {
setElement(element);
highlightLink(-1);
myEditorPane.setText(text);
applyFontSize();
if (!myIsShown && myHint != null && !ApplicationManager.getApplication().isUnitTestMode()) {
myManager.showHint(myHint);
myIsShown = true;
}
myText = text;
//noinspection SSBasedInspection
SwingUtilities.invokeLater(() -> {
myEditorPane.scrollRectToVisible(viewRect); // if ref is defined but is not found in document, this provides a default location
if (ref != null) {
myEditorPane.scrollToReference(ref);
} else if (ScreenReader.isActive()) {
myEditorPane.setCaretPosition(0);
}});
}
private void applyFontSize() {
Document document = myEditorPane.getDocument();
if (!(document instanceof StyledDocument)) {
return;
}
final StyledDocument styledDocument = (StyledDocument)document;
EditorColorsManager colorsManager = EditorColorsManager.getInstance();
EditorColorsScheme scheme = colorsManager.getGlobalScheme();
StyleConstants.setFontSize(myFontSizeStyle, JBUI.scale(getQuickDocFontSize().getSize()));
if (Registry.is("documentation.component.editor.font")) {
StyleConstants.setFontFamily(myFontSizeStyle, scheme.getEditorFontName());
}
ApplicationManager.getApplication().executeOnPooledThread(
() -> styledDocument.setCharacterAttributes(0, styledDocument.getLength(), myFontSizeStyle, false));
}
private void goBack() {
if (myBackStack.isEmpty()) return;
Context context = myBackStack.pop();
myForwardStack.push(saveContext());
restoreContext(context);
updateControlState();
}
private void goForward() {
if (myForwardStack.isEmpty()) return;
Context context = myForwardStack.pop();
myBackStack.push(saveContext());
restoreContext(context);
updateControlState();
}
private Context saveContext() {
Rectangle rect = myScrollPane.getViewport().getViewRect();
return new Context(myElement, myText, myEffectiveExternalUrl, rect, myHighlightedLink);
}
private void restoreContext(Context context) {
setDataInternal(context.element, context.text, context.viewRect, null);
myEffectiveExternalUrl = context.externalUrl;
if (myNavigateCallback != null) {
final PsiElement element = context.element.getElement();
if (element != null) {
myNavigateCallback.consume(element);
}
}
highlightLink(context.highlightedLink);
}
private void updateControlState() {
ElementLocationUtil.customizeElementLabel(myElement != null ? myElement.getElement() : null, myElementLabel);
myToolBar.updateActionsImmediately(); // update faster
setControlPanelVisible(true);//(!myBackStack.isEmpty() || !myForwardStack.isEmpty());
}
private class BackAction extends AnAction implements HintManagerImpl.ActionToIgnore {
public BackAction() {
super(CodeInsightBundle.message("javadoc.action.back"), null, AllIcons.Actions.Back);
}
@Override
public void actionPerformed(AnActionEvent e) {
goBack();
}
@Override
public void update(AnActionEvent e) {
Presentation presentation = e.getPresentation();
presentation.setEnabled(!myBackStack.isEmpty());
}
}
private class ForwardAction extends AnAction implements HintManagerImpl.ActionToIgnore {
public ForwardAction() {
super(CodeInsightBundle.message("javadoc.action.forward"), null, AllIcons.Actions.Forward);
}
@Override
public void actionPerformed(AnActionEvent e) {
goForward();
}
@Override
public void update(AnActionEvent e) {
Presentation presentation = e.getPresentation();
presentation.setEnabled(!myForwardStack.isEmpty());
}
}
private class EditDocumentationSourceAction extends BaseNavigateToSourceAction {
private EditDocumentationSourceAction() {
super(true);
getTemplatePresentation().setIcon(AllIcons.Actions.EditSource);
getTemplatePresentation().setText("Edit Source");
}
@Override
public void actionPerformed(AnActionEvent e) {
super.actionPerformed(e);
final JBPopup hint = myHint;
if (hint != null && hint.isVisible()) {
hint.cancel();
}
}
@Nullable
@Override
protected Navigatable[] getNavigatables(DataContext dataContext) {
SmartPsiElementPointer element = myElement;
if (element != null) {
PsiElement psiElement = element.getElement();
return psiElement instanceof Navigatable ? new Navigatable[] {(Navigatable)psiElement} : null;
}
return null;
}
}
private class ExternalDocAction extends AnAction implements HintManagerImpl.ActionToIgnore {
private ExternalDocAction() {
super(CodeInsightBundle.message("javadoc.action.view.external"), null, AllIcons.Actions.Browser_externalJavaDoc);
registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_EXTERNAL_JAVADOC).getShortcutSet(), null);
}
@Override
public void actionPerformed(AnActionEvent e) {
if (myElement == null) {
return;
}
final PsiElement element = myElement.getElement();
final PsiElement originalElement = DocumentationManager.getOriginalElement(element);
ExternalJavaDocAction.showExternalJavadoc(element, originalElement, myEffectiveExternalUrl, e.getDataContext());
}
@Override
public void update(AnActionEvent e) {
final Presentation presentation = e.getPresentation();
presentation.setEnabled(false);
if (myElement != null) {
final PsiElement element = myElement.getElement();
final DocumentationProvider provider = DocumentationManager.getProviderFromElement(element);
final PsiElement originalElement = DocumentationManager.getOriginalElement(element);
if (provider instanceof ExternalDocumentationProvider) {
presentation.setEnabled(element != null && ((ExternalDocumentationProvider)provider).hasDocumentationFor(element, originalElement));
}
else {
final List<String> urls = provider.getUrlFor(element, originalElement);
presentation.setEnabled(element != null && urls != null && !urls.isEmpty());
}
}
}
}
private void registerActions() {
myExternalDocAction
.registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_EXTERNAL_JAVADOC).getShortcutSet(), myEditorPane);
// With screen readers, we want the default keyboard behavior inside
// the document text editor, i.e. the caret moves with cursor keys, etc.
if (!ScreenReader.isActive()) {
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
int value = scrollBar.getValue() - scrollBar.getUnitIncrement(-1);
value = Math.max(value, 0);
scrollBar.setValue(value);
}
});
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
int value = scrollBar.getValue() + scrollBar.getUnitIncrement(+1);
value = Math.min(value, scrollBar.getMaximum());
scrollBar.setValue(value);
}
});
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
int value = scrollBar.getValue() - scrollBar.getUnitIncrement(-1);
value = Math.max(value, 0);
scrollBar.setValue(value);
}
});
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
int value = scrollBar.getValue() + scrollBar.getUnitIncrement(+1);
value = Math.min(value, scrollBar.getMaximum());
scrollBar.setValue(value);
}
});
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
int value = scrollBar.getValue() - scrollBar.getBlockIncrement(-1);
value = Math.max(value, 0);
scrollBar.setValue(value);
}
});
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
int value = scrollBar.getValue() + scrollBar.getBlockIncrement(+1);
value = Math.min(value, scrollBar.getMaximum());
scrollBar.setValue(value);
}
});
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
scrollBar.setValue(0);
}
});
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
scrollBar.setValue(scrollBar.getMaximum());
}
});
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.CTRL_MASK), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
scrollBar.setValue(0);
}
});
myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.CTRL_MASK), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
scrollBar.setValue(scrollBar.getMaximum());
}
});
}
}
public String getText() {
return myText;
}
@Override
public void dispose() {
myBackStack.clear();
myForwardStack.clear();
myKeyboardActions.clear();
myElement = null;
myManager = null;
myHint = null;
myNavigateCallback = null;
}
private int getLinkCount() {
HTMLDocument document = (HTMLDocument)myEditorPane.getDocument();
int linkCount = 0;
for (HTMLDocument.Iterator it = document.getIterator(HTML.Tag.A); it.isValid(); it.next()) {
if (it.getAttributes().isDefined(HTML.Attribute.HREF)) linkCount++;
}
return linkCount;
}
@Nullable
private HTMLDocument.Iterator getLink(int n) {
if (n >= 0) {
HTMLDocument document = (HTMLDocument)myEditorPane.getDocument();
int linkCount = 0;
for (HTMLDocument.Iterator it = document.getIterator(HTML.Tag.A); it.isValid(); it.next()) {
if (it.getAttributes().isDefined(HTML.Attribute.HREF) && linkCount++ == n) return it;
}
}
return null;
}
private void highlightLink(int n) {
myHighlightedLink = n;
Highlighter highlighter = myEditorPane.getHighlighter();
HTMLDocument.Iterator link = getLink(n);
if (link != null) {
int startOffset = link.getStartOffset();
int endOffset = link.getEndOffset();
try {
if (myHighlightingTag == null) {
myHighlightingTag = highlighter.addHighlight(startOffset, endOffset, LINK_HIGHLIGHTER);
}
else {
highlighter.changeHighlight(myHighlightingTag, startOffset, endOffset);
}
myEditorPane.setCaretPosition(startOffset);
}
catch (BadLocationException e) {
LOG.warn("Error highlighting link", e);
}
}
else if (myHighlightingTag != null) {
highlighter.removeHighlight(myHighlightingTag);
myHighlightingTag = null;
}
}
private void activateLink(int n) {
HTMLDocument.Iterator link = getLink(n);
if (link != null) {
String href = (String)link.getAttributes().getAttribute(HTML.Attribute.HREF);
myManager.navigateByLink(this, href);
}
}
private class MyShowSettingsButton extends ActionButton {
private MyShowSettingsButton() {
this(new MyShowSettingsAction(), new Presentation(), ActionPlaces.JAVADOC_INPLACE_SETTINGS, ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE);
}
private MyShowSettingsButton(AnAction action, Presentation presentation, String place, @NotNull Dimension minimumSize) {
super(action, presentation, place, minimumSize);
myPresentation.setIcon(AllIcons.General.SecondaryGroup);
}
private void hideSettings() {
if (!mySettingsPanel.isVisible()) {
return;
}
AnActionEvent event = AnActionEvent.createFromDataContext(myPlace, myPresentation, DataContext.EMPTY_CONTEXT);
myAction.actionPerformed(event);
}
}
private class MyShowSettingsAction extends ToggleAction {
@Override
public boolean isSelected(AnActionEvent e) {
return mySettingsPanel.isVisible();
}
@Override
public void setSelected(AnActionEvent e, boolean state) {
if (!state) {
mySettingsPanel.setVisible(false);
return;
}
setFontSizeSliderSize(getQuickDocFontSize());
mySettingsPanel.setVisible(true);
}
}
private abstract static class MyDictionary<K, V> extends Dictionary<K, V> {
@Override
public int size() {
throw new UnsupportedOperationException();
}
@Override
public boolean isEmpty() {
throw new UnsupportedOperationException();
}
@Override
public Enumeration<K> keys() {
throw new UnsupportedOperationException();
}
@Override
public Enumeration<V> elements() {
throw new UnsupportedOperationException();
}
@Override
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
@Override
public V remove(Object key) {
throw new UnsupportedOperationException();
}
}
private class PreviousLinkAction extends AnAction implements HintManagerImpl.ActionToIgnore {
@Override
public void actionPerformed(AnActionEvent e) {
int linkCount = getLinkCount();
if (linkCount <= 0) return;
highlightLink(myHighlightedLink < 0 ? (linkCount - 1) : (myHighlightedLink + linkCount - 1) % linkCount);
}
}
private class NextLinkAction extends AnAction implements HintManagerImpl.ActionToIgnore {
@Override
public void actionPerformed(AnActionEvent e) {
int linkCount = getLinkCount();
if (linkCount <= 0) return;
highlightLink((myHighlightedLink + 1) % linkCount);
}
}
private class ActivateLinkAction extends AnAction implements HintManagerImpl.ActionToIgnore {
@Override
public void actionPerformed(AnActionEvent e) {
activateLink(myHighlightedLink);
}
}
private static class LinkHighlighter implements Highlighter.HighlightPainter {
private static final Stroke STROKE = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[]{1}, 0);
@Override
public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) {
try {
Rectangle target = c.getUI().getRootView(c).modelToView(p0, Position.Bias.Forward, p1, Position.Bias.Backward, bounds).getBounds();
Graphics2D g2d = (Graphics2D)g.create();
try {
g2d.setStroke(STROKE);
g2d.setColor(c.getSelectionColor());
g2d.drawRect(target.x, target.y, target.width - 1, target.height - 1);
}
finally {
g2d.dispose();
}
}
catch (Exception e) {
LOG.warn("Error painting link highlight", e);
}
}
}
}