/* This file is part of jpcsp. Jpcsp is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Jpcsp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Jpcsp. If not, see <http://www.gnu.org/licenses/>. */ package jpcsp.settings; import java.awt.Dimension; import java.awt.Font; import java.awt.Point; import java.awt.event.KeyEvent; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.EnumMap; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Properties; import jpcsp.Controller; import jpcsp.Emulator; import jpcsp.State; import jpcsp.Controller.keyCode; import jpcsp.GUI.RecentElement; import jpcsp.util.Utilities; /** * * @author spip2001, gid15 */ public class Settings { private final static String SETTINGS_FILE_NAME = "Settings.properties"; private final static String DEFAULT_SETTINGS_FILE_NAME = "/jpcsp/DefaultSettings.properties"; private static Settings instance = null; private Properties defaultSettings; private SortedProperties loadedSettings; private Properties patchSettings; private HashMap<String, List<ISettingsListener>> listenersByKey; private List<SettingsListenerInfo> allListeners; private boolean useUmdIdForDiscDirectory; public static Settings getInstance() { if (instance == null) { instance = new Settings(); } return instance; } private Settings() { listenersByKey = new HashMap<String, List<ISettingsListener>>(); allListeners = new LinkedList<SettingsListenerInfo>(); defaultSettings = new Properties(); patchSettings = new Properties(); InputStream defaultSettingsStream = null; InputStream loadedSettingsStream = null; try { defaultSettingsStream = getClass().getResourceAsStream(DEFAULT_SETTINGS_FILE_NAME); defaultSettings.load(defaultSettingsStream); loadedSettings = new SortedProperties(defaultSettings); File settingsFile = new File(SETTINGS_FILE_NAME); settingsFile.createNewFile(); loadedSettingsStream = new BufferedInputStream(new FileInputStream(settingsFile)); loadedSettings.load(loadedSettingsStream); } catch (FileNotFoundException e) { Emulator.log.error("Settings file not found:", e); } catch (IOException e) { Emulator.log.error("Problem loading settings:", e); } catch (NullPointerException e) { // This except is thrown by java.util.Properties when the directory // contains special characters or is too long. Emulator.log.error("Could not initialize properly Jpcsp, try to install jpcsp directly under C:\\jpcsp", e); } finally { Utilities.close(defaultSettingsStream, loadedSettingsStream); } } public String getTmpDirectory() { return readString("emu.tmppath") + File.separatorChar; } public String getDiscTmpDirectory() { return getTmpDirectory() + getDiscDirectory(); } public String getDiscDirectory() { if (useUmdIdForDiscDirectory) { return String.format("%s-%s%c", State.discId, State.umdId, File.separatorChar); } return String.format("%s%c", State.discId, File.separatorChar); } public void loadPatchSettings() { Properties previousPatchSettings = new Properties(patchSettings); patchSettings.clear(); String discId = State.discId; if (!discId.equals(State.DISCID_UNKNOWN_FILE) && !discId.equals(State.DISCID_UNKNOWN_NOTHING_LOADED)) { // Try to read patch settings using the Disc ID and the UMD ID. String patchFileName = String.format("patches/%s-%s.properties", discId, State.umdId); File patchFile = new File(patchFileName); if (!patchFile.exists()) { // If no patch settings are found using the UMD ID, try with only the Disc ID patchFileName = String.format("patches/%s.properties", discId); patchFile = new File(patchFileName); useUmdIdForDiscDirectory = false; } else { useUmdIdForDiscDirectory = true; } InputStream patchSettingsStream = null; try { patchSettingsStream = new BufferedInputStream(new FileInputStream(patchFile)); patchSettings.load(patchSettingsStream); Emulator.log.info(String.format("Overwriting default settings with patch file '%s'", patchFileName)); } catch (FileNotFoundException e) { Emulator.log.debug(String.format("Patch file not found: %s", e.toString())); } catch (IOException e) { Emulator.log.error("Problem loading patch:", e); } finally { Utilities.close(patchSettingsStream); } } // Trigger the settings listener for all values modified // by the new patch settings. for (Enumeration<Object> e = patchSettings.keys(); e.hasMoreElements();) { String key = e.nextElement().toString(); previousPatchSettings.remove(key); String value = patchSettings.getProperty(key); if (!value.equals(loadedSettings.getProperty(key))) { triggerSettingsListener(key, value); } } // Trigger the settings listener for all values that disappeared from the // previous patch settings. for (Enumeration<Object> e = previousPatchSettings.keys(); e.hasMoreElements();) { String key = e.nextElement().toString(); String oldValue = previousPatchSettings.getProperty(key); String newValue = getProperty(key); if (!oldValue.equals(newValue)) { triggerSettingsListener(key, newValue); } } } /** * Write settings in file * * @param doc Settings as XML document */ private void writeSettings() { BufferedOutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(SETTINGS_FILE_NAME)); loadedSettings.store(out, null); } catch (FileNotFoundException e) { Emulator.log.error("Settings file not found:", e); } catch (IOException e) { Emulator.log.error("Problem saving settings:", e); } finally { Utilities.close(out); } } private String getProperty(String key) { String value = patchSettings.getProperty(key); if (value == null) { value = loadedSettings.getProperty(key); } return value; } private String getProperty(String key, String defaultValue) { String value = patchSettings.getProperty(key); if (value == null) { value = loadedSettings.getProperty(key, defaultValue); } return value; } private void setProperty(String key, String value) { String previousValue = getProperty(key); // Store the value in the loadedSettings, // the patchSettings staying unchanged. loadedSettings.setProperty(key, value); // Retrieve the new value (might be different from the value // just set in the loadedSettings as it might be overwritten by // a patchSettings). String newValue = getProperty(key); // Trigger the settings listener if this resulted in a changed value if (previousValue == null || !previousValue.equals(newValue)) { triggerSettingsListener(key, newValue); } } public boolean hasProperty(String key) { return loadedSettings.containsKey(key); } public void clearProperty(String key) { loadedSettings.remove(key); } public Point readWindowPos(String windowname) { String x = getProperty("gui.windows." + windowname + ".x"); String y = getProperty("gui.windows." + windowname + ".y"); // check if the read position is valid - i.e. already exists if (x == null || y == null) { return null; } Point position = new Point(); position.x = Integer.parseInt(x); position.y = Integer.parseInt(y); return position; } public Dimension readWindowSize(String windowname) { String w = getProperty("gui.windows." + windowname + ".w"); String h = getProperty("gui.windows." + windowname + ".h"); // check if the read size is valid - i.e. already exists if (w == null || h == null) { return null; } Dimension dimension = new Dimension(); dimension.width = Integer.parseInt(w); dimension.height = Integer.parseInt(h); return dimension; } public void writeWindowPos(String windowname, Point pos) { setProperty("gui.windows." + windowname + ".x", Integer.toString(pos.x)); setProperty("gui.windows." + windowname + ".y", Integer.toString(pos.y)); writeSettings(); } public void writeWindowSize(String windowname, Dimension dimension) { setProperty("gui.windows." + windowname + ".w", Integer.toString(dimension.width)); setProperty("gui.windows." + windowname + ".h", Integer.toString(dimension.height)); writeSettings(); } public static boolean parseBool(String value) { if ("true".equalsIgnoreCase(value)) { return true; } if ("false".equalsIgnoreCase(value)) { return false; } return Integer.parseInt(value) != 0; } public static int parseInt(String value) { value = value.trim(); if (value.startsWith("0x")) { return Integer.parseInt(value.substring(2), 16); } return Integer.parseInt(value); } public static float parseFloat(String value) { return Float.parseFloat(value); } public boolean readBool(String option) { String bool = getProperty(option); if (bool == null) { return false; } return parseBool(bool); } public int readInt(String option) { return readInt(option, 0); } public int readInt(String option, int defaultValue) { String value = getProperty(option); if (value == null) { return defaultValue; } return parseInt(value); } public void writeBool(String option, boolean value) { String state = value ? "1" : "0"; setProperty(option, state); writeSettings(); } public void writeInt(String option, int value) { String state = Integer.toString(value); setProperty(option, state); writeSettings(); } public String readString(String option) { return readString(option, ""); } public String readString(String option, String defaultValue) { return getProperty(option, defaultValue); } public boolean isOptionFromPatch(String option) { return patchSettings.containsKey(option); } public void writeString(String option, String value) { setProperty(option, value); writeSettings(); } public void writeFloat(String option, float value) { String state = Float.toString(value); setProperty(option, state); writeSettings(); } public float readFloat(String option, float defaultValue) { String value = getProperty(option); if (value == null) { return defaultValue; } return parseFloat(value); } public HashMap<Integer, keyCode> loadKeys() { HashMap<Integer, keyCode> m = new HashMap<Integer, keyCode>(22); m.put(readKey("up"), keyCode.UP); m.put(readKey("down"), keyCode.DOWN); m.put(readKey("left"), keyCode.LEFT); m.put(readKey("right"), keyCode.RIGHT); m.put(readKey("analogUp"), keyCode.LANUP); m.put(readKey("analogDown"), keyCode.LANDOWN); m.put(readKey("analogLeft"), keyCode.LANLEFT); m.put(readKey("analogRight"), keyCode.LANRIGHT); if (Controller.getInstance().hasRightAnalogController()) { m.put(readKey("rightAnalogUp"), keyCode.RANUP); m.put(readKey("rightAnalogDown"), keyCode.RANDOWN); m.put(readKey("rightAnalogLeft"), keyCode.RANLEFT); m.put(readKey("rightAnalogRight"), keyCode.RANRIGHT); } m.put(readKey("start"), keyCode.START); m.put(readKey("select"), keyCode.SELECT); m.put(readKey("triangle"), keyCode.TRIANGLE); m.put(readKey("square"), keyCode.SQUARE); m.put(readKey("circle"), keyCode.CIRCLE); m.put(readKey("cross"), keyCode.CROSS); m.put(readKey("lTrigger"), keyCode.L1); m.put(readKey("rTrigger"), keyCode.R1); m.put(readKey("home"), keyCode.HOME); m.put(readKey("hold"), keyCode.HOLD); m.put(readKey("volPlus"), keyCode.VOLPLUS); m.put(readKey("volMin"), keyCode.VOLMIN); m.put(readKey("screen"), keyCode.SCREEN); m.put(readKey("music"), keyCode.MUSIC); return m; } public Map<keyCode, String> loadController() { Map<keyCode, String> m = new EnumMap<keyCode, String>(keyCode.class); m.put(keyCode.UP, readController("up")); m.put(keyCode.DOWN, readController("down")); m.put(keyCode.LEFT, readController("left")); m.put(keyCode.RIGHT, readController("right")); m.put(keyCode.LANUP, readController("analogUp")); m.put(keyCode.LANDOWN, readController("analogDown")); m.put(keyCode.LANLEFT, readController("analogLeft")); m.put(keyCode.LANRIGHT, readController("analogRight")); if (Controller.getInstance().hasRightAnalogController()) { m.put(keyCode.RANUP, readController("rightAnalogUp")); m.put(keyCode.RANDOWN, readController("rightAnalogDown")); m.put(keyCode.RANLEFT, readController("rightAnalogLeft")); m.put(keyCode.RANRIGHT, readController("rightAnalogRight")); } m.put(keyCode.START, readController("start")); m.put(keyCode.SELECT, readController("select")); m.put(keyCode.TRIANGLE, readController("triangle")); m.put(keyCode.SQUARE, readController("square")); m.put(keyCode.CIRCLE, readController("circle")); m.put(keyCode.CROSS, readController("cross")); m.put(keyCode.L1, readController("lTrigger")); m.put(keyCode.R1, readController("rTrigger")); m.put(keyCode.HOME, readController("home")); m.put(keyCode.HOLD, readController("hold")); m.put(keyCode.VOLPLUS, readController("volPlus")); m.put(keyCode.VOLMIN, readController("volMin")); m.put(keyCode.SCREEN, readController("screen")); m.put(keyCode.MUSIC, readController("music")); // Removed unset entries for (keyCode key : keyCode.values()) { if (m.get(key) == null) { m.remove(key); } } return m; } public void writeKeys(Map<Integer, keyCode> keys) { for (Map.Entry<Integer, keyCode> entry : keys.entrySet()) { keyCode key = entry.getValue(); int value = entry.getKey(); switch (key) { case DOWN: writeKey("down", value); break; case UP: writeKey("up", value); break; case LEFT: writeKey("left", value); break; case RIGHT: writeKey("right", value); break; case LANDOWN: writeKey("analogDown", value); break; case LANUP: writeKey("analogUp", value); break; case LANLEFT: writeKey("analogLeft", value); break; case LANRIGHT: writeKey("analogRight", value); break; case RANDOWN: writeKey("rightAnalogDown", value); break; case RANUP: writeKey("rightAnalogUp", value); break; case RANLEFT: writeKey("rightAnalogLeft", value); break; case RANRIGHT: writeKey("rightAnalogRight", value); break; case TRIANGLE: writeKey("triangle", value); break; case SQUARE: writeKey("square", value); break; case CIRCLE: writeKey("circle", value); break; case CROSS: writeKey("cross", value); break; case L1: writeKey("lTrigger", value); break; case R1: writeKey("rTrigger", value); break; case START: writeKey("start", value); break; case SELECT: writeKey("select", value); break; case HOME: writeKey("home", value); break; case HOLD: writeKey("hold", value); break; case VOLMIN: writeKey("volMin", value); break; case VOLPLUS: writeKey("volPlus", value); break; case SCREEN: writeKey("screen", value); break; case MUSIC: writeKey("music", value); break; case RELEASED: break; } } writeSettings(); } public void writeController(Map<keyCode, String> keys) { for (Map.Entry<keyCode, String> entry : keys.entrySet()) { keyCode key = entry.getKey(); String value = entry.getValue(); switch (key) { case DOWN: writeController("down", value); break; case UP: writeController("up", value); break; case LEFT: writeController("left", value); break; case RIGHT: writeController("right", value); break; case LANDOWN: writeController("analogDown", value); break; case LANUP: writeController("analogUp", value); break; case LANLEFT: writeController("analogLeft", value); break; case LANRIGHT: writeController("analogRight", value); break; case RANDOWN: writeController("rightAnalogDown", value); break; case RANUP: writeController("rightAnalogUp", value); break; case RANLEFT: writeController("rightAnalogLeft", value); break; case RANRIGHT: writeController("rightAnalogRight", value); break; case TRIANGLE: writeController("triangle", value); break; case SQUARE: writeController("square", value); break; case CIRCLE: writeController("circle", value); break; case CROSS: writeController("cross", value); break; case L1: writeController("lTrigger", value); break; case R1: writeController("rTrigger", value); break; case START: writeController("start", value); break; case SELECT: writeController("select", value); break; case HOME: writeController("home", value); break; case HOLD: writeController("hold", value); break; case VOLMIN: writeController("volMin", value); break; case VOLPLUS: writeController("volPlus", value); break; case SCREEN: writeController("screen", value); break; case MUSIC: writeController("music", value); break; case RELEASED: break; } } writeSettings(); } private int readKey(String keyName) { String str = getProperty("keys." + keyName); if (str == null) { return KeyEvent.VK_UNDEFINED; } return Integer.parseInt(str); } private void writeKey(String keyName, int key) { setProperty("keys." + keyName, Integer.toString(key)); } private String readController(String name) { return getProperty("controller." + name); } private void writeController(String name, String value) { setProperty("controller." + name, value); } private static class SortedProperties extends Properties { private static final long serialVersionUID = -8127868945637348944L; public SortedProperties(Properties defaultSettings) { super(defaultSettings); } @Override @SuppressWarnings({"unchecked", "rawtypes"}) public synchronized Enumeration<Object> keys() { Enumeration keysEnum = super.keys(); List keyList = Collections.list(keysEnum); Collections.sort(keyList); return Collections.enumeration(keyList); } } public void readRecent(String cat, List<RecentElement> recent) { for (int i = 0;; ++i) { String r = getProperty("gui.recent." + cat + "." + i); if (r == null) { break; } String title = getProperty("gui.recent." + cat + "." + i + ".title"); recent.add(new RecentElement(r, title)); } } public void writeRecent(String cat, List<RecentElement> recent) { Enumeration<Object> keys = loadedSettings.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); if (key.startsWith("gui.recent." + cat)) { loadedSettings.remove(key); } } int index = 0; for (RecentElement elem : recent) { setProperty("gui.recent." + cat + "." + index, elem.path); if (elem.title != null) { setProperty("gui.recent." + cat + "." + index + ".title", elem.title); } index++; } writeSettings(); } /** * Reads the following settings: gui.memStickBrowser.font.name=SansSerif * gui.memStickBrowser.font.file= gui.memStickBrowser.font.size=11 * * @return Tries to return a font in this order: - Font from local file * (somefont.ttf), - Font registered with the operating system, - SansSerif, * Plain, 11. */ private Font loadedFont = null; public Font getFont() { if (loadedFont != null) { return loadedFont; } Font font = new Font("SansSerif", Font.PLAIN, 1); int fontsize = 11; try { Font base = font; // Default font String fontname = readString("gui.font.name"); String fontfilename = readString("gui.font.file"); String fontsizestr = readString("gui.font.size"); if (fontfilename.length() != 0) { // Load file font File fontfile = new File(fontfilename); if (fontfile.exists()) { base = Font.createFont(Font.TRUETYPE_FONT, fontfile); } else { System.err.println("gui.font.file '" + fontfilename + "' doesn't exist."); } } else if (fontname.length() != 0) { // Load system font base = new Font(fontname, Font.PLAIN, 1); } // Set font size if (fontsizestr.length() > 0) { fontsize = Integer.parseInt(fontsizestr); } else { System.err.println("gui.font.size setting is missing."); } font = base.deriveFont(Font.PLAIN, fontsize); // register font as a font family so we can use it in StyledDocument's java.awt.GraphicsEnvironment ge = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment(); ge.registerFont(base); } catch (NumberFormatException e) { System.err.println("gui.font.size setting is invalid."); } catch (Exception e) { e.printStackTrace(); System.out.println(e.getMessage()); } loadedFont = font; return font; } /** * Register a settings listener for a specific option. The settings listener * will be called as soon as the option value changes, e.g. when modifying * the configuration through the GUI, or when loading a game having a patch * file defined. The settings listener is also called immediately by this * method while registering. * * Only one settings listener can be defined for each name/option * combination. This allows to call this method for the same listener * multiple times and have it registered only once. * * @param name the name of the settings listener * @param option the settings option * @param listener the listener to be called when the settings option value * changes */ public void registerSettingsListener(String name, String option, ISettingsListener listener) { removeSettingsListener(name, option); SettingsListenerInfo info = new SettingsListenerInfo(name, option, listener); allListeners.add(info); List<ISettingsListener> listenersForKey = listenersByKey.get(option); if (listenersForKey == null) { listenersForKey = new LinkedList<ISettingsListener>(); listenersByKey.put(option, listenersForKey); } listenersForKey.add(listener); // Trigger the settings listener immediately if a value is defined String value = getProperty(option); if (value != null) { listener.settingsValueChanged(option, value); } } /** * Remove the settings listeners matching the name and option parameters. * * @param name the name of the settings listener, or null to match any name * @param option the settings open, or null to match any settings option */ public void removeSettingsListener(String name, String option) { for (ListIterator<SettingsListenerInfo> lit = allListeners.listIterator(); lit.hasNext();) { SettingsListenerInfo info = lit.next(); if (info.equals(name, option)) { lit.remove(); String key = info.getKey(); List<ISettingsListener> listenersForKey = listenersByKey.get(key); listenersForKey.remove(info.getListener()); if (listenersForKey.isEmpty()) { listenersByKey.remove(key); } } } } /** * Remove all the settings listeners matching the name parameter. * * @param name the name of the settings listener, or null to match any name * (in which case all the settings listeners will be removed). */ public void removeSettingsListener(String name) { removeSettingsListener(name, null); } /** * Trigger the settings listener for a given settings key. This method has * to be called when the value of a settings key changes. * * @param key the key * @param value the settings value */ private void triggerSettingsListener(String key, String value) { List<ISettingsListener> listenersForKey = listenersByKey.get(key); if (listenersForKey != null) { for (ISettingsListener listener : listenersForKey) { listener.settingsValueChanged(key, value); } } } }