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