Merge commit 'origin/master'
[idea/community.git] / platform / platform-impl / src / com / intellij / codeInsight / hint / LineTooltipRenderer.java
1 /*
2  * Copyright 2000-2009 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 package com.intellij.codeInsight.hint;
17
18 import com.intellij.ide.BrowserUtil;
19 import com.intellij.openapi.actionSystem.AnAction;
20 import com.intellij.openapi.actionSystem.AnActionEvent;
21 import com.intellij.openapi.actionSystem.CustomShortcutSet;
22 import com.intellij.openapi.actionSystem.IdeActions;
23 import com.intellij.openapi.editor.Editor;
24 import com.intellij.openapi.extensions.Extensions;
25 import com.intellij.openapi.keymap.KeymapManager;
26 import com.intellij.openapi.util.Comparing;
27 import com.intellij.openapi.util.Ref;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.ui.HintHint;
30 import com.intellij.ui.LightweightHint;
31 import com.intellij.ui.ScrollPaneFactory;
32 import com.intellij.util.ui.UIUtil;
33 import org.jetbrains.annotations.NonNls;
34
35 import javax.swing.*;
36 import javax.swing.event.HyperlinkEvent;
37 import javax.swing.event.HyperlinkListener;
38 import javax.swing.text.*;
39 import javax.swing.text.html.HTML;
40 import javax.swing.text.html.HTMLEditorKit;
41 import java.awt.*;
42 import java.awt.event.MouseAdapter;
43 import java.awt.event.MouseEvent;
44
45 /**
46  * @author cdr
47  */
48 public class LineTooltipRenderer implements TooltipRenderer {
49   @NonNls protected String myText;
50
51   private boolean myActiveLink = false;
52   private int myCurrentWidth;
53   @NonNls protected static final String BORDER_LINE = "<hr size=1 noshade>";
54
55   public LineTooltipRenderer(String text) {
56     myText = text;
57   }
58
59   public LineTooltipRenderer(final String text, final int width) {
60     this(text);
61     myCurrentWidth = width;
62   }
63
64   public LightweightHint show(final Editor editor,
65                               final Point p,
66                               final boolean alignToRight,
67                               final TooltipGroup group,
68                               final HintHint hintHint) {
69     if (myText == null) return null;
70
71     //setup text
72     myText = myText.replaceAll(String.valueOf(UIUtil.MNEMONIC), "");
73     final boolean expanded = myCurrentWidth > 0 && dressDescription(editor);
74
75     final HintManagerImpl hintManager = HintManagerImpl.getInstanceImpl();
76     final JComponent contentComponent = editor.getContentComponent();
77
78     final JComponent editorComponent = editor.getComponent();
79     final JLayeredPane layeredPane = editorComponent.getRootPane().getLayeredPane();
80
81     final JEditorPane pane = initPane(myText, hintHint, layeredPane);
82     if (!hintHint.isAwtTooltip()) {
83       correctLocation(editor, pane, p, alignToRight, expanded, myCurrentWidth);
84     }
85
86     final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(pane);
87     scrollPane.setBorder(null);
88
89     scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
90     scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
91
92     scrollPane.setOpaque(hintHint.isOpaqueAllowed());
93     scrollPane.getViewport().setOpaque(hintHint.isOpaqueAllowed());
94
95     scrollPane.setBackground(hintHint.getTextBackground());
96     scrollPane.getViewport().setBackground(hintHint.getTextBackground());
97
98     scrollPane.setViewportBorder(null);
99
100     final Ref<AnAction> anAction = new Ref<AnAction>();
101     final LightweightHint hint = new LightweightHint(scrollPane) {
102       public void hide() {
103         onHide(pane);
104         super.hide();
105         final AnAction action = anAction.get();
106         if (action != null) {
107           action.unregisterCustomShortcutSet(contentComponent);
108         }
109       }
110     };
111     anAction
112       .set(new AnAction() { //action to expand description when tooltip was shown after mouse move; need to unregister from editor component
113
114         {
115           registerCustomShortcutSet(
116             new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_SHOW_ERROR_DESCRIPTION)),
117             contentComponent);
118         }
119
120         public void actionPerformed(final AnActionEvent e) {
121           hint.hide();
122           if (myCurrentWidth > 0) {
123             stripDescription();
124           }
125           createRenderer(myText, myCurrentWidth > 0 ? 0 : pane.getWidth())
126             .show(editor, new Point(p.x - 3, p.y - 3), false, group, hintHint);
127         }
128       });
129
130     pane.addHyperlinkListener(new HyperlinkListener() {
131       public void hyperlinkUpdate(final HyperlinkEvent e) {
132         myActiveLink = true;
133         if (e.getEventType() == HyperlinkEvent.EventType.EXITED) {
134           myActiveLink = false;
135           return;
136         }
137         if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
138           if (!expanded) { // more -> less
139             for (final TooltipLinkHandlerEP handlerEP : Extensions.getExtensions(TooltipLinkHandlerEP.EP_NAME)) {
140               if (handlerEP.handleLink(e.getDescription(), editor, pane)) {
141                 myText = convertTextOnLinkHandled(myText);
142                 pane.setText(myText);
143                 return;
144               }
145             }
146             if (e.getURL() != null) {
147               BrowserUtil.launchBrowser(e.getURL().toString());
148             }
149           }
150           else { //less -> more
151             if (e.getURL() != null) {
152               BrowserUtil.launchBrowser(e.getURL().toString());
153               return;
154             }
155             stripDescription();
156             hint.hide();
157             createRenderer(myText, 0).show(editor, new Point(p.x - 3, p.y - 3), false, group, hintHint);
158           }
159         }
160       }
161     });
162
163     // This listener makes hint transparent for mouse events. It means that hint is closed
164     // by MousePressed and this MousePressed goes into the underlying editor component.
165     pane.addMouseListener(new MouseAdapter() {
166       public void mouseReleased(final MouseEvent e) {
167         if (!myActiveLink) {
168           MouseEvent newMouseEvent = SwingUtilities.convertMouseEvent(e.getComponent(), e, contentComponent);
169           hint.hide();
170           contentComponent.dispatchEvent(newMouseEvent);
171         }
172       }
173
174       public void mouseExited(final MouseEvent e) {
175         if (!expanded) {
176           hint.hide();
177         }
178       }
179     });
180
181     hintManager.showEditorHint(hint, editor, p, HintManager.HIDE_BY_ANY_KEY |
182                                                 HintManager.HIDE_BY_TEXT_CHANGE |
183                                                 HintManager.HIDE_BY_OTHER_HINT |
184                                                 HintManager.HIDE_BY_SCROLLING, 0, false, hintHint);
185     return hint;
186   }
187
188   public static void correctLocation(Editor editor,
189                                      JComponent tooltipComponent,
190                                      Point p,
191                                      boolean alignToRight,
192                                      boolean expanded,
193                                      int currentWidth) {
194     final JComponent editorComponent = editor.getComponent();
195     final JLayeredPane layeredPane = editorComponent.getRootPane().getLayeredPane();
196
197     int widthLimit = layeredPane.getWidth() - 10;
198     int heightLimit = layeredPane.getHeight() - 5;
199
200     Dimension dimension =
201       correctLocation(editor, p, alignToRight, expanded, tooltipComponent, layeredPane, widthLimit, heightLimit, currentWidth);
202
203     // in order to restrict tooltip size
204     tooltipComponent.setSize(dimension);
205     tooltipComponent.setMaximumSize(dimension);
206     tooltipComponent.setMinimumSize(dimension);
207     tooltipComponent.setPreferredSize(dimension);
208   }
209
210   private static Dimension correctLocation(Editor editor,
211                                            Point p,
212                                            boolean alignToRight,
213                                            boolean expanded,
214                                            JComponent tooltipComponent,
215                                            JLayeredPane layeredPane,
216                                            int widthLimit,
217                                            int heightLimit,
218                                            int currentWidth) {
219     Dimension preferredSize = tooltipComponent.getPreferredSize();
220     int width = expanded ? 3 * currentWidth / 2 : preferredSize.width;
221     int height = expanded ? Math.max(preferredSize.height, 150) : preferredSize.height;
222     Dimension dimension = new Dimension(width, height);
223
224     if (alignToRight) {
225       p.x = Math.max(0, p.x - width);
226     }
227
228     // try to make cursor outside tooltip. SCR 15038
229     p.x += 3;
230     p.y += 3;
231
232     if (p.x >= widthLimit - width) {
233       p.x = widthLimit - width;
234       width = Math.min(width, widthLimit);
235       height += 20;
236       dimension = new Dimension(width, height);
237     }
238
239     if (p.x < 3) {
240       p.x = 3;
241     }
242
243     if (p.y > heightLimit - height) {
244       p.y = heightLimit - height;
245       height = Math.min(heightLimit, height);
246       dimension = new Dimension(width, height);
247     }
248
249     if (p.y < 3) {
250       p.y = 3;
251     }
252
253     locateOutsideMouseCursor(editor, layeredPane, p, width, height, heightLimit);
254     return dimension;
255   }
256
257   private static void locateOutsideMouseCursor(Editor editor, JComponent editorComponent, Point p, int width, int height, int heightLimit) {
258     Point mouse = MouseInfo.getPointerInfo().getLocation();
259     SwingUtilities.convertPointFromScreen(mouse, editorComponent);
260     Rectangle tooltipRect = new Rectangle(p, new Dimension(width, height));
261     // should show at least one line apart
262     tooltipRect.setBounds(tooltipRect.x, tooltipRect.y - editor.getLineHeight(), width, height + 2 * editor.getLineHeight());
263     if (tooltipRect.contains(mouse)) {
264       if (mouse.y + height + editor.getLineHeight() > heightLimit && mouse.y - height - editor.getLineHeight() > 0) {
265         p.y = mouse.y - height - editor.getLineHeight();
266       }
267       else {
268         p.y = mouse.y + editor.getLineHeight();
269       }
270     }
271   }
272
273   protected String convertTextOnLinkHandled(String text) {
274     return text;
275   }
276
277   protected void onHide(JComponent contentComponent) {
278   }
279
280   protected LineTooltipRenderer createRenderer(String text, int width) {
281     return new LineTooltipRenderer(text, width);
282   }
283
284   protected boolean dressDescription(Editor editor) {
285     return false;
286   }
287
288   protected void stripDescription() {
289   }
290
291   static JEditorPane initPane(@NonNls String text, final HintHint hintHint, JLayeredPane layeredPane) {
292     final Ref<Dimension> prefSize = new Ref<Dimension>(null);
293     text = "<html><head>" +
294            UIUtil.getCssFontDeclaration(hintHint.getTextFont(), hintHint.getTextForeground(), hintHint.getLinkForeground()) +
295            "</head><body>" +
296            getHtmlBody(text) +
297            "</body></html>";
298
299     final JEditorPane pane = new JEditorPane() {
300       @Override
301       public Dimension getPreferredSize() {
302         return prefSize.get() != null ? prefSize.get() : super.getPreferredSize();
303       }
304     };
305
306     final HTMLEditorKit.HTMLFactory factory = new HTMLEditorKit.HTMLFactory() {
307       @Override
308       public View create(Element elem) {
309         AttributeSet attrs = elem.getAttributes();
310         Object elementName = attrs.getAttribute(AbstractDocument.ElementNameAttribute);
311         Object o = (elementName != null) ? null : attrs.getAttribute(StyleConstants.NameAttribute);
312         if (o instanceof HTML.Tag) {
313           HTML.Tag kind = (HTML.Tag)o;
314           if (kind == HTML.Tag.HR) {
315             return new CustomHrView(elem, hintHint.getTextForeground());
316           }
317         }
318         return super.create(elem);
319       }
320     };
321
322     HTMLEditorKit kit = new HTMLEditorKit() {
323       @Override
324       public ViewFactory getViewFactory() {
325         return factory;
326       }
327     };
328     pane.setEditorKit(kit);
329     pane.setText(text);
330
331     pane.setCaretPosition(0);
332     pane.setEditable(false);
333
334     if (hintHint.isOwnBorderAllowed()) {
335       setBorder(pane);
336       setColors(pane);
337     }
338     else {
339       pane.setBorder(null);
340     }
341
342     if (hintHint.isAwtTooltip()) {
343       Dimension size = layeredPane.getSize();
344       int fitWidth = (int)(size.width * 0.8);
345       Dimension prefSizeOriginal = pane.getPreferredSize();
346       if (prefSizeOriginal.width > fitWidth) {
347         pane.setSize(new Dimension(fitWidth, Integer.MAX_VALUE));
348         Dimension fixedWidthSize = pane.getPreferredSize();
349         prefSize.set(new Dimension(fitWidth, fixedWidthSize.height));
350       }
351       else {
352         prefSize.set(prefSizeOriginal);
353       }
354     }
355
356     pane.setOpaque(hintHint.isOpaqueAllowed());
357     pane.setBackground(hintHint.getTextBackground());
358
359     return pane;
360   }
361
362
363   public static void setColors(JComponent pane) {
364     pane.setForeground(Color.black);
365     pane.setBackground(HintUtil.INFORMATION_COLOR);
366     pane.setOpaque(true);
367   }
368
369   public static void setBorder(JComponent pane) {
370     pane.setBorder(
371       BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.black), BorderFactory.createEmptyBorder(0, 5, 0, 5)));
372   }
373
374   public void addBelow(String text) {
375     @NonNls String newBody;
376     if (myText == null) {
377       newBody = getHtmlBody(text);
378     }
379     else {
380       String html1 = getHtmlBody(myText);
381       String html2 = getHtmlBody(text);
382       newBody = html1 + BORDER_LINE + html2;
383     }
384     myText = "<html><body>" + newBody + "</body></html>";
385   }
386
387   protected static String getHtmlBody(@NonNls String text) {
388     String result = text;
389     if (!text.startsWith("<html>")) {
390       result = text.replaceAll("\n", "<br>");
391     }
392     else {
393       final int bodyIdx = text.indexOf("<body>");
394       final int closedBodyIdx = text.indexOf("</body>");
395       if (bodyIdx != -1 && closedBodyIdx != -1) {
396         result = text.substring(bodyIdx + "<body>".length(), closedBodyIdx);
397       }
398       else {
399         text = StringUtil.trimStart(text, "<html>").trim();
400         text = StringUtil.trimEnd(text, "</html>").trim();
401         text = StringUtil.trimStart(text, "<body>").trim();
402         text = StringUtil.trimEnd(text, "</body>").trim();
403         result = text;
404       }
405     }
406
407     return result;
408   }
409
410   public boolean equals(Object o) {
411     if (this == o) return true;
412     if (!(o instanceof LineTooltipRenderer)) return false;
413
414     final LineTooltipRenderer lineTooltipRenderer = (LineTooltipRenderer)o;
415
416     return Comparing.strEqual(myText, lineTooltipRenderer.myText);
417   }
418
419   public int hashCode() {
420     return myText == null ? 0 : myText.hashCode();
421   }
422
423   public String getText() {
424     return myText;
425   }
426 }