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