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