1 /*
   2  * Copyright (c) 2006, 2014, 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.sun.java.swing.plaf.windows;
  27 
  28 import java.security.AccessController;
  29 import sun.security.action.GetBooleanAction;
  30 
  31 import java.util.*;
  32 import java.beans.PropertyChangeListener;
  33 import java.beans.PropertyChangeEvent;
  34 import java.awt.*;
  35 import java.awt.event.*;
  36 import javax.swing.*;
  37 
  38 
  39 
  40 import sun.swing.UIClientPropertyKey;
  41 import com.sun.java.swing.plaf.windows.TMSchema.State;
  42 import static com.sun.java.swing.plaf.windows.TMSchema.State.*;
  43 import com.sun.java.swing.plaf.windows.TMSchema.Part;
  44 import com.sun.java.swing.plaf.windows.TMSchema.Prop;
  45 import com.sun.java.swing.plaf.windows.XPStyle.Skin;
  46 
  47 import sun.awt.AppContext;
  48 
  49 /**
  50  * A class to help mimic Vista theme animations.  The only kind of
  51  * animation it handles for now is 'transition' animation (this seems
  52  * to be the only animation which Vista theme can do). This is when
  53  * one picture fadein over another one in some period of time.
  54  * According to
  55  * https://connect.microsoft.com/feedback/ViewFeedback.aspx?FeedbackID=86852&SiteID=4
  56  * The animations are all linear.
  57  *
  58  * This class has a number of responsibilities.
  59  * <ul>
  60  *   <li> It trigger rapaint for the UI components involved in the animation
  61  *   <li> It tracks the animation state for every UI component involved in the
  62  *        animation and paints {@code Skin} in new {@code State} over the
  63  *        {@code Skin} in last {@code State} using
  64  *        {@code AlphaComposite.SrcOver.derive(alpha)} where {code alpha}
  65  *        depends on the state of animation
  66  * </ul>
  67  *
  68  * @author Igor Kushnirskiy
  69  */
  70 class AnimationController implements ActionListener, PropertyChangeListener {
  71 
  72     private final static boolean VISTA_ANIMATION_DISABLED =
  73         AccessController.doPrivileged(new GetBooleanAction("swing.disablevistaanimation"));
  74 
  75 
  76     private final static Object ANIMATION_CONTROLLER_KEY =
  77         new StringBuilder("ANIMATION_CONTROLLER_KEY");
  78 
  79     private final Map<JComponent, Map<Part, AnimationState>> animationStateMap =
  80             new WeakHashMap<JComponent, Map<Part, AnimationState>>();
  81 
  82     //this timer is used to cause repaint on animated components
  83     //30 repaints per second should give smooth animation affect
  84     private final javax.swing.Timer timer =
  85         new javax.swing.Timer(1000/30, this);
  86 
  87     private static synchronized AnimationController getAnimationController() {
  88         AppContext appContext = AppContext.getAppContext();
  89         Object obj = appContext.get(ANIMATION_CONTROLLER_KEY);
  90         if (obj == null) {
  91             obj = new AnimationController();
  92             appContext.put(ANIMATION_CONTROLLER_KEY, obj);
  93         }
  94         return (AnimationController) obj;
  95     }
  96 
  97     private AnimationController() {
  98         timer.setRepeats(true);
  99         timer.setCoalesce(true);
 100         //we need to dispose the controller on l&f change
 101         UIManager.addPropertyChangeListener(this);
 102     }
 103 
 104     private static void triggerAnimation(JComponent c,
 105                            Part part, State newState) {
 106         if (c instanceof javax.swing.JTabbedPane
 107             || part == Part.TP_BUTTON) {
 108             //idk: we can not handle tabs animation because
 109             //the same (component,part) is used to handle all the tabs
 110             //and we can not track the states
 111             //Vista theme might have transition duration for toolbar buttons
 112             //but native application does not seem to animate them
 113             return;
 114         }
 115         AnimationController controller =
 116             AnimationController.getAnimationController();
 117         State oldState = controller.getState(c, part);
 118         if (oldState != newState) {
 119             controller.putState(c, part, newState);
 120             if (newState == State.DEFAULTED) {
 121                 // it seems for DEFAULTED button state Vista does animation from
 122                 // HOT
 123                 oldState = State.HOT;
 124             }
 125             if (oldState != null) {
 126                 long duration;
 127                 if (newState == State.DEFAULTED) {
 128                     //Only button might have DEFAULTED state
 129                     //idk: do not know how to get the value from Vista
 130                     //one second seems plausible value
 131                     duration = 1000;
 132                 } else {
 133                     XPStyle xp = XPStyle.getXP();
 134                     duration = (xp != null)
 135                                ? xp.getThemeTransitionDuration(
 136                                        c, part,
 137                                        normalizeState(oldState),
 138                                        normalizeState(newState),
 139                                        Prop.TRANSITIONDURATIONS)
 140                                : 1000;
 141                 }
 142                 controller.startAnimation(c, part, oldState, newState, duration);
 143             }
 144         }
 145     }
 146 
 147     // for scrollbar up, down, left and right button pictures are
 148     // defined by states.  It seems that theme has duration defined
 149     // only for up button states thus we doing this translation here.
 150     private static State normalizeState(State state) {
 151         State rv;
 152         switch (state) {
 153         case DOWNPRESSED:
 154             /* falls through */
 155         case LEFTPRESSED:
 156             /* falls through */
 157         case RIGHTPRESSED:
 158             rv = UPPRESSED;
 159             break;
 160 
 161         case DOWNDISABLED:
 162             /* falls through */
 163         case LEFTDISABLED:
 164             /* falls through */
 165         case RIGHTDISABLED:
 166             rv = UPDISABLED;
 167             break;
 168 
 169         case DOWNHOT:
 170             /* falls through */
 171         case LEFTHOT:
 172             /* falls through */
 173         case RIGHTHOT:
 174             rv = UPHOT;
 175             break;
 176 
 177         case DOWNNORMAL:
 178             /* falls through */
 179         case LEFTNORMAL:
 180             /* falls through */
 181         case RIGHTNORMAL:
 182             rv = UPNORMAL;
 183             break;
 184 
 185         default :
 186             rv = state;
 187             break;
 188         }
 189         return rv;
 190     }
 191 
 192     private synchronized State getState(JComponent component, Part part) {
 193         State rv = null;
 194         Object tmpObject =
 195             component.getClientProperty(PartUIClientPropertyKey.getKey(part));
 196         if (tmpObject instanceof State) {
 197             rv = (State) tmpObject;
 198         }
 199         return rv;
 200     }
 201 
 202     private synchronized void putState(JComponent component, Part part,
 203                                        State state) {
 204         component.putClientProperty(PartUIClientPropertyKey.getKey(part),
 205                                     state);
 206     }
 207 
 208     private synchronized void startAnimation(JComponent component,
 209                                      Part part,
 210                                      State startState,
 211                                      State endState,
 212                                      long millis) {
 213         boolean isForwardAndReverse = false;
 214         if (endState == State.DEFAULTED) {
 215             isForwardAndReverse = true;
 216         }
 217         Map<Part, AnimationState> map = animationStateMap.get(component);
 218         if (millis <= 0) {
 219             if (map != null) {
 220                 map.remove(part);
 221                 if (map.size() == 0) {
 222                     animationStateMap.remove(component);
 223                 }
 224             }
 225             return;
 226         }
 227         if (map == null) {
 228             map = new EnumMap<Part, AnimationState>(Part.class);
 229             animationStateMap.put(component, map);
 230         }
 231         map.put(part,
 232                 new AnimationState(startState, millis, isForwardAndReverse));
 233         if (! timer.isRunning()) {
 234             timer.start();
 235         }
 236     }
 237 
 238     static void paintSkin(JComponent component, Skin skin,
 239                       Graphics g, int dx, int dy, int dw, int dh, State state) {
 240         if (VISTA_ANIMATION_DISABLED) {
 241             skin.paintSkinRaw(g, dx, dy, dw, dh, state);
 242             return;
 243         }
 244         triggerAnimation(component, skin.part, state);
 245         AnimationController controller = getAnimationController();
 246         synchronized (controller) {
 247             AnimationState animationState = null;
 248             Map<Part, AnimationState> map =
 249                 controller.animationStateMap.get(component);
 250             if (map != null) {
 251                 animationState = map.get(skin.part);
 252             }
 253             if (animationState != null) {
 254                 animationState.paintSkin(skin, g, dx, dy, dw, dh, state);
 255             } else {
 256                 skin.paintSkinRaw(g, dx, dy, dw, dh, state);
 257             }
 258         }
 259     }
 260 
 261     public synchronized void propertyChange(PropertyChangeEvent e) {
 262         if ("lookAndFeel" == e.getPropertyName()
 263             && ! (e.getNewValue() instanceof WindowsLookAndFeel) ) {
 264             dispose();
 265         }
 266     }
 267 
 268     public synchronized void actionPerformed(ActionEvent e) {
 269         java.util.List<JComponent> componentsToRemove = null;
 270         java.util.List<Part> partsToRemove = null;
 271         for (JComponent component : animationStateMap.keySet()) {
 272             component.repaint();
 273             if (partsToRemove != null) {
 274                 partsToRemove.clear();
 275             }
 276             Map<Part, AnimationState> map = animationStateMap.get(component);
 277             if (! component.isShowing()
 278                   || map == null
 279                   || map.size() == 0) {
 280                 if (componentsToRemove == null) {
 281                     componentsToRemove = new ArrayList<JComponent>();
 282                 }
 283                 componentsToRemove.add(component);
 284                 continue;
 285             }
 286             for (Part part : map.keySet()) {
 287                 if (map.get(part).isDone()) {
 288                     if (partsToRemove == null) {
 289                         partsToRemove = new ArrayList<Part>();
 290                     }
 291                     partsToRemove.add(part);
 292                 }
 293             }
 294             if (partsToRemove != null) {
 295                 if (partsToRemove.size() == map.size()) {
 296                     //animation is done for the component
 297                     if (componentsToRemove == null) {
 298                         componentsToRemove = new ArrayList<JComponent>();
 299                     }
 300                     componentsToRemove.add(component);
 301                 } else {
 302                     for (Part part : partsToRemove) {
 303                         map.remove(part);
 304                     }
 305                 }
 306             }
 307         }
 308         if (componentsToRemove != null) {
 309             for (JComponent component : componentsToRemove) {
 310                 animationStateMap.remove(component);
 311             }
 312         }
 313         if (animationStateMap.size() == 0) {
 314             timer.stop();
 315         }
 316     }
 317 
 318     private synchronized void dispose() {
 319         timer.stop();
 320         UIManager.removePropertyChangeListener(this);
 321         synchronized (AnimationController.class) {
 322             AppContext.getAppContext()
 323                 .put(ANIMATION_CONTROLLER_KEY, null);
 324         }
 325     }
 326 
 327     private static class AnimationState {
 328         private final State startState;
 329 
 330         //animation duration in nanoseconds
 331         private final long duration;
 332 
 333         //animatin start time in nanoseconds
 334         private long startTime;
 335 
 336         //direction the alpha value is changing
 337         //forward  - from 0 to 1
 338         //!forward - from 1 to 0
 339         private boolean isForward = true;
 340 
 341         //if isForwardAndReverse the animation continually goes
 342         //forward and reverse. alpha value is changing from 0 to 1 then
 343         //from 1 to 0 and so forth
 344         private boolean isForwardAndReverse;
 345 
 346         private float progress;
 347 
 348         AnimationState(final State startState,
 349                        final long milliseconds,
 350                        boolean isForwardAndReverse) {
 351             assert startState != null && milliseconds > 0;
 352             assert SwingUtilities.isEventDispatchThread();
 353 
 354             this.startState = startState;
 355             this.duration = milliseconds * 1000000;
 356             this.startTime = System.nanoTime();
 357             this.isForwardAndReverse = isForwardAndReverse;
 358             progress = 0f;
 359         }
 360         private void updateProgress() {
 361             assert SwingUtilities.isEventDispatchThread();
 362 
 363             if (isDone()) {
 364                 return;
 365             }
 366             long currentTime = System.nanoTime();
 367 
 368             progress = ((float) (currentTime - startTime))
 369                 / duration;
 370             progress = Math.max(progress, 0); //in case time was reset
 371             if (progress >= 1) {
 372                 progress = 1;
 373                 if (isForwardAndReverse) {
 374                     startTime = currentTime;
 375                     progress = 0;
 376                     isForward = ! isForward;
 377                 }
 378             }
 379         }
 380         void paintSkin(Skin skin, Graphics _g,
 381                        int dx, int dy, int dw, int dh, State state) {
 382             assert SwingUtilities.isEventDispatchThread();
 383 
 384             updateProgress();
 385             if (! isDone()) {
 386                 Graphics2D g = (Graphics2D) _g.create();
 387                 skin.paintSkinRaw(g, dx, dy, dw, dh, startState);
 388                 float alpha;
 389                 if (isForward) {
 390                     alpha = progress;
 391                 } else {
 392                     alpha = 1 - progress;
 393                 }
 394                 g.setComposite(AlphaComposite.SrcOver.derive(alpha));
 395                 skin.paintSkinRaw(g, dx, dy, dw, dh, state);
 396                 g.dispose();
 397             } else {
 398                 skin.paintSkinRaw(_g, dx, dy, dw, dh, state);
 399             }
 400         }
 401         boolean isDone() {
 402             assert SwingUtilities.isEventDispatchThread();
 403 
 404             return  progress >= 1;
 405         }
 406     }
 407 
 408     private static class PartUIClientPropertyKey
 409           implements UIClientPropertyKey {
 410 
 411         private static final Map<Part, PartUIClientPropertyKey> map =
 412             new EnumMap<Part, PartUIClientPropertyKey>(Part.class);
 413 
 414         static synchronized PartUIClientPropertyKey getKey(Part part) {
 415             PartUIClientPropertyKey rv = map.get(part);
 416             if (rv == null) {
 417                 rv = new PartUIClientPropertyKey(part);
 418                 map.put(part, rv);
 419             }
 420             return rv;
 421         }
 422 
 423         private final Part part;
 424         private PartUIClientPropertyKey(Part part) {
 425             this.part  = part;
 426         }
 427         public String toString() {
 428             return part.toString();
 429         }
 430     }
 431 }