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