package org.vaadin.touchmenu.client.ui;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import com.google.gwt.animation.client.Animation;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.terminal.gwt.client.ApplicationConnection;
import com.vaadin.terminal.gwt.client.Paintable;
import com.vaadin.terminal.gwt.client.UIDL;
public class VTouchMenu extends Widget implements Paintable {
private static final String BASE_NAME = "touchmenu";
/** Set the CSS class name to allow styling. */
public static final String CLASSNAME = "v-" + BASE_NAME;
public static final String LEFT_NAVI = "v-left";
public static final String RIGHT_NAVI = "v-right";
/** The client side widget identifier */
protected String paintableId;
protected final VTouchMenu hostReference = this;
/** Reference to the server connection object. */
ApplicationConnection client;
private Element left, right;
private int width, height, areaWidth;
private int down;
protected List<VMenuButton> items;
private Element touchArea;
private int rows = 2;
private int columns = 3;
private int requestedColumns = columns;
private int requestedRows = rows;
private int selected, distance;
private boolean from = true;
private boolean pressed = false;
private boolean animating = false;
private boolean animate = true;
private int animationTime = 500;
private int softAnimationTime = 250;
private int firstItemLeft = 0;
private boolean useArrows = true;
private boolean wpercent = false;
private boolean hpercent = false;
/**
* The constructor should first call super() to initialize the component and
* then handle any initialization relevant to Vaadin.
*/
public VTouchMenu() {
super();
items = new LinkedList<VMenuButton>();
// Create base element
setElement(DOM.createDiv());
sinkEvents(Event.MOUSEEVENTS);
// If window is resized check width/height and positions
Window.addResizeHandler(resize);
// This method call of the Paintable interface sets the component
// style name in DOM tree
setStyleName(CLASSNAME);
// Create directional buttons left & right
// Set Default style properties/sizes
left = DOM.createButton();
right = DOM.createButton();
left.setClassName(LEFT_NAVI);
left.getStyle().setProperty("position", "absolute");
left.getStyle().setPropertyPx("width", 40);
right.setClassName(RIGHT_NAVI);
right.getStyle().setProperty("position", "absolute");
right.getStyle().setPropertyPx("width", 40);
// At start selected == 0 and direction == from
setTransparent(left);
touchArea = DOM.createDiv();
touchArea.setClassName("v-toucharea");
touchArea.getStyle().setProperty("position", "absolute");
getElement().appendChild(left);
getElement().appendChild(touchArea);
getElement().appendChild(right);
}
/**
* Called whenever an update is received from the server
*/
public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
// This call should be made first.
// It handles sizes, captions, tooltips, etc. automatically.
if (client.updateComponent(this, uidl, true)) {
// If client.updateComponent returns true there has been no changes
// and we do not need to update anything.
return;
}
// Save reference to server connection object to be able to send
// user interaction later
this.client = client;
// Save the client side identifier (paintable id) for the widget
paintableId = uidl.getId();
// Get all Child UIDLs and iterate through them
Iterator<Object> childUIDL = uidl.getChildIterator();
while (childUIDL.hasNext()) {
UIDL child = (UIDL) childUIDL.next();
if (child.getTag().equals("options")) {
width = child.getIntAttribute("width");
height = child.getIntAttribute("height");
// Percentual width
if (child.hasAttribute("widthpercentage")) {
width = getElement().getParentElement().getClientWidth();
wpercent = true;
} else {
wpercent = false;
}
// Percentual height
if (child.hasAttribute("heightpercentage")) {
height = getElement().getParentElement().getClientHeight();
hpercent = true;
} else {
hpercent = false;
}
if (child.hasAttribute("rows")) {
setRows(child.getIntAttribute("rows"));
}
if (child.hasAttribute("columns")) {
setColumns(child.getIntAttribute("columns"));
}
if (child.hasAttribute("moveFrom")) {
setDirection(child.getBooleanAttribute("moveFrom"));
}
if (child.hasAttribute("animate")) {
animate = child.getBooleanAttribute("animate");
}
if (child.hasAttribute("selected")) {
selected = child.getIntAttribute("selected");
}
if (child.hasAttribute("useArrows")) {
useArrows = child.getBooleanAttribute("useArrows");
if (!useArrows) {
if (getElement().isOrHasChild(left)) {
getElement().removeChild(left);
}
if (getElement().isOrHasChild(right)) {
getElement().removeChild(right);
}
} else {
if (!getElement().isOrHasChild(left)) {
getElement().appendChild(left);
}
if (!getElement().isOrHasChild(right)) {
getElement().appendChild(right);
}
}
}
// Set left/right arrow styles
if (child.hasAttribute("leftArrow")) {
left.setClassName(child.getStringAttribute("leftArrow"));
}
if (child.hasAttribute("rightArrow")) {
right.setClassName(child.getStringAttribute("rightArrow"));
}
} else if (child.getTag().equals("item")) {
UIDL item = child;
VMenuButton current = null;
String itemCaption = item.getStringAttribute("caption");
final int itemId = item.getIntAttribute("id");
boolean found = false;
for (VMenuButton button : items) {
if (button.getId() == itemId) {
found = true;
}
}
if (!found) {
final Command cmd;
if (item.hasAttribute("command")) {
cmd = new Command() {
public void execute() {
hostReference.onMenuClick(itemId);
}
};
} else {
cmd = null;
}
String icon = null;
if (item.hasAttribute("icon")) {
icon = client.translateVaadinUri(item
.getStringAttribute("icon"));
}
current = addMenuItem(itemCaption, icon, cmd, itemId);
if (item.hasAttribute("style")) {
String styleName = item.getStringAttribute("style");
current.setStyle(styleName);
}
current.setSize(item.getStringAttribute("buttonsize"));
}
}
if (child.getTag().equals("updateItem")) {
// Update item data
UIDL item = child;
VMenuButton current = null;
String itemCaption = item.getStringAttribute("caption");
final int itemId = item.getIntAttribute("id");
for (VMenuButton button : items) {
if (button.getId() == itemId) {
current = button;
break;
}
}
current.setCaption(itemCaption);
if (item.hasAttribute("icon")) {
current.setIcon(client.translateVaadinUri(item
.getStringAttribute("icon")));
}
if (item.hasAttribute("style")) {
current.setStyle(item.getStringAttribute("style"));
}
if (item.hasAttribute("buttonsize")) {
current.setSize(item.getStringAttribute("buttonsize"));
}
} else if (child.getTag().equals("removeItem")) {
removeButton(child.getIntAttribute("id"));
}
}
checkSize();
// Set touch area size depending on arrows or not
if (useArrows) {
left.getStyle().setPropertyPx("left", 0);
left.getStyle().setPropertyPx("height", height);
touchArea.getStyle().setPropertyPx("left", 40);
right.getStyle().setPropertyPx("left", width - 40);
right.getStyle().setPropertyPx("height", height);
} else {
touchArea.getStyle().setPropertyPx("left", 0);
}
setColumns(requestedColumns);
setSize();
// Set base element width and height
getElement().getStyle().setPropertyPx("width", width);
getElement().getStyle().setPropertyPx("height", height);
}
/**
* Checks size according to content
*/
private void checkSize() {
// If width == 0 or is undefined -1 then get size
// according to content
if (width <= 0) {
width = requestedColumns * items.get(0).WIDTH + 20;
if (useArrows) {
width += 80;
}
}
// If height == 0 or is undefined -1 then get size
// according to content
if (height <= 0) {
height = requestedRows * items.get(0).HEIGHT + 20;
}
if (useArrows) {
areaWidth = width - 80;
} else {
areaWidth = width;
}
}
@Override
public void onBrowserEvent(Event event) {
if (paintableId == null || client == null || animating) {
return;
}
switch (DOM.eventGetType(event)) {
case Event.ONMOUSEDOWN: {
// prevent default events
event.preventDefault();
// Get X position for mouse down
down = event.getClientX();
pressed = true;
break;
}
case Event.ONMOUSEMOVE: {
// prevent default events
event.preventDefault();
// If mouse pressed and animate on update item positions
if (pressed) {
moveButtons(event.getClientX() - down);
}
break;
}
case Event.ONMOUSEOUT: {
if (pressed) {
event.preventDefault();
int amount = Math.abs(event.getClientX() - down);
// If user dragged more than button width
if (amount > items.get(0).WIDTH) {
setSelectedAfterDragging(event, amount);
softPositioning();
} else {
moveButtons(0);
}
pressed = false;
}
break;
}
case Event.ONMOUSEUP: {
pressed = false;
if (event.getButton() == NativeEvent.BUTTON_LEFT) {
event.preventDefault();
int amount = Math.abs(event.getClientX() - down);
// If user dragged more than button width
if (amount > items.get(0).WIDTH) {
setSelectedAfterDragging(event, amount);
softPositioning();
break;
} else {
// Set positions for items
for (VMenuButton button : items) {
button.setLeft(button.getLeft() + event.getClientX()
- down);
}
softPositioning();
}
Element target = (Element) Element.as(event.getEventTarget());
if (left.equals(target)) {
moveLeft();
} else if (right.equals(target)) {
moveRight();
} else {
// Else check if a button (or button icon/caption)
// was clicked
for (VMenuButton item : items) {
if (item.getElement().equals(target)
|| item.getElement().equals(
target.getParentElement())) {
itemClick(item);
}
}
}
}
break;
}
}// End switch
}
/**
* Set selected item after drag event
*
* @param event
* Event
* @param amount
* Absolute px moved
*/
private void setSelectedAfterDragging(Event event, int amount) {
int modifier = (amount / distance);
if (modifier < 1) {
modifier = 1;
}
// Set positions for items
for (VMenuButton button : items) {
button.setLeft(button.getLeft() + event.getClientX() - down);
}
if (event.getClientX() < down) {
// if we are in last position return
if ((selected + (rows * columns)) >= items.size()) {
softPositioning();
return;
}
// set new position
selected += (rows * modifier);
if ((selected + (rows * columns)) >= items.size()) {
selected = (items.size() - (rows * columns));
if (selected % rows != 0) {
selected += selected % rows;
}
}
} else {
// if at first position return
if (selected == 0) {
softPositioning();
return;
}
// set new position
selected -= (rows * modifier);
if (selected < rows) {
selected = 0;
}
}
}
/**
* Animate movement for buttons from place dropped after dragging to
* "static" position
*/
private void softPositioning() {
// Calculate the position of the first button and how far it is from
// this position
int columnMargine = (int) Math.ceil(((areaWidth / columns) - items
.get(0).WIDTH) / 2.0);
int step = 2 * columnMargine + items.get(0).WIDTH;
firstItemLeft = columnMargine
- (int) (Math.ceil(selected / rows) * step);
Animation animation = new Animation() {
int move = (firstItemLeft - items.get(0).getLeft());
@Override
protected void onUpdate(double progress) {
moveButtons((int) (move * progress));
if (progress >= 1) {
animating = false;
setSize();
client
.updateVariable(paintableId, "cursor", selected,
true);
}
}
};
// softAnimationTime = 250 * (Math.abs(firstItemLeft
// - items.get(0).getLeft()) / 100);
animating = true;
animation.run(softAnimationTime);
}
/**
* Send clicked item id to server
*
* @param id
* Clicked button
*/
public void onMenuClick(int id) {
if (paintableId != null && client != null) {
client.updateVariable(paintableId, "clickedId", id, true);
}
}
// Enqueue Command to be fired after all current events have been handled.
public void itemClick(VMenuButton item) {
if (item.getCommand() != null) {
DeferredCommand.addCommand(item.getCommand());
}
}
/**
* Set number of columns. Min 1.
*
* @param columns
*/
public void setColumns(int columns) {
if (columns < 1) {
columns = 1;
} else {
this.columns = columns;
}
requestedColumns = columns;
}
/**
* Set number of rows. Min 1.
*
* @param rows
*/
public void setRows(int rows) {
if (rows < 1) {
rows = 1;
} else {
this.rows = rows;
}
requestedRows = rows;
}
/**
* Check that columns fit inside touch area and calculate animation time
* based on how far on step needs to move
*/
private void checkColumns() {
if (requestedColumns != columns) {
columns = requestedColumns;
} else {
requestedColumns = columns;
}
if ((columns * items.get(0).WIDTH) > touchArea.getClientWidth()) {
columns = touchArea.getClientWidth() / items.get(0).WIDTH;
}
animationTime = 250 * ((areaWidth / columns) / 100);
}
/**
* Check that rows fit into area
*/
private void checkRows() {
if (requestedRows != rows) {
rows = requestedRows;
} else {
requestedRows = rows;
}
if ((rows * items.get(0).HEIGHT) > touchArea.getClientHeight()) {
rows = touchArea.getClientHeight() / items.get(0).HEIGHT;
}
}
/**
* Set direction for scroll buttons
*
* @param from
*/
public void setDirection(boolean from) {
this.from = from;
}
/**
* Animate moving one item in from the left
*/
private void animateFromLeft() {
Animation animation = new Animation() {
int move = (areaWidth / columns);
@Override
protected void onUpdate(double progress) {
moveButtons((int) (move * progress));
if (progress >= 1) {
animating = false;
selected -= rows;
if (selected < rows) {
selected = 0;
}
setSize();
client
.updateVariable(paintableId, "cursor", selected,
true);
}
}
};
animating = true;
animation.run(animationTime);
}
/**
* Animate moving one item in from the right
*/
private void animateFromRight() {
Animation animation = new Animation() {
int move = (areaWidth / columns);
@Override
protected void onUpdate(double progress) {
moveButtons(-(int) (move * progress));
if (progress >= 1) {
animating = false;
selected += rows;
setSize();
client
.updateVariable(paintableId, "cursor", selected,
true);
}
}
};
animating = true;
animation.run(animationTime);
}
/**
* Move in buttons from left(default)
*/
public void moveLeft() {
if (from) {
if (selected <= 0) {
return;
}
if (animate) {
animateFromLeft();
} else {
selected -= rows;
if (selected < rows) {
selected = 0;
}
setSize();
client.updateVariable(paintableId, "cursor", selected, true);
}
} else {
if ((selected + (rows * columns)) >= items.size()) {
return;
}
if (animate) {
animateFromRight();
} else {
selected += rows;
setSize();
client.updateVariable(paintableId, "cursor", selected, true);
}
}
}
/**
* Move in buttons from right(default)
*/
public void moveRight() {
if (from) {
if ((selected + (rows * columns)) >= items.size()) {
return;
}
if (animate) {
animateFromRight();
} else {
selected += rows;
// setSize();
client.updateVariable(paintableId, "cursor", selected, true);
}
} else {
if (selected <= 0) {
return;
}
if (animate) {
animateFromLeft();
} else {
selected -= rows;
if (selected < rows) {
selected = 0;
}
// setSize();
client.updateVariable(paintableId, "cursor", selected, true);
}
}
}
/**
* Set touchArea size and calculate button positions.
*/
private void setSize() {
touchArea.getStyle().setPropertyPx("width", areaWidth);
touchArea.getStyle().setPropertyPx("height", height);
// Check columns/rows
checkColumns();
checkRows();
// Set button positions
if (!items.isEmpty()) {
int columnMargine = (int) Math.ceil(((areaWidth / columns) - items
.get(0).WIDTH) / 2.0);
distance = ((areaWidth / columns) - (columnMargine / 2));
int step = 2 * columnMargine + items.get(0).WIDTH;
int rowMargine = (int) Math
.ceil(((height / rows) - items.get(0).HEIGHT) / 2);
int left = columnMargine
- (int) (Math.ceil(selected / rows) * step);
int top = rowMargine;
for (int i = 0; i < items.size(); i++) {
VMenuButton item = items.get(i);
// if rows filled move next button left;
if ((rows == 1 && i > 0) || (i > 0 && (i % rows) == 0)) {
left += step;
}
item.getElement().getStyle().setPropertyPx("left", left);
item.getElement().getStyle().setPropertyPx(
"top",
top
+ ((i % rows) * (rowMargine
+ items.get(0).HEIGHT + rowMargine)));
// Set button position to use for animation.
item.setLeft(left);
}
}
// Check if at last or first item and set transparent accordingly
if (selected == 0) {
if (from) {
setTransparent(left);
} else {
setTransparent(right);
}
} else {
if (from) {
removeTransparent(left);
} else {
removeTransparent(right);
}
}
if ((selected + (rows * columns)) >= items.size()) {
if (from) {
setTransparent(right);
} else {
setTransparent(left);
}
} else {
if (from) {
removeTransparent(right);
} else {
removeTransparent(left);
}
}
}
/**
* Move button position relative to starting position
*
* @param distance
* Amount to move
*/
private void moveButtons(int distance) {
for (VMenuButton item : items) {
item.getElement().getStyle().setPropertyPx("left",
item.getLeft() + distance);
}
}
/**
* Remove a button
*
* @param id
* Id of button to remove
*/
private void removeButton(int id) {
VMenuButton target = null;
for (VMenuButton item : items) {
if (item.getId() == id) {
target = item;
break;
}
}
if (target != null) {
items.remove(target);
touchArea.removeChild(target.getElement());
}
if ((selected + (rows * columns)) >= items.size()) {
moveLeft();
}
}
/**
* Make element transparent
*
* @param target
*/
private void setTransparent(Element target) {
Style targetStyle = target.getStyle();
targetStyle.setProperty("opacity", "0.3");
targetStyle.setProperty("filter", "alpha(opacity=30)");
}
/**
* Make element opaque
*
* @param target
*/
private void removeTransparent(Element target) {
Style targetStyle = target.getStyle();
targetStyle.setProperty("opacity", "1");
targetStyle.setProperty("filter", "alpha(opacity=100)");
}
/**
* Create and add a new menu item with caption, icon and command
*
* @param caption
* Button caption
* @param image
* Button icon
* @param cmd
* Button command
* @return Created VMenuButton
*/
public VMenuButton addMenuItem(String caption, String image, Command cmd,
int id) {
VMenuButton newItem = new VMenuButton(caption, image, cmd, id);
items.add(newItem);
touchArea.appendChild(newItem.getElement());
setSize();
return newItem;
}
/**
* Handle resizing of Window
*/
private ResizeHandler resize = new ResizeHandler() {
@Override
public void onResize(ResizeEvent event) {
// if percentual value given resize else return
if (!wpercent || hpercent) {
return;
}
if (wpercent) {
width = getElement().getParentElement().getClientWidth();
}
if (hpercent) {
height = getElement().getParentElement().getClientHeight();
if (height <= 0) {
height = rows * items.get(0).HEIGHT + 20;
}
}
if (useArrows) {
areaWidth = width - 80;
// move right arrow to place
right.getStyle().setPropertyPx("left", width - 40);
} else {
areaWidth = width;
}
// set new sizes,positions,check rows & columns
setSize();
}
};
}