quick doc: scale font size for HiDPI displays
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / documentation / DocumentationComponent.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.codeInsight.documentation;
18
19 import com.intellij.codeInsight.CodeInsightBundle;
20 import com.intellij.codeInsight.hint.ElementLocationUtil;
21 import com.intellij.codeInsight.hint.HintManagerImpl;
22 import com.intellij.codeInsight.hint.HintUtil;
23 import com.intellij.icons.AllIcons;
24 import com.intellij.ide.DataManager;
25 import com.intellij.ide.actions.BaseNavigateToSourceAction;
26 import com.intellij.ide.actions.ExternalJavaDocAction;
27 import com.intellij.lang.documentation.CompositeDocumentationProvider;
28 import com.intellij.lang.documentation.DocumentationProvider;
29 import com.intellij.lang.documentation.ExternalDocumentationProvider;
30 import com.intellij.openapi.Disposable;
31 import com.intellij.openapi.actionSystem.*;
32 import com.intellij.openapi.actionSystem.impl.ActionButton;
33 import com.intellij.openapi.application.ApplicationBundle;
34 import com.intellij.openapi.application.ApplicationManager;
35 import com.intellij.openapi.diagnostic.Logger;
36 import com.intellij.openapi.editor.colors.EditorColorsManager;
37 import com.intellij.openapi.editor.colors.EditorColorsScheme;
38 import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
39 import com.intellij.openapi.editor.ex.util.EditorUtil;
40 import com.intellij.openapi.keymap.KeymapUtil;
41 import com.intellij.openapi.options.FontSize;
42 import com.intellij.openapi.ui.popup.JBPopup;
43 import com.intellij.openapi.util.Disposer;
44 import com.intellij.openapi.util.InvalidDataException;
45 import com.intellij.openapi.util.registry.Registry;
46 import com.intellij.openapi.util.text.StringUtil;
47 import com.intellij.openapi.wm.ex.WindowManagerEx;
48 import com.intellij.pom.Navigatable;
49 import com.intellij.psi.PsiElement;
50 import com.intellij.psi.SmartPointerManager;
51 import com.intellij.psi.SmartPsiElementPointer;
52 import com.intellij.psi.util.PsiModificationTracker;
53 import com.intellij.ui.IdeBorderFactory;
54 import com.intellij.ui.JBColor;
55 import com.intellij.ui.SideBorder;
56 import com.intellij.ui.components.JBLayeredPane;
57 import com.intellij.ui.components.JBScrollPane;
58 import com.intellij.util.Consumer;
59 import com.intellij.util.containers.HashMap;
60 import com.intellij.util.ui.GraphicsUtil;
61 import com.intellij.util.ui.JBDimension;
62 import com.intellij.util.ui.JBUI;
63 import com.intellij.util.ui.UIUtil;
64 import org.jetbrains.annotations.NonNls;
65 import org.jetbrains.annotations.NotNull;
66 import org.jetbrains.annotations.Nullable;
67
68 import javax.swing.*;
69 import javax.swing.event.ChangeEvent;
70 import javax.swing.event.ChangeListener;
71 import javax.swing.event.HyperlinkEvent;
72 import javax.swing.event.HyperlinkListener;
73 import javax.swing.text.*;
74 import javax.swing.text.html.HTML;
75 import javax.swing.text.html.HTMLDocument;
76 import java.awt.*;
77 import java.awt.event.*;
78 import java.net.URL;
79 import java.util.*;
80 import java.util.List;
81
82 public class DocumentationComponent extends JPanel implements Disposable, DataProvider {
83   private static Logger LOGGER = Logger.getInstance(DocumentationComponent.class);
84
85   private static final Highlighter.HighlightPainter LINK_HIGHLIGHTER = new LinkHighlighter();
86   @NonNls private static final String DOCUMENTATION_TOPIC_ID = "reference.toolWindows.Documentation";
87
88   private static final int PREFERRED_WIDTH_EM = 37;
89   private static final int PREFERRED_HEIGHT_MIN_EM = 7;
90   private static final int PREFERRED_HEIGHT_MAX_EM = 20;
91
92   private DocumentationManager myManager;
93   private SmartPsiElementPointer myElement;
94   private long myModificationCount;
95
96   private final Stack<Context> myBackStack = new Stack<Context>();
97   private final Stack<Context> myForwardStack = new Stack<Context>();
98   private final ActionToolbar myToolBar;
99   private volatile boolean myIsEmpty;
100   private boolean myIsShown;
101   private final JLabel myElementLabel;
102   private final MutableAttributeSet myFontSizeStyle = new SimpleAttributeSet();
103   private JSlider myFontSizeSlider;
104   private final JComponent mySettingsPanel;
105   private final MyShowSettingsButton myShowSettingsButton;
106   private boolean myIgnoreFontSizeSliderChange;
107   private String myEffectiveExternalUrl;
108   private final MyDictionary<String, Image> myImageProvider = new MyDictionary<String, Image>() {
109     @Override
110     public Image get(Object key) {
111       if (myManager == null || key == null) return null;
112       PsiElement element = getElement();
113       if (element == null) return null;
114       URL url = (URL)key;
115       Image inMemory = myManager.getElementImage(element, url.toExternalForm());
116       return inMemory != null ? inMemory : Toolkit.getDefaultToolkit().createImage(url);
117     }
118   };
119
120   private static class Context {
121     private final SmartPsiElementPointer element;
122     private final String text;
123     private final String externalUrl;
124     private final Rectangle viewRect;
125     private final int highlightedLink;
126
127     public Context(SmartPsiElementPointer element, String text, String externalUrl, Rectangle viewRect, int highlightedLink) {
128       this.element = element;
129       this.text = text;
130       this.externalUrl = externalUrl;
131       this.viewRect = viewRect;
132       this.highlightedLink = highlightedLink;
133     }
134   }
135
136   private final JScrollPane myScrollPane;
137   private final JEditorPane myEditorPane;
138   private String myText; // myEditorPane.getText() surprisingly crashes.., let's cache the text
139   private final JPanel myControlPanel;
140   private boolean myControlPanelVisible;
141   private final ExternalDocAction myExternalDocAction;
142   private Consumer<PsiElement> myNavigateCallback;
143   private int myHighlightedLink = -1;
144   private Object myHighlightingTag;
145
146   private JBPopup myHint;
147
148   private final Map<KeyStroke, ActionListener> myKeyboardActions = new HashMap<KeyStroke, ActionListener>();
149
150   @Override
151   public boolean requestFocusInWindow() {
152     return myScrollPane.requestFocusInWindow();
153   }
154
155
156   @Override
157   public void requestFocus() {
158     myScrollPane.requestFocus();
159   }
160
161   public DocumentationComponent(final DocumentationManager manager, final AnAction[] additionalActions) {
162     myManager = manager;
163     myIsEmpty = true;
164     myIsShown = false;
165
166     myEditorPane = new JEditorPane(UIUtil.HTML_MIME, "") {
167       @Override
168       public Dimension getPreferredScrollableViewportSize() {
169         int em = myEditorPane.getFont().getSize();
170         int prefWidth = PREFERRED_WIDTH_EM * em;
171         int prefHeightMin = PREFERRED_HEIGHT_MIN_EM * em;
172         int prefHeightMax = PREFERRED_HEIGHT_MAX_EM * em;
173
174         if (getWidth() == 0 || getHeight() == 0) {
175           setSize(prefWidth, prefHeightMax);
176         }
177
178         Insets ins = myEditorPane.getInsets();
179         View rootView = myEditorPane.getUI().getRootView(myEditorPane);
180         rootView.setSize(prefWidth, prefHeightMax);  // Necessary! Without this line, the size won't increase when the content does
181
182         int prefHeight = (int)rootView.getPreferredSpan(View.Y_AXIS) + ins.bottom + ins.top +
183                          myScrollPane.getHorizontalScrollBar().getMaximumSize().height;
184         prefHeight = Math.max(prefHeightMin, Math.min(prefHeightMax, prefHeight));
185         return new Dimension(prefWidth, prefHeight);
186       }
187
188       {
189         enableEvents(AWTEvent.KEY_EVENT_MASK);
190       }
191
192       @Override
193       protected void processKeyEvent(KeyEvent e) {
194         KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
195         ActionListener listener = myKeyboardActions.get(keyStroke);
196         if (listener != null) {
197           listener.actionPerformed(new ActionEvent(DocumentationComponent.this, 0, ""));
198           e.consume();
199           return;
200         }
201         super.processKeyEvent(e);
202       }
203
204       @Override
205       protected void paintComponent(Graphics g) {
206         GraphicsUtil.setupAntialiasing(g);
207         super.paintComponent(g);
208       }
209
210       @Override
211       public void setDocument(Document doc) {
212         super.setDocument(doc);
213         if (doc instanceof StyledDocument) {
214           doc.putProperty("imageCache", myImageProvider);
215         }
216       }
217     };
218     DataProvider helpDataProvider = new DataProvider() {
219       @Override
220       public Object getData(@NonNls String dataId) {
221         return PlatformDataKeys.HELP_ID.is(dataId) ? DOCUMENTATION_TOPIC_ID : null;
222       }
223     };
224     myEditorPane.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, helpDataProvider);
225     myText = "";
226     myEditorPane.setEditable(false);
227     myEditorPane.setBackground(HintUtil.INFORMATION_COLOR);
228     myEditorPane.setEditorKit(UIUtil.getHTMLEditorKit(false));
229     myScrollPane = new JBScrollPane(myEditorPane) {
230       @Override
231       protected void processMouseWheelEvent(MouseWheelEvent e) {
232         if (!EditorSettingsExternalizable.getInstance().isWheelFontChangeEnabled() || !EditorUtil.isChangeFontSize(e)) {
233           super.processMouseWheelEvent(e);
234           return;
235         }
236
237         int change = Math.abs(e.getWheelRotation());
238         boolean increase = e.getWheelRotation() <= 0;
239         EditorColorsManager colorsManager = EditorColorsManager.getInstance();
240         EditorColorsScheme scheme = colorsManager.getGlobalScheme();
241         FontSize newFontSize = scheme.getQuickDocFontSize();
242         for (; change > 0; change--) {
243           if (increase) {
244             newFontSize = newFontSize.larger();
245           }
246           else {
247             newFontSize = newFontSize.smaller();
248           }
249         }
250
251         if (newFontSize == scheme.getQuickDocFontSize()) {
252           return;
253         }
254
255         scheme.setQuickDocFontSize(newFontSize);
256         applyFontSize();
257         setFontSizeSliderSize(newFontSize);
258       }
259     };
260     myScrollPane.setBorder(null);
261     myScrollPane.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, helpDataProvider);
262
263     final MouseListener mouseAdapter = new MouseAdapter() {
264       @Override
265       public void mousePressed(MouseEvent e) {
266         myManager.requestFocus();
267         myShowSettingsButton.hideSettings();
268       }
269     };
270     myEditorPane.addMouseListener(mouseAdapter);
271     Disposer.register(this, new Disposable() {
272       @Override
273       public void dispose() {
274         myEditorPane.removeMouseListener(mouseAdapter);
275       }
276     });
277
278     final FocusListener focusAdapter = new FocusAdapter() {
279       @Override
280       public void focusLost(FocusEvent e) {
281         Component previouslyFocused = WindowManagerEx.getInstanceEx().getFocusedComponent(manager.getProject(getElement()));
282
283         if (previouslyFocused != myEditorPane) {
284           if (myHint != null && !myHint.isDisposed()) myHint.cancel();
285         }
286       }
287     };
288     myEditorPane.addFocusListener(focusAdapter);
289
290     Disposer.register(this, new Disposable() {
291       @Override
292       public void dispose() {
293         myEditorPane.removeFocusListener(focusAdapter);
294       }
295     });
296
297     setLayout(new BorderLayout());
298     JLayeredPane layeredPane = new JBLayeredPane() {
299       @Override
300       public void doLayout() {
301         final Rectangle r = getBounds();
302         for (Component component : getComponents()) {
303           if (component instanceof JScrollPane) {
304             component.setBounds(0, 0, r.width, r.height);
305           }
306           else {
307             int insets = 2;
308             Dimension d = component.getPreferredSize();
309             component.setBounds(r.width - d.width - insets, insets, d.width, d.height);
310           }
311         }
312       }
313
314       @Override
315       public Dimension getPreferredSize() {
316         Dimension editorPaneSize = myEditorPane.getPreferredScrollableViewportSize();
317         Dimension controlPanelSize = myControlPanel.getPreferredSize();
318         return getSize(editorPaneSize, controlPanelSize);
319       }
320
321       @Override
322       public Dimension getMinimumSize() {
323         Dimension editorPaneSize = new JBDimension(20, 20);
324         Dimension controlPanelSize = myControlPanel.getMinimumSize();
325         return getSize(editorPaneSize, controlPanelSize);
326       }
327
328       private Dimension getSize(Dimension editorPaneSize, Dimension controlPanelSize) {
329         return new Dimension(Math.max(editorPaneSize.width, controlPanelSize.width), editorPaneSize.height + controlPanelSize.height);
330       }
331     };
332     layeredPane.add(myScrollPane);
333     layeredPane.setLayer(myScrollPane, 0);
334
335     mySettingsPanel = createSettingsPanel();
336     layeredPane.add(mySettingsPanel);
337     layeredPane.setLayer(mySettingsPanel, JLayeredPane.POPUP_LAYER);
338     add(layeredPane, BorderLayout.CENTER);
339     setOpaque(true);
340     myScrollPane.setViewportBorder(JBScrollPane.createIndentBorder());
341
342     final DefaultActionGroup actions = new DefaultActionGroup();
343     final BackAction back = new BackAction();
344     final ForwardAction forward = new ForwardAction();
345     EditDocumentationSourceAction edit = new EditDocumentationSourceAction();
346     actions.add(back);
347     actions.add(forward);
348     actions.add(myExternalDocAction = new ExternalDocAction());
349     actions.add(edit);
350
351     try {
352       CustomShortcutSet backShortcutSet = new CustomShortcutSet(KeyboardShortcut.fromString("LEFT"),
353                                                                 KeymapUtil.parseMouseShortcut("button4"));
354       CustomShortcutSet forwardShortcutSet = new CustomShortcutSet(KeyboardShortcut.fromString("RIGHT"),
355                                                                    KeymapUtil.parseMouseShortcut("button5"));
356       back.registerCustomShortcutSet(backShortcutSet, this);
357       forward.registerCustomShortcutSet(forwardShortcutSet, this);
358       // mouse actions are checked only for exact component over which click was performed, 
359       // so we need to register shortcuts for myEditorPane as well
360       back.registerCustomShortcutSet(backShortcutSet, myEditorPane); 
361       forward.registerCustomShortcutSet(forwardShortcutSet, myEditorPane);
362     }
363     catch (InvalidDataException e) {
364       LOGGER.error(e);
365     }
366     
367     myExternalDocAction.registerCustomShortcutSet(CustomShortcutSet.fromString("UP"), this);
368     edit.registerCustomShortcutSet(CommonShortcuts.getEditSource(), this);
369     if (additionalActions != null) {
370       for (final AnAction action : additionalActions) {
371         actions.add(action);
372         ShortcutSet shortcutSet = action.getShortcutSet();
373         if (shortcutSet != null) {
374           action.registerCustomShortcutSet(shortcutSet, this);
375         }
376       }
377     }
378
379     new NextLinkAction().registerCustomShortcutSet(CustomShortcutSet.fromString("TAB"), this);
380     new PreviousLinkAction().registerCustomShortcutSet(CustomShortcutSet.fromString("shift TAB"), this);
381     new ActivateLinkAction().registerCustomShortcutSet(CustomShortcutSet.fromString("ENTER"), this);
382
383     myToolBar = ActionManager.getInstance().createActionToolbar(ActionPlaces.JAVADOC_TOOLBAR, actions, true);
384
385     myControlPanel = new JPanel(new BorderLayout(5, 5));
386     myControlPanel.setBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM));
387
388     myElementLabel = new JLabel();
389     myElementLabel.setMinimumSize(new Dimension(100, 0)); // do not recalculate size according to the text
390
391     myControlPanel.add(myToolBar.getComponent(), BorderLayout.WEST);
392     myControlPanel.add(myElementLabel, BorderLayout.CENTER);
393     myControlPanel.add(myShowSettingsButton = new MyShowSettingsButton(), BorderLayout.EAST);
394     myControlPanelVisible = false;
395
396     final HyperlinkListener hyperlinkListener = new HyperlinkListener() {
397       @Override
398       public void hyperlinkUpdate(HyperlinkEvent e) {
399         HyperlinkEvent.EventType type = e.getEventType();
400         if (type == HyperlinkEvent.EventType.ACTIVATED) {
401           manager.navigateByLink(DocumentationComponent.this, e.getDescription());
402         }
403       }
404     };
405     myEditorPane.addHyperlinkListener(hyperlinkListener);
406     Disposer.register(this, new Disposable() {
407       @Override
408       public void dispose() {
409         myEditorPane.removeHyperlinkListener(hyperlinkListener);
410       }
411     });
412
413     registerActions();
414
415     updateControlState();
416   }
417
418   public DocumentationComponent(final DocumentationManager manager) {
419     this(manager, null);
420   }
421
422   @Override
423   public Object getData(@NonNls String dataId) {
424     if (DocumentationManager.SELECTED_QUICK_DOC_TEXT.getName().equals(dataId)) {
425       // Javadocs often contain &nbsp; symbols (non-breakable white space). We don't want to copy them as is and replace
426       // with raw white spaces. See IDEA-86633 for more details.
427       String selectedText = myEditorPane.getSelectedText();
428       return selectedText == null? null : selectedText.replace((char)160, ' ');
429     }
430
431     return null;
432   }
433
434   private JComponent createSettingsPanel() {
435     JPanel result = new JPanel(new FlowLayout(FlowLayout.RIGHT, 3, 0));
436     result.add(new JLabel(ApplicationBundle.message("label.font.size")));
437     myFontSizeSlider = new JSlider(SwingConstants.HORIZONTAL, 0, FontSize.values().length - 1, 3);
438     myFontSizeSlider.setMinorTickSpacing(1);
439     myFontSizeSlider.setPaintTicks(true);
440     myFontSizeSlider.setPaintTrack(true);
441     myFontSizeSlider.setSnapToTicks(true);
442     UIUtil.setSliderIsFilled(myFontSizeSlider, true);
443     result.add(myFontSizeSlider);
444     result.setBorder(BorderFactory.createLineBorder(JBColor.border(), 1));
445
446     myFontSizeSlider.addChangeListener(new ChangeListener() {
447       @Override
448       public void stateChanged(ChangeEvent e) {
449         if (myIgnoreFontSizeSliderChange) {
450           return;
451         }
452         EditorColorsManager colorsManager = EditorColorsManager.getInstance();
453         EditorColorsScheme scheme = colorsManager.getGlobalScheme();
454         scheme.setQuickDocFontSize(FontSize.values()[myFontSizeSlider.getValue()]);
455         applyFontSize();
456       }
457     });
458
459     String tooltipText = ApplicationBundle.message("quickdoc.tooltip.font.size.by.wheel");
460     result.setToolTipText(tooltipText);
461     myFontSizeSlider.setToolTipText(tooltipText);
462     result.setVisible(false);
463     result.setOpaque(true);
464     myFontSizeSlider.setOpaque(true);
465     return result;
466   }
467
468   private void setFontSizeSliderSize(FontSize fontSize) {
469     myIgnoreFontSizeSliderChange = true;
470     try {
471       FontSize[] sizes = FontSize.values();
472       for (int i = 0; i < sizes.length; i++) {
473         if (fontSize == sizes[i]) {
474           myFontSizeSlider.setValue(i);
475           break;
476         }
477       }
478     }
479     finally {
480       myIgnoreFontSizeSliderChange = false;
481     }
482   }
483
484   public boolean isEmpty() {
485     return myIsEmpty;
486   }
487
488   public void startWait() {
489     myIsEmpty = true;
490   }
491
492   private void setControlPanelVisible(boolean visible) {
493     if (visible == myControlPanelVisible) return;
494     if (visible) {
495       add(myControlPanel, BorderLayout.NORTH);
496     }
497     else {
498       remove(myControlPanel);
499     }
500     myControlPanelVisible = visible;
501   }
502
503   public void setHint(JBPopup hint) {
504     myHint = hint;
505   }
506
507   public JBPopup getHint() {
508     return myHint;
509   }
510
511   public JComponent getComponent() {
512     return myEditorPane;
513   }
514
515   @Nullable
516   public PsiElement getElement() {
517     return myElement != null ? myElement.getElement() : null;
518   }
519
520   private void setElement(SmartPsiElementPointer element) {
521     myElement = element;
522     myModificationCount = getCurrentModificationCount();
523   }
524
525   public boolean isUpToDate() {
526     return getElement() != null && myModificationCount == getCurrentModificationCount();
527   }
528
529   private long getCurrentModificationCount() {
530     return myElement != null ? PsiModificationTracker.SERVICE.getInstance(myElement.getProject()).getModificationCount() : -1;
531   }
532
533   public void setNavigateCallback(Consumer<PsiElement> navigateCallback) {
534     myNavigateCallback = navigateCallback;
535   }
536
537   public void setText(String text, @Nullable PsiElement element, boolean clearHistory) {
538     setText(text, element, false, clearHistory);
539   }
540
541   public void setText(String text, PsiElement element, boolean clean, boolean clearHistory) {
542     if (clean && myElement != null) {
543       myBackStack.push(saveContext());
544       myForwardStack.clear();
545     }
546     updateControlState();
547     setData(element, text, clearHistory, null);
548     if (clean) {
549       myIsEmpty = false;
550     }
551
552     if (clearHistory) clearHistory();
553   }
554
555   public void replaceText(String text, PsiElement element) {
556     PsiElement current = getElement();
557     if (current == null || !current.getManager().areElementsEquivalent(current, element)) return;
558     setText(text, element, false);
559     if (!myBackStack.empty()) myBackStack.pop();
560   }
561
562   private void clearHistory() {
563     myForwardStack.clear();
564     myBackStack.clear();
565   }
566
567   public void setData(PsiElement _element, String text, final boolean clearHistory, String effectiveExternalUrl) {
568     setData(_element, text, clearHistory, effectiveExternalUrl, null);
569   }
570   
571   public void setData(PsiElement _element, String text, final boolean clearHistory, String effectiveExternalUrl, String ref) {
572     if (myElement != null) {
573       myBackStack.push(saveContext());
574       myForwardStack.clear();
575     }
576     myEffectiveExternalUrl = effectiveExternalUrl;
577
578     final SmartPsiElementPointer element = _element != null && _element.isValid()
579                                            ? SmartPointerManager.getInstance(_element.getProject()).createSmartPsiElementPointer(_element)
580                                            : null;
581
582     if (element != null) {
583       setElement(element);
584     }
585
586     myIsEmpty = false;
587     updateControlState();
588     setDataInternal(element, text, new Rectangle(0, 0), ref);
589
590     if (clearHistory) clearHistory();
591   }
592
593   private void setDataInternal(SmartPsiElementPointer element, String text, final Rectangle viewRect, final String ref) {
594     setElement(element);
595     
596     highlightLink(-1);
597
598     myEditorPane.setText(text);
599     applyFontSize();
600     
601     if (!myIsShown && myHint != null && !ApplicationManager.getApplication().isUnitTestMode()) {
602       myManager.showHint(myHint);
603       myIsShown = true;
604     }
605
606     myText = text;
607
608     //noinspection SSBasedInspection
609     SwingUtilities.invokeLater(new Runnable() {
610       @Override
611       public void run() {
612         myEditorPane.scrollRectToVisible(viewRect); // if ref is defined but is not found in document, this provides a default location
613         if (ref != null) {
614           myEditorPane.scrollToReference(ref);
615         }
616       }
617     });
618   }
619
620   private void applyFontSize() {
621     Document document = myEditorPane.getDocument();
622     if (!(document instanceof StyledDocument)) {
623       return;
624     }
625
626     final StyledDocument styledDocument = (StyledDocument)document;
627
628     EditorColorsManager colorsManager = EditorColorsManager.getInstance();
629     EditorColorsScheme scheme = colorsManager.getGlobalScheme();
630     StyleConstants.setFontSize(myFontSizeStyle, JBUI.scale(scheme.getQuickDocFontSize().getSize()));
631     if (Registry.is("documentation.component.editor.font")) {
632       StyleConstants.setFontFamily(myFontSizeStyle, scheme.getEditorFontName());
633     }
634
635     ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
636       @Override
637       public void run() {
638         styledDocument.setCharacterAttributes(0, styledDocument.getLength(), myFontSizeStyle, false);
639       }
640     });
641   }
642
643   private void goBack() {
644     if (myBackStack.isEmpty()) return;
645     Context context = myBackStack.pop();
646     myForwardStack.push(saveContext());
647     restoreContext(context);
648     updateControlState();
649   }
650
651   private void goForward() {
652     if (myForwardStack.isEmpty()) return;
653     Context context = myForwardStack.pop();
654     myBackStack.push(saveContext());
655     restoreContext(context);
656     updateControlState();
657   }
658
659   private Context saveContext() {
660     Rectangle rect = myScrollPane.getViewport().getViewRect();
661     return new Context(myElement, myText, myEffectiveExternalUrl, rect, myHighlightedLink);
662   }
663
664   private void restoreContext(Context context) {
665     setDataInternal(context.element, context.text, context.viewRect, null);
666     myEffectiveExternalUrl = context.externalUrl;
667     if (myNavigateCallback != null) {
668       final PsiElement element = context.element.getElement();
669       if (element != null) {
670         myNavigateCallback.consume(element);
671       }
672     }
673     highlightLink(context.highlightedLink);
674   }
675
676   private void updateControlState() {
677     ElementLocationUtil.customizeElementLabel(myElement != null ? myElement.getElement() : null, myElementLabel);
678     myToolBar.updateActionsImmediately(); // update faster
679     setControlPanelVisible(true);//(!myBackStack.isEmpty() || !myForwardStack.isEmpty());
680   }
681
682   private class BackAction extends AnAction implements HintManagerImpl.ActionToIgnore {
683     public BackAction() {
684       super(CodeInsightBundle.message("javadoc.action.back"), null, AllIcons.Actions.Back);
685     }
686
687     @Override
688     public void actionPerformed(AnActionEvent e) {
689       goBack();
690     }
691
692     @Override
693     public void update(AnActionEvent e) {
694       Presentation presentation = e.getPresentation();
695       presentation.setEnabled(!myBackStack.isEmpty());
696     }
697   }
698
699   private class ForwardAction extends AnAction implements HintManagerImpl.ActionToIgnore {
700     public ForwardAction() {
701       super(CodeInsightBundle.message("javadoc.action.forward"), null, AllIcons.Actions.Forward);
702     }
703
704     @Override
705     public void actionPerformed(AnActionEvent e) {
706       goForward();
707     }
708
709     @Override
710     public void update(AnActionEvent e) {
711       Presentation presentation = e.getPresentation();
712       presentation.setEnabled(!myForwardStack.isEmpty());
713     }
714   }
715
716   private class EditDocumentationSourceAction extends BaseNavigateToSourceAction {
717
718     private EditDocumentationSourceAction() {
719       super(true);
720       getTemplatePresentation().setIcon(AllIcons.Actions.EditSource);
721       getTemplatePresentation().setText("Edit Source");
722     }
723
724     @Override
725     public void actionPerformed(AnActionEvent e) {
726       super.actionPerformed(e);
727       final JBPopup hint = myHint;
728       if (hint != null && hint.isVisible()) {
729         hint.cancel();
730       }
731     }
732
733     @Nullable
734     @Override
735     protected Navigatable[] getNavigatables(DataContext dataContext) {
736       SmartPsiElementPointer element = myElement;
737       if (element != null) {
738         PsiElement psiElement = element.getElement();
739         return psiElement instanceof Navigatable ? new Navigatable[] {(Navigatable)psiElement} : null;
740       }
741       return null;
742     }
743   }
744
745
746   private class ExternalDocAction extends AnAction implements HintManagerImpl.ActionToIgnore {
747     private ExternalDocAction() {
748       super(CodeInsightBundle.message("javadoc.action.view.external"), null, AllIcons.Actions.Browser_externalJavaDoc);
749       registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_EXTERNAL_JAVADOC).getShortcutSet(), null);
750     }
751
752     @Override
753     public void actionPerformed(AnActionEvent e) {
754       if (myElement == null) {
755         return;
756       }
757
758       final PsiElement element = myElement.getElement();
759       final DocumentationProvider provider = DocumentationManager.getProviderFromElement(element);
760       final PsiElement originalElement = DocumentationManager.getOriginalElement(element);
761       if (!(provider instanceof CompositeDocumentationProvider &&
762             ((CompositeDocumentationProvider)provider).handleExternal(element, originalElement))) {
763         List<String> urls;
764         if (!StringUtil.isEmptyOrSpaces(myEffectiveExternalUrl)) {
765           urls = Collections.singletonList(myEffectiveExternalUrl);
766         }
767         else {
768           urls = provider.getUrlFor(element, originalElement);
769           assert urls != null : provider;
770           assert !urls.isEmpty() : provider;
771         }
772         ExternalJavaDocAction.showExternalJavadoc(urls, PlatformDataKeys.CONTEXT_COMPONENT.getData(e.getDataContext()));
773       }
774     }
775
776     @Override
777     public void update(AnActionEvent e) {
778       final Presentation presentation = e.getPresentation();
779       presentation.setEnabled(false);
780       if (myElement != null) {
781         final PsiElement element = myElement.getElement();
782         final DocumentationProvider provider = DocumentationManager.getProviderFromElement(element);
783         final PsiElement originalElement = DocumentationManager.getOriginalElement(element);
784         if (provider instanceof ExternalDocumentationProvider) {
785           presentation.setEnabled(element != null && ((ExternalDocumentationProvider)provider).hasDocumentationFor(element, originalElement));
786         }
787         else {
788           final List<String> urls = provider.getUrlFor(element, originalElement);
789           presentation.setEnabled(element != null && urls != null && !urls.isEmpty());
790         }
791       }
792     }
793   }
794
795   private void registerActions() {
796     myExternalDocAction
797       .registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_EXTERNAL_JAVADOC).getShortcutSet(), myEditorPane);
798
799     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new ActionListener() {
800       @Override
801       public void actionPerformed(ActionEvent e) {
802         JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
803         int value = scrollBar.getValue() - scrollBar.getUnitIncrement(-1);
804         value = Math.max(value, 0);
805         scrollBar.setValue(value);
806       }
807     });
808
809     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new ActionListener() {
810       @Override
811       public void actionPerformed(ActionEvent e) {
812         JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
813         int value = scrollBar.getValue() + scrollBar.getUnitIncrement(+1);
814         value = Math.min(value, scrollBar.getMaximum());
815         scrollBar.setValue(value);
816       }
817     });
818
819     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), new ActionListener() {
820       @Override
821       public void actionPerformed(ActionEvent e) {
822         JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
823         int value = scrollBar.getValue() - scrollBar.getUnitIncrement(-1);
824         value = Math.max(value, 0);
825         scrollBar.setValue(value);
826       }
827     });
828
829     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), new ActionListener() {
830       @Override
831       public void actionPerformed(ActionEvent e) {
832         JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
833         int value = scrollBar.getValue() + scrollBar.getUnitIncrement(+1);
834         value = Math.min(value, scrollBar.getMaximum());
835         scrollBar.setValue(value);
836       }
837     });
838
839     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), new ActionListener() {
840       @Override
841       public void actionPerformed(ActionEvent e) {
842         JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
843         int value = scrollBar.getValue() - scrollBar.getBlockIncrement(-1);
844         value = Math.max(value, 0);
845         scrollBar.setValue(value);
846       }
847     });
848
849     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), new ActionListener() {
850       @Override
851       public void actionPerformed(ActionEvent e) {
852         JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
853         int value = scrollBar.getValue() + scrollBar.getBlockIncrement(+1);
854         value = Math.min(value, scrollBar.getMaximum());
855         scrollBar.setValue(value);
856       }
857     });
858
859     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), new ActionListener() {
860       @Override
861       public void actionPerformed(ActionEvent e) {
862         JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
863         scrollBar.setValue(0);
864       }
865     });
866
867     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), new ActionListener() {
868       @Override
869       public void actionPerformed(ActionEvent e) {
870         JScrollBar scrollBar = myScrollPane.getHorizontalScrollBar();
871         scrollBar.setValue(scrollBar.getMaximum());
872       }
873     });
874
875     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.CTRL_MASK), new ActionListener() {
876       @Override
877       public void actionPerformed(ActionEvent e) {
878         JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
879         scrollBar.setValue(0);
880       }
881     });
882
883     myKeyboardActions.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.CTRL_MASK), new ActionListener() {
884       @Override
885       public void actionPerformed(ActionEvent e) {
886         JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
887         scrollBar.setValue(scrollBar.getMaximum());
888       }
889     });
890   }
891
892   public String getText() {
893     return myText;
894   }
895
896   @Override
897   public void dispose() {
898     myBackStack.clear();
899     myForwardStack.clear();
900     myKeyboardActions.clear();
901     myElement = null;
902     myManager = null;
903     myHint = null;
904     myNavigateCallback = null;
905   }
906
907   private int getLinkCount() {
908     HTMLDocument document = (HTMLDocument)myEditorPane.getDocument();
909     int linkCount = 0;
910     for (HTMLDocument.Iterator it = document.getIterator(HTML.Tag.A); it.isValid(); it.next()) {
911       if (it.getAttributes().isDefined(HTML.Attribute.HREF)) linkCount++;
912     }
913     return linkCount;
914   }
915
916   @Nullable
917   private HTMLDocument.Iterator getLink(int n) {
918     if (n >= 0) {
919       HTMLDocument document = (HTMLDocument)myEditorPane.getDocument();
920       int linkCount = 0;
921       for (HTMLDocument.Iterator it = document.getIterator(HTML.Tag.A); it.isValid(); it.next()) {
922         if (it.getAttributes().isDefined(HTML.Attribute.HREF) && linkCount++ == n) return it;
923       }
924     }
925     return null;
926   }
927   
928   private void highlightLink(int n) {
929     myHighlightedLink = n;
930     Highlighter highlighter = myEditorPane.getHighlighter();
931     HTMLDocument.Iterator link = getLink(n);
932     if (link != null) {
933       int startOffset = link.getStartOffset();
934       int endOffset = link.getEndOffset();
935       try {
936         if (myHighlightingTag == null) {
937           myHighlightingTag = highlighter.addHighlight(startOffset, endOffset, LINK_HIGHLIGHTER);
938         }
939         else {
940           highlighter.changeHighlight(myHighlightingTag, startOffset, endOffset);
941         }
942         myEditorPane.setCaretPosition(startOffset);
943       }
944       catch (BadLocationException e) {
945         LOGGER.warn("Error highlighting link", e);
946       }
947     }
948     else if (myHighlightingTag != null) {
949       highlighter.removeHighlight(myHighlightingTag);
950       myHighlightingTag = null;
951     }
952   }
953
954   private void activateLink(int n) {
955     HTMLDocument.Iterator link = getLink(n);
956     if (link != null) {
957       String href = (String)link.getAttributes().getAttribute(HTML.Attribute.HREF);
958       myManager.navigateByLink(this, href);
959     }
960   }
961
962   private class MyShowSettingsButton extends ActionButton {
963
964     private MyShowSettingsButton() {
965       this(new MyShowSettingsAction(), new Presentation(), ActionPlaces.JAVADOC_INPLACE_SETTINGS, ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE);
966     }
967
968     private MyShowSettingsButton(AnAction action, Presentation presentation, String place, @NotNull Dimension minimumSize) {
969       super(action, presentation, place, minimumSize);
970       myPresentation.setIcon(AllIcons.General.SecondaryGroup);
971     }
972
973     private void hideSettings() {
974       if (!mySettingsPanel.isVisible()) {
975         return;
976       }
977       AnActionEvent event = AnActionEvent.createFromDataContext(myPlace, myPresentation, DataContext.EMPTY_CONTEXT);
978       myAction.actionPerformed(event);
979     }
980   }
981
982   private class MyShowSettingsAction extends ToggleAction {
983
984     @Override
985     public boolean isSelected(AnActionEvent e) {
986       return mySettingsPanel.isVisible();
987     }
988
989     @Override
990     public void setSelected(AnActionEvent e, boolean state) {
991       if (!state) {
992         mySettingsPanel.setVisible(false);
993         return;
994       }
995
996       EditorColorsManager colorsManager = EditorColorsManager.getInstance();
997       EditorColorsScheme scheme = colorsManager.getGlobalScheme();
998       setFontSizeSliderSize(scheme.getQuickDocFontSize());
999       mySettingsPanel.setVisible(true);
1000     }
1001   }
1002
1003   private abstract static class MyDictionary<K, V> extends Dictionary<K, V> {
1004     @Override
1005     public int size() {
1006       throw new UnsupportedOperationException();
1007     }
1008
1009     @Override
1010     public boolean isEmpty() {
1011       throw new UnsupportedOperationException();
1012     }
1013
1014     @Override
1015     public Enumeration<K> keys() {
1016       throw new UnsupportedOperationException();
1017     }
1018
1019     @Override
1020     public Enumeration<V> elements() {
1021       throw new UnsupportedOperationException();
1022     }
1023
1024     @Override
1025     public V put(K key, V value) {
1026       throw new UnsupportedOperationException();
1027     }
1028
1029     @Override
1030     public V remove(Object key) {
1031       throw new UnsupportedOperationException();
1032     }
1033   }
1034
1035   private class PreviousLinkAction extends AnAction implements HintManagerImpl.ActionToIgnore {
1036     @Override
1037     public void actionPerformed(AnActionEvent e) {
1038       int linkCount = getLinkCount();
1039       if (linkCount <= 0) return;
1040       highlightLink(myHighlightedLink < 0 ? (linkCount - 1) : (myHighlightedLink + linkCount - 1) % linkCount);
1041     }
1042   }
1043
1044   private class NextLinkAction extends AnAction implements HintManagerImpl.ActionToIgnore {
1045     @Override
1046     public void actionPerformed(AnActionEvent e) {
1047       int linkCount = getLinkCount();
1048       if (linkCount <= 0) return;
1049       highlightLink((myHighlightedLink + 1) % linkCount);
1050     }
1051   }
1052
1053   private class ActivateLinkAction extends AnAction implements HintManagerImpl.ActionToIgnore {
1054     @Override
1055     public void actionPerformed(AnActionEvent e) {
1056       activateLink(myHighlightedLink);
1057     }
1058   }
1059   
1060   private static class LinkHighlighter implements Highlighter.HighlightPainter {
1061     private static final Stroke STROKE = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[]{1}, 0);
1062     
1063     @Override
1064     public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) {
1065       try {
1066         Rectangle target = c.getUI().getRootView(c).modelToView(p0, Position.Bias.Forward, p1, Position.Bias.Backward, bounds).getBounds();
1067         Graphics2D g2d = (Graphics2D)g.create();
1068         try {
1069           g2d.setStroke(STROKE);
1070           g2d.setColor(c.getSelectionColor());
1071           g2d.drawRect(target.x, target.y, target.width - 1, target.height - 1);
1072         }
1073         finally {
1074           g2d.dispose();
1075         }
1076       }
1077       catch (Exception e) {
1078         LOGGER.warn("Error painting link highlight", e);
1079       }
1080     }
1081   }
1082 }