/*******************************************************************************
* Copyright (c) 2013 EclipseSource and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* EclipseSource - initial API and implementation
******************************************************************************/
package com.eclipsesource.tabris.widgets.swipe;
import static com.eclipsesource.tabris.internal.Clauses.when;
import static com.eclipsesource.tabris.internal.Clauses.whenNot;
import static com.eclipsesource.tabris.internal.Clauses.whenNull;
import static com.eclipsesource.tabris.internal.Constants.METHOD_ADD;
import static com.eclipsesource.tabris.internal.Constants.METHOD_LOCK_LEFT;
import static com.eclipsesource.tabris.internal.Constants.METHOD_LOCK_RIGHT;
import static com.eclipsesource.tabris.internal.Constants.METHOD_REMOVE;
import static com.eclipsesource.tabris.internal.Constants.METHOD_UNLOCK_LEFT;
import static com.eclipsesource.tabris.internal.Constants.METHOD_UNLOCK_RIGHT;
import static com.eclipsesource.tabris.internal.Constants.PROPERTY_ACTIVE;
import static com.eclipsesource.tabris.internal.Constants.PROPERTY_CONTROL;
import static com.eclipsesource.tabris.internal.Constants.PROPERTY_INDEX;
import static com.eclipsesource.tabris.internal.Constants.PROPERTY_ITEMS;
import static com.eclipsesource.tabris.internal.Constants.PROPERTY_ITEM_COUNT;
import static com.eclipsesource.tabris.internal.Constants.PROPERTY_PARENT;
import static com.eclipsesource.tabris.internal.Constants.TYPE_SWIPE;
import static com.eclipsesource.tabris.internal.DataWhitelist.WhiteListEntry.SWIPE;
import static com.eclipsesource.tabris.internal.SwipeItemIndexer.getAsArray;
import static com.eclipsesource.tabris.internal.SwipeUtil.notifyDisposed;
import static com.eclipsesource.tabris.internal.SwipeUtil.notifyItemActivated;
import static com.eclipsesource.tabris.internal.SwipeUtil.notifyItemDeactivated;
import static com.eclipsesource.tabris.internal.SwipeUtil.notifyItemLoaded;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.rap.json.JsonObject;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.internal.lifecycle.WidgetUtil;
import org.eclipse.rap.rwt.remote.RemoteObject;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import com.eclipsesource.tabris.internal.JsonUtil;
import com.eclipsesource.tabris.internal.SwipeItemHolder;
import com.eclipsesource.tabris.internal.SwipeManager;
import com.eclipsesource.tabris.internal.SwipeOperationHandler;
import com.eclipsesource.tabris.internal.ZIndexStackLayout;
/**
* <p>
* A <code>Swipe</code> object can be used to navigate through a stack of {@link Composite} objects (represented by a
* {@link SwipeItem}using a swipe gesture. The content of a <code>Swipe</code> can be modified in a dynamic way using
* a {@link SwipeItemProvider}. To increase the user experience the content of each item can be pre-loaded.
* </p>
* <p>
* E.g. the default size of pre loaded items is 1. This means when you show item 1, item 0 an 2 will be pre loaded. The
* size of this pre loading cache is configurable and can change during runtime.
* </p>
* <p>
* It's also possible to lock item in two directions. This means when a item 1 is locked with <code>SWT.LEFT</code> a
* user is not able to swipe to item 0.
* </p>
*
* @see SwipeItemProvider
* @see SwipeListener
* @see SwipeContext
*
* @since 0.10
*/
@SuppressWarnings("restriction")
public class Swipe implements Serializable {
private final SwipeOperationHandler operationHandler;
private final Composite container;
private final List<SwipeListener> listeners;
private final RemoteObject remoteObject;
private final SwipeManager manager;
private int oldCount;
public Swipe( Composite parent, SwipeItemProvider itemProvider ) {
whenNull( parent ).throwIllegalArgument( "Parent must not be null" );
whenNull( itemProvider ).throwIllegalArgument( "SwipeItemProvider must not be null" );
this.operationHandler = new SwipeOperationHandler( this );
this.manager = new SwipeManager( itemProvider );
this.listeners = new ArrayList<SwipeListener>();
this.container = new Composite( parent, SWT.NONE );
container.setData( SWIPE.getKey(), Boolean.TRUE );
addDisposeListener();
this.remoteObject = RWT.getUISession().getConnection().createRemoteObject( TYPE_SWIPE );
initialize();
}
private void addDisposeListener() {
container.addDisposeListener( new DisposeListener() {
@Override
public void widgetDisposed( DisposeEvent event ) {
dispose();
}
} );
}
private void initialize() {
remoteObject.set( PROPERTY_PARENT, WidgetUtil.getId( container ) );
updateItemCount();
remoteObject.setHandler( operationHandler );
container.setLayout( new ZIndexStackLayout() );
if( manager.getProvider().getItemCount() > 0 ) {
show( 0 );
}
}
/**
* <p>
* Configures the amount of pre loaded items. A size of 2 means that when showing item 3, item 1, 2, 4 and 5 will
* be loaded. The cache size must be > 0.
* </p>
*
* @param size The amount of item to pre loading each direction.
* @throws IllegalArgumentException when cache size is <= 0
*/
public void setCacheSize( int size ) throws IllegalArgumentException {
verifyIsNotDisposed();
manager.getIndexer().setRange( size );
if( isValidIndex( manager.getIndexer().getCurrent() ) ) {
refresh();
}
}
/**
* <p>
* Triggers a refresh to get new input from the {@link SwipeItemProvider}. This is like calling the show method with
* the current index.
* </p>
*
* @throws IllegalStateException when the current item was removed in the {@link SwipeItemProvider}.
*/
public void refresh() throws IllegalStateException {
int current = manager.getIndexer().getCurrent();
if( isValidIndex( current ) ) {
manager.getIndexer().reset();
show( current, false );
} else {
throw new IllegalStateException( "Item at index " + current + " does not exist anymore." );
}
}
/**
* <p>
* Shows the item at the given index. Calling this method is comparable with a programmatic swiping to a given item.
* </p>
*
* @param index the index of the item to swipe to.
*
* @throws IllegalArgumentException when the passed index is negative or does not exist in the
* {@link SwipeItemProvider}.
* @throws IllegalStateException when already disposed or the move is not valid e.g. when a item is locked.
*/
public void show( int index ) throws IllegalArgumentException, IllegalStateException {
show( index, hasCurrentIndexChanged( index ) );
}
private void show( int index, boolean needsToShow ) {
verifyIsNotDisposed();
whenNot( manager.isMoveAllowed( manager.getIndexer().getCurrent(), index ) )
.throwIllegalState( "Move not allowed. Item " + index + " is locked." );
if( isValidIndex( index ) ) {
verifyLocks();
showItemAtIndex( index, needsToShow );
} else {
throw new IllegalArgumentException( "Item at index " + index + " does not exist." );
}
}
private void verifyLocks() {
removeLockIfOutOfBounds( manager.getLeftLock(), SWT.LEFT );
removeLockIfOutOfBounds( manager.getRightLock(), SWT.RIGHT );
}
private void removeLockIfOutOfBounds( int lock, int direction ) {
if( lock != -1 && !isValidIndex( lock ) ) {
unlock( direction );
}
}
private boolean isValidIndex( int index ) {
return index >= 0 && manager.getProvider().getItemCount() > index;
}
private void showItemAtIndex( int index, boolean needsToShow ) {
manager.getIndexer().setCurrent( index );
removeOutOfRangeItems();
handlePreviousItem();
if( needsToShow ) {
showCurrentItem();
}
initializeNextItem();
updateItemCount();
}
private boolean hasCurrentIndexChanged( int index ) {
boolean needToShow = false;
if( index != manager.getIndexer().getCurrent() ) {
needToShow = true;
}
return needToShow;
}
private void removeOutOfRangeItems() {
int[] outOfRangeIndexes = filterRespectingBounds( manager.getIndexer().popOutOfRangeIndexes() );
for( int index : outOfRangeIndexes ) {
if( wasActiveItem( index ) ) {
manager.getItemHolder().getItem( index ).deactivate( manager.getContext() );
}
manager.getItemHolder().removeItem( index );
}
callRemoveItems( outOfRangeIndexes );
}
private int[] filterRespectingBounds( int[] outOfRangeIndexes ) {
List<Integer> indexes = new ArrayList<Integer>();
for( int index : outOfRangeIndexes ) {
if( index < manager.getProvider().getItemCount() && manager.getItemHolder().isLoaded( index ) ) {
indexes.add( Integer.valueOf( index ) );
}
}
addLoadedOutOfBoundsItems( indexes );
return getAsArray( indexes );
}
private void addLoadedOutOfBoundsItems( List<Integer> indexes ) {
List<Integer> loadedItems = manager.getItemHolder().getLoadedItems();
for( Integer item : loadedItems ) {
if( item.intValue() > ( manager.getProvider().getItemCount() -1 ) || isOutOfRange( item ) ) {
indexes.add( item );
manager.getItemHolder().removeItem( item.intValue() );
}
}
}
private boolean isOutOfRange( Integer item ) {
return manager.getIndexer().getRange() == 0 && item.intValue() != manager.getIndexer().getCurrent();
}
private boolean wasActiveItem( int index ) {
boolean result = false;
Control topControl = ( ( ZIndexStackLayout ) container.getLayout() ).getOnTopControl();
if( manager.getItemHolder().isLoaded( index ) ) {
if( manager.getItemHolder().getContentForItem( index ).equals( topControl ) ) {
result = true;
}
}
return result;
}
private void callRemoveItems( int[] outOfRangeIndexes ) {
if( outOfRangeIndexes.length > 0 ) {
JsonObject properties = new JsonObject();
properties.add( PROPERTY_ITEMS, JsonUtil.createJsonArray( outOfRangeIndexes ) );
remoteObject.call( METHOD_REMOVE, properties );
}
}
private void handlePreviousItem() {
int[] previousItems = manager.getIndexer().getPrevious();
for( int previousItemIndex : previousItems ) {
if( manager.getProvider().getItemCount() > previousItemIndex && previousItemIndex >= 0 ) {
ensureItemExists( previousItemIndex );
preloadItem( previousItemIndex );
}
}
}
private void showCurrentItem() {
int currentIndex = manager.getIndexer().getCurrent();
ensureItemExists( currentIndex );
ensureItemIsLoaded( currentIndex );
deactivateLastActiveItem();
activateItem( currentIndex );
setOnTopControl( manager.getItemHolder().getContentForItem( currentIndex ) );
}
private void deactivateLastActiveItem() {
int oldIndex = manager.getIndexer().getOld();
if( oldIndex != -1 ) {
ensureItemExists( oldIndex );
SwipeItem item = manager.getItemHolder().getItem( oldIndex );
item.deactivate( manager.getContext() );
notifyItemDeactivated( listeners, item, oldIndex, manager.getContext() );
}
}
private void activateItem( int currentIndex ) {
SwipeItem currentItem = manager.getItemHolder().getItem( currentIndex );
currentItem.activate( manager.getContext() );
if( currentIndex != operationHandler.getActiveClientItem() ) {
remoteObject.set( PROPERTY_ACTIVE, currentIndex );
}
notifyItemActivated( listeners, currentItem, currentIndex, manager.getContext() );
}
private void setOnTopControl( Control control ) {
ZIndexStackLayout layout = ( ZIndexStackLayout )container.getLayout();
layout.setOnTopControl( control );
}
private void initializeNextItem() {
int[] nextItems = manager.getIndexer().getNext();
for( int nextItemIndex : nextItems ) {
if( manager.getProvider().getItemCount() > nextItemIndex && nextItemIndex >= 0 ) {
ensureItemExists( nextItemIndex );
preloadItem( nextItemIndex );
}
}
}
private void preloadItem( int index ) {
SwipeItem item = manager.getProvider().getItem( index );
if( item.isPreloadable() ) {
ensureItemIsLoaded( index );
}
}
private void ensureItemExists( int index ) {
if( !manager.getItemHolder().hasItem( index ) ) {
SwipeItem item = manager.getProvider().getItem( index );
manager.getItemHolder().addItem( index, item );
}
}
private void ensureItemIsLoaded( int index ) {
if( !manager.getItemHolder().isLoaded( index ) ) {
SwipeItem item = manager.getItemHolder().getItem( index );
Control content = item.load( container );
container.layout( true );
manager.getItemHolder().setContentForItem( index, content );
updateItemCount();
remoteObject.call( METHOD_ADD, createLoadProperties( index, content ) );
notifyItemLoaded( listeners, item, index );
}
}
private JsonObject createLoadProperties( int index, Control content ) {
JsonObject result = new JsonObject();
result.add( PROPERTY_INDEX, index );
result.add( PROPERTY_CONTROL, WidgetUtil.getId( content ) );
return result;
}
/**
* <p>
* Adds a {@link SwipeListener} to get notified about swipping events.
* </p>
*/
public void addSwipeListener( SwipeListener listener ) {
verifyIsNotDisposed();
listeners.add( listener );
}
/**
* <p>
* Removes the given {@link SwipeListener}.
* </p>
*/
public void removeSwipeListener( SwipeListener listener ) {
when( container.isDisposed() ).throwIllegalState( "Swipe is already disposed" );
listeners.remove( listener );
}
/**
* <p>
* Returns the underlying control. This control is the parent of all {@link SwipeItem}s. Should only be used to do
* layouting stuff.
* </p>
*/
public Control getControl() {
return container;
}
/**
* <p>
* Returns the {@link SwipeContext} that is shared during all swipping events.
* </p>
*/
public SwipeContext getContext() {
return manager.getContext();
}
/**
* <p>
* Locks the current item in the given direction. Allowed directions are SWT.LEFT and SWT.RIGHT.
* <p>
*
* @throws IllegalArgumentException when not SWT.LEFT or SWT.RIGHT
*/
public void lock( int direction ) throws IllegalArgumentException {
when( direction != SWT.LEFT && direction != SWT.RIGHT )
.throwIllegalArgument( "Invalid lock direction. Only SWT.LEFT and SWT.RIGHT are supported." );
int indexToLock = manager.getIndexer().getCurrent();
manager.lock( direction, indexToLock, true );
String method = direction == SWT.LEFT ? METHOD_LOCK_LEFT : METHOD_LOCK_RIGHT;
remoteObject.call( method, createLockProperties( direction, indexToLock ) );
updateItemCount();
}
/**
* <p>
* Unlocks the current item in the given direction. Allowed directions are SWT.LEFT and SWT.RIGHT.
* <p>
*
* @throws IllegalArgumentException when not SWT.LEFT or SWT.RIGHT
*/
public void unlock( int direction ) throws IllegalArgumentException {
when( direction != SWT.LEFT && direction != SWT.RIGHT )
.throwIllegalArgument( "Invalid lock direction. Only SWT.LEFT and SWT.RIGHT are supported." );
manager.unlock( direction );
String method = direction == SWT.LEFT ? METHOD_UNLOCK_LEFT : METHOD_UNLOCK_RIGHT;
remoteObject.call( method, null );
updateItemCount();
}
private JsonObject createLockProperties( int direction, int index ) {
JsonObject properties = new JsonObject();
properties.add( PROPERTY_INDEX, index );
return properties;
}
private void verifyIsNotDisposed() {
when( container.isDisposed() ).throwIllegalState( "Swipe is already disposed" );
}
private void updateItemCount() {
int newCount = manager.getProvider().getItemCount();
if( newCount != oldCount ) {
oldCount = newCount;
remoteObject.set( PROPERTY_ITEM_COUNT, newCount );
}
}
/**
* <p>
* Disposes the complete {@link Swipe} object and all it's children.
* </p>
*/
public void dispose() {
manager.getItemHolder().removeAllItems();
if( !container.isDisposed() ) {
container.dispose();
}
notifyDisposed( listeners, manager.getContext() );
remoteObject.destroy();
}
SwipeItemHolder getItemHolder() {
return manager.getItemHolder();
}
Composite getContainer() {
return container;
}
}