/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion 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. */ package illarion.easynpc.gui; import illarion.common.bug.CrashReporter; import illarion.common.config.ConfigChangedEvent; import illarion.common.config.ConfigDialog; import illarion.common.config.ConfigDialog.Page; import illarion.common.config.ConfigSystem; import illarion.common.config.entries.CheckEntry; import illarion.common.config.entries.DirectoryEntry; import illarion.common.config.entries.NumberEntry; import illarion.common.config.entries.SelectEntry; import illarion.common.util.DirectoryManager; import illarion.common.util.DirectoryManager.Directory; import illarion.easynpc.Lang; import javolution.util.FastTable; import org.bushe.swing.event.annotation.AnnotationProcessor; import org.bushe.swing.event.annotation.EventTopicSubscriber; import org.pushingpixels.substance.api.SubstanceLookAndFeel; import org.pushingpixels.substance.api.skin.SkinInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Map.Entry; /** * This class is used to store and to publish the settings used by the editor GUI. * * @author Martin Karing <nitram@illarion.org> */ public final class Config { /** * The class path of the look and feel that is used by default. */ public static final String DEFAULT_LOOK_AND_FEEL = "org.pushingpixels.substance.api.skin.OfficeSilver2007Skin"; /** * The amount of last opened files that shall be stored. */ private static final int LAST_OPEN_FILES_COUNT = 10; /** * The key in the property file for the auto build flag */ private static final String AUTO_BUILD_KEY = "autoBuild"; /** * The property key value for the easyNPC script folder. */ private static final String EASY_NPC_FOLDER = "easyNpcFolder"; /** * The singleton instance of this class. */ private static final Config INSTANCE = new Config(); /** * The key of the last files list on the configuration file */ private static final String LAST_FILES_KEY = "lastFiles"; /** * The key for the height of the stored window. */ private static final String LAST_WINDOW_H = "lastWindowH"; /** * The key for the extended state of the stored window. */ private static final String LAST_WINDOW_STATE = "lastWindowState"; /** * The key for the width of the stored window. */ private static final String LAST_WINDOW_W = "lastWindowW"; /** * The key for the x coordinate of the stored window. */ private static final String LAST_WINDOW_X = "lastWindowX"; /** * The key for the y coordinate of the stored window. */ private static final String LAST_WINDOW_Y = "lastWindowY"; /** * The logger instance that takes care for the logging output of this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(Config.class); /** * The property key value for the luaNPC script folder. */ private static final String LUA_NPC_FOLDER = "luaNpcFolder"; /** * The property key for the list of files that were open at the last time * the editor was running. */ private static final String OPEN_FILES = "openFiles"; /** * The property key of the value that describes the state of the split pane * in the editor view. */ private static final String SPLIT_STATE = "splitState"; /** * Get the key that is used to store the amount the undo operations. */ private static final String UNDO_COUNT_KEY = "undoCount"; /** * The property key value for the name of the look and feel shall be used. */ private static final String USED_LOOK_AND_FEEL = "usedLookAndFeel"; /** * The property key for the flag that says of the editor shall use syntax * highlighting or not. */ private static final String USE_SYNTAX_HIGHLIGHT = "useSyntaxHighlight"; /** * The property key value for the use window decoration flag. */ private static final String USE_WINDOW_DECO = "useWindowDeco"; /** * The buffered state of the auto build value. This is used because the auto * build state is likely requested really often. */ private boolean autoBuildState; /** * The properties that store the values of this configuration. */ @Nullable private ConfigSystem cfg; /** * The last generated list of opened files. When this is set to * {@code null} the list is generated fresh once its requested the next * time. */ @Nullable private Path[] lastOpenedFilesBuffer; /** * If this is set to true, the application requires to restart to take all * settings done. */ private boolean requireRestart; /** * Private constructor to ensure that no instance but the singleton instance * is created. */ private Config() { AnnotationProcessor.process(this); } /** * Get the singleton instance of this class. * * @return the singleton instance */ @Nonnull public static Config getInstance() { return INSTANCE; } /** * This function determines the user data directory and requests the folder * to store the editor data in case it is needed. It also performs checks to * see if the folder is valid. * * @return a string with the path to the folder or null in case no folder is * set */ @Nonnull private static Path checkFolder() { return DirectoryManager.getInstance().getDirectory(Directory.User); } /** * Prepend this file to the list of last opened files. * * @param file the file to prepend */ public void addLastOpenedFile(@Nonnull Path file) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } cfg.set(LAST_FILES_KEY, file.toAbsolutePath() + File.pathSeparator + cfg.getString(LAST_FILES_KEY)); lastOpenedFilesBuffer = null; } @EventTopicSubscriber(topic = USED_LOOK_AND_FEEL) public void onConfigChanged(@Nonnull String topic, ConfigChangedEvent event) { if (topic.equals(USED_LOOK_AND_FEEL)) { SubstanceLookAndFeel.setSkin(getLookAndFeel()); MainFrame mainFrame = MainFrame.getInstance(); if (mainFrame != null) { int count = mainFrame.getOpenTabs(); for (int i = 0; i < count; i++) { mainFrame.getScriptEditor(i).resetEditorKit(); } SwingUtilities.updateComponentTreeUI(mainFrame); } } } @Nonnull public ConfigDialog createDialog() { if (cfg == null) { throw new IllegalStateException("Configuration system not initialized yet."); } ConfigDialog dialog = new ConfigDialog(); dialog.setConfig(cfg); dialog.setMessageSource(Lang.getInstance()); Page page = new Page("illarion.easynpc.gui.config.generalTab"); page.addEntry(new ConfigDialog.Entry("illarion.easynpc.gui.config.easyNpcFolderLabel", new DirectoryEntry(EASY_NPC_FOLDER, null))); page.addEntry(new ConfigDialog.Entry("illarion.easynpc.gui.config.luaFolderLabel", new DirectoryEntry(LUA_NPC_FOLDER, null))); page.addEntry(new ConfigDialog.Entry("illarion.easynpc.gui.config.maxUndoLabel", new NumberEntry(UNDO_COUNT_KEY, 0, 10000))); page.addEntry(new ConfigDialog.Entry("illarion.easynpc.gui.config.errorReport", new SelectEntry(CrashReporter.CFG_KEY, SelectEntry.STORE_INDEX, Lang.getMsg( "illarion.easynpc.gui.config.errorAsk"), Lang.getMsg( "illarion.easynpc.gui.config.errorAlways"), Lang.getMsg( "illarion.easynpc.gui.config.errorNever")) )); dialog.addPage(page); page = new Page("illarion.easynpc.gui.config.lookAndFeelTab"); page.addEntry(new ConfigDialog.Entry("illarion.easynpc.gui.config.useWindowDecoLabel", new CheckEntry(USE_WINDOW_DECO))); Collection<String> themeObject = new FastTable<>(); Collection<String> themeLabel = new FastTable<>(); for (Entry<String, SkinInfo> skin : SubstanceLookAndFeel.getAllSkins().entrySet()) { themeObject.add(skin.getValue().getClassName()); themeLabel.add(skin.getValue().getDisplayName()); } page.addEntry(new ConfigDialog.Entry("illarion.easynpc.gui.config.usedThemeLabel", new SelectEntry(USED_LOOK_AND_FEEL, SelectEntry.STORE_VALUE, themeObject.toArray(), themeLabel.toArray(new String[themeLabel.size()])) )); page.addEntry(new ConfigDialog.Entry("illarion.easynpc.gui.config.useSyntaxLabel", new CheckEntry(USE_SYNTAX_HIGHLIGHT))); dialog.addPage(page); return dialog; } /** * Check if auto building is enabled or not. * * @return {@code true} in case auto building is enabled */ public boolean getAutoBuild() { return autoBuildState; } /** * Get the folder where to store the easyNPC scripts. * * @return the folder to store the easyNPC scripts */ @Nonnull public String getEasyNpcFolder() { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return Paths.get(System.getProperty("user.home")).toString(); } Path easyNpcFolderFile = cfg.getPath(EASY_NPC_FOLDER); if (easyNpcFolderFile == null) { return Paths.get(System.getProperty("user.home")).toString(); } return easyNpcFolderFile.toString(); } /** * Get the internal used config object. * * @return the internal used config object */ @Nullable public illarion.common.config.Config getInternalCfg() { return cfg; } /** * Get the list of the last opened files. * * @return the list of last opened files */ @Nonnull public Path[] getLastOpenedFiles() { if (lastOpenedFilesBuffer != null) { return lastOpenedFilesBuffer; } if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return new Path[LAST_OPEN_FILES_COUNT]; } String fetchedListString = cfg.getString(LAST_FILES_KEY); if (fetchedListString == null) { return new Path[LAST_OPEN_FILES_COUNT]; } String[] fetchedList = fetchedListString.split(File.pathSeparator); Path[] returnList = new Path[LAST_OPEN_FILES_COUNT]; Path[] cleanList = new Path[LAST_OPEN_FILES_COUNT]; int entryPos = 0; for (int i = 0; (i < fetchedList.length) && (i < LAST_OPEN_FILES_COUNT); i++) { String workString = fetchedList[i]; if (workString.length() < 5) { continue; } Path createdFile = Paths.get(workString); if (Files.isReadable(createdFile)) { Path absolutPath = createdFile.toAbsolutePath(); boolean alreadyInsert = false; for (int j = 0; j < entryPos; j++) { if (cleanList[j].equals(absolutPath)) { alreadyInsert = true; break; } } if (alreadyInsert) { continue; } returnList[entryPos] = createdFile; cleanList[entryPos] = createdFile; entryPos++; } } if (entryPos == 0) { return returnList; } StringBuilder cleanedResult = new StringBuilder(); for (int i = 0; i < entryPos; i++) { cleanedResult.append(cleanList[i]); cleanedResult.append(File.pathSeparator); } cleanedResult.setLength(cleanedResult.length() - 1); cfg.set(LAST_FILES_KEY, cleanedResult.toString()); lastOpenedFilesBuffer = returnList; return returnList; } /** * Read the last window state from the properties and set them to the * windows. * * @param comp The window that shall receive the stored settings */ public void getLastWindowValue(@Nonnull JFrame comp) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); } if ((cfg == null) || (cfg.getInteger(LAST_WINDOW_X) <= 0) || (cfg.getInteger(LAST_WINDOW_Y) <= 0) || (cfg.getInteger(LAST_WINDOW_W) <= 0) || (cfg.getInteger(LAST_WINDOW_H) <= 0) || (cfg.getInteger(LAST_WINDOW_STATE) <= 0)) { Dimension screenSize = comp.getToolkit().getScreenSize(); int width = (screenSize.width * 8) / 10; int height = (screenSize.height * 8) / 10; comp.setBounds(width / 8, height / 8, width, height); return; } try { Rectangle newBounds = new Rectangle(); newBounds.x = cfg.getInteger(LAST_WINDOW_X); newBounds.y = cfg.getInteger(LAST_WINDOW_Y); newBounds.width = cfg.getInteger(LAST_WINDOW_W); newBounds.height = cfg.getInteger(LAST_WINDOW_H); Rectangle testBounds = new Rectangle(new Point(0, 0), comp.getToolkit().getScreenSize()); Rectangle intersectionBounds = newBounds.intersection(testBounds); if (newBounds.equals(intersectionBounds)) { comp.setBounds(newBounds); comp.setExtendedState(cfg.getInteger(LAST_WINDOW_STATE)); return; } } catch (@Nonnull Exception e) { // nothing to do } Dimension screenSize = comp.getToolkit().getScreenSize(); int width = (screenSize.width * 8) / 10; int height = (screenSize.height * 8) / 10; comp.setBounds(width / 8, height / 8, width, height); } /** * Get the look and feel that shall be used. * * @return the class path of the look and feel that shall be used */ @Nonnull public String getLookAndFeel() { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return DEFAULT_LOOK_AND_FEEL; } String lookAndFeel = cfg.getString(USED_LOOK_AND_FEEL); if (lookAndFeel == null) { return DEFAULT_LOOK_AND_FEEL; } return lookAndFeel; } /** * Get the folder where to store the luaNPC scripts. * * @return the folder to store the luaNPC scripts */ public String getLuaNpcFolder() { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return Paths.get(System.getProperty("user.home")).toString(); } Path luaNpcFolderFile = cfg.getPath(LUA_NPC_FOLDER); if (luaNpcFolderFile == null) { return Paths.get(System.getProperty("user.home")).toString(); } return luaNpcFolderFile.toString(); } /** * Get the list of files that were open the last time the editor was * running. * * @return the list of file paths */ @Nonnull public Collection<String> getOldFiles() { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return Collections.emptyList(); } String files = cfg.getString(OPEN_FILES); if (files == null) { return Collections.emptyList(); } return Arrays.asList(files.split(File.pathSeparator)); } /** * Get the state of the split pane in the editor view. * * @return the state of the split pane in the editor view */ public double getSplitPaneState() { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return 0.75; } double value = cfg.getDouble(SPLIT_STATE); if ((value >= 0.1) && (value <= 0.9)) { return value; } return 0.75; } /** * Get the amount of undo operations that are supposed to be stored. * * @return the amount of undo operations */ public int getUndoCount() { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return 200; } return cfg.getInteger(UNDO_COUNT_KEY); } /** * Get the flag if the editor is supposed to highlight the syntax * * @return {@code true} in case the syntax shall be highlighted */ boolean getUseSyntaxHighlighting() { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return true; } return cfg.getBoolean(USE_SYNTAX_HIGHLIGHT); } /** * Get the flag if the editor is supposed to decorate the windows. * * @return {@code true} in case the editor is expected to decorate the * windows */ public boolean getUseWindowDecoration() { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return false; } return cfg.getBoolean(USE_WINDOW_DECO); } /** * Initialize the configuration class and load all configuration values. */ public void init() { Path folder = checkFolder(); Path configFile = folder.resolve("easynpceditor.xcfgz"); cfg = new ConfigSystem(configFile); cfg.setDefault(LAST_FILES_KEY, ""); cfg.setDefault(EASY_NPC_FOLDER, Paths.get(System.getProperty("user.home"))); cfg.setDefault(LAST_FILES_KEY, ""); cfg.setDefault(LAST_WINDOW_H, -1); cfg.setDefault(LAST_WINDOW_STATE, -1); cfg.setDefault(LAST_WINDOW_W, -1); cfg.setDefault(LAST_WINDOW_X, -1); cfg.setDefault(LAST_WINDOW_Y, -1); cfg.setDefault(USED_LOOK_AND_FEEL, DEFAULT_LOOK_AND_FEEL); cfg.setDefault(LUA_NPC_FOLDER, Paths.get(System.getProperty("user.home"))); cfg.setDefault(OPEN_FILES, ""); cfg.setDefault(SPLIT_STATE, 0.75d); cfg.setDefault(UNDO_COUNT_KEY, 100); cfg.setDefault(USE_SYNTAX_HIGHLIGHT, true); cfg.setDefault(USE_WINDOW_DECO, true); cfg.setDefault(AUTO_BUILD_KEY, true); cfg.setDefault(CrashReporter.CFG_KEY, CrashReporter.MODE_ASK); // init values autoBuildState = cfg.getBoolean(AUTO_BUILD_KEY); } /** * Save the configuration file to the filesystem of the local system. */ public void save() { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } cfg.save(); } /** * Set the new value for the auto building flag. * * @param autobuild the new value for the auto building flag */ public void setAutoBuild(boolean autobuild) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } cfg.set(AUTO_BUILD_KEY, autobuild); autoBuildState = autobuild; } /** * Set the folder where to store the easyNPC scripts. * * @param newFolder the folder where to store the easyNPC scripts */ public void setEasyNpcFolder(@Nonnull String newFolder) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } cfg.set(EASY_NPC_FOLDER, Paths.get(newFolder)); } /** * Set the required values of a window into the properties so it can be * restored after the restart. * * @param comp the window that is the source for the stored data */ public void setLastWindowValues(@Nonnull JFrame comp) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } cfg.set(LAST_WINDOW_X, comp.getBounds().x); cfg.set(LAST_WINDOW_Y, comp.getBounds().y); cfg.set(LAST_WINDOW_W, comp.getBounds().width); cfg.set(LAST_WINDOW_H, comp.getBounds().height); cfg.set(LAST_WINDOW_STATE, comp.getExtendedState()); } /** * Set the class path of the look and feel that shall be used from the next * start of the editor on. * * @param lookAndFeel the class path of the look and feel */ public void setLookAndFeel(@Nonnull String lookAndFeel) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } if (getLookAndFeel().equals(lookAndFeel)) { return; } cfg.set(USED_LOOK_AND_FEEL, lookAndFeel); } /** * Set the folder where to store the luaNPC scripts. * * @param newFolder the folder where to store the easyNPC scripts */ public void setLuaNpcFolder(@Nonnull Path newFolder) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } cfg.set(LUA_NPC_FOLDER, newFolder); } /** * Set the list of files that shall be opened the next time the editor is * started. * * @param files the files to open */ public void setOldFiles(@Nonnull Iterable<Path> files) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } StringBuilder buffer = new StringBuilder(); for (Path file : files) { buffer.append(file.toAbsolutePath()); buffer.append(File.pathSeparator); } buffer.setLength(buffer.length() - 1); cfg.set(OPEN_FILES, buffer.toString()); } /** * Save the state of the split pane to the configuration so its restored the * next time its load. * * @param state the state of the split pane */ public void setSplitPaneState(double state) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } cfg.set(SPLIT_STATE, state); } /** * Set the amount of undo operations that should be stored. * * @param count the amount of undo operations that should be stored */ public void setUndoCount(int count) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } if (count >= 0) { cfg.set(UNDO_COUNT_KEY, count); } } /** * Set the flag if the editor is expected to highlight the syntax. * * @param highlight {@code true} in case the editor is expected * highlight the syntax. */ public void setUseSyntaxHighlighting(boolean highlight) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } if (getUseSyntaxHighlighting() == highlight) { return; } cfg.set(USE_SYNTAX_HIGHLIGHT, highlight); requireRestart = true; } /** * Set the flag if the editor is expected to decorate the window or not. * * @param deco {@code true} in case the editor is expected to decorate * the window */ public void setUseWindowDecoration(boolean deco) { if (cfg == null) { LOGGER.error("Configuration system not initialized yet."); return; } if (getUseWindowDecoration() == deco) { return; } cfg.set(USE_WINDOW_DECO, deco); requireRestart = true; } /** * Check if the application needs to restart due changed settings and * display a message in this case. */ public void showRestartWarning() { if (!requireRestart) { return; } SwingUtilities.invokeLater(() -> { requireRestart = false; JOptionPane.showMessageDialog(MainFrame.getInstance(), "Some of the settings changed require a " + "restart to take effect.", "Restart needed", JOptionPane.INFORMATION_MESSAGE); }); } @EventTopicSubscriber(topic = "autoCheckScript") public void onAutoBuildModeChangedEvent(String topic, ActionEvent event) { setAutoBuild(!getAutoBuild()); } }