001 /* JAPI - (Yet another (hopefully) useful) Java API
002 *
003 * Copyright (C) 2004-2006 Christian Hujer
004 *
005 * This program is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU General Public License as
007 * published by the Free Software Foundation; either version 2 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but
011 * WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013 * General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program; if not, write to the Free Software
017 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
018 * 02111-1307, USA.
019 */
020
021 package net.sf.japi.swing;
022
023 import java.awt.Component;
024 import java.awt.Dimension;
025 import java.awt.Font;
026 import java.util.ArrayList;
027 import java.util.List;
028 import java.util.MissingResourceException;
029 import java.util.Random;
030 import java.util.ResourceBundle;
031 import static java.util.ResourceBundle.getBundle;
032 import java.util.prefs.Preferences;
033 import static java.util.prefs.Preferences.userNodeForPackage;
034 import java.io.InputStream;
035 import java.io.BufferedReader;
036 import java.io.InputStreamReader;
037 import java.io.UnsupportedEncodingException;
038 import java.security.PrivilegedAction;
039 import java.security.AccessController;
040 import javax.swing.Action;
041 import static javax.swing.Action.ACCELERATOR_KEY;
042 import javax.swing.JButton;
043 import javax.swing.JCheckBox;
044 import javax.swing.JDialog;
045 import javax.swing.JEditorPane;
046 import javax.swing.JLabel;
047 import javax.swing.JOptionPane;
048 import javax.swing.JScrollPane;
049 import javax.swing.KeyStroke;
050 import static javax.swing.SwingConstants.TRAILING;
051 import org.jetbrains.annotations.Nullable;
052 import static net.sf.japi.swing.ActionFactory.ACCELERATOR_KEY_2;
053 import static net.sf.japi.swing.IconManager.getDefaultIconManager;
054
055 /** Class that manages tips of the day.
056 * The tips of the day are read from a property file.
057 * The name of the property file is tried to retrieve in exactly the following order:
058 * <ol>
059 * <li>The System Property <code>net.sf.japi.swing.tod</code> is queried and taken as a ResourceBundle base name.</li>
060 * <li>The Service file <code>META-INF/services/net.sf.japi.swing.tod</code> is read and its first line taken as a ResourceBundle base name.</li>
061 * </ol>
062 * If both fails, the behaviour is undefined.
063 * <p/>
064 * Setting the ResourceBundle for the TipOfTheDayManager via services is the preferred way, because you do not need any additional coding apart from
065 * invoking the TipOfTheDayManager somewhere at startup.
066 * <p />
067 * The format of that property file follows the normal Java Properties convention, with the property keys being numbered, starting at "tod.text.1".
068 * Example:
069 * <pre># Tip Of The Days, English Version
070 * tod.text.1=<html>For analysis with other tools you can export the symbol map to XML, MS-Excel and CSV.
071 * tod.text.2=<html>For analysis with other tools you can export the mapping map to XML, MS-Excel and CSV.
072 * tod.text.3=<html>The supported map file formats are: Intel, GCC and MSVC.
073 * </pre>
074 * @fixme The preferences properties lastTipOfTheDayNumber and showTipOfTheDayAtStartup are stored in the wrong package, they must be stored in the client package instead of this package
075 * @todo Allow parametrization of properties, e.g. via a String sequence like <code>${property.name}</code>, which should then be looked up using
076 * a defined scheme from one or perhaps more definable {@link ActionFactory ActionFactories}.
077 * @author <a href="mailto:chris@riedquat.de">Christian Hujer</a>
078 * @serial exclude
079 */
080 public final class TipOfTheDayManager extends JOptionPane {
081
082 /** Action Factory. */
083 private static final ActionFactory ACTION_FACTORY = ActionFactory.getFactory("net.sf.japi.swing");
084
085 /** Random number generator for random tods. */
086 private static final Random RND = new Random();
087
088 /** Preferences. */
089 private static final Preferences PREFS = userNodeForPackage(TipOfTheDayManager.class);
090
091 /** The Action keys used for accelerators. */
092 private static final String[] ACCELERATOR_KEYS = new String[] { ACCELERATOR_KEY, ACCELERATOR_KEY_2 };
093
094 /** The static instance. */
095 private static final TipOfTheDayManager INSTANCE = new TipOfTheDayManager();
096
097 /** JButton for close. */
098 private JButton closeButton;
099
100 /** JDialog. */
101 private JDialog dialog;
102
103 /** JCheckBox for showing at startup. */
104 private JCheckBox showAtStartup = new JCheckBox(ACTION_FACTORY.createAction(false, "todShowAtStartup", null));
105
106 /** The JLabel showing the current number of the tod. */
107 private JLabel currentTodIndex = new JLabel();
108
109 /** The JEditorPane displaying the tod text. */
110 private JEditorPane todText = new JEditorPane("text/html", "");
111
112 /** List with tod texts. */
113 private List<String> todTexts = new ArrayList<String>();
114
115 /** Number of current Tip of the day.
116 * In visible state, the index must range between 0 and the number of tods available - 1, inclusive.
117 */
118 private int todIndex;
119
120 /** Show Tip Of The Day at startup.
121 * This method is only a proxy for show(Component) but looks at user preferences.
122 * If the user chose not to see TipOfTheDays this method simply returns.
123 * @param parentComponent the parent component of this dialog.
124 */
125 public static void showAtStartup(final Component parentComponent) {
126 if (PREFS.getBoolean("showTipOfTheDayAtStartup", true)) {
127 show(parentComponent);
128 }
129 }
130
131 /** Show a Tip Of The Day.
132 * @param parentComponent the parent component of this dialog.
133 */
134 public static void show(final Component parentComponent) {
135 if (INSTANCE.dialog == null) {
136 INSTANCE.dialog = INSTANCE.createDialog(parentComponent, ACTION_FACTORY.getString("tipOfTheDay.windowTitle"));
137 }
138 INSTANCE.dialog.getRootPane().setDefaultButton(INSTANCE.closeButton);
139 INSTANCE.dialog.setModal(false);
140 INSTANCE.dialog.setResizable(true);
141 INSTANCE.dialog.setVisible(true);
142 INSTANCE.closeButton.requestFocusInWindow();
143 }
144
145 /** Finds the bundle name.
146 * @return bundle name
147 */
148 private static String getBundleName() {
149 String bundleName = System.getProperty("net.sf.japi.swing.tod");
150 if (bundleName == null) {
151 bundleName = getServiceValue(getClassLoader());
152 }
153 return bundleName;
154 }
155
156 /** This method attempts to return the first line of the resource META_INF/services/net.sf.japi.swing.tod from the provided ClassLoader.
157 * @param classLoader ClassLoader, may not be <code>null</code>.
158 * @return first line of resource, or <code>null</code>
159 */
160 @Nullable private static String getServiceValue(final ClassLoader classLoader) {
161 BufferedReader rd = null;
162 try {
163 final String serviceId = "META-INF/services/net.sf.japi.swing.tod";
164 final InputStream in = getResourceAsStream(classLoader, serviceId);
165 if (in != null) {
166 try {
167 rd = new BufferedReader(new InputStreamReader(in, "UTF-8"));
168 } catch (final UnsupportedEncodingException e) {
169 rd = new BufferedReader(new InputStreamReader(in));
170 }
171 return rd.readLine();
172 }
173 } catch (final Exception e) {
174 /* ignore. */
175 } finally {
176 try { rd.close(); } catch (final Exception e) { /* ignore */ } finally { rd = null; }
177 }
178 return null;
179 }
180
181 /** Get a resource from a classloader as stream.
182 * @param classLoader ClassLoader to get resource from
183 * @param serviceId service file to get stream from
184 * @return stream for resource <var>serviceId</var> in <var>classLoader</var>
185 */
186 private static InputStream getResourceAsStream(final ClassLoader classLoader, final String serviceId) {
187 return AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
188
189 /** {@inheritDoc} */
190 public InputStream run() {
191 return classLoader == null ?
192 ClassLoader.getSystemResourceAsStream(serviceId) :
193 classLoader.getResourceAsStream(serviceId);
194 }
195
196 });
197 }
198
199 /** Get the ClassLoader.
200 * @return ClassLoader
201 */
202 private static ClassLoader getClassLoader() {
203 try {
204 final ClassLoader contextClassLoader = getContextClassLoader();
205 if (contextClassLoader != null) {
206 return contextClassLoader;
207 }
208 } catch (final Exception e) {
209 /* ignore */
210 }
211 return TipOfTheDayManager.class.getClassLoader();
212 }
213
214 /** Get the context ClassLoader.
215 * @return context ClassLoader
216 */
217 private static ClassLoader getContextClassLoader() {
218 return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
219
220 /** {@inheritDoc} */
221 @Nullable public ClassLoader run() {
222 try {
223 return Thread.currentThread().getContextClassLoader();
224 } catch (final SecurityException ex) {
225 return null;
226 }
227 }
228
229 });
230 }
231
232 /** Constructor. */
233 private TipOfTheDayManager() {
234 final String[] keys = { "todPrev", "todRand", "todNext", "todClose" };
235 final Action[] actions = new Action[keys.length];
236 final Object[] newOptions = new Object[keys.length];
237 for (int i = 0; i < keys.length; i++) {
238 actions[i] = ACTION_FACTORY.createAction(false, keys[i], this);
239 newOptions[i] = new JButton(actions[i]);
240 if ("todClose".equals(keys[i])) {
241 closeButton = (JButton) newOptions[i];
242 }
243 for (final String key : ACCELERATOR_KEYS) {
244 final KeyStroke ks = (KeyStroke) actions[i].getValue(key);
245 if (ks != null) {
246 getInputMap(WHEN_IN_FOCUSED_WINDOW).put(ks, keys[i]);
247 getActionMap().put(keys[i], actions[i]);
248 }
249 }
250 }
251 final Dimension size = new Dimension(512, 128);
252 showAtStartup.setSelected(PREFS.getBoolean("showTipOfTheDayAtStartup", true));
253 final JScrollPane todTextScroller = new JScrollPane(todText);
254 todTextScroller.setMinimumSize(size);
255 todTextScroller.setPreferredSize(size);
256 todTextScroller.setFocusable(true);
257 todTextScroller.setAutoscrolls(true);
258 todText.setEditable(false);
259 todText.setRequestFocusEnabled(false);
260 currentTodIndex.setHorizontalAlignment(TRAILING);
261 loadTodTexts();
262 setTodIndex(PREFS.getInt("lastTipOfTheDayNumber", -1) + 1);
263 setIcon(getDefaultIconManager().getIcon(ACTION_FACTORY.getString("tipOfTheDay.icon")));
264 final JLabel heading = new JLabel(ACTION_FACTORY.getString("todHeading"));
265 final Font oldFont = heading.getFont();
266 heading.setFont(oldFont.deriveFont((float) (oldFont.getSize2D() * 1.5)));
267 setMessage(new Object[] { heading, todTextScroller, currentTodIndex, showAtStartup });
268 setMessageType(INFORMATION_MESSAGE);
269 setOptions(newOptions);
270 }
271
272 /** Loads the Tip of the day texts. */
273 private void loadTodTexts() {
274 final ResourceBundle todBundle = getBundle(getBundleName());
275 for (int i = 1;; i++) {
276 try {
277 todTexts.add(todBundle.getString(new StringBuilder().append("tod.text.").append(i).toString()));
278 } catch (final MissingResourceException e) {
279 break;
280 }
281 }
282 }
283
284 /** Sets the tod index.
285 * @param todIndex new todIndex
286 */
287 private void setTodIndex(final int todIndex) {
288 try {
289 this.todIndex = (todIndex + todTexts.size()) % todTexts.size();
290 todText.setText(todTexts.get(this.todIndex));
291 currentTodIndex.setText(ACTION_FACTORY.format("todIndex", this.todIndex + 1, todTexts.size()));
292 } catch (final ArithmeticException e) {
293 todText.setText(ACTION_FACTORY.getString("todsUnavailable"));
294 currentTodIndex.setText("");
295 }
296 }
297
298 /** Returns number of current Tip of the day.
299 * In visible state, the index must range between 0 and the number of tods available - 1, inclusive.
300 * @return number of current Tip of the day.
301 */
302 public int getTodIndex() {
303 return todIndex;
304 }
305
306 /** Action method for close. */
307 public void todClose() {
308 setValue(closeButton);
309 PREFS.putBoolean("showTipOfTheDayAtStartup", showAtStartup.isSelected());
310 PREFS.putInt("lastTipOfTheDayNumber", todIndex);
311 dialog.setVisible(false);
312 dialog.dispose();
313 dialog = null;
314 }
315
316 /** Action method for next. */
317 public void todNext() {
318 setTodIndex(todIndex + 1);
319 }
320
321 /** Action method for previous. */
322 public void todPrev() {
323 setTodIndex(todIndex - 1);
324 }
325
326 /** Action method for random. */
327 public void todRand() {
328 setTodIndex(RND.nextInt(todTexts.size()));
329 }
330
331 } // class TipOfTheDayManager