View Javadoc
1   package org.opentrafficsim.editor;
2   
3   import java.awt.Color;
4   import java.io.File;
5   import java.io.FileReader;
6   import java.io.FileWriter;
7   import java.io.IOException;
8   import java.util.ArrayList;
9   import java.util.Arrays;
10  import java.util.LinkedHashMap;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.Optional;
14  import java.util.Properties;
15  
16  import org.djutils.exceptions.Throw;
17  
18  /**
19   * Stores preferences, recently opened files, etc.
20   * <p>
21   * Copyright (c) 2024-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
22   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
23   * </p>
24   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
25   */
26  public class ApplicationStore
27  {
28  
29      /** Maximum number of recent files. */
30      private static final int MAX_SAVED_FILES = 10;
31  
32      /** Maximum length for tooltips. */
33      private static final int MAX_TOOLTIP_LENGTH = 96;
34  
35      /** Maximum number of items to show in a dropdown menu. */
36      private static final int MAX_DROPDOWN_ITEMS = 20;
37  
38      /** Maximum number of back navigation steps stored. */
39      private static final int MAX_NAVIGATE = 50;
40  
41      /** Color for invalid nodes and values (background). */
42      private static final Color INVALID_COLOR = new Color(255, 240, 240);
43  
44      /** Color for expression nodes and values (background). */
45      private static final Color EXPRESSION_COLOR = new Color(252, 250, 239);
46  
47      /** Store of loaded and set properties. */
48      private final Properties store;
49  
50      /** Application name. */
51      private final String applicationName;
52  
53      /** File to store settings. */
54      private final String file;
55  
56      /** Cached colors. */
57      private final Map<String, Color> colorCache = new LinkedHashMap<>();
58  
59      /** Cached ints. */
60      private final Map<String, Integer> intCache = new LinkedHashMap<>();
61  
62      /**
63       * Constructor. Properties are stored under "{user.home}/{enterprise}/{application}.ini".
64       * @param enterpriseName name of the enterprise.
65       * @param applicationName name of the application.
66       */
67      public ApplicationStore(final String enterpriseName, final String applicationName)
68      {
69          Throw.whenNull(enterpriseName, "Enterprise may not bee null.");
70          this.store = new Properties();
71          this.store.put("expression_color", stringFromColor(EXPRESSION_COLOR));
72          this.store.put("invalid_color", stringFromColor(INVALID_COLOR));
73          this.store.put("max_tooltip_length", Integer.toString(MAX_TOOLTIP_LENGTH));
74          this.store.put("max_dropdown_items", Integer.toString(MAX_DROPDOWN_ITEMS));
75          this.store.put("max_navigate", Integer.toString(MAX_NAVIGATE));
76          this.applicationName = applicationName;
77          this.file =
78                  System.getProperty("user.home") + File.separator + enterpriseName + File.separator + applicationName + ".ini";
79          if (new File(this.file).exists())
80          {
81              try
82              {
83                  FileReader reader = new FileReader(this.file);
84                  this.store.load(reader);
85              }
86              catch (IOException ex)
87              {
88                  //
89              }
90          }
91          save(); // stores defaults as an example to edit the file
92      }
93  
94      /**
95       * Returns the property value.
96       * @param key key.
97       * @return property value, empty if no value is given.
98       */
99      public synchronized Optional<String> getProperty(final String key)
100     {
101         Throw.whenNull(key, "Key may not be null.");
102         return Optional.ofNullable((String) this.store.get(key));
103     }
104 
105     /**
106      * Sets a property value.
107      * @param key key.
108      * @param value value.
109      */
110     public synchronized void setProperty(final String key, final String value)
111     {
112         Throw.whenNull(key, "Key may not be null.");
113         Throw.whenNull(value, "Value may not be null.");
114         this.store.put(key, value);
115         save();
116     }
117 
118     /**
119      * Save the properties.
120      */
121     private void save()
122     {
123         try
124         {
125             File f = new File(this.file);
126             f.getParentFile().mkdirs();
127             FileWriter writer = new FileWriter(f);
128             this.store.store(writer, this.applicationName + " user settings");
129         }
130         catch (IOException exception)
131         {
132             //
133         }
134     }
135 
136     /**
137      * Returns recent files.
138      * @param key key under which files are stored.
139      * @return files (recent to old).
140      */
141     public List<String> getRecentFiles(final String key)
142     {
143         Throw.whenNull(key, "Key may not be null.");
144         List<String> out = new ArrayList<>();
145         if (this.store.containsKey(key))
146         {
147             String filesAll = (String) this.store.get(key);
148             String[] files = filesAll.split("\\|");
149             Arrays.stream(files).forEach(out::add);
150         }
151         return out;
152     }
153 
154     /**
155      * Add recent file. If the file is already in the list, it is moved to the front.
156      * @param key key under which files are stored.
157      * @param fileName latest file.
158      */
159     public void addRecentFile(final String key, final String fileName)
160     {
161         Throw.whenNull(key, "Key may not be null.");
162         Throw.whenNull(fileName, "File may not be null.");
163         List<String> files = getRecentFiles(key);
164         if (files.contains(fileName))
165         {
166             if (files.get(0).equals(fileName))
167             {
168                 return;
169             }
170             files.remove(fileName);
171         }
172         files.add(0, fileName);
173         setFiles(key, files);
174     }
175 
176     /**
177      * Clears a recent file.
178      * @param key key.
179      * @param fileName file.
180      */
181     public void removeRecentFile(final String key, final String fileName)
182     {
183         List<String> files = getRecentFiles(key);
184         files.remove(fileName);
185         setFiles(key, files);
186     }
187 
188     /**
189      * Sets the files.
190      * @param key key.
191      * @param files files.
192      */
193     private void setFiles(final String key, final List<String> files)
194     {
195         StringBuilder str = new StringBuilder();
196         int n = Math.min(files.size(), MAX_SAVED_FILES);
197         if (n > 0)
198         {
199             files.stream().limit(n - 1).forEach((f) -> str.append(f).append("|"));
200             str.append(files.get(n - 1));
201             setProperty(key, str.toString());
202         }
203         else
204         {
205             clearProperty(key);
206         }
207     }
208 
209     /**
210      * Removes key from the store.
211      * @param key key.
212      */
213     public void clearProperty(final String key)
214     {
215         Throw.whenNull(key, "Key may not be null.");
216         this.store.remove(key);
217         save();
218     }
219 
220     /**
221      * Returns color of given key.
222      * @param key key
223      * @return color
224      */
225     public Color getColor(final String key)
226     {
227         return this.colorCache.computeIfAbsent(key, (k) -> colorFromString(this.store.getProperty(k)));
228     }
229 
230     /**
231      * Returns color from string.
232      * @param colorString color as string
233      * @return color
234      */
235     private static Color colorFromString(final String colorString)
236     {
237         String value = colorString.replace(" ", "");
238         String[] channels = value.substring(1, value.length() - 1).split(",");
239         return new Color(Integer.valueOf(channels[0]), Integer.valueOf(channels[1]), Integer.valueOf(channels[2]));
240     }
241 
242     /**
243      * Returns string from color.
244      * @param color color
245      * @return string from color.
246      */
247     private String stringFromColor(final Color color)
248     {
249         return String.format("[%d, %d, %d]", color.getRed(), color.getGreen(), color.getBlue());
250     }
251 
252     /**
253      * Returns int for given key.
254      * @param key key
255      * @return int
256      */
257     public int getInt(final String key)
258     {
259         return this.intCache.computeIfAbsent(key, (k) -> Integer.valueOf(this.store.getProperty(k)));
260     }
261 }