package games.strategy.triplea.ui; import java.awt.BorderLayout; import java.awt.Window; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.logging.Logger; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JMenuBar; import javax.swing.JPanel; import javax.swing.SwingUtilities; import games.strategy.debug.ClientLogger; import games.strategy.engine.ClientContext; import games.strategy.engine.ClientFileSystemHelper; import games.strategy.engine.data.GameData; import games.strategy.engine.framework.LocalPlayers; import games.strategy.triplea.Constants; import games.strategy.triplea.ResourceLoader; import games.strategy.util.CountDownLatchHandler; public abstract class AbstractUIContext implements IUIContext { protected static final String UNIT_SCALE_PREF = "UnitScale"; protected static final String MAP_SKIN_PREF = "MapSkin"; protected static final String MAP_SCALE_PREF = "MapScale"; protected static final Logger s_logger = Logger.getLogger(AbstractUIContext.class.getName()); protected static String m_mapDir; protected static final String LOCK_MAP = "LockMap"; protected static final String SHOW_END_OF_TURN_REPORT = "ShowEndOfTurnReport"; protected static final String SHOW_TRIGGERED_NOTIFICATIONS = "ShowTriggeredNotifications"; protected static final String SHOW_TRIGGERED_CHANCE_SUCCESSFUL = "ShowTriggeredChanceSuccessful"; protected static final String SHOW_TRIGGERED_CHANCE_FAILURE = "ShowTriggeredChanceFailure"; protected static final String SHOW_BATTLES_BETWEEN_AIS = "ShowBattlesBetweenAIs"; protected static ResourceLoader m_resourceLoader; // instance protected boolean m_isShutDown = false; protected final List<Window> m_windowsToCloseOnShutdown = new ArrayList<>(); protected final List<Active> m_activeToDeactivate = new ArrayList<>(); protected final CountDownLatchHandler m_latchesToCloseOnShutdown = new CountDownLatchHandler(false); protected LocalPlayers m_localPlayers; protected double m_scale = 1; public static ResourceLoader getResourceLoader() { return m_resourceLoader; } public static int getAIPauseDuration() { return ClientContext.aiSettings().getAiPauseDuration(); } public static void setAIPauseDuration(final int value) { ClientContext.aiSettings().setAiPauseDuration(String.valueOf(value)); } @Override public double getScale() { return m_scale; } @Override public void setScale(final double scale) { m_scale = scale; final Preferences prefs = getPreferencesMapOrSkin(getMapDir()); prefs.putDouble(MAP_SCALE_PREF, scale); try { prefs.flush(); } catch (final BackingStoreException e) { ClientLogger.logQuietly(e); } } /** * Get the preferences for the map. */ protected static Preferences getPreferencesForMap(final String mapName) { return Preferences.userNodeForPackage(AbstractUIContext.class).node(mapName); } /** * Get the preferences for the map or map skin. */ protected static Preferences getPreferencesMapOrSkin(final String mapDir) { return Preferences.userNodeForPackage(AbstractUIContext.class).node(mapDir); } protected static String getDefaultMapDir(final GameData data) { final String mapName = (String) data.getProperties().get(Constants.MAP_NAME); if (mapName == null || mapName.trim().length() == 0) { throw new IllegalStateException("Map name property not set on game"); } final Preferences prefs = getPreferencesForMap(mapName); final String mapDir = prefs.get(MAP_SKIN_PREF, mapName); // check for existence try { ResourceLoader.getMapResourceLoader(mapDir).close(); } catch (final RuntimeException re) { // an error, clear the skin prefs.remove(MAP_SKIN_PREF); // return the default return mapName; } return mapDir; } @Override public void setDefaultMapDir(final GameData data) { internalSetMapDir(getDefaultMapDir(data), data); } @Override public void setMapDir(final GameData data, final String mapDir) { internalSetMapDir(mapDir, data); this.getMapData().verify(data); // set the default after internal succeeds, if an error is thrown // we don't want to persist it final String mapName = (String) data.getProperties().get(Constants.MAP_NAME); final Preferences prefs = getPreferencesForMap(mapName); prefs.put(MAP_SKIN_PREF, mapDir); try { prefs.flush(); } catch (final BackingStoreException e) { ClientLogger.logQuietly(e); } } protected abstract void internalSetMapDir(final String dir, final GameData data); public static String getMapDir() { return m_mapDir; } @Override public void removeActive(final Active actor) { if (m_isShutDown) { return; } synchronized (this) { m_activeToDeactivate.remove(actor); } } /** * Add a latch that will be released when the game shuts down. */ @Override public void addActive(final Active actor) { if (m_isShutDown) { closeActor(actor); return; } synchronized (this) { if (m_isShutDown) { closeActor(actor); return; } m_activeToDeactivate.add(actor); } } /** * Add a latch that will be released when the game shuts down. */ @Override public void addShutdownLatch(final CountDownLatch latch) { m_latchesToCloseOnShutdown.addShutdownLatch(latch); } @Override public void removeShutdownLatch(final CountDownLatch latch) { m_latchesToCloseOnShutdown.removeShutdownLatch(latch); } @Override public CountDownLatchHandler getCountDownLatchHandler() { return m_latchesToCloseOnShutdown; } /** * Add a latch that will be released when the game shuts down. */ @Override public void addShutdownWindow(final Window window) { if (m_isShutDown) { closeWindow(window); return; } synchronized (this) { if (m_isShutDown) { closeWindow(window); return; } m_windowsToCloseOnShutdown.add(window); } } protected static void closeWindow(final Window window) { window.setVisible(false); SwingUtilities.invokeLater(() -> { // Having dispose run on anything but the Swing Event Dispatch Thread is very dangerous. // This is because dispose will call invokeAndWait if it is not on this thread already. // If you are calling this method while holding a lock on an object, while the EDT is separately // waiting for that lock, then you have a deadlock. // A real life example: player disconnects while you have the battle calc open. // Non-EDT thread does shutdown on IGame and UIContext, causing btl calc to shutdown, which calls the // window closed event on the EDT, and waits for the lock on UIContext to removeShutdownWindow, meanwhile // our non-EDT tries to dispose the battle panel, which requires the EDT with a invokeAndWait, resulting in a // deadlock. window.dispose(); // there is a bug in java (1.50._06 for linux at least) // where frames are not garbage collected. // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6364875 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6368950 // so remove all references to everything // to minimize the damage if (window instanceof JFrame) { final JFrame frame = ((JFrame) window); final JMenuBar menu = frame.getJMenuBar(); if (menu != null) { while (menu.getMenuCount() > 0) { menu.remove(0); } } frame.setMenuBar(null); frame.setJMenuBar(null); frame.getRootPane().removeAll(); frame.getRootPane().setJMenuBar(null); frame.getContentPane().removeAll(); frame.getContentPane().setLayout(new BorderLayout()); frame.setContentPane(new JPanel()); frame.setIconImage(null); clearInputMap(frame.getRootPane()); } }); } protected static void clearInputMap(final JComponent c) { c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).clear(); c.getInputMap(JComponent.WHEN_FOCUSED).clear(); c.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).clear(); c.getActionMap().clear(); } @Override public void removeShutdownWindow(final Window window) { if (m_isShutDown) { return; } synchronized (this) { m_windowsToCloseOnShutdown.remove(window); } } @Override public boolean isShutDown() { return m_isShutDown; } @Override public void shutDown() { synchronized (this) { if (m_isShutDown) { return; } m_isShutDown = true; m_latchesToCloseOnShutdown.shutDown(); for (final Window window : m_windowsToCloseOnShutdown) { closeWindow(window); } for (final Active actor : m_activeToDeactivate) { closeActor(actor); } m_activeToDeactivate.clear(); m_windowsToCloseOnShutdown.clear(); } } /** * returns the map skins for the game data. * returns is a map of display-name -> map directory */ public static Map<String, String> getSkins(final GameData data) { final String mapName = data.getProperties().get(Constants.MAP_NAME).toString(); final Map<String, String> rVal = new LinkedHashMap<>(); rVal.put("Original", mapName); rVal.putAll(getSkins(mapName)); return rVal; } private static Map<String, String> getSkins(final String mapName) { final Map<String, String> rVal = new HashMap<>(); final File[] files = ClientFileSystemHelper.getUserMapsFolder().listFiles(); if (files == null) { return rVal; } for (final File f : files) { if (mapSkinNameMatchesMapName(f.getName(), mapName)) { final String displayName = f.getName().replace(mapName + "-", "").replace("-master", "").replace(".zip", ""); rVal.put(displayName, f.getName()); } } return rVal; } private static boolean mapSkinNameMatchesMapName(final String mapSkin, final String mapName) { return mapSkin.startsWith(mapName) && mapSkin.toLowerCase().contains("skin") && mapSkin.contains("-") && !mapSkin.endsWith("properties"); } private static void closeActor(final Active actor) { try { actor.deactivate(); } catch (final RuntimeException e) { ClientLogger.logQuietly(e); } } @Override public boolean getLockMap() { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); return prefs.getBoolean(LOCK_MAP, false); } @Override public void setLockMap(final boolean aBool) { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); prefs.putBoolean(LOCK_MAP, aBool); try { prefs.flush(); } catch (final BackingStoreException ex) { ex.printStackTrace(); } } @Override public boolean getShowEndOfTurnReport() { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); return prefs.getBoolean(SHOW_END_OF_TURN_REPORT, true); } @Override public void setShowEndOfTurnReport(final boolean value) { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); prefs.putBoolean(SHOW_END_OF_TURN_REPORT, value); try { prefs.flush(); } catch (final BackingStoreException ex) { ClientLogger.logQuietly(ex); } } @Override public boolean getShowTriggeredNotifications() { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); return prefs.getBoolean(SHOW_TRIGGERED_NOTIFICATIONS, true); } @Override public void setShowTriggeredNotifications(final boolean value) { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); prefs.putBoolean(SHOW_TRIGGERED_NOTIFICATIONS, value); try { prefs.flush(); } catch (final BackingStoreException ex) { ex.printStackTrace(); } } @Override public boolean getShowTriggerChanceSuccessful() { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); return prefs.getBoolean(SHOW_TRIGGERED_CHANCE_SUCCESSFUL, true); } @Override public void setShowTriggerChanceSuccessful(final boolean value) { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); prefs.putBoolean(SHOW_TRIGGERED_CHANCE_SUCCESSFUL, value); try { prefs.flush(); } catch (final BackingStoreException ex) { ClientLogger.logQuietly(ex); } } @Override public boolean getShowTriggerChanceFailure() { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); return prefs.getBoolean(SHOW_TRIGGERED_CHANCE_FAILURE, true); } @Override public void setShowTriggerChanceFailure(final boolean value) { final Preferences prefs = Preferences.userNodeForPackage(AbstractUIContext.class); prefs.putBoolean(SHOW_TRIGGERED_CHANCE_FAILURE, value); try { prefs.flush(); } catch (final BackingStoreException ex) { ClientLogger.logQuietly(ex); } } @Override public boolean getShowBattlesBetweenAIs() { return ClientContext.aiSettings().showBattlesBetweenAi(); } @Override public void setShowBattlesBetweenAIs(final boolean aBool) { ClientContext.aiSettings().setShowBattlesBetweenAi(aBool); } @Override public LocalPlayers getLocalPlayers() { return m_localPlayers; } @Override public void setLocalPlayers(final LocalPlayers players) { m_localPlayers = players; } }