001/*
002 * $Id: JXTipOfTheDay.java 4147 2012-02-01 17:13:24Z kschaefe $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 * 
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 * 
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.jdesktop.swingx;
022
023import java.awt.Component;
024import java.awt.HeadlessException;
025import java.util.prefs.Preferences;
026
027import javax.swing.JDialog;
028
029import org.jdesktop.beans.JavaBean;
030import org.jdesktop.swingx.plaf.LookAndFeelAddons;
031import org.jdesktop.swingx.plaf.TipOfTheDayAddon;
032import org.jdesktop.swingx.plaf.TipOfTheDayUI;
033import org.jdesktop.swingx.tips.DefaultTipOfTheDayModel;
034import org.jdesktop.swingx.tips.TipOfTheDayModel;
035import org.jdesktop.swingx.tips.TipOfTheDayModel.Tip;
036
037/**
038 * Provides the "Tip of The Day" pane and dialog.<br>
039 * 
040 * <p>
041 * Tips are retrieved from the {@link org.jdesktop.swingx.tips.TipOfTheDayModel}.
042 * In the most common usage, a tip (as returned by
043 * {@link org.jdesktop.swingx.tips.TipOfTheDayModel.Tip#getTip()}) is just a
044 * <code>String</code>. However, the return type of this method is actually
045 * <code>Object</code>. Its interpretation depends on its type:
046 * <dl compact>
047 * <dt>Component
048 * <dd>The <code>Component</code> is displayed in the dialog.
049 * <dt>Icon
050 * <dd>The <code>Icon</code> is wrapped in a <code>JLabel</code> and
051 * displayed in the dialog.
052 * <dt>others
053 * <dd>The object is converted to a <code>String</code> by calling its
054 * <code>toString</code> method. The result is wrapped in a
055 * <code>JEditorPane</code> or <code>JTextArea</code> and displayed.
056 * </dl>
057 * 
058 * <p>
059 * <code>JXTipOfTheDay</code> finds its tips in its {@link org.jdesktop.swingx.tips.TipOfTheDayModel}.
060 * Such model can be programmatically built using {@link org.jdesktop.swingx.tips.DefaultTipOfTheDayModel}
061 * and {@link org.jdesktop.swingx.tips.DefaultTip} but
062 * the {@link org.jdesktop.swingx.tips.TipLoader} provides a convenient method to
063 * build a model and its tips from a {@link java.util.Properties} object.
064 *
065 * <p>
066 * Example:
067 * <p>
068 * Let's consider a file <i>tips.properties</i> with the following content:
069 * <pre>
070 * <code>
071 * tip.1.description=This is the first time! Plain text.
072 * tip.2.description=&lt;html&gt;This is &lt;b&gt;another tip&lt;/b&gt;, it uses HTML!
073 * tip.3.description=A third one
074 * </code>
075 * </pre>
076 *
077 * To load and display the tips:
078 * 
079 * <pre>
080 * <code>
081 * Properties tips = new Properties();
082 * tips.load(new FileInputStream("tips.properties"));
083 * 
084 * TipOfTheDayModel model = TipLoader.load(tips);
085 * JXTipOfTheDay totd = new JXTipOfTheDay(model);
086 * 
087 * totd.showDialog(someParentComponent);
088 * </code>
089 * </pre>
090 * 
091 * <p>
092 * Additionally, <code>JXTipOfTheDay</code> features an option enabling the end-user
093 * to choose to not display the "Tip Of The Day" dialog. This user choice can be stored
094 * in the user {@link java.util.prefs.Preferences} but <code>JXTipOfTheDay</code> also
095 * supports custom storage through the {@link org.jdesktop.swingx.JXTipOfTheDay.ShowOnStartupChoice} interface.
096 * 
097 * <pre>
098 * <code>
099 * Preferences userPreferences = Preferences.userRoot().node("myApp");
100 * totd.showDialog(someParentComponent, userPreferences);
101 * </code>
102 * </pre>
103 * In this code, the first time showDialog is called, the dialog will be made 
104 * visible and the user will have the choice to not display it again in the future
105 * (usually this is controlled by a checkbox "Show tips on startup"). If the user
106 * unchecks the option, subsequent calls to showDialog will not display the dialog.
107 * As the choice is saved in the user Preferences, it will persist when the application is relaunched.
108 * 
109 * @see org.jdesktop.swingx.tips.TipLoader
110 * @see org.jdesktop.swingx.tips.TipOfTheDayModel
111 * @see org.jdesktop.swingx.tips.TipOfTheDayModel.Tip
112 * @see #showDialog(Component, Preferences)
113 * @see #showDialog(Component, ShowOnStartupChoice)
114 * 
115 * @author <a href="mailto:fred@L2FProd.com">Frederic Lavigne</a>
116 */
117@JavaBean
118public class JXTipOfTheDay extends JXPanel {
119
120  /**
121   * JXTipOfTheDay pluggable UI key <i>swingx/TipOfTheDayUI</i> 
122   */
123  public final static String uiClassID = "swingx/TipOfTheDayUI";
124
125  // ensure at least the default ui is registered
126  static {
127    LookAndFeelAddons.contribute(new TipOfTheDayAddon());
128  }
129
130  /**
131   * Key used to store the status of the "Show tip on startup" checkbox"
132   */
133  public static final String PREFERENCE_KEY = "ShowTipOnStartup";
134
135  /**
136   * Used when generating PropertyChangeEvents for the "currentTip" property
137   */
138  public static final String CURRENT_TIP_CHANGED_KEY = "currentTip";
139
140  private TipOfTheDayModel model;
141  private int currentTip = 0;
142
143  /**
144   * Constructs a new <code>JXTipOfTheDay</code> with an empty
145   * TipOfTheDayModel
146   */
147  public JXTipOfTheDay() {
148    this(new DefaultTipOfTheDayModel(new Tip[0]));
149  }
150  
151  /**
152   * Constructs a new <code>JXTipOfTheDay</code> showing tips from the given
153   * TipOfTheDayModel.
154   * 
155   * @param model
156   */
157  public JXTipOfTheDay(TipOfTheDayModel model) {
158    this.model = model;
159    updateUI();
160  }
161
162  /**
163   * Notification from the <code>UIManager</code> that the L&F has changed.
164   * Replaces the current UI object with the latest version from the
165   * <code>UIManager</code>.
166   * 
167   * @see javax.swing.JComponent#updateUI
168   */
169  @Override
170public void updateUI() {
171    setUI((TipOfTheDayUI)LookAndFeelAddons.getUI(this, TipOfTheDayUI.class));
172  }
173
174  /**
175   * Sets the L&F object that renders this component.
176   * 
177   * @param ui
178   *          the <code>TipOfTheDayUI</code> L&F object
179   * @see javax.swing.UIDefaults#getUI
180   * 
181   * @beaninfo bound: true hidden: true description: The UI object that
182   *           implements the taskpane group's LookAndFeel.
183   */
184  public void setUI(TipOfTheDayUI ui) {
185    super.setUI(ui);
186  }
187
188  /**
189   * Gets the UI object which implements the L&F for this component.
190   * 
191   * @return the TipOfTheDayUI object that implements the TipOfTheDayUI L&F
192   */
193  @Override
194  public TipOfTheDayUI getUI() {
195    return (TipOfTheDayUI)ui;
196  }
197
198  /**
199   * Returns the name of the L&F class that renders this component.
200   * 
201   * @return the string {@link #uiClassID}
202   * @see javax.swing.JComponent#getUIClassID
203   * @see javax.swing.UIDefaults#getUI
204   */
205  @Override
206  public String getUIClassID() {
207    return uiClassID;
208  }
209
210  public TipOfTheDayModel getModel() {
211    return model;
212  }
213
214  public void setModel(TipOfTheDayModel model) {
215    if (model == null) {
216      throw new IllegalArgumentException("model can not be null");
217    }
218    TipOfTheDayModel old = this.model;
219    this.model = model;
220    firePropertyChange("model", old, model);
221  }
222
223  public int getCurrentTip() {
224    return currentTip;
225  }
226
227  /**
228   * Sets the index of the tip to show
229   * 
230   * @param currentTip
231   * @throws IllegalArgumentException if currentTip is not within the bounds [0,
232   *        getModel().getTipCount()[.
233   */
234  public void setCurrentTip(int currentTip) {
235    if (currentTip < 0 || currentTip >= getModel().getTipCount()) {
236      throw new IllegalArgumentException(
237      "Current tip must be within the bounds [0, " + getModel().getTipCount()
238        + "]");
239    }
240
241    int oldTip = this.currentTip;
242    this.currentTip = currentTip;
243    firePropertyChange(CURRENT_TIP_CHANGED_KEY, oldTip, currentTip);
244  }
245
246  /**
247   * Shows the next tip in the list. It cycles the tip list.
248   */
249  public void nextTip() {
250    int count = getModel().getTipCount();
251    if (count == 0) { return; }
252
253    int nextTip = currentTip + 1;
254    if (nextTip >= count) {
255      nextTip = 0;
256    }
257    setCurrentTip(nextTip);
258  }
259
260  /**
261   * Shows the previous tip in the list. It cycles the tip list.
262   */
263  public void previousTip() {
264    int count = getModel().getTipCount();
265    if (count == 0) { return; }
266
267    int previousTip = currentTip - 1;
268    if (previousTip < 0) {
269      previousTip = count - 1;
270    }
271    setCurrentTip(previousTip);
272  }
273
274  /**
275   * Pops up a "Tip of the day" dialog.
276   * 
277   * @param parentComponent
278   * @exception HeadlessException
279   *              if GraphicsEnvironment.isHeadless() returns true.
280   * @see java.awt.GraphicsEnvironment#isHeadless
281   */
282  public void showDialog(Component parentComponent) throws HeadlessException {
283    showDialog(parentComponent, (ShowOnStartupChoice)null);
284  }
285
286  /**
287   * Pops up a "Tip of the day" dialog. Additionally, it saves the state of the
288   * "Show tips on startup" checkbox in a key named "ShowTipOnStartup" in the
289   * given Preferences.
290   * 
291   * @param parentComponent
292   * @param showOnStartupPref
293   * @exception HeadlessException
294   *              if GraphicsEnvironment.isHeadless() returns true.
295   * @throws IllegalArgumentException
296   *           if showOnStartupPref is null
297   * @see java.awt.GraphicsEnvironment#isHeadless
298   * @return true if the user chooses to see the tips again, false otherwise.
299   */
300  public boolean showDialog(Component parentComponent,
301    Preferences showOnStartupPref) throws HeadlessException {
302    return showDialog(parentComponent, showOnStartupPref, false);
303  }
304  
305  /**
306   * Pops up a "Tip of the day" dialog. Additionally, it saves the state of the
307   * "Show tips on startup" checkbox in a key named "ShowTipOnStartup" in the
308   * given Preferences.
309   * 
310   * @param parentComponent
311   * @param showOnStartupPref
312   * @param force
313   *          if true, the dialog is displayed even if the Preferences is set to
314   *          hide the dialog
315   * @exception HeadlessException
316   *              if GraphicsEnvironment.isHeadless() returns true.
317   * @throws IllegalArgumentException
318   *           if showOnStartupPref is null
319   * @see java.awt.GraphicsEnvironment#isHeadless
320   * @return true if the user chooses to see the tips again, false
321   *         otherwise.
322   */
323  public boolean showDialog(Component parentComponent,
324    final Preferences showOnStartupPref, boolean force) throws HeadlessException {
325    if (showOnStartupPref == null) { throw new IllegalArgumentException(
326      "Preferences can not be null"); }
327
328    ShowOnStartupChoice store = new ShowOnStartupChoice() {
329      @Override
330    public boolean isShowingOnStartup() {
331        return showOnStartupPref.getBoolean(PREFERENCE_KEY, true);
332      }
333      @Override
334    public void setShowingOnStartup(boolean showOnStartup) {
335        if (showOnStartup && !showOnStartupPref.getBoolean(PREFERENCE_KEY, true)) {
336          // if the choice was previously not enable and now we re-enable it, we
337          // must remove the key
338          showOnStartupPref.remove(PREFERENCE_KEY);
339        } else if (!showOnStartup) {
340          // user does not want to see the tip
341          showOnStartupPref.putBoolean(PREFERENCE_KEY, showOnStartup);
342        }
343      }
344    };
345    return showDialog(parentComponent, store, force);
346  }
347
348  /**
349   * Pops up a "Tip of the day" dialog.
350   * 
351   * If <code>choice</code> is not null, the method first checks if
352   * {@link ShowOnStartupChoice#isShowingOnStartup()} is true before showing the
353   * dialog.
354   * 
355   * Additionally, it saves the state of the "Show tips on startup" checkbox
356   * using the given {@link ShowOnStartupChoice} object.
357   * 
358   * @param parentComponent
359   * @param choice
360   * @exception HeadlessException
361   *              if GraphicsEnvironment.isHeadless() returns true.
362   * @see java.awt.GraphicsEnvironment#isHeadless
363   * @return true if the user chooses to see the tips again, false otherwise.
364   */
365  public boolean showDialog(Component parentComponent,
366    ShowOnStartupChoice choice) {
367    return showDialog(parentComponent, choice, false);
368  }
369
370  /**
371   * Pops up a "Tip of the day" dialog.
372   * 
373   * If <code>choice</code> is not null, the method first checks if
374   * <code>force</code> is true or if
375   * {@link ShowOnStartupChoice#isShowingOnStartup()} is true before showing the
376   * dialog.
377   * 
378   * Additionally, it saves the state of the "Show tips on startup" checkbox
379   * using the given {@link ShowOnStartupChoice} object.
380   * 
381   * @param parentComponent
382   * @param choice
383   * @param force
384   *          if true, the dialog is displayed even if
385   *          {@link ShowOnStartupChoice#isShowingOnStartup()} is false
386   * @exception HeadlessException
387   *              if GraphicsEnvironment.isHeadless() returns true.
388   * @see java.awt.GraphicsEnvironment#isHeadless
389   * @return true if the user chooses to see the tips again, false otherwise.
390   */
391  public boolean showDialog(Component parentComponent,
392    ShowOnStartupChoice choice, boolean force) {    
393    if (choice == null) {
394      JDialog dialog = createDialog(parentComponent, choice);
395      dialog.setVisible(true);
396      dialog.dispose();
397      return true;
398    } else if (force || choice.isShowingOnStartup()) {
399      JDialog dialog = createDialog(parentComponent, choice);
400      dialog.setVisible(true);
401      dialog.dispose();
402      return choice.isShowingOnStartup();
403    } else {
404      return false;
405    }
406  }
407  
408  /**
409   * @param showOnStartupPref
410   * @return true if the key named "ShowTipOnStartup" is not set to false
411   */
412  public static boolean isShowingOnStartup(Preferences showOnStartupPref) {
413    return showOnStartupPref.getBoolean(PREFERENCE_KEY, true);
414  }
415
416  /**
417   * Removes the value set for "ShowTipOnStartup" in the given Preferences to
418   * ensure the dialog shown by a later call to
419   * {@link #showDialog(Component, Preferences)} will be visible to the user.
420   * 
421   * @param showOnStartupPref
422   */
423  public static void forceShowOnStartup(Preferences showOnStartupPref) {
424    showOnStartupPref.remove(PREFERENCE_KEY);
425  }
426
427  /**
428   * Calls
429   * {@link TipOfTheDayUI#createDialog(Component, JXTipOfTheDay.ShowOnStartupChoice)}.
430   * 
431   * This method can be overriden in order to control things such as the
432   * placement of the dialog or its title.
433   * 
434   * @param parentComponent
435   * @param choice
436   * @return a JDialog to show this TipOfTheDay pane
437   */
438  protected JDialog createDialog(Component parentComponent,
439    ShowOnStartupChoice choice) {
440    return getUI().createDialog(parentComponent, choice);
441  }
442
443  /**
444   * Used in conjunction with the
445   * {@link JXTipOfTheDay#showDialog(Component, ShowOnStartupChoice)} to save the
446   * "Show tips on startup" choice.
447   */
448  public static interface ShowOnStartupChoice {
449    
450    /**
451     * Persists the user choice
452     * @param showOnStartup the user choice
453     */
454    void setShowingOnStartup(boolean showOnStartup);
455    
456    /**
457     * @return the previously stored user choice
458     */
459    boolean isShowingOnStartup();
460  }
461
462}