| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.ui; |
| |
| import com.intellij.Patches; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.util.containers.WeakHashMap; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.awt.geom.Area; |
| import java.util.Map; |
| |
| /** |
| * @author kir |
| * @author Konstantin Bulenkov |
| */ |
| public class ScreenUtil { |
| public static final String DISPOSE_TEMPORARY = "dispose.temporary"; |
| |
| @Nullable private static final Map<GraphicsConfiguration, Pair<Insets, Long>> ourInsetsCache = |
| Patches.JDK_BUG_ID_8004103 ? new WeakHashMap<GraphicsConfiguration, Pair<Insets, Long>>() : null; |
| private static final int ourInsetsTimeout = 5000; // shouldn't be too long |
| |
| private ScreenUtil() { } |
| |
| public static boolean isVisible(@NotNull Rectangle bounds) { |
| if (bounds.isEmpty()) return false; |
| Rectangle[] allScreenBounds = getAllScreenBounds(); |
| for (Rectangle screenBounds : allScreenBounds) { |
| final Rectangle intersection = screenBounds.intersection(bounds); |
| if (intersection.isEmpty()) continue; |
| final int sq1 = intersection.width * intersection.height; |
| final int sq2 = bounds.width * bounds.height; |
| double visibleFraction = (double)sq1 / (double)sq2; |
| if (visibleFraction > 0.1) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public static Rectangle getMainScreenBounds() { |
| return getScreenRectangle(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice()); |
| } |
| |
| private static Rectangle[] getAllScreenBounds() { |
| GraphicsDevice[] devices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices(); |
| Rectangle[] result = new Rectangle[devices.length]; |
| for (int i = 0; i < devices.length; i++) { |
| result[i] = getScreenRectangle(devices[i]); |
| } |
| return result; |
| } |
| |
| public static Shape getAllScreensShape() { |
| Rectangle[] rectangles = getAllScreenBounds(); |
| Area area = new Area(); |
| for (Rectangle rectangle : rectangles) { |
| area.add(new Area(rectangle)); |
| } |
| return area; |
| } |
| |
| public static Rectangle getScreenRectangle(@NotNull Point p) { |
| return getScreenRectangle(p.x, p.y); |
| } |
| |
| public static GraphicsDevice getScreenDevice(Rectangle bounds) { |
| GraphicsDevice candidate = null; |
| int maxIntersection = 0; |
| |
| for (GraphicsDevice device : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { |
| GraphicsConfiguration config = device.getDefaultConfiguration(); |
| final Rectangle rect = config.getBounds(); |
| Rectangle intersection = rect.intersection(bounds); |
| if (intersection.isEmpty()) { |
| continue; |
| } |
| if (intersection.width * intersection.height > maxIntersection) { |
| maxIntersection = intersection.width * intersection.height; |
| candidate = device; |
| } |
| } |
| |
| return candidate; |
| } |
| |
| /** |
| * Method removeNotify (and then addNotify) will be invoked for all components when main frame switches between states "Normal" <-> "FullScreen". |
| * 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). |
| */ |
| public static boolean isStandardAddRemoveNotify(Component component) { |
| JRootPane rootPane = findMainRootPane(component); |
| return rootPane == null || rootPane.getClientProperty(DISPOSE_TEMPORARY) == null; |
| } |
| |
| private static JRootPane findMainRootPane(Component component) { |
| while (component != null) { |
| Container parent = component.getParent(); |
| if (parent == null) { |
| return component instanceof RootPaneContainer ? ((RootPaneContainer)component).getRootPane() : null; |
| } |
| component = parent; |
| } |
| return null; |
| } |
| |
| private static Rectangle applyInsets(Rectangle rect, Insets i) { |
| return (i == null) |
| ? new Rectangle(rect) |
| : new Rectangle(rect.x + i.left, |
| rect.y + i.top, |
| rect.width - (i.left + i.right), |
| rect.height - (i.top + i.bottom)); |
| } |
| |
| public static Insets getScreenInsets(final GraphicsConfiguration gc) { |
| if (ourInsetsCache == null) { |
| return calcInsets(gc); |
| } |
| |
| synchronized (ourInsetsCache) { |
| Pair<Insets, Long> data = ourInsetsCache.get(gc); |
| final long now = System.currentTimeMillis(); |
| if (data == null || now > data.second + ourInsetsTimeout) { |
| data = Pair.create(calcInsets(gc), now); |
| ourInsetsCache.put(gc, data); |
| } |
| return data.first; |
| } |
| } |
| |
| private static Insets calcInsets(GraphicsConfiguration gc) { |
| if (Patches.SUN_BUG_ID_7172665 && GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices().length > 1) { |
| return new Insets(0, 0, 0, 0); |
| } |
| |
| return Toolkit.getDefaultToolkit().getScreenInsets(gc); |
| } |
| |
| /** |
| * Returns a visible area for the specified graphics device. |
| * |
| * @param device one of available devices |
| * @return a visible area rectangle |
| */ |
| private static Rectangle getScreenRectangle(GraphicsDevice device) { |
| GraphicsConfiguration configuration = device.getDefaultConfiguration(); |
| return applyInsets(configuration.getBounds(), getScreenInsets(configuration)); |
| } |
| |
| /** |
| * Finds a device that is the closest to the specified point and |
| * returns its visible area. |
| * |
| * @param x the X coordinate of the specified point |
| * @param y the Y coordinate of the specified point |
| * @return a visible area rectangle |
| */ |
| public static Rectangle getScreenRectangle(int x, int y) { |
| GraphicsDevice[] devices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices(); |
| if (devices.length == 0) { |
| return new Rectangle(x, y, 0, 0); |
| } |
| if (devices.length == 1) { |
| return getScreenRectangle(devices[0]); |
| } |
| Rectangle[] rectangles = new Rectangle[devices.length]; |
| for (int i = 0; i < devices.length; i++) { |
| GraphicsConfiguration configuration = devices[i].getDefaultConfiguration(); |
| Rectangle bounds = configuration.getBounds(); |
| rectangles[i] = applyInsets(bounds, getScreenInsets(configuration)); |
| if (bounds.contains(x, y)) { |
| return rectangles[i]; |
| } |
| } |
| Rectangle bounds = rectangles[0]; |
| int minimum = distance(bounds, x, y); |
| for (int i = 1; i < rectangles.length; i++) { |
| int distance = distance(rectangles[i], x, y); |
| if (minimum > distance) { |
| minimum = distance; |
| bounds = rectangles[i]; |
| } |
| } |
| return bounds; |
| } |
| |
| /** |
| * Normalizes a specified value in the specified range. |
| * If value less than the minimal value, |
| * the method returns the minimal value. |
| * If value greater than the maximal value, |
| * the method returns the maximal value. |
| * |
| * @param value the value to normalize |
| * @param min the minimal value of the range |
| * @param max the maximal value of the range |
| * @return a normalized value |
| */ |
| private static int normalize(int value, int min, int max) { |
| return value < min ? min : value > max ? max : value; |
| } |
| |
| /** |
| * Returns a square of the distance from |
| * the specified point to the specified rectangle, |
| * which does not contain the specified point. |
| * |
| * @param x the X coordinate of the specified point |
| * @param y the Y coordinate of the specified point |
| * @return a square of the distance |
| */ |
| private static int distance(Rectangle bounds, int x, int y) { |
| x -= normalize(x, bounds.x, bounds.x + bounds.width); |
| y -= normalize(y, bounds.y, bounds.y + bounds.height); |
| return x * x + y * y; |
| } |
| |
| public static boolean isOutsideOnTheRightOFScreen(Rectangle rect) { |
| final int screenX = rect.x; |
| final int screenY = rect.y; |
| Rectangle screen = getScreenRectangle(screenX, screenY); |
| return rect.getMaxX() > screen.getMaxX(); |
| } |
| |
| public static void moveRectangleToFitTheScreen(Rectangle aRectangle) { |
| int screenX = aRectangle.x + aRectangle.width / 2; |
| int screenY = aRectangle.y + aRectangle.height / 2; |
| Rectangle screen = getScreenRectangle(screenX, screenY); |
| |
| moveToFit(aRectangle, screen, null); |
| } |
| |
| public static void moveToFit(final Rectangle rectangle, final Rectangle container, @Nullable Insets padding) { |
| Insets insets = padding != null ? padding : new Insets(0, 0, 0, 0); |
| |
| Rectangle move = new Rectangle(rectangle.x - insets.left, rectangle.y - insets.top, rectangle.width + insets.left + insets.right, |
| rectangle.height + insets.top + insets.bottom); |
| |
| if (move.getMaxX() > container.getMaxX()) { |
| move.x = (int)container.getMaxX() - move.width; |
| } |
| |
| |
| if (move.getMinX() < container.getMinX()) { |
| move.x = (int)container.getMinX(); |
| } |
| |
| if (move.getMaxY() > container.getMaxY()) { |
| move.y = (int)container.getMaxY() - move.height; |
| } |
| |
| if (move.getMinY() < container.getMinY()) { |
| move.y = (int)container.getMinY(); |
| } |
| |
| rectangle.x = move.x + insets.left; |
| rectangle.y = move.y + insets.right; |
| rectangle.width = move.width - insets.left - insets.right; |
| rectangle.height = move.height - insets.top - insets.bottom; |
| } |
| |
| public static void fitToScreen(Rectangle r) { |
| Rectangle screen = getScreenRectangle(r.x, r.y); |
| |
| int xOverdraft = r.x + r.width - screen.x - screen.width; |
| if (xOverdraft > 0) { |
| int shift = Math.min(xOverdraft, r.x - screen.x); |
| xOverdraft -= shift; |
| r.x -= shift; |
| if (xOverdraft > 0) { |
| r.width -= xOverdraft; |
| } |
| } |
| |
| int yOverdraft = r.y + r.height - screen.y - screen.height; |
| if (yOverdraft > 0) { |
| int shift = Math.min(yOverdraft, r.y - screen.y); |
| yOverdraft -= shift; |
| r.y -= shift; |
| if (yOverdraft > 0) { |
| r.height -= yOverdraft; |
| } |
| } |
| } |
| |
| public static Point findNearestPointOnBorder(Rectangle rect, Point p) { |
| final int x0 = rect.x; |
| final int y0 = rect.y; |
| final int x1 = x0 + rect.width; |
| final int y1 = y0 + rect.height; |
| double distance = -1; |
| Point best = null; |
| final Point[] variants = {new Point(p.x, y0), new Point(p.x, y1), new Point(x0, p.y), new Point(x1, p.y)}; |
| for (Point variant : variants) { |
| final double d = variant.distance(p.x, p.y); |
| if (best == null || distance > d) { |
| best = variant; |
| distance = d; |
| } |
| } |
| assert best != null; |
| return best; |
| } |
| |
| public static void cropRectangleToFitTheScreen(Rectangle rect) { |
| int screenX = rect.x; |
| int screenY = rect.y; |
| final Rectangle screen = getScreenRectangle(screenX, screenY); |
| |
| if (rect.getMaxX() > screen.getMaxX()) { |
| rect.width = (int)screen.getMaxX() - rect.x; |
| } |
| |
| if (rect.getMinX() < screen.getMinX()) { |
| rect.x = (int)screen.getMinX(); |
| } |
| |
| if (rect.getMaxY() > screen.getMaxY()) { |
| rect.height = (int)screen.getMaxY() - rect.y; |
| } |
| |
| if (rect.getMinY() < screen.getMinY()) { |
| rect.y = (int)screen.getMinY(); |
| } |
| } |
| |
| /** |
| * |
| * @param prevLocation - previous location on screen |
| * @param location - current location on screen |
| * @param bounds - area to check if location shifted towards or not. Also in screen coordinates |
| * @return true if movement from prevLocation to location is towards specified rectangular area |
| */ |
| public static boolean isMovementTowards(final Point prevLocation, final Point location, final Rectangle bounds) { |
| if (bounds == null) { |
| return false; |
| } |
| if (prevLocation == null || prevLocation.equals(location)) { |
| return true; |
| } |
| |
| int dx = prevLocation.x - location.x; |
| int dy = prevLocation.y - location.y; |
| |
| // Check if the mouse goes out of the control. |
| if (dx > 0 && bounds.x >= prevLocation.x) return false; |
| if (dx < 0 && bounds.x + bounds.width <= prevLocation.x) return false; |
| if (dy > 0 && bounds.y + bounds.height >= prevLocation.y) return false; |
| if (dy < 0 && bounds.y <= prevLocation.y) return false; |
| if (dx == 0) { |
| return (location.x >= bounds.x && location.x < bounds.x + bounds.width) |
| && (dy > 0 ^ bounds.y > location.y); |
| } |
| if (dy == 0) { |
| return (location.y >= bounds.y && location.y < bounds.y + bounds.height) |
| && (dx > 0 ^ bounds.x > location.x); |
| } |
| |
| |
| // Calculate line equation parameters - y = a * x + b |
| float a = (float)dy / dx; |
| float b = location.y - a * location.x; |
| |
| // Check if crossing point with any tooltip border line is within bounds. Don't bother with floating point inaccuracy here. |
| |
| // Left border. |
| float crossY = a * bounds.x + b; |
| if (crossY >= bounds.y && crossY < bounds.y + bounds.height) return true; |
| |
| // Right border. |
| crossY = a * (bounds.x + bounds.width) + b; |
| if (crossY >= bounds.y && crossY < bounds.y + bounds.height) return true; |
| |
| // Top border. |
| float crossX = (bounds.y - b) / a; |
| if (crossX >= bounds.x && crossX < bounds.x + bounds.width) return true; |
| |
| // Bottom border |
| crossX = (bounds.y + bounds.height - b) / a; |
| if (crossX >= bounds.x && crossX < bounds.x + bounds.width) return true; |
| |
| return false; |
| } |
| } |