IDEA-141464 Debugger popup dissappears when trying to hover mouse on "+"
[idea/community.git] / platform / xdebugger-impl / src / com / intellij / xdebugger / impl / evaluate / quick / common / AbstractValueHint.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 package com.intellij.xdebugger.impl.evaluate.quick.common;
17
18 import com.intellij.codeInsight.hint.HintManager;
19 import com.intellij.codeInsight.hint.HintManagerImpl;
20 import com.intellij.codeInsight.hint.HintUtil;
21 import com.intellij.codeInsight.navigation.NavigationUtil;
22 import com.intellij.ide.TooltipEvent;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.editor.Editor;
25 import com.intellij.openapi.editor.colors.EditorColors;
26 import com.intellij.openapi.editor.colors.EditorColorsManager;
27 import com.intellij.openapi.editor.colors.EditorColorsScheme;
28 import com.intellij.openapi.editor.event.EditorMouseEvent;
29 import com.intellij.openapi.editor.impl.EditorComponentImpl;
30 import com.intellij.openapi.editor.impl.EditorImpl;
31 import com.intellij.openapi.editor.markup.HighlighterLayer;
32 import com.intellij.openapi.editor.markup.HighlighterTargetArea;
33 import com.intellij.openapi.editor.markup.RangeHighlighter;
34 import com.intellij.openapi.editor.markup.TextAttributes;
35 import com.intellij.openapi.keymap.KeymapManager;
36 import com.intellij.openapi.keymap.KeymapUtil;
37 import com.intellij.openapi.project.Project;
38 import com.intellij.openapi.util.TextRange;
39 import com.intellij.ui.ClickListener;
40 import com.intellij.ui.HintListener;
41 import com.intellij.ui.LightweightHint;
42 import com.intellij.ui.SimpleColoredText;
43 import com.intellij.ui.awt.RelativePoint;
44 import com.intellij.util.IconUtil;
45 import com.intellij.xdebugger.impl.actions.XDebuggerActions;
46 import org.intellij.lang.annotations.JdkConstants;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49
50 import javax.swing.*;
51 import java.awt.*;
52 import java.awt.event.*;
53 import java.util.EventObject;
54
55 /**
56  * @author nik
57  */
58 public abstract class AbstractValueHint {
59   private static final Logger LOG = Logger.getInstance(AbstractValueHint.class);
60
61   private final KeyListener myEditorKeyListener = new KeyAdapter() {
62     @Override
63     public void keyReleased(KeyEvent e) {
64       if (!isAltMask(e.getModifiers())) {
65         ValueLookupManager.getInstance(myProject).hideHint();
66       }
67     }
68   };
69
70   private RangeHighlighter myHighlighter;
71   private Cursor myStoredCursor;
72   private final Project myProject;
73   private final Editor myEditor;
74   private final ValueHintType myType;
75   protected final Point myPoint;
76   private LightweightHint myCurrentHint;
77   private boolean myHintHidden;
78   private TextRange myCurrentRange;
79   private Runnable myHideRunnable;
80
81   public AbstractValueHint(@NotNull Project project, @NotNull Editor editor, @NotNull Point point, @NotNull ValueHintType type,
82                            final TextRange textRange) {
83     myPoint = point;
84     myProject = project;
85     myEditor = editor;
86     myType = type;
87     myCurrentRange = textRange;
88   }
89
90   protected abstract boolean canShowHint();
91
92   protected abstract void evaluateAndShowHint();
93
94   public boolean isKeepHint(Editor editor, Point point) {
95     if (myCurrentHint != null && myCurrentHint.canControlAutoHide()) {
96       return true;
97     }
98
99     if (myType == ValueHintType.MOUSE_ALT_OVER_HINT) {
100       return false;
101     }
102     else if (myType == ValueHintType.MOUSE_CLICK_HINT) {
103       if (myCurrentHint != null && myCurrentHint.isVisible()) {
104         return true;
105       }
106     }
107     else {
108       if (isInsideCurrentRange(editor, point)) {
109         return true;
110       }
111     }
112     return false;
113   }
114
115   boolean isInsideCurrentRange(Editor editor, Point point) {
116     return myCurrentRange != null && myCurrentRange.contains(calculateOffset(editor, point));
117   }
118
119   public static int calculateOffset(@NotNull Editor editor, @NotNull Point point) {
120     return editor.logicalPositionToOffset(editor.xyToLogicalPosition(point));
121   }
122
123   public void hideHint() {
124     myHintHidden = true;
125     myCurrentRange = null;
126     if (myStoredCursor != null) {
127       Component internalComponent = myEditor.getContentComponent();
128       internalComponent.setCursor(myStoredCursor);
129       if (LOG.isDebugEnabled()) {
130         LOG.debug("internalComponent.setCursor(myStoredCursor)");
131       }
132       internalComponent.removeKeyListener(myEditorKeyListener);
133     }
134
135     if (myCurrentHint != null) {
136       myCurrentHint.hide();
137       myCurrentHint = null;
138     }
139     if (myHighlighter != null) {
140       myHighlighter.dispose();
141       myHighlighter = null;
142     }
143   }
144
145   public void invokeHint() {
146     invokeHint(null);
147   }
148
149   public void invokeHint(Runnable hideRunnable) {
150     myHideRunnable = hideRunnable;
151
152     if (!canShowHint()) {
153       hideHint();
154       return;
155     }
156
157     if (myType == ValueHintType.MOUSE_ALT_OVER_HINT) {
158       EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
159       TextAttributes attributes = scheme.getAttributes(EditorColors.REFERENCE_HYPERLINK_COLOR);
160       attributes = NavigationUtil.patchAttributesColor(attributes, myCurrentRange, myEditor);
161
162       myHighlighter = myEditor.getMarkupModel().addRangeHighlighter(myCurrentRange.getStartOffset(), myCurrentRange.getEndOffset(),
163                                                                     HighlighterLayer.SELECTION + 1, attributes,
164                                                                     HighlighterTargetArea.EXACT_RANGE);
165       Component internalComponent = myEditor.getContentComponent();
166       myStoredCursor = internalComponent.getCursor();
167       internalComponent.addKeyListener(myEditorKeyListener);
168       internalComponent.setCursor(hintCursor());
169       if (LOG.isDebugEnabled()) {
170         LOG.debug("internalComponent.setCursor(hintCursor())");
171       }
172     }
173     else {
174       evaluateAndShowHint();
175     }
176   }
177
178   private static Cursor hintCursor() {
179     return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
180   }
181
182   public Project getProject() {
183     return myProject;
184   }
185
186   @NotNull
187   protected Editor getEditor() {
188     return myEditor;
189   }
190
191   protected ValueHintType getType() {
192     return myType;
193   }
194
195   protected boolean showHint(final JComponent component) {
196     if (myCurrentHint != null) {
197       myCurrentHint.hide();
198     }
199     myCurrentHint = new LightweightHint(component) {
200       @Override
201       protected boolean canAutoHideOn(TooltipEvent event) {
202         InputEvent inputEvent = event.getInputEvent();
203         if (inputEvent instanceof MouseEvent) {
204           Component comp = inputEvent.getComponent();
205           if (comp instanceof EditorComponentImpl) {
206             EditorImpl editor = ((EditorComponentImpl)comp).getEditor();
207             return !isInsideCurrentRange(editor, ((MouseEvent)inputEvent).getPoint());
208           }
209         }
210         return true;
211       }
212     };
213     myCurrentHint.addHintListener(new HintListener() {
214       @Override
215       public void hintHidden(EventObject event) {
216         if (myHideRunnable != null) {
217           myHideRunnable.run();
218         }
219         onHintHidden();
220       }
221     });
222
223     // editor may be disposed before later invokator process this action
224     if (myEditor.isDisposed() || myEditor.getComponent().getRootPane() == null) {
225       return false;
226     }
227
228     Point p = HintManagerImpl.getHintPosition(myCurrentHint, myEditor, myEditor.xyToLogicalPosition(myPoint), HintManager.UNDER);
229     HintManagerImpl.getInstanceImpl().showEditorHint(myCurrentHint, myEditor, p,
230                                                      HintManager.HIDE_BY_ANY_KEY |
231                                                      HintManager.HIDE_BY_TEXT_CHANGE |
232                                                      HintManager.HIDE_BY_SCROLLING, 0, false,
233                                                      HintManagerImpl.createHintHint(myEditor, p, myCurrentHint, HintManager.UNDER, true));
234     return true;
235   }
236
237   protected void onHintHidden() {
238
239   }
240
241   protected boolean isHintHidden() {
242     return myHintHidden;
243   }
244
245   protected JComponent createExpandableHintComponent(final SimpleColoredText text, final Runnable expand) {
246     final JComponent component = HintUtil.createInformationLabel(text, IconUtil.getAddIcon());
247     addClickListenerToHierarchy(component, new ClickListener() {
248       @Override
249       public boolean onClick(@NotNull MouseEvent event, int clickCount) {
250         if (myCurrentHint != null) {
251           myCurrentHint.hide();
252         }
253         expand.run();
254         return true;
255       }
256     });
257     return component;
258   }
259
260   private static void addClickListenerToHierarchy(Component c, ClickListener l) {
261     l.installOn(c);
262     if (c instanceof Container) {
263       Component[] children = ((Container)c).getComponents();
264       for (Component child : children) {
265         addClickListenerToHierarchy(child, l);
266       }
267     }
268   }
269
270   @Nullable
271   protected TextRange getCurrentRange() {
272     return myCurrentRange;
273   }
274
275   private static boolean isAltMask(@JdkConstants.InputEventMask int modifiers) {
276     return KeymapUtil.matchActionMouseShortcutsModifiers(KeymapManager.getInstance().getActiveKeymap(),
277                                                          modifiers,
278                                                          XDebuggerActions.QUICK_EVALUATE_EXPRESSION);
279   }
280
281   @Nullable
282   public static ValueHintType getHintType(final EditorMouseEvent e) {
283     int modifiers = e.getMouseEvent().getModifiers();
284     if (modifiers == 0) {
285       return ValueHintType.MOUSE_OVER_HINT;
286     }
287     else if (isAltMask(modifiers)) {
288       return ValueHintType.MOUSE_ALT_OVER_HINT;
289     }
290     return null;
291   }
292
293   public boolean isInsideHint(Editor editor, Point point) {
294     return myCurrentHint != null && myCurrentHint.isInsideHint(new RelativePoint(editor.getContentComponent(), point));
295   }
296
297   protected <D> void showTreePopup(@NotNull DebuggerTreeCreator<D> creator, @NotNull D descriptor) {
298     DebuggerTreeWithHistoryPopup.showTreePopup(creator, descriptor, getEditor(), myPoint, getProject());
299   }
300 }