/*
* Copyright 2014-2015 CyberVision, Inc.
*
* 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.kaaproject.avro.ui.gwt.client.widget;
import java.util.ArrayList;
import java.util.List;
import org.kaaproject.avro.ui.gwt.client.AvroUiResources.AvroUiStyle;
import org.kaaproject.avro.ui.gwt.client.util.Utils;
import org.kaaproject.avro.ui.gwt.client.widget.ResizePanel.PanelResizeListener;
import org.kaaproject.avro.ui.gwt.client.widget.nav.NavigationAction;
import org.kaaproject.avro.ui.gwt.client.widget.nav.NavigationActionListener;
import org.kaaproject.avro.ui.gwt.client.widget.nav.NavigationContainer;
import org.kaaproject.avro.ui.gwt.client.widget.nav.NavigationElement;
import org.kaaproject.avro.ui.gwt.client.widget.nav.NavigationPanel;
import org.kaaproject.avro.ui.gwt.client.widget.nav.NavigationPanel.ZoomListener;
import org.kaaproject.avro.ui.shared.ArrayField;
import org.kaaproject.avro.ui.shared.FieldType;
import org.kaaproject.avro.ui.shared.FormField;
import org.kaaproject.avro.ui.shared.RecordField;
import org.kaaproject.avro.ui.shared.UnionField;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.layout.client.Layout.Alignment;
import com.google.gwt.layout.client.Layout.AnimationCallback;
import com.google.gwt.layout.client.Layout.Layer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DeckLayoutPanel;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.LayoutPanel;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;
public class RecordFieldWidget extends AbstractFieldWidget<RecordField> implements NavigationContainer {
private static final int NAVIGATION_HEADER_HEIGHT = 55;
private static final int FRAGMENT_SWITCH_ANIMATION_DURATION = 500;
private static final String MAX_HEIGHT = "550px";
private ResizePanel resizePanel;
private LayoutPanel rootPanel;
private NavigationPanel navPanel;
private FragmentLayoutPanel fragmentPanel;
private FlexTable table = new FlexTable();
private FieldWidgetPanel fieldWidgetPanel;
private List<NavigationElement> navElements;
private boolean isRoot;
private boolean isAnimating = false;
private boolean forceNavigation = false;
private boolean navigationDisabled = false;
private boolean isLayoutComplete = false;
private int preferredWidthPx = -1;
private int preferredHeightPx = -1;
private String lastWidth = null;
private String lastHeight = null;
public RecordFieldWidget(AvroWidgetsConfig config, AvroUiStyle style, boolean readOnly) {
this(config, style, null, readOnly);
}
public RecordFieldWidget(AvroWidgetsConfig config, AvroUiStyle style, NavigationContainer container, boolean readOnly) {
super(config, style, container, readOnly);
this.isRoot = container == null;
init();
}
public RecordFieldWidget(AvroWidgetsConfig config) {
this(config, false);
}
public RecordFieldWidget(AvroWidgetsConfig config, boolean readOnly) {
this(config, (NavigationContainer)null, readOnly);
}
public RecordFieldWidget(AvroWidgetsConfig config, NavigationContainer container, boolean readOnly) {
super(config, container, readOnly);
this.isRoot = container == null;
init();
}
public void setForceNavigation(boolean force) {
this.forceNavigation = force;
}
public void setReadOnly(boolean readOnly) {
if (this.readOnly != readOnly) {
this.readOnly = readOnly;
RecordField currentValue = value;
setValue(null);
setValue(currentValue);
}
}
public void setPreferredHeightPx(int height) {
preferredHeightPx = height;
doLayout();
}
public void setPreferredWidthPx(int width) {
preferredWidthPx = width;
doLayout();
}
public static boolean isNavigationNeeded(RecordField recordField) {
if (recordField != null && recordField.getValue() != null) {
for (FormField field : recordField.getValue()) {
if (isNavigationNeeded(field)) {
return true;
}
}
}
return false;
}
private static boolean isNavigationNeeded(FormField field) {
FieldType type = field.getFieldType();
if (type.isComplex()) {
if (type == FieldType.RECORD) {
return true;
} else if (type == FieldType.ARRAY) {
field.finalizeMetadata();
if (ArrayFieldWidget.isGridNeeded((ArrayField)field)) {
return true;
}
} else if (type == FieldType.UNION) {
field.finalizeMetadata();
for (FormField unionValue : ((UnionField)field).getAcceptableValues()) {
if (unionValue.getFieldType() == FieldType.UNION) {
return true;
} else if (unionValue.getFieldType() == FieldType.RECORD) {
if (isNavigationNeeded((RecordField)unionValue)) {
return true;
}
}
}
}
}
return false;
}
private void init() {
if (isRoot) {
setNavigationContainer(this);
}
}
@Override
public void setHeight(String height) {
if (resizePanel != null && rootPanel != null) {
resizePanel.setHeight(height);
rootPanel.setHeight(height);
super.setHeight("100%");
lastHeight = height;
} else {
super.setHeight(height);
}
}
@Override
public void setWidth(String width) {
if (resizePanel != null && rootPanel != null) {
resizePanel.setWidth(width);
rootPanel.setWidth(width);
super.setWidth("100%");
lastWidth = width;
} else {
super.setWidth(width);
}
}
public void enableZoom(boolean enable) {
if (navPanel != null) {
navPanel.enableZoom(enable);
}
}
public Widget getAnchorWidget() {
return resizePanel;
}
private void initNavigation() {
if (resizePanel == null) {
resizePanel = new ResizePanel(style);
rootPanel = new LayoutPanel();
navPanel = new NavigationPanel();
navPanel.setZoomListener(new ZoomListener() {
@Override
public void onZoom() {
final int prevPreferredWidthPx = preferredWidthPx;
final int prevPreferredHeightPx = preferredHeightPx;
final String prevWidth;
if (lastWidth != null) {
prevWidth = lastWidth;
} else {
prevWidth = resizePanel.getElement().getClientWidth() + "px";
}
final String prevHeight;
if (lastHeight != null) {
prevHeight = lastHeight;
} else {
prevHeight = resizePanel.getElement().getClientHeight() + "px";
}
final AvroWidgetsConfig prevConfig = config;
final FormPopup popup = new FormPopup();
popup.setTitle(value.getDisplayName());
int dWidth = Window.getClientWidth() - 150;
int dHeight = Window.getClientHeight() - 200;
AvroWidgetsConfig config = new AvroWidgetsConfig.Builder().recordPanelWidth(dWidth-100).
gridHeight(dHeight-350).tableHeight(dHeight-370).createConfig();
enableZoom(false);
setPreferredWidthPx(dWidth);
setPreferredHeightPx(dHeight);
updateConfig(config);
popup.add(getAnchorWidget());
Button close = new Button(Utils.constants.close(), new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
popup.hide();
}
});
popup.addButton(close);
popup.addCloseHandler(new CloseHandler<PopupPanel>() {
@Override
public void onClose(CloseEvent<PopupPanel> event) {
enableZoom(true);
setWidget(getAnchorWidget());
updateConfig(prevConfig);
setPreferredWidthPx(prevPreferredWidthPx);
setPreferredHeightPx(prevPreferredHeightPx);
if (prevWidth != null) {
setWidth(prevWidth);
}
if (prevHeight != null) {
setHeight(prevHeight);
}
traverseShown((HasWidgets)getAnchorWidget());
}
});
popup.center();
popup.show();
traverseShown((HasWidgets)getAnchorWidget());
}
});
fragmentPanel = new FragmentLayoutPanel();
fragmentPanel.setAnimationDuration(FRAGMENT_SWITCH_ANIMATION_DURATION);
rootPanel.add(navPanel);
rootPanel.setWidgetLeftRight(navPanel, 0, Unit.PX, 0, Unit.PX);
rootPanel.setWidgetTopHeight(navPanel, 0, Unit.PX, NAVIGATION_HEADER_HEIGHT, Unit.PX);
rootPanel.setWidgetVerticalPosition(navPanel, Alignment.END);
rootPanel.add(fragmentPanel);
rootPanel.setWidgetLeftRight(fragmentPanel, 0, Unit.PX, 0, Unit.PX);
rootPanel.setWidgetTopBottom(fragmentPanel, NAVIGATION_HEADER_HEIGHT, Unit.PX, 0, Unit.PX);
rootPanel.setWidgetVerticalPosition(fragmentPanel, Alignment.STRETCH);
resizePanel.add(rootPanel);
resizePanel.setWidth(FULL_WIDTH);
resizePanel.addPanelResizedListener(new PanelResizeListener() {
@Override
public void onResized(int width, int height) {
setWidth(width + "px");
setHeight(height + "px");
}
});
navElements = new ArrayList<>();
}
}
private void clearNavigation() {
if (resizePanel != null) {
resizePanel = null;
rootPanel = null;
navPanel.clear();
navPanel = null;
fragmentPanel = null;
navElements.clear();
navElements = null;
}
}
private void doLayout () {
if (isRoot) {
Element element = getElement();
element.getStyle().clearOverflow();
element.getStyle().clearHeight();
if (element.getParentElement() != null) {
element.getParentElement().getStyle().clearProperty("minHeight");
}
if (preferredWidthPx > 0 || preferredHeightPx > 0) {
if (preferredWidthPx > 0) {
setWidth(preferredWidthPx + "px");
}
if (preferredHeightPx > 0) {
setHeight(preferredHeightPx + "px");
if (element.getParentElement() != null) {
Element parentElement = element.getParentElement();
parentElement.getStyle().setPropertyPx("minHeight", childsOffsetHeight(parentElement));
}
}
if (navigationDisabled) {
element.getStyle().setOverflow(Overflow.AUTO);
}
} else if (!navigationDisabled) {
setHeight(MAX_HEIGHT);
}
}
}
private static int childsOffsetHeight(Element element) {
int height = 0;
for (int i=0; i<element.getChildNodes().getLength();i++) {
Node node = element.getChildNodes().getItem(i);
if (Element.is(node)) {
Element childElement = Element.as(node);
height += childElement.getOffsetHeight();
}
}
return height;
}
private static int maxHeight(Element element) {
int height = getInnerHeight(element);
for (int i=0; i<element.getChildNodes().getLength();i++) {
Node node = element.getChildNodes().getItem(i);
if (Element.is(node)) {
Element childElement = Element.as(node);
if (isElementVisible(element)) {
height = Math.max(height, maxHeight(childElement));
}
}
}
return height;
}
private static boolean isElementVisible(Element element) {
if (UIObject.isVisible(element)) {
String visibility = element.getStyle().getVisibility();
if (visibility == null || !visibility.equals(Visibility.HIDDEN.getCssName())) {
return true;
}
}
return false;
}
private static int getInnerHeight(Element element) {
int height = element.getClientHeight();
if (height == 0) {
height = getComputedStylePropertyPixels(element, "height");
}
if (height == 0) {
height = getComputedStylePropertyPixels(element, "min-height");
}
height -= getComputedStylePropertyPixels(element, "padding-top");
height -= getComputedStylePropertyPixels(element, "padding-bottom");
height -= getComputedStylePropertyPixels(element, "border-top-width");
height -= getComputedStylePropertyPixels(element, "border-bottom-width");
return height;
}
private static int getComputedStylePropertyPixels(Element element, String prop) {
int propInt = 0;
String propString = getComputedStyleProperty(element, prop);
if (!Utils.isBlank(propString) && propString.endsWith("px")) {
try {
propInt = (int)Double.parseDouble(propString.substring(0, propString.length()-2));
} catch (NumberFormatException e) {}
}
return propInt;
}
private static native String getComputedStyleProperty(Element el, String prop) /*-{
var computedStyle;
if (document.defaultView && document.defaultView.getComputedStyle) { // standard (includes ie9)
computedStyle = document.defaultView.getComputedStyle(el, null)[prop];
} else if (el.currentStyle) { // IE older
computedStyle = el.currentStyle[prop];
} else { // inline style
computedStyle = el.style[prop];
}
return computedStyle;
}-*/;
@Override
protected Widget constructForm() {
Widget form;
if (isRoot) {
isLayoutComplete = false;
}
navigationDisabled = isRoot && !(forceNavigation || isNavigationNeeded(value));
table.getColumnFormatter().setWidth(0, config.getLabelsColumnWidth());
table.getColumnFormatter().setWidth(1, config.getFieldsColumnWidth());
constructFormData(table, value, registrations);
if (isRoot && !navigationDisabled) {
isAnimating = false;
initNavigation();
navPanel.clearNavElements();
navElements.clear();
fragmentPanel.clear();
if (value != null) {
navPanel.setVisible(true);
showField(value, null);
} else {
navPanel.setVisible(false);
}
form = resizePanel;
} else {
clearNavigation();
if (value != null && value.isOverride()) {
fieldWidgetPanel = new FieldWidgetPanel(style, value, readOnly, true);
fieldWidgetPanel.setWidth(config.getRecordPanelWidth());
if (value.isOverride() && !readOnly && !value.isReadOnly()) {
fieldWidgetPanel.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
@Override
public void onValueChange(ValueChangeEvent<Boolean> event) {
fireChanged();
}
});
}
fieldWidgetPanel.setContent(table);
form = fieldWidgetPanel;
} else {
form = table;
}
}
return form;
}
@Override
public void updateConfig(AvroWidgetsConfig config) {
super.updateConfig(config);
if (table != null) {
table.getColumnFormatter().setWidth(0, config.getLabelsColumnWidth());
table.getColumnFormatter().setWidth(1, config.getFieldsColumnWidth());
}
if (fieldWidgetPanel != null) {
fieldWidgetPanel.setWidth(config.getRecordPanelWidth());
}
}
@Override
protected Widget constructLabel(FlexTable table, FormField field, int row,
int column) {
Widget label = super.constructLabel(table, field, row, column);
if (!navigationDisabled) {
label.addStyleName(style.padded());
}
return label;
}
@Override
protected int placeWidget(FlexTable table, FieldType type, Widget widget,
int row, int column, List<HandlerRegistration> handlerRegistrations) {
if (!navigationDisabled && (!type.isComplex() || shouldPlaceNestedWidgetButton(type))) {
widget.addStyleName(style.padded());
}
return super.placeWidget(table, type, widget, row, column, handlerRegistrations);
}
@Override
public void goBack() {
gotoIndex(navElements.size()-2);
}
@Override
public void gotoIndex(final int gotoIndex) {
final int index = confirmIndex(gotoIndex);
if (!isAnimating && index < navElements.size()-1) {
final NavigationElement navElement = navElements.get(index);
isAnimating = true;
fragmentPanel.setAnimationCallback(new AnimationCallback() {
@Override
public void onLayout(Layer layer, double progress) {
if (progress == 0 && navElement.getWidget().equals(layer.getUserObject())) {
navElement.onShown();
}
}
@Override
public void onAnimationComplete() {
for (NavigationElement oldNavElement : navElements.subList(index+1, navElements.size())) {
navPanel.removeNavElement(oldNavElement.getLink());
fragmentPanel.remove(oldNavElement.getWidget());
}
navElements = navElements.subList(0, index+1);
fragmentPanel.setAnimationCallback(null);
if (!readOnly) {
fireChanged();
}
isAnimating = false;
}
});
fragmentPanel.showWidget(navElement.getIndex());
}
}
private int confirmIndex(int index) {
int confirmedIndex = index;
for (int i=navElements.size()-1;i>index;i--) {
NavigationElement navElement = navElements.get(i);
String mayClose = navElement.mayClose();
if (mayClose != null && !Window.confirm(mayClose)) {
confirmedIndex = i;
break;
}
}
return confirmedIndex;
}
@Override
public void showField(FormField field, NavigationActionListener listener) {
if (!isAnimating) {
NavigationAction action = (readOnly || field.isReadOnly()) ? NavigationAction.VIEW : NavigationAction.EDIT;
constructNavigationElement(field, action, listener);
}
}
@Override
public void addNewField(FormField field, NavigationActionListener listener) {
if (!isAnimating) {
constructNavigationElement(field, NavigationAction.ADD, listener);
fireChanged();
}
}
@Override
public boolean validate() {
boolean valid = true;
if (navElements != null) {
for (NavigationElement navElement : navElements) {
valid &= navElement.isAdded();
}
}
if (valid) {
valid &= super.validate();
}
return valid;
}
private void constructNavigationElement(FormField field, NavigationAction action, final NavigationActionListener listener) {
final NavigationElement navElement = new NavigationElement(config, style, this, navElements.size(),
field, action,
new NavigationActionListener() {
@Override
public void onAdded(FormField field) {
if (listener != null) {
listener.onAdded(field);
}
fireChanged();
}
@Override
public void onChanged(FormField field) {
if (listener != null) {
listener.onChanged(field);
}
fireChanged();
}
});
navElements.add(navElement);
navPanel.addNavElement(navElement.getLink());
fragmentPanel.add(navElement.getWidget());
final boolean doLayout = navElements.size()==1 && !isLayoutComplete;
isAnimating = true;
fragmentPanel.setAnimationCallback(new AnimationCallback() {
@Override
public void onLayout(Layer layer, double progress) {
if (progress == 0 && navElement.getWidget().equals(layer.getUserObject())) {
navElement.onShown();
}
}
@Override
public void onAnimationComplete() {
if (doLayout && !isLayoutComplete && isAnimating) {
doLayout();
isLayoutComplete = true;
fragmentPanel.remove(navElement.getIndex());
fragmentPanel.setAnimationDuration(FRAGMENT_SWITCH_ANIMATION_DURATION);
fragmentPanel.add(navElement.getWidget());
fragmentPanel.showWidget(navElement.getIndex());
} else {
fragmentPanel.setAnimationCallback(null);
isAnimating = false;
}
}
});
if (doLayout) {
fragmentPanel.setAnimationDuration(0);
fragmentPanel.showWidget(navElement.getIndex());
} else {
fragmentPanel.showWidget(navElement.getIndex());
}
}
private class FragmentLayoutPanel extends DeckLayoutPanel {
private AnimationCallback animationCallback;
private FragmentLayoutPanel() {
super();
}
private void setAnimationCallback(AnimationCallback animationCallback) {
this.animationCallback = animationCallback;
}
@Override
public void animate(int duration) {
super.animate(duration, animationCallback);
}
}
}