Merge pull request #350 (https://github.com/JetBrains/intellij-community/pull/350)
[idea/community.git] / platform / platform-api / src / com / intellij / ui / ScreenUtil.java
1 /*
2  * Copyright 2000-2014 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.ui;
17
18 import com.intellij.Patches;
19 import com.intellij.openapi.util.Pair;
20 import com.intellij.util.containers.WeakHashMap;
21 import com.intellij.util.ui.JBInsets;
22 import org.jetbrains.annotations.NotNull;
23 import org.jetbrains.annotations.Nullable;
24
25 import javax.swing.*;
26 import java.awt.*;
27 import java.awt.geom.Area;
28 import java.util.Map;
29
30 /**
31  * @author kir
32  * @author Konstantin Bulenkov
33  */
34 public class ScreenUtil {
35   public static final String DISPOSE_TEMPORARY = "dispose.temporary";
36
37   @Nullable private static final Map<GraphicsConfiguration, Pair<Insets, Long>> ourInsetsCache =
38     Patches.isJdkBugId8004103() ? new WeakHashMap<GraphicsConfiguration, Pair<Insets, Long>>() : null;
39   private static final int ourInsetsTimeout = 5000;  // shouldn't be too long
40
41   private ScreenUtil() { }
42
43   public static boolean isVisible(@NotNull Point location) {
44     return getScreenRectangle(location).contains(location);
45   }
46
47   public static boolean isVisible(@NotNull Rectangle bounds) {
48     if (bounds.isEmpty()) return false;
49     Rectangle[] allScreenBounds = getAllScreenBounds();
50     for (Rectangle screenBounds : allScreenBounds) {
51       final Rectangle intersection = screenBounds.intersection(bounds);
52       if (intersection.isEmpty()) continue;
53       final int sq1 = intersection.width * intersection.height;
54       final int sq2 = bounds.width * bounds.height;
55       double visibleFraction = (double)sq1 / (double)sq2;
56       if (visibleFraction > 0.1) {
57         return true;
58       }
59     }
60     return false;
61   }
62
63   public static Rectangle getMainScreenBounds() {
64     return getScreenRectangle(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice());
65   }
66
67   private static Rectangle[] getAllScreenBounds() {
68     GraphicsDevice[] devices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
69     Rectangle[] result = new Rectangle[devices.length];
70     for (int i = 0; i < devices.length; i++) {
71       result[i] = getScreenRectangle(devices[i]);
72     }
73     return result;
74   }
75
76   public static Shape getAllScreensShape() {
77     GraphicsDevice[] devices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
78     if (devices.length == 0) {
79       return new Rectangle();
80     }
81     if (devices.length == 1) {
82       return getScreenRectangle(devices[0]);
83     }
84     Area area = new Area();
85     for (GraphicsDevice device : devices) {
86       area.add(new Area(getScreenRectangle(device)));
87     }
88     return area;
89   }
90
91   /**
92    * Returns the smallest rectangle that encloses a visible area of every screen.
93    *
94    * @return the smallest rectangle that encloses a visible area of every screen
95    */
96   public static Rectangle getAllScreensRectangle() {
97     GraphicsDevice[] devices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
98     if (devices.length == 0) {
99       return new Rectangle();
100     }
101     if (devices.length == 1) {
102       return getScreenRectangle(devices[0]);
103     }
104     int minX = 0;
105     int maxX = 0;
106     int minY = 0;
107     int maxY = 0;
108     for (GraphicsDevice device : devices) {
109       Rectangle rectangle = getScreenRectangle(device);
110       int x = rectangle.x;
111       if (minX > x) {
112         minX = x;
113       }
114       x += rectangle.width;
115       if (maxX < x) {
116         maxX = x;
117       }
118       int y = rectangle.y;
119       if (minY > y) {
120         minY = y;
121       }
122       y += rectangle.height;
123       if (maxY < y) {
124         maxY = y;
125       }
126     }
127     return new Rectangle(minX, minY, maxX - minX, maxY - minY);
128   }
129
130   public static Rectangle getScreenRectangle(@NotNull Point p) {
131     return getScreenRectangle(p.x, p.y);
132   }
133
134   /**
135    * @param bounds a rectangle used to find corresponding graphics device
136    * @return a graphics device that contains the biggest part of the specified rectangle
137    */
138   public static GraphicsDevice getScreenDevice(Rectangle bounds) {
139     GraphicsDevice candidate = null;
140     int maxIntersection = 0;
141
142     for (GraphicsDevice device : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) {
143       GraphicsConfiguration config = device.getDefaultConfiguration();
144       final Rectangle rect = config.getBounds();
145       Rectangle intersection = rect.intersection(bounds);
146       if (intersection.isEmpty()) {
147         continue;
148       }
149       if (intersection.width * intersection.height > maxIntersection) {
150         maxIntersection = intersection.width * intersection.height;
151         candidate = device;
152       }
153     }
154
155     return candidate;
156   }
157
158   /**
159    * Method removeNotify (and then addNotify) will be invoked for all components when main frame switches between states "Normal" <-> "FullScreen".
160    * In this case we shouldn't call Disposer  in removeNotify and/or release some resources that we won't initialize again in addNotify (e.g. listeners).
161    */
162   public static boolean isStandardAddRemoveNotify(Component component) {
163     JRootPane rootPane = findMainRootPane(component);
164     return rootPane == null || rootPane.getClientProperty(DISPOSE_TEMPORARY) == null;
165   }
166
167   private static JRootPane findMainRootPane(Component component) {
168     while (component != null) {
169       Container parent = component.getParent();
170       if (parent == null) {
171         return component instanceof RootPaneContainer ? ((RootPaneContainer)component).getRootPane() : null;
172       }
173       component = parent;
174     }
175     return null;
176   }
177
178   private static Rectangle applyInsets(Rectangle rect, Insets i) {
179     rect = new Rectangle(rect);
180     JBInsets.removeFrom(rect, i);
181     return rect;
182   }
183
184   public static Insets getScreenInsets(final GraphicsConfiguration gc) {
185     if (ourInsetsCache == null) {
186       return calcInsets(gc);
187     }
188
189     synchronized (ourInsetsCache) {
190       Pair<Insets, Long> data = ourInsetsCache.get(gc);
191       final long now = System.currentTimeMillis();
192       if (data == null || now > data.second + ourInsetsTimeout) {
193         data = Pair.create(calcInsets(gc), now);
194         ourInsetsCache.put(gc, data);
195       }
196       return data.first;
197     }
198   }
199
200   private static Insets calcInsets(GraphicsConfiguration gc) {
201     if (Patches.SUN_BUG_ID_8020443 && GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices().length > 1) {
202       return new Insets(0, 0, 0, 0);
203     }
204
205     return Toolkit.getDefaultToolkit().getScreenInsets(gc);
206   }
207
208   /**
209    * Returns a visible area for the specified graphics device.
210    *
211    * @param device one of available devices
212    * @return a visible area rectangle
213    */
214   private static Rectangle getScreenRectangle(GraphicsDevice device) {
215     return getScreenRectangle(device.getDefaultConfiguration());
216   }
217
218   /**
219    * Returns a visible area for the specified graphics configuration.
220    *
221    * @param configuration one of available configurations
222    * @return a visible area rectangle
223    */
224   public static Rectangle getScreenRectangle(GraphicsConfiguration configuration) {
225     return applyInsets(configuration.getBounds(), getScreenInsets(configuration));
226   }
227
228   /**
229    * Returns a visible area for a graphics device that is the closest to the specified point.
230    *
231    * @param x the X coordinate of the specified point
232    * @param y the Y coordinate of the specified point
233    * @return a visible area rectangle
234    */
235   public static Rectangle getScreenRectangle(int x, int y) {
236     GraphicsDevice[] devices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
237     if (devices.length == 0) {
238       return new Rectangle(x, y, 0, 0);
239     }
240     if (devices.length == 1) {
241       return getScreenRectangle(devices[0]);
242     }
243     Rectangle[] rectangles = new Rectangle[devices.length];
244     for (int i = 0; i < devices.length; i++) {
245       GraphicsConfiguration configuration = devices[i].getDefaultConfiguration();
246       Rectangle bounds = configuration.getBounds();
247       rectangles[i] = applyInsets(bounds, getScreenInsets(configuration));
248       if (bounds.contains(x, y)) {
249         return rectangles[i];
250       }
251     }
252     Rectangle bounds = rectangles[0];
253     int minimum = distance(bounds, x, y);
254     if (bounds.width == 0 || bounds.height == 0) {
255       //Screen is invalid, give maximum score
256       minimum = Integer.MAX_VALUE;
257     }
258     for (int i = 1; i < rectangles.length; i++) {
259       if (rectangles[i].width == 0 || rectangles[i].height == 0) {
260         //Screen is invalid
261         continue;
262       }
263       int distance = distance(rectangles[i], x, y);
264       if (minimum > distance) {
265         minimum = distance;
266         bounds = rectangles[i];
267       }
268     }
269     if (bounds.width == 0 || bounds.height == 0) {
270       //All screens were invalid, return sensible default
271       return new Rectangle(x, y, 0, 0);
272     }
273     return bounds;
274   }
275
276   /**
277    * Normalizes a specified value in the specified range.
278    * If value less than the minimal value,
279    * the method returns the minimal value.
280    * If value greater than the maximal value,
281    * the method returns the maximal value.
282    *
283    * @param value the value to normalize
284    * @param min   the minimal value of the range
285    * @param max   the maximal value of the range
286    * @return a normalized value
287    */
288   private static int normalize(int value, int min, int max) {
289     return value < min ? min : value > max ? max : value;
290   }
291
292   /**
293    * Returns a square of the distance from
294    * the specified point to the specified rectangle,
295    * which does not contain the specified point.
296    *
297    * @param x the X coordinate of the specified point
298    * @param y the Y coordinate of the specified point
299    * @return a square of the distance
300    */
301   private static int distance(Rectangle bounds, int x, int y) {
302     x -= normalize(x, bounds.x, bounds.x + bounds.width);
303     y -= normalize(y, bounds.y, bounds.y + bounds.height);
304     return x * x + y * y;
305   }
306
307   public static boolean isOutsideOnTheRightOFScreen(Rectangle rect) {
308     final int screenX = rect.x;
309     final int screenY = rect.y;
310     Rectangle screen = getScreenRectangle(screenX, screenY);
311     return rect.getMaxX() > screen.getMaxX();
312   }
313
314   public static void moveRectangleToFitTheScreen(Rectangle aRectangle) {
315     int screenX = aRectangle.x + aRectangle.width / 2;
316     int screenY = aRectangle.y + aRectangle.height / 2;
317     Rectangle screen = getScreenRectangle(screenX, screenY);
318
319     moveToFit(aRectangle, screen, null);
320   }
321
322   public static void moveToFit(final Rectangle rectangle, final Rectangle container, @Nullable Insets padding) {
323     Rectangle move = new Rectangle(rectangle);
324     JBInsets.addTo(move, padding);
325
326     if (move.getMaxX() > container.getMaxX()) {
327       move.x = (int)container.getMaxX() - move.width;
328     }
329
330
331     if (move.getMinX() < container.getMinX()) {
332       move.x = (int)container.getMinX();
333     }
334
335     if (move.getMaxY() > container.getMaxY()) {
336       move.y = (int)container.getMaxY() - move.height;
337     }
338
339     if (move.getMinY() < container.getMinY()) {
340       move.y = (int)container.getMinY();
341     }
342
343     JBInsets.removeFrom(move, padding);
344     rectangle.setBounds(move);
345   }
346
347   /**
348    * Finds the best place for the specified rectangle on the screen.
349    *
350    * @param rectangle    the rectangle to move and resize
351    * @param top          preferred offset between {@code rectangle.y} and popup above
352    * @param bottom       preferred offset between {@code rectangle.y} and popup below
353    * @param rightAligned shows that the rectangle should be moved to the left
354    */
355   public static void fitToScreenVertical(Rectangle rectangle, int top, int bottom, boolean rightAligned) {
356     Rectangle screen = getScreenRectangle(rectangle.x, rectangle.y);
357     if (rectangle.width > screen.width) {
358       rectangle.width = screen.width;
359     }
360     if (rightAligned) {
361       rectangle.x -= rectangle.width;
362     }
363     if (rectangle.x < screen.x) {
364       rectangle.x = screen.x;
365     }
366     else {
367       int max = screen.x + screen.width;
368       if (rectangle.x > max) {
369         rectangle.x = max - rectangle.width;
370       }
371     }
372     int above = rectangle.y - screen.y - top;
373     int below = screen.height - above - top - bottom;
374     if (below > rectangle.height) {
375       rectangle.y += bottom;
376     }
377     else if (above > rectangle.height) {
378       rectangle.y -= rectangle.height + top;
379     }
380     else if (below > above) {
381       rectangle.y += bottom;
382       rectangle.height = below;
383     }
384     else {
385       rectangle.y -= rectangle.height + top;
386       rectangle.height = above;
387     }
388   }
389
390   public static void fitToScreen(Rectangle r) {
391     Rectangle screen = getScreenRectangle(r.x, r.y);
392
393     int xOverdraft = r.x + r.width - screen.x - screen.width;
394     if (xOverdraft > 0) {
395       int shift = Math.min(xOverdraft, r.x - screen.x);
396       xOverdraft -= shift;
397       r.x -= shift;
398       if (xOverdraft > 0) {
399         r.width -= xOverdraft;
400       }
401     }
402
403     int yOverdraft = r.y + r.height - screen.y - screen.height;
404     if (yOverdraft > 0) {
405       int shift = Math.min(yOverdraft, r.y - screen.y);
406       yOverdraft -= shift;
407       r.y -= shift;
408       if (yOverdraft > 0) {
409         r.height -= yOverdraft;
410       }
411     }
412   }
413
414   public static Point findNearestPointOnBorder(Rectangle rect, Point p) {
415     final int x0 = rect.x;
416     final int y0 = rect.y;
417     final int x1 = x0 + rect.width;
418     final int y1 = y0 + rect.height;
419     double distance = -1;
420     Point best = null;
421     final Point[] variants = {new Point(p.x, y0), new Point(p.x, y1), new Point(x0, p.y), new Point(x1, p.y)};
422     for (Point variant : variants) {
423       final double d = variant.distance(p.x, p.y);
424       if (best == null || distance > d) {
425         best = variant;
426         distance = d;
427       }
428     }
429     assert best != null;
430     return best;
431   }
432
433   public static void cropRectangleToFitTheScreen(Rectangle rect) {
434     int screenX = rect.x;
435     int screenY = rect.y;
436     final Rectangle screen = getScreenRectangle(screenX, screenY);
437
438     if (rect.getMaxX() > screen.getMaxX()) {
439       rect.width = (int)screen.getMaxX() - rect.x;
440     }
441
442     if (rect.getMinX() < screen.getMinX()) {
443       rect.x = (int)screen.getMinX();
444     }
445
446     if (rect.getMaxY() > screen.getMaxY()) {
447       rect.height = (int)screen.getMaxY() - rect.y;
448     }
449
450     if (rect.getMinY() < screen.getMinY()) {
451       rect.y = (int)screen.getMinY();
452     }
453   }
454
455   /**
456    *
457    * @param prevLocation - previous location on screen
458    * @param location - current location on screen
459    * @param bounds - area to check if location shifted towards or not. Also in screen coordinates
460    * @return true if movement from prevLocation to location is towards specified rectangular area
461    */
462   public static boolean isMovementTowards(final Point prevLocation, final Point location, final Rectangle bounds) {
463     if (bounds == null) {
464       return false;
465     }
466     if (prevLocation == null || prevLocation.equals(location)) {
467       return true;
468     }
469     // consider any movement inside a rectangle as a valid movement towards
470     if (bounds.contains(location)) {
471       return true;
472     }
473
474     int dx = prevLocation.x - location.x;
475     int dy = prevLocation.y - location.y;
476
477     // Check if the mouse goes out of the control.
478     if (dx > 0 && bounds.x >= prevLocation.x) return false;
479     if (dx < 0 && bounds.x + bounds.width <= prevLocation.x) return false;
480     if (dy > 0 && bounds.y + bounds.height >= prevLocation.y) return false;
481     if (dy < 0 && bounds.y <= prevLocation.y) return false;
482     if (dx == 0) {
483       return (location.x >= bounds.x && location.x < bounds.x + bounds.width)
484              && (dy > 0 ^ bounds.y > location.y);
485     }
486     if (dy == 0) {
487       return (location.y >= bounds.y && location.y < bounds.y + bounds.height)
488              && (dx > 0 ^ bounds.x > location.x);
489     }
490
491
492     // Calculate line equation parameters - y = a * x + b
493     float a = (float)dy / dx;
494     float b = location.y - a * location.x;
495
496     // Check if crossing point with any tooltip border line is within bounds. Don't bother with floating point inaccuracy here.
497
498     // Left border.
499     float crossY = a * bounds.x + b;
500     if (crossY >= bounds.y && crossY < bounds.y + bounds.height) return true;
501
502     // Right border.
503     crossY = a * (bounds.x + bounds.width) + b;
504     if (crossY >= bounds.y && crossY < bounds.y + bounds.height) return true;
505
506     // Top border.
507     float crossX = (bounds.y - b) / a;
508     if (crossX >= bounds.x && crossX < bounds.x + bounds.width) return true;
509
510     // Bottom border
511     crossX = (bounds.y + bounds.height - b) / a;
512     if (crossX >= bounds.x && crossX < bounds.x + bounds.width) return true;
513
514     return false;
515   }
516 }