/*
* Copyright 2014 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.shared.GanttUtil.getBoundingClientRectWidth;
import static org.tltv.gantt.client.shared.GanttUtil.getMarginByComputedStyle;
import static org.tltv.gantt.client.shared.GanttUtil.getTouchOrMouseClientX;
import static org.tltv.gantt.client.shared.GanttUtil.getTouchOrMouseClientY;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.tltv.gantt.client.shared.GanttUtil;
import org.tltv.gantt.client.shared.Resolution;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Cursor;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ContextMenuEvent;
import com.google.gwt.event.dom.client.ContextMenuHandler;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
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.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.ScrollEvent;
import com.google.gwt.event.dom.client.ScrollHandler;
import com.google.gwt.event.dom.client.TouchCancelEvent;
import com.google.gwt.event.dom.client.TouchCancelHandler;
import com.google.gwt.event.dom.client.TouchEndEvent;
import com.google.gwt.event.dom.client.TouchEndHandler;
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.AbstractNativeScrollbar;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.HasEnabled;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.WidgetCollection;
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;
/**
* GWT Gantt chart widget. Includes {@link TimelineWidget} to show timeline, and
* below the timeline, shows content of the Gantt. Content is freely (position:
* absolute) positioned steps aligned vertically on top of each others.
* <p>
* These steps can be moved and resized freely in the space available, limited
* only by the timeline's borders.
* <p>
* All events are handled via {@link GanttRpc}.
* <p>
* Timeline's localization is handled via {@link LocaleDataProvider}.
* <p>
* Here are few steps that need to be notified when taking this widget in use.
* <br>
* First of all, after constructing this widget, you need to initialize it by
* {@link #initWidget(GanttRpc, LocaleDataProvider)} method. But before doing
* that, make sure to call
* {@link #setBrowserInfo(boolean, boolean, boolean, boolean, int)} to let this
* widget know some details of the browser. And if client supports touch events,
* let this widget know that by calling {@link #setTouchSupported(boolean)}
* method before initWidget.
* <p>
* Sample code snippet:
*
* <pre>
* GanttWidget widget = new GanttWidget();
* widget.setBrowserInfo(isIe(), isChrome(), isSafari(), isWebkit(), getMajorBrowserVersion());
* widget.setTouchSupportted(isTouchDevice());
* widget.initWidget(ganttRpc, localeDataProvider);
* </pre>
* <p>
* After initializing, widget is ready to go. But to let this widget know when
* it should re-calculate content widths/heights, call either
* {@link #notifyHeightChanged(int)} or {@link #notifyWidthChanged(int)} methods
* to do that. This needs to be done explicitly for example when widget's width
* is 100%, and the parent's width changes due to browser window's resize event.
*
* @author Tltv
*
*/
public class GanttWidget extends ComplexPanel implements HasEnabled, HasWidgets {
private static final int RESIZE_WIDTH = 10;
private static final int BAR_MIN_WIDTH = RESIZE_WIDTH;
private static final int CLICK_INTERVAL = 250;
/*
* Time mouse-up is ignored after mouse-down to detect double-click.
*/
private static final int DOUBLECLICK_DETECTION_INTERVAL = 250;
private static final int POINTER_TOUCH_DETECTION_INTERVAL = 100;
private static final String STYLE_GANTT = "gantt";
private static final String STYLE_GANTT_CONTAINER = "gantt-container";
private static final String STYLE_GANTT_CONTENT = "gantt-content";
private static final String STYLE_MOVING = "moving";
private static final String STYLE_RESIZING = "resizing";
private static final String STYLE_MOVE_ELEMENT = "mv-el";
private HandlerRegistration pointerDownHandlerRegistration;
private HandlerRegistration pointerUpHandlerRegistration;
private HandlerRegistration pointerMoveHandlerRegistration;
private HandlerRegistration pointerCancelHandlerRegistration;
private HandlerRegistration touchStartHandlerRegistration;
private HandlerRegistration touchEndHandlerRegistration;
private HandlerRegistration touchMoveHandlerRegistration;
private HandlerRegistration touchCancelHandlerRegistration;
private HandlerRegistration scrollHandlerRegistration;
private HandlerRegistration mouseMoveHandlerRegistration;
private HandlerRegistration mouseDownHandlerRegistration;
private HandlerRegistration mouseUpHandlerRegistration;
private HandlerRegistration mouseDblClickHandlerRegistration;
private HandlerRegistration contextMenuHandlerRegistration;
private WidgetCollection children = new WidgetCollection(this);
private boolean enabled = true;
private boolean touchSupported = false;
private boolean movableSteps;
private boolean movableStepsBetweenRows;
private boolean resizableSteps;
private boolean backgroundGridEnabled;
private boolean defaultContextMenuEnabled = false;
private GanttRpc ganttRpc;
private LocaleDataProvider localeDataProvider;
private String locale;
private Resolution resolution;
private int firstDayOfRange;
private int firstHourOfRange;
private long startDate;
private long endDate;
private int minWidth;
protected double contentHeight;
protected boolean wasTimelineOverflowingHorizontally = false;
protected boolean wasContentOverflowingVertically = false;
protected TimelineWidget timeline;
protected DivElement container;
protected DivElement content;
protected DivElement scrollbarSpacer;
protected BgGridElement bgGrid;
/* Extra elements inside the content. */
protected Set<Widget> extraContentElements = new HashSet<Widget>();
protected boolean clickOnNextMouseUp = true;
protected boolean secondaryClickOnNextMouseUp = true;
protected boolean insideDoubleClickDetectionInterval = false;
protected int numberOfMouseClicksDetected = 0;
protected NativeEvent previousMouseUpEvent;
protected Element previousMouseUpBarElement;
protected Point movePoint;
protected boolean resizing = false;
protected boolean resizingFromLeft = false;
protected boolean resizingInProgress = false;
protected boolean moveInProgress = false;
protected int currentPointerEventId;
// following variables are set during mouse down event over a bar element,
// or over it's children element.
protected Point capturePoint;
protected String capturePointLeftPercentage;
protected String capturePointWidthPercentage;
protected double capturePointLeftPx;
protected double capturePointTopPx;
protected double capturePointAbsTopPx;
protected double capturePointWidthPx;
protected String capturePointBgColor;
protected Element targetBarElement;
// these variables are used to memorize the Y and Y origin to scroll the
// container (with touchStart/touchEnd/touchMove events).
protected int containerScrollStartPosY = -1;
protected int containerScrollStartPosX = -1;
// additional element that appears when moving or resizing
protected DivElement moveElement = DivElement.as(DOM.createDiv());
// click is actually detected by 'mouseup'/'mousedown' events, not with
// 'onclick'. click event is not used. disallowClickTimer helps to detect
// click during a short interval. And disallowing it after a short interval.
private Timer disallowClickTimer = new Timer() {
@Override
public void run() {
disableClickOnNextMouseUp();
if (isMovableSteps() && !isResizingInProgress()) {
addMovingStyles(targetBarElement);
}
}
};
/*
* When this timer is started, all clicks (mouse down+up) will be postponed
* until timer executes, but only if double-click event is not fired during
* that time. This also means that with very slow double-click speeds double
* clicks are never registered.
*/
private Timer doubleClickDetectionMaxTimer = new Timer() {
@Override
public void run() {
GWT.log("doubleClickDetectionMaxTimer.run()");
boolean doFireClick = numberOfMouseClicksDetected > 0 && previousMouseUpEvent != null
&& previousMouseUpBarElement != null && !isMoveOrResizingInProgress();
NativeEvent targetEvent = previousMouseUpEvent;
Element targetElement = previousMouseUpBarElement;
cancelDoubleClickDetection();
if (doFireClick) {
fireClickRpc(targetElement, targetEvent);
}
}
};
private NativeEvent pendingPointerDownEvent;
private Timer pointerTouchStartedTimer = new Timer() {
@Override
public void run() {
GanttWidget.this.onTouchOrMouseDown(pendingPointerDownEvent);
pendingPointerDownEvent = null;
}
};
private int previousContainerScrollLeft = 0;
private int previousContainerScrollTop = 0;
private ScrollHandler scrollHandler = new ScrollHandler() {
@Override
public void onScroll(ScrollEvent event) {
Element element = event.getNativeEvent().getEventTarget().cast();
if (element != container) {
return;
}
int sl = container.getScrollLeft();
int st = container.getScrollTop();
if (sl != previousContainerScrollLeft) {
timeline.setScrollLeft(sl);
previousContainerScrollLeft = sl;
}
if (st != previousContainerScrollTop) {
previousContainerScrollTop = st;
}
}
};
private DoubleClickHandler doubleClickHandler = new DoubleClickHandler() {
@Override
public void onDoubleClick(DoubleClickEvent event) {
GWT.log("onDoubleClick(DoubleClickEvent)");
if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
doubleClickDetectionMaxTimer.cancel();
if (!insideDoubleClickDetectionInterval && numberOfMouseClicksDetected < 2) {
return; // ignore double-click
}
if (targetBarElement != null) {
disableClickOnNextMouseUp();
targetBarElement = null;
}
Element bar = getBar(event.getNativeEvent());
if (bar != null && numberOfMouseClicksDetected > 1) {
fireClickRpc(bar, event.getNativeEvent());
}
cancelDoubleClickDetection();
}
}
};
private MouseDownHandler mouseDownHandler = new MouseDownHandler() {
@Override
public void onMouseDown(MouseDownEvent event) {
GWT.log("onMouseDown(MouseDownEvent)");
if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
GanttWidget.this.onTouchOrMouseDown(event.getNativeEvent());
} else {
secondaryClickOnNextMouseUp = true;
new Timer() {
@Override
public void run() {
secondaryClickOnNextMouseUp = false;
}
}.schedule(CLICK_INTERVAL);
event.stopPropagation();
}
}
};
private MouseUpHandler mouseUpHandler = new MouseUpHandler() {
@Override
public void onMouseUp(MouseUpEvent event) {
GWT.log("onMouseUp(MouseUpEvent)");
if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
GanttWidget.this.onTouchOrMouseUp(event.getNativeEvent());
} else {
if (secondaryClickOnNextMouseUp) {
Element bar = getBar(event.getNativeEvent());
if (bar != null && isEnabled()) {
getRpc().stepClicked(getStepUid(bar), event.getNativeEvent(), bar);
}
}
secondaryClickOnNextMouseUp = true;
}
}
};
private ContextMenuHandler contextMenuHandler = new ContextMenuHandler() {
@Override
public void onContextMenu(ContextMenuEvent event) {
if (!defaultContextMenuEnabled) {
event.preventDefault();
}
}
};
private MouseMoveHandler mouseMoveHandler = new MouseMoveHandler() {
@Override
public void onMouseMove(MouseMoveEvent event) {
GanttWidget.this.onTouchOrMouseMove(event.getNativeEvent());
event.preventDefault();
}
};
private PointerDownHandler msPointerDownHandler = new PointerDownHandler() {
@Override
public void onPointerDown(PointerDownEvent event) {
GWT.log("onPointerDown(PointerDownEvent)");
if (currentPointerEventId == -1) {
currentPointerEventId = event.getPointerId();
} else {
event.preventDefault();
return; // multi-touch not supported
}
pendingPointerDownEvent = event.getNativeEvent();
capturePoint = new Point(getTouchOrMouseClientX(event.getNativeEvent()),
getTouchOrMouseClientY(event.getNativeEvent()));
pointerTouchStartedTimer.schedule(POINTER_TOUCH_DETECTION_INTERVAL);
event.preventDefault();
}
};
private PointerUpHandler msPointerUpHandler = new PointerUpHandler() {
@Override
public void onPointerUp(PointerUpEvent event) {
currentPointerEventId = -1;
pointerTouchStartedTimer.cancel();
pendingPointerDownEvent = null;
GanttWidget.this.onTouchOrMouseUp(event.getNativeEvent());
event.preventDefault();
}
};
private PointerMoveHandler msPointerMoveHandler = new PointerMoveHandler() {
@Override
public void onPointerMove(PointerMoveEvent event) {
if (capturePoint == null) {
return;
}
movePoint = new Point(getTouchOrMouseClientX(event.getNativeEvent()),
getTouchOrMouseClientY(event.getNativeEvent()));
// do nothing, if touch position has not changed
if (!(capturePoint.getX() == movePoint.getX() && capturePoint.getY() == movePoint.getY())) {
GanttWidget.this.onTouchOrMouseMove(event.getNativeEvent());
}
}
};
private PointerCancelHandler msPointerCancelHandler = new PointerCancelHandler() {
@Override
public void onPointerCancel(PointerCancelEvent event) {
currentPointerEventId = -1;
pointerTouchStartedTimer.cancel();
pendingPointerDownEvent = null;
onCancelTouch(event.getNativeEvent());
}
};
private TouchStartHandler touchStartHandler = new TouchStartHandler() {
@Override
public void onTouchStart(TouchStartEvent event) {
if (event.getTargetTouches().length() == 1) {
JavaScriptObject target = event.getNativeEvent().getEventTarget().cast();
containerScrollStartPosY = -1;
containerScrollStartPosX = -1;
if (target == container || target == content || (!isMovableSteps())) {
boolean preventDefaultAndReturn = false;
// store x,y position for 'manual' vertical scrolling
if (isContentOverflowingVertically()) {
containerScrollStartPosY = container.getScrollTop() + event.getTouches().get(0).getPageY();
preventDefaultAndReturn = true;
}
if (isContentOverflowingHorizontally()) {
containerScrollStartPosX = container.getScrollLeft() + event.getTouches().get(0).getPageX();
preventDefaultAndReturn = true;
}
if (preventDefaultAndReturn) {
event.preventDefault();
return;
}
}
GanttWidget.this.onTouchOrMouseDown(event.getNativeEvent());
}
event.preventDefault();
}
};
private TouchEndHandler touchEndHandler = new TouchEndHandler() {
@Override
public void onTouchEnd(TouchEndEvent event) {
containerScrollStartPosY = -1;
containerScrollStartPosX = -1;
GanttWidget.this.onTouchOrMouseUp(event.getNativeEvent());
event.preventDefault();
}
};
private TouchMoveHandler touchMoveHandler = new TouchMoveHandler() {
@Override
public void onTouchMove(TouchMoveEvent event) {
if (event.getChangedTouches().length() == 1) {
boolean preventDefaultAndReturn = false;
// did we intend to scroll the container?
// apply 'manual' vertical scrolling
if (containerScrollStartPosY != -1) {
container.setScrollTop(containerScrollStartPosY - event.getChangedTouches().get(0).getPageY());
preventDefaultAndReturn = true;
}
if (containerScrollStartPosX != -1) {
container.setScrollLeft(containerScrollStartPosX - event.getChangedTouches().get(0).getPageX());
preventDefaultAndReturn = true;
}
if (preventDefaultAndReturn) {
event.preventDefault();
return;
}
if (GanttWidget.this.onTouchOrMouseMove(event.getNativeEvent())) {
event.preventDefault();
}
}
}
};
private TouchCancelHandler touchCancelHandler = new TouchCancelHandler() {
@Override
public void onTouchCancel(TouchCancelEvent event) {
containerScrollStartPosY = -1;
containerScrollStartPosX = -1;
onCancelTouch(event.getNativeEvent());
}
};
private boolean ie, chrome, safari, webkit;
public GanttWidget() {
setElement(DivElement.as(DOM.createDiv()));
setStyleName(STYLE_GANTT);
moveElement.setClassName(STYLE_MOVE_ELEMENT);
// not visible by default
moveElement.getStyle().setDisplay(Display.NONE);
timeline = GWT.create(TimelineWidget.class);
container = DivElement.as(DOM.createDiv());
container.setClassName(STYLE_GANTT_CONTAINER);
content = DivElement.as(DOM.createDiv());
content.setClassName(STYLE_GANTT_CONTENT);
container.appendChild(content);
content.appendChild(moveElement);
scrollbarSpacer = DivElement.as(DOM.createDiv());
scrollbarSpacer.getStyle().setHeight(AbstractNativeScrollbar.getNativeScrollbarHeight(), Unit.PX);
scrollbarSpacer.getStyle().setDisplay(Display.NONE);
getElement().appendChild(timeline.getElement());
getElement().appendChild(container);
getElement().appendChild(scrollbarSpacer);
}
public void initWidget(GanttRpc ganttRpc, LocaleDataProvider localeDataProvider) {
setRpc(ganttRpc);
setLocaleDataProvider(localeDataProvider);
resetListeners();
}
/**
* Add new StepWidget into content area.
*
* @param stepIndex
* Index of step (0 based) (not element index in container)
* @param widget
* @param updateAffectedSteps
* Updates position of affected steps. Usually it means steps
* below the target.
*/
public void addStep(int stepIndex, StepWidget stepWidget, boolean updateAffectedSteps) {
DivElement bar = DivElement.as(stepWidget.getElement());
boolean newStep = !bar.hasParentElement();
boolean moving = !newStep && getStepIndex(stepWidget) != stepIndex;
boolean insertDOM = newStep || moving;
if (insertDOM) {
insert(stepIndex + getAdditonalContentElementCount(), stepWidget);
}
// Update top
int stepsInContainer = getChildren().size() - getAdditionalWidgetContentElementCount();
int indexInWidgetContainer = stepIndex + getAdditionalWidgetContentElementCount();
// bar height should be defined in css
int height = getElementHeightWithMargin(bar);
if (stepIndex == 0) {
bar.getStyle().setTop(0, Unit.PX);
if (updateAffectedSteps) {
updateTopForAllStepsBelow(indexInWidgetContainer + 1, height);
}
} else if (stepIndex < stepsInContainer) {
// update top by the previous step top + step height.
// Requires that previous steps top is already correct.
int prevWidgetIndex = indexInWidgetContainer - 1;
Widget w = getWidget(prevWidgetIndex);
if (w instanceof StepWidget) {
double top = parseSize(w.getElement().getStyle().getTop(), "px");
top += getElementHeightWithMargin(w.getElement());
bar.getStyle().setTop(top, Unit.PX);
if (updateAffectedSteps) {
updateTopForAllStepsBelow(indexInWidgetContainer + 1, height);
}
}
}
if (insertDOM) {
contentHeight += height;
}
if (newStep) {
registerBarEventListener(bar);
}
}
/**
* Remove Widget from the content area.
*
* @param widget
*/
public void removeStep(Widget widget) {
remove(widget);
}
/**
* Update Gantt chart's timeline and content for the given steps. This won't
* add any steps, but will update the content widths and heights.
*
* @param steps
*/
public void update(List<StepWidget> steps) {
if (startDate < 0 || endDate < 0 || startDate >= endDate) {
GWT.log("Invalid start and end dates. Gantt chart can't be rendered. Start: " + startDate + ", End: "
+ endDate);
return;
}
content.getStyle().setHeight(contentHeight, Unit.PX);
GWT.log("GanttWidget's active TimeZone: " + getLocaleDataProvider().getTimeZone().getID() + " (raw offset: "
+ getLocaleDataProvider().getTimeZone().getStandardOffset() + ")");
// tell timeline to notice vertical scrollbar before updating it
timeline.setNoticeVerticalScrollbarWidth(isContentOverflowingVertically());
timeline.update(resolution, startDate, endDate, firstDayOfRange, firstHourOfRange, localeDataProvider);
setContentMinWidth(timeline.getMinWidth());
updateContainerStyle();
updateContentWidth();
updateStepWidths(steps);
wasTimelineOverflowingHorizontally = timeline.isTimelineOverflowingHorizontally();
}
/**
* Set minimum width for the content element.
*
* @param minWidth
* Minimum width in pixels.
*/
public void setContentMinWidth(int minWidth) {
this.minWidth = minWidth;
content.getStyle().setProperty("minWidth", this.minWidth + "px");
}
/**
* Return minimal width of the content.
*
* @return Minimum width in pixels.
*/
public int getMinWidth() {
return minWidth;
}
/**
* Get current currently active timeline {@link Resolution}.
*
* @return resolution enum
*/
public Resolution getResolution() {
return resolution;
}
/**
* Set Gantt's timeline resolution.
*
* @param resolution
* New timeline resolution.
*/
public void setResolution(Resolution resolution) {
this.resolution = resolution;
}
/**
* Get timeline's start date. Date value should follow specification in
* {@link Date#getTime()}.
*
* @return Start date in milliseconds.
*/
public long getStartDate() {
return startDate;
}
/**
* Set timeline's start date. Date value should follow specification in
* {@link Date#getTime()}.
*
* @param startDate
* New start date in milliseconds.
*/
public void setStartDate(Long startDate) {
this.startDate = (startDate != null) ? startDate : 0;
}
/**
* Get timeline's end date. Date value should follow specification in
* {@link Date#getTime()}.
*
* @return End date in milliseconds.
*/
public long getEndDate() {
return endDate;
}
/**
* Set timeline's end date. Date value should follow specification in
* {@link Date#getTime()}.
*
* @param endDate
* New end date in milliseconds.
*/
public void setEndDate(Long endDate) {
this.endDate = (endDate != null) ? endDate : 0;
}
/**
* Set first day of timeline's date range. Value is between 1-7, where 1 is
* SUNDAY.
*
* @param firstDayOfRange
* Value between 1-7.
*/
public void setFirstDayOfRange(int firstDayOfRange) {
this.firstDayOfRange = firstDayOfRange;
}
public void setFirstHourOfRange(int firstHourOfRange) {
this.firstHourOfRange = firstHourOfRange;
}
/**
* Notify Gantt widget that height has changed. Delegates necessary changes
* to child elements.
*
* @param height
* New height in pixels
*/
public void notifyHeightChanged(int height) {
if (container != null && timeline != null) {
if (!"".equals(getElement().getStyle().getHeight())) {
container.getStyle().setHeight(height - getTimelineHeight() - getHorizontalScrollbarSpacerHeight(),
Unit.PX);
} else {
// if the component has undefined height also set undefined
// height to the container
container.getStyle().setHeight(-1, Unit.PX);
}
boolean overflow = isContentOverflowingVertically();
if (wasContentOverflowingVertically != overflow) {
wasContentOverflowingVertically = overflow;
timeline.setNoticeVerticalScrollbarWidth(overflow);
// width has changed due to vertical scrollbar
// appearing/disappearing
internalHandleWidthChange();
}
}
}
/**
* Get Timeline widget height.
*
* @return
*/
public int getTimelineHeight() {
if (timeline != null) {
return timeline.getElement().getClientHeight();
}
return 0;
}
/**
* Notify Gantt widget that width has changed. Delegates necessary changes
* to child elements.
*
* @param width
* New width in pixels
*/
public void notifyWidthChanged(int width) {
if (timeline != null) {
boolean overflow = timeline.checkTimelineOverflowingHorizontally();
if (timeline.isAlwaysCalculatePixelWidths() || wasTimelineOverflowingHorizontally != overflow) {
// scrollbar has just appeared/disappeared
wasTimelineOverflowingHorizontally = overflow;
if (!wasTimelineOverflowingHorizontally) {
timeline.setScrollLeft(0);
}
}
internalHandleWidthChange();
}
}
protected void internalHandleWidthChange() {
timeline.updateWidths();
updateContainerStyle();
updateContentWidth();
}
/**
* Set RPC implementation that is used to communicate with the server.
*
* @param ganttRpc
* GanttRpc
*/
public void setRpc(GanttRpc ganttRpc) {
this.ganttRpc = ganttRpc;
}
/**
* Get RPC implementation that is used to communicate with the server.
*
* @return GanttRpc
*/
public GanttRpc getRpc() {
return ganttRpc;
}
/**
* Reset listeners.
*/
public void resetListeners() {
Event.sinkEvents(container, Event.ONSCROLL | Event.ONCONTEXTMENU);
if (contextMenuHandlerRegistration == null) {
contextMenuHandlerRegistration = addDomHandler(contextMenuHandler, ContextMenuEvent.getType());
}
if (scrollHandlerRegistration == null) {
scrollHandlerRegistration = addHandler(scrollHandler, ScrollEvent.getType());
}
if (isMsTouchSupported()) {
// IE10 pointer events (ms-prefixed events)
if (pointerDownHandlerRegistration == null) {
pointerDownHandlerRegistration = addDomHandler(msPointerDownHandler, PointerDownEvent.getType());
}
if (pointerUpHandlerRegistration == null) {
pointerUpHandlerRegistration = addDomHandler(msPointerUpHandler, PointerUpEvent.getType());
}
if (pointerMoveHandlerRegistration == null) {
pointerMoveHandlerRegistration = addDomHandler(msPointerMoveHandler, PointerMoveEvent.getType());
}
if (pointerCancelHandlerRegistration == null) {
pointerCancelHandlerRegistration = addHandler(msPointerCancelHandler, PointerCancelEvent.getType());
}
} else if (touchSupported) {
// touch events replaces mouse events
if (touchStartHandlerRegistration == null) {
touchStartHandlerRegistration = addDomHandler(touchStartHandler, TouchStartEvent.getType());
}
if (touchEndHandlerRegistration == null) {
touchEndHandlerRegistration = addDomHandler(touchEndHandler, TouchEndEvent.getType());
}
if (touchMoveHandlerRegistration == null) {
touchMoveHandlerRegistration = addDomHandler(touchMoveHandler, TouchMoveEvent.getType());
}
if (touchCancelHandlerRegistration == null) {
touchCancelHandlerRegistration = addHandler(touchCancelHandler, TouchCancelEvent.getType());
}
} else {
if (mouseDblClickHandlerRegistration == null) {
mouseDblClickHandlerRegistration = addDomHandler(doubleClickHandler, DoubleClickEvent.getType());
}
if (mouseDownHandlerRegistration == null) {
mouseDownHandlerRegistration = addDomHandler(mouseDownHandler, MouseDownEvent.getType());
}
if (mouseUpHandlerRegistration == null) {
mouseUpHandlerRegistration = addDomHandler(mouseUpHandler, MouseUpEvent.getType());
}
if (isMovableSteps() || isResizableSteps()) {
if (mouseMoveHandlerRegistration == null) {
mouseMoveHandlerRegistration = addDomHandler(mouseMoveHandler, MouseMoveEvent.getType());
}
} else if (mouseMoveHandlerRegistration != null) {
mouseMoveHandlerRegistration.removeHandler();
mouseMoveHandlerRegistration = null;
}
}
}
/**
* Return true if background grid is enabled.
*
* @return True if background grid is enabled.
*/
public boolean isBackgroundGridEnabled() {
return backgroundGridEnabled;
}
/**
* Set background grid enabled. Next time the widget is painted, the grid is
* shown on the background of the container.
*
* @param backgroundGridEnabled
* True sets background grid enabled.
*/
public void setBackgroundGridEnabled(boolean backgroundGridEnabled) {
this.backgroundGridEnabled = backgroundGridEnabled;
}
/**
* Enable or disable touch support.
*
* @param touchSupported
* True enables touch support.
*/
public void setTouchSupported(boolean touchSupported) {
this.touchSupported = touchSupported;
}
/**
* Are touch events supported.
*
* @return True if touch events are supported.
*/
public boolean isTouchSupported() {
return touchSupported;
}
/**
* Enable or disable resizable steps.
*
* @param resizableSteps
* True enables step resizing.
*/
public void setResizableSteps(boolean resizableSteps) {
this.resizableSteps = resizableSteps;
}
/**
* Returns true if widget is enabled and steps are resizable.
*
* @return
*/
public boolean isResizableSteps() {
return isEnabled() && resizableSteps;
}
/**
* Enable or disable movable step's -feature.
*
* @param movableSteps
* True makes steps movable.
*/
public void setMovableSteps(boolean movableSteps) {
this.movableSteps = movableSteps;
}
/**
* Returns true if widget is enabled and steps are movable.
*
* @return
*/
public boolean isMovableSteps() {
return isEnabled() && movableSteps;
}
/**
* Returns true if the widget is enabled the steps are movable and the steps
* are movable between rows.
*
* @return
*/
public boolean isMovableStepsBetweenRows() {
return isMovableSteps() && movableStepsBetweenRows;
}
/**
* Returns true if the widget is enabled the steps are movable and the steps
* are movable between rows.
*
* @return
*/
public boolean isMovableStepsBetweenRows(Element bar) {
return isMovableStepsBetweenRows()/* && !isSubBar(bar) */;
}
/**
* Enable or disable movable steps between lines feature
*
* @param movableStepsBetweenRows
* True makes steps movable between lines
*/
public void setMovableStepsBetweenRows(boolean movableStepsBetweenRows) {
this.movableStepsBetweenRows = movableStepsBetweenRows;
}
public boolean isMonthRowVisible() {
return timeline.isMonthRowVisible();
}
public void setMonthRowVisible(boolean monthRowVisible) {
timeline.setMonthRowVisible(monthRowVisible);
}
public boolean isYearRowVisible() {
return timeline.isYearRowVisible();
}
public void setYearRowVisible(boolean yearRowVisible) {
timeline.setYearRowVisible(yearRowVisible);
}
public String getMonthFormat() {
return timeline.getMonthFormat();
}
public void setMonthFormat(String monthFormat) {
timeline.setMonthFormat(monthFormat);
}
public void setYearFormat(String yearFormat) {
timeline.setYearFormat(yearFormat);
}
public String getYearFormat() {
return timeline.getYearFormat();
}
public void setWeekFormat(String weekFormat) {
timeline.setWeekFormat(weekFormat);
}
public void setDayFormat(String dayFormat) {
timeline.setDayFormat(dayFormat);
}
public boolean isDefaultContextMenuEnabled() {
return defaultContextMenuEnabled;
}
public void setDefaultContextMenuEnabled(boolean defaultContextMenuEnabled) {
this.defaultContextMenuEnabled = defaultContextMenuEnabled;
}
/**
* Set LocaleDataProvider that is used to provide translations of months and
* weekdays.
*
* @param localeDataProvider
*/
public void setLocaleDataProvider(LocaleDataProvider localeDataProvider) {
this.localeDataProvider = localeDataProvider;
}
public LocaleDataProvider getLocaleDataProvider() {
return localeDataProvider;
}
/**
* Notify which browser this widget should optimize to. Usually just one
* argument is true.
*
* @param ie
* @param chrome
* @param safari
* @param webkit
* @param majorVersion
*/
public void setBrowserInfo(boolean ie, boolean chrome, boolean safari, boolean webkit, int majorVersion) {
this.ie = ie;
this.chrome = chrome;
this.safari = safari;
this.webkit = webkit;
timeline.setBrowserInfo(ie, majorVersion);
}
/**
* @see TimelineWidget#setAlwaysCalculatePixelWidths(boolean)
* @param calcPx
*/
public void setAlwaysCalculatePixelWidths(boolean calcPx) {
timeline.setAlwaysCalculatePixelWidths(calcPx);
}
/**
* Sets timeline's force update flag up. Next
* {@link TimelineWidget#update(Resolution, long, long, int, int, LocaleDataProvider)}
* call knows then to update everything.
*/
public void setForceUpdateTimeline() {
if (timeline == null) {
return;
}
timeline.setForceUpdate();
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Element getScrollContainer() {
return container;
}
/**
* Get height of the scroll container. Includes the horizontal scrollbar
* spacer.
*
* @return
*/
public int getScrollContainerHeight() {
if (scrollbarSpacer.getStyle().getDisplay().isEmpty()) {
return getScrollContainer().getClientHeight() + scrollbarSpacer.getClientHeight();
}
return getScrollContainer().getClientHeight();
}
/**
* Return true, if content is overflowing vertically. This means also that
* vertical scroll bar is visible.
*
* @return
*/
public boolean isContentOverflowingVertically() {
if (content == null || container == null) {
return false;
}
return content.getClientHeight() > container.getClientHeight();
}
/**
* Return true, if content is overflowing horizontally. This means also that
* horizontal scroll bar is visible.
*
* @return
*/
public boolean isContentOverflowingHorizontally() {
// state of horizontal overflow is handled by timeline widget
if (content == null || container == null || timeline == null) {
return false;
}
return timeline.isTimelineOverflowingHorizontally();
}
/**
* Show empty spacing in horizontal scrollbar's position.
*/
public void showHorizontalScrollbarSpacer() {
if (!scrollbarSpacer.getStyle().getDisplay().isEmpty()) {
scrollbarSpacer.getStyle().clearDisplay();
notifyHeightChanged(getOffsetHeight());
}
}
/**
* Hide empty spacing in horizontal scrollbar's position.
*/
public void hideHorizontalScrollbarSpacer() {
if (scrollbarSpacer.getStyle().getDisplay().isEmpty()) {
scrollbarSpacer.getStyle().setDisplay(Display.NONE);
notifyHeightChanged(getOffsetHeight());
}
}
public void updateBarPercentagePosition(long startDate, long endDate, long ownerStartDate, long ownerEndDate,
Element bar) {
double ownerStepWidth = GanttUtil.getBoundingClientRectWidth(bar.getParentElement());
String sLeft = timeline.getLeftPositionPercentageStringForDate(startDate, ownerStepWidth, ownerStartDate,
ownerEndDate);
bar.getStyle().setProperty("left", sLeft);
double range = ownerEndDate - ownerStartDate;
String sWidth = timeline.getWidthPercentageStringForDateInterval(endDate - startDate, range);
bar.getStyle().setProperty("width", sWidth);
}
public void updateBarPercentagePosition(long startDate, long endDate, Element bar) {
String sLeft = timeline.getLeftPositionPercentageStringForDate(startDate, getContentWidth());
bar.getStyle().setProperty("left", sLeft);
String sWidth = timeline.getWidthPercentageStringForDateInterval(endDate - startDate);
bar.getStyle().setProperty("width", sWidth);
}
/**
* Register and add Widget inside the content.
*/
public void registerContentElement(Widget widget) {
if (extraContentElements.add(widget)) {
insertFirst(widget);
}
}
/**
* Unregister and remove element from the content.
*/
public void unregisterContentElement(Widget widget) {
if (widget != null) {
extraContentElements.remove(widget);
widget.removeFromParent();
}
}
public native boolean isMsTouchSupported()
/*-{
return !!(navigator.maxTouchPoints > 0);
}-*/;
/**
* Get current step index if widget element is attached to DOM. Otherwise
* return -1.
*/
public int getStepIndex(StepWidget stepWidget) {
if (stepWidget != null && stepWidget.getElement().hasParentElement()) {
int widgetIndex = getWidgetIndex(stepWidget);
return widgetIndex - getAdditionalWidgetContentElementCount();
}
return -1;
}
@Override
public void add(Widget w) {
super.add(w, content);
}
/**
* @param child
* Child widget.
* @param container
* Parent element.
* @param beforeIndex
* Target index of element in DOM.
* @param domInsert
* true: Insert at specific position. false: append at the end.
*/
@Override
protected void insert(Widget child, Element container, int beforeIndex, boolean domInsert) {
GWT.log("Count content elements: " + content.getChildCount() + " ("
+ getAdditionalNonWidgetContentElementCount() + " non-widget non-step elements, "
+ (getAdditonalContentElementCount() - getAdditionalNonWidgetContentElementCount())
+ " non-step widgets.)");
// Validate index; adjust if the widget is already a child of this
// panel.
int adjustedBeforeStepIndex = adjustIndex(child, beforeIndex - getAdditionalNonWidgetContentElementCount())
- getAdditionalWidgetContentElementCount();
// Detach new child. Might also remove additional widgets like
// predecessor arrows. May affect contentHeight.
child.removeFromParent();
// Logical attach.
getChildren().insert(child, adjustedBeforeStepIndex + getAdditionalWidgetContentElementCount());
// Physical attach.
if (domInsert) {
DOM.insertChild(container, child.getElement(), adjustedBeforeStepIndex + getAdditonalContentElementCount());
} else {
DOM.appendChild(container, child.getElement());
}
// Adopt.
adopt(child);
}
public void insert(int beforeIndex, Widget w) {
insert(w, content, beforeIndex, true);
}
public void insertFirst(Widget child) {
// Detach new child.
child.removeFromParent();
// Logical attach.
getChildren().insert(child, 0);
// Physical attach.
content.insertFirst(child.getElement());
// Adopt.
adopt(child);
}
@Override
public boolean remove(Widget w) {
if (!(w instanceof StepWidget)) {
return super.remove(w);
} else {
int startIndex = getWidgetIndex(w);
int height = getElementHeightWithMargin(w.getElement());
contentHeight -= height;
if ((startIndex = removeAndReturnIndex(w)) >= 0) {
updateTopForAllStepsBelow(startIndex, -height);
// update content height
content.getStyle().setHeight(contentHeight, Unit.PX);
return true;
}
return false;
}
}
public int removeAndReturnIndex(Widget w) {
int index = -1;
// Validate.
if (w.getParent() != this) {
return index;
}
// Orphan.
try {
orphan(w);
} finally {
index = getWidgetIndex(w);
// Physical detach.
Element elem = w.getElement();
content.removeChild(elem);
// Logical detach.
getChildren().remove(w);
}
return index;
}
/**
* Update step widths based on the timeline. Timeline's width have to be
* final at this point.
*
* @param steps
*/
protected void updateStepWidths(Collection<StepWidget> steps) {
for (StepWidget step : steps) {
step.updateWidth();
}
}
protected Element getBar(NativeEvent event) {
Element element = event.getEventTarget().cast();
if (element == null || isSvg(element)) {
return null;
}
Element parent = element;
while (parent.getParentElement() != null && parent.getParentElement() != content && !isBar(parent)) {
parent = parent.getParentElement();
}
if (isBar(parent)) {
return parent;
}
return null;
}
protected boolean isBgElement(Element target) {
return bgGrid != null && bgGrid.equals(target);
}
private boolean isSvg(Element element) {
// safety check to avoid calling non existing functions
if (!isPartOfSvg(element)) {
return element.hasTagName("svg");
}
return true;
}
private static native boolean isPartOfSvg(Element element)
/*-{
if(element.ownerSVGElement) {
return true;
}
return false;
}-*/;
protected boolean isBar(Element element) {
if (isSvg(element)) {
return false;
}
return element.hasClassName(AbstractStepWidget.STYLE_BAR);
}
protected boolean isSubBar(Element element) {
if (isSvg(element)) {
return false;
}
return element.hasClassName(SubStepWidget.STYLE_SUB_BAR);
}
protected boolean hasSubBars(Element element) {
if (isSvg(element)) {
return false;
}
return element.hasClassName(StepWidget.STYLE_HAS_SUB_STEPS);
}
/**
* This is called when target bar element is moved successfully. Element's
* CSS attributes 'left' and 'width' are updated (unit in pixels).
*
* @param bar
* Moved Bar element
* @param y
*/
protected void moveCompleted(Element bar, int y, NativeEvent event) {
double deltay = y - capturePoint.getY();
GWT.log("Position delta y: " + deltay + "px" + " capture point y is " + capturePoint.getY());
Element newPosition = findStepElement(bar, (int) capturePointAbsTopPx,
(int) (capturePointAbsTopPx + getElementHeightWithMargin(bar)), y, deltay);
internalMoveOrResizeCompleted(bar, newPosition, true, event);
}
/**
* This is called when target bar is resized successfully. Element's CSS
* attributes 'left' and 'width' are updated (unit in pixels).
*
* @param bar
* Resized Bar element
*/
protected void resizingCompleted(Element bar, NativeEvent event) {
internalMoveOrResizeCompleted(bar, null, false, event);
}
protected void onTouchOrMouseDown(NativeEvent event) {
if (targetBarElement != null && isMoveOrResizingInProgress()) {
// discard previous 'operation'.
resetBarPosition(targetBarElement);
stopDrag(event);
return;
}
Element bar = getBar(event);
if (bar == null) {
return;
}
targetBarElement = bar;
capturePoint = new Point(getTouchOrMouseClientX(event), getTouchOrMouseClientY(event));
movePoint = new Point(getTouchOrMouseClientX(event), getTouchOrMouseClientY(event));
capturePointLeftPercentage = bar.getStyle().getProperty("left");
capturePointWidthPercentage = bar.getStyle().getProperty("width");
capturePointLeftPx = bar.getOffsetLeft();
capturePointTopPx = bar.getOffsetTop();
capturePointAbsTopPx = bar.getAbsoluteTop();
capturePointWidthPx = bar.getClientWidth();
capturePointBgColor = bar.getStyle().getBackgroundColor();
if (detectResizing(bar)) {
resizing = true;
resizingFromLeft = isResizingLeft(bar);
} else {
resizing = false;
}
disallowClickTimer.schedule(CLICK_INTERVAL);
insideDoubleClickDetectionInterval = true;
doubleClickDetectionMaxTimer.schedule(DOUBLECLICK_DETECTION_INTERVAL);
event.stopPropagation();
}
protected void onTouchOrMouseUp(NativeEvent event) {
if (targetBarElement == null) {
return;
}
Element bar = getBar(event);
disallowClickTimer.cancel();
if (bar == targetBarElement && isClickOnNextMouseup()) {
clickOnNextMouseUp = true;
if (insideDoubleClickDetectionInterval) {
numberOfMouseClicksDetected++;
previousMouseUpEvent = event;
previousMouseUpBarElement = bar;
// event handling postponed until doubleClickDetectionTimer
// runs.
} else {
fireClickRpc(bar, event);
}
} else {
clickOnNextMouseUp = true;
bar = targetBarElement;
if (resizing) {
removeResizingStyles(bar);
if (resizingInProgress) {
resizingCompleted(bar, event);
} else {
resetBarPosition(bar);
}
} else if (isMovableStep(bar)) {
// moving in progress
removeMovingStyles(bar);
if (moveInProgress) {
moveCompleted(bar, getTouchOrMouseClientY(event), event);
} else {
resetBarPosition(bar);
}
}
bar.getStyle().setBackgroundColor(capturePointBgColor);
}
stopDrag(event);
}
protected void fireClickRpc(Element bar, NativeEvent event) {
if (isEnabled()) {
getRpc().stepClicked(getStepUid(bar), event, bar);
}
}
protected void stopDrag(NativeEvent event) {
hideMoveElement();
targetBarElement = null;
capturePoint = null;
resizing = false;
resizingInProgress = false;
moveInProgress = false;
event.stopPropagation();
}
protected void onCancelTouch(NativeEvent event) {
if (targetBarElement == null) {
return;
}
resetBarPosition(targetBarElement);
stopDrag(event);
}
/**
* Handle step's move event.
*
* @param event
* NativeEvent
* @return True, if this event was handled and had effect on step.
*/
protected boolean onTouchOrMouseMove(NativeEvent event) {
Element bar = getBar(event);
if (bar != null) {
movePoint = new Point(getTouchOrMouseClientX(event), getTouchOrMouseClientY(event));
showResizingPointer(bar, detectResizing(bar));
}
if (targetBarElement == null) {
return false;
}
bar = targetBarElement;
doubleClickDetectionMaxTimer.cancel();
disallowClickTimer.cancel();
clickOnNextMouseUp = false;
cancelDoubleClickDetection();
// calculate delta x and y by original position and the current one.
double deltax = getTouchOrMouseClientX(event) - capturePoint.getX();
double deltay = getTouchOrMouseClientY(event) - capturePoint.getY();
GWT.log("Position delta x: " + deltax + "px");
if (resizing) {
resizingInProgress = deltax != 0.0;
if (resizingFromLeft) {
updateBarResizingLeft(bar, deltax);
} else {
updateBarResizingRight(bar, deltax);
}
addResizingStyles(bar);
bar.getStyle().clearBackgroundColor();
} else if (isMovableStep(bar)) {
updateMoveInProgressFlag(bar, deltax, deltay);
updateBarMovingPosition(bar, deltax);
addMovingStyles(bar);
bar.getStyle().clearBackgroundColor();
if (isMovableStepsBetweenRows(bar)) {
updateBarYPosition(bar, deltay);
}
}
// event.stopPropagation();
return true;
}
protected void updateMoveInProgressFlag(Element bar, double deltax, double deltay) {
moveInProgress = deltax != 0.0 || (isMovableStepsBetweenRows(bar) && Math.abs(deltay) > 3);
}
/**
* Helper method to find Step element by given starting point and y-position
* and delta-y. Starting point is there to optimize performance a bit as
* there's no need to iterate through every single step element.
*
* @param startFromBar
* Starting point element
* @param newY
* target y-axis position
* @param deltay
* delta-y relative to starting point element.
* @return Step element at y-axis position. May be same element as given
* startFromBar element.
*/
protected Element findStepElement(Element startFromBar, int startTopY, int startBottomY, int newY, double deltay) {
boolean subStep = isSubBar(startFromBar);
if (subStep) {
startFromBar = startFromBar.getParentElement();
}
if (isBetween(newY, startTopY, startBottomY)) {
GWT.log("findStepElement returns same: Y " + newY + " between " + startTopY + "-" + startBottomY);
return startFromBar;
}
int startIndex = getChildIndex(content, startFromBar);
Element barCanditate;
int i = startIndex;
if (deltay > 0) {
i++;
for (; i < content.getChildCount(); i++) {
barCanditate = Element.as(content.getChild(i));
if (isBetween(newY, barCanditate.getAbsoluteTop(), barCanditate.getAbsoluteBottom())) {
if (!subStep && i == (startIndex + 1)) {
// moving directly over the following step will be
// ignored (if not sub-step).
return startFromBar;
}
return barCanditate;
}
}
} else if (deltay < 0) {
i--;
for (; i >= getAdditonalContentElementCount(); i--) {
barCanditate = Element.as(content.getChild(i));
if (isBetween(newY, barCanditate.getAbsoluteTop(), barCanditate.getAbsoluteBottom())) {
return barCanditate;
}
}
}
return startFromBar;
}
/**
* Get UID for the given Step element. Or null if StepWidget for the element
* doesn't exist in container.
*/
protected String getStepUid(Element stepElement) {
if (isSubBar(stepElement)) {
return getSubStepUid(stepElement);
}
// get widget by index known by this ComplexPanel.
StepWidget widget = getStepWidget(stepElement);
if (widget != null) {
return widget.getStep().getUid();
}
return null;
}
protected String getSubStepUid(Element subStepElement) {
Element stepElement = subStepElement.getParentElement();
StepWidget widget = getStepWidget(stepElement);
if (widget != null) {
return widget.getStepUidBySubStepElement(subStepElement);
}
return null;
}
protected SubStepWidget getSubStepWidget(Element subStepElement) {
Element stepElement = subStepElement.getParentElement();
StepWidget widget = getStepWidget(stepElement);
if (widget != null) {
return widget.getSubStepWidgetByElement(subStepElement);
}
return null;
}
protected StepWidget getStepWidget(Element stepElement) {
Widget widget = getWidget(getChildIndex(content, stepElement) - getAdditionalNonWidgetContentElementCount());
if (widget instanceof StepWidget) {
return (StepWidget) widget;
}
return null;
}
protected AbstractStepWidget getAbstractStepWidget(Element stepElement) {
if (isSubBar(stepElement)) {
return getSubStepWidget(stepElement);
}
return getStepWidget(stepElement);
}
protected BgGridElement createBackgroundGrid() {
// implementation may be overridden via deferred binding. There is
// BgGridSvgElement alternative available and also used by Chrome.
BgGridElement grid = GWT.create(BgGridCssElement.class);
grid.init(container, content);
return grid;
}
private void updateTopForAllStepsBelow(int startIndex, int delta) {
GWT.log("Updating top for all steps below index " + startIndex + ". Delta y: " + delta + "px");
// update top for all elements below
Element elementBelow;
for (int i = startIndex; i < getChildren().size(); i++) {
elementBelow = getWidget(i).getElement();
double top = parseSize(elementBelow.getStyle().getTop(), "px");
elementBelow.getStyle().setTop(top + delta, Unit.PX);
}
}
private double calculateBackgroundGridWidth() {
return timeline.calculateTimelineWidth();
}
private void updateContainerStyle() {
if (bgGrid == null) {
bgGrid = createBackgroundGrid();
}
if (!isBackgroundGridEnabled()) {
bgGrid.hide();
return;
}
// Container element has a background image that is positioned, sized
// and repeated to fill the whole container with a nice grid background.
// Update 'background-size' in container element to match the background
// grid's cell width and height to match with the timeline and rows.
// Update also 'background-position' in container to match the first
// resolution element width in the timeline, IF it's not same as all
// other resolution element widths.
int resDivElementCount = timeline.getResolutionDiv().getChildCount();
if (resDivElementCount == 0) {
return;
}
Element secondResolutionBlock = null;
Double firstResolutionBlockWidth = timeline.getFirstResolutionElementWidth();
if (firstResolutionBlockWidth == null) {
return;
}
Double secondResolutionBlockWidth = null;
if (resDivElementCount > 2) {
secondResolutionBlock = Element.as(timeline.getResolutionDiv().getChild(1));
secondResolutionBlockWidth = getBoundingClientRectWidth(secondResolutionBlock);
}
boolean contentOverflowingHorizontally = isContentOverflowingHorizontally();
boolean adjustBgPosition = secondResolutionBlockWidth != null
&& !firstResolutionBlockWidth.equals(secondResolutionBlockWidth);
double gridBlockWidthPx = 0.0;
if (!adjustBgPosition) {
gridBlockWidthPx = firstResolutionBlockWidth;
} else {
gridBlockWidthPx = secondResolutionBlockWidth;
}
updateContainerBackgroundSize(contentOverflowingHorizontally, gridBlockWidthPx);
updateContainerBackgroundPosition(firstResolutionBlockWidth, contentOverflowingHorizontally, gridBlockWidthPx,
adjustBgPosition);
}
private void updateContainerBackgroundSize(boolean contentOverflowingHorizontally, double gridBlockWidthPx) {
String gridBlockWidth = null;
if (contentOverflowingHorizontally || useAlwaysPxSizeInBackground()) {
gridBlockWidth = timeline.toCssCalcOrNumberString(gridBlockWidthPx, "px");
} else {
double contentWidth = getBoundingClientRectWidth(content);
gridBlockWidth = timeline.toCssCalcOrNumberString((100.0 / contentWidth) * gridBlockWidthPx, "%");
}
int gridBlockHeightPx = getBgGridCellHeight();
bgGrid.setBackgroundSize(gridBlockWidth, gridBlockWidthPx, gridBlockHeightPx);
}
private void updateContainerBackgroundPosition(double firstResolutionBlockWidth,
boolean contentOverflowingHorizontally, double gridBlockWidthPx, boolean adjustBgPosition) {
if (adjustBgPosition) {
double realBgPosXPx = firstResolutionBlockWidth - 1.0;
if (useAlwaysPxSizeInBackground() || contentOverflowingHorizontally) {
bgGrid.setBackgroundPosition(timeline.toCssCalcOrNumberString(realBgPosXPx, "px"), "0px", realBgPosXPx,
0);
} else {
double timelineWidth = calculateBackgroundGridWidth();
double relativeBgAreaWidth = timelineWidth - gridBlockWidthPx;
double bgPosX = (100.0 / relativeBgAreaWidth) * realBgPosXPx;
bgGrid.setBackgroundPosition(timeline.toCssCalcOrNumberString(bgPosX, "%"), "0px", realBgPosXPx, 0);
}
} else {
bgGrid.setBackgroundPosition("-1px", "0", -1, 0);
}
}
private boolean useAlwaysPxSizeInBackground() {
return ie || chrome || safari || webkit;
}
private int getBgGridCellHeight() {
int gridBlockHeightPx = 0;
int firstStepIndex = getAdditonalContentElementCount();
if (firstStepIndex < content.getChildCount()) {
Element firstBar = Element.as(content.getChild(firstStepIndex));
gridBlockHeightPx = getElementHeightWithMargin(firstBar);
if ((contentHeight % gridBlockHeightPx) != 0) {
// height is not divided evenly for each bar.
// Can't use background grid with background-size trick.
gridBlockHeightPx = 0;
}
}
return gridBlockHeightPx;
}
private void updateContentWidth() {
if (timeline.isAlwaysCalculatePixelWidths()) {
content.getStyle().setWidth(timeline.getResolutionWidth(), Unit.PX);
}
}
private int getElementHeightWithMargin(Element div) {
int height = div.getClientHeight();
double marginHeight = 0;
marginHeight = getMarginByComputedStyle(div);
return height + (int) Math.round(marginHeight);
}
private boolean isBetween(int v, int min, int max) {
return v >= min && v <= max;
}
private void updateMoveElementFor(Element target) {
if (target == null) {
moveElement.getStyle().setDisplay(Display.NONE);
}
moveElement.getStyle().clearDisplay();
String styleLeft = target.getStyle().getLeft();
// user capturePointLeftPx as default
double left = capturePointLeftPx;
if (styleLeft != null && styleLeft.length() > 2 && styleLeft.endsWith("px")) {
// if target's 'left' is pixel value like '123px', use that.
// When target is already moved, then it's using pixel values. If
// it's not moved yet, it may use percentage value.
left = parseSize(styleLeft, "px");
}
if (isSubBar(target)) {
left += target.getParentElement().getOffsetLeft();
}
moveElement.getStyle().setProperty("left", left + "px");
moveElement.getStyle().setProperty("width", target.getClientWidth() + "px");
}
private void hideMoveElement() {
moveElement.getStyle().setDisplay(Display.NONE);
}
private void internalMoveOrResizeCompleted(Element bar, Element newPosition, boolean move, NativeEvent event) {
String stepUid = getStepUid(bar);
String newStepUid = stepUid;
if (newPosition != null && bar != newPosition) {
newStepUid = getStepUid(newPosition);
}
boolean subBar = isSubBar(bar);
long ownerStartDate = 0;
long ownerEndDate = 0;
double left = parseSize(bar.getStyle().getLeft(), "px");
if (subBar) {
double ownerLeft = bar.getParentElement().getOffsetLeft();
left += ownerLeft;
ownerStartDate = timeline.getDateForLeftPosition(ownerLeft);
ownerLeft += GanttUtil.getBoundingClientRectWidth(bar.getParentElement());
ownerEndDate = timeline.getDateForLeftPosition(ownerLeft);
}
long startDate = timeline.getDateForLeftPosition(left);
left += GanttUtil.getBoundingClientRectWidth(bar);
long endDate = timeline.getDateForLeftPosition(left);
// update left-position to percentage (so that it scales again)
if (subBar) {
updateBarPercentagePosition(startDate, endDate, ownerStartDate, ownerEndDate, bar);
} else {
updateBarPercentagePosition(startDate, endDate, bar);
}
if (move) {
if (isMovableStepsBetweenRows() && stepUid == newStepUid) {
resetBarYPosition(bar);
}
getRpc().onMove(stepUid, newStepUid, startDate, endDate, event, bar);
} else {
getRpc().onResize(stepUid, startDate, endDate, event, bar);
}
}
private void registerBarEventListener(final DivElement bar) {
Event.sinkEvents(bar, Event.ONSCROLL | Event.MOUSEEVENTS | Event.TOUCHEVENTS);
}
private boolean isBar(NativeEvent event) {
Element element = getBar(event);
return element != null;
}
private double parseSize(String size, String suffix) {
if (size == null || size.length() == 0 || "0".equals(size) || "0.0".equals(size)) {
return 0;
}
return Double.parseDouble(size.substring(0, size.length() - suffix.length()));
}
private int getStepIndex(Element parent, Element child) {
// return child index minus additional elements in the content
return getChildIndex(parent, child) - getAdditonalContentElementCount();
}
private int getAdditonalContentElementCount() {
// moveElement and background element inside the content is noticed.
// extraContentElements are also noticed.
return getAdditionalNonWidgetContentElementCount() + extraContentElements.size();
}
private int isMoveElementAttached() {
return moveElement.hasParentElement() ? 1 : 0;
}
private int getAdditionalNonWidgetContentElementCount() {
return isMoveElementAttached() + isBgGridAttached();
}
private int getAdditionalWidgetContentElementCount() {
return extraContentElements.size();
}
/**
* Return the number of background grid related elements in the content. May
* return 0...n, depending on which browser is used.
*/
private int isBgGridAttached() {
return (bgGrid != null && bgGrid.isAttached()) ? 1 : 0;
}
private int getChildIndex(Element parent, Element child) {
return DOM.getChildIndex(com.google.gwt.dom.client.Element.as(parent),
com.google.gwt.dom.client.Element.as(child));
}
private boolean detectResizing(Element bar) {
return isResizableStep(bar) && !hasSubBars(bar) && (isResizingLeft(bar) || isResizingRight(bar));
}
private boolean isMoveOrResizingInProgress() {
return moveInProgress || resizingInProgress;
}
private boolean isResizableStep(Element bar) {
if (!isResizableSteps()) {
return false;
}
AbstractStepWidget step = getAbstractStepWidget(bar);
return step != null && step.getStep() != null && step.getStep().isResizable();
}
private boolean isMovableStep(Element bar) {
if (!isMovableSteps()) {
return false;
}
AbstractStepWidget step = getAbstractStepWidget(bar);
return step != null && step.getStep() != null && step.getStep().isMovable();
}
private boolean isResizingLeft(Element bar) {
if (movePoint.getX() <= (bar.getAbsoluteLeft() + RESIZE_WIDTH)) {
return true;
}
return false;
}
private boolean isResizingRight(Element bar) {
if (movePoint.getX() >= (bar.getAbsoluteRight() + -RESIZE_WIDTH)) {
return true;
}
return false;
}
private void addResizingStyles(Element bar) {
bar.addClassName(STYLE_RESIZING);
updateMoveElementFor(bar);
}
private void removeResizingStyles(Element bar) {
bar.removeClassName(STYLE_RESIZING);
}
private void showResizingPointer(Element bar, boolean showPointer) {
if (showPointer) {
bar.getStyle().setCursor(Cursor.E_RESIZE);
} else {
bar.getStyle().clearCursor();
}
}
private void addMovingStyles(Element bar) {
if (bar == null) {
return;
}
bar.addClassName(STYLE_MOVING);
updateMoveElementFor(bar);
}
private void removeMovingStyles(Element bar) {
bar.removeClassName(STYLE_MOVING);
}
private void updateBarResizingRight(Element bar, double deltax) {
double newWidth = capturePointWidthPx + deltax;
if (newWidth >= BAR_MIN_WIDTH) {
bar.getStyle().setLeft(capturePointLeftPx, Unit.PX);
bar.getStyle().setWidth(newWidth, Unit.PX);
}
}
private void updateBarResizingLeft(Element bar, double deltax) {
double newLeft = capturePointLeftPx + deltax;
double newWidth = capturePointWidthPx - deltax;
if (newWidth >= BAR_MIN_WIDTH) {
bar.getStyle().setLeft(newLeft, Unit.PX);
bar.getStyle().setWidth(newWidth, Unit.PX);
}
}
private void updateBarMovingPosition(Element bar, double deltax) {
bar.getStyle().setLeft(capturePointLeftPx + deltax, Unit.PX);
}
private void updateBarYPosition(Element bar, double deltay) {
int barHeight = getElementHeightWithMargin(bar);
double offsetY = 0; // offset from content top edge
if (isSubBar(bar)) {
Element stepElement = bar.getParentElement();
offsetY = parseSize(stepElement.getStyle().getTop(), "px");
}
double barTop = parseSize(bar.getStyle().getTop(), "px") + offsetY;
double movementFromTop = capturePointTopPx + offsetY + deltay;
double deltaTop = movementFromTop - barTop;
double maxDeltaUp = capturePoint.getY() - capturePointAbsTopPx;
double maxDeltaDown = barHeight - maxDeltaUp;
if (deltaTop <= (-1 * maxDeltaUp)) {
// move up
if ((barTop - barHeight) >= 0) {
bar.getStyle().setTop(barTop - barHeight - offsetY, Unit.PX);
}
} else if (deltaTop >= maxDeltaDown) {
// move down
bar.getStyle().setTop(barTop + barHeight - offsetY, Unit.PX);
}
}
private void resetBarPosition(Element bar) {
bar.getStyle().setBackgroundColor(capturePointBgColor);
bar.getStyle().setProperty("left", capturePointLeftPercentage);
bar.getStyle().setProperty("width", capturePointWidthPercentage);
resetBarYPosition(bar);
}
private void resetBarYPosition(Element bar) {
bar.getStyle().setTop(capturePointTopPx, Unit.PX);
}
private void disableClickOnNextMouseUp() {
clickOnNextMouseUp = false;
}
private void cancelDoubleClickDetection() {
insideDoubleClickDetectionInterval = false;
numberOfMouseClicksDetected = 0;
previousMouseUpEvent = null;
previousMouseUpBarElement = null;
}
private boolean isClickOnNextMouseup() {
return clickOnNextMouseUp;
}
private boolean isResizingInProgress() {
return resizingInProgress;
}
private int getHorizontalScrollbarSpacerHeight() {
if (scrollbarSpacer.getStyle().getDisplay().isEmpty()) {
return scrollbarSpacer.getClientHeight();
}
return 0;
}
private double getContentWidth() {
return getBoundingClientRectWidth(content);
}
}