/*
* Copyright (c) 2005-2016 Substance Kirill Grouchnikov. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of Substance Kirill Grouchnikov nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.pushingpixels.substance.internal.animation;
import java.awt.Container;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.HashMap;
import java.util.Map;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.TableCellRenderer;
import javax.swing.tree.TreeCellRenderer;
import org.pushingpixels.lafwidget.animation.AnimationConfigurationManager;
import org.pushingpixels.lafwidget.animation.AnimationFacet;
import org.pushingpixels.substance.api.*;
import org.pushingpixels.substance.api.renderers.SubstanceRenderer;
import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities;
import org.pushingpixels.trident.Timeline;
import org.pushingpixels.trident.Timeline.RepeatBehavior;
import org.pushingpixels.trident.Timeline.TimelineState;
import org.pushingpixels.trident.callback.TimelineCallback;
import org.pushingpixels.trident.callback.TimelineCallbackAdapter;
import org.pushingpixels.trident.swing.SwingRepaintCallback;
public class StateTransitionTracker {
public static interface RepaintCallback {
public TimelineCallback getRepaintCallback();
}
JComponent component;
private ButtonModel model;
private ChangeListener modelChangeListener;
private Timeline transitionTimeline;
private float transitionPosition;
/**
* Listener on the focus gain and loss.
*/
private FocusListener focusListener;
private Timeline focusTimeline;
private Timeline focusLoopTimeline;
private IconGlowTracker iconGlowTracker;
private StateTransitionTracker.RepaintCallback repaintCallback;
private boolean isAutoTrackingModelChanges;
private EventListenerList eventListenerList;
private String name;
private ModelStateInfo modelStateInfo;
public static class StateContributionInfo {
float start;
float end;
float curr;
public StateContributionInfo(float start, float end) {
this.start = start;
this.end = end;
this.curr = start;
}
public float getContribution() {
return curr;
}
void updateContribution(float timelinePosition) {
this.curr = this.start + timelinePosition * (this.end - this.start);
}
}
public static class ModelStateInfo {
private Map<ComponentState, StateContributionInfo> stateContributionMap;
private Map<ComponentState, StateContributionInfo> stateNoSelectionContributionMap;
ComponentState currState;
ComponentState currStateNoSelection;
float activeStrength;
public ModelStateInfo() {
this.stateContributionMap = new HashMap<ComponentState, StateContributionInfo>();
this.stateNoSelectionContributionMap = new HashMap<ComponentState, StateContributionInfo>();
this.activeStrength = 0.0f;
}
public ComponentState getCurrModelState() {
return currState;
}
public ComponentState getCurrModelStateNoSelection() {
return currStateNoSelection;
}
public Map<ComponentState, StateContributionInfo> getStateContributionMap() {
return this.stateContributionMap;
}
public Map<ComponentState, StateContributionInfo> getStateNoSelectionContributionMap() {
return this.stateNoSelectionContributionMap;
}
void sync() {
this.activeStrength = 0.0f;
for (Map.Entry<ComponentState, StateContributionInfo> activeEntry : this.stateContributionMap
.entrySet()) {
ComponentState activeState = activeEntry.getKey();
if (activeState.isActive()) {
this.activeStrength += activeEntry.getValue()
.getContribution();
}
}
}
float getActiveStrength() {
return this.activeStrength;
}
void clear() {
if (!SwingUtilities.isEventDispatchThread()) {
UiThreadingViolationException uiThreadingViolationError = new UiThreadingViolationException(
"State tracking must be done on Event Dispatch Thread");
uiThreadingViolationError.printStackTrace(System.err);
throw uiThreadingViolationError;
}
this.stateContributionMap.clear();
this.stateContributionMap.put(this.currState,
new StateContributionInfo(1.0f, 1.0f));
this.stateNoSelectionContributionMap.clear();
this.stateNoSelectionContributionMap.put(this.currStateNoSelection,
new StateContributionInfo(1.0f, 1.0f));
this.sync();
}
}
public StateTransitionTracker(final JComponent component, ButtonModel model) {
this.component = component;
this.model = model;
this.modelStateInfo = new ModelStateInfo();
this.modelStateInfo.currState = ComponentState.getState(model,
component);
this.modelStateInfo.currStateNoSelection = ComponentState.getState(
model, component, true);
this.modelStateInfo.clear();
this.repaintCallback = () -> new SwingRepaintCallback(component);
this.isAutoTrackingModelChanges = true;
this.eventListenerList = new EventListenerList();
this.focusTimeline = new Timeline(this.component);
AnimationConfigurationManager.getInstance().configureTimeline(
this.focusTimeline);
this.focusTimeline.addCallback(this.repaintCallback
.getRepaintCallback());
// notify listeners on focus state transition
this.focusTimeline.addCallback(new TimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(TimelineState oldState,
TimelineState newState, float durationFraction,
float timelinePosition) {
SwingUtilities.invokeLater(() -> fireFocusStateTransitionEvent(oldState, newState));
}
});
this.focusLoopTimeline = new Timeline(this.component);
AnimationConfigurationManager.getInstance().configureTimeline(
this.focusLoopTimeline);
this.focusLoopTimeline.addCallback(this.repaintCallback
.getRepaintCallback());
this.iconGlowTracker = new IconGlowTracker(this.component);
this.name = "";
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setRepaintCallback(
StateTransitionTracker.RepaintCallback repaintCallback) {
this.repaintCallback = repaintCallback;
}
public void registerFocusListeners() {
this.focusListener = new FocusListener() {
public void focusGained(FocusEvent e) {
setFocusState(true);
}
public void focusLost(FocusEvent e) {
setFocusState(false);
}
};
this.component.addFocusListener(this.focusListener);
}
public void registerModelListeners() {
this.modelChangeListener = (ChangeEvent e) -> {
if (isAutoTrackingModelChanges)
onModelStateChanged();
};
this.model.addChangeListener(this.modelChangeListener);
}
public void unregisterFocusListeners() {
this.component.removeFocusListener(this.focusListener);
this.focusListener = null;
}
public void unregisterModelListeners() {
this.model.removeChangeListener(this.modelChangeListener);
this.modelChangeListener = null;
}
public void setTransitionPosition(float transitionPosition) {
this.transitionPosition = transitionPosition;
}
public void setModel(ButtonModel model) {
this.model.removeChangeListener(this.modelChangeListener);
if (this.transitionTimeline != null) {
this.transitionTimeline.abort();
this.transitionPosition = 0.0f;
}
this.modelStateInfo.currState = ComponentState.getState(model,
component);
this.modelStateInfo.currStateNoSelection = ComponentState.getState(
model, component, true);
this.modelStateInfo.clear();
this.model = model;
this.model.addChangeListener(this.modelChangeListener);
this.component.repaint();
}
public ButtonModel getModel() {
return model;
}
public void turnOffModelChangeTracking() {
this.isAutoTrackingModelChanges = false;
}
public void onModelStateChanged() {
this.isAutoTrackingModelChanges = true;
ComponentState newState = ComponentState.getState(this.model,
this.component);
ComponentState newStateNoSelection = ComponentState.getState(
this.model, this.component, true);
boolean isInRenderer = this.component.getClass().isAnnotationPresent(
SubstanceRenderer.class);
if (!isInRenderer) {
Container parent = this.component.getParent();
while (parent != null) {
if (CellRendererPane.class.isInstance(parent)
|| ListCellRenderer.class.isInstance(parent)
|| TreeCellRenderer.class.isInstance(parent)
|| TableCellRenderer.class.isInstance(parent)) {
isInRenderer = true;
break;
}
parent = parent.getParent();
}
}
if (isInRenderer || (this.component.getParent() == null)) {
// no animations on renderers and parentless components
this.modelStateInfo.currState = newState;
this.modelStateInfo.currStateNoSelection = newStateNoSelection;
this.modelStateInfo.clear();
return;
}
// System.out.println("State changed from " + currState + " to "
// + newState);
// if (this.component instanceof JMenuItem) {
// System.out.println(((JMenuItem) this.component).getText());
// System.out.println("\tCURR:" + this.modelStateInfo.currState
// + ", NEW:" + newState);
// }
if (this.modelStateInfo.currState == newState)
return;
if (this.transitionTimeline != null) {
this.transitionTimeline.abort();
}
this.transitionTimeline = new Timeline(this);
this.transitionTimeline.setName("Model transitions");
this.transitionTimeline.addCallback(this.repaintCallback
.getRepaintCallback());
AnimationConfigurationManager.getInstance().configureTimeline(
this.transitionTimeline);
if (!this.modelStateInfo.currState
.isFacetActive(ComponentStateFacet.SELECTION)
&& newState.isFacetActive(ComponentStateFacet.SELECTION)) {
// special handling for transition from non-selected to
// selected state - make it twice faster
this.transitionTimeline.setDuration(this.transitionTimeline
.getDuration() / 2);
}
long fullDuration = this.transitionTimeline.getDuration();
if (this.modelStateInfo.stateContributionMap.containsKey(newState)) {
// Going to a state that is already partially active. The
// new timeline is going to be shorter. The new state will go to
// 1.0f, hence the transition position begins from its current
// contribution.
this.transitionPosition = this.modelStateInfo.stateContributionMap
.get(newState).getContribution();
this.transitionTimeline.addPropertyToInterpolate(
"transitionPosition", this.transitionPosition, 1.0f);
this.transitionTimeline
.setDuration((long) (fullDuration * (1.0f - this.transitionPosition)));
// if ((this.component instanceof JMenuItem)
// && "Check enabled unselected"
// .equals(((JMenuItem) this.component).getText())) {
// System.out.println("*******************************");
// System.out.println("Timeline will run from "
// + this.transitionPosition + " with state " + newState);
// for (Map.Entry<ComponentState, StateContributionInfo> existing :
// this.modelStateInfo.stateContributionMap
// .entrySet()) {
// System.out.println("\t" + existing.getKey() + " in ["
// + existing.getValue().start + ":"
// + existing.getValue().end + "]:"
// + existing.getValue().curr);
// }
// System.out.println("*******************************");
// }
} else {
this.transitionPosition = 0.0f;
this.transitionTimeline.addPropertyToInterpolate(
"transitionPosition", 0.0f, 1.0f);
// if ((this.component instanceof JMenuItem)
// && "Check enabled unselected"
// .equals(((JMenuItem) this.component).getText())) {
// System.out.println("Timeline will run fully");
// }
}
Map<ComponentState, StateContributionInfo> newContributionMap = new HashMap<ComponentState, StateContributionInfo>();
if (this.modelStateInfo.stateContributionMap.containsKey(newState)) {
// 1. the new state goes from current value to 1.0
// 2. the rest go from current value to 0.0
for (Map.Entry<ComponentState, StateContributionInfo> existing : this.modelStateInfo.stateContributionMap
.entrySet()) {
StateContributionInfo currRange = existing.getValue();
ComponentState state = existing.getKey();
float newEnd = (state == newState) ? 1.0f : 0.0f;
newContributionMap.put(state, new StateContributionInfo(
currRange.curr, newEnd));
}
} else {
// 1. all existing states go from current value to 0.0
// 2. the new state goes from 0.0 to 1.0
for (Map.Entry<ComponentState, StateContributionInfo> existing : this.modelStateInfo.stateContributionMap
.entrySet()) {
StateContributionInfo currRange = existing.getValue();
ComponentState state = existing.getKey();
newContributionMap.put(state, new StateContributionInfo(
currRange.curr, 0.0f));
}
newContributionMap.put(newState, new StateContributionInfo(0.0f,
1.0f));
}
this.modelStateInfo.stateContributionMap = newContributionMap;
Map<ComponentState, StateContributionInfo> newNoSelectionContributionMap = new HashMap<ComponentState, StateContributionInfo>();
if (this.modelStateInfo.stateNoSelectionContributionMap
.containsKey(newStateNoSelection)) {
// 1. the new state goes from current value to 1.0
// 2. the rest go from current value to 0.0
for (Map.Entry<ComponentState, StateContributionInfo> existing : this.modelStateInfo.stateNoSelectionContributionMap
.entrySet()) {
StateContributionInfo currRange = existing.getValue();
ComponentState state = existing.getKey();
float newEnd = (state == newStateNoSelection) ? 1.0f : 0.0f;
newNoSelectionContributionMap.put(state,
new StateContributionInfo(currRange.curr, newEnd));
}
} else {
// 1. all existing states go from current value to 0.0
// 2. the new state goes from 0.0 to 1.0
for (Map.Entry<ComponentState, StateContributionInfo> existing : this.modelStateInfo.stateNoSelectionContributionMap
.entrySet()) {
StateContributionInfo currRange = existing.getValue();
ComponentState state = existing.getKey();
newNoSelectionContributionMap.put(state,
new StateContributionInfo(currRange.curr, 0.0f));
}
newNoSelectionContributionMap.put(newStateNoSelection,
new StateContributionInfo(0.0f, 1.0f));
}
this.modelStateInfo.stateNoSelectionContributionMap = newNoSelectionContributionMap;
this.modelStateInfo.sync();
// if (this.component instanceof SubstanceScrollButton) {
// System.out.println("New contribution map for "
// + this.transitionTimeline.getDuration() + "ms");
// for (Map.Entry<ComponentState, StateContributionInfo> existing :
// this.modelStateInfo.stateContributionMap
// .entrySet()) {
// System.out.println("\t" + existing.getKey() + " in ["
// + existing.getValue().start + ":"
// + existing.getValue().end + "]");
// }
// }
this.transitionTimeline.addCallback(new TimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(final TimelineState oldState,
final TimelineState newState, final float durationFraction,
final float timelinePosition) {
if (newState == TimelineState.DONE) {
SwingUtilities.invokeLater(() -> {
modelStateInfo.clear();
// repaint after the model state info has
// been cleared
repaintCallback.getRepaintCallback()
.onTimelineStateChanged(oldState, newState,
durationFraction, timelinePosition);
});
}
}
});
// notify listeners on model state transition
this.transitionTimeline.addCallback(new TimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(final TimelineState oldState,
final TimelineState newState, float durationFraction,
float timelinePosition) {
SwingUtilities.invokeLater(() -> fireModelStateTransitionEvent(oldState, newState));
}
});
// Add fix for issue 297 - menu items partially covered by lightweight
// popups (such as tooltips).
this.transitionTimeline.addCallback(new TimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(TimelineState oldState,
TimelineState newState, float durationFraction,
float timelinePosition) {
SwingUtilities.invokeLater(() -> {
if (component instanceof JMenuItem) {
if (SubstanceCoreUtilities.isCoveredByLightweightPopups(component)) {
component.putClientProperty(
SubstanceCoreUtilities.IS_COVERED_BY_LIGHTWEIGHT_POPUPS,
Boolean.TRUE);
} else {
component.putClientProperty(
SubstanceCoreUtilities.IS_COVERED_BY_LIGHTWEIGHT_POPUPS,
null);
}
}
});
}
@Override
public void onTimelinePulse(float durationFraction,
float timelinePosition) {
SwingUtilities.invokeLater(() -> {
if (component instanceof JMenuItem) {
if (SubstanceCoreUtilities.isCoveredByLightweightPopups(component)) {
component.putClientProperty(
SubstanceCoreUtilities.IS_COVERED_BY_LIGHTWEIGHT_POPUPS,
Boolean.TRUE);
} else {
component.putClientProperty(
SubstanceCoreUtilities.IS_COVERED_BY_LIGHTWEIGHT_POPUPS,
null);
}
}
});
}
});
this.transitionTimeline.addCallback(new TimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(TimelineState oldState,
TimelineState newState, float durationFraction,
float timelinePosition) {
updateActiveStates(timelinePosition);
}
@Override
public void onTimelinePulse(float durationFraction,
float timelinePosition) {
updateActiveStates(timelinePosition);
}
private void updateActiveStates(final float timelinePosition) {
SwingUtilities.invokeLater(() -> {
for (StateContributionInfo pair : modelStateInfo.stateContributionMap.values()) {
pair.updateContribution(timelinePosition);
}
for (StateContributionInfo pair : modelStateInfo.stateNoSelectionContributionMap.values()) {
pair.updateContribution(timelinePosition);
}
modelStateInfo.sync();
});
}
});
this.modelStateInfo.currState = newState;
this.modelStateInfo.currStateNoSelection = newStateNoSelection;
// if (this.component instanceof JMenuItem) {
// System.out.println("Running timeline on "
// + ((JMenuItem) this.component).getText() + " for "
// + this.transitionTimeline.getDuration());
// }
this.transitionTimeline.play();
// track icon glowing
if (AnimationConfigurationManager.getInstance().isAnimationAllowed(
AnimationFacet.ICON_GLOW, this.component)) {
boolean wasRollover = false;
for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry :
this.modelStateInfo.stateContributionMap.entrySet()) {
ComponentState activeState = activeEntry.getKey();
if (activeState == this.modelStateInfo.currState)
continue;
if (activeState.isFacetActive(ComponentStateFacet.ROLLOVER)) {
wasRollover = true;
break;
}
}
boolean isRollover = this.modelStateInfo.currState
.isFacetActive(ComponentStateFacet.ROLLOVER);
if (wasRollover && !isRollover) {
this.iconGlowTracker.cancel();
}
if (!wasRollover && isRollover) {
this.iconGlowTracker.play();
}
}
}
public float getFocusStrength(boolean hasFocus) {
if (this.focusTimeline == null)
return 0.0f;
TimelineState focusTimelineState = this.focusTimeline.getState();
if ((focusTimelineState == TimelineState.READY)
|| (focusTimelineState == TimelineState.PLAYING_FORWARD)
|| (focusTimelineState == TimelineState.PLAYING_REVERSE)) {
return this.focusTimeline.getTimelinePosition();
}
return hasFocus ? 1.0f : 0.0f;
}
public float getFocusLoopPosition() {
if (this.focusLoopTimeline == null)
return 0.0f;
return this.focusLoopTimeline.getTimelinePosition();
}
public float getIconGlowPosition() {
return this.iconGlowTracker.getIconGlowPosition();
}
public float getFacetStrength(ComponentStateFacet stateFacet) {
float result = 0.0f;
for (Map.Entry<ComponentState, StateContributionInfo> activeEntry : this.modelStateInfo.stateContributionMap
.entrySet()) {
ComponentState activeState = activeEntry.getKey();
if (activeState.isFacetActive(stateFacet)) {
result += activeEntry.getValue().getContribution();
}
}
return result;
}
public float getActiveStrength() {
return this.modelStateInfo.getActiveStrength();
}
public void addStateTransitionListener(
StateTransitionListener stateTransitionListener) {
// System.out.println("Adding state listener to @" + this.hashCode());
this.eventListenerList.add(StateTransitionListener.class,
stateTransitionListener);
}
public void removeStateTransitionListener(
StateTransitionListener stateTransitionListener) {
// System.out.println("Removing state listener from @" +
// this.hashCode());
this.eventListenerList.remove(StateTransitionListener.class,
stateTransitionListener);
}
private void fireModelStateTransitionEvent(TimelineState oldState,
TimelineState newState) {
// System.out.println("Fired state event from " + oldState + " to "
// + newState + " on @" + this.hashCode());
if (this.eventListenerList.getListenerCount() == 0)
return;
StateTransitionListener[] listeners = this.eventListenerList
.getListeners(StateTransitionListener.class);
if ((listeners == null) || (listeners.length == 0))
return;
StateTransitionEvent event = new StateTransitionEvent(this, oldState,
newState);
for (StateTransitionListener listener : listeners) {
listener.onModelStateTransition(event);
}
}
private void fireFocusStateTransitionEvent(TimelineState oldState,
TimelineState newState) {
if (this.eventListenerList.getListenerCount() == 0)
return;
StateTransitionListener[] listeners = this.eventListenerList
.getListeners(StateTransitionListener.class);
if ((listeners == null) || (listeners.length == 0))
return;
StateTransitionEvent event = new StateTransitionEvent(this, oldState,
newState);
for (StateTransitionListener listener : listeners) {
listener.onFocusStateTransition(event);
}
}
public void endTransition() {
if (this.transitionTimeline != null)
this.transitionTimeline.end();
}
public void setFocusState(boolean hasFocus) {
if (hasFocus) {
this.focusTimeline.play();
if (AnimationConfigurationManager.getInstance().isAnimationAllowed(
AnimationFacet.FOCUS_LOOP_ANIMATION, this.component)) {
this.focusLoopTimeline.playLoop(RepeatBehavior.LOOP);
}
} else {
this.focusTimeline.playReverse();
if (AnimationConfigurationManager.getInstance().isAnimationAllowed(
AnimationFacet.FOCUS_LOOP_ANIMATION, this.component)) {
this.focusLoopTimeline.cancel();
}
}
}
public boolean hasRunningTimelines() {
if (this.focusTimeline != null) {
TimelineState focusTimelineState = this.focusTimeline.getState();
if (focusTimelineState != TimelineState.IDLE)
return true;
}
if (this.focusLoopTimeline != null) {
TimelineState focusLoopTimelineState = this.focusLoopTimeline
.getState();
if (focusLoopTimelineState != TimelineState.IDLE)
return true;
}
if (this.iconGlowTracker.isPlaying()) {
return true;
}
if (this.transitionTimeline != null) {
TimelineState modelTransitionTimelineState = this.transitionTimeline
.getState();
if (modelTransitionTimelineState != TimelineState.IDLE)
return true;
}
return false;
}
public IconGlowTracker getIconGlowTracker() {
return iconGlowTracker;
}
public ModelStateInfo getModelStateInfo() {
return modelStateInfo;
}
}