001    /*
002     * $Id: JXRootPane.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 java.awt.BorderLayout;
025    import java.awt.Component;
026    import java.awt.Container;
027    import java.awt.Dimension;
028    import java.awt.Insets;
029    import java.awt.LayoutManager;
030    import java.awt.LayoutManager2;
031    import java.awt.Rectangle;
032    import java.awt.event.ActionEvent;
033    import java.awt.event.KeyEvent;
034    
035    import javax.swing.AbstractAction;
036    import javax.swing.Action;
037    import javax.swing.InputMap;
038    import javax.swing.JButton;
039    import javax.swing.JComponent;
040    import javax.swing.JMenuBar;
041    import javax.swing.JRootPane;
042    import javax.swing.JToolBar;
043    import javax.swing.KeyStroke;
044    
045    /**
046     * Extends the JRootPane by supporting specific placements for a toolbar and a
047     * status bar. If a status bar exists, then toolbars, menus will be registered 
048     * with the status bar.
049     * 
050     * @see JXStatusBar
051     * @author Mark Davidson
052     */
053    public class JXRootPane extends JRootPane {
054        /**
055         * An extended {@code RootLayout} offering support for managing the status
056         * bar.
057         * 
058         * @author Karl George Schaefer
059         * @author Jeanette Winzenberg
060         */
061        protected class XRootLayout extends RootLayout {
062    
063            LayoutManager2 delegate;
064    
065            /**
066             * The layout manager backing this manager. The delegate is used to
067             * calculate the size when the UI handles the window decorations.
068             * 
069             * @param delegate
070             *            the backing manager
071             */
072            public void setLayoutManager(LayoutManager2 delegate) {
073                this.delegate = delegate;
074            }
075    
076            private Dimension delegatePreferredLayoutSize(Container parent) {
077                if (delegate == null)
078                    return super.preferredLayoutSize(parent);
079                return delegate.preferredLayoutSize(parent);
080            }
081    
082            /**
083             * {@inheritDoc}
084             */
085            @Override
086            public Dimension preferredLayoutSize(Container parent) {
087                Dimension pref = delegatePreferredLayoutSize(parent);
088                if (statusBar != null && statusBar.isVisible()) {
089                    Dimension statusPref = statusBar.getPreferredSize();
090                    pref.width = Math.max(pref.width, statusPref.width);
091                    pref.height += statusPref.height;
092                }
093                return pref;
094            }
095    
096            private Dimension delegateMinimumLayoutSize(Container parent) {
097                if (delegate == null)
098                    return super.minimumLayoutSize(parent);
099                return delegate.minimumLayoutSize(parent);
100            }
101    
102            /**
103             * {@inheritDoc}
104             */
105            @Override
106            public Dimension minimumLayoutSize(Container parent) {
107                Dimension pref = delegateMinimumLayoutSize(parent);
108                if (statusBar != null && statusBar.isVisible()) {
109                    Dimension statusPref = statusBar.getMinimumSize();
110                    pref.width = Math.max(pref.width, statusPref.width);
111                    pref.height += statusPref.height;
112                }
113                return pref;
114    
115            }
116    
117            private Dimension delegateMaximumLayoutSize(Container parent) {
118                if (delegate == null)
119    
120                    return super.maximumLayoutSize(parent);
121                return delegate.maximumLayoutSize(parent);
122            }
123    
124            /**
125             * {@inheritDoc}
126             */
127            @Override
128            public Dimension maximumLayoutSize(Container target) {
129                Dimension pref = delegateMaximumLayoutSize(target);
130                if (statusBar != null && statusBar.isVisible()) {
131                    Dimension statusPref = statusBar.getMaximumSize();
132                    pref.width = Math.max(pref.width, statusPref.width);
133                    // PENDING JW: overflow?
134                    pref.height += statusPref.height;
135                }
136                return pref;
137            }
138    
139            private void delegateLayoutContainer(Container parent) {
140                if (delegate == null) {
141                    super.layoutContainer(parent);
142                } else {
143                    delegate.layoutContainer(parent);
144                }
145            }
146    
147            /**
148             * {@inheritDoc}
149             */
150            @Override
151            public void layoutContainer(Container parent) {
152                delegateLayoutContainer(parent);
153                if (statusBar == null || !statusBar.isVisible())
154                    return;
155                Rectangle b = parent.getBounds();
156                Insets i = getInsets();
157                int w = b.width - i.right - i.left;
158                int h = b.height - i.top - i.bottom;
159                Dimension statusPref = statusBar.getPreferredSize();
160                statusBar.setBounds(i.right, b.height - i.bottom
161                        - statusPref.height, w, statusPref.height);
162                if (contentPane != null) {
163                    Rectangle bounds = contentPane.getBounds();
164                    contentPane.setBounds(bounds.x, bounds.y, bounds.width,
165                            bounds.height - statusPref.height);
166                }
167    
168            }
169        }
170        
171        /**
172         * The current status bar for this root pane.
173         */
174        protected JXStatusBar statusBar;
175    
176        private JToolBar toolBar;
177    
178        /** 
179         * The button that gets activated when the pane has the focus and
180         * a UI-specific action like pressing the <b>ESC</b> key occurs.
181         */
182        private JButton cancelButton;
183    
184        /**
185         * Creates an extended root pane.
186         */
187        public JXRootPane() {
188            installKeyboardActions();
189        }
190    
191        /**
192         * {@inheritDoc}
193         */
194        @Override
195        protected Container createContentPane() {
196            JComponent c = new JXPanel() {
197                /**
198                 * {@inheritDoc}
199                 */
200                @Override
201                protected void addImpl(Component comp, Object constraints, int index) {
202                    synchronized (getTreeLock()) {
203                        super.addImpl(comp, constraints, index);
204                        registerStatusBar(comp);
205                    }
206                }
207    
208                /**
209                 * {@inheritDoc}
210                 */
211                @Override
212                public void remove(int index) {
213                    synchronized (getTreeLock()) {
214                        unregisterStatusBar(getComponent(index));
215                        super.remove(index);
216                    }
217                }
218    
219                /**
220                 * {@inheritDoc}
221                 */
222                @Override
223                public void removeAll() {
224                    synchronized (getTreeLock()) {
225                        for (Component c : getComponents()) {
226                            unregisterStatusBar(c);
227                        }
228                        
229                        super.removeAll();
230                    }
231                }
232            };
233            c.setName(this.getName()+".contentPane");
234            c.setLayout(new BorderLayout() {
235                /* This BorderLayout subclass maps a null constraint to CENTER.
236                 * Although the reference BorderLayout also does this, some VMs
237                 * throw an IllegalArgumentException.
238                 */
239                @Override
240                public void addLayoutComponent(Component comp, Object constraints) {
241                    if (constraints == null) {
242                        constraints = BorderLayout.CENTER;
243                    }
244                    super.addLayoutComponent(comp, constraints);
245                }
246            });
247            return c;
248        }
249    
250        
251        /**
252         * {@inheritDoc}
253         */
254        @Override
255        public void setLayout(LayoutManager layout) {
256            if (layout instanceof XRootLayout) {
257                // happens if decoration is uninstalled by ui
258                if ((layout != null) && (layout == getLayout())) {
259                    ((XRootLayout) layout).setLayoutManager(null);
260                }
261                super.setLayout(layout);
262            } else {
263                if (layout instanceof LayoutManager2) {
264                    ((XRootLayout) getLayout()).setLayoutManager((LayoutManager2) layout);
265                    if (!isValid()) {
266                        invalidate();
267                    }
268                }
269            }
270        }
271    
272        /**
273         * {@inheritDoc}
274         */
275        @Override
276        protected LayoutManager createRootLayout() {
277            return new XRootLayout();
278        } 
279    
280        /**
281         * PENDING: move to UI
282         * 
283         */
284        private void installKeyboardActions() {
285            Action escAction = new AbstractAction() {
286                public void actionPerformed(ActionEvent evt) {
287                    JButton cancelButton = getCancelButton();
288                    if (cancelButton != null) {
289                        cancelButton.doClick(20);
290                    }
291                }
292                
293                /**
294                 * Overridden to hack around #566-swing: 
295                 * JXRootPane eats escape keystrokes from datepicker popup.
296                 * Disable action if there is no cancel button.<p>
297                 * 
298                 * That's basically what RootPaneUI does - only not in 
299                 * the parameterless isEnabled, but in the one that passes
300                 * in the sender (available in UIAction only). We can't test 
301                 * nor compare against core behaviour, UIAction has
302                 * sun package scope. <p>
303                 * 
304                 * 
305                 */
306                @Override
307                public boolean isEnabled() {
308                    return (cancelButton != null) && (cancelButton.isEnabled());
309                }
310            };
311            getActionMap().put("esc-action", escAction);
312            InputMap im = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
313            KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
314            im.put(key, "esc-action");
315        }
316        
317        private void registerStatusBar(Component comp) {
318            if (statusBar == null || comp == null) {
319                return;
320            }
321    //        if (comp instanceof MessageSource) {
322    //            MessageSource source = (MessageSource) comp;
323    //            source.addMessageListener(statusBar);
324    //        }
325    //        if (comp instanceof ProgressSource) {
326    //            ProgressSource source = (ProgressSource) comp;
327    //            source.addProgressListener(statusBar);
328    //        }
329            if (comp instanceof Container) {
330                Component[] comps = ((Container) comp).getComponents();
331                for (int i = 0; i < comps.length; i++) {
332                    registerStatusBar(comps[i]);
333                }
334            }
335        }
336    
337        private void unregisterStatusBar(Component comp) {
338            if (statusBar == null || comp == null) {
339                return;
340            }
341    //        if (comp instanceof MessageSource) {
342    //            MessageSource source = (MessageSource) comp;
343    //            source.removeMessageListener(statusBar);
344    //        }
345    //        if (comp instanceof ProgressSource) {
346    //            ProgressSource source = (ProgressSource) comp;
347    //            source.removeProgressListener(statusBar);
348    //        }
349            if (comp instanceof Container) {
350                Component[] comps = ((Container) comp).getComponents();
351                for (int i = 0; i < comps.length; i++) {
352                    unregisterStatusBar(comps[i]);
353                }
354            }
355        }
356    
357        /**
358         * Set the status bar for this root pane. Any components held by this root
359         * pane will be registered. If this is replacing an existing status bar then
360         * the existing component will be unregistered from the old status bar.
361         * 
362         * @param statusBar
363         *            the status bar to use
364         */
365        public void setStatusBar(JXStatusBar statusBar) {
366            JXStatusBar oldStatusBar = this.statusBar;
367            this.statusBar = statusBar;
368    
369    //        if (statusBar != null) {
370    //            if (handler == null) {
371                    // Create the new mouse handler and register the toolbar
372                    // and menu components.
373    //                handler = new MouseMessagingHandler(this, statusBar);
374    //                if (toolBar != null) {
375    //                    handler.registerListeners(toolBar.getComponents());
376    //                }
377    //                if (menuBar != null) {
378    //                    handler.registerListeners(menuBar.getSubElements());
379    //                }
380    //            } else {
381    //                handler.setMessageListener(statusBar);
382    //            }
383    //        }
384    
385            Component[] comps = getContentPane().getComponents();
386            for (int i = 0; i < comps.length; i++) {
387                // Unregister the old status bar.
388                unregisterStatusBar(comps[i]);
389    
390                // register the new status bar.
391                registerStatusBar(comps[i]);
392            }
393            if (oldStatusBar != null) {
394                remove(oldStatusBar);
395            }
396            if (statusBar != null) {
397                add(statusBar);
398            }
399            firePropertyChange("statusBar", oldStatusBar, getStatusBar());
400        }
401    
402        /**
403         * Gets the currently installed status bar.
404         * 
405         * @return the current status bar
406         */
407        public JXStatusBar getStatusBar() {
408            return statusBar;
409        }
410    
411    //    private MouseMessagingHandler handler;
412    
413        /**
414         * Set the toolbar bar for this root pane. If a tool bar is currently registered with this
415         * {@code JXRootPane}, then it is removed prior to setting the new tool
416         * bar. If an implementation needs to handle more than one tool bar, a
417         * subclass will need to override the singleton logic used here or manually
418         * add toolbars with {@code getContentPane().add}.
419         * 
420         * @param toolBar
421         *            the toolbar to register
422         */
423        public void setToolBar(JToolBar toolBar) {
424            JToolBar oldToolBar = getToolBar();
425            this.toolBar = toolBar;
426    
427            if (oldToolBar != null) {
428                getContentPane().remove(oldToolBar);
429                
430    //            if (handler != null) {
431    //                handler.unregisterListeners(oldToolBar.getComponents());
432    //            }
433            }
434            
435    //        if (handler != null && this.toolBar != null) {
436    //            handler.registerListeners(this.toolBar.getComponents());
437    //        }
438    
439            getContentPane().add(BorderLayout.NORTH, this.toolBar);
440            
441            //ensure the new toolbar is correctly sized and displayed
442            getContentPane().validate();
443            
444            firePropertyChange("toolBar", oldToolBar, getToolBar());
445        }
446    
447        /**
448         * The currently installed tool bar.
449         * 
450         * @return the current tool bar
451         */
452        public JToolBar getToolBar() {
453            return toolBar;
454        }
455    
456        /**
457         * {@inheritDoc}
458         */
459        @Override
460        public void setJMenuBar(JMenuBar menuBar) {
461            JMenuBar oldMenuBar = this.menuBar;
462    
463            super.setJMenuBar(menuBar);
464    
465    //        if (handler != null && oldMenuBar != null) {
466    //            handler.unregisterListeners(oldMenuBar.getSubElements());
467    //        }
468    //
469    //        if (handler != null && menuBar != null) {
470    //            handler.registerListeners(menuBar.getSubElements());
471    //        }
472        }
473    
474        /**
475         * Sets the <code>cancelButton</code> property,
476         * which determines the current default cancel button for this <code>JRootPane</code>.
477         * The cancel button is the button which will be activated 
478         * when a UI-defined activation event (typically the <b>ESC</b> key) 
479         * occurs in the root pane regardless of whether or not the button 
480         * has keyboard focus (unless there is another component within 
481         * the root pane which consumes the activation event,
482         * such as a <code>JTextPane</code>).
483         * For default activation to work, the button must be an enabled
484         * descendant of the root pane when activation occurs.
485         * To remove a cancel button from this root pane, set this
486         * property to <code>null</code>.
487         *
488         * @param cancelButton the <code>JButton</code> which is to be the cancel button
489         * @see #getCancelButton() 
490         *
491         * @beaninfo
492         *  description: The button activated by default for cancel actions in this root pane
493         */
494        public void setCancelButton(JButton cancelButton) { 
495            JButton old = this.cancelButton;
496    
497            if (old != cancelButton) {
498                this.cancelButton = cancelButton;
499    
500                if (old != null) {
501                    old.repaint();
502                }
503                if (cancelButton != null) {
504                    cancelButton.repaint();
505                } 
506            }
507    
508            firePropertyChange("cancelButton", old, cancelButton);        
509        }
510    
511        /**
512         * Returns the value of the <code>cancelButton</code> property. 
513         * @return the <code>JButton</code> which is currently the default cancel button
514         * @see #setCancelButton
515         */
516        public JButton getCancelButton() { 
517            return cancelButton;
518        }
519    
520    }