00cd521ab262712a6c7d01d63afe145d737303be
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / util / DimensionService.java
1 /*
2  * Copyright 2000-2016 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.openapi.util;
17
18 import com.intellij.openapi.components.*;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.project.ProjectManager;
22 import com.intellij.openapi.wm.IdeFocusManager;
23 import com.intellij.openapi.wm.IdeFrame;
24 import com.intellij.openapi.wm.WindowManager;
25 import com.intellij.ui.ScreenUtil;
26 import com.intellij.util.containers.hash.LinkedHashMap;
27 import com.intellij.util.ui.JBUI;
28 import com.intellij.util.ui.UIUtil;
29 import gnu.trove.TObjectIntHashMap;
30 import org.jdom.Element;
31 import org.jetbrains.annotations.NonNls;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34
35 import javax.swing.*;
36 import java.awt.*;
37 import java.awt.geom.Point2D;
38 import java.util.Map;
39
40 /**
41  * This class represents map between strings and rectangles. It's intended to store
42  * sizes of window, dialogs, etc.
43  */
44 @State(
45   name = "DimensionService",
46   storages = {
47     @Storage(value = "dimensions.xml", roamingType = RoamingType.DISABLED),
48     @Storage(value = "options.xml", deprecated = true)
49   }
50 )
51 public class DimensionService implements PersistentStateComponent<Element> {
52   private static final Logger LOG = Logger.getInstance(DimensionService.class);
53
54   private final Map<String, Point> myKey2Location;
55   private final Map<String, Dimension> myKey2Size;
56   private final TObjectIntHashMap<String> myKey2ExtendedState;
57   @NonNls private static final String EXTENDED_STATE = "extendedState";
58   @NonNls private static final String KEY = "key";
59   @NonNls private static final String STATE = "state";
60   @NonNls private static final String ELEMENT_LOCATION = "location";
61   @NonNls private static final String ELEMENT_SIZE = "size";
62   @NonNls private static final String ATTRIBUTE_X = "x";
63   @NonNls private static final String ATTRIBUTE_Y = "y";
64   @NonNls private static final String ATTRIBUTE_WIDTH = "width";
65   @NonNls private static final String ATTRIBUTE_HEIGHT = "height";
66
67   public static DimensionService getInstance() {
68     return ServiceManager.getService(DimensionService.class);
69   }
70
71   /**
72    * Invoked by reflection
73    */
74   private DimensionService() {
75     myKey2Location = new LinkedHashMap<>();
76     myKey2Size = new LinkedHashMap<>();
77     myKey2ExtendedState = new TObjectIntHashMap<>();
78   }
79
80   /**
81    * @param key a String key to perform a query for.
82    * @return point stored under the specified <code>key</code>. The method returns
83    * <code>null</code> if there is no stored value under the <code>key</code>. If point
84    * is outside of current screen bounds then the method returns <code>null</code>. It
85    * properly works in multi-monitor configuration.
86    * @throws java.lang.IllegalArgumentException if <code>key</code> is <code>null</code>.
87    */
88   @Nullable
89   public synchronized Point getLocation(String key) {
90     return getLocation(key, guessProject());
91   }
92
93   @Nullable
94   public synchronized Point getLocation(@NotNull String key, Project project) {
95     Pair<String, Float> pair = keyPair(key, project);
96     Point point = myKey2Location.get(pair.first);
97     if (point != null) {
98       point = (Point)point.clone();
99       float scale = pair.second;
100       point.setLocation(point.x / scale, point.y / scale);
101     }
102     if (point != null && !ScreenUtil.getScreenRectangle(point).contains(point)) {
103       point = null;
104     }
105     return point;
106   }
107
108   /**
109    * Store specified <code>point</code> under the <code>key</code>. If <code>point</code> is
110    * <code>null</code> then the value stored under <code>key</code> will be removed.
111    *
112    * @param key   a String key to store location for.
113    * @param point location to save.
114    * @throws java.lang.IllegalArgumentException if <code>key</code> is <code>null</code>.
115    */
116   public synchronized void setLocation(String key, Point point) {
117     setLocation(key, point, guessProject());
118   }
119
120   public synchronized void setLocation(@NotNull String key, Point point, Project project) {
121     Pair<String, Float> pair = keyPair(key, project);
122     if (point != null) {
123       point = (Point)point.clone();
124       float scale = pair.second;
125       point.setLocation(point.x * scale, point.y * scale);
126       myKey2Location.put(pair.first, point);
127     }
128     else {
129       myKey2Location.remove(key);
130     }
131   }
132
133   /**
134    * @param key a String key to perform a query for.
135    * @return point stored under the specified <code>key</code>. The method returns
136    * <code>null</code> if there is no stored value under the <code>key</code>.
137    * @throws java.lang.IllegalArgumentException if <code>key</code> is <code>null</code>.
138    */
139   @Nullable
140   public synchronized Dimension getSize(@NotNull @NonNls String key) {
141     return getSize(key, guessProject());
142   }
143
144   @Nullable
145   public synchronized Dimension getSize(@NotNull @NonNls String key, Project project) {
146     Pair<String, Float> pair = keyPair(key, project);
147     Dimension size = myKey2Size.get(pair.first);
148     if (size != null) {
149       size = (Dimension)size.clone();
150       float scale = pair.second;
151       size.setSize(size.width / scale, size.height / scale);
152     }
153     return size;
154   }
155
156   /**
157    * Store specified <code>size</code> under the <code>key</code>. If <code>size</code> is
158    * <code>null</code> then the value stored under <code>key</code> will be removed.
159    *
160    * @param key  a String key to to save size for.
161    * @param size a Size to save.
162    * @throws java.lang.IllegalArgumentException if <code>key</code> is <code>null</code>.
163    */
164   public synchronized void setSize(@NotNull @NonNls String key, Dimension size) {
165     setSize(key, size, guessProject());
166   }
167
168   public synchronized void setSize(@NotNull @NonNls String key, Dimension size, Project project) {
169     Pair<String, Float> pair = keyPair(key, project);
170     if (size != null) {
171       size = (Dimension)size.clone();
172       float scale = pair.second;
173       size.setSize(size.width * scale, size.height * scale);
174       myKey2Size.put(pair.first, size);
175     }
176     else {
177       myKey2Size.remove(key);
178     }
179   }
180
181   @Override
182   public Element getState() {
183     Element element = new Element("state");
184     // Save locations
185     for (String key : myKey2Location.keySet()) {
186       Point point = myKey2Location.get(key);
187       LOG.assertTrue(point != null);
188       Element e = new Element(ELEMENT_LOCATION);
189       e.setAttribute(KEY, key);
190       e.setAttribute(ATTRIBUTE_X, String.valueOf(point.x));
191       e.setAttribute(ATTRIBUTE_Y, String.valueOf(point.y));
192       element.addContent(e);
193     }
194
195     // Save sizes
196     for (String key : myKey2Size.keySet()) {
197       Dimension size = myKey2Size.get(key);
198       LOG.assertTrue(size != null);
199       Element e = new Element(ELEMENT_SIZE);
200       e.setAttribute(KEY, key);
201       e.setAttribute(ATTRIBUTE_WIDTH, String.valueOf(size.width));
202       e.setAttribute(ATTRIBUTE_HEIGHT, String.valueOf(size.height));
203       element.addContent(e);
204     }
205
206     // Save extended states
207     for (Object stateKey : myKey2ExtendedState.keys()) {
208       String key = (String)stateKey;
209       Element e = new Element(EXTENDED_STATE);
210       e.setAttribute(KEY, key);
211       e.setAttribute(STATE, String.valueOf(myKey2ExtendedState.get(key)));
212       element.addContent(e);
213     }
214     return element;
215   }
216
217   @Override
218   public void loadState(final Element element) {
219     myKey2Location.clear();
220     myKey2Size.clear();
221     myKey2ExtendedState.clear();
222
223     for (Element e : element.getChildren()) {
224       if (ELEMENT_LOCATION.equals(e.getName())) {
225         try {
226           myKey2Location.put(e.getAttributeValue(KEY), new Point(Integer.parseInt(e.getAttributeValue(ATTRIBUTE_X)),
227                                                                  Integer.parseInt(e.getAttributeValue(ATTRIBUTE_Y))));
228         }
229         catch (NumberFormatException ignored) {
230         }
231       }
232       else if (ELEMENT_SIZE.equals(e.getName())) {
233         try {
234           myKey2Size.put(e.getAttributeValue(KEY), new Dimension(Integer.parseInt(e.getAttributeValue(ATTRIBUTE_WIDTH)),
235                                                                  Integer.parseInt(e.getAttributeValue(ATTRIBUTE_HEIGHT))));
236         }
237         catch (NumberFormatException ignored) {
238         }
239       }
240       else if (EXTENDED_STATE.equals(e.getName())) {
241         try {
242           myKey2ExtendedState.put(e.getAttributeValue(KEY), Integer.parseInt(e.getAttributeValue(STATE)));
243         }
244         catch (NumberFormatException ignored) {
245         }
246       }
247     }
248   }
249
250   /**
251    * @deprecated Use {@link com.intellij.ide.util.PropertiesComponent}
252    */
253   @Deprecated
254   public void setExtendedState(String key, int extendedState) {
255     myKey2ExtendedState.put(key, extendedState);
256   }
257
258   /**
259    * @deprecated Use {@link com.intellij.ide.util.PropertiesComponent}
260    */
261   @Deprecated
262   public int getExtendedState(String key) {
263     if (!myKey2ExtendedState.containsKey(key)) return -1;
264     return myKey2ExtendedState.get(key);
265   }
266
267   @Nullable
268   private static Project guessProject() {
269     final Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
270     return openProjects.length == 1 ? openProjects[0] : null;
271   }
272
273   /**
274    * @return Pair(key, scale) where:
275    * key is the HiDPI-aware key,
276    * scale is the HiDPI-aware factor to transform size metrics.
277    */
278   @NotNull
279   private static Pair<String, Float> keyPair(String key, @Nullable Project project) {
280     GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
281     if (env.isHeadlessInstance()) {
282       return new Pair<>(key + ".headless", 1f);
283     }
284     JFrame frame = null;
285     final Component owner = IdeFocusManager.findInstance().getFocusOwner();
286     if (owner != null) {
287       frame = UIUtil.getParentOfType(JFrame.class, owner);
288     }
289     if (frame == null) {
290       frame = WindowManager.getInstance().findVisibleFrame();
291     }
292     if (project != null && (frame == null || (frame instanceof IdeFrame && project != ((IdeFrame)frame).getProject()))) {
293       frame = WindowManager.getInstance().getFrame(project);
294     }
295     Rectangle screen = new Rectangle(0, 0, 0, 0);
296     GraphicsDevice gd = null;
297     if (frame != null) {
298       final Point topLeft = frame.getLocation();
299       Point2D center = new Point2D.Float(topLeft.x + frame.getWidth() / 2, topLeft.y + frame.getHeight() / 2);
300       for (GraphicsDevice device : env.getScreenDevices()) {
301         Rectangle bounds = device.getDefaultConfiguration().getBounds();
302         if (bounds.contains(center)) {
303           screen = bounds;
304           gd = device;
305           break;
306         }
307       }
308     }
309     if (gd == null) {
310       gd = env.getDefaultScreenDevice();
311       screen = gd.getDefaultConfiguration().getBounds();
312     }
313     float scale = 1f;
314     if (UIUtil.isJreHiDPIEnabled()) {
315       scale = JBUI.sysScale(gd.getDefaultConfiguration());
316       // normalize screen bounds
317       screen.setBounds((int)Math.floor(screen.x * scale), (int)Math.floor(screen.y * scale),
318                        (int)Math.ceil(screen.width * scale), (int)Math.ceil(screen.height * scale));
319     }
320     String realKey = key + '.' + screen.x + '.' + screen.y + '.' + screen.width + '.' + screen.height;
321     if (JBUI.isPixHiDPI(gd.getDefaultConfiguration())) {
322       int dpi = ((int)(96 * JBUI.pixScale(gd.getDefaultConfiguration())));
323       realKey += "@" + dpi + "dpi";
324     }
325     return new Pair<>(realKey, scale);
326   }
327 }