1 /*
   2  * Copyright (c) 2006, 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                      duration = XPStyle.getXP().getThemeTransitionDuration(
 134                            c, part,
 135                            normalizeState(oldState),
 136                            normalizeState(newState),
 137                            Prop.TRANSITIONDURATIONS);
 138                 }
 139                 controller.startAnimation(c, part, oldState, newState, duration);
 140             }
 141         }
 142     }
 143 
 144     // for scrollbar up, down, left and right button pictures are
 145     // defined by states.  It seems that theme has duration defined
 146     // only for up button states thus we doing this translation here.
 147     private static State normalizeState(State state) {
 148         State rv;
 149         switch (state) {
 150         case DOWNPRESSED:
 151             /* falls through */
 152         case LEFTPRESSED:
 153             /* falls through */
 154         case RIGHTPRESSED:
 155             rv = UPPRESSED;
 156             break;
 157 
 158         case DOWNDISABLED:
 159             /* falls through */
 160         case LEFTDISABLED:
 161             /* falls through */
 162         case RIGHTDISABLED:
 163             rv = UPDISABLED;
 164             break;
 165 
 166         case DOWNHOT:
 167             /* falls through */
 168         case LEFTHOT:
 169             /* falls through */
 170         case RIGHTHOT:
 171             rv = UPHOT;
 172             break;
 173 
 174         case DOWNNORMAL:
 175             /* falls through */
 176         case LEFTNORMAL:
 177             /* falls through */
 178         case RIGHTNORMAL:
 179             rv = UPNORMAL;
 180             break;
 181 
 182         default :
 183             rv = state;
 184             break;
 185         }
 186         return rv;
 187     }
 188 
 189     private synchronized State getState(JComponent component, Part part) {
 190         State rv = null;
 191         Object tmpObject =
 192             component.getClientProperty(PartUIClientPropertyKey.getKey(part));
 193         if (tmpObject instanceof State) {
 194             rv = (State) tmpObject;
 195         }
 196         return rv;
 197     }
 198 
 199     private synchronized void putState(JComponent component, Part part,
 200                                        State state) {
 201         component.putClientProperty(PartUIClientPropertyKey.getKey(part),
 202                                     state);
 203     }
 204 
 205     private synchronized void startAnimation(JComponent component,
 206                                      Part part,
 207                                      State startState,
 208                                      State endState,
 209                                      long millis) {
 210         boolean isForwardAndReverse = false;
 211         if (endState == State.DEFAULTED) {
 212             isForwardAndReverse = true;
 213         }
 214         Map<Part, AnimationState> map = animationStateMap.get(component);
 215         if (millis <= 0) {
 216             if (map != null) {
 217                 map.remove(part);
 218                 if (map.size() == 0) {
 219                     animationStateMap.remove(component);
 220                 }
 221             }
 222             return;
 223         }
 224         if (map == null) {
 225             map = new EnumMap<Part, AnimationState>(Part.class);
 226             animationStateMap.put(component, map);
 227         }
 228         map.put(part,
 229                 new AnimationState(startState, millis, isForwardAndReverse));
 230         if (! timer.isRunning()) {
 231             timer.start();
 232         }
 233     }
 234 
 235     static void paintSkin(JComponent component, Skin skin,
 236                       Graphics g, int dx, int dy, int dw, int dh, State state) {
 237         if (VISTA_ANIMATION_DISABLED) {
 238             skin.paintSkinRaw(g, dx, dy, dw, dh, state);
 239             return;
 240         }
 241         triggerAnimation(component, skin.part, state);
 242         AnimationController controller = getAnimationController();
 243         synchronized (controller) {
 244             AnimationState animationState = null;
 245             Map<Part, AnimationState> map =
 246                 controller.animationStateMap.get(component);
 247             if (map != null) {
 248                 animationState = map.get(skin.part);
 249             }
 250             if (animationState != null) {
 251                 animationState.paintSkin(skin, g, dx, dy, dw, dh, state);
 252             } else {
 253                 skin.paintSkinRaw(g, dx, dy, dw, dh, state);
 254             }
 255         }
 256     }
 257 
 258     public synchronized void propertyChange(PropertyChangeEvent e) {
 259         if ("lookAndFeel" == e.getPropertyName()
 260             && ! (e.getNewValue() instanceof WindowsLookAndFeel) ) {
 261             dispose();
 262         }
 263     }
 264 
 265     public synchronized void actionPerformed(ActionEvent e) {
 266         java.util.List<JComponent> componentsToRemove = null;
 267         java.util.List<Part> partsToRemove = null;
 268         for (JComponent component : animationStateMap.keySet()) {
 269             component.repaint();
 270             if (partsToRemove != null) {
 271                 partsToRemove.clear();
 272             }
 273             Map<Part, AnimationState> map = animationStateMap.get(component);
 274             if (! component.isShowing()
 275                   || map == null
 276                   || map.size() == 0) {
 277                 if (componentsToRemove == null) {
 278                     componentsToRemove = new ArrayList<JComponent>();
 279                 }
 280                 componentsToRemove.add(component);
 281                 continue;
 282             }
 283             for (Part part : map.keySet()) {
 284                 if (map.get(part).isDone()) {
 285                     if (partsToRemove == null) {
 286                         partsToRemove = new ArrayList<Part>();
 287                     }
 288                     partsToRemove.add(part);
 289                 }
 290             }
 291             if (partsToRemove != null) {
 292                 if (partsToRemove.size() == map.size()) {
 293                     //animation is done for the component
 294                     if (componentsToRemove == null) {
 295                         componentsToRemove = new ArrayList<JComponent>();
 296                     }
 297                     componentsToRemove.add(component);
 298                 } else {
 299                     for (Part part : partsToRemove) {
 300                         map.remove(part);
 301                     }
 302                 }
 303             }
 304         }
 305         if (componentsToRemove != null) {
 306             for (JComponent component : componentsToRemove) {
 307                 animationStateMap.remove(component);
 308             }
 309         }
 310         if (animationStateMap.size() == 0) {
 311             timer.stop();
 312         }
 313     }
 314 
 315     private synchronized void dispose() {
 316         timer.stop();
 317         UIManager.removePropertyChangeListener(this);
 318         synchronized (AnimationController.class) {
 319             AppContext.getAppContext()
 320                 .put(ANIMATION_CONTROLLER_KEY, null);
 321         }
 322     }
 323 
 324     private static class AnimationState {
 325         private final State startState;
 326 
 327         //animation duration in nanoseconds
 328         private final long duration;
 329 
 330         //animatin start time in nanoseconds
 331         private long startTime;
 332 
 333         //direction the alpha value is changing
 334         //forward  - from 0 to 1
 335         //!forward - from 1 to 0
 336         private boolean isForward = true;
 337 
 338         //if isForwardAndReverse the animation continually goes
 339         //forward and reverse. alpha value is changing from 0 to 1 then
 340         //from 1 to 0 and so forth
 341         private boolean isForwardAndReverse;
 342 
 343         private float progress;
 344 
 345         AnimationState(final State startState,
 346                        final long milliseconds,
 347                        boolean isForwardAndReverse) {
 348             assert startState != null && milliseconds > 0;
 349             assert SwingUtilities.isEventDispatchThread();
 350 
 351             this.startState = startState;
 352             this.duration = milliseconds * 1000000;
 353             this.startTime = System.nanoTime();
 354             this.isForwardAndReverse = isForwardAndReverse;
 355             progress = 0f;
 356         }
 357         private void updateProgress() {
 358             assert SwingUtilities.isEventDispatchThread();
 359 
 360             if (isDone()) {
 361                 return;
 362             }
 363             long currentTime = System.nanoTime();
 364 
 365             progress = ((float) (currentTime - startTime))
 366                 / duration;
 367             progress = Math.max(progress, 0); //in case time was reset
 368             if (progress >= 1) {
 369                 progress = 1;
 370                 if (isForwardAndReverse) {
 371                     startTime = currentTime;
 372                     progress = 0;
 373                     isForward = ! isForward;
 374                 }
 375             }
 376         }
 377         void paintSkin(Skin skin, Graphics _g,
 378                        int dx, int dy, int dw, int dh, State state) {
 379             assert SwingUtilities.isEventDispatchThread();
 380 
 381             updateProgress();
 382             if (! isDone()) {
 383                 Graphics2D g = (Graphics2D) _g.create();
 384                 skin.paintSkinRaw(g, dx, dy, dw, dh, startState);
 385                 float alpha;
 386                 if (isForward) {
 387                     alpha = progress;
 388                 } else {
 389                     alpha = 1 - progress;
 390                 }
 391                 g.setComposite(AlphaComposite.SrcOver.derive(alpha));
 392                 skin.paintSkinRaw(g, dx, dy, dw, dh, state);
 393                 g.dispose();
 394             } else {
 395                 skin.paintSkinRaw(_g, dx, dy, dw, dh, state);
 396             }
 397         }
 398         boolean isDone() {
 399             assert SwingUtilities.isEventDispatchThread();
 400 
 401             return  progress >= 1;
 402         }
 403     }
 404 
 405     private static class PartUIClientPropertyKey
 406           implements UIClientPropertyKey {
 407 
 408         private static final Map<Part, PartUIClientPropertyKey> map =
 409             new EnumMap<Part, PartUIClientPropertyKey>(Part.class);
 410 
 411         static synchronized PartUIClientPropertyKey getKey(Part part) {
 412             PartUIClientPropertyKey rv = map.get(part);
 413             if (rv == null) {
 414                 rv = new PartUIClientPropertyKey(part);
 415                 map.put(part, rv);
 416             }
 417             return rv;
 418         }
 419 
 420         private final Part part;
 421         private PartUIClientPropertyKey(Part part) {
 422             this.part  = part;
 423         }
 424         public String toString() {
 425             return part.toString();
 426         }
 427     }
 428 }