if (!cc.getParent().getChildren().contains(cc)) {
VConsole.error("ERROR: Connector is connected to a parent but the parent does not contain the connector");
}
- } else if ((cc instanceof RootConnector && cc == getView())) {
+ } else if ((cc instanceof RootConnector && cc == getRootConnector())) {
// RootConnector for this connection, leave as-is
} else if (cc instanceof WindowConnector
- && getRootConnector().hasSubWindow((WindowConnector) cc)) {
- && getView().hasSubWindow((WindowConnector) cc)) {
++ && getRootConnector().hasSubWindow(
++ (WindowConnector) cc)) {
// Sub window attached to this RootConnector, leave
// as-is
} else {
// RootConnector has been created but not
// initialized as the connector id has not been
// known
- connectorMap.registerConnector(connectorId, rootConnector);
- rootConnector.doInit(connectorId, ApplicationConnection.this);
- connectorMap.registerConnector(connectorId, view);
- view.doInit(connectorId, ApplicationConnection.this);
++ connectorMap.registerConnector(connectorId,
++ rootConnector);
++ rootConnector.doInit(connectorId,
++ ApplicationConnection.this);
}
} catch (final Throwable e) {
VConsole.error(e);
import com.vaadin.terminal.gwt.client.ui.ManagedLayout;
import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
-import com.vaadin.terminal.gwt.client.ui.VNotification;
+ import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeEvent;
+ import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeListener;
+ import com.vaadin.terminal.gwt.client.ui.layout.LayoutDependencyTree;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
public class LayoutManager {
private static final String LOOP_ABORT_MESSAGE = "Aborting layout after 100 passes. This would probably be an infinite loop.";
--- /dev/null
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.absolutelayout;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickRPC;
+import com.vaadin.terminal.gwt.client.ui.absolutelayout.VAbsoluteLayout.AbsoluteWrapper;
+import com.vaadin.ui.AbsoluteLayout;
+
+@Component(AbsoluteLayout.class)
+public class AbsoluteLayoutConnector extends
+ AbstractComponentContainerConnector implements DirectionalManagedLayout {
+
+ private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler(
+ this) {
+
+ @Override
+ protected ComponentConnector getChildComponent(Element element) {
+ return getConnectorForElement(element);
+ }
+
+ @Override
+ protected LayoutClickRPC getLayoutClickRPC() {
+ return rpc;
+ };
+
+ };
+
+ private AbsoluteLayoutServerRPC rpc;
+
+ private Map<String, AbsoluteWrapper> connectorIdToComponentWrapper = new HashMap<String, AbsoluteWrapper>();
+
+ @Override
+ protected void init() {
+ super.init();
+ rpc = RpcProxy.create(AbsoluteLayoutServerRPC.class, this);
+ }
+
+ /**
+ * Returns the deepest nested child component which contains "element". The
+ * child component is also returned if "element" is part of its caption.
+ *
+ * @param element
+ * An element that is a nested sub element of the root element in
+ * this layout
+ * @return The Paintable which the element is a part of. Null if the element
+ * belongs to the layout and not to a child.
+ */
+ protected ComponentConnector getConnectorForElement(Element element) {
+ return Util.getConnectorForElement(getConnection(), getWidget(),
+ element);
+ }
+
+ public void updateCaption(ComponentConnector component) {
+ VAbsoluteLayout absoluteLayoutWidget = getWidget();
+ AbsoluteWrapper componentWrapper = getWrapper(component);
+
+ boolean captionIsNeeded = VCaption.isNeeded(component.getState());
+
+ VCaption caption = componentWrapper.getCaption();
+
+ if (captionIsNeeded) {
+ if (caption == null) {
+ caption = new VCaption(component, getConnection());
+ absoluteLayoutWidget.add(caption);
+ componentWrapper.setCaption(caption);
+ }
+ caption.updateCaption();
+ componentWrapper.updateCaptionPosition();
+ } else {
+ if (caption != null) {
+ caption.removeFromParent();
+ }
+ }
+
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VAbsoluteLayout.class);
+ }
+
+ @Override
+ public VAbsoluteLayout getWidget() {
+ return (VAbsoluteLayout) super.getWidget();
+ }
+
+ @Override
+ public AbsoluteLayoutState getState() {
+ return (AbsoluteLayoutState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+ clickEventHandler.handleEventHandlerRegistration();
+
+ // TODO Margin handling
+
+ for (ComponentConnector child : getChildren()) {
+ getWrapper(child).setPosition(
+ getState().getConnectorPosition(child));
+ }
+ };
+
+ private AbsoluteWrapper getWrapper(ComponentConnector child) {
+ String childId = child.getConnectorId();
+ AbsoluteWrapper wrapper = connectorIdToComponentWrapper.get(childId);
+ if (wrapper != null) {
+ return wrapper;
+ }
+
+ wrapper = new AbsoluteWrapper(child.getWidget());
+ connectorIdToComponentWrapper.put(childId, wrapper);
+ getWidget().add(wrapper);
+ return wrapper;
+
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ for (ComponentConnector child : getChildren()) {
+ getWrapper(child);
+ }
+
+ for (ComponentConnector oldChild : event.getOldChildren()) {
+ if (oldChild.getParent() != this) {
+ String connectorId = oldChild.getConnectorId();
+ AbsoluteWrapper absoluteWrapper = connectorIdToComponentWrapper
+ .remove(connectorId);
+ absoluteWrapper.destroy();
+ }
+ }
+ }
+
+ public void layoutVertically() {
+ VAbsoluteLayout layout = getWidget();
+ for (ComponentConnector paintable : getChildren()) {
+ Widget widget = paintable.getWidget();
+ AbsoluteWrapper wrapper = (AbsoluteWrapper) widget.getParent();
+ Style wrapperStyle = wrapper.getElement().getStyle();
+
+ if (paintable.isRelativeHeight()) {
+ int h;
+ if (wrapper.top != null && wrapper.bottom != null) {
+ h = wrapper.getOffsetHeight();
+ } else if (wrapper.bottom != null) {
+ // top not defined, available space 0... bottom of
+ // wrapper
+ h = wrapper.getElement().getOffsetTop()
+ + wrapper.getOffsetHeight();
+ } else {
+ // top defined or both undefined, available space ==
+ // canvas - top
+ h = layout.canvas.getOffsetHeight()
+ - wrapper.getElement().getOffsetTop();
+ }
+ wrapperStyle.setHeight(h, Unit.PX);
++ getLayoutManager().reportHeightAssignedToRelative(paintable, h);
+ } else {
+ wrapperStyle.clearHeight();
+ }
+
+ wrapper.updateCaptionPosition();
+ }
+ }
+
+ public void layoutHorizontally() {
+ VAbsoluteLayout layout = getWidget();
+ for (ComponentConnector paintable : getChildren()) {
+ AbsoluteWrapper wrapper = getWrapper(paintable);
+ Style wrapperStyle = wrapper.getElement().getStyle();
+
+ if (paintable.isRelativeWidth()) {
+ int w;
+ if (wrapper.left != null && wrapper.right != null) {
+ w = wrapper.getOffsetWidth();
+ } else if (wrapper.right != null) {
+ // left == null
+ // available width == right edge == offsetleft + width
+ w = wrapper.getOffsetWidth()
+ + wrapper.getElement().getOffsetLeft();
+ } else {
+ // left != null && right == null || left == null &&
+ // right == null
+ // available width == canvas width - offset left
+ w = layout.canvas.getOffsetWidth()
+ - wrapper.getElement().getOffsetLeft();
+ }
+ wrapperStyle.setWidth(w, Unit.PX);
++ getLayoutManager().reportWidthAssignedToRelative(paintable, w);
+ } else {
+ wrapperStyle.clearWidth();
+ }
+
+ wrapper.updateCaptionPosition();
+ }
+ }
+}
--- /dev/null
- SimpleManagedLayout {
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.accordion;
+
+import java.util.Iterator;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.accordion.VAccordion.StackItem;
++import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.terminal.gwt.client.ui.tabsheet.TabsheetBaseConnector;
+import com.vaadin.ui.Accordion;
+
+@Component(Accordion.class)
+public class AccordionConnector extends TabsheetBaseConnector implements
++ SimpleManagedLayout, MayScrollChildren {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().selectedUIDLItemIndex = -1;
+ super.updateFromUIDL(uidl, client);
+ /*
+ * Render content after all tabs have been created and we know how large
+ * the content area is
+ */
+ if (getWidget().selectedUIDLItemIndex >= 0) {
+ StackItem selectedItem = getWidget().getStackItem(
+ getWidget().selectedUIDLItemIndex);
+ UIDL selectedTabUIDL = getWidget().lazyUpdateMap
+ .remove(selectedItem);
+ getWidget().open(getWidget().selectedUIDLItemIndex);
+
+ selectedItem.setContent(selectedTabUIDL);
+ } else if (isRealUpdate(uidl) && getWidget().openTab != null) {
+ getWidget().close(getWidget().openTab);
+ }
+
+ getWidget().iLayout();
+ // finally render possible hidden tabs
+ if (getWidget().lazyUpdateMap.size() > 0) {
+ for (Iterator iterator = getWidget().lazyUpdateMap.keySet()
+ .iterator(); iterator.hasNext();) {
+ StackItem item = (StackItem) iterator.next();
+ item.setContent(getWidget().lazyUpdateMap.get(item));
+ }
+ getWidget().lazyUpdateMap.clear();
+ }
+
+ }
+
+ @Override
+ public VAccordion getWidget() {
+ return (VAccordion) super.getWidget();
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VAccordion.class);
+ }
+
+ public void updateCaption(ComponentConnector component) {
+ /* Accordion does not render its children's captions */
+ }
+
+ public void layout() {
+ VAccordion accordion = getWidget();
+
+ accordion.updateOpenTabSize();
+
+ if (isUndefinedHeight()) {
+ accordion.openTab.setHeightFromWidget();
+ }
+ accordion.iLayout();
+
+ }
+
+}
--- /dev/null
-
- Util.runWebkitOverflowAutoFix(openTab.getContainerElement());
-
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.accordion;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.ui.tabsheet.TabsheetBaseConnector;
+import com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheetBase;
+
+public class VAccordion extends VTabsheetBase {
+
+ public static final String CLASSNAME = "v-accordion";
+
+ private Set<Widget> widgets = new HashSet<Widget>();
+
+ HashMap<StackItem, UIDL> lazyUpdateMap = new HashMap<StackItem, UIDL>();
+
+ StackItem openTab = null;
+
+ int selectedUIDLItemIndex = -1;
+
+ public VAccordion() {
+ super(CLASSNAME);
+ }
+
+ @Override
+ protected void renderTab(UIDL tabUidl, int index, boolean selected,
+ boolean hidden) {
+ StackItem item;
+ int itemIndex;
+ if (getWidgetCount() <= index) {
+ // Create stackItem and render caption
+ item = new StackItem(tabUidl);
+ if (getWidgetCount() == 0) {
+ item.addStyleDependentName("first");
+ }
+ itemIndex = getWidgetCount();
+ add(item, getElement());
+ } else {
+ item = getStackItem(index);
+ item = moveStackItemIfNeeded(item, index, tabUidl);
+ itemIndex = index;
+ }
+ item.updateCaption(tabUidl);
+
+ item.setVisible(!hidden);
+
+ if (selected) {
+ selectedUIDLItemIndex = itemIndex;
+ }
+
+ if (tabUidl.getChildCount() > 0) {
+ lazyUpdateMap.put(item, tabUidl.getChildUIDL(0));
+ }
+ }
+
+ /**
+ * This method tries to find out if a tab has been rendered with a different
+ * index previously. If this is the case it re-orders the children so the
+ * same StackItem is used for rendering this time. E.g. if the first tab has
+ * been removed all tabs which contain cached content must be moved 1 step
+ * up to preserve the cached content.
+ *
+ * @param item
+ * @param newIndex
+ * @param tabUidl
+ * @return
+ */
+ private StackItem moveStackItemIfNeeded(StackItem item, int newIndex,
+ UIDL tabUidl) {
+ UIDL tabContentUIDL = null;
+ ComponentConnector tabContent = null;
+ if (tabUidl.getChildCount() > 0) {
+ tabContentUIDL = tabUidl.getChildUIDL(0);
+ tabContent = client.getPaintable(tabContentUIDL);
+ }
+
+ Widget itemWidget = item.getComponent();
+ if (tabContent != null) {
+ if (tabContent != itemWidget) {
+ /*
+ * This is not the same widget as before, find out if it has
+ * been moved
+ */
+ int oldIndex = -1;
+ StackItem oldItem = null;
+ for (int i = 0; i < getWidgetCount(); i++) {
+ Widget w = getWidget(i);
+ oldItem = (StackItem) w;
+ if (tabContent == oldItem.getComponent()) {
+ oldIndex = i;
+ break;
+ }
+ }
+
+ if (oldIndex != -1 && oldIndex > newIndex) {
+ /*
+ * The tab has previously been rendered in another position
+ * so we must move the cached content to correct position.
+ * We move only items with oldIndex > newIndex to prevent
+ * moving items already rendered in this update. If for
+ * instance tabs 1,2,3 are removed and added as 3,2,1 we
+ * cannot re-use "1" when we get to the third tab.
+ */
+ insert(oldItem, getElement(), newIndex, true);
+ return oldItem;
+ }
+ }
+ } else {
+ // Tab which has never been loaded. Must assure we use an empty
+ // StackItem
+ Widget oldWidget = item.getComponent();
+ if (oldWidget != null) {
+ oldWidget.removeFromParent();
+ }
+ }
+ return item;
+ }
+
+ void open(int itemIndex) {
+ StackItem item = (StackItem) getWidget(itemIndex);
+ boolean alreadyOpen = false;
+ if (openTab != null) {
+ if (openTab.isOpen()) {
+ if (openTab == item) {
+ alreadyOpen = true;
+ } else {
+ openTab.close();
+ }
+ }
+ }
+
+ if (!alreadyOpen) {
+ item.open();
+ activeTabIndex = itemIndex;
+ openTab = item;
+ }
+
+ // Update the size for the open tab
+ updateOpenTabSize();
+ }
+
+ void close(StackItem item) {
+ if (!item.isOpen()) {
+ return;
+ }
+
+ item.close();
+ activeTabIndex = -1;
+ openTab = null;
+
+ }
+
+ @Override
+ protected void selectTab(final int index, final UIDL contentUidl) {
+ StackItem item = getStackItem(index);
+ if (index != activeTabIndex) {
+ open(index);
+ iLayout();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+
+ }
+ item.setContent(contentUidl);
+ }
+
+ public void onSelectTab(StackItem item) {
+ final int index = getWidgetIndex(item);
+ if (index != activeTabIndex && !disabled && !readonly
+ && !disabledTabKeys.contains(tabKeys.get(index))) {
+ addStyleDependentName("loading");
+ client.updateVariable(id, "selected", "" + tabKeys.get(index), true);
+ }
+ }
+
+ /**
+ * Sets the size of the open tab
+ */
+ void updateOpenTabSize() {
+ if (openTab == null) {
+ return;
+ }
+
+ // WIDTH
+ if (!isDynamicWidth()) {
+ openTab.setWidth("100%");
+ } else {
+ openTab.setWidth(null);
+ }
+
+ // HEIGHT
+ if (!isDynamicHeight()) {
+ int usedPixels = 0;
+ for (Widget w : getChildren()) {
+ StackItem item = (StackItem) w;
+ if (item == openTab) {
+ usedPixels += item.getCaptionHeight();
+ } else {
+ // This includes the captionNode borders
+ usedPixels += item.getHeight();
+ }
+ }
+
+ int offsetHeight = getOffsetHeight();
+
+ int spaceForOpenItem = offsetHeight - usedPixels;
+
+ if (spaceForOpenItem < 0) {
+ spaceForOpenItem = 0;
+ }
+
+ openTab.setHeight(spaceForOpenItem);
+ } else {
+ openTab.setHeightFromWidget();
+
+ }
+
+ }
+
+ public void iLayout() {
+ if (openTab == null) {
+ return;
+ }
+
+ if (isDynamicWidth()) {
+ int maxWidth = 40;
+ for (Widget w : getChildren()) {
+ StackItem si = (StackItem) w;
+ int captionWidth = si.getCaptionWidth();
+ if (captionWidth > maxWidth) {
+ maxWidth = captionWidth;
+ }
+ }
+ int widgetWidth = openTab.getWidgetWidth();
+ if (widgetWidth > maxWidth) {
+ maxWidth = widgetWidth;
+ }
+ super.setWidth(maxWidth + "px");
+ openTab.setWidth(maxWidth);
+ }
+ }
+
+ /**
+ * A StackItem has always two children, Child 0 is a VCaption, Child 1 is
+ * the actual child widget.
+ */
+ protected class StackItem extends ComplexPanel implements ClickHandler {
+
+ public void setHeight(int height) {
+ if (height == -1) {
+ super.setHeight("");
+ DOM.setStyleAttribute(content, "height", "0px");
+ } else {
+ super.setHeight((height + getCaptionHeight()) + "px");
+ DOM.setStyleAttribute(content, "height", height + "px");
+ DOM.setStyleAttribute(content, "top", getCaptionHeight() + "px");
+
+ }
+ }
+
+ public Widget getComponent() {
+ if (getWidgetCount() < 2) {
+ return null;
+ }
+ return getWidget(1);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ }
+
+ public void setHeightFromWidget() {
+ Widget widget = getChildWidget();
+ if (widget == null) {
+ return;
+ }
+
+ int paintableHeight = widget.getElement().getOffsetHeight();
+ setHeight(paintableHeight);
+
+ }
+
+ /**
+ * Returns caption width including padding
+ *
+ * @return
+ */
+ public int getCaptionWidth() {
+ if (caption == null) {
+ return 0;
+ }
+
+ int captionWidth = caption.getRequiredWidth();
+ int padding = Util.measureHorizontalPaddingAndBorder(
+ caption.getElement(), 18);
+ return captionWidth + padding;
+ }
+
+ public void setWidth(int width) {
+ if (width == -1) {
+ super.setWidth("");
+ } else {
+ super.setWidth(width + "px");
+ }
+ }
+
+ public int getHeight() {
+ return getOffsetHeight();
+ }
+
+ public int getCaptionHeight() {
+ return captionNode.getOffsetHeight();
+ }
+
+ private VCaption caption;
+ private boolean open = false;
+ private Element content = DOM.createDiv();
+ private Element captionNode = DOM.createDiv();
+
+ public StackItem(UIDL tabUidl) {
+ setElement(DOM.createDiv());
+ caption = new VCaption(client);
+ caption.addClickHandler(this);
+ super.add(caption, captionNode);
+ DOM.appendChild(captionNode, caption.getElement());
+ DOM.appendChild(getElement(), captionNode);
+ DOM.appendChild(getElement(), content);
+ setStyleName(CLASSNAME + "-item");
+ DOM.setElementProperty(content, "className", CLASSNAME
+ + "-item-content");
+ DOM.setElementProperty(captionNode, "className", CLASSNAME
+ + "-item-caption");
+ close();
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ onSelectTab(this);
+ }
+
+ public Element getContainerElement() {
+ return content;
+ }
+
+ public Widget getChildWidget() {
+ if (getWidgetCount() > 1) {
+ return getWidget(1);
+ } else {
+ return null;
+ }
+ }
+
+ public void replaceWidget(Widget newWidget) {
+ if (getWidgetCount() > 1) {
+ Widget oldWidget = getWidget(1);
+ ComponentConnector oldPaintable = ConnectorMap.get(client)
+ .getConnector(oldWidget);
+ ConnectorMap.get(client).unregisterConnector(oldPaintable);
+ widgets.remove(oldWidget);
+ remove(1);
+ }
+ add(newWidget, content);
+ widgets.add(newWidget);
+ }
+
+ public void open() {
+ open = true;
+ DOM.setStyleAttribute(content, "top", getCaptionHeight() + "px");
+ DOM.setStyleAttribute(content, "left", "0px");
+ DOM.setStyleAttribute(content, "visibility", "");
+ addStyleDependentName("open");
+ }
+
+ public void hide() {
+ DOM.setStyleAttribute(content, "visibility", "hidden");
+ }
+
+ public void close() {
+ DOM.setStyleAttribute(content, "visibility", "hidden");
+ DOM.setStyleAttribute(content, "top", "-100000px");
+ DOM.setStyleAttribute(content, "left", "-100000px");
+ removeStyleDependentName("open");
+ setHeight(-1);
+ setWidth("");
+ open = false;
+ }
+
+ public boolean isOpen() {
+ return open;
+ }
+
+ public void setContent(UIDL contentUidl) {
+ final ComponentConnector newPntbl = client
+ .getPaintable(contentUidl);
+ Widget newWidget = newPntbl.getWidget();
+ if (getChildWidget() == null) {
+ add(newWidget, content);
+ widgets.add(newWidget);
+ } else if (getChildWidget() != newWidget) {
+ replaceWidget(newWidget);
+ }
+ if (contentUidl.getBooleanAttribute("cached")) {
+ /*
+ * The size of a cached, relative sized component must be
+ * updated to report correct size.
+ */
+ client.handleComponentRelativeSize(newPntbl.getWidget());
+ }
+ if (isOpen() && isDynamicHeight()) {
+ setHeightFromWidget();
+ }
+ }
+
+ public void onClick(ClickEvent event) {
+ onSelectTab(this);
+ }
+
+ public void updateCaption(UIDL uidl) {
+ // TODO need to call this because the caption does not have an owner
+ caption.updateCaptionWithoutOwner(
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_CAPTION),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DISABLED),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ERROR_MESSAGE),
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ICON));
+ }
+
+ public int getWidgetWidth() {
+ return DOM.getFirstChild(content).getOffsetWidth();
+ }
+
+ public boolean contains(ComponentConnector p) {
+ return (getChildWidget() == p.getWidget());
+ }
+
+ public boolean isCaptionVisible() {
+ return caption.isVisible();
+ }
+
+ }
+
+ @Override
+ protected void clearPaintables() {
+ clear();
+ }
+
+ boolean isDynamicWidth() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedWidth();
+ }
+
+ boolean isDynamicHeight() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedHeight();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected Iterator<Widget> getWidgetIterator() {
+ return widgets.iterator();
+ }
+
+ @Override
+ protected int getTabCount() {
+ return getWidgetCount();
+ }
+
+ @Override
+ protected void removeTab(int index) {
+ StackItem item = getStackItem(index);
+ remove(item);
+ }
+
+ @Override
+ protected ComponentConnector getTab(int index) {
+ if (index < getWidgetCount()) {
+ Widget w = getStackItem(index);
+ return ConnectorMap.get(client).getConnector(w);
+ }
+
+ return null;
+ }
+
+ StackItem getStackItem(int index) {
+ return (StackItem) getWidget(index);
+ }
+
+}
--- /dev/null
- client.doLayout(false);
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.draganddropwrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.TouchStartEvent;
+import com.google.gwt.event.dom.client.TouchStartHandler;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.xhr.client.ReadyStateChangeHandler;
+import com.google.gwt.xhr.client.XMLHttpRequest;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
++import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.VTooltip;
+import com.vaadin.terminal.gwt.client.ValueMap;
+import com.vaadin.terminal.gwt.client.ui.customcomponent.VCustomComponent;
+import com.vaadin.terminal.gwt.client.ui.dd.DDUtil;
+import com.vaadin.terminal.gwt.client.ui.dd.HorizontalDropLocation;
+import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent;
+import com.vaadin.terminal.gwt.client.ui.dd.VDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VHtml5DragEvent;
+import com.vaadin.terminal.gwt.client.ui.dd.VHtml5File;
+import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
+import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation;
+
+/**
+ *
+ * Must have features pending:
+ *
+ * drop details: locations + sizes in document hierarchy up to wrapper
+ *
+ */
+public class VDragAndDropWrapper extends VCustomComponent implements
+ VHasDropHandler {
+ public static final String DRAG_START_MODE = "dragStartMode";
+ public static final String HTML5_DATA_FLAVORS = "html5-data-flavors";
+
+ private static final String CLASSNAME = "v-ddwrapper";
+ protected static final String DRAGGABLE = "draggable";
+
+ public VDragAndDropWrapper() {
+ super();
+ sinkEvents(VTooltip.TOOLTIP_EVENTS);
+
+ hookHtml5Events(getElement());
+ setStyleName(CLASSNAME);
+ addDomHandler(new MouseDownHandler() {
+ public void onMouseDown(MouseDownEvent event) {
+ if (startDrag(event.getNativeEvent())) {
+ event.preventDefault(); // prevent text selection
+ }
+ }
+ }, MouseDownEvent.getType());
+
+ addDomHandler(new TouchStartHandler() {
+ public void onTouchStart(TouchStartEvent event) {
+ if (startDrag(event.getNativeEvent())) {
+ /*
+ * Dont let eg. panel start scrolling.
+ */
+ event.stopPropagation();
+ }
+ }
+ }, TouchStartEvent.getType());
+
+ sinkEvents(Event.TOUCHEVENTS);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+
+ if (client != null) {
+ client.handleTooltipEvent(event, this);
+ }
+ }
+
+ /**
+ * Starts a drag and drop operation from mousedown or touchstart event if
+ * required conditions are met.
+ *
+ * @param event
+ * @return true if the event was handled as a drag start event
+ */
+ private boolean startDrag(NativeEvent event) {
+ if (dragStartMode == WRAPPER || dragStartMode == COMPONENT) {
+ VTransferable transferable = new VTransferable();
+ transferable.setDragSource(ConnectorMap.get(client).getConnector(
+ VDragAndDropWrapper.this));
+
+ ComponentConnector paintable = Util.findPaintable(client,
+ (Element) event.getEventTarget().cast());
+ Widget widget = paintable.getWidget();
+ transferable.setData("component", paintable);
+ VDragEvent dragEvent = VDragAndDropManager.get().startDrag(
+ transferable, event, true);
+
+ transferable.setData("mouseDown", MouseEventDetailsBuilder
+ .buildMouseEventDetails(event).serialize());
+
+ if (dragStartMode == WRAPPER) {
+ dragEvent.createDragImage(getElement(), true);
+ } else {
+ dragEvent.createDragImage(widget.getElement(), true);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ protected final static int NONE = 0;
+ protected final static int COMPONENT = 1;
+ protected final static int WRAPPER = 2;
+ protected final static int HTML5 = 3;
+
+ protected int dragStartMode;
+
+ ApplicationConnection client;
+ VAbstractDropHandler dropHandler;
+ private VDragEvent vaadinDragEvent;
+
+ int filecounter = 0;
+ Map<String, String> fileIdToReceiver;
+ ValueMap html5DataFlavors;
+ private Element dragStartElement;
+
+ protected void initDragStartMode() {
+ Element div = getElement();
+ if (dragStartMode == HTML5) {
+ if (dragStartElement == null) {
+ dragStartElement = getDragStartElement();
+ dragStartElement.setPropertyBoolean(DRAGGABLE, true);
+ VConsole.log("draggable = "
+ + dragStartElement.getPropertyBoolean(DRAGGABLE));
+ hookHtml5DragStart(dragStartElement);
+ VConsole.log("drag start listeners hooked.");
+ }
+ } else {
+ dragStartElement = null;
+ if (div.hasAttribute(DRAGGABLE)) {
+ div.removeAttribute(DRAGGABLE);
+ }
+ }
+ }
+
+ protected Element getDragStartElement() {
+ return getElement();
+ }
+
+ private boolean uploading;
+
+ private ReadyStateChangeHandler readyStateChangeHandler = new ReadyStateChangeHandler() {
+ public void onReadyStateChange(XMLHttpRequest xhr) {
+ if (xhr.getReadyState() == XMLHttpRequest.DONE) {
+ // visit server for possible
+ // variable changes
+ client.sendPendingVariableChanges();
+ uploading = false;
+ startNextUpload();
+ xhr.clearOnReadyStateChange();
+ }
+ }
+ };
+ private Timer dragleavetimer;
+
+ void startNextUpload() {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ public void execute() {
+ if (!uploading) {
+ if (fileIds.size() > 0) {
+
+ uploading = true;
+ final Integer fileId = fileIds.remove(0);
+ VHtml5File file = files.remove(0);
+ final String receiverUrl = client
+ .translateVaadinUri(fileIdToReceiver
+ .remove(fileId.toString()));
+ ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR
+ .create();
+ extendedXHR
+ .setOnReadyStateChange(readyStateChangeHandler);
+ extendedXHR.open("POST", receiverUrl);
+ extendedXHR.postFile(file);
+ }
+ }
+
+ }
+ });
+
+ }
+
+ public boolean html5DragStart(VHtml5DragEvent event) {
+ if (dragStartMode == HTML5) {
+ /*
+ * Populate html5 payload with dataflavors from the serverside
+ */
+ JsArrayString flavors = html5DataFlavors.getKeyArray();
+ for (int i = 0; i < flavors.length(); i++) {
+ String flavor = flavors.get(i);
+ event.setHtml5DataFlavor(flavor,
+ html5DataFlavors.getString(flavor));
+ }
+ event.setEffectAllowed("copy");
+ return true;
+ }
+ return false;
+ }
+
+ public boolean html5DragEnter(VHtml5DragEvent event) {
+ if (dropHandler == null) {
+ return true;
+ }
+ try {
+ if (dragleavetimer != null) {
+ // returned quickly back to wrapper
+ dragleavetimer.cancel();
+ dragleavetimer = null;
+ }
+ if (VDragAndDropManager.get().getCurrentDropHandler() != getDropHandler()) {
+ VTransferable transferable = new VTransferable();
+ transferable.setDragSource(ConnectorMap.get(client)
+ .getConnector(this));
+
+ vaadinDragEvent = VDragAndDropManager.get().startDrag(
+ transferable, event, false);
+ VDragAndDropManager.get().setCurrentDropHandler(
+ getDropHandler());
+ }
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ } catch (Exception e) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ return true;
+ }
+ }
+
+ public boolean html5DragLeave(VHtml5DragEvent event) {
+ if (dropHandler == null) {
+ return true;
+ }
+
+ try {
+ dragleavetimer = new Timer() {
+ @Override
+ public void run() {
+ // Yes, dragleave happens before drop. Makes no sense to me.
+ // IMO shouldn't fire leave at all if drop happens (I guess
+ // this
+ // is what IE does).
+ // In Vaadin we fire it only if drop did not happen.
+ if (vaadinDragEvent != null
+ && VDragAndDropManager.get()
+ .getCurrentDropHandler() == getDropHandler()) {
+ VDragAndDropManager.get().interruptDrag();
+ }
+ }
+ };
+ dragleavetimer.schedule(350);
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ } catch (Exception e) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ return true;
+ }
+ }
+
+ public boolean html5DragOver(VHtml5DragEvent event) {
+ if (dropHandler == null) {
+ return true;
+ }
+
+ if (dragleavetimer != null) {
+ // returned quickly back to wrapper
+ dragleavetimer.cancel();
+ dragleavetimer = null;
+ }
+
+ vaadinDragEvent.setCurrentGwtEvent(event);
+ getDropHandler().dragOver(vaadinDragEvent);
+
+ String s = event.getEffectAllowed();
+ if ("all".equals(s) || s.contains("opy")) {
+ event.setDropEffect("copy");
+ } else {
+ event.setDropEffect(s);
+ }
+
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ }
+
+ public boolean html5DragDrop(VHtml5DragEvent event) {
+ if (dropHandler == null || !currentlyValid) {
+ return true;
+ }
+ try {
+
+ VTransferable transferable = vaadinDragEvent.getTransferable();
+
+ JsArrayString types = event.getTypes();
+ for (int i = 0; i < types.length(); i++) {
+ String type = types.get(i);
+ if (isAcceptedType(type)) {
+ String data = event.getDataAsText(type);
+ if (data != null) {
+ transferable.setData(type, data);
+ }
+ }
+ }
+
+ int fileCount = event.getFileCount();
+ if (fileCount > 0) {
+ transferable.setData("filecount", fileCount);
+ for (int i = 0; i < fileCount; i++) {
+ final int fileId = filecounter++;
+ final VHtml5File file = event.getFile(i);
+ transferable.setData("fi" + i, "" + fileId);
+ transferable.setData("fn" + i, file.getName());
+ transferable.setData("ft" + i, file.getType());
+ transferable.setData("fs" + i, file.getSize());
+ queueFilePost(fileId, file);
+ }
+
+ }
+
+ VDragAndDropManager.get().endDrag();
+ vaadinDragEvent = null;
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ } catch (Exception e) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ return true;
+ }
+
+ }
+
+ protected String[] acceptedTypes = new String[] { "Text", "Url",
+ "text/html", "text/plain", "text/rtf" };
+
+ private boolean isAcceptedType(String type) {
+ for (String t : acceptedTypes) {
+ if (t.equals(type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static class ExtendedXHR extends XMLHttpRequest {
+
+ protected ExtendedXHR() {
+ }
+
+ public final native void postFile(VHtml5File file)
+ /*-{
+
+ this.setRequestHeader('Content-Type', 'multipart/form-data');
+ this.send(file);
+ }-*/;
+
+ }
+
+ /**
+ * Currently supports only FF36 as no other browser supports natively File
+ * api.
+ *
+ * @param fileId
+ * @param data
+ */
+ List<Integer> fileIds = new ArrayList<Integer>();
+ List<VHtml5File> files = new ArrayList<VHtml5File>();
+
+ private void queueFilePost(final int fileId, final VHtml5File file) {
+ fileIds.add(fileId);
+ files.add(file);
+ }
+
+ public VDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ protected VerticalDropLocation verticalDropLocation;
+ protected HorizontalDropLocation horizontalDropLocation;
+ private VerticalDropLocation emphasizedVDrop;
+ private HorizontalDropLocation emphasizedHDrop;
+
+ /**
+ * Flag used by html5 dd
+ */
+ private boolean currentlyValid;
+
+ private static final String OVER_STYLE = "v-ddwrapper-over";
+
+ public class CustomDropHandler extends VAbstractDropHandler {
+
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ updateDropDetails(drag);
+ currentlyValid = false;
+ super.dragEnter(drag);
+ }
+
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ deEmphasis(true);
+ dragleavetimer = null;
+ }
+
+ @Override
+ public void dragOver(final VDragEvent drag) {
+ boolean detailsChanged = updateDropDetails(drag);
+ if (detailsChanged) {
+ currentlyValid = false;
+ validate(new VAcceptCallback() {
+ public void accepted(VDragEvent event) {
+ dragAccepted(drag);
+ }
+ }, drag);
+ }
+ }
+
+ @Override
+ public boolean drop(VDragEvent drag) {
+ deEmphasis(true);
+
+ Map<String, Object> dd = drag.getDropDetails();
+
+ // this is absolute layout based, and we may want to set
+ // component
+ // relatively to where the drag ended.
+ // need to add current location of the drop area
+
+ int absoluteLeft = getAbsoluteLeft();
+ int absoluteTop = getAbsoluteTop();
+
+ dd.put("absoluteLeft", absoluteLeft);
+ dd.put("absoluteTop", absoluteTop);
+
+ if (verticalDropLocation != null) {
+ dd.put("verticalLocation", verticalDropLocation.toString());
+ dd.put("horizontalLocation", horizontalDropLocation.toString());
+ }
+
+ return super.drop(drag);
+ }
+
+ @Override
+ protected void dragAccepted(VDragEvent drag) {
+ currentlyValid = true;
+ emphasis(drag);
+ }
+
+ @Override
+ public ComponentConnector getConnector() {
+ return ConnectorMap.get(client).getConnector(
+ VDragAndDropWrapper.this);
+ }
+
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ }
+
+ protected native void hookHtml5DragStart(Element el)
+ /*-{
+ var me = this;
+ el.addEventListener("dragstart", function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }, false);
+ }-*/;
+
+ /**
+ * Prototype code, memory leak risk.
+ *
+ * @param el
+ */
+ protected native void hookHtml5Events(Element el)
+ /*-{
+ var me = this;
+
+ el.addEventListener("dragenter", function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }, false);
+
+ el.addEventListener("dragleave", function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }, false);
+
+ el.addEventListener("dragover", function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }, false);
+
+ el.addEventListener("drop", function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }, false);
+ }-*/;
+
+ public boolean updateDropDetails(VDragEvent drag) {
+ VerticalDropLocation oldVL = verticalDropLocation;
+ verticalDropLocation = DDUtil.getVerticalDropLocation(getElement(),
+ drag.getCurrentGwtEvent(), 0.2);
+ drag.getDropDetails().put("verticalLocation",
+ verticalDropLocation.toString());
+ HorizontalDropLocation oldHL = horizontalDropLocation;
+ horizontalDropLocation = DDUtil.getHorizontalDropLocation(getElement(),
+ drag.getCurrentGwtEvent(), 0.2);
+ drag.getDropDetails().put("horizontalLocation",
+ horizontalDropLocation.toString());
+ if (oldHL != horizontalDropLocation || oldVL != verticalDropLocation) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected void deEmphasis(boolean doLayout) {
+ if (emphasizedVDrop != null) {
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, false);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + emphasizedVDrop.toString().toLowerCase(), false);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + emphasizedHDrop.toString().toLowerCase(), false);
+ }
+ if (doLayout) {
- client.doLayout(false);
++ notifySizePotentiallyChanged();
+ }
+ }
+
++ private void notifySizePotentiallyChanged() {
++ LayoutManager.get(client).setNeedsMeasure(
++ ConnectorMap.get(client).getConnector(getElement()));
++ }
++
+ protected void emphasis(VDragEvent drag) {
+ deEmphasis(false);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, true);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + verticalDropLocation.toString().toLowerCase(), true);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + horizontalDropLocation.toString().toLowerCase(), true);
+ emphasizedVDrop = verticalDropLocation;
+ emphasizedHDrop = horizontalDropLocation;
+
+ // TODO build (to be an example) an emphasis mode where drag image
+ // is fitted before or after the content
++ notifySizePotentiallyChanged();
+ }
+
+}
--- /dev/null
- import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.form;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
- implements Paintable, SimpleManagedLayout {
++import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeEvent;
++import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeListener;
++import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.ui.Form;
+
+@Component(Form.class)
+public class FormConnector extends AbstractComponentContainerConnector
- public void init() {
++ implements Paintable, MayScrollChildren {
++
++ private final ElementResizeListener footerResizeListener = new ElementResizeListener() {
++ public void onElementResize(ElementResizeEvent e) {
++ VForm form = getWidget();
++
++ int footerHeight;
++ if (form.footer != null) {
++ LayoutManager lm = getLayoutManager();
++ footerHeight = lm.getOuterHeight(form.footer.getElement());
++ } else {
++ footerHeight = 0;
++ }
++
++ form.fieldContainer.getStyle().setPaddingBottom(footerHeight,
++ Unit.PX);
++ form.footerContainer.getStyle()
++ .setMarginTop(-footerHeight, Unit.PX);
++ }
++ };
+
+ @Override
- getLayoutManager().registerDependency(this, form.footerContainer);
++ public void onUnregister() {
+ VForm form = getWidget();
- public void layout() {
- VForm form = getWidget();
-
- LayoutManager lm = getLayoutManager();
- int footerHeight = lm.getOuterHeight(form.footerContainer)
- - lm.getMarginTop(form.footerContainer);
-
- form.fieldContainer.getStyle().setPaddingBottom(footerHeight, Unit.PX);
- form.footerContainer.getStyle().setMarginTop(-footerHeight, Unit.PX);
- }
-
++ if (form.footer != null) {
++ getLayoutManager().removeElementResizeListener(
++ form.footer.getElement(), footerResizeListener);
++ }
+ }
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().client = client;
+ getWidget().id = uidl.getId();
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ boolean legendEmpty = true;
+ if (getState().getCaption() != null) {
+ getWidget().caption.setInnerText(getState().getCaption());
+ legendEmpty = false;
+ } else {
+ getWidget().caption.setInnerText("");
+ }
+ if (getState().getIcon() != null) {
+ if (getWidget().icon == null) {
+ getWidget().icon = new Icon(client);
+ getWidget().legend.insertFirst(getWidget().icon.getElement());
+ }
+ getWidget().icon.setUri(getState().getIcon().getURL());
+ legendEmpty = false;
+ } else {
+ if (getWidget().icon != null) {
+ getWidget().legend.removeChild(getWidget().icon.getElement());
+ }
+ }
+ if (legendEmpty) {
+ getWidget().addStyleDependentName("nocaption");
+ } else {
+ getWidget().removeStyleDependentName("nocaption");
+ }
+
+ if (null != getState().getErrorMessage()) {
+ getWidget().errorMessage
+ .updateMessage(getState().getErrorMessage());
+ getWidget().errorMessage.setVisible(true);
+ } else {
+ getWidget().errorMessage.setVisible(false);
+ }
+
+ if (getState().hasDescription()) {
+ getWidget().desc.setInnerHTML(getState().getDescription());
+ if (getWidget().desc.getParentElement() == null) {
+ getWidget().fieldSet.insertAfter(getWidget().desc,
+ getWidget().legend);
+ }
+ } else {
+ getWidget().desc.setInnerHTML("");
+ if (getWidget().desc.getParentElement() != null) {
+ getWidget().fieldSet.removeChild(getWidget().desc);
+ }
+ }
+
+ // first render footer so it will be easier to handle relative height of
+ // main layout
+ if (getState().getFooter() != null) {
+ // render footer
+ ComponentConnector newFooter = (ComponentConnector) getState()
+ .getFooter();
+ Widget newFooterWidget = newFooter.getWidget();
+ if (getWidget().footer == null) {
++ getLayoutManager().addElementResizeListener(
++ newFooterWidget.getElement(), footerResizeListener);
+ getWidget().add(newFooter.getWidget(),
+ getWidget().footerContainer);
+ getWidget().footer = newFooterWidget;
+ } else if (newFooter != getWidget().footer) {
++ getLayoutManager().removeElementResizeListener(
++ getWidget().footer.getElement(), footerResizeListener);
++ getLayoutManager().addElementResizeListener(
++ newFooterWidget.getElement(), footerResizeListener);
+ getWidget().remove(getWidget().footer);
+ getWidget().add(newFooter.getWidget(),
+ getWidget().footerContainer);
+ }
+ getWidget().footer = newFooterWidget;
+ } else {
+ if (getWidget().footer != null) {
++ getLayoutManager().removeElementResizeListener(
++ getWidget().footer.getElement(), footerResizeListener);
+ getWidget().remove(getWidget().footer);
++ getWidget().footer = null;
+ }
+ }
+
+ ComponentConnector newLayout = (ComponentConnector) getState()
+ .getLayout();
+ Widget newLayoutWidget = newLayout.getWidget();
+ if (getWidget().lo == null) {
+ // Layout not rendered before
+ getWidget().lo = newLayoutWidget;
+ getWidget().add(newLayoutWidget, getWidget().fieldContainer);
+ } else if (getWidget().lo != newLayoutWidget) {
+ // Layout has changed
+ getWidget().remove(getWidget().lo);
+ getWidget().lo = newLayoutWidget;
+ getWidget().add(newLayoutWidget, getWidget().fieldContainer);
+ }
+
+ // also recalculates size of the footer if undefined size form - see
+ // #3710
+ client.runDescendentsLayout(getWidget());
+
+ // We may have actions attached
+ if (uidl.getChildCount() >= 1) {
+ UIDL childUidl = uidl.getChildByTagName("actions");
+ if (childUidl != null) {
+ if (getWidget().shortcutHandler == null) {
+ getWidget().shortcutHandler = new ShortcutActionHandler(
+ getConnectorId(), client);
+ getWidget().keyDownRegistration = getWidget()
+ .addDomHandler(getWidget(), KeyDownEvent.getType());
+ }
+ getWidget().shortcutHandler.updateActionMap(childUidl);
+ }
+ } else if (getWidget().shortcutHandler != null) {
+ getWidget().keyDownRegistration.removeHandler();
+ getWidget().shortcutHandler = null;
+ getWidget().keyDownRegistration = null;
+ }
+ }
+
+ public void updateCaption(ComponentConnector component) {
+ // NOP form don't render caption for neither field layout nor footer
+ // layout
+ }
+
+ @Override
+ public VForm getWidget() {
+ return (VForm) super.getWidget();
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VForm.class);
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || getState().isPropertyReadOnly();
+ }
+
+ @Override
+ public FormState getState() {
+ return (FormState) super.getState();
+ }
+
+}
--- /dev/null
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.gridlayout;
+
+import java.util.Iterator;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.AlignmentInfo;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickRPC;
+import com.vaadin.terminal.gwt.client.ui.VMarginInfo;
+import com.vaadin.terminal.gwt.client.ui.gridlayout.VGridLayout.Cell;
+import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot;
+import com.vaadin.ui.GridLayout;
+
+@Component(GridLayout.class)
+public class GridLayoutConnector extends AbstractComponentContainerConnector
+ implements Paintable, DirectionalManagedLayout {
+
+ private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler(
+ this) {
+
+ @Override
+ protected ComponentConnector getChildComponent(Element element) {
+ return getWidget().getComponent(element);
+ }
+
+ @Override
+ protected LayoutClickRPC getLayoutClickRPC() {
+ return rpc;
+ };
+
+ };
+
+ private GridLayoutServerRPC rpc;
+ private boolean needCaptionUpdate = false;
+
+ @Override
+ public void init() {
+ rpc = RpcProxy.create(GridLayoutServerRPC.class, this);
+ getLayoutManager().registerDependency(this,
+ getWidget().spacingMeasureElement);
+ }
+
++ @Override
++ public void onUnregister() {
++ VGridLayout layout = getWidget();
++ getLayoutManager().unregisterDependency(this,
++ layout.spacingMeasureElement);
++
++ // Unregister caption size dependencies
++ for (ComponentConnector child : getChildren()) {
++ Cell cell = layout.widgetToCell.get(child.getWidget());
++ cell.slot.setCaption(null);
++ }
++ }
++
+ @Override
+ public GridLayoutState getState() {
+ return (GridLayoutState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ VGridLayout layout = getWidget();
+ layout.client = client;
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ int cols = getState().getColumns();
+ int rows = getState().getRows();
+
+ layout.columnWidths = new int[cols];
+ layout.rowHeights = new int[rows];
+
+ layout.setSize(rows, cols);
+
+ final int[] alignments = uidl.getIntArrayAttribute("alignments");
+ int alignmentIndex = 0;
+
+ for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) {
+ final UIDL r = (UIDL) i.next();
+ if ("gr".equals(r.getTag())) {
+ for (final Iterator<?> j = r.getChildIterator(); j.hasNext();) {
+ final UIDL cellUidl = (UIDL) j.next();
+ if ("gc".equals(cellUidl.getTag())) {
+ int row = cellUidl.getIntAttribute("y");
+ int col = cellUidl.getIntAttribute("x");
+
+ Widget previousWidget = null;
+
+ Cell cell = layout.getCell(row, col);
+ if (cell != null && cell.slot != null) {
+ // This is an update. Track if the widget changes
+ // and update the caption if that happens. This
+ // workaround can be removed once the DOM update is
+ // done in onContainerHierarchyChange
+ previousWidget = cell.slot.getWidget();
+ }
+
+ cell = layout.createCell(row, col);
+
+ cell.updateFromUidl(cellUidl);
+
+ if (cell.hasContent()) {
+ cell.setAlignment(new AlignmentInfo(
+ alignments[alignmentIndex++]));
+ if (cell.slot.getWidget() != previousWidget) {
+ // Widget changed or widget moved from another
+ // slot. Update its caption as the widget might
+ // have called updateCaption when the widget was
+ // still in its old slot. This workaround can be
+ // removed once the DOM update
+ // is done in onContainerHierarchyChange
+ updateCaption(ConnectorMap.get(getConnection())
+ .getConnector(cell.slot.getWidget()));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ layout.colExpandRatioArray = uidl.getIntArrayAttribute("colExpand");
+ layout.rowExpandRatioArray = uidl.getIntArrayAttribute("rowExpand");
+
+ layout.updateMarginStyleNames(new VMarginInfo(getState()
+ .getMarginsBitmask()));
+
+ layout.updateSpacingStyleName(getState().isSpacing());
+
+ if (needCaptionUpdate) {
+ needCaptionUpdate = false;
+
+ for (ComponentConnector child : getChildren()) {
+ updateCaption(child);
+ }
+ }
+ getLayoutManager().setNeedsUpdate(this);
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ VGridLayout layout = getWidget();
+
+ // clean non rendered components
+ for (ComponentConnector oldChild : event.getOldChildren()) {
+ if (oldChild.getParent() == this) {
+ continue;
+ }
+
+ Widget childWidget = oldChild.getWidget();
+ layout.remove(childWidget);
+
+ Cell cell = layout.widgetToCell.remove(childWidget);
+ cell.slot.setCaption(null);
+ cell.slot.getWrapperElement().removeFromParent();
+ cell.slot = null;
+ }
+
+ }
+
+ public void updateCaption(ComponentConnector childConnector) {
+ if (!childConnector.delegateCaptionHandling()) {
+ // Check not required by interface but by workarounds in this class
+ // when updateCaption is explicitly called for all children.
+ return;
+ }
+
+ VGridLayout layout = getWidget();
+ Cell cell = layout.widgetToCell.get(childConnector.getWidget());
+ if (cell == null) {
+ // workaround before updateFromUidl is removed. We currently update
+ // the captions at the end of updateFromUidl instead of immediately
+ // because the DOM has not been set up at this point (as it is done
+ // in updateFromUidl)
+ needCaptionUpdate = true;
+ return;
+ }
+ if (VCaption.isNeeded(childConnector.getState())) {
+ VLayoutSlot layoutSlot = cell.slot;
+ VCaption caption = layoutSlot.getCaption();
+ if (caption == null) {
+ caption = new VCaption(childConnector, getConnection());
+
+ Widget widget = childConnector.getWidget();
+
+ layout.setCaption(widget, caption);
+ }
+ caption.updateCaption();
+ } else {
+ layout.setCaption(childConnector.getWidget(), null);
+ }
+ }
+
+ @Override
+ public VGridLayout getWidget() {
+ return (VGridLayout) super.getWidget();
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VGridLayout.class);
+ }
+
+ public void layoutVertically() {
+ getWidget().updateHeight();
+ }
+
+ public void layoutHorizontally() {
+ getWidget().updateWidth();
+ }
+}
--- /dev/null
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.gridlayout;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.ui.AlignmentInfo;
+import com.vaadin.terminal.gwt.client.ui.VMarginInfo;
+import com.vaadin.terminal.gwt.client.ui.layout.ComponentConnectorLayoutSlot;
+import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot;
+
+public class VGridLayout extends ComplexPanel {
+
+ public static final String CLASSNAME = "v-gridlayout";
+
+ ApplicationConnection client;
+
+ HashMap<Widget, Cell> widgetToCell = new HashMap<Widget, Cell>();
+
+ int[] columnWidths;
+ int[] rowHeights;
+
+ int[] colExpandRatioArray;
+
+ int[] rowExpandRatioArray;
+
+ int[] minColumnWidths;
+
+ private int[] minRowHeights;
+
+ DivElement spacingMeasureElement;
+
+ public VGridLayout() {
+ super();
+ setElement(Document.get().createDivElement());
+
+ spacingMeasureElement = Document.get().createDivElement();
+ Style spacingStyle = spacingMeasureElement.getStyle();
+ spacingStyle.setPosition(Position.ABSOLUTE);
+ getElement().appendChild(spacingMeasureElement);
+
+ setStyleName(CLASSNAME);
+ }
+
+ private GridLayoutConnector getConnector() {
+ return (GridLayoutConnector) ConnectorMap.get(client)
+ .getConnector(this);
+ }
+
+ /**
+ * Returns the column widths measured in pixels
+ *
+ * @return
+ */
+ protected int[] getColumnWidths() {
+ return columnWidths;
+ }
+
+ /**
+ * Returns the row heights measured in pixels
+ *
+ * @return
+ */
+ protected int[] getRowHeights() {
+ return rowHeights;
+ }
+
+ /**
+ * Returns the spacing between the cells horizontally in pixels
+ *
+ * @return
+ */
+ protected int getHorizontalSpacing() {
+ return LayoutManager.get(client).getOuterWidth(spacingMeasureElement);
+ }
+
+ /**
+ * Returns the spacing between the cells vertically in pixels
+ *
+ * @return
+ */
+ protected int getVerticalSpacing() {
+ return LayoutManager.get(client).getOuterHeight(spacingMeasureElement);
+ }
+
+ static int[] cloneArray(int[] toBeCloned) {
+ int[] clone = new int[toBeCloned.length];
+ for (int i = 0; i < clone.length; i++) {
+ clone[i] = toBeCloned[i] * 1;
+ }
+ return clone;
+ }
+
+ void expandRows() {
+ if (!isUndefinedHeight()) {
+ int usedSpace = minRowHeights[0];
+ int verticalSpacing = getVerticalSpacing();
+ for (int i = 1; i < minRowHeights.length; i++) {
+ usedSpace += verticalSpacing + minRowHeights[i];
+ }
+ int availableSpace = LayoutManager.get(client).getInnerHeight(
+ getElement());
+ int excessSpace = availableSpace - usedSpace;
+ int distributed = 0;
+ if (excessSpace > 0) {
+ for (int i = 0; i < rowHeights.length; i++) {
+ int ew = excessSpace * rowExpandRatioArray[i] / 1000;
+ rowHeights[i] = minRowHeights[i] + ew;
+ distributed += ew;
+ }
+ excessSpace -= distributed;
+ int c = 0;
+ while (excessSpace > 0) {
+ rowHeights[c % rowHeights.length]++;
+ excessSpace--;
+ c++;
+ }
+ }
+ }
+ }
+
+ void updateHeight() {
+ // Detect minimum heights & calculate spans
+ detectRowHeights();
+
+ // Expand
+ expandRows();
+
+ // Position
+ layoutCellsVertically();
+ }
+
+ void updateWidth() {
+ // Detect widths & calculate spans
+ detectColWidths();
+ // Expand
+ expandColumns();
+ // Position
+ layoutCellsHorizontally();
+
+ }
+
+ void expandColumns() {
+ if (!isUndefinedWidth()) {
+ int usedSpace = minColumnWidths[0];
+ int horizontalSpacing = getHorizontalSpacing();
+ for (int i = 1; i < minColumnWidths.length; i++) {
+ usedSpace += horizontalSpacing + minColumnWidths[i];
+ }
+
+ int availableSpace = LayoutManager.get(client).getInnerWidth(
+ getElement());
+ int excessSpace = availableSpace - usedSpace;
+ int distributed = 0;
+ if (excessSpace > 0) {
+ for (int i = 0; i < columnWidths.length; i++) {
+ int ew = excessSpace * colExpandRatioArray[i] / 1000;
+ columnWidths[i] = minColumnWidths[i] + ew;
+ distributed += ew;
+ }
+ excessSpace -= distributed;
+ int c = 0;
+ while (excessSpace > 0) {
+ columnWidths[c % columnWidths.length]++;
+ excessSpace--;
+ c++;
+ }
+ }
+ }
+ }
+
+ void layoutCellsVertically() {
+ int verticalSpacing = getVerticalSpacing();
+ LayoutManager layoutManager = LayoutManager.get(client);
+ Element element = getElement();
+ int paddingTop = layoutManager.getPaddingTop(element);
+ int y = paddingTop;
+
+ for (int i = 0; i < cells.length; i++) {
+ y = paddingTop;
+ for (int j = 0; j < cells[i].length; j++) {
+ Cell cell = cells[i][j];
+ if (cell != null) {
+ cell.layoutVertically(y);
+ }
+ y += rowHeights[j] + verticalSpacing;
+ }
+ }
+
+ if (isUndefinedHeight()) {
+ int outerHeight = y - verticalSpacing
+ + layoutManager.getPaddingBottom(element)
+ + layoutManager.getBorderHeight(element);
+ element.getStyle().setHeight(outerHeight, Unit.PX);
++ getConnector().getLayoutManager().reportOuterHeight(getConnector(),
++ outerHeight);
+ }
+ }
+
+ void layoutCellsHorizontally() {
+ LayoutManager layoutManager = LayoutManager.get(client);
+ Element element = getElement();
+ int x = layoutManager.getPaddingLeft(element);
+ int horizontalSpacing = getHorizontalSpacing();
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < cells[i].length; j++) {
+ Cell cell = cells[i][j];
+ if (cell != null) {
+ cell.layoutHorizontally(x);
+ }
+ }
+ x += columnWidths[i] + horizontalSpacing;
+ }
+
+ if (isUndefinedWidth()) {
+ int outerWidth = x - horizontalSpacing
+ + layoutManager.getPaddingRight(element)
+ + layoutManager.getBorderWidth(element);
+ element.getStyle().setWidth(outerWidth, Unit.PX);
++ getConnector().getLayoutManager().reportOuterWidth(getConnector(),
++ outerWidth);
+ }
+ }
+
+ private boolean isUndefinedHeight() {
+ return getConnector().isUndefinedHeight();
+ }
+
+ private boolean isUndefinedWidth() {
+ return getConnector().isUndefinedWidth();
+ }
+
+ private void detectRowHeights() {
+ for (int i = 0; i < rowHeights.length; i++) {
+ rowHeights[i] = 0;
+ }
+
+ // collect min rowheight from non-rowspanned cells
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < cells[i].length; j++) {
+ Cell cell = cells[i][j];
+ if (cell != null) {
+ if (cell.rowspan == 1) {
+ if (!cell.hasRelativeHeight()
+ && rowHeights[j] < cell.getHeight()) {
+ rowHeights[j] = cell.getHeight();
+ }
+ } else {
+ storeRowSpannedCell(cell);
+ }
+ }
+ }
+ }
+
+ distributeRowSpanHeights();
+
+ minRowHeights = cloneArray(rowHeights);
+ }
+
+ private void detectColWidths() {
+ // collect min colwidths from non-colspanned cells
+ for (int i = 0; i < columnWidths.length; i++) {
+ columnWidths[i] = 0;
+ }
+
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < cells[i].length; j++) {
+ Cell cell = cells[i][j];
+ if (cell != null) {
+ if (cell.colspan == 1) {
+ if (!cell.hasRelativeWidth()
+ && columnWidths[i] < cell.getWidth()) {
+ columnWidths[i] = cell.getWidth();
+ }
+ } else {
+ storeColSpannedCell(cell);
+ }
+ }
+ }
+ }
+
+ distributeColSpanWidths();
+
+ minColumnWidths = cloneArray(columnWidths);
+ }
+
+ private void storeRowSpannedCell(Cell cell) {
+ SpanList l = null;
+ for (SpanList list : rowSpans) {
+ if (list.span < cell.rowspan) {
+ continue;
+ } else {
+ // insert before this
+ l = list;
+ break;
+ }
+ }
+ if (l == null) {
+ l = new SpanList(cell.rowspan);
+ rowSpans.add(l);
+ } else if (l.span != cell.rowspan) {
+ SpanList newL = new SpanList(cell.rowspan);
+ rowSpans.add(rowSpans.indexOf(l), newL);
+ l = newL;
+ }
+ l.cells.add(cell);
+ }
+
+ /**
+ * Iterates colspanned cells, ensures cols have enough space to accommodate
+ * them
+ */
+ void distributeColSpanWidths() {
+ for (SpanList list : colSpans) {
+ for (Cell cell : list.cells) {
+ // cells with relative content may return non 0 here if on
+ // subsequent renders
+ int width = cell.hasRelativeWidth() ? 0 : cell.getWidth();
+ distributeSpanSize(columnWidths, cell.col, cell.colspan,
+ getHorizontalSpacing(), width, colExpandRatioArray);
+ }
+ }
+ }
+
+ /**
+ * Iterates rowspanned cells, ensures rows have enough space to accommodate
+ * them
+ */
+ private void distributeRowSpanHeights() {
+ for (SpanList list : rowSpans) {
+ for (Cell cell : list.cells) {
+ // cells with relative content may return non 0 here if on
+ // subsequent renders
+ int height = cell.hasRelativeHeight() ? 0 : cell.getHeight();
+ distributeSpanSize(rowHeights, cell.row, cell.rowspan,
+ getVerticalSpacing(), height, rowExpandRatioArray);
+ }
+ }
+ }
+
+ private static void distributeSpanSize(int[] dimensions,
+ int spanStartIndex, int spanSize, int spacingSize, int size,
+ int[] expansionRatios) {
+ int allocated = dimensions[spanStartIndex];
+ for (int i = 1; i < spanSize; i++) {
+ allocated += spacingSize + dimensions[spanStartIndex + i];
+ }
+ if (allocated < size) {
+ // dimensions needs to be expanded due spanned cell
+ int neededExtraSpace = size - allocated;
+ int allocatedExtraSpace = 0;
+
+ // Divide space according to expansion ratios if any span has a
+ // ratio
+ int totalExpansion = 0;
+ for (int i = 0; i < spanSize; i++) {
+ int itemIndex = spanStartIndex + i;
+ totalExpansion += expansionRatios[itemIndex];
+ }
+
+ for (int i = 0; i < spanSize; i++) {
+ int itemIndex = spanStartIndex + i;
+ int expansion;
+ if (totalExpansion == 0) {
+ // Divide equally among all cells if there are no
+ // expansion ratios
+ expansion = neededExtraSpace / spanSize;
+ } else {
+ expansion = neededExtraSpace * expansionRatios[itemIndex]
+ / totalExpansion;
+ }
+ dimensions[itemIndex] += expansion;
+ allocatedExtraSpace += expansion;
+ }
+
+ // We might still miss a couple of pixels because of
+ // rounding errors...
+ if (neededExtraSpace > allocatedExtraSpace) {
+ for (int i = 0; i < spanSize; i++) {
+ // Add one pixel to every cell until we have
+ // compensated for any rounding error
+ int itemIndex = spanStartIndex + i;
+ dimensions[itemIndex] += 1;
+ allocatedExtraSpace += 1;
+ if (neededExtraSpace == allocatedExtraSpace) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private LinkedList<SpanList> colSpans = new LinkedList<SpanList>();
+ private LinkedList<SpanList> rowSpans = new LinkedList<SpanList>();
+
+ private class SpanList {
+ final int span;
+ List<Cell> cells = new LinkedList<Cell>();
+
+ public SpanList(int span) {
+ this.span = span;
+ }
+ }
+
+ void storeColSpannedCell(Cell cell) {
+ SpanList l = null;
+ for (SpanList list : colSpans) {
+ if (list.span < cell.colspan) {
+ continue;
+ } else {
+ // insert before this
+ l = list;
+ break;
+ }
+ }
+ if (l == null) {
+ l = new SpanList(cell.colspan);
+ colSpans.add(l);
+ } else if (l.span != cell.colspan) {
+
+ SpanList newL = new SpanList(cell.colspan);
+ colSpans.add(colSpans.indexOf(l), newL);
+ l = newL;
+ }
+ l.cells.add(cell);
+ }
+
+ Cell[][] cells;
+
+ /**
+ * Private helper class.
+ */
+ class Cell {
+ public Cell(int row, int col) {
+ this.row = row;
+ this.col = col;
+ }
+
+ public boolean hasContent() {
+ return hasContent;
+ }
+
+ public boolean hasRelativeHeight() {
+ if (slot != null) {
+ return slot.getChild().isRelativeHeight();
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * @return total of spanned cols
+ */
+ private int getAvailableWidth() {
+ int width = columnWidths[col];
+ for (int i = 1; i < colspan; i++) {
+ width += getHorizontalSpacing() + columnWidths[col + i];
+ }
+ return width;
+ }
+
+ /**
+ * @return total of spanned rows
+ */
+ private int getAvailableHeight() {
+ int height = rowHeights[row];
+ for (int i = 1; i < rowspan; i++) {
+ height += getVerticalSpacing() + rowHeights[row + i];
+ }
+ return height;
+ }
+
+ public void layoutHorizontally(int x) {
+ if (slot != null) {
+ slot.positionHorizontally(x, getAvailableWidth());
+ }
+ }
+
+ public void layoutVertically(int y) {
+ if (slot != null) {
+ slot.positionVertically(y, getAvailableHeight());
+ }
+ }
+
+ public int getWidth() {
+ if (slot != null) {
+ return slot.getUsedWidth();
+ } else {
+ return 0;
+ }
+ }
+
+ public int getHeight() {
+ if (slot != null) {
+ return slot.getUsedHeight();
+ } else {
+ return 0;
+ }
+ }
+
+ protected boolean hasRelativeWidth() {
+ if (slot != null) {
+ return slot.getChild().isRelativeWidth();
+ } else {
+ return true;
+ }
+ }
+
+ final int row;
+ final int col;
+ int colspan = 1;
+ int rowspan = 1;
+
+ private boolean hasContent;
+
+ private AlignmentInfo alignment;
+
+ ComponentConnectorLayoutSlot slot;
+
+ public void updateFromUidl(UIDL cellUidl) {
+ // Set cell width
+ colspan = cellUidl.hasAttribute("w") ? cellUidl
+ .getIntAttribute("w") : 1;
+ // Set cell height
+ rowspan = cellUidl.hasAttribute("h") ? cellUidl
+ .getIntAttribute("h") : 1;
+ // ensure we will lose reference to old cells, now overlapped by
+ // this cell
+ for (int i = 0; i < colspan; i++) {
+ for (int j = 0; j < rowspan; j++) {
+ if (i > 0 || j > 0) {
+ cells[col + i][row + j] = null;
+ }
+ }
+ }
+
+ UIDL childUidl = cellUidl.getChildUIDL(0); // we are interested
+ // about childUidl
+ hasContent = childUidl != null;
+ if (hasContent) {
+ ComponentConnector childConnector = client
+ .getPaintable(childUidl);
+
+ if (slot == null || slot.getChild() != childConnector) {
+ slot = new ComponentConnectorLayoutSlot(CLASSNAME,
+ childConnector, getConnector());
++ if (childConnector.isRelativeWidth()) {
++ slot.getWrapperElement().getStyle()
++ .setWidth(100, Unit.PCT);
++ }
+ Element slotWrapper = slot.getWrapperElement();
+ getElement().appendChild(slotWrapper);
+
+ Widget widget = childConnector.getWidget();
+ insert(widget, slotWrapper, getWidgetCount(), false);
+ Cell oldCell = widgetToCell.put(widget, this);
+ if (oldCell != null) {
+ oldCell.slot.getWrapperElement().removeFromParent();
+ oldCell.slot = null;
+ }
+ }
+
+ }
+ }
+
+ public void setAlignment(AlignmentInfo alignmentInfo) {
+ slot.setAlignment(alignmentInfo);
+ }
+ }
+
+ Cell getCell(int row, int col) {
+ return cells[col][row];
+ }
+
+ /**
+ * Creates a new Cell with the given coordinates. If an existing cell was
+ * found, returns that one.
+ *
+ * @param row
+ * @param col
+ * @return
+ */
+ Cell createCell(int row, int col) {
+ Cell cell = getCell(row, col);
+ if (cell == null) {
+ cell = new Cell(row, col);
+ cells[col][row] = cell;
+ }
+ return cell;
+ }
+
+ /**
+ * Returns the deepest nested child component which contains "element". The
+ * child component is also returned if "element" is part of its caption.
+ *
+ * @param element
+ * An element that is a nested sub element of the root element in
+ * this layout
+ * @return The Paintable which the element is a part of. Null if the element
+ * belongs to the layout and not to a child.
+ */
+ ComponentConnector getComponent(Element element) {
+ return Util.getConnectorForElement(client, this, element);
+ }
+
+ void setCaption(Widget widget, VCaption caption) {
+ VLayoutSlot slot = widgetToCell.get(widget).slot;
+
+ if (caption != null) {
+ // Logical attach.
+ getChildren().add(caption);
+ }
+
+ // Physical attach if not null, also removes old caption
+ slot.setCaption(caption);
+
+ if (caption != null) {
+ // Adopt.
+ adopt(caption);
+ }
+ }
+
+ private void togglePrefixedStyleName(String name, boolean enabled) {
+ if (enabled) {
+ addStyleDependentName(name);
+ } else {
+ removeStyleDependentName(name);
+ }
+ }
+
+ void updateMarginStyleNames(VMarginInfo marginInfo) {
+ togglePrefixedStyleName("margin-top", marginInfo.hasTop());
+ togglePrefixedStyleName("margin-right", marginInfo.hasRight());
+ togglePrefixedStyleName("margin-bottom", marginInfo.hasBottom());
+ togglePrefixedStyleName("margin-left", marginInfo.hasLeft());
+ }
+
+ void updateSpacingStyleName(boolean spacingEnabled) {
+ String styleName = getStylePrimaryName();
+ if (spacingEnabled) {
+ spacingMeasureElement.addClassName(styleName + "-spacing-on");
+ spacingMeasureElement.removeClassName(styleName + "-spacing-off");
+ } else {
+ spacingMeasureElement.removeClassName(styleName + "-spacing-on");
+ spacingMeasureElement.addClassName(styleName + "-spacing-off");
+ }
+ }
+
+ public void setSize(int rows, int cols) {
+ if (cells == null) {
+ cells = new Cell[cols][rows];
+ } else if (cells.length != cols || cells[0].length != rows) {
+ Cell[][] newCells = new Cell[cols][rows];
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < cells[i].length; j++) {
+ if (i < cols && j < rows) {
+ newCells[i][j] = cells[i][j];
+ }
+ }
+ }
+ cells = newCells;
+ }
+ }
+
+}
--- /dev/null
- ownStyle.setPropertyPx(getSizeProperty(isVertical),
- getSizeForInnerSize(allocatedSize, isVertical));
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.orderedlayout;
+
+import java.util.List;
+
+import com.google.gwt.dom.client.Style;
++import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.ValueMap;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.ui.AbstractLayoutConnector;
+import com.vaadin.terminal.gwt.client.ui.AlignmentInfo;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickRPC;
+import com.vaadin.terminal.gwt.client.ui.VMarginInfo;
+import com.vaadin.terminal.gwt.client.ui.layout.ComponentConnectorLayoutSlot;
+import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot;
+
+public abstract class AbstractOrderedLayoutConnector extends
+ AbstractLayoutConnector implements Paintable, DirectionalManagedLayout {
+
+ AbstractOrderedLayoutServerRPC rpc;
+
+ private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler(
+ this) {
+
+ @Override
+ protected ComponentConnector getChildComponent(Element element) {
+ return Util.getConnectorForElement(getConnection(), getWidget(),
+ element);
+ }
+
+ @Override
+ protected LayoutClickRPC getLayoutClickRPC() {
+ return rpc;
+ };
+
+ };
+
+ @Override
+ public void init() {
+ rpc = RpcProxy.create(AbstractOrderedLayoutServerRPC.class, this);
+ getLayoutManager().registerDependency(this,
+ getWidget().spacingMeasureElement);
+ }
+
++ @Override
++ public void onUnregister() {
++ LayoutManager lm = getLayoutManager();
++
++ VMeasuringOrderedLayout layout = getWidget();
++ lm.unregisterDependency(this, layout.spacingMeasureElement);
++
++ // Unregister child caption listeners
++ for (ComponentConnector child : getChildren()) {
++ VLayoutSlot slot = layout.getSlotForChild(child.getWidget());
++ slot.setCaption(null);
++ }
++ }
++
+ @Override
+ public AbstractOrderedLayoutState getState() {
+ return (AbstractOrderedLayoutState) super.getState();
+ }
+
+ public void updateCaption(ComponentConnector component) {
+ VMeasuringOrderedLayout layout = getWidget();
+ if (VCaption.isNeeded(component.getState())) {
+ VLayoutSlot layoutSlot = layout.getSlotForChild(component
+ .getWidget());
+ VCaption caption = layoutSlot.getCaption();
+ if (caption == null) {
+ caption = new VCaption(component, getConnection());
+
+ Widget widget = component.getWidget();
+
+ layout.setCaption(widget, caption);
+ }
+ caption.updateCaption();
+ } else {
+ layout.setCaption(component.getWidget(), null);
+ getLayoutManager().setNeedsUpdate(this);
+ }
+ }
+
+ @Override
+ public VMeasuringOrderedLayout getWidget() {
+ return (VMeasuringOrderedLayout) super.getWidget();
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+ clickEventHandler.handleEventHandlerRegistration();
+
+ VMeasuringOrderedLayout layout = getWidget();
+
+ ValueMap expandRatios = uidl.getMapAttribute("expandRatios");
+ ValueMap alignments = uidl.getMapAttribute("alignments");
+
+ for (ComponentConnector child : getChildren()) {
+ VLayoutSlot slot = layout.getSlotForChild(child.getWidget());
+ String pid = child.getConnectorId();
+
+ AlignmentInfo alignment;
+ if (alignments.containsKey(pid)) {
+ alignment = new AlignmentInfo(alignments.getInt(pid));
+ } else {
+ alignment = AlignmentInfo.TOP_LEFT;
+ }
+ slot.setAlignment(alignment);
+
+ double expandRatio;
+ if (expandRatios.containsKey(pid)) {
+ expandRatio = expandRatios.getRawNumber(pid);
+ } else {
+ expandRatio = 0;
+ }
+ slot.setExpandRatio(expandRatio);
+ }
+
+ layout.updateMarginStyleNames(new VMarginInfo(getState()
+ .getMarginsBitmask()));
+
+ layout.updateSpacingStyleName(getState().isSpacing());
+
+ getLayoutManager().setNeedsUpdate(this);
+ }
+
+ private int getSizeForInnerSize(int size, boolean isVertical) {
+ LayoutManager layoutManager = getLayoutManager();
+ Element element = getWidget().getElement();
+ if (isVertical) {
+ return size + layoutManager.getBorderHeight(element)
+ + layoutManager.getPaddingHeight(element);
+ } else {
+ return size + layoutManager.getBorderWidth(element)
+ + layoutManager.getPaddingWidth(element);
+ }
+ }
+
+ private static String getSizeProperty(boolean isVertical) {
+ return isVertical ? "height" : "width";
+ }
+
+ private boolean isUndefinedInDirection(boolean isVertical) {
+ if (isVertical) {
+ return isUndefinedHeight();
+ } else {
+ return isUndefinedWidth();
+ }
+ }
+
+ private int getInnerSizeInDirection(boolean isVertical) {
+ if (isVertical) {
+ return getLayoutManager().getInnerHeight(getWidget().getElement());
+ } else {
+ return getLayoutManager().getInnerWidth(getWidget().getElement());
+ }
+ }
+
+ private void layoutPrimaryDirection() {
+ VMeasuringOrderedLayout layout = getWidget();
+ boolean isVertical = layout.isVertical;
+ boolean isUndefined = isUndefinedInDirection(isVertical);
+
+ int startPadding = getStartPadding(isVertical);
+ int spacingSize = getSpacingInDirection(isVertical);
+ int allocatedSize;
+
+ if (isUndefined) {
+ allocatedSize = -1;
+ } else {
+ allocatedSize = getInnerSizeInDirection(isVertical);
+ }
+
+ allocatedSize = layout.layoutPrimaryDirection(spacingSize,
+ allocatedSize, startPadding);
+
+ Style ownStyle = getWidget().getElement().getStyle();
+ if (isUndefined) {
- getSizeForInnerSize(allocatedSize, !getWidget().isVertical));
++ int outerSize = getSizeForInnerSize(allocatedSize, isVertical);
++ ownStyle.setPropertyPx(getSizeProperty(isVertical), outerSize);
++ reportUndefinedSize(outerSize, isVertical);
+ } else {
+ ownStyle.setProperty(getSizeProperty(isVertical),
+ getDefinedSize(isVertical));
+ }
+ }
+
++ private void reportUndefinedSize(int outerSize, boolean isVertical) {
++ if (isVertical) {
++ getLayoutManager().reportOuterHeight(this, outerSize);
++ } else {
++ getLayoutManager().reportOuterWidth(this, outerSize);
++ }
++ }
++
+ private int getSpacingInDirection(boolean isVertical) {
+ if (isVertical) {
+ return getLayoutManager().getOuterHeight(
+ getWidget().spacingMeasureElement);
+ } else {
+ return getLayoutManager().getOuterWidth(
+ getWidget().spacingMeasureElement);
+ }
+ }
+
+ private void layoutSecondaryDirection() {
+ VMeasuringOrderedLayout layout = getWidget();
+ boolean isVertical = layout.isVertical;
+ boolean isUndefined = isUndefinedInDirection(!isVertical);
+
+ int startPadding = getStartPadding(!isVertical);
+
+ int allocatedSize;
+ if (isUndefined) {
+ allocatedSize = -1;
+ } else {
+ allocatedSize = getInnerSizeInDirection(!isVertical);
+ }
+
+ allocatedSize = layout.layoutSecondaryDirection(allocatedSize,
+ startPadding);
+
+ Style ownStyle = getWidget().getElement().getStyle();
+
+ if (isUndefined) {
++ int outerSize = getSizeForInnerSize(allocatedSize,
++ !getWidget().isVertical);
+ ownStyle.setPropertyPx(getSizeProperty(!getWidget().isVertical),
++ outerSize);
++ reportUndefinedSize(outerSize, !isVertical);
+ } else {
+ ownStyle.setProperty(getSizeProperty(!getWidget().isVertical),
+ getDefinedSize(!getWidget().isVertical));
+ }
+ }
+
+ private String getDefinedSize(boolean isVertical) {
+ if (isVertical) {
+ return getState().getHeight();
+ } else {
+ return getState().getWidth();
+ }
+ }
+
+ private int getStartPadding(boolean isVertical) {
+ if (isVertical) {
+ return getLayoutManager().getPaddingTop(getWidget().getElement());
+ } else {
+ return getLayoutManager().getPaddingLeft(getWidget().getElement());
+ }
+ }
+
+ public void layoutHorizontally() {
+ if (getWidget().isVertical) {
+ layoutSecondaryDirection();
+ } else {
+ layoutPrimaryDirection();
+ }
+ }
+
+ public void layoutVertically() {
+ if (getWidget().isVertical) {
+ layoutPrimaryDirection();
+ } else {
+ layoutSecondaryDirection();
+ }
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+ List<ComponentConnector> previousChildren = event.getOldChildren();
+ int currentIndex = 0;
+ VMeasuringOrderedLayout layout = getWidget();
+
+ for (ComponentConnector child : getChildren()) {
+ Widget childWidget = child.getWidget();
+ VLayoutSlot slot = layout.getSlotForChild(childWidget);
+
+ if (childWidget.getParent() != layout) {
+ // If the child widget was previously attached to another
+ // AbstractOrderedLayout a slot might be found that belongs to
+ // another AbstractOrderedLayout. In this case we discard it and
+ // create a new slot.
+ slot = new ComponentConnectorLayoutSlot(getWidget()
+ .getStylePrimaryName(), child, this);
+ }
+ layout.addOrMove(slot, currentIndex++);
++ if (child.isRelativeWidth()) {
++ slot.getWrapperElement().getStyle().setWidth(100, Unit.PCT);
++ }
+ }
+
+ for (ComponentConnector child : previousChildren) {
+ if (child.getParent() != this) {
+ // Remove slot if the connector is no longer a child of this
+ // layout
+ layout.removeSlotForWidget(child.getWidget());
+ }
+ }
+
+ };
+
+}
--- /dev/null
- import com.vaadin.terminal.gwt.client.Util;
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.panel;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.MouseEventDetails;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
- implements Paintable, SimpleManagedLayout, PostLayoutListener {
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
++import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.ui.Panel;
+
+@Component(Panel.class)
+public class PanelConnector extends AbstractComponentContainerConnector
-
- Util.runWebkitOverflowAutoFix(panel.contentNode);
++ implements Paintable, SimpleManagedLayout, PostLayoutListener,
++ MayScrollChildren {
+
+ private Integer uidlScrollTop;
+
+ private ClickEventHandler clickEventHandler = new ClickEventHandler(this) {
+
+ @Override
+ protected void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails) {
+ rpc.click(mouseDetails);
+ }
+ };
+
+ private Integer uidlScrollLeft;
+
+ private PanelServerRPC rpc;
+
+ @Override
+ public void init() {
+ rpc = RpcProxy.create(PanelServerRPC.class, this);
+ VPanel panel = getWidget();
+ LayoutManager layoutManager = getLayoutManager();
+
+ layoutManager.registerDependency(this, panel.captionNode);
+ layoutManager.registerDependency(this, panel.bottomDecoration);
+ layoutManager.registerDependency(this, panel.contentNode);
+ }
+
++ @Override
++ public void onUnregister() {
++ VPanel panel = getWidget();
++ LayoutManager layoutManager = getLayoutManager();
++
++ layoutManager.unregisterDependency(this, panel.captionNode);
++ layoutManager.unregisterDependency(this, panel.bottomDecoration);
++ layoutManager.unregisterDependency(this, panel.contentNode);
++ }
++
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (isRealUpdate(uidl)) {
+
+ // Handle caption displaying and style names, prior generics.
+ // Affects size calculations
+
+ // Restore default stylenames
+ getWidget().contentNode.setClassName(VPanel.CLASSNAME + "-content");
+ getWidget().bottomDecoration.setClassName(VPanel.CLASSNAME
+ + "-deco");
+ getWidget().captionNode.setClassName(VPanel.CLASSNAME + "-caption");
+ boolean hasCaption = false;
+ if (getState().getCaption() != null
+ && !"".equals(getState().getCaption())) {
+ getWidget().setCaption(getState().getCaption());
+ hasCaption = true;
+ } else {
+ getWidget().setCaption("");
+ getWidget().captionNode.setClassName(VPanel.CLASSNAME
+ + "-nocaption");
+ }
+
+ // Add proper stylenames for all elements. This way we can prevent
+ // unwanted CSS selector inheritance.
+ final String captionBaseClass = VPanel.CLASSNAME
+ + (hasCaption ? "-caption" : "-nocaption");
+ final String contentBaseClass = VPanel.CLASSNAME + "-content";
+ final String decoBaseClass = VPanel.CLASSNAME + "-deco";
+ String captionClass = captionBaseClass;
+ String contentClass = contentBaseClass;
+ String decoClass = decoBaseClass;
+ if (getState().hasStyles()) {
+ for (String style : getState().getStyles()) {
+ captionClass += " " + captionBaseClass + "-" + style;
+ contentClass += " " + contentBaseClass + "-" + style;
+ decoClass += " " + decoBaseClass + "-" + style;
+ }
+ }
+ getWidget().captionNode.setClassName(captionClass);
+ getWidget().contentNode.setClassName(contentClass);
+ getWidget().bottomDecoration.setClassName(decoClass);
+ }
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ getWidget().client = client;
+ getWidget().id = uidl.getId();
+
+ if (getState().getIcon() != null) {
+ getWidget().setIconUri(getState().getIcon().getURL(), client);
+ } else {
+ getWidget().setIconUri(null, client);
+ }
+
+ getWidget().setErrorIndicatorVisible(
+ null != getState().getErrorMessage());
+
+ // We may have actions attached to this panel
+ if (uidl.getChildCount() > 0) {
+ final int cnt = uidl.getChildCount();
+ for (int i = 0; i < cnt; i++) {
+ UIDL childUidl = uidl.getChildUIDL(i);
+ if (childUidl.getTag().equals("actions")) {
+ if (getWidget().shortcutHandler == null) {
+ getWidget().shortcutHandler = new ShortcutActionHandler(
+ getConnectorId(), client);
+ }
+ getWidget().shortcutHandler.updateActionMap(childUidl);
+ }
+ }
+ }
+
+ if (getState().getScrollTop() != getWidget().scrollTop) {
+ // Sizes are not yet up to date, so changing the scroll position
+ // is deferred to after the layout phase
+ uidlScrollTop = getState().getScrollTop();
+ }
+
+ if (getState().getScrollLeft() != getWidget().scrollLeft) {
+ // Sizes are not yet up to date, so changing the scroll position
+ // is deferred to after the layout phase
+ uidlScrollLeft = getState().getScrollLeft();
+ }
+
+ // And apply tab index
+ getWidget().contentNode.setTabIndex(getState().getTabIndex());
+ }
+
+ public void updateCaption(ComponentConnector component) {
+ // NOP: layouts caption, errors etc not rendered in Panel
+ }
+
+ @Override
+ public VPanel getWidget() {
+ return (VPanel) super.getWidget();
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VPanel.class);
+ }
+
+ public void layout() {
+ updateSizes();
+ }
+
+ void updateSizes() {
+ VPanel panel = getWidget();
+
+ LayoutManager layoutManager = getLayoutManager();
+ int top = layoutManager.getInnerHeight(panel.captionNode);
+ int bottom = layoutManager.getInnerHeight(panel.bottomDecoration);
+
+ Style style = panel.getElement().getStyle();
+ panel.captionNode.getStyle().setMarginTop(-top, Unit.PX);
+ panel.bottomDecoration.getStyle().setMarginBottom(-bottom, Unit.PX);
+ style.setPaddingTop(top, Unit.PX);
+ style.setPaddingBottom(bottom, Unit.PX);
+
+ // Update scroll positions
+ panel.contentNode.setScrollTop(panel.scrollTop);
+ panel.contentNode.setScrollLeft(panel.scrollLeft);
+ // Read actual value back to ensure update logic is correct
+ panel.scrollTop = panel.contentNode.getScrollTop();
+ panel.scrollLeft = panel.contentNode.getScrollLeft();
+ }
+
+ public void postLayout() {
+ VPanel panel = getWidget();
+ if (uidlScrollTop != null) {
+ panel.contentNode.setScrollTop(uidlScrollTop.intValue());
+ // Read actual value back to ensure update logic is correct
+ // TODO Does this trigger reflows?
+ panel.scrollTop = panel.contentNode.getScrollTop();
+ uidlScrollTop = null;
+ }
+
+ if (uidlScrollLeft != null) {
+ panel.contentNode.setScrollLeft(uidlScrollLeft.intValue());
+ // Read actual value back to ensure update logic is correct
+ // TODO Does this trigger reflows?
+ panel.scrollLeft = panel.contentNode.getScrollLeft();
+ uidlScrollLeft = null;
+ }
+ }
+
+ @Override
+ public PanelState getState() {
+ return (PanelState) super.getState();
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+ // We always have 1 child, unless the child is hidden
+ Widget newChildWidget = null;
+ if (getChildren().size() == 1) {
+ ComponentConnector newChild = getChildren().get(0);
+ newChildWidget = newChild.getWidget();
+ }
+
+ getWidget().setWidget(newChildWidget);
+ }
+
+}
--- /dev/null
- import com.vaadin.terminal.gwt.client.Util;
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.root;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.MouseEventDetails;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
- implements Paintable {
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.Component.LoadStyle;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
++import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
+import com.vaadin.terminal.gwt.client.ui.window.WindowConnector;
+import com.vaadin.ui.Root;
+
+@Component(value = Root.class, loadStyle = LoadStyle.EAGER)
+public class RootConnector extends AbstractComponentContainerConnector
- // Safari workaround must be run after scrollTop is updated as it sets
- // scrollTop using a deferred command.
- if (BrowserInfo.get().isSafari()) {
- Util.runWebkitOverflowAutoFix(getWidget().getElement());
- }
-
++ implements Paintable, MayScrollChildren {
+
+ private RootServerRPC rpc = RpcProxy.create(RootServerRPC.class, this);
+
+ private HandlerRegistration childStateChangeHandlerRegistration;
+
+ private final StateChangeHandler childStateChangeHandler = new StateChangeHandler() {
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ // TODO Should use a more specific handler that only reacts to
+ // size changes
+ onChildSizeChange();
+ }
+ };
+
+ @Override
+ protected void init() {
+ super.init();
+ }
+
+ public void updateFromUIDL(final UIDL uidl, ApplicationConnection client) {
+ ConnectorMap paintableMap = ConnectorMap.get(getConnection());
+ getWidget().rendering = true;
+ getWidget().id = getConnectorId();
+ boolean firstPaint = getWidget().connection == null;
+ getWidget().connection = client;
+
+ getWidget().immediate = getState().isImmediate();
+ getWidget().resizeLazy = uidl.hasAttribute(VRoot.RESIZE_LAZY);
+ String newTheme = uidl.getStringAttribute("theme");
+ if (getWidget().theme != null && !newTheme.equals(getWidget().theme)) {
+ // Complete page refresh is needed due css can affect layout
+ // calculations etc
+ getWidget().reloadHostPage();
+ } else {
+ getWidget().theme = newTheme;
+ }
+ // this also implicitly removes old styles
+ String styles = "";
+ styles += getWidget().getStylePrimaryName() + " ";
+ if (getState().hasStyles()) {
+ for (String style : getState().getStyles()) {
+ styles += style + " ";
+ }
+ }
+ if (!client.getConfiguration().isStandalone()) {
+ styles += getWidget().getStylePrimaryName() + "-embedded";
+ }
+ getWidget().setStyleName(styles.trim());
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ if (!getWidget().isEmbedded() && getState().getCaption() != null) {
+ // only change window title if we're in charge of the whole page
+ com.google.gwt.user.client.Window.setTitle(getState().getCaption());
+ }
+
+ // Process children
+ int childIndex = 0;
+
+ // Open URL:s
+ boolean isClosed = false; // was this window closed?
+ while (childIndex < uidl.getChildCount()
+ && "open".equals(uidl.getChildUIDL(childIndex).getTag())) {
+ final UIDL open = uidl.getChildUIDL(childIndex);
+ final String url = client.translateVaadinUri(open
+ .getStringAttribute("src"));
+ final String target = open.getStringAttribute("name");
+ if (target == null) {
+ // source will be opened to this browser window, but we may have
+ // to finish rendering this window in case this is a download
+ // (and window stays open).
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ VRoot.goTo(url);
+ }
+ });
+ } else if ("_self".equals(target)) {
+ // This window is closing (for sure). Only other opens are
+ // relevant in this change. See #3558, #2144
+ isClosed = true;
+ VRoot.goTo(url);
+ } else {
+ String options;
+ if (open.hasAttribute("border")) {
+ if (open.getStringAttribute("border").equals("minimal")) {
+ options = "menubar=yes,location=no,status=no";
+ } else {
+ options = "menubar=no,location=no,status=no";
+ }
+
+ } else {
+ options = "resizable=yes,menubar=yes,toolbar=yes,directories=yes,location=yes,scrollbars=yes,status=yes";
+ }
+
+ if (open.hasAttribute("width")) {
+ int w = open.getIntAttribute("width");
+ options += ",width=" + w;
+ }
+ if (open.hasAttribute("height")) {
+ int h = open.getIntAttribute("height");
+ options += ",height=" + h;
+ }
+
+ Window.open(url, target, options);
+ }
+ childIndex++;
+ }
+ if (isClosed) {
+ // don't render the content, something else will be opened to this
+ // browser view
+ getWidget().rendering = false;
+ return;
+ }
+
+ // Handle other UIDL children
+ UIDL childUidl;
+ while ((childUidl = uidl.getChildUIDL(childIndex++)) != null) {
+ String tag = childUidl.getTag().intern();
+ if (tag == "actions") {
+ if (getWidget().actionHandler == null) {
+ getWidget().actionHandler = new ShortcutActionHandler(
+ getWidget().id, client);
+ }
+ getWidget().actionHandler.updateActionMap(childUidl);
+ } else if (tag == "execJS") {
+ String script = childUidl.getStringAttribute("script");
+ VRoot.eval(script);
+ } else if (tag == "notifications") {
+ for (final Iterator<?> it = childUidl.getChildIterator(); it
+ .hasNext();) {
+ final UIDL notification = (UIDL) it.next();
+ VNotification.showNotification(client, notification);
+ }
+ }
+ }
+
+ if (uidl.hasAttribute("focused")) {
+ // set focused component when render phase is finished
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ ComponentConnector paintable = (ComponentConnector) uidl
+ .getPaintableAttribute("focused", getConnection());
+
+ final Widget toBeFocused = paintable.getWidget();
+ /*
+ * Two types of Widgets can be focused, either implementing
+ * GWT HasFocus of a thinner Vaadin specific Focusable
+ * interface.
+ */
+ if (toBeFocused instanceof com.google.gwt.user.client.ui.Focusable) {
+ final com.google.gwt.user.client.ui.Focusable toBeFocusedWidget = (com.google.gwt.user.client.ui.Focusable) toBeFocused;
+ toBeFocusedWidget.setFocus(true);
+ } else if (toBeFocused instanceof Focusable) {
+ ((Focusable) toBeFocused).focus();
+ } else {
+ VConsole.log("Could not focus component");
+ }
+ }
+ });
+ }
+
+ // Add window listeners on first paint, to prevent premature
+ // variablechanges
+ if (firstPaint) {
+ Window.addWindowClosingHandler(getWidget());
+ Window.addResizeHandler(getWidget());
+ }
+
+ // finally set scroll position from UIDL
+ if (uidl.hasVariable("scrollTop")) {
+ getWidget().scrollable = true;
+ getWidget().scrollTop = uidl.getIntVariable("scrollTop");
+ DOM.setElementPropertyInt(getWidget().getElement(), "scrollTop",
+ getWidget().scrollTop);
+ getWidget().scrollLeft = uidl.getIntVariable("scrollLeft");
+ DOM.setElementPropertyInt(getWidget().getElement(), "scrollLeft",
+ getWidget().scrollLeft);
+ } else {
+ getWidget().scrollable = false;
+ }
+
+ if (uidl.hasAttribute("scrollTo")) {
+ final ComponentConnector connector = (ComponentConnector) uidl
+ .getPaintableAttribute("scrollTo", getConnection());
+ scrollIntoView(connector);
+ }
+
+ if (uidl.hasAttribute(VRoot.FRAGMENT_VARIABLE)) {
+ getWidget().currentFragment = uidl
+ .getStringAttribute(VRoot.FRAGMENT_VARIABLE);
+ if (!getWidget().currentFragment.equals(History.getToken())) {
+ History.newItem(getWidget().currentFragment, true);
+ }
+ } else {
+ // Initial request for which the server doesn't yet have a fragment
+ // (and haven't shown any interest in getting one)
+ getWidget().currentFragment = History.getToken();
+
+ // Include current fragment in the next request
+ client.updateVariable(getWidget().id, VRoot.FRAGMENT_VARIABLE,
+ getWidget().currentFragment, false);
+ }
+
+ getWidget().rendering = false;
+ }
+
+ public void init(String rootPanelId,
+ ApplicationConnection applicationConnection) {
+ DOM.sinkEvents(getWidget().getElement(), Event.ONKEYDOWN
+ | Event.ONSCROLL);
+
+ // iview is focused when created so element needs tabIndex
+ // 1 due 0 is at the end of natural tabbing order
+ DOM.setElementProperty(getWidget().getElement(), "tabIndex", "1");
+
+ RootPanel root = RootPanel.get(rootPanelId);
+
+ // Remove the v-app-loading or any splash screen added inside the div by
+ // the user
+ root.getElement().setInnerHTML("");
+
+ root.addStyleName("v-theme-"
+ + applicationConnection.getConfiguration().getThemeName());
+
+ root.add(getWidget());
+
+ if (applicationConnection.getConfiguration().isStandalone()) {
+ // set focus to iview element by default to listen possible keyboard
+ // shortcuts. For embedded applications this is unacceptable as we
+ // don't want to steal focus from the main page nor we don't want
+ // side-effects from focusing (scrollIntoView).
+ getWidget().getElement().focus();
+ }
+ }
+
+ private ClickEventHandler clickEventHandler = new ClickEventHandler(this) {
+
+ @Override
+ protected void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails) {
+ rpc.click(mouseDetails);
+ }
+
+ };
+
+ public void updateCaption(ComponentConnector component) {
+ // NOP The main view never draws caption for its layout
+ }
+
+ @Override
+ public VRoot getWidget() {
+ return (VRoot) super.getWidget();
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VRoot.class);
+ }
+
+ protected ComponentConnector getContent() {
+ return (ComponentConnector) getState().getContent();
+ }
+
+ protected void onChildSizeChange() {
+ ComponentConnector child = getContent();
+ Style childStyle = child.getWidget().getElement().getStyle();
+ /*
+ * Must set absolute position if the child has relative height and
+ * there's a chance of horizontal scrolling as some browsers will
+ * otherwise not take the scrollbar into account when calculating the
+ * height. Assuming v-view does not have an undefined width for now, see
+ * #8460.
+ */
+ if (child.isRelativeHeight() && !BrowserInfo.get().isIE9()) {
+ childStyle.setPosition(Position.ABSOLUTE);
+ } else {
+ childStyle.clearPosition();
+ }
+ }
+
+ /**
+ * Checks if the given sub window is a child of this Root Connector
+ *
+ * @deprecated Should be replaced by a more generic mechanism for getting
+ * non-ComponentConnector children
+ * @param wc
+ * @return
+ */
+ @Deprecated
+ public boolean hasSubWindow(WindowConnector wc) {
+ return getChildren().contains(wc);
+ }
+
+ /**
+ * Return an iterator for current subwindows. This method is meant for
+ * testing purposes only.
+ *
+ * @return
+ */
+ public List<WindowConnector> getSubWindows() {
+ ArrayList<WindowConnector> windows = new ArrayList<WindowConnector>();
+ for (ComponentConnector child : getChildren()) {
+ if (child instanceof WindowConnector) {
+ windows.add((WindowConnector) child);
+ }
+ }
+ return windows;
+ }
+
+ @Override
+ public RootState getState() {
+ return (RootState) super.getState();
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ ComponentConnector oldChild = null;
+ ComponentConnector newChild = getContent();
+
+ for (ComponentConnector c : event.getOldChildren()) {
+ if (!(c instanceof WindowConnector)) {
+ oldChild = c;
+ break;
+ }
+ }
+
+ if (oldChild != newChild) {
+ if (childStateChangeHandlerRegistration != null) {
+ childStateChangeHandlerRegistration.removeHandler();
+ childStateChangeHandlerRegistration = null;
+ }
+ getWidget().setWidget(newChild.getWidget());
+ childStateChangeHandlerRegistration = newChild
+ .addStateChangeHandler(childStateChangeHandler);
+ // Must handle new child here as state change events are already
+ // fired
+ onChildSizeChange();
+ }
+
+ for (ComponentConnector c : getChildren()) {
+ if (c instanceof WindowConnector) {
+ WindowConnector wc = (WindowConnector) c;
+ wc.setWindowOrderAndPosition();
+ }
+ }
+
+ // Close removed sub windows
+ for (ComponentConnector c : event.getOldChildren()) {
+ if (c.getParent() != this && c instanceof WindowConnector) {
+ ((WindowConnector) c).getWidget().hide();
+ }
+ }
+ }
+
+ /**
+ * Tries to scroll the viewport so that the given connector is in view.
+ *
+ * @param componentConnector
+ * The connector which should be visible
+ *
+ */
+ public void scrollIntoView(final ComponentConnector componentConnector) {
+ if (componentConnector == null) {
+ return;
+ }
+
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ componentConnector.getWidget().getElement().scrollIntoView();
+ }
+ });
+ }
+
+}
--- /dev/null
- import com.vaadin.terminal.gwt.client.Util;
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.root;
+
+import java.util.ArrayList;
+
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+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.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
++import com.vaadin.terminal.gwt.client.ComponentConnector;
++import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Focusable;
- Util.runWebkitOverflowAutoFix(getElement());
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+/**
+ *
+ */
+public class VRoot extends SimplePanel implements ResizeHandler,
+ Window.ClosingHandler, ShortcutActionHandlerOwner, Focusable {
+
+ public static final String FRAGMENT_VARIABLE = "fragment";
+
+ private static final String CLASSNAME = "v-view";
+
+ public static final String NOTIFICATION_HTML_CONTENT_NOT_ALLOWED = "useplain";
+
+ String theme;
+
+ String id;
+
+ ShortcutActionHandler actionHandler;
+
+ /** stored width for IE resize optimization */
+ private int width;
+
+ /** stored height for IE resize optimization */
+ private int height;
+
+ ApplicationConnection connection;
+
+ /** Identifies the click event */
+ public static final String CLICK_EVENT_ID = "click";
+
+ /**
+ * We are postponing resize process with IE. IE bugs with scrollbars in some
+ * situations, that causes false onWindowResized calls. With Timer we will
+ * give IE some time to decide if it really wants to keep current size
+ * (scrollbars).
+ */
+ private Timer resizeTimer;
+
+ int scrollTop;
+
+ int scrollLeft;
+
+ boolean rendering;
+
+ boolean scrollable;
+
+ boolean immediate;
+
+ boolean resizeLazy = false;
+
+ /**
+ * Attribute name for the lazy resize setting .
+ */
+ public static final String RESIZE_LAZY = "rL";
+
+ private HandlerRegistration historyHandlerRegistration;
+
+ /**
+ * The current URI fragment, used to avoid sending updates if nothing has
+ * changed.
+ */
+ String currentFragment;
+
+ /**
+ * Listener for URI fragment changes. Notifies the server of the new value
+ * whenever the value changes.
+ */
+ private final ValueChangeHandler<String> historyChangeHandler = new ValueChangeHandler<String>() {
+ public void onValueChange(ValueChangeEvent<String> event) {
+ String newFragment = event.getValue();
+
+ // Send the new fragment to the server if it has changed
+ if (!newFragment.equals(currentFragment) && connection != null) {
+ currentFragment = newFragment;
+ connection.updateVariable(id, FRAGMENT_VARIABLE, newFragment,
+ true);
+ }
+ }
+ };
+
+ private VLazyExecutor delayedResizeExecutor = new VLazyExecutor(200,
+ new ScheduledCommand() {
+ public void execute() {
+ windowSizeMaybeChanged(Window.getClientWidth(),
+ Window.getClientHeight());
+ }
+
+ });
+
+ public VRoot() {
+ super();
+ setStyleName(CLASSNAME);
+
+ // Allow focusing the view by using the focus() method, the view
+ // should not be in the document focus flow
+ getElement().setTabIndex(-1);
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ historyHandlerRegistration = History
+ .addValueChangeHandler(historyChangeHandler);
+ currentFragment = History.getToken();
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ historyHandlerRegistration.removeHandler();
+ historyHandlerRegistration = null;
+ }
+
+ /**
+ * Called when the window might have been resized.
+ *
+ * @param newWidth
+ * The new width of the window
+ * @param newHeight
+ * The new height of the window
+ */
+ protected void windowSizeMaybeChanged(int newWidth, int newHeight) {
+ boolean changed = false;
++ ComponentConnector connector = ConnectorMap.get(connection)
++ .getConnector(this);
+ if (width != newWidth) {
+ width = newWidth;
+ changed = true;
++ connector.getLayoutManager().reportOuterWidth(connector, newWidth);
+ VConsole.log("New window width: " + width);
+ }
+ if (height != newHeight) {
+ height = newHeight;
+ changed = true;
++ connector.getLayoutManager()
++ .reportOuterHeight(connector, newHeight);
+ VConsole.log("New window height: " + height);
+ }
+ if (changed) {
+ VConsole.log("Running layout functions due to window resize");
- connection.doLayout(false);
+
+ sendClientResized();
+
++ connector.getLayoutManager().layoutNow();
+ }
+ }
+
+ public String getTheme() {
+ return theme;
+ }
+
+ /**
+ * Used to reload host page on theme changes.
+ */
+ static native void reloadHostPage()
+ /*-{
+ $wnd.location.reload();
+ }-*/;
+
+ /**
+ * Evaluate the given script in the browser document.
+ *
+ * @param script
+ * Script to be executed.
+ */
+ static native void eval(String script)
+ /*-{
+ try {
+ if (script == null) return;
+ $wnd.eval(script);
+ } catch (e) {
+ }
+ }-*/;
+
+ /**
+ * Returns true if the body is NOT generated, i.e if someone else has made
+ * the page that we're running in. Otherwise we're in charge of the whole
+ * page.
+ *
+ * @return true if we're running embedded
+ */
+ public boolean isEmbedded() {
+ return !getElement().getOwnerDocument().getBody().getClassName()
+ .contains(ApplicationConnection.GENERATED_BODY_CLASSNAME);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ int type = DOM.eventGetType(event);
+ if (type == Event.ONKEYDOWN && actionHandler != null) {
+ actionHandler.handleKeyboardEvent(event);
+ return;
+ } else if (scrollable && type == Event.ONSCROLL) {
+ updateScrollPosition();
+ }
+ }
+
+ /**
+ * Updates scroll position from DOM and saves variables to server.
+ */
+ private void updateScrollPosition() {
+ int oldTop = scrollTop;
+ int oldLeft = scrollLeft;
+ scrollTop = DOM.getElementPropertyInt(getElement(), "scrollTop");
+ scrollLeft = DOM.getElementPropertyInt(getElement(), "scrollLeft");
+ if (connection != null && !rendering) {
+ if (oldTop != scrollTop) {
+ connection.updateVariable(id, "scrollTop", scrollTop, false);
+ }
+ if (oldLeft != scrollLeft) {
+ connection.updateVariable(id, "scrollLeft", scrollLeft, false);
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.logical.shared.ResizeHandler#onResize(com.google
+ * .gwt.event.logical.shared.ResizeEvent)
+ */
+ public void onResize(ResizeEvent event) {
+ onResize();
+ }
+
+ /**
+ * Called when a resize event is received.
+ */
+ void onResize() {
+ /*
+ * IE (pre IE9 at least) will give us some false resize events due to
+ * problems with scrollbars. Firefox 3 might also produce some extra
+ * events. We postpone both the re-layouting and the server side event
+ * for a while to deal with these issues.
+ *
+ * We may also postpone these events to avoid slowness when resizing the
+ * browser window. Constantly recalculating the layout causes the resize
+ * operation to be really slow with complex layouts.
+ */
+ boolean lazy = resizeLazy || BrowserInfo.get().isIE8();
+
+ if (lazy) {
+ delayedResizeExecutor.trigger();
+ } else {
+ windowSizeMaybeChanged(Window.getClientWidth(),
+ Window.getClientHeight());
+ }
+ }
+
+ /**
+ * Send new dimensions to the server.
+ */
+ private void sendClientResized() {
+ connection.updateVariable(id, "height", height, false);
+ connection.updateVariable(id, "width", width, immediate);
+ }
+
+ public native static void goTo(String url)
+ /*-{
+ $wnd.location = url;
+ }-*/;
+
+ public void onWindowClosing(Window.ClosingEvent event) {
+ // Change focus on this window in order to ensure that all state is
+ // collected from textfields
+ // TODO this is a naive hack, that only works with text fields and may
+ // cause some odd issues. Should be replaced with a decent solution, see
+ // also related BeforeShortcutActionListener interface. Same interface
+ // might be usable here.
+ VTextField.flushChangesFromFocusedTextField();
+ }
+
+ private native static void loadAppIdListFromDOM(ArrayList<String> list)
+ /*-{
+ var j;
+ for(j in $wnd.vaadin.vaadinConfigurations) {
+ list.@java.util.Collection::add(Ljava/lang/Object;)(j);
+ }
+ }-*/;
+
+ public ShortcutActionHandler getShortcutActionHandler() {
+ return actionHandler;
+ }
+
+ public void focus() {
+ getElement().focus();
+ }
+
+}
--- /dev/null
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.splitpanel;
+
+import java.util.LinkedList;
++import java.util.List;
+
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.DomEvent;
+import com.google.gwt.event.dom.client.DomEvent.Type;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
++import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.MouseEventDetails;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.splitpanel.AbstractSplitPanelState.SplitterState;
+import com.vaadin.terminal.gwt.client.ui.splitpanel.VAbstractSplitPanel.SplitterMoveHandler;
+import com.vaadin.terminal.gwt.client.ui.splitpanel.VAbstractSplitPanel.SplitterMoveHandler.SplitterMoveEvent;
+
+public abstract class AbstractSplitPanelConnector extends
+ AbstractComponentContainerConnector implements SimpleManagedLayout {
+
+ private AbstractSplitPanelRPC rpc;
+
+ @Override
+ protected void init() {
+ super.init();
+ rpc = RpcProxy.create(AbstractSplitPanelRPC.class, this);
+ // TODO Remove
+ getWidget().client = getConnection();
+
+ getWidget().addHandler(new SplitterMoveHandler() {
+
+ public void splitterMoved(SplitterMoveEvent event) {
+ String position = getWidget().getSplitterPosition();
+ float pos = 0;
+ if (position.indexOf("%") > 0) {
+ // Send % values as a fraction to avoid that the splitter
+ // "jumps" when server responds with the integer pct value
+ // (e.g. dragged 16.6% -> should not jump to 17%)
+ pos = Float.valueOf(position.substring(0,
+ position.length() - 1));
+ } else {
+ pos = Integer.parseInt(position.substring(0,
+ position.length() - 2));
+ }
+
+ rpc.setSplitterPosition(pos);
+ }
+
+ }, SplitterMoveEvent.TYPE);
+ }
+
+ public void updateCaption(ComponentConnector component) {
+ // TODO Implement caption handling
+ }
+
+ ClickEventHandler clickEventHandler = new ClickEventHandler(this) {
+
+ @Override
+ protected <H extends EventHandler> HandlerRegistration registerHandler(
+ H handler, Type<H> type) {
+ if ((Event.getEventsSunk(getWidget().splitter) & Event
+ .getTypeInt(type.getName())) != 0) {
+ // If we are already sinking the event for the splitter we do
+ // not want to additionally sink it for the root element
+ return getWidget().addHandler(handler, type);
+ } else {
+ return getWidget().addDomHandler(handler, type);
+ }
+ }
+
+ @Override
+ protected boolean shouldFireEvent(DomEvent<?> event) {
+ Element target = event.getNativeEvent().getEventTarget().cast();
+ if (!getWidget().splitter.isOrHasChild(target)) {
+ return false;
+ }
+
+ return super.shouldFireEvent(event);
+ };
+
+ @Override
+ protected Element getRelativeToElement() {
+ return getWidget().splitter;
+ };
+
+ @Override
+ protected void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails) {
+ rpc.splitterClick(mouseDetails);
+ }
+
+ };
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ getWidget().immediate = getState().isImmediate();
+
+ getWidget().setEnabled(isEnabled());
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ if (getState().hasStyles()) {
+ getWidget().componentStyleNames = getState().getStyles();
+ } else {
+ getWidget().componentStyleNames = new LinkedList<String>();
+ }
+
+ // Splitter updates
+ SplitterState splitterState = getState().getSplitterState();
+
+ getWidget().setLocked(splitterState.isLocked());
+ getWidget().setPositionReversed(splitterState.isPositionReversed());
+
+ getWidget().setStylenames();
+
+ getWidget().position = splitterState.getPosition()
+ + splitterState.getPositionUnit();
+
+ // This is needed at least for cases like #3458 to take
+ // appearing/disappearing scrollbars into account.
+ getConnection().runDescendentsLayout(getWidget());
+
+ getLayoutManager().setNeedsUpdate(this);
+
+ }
+
+ public void layout() {
+ VAbstractSplitPanel splitPanel = getWidget();
+ splitPanel.setSplitPosition(splitPanel.position);
+ splitPanel.updateSizes();
++ // Report relative sizes in other direction for quicker propagation
++ List<ComponentConnector> children = getChildren();
++ for (ComponentConnector child : children) {
++ reportOtherDimension(child);
++ }
++ }
++
++ private void reportOtherDimension(ComponentConnector child) {
++ LayoutManager layoutManager = getLayoutManager();
++ if (this instanceof HorizontalSplitPanelConnector) {
++ if (child.isRelativeHeight()) {
++ int height = layoutManager.getInnerHeight(getWidget()
++ .getElement());
++ layoutManager.reportHeightAssignedToRelative(child, height);
++ }
++ } else {
++ if (child.isRelativeWidth()) {
++ int width = layoutManager.getInnerWidth(getWidget()
++ .getElement());
++ layoutManager.reportWidthAssignedToRelative(child, width);
++ }
++ }
+ }
+
+ @Override
+ public VAbstractSplitPanel getWidget() {
+ return (VAbstractSplitPanel) super.getWidget();
+ }
+
+ @Override
+ protected abstract VAbstractSplitPanel createWidget();
+
+ @Override
+ public AbstractSplitPanelState getState() {
+ return (AbstractSplitPanelState) super.getState();
+ }
+
+ private ComponentConnector getFirstChild() {
+ return (ComponentConnector) getState().getFirstChild();
+ }
+
+ private ComponentConnector getSecondChild() {
+ return (ComponentConnector) getState().getSecondChild();
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ Widget newFirstChildWidget = null;
+ if (getFirstChild() != null) {
+ newFirstChildWidget = getFirstChild().getWidget();
+ }
+ getWidget().setFirstWidget(newFirstChildWidget);
+
+ Widget newSecondChildWidget = null;
+ if (getSecondChild() != null) {
+ newSecondChildWidget = getSecondChild().getWidget();
+ }
+ getWidget().setSecondWidget(newSecondChildWidget);
+ }
+}
--- /dev/null
- client.doLayout(false);
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.splitpanel;
+
+import java.util.List;
+
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.Style;
+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.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
++import com.vaadin.terminal.gwt.client.ComponentConnector;
++import com.vaadin.terminal.gwt.client.ConnectorMap;
++import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+import com.vaadin.terminal.gwt.client.ui.splitpanel.VAbstractSplitPanel.SplitterMoveHandler.SplitterMoveEvent;
+
+public class VAbstractSplitPanel extends ComplexPanel {
+
+ private boolean enabled = false;
+
+ public static final String CLASSNAME = "v-splitpanel";
+
+ public static final int ORIENTATION_HORIZONTAL = 0;
+
+ public static final int ORIENTATION_VERTICAL = 1;
+
+ private static final int MIN_SIZE = 30;
+
+ private int orientation = ORIENTATION_HORIZONTAL;
+
+ Widget firstChild;
+
+ Widget secondChild;
+
+ private final Element wrapper = DOM.createDiv();
+
+ private final Element firstContainer = DOM.createDiv();
+
+ private final Element secondContainer = DOM.createDiv();
+
+ final Element splitter = DOM.createDiv();
+
+ private boolean resizing;
+
+ private boolean resized = false;
+
+ private int origX;
+
+ private int origY;
+
+ private int origMouseX;
+
+ private int origMouseY;
+
+ private boolean locked = false;
+
+ private boolean positionReversed = false;
+
+ List<String> componentStyleNames;
+
+ private Element draggingCurtain;
+
+ ApplicationConnection client;
+
+ boolean immediate;
+
+ /* The current position of the split handle in either percentages or pixels */
+ String position;
+
+ protected Element scrolledContainer;
+
+ protected int origScrollTop;
+
+ private TouchScrollDelegate touchScrollDelegate;
+
+ public VAbstractSplitPanel() {
+ this(ORIENTATION_HORIZONTAL);
+ }
+
+ public VAbstractSplitPanel(int orientation) {
+ setElement(DOM.createDiv());
+ switch (orientation) {
+ case ORIENTATION_HORIZONTAL:
+ setStyleName(CLASSNAME + "-horizontal");
+ break;
+ case ORIENTATION_VERTICAL:
+ default:
+ setStyleName(CLASSNAME + "-vertical");
+ break;
+ }
+ // size below will be overridden in update from uidl, initial size
+ // needed to keep IE alive
+ setWidth(MIN_SIZE + "px");
+ setHeight(MIN_SIZE + "px");
+ constructDom();
+ setOrientation(orientation);
+ sinkEvents(Event.MOUSEEVENTS);
+
+ addDomHandler(new TouchCancelHandler() {
+ public void onTouchCancel(TouchCancelEvent event) {
+ // TODO When does this actually happen??
+ VConsole.log("TOUCH CANCEL");
+ }
+ }, TouchCancelEvent.getType());
+ addDomHandler(new TouchStartHandler() {
+ public void onTouchStart(TouchStartEvent event) {
+ Node target = event.getTouches().get(0).getTarget().cast();
+ if (splitter.isOrHasChild(target)) {
+ onMouseDown(Event.as(event.getNativeEvent()));
+ } else {
+ getTouchScrollDelegate().onTouchStart(event);
+ }
+ }
+
+ }, TouchStartEvent.getType());
+ addDomHandler(new TouchMoveHandler() {
+ public void onTouchMove(TouchMoveEvent event) {
+ if (resizing) {
+ onMouseMove(Event.as(event.getNativeEvent()));
+ }
+ }
+ }, TouchMoveEvent.getType());
+ addDomHandler(new TouchEndHandler() {
+ public void onTouchEnd(TouchEndEvent event) {
+ if (resizing) {
+ onMouseUp(Event.as(event.getNativeEvent()));
+ }
+ }
+ }, TouchEndEvent.getType());
+
+ }
+
+ private TouchScrollDelegate getTouchScrollDelegate() {
+ if (touchScrollDelegate == null) {
+ touchScrollDelegate = new TouchScrollDelegate(firstContainer,
+ secondContainer);
+ }
+ return touchScrollDelegate;
+ }
+
+ protected void constructDom() {
+ DOM.appendChild(splitter, DOM.createDiv()); // for styling
+ DOM.appendChild(getElement(), wrapper);
+ DOM.setStyleAttribute(wrapper, "position", "relative");
+ DOM.setStyleAttribute(wrapper, "width", "100%");
+ DOM.setStyleAttribute(wrapper, "height", "100%");
+
+ DOM.appendChild(wrapper, secondContainer);
+ DOM.appendChild(wrapper, firstContainer);
+ DOM.appendChild(wrapper, splitter);
+
+ DOM.setStyleAttribute(splitter, "position", "absolute");
+ DOM.setStyleAttribute(secondContainer, "position", "absolute");
+
+ DOM.setStyleAttribute(firstContainer, "overflow", "auto");
+ DOM.setStyleAttribute(secondContainer, "overflow", "auto");
+
+ }
+
+ private void setOrientation(int orientation) {
+ this.orientation = orientation;
+ if (orientation == ORIENTATION_HORIZONTAL) {
+ DOM.setStyleAttribute(splitter, "height", "100%");
+ DOM.setStyleAttribute(splitter, "top", "0");
+ DOM.setStyleAttribute(firstContainer, "height", "100%");
+ DOM.setStyleAttribute(secondContainer, "height", "100%");
+ } else {
+ DOM.setStyleAttribute(splitter, "width", "100%");
+ DOM.setStyleAttribute(splitter, "left", "0");
+ DOM.setStyleAttribute(firstContainer, "width", "100%");
+ DOM.setStyleAttribute(secondContainer, "width", "100%");
+ }
+
+ DOM.setElementProperty(firstContainer, "className", CLASSNAME
+ + "-first-container");
+ DOM.setElementProperty(secondContainer, "className", CLASSNAME
+ + "-second-container");
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ boolean removed = super.remove(w);
+ if (removed) {
+ if (firstChild == w) {
+ firstChild = null;
+ } else {
+ secondChild = null;
+ }
+ }
+ return removed;
+ }
+
+ void setLocked(boolean newValue) {
+ if (locked != newValue) {
+ locked = newValue;
+ splitterSize = -1;
+ setStylenames();
+ }
+ }
+
+ void setPositionReversed(boolean reversed) {
+ if (positionReversed != reversed) {
+ if (orientation == ORIENTATION_HORIZONTAL) {
+ DOM.setStyleAttribute(splitter, "right", "");
+ DOM.setStyleAttribute(splitter, "left", "");
+ } else if (orientation == ORIENTATION_VERTICAL) {
+ DOM.setStyleAttribute(splitter, "top", "");
+ DOM.setStyleAttribute(splitter, "bottom", "");
+ }
+
+ positionReversed = reversed;
+ }
+ }
+
+ void setSplitPosition(String pos) {
+ if (pos == null) {
+ return;
+ }
+
+ // Convert percentage values to pixels
+ if (pos.indexOf("%") > 0) {
+ int size = orientation == ORIENTATION_HORIZONTAL ? getOffsetWidth()
+ : getOffsetHeight();
+ float percentage = Float.parseFloat(pos.substring(0,
+ pos.length() - 1));
+ pos = percentage / 100 * size + "px";
+ }
+
+ String attributeName;
+ if (orientation == ORIENTATION_HORIZONTAL) {
+ if (positionReversed) {
+ attributeName = "right";
+ } else {
+ attributeName = "left";
+ }
+ } else {
+ if (positionReversed) {
+ attributeName = "bottom";
+ } else {
+ attributeName = "top";
+ }
+ }
+
+ Style style = splitter.getStyle();
+ if (!pos.equals(style.getProperty(attributeName))) {
+ style.setProperty(attributeName, pos);
+ updateSizes();
+ }
+ }
+
+ void updateSizes() {
+ if (!isAttached()) {
+ return;
+ }
+
+ int wholeSize;
+ int pixelPosition;
+
+ switch (orientation) {
+ case ORIENTATION_HORIZONTAL:
+ wholeSize = DOM.getElementPropertyInt(wrapper, "clientWidth");
+ pixelPosition = DOM.getElementPropertyInt(splitter, "offsetLeft");
+
+ // reposition splitter in case it is out of box
+ if ((pixelPosition > 0 && pixelPosition + getSplitterSize() > wholeSize)
+ || (positionReversed && pixelPosition < 0)) {
+ pixelPosition = wholeSize - getSplitterSize();
+ if (pixelPosition < 0) {
+ pixelPosition = 0;
+ }
+ setSplitPosition(pixelPosition + "px");
+ return;
+ }
+
+ DOM.setStyleAttribute(firstContainer, "width", pixelPosition + "px");
+ int secondContainerWidth = (wholeSize - pixelPosition - getSplitterSize());
+ if (secondContainerWidth < 0) {
+ secondContainerWidth = 0;
+ }
+ DOM.setStyleAttribute(secondContainer, "width",
+ secondContainerWidth + "px");
+ DOM.setStyleAttribute(secondContainer, "left",
+ (pixelPosition + getSplitterSize()) + "px");
+
++ LayoutManager layoutManager = LayoutManager.get(client);
++ ConnectorMap connectorMap = ConnectorMap.get(client);
++ if (firstChild != null) {
++ ComponentConnector connector = connectorMap
++ .getConnector(firstChild);
++ if (connector.isRelativeWidth()) {
++ layoutManager.reportWidthAssignedToRelative(connector,
++ pixelPosition);
++ } else {
++ layoutManager.setNeedsMeasure(connector);
++ }
++ }
++ if (secondChild != null) {
++ ComponentConnector connector = connectorMap
++ .getConnector(secondChild);
++ if (connector.isRelativeWidth()) {
++ layoutManager.reportWidthAssignedToRelative(connector,
++ secondContainerWidth);
++ } else {
++ layoutManager.setNeedsMeasure(connector);
++ }
++ }
+ break;
+ case ORIENTATION_VERTICAL:
+ wholeSize = DOM.getElementPropertyInt(wrapper, "clientHeight");
+ pixelPosition = DOM.getElementPropertyInt(splitter, "offsetTop");
+
+ // reposition splitter in case it is out of box
+ if ((pixelPosition > 0 && pixelPosition + getSplitterSize() > wholeSize)
+ || (positionReversed && pixelPosition < 0)) {
+ pixelPosition = wholeSize - getSplitterSize();
+ if (pixelPosition < 0) {
+ pixelPosition = 0;
+ }
+ setSplitPosition(pixelPosition + "px");
+ return;
+ }
+
+ DOM.setStyleAttribute(firstContainer, "height", pixelPosition
+ + "px");
+ int secondContainerHeight = (wholeSize - pixelPosition - getSplitterSize());
+ if (secondContainerHeight < 0) {
+ secondContainerHeight = 0;
+ }
+ DOM.setStyleAttribute(secondContainer, "height",
+ secondContainerHeight + "px");
+ DOM.setStyleAttribute(secondContainer, "top",
+ (pixelPosition + getSplitterSize()) + "px");
+
++ layoutManager = LayoutManager.get(client);
++ connectorMap = ConnectorMap.get(client);
++ if (firstChild != null) {
++ ComponentConnector connector = connectorMap
++ .getConnector(firstChild);
++ if (connector.isRelativeHeight()) {
++ layoutManager.reportHeightAssignedToRelative(connector,
++ pixelPosition);
++ } else {
++ layoutManager.setNeedsMeasure(connector);
++ }
++ }
++ if (secondChild != null) {
++ ComponentConnector connector = connectorMap
++ .getConnector(secondChild);
++ if (connector.isRelativeHeight()) {
++ layoutManager.reportHeightAssignedToRelative(connector,
++ secondContainerHeight);
++ } else {
++ layoutManager.setNeedsMeasure(connector);
++ }
++ }
+ break;
+ }
+
+ }
+
+ void setFirstWidget(Widget w) {
+ if (firstChild != null) {
+ firstChild.removeFromParent();
+ }
+ if (w != null) {
+ super.add(w, firstContainer);
+ }
+ firstChild = w;
+ }
+
+ void setSecondWidget(Widget w) {
+ if (secondChild != null) {
+ secondChild.removeFromParent();
+ }
+ if (w != null) {
+ super.add(w, secondContainer);
+ }
+ secondChild = w;
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEMOVE:
+ // case Event.ONTOUCHMOVE:
+ if (resizing) {
+ onMouseMove(event);
+ }
+ break;
+ case Event.ONMOUSEDOWN:
+ // case Event.ONTOUCHSTART:
+ onMouseDown(event);
+ break;
+ case Event.ONMOUSEOUT:
+ // Dragging curtain interferes with click events if added in
+ // mousedown so we add it only when needed i.e., if the mouse moves
+ // outside the splitter.
+ if (resizing) {
+ showDraggingCurtain();
+ }
+ break;
+ case Event.ONMOUSEUP:
+ // case Event.ONTOUCHEND:
+ if (resizing) {
+ onMouseUp(event);
+ }
+ break;
+ case Event.ONCLICK:
+ resizing = false;
+ break;
+ }
+ // Only fire click event listeners if the splitter isn't moved
+ if (Util.isTouchEvent(event) || !resized) {
+ super.onBrowserEvent(event);
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEUP) {
+ // Reset the resized flag after a mouseup has occured so the next
+ // mousedown/mouseup can be interpreted as a click.
+ resized = false;
+ }
+ }
+
+ public void onMouseDown(Event event) {
+ if (locked || !isEnabled()) {
+ return;
+ }
+ final Element trg = event.getEventTarget().cast();
+ if (trg == splitter || trg == DOM.getChild(splitter, 0)) {
+ resizing = true;
+ DOM.setCapture(getElement());
+ origX = DOM.getElementPropertyInt(splitter, "offsetLeft");
+ origY = DOM.getElementPropertyInt(splitter, "offsetTop");
+ origMouseX = Util.getTouchOrMouseClientX(event);
+ origMouseY = Util.getTouchOrMouseClientY(event);
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ public void onMouseMove(Event event) {
+ switch (orientation) {
+ case ORIENTATION_HORIZONTAL:
+ final int x = Util.getTouchOrMouseClientX(event);
+ onHorizontalMouseMove(x);
+ break;
+ case ORIENTATION_VERTICAL:
+ default:
+ final int y = Util.getTouchOrMouseClientY(event);
+ onVerticalMouseMove(y);
+ break;
+ }
+
+ }
+
+ private void onHorizontalMouseMove(int x) {
+ int newX = origX + x - origMouseX;
+ if (newX < 0) {
+ newX = 0;
+ }
+ if (newX + getSplitterSize() > getOffsetWidth()) {
+ newX = getOffsetWidth() - getSplitterSize();
+ }
+
+ if (position.indexOf("%") > 0) {
+ float pos = newX;
+ // 100% needs special handling
+ if (newX + getSplitterSize() >= getOffsetWidth()) {
+ pos = getOffsetWidth();
+ }
+ // Reversed position
+ if (positionReversed) {
+ pos = getOffsetWidth() - pos - getSplitterSize();
+ }
+ position = (pos / getOffsetWidth() * 100) + "%";
+ } else {
+ // Reversed position
+ if (positionReversed) {
+ position = (getOffsetWidth() - newX - getSplitterSize()) + "px";
+ } else {
+ position = newX + "px";
+ }
+ }
+
+ if (origX != newX) {
+ resized = true;
+ }
+
+ // Reversed position
+ if (positionReversed) {
+ newX = getOffsetWidth() - newX - getSplitterSize();
+ }
+
+ setSplitPosition(newX + "px");
- client.doLayout(false);
+ }
+
+ private void onVerticalMouseMove(int y) {
+ int newY = origY + y - origMouseY;
+ if (newY < 0) {
+ newY = 0;
+ }
+
+ if (newY + getSplitterSize() > getOffsetHeight()) {
+ newY = getOffsetHeight() - getSplitterSize();
+ }
+
+ if (position.indexOf("%") > 0) {
+ float pos = newY;
+ // 100% needs special handling
+ if (newY + getSplitterSize() >= getOffsetHeight()) {
+ pos = getOffsetHeight();
+ }
+ // Reversed position
+ if (positionReversed) {
+ pos = getOffsetHeight() - pos - getSplitterSize();
+ }
+ position = pos / getOffsetHeight() * 100 + "%";
+ } else {
+ // Reversed position
+ if (positionReversed) {
+ position = (getOffsetHeight() - newY - getSplitterSize())
+ + "px";
+ } else {
+ position = newY + "px";
+ }
+ }
+
+ if (origY != newY) {
+ resized = true;
+ }
+
+ // Reversed position
+ if (positionReversed) {
+ newY = getOffsetHeight() - newY - getSplitterSize();
+ }
+
+ setSplitPosition(newY + "px");
+ }
+
+ public void onMouseUp(Event event) {
+ DOM.releaseCapture(getElement());
+ hideDraggingCurtain();
+ resizing = false;
+ if (!Util.isTouchEvent(event)) {
+ onMouseMove(event);
+ }
+ fireEvent(new SplitterMoveEvent(this));
+ }
+
+ public interface SplitterMoveHandler extends EventHandler {
+ public void splitterMoved(SplitterMoveEvent event);
+
+ public static class SplitterMoveEvent extends
+ GwtEvent<SplitterMoveHandler> {
+
+ public static final Type<SplitterMoveHandler> TYPE = new Type<SplitterMoveHandler>();
+
+ private Widget splitPanel;
+
+ public SplitterMoveEvent(Widget splitPanel) {
+ this.splitPanel = splitPanel;
+ }
+
+ @Override
+ public com.google.gwt.event.shared.GwtEvent.Type<SplitterMoveHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(SplitterMoveHandler handler) {
+ handler.splitterMoved(this);
+ }
+
+ }
+ }
+
+ String getSplitterPosition() {
+ return position;
+ }
+
+ /**
+ * Used in FF to avoid losing mouse capture when pointer is moved on an
+ * iframe.
+ */
+ private void showDraggingCurtain() {
+ if (!isDraggingCurtainRequired()) {
+ return;
+ }
+ if (draggingCurtain == null) {
+ draggingCurtain = DOM.createDiv();
+ DOM.setStyleAttribute(draggingCurtain, "position", "absolute");
+ DOM.setStyleAttribute(draggingCurtain, "top", "0px");
+ DOM.setStyleAttribute(draggingCurtain, "left", "0px");
+ DOM.setStyleAttribute(draggingCurtain, "width", "100%");
+ DOM.setStyleAttribute(draggingCurtain, "height", "100%");
+ DOM.setStyleAttribute(draggingCurtain, "zIndex", ""
+ + VOverlay.Z_INDEX);
+
+ DOM.appendChild(wrapper, draggingCurtain);
+ }
+ }
+
+ /**
+ * A dragging curtain is required in Gecko and Webkit.
+ *
+ * @return true if the browser requires a dragging curtain
+ */
+ private boolean isDraggingCurtainRequired() {
+ return (BrowserInfo.get().isGecko() || BrowserInfo.get().isWebkit());
+ }
+
+ /**
+ * Hides dragging curtain
+ */
+ private void hideDraggingCurtain() {
+ if (draggingCurtain != null) {
+ DOM.removeChild(wrapper, draggingCurtain);
+ draggingCurtain = null;
+ }
+ }
+
+ private int splitterSize = -1;
+
+ private int getSplitterSize() {
+ if (splitterSize < 0) {
+ if (isAttached()) {
+ switch (orientation) {
+ case ORIENTATION_HORIZONTAL:
+ splitterSize = DOM.getElementPropertyInt(splitter,
+ "offsetWidth");
+ break;
+
+ default:
+ splitterSize = DOM.getElementPropertyInt(splitter,
+ "offsetHeight");
+ break;
+ }
+ }
+ }
+ return splitterSize;
+ }
+
+ void setStylenames() {
+ final String splitterSuffix = (orientation == ORIENTATION_HORIZONTAL ? "-hsplitter"
+ : "-vsplitter");
+ final String firstContainerSuffix = "-first-container";
+ final String secondContainerSuffix = "-second-container";
+ String lockedSuffix = "";
+
+ String splitterStyle = CLASSNAME + splitterSuffix;
+ String firstStyle = CLASSNAME + firstContainerSuffix;
+ String secondStyle = CLASSNAME + secondContainerSuffix;
+
+ if (locked) {
+ splitterStyle = CLASSNAME + splitterSuffix + "-locked";
+ lockedSuffix = "-locked";
+ }
+ for (String style : componentStyleNames) {
+ splitterStyle += " " + CLASSNAME + splitterSuffix + "-" + style
+ + lockedSuffix;
+ firstStyle += " " + CLASSNAME + firstContainerSuffix + "-" + style;
+ secondStyle += " " + CLASSNAME + secondContainerSuffix + "-"
+ + style;
+ }
+ DOM.setElementProperty(splitter, "className", splitterStyle);
+ DOM.setElementProperty(firstContainer, "className", firstStyle);
+ DOM.setElementProperty(secondContainer, "className", secondStyle);
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+}
--- /dev/null
- getWidget().sizeInit();
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.table;
+
+import java.util.Iterator;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
++import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.AbstractFieldState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.ContextMenuDetails;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow;
+
+@Component(com.vaadin.ui.Table.class)
+public class TableConnector extends AbstractComponentContainerConnector
+ implements Paintable, DirectionalManagedLayout, PostLayoutListener {
+
+ @Override
+ protected void init() {
+ super.init();
+ getWidget().init(getConnection());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal
+ * .gwt.client.UIDL, com.vaadin.terminal.gwt.client.ApplicationConnection)
+ */
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().rendering = true;
+
+ // If a row has an open context menu, it will be closed as the row is
+ // detached. Retain a reference here so we can restore the menu if
+ // required.
+ ContextMenuDetails contextMenuBeforeUpdate = getWidget().contextMenu;
+
+ if (uidl.hasAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_FIRST)) {
+ getWidget().serverCacheFirst = uidl
+ .getIntAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_FIRST);
+ getWidget().serverCacheLast = uidl
+ .getIntAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_LAST);
+ } else {
+ getWidget().serverCacheFirst = -1;
+ getWidget().serverCacheLast = -1;
+ }
+ /*
+ * We need to do this before updateComponent since updateComponent calls
+ * this.setHeight() which will calculate a new body height depending on
+ * the space available.
+ */
+ if (uidl.hasAttribute("colfooters")) {
+ getWidget().showColFooters = uidl.getBooleanAttribute("colfooters");
+ }
+
+ getWidget().tFoot.setVisible(getWidget().showColFooters);
+
+ if (!isRealUpdate(uidl)) {
+ getWidget().rendering = false;
+ return;
+ }
+
+ getWidget().enabled = isEnabled();
+
+ if (BrowserInfo.get().isIE8() && !getWidget().enabled) {
+ /*
+ * The disabled shim will not cover the table body if it is relative
+ * in IE8. See #7324
+ */
+ getWidget().scrollBodyPanel.getElement().getStyle()
+ .setPosition(Position.STATIC);
+ } else if (BrowserInfo.get().isIE8()) {
+ getWidget().scrollBodyPanel.getElement().getStyle()
+ .setPosition(Position.RELATIVE);
+ }
+
+ getWidget().paintableId = uidl.getStringAttribute("id");
+ getWidget().immediate = getState().isImmediate();
+
+ int previousTotalRows = getWidget().totalRows;
+ getWidget().updateTotalRows(uidl);
+ boolean totalRowsChanged = (getWidget().totalRows != previousTotalRows);
+
+ getWidget().updateDragMode(uidl);
+
+ getWidget().updateSelectionProperties(uidl, getState(), isReadOnly());
+
+ if (uidl.hasAttribute("alb")) {
+ getWidget().bodyActionKeys = uidl.getStringArrayAttribute("alb");
+ } else {
+ // Need to clear the actions if the action handlers have been
+ // removed
+ getWidget().bodyActionKeys = null;
+ }
+
+ getWidget().setCacheRateFromUIDL(uidl);
+
+ getWidget().recalcWidths = uidl.hasAttribute("recalcWidths");
+ if (getWidget().recalcWidths) {
+ getWidget().tHead.clear();
+ getWidget().tFoot.clear();
+ }
+
+ getWidget().updatePageLength(uidl);
+
+ getWidget().updateFirstVisibleAndScrollIfNeeded(uidl);
+
+ getWidget().showRowHeaders = uidl.getBooleanAttribute("rowheaders");
+ getWidget().showColHeaders = uidl.getBooleanAttribute("colheaders");
+
+ getWidget().updateSortingProperties(uidl);
+
+ boolean keyboardSelectionOverRowFetchInProgress = getWidget()
+ .selectSelectedRows(uidl);
+
+ getWidget().updateActionMap(uidl);
+
+ getWidget().updateColumnProperties(uidl);
+
+ UIDL ac = uidl.getChildByTagName("-ac");
+ if (ac == null) {
+ if (getWidget().dropHandler != null) {
+ // remove dropHandler if not present anymore
+ getWidget().dropHandler = null;
+ }
+ } else {
+ if (getWidget().dropHandler == null) {
+ getWidget().dropHandler = getWidget().new VScrollTableDropHandler();
+ }
+ getWidget().dropHandler.updateAcceptRules(ac);
+ }
+
+ UIDL partialRowAdditions = uidl.getChildByTagName("prows");
+ UIDL partialRowUpdates = uidl.getChildByTagName("urows");
+ if (partialRowUpdates != null || partialRowAdditions != null) {
+ // we may have pending cache row fetch, cancel it. See #2136
+ getWidget().rowRequestHandler.cancel();
+
+ getWidget().updateRowsInBody(partialRowUpdates);
+ getWidget().addAndRemoveRows(partialRowAdditions);
+ } else {
+ UIDL rowData = uidl.getChildByTagName("rows");
+ if (rowData != null) {
+ // we may have pending cache row fetch, cancel it. See #2136
+ getWidget().rowRequestHandler.cancel();
+
+ if (!getWidget().recalcWidths
+ && getWidget().initializedAndAttached) {
+ getWidget().updateBody(rowData,
+ uidl.getIntAttribute("firstrow"),
+ uidl.getIntAttribute("rows"));
+ if (getWidget().headerChangedDuringUpdate) {
+ getWidget().triggerLazyColumnAdjustment(true);
+ } else if (!getWidget().isScrollPositionVisible()
+ || totalRowsChanged
+ || getWidget().lastRenderedHeight != getWidget().scrollBody
+ .getOffsetHeight()) {
+ // webkits may still bug with their disturbing scrollbar
+ // bug, see #3457
+ // Run overflow fix for the scrollable area
+ // #6698 - If there's a scroll going on, don't abort it
+ // by changing overflows as the length of the contents
+ // *shouldn't* have changed (unless the number of rows
+ // or the height of the widget has also changed)
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ Util.runWebkitOverflowAutoFix(getWidget().scrollBodyPanel
+ .getElement());
+ }
+ });
+ }
+ } else {
+ getWidget().initializeRows(uidl, rowData);
+ }
+ }
+ }
+
+ // If a row had an open context menu before the update, and after the
+ // update there's a row with the same key as that row, restore the
+ // context menu. See #8526.
+ showSavedContextMenu(contextMenuBeforeUpdate);
+
+ if (!getWidget().isSelectable()) {
+ getWidget().scrollBody.addStyleName(VScrollTable.CLASSNAME
+ + "-body-noselection");
+ } else {
+ getWidget().scrollBody.removeStyleName(VScrollTable.CLASSNAME
+ + "-body-noselection");
+ }
+
+ getWidget().hideScrollPositionAnnotation();
+
+ // selection is no in sync with server, avoid excessive server visits by
+ // clearing to flag used during the normal operation
+ if (!keyboardSelectionOverRowFetchInProgress) {
+ getWidget().selectionChanged = false;
+ }
+
+ /*
+ * This is called when the Home or page up button has been pressed in
+ * selectable mode and the next selected row was not yet rendered in the
+ * client
+ */
+ if (getWidget().selectFirstItemInNextRender
+ || getWidget().focusFirstItemInNextRender) {
+ getWidget().selectFirstRenderedRowInViewPort(
+ getWidget().focusFirstItemInNextRender);
+ getWidget().selectFirstItemInNextRender = getWidget().focusFirstItemInNextRender = false;
+ }
+
+ /*
+ * This is called when the page down or end button has been pressed in
+ * selectable mode and the next selected row was not yet rendered in the
+ * client
+ */
+ if (getWidget().selectLastItemInNextRender
+ || getWidget().focusLastItemInNextRender) {
+ getWidget().selectLastRenderedRowInViewPort(
+ getWidget().focusLastItemInNextRender);
+ getWidget().selectLastItemInNextRender = getWidget().focusLastItemInNextRender = false;
+ }
+ getWidget().multiselectPending = false;
+
+ if (getWidget().focusedRow != null) {
+ if (!getWidget().focusedRow.isAttached()
+ && !getWidget().rowRequestHandler.isRunning()) {
+ // focused row has been orphaned, can't focus
+ getWidget().focusRowFromBody();
+ }
+ }
+
+ getWidget().tabIndex = uidl.hasAttribute("tabindex") ? uidl
+ .getIntAttribute("tabindex") : 0;
+ getWidget().setProperTabIndex();
+
+ getWidget().resizeSortedColumnForSortIndicator();
+
+ // Remember this to detect situations where overflow hack might be
+ // needed during scrolling
+ getWidget().lastRenderedHeight = getWidget().scrollBody
+ .getOffsetHeight();
+
+ getWidget().rendering = false;
+ getWidget().headerChangedDuringUpdate = false;
+
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VScrollTable.class);
+ }
+
+ @Override
+ public VScrollTable getWidget() {
+ return (VScrollTable) super.getWidget();
+ }
+
+ public void updateCaption(ComponentConnector component) {
+ // NOP, not rendered
+ }
+
+ public void layoutVertically() {
+ getWidget().updateHeight();
+ }
+
+ public void layoutHorizontally() {
+ getWidget().updateWidth();
+ }
+
+ public void postLayout() {
++ VScrollTable table = getWidget();
++ if (table.sizeNeedsInit) {
++ table.sizeInit();
++ Scheduler.get().scheduleFinally(new ScheduledCommand() {
++ public void execute() {
++ getLayoutManager().setNeedsMeasure(TableConnector.this);
++ getLayoutManager()
++ .setHeightNeedsUpdate(TableConnector.this);
++ getLayoutManager().layoutNow();
++ }
++ });
++ }
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || getState().isPropertyReadOnly();
+ }
+
+ @Override
+ public AbstractFieldState getState() {
+ return (AbstractFieldState) super.getState();
+ }
+
+ /**
+ * Shows a saved row context menu if the row for the context menu is still
+ * visible. Does nothing if a context menu has not been saved.
+ *
+ * @param savedContextMenu
+ */
+ public void showSavedContextMenu(ContextMenuDetails savedContextMenu) {
+ if (isEnabled() && savedContextMenu != null) {
+ Iterator<Widget> iterator = getWidget().scrollBody.iterator();
+ while (iterator.hasNext()) {
+ Widget w = iterator.next();
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (row.getKey().equals(savedContextMenu.rowKey)) {
+ getWidget().contextMenu = savedContextMenu;
+ getConnection().getContextMenu().showAt(row,
+ savedContextMenu.left, savedContextMenu.top);
+ }
+ }
+ }
+ }
+
+}
--- /dev/null
- private boolean sizeNeedsInit = true;
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.table;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Display;
+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.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableRowElement;
+import com.google.gwt.dom.client.TableSectionElement;
+import com.google.gwt.dom.client.Touch;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.dom.client.ScrollEvent;
+import com.google.gwt.event.dom.client.ScrollHandler;
+import com.google.gwt.event.dom.client.TouchStartEvent;
+import com.google.gwt.event.dom.client.TouchStartHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ComponentState;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.MouseEventDetails;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.VTooltip;
+import com.vaadin.terminal.gwt.client.ui.Action;
+import com.vaadin.terminal.gwt.client.ui.ActionOwner;
+import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
+import com.vaadin.terminal.gwt.client.ui.TreeAction;
+import com.vaadin.terminal.gwt.client.ui.dd.DDUtil;
+import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent;
+import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
+import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation;
+import com.vaadin.terminal.gwt.client.ui.embedded.VEmbedded;
+import com.vaadin.terminal.gwt.client.ui.label.VLabel;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+/**
+ * VScrollTable
+ *
+ * VScrollTable is a FlowPanel having two widgets in it: * TableHead component *
+ * ScrollPanel
+ *
+ * TableHead contains table's header and widgets + logic for resizing,
+ * reordering and hiding columns.
+ *
+ * ScrollPanel contains VScrollTableBody object which handles content. To save
+ * some bandwidth and to improve clients responsiveness with loads of data, in
+ * VScrollTableBody all rows are not necessary rendered. There are "spacers" in
+ * VScrollTableBody to use the exact same space as non-rendered rows would use.
+ * This way we can use seamlessly traditional scrollbars and scrolling to fetch
+ * more rows instead of "paging".
+ *
+ * In VScrollTable we listen to scroll events. On horizontal scrolling we also
+ * update TableHeads scroll position which has its scrollbars hidden. On
+ * vertical scroll events we will check if we are reaching the end of area where
+ * we have rows rendered and
+ *
+ * TODO implement unregistering for child components in Cells
+ */
+public class VScrollTable extends FlowPanel implements HasWidgets,
+ ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable,
+ ActionOwner {
+
+ public enum SelectMode {
+ NONE(0), SINGLE(1), MULTI(2);
+ private int id;
+
+ private SelectMode(int id) {
+ this.id = id;
+ }
+
+ public int getId() {
+ return id;
+ }
+ }
+
+ private static final String ROW_HEADER_COLUMN_KEY = "0";
+
+ public static final String CLASSNAME = "v-table";
+ public static final String CLASSNAME_SELECTION_FOCUS = CLASSNAME + "-focus";
+
+ public static final String ATTRIBUTE_PAGEBUFFER_FIRST = "pb-ft";
+ public static final String ATTRIBUTE_PAGEBUFFER_LAST = "pb-l";
+
+ public static final String ITEM_CLICK_EVENT_ID = "itemClick";
+ public static final String HEADER_CLICK_EVENT_ID = "handleHeaderClick";
+ public static final String FOOTER_CLICK_EVENT_ID = "handleFooterClick";
+ public static final String COLUMN_RESIZE_EVENT_ID = "columnResize";
+ public static final String COLUMN_REORDER_EVENT_ID = "columnReorder";
+
+ private static final double CACHE_RATE_DEFAULT = 2;
+
+ /**
+ * The default multi select mode where simple left clicks only selects one
+ * item, CTRL+left click selects multiple items and SHIFT-left click selects
+ * a range of items.
+ */
+ private static final int MULTISELECT_MODE_DEFAULT = 0;
+
+ /**
+ * The simple multiselect mode is what the table used to have before
+ * ctrl/shift selections were added. That is that when this is set clicking
+ * on an item selects/deselects the item and no ctrl/shift selections are
+ * available.
+ */
+ private static final int MULTISELECT_MODE_SIMPLE = 1;
+
+ /**
+ * multiple of pagelength which component will cache when requesting more
+ * rows
+ */
+ private double cache_rate = CACHE_RATE_DEFAULT;
+ /**
+ * fraction of pageLenght which can be scrolled without making new request
+ */
+ private double cache_react_rate = 0.75 * cache_rate;
+
+ public static final char ALIGN_CENTER = 'c';
+ public static final char ALIGN_LEFT = 'b';
+ public static final char ALIGN_RIGHT = 'e';
+ private static final int CHARCODE_SPACE = 32;
+ private int firstRowInViewPort = 0;
+ private int pageLength = 15;
+ private int lastRequestedFirstvisible = 0; // to detect "serverside scroll"
+
+ protected boolean showRowHeaders = false;
+
+ private String[] columnOrder;
+
+ protected ApplicationConnection client;
+ protected String paintableId;
+
+ boolean immediate;
+ private boolean nullSelectionAllowed = true;
+
+ private SelectMode selectMode = SelectMode.NONE;
+
+ private final HashSet<String> selectedRowKeys = new HashSet<String>();
+
+ /*
+ * When scrolling and selecting at the same time, the selections are not in
+ * sync with the server while retrieving new rows (until key is released).
+ */
+ private HashSet<Object> unSyncedselectionsBeforeRowFetch;
+
+ /*
+ * These are used when jumping between pages when pressing Home and End
+ */
+ boolean selectLastItemInNextRender = false;
+ boolean selectFirstItemInNextRender = false;
+ boolean focusFirstItemInNextRender = false;
+ boolean focusLastItemInNextRender = false;
+
+ /*
+ * The currently focused row
+ */
+ VScrollTableRow focusedRow;
+
+ /*
+ * Helper to store selection range start in when using the keyboard
+ */
+ private VScrollTableRow selectionRangeStart;
+
+ /*
+ * Flag for notifying when the selection has changed and should be sent to
+ * the server
+ */
+ boolean selectionChanged = false;
+
+ /*
+ * The speed (in pixels) which the scrolling scrolls vertically/horizontally
+ */
+ private int scrollingVelocity = 10;
+
+ private Timer scrollingVelocityTimer = null;
+
+ String[] bodyActionKeys;
+
+ private boolean enableDebug = false;
+
+ /**
+ * Represents a select range of rows
+ */
+ private class SelectionRange {
+ private VScrollTableRow startRow;
+ private final int length;
+
+ /**
+ * Constuctor.
+ */
+ public SelectionRange(VScrollTableRow row1, VScrollTableRow row2) {
+ VScrollTableRow endRow;
+ if (row2.isBefore(row1)) {
+ startRow = row2;
+ endRow = row1;
+ } else {
+ startRow = row1;
+ endRow = row2;
+ }
+ length = endRow.getIndex() - startRow.getIndex() + 1;
+ }
+
+ public SelectionRange(VScrollTableRow row, int length) {
+ startRow = row;
+ this.length = length;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return startRow.getKey() + "-" + length;
+ }
+
+ private boolean inRange(VScrollTableRow row) {
+ return row.getIndex() >= startRow.getIndex()
+ && row.getIndex() < startRow.getIndex() + length;
+ }
+
+ public Collection<SelectionRange> split(VScrollTableRow row) {
+ assert row.isAttached();
+ ArrayList<SelectionRange> ranges = new ArrayList<SelectionRange>(2);
+
+ int endOfFirstRange = row.getIndex() - 1;
+ if (!(endOfFirstRange - startRow.getIndex() < 0)) {
+ // create range of first part unless its length is < 1
+ ranges.add(new SelectionRange(startRow, endOfFirstRange
+ - startRow.getIndex() + 1));
+ }
+ int startOfSecondRange = row.getIndex() + 1;
+ if (!(getEndIndex() - startOfSecondRange < 0)) {
+ // create range of second part unless its length is < 1
+ VScrollTableRow startOfRange = scrollBody
+ .getRowByRowIndex(startOfSecondRange);
+ ranges.add(new SelectionRange(startOfRange, getEndIndex()
+ - startOfSecondRange + 1));
+ }
+ return ranges;
+ }
+
+ private int getEndIndex() {
+ return startRow.getIndex() + length - 1;
+ }
+
+ };
+
+ private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>();
+
+ boolean initializedAndAttached = false;
+
+ /**
+ * Flag to indicate if a column width recalculation is needed due update.
+ */
+ boolean headerChangedDuringUpdate = false;
+
+ protected final TableHead tHead = new TableHead();
+
+ final TableFooter tFoot = new TableFooter();
+
+ final FocusableScrollPanel scrollBodyPanel = new FocusableScrollPanel(true);
+
+ private KeyPressHandler navKeyPressHandler = new KeyPressHandler() {
+ public void onKeyPress(KeyPressEvent keyPressEvent) {
+ // This is used for Firefox only, since Firefox auto-repeat
+ // works correctly only if we use a key press handler, other
+ // browsers handle it correctly when using a key down handler
+ if (!BrowserInfo.get().isGecko()) {
+ return;
+ }
+
+ NativeEvent event = keyPressEvent.getNativeEvent();
+ if (!enabled) {
+ // Cancel default keyboard events on a disabled Table
+ // (prevents scrolling)
+ event.preventDefault();
+ } else if (hasFocus) {
+ // Key code in Firefox/onKeyPress is present only for
+ // special keys, otherwise 0 is returned
+ int keyCode = event.getKeyCode();
+ if (keyCode == 0 && event.getCharCode() == ' ') {
+ // Provide a keyCode for space to be compatible with
+ // FireFox keypress event
+ keyCode = CHARCODE_SPACE;
+ }
+
+ if (handleNavigation(keyCode,
+ event.getCtrlKey() || event.getMetaKey(),
+ event.getShiftKey())) {
+ event.preventDefault();
+ }
+
+ startScrollingVelocityTimer();
+ }
+ }
+
+ };
+
+ private KeyUpHandler navKeyUpHandler = new KeyUpHandler() {
+
+ public void onKeyUp(KeyUpEvent keyUpEvent) {
+ NativeEvent event = keyUpEvent.getNativeEvent();
+ int keyCode = event.getKeyCode();
+
+ if (!isFocusable()) {
+ cancelScrollingVelocityTimer();
+ } else if (isNavigationKey(keyCode)) {
+ if (keyCode == getNavigationDownKey()
+ || keyCode == getNavigationUpKey()) {
+ /*
+ * in multiselect mode the server may still have value from
+ * previous page. Clear it unless doing multiselection or
+ * just moving focus.
+ */
+ if (!event.getShiftKey() && !event.getCtrlKey()) {
+ instructServerToForgetPreviousSelections();
+ }
+ sendSelectedRows();
+ }
+ cancelScrollingVelocityTimer();
+ navKeyDown = false;
+ }
+ }
+ };
+
+ private KeyDownHandler navKeyDownHandler = new KeyDownHandler() {
+
+ public void onKeyDown(KeyDownEvent keyDownEvent) {
+ NativeEvent event = keyDownEvent.getNativeEvent();
+ // This is not used for Firefox
+ if (BrowserInfo.get().isGecko()) {
+ return;
+ }
+
+ if (!enabled) {
+ // Cancel default keyboard events on a disabled Table
+ // (prevents scrolling)
+ event.preventDefault();
+ } else if (hasFocus) {
+ if (handleNavigation(event.getKeyCode(), event.getCtrlKey()
+ || event.getMetaKey(), event.getShiftKey())) {
+ navKeyDown = true;
+ event.preventDefault();
+ }
+
+ startScrollingVelocityTimer();
+ }
+ }
+ };
+ int totalRows;
+
+ private Set<String> collapsedColumns;
+
+ final RowRequestHandler rowRequestHandler;
+ VScrollTableBody scrollBody;
+ private int firstvisible = 0;
+ private boolean sortAscending;
+ private String sortColumn;
+ private String oldSortColumn;
+ private boolean columnReordering;
+
+ /**
+ * This map contains captions and icon urls for actions like: * "33_c" ->
+ * "Edit" * "33_i" -> "http://dom.com/edit.png"
+ */
+ private final HashMap<Object, String> actionMap = new HashMap<Object, String>();
+ private String[] visibleColOrder;
+ private boolean initialContentReceived = false;
+ private Element scrollPositionElement;
+ boolean enabled;
+ boolean showColHeaders;
+ boolean showColFooters;
+
+ /** flag to indicate that table body has changed */
+ private boolean isNewBody = true;
+
+ /*
+ * Read from the "recalcWidths" -attribute. When it is true, the table will
+ * recalculate the widths for columns - desirable in some cases. For #1983,
+ * marked experimental.
+ */
+ boolean recalcWidths = false;
+
+ boolean rendering = false;
+ private boolean hasFocus = false;
+ private int dragmode;
+
+ private int multiselectmode;
+ int tabIndex;
+ private TouchScrollDelegate touchScrollDelegate;
+
+ int lastRenderedHeight;
+
+ /**
+ * Values (serverCacheFirst+serverCacheLast) sent by server that tells which
+ * rows (indexes) are in the server side cache (page buffer). -1 means
+ * unknown. The server side cache row MUST MATCH the client side cache rows.
+ *
+ * If the client side cache contains additional rows with e.g. buttons, it
+ * will cause out of sync when such a button is pressed.
+ *
+ * If the server side cache contains additional rows with e.g. buttons,
+ * scrolling in the client will cause empty buttons to be rendered
+ * (cached=true request for non-existing components)
+ */
+ int serverCacheFirst = -1;
+ int serverCacheLast = -1;
+
- if (!sizeNeedsInit) {
- return;
- }
++ boolean sizeNeedsInit = true;
+
+ /**
+ * Used to recall the position of an open context menu if we need to close
+ * and reopen it during a row update.
+ */
+ class ContextMenuDetails {
+ String rowKey;
+ int left;
+ int top;
+
+ ContextMenuDetails(String rowKey, int left, int top) {
+ this.rowKey = rowKey;
+ this.left = left;
+ this.top = top;
+ }
+ }
+
+ protected ContextMenuDetails contextMenu = null;
+
+ public VScrollTable() {
+ setMultiSelectMode(MULTISELECT_MODE_DEFAULT);
+
+ scrollBodyPanel.setStyleName(CLASSNAME + "-body-wrapper");
+ scrollBodyPanel.addFocusHandler(this);
+ scrollBodyPanel.addBlurHandler(this);
+
+ scrollBodyPanel.addScrollHandler(this);
+ scrollBodyPanel.setStyleName(CLASSNAME + "-body");
+
+ /*
+ * Firefox auto-repeat works correctly only if we use a key press
+ * handler, other browsers handle it correctly when using a key down
+ * handler
+ */
+ if (BrowserInfo.get().isGecko()) {
+ scrollBodyPanel.addKeyPressHandler(navKeyPressHandler);
+ } else {
+ scrollBodyPanel.addKeyDownHandler(navKeyDownHandler);
+ }
+ scrollBodyPanel.addKeyUpHandler(navKeyUpHandler);
+
+ scrollBodyPanel.sinkEvents(Event.TOUCHEVENTS);
+ scrollBodyPanel.addDomHandler(new TouchStartHandler() {
+ public void onTouchStart(TouchStartEvent event) {
+ getTouchScrollDelegate().onTouchStart(event);
+ }
+ }, TouchStartEvent.getType());
+
+ scrollBodyPanel.sinkEvents(Event.ONCONTEXTMENU);
+ scrollBodyPanel.addDomHandler(new ContextMenuHandler() {
+ public void onContextMenu(ContextMenuEvent event) {
+ handleBodyContextMenu(event);
+ }
+ }, ContextMenuEvent.getType());
+
+ setStyleName(CLASSNAME);
+
+ add(tHead);
+ add(scrollBodyPanel);
+ add(tFoot);
+
+ rowRequestHandler = new RowRequestHandler();
+ }
+
+ public void init(ApplicationConnection client) {
+ this.client = client;
+ // Add a handler to clear saved context menu details when the menu
+ // closes. See #8526.
+ client.getContextMenu().addCloseHandler(new CloseHandler<PopupPanel>() {
+ public void onClose(CloseEvent<PopupPanel> event) {
+ contextMenu = null;
+ }
+ });
+ }
+
+ protected TouchScrollDelegate getTouchScrollDelegate() {
+ if (touchScrollDelegate == null) {
+ touchScrollDelegate = new TouchScrollDelegate(
+ scrollBodyPanel.getElement());
+ }
+ return touchScrollDelegate;
+
+ }
+
+ private void handleBodyContextMenu(ContextMenuEvent event) {
+ if (enabled && bodyActionKeys != null) {
+ int left = Util.getTouchOrMouseClientX(event.getNativeEvent());
+ int top = Util.getTouchOrMouseClientY(event.getNativeEvent());
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+
+ // Only prevent browser context menu if there are action handlers
+ // registered
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ /**
+ * Fires a column resize event which sends the resize information to the
+ * server.
+ *
+ * @param columnId
+ * The columnId of the column which was resized
+ * @param originalWidth
+ * The width in pixels of the column before the resize event
+ * @param newWidth
+ * The width in pixels of the column after the resize event
+ */
+ private void fireColumnResizeEvent(String columnId, int originalWidth,
+ int newWidth) {
+ client.updateVariable(paintableId, "columnResizeEventColumn", columnId,
+ false);
+ client.updateVariable(paintableId, "columnResizeEventPrev",
+ originalWidth, false);
+ client.updateVariable(paintableId, "columnResizeEventCurr", newWidth,
+ immediate);
+
+ }
+
+ /**
+ * Non-immediate variable update of column widths for a collection of
+ * columns.
+ *
+ * @param columns
+ * the columns to trigger the events for.
+ */
+ private void sendColumnWidthUpdates(Collection<HeaderCell> columns) {
+ String[] newSizes = new String[columns.size()];
+ int ix = 0;
+ for (HeaderCell cell : columns) {
+ newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth();
+ }
+ client.updateVariable(paintableId, "columnWidthUpdates", newSizes,
+ false);
+ }
+
+ /**
+ * Moves the focus one step down
+ *
+ * @return Returns true if succeeded
+ */
+ private boolean moveFocusDown() {
+ return moveFocusDown(0);
+ }
+
+ /**
+ * Moves the focus down by 1+offset rows
+ *
+ * @return Returns true if succeeded, else false if the selection could not
+ * be move downwards
+ */
+ private boolean moveFocusDown(int offset) {
+ if (isSelectable()) {
+ if (focusedRow == null && scrollBody.iterator().hasNext()) {
+ // FIXME should focus first visible from top, not first rendered
+ // ??
+ return setRowFocus((VScrollTableRow) scrollBody.iterator()
+ .next());
+ } else {
+ VScrollTableRow next = getNextRow(focusedRow, offset);
+ if (next != null) {
+ return setRowFocus(next);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Moves the selection one step up
+ *
+ * @return Returns true if succeeded
+ */
+ private boolean moveFocusUp() {
+ return moveFocusUp(0);
+ }
+
+ /**
+ * Moves the focus row upwards
+ *
+ * @return Returns true if succeeded, else false if the selection could not
+ * be move upwards
+ *
+ */
+ private boolean moveFocusUp(int offset) {
+ if (isSelectable()) {
+ if (focusedRow == null && scrollBody.iterator().hasNext()) {
+ // FIXME logic is exactly the same as in moveFocusDown, should
+ // be the opposite??
+ return setRowFocus((VScrollTableRow) scrollBody.iterator()
+ .next());
+ } else {
+ VScrollTableRow prev = getPreviousRow(focusedRow, offset);
+ if (prev != null) {
+ return setRowFocus(prev);
+ } else {
+ VConsole.log("no previous available");
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Selects a row where the current selection head is
+ *
+ * @param ctrlSelect
+ * Is the selection a ctrl+selection
+ * @param shiftSelect
+ * Is the selection a shift+selection
+ * @return Returns truw
+ */
+ private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) {
+ if (focusedRow != null) {
+ // Arrows moves the selection and clears previous selections
+ if (isSelectable() && !ctrlSelect && !shiftSelect) {
+ deselectAll();
+ focusedRow.toggleSelection();
+ selectionRangeStart = focusedRow;
+ } else if (isSelectable() && ctrlSelect && !shiftSelect) {
+ // Ctrl+arrows moves selection head
+ selectionRangeStart = focusedRow;
+ // No selection, only selection head is moved
+ } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) {
+ // Shift+arrows selection selects a range
+ focusedRow.toggleShiftSelection(shiftSelect);
+ }
+ }
+ }
+
+ /**
+ * Sends the selection to the server if changed since the last update/visit.
+ */
+ protected void sendSelectedRows() {
+ sendSelectedRows(immediate);
+ }
+
+ /**
+ * Sends the selection to the server if it has been changed since the last
+ * update/visit.
+ *
+ * @param immediately
+ * set to true to immediately send the rows
+ */
+ protected void sendSelectedRows(boolean immediately) {
+ // Don't send anything if selection has not changed
+ if (!selectionChanged) {
+ return;
+ }
+
+ // Reset selection changed flag
+ selectionChanged = false;
+
+ // Note: changing the immediateness of this might require changes to
+ // "clickEvent" immediateness also.
+ if (isMultiSelectModeDefault()) {
+ // Convert ranges to a set of strings
+ Set<String> ranges = new HashSet<String>();
+ for (SelectionRange range : selectedRowRanges) {
+ ranges.add(range.toString());
+ }
+
+ // Send the selected row ranges
+ client.updateVariable(paintableId, "selectedRanges",
+ ranges.toArray(new String[selectedRowRanges.size()]), false);
+
+ // clean selectedRowKeys so that they don't contain excess values
+ for (Iterator<String> iterator = selectedRowKeys.iterator(); iterator
+ .hasNext();) {
+ String key = iterator.next();
+ VScrollTableRow renderedRowByKey = getRenderedRowByKey(key);
+ if (renderedRowByKey != null) {
+ for (SelectionRange range : selectedRowRanges) {
+ if (range.inRange(renderedRowByKey)) {
+ iterator.remove();
+ }
+ }
+ } else {
+ // orphaned selected key, must be in a range, ignore
+ iterator.remove();
+ }
+
+ }
+ }
+
+ // Send the selected rows
+ client.updateVariable(paintableId, "selected",
+ selectedRowKeys.toArray(new String[selectedRowKeys.size()]),
+ immediately);
+
+ }
+
+ /**
+ * Get the key that moves the selection head upwards. By default it is the
+ * up arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationUpKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /**
+ * Get the key that moves the selection head downwards. By default it is the
+ * down arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationDownKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Get the key that scrolls to the left in the table. By default it is the
+ * left arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationLeftKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Get the key that scroll to the right on the table. By default it is the
+ * right arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationRightKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Get the key that selects an item in the table. By default it is the space
+ * bar key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return
+ */
+ protected int getNavigationSelectKey() {
+ return CHARCODE_SPACE;
+ }
+
+ /**
+ * Get the key the moves the selection one page up in the table. By default
+ * this is the Page Up key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageUpKey() {
+ return KeyCodes.KEY_PAGEUP;
+ }
+
+ /**
+ * Get the key the moves the selection one page down in the table. By
+ * default this is the Page Down key but by overriding this you can change
+ * the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageDownKey() {
+ return KeyCodes.KEY_PAGEDOWN;
+ }
+
+ /**
+ * Get the key the moves the selection to the beginning of the table. By
+ * default this is the Home key but by overriding this you can change the
+ * key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationStartKey() {
+ return KeyCodes.KEY_HOME;
+ }
+
+ /**
+ * Get the key the moves the selection to the end of the table. By default
+ * this is the End key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationEndKey() {
+ return KeyCodes.KEY_END;
+ }
+
+ void initializeRows(UIDL uidl, UIDL rowData) {
+ if (scrollBody != null) {
+ scrollBody.removeFromParent();
+ }
+ scrollBody = createScrollBody();
+
+ scrollBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"),
+ uidl.getIntAttribute("rows"));
+ scrollBodyPanel.add(scrollBody);
+
+ // New body starts scrolled to the left, make sure the header and footer
+ // are also scrolled to the left
+ tHead.setHorizontalScrollPosition(0);
+ tFoot.setHorizontalScrollPosition(0);
+
+ initialContentReceived = true;
+ sizeNeedsInit = true;
+ scrollBody.restoreRowVisibility();
+ }
+
+ void updateColumnProperties(UIDL uidl) {
+ updateColumnOrder(uidl);
+
+ updateCollapsedColumns(uidl);
+
+ UIDL vc = uidl.getChildByTagName("visiblecolumns");
+ if (vc != null) {
+ tHead.updateCellsFromUIDL(vc);
+ tFoot.updateCellsFromUIDL(vc);
+ }
+
+ updateHeader(uidl.getStringArrayAttribute("vcolorder"));
+ updateFooter(uidl.getStringArrayAttribute("vcolorder"));
+ }
+
+ private void updateCollapsedColumns(UIDL uidl) {
+ if (uidl.hasVariable("collapsedcolumns")) {
+ tHead.setColumnCollapsingAllowed(true);
+ collapsedColumns = uidl
+ .getStringArrayVariableAsSet("collapsedcolumns");
+ } else {
+ tHead.setColumnCollapsingAllowed(false);
+ }
+ }
+
+ private void updateColumnOrder(UIDL uidl) {
+ if (uidl.hasVariable("columnorder")) {
+ columnReordering = true;
+ columnOrder = uidl.getStringArrayVariable("columnorder");
+ } else {
+ columnReordering = false;
+ columnOrder = null;
+ }
+ }
+
+ boolean selectSelectedRows(UIDL uidl) {
+ boolean keyboardSelectionOverRowFetchInProgress = false;
+
+ if (uidl.hasVariable("selected")) {
+ final Set<String> selectedKeys = uidl
+ .getStringArrayVariableAsSet("selected");
+ if (scrollBody != null) {
+ Iterator<Widget> iterator = scrollBody.iterator();
+ while (iterator.hasNext()) {
+ /*
+ * Make the focus reflect to the server side state unless we
+ * are currently selecting multiple rows with keyboard.
+ */
+ VScrollTableRow row = (VScrollTableRow) iterator.next();
+ boolean selected = selectedKeys.contains(row.getKey());
+ if (!selected
+ && unSyncedselectionsBeforeRowFetch != null
+ && unSyncedselectionsBeforeRowFetch.contains(row
+ .getKey())) {
+ selected = true;
+ keyboardSelectionOverRowFetchInProgress = true;
+ }
+ if (selected != row.isSelected()) {
+ row.toggleSelection();
+ if (!isSingleSelectMode() && !selected) {
+ // Update selection range in case a row is
+ // unselected from the middle of a range - #8076
+ removeRowFromUnsentSelectionRanges(row);
+ }
+ }
+ }
+ }
+ }
+ unSyncedselectionsBeforeRowFetch = null;
+ return keyboardSelectionOverRowFetchInProgress;
+ }
+
+ void updateSortingProperties(UIDL uidl) {
+ oldSortColumn = sortColumn;
+ if (uidl.hasVariable("sortascending")) {
+ sortAscending = uidl.getBooleanVariable("sortascending");
+ sortColumn = uidl.getStringVariable("sortcolumn");
+ }
+ }
+
+ void resizeSortedColumnForSortIndicator() {
+ // Force recalculation of the captionContainer element inside the header
+ // cell to accomodate for the size of the sort arrow.
+ HeaderCell sortedHeader = tHead.getHeaderCell(sortColumn);
+ if (sortedHeader != null) {
+ tHead.resizeCaptionContainer(sortedHeader);
+ }
+ // Also recalculate the width of the captionContainer element in the
+ // previously sorted header, since this now has more room.
+ HeaderCell oldSortedHeader = tHead.getHeaderCell(oldSortColumn);
+ if (oldSortedHeader != null) {
+ tHead.resizeCaptionContainer(oldSortedHeader);
+ }
+ }
+
+ void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) {
+ firstvisible = uidl.hasVariable("firstvisible") ? uidl
+ .getIntVariable("firstvisible") : 0;
+ if (firstvisible != lastRequestedFirstvisible && scrollBody != null) {
+ // received 'surprising' firstvisible from server: scroll there
+ firstRowInViewPort = firstvisible;
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstvisible));
+ }
+ }
+
+ protected int measureRowHeightOffset(int rowIx) {
+ return (int) (rowIx * scrollBody.getRowHeight());
+ }
+
+ void updatePageLength(UIDL uidl) {
+ int oldPageLength = pageLength;
+ if (uidl.hasAttribute("pagelength")) {
+ pageLength = uidl.getIntAttribute("pagelength");
+ } else {
+ // pagelenght is "0" meaning scrolling is turned off
+ pageLength = totalRows;
+ }
+
+ if (oldPageLength != pageLength && initializedAndAttached) {
+ // page length changed, need to update size
+ sizeNeedsInit = true;
+ }
+ }
+
+ void updateSelectionProperties(UIDL uidl, ComponentState state,
+ boolean readOnly) {
+ setMultiSelectMode(uidl.hasAttribute("multiselectmode") ? uidl
+ .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT);
+
+ nullSelectionAllowed = uidl.hasAttribute("nsa") ? uidl
+ .getBooleanAttribute("nsa") : true;
+
+ if (uidl.hasAttribute("selectmode")) {
+ if (readOnly) {
+ selectMode = SelectMode.NONE;
+ } else if (uidl.getStringAttribute("selectmode").equals("multi")) {
+ selectMode = SelectMode.MULTI;
+ } else if (uidl.getStringAttribute("selectmode").equals("single")) {
+ selectMode = SelectMode.SINGLE;
+ } else {
+ selectMode = SelectMode.NONE;
+ }
+ }
+ }
+
+ void updateDragMode(UIDL uidl) {
+ dragmode = uidl.hasAttribute("dragmode") ? uidl
+ .getIntAttribute("dragmode") : 0;
+ if (BrowserInfo.get().isIE()) {
+ if (dragmode > 0) {
+ getElement().setPropertyJSO("onselectstart",
+ getPreventTextSelectionIEHack());
+ } else {
+ getElement().setPropertyJSO("onselectstart", null);
+ }
+ }
+ }
+
+ protected void updateTotalRows(UIDL uidl) {
+ int newTotalRows = uidl.getIntAttribute("totalrows");
+ if (newTotalRows != getTotalRows()) {
+ if (scrollBody != null) {
+ if (getTotalRows() == 0) {
+ tHead.clear();
+ tFoot.clear();
+ }
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ setTotalRows(newTotalRows);
+ }
+ }
+
+ protected void setTotalRows(int newTotalRows) {
+ totalRows = newTotalRows;
+ }
+
+ public int getTotalRows() {
+ return totalRows;
+ }
+
+ void focusRowFromBody() {
+ if (selectedRowKeys.size() == 1) {
+ // try to focus a row currently selected and in viewport
+ String selectedRowKey = selectedRowKeys.iterator().next();
+ if (selectedRowKey != null) {
+ VScrollTableRow renderedRow = getRenderedRowByKey(selectedRowKey);
+ if (renderedRow == null || !renderedRow.isInViewPort()) {
+ setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
+ } else {
+ setRowFocus(renderedRow);
+ }
+ }
+ } else {
+ // multiselect mode
+ setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
+ }
+ }
+
+ protected VScrollTableBody createScrollBody() {
+ return new VScrollTableBody();
+ }
+
+ /**
+ * Selects the last row visible in the table
+ *
+ * @param focusOnly
+ * Should the focus only be moved to the last row
+ */
+ void selectLastRenderedRowInViewPort(boolean focusOnly) {
+ int index = firstRowInViewPort + getFullyVisibleRowCount();
+ VScrollTableRow lastRowInViewport = scrollBody.getRowByRowIndex(index);
+ if (lastRowInViewport == null) {
+ // this should not happen in normal situations (white space at the
+ // end of viewport). Select the last rendered as a fallback.
+ lastRowInViewport = scrollBody.getRowByRowIndex(scrollBody
+ .getLastRendered());
+ if (lastRowInViewport == null) {
+ return; // empty table
+ }
+ }
+ setRowFocus(lastRowInViewport);
+ if (!focusOnly) {
+ selectFocusedRow(false, multiselectPending);
+ sendSelectedRows();
+ }
+ }
+
+ /**
+ * Selects the first row visible in the table
+ *
+ * @param focusOnly
+ * Should the focus only be moved to the first row
+ */
+ void selectFirstRenderedRowInViewPort(boolean focusOnly) {
+ int index = firstRowInViewPort;
+ VScrollTableRow firstInViewport = scrollBody.getRowByRowIndex(index);
+ if (firstInViewport == null) {
+ // this should not happen in normal situations
+ return;
+ }
+ setRowFocus(firstInViewport);
+ if (!focusOnly) {
+ selectFocusedRow(false, multiselectPending);
+ sendSelectedRows();
+ }
+ }
+
+ void setCacheRateFromUIDL(UIDL uidl) {
+ setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr")
+ : CACHE_RATE_DEFAULT);
+ }
+
+ private void setCacheRate(double d) {
+ if (cache_rate != d) {
+ cache_rate = d;
+ cache_react_rate = 0.75 * d;
+ }
+ }
+
+ void updateActionMap(UIDL mainUidl) {
+ UIDL actionsUidl = mainUidl.getChildByTagName("actions");
+ if (actionsUidl == null) {
+ return;
+ }
+
+ final Iterator<?> it = actionsUidl.getChildIterator();
+ while (it.hasNext()) {
+ final UIDL action = (UIDL) it.next();
+ final String key = action.getStringAttribute("key");
+ final String caption = action.getStringAttribute("caption");
+ actionMap.put(key + "_c", caption);
+ if (action.hasAttribute("icon")) {
+ // TODO need some uri handling ??
+ actionMap.put(key + "_i", client.translateVaadinUri(action
+ .getStringAttribute("icon")));
+ } else {
+ actionMap.remove(key + "_i");
+ }
+ }
+
+ }
+
+ public String getActionCaption(String actionKey) {
+ return actionMap.get(actionKey + "_c");
+ }
+
+ public String getActionIcon(String actionKey) {
+ return actionMap.get(actionKey + "_i");
+ }
+
+ private void updateHeader(String[] strings) {
+ if (strings == null) {
+ return;
+ }
+
+ int visibleCols = strings.length;
+ int colIndex = 0;
+ if (showRowHeaders) {
+ tHead.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
+ visibleCols++;
+ visibleColOrder = new String[visibleCols];
+ visibleColOrder[colIndex] = ROW_HEADER_COLUMN_KEY;
+ colIndex++;
+ } else {
+ visibleColOrder = new String[visibleCols];
+ tHead.removeCell(ROW_HEADER_COLUMN_KEY);
+ }
+
+ int i;
+ for (i = 0; i < strings.length; i++) {
+ final String cid = strings[i];
+ visibleColOrder[colIndex] = cid;
+ tHead.enableColumn(cid, colIndex);
+ colIndex++;
+ }
+
+ tHead.setVisible(showColHeaders);
+ setContainerHeight();
+
+ }
+
+ /**
+ * Updates footers.
+ * <p>
+ * Update headers whould be called before this method is called!
+ * </p>
+ *
+ * @param strings
+ */
+ private void updateFooter(String[] strings) {
+ if (strings == null) {
+ return;
+ }
+
+ // Add dummy column if row headers are present
+ int colIndex = 0;
+ if (showRowHeaders) {
+ tFoot.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
+ colIndex++;
+ } else {
+ tFoot.removeCell(ROW_HEADER_COLUMN_KEY);
+ }
+
+ int i;
+ for (i = 0; i < strings.length; i++) {
+ final String cid = strings[i];
+ tFoot.enableColumn(cid, colIndex);
+ colIndex++;
+ }
+
+ tFoot.setVisible(showColFooters);
+ }
+
+ /**
+ * @param uidl
+ * which contains row data
+ * @param firstRow
+ * first row in data set
+ * @param reqRows
+ * amount of rows in data set
+ */
+ void updateBody(UIDL uidl, int firstRow, int reqRows) {
+ if (uidl == null || reqRows < 1) {
+ // container is empty, remove possibly existing rows
+ if (firstRow <= 0) {
+ while (scrollBody.getLastRendered() > scrollBody.firstRendered) {
+ scrollBody.unlinkRow(false);
+ }
+ scrollBody.unlinkRow(false);
+ }
+ return;
+ }
+
+ scrollBody.renderRows(uidl, firstRow, reqRows);
+
+ discardRowsOutsideCacheWindow();
+ }
+
+ void updateRowsInBody(UIDL partialRowUpdates) {
+ if (partialRowUpdates == null) {
+ return;
+ }
+ int firstRowIx = partialRowUpdates.getIntAttribute("firsturowix");
+ int count = partialRowUpdates.getIntAttribute("numurows");
+ scrollBody.unlinkRows(firstRowIx, count);
+ scrollBody.insertRows(partialRowUpdates, firstRowIx, count);
+ }
+
+ /**
+ * Updates the internal cache by unlinking rows that fall outside of the
+ * caching window.
+ */
+ protected void discardRowsOutsideCacheWindow() {
+ int firstRowToKeep = (int) (firstRowInViewPort - pageLength
+ * cache_rate);
+ int lastRowToKeep = (int) (firstRowInViewPort + pageLength + pageLength
+ * cache_rate);
+ debug("Client side calculated cache rows to keep: " + firstRowToKeep
+ + "-" + lastRowToKeep);
+
+ if (serverCacheFirst != -1) {
+ firstRowToKeep = serverCacheFirst;
+ lastRowToKeep = serverCacheLast;
+ debug("Server cache rows that override: " + serverCacheFirst + "-"
+ + serverCacheLast);
+ if (firstRowToKeep < scrollBody.getFirstRendered()
+ || lastRowToKeep > scrollBody.getLastRendered()) {
+ debug("*** Server wants us to keep " + serverCacheFirst + "-"
+ + serverCacheLast + " but we only have rows "
+ + scrollBody.getFirstRendered() + "-"
+ + scrollBody.getLastRendered() + " rendered!");
+ }
+ }
+ discardRowsOutsideOf(firstRowToKeep, lastRowToKeep);
+
+ scrollBody.fixSpacers();
+
+ scrollBody.restoreRowVisibility();
+ }
+
+ private void discardRowsOutsideOf(int optimalFirstRow, int optimalLastRow) {
+ /*
+ * firstDiscarded and lastDiscarded are only calculated for debug
+ * purposes
+ */
+ int firstDiscarded = -1, lastDiscarded = -1;
+ boolean cont = true;
+ while (cont && scrollBody.getLastRendered() > optimalFirstRow
+ && scrollBody.getFirstRendered() < optimalFirstRow) {
+ if (firstDiscarded == -1) {
+ firstDiscarded = scrollBody.getFirstRendered();
+ }
+
+ // removing row from start
+ cont = scrollBody.unlinkRow(true);
+ }
+ if (firstDiscarded != -1) {
+ lastDiscarded = scrollBody.getFirstRendered() - 1;
+ debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
+ }
+ firstDiscarded = lastDiscarded = -1;
+
+ cont = true;
+ while (cont && scrollBody.getLastRendered() > optimalLastRow) {
+ if (lastDiscarded == -1) {
+ lastDiscarded = scrollBody.getLastRendered();
+ }
+
+ // removing row from the end
+ cont = scrollBody.unlinkRow(false);
+ }
+ if (lastDiscarded != -1) {
+ firstDiscarded = scrollBody.getLastRendered() + 1;
+ debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
+ }
+
+ debug("Now in cache: " + scrollBody.getFirstRendered() + "-"
+ + scrollBody.getLastRendered());
+ }
+
+ /**
+ * Inserts rows in the table body or removes them from the table body based
+ * on the commands in the UIDL.
+ *
+ * @param partialRowAdditions
+ * the UIDL containing row updates.
+ */
+ protected void addAndRemoveRows(UIDL partialRowAdditions) {
+ if (partialRowAdditions == null) {
+ return;
+ }
+ if (partialRowAdditions.hasAttribute("hide")) {
+ scrollBody.unlinkAndReindexRows(
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ scrollBody.ensureCacheFilled();
+ } else {
+ if (partialRowAdditions.hasAttribute("delbelow")) {
+ scrollBody.insertRowsDeleteBelow(partialRowAdditions,
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ } else {
+ scrollBody.insertAndReindexRows(partialRowAdditions,
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ }
+ }
+
+ discardRowsOutsideCacheWindow();
+ }
+
+ /**
+ * Gives correct column index for given column key ("cid" in UIDL).
+ *
+ * @param colKey
+ * @return column index of visible columns, -1 if column not visible
+ */
+ private int getColIndexByKey(String colKey) {
+ // return 0 if asked for rowHeaders
+ if (ROW_HEADER_COLUMN_KEY.equals(colKey)) {
+ return 0;
+ }
+ for (int i = 0; i < visibleColOrder.length; i++) {
+ if (visibleColOrder[i].equals(colKey)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private boolean isMultiSelectModeSimple() {
+ return selectMode == SelectMode.MULTI
+ && multiselectmode == MULTISELECT_MODE_SIMPLE;
+ }
+
+ private boolean isSingleSelectMode() {
+ return selectMode == SelectMode.SINGLE;
+ }
+
+ private boolean isMultiSelectModeAny() {
+ return selectMode == SelectMode.MULTI;
+ }
+
+ private boolean isMultiSelectModeDefault() {
+ return selectMode == SelectMode.MULTI
+ && multiselectmode == MULTISELECT_MODE_DEFAULT;
+ }
+
+ private void setMultiSelectMode(int multiselectmode) {
+ if (BrowserInfo.get().isTouchDevice()) {
+ // Always use the simple mode for touch devices that do not have
+ // shift/ctrl keys
+ this.multiselectmode = MULTISELECT_MODE_SIMPLE;
+ } else {
+ this.multiselectmode = multiselectmode;
+ }
+
+ }
+
+ protected boolean isSelectable() {
+ return selectMode.getId() > SelectMode.NONE.getId();
+ }
+
+ private boolean isCollapsedColumn(String colKey) {
+ if (collapsedColumns == null) {
+ return false;
+ }
+ if (collapsedColumns.contains(colKey)) {
+ return true;
+ }
+ return false;
+ }
+
+ private String getColKeyByIndex(int index) {
+ return tHead.getHeaderCell(index).getColKey();
+ }
+
+ private void setColWidth(int colIndex, int w, boolean isDefinedWidth) {
+ final HeaderCell hcell = tHead.getHeaderCell(colIndex);
+
+ // Make sure that the column grows to accommodate the sort indicator if
+ // necessary.
+ if (w < hcell.getMinWidth()) {
+ w = hcell.getMinWidth();
+ }
+
+ // Set header column width
+ hcell.setWidth(w, isDefinedWidth);
+
+ // Ensure indicators have been taken into account
+ tHead.resizeCaptionContainer(hcell);
+
+ // Set body column width
+ scrollBody.setColWidth(colIndex, w);
+
+ // Set footer column width
+ FooterCell fcell = tFoot.getFooterCell(colIndex);
+ fcell.setWidth(w, isDefinedWidth);
+ }
+
+ private int getColWidth(String colKey) {
+ return tHead.getHeaderCell(colKey).getWidth();
+ }
+
+ /**
+ * Get a rendered row by its key
+ *
+ * @param key
+ * The key to search with
+ * @return
+ */
+ public VScrollTableRow getRenderedRowByKey(String key) {
+ if (scrollBody != null) {
+ final Iterator<Widget> it = scrollBody.iterator();
+ VScrollTableRow r = null;
+ while (it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ if (r.getKey().equals(key)) {
+ return r;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the next row to the given row
+ *
+ * @param row
+ * The row to calculate from
+ *
+ * @return The next row or null if no row exists
+ */
+ private VScrollTableRow getNextRow(VScrollTableRow row, int offset) {
+ final Iterator<Widget> it = scrollBody.iterator();
+ VScrollTableRow r = null;
+ while (it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ if (r == row) {
+ r = null;
+ while (offset >= 0 && it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ offset--;
+ }
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the previous row from the given row
+ *
+ * @param row
+ * The row to calculate from
+ * @return The previous row or null if no row exists
+ */
+ private VScrollTableRow getPreviousRow(VScrollTableRow row, int offset) {
+ final Iterator<Widget> it = scrollBody.iterator();
+ final Iterator<Widget> offsetIt = scrollBody.iterator();
+ VScrollTableRow r = null;
+ VScrollTableRow prev = null;
+ while (it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ if (offset < 0) {
+ prev = (VScrollTableRow) offsetIt.next();
+ }
+ if (r == row) {
+ return prev;
+ }
+ offset--;
+ }
+
+ return null;
+ }
+
+ protected void reOrderColumn(String columnKey, int newIndex) {
+
+ final int oldIndex = getColIndexByKey(columnKey);
+
+ // Change header order
+ tHead.moveCell(oldIndex, newIndex);
+
+ // Change body order
+ scrollBody.moveCol(oldIndex, newIndex);
+
+ // Change footer order
+ tFoot.moveCell(oldIndex, newIndex);
+
+ /*
+ * Build new columnOrder and update it to server Note that columnOrder
+ * also contains collapsed columns so we cannot directly build it from
+ * cells vector Loop the old columnOrder and append in order to new
+ * array unless on moved columnKey. On new index also put the moved key
+ * i == index on columnOrder, j == index on newOrder
+ */
+ final String oldKeyOnNewIndex = visibleColOrder[newIndex];
+ if (showRowHeaders) {
+ newIndex--; // columnOrder don't have rowHeader
+ }
+ // add back hidden rows,
+ for (int i = 0; i < columnOrder.length; i++) {
+ if (columnOrder[i].equals(oldKeyOnNewIndex)) {
+ break; // break loop at target
+ }
+ if (isCollapsedColumn(columnOrder[i])) {
+ newIndex++;
+ }
+ }
+ // finally we can build the new columnOrder for server
+ final String[] newOrder = new String[columnOrder.length];
+ for (int i = 0, j = 0; j < newOrder.length; i++) {
+ if (j == newIndex) {
+ newOrder[j] = columnKey;
+ j++;
+ }
+ if (i == columnOrder.length) {
+ break;
+ }
+ if (columnOrder[i].equals(columnKey)) {
+ continue;
+ }
+ newOrder[j] = columnOrder[i];
+ j++;
+ }
+ columnOrder = newOrder;
+ // also update visibleColumnOrder
+ int i = showRowHeaders ? 1 : 0;
+ for (int j = 0; j < newOrder.length; j++) {
+ final String cid = newOrder[j];
+ if (!isCollapsedColumn(cid)) {
+ visibleColOrder[i++] = cid;
+ }
+ }
+ client.updateVariable(paintableId, "columnorder", columnOrder, false);
+ if (client.hasEventListeners(this, COLUMN_REORDER_EVENT_ID)) {
+ client.sendPendingVariableChanges();
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ rowRequestHandler.cancel();
+ super.onDetach();
+ // ensure that scrollPosElement will be detached
+ if (scrollPositionElement != null) {
+ final Element parent = DOM.getParent(scrollPositionElement);
+ if (parent != null) {
+ DOM.removeChild(parent, scrollPositionElement);
+ }
+ }
+ }
+
+ /**
+ * Run only once when component is attached and received its initial
+ * content. This function:
+ *
+ * * Syncs headers and bodys "natural widths and saves the values.
+ *
+ * * Sets proper width and height
+ *
+ * * Makes deferred request to get some cache rows
+ */
+ void sizeInit() {
-
- client.doLayout(true);
+ sizeNeedsInit = false;
+
+ scrollBody.setContainerHeight();
+
+ /*
+ * We will use browsers table rendering algorithm to find proper column
+ * widths. If content and header take less space than available, we will
+ * divide extra space relatively to each column which has not width set.
+ *
+ * Overflow pixels are added to last column.
+ */
+
+ Iterator<Widget> headCells = tHead.iterator();
+ Iterator<Widget> footCells = tFoot.iterator();
+ int i = 0;
+ int totalExplicitColumnsWidths = 0;
+ int total = 0;
+ float expandRatioDivider = 0;
+
+ final int[] widths = new int[tHead.visibleCells.size()];
+
+ tHead.enableBrowserIntelligence();
+ tFoot.enableBrowserIntelligence();
+
+ // first loop: collect natural widths
+ while (headCells.hasNext()) {
+ final HeaderCell hCell = (HeaderCell) headCells.next();
+ final FooterCell fCell = (FooterCell) footCells.next();
+ int w = hCell.getWidth();
+ if (hCell.isDefinedWidth()) {
+ // server has defined column width explicitly
+ totalExplicitColumnsWidths += w;
+ } else {
+ if (hCell.getExpandRatio() > 0) {
+ expandRatioDivider += hCell.getExpandRatio();
+ w = 0;
+ } else {
+ // get and store greater of header width and column width,
+ // and
+ // store it as a minimumn natural col width
+ int headerWidth = hCell.getNaturalColumnWidth(i);
+ int footerWidth = fCell.getNaturalColumnWidth(i);
+ w = headerWidth > footerWidth ? headerWidth : footerWidth;
+ }
+ hCell.setNaturalMinimumColumnWidth(w);
+ fCell.setNaturalMinimumColumnWidth(w);
+ }
+ widths[i] = w;
+ total += w;
+ i++;
+ }
+
+ tHead.disableBrowserIntelligence();
+ tFoot.disableBrowserIntelligence();
+
+ boolean willHaveScrollbarz = willHaveScrollbars();
+
+ // fix "natural" width if width not set
+ if (isDynamicWidth()) {
+ int w = total;
+ w += scrollBody.getCellExtraWidth() * visibleColOrder.length;
+ if (willHaveScrollbarz) {
+ w += Util.getNativeScrollbarSize();
+ }
+ setContentWidth(w);
+ }
+
+ int availW = scrollBody.getAvailableWidth();
+ if (BrowserInfo.get().isIE()) {
+ // Hey IE, are you really sure about this?
+ availW = scrollBody.getAvailableWidth();
+ }
+ availW -= scrollBody.getCellExtraWidth() * visibleColOrder.length;
+
+ if (willHaveScrollbarz) {
+ availW -= Util.getNativeScrollbarSize();
+ }
+
+ // TODO refactor this code to be the same as in resize timer
+ boolean needsReLayout = false;
+
+ if (availW > total) {
+ // natural size is smaller than available space
+ final int extraSpace = availW - total;
+ final int totalWidthR = total - totalExplicitColumnsWidths;
+ int checksum = 0;
+ needsReLayout = true;
+
+ if (extraSpace == 1) {
+ // We cannot divide one single pixel so we give it the first
+ // undefined column
+ headCells = tHead.iterator();
+ i = 0;
+ checksum = availW;
+ while (headCells.hasNext()) {
+ HeaderCell hc = (HeaderCell) headCells.next();
+ if (!hc.isDefinedWidth()) {
+ widths[i]++;
+ break;
+ }
+ i++;
+ }
+
+ } else if (expandRatioDivider > 0) {
+ // visible columns have some active expand ratios, excess
+ // space is divided according to them
+ headCells = tHead.iterator();
+ i = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hCell = (HeaderCell) headCells.next();
+ if (hCell.getExpandRatio() > 0) {
+ int w = widths[i];
+ final int newSpace = Math.round((extraSpace * (hCell
+ .getExpandRatio() / expandRatioDivider)));
+ w += newSpace;
+ widths[i] = w;
+ }
+ checksum += widths[i];
+ i++;
+ }
+ } else if (totalWidthR > 0) {
+ // no expand ratios defined, we will share extra space
+ // relatively to "natural widths" among those without
+ // explicit width
+ headCells = tHead.iterator();
+ i = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hCell = (HeaderCell) headCells.next();
+ if (!hCell.isDefinedWidth()) {
+ int w = widths[i];
+ final int newSpace = Math.round((float) extraSpace
+ * (float) w / totalWidthR);
+ w += newSpace;
+ widths[i] = w;
+ }
+ checksum += widths[i];
+ i++;
+ }
+ }
+
+ if (extraSpace > 0 && checksum != availW) {
+ /*
+ * There might be in some cases a rounding error of 1px when
+ * extra space is divided so if there is one then we give the
+ * first undefined column 1 more pixel
+ */
+ headCells = tHead.iterator();
+ i = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hc = (HeaderCell) headCells.next();
+ if (!hc.isDefinedWidth()) {
+ widths[i] += availW - checksum;
+ break;
+ }
+ i++;
+ }
+ }
+
+ } else {
+ // bodys size will be more than available and scrollbar will appear
+ }
+
+ // last loop: set possibly modified values or reset if new tBody
+ i = 0;
+ headCells = tHead.iterator();
+ while (headCells.hasNext()) {
+ final HeaderCell hCell = (HeaderCell) headCells.next();
+ if (isNewBody || hCell.getWidth() == -1) {
+ final int w = widths[i];
+ setColWidth(i, w, false);
+ }
+ i++;
+ }
+
+ initializedAndAttached = true;
+
+ if (needsReLayout) {
+ scrollBody.reLayoutComponents();
+ }
+
+ updatePageLength();
+
+ /*
+ * Fix "natural" height if height is not set. This must be after width
+ * fixing so the components' widths have been adjusted.
+ */
+ if (isDynamicHeight()) {
+ /*
+ * We must force an update of the row height as this point as it
+ * might have been (incorrectly) calculated earlier
+ */
+
+ int bodyHeight;
+ if (pageLength == totalRows) {
+ /*
+ * A hack to support variable height rows when paging is off.
+ * Generally this is not supported by scrolltable. We want to
+ * show all rows so the bodyHeight should be equal to the table
+ * height.
+ */
+ // int bodyHeight = scrollBody.getOffsetHeight();
+ bodyHeight = scrollBody.getRequiredHeight();
+ } else {
+ bodyHeight = (int) Math.round(scrollBody.getRowHeight(true)
+ * pageLength);
+ }
+ boolean needsSpaceForHorizontalSrollbar = (total > availW);
+ if (needsSpaceForHorizontalSrollbar) {
+ bodyHeight += Util.getNativeScrollbarSize();
+ }
+ scrollBodyPanel.setHeight(bodyHeight + "px");
+ Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ }
+
+ isNewBody = false;
+
+ if (firstvisible > 0) {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstvisible));
+ firstRowInViewPort = firstvisible;
+ }
+
+ if (enabled) {
+ // Do we need cache rows
+ if (scrollBody.getLastRendered() + 1 < firstRowInViewPort
+ + pageLength + (int) cache_react_rate * pageLength) {
+ if (totalRows - 1 > scrollBody.getLastRendered()) {
+ // fetch cache rows
+ int firstInNewSet = scrollBody.getLastRendered() + 1;
+ rowRequestHandler.setReqFirstRow(firstInNewSet);
+ int lastInNewSet = (int) (firstRowInViewPort + pageLength + cache_rate
+ * pageLength);
+ if (lastInNewSet > totalRows - 1) {
+ lastInNewSet = totalRows - 1;
+ }
+ rowRequestHandler.setReqRows(lastInNewSet - firstInNewSet
+ + 1);
+ rowRequestHandler.deferRowFetch(1);
+ }
+ }
+ }
+
+ /*
+ * Ensures the column alignments are correct at initial loading. <br/>
+ * (child components widths are correct)
+ */
+ scrollBody.reLayoutComponents();
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ }
+ });
+ }
+
+ /**
+ * Note, this method is not official api although declared as protected.
+ * Extend at you own risk.
+ *
+ * @return true if content area will have scrollbars visible.
+ */
+ protected boolean willHaveScrollbars() {
+ if (isDynamicHeight()) {
+ if (pageLength < totalRows) {
+ return true;
+ }
+ } else {
+ int fakeheight = (int) Math.round(scrollBody.getRowHeight()
+ * totalRows);
+ int availableHeight = scrollBodyPanel.getElement().getPropertyInt(
+ "clientHeight");
+ if (fakeheight > availableHeight) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void announceScrollPosition() {
+ if (scrollPositionElement == null) {
+ scrollPositionElement = DOM.createDiv();
+ scrollPositionElement.setClassName(CLASSNAME + "-scrollposition");
+ scrollPositionElement.getStyle().setPosition(Position.ABSOLUTE);
+ scrollPositionElement.getStyle().setDisplay(Display.NONE);
+ getElement().appendChild(scrollPositionElement);
+ }
+
+ Style style = scrollPositionElement.getStyle();
+ style.setMarginLeft(getElement().getOffsetWidth() / 2 - 80, Unit.PX);
+ style.setMarginTop(-scrollBodyPanel.getOffsetHeight(), Unit.PX);
+
+ // indexes go from 1-totalRows, as rowheaders in index-mode indicate
+ int last = (firstRowInViewPort + pageLength);
+ if (last > totalRows) {
+ last = totalRows;
+ }
+ scrollPositionElement.setInnerHTML("<span>" + (firstRowInViewPort + 1)
+ + " – " + (last) + "..." + "</span>");
+ style.setDisplay(Display.BLOCK);
+ }
+
+ void hideScrollPositionAnnotation() {
+ if (scrollPositionElement != null) {
+ DOM.setStyleAttribute(scrollPositionElement, "display", "none");
+ }
+ }
+
+ boolean isScrollPositionVisible() {
+ return scrollPositionElement != null
+ && !scrollPositionElement.getStyle().getDisplay()
+ .equals(Display.NONE.toString());
+ }
+
+ class RowRequestHandler extends Timer {
+
+ private int reqFirstRow = 0;
+ private int reqRows = 0;
+ private boolean isRunning = false;
+
+ public void deferRowFetch() {
+ deferRowFetch(250);
+ }
+
+ public boolean isRunning() {
+ return isRunning;
+ }
+
+ public void deferRowFetch(int msec) {
+ isRunning = true;
+ if (reqRows > 0 && reqFirstRow < totalRows) {
+ schedule(msec);
+
+ // tell scroll position to user if currently "visible" rows are
+ // not rendered
+ if (totalRows > pageLength
+ && ((firstRowInViewPort + pageLength > scrollBody
+ .getLastRendered()) || (firstRowInViewPort < scrollBody
+ .getFirstRendered()))) {
+ announceScrollPosition();
+ } else {
+ hideScrollPositionAnnotation();
+ }
+ }
+ }
+
+ public void setReqFirstRow(int reqFirstRow) {
+ if (reqFirstRow < 0) {
+ reqFirstRow = 0;
+ } else if (reqFirstRow >= totalRows) {
+ reqFirstRow = totalRows - 1;
+ }
+ this.reqFirstRow = reqFirstRow;
+ }
+
+ public void setReqRows(int reqRows) {
+ this.reqRows = reqRows;
+ }
+
+ @Override
+ public void run() {
+ if (client.hasActiveRequest() || navKeyDown) {
+ // if client connection is busy, don't bother loading it more
+ VConsole.log("Postponed rowfetch");
+ schedule(250);
+ } else {
+
+ int firstToBeRendered = scrollBody.firstRendered;
+ if (reqFirstRow < firstToBeRendered) {
+ firstToBeRendered = reqFirstRow;
+ } else if (firstRowInViewPort - (int) (cache_rate * pageLength) > firstToBeRendered) {
+ firstToBeRendered = firstRowInViewPort
+ - (int) (cache_rate * pageLength);
+ if (firstToBeRendered < 0) {
+ firstToBeRendered = 0;
+ }
+ }
+
+ int lastToBeRendered = scrollBody.lastRendered;
+
+ if (reqFirstRow + reqRows - 1 > lastToBeRendered) {
+ lastToBeRendered = reqFirstRow + reqRows - 1;
+ } else if (firstRowInViewPort + pageLength + pageLength
+ * cache_rate < lastToBeRendered) {
+ lastToBeRendered = (firstRowInViewPort + pageLength + (int) (pageLength * cache_rate));
+ if (lastToBeRendered >= totalRows) {
+ lastToBeRendered = totalRows - 1;
+ }
+ // due Safari 3.1 bug (see #2607), verify reqrows, original
+ // problem unknown, but this should catch the issue
+ if (reqFirstRow + reqRows - 1 > lastToBeRendered) {
+ reqRows = lastToBeRendered - reqFirstRow;
+ }
+ }
+
+ client.updateVariable(paintableId, "firstToBeRendered",
+ firstToBeRendered, false);
+
+ client.updateVariable(paintableId, "lastToBeRendered",
+ lastToBeRendered, false);
+ // remember which firstvisible we requested, in case the server
+ // has
+ // a differing opinion
+ lastRequestedFirstvisible = firstRowInViewPort;
+ client.updateVariable(paintableId, "firstvisible",
+ firstRowInViewPort, false);
+ client.updateVariable(paintableId, "reqfirstrow", reqFirstRow,
+ false);
+ client.updateVariable(paintableId, "reqrows", reqRows, true);
+
+ if (selectionChanged) {
+ unSyncedselectionsBeforeRowFetch = new HashSet<Object>(
+ selectedRowKeys);
+ }
+ isRunning = false;
+ }
+ }
+
+ public int getReqFirstRow() {
+ return reqFirstRow;
+ }
+
+ /**
+ * Sends request to refresh content at this position.
+ */
+ public void refreshContent() {
+ isRunning = true;
+ int first = (int) (firstRowInViewPort - pageLength * cache_rate);
+ int reqRows = (int) (2 * pageLength * cache_rate + pageLength);
+ if (first < 0) {
+ reqRows = reqRows + first;
+ first = 0;
+ }
+ setReqFirstRow(first);
+ setReqRows(reqRows);
+ run();
+ }
+ }
+
+ public class HeaderCell extends Widget {
+
+ Element td = DOM.createTD();
+
+ Element captionContainer = DOM.createDiv();
+
+ Element sortIndicator = DOM.createDiv();
+
+ Element colResizeWidget = DOM.createDiv();
+
+ Element floatingCopyOfHeaderCell;
+
+ private boolean sortable = false;
+ private final String cid;
+ private boolean dragging;
+
+ private int dragStartX;
+ private int colIndex;
+ private int originalWidth;
+
+ private boolean isResizing;
+
+ private int headerX;
+
+ private boolean moved;
+
+ private int closestSlot;
+
+ private int width = -1;
+
+ private int naturalWidth = -1;
+
+ private char align = ALIGN_LEFT;
+
+ boolean definedWidth = false;
+
+ private float expandRatio = 0;
+
+ private boolean sorted;
+
+ public void setSortable(boolean b) {
+ sortable = b;
+ }
+
+ /**
+ * Makes room for the sorting indicator in case the column that the
+ * header cell belongs to is sorted. This is done by resizing the width
+ * of the caption container element by the correct amount
+ */
+ public void resizeCaptionContainer(int rightSpacing) {
+ int captionContainerWidth = width
+ - colResizeWidget.getOffsetWidth() - rightSpacing;
+
+ if (td.getClassName().contains("-asc")
+ || td.getClassName().contains("-desc")) {
+ // Leave room for the sort indicator
+ captionContainerWidth -= sortIndicator.getOffsetWidth();
+ }
+
+ if (captionContainerWidth < 0) {
+ rightSpacing += captionContainerWidth;
+ captionContainerWidth = 0;
+ }
+
+ captionContainer.getStyle().setPropertyPx("width",
+ captionContainerWidth);
+
+ // Apply/Remove spacing if defined
+ if (rightSpacing > 0) {
+ colResizeWidget.getStyle().setMarginLeft(rightSpacing, Unit.PX);
+ } else {
+ colResizeWidget.getStyle().clearMarginLeft();
+ }
+ }
+
+ public void setNaturalMinimumColumnWidth(int w) {
+ naturalWidth = w;
+ }
+
+ public HeaderCell(String colId, String headerText) {
+ cid = colId;
+
+ DOM.setElementProperty(colResizeWidget, "className", CLASSNAME
+ + "-resizer");
+
+ setText(headerText);
+
+ DOM.appendChild(td, colResizeWidget);
+
+ DOM.setElementProperty(sortIndicator, "className", CLASSNAME
+ + "-sort-indicator");
+ DOM.appendChild(td, sortIndicator);
+
+ DOM.setElementProperty(captionContainer, "className", CLASSNAME
+ + "-caption-container");
+
+ // ensure no clipping initially (problem on column additions)
+ DOM.setStyleAttribute(captionContainer, "overflow", "visible");
+
+ DOM.appendChild(td, captionContainer);
+
+ DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK
+ | Event.ONCONTEXTMENU | Event.TOUCHEVENTS);
+
+ setElement(td);
+
+ setAlign(ALIGN_LEFT);
+ }
+
+ public void disableAutoWidthCalculation() {
+ definedWidth = true;
+ expandRatio = 0;
+ }
+
+ public void setWidth(int w, boolean ensureDefinedWidth) {
+ if (ensureDefinedWidth) {
+ definedWidth = true;
+ // on column resize expand ratio becomes zero
+ expandRatio = 0;
+ }
+ if (width == -1) {
+ // go to default mode, clip content if necessary
+ DOM.setStyleAttribute(captionContainer, "overflow", "");
+ }
+ width = w;
+ if (w == -1) {
+ DOM.setStyleAttribute(captionContainer, "width", "");
+ setWidth("");
+ } else {
+ tHead.resizeCaptionContainer(this);
+
+ /*
+ * if we already have tBody, set the header width properly, if
+ * not defer it. IE will fail with complex float in table header
+ * unless TD width is not explicitly set.
+ */
+ if (scrollBody != null) {
+ int tdWidth = width + scrollBody.getCellExtraWidth();
+ setWidth(tdWidth + "px");
+ } else {
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ int tdWidth = width
+ + scrollBody.getCellExtraWidth();
+ setWidth(tdWidth + "px");
+ }
+ });
+ }
+ }
+ }
+
+ public void setUndefinedWidth() {
+ definedWidth = false;
+ setWidth(-1, false);
+ }
+
+ /**
+ * Detects if width is fixed by developer on server side or resized to
+ * current width by user.
+ *
+ * @return true if defined, false if "natural" width
+ */
+ public boolean isDefinedWidth() {
+ return definedWidth && width >= 0;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setText(String headerText) {
+ DOM.setInnerHTML(captionContainer, headerText);
+ }
+
+ public String getColKey() {
+ return cid;
+ }
+
+ private void setSorted(boolean sorted) {
+ this.sorted = sorted;
+ if (sorted) {
+ if (sortAscending) {
+ this.setStyleName(CLASSNAME + "-header-cell-asc");
+ } else {
+ this.setStyleName(CLASSNAME + "-header-cell-desc");
+ }
+ } else {
+ this.setStyleName(CLASSNAME + "-header-cell");
+ }
+ }
+
+ /**
+ * Handle column reordering.
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (enabled && event != null) {
+ if (isResizing
+ || event.getEventTarget().cast() == colResizeWidget) {
+ if (dragging
+ && (event.getTypeInt() == Event.ONMOUSEUP || event
+ .getTypeInt() == Event.ONTOUCHEND)) {
+ // Handle releasing column header on spacer #5318
+ handleCaptionEvent(event);
+ } else {
+ onResizeEvent(event);
+ }
+ } else {
+ /*
+ * Ensure focus before handling caption event. Otherwise
+ * variables changed from caption event may be before
+ * variables from other components that fire variables when
+ * they lose focus.
+ */
+ if (event.getTypeInt() == Event.ONMOUSEDOWN
+ || event.getTypeInt() == Event.ONTOUCHSTART) {
+ scrollBodyPanel.setFocus(true);
+ }
+ handleCaptionEvent(event);
+ boolean stopPropagation = true;
+ if (event.getTypeInt() == Event.ONCONTEXTMENU
+ && !client.hasEventListeners(VScrollTable.this,
+ HEADER_CLICK_EVENT_ID)) {
+ // Prevent showing the browser's context menu only when
+ // there is a header click listener.
+ stopPropagation = false;
+ }
+ if (stopPropagation) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+ }
+ }
+
+ private void createFloatingCopy() {
+ floatingCopyOfHeaderCell = DOM.createDiv();
+ DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td));
+ floatingCopyOfHeaderCell = DOM
+ .getChild(floatingCopyOfHeaderCell, 2);
+ DOM.setElementProperty(floatingCopyOfHeaderCell, "className",
+ CLASSNAME + "-header-drag");
+ // otherwise might wrap or be cut if narrow column
+ DOM.setStyleAttribute(floatingCopyOfHeaderCell, "width", "auto");
+ updateFloatingCopysPosition(DOM.getAbsoluteLeft(td),
+ DOM.getAbsoluteTop(td));
+ DOM.appendChild(RootPanel.get().getElement(),
+ floatingCopyOfHeaderCell);
+ }
+
+ private void updateFloatingCopysPosition(int x, int y) {
+ x -= DOM.getElementPropertyInt(floatingCopyOfHeaderCell,
+ "offsetWidth") / 2;
+ DOM.setStyleAttribute(floatingCopyOfHeaderCell, "left", x + "px");
+ if (y > 0) {
+ DOM.setStyleAttribute(floatingCopyOfHeaderCell, "top", (y + 7)
+ + "px");
+ }
+ }
+
+ private void hideFloatingCopy() {
+ DOM.removeChild(RootPanel.get().getElement(),
+ floatingCopyOfHeaderCell);
+ floatingCopyOfHeaderCell = null;
+ }
+
+ /**
+ * Fires a header click event after the user has clicked a column header
+ * cell
+ *
+ * @param event
+ * The click event
+ */
+ private void fireHeaderClickedEvent(Event event) {
+ if (client.hasEventListeners(VScrollTable.this,
+ HEADER_CLICK_EVENT_ID)) {
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event);
+ client.updateVariable(paintableId, "headerClickEvent",
+ details.toString(), false);
+ client.updateVariable(paintableId, "headerClickCID", cid, true);
+ }
+ }
+
+ protected void handleCaptionEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONTOUCHSTART:
+ case Event.ONMOUSEDOWN:
+ if (columnReordering
+ && Util.isTouchEventOrLeftMouseButton(event)) {
+ if (event.getTypeInt() == Event.ONTOUCHSTART) {
+ /*
+ * prevent using this event in e.g. scrolling
+ */
+ event.stopPropagation();
+ }
+ dragging = true;
+ moved = false;
+ colIndex = getColIndexByKey(cid);
+ DOM.setCapture(getElement());
+ headerX = tHead.getAbsoluteLeft();
+ event.preventDefault(); // prevent selecting text &&
+ // generated touch events
+ }
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ if (columnReordering
+ && Util.isTouchEventOrLeftMouseButton(event)) {
+ dragging = false;
+ DOM.releaseCapture(getElement());
+ if (moved) {
+ hideFloatingCopy();
+ tHead.removeSlotFocus();
+ if (closestSlot != colIndex
+ && closestSlot != (colIndex + 1)) {
+ if (closestSlot > colIndex) {
+ reOrderColumn(cid, closestSlot - 1);
+ } else {
+ reOrderColumn(cid, closestSlot);
+ }
+ }
+ }
+ if (Util.isTouchEvent(event)) {
+ /*
+ * Prevent using in e.g. scrolling and prevent generated
+ * events.
+ */
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ if (!moved) {
+ // mouse event was a click to header -> sort column
+ if (sortable && Util.isTouchEventOrLeftMouseButton(event)) {
+ if (sortColumn.equals(cid)) {
+ // just toggle order
+ client.updateVariable(paintableId, "sortascending",
+ !sortAscending, false);
+ } else {
+ // set table sorted by this column
+ client.updateVariable(paintableId, "sortcolumn",
+ cid, false);
+ }
+ // get also cache columns at the same request
+ scrollBodyPanel.setScrollPosition(0);
+ firstvisible = 0;
+ rowRequestHandler.setReqFirstRow(0);
+ rowRequestHandler.setReqRows((int) (2 * pageLength
+ * cache_rate + pageLength));
+ rowRequestHandler.deferRowFetch(); // some validation +
+ // defer 250ms
+ rowRequestHandler.cancel(); // instead of waiting
+ rowRequestHandler.run(); // run immediately
+ }
+ fireHeaderClickedEvent(event);
+ if (Util.isTouchEvent(event)) {
+ /*
+ * Prevent using in e.g. scrolling and prevent generated
+ * events.
+ */
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ break;
+ }
+ break;
+ case Event.ONDBLCLICK:
+ fireHeaderClickedEvent(event);
+ break;
+ case Event.ONTOUCHMOVE:
+ case Event.ONMOUSEMOVE:
+ if (dragging && Util.isTouchEventOrLeftMouseButton(event)) {
+ if (event.getTypeInt() == Event.ONTOUCHMOVE) {
+ /*
+ * prevent using this event in e.g. scrolling
+ */
+ event.stopPropagation();
+ }
+ if (!moved) {
+ createFloatingCopy();
+ moved = true;
+ }
+
+ final int clientX = Util.getTouchOrMouseClientX(event);
+ final int x = clientX + tHead.hTableWrapper.getScrollLeft();
+ int slotX = headerX;
+ closestSlot = colIndex;
+ int closestDistance = -1;
+ int start = 0;
+ if (showRowHeaders) {
+ start++;
+ }
+ final int visibleCellCount = tHead.getVisibleCellCount();
+ for (int i = start; i <= visibleCellCount; i++) {
+ if (i > 0) {
+ final String colKey = getColKeyByIndex(i - 1);
+ slotX += getColWidth(colKey);
+ }
+ final int dist = Math.abs(x - slotX);
+ if (closestDistance == -1 || dist < closestDistance) {
+ closestDistance = dist;
+ closestSlot = i;
+ }
+ }
+ tHead.focusSlot(closestSlot);
+
+ updateFloatingCopysPosition(clientX, -1);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void onResizeEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEDOWN:
+ if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ return;
+ }
+ isResizing = true;
+ DOM.setCapture(getElement());
+ dragStartX = DOM.eventGetClientX(event);
+ colIndex = getColIndexByKey(cid);
+ originalWidth = getWidth();
+ DOM.eventPreventDefault(event);
+ break;
+ case Event.ONMOUSEUP:
+ if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ return;
+ }
+ isResizing = false;
+ DOM.releaseCapture(getElement());
+ tHead.disableAutoColumnWidthCalculation(this);
+
+ // Ensure last header cell is taking into account possible
+ // column selector
+ HeaderCell lastCell = tHead.getHeaderCell(tHead
+ .getVisibleCellCount() - 1);
+ tHead.resizeCaptionContainer(lastCell);
+ triggerLazyColumnAdjustment(true);
+
+ fireColumnResizeEvent(cid, originalWidth, getColWidth(cid));
+ break;
+ case Event.ONMOUSEMOVE:
+ if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ return;
+ }
+ if (isResizing) {
+ final int deltaX = DOM.eventGetClientX(event) - dragStartX;
+ if (deltaX == 0) {
+ return;
+ }
+ tHead.disableAutoColumnWidthCalculation(this);
+
+ int newWidth = originalWidth + deltaX;
+ if (newWidth < getMinWidth()) {
+ newWidth = getMinWidth();
+ }
+ setColWidth(colIndex, newWidth, true);
+ triggerLazyColumnAdjustment(false);
+ forceRealignColumnHeaders();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ public int getMinWidth() {
+ int cellExtraWidth = 0;
+ if (scrollBody != null) {
+ cellExtraWidth += scrollBody.getCellExtraWidth();
+ }
+ return cellExtraWidth + sortIndicator.getOffsetWidth();
+ }
+
+ public String getCaption() {
+ return DOM.getInnerText(captionContainer);
+ }
+
+ public boolean isEnabled() {
+ return getParent() != null;
+ }
+
+ public void setAlign(char c) {
+ final String ALIGN_PREFIX = CLASSNAME + "-caption-container-align-";
+ if (align != c) {
+ captionContainer.removeClassName(ALIGN_PREFIX + "center");
+ captionContainer.removeClassName(ALIGN_PREFIX + "right");
+ captionContainer.removeClassName(ALIGN_PREFIX + "left");
+ switch (c) {
+ case ALIGN_CENTER:
+ captionContainer.addClassName(ALIGN_PREFIX + "center");
+ break;
+ case ALIGN_RIGHT:
+ captionContainer.addClassName(ALIGN_PREFIX + "right");
+ break;
+ default:
+ captionContainer.addClassName(ALIGN_PREFIX + "left");
+ break;
+ }
+ }
+ align = c;
+ }
+
+ public char getAlign() {
+ return align;
+ }
+
+ /**
+ * Detects the natural minimum width for the column of this header cell.
+ * If column is resized by user or the width is defined by server the
+ * actual width is returned. Else the natural min width is returned.
+ *
+ * @param columnIndex
+ * column index hint, if -1 (unknown) it will be detected
+ *
+ * @return
+ */
+ public int getNaturalColumnWidth(int columnIndex) {
+ if (isDefinedWidth()) {
+ return width;
+ } else {
+ if (naturalWidth < 0) {
+ // This is recently revealed column. Try to detect a proper
+ // value (greater of header and data
+ // cols)
+
+ int hw = captionContainer.getOffsetWidth()
+ + scrollBody.getCellExtraWidth();
+ if (BrowserInfo.get().isGecko()) {
+ hw += sortIndicator.getOffsetWidth();
+ }
+ if (columnIndex < 0) {
+ columnIndex = 0;
+ for (Iterator<Widget> it = tHead.iterator(); it
+ .hasNext(); columnIndex++) {
+ if (it.next() == this) {
+ break;
+ }
+ }
+ }
+ final int cw = scrollBody.getColWidth(columnIndex);
+ naturalWidth = (hw > cw ? hw : cw);
+ }
+ return naturalWidth;
+ }
+ }
+
+ public void setExpandRatio(float floatAttribute) {
+ if (floatAttribute != expandRatio) {
+ triggerLazyColumnAdjustment(false);
+ }
+ expandRatio = floatAttribute;
+ }
+
+ public float getExpandRatio() {
+ return expandRatio;
+ }
+
+ public boolean isSorted() {
+ return sorted;
+ }
+ }
+
+ /**
+ * HeaderCell that is header cell for row headers.
+ *
+ * Reordering disabled and clicking on it resets sorting.
+ */
+ public class RowHeadersHeaderCell extends HeaderCell {
+
+ RowHeadersHeaderCell() {
+ super(ROW_HEADER_COLUMN_KEY, "");
+ this.setStyleName(CLASSNAME + "-header-cell-rowheader");
+ }
+
+ @Override
+ protected void handleCaptionEvent(Event event) {
+ // NOP: RowHeaders cannot be reordered
+ // TODO It'd be nice to reset sorting here
+ }
+ }
+
+ public class TableHead extends Panel implements ActionOwner {
+
+ private static final int WRAPPER_WIDTH = 900000;
+
+ ArrayList<Widget> visibleCells = new ArrayList<Widget>();
+
+ HashMap<String, HeaderCell> availableCells = new HashMap<String, HeaderCell>();
+
+ Element div = DOM.createDiv();
+ Element hTableWrapper = DOM.createDiv();
+ Element hTableContainer = DOM.createDiv();
+ Element table = DOM.createTable();
+ Element headerTableBody = DOM.createTBody();
+ Element tr = DOM.createTR();
+
+ private final Element columnSelector = DOM.createDiv();
+
+ private int focusedSlot = -1;
+
+ public TableHead() {
+ if (BrowserInfo.get().isIE()) {
+ table.setPropertyInt("cellSpacing", 0);
+ }
+
+ DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden");
+ DOM.setElementProperty(hTableWrapper, "className", CLASSNAME
+ + "-header");
+
+ // TODO move styles to CSS
+ DOM.setElementProperty(columnSelector, "className", CLASSNAME
+ + "-column-selector");
+ DOM.setStyleAttribute(columnSelector, "display", "none");
+
+ DOM.appendChild(table, headerTableBody);
+ DOM.appendChild(headerTableBody, tr);
+ DOM.appendChild(hTableContainer, table);
+ DOM.appendChild(hTableWrapper, hTableContainer);
+ DOM.appendChild(div, hTableWrapper);
+ DOM.appendChild(div, columnSelector);
+ setElement(div);
+
+ setStyleName(CLASSNAME + "-header-wrap");
+
+ DOM.sinkEvents(columnSelector, Event.ONCLICK);
+
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersHeaderCell());
+ }
+
+ public void resizeCaptionContainer(HeaderCell cell) {
+ HeaderCell lastcell = getHeaderCell(visibleCells.size() - 1);
+
+ // Measure column widths
+ int columnTotalWidth = 0;
+ for (Widget w : visibleCells) {
+ columnTotalWidth += w.getOffsetWidth();
+ }
+
+ if (cell == lastcell
+ && columnSelector.getOffsetWidth() > 0
+ && columnTotalWidth >= div.getOffsetWidth()
+ - columnSelector.getOffsetWidth()
+ && !hasVerticalScrollbar()) {
+ // Ensure column caption is visible when placed under the column
+ // selector widget by shifting and resizing the caption.
+ int offset = 0;
+ int diff = div.getOffsetWidth() - columnTotalWidth;
+ if (diff < columnSelector.getOffsetWidth() && diff > 0) {
+ // If the difference is less than the column selectors width
+ // then just offset by the
+ // difference
+ offset = columnSelector.getOffsetWidth() - diff;
+ } else {
+ // Else offset by the whole column selector
+ offset = columnSelector.getOffsetWidth();
+ }
+ lastcell.resizeCaptionContainer(offset);
+ } else {
+ cell.resizeCaptionContainer(0);
+ }
+ }
+
+ @Override
+ public void clear() {
+ for (String cid : availableCells.keySet()) {
+ removeCell(cid);
+ }
+ availableCells.clear();
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersHeaderCell());
+ }
+
+ public void updateCellsFromUIDL(UIDL uidl) {
+ Iterator<?> it = uidl.getChildIterator();
+ HashSet<String> updated = new HashSet<String>();
+ boolean refreshContentWidths = false;
+ while (it.hasNext()) {
+ final UIDL col = (UIDL) it.next();
+ final String cid = col.getStringAttribute("cid");
+ updated.add(cid);
+
+ String caption = buildCaptionHtmlSnippet(col);
+ HeaderCell c = getHeaderCell(cid);
+ if (c == null) {
+ c = new HeaderCell(cid, caption);
+ availableCells.put(cid, c);
+ if (initializedAndAttached) {
+ // we will need a column width recalculation
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ } else {
+ c.setText(caption);
+ }
+
+ if (col.hasAttribute("sortable")) {
+ c.setSortable(true);
+ if (cid.equals(sortColumn)) {
+ c.setSorted(true);
+ } else {
+ c.setSorted(false);
+ }
+ } else {
+ c.setSortable(false);
+ }
+
+ if (col.hasAttribute("align")) {
+ c.setAlign(col.getStringAttribute("align").charAt(0));
+ } else {
+ c.setAlign(ALIGN_LEFT);
+
+ }
+ if (col.hasAttribute("width")) {
+ final String widthStr = col.getStringAttribute("width");
+ // Make sure to accomodate for the sort indicator if
+ // necessary.
+ int width = Integer.parseInt(widthStr);
+ if (width < c.getMinWidth()) {
+ width = c.getMinWidth();
+ }
+ if (width != c.getWidth() && scrollBody != null) {
+ // Do a more thorough update if a column is resized from
+ // the server *after* the header has been properly
+ // initialized
+ final int colIx = getColIndexByKey(c.cid);
+ final int newWidth = width;
+ Scheduler.get().scheduleDeferred(
+ new ScheduledCommand() {
+ public void execute() {
+ setColWidth(colIx, newWidth, true);
+ }
+ });
+ refreshContentWidths = true;
+ } else {
+ c.setWidth(width, true);
+ }
+ } else if (recalcWidths) {
+ c.setUndefinedWidth();
+ }
+ if (col.hasAttribute("er")) {
+ c.setExpandRatio(col.getFloatAttribute("er"));
+ }
+ if (col.hasAttribute("collapsed")) {
+ // ensure header is properly removed from parent (case when
+ // collapsing happens via servers side api)
+ if (c.isAttached()) {
+ c.removeFromParent();
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ if (refreshContentWidths) {
+ // Recalculate the column sizings if any column has changed
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ public void execute() {
+ triggerLazyColumnAdjustment(true);
+ }
+ });
+ }
+
+ // check for orphaned header cells
+ for (Iterator<String> cit = availableCells.keySet().iterator(); cit
+ .hasNext();) {
+ String cid = cit.next();
+ if (!updated.contains(cid)) {
+ removeCell(cid);
+ cit.remove();
+ // we will need a column width recalculation, since columns
+ // with expand ratios should expand to fill the void.
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ }
+ }
+
+ public void enableColumn(String cid, int index) {
+ final HeaderCell c = getHeaderCell(cid);
+ if (!c.isEnabled() || getHeaderCell(index) != c) {
+ setHeaderCell(index, c);
+ if (initializedAndAttached) {
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ public int getVisibleCellCount() {
+ return visibleCells.size();
+ }
+
+ public void setHorizontalScrollPosition(int scrollLeft) {
+ hTableWrapper.setScrollLeft(scrollLeft);
+ }
+
+ public void setColumnCollapsingAllowed(boolean cc) {
+ if (cc) {
+ columnSelector.getStyle().setDisplay(Display.BLOCK);
+ } else {
+ columnSelector.getStyle().setDisplay(Display.NONE);
+ }
+ }
+
+ public void disableBrowserIntelligence() {
+ hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX);
+ }
+
+ public void enableBrowserIntelligence() {
+ hTableContainer.getStyle().clearWidth();
+ }
+
+ public void setHeaderCell(int index, HeaderCell cell) {
+ if (cell.isEnabled()) {
+ // we're moving the cell
+ DOM.removeChild(tr, cell.getElement());
+ orphan(cell);
+ visibleCells.remove(cell);
+ }
+ if (index < visibleCells.size()) {
+ // insert to right slot
+ DOM.insertChild(tr, cell.getElement(), index);
+ adopt(cell);
+ visibleCells.add(index, cell);
+ } else if (index == visibleCells.size()) {
+ // simply append
+ DOM.appendChild(tr, cell.getElement());
+ adopt(cell);
+ visibleCells.add(cell);
+ } else {
+ throw new RuntimeException(
+ "Header cells must be appended in order");
+ }
+ }
+
+ public HeaderCell getHeaderCell(int index) {
+ if (index >= 0 && index < visibleCells.size()) {
+ return (HeaderCell) visibleCells.get(index);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get's HeaderCell by it's column Key.
+ *
+ * Note that this returns HeaderCell even if it is currently collapsed.
+ *
+ * @param cid
+ * Column key of accessed HeaderCell
+ * @return HeaderCell
+ */
+ public HeaderCell getHeaderCell(String cid) {
+ return availableCells.get(cid);
+ }
+
+ public void moveCell(int oldIndex, int newIndex) {
+ final HeaderCell hCell = getHeaderCell(oldIndex);
+ final Element cell = hCell.getElement();
+
+ visibleCells.remove(oldIndex);
+ DOM.removeChild(tr, cell);
+
+ DOM.insertChild(tr, cell, newIndex);
+ visibleCells.add(newIndex, hCell);
+ }
+
+ public Iterator<Widget> iterator() {
+ return visibleCells.iterator();
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ if (visibleCells.contains(w)) {
+ visibleCells.remove(w);
+ orphan(w);
+ DOM.removeChild(DOM.getParent(w.getElement()), w.getElement());
+ return true;
+ }
+ return false;
+ }
+
+ public void removeCell(String colKey) {
+ final HeaderCell c = getHeaderCell(colKey);
+ remove(c);
+ }
+
+ private void focusSlot(int index) {
+ removeSlotFocus();
+ if (index > 0) {
+ DOM.setElementProperty(
+ DOM.getFirstChild(DOM.getChild(tr, index - 1)),
+ "className", CLASSNAME + "-resizer " + CLASSNAME
+ + "-focus-slot-right");
+ } else {
+ DOM.setElementProperty(
+ DOM.getFirstChild(DOM.getChild(tr, index)),
+ "className", CLASSNAME + "-resizer " + CLASSNAME
+ + "-focus-slot-left");
+ }
+ focusedSlot = index;
+ }
+
+ private void removeSlotFocus() {
+ if (focusedSlot < 0) {
+ return;
+ }
+ if (focusedSlot == 0) {
+ DOM.setElementProperty(
+ DOM.getFirstChild(DOM.getChild(tr, focusedSlot)),
+ "className", CLASSNAME + "-resizer");
+ } else if (focusedSlot > 0) {
+ DOM.setElementProperty(
+ DOM.getFirstChild(DOM.getChild(tr, focusedSlot - 1)),
+ "className", CLASSNAME + "-resizer");
+ }
+ focusedSlot = -1;
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (enabled) {
+ if (event.getEventTarget().cast() == columnSelector) {
+ final int left = DOM.getAbsoluteLeft(columnSelector);
+ final int top = DOM.getAbsoluteTop(columnSelector)
+ + DOM.getElementPropertyInt(columnSelector,
+ "offsetHeight");
+ client.getContextMenu().showAt(this, left, top);
+ }
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ if (client != null) {
+ client.getContextMenu().ensureHidden(this);
+ }
+ }
+
+ class VisibleColumnAction extends Action {
+
+ String colKey;
+ private boolean collapsed;
+ private VScrollTableRow currentlyFocusedRow;
+
+ public VisibleColumnAction(String colKey) {
+ super(VScrollTable.TableHead.this);
+ this.colKey = colKey;
+ caption = tHead.getHeaderCell(colKey).getCaption();
+ currentlyFocusedRow = focusedRow;
+ }
+
+ @Override
+ public void execute() {
+ client.getContextMenu().hide();
+ // toggle selected column
+ if (collapsedColumns.contains(colKey)) {
+ collapsedColumns.remove(colKey);
+ } else {
+ tHead.removeCell(colKey);
+ collapsedColumns.add(colKey);
+ triggerLazyColumnAdjustment(true);
+ }
+
+ // update variable to server
+ client.updateVariable(paintableId, "collapsedcolumns",
+ collapsedColumns.toArray(new String[collapsedColumns
+ .size()]), false);
+ // let rowRequestHandler determine proper rows
+ rowRequestHandler.refreshContent();
+ lazyRevertFocusToRow(currentlyFocusedRow);
+ }
+
+ public void setCollapsed(boolean b) {
+ collapsed = b;
+ }
+
+ /**
+ * Override default method to distinguish on/off columns
+ */
+ @Override
+ public String getHTML() {
+ final StringBuffer buf = new StringBuffer();
+ if (collapsed) {
+ buf.append("<span class=\"v-off\">");
+ } else {
+ buf.append("<span class=\"v-on\">");
+ }
+ buf.append(super.getHTML());
+ buf.append("</span>");
+
+ return buf.toString();
+ }
+
+ }
+
+ /*
+ * Returns columns as Action array for column select popup
+ */
+ public Action[] getActions() {
+ Object[] cols;
+ if (columnReordering && columnOrder != null) {
+ cols = columnOrder;
+ } else {
+ // if columnReordering is disabled, we need different way to get
+ // all available columns
+ cols = visibleColOrder;
+ cols = new Object[visibleColOrder.length
+ + collapsedColumns.size()];
+ int i;
+ for (i = 0; i < visibleColOrder.length; i++) {
+ cols[i] = visibleColOrder[i];
+ }
+ for (final Iterator<String> it = collapsedColumns.iterator(); it
+ .hasNext();) {
+ cols[i++] = it.next();
+ }
+ }
+ final Action[] actions = new Action[cols.length];
+
+ for (int i = 0; i < cols.length; i++) {
+ final String cid = (String) cols[i];
+ final HeaderCell c = getHeaderCell(cid);
+ final VisibleColumnAction a = new VisibleColumnAction(
+ c.getColKey());
+ a.setCaption(c.getCaption());
+ if (!c.isEnabled()) {
+ a.setCollapsed(true);
+ }
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ /**
+ * Returns column alignments for visible columns
+ */
+ public char[] getColumnAlignments() {
+ final Iterator<Widget> it = visibleCells.iterator();
+ final char[] aligns = new char[visibleCells.size()];
+ int colIndex = 0;
+ while (it.hasNext()) {
+ aligns[colIndex++] = ((HeaderCell) it.next()).getAlign();
+ }
+ return aligns;
+ }
+
+ /**
+ * Disables the automatic calculation of all column widths by forcing
+ * the widths to be "defined" thus turning off expand ratios and such.
+ */
+ public void disableAutoColumnWidthCalculation(HeaderCell source) {
+ for (HeaderCell cell : availableCells.values()) {
+ cell.disableAutoWidthCalculation();
+ }
+ // fire column resize events for all columns but the source of the
+ // resize action, since an event will fire separately for this.
+ ArrayList<HeaderCell> columns = new ArrayList<HeaderCell>(
+ availableCells.values());
+ columns.remove(source);
+ sendColumnWidthUpdates(columns);
+ forceRealignColumnHeaders();
+ }
+ }
+
+ /**
+ * A cell in the footer
+ */
+ public class FooterCell extends Widget {
+ private final Element td = DOM.createTD();
+ private final Element captionContainer = DOM.createDiv();
+ private char align = ALIGN_LEFT;
+ private int width = -1;
+ private float expandRatio = 0;
+ private final String cid;
+ boolean definedWidth = false;
+ private int naturalWidth = -1;
+
+ public FooterCell(String colId, String headerText) {
+ cid = colId;
+
+ setText(headerText);
+
+ DOM.setElementProperty(captionContainer, "className", CLASSNAME
+ + "-footer-container");
+
+ // ensure no clipping initially (problem on column additions)
+ DOM.setStyleAttribute(captionContainer, "overflow", "visible");
+
+ DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS);
+
+ DOM.appendChild(td, captionContainer);
+
+ DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK
+ | Event.ONCONTEXTMENU);
+
+ setElement(td);
+ }
+
+ /**
+ * Sets the text of the footer
+ *
+ * @param footerText
+ * The text in the footer
+ */
+ public void setText(String footerText) {
+ DOM.setInnerHTML(captionContainer, footerText);
+ }
+
+ /**
+ * Set alignment of the text in the cell
+ *
+ * @param c
+ * The alignment which can be ALIGN_CENTER, ALIGN_LEFT,
+ * ALIGN_RIGHT
+ */
+ public void setAlign(char c) {
+ if (align != c) {
+ switch (c) {
+ case ALIGN_CENTER:
+ DOM.setStyleAttribute(captionContainer, "textAlign",
+ "center");
+ break;
+ case ALIGN_RIGHT:
+ DOM.setStyleAttribute(captionContainer, "textAlign",
+ "right");
+ break;
+ default:
+ DOM.setStyleAttribute(captionContainer, "textAlign", "");
+ break;
+ }
+ }
+ align = c;
+ }
+
+ /**
+ * Get the alignment of the text int the cell
+ *
+ * @return Returns either ALIGN_CENTER, ALIGN_LEFT or ALIGN_RIGHT
+ */
+ public char getAlign() {
+ return align;
+ }
+
+ /**
+ * Sets the width of the cell
+ *
+ * @param w
+ * The width of the cell
+ * @param ensureDefinedWidth
+ * Ensures the the given width is not recalculated
+ */
+ public void setWidth(int w, boolean ensureDefinedWidth) {
+
+ if (ensureDefinedWidth) {
+ definedWidth = true;
+ // on column resize expand ratio becomes zero
+ expandRatio = 0;
+ }
+ if (width == w) {
+ return;
+ }
+ if (width == -1) {
+ // go to default mode, clip content if necessary
+ DOM.setStyleAttribute(captionContainer, "overflow", "");
+ }
+ width = w;
+ if (w == -1) {
+ DOM.setStyleAttribute(captionContainer, "width", "");
+ setWidth("");
+ } else {
+
+ /*
+ * Reduce width with one pixel for the right border since the
+ * footers does not have any spacers between them.
+ */
+ int borderWidths = 1;
+
+ // Set the container width (check for negative value)
+ if (w - borderWidths >= 0) {
+ captionContainer.getStyle().setPropertyPx("width",
+ w - borderWidths);
+ } else {
+ captionContainer.getStyle().setPropertyPx("width", 0);
+ }
+
+ /*
+ * if we already have tBody, set the header width properly, if
+ * not defer it. IE will fail with complex float in table header
+ * unless TD width is not explicitly set.
+ */
+ if (scrollBody != null) {
+ /*
+ * Reduce with one since footer does not have any spacers,
+ * instead a 1 pixel border.
+ */
+ int tdWidth = width + scrollBody.getCellExtraWidth()
+ - borderWidths;
+ setWidth(tdWidth + "px");
+ } else {
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ int borderWidths = 1;
+ int tdWidth = width
+ + scrollBody.getCellExtraWidth()
+ - borderWidths;
+ setWidth(tdWidth + "px");
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Sets the width to undefined
+ */
+ public void setUndefinedWidth() {
+ setWidth(-1, false);
+ }
+
+ /**
+ * Detects if width is fixed by developer on server side or resized to
+ * current width by user.
+ *
+ * @return true if defined, false if "natural" width
+ */
+ public boolean isDefinedWidth() {
+ return definedWidth && width >= 0;
+ }
+
+ /**
+ * Returns the pixels width of the footer cell
+ *
+ * @return The width in pixels
+ */
+ public int getWidth() {
+ return width;
+ }
+
+ /**
+ * Sets the expand ratio of the cell
+ *
+ * @param floatAttribute
+ * The expand ratio
+ */
+ public void setExpandRatio(float floatAttribute) {
+ expandRatio = floatAttribute;
+ }
+
+ /**
+ * Returns the expand ration of the cell
+ *
+ * @return The expand ratio
+ */
+ public float getExpandRatio() {
+ return expandRatio;
+ }
+
+ /**
+ * Is the cell enabled?
+ *
+ * @return True if enabled else False
+ */
+ public boolean isEnabled() {
+ return getParent() != null;
+ }
+
+ /**
+ * Handle column clicking
+ */
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (enabled && event != null) {
+ handleCaptionEvent(event);
+
+ if (DOM.eventGetType(event) == Event.ONMOUSEUP) {
+ scrollBodyPanel.setFocus(true);
+ }
+ boolean stopPropagation = true;
+ if (event.getTypeInt() == Event.ONCONTEXTMENU
+ && !client.hasEventListeners(VScrollTable.this,
+ FOOTER_CLICK_EVENT_ID)) {
+ // Show browser context menu if a footer click listener is
+ // not present
+ stopPropagation = false;
+ }
+ if (stopPropagation) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+ }
+
+ /**
+ * Handles a event on the captions
+ *
+ * @param event
+ * The event to handle
+ */
+ protected void handleCaptionEvent(Event event) {
+ if (event.getTypeInt() == Event.ONMOUSEUP
+ || event.getTypeInt() == Event.ONDBLCLICK) {
+ fireFooterClickedEvent(event);
+ }
+ }
+
+ /**
+ * Fires a footer click event after the user has clicked a column footer
+ * cell
+ *
+ * @param event
+ * The click event
+ */
+ private void fireFooterClickedEvent(Event event) {
+ if (client.hasEventListeners(VScrollTable.this,
+ FOOTER_CLICK_EVENT_ID)) {
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event);
+ client.updateVariable(paintableId, "footerClickEvent",
+ details.toString(), false);
+ client.updateVariable(paintableId, "footerClickCID", cid, true);
+ }
+ }
+
+ /**
+ * Returns the column key of the column
+ *
+ * @return The column key
+ */
+ public String getColKey() {
+ return cid;
+ }
+
+ /**
+ * Detects the natural minimum width for the column of this header cell.
+ * If column is resized by user or the width is defined by server the
+ * actual width is returned. Else the natural min width is returned.
+ *
+ * @param columnIndex
+ * column index hint, if -1 (unknown) it will be detected
+ *
+ * @return
+ */
+ public int getNaturalColumnWidth(int columnIndex) {
+ if (isDefinedWidth()) {
+ return width;
+ } else {
+ if (naturalWidth < 0) {
+ // This is recently revealed column. Try to detect a proper
+ // value (greater of header and data
+ // cols)
+
+ final int hw = ((Element) getElement().getLastChild())
+ .getOffsetWidth() + scrollBody.getCellExtraWidth();
+ if (columnIndex < 0) {
+ columnIndex = 0;
+ for (Iterator<Widget> it = tHead.iterator(); it
+ .hasNext(); columnIndex++) {
+ if (it.next() == this) {
+ break;
+ }
+ }
+ }
+ final int cw = scrollBody.getColWidth(columnIndex);
+ naturalWidth = (hw > cw ? hw : cw);
+ }
+ return naturalWidth;
+ }
+ }
+
+ public void setNaturalMinimumColumnWidth(int w) {
+ naturalWidth = w;
+ }
+ }
+
+ /**
+ * HeaderCell that is header cell for row headers.
+ *
+ * Reordering disabled and clicking on it resets sorting.
+ */
+ public class RowHeadersFooterCell extends FooterCell {
+
+ RowHeadersFooterCell() {
+ super(ROW_HEADER_COLUMN_KEY, "");
+ }
+
+ @Override
+ protected void handleCaptionEvent(Event event) {
+ // NOP: RowHeaders cannot be reordered
+ // TODO It'd be nice to reset sorting here
+ }
+ }
+
+ /**
+ * The footer of the table which can be seen in the bottom of the Table.
+ */
+ public class TableFooter extends Panel {
+
+ private static final int WRAPPER_WIDTH = 900000;
+
+ ArrayList<Widget> visibleCells = new ArrayList<Widget>();
+ HashMap<String, FooterCell> availableCells = new HashMap<String, FooterCell>();
+
+ Element div = DOM.createDiv();
+ Element hTableWrapper = DOM.createDiv();
+ Element hTableContainer = DOM.createDiv();
+ Element table = DOM.createTable();
+ Element headerTableBody = DOM.createTBody();
+ Element tr = DOM.createTR();
+
+ public TableFooter() {
+
+ DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden");
+ DOM.setElementProperty(hTableWrapper, "className", CLASSNAME
+ + "-footer");
+
+ DOM.appendChild(table, headerTableBody);
+ DOM.appendChild(headerTableBody, tr);
+ DOM.appendChild(hTableContainer, table);
+ DOM.appendChild(hTableWrapper, hTableContainer);
+ DOM.appendChild(div, hTableWrapper);
+ setElement(div);
+
+ setStyleName(CLASSNAME + "-footer-wrap");
+
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersFooterCell());
+ }
+
+ @Override
+ public void clear() {
+ for (String cid : availableCells.keySet()) {
+ removeCell(cid);
+ }
+ availableCells.clear();
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersFooterCell());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client
+ * .ui.Widget)
+ */
+ @Override
+ public boolean remove(Widget w) {
+ if (visibleCells.contains(w)) {
+ visibleCells.remove(w);
+ orphan(w);
+ DOM.removeChild(DOM.getParent(w.getElement()), w.getElement());
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.HasWidgets#iterator()
+ */
+ public Iterator<Widget> iterator() {
+ return visibleCells.iterator();
+ }
+
+ /**
+ * Gets a footer cell which represents the given columnId
+ *
+ * @param cid
+ * The columnId
+ *
+ * @return The cell
+ */
+ public FooterCell getFooterCell(String cid) {
+ return availableCells.get(cid);
+ }
+
+ /**
+ * Gets a footer cell by using a column index
+ *
+ * @param index
+ * The index of the column
+ * @return The Cell
+ */
+ public FooterCell getFooterCell(int index) {
+ if (index < visibleCells.size()) {
+ return (FooterCell) visibleCells.get(index);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Updates the cells contents when updateUIDL request is received
+ *
+ * @param uidl
+ * The UIDL
+ */
+ public void updateCellsFromUIDL(UIDL uidl) {
+ Iterator<?> columnIterator = uidl.getChildIterator();
+ HashSet<String> updated = new HashSet<String>();
+ while (columnIterator.hasNext()) {
+ final UIDL col = (UIDL) columnIterator.next();
+ final String cid = col.getStringAttribute("cid");
+ updated.add(cid);
+
+ String caption = col.hasAttribute("fcaption") ? col
+ .getStringAttribute("fcaption") : "";
+ FooterCell c = getFooterCell(cid);
+ if (c == null) {
+ c = new FooterCell(cid, caption);
+ availableCells.put(cid, c);
+ if (initializedAndAttached) {
+ // we will need a column width recalculation
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ } else {
+ c.setText(caption);
+ }
+
+ if (col.hasAttribute("align")) {
+ c.setAlign(col.getStringAttribute("align").charAt(0));
+ } else {
+ c.setAlign(ALIGN_LEFT);
+
+ }
+ if (col.hasAttribute("width")) {
+ if (scrollBody == null) {
+ // Already updated by setColWidth called from
+ // TableHeads.updateCellsFromUIDL in case of a server
+ // side resize
+ final String width = col.getStringAttribute("width");
+ c.setWidth(Integer.parseInt(width), true);
+ }
+ } else if (recalcWidths) {
+ c.setUndefinedWidth();
+ }
+ if (col.hasAttribute("er")) {
+ c.setExpandRatio(col.getFloatAttribute("er"));
+ }
+ if (col.hasAttribute("collapsed")) {
+ // ensure header is properly removed from parent (case when
+ // collapsing happens via servers side api)
+ if (c.isAttached()) {
+ c.removeFromParent();
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ // check for orphaned header cells
+ for (Iterator<String> cit = availableCells.keySet().iterator(); cit
+ .hasNext();) {
+ String cid = cit.next();
+ if (!updated.contains(cid)) {
+ removeCell(cid);
+ cit.remove();
+ }
+ }
+ }
+
+ /**
+ * Set a footer cell for a specified column index
+ *
+ * @param index
+ * The index
+ * @param cell
+ * The footer cell
+ */
+ public void setFooterCell(int index, FooterCell cell) {
+ if (cell.isEnabled()) {
+ // we're moving the cell
+ DOM.removeChild(tr, cell.getElement());
+ orphan(cell);
+ visibleCells.remove(cell);
+ }
+ if (index < visibleCells.size()) {
+ // insert to right slot
+ DOM.insertChild(tr, cell.getElement(), index);
+ adopt(cell);
+ visibleCells.add(index, cell);
+ } else if (index == visibleCells.size()) {
+ // simply append
+ DOM.appendChild(tr, cell.getElement());
+ adopt(cell);
+ visibleCells.add(cell);
+ } else {
+ throw new RuntimeException(
+ "Header cells must be appended in order");
+ }
+ }
+
+ /**
+ * Remove a cell by using the columnId
+ *
+ * @param colKey
+ * The columnId to remove
+ */
+ public void removeCell(String colKey) {
+ final FooterCell c = getFooterCell(colKey);
+ remove(c);
+ }
+
+ /**
+ * Enable a column (Sets the footer cell)
+ *
+ * @param cid
+ * The columnId
+ * @param index
+ * The index of the column
+ */
+ public void enableColumn(String cid, int index) {
+ final FooterCell c = getFooterCell(cid);
+ if (!c.isEnabled() || getFooterCell(index) != c) {
+ setFooterCell(index, c);
+ if (initializedAndAttached) {
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ /**
+ * Disable browser measurement of the table width
+ */
+ public void disableBrowserIntelligence() {
+ DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH
+ + "px");
+ }
+
+ /**
+ * Enable browser measurement of the table width
+ */
+ public void enableBrowserIntelligence() {
+ DOM.setStyleAttribute(hTableContainer, "width", "");
+ }
+
+ /**
+ * Set the horizontal position in the cell in the footer. This is done
+ * when a horizontal scrollbar is present.
+ *
+ * @param scrollLeft
+ * The value of the leftScroll
+ */
+ public void setHorizontalScrollPosition(int scrollLeft) {
+ hTableWrapper.setScrollLeft(scrollLeft);
+ }
+
+ /**
+ * Swap cells when the column are dragged
+ *
+ * @param oldIndex
+ * The old index of the cell
+ * @param newIndex
+ * The new index of the cell
+ */
+ public void moveCell(int oldIndex, int newIndex) {
+ final FooterCell hCell = getFooterCell(oldIndex);
+ final Element cell = hCell.getElement();
+
+ visibleCells.remove(oldIndex);
+ DOM.removeChild(tr, cell);
+
+ DOM.insertChild(tr, cell, newIndex);
+ visibleCells.add(newIndex, hCell);
+ }
+ }
+
+ /**
+ * This Panel can only contain VScrollTableRow type of widgets. This
+ * "simulates" very large table, keeping spacers which take room of
+ * unrendered rows.
+ *
+ */
+ public class VScrollTableBody extends Panel {
+
+ public static final int DEFAULT_ROW_HEIGHT = 24;
+
+ private double rowHeight = -1;
+
+ private final LinkedList<Widget> renderedRows = new LinkedList<Widget>();
+
+ /**
+ * Due some optimizations row height measuring is deferred and initial
+ * set of rows is rendered detached. Flag set on when table body has
+ * been attached in dom and rowheight has been measured.
+ */
+ private boolean tBodyMeasurementsDone = false;
+
+ Element preSpacer = DOM.createDiv();
+ Element postSpacer = DOM.createDiv();
+
+ Element container = DOM.createDiv();
+
+ TableSectionElement tBodyElement = Document.get().createTBodyElement();
+ Element table = DOM.createTable();
+
+ private int firstRendered;
+ private int lastRendered;
+
+ private char[] aligns;
+
+ protected VScrollTableBody() {
+ constructDOM();
+ setElement(container);
+ }
+
+ public VScrollTableRow getRowByRowIndex(int indexInTable) {
+ int internalIndex = indexInTable - firstRendered;
+ if (internalIndex >= 0 && internalIndex < renderedRows.size()) {
+ return (VScrollTableRow) renderedRows.get(internalIndex);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return the height of scrollable body, subpixels ceiled.
+ */
+ public int getRequiredHeight() {
+ return preSpacer.getOffsetHeight() + postSpacer.getOffsetHeight()
+ + Util.getRequiredHeight(table);
+ }
+
+ private void constructDOM() {
+ DOM.setElementProperty(table, "className", CLASSNAME + "-table");
+ if (BrowserInfo.get().isIE()) {
+ table.setPropertyInt("cellSpacing", 0);
+ }
+ DOM.setElementProperty(preSpacer, "className", CLASSNAME
+ + "-row-spacer");
+ DOM.setElementProperty(postSpacer, "className", CLASSNAME
+ + "-row-spacer");
+
+ table.appendChild(tBodyElement);
+ DOM.appendChild(container, preSpacer);
+ DOM.appendChild(container, table);
+ DOM.appendChild(container, postSpacer);
+
+ }
+
+ public int getAvailableWidth() {
+ int availW = scrollBodyPanel.getOffsetWidth() - getBorderWidth();
+ return availW;
+ }
+
+ public void renderInitialRows(UIDL rowData, int firstIndex, int rows) {
+ firstRendered = firstIndex;
+ lastRendered = firstIndex + rows - 1;
+ final Iterator<?> it = rowData.getChildIterator();
+ aligns = tHead.getColumnAlignments();
+ while (it.hasNext()) {
+ final VScrollTableRow row = createRow((UIDL) it.next(), aligns);
+ addRow(row);
+ }
+ if (isAttached()) {
+ fixSpacers();
+ }
+ }
+
+ public void renderRows(UIDL rowData, int firstIndex, int rows) {
+ // FIXME REVIEW
+ aligns = tHead.getColumnAlignments();
+ final Iterator<?> it = rowData.getChildIterator();
+ if (firstIndex == lastRendered + 1) {
+ while (it.hasNext()) {
+ final VScrollTableRow row = prepareRow((UIDL) it.next());
+ addRow(row);
+ lastRendered++;
+ }
+ fixSpacers();
+ } else if (firstIndex + rows == firstRendered) {
+ final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
+ int i = rows;
+ while (it.hasNext()) {
+ i--;
+ rowArray[i] = prepareRow((UIDL) it.next());
+ }
+ for (i = 0; i < rows; i++) {
+ addRowBeforeFirstRendered(rowArray[i]);
+ firstRendered--;
+ }
+ } else {
+ // completely new set of rows
+ while (lastRendered + 1 > firstRendered) {
+ unlinkRow(false);
+ }
+ final VScrollTableRow row = prepareRow((UIDL) it.next());
+ firstRendered = firstIndex;
+ lastRendered = firstIndex - 1;
+ addRow(row);
+ lastRendered++;
+ setContainerHeight();
+ fixSpacers();
+ while (it.hasNext()) {
+ addRow(prepareRow((UIDL) it.next()));
+ lastRendered++;
+ }
+ fixSpacers();
+ }
+
+ // this may be a new set of rows due content change,
+ // ensure we have proper cache rows
+ ensureCacheFilled();
+ }
+
+ /**
+ * Ensure we have the correct set of rows on client side, e.g. if the
+ * content on the server side has changed, or the client scroll position
+ * has changed since the last request.
+ */
+ protected void ensureCacheFilled() {
+ int reactFirstRow = (int) (firstRowInViewPort - pageLength
+ * cache_react_rate);
+ int reactLastRow = (int) (firstRowInViewPort + pageLength + pageLength
+ * cache_react_rate);
+ if (reactFirstRow < 0) {
+ reactFirstRow = 0;
+ }
+ if (reactLastRow >= totalRows) {
+ reactLastRow = totalRows - 1;
+ }
+ if (lastRendered < reactFirstRow || firstRendered > reactLastRow) {
+ /*
+ * #8040 - scroll position is completely changed since the
+ * latest request, so request a new set of rows.
+ *
+ * TODO: We should probably check whether the fetched rows match
+ * the current scroll position right when they arrive, so as to
+ * not waste time rendering a set of rows that will never be
+ * visible...
+ */
+ rowRequestHandler.setReqFirstRow(reactFirstRow);
+ rowRequestHandler.setReqRows(reactLastRow - reactFirstRow + 1);
+ rowRequestHandler.deferRowFetch(1);
+ } else if (lastRendered < reactLastRow) {
+ // get some cache rows below visible area
+ rowRequestHandler.setReqFirstRow(lastRendered + 1);
+ rowRequestHandler.setReqRows(reactLastRow - lastRendered);
+ rowRequestHandler.deferRowFetch(1);
+ } else if (firstRendered > reactFirstRow) {
+ /*
+ * Branch for fetching cache above visible area.
+ *
+ * If cache needed for both before and after visible area, this
+ * will be rendered after-cache is received and rendered. So in
+ * some rare situations the table may make two cache visits to
+ * server.
+ */
+ rowRequestHandler.setReqFirstRow(reactFirstRow);
+ rowRequestHandler.setReqRows(firstRendered - reactFirstRow);
+ rowRequestHandler.deferRowFetch(1);
+ }
+ }
+
+ /**
+ * Inserts rows as provided in the rowData starting at firstIndex.
+ *
+ * @param rowData
+ * @param firstIndex
+ * @param rows
+ * the number of rows
+ * @return a list of the rows added.
+ */
+ protected List<VScrollTableRow> insertRows(UIDL rowData,
+ int firstIndex, int rows) {
+ aligns = tHead.getColumnAlignments();
+ final Iterator<?> it = rowData.getChildIterator();
+ List<VScrollTableRow> insertedRows = new ArrayList<VScrollTableRow>();
+
+ if (firstIndex == lastRendered + 1) {
+ while (it.hasNext()) {
+ final VScrollTableRow row = prepareRow((UIDL) it.next());
+ addRow(row);
+ insertedRows.add(row);
+ lastRendered++;
+ }
+ fixSpacers();
+ } else if (firstIndex + rows == firstRendered) {
+ final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
+ int i = rows;
+ while (it.hasNext()) {
+ i--;
+ rowArray[i] = prepareRow((UIDL) it.next());
+ }
+ for (i = 0; i < rows; i++) {
+ addRowBeforeFirstRendered(rowArray[i]);
+ insertedRows.add(rowArray[i]);
+ firstRendered--;
+ }
+ } else {
+ // insert in the middle
+ int ix = firstIndex;
+ while (it.hasNext()) {
+ VScrollTableRow row = prepareRow((UIDL) it.next());
+ insertRowAt(row, ix);
+ insertedRows.add(row);
+ lastRendered++;
+ ix++;
+ }
+ fixSpacers();
+ }
+ return insertedRows;
+ }
+
+ protected List<VScrollTableRow> insertAndReindexRows(UIDL rowData,
+ int firstIndex, int rows) {
+ List<VScrollTableRow> inserted = insertRows(rowData, firstIndex,
+ rows);
+ int actualIxOfFirstRowAfterInserted = firstIndex + rows
+ - firstRendered;
+ for (int ix = actualIxOfFirstRowAfterInserted; ix < renderedRows
+ .size(); ix++) {
+ VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix);
+ r.setIndex(r.getIndex() + rows);
+ }
+ setContainerHeight();
+ return inserted;
+ }
+
+ protected void insertRowsDeleteBelow(UIDL rowData, int firstIndex,
+ int rows) {
+ unlinkAllRowsStartingAt(firstIndex);
+ insertRows(rowData, firstIndex, rows);
+ setContainerHeight();
+ }
+
+ /**
+ * This method is used to instantiate new rows for this table. It
+ * automatically sets correct widths to rows cells and assigns correct
+ * client reference for child widgets.
+ *
+ * This method can be called only after table has been initialized
+ *
+ * @param uidl
+ */
+ private VScrollTableRow prepareRow(UIDL uidl) {
+ final VScrollTableRow row = createRow(uidl, aligns);
+ row.initCellWidths();
+ return row;
+ }
+
+ protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
+ if (uidl.hasAttribute("gen_html")) {
+ // This is a generated row.
+ return new VScrollTableGeneratedRow(uidl, aligns2);
+ }
+ return new VScrollTableRow(uidl, aligns2);
+ }
+
+ private void addRowBeforeFirstRendered(VScrollTableRow row) {
+ row.setIndex(firstRendered - 1);
+ if (row.isSelected()) {
+ row.addStyleName("v-selected");
+ }
+ tBodyElement.insertBefore(row.getElement(),
+ tBodyElement.getFirstChild());
+ adopt(row);
+ renderedRows.add(0, row);
+ }
+
+ private void addRow(VScrollTableRow row) {
+ row.setIndex(firstRendered + renderedRows.size());
+ if (row.isSelected()) {
+ row.addStyleName("v-selected");
+ }
+ tBodyElement.appendChild(row.getElement());
+ adopt(row);
+ renderedRows.add(row);
+ }
+
+ private void insertRowAt(VScrollTableRow row, int index) {
+ row.setIndex(index);
+ if (row.isSelected()) {
+ row.addStyleName("v-selected");
+ }
+ if (index > 0) {
+ VScrollTableRow sibling = getRowByRowIndex(index - 1);
+ tBodyElement
+ .insertAfter(row.getElement(), sibling.getElement());
+ } else {
+ VScrollTableRow sibling = getRowByRowIndex(index);
+ tBodyElement.insertBefore(row.getElement(),
+ sibling.getElement());
+ }
+ adopt(row);
+ int actualIx = index - firstRendered;
+ renderedRows.add(actualIx, row);
+ }
+
+ public Iterator<Widget> iterator() {
+ return renderedRows.iterator();
+ }
+
+ /**
+ * @return false if couldn't remove row
+ */
+ protected boolean unlinkRow(boolean fromBeginning) {
+ if (lastRendered - firstRendered < 0) {
+ return false;
+ }
+ int actualIx;
+ if (fromBeginning) {
+ actualIx = 0;
+ firstRendered++;
+ } else {
+ actualIx = renderedRows.size() - 1;
+ lastRendered--;
+ }
+ if (actualIx >= 0) {
+ unlinkRowAtActualIndex(actualIx);
+ fixSpacers();
+ return true;
+ }
+ return false;
+ }
+
+ protected void unlinkRows(int firstIndex, int count) {
+ if (count < 1) {
+ return;
+ }
+ if (firstRendered > firstIndex
+ && firstRendered < firstIndex + count) {
+ firstIndex = firstRendered;
+ }
+ int lastIndex = firstIndex + count - 1;
+ if (lastRendered < lastIndex) {
+ lastIndex = lastRendered;
+ }
+ for (int ix = lastIndex; ix >= firstIndex; ix--) {
+ unlinkRowAtActualIndex(actualIndex(ix));
+ lastRendered--;
+ }
+ fixSpacers();
+ }
+
+ protected void unlinkAndReindexRows(int firstIndex, int count) {
+ unlinkRows(firstIndex, count);
+ int actualFirstIx = firstIndex - firstRendered;
+ for (int ix = actualFirstIx; ix < renderedRows.size(); ix++) {
+ VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix);
+ r.setIndex(r.getIndex() - count);
+ }
+ setContainerHeight();
+ }
+
+ protected void unlinkAllRowsStartingAt(int index) {
+ if (firstRendered > index) {
+ index = firstRendered;
+ }
+ for (int ix = renderedRows.size() - 1; ix >= index; ix--) {
+ unlinkRowAtActualIndex(actualIndex(ix));
+ lastRendered--;
+ }
+ fixSpacers();
+ }
+
+ private int actualIndex(int index) {
+ return index - firstRendered;
+ }
+
+ private void unlinkRowAtActualIndex(int index) {
+ final VScrollTableRow toBeRemoved = (VScrollTableRow) renderedRows
+ .get(index);
+ // Unregister row tooltip
+ client.registerTooltip(VScrollTable.this, toBeRemoved.getElement(),
+ null);
+ for (int i = 0; i < toBeRemoved.getElement().getChildCount(); i++) {
+ // Unregister cell tooltips
+ Element td = toBeRemoved.getElement().getChild(i).cast();
+ client.registerTooltip(VScrollTable.this, td, null);
+ }
+ tBodyElement.removeChild(toBeRemoved.getElement());
+ orphan(toBeRemoved);
+ renderedRows.remove(index);
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Fix container blocks height according to totalRows to avoid
+ * "bouncing" when scrolling
+ */
+ private void setContainerHeight() {
+ fixSpacers();
+ DOM.setStyleAttribute(container, "height",
+ measureRowHeightOffset(totalRows) + "px");
+ }
+
+ private void fixSpacers() {
+ int prepx = measureRowHeightOffset(firstRendered);
+ if (prepx < 0) {
+ prepx = 0;
+ }
+ preSpacer.getStyle().setPropertyPx("height", prepx);
+ int postpx = measureRowHeightOffset(totalRows - 1)
+ - measureRowHeightOffset(lastRendered);
+ if (postpx < 0) {
+ postpx = 0;
+ }
+ postSpacer.getStyle().setPropertyPx("height", postpx);
+ }
+
+ public double getRowHeight() {
+ return getRowHeight(false);
+ }
+
+ public double getRowHeight(boolean forceUpdate) {
+ if (tBodyMeasurementsDone && !forceUpdate) {
+ return rowHeight;
+ } else {
+
+ if (tBodyElement.getRows().getLength() > 0) {
+ int tableHeight = getTableHeight();
+ int rowCount = tBodyElement.getRows().getLength();
+ rowHeight = tableHeight / (double) rowCount;
+ } else {
+ if (isAttached()) {
+ // measure row height by adding a dummy row
+ VScrollTableRow scrollTableRow = new VScrollTableRow();
+ tBodyElement.appendChild(scrollTableRow.getElement());
+ getRowHeight(forceUpdate);
+ tBodyElement.removeChild(scrollTableRow.getElement());
+ } else {
+ // TODO investigate if this can never happen anymore
+ return DEFAULT_ROW_HEIGHT;
+ }
+ }
+ tBodyMeasurementsDone = true;
+ return rowHeight;
+ }
+ }
+
+ public int getTableHeight() {
+ return table.getOffsetHeight();
+ }
+
+ /**
+ * Returns the width available for column content.
+ *
+ * @param columnIndex
+ * @return
+ */
+ public int getColWidth(int columnIndex) {
+ if (tBodyMeasurementsDone) {
+ if (renderedRows.isEmpty()) {
+ // no rows yet rendered
+ return 0;
+ }
+ for (Widget row : renderedRows) {
+ if (!(row instanceof VScrollTableGeneratedRow)) {
+ TableRowElement tr = row.getElement().cast();
+ Element wrapperdiv = tr.getCells().getItem(columnIndex)
+ .getFirstChildElement().cast();
+ return wrapperdiv.getOffsetWidth();
+ }
+ }
+ return 0;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Sets the content width of a column.
+ *
+ * Due IE limitation, we must set the width to a wrapper elements inside
+ * table cells (with overflow hidden, which does not work on td
+ * elements).
+ *
+ * To get this work properly crossplatform, we will also set the width
+ * of td.
+ *
+ * @param colIndex
+ * @param w
+ */
+ public void setColWidth(int colIndex, int w) {
+ for (Widget row : renderedRows) {
+ ((VScrollTableRow) row).setCellWidth(colIndex, w);
+ }
+ }
+
+ private int cellExtraWidth = -1;
+
+ /**
+ * Method to return the space used for cell paddings + border.
+ */
+ private int getCellExtraWidth() {
+ if (cellExtraWidth < 0) {
+ detectExtrawidth();
+ }
+ return cellExtraWidth;
+ }
+
+ private void detectExtrawidth() {
+ NodeList<TableRowElement> rows = tBodyElement.getRows();
+ if (rows.getLength() == 0) {
+ /* need to temporary add empty row and detect */
+ VScrollTableRow scrollTableRow = new VScrollTableRow();
+ tBodyElement.appendChild(scrollTableRow.getElement());
+ detectExtrawidth();
+ tBodyElement.removeChild(scrollTableRow.getElement());
+ } else {
+ boolean noCells = false;
+ TableRowElement item = rows.getItem(0);
+ TableCellElement firstTD = item.getCells().getItem(0);
+ if (firstTD == null) {
+ // content is currently empty, we need to add a fake cell
+ // for measuring
+ noCells = true;
+ VScrollTableRow next = (VScrollTableRow) iterator().next();
+ boolean sorted = tHead.getHeaderCell(0) != null ? tHead
+ .getHeaderCell(0).isSorted() : false;
+ next.addCell(null, "", ALIGN_LEFT, "", true, sorted);
+ firstTD = item.getCells().getItem(0);
+ }
+ com.google.gwt.dom.client.Element wrapper = firstTD
+ .getFirstChildElement();
+ cellExtraWidth = firstTD.getOffsetWidth()
+ - wrapper.getOffsetWidth();
+ if (noCells) {
+ firstTD.getParentElement().removeChild(firstTD);
+ }
+ }
+ }
+
+ private void reLayoutComponents() {
+ for (Widget w : this) {
+ VScrollTableRow r = (VScrollTableRow) w;
+ for (Widget widget : r) {
+ client.handleComponentRelativeSize(widget);
+ }
+ }
+ }
+
+ public int getLastRendered() {
+ return lastRendered;
+ }
+
+ public int getFirstRendered() {
+ return firstRendered;
+ }
+
+ public void moveCol(int oldIndex, int newIndex) {
+
+ // loop all rows and move given index to its new place
+ final Iterator<?> rows = iterator();
+ while (rows.hasNext()) {
+ final VScrollTableRow row = (VScrollTableRow) rows.next();
+
+ final Element td = DOM.getChild(row.getElement(), oldIndex);
+ if (td != null) {
+ DOM.removeChild(row.getElement(), td);
+
+ DOM.insertChild(row.getElement(), td, newIndex);
+ }
+ }
+
+ }
+
+ /**
+ * Restore row visibility which is set to "none" when the row is
+ * rendered (due a performance optimization).
+ */
+ private void restoreRowVisibility() {
+ for (Widget row : renderedRows) {
+ row.getElement().getStyle().setProperty("visibility", "");
+ }
+ }
+
+ public class VScrollTableRow extends Panel implements ActionOwner {
+
+ private static final int TOUCHSCROLL_TIMEOUT = 70;
+ private static final int DRAGMODE_MULTIROW = 2;
+ protected ArrayList<Widget> childWidgets = new ArrayList<Widget>();
+ private boolean selected = false;
+ protected final int rowKey;
+
+ private String[] actionKeys = null;
+ private final TableRowElement rowElement;
+ private boolean mDown;
+ private int index;
+ private Event touchStart;
+ private static final String ROW_CLASSNAME_EVEN = CLASSNAME + "-row";
+ private static final String ROW_CLASSNAME_ODD = CLASSNAME
+ + "-row-odd";
+ private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500;
+ private Timer contextTouchTimeout;
+ private int touchStartY;
+ private int touchStartX;
+
+ private VScrollTableRow(int rowKey) {
+ this.rowKey = rowKey;
+ rowElement = Document.get().createTRElement();
+ setElement(rowElement);
+ DOM.sinkEvents(getElement(), Event.MOUSEEVENTS
+ | Event.TOUCHEVENTS | Event.ONDBLCLICK
+ | Event.ONCONTEXTMENU | VTooltip.TOOLTIP_EVENTS);
+ }
+
+ public VScrollTableRow(UIDL uidl, char[] aligns) {
+ this(uidl.getIntAttribute("key"));
+
+ /*
+ * Rendering the rows as hidden improves Firefox and Safari
+ * performance drastically.
+ */
+ getElement().getStyle().setProperty("visibility", "hidden");
+
+ String rowStyle = uidl.getStringAttribute("rowstyle");
+ if (rowStyle != null) {
+ addStyleName(CLASSNAME + "-row-" + rowStyle);
+ }
+
+ String rowDescription = uidl.getStringAttribute("rowdescr");
+ if (rowDescription != null && !rowDescription.equals("")) {
+ TooltipInfo info = new TooltipInfo(rowDescription);
+ client.registerTooltip(VScrollTable.this, rowElement, info);
+ } else {
+ // Remove possibly previously set tooltip
+ client.registerTooltip(VScrollTable.this, rowElement, null);
+ }
+
+ tHead.getColumnAlignments();
+ int col = 0;
+ int visibleColumnIndex = -1;
+
+ // row header
+ if (showRowHeaders) {
+ boolean sorted = tHead.getHeaderCell(col).isSorted();
+ addCell(uidl, buildCaptionHtmlSnippet(uidl), aligns[col++],
+ "rowheader", true, sorted);
+ visibleColumnIndex++;
+ }
+
+ if (uidl.hasAttribute("al")) {
+ actionKeys = uidl.getStringArrayAttribute("al");
+ }
+
+ addCellsFromUIDL(uidl, aligns, col, visibleColumnIndex);
+
+ if (uidl.hasAttribute("selected") && !isSelected()) {
+ toggleSelection();
+ }
+ }
+
+ /**
+ * Add a dummy row, used for measurements if Table is empty.
+ */
+ public VScrollTableRow() {
+ this(0);
+ addStyleName(CLASSNAME + "-row");
+ addCell(null, "_", 'b', "", true, false);
+ }
+
+ protected void initCellWidths() {
+ final int cells = tHead.getVisibleCellCount();
+ for (int i = 0; i < cells; i++) {
+ int w = VScrollTable.this.getColWidth(getColKeyByIndex(i));
+ if (w < 0) {
+ w = 0;
+ }
+ setCellWidth(i, w);
+ }
+ }
+
+ protected void setCellWidth(int cellIx, int width) {
+ final Element cell = DOM.getChild(getElement(), cellIx);
+ cell.getFirstChildElement().getStyle()
+ .setPropertyPx("width", width);
+ cell.getStyle().setPropertyPx("width", width);
+ }
+
+ protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
+ int visibleColumnIndex) {
+ final Iterator<?> cells = uidl.getChildIterator();
+ while (cells.hasNext()) {
+ final Object cell = cells.next();
+ visibleColumnIndex++;
+
+ String columnId = visibleColOrder[visibleColumnIndex];
+
+ String style = "";
+ if (uidl.hasAttribute("style-" + columnId)) {
+ style = uidl.getStringAttribute("style-" + columnId);
+ }
+
+ String description = null;
+ if (uidl.hasAttribute("descr-" + columnId)) {
+ description = uidl.getStringAttribute("descr-"
+ + columnId);
+ }
+
+ boolean sorted = tHead.getHeaderCell(col).isSorted();
+ if (cell instanceof String) {
+ addCell(uidl, cell.toString(), aligns[col++], style,
+ isRenderHtmlInCells(), sorted, description);
+ } else {
+ final ComponentConnector cellContent = client
+ .getPaintable((UIDL) cell);
+
+ addCell(uidl, cellContent.getWidget(), aligns[col++],
+ style, sorted);
+ }
+ }
+ }
+
+ /**
+ * Overriding this and returning true causes all text cells to be
+ * rendered as HTML.
+ *
+ * @return always returns false in the default implementation
+ */
+ protected boolean isRenderHtmlInCells() {
+ return false;
+ }
+
+ /**
+ * Detects whether row is visible in tables viewport.
+ *
+ * @return
+ */
+ public boolean isInViewPort() {
+ int absoluteTop = getAbsoluteTop();
+ int scrollPosition = scrollBodyPanel.getScrollPosition();
+ if (absoluteTop < scrollPosition) {
+ return false;
+ }
+ int maxVisible = scrollPosition
+ + scrollBodyPanel.getOffsetHeight() - getOffsetHeight();
+ if (absoluteTop > maxVisible) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Makes a check based on indexes whether the row is before the
+ * compared row.
+ *
+ * @param row1
+ * @return true if this rows index is smaller than in the row1
+ */
+ public boolean isBefore(VScrollTableRow row1) {
+ return getIndex() < row1.getIndex();
+ }
+
+ /**
+ * Sets the index of the row in the whole table. Currently used just
+ * to set even/odd classname
+ *
+ * @param indexInWholeTable
+ */
+ private void setIndex(int indexInWholeTable) {
+ index = indexInWholeTable;
+ boolean isOdd = indexInWholeTable % 2 == 0;
+ // Inverted logic to be backwards compatible with earlier 6.4.
+ // It is very strange because rows 1,3,5 are considered "even"
+ // and 2,4,6 "odd".
+ //
+ // First remove any old styles so that both styles aren't
+ // applied when indexes are updated.
+ removeStyleName(ROW_CLASSNAME_ODD);
+ removeStyleName(ROW_CLASSNAME_EVEN);
+ if (!isOdd) {
+ addStyleName(ROW_CLASSNAME_ODD);
+ } else {
+ addStyleName(ROW_CLASSNAME_EVEN);
+ }
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ client.getContextMenu().ensureHidden(this);
+ }
+
+ public String getKey() {
+ return String.valueOf(rowKey);
+ }
+
+ public void addCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean sorted) {
+ addCell(rowUidl, text, align, style, textIsHTML, sorted, null);
+ }
+
+ public void addCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean sorted,
+ String description) {
+ // String only content is optimized by not using Label widget
+ final TableCellElement td = DOM.createTD().cast();
+ initCellWithText(text, align, style, textIsHTML, sorted,
+ description, td);
+ }
+
+ protected void initCellWithText(String text, char align,
+ String style, boolean textIsHTML, boolean sorted,
+ String description, final TableCellElement td) {
+ final Element container = DOM.createDiv();
+ String className = CLASSNAME + "-cell-content";
+ if (style != null && !style.equals("")) {
+ className += " " + CLASSNAME + "-cell-content-" + style;
+ }
+ if (sorted) {
+ className += " " + CLASSNAME + "-cell-content-sorted";
+ }
+ td.setClassName(className);
+ container.setClassName(CLASSNAME + "-cell-wrapper");
+ if (textIsHTML) {
+ container.setInnerHTML(text);
+ } else {
+ container.setInnerText(text);
+ }
+ if (align != ALIGN_LEFT) {
+ switch (align) {
+ case ALIGN_CENTER:
+ container.getStyle().setProperty("textAlign", "center");
+ break;
+ case ALIGN_RIGHT:
+ default:
+ container.getStyle().setProperty("textAlign", "right");
+ break;
+ }
+ }
+
+ if (description != null && !description.equals("")) {
+ TooltipInfo info = new TooltipInfo(description);
+ client.registerTooltip(VScrollTable.this, td, info);
+ } else {
+ // Remove possibly previously set tooltip
+ client.registerTooltip(VScrollTable.this, td, null);
+ }
+
+ td.appendChild(container);
+ getElement().appendChild(td);
+ }
+
+ public void addCell(UIDL rowUidl, Widget w, char align,
+ String style, boolean sorted) {
+ final TableCellElement td = DOM.createTD().cast();
+ initCellWithWidget(w, align, style, sorted, td);
+ }
+
+ protected void initCellWithWidget(Widget w, char align,
+ String style, boolean sorted, final TableCellElement td) {
+ final Element container = DOM.createDiv();
+ String className = CLASSNAME + "-cell-content";
+ if (style != null && !style.equals("")) {
+ className += " " + CLASSNAME + "-cell-content-" + style;
+ }
+ if (sorted) {
+ className += " " + CLASSNAME + "-cell-content-sorted";
+ }
+ td.setClassName(className);
+ container.setClassName(CLASSNAME + "-cell-wrapper");
+ // TODO most components work with this, but not all (e.g.
+ // Select)
+ // Old comment: make widget cells respect align.
+ // text-align:center for IE, margin: auto for others
+ if (align != ALIGN_LEFT) {
+ switch (align) {
+ case ALIGN_CENTER:
+ container.getStyle().setProperty("textAlign", "center");
+ break;
+ case ALIGN_RIGHT:
+ default:
+ container.getStyle().setProperty("textAlign", "right");
+ break;
+ }
+ }
+ td.appendChild(container);
+ getElement().appendChild(td);
+ // ensure widget not attached to another element (possible tBody
+ // change)
+ w.removeFromParent();
+ container.appendChild(w.getElement());
+ adopt(w);
+ childWidgets.add(w);
+ }
+
+ public Iterator<Widget> iterator() {
+ return childWidgets.iterator();
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ if (childWidgets.contains(w)) {
+ orphan(w);
+ DOM.removeChild(DOM.getParent(w.getElement()),
+ w.getElement());
+ childWidgets.remove(w);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * If there are registered click listeners, sends a click event and
+ * returns true. Otherwise, does nothing and returns false.
+ *
+ * @param event
+ * @param targetTdOrTr
+ * @param immediate
+ * Whether the event is sent immediately
+ * @return Whether a click event was sent
+ */
+ private boolean handleClickEvent(Event event, Element targetTdOrTr,
+ boolean immediate) {
+ if (!client.hasEventListeners(VScrollTable.this,
+ ITEM_CLICK_EVENT_ID)) {
+ // Don't send an event if nobody is listening
+ return false;
+ }
+
+ // This row was clicked
+ client.updateVariable(paintableId, "clickedKey", "" + rowKey,
+ false);
+
+ if (getElement() == targetTdOrTr.getParentElement()) {
+ // A specific column was clicked
+ int childIndex = DOM.getChildIndex(getElement(),
+ targetTdOrTr);
+ String colKey = null;
+ colKey = tHead.getHeaderCell(childIndex).getColKey();
+ client.updateVariable(paintableId, "clickedColKey", colKey,
+ false);
+ }
+
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event);
+
+ client.updateVariable(paintableId, "clickEvent",
+ details.toString(), immediate);
+
+ return true;
+ }
+
+ private void handleTooltips(final Event event, Element target) {
+ if (target.hasTagName("TD")) {
+ // Table cell (td)
+ Element container = target.getFirstChildElement().cast();
+ Element widget = container.getFirstChildElement().cast();
+
+ boolean containsWidget = false;
+ for (Widget w : childWidgets) {
+ if (widget == w.getElement()) {
+ containsWidget = true;
+ break;
+ }
+ }
+
+ if (!containsWidget) {
+ // Only text nodes has tooltips
+ if (ConnectorMap.get(client).getWidgetTooltipInfo(
+ VScrollTable.this, target) != null) {
+ // Cell has description, use it
+ client.handleTooltipEvent(event, VScrollTable.this,
+ target);
+ } else {
+ // Cell might have row description, use row
+ // description
+ client.handleTooltipEvent(event, VScrollTable.this,
+ target.getParentElement());
+ }
+ }
+
+ } else {
+ // Table row (tr)
+ client.handleTooltipEvent(event, VScrollTable.this, target);
+ }
+ }
+
+ /*
+ * React on click that occur on content cells only
+ */
+ @Override
+ public void onBrowserEvent(final Event event) {
+ if (enabled) {
+ final int type = event.getTypeInt();
+ final Element targetTdOrTr = getEventTargetTdOrTr(event);
+ if (type == Event.ONCONTEXTMENU) {
+ showContextMenu(event);
+ if (enabled
+ && (actionKeys != null || client
+ .hasEventListeners(VScrollTable.this,
+ ITEM_CLICK_EVENT_ID))) {
+ /*
+ * Prevent browser context menu only if there are
+ * action handlers or item click listeners
+ * registered
+ */
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ return;
+ }
+
+ boolean targetCellOrRowFound = targetTdOrTr != null;
+ if (targetCellOrRowFound) {
+ handleTooltips(event, targetTdOrTr);
+ }
+
+ switch (type) {
+ case Event.ONDBLCLICK:
+ if (targetCellOrRowFound) {
+ handleClickEvent(event, targetTdOrTr, true);
+ }
+ break;
+ case Event.ONMOUSEUP:
+ if (targetCellOrRowFound) {
+ mDown = false;
+ /*
+ * Queue here, send at the same time as the
+ * corresponding value change event - see #7127
+ */
+ boolean clickEventSent = handleClickEvent(event,
+ targetTdOrTr, false);
+
+ if (event.getButton() == Event.BUTTON_LEFT
+ && isSelectable()) {
+
+ // Ctrl+Shift click
+ if ((event.getCtrlKey() || event.getMetaKey())
+ && event.getShiftKey()
+ && isMultiSelectModeDefault()) {
+ toggleShiftSelection(false);
+ setRowFocus(this);
+
+ // Ctrl click
+ } else if ((event.getCtrlKey() || event
+ .getMetaKey())
+ && isMultiSelectModeDefault()) {
+ boolean wasSelected = isSelected();
+ toggleSelection();
+ setRowFocus(this);
+ /*
+ * next possible range select must start on
+ * this row
+ */
+ selectionRangeStart = this;
+ if (wasSelected) {
+ removeRowFromUnsentSelectionRanges(this);
+ }
+
+ } else if ((event.getCtrlKey() || event
+ .getMetaKey()) && isSingleSelectMode()) {
+ // Ctrl (or meta) click (Single selection)
+ if (!isSelected()
+ || (isSelected() && nullSelectionAllowed)) {
+
+ if (!isSelected()) {
+ deselectAll();
+ }
+
+ toggleSelection();
+ setRowFocus(this);
+ }
+
+ } else if (event.getShiftKey()
+ && isMultiSelectModeDefault()) {
+ // Shift click
+ toggleShiftSelection(true);
+
+ } else {
+ // click
+ boolean currentlyJustThisRowSelected = selectedRowKeys
+ .size() == 1
+ && selectedRowKeys
+ .contains(getKey());
+
+ if (!currentlyJustThisRowSelected) {
+ if (isSingleSelectMode()
+ || isMultiSelectModeDefault()) {
+ /*
+ * For default multi select mode
+ * (ctrl/shift) and for single
+ * select mode we need to clear the
+ * previous selection before
+ * selecting a new one when the user
+ * clicks on a row. Only in
+ * multiselect/simple mode the old
+ * selection should remain after a
+ * normal click.
+ */
+ deselectAll();
+ }
+ toggleSelection();
+ } else if ((isSingleSelectMode() || isMultiSelectModeSimple())
+ && nullSelectionAllowed) {
+ toggleSelection();
+ }/*
+ * else NOP to avoid excessive server
+ * visits (selection is removed with
+ * CTRL/META click)
+ */
+
+ selectionRangeStart = this;
+ setRowFocus(this);
+ }
+
+ // Remove IE text selection hack
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast())
+ .setPropertyJSO("onselectstart",
+ null);
+ }
+ // Queue value change
+ sendSelectedRows(false);
+ }
+ /*
+ * Send queued click and value change events if any
+ * If a click event is sent, send value change with
+ * it regardless of the immediate flag, see #7127
+ */
+ if (immediate || clickEventSent) {
+ client.sendPendingVariableChanges();
+ }
+ }
+ break;
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ if (touchStart != null) {
+ /*
+ * Touch has not been handled as neither context or
+ * drag start, handle it as a click.
+ */
+ Util.simulateClickFromTouchEvent(touchStart, this);
+ touchStart = null;
+ }
+ if (contextTouchTimeout != null) {
+ contextTouchTimeout.cancel();
+ }
+ break;
+ case Event.ONTOUCHMOVE:
+ if (isSignificantMove(event)) {
+ /*
+ * TODO figure out scroll delegate don't eat events
+ * if row is selected. Null check for active
+ * delegate is as a workaround.
+ */
+ if (dragmode != 0
+ && touchStart != null
+ && (TouchScrollDelegate
+ .getActiveScrollDelegate() == null)) {
+ startRowDrag(touchStart, type, targetTdOrTr);
+ }
+ if (contextTouchTimeout != null) {
+ contextTouchTimeout.cancel();
+ }
+ /*
+ * Avoid clicks and drags by clearing touch start
+ * flag.
+ */
+ touchStart = null;
+ }
+
+ break;
+ case Event.ONTOUCHSTART:
+ touchStart = event;
+ Touch touch = event.getChangedTouches().get(0);
+ // save position to fields, touches in events are same
+ // isntance during the operation.
+ touchStartX = touch.getClientX();
+ touchStartY = touch.getClientY();
+ /*
+ * Prevent simulated mouse events.
+ */
+ touchStart.preventDefault();
+ if (dragmode != 0 || actionKeys != null) {
+ new Timer() {
+ @Override
+ public void run() {
+ TouchScrollDelegate activeScrollDelegate = TouchScrollDelegate
+ .getActiveScrollDelegate();
+ if (activeScrollDelegate != null
+ && !activeScrollDelegate.isMoved()) {
+ /*
+ * scrolling hasn't started. Cancel
+ * scrolling and let row handle this as
+ * drag start or context menu.
+ */
+ activeScrollDelegate.stopScrolling();
+ } else {
+ /*
+ * Scrolled or scrolling, clear touch
+ * start to indicate that row shouldn't
+ * handle touch move/end events.
+ */
+ touchStart = null;
+ }
+ }
+ }.schedule(TOUCHSCROLL_TIMEOUT);
+
+ if (contextTouchTimeout == null
+ && actionKeys != null) {
+ contextTouchTimeout = new Timer() {
+ @Override
+ public void run() {
+ if (touchStart != null) {
+ showContextMenu(touchStart);
+ touchStart = null;
+ }
+ }
+ };
+ }
+ contextTouchTimeout.cancel();
+ contextTouchTimeout
+ .schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
+ }
+ break;
+ case Event.ONMOUSEDOWN:
+ if (targetCellOrRowFound) {
+ setRowFocus(this);
+ ensureFocus();
+ if (dragmode != 0
+ && (event.getButton() == NativeEvent.BUTTON_LEFT)) {
+ startRowDrag(event, type, targetTdOrTr);
+
+ } else if (event.getCtrlKey()
+ || event.getShiftKey()
+ || event.getMetaKey()
+ && isMultiSelectModeDefault()) {
+
+ // Prevent default text selection in Firefox
+ event.preventDefault();
+
+ // Prevent default text selection in IE
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast())
+ .setPropertyJSO(
+ "onselectstart",
+ getPreventTextSelectionIEHack());
+ }
+
+ event.stopPropagation();
+ }
+ }
+ break;
+ case Event.ONMOUSEOUT:
+ if (targetCellOrRowFound) {
+ mDown = false;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ super.onBrowserEvent(event);
+ }
+
+ private boolean isSignificantMove(Event event) {
+ if (touchStart == null) {
+ // no touch start
+ return false;
+ }
+ /*
+ * TODO calculate based on real distance instead of separate
+ * axis checks
+ */
+ Touch touch = event.getChangedTouches().get(0);
+ if (Math.abs(touch.getClientX() - touchStartX) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
+ return true;
+ }
+ if (Math.abs(touch.getClientY() - touchStartY) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
+ return true;
+ }
+ return false;
+ }
+
+ protected void startRowDrag(Event event, final int type,
+ Element targetTdOrTr) {
+ mDown = true;
+ VTransferable transferable = new VTransferable();
+ transferable.setDragSource(ConnectorMap.get(client)
+ .getConnector(VScrollTable.this));
+ transferable.setData("itemId", "" + rowKey);
+ NodeList<TableCellElement> cells = rowElement.getCells();
+ for (int i = 0; i < cells.getLength(); i++) {
+ if (cells.getItem(i).isOrHasChild(targetTdOrTr)) {
+ HeaderCell headerCell = tHead.getHeaderCell(i);
+ transferable.setData("propertyId", headerCell.cid);
+ break;
+ }
+ }
+
+ VDragEvent ev = VDragAndDropManager.get().startDrag(
+ transferable, event, true);
+ if (dragmode == DRAGMODE_MULTIROW && isMultiSelectModeAny()
+ && selectedRowKeys.contains("" + rowKey)) {
+ ev.createDragImage(
+ (Element) scrollBody.tBodyElement.cast(), true);
+ Element dragImage = ev.getDragImage();
+ int i = 0;
+ for (Iterator<Widget> iterator = scrollBody.iterator(); iterator
+ .hasNext();) {
+ VScrollTableRow next = (VScrollTableRow) iterator
+ .next();
+ Element child = (Element) dragImage.getChild(i++);
+ if (!selectedRowKeys.contains("" + next.rowKey)) {
+ child.getStyle().setVisibility(Visibility.HIDDEN);
+ }
+ }
+ } else {
+ ev.createDragImage(getElement(), true);
+ }
+ if (type == Event.ONMOUSEDOWN) {
+ event.preventDefault();
+ }
+ event.stopPropagation();
+ }
+
+ /**
+ * Finds the TD that the event interacts with. Returns null if the
+ * target of the event should not be handled. If the event target is
+ * the row directly this method returns the TR element instead of
+ * the TD.
+ *
+ * @param event
+ * @return TD or TR element that the event targets (the actual event
+ * target is this element or a child of it)
+ */
+ private Element getEventTargetTdOrTr(Event event) {
+ final Element eventTarget = event.getEventTarget().cast();
+ Widget widget = Util.findWidget(eventTarget, null);
+ final Element thisTrElement = getElement();
+
+ if (widget != this) {
+ /*
+ * This is a workaround to make Labels, read only TextFields
+ * and Embedded in a Table clickable (see #2688). It is
+ * really not a fix as it does not work with a custom read
+ * only components (not extending VLabel/VEmbedded).
+ */
+ while (widget != null && widget.getParent() != this) {
+ widget = widget.getParent();
+ }
+
+ if (!(widget instanceof VLabel)
+ && !(widget instanceof VEmbedded)
+ && !(widget instanceof VTextField && ((VTextField) widget)
+ .isReadOnly())) {
+ return null;
+ }
+ }
+ if (eventTarget == thisTrElement) {
+ // This was a click on the TR element
+ return thisTrElement;
+ }
+
+ // Iterate upwards until we find the TR element
+ Element element = eventTarget;
+ while (element != null
+ && element.getParentElement().cast() != thisTrElement) {
+ element = element.getParentElement().cast();
+ }
+ return element;
+ }
+
+ public void showContextMenu(Event event) {
+ if (enabled && actionKeys != null) {
+ // Show context menu if there are registered action handlers
+ int left = Util.getTouchOrMouseClientX(event);
+ int top = Util.getTouchOrMouseClientY(event);
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ contextMenu = new ContextMenuDetails(getKey(), left, top);
+ client.getContextMenu().showAt(this, left, top);
+ }
+ }
+
+ /**
+ * Has the row been selected?
+ *
+ * @return Returns true if selected, else false
+ */
+ public boolean isSelected() {
+ return selected;
+ }
+
+ /**
+ * Toggle the selection of the row
+ */
+ public void toggleSelection() {
+ selected = !selected;
+ selectionChanged = true;
+ if (selected) {
+ selectedRowKeys.add(String.valueOf(rowKey));
+ addStyleName("v-selected");
+ } else {
+ removeStyleName("v-selected");
+ selectedRowKeys.remove(String.valueOf(rowKey));
+ }
+ }
+
+ /**
+ * Is called when a user clicks an item when holding SHIFT key down.
+ * This will select a new range from the last focused row
+ *
+ * @param deselectPrevious
+ * Should the previous selected range be deselected
+ */
+ private void toggleShiftSelection(boolean deselectPrevious) {
+
+ /*
+ * Ensures that we are in multiselect mode and that we have a
+ * previous selection which was not a deselection
+ */
+ if (isSingleSelectMode()) {
+ // No previous selection found
+ deselectAll();
+ toggleSelection();
+ return;
+ }
+
+ // Set the selectable range
+ VScrollTableRow endRow = this;
+ VScrollTableRow startRow = selectionRangeStart;
+ if (startRow == null) {
+ startRow = focusedRow;
+ // If start row is null then we have a multipage selection
+ // from
+ // above
+ if (startRow == null) {
+ startRow = (VScrollTableRow) scrollBody.iterator()
+ .next();
+ setRowFocus(endRow);
+ }
+ }
+ // Deselect previous items if so desired
+ if (deselectPrevious) {
+ deselectAll();
+ }
+
+ // we'll ensure GUI state from top down even though selection
+ // was the opposite way
+ if (!startRow.isBefore(endRow)) {
+ VScrollTableRow tmp = startRow;
+ startRow = endRow;
+ endRow = tmp;
+ }
+ SelectionRange range = new SelectionRange(startRow, endRow);
+
+ for (Widget w : scrollBody) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (range.inRange(row)) {
+ if (!row.isSelected()) {
+ row.toggleSelection();
+ }
+ selectedRowKeys.add(row.getKey());
+ }
+ }
+
+ // Add range
+ if (startRow != endRow) {
+ selectedRowRanges.add(range);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.ui.IActionOwner#getActions ()
+ */
+ public Action[] getActions() {
+ if (actionKeys == null) {
+ return new Action[] {};
+ }
+ final Action[] actions = new Action[actionKeys.length];
+ for (int i = 0; i < actions.length; i++) {
+ final String actionKey = actionKeys[i];
+ final TreeAction a = new TreeAction(this,
+ String.valueOf(rowKey), actionKey) {
+ @Override
+ public void execute() {
+ super.execute();
+ lazyRevertFocusToRow(VScrollTableRow.this);
+ }
+ };
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ private int getColIndexOf(Widget child) {
+ com.google.gwt.dom.client.Element widgetCell = child
+ .getElement().getParentElement().getParentElement();
+ NodeList<TableCellElement> cells = rowElement.getCells();
+ for (int i = 0; i < cells.getLength(); i++) {
+ if (cells.getItem(i) == widgetCell) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public Widget getWidgetForPaintable() {
+ return this;
+ }
+ }
+
+ protected class VScrollTableGeneratedRow extends VScrollTableRow {
+
+ private boolean spanColumns;
+ private boolean htmlContentAllowed;
+
+ public VScrollTableGeneratedRow(UIDL uidl, char[] aligns) {
+ super(uidl, aligns);
+ addStyleName("v-table-generated-row");
+ }
+
+ public boolean isSpanColumns() {
+ return spanColumns;
+ }
+
+ @Override
+ protected void initCellWidths() {
+ if (spanColumns) {
+ setSpannedColumnWidthAfterDOMFullyInited();
+ } else {
+ super.initCellWidths();
+ }
+ }
+
+ private void setSpannedColumnWidthAfterDOMFullyInited() {
+ // Defer setting width on spanned columns to make sure that
+ // they are added to the DOM before trying to calculate
+ // widths.
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ public void execute() {
+ if (showRowHeaders) {
+ setCellWidth(0, tHead.getHeaderCell(0).getWidth());
+ calcAndSetSpanWidthOnCell(1);
+ } else {
+ calcAndSetSpanWidthOnCell(0);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected boolean isRenderHtmlInCells() {
+ return htmlContentAllowed;
+ }
+
+ @Override
+ protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
+ int visibleColumnIndex) {
+ htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
+ spanColumns = uidl.getBooleanAttribute("gen_span");
+
+ final Iterator<?> cells = uidl.getChildIterator();
+ if (spanColumns) {
+ int colCount = uidl.getChildCount();
+ if (cells.hasNext()) {
+ final Object cell = cells.next();
+ if (cell instanceof String) {
+ addSpannedCell(uidl, cell.toString(), aligns[0],
+ "", htmlContentAllowed, false, null,
+ colCount);
+ } else {
+ addSpannedCell(uidl, (Widget) cell, aligns[0], "",
+ false, colCount);
+ }
+ }
+ } else {
+ super.addCellsFromUIDL(uidl, aligns, col,
+ visibleColumnIndex);
+ }
+ }
+
+ private void addSpannedCell(UIDL rowUidl, Widget w, char align,
+ String style, boolean sorted, int colCount) {
+ TableCellElement td = DOM.createTD().cast();
+ td.setColSpan(colCount);
+ initCellWithWidget(w, align, style, sorted, td);
+ }
+
+ private void addSpannedCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean sorted,
+ String description, int colCount) {
+ // String only content is optimized by not using Label widget
+ final TableCellElement td = DOM.createTD().cast();
+ td.setColSpan(colCount);
+ initCellWithText(text, align, style, textIsHTML, sorted,
+ description, td);
+ }
+
+ @Override
+ protected void setCellWidth(int cellIx, int width) {
+ if (isSpanColumns()) {
+ if (showRowHeaders) {
+ if (cellIx == 0) {
+ super.setCellWidth(0, width);
+ } else {
+ // We need to recalculate the spanning TDs width for
+ // every cellIx in order to support column resizing.
+ calcAndSetSpanWidthOnCell(1);
+ }
+ } else {
+ // Same as above.
+ calcAndSetSpanWidthOnCell(0);
+ }
+ } else {
+ super.setCellWidth(cellIx, width);
+ }
+ }
+
+ private void calcAndSetSpanWidthOnCell(final int cellIx) {
+ int spanWidth = 0;
+ for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
+ .getVisibleCellCount(); ix++) {
+ spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
+ }
+ Util.setWidthExcludingPaddingAndBorder((Element) getElement()
+ .getChild(cellIx), spanWidth, 13, false);
+ }
+ }
+
+ /**
+ * Ensure the component has a focus.
+ *
+ * TODO the current implementation simply always calls focus for the
+ * component. In case the Table at some point implements focus/blur
+ * listeners, this method needs to be evolved to conditionally call
+ * focus only if not currently focused.
+ */
+ protected void ensureFocus() {
+ if (!hasFocus) {
+ scrollBodyPanel.setFocus(true);
+ }
+
+ }
+
+ }
+
+ /**
+ * Deselects all items
+ */
+ public void deselectAll() {
+ for (Widget w : scrollBody) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (row.isSelected()) {
+ row.toggleSelection();
+ }
+ }
+ // still ensure all selects are removed from (not necessary rendered)
+ selectedRowKeys.clear();
+ selectedRowRanges.clear();
+ // also notify server that it clears all previous selections (the client
+ // side does not know about the invisible ones)
+ instructServerToForgetPreviousSelections();
+ }
+
+ /**
+ * Used in multiselect mode when the client side knows that all selections
+ * are in the next request.
+ */
+ private void instructServerToForgetPreviousSelections() {
+ client.updateVariable(paintableId, "clearSelections", true, false);
+ }
+
+ /**
+ * Determines the pagelength when the table height is fixed.
+ */
+ public void updatePageLength() {
+ // Only update if visible and enabled
+ if (!isVisible() || !enabled) {
+ return;
+ }
+
+ if (scrollBody == null) {
+ return;
+ }
+
+ if (isDynamicHeight()) {
+ return;
+ }
+
+ int rowHeight = (int) Math.round(scrollBody.getRowHeight());
+ int bodyH = scrollBodyPanel.getOffsetHeight();
+ int rowsAtOnce = bodyH / rowHeight;
+ boolean anotherPartlyVisible = ((bodyH % rowHeight) != 0);
+ if (anotherPartlyVisible) {
+ rowsAtOnce++;
+ }
+ if (pageLength != rowsAtOnce) {
+ pageLength = rowsAtOnce;
+ client.updateVariable(paintableId, "pagelength", pageLength, false);
+
+ if (!rendering) {
+ int currentlyVisible = scrollBody.lastRendered
+ - scrollBody.firstRendered;
+ if (currentlyVisible < pageLength
+ && currentlyVisible < totalRows) {
+ // shake scrollpanel to fill empty space
+ scrollBodyPanel.setScrollPosition(scrollTop + 1);
+ scrollBodyPanel.setScrollPosition(scrollTop - 1);
+ }
+
+ sizeNeedsInit = true;
+ }
+ }
+
+ }
+
+ void updateWidth() {
+ if (!isVisible()) {
+ /*
+ * Do not update size when the table is hidden as all column widths
+ * will be set to zero and they won't be recalculated when the table
+ * is set visible again (until the size changes again)
+ */
+ return;
+ }
+
+ if (!isDynamicWidth()) {
+ int innerPixels = getOffsetWidth() - getBorderWidth();
+ if (innerPixels < 0) {
+ innerPixels = 0;
+ }
+ setContentWidth(innerPixels);
+
+ // readjust undefined width columns
+ triggerLazyColumnAdjustment(false);
+
+ } else {
+
+ sizeNeedsInit = true;
+
+ // readjust undefined width columns
+ triggerLazyColumnAdjustment(false);
+ }
+
+ /*
+ * setting width may affect wheter the component has scrollbars -> needs
+ * scrolling or not
+ */
+ setProperTabIndex();
+ }
+
+ private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300;
+
+ private final Timer lazyAdjustColumnWidths = new Timer() {
+ /**
+ * Check for column widths, and available width, to see if we can fix
+ * column widths "optimally". Doing this lazily to avoid expensive
+ * calculation when resizing is not yet finished.
+ */
+ @Override
+ public void run() {
+ if (scrollBody == null) {
+ // Try again later if we get here before scrollBody has been
+ // initalized
+ triggerLazyColumnAdjustment(false);
+ return;
+ }
+
+ Iterator<Widget> headCells = tHead.iterator();
+ int usedMinimumWidth = 0;
+ int totalExplicitColumnsWidths = 0;
+ float expandRatioDivider = 0;
+ int colIndex = 0;
+ while (headCells.hasNext()) {
+ final HeaderCell hCell = (HeaderCell) headCells.next();
+ if (hCell.isDefinedWidth()) {
+ totalExplicitColumnsWidths += hCell.getWidth();
+ usedMinimumWidth += hCell.getWidth();
+ } else {
+ usedMinimumWidth += hCell.getNaturalColumnWidth(colIndex);
+ expandRatioDivider += hCell.getExpandRatio();
+ }
+ colIndex++;
+ }
+
+ int availW = scrollBody.getAvailableWidth();
+ // Hey IE, are you really sure about this?
+ availW = scrollBody.getAvailableWidth();
+ int visibleCellCount = tHead.getVisibleCellCount();
+ availW -= scrollBody.getCellExtraWidth() * visibleCellCount;
+ if (willHaveScrollbars()) {
+ availW -= Util.getNativeScrollbarSize();
+ }
+
+ int extraSpace = availW - usedMinimumWidth;
+ if (extraSpace < 0) {
+ extraSpace = 0;
+ }
+
+ int totalUndefinedNaturalWidths = usedMinimumWidth
+ - totalExplicitColumnsWidths;
+
+ // we have some space that can be divided optimally
+ HeaderCell hCell;
+ colIndex = 0;
+ headCells = tHead.iterator();
+ int checksum = 0;
+ while (headCells.hasNext()) {
+ hCell = (HeaderCell) headCells.next();
+ if (!hCell.isDefinedWidth()) {
+ int w = hCell.getNaturalColumnWidth(colIndex);
+ int newSpace;
+ if (expandRatioDivider > 0) {
+ // divide excess space by expand ratios
+ newSpace = Math.round((w + extraSpace
+ * hCell.getExpandRatio() / expandRatioDivider));
+ } else {
+ if (totalUndefinedNaturalWidths != 0) {
+ // divide relatively to natural column widths
+ newSpace = Math.round(w + (float) extraSpace
+ * (float) w / totalUndefinedNaturalWidths);
+ } else {
+ newSpace = w;
+ }
+ }
+ checksum += newSpace;
+ setColWidth(colIndex, newSpace, false);
+ } else {
+ checksum += hCell.getWidth();
+ }
+ colIndex++;
+ }
+
+ if (extraSpace > 0 && checksum != availW) {
+ /*
+ * There might be in some cases a rounding error of 1px when
+ * extra space is divided so if there is one then we give the
+ * first undefined column 1 more pixel
+ */
+ headCells = tHead.iterator();
+ colIndex = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hc = (HeaderCell) headCells.next();
+ if (!hc.isDefinedWidth()) {
+ setColWidth(colIndex,
+ hc.getWidth() + availW - checksum, false);
+ break;
+ }
+ colIndex++;
+ }
+ }
+
+ if (isDynamicHeight() && totalRows == pageLength) {
+ // fix body height (may vary if lazy loading is offhorizontal
+ // scrollbar appears/disappears)
+ int bodyHeight = scrollBody.getRequiredHeight();
+ boolean needsSpaceForHorizontalScrollbar = (availW < usedMinimumWidth);
+ if (needsSpaceForHorizontalScrollbar) {
+ bodyHeight += Util.getNativeScrollbarSize();
+ }
+ int heightBefore = getOffsetHeight();
+ scrollBodyPanel.setHeight(bodyHeight + "px");
+ if (heightBefore != getOffsetHeight()) {
+ Util.notifyParentOfSizeChange(VScrollTable.this, false);
+ }
+ }
+ scrollBody.reLayoutComponents();
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ }
+ });
+
+ forceRealignColumnHeaders();
+ }
+
+ };
+
+ private void forceRealignColumnHeaders() {
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * IE does not fire onscroll event if scroll position is reverted to
+ * 0 due to the content element size growth. Ensure headers are in
+ * sync with content manually. Safe to use null event as we don't
+ * actually use the event object in listener.
+ */
+ onScroll(null);
+ }
+ }
+
+ /**
+ * helper to set pixel size of head and body part
+ *
+ * @param pixels
+ */
+ private void setContentWidth(int pixels) {
+ tHead.setWidth(pixels + "px");
+ scrollBodyPanel.setWidth(pixels + "px");
+ tFoot.setWidth(pixels + "px");
+ }
+
+ private int borderWidth = -1;
+
+ /**
+ * @return border left + border right
+ */
+ private int getBorderWidth() {
+ if (borderWidth < 0) {
+ borderWidth = Util.measureHorizontalPaddingAndBorder(
+ scrollBodyPanel.getElement(), 2);
+ if (borderWidth < 0) {
+ borderWidth = 0;
+ }
+ }
+ return borderWidth;
+ }
+
+ /**
+ * Ensures scrollable area is properly sized. This method is used when fixed
+ * size is used.
+ */
+ private int containerHeight;
+
+ private void setContainerHeight() {
+ if (!isDynamicHeight()) {
+ containerHeight = getOffsetHeight();
+ containerHeight -= showColHeaders ? tHead.getOffsetHeight() : 0;
+ containerHeight -= tFoot.getOffsetHeight();
+ containerHeight -= getContentAreaBorderHeight();
+ if (containerHeight < 0) {
+ containerHeight = 0;
+ }
+ scrollBodyPanel.setHeight(containerHeight + "px");
+ }
+ }
+
+ private int contentAreaBorderHeight = -1;
+ private int scrollLeft;
+ private int scrollTop;
+ VScrollTableDropHandler dropHandler;
+ private boolean navKeyDown;
+ boolean multiselectPending;
+
+ /**
+ * @return border top + border bottom of the scrollable area of table
+ */
+ private int getContentAreaBorderHeight() {
+ if (contentAreaBorderHeight < 0) {
+
+ DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow",
+ "hidden");
+ int oh = scrollBodyPanel.getOffsetHeight();
+ int ch = scrollBodyPanel.getElement()
+ .getPropertyInt("clientHeight");
+ contentAreaBorderHeight = oh - ch;
+ DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow",
+ "auto");
+ }
+ return contentAreaBorderHeight;
+ }
+
+ @Override
+ public void setHeight(String height) {
+ if (height.length() == 0
+ && getElement().getStyle().getHeight().length() != 0) {
+ /*
+ * Changing from defined to undefined size -> should do a size init
+ * to take page length into account again
+ */
+ sizeNeedsInit = true;
+ }
+ super.setHeight(height);
+ }
+
+ void updateHeight() {
+ setContainerHeight();
+
+ updatePageLength();
+
+ if (!rendering) {
+ // Webkit may sometimes get an odd rendering bug (white space
+ // between header and body), see bug #3875. Running
+ // overflow hack here to shake body element a bit.
+ Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ }
+
+ /*
+ * setting height may affect wheter the component has scrollbars ->
+ * needs scrolling or not
+ */
+ setProperTabIndex();
+
+ }
+
+ /*
+ * Overridden due Table might not survive of visibility change (scroll pos
+ * lost). Example ITabPanel just set contained components invisible and back
+ * when changing tabs.
+ */
+ @Override
+ public void setVisible(boolean visible) {
+ if (isVisible() != visible) {
+ super.setVisible(visible);
+ if (initializedAndAttached) {
+ if (visible) {
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function to build html snippet for column or row headers
+ *
+ * @param uidl
+ * possibly with values caption and icon
+ * @return html snippet containing possibly an icon + caption text
+ */
+ protected String buildCaptionHtmlSnippet(UIDL uidl) {
+ String s = uidl.hasAttribute("caption") ? uidl
+ .getStringAttribute("caption") : "";
+ if (uidl.hasAttribute("icon")) {
+ s = "<img src=\""
+ + Util.escapeAttribute(client.translateVaadinUri(uidl
+ .getStringAttribute("icon")))
+ + "\" alt=\"icon\" class=\"v-icon\">" + s;
+ }
+ return s;
+ }
+
+ /**
+ * This method has logic which rows needs to be requested from server when
+ * user scrolls
+ */
+ public void onScroll(ScrollEvent event) {
+ scrollLeft = scrollBodyPanel.getElement().getScrollLeft();
+ scrollTop = scrollBodyPanel.getScrollPosition();
+ /*
+ * #6970 - IE sometimes fires scroll events for a detached table.
+ *
+ * FIXME initializedAndAttached should probably be renamed - its name
+ * doesn't seem to reflect its semantics. onDetach() doesn't set it to
+ * false, and changing that might break something else, so we need to
+ * check isAttached() separately.
+ */
+ if (!initializedAndAttached || !isAttached()) {
+ return;
+ }
+ if (!enabled) {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
+ return;
+ }
+
+ rowRequestHandler.cancel();
+
+ if (BrowserInfo.get().isSafari() && event != null && scrollTop == 0) {
+ // due to the webkitoverflowworkaround, top may sometimes report 0
+ // for webkit, although it really is not. Expecting to have the
+ // correct
+ // value available soon.
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ onScroll(null);
+ }
+ });
+ return;
+ }
+
+ // fix headers horizontal scrolling
+ tHead.setHorizontalScrollPosition(scrollLeft);
+
+ // fix footers horizontal scrolling
+ tFoot.setHorizontalScrollPosition(scrollLeft);
+
+ firstRowInViewPort = calcFirstRowInViewPort();
+ if (firstRowInViewPort > totalRows - pageLength) {
+ firstRowInViewPort = totalRows - pageLength;
+ }
+
+ int postLimit = (int) (firstRowInViewPort + (pageLength - 1) + pageLength
+ * cache_react_rate);
+ if (postLimit > totalRows - 1) {
+ postLimit = totalRows - 1;
+ }
+ int preLimit = (int) (firstRowInViewPort - pageLength
+ * cache_react_rate);
+ if (preLimit < 0) {
+ preLimit = 0;
+ }
+ final int lastRendered = scrollBody.getLastRendered();
+ final int firstRendered = scrollBody.getFirstRendered();
+
+ if (postLimit <= lastRendered && preLimit >= firstRendered) {
+ // we're within no-react area, no need to request more rows
+ // remember which firstvisible we requested, in case the server has
+ // a differing opinion
+ lastRequestedFirstvisible = firstRowInViewPort;
+ client.updateVariable(paintableId, "firstvisible",
+ firstRowInViewPort, false);
+ return;
+ }
+
+ if (firstRowInViewPort - pageLength * cache_rate > lastRendered
+ || firstRowInViewPort + pageLength + pageLength * cache_rate < firstRendered) {
+ // need a totally new set of rows
+ rowRequestHandler
+ .setReqFirstRow((firstRowInViewPort - (int) (pageLength * cache_rate)));
+ int last = firstRowInViewPort + (int) (cache_rate * pageLength)
+ + pageLength - 1;
+ if (last >= totalRows) {
+ last = totalRows - 1;
+ }
+ rowRequestHandler.setReqRows(last
+ - rowRequestHandler.getReqFirstRow() + 1);
+ rowRequestHandler.deferRowFetch();
+ return;
+ }
+ if (preLimit < firstRendered) {
+ // need some rows to the beginning of the rendered area
+ rowRequestHandler
+ .setReqFirstRow((int) (firstRowInViewPort - pageLength
+ * cache_rate));
+ rowRequestHandler.setReqRows(firstRendered
+ - rowRequestHandler.getReqFirstRow());
+ rowRequestHandler.deferRowFetch();
+
+ return;
+ }
+ if (postLimit > lastRendered) {
+ // need some rows to the end of the rendered area
+ rowRequestHandler.setReqFirstRow(lastRendered + 1);
+ rowRequestHandler.setReqRows((int) ((firstRowInViewPort
+ + pageLength + pageLength * cache_rate) - lastRendered));
+ rowRequestHandler.deferRowFetch();
+ }
+ }
+
+ protected int calcFirstRowInViewPort() {
+ return (int) Math.ceil(scrollTop / scrollBody.getRowHeight());
+ }
+
+ public VScrollTableDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ private static class TableDDDetails {
+ int overkey = -1;
+ VerticalDropLocation dropLocation;
+ String colkey;
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof TableDDDetails) {
+ TableDDDetails other = (TableDDDetails) obj;
+ return dropLocation == other.dropLocation
+ && overkey == other.overkey
+ && ((colkey != null && colkey.equals(other.colkey)) || (colkey == null && other.colkey == null));
+ }
+ return false;
+ }
+
+ // @Override
+ // public int hashCode() {
+ // return overkey;
+ // }
+ }
+
+ public class VScrollTableDropHandler extends VAbstractDropHandler {
+
+ private static final String ROWSTYLEBASE = "v-table-row-drag-";
+ private TableDDDetails dropDetails;
+ private TableDDDetails lastEmphasized;
+
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ updateDropDetails(drag);
+ super.dragEnter(drag);
+ }
+
+ private void updateDropDetails(VDragEvent drag) {
+ dropDetails = new TableDDDetails();
+ Element elementOver = drag.getElementOver();
+
+ VScrollTableRow row = Util.findWidget(elementOver, getRowClass());
+ if (row != null) {
+ dropDetails.overkey = row.rowKey;
+ Element tr = row.getElement();
+ Element element = elementOver;
+ while (element != null && element.getParentElement() != tr) {
+ element = (Element) element.getParentElement();
+ }
+ int childIndex = DOM.getChildIndex(tr, element);
+ dropDetails.colkey = tHead.getHeaderCell(childIndex)
+ .getColKey();
+ dropDetails.dropLocation = DDUtil.getVerticalDropLocation(
+ row.getElement(), drag.getCurrentGwtEvent(), 0.2);
+ }
+
+ drag.getDropDetails().put("itemIdOver", dropDetails.overkey + "");
+ drag.getDropDetails().put(
+ "detail",
+ dropDetails.dropLocation != null ? dropDetails.dropLocation
+ .toString() : null);
+
+ }
+
+ private Class<? extends Widget> getRowClass() {
+ // get the row type this way to make dd work in derived
+ // implementations
+ return scrollBody.iterator().next().getClass();
+ }
+
+ @Override
+ public void dragOver(VDragEvent drag) {
+ TableDDDetails oldDetails = dropDetails;
+ updateDropDetails(drag);
+ if (!oldDetails.equals(dropDetails)) {
+ deEmphasis();
+ final TableDDDetails newDetails = dropDetails;
+ VAcceptCallback cb = new VAcceptCallback() {
+ public void accepted(VDragEvent event) {
+ if (newDetails.equals(dropDetails)) {
+ dragAccepted(event);
+ }
+ /*
+ * Else new target slot already defined, ignore
+ */
+ }
+ };
+ validate(cb, drag);
+ }
+ }
+
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ deEmphasis();
+ super.dragLeave(drag);
+ }
+
+ @Override
+ public boolean drop(VDragEvent drag) {
+ deEmphasis();
+ return super.drop(drag);
+ }
+
+ private void deEmphasis() {
+ UIObject.setStyleName(getElement(), CLASSNAME + "-drag", false);
+ if (lastEmphasized == null) {
+ return;
+ }
+ for (Widget w : scrollBody.renderedRows) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (lastEmphasized != null
+ && row.rowKey == lastEmphasized.overkey) {
+ String stylename = ROWSTYLEBASE
+ + lastEmphasized.dropLocation.toString()
+ .toLowerCase();
+ VScrollTableRow.setStyleName(row.getElement(), stylename,
+ false);
+ lastEmphasized = null;
+ return;
+ }
+ }
+ }
+
+ /**
+ * TODO needs different drop modes ?? (on cells, on rows), now only
+ * supports rows
+ */
+ private void emphasis(TableDDDetails details) {
+ deEmphasis();
+ UIObject.setStyleName(getElement(), CLASSNAME + "-drag", true);
+ // iterate old and new emphasized row
+ for (Widget w : scrollBody.renderedRows) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (details != null && details.overkey == row.rowKey) {
+ String stylename = ROWSTYLEBASE
+ + details.dropLocation.toString().toLowerCase();
+ VScrollTableRow.setStyleName(row.getElement(), stylename,
+ true);
+ lastEmphasized = details;
+ return;
+ }
+ }
+ }
+
+ @Override
+ protected void dragAccepted(VDragEvent drag) {
+ emphasis(dropDetails);
+ }
+
+ @Override
+ public ComponentConnector getConnector() {
+ return ConnectorMap.get(client).getConnector(VScrollTable.this);
+ }
+
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ }
+
+ protected VScrollTableRow getFocusedRow() {
+ return focusedRow;
+ }
+
+ /**
+ * Moves the selection head to a specific row
+ *
+ * @param row
+ * The row to where the selection head should move
+ * @return Returns true if focus was moved successfully, else false
+ */
+ public boolean setRowFocus(VScrollTableRow row) {
+
+ if (!isSelectable()) {
+ return false;
+ }
+
+ // Remove previous selection
+ if (focusedRow != null && focusedRow != row) {
+ focusedRow.removeStyleName(CLASSNAME_SELECTION_FOCUS);
+ }
+
+ if (row != null) {
+
+ // Apply focus style to new selection
+ row.addStyleName(CLASSNAME_SELECTION_FOCUS);
+
+ /*
+ * Trying to set focus on already focused row
+ */
+ if (row == focusedRow) {
+ return false;
+ }
+
+ // Set new focused row
+ focusedRow = row;
+
+ ensureRowIsVisible(row);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Ensures that the row is visible
+ *
+ * @param row
+ * The row to ensure is visible
+ */
+ private void ensureRowIsVisible(VScrollTableRow row) {
+ Util.scrollIntoViewVertically(row.getElement());
+ }
+
+ /**
+ * Handles the keyboard events handled by the table
+ *
+ * @param event
+ * The keyboard event received
+ * @return true iff the navigation event was handled
+ */
+ protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+ if (keycode == KeyCodes.KEY_TAB || keycode == KeyCodes.KEY_SHIFT) {
+ // Do not handle tab key
+ return false;
+ }
+
+ // Down navigation
+ if (!isSelectable() && keycode == getNavigationDownKey()) {
+ scrollBodyPanel.setScrollPosition(scrollBodyPanel
+ .getScrollPosition() + scrollingVelocity);
+ return true;
+ } else if (keycode == getNavigationDownKey()) {
+ if (isMultiSelectModeAny() && moveFocusDown()) {
+ selectFocusedRow(ctrl, shift);
+
+ } else if (isSingleSelectMode() && !shift && moveFocusDown()) {
+ selectFocusedRow(ctrl, shift);
+ }
+ return true;
+ }
+
+ // Up navigation
+ if (!isSelectable() && keycode == getNavigationUpKey()) {
+ scrollBodyPanel.setScrollPosition(scrollBodyPanel
+ .getScrollPosition() - scrollingVelocity);
+ return true;
+ } else if (keycode == getNavigationUpKey()) {
+ if (isMultiSelectModeAny() && moveFocusUp()) {
+ selectFocusedRow(ctrl, shift);
+ } else if (isSingleSelectMode() && !shift && moveFocusUp()) {
+ selectFocusedRow(ctrl, shift);
+ }
+ return true;
+ }
+
+ if (keycode == getNavigationLeftKey()) {
+ // Left navigation
+ scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel
+ .getHorizontalScrollPosition() - scrollingVelocity);
+ return true;
+
+ } else if (keycode == getNavigationRightKey()) {
+ // Right navigation
+ scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel
+ .getHorizontalScrollPosition() + scrollingVelocity);
+ return true;
+ }
+
+ // Select navigation
+ if (isSelectable() && keycode == getNavigationSelectKey()) {
+ if (isSingleSelectMode()) {
+ boolean wasSelected = focusedRow.isSelected();
+ deselectAll();
+ if (!wasSelected || !nullSelectionAllowed) {
+ focusedRow.toggleSelection();
+ }
+ } else {
+ focusedRow.toggleSelection();
+ removeRowFromUnsentSelectionRanges(focusedRow);
+ }
+
+ sendSelectedRows();
+ return true;
+ }
+
+ // Page Down navigation
+ if (keycode == getNavigationPageDownKey()) {
+ if (isSelectable()) {
+ /*
+ * If selectable we plagiate MSW behaviour: first scroll to the
+ * end of current view. If at the end, scroll down one page
+ * length and keep the selected row in the bottom part of
+ * visible area.
+ */
+ if (!isFocusAtTheEndOfTable()) {
+ VScrollTableRow lastVisibleRowInViewPort = scrollBody
+ .getRowByRowIndex(firstRowInViewPort
+ + getFullyVisibleRowCount() - 1);
+ if (lastVisibleRowInViewPort != null
+ && lastVisibleRowInViewPort != focusedRow) {
+ // focused row is not at the end of the table, move
+ // focus and select the last visible row
+ setRowFocus(lastVisibleRowInViewPort);
+ selectFocusedRow(ctrl, shift);
+ sendSelectedRows();
+ } else {
+ int indexOfToBeFocused = focusedRow.getIndex()
+ + getFullyVisibleRowCount();
+ if (indexOfToBeFocused >= totalRows) {
+ indexOfToBeFocused = totalRows - 1;
+ }
+ VScrollTableRow toBeFocusedRow = scrollBody
+ .getRowByRowIndex(indexOfToBeFocused);
+
+ if (toBeFocusedRow != null) {
+ /*
+ * if the next focused row is rendered
+ */
+ setRowFocus(toBeFocusedRow);
+ selectFocusedRow(ctrl, shift);
+ // TODO needs scrollintoview ?
+ sendSelectedRows();
+ } else {
+ // scroll down by pixels and return, to wait for
+ // new rows, then select the last item in the
+ // viewport
+ selectLastItemInNextRender = true;
+ multiselectPending = shift;
+ scrollByPagelenght(1);
+ }
+ }
+ }
+ } else {
+ /* No selections, go page down by scrolling */
+ scrollByPagelenght(1);
+ }
+ return true;
+ }
+
+ // Page Up navigation
+ if (keycode == getNavigationPageUpKey()) {
+ if (isSelectable()) {
+ /*
+ * If selectable we plagiate MSW behaviour: first scroll to the
+ * end of current view. If at the end, scroll down one page
+ * length and keep the selected row in the bottom part of
+ * visible area.
+ */
+ if (!isFocusAtTheBeginningOfTable()) {
+ VScrollTableRow firstVisibleRowInViewPort = scrollBody
+ .getRowByRowIndex(firstRowInViewPort);
+ if (firstVisibleRowInViewPort != null
+ && firstVisibleRowInViewPort != focusedRow) {
+ // focus is not at the beginning of the table, move
+ // focus and select the first visible row
+ setRowFocus(firstVisibleRowInViewPort);
+ selectFocusedRow(ctrl, shift);
+ sendSelectedRows();
+ } else {
+ int indexOfToBeFocused = focusedRow.getIndex()
+ - getFullyVisibleRowCount();
+ if (indexOfToBeFocused < 0) {
+ indexOfToBeFocused = 0;
+ }
+ VScrollTableRow toBeFocusedRow = scrollBody
+ .getRowByRowIndex(indexOfToBeFocused);
+
+ if (toBeFocusedRow != null) { // if the next focused row
+ // is rendered
+ setRowFocus(toBeFocusedRow);
+ selectFocusedRow(ctrl, shift);
+ // TODO needs scrollintoview ?
+ sendSelectedRows();
+ } else {
+ // unless waiting for the next rowset already
+ // scroll down by pixels and return, to wait for
+ // new rows, then select the last item in the
+ // viewport
+ selectFirstItemInNextRender = true;
+ multiselectPending = shift;
+ scrollByPagelenght(-1);
+ }
+ }
+ }
+ } else {
+ /* No selections, go page up by scrolling */
+ scrollByPagelenght(-1);
+ }
+
+ return true;
+ }
+
+ // Goto start navigation
+ if (keycode == getNavigationStartKey()) {
+ scrollBodyPanel.setScrollPosition(0);
+ if (isSelectable()) {
+ if (focusedRow != null && focusedRow.getIndex() == 0) {
+ return false;
+ } else {
+ VScrollTableRow rowByRowIndex = (VScrollTableRow) scrollBody
+ .iterator().next();
+ if (rowByRowIndex.getIndex() == 0) {
+ setRowFocus(rowByRowIndex);
+ selectFocusedRow(ctrl, shift);
+ sendSelectedRows();
+ } else {
+ // first row of table will come in next row fetch
+ if (ctrl) {
+ focusFirstItemInNextRender = true;
+ } else {
+ selectFirstItemInNextRender = true;
+ multiselectPending = shift;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ // Goto end navigation
+ if (keycode == getNavigationEndKey()) {
+ scrollBodyPanel.setScrollPosition(scrollBody.getOffsetHeight());
+ if (isSelectable()) {
+ final int lastRendered = scrollBody.getLastRendered();
+ if (lastRendered + 1 == totalRows) {
+ VScrollTableRow rowByRowIndex = scrollBody
+ .getRowByRowIndex(lastRendered);
+ if (focusedRow != rowByRowIndex) {
+ setRowFocus(rowByRowIndex);
+ selectFocusedRow(ctrl, shift);
+ sendSelectedRows();
+ }
+ } else {
+ if (ctrl) {
+ focusLastItemInNextRender = true;
+ } else {
+ selectLastItemInNextRender = true;
+ multiselectPending = shift;
+ }
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean isFocusAtTheBeginningOfTable() {
+ return focusedRow.getIndex() == 0;
+ }
+
+ private boolean isFocusAtTheEndOfTable() {
+ return focusedRow.getIndex() + 1 >= totalRows;
+ }
+
+ private int getFullyVisibleRowCount() {
+ return (int) (scrollBodyPanel.getOffsetHeight() / scrollBody
+ .getRowHeight());
+ }
+
+ private void scrollByPagelenght(int i) {
+ int pixels = i * scrollBodyPanel.getOffsetHeight();
+ int newPixels = scrollBodyPanel.getScrollPosition() + pixels;
+ if (newPixels < 0) {
+ newPixels = 0;
+ } // else if too high, NOP (all know browsers accept illegally big
+ // values here)
+ scrollBodyPanel.setScrollPosition(newPixels);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+ public void onFocus(FocusEvent event) {
+ if (isFocusable()) {
+ hasFocus = true;
+
+ // Focus a row if no row is in focus
+ if (focusedRow == null) {
+ focusRowFromBody();
+ } else {
+ setRowFocus(focusedRow);
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
+ * .dom.client.BlurEvent)
+ */
+ public void onBlur(BlurEvent event) {
+ hasFocus = false;
+ navKeyDown = false;
+
+ if (BrowserInfo.get().isIE()) {
+ // IE sometimes moves focus to a clicked table cell...
+ Element focusedElement = Util.getIEFocusedElement();
+ if (Util.getConnectorForElement(client, getParent(), focusedElement) == this) {
+ // ..in that case, steal the focus back to the focus handler
+ // but not if focus is in a child component instead (#7965)
+ focus();
+ return;
+ }
+ }
+
+ if (isFocusable()) {
+ // Unfocus any row
+ setRowFocus(null);
+ }
+ }
+
+ /**
+ * Removes a key from a range if the key is found in a selected range
+ *
+ * @param key
+ * The key to remove
+ */
+ private void removeRowFromUnsentSelectionRanges(VScrollTableRow row) {
+ Collection<SelectionRange> newRanges = null;
+ for (Iterator<SelectionRange> iterator = selectedRowRanges.iterator(); iterator
+ .hasNext();) {
+ SelectionRange range = iterator.next();
+ if (range.inRange(row)) {
+ // Split the range if given row is in range
+ Collection<SelectionRange> splitranges = range.split(row);
+ if (newRanges == null) {
+ newRanges = new ArrayList<SelectionRange>();
+ }
+ newRanges.addAll(splitranges);
+ iterator.remove();
+ }
+ }
+ if (newRanges != null) {
+ selectedRowRanges.addAll(newRanges);
+ }
+ }
+
+ /**
+ * Can the Table be focused?
+ *
+ * @return True if the table can be focused, else false
+ */
+ public boolean isFocusable() {
+ if (scrollBody != null && enabled) {
+ return !(!hasHorizontalScrollbar() && !hasVerticalScrollbar() && !isSelectable());
+ }
+ return false;
+ }
+
+ private boolean hasHorizontalScrollbar() {
+ return scrollBody.getOffsetWidth() > scrollBodyPanel.getOffsetWidth();
+ }
+
+ private boolean hasVerticalScrollbar() {
+ return scrollBody.getOffsetHeight() > scrollBodyPanel.getOffsetHeight();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Focusable#focus()
+ */
+ public void focus() {
+ if (isFocusable()) {
+ scrollBodyPanel.focus();
+ }
+ }
+
+ /**
+ * Sets the proper tabIndex for scrollBodyPanel (the focusable elemen in the
+ * component).
+ *
+ * If the component has no explicit tabIndex a zero is given (default
+ * tabbing order based on dom hierarchy) or -1 if the component does not
+ * need to gain focus. The component needs no focus if it has no scrollabars
+ * (not scrollable) and not selectable. Note that in the future shortcut
+ * actions may need focus.
+ *
+ */
+ void setProperTabIndex() {
+ int storedScrollTop = 0;
+ int storedScrollLeft = 0;
+
+ if (BrowserInfo.get().getOperaVersion() >= 11) {
+ // Workaround for Opera scroll bug when changing tabIndex (#6222)
+ storedScrollTop = scrollBodyPanel.getScrollPosition();
+ storedScrollLeft = scrollBodyPanel.getHorizontalScrollPosition();
+ }
+
+ if (tabIndex == 0 && !isFocusable()) {
+ scrollBodyPanel.setTabIndex(-1);
+ } else {
+ scrollBodyPanel.setTabIndex(tabIndex);
+ }
+
+ if (BrowserInfo.get().getOperaVersion() >= 11) {
+ // Workaround for Opera scroll bug when changing tabIndex (#6222)
+ scrollBodyPanel.setScrollPosition(storedScrollTop);
+ scrollBodyPanel.setHorizontalScrollPosition(storedScrollLeft);
+ }
+ }
+
+ public void startScrollingVelocityTimer() {
+ if (scrollingVelocityTimer == null) {
+ scrollingVelocityTimer = new Timer() {
+ @Override
+ public void run() {
+ scrollingVelocity++;
+ }
+ };
+ scrollingVelocityTimer.scheduleRepeating(100);
+ }
+ }
+
+ public void cancelScrollingVelocityTimer() {
+ if (scrollingVelocityTimer != null) {
+ // Remove velocityTimer if it exists and the Table is disabled
+ scrollingVelocityTimer.cancel();
+ scrollingVelocityTimer = null;
+ scrollingVelocity = 10;
+ }
+ }
+
+ /**
+ *
+ * @param keyCode
+ * @return true if the given keyCode is used by the table for navigation
+ */
+ private boolean isNavigationKey(int keyCode) {
+ return keyCode == getNavigationUpKey()
+ || keyCode == getNavigationLeftKey()
+ || keyCode == getNavigationRightKey()
+ || keyCode == getNavigationDownKey()
+ || keyCode == getNavigationPageUpKey()
+ || keyCode == getNavigationPageDownKey()
+ || keyCode == getNavigationEndKey()
+ || keyCode == getNavigationStartKey();
+ }
+
+ public void lazyRevertFocusToRow(final VScrollTableRow currentlyFocusedRow) {
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ public void execute() {
+ if (currentlyFocusedRow != null) {
+ setRowFocus(currentlyFocusedRow);
+ } else {
+ VConsole.log("no row?");
+ focusRowFromBody();
+ }
+ scrollBody.ensureFocus();
+ }
+ });
+ }
+
+ public Action[] getActions() {
+ if (bodyActionKeys == null) {
+ return new Action[] {};
+ }
+ final Action[] actions = new Action[bodyActionKeys.length];
+ for (int i = 0; i < actions.length; i++) {
+ final String actionKey = bodyActionKeys[i];
+ Action bodyAction = new TreeAction(this, null, actionKey);
+ bodyAction.setCaption(getActionCaption(actionKey));
+ bodyAction.setIconUrl(getActionIcon(actionKey));
+ actions[i] = bodyAction;
+ }
+ return actions;
+ }
+
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ /**
+ * Add this to the element mouse down event by using element.setPropertyJSO
+ * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again
+ * when the mouse is depressed in the mouse up event.
+ *
+ * @return Returns the JSO preventing text selection
+ */
+ private static native JavaScriptObject getPreventTextSelectionIEHack()
+ /*-{
+ return function(){ return false; };
+ }-*/;
+
+ public void triggerLazyColumnAdjustment(boolean now) {
+ lazyAdjustColumnWidths.cancel();
+ if (now) {
+ lazyAdjustColumnWidths.run();
+ } else {
+ lazyAdjustColumnWidths.schedule(LAZY_COLUMN_ADJUST_TIMEOUT);
+ }
+ }
+
+ private boolean isDynamicWidth() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedWidth();
+ }
+
+ private boolean isDynamicHeight() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ if (paintable == null) {
+ // This should be refactored. As isDynamicHeight can be called from
+ // a timer it is possible that the connector has been unregistered
+ // when this method is called, causing getConnector to return null.
+ return false;
+ }
+ return paintable.isUndefinedHeight();
+ }
+
+ private void debug(String msg) {
+ if (enableDebug) {
+ VConsole.error(msg);
+ }
+ }
+
+ public Widget getWidgetForPaintable() {
+ return this;
+ }
+}
--- /dev/null
- SimpleManagedLayout {
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.tabsheet;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
++import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.ui.TabSheet;
+
+@Component(TabSheet.class)
+public class TabsheetConnector extends TabsheetBaseConnector implements
++ SimpleManagedLayout, MayScrollChildren {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ if (isRealUpdate(uidl)) {
+ // Handle stylename changes before generics (might affect size
+ // calculations)
+ getWidget().handleStyleNames(uidl, getState());
+ }
+
+ super.updateFromUIDL(uidl, client);
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ // tabs; push or not
+ if (!isUndefinedWidth()) {
+ DOM.setStyleAttribute(getWidget().tabs, "overflow", "hidden");
+ } else {
+ getWidget().showAllTabs();
+ DOM.setStyleAttribute(getWidget().tabs, "width", "");
+ DOM.setStyleAttribute(getWidget().tabs, "overflow", "visible");
+ getWidget().updateDynamicWidth();
+ }
+
+ if (!isUndefinedHeight()) {
+ // Must update height after the styles have been set
+ getWidget().updateContentNodeHeight();
+ getWidget().updateOpenTabSize();
+ }
+
+ getWidget().iLayout();
+
+ // Re run relative size update to ensure optimal scrollbars
+ // TODO isolate to situation that visible tab has undefined height
+ try {
+ client.handleComponentRelativeSize(getWidget().tp
+ .getWidget(getWidget().tp.getVisibleWidget()));
+ } catch (Exception e) {
+ // Ignore, most likely empty tabsheet
+ }
+
+ getWidget().waitingForResponse = false;
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VTabsheet.class);
+ }
+
+ @Override
+ public VTabsheet getWidget() {
+ return (VTabsheet) super.getWidget();
+ }
+
+ public void updateCaption(ComponentConnector component) {
+ /* Tabsheet does not render its children's captions */
+ }
+
+ public void layout() {
+ VTabsheet tabsheet = getWidget();
+
+ tabsheet.updateContentNodeHeight();
+
+ if (isUndefinedWidth()) {
+ tabsheet.contentNode.getStyle().setProperty("width", "");
+ } else {
+ int contentWidth = tabsheet.getOffsetWidth()
+ - tabsheet.getContentAreaBorderWidth();
+ if (contentWidth < 0) {
+ contentWidth = 0;
+ }
+ tabsheet.contentNode.getStyle().setProperty("width",
+ contentWidth + "px");
+ }
+
+ tabsheet.updateOpenTabSize();
+ if (isUndefinedWidth()) {
+ tabsheet.updateDynamicWidth();
+ }
+
+ tabsheet.iLayout();
+
+ }
+
+}
--- /dev/null
- tp.runWebkitOverflowAutoFix();
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.tabsheet;
+
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.HasBlurHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ComponentState;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.EventId;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.ui.label.VLabel;
+
+public class VTabsheet extends VTabsheetBase implements Focusable,
+ FocusHandler, BlurHandler, KeyDownHandler {
+
+ private static class VCloseEvent {
+ private Tab tab;
+
+ VCloseEvent(Tab tab) {
+ this.tab = tab;
+ }
+
+ public Tab getTab() {
+ return tab;
+ }
+
+ }
+
+ private interface VCloseHandler {
+ public void onClose(VCloseEvent event);
+ }
+
+ /**
+ * Representation of a single "tab" shown in the TabBar
+ *
+ */
+ private static class Tab extends SimplePanel implements HasFocusHandlers,
+ HasBlurHandlers, HasKeyDownHandlers {
+ private static final String TD_CLASSNAME = CLASSNAME + "-tabitemcell";
+ private static final String TD_FIRST_CLASSNAME = TD_CLASSNAME
+ + "-first";
+ private static final String TD_SELECTED_CLASSNAME = TD_CLASSNAME
+ + "-selected";
+ private static final String TD_SELECTED_FIRST_CLASSNAME = TD_SELECTED_CLASSNAME
+ + "-first";
+ private static final String TD_DISABLED_CLASSNAME = TD_CLASSNAME
+ + "-disabled";
+
+ private static final String DIV_CLASSNAME = CLASSNAME + "-tabitem";
+ private static final String DIV_SELECTED_CLASSNAME = DIV_CLASSNAME
+ + "-selected";
+
+ private TabCaption tabCaption;
+ Element td = getElement();
+ private VCloseHandler closeHandler;
+
+ private boolean enabledOnServer = true;
+ private Element div;
+ private TabBar tabBar;
+ private boolean hiddenOnServer = false;
+
+ private String styleName;
+
+ private Tab(TabBar tabBar) {
+ super(DOM.createTD());
+ this.tabBar = tabBar;
+ setStyleName(td, TD_CLASSNAME);
+
+ div = DOM.createDiv();
+ focusImpl.setTabIndex(td, -1);
+ setStyleName(div, DIV_CLASSNAME);
+
+ DOM.appendChild(td, div);
+
+ tabCaption = new TabCaption(this, getTabsheet()
+ .getApplicationConnection());
+ add(tabCaption);
+
+ addFocusHandler(getTabsheet());
+ addBlurHandler(getTabsheet());
+ addKeyDownHandler(getTabsheet());
+ }
+
+ public boolean isHiddenOnServer() {
+ return hiddenOnServer;
+ }
+
+ public void setHiddenOnServer(boolean hiddenOnServer) {
+ this.hiddenOnServer = hiddenOnServer;
+ }
+
+ @Override
+ protected Element getContainerElement() {
+ // Attach caption element to div, not td
+ return div;
+ }
+
+ public boolean isEnabledOnServer() {
+ return enabledOnServer;
+ }
+
+ public void setEnabledOnServer(boolean enabled) {
+ enabledOnServer = enabled;
+ setStyleName(td, TD_DISABLED_CLASSNAME, !enabled);
+ if (!enabled) {
+ focusImpl.setTabIndex(td, -1);
+ }
+ }
+
+ public void addClickHandler(ClickHandler handler) {
+ tabCaption.addClickHandler(handler);
+ }
+
+ public void setCloseHandler(VCloseHandler closeHandler) {
+ this.closeHandler = closeHandler;
+ }
+
+ /**
+ * Toggles the style names for the Tab
+ *
+ * @param selected
+ * true if the Tab is selected
+ * @param first
+ * true if the Tab is the first visible Tab
+ */
+ public void setStyleNames(boolean selected, boolean first) {
+ setStyleName(td, TD_FIRST_CLASSNAME, first);
+ setStyleName(td, TD_SELECTED_CLASSNAME, selected);
+ setStyleName(td, TD_SELECTED_FIRST_CLASSNAME, selected && first);
+ setStyleName(div, DIV_SELECTED_CLASSNAME, selected);
+ }
+
+ public void setTabulatorIndex(int tabIndex) {
+ focusImpl.setTabIndex(td, tabIndex);
+ }
+
+ public boolean isClosable() {
+ return tabCaption.isClosable();
+ }
+
+ public void onClose() {
+ closeHandler.onClose(new VCloseEvent(this));
+ }
+
+ public VTabsheet getTabsheet() {
+ return tabBar.getTabsheet();
+ }
+
+ public void updateFromUIDL(UIDL tabUidl) {
+ tabCaption.updateCaption(tabUidl);
+
+ // Apply the styleName set for the tab
+ String newStyleName = tabUidl.getStringAttribute(TAB_STYLE_NAME);
+ // Find the nth td element
+ if (newStyleName != null && newStyleName.length() != 0) {
+ if (!newStyleName.equals(styleName)) {
+ // If we have a new style name
+ if (styleName != null && styleName.length() != 0) {
+ // Remove old style name if present
+ td.removeClassName(TD_CLASSNAME + "-" + styleName);
+ }
+ // Set new style name
+ td.addClassName(TD_CLASSNAME + "-" + newStyleName);
+ styleName = newStyleName;
+ }
+ } else if (styleName != null) {
+ // Remove the set stylename if no stylename is present in the
+ // uidl
+ td.removeClassName(TD_CLASSNAME + "-" + styleName);
+ styleName = null;
+ }
+ }
+
+ public void recalculateCaptionWidth() {
+ tabCaption.setWidth(tabCaption.getRequiredWidth() + "px");
+ }
+
+ public HandlerRegistration addFocusHandler(FocusHandler handler) {
+ return addDomHandler(handler, FocusEvent.getType());
+ }
+
+ public HandlerRegistration addBlurHandler(BlurHandler handler) {
+ return addDomHandler(handler, BlurEvent.getType());
+ }
+
+ public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
+ return addDomHandler(handler, KeyDownEvent.getType());
+ }
+
+ public void focus() {
+ focusImpl.focus(td);
+ }
+
+ public void blur() {
+ focusImpl.blur(td);
+ }
+ }
+
+ private static class TabCaption extends VCaption {
+
+ private boolean closable = false;
+ private Element closeButton;
+ private Tab tab;
+ private ApplicationConnection client;
+
+ TabCaption(Tab tab, ApplicationConnection client) {
+ super(client);
+ this.client = client;
+ this.tab = tab;
+ }
+
+ public boolean updateCaption(UIDL uidl) {
+ if (uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION)) {
+ TooltipInfo tooltipInfo = new TooltipInfo();
+ tooltipInfo
+ .setTitle(uidl
+ .getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION));
+ tooltipInfo
+ .setErrorMessage(uidl
+ .getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ERROR_MESSAGE));
+ client.registerTooltip(getTabsheet(), getElement(), tooltipInfo);
+ } else {
+ client.registerTooltip(getTabsheet(), getElement(), null);
+ }
+
+ // TODO need to call this instead of super because the caption does
+ // not have an owner
+ boolean ret = updateCaptionWithoutOwner(
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_CAPTION),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DISABLED),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ERROR_MESSAGE),
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ICON));
+
+ setClosable(uidl.hasAttribute("closable"));
+
+ return ret;
+ }
+
+ private VTabsheet getTabsheet() {
+ return tab.getTabsheet();
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (closable && event.getTypeInt() == Event.ONCLICK
+ && event.getEventTarget().cast() == closeButton) {
+ tab.onClose();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ super.onBrowserEvent(event);
+
+ if (event.getTypeInt() == Event.ONLOAD) {
+ getTabsheet().tabSizeMightHaveChanged(getTab());
+ }
+ client.handleTooltipEvent(event, getTabsheet(), getElement());
+ }
+
+ public Tab getTab() {
+ return tab;
+ }
+
+ public void setClosable(boolean closable) {
+ this.closable = closable;
+ if (closable && closeButton == null) {
+ closeButton = DOM.createSpan();
+ closeButton.setInnerHTML("×");
+ closeButton
+ .setClassName(VTabsheet.CLASSNAME + "-caption-close");
+ getElement().insertBefore(closeButton,
+ getElement().getLastChild());
+ } else if (!closable && closeButton != null) {
+ getElement().removeChild(closeButton);
+ closeButton = null;
+ }
+ if (closable) {
+ addStyleDependentName("closable");
+ } else {
+ removeStyleDependentName("closable");
+ }
+ }
+
+ public boolean isClosable() {
+ return closable;
+ }
+
+ @Override
+ public int getRequiredWidth() {
+ int width = super.getRequiredWidth();
+ if (closeButton != null) {
+ width += Util.getRequiredWidth(closeButton);
+ }
+ return width;
+ }
+ }
+
+ static class TabBar extends ComplexPanel implements ClickHandler,
+ VCloseHandler {
+
+ private final Element tr = DOM.createTR();
+
+ private final Element spacerTd = DOM.createTD();
+
+ private Tab selected;
+
+ private VTabsheet tabsheet;
+
+ TabBar(VTabsheet tabsheet) {
+ this.tabsheet = tabsheet;
+
+ Element el = DOM.createTable();
+ Element tbody = DOM.createTBody();
+ DOM.appendChild(el, tbody);
+ DOM.appendChild(tbody, tr);
+ setStyleName(spacerTd, CLASSNAME + "-spacertd");
+ DOM.appendChild(tr, spacerTd);
+ DOM.appendChild(spacerTd, DOM.createDiv());
+ setElement(el);
+ }
+
+ public void onClose(VCloseEvent event) {
+ Tab tab = event.getTab();
+ if (!tab.isEnabledOnServer()) {
+ return;
+ }
+ int tabIndex = getWidgetIndex(tab);
+ getTabsheet().sendTabClosedEvent(tabIndex);
+ }
+
+ protected Element getContainerElement() {
+ return tr;
+ }
+
+ public int getTabCount() {
+ return getWidgetCount();
+ }
+
+ public Tab addTab() {
+ Tab t = new Tab(this);
+ int tabIndex = getTabCount();
+
+ // Logical attach
+ insert(t, tr, tabIndex, true);
+
+ if (tabIndex == 0) {
+ // Set the "first" style
+ t.setStyleNames(false, true);
+ }
+
+ t.addClickHandler(this);
+ t.setCloseHandler(this);
+
+ return t;
+ }
+
+ public void onClick(ClickEvent event) {
+ Widget caption = (Widget) event.getSource();
+ int index = getWidgetIndex(caption.getParent());
+ // IE needs explicit focus()
+ if (BrowserInfo.get().isIE()) {
+ getTabsheet().focus();
+ }
+ getTabsheet().onTabSelected(index);
+ }
+
+ public VTabsheet getTabsheet() {
+ return tabsheet;
+ }
+
+ public Tab getTab(int index) {
+ if (index < 0 || index >= getTabCount()) {
+ return null;
+ }
+ return (Tab) super.getWidget(index);
+ }
+
+ public void selectTab(int index) {
+ final Tab newSelected = getTab(index);
+ final Tab oldSelected = selected;
+
+ newSelected.setStyleNames(true, isFirstVisibleTab(index));
+ newSelected.setTabulatorIndex(getTabsheet().tabulatorIndex);
+
+ if (oldSelected != null && oldSelected != newSelected) {
+ oldSelected.setStyleNames(false,
+ isFirstVisibleTab(getWidgetIndex(oldSelected)));
+ oldSelected.setTabulatorIndex(-1);
+ }
+
+ // Update the field holding the currently selected tab
+ selected = newSelected;
+
+ // The selected tab might need more (or less) space
+ newSelected.recalculateCaptionWidth();
+ getTab(tabsheet.activeTabIndex).recalculateCaptionWidth();
+ }
+
+ public void removeTab(int i) {
+ Tab tab = getTab(i);
+ if (tab == null) {
+ return;
+ }
+
+ remove(tab);
+
+ /*
+ * If this widget was selected we need to unmark it as the last
+ * selected
+ */
+ if (tab == selected) {
+ selected = null;
+ }
+
+ // FIXME: Shouldn't something be selected instead?
+ }
+
+ private boolean isFirstVisibleTab(int index) {
+ return getFirstVisibleTab() == index;
+ }
+
+ /**
+ * Returns the index of the first visible tab
+ *
+ * @return
+ */
+ private int getFirstVisibleTab() {
+ return getNextVisibleTab(-1);
+ }
+
+ /**
+ * Find the next visible tab. Returns -1 if none is found.
+ *
+ * @param i
+ * @return
+ */
+ private int getNextVisibleTab(int i) {
+ int tabs = getTabCount();
+ do {
+ i++;
+ } while (i < tabs && getTab(i).isHiddenOnServer());
+
+ if (i == tabs) {
+ return -1;
+ } else {
+ return i;
+ }
+ }
+
+ /**
+ * Find the previous visible tab. Returns -1 if none is found.
+ *
+ * @param i
+ * @return
+ */
+ private int getPreviousVisibleTab(int i) {
+ do {
+ i--;
+ } while (i >= 0 && getTab(i).isHiddenOnServer());
+
+ return i;
+
+ }
+
+ public int scrollLeft(int currentFirstVisible) {
+ int prevVisible = getPreviousVisibleTab(currentFirstVisible);
+ if (prevVisible == -1) {
+ return -1;
+ }
+
+ Tab newFirst = getTab(prevVisible);
+ newFirst.setVisible(true);
+ newFirst.recalculateCaptionWidth();
+
+ return prevVisible;
+ }
+
+ public int scrollRight(int currentFirstVisible) {
+ int nextVisible = getNextVisibleTab(currentFirstVisible);
+ if (nextVisible == -1) {
+ return -1;
+ }
+ Tab currentFirst = getTab(currentFirstVisible);
+ currentFirst.setVisible(false);
+ currentFirst.recalculateCaptionWidth();
+ return nextVisible;
+ }
+ }
+
+ public static final String CLASSNAME = "v-tabsheet";
+
+ public static final String TABS_CLASSNAME = "v-tabsheet-tabcontainer";
+ public static final String SCROLLER_CLASSNAME = "v-tabsheet-scroller";
+
+ // Can't use "style" as it's already in use
+ public static final String TAB_STYLE_NAME = "tabstyle";
+
+ final Element tabs; // tabbar and 'scroller' container
+ Tab focusedTab;
+ /**
+ * The tabindex property (position in the browser's focus cycle.) Named like
+ * this to avoid confusion with activeTabIndex.
+ */
+ int tabulatorIndex = 0;
+
+ private static final FocusImpl focusImpl = FocusImpl.getFocusImplForPanel();
+
+ private final Element scroller; // tab-scroller element
+ private final Element scrollerNext; // tab-scroller next button element
+ private final Element scrollerPrev; // tab-scroller prev button element
+
+ /**
+ * The index of the first visible tab (when scrolled)
+ */
+ private int scrollerIndex = 0;
+
+ final TabBar tb = new TabBar(this);
+ final VTabsheetPanel tp = new VTabsheetPanel();
+ final Element contentNode;
+
+ private final Element deco;
+
+ boolean waitingForResponse;
+
+ private String currentStyle;
+
+ /**
+ * @return Whether the tab could be selected or not.
+ */
+ private boolean onTabSelected(final int tabIndex) {
+ Tab tab = tb.getTab(tabIndex);
+ if (client == null || disabled || waitingForResponse) {
+ return false;
+ }
+ if (!tab.isEnabledOnServer() || tab.isHiddenOnServer()) {
+ return false;
+ }
+ if (activeTabIndex != tabIndex) {
+ tb.selectTab(tabIndex);
+
+ // If this TabSheet already has focus, set the new selected tab
+ // as focused.
+ if (focusedTab != null) {
+ focusedTab = tab;
+ }
+
+ addStyleDependentName("loading");
+ // Hide the current contents so a loading indicator can be shown
+ // instead
+ Widget currentlyDisplayedWidget = tp.getWidget(tp
+ .getVisibleWidget());
+ currentlyDisplayedWidget.getElement().getParentElement().getStyle()
+ .setVisibility(Visibility.HIDDEN);
+ client.updateVariable(id, "selected", tabKeys.get(tabIndex)
+ .toString(), true);
+ waitingForResponse = true;
+ }
+ // Note that we return true when tabIndex == activeTabIndex; the active
+ // tab could be selected, it's just a no-op.
+ return true;
+ }
+
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ public void tabSizeMightHaveChanged(Tab tab) {
+ // icon onloads may change total width of tabsheet
+ if (isDynamicWidth()) {
+ updateDynamicWidth();
+ }
+ updateTabScroller();
+
+ }
+
+ void sendTabClosedEvent(int tabIndex) {
+ client.updateVariable(id, "close", tabKeys.get(tabIndex), true);
+ }
+
+ boolean isDynamicWidth() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedWidth();
+ }
+
+ boolean isDynamicHeight() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedHeight();
+ }
+
+ public VTabsheet() {
+ super(CLASSNAME);
+
+ addHandler(this, FocusEvent.getType());
+ addHandler(this, BlurEvent.getType());
+
+ // Tab scrolling
+ DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+ tabs = DOM.createDiv();
+ DOM.setElementProperty(tabs, "className", TABS_CLASSNAME);
+ scroller = DOM.createDiv();
+
+ DOM.setElementProperty(scroller, "className", SCROLLER_CLASSNAME);
+ scrollerPrev = DOM.createButton();
+ DOM.setElementProperty(scrollerPrev, "className", SCROLLER_CLASSNAME
+ + "Prev");
+ DOM.sinkEvents(scrollerPrev, Event.ONCLICK);
+ scrollerNext = DOM.createButton();
+ DOM.setElementProperty(scrollerNext, "className", SCROLLER_CLASSNAME
+ + "Next");
+ DOM.sinkEvents(scrollerNext, Event.ONCLICK);
+ DOM.appendChild(getElement(), tabs);
+
+ // Tabs
+ tp.setStyleName(CLASSNAME + "-tabsheetpanel");
+ contentNode = DOM.createDiv();
+
+ deco = DOM.createDiv();
+
+ addStyleDependentName("loading"); // Indicate initial progress
+ tb.setStyleName(CLASSNAME + "-tabs");
+ DOM.setElementProperty(contentNode, "className", CLASSNAME + "-content");
+ DOM.setElementProperty(deco, "className", CLASSNAME + "-deco");
+
+ add(tb, tabs);
+ DOM.appendChild(scroller, scrollerPrev);
+ DOM.appendChild(scroller, scrollerNext);
+
+ DOM.appendChild(getElement(), contentNode);
+ add(tp, contentNode);
+ DOM.appendChild(getElement(), deco);
+
+ DOM.appendChild(tabs, scroller);
+
+ // TODO Use for Safari only. Fix annoying 1px first cell in TabBar.
+ // DOM.setStyleAttribute(DOM.getFirstChild(DOM.getFirstChild(DOM
+ // .getFirstChild(tb.getElement()))), "display", "none");
+
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+
+ // Tab scrolling
+ if (isScrolledTabs() && DOM.eventGetTarget(event) == scrollerPrev) {
+ int newFirstIndex = tb.scrollLeft(scrollerIndex);
+ if (newFirstIndex != -1) {
+ scrollerIndex = newFirstIndex;
+ updateTabScroller();
+ }
+ } else if (isClippedTabs() && DOM.eventGetTarget(event) == scrollerNext) {
+ int newFirstIndex = tb.scrollRight(scrollerIndex);
+
+ if (newFirstIndex != -1) {
+ scrollerIndex = newFirstIndex;
+ updateTabScroller();
+ }
+ } else {
+ super.onBrowserEvent(event);
+ }
+ }
+
+ /**
+ * Checks if the tab with the selected index has been scrolled out of the
+ * view (on the left side).
+ *
+ * @param index
+ * @return
+ */
+ private boolean scrolledOutOfView(int index) {
+ return scrollerIndex > index;
+ }
+
+ void handleStyleNames(UIDL uidl, ComponentState state) {
+ // Add proper stylenames for all elements (easier to prevent unwanted
+ // style inheritance)
+ if (state.hasStyles()) {
+ final List<String> styles = state.getStyles();
+ if (!currentStyle.equals(styles.toString())) {
+ currentStyle = styles.toString();
+ final String tabsBaseClass = TABS_CLASSNAME;
+ String tabsClass = tabsBaseClass;
+ final String contentBaseClass = CLASSNAME + "-content";
+ String contentClass = contentBaseClass;
+ final String decoBaseClass = CLASSNAME + "-deco";
+ String decoClass = decoBaseClass;
+ for (String style : styles) {
+ tb.addStyleDependentName(style);
+ tabsClass += " " + tabsBaseClass + "-" + style;
+ contentClass += " " + contentBaseClass + "-" + style;
+ decoClass += " " + decoBaseClass + "-" + style;
+ }
+ DOM.setElementProperty(tabs, "className", tabsClass);
+ DOM.setElementProperty(contentNode, "className", contentClass);
+ DOM.setElementProperty(deco, "className", decoClass);
+ borderW = -1;
+ }
+ } else {
+ tb.setStyleName(CLASSNAME + "-tabs");
+ DOM.setElementProperty(tabs, "className", TABS_CLASSNAME);
+ DOM.setElementProperty(contentNode, "className", CLASSNAME
+ + "-content");
+ DOM.setElementProperty(deco, "className", CLASSNAME + "-deco");
+ }
+
+ if (uidl.hasAttribute("hidetabs")) {
+ tb.setVisible(false);
+ addStyleName(CLASSNAME + "-hidetabs");
+ } else {
+ tb.setVisible(true);
+ removeStyleName(CLASSNAME + "-hidetabs");
+ }
+ }
+
+ void updateDynamicWidth() {
+ // Find width consumed by tabs
+ TableCellElement spacerCell = ((TableElement) tb.getElement().cast())
+ .getRows().getItem(0).getCells().getItem(tb.getTabCount());
+
+ int spacerWidth = spacerCell.getOffsetWidth();
+ DivElement div = (DivElement) spacerCell.getFirstChildElement();
+
+ int spacerMinWidth = spacerCell.getOffsetWidth() - div.getOffsetWidth();
+
+ int tabsWidth = tb.getOffsetWidth() - spacerWidth + spacerMinWidth;
+
+ // Find content width
+ Style style = tp.getElement().getStyle();
+ String overflow = style.getProperty("overflow");
+ style.setProperty("overflow", "hidden");
+ style.setPropertyPx("width", tabsWidth);
+
+ boolean hasTabs = tp.getWidgetCount() > 0;
+
+ Style wrapperstyle = null;
+ if (hasTabs) {
+ wrapperstyle = tp.getWidget(tp.getVisibleWidget()).getElement()
+ .getParentElement().getStyle();
+ wrapperstyle.setPropertyPx("width", tabsWidth);
+ }
+ // Get content width from actual widget
+
+ int contentWidth = 0;
+ if (hasTabs) {
+ contentWidth = tp.getWidget(tp.getVisibleWidget()).getOffsetWidth();
+ }
+ style.setProperty("overflow", overflow);
+
+ // Set widths to max(tabs,content)
+ if (tabsWidth < contentWidth) {
+ tabsWidth = contentWidth;
+ }
+
+ int outerWidth = tabsWidth + getContentAreaBorderWidth();
+
+ tabs.getStyle().setPropertyPx("width", outerWidth);
+ style.setPropertyPx("width", tabsWidth);
+ if (hasTabs) {
+ wrapperstyle.setPropertyPx("width", tabsWidth);
+ }
+
+ contentNode.getStyle().setPropertyPx("width", tabsWidth);
+ super.setWidth(outerWidth + "px");
+ updateOpenTabSize();
+ }
+
+ @Override
+ protected void renderTab(final UIDL tabUidl, int index, boolean selected,
+ boolean hidden) {
+ Tab tab = tb.getTab(index);
+ if (tab == null) {
+ tab = tb.addTab();
+ }
+ tab.updateFromUIDL(tabUidl);
+ tab.setEnabledOnServer((!disabledTabKeys.contains(tabKeys.get(index))));
+ tab.setHiddenOnServer(hidden);
+
+ if (scrolledOutOfView(index)) {
+ // Should not set tabs visible if they are scrolled out of view
+ hidden = true;
+ }
+ // Set the current visibility of the tab (in the browser)
+ tab.setVisible(!hidden);
+
+ /*
+ * Force the width of the caption container so the content will not wrap
+ * and tabs won't be too narrow in certain browsers
+ */
+ tab.recalculateCaptionWidth();
+
+ UIDL tabContentUIDL = null;
+ ComponentConnector tabContentPaintable = null;
+ Widget tabContentWidget = null;
+ if (tabUidl.getChildCount() > 0) {
+ tabContentUIDL = tabUidl.getChildUIDL(0);
+ tabContentPaintable = client.getPaintable(tabContentUIDL);
+ tabContentWidget = tabContentPaintable.getWidget();
+ }
+
+ if (tabContentPaintable != null) {
+ /* This is a tab with content information */
+
+ int oldIndex = tp.getWidgetIndex(tabContentWidget);
+ if (oldIndex != -1 && oldIndex != index) {
+ /*
+ * The tab has previously been rendered in another position so
+ * we must move the cached content to correct position
+ */
+ tp.insert(tabContentWidget, index);
+ }
+ } else {
+ /* A tab whose content has not yet been loaded */
+
+ /*
+ * Make sure there is a corresponding empty tab in tp. The same
+ * operation as the moving above but for not-loaded tabs.
+ */
+ if (index < tp.getWidgetCount()) {
+ Widget oldWidget = tp.getWidget(index);
+ if (!(oldWidget instanceof PlaceHolder)) {
+ tp.insert(new PlaceHolder(), index);
+ }
+ }
+
+ }
+
+ if (selected) {
+ renderContent(tabContentUIDL);
+ tb.selectTab(index);
+ } else {
+ if (tabContentUIDL != null) {
+ // updating a drawn child on hidden tab
+ if (tp.getWidgetIndex(tabContentWidget) < 0) {
+ tp.insert(tabContentWidget, index);
+ }
+ } else if (tp.getWidgetCount() <= index) {
+ tp.add(new PlaceHolder());
+ }
+ }
+ }
+
+ public class PlaceHolder extends VLabel {
+ public PlaceHolder() {
+ super("");
+ }
+ }
+
+ @Override
+ protected void selectTab(int index, final UIDL contentUidl) {
+ if (index != activeTabIndex) {
+ activeTabIndex = index;
+ tb.selectTab(activeTabIndex);
+ }
+ renderContent(contentUidl);
+ }
+
+ private void renderContent(final UIDL contentUIDL) {
+ final ComponentConnector content = client.getPaintable(contentUIDL);
+ Widget newWidget = content.getWidget();
+ if (tp.getWidgetCount() > activeTabIndex) {
+ Widget old = tp.getWidget(activeTabIndex);
+ if (old != newWidget) {
+ tp.remove(activeTabIndex);
+ ConnectorMap paintableMap = ConnectorMap.get(client);
+ if (paintableMap.isConnector(old)) {
+ paintableMap.unregisterConnector(paintableMap
+ .getConnector(old));
+ }
+ tp.insert(content.getWidget(), activeTabIndex);
+ }
+ } else {
+ tp.add(content.getWidget());
+ }
+
+ tp.showWidget(activeTabIndex);
+
+ VTabsheet.this.iLayout();
+ /*
+ * The size of a cached, relative sized component must be updated to
+ * report correct size to updateOpenTabSize().
+ */
+ if (contentUIDL.getBooleanAttribute("cached")) {
+ client.handleComponentRelativeSize(content.getWidget());
+ }
+ updateOpenTabSize();
+ VTabsheet.this.removeStyleDependentName("loading");
+ }
+
+ void updateContentNodeHeight() {
+ if (!isDynamicHeight()) {
+ int contentHeight = getOffsetHeight();
+ contentHeight -= DOM.getElementPropertyInt(deco, "offsetHeight");
+ contentHeight -= tb.getOffsetHeight();
+ if (contentHeight < 0) {
+ contentHeight = 0;
+ }
+
+ // Set proper values for content element
+ DOM.setStyleAttribute(contentNode, "height", contentHeight + "px");
+ } else {
+ DOM.setStyleAttribute(contentNode, "height", "");
+ }
+ }
+
+ public void iLayout() {
+ updateTabScroller();
+ }
+
+ /**
+ * Sets the size of the visible tab (component). As the tab is set to
+ * position: absolute (to work around a firefox flickering bug) we must keep
+ * this up-to-date by hand.
+ */
+ void updateOpenTabSize() {
+ /*
+ * The overflow=auto element must have a height specified, otherwise it
+ * will be just as high as the contents and no scrollbars will appear
+ */
+ int height = -1;
+ int width = -1;
+ int minWidth = 0;
+
+ if (!isDynamicHeight()) {
+ height = contentNode.getOffsetHeight();
+ }
+ if (!isDynamicWidth()) {
+ width = contentNode.getOffsetWidth() - getContentAreaBorderWidth();
+ } else {
+ /*
+ * If the tabbar is wider than the content we need to use the tabbar
+ * width as minimum width so scrollbars get placed correctly (at the
+ * right edge).
+ */
+ minWidth = tb.getOffsetWidth() - getContentAreaBorderWidth();
+ }
+ tp.fixVisibleTabSize(width, height, minWidth);
+
+ }
+
+ /**
+ * Layouts the tab-scroller elements, and applies styles.
+ */
+ private void updateTabScroller() {
+ if (!isDynamicWidth()) {
+ ComponentConnector paintable = ConnectorMap.get(client)
+ .getConnector(this);
+ DOM.setStyleAttribute(tabs, "width", paintable.getState()
+ .getWidth());
+ }
+
+ // Make sure scrollerIndex is valid
+ if (scrollerIndex < 0 || scrollerIndex > tb.getTabCount()) {
+ scrollerIndex = tb.getFirstVisibleTab();
+ } else if (tb.getTabCount() > 0
+ && tb.getTab(scrollerIndex).isHiddenOnServer()) {
+ scrollerIndex = tb.getNextVisibleTab(scrollerIndex);
+ }
+
+ boolean scrolled = isScrolledTabs();
+ boolean clipped = isClippedTabs();
+ if (tb.getTabCount() > 0 && tb.isVisible() && (scrolled || clipped)) {
+ DOM.setStyleAttribute(scroller, "display", "");
+ DOM.setElementProperty(scrollerPrev, "className",
+ SCROLLER_CLASSNAME + (scrolled ? "Prev" : "Prev-disabled"));
+ DOM.setElementProperty(scrollerNext, "className",
+ SCROLLER_CLASSNAME + (clipped ? "Next" : "Next-disabled"));
+ } else {
+ DOM.setStyleAttribute(scroller, "display", "none");
+ }
+
+ if (BrowserInfo.get().isSafari()) {
+ // fix tab height for safari, bugs sometimes if tabs contain icons
+ String property = tabs.getStyle().getProperty("height");
+ if (property == null || property.equals("")) {
+ tabs.getStyle().setPropertyPx("height", tb.getOffsetHeight());
+ }
+ /*
+ * another hack for webkits. tabscroller sometimes drops without
+ * "shaking it" reproducable in
+ * com.vaadin.tests.components.tabsheet.TabSheetIcons
+ */
+ final Style style = scroller.getStyle();
+ style.setProperty("whiteSpace", "normal");
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ style.setProperty("whiteSpace", "");
+ }
+ });
+ }
+
+ }
+
+ void showAllTabs() {
+ scrollerIndex = tb.getFirstVisibleTab();
+ for (int i = 0; i < tb.getTabCount(); i++) {
+ Tab t = tb.getTab(i);
+ if (!t.isHiddenOnServer()) {
+ t.setVisible(true);
+ }
+ }
+ }
+
+ private boolean isScrolledTabs() {
+ return scrollerIndex > tb.getFirstVisibleTab();
+ }
+
+ private boolean isClippedTabs() {
+ return (tb.getOffsetWidth() - DOM.getElementPropertyInt((Element) tb
+ .getContainerElement().getLastChild().cast(), "offsetWidth")) > getOffsetWidth()
+ - (isScrolledTabs() ? scroller.getOffsetWidth() : 0);
+ }
+
+ private boolean isClipped(Tab tab) {
+ return tab.getAbsoluteLeft() + tab.getOffsetWidth() > getAbsoluteLeft()
+ + getOffsetWidth() - scroller.getOffsetWidth();
+ }
+
+ @Override
+ protected void clearPaintables() {
+
+ int i = tb.getTabCount();
+ while (i > 0) {
+ tb.removeTab(--i);
+ }
+ tp.clear();
+
+ }
+
+ @Override
+ protected Iterator<Widget> getWidgetIterator() {
+ return tp.iterator();
+ }
+
+ private int borderW = -1;
+
+ int getContentAreaBorderWidth() {
+ if (borderW < 0) {
+ borderW = Util.measureHorizontalBorder(contentNode);
+ }
+ return borderW;
+ }
+
+ @Override
+ protected int getTabCount() {
+ return tb.getTabCount();
+ }
+
+ @Override
+ protected ComponentConnector getTab(int index) {
+ if (tp.getWidgetCount() > index) {
+ Widget widget = tp.getWidget(index);
+ return ConnectorMap.get(client).getConnector(widget);
+ }
+ return null;
+ }
+
+ @Override
+ protected void removeTab(int index) {
+ tb.removeTab(index);
+ /*
+ * This must be checked because renderTab automatically removes the
+ * active tab content when it changes
+ */
+ if (tp.getWidgetCount() > index) {
+ tp.remove(index);
+ }
+ }
+
+ public void onBlur(BlurEvent event) {
+ if (focusedTab != null && event.getSource() instanceof Tab) {
+ focusedTab = null;
+ if (client.hasEventListeners(this, EventId.BLUR)) {
+ client.updateVariable(id, EventId.BLUR, "", true);
+ }
+ }
+ }
+
+ public void onFocus(FocusEvent event) {
+ if (focusedTab == null && event.getSource() instanceof Tab) {
+ focusedTab = (Tab) event.getSource();
+ if (client.hasEventListeners(this, EventId.FOCUS)) {
+ client.updateVariable(id, EventId.FOCUS, "", true);
+ }
+ }
+ }
+
+ public void focus() {
+ tb.getTab(activeTabIndex).focus();
+ }
+
+ public void blur() {
+ tb.getTab(activeTabIndex).blur();
+ }
+
+ public void onKeyDown(KeyDownEvent event) {
+ if (event.getSource() instanceof Tab) {
+ int keycode = event.getNativeEvent().getKeyCode();
+
+ if (keycode == getPreviousTabKey()) {
+ selectPreviousTab();
+ } else if (keycode == getNextTabKey()) {
+ selectNextTab();
+ } else if (keycode == getCloseTabKey()) {
+ Tab tab = tb.getTab(activeTabIndex);
+ if (tab.isClosable()) {
+ tab.onClose();
+ }
+ }
+ }
+ }
+
+ /**
+ * @return The key code of the keyboard shortcut that selects the previous
+ * tab in a focused tabsheet.
+ */
+ protected int getPreviousTabKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * @return The key code of the keyboard shortcut that selects the next tab
+ * in a focused tabsheet.
+ */
+ protected int getNextTabKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * @return The key code of the keyboard shortcut that closes the currently
+ * selected tab in a focused tabsheet.
+ */
+ protected int getCloseTabKey() {
+ return KeyCodes.KEY_DELETE;
+ }
+
+ private void selectPreviousTab() {
+ int newTabIndex = activeTabIndex;
+ // Find the previous visible and enabled tab if any.
+ do {
+ newTabIndex--;
+ } while (newTabIndex >= 0 && !onTabSelected(newTabIndex));
+
+ if (newTabIndex >= 0) {
+ activeTabIndex = newTabIndex;
+ if (isScrolledTabs()) {
+ // Scroll until the new active tab is visible
+ int newScrollerIndex = scrollerIndex;
+ while (tb.getTab(activeTabIndex).getAbsoluteLeft() < getAbsoluteLeft()
+ && newScrollerIndex != -1) {
+ newScrollerIndex = tb.scrollLeft(newScrollerIndex);
+ }
+ scrollerIndex = newScrollerIndex;
+ updateTabScroller();
+ }
+ }
+ }
+
+ private void selectNextTab() {
+ int newTabIndex = activeTabIndex;
+ // Find the next visible and enabled tab if any.
+ do {
+ newTabIndex++;
+ } while (newTabIndex < getTabCount() && !onTabSelected(newTabIndex));
+
+ if (newTabIndex < getTabCount()) {
+ activeTabIndex = newTabIndex;
+ if (isClippedTabs()) {
+ // Scroll until the new active tab is completely visible
+ int newScrollerIndex = scrollerIndex;
+ while (isClipped(tb.getTab(activeTabIndex))
+ && newScrollerIndex != -1) {
+ newScrollerIndex = tb.scrollRight(newScrollerIndex);
+ }
+ scrollerIndex = newScrollerIndex;
+ updateTabScroller();
+ }
+ }
+ }
+}
--- /dev/null
- import com.vaadin.terminal.gwt.client.Util;
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.tabsheet;
+
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.event.dom.client.TouchStartEvent;
+import com.google.gwt.event.dom.client.TouchStartHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
- wrapperDiv.getStyle().setPropertyPx("height", height);
- runWebkitOverflowAutoFix();
- }
-
- public void runWebkitOverflowAutoFix() {
- if (visibleWidget != null) {
- Util.runWebkitOverflowAutoFix(DOM.getParent(visibleWidget
- .getElement()));
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
+
+/**
+ * A panel that displays all of its child widgets in a 'deck', where only one
+ * can be visible at a time. It is used by
+ * {@link com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheet}.
+ *
+ * This class has the same basic functionality as the GWT DeckPanel
+ * {@link com.google.gwt.user.client.ui.DeckPanel}, with the exception that it
+ * doesn't manipulate the child widgets' width and height attributes.
+ */
+public class VTabsheetPanel extends ComplexPanel {
+
+ private Widget visibleWidget;
+ private TouchScrollDelegate touchScrollDelegate;
+
+ /**
+ * Creates an empty tabsheet panel.
+ */
+ public VTabsheetPanel() {
+ setElement(DOM.createDiv());
+ sinkEvents(Event.TOUCHEVENTS);
+ addDomHandler(new TouchStartHandler() {
+ public void onTouchStart(TouchStartEvent event) {
+ /*
+ * All container elements needs to be scrollable by one finger.
+ * Update the scrollable element list of touch delegate on each
+ * touch start.
+ */
+ NodeList<Node> childNodes = getElement().getChildNodes();
+ Element[] elements = new Element[childNodes.getLength()];
+ for (int i = 0; i < elements.length; i++) {
+ elements[i] = (Element) childNodes.getItem(i);
+ }
+ getTouchScrollDelegate().setElements(elements);
+ getTouchScrollDelegate().onTouchStart(event);
+ }
+ }, TouchStartEvent.getType());
+ }
+
+ protected TouchScrollDelegate getTouchScrollDelegate() {
+ if (touchScrollDelegate == null) {
+ touchScrollDelegate = new TouchScrollDelegate();
+ }
+ return touchScrollDelegate;
+
+ }
+
+ /**
+ * Adds the specified widget to the deck.
+ *
+ * @param w
+ * the widget to be added
+ */
+ @Override
+ public void add(Widget w) {
+ Element el = createContainerElement();
+ DOM.appendChild(getElement(), el);
+ super.add(w, el);
+ }
+
+ private Element createContainerElement() {
+ Element el = DOM.createDiv();
+ DOM.setStyleAttribute(el, "position", "absolute");
+ DOM.setStyleAttribute(el, "overflow", "auto");
+ hide(el);
+ return el;
+ }
+
+ /**
+ * Gets the index of the currently-visible widget.
+ *
+ * @return the visible widget's index
+ */
+ public int getVisibleWidget() {
+ return getWidgetIndex(visibleWidget);
+ }
+
+ /**
+ * Inserts a widget before the specified index.
+ *
+ * @param w
+ * the widget to be inserted
+ * @param beforeIndex
+ * the index before which it will be inserted
+ * @throws IndexOutOfBoundsException
+ * if <code>beforeIndex</code> is out of range
+ */
+ public void insert(Widget w, int beforeIndex) {
+ Element el = createContainerElement();
+ DOM.insertChild(getElement(), el, beforeIndex);
+ super.insert(w, el, beforeIndex, false);
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ Element child = w.getElement();
+ Element parent = null;
+ if (child != null) {
+ parent = DOM.getParent(child);
+ }
+ final boolean removed = super.remove(w);
+ if (removed) {
+ if (visibleWidget == w) {
+ visibleWidget = null;
+ }
+ if (parent != null) {
+ DOM.removeChild(getElement(), parent);
+ }
+ }
+ return removed;
+ }
+
+ /**
+ * Shows the widget at the specified index. This causes the currently-
+ * visible widget to be hidden.
+ *
+ * @param index
+ * the index of the widget to be shown
+ */
+ public void showWidget(int index) {
+ checkIndexBoundsForAccess(index);
+ Widget newVisible = getWidget(index);
+ if (visibleWidget != newVisible) {
+ if (visibleWidget != null) {
+ hide(DOM.getParent(visibleWidget.getElement()));
+ }
+ visibleWidget = newVisible;
+ }
+ // Always ensure the selected tab is visible. If server prevents a tab
+ // change we might end up here with visibleWidget == newVisible but its
+ // parent is still hidden.
+ unHide(DOM.getParent(visibleWidget.getElement()));
+ }
+
+ private void hide(Element e) {
+ DOM.setStyleAttribute(e, "visibility", "hidden");
+ DOM.setStyleAttribute(e, "top", "-100000px");
+ DOM.setStyleAttribute(e, "left", "-100000px");
+ }
+
+ private void unHide(Element e) {
+ DOM.setStyleAttribute(e, "top", "0px");
+ DOM.setStyleAttribute(e, "left", "0px");
+ DOM.setStyleAttribute(e, "visibility", "");
+ }
+
+ public void fixVisibleTabSize(int width, int height, int minWidth) {
+ if (visibleWidget == null) {
+ return;
+ }
+
+ boolean dynamicHeight = false;
+
+ if (height < 0) {
+ height = visibleWidget.getOffsetHeight();
+ dynamicHeight = true;
+ }
+ if (width < 0) {
+ width = visibleWidget.getOffsetWidth();
+ }
+ if (width < minWidth) {
+ width = minWidth;
+ }
+
+ Element wrapperDiv = (Element) visibleWidget.getElement()
+ .getParentElement();
+
+ // width first
+ getElement().getStyle().setPropertyPx("width", width);
+ wrapperDiv.getStyle().setPropertyPx("width", width);
+
+ if (dynamicHeight) {
+ // height of widget might have changed due wrapping
+ height = visibleWidget.getOffsetHeight();
+ }
+ // v-tabsheet-tabsheetpanel height
+ getElement().getStyle().setPropertyPx("height", height);
+
+ // widget wrapper height
-
++ if (dynamicHeight) {
++ wrapperDiv.getStyle().clearHeight();
++ } else {
++ // widget wrapper height
++ wrapperDiv.getStyle().setPropertyPx("height", height);
+ }
+ }
+
+ public void replaceComponent(Widget oldComponent, Widget newComponent) {
+ boolean isVisible = (visibleWidget == oldComponent);
+ int widgetIndex = getWidgetIndex(oldComponent);
+ remove(oldComponent);
+ insert(newComponent, widgetIndex);
+ if (isVisible) {
+ showWidget(widgetIndex);
+ }
+ }
+}
--- /dev/null
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.twincolselect;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.optiongroup.OptionGroupBaseConnector;
+import com.vaadin.ui.TwinColSelect;
+
+@Component(TwinColSelect.class)
+public class TwinColSelectConnector extends OptionGroupBaseConnector implements
+ DirectionalManagedLayout {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Captions are updated before super call to ensure the widths are set
+ // correctly
+ if (isRealUpdate(uidl)) {
+ getWidget().updateCaptions(uidl);
+ getLayoutManager().setWidthNeedsUpdate(this);
+ }
+
+ super.updateFromUIDL(uidl, client);
+ }
+
+ @Override
+ protected void init() {
+ getLayoutManager().registerDependency(this,
+ getWidget().captionWrapper.getElement());
+ }
+
++ @Override
++ public void onUnregister() {
++ getLayoutManager().unregisterDependency(this,
++ getWidget().captionWrapper.getElement());
++ }
++
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VTwinColSelect.class);
+ }
+
+ @Override
+ public VTwinColSelect getWidget() {
+ return (VTwinColSelect) super.getWidget();
+ }
+
+ public void layoutVertically() {
+ if (isUndefinedHeight()) {
+ getWidget().clearInternalHeights();
+ } else {
+ getWidget().setInternalHeights();
+ }
+ }
+
+ public void layoutHorizontally() {
+ if (isUndefinedWidth()) {
+ getWidget().clearInternalWidths();
+ } else {
+ getWidget().setInternalWidths();
+ }
+ }
+}
--- /dev/null
- Util.runWebkitOverflowAutoFix(contentPanel.getElement());
- client.doLayout(false);
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.window;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.ScrollEvent;
+import com.google.gwt.event.dom.client.ScrollHandler;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Console;
+import com.vaadin.terminal.gwt.client.EventId;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+
+/**
+ * "Sub window" component.
+ *
+ * @author Vaadin Ltd
+ */
+public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
+ ScrollHandler, KeyDownHandler, FocusHandler, BlurHandler, Focusable {
+
+ /**
+ * Minimum allowed height of a window. This refers to the content area, not
+ * the outer borders.
+ */
+ private static final int MIN_CONTENT_AREA_HEIGHT = 100;
+
+ /**
+ * Minimum allowed width of a window. This refers to the content area, not
+ * the outer borders.
+ */
+ private static final int MIN_CONTENT_AREA_WIDTH = 150;
+
+ private static ArrayList<VWindow> windowOrder = new ArrayList<VWindow>();
+
+ private static boolean orderingDefered;
+
+ public static final String CLASSNAME = "v-window";
+
+ private static final int STACKING_OFFSET_PIXELS = 15;
+
+ public static final int Z_INDEX = 10000;
+
+ ComponentConnector layout;
+
+ Element contents;
+
+ Element header;
+
+ Element footer;
+
+ private Element resizeBox;
+
+ final FocusableScrollPanel contentPanel = new FocusableScrollPanel();
+
+ private boolean dragging;
+
+ private int startX;
+
+ private int startY;
+
+ private int origX;
+
+ private int origY;
+
+ private boolean resizing;
+
+ private int origW;
+
+ private int origH;
+
+ Element closeBox;
+
+ protected ApplicationConnection client;
+
+ String id;
+
+ ShortcutActionHandler shortcutHandler;
+
+ /** Last known positionx read from UIDL or updated to application connection */
+ private int uidlPositionX = -1;
+
+ /** Last known positiony read from UIDL or updated to application connection */
+ private int uidlPositionY = -1;
+
+ boolean vaadinModality = false;
+
+ boolean resizable = true;
+
+ private boolean draggable = true;
+
+ boolean resizeLazy = false;
+
+ private Element modalityCurtain;
+ private Element draggingCurtain;
+ private Element resizingCurtain;
+
+ private Element headerText;
+
+ private boolean closable = true;
+
+ // If centered (via UIDL), the window should stay in the centered -mode
+ // until a position is received from the server, or the user moves or
+ // resizes the window.
+ boolean centered = false;
+
+ boolean immediate;
+
+ private Element wrapper;
+
+ boolean visibilityChangesDisabled;
+
+ int bringToFrontSequence = -1;
+
+ private VLazyExecutor delayedContentsSizeUpdater = new VLazyExecutor(200,
+ new ScheduledCommand() {
+
+ public void execute() {
+ updateContentsSize();
+ }
+ });
+
+ public VWindow() {
+ super(false, false, true); // no autohide, not modal, shadow
+ // Different style of shadow for windows
+ setShadowStyle("window");
+
+ constructDOM();
+ contentPanel.addScrollHandler(this);
+ contentPanel.addKeyDownHandler(this);
+ contentPanel.addFocusHandler(this);
+ contentPanel.addBlurHandler(this);
+ }
+
+ public void bringToFront() {
+ int curIndex = windowOrder.indexOf(this);
+ if (curIndex + 1 < windowOrder.size()) {
+ windowOrder.remove(this);
+ windowOrder.add(this);
+ for (; curIndex < windowOrder.size(); curIndex++) {
+ windowOrder.get(curIndex).setWindowOrder(curIndex);
+ }
+ }
+ }
+
+ /**
+ * Returns true if this window is the topmost VWindow
+ *
+ * @return
+ */
+ private boolean isActive() {
+ return equals(getTopmostWindow());
+ }
+
+ private static VWindow getTopmostWindow() {
+ return windowOrder.get(windowOrder.size() - 1);
+ }
+
+ void setWindowOrderAndPosition() {
+ // This cannot be done in the constructor as the widgets are created in
+ // a different order than on they should appear on screen
+ if (windowOrder.contains(this)) {
+ // Already set
+ return;
+ }
+ final int order = windowOrder.size();
+ setWindowOrder(order);
+ windowOrder.add(this);
+ setPopupPosition(order * STACKING_OFFSET_PIXELS, order
+ * STACKING_OFFSET_PIXELS);
+
+ }
+
+ private void setWindowOrder(int order) {
+ setZIndex(order + Z_INDEX);
+ }
+
+ @Override
+ protected void setZIndex(int zIndex) {
+ super.setZIndex(zIndex);
+ if (vaadinModality) {
+ DOM.setStyleAttribute(getModalityCurtain(), "zIndex", "" + zIndex);
+ }
+ }
+
+ protected Element getModalityCurtain() {
+ if (modalityCurtain == null) {
+ modalityCurtain = DOM.createDiv();
+ modalityCurtain.setClassName(CLASSNAME + "-modalitycurtain");
+ }
+ return modalityCurtain;
+ }
+
+ protected void constructDOM() {
+ setStyleName(CLASSNAME);
+
+ header = DOM.createDiv();
+ DOM.setElementProperty(header, "className", CLASSNAME + "-outerheader");
+ headerText = DOM.createDiv();
+ DOM.setElementProperty(headerText, "className", CLASSNAME + "-header");
+ contents = DOM.createDiv();
+ DOM.setElementProperty(contents, "className", CLASSNAME + "-contents");
+ footer = DOM.createDiv();
+ DOM.setElementProperty(footer, "className", CLASSNAME + "-footer");
+ resizeBox = DOM.createDiv();
+ DOM.setElementProperty(resizeBox, "className", CLASSNAME + "-resizebox");
+ closeBox = DOM.createDiv();
+ DOM.setElementProperty(closeBox, "className", CLASSNAME + "-closebox");
+ DOM.appendChild(footer, resizeBox);
+
+ wrapper = DOM.createDiv();
+ DOM.setElementProperty(wrapper, "className", CLASSNAME + "-wrap");
+
+ DOM.appendChild(wrapper, header);
+ DOM.appendChild(wrapper, closeBox);
+ DOM.appendChild(header, headerText);
+ DOM.appendChild(wrapper, contents);
+ DOM.appendChild(wrapper, footer);
+ DOM.appendChild(super.getContainerElement(), wrapper);
+
+ sinkEvents(Event.MOUSEEVENTS | Event.TOUCHEVENTS | Event.ONCLICK
+ | Event.ONLOSECAPTURE);
+
+ setWidget(contentPanel);
+
+ }
+
+ /**
+ * Calling this method will defer ordering algorithm, to order windows based
+ * on servers bringToFront and modality instructions. Non changed windows
+ * will be left intact.
+ */
+ static void deferOrdering() {
+ if (!orderingDefered) {
+ orderingDefered = true;
+ Scheduler.get().scheduleFinally(new Command() {
+ public void execute() {
+ doServerSideOrdering();
+ }
+ });
+ }
+ }
+
+ private static void doServerSideOrdering() {
+ orderingDefered = false;
+ VWindow[] array = windowOrder.toArray(new VWindow[windowOrder.size()]);
+ Arrays.sort(array, new Comparator<VWindow>() {
+ public int compare(VWindow o1, VWindow o2) {
+ /*
+ * Order by modality, then by bringtofront sequence.
+ */
+
+ if (o1.vaadinModality && !o2.vaadinModality) {
+ return 1;
+ } else if (!o1.vaadinModality && o2.vaadinModality) {
+ return -1;
+ } else if (o1.bringToFrontSequence > o2.bringToFrontSequence) {
+ return 1;
+ } else if (o1.bringToFrontSequence < o2.bringToFrontSequence) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+ });
+ for (int i = 0; i < array.length; i++) {
+ VWindow w = array[i];
+ if (w.bringToFrontSequence != -1 || w.vaadinModality) {
+ w.bringToFront();
+ w.bringToFrontSequence = -1;
+ }
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ /*
+ * Visibility with VWindow works differently than with other Paintables
+ * in Vaadin. Invisible VWindows are not attached to DOM at all. Flag is
+ * used to avoid visibility call from
+ * ApplicationConnection.updateComponent();
+ */
+ if (!visibilityChangesDisabled) {
+ super.setVisible(visible);
+ }
+ }
+
+ void setDraggable(boolean draggable) {
+ if (this.draggable == draggable) {
+ return;
+ }
+
+ this.draggable = draggable;
+
+ setCursorProperties();
+ }
+
+ private void setCursorProperties() {
+ if (!draggable) {
+ header.getStyle().setProperty("cursor", "default");
+ footer.getStyle().setProperty("cursor", "default");
+ } else {
+ header.getStyle().setProperty("cursor", "");
+ footer.getStyle().setProperty("cursor", "");
+ }
+ }
+
+ /**
+ * Sets the closable state of the window. Additionally hides/shows the close
+ * button according to the new state.
+ *
+ * @param closable
+ * true if the window can be closed by the user
+ */
+ protected void setClosable(boolean closable) {
+ if (this.closable == closable) {
+ return;
+ }
+
+ this.closable = closable;
+ if (closable) {
+ DOM.setStyleAttribute(closeBox, "display", "");
+ } else {
+ DOM.setStyleAttribute(closeBox, "display", "none");
+ }
+
+ }
+
+ /**
+ * Returns the closable state of the sub window. If the sub window is
+ * closable a decoration (typically an X) is shown to the user. By clicking
+ * on the X the user can close the window.
+ *
+ * @return true if the sub window is closable
+ */
+ protected boolean isClosable() {
+ return closable;
+ }
+
+ @Override
+ public void show() {
+ if (!windowOrder.contains(this)) {
+ // This is needed if the window is hidden and then shown again.
+ // Otherwise this VWindow is added to windowOrder in the
+ // constructor.
+ windowOrder.add(this);
+ }
+
+ if (vaadinModality) {
+ showModalityCurtain();
+ }
+ super.show();
+ }
+
+ @Override
+ public void hide() {
+ if (vaadinModality) {
+ hideModalityCurtain();
+ }
+ super.hide();
+
+ // Remove window from windowOrder to avoid references being left
+ // hanging.
+ windowOrder.remove(this);
+ }
+
+ void setVaadinModality(boolean modality) {
+ vaadinModality = modality;
+ if (vaadinModality) {
+ if (isAttached()) {
+ showModalityCurtain();
+ }
+ deferOrdering();
+ } else {
+ if (modalityCurtain != null) {
+ if (isAttached()) {
+ hideModalityCurtain();
+ }
+ modalityCurtain = null;
+ }
+ }
+ }
+
+ private void showModalityCurtain() {
+ DOM.setStyleAttribute(getModalityCurtain(), "zIndex",
+ "" + (windowOrder.indexOf(this) + Z_INDEX));
+ if (isShowing()) {
+ RootPanel.getBodyElement().insertBefore(getModalityCurtain(),
+ getElement());
+ } else {
+ DOM.appendChild(RootPanel.getBodyElement(), getModalityCurtain());
+ }
+ }
+
+ private void hideModalityCurtain() {
+ DOM.removeChild(RootPanel.getBodyElement(), modalityCurtain);
+ }
+
+ /*
+ * Shows an empty div on top of all other content; used when moving, so that
+ * iframes (etc) do not steal event.
+ */
+ private void showDraggingCurtain() {
+ DOM.appendChild(RootPanel.getBodyElement(), getDraggingCurtain());
+ }
+
+ private void hideDraggingCurtain() {
+ if (draggingCurtain != null) {
+ DOM.removeChild(RootPanel.getBodyElement(), draggingCurtain);
+ }
+ }
+
+ /*
+ * Shows an empty div on top of all other content; used when resizing, so
+ * that iframes (etc) do not steal event.
+ */
+ private void showResizingCurtain() {
+ DOM.appendChild(RootPanel.getBodyElement(), getResizingCurtain());
+ }
+
+ private void hideResizingCurtain() {
+ if (resizingCurtain != null) {
+ DOM.removeChild(RootPanel.getBodyElement(), resizingCurtain);
+ }
+ }
+
+ private Element getDraggingCurtain() {
+ if (draggingCurtain == null) {
+ draggingCurtain = createCurtain();
+ draggingCurtain.setClassName(CLASSNAME + "-draggingCurtain");
+ }
+
+ return draggingCurtain;
+ }
+
+ private Element getResizingCurtain() {
+ if (resizingCurtain == null) {
+ resizingCurtain = createCurtain();
+ resizingCurtain.setClassName(CLASSNAME + "-resizingCurtain");
+ }
+
+ return resizingCurtain;
+ }
+
+ private Element createCurtain() {
+ Element curtain = DOM.createDiv();
+
+ DOM.setStyleAttribute(curtain, "position", "absolute");
+ DOM.setStyleAttribute(curtain, "top", "0px");
+ DOM.setStyleAttribute(curtain, "left", "0px");
+ DOM.setStyleAttribute(curtain, "width", "100%");
+ DOM.setStyleAttribute(curtain, "height", "100%");
+ DOM.setStyleAttribute(curtain, "zIndex", "" + VOverlay.Z_INDEX);
+
+ return curtain;
+ }
+
+ void setResizable(boolean resizability) {
+ resizable = resizability;
+ if (resizability) {
+ DOM.setElementProperty(footer, "className", CLASSNAME + "-footer");
+ DOM.setElementProperty(resizeBox, "className", CLASSNAME
+ + "-resizebox");
+ } else {
+ DOM.setElementProperty(footer, "className", CLASSNAME + "-footer "
+ + CLASSNAME + "-footer-noresize");
+ DOM.setElementProperty(resizeBox, "className", CLASSNAME
+ + "-resizebox " + CLASSNAME + "-resizebox-disabled");
+ }
+ }
+
+ @Override
+ public void setPopupPosition(int left, int top) {
+ if (top < 0) {
+ // ensure window is not moved out of browser window from top of the
+ // screen
+ top = 0;
+ }
+ super.setPopupPosition(left, top);
+ if (left != uidlPositionX && client != null) {
+ client.updateVariable(id, "positionx", left, false);
+ uidlPositionX = left;
+ }
+ if (top != uidlPositionY && client != null) {
+ client.updateVariable(id, "positiony", top, false);
+ uidlPositionY = top;
+ }
+ }
+
+ public void setCaption(String c) {
+ setCaption(c, null);
+ }
+
+ public void setCaption(String c, String icon) {
+ String html = Util.escapeHTML(c);
+ if (icon != null) {
+ icon = client.translateVaadinUri(icon);
+ html = "<img src=\"" + Util.escapeAttribute(icon)
+ + "\" class=\"v-icon\" />" + html;
+ }
+ DOM.setInnerHTML(headerText, html);
+ }
+
+ @Override
+ protected Element getContainerElement() {
+ // in GWT 1.5 this method is used in PopupPanel constructor
+ if (contents == null) {
+ return super.getContainerElement();
+ }
+ return contents;
+ }
+
+ @Override
+ public void onBrowserEvent(final Event event) {
+ boolean bubble = true;
+
+ final int type = event.getTypeInt();
+
+ final Element target = DOM.eventGetTarget(event);
+
+ if (client != null && header.isOrHasChild(target)) {
+ // Handle window caption tooltips
+ client.handleTooltipEvent(event, this);
+ }
+
+ if (resizing || resizeBox == target) {
+ onResizeEvent(event);
+ bubble = false;
+ } else if (isClosable() && target == closeBox) {
+ if (type == Event.ONCLICK) {
+ onCloseClick();
+ }
+ bubble = false;
+ } else if (dragging || !contents.isOrHasChild(target)) {
+ onDragEvent(event);
+ bubble = false;
+ } else if (type == Event.ONCLICK) {
+ // clicked inside window, ensure to be on top
+ if (!isActive()) {
+ bringToFront();
+ }
+ }
+
+ /*
+ * If clicking on other than the content, move focus to the window.
+ * After that this windows e.g. gets all keyboard shortcuts.
+ */
+ if (type == Event.ONMOUSEDOWN
+ && !contentPanel.getElement().isOrHasChild(target)
+ && target != closeBox) {
+ contentPanel.focus();
+ }
+
+ if (!bubble) {
+ event.stopPropagation();
+ } else {
+ // Super.onBrowserEvent takes care of Handlers added by the
+ // ClickEventHandler
+ super.onBrowserEvent(event);
+ }
+ }
+
+ private void onCloseClick() {
+ client.updateVariable(id, "close", true, true);
+ }
+
+ private void onResizeEvent(Event event) {
+ if (resizable && Util.isTouchEventOrLeftMouseButton(event)) {
+ switch (event.getTypeInt()) {
+ case Event.ONMOUSEDOWN:
+ case Event.ONTOUCHSTART:
+ if (!isActive()) {
+ bringToFront();
+ }
+ showResizingCurtain();
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(resizeBox, "visibility", "hidden");
+ }
+ resizing = true;
+ startX = Util.getTouchOrMouseClientX(event);
+ startY = Util.getTouchOrMouseClientY(event);
+ origW = getElement().getOffsetWidth();
+ origH = getElement().getOffsetHeight();
+ DOM.setCapture(getElement());
+ event.preventDefault();
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ setSize(event, true);
+ case Event.ONTOUCHCANCEL:
+ DOM.releaseCapture(getElement());
+ case Event.ONLOSECAPTURE:
+ hideResizingCurtain();
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(resizeBox, "visibility", "");
+ }
+ resizing = false;
+ break;
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ if (resizing) {
+ centered = false;
+ setSize(event, false);
+ event.preventDefault();
+ }
+ break;
+ default:
+ event.preventDefault();
+ break;
+ }
+ }
+ }
+
+ /**
+ * TODO check if we need to support this with touch based devices.
+ *
+ * Checks if the cursor was inside the browser content area when the event
+ * happened.
+ *
+ * @param event
+ * The event to be checked
+ * @return true, if the cursor is inside the browser content area
+ *
+ * false, otherwise
+ */
+ private boolean cursorInsideBrowserContentArea(Event event) {
+ if (event.getClientX() < 0 || event.getClientY() < 0) {
+ // Outside to the left or above
+ return false;
+ }
+
+ if (event.getClientX() > Window.getClientWidth()
+ || event.getClientY() > Window.getClientHeight()) {
+ // Outside to the right or below
+ return false;
+ }
+
+ return true;
+ }
+
+ private void setSize(Event event, boolean updateVariables) {
+ if (!cursorInsideBrowserContentArea(event)) {
+ // Only drag while cursor is inside the browser client area
+ return;
+ }
+
+ int w = Util.getTouchOrMouseClientX(event) - startX + origW;
+ int minWidth = getMinWidth();
+ if (w < minWidth) {
+ w = minWidth;
+ }
+
+ int h = Util.getTouchOrMouseClientY(event) - startY + origH;
+ int minHeight = getMinHeight();
+ if (h < minHeight) {
+ h = minHeight;
+ }
+
+ setWidth(w + "px");
+ setHeight(h + "px");
+
+ if (updateVariables) {
+ // sending width back always as pixels, no need for unit
+ client.updateVariable(id, "width", w, false);
+ client.updateVariable(id, "height", h, immediate);
+ }
+
+ if (updateVariables || !resizeLazy) {
+ // Resize has finished or is not lazy
+ updateContentsSize();
+ } else {
+ // Lazy resize - wait for a while before re-rendering contents
+ delayedContentsSizeUpdater.trigger();
+ }
+ }
+
+ private void updateContentsSize() {
+ // Update child widget dimensions
+ if (client != null) {
+ client.handleComponentRelativeSize(layout.getWidget());
+ client.runDescendentsLayout((HasWidgets) layout.getWidget());
+ }
+
++ LayoutManager layoutManager = LayoutManager.get(client);
++ layoutManager.setNeedsMeasure(ConnectorMap.get(client).getConnector(
++ this));
++ layoutManager.layoutNow();
+ }
+
+ @Override
+ public void setWidth(String width) {
+ // Override PopupPanel which sets the width to the contents
+ getElement().getStyle().setProperty("width", width);
+ // Update v-has-width in case undefined window is resized
+ setStyleName("v-has-width", width != null && width.length() > 0);
+ }
+
+ @Override
+ public void setHeight(String height) {
+ // Override PopupPanel which sets the height to the contents
+ getElement().getStyle().setProperty("height", height);
+ // Update v-has-height in case undefined window is resized
+ setStyleName("v-has-height", height != null && height.length() > 0);
+ }
+
+ private void onDragEvent(Event event) {
+ if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ return;
+ }
+
+ switch (DOM.eventGetType(event)) {
+ case Event.ONTOUCHSTART:
+ if (event.getTouches().length() > 1) {
+ return;
+ }
+ case Event.ONMOUSEDOWN:
+ if (!isActive()) {
+ bringToFront();
+ }
+ beginMovingWindow(event);
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ case Event.ONLOSECAPTURE:
+ stopMovingWindow();
+ break;
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ moveWindow(event);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void moveWindow(Event event) {
+ if (dragging) {
+ centered = false;
+ if (cursorInsideBrowserContentArea(event)) {
+ // Only drag while cursor is inside the browser client area
+ final int x = Util.getTouchOrMouseClientX(event) - startX
+ + origX;
+ final int y = Util.getTouchOrMouseClientY(event) - startY
+ + origY;
+ setPopupPosition(x, y);
+ }
+ DOM.eventPreventDefault(event);
+ }
+ }
+
+ private void beginMovingWindow(Event event) {
+ if (draggable) {
+ showDraggingCurtain();
+ dragging = true;
+ startX = Util.getTouchOrMouseClientX(event);
+ startY = Util.getTouchOrMouseClientY(event);
+ origX = DOM.getAbsoluteLeft(getElement());
+ origY = DOM.getAbsoluteTop(getElement());
+ DOM.setCapture(getElement());
+ DOM.eventPreventDefault(event);
+ }
+ }
+
+ private void stopMovingWindow() {
+ dragging = false;
+ hideDraggingCurtain();
+ DOM.releaseCapture(getElement());
+ }
+
+ @Override
+ public boolean onEventPreview(Event event) {
+ if (dragging) {
+ onDragEvent(event);
+ return false;
+ } else if (resizing) {
+ onResizeEvent(event);
+ return false;
+ }
+
+ // TODO This is probably completely unnecessary as the modality curtain
+ // prevents events from reaching other windows and any security check
+ // must be done on the server side and not here.
+ // The code here is also run many times as each VWindow has an event
+ // preview but we cannot check only the current VWindow here (e.g.
+ // if(isTopMost) {...}) because PopupPanel will cause all events that
+ // are not cancelled here and target this window to be consume():d
+ // meaning the event won't be sent to the rest of the preview handlers.
+
+ if (getTopmostWindow().vaadinModality) {
+ // Topmost window is modal. Cancel the event if it targets something
+ // outside that window (except debug console...)
+ if (DOM.getCaptureElement() != null) {
+ // Allow events when capture is set
+ return true;
+ }
+
+ final Element target = event.getEventTarget().cast();
+ if (!DOM.isOrHasChild(getTopmostWindow().getElement(), target)) {
+ // not within the modal window, but let's see if it's in the
+ // debug window
+ Widget w = Util.findWidget(target, null);
+ while (w != null) {
+ if (w instanceof Console) {
+ return true; // allow debug-window clicks
+ } else if (ConnectorMap.get(client).isConnector(w)) {
+ return false;
+ }
+ w = w.getParent();
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void addStyleDependentName(String styleSuffix) {
+ // VWindow's getStyleElement() does not return the same element as
+ // getElement(), so we need to override this.
+ setStyleName(getElement(), getStylePrimaryName() + "-" + styleSuffix,
+ true);
+ }
+
+ public ShortcutActionHandler getShortcutActionHandler() {
+ return shortcutHandler;
+ }
+
+ public void onScroll(ScrollEvent event) {
+ client.updateVariable(id, "scrollTop",
+ contentPanel.getScrollPosition(), false);
+ client.updateVariable(id, "scrollLeft",
+ contentPanel.getHorizontalScrollPosition(), false);
+
+ }
+
+ public void onKeyDown(KeyDownEvent event) {
+ if (shortcutHandler != null) {
+ shortcutHandler
+ .handleKeyboardEvent(Event.as(event.getNativeEvent()));
+ return;
+ }
+ }
+
+ public void onBlur(BlurEvent event) {
+ if (client.hasEventListeners(this, EventId.BLUR)) {
+ client.updateVariable(id, EventId.BLUR, "", true);
+ }
+ }
+
+ public void onFocus(FocusEvent event) {
+ if (client.hasEventListeners(this, EventId.FOCUS)) {
+ client.updateVariable(id, EventId.FOCUS, "", true);
+ }
+ }
+
+ public void focus() {
+ contentPanel.focus();
+ }
+
+ public int getMinHeight() {
+ return MIN_CONTENT_AREA_HEIGHT + getDecorationHeight();
+ }
+
+ private int getDecorationHeight() {
+ LayoutManager layoutManager = layout.getLayoutManager();
+ return layoutManager.getOuterHeight(getElement())
+ - layoutManager.getInnerHeight(contentPanel.getElement());
+ }
+
+ public int getMinWidth() {
+ return MIN_CONTENT_AREA_WIDTH + getDecorationWidth();
+ }
+
+ private int getDecorationWidth() {
+ LayoutManager layoutManager = layout.getLayoutManager();
+ return layoutManager.getOuterWidth(getElement())
+ - layoutManager.getInnerWidth(contentPanel.getElement());
+ }
+
+}
--- /dev/null
- import com.vaadin.terminal.gwt.client.Util;
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.window;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.MouseEventDetails;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
- SimpleManagedLayout, PostLayoutListener {
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.Component;
+import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
++import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+
+@Component(value = com.vaadin.ui.Window.class)
+public class WindowConnector extends AbstractComponentContainerConnector
+ implements Paintable, BeforeShortcutActionListener,
-
- Util.runWebkitOverflowAutoFix(window.contentPanel.getElement());
++ SimpleManagedLayout, PostLayoutListener, MayScrollChildren {
+
+ private ClickEventHandler clickEventHandler = new ClickEventHandler(this) {
+ @Override
+ protected void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails) {
+ rpc.click(mouseDetails);
+ }
+ };
+
+ private WindowServerRPC rpc;
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ };
+
+ @Override
+ protected void init() {
+ super.init();
+ rpc = RpcProxy.create(WindowServerRPC.class, this);
+
+ getLayoutManager().registerDependency(this,
+ getWidget().contentPanel.getElement());
+ getLayoutManager().registerDependency(this, getWidget().header);
+ getLayoutManager().registerDependency(this, getWidget().footer);
+ }
+
++ @Override
++ public void onUnregister() {
++ LayoutManager lm = getLayoutManager();
++ VWindow window = getWidget();
++ lm.unregisterDependency(this, window.contentPanel.getElement());
++ lm.unregisterDependency(this, window.header);
++ lm.unregisterDependency(this, window.footer);
++ }
++
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().id = getConnectorId();
+ getWidget().client = client;
+
+ // Workaround needed for Testing Tools (GWT generates window DOM
+ // slightly different in different browsers).
+ DOM.setElementProperty(getWidget().closeBox, "id", getConnectorId()
+ + "_window_close");
+
+ if (isRealUpdate(uidl)) {
+ if (getState().isModal() != getWidget().vaadinModality) {
+ getWidget().setVaadinModality(!getWidget().vaadinModality);
+ }
+ if (!getWidget().isAttached()) {
+ getWidget().setVisible(false); // hide until
+ // possible centering
+ getWidget().show();
+ }
+ if (getState().isResizable() != getWidget().resizable) {
+ getWidget().setResizable(getState().isResizable());
+ }
+ getWidget().resizeLazy = getState().isResizeLazy();
+
+ getWidget().setDraggable(getState().isDraggable());
+
+ // Caption must be set before required header size is measured. If
+ // the caption attribute is missing the caption should be cleared.
+ String iconURL = null;
+ if (getState().getIcon() != null) {
+ iconURL = getState().getIcon().getURL();
+ }
+ getWidget().setCaption(getState().getCaption(), iconURL);
+ }
+
+ getWidget().visibilityChangesDisabled = true;
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+ getWidget().visibilityChangesDisabled = false;
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ getWidget().immediate = getState().isImmediate();
+
+ getWidget().setClosable(!isReadOnly());
+
+ // Initialize the position form UIDL
+ int positionx = getState().getPositionX();
+ int positiony = getState().getPositionY();
+ if (positionx >= 0 || positiony >= 0) {
+ if (positionx < 0) {
+ positionx = 0;
+ }
+ if (positiony < 0) {
+ positiony = 0;
+ }
+ getWidget().setPopupPosition(positionx, positiony);
+ }
+
+ int childIndex = 0;
+
+ // we may have actions
+ for (int i = 0; i < uidl.getChildCount(); i++) {
+ UIDL childUidl = uidl.getChildUIDL(i);
+ if (childUidl.getTag().equals("actions")) {
+ if (getWidget().shortcutHandler == null) {
+ getWidget().shortcutHandler = new ShortcutActionHandler(
+ getConnectorId(), client);
+ }
+ getWidget().shortcutHandler.updateActionMap(childUidl);
+ }
+
+ }
+
+ // setting scrollposition must happen after children is rendered
+ getWidget().contentPanel.setScrollPosition(getState().getScrollTop());
+ getWidget().contentPanel.setHorizontalScrollPosition(getState()
+ .getScrollLeft());
+
+ // Center this window on screen if requested
+ // This had to be here because we might not know the content size before
+ // everything is painted into the window
+
+ // centered is this is unset on move/resize
+ getWidget().centered = getState().isCentered();
+ getWidget().setVisible(true);
+
+ // ensure window is not larger than browser window
+ if (getWidget().getOffsetWidth() > Window.getClientWidth()) {
+ getWidget().setWidth(Window.getClientWidth() + "px");
+ }
+ if (getWidget().getOffsetHeight() > Window.getClientHeight()) {
+ getWidget().setHeight(Window.getClientHeight() + "px");
+ }
+
+ if (uidl.hasAttribute("bringToFront")) {
+ /*
+ * Focus as a side-effect. Will be overridden by
+ * ApplicationConnection if another component was focused by the
+ * server side.
+ */
+ getWidget().contentPanel.focus();
+ getWidget().bringToFrontSequence = uidl
+ .getIntAttribute("bringToFront");
+ VWindow.deferOrdering();
+ }
+ }
+
+ public void updateCaption(ComponentConnector component) {
+ // NOP, window has own caption, layout caption not rendered
+ }
+
+ public void onBeforeShortcutAction(Event e) {
+ // NOP, nothing to update just avoid workaround ( causes excess
+ // blur/focus )
+ }
+
+ @Override
+ public VWindow getWidget() {
+ return (VWindow) super.getWidget();
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VWindow.class);
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ // We always have 1 child, unless the child is hidden
+ Widget newChildWidget = null;
+ ComponentConnector newChild = null;
+ if (getChildren().size() == 1) {
+ newChild = getChildren().get(0);
+ newChildWidget = newChild.getWidget();
+ }
+
+ getWidget().layout = newChild;
+ getWidget().contentPanel.setWidget(newChildWidget);
+ }
+
+ public void layout() {
+ LayoutManager lm = getLayoutManager();
+ VWindow window = getWidget();
+ ComponentConnector layout = window.layout;
+ Element contentElement = window.contentPanel.getElement();
+
+ boolean needsMinWidth = !isUndefinedWidth() || layout.isRelativeWidth();
+ int minWidth = window.getMinWidth();
+ if (needsMinWidth && lm.getInnerWidth(contentElement) < minWidth) {
+ // Use minimum width if less than a certain size
+ window.setWidth(minWidth + "px");
+ }
+
+ boolean needsMinHeight = !isUndefinedHeight()
+ || layout.isRelativeHeight();
+ int minHeight = window.getMinHeight();
+ if (needsMinHeight && lm.getInnerHeight(contentElement) < minHeight) {
+ // Use minimum height if less than a certain size
+ window.setHeight(minHeight + "px");
+ }
+
+ Style contentStyle = window.contents.getStyle();
+
+ int headerHeight = lm.getOuterHeight(window.header);
+ contentStyle.setPaddingTop(headerHeight, Unit.PX);
+ contentStyle.setMarginTop(-headerHeight, Unit.PX);
+
+ int footerHeight = lm.getOuterHeight(window.footer);
+ contentStyle.setPaddingBottom(footerHeight, Unit.PX);
+ contentStyle.setMarginBottom(-footerHeight, Unit.PX);
+
+ /*
+ * Must set absolute position if the child has relative height and
+ * there's a chance of horizontal scrolling as some browsers will
+ * otherwise not take the scrollbar into account when calculating the
+ * height.
+ */
+ Element layoutElement = layout.getWidget().getElement();
+ Style childStyle = layoutElement.getStyle();
+ if (layout.isRelativeHeight() && !BrowserInfo.get().isIE9()) {
+ childStyle.setPosition(Position.ABSOLUTE);
+
+ Style wrapperStyle = contentElement.getStyle();
+ if (window.getElement().getStyle().getWidth().length() == 0
+ && !layout.isRelativeWidth()) {
+ /*
+ * Need to lock width to make undefined width work even with
+ * absolute positioning
+ */
+ int contentWidth = lm.getOuterWidth(layoutElement);
+ wrapperStyle.setWidth(contentWidth, Unit.PX);
+ } else {
+ wrapperStyle.clearWidth();
+ }
+ } else {
+ childStyle.clearPosition();
+ }
+ }
+
+ public void postLayout() {
+ VWindow window = getWidget();
+ if (window.centered) {
+ window.center();
+ }
+ window.updateShadowSizeAndPosition();
+ }
+
+ @Override
+ public WindowState getState() {
+ return (WindowState) super.getState();
+ }
+
+ /**
+ * Gives the WindowConnector an order number. As a side effect, moves the
+ * window according to its order number so the windows are stacked. This
+ * method should be called for each window in the order they should appear.
+ */
+ public void setWindowOrderAndPosition() {
+ getWidget().setWindowOrderAndPosition();
+ }
+}