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