blob: 2c0dc4a5934fd0247ba63995d16d0804036e2c5c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.wtk.skin;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.util.Utils;
import org.apache.pivot.util.Vote;
import org.apache.pivot.wtk.CardPane;
import org.apache.pivot.wtk.CardPaneListener;
import org.apache.pivot.wtk.Component;
import org.apache.pivot.wtk.Container;
import org.apache.pivot.wtk.Dimensions;
import org.apache.pivot.wtk.Insets;
import org.apache.pivot.wtk.Orientation;
import org.apache.pivot.wtk.effects.FadeDecorator;
import org.apache.pivot.wtk.effects.ScaleDecorator;
import org.apache.pivot.wtk.effects.Transition;
import org.apache.pivot.wtk.effects.TransitionListener;
import org.apache.pivot.wtk.effects.easing.Easing;
import org.apache.pivot.wtk.effects.easing.Quartic;
/**
* Card pane skin.
*/
public class CardPaneSkin extends ContainerSkin implements CardPaneListener {
/**
* Defines the supported selection change effects.
*/
public enum SelectionChangeEffect {
CROSSFADE,
HORIZONTAL_SLIDE,
VERTICAL_SLIDE,
HORIZONTAL_FLIP,
VERTICAL_FLIP,
ZOOM
}
/**
* Abstract base class for selection change transitions.
*/
public abstract class SelectionChangeTransition extends Transition {
public final int from;
public final int to;
public final Component fromCard;
public final Component toCard;
public final int direction;
public SelectionChangeTransition(final int from, final int to) {
super(selectionChangeDuration, selectionChangeRate, false);
this.from = from;
this.to = to;
CardPane cardPane = (CardPane) getComponent();
fromCard = (from == -1) ? null : cardPane.get(from);
toCard = (to == -1) ? null : cardPane.get(to);
int length = cardPane.getLength();
if (circular && length >= 3) {
if (from == length - 1 && to == 0) {
direction = 1;
} else if (from == 0 && to == length - 1) {
direction = -1;
} else {
direction = Integer.signum(to - from);
}
} else {
direction = Integer.signum(to - from);
}
}
}
/**
* Class that performs cross-fade selection change transitions.
*/
public class CrossfadeTransition extends SelectionChangeTransition {
private FadeDecorator fadeOutDecorator = new FadeDecorator();
private FadeDecorator fadeInDecorator = new FadeDecorator();
public CrossfadeTransition(final int from, final int to) {
super(from, to);
}
@Override
public void start(final TransitionListener transitionListener) {
if (fromCard != null) {
fromCard.getDecorators().add(fadeOutDecorator);
}
if (toCard != null) {
toCard.getDecorators().add(fadeInDecorator);
toCard.setVisible(true);
}
super.start(transitionListener);
}
@Override
public void stop() {
super.stop();
if (fromCard != null) {
fromCard.getDecorators().remove(fadeOutDecorator);
fromCard.setVisible(false);
}
if (toCard != null) {
toCard.getDecorators().remove(fadeInDecorator);
}
}
@Override
protected void update() {
float percentComplete = getPercentComplete();
fadeOutDecorator.setOpacity(1.0f - percentComplete);
fadeInDecorator.setOpacity(percentComplete);
if (sizeToSelection) {
invalidateComponent();
} else {
repaintComponent();
}
}
}
/**
* Class that performs slide selection change transitions.
*/
public class SlideTransition extends SelectionChangeTransition {
private Easing slideEasing = new Quartic();
public SlideTransition(final int from, final int to) {
super(from, to);
}
@Override
public void start(final TransitionListener transitionListener) {
toCard.setVisible(true);
super.start(transitionListener);
}
@Override
public void stop() {
fromCard.setVisible(false);
super.stop();
}
@Override
protected void update() {
int width = getWidth();
int height = getHeight();
float percentComplete = slideEasing.easeOut(getElapsedTime(), 0, 1, getDuration());
int dx = (int) (width * percentComplete) * -direction;
int dy = (int) (height * percentComplete) * -direction;
if (selectionChangeEffect == SelectionChangeEffect.HORIZONTAL_SLIDE) {
fromCard.setLocation(padding.left + dx, padding.top);
toCard.setLocation(padding.left - (width * -direction) + dx, padding.top);
} else {
fromCard.setLocation(padding.left, padding.top + dy);
toCard.setLocation(padding.left, padding.top - (height * -direction) + dy);
}
}
}
/**
* Class that performs flip selection change transitions.
*/
public class FlipTransition extends SelectionChangeTransition {
private Orientation orientation;
private double theta;
private ScaleDecorator scaleDecorator = new ScaleDecorator();
public FlipTransition(final Orientation orientation, final int from, final int to) {
super(from, to);
this.orientation = orientation;
}
@Override
public void start(final TransitionListener transitionListener) {
theta = 0;
getComponent().getDecorators().add(scaleDecorator);
super.start(transitionListener);
}
@Override
public void stop() {
getComponent().getDecorators().remove(scaleDecorator);
super.stop();
}
@Override
protected void update() {
float percentComplete = getPercentComplete();
if (percentComplete < 1.0f) {
theta = Math.PI * percentComplete;
float scale = (float) Math.abs(Math.cos(theta));
if (orientation == Orientation.HORIZONTAL) {
scaleDecorator.setScale(scale, 1.0f);
} else {
scaleDecorator.setScale(1.0f, scale);
}
fromCard.setVisible(theta < Math.PI / 2);
toCard.setVisible(theta >= Math.PI / 2);
repaintComponent();
}
}
}
/**
* Class that performs zoom change transitions.
*/
public class ZoomTransition extends CrossfadeTransition {
private ScaleDecorator fromScaleDecorator = new ScaleDecorator();
private ScaleDecorator toScaleDecorator = new ScaleDecorator();
public ZoomTransition(final int from, final int to) {
super(from, to);
}
@Override
public void start(final TransitionListener transitionListener) {
if (fromCard != null) {
fromCard.getDecorators().add(fromScaleDecorator);
}
if (toCard != null) {
toCard.getDecorators().add(toScaleDecorator);
toCard.setVisible(true);
}
super.start(transitionListener);
}
@Override
public void stop() {
super.stop();
if (fromCard != null) {
fromCard.getDecorators().remove(fromScaleDecorator);
fromCard.setVisible(false);
}
if (toCard != null) {
toCard.getDecorators().remove(toScaleDecorator);
}
}
@Override
protected void update() {
float percentComplete = getPercentComplete();
if (direction == 1) {
fromScaleDecorator.setScale(1.0f + percentComplete);
toScaleDecorator.setScale(percentComplete);
} else {
fromScaleDecorator.setScale(1.0f - percentComplete);
toScaleDecorator.setScale(2.0f - percentComplete);
}
super.update();
}
}
private Insets padding = Insets.NONE;
private boolean sizeToSelection = false;
private SelectionChangeEffect selectionChangeEffect = null;
private int selectionChangeDuration = DEFAULT_SELECTION_CHANGE_DURATION;
private int selectionChangeRate = DEFAULT_SELECTION_CHANGE_RATE;
private boolean circular = false;
private SelectionChangeTransition selectionChangeTransition = null;
private static final int DEFAULT_SELECTION_CHANGE_DURATION = 250;
private static final int DEFAULT_SELECTION_CHANGE_RATE = 30;
@Override
public void install(final Component component) {
super.install(component);
CardPane cardPane = (CardPane) component;
cardPane.getCardPaneListeners().add(this);
}
@Override
public int getPreferredWidth(final int height) {
int preferredWidth = 0;
CardPane cardPane = (CardPane) getComponent();
if (sizeToSelection) {
if (selectionChangeTransition == null) {
Component selectedCard = cardPane.getSelectedCard();
if (selectedCard != null) {
preferredWidth = selectedCard.getPreferredWidth(height);
}
} else {
float percentComplete = selectionChangeTransition.getPercentComplete();
int previousWidth;
if (selectionChangeTransition.fromCard == null) {
previousWidth = 0;
} else {
previousWidth = selectionChangeTransition.fromCard.getPreferredWidth(height);
}
int width;
if (selectionChangeTransition.toCard == null) {
width = 0;
} else {
width = selectionChangeTransition.toCard.getPreferredWidth(height);
}
preferredWidth = previousWidth + (int) ((width - previousWidth) * percentComplete);
}
} else {
for (Component card : cardPane) {
preferredWidth = Math.max(preferredWidth, card.getPreferredWidth(height));
}
preferredWidth += padding.getWidth();
}
return preferredWidth;
}
@Override
public int getPreferredHeight(final int width) {
int preferredHeight = 0;
CardPane cardPane = (CardPane) getComponent();
if (sizeToSelection) {
if (selectionChangeTransition == null) {
Component selectedCard = cardPane.getSelectedCard();
if (selectedCard != null) {
preferredHeight = selectedCard.getPreferredHeight(width);
}
} else {
float percentComplete = selectionChangeTransition.getPercentComplete();
int previousHeight;
if (selectionChangeTransition.fromCard == null) {
previousHeight = 0;
} else {
previousHeight = selectionChangeTransition.fromCard.getPreferredHeight(width);
}
int height;
if (selectionChangeTransition.toCard == null) {
height = 0;
} else {
height = selectionChangeTransition.toCard.getPreferredHeight(width);
}
preferredHeight = previousHeight
+ (int) ((height - previousHeight) * percentComplete);
}
} else {
for (Component card : cardPane) {
preferredHeight = Math.max(preferredHeight, card.getPreferredHeight(width));
}
preferredHeight += padding.getHeight();
}
return preferredHeight;
}
@Override
public Dimensions getPreferredSize() {
int preferredWidth = 0;
int preferredHeight = 0;
CardPane cardPane = (CardPane) getComponent();
if (sizeToSelection) {
if (selectionChangeTransition == null) {
Component selectedCard = cardPane.getSelectedCard();
if (selectedCard != null) {
Dimensions cardSize = selectedCard.getPreferredSize();
preferredWidth = cardSize.width;
preferredHeight = cardSize.height;
}
} else {
float percentComplete = selectionChangeTransition.getPercentComplete();
int previousWidth;
int previousHeight;
if (selectionChangeTransition.fromCard == null) {
previousWidth = 0;
previousHeight = 0;
} else {
Dimensions fromSize = selectionChangeTransition.fromCard.getPreferredSize();
previousWidth = fromSize.width;
previousHeight = fromSize.height;
}
int width;
int height;
if (selectionChangeTransition.toCard == null) {
width = 0;
height = 0;
} else {
Dimensions toSize = selectionChangeTransition.toCard.getPreferredSize();
width = toSize.width;
height = toSize.height;
}
preferredWidth = previousWidth + (int) ((width - previousWidth) * percentComplete);
preferredHeight = previousHeight
+ (int) ((height - previousHeight) * percentComplete);
}
} else {
for (Component card : cardPane) {
Dimensions cardSize = card.getPreferredSize();
preferredWidth = Math.max(cardSize.width, preferredWidth);
preferredHeight = Math.max(cardSize.height, preferredHeight);
}
}
preferredWidth += padding.getWidth();
preferredHeight += padding.getHeight();
return new Dimensions(preferredWidth, preferredHeight);
}
@Override
public int getBaseline(final int width, final int height) {
int baseline = -1;
if (sizeToSelection) {
CardPane cardPane = (CardPane) getComponent();
Component selectedCard = cardPane.getSelectedCard();
if (selectedCard != null) {
int cardWidth = Math.max(width - padding.getWidth(), 0);
int cardHeight = Math.max(height - padding.getHeight(), 0);
baseline = selectedCard.getBaseline(cardWidth, cardHeight);
if (baseline != -1) {
baseline += padding.top;
}
}
}
return baseline;
}
@Override
public void layout() {
// Set the size of all components to match the size of the card pane,
// minus padding
CardPane cardPane = (CardPane) getComponent();
int width = Math.max(getWidth() - padding.getWidth(), 0);
int height = Math.max(getHeight() - padding.getHeight(), 0);
for (Component card : cardPane) {
card.setLocation(padding.left, padding.top);
card.setSize(width, height);
}
}
/**
* @return The amount of space between the edge of the CardPane and its
* content.
*/
public final Insets getPadding() {
return padding;
}
/**
* Sets the amount of space to leave between the edge of the CardPane and
* its content.
*
* @param padding The new padding values for all edges.
*/
public final void setPadding(final Insets padding) {
Utils.checkNull(padding, "padding");
this.padding = padding;
invalidateComponent();
}
/**
* Sets the amount of space to leave between the edge of the CardPane and
* its content.
*
* @param padding A dictionary with keys in the set {top, left, bottom, right}.
*/
public final void setPadding(final Dictionary<String, ?> padding) {
setPadding(new Insets(padding));
}
/**
* Sets the amount of space to leave between the edge of the CardPane and
* its content.
*
* @param padding A sequence with values in the order [top, left, bottom, right].
*/
public final void setPadding(final Sequence<?> padding) {
setPadding(new Insets(padding));
}
/**
* Sets the amount of space to leave between the edge of the CardPane and
* its content, uniformly on all four edges.
*
* @param padding The new single padding value to use for all edges.
*/
public final void setPadding(final int padding) {
setPadding(new Insets(padding));
}
/**
* Sets the amount of space to leave between the edge of the CardPane and
* its content, uniformly on all four edges.
*
* @param padding The new integer value to use for the padding on all edges.
*/
public final void setPadding(final Number padding) {
setPadding(new Insets(padding));
}
/**
* Sets the amount of space to leave between the edge of the CardPane and
* its content.
*
* @param padding A string containing an integer or a JSON dictionary with
* keys top, left, bottom, and/or right.
*/
public final void setPadding(final String padding) {
setPadding(Insets.decode(padding));
}
public boolean getSizeToSelection() {
return sizeToSelection;
}
public void setSizeToSelection(final boolean sizeToSelection) {
if (selectionChangeTransition != null) {
selectionChangeTransition.end();
}
this.sizeToSelection = sizeToSelection;
invalidateComponent();
}
public SelectionChangeEffect getSelectionChangeEffect() {
return selectionChangeEffect;
}
public void setSelectionChangeEffect(final SelectionChangeEffect selectionChangeEffect) {
if (selectionChangeTransition != null) {
selectionChangeTransition.end();
}
this.selectionChangeEffect = selectionChangeEffect;
}
public int getSelectionChangeDuration() {
return selectionChangeDuration;
}
public void setSelectionChangeDuration(final int selectionChangeDuration) {
this.selectionChangeDuration = selectionChangeDuration;
}
public int getSelectionChangeRate() {
return selectionChangeRate;
}
public void setSelectionChangeRate(final int selectionChangeRate) {
this.selectionChangeRate = selectionChangeRate;
}
/**
* Returns the circular style, which controls the direction of certain
* transitions (transitions for which a direction makes sense) when looping
* from the first index of a card pane to the last, or vice versa. When this
* style is <tt>false</tt> (the default), directional transitions will
* always appear to move forward when transitioning from a lower card index
* to a higher card index, and vice versa. When this style is <tt>true</tt>,
* directional transitions will appear to move forward when transitioning
* from the last card to the first, and backward when they transition from
* the first card to the last. <p> Note: to avoid ambiguity, the circular
* style will be ignored if the card pane has fewer than three cards.
*
* @return <tt>true</tt> if directional transitions will be circular;
* <tt>false</tt> otherwise
*/
public boolean isCircular() {
return circular;
}
/**
* Sets the circular style, which controls the direction of certain
* transitions (transitions for which a direction makes sense) when looping
* from the first index of a card pane to the last, or vice versa. When this
* style is <tt>false</tt> (the default), directional transitions will
* always appear to move forward when transitioning from a lower card index
* to a higher card index, and vice versa. When this style is <tt>true</tt>,
* directional transitions will appear to move forward when transitioning
* from the last card to the first, and backward when they transition from
* the first card to the last. <p> Note: to avoid ambiguity, the circular
* style will be ignored if the card pane has fewer than three cards.
*
* @param circular <tt>true</tt> if directional transitions should be
* circular; <tt>false</tt> otherwise
*/
public void setCircular(final boolean circular) {
this.circular = circular;
}
@Override
public void componentInserted(final Container container, final int index) {
if (selectionChangeTransition != null) {
selectionChangeTransition.end();
}
super.componentInserted(container, index);
CardPane cardPane = (CardPane) container;
Component card = cardPane.get(index);
card.setVisible(false);
if (cardPane.getLength() == 1) {
cardPane.setSelectedIndex(0);
}
invalidateComponent();
}
@Override
public void componentsRemoved(final Container container, final int index, final Sequence<Component> removed) {
if (selectionChangeTransition != null) {
selectionChangeTransition.end();
}
super.componentsRemoved(container, index, removed);
for (int i = 0, n = removed.getLength(); i < n; i++) {
Component card = removed.get(i);
card.setVisible(true);
}
invalidateComponent();
}
@Override
public Vote previewSelectedIndexChange(final CardPane cardPane, final int selectedIndex) {
Vote vote;
if (cardPane.isShowing() && selectionChangeEffect != null
&& selectionChangeTransition == null) {
int previousSelectedIndex = cardPane.getSelectedIndex();
switch (selectionChangeEffect) {
case CROSSFADE:
selectionChangeTransition = new CrossfadeTransition(previousSelectedIndex,
selectedIndex);
break;
case HORIZONTAL_SLIDE:
case VERTICAL_SLIDE:
if (previousSelectedIndex != -1 && selectedIndex != -1) {
selectionChangeTransition = new SlideTransition(previousSelectedIndex,
selectedIndex);
}
break;
case HORIZONTAL_FLIP:
if (previousSelectedIndex != -1 && selectedIndex != -1) {
selectionChangeTransition = new FlipTransition(Orientation.HORIZONTAL,
previousSelectedIndex, selectedIndex);
}
break;
case VERTICAL_FLIP:
if (previousSelectedIndex != -1 && selectedIndex != -1) {
selectionChangeTransition = new FlipTransition(Orientation.VERTICAL,
previousSelectedIndex, selectedIndex);
}
break;
case ZOOM:
if (previousSelectedIndex != -1 && selectedIndex != -1) {
selectionChangeTransition = new ZoomTransition(previousSelectedIndex,
selectedIndex);
}
break;
default:
break;
}
if (selectionChangeTransition != null) {
selectionChangeTransition.start(new TransitionListener() {
@Override
public void transitionCompleted(Transition transition) {
CardPane cardPaneLocal = (CardPane) getComponent();
SelectionChangeTransition selChangeTransitionLocal = (SelectionChangeTransition) transition;
int selectedIndexLocal = cardPaneLocal.indexOf(selChangeTransitionLocal.toCard);
cardPaneLocal.setSelectedIndex(selectedIndexLocal);
CardPaneSkin.this.selectionChangeTransition = null;
}
});
}
}
if (selectionChangeTransition == null || !selectionChangeTransition.isRunning()) {
vote = Vote.APPROVE;
} else {
vote = Vote.DEFER;
}
return vote;
}
@Override
public void selectedIndexChangeVetoed(final CardPane cardPane, final Vote reason) {
if (reason == Vote.DENY && selectionChangeTransition != null) {
// NOTE We stop, rather than end, the transition so the completion
// event isn't fired; if the event fires, the listener will set
// the selection state
selectionChangeTransition.stop();
selectionChangeTransition = null;
if (sizeToSelection) {
invalidateComponent();
}
}
}
@Override
public void selectedIndexChanged(final CardPane cardPane, final int previousSelectedIndex) {
int selectedIndex = cardPane.getSelectedIndex();
if (selectedIndex != previousSelectedIndex) {
// This was not an indirect selection change
if (selectedIndex != -1) {
Component selectedCard = cardPane.get(selectedIndex);
selectedCard.setVisible(true);
}
if (previousSelectedIndex != -1) {
Component previousSelectedCard = cardPane.get(previousSelectedIndex);
previousSelectedCard.setVisible(false);
}
if (selectedIndex == -1 || previousSelectedIndex == -1 || sizeToSelection) {
invalidateComponent();
}
}
}
}