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