From: Leif Åstrand Date: Wed, 25 Apr 2012 11:31:58 +0000 (+0300) Subject: Merge remote branch 'origin/6.8' X-Git-Tag: 7.0.0.alpha3~260 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=46c74e7362271ccbcf54c506657d8c0846a61858;p=vaadin-framework.git Merge remote branch 'origin/6.8' Conflicts: src/com/vaadin/terminal/gwt/client/ApplicationConnection.java src/com/vaadin/terminal/gwt/client/HistoryImplIEVaadin.java src/com/vaadin/terminal/gwt/client/Util.java src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java src/com/vaadin/terminal/gwt/client/ui/UploadIFrameOnloadStrategy.java src/com/vaadin/terminal/gwt/client/ui/UploadIFrameOnloadStrategyIE.java src/com/vaadin/terminal/gwt/client/ui/VCustomLayout.java src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapper.java src/com/vaadin/terminal/gwt/client/ui/VDragAndDropWrapperIE.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/VTextField.java src/com/vaadin/terminal/gwt/client/ui/VVideo.java src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java tests/testbench/com/vaadin/tests/components/table/TestCurrentPageFirstItem.java --- 46c74e7362271ccbcf54c506657d8c0846a61858 diff --cc src/com/vaadin/terminal/gwt/client/ApplicationConnection.java index 2be35f053f,de7ad83b54..be6d578112 --- a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java @@@ -256,24 -220,38 +256,33 @@@ public class ApplicationConnection /*-{ var ap = this; var client = {}; - client.isActive = function() { + client.isActive = $entry(function() { return ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::hasActiveRequest()() || ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::isExecutingDeferredCommands()(); - } + }); - var vi = ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::getVersionInfo()(); if (vi) { client.getVersionInfo = function() { return vi; } } - + - client.getElementByPath = function(id) { + client.getProfilingData = $entry(function() { + var pd = [ + ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::lastProcessingTime, + ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::totalProcessingTime + ]; + pd = pd.concat(ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::testBenchServerStatus); + return pd; + }); + + client.getElementByPath = $entry(function(id) { return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id); - } - client.getPathForElement = function(element) { + }); + client.getPathForElement = $entry(function(element) { return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element); - } + }); - if (!$wnd.vaadin.clients) { - $wnd.vaadin.clients = {}; - } - $wnd.vaadin.clients[TTAppId] = client; }-*/; @@@ -997,8 -990,12 +1034,14 @@@ json.getValueMap("typeMappings"), widgetSet); } + handleUIDLDuration.logDuration( + " * Handling type mappings from server completed", 10); + /* + * Hook for TestBench to get details about server status + */ + if (json.containsKey("tbss")) { + testBenchServerStatus = json.getValueMap("tbss"); + } Command c = new Command() { public void execute() { @@@ -1183,12 -1174,15 +1226,14 @@@ // TODO build profiling for widget impl loading time - final long prosessingTime = (new Date().getTime()) - - start.getTime(); + lastProcessingTime = (int) ((new Date().getTime()) - start + .getTime()); + totalProcessingTime += lastProcessingTime; + VConsole.log(" Processing time was " - + String.valueOf(prosessingTime) + "ms for " + + String.valueOf(lastProcessingTime) + "ms for " + jsonText.length() + " characters of JSON"); - VConsole.log("Referenced paintables: " - + idToPaintableDetail.size()); + VConsole.log("Referenced paintables: " + connectorMap.size()); endRequest(); diff --cc src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java index 6b22f3c9f3,96cb4b8a35..533d6a78ae --- a/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java +++ b/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java @@@ -21,6 -23,8 +23,7 @@@ import com.google.gwt.user.client.Event import com.google.gwt.user.client.ui.ScrollPanel; import com.google.gwt.user.client.ui.Widget; import com.google.gwt.user.client.ui.impl.FocusImpl; + import com.vaadin.terminal.gwt.client.BrowserInfo; -import com.vaadin.terminal.gwt.client.VConsole; /** * A scrollhandlers similar to {@link ScrollPanel}. diff --cc src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java index 5208d7cacf,0000000000..b4194c40a6 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java +++ b/src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java @@@ -1,408 -1,0 +1,408 @@@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui.customlayout; + +import java.util.HashMap; +import java.util.Iterator; + +import com.google.gwt.dom.client.ImageElement; +import com.google.gwt.dom.client.NodeList; +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.Util; +import com.vaadin.terminal.gwt.client.VCaption; +import com.vaadin.terminal.gwt.client.VCaptionWrapper; + +/** + * Custom Layout implements complex layout defined with HTML template. + * + * @author Vaadin Ltd + * + */ +public class VCustomLayout extends ComplexPanel { + + public static final String CLASSNAME = "v-customlayout"; + + /** Location-name to containing element in DOM map */ + private final HashMap locationToElement = new HashMap(); + + /** Location-name to contained widget map */ + final HashMap locationToWidget = new HashMap(); + + /** Widget to captionwrapper map */ + private final HashMap childWidgetToCaptionWrapper = new HashMap(); + + /** Name of the currently rendered style */ + String currentTemplateName; + + /** Unexecuted scripts loaded from the template */ + String scripts = ""; + + /** Paintable ID of this paintable */ + String pid; + + ApplicationConnection client; + + private boolean htmlInitialized = false; + + private Element elementWithNativeResizeFunction; + + private String height = ""; + + private String width = ""; + + public VCustomLayout() { + setElement(DOM.createDiv()); + // Clear any unwanted styling + DOM.setStyleAttribute(getElement(), "border", "none"); + DOM.setStyleAttribute(getElement(), "margin", "0"); + DOM.setStyleAttribute(getElement(), "padding", "0"); + + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(getElement(), "position", "relative"); + } + + setStyleName(CLASSNAME); + } + + /** + * Sets widget to given location. + * + * If location already contains a widget it will be removed. + * + * @param widget + * Widget to be set into location. + * @param location + * location name where widget will be added + * + * @throws IllegalArgumentException + * if no such location is found in the layout. + */ + public void setWidget(Widget widget, String location) { + + if (widget == null) { + return; + } + + // If no given location is found in the layout, and exception is throws + Element elem = locationToElement.get(location); + if (elem == null && hasTemplate()) { + throw new IllegalArgumentException("No location " + location + + " found"); + } + + // Get previous widget + final Widget previous = locationToWidget.get(location); + // NOP if given widget already exists in this location + if (previous == widget) { + return; + } + + if (previous != null) { + remove(previous); + } + + // if template is missing add element in order + if (!hasTemplate()) { + elem = getElement(); + } + + // Add widget to location + super.add(widget, elem); + locationToWidget.put(location, widget); + } + + /** Initialize HTML-layout. */ + public void initializeHTML(String template, String themeUri) { + + // Connect body of the template to DOM + template = extractBodyAndScriptsFromTemplate(template); + + // TODO prefix img src:s here with a regeps, cannot work further with IE + + String relImgPrefix = themeUri + "/layouts/"; + + // prefix all relative image elements to point to theme dir with a + // regexp search + template = template.replaceAll( + "<((?:img)|(?:IMG))\\s([^>]*)src=\"((?![a-z]+:)[^/][^\"]+)\"", + "<$1 $2src=\"" + relImgPrefix + "$3\""); + // also support src attributes without quotes + template = template + .replaceAll( + "<((?:img)|(?:IMG))\\s([^>]*)src=[^\"]((?![a-z]+:)[^/][^ />]+)[ />]", + "<$1 $2src=\"" + relImgPrefix + "$3\""); + // also prefix relative style="...url(...)..." + template = template + .replaceAll( + "(<[^>]+style=\"[^\"]*url\\()((?![a-z]+:)[^/][^\"]+)(\\)[^>]*>)", + "$1 " + relImgPrefix + "$2 $3"); + + getElement().setInnerHTML(template); + + // Remap locations to elements + locationToElement.clear(); + scanForLocations(getElement()); + + initImgElements(); + + elementWithNativeResizeFunction = DOM.getFirstChild(getElement()); + if (elementWithNativeResizeFunction == null) { + elementWithNativeResizeFunction = getElement(); + } + publishResizedFunction(elementWithNativeResizeFunction); + + htmlInitialized = true; + } + + private native boolean uriEndsWithSlash() + /*-{ + var path = $wnd.location.pathname; + if(path.charAt(path.length - 1) == "/") + return true; + return false; + }-*/; + + boolean hasTemplate() { + return htmlInitialized; + } + + /** Collect locations from template */ + private void scanForLocations(Element elem) { + + final String location = elem.getAttribute("location"); + if (!"".equals(location)) { + locationToElement.put(location, elem); + elem.setInnerHTML(""); + + } else { + final int len = DOM.getChildCount(elem); + for (int i = 0; i < len; i++) { + scanForLocations(DOM.getChild(elem, i)); + } + } + } + + /** Evaluate given script in browser document */ + static native void eval(String script) + /*-{ + try { + if (script != null) + eval("{ var document = $doc; var window = $wnd; "+ script + "}"); + } catch (e) { + } + }-*/; + + /** + * Img elements needs some special handling in custom layout. Img elements + * will get their onload events sunk. This way custom layout can notify + * parent about possible size change. + */ + private void initImgElements() { + NodeList nodeList = getElement() + .getElementsByTagName("IMG"); + for (int i = 0; i < nodeList.getLength(); i++) { + com.google.gwt.dom.client.ImageElement img = (ImageElement) nodeList + .getItem(i); + DOM.sinkEvents((Element) img.cast(), Event.ONLOAD); + } + } + + /** + * Extract body part and script tags from raw html-template. + * + * Saves contents of all script-tags to private property: scripts. Returns + * contents of the body part for the html without script-tags. Also replaces + * all _UID_ tags with an unique id-string. + * + * @param html + * Original HTML-template received from server + * @return html that is used to create the HTMLPanel. + */ + private String extractBodyAndScriptsFromTemplate(String html) { + + // Replace UID:s + html = html.replaceAll("_UID_", pid + "__"); + + // Exctract script-tags + scripts = ""; + int endOfPrevScript = 0; + int nextPosToCheck = 0; + String lc = html.toLowerCase(); + String res = ""; + int scriptStart = lc.indexOf(" 0) { + res += html.substring(endOfPrevScript, scriptStart); + scriptStart = lc.indexOf(">", scriptStart); + final int j = lc.indexOf("", scriptStart); + scripts += html.substring(scriptStart + 1, j) + ";"; + nextPosToCheck = endOfPrevScript = j + "".length(); + scriptStart = lc.indexOf("", startOfBody) + 1; + final int endOfBody = lc.indexOf("", startOfBody); + if (endOfBody > startOfBody) { + res = html.substring(startOfBody, endOfBody); + } else { + res = html.substring(startOfBody); + } + } + + return res; + } + + /** Update caption for given widget */ + public void updateCaption(ComponentConnector paintable) { + Widget widget = paintable.getWidget(); + VCaptionWrapper wrapper = childWidgetToCaptionWrapper.get(widget); + if (VCaption.isNeeded(paintable.getState())) { + if (wrapper == null) { + // Add a wrapper between the layout and the child widget + final String loc = getLocation(widget); + super.remove(widget); + wrapper = new VCaptionWrapper(paintable, client); + super.add(wrapper, locationToElement.get(loc)); + childWidgetToCaptionWrapper.put(widget, wrapper); + } + wrapper.updateCaption(); + } else { + if (wrapper != null) { + // Remove the wrapper and add the widget directly to the layout + final String loc = getLocation(widget); + super.remove(wrapper); + super.add(widget, locationToElement.get(loc)); + childWidgetToCaptionWrapper.remove(widget); + } + } + } + + /** Get the location of an widget */ + public String getLocation(Widget w) { + for (final Iterator i = locationToWidget.keySet().iterator(); i + .hasNext();) { + final String location = i.next(); + if (locationToWidget.get(location) == w) { + return location; + } + } + return null; + } + + /** Removes given widget from the layout */ + @Override + public boolean remove(Widget w) { + final String location = getLocation(w); + if (location != null) { + locationToWidget.remove(location); + } + final VCaptionWrapper cw = childWidgetToCaptionWrapper.get(w); + if (cw != null) { + childWidgetToCaptionWrapper.remove(w); + return super.remove(cw); + } else if (w != null) { + return super.remove(w); + } + return false; + } + + /** Adding widget without specifying location is not supported */ + @Override + public void add(Widget w) { + throw new UnsupportedOperationException(); + } + + /** Clear all widgets from the layout */ + @Override + public void clear() { + super.clear(); + locationToWidget.clear(); + childWidgetToCaptionWrapper.clear(); + } + + /** + * This method is published to JS side with the same name into first DOM + * node of custom layout. This way if one implements some resizeable + * containers in custom layout he/she can notify children after resize. + */ + public void notifyChildrenOfSizeChange() { + client.runDescendentsLayout(this); + } + + @Override + public void onDetach() { + super.onDetach(); + if (elementWithNativeResizeFunction != null) { + detachResizedFunction(elementWithNativeResizeFunction); + } + } + + private native void detachResizedFunction(Element element) + /*-{ + element.notifyChildrenOfSizeChange = null; + }-*/; + + private native void publishResizedFunction(Element element) + /*-{ + var self = this; - element.notifyChildrenOfSizeChange = function() { ++ element.notifyChildrenOfSizeChange = $entry(function() { + self.@com.vaadin.terminal.gwt.client.ui.customlayout.VCustomLayout::notifyChildrenOfSizeChange()(); - }; ++ }); + }-*/; + + /** + * In custom layout one may want to run layout functions made with + * JavaScript. This function tests if one exists (with name "iLayoutJS" in + * layouts first DOM node) and runs et. Return value is used to determine if + * children needs to be notified of size changes. + * + * Note! When implementing a JS layout function you most likely want to call + * notifyChildrenOfSizeChange() function on your custom layouts main + * element. That method is used to control whether child components layout + * functions are to be run. + * + * @param el + * @return true if layout function exists and was run successfully, else + * false. + */ + native boolean iLayoutJS(Element el) + /*-{ + if(el && el.iLayoutJS) { + try { + el.iLayoutJS(); + return true; + } catch (e) { + return false; + } + } else { + return false; + } + }-*/; + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + event.cancelBubble(true); + } + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java index 5cbfabbb11,0000000000..d09b81e1e1 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,593 -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) { ++ el.addEventListener("dragstart", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }, false); ++ }), false); + }-*/; + + /** + * Prototype code, memory leak risk. + * + * @param el + */ + protected native void hookHtml5Events(Element el) + /*-{ + var me = this; + - el.addEventListener("dragenter", function(ev) { ++ el.addEventListener("dragenter", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }, false); ++ }), false); + - el.addEventListener("dragleave", function(ev) { ++ el.addEventListener("dragleave", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }, false); ++ }), false); + - el.addEventListener("dragover", function(ev) { ++ el.addEventListener("dragover", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }, false); ++ }), false); + - el.addEventListener("drop", function(ev) { ++ el.addEventListener("drop", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }, false); ++ }), 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) { + notifySizePotentiallyChanged(); + } + } + + private void notifySizePotentiallyChanged() { + LayoutManager.get(client).setNeedsMeasure( + ConnectorMap.get(client).getConnector(getElement())); + } + + protected void emphasis(VDragEvent drag) { + deEmphasis(false); + VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, true); + VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-" + + verticalDropLocation.toString().toLowerCase(), true); + VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-" + + horizontalDropLocation.toString().toLowerCase(), true); + emphasizedVDrop = verticalDropLocation; + emphasizedHDrop = horizontalDropLocation; + + // TODO build (to be an example) an emphasis mode where drag image + // is fitted before or after the content + notifySizePotentiallyChanged(); + } + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java index f819b0559a,0000000000..bb511524e5 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java +++ b/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java @@@ -1,69 -1,0 +1,69 @@@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui.draganddropwrapper; + +import com.google.gwt.dom.client.AnchorElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.Element; +import com.vaadin.terminal.gwt.client.VConsole; + +public class VDragAndDropWrapperIE extends VDragAndDropWrapper { + private AnchorElement anchor = null; + + @Override + protected Element getDragStartElement() { + VConsole.log("IE get drag start element..."); + Element div = getElement(); + if (dragStartMode == HTML5) { + if (anchor == null) { + anchor = Document.get().createAnchorElement(); + anchor.setHref("#"); + anchor.setClassName("drag-start"); + div.appendChild(anchor); + } + VConsole.log("IE get drag start element..."); + return (Element) anchor.cast(); + } else { + if (anchor != null) { + div.removeChild(anchor); + anchor = null; + } + return div; + } + } + + @Override + protected native void hookHtml5DragStart(Element el) + /*-{ + var me = this; + - el.attachEvent("ondragstart", function(ev) { ++ el.attachEvent("ondragstart", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }); ++ })); + }-*/; + + @Override + protected native void hookHtml5Events(Element el) + /*-{ + var me = this; + - el.attachEvent("ondragenter", function(ev) { ++ el.attachEvent("ondragenter", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }); ++ })); + - el.attachEvent("ondragleave", function(ev) { ++ el.attachEvent("ondragleave", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }); ++ })); + - el.attachEvent("ondragover", function(ev) { ++ el.attachEvent("ondragover", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }); ++ })); + - el.attachEvent("ondrop", function(ev) { ++ el.attachEvent("ondrop", $entry(function(ev) { + return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev); - }); ++ })); + }-*/; + +} diff --cc src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java index 563ca04abe,0000000000..c45c26c4ac 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,6699 -1,0 +1,6716 @@@ +/* +@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.Node; +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; + + 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()); ++ touchScrollDelegate.setScrollHandler(this); + } + 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() { + 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) { + // FIXME #7607 + // Originally deferred due to Firefox oddities which should not + // occur any more. Currently deferring breaks Webkit scrolling with + // relative-height tables, but not deferring instead breaks tables + // with explicit page length. + Scheduler.get().scheduleDeferred(new Command() { + public void execute() { + 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()); + } + }); + } + + /** + * 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); ++ if (BrowserInfo.get().isTouchDevice()) { ++ NodeList childNodes = container.getChildNodes(); ++ for (int i = 0; i < childNodes.getLength(); i++) { ++ Element item = (Element) childNodes.getItem(i); ++ item.getStyle().setProperty("webkitTransform", ++ "translate3d(0,0,0)"); ++ } ++ } + + } + + 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 TOUCHSCROLL_TIMEOUT = 100; + 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 there's a scroll delegate, check if + * we're actually scrolling and handle it. + * If no delegate, do nothing here and let + * the row handle potential drag'n'drop or + * context menu. + */ + if (activeScrollDelegate != null) { + if (activeScrollDelegate.isMoved()) { + /* + * Prevent the row from handling + * touch move/end events (the + * delegate handles those) and from + * doing drag'n'drop or opening a + * context menu. + */ + touchStart = null; + } else { + /* + * Scrolling hasn't started, so + * cancel delegate and let the row + * handle potential drag'n'drop or + * context menu. + */ + activeScrollDelegate + .stopScrolling(); + } + } + } + }.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); ++ if (contextTouchTimeout != 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) { ++ if (BrowserInfo.get().isTouchDevice()) { ++ // Skip due to android devices that have broken scrolltop will may ++ // get odd scrolling here. ++ return; ++ } + 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/textfield/VTextField.java index b2141e06e5,0000000000..7bd392b503 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java +++ b/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java @@@ -1,442 -1,0 +1,442 @@@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui.textfield; + +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +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.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.ui.TextBoxBase; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.EventId; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VTooltip; +import com.vaadin.terminal.gwt.client.ui.Field; + +/** + * This class represents a basic text input field with one row. + * + * @author Vaadin Ltd. + * + */ +public class VTextField extends TextBoxBase implements Field, ChangeHandler, + FocusHandler, BlurHandler, KeyDownHandler { + + public static final String VAR_CUR_TEXT = "curText"; + + public static final String ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS = "nvc"; + /** + * The input node CSS classname. + */ + public static final String CLASSNAME = "v-textfield"; + /** + * This CSS classname is added to the input node on hover. + */ + public static final String CLASSNAME_FOCUS = "focus"; + + protected String paintableId; + + protected ApplicationConnection client; + + protected String valueBeforeEdit = null; + + /** + * Set to false if a text change event has been sent since the last value + * change event. This means that {@link #valueBeforeEdit} should not be + * trusted when determining whether a text change even should be sent. + */ + private boolean valueBeforeEditIsSynced = true; + + protected boolean immediate = false; + private int maxLength = -1; + + private static final String CLASSNAME_PROMPT = "prompt"; + protected static final String ATTR_INPUTPROMPT = "prompt"; + public static final String ATTR_TEXTCHANGE_TIMEOUT = "iet"; + public static final String VAR_CURSOR = "c"; + public static final String ATTR_TEXTCHANGE_EVENTMODE = "iem"; + protected static final String TEXTCHANGE_MODE_EAGER = "EAGER"; + private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT"; + + protected String inputPrompt = null; + private boolean prompting = false; + private int lastCursorPos = -1; + private boolean wordwrap = true; + + public VTextField() { + this(DOM.createInputText()); + } + + protected VTextField(Element node) { + super(node); + setStyleName(CLASSNAME); + addChangeHandler(this); + if (BrowserInfo.get().isIE()) { + // IE does not send change events when pressing enter in a text + // input so we handle it using a key listener instead + addKeyDownHandler(this); + } + addFocusHandler(this); + addBlurHandler(this); + sinkEvents(VTooltip.TOOLTIP_EVENTS); + } + + /* + * TODO When GWT adds ONCUT, add it there and remove workaround. See + * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030 + * + * Also note that the cut/paste are not totally crossbrowsers compatible. + * E.g. in Opera mac works via context menu, but on via File->Paste/Cut. + * Opera might need the polling method for 100% working textchanceevents. + * Eager polling for a change is bit dum and heavy operation, so I guess we + * should first try to survive without. + */ + protected static final int TEXTCHANGE_EVENTS = Event.ONPASTE + | Event.KEYEVENTS | Event.ONMOUSEUP; + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (client != null) { + client.handleTooltipEvent(event, this); + } + + if (listenTextChangeEvents + && (event.getTypeInt() & TEXTCHANGE_EVENTS) == event + .getTypeInt()) { + deferTextChangeEvent(); + } + + } + + /* + * TODO optimize this so that only changes are sent + make the value change + * event just a flag that moves the current text to value + */ + private String lastTextChangeString = null; + + private String getLastCommunicatedString() { + return lastTextChangeString; + } + + private void communicateTextValueToServer() { + String text = getText(); + if (prompting) { + // Input prompt visible, text is actually "" + text = ""; + } + if (!text.equals(getLastCommunicatedString())) { + if (valueBeforeEditIsSynced && text.equals(valueBeforeEdit)) { + /* + * Value change for the current text has been enqueued since the + * last text change event was sent, but we can't know that it + * has been sent to the server. Ensure that all pending changes + * are sent now. Sending a value change without a text change + * will simulate a TextChangeEvent on the server. + */ + client.sendPendingVariableChanges(); + } else { + // Default case - just send an immediate text change message + client.updateVariable(paintableId, VAR_CUR_TEXT, text, true); + + // Shouldn't investigate valueBeforeEdit to avoid duplicate text + // change events as the states are not in sync any more + valueBeforeEditIsSynced = false; + } + lastTextChangeString = text; + } + } + + private Timer textChangeEventTrigger = new Timer() { + + @Override + public void run() { + if (isAttached()) { + updateCursorPosition(); + communicateTextValueToServer(); + scheduled = false; + } + } + }; + private boolean scheduled = false; + protected boolean listenTextChangeEvents; + protected String textChangeEventMode; + protected int textChangeEventTimeout; + + private void deferTextChangeEvent() { + if (textChangeEventMode.equals(TEXTCHANGE_MODE_TIMEOUT) && scheduled) { + return; + } else { + textChangeEventTrigger.cancel(); + } + textChangeEventTrigger.schedule(getTextChangeEventTimeout()); + scheduled = true; + } + + private int getTextChangeEventTimeout() { + return textChangeEventTimeout; + } + + @Override + public void setReadOnly(boolean readOnly) { + boolean wasReadOnly = isReadOnly(); + + if (readOnly) { + setTabIndex(-1); + } else if (wasReadOnly && !readOnly && getTabIndex() == -1) { + /* + * Need to manually set tab index to 0 since server will not send + * the tab index if it is 0. + */ + setTabIndex(0); + } + + super.setReadOnly(readOnly); + } + + protected void updateFieldContent(final String text) { + setPrompting(inputPrompt != null && focusedTextField != this + && (text.equals(""))); + + String fieldValue; + if (prompting) { + fieldValue = isReadOnly() ? "" : inputPrompt; + addStyleDependentName(CLASSNAME_PROMPT); + } else { + fieldValue = text; + removeStyleDependentName(CLASSNAME_PROMPT); + } + setText(fieldValue); + + lastTextChangeString = valueBeforeEdit = text; + valueBeforeEditIsSynced = true; + } + + protected void onCut() { + if (listenTextChangeEvents) { + deferTextChangeEvent(); + } + } + + protected native void attachCutEventListener(Element el) + /*-{ + var me = this; - el.oncut = function() { ++ el.oncut = $entry(function() { + me.@com.vaadin.terminal.gwt.client.ui.textfield.VTextField::onCut()(); - }; ++ }); + }-*/; + + protected native void detachCutEventListener(Element el) + /*-{ + el.oncut = null; + }-*/; + + @Override + protected void onDetach() { + super.onDetach(); + detachCutEventListener(getElement()); + if (focusedTextField == this) { + focusedTextField = null; + } + } + + @Override + protected void onAttach() { + super.onAttach(); + if (listenTextChangeEvents) { + detachCutEventListener(getElement()); + } + } + + protected void setMaxLength(int newMaxLength) { + if (newMaxLength >= 0) { + maxLength = newMaxLength; + if (getElement().getTagName().toLowerCase().equals("textarea")) { + // NOP no maxlength property for textarea + } else { + getElement().setPropertyInt("maxLength", maxLength); + } + } else if (maxLength != -1) { + if (getElement().getTagName().toLowerCase().equals("textarea")) { + // NOP no maxlength property for textarea + } else { + getElement().removeAttribute("maxLength"); + } + maxLength = -1; + } + + } + + public int getMaxLength() { + return maxLength; + } + + public void onChange(ChangeEvent event) { + valueChange(false); + } + + /** + * Called when the field value might have changed and/or the field was + * blurred. These are combined so the blur event is sent in the same batch + * as a possible value change event (these are often connected). + * + * @param blurred + * true if the field was blurred + */ + public void valueChange(boolean blurred) { + if (client != null && paintableId != null) { + boolean sendBlurEvent = false; + boolean sendValueChange = false; + + if (blurred && client.hasEventListeners(this, EventId.BLUR)) { + sendBlurEvent = true; + client.updateVariable(paintableId, EventId.BLUR, "", false); + } + + String newText = getText(); + if (!prompting && newText != null + && !newText.equals(valueBeforeEdit)) { + sendValueChange = immediate; + client.updateVariable(paintableId, "text", getText(), false); + valueBeforeEdit = newText; + valueBeforeEditIsSynced = true; + } + + /* + * also send cursor position, no public api yet but for easier + * extension + */ + updateCursorPosition(); + + if (sendBlurEvent || sendValueChange) { + /* + * Avoid sending text change event as we will simulate it on the + * server side before value change events. + */ + textChangeEventTrigger.cancel(); + scheduled = false; + client.sendPendingVariableChanges(); + } + } + } + + /** + * Updates the cursor position variable if it has changed since the last + * update. + * + * @return true iff the value was updated + */ + protected boolean updateCursorPosition() { + if (Util.isAttachedAndDisplayed(this)) { + int cursorPos = getCursorPos(); + if (lastCursorPos != cursorPos) { + client.updateVariable(paintableId, VAR_CURSOR, cursorPos, false); + lastCursorPos = cursorPos; + return true; + } + } + return false; + } + + private static VTextField focusedTextField; + + public static void flushChangesFromFocusedTextField() { + if (focusedTextField != null) { + focusedTextField.onChange(null); + } + } + + public void onFocus(FocusEvent event) { + addStyleDependentName(CLASSNAME_FOCUS); + if (prompting) { + setText(""); + removeStyleDependentName(CLASSNAME_PROMPT); + setPrompting(false); + } + focusedTextField = this; + if (client.hasEventListeners(this, EventId.FOCUS)) { + client.updateVariable(paintableId, EventId.FOCUS, "", true); + } + } + + public void onBlur(BlurEvent event) { + removeStyleDependentName(CLASSNAME_FOCUS); + focusedTextField = null; + String text = getText(); + setPrompting(inputPrompt != null && (text == null || "".equals(text))); + if (prompting) { + setText(isReadOnly() ? "" : inputPrompt); + addStyleDependentName(CLASSNAME_PROMPT); + } + + valueChange(true); + } + + private void setPrompting(boolean prompting) { + this.prompting = prompting; + } + + public void setColumns(int columns) { + setColumns(getElement(), columns); + } + + private native void setColumns(Element e, int c) + /*-{ + try { + switch(e.tagName.toLowerCase()) { + case "input": + //e.size = c; + e.style.width = c+"em"; + break; + case "textarea": + //e.cols = c; + e.style.width = c+"em"; + break; + default:; + } + } catch (e) {} + }-*/; + + // Here for backward compatibility; to be moved to TextArea + public void setWordwrap(boolean enabled) { + if (enabled == wordwrap) { + return; // No change + } + + if (enabled) { + getElement().removeAttribute("wrap"); + getElement().getStyle().clearOverflow(); + } else { + getElement().setAttribute("wrap", "off"); + getElement().getStyle().setOverflow(Overflow.AUTO); + } + if (BrowserInfo.get().isSafari4()) { + // Force redraw as Safari 4 does not properly update the screen + Util.forceWebkitRedraw(getElement()); + } else if (BrowserInfo.get().isOpera()) { + // Opera fails to dynamically update the wrap attribute so we detach + // and reattach the whole TextArea. + Util.detachAttach(getElement()); + } + wordwrap = enabled; + } + + public void onKeyDown(KeyDownEvent event) { + if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) { + valueChange(false); + } + } +} diff --cc src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java index d40f954cdc,0000000000..484000b8d1 mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java +++ b/src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java @@@ -1,64 -1,0 +1,64 @@@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui.video; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.VideoElement; +import com.google.gwt.user.client.Element; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.VMediaBase; + +public class VVideo extends VMediaBase { + + private static String CLASSNAME = "v-video"; + + private VideoElement video; + + public VVideo() { + video = Document.get().createVideoElement(); + setMediaElement(video); + setStyleName(CLASSNAME); + + updateDimensionsWhenMetadataLoaded(getElement()); + } + + /** + * Registers a listener that updates the dimensions of the widget when the + * video metadata has been loaded. + * + * @param el + */ + private native void updateDimensionsWhenMetadataLoaded(Element el) + /*-{ + var self = this; - el.addEventListener('loadedmetadata', function(e) { - $entry(self.@com.vaadin.terminal.gwt.client.ui.video.VVideo::updateElementDynamicSize(II)(el.videoWidth, el.videoHeight)); - }, false); ++ el.addEventListener('loadedmetadata', $entry(function(e) { ++ self.@com.vaadin.terminal.gwt.client.ui.video.VVideo::updateElementDynamicSize(II)(el.videoWidth, el.videoHeight); ++ }), false); + + }-*/; + + /** + * Updates the dimensions of the widget. + * + * @param w + * @param h + */ + private void updateElementDynamicSize(int w, int h) { + video.getStyle().setWidth(w, Unit.PX); + video.getStyle().setHeight(h, Unit.PX); + Util.notifyParentOfSizeChange(this, true); + } + + @Override + protected String getDefaultAltHtml() { + return "Your browser does not support the video element."; + } + + public void setPoster(String poster) { + video.setPoster(poster); + } + +} diff --cc src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java index 0d67086339,a5923cb47f..58d6a18592 --- a/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java @@@ -509,27 -326,11 +509,30 @@@ public abstract class AbstractApplicati protected void handleRequest(PortletRequest request, PortletResponse response) throws PortletException, IOException { - RequestTimer.RequestWrapper wrappedRequest = new RequestTimer.RequestWrapper( - request); + AbstractApplicationPortletWrapper portletWrapper = new AbstractApplicationPortletWrapper( + this); + + WrappedPortletRequest wrappedRequest; + + String portalInfo = request.getPortalContext().getPortalInfo() + .toLowerCase(); + if (portalInfo.contains("liferay")) { + wrappedRequest = new WrappedLiferayRequest(request, + getDeploymentConfiguration()); + } else if (portalInfo.contains("gatein")) { + wrappedRequest = new WrappedGateinRequest(request, + getDeploymentConfiguration()); + } else { + wrappedRequest = new WrappedPortletRequest(request, + getDeploymentConfiguration()); + } + + WrappedPortletResponse wrappedResponse = new WrappedPortletResponse( + response, getDeploymentConfiguration()); + + RequestTimer requestTimer = RequestTimer.get(wrappedRequest); + requestTimer.start(wrappedRequest); + RequestType requestType = getRequestType(request); if (requestType == RequestType.UNKNOWN) { @@@ -709,16 -490,14 +712,19 @@@ .endTransaction(application, request); } } finally { - if (requestStarted) { - ((PortletRequestListener) application).onRequestEnd( - request, response); + try { + if (requestStarted) { + ((PortletRequestListener) application) + .onRequestEnd(request, response); + } + } finally { + Root.setCurrentRoot(null); + Application.setCurrentApplication(null); } + + requestTimer.stop(); + RequestTimer.set(wrappedRequest, requestTimer); } } } diff --cc src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java index 18cc3f97f4,04ea423004..799271b979 --- a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java @@@ -394,15 -400,12 +394,18 @@@ public abstract class AbstractApplicati @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - RequestTimer.RequestWrapper wrappedRequest = new RequestTimer.RequestWrapper( - request); - RequestTimer requestTimer = RequestTimer.get(wrappedRequest); - requestTimer.start(wrappedRequest); + service(createWrappedRequest(request), createWrappedResponse(response)); + } + + private void service(WrappedHttpServletRequest request, + WrappedHttpServletResponse response) throws ServletException, + IOException { + AbstractApplicationServletWrapper servletWrapper = new AbstractApplicationServletWrapper( + this); + ++ RequestTimer requestTimer = RequestTimer.get(request); ++ requestTimer.start(request); + RequestType requestType = getRequestType(request); if (!ensureCookiesEnabled(requestType, request, response)) { return; @@@ -529,16 -548,13 +532,18 @@@ } } finally { - if (requestStarted) { - ((HttpServletRequestListener) application).onRequestEnd( - request, response); + try { + if (requestStarted) { + ((HttpServletRequestListener) application) + .onRequestEnd(request, response); + } + } finally { + Root.setCurrentRoot(null); + Application.setCurrentApplication(null); } + requestTimer.stop(); - RequestTimer.set(wrappedRequest, requestTimer); ++ RequestTimer.set(request, requestTimer); } } diff --cc src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java index a1a917130f,9a6bccebb8..b780f66a23 --- a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java @@@ -688,65 -886,64 +688,65 @@@ public abstract class AbstractCommunica .getAttribute(WRITE_SECURITY_TOKEN_FLAG); if (writeSecurityTokenFlag != null) { - String seckey = (String) request.getSession().getAttribute( - ApplicationConnection.UIDL_SECURITY_TOKEN_ID); - if (seckey == null) { - seckey = UUID.randomUUID().toString(); - request.getSession().setAttribute( - ApplicationConnection.UIDL_SECURITY_TOKEN_ID, seckey); - } - outWriter.print("\"" + ApplicationConnection.UIDL_SECURITY_TOKEN_ID - + "\":\""); - outWriter.print(seckey); - outWriter.print("\","); + outWriter.print(getSecurityKeyUIDL(request)); } - writeUidlResponse(repaintAll, outWriter, root, analyzeLayouts); - // If the browser-window has been closed - we do not need to paint it at - // all - if (window.getName().equals(closingWindowName)) { - outWriter.print("\"changes\":[]"); - } else { - // re-get window - may have been changed - Window newWindow = doGetApplicationWindow(request, callback, - application, window); - if (newWindow != window) { - window = newWindow; - repaintAll = true; - } - - writeUidlResponse(request, callback, repaintAll, outWriter, window, - analyzeLayouts); ++ writeUidlResponse(request, repaintAll, outWriter, root, analyzeLayouts); - } closeJsonMessage(outWriter); outWriter.close(); } - public void writeUidlResponse(Request request, Callback callback, - boolean repaintAll, final PrintWriter outWriter, Window window, - boolean analyzeLayouts) throws PaintException { - outWriter.print("\"changes\":["); - - ArrayList paintables = null; - - List invalidComponentRelativeSizes = null; + /** + * Gets the security key (and generates one if needed) as UIDL. + * + * @param request + * @return the security key UIDL or "" if the feature is turned off + */ + public String getSecurityKeyUIDL(WrappedRequest request) { + final String seckey = getSecurityKey(request); + if (seckey != null) { + return "\"" + ApplicationConnection.UIDL_SECURITY_TOKEN_ID + + "\":\"" + seckey + "\","; + } else { + return ""; + } + } - JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter, - !repaintAll); - OpenWindowCache windowCache = currentlyOpenWindowsInClient.get(window - .getName()); - if (windowCache == null) { - windowCache = new OpenWindowCache(); - currentlyOpenWindowsInClient.put(window.getName(), windowCache); + /** + * Gets the security key (and generates one if needed). + * + * @param request + * @return the security key + */ + protected String getSecurityKey(WrappedRequest request) { + String seckey = null; + seckey = (String) request + .getSessionAttribute(ApplicationConnection.UIDL_SECURITY_TOKEN_ID); + if (seckey == null) { + seckey = UUID.randomUUID().toString(); + request.setSessionAttribute( + ApplicationConnection.UIDL_SECURITY_TOKEN_ID, seckey); } + return seckey; + } + + @SuppressWarnings("unchecked") - public void writeUidlResponse(boolean repaintAll, ++ public void writeUidlResponse(WrappedRequest request, boolean repaintAll, + final PrintWriter outWriter, Root root, boolean analyzeLayouts) + throws PaintException { + ArrayList dirtyVisibleConnectors = new ArrayList(); + Application application = root.getApplication(); // Paints components + DirtyConnectorTracker rootConnectorTracker = root + .getDirtyConnectorTracker(); + logger.log(Level.FINE, "* Creating response to client"); if (repaintAll) { - paintables = new ArrayList(); - paintables.add(window); + getClientCache(root).clear(); + rootConnectorTracker.markAllComponentsDirty(); // Reset sent locales locales = null; @@@ -1096,170 -1205,22 +1096,184 @@@ if (dragAndDropService != null) { dragAndDropService.printJSONResponse(outWriter); } + + writePerformanceDataForTestBench(request, outWriter); + } + + /** + * Adds the performance timing data used by TestBench 3 to the UIDL + * response. + */ - private void writePerformanceDataForTestBench(final Request request, ++ private void writePerformanceDataForTestBench(final WrappedRequest request, + final PrintWriter outWriter) { + Long totalTime = (Long) request.getAttribute("TOTAL"); + Long lastRequestTime = (Long) request.getAttribute("LASTREQUEST"); + outWriter.write(String.format(", \"tbss\":[%d, %d]", totalTime, + lastRequestTime)); } + private void legacyPaint(PaintTarget paintTarget, + ArrayList dirtyVisibleConnectors) + throws PaintException { + List legacyComponents = new ArrayList(); + for (Connector connector : dirtyVisibleConnectors) { + // All Components that want to use paintContent must implement + // Vaadin6Component + if (connector instanceof Vaadin6Component) { + legacyComponents.add((Vaadin6Component) connector); + } + } + sortByHierarchy((List) legacyComponents); + for (Vaadin6Component c : legacyComponents) { + logger.fine("Painting Vaadin6Component " + c.getClass().getName() + + "@" + Integer.toHexString(c.hashCode())); + paintTarget.startTag("change"); + final String pid = c.getConnectorId(); + paintTarget.addAttribute("pid", pid); + LegacyPaint.paint(c, paintTarget); + paintTarget.endTag("change"); + } + + } + + private void sortByHierarchy(List paintables) { + // Vaadin 6 requires parents to be painted before children as component + // containers rely on that their updateFromUIDL method has been called + // before children start calling e.g. updateCaption + Collections.sort(paintables, new Comparator() { + public int compare(Component c1, Component c2) { + int depth1 = 0; + while (c1.getParent() != null) { + depth1++; + c1 = c1.getParent(); + } + int depth2 = 0; + while (c2.getParent() != null) { + depth2++; + c2 = c2.getParent(); + } + if (depth1 < depth2) { + return -1; + } + if (depth1 > depth2) { + return 1; + } + return 0; + } + }); + + } + + private ClientCache getClientCache(Root root) { + Integer rootId = Integer.valueOf(root.getRootId()); + ClientCache cache = rootToClientCache.get(rootId); + if (cache == null) { + cache = new ClientCache(); + rootToClientCache.put(rootId, cache); + } + return cache; + } + + /** + * Checks if the component is visible in context, i.e. returns false if the + * child is hidden, the parent is hidden or the parent says the child should + * not be rendered (using + * {@link HasComponents#isComponentVisible(Component)} + * + * @param child + * The child to check + * @return true if the child is visible to the client, false otherwise + */ + static boolean isVisible(Component child) { + HasComponents parent = child.getParent(); + if (parent == null || !child.isVisible()) { + return child.isVisible(); + } + + return parent.isComponentVisible(child) && isVisible(parent); + } + + private static class NullIterator implements Iterator { + + public boolean hasNext() { + return false; + } + + public E next() { + return null; + } + + public void remove() { + } + + } + + public static Iterable getChildComponents(HasComponents cc) { + // TODO This must be moved to Root/Panel + if (cc instanceof Root) { + Root root = (Root) cc; + List children = new ArrayList(); + if (root.getContent() != null) { + children.add(root.getContent()); + } + for (Window w : root.getWindows()) { + children.add(w); + } + return children; + } else if (cc instanceof Panel) { + // This is so wrong.. (#2924) + if (((Panel) cc).getContent() == null) { + return Collections.emptyList(); + } else { + return Collections.singleton((Component) ((Panel) cc) + .getContent()); + } + } + return cc; + } + + /** + * Collects all pending RPC calls from listed {@link ClientConnector}s and + * clears their RPC queues. + * + * @param rpcPendingQueue + * list of {@link ClientConnector} of interest + * @return ordered list of pending RPC calls + */ + private List collectPendingRpcCalls( + List rpcPendingQueue) { + List pendingInvocations = new ArrayList(); + for (ClientConnector connector : rpcPendingQueue) { + List paintablePendingRpc = connector + .retrievePendingRpcCalls(); + if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { + List oldPendingRpc = pendingInvocations; + int totalCalls = pendingInvocations.size() + + paintablePendingRpc.size(); + pendingInvocations = new ArrayList( + totalCalls); + + // merge two ordered comparable lists + for (int destIndex = 0, oldIndex = 0, paintableIndex = 0; destIndex < totalCalls; destIndex++) { + if (paintableIndex >= paintablePendingRpc.size() + || (oldIndex < oldPendingRpc.size() && ((Comparable) oldPendingRpc + .get(oldIndex)) + .compareTo(paintablePendingRpc + .get(paintableIndex)) <= 0)) { + pendingInvocations.add(oldPendingRpc.get(oldIndex++)); + } else { + pendingInvocations.add(paintablePendingRpc + .get(paintableIndex++)); + } + } + } + } + return pendingInvocations; + } + + protected abstract InputStream getThemeResourceAsStream(Root root, + String themeName, String resource); + private int getTimeoutInterval() { return maxInactiveInterval; } @@@ -2057,116 -2330,10 +2071,116 @@@ } - abstract String getStreamVariableTargetUrl(VariableOwner owner, - String name, StreamVariable value); + abstract String getStreamVariableTargetUrl(Connector owner, String name, + StreamVariable value); - abstract protected void cleanStreamVariable(VariableOwner owner, String name); + abstract protected void cleanStreamVariable(Connector owner, String name); + + /** + * Gets the bootstrap handler that should be used for generating the pages + * bootstrapping applications for this communication manager. + * + * @return the bootstrap handler to use + */ + private BootstrapHandler getBootstrapHandler() { + if (bootstrapHandler == null) { + bootstrapHandler = createBootstrapHandler(); + } + + return bootstrapHandler; + } + + protected abstract BootstrapHandler createBootstrapHandler(); + + protected boolean handleApplicationRequest(WrappedRequest request, + WrappedResponse response) throws IOException { + return application.handleRequest(request, response); + } + + public void handleBrowserDetailsRequest(WrappedRequest request, + WrappedResponse response, Application application) + throws IOException { + + // if we do not yet have a currentRoot, it should be initialized + // shortly, and we should send the initial UIDL + boolean sendUIDL = Root.getCurrentRoot() == null; + + try { + CombinedRequest combinedRequest = new CombinedRequest(request); + + Root root = application.getRootForRequest(combinedRequest); + response.setContentType("application/json; charset=UTF-8"); + + // Use the same logic as for determined roots + BootstrapHandler bootstrapHandler = getBootstrapHandler(); + BootstrapContext context = bootstrapHandler.createContext( + combinedRequest, response, application, root.getRootId()); + + String widgetset = context.getWidgetsetName(); + String theme = context.getThemeName(); + String themeUri = bootstrapHandler.getThemeUri(context, theme); + + // TODO These are not required if it was only the init of the root + // that was delayed + JSONObject params = new JSONObject(); + params.put("widgetset", widgetset); + params.put("themeUri", themeUri); + // Root id might have changed based on e.g. window.name + params.put(ApplicationConnection.ROOT_ID_PARAMETER, + root.getRootId()); + if (sendUIDL) { + String initialUIDL = getInitialUIDL(combinedRequest, root); + params.put("uidl", initialUIDL); + } + + // NOTE! GateIn requires, for some weird reason, getOutputStream + // to be used instead of getWriter() (it seems to interpret + // application/json as a binary content type) + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + + outWriter.write(params.toString()); + // NOTE GateIn requires the buffers to be flushed to work + outWriter.flush(); + out.flush(); + } catch (RootRequiresMoreInformationException e) { + // Requiring more information at this point is not allowed + // TODO handle in a better way + throw new RuntimeException(e); + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Generates the initial UIDL message that can e.g. be included in a html + * page to avoid a separate round trip just for getting the UIDL. + * + * @param request + * the request that caused the initialization + * @param root + * the root for which the UIDL should be generated + * @return a string with the initial UIDL message + * @throws PaintException + * if an exception occurs while painting + */ + protected String getInitialUIDL(WrappedRequest request, Root root) + throws PaintException { + // TODO maybe unify writeUidlResponse()? + StringWriter sWriter = new StringWriter(); + PrintWriter pWriter = new PrintWriter(sWriter); + pWriter.print("{"); + if (isXSRFEnabled(root.getApplication())) { + pWriter.print(getSecurityKeyUIDL(request)); + } - writeUidlResponse(true, pWriter, root, false); ++ writeUidlResponse(request, true, pWriter, root, false); + pWriter.print("}"); + String initialUIDL = sWriter.toString(); + logger.log(Level.FINE, "Initial UIDL:" + initialUIDL); + return initialUIDL; + } /** * Stream that extracts content from another stream until the boundary diff --cc src/com/vaadin/terminal/gwt/server/RequestTimer.java index 0000000000,5ed89c2d29..d47f444bef mode 000000,100644..100644 --- a/src/com/vaadin/terminal/gwt/server/RequestTimer.java +++ b/src/com/vaadin/terminal/gwt/server/RequestTimer.java @@@ -1,0 -1,142 +1,80 @@@ + /* + @VaadinApache2LicenseForJavaFiles@ + */ + + package com.vaadin.terminal.gwt.server; + -import javax.portlet.PortletRequest; -import javax.portlet.PortletSession; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; ++import com.vaadin.terminal.WrappedRequest; + + /** + * Times the handling of requests and stores the information as an attribute in + * the request. The timing info is later passed on to the client in the UIDL and + * the client provides JavaScript API for accessing this data from e.g. + * TestBench. + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ + public class RequestTimer { + public static final String SESSION_ATTR_ID = "REQUESTTIMER"; + + private long requestStartTime = 0; + private long totalSessionTime = 0; + private long lastRequestTime = -1; + - /** - * This class acts as a proxy for setting and getting session and request - * attributes on HttpServletRequests and PortletRequests. Using this class - * we don't need to duplicate code everywhere. - */ - static class RequestWrapper { - private final HttpServletRequest servletRequest; - private final PortletRequest portletRequest; - - public RequestWrapper(HttpServletRequest servletRequest) { - this.servletRequest = servletRequest; - portletRequest = null; - } - - public RequestWrapper(PortletRequest portletRequest) { - this.portletRequest = portletRequest; - servletRequest = null; - } - - public void setAttribute(String name, Object value) { - if (servletRequest != null) { - servletRequest.setAttribute(name, value); - } else { - portletRequest.setAttribute(name, value); - } - } - - public void setSessionAttribute(String name, Object value) { - if (servletRequest != null) { - HttpSession session = servletRequest.getSession(); - if (session != null) { - session.setAttribute(name, value); - } - } else { - PortletSession portletSession = portletRequest - .getPortletSession(); - if (portletSession != null) { - portletSession.setAttribute(name, value); - } - } - } - - public Object getSessionAttribute(String name) { - if (servletRequest != null) { - HttpSession session = servletRequest.getSession(); - if (session != null) { - return session.getAttribute(name); - } - } else { - PortletSession portletSession = portletRequest - .getPortletSession(); - if (portletSession != null) { - return portletSession.getAttribute(name); - } - } - return null; - } - } - + /** + * Starts the timing of a request. This should be called before any + * processing of the request. + * + * @param request + * the request. + */ - public void start(RequestWrapper request) { ++ public void start(WrappedRequest request) { + requestStartTime = System.nanoTime(); + request.setAttribute("TOTAL", totalSessionTime); + request.setAttribute("LASTREQUEST", lastRequestTime); + } + + /** + * Stops the timing of a request. This should be called when all processing + * of a request has finished. + */ + public void stop() { + // Measure and store the total handling time. This data can be + // used in TestBench 3 tests. + long time = (System.nanoTime() - requestStartTime) / 1000000; + lastRequestTime = time; + totalSessionTime += time; + } + + /** + * Returns a valid request timer for the specified request. Timers are + * session bound. + * + * @param request + * the request for which to get a valid timer. + * @return a valid timer. + */ - public static RequestTimer get(RequestWrapper request) { ++ public static RequestTimer get(WrappedRequest request) { + RequestTimer timer = (RequestTimer) request + .getSessionAttribute(SESSION_ATTR_ID); + if (timer == null) { + timer = new RequestTimer(); + } + return timer; + } + + /** + * Associates the specified request timer with the specified request. Since + * {@link #get(RequestWrapper)} will, at one point or another, return a new + * instance, this method should be called to keep the request <-> timer + * association updated. + * + * @param request + * the request for which to set the timer. + * @param requestTimer + * the timer. + */ - public static void set(RequestWrapper request, RequestTimer requestTimer) { ++ public static void set(WrappedRequest request, RequestTimer requestTimer) { + request.setSessionAttribute(RequestTimer.SESSION_ATTR_ID, requestTimer); + } + } diff --cc tests/testbench/com/vaadin/tests/components/TouchScrollables.java index 0000000000,88335a1996..636bddead8 mode 000000,100644..100644 --- a/tests/testbench/com/vaadin/tests/components/TouchScrollables.java +++ b/tests/testbench/com/vaadin/tests/components/TouchScrollables.java @@@ -1,0 -1,268 +1,267 @@@ + package com.vaadin.tests.components; + + import java.util.Collection; + + import com.vaadin.data.Item; + import com.vaadin.data.util.IndexedContainer; + import com.vaadin.event.Action; + import com.vaadin.event.Action.Handler; + import com.vaadin.event.DataBoundTransferable; + import com.vaadin.event.dd.DragAndDropEvent; + import com.vaadin.event.dd.DropHandler; + import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; + import com.vaadin.event.dd.acceptcriteria.SourceIs; + import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation; + import com.vaadin.tests.util.Person; + import com.vaadin.tests.util.PersonContainer; + import com.vaadin.tests.util.TestUtils; + import com.vaadin.ui.AbstractComponent; + import com.vaadin.ui.AbstractSelect.AbstractSelectTargetDetails; + import com.vaadin.ui.Button; + import com.vaadin.ui.Button.ClickEvent; + import com.vaadin.ui.Component; + import com.vaadin.ui.CssLayout; + import com.vaadin.ui.HorizontalLayout; + import com.vaadin.ui.Label; + import com.vaadin.ui.Panel; + import com.vaadin.ui.Table; + + public class TouchScrollables extends TestBase { + java.util.Random r = new java.util.Random(1); + + private HorizontalLayout testSelector; + + @Override + public void setup() { + testSelector = new HorizontalLayout(); + getLayout().addComponent(testSelector); + + CssLayout cssLayout = new CssLayout(); + final Table table = new Table(); + + Button button = new Button("Toggle lazyloading"); + button.addListener(new Button.ClickListener() { + public void buttonClick(ClickEvent event) { + if (table.getCacheRate() == 100) { + table.setCacheRate(2); + table.setPageLength(15); + } else { + table.setCacheRate(100); + table.setHeight("400px"); + } + } + }); + cssLayout.addComponent(button); + + button = new Button("Toggle selectable"); + button.addListener(new Button.ClickListener() { + public void buttonClick(ClickEvent event) { + table.setSelectable(!table.isSelectable()); + } + }); + cssLayout.addComponent(button); + + table.addContainerProperty("foo", String.class, "bar"); + table.setRowHeaderMode(Table.ROW_HEADER_MODE_INDEX); + for (int i = 0; i < 1000; i++) { + table.addItem(); + } + cssLayout.addComponent(table); + cssLayout.setCaption("Table"); + + addTest(cssLayout); + + cssLayout = new CssLayout(); + cssLayout.setCaption("Panel"); + + final Panel p = new Panel(); - p.setScrollable(true); + p.setHeight("400px"); + p.setCaption("Panel"); + Label l50 = null; + for (int i = 0; i < 100; i++) { + Label c = new Label("Label" + i); + p.addComponent(c); + if (i == 50) { + l50 = c; + } + } + + final Label l = l50; + button = new Button("Scroll to label 50", new Button.ClickListener() { + public void buttonClick(ClickEvent event) { - getLayout().getWindow().scrollIntoView(l); ++ getLayout().getRoot().scrollIntoView(l); + } + }); + cssLayout.addComponent(button); + button = new Button("Scroll to 100px", new Button.ClickListener() { + public void buttonClick(ClickEvent event) { + p.setScrollTop(100); + } + }); + cssLayout.addComponent(button); + cssLayout.addComponent(p); + + addTest(cssLayout); + + TestUtils + .injectCSS( - getLayout().getWindow(), ++ getLayout().getRoot(), + "body * {-webkit-user-select: none;} .v-table-row-drag-middle .v-table-cell-content {" + + " background-color: inherit ; border-bottom: 1px solid cyan;" + + "}" + + ".v-table-row-drag-middle .v-table-cell-wrapper {" + + " margin-bottom: -1px;" + "}" + "" + + ); + + addDDSortableTable(); + + } + + private void addDDSortableTable() { + final Table table; + table = new Table(); + table.setCaption("DD sortable table with context menus"); + // table.setWidth("100%"); + table.setPageLength(10); + table.setRowHeaderMode(Table.ROW_HEADER_MODE_ID); + table.setSelectable(true); + table.setMultiSelect(true); + + table.addActionHandler(new Handler() { + + Action[] actions = new Action[] { new Action("FOO"), + new Action("BAR"), new Action("CAR") }; + + public Action[] getActions(Object target, Object sender) { + return actions; + } + + public void handleAction(Action action, Object sender, Object target) { - getLayout().getWindow().showNotification(action.getCaption()); ++ getLayout().getRoot().showNotification(action.getCaption()); + + } + }); + + populateTable(table); + + /* + * Make table rows draggable + */ + table.setDragMode(Table.TableDragMode.ROW); + + table.setDropHandler(new DropHandler() { + // accept only drags from this table + AcceptCriterion crit = new SourceIs(table); + + public AcceptCriterion getAcceptCriterion() { + return crit; + } + + public void drop(DragAndDropEvent dropEvent) { + AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dropEvent + .getTargetDetails(); + DataBoundTransferable transferable = (DataBoundTransferable) dropEvent + .getTransferable(); + Object itemIdOver = dropTargetData.getItemIdOver(); + Object itemId = transferable.getItemId(); + if (itemId == null || itemIdOver == null + || itemId.equals(itemIdOver)) { + return; // no move happened + } + + // IndexedContainer goodies... (hint: don't use it in real apps) + IndexedContainer containerDataSource = (IndexedContainer) table + .getContainerDataSource(); + int newIndex = containerDataSource.indexOfId(itemIdOver) - 1; + if (dropTargetData.getDropLocation() != VerticalDropLocation.TOP) { + newIndex++; + } + if (newIndex < 0) { + newIndex = 0; + } + Object idAfter = containerDataSource.getIdByIndex(newIndex); + Collection selections = (Collection) table.getValue(); + if (selections != null && selections.contains(itemId)) { + // dragged a selected item, if multiple rows selected, drag + // them too (functionality similar to apple mail) + for (Object object : selections) { + moveAfter(containerDataSource, object, idAfter); + } + + } else { + // move just the dragged row, not considering selection at + // all + moveAfter(containerDataSource, itemId, idAfter); + } + + } + + private void moveAfter(IndexedContainer containerDataSource, + Object itemId, Object idAfter) { + try { + IndexedContainer clone = null; + clone = (IndexedContainer) containerDataSource.clone(); + containerDataSource.removeItem(itemId); + Item newItem = containerDataSource.addItemAfter(idAfter, + itemId); + Item item = clone.getItem(itemId); + for (Object propId : item.getItemPropertyIds()) { + newItem.getItemProperty(propId).setValue( + item.getItemProperty(propId).getValue()); + } + + // TODO Auto-generated method stub + } catch (CloneNotSupportedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + }); + addTest(table); + } + + private void populateTable(Table table) { + table.addContainerProperty("Name", String.class, ""); + table.addContainerProperty("Weight", Integer.class, 0); + + PersonContainer testData = PersonContainer.createWithTestData(); + + for (int i = 0; i < 40; i++) { + Item addItem = table.addItem("Item" + i); + Person p = testData.getIdByIndex(i); + addItem.getItemProperty("Name").setValue( + p.getFirstName() + " " + p.getLastName()); + addItem.getItemProperty("Weight").setValue(50 + r.nextInt(60)); + } + + } + + private Component testComponent; + + private void addTest(final AbstractComponent t) { + Button button = new Button(t.getCaption()); + testSelector.addComponent(button); + button.addListener(new Button.ClickListener() { + + public void buttonClick(ClickEvent event) { + if (testComponent != null) { + getLayout().removeComponent(testComponent); + } + testComponent = t; + getLayout().addComponent(t); + } + }); + } + + @Override + protected String getDescription() { + return "Various components and setups suitable for testing scrolling on touch devices."; + } + + @Override + protected Integer getTicketNumber() { + return null; + } + } diff --cc tests/testbench/com/vaadin/tests/components/table/TestCurrentPageFirstItem.java index 1748c27426,2bc03f9814..7fb096739a --- a/tests/testbench/com/vaadin/tests/components/table/TestCurrentPageFirstItem.java +++ b/tests/testbench/com/vaadin/tests/components/table/TestCurrentPageFirstItem.java @@@ -7,12 -7,10 +7,12 @@@ import com.vaadin.tests.components.Test import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.Root.LegacyWindow; + import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Table; +import com.vaadin.ui.VerticalLayout; - public class TestCurrentPageFirstItem extends Application.LegacyApplication - implements ClickListener { + public class TestCurrentPageFirstItem extends TestBase implements ClickListener { private Button buttonIndex; private Button buttonItem;