001package jmri.util; 002 003import java.awt.Component; 004import java.awt.Dimension; 005import java.awt.DisplayMode; 006import java.awt.GraphicsDevice; 007import java.awt.GraphicsEnvironment; 008import java.awt.IllegalComponentStateException; 009import java.awt.Point; 010import java.awt.Window; 011//import java.util.ArrayList; 012import org.slf4j.Logger; 013import org.slf4j.LoggerFactory; 014 015import jmri.InstanceManager; 016import jmri.InstanceManagerAutoDefault; 017 018/** 019 * Position a Window relative to a component in another window so as 020 * to not obscure a component in that window. Typically, the Component 021 * is being edited by actions done in the target Window.\p 022 * Note the assumption in multiple screen environments is the screens 023 * are configured horizontally. 024 * 025 * @author Pete Cressman Copyright (C) 2018 026 * @since 4.13.1 027 */ 028public class PlaceWindow implements InstanceManagerAutoDefault { 029 static GraphicsEnvironment _environ = GraphicsEnvironment.getLocalGraphicsEnvironment(); 030 static Dimension _screenSize[]; 031 static Dimension _totalScreenDim = new Dimension(0, 0); 032 033 public PlaceWindow() { 034 getScreens(); 035 } 036 private void getScreens() { 037 GraphicsDevice[] gd = _environ.getScreenDevices(); 038 _screenSize = new Dimension[gd.length]; 039 int maxHeight = 0; 040 for (int i = 0; i < gd.length; i++) { 041 String deviceID = gd[i].getIDstring(); 042 DisplayMode dm = gd[i].getDisplayMode(); 043 _screenSize[i] = new Dimension(dm.getWidth(), dm.getHeight()); 044 _totalScreenDim.width += dm.getWidth(); // assuming screens are horizontal 045 maxHeight = Math.max(maxHeight, dm.getHeight()); // use maximum height 046 if (log.isDebugEnabled()) { 047 log.debug("\"Screen # {} deviceID= {}: width= {}, height= {}", 048 i, deviceID, dm.getWidth(), dm.getHeight()); 049 } 050 } 051 _totalScreenDim.height = maxHeight; 052 if (log.isDebugEnabled()) { 053 try { 054 GraphicsDevice dgd = _environ.getDefaultScreenDevice(); 055 DisplayMode dm = dgd.getDisplayMode(); 056 log.debug("\"DefaultScreen= {}: width= {}, height= {}", dgd.getIDstring(), dm.getWidth(), dm.getHeight()); 057 log.debug("\"Total Screen size: width= {}, height= {}", _totalScreenDim.width, _totalScreenDim.height); 058 } catch (java.awt.IllegalComponentStateException icse ) { 059 log.debug( "unable to construct debug information due to illegal component state"); 060 } 061 } 062 } 063 064 public static PlaceWindow getDefault() { 065 return InstanceManager.getOptionalDefault(PlaceWindow.class).orElseGet(() -> { 066 return InstanceManager.setDefault(PlaceWindow.class, new PlaceWindow()); 067 }); 068 } 069 070 /** 071 * In a possibly multi-monitor environment, find the screen displaying 072 * the window and return its dimensions. 073 * \p 074 * getLocation() and getLocationOnScreen() return the same Point which 075 * has coordinates in the total display area, i.e. all screens combined. 076 * Note DefaultScreen is NOT this total combined display area. 077 * 078 * We assume monitors are aligned horizontally - at least this is the only 079 * configuration possible from Windows settings. 080 * 081 * @param window a window 082 * @return Screen number of window location 083 */ 084 public int getScreenNum(Window window) { 085 /* this always has window on device #0 ?? 086 GraphicsDevice windowDevice = window.getGraphicsConfiguration().getDevice(); 087 DisplayMode windowDM = windowDevice.getDisplayMode(); 088 GraphicsDevice[] gd = _environ.getScreenDevices(); 089 for (int i = 0; i < gd.length; i++) { 090 if (gd[i].getDisplayMode().equals(windowDM)) { 091 return i; 092 } 093 }*/ 094 int x = 0; 095 try { 096 for (int i = 0; i < _screenSize.length; i++) { 097 x += _screenSize[i].width; 098 if (window.getLocation().x < x) { 099 return i; 100 } 101 } 102 103 } catch (IllegalComponentStateException icse) { 104 return 0; 105 } 106 return 0; 107 } 108 109 public Dimension getScreenSize(int screenNum) { 110 if (screenNum >= 0 && screenNum <= _screenSize.length) { 111 return _screenSize[screenNum]; 112 } 113 return new Dimension(0, 0); 114 } 115 /** 116 * Find the best place to position the target window next to the component but not 117 * obscuring it. Positions target to the Left, Right, Below or Above. Tries in 118 * that order to keep target within the parent window. If not possible, tries 119 * to keep the target window within the parent's screen. Failing that, will 120 * minimize the amount the target window is off screen. The method guarantees 121 * a non-null component will not be obscured.\p 122 * If the component is null, the target window is placed beside the parent 123 * window, to the Left, Right, Below or Above it.\b 124 * Should be called after target is packed and <strong>before</strong> target is 125 * set visible. 126 * @param parent Window containing the Component 127 * @param comp Component contained in the parent Window. May be null. 128 * @param target a popup or some kind of window associated with the component 129 * 130 * @return the location Point to open the target window. 131 */ 132 public Point nextTo(Window parent, Component comp, Window target) { 133 if (target == null || parent == null) { 134 return new Point(0, 0); 135 } 136 Point loc = findLocation(parent, comp, target); 137 if (log.isDebugEnabled()) { 138 log.debug("return target location: X= {}, Y= {}", loc.x, loc.y); 139 } 140 target.setLocation(loc); 141 return loc; 142 } 143 144 private Point findLocation(Window parent, Component comp, Window target) { 145 Point loc; 146 Point parentLoc = parent.getLocation(); 147 Dimension parentDim = parent.getSize(); 148 int screenNum = getScreenNum(parent); 149 Dimension parentScreen =getScreenSize(screenNum); 150 Dimension targetDim = target.getPreferredSize(); 151 Point compLoc; 152 Dimension compDim; 153 int margin; 154 if (comp != null) { 155 try { 156 compLoc = new Point(comp.getLocationOnScreen()); 157 } catch (IllegalComponentStateException icse) { 158 compLoc = comp.getLocation(); 159 compLoc = new Point(compLoc.x + parentLoc.x, compLoc.y + parentLoc.y); 160 } 161 compDim = comp.getSize(); 162 margin = 20; 163 } else { 164 compLoc = parentLoc; 165 compDim = parentDim; 166 margin = 0; 167 } 168 int num = screenNum - 1; 169 int screenLeft = 0; 170 while (num >= 0) { 171 screenLeft += getScreenSize(num).width; 172 num--; 173 } 174 int screenRight = screenLeft + parentScreen.width; 175 if (log.isDebugEnabled()) { 176 log.debug("parent at loc ({}, {}) is on screen #{}. Size: width= {}, height= {}", 177 parentLoc.x, parentLoc.y, screenNum, parentDim.width, parentDim.height); 178 log.debug("Component at loc ({}, {}). Size: width= {}, height= {}", 179 compLoc.x, compLoc.y, compDim.width, compDim.height); 180 log.debug("targetDim: width= {}, height= {}. screenLeft= {}, screen= {} x {}", 181 targetDim.width, targetDim.height, screenLeft, parentScreen.width, parentScreen.height); 182 } 183 184 // try left or right of Component 185 int xr = compLoc.x + compDim.width + margin; 186 int xl = compLoc.x - targetDim.width - margin; 187 // compute the corresponding vertical offset 188 int hOff = compLoc.y + (compDim.height - targetDim.height)/2; 189 if (hOff + targetDim.height > parentScreen.height) { 190 hOff = parentScreen.height - targetDim.height; 191 } 192 if (hOff < 0) { 193 hOff = 0; 194 } 195 // try above or below Component 196 int yb = compLoc.y + compDim.height + margin; 197 int ya = compLoc.y - targetDim.height - margin; 198 // compute the corresponding horizontal offset 199 int vOff = compLoc.x + (compDim.width - targetDim.width)/2; 200 if (vOff + targetDim.width > parentScreen.width - targetDim.width) { 201 vOff = parentScreen.width - targetDim.width; 202 } 203 if (vOff < screenLeft) { 204 vOff = screenLeft; 205 } 206 if (log.isDebugEnabled()) { 207 log.debug("UpperleftCorners: xl=({},{}), xr=({},{}), yb=({},{}), ya=({},{})", 208 xl,hOff, xr,hOff, vOff,yb, vOff,ya); 209 } 210 211 // try to keep completely within the parent window 212 if (xl >= parentLoc.x){ 213 return new Point(xl, hOff); 214 } else if ((xr + targetDim.width <= parentLoc.x + parentDim.width)) { 215 return new Point(xr, hOff); 216 } else if (yb + targetDim.height <= parentLoc.y + parentDim.height) { 217 return new Point(vOff, yb); 218 } else if (ya >= parentLoc.y) { 219 if (ya < 0) { 220 ya = 0; 221 } 222 return new Point(vOff, ya); 223 } 224 // none were entirely within the parent window 225 226 // try to keep completely within the parent screen 227 if (log.isDebugEnabled()) { 228 log.debug("Off screen: left= {}, right = {}, below= {}, above= {}", 229 xl, xr, yb, ya); 230 } 231 if (xl > screenLeft){ 232 return new Point(xl, hOff); 233 } else if (xr + targetDim.width <= screenRight) { 234 return new Point(xr, hOff); 235 } else if (yb + targetDim.height <= parentScreen.height) { 236 return new Point(vOff, yb); 237 } else if (ya >= 0) { 238 return new Point(vOff, ya); 239 } 240 241 // none were entirely within the parent screen. 242 // position, but insure target stays on the total screen 243 if (log.isDebugEnabled()) log.debug("Outside: widthUpToParent= {}, _totalScreenWidth= {}, screenHeight={}", 244 parentLoc.x, _totalScreenDim.width, parentScreen.height); 245 int offScreen = screenLeft - xl; 246 int minOff = offScreen; 247 log.debug("offScreen= {} minOff= {}, xl= {}", offScreen, minOff, xl); 248 if (xl < 0) { 249 xl = 0; 250 } 251 loc = new Point(xl, hOff); 252 253 offScreen = xr + targetDim.width - screenRight; 254 xr = screenRight - targetDim.width; 255 log.debug("offScreen= {} minOff= {}, xr= {}", offScreen, minOff, xr); 256 if (offScreen < minOff) { 257 minOff = offScreen; 258 loc = new Point(xr, hOff); 259 } 260 261 offScreen = (yb + targetDim.height) - parentScreen.height; 262 yb = parentScreen.height - targetDim.height; 263 log.debug("offScreen= {} minOff = {}, yb= {}", offScreen, minOff, yb); 264 if (offScreen < minOff) { 265 minOff = offScreen; 266 if (yb < 0) { 267 yb = 0; 268 } 269 loc = new Point(vOff, yb); 270 } 271 272 offScreen = -ya; // !(ya >= 0) 273 log.debug("offScreen= {} minOff = {}, ya= {}", offScreen, minOff, ya); 274 if (offScreen < minOff) { 275 ya = 0; 276 loc = new Point(vOff, ya); 277 } 278 279 return loc; 280 } 281 282 private final static Logger log = LoggerFactory.getLogger(PlaceWindow.class); 283}