diff --git a/.gitattributes b/.gitattributes index c50b4c102fe..6bb27d532d2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13094,6 +13094,7 @@ res/defaults/gauntlet/LOCKED_DotP[!!-~]Preconstructed.dat -text res/defaults/gauntlet/LOCKED_Swimming[!!-~]With[!!-~]Sharks.dat -text res/defaults/home.xml svneol=native#text/xml res/defaults/match.xml svneol=native#text/xml +res/defaults/window.xml -text res/draft/cube_juzamjedi.draft -text res/draft/cube_skiera.draft -text res/draft/rankings.txt -text diff --git a/CHANGES.txt b/CHANGES.txt index cbc4e1990f8..7da93ff17d0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,12 @@ Forge Beta: 0#-##-2013 ver 1.4.6 Release Notes ------------- +- Window size/position now remembered between sessions - +Works with multiple monitors. +Remembers whether the window was maximized or un-maximized. +Remembers last un-maximized size even if the window is currently maximized such that, if you un-maximize the window, it will restore to that size at the center of the current monitor. +Window will be made accessible even if monitor setup or screen resolution is different between sessions. + - Zoomer Updates - Split cards (name contains "//") are now rotated 90 degrees for easier viewing. diff --git a/res/defaults/window.xml b/res/defaults/window.xml new file mode 100644 index 00000000000..35ddbd4665d --- /dev/null +++ b/res/defaults/window.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/main/java/forge/control/FControl.java b/src/main/java/forge/control/FControl.java index bc327819ad0..e270bcb3e7d 100644 --- a/src/main/java/forge/control/FControl.java +++ b/src/main/java/forge/control/FControl.java @@ -185,12 +185,18 @@ public enum FControl { Singletons.getModel().getQuest().load(QuestDataIO.loadData(data)); } - // Handles resizing in null layouts of layers in JLayeredPane. + // Handles resizing in null layouts of layers in JLayeredPane as well as saving window layout Singletons.getView().getFrame().addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(final ComponentEvent e) { - sizeChildren(); - } + @Override + public void componentResized(final ComponentEvent e) { + sizeChildren(); + SLayoutIO.saveWindowLayout(); + } + + @Override + public void componentMoved(final ComponentEvent e) { + SLayoutIO.saveWindowLayout(); + } }); FView.SINGLETON_INSTANCE.getLpnDocument().addMouseListener(SOverflowUtil.getHideOverflowListener()); diff --git a/src/main/java/forge/gui/framework/SDisplayUtil.java b/src/main/java/forge/gui/framework/SDisplayUtil.java index 0d09e31f696..307864c78ac 100644 --- a/src/main/java/forge/gui/framework/SDisplayUtil.java +++ b/src/main/java/forge/gui/framework/SDisplayUtil.java @@ -2,7 +2,12 @@ package forge.gui.framework; import java.awt.Color; import java.awt.Component; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; import java.awt.KeyboardFocusManager; +import java.awt.Point; +import java.awt.Rectangle; import java.util.Timer; import java.util.TimerTask; @@ -105,4 +110,18 @@ public class SDisplayUtil { }; FThreads.invokeInEdtLater(showTabRoutine); } + + public static Rectangle getScreenBoundsForPoint(Point point) { + Rectangle bounds; + for (GraphicsDevice device : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { + for (GraphicsConfiguration config : device.getConfigurations()) { + bounds = config.getBounds(); + if (bounds.contains(point)) { + return bounds; + } + } + } + //return bounds of default monitor if point not on any screen + return GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds(); + } } diff --git a/src/main/java/forge/gui/framework/SLayoutIO.java b/src/main/java/forge/gui/framework/SLayoutIO.java index bb5f868f1e8..c4bb1232567 100644 --- a/src/main/java/forge/gui/framework/SLayoutIO.java +++ b/src/main/java/forge/gui/framework/SLayoutIO.java @@ -1,6 +1,10 @@ package forge.gui.framework; import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.Point; +import java.awt.Rectangle; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -12,6 +16,7 @@ import java.util.List; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicBoolean; +import javax.swing.JFrame; import javax.swing.border.EmptyBorder; import javax.xml.stream.XMLEventFactory; import javax.xml.stream.XMLEventReader; @@ -41,7 +46,6 @@ import forge.view.FView; *

(S at beginning of class name denotes a static factory.) */ public final class SLayoutIO { - /** Each cell must save these elements of its display. */ private static class Property { public final static String x = "x"; public final static String y = "y"; @@ -49,11 +53,203 @@ public final class SLayoutIO { public final static String h = "h"; public final static String sel = "sel"; public final static String doc = "doc"; + public final static String state = "state"; } private static final XMLEventFactory EF = XMLEventFactory.newInstance(); private static final XMLEvent NEWLINE = EF.createDTD("\n"); private static final XMLEvent TAB = EF.createDTD("\t"); + + private static int normalWindowWidth, normalWindowHeight; + private final static AtomicBoolean saveWindowRequested = new AtomicBoolean(false); + + public static void saveWindowLayout() { + if (saveWindowRequested.getAndSet(true)) { return; } + FThreads.delay(500, new Runnable() { + @Override + public void run() { + finishSaveWindowLayout(); + saveWindowRequested.set(false); + } + }); + } + + private synchronized static void finishSaveWindowLayout() { + final JFrame window = FView.SINGLETON_INSTANCE.getFrame(); + final int state = window.getExtendedState(); + if (state == Frame.ICONIFIED) { return; } //don't update saved layout if minimized + + final Rectangle bounds = window.getBounds(); + final int x = bounds.x; + final int y = bounds.y; + if ((state & Frame.MAXIMIZED_HORIZ) != Frame.MAXIMIZED_HORIZ) { //only modify saved width if not maximized horizontally + normalWindowWidth = bounds.width; + } + else if (normalWindowWidth == 0) { + normalWindowWidth = window.getMinimumSize().width; + } + if ((state & Frame.MAXIMIZED_VERT) != Frame.MAXIMIZED_VERT) { //only modify saved width if not maximized vertically + normalWindowHeight = bounds.height; + } + else if (normalWindowHeight == 0) { + normalWindowHeight = window.getMinimumSize().height; + } + + final FileLocation file = NewConstants.WINDOW_LAYOUT_FILE; + final String fWriteTo = file.userPrefLoc; + final XMLOutputFactory out = XMLOutputFactory.newInstance(); + FileOutputStream fos = null; + XMLEventWriter writer = null; + try { + fos = new FileOutputStream(fWriteTo); + writer = out.createXMLEventWriter(fos); + + writer.add(EF.createStartDocument()); + writer.add(NEWLINE); + writer.add(EF.createStartElement("", "", "layout")); + writer.add(EF.createAttribute(Property.x, String.valueOf(x))); + writer.add(EF.createAttribute(Property.y, String.valueOf(y))); + writer.add(EF.createAttribute(Property.w, String.valueOf(normalWindowWidth))); + writer.add(EF.createAttribute(Property.h, String.valueOf(normalWindowHeight))); + writer.add(EF.createAttribute(Property.state, String.valueOf(state))); + writer.add(EF.createEndElement("", "", "layout")); + writer.flush(); + writer.add(EF.createEndDocument()); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block ignores the exception, but sends it to System.err and probably forge.log. + e.printStackTrace(); + } catch (XMLStreamException e) { + // TODO Auto-generated catch block ignores the exception, but sends it to System.err and probably forge.log. + e.printStackTrace(); + } finally { + if (writer != null ) { + try { writer.close(); } catch (XMLStreamException e) {} + } + if ( fos != null ) { + try { fos.close(); } catch (IOException e) {} + } + } + } + + public static void loadWindowLayout() { + final JFrame window = FView.SINGLETON_INSTANCE.getFrame(); + final XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + final FileLocation file = NewConstants.WINDOW_LAYOUT_FILE; + boolean usedCustomPrefsFile = false; + FileInputStream fis = null; + try { + File userSetting = new File(file.userPrefLoc); + if (userSetting.exists()) { + usedCustomPrefsFile = true; + fis = new FileInputStream(userSetting); + } + else { + fis = new FileInputStream(file.defaultLoc); + } + + XMLEventReader reader = null; + try { + XMLEvent event; + StartElement element; + Iterator attributes; + Attribute attribute; + reader = inputFactory.createXMLEventReader(fis); + + while (reader != null && reader.hasNext()) { + event = reader.nextEvent(); + + if (event.isStartElement()) { + element = event.asStartElement(); + + if (element.getName().getLocalPart().equals("layout")) { + attributes = element.getAttributes(); + Dimension minSize = window.getMinimumSize(); + int x = 0, y = 0, w = minSize.width, h = minSize.height, state = Frame.MAXIMIZED_BOTH; + while (attributes.hasNext()) { + attribute = (Attribute) attributes.next(); + switch (attribute.getName().toString()) { + case Property.x: x = Integer.parseInt(attribute.getValue()); break; + case Property.y: y = Integer.parseInt(attribute.getValue()); break; + case Property.w: w = Integer.parseInt(attribute.getValue()); break; + case Property.h: h = Integer.parseInt(attribute.getValue()); break; + case Property.state: state = Integer.parseInt(attribute.getValue()); break; + } + } + + //set normal size to loaded size + normalWindowWidth = w; + normalWindowHeight = h; + + //update x and y if needed such that window is centered on that axis + //when un-maximized if starting out maximized on that axis + int centerX = x + w / 2; + int centerY = y + h / 2; + Rectangle screenBounds = SDisplayUtil.getScreenBoundsForPoint(new Point(centerX, centerY)); + if ((state & Frame.MAXIMIZED_HORIZ) == Frame.MAXIMIZED_HORIZ) { + x = screenBounds.x + (screenBounds.width - w) / 2; + } + else { //ensure the window is accessible + if (centerX < screenBounds.x) { + x = screenBounds.x; + } + else if (centerX > screenBounds.x + screenBounds.width) { + x = screenBounds.x + screenBounds.width - w; + if (x < screenBounds.x) { + x = screenBounds.x; + } + } + } + if ((state & Frame.MAXIMIZED_VERT) == Frame.MAXIMIZED_VERT) { + y = screenBounds.y + (screenBounds.height - h) / 2; + } + else { //ensure the window is accessible + if (centerY < screenBounds.y) { + y = screenBounds.y; + } + else if (centerY > screenBounds.y + screenBounds.height) { + y = screenBounds.y + screenBounds.height - h; + if (y < screenBounds.y) { + y = screenBounds.y; + } + } + } + + window.setBounds(x, y, w, h); + window.setExtendedState(state); + } + } + } + } + catch (final Exception e) { + try { + if (reader != null) { reader.close(); }; + } + catch (final XMLStreamException x) { + e.printStackTrace(); + } + e.printStackTrace(); + if (usedCustomPrefsFile) { + throw new InvalidLayoutFileException(); + } + else { + throw new RuntimeException(e); + } + } + } + catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + finally { + if (fis != null ) { + try { + fis.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + } + } /** * Gets preferred layout file corresponding to current state of UI. diff --git a/src/main/java/forge/properties/NewConstants.java b/src/main/java/forge/properties/NewConstants.java index d1a6bd624ed..f3a1abdeddc 100644 --- a/src/main/java/forge/properties/NewConstants.java +++ b/src/main/java/forge/properties/NewConstants.java @@ -86,6 +86,7 @@ public final class NewConstants { // data that has defaults in the program dir but overrides/additions in the user dir private static final String _DEFAULTS_DIR = _RES_ROOT + "defaults/"; public static final FileLocation EDITOR_PREFERENCES_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "editor.preferences"); + public static final FileLocation WINDOW_LAYOUT_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "window.xml"); public static final FileLocation HOME_LAYOUT_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "home.xml"); public static final FileLocation MATCH_LAYOUT_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "match.xml"); public static final FileLocation EDITOR_LAYOUT_FILE = new FileLocation(_DEFAULTS_DIR, USER_PREFS_DIR, "editor.xml"); diff --git a/src/main/java/forge/view/FView.java b/src/main/java/forge/view/FView.java index a3f38796c40..b5d127ba5e9 100644 --- a/src/main/java/forge/view/FView.java +++ b/src/main/java/forge/view/FView.java @@ -4,7 +4,6 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; -import java.awt.Frame; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -42,6 +41,7 @@ import forge.gui.deckeditor.VDeckEditorUI; import forge.gui.framework.DragCell; import forge.gui.framework.EDocID; import forge.gui.framework.SLayoutConstants; +import forge.gui.framework.SLayoutIO; import forge.gui.home.VHomeUI; import forge.gui.match.TargetingOverlay; import forge.gui.match.VMatchUI; @@ -95,7 +95,6 @@ public enum FView { // Frame styling frmDocument.setMinimumSize(new Dimension(800, 600)); frmDocument.setLocationRelativeTo(null); - frmDocument.setExtendedState(frmDocument.getExtendedState() | Frame.MAXIMIZED_BOTH); frmDocument.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); frmDocument.setIconImage(FSkin.getIcon(FSkin.InterfaceIcons.ICO_FAVICON).getImage()); frmDocument.setTitle("Forge: " + BuildInfo.getVersionString()); @@ -141,9 +140,8 @@ public enum FView { FView.this.frmSplash.dispose(); FView.this.frmSplash = null; - - // Allow OS to set location. Hopefully this doesn't cause issues - frmDocument.setLocationByPlatform(true); + + SLayoutIO.loadWindowLayout(); frmDocument.setVisible(true); // remove this once our userbase has been migrated to the profile layout