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