/*
* Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.java.swing.plaf.windows;
import java.security.AccessController;
import sun.security.action.GetBooleanAction;
import java.util.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import sun.swing.UIClientPropertyKey;
import com.sun.java.swing.plaf.windows.TMSchema.State;
import static com.sun.java.swing.plaf.windows.TMSchema.State.*;
import com.sun.java.swing.plaf.windows.TMSchema.Part;
import com.sun.java.swing.plaf.windows.TMSchema.Prop;
import com.sun.java.swing.plaf.windows.XPStyle.Skin;
import sun.awt.AppContext;
/**
* A class to help mimic Vista theme animations. The only kind of
* animation it handles for now is 'transition' animation (this seems
* to be the only animation which Vista theme can do). This is when
* one picture fadein over another one in some period of time.
* According to
* https://connect.microsoft.com/feedback/ViewFeedback.aspx?FeedbackID=86852&SiteID=4
* The animations are all linear.
*
* This class has a number of responsibilities.
* <ul>
* <li> It trigger rapaint for the UI components involved in the animation
* <li> It tracks the animation state for every UI component involved in the
* animation and paints {@code Skin} in new {@code State} over the
* {@code Skin} in last {@code State} using
* {@code AlphaComposite.SrcOver.derive(alpha)} where {code alpha}
* depends on the state of animation
* </ul>
*
* @author Igor Kushnirskiy
*/
class AnimationController implements ActionListener, PropertyChangeListener {
private final static boolean VISTA_ANIMATION_DISABLED =
AccessController.doPrivileged(new GetBooleanAction("swing.disablevistaanimation"));
private final static Object ANIMATION_CONTROLLER_KEY =
new StringBuilder("ANIMATION_CONTROLLER_KEY");
private final Map<JComponent, Map<Part, AnimationState>> animationStateMap =
new WeakHashMap<JComponent, Map<Part, AnimationState>>();
//this timer is used to cause repaint on animated components
//30 repaints per second should give smooth animation affect
private final javax.swing.Timer timer =
new javax.swing.Timer(1000/30, this);
private static synchronized AnimationController getAnimationController() {
AppContext appContext = AppContext.getAppContext();
Object obj = appContext.get(ANIMATION_CONTROLLER_KEY);
if (obj == null) {
obj = new AnimationController();
appContext.put(ANIMATION_CONTROLLER_KEY, obj);
}
return (AnimationController) obj;
}
private AnimationController() {
timer.setRepeats(true);
timer.setCoalesce(true);
//we need to dispose the controller on l&f change
UIManager.addPropertyChangeListener(this);
}
private static void triggerAnimation(JComponent c,
Part part, State newState) {
if (c instanceof javax.swing.JTabbedPane
|| part == Part.TP_BUTTON) {
//idk: we can not handle tabs animation because
//the same (component,part) is used to handle all the tabs
//and we can not track the states
//Vista theme might have transition duration for toolbar buttons
//but native application does not seem to animate them
return;
}
AnimationController controller =
AnimationController.getAnimationController();
State oldState = controller.getState(c, part);
if (oldState != newState) {
controller.putState(c, part, newState);
if (newState == State.DEFAULTED) {
// it seems for DEFAULTED button state Vista does animation from
// HOT
oldState = State.HOT;
}
if (oldState != null) {
long duration;
if (newState == State.DEFAULTED) {
//Only button might have DEFAULTED state
//idk: do not know how to get the value from Vista
//one second seems plausible value
duration = 1000;
} else {
duration = XPStyle.getXP().getThemeTransitionDuration(
c, part,
normalizeState(oldState),
normalizeState(newState),
Prop.TRANSITIONDURATIONS);
}
controller.startAnimation(c, part, oldState, newState, duration);
}
}
}
// for scrollbar up, down, left and right button pictures are
// defined by states. It seems that theme has duration defined
// only for up button states thus we doing this translation here.
private static State normalizeState(State state) {
State rv;
switch (state) {
case DOWNPRESSED:
/* falls through */
case LEFTPRESSED:
/* falls through */
case RIGHTPRESSED:
rv = UPPRESSED;
break;
case DOWNDISABLED:
/* falls through */
case LEFTDISABLED:
/* falls through */
case RIGHTDISABLED:
rv = UPDISABLED;
break;
case DOWNHOT:
/* falls through */
case LEFTHOT:
/* falls through */
case RIGHTHOT:
rv = UPHOT;
break;
case DOWNNORMAL:
/* falls through */
case LEFTNORMAL:
/* falls through */
case RIGHTNORMAL:
rv = UPNORMAL;
break;
default :
rv = state;
break;
}
return rv;
}
private synchronized State getState(JComponent component, Part part) {
State rv = null;
Object tmpObject =
component.getClientProperty(PartUIClientPropertyKey.getKey(part));
if (tmpObject instanceof State) {
rv = (State) tmpObject;
}
return rv;
}
private synchronized void putState(JComponent component, Part part,
State state) {
component.putClientProperty(PartUIClientPropertyKey.getKey(part),
state);
}
private synchronized void startAnimation(JComponent component,
Part part,
State startState,
State endState,
long millis) {
boolean isForwardAndReverse = false;
if (endState == State.DEFAULTED) {
isForwardAndReverse = true;
}
Map<Part, AnimationState> map = animationStateMap.get(component);
if (millis <= 0) {
if (map != null) {
map.remove(part);
if (map.size() == 0) {
animationStateMap.remove(component);
}
}
return;
}
if (map == null) {
map = new EnumMap<Part, AnimationState>(Part.class);
animationStateMap.put(component, map);
}
map.put(part,
new AnimationState(startState, millis, isForwardAndReverse));
if (! timer.isRunning()) {
timer.start();
}
}
static void paintSkin(JComponent component, Skin skin,
Graphics g, int dx, int dy, int dw, int dh, State state) {
if (VISTA_ANIMATION_DISABLED) {
skin.paintSkinRaw(g, dx, dy, dw, dh, state);
return;
}
triggerAnimation(component, skin.part, state);
AnimationController controller = getAnimationController();
synchronized (controller) {
AnimationState animationState = null;
Map<Part, AnimationState> map =
controller.animationStateMap.get(component);
if (map != null) {
animationState = map.get(skin.part);
}
if (animationState != null) {
animationState.paintSkin(skin, g, dx, dy, dw, dh, state);
} else {
skin.paintSkinRaw(g, dx, dy, dw, dh, state);
}
}
}
public synchronized void propertyChange(PropertyChangeEvent e) {
if ("lookAndFeel" == e.getPropertyName()
&& ! (e.getNewValue() instanceof WindowsLookAndFeel) ) {
dispose();
}
}
public synchronized void actionPerformed(ActionEvent e) {
java.util.List<JComponent> componentsToRemove = null;
java.util.List<Part> partsToRemove = null;
for (JComponent component : animationStateMap.keySet()) {
component.repaint();
if (partsToRemove != null) {
partsToRemove.clear();
}
Map<Part, AnimationState> map = animationStateMap.get(component);
if (! component.isShowing()
|| map == null
|| map.size() == 0) {
if (componentsToRemove == null) {
componentsToRemove = new ArrayList<JComponent>();
}
componentsToRemove.add(component);
continue;
}
for (Part part : map.keySet()) {
if (map.get(part).isDone()) {
if (partsToRemove == null) {
partsToRemove = new ArrayList<Part>();
}
partsToRemove.add(part);
}
}
if (partsToRemove != null) {
if (partsToRemove.size() == map.size()) {
//animation is done for the component
if (componentsToRemove == null) {
componentsToRemove = new ArrayList<JComponent>();
}
componentsToRemove.add(component);
} else {
for (Part part : partsToRemove) {
map.remove(part);
}
}
}
}
if (componentsToRemove != null) {
for (JComponent component : componentsToRemove) {
animationStateMap.remove(component);
}
}
if (animationStateMap.size() == 0) {
timer.stop();
}
}
private synchronized void dispose() {
timer.stop();
UIManager.removePropertyChangeListener(this);
synchronized (AnimationController.class) {
AppContext.getAppContext()
.put(ANIMATION_CONTROLLER_KEY, null);
}
}
private static class AnimationState {
private final State startState;
//animation duration in nanoseconds
private final long duration;
//animatin start time in nanoseconds
private long startTime;
//direction the alpha value is changing
//forward - from 0 to 1
//!forward - from 1 to 0
private boolean isForward = true;
//if isForwardAndReverse the animation continually goes
//forward and reverse. alpha value is changing from 0 to 1 then
//from 1 to 0 and so forth
private boolean isForwardAndReverse;
private float progress;
AnimationState(final State startState,
final long milliseconds,
boolean isForwardAndReverse) {
assert startState != null && milliseconds > 0;
assert SwingUtilities.isEventDispatchThread();
this.startState = startState;
this.duration = milliseconds * 1000000;
this.startTime = System.nanoTime();
this.isForwardAndReverse = isForwardAndReverse;
progress = 0f;
}
private void updateProgress() {
assert SwingUtilities.isEventDispatchThread();
if (isDone()) {
return;
}
long currentTime = System.nanoTime();
progress = ((float) (currentTime - startTime))
/ duration;
progress = Math.max(progress, 0); //in case time was reset
if (progress >= 1) {
progress = 1;
if (isForwardAndReverse) {
startTime = currentTime;
progress = 0;
isForward = ! isForward;
}
}
}
void paintSkin(Skin skin, Graphics _g,
int dx, int dy, int dw, int dh, State state) {
assert SwingUtilities.isEventDispatchThread();
updateProgress();
if (! isDone()) {
Graphics2D g = (Graphics2D) _g.create();
skin.paintSkinRaw(g, dx, dy, dw, dh, startState);
float alpha;
if (isForward) {
alpha = progress;
} else {
alpha = 1 - progress;
}
g.setComposite(AlphaComposite.SrcOver.derive(alpha));
skin.paintSkinRaw(g, dx, dy, dw, dh, state);
g.dispose();
} else {
skin.paintSkinRaw(_g, dx, dy, dw, dh, state);
}
}
boolean isDone() {
assert SwingUtilities.isEventDispatchThread();
return progress >= 1;
}
}
private static class PartUIClientPropertyKey
implements UIClientPropertyKey {
private static final Map<Part, PartUIClientPropertyKey> map =
new EnumMap<Part, PartUIClientPropertyKey>(Part.class);
static synchronized PartUIClientPropertyKey getKey(Part part) {
PartUIClientPropertyKey rv = map.get(part);
if (rv == null) {
rv = new PartUIClientPropertyKey(part);
map.put(part, rv);
}
return rv;
}
private final Part part;
private PartUIClientPropertyKey(Part part) {
this.part = part;
}
public String toString() {
return part.toString();
}
}
}