diff options
author | Artur Signell <artur@vaadin.com> | 2016-08-18 21:58:46 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2016-08-20 00:08:47 +0300 |
commit | 65370e12a0605926cb80e205c2b0e74fefe83e5b (patch) | |
tree | 013b7a60741db3c902bb1a8ea29733aa117bd0b2 /compatibility-client | |
parent | 34852cdb88c6c27b1341684204d78db0fdd061a6 (diff) | |
download | vaadin-framework-65370e12a0605926cb80e205c2b0e74fefe83e5b.tar.gz vaadin-framework-65370e12a0605926cb80e205c2b0e74fefe83e5b.zip |
Move Table/TreeTable to compatibility package
Change-Id: Ic9f2badf8688c32d704be67519c0f4c9a3da0e28
Diffstat (limited to 'compatibility-client')
5 files changed, 10040 insertions, 0 deletions
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/VScrollTable.java b/compatibility-client/src/main/java/com/vaadin/client/ui/VScrollTable.java new file mode 100644 index 0000000000..24ea202189 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/VScrollTable.java @@ -0,0 +1,8413 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui; + +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.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +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.Element; +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.Overflow; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.TextAlign; +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.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.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; +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.UIObject; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.BrowserInfo; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.DeferredWorker; +import com.vaadin.client.Focusable; +import com.vaadin.client.HasChildMeasurementHintConnector.ChildMeasurementHint; +import com.vaadin.client.MouseEventDetailsBuilder; +import com.vaadin.client.StyleConstants; +import com.vaadin.client.TooltipInfo; +import com.vaadin.client.UIDL; +import com.vaadin.client.Util; +import com.vaadin.client.VConsole; +import com.vaadin.client.VTooltip; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.VScrollTable.VScrollTableBody.VScrollTableRow; +import com.vaadin.client.ui.dd.DDUtil; +import com.vaadin.client.ui.dd.VAbstractDropHandler; +import com.vaadin.client.ui.dd.VAcceptCallback; +import com.vaadin.client.ui.dd.VDragAndDropManager; +import com.vaadin.client.ui.dd.VDragEvent; +import com.vaadin.client.ui.dd.VHasDropHandler; +import com.vaadin.client.ui.dd.VTransferable; +import com.vaadin.client.v7.ui.VLegacyTextField; +import com.vaadin.shared.AbstractComponentState; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.shared.ui.table.CollapseMenuContent; +import com.vaadin.shared.ui.table.TableConstants; + +/** + * 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, SubPartAware, DeferredWorker { + + /** + * Simple interface for parts of the table capable of owning a context menu. + * + * @since 7.2 + * @author Vaadin Ltd + */ + private interface ContextMenuOwner { + public void showContextMenu(Event event); + } + + /** + * Handles showing context menu on "long press" from a touch screen. + * + * @since 7.2 + * @author Vaadin Ltd + */ + private class TouchContextProvider { + private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500; + private Timer contextTouchTimeout; + + private Event touchStart; + private int touchStartY; + private int touchStartX; + + private ContextMenuOwner target; + + /** + * Initializes a handler for a certain context menu owner. + * + * @param target + * the owner of the context menu + */ + public TouchContextProvider(ContextMenuOwner target) { + this.target = target; + } + + /** + * Cancels the current context touch timeout. + */ + public void cancel() { + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + contextTouchTimeout = null; + } + touchStart = null; + } + + /** + * A function to handle touch context events in a table. + * + * @param event + * browser event to handle + */ + public void handleTouchEvent(final Event event) { + int type = event.getTypeInt(); + + switch (type) { + case Event.ONCONTEXTMENU: + target.showContextMenu(event); + break; + case Event.ONTOUCHSTART: + // save position to fields, touches in events are same + // instance during the operation. + touchStart = event; + + Touch touch = event.getChangedTouches().get(0); + touchStartX = touch.getClientX(); + touchStartY = touch.getClientY(); + + if (contextTouchTimeout == null) { + contextTouchTimeout = new Timer() { + + @Override + public void run() { + if (touchStart != null) { + // Open the context menu if finger + // is held in place long enough. + target.showContextMenu(touchStart); + event.preventDefault(); + touchStart = null; + } + } + }; + } + contextTouchTimeout.schedule(TOUCH_CONTEXT_MENU_TIMEOUT); + break; + case Event.ONTOUCHCANCEL: + case Event.ONTOUCHEND: + cancel(); + break; + case Event.ONTOUCHMOVE: + if (isSignificantMove(event)) { + // Moved finger before the context menu timer + // expired, so let the browser handle the event. + cancel(); + } + } + } + + /** + * Calculates how many pixels away the user's finger has traveled. This + * reduces the chance of small non-intentional movements from canceling + * the long press detection. + * + * @param event + * the Event for which to check the move distance + * @return true if this is considered an intentional move by the user + */ + protected boolean isSignificantMove(Event event) { + if (touchStart == null) { + // no touch start + return false; + } + + // Calculate the distance between touch start and the current touch + // position + Touch touch = event.getChangedTouches().get(0); + int deltaX = touch.getClientX() - touchStartX; + int deltaY = touch.getClientY() - touchStartY; + int delta = deltaX * deltaX + deltaY * deltaY; + + // Compare to the square of the significant move threshold to remove + // the need for a square root + if (delta > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD + * TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) { + return true; + } + return false; + } + } + + public static final String STYLENAME = "v-table"; + + 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"; + + 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 pageLength 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" + private int firstvisibleOnLastPage = -1; // To detect if the first visible + // is on the last page + + /** For internal use only. May be removed or replaced in the future. */ + public boolean showRowHeaders = false; + + private String[] columnOrder; + + protected ApplicationConnection client; + + /** For internal use only. May be removed or replaced in the future. */ + public String paintableId; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean immediate; + + private boolean updatedReqRows = true; + + private boolean nullSelectionAllowed = true; + + private SelectMode selectMode = SelectMode.NONE; + + public final HashSet<String> selectedRowKeys = new HashSet<String>(); + + /* + * When scrolling and selecting at the same time, the selections are not in + * sync with the server while retrieving new rows (until key is released). + */ + private HashSet<Object> unSyncedselectionsBeforeRowFetch; + + /* + * These are used when jumping between pages when pressing Home and End + */ + + /** For internal use only. May be removed or replaced in the future. */ + public boolean selectLastItemInNextRender = false; + /** For internal use only. May be removed or replaced in the future. */ + public boolean selectFirstItemInNextRender = false; + /** For internal use only. May be removed or replaced in the future. */ + public boolean focusFirstItemInNextRender = false; + /** For internal use only. May be removed or replaced in the future. */ + public boolean focusLastItemInNextRender = false; + + /** + * The currently focused row. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public VScrollTableRow focusedRow; + + /** + * Helper to store selection range start in when using the keyboard + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public VScrollTableRow selectionRangeStart; + + /** + * Flag for notifying when the selection has changed and should be sent to + * the server + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public boolean selectionChanged = false; + + /* + * The speed (in pixels) which the scrolling scrolls vertically/horizontally + */ + private int scrollingVelocity = 10; + + private Timer scrollingVelocityTimer = null; + + /** For internal use only. May be removed or replaced in the future. */ + public String[] bodyActionKeys; + + private boolean enableDebug = false; + + private static final boolean hasNativeTouchScrolling = BrowserInfo.get() + .isTouchDevice() + && !BrowserInfo.get().requiresTouchScrollDelegate(); + + private Set<String> noncollapsibleColumns; + + /** + * The last known row height used to preserve the height of a table with + * custom row heights and a fixed page length after removing the last row + * from the table. + * + * A new VScrollTableBody instance is created every time the number of rows + * changes causing {@link VScrollTableBody#rowHeight} to be discarded and + * the height recalculated by {@link VScrollTableBody#getRowHeight(boolean)} + * to avoid some rounding problems, e.g. round(2 * 19.8) / 2 = 20 but + * round(3 * 19.8) / 3 = 19.66. + */ + private double lastKnownRowHeight = Double.NaN; + + /** + * Remember scroll position when getting detached to properly scroll back to + * the location that there is data for if getting attached again. + */ + private int detachedScrollPosition = 0; + + /** + * Represents a select range of rows + */ + private class SelectionRange { + private VScrollTableRow startRow; + private final int length; + + /** + * Constuctor. + */ + public SelectionRange(VScrollTableRow row1, VScrollTableRow row2) { + VScrollTableRow endRow; + if (row2.isBefore(row1)) { + startRow = row2; + endRow = row1; + } else { + startRow = row1; + endRow = row2; + } + length = endRow.getIndex() - startRow.getIndex() + 1; + } + + public SelectionRange(VScrollTableRow row, int length) { + startRow = row; + this.length = length; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + + @Override + public String toString() { + return startRow.getKey() + "-" + length; + } + + private boolean inRange(VScrollTableRow row) { + return row.getIndex() >= startRow.getIndex() + && row.getIndex() < startRow.getIndex() + length; + } + + public Collection<SelectionRange> split(VScrollTableRow row) { + assert row.isAttached(); + ArrayList<SelectionRange> ranges = new ArrayList<SelectionRange>(2); + + int endOfFirstRange = row.getIndex() - 1; + if (endOfFirstRange >= startRow.getIndex()) { + // 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) { + // create range of second part unless its length is < 1 + VScrollTableRow startOfRange = scrollBody + .getRowByRowIndex(startOfSecondRange); + if (startOfRange != null) { + ranges.add(new SelectionRange(startOfRange, + getEndIndex() - startOfSecondRange + 1)); + } + } + return ranges; + } + + private int getEndIndex() { + return startRow.getIndex() + length - 1; + } + + } + + private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>(); + + /** For internal use only. May be removed or replaced in the future. */ + public boolean initializedAndAttached = false; + + /** + * Flag to indicate if a column width recalculation is needed due update. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public boolean headerChangedDuringUpdate = false; + + /** For internal use only. May be removed or replaced in the future. */ + public final TableHead tHead = new TableHead(); + + /** For internal use only. May be removed or replaced in the future. */ + public final TableFooter tFoot = new TableFooter(); + + /** Handles context menu for table body */ + private ContextMenuOwner contextMenuOwner = new ContextMenuOwner() { + + @Override + public void showContextMenu(Event event) { + int left = WidgetUtil.getTouchOrMouseClientX(event); + int top = WidgetUtil.getTouchOrMouseClientY(event); + boolean menuShown = handleBodyContextMenu(left, top); + if (menuShown) { + event.stopPropagation(); + event.preventDefault(); + } + } + }; + + /** Handles touch events to display a context menu for table body */ + private TouchContextProvider touchContextProvider = new TouchContextProvider( + contextMenuOwner); + + /** + * For internal use only. May be removed or replaced in the future. + * + * Overwrites onBrowserEvent function on FocusableScrollPanel to give event + * access to touchContextProvider. Has to be public to give TableConnector + * access to the scrollBodyPanel field. + * + * @since 7.2 + * @author Vaadin Ltd + */ + public class FocusableScrollContextPanel extends FocusableScrollPanel { + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + touchContextProvider.handleTouchEvent(event); + }; + + public FocusableScrollContextPanel(boolean useFakeFocusElement) { + super(useFakeFocusElement); + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public final FocusableScrollContextPanel scrollBodyPanel = new FocusableScrollContextPanel( + true); + + private KeyPressHandler navKeyPressHandler = new KeyPressHandler() { + + @Override + 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() { + + @Override + 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() { + + @Override + 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(); + } + } + }; + + /** For internal use only. May be removed or replaced in the future. */ + public int totalRows; + + private Set<String> collapsedColumns; + + /** For internal use only. May be removed or replaced in the future. */ + public final RowRequestHandler rowRequestHandler; + + /** For internal use only. May be removed or replaced in the future. */ + public VScrollTableBody scrollBody; + + private int firstvisible = 0; + private boolean sortAscending; + private String sortColumn; + private String oldSortColumn; + private boolean columnReordering; + + /** + * This map contains captions and icon urls for actions like: * "33_c" -> + * "Edit" * "33_i" -> "http://dom.com/edit.png" + */ + private final HashMap<Object, String> actionMap = new HashMap<Object, String>(); + private String[] visibleColOrder; + private boolean initialContentReceived = false; + private Element scrollPositionElement; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean enabled; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean showColHeaders; + + /** For internal use only. May be removed or replaced in the future. */ + public 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. See also variable <code>refreshContentWidths</code> + * in method {@link TableHead#updateCellsFromUIDL(UIDL)}. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public boolean recalcWidths = false; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean rendering = false; + + private boolean hasFocus = false; + private int dragmode; + + private int multiselectmode; + + /** + * Hint for how to handle measurement of child components + */ + private ChildMeasurementHint childMeasurementHint = ChildMeasurementHint.MEASURE_ALWAYS; + + /** For internal use only. May be removed or replaced in the future. */ + public int tabIndex; + + private TouchScrollDelegate touchScrollDelegate; + + /** For internal use only. May be removed or replaced in the future. */ + public 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) + * + * For internal use only. May be removed or replaced in the future. + */ + public int serverCacheFirst = -1; + public int serverCacheLast = -1; + + /** + * In several cases TreeTable depends on the scrollBody.lastRendered being + * 'out of sync' while the update is being done. In those cases the sanity + * check must be performed afterwards. + */ + public boolean postponeSanityCheckForLastRendered; + + /** For internal use only. May be removed or replaced in the future. */ + public 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. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public class ContextMenuDetails implements CloseHandler<PopupPanel> { + public String rowKey; + public int left; + public int top; + HandlerRegistration closeRegistration; + + public ContextMenuDetails(VContextMenu menu, String rowKey, int left, + int top) { + this.rowKey = rowKey; + this.left = left; + this.top = top; + closeRegistration = menu.addCloseHandler(this); + } + + @Override + public void onClose(CloseEvent<PopupPanel> event) { + contextMenu = null; + closeRegistration.removeHandler(); + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public ContextMenuDetails contextMenu = null; + + private boolean hadScrollBars = false; + + private HandlerRegistration addCloseHandler; + + /** + * Changes to manage mouseDown and mouseUp + */ + /** + * The element where the last mouse down event was registered. + */ + private Element lastMouseDownTarget; + + /** + * Set to true by {@link #mouseUpPreviewHandler} if it gets a mouseup at the + * same element as {@link #lastMouseDownTarget}. + */ + private boolean mouseUpPreviewMatched = false; + + private HandlerRegistration mouseUpEventPreviewRegistration; + + /** + * Previews events after a mousedown to detect where the following mouseup + * hits. + */ + private final NativePreviewHandler mouseUpPreviewHandler = new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + if (event.getTypeInt() == Event.ONMOUSEUP) { + mouseUpEventPreviewRegistration.removeHandler(); + + // Event's reported target not always correct if event + // capture is in use + Element elementUnderMouse = WidgetUtil + .getElementUnderMouse(event.getNativeEvent()); + if (lastMouseDownTarget != null && lastMouseDownTarget + .isOrHasChild(elementUnderMouse)) { + mouseUpPreviewMatched = true; + } else { + getLogger().log(Level.FINEST, + "Ignoring mouseup from " + elementUnderMouse + + " when mousedown was on " + + lastMouseDownTarget); + } + } + } + }; + + public VScrollTable() { + setMultiSelectMode(MULTISELECT_MODE_DEFAULT); + + scrollBodyPanel.addFocusHandler(this); + scrollBodyPanel.addBlurHandler(this); + + scrollBodyPanel.addScrollHandler(this); + + /* + * 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 | Event.ONCONTEXTMENU); + + setStyleName(STYLENAME); + + add(tHead); + add(scrollBodyPanel); + add(tFoot); + + rowRequestHandler = new RowRequestHandler(); + } + + @Override + public void setStyleName(String style) { + updateStyleNames(style, false); + } + + @Override + public void setStylePrimaryName(String style) { + updateStyleNames(style, true); + } + + private void updateStyleNames(String newStyle, boolean isPrimary) { + scrollBodyPanel + .removeStyleName(getStylePrimaryName() + "-body-wrapper"); + scrollBodyPanel.removeStyleName(getStylePrimaryName() + "-body"); + + if (scrollBody != null) { + scrollBody.removeStyleName( + getStylePrimaryName() + "-body-noselection"); + } + + if (isPrimary) { + super.setStylePrimaryName(newStyle); + } else { + super.setStyleName(newStyle); + } + + scrollBodyPanel.addStyleName(getStylePrimaryName() + "-body-wrapper"); + scrollBodyPanel.addStyleName(getStylePrimaryName() + "-body"); + + tHead.updateStyleNames(getStylePrimaryName()); + tFoot.updateStyleNames(getStylePrimaryName()); + + if (scrollBody != null) { + scrollBody.updateStyleNames(getStylePrimaryName()); + } + } + + public void init(ApplicationConnection client) { + this.client = client; + // Add a handler to clear saved context menu details when the menu + // closes. See #8526. + addCloseHandler = client.getContextMenu() + .addCloseHandler(new CloseHandler<PopupPanel>() { + + @Override + public void onClose(CloseEvent<PopupPanel> event) { + contextMenu = null; + } + }); + } + + /** + * Handles a context menu event on table body. + * + * @param left + * left position of the context menu + * @param top + * top position of the context menu + * @return true if a context menu was shown, otherwise false + */ + private boolean handleBodyContextMenu(int left, int top) { + if (enabled && bodyActionKeys != null) { + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + client.getContextMenu().showAt(this, left, top); + return true; + } + return false; + } + + /** + * Fires a column resize event which sends the resize information to the + * server. + * + * @param columnId + * The columnId of the column which was resized + * @param originalWidth + * The width in pixels of the column before the resize event + * @param newWidth + * The width in pixels of the column after the resize event + */ + private void fireColumnResizeEvent(String columnId, int originalWidth, + int newWidth) { + client.updateVariable(paintableId, "columnResizeEventColumn", columnId, + false); + client.updateVariable(paintableId, "columnResizeEventPrev", + originalWidth, false); + client.updateVariable(paintableId, "columnResizeEventCurr", newWidth, + immediate); + + } + + /** + * Non-immediate variable update of column widths for a collection of + * columns. + * + * @param columns + * the columns to trigger the events for. + */ + private void sendColumnWidthUpdates(Collection<HeaderCell> columns) { + String[] newSizes = new String[columns.size()]; + int ix = 0; + for (HeaderCell cell : columns) { + newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth(); + } + client.updateVariable(paintableId, "columnWidthUpdates", newSizes, + false); + } + + /** + * Moves the focus one step down + * + * @return Returns true if succeeded + */ + private boolean moveFocusDown() { + return moveFocusDown(0); + } + + /** + * Moves the focus down by 1+offset rows + * + * @return Returns true if succeeded, else false if the selection could not + * be move downwards + */ + private boolean moveFocusDown(int offset) { + if (isSelectable()) { + if (focusedRow == null && scrollBody.iterator().hasNext()) { + // FIXME should focus first visible from top, not first rendered + // ?? + return setRowFocus( + (VScrollTableRow) scrollBody.iterator().next()); + } else { + VScrollTableRow next = getNextRow(focusedRow, offset); + if (next != null) { + return setRowFocus(next); + } + } + } + + return false; + } + + /** + * Moves the selection one step up + * + * @return Returns true if succeeded + */ + private boolean moveFocusUp() { + return moveFocusUp(0); + } + + /** + * Moves the focus row upwards + * + * @return Returns true if succeeded, else false if the selection could not + * be move upwards + * + */ + private boolean moveFocusUp(int offset) { + if (isSelectable()) { + if (focusedRow == null && scrollBody.iterator().hasNext()) { + // FIXME logic is exactly the same as in moveFocusDown, should + // be the opposite?? + return setRowFocus( + (VScrollTableRow) scrollBody.iterator().next()); + } else { + VScrollTableRow prev = getPreviousRow(focusedRow, offset); + if (prev != null) { + return setRowFocus(prev); + } else { + VConsole.log("no previous available"); + } + } + } + + return false; + } + + /** + * Selects a row where the current selection head is + * + * @param ctrlSelect + * Is the selection a ctrl+selection + * @param shiftSelect + * Is the selection a shift+selection + * @return Returns truw + */ + private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) { + if (focusedRow != null) { + // Arrows moves the selection and clears previous selections + if (isSelectable() && !ctrlSelect && !shiftSelect) { + deselectAll(); + focusedRow.toggleSelection(); + selectionRangeStart = focusedRow; + } else if (isSelectable() && ctrlSelect && !shiftSelect) { + // Ctrl+arrows moves selection head + selectionRangeStart = focusedRow; + // No selection, only selection head is moved + } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) { + // Shift+arrows selection selects a range + focusedRow.toggleShiftSelection(shiftSelect); + } + } + } + + /** + * Sends the selection to the server if changed since the last update/visit. + */ + protected void sendSelectedRows() { + sendSelectedRows(immediate); + } + + private void updateFirstVisibleAndSendSelectedRows() { + updateFirstVisibleRow(); + sendSelectedRows(immediate); + } + + /** + * Sends the selection to the server if it has been changed since the last + * update/visit. + * + * @param immediately + * set to true to immediately send the rows + */ + protected void sendSelectedRows(boolean immediately) { + // Don't send anything if selection has not changed + if (!selectionChanged) { + return; + } + + // Reset selection changed flag + selectionChanged = false; + + // Note: changing the immediateness of this might require changes to + // "clickEvent" immediateness also. + if (isMultiSelectModeDefault()) { + // Convert ranges to a set of strings + Set<String> ranges = new HashSet<String>(); + for (SelectionRange range : selectedRowRanges) { + ranges.add(range.toString()); + } + + // Send the selected row ranges + client.updateVariable(paintableId, "selectedRanges", + ranges.toArray(new String[selectedRowRanges.size()]), + false); + selectedRowRanges.clear(); + + // clean selectedRowKeys so that they don't contain excess values + for (Iterator<String> iterator = selectedRowKeys + .iterator(); iterator.hasNext();) { + String key = iterator.next(); + VScrollTableRow renderedRowByKey = getRenderedRowByKey(key); + if (renderedRowByKey != null) { + for (SelectionRange range : selectedRowRanges) { + if (range.inRange(renderedRowByKey)) { + iterator.remove(); + } + } + } else { + // orphaned selected key, must be in a range, ignore + iterator.remove(); + } + + } + } + + // Send the selected rows + client.updateVariable(paintableId, "selected", + selectedRowKeys.toArray(new String[selectedRowKeys.size()]), + immediately); + + } + + /** + * Get the key that moves the selection head upwards. By default it is the + * up arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that moves the selection head downwards. By default it is the + * down arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that scrolls to the left in the table. By default it is the + * left arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that scroll to the right on the table. By default it is the + * right arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * Get the key that selects an item in the table. By default it is the space + * bar key but by overriding this you can change the key to whatever you + * want. + * + * @return + */ + protected int getNavigationSelectKey() { + return CHARCODE_SPACE; + } + + /** + * Get the key the moves the selection one page up in the table. By default + * this is the Page Up key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationPageUpKey() { + return KeyCodes.KEY_PAGEUP; + } + + /** + * Get the key the moves the selection one page down in the table. By + * default this is the Page Down key but by overriding this you can change + * the key to whatever you want. + * + * @return + */ + protected int getNavigationPageDownKey() { + return KeyCodes.KEY_PAGEDOWN; + } + + /** + * Get the key the moves the selection to the beginning of the table. By + * default this is the Home key but by overriding this you can change the + * key to whatever you want. + * + * @return + */ + protected int getNavigationStartKey() { + return KeyCodes.KEY_HOME; + } + + /** + * Get the key the moves the selection to the end of the table. By default + * this is the End key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationEndKey() { + return KeyCodes.KEY_END; + } + + /** For internal use only. May be removed or replaced in the future. */ + public void initializeRows(UIDL uidl, UIDL rowData) { + if (scrollBody != null) { + scrollBody.removeFromParent(); + } + + // Without this call the scroll position is messed up in IE even after + // the lazy scroller has set the scroll position to the first visible + // item + int pos = scrollBodyPanel.getScrollPosition(); + + // Reset first row in view port so client requests correct last row. + if (pos == 0) { + firstRowInViewPort = 0; + } + + scrollBody = createScrollBody(); + + scrollBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"), + uidl.getIntAttribute("rows")); + scrollBodyPanel.add(scrollBody); + + initialContentReceived = true; + sizeNeedsInit = true; + scrollBody.restoreRowVisibility(); + } + + /** For internal use only. May be removed or replaced in the future. */ + public 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")); + if (uidl.hasVariable("noncollapsiblecolumns")) { + noncollapsibleColumns = uidl + .getStringArrayVariableAsSet("noncollapsiblecolumns"); + } + } + + 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; + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public boolean selectSelectedRows(UIDL uidl) { + boolean keyboardSelectionOverRowFetchInProgress = false; + + if (uidl.hasVariable("selected")) { + final Set<String> selectedKeys = uidl + .getStringArrayVariableAsSet("selected"); + // Do not update focus if there is a single selected row + // that is the same as the previous selection. This prevents + // unwanted scrolling (#18247). + boolean rowsUnSelected = removeUnselectedRowKeys(selectedKeys); + boolean updateFocus = rowsUnSelected || selectedRowKeys.size() == 0 + || focusedRow == null; + if (scrollBody != null) { + Iterator<Widget> iterator = scrollBody.iterator(); + while (iterator.hasNext()) { + /* + * Make the focus reflect to the server side state unless we + * are currently selecting multiple rows with keyboard. + */ + VScrollTableRow row = (VScrollTableRow) iterator.next(); + boolean selected = selectedKeys.contains(row.getKey()); + if (!selected && unSyncedselectionsBeforeRowFetch != null + && unSyncedselectionsBeforeRowFetch + .contains(row.getKey())) { + selected = true; + keyboardSelectionOverRowFetchInProgress = true; + } + if (selected && selectedKeys.size() == 1 && updateFocus) { + /* + * If a single item is selected, move focus to the + * selected row. (#10522) + */ + setRowFocus(row); + } + + 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; + } + + private boolean removeUnselectedRowKeys(final Set<String> selectedKeys) { + List<String> unselectedKeys = new ArrayList<String>(0); + for (String key : selectedRowKeys) { + if (!selectedKeys.contains(key)) { + unselectedKeys.add(key); + } + } + return selectedRowKeys.removeAll(unselectedKeys); + } + + /** For internal use only. May be removed or replaced in the future. */ + public void updateSortingProperties(UIDL uidl) { + oldSortColumn = sortColumn; + if (uidl.hasVariable("sortascending")) { + sortAscending = uidl.getBooleanVariable("sortascending"); + sortColumn = uidl.getStringVariable("sortcolumn"); + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public 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) { + // Mark header as sorted now. Any earlier marking would lead to + // columns with wrong sizes + sortedHeader.setSorted(true); + 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); + } + } + + private boolean lazyScrollerIsActive; + + private void disableLazyScroller() { + lazyScrollerIsActive = false; + scrollBodyPanel.getElement().getStyle().clearOverflowX(); + scrollBodyPanel.getElement().getStyle().clearOverflowY(); + } + + private void enableLazyScroller() { + Scheduler.get().scheduleDeferred(lazyScroller); + lazyScrollerIsActive = true; + // prevent scrolling to jump in IE11 + scrollBodyPanel.getElement().getStyle().setOverflowX(Overflow.HIDDEN); + scrollBodyPanel.getElement().getStyle().setOverflowY(Overflow.HIDDEN); + } + + private boolean isLazyScrollerActive() { + return lazyScrollerIsActive; + } + + private ScheduledCommand lazyScroller = new ScheduledCommand() { + + @Override + public void execute() { + if (firstvisible >= 0) { + firstRowInViewPort = firstvisible; + if (firstvisibleOnLastPage > -1) { + scrollBodyPanel.setScrollPosition( + measureRowHeightOffset(firstvisibleOnLastPage)); + } else { + scrollBodyPanel.setScrollPosition( + measureRowHeightOffset(firstvisible)); + } + } + disableLazyScroller(); + } + }; + + /** For internal use only. May be removed or replaced in the future. */ + public void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) { + firstvisible = uidl.hasVariable("firstvisible") + ? uidl.getIntVariable("firstvisible") : 0; + firstvisibleOnLastPage = uidl.hasVariable("firstvisibleonlastpage") + ? uidl.getIntVariable("firstvisibleonlastpage") : -1; + if (firstvisible != lastRequestedFirstvisible && scrollBody != null) { + + // Update lastRequestedFirstvisible right away here + // (don't rely on update in the timer which could be cancelled). + lastRequestedFirstvisible = firstRowInViewPort; + + // Only scroll if the first visible changes from the server side. + // Else we might unintentionally scroll even when the scroll + // position has not changed. + enableLazyScroller(); + } + } + + protected int measureRowHeightOffset(int rowIx) { + return (int) (rowIx * scrollBody.getRowHeight()); + } + + /** For internal use only. May be removed or replaced in the future. */ + public void updatePageLength(UIDL uidl) { + int oldPageLength = pageLength; + if (uidl.hasAttribute("pagelength")) { + pageLength = uidl.getIntAttribute("pagelength"); + } else { + // pagelength is "0" meaning scrolling is turned off + pageLength = totalRows; + } + + if (oldPageLength != pageLength && initializedAndAttached) { + // page length changed, need to update size + sizeNeedsInit = true; + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public void updateSelectionProperties(UIDL uidl, + AbstractComponentState 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; + } + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public 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); + } + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public 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; + } + + /** + * Returns the extra space that is given to the header column when column + * width is determined by header text. + * + * @return extra space in pixels + */ + private int getHeaderPadding() { + return scrollBody.getCellExtraWidth(); + } + + /** + * This method exists for the needs of {@link VTreeTable} only. Not part of + * the official API, <b>extend at your own risk</b>. May be removed or + * replaced in the future. + * + * @return index of TreeTable's hierarchy column, or -1 if not applicable + */ + protected int getHierarchyColumnIndex() { + return -1; + } + + /** + * For internal use only. May be removed or replaced in the future. + */ + public void updateMaxIndent() { + int oldIndent = scrollBody.getMaxIndent(); + scrollBody.calculateMaxIndent(); + if (oldIndent != scrollBody.getMaxIndent()) { + // indent updated, headers might need adjusting + triggerLazyColumnAdjustment(true); + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public 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 + * <p> + * For internal use only. May be removed or replaced in the future. + * + * @param focusOnly + * Should the focus only be moved to the last row + */ + public 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 + * <p> + * For internal use only. May be removed or replaced in the future. + * + * @param focusOnly + * Should the focus only be moved to the first row + */ + public 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(); + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public 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; + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public 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", action.getStringAttribute("icon")); + } else { + actionMap.remove(key + "_i"); + } + } + + } + + public String getActionCaption(String actionKey) { + return actionMap.get(actionKey + "_c"); + } + + public String getActionIcon(String actionKey) { + return client.translateVaadinUri(actionMap.get(actionKey + "_i")); + } + + private void updateHeader(String[] strings) { + if (strings == null) { + return; + } + + int visibleCols = strings.length; + int colIndex = 0; + if (showRowHeaders) { + tHead.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex); + visibleCols++; + visibleColOrder = new String[visibleCols]; + visibleColOrder[colIndex] = ROW_HEADER_COLUMN_KEY; + colIndex++; + } else { + visibleColOrder = new String[visibleCols]; + tHead.removeCell(ROW_HEADER_COLUMN_KEY); + } + + int i; + for (i = 0; i < strings.length; i++) { + final String cid = strings[i]; + visibleColOrder[colIndex] = cid; + tHead.enableColumn(cid, colIndex); + colIndex++; + } + + tHead.setVisible(showColHeaders); + setContainerHeight(); + + } + + /** + * Updates footers. + * <p> + * Update headers whould be called before this method is called! + * </p> + * + * @param strings + */ + private void updateFooter(String[] strings) { + if (strings == null) { + return; + } + + // Add dummy column if row headers are present + int colIndex = 0; + if (showRowHeaders) { + tFoot.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex); + colIndex++; + } else { + tFoot.removeCell(ROW_HEADER_COLUMN_KEY); + } + + int i; + for (i = 0; i < strings.length; i++) { + final String cid = strings[i]; + tFoot.enableColumn(cid, colIndex); + colIndex++; + } + + tFoot.setVisible(showColFooters); + } + + /** + * For internal use only. May be removed or replaced in the future. + * + * @param uidl + * which contains row data + * @param firstRow + * first row in data set + * @param reqRows + * amount of rows in data set + */ + public void updateBody(UIDL uidl, int firstRow, int reqRows) { + int oldIndent = scrollBody.getMaxIndent(); + if (uidl == null || reqRows < 1) { + // container is empty, remove possibly existing rows + if (firstRow <= 0) { + postponeSanityCheckForLastRendered = true; + while (scrollBody.getLastRendered() > scrollBody + .getFirstRendered()) { + scrollBody.unlinkRow(false); + } + postponeSanityCheckForLastRendered = false; + scrollBody.unlinkRow(false); + } + return; + } + + scrollBody.renderRows(uidl, firstRow, reqRows); + + discardRowsOutsideCacheWindow(); + scrollBody.calculateMaxIndent(); + if (oldIndent != scrollBody.getMaxIndent()) { + // indent updated, headers might need adjusting + headerChangedDuringUpdate = true; + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public 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); + // sanity checks: + if (firstRowToKeep < 0) { + firstRowToKeep = 0; + } + if (lastRowToKeep > totalRows) { + lastRowToKeep = totalRows - 1; + } + 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. + * <p> + * For internal use only. May be removed or replaced in the future. + * + * @param partialRowAdditions + * the UIDL containing row updates. + */ + public 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; + } + + } + + /** For internal use only. May be removed or replaced in the future. */ + public 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(); + } + + /** + * Note: not part of the official API, extend at your own risk. May be + * removed or replaced in the future. + * + * Sets the indicated column's width for headers and scrollBody alike. + * + * @param colIndex + * index of the modified column + * @param w + * new width (may be subject to modifications if doesn't meet + * minimum requirements) + * @param isDefinedWidth + * disables expand ratio if set true + */ + protected 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. + // get min width with no indent or padding + int minWidth = hcell.getMinWidth(false, false); + if (w < minWidth) { + w = minWidth; + } + + // Set header column width WITHOUT INDENT + hcell.setWidth(w, isDefinedWidth); + + // Set footer column width likewise + FooterCell fcell = tFoot.getFooterCell(colIndex); + fcell.setWidth(w, isDefinedWidth); + + // Ensure indicators have been taken into account + tHead.resizeCaptionContainer(hcell); + + // Make sure that the body column grows to accommodate the indent if + // necessary. + // get min width with indent, no padding + minWidth = hcell.getMinWidth(true, false); + if (w < minWidth) { + w = minWidth; + } + + // Set body column width + scrollBody.setColWidth(colIndex, w); + } + + private int getColWidth(String colKey) { + return tHead.getHeaderCell(colKey).getWidthWithIndent(); + } + + /** + * Get a rendered row by its key + * + * @param key + * The key to search with + * @return + */ + public VScrollTableRow getRenderedRowByKey(String key) { + if (scrollBody != null) { + final Iterator<Widget> it = scrollBody.iterator(); + VScrollTableRow r = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (r.getKey().equals(key)) { + return r; + } + } + } + return null; + } + + /** + * Returns the next row to the given row + * + * @param row + * The row to calculate from + * + * @return The next row or null if no row exists + */ + private VScrollTableRow getNextRow(VScrollTableRow row, int offset) { + final Iterator<Widget> it = scrollBody.iterator(); + VScrollTableRow r = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (r == row) { + r = null; + while (offset >= 0 && it.hasNext()) { + r = (VScrollTableRow) it.next(); + offset--; + } + return r; + } + } + + return null; + } + + /** + * Returns the previous row from the given row + * + * @param row + * The row to calculate from + * @return The previous row or null if no row exists + */ + private VScrollTableRow getPreviousRow(VScrollTableRow row, int offset) { + final Iterator<Widget> it = scrollBody.iterator(); + final Iterator<Widget> offsetIt = scrollBody.iterator(); + VScrollTableRow r = null; + VScrollTableRow prev = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (offset < 0) { + prev = (VScrollTableRow) offsetIt.next(); + } + if (r == row) { + return prev; + } + offset--; + } + + return null; + } + + protected void reOrderColumn(String columnKey, int newIndex) { + + final int oldIndex = getColIndexByKey(columnKey); + + // Change header order + tHead.moveCell(oldIndex, newIndex); + + // Change body order + scrollBody.moveCol(oldIndex, newIndex); + + // Change footer order + tFoot.moveCell(oldIndex, newIndex); + + /* + * Build new columnOrder and update it to server Note that columnOrder + * also contains collapsed columns so we cannot directly build it from + * cells vector Loop the old columnOrder and append in order to new + * array unless on moved columnKey. On new index also put the moved key + * i == index on columnOrder, j == index on newOrder + */ + final String oldKeyOnNewIndex = visibleColOrder[newIndex]; + if (showRowHeaders) { + newIndex--; // columnOrder don't have rowHeader + } + // add back hidden rows, + for (int i = 0; i < columnOrder.length; i++) { + if (columnOrder[i].equals(oldKeyOnNewIndex)) { + break; // break loop at target + } + if (isCollapsedColumn(columnOrder[i])) { + newIndex++; + } + } + // finally we can build the new columnOrder for server + final String[] newOrder = new String[columnOrder.length]; + for (int i = 0, j = 0; j < newOrder.length; i++) { + if (j == newIndex) { + newOrder[j] = columnKey; + j++; + } + if (i == columnOrder.length) { + break; + } + if (columnOrder[i].equals(columnKey)) { + continue; + } + newOrder[j] = columnOrder[i]; + j++; + } + columnOrder = newOrder; + // also update visibleColumnOrder + int i = showRowHeaders ? 1 : 0; + for (int j = 0; j < newOrder.length; j++) { + final String cid = newOrder[j]; + if (!isCollapsedColumn(cid)) { + visibleColOrder[i++] = cid; + } + } + client.updateVariable(paintableId, "columnorder", columnOrder, false); + if (client.hasEventListeners(this, + TableConstants.COLUMN_REORDER_EVENT_ID)) { + client.sendPendingVariableChanges(); + } + } + + @Override + protected void onDetach() { + detachedScrollPosition = scrollBodyPanel.getScrollPosition(); + 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); + } + } + } + + @Override + public void onAttach() { + super.onAttach(); + scrollBodyPanel.setScrollPosition(detachedScrollPosition); + } + + /** + * 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 + * + * For internal use only. May be removed or replaced in the future. + */ + public 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<Widget> headCells = tHead.iterator(); + Iterator<Widget> footCells = tFoot.iterator(); + int i = 0; + int totalExplicitColumnsWidths = 0; + int total = 0; + float expandRatioDivider = 0; + + final int[] widths = new int[tHead.visibleCells.size()]; + + tHead.enableBrowserIntelligence(); + tFoot.enableBrowserIntelligence(); + + int hierarchyColumnIndent = scrollBody != null + ? scrollBody.getMaxIndent() : 0; + HeaderCell hierarchyHeaderWithExpandRatio = null; + + // first loop: collect natural widths + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + final FooterCell fCell = (FooterCell) footCells.next(); + boolean needsIndent = hierarchyColumnIndent > 0 + && hCell.isHierarchyColumn(); + hCell.saveNaturalColumnWidthIfNotSaved(i); + fCell.saveNaturalColumnWidthIfNotSaved(i); + int w = hCell.getWidth(); + if (hCell.isDefinedWidth()) { + // server has defined column width explicitly + if (needsIndent && w < hierarchyColumnIndent) { + // hierarchy indent overrides explicitly set width + w = hierarchyColumnIndent; + } + totalExplicitColumnsWidths += w; + } else { + if (hCell.getExpandRatio() > 0) { + expandRatioDivider += hCell.getExpandRatio(); + w = 0; + if (needsIndent && w < hierarchyColumnIndent) { + hierarchyHeaderWithExpandRatio = hCell; + // don't add to widths here, because will be included in + // the expand ratio space if there's enough of it + } + } else { + // get and store greater of header width and column width, + // and store it as a minimum natural column width (these + // already contain the indent if any) + int headerWidth = hCell.getNaturalColumnWidth(i); + int footerWidth = fCell.getNaturalColumnWidth(i); + w = headerWidth > footerWidth ? headerWidth : footerWidth; + } + if (w != 0) { + hCell.setNaturalMinimumColumnWidth(w); + fCell.setNaturalMinimumColumnWidth(w); + } + } + widths[i] = w; + total += w; + i++; + } + if (hierarchyHeaderWithExpandRatio != null) { + total += hierarchyColumnIndent; + } + + 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 += WidgetUtil.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 -= WidgetUtil.getNativeScrollbarSize(); + } + + // TODO refactor this code to be the same as in resize timer + + if (availW > total) { + // natural size is smaller than available space + int extraSpace = availW - total; + if (hierarchyHeaderWithExpandRatio != null) { + /* + * add the indent's space back to ensure each column gets an + * even share according to the expand ratios (note: if the + * allocated space isn't enough for the hierarchy column it + * shall be treated like a defined width column and the indent + * space gets removed from the extra space again) + */ + extraSpace += hierarchyColumnIndent; + } + final int totalWidthR = total - totalExplicitColumnsWidths; + int checksum = 0; + + if (extraSpace == 1) { + // We cannot divide one single pixel so we give it the first + // undefined column + // no need to worry about indent here + 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) { + boolean setIndentToHierarchyHeader = false; + if (hierarchyHeaderWithExpandRatio != null) { + // ensure first that the hierarchyColumn gets at least the + // space allocated for indent + final int newSpace = Math.round((extraSpace + * (hierarchyHeaderWithExpandRatio.getExpandRatio() + / expandRatioDivider))); + if (newSpace < hierarchyColumnIndent) { + // not enough space for indent, remove indent from the + // extraSpace again and handle hierarchy column's header + // separately + setIndentToHierarchyHeader = true; + extraSpace -= hierarchyColumnIndent; + } + } + + // 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]; + if (setIndentToHierarchyHeader + && hierarchyHeaderWithExpandRatio + .equals(hCell)) { + // hierarchy column's header is no longer part of + // the expansion divide and only gets indent + w += hierarchyColumnIndent; + } else { + 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 + // no need to worry about indent here, it's already included + 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 { + // body's 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; + + 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 + */ + + /* + * TreeTable updates stuff in a funky order, so we must set the + * height as zero here before doing the real update to make it + * realize that there is no content, + */ + if (pageLength == totalRows && pageLength == 0) { + scrollBody.setHeight("0px"); + } + + 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 += WidgetUtil.getNativeScrollbarSize(); + } + scrollBodyPanel.setHeight(bodyHeight + "px"); + WidgetUtil.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + + isNewBody = false; + + if (firstvisible > 0) { + enableLazyScroller(); + } + + 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; + int lastInNewSet = (int) (firstRowInViewPort + pageLength + + cache_rate * pageLength); + if (lastInNewSet > totalRows - 1) { + lastInNewSet = totalRows - 1; + } + rowRequestHandler.triggerRowFetch(firstInNewSet, + lastInNewSet - firstInNewSet + 1, 1); + } + } + } + + /* + * Ensures the column alignments are correct at initial loading. <br/> + * (child components widths are correct) + */ + WidgetUtil + .runWebkitOverflowAutoFixDeferred(scrollBodyPanel.getElement()); + + hadScrollBars = willHaveScrollbarz; + } + + /** + * Note: this method is not part of official API although declared as + * protected. Extend at your 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(getStylePrimaryName() + "-scrollposition"); + scrollPositionElement.getStyle().setPosition(Position.ABSOLUTE); + scrollPositionElement.getStyle().setDisplay(Display.NONE); + getElement().appendChild(scrollPositionElement); + } + + Style style = scrollPositionElement.getStyle(); + style.setMarginLeft(getElement().getOffsetWidth() / 2 - 80, Unit.PX); + style.setMarginTop(-scrollBodyPanel.getOffsetHeight(), Unit.PX); + + // indexes go from 1-totalRows, as rowheaders in index-mode indicate + int last = (firstRowInViewPort + pageLength); + if (last > totalRows) { + last = totalRows; + } + scrollPositionElement.setInnerHTML("<span>" + (firstRowInViewPort + 1) + + " – " + (last) + "..." + "</span>"); + style.setDisplay(Display.BLOCK); + } + + /** For internal use only. May be removed or replaced in the future. */ + public void hideScrollPositionAnnotation() { + if (scrollPositionElement != null) { + scrollPositionElement.getStyle().setDisplay(Display.NONE); + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public boolean isScrollPositionVisible() { + return scrollPositionElement != null && !scrollPositionElement + .getStyle().getDisplay().equals(Display.NONE.toString()); + } + + /** For internal use only. May be removed or replaced in the future. */ + public class RowRequestHandler extends Timer { + + private int reqFirstRow = 0; + private int reqRows = 0; + private boolean isRequestHandlerRunning = false; + + public void triggerRowFetch(int first, int rows) { + setReqFirstRow(first); + setReqRows(rows); + deferRowFetch(); + } + + public void triggerRowFetch(int first, int rows, int delay) { + setReqFirstRow(first); + setReqRows(rows); + deferRowFetch(delay); + } + + public void deferRowFetch() { + deferRowFetch(250); + } + + public boolean isRequestHandlerRunning() { + return isRequestHandlerRunning; + } + + public void deferRowFetch(int msec) { + isRequestHandlerRunning = 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 int getReqFirstRow() { + return reqFirstRow; + } + + public void setReqFirstRow(int reqFirstRow) { + if (reqFirstRow < 0) { + this.reqFirstRow = 0; + } else if (reqFirstRow >= totalRows) { + this.reqFirstRow = totalRows - 1; + } else { + this.reqFirstRow = reqFirstRow; + } + } + + public void setReqRows(int reqRows) { + if (reqRows < 0) { + this.reqRows = 0; + } else if (reqFirstRow + reqRows > totalRows) { + this.reqRows = totalRows - reqFirstRow; + } else { + this.reqRows = reqRows; + } + } + + @Override + public void run() { + + if (client.getMessageSender().hasActiveRequest() || navKeyDown) { + // if client connection is busy, don't bother loading it more + VConsole.log("Postponed rowfetch"); + schedule(250); + } else if (allRenderedRowsAreNew() && !updatedReqRows) { + + /* + * If all rows are new, there might have been a server-side call + * to Table.setCurrentPageFirstItemIndex(int) In this case, + * scrolling event takes way too late, and all the rows from + * previous viewport to this one were requested. + * + * This should prevent requesting unneeded rows by updating + * reqFirstRow and reqRows before needing them. See (#14135) + */ + + setReqFirstRow( + (firstRowInViewPort - (int) (pageLength * cache_rate))); + int last = firstRowInViewPort + (int) (cache_rate * pageLength) + + pageLength - 1; + if (last >= totalRows) { + last = totalRows - 1; + } + setReqRows(last - getReqFirstRow() + 1); + updatedReqRows = true; + schedule(250); + + } else { + + int firstRendered = scrollBody.getFirstRendered(); + int lastRendered = scrollBody.getLastRendered(); + if (lastRendered > totalRows) { + lastRendered = totalRows - 1; + } + boolean rendered = firstRendered >= 0 && lastRendered >= 0; + + int firstToBeRendered = 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; + } + } else if (rendered && firstRendered + 1 < reqFirstRow + && lastRendered + 1 < reqFirstRow) { + // requested rows must fall within the requested rendering + // area + firstToBeRendered = reqFirstRow; + } + if (firstToBeRendered + reqRows < firstRendered) { + // must increase the required row count accordingly, + // otherwise may leave a gap and the rows beyond will get + // removed + setReqRows(firstRendered - firstToBeRendered); + } + + int lastToBeRendered = lastRendered; + int lastReqRow = reqFirstRow + reqRows - 1; + + if (lastReqRow > lastToBeRendered) { + lastToBeRendered = lastReqRow; + } 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 (lastReqRow > lastToBeRendered) { + setReqRows(lastToBeRendered - reqFirstRow); + } + } else if (rendered && lastRendered - 1 > lastReqRow + && firstRendered - 1 > lastReqRow) { + // requested rows must fall within the requested rendering + // area + lastToBeRendered = lastReqRow; + } + + if (lastToBeRendered > totalRows) { + lastToBeRendered = totalRows - 1; + } + if (reqFirstRow < firstToBeRendered + || (reqFirstRow > firstToBeRendered + && (reqFirstRow < firstRendered + || reqFirstRow > lastRendered + 1))) { + setReqFirstRow(firstToBeRendered); + } + if (lastRendered < lastToBeRendered + && lastRendered + reqRows < lastToBeRendered) { + // must increase the required row count accordingly, + // otherwise may leave a gap and the rows after will get + // removed + setReqRows(lastToBeRendered - lastRendered); + } else if (lastToBeRendered >= firstRendered + && reqFirstRow + reqRows < firstRendered) { + setReqRows(lastToBeRendered - lastRendered); + } + + client.updateVariable(paintableId, "firstToBeRendered", + firstToBeRendered, false); + client.updateVariable(paintableId, "lastToBeRendered", + lastToBeRendered, false); + + // don't request server to update page first index in case it + // has not been changed + if (firstRowInViewPort != firstvisible) { + // remember which firstvisible we requested, in case the + // server has a differing opinion + lastRequestedFirstvisible = firstRowInViewPort; + client.updateVariable(paintableId, "firstvisible", + firstRowInViewPort, false); + } + + client.updateVariable(paintableId, "reqfirstrow", reqFirstRow, + false); + client.updateVariable(paintableId, "reqrows", reqRows, true); + + if (selectionChanged) { + unSyncedselectionsBeforeRowFetch = new HashSet<Object>( + selectedRowKeys); + } + isRequestHandlerRunning = false; + } + } + + /** + * Sends request to refresh content at this position. + */ + public void refreshContent() { + isRequestHandlerRunning = 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 Integer currentDragX = null; // is used to resolve #14796 + + 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; + /* + * Should in theory call updateStyleNames here, but that would just + * be a waste of time since this method is only called from + * updateCellsFromUIDL which immediately afterwards calls setAlign + * which also updates the style names. + */ + } + + /** + * 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; + + setText(headerText); + + td.appendChild(colResizeWidget); + + // ensure no clipping initially (problem on column additions) + captionContainer.getStyle().setOverflow(Overflow.VISIBLE); + + td.appendChild(sortIndicator); + td.appendChild(captionContainer); + + DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK + | Event.ONCONTEXTMENU | Event.TOUCHEVENTS); + + setElement(td); + + setAlign(ALIGN_LEFT); + } + + protected void updateStyleNames(String primaryStyleName) { + colResizeWidget.setClassName(primaryStyleName + "-resizer"); + sortIndicator.setClassName(primaryStyleName + "-sort-indicator"); + captionContainer + .setClassName(primaryStyleName + "-caption-container"); + if (sorted) { + if (sortAscending) { + setStyleName(primaryStyleName + "-header-cell-asc"); + } else { + setStyleName(primaryStyleName + "-header-cell-desc"); + } + } else { + setStyleName(primaryStyleName + "-header-cell"); + } + + if (sortable) { + addStyleName(primaryStyleName + "-header-sortable"); + } + + final String ALIGN_PREFIX = primaryStyleName + + "-caption-container-align-"; + + switch (align) { + 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; + } + + } + + public void disableAutoWidthCalculation() { + definedWidth = true; + expandRatio = 0; + } + + /** + * Sets width to the header cell. This width should not include any + * possible indent modifications that are present in + * {@link VScrollTableBody#getMaxIndent()}. + * + * @param w + * required width of the cell sans indentations + * @param ensureDefinedWidth + * disables expand ratio if required + */ + 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 + captionContainer.getStyle().clearOverflow(); + } + width = w; + if (w == -1) { + captionContainer.getStyle().clearWidth(); + 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 maxIndent = scrollBody.getMaxIndent(); + if (w < maxIndent && isHierarchyColumn()) { + w = maxIndent; + } + int tdWidth = w + scrollBody.getCellExtraWidth(); + setWidth(tdWidth + "px"); + } else { + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + int maxIndent = scrollBody.getMaxIndent(); + int tdWidth = width; + if (tdWidth < maxIndent && isHierarchyColumn()) { + tdWidth = maxIndent; + } + tdWidth += scrollBody.getCellExtraWidth(); + setWidth(tdWidth + "px"); + } + }); + } + } + } + + public void setUndefinedWidth() { + definedWidth = false; + if (!isResizing) { + setWidth(-1, false); + } + } + + private void setUndefinedWidthFlagOnly() { + definedWidth = 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; + } + + /** + * This method exists for the needs of {@link VTreeTable} only. + * + * Returns the pixels width of the header cell. This includes the + * indent, if applicable. + * + * @return The width in pixels + */ + protected int getWidthWithIndent() { + if (scrollBody != null && isHierarchyColumn()) { + int maxIndent = scrollBody.getMaxIndent(); + if (maxIndent > width) { + return maxIndent; + } + } + return width; + } + + /** + * Returns the pixels width of the header cell. + * + * @return The width in pixels + */ + public int getWidth() { + return width; + } + + /** + * This method exists for the needs of {@link VTreeTable} only. + * + * @return <code>true</code> if this is hierarcyColumn's header cell, + * <code>false</code> otherwise + */ + private boolean isHierarchyColumn() { + int hierarchyColumnIndex = getHierarchyColumnIndex(); + return hierarchyColumnIndex >= 0 + && tHead.visibleCells.indexOf(this) == hierarchyColumnIndex; + } + + public void setText(String headerText) { + DOM.setInnerHTML(captionContainer, headerText); + } + + public String getColKey() { + return cid; + } + + private void setSorted(boolean sorted) { + this.sorted = sorted; + updateStyleNames(VScrollTable.this.getStylePrimaryName()); + } + + /** + * 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, + TableConstants.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); + // #12714 the shown "ghost element" should be inside + // v-overlay-container, and it should contain the same styles as the + // table to enable theming (except v-table & v-widget). + String stylePrimaryName = VScrollTable.this.getStylePrimaryName(); + StringBuilder sb = new StringBuilder(); + for (String s : VScrollTable.this.getStyleName().split(" ")) { + if (!s.equals(StyleConstants.UI_WIDGET)) { + sb.append(s); + if (s.equals(stylePrimaryName)) { + sb.append("-header-drag "); + } else { + sb.append(" "); + } + } + } + floatingCopyOfHeaderCell.setClassName(sb.toString().trim()); + // otherwise might wrap or be cut if narrow column + floatingCopyOfHeaderCell.getStyle().setProperty("width", "auto"); + updateFloatingCopysPosition(DOM.getAbsoluteLeft(td), + DOM.getAbsoluteTop(td)); + DOM.appendChild(VOverlay.getOverlayContainer(client), + floatingCopyOfHeaderCell); + } + + private void updateFloatingCopysPosition(int x, int y) { + x -= DOM.getElementPropertyInt(floatingCopyOfHeaderCell, + "offsetWidth") / 2; + floatingCopyOfHeaderCell.getStyle().setLeft(x, Unit.PX); + if (y > 0) { + floatingCopyOfHeaderCell.getStyle().setTop(y + 7, Unit.PX); + } + } + + private void hideFloatingCopy() { + floatingCopyOfHeaderCell.removeFromParent(); + 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, + TableConstants.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 + && WidgetUtil.isTouchEventOrLeftMouseButton(event)) { + if (event.getTypeInt() == Event.ONTOUCHSTART) { + /* + * prevent using this event in e.g. scrolling + */ + event.stopPropagation(); + } + dragging = true; + currentDragX = WidgetUtil.getTouchOrMouseClientX(event); + 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 + && WidgetUtil.isTouchEventOrLeftMouseButton(event)) { + dragging = false; + currentDragX = null; + DOM.releaseCapture(getElement()); + + if (WidgetUtil.isTouchEvent(event)) { + /* + * Prevent using in e.g. scrolling and prevent generated + * events. + */ + event.preventDefault(); + event.stopPropagation(); + } + if (moved) { + hideFloatingCopy(); + tHead.removeSlotFocus(); + if (closestSlot != colIndex + && closestSlot != (colIndex + 1)) { + if (closestSlot > colIndex) { + reOrderColumn(cid, closestSlot - 1); + } else { + reOrderColumn(cid, closestSlot); + } + } + moved = false; + break; + } + } + + if (!moved) { + // mouse event was a click to header -> sort column + if (sortable && WidgetUtil + .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 (WidgetUtil.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: + // only start the drag if the mouse / touch has moved a minimum + // distance in x-axis (the same idea as in #13381) + int currentX = WidgetUtil.getTouchOrMouseClientX(event); + + if (currentDragX == null || Math.abs(currentDragX + - currentX) > VDragAndDropManager.MINIMUM_DISTANCE_TO_START_DRAG) { + if (dragging && WidgetUtil + .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 = WidgetUtil + .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); + // getColWidth only returns the internal width + // without padding, not the offset width of the + // whole td (#10890) + slotX += getColWidth(colKey) + + scrollBody.getCellExtraWidth(); + } + 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 (!WidgetUtil.isTouchEventOrLeftMouseButton(event)) { + return; + } + isResizing = true; + DOM.setCapture(getElement()); + dragStartX = DOM.eventGetClientX(event); + colIndex = getColIndexByKey(cid); + originalWidth = getWidthWithIndent(); + DOM.eventPreventDefault(event); + break; + case Event.ONMOUSEUP: + if (!WidgetUtil.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 (!WidgetUtil.isTouchEventOrLeftMouseButton(event)) { + return; + } + if (isResizing) { + final int deltaX = DOM.eventGetClientX(event) - dragStartX; + if (deltaX == 0) { + return; + } + tHead.disableAutoColumnWidthCalculation(this); + + int newWidth = originalWidth + deltaX; + // get min width with indent, no padding + int minWidth = getMinWidth(true, false); + if (newWidth < minWidth) { + // already includes indent if any + newWidth = minWidth; + } + setColWidth(colIndex, newWidth, true); + triggerLazyColumnAdjustment(false); + forceRealignColumnHeaders(); + } + break; + default: + break; + } + } + + /** + * Returns the smallest possible cell width in pixels. + * + * @param includeIndent + * - width should include hierarchy column indent if + * applicable (VTreeTable only) + * @param includeCellExtraWidth + * - width should include paddings etc. + * @return + */ + private int getMinWidth(boolean includeIndent, + boolean includeCellExtraWidth) { + int minWidth = sortIndicator.getOffsetWidth(); + if (scrollBody != null) { + // check the need for indent before adding paddings etc. + if (includeIndent && isHierarchyColumn()) { + int maxIndent = scrollBody.getMaxIndent(); + if (minWidth < maxIndent) { + minWidth = maxIndent; + } + } + if (includeCellExtraWidth) { + minWidth += scrollBody.getCellExtraWidth(); + } + } + return minWidth; + } + + public int getMinWidth() { + // get min width with padding, no indent + return getMinWidth(false, true); + } + + public String getCaption() { + return DOM.getInnerText(captionContainer); + } + + public boolean isEnabled() { + return getParent() != null; + } + + public void setAlign(char c) { + align = c; + updateStyleNames(VScrollTable.this.getStylePrimaryName()); + } + + public char getAlign() { + return align; + } + + /** + * Saves natural column width if it hasn't been saved already. + * + * @param columnIndex + * @since 7.3.9 + */ + protected void saveNaturalColumnWidthIfNotSaved(int columnIndex) { + if (naturalWidth < 0) { + // This is recently revealed column. Try to detect a proper + // value (greater of header and data columns) + + int hw = captionContainer.getOffsetWidth() + getHeaderPadding(); + if (BrowserInfo.get().isGecko()) { + hw += sortIndicator.getOffsetWidth(); + } + if (columnIndex < 0) { + columnIndex = 0; + for (Iterator<Widget> it = tHead.iterator(); it + .hasNext(); columnIndex++) { + if (it.next() == this) { + break; + } + } + } + final int cw = scrollBody.getColWidth(columnIndex); + naturalWidth = (hw > cw ? hw : cw); + } + } + + /** + * 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) { + final int iw = columnIndex == getHierarchyColumnIndex() + ? scrollBody.getMaxIndent() : 0; + saveNaturalColumnWidthIfNotSaved(columnIndex); + if (isDefinedWidth()) { + if (iw > width) { + return iw; + } + return width; + } else { + if (iw > naturalWidth) { + // indent is temporary value, naturalWidth shouldn't be + // updated + return iw; + } else { + 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, ""); + updateStyleNames(VScrollTable.this.getStylePrimaryName()); + } + + @Override + protected void updateStyleNames(String primaryStyleName) { + super.updateStyleNames(primaryStyleName); + setStyleName(primaryStyleName + "-header-cell-rowheader"); + } + + @Override + protected void handleCaptionEvent(Event event) { + // NOP: RowHeaders cannot be reordered + // TODO It'd be nice to reset sorting here + } + } + + public class TableHead extends Panel implements ActionOwner { + + private static final int WRAPPER_WIDTH = 900000; + + ArrayList<Widget> visibleCells = new ArrayList<Widget>(); + + HashMap<String, HeaderCell> availableCells = new HashMap<String, HeaderCell>(); + + Element div = DOM.createDiv(); + Element hTableWrapper = DOM.createDiv(); + Element hTableContainer = DOM.createDiv(); + Element table = DOM.createTable(); + Element headerTableBody = DOM.createTBody(); + Element tr = DOM.createTR(); + + private final Element columnSelector = DOM.createDiv(); + + private int focusedSlot = -1; + + public TableHead() { + if (BrowserInfo.get().isIE()) { + table.setPropertyInt("cellSpacing", 0); + } + + hTableWrapper.getStyle().setOverflow(Overflow.HIDDEN); + columnSelector.getStyle().setDisplay(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); + + DOM.sinkEvents(columnSelector, Event.ONCLICK); + + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersHeaderCell()); + } + + protected void updateStyleNames(String primaryStyleName) { + hTableWrapper.setClassName(primaryStyleName + "-header"); + columnSelector.setClassName(primaryStyleName + "-column-selector"); + setStyleName(primaryStyleName + "-header-wrap"); + for (HeaderCell c : availableCells.values()) { + c.updateStyleNames(primaryStyleName); + } + } + + public void resizeCaptionContainer(HeaderCell cell) { + HeaderCell lastcell = getHeaderCell(visibleCells.size() - 1); + int columnSelectorOffset = columnSelector.getOffsetWidth(); + + if (cell == lastcell && columnSelectorOffset > 0 + && !hasVerticalScrollbar()) { + + // Measure column widths + int columnTotalWidth = 0; + for (Widget w : visibleCells) { + int cellExtraWidth = w.getOffsetWidth(); + if (scrollBody != null + && visibleCells + .indexOf(w) == getHierarchyColumnIndex() + && cellExtraWidth < scrollBody.getMaxIndent()) { + // indent must be taken into consideration even if it + // hasn't been applied yet + columnTotalWidth += scrollBody.getMaxIndent(); + } else { + columnTotalWidth += cellExtraWidth; + } + } + + int divOffset = div.getOffsetWidth(); + if (columnTotalWidth >= divOffset - columnSelectorOffset) { + /* + * Ensure column caption is visible when placed under the + * column selector widget by shifting and resizing the + * caption. + */ + int offset = 0; + int diff = divOffset - columnTotalWidth; + if (diff < columnSelectorOffset && diff > 0) { + /* + * If the difference is less than the column selectors + * width then just offset by the difference + */ + offset = columnSelectorOffset - diff; + } else { + // Else offset by the whole column selector + offset = columnSelectorOffset; + } + lastcell.resizeCaptionContainer(offset); + } else { + cell.resizeCaptionContainer(0); + } + } else { + cell.resizeCaptionContainer(0); + } + } + + @Override + public void clear() { + for (String cid : availableCells.keySet()) { + removeCell(cid); + } + availableCells.clear(); + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersHeaderCell()); + } + + public void updateCellsFromUIDL(UIDL uidl) { + Iterator<?> it = uidl.getChildIterator(); + HashSet<String> updated = new HashSet<String>(); + boolean refreshContentWidths = initializedAndAttached + && hadScrollBars != willHaveScrollbars(); + 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); + } + + c.setSorted(false); + if (col.hasAttribute("sortable")) { + c.setSortable(true); + } else { + c.setSortable(false); + } + + // The previous call to setSortable relies on c.setAlign calling + // c.updateStyleNames + if (col.hasAttribute("align")) { + c.setAlign(col.getStringAttribute("align").charAt(0)); + } else { + c.setAlign(ALIGN_LEFT); + + } + if (col.hasAttribute("width") && !c.isResizing) { + // Make sure to accomodate for the sort indicator if + // necessary. + int width = col.getIntAttribute("width"); + int widthWithoutAddedIndent = width; + + // get min width with indent, no padding + int minWidth = c.getMinWidth(true, false); + if (width < minWidth) { + width = minWidth; + } + if (scrollBody != null && width != c.getWidthWithIndent()) { + // Do a more thorough update if a column is resized from + // the server *after* the header has been properly + // initialized + final int newWidth = width; + Scheduler.get().scheduleFinally(new ScheduledCommand() { + + @Override + public void execute() { + final int colIx = getColIndexByKey(cid); + setColWidth(colIx, newWidth, true); + } + }); + refreshContentWidths = true; + } else { + // get min width with no indent or padding + minWidth = c.getMinWidth(false, false); + if (widthWithoutAddedIndent < minWidth) { + widthWithoutAddedIndent = minWidth; + } + // save min width without indent + c.setWidth(widthWithoutAddedIndent, true); + } + } else if (col.hasAttribute("er")) { + c.setExpandRatio(col.getFloatAttribute("er")); + c.setUndefinedWidthFlagOnly(); + } else if (recalcWidths) { + c.setUndefinedWidth(); + + } else { + boolean hadExpandRatio = c.getExpandRatio() > 0; + boolean hadDefinedWidth = c.isDefinedWidth(); + if (hadExpandRatio || hadDefinedWidth) { + // Someone has removed a expand width or the defined + // width on the server side (setting it to -1), make the + // column undefined again and measure columns again. + c.setUndefinedWidth(); + c.setExpandRatio(0); + refreshContentWidths = true; + } + } + + 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().scheduleFinally(new ScheduledCommand() { + + @Override + public void execute() { + triggerLazyColumnAdjustment(true); + } + }); + } + + // check for orphaned header cells + for (Iterator<String> cit = availableCells.keySet().iterator(); cit + .hasNext();) { + String cid = cit.next(); + if (!updated.contains(cid)) { + removeCell(cid); + cit.remove(); + // we will need a column width recalculation, since columns + // with expand ratios should expand to fill the void. + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } + } + + public void enableColumn(String cid, int index) { + final HeaderCell c = getHeaderCell(cid); + if (!c.isEnabled() || getHeaderCell(index) != c) { + setHeaderCell(index, c); + if (initializedAndAttached) { + headerChangedDuringUpdate = true; + } + } + } + + public int getVisibleCellCount() { + return visibleCells.size(); + } + + public void setHorizontalScrollPosition(int scrollLeft) { + hTableWrapper.setScrollLeft(scrollLeft); + } + + public void setColumnCollapsingAllowed(boolean cc) { + if (cc) { + columnSelector.getStyle().setDisplay(Display.BLOCK); + } else { + columnSelector.getStyle().setDisplay(Display.NONE); + } + } + + public void disableBrowserIntelligence() { + hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX); + } + + public void enableBrowserIntelligence() { + hTableContainer.getStyle().clearWidth(); + } + + public void setHeaderCell(int index, HeaderCell cell) { + if (cell.isEnabled()) { + // we're moving the cell + DOM.removeChild(tr, cell.getElement()); + orphan(cell); + visibleCells.remove(cell); + } + if (index < visibleCells.size()) { + // insert to right slot + DOM.insertChild(tr, cell.getElement(), index); + adopt(cell); + visibleCells.add(index, cell); + } else if (index == visibleCells.size()) { + // simply append + DOM.appendChild(tr, cell.getElement()); + adopt(cell); + visibleCells.add(cell); + } else { + throw new RuntimeException( + "Header cells must be appended in order"); + } + } + + public HeaderCell getHeaderCell(int index) { + if (index >= 0 && index < visibleCells.size()) { + return (HeaderCell) visibleCells.get(index); + } else { + return null; + } + } + + /** + * Get's HeaderCell by it's column Key. + * + * Note that this returns HeaderCell even if it is currently collapsed. + * + * @param cid + * Column key of accessed HeaderCell + * @return HeaderCell + */ + public HeaderCell getHeaderCell(String cid) { + return availableCells.get(cid); + } + + public void moveCell(int oldIndex, int newIndex) { + final HeaderCell hCell = getHeaderCell(oldIndex); + final Element cell = hCell.getElement(); + + visibleCells.remove(oldIndex); + DOM.removeChild(tr, cell); + + DOM.insertChild(tr, cell, newIndex); + visibleCells.add(newIndex, hCell); + } + + @Override + public Iterator<Widget> iterator() { + return visibleCells.iterator(); + } + + @Override + public boolean remove(Widget w) { + if (visibleCells.contains(w)) { + visibleCells.remove(w); + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), w.getElement()); + return true; + } + return false; + } + + public void removeCell(String colKey) { + final HeaderCell c = getHeaderCell(colKey); + remove(c); + } + + private void focusSlot(int index) { + removeSlotFocus(); + if (index > 0) { + Element child = tr.getChild(index - 1).getFirstChild().cast(); + child.setClassName( + VScrollTable.this.getStylePrimaryName() + "-resizer"); + child.addClassName(VScrollTable.this.getStylePrimaryName() + + "-focus-slot-right"); + } else { + Element child = tr.getChild(index).getFirstChild().cast(); + child.setClassName( + VScrollTable.this.getStylePrimaryName() + "-resizer"); + child.addClassName(VScrollTable.this.getStylePrimaryName() + + "-focus-slot-left"); + } + focusedSlot = index; + } + + private void removeSlotFocus() { + if (focusedSlot < 0) { + return; + } + if (focusedSlot == 0) { + Element child = tr.getChild(focusedSlot).getFirstChild().cast(); + child.setClassName( + VScrollTable.this.getStylePrimaryName() + "-resizer"); + } else if (focusedSlot > 0) { + Element child = tr.getChild(focusedSlot - 1).getFirstChild() + .cast(); + child.setClassName( + VScrollTable.this.getStylePrimaryName() + "-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 boolean noncollapsible = false; + 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() { + if (noncollapsible) { + return; + } + 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; + } + + public void setNoncollapsible(boolean b) { + noncollapsible = b; + } + + /** + * Override default method to distinguish on/off columns + */ + + @Override + public String getHTML() { + final StringBuffer buf = new StringBuffer(); + buf.append("<span class=\""); + if (collapsed) { + buf.append("v-off"); + } else { + buf.append("v-on"); + } + if (noncollapsible) { + buf.append(" v-disabled"); + } + buf.append("\">"); + + buf.append(super.getHTML()); + buf.append("</span>"); + + return buf.toString(); + } + + } + + /* + * Returns columns as Action array for column select popup + */ + + @Override + public Action[] getActions() { + Object[] cols; + if (columnReordering && columnOrder != null) { + cols = columnOrder; + } else { + // if columnReordering is disabled, we need different way to get + // all available columns + cols = visibleColOrder; + cols = new Object[visibleColOrder.length + + collapsedColumns.size()]; + int i; + for (i = 0; i < visibleColOrder.length; i++) { + cols[i] = visibleColOrder[i]; + } + for (final Iterator<String> it = collapsedColumns.iterator(); it + .hasNext();) { + cols[i++] = it.next(); + } + } + List<Action> actions = new ArrayList<Action>(cols.length); + + for (int i = 0; i < cols.length; i++) { + final String cid = (String) cols[i]; + boolean noncollapsible = noncollapsibleColumns.contains(cid); + + if (noncollapsible + && collapsibleMenuContent == CollapseMenuContent.COLLAPSIBLE_COLUMNS) { + continue; + } + + final HeaderCell c = getHeaderCell(cid); + final VisibleColumnAction a = new VisibleColumnAction( + c.getColKey()); + a.setCaption(c.getCaption()); + if (!c.isEnabled()) { + a.setCollapsed(true); + } + if (noncollapsible) { + a.setNoncollapsible(true); + } + actions.add(a); + } + return actions.toArray(new Action[actions.size()]); + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + /** + * Returns column alignments for visible columns + */ + public char[] getColumnAlignments() { + final Iterator<Widget> it = visibleCells.iterator(); + final char[] aligns = new char[visibleCells.size()]; + int colIndex = 0; + while (it.hasNext()) { + aligns[colIndex++] = ((HeaderCell) it.next()).getAlign(); + } + return aligns; + } + + /** + * Disables the automatic calculation of all column widths by forcing + * the widths to be "defined" thus turning off expand ratios and such. + */ + public void disableAutoColumnWidthCalculation(HeaderCell source) { + for (HeaderCell cell : availableCells.values()) { + cell.disableAutoWidthCalculation(); + } + // fire column resize events for all columns but the source of the + // resize action, since an event will fire separately for this. + ArrayList<HeaderCell> columns = new ArrayList<HeaderCell>( + availableCells.values()); + columns.remove(source); + sendColumnWidthUpdates(columns); + forceRealignColumnHeaders(); + } + } + + /** + * A cell in the footer + */ + public class FooterCell extends Widget { + private final Element td = DOM.createTD(); + private final Element captionContainer = DOM.createDiv(); + private char align = ALIGN_LEFT; + private int width = -1; + private float expandRatio = 0; + private final String cid; + boolean definedWidth = false; + private int naturalWidth = -1; + + public FooterCell(String colId, String headerText) { + cid = colId; + + setText(headerText); + + // ensure no clipping initially (problem on column additions) + captionContainer.getStyle().setOverflow(Overflow.VISIBLE); + + DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS); + + DOM.appendChild(td, captionContainer); + + DOM.sinkEvents(td, + Event.MOUSEEVENTS | Event.ONDBLCLICK | Event.ONCONTEXTMENU); + + setElement(td); + + updateStyleNames(VScrollTable.this.getStylePrimaryName()); + } + + protected void updateStyleNames(String primaryStyleName) { + captionContainer + .setClassName(primaryStyleName + "-footer-container"); + } + + /** + * Sets the text of the footer + * + * @param footerText + * The text in the footer + */ + public void setText(String footerText) { + if (footerText == null || footerText.equals("")) { + 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: + captionContainer.getStyle().setTextAlign(TextAlign.CENTER); + break; + case ALIGN_RIGHT: + captionContainer.getStyle().setTextAlign(TextAlign.RIGHT); + break; + default: + captionContainer.getStyle().setTextAlign(TextAlign.LEFT); + 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. This width should not include any + * possible indent modifications that are present in + * {@link VScrollTableBody#getMaxIndent()}. + * + * @param w + * The width of the cell + * @param ensureDefinedWidth + * Ensures that 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 + captionContainer.getStyle().clearOverflow(); + } + width = w; + if (w == -1) { + captionContainer.getStyle().clearWidth(); + setWidth(""); + } else { + /* + * Reduce width with one pixel for the right border since the + * footers does not have any spacers between them. + */ + final int borderWidths = 1; + + // Set the container width (check for negative value) + captionContainer.getStyle().setPropertyPx("width", + Math.max(w - borderWidths, 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) { + int maxIndent = scrollBody.getMaxIndent(); + if (w < maxIndent && tFoot.visibleCells + .indexOf(this) == getHierarchyColumnIndex()) { + // ensure there's room for the indent + w = maxIndent; + } + int tdWidth = w + scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(Math.max(tdWidth, 0) + "px"); + } else { + Scheduler.get().scheduleDeferred(new Command() { + + @Override + public void execute() { + int tdWidth = width; + int maxIndent = scrollBody.getMaxIndent(); + if (tdWidth < maxIndent + && tFoot.visibleCells.indexOf( + this) == getHierarchyColumnIndex()) { + // ensure there's room for the indent + tdWidth = maxIndent; + } + tdWidth += scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(Math.max(tdWidth, 0) + "px"); + } + }); + } + } + } + + /** + * Sets the width to undefined + */ + 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; + } + + /** + * 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 ratio 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, + TableConstants.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, + TableConstants.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; + } + + /** + * Saves natural column width if it hasn't been saved already. + * + * @param columnIndex + * @since 7.3.9 + */ + protected void saveNaturalColumnWidthIfNotSaved(int columnIndex) { + 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() + getHeaderPadding(); + if (columnIndex < 0) { + columnIndex = 0; + for (Iterator<Widget> it = tHead.iterator(); it + .hasNext(); columnIndex++) { + if (it.next() == this) { + break; + } + } + } + final int cw = scrollBody.getColWidth(columnIndex); + naturalWidth = (hw > cw ? hw : cw); + } + } + + /** + * 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) { + final int iw = columnIndex == getHierarchyColumnIndex() + ? scrollBody.getMaxIndent() : 0; + saveNaturalColumnWidthIfNotSaved(columnIndex); + if (isDefinedWidth()) { + if (iw > width) { + return iw; + } + return width; + } else { + if (iw > naturalWidth) { + return iw; + } else { + return naturalWidth; + } + } + } + + public void setNaturalMinimumColumnWidth(int w) { + naturalWidth = w; + } + } + + /** + * HeaderCell that is header cell for row headers. + * + * Reordering disabled and clicking on it resets sorting. + */ + public class RowHeadersFooterCell extends FooterCell { + + RowHeadersFooterCell() { + super(ROW_HEADER_COLUMN_KEY, ""); + } + + @Override + protected void handleCaptionEvent(Event event) { + // NOP: RowHeaders cannot be reordered + // TODO It'd be nice to reset sorting here + } + } + + /** + * The footer of the table which can be seen in the bottom of the Table. + */ + public class TableFooter extends Panel { + + private static final int WRAPPER_WIDTH = 900000; + + ArrayList<Widget> visibleCells = new ArrayList<Widget>(); + HashMap<String, FooterCell> availableCells = new HashMap<String, FooterCell>(); + + Element div = DOM.createDiv(); + Element hTableWrapper = DOM.createDiv(); + Element hTableContainer = DOM.createDiv(); + Element table = DOM.createTable(); + Element headerTableBody = DOM.createTBody(); + Element tr = DOM.createTR(); + + public TableFooter() { + + hTableWrapper.getStyle().setOverflow(Overflow.HIDDEN); + + DOM.appendChild(table, headerTableBody); + DOM.appendChild(headerTableBody, tr); + DOM.appendChild(hTableContainer, table); + DOM.appendChild(hTableWrapper, hTableContainer); + DOM.appendChild(div, hTableWrapper); + setElement(div); + + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersFooterCell()); + + updateStyleNames(VScrollTable.this.getStylePrimaryName()); + } + + protected void updateStyleNames(String primaryStyleName) { + hTableWrapper.setClassName(primaryStyleName + "-footer"); + setStyleName(primaryStyleName + "-footer-wrap"); + for (FooterCell c : availableCells.values()) { + c.updateStyleNames(primaryStyleName); + } + } + + @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() + */ + + @Override + public Iterator<Widget> iterator() { + return visibleCells.iterator(); + } + + /** + * Gets a footer cell which represents the given columnId + * + * @param cid + * The columnId + * + * @return The cell + */ + public FooterCell getFooterCell(String cid) { + return availableCells.get(cid); + } + + /** + * Gets a footer cell by using a column index + * + * @param index + * The index of the column + * @return The Cell + */ + public FooterCell getFooterCell(int index) { + if (index < visibleCells.size()) { + return (FooterCell) visibleCells.get(index); + } else { + return null; + } + } + + /** + * Updates the cells contents when updateUIDL request is received + * + * @param uidl + * The UIDL + */ + public void updateCellsFromUIDL(UIDL uidl) { + Iterator<?> columnIterator = uidl.getChildIterator(); + HashSet<String> updated = new HashSet<String>(); + while (columnIterator.hasNext()) { + final UIDL col = (UIDL) columnIterator.next(); + final String cid = col.getStringAttribute("cid"); + updated.add(cid); + + String caption = col.hasAttribute("fcaption") + ? col.getStringAttribute("fcaption") : ""; + FooterCell c = getFooterCell(cid); + if (c == null) { + c = new FooterCell(cid, caption); + availableCells.put(cid, c); + if (initializedAndAttached) { + // we will need a column width recalculation + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } else { + c.setText(caption); + } + + if (col.hasAttribute("align")) { + c.setAlign(col.getStringAttribute("align").charAt(0)); + } else { + c.setAlign(ALIGN_LEFT); + + } + if (col.hasAttribute("width")) { + if (scrollBody == null || isNewBody) { + // Already updated by setColWidth called from + // TableHeads.updateCellsFromUIDL in case of a server + // side resize + final int width = col.getIntAttribute("width"); + 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; + } + } + } + + // check for orphaned header cells + for (Iterator<String> cit = availableCells.keySet().iterator(); cit + .hasNext();) { + String cid = cit.next(); + if (!updated.contains(cid)) { + removeCell(cid); + cit.remove(); + } + } + } + + /** + * Set a footer cell for a specified column index + * + * @param index + * The index + * @param cell + * The footer cell + */ + public void setFooterCell(int index, FooterCell cell) { + if (cell.isEnabled()) { + // we're moving the cell + DOM.removeChild(tr, cell.getElement()); + orphan(cell); + visibleCells.remove(cell); + } + if (index < visibleCells.size()) { + // insert to right slot + DOM.insertChild(tr, cell.getElement(), index); + adopt(cell); + visibleCells.add(index, cell); + } else if (index == visibleCells.size()) { + // simply append + DOM.appendChild(tr, cell.getElement()); + adopt(cell); + visibleCells.add(cell); + } else { + throw new RuntimeException( + "Header cells must be appended in order"); + } + } + + /** + * Remove a cell by using the columnId + * + * @param colKey + * The columnId to remove + */ + public void removeCell(String colKey) { + final FooterCell c = getFooterCell(colKey); + remove(c); + } + + /** + * Enable a column (Sets the footer cell) + * + * @param cid + * The columnId + * @param index + * The index of the column + */ + public void enableColumn(String cid, int index) { + final FooterCell c = getFooterCell(cid); + if (!c.isEnabled() || getFooterCell(index) != c) { + setFooterCell(index, c); + if (initializedAndAttached) { + headerChangedDuringUpdate = true; + } + } + } + + /** + * Disable browser measurement of the table width + */ + public void disableBrowserIntelligence() { + hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX); + } + + /** + * Enable browser measurement of the table width + */ + public void enableBrowserIntelligence() { + hTableContainer.getStyle().clearWidth(); + } + + /** + * Set the horizontal position in the cell in the footer. This is done + * when a horizontal scrollbar is present. + * + * @param scrollLeft + * The value of the leftScroll + */ + public void setHorizontalScrollPosition(int scrollLeft) { + hTableWrapper.setScrollLeft(scrollLeft); + } + + /** + * Swap cells when the column are dragged + * + * @param oldIndex + * The old index of the cell + * @param newIndex + * The new index of the cell + */ + public void moveCell(int oldIndex, int newIndex) { + final FooterCell hCell = getFooterCell(oldIndex); + final Element cell = hCell.getElement(); + + visibleCells.remove(oldIndex); + DOM.removeChild(tr, cell); + + DOM.insertChild(tr, cell, newIndex); + visibleCells.add(newIndex, hCell); + } + } + + /** + * This Panel can only contain VScrollTableRow type of widgets. This + * "simulates" very large table, keeping spacers which take room of + * unrendered rows. + * + */ + public class VScrollTableBody extends Panel { + + public static final int DEFAULT_ROW_HEIGHT = 24; + + private double rowHeight = -1; + + private final LinkedList<Widget> renderedRows = new LinkedList<Widget>(); + + /** + * Due some optimizations row height measuring is deferred and initial + * set of rows is rendered detached. Flag set on when table body has + * been attached in dom and rowheight has been measured. + */ + private boolean tBodyMeasurementsDone = false; + + Element preSpacer = DOM.createDiv(); + Element postSpacer = DOM.createDiv(); + + Element container = DOM.createDiv(); + + TableSectionElement tBodyElement = Document.get().createTBodyElement(); + Element table = DOM.createTable(); + + private int firstRendered; + private int lastRendered; + + private char[] aligns; + + protected VScrollTableBody() { + constructDOM(); + setElement(container); + } + + public void setLastRendered(int lastRendered) { + if (totalRows >= 0 && lastRendered > totalRows) { + VConsole.log("setLastRendered: " + this.lastRendered + " -> " + + lastRendered); + this.lastRendered = totalRows - 1; + } else { + this.lastRendered = lastRendered; + } + } + + public int getLastRendered() { + + return lastRendered; + } + + public int getFirstRendered() { + + return firstRendered; + } + + 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() + + WidgetUtil.getRequiredHeight(table); + } + + private void constructDOM() { + if (BrowserInfo.get().isIE()) { + table.setPropertyInt("cellSpacing", 0); + } + + table.appendChild(tBodyElement); + DOM.appendChild(container, preSpacer); + DOM.appendChild(container, table); + DOM.appendChild(container, postSpacer); + if (BrowserInfo.get().requiresTouchScrollDelegate()) { + NodeList<Node> 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)"); + } + } + updateStyleNames(VScrollTable.this.getStylePrimaryName()); + } + + protected void updateStyleNames(String primaryStyleName) { + table.setClassName(primaryStyleName + "-table"); + preSpacer.setClassName(primaryStyleName + "-row-spacer"); + postSpacer.setClassName(primaryStyleName + "-row-spacer"); + for (Widget w : renderedRows) { + VScrollTableRow row = (VScrollTableRow) w; + row.updateStyleNames(primaryStyleName); + } + } + + public int getAvailableWidth() { + int availW = scrollBodyPanel.getOffsetWidth() - getBorderWidth(); + return availW; + } + + public void renderInitialRows(UIDL rowData, int firstIndex, int rows) { + firstRendered = firstIndex; + setLastRendered(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); + setLastRendered(lastRendered + 1); + } + 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 + + // there can't be sanity checks for last rendered within this + // while loop regardless of what has been set previously, so + // change it temporarily to true and then return the original + // value + boolean temp = postponeSanityCheckForLastRendered; + postponeSanityCheckForLastRendered = true; + while (lastRendered + 1 > firstRendered) { + unlinkRow(false); + } + postponeSanityCheckForLastRendered = temp; + VScrollTableRow row = prepareRow((UIDL) it.next()); + firstRendered = firstIndex; + setLastRendered(firstIndex - 1); + addRow(row); + setLastRendered(lastRendered + 1); + setContainerHeight(); + fixSpacers(); + + while (it.hasNext()) { + addRow(prepareRow((UIDL) it.next())); + setLastRendered(lastRendered + 1); + } + + 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() { + + /** + * Fixes cache issue #13576 where unnecessary rows are fetched + */ + if (isLazyScrollerActive()) { + return; + } + + 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.triggerRowFetch(reactFirstRow, + reactLastRow - reactFirstRow + 1, 1); + } else if (lastRendered < reactLastRow) { + // get some cache rows below visible area + rowRequestHandler.triggerRowFetch(lastRendered + 1, + reactLastRow - lastRendered, 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.triggerRowFetch(reactFirstRow, + firstRendered - reactFirstRow, 1); + } + } + + /** + * Inserts rows as provided in the rowData starting at firstIndex. + * + * @param rowData + * @param firstIndex + * @param rows + * the number of rows + * @return a list of the rows added. + */ + protected List<VScrollTableRow> insertRows(UIDL rowData, int firstIndex, + int rows) { + aligns = tHead.getColumnAlignments(); + final Iterator<?> it = rowData.getChildIterator(); + List<VScrollTableRow> insertedRows = new ArrayList<VScrollTableRow>(); + + if (firstIndex == lastRendered + 1) { + while (it.hasNext()) { + final VScrollTableRow row = prepareRow((UIDL) it.next()); + addRow(row); + insertedRows.add(row); + if (postponeSanityCheckForLastRendered) { + lastRendered++; + } else { + setLastRendered(lastRendered + 1); + } + } + 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); + if (postponeSanityCheckForLastRendered) { + lastRendered++; + } else { + setLastRendered(lastRendered + 1); + } + ix++; + } + fixSpacers(); + } + return insertedRows; + } + + protected List<VScrollTableRow> insertAndReindexRows(UIDL rowData, + int firstIndex, int rows) { + List<VScrollTableRow> inserted = insertRows(rowData, firstIndex, + rows); + int actualIxOfFirstRowAfterInserted = firstIndex + rows + - firstRendered; + for (int ix = actualIxOfFirstRowAfterInserted; ix < renderedRows + .size(); ix++) { + VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix); + r.setIndex(r.getIndex() + rows); + } + setContainerHeight(); + return inserted; + } + + protected void insertRowsDeleteBelow(UIDL rowData, int firstIndex, + int rows) { + unlinkAllRowsStartingAt(firstIndex); + insertRows(rowData, firstIndex, rows); + setContainerHeight(); + } + + /** + * This method is used to instantiate new rows for this table. It + * automatically sets correct widths to rows cells and assigns correct + * client reference for child widgets. + * + * This method can be called only after table has been initialized + * + * @param uidl + */ + private VScrollTableRow prepareRow(UIDL uidl) { + final VScrollTableRow row = createRow(uidl, aligns); + row.initCellWidths(); + return row; + } + + protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) { + if (uidl.hasAttribute("gen_html")) { + // This is a generated row. + return new VScrollTableGeneratedRow(uidl, aligns2); + } + return new VScrollTableRow(uidl, aligns2); + } + + private void addRowBeforeFirstRendered(VScrollTableRow row) { + row.setIndex(firstRendered - 1); + if (row.isSelected()) { + row.addStyleName("v-selected"); + } + tBodyElement.insertBefore(row.getElement(), + tBodyElement.getFirstChild()); + adopt(row); + renderedRows.add(0, row); + } + + private void addRow(VScrollTableRow row) { + row.setIndex(firstRendered + renderedRows.size()); + if (row.isSelected()) { + row.addStyleName("v-selected"); + } + tBodyElement.appendChild(row.getElement()); + // Add to renderedRows before adopt so iterator() will return also + // this row if called in an attach handler (#9264) + renderedRows.add(row); + adopt(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); + } + + @Override + public Iterator<Widget> iterator() { + return renderedRows.iterator(); + } + + /** + * @return false if couldn't remove row + */ + protected boolean unlinkRow(boolean fromBeginning) { + if (lastRendered - firstRendered < 0) { + return false; + } + int actualIx; + if (fromBeginning) { + actualIx = 0; + firstRendered++; + } else { + actualIx = renderedRows.size() - 1; + if (postponeSanityCheckForLastRendered) { + --lastRendered; + } else { + setLastRendered(lastRendered - 1); + } + } + 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) { + count = count - (firstRendered - firstIndex); + firstIndex = firstRendered; + } + int lastIndex = firstIndex + count - 1; + if (lastRendered < lastIndex) { + lastIndex = lastRendered; + } + for (int ix = lastIndex; ix >= firstIndex; ix--) { + unlinkRowAtActualIndex(actualIndex(ix)); + if (postponeSanityCheckForLastRendered) { + // partialUpdate handles sanity check later + lastRendered--; + } else { + setLastRendered(lastRendered - 1); + } + } + 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)); + setLastRendered(lastRendered - 1); + } + fixSpacers(); + } + + private int actualIndex(int index) { + return index - firstRendered; + } + + private void unlinkRowAtActualIndex(int index) { + final VScrollTableRow toBeRemoved = (VScrollTableRow) renderedRows + .get(index); + 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(); + container.getStyle().setHeight(measureRowHeightOffset(totalRows), + Unit.PX); + } + + private void fixSpacers() { + int prepx = measureRowHeightOffset(firstRendered); + if (prepx < 0) { + prepx = 0; + } + preSpacer.getStyle().setPropertyPx("height", prepx); + int postpx; + if (pageLength == 0 && totalRows == pageLength) { + /* + * TreeTable depends on having lastRendered out of sync in some + * situations, which makes this method miss the special + * situation in which one row worth of post spacer to be added + * if there are no rows in the table. #9203 + */ + postpx = measureRowHeightOffset(1); + } else { + 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 { + // Special cases if we can't just measure the current rows + if (!Double.isNaN(lastKnownRowHeight)) { + // Use previous value if available + if (BrowserInfo.get().isIE()) { + /* + * IE needs to reflow the table element at this + * point to work correctly (e.g. + * com.vaadin.tests.components.table. + * ContainerSizeChange) - the other code paths + * already trigger reflows, but here it must be done + * explicitly. + */ + getTableHeight(); + } + rowHeight = lastKnownRowHeight; + } 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; + } + } + lastKnownRowHeight = rowHeight; + 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(); + // Spanned rows might cause an NPE. + if (columnIndex < tr.getChildCount()) { + 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; + } + + /** + * This method exists for the needs of {@link VTreeTable} only. May be + * removed or replaced in the future.</br> + * </br> + * Returns the maximum indent of the hierarcyColumn, if applicable. + * + * @see {@link VScrollTable#getHierarchyColumnIndex()} + * + * @return maximum indent in pixels + */ + protected int getMaxIndent() { + return 0; + } + + /** + * This method exists for the needs of {@link VTreeTable} only. May be + * removed or replaced in the future.</br> + * </br> + * Calculates the maximum indent of the hierarcyColumn, if applicable. + */ + protected void calculateMaxIndent() { + // NOP + } + + private void detectExtrawidth() { + NodeList<TableRowElement> rows = tBodyElement.getRows(); + if (rows.getLength() == 0) { + /* need to temporary add empty row and detect */ + VScrollTableRow scrollTableRow = new VScrollTableRow(); + scrollTableRow.updateStyleNames( + VScrollTable.this.getStylePrimaryName()); + 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); + } + } + } + + 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 int indexOf(Widget row) { + int relIx = -1; + for (int ix = 0; ix < renderedRows.size(); ix++) { + if (renderedRows.get(ix) == row) { + relIx = ix; + break; + } + } + if (relIx >= 0) { + return firstRendered + relIx; + } + return -1; + } + + public class VScrollTableRow extends Panel + implements ActionOwner, ContextMenuOwner { + + private static final int TOUCHSCROLL_TIMEOUT = 100; + private static final int DRAGMODE_MULTIROW = 2; + protected ArrayList<Widget> childWidgets = new ArrayList<Widget>(); + private boolean selected = false; + protected final int rowKey; + + private String[] actionKeys = null; + private final TableRowElement rowElement; + private int index; + private Event touchStart; + + private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500; + private Timer contextTouchTimeout; + private Timer dragTouchTimeout; + private int touchStartY; + private int touchStartX; + + private TouchContextProvider touchContextProvider = new TouchContextProvider( + this); + + private TooltipInfo tooltipInfo = null; + private Map<TableCellElement, TooltipInfo> cellToolTips = new HashMap<TableCellElement, TooltipInfo>(); + private boolean isDragging = false; + private String rowStyle = null; + protected boolean applyZeroWidthFix = true; + + 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"); + + rowStyle = uidl.getStringAttribute("rowstyle"); + updateStyleNames(VScrollTable.this.getStylePrimaryName()); + + String rowDescription = uidl.getStringAttribute("rowdescr"); + if (rowDescription != null && !rowDescription.equals("")) { + tooltipInfo = new TooltipInfo(rowDescription, null, this); + } else { + tooltipInfo = 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(); + } + } + + protected void updateStyleNames(String primaryStyleName) { + + if (getStylePrimaryName().contains("odd")) { + setStyleName(primaryStyleName + "-row-odd"); + } else { + setStyleName(primaryStyleName + "-row"); + } + + if (rowStyle != null) { + addStyleName(primaryStyleName + "-row-" + rowStyle); + } + + for (int i = 0; i < rowElement.getChildCount(); i++) { + TableCellElement cell = (TableCellElement) rowElement + .getChild(i); + updateCellStyleNames(cell, primaryStyleName); + } + } + + public TooltipInfo getTooltipInfo() { + return tooltipInfo; + } + + /** + * Add a dummy row, used for measurements if Table is empty. + */ + public VScrollTableRow() { + this(0); + 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); + Style wrapperStyle = cell.getFirstChildElement().getStyle(); + int wrapperWidth = width; + if (BrowserInfo.get().isWebkit() + || BrowserInfo.get().isOpera10()) { + /* + * Some versions of Webkit and Opera ignore the width + * definition of zero width table cells. Instead, use 1px + * and compensate with a negative margin. + */ + if (applyZeroWidthFix && width == 0) { + wrapperWidth = 1; + wrapperStyle.setMarginRight(-1, Unit.PX); + } else { + wrapperStyle.clearMarginRight(); + } + } + wrapperStyle.setPropertyPx("width", wrapperWidth); + 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, description); + } + } + } + + /** + * 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 absoluteBottom = absoluteTop + getOffsetHeight(); + int viewPortTop = scrollBodyPanel.getAbsoluteTop(); + int viewPortBottom = viewPortTop + + scrollBodyPanel.getOffsetHeight(); + return absoluteBottom > viewPortTop + && absoluteTop < viewPortBottom; + } + + /** + * 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. + String primaryStyleName = getStylePrimaryName(); + if (primaryStyleName != null && !primaryStyleName.equals("")) { + removeStyleName(getStylePrimaryName()); + } + if (!isOdd) { + addStyleName(VScrollTable.this.getStylePrimaryName() + + "-row-odd"); + } else { + addStyleName( + VScrollTable.this.getStylePrimaryName() + "-row"); + } + } + + 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(); + container.setClassName(VScrollTable.this.getStylePrimaryName() + + "-cell-wrapper"); + + td.setClassName(VScrollTable.this.getStylePrimaryName() + + "-cell-content"); + + if (style != null && !style.equals("")) { + td.addClassName(VScrollTable.this.getStylePrimaryName() + + "-cell-content-" + style); + } + + if (sorted) { + td.addClassName(VScrollTable.this.getStylePrimaryName() + + "-cell-content-sorted"); + } + + if (textIsHTML) { + container.setInnerHTML(text); + } else { + container.setInnerText(text); + } + setAlign(align, container); + setTooltip(td, description); + + td.appendChild(container); + getElement().appendChild(td); + } + + protected void updateCellStyleNames(TableCellElement td, + String primaryStyleName) { + Element container = td.getFirstChild().cast(); + container.setClassName(primaryStyleName + "-cell-wrapper"); + + /* + * Replace old primary style name with new one + */ + String className = td.getClassName(); + String oldPrimaryName = className.split("-cell-content")[0]; + td.setClassName( + className.replaceAll(oldPrimaryName, primaryStyleName)); + } + + public void addCell(UIDL rowUidl, Widget w, char align, + String style, boolean sorted, String description) { + final TableCellElement td = DOM.createTD().cast(); + initCellWithWidget(w, align, style, sorted, td); + setTooltip(td, description); + } + + private void setTooltip(TableCellElement td, String description) { + if (description != null && !description.equals("")) { + TooltipInfo info = new TooltipInfo(description, null, this); + cellToolTips.put(td, info); + } else { + cellToolTips.remove(td); + } + + } + + private void setAlign(char align, final Element container) { + switch (align) { + case ALIGN_CENTER: + container.getStyle().setProperty("textAlign", "center"); + break; + case ALIGN_LEFT: + container.getStyle().setProperty("textAlign", "left"); + break; + case ALIGN_RIGHT: + default: + container.getStyle().setProperty("textAlign", "right"); + break; + } + } + + protected void initCellWithWidget(Widget w, char align, + String style, boolean sorted, final TableCellElement td) { + final Element container = DOM.createDiv(); + String className = VScrollTable.this.getStylePrimaryName() + + "-cell-content"; + if (style != null && !style.equals("")) { + className += " " + VScrollTable.this.getStylePrimaryName() + + "-cell-content-" + style; + } + if (sorted) { + className += " " + VScrollTable.this.getStylePrimaryName() + + "-cell-content-sorted"; + } + td.setClassName(className); + container.setClassName(VScrollTable.this.getStylePrimaryName() + + "-cell-wrapper"); + setAlign(align, container); + 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); + } + + @Override + public Iterator<Widget> iterator() { + return childWidgets.iterator(); + } + + @Override + public boolean remove(Widget w) { + if (childWidgets.contains(w)) { + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), + w.getElement()); + childWidgets.remove(w); + return true; + } else { + return false; + } + } + + /** + * If there are registered click listeners, sends a click event and + * returns true. Otherwise, does nothing and returns false. + * + * @param event + * @param targetTdOrTr + * @param immediate + * Whether the event is sent immediately + * @return Whether a click event was sent + */ + private boolean handleClickEvent(Event event, Element targetTdOrTr, + boolean immediate) { + if (!client.hasEventListeners(VScrollTable.this, + TableConstants.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; + } + + public TooltipInfo getTooltip( + com.google.gwt.dom.client.Element target) { + + TooltipInfo info = null; + final Element targetTdOrTr = getTdOrTr(target); + if (targetTdOrTr != null && "td" + .equals(targetTdOrTr.getTagName().toLowerCase())) { + TableCellElement td = (TableCellElement) targetTdOrTr + .cast(); + info = cellToolTips.get(td); + } + + if (info == null) { + info = tooltipInfo; + } + + return info; + } + + private Element getTdOrTr(Element target) { + Element thisTrElement = getElement(); + if (target == thisTrElement) { + // This was a on the TR element + return target; + } + + // Iterate upwards until we find the TR element + Element element = target; + while (element != null + && element.getParentElement() != thisTrElement) { + element = element.getParentElement(); + } + return element; + } + + /** + * Special handler for touch devices that support native scrolling + * + * @return Whether the event was handled by this method. + */ + private boolean handleTouchEvent(final Event event) { + + boolean touchEventHandled = false; + + if (enabled && hasNativeTouchScrolling) { + touchContextProvider.handleTouchEvent(event); + + final Element targetTdOrTr = getEventTargetTdOrTr(event); + final int type = event.getTypeInt(); + + switch (type) { + case Event.ONTOUCHSTART: + touchEventHandled = true; + touchStart = event; + isDragging = false; + Touch touch = event.getChangedTouches().get(0); + // save position to fields, touches in events are same + // instance during the operation. + touchStartX = touch.getClientX(); + touchStartY = touch.getClientY(); + + if (dragmode != 0) { + if (dragTouchTimeout == null) { + dragTouchTimeout = new Timer() { + + @Override + public void run() { + if (touchStart != null) { + // Start a drag if a finger is held + // in place long enough, then moved + isDragging = true; + } + } + }; + } + dragTouchTimeout.schedule(TOUCHSCROLL_TIMEOUT); + } + + if (actionKeys != null) { + if (contextTouchTimeout == null) { + contextTouchTimeout = new Timer() { + + @Override + public void run() { + if (touchStart != null) { + // Open the context menu if finger + // is held in place long enough. + showContextMenu(touchStart); + event.preventDefault(); + touchStart = null; + } + } + }; + } + contextTouchTimeout + .schedule(TOUCH_CONTEXT_MENU_TIMEOUT); + event.stopPropagation(); + } + break; + case Event.ONTOUCHMOVE: + touchEventHandled = true; + if (isSignificantMove(event)) { + if (contextTouchTimeout != null) { + // Moved finger before the context menu timer + // expired, so let the browser handle this as a + // scroll. + contextTouchTimeout.cancel(); + contextTouchTimeout = null; + } + if (!isDragging && dragTouchTimeout != null) { + // Moved finger before the drag timer expired, + // so let the browser handle this as a scroll. + dragTouchTimeout.cancel(); + dragTouchTimeout = null; + } + + if (dragmode != 0 && touchStart != null + && isDragging) { + event.preventDefault(); + event.stopPropagation(); + startRowDrag(touchStart, type, targetTdOrTr); + } + touchStart = null; + } + break; + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + touchEventHandled = true; + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + } + if (dragTouchTimeout != null) { + dragTouchTimeout.cancel(); + } + if (touchStart != null) { + if (!BrowserInfo.get().isAndroid()) { + event.preventDefault(); + WidgetUtil.simulateClickFromTouchEvent( + touchStart, this); + } + event.stopPropagation(); + touchStart = null; + } + isDragging = false; + break; + } + } + return touchEventHandled; + } + + /* + * React on click that occur on content cells only + */ + + @Override + public void onBrowserEvent(final Event event) { + + final boolean touchEventHandled = handleTouchEvent(event); + + if (enabled && !touchEventHandled) { + final int type = event.getTypeInt(); + final Element targetTdOrTr = getEventTargetTdOrTr(event); + if (type == Event.ONCONTEXTMENU) { + showContextMenu(event); + if (enabled && (actionKeys != null + || client.hasEventListeners(VScrollTable.this, + TableConstants.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; + + switch (type) { + case Event.ONDBLCLICK: + if (targetCellOrRowFound) { + handleClickEvent(event, targetTdOrTr, true); + } + break; + case Event.ONMOUSEUP: + /* + * Only fire a click if the mouseup hits the same + * element as the corresponding mousedown. This is first + * checked in the event preview but we can't fire the + * event there as the event might get canceled before it + * gets here. + */ + if (mouseUpPreviewMatched && lastMouseDownTarget != null + && lastMouseDownTarget == getElementTdOrTr( + WidgetUtil + .getElementUnderMouse(event))) { + // "Click" with left, right or middle button + + if (targetCellOrRowFound) { + /* + * 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(); + } + } + } + mouseUpPreviewMatched = false; + lastMouseDownTarget = null; + 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. + */ + WidgetUtil.simulateClickFromTouchEvent(touchStart, + this); + touchStart = null; + } + touchContextProvider.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); + } + touchContextProvider.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 + // instance 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; + } + } + }; + } + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + contextTouchTimeout + .schedule(TOUCH_CONTEXT_MENU_TIMEOUT); + } + } + break; + case Event.ONMOUSEDOWN: + /* + * When getting a mousedown event, we must detect where + * the corresponding mouseup event if it's on a + * different part of the page. + */ + lastMouseDownTarget = getElementTdOrTr( + WidgetUtil.getElementUnderMouse(event)); + mouseUpPreviewMatched = false; + mouseUpEventPreviewRegistration = Event + .addNativePreviewHandler(mouseUpPreviewHandler); + + 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: + 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; + } + + /** + * Checks if the row represented by the row key has been selected + * + * @param key + * The generated row key + */ + private boolean rowKeyIsSelected(int rowKey) { + // Check single selections + if (selectedRowKeys.contains("" + rowKey)) { + return true; + } + + // Check range selections + for (SelectionRange r : selectedRowRanges) { + if (r.inRange(getRenderedRowByKey("" + rowKey))) { + return true; + } + } + return false; + } + + protected void startRowDrag(Event event, final int type, + Element targetTdOrTr) { + VTransferable transferable = new VTransferable(); + transferable.setDragSource(ConnectorMap.get(client) + .getConnector(VScrollTable.this)); + transferable.setData("itemId", "" + rowKey); + NodeList<TableCellElement> cells = rowElement.getCells(); + for (int i = 0; i < cells.getLength(); i++) { + if (cells.getItem(i).isOrHasChild(targetTdOrTr)) { + HeaderCell headerCell = tHead.getHeaderCell(i); + transferable.setData("propertyId", headerCell.cid); + break; + } + } + + VDragEvent ev = VDragAndDropManager.get() + .startDrag(transferable, event, true); + if (dragmode == DRAGMODE_MULTIROW && isMultiSelectModeAny() + && rowKeyIsSelected(rowKey)) { + + // Create a drag image of ALL rows + ev.createDragImage(scrollBody.tBodyElement, true); + + // Hide rows which are not selected + Element dragImage = ev.getDragImage(); + int i = 0; + for (Iterator<Widget> iterator = scrollBody + .iterator(); iterator.hasNext();) { + VScrollTableRow next = (VScrollTableRow) iterator + .next(); + + Element child = (Element) dragImage.getChild(i++); + + if (!rowKeyIsSelected(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(); + return getElementTdOrTr(eventTarget); + } + + private Element getElementTdOrTr(Element element) { + + Widget widget = WidgetUtil.findWidget(element, null); + + 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 VLegacyTextField + && ((VLegacyTextField) widget) + .isReadOnly())) { + return null; + } + } + return getTdOrTr(element); + } + + @Override + public void showContextMenu(Event event) { + if (enabled && actionKeys != null) { + // Show context menu if there are registered action handlers + int left = WidgetUtil.getTouchOrMouseClientX(event) + + Window.getScrollLeft(); + int top = WidgetUtil.getTouchOrMouseClientY(event) + + Window.getScrollTop(); + showContextMenu(left, top); + } + } + + public void showContextMenu(int left, int top) { + VContextMenu menu = client.getContextMenu(); + contextMenu = new ContextMenuDetails(menu, getKey(), left, top); + menu.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; + selectionRangeStart = focusedRow; + // If start row is null then we have a multipage selection + // from above + if (startRow == null) { + startRow = (VScrollTableRow) scrollBody.iterator() + .next(); + setRowFocus(endRow); + } + } else if (!startRow.isSelected()) { + // The start row is no longer selected (probably removed) + // and so we select from above + 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.client.ui.IActionOwner#getActions () + */ + + @Override + 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; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + private int getColIndexOf(Widget child) { + com.google.gwt.dom.client.Element widgetCell = child + .getElement().getParentElement().getParentElement(); + NodeList<TableCellElement> cells = rowElement.getCells(); + for (int i = 0; i < cells.getLength(); i++) { + if (cells.getItem(i) == widgetCell) { + return i; + } + } + return -1; + } + + public Widget getWidgetForPaintable() { + return this; + } + } + + protected class VScrollTableGeneratedRow extends VScrollTableRow { + + private boolean spanColumns; + private boolean htmlContentAllowed; + + public VScrollTableGeneratedRow(UIDL uidl, char[] aligns) { + super(uidl, aligns); + addStyleName("v-table-generated-row"); + } + + public boolean isSpanColumns() { + return spanColumns; + } + + @Override + protected void initCellWidths() { + if (spanColumns) { + setSpannedColumnWidthAfterDOMFullyInited(); + } else { + super.initCellWidths(); + } + } + + private void setSpannedColumnWidthAfterDOMFullyInited() { + // Defer setting width on spanned columns to make sure that + // they are added to the DOM before trying to calculate + // widths. + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + if (showRowHeaders) { + setCellWidth(0, tHead.getHeaderCell(0) + .getWidthWithIndent()); + 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(); + } + WidgetUtil.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.getLastRendered() + - scrollBody.getFirstRendered(); + if (currentlyVisible < pageLength + && currentlyVisible < totalRows) { + // shake scrollpanel to fill empty space + scrollBodyPanel.setScrollPosition(scrollTop + 1); + scrollBodyPanel.setScrollPosition(scrollTop - 1); + } + + sizeNeedsInit = true; + } + } + + } + + /** For internal use only. May be removed or replaced in the future. */ + public void updateWidth() { + if (!isVisible()) { + /* + * Do not update size when the table is hidden as all column widths + * will be set to zero and they won't be recalculated when the table + * is set visible again (until the size changes again) + */ + return; + } + + if (!isDynamicWidth()) { + int innerPixels = getOffsetWidth() - getBorderWidth(); + if (innerPixels < 0) { + innerPixels = 0; + } + setContentWidth(innerPixels); + + // readjust undefined width columns + triggerLazyColumnAdjustment(false); + + } else { + + sizeNeedsInit = true; + + // readjust undefined width columns + triggerLazyColumnAdjustment(false); + } + + /* + * setting width may affect wheter the component has scrollbars -> needs + * scrolling or not + */ + setProperTabIndex(); + } + + private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300; + + private final Timer lazyAdjustColumnWidths = new Timer() { + /** + * Check for column widths, and available width, to see if we can fix + * column widths "optimally". Doing this lazily to avoid expensive + * calculation when resizing is not yet finished. + */ + + @Override + public void run() { + if (scrollBody == null) { + // Try again later if we get here before scrollBody has been + // initalized + triggerLazyColumnAdjustment(false); + return; + } + + Iterator<Widget> headCells = tHead.iterator(); + int usedMinimumWidth = 0; + int totalExplicitColumnsWidths = 0; + float expandRatioDivider = 0; + int colIndex = 0; + + int hierarchyColumnIndent = scrollBody.getMaxIndent(); + int hierarchyColumnIndex = getHierarchyColumnIndex(); + HeaderCell hierarchyHeaderInNeedOfFurtherHandling = null; + + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + boolean hasIndent = hierarchyColumnIndent > 0 + && hCell.isHierarchyColumn(); + if (hCell.isDefinedWidth()) { + // get width without indent to find out whether adjustments + // are needed (requires special handling further ahead) + int w = hCell.getWidth(); + if (hasIndent && w < hierarchyColumnIndent) { + // enforce indent if necessary + w = hierarchyColumnIndent; + hierarchyHeaderInNeedOfFurtherHandling = hCell; + } + totalExplicitColumnsWidths += w; + usedMinimumWidth += w; + } else { + // natural width already includes indent if any + int naturalColumnWidth = hCell + .getNaturalColumnWidth(colIndex); + /* + * TODO If there is extra width, expand ratios are for + * additional extra widths, not for absolute column widths. + * Should be fixed in sizeInit(), too. + */ + if (hCell.getExpandRatio() > 0) { + naturalColumnWidth = 0; + } + usedMinimumWidth += naturalColumnWidth; + expandRatioDivider += hCell.getExpandRatio(); + if (hasIndent) { + hierarchyHeaderInNeedOfFurtherHandling = hCell; + } + } + colIndex++; + } + + int availW = scrollBody.getAvailableWidth(); + // Hey IE, are you really sure about this? + availW = scrollBody.getAvailableWidth(); + int visibleCellCount = tHead.getVisibleCellCount(); + int totalExtraWidth = scrollBody.getCellExtraWidth() + * visibleCellCount; + if (willHaveScrollbars()) { + totalExtraWidth += WidgetUtil.getNativeScrollbarSize(); + // if there will be vertical scrollbar, let's enable it + scrollBodyPanel.getElement().getStyle().clearOverflowY(); + } else { + // if there is no need for vertical scrollbar, let's disable it + // this is necessary since sometimes the browsers insist showing + // the scrollbar even if the content would fit perfectly + scrollBodyPanel.getElement().getStyle() + .setOverflowY(Overflow.HIDDEN); + } + + availW -= totalExtraWidth; + int forceScrollBodyWidth = -1; + + int extraSpace = availW - usedMinimumWidth; + if (extraSpace < 0) { + if (getTotalRows() == 0) { + /* + * Too wide header combined with no rows in the table. + * + * No horizontal scrollbars would be displayed because + * there's no rows that grows too wide causing the + * scrollBody container div to overflow. Must explicitely + * force a width to a scrollbar. (see #9187) + */ + forceScrollBodyWidth = usedMinimumWidth + totalExtraWidth; + } + extraSpace = 0; + // if there will be horizontal scrollbar, let's enable it + scrollBodyPanel.getElement().getStyle().clearOverflowX(); + } else { + // if there is no need for horizontal scrollbar, let's disable + // it + // this is necessary since sometimes the browsers insist showing + // the scrollbar even if the content would fit perfectly + scrollBodyPanel.getElement().getStyle() + .setOverflowX(Overflow.HIDDEN); + } + + if (forceScrollBodyWidth > 0) { + scrollBody.container.getStyle().setWidth(forceScrollBodyWidth, + Unit.PX); + } else { + // Clear width that might have been set to force horizontal + // scrolling if there are no rows + scrollBody.container.getStyle().clearWidth(); + } + + int totalUndefinedNaturalWidths = usedMinimumWidth + - totalExplicitColumnsWidths; + + if (hierarchyHeaderInNeedOfFurtherHandling != null + && !hierarchyHeaderInNeedOfFurtherHandling + .isDefinedWidth()) { + // ensure the cell gets enough space for the indent + int w = hierarchyHeaderInNeedOfFurtherHandling + .getNaturalColumnWidth(hierarchyColumnIndex); + int newSpace = Math.round(w + (float) extraSpace * (float) w + / totalUndefinedNaturalWidths); + if (newSpace >= hierarchyColumnIndent) { + // no special handling required + hierarchyHeaderInNeedOfFurtherHandling = null; + } else { + // treat as a defined width column of indent's width + totalExplicitColumnsWidths += hierarchyColumnIndent; + usedMinimumWidth -= w - hierarchyColumnIndent; + totalUndefinedNaturalWidths = usedMinimumWidth + - totalExplicitColumnsWidths; + expandRatioDivider += hierarchyHeaderInNeedOfFurtherHandling + .getExpandRatio(); + extraSpace = Math.max(availW - usedMinimumWidth, 0); + } + } + + // 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.isResizing) { + continue; + } + if (!hCell.isDefinedWidth()) { + int w = hCell.getNaturalColumnWidth(colIndex); + int newSpace; + if (expandRatioDivider > 0) { + // divide excess space by expand ratios + if (hCell.getExpandRatio() > 0) { + w = 0; + } + newSpace = Math.round((w + extraSpace + * hCell.getExpandRatio() / expandRatioDivider)); + } else { + if (hierarchyHeaderInNeedOfFurtherHandling == hCell) { + // still exists, so needs exactly the indent's width + newSpace = hierarchyColumnIndent; + } 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 { + if (hierarchyHeaderInNeedOfFurtherHandling == hCell) { + // defined with enforced into indent width + checksum += hierarchyColumnIndent; + setColWidth(colIndex, hierarchyColumnIndent, false); + } else { + int cellWidth = hCell.getWidthWithIndent(); + checksum += cellWidth; + if (hCell.isHierarchyColumn()) { + // update in case the indent has changed + // (not detectable earlier) + setColWidth(colIndex, cellWidth, true); + } + } + } + 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.isResizing && !hc.isDefinedWidth()) { + setColWidth(colIndex, + hc.getWidthWithIndent() + 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 += WidgetUtil.getNativeScrollbarSize(); + } + int heightBefore = getOffsetHeight(); + scrollBodyPanel.setHeight(bodyHeight + "px"); + + if (heightBefore != getOffsetHeight()) { + Util.notifyParentOfSizeChange(VScrollTable.this, rendering); + } + } + + 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 = WidgetUtil.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()) { + + /* + * Android 2.3 cannot measure the height of the inline-block + * properly, and will return the wrong offset height. So for android + * 2.3 we set the element to a block element while measuring and + * then restore it which yields the correct result. #11331 + */ + if (BrowserInfo.get().isAndroid23()) { + getElement().getStyle().setDisplay(Display.BLOCK); + } + + containerHeight = getOffsetHeight(); + containerHeight -= showColHeaders ? tHead.getOffsetHeight() : 0; + containerHeight -= tFoot.getOffsetHeight(); + containerHeight -= getContentAreaBorderHeight(); + if (containerHeight < 0) { + containerHeight = 0; + } + + scrollBodyPanel.setHeight(containerHeight + "px"); + + if (BrowserInfo.get().isAndroid23()) { + getElement().getStyle().clearDisplay(); + } + } + } + + private int contentAreaBorderHeight = -1; + private int scrollLeft; + private int scrollTop; + + /** For internal use only. May be removed or replaced in the future. */ + public VScrollTableDropHandler dropHandler; + + private boolean navKeyDown; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean multiselectPending; + + /** For internal use only. May be removed or replaced in the future. */ + public CollapseMenuContent collapsibleMenuContent; + + /** + * @return border top + border bottom of the scrollable area of table + */ + private int getContentAreaBorderHeight() { + if (contentAreaBorderHeight < 0) { + + scrollBodyPanel.getElement().getStyle() + .setOverflow(Overflow.HIDDEN); + int oh = scrollBodyPanel.getOffsetHeight(); + int ch = scrollBodyPanel.getElement() + .getPropertyInt("clientHeight"); + contentAreaBorderHeight = oh - ch; + scrollBodyPanel.getElement().getStyle().setOverflow(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); + } + + /** For internal use only. May be removed or replaced in the future. */ + public void updateHeight() { + setContainerHeight(); + + if (initializedAndAttached) { + updatePageLength(); + } + + triggerLazyColumnAdjustment(false); + + /* + * 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() { + + @Override + 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")) { + Icon icon = client.getIcon(uidl.getStringAttribute("icon")); + icon.setAlternateText("icon"); + s = icon.getElement().getString() + s; + } + return s; + } + + // Updates first visible row for the case we cannot wait + // for onScroll + private void updateFirstVisibleRow() { + scrollTop = scrollBodyPanel.getScrollPosition(); + firstRowInViewPort = calcFirstRowInViewPort(); + int maxFirstRow = totalRows - pageLength; + if (firstRowInViewPort > maxFirstRow && maxFirstRow >= 0) { + firstRowInViewPort = maxFirstRow; + } + lastRequestedFirstvisible = firstRowInViewPort; + client.updateVariable(paintableId, "firstvisible", firstRowInViewPort, + false); + } + + /** + * This method has logic which rows needs to be requested from server when + * user scrolls + */ + + @Override + public void onScroll(ScrollEvent event) { + // Do not handle scroll events while there is scroll initiated from + // server side which is not yet executed (#11454) + if (isLazyScrollerActive()) { + return; + } + + 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() { + + @Override + public void execute() { + onScroll(null); + } + }); + return; + } + + // fix headers horizontal scrolling + tHead.setHorizontalScrollPosition(scrollLeft); + + // fix footers horizontal scrolling + tFoot.setHorizontalScrollPosition(scrollLeft); + + if (totalRows == 0) { + // No rows, no need to fetch new rows + return; + } + + firstRowInViewPort = calcFirstRowInViewPort(); + int maxFirstRow = totalRows - pageLength; + if (firstRowInViewPort > maxFirstRow && maxFirstRow >= 0) { + firstRowInViewPort = maxFirstRow; + } + + 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 (allRenderedRowsAreNew()) { + // 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); + updatedReqRows = false; + 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 + int reqRows = (int) ((firstRowInViewPort + pageLength + + pageLength * cache_rate) - lastRendered); + rowRequestHandler.triggerRowFetch(lastRendered + 1, reqRows); + } + } + + private boolean allRenderedRowsAreNew() { + int firstRowInViewPort = calcFirstRowInViewPort(); + int firstRendered = scrollBody.getFirstRendered(); + int lastRendered = scrollBody.getLastRendered(); + return (firstRowInViewPort - pageLength * cache_rate > lastRendered + || firstRowInViewPort + pageLength + + pageLength * cache_rate < firstRendered); + } + + protected int calcFirstRowInViewPort() { + return (int) Math.ceil(scrollTop / scrollBody.getRowHeight()); + } + + @Override + 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; + } + + // + // 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(); + + Class<? extends Widget> clazz = getRowClass(); + VScrollTableRow row = null; + if (clazz != null) { + row = WidgetUtil.findWidget(elementOver, clazz); + } + if (row != null) { + dropDetails.overkey = row.rowKey; + Element tr = row.getElement(); + Element element = elementOver; + while (element != null && element.getParentElement() != tr) { + element = element.getParentElement(); + } + int childIndex = DOM.getChildIndex(tr, element); + dropDetails.colkey = tHead.getHeaderCell(childIndex) + .getColKey(); + dropDetails.dropLocation = DDUtil.getVerticalDropLocation( + row.getElement(), drag.getCurrentGwtEvent(), 0.2); + } + + drag.getDropDetails().put("itemIdOver", dropDetails.overkey + ""); + drag.getDropDetails().put("detail", dropDetails.dropLocation != null + ? dropDetails.dropLocation.toString() : null); + + } + + private Class<? extends Widget> getRowClass() { + // get the row type this way to make dd work in derived + // implementations + Iterator<Widget> iterator = scrollBody.iterator(); + if (iterator.hasNext()) { + return iterator.next().getClass(); + } else { + return null; + } + } + + @Override + public void dragOver(VDragEvent drag) { + TableDDDetails oldDetails = dropDetails; + updateDropDetails(drag); + if (!oldDetails.equals(dropDetails)) { + deEmphasis(); + final TableDDDetails newDetails = dropDetails; + VAcceptCallback cb = new VAcceptCallback() { + + @Override + 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(), getStylePrimaryName() + "-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(), getStylePrimaryName() + "-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); + } + + @Override + 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(getStylePrimaryName() + "-focus"); + } + + if (row != null) { + // Apply focus style to new selection + row.addStyleName(getStylePrimaryName() + "-focus"); + + /* + * Trying to set focus on already focused row + */ + if (row == focusedRow) { + return false; + } + + // Set new focused row + focusedRow = row; + + if (hasFocus()) { + 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; + } + /* + * FIXME The next line doesn't always do what expected, because if the + * row is not in the DOM it won't scroll to it. + */ + WidgetUtil.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); + updateFirstVisibleAndSendSelectedRows(); + } 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 ? + updateFirstVisibleAndSendSelectedRows(); + } else { + // scroll down by pixels and return, to wait for + // new rows, then select the last item in the + // viewport + selectLastItemInNextRender = true; + multiselectPending = shift; + scrollByPagelength(1); + } + } + } + } else { + /* No selections, go page down by scrolling */ + scrollByPagelength(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); + updateFirstVisibleAndSendSelectedRows(); + } 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 ? + updateFirstVisibleAndSendSelectedRows(); + } 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; + scrollByPagelength(-1); + } + } + } + } else { + /* No selections, go page up by scrolling */ + scrollByPagelength(-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); + updateFirstVisibleAndSendSelectedRows(); + } 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); + updateFirstVisibleAndSendSelectedRows(); + } + } 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 scrollByPagelength(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) + */ + + @Override + 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) + */ + + @Override + public void onBlur(BlurEvent event) { + onBlur(); + } + + private void onBlur() { + hasFocus = false; + navKeyDown = false; + + if (BrowserInfo.get().isIE()) { + /* + * IE sometimes moves focus to a clicked table cell... (#7965) + * ...and sometimes it sends blur events even though the focus + * handler is still active. (#10464) + */ + Element focusedElement = WidgetUtil.getFocusedElement(); + if (focusedElement != null + && focusedElement != scrollBodyPanel.getFocusElement() + && Util.getConnectorForElement(client, getParent(), + focusedElement) == ConnectorMap.get(client) + .getConnector(this)) { + /* + * Steal focus back to the focus handler if it was moved to some + * other part of the table. Avoid stealing focus in other cases. + */ + focus(); + return; + } + } + + if (isFocusable()) { + // Unfocus any row + setRowFocus(null); + } + } + + /** + * Removes a key from a range if the key is found in a selected range + * + * @param key + * The key to remove + */ + private void removeRowFromUnsentSelectionRanges(VScrollTableRow row) { + Collection<SelectionRange> newRanges = null; + for (Iterator<SelectionRange> iterator = selectedRowRanges + .iterator(); iterator.hasNext();) { + SelectionRange range = iterator.next(); + if (range.inRange(row)) { + // Split the range if given row is in range + Collection<SelectionRange> splitranges = range.split(row); + if (newRanges == null) { + newRanges = new ArrayList<SelectionRange>(); + } + newRanges.addAll(splitranges); + iterator.remove(); + } + } + if (newRanges != null) { + selectedRowRanges.addAll(newRanges); + } + } + + /** + * Can the Table be focused? + * + * @return True if the table can be focused, else false + */ + public boolean isFocusable() { + if (scrollBody != null && enabled) { + return !(!hasHorizontalScrollbar() && !hasVerticalScrollbar() + && !isSelectable()); + } + return false; + } + + private boolean hasHorizontalScrollbar() { + return scrollBody.getOffsetWidth() > scrollBodyPanel.getOffsetWidth(); + } + + private boolean hasVerticalScrollbar() { + return scrollBody.getOffsetHeight() > scrollBodyPanel.getOffsetHeight(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.Focusable#focus() + */ + + @Override + public void focus() { + if (isFocusable()) { + scrollBodyPanel.focus(); + } + } + + /** + * Sets the proper tabIndex for scrollBodyPanel (the focusable elemen in the + * component). + * <p> + * 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. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public 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() { + @Override + public void execute() { + if (currentlyFocusedRow != null) { + setRowFocus(currentlyFocusedRow); + } else { + VConsole.log("no row?"); + focusRowFromBody(); + } + scrollBody.ensureFocus(); + } + }); + } + + @Override + 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; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + 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; + } + + private static final String SUBPART_HEADER = "header"; + private static final String SUBPART_FOOTER = "footer"; + private static final String SUBPART_ROW = "row"; + private static final String SUBPART_COL = "col"; + /** + * Matches header[ix] - used for extracting the index of the targeted header + * cell + */ + private static final RegExp SUBPART_HEADER_REGEXP = RegExp + .compile(SUBPART_HEADER + "\\[(\\d+)\\]"); + /** + * Matches footer[ix] - used for extracting the index of the targeted footer + * cell + */ + private static final RegExp SUBPART_FOOTER_REGEXP = RegExp + .compile(SUBPART_FOOTER + "\\[(\\d+)\\]"); + /** Matches row[ix] - used for extracting the index of the targeted row */ + private static final RegExp SUBPART_ROW_REGEXP = RegExp + .compile(SUBPART_ROW + "\\[(\\d+)]"); + /** + * Matches col[ix] - used for extracting the index of the targeted column + */ + private static final RegExp SUBPART_ROW_COL_REGEXP = RegExp.compile( + SUBPART_ROW + "\\[(\\d+)\\]/" + SUBPART_COL + "\\[(\\d+)\\]"); + + @Override + public com.google.gwt.user.client.Element getSubPartElement( + String subPart) { + if (SUBPART_ROW_COL_REGEXP.test(subPart)) { + MatchResult result = SUBPART_ROW_COL_REGEXP.exec(subPart); + int rowIx = Integer.valueOf(result.getGroup(1)); + int colIx = Integer.valueOf(result.getGroup(2)); + VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx); + if (row != null) { + Element rowElement = row.getElement(); + if (colIx < rowElement.getChildCount()) { + return rowElement.getChild(colIx).getFirstChild().cast(); + } + } + + } else if (SUBPART_ROW_REGEXP.test(subPart)) { + MatchResult result = SUBPART_ROW_REGEXP.exec(subPart); + int rowIx = Integer.valueOf(result.getGroup(1)); + VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx); + if (row != null) { + return row.getElement(); + } + + } else if (SUBPART_HEADER_REGEXP.test(subPart)) { + MatchResult result = SUBPART_HEADER_REGEXP.exec(subPart); + int headerIx = Integer.valueOf(result.getGroup(1)); + HeaderCell headerCell = tHead.getHeaderCell(headerIx); + if (headerCell != null) { + return headerCell.getElement(); + } + + } else if (SUBPART_FOOTER_REGEXP.test(subPart)) { + MatchResult result = SUBPART_FOOTER_REGEXP.exec(subPart); + int footerIx = Integer.valueOf(result.getGroup(1)); + FooterCell footerCell = tFoot.getFooterCell(footerIx); + if (footerCell != null) { + return footerCell.getElement(); + } + } + // Nothing found. + return null; + } + + @Override + public String getSubPartName( + com.google.gwt.user.client.Element subElement) { + Widget widget = WidgetUtil.findWidget(subElement, null); + if (widget instanceof HeaderCell) { + return SUBPART_HEADER + "[" + tHead.visibleCells.indexOf(widget) + + "]"; + } else if (widget instanceof FooterCell) { + return SUBPART_FOOTER + "[" + tFoot.visibleCells.indexOf(widget) + + "]"; + } else if (widget instanceof VScrollTableRow) { + // a cell in a row + VScrollTableRow row = (VScrollTableRow) widget; + int rowIx = scrollBody.indexOf(row); + if (rowIx >= 0) { + int colIx = -1; + for (int ix = 0; ix < row.getElement().getChildCount(); ix++) { + if (row.getElement().getChild(ix) + .isOrHasChild(subElement)) { + colIx = ix; + break; + } + } + if (colIx >= 0) { + return SUBPART_ROW + "[" + rowIx + "]/" + SUBPART_COL + "[" + + colIx + "]"; + } + return SUBPART_ROW + "[" + rowIx + "]"; + } + } + // Nothing found. + return null; + } + + /** + * @since 7.2.6 + */ + public void onUnregister() { + if (addCloseHandler != null) { + addCloseHandler.removeHandler(); + } + } + + /* + * Return true if component need to perform some work and false otherwise. + */ + @Override + public boolean isWorkPending() { + return lazyAdjustColumnWidths.isRunning(); + } + + private static Logger getLogger() { + return Logger.getLogger(VScrollTable.class.getName()); + } + + public ChildMeasurementHint getChildMeasurementHint() { + return childMeasurementHint; + } + + public void setChildMeasurementHint(ChildMeasurementHint hint) { + childMeasurementHint = hint; + } + + private boolean hasFocus() { + if (hasFocus && BrowserInfo.get().isIE()) { + com.google.gwt.user.client.Element focusedElement = Util + .getIEFocusedElement(); + if (!getElement().isOrHasChild(focusedElement)) { + // Does not really have focus but a blur event has been lost + getLogger().warning( + "IE did not send a blur event, firing manually"); + onBlur(); + } + } + return hasFocus; + } + +} + diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/VTreeTable.java b/compatibility-client/src/main/java/com/vaadin/client/ui/VTreeTable.java new file mode 100644 index 0000000000..25a9d4e29b --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/VTreeTable.java @@ -0,0 +1,907 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.google.gwt.animation.client.Animation; +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.Element; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.dom.client.Style.Display; +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.event.dom.client.KeyCodes; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ComputedStyle; +import com.vaadin.client.UIDL; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.VTreeTable.VTreeTableScrollBody.VTreeTableRow; + +public class VTreeTable extends VScrollTable { + + /** For internal use only. May be removed or replaced in the future. */ + public static class PendingNavigationEvent { + public final int keycode; + public final boolean ctrl; + public final boolean shift; + + public PendingNavigationEvent(int keycode, boolean ctrl, + boolean shift) { + this.keycode = keycode; + this.ctrl = ctrl; + this.shift = shift; + } + + @Override + public String toString() { + String string = "Keyboard event: " + keycode; + if (ctrl) { + string += " + ctrl"; + } + if (shift) { + string += " + shift"; + } + return string; + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public boolean collapseRequest; + + private boolean selectionPending; + + /** For internal use only. May be removed or replaced in the future. */ + public int colIndexOfHierarchy; + + /** For internal use only. May be removed or replaced in the future. */ + public String collapsedRowKey; + + /** For internal use only. May be removed or replaced in the future. */ + public VTreeTableScrollBody scrollBody; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean animationsEnabled; + + /** For internal use only. May be removed or replaced in the future. */ + public LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTable.PendingNavigationEvent>(); + + /** For internal use only. May be removed or replaced in the future. */ + public boolean focusParentResponsePending; + + @Override + protected VScrollTableBody createScrollBody() { + scrollBody = new VTreeTableScrollBody(); + return scrollBody; + } + + /* + * Overridden to allow animation of expands and collapses of nodes. + */ + @Override + public void addAndRemoveRows(UIDL partialRowAdditions) { + if (partialRowAdditions == null) { + return; + } + + if (animationsEnabled) { + if (partialRowAdditions.hasAttribute("hide")) { + scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished( + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + } else { + scrollBody.insertRowsAnimated(partialRowAdditions, + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + discardRowsOutsideCacheWindow(); + } + } else { + super.addAndRemoveRows(partialRowAdditions); + } + } + + @Override + protected int getHierarchyColumnIndex() { + return colIndexOfHierarchy + (showRowHeaders ? 1 : 0); + } + + public class VTreeTableScrollBody extends VScrollTable.VScrollTableBody { + private int indentWidth = -1; + private int maxIndent = 0; + + protected VTreeTableScrollBody() { + super(); + } + + @Override + protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) { + if (uidl.hasAttribute("gen_html")) { + // This is a generated row. + return new VTreeTableGeneratedRow(uidl, aligns2); + } + return new VTreeTableRow(uidl, aligns2); + } + + public class VTreeTableRow + extends VScrollTable.VScrollTableBody.VScrollTableRow { + + private boolean isTreeCellAdded = false; + private SpanElement treeSpacer; + private boolean open; + private int depth; + private boolean canHaveChildren; + protected Widget widgetInHierarchyColumn; + + public VTreeTableRow(UIDL uidl, char[] aligns2) { + super(uidl, aligns2); + // this fix causes #15118 and doesn't work for treetable anyway + applyZeroWidthFix = false; + } + + @Override + public void addCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean isSorted, + String description) { + super.addCell(rowUidl, text, align, style, textIsHTML, isSorted, + description); + + addTreeSpacer(rowUidl); + } + + protected boolean addTreeSpacer(UIDL rowUidl) { + if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) { + Element container = (Element) getElement().getLastChild() + .getFirstChild(); + + if (rowUidl.hasAttribute("icon")) { + Icon icon = client + .getIcon(rowUidl.getStringAttribute("icon")); + icon.setAlternateText("icon"); + container.insertFirst(icon.getElement()); + } + + String classname = "v-treetable-treespacer"; + if (rowUidl.getBooleanAttribute("ca")) { + canHaveChildren = true; + open = rowUidl.getBooleanAttribute("open"); + classname += open ? " v-treetable-node-open" + : " v-treetable-node-closed"; + } + + treeSpacer = Document.get().createSpanElement(); + + treeSpacer.setClassName(classname); + container.insertFirst(treeSpacer); + depth = rowUidl.hasAttribute("depth") + ? rowUidl.getIntAttribute("depth") : 0; + setIndent(); + isTreeCellAdded = true; + return true; + } + return false; + } + + private boolean cellShowsTreeHierarchy(int curColIndex) { + if (isTreeCellAdded) { + return false; + } + return curColIndex == getHierarchyColumnIndex(); + } + + @Override + public void onBrowserEvent(Event event) { + if (event.getEventTarget().cast() == treeSpacer + && treeSpacer.getClassName().contains("node")) { + if (event.getTypeInt() == Event.ONMOUSEUP) { + sendToggleCollapsedUpdate(getKey()); + } + return; + } + super.onBrowserEvent(event); + } + + @Override + public void addCell(UIDL rowUidl, Widget w, char align, + String style, boolean isSorted, String description) { + super.addCell(rowUidl, w, align, style, isSorted, description); + if (addTreeSpacer(rowUidl)) { + widgetInHierarchyColumn = w; + } + + } + + private void setIndent() { + if (getIndentWidth() > 0) { + treeSpacer.getParentElement().getStyle() + .setPaddingLeft(getIndent(), Unit.PX); + treeSpacer.getStyle().setWidth(getIndent(), Unit.PX); + int colWidth = getColWidth(getHierarchyColumnIndex()); + if (colWidth > 0 && getIndent() > colWidth) { + VTreeTable.this.setColWidth(getHierarchyColumnIndex(), + getIndent(), false); + } + } + } + + @Override + protected void onAttach() { + super.onAttach(); + if (getIndentWidth() < 0) { + detectIndent(this); + // If we detect indent here then the size of the hierarchy + // column is still wrong as it has been set when the indent + // was not known. + int w = getCellWidthFromDom(getHierarchyColumnIndex()); + if (w >= 0) { + setColWidth(getHierarchyColumnIndex(), w); + } + } + } + + private int getCellWidthFromDom(int cellIndex) { + final Element cell = DOM.getChild(getElement(), cellIndex); + String w = cell.getStyle().getProperty("width"); + if (w == null || "".equals(w) || !w.endsWith("px")) { + return -1; + } else { + return Integer.parseInt(w.substring(0, w.length() - 2)); + } + } + + private int getHierarchyAndIconWidth() { + int consumedSpace = treeSpacer.getOffsetWidth(); + if (treeSpacer.getParentElement().getChildCount() > 2) { + // icon next to tree spacer + consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer + .getNextSibling()).getOffsetWidth(); + } + return consumedSpace; + } + + @Override + protected void setCellWidth(int cellIx, int width) { + if (cellIx == getHierarchyColumnIndex()) { + // take indentation padding into account if this is the + // hierarchy column + int indent = getIndent(); + if (indent != -1) { + width = Math.max(width - indent, 0); + } + } + super.setCellWidth(cellIx, width); + } + + private int getIndent() { + return (depth + 1) * getIndentWidth(); + } + } + + protected class VTreeTableGeneratedRow extends VTreeTableRow { + private boolean spanColumns; + private boolean htmlContentAllowed; + + public VTreeTableGeneratedRow(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() { + + @Override + public void execute() { + if (showRowHeaders) { + setCellWidth(0, tHead.getHeaderCell(0) + .getWidthWithIndent()); + 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); + td.getStyle().setHeight(getRowHeight(), Unit.PX); + if (addTreeSpacer(rowUidl)) { + widgetInHierarchyColumn = w; + } + } + + 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); + td.getStyle().setHeight(getRowHeight(), Unit.PX); + addTreeSpacer(rowUidl); + } + + @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(); + } + WidgetUtil.setWidthExcludingPaddingAndBorder( + (Element) getElement().getChild(cellIx), spanWidth, 13, + false); + } + } + + private int getIndentWidth() { + return indentWidth; + } + + @Override + protected int getMaxIndent() { + return maxIndent; + } + + @Override + protected void calculateMaxIndent() { + int maxIndent = 0; + Iterator<Widget> iterator = iterator(); + while (iterator.hasNext()) { + VTreeTableRow next = (VTreeTableRow) iterator.next(); + maxIndent = Math.max(maxIndent, next.getIndent()); + } + this.maxIndent = maxIndent; + } + + private void detectIndent(VTreeTableRow vTreeTableRow) { + indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth(); + if (indentWidth == 0) { + indentWidth = -1; + return; + } + Iterator<Widget> iterator = iterator(); + while (iterator.hasNext()) { + VTreeTableRow next = (VTreeTableRow) iterator.next(); + next.setIndent(); + } + calculateMaxIndent(); + } + + protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished( + final int firstIndex, final int rows) { + List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>(); + for (int ix = firstIndex; ix < firstIndex + rows; ix++) { + VScrollTableRow row = getRowByRowIndex(ix); + if (row != null) { + rowsToDelete.add(row); + } + } + if (!rowsToDelete.isEmpty()) { + // #8810 Only animate if there's something to animate + RowCollapseAnimation anim = new RowCollapseAnimation( + rowsToDelete) { + @Override + protected void onComplete() { + super.onComplete(); + // Actually unlink the rows and update the cache after + // the + // animation is done. + unlinkAndReindexRows(firstIndex, rows); + discardRowsOutsideCacheWindow(); + ensureCacheFilled(); + } + }; + anim.run(150); + } + } + + protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData, + int firstIndex, int rows) { + List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData, + firstIndex, rows); + if (!insertedRows.isEmpty()) { + // Only animate if there's something to animate (#8810) + RowExpandAnimation anim = new RowExpandAnimation(insertedRows); + anim.run(150); + } + scrollBody.calculateMaxIndent(); + return insertedRows; + } + + /** + * Prepares the table for animation by copying the background colors of + * all TR elements to their respective TD elements if the TD element is + * transparent. This is needed, since if TDs have transparent + * backgrounds, the rows sliding behind them are visible. + */ + private class AnimationPreparator { + private final int lastItemIx; + + public AnimationPreparator(int lastItemIx) { + this.lastItemIx = lastItemIx; + } + + public void prepareTableForAnimation() { + int ix = lastItemIx; + VScrollTableRow row = null; + while ((row = getRowByRowIndex(ix)) != null) { + copyTRBackgroundsToTDs(row); + --ix; + } + } + + private void copyTRBackgroundsToTDs(VScrollTableRow row) { + Element tr = row.getElement(); + ComputedStyle cs = new ComputedStyle(tr); + String backgroundAttachment = cs + .getProperty("backgroundAttachment"); + String backgroundClip = cs.getProperty("backgroundClip"); + String backgroundColor = cs.getProperty("backgroundColor"); + String backgroundImage = cs.getProperty("backgroundImage"); + String backgroundOrigin = cs.getProperty("backgroundOrigin"); + for (int ix = 0; ix < tr.getChildCount(); ix++) { + Element td = tr.getChild(ix).cast(); + if (!elementHasBackground(td)) { + td.getStyle().setProperty("backgroundAttachment", + backgroundAttachment); + td.getStyle().setProperty("backgroundClip", + backgroundClip); + td.getStyle().setProperty("backgroundColor", + backgroundColor); + td.getStyle().setProperty("backgroundImage", + backgroundImage); + td.getStyle().setProperty("backgroundOrigin", + backgroundOrigin); + } + } + } + + private boolean elementHasBackground(Element element) { + ComputedStyle cs = new ComputedStyle(element); + String clr = cs.getProperty("backgroundColor"); + String img = cs.getProperty("backgroundImage"); + return !("rgba(0, 0, 0, 0)".equals(clr.trim()) + || "transparent".equals(clr.trim()) || img == null); + } + + public void restoreTableAfterAnimation() { + int ix = lastItemIx; + VScrollTableRow row = null; + while ((row = getRowByRowIndex(ix)) != null) { + restoreStyleForTDsInRow(row); + + --ix; + } + } + + private void restoreStyleForTDsInRow(VScrollTableRow row) { + Element tr = row.getElement(); + for (int ix = 0; ix < tr.getChildCount(); ix++) { + Element td = tr.getChild(ix).cast(); + td.getStyle().clearProperty("backgroundAttachment"); + td.getStyle().clearProperty("backgroundClip"); + td.getStyle().clearProperty("backgroundColor"); + td.getStyle().clearProperty("backgroundImage"); + td.getStyle().clearProperty("backgroundOrigin"); + } + } + } + + /** + * Animates row expansion using the GWT animation framework. + * + * The idea is as follows: + * + * 1. Insert all rows normally + * + * 2. Insert a newly created DIV containing a new TABLE element below + * the DIV containing the actual scroll table body. + * + * 3. Clone the rows that were inserted in step 1 and attach the clones + * to the new TABLE element created in step 2. + * + * 4. The new DIV from step 2 is absolutely positioned so that the last + * inserted row is just behind the row that was expanded. + * + * 5. Hide the contents of the originally inserted rows by setting the + * DIV.v-table-cell-wrapper to display:none;. + * + * 6. Set the height of the originally inserted rows to 0. + * + * 7. The animation loop slides the DIV from step 2 downwards, while at + * the same pace growing the height of each of the inserted rows from 0 + * to full height. The first inserted row grows from 0 to full and after + * this the second row grows from 0 to full, etc until all rows are full + * height. + * + * 8. Remove the DIV from step 2 + * + * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements. + * + * 10. DONE + */ + private class RowExpandAnimation extends Animation { + + private final List<VScrollTableRow> rows; + private Element cloneDiv; + private Element cloneTable; + private AnimationPreparator preparator; + + /** + * @param rows + * List of rows to animate. Must not be empty. + */ + public RowExpandAnimation(List<VScrollTableRow> rows) { + this.rows = rows; + buildAndInsertAnimatingDiv(); + preparator = new AnimationPreparator( + rows.get(0).getIndex() - 1); + preparator.prepareTableForAnimation(); + for (VScrollTableRow row : rows) { + cloneAndAppendRow(row); + row.addStyleName("v-table-row-animating"); + setCellWrapperDivsToDisplayNone(row); + row.setHeight(getInitialHeight()); + } + } + + protected String getInitialHeight() { + return "0px"; + } + + private void cloneAndAppendRow(VScrollTableRow row) { + Element clonedTR = null; + clonedTR = row.getElement().cloneNode(true).cast(); + clonedTR.getStyle().setVisibility(Visibility.VISIBLE); + cloneTable.appendChild(clonedTR); + } + + protected double getBaseOffset() { + return rows.get(0).getAbsoluteTop() + - rows.get(0).getParent().getAbsoluteTop() + - rows.size() * getRowHeight(); + } + + private void buildAndInsertAnimatingDiv() { + cloneDiv = DOM.createDiv(); + cloneDiv.addClassName("v-treetable-animation-clone-wrapper"); + cloneTable = DOM.createTable(); + cloneTable.addClassName("v-treetable-animation-clone"); + cloneDiv.appendChild(cloneTable); + insertAnimatingDiv(); + } + + private void insertAnimatingDiv() { + Element tableBody = getElement(); + Element tableBodyParent = tableBody.getParentElement(); + tableBodyParent.insertAfter(cloneDiv, tableBody); + } + + @Override + protected void onUpdate(double progress) { + animateDiv(progress); + animateRowHeights(progress); + } + + private void animateDiv(double progress) { + double offset = calculateDivOffset(progress, getRowHeight()); + + cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX); + } + + private void animateRowHeights(double progress) { + double rh = getRowHeight(); + double vlh = calculateHeightOfAllVisibleLines(progress, rh); + int ix = 0; + + while (ix < rows.size()) { + double height = vlh < rh ? vlh : rh; + rows.get(ix).setHeight(height + "px"); + vlh -= height; + ix++; + } + } + + protected double calculateHeightOfAllVisibleLines(double progress, + double rh) { + return rows.size() * rh * progress; + } + + protected double calculateDivOffset(double progress, double rh) { + return progress * rows.size() * rh; + } + + @Override + protected void onComplete() { + preparator.restoreTableAfterAnimation(); + for (VScrollTableRow row : rows) { + resetCellWrapperDivsDisplayProperty(row); + row.removeStyleName("v-table-row-animating"); + } + Element tableBodyParent = getElement().getParentElement(); + tableBodyParent.removeChild(cloneDiv); + } + + private void setCellWrapperDivsToDisplayNone(VScrollTableRow row) { + Element tr = row.getElement(); + for (int ix = 0; ix < tr.getChildCount(); ix++) { + getWrapperDiv(tr, ix).getStyle().setDisplay(Display.NONE); + } + } + + private Element getWrapperDiv(Element tr, int tdIx) { + Element td = tr.getChild(tdIx).cast(); + return td.getChild(0).cast(); + } + + private void resetCellWrapperDivsDisplayProperty( + VScrollTableRow row) { + Element tr = row.getElement(); + for (int ix = 0; ix < tr.getChildCount(); ix++) { + getWrapperDiv(tr, ix).getStyle().clearProperty("display"); + } + } + + } + + /** + * This is the inverse of the RowExpandAnimation and is implemented by + * extending it and overriding the calculation of offsets and heights. + */ + private class RowCollapseAnimation extends RowExpandAnimation { + + private final List<VScrollTableRow> rows; + + /** + * @param rows + * List of rows to animate. Must not be empty. + */ + public RowCollapseAnimation(List<VScrollTableRow> rows) { + super(rows); + this.rows = rows; + } + + @Override + protected String getInitialHeight() { + return getRowHeight() + "px"; + } + + @Override + protected double getBaseOffset() { + return getRowHeight(); + } + + @Override + protected double calculateHeightOfAllVisibleLines(double progress, + double rh) { + return rows.size() * rh * (1 - progress); + } + + @Override + protected double calculateDivOffset(double progress, double rh) { + return -super.calculateDivOffset(progress, rh); + } + } + } + + /** + * Icons rendered into first actual column in TreeTable, not to row header + * cell + */ + @Override + protected String buildCaptionHtmlSnippet(UIDL uidl) { + if (uidl.getTag().equals("column")) { + return super.buildCaptionHtmlSnippet(uidl); + } else { + String s = uidl.getStringAttribute("caption"); + return s; + } + } + + /** For internal use only. May be removed or replaced in the future. */ + @Override + public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + if (collapseRequest || focusParentResponsePending) { + // Enqueue the event if there might be pending content changes from + // the server + if (pendingNavigationEvents.size() < 10) { + // Only keep 10 keyboard events in the queue + PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent( + keycode, ctrl, shift); + pendingNavigationEvents.add(pendingNavigationEvent); + } + return true; + } + + VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow(); + if (focusedRow != null) { + if (focusedRow.canHaveChildren && ((keycode == KeyCodes.KEY_RIGHT + && !focusedRow.open) + || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) { + if (!ctrl) { + client.updateVariable(paintableId, "selectCollapsed", true, + false); + } + sendSelectedRows(false); + sendToggleCollapsedUpdate(focusedRow.getKey()); + return true; + } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) { + // already expanded, move selection down if next is on a deeper + // level (is-a-child) + VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow + .getParent(); + Iterator<Widget> iterator = body.iterator(); + VTreeTableRow next = null; + while (iterator.hasNext()) { + next = (VTreeTableRow) iterator.next(); + if (next == focusedRow) { + next = (VTreeTableRow) iterator.next(); + break; + } + } + if (next != null) { + if (next.depth > focusedRow.depth) { + selectionPending = true; + return super.handleNavigation(getNavigationDownKey(), + ctrl, shift); + } + } else { + // Note, a minor change here for a bit false behavior if + // cache rows is disabled + last visible row + no childs for + // the node + selectionPending = true; + return super.handleNavigation(getNavigationDownKey(), ctrl, + shift); + } + } else if (keycode == KeyCodes.KEY_LEFT) { + // already collapsed move selection up to parent node + // do on the server side as the parent is not necessary + // rendered on the client, could check if parent is visible if + // a performance issue arises + + client.updateVariable(paintableId, "focusParent", + focusedRow.getKey(), true); + + // Set flag that we should enqueue navigation events until we + // get a response to this request + focusParentResponsePending = true; + + return true; + } + } + return super.handleNavigation(keycode, ctrl, shift); + } + + private void sendToggleCollapsedUpdate(String rowKey) { + collapsedRowKey = rowKey; + collapseRequest = true; + client.updateVariable(paintableId, "toggleCollapsed", rowKey, true); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONKEYUP && selectionPending) { + sendSelectedRows(); + } + } + + @Override + protected void sendSelectedRows(boolean immediately) { + super.sendSelectedRows(immediately); + selectionPending = false; + } + + @Override + protected void reOrderColumn(String columnKey, int newIndex) { + super.reOrderColumn(columnKey, newIndex); + // current impl not intelligent enough to survive without visiting the + // server to redraw content + client.sendPendingVariableChanges(); + } + + @Override + public void setStyleName(String style) { + super.setStyleName(style + " v-treetable"); + } + + @Override + public void updateTotalRows(UIDL uidl) { + // Make sure that initializedAndAttached & al are not reset when the + // totalrows are updated on expand/collapse requests. + int newTotalRows = uidl.getIntAttribute("totalrows"); + setTotalRows(newTotalRows); + } + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/table/TableConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/table/TableConnector.java new file mode 100644 index 0000000000..ccfb715291 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/table/TableConnector.java @@ -0,0 +1,543 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.table; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.EventTarget; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorHierarchyChangeEvent; +import com.vaadin.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler; +import com.vaadin.client.DirectionalManagedLayout; +import com.vaadin.client.HasChildMeasurementHintConnector; +import com.vaadin.client.HasComponentsConnector; +import com.vaadin.client.Paintable; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.TooltipInfo; +import com.vaadin.client.UIDL; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.AbstractFieldConnector; +import com.vaadin.client.ui.PostLayoutListener; +import com.vaadin.client.ui.VScrollTable; +import com.vaadin.client.ui.VScrollTable.ContextMenuDetails; +import com.vaadin.client.ui.VScrollTable.FooterCell; +import com.vaadin.client.ui.VScrollTable.HeaderCell; +import com.vaadin.client.ui.VScrollTable.VScrollTableBody.VScrollTableRow; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.table.TableConstants; +import com.vaadin.shared.ui.table.TableConstants.Section; +import com.vaadin.shared.ui.table.TableServerRpc; +import com.vaadin.shared.ui.table.TableState; + +@Connect(com.vaadin.ui.Table.class) +public class TableConnector extends AbstractFieldConnector + implements HasComponentsConnector, ConnectorHierarchyChangeHandler, + Paintable, DirectionalManagedLayout, PostLayoutListener, + HasChildMeasurementHintConnector { + + private List<ComponentConnector> childComponents; + + public TableConnector() { + addConnectorHierarchyChangeHandler(this); + } + + @Override + protected void init() { + super.init(); + getWidget().init(getConnection()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.ui.AbstractComponentConnector#onUnregister() + */ + @Override + public void onUnregister() { + super.onUnregister(); + getWidget().onUnregister(); + } + + @Override + protected void sendContextClickEvent(MouseEventDetails details, + EventTarget eventTarget) { + + if (!Element.is(eventTarget)) { + return; + } + Element e = Element.as(eventTarget); + + Section section; + String colKey = null; + String rowKey = null; + if (getWidget().tFoot.getElement().isOrHasChild(e)) { + section = Section.FOOTER; + FooterCell w = WidgetUtil.findWidget(e, FooterCell.class); + colKey = w.getColKey(); + } else if (getWidget().tHead.getElement().isOrHasChild(e)) { + section = Section.HEADER; + HeaderCell w = WidgetUtil.findWidget(e, HeaderCell.class); + colKey = w.getColKey(); + } else { + section = Section.BODY; + if (getWidget().scrollBody.getElement().isOrHasChild(e)) { + VScrollTableRow w = getScrollTableRow(e); + /* + * if w is null because we've clicked on an empty area, we will + * let rowKey and colKey be null too, which will then lead to + * the server side returning a null object. + */ + if (w != null) { + rowKey = w.getKey(); + colKey = getWidget().tHead + .getHeaderCell(getElementIndex(e, w.getElement())) + .getColKey(); + } + } + } + + getRpcProxy(TableServerRpc.class).contextClick(rowKey, colKey, section, + details); + + WidgetUtil.clearTextSelection(); + } + + protected VScrollTableRow getScrollTableRow(Element e) { + return WidgetUtil.findWidget(e, VScrollTableRow.class); + } + + private int getElementIndex(Element e, + com.google.gwt.user.client.Element element) { + int i = 0; + Element current = element.getFirstChildElement(); + while (!current.isOrHasChild(e)) { + current = current.getNextSiblingElement(); + ++i; + } + return i; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.Paintable#updateFromUIDL(com.vaadin.client.UIDL, + * com.vaadin.client.ApplicationConnection) + */ + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + getWidget().rendering = true; + + // If a row has an open context menu, it will be closed as the row is + // detached. Retain a reference here so we can restore the menu if + // required. + ContextMenuDetails contextMenuBeforeUpdate = getWidget().contextMenu; + + if (uidl.hasAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_FIRST)) { + getWidget().serverCacheFirst = uidl + .getIntAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_FIRST); + getWidget().serverCacheLast = uidl + .getIntAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_LAST); + } else { + getWidget().serverCacheFirst = -1; + getWidget().serverCacheLast = -1; + } + /* + * We need to do this before updateComponent since updateComponent calls + * this.setHeight() which will calculate a new body height depending on + * the space available. + */ + if (uidl.hasAttribute("colfooters")) { + getWidget().showColFooters = uidl.getBooleanAttribute("colfooters"); + } + + getWidget().tFoot.setVisible(getWidget().showColFooters); + + if (!isRealUpdate(uidl)) { + getWidget().rendering = false; + return; + } + + getWidget().paintableId = uidl.getStringAttribute("id"); + getWidget().immediate = getState().immediate; + + int previousTotalRows = getWidget().totalRows; + getWidget().updateTotalRows(uidl); + boolean totalRowsHaveChanged = (getWidget().totalRows != previousTotalRows); + + getWidget().updateDragMode(uidl); + + // Update child measure hint + int childMeasureHint = uidl.hasAttribute("measurehint") + ? uidl.getIntAttribute("measurehint") : 0; + getWidget().setChildMeasurementHint( + ChildMeasurementHint.values()[childMeasureHint]); + + getWidget().updateSelectionProperties(uidl, getState(), isReadOnly()); + + if (uidl.hasAttribute("alb")) { + getWidget().bodyActionKeys = uidl.getStringArrayAttribute("alb"); + } else { + // Need to clear the actions if the action handlers have been + // removed + getWidget().bodyActionKeys = null; + } + + getWidget().setCacheRateFromUIDL(uidl); + + getWidget().recalcWidths = uidl.hasAttribute("recalcWidths"); + if (getWidget().recalcWidths) { + getWidget().tHead.clear(); + getWidget().tFoot.clear(); + } + + getWidget().updatePageLength(uidl); + + getWidget().updateFirstVisibleAndScrollIfNeeded(uidl); + + getWidget().showRowHeaders = uidl.getBooleanAttribute("rowheaders"); + getWidget().showColHeaders = uidl.getBooleanAttribute("colheaders"); + + getWidget().updateSortingProperties(uidl); + + getWidget().updateActionMap(uidl); + + getWidget().updateColumnProperties(uidl); + + UIDL ac = uidl.getChildByTagName("-ac"); + if (ac == null) { + if (getWidget().dropHandler != null) { + // remove dropHandler if not present anymore + getWidget().dropHandler = null; + } + } else { + if (getWidget().dropHandler == null) { + getWidget().dropHandler = getWidget().new VScrollTableDropHandler(); + } + getWidget().dropHandler.updateAcceptRules(ac); + } + + UIDL partialRowAdditions = uidl.getChildByTagName("prows"); + UIDL partialRowUpdates = uidl.getChildByTagName("urows"); + if (partialRowUpdates != null || partialRowAdditions != null) { + getWidget().postponeSanityCheckForLastRendered = true; + // we may have pending cache row fetch, cancel it. See #2136 + getWidget().rowRequestHandler.cancel(); + + getWidget().updateRowsInBody(partialRowUpdates); + getWidget().addAndRemoveRows(partialRowAdditions); + + // sanity check (in case the value has slipped beyond the total + // amount of rows) + getWidget().scrollBody + .setLastRendered(getWidget().scrollBody.getLastRendered()); + getWidget().updateMaxIndent(); + } else { + getWidget().postponeSanityCheckForLastRendered = false; + UIDL rowData = uidl.getChildByTagName("rows"); + if (rowData != null) { + // we may have pending cache row fetch, cancel it. See #2136 + getWidget().rowRequestHandler.cancel(); + + if (!getWidget().recalcWidths + && getWidget().initializedAndAttached) { + getWidget().updateBody(rowData, + uidl.getIntAttribute("firstrow"), + uidl.getIntAttribute("rows")); + if (getWidget().headerChangedDuringUpdate) { + getWidget().triggerLazyColumnAdjustment(true); + } + } else { + getWidget().initializeRows(uidl, rowData); + } + } + } + + boolean keyboardSelectionOverRowFetchInProgress = getWidget() + .selectSelectedRows(uidl); + + // If a row had an open context menu before the update, and after the + // update there's a row with the same key as that row, restore the + // context menu. See #8526. + showSavedContextMenu(contextMenuBeforeUpdate); + + if (!getWidget().isSelectable()) { + getWidget().scrollBody.addStyleName( + getWidget().getStylePrimaryName() + "-body-noselection"); + } else { + getWidget().scrollBody.removeStyleName( + getWidget().getStylePrimaryName() + "-body-noselection"); + } + + getWidget().hideScrollPositionAnnotation(); + + // selection is no in sync with server, avoid excessive server visits by + // clearing to flag used during the normal operation + if (!keyboardSelectionOverRowFetchInProgress) { + getWidget().selectionChanged = false; + } + + /* + * This is called when the Home or page up button has been pressed in + * selectable mode and the next selected row was not yet rendered in the + * client + */ + if (getWidget().selectFirstItemInNextRender + || getWidget().focusFirstItemInNextRender) { + getWidget().selectFirstRenderedRowInViewPort( + getWidget().focusFirstItemInNextRender); + getWidget().selectFirstItemInNextRender = getWidget().focusFirstItemInNextRender = false; + } + + /* + * This is called when the page down or end button has been pressed in + * selectable mode and the next selected row was not yet rendered in the + * client + */ + if (getWidget().selectLastItemInNextRender + || getWidget().focusLastItemInNextRender) { + getWidget().selectLastRenderedRowInViewPort( + getWidget().focusLastItemInNextRender); + getWidget().selectLastItemInNextRender = getWidget().focusLastItemInNextRender = false; + } + getWidget().multiselectPending = false; + + if (getWidget().focusedRow != null) { + if (!getWidget().focusedRow.isAttached() + && !getWidget().rowRequestHandler + .isRequestHandlerRunning()) { + // focused row has been orphaned, can't focus + if (getWidget().selectedRowKeys + .contains(getWidget().focusedRow.getKey())) { + // if row cache was refreshed, focused row should be + // in selection and exists with same index + getWidget().setRowFocus(getWidget().getRenderedRowByKey( + getWidget().focusedRow.getKey())); + } else if (getWidget().selectedRowKeys.size() > 0) { + // try to focus any row in selection + getWidget().setRowFocus(getWidget().getRenderedRowByKey( + getWidget().selectedRowKeys.iterator().next())); + } else { + // try to focus any row + getWidget().focusRowFromBody(); + } + } + } + + /* + * If the server has (re)initialized the rows, our selectionRangeStart + * row will point to an index that the server knows nothing about, + * causing problems if doing multi selection with shift. The field will + * be cleared a little later when the row focus has been restored. + * (#8584) + */ + if (uidl.hasAttribute(TableConstants.ATTRIBUTE_KEY_MAPPER_RESET) + && uidl.getBooleanAttribute( + TableConstants.ATTRIBUTE_KEY_MAPPER_RESET) + && getWidget().selectionRangeStart != null) { + assert !getWidget().selectionRangeStart.isAttached(); + getWidget().selectionRangeStart = getWidget().focusedRow; + } + + getWidget().tabIndex = getState().tabIndex; + getWidget().setProperTabIndex(); + + Scheduler.get().scheduleFinally(new ScheduledCommand() { + + @Override + public void execute() { + getWidget().resizeSortedColumnForSortIndicator(); + } + }); + + // Remember this to detect situations where overflow hack might be + // needed during scrolling + getWidget().lastRenderedHeight = getWidget().scrollBody + .getOffsetHeight(); + + getWidget().rendering = false; + getWidget().headerChangedDuringUpdate = false; + + getWidget().collapsibleMenuContent = getState().collapseMenuContent; + } + + @Override + public void updateEnabledState(boolean enabledState) { + super.updateEnabledState(enabledState); + getWidget().enabled = isEnabled(); + } + + @Override + public VScrollTable getWidget() { + return (VScrollTable) super.getWidget(); + } + + @Override + public void updateCaption(ComponentConnector component) { + // NOP, not rendered + } + + @Override + public void layoutVertically() { + getWidget().updateHeight(); + } + + @Override + public void layoutHorizontally() { + getWidget().updateWidth(); + } + + @Override + public void postLayout() { + VScrollTable table = getWidget(); + if (table.sizeNeedsInit) { + table.sizeInit(); + Scheduler.get().scheduleFinally(new ScheduledCommand() { + @Override + public void execute() { + getLayoutManager().setNeedsMeasure(TableConnector.this); + ServerConnector parent = getParent(); + if (parent instanceof ComponentConnector) { + getLayoutManager() + .setNeedsMeasure((ComponentConnector) parent); + } + getLayoutManager() + .setNeedsVerticalLayout(TableConnector.this); + getLayoutManager().layoutNow(); + } + }); + } + } + + @Override + public boolean isReadOnly() { + return super.isReadOnly() || getState().propertyReadOnly; + } + + @Override + public TableState getState() { + return (TableState) super.getState(); + } + + /** + * Shows a saved row context menu if the row for the context menu is still + * visible. Does nothing if a context menu has not been saved. + * + * @param savedContextMenu + */ + public void showSavedContextMenu(ContextMenuDetails savedContextMenu) { + if (isEnabled() && savedContextMenu != null) { + Iterator<Widget> iterator = getWidget().scrollBody.iterator(); + while (iterator.hasNext()) { + Widget w = iterator.next(); + VScrollTableRow row = (VScrollTableRow) w; + if (row.getKey().equals(savedContextMenu.rowKey)) { + row.showContextMenu(savedContextMenu.left, + savedContextMenu.top); + } + } + } + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + + TooltipInfo info = null; + + if (element != getWidget().getElement()) { + Object node = WidgetUtil.findWidget(element, VScrollTableRow.class); + + if (node != null) { + VScrollTableRow row = (VScrollTableRow) node; + info = row.getTooltip(element); + } + } + + if (info == null) { + info = super.getTooltipInfo(element); + } + + return info; + } + + @Override + public boolean hasTooltip() { + /* + * Tooltips for individual rows and cells are not processed until + * updateFromUIDL, so we can't be sure that there are no tooltips during + * onStateChange when this method is used. + */ + return true; + } + + @Override + public void onConnectorHierarchyChange( + ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) { + // TODO Move code from updateFromUIDL to this method + } + + @Override + protected void updateComponentSize(String newWidth, String newHeight) { + super.updateComponentSize(newWidth, newHeight); + + if ("".equals(newWidth)) { + getWidget().updateWidth(); + } + if ("".equals(newHeight)) { + getWidget().updateHeight(); + } + } + + @Override + public List<ComponentConnector> getChildComponents() { + if (childComponents == null) { + return Collections.emptyList(); + } + + return childComponents; + } + + @Override + public void setChildComponents(List<ComponentConnector> childComponents) { + this.childComponents = childComponents; + } + + @Override + public HandlerRegistration addConnectorHierarchyChangeHandler( + ConnectorHierarchyChangeHandler handler) { + return ensureHandlerManager() + .addHandler(ConnectorHierarchyChangeEvent.TYPE, handler); + } + + @Override + public void setChildMeasurementHint(ChildMeasurementHint hint) { + getWidget().setChildMeasurementHint(hint); + } + + @Override + public ChildMeasurementHint getChildMeasurementHint() { + return getWidget().getChildMeasurementHint(); + } + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/table/VTableLazyInitItemIdentifiers.java b/compatibility-client/src/main/java/com/vaadin/client/ui/table/VTableLazyInitItemIdentifiers.java new file mode 100644 index 0000000000..20433dd960 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/table/VTableLazyInitItemIdentifiers.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.table; + +import com.vaadin.client.ui.dd.VLazyInitItemIdentifiers; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.ui.Table; + +@AcceptCriterion(Table.TableDropCriterion.class) +public final class VTableLazyInitItemIdentifiers + extends VLazyInitItemIdentifiers { + // all logic in superclass +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/treetable/TreeTableConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/treetable/TreeTableConnector.java new file mode 100644 index 0000000000..77074d4cb5 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/treetable/TreeTableConnector.java @@ -0,0 +1,151 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.treetable; + +import com.google.gwt.dom.client.Element; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.TooltipInfo; +import com.vaadin.client.UIDL; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.FocusableScrollPanel; +import com.vaadin.client.ui.VScrollTable.VScrollTableBody.VScrollTableRow; +import com.vaadin.client.ui.VTreeTable; +import com.vaadin.client.ui.VTreeTable.PendingNavigationEvent; +import com.vaadin.client.ui.VTreeTable.VTreeTableScrollBody.VTreeTableRow; +import com.vaadin.client.ui.table.TableConnector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.treetable.TreeTableConstants; +import com.vaadin.shared.ui.treetable.TreeTableState; +import com.vaadin.ui.TreeTable; + +@Connect(TreeTable.class) +public class TreeTableConnector extends TableConnector { + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + FocusableScrollPanel widget = null; + int scrollPosition = 0; + if (getWidget().collapseRequest) { + widget = (FocusableScrollPanel) getWidget().getWidget(1); + scrollPosition = widget.getScrollPosition(); + } + getWidget().animationsEnabled = uidl.getBooleanAttribute("animate"); + getWidget().colIndexOfHierarchy = uidl.hasAttribute( + TreeTableConstants.ATTRIBUTE_HIERARCHY_COLUMN_INDEX) + ? uidl.getIntAttribute( + TreeTableConstants.ATTRIBUTE_HIERARCHY_COLUMN_INDEX) + : 0; + int oldTotalRows = getWidget().getTotalRows(); + + super.updateFromUIDL(uidl, client); + // super.updateFromUIDL set rendering to false, even though we continue + // rendering here. Set it back to true. + getWidget().rendering = true; + + if (getWidget().collapseRequest) { + if (getWidget().collapsedRowKey != null + && getWidget().scrollBody != null) { + VScrollTableRow row = getWidget() + .getRenderedRowByKey(getWidget().collapsedRowKey); + if (row != null) { + getWidget().setRowFocus(row); + getWidget().focus(); + } + } + + int scrollPosition2 = widget.getScrollPosition(); + if (scrollPosition != scrollPosition2) { + widget.setScrollPosition(scrollPosition); + } + + // check which rows are needed from the server and initiate a + // deferred fetch + getWidget().onScroll(null); + } + // Recalculate table size if collapse request, or if page length is zero + // (not sent by server) and row count changes (#7908). + if (getWidget().collapseRequest || (!uidl.hasAttribute("pagelength") + && getWidget().getTotalRows() != oldTotalRows)) { + /* + * Ensure that possibly removed/added scrollbars are considered. + * Triggers row calculations, removes cached rows etc. Basically + * cleans up state. Be careful if touching this, you will break + * pageLength=0 if you remove this. + */ + getWidget().triggerLazyColumnAdjustment(true); + + getWidget().collapseRequest = false; + } + if (uidl.hasAttribute("focusedRow")) { + String key = uidl.getStringAttribute("focusedRow"); + getWidget().setRowFocus(getWidget().getRenderedRowByKey(key)); + getWidget().focusParentResponsePending = false; + } else if (uidl.hasAttribute("clearFocusPending")) { + // Special case to detect a response to a focusParent request that + // does not return any focusedRow because the selected node has no + // parent + getWidget().focusParentResponsePending = false; + } + + while (!getWidget().collapseRequest + && !getWidget().focusParentResponsePending + && !getWidget().pendingNavigationEvents.isEmpty()) { + // Keep replaying any queued events as long as we don't have any + // potential content changes pending + PendingNavigationEvent event = getWidget().pendingNavigationEvents + .removeFirst(); + getWidget().handleNavigation(event.keycode, event.ctrl, + event.shift); + } + getWidget().rendering = false; + } + + @Override + public VTreeTable getWidget() { + return (VTreeTable) super.getWidget(); + } + + @Override + public TreeTableState getState() { + return (TreeTableState) super.getState(); + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + + TooltipInfo info = null; + + if (element != getWidget().getElement()) { + Object node = WidgetUtil.findWidget(element, VTreeTableRow.class); + + if (node != null) { + VTreeTableRow row = (VTreeTableRow) node; + info = row.getTooltip(element); + } + } + + if (info == null) { + info = super.getTooltipInfo(element); + } + + return info; + } + + @Override + protected VScrollTableRow getScrollTableRow(Element e) { + return WidgetUtil.findWidget(e, VTreeTableRow.class); + } +} |