From: Leif Åstrand Date: Thu, 12 Apr 2012 07:39:20 +0000 (+0300) Subject: Merge branch 'layoutgraph' X-Git-Tag: 7.0.0.alpha2~65 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=b65f72265230869b03a09c4e8c353f39b8cc1ecf;p=vaadin-framework.git Merge branch 'layoutgraph' Conflicts: src/com/vaadin/terminal/gwt/client/ApplicationConnection.java src/com/vaadin/terminal/gwt/client/LayoutManager.java src/com/vaadin/terminal/gwt/client/ui/AbsoluteLayoutConnector.java src/com/vaadin/terminal/gwt/client/ui/AbstractOrderedLayoutConnector.java src/com/vaadin/terminal/gwt/client/ui/AbstractSplitPanelConnector.java src/com/vaadin/terminal/gwt/client/ui/AccordionConnector.java src/com/vaadin/terminal/gwt/client/ui/FormConnector.java src/com/vaadin/terminal/gwt/client/ui/GridLayoutConnector.java src/com/vaadin/terminal/gwt/client/ui/PanelConnector.java src/com/vaadin/terminal/gwt/client/ui/RootConnector.java src/com/vaadin/terminal/gwt/client/ui/TableConnector.java src/com/vaadin/terminal/gwt/client/ui/TabsheetConnector.java src/com/vaadin/terminal/gwt/client/ui/TwinColSelectConnector.java src/com/vaadin/terminal/gwt/client/ui/VAbstractSplitPanel.java src/com/vaadin/terminal/gwt/client/ui/VAccordion.java src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java src/com/vaadin/terminal/gwt/client/ui/VGridLayout.java src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java src/com/vaadin/terminal/gwt/client/ui/VTabsheet.java src/com/vaadin/terminal/gwt/client/ui/VTabsheetPanel.java src/com/vaadin/terminal/gwt/client/ui/VView.java src/com/vaadin/terminal/gwt/client/ui/VWindow.java src/com/vaadin/terminal/gwt/client/ui/WindowConnector.java --- b65f72265230869b03a09c4e8c353f39b8cc1ecf diff --cc src/com/vaadin/terminal/gwt/client/ApplicationConnection.java index 555112a636,df67bcd1e1..a2816728f9 --- a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java @@@ -1226,10 -1228,10 +1228,11 @@@ public class ApplicationConnection 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 { @@@ -1279,8 -1281,8 +1282,10 @@@ // 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); diff --cc src/com/vaadin/terminal/gwt/client/LayoutManager.java index 60a2d3543a,db57b11c9e..4338f1284a --- a/src/com/vaadin/terminal/gwt/client/LayoutManager.java +++ b/src/com/vaadin/terminal/gwt/client/LayoutManager.java @@@ -12,7 -19,10 +19,10 @@@ import com.vaadin.terminal.gwt.client.M 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."; diff --cc src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java index a893657c40,0000000000..f33d582ef1 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java @@@ -1,217 -1,0 +1,219 @@@ +/* +@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 connectorIdToComponentWrapper = new HashMap(); + + @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(); + } + } +} diff --cc src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java index e5eda7607b,0000000000..8ab33f615d mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java @@@ -1,82 -1,0 +1,83 @@@ +/* +@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 { ++ 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(); + + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java index 71361b79ad,0000000000..dcd520bbb3 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java +++ b/src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java @@@ -1,510 -1,0 +1,507 @@@ +/* +@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 widgets = new HashSet(); + + HashMap lazyUpdateMap = new HashMap(); + + 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); + } - - Util.runWebkitOverflowAutoFix(openTab.getContainerElement()); - + } + + /** + * 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 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); + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java index c5f18ba23c,0000000000..5cbfabbb11 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java +++ b/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java @@@ -1,587 -1,0 +1,593 @@@ +/* +@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 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 fileIds = new ArrayList(); + List files = new ArrayList(); + + 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 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 - client.doLayout(false); ++ notifySizePotentiallyChanged(); + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java index 34d8461b20,0000000000..11d5385213 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java @@@ -1,187 -1,0 +1,209 @@@ +/* +@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; - import com.vaadin.terminal.gwt.client.ui.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 - implements Paintable, SimpleManagedLayout { ++ 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 - public void init() { ++ public void onUnregister() { + VForm form = getWidget(); - getLayoutManager().registerDependency(this, form.footerContainer); ++ 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); + } + - 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); - } - + @Override + public boolean isReadOnly() { + return super.isReadOnly() || getState().isPropertyReadOnly(); + } + + @Override + public FormState getState() { + return (FormState) super.getState(); + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java index 9858ed362a,0000000000..0a26fa020a mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java @@@ -1,226 -1,0 +1,239 @@@ +/* +@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(); + } +} diff --cc src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java index 1949cb191c,0000000000..6c5d018161 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java +++ b/src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java @@@ -1,676 -1,0 +1,684 @@@ +/* +@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 widgetToCell = new HashMap(); + + 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 colSpans = new LinkedList(); + private LinkedList rowSpans = new LinkedList(); + + private class SpanList { + final int span; + List cells = new LinkedList(); + + 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; + } + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java index 486621fae2,0000000000..4a9190e0f1 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java @@@ -1,290 -1,0 +1,320 @@@ +/* +@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) { - ownStyle.setPropertyPx(getSizeProperty(isVertical), - getSizeForInnerSize(allocatedSize, 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), - getSizeForInnerSize(allocatedSize, !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 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()); + } + } + + }; + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java index 5fe5c6890e,0000000000..43f58c10b8 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java @@@ -1,233 -1,0 +1,242 @@@ +/* +@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; - import com.vaadin.terminal.gwt.client.Util; +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 - implements Paintable, SimpleManagedLayout, PostLayoutListener { ++ 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(); - - Util.runWebkitOverflowAutoFix(panel.contentNode); + } + + 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); + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java index 7e84ee1328,0000000000..048e3ee94c mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java @@@ -1,429 -1,0 +1,423 @@@ +/* +@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; - import com.vaadin.terminal.gwt.client.Util; +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 - implements Paintable { ++ 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; + } + - // 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()); - } - + 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 getSubWindows() { + ArrayList windows = new ArrayList(); + 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(); + } + }); + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java index 8182753ab2,0000000000..13fc55d7ea mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java +++ b/src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java @@@ -1,316 -1,0 +1,321 @@@ +/* +@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; - import com.vaadin.terminal.gwt.client.Util; +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 historyChangeHandler = new ValueChangeHandler() { + public void onValueChange(ValueChangeEvent 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"); - Util.runWebkitOverflowAutoFix(getElement()); + + sendClientResized(); + - connection.doLayout(false); ++ 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 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(); + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java index 66416cf0ec,0000000000..f8fd2faf41 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java @@@ -1,182 -1,0 +1,206 @@@ +/* +@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 HandlerRegistration registerHandler( + H handler, Type 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(); + } + + // 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 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); + } +} diff --cc src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java index 6aad93dc5c,0000000000..166e79e92e mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java +++ b/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java @@@ -1,651 -1,0 +1,696 @@@ +/* +@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 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"); - client.doLayout(false); + } + + 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 { + + public static final Type TYPE = new Type(); + + private Widget splitPanel; + + public SplitterMoveEvent(Widget splitPanel) { + this.splitPanel = splitPanel; + } + + @Override + public com.google.gwt.event.shared.GwtEvent.Type 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; + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java index 76c6c21571,0000000000..e0cd3dc4c7 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java @@@ -1,319 -1,0 +1,331 @@@ +/* +@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() { - getWidget().sizeInit(); ++ 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 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); + } + } + } + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java index fbe1ef2f27,0000000000..cf6209e312 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java +++ b/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java @@@ -1,6683 -1,0 +1,6678 @@@ +/* +@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 selectedRowKeys = new HashSet(); + + /* + * 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 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 split(VScrollTableRow row) { + assert row.isAttached(); + ArrayList ranges = new ArrayList(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 selectedRowRanges = new HashSet(); + + 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 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 actionMap = new HashMap(); + 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; + - private boolean sizeNeedsInit = true; ++ 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() { + public void onClose(CloseEvent 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 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 ranges = new HashSet(); + 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 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 selectedKeys = uidl + .getStringArrayVariableAsSet("selected"); + if (scrollBody != null) { + Iterator 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. + *

+ * Update headers whould be called before this method is called! + *

+ * + * @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 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 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 it = scrollBody.iterator(); + final Iterator 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() { - if (!sizeNeedsInit) { - return; - } + 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 headCells = tHead.iterator(); + Iterator 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.
+ * (child components widths are correct) + */ + scrollBody.reLayoutComponents(); + Scheduler.get().scheduleDeferred(new Command() { + public void execute() { + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + }); - - client.doLayout(true); + } + + /** + * 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("" + (firstRowInViewPort + 1) + + " – " + (last) + "..." + ""); + 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( + 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 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 visibleCells = new ArrayList(); + + HashMap availableCells = new HashMap(); + + 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 updated = new HashSet(); + 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 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 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(""); + } else { + buf.append(""); + } + buf.append(super.getHTML()); + buf.append(""); + + 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 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 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 columns = new ArrayList( + 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 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 visibleCells = new ArrayList(); + HashMap availableCells = new HashMap(); + + 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 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 updated = new HashSet(); + 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 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 renderedRows = new LinkedList(); + + /** + * 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 insertRows(UIDL rowData, + int firstIndex, int rows) { + aligns = tHead.getColumnAlignments(); + final Iterator it = rowData.getChildIterator(); + List insertedRows = new ArrayList(); + + 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 insertAndReindexRows(UIDL rowData, + int firstIndex, int rows) { + List 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 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 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 childWidgets = new ArrayList(); + 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 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 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 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 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 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 = "\"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 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 newRanges = null; + for (Iterator iterator = selectedRowRanges.iterator(); iterator + .hasNext();) { + SelectionRange range = iterator.next(); + if (range.inRange(row)) { + // Split the range if given row is in range + Collection splitranges = range.split(row); + if (newRanges == null) { + newRanges = new ArrayList(); + } + 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; + } +} diff --cc src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java index 7423a536f2,0000000000..0518b3a480 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java @@@ -1,104 -1,0 +1,105 @@@ +/* +@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 { ++ 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(); + + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java index 908a984dbb,0000000000..c97ede1252 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java +++ b/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java @@@ -1,1219 -1,0 +1,1218 @@@ +/* +@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 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(); - tp.runWebkitOverflowAutoFix(); + } + + /** + * 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 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(); + } + } + } +} diff --cc src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java index ee0571d3a7,0000000000..f2b37c3a1c mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java +++ b/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java @@@ -1,218 -1,0 +1,213 @@@ +/* +@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; - import com.vaadin.terminal.gwt.client.Util; +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 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 beforeIndex 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 - wrapperDiv.getStyle().setPropertyPx("height", height); - runWebkitOverflowAutoFix(); - } - - public void runWebkitOverflowAutoFix() { - if (visibleWidget != null) { - Util.runWebkitOverflowAutoFix(DOM.getParent(visibleWidget - .getElement())); ++ 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); + } + } +} diff --cc src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java index de1095f664,0000000000..d8ad511d86 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java @@@ -1,63 -1,0 +1,69 @@@ +/* +@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(); + } + } +} diff --cc src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java index 3a1e0e8663,0000000000..2c5fadff3b mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java +++ b/src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java @@@ -1,909 -1,0 +1,911 @@@ +/* +@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 windowOrder = new ArrayList(); + + 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() { + 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 = "" + 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()); + } + - Util.runWebkitOverflowAutoFix(contentPanel.getElement()); - client.doLayout(false); ++ 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()); + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java index 57c931fac9,0000000000..fbb7a3683b mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java @@@ -1,290 -1,0 +1,297 @@@ +/* +@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; - import com.vaadin.terminal.gwt.client.Util; +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, - SimpleManagedLayout, PostLayoutListener { ++ 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(); + } - - Util.runWebkitOverflowAutoFix(window.contentPanel.getElement()); + } + + 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(); + } +}