1 /*
   2  * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.apple.laf;
  27 
  28 import java.awt.*;
  29 import java.awt.event.*;
  30 import java.beans.PropertyChangeEvent;
  31 
  32 import javax.swing.*;
  33 import javax.swing.event.*;
  34 import javax.swing.plaf.*;
  35 import javax.swing.plaf.basic.BasicRootPaneUI;
  36 
  37 import com.apple.laf.AquaUtils.RecyclableSingleton;
  38 import com.apple.laf.AquaUtils.RecyclableSingletonFromDefaultConstructor;
  39 
  40 /**
  41  * From AquaRootPaneUI.java
  42  *
  43  * The JRootPane manages the default button.  There can be only one active rootpane,
  44  * and one default button, so we need only one timer
  45  *
  46  * AquaRootPaneUI is a singleton object
  47  */
  48 public class AquaRootPaneUI extends BasicRootPaneUI implements AncestorListener, WindowListener, ContainerListener {
  49     private static final RecyclableSingleton<AquaRootPaneUI> sRootPaneUI = new RecyclableSingletonFromDefaultConstructor<AquaRootPaneUI>(AquaRootPaneUI.class);
  50 
  51     final static int kDefaultButtonPaintDelayBetweenFrames = 50;
  52     JButton fCurrentDefaultButton = null;
  53     Timer fTimer = null;
  54     static final boolean sUseScreenMenuBar = AquaMenuBarUI.getScreenMenuBarProperty();
  55 
  56     public static ComponentUI createUI(final JComponent c) {
  57         return sRootPaneUI.get();
  58     }
  59 
  60     public void installUI(final JComponent c) {
  61         super.installUI(c);
  62         c.addAncestorListener(this);
  63 
  64         if (c.isShowing() && c.isEnabled()) {
  65             updateDefaultButton((JRootPane)c);
  66         }
  67 
  68         // for <rdar://problem/3689020> REGR: Realtime LAF updates no longer work
  69         //
  70         // because the JFrame parent has a LAF background set (why without a UI element I don't know!)
  71         // we have to set it from the root pane so when we are coming from metal we will set it to
  72         // the aqua color.
  73         // This is because the aqua color is a magical color that gets the background of the window,
  74         // so for most other LAFs the root pane changing is enough since it would be opaque, but for us
  75         // it is not since we are going to grab the one that was set on the JFrame. :(
  76         final Component parent = c.getParent();
  77 
  78         if (parent != null && parent instanceof JFrame) {
  79             final JFrame frameParent = (JFrame)parent;
  80             final Color bg = frameParent.getBackground();
  81             if (bg == null || bg instanceof UIResource) {
  82                 frameParent.setBackground(UIManager.getColor("Panel.background"));
  83             }
  84         }
  85 
  86         // for <rdar://problem/3750909> OutOfMemoryError swapping menus.
  87         // Listen for layered pane/JMenuBar updates if the screen menu bar is active.
  88         if (sUseScreenMenuBar) {
  89             final JRootPane root = (JRootPane)c;
  90             root.addContainerListener(this);
  91             root.getLayeredPane().addContainerListener(this);
  92         }
  93     }
  94 
  95     public void uninstallUI(final JComponent c) {
  96         stopTimer();
  97         c.removeAncestorListener(this);
  98 
  99         if (sUseScreenMenuBar) {
 100             final JRootPane root = (JRootPane)c;
 101             root.removeContainerListener(this);
 102             root.getLayeredPane().removeContainerListener(this);
 103         }
 104 
 105         super.uninstallUI(c);
 106     }
 107 
 108     /**
 109      * If the screen menu bar is active we need to listen to the layered pane of the root pane
 110      * because it holds the JMenuBar.  So, if a new layered pane was added, listen to it.
 111      * If a new JMenuBar was added, tell the menu bar UI, because it will need to update the menu bar.
 112      */
 113     public void componentAdded(final ContainerEvent e) {
 114         if (e.getContainer() instanceof JRootPane) {
 115             final JRootPane root = (JRootPane)e.getContainer();
 116             if (e.getChild() == root.getLayeredPane()) {
 117                 final JLayeredPane layered = root.getLayeredPane();
 118                 layered.addContainerListener(this);
 119             }
 120         } else {
 121             if (e.getChild() instanceof JMenuBar) {
 122                 final JMenuBar jmb = (JMenuBar)e.getChild();
 123                 final MenuBarUI mbui = jmb.getUI();
 124 
 125                 if (mbui instanceof AquaMenuBarUI) {
 126                     final Window owningWindow = SwingUtilities.getWindowAncestor(jmb);
 127 
 128                     // Could be a JDialog, and may have been added to a JRootPane not yet in a window.
 129                     if (owningWindow != null && owningWindow instanceof JFrame) {
 130                         ((AquaMenuBarUI)mbui).setScreenMenuBar((JFrame)owningWindow);
 131                     }
 132                 }
 133             }
 134         }
 135     }
 136 
 137     /**
 138      * Likewise, when the layered pane is removed from the root pane, stop listening to it.
 139      * If the JMenuBar is removed, tell the menu bar UI to clear the menu bar.
 140      */
 141     public void componentRemoved(final ContainerEvent e) {
 142         if (e.getContainer() instanceof JRootPane) {
 143             final JRootPane root = (JRootPane)e.getContainer();
 144             if (e.getChild() == root.getLayeredPane()) {
 145                 final JLayeredPane layered = root.getLayeredPane();
 146                 layered.removeContainerListener(this);
 147             }
 148         } else {
 149             if (e.getChild() instanceof JMenuBar) {
 150                 final JMenuBar jmb = (JMenuBar)e.getChild();
 151                 final MenuBarUI mbui = jmb.getUI();
 152 
 153                 if (mbui instanceof AquaMenuBarUI) {
 154                     final Window owningWindow = SwingUtilities.getWindowAncestor(jmb);
 155 
 156                     // Could be a JDialog, and may have been added to a JRootPane not yet in a window.
 157                     if (owningWindow != null && owningWindow instanceof JFrame) {
 158                         ((AquaMenuBarUI)mbui).clearScreenMenuBar((JFrame)owningWindow);
 159                     }
 160                 }
 161             }
 162         }
 163     }
 164 
 165     /**
 166      * Invoked when a property changes on the root pane. If the event
 167      * indicates the <code>defaultButton</code> has changed, this will
 168      * update the animation.
 169      * If the enabled state changed, it will start or stop the animation
 170      */
 171     public void propertyChange(final PropertyChangeEvent e) {
 172         super.propertyChange(e);
 173 
 174         final String prop = e.getPropertyName();
 175         if ("defaultButton".equals(prop) || "temporaryDefaultButton".equals(prop)) {
 176             // Change the animating button if this root is showing and enabled
 177             // otherwise do nothing - someone else may be active
 178             final JRootPane root = (JRootPane)e.getSource();
 179 
 180             if (root.isShowing() && root.isEnabled()) {
 181                 updateDefaultButton(root);
 182             }
 183         } else if ("enabled".equals(prop) || AquaFocusHandler.FRAME_ACTIVE_PROPERTY.equals(prop)) {
 184             final JRootPane root = (JRootPane)e.getSource();
 185             if (root.isShowing()) {
 186                 if (((Boolean)e.getNewValue()).booleanValue()) {
 187                     updateDefaultButton((JRootPane)e.getSource());
 188                 } else {
 189                     stopTimer();
 190                 }
 191             }
 192         }
 193     }
 194 
 195     synchronized void stopTimer() {
 196         if (fTimer != null) {
 197             fTimer.stop();
 198             fTimer = null;
 199         }
 200     }
 201 
 202     synchronized void updateDefaultButton(final JRootPane root) {
 203         final JButton button = root.getDefaultButton();
 204         //System.err.println("in updateDefaultButton button = " + button);
 205         fCurrentDefaultButton = button;
 206         stopTimer();
 207         if (button != null) {
 208             fTimer = new Timer(kDefaultButtonPaintDelayBetweenFrames, new DefaultButtonPainter(root));
 209             fTimer.start();
 210         }
 211     }
 212 
 213     class DefaultButtonPainter implements ActionListener {
 214         JRootPane root;
 215 
 216         public DefaultButtonPainter(final JRootPane root) {
 217             this.root = root;
 218         }
 219 
 220         public void actionPerformed(final ActionEvent e) {
 221             final JButton defaultButton = root.getDefaultButton();
 222             if ((defaultButton != null) && defaultButton.isShowing()) {
 223                 if (defaultButton.isEnabled()) {
 224                     defaultButton.repaint();
 225                 }
 226             } else {
 227                 stopTimer();
 228             }
 229         }
 230     }
 231 
 232     /**
 233      * This is sort of like viewDidMoveToWindow:.  When the root pane is put into a window
 234      * this method gets called for the notification.
 235      * We need to set up the listener relationship so we can pick up activation events.
 236      * And, if a JMenuBar was added before the root pane was added to the window, we now need
 237      * to notify the menu bar UI.
 238      */
 239     public void ancestorAdded(final AncestorEvent event) {
 240         // this is so we can handle window activated and deactivated events so
 241         // our swing controls can color/enable/disable/focus draw correctly
 242         final Container ancestor = event.getComponent();
 243         final Window owningWindow = SwingUtilities.getWindowAncestor(ancestor);
 244 
 245         if (owningWindow != null) {
 246             // We get this message even when a dialog is opened and the owning window is a window
 247             // that could already be listened to. We should only be a listener once.
 248             // adding multiple listeners was the cause of <rdar://problem/3534047>
 249             // but the incorrect removal of them caused <rdar://problem/3617848>
 250             owningWindow.removeWindowListener(this);
 251             owningWindow.addWindowListener(this);
 252         }
 253 
 254         // The root pane has been added to the hierarchy.  If it's enabled update the default
 255         // button to start the throbbing.  Since the UI is a singleton make sure the root pane
 256         // we are checking has a default button before calling update otherwise we will stop
 257         // throbbing the current default button.
 258         final JComponent comp = event.getComponent();
 259         if (comp instanceof JRootPane) {
 260             final JRootPane rp = (JRootPane)comp;
 261             if (rp.isEnabled() && rp.getDefaultButton() != null) {
 262                 updateDefaultButton((JRootPane)comp);
 263             }
 264         }
 265     }
 266 
 267     /**
 268      * If the JRootPane was removed from the window we should clear the screen menu bar.
 269      * That's a non-trivial problem, because you need to know which window the JRootPane was in
 270      * before it was removed.  By the time ancestorRemoved was called, the JRootPane has already been removed
 271      */
 272 
 273     public void ancestorRemoved(final AncestorEvent event) { }
 274     public void ancestorMoved(final AncestorEvent event) { }
 275 
 276     public void windowActivated(final WindowEvent e) {
 277         updateComponentTreeUIActivation((Component)e.getSource(), Boolean.TRUE);
 278     }
 279 
 280     public void windowDeactivated(final WindowEvent e) {
 281         updateComponentTreeUIActivation((Component)e.getSource(), Boolean.FALSE);
 282     }
 283 
 284     public void windowOpened(final WindowEvent e) { }
 285     public void windowClosing(final WindowEvent e) { }
 286 
 287     public void windowClosed(final WindowEvent e) {
 288         // We know the window is closed so remove the listener.
 289         final Window w = e.getWindow();
 290         w.removeWindowListener(this);
 291     }
 292 
 293     public void windowIconified(final WindowEvent e) { }
 294     public void windowDeiconified(final WindowEvent e) { }
 295     public void windowStateChanged(final WindowEvent e) { }
 296     public void windowGainedFocus(final WindowEvent e) { }
 297     public void windowLostFocus(final WindowEvent e) { }
 298 
 299     private static void updateComponentTreeUIActivation(final Component c, Object active) {
 300         if (c instanceof javax.swing.JInternalFrame) {
 301             active = (((JInternalFrame)c).isSelected() ? Boolean.TRUE : Boolean.FALSE);
 302         }
 303 
 304         if (c instanceof javax.swing.JComponent) {
 305             ((javax.swing.JComponent)c).putClientProperty(AquaFocusHandler.FRAME_ACTIVE_PROPERTY, active);
 306         }
 307 
 308         Component[] children = null;
 309 
 310         if (c instanceof javax.swing.JMenu) {
 311             children = ((javax.swing.JMenu)c).getMenuComponents();
 312         } else if (c instanceof Container) {
 313             children = ((Container)c).getComponents();
 314         }
 315 
 316         if (children == null) return;
 317 
 318         for (final Component element : children) {
 319             updateComponentTreeUIActivation(element, active);
 320         }
 321     }
 322 }