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