/*
* Copyright 2015 Tomi Virtanen
*
* Licensed 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.tltv.gantt.client;
import static org.tltv.gantt.client.SvgUtil.createSVGElementNS;
import static org.tltv.gantt.client.SvgUtil.setAttributeNS;
import static org.tltv.gantt.client.shared.GanttUtil.getTouchOrMouseClientX;
import static org.tltv.gantt.client.shared.GanttUtil.getTouchOrMouseClientY;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.TouchCancelEvent;
import com.google.gwt.event.dom.client.TouchCancelHandler;
import com.google.gwt.event.dom.client.TouchMoveEvent;
import com.google.gwt.event.dom.client.TouchMoveHandler;
import com.google.gwt.event.dom.client.TouchStartEvent;
import com.google.gwt.event.dom.client.TouchStartHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.touch.client.Point;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.event.PointerCancelEvent;
import com.vaadin.client.event.PointerCancelHandler;
import com.vaadin.client.event.PointerDownEvent;
import com.vaadin.client.event.PointerDownHandler;
import com.vaadin.client.event.PointerMoveEvent;
import com.vaadin.client.event.PointerMoveHandler;
import com.vaadin.client.event.PointerUpEvent;
import com.vaadin.client.event.PointerUpHandler;
/** SVG implementation of {@link ArrowElement} arrow between two elements. */
public class SvgArrowWidget extends Widget implements ArrowElement {
public static final String SELECTION_STYLE_NAME = "select-target-step";
private static final int POINTER_TOUCH_DETECTION_INTERVAL = 100;
protected boolean readOnly;
protected Element curve;
protected Element startingPoint;
protected Element endingPoint;
protected Element movePointElement = DOM.createDiv();
protected int width = 0;
protected int height = 0;
protected int margin = 8; // px
protected ArrowPositionData originalData;
protected int originalWidth;
protected int originalHeight;
protected HandlerRegistration moveRegisteration;
protected HandlerRegistration touchCancelRegisteration;
protected ArrowPositionData movingData;
protected Point capturePoint;
protected int capturePointScrollTop = 0;
protected int capturePointScrollLeft = 0;
protected ArrowChangeHandler handler;
protected boolean selectPredecessorMode = false;
protected boolean selectFollowerMode = false;
protected Element captureElement = null;
protected int currentPointerEventId = -1;
protected NativeEvent pendingPointerDownEvent;
protected Point pointerDownPoint;
protected Timer pointerTouchStartedTimer = new Timer() {
@Override
public void run() {
handleDownEvent(pendingPointerDownEvent);
pendingPointerDownEvent = null;
}
};
protected PointerDownHandler pointerDownHandler = new PointerDownHandler() {
@Override
public void onPointerDown(PointerDownEvent event) {
GWT.log("Starting point touched (pointerDown)!");
if (currentPointerEventId == -1) {
currentPointerEventId = event.getPointerId();
} else {
event.preventDefault();
return; // multi-touch not supported
}
pointerDownPoint = new Point(
getTouchOrMouseClientX(event.getNativeEvent()),
getTouchOrMouseClientY(event.getNativeEvent()));
pendingPointerDownEvent = event.getNativeEvent();
pointerTouchStartedTimer.schedule(POINTER_TOUCH_DETECTION_INTERVAL);
}
};
protected PointerUpHandler pointerUpHandler = new PointerUpHandler() {
@Override
public void onPointerUp(PointerUpEvent event) {
pointerTouchStartedTimer.cancel();
currentPointerEventId = -1;
event.preventDefault();
}
};
protected TouchStartHandler touchStartHandler = new TouchStartHandler() {
@Override
public void onTouchStart(TouchStartEvent event) {
GWT.log("Starting point touched (touchDown)!");
handleDownEvent(event.getNativeEvent());
}
};
protected MouseDownHandler mouseDownHandler = new MouseDownHandler() {
@Override
public void onMouseDown(MouseDownEvent event) {
if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
GWT.log("Starting point Clicked!");
handleDownEvent(event.getNativeEvent());
}
}
};
protected MouseMoveHandler mouseMoveHandler = new MouseMoveHandler() {
@Override
public void onMouseMove(MouseMoveEvent event) {
GWT.log("SvgArrowWidget.onMouseMove(MouseMoveEvent)");
handleMove(event.getNativeEvent());
}
};
protected PointerMoveHandler pointerMoveHandler = new PointerMoveHandler() {
@Override
public void onPointerMove(PointerMoveEvent event) {
GWT.log("SvgArrowWidget.onPointerMove(PointerMoveEvent)");
if (pointerDownPoint == null) {
return;
}
// do nothing, if touch position has not changed
if (!(pointerDownPoint.getX() == getTouchOrMouseClientX(event
.getNativeEvent()) && pointerDownPoint.getY() == getTouchOrMouseClientY(event
.getNativeEvent()))) {
pointerTouchStartedTimer.cancel();
handleMove(event.getNativeEvent());
}
}
};
protected PointerCancelHandler pointerCancelHandler = new PointerCancelHandler() {
@Override
public void onPointerCancel(PointerCancelEvent event) {
GWT.log("SvgArrowWidget.onPointerCancel(PointerCancelEvent)");
pointerTouchStartedTimer.cancel();
cancelMove(true, null);
}
};
protected TouchMoveHandler touchMoveHandler = new TouchMoveHandler() {
@Override
public void onTouchMove(TouchMoveEvent event) {
GWT.log("SvgArrowWidget.onTouchMove(TouchMoveEvent)");
if (event.getChangedTouches().length() == 1) {
handleMove(event.getNativeEvent());
event.preventDefault();
}
}
};
protected TouchCancelHandler touchCancelHandler = new TouchCancelHandler() {
@Override
public void onTouchCancel(TouchCancelEvent event) {
GWT.log("SvgArrowWidget.onTouchCancel(TouchCancelEvent)");
cancelMove(true, null);
}
};
@Override
public void setUpEventHandlers(boolean touchSupported,
boolean msTouchSupported) {
this.touchSupported = touchSupported;
this.msTouchSupported = msTouchSupported;
if (msTouchSupported) {
addDomHandler(pointerDownHandler, PointerDownEvent.getType());
addDomHandler(pointerUpHandler, PointerUpEvent.getType());
} else if (touchSupported) {
addDomHandler(touchStartHandler, TouchStartEvent.getType());
} else {
addHandler(mouseDownHandler, MouseDownEvent.getType());
}
registerMouseDownAndTouchDownEventListener(startingPoint);
registerMouseDownAndTouchDownEventListener(endingPoint);
}
public SvgArrowWidget() {
Element predecessorArrow = createSVGElementNS("svg");
addStyleName(predecessorArrow, "arrow");
predecessorArrow.getStyle().setPosition(Position.ABSOLUTE);
predecessorArrow.getStyle().setZIndex(2);
predecessorArrow.getStyle().setProperty("pointerEvents", "none");
Element g = createSVGElementNS("g");
setAttributeNS(g, "stroke", "black");
setAttributeNS(g, "stroke-width", "1");
curve = createSVGElementNS("path");
addStyleName(curve, "curve-line");
setAttributeNS(curve, "fill", "none");
startingPoint = createSVGElementNS("circle");
startingPoint.getStyle().setProperty("pointerEvents", "visiblePainted");
addStyleName(startingPoint, "start-p");
setAttributeNS(startingPoint, "stroke-width", "2");
setAttributeNS(startingPoint, "r", "7");
setAttributeNS(startingPoint, "fill", "black");
endingPoint = createSVGElementNS("circle");
endingPoint.getStyle().setProperty("pointerEvents", "visiblePainted");
addStyleName(endingPoint, "end-p");
setAttributeNS(endingPoint, "stroke-width", "2");
setAttributeNS(endingPoint, "r", "5");
setAttributeNS(endingPoint, "fill", "black");
DOM.appendChild(g, curve);
DOM.appendChild(g, startingPoint);
DOM.appendChild(g, endingPoint);
DOM.appendChild(predecessorArrow, g);
setElement(predecessorArrow);
}
@Override
protected void onDetach() {
if (isAttached()) {
// Only call onDetach for attached widget. Otherwise GanttWidget's
// content widget container may throw IllegalStateException on
// detach when this widget is already detached explicitly by
// GanttWidget.unregisterContentElement(SvgArrowWidget).
super.onDetach();
}
}
@Override
public void setWidth(double width) {
this.width = (int) width;
setAttributeNS(getElement(), "width", getWidthWithMargin());
}
@Override
public void setHeight(double height) {
this.height = (int) height;
setAttributeNS(getElement(), "height", getHeightWithMargin());
}
@Override
public void setTop(int top) {
getElement().getStyle().setTop(top - getMargin(), Unit.PX);
}
@Override
public void setLeft(int left) {
getElement().getStyle().setLeft(left - getMargin(), Unit.PX);
}
public boolean isReadOnly() {
return readOnly;
}
@Override
public void setReadOnly(boolean readOnly) {
this.readOnly = readOnly;
}
@Override
public void draw(ArrowPositionData d) {
originalData = d;
startingPoint.getStyle().setVisibility(Visibility.VISIBLE);
endingPoint.getStyle().setVisibility(Visibility.VISIBLE);
internalDraw(d);
}
public int getMargin() {
return margin;
}
@Override
public void setArrowChangeHandler(ArrowChangeHandler handler) {
this.handler = handler;
}
protected void addStyleName(Element element, String style) {
String curStyles = element.getAttribute("class");
if (curStyles == null) {
curStyles = "";
}
if (!curStyles.contains(style)) {
curStyles += " " + style;
element.setAttribute("class", curStyles);
}
}
protected int getWidthWithMargin() {
return width + (2 * getMargin());
}
protected int getHeightWithMargin() {
return height + (2 * getMargin());
}
protected int getHalfMargin() {
return getMargin() / 2;
}
protected void internalDraw(ArrowPositionData d) {
internalDrawCurve(d);
setAttributeNS(startingPoint, "cx", d.calcStartPointX() + getMargin());
setAttributeNS(startingPoint, "cy", d.calcStartPointY() + getMargin());
int endPointX = d.calcEndPointX();
int endPointY = d.calcEndPointY();
setAttributeNS(endingPoint, "cx", endPointX + getMargin());
setAttributeNS(endingPoint, "cy", endPointY + getMargin());
}
protected void internalDrawCurve(ArrowPositionData d) {
int y1 = ((d.isFromTop()) ? d.getFromHeightCenter() : height
- d.getFromHeightCenter());
int y2 = ((d.isFromTop()) ? height - d.getToHeightCenter() : d
.getToHeightCenter());
StringBuilder s = new StringBuilder("M");
s.append(" ").append(
(d.isFromLeft()) ? getMargin() : width + getMargin()); // x1
s.append(", ").append(y1 + getMargin()); // y1
s.append(" C");
s.append(" ").append(d.getHalfWidth() + getHalfMargin()); // cx1
s.append(", ").append(y1 + getMargin()); // cy1
s.append(", ").append(d.getHalfWidth() + getHalfMargin()); // cx2
s.append(", ").append(y2 + getMargin()); // cy2
s.append(", ").append(
(d.isFromLeft()) ? width + getMargin() : getMargin()); // x2
s.append(", ").append(y2 + getMargin()); // y2
setAttributeNS(curve, "d", s.toString());
}
protected void updateMovingData(Point forPoint) {
if (selectPredecessorMode) {
movingData = new ArrowPositionData(createSnapshotElement(forPoint),
originalData.getTo());
} else {
movingData = new ArrowPositionData(originalData.getFrom(),
createSnapshotElement(forPoint));
}
}
protected Element createSnapshotElement(Point point) {
int deltaX = (int) (point.getX() - capturePoint.getX());
int deltaY = (int) (point.getY() - capturePoint.getY());
int scrollDeltaY = getElement().getParentElement().getParentElement()
.getScrollTop()
- capturePointScrollTop;
int scrollDeltaX = getElement().getParentElement().getParentElement()
.getScrollLeft()
- capturePointScrollLeft;
int originalTopPoint = selectPredecessorMode ? originalData
.calcStartPointY() : originalData.calcEndPointY();
int originalLeftPoint = selectPredecessorMode ? originalData
.calcStartPointX() : originalData.calcEndPointX();
movePointElement.getStyle().setVisibility(Visibility.HIDDEN);
movePointElement.getStyle().setPosition(Position.ABSOLUTE);
movePointElement.getStyle().setTop(
Math.max(0, originalData.getTop() + originalTopPoint + deltaY
+ scrollDeltaY), Unit.PX);
movePointElement.getStyle().setLeft(
Math.max(0, originalData.getLeft() + originalLeftPoint + deltaX
+ scrollDeltaX), Unit.PX);
movePointElement.getStyle().setWidth(2, Unit.PX);
movePointElement.getStyle().setHeight(2, Unit.PX);
return movePointElement;
}
protected void startMoving(NativeEvent event, Element element) {
if (element.equals(startingPoint)) {
selectPredecessorMode = true;
startingPoint.getStyle().setVisibility(Visibility.HIDDEN);
} else if (element.equals(endingPoint)) {
selectFollowerMode = true;
endingPoint.getStyle().setVisibility(Visibility.HIDDEN);
}
capturePointScrollTop = getElement().getParentElement()
.getParentElement().getScrollTop();
capturePointScrollLeft = getElement().getParentElement()
.getParentElement().getScrollLeft();
getParent().getElement().appendChild(movePointElement);
getElement().getParentElement().addClassName(SELECTION_STYLE_NAME);
GWT.log("Capturing clicked point.");
captureElement = getElement();
Event.setCapture(getElement());
event.stopPropagation();
// enable MODE for new predecessor/following step
// selection.
addMoveHandler();
capturePoint = new Point(getTouchOrMouseClientX(event),
getTouchOrMouseClientY(event));
originalWidth = width;
originalHeight = height;
}
protected void addMoveHandler() {
if (msTouchSupported) {
moveRegisteration = addDomHandler(pointerMoveHandler,
PointerMoveEvent.getType());
touchCancelRegisteration = addDomHandler(pointerCancelHandler,
PointerCancelEvent.getType());
} else if (touchSupported) {
moveRegisteration = addDomHandler(touchMoveHandler,
TouchMoveEvent.getType());
touchCancelRegisteration = addDomHandler(touchCancelHandler,
TouchCancelEvent.getType());
} else {
moveRegisteration = addDomHandler(mouseMoveHandler,
MouseMoveEvent.getType());
}
}
protected void stopMoving(NativeEvent event) {
cancelMove(false, event);
}
protected void cancelMove(boolean forceReset, NativeEvent event) {
GWT.log("Relasing captured point.");
if (captureElement != null) {
Event.releaseCapture(captureElement);
}
movePointElement.removeFromParent();
getElement().getParentElement().removeClassName(SELECTION_STYLE_NAME);
moveRegisteration.removeHandler();
if (touchCancelRegisteration != null) {
touchCancelRegisteration.removeHandler();
}
captureElement = null;
if (forceReset
|| (handler != null && !handler.onArrowChanged(
selectPredecessorMode, event))) {
// cancel
resetArrow();
}
selectPredecessorMode = false;
selectFollowerMode = false;
currentPointerEventId = -1;
pendingPointerDownEvent = null;
}
/** MouseTtouch down event handler. */
protected void handleDownEvent(NativeEvent event) {
if (isReadOnly()) {
return;
}
if (captureElement != null) {
stopMoving(event);
return;
}
Element element = event.getEventTarget().cast();
if (element != null
&& (element.equals(startingPoint) || element
.equals(endingPoint))) {
startMoving(event, element);
}
}
protected void resetArrow() {
setWidth(originalData.getWidth());
setHeight(originalData.getHeight());
setTop((int) originalData.getTop());
setLeft((int) originalData.getLeft());
draw(originalData);
startingPoint.getStyle().setVisibility(Visibility.VISIBLE);
endingPoint.getStyle().setVisibility(Visibility.VISIBLE);
}
protected void handleMove(NativeEvent event) {
Point movePoint = new Point(getTouchOrMouseClientX(event),
getTouchOrMouseClientY(event));
updateMovingData(movePoint);
setWidth(movingData.getWidth());
setHeight(movingData.getHeight());
setTop((int) movingData.getTop());
setLeft((int) movingData.getLeft());
startingPoint.getStyle().setVisibility(Visibility.HIDDEN);
endingPoint.getStyle().setVisibility(Visibility.HIDDEN);
internalDrawCurve(movingData);
event.stopPropagation();
}
boolean touchSupported;
boolean msTouchSupported;
protected void registerMouseDownAndTouchDownEventListener(
final Element element) {
Event.sinkEvents(element, Event.ONMOUSEDOWN | Event.ONTOUCHSTART);
}
}