001/*
002 * $Id: JXFrame.java 4158 2012-02-03 18:29:40Z 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 */
021
022package org.jdesktop.swingx;
023
024import java.awt.AWTEvent;
025import java.awt.Component;
026import java.awt.Cursor;
027import java.awt.GraphicsConfiguration;
028import java.awt.Toolkit;
029import java.awt.event.AWTEventListener;
030import java.awt.event.ActionEvent;
031import java.awt.event.ActionListener;
032import java.awt.event.KeyEvent;
033import java.awt.event.KeyListener;
034
035import javax.swing.JButton;
036import javax.swing.JFrame;
037import javax.swing.JRootPane;
038import javax.swing.JToolBar;
039import javax.swing.Timer;
040
041import org.jdesktop.beans.JavaBean;
042import org.jdesktop.swingx.util.WindowUtils;
043
044/**
045 * <p>
046 * {@code JXFrame} is an enhanced {@link JFrame}. While {@code JXFrame} can
047 * replace any {@code JFrame}, it has features that make it particularly useful
048 * as the "main" frame for an application.
049 * </p>
050 * <h3>Additional Features</h3>
051 * <p>
052 * Root pane: {@code JXFrame} uses {@link JXRootPane} as its default root pane.
053 * The frame provide several convenience methods to provide easy access to the
054 * additional features.
055 * </p>
056 * <p>
057 * Idle: {@code JXFrame} offers an idle timer. Registering a
058 * {@link java.beans.PropertyChangeListener} for "idle" will notify when the
059 * user has not interacted with the JVM. A primary use for this type of
060 * functionality is to secure the application, blocking access and requiring the
061 * user to login again.
062 * </p>
063 * <p>
064 * Wait (busy) glass pane: The {@code JXFrame} can be configured with an
065 * alternate glass pane. Typically, this glass pane is used to notify the user
066 * that the application is busy, but the glass pane could be for any purpose.
067 * This secondary glass pane can be quickly enabled or disabled by
068 * {@linkplain #setWaitPaneVisible(boolean) setting the wait pane visible}.
069 * </p>
070 * 
071 * @author unascribed from JDNC
072 */
073@JavaBean
074@SuppressWarnings({ "nls", "serial" })
075public class JXFrame extends JFrame {
076    /**
077     * An enumeration of {@link JXFrame} starting locations.
078     *
079     * @author unascribed from JDNC
080     */
081    public enum StartPosition {CenterInScreen, CenterInParent, Manual}
082    
083    private Component waitPane = null;
084    private Component glassPane = null;
085    private boolean waitPaneVisible = false;
086    private Cursor realCursor = null;
087    private boolean waitCursorVisible = false;
088    private boolean waiting = false;
089    private StartPosition startPosition;
090    private boolean hasBeenVisible = false; //startPosition is only used the first time the window is shown
091    private AWTEventListener keyEventListener; //for listening to KeyPreview events
092    private boolean keyPreview = false;
093    private AWTEventListener idleListener; //for listening to events. If no events happen for a specific amount of time, mark as idle
094    private Timer idleTimer;
095    private long idleThreshold = 0;
096    private boolean idle;
097    
098    /**
099     * Creates a {@code JXFrame} with no title and standard closing behavior.
100     */
101    public JXFrame() {
102        this(null, false);
103    }
104
105    /**
106     * Creates a {@code JXFrame} with the specified title and default closing
107     * behavior.
108     * 
109     * @param title
110     *            the frame title
111     */
112    public JXFrame(String title) {
113        this(title, false);
114    }
115
116    /**
117     * Creates a <code>JXFrame</code> in the specified
118     * <code>GraphicsConfiguration</code> of
119     * a screen device, a blank title and default closing behaviour.
120     * <p>
121     *
122     * @param gc the <code>GraphicsConfiguration</code> that is used
123     *          to construct the new <code>Frame</code>;
124     *          if <code>gc</code> is <code>null</code>, the system
125     *          default <code>GraphicsConfiguration</code> is assumed
126     * @exception IllegalArgumentException if <code>gc</code> is not from
127     *          a screen device.  This exception is always thrown when
128     *      GraphicsEnvironment.isHeadless() returns true.
129     */
130    public JXFrame(GraphicsConfiguration gc) {
131        this(null, gc, false);
132    }
133
134    
135    /**
136     * Creates a <code>JXFrame</code> with the specified title, the
137     * specified <code>GraphicsConfiguration</code> of a screen device and
138     * default closing behaviour.
139     * <p>
140     *
141     * @param title the title to be displayed in the
142     *          frame's border. A <code>null</code> value is treated as
143     *          an empty string, "".
144     * @param gc the <code>GraphicsConfiguration</code> that is used
145     *          to construct the new <code>JFrame</code> with;
146     *          if <code>gc</code> is <code>null</code>, the system
147     *          default <code>GraphicsConfiguration</code> is assumed
148     * @exception IllegalArgumentException if <code>gc</code> is not from
149     *          a screen device.  This exception is always thrown when
150     *      GraphicsEnvironment.isHeadless() returns true.
151     */
152    public JXFrame(String title, GraphicsConfiguration gc) {
153        this(title, gc, false);
154     }
155
156    /**
157     * Creates a {@code JXFrame} with the specified title and closing behavior.
158     * 
159     * @param title
160     *            the frame title
161     * @param exitOnClose
162     *            {@code true} to override the default ({@link JFrame}) closing
163     *            behavior and use {@link JFrame#EXIT_ON_CLOSE EXIT_ON_CLOSE}
164     *            instead; {@code false} to use the default behavior
165     */
166    public JXFrame(String title, boolean exitOnClose) {
167        this(title, null, exitOnClose);
168    }
169
170    /**
171     * Creates a {@code JXFrame} with the specified title, GraphicsConfiguration
172     * and closing behavior.
173     * 
174     * @param title the frame title
175     * @param gc the <code>GraphicsConfiguration</code> of the target screen
176     *        device. If <code>gc</code> is <code>null</code>, the system
177     *        default <code>GraphicsConfiguration</code> is assumed.
178     * @param exitOnClose {@code true} to override the default ({@link JFrame})
179     *        closing behavior and use {@link JFrame#EXIT_ON_CLOSE
180     *        EXIT_ON_CLOSE} instead; {@code false} to use the default behavior
181     * @exception IllegalArgumentException if <code>gc</code> is not from a
182     *            screen device.
183     * 
184     */
185   public JXFrame(String title, GraphicsConfiguration gc, boolean exitOnClose) {
186        super(title, gc);
187        if (exitOnClose) {
188            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
189        }
190        
191        //create the event handler for key preview functionality
192        keyEventListener = new AWTEventListener() {
193            @Override
194            public void eventDispatched(AWTEvent aWTEvent) {
195                if (aWTEvent instanceof KeyEvent) {
196                    KeyEvent evt = (KeyEvent)aWTEvent;
197                    for (KeyListener kl : getKeyListeners()) {
198                        int id = aWTEvent.getID();
199                        switch (id) {
200                            case KeyEvent.KEY_PRESSED:
201                                kl.keyPressed(evt);
202                                break;
203                            case KeyEvent.KEY_RELEASED:
204                                kl.keyReleased(evt);
205                                break;
206                            case KeyEvent.KEY_TYPED:
207                                kl.keyTyped(evt);
208                                break;
209                            default:
210                                System.err.println("Unhandled Key ID: " + id);    
211                        }
212                    }
213                }
214            }
215        };
216        
217        idleTimer = new Timer(100, new ActionListener() {
218            @Override
219            public void actionPerformed(ActionEvent actionEvent) {
220                setIdle(true);
221            }
222        });
223        
224        //create the event handler for key preview functionality
225        idleListener = new AWTEventListener() {
226            @Override
227            public void eventDispatched(AWTEvent aWTEvent) {
228                //reset the timer
229                idleTimer.stop();
230                //if the user is idle, then change to not idle
231                if (isIdle()) {
232                    setIdle(false);
233                }
234                //start the timer
235                idleTimer.restart();
236            }
237        };
238    }
239
240    /**
241     * Sets the cancel button property on the underlying {@code JXRootPane}.
242     * 
243     * @param button
244     *            the {@code JButton} which is to be the cancel button
245     * @see #getCancelButton()
246     * @see JXRootPane#setCancelButton(JButton)
247     */
248    public void setCancelButton(JButton button) {
249        getRootPaneExt().setCancelButton(button);
250    }
251
252    /**
253     * Returns the value of the cancel button property from the underlying
254     * {@code JXRootPane}.
255     * 
256     * @return the {@code JButton} which is the cancel button
257     * @see #setCancelButton(JButton)
258     * @see JXRootPane#getCancelButton()
259     */
260    public JButton getCancelButton() {
261        return getRootPaneExt().getCancelButton();
262    }
263    
264    /**
265     * Sets the default button property on the underlying {@code JRootPane}.
266     * 
267     * @param button
268     *            the {@code JButton} which is to be the default button
269     * @see #getDefaultButton()
270     * @see JXRootPane#setDefaultButton(JButton)
271     */
272    public void setDefaultButton(JButton button) {
273        JButton old = getDefaultButton();
274        getRootPane().setDefaultButton(button);
275        firePropertyChange("defaultButton", old, getDefaultButton());
276    }
277    
278    /**
279     * Returns the value of the default button property from the underlying
280     * {@code JRootPane}.
281     * 
282     * @return the {@code JButton} which is the default button
283     * @see #setDefaultButton(JButton)
284     * @see JXRootPane#getDefaultButton()
285     */
286    public JButton getDefaultButton() {
287        return getRootPane().getDefaultButton();
288    }
289
290    /**
291     * If enabled the {@code KeyListener}s will receive a preview of the {@code
292     * KeyEvent} prior to normal viewing.
293     * 
294     * @param flag {@code true} to enable previewing; {@code false} otherwise
295     * @see #getKeyPreview()
296     * @see #addKeyListener(KeyListener)
297     */
298    public void setKeyPreview(boolean flag) {
299        Toolkit.getDefaultToolkit().removeAWTEventListener(keyEventListener);
300        if (flag) {
301            Toolkit.getDefaultToolkit().addAWTEventListener(keyEventListener, AWTEvent.KEY_EVENT_MASK);
302        }
303        boolean old = keyPreview;
304        keyPreview = flag;
305        firePropertyChange("keyPreview", old, keyPreview);
306    }
307
308    /**
309     * Returns the value for the key preview.
310     * 
311     * @return if {@code true} previewing is enabled; otherwise it is not
312     * @see #setKeyPreview(boolean)
313     */
314    public final boolean getKeyPreview() {
315        return keyPreview;
316    }
317
318    /**
319     * Sets the start position for this frame. Setting this value only has an
320     * effect is the frame has never been displayed.
321     * 
322     * @param position
323     *            the position to display the frame at
324     * @see #getStartPosition()
325     * @see #setVisible(boolean)
326     */
327    public void setStartPosition(StartPosition position) {
328        StartPosition old = getStartPosition();
329        this.startPosition = position;
330        firePropertyChange("startPosition", old, getStartPosition());
331    }
332
333    /**
334     * Returns the start position for this frame.
335     * 
336     * @return the start position of the frame
337     * @see #setStartPosition(StartPosition)
338     */
339    public StartPosition getStartPosition() {
340        return startPosition == null ? StartPosition.Manual : startPosition;
341    }
342
343    /**
344     * Switches the display cursor to or from the wait cursor.
345     * 
346     * @param flag
347     *            {@code true} to enable the wait cursor; {@code false} to
348     *            enable the previous cursor
349     * @see #isWaitCursorVisible()
350     * @see Cursor#WAIT_CURSOR
351     */
352    public void setWaitCursorVisible(boolean flag) {
353        boolean old = isWaitCursorVisible();
354        if (flag != old) {
355            waitCursorVisible = flag;
356            if (isWaitCursorVisible()) {
357                realCursor = getCursor();
358                super.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
359            } else {
360                super.setCursor(realCursor);
361            }
362            firePropertyChange("waitCursorVisible", old, isWaitCursorVisible());
363        }
364    }
365
366    /**
367     * Returns the state of the wait cursor visibility.
368     * 
369     * @return {@code true} if the current cursor is the wait cursor; {@code
370     *         false} otherwise
371     */
372    public boolean isWaitCursorVisible() {
373        return waitCursorVisible;
374    }
375    
376    /**
377     * {@inheritDoc}
378     */
379    @Override
380    public void setCursor(Cursor c) {
381        if (!isWaitCursorVisible()) {
382            super.setCursor(c);
383        } else {
384            this.realCursor = c;
385        }
386    }
387
388    /**
389     * Sets the component to use as a wait glass pane. This component is not
390     * part of the display hierarchy unless {@code isWaitPaneVisible() == true}.
391     * 
392     * @param c
393     *            the wait glass pane for this frame
394     * @see #getWaitPane()
395     * @see #setWaitPaneVisible(boolean)
396     */
397    public void setWaitPane(Component c) {
398        Component old = getWaitPane();
399        this.waitPane = c;
400        firePropertyChange("waitPane", old, getWaitPane());
401    }
402
403    /**
404     * Returns the current wait pane for this frame. This component may or may
405     * not be part of the display hierarchy.
406     * 
407     * @return the current wait pane
408     * @see #setWaitPane(Component)
409     */
410    public Component getWaitPane() {
411        return waitPane;
412    }
413
414    /**
415     * Enabled or disabled the display of the normal or wait glass pane. If
416     * {@code true} the wait pane is be displayed. Altering this property alters
417     * the display hierarchy.
418     * 
419     * @param flag
420     *            {@code true} to display the wait glass pane; {@code false} to
421     *            display the normal glass pane
422     * @see #isWaitPaneVisible()
423     * @see #setWaitPane(Component)
424     */
425    public void setWaitPaneVisible(boolean flag) {
426        boolean old = isWaitPaneVisible();
427        if (flag != old) {
428            this.waitPaneVisible = flag;
429            Component wp = getWaitPane();
430            if (isWaitPaneVisible()) {
431                glassPane = getRootPane().getGlassPane();
432                if (wp != null) {
433                    getRootPane().setGlassPane(wp);
434                    wp.setVisible(true);
435                }
436            } else {
437                if (wp != null) {
438                    wp.setVisible(false);
439                }
440                getRootPane().setGlassPane(glassPane);
441            }
442            firePropertyChange("waitPaneVisible", old, isWaitPaneVisible());
443        }
444    }
445
446    /**
447     * Returns the current visibility of the wait glass pane.
448     * 
449     * @return {@code true} if the wait glass pane is visible; {@code false}
450     *         otherwise
451     */
452    public boolean isWaitPaneVisible() {
453        return waitPaneVisible;
454    }
455
456    /**
457     * Sets the frame into a wait state or restores the frame from a wait state.
458     * 
459     * @param waiting
460     *            {@code true} to place the frame in a wait state; {@code false}
461     *            otherwise
462     * @see #isWaiting()
463     * @see #setWaitCursorVisible(boolean)
464     * @see #setWaitPaneVisible(boolean)
465     */
466    public void setWaiting(boolean waiting) {
467        boolean old = isWaiting();
468        this.waiting = waiting;
469        firePropertyChange("waiting", old, isWaiting());
470        setWaitPaneVisible(waiting);
471        setWaitCursorVisible(waiting);
472    }
473
474    /**
475     * Determines if the frame is in a wait state or not.
476     * 
477     * @return {@code true} if the frame is in the wait state; {@code false}
478     *         otherwise
479     * @see #setWaiting(boolean)
480     */
481    public boolean isWaiting() {
482        return waiting;
483    }
484    
485    /**
486     * {@inheritDoc}
487     */
488    @Override
489    public void setVisible(boolean visible) {
490        if (!hasBeenVisible && visible) {
491            //move to the proper start position
492            StartPosition pos = getStartPosition();
493            switch (pos) {
494                case CenterInParent:
495                    setLocationRelativeTo(getParent());
496                    break;
497                case CenterInScreen:
498                    setLocation(WindowUtils.getPointForCentering(this));
499                    break;
500                case Manual:
501                default:
502                    //nothing to do!
503            }
504        }
505        super.setVisible(visible);
506    }
507    
508    public boolean isIdle() {
509        return idle;
510    }
511    
512    /**
513     * Sets the frame into an idle state or restores the frame from an idle state.
514     * 
515     * @param idle
516     *            {@code true} to place the frame in an idle state; {@code false}
517     *            otherwise
518     * @see #isIdle()
519     * @see #setIdleThreshold(long)
520     */
521    public void setIdle(boolean idle) {
522        boolean old = isIdle();
523        this.idle = idle;
524        firePropertyChange("idle", old, isIdle());
525    }
526
527    /**
528     * Sets a threshold for user interaction before automatically placing the
529     * frame in an idle state.
530     * 
531     * @param threshold
532     *            the time (in milliseconds) to elapse before setting the frame
533     *            idle
534     * @see #getIdleThreshold()
535     * @see #setIdle(boolean)
536     */
537    public void setIdleThreshold(long threshold) {
538        long old = getIdleThreshold();
539        this.idleThreshold = threshold;
540        firePropertyChange("idleThreshold", old, getIdleThreshold());
541        
542        threshold = getIdleThreshold(); // in case the getIdleThreshold method has been overridden
543        
544        Toolkit.getDefaultToolkit().removeAWTEventListener(idleListener);
545        if (threshold > 0) {
546            Toolkit.getDefaultToolkit().addAWTEventListener(idleListener, AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.MOUSE_WHEEL_EVENT_MASK);
547        }
548        idleTimer.stop();
549        idleTimer.setInitialDelay((int)threshold);
550        idleTimer.restart();
551    }
552
553    /**
554     * Returns the amount of time that must elapse before the frame
555     * automatically enters an idle state.
556     * 
557     * @return the time in milliseconds
558     */
559    public long getIdleThreshold() {
560        return idleThreshold;
561    }
562    
563    /**
564     * Sets the status bar property on the underlying {@code JXRootPane}.
565     * 
566     * @param statusBar
567     *            the {@code JXStatusBar} which is to be the status bar
568     * @see #getStatusBar()
569     * @see JXRootPane#setStatusBar(JXStatusBar)
570     */
571    public void setStatusBar(JXStatusBar statusBar) {
572        getRootPaneExt().setStatusBar(statusBar);
573    }
574    
575    /**
576     * Returns the value of the status bar property from the underlying
577     * {@code JXRootPane}.
578     * 
579     * @return the {@code JXStatusBar} which is the current status bar
580     * @see #setStatusBar(JXStatusBar)
581     * @see JXRootPane#getStatusBar()
582     */
583    public JXStatusBar getStatusBar() {
584        return getRootPaneExt().getStatusBar();
585    }
586    
587    /**
588     * Sets the tool bar property on the underlying {@code JXRootPane}.
589     * 
590     * @param toolBar
591     *            the {@code JToolBar} which is to be the tool bar
592     * @see #getToolBar()
593     * @see JXRootPane#setToolBar(JToolBar)
594     */
595    public void setToolBar(JToolBar toolBar) {
596        getRootPaneExt().setToolBar(toolBar);
597    }
598    
599    /**
600     * Returns the value of the tool bar property from the underlying
601     * {@code JXRootPane}.
602     * 
603     * @return the {@code JToolBar} which is the current tool bar
604     * @see #setToolBar(JToolBar)
605     * @see JXRootPane#getToolBar()
606     */
607    public JToolBar getToolBar() {
608        return getRootPaneExt().getToolBar();
609    }
610    
611    //---------------------------------------------------- Root Pane Methods
612    /**
613     * Overridden to create a JXRootPane.
614     */
615    @Override
616    protected JRootPane createRootPane() {
617        return new JXRootPane();
618    }
619
620    /**
621     * Overridden to make this public.
622     */
623    @Override
624    public void setRootPane(JRootPane root) {
625        super.setRootPane(root);
626    }
627
628    /**
629     * Return the extended root pane. If this frame doesn't contain
630     * an extended root pane the root pane should be accessed with
631     * getRootPane().
632     *
633     * @return the extended root pane or null.
634     */
635    public JXRootPane getRootPaneExt() {
636        if (rootPane instanceof JXRootPane) {
637            return (JXRootPane)rootPane;
638        }
639        return null;
640    }
641}
642