/*
* 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.Component;
import java.awt.Container;
import java.lang.ref.WeakReference;
import java.util.WeakHashMap;
import javax.swing.*;
import org.pushingpixels.substance.api.SubstanceLookAndFeel;
import org.pushingpixels.trident.Timeline;
import org.pushingpixels.trident.Timeline.RepeatBehavior;
import org.pushingpixels.trident.Timeline.TimelineState;
import org.pushingpixels.trident.callback.UIThreadTimelineCallbackAdapter;
/**
* Tracker for pulsating (default and focused) <code>JButton</code>s. This class
* is <b>for internal use only</b>.
*
* @author Kirill Grouchnikov
*/
public class RootPaneDefaultButtonTracker extends
UIThreadTimelineCallbackAdapter {
/**
* Map (with weakly-referenced keys) of all trackers. For each default
* button which has not been claimed by GC, we have a tracker (with
* associated <code>Timer</code>).
*/
private static WeakHashMap<JButton, RootPaneDefaultButtonTracker> trackers = new WeakHashMap<JButton, RootPaneDefaultButtonTracker>();
/**
* Waek reference to the associated button.
*/
private WeakReference<JButton> buttonRef;
/**
* The associated timeline.
*/
private Timeline timeline;
/**
* Simple constructor.
*
* @param jbutton
*/
private RootPaneDefaultButtonTracker(JButton jbutton) {
// Create weak reference.
this.buttonRef = new WeakReference<JButton>(jbutton);
// Create coalesced timer.
this.timeline = new Timeline(jbutton);
this.timeline.addCallback(this);
// Store event handler and initial cycle count.
RootPaneDefaultButtonTracker.trackers.put(jbutton, this);
}
/**
* Recursively checks whether the specified component or one of its inner
* components has focus.
*
* @param component
* Component to check.
* @return <code>true</code> if the specified component or one of its inner
* components has focus, <code>false</code> otherwise.
*/
private static boolean isInFocusedWindow(Component component) {
if (component == null) {
return false;
}
// check if the component itself is focus owner
if (component.isFocusOwner()) {
return true;
}
// recursively find if has focus owner component
if (component instanceof Container) {
for (Component comp : ((Container) component).getComponents()) {
if (isInFocusedWindow(comp)) {
return true;
}
}
}
return false;
}
/**
* Recursively checks whether the specified component has visible glass
* pane.
*
* @param component
* Component to check.
* @return <code>true</code> if the specified component has visible glass
* pane, <code>false</code> otherwise.
*/
private static boolean hasGlassPane(Component component) {
if (component == null) {
return false;
}
// check if the component has visible glass pane
Component glassPane = null;
if (component instanceof JDialog) {
glassPane = ((JDialog) component).getGlassPane();
}
if (component instanceof JFrame) {
glassPane = ((JFrame) component).getGlassPane();
}
if ((glassPane != null) && (glassPane.isVisible())) {
return true;
}
return false;
}
@Override
public void onTimelineStateChanged(TimelineState oldState,
TimelineState newState, float durationFraction,
float timelinePosition) {
this.onTimelineEvent();
}
@Override
public void onTimelinePulse(float durationFraction, float timelinePosition) {
this.onTimelineEvent();
}
void onTimelineEvent() {
// get the button and check if it wasn't GC'd
JButton jButton = this.buttonRef.get();
if (jButton == null) {
return;
}
if (!jButton.isDisplayable()) {
timeline.abort();
return;
}
if (RootPaneDefaultButtonTracker.hasGlassPane(jButton
.getTopLevelAncestor()))
return;
if (!isPulsating(jButton)) {
// has since lost its default status
RootPaneDefaultButtonTracker tracker = trackers.get(jButton);
tracker.stopTimer();
tracker.buttonRef.clear();
trackers.remove(jButton);
} else {
if (!RootPaneDefaultButtonTracker.isInFocusedWindow(jButton
.getTopLevelAncestor())) {
// && (!isAttentionDrawingCloseButton(jButton))) {
// no focus in button window - will restore original (not
// animated) painting
RootPaneDefaultButtonTracker.update(jButton);
} else {
// check if it's enabled
if (!jButton.isEnabled()) {
if (timeline.getState() != TimelineState.SUSPENDED) {
timeline.suspend();
}
} else {
if (timeline.getState() == TimelineState.SUSPENDED) {
timeline.resume();
}
}
// if (jButton.isEnabled()) {
// // increment cycle count for pulsating buttons.
// long oldCycle = cycles.get(jButton);
// if (oldCycle == 20) {
// oldCycle = isAttentionDrawingCloseButton(jButton) ? -80
// : 0;
// }
// cycles.put(jButton, oldCycle + 1);
// } else {
// // revert to 0 if it's not enabled
// if (cycles.get(jButton) != 0) {
// cycles.put(jButton, (long) 0);
// }
// }
}
}
// maybe LAF has changed
if (SubstanceLookAndFeel.isCurrentLookAndFeel())
jButton.repaint();
}
/**
* Starts the associated timer.
*/
private void startTimer() {
this.timeline.playLoop(RepeatBehavior.REVERSE);
}
/**
* Stops the associated timer.
*/
private void stopTimer() {
this.timeline.cancel();
}
/**
* Returns the status of the associated timer.
*
* @return <code>true</code> is the associated timer is running,
* <code>false</code> otherwise.
*/
private boolean isRunning() {
TimelineState state = this.timeline.getState();
return ((state == TimelineState.PLAYING_FORWARD) || (state == TimelineState.PLAYING_REVERSE));
}
/**
* Updates the state of the specified button which must be a default button
* in some window. The button state is determined based on focus ownership.
*
* @param jButton
* Button.
*/
public static void update(JButton jButton) {
if (jButton == null)
return;
boolean hasFocus = RootPaneDefaultButtonTracker
.isInFocusedWindow(jButton.getTopLevelAncestor());
RootPaneDefaultButtonTracker tracker = trackers.get(jButton);
if (!hasFocus) {
// remove
if (tracker == null) {
return;
}
// if (cycles.get(jButton) == 0) {
// return;
// }
// cycles.put(jButton, (long) 0);
// System.out.println("r::" + trackers.size());
} else {
// add
if (tracker != null) {
tracker.startTimer();
return;
}
tracker = new RootPaneDefaultButtonTracker(jButton);
tracker.startTimer();
trackers.put(jButton, tracker);
// long initialCycle = isAttentionDrawingCloseButton(jButton) ? -80
// : 0;
// cycles.put(jButton, initialCycle);
// System.out.println("a::" + trackers.size());
}
}
/**
* Retrieves the current cycle count for the specified button.
*
* @param jButton
* Button.
* @return Current cycle count for the specified button.
*/
public static float getTimelinePosition(JButton jButton) {
RootPaneDefaultButtonTracker tracker = trackers.get(jButton);
if (tracker == null)
return 0;
return tracker.timeline.getTimelinePosition();
// Long cycleCount = cycles.get(jButton);
// if (cycleCount == null) {
// return 0;
// }
// long result = cycleCount.longValue();
// if (result < 0)
// result = 0;
// return result;
}
/**
* Retrieves the animation state for the specified button.
*
* @param jButton
* Button.
* @return <code>true</code> if the specified button is being animated,
* <code>false</code> otherwise.
*/
public static boolean isAnimating(JButton jButton) {
RootPaneDefaultButtonTracker tracker = trackers.get(jButton);
if (tracker == null) {
return false;
}
return tracker.isRunning();
}
/**
* Returns memory usage.
*
* @return Memory usage string.
*/
static String getMemoryUsage() {
StringBuffer sb = new StringBuffer();
sb.append("PulseTracker: \n");
sb.append("\t" + trackers.size() + " trackers");
// + cycles.size();
// + " cycles");
return sb.toString();
}
// /**
// * Checks whether the specified button is attention-drawing
// * <code>close</code> button of some internal frame, root pane or desktop
// * icon.
// *
// * @param jButton
// * Button.
// * @return <code>true</code> if the specified button is <code>close</code>
// * button of some internal frame, root pane or desktop icon,
// * <code>false</code> otherwise.
// */
// public static boolean isAttentionDrawingCloseButton(JButton jButton) {
// if (SubstanceCoreUtilities.isTitleCloseButton(jButton)) {
// // check if have windowModified property
// Component comp = jButton;
// boolean isWindowModified = false;
// while (comp != null) {
// if (comp instanceof JInternalFrame) {
// JInternalFrame jif = (JInternalFrame) comp;
// isWindowModified = Boolean.TRUE
// .equals(jif
// .getClientProperty(SubstanceLookAndFeel.WINDOW_MODIFIED));
// break;
// }
// if (comp instanceof JRootPane) {
// JRootPane jrp = (JRootPane) comp;
// isWindowModified = Boolean.TRUE
// .equals(jrp
// .getClientProperty(SubstanceLookAndFeel.WINDOW_MODIFIED));
// break;
// }
// if (comp instanceof JDesktopIcon) {
// JDesktopIcon jdi = (JDesktopIcon) comp;
// JInternalFrame jif = jdi.getInternalFrame();
// isWindowModified = Boolean.TRUE
// .equals(jif
// .getClientProperty(SubstanceLookAndFeel.WINDOW_MODIFIED));
// break;
// }
// comp = comp.getParent();
// }
// if (isWindowModified) {
// return true;
// }
// }
// return false;
// }
/**
* Checks whether the specified button is pulsating.
*
* @param jButton
* Button.
* @return <code>true</code> if the specified button is pulsating,
* <code>false</code> otherwise.
*/
public static boolean isPulsating(JButton jButton) {
// default button pulsates.
boolean isDefault = jButton.isDefaultButton();
if (isDefault) {
return true;
}
return false;
// attention-drawing close button pulsates.
// return isAttentionDrawingCloseButton(jButton);
}
/**
* Stops all timers.
*/
public static void stopAllTimers() {
for (RootPaneDefaultButtonTracker tracker : trackers.values()) {
tracker.stopTimer();
}
}
}