From: Johannes Dahlström Date: Mon, 11 Jun 2012 12:59:39 +0000 (+0300) Subject: Merge commit 'e8a652' X-Git-Tag: 7.0.0.alpha3~139 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=a7da5f34ff61389e81b8ab8bed4cdf45236765c6;p=vaadin-framework.git Merge commit 'e8a652' --- a7da5f34ff61389e81b8ab8bed4cdf45236765c6 diff --cc src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java index 64018fa49c,0000000000..efc4d58a3b mode 100644,000000..100644 --- a/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java +++ b/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java @@@ -1,6855 -1,0 +1,6878 @@@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui.table; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.dom.client.TableRowElement; +import com.google.gwt.dom.client.TableSectionElement; +import com.google.gwt.dom.client.Touch; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.event.dom.client.ScrollEvent; +import com.google.gwt.event.dom.client.ScrollHandler; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.UIObject; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.ComponentConnector; +import com.vaadin.terminal.gwt.client.ComponentState; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.MouseEventDetails; +import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder; +import com.vaadin.terminal.gwt.client.TooltipInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.VTooltip; +import com.vaadin.terminal.gwt.client.ui.Action; +import com.vaadin.terminal.gwt.client.ui.ActionOwner; +import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel; +import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate; +import com.vaadin.terminal.gwt.client.ui.TreeAction; +import com.vaadin.terminal.gwt.client.ui.dd.DDUtil; +import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback; +import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager; +import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent; +import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler; +import com.vaadin.terminal.gwt.client.ui.dd.VTransferable; +import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.gwt.client.ui.embedded.VEmbedded; +import com.vaadin.terminal.gwt.client.ui.label.VLabel; +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +/** + * VScrollTable + * + * VScrollTable is a FlowPanel having two widgets in it: * TableHead component * + * ScrollPanel + * + * TableHead contains table's header and widgets + logic for resizing, + * reordering and hiding columns. + * + * ScrollPanel contains VScrollTableBody object which handles content. To save + * some bandwidth and to improve clients responsiveness with loads of data, in + * VScrollTableBody all rows are not necessary rendered. There are "spacers" in + * VScrollTableBody to use the exact same space as non-rendered rows would use. + * This way we can use seamlessly traditional scrollbars and scrolling to fetch + * more rows instead of "paging". + * + * In VScrollTable we listen to scroll events. On horizontal scrolling we also + * update TableHeads scroll position which has its scrollbars hidden. On + * vertical scroll events we will check if we are reaching the end of area where + * we have rows rendered and + * + * TODO implement unregistering for child components in Cells + */ +public class VScrollTable extends FlowPanel implements HasWidgets, + ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable, + ActionOwner { + + public enum SelectMode { + NONE(0), SINGLE(1), MULTI(2); + private int id; + + private SelectMode(int id) { + this.id = id; + } + + public int getId() { + return id; + } + } + + private static final String ROW_HEADER_COLUMN_KEY = "0"; + + public static final String CLASSNAME = "v-table"; + public static final String CLASSNAME_SELECTION_FOCUS = CLASSNAME + "-focus"; + + public static final String ATTRIBUTE_PAGEBUFFER_FIRST = "pb-ft"; + public static final String ATTRIBUTE_PAGEBUFFER_LAST = "pb-l"; + + public static final String ITEM_CLICK_EVENT_ID = "itemClick"; + public static final String HEADER_CLICK_EVENT_ID = "handleHeaderClick"; + public static final String FOOTER_CLICK_EVENT_ID = "handleFooterClick"; + public static final String COLUMN_RESIZE_EVENT_ID = "columnResize"; + public static final String COLUMN_REORDER_EVENT_ID = "columnReorder"; + + private static final double CACHE_RATE_DEFAULT = 2; + + /** + * The default multi select mode where simple left clicks only selects one + * item, CTRL+left click selects multiple items and SHIFT-left click selects + * a range of items. + */ + private static final int MULTISELECT_MODE_DEFAULT = 0; + + /** + * The simple multiselect mode is what the table used to have before + * ctrl/shift selections were added. That is that when this is set clicking + * on an item selects/deselects the item and no ctrl/shift selections are + * available. + */ + private static final int MULTISELECT_MODE_SIMPLE = 1; + + /** + * multiple of pagelength which component will cache when requesting more + * rows + */ + private double cache_rate = CACHE_RATE_DEFAULT; + /** + * fraction of pageLenght which can be scrolled without making new request + */ + private double cache_react_rate = 0.75 * cache_rate; + + public static final char ALIGN_CENTER = 'c'; + public static final char ALIGN_LEFT = 'b'; + public static final char ALIGN_RIGHT = 'e'; + private static final int CHARCODE_SPACE = 32; + private int firstRowInViewPort = 0; + private int pageLength = 15; + private int lastRequestedFirstvisible = 0; // to detect "serverside scroll" + + protected boolean showRowHeaders = false; + + private String[] columnOrder; + + protected ApplicationConnection client; + protected String paintableId; + + boolean immediate; + private boolean nullSelectionAllowed = true; + + private SelectMode selectMode = SelectMode.NONE; + + private final HashSet selectedRowKeys = new HashSet(); + + /* + * When scrolling and selecting at the same time, the selections are not in + * sync with the server while retrieving new rows (until key is released). + */ + private HashSet unSyncedselectionsBeforeRowFetch; + + /* + * These are used when jumping between pages when pressing Home and End + */ + boolean selectLastItemInNextRender = false; + boolean selectFirstItemInNextRender = false; + boolean focusFirstItemInNextRender = false; + boolean focusLastItemInNextRender = false; + + /* + * The currently focused row + */ + VScrollTableRow focusedRow; + + /* + * Helper to store selection range start in when using the keyboard + */ + private VScrollTableRow selectionRangeStart; + + /* + * Flag for notifying when the selection has changed and should be sent to + * the server + */ + boolean selectionChanged = false; + + /* + * The speed (in pixels) which the scrolling scrolls vertically/horizontally + */ + private int scrollingVelocity = 10; + + private Timer scrollingVelocityTimer = null; + + String[] bodyActionKeys; + + private boolean enableDebug = false; + + private static final boolean hasNativeTouchScrolling = BrowserInfo.get() + .isTouchDevice() + && !BrowserInfo.get().requiresTouchScrollDelegate(); + ++ private Set noncollapsibleColumns; ++ + /** + * Represents a select range of rows + */ + private class SelectionRange { + private VScrollTableRow startRow; + private final int length; + + /** + * Constuctor. + */ + public SelectionRange(VScrollTableRow row1, VScrollTableRow row2) { + VScrollTableRow endRow; + if (row2.isBefore(row1)) { + startRow = row2; + endRow = row1; + } else { + startRow = row1; + endRow = row2; + } + length = endRow.getIndex() - startRow.getIndex() + 1; + } + + public SelectionRange(VScrollTableRow row, int length) { + startRow = row; + this.length = length; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return startRow.getKey() + "-" + length; + } + + private boolean inRange(VScrollTableRow row) { + return row.getIndex() >= startRow.getIndex() + && row.getIndex() < startRow.getIndex() + length; + } + + public Collection split(VScrollTableRow row) { + assert row.isAttached(); + ArrayList ranges = new ArrayList(2); + + int endOfFirstRange = row.getIndex() - 1; + if (!(endOfFirstRange - startRow.getIndex() < 0)) { + // create range of first part unless its length is < 1 + ranges.add(new SelectionRange(startRow, endOfFirstRange + - startRow.getIndex() + 1)); + } + int startOfSecondRange = row.getIndex() + 1; + if (!(getEndIndex() - startOfSecondRange < 0)) { + // create range of second part unless its length is < 1 + VScrollTableRow startOfRange = scrollBody + .getRowByRowIndex(startOfSecondRange); + ranges.add(new SelectionRange(startOfRange, getEndIndex() + - startOfSecondRange + 1)); + } + return ranges; + } + + private int getEndIndex() { + return startRow.getIndex() + length - 1; + } + + }; + + private final HashSet selectedRowRanges = new HashSet(); + + boolean initializedAndAttached = false; + + /** + * Flag to indicate if a column width recalculation is needed due update. + */ + boolean headerChangedDuringUpdate = false; + + protected final TableHead tHead = new TableHead(); + + final TableFooter tFoot = new TableFooter(); + + final FocusableScrollPanel scrollBodyPanel = new FocusableScrollPanel(true); + + private KeyPressHandler navKeyPressHandler = new KeyPressHandler() { + @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(); + } + } + }; + int totalRows; + + private Set collapsedColumns; + + final RowRequestHandler rowRequestHandler; + VScrollTableBody scrollBody; + private int firstvisible = 0; + private boolean sortAscending; + private String sortColumn; + private String oldSortColumn; + private boolean columnReordering; + + /** + * This map contains captions and icon urls for actions like: * "33_c" -> + * "Edit" * "33_i" -> "http://dom.com/edit.png" + */ + private final HashMap actionMap = new HashMap(); + private String[] visibleColOrder; + private boolean initialContentReceived = false; + private Element scrollPositionElement; + boolean enabled; + boolean showColHeaders; + boolean showColFooters; + + /** flag to indicate that table body has changed */ + private boolean isNewBody = true; + + /* + * Read from the "recalcWidths" -attribute. When it is true, the table will + * recalculate the widths for columns - desirable in some cases. For #1983, + * marked experimental. + */ + boolean recalcWidths = false; + + boolean rendering = false; + private boolean hasFocus = false; + private int dragmode; + + private int multiselectmode; + int tabIndex; + private TouchScrollDelegate touchScrollDelegate; + + int lastRenderedHeight; + + /** + * Values (serverCacheFirst+serverCacheLast) sent by server that tells which + * rows (indexes) are in the server side cache (page buffer). -1 means + * unknown. The server side cache row MUST MATCH the client side cache rows. + * + * If the client side cache contains additional rows with e.g. buttons, it + * will cause out of sync when such a button is pressed. + * + * If the server side cache contains additional rows with e.g. buttons, + * scrolling in the client will cause empty buttons to be rendered + * (cached=true request for non-existing components) + */ + int serverCacheFirst = -1; + int serverCacheLast = -1; + + boolean sizeNeedsInit = true; + + /** + * Used to recall the position of an open context menu if we need to close + * and reopen it during a row update. + */ + class ContextMenuDetails { + String rowKey; + int left; + int top; + + ContextMenuDetails(String rowKey, int left, int top) { + this.rowKey = rowKey; + this.left = left; + this.top = top; + } + } + + protected ContextMenuDetails contextMenu = null; + + public VScrollTable() { + setMultiSelectMode(MULTISELECT_MODE_DEFAULT); + + scrollBodyPanel.addStyleName(CLASSNAME + "-body-wrapper"); + scrollBodyPanel.addFocusHandler(this); + scrollBodyPanel.addBlurHandler(this); + + scrollBodyPanel.addScrollHandler(this); + scrollBodyPanel.addStyleName(CLASSNAME + "-body"); + + /* + * Firefox auto-repeat works correctly only if we use a key press + * handler, other browsers handle it correctly when using a key down + * handler + */ + if (BrowserInfo.get().isGecko()) { + scrollBodyPanel.addKeyPressHandler(navKeyPressHandler); + } else { + scrollBodyPanel.addKeyDownHandler(navKeyDownHandler); + } + scrollBodyPanel.addKeyUpHandler(navKeyUpHandler); + + scrollBodyPanel.sinkEvents(Event.TOUCHEVENTS); + + scrollBodyPanel.sinkEvents(Event.ONCONTEXTMENU); + scrollBodyPanel.addDomHandler(new ContextMenuHandler() { + @Override + public void onContextMenu(ContextMenuEvent event) { + handleBodyContextMenu(event); + } + }, ContextMenuEvent.getType()); + + setStyleName(CLASSNAME); + + add(tHead); + add(scrollBodyPanel); + add(tFoot); + + rowRequestHandler = new RowRequestHandler(); + } + + public void init(ApplicationConnection client) { + this.client = client; + // Add a handler to clear saved context menu details when the menu + // closes. See #8526. + client.getContextMenu().addCloseHandler(new CloseHandler() { + @Override + public void onClose(CloseEvent event) { + contextMenu = null; + } + }); + } + + private void handleBodyContextMenu(ContextMenuEvent event) { + if (enabled && bodyActionKeys != null) { + int left = Util.getTouchOrMouseClientX(event.getNativeEvent()); + int top = Util.getTouchOrMouseClientY(event.getNativeEvent()); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + client.getContextMenu().showAt(this, left, top); + + // Only prevent browser context menu if there are action handlers + // registered + event.stopPropagation(); + event.preventDefault(); + } + } + + /** + * Fires a column resize event which sends the resize information to the + * server. + * + * @param columnId + * The columnId of the column which was resized + * @param originalWidth + * The width in pixels of the column before the resize event + * @param newWidth + * The width in pixels of the column after the resize event + */ + private void fireColumnResizeEvent(String columnId, int originalWidth, + int newWidth) { + client.updateVariable(paintableId, "columnResizeEventColumn", columnId, + false); + client.updateVariable(paintableId, "columnResizeEventPrev", + originalWidth, false); + client.updateVariable(paintableId, "columnResizeEventCurr", newWidth, + immediate); + + } + + /** + * Non-immediate variable update of column widths for a collection of + * columns. + * + * @param columns + * the columns to trigger the events for. + */ + private void sendColumnWidthUpdates(Collection columns) { + String[] newSizes = new String[columns.size()]; + int ix = 0; + for (HeaderCell cell : columns) { + newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth(); + } + client.updateVariable(paintableId, "columnWidthUpdates", newSizes, + false); + } + + /** + * Moves the focus one step down + * + * @return Returns true if succeeded + */ + private boolean moveFocusDown() { + return moveFocusDown(0); + } + + /** + * Moves the focus down by 1+offset rows + * + * @return Returns true if succeeded, else false if the selection could not + * be move downwards + */ + private boolean moveFocusDown(int offset) { + if (isSelectable()) { + if (focusedRow == null && scrollBody.iterator().hasNext()) { + // FIXME should focus first visible from top, not first rendered + // ?? + return setRowFocus((VScrollTableRow) scrollBody.iterator() + .next()); + } else { + VScrollTableRow next = getNextRow(focusedRow, offset); + if (next != null) { + return setRowFocus(next); + } + } + } + + return false; + } + + /** + * Moves the selection one step up + * + * @return Returns true if succeeded + */ + private boolean moveFocusUp() { + return moveFocusUp(0); + } + + /** + * Moves the focus row upwards + * + * @return Returns true if succeeded, else false if the selection could not + * be move upwards + * + */ + private boolean moveFocusUp(int offset) { + if (isSelectable()) { + if (focusedRow == null && scrollBody.iterator().hasNext()) { + // FIXME logic is exactly the same as in moveFocusDown, should + // be the opposite?? + return setRowFocus((VScrollTableRow) scrollBody.iterator() + .next()); + } else { + VScrollTableRow prev = getPreviousRow(focusedRow, offset); + if (prev != null) { + return setRowFocus(prev); + } else { + VConsole.log("no previous available"); + } + } + } + + return false; + } + + /** + * Selects a row where the current selection head is + * + * @param ctrlSelect + * Is the selection a ctrl+selection + * @param shiftSelect + * Is the selection a shift+selection + * @return Returns truw + */ + private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) { + if (focusedRow != null) { + // Arrows moves the selection and clears previous selections + if (isSelectable() && !ctrlSelect && !shiftSelect) { + deselectAll(); + focusedRow.toggleSelection(); + selectionRangeStart = focusedRow; + } else if (isSelectable() && ctrlSelect && !shiftSelect) { + // Ctrl+arrows moves selection head + selectionRangeStart = focusedRow; + // No selection, only selection head is moved + } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) { + // Shift+arrows selection selects a range + focusedRow.toggleShiftSelection(shiftSelect); + } + } + } + + /** + * Sends the selection to the server if changed since the last update/visit. + */ + protected void sendSelectedRows() { + sendSelectedRows(immediate); + } + + /** + * Sends the selection to the server if it has been changed since the last + * update/visit. + * + * @param immediately + * set to true to immediately send the rows + */ + protected void sendSelectedRows(boolean immediately) { + // Don't send anything if selection has not changed + if (!selectionChanged) { + return; + } + + // Reset selection changed flag + selectionChanged = false; + + // Note: changing the immediateness of this might require changes to + // "clickEvent" immediateness also. + if (isMultiSelectModeDefault()) { + // Convert ranges to a set of strings + Set ranges = new HashSet(); + for (SelectionRange range : selectedRowRanges) { + ranges.add(range.toString()); + } + + // Send the selected row ranges + client.updateVariable(paintableId, "selectedRanges", + ranges.toArray(new String[selectedRowRanges.size()]), false); + + // clean selectedRowKeys so that they don't contain excess values + for (Iterator iterator = selectedRowKeys.iterator(); iterator + .hasNext();) { + String key = iterator.next(); + VScrollTableRow renderedRowByKey = getRenderedRowByKey(key); + if (renderedRowByKey != null) { + for (SelectionRange range : selectedRowRanges) { + if (range.inRange(renderedRowByKey)) { + iterator.remove(); + } + } + } else { + // orphaned selected key, must be in a range, ignore + iterator.remove(); + } + + } + } + + // Send the selected rows + client.updateVariable(paintableId, "selected", + selectedRowKeys.toArray(new String[selectedRowKeys.size()]), + immediately); + + } + + /** + * Get the key that moves the selection head upwards. By default it is the + * up arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that moves the selection head downwards. By default it is the + * down arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that scrolls to the left in the table. By default it is the + * left arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that scroll to the right on the table. By default it is the + * right arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * Get the key that selects an item in the table. By default it is the space + * bar key but by overriding this you can change the key to whatever you + * want. + * + * @return + */ + protected int getNavigationSelectKey() { + return CHARCODE_SPACE; + } + + /** + * Get the key the moves the selection one page up in the table. By default + * this is the Page Up key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationPageUpKey() { + return KeyCodes.KEY_PAGEUP; + } + + /** + * Get the key the moves the selection one page down in the table. By + * default this is the Page Down key but by overriding this you can change + * the key to whatever you want. + * + * @return + */ + protected int getNavigationPageDownKey() { + return KeyCodes.KEY_PAGEDOWN; + } + + /** + * Get the key the moves the selection to the beginning of the table. By + * default this is the Home key but by overriding this you can change the + * key to whatever you want. + * + * @return + */ + protected int getNavigationStartKey() { + return KeyCodes.KEY_HOME; + } + + /** + * Get the key the moves the selection to the end of the table. By default + * this is the End key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationEndKey() { + return KeyCodes.KEY_END; + } + + void initializeRows(UIDL uidl, UIDL rowData) { + if (scrollBody != null) { + scrollBody.removeFromParent(); + } + scrollBody = createScrollBody(); + + scrollBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"), + uidl.getIntAttribute("rows")); + scrollBodyPanel.add(scrollBody); + + // New body starts scrolled to the left, make sure the header and footer + // are also scrolled to the left + tHead.setHorizontalScrollPosition(0); + tFoot.setHorizontalScrollPosition(0); + + initialContentReceived = true; + sizeNeedsInit = true; + scrollBody.restoreRowVisibility(); + } + + void updateColumnProperties(UIDL uidl) { + updateColumnOrder(uidl); + + updateCollapsedColumns(uidl); + + UIDL vc = uidl.getChildByTagName("visiblecolumns"); + if (vc != null) { + tHead.updateCellsFromUIDL(vc); + tFoot.updateCellsFromUIDL(vc); + } + + updateHeader(uidl.getStringArrayAttribute("vcolorder")); + updateFooter(uidl.getStringArrayAttribute("vcolorder")); ++ 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; + } + } + + boolean selectSelectedRows(UIDL uidl) { + boolean keyboardSelectionOverRowFetchInProgress = false; + + if (uidl.hasVariable("selected")) { + final Set selectedKeys = uidl + .getStringArrayVariableAsSet("selected"); + if (scrollBody != null) { + Iterator iterator = scrollBody.iterator(); + while (iterator.hasNext()) { + /* + * Make the focus reflect to the server side state unless we + * are currently selecting multiple rows with keyboard. + */ + VScrollTableRow row = (VScrollTableRow) iterator.next(); + boolean selected = selectedKeys.contains(row.getKey()); + if (!selected + && unSyncedselectionsBeforeRowFetch != null + && unSyncedselectionsBeforeRowFetch.contains(row + .getKey())) { + selected = true; + keyboardSelectionOverRowFetchInProgress = true; + } + if (selected != row.isSelected()) { + row.toggleSelection(); + if (!isSingleSelectMode() && !selected) { + // Update selection range in case a row is + // unselected from the middle of a range - #8076 + removeRowFromUnsentSelectionRanges(row); + } + } + } + } + } + unSyncedselectionsBeforeRowFetch = null; + return keyboardSelectionOverRowFetchInProgress; + } + + void updateSortingProperties(UIDL uidl) { + oldSortColumn = sortColumn; + if (uidl.hasVariable("sortascending")) { + sortAscending = uidl.getBooleanVariable("sortascending"); + sortColumn = uidl.getStringVariable("sortcolumn"); + } + } + + void resizeSortedColumnForSortIndicator() { + // Force recalculation of the captionContainer element inside the header + // cell to accomodate for the size of the sort arrow. + HeaderCell sortedHeader = tHead.getHeaderCell(sortColumn); + if (sortedHeader != null) { + tHead.resizeCaptionContainer(sortedHeader); + } + // Also recalculate the width of the captionContainer element in the + // previously sorted header, since this now has more room. + HeaderCell oldSortedHeader = tHead.getHeaderCell(oldSortColumn); + if (oldSortedHeader != null) { + tHead.resizeCaptionContainer(oldSortedHeader); + } + } + + void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) { + firstvisible = uidl.hasVariable("firstvisible") ? uidl + .getIntVariable("firstvisible") : 0; + if (firstvisible != lastRequestedFirstvisible && scrollBody != null) { + // received 'surprising' firstvisible from server: scroll there + firstRowInViewPort = firstvisible; + scrollBodyPanel + .setScrollPosition(measureRowHeightOffset(firstvisible)); + } + } + + protected int measureRowHeightOffset(int rowIx) { + return (int) (rowIx * scrollBody.getRowHeight()); + } + + void updatePageLength(UIDL uidl) { + int oldPageLength = pageLength; + if (uidl.hasAttribute("pagelength")) { + pageLength = uidl.getIntAttribute("pagelength"); + } else { + // pagelenght is "0" meaning scrolling is turned off + pageLength = totalRows; + } + + if (oldPageLength != pageLength && initializedAndAttached) { + // page length changed, need to update size + sizeNeedsInit = true; + } + } + + void updateSelectionProperties(UIDL uidl, ComponentState state, + boolean readOnly) { + setMultiSelectMode(uidl.hasAttribute("multiselectmode") ? uidl + .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT); + + nullSelectionAllowed = uidl.hasAttribute("nsa") ? uidl + .getBooleanAttribute("nsa") : true; + + if (uidl.hasAttribute("selectmode")) { + if (readOnly) { + selectMode = SelectMode.NONE; + } else if (uidl.getStringAttribute("selectmode").equals("multi")) { + selectMode = SelectMode.MULTI; + } else if (uidl.getStringAttribute("selectmode").equals("single")) { + selectMode = SelectMode.SINGLE; + } else { + selectMode = SelectMode.NONE; + } + } + } + + void updateDragMode(UIDL uidl) { + dragmode = uidl.hasAttribute("dragmode") ? uidl + .getIntAttribute("dragmode") : 0; + if (BrowserInfo.get().isIE()) { + if (dragmode > 0) { + getElement().setPropertyJSO("onselectstart", + getPreventTextSelectionIEHack()); + } else { + getElement().setPropertyJSO("onselectstart", null); + } + } + } + + protected void updateTotalRows(UIDL uidl) { + int newTotalRows = uidl.getIntAttribute("totalrows"); + if (newTotalRows != getTotalRows()) { + if (scrollBody != null) { + if (getTotalRows() == 0) { + tHead.clear(); + tFoot.clear(); + } + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + setTotalRows(newTotalRows); + } + } + + protected void setTotalRows(int newTotalRows) { + totalRows = newTotalRows; + } + + public int getTotalRows() { + return totalRows; + } + + void focusRowFromBody() { + if (selectedRowKeys.size() == 1) { + // try to focus a row currently selected and in viewport + String selectedRowKey = selectedRowKeys.iterator().next(); + if (selectedRowKey != null) { + VScrollTableRow renderedRow = getRenderedRowByKey(selectedRowKey); + if (renderedRow == null || !renderedRow.isInViewPort()) { + setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort)); + } else { + setRowFocus(renderedRow); + } + } + } else { + // multiselect mode + setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort)); + } + } + + protected VScrollTableBody createScrollBody() { + return new VScrollTableBody(); + } + + /** + * Selects the last row visible in the table + * + * @param focusOnly + * Should the focus only be moved to the last row + */ + void selectLastRenderedRowInViewPort(boolean focusOnly) { + int index = firstRowInViewPort + getFullyVisibleRowCount(); + VScrollTableRow lastRowInViewport = scrollBody.getRowByRowIndex(index); + if (lastRowInViewport == null) { + // this should not happen in normal situations (white space at the + // end of viewport). Select the last rendered as a fallback. + lastRowInViewport = scrollBody.getRowByRowIndex(scrollBody + .getLastRendered()); + if (lastRowInViewport == null) { + return; // empty table + } + } + setRowFocus(lastRowInViewport); + if (!focusOnly) { + selectFocusedRow(false, multiselectPending); + sendSelectedRows(); + } + } + + /** + * Selects the first row visible in the table + * + * @param focusOnly + * Should the focus only be moved to the first row + */ + void selectFirstRenderedRowInViewPort(boolean focusOnly) { + int index = firstRowInViewPort; + VScrollTableRow firstInViewport = scrollBody.getRowByRowIndex(index); + if (firstInViewport == null) { + // this should not happen in normal situations + return; + } + setRowFocus(firstInViewport); + if (!focusOnly) { + selectFocusedRow(false, multiselectPending); + sendSelectedRows(); + } + } + + void setCacheRateFromUIDL(UIDL uidl) { + setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr") + : CACHE_RATE_DEFAULT); + } + + private void setCacheRate(double d) { + if (cache_rate != d) { + cache_rate = d; + cache_react_rate = 0.75 * d; + } + } + + void updateActionMap(UIDL mainUidl) { + UIDL actionsUidl = mainUidl.getChildByTagName("actions"); + if (actionsUidl == null) { + return; + } + + final Iterator it = actionsUidl.getChildIterator(); + while (it.hasNext()) { + final UIDL action = (UIDL) it.next(); + final String key = action.getStringAttribute("key"); + final String caption = action.getStringAttribute("caption"); + actionMap.put(key + "_c", caption); + if (action.hasAttribute("icon")) { + // TODO need some uri handling ?? + actionMap.put(key + "_i", client.translateVaadinUri(action + .getStringAttribute("icon"))); + } else { + actionMap.remove(key + "_i"); + } + } + + } + + public String getActionCaption(String actionKey) { + return actionMap.get(actionKey + "_c"); + } + + public String getActionIcon(String actionKey) { + return actionMap.get(actionKey + "_i"); + } + + private void updateHeader(String[] strings) { + if (strings == null) { + return; + } + + int visibleCols = strings.length; + int colIndex = 0; + if (showRowHeaders) { + tHead.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex); + visibleCols++; + visibleColOrder = new String[visibleCols]; + visibleColOrder[colIndex] = ROW_HEADER_COLUMN_KEY; + colIndex++; + } else { + visibleColOrder = new String[visibleCols]; + tHead.removeCell(ROW_HEADER_COLUMN_KEY); + } + + int i; + for (i = 0; i < strings.length; i++) { + final String cid = strings[i]; + visibleColOrder[colIndex] = cid; + tHead.enableColumn(cid, colIndex); + colIndex++; + } + + tHead.setVisible(showColHeaders); + setContainerHeight(); + + } + + /** + * Updates footers. + *

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

+ * + * @param strings + */ + private void updateFooter(String[] strings) { + if (strings == null) { + return; + } + + // Add dummy column if row headers are present + int colIndex = 0; + if (showRowHeaders) { + tFoot.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex); + colIndex++; + } else { + tFoot.removeCell(ROW_HEADER_COLUMN_KEY); + } + + int i; + for (i = 0; i < strings.length; i++) { + final String cid = strings[i]; + tFoot.enableColumn(cid, colIndex); + colIndex++; + } + + tFoot.setVisible(showColFooters); + } + + /** + * @param uidl + * which contains row data + * @param firstRow + * first row in data set + * @param reqRows + * amount of rows in data set + */ + void updateBody(UIDL uidl, int firstRow, int reqRows) { + if (uidl == null || reqRows < 1) { + // container is empty, remove possibly existing rows + if (firstRow <= 0) { + while (scrollBody.getLastRendered() > scrollBody.firstRendered) { + scrollBody.unlinkRow(false); + } + scrollBody.unlinkRow(false); + } + return; + } + + scrollBody.renderRows(uidl, firstRow, reqRows); + + discardRowsOutsideCacheWindow(); + } + + void updateRowsInBody(UIDL partialRowUpdates) { + if (partialRowUpdates == null) { + return; + } + int firstRowIx = partialRowUpdates.getIntAttribute("firsturowix"); + int count = partialRowUpdates.getIntAttribute("numurows"); + scrollBody.unlinkRows(firstRowIx, count); + scrollBody.insertRows(partialRowUpdates, firstRowIx, count); + } + + /** + * Updates the internal cache by unlinking rows that fall outside of the + * caching window. + */ + protected void discardRowsOutsideCacheWindow() { + int firstRowToKeep = (int) (firstRowInViewPort - pageLength + * cache_rate); + int lastRowToKeep = (int) (firstRowInViewPort + pageLength + pageLength + * cache_rate); + debug("Client side calculated cache rows to keep: " + firstRowToKeep + + "-" + lastRowToKeep); + + if (serverCacheFirst != -1) { + firstRowToKeep = serverCacheFirst; + lastRowToKeep = serverCacheLast; + debug("Server cache rows that override: " + serverCacheFirst + "-" + + serverCacheLast); + if (firstRowToKeep < scrollBody.getFirstRendered() + || lastRowToKeep > scrollBody.getLastRendered()) { + debug("*** Server wants us to keep " + serverCacheFirst + "-" + + serverCacheLast + " but we only have rows " + + scrollBody.getFirstRendered() + "-" + + scrollBody.getLastRendered() + " rendered!"); + } + } + discardRowsOutsideOf(firstRowToKeep, lastRowToKeep); + + scrollBody.fixSpacers(); + + scrollBody.restoreRowVisibility(); + } + + private void discardRowsOutsideOf(int optimalFirstRow, int optimalLastRow) { + /* + * firstDiscarded and lastDiscarded are only calculated for debug + * purposes + */ + int firstDiscarded = -1, lastDiscarded = -1; + boolean cont = true; + while (cont && scrollBody.getLastRendered() > optimalFirstRow + && scrollBody.getFirstRendered() < optimalFirstRow) { + if (firstDiscarded == -1) { + firstDiscarded = scrollBody.getFirstRendered(); + } + + // removing row from start + cont = scrollBody.unlinkRow(true); + } + if (firstDiscarded != -1) { + lastDiscarded = scrollBody.getFirstRendered() - 1; + debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded); + } + firstDiscarded = lastDiscarded = -1; + + cont = true; + while (cont && scrollBody.getLastRendered() > optimalLastRow) { + if (lastDiscarded == -1) { + lastDiscarded = scrollBody.getLastRendered(); + } + + // removing row from the end + cont = scrollBody.unlinkRow(false); + } + if (lastDiscarded != -1) { + firstDiscarded = scrollBody.getLastRendered() + 1; + debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded); + } + + debug("Now in cache: " + scrollBody.getFirstRendered() + "-" + + scrollBody.getLastRendered()); + } + + /** + * Inserts rows in the table body or removes them from the table body based + * on the commands in the UIDL. + * + * @param partialRowAdditions + * the UIDL containing row updates. + */ + protected void addAndRemoveRows(UIDL partialRowAdditions) { + if (partialRowAdditions == null) { + return; + } + if (partialRowAdditions.hasAttribute("hide")) { + scrollBody.unlinkAndReindexRows( + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + scrollBody.ensureCacheFilled(); + } else { + if (partialRowAdditions.hasAttribute("delbelow")) { + scrollBody.insertRowsDeleteBelow(partialRowAdditions, + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + } else { + scrollBody.insertAndReindexRows(partialRowAdditions, + partialRowAdditions.getIntAttribute("firstprowix"), + partialRowAdditions.getIntAttribute("numprows")); + } + } + + discardRowsOutsideCacheWindow(); + } + + /** + * Gives correct column index for given column key ("cid" in UIDL). + * + * @param colKey + * @return column index of visible columns, -1 if column not visible + */ + private int getColIndexByKey(String colKey) { + // return 0 if asked for rowHeaders + if (ROW_HEADER_COLUMN_KEY.equals(colKey)) { + return 0; + } + for (int i = 0; i < visibleColOrder.length; i++) { + if (visibleColOrder[i].equals(colKey)) { + return i; + } + } + return -1; + } + + private boolean isMultiSelectModeSimple() { + return selectMode == SelectMode.MULTI + && multiselectmode == MULTISELECT_MODE_SIMPLE; + } + + private boolean isSingleSelectMode() { + return selectMode == SelectMode.SINGLE; + } + + private boolean isMultiSelectModeAny() { + return selectMode == SelectMode.MULTI; + } + + private boolean isMultiSelectModeDefault() { + return selectMode == SelectMode.MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT; + } + + private void setMultiSelectMode(int multiselectmode) { + if (BrowserInfo.get().isTouchDevice()) { + // Always use the simple mode for touch devices that do not have + // shift/ctrl keys + this.multiselectmode = MULTISELECT_MODE_SIMPLE; + } else { + this.multiselectmode = multiselectmode; + } + + } + + protected boolean isSelectable() { + return selectMode.getId() > SelectMode.NONE.getId(); + } + + private boolean isCollapsedColumn(String colKey) { + if (collapsedColumns == null) { + return false; + } + if (collapsedColumns.contains(colKey)) { + return true; + } + return false; + } + + private String getColKeyByIndex(int index) { + return tHead.getHeaderCell(index).getColKey(); + } + + private void setColWidth(int colIndex, int w, boolean isDefinedWidth) { + final HeaderCell hcell = tHead.getHeaderCell(colIndex); + + // Make sure that the column grows to accommodate the sort indicator if + // necessary. + if (w < hcell.getMinWidth()) { + w = hcell.getMinWidth(); + } + + // Set header column width + hcell.setWidth(w, isDefinedWidth); + + // Ensure indicators have been taken into account + tHead.resizeCaptionContainer(hcell); + + // Set body column width + scrollBody.setColWidth(colIndex, w); + + // Set footer column width + FooterCell fcell = tFoot.getFooterCell(colIndex); + fcell.setWidth(w, isDefinedWidth); + } + + private int getColWidth(String colKey) { + return tHead.getHeaderCell(colKey).getWidth(); + } + + /** + * Get a rendered row by its key + * + * @param key + * The key to search with + * @return + */ + public VScrollTableRow getRenderedRowByKey(String key) { + if (scrollBody != null) { + final Iterator it = scrollBody.iterator(); + VScrollTableRow r = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (r.getKey().equals(key)) { + return r; + } + } + } + return null; + } + + /** + * Returns the next row to the given row + * + * @param row + * The row to calculate from + * + * @return The next row or null if no row exists + */ + private VScrollTableRow getNextRow(VScrollTableRow row, int offset) { + final Iterator it = scrollBody.iterator(); + VScrollTableRow r = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (r == row) { + r = null; + while (offset >= 0 && it.hasNext()) { + r = (VScrollTableRow) it.next(); + offset--; + } + return r; + } + } + + return null; + } + + /** + * Returns the previous row from the given row + * + * @param row + * The row to calculate from + * @return The previous row or null if no row exists + */ + private VScrollTableRow getPreviousRow(VScrollTableRow row, int offset) { + final Iterator it = scrollBody.iterator(); + final Iterator offsetIt = scrollBody.iterator(); + VScrollTableRow r = null; + VScrollTableRow prev = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (offset < 0) { + prev = (VScrollTableRow) offsetIt.next(); + } + if (r == row) { + return prev; + } + offset--; + } + + return null; + } + + protected void reOrderColumn(String columnKey, int newIndex) { + + final int oldIndex = getColIndexByKey(columnKey); + + // Change header order + tHead.moveCell(oldIndex, newIndex); + + // Change body order + scrollBody.moveCol(oldIndex, newIndex); + + // Change footer order + tFoot.moveCell(oldIndex, newIndex); + + /* + * Build new columnOrder and update it to server Note that columnOrder + * also contains collapsed columns so we cannot directly build it from + * cells vector Loop the old columnOrder and append in order to new + * array unless on moved columnKey. On new index also put the moved key + * i == index on columnOrder, j == index on newOrder + */ + final String oldKeyOnNewIndex = visibleColOrder[newIndex]; + if (showRowHeaders) { + newIndex--; // columnOrder don't have rowHeader + } + // add back hidden rows, + for (int i = 0; i < columnOrder.length; i++) { + if (columnOrder[i].equals(oldKeyOnNewIndex)) { + break; // break loop at target + } + if (isCollapsedColumn(columnOrder[i])) { + newIndex++; + } + } + // finally we can build the new columnOrder for server + final String[] newOrder = new String[columnOrder.length]; + for (int i = 0, j = 0; j < newOrder.length; i++) { + if (j == newIndex) { + newOrder[j] = columnKey; + j++; + } + if (i == columnOrder.length) { + break; + } + if (columnOrder[i].equals(columnKey)) { + continue; + } + newOrder[j] = columnOrder[i]; + j++; + } + columnOrder = newOrder; + // also update visibleColumnOrder + int i = showRowHeaders ? 1 : 0; + for (int j = 0; j < newOrder.length; j++) { + final String cid = newOrder[j]; + if (!isCollapsedColumn(cid)) { + visibleColOrder[i++] = cid; + } + } + client.updateVariable(paintableId, "columnorder", columnOrder, false); + if (client.hasEventListeners(this, COLUMN_REORDER_EVENT_ID)) { + client.sendPendingVariableChanges(); + } + } + + @Override + protected void onDetach() { + rowRequestHandler.cancel(); + super.onDetach(); + // ensure that scrollPosElement will be detached + if (scrollPositionElement != null) { + final Element parent = DOM.getParent(scrollPositionElement); + if (parent != null) { + DOM.removeChild(parent, scrollPositionElement); + } + } + } + + /** + * Run only once when component is attached and received its initial + * content. This function: + * + * * Syncs headers and bodys "natural widths and saves the values. + * + * * Sets proper width and height + * + * * Makes deferred request to get some cache rows + */ + void sizeInit() { + sizeNeedsInit = false; + + scrollBody.setContainerHeight(); + + /* + * We will use browsers table rendering algorithm to find proper column + * widths. If content and header take less space than available, we will + * divide extra space relatively to each column which has not width set. + * + * Overflow pixels are added to last column. + */ + + Iterator headCells = tHead.iterator(); + Iterator footCells = tFoot.iterator(); + int i = 0; + int totalExplicitColumnsWidths = 0; + int total = 0; + float expandRatioDivider = 0; + + final int[] widths = new int[tHead.visibleCells.size()]; + + tHead.enableBrowserIntelligence(); + tFoot.enableBrowserIntelligence(); + + // first loop: collect natural widths + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + final FooterCell fCell = (FooterCell) footCells.next(); + int w = hCell.getWidth(); + if (hCell.isDefinedWidth()) { + // server has defined column width explicitly + totalExplicitColumnsWidths += w; + } else { + if (hCell.getExpandRatio() > 0) { + expandRatioDivider += hCell.getExpandRatio(); + w = 0; + } else { + // get and store greater of header width and column width, + // and + // store it as a minimumn natural col width + int headerWidth = hCell.getNaturalColumnWidth(i); + int footerWidth = fCell.getNaturalColumnWidth(i); + w = headerWidth > footerWidth ? headerWidth : footerWidth; + } + hCell.setNaturalMinimumColumnWidth(w); + fCell.setNaturalMinimumColumnWidth(w); + } + widths[i] = w; + total += w; + i++; + } + + tHead.disableBrowserIntelligence(); + tFoot.disableBrowserIntelligence(); + + boolean willHaveScrollbarz = willHaveScrollbars(); + + // fix "natural" width if width not set + if (isDynamicWidth()) { + int w = total; + w += scrollBody.getCellExtraWidth() * visibleColOrder.length; + if (willHaveScrollbarz) { + w += Util.getNativeScrollbarSize(); + } + setContentWidth(w); + } + + int availW = scrollBody.getAvailableWidth(); + if (BrowserInfo.get().isIE()) { + // Hey IE, are you really sure about this? + availW = scrollBody.getAvailableWidth(); + } + availW -= scrollBody.getCellExtraWidth() * visibleColOrder.length; + + if (willHaveScrollbarz) { + availW -= Util.getNativeScrollbarSize(); + } + + // TODO refactor this code to be the same as in resize timer + boolean needsReLayout = false; + + if (availW > total) { + // natural size is smaller than available space + final int extraSpace = availW - total; + final int totalWidthR = total - totalExplicitColumnsWidths; + int checksum = 0; + needsReLayout = true; + + if (extraSpace == 1) { + // We cannot divide one single pixel so we give it the first + // undefined column + headCells = tHead.iterator(); + i = 0; + checksum = availW; + while (headCells.hasNext()) { + HeaderCell hc = (HeaderCell) headCells.next(); + if (!hc.isDefinedWidth()) { + widths[i]++; + break; + } + i++; + } + + } else if (expandRatioDivider > 0) { + // visible columns have some active expand ratios, excess + // space is divided according to them + headCells = tHead.iterator(); + i = 0; + while (headCells.hasNext()) { + HeaderCell hCell = (HeaderCell) headCells.next(); + if (hCell.getExpandRatio() > 0) { + int w = widths[i]; + final int newSpace = Math.round((extraSpace * (hCell + .getExpandRatio() / expandRatioDivider))); + w += newSpace; + widths[i] = w; + } + checksum += widths[i]; + i++; + } + } else if (totalWidthR > 0) { + // no expand ratios defined, we will share extra space + // relatively to "natural widths" among those without + // explicit width + headCells = tHead.iterator(); + i = 0; + while (headCells.hasNext()) { + HeaderCell hCell = (HeaderCell) headCells.next(); + if (!hCell.isDefinedWidth()) { + int w = widths[i]; + final int newSpace = Math.round((float) extraSpace + * (float) w / totalWidthR); + w += newSpace; + widths[i] = w; + } + checksum += widths[i]; + i++; + } + } + + if (extraSpace > 0 && checksum != availW) { + /* + * There might be in some cases a rounding error of 1px when + * extra space is divided so if there is one then we give the + * first undefined column 1 more pixel + */ + headCells = tHead.iterator(); + i = 0; + while (headCells.hasNext()) { + HeaderCell hc = (HeaderCell) headCells.next(); + if (!hc.isDefinedWidth()) { + widths[i] += availW - checksum; + break; + } + i++; + } + } + + } else { + // bodys size will be more than available and scrollbar will appear + } + + // last loop: set possibly modified values or reset if new tBody + i = 0; + headCells = tHead.iterator(); + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + if (isNewBody || hCell.getWidth() == -1) { + final int w = widths[i]; + setColWidth(i, w, false); + } + i++; + } + + initializedAndAttached = true; + + if (needsReLayout) { + scrollBody.reLayoutComponents(); + } + + updatePageLength(); + + /* + * Fix "natural" height if height is not set. This must be after width + * fixing so the components' widths have been adjusted. + */ + if (isDynamicHeight()) { + /* + * We must force an update of the row height as this point as it + * might have been (incorrectly) calculated earlier + */ + + int bodyHeight; + if (pageLength == totalRows) { + /* + * A hack to support variable height rows when paging is off. + * Generally this is not supported by scrolltable. We want to + * show all rows so the bodyHeight should be equal to the table + * height. + */ + // int bodyHeight = scrollBody.getOffsetHeight(); + bodyHeight = scrollBody.getRequiredHeight(); + } else { + bodyHeight = (int) Math.round(scrollBody.getRowHeight(true) + * pageLength); + } + boolean needsSpaceForHorizontalSrollbar = (total > availW); + if (needsSpaceForHorizontalSrollbar) { + bodyHeight += Util.getNativeScrollbarSize(); + } + scrollBodyPanel.setHeight(bodyHeight + "px"); + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + + isNewBody = false; + + if (firstvisible > 0) { + // FIXME #7607 + // Originally deferred due to Firefox oddities which should not + // occur any more. Currently deferring breaks Webkit scrolling with + // relative-height tables, but not deferring instead breaks tables + // with explicit page length. + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + scrollBodyPanel + .setScrollPosition(measureRowHeightOffset(firstvisible)); + firstRowInViewPort = firstvisible; + } + }); + } + + if (enabled) { + // Do we need cache rows + if (scrollBody.getLastRendered() + 1 < firstRowInViewPort + + pageLength + (int) cache_react_rate * pageLength) { + if (totalRows - 1 > scrollBody.getLastRendered()) { + // fetch cache rows + int firstInNewSet = scrollBody.getLastRendered() + 1; + rowRequestHandler.setReqFirstRow(firstInNewSet); + int lastInNewSet = (int) (firstRowInViewPort + pageLength + cache_rate + * pageLength); + if (lastInNewSet > totalRows - 1) { + lastInNewSet = totalRows - 1; + } + rowRequestHandler.setReqRows(lastInNewSet - firstInNewSet + + 1); + rowRequestHandler.deferRowFetch(1); + } + } + } + + /* + * Ensures the column alignments are correct at initial loading.
+ * (child components widths are correct) + */ + scrollBody.reLayoutComponents(); + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + }); + } + + /** + * Note, this method is not official api although declared as protected. + * Extend at you own risk. + * + * @return true if content area will have scrollbars visible. + */ + protected boolean willHaveScrollbars() { + if (isDynamicHeight()) { + if (pageLength < totalRows) { + return true; + } + } else { + if (scrollBody == null) { + return false; + } + int fakeheight = (int) Math.round(scrollBody.getRowHeight() + * totalRows); + int availableHeight = scrollBodyPanel.getElement().getPropertyInt( + "clientHeight"); + if (fakeheight > availableHeight) { + return true; + } + } + return false; + } + + private void announceScrollPosition() { + if (scrollPositionElement == null) { + scrollPositionElement = DOM.createDiv(); + scrollPositionElement.setClassName(CLASSNAME + "-scrollposition"); + scrollPositionElement.getStyle().setPosition(Position.ABSOLUTE); + scrollPositionElement.getStyle().setDisplay(Display.NONE); + getElement().appendChild(scrollPositionElement); + } + + Style style = scrollPositionElement.getStyle(); + style.setMarginLeft(getElement().getOffsetWidth() / 2 - 80, Unit.PX); + style.setMarginTop(-scrollBodyPanel.getOffsetHeight(), Unit.PX); + + // indexes go from 1-totalRows, as rowheaders in index-mode indicate + int last = (firstRowInViewPort + pageLength); + if (last > totalRows) { + last = totalRows; + } + scrollPositionElement.setInnerHTML("" + (firstRowInViewPort + 1) + + " – " + (last) + "..." + ""); + style.setDisplay(Display.BLOCK); + } + + void hideScrollPositionAnnotation() { + if (scrollPositionElement != null) { + DOM.setStyleAttribute(scrollPositionElement, "display", "none"); + } + } + + boolean isScrollPositionVisible() { + return scrollPositionElement != null + && !scrollPositionElement.getStyle().getDisplay() + .equals(Display.NONE.toString()); + } + + class RowRequestHandler extends Timer { + + private int reqFirstRow = 0; + private int reqRows = 0; + private boolean isRunning = false; + + public void deferRowFetch() { + deferRowFetch(250); + } + + public boolean isRunning() { + return isRunning; + } + + public void deferRowFetch(int msec) { + isRunning = true; + if (reqRows > 0 && reqFirstRow < totalRows) { + schedule(msec); + + // tell scroll position to user if currently "visible" rows are + // not rendered + if (totalRows > pageLength + && ((firstRowInViewPort + pageLength > scrollBody + .getLastRendered()) || (firstRowInViewPort < scrollBody + .getFirstRendered()))) { + announceScrollPosition(); + } else { + hideScrollPositionAnnotation(); + } + } + } + + public void setReqFirstRow(int reqFirstRow) { + if (reqFirstRow < 0) { + reqFirstRow = 0; + } else if (reqFirstRow >= totalRows) { + reqFirstRow = totalRows - 1; + } + this.reqFirstRow = reqFirstRow; + } + + public void setReqRows(int reqRows) { + this.reqRows = reqRows; + } + + @Override + public void run() { + if (client.hasActiveRequest() || navKeyDown) { + // if client connection is busy, don't bother loading it more + VConsole.log("Postponed rowfetch"); + schedule(250); + } else { + + int firstToBeRendered = scrollBody.firstRendered; + if (reqFirstRow < firstToBeRendered) { + firstToBeRendered = reqFirstRow; + } else if (firstRowInViewPort - (int) (cache_rate * pageLength) > firstToBeRendered) { + firstToBeRendered = firstRowInViewPort + - (int) (cache_rate * pageLength); + if (firstToBeRendered < 0) { + firstToBeRendered = 0; + } + } + + int lastToBeRendered = scrollBody.lastRendered; + + if (reqFirstRow + reqRows - 1 > lastToBeRendered) { + lastToBeRendered = reqFirstRow + reqRows - 1; + } else if (firstRowInViewPort + pageLength + pageLength + * cache_rate < lastToBeRendered) { + lastToBeRendered = (firstRowInViewPort + pageLength + (int) (pageLength * cache_rate)); + if (lastToBeRendered >= totalRows) { + lastToBeRendered = totalRows - 1; + } + // due Safari 3.1 bug (see #2607), verify reqrows, original + // problem unknown, but this should catch the issue + if (reqFirstRow + reqRows - 1 > lastToBeRendered) { + reqRows = lastToBeRendered - reqFirstRow; + } + } + + client.updateVariable(paintableId, "firstToBeRendered", + firstToBeRendered, false); + + client.updateVariable(paintableId, "lastToBeRendered", + lastToBeRendered, false); + // remember which firstvisible we requested, in case the server + // has + // a differing opinion + lastRequestedFirstvisible = firstRowInViewPort; + client.updateVariable(paintableId, "firstvisible", + firstRowInViewPort, false); + client.updateVariable(paintableId, "reqfirstrow", reqFirstRow, + false); + client.updateVariable(paintableId, "reqrows", reqRows, true); + + if (selectionChanged) { + unSyncedselectionsBeforeRowFetch = new HashSet( + selectedRowKeys); + } + isRunning = false; + } + } + + public int getReqFirstRow() { + return reqFirstRow; + } + + /** + * Sends request to refresh content at this position. + */ + public void refreshContent() { + isRunning = true; + int first = (int) (firstRowInViewPort - pageLength * cache_rate); + int reqRows = (int) (2 * pageLength * cache_rate + pageLength); + if (first < 0) { + reqRows = reqRows + first; + first = 0; + } + setReqFirstRow(first); + setReqRows(reqRows); + run(); + } + } + + public class HeaderCell extends Widget { + + Element td = DOM.createTD(); + + Element captionContainer = DOM.createDiv(); + + Element sortIndicator = DOM.createDiv(); + + Element colResizeWidget = DOM.createDiv(); + + Element floatingCopyOfHeaderCell; + + private boolean sortable = false; + private final String cid; + private boolean dragging; + + private int dragStartX; + private int colIndex; + private int originalWidth; + + private boolean isResizing; + + private int headerX; + + private boolean moved; + + private int closestSlot; + + private int width = -1; + + private int naturalWidth = -1; + + private char align = ALIGN_LEFT; + + boolean definedWidth = false; + + private float expandRatio = 0; + + private boolean sorted; + + public void setSortable(boolean b) { + sortable = b; + } + + /** + * Makes room for the sorting indicator in case the column that the + * header cell belongs to is sorted. This is done by resizing the width + * of the caption container element by the correct amount + */ + public void resizeCaptionContainer(int rightSpacing) { + int captionContainerWidth = width + - colResizeWidget.getOffsetWidth() - rightSpacing; + + if (td.getClassName().contains("-asc") + || td.getClassName().contains("-desc")) { + // Leave room for the sort indicator + captionContainerWidth -= sortIndicator.getOffsetWidth(); + } + + if (captionContainerWidth < 0) { + rightSpacing += captionContainerWidth; + captionContainerWidth = 0; + } + + captionContainer.getStyle().setPropertyPx("width", + captionContainerWidth); + + // Apply/Remove spacing if defined + if (rightSpacing > 0) { + colResizeWidget.getStyle().setMarginLeft(rightSpacing, Unit.PX); + } else { + colResizeWidget.getStyle().clearMarginLeft(); + } + } + + public void setNaturalMinimumColumnWidth(int w) { + naturalWidth = w; + } + + public HeaderCell(String colId, String headerText) { + cid = colId; + + DOM.setElementProperty(colResizeWidget, "className", CLASSNAME + + "-resizer"); + + setText(headerText); + + DOM.appendChild(td, colResizeWidget); + + DOM.setElementProperty(sortIndicator, "className", CLASSNAME + + "-sort-indicator"); + DOM.appendChild(td, sortIndicator); + + DOM.setElementProperty(captionContainer, "className", CLASSNAME + + "-caption-container"); + + // ensure no clipping initially (problem on column additions) + DOM.setStyleAttribute(captionContainer, "overflow", "visible"); + + DOM.appendChild(td, captionContainer); + + DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK + | Event.ONCONTEXTMENU | Event.TOUCHEVENTS); + + setElement(td); + + setAlign(ALIGN_LEFT); + } + + public void disableAutoWidthCalculation() { + definedWidth = true; + expandRatio = 0; + } + + public void setWidth(int w, boolean ensureDefinedWidth) { + if (ensureDefinedWidth) { + definedWidth = true; + // on column resize expand ratio becomes zero + expandRatio = 0; + } + if (width == -1) { + // go to default mode, clip content if necessary + DOM.setStyleAttribute(captionContainer, "overflow", ""); + } + width = w; + if (w == -1) { + DOM.setStyleAttribute(captionContainer, "width", ""); + setWidth(""); + } else { + tHead.resizeCaptionContainer(this); + + /* + * if we already have tBody, set the header width properly, if + * not defer it. IE will fail with complex float in table header + * unless TD width is not explicitly set. + */ + if (scrollBody != null) { + int tdWidth = width + scrollBody.getCellExtraWidth(); + setWidth(tdWidth + "px"); + } else { + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + int tdWidth = width + + scrollBody.getCellExtraWidth(); + setWidth(tdWidth + "px"); + } + }); + } + } + } + + public void setUndefinedWidth() { + definedWidth = false; + setWidth(-1, false); + } + + /** + * Detects if width is fixed by developer on server side or resized to + * current width by user. + * + * @return true if defined, false if "natural" width + */ + public boolean isDefinedWidth() { + return definedWidth && width >= 0; + } + + public int getWidth() { + return width; + } + + public void setText(String headerText) { + DOM.setInnerHTML(captionContainer, headerText); + } + + public String getColKey() { + return cid; + } + + private void setSorted(boolean sorted) { + this.sorted = sorted; + if (sorted) { + if (sortAscending) { + this.setStyleName(CLASSNAME + "-header-cell-asc"); + } else { + this.setStyleName(CLASSNAME + "-header-cell-desc"); + } + } else { + this.setStyleName(CLASSNAME + "-header-cell"); + } + } + + /** + * Handle column reordering. + */ + @Override + public void onBrowserEvent(Event event) { + if (enabled && event != null) { + if (isResizing + || event.getEventTarget().cast() == colResizeWidget) { + if (dragging + && (event.getTypeInt() == Event.ONMOUSEUP || event + .getTypeInt() == Event.ONTOUCHEND)) { + // Handle releasing column header on spacer #5318 + handleCaptionEvent(event); + } else { + onResizeEvent(event); + } + } else { + /* + * Ensure focus before handling caption event. Otherwise + * variables changed from caption event may be before + * variables from other components that fire variables when + * they lose focus. + */ + if (event.getTypeInt() == Event.ONMOUSEDOWN + || event.getTypeInt() == Event.ONTOUCHSTART) { + scrollBodyPanel.setFocus(true); + } + handleCaptionEvent(event); + boolean stopPropagation = true; + if (event.getTypeInt() == Event.ONCONTEXTMENU + && !client.hasEventListeners(VScrollTable.this, + HEADER_CLICK_EVENT_ID)) { + // Prevent showing the browser's context menu only when + // there is a header click listener. + stopPropagation = false; + } + if (stopPropagation) { + event.stopPropagation(); + event.preventDefault(); + } + } + } + } + + private void createFloatingCopy() { + floatingCopyOfHeaderCell = DOM.createDiv(); + DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td)); + floatingCopyOfHeaderCell = DOM + .getChild(floatingCopyOfHeaderCell, 2); + DOM.setElementProperty(floatingCopyOfHeaderCell, "className", + CLASSNAME + "-header-drag"); + // otherwise might wrap or be cut if narrow column + DOM.setStyleAttribute(floatingCopyOfHeaderCell, "width", "auto"); + updateFloatingCopysPosition(DOM.getAbsoluteLeft(td), + DOM.getAbsoluteTop(td)); + DOM.appendChild(RootPanel.get().getElement(), + floatingCopyOfHeaderCell); + } + + private void updateFloatingCopysPosition(int x, int y) { + x -= DOM.getElementPropertyInt(floatingCopyOfHeaderCell, + "offsetWidth") / 2; + DOM.setStyleAttribute(floatingCopyOfHeaderCell, "left", x + "px"); + if (y > 0) { + DOM.setStyleAttribute(floatingCopyOfHeaderCell, "top", (y + 7) + + "px"); + } + } + + private void hideFloatingCopy() { + DOM.removeChild(RootPanel.get().getElement(), + floatingCopyOfHeaderCell); + floatingCopyOfHeaderCell = null; + } + + /** + * Fires a header click event after the user has clicked a column header + * cell + * + * @param event + * The click event + */ + private void fireHeaderClickedEvent(Event event) { + if (client.hasEventListeners(VScrollTable.this, + HEADER_CLICK_EVENT_ID)) { + MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(event); + client.updateVariable(paintableId, "headerClickEvent", + details.toString(), false); + client.updateVariable(paintableId, "headerClickCID", cid, true); + } + } + + protected void handleCaptionEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONTOUCHSTART: + case Event.ONMOUSEDOWN: + if (columnReordering + && Util.isTouchEventOrLeftMouseButton(event)) { + if (event.getTypeInt() == Event.ONTOUCHSTART) { + /* + * prevent using this event in e.g. scrolling + */ + event.stopPropagation(); + } + dragging = true; + moved = false; + colIndex = getColIndexByKey(cid); + DOM.setCapture(getElement()); + headerX = tHead.getAbsoluteLeft(); + event.preventDefault(); // prevent selecting text && + // generated touch events + } + break; + case Event.ONMOUSEUP: + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + if (columnReordering + && Util.isTouchEventOrLeftMouseButton(event)) { + dragging = false; + DOM.releaseCapture(getElement()); + if (moved) { + hideFloatingCopy(); + tHead.removeSlotFocus(); + if (closestSlot != colIndex + && closestSlot != (colIndex + 1)) { + if (closestSlot > colIndex) { + reOrderColumn(cid, closestSlot - 1); + } else { + reOrderColumn(cid, closestSlot); + } + } + } + if (Util.isTouchEvent(event)) { + /* + * Prevent using in e.g. scrolling and prevent generated + * events. + */ + event.preventDefault(); + event.stopPropagation(); + } + } + + if (!moved) { + // mouse event was a click to header -> sort column + if (sortable && Util.isTouchEventOrLeftMouseButton(event)) { + if (sortColumn.equals(cid)) { + // just toggle order + client.updateVariable(paintableId, "sortascending", + !sortAscending, false); + } else { + // set table sorted by this column + client.updateVariable(paintableId, "sortcolumn", + cid, false); + } + // get also cache columns at the same request + scrollBodyPanel.setScrollPosition(0); + firstvisible = 0; + rowRequestHandler.setReqFirstRow(0); + rowRequestHandler.setReqRows((int) (2 * pageLength + * cache_rate + pageLength)); + rowRequestHandler.deferRowFetch(); // some validation + + // defer 250ms + rowRequestHandler.cancel(); // instead of waiting + rowRequestHandler.run(); // run immediately + } + fireHeaderClickedEvent(event); + if (Util.isTouchEvent(event)) { + /* + * Prevent using in e.g. scrolling and prevent generated + * events. + */ + event.preventDefault(); + event.stopPropagation(); + } + break; + } + break; + case Event.ONDBLCLICK: + fireHeaderClickedEvent(event); + break; + case Event.ONTOUCHMOVE: + case Event.ONMOUSEMOVE: + if (dragging && Util.isTouchEventOrLeftMouseButton(event)) { + if (event.getTypeInt() == Event.ONTOUCHMOVE) { + /* + * prevent using this event in e.g. scrolling + */ + event.stopPropagation(); + } + if (!moved) { + createFloatingCopy(); + moved = true; + } + + final int clientX = Util.getTouchOrMouseClientX(event); + final int x = clientX + tHead.hTableWrapper.getScrollLeft(); + int slotX = headerX; + closestSlot = colIndex; + int closestDistance = -1; + int start = 0; + if (showRowHeaders) { + start++; + } + final int visibleCellCount = tHead.getVisibleCellCount(); + for (int i = start; i <= visibleCellCount; i++) { + if (i > 0) { + final String colKey = getColKeyByIndex(i - 1); + slotX += getColWidth(colKey); + } + final int dist = Math.abs(x - slotX); + if (closestDistance == -1 || dist < closestDistance) { + closestDistance = dist; + closestSlot = i; + } + } + tHead.focusSlot(closestSlot); + + updateFloatingCopysPosition(clientX, -1); + } + break; + default: + break; + } + } + + private void onResizeEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEDOWN: + if (!Util.isTouchEventOrLeftMouseButton(event)) { + return; + } + isResizing = true; + DOM.setCapture(getElement()); + dragStartX = DOM.eventGetClientX(event); + colIndex = getColIndexByKey(cid); + originalWidth = getWidth(); + DOM.eventPreventDefault(event); + break; + case Event.ONMOUSEUP: + if (!Util.isTouchEventOrLeftMouseButton(event)) { + return; + } + isResizing = false; + DOM.releaseCapture(getElement()); + tHead.disableAutoColumnWidthCalculation(this); + + // Ensure last header cell is taking into account possible + // column selector + HeaderCell lastCell = tHead.getHeaderCell(tHead + .getVisibleCellCount() - 1); + tHead.resizeCaptionContainer(lastCell); + triggerLazyColumnAdjustment(true); + + fireColumnResizeEvent(cid, originalWidth, getColWidth(cid)); + break; + case Event.ONMOUSEMOVE: + if (!Util.isTouchEventOrLeftMouseButton(event)) { + return; + } + if (isResizing) { + final int deltaX = DOM.eventGetClientX(event) - dragStartX; + if (deltaX == 0) { + return; + } + tHead.disableAutoColumnWidthCalculation(this); + + int newWidth = originalWidth + deltaX; + if (newWidth < getMinWidth()) { + newWidth = getMinWidth(); + } + setColWidth(colIndex, newWidth, true); + triggerLazyColumnAdjustment(false); + forceRealignColumnHeaders(); + } + break; + default: + break; + } + } + + public int getMinWidth() { + int cellExtraWidth = 0; + if (scrollBody != null) { + cellExtraWidth += scrollBody.getCellExtraWidth(); + } + return cellExtraWidth + sortIndicator.getOffsetWidth(); + } + + public String getCaption() { + return DOM.getInnerText(captionContainer); + } + + public boolean isEnabled() { + return getParent() != null; + } + + public void setAlign(char c) { + final String ALIGN_PREFIX = CLASSNAME + "-caption-container-align-"; + if (align != c) { + captionContainer.removeClassName(ALIGN_PREFIX + "center"); + captionContainer.removeClassName(ALIGN_PREFIX + "right"); + captionContainer.removeClassName(ALIGN_PREFIX + "left"); + switch (c) { + case ALIGN_CENTER: + captionContainer.addClassName(ALIGN_PREFIX + "center"); + break; + case ALIGN_RIGHT: + captionContainer.addClassName(ALIGN_PREFIX + "right"); + break; + default: + captionContainer.addClassName(ALIGN_PREFIX + "left"); + break; + } + } + align = c; + } + + public char getAlign() { + return align; + } + + /** + * Detects the natural minimum width for the column of this header cell. + * If column is resized by user or the width is defined by server the + * actual width is returned. Else the natural min width is returned. + * + * @param columnIndex + * column index hint, if -1 (unknown) it will be detected + * + * @return + */ + public int getNaturalColumnWidth(int columnIndex) { + if (isDefinedWidth()) { + return width; + } else { + if (naturalWidth < 0) { + // This is recently revealed column. Try to detect a proper + // value (greater of header and data + // cols) + + int hw = captionContainer.getOffsetWidth() + + scrollBody.getCellExtraWidth(); + if (BrowserInfo.get().isGecko()) { + hw += sortIndicator.getOffsetWidth(); + } + if (columnIndex < 0) { + columnIndex = 0; + for (Iterator it = tHead.iterator(); it + .hasNext(); columnIndex++) { + if (it.next() == this) { + break; + } + } + } + final int cw = scrollBody.getColWidth(columnIndex); + naturalWidth = (hw > cw ? hw : cw); + } + return naturalWidth; + } + } + + public void setExpandRatio(float floatAttribute) { + if (floatAttribute != expandRatio) { + triggerLazyColumnAdjustment(false); + } + expandRatio = floatAttribute; + } + + public float getExpandRatio() { + return expandRatio; + } + + public boolean isSorted() { + return sorted; + } + } + + /** + * HeaderCell that is header cell for row headers. + * + * Reordering disabled and clicking on it resets sorting. + */ + public class RowHeadersHeaderCell extends HeaderCell { + + RowHeadersHeaderCell() { + super(ROW_HEADER_COLUMN_KEY, ""); + this.setStyleName(CLASSNAME + "-header-cell-rowheader"); + } + + @Override + protected void handleCaptionEvent(Event event) { + // NOP: RowHeaders cannot be reordered + // TODO It'd be nice to reset sorting here + } + } + + public class TableHead extends Panel implements ActionOwner { + + private static final int WRAPPER_WIDTH = 900000; + + ArrayList visibleCells = new ArrayList(); + + HashMap availableCells = new HashMap(); + + Element div = DOM.createDiv(); + Element hTableWrapper = DOM.createDiv(); + Element hTableContainer = DOM.createDiv(); + Element table = DOM.createTable(); + Element headerTableBody = DOM.createTBody(); + Element tr = DOM.createTR(); + + private final Element columnSelector = DOM.createDiv(); + + private int focusedSlot = -1; + + public TableHead() { + if (BrowserInfo.get().isIE()) { + table.setPropertyInt("cellSpacing", 0); + } + + DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden"); + DOM.setElementProperty(hTableWrapper, "className", CLASSNAME + + "-header"); + + // TODO move styles to CSS + DOM.setElementProperty(columnSelector, "className", CLASSNAME + + "-column-selector"); + DOM.setStyleAttribute(columnSelector, "display", "none"); + + DOM.appendChild(table, headerTableBody); + DOM.appendChild(headerTableBody, tr); + DOM.appendChild(hTableContainer, table); + DOM.appendChild(hTableWrapper, hTableContainer); + DOM.appendChild(div, hTableWrapper); + DOM.appendChild(div, columnSelector); + setElement(div); + + setStyleName(CLASSNAME + "-header-wrap"); + + DOM.sinkEvents(columnSelector, Event.ONCLICK); + + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersHeaderCell()); + } + + public void resizeCaptionContainer(HeaderCell cell) { + HeaderCell lastcell = getHeaderCell(visibleCells.size() - 1); + + // Measure column widths + int columnTotalWidth = 0; + for (Widget w : visibleCells) { + columnTotalWidth += w.getOffsetWidth(); + } + + if (cell == lastcell + && columnSelector.getOffsetWidth() > 0 + && columnTotalWidth >= div.getOffsetWidth() + - columnSelector.getOffsetWidth() + && !hasVerticalScrollbar()) { + // Ensure column caption is visible when placed under the column + // selector widget by shifting and resizing the caption. + int offset = 0; + int diff = div.getOffsetWidth() - columnTotalWidth; + if (diff < columnSelector.getOffsetWidth() && diff > 0) { + // If the difference is less than the column selectors width + // then just offset by the + // difference + offset = columnSelector.getOffsetWidth() - diff; + } else { + // Else offset by the whole column selector + offset = columnSelector.getOffsetWidth(); + } + lastcell.resizeCaptionContainer(offset); + } else { + cell.resizeCaptionContainer(0); + } + } + + @Override + public void clear() { + for (String cid : availableCells.keySet()) { + removeCell(cid); + } + availableCells.clear(); + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersHeaderCell()); + } + + public void updateCellsFromUIDL(UIDL uidl) { + Iterator it = uidl.getChildIterator(); + HashSet updated = new HashSet(); + boolean refreshContentWidths = false; + while (it.hasNext()) { + final UIDL col = (UIDL) it.next(); + final String cid = col.getStringAttribute("cid"); + updated.add(cid); + + String caption = buildCaptionHtmlSnippet(col); + HeaderCell c = getHeaderCell(cid); + if (c == null) { + c = new HeaderCell(cid, caption); + availableCells.put(cid, c); + if (initializedAndAttached) { + // we will need a column width recalculation + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } else { + c.setText(caption); + } + + if (col.hasAttribute("sortable")) { + c.setSortable(true); + if (cid.equals(sortColumn)) { + c.setSorted(true); + } else { + c.setSorted(false); + } + } else { + c.setSortable(false); + } + + if (col.hasAttribute("align")) { + c.setAlign(col.getStringAttribute("align").charAt(0)); + } else { + c.setAlign(ALIGN_LEFT); + + } + if (col.hasAttribute("width")) { + final String widthStr = col.getStringAttribute("width"); + // Make sure to accomodate for the sort indicator if + // necessary. + int width = Integer.parseInt(widthStr); + if (width < c.getMinWidth()) { + width = c.getMinWidth(); + } + if (width != c.getWidth() && scrollBody != null) { + // Do a more thorough update if a column is resized from + // the server *after* the header has been properly + // initialized + final int colIx = getColIndexByKey(c.cid); + final int newWidth = width; + Scheduler.get().scheduleDeferred( + new ScheduledCommand() { + @Override + public void execute() { + setColWidth(colIx, newWidth, true); + } + }); + refreshContentWidths = true; + } else { + c.setWidth(width, true); + } + } else if (recalcWidths) { + c.setUndefinedWidth(); + } + if (col.hasAttribute("er")) { + c.setExpandRatio(col.getFloatAttribute("er")); + } + if (col.hasAttribute("collapsed")) { + // ensure header is properly removed from parent (case when + // collapsing happens via servers side api) + if (c.isAttached()) { + c.removeFromParent(); + headerChangedDuringUpdate = true; + } + } + } + + if (refreshContentWidths) { + // Recalculate the column sizings if any column has changed + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + triggerLazyColumnAdjustment(true); + } + }); + } + + // check for orphaned header cells + for (Iterator cit = availableCells.keySet().iterator(); cit + .hasNext();) { + String cid = cit.next(); + if (!updated.contains(cid)) { + removeCell(cid); + cit.remove(); + // we will need a column width recalculation, since columns + // with expand ratios should expand to fill the void. + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } + } + + public void enableColumn(String cid, int index) { + final HeaderCell c = getHeaderCell(cid); + if (!c.isEnabled() || getHeaderCell(index) != c) { + setHeaderCell(index, c); + if (initializedAndAttached) { + headerChangedDuringUpdate = true; + } + } + } + + public int getVisibleCellCount() { + return visibleCells.size(); + } + + public void setHorizontalScrollPosition(int scrollLeft) { + hTableWrapper.setScrollLeft(scrollLeft); + } + + public void setColumnCollapsingAllowed(boolean cc) { + if (cc) { + columnSelector.getStyle().setDisplay(Display.BLOCK); + } else { + columnSelector.getStyle().setDisplay(Display.NONE); + } + } + + public void disableBrowserIntelligence() { + hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX); + } + + public void enableBrowserIntelligence() { + hTableContainer.getStyle().clearWidth(); + } + + public void setHeaderCell(int index, HeaderCell cell) { + if (cell.isEnabled()) { + // we're moving the cell + DOM.removeChild(tr, cell.getElement()); + orphan(cell); + visibleCells.remove(cell); + } + if (index < visibleCells.size()) { + // insert to right slot + DOM.insertChild(tr, cell.getElement(), index); + adopt(cell); + visibleCells.add(index, cell); + } else if (index == visibleCells.size()) { + // simply append + DOM.appendChild(tr, cell.getElement()); + adopt(cell); + visibleCells.add(cell); + } else { + throw new RuntimeException( + "Header cells must be appended in order"); + } + } + + public HeaderCell getHeaderCell(int index) { + if (index >= 0 && index < visibleCells.size()) { + return (HeaderCell) visibleCells.get(index); + } else { + return null; + } + } + + /** + * Get's HeaderCell by it's column Key. + * + * Note that this returns HeaderCell even if it is currently collapsed. + * + * @param cid + * Column key of accessed HeaderCell + * @return HeaderCell + */ + public HeaderCell getHeaderCell(String cid) { + return availableCells.get(cid); + } + + public void moveCell(int oldIndex, int newIndex) { + final HeaderCell hCell = getHeaderCell(oldIndex); + final Element cell = hCell.getElement(); + + visibleCells.remove(oldIndex); + DOM.removeChild(tr, cell); + + DOM.insertChild(tr, cell, newIndex); + visibleCells.add(newIndex, hCell); + } + + @Override + public Iterator iterator() { + return visibleCells.iterator(); + } + + @Override + public boolean remove(Widget w) { + if (visibleCells.contains(w)) { + visibleCells.remove(w); + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), w.getElement()); + return true; + } + return false; + } + + public void removeCell(String colKey) { + final HeaderCell c = getHeaderCell(colKey); + remove(c); + } + + private void focusSlot(int index) { + removeSlotFocus(); + if (index > 0) { + DOM.setElementProperty( + DOM.getFirstChild(DOM.getChild(tr, index - 1)), + "className", CLASSNAME + "-resizer " + CLASSNAME + + "-focus-slot-right"); + } else { + DOM.setElementProperty( + DOM.getFirstChild(DOM.getChild(tr, index)), + "className", CLASSNAME + "-resizer " + CLASSNAME + + "-focus-slot-left"); + } + focusedSlot = index; + } + + private void removeSlotFocus() { + if (focusedSlot < 0) { + return; + } + if (focusedSlot == 0) { + DOM.setElementProperty( + DOM.getFirstChild(DOM.getChild(tr, focusedSlot)), + "className", CLASSNAME + "-resizer"); + } else if (focusedSlot > 0) { + DOM.setElementProperty( + DOM.getFirstChild(DOM.getChild(tr, focusedSlot - 1)), + "className", CLASSNAME + "-resizer"); + } + focusedSlot = -1; + } + + @Override + public void onBrowserEvent(Event event) { + if (enabled) { + if (event.getEventTarget().cast() == columnSelector) { + final int left = DOM.getAbsoluteLeft(columnSelector); + final int top = DOM.getAbsoluteTop(columnSelector) + + DOM.getElementPropertyInt(columnSelector, + "offsetHeight"); + client.getContextMenu().showAt(this, left, top); + } + } + } + + @Override + protected void onDetach() { + super.onDetach(); + if (client != null) { + client.getContextMenu().ensureHidden(this); + } + } + + class VisibleColumnAction extends Action { + + String colKey; + private boolean collapsed; ++ private 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(""); ++ buf.append("v-off"); + } else { - buf.append(""); ++ buf.append("v-on"); ++ } ++ if (noncollapsible) { ++ buf.append(" v-disabled"); + } ++ buf.append("\">"); ++ + buf.append(super.getHTML()); + buf.append(""); + + 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 it = collapsedColumns.iterator(); it + .hasNext();) { + cols[i++] = it.next(); + } + } + final Action[] actions = new Action[cols.length]; + + for (int i = 0; i < cols.length; i++) { + final String cid = (String) cols[i]; + final HeaderCell c = getHeaderCell(cid); + final VisibleColumnAction a = new VisibleColumnAction( + c.getColKey()); + a.setCaption(c.getCaption()); + if (!c.isEnabled()) { + a.setCollapsed(true); + } ++ if (noncollapsibleColumns.contains(cid)) { ++ a.setNoncollapsible(true); ++ } + actions[i] = a; + } + return actions; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + /** + * Returns column alignments for visible columns + */ + public char[] getColumnAlignments() { + final Iterator it = visibleCells.iterator(); + final char[] aligns = new char[visibleCells.size()]; + int colIndex = 0; + while (it.hasNext()) { + aligns[colIndex++] = ((HeaderCell) it.next()).getAlign(); + } + return aligns; + } + + /** + * Disables the automatic calculation of all column widths by forcing + * the widths to be "defined" thus turning off expand ratios and such. + */ + public void disableAutoColumnWidthCalculation(HeaderCell source) { + for (HeaderCell cell : availableCells.values()) { + cell.disableAutoWidthCalculation(); + } + // fire column resize events for all columns but the source of the + // resize action, since an event will fire separately for this. + ArrayList columns = new ArrayList( + availableCells.values()); + columns.remove(source); + sendColumnWidthUpdates(columns); + forceRealignColumnHeaders(); + } + } + + /** + * A cell in the footer + */ + public class FooterCell extends Widget { + private final Element td = DOM.createTD(); + private final Element captionContainer = DOM.createDiv(); + private char align = ALIGN_LEFT; + private int width = -1; + private float expandRatio = 0; + private final String cid; + boolean definedWidth = false; + private int naturalWidth = -1; + + public FooterCell(String colId, String headerText) { + cid = colId; + + setText(headerText); + + DOM.setElementProperty(captionContainer, "className", CLASSNAME + + "-footer-container"); + + // ensure no clipping initially (problem on column additions) + DOM.setStyleAttribute(captionContainer, "overflow", "visible"); + + DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS); + + DOM.appendChild(td, captionContainer); + + DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK + | Event.ONCONTEXTMENU); + + setElement(td); + } + + /** + * Sets the text of the footer + * + * @param footerText + * The text in the footer + */ + public void setText(String footerText) { + 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: + DOM.setStyleAttribute(captionContainer, "textAlign", + "center"); + break; + case ALIGN_RIGHT: + DOM.setStyleAttribute(captionContainer, "textAlign", + "right"); + break; + default: + DOM.setStyleAttribute(captionContainer, "textAlign", ""); + break; + } + } + align = c; + } + + /** + * Get the alignment of the text int the cell + * + * @return Returns either ALIGN_CENTER, ALIGN_LEFT or ALIGN_RIGHT + */ + public char getAlign() { + return align; + } + + /** + * Sets the width of the cell + * + * @param w + * The width of the cell + * @param ensureDefinedWidth + * Ensures the the given width is not recalculated + */ + public void setWidth(int w, boolean ensureDefinedWidth) { + + if (ensureDefinedWidth) { + definedWidth = true; + // on column resize expand ratio becomes zero + expandRatio = 0; + } + if (width == w) { + return; + } + if (width == -1) { + // go to default mode, clip content if necessary + DOM.setStyleAttribute(captionContainer, "overflow", ""); + } + width = w; + if (w == -1) { + DOM.setStyleAttribute(captionContainer, "width", ""); + setWidth(""); + } else { + + /* + * Reduce width with one pixel for the right border since the + * footers does not have any spacers between them. + */ + int borderWidths = 1; + + // Set the container width (check for negative value) + if (w - borderWidths >= 0) { + captionContainer.getStyle().setPropertyPx("width", + w - borderWidths); + } else { + captionContainer.getStyle().setPropertyPx("width", 0); + } + + /* + * if we already have tBody, set the header width properly, if + * not defer it. IE will fail with complex float in table header + * unless TD width is not explicitly set. + */ + if (scrollBody != null) { + /* + * Reduce with one since footer does not have any spacers, + * instead a 1 pixel border. + */ + int tdWidth = width + scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(tdWidth + "px"); + } else { + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + int borderWidths = 1; + int tdWidth = width + + scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(tdWidth + "px"); + } + }); + } + } + } + + /** + * Sets the width to undefined + */ + public void setUndefinedWidth() { + setWidth(-1, false); + } + + /** + * Detects if width is fixed by developer on server side or resized to + * current width by user. + * + * @return true if defined, false if "natural" width + */ + public boolean isDefinedWidth() { + return definedWidth && width >= 0; + } + + /** + * Returns the pixels width of the footer cell + * + * @return The width in pixels + */ + public int getWidth() { + return width; + } + + /** + * Sets the expand ratio of the cell + * + * @param floatAttribute + * The expand ratio + */ + public void setExpandRatio(float floatAttribute) { + expandRatio = floatAttribute; + } + + /** + * Returns the expand ration of the cell + * + * @return The expand ratio + */ + public float getExpandRatio() { + return expandRatio; + } + + /** + * Is the cell enabled? + * + * @return True if enabled else False + */ + public boolean isEnabled() { + return getParent() != null; + } + + /** + * Handle column clicking + */ + + @Override + public void onBrowserEvent(Event event) { + if (enabled && event != null) { + handleCaptionEvent(event); + + if (DOM.eventGetType(event) == Event.ONMOUSEUP) { + scrollBodyPanel.setFocus(true); + } + boolean stopPropagation = true; + if (event.getTypeInt() == Event.ONCONTEXTMENU + && !client.hasEventListeners(VScrollTable.this, + FOOTER_CLICK_EVENT_ID)) { + // Show browser context menu if a footer click listener is + // not present + stopPropagation = false; + } + if (stopPropagation) { + event.stopPropagation(); + event.preventDefault(); + } + } + } + + /** + * Handles a event on the captions + * + * @param event + * The event to handle + */ + protected void handleCaptionEvent(Event event) { + if (event.getTypeInt() == Event.ONMOUSEUP + || event.getTypeInt() == Event.ONDBLCLICK) { + fireFooterClickedEvent(event); + } + } + + /** + * Fires a footer click event after the user has clicked a column footer + * cell + * + * @param event + * The click event + */ + private void fireFooterClickedEvent(Event event) { + if (client.hasEventListeners(VScrollTable.this, + FOOTER_CLICK_EVENT_ID)) { + MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(event); + client.updateVariable(paintableId, "footerClickEvent", + details.toString(), false); + client.updateVariable(paintableId, "footerClickCID", cid, true); + } + } + + /** + * Returns the column key of the column + * + * @return The column key + */ + public String getColKey() { + return cid; + } + + /** + * Detects the natural minimum width for the column of this header cell. + * If column is resized by user or the width is defined by server the + * actual width is returned. Else the natural min width is returned. + * + * @param columnIndex + * column index hint, if -1 (unknown) it will be detected + * + * @return + */ + public int getNaturalColumnWidth(int columnIndex) { + if (isDefinedWidth()) { + return width; + } else { + if (naturalWidth < 0) { + // This is recently revealed column. Try to detect a proper + // value (greater of header and data + // cols) + + final int hw = ((Element) getElement().getLastChild()) + .getOffsetWidth() + scrollBody.getCellExtraWidth(); + if (columnIndex < 0) { + columnIndex = 0; + for (Iterator it = tHead.iterator(); it + .hasNext(); columnIndex++) { + if (it.next() == this) { + break; + } + } + } + final int cw = scrollBody.getColWidth(columnIndex); + naturalWidth = (hw > cw ? hw : cw); + } + return naturalWidth; + } + } + + public void setNaturalMinimumColumnWidth(int w) { + naturalWidth = w; + } + } + + /** + * HeaderCell that is header cell for row headers. + * + * Reordering disabled and clicking on it resets sorting. + */ + public class RowHeadersFooterCell extends FooterCell { + + RowHeadersFooterCell() { + super(ROW_HEADER_COLUMN_KEY, ""); + } + + @Override + protected void handleCaptionEvent(Event event) { + // NOP: RowHeaders cannot be reordered + // TODO It'd be nice to reset sorting here + } + } + + /** + * The footer of the table which can be seen in the bottom of the Table. + */ + public class TableFooter extends Panel { + + private static final int WRAPPER_WIDTH = 900000; + + ArrayList visibleCells = new ArrayList(); + HashMap availableCells = new HashMap(); + + Element div = DOM.createDiv(); + Element hTableWrapper = DOM.createDiv(); + Element hTableContainer = DOM.createDiv(); + Element table = DOM.createTable(); + Element headerTableBody = DOM.createTBody(); + Element tr = DOM.createTR(); + + public TableFooter() { + + DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden"); + DOM.setElementProperty(hTableWrapper, "className", CLASSNAME + + "-footer"); + + DOM.appendChild(table, headerTableBody); + DOM.appendChild(headerTableBody, tr); + DOM.appendChild(hTableContainer, table); + DOM.appendChild(hTableWrapper, hTableContainer); + DOM.appendChild(div, hTableWrapper); + setElement(div); + + setStyleName(CLASSNAME + "-footer-wrap"); + + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersFooterCell()); + } + + @Override + public void clear() { + for (String cid : availableCells.keySet()) { + removeCell(cid); + } + availableCells.clear(); + availableCells.put(ROW_HEADER_COLUMN_KEY, + new RowHeadersFooterCell()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client + * .ui.Widget) + */ + @Override + public boolean remove(Widget w) { + if (visibleCells.contains(w)) { + visibleCells.remove(w); + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), w.getElement()); + return true; + } + return false; + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.HasWidgets#iterator() + */ + @Override + public Iterator iterator() { + return visibleCells.iterator(); + } + + /** + * Gets a footer cell which represents the given columnId + * + * @param cid + * The columnId + * + * @return The cell + */ + public FooterCell getFooterCell(String cid) { + return availableCells.get(cid); + } + + /** + * Gets a footer cell by using a column index + * + * @param index + * The index of the column + * @return The Cell + */ + public FooterCell getFooterCell(int index) { + if (index < visibleCells.size()) { + return (FooterCell) visibleCells.get(index); + } else { + return null; + } + } + + /** + * Updates the cells contents when updateUIDL request is received + * + * @param uidl + * The UIDL + */ + public void updateCellsFromUIDL(UIDL uidl) { + Iterator columnIterator = uidl.getChildIterator(); + HashSet updated = new HashSet(); + while (columnIterator.hasNext()) { + final UIDL col = (UIDL) columnIterator.next(); + final String cid = col.getStringAttribute("cid"); + updated.add(cid); + + String caption = col.hasAttribute("fcaption") ? col + .getStringAttribute("fcaption") : ""; + FooterCell c = getFooterCell(cid); + if (c == null) { + c = new FooterCell(cid, caption); + availableCells.put(cid, c); + if (initializedAndAttached) { + // we will need a column width recalculation + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } else { + c.setText(caption); + } + + if (col.hasAttribute("align")) { + c.setAlign(col.getStringAttribute("align").charAt(0)); + } else { + c.setAlign(ALIGN_LEFT); + + } + if (col.hasAttribute("width")) { + if (scrollBody == null) { + // Already updated by setColWidth called from + // TableHeads.updateCellsFromUIDL in case of a server + // side resize + final String width = col.getStringAttribute("width"); + c.setWidth(Integer.parseInt(width), true); + } + } else if (recalcWidths) { + c.setUndefinedWidth(); + } + if (col.hasAttribute("er")) { + c.setExpandRatio(col.getFloatAttribute("er")); + } + if (col.hasAttribute("collapsed")) { + // ensure header is properly removed from parent (case when + // collapsing happens via servers side api) + if (c.isAttached()) { + c.removeFromParent(); + headerChangedDuringUpdate = true; + } + } + } + + // check for orphaned header cells + for (Iterator cit = availableCells.keySet().iterator(); cit + .hasNext();) { + String cid = cit.next(); + if (!updated.contains(cid)) { + removeCell(cid); + cit.remove(); + } + } + } + + /** + * Set a footer cell for a specified column index + * + * @param index + * The index + * @param cell + * The footer cell + */ + public void setFooterCell(int index, FooterCell cell) { + if (cell.isEnabled()) { + // we're moving the cell + DOM.removeChild(tr, cell.getElement()); + orphan(cell); + visibleCells.remove(cell); + } + if (index < visibleCells.size()) { + // insert to right slot + DOM.insertChild(tr, cell.getElement(), index); + adopt(cell); + visibleCells.add(index, cell); + } else if (index == visibleCells.size()) { + // simply append + DOM.appendChild(tr, cell.getElement()); + adopt(cell); + visibleCells.add(cell); + } else { + throw new RuntimeException( + "Header cells must be appended in order"); + } + } + + /** + * Remove a cell by using the columnId + * + * @param colKey + * The columnId to remove + */ + public void removeCell(String colKey) { + final FooterCell c = getFooterCell(colKey); + remove(c); + } + + /** + * Enable a column (Sets the footer cell) + * + * @param cid + * The columnId + * @param index + * The index of the column + */ + public void enableColumn(String cid, int index) { + final FooterCell c = getFooterCell(cid); + if (!c.isEnabled() || getFooterCell(index) != c) { + setFooterCell(index, c); + if (initializedAndAttached) { + headerChangedDuringUpdate = true; + } + } + } + + /** + * Disable browser measurement of the table width + */ + public void disableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH + + "px"); + } + + /** + * Enable browser measurement of the table width + */ + public void enableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", ""); + } + + /** + * Set the horizontal position in the cell in the footer. This is done + * when a horizontal scrollbar is present. + * + * @param scrollLeft + * The value of the leftScroll + */ + public void setHorizontalScrollPosition(int scrollLeft) { + hTableWrapper.setScrollLeft(scrollLeft); + } + + /** + * Swap cells when the column are dragged + * + * @param oldIndex + * The old index of the cell + * @param newIndex + * The new index of the cell + */ + public void moveCell(int oldIndex, int newIndex) { + final FooterCell hCell = getFooterCell(oldIndex); + final Element cell = hCell.getElement(); + + visibleCells.remove(oldIndex); + DOM.removeChild(tr, cell); + + DOM.insertChild(tr, cell, newIndex); + visibleCells.add(newIndex, hCell); + } + } + + /** + * This Panel can only contain VScrollTableRow type of widgets. This + * "simulates" very large table, keeping spacers which take room of + * unrendered rows. + * + */ + public class VScrollTableBody extends Panel { + + public static final int DEFAULT_ROW_HEIGHT = 24; + + private double rowHeight = -1; + + private final LinkedList renderedRows = new LinkedList(); + + /** + * Due some optimizations row height measuring is deferred and initial + * set of rows is rendered detached. Flag set on when table body has + * been attached in dom and rowheight has been measured. + */ + private boolean tBodyMeasurementsDone = false; + + Element preSpacer = DOM.createDiv(); + Element postSpacer = DOM.createDiv(); + + Element container = DOM.createDiv(); + + TableSectionElement tBodyElement = Document.get().createTBodyElement(); + Element table = DOM.createTable(); + + private int firstRendered; + private int lastRendered; + + private char[] aligns; + + protected VScrollTableBody() { + constructDOM(); + setElement(container); + } + + public VScrollTableRow getRowByRowIndex(int indexInTable) { + int internalIndex = indexInTable - firstRendered; + if (internalIndex >= 0 && internalIndex < renderedRows.size()) { + return (VScrollTableRow) renderedRows.get(internalIndex); + } else { + return null; + } + } + + /** + * @return the height of scrollable body, subpixels ceiled. + */ + public int getRequiredHeight() { + return preSpacer.getOffsetHeight() + postSpacer.getOffsetHeight() + + Util.getRequiredHeight(table); + } + + private void constructDOM() { + DOM.setElementProperty(table, "className", CLASSNAME + "-table"); + if (BrowserInfo.get().isIE()) { + table.setPropertyInt("cellSpacing", 0); + } + DOM.setElementProperty(preSpacer, "className", CLASSNAME + + "-row-spacer"); + DOM.setElementProperty(postSpacer, "className", CLASSNAME + + "-row-spacer"); + + table.appendChild(tBodyElement); + DOM.appendChild(container, preSpacer); + DOM.appendChild(container, table); + DOM.appendChild(container, postSpacer); + if (BrowserInfo.get().requiresTouchScrollDelegate()) { + NodeList childNodes = container.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Element item = (Element) childNodes.getItem(i); + item.getStyle().setProperty("webkitTransform", + "translate3d(0,0,0)"); + } + } + + } + + public int getAvailableWidth() { + int availW = scrollBodyPanel.getOffsetWidth() - getBorderWidth(); + return availW; + } + + public void renderInitialRows(UIDL rowData, int firstIndex, int rows) { + firstRendered = firstIndex; + lastRendered = firstIndex + rows - 1; + final Iterator it = rowData.getChildIterator(); + aligns = tHead.getColumnAlignments(); + while (it.hasNext()) { + final VScrollTableRow row = createRow((UIDL) it.next(), aligns); + addRow(row); + } + if (isAttached()) { + fixSpacers(); + } + } + + public void renderRows(UIDL rowData, int firstIndex, int rows) { + // FIXME REVIEW + aligns = tHead.getColumnAlignments(); + final Iterator it = rowData.getChildIterator(); + if (firstIndex == lastRendered + 1) { + while (it.hasNext()) { + final VScrollTableRow row = prepareRow((UIDL) it.next()); + addRow(row); + lastRendered++; + } + fixSpacers(); + } else if (firstIndex + rows == firstRendered) { + final VScrollTableRow[] rowArray = new VScrollTableRow[rows]; + int i = rows; + while (it.hasNext()) { + i--; + rowArray[i] = prepareRow((UIDL) it.next()); + } + for (i = 0; i < rows; i++) { + addRowBeforeFirstRendered(rowArray[i]); + firstRendered--; + } + } else { + // completely new set of rows + while (lastRendered + 1 > firstRendered) { + unlinkRow(false); + } + final VScrollTableRow row = prepareRow((UIDL) it.next()); + firstRendered = firstIndex; + lastRendered = firstIndex - 1; + addRow(row); + lastRendered++; + setContainerHeight(); + fixSpacers(); + while (it.hasNext()) { + addRow(prepareRow((UIDL) it.next())); + lastRendered++; + } + fixSpacers(); + } + + // this may be a new set of rows due content change, + // ensure we have proper cache rows + ensureCacheFilled(); + } + + /** + * Ensure we have the correct set of rows on client side, e.g. if the + * content on the server side has changed, or the client scroll position + * has changed since the last request. + */ + protected void ensureCacheFilled() { + int reactFirstRow = (int) (firstRowInViewPort - pageLength + * cache_react_rate); + int reactLastRow = (int) (firstRowInViewPort + pageLength + pageLength + * cache_react_rate); + if (reactFirstRow < 0) { + reactFirstRow = 0; + } + if (reactLastRow >= totalRows) { + reactLastRow = totalRows - 1; + } + if (lastRendered < reactFirstRow || firstRendered > reactLastRow) { + /* + * #8040 - scroll position is completely changed since the + * latest request, so request a new set of rows. + * + * TODO: We should probably check whether the fetched rows match + * the current scroll position right when they arrive, so as to + * not waste time rendering a set of rows that will never be + * visible... + */ + rowRequestHandler.setReqFirstRow(reactFirstRow); + rowRequestHandler.setReqRows(reactLastRow - reactFirstRow + 1); + rowRequestHandler.deferRowFetch(1); + } else if (lastRendered < reactLastRow) { + // get some cache rows below visible area + rowRequestHandler.setReqFirstRow(lastRendered + 1); + rowRequestHandler.setReqRows(reactLastRow - lastRendered); + rowRequestHandler.deferRowFetch(1); + } else if (firstRendered > reactFirstRow) { + /* + * Branch for fetching cache above visible area. + * + * If cache needed for both before and after visible area, this + * will be rendered after-cache is received and rendered. So in + * some rare situations the table may make two cache visits to + * server. + */ + rowRequestHandler.setReqFirstRow(reactFirstRow); + rowRequestHandler.setReqRows(firstRendered - reactFirstRow); + rowRequestHandler.deferRowFetch(1); + } + } + + /** + * Inserts rows as provided in the rowData starting at firstIndex. + * + * @param rowData + * @param firstIndex + * @param rows + * the number of rows + * @return a list of the rows added. + */ + protected List insertRows(UIDL rowData, + int firstIndex, int rows) { + aligns = tHead.getColumnAlignments(); + final Iterator it = rowData.getChildIterator(); + List insertedRows = new ArrayList(); + + if (firstIndex == lastRendered + 1) { + while (it.hasNext()) { + final VScrollTableRow row = prepareRow((UIDL) it.next()); + addRow(row); + insertedRows.add(row); + lastRendered++; + } + fixSpacers(); + } else if (firstIndex + rows == firstRendered) { + final VScrollTableRow[] rowArray = new VScrollTableRow[rows]; + int i = rows; + while (it.hasNext()) { + i--; + rowArray[i] = prepareRow((UIDL) it.next()); + } + for (i = 0; i < rows; i++) { + addRowBeforeFirstRendered(rowArray[i]); + insertedRows.add(rowArray[i]); + firstRendered--; + } + } else { + // insert in the middle + int ix = firstIndex; + while (it.hasNext()) { + VScrollTableRow row = prepareRow((UIDL) it.next()); + insertRowAt(row, ix); + insertedRows.add(row); + lastRendered++; + ix++; + } + fixSpacers(); + } + return insertedRows; + } + + protected List insertAndReindexRows(UIDL rowData, + int firstIndex, int rows) { + List inserted = insertRows(rowData, firstIndex, + rows); + int actualIxOfFirstRowAfterInserted = firstIndex + rows + - firstRendered; + for (int ix = actualIxOfFirstRowAfterInserted; ix < renderedRows + .size(); ix++) { + VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix); + r.setIndex(r.getIndex() + rows); + } + setContainerHeight(); + return inserted; + } + + protected void insertRowsDeleteBelow(UIDL rowData, int firstIndex, + int rows) { + unlinkAllRowsStartingAt(firstIndex); + insertRows(rowData, firstIndex, rows); + setContainerHeight(); + } + + /** + * This method is used to instantiate new rows for this table. It + * automatically sets correct widths to rows cells and assigns correct + * client reference for child widgets. + * + * This method can be called only after table has been initialized + * + * @param uidl + */ + private VScrollTableRow prepareRow(UIDL uidl) { + final VScrollTableRow row = createRow(uidl, aligns); + row.initCellWidths(); + return row; + } + + protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) { + if (uidl.hasAttribute("gen_html")) { + // This is a generated row. + return new VScrollTableGeneratedRow(uidl, aligns2); + } + return new VScrollTableRow(uidl, aligns2); + } + + private void addRowBeforeFirstRendered(VScrollTableRow row) { + row.setIndex(firstRendered - 1); + if (row.isSelected()) { + row.addStyleName("v-selected"); + } + tBodyElement.insertBefore(row.getElement(), + tBodyElement.getFirstChild()); + adopt(row); + renderedRows.add(0, row); + } + + private void addRow(VScrollTableRow row) { + row.setIndex(firstRendered + renderedRows.size()); + if (row.isSelected()) { + row.addStyleName("v-selected"); + } + tBodyElement.appendChild(row.getElement()); + adopt(row); + renderedRows.add(row); + } + + private void insertRowAt(VScrollTableRow row, int index) { + row.setIndex(index); + if (row.isSelected()) { + row.addStyleName("v-selected"); + } + if (index > 0) { + VScrollTableRow sibling = getRowByRowIndex(index - 1); + tBodyElement + .insertAfter(row.getElement(), sibling.getElement()); + } else { + VScrollTableRow sibling = getRowByRowIndex(index); + tBodyElement.insertBefore(row.getElement(), + sibling.getElement()); + } + adopt(row); + int actualIx = index - firstRendered; + renderedRows.add(actualIx, row); + } + + @Override + public Iterator iterator() { + return renderedRows.iterator(); + } + + /** + * @return false if couldn't remove row + */ + protected boolean unlinkRow(boolean fromBeginning) { + if (lastRendered - firstRendered < 0) { + return false; + } + int actualIx; + if (fromBeginning) { + actualIx = 0; + firstRendered++; + } else { + actualIx = renderedRows.size() - 1; + lastRendered--; + } + if (actualIx >= 0) { + unlinkRowAtActualIndex(actualIx); + fixSpacers(); + return true; + } + return false; + } + + protected void unlinkRows(int firstIndex, int count) { + if (count < 1) { + return; + } + if (firstRendered > firstIndex + && firstRendered < firstIndex + count) { + firstIndex = firstRendered; + } + int lastIndex = firstIndex + count - 1; + if (lastRendered < lastIndex) { + lastIndex = lastRendered; + } + for (int ix = lastIndex; ix >= firstIndex; ix--) { + unlinkRowAtActualIndex(actualIndex(ix)); + lastRendered--; + } + fixSpacers(); + } + + protected void unlinkAndReindexRows(int firstIndex, int count) { + unlinkRows(firstIndex, count); + int actualFirstIx = firstIndex - firstRendered; + for (int ix = actualFirstIx; ix < renderedRows.size(); ix++) { + VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix); + r.setIndex(r.getIndex() - count); + } + setContainerHeight(); + } + + protected void unlinkAllRowsStartingAt(int index) { + if (firstRendered > index) { + index = firstRendered; + } + for (int ix = renderedRows.size() - 1; ix >= index; ix--) { + unlinkRowAtActualIndex(actualIndex(ix)); + lastRendered--; + } + fixSpacers(); + } + + private int actualIndex(int index) { + return index - firstRendered; + } + + private void unlinkRowAtActualIndex(int index) { + final VScrollTableRow toBeRemoved = (VScrollTableRow) renderedRows + .get(index); + // Unregister row tooltip + client.registerTooltip(VScrollTable.this, toBeRemoved.getElement(), + null); + for (int i = 0; i < toBeRemoved.getElement().getChildCount(); i++) { + // Unregister cell tooltips + Element td = toBeRemoved.getElement().getChild(i).cast(); + client.registerTooltip(VScrollTable.this, td, null); + } + tBodyElement.removeChild(toBeRemoved.getElement()); + orphan(toBeRemoved); + renderedRows.remove(index); + } + + @Override + public boolean remove(Widget w) { + throw new UnsupportedOperationException(); + } + + /** + * Fix container blocks height according to totalRows to avoid + * "bouncing" when scrolling + */ + private void setContainerHeight() { + fixSpacers(); + DOM.setStyleAttribute(container, "height", + measureRowHeightOffset(totalRows) + "px"); + } + + private void fixSpacers() { + int prepx = measureRowHeightOffset(firstRendered); + if (prepx < 0) { + prepx = 0; + } + preSpacer.getStyle().setPropertyPx("height", prepx); + int postpx = measureRowHeightOffset(totalRows - 1) + - measureRowHeightOffset(lastRendered); + if (postpx < 0) { + postpx = 0; + } + postSpacer.getStyle().setPropertyPx("height", postpx); + } + + public double getRowHeight() { + return getRowHeight(false); + } + + public double getRowHeight(boolean forceUpdate) { + if (tBodyMeasurementsDone && !forceUpdate) { + return rowHeight; + } else { + + if (tBodyElement.getRows().getLength() > 0) { + int tableHeight = getTableHeight(); + int rowCount = tBodyElement.getRows().getLength(); + rowHeight = tableHeight / (double) rowCount; + } else { + if (isAttached()) { + // measure row height by adding a dummy row + VScrollTableRow scrollTableRow = new VScrollTableRow(); + tBodyElement.appendChild(scrollTableRow.getElement()); + getRowHeight(forceUpdate); + tBodyElement.removeChild(scrollTableRow.getElement()); + } else { + // TODO investigate if this can never happen anymore + return DEFAULT_ROW_HEIGHT; + } + } + tBodyMeasurementsDone = true; + return rowHeight; + } + } + + public int getTableHeight() { + return table.getOffsetHeight(); + } + + /** + * Returns the width available for column content. + * + * @param columnIndex + * @return + */ + public int getColWidth(int columnIndex) { + if (tBodyMeasurementsDone) { + if (renderedRows.isEmpty()) { + // no rows yet rendered + return 0; + } + for (Widget row : renderedRows) { + if (!(row instanceof VScrollTableGeneratedRow)) { + TableRowElement tr = row.getElement().cast(); + Element wrapperdiv = tr.getCells().getItem(columnIndex) + .getFirstChildElement().cast(); + return wrapperdiv.getOffsetWidth(); + } + } + return 0; + } else { + return 0; + } + } + + /** + * Sets the content width of a column. + * + * Due IE limitation, we must set the width to a wrapper elements inside + * table cells (with overflow hidden, which does not work on td + * elements). + * + * To get this work properly crossplatform, we will also set the width + * of td. + * + * @param colIndex + * @param w + */ + public void setColWidth(int colIndex, int w) { + for (Widget row : renderedRows) { + ((VScrollTableRow) row).setCellWidth(colIndex, w); + } + } + + private int cellExtraWidth = -1; + + /** + * Method to return the space used for cell paddings + border. + */ + private int getCellExtraWidth() { + if (cellExtraWidth < 0) { + detectExtrawidth(); + } + return cellExtraWidth; + } + + private void detectExtrawidth() { + NodeList rows = tBodyElement.getRows(); + if (rows.getLength() == 0) { + /* need to temporary add empty row and detect */ + VScrollTableRow scrollTableRow = new VScrollTableRow(); + tBodyElement.appendChild(scrollTableRow.getElement()); + detectExtrawidth(); + tBodyElement.removeChild(scrollTableRow.getElement()); + } else { + boolean noCells = false; + TableRowElement item = rows.getItem(0); + TableCellElement firstTD = item.getCells().getItem(0); + if (firstTD == null) { + // content is currently empty, we need to add a fake cell + // for measuring + noCells = true; + VScrollTableRow next = (VScrollTableRow) iterator().next(); + boolean sorted = tHead.getHeaderCell(0) != null ? tHead + .getHeaderCell(0).isSorted() : false; + next.addCell(null, "", ALIGN_LEFT, "", true, sorted); + firstTD = item.getCells().getItem(0); + } + com.google.gwt.dom.client.Element wrapper = firstTD + .getFirstChildElement(); + cellExtraWidth = firstTD.getOffsetWidth() + - wrapper.getOffsetWidth(); + if (noCells) { + firstTD.getParentElement().removeChild(firstTD); + } + } + } + + private void reLayoutComponents() { + for (Widget w : this) { + VScrollTableRow r = (VScrollTableRow) w; + for (Widget widget : r) { + client.handleComponentRelativeSize(widget); + } + } + } + + public int getLastRendered() { + return lastRendered; + } + + public int getFirstRendered() { + return firstRendered; + } + + public void moveCol(int oldIndex, int newIndex) { + + // loop all rows and move given index to its new place + final Iterator rows = iterator(); + while (rows.hasNext()) { + final VScrollTableRow row = (VScrollTableRow) rows.next(); + + final Element td = DOM.getChild(row.getElement(), oldIndex); + if (td != null) { + DOM.removeChild(row.getElement(), td); + + DOM.insertChild(row.getElement(), td, newIndex); + } + } + + } + + /** + * Restore row visibility which is set to "none" when the row is + * rendered (due a performance optimization). + */ + private void restoreRowVisibility() { + for (Widget row : renderedRows) { + row.getElement().getStyle().setProperty("visibility", ""); + } + } + + public class VScrollTableRow extends Panel implements ActionOwner { + + private static final int TOUCHSCROLL_TIMEOUT = 100; + private static final int DRAGMODE_MULTIROW = 2; + protected ArrayList childWidgets = new ArrayList(); + private boolean selected = false; + protected final int rowKey; + + private String[] actionKeys = null; + private final TableRowElement rowElement; + private int index; + private Event touchStart; + private static final String ROW_CLASSNAME_EVEN = CLASSNAME + "-row"; + private static final String ROW_CLASSNAME_ODD = CLASSNAME + + "-row-odd"; + private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500; + private Timer contextTouchTimeout; + private Timer dragTouchTimeout; + private int touchStartY; + private int touchStartX; + private boolean isDragging = false; + + private VScrollTableRow(int rowKey) { + this.rowKey = rowKey; + rowElement = Document.get().createTRElement(); + setElement(rowElement); + DOM.sinkEvents(getElement(), Event.MOUSEEVENTS + | Event.TOUCHEVENTS | Event.ONDBLCLICK + | Event.ONCONTEXTMENU | VTooltip.TOOLTIP_EVENTS); + } + + public VScrollTableRow(UIDL uidl, char[] aligns) { + this(uidl.getIntAttribute("key")); + + /* + * Rendering the rows as hidden improves Firefox and Safari + * performance drastically. + */ + getElement().getStyle().setProperty("visibility", "hidden"); + + String rowStyle = uidl.getStringAttribute("rowstyle"); + if (rowStyle != null) { + addStyleName(CLASSNAME + "-row-" + rowStyle); + } + + String rowDescription = uidl.getStringAttribute("rowdescr"); + if (rowDescription != null && !rowDescription.equals("")) { + TooltipInfo info = new TooltipInfo(rowDescription); + client.registerTooltip(VScrollTable.this, rowElement, info); + } else { + // Remove possibly previously set tooltip + client.registerTooltip(VScrollTable.this, rowElement, null); + } + + tHead.getColumnAlignments(); + int col = 0; + int visibleColumnIndex = -1; + + // row header + if (showRowHeaders) { + boolean sorted = tHead.getHeaderCell(col).isSorted(); + addCell(uidl, buildCaptionHtmlSnippet(uidl), aligns[col++], + "rowheader", true, sorted); + visibleColumnIndex++; + } + + if (uidl.hasAttribute("al")) { + actionKeys = uidl.getStringArrayAttribute("al"); + } + + addCellsFromUIDL(uidl, aligns, col, visibleColumnIndex); + + if (uidl.hasAttribute("selected") && !isSelected()) { + toggleSelection(); + } + } + + /** + * Add a dummy row, used for measurements if Table is empty. + */ + public VScrollTableRow() { + this(0); + addStyleName(CLASSNAME + "-row"); + addCell(null, "_", 'b', "", true, false); + } + + protected void initCellWidths() { + final int cells = tHead.getVisibleCellCount(); + for (int i = 0; i < cells; i++) { + int w = VScrollTable.this.getColWidth(getColKeyByIndex(i)); + if (w < 0) { + w = 0; + } + setCellWidth(i, w); + } + } + + protected void setCellWidth(int cellIx, int width) { + final Element cell = DOM.getChild(getElement(), cellIx); + cell.getFirstChildElement().getStyle() + .setPropertyPx("width", width); + cell.getStyle().setPropertyPx("width", width); + } + + protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col, + int visibleColumnIndex) { + final Iterator cells = uidl.getChildIterator(); + while (cells.hasNext()) { + final Object cell = cells.next(); + visibleColumnIndex++; + + String columnId = visibleColOrder[visibleColumnIndex]; + + String style = ""; + if (uidl.hasAttribute("style-" + columnId)) { + style = uidl.getStringAttribute("style-" + columnId); + } + + String description = null; + if (uidl.hasAttribute("descr-" + columnId)) { + description = uidl.getStringAttribute("descr-" + + columnId); + } + + boolean sorted = tHead.getHeaderCell(col).isSorted(); + if (cell instanceof String) { + addCell(uidl, cell.toString(), aligns[col++], style, + isRenderHtmlInCells(), sorted, description); + } else { + final ComponentConnector cellContent = client + .getPaintable((UIDL) cell); + + addCell(uidl, cellContent.getWidget(), aligns[col++], + style, sorted); + } + } + } + + /** + * Overriding this and returning true causes all text cells to be + * rendered as HTML. + * + * @return always returns false in the default implementation + */ + protected boolean isRenderHtmlInCells() { + return false; + } + + /** + * Detects whether row is visible in tables viewport. + * + * @return + */ + public boolean isInViewPort() { + int absoluteTop = getAbsoluteTop(); + int scrollPosition = scrollBodyPanel.getScrollPosition(); + if (absoluteTop < scrollPosition) { + return false; + } + int maxVisible = scrollPosition + + scrollBodyPanel.getOffsetHeight() - getOffsetHeight(); + if (absoluteTop > maxVisible) { + return false; + } + return true; + } + + /** + * Makes a check based on indexes whether the row is before the + * compared row. + * + * @param row1 + * @return true if this rows index is smaller than in the row1 + */ + public boolean isBefore(VScrollTableRow row1) { + return getIndex() < row1.getIndex(); + } + + /** + * Sets the index of the row in the whole table. Currently used just + * to set even/odd classname + * + * @param indexInWholeTable + */ + private void setIndex(int indexInWholeTable) { + index = indexInWholeTable; + boolean isOdd = indexInWholeTable % 2 == 0; + // Inverted logic to be backwards compatible with earlier 6.4. + // It is very strange because rows 1,3,5 are considered "even" + // and 2,4,6 "odd". + // + // First remove any old styles so that both styles aren't + // applied when indexes are updated. + removeStyleName(ROW_CLASSNAME_ODD); + removeStyleName(ROW_CLASSNAME_EVEN); + if (!isOdd) { + addStyleName(ROW_CLASSNAME_ODD); + } else { + addStyleName(ROW_CLASSNAME_EVEN); + } + } + + public int getIndex() { + return index; + } + + @Override + protected void onDetach() { + super.onDetach(); + client.getContextMenu().ensureHidden(this); + } + + public String getKey() { + return String.valueOf(rowKey); + } + + public void addCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean sorted) { + addCell(rowUidl, text, align, style, textIsHTML, sorted, null); + } + + public void addCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean sorted, + String description) { + // String only content is optimized by not using Label widget + final TableCellElement td = DOM.createTD().cast(); + initCellWithText(text, align, style, textIsHTML, sorted, + description, td); + } + + protected void initCellWithText(String text, char align, + String style, boolean textIsHTML, boolean sorted, + String description, final TableCellElement td) { + final Element container = DOM.createDiv(); + String className = CLASSNAME + "-cell-content"; + if (style != null && !style.equals("")) { + className += " " + CLASSNAME + "-cell-content-" + style; + } + if (sorted) { + className += " " + CLASSNAME + "-cell-content-sorted"; + } + td.setClassName(className); + container.setClassName(CLASSNAME + "-cell-wrapper"); + if (textIsHTML) { + container.setInnerHTML(text); + } else { + container.setInnerText(text); + } + if (align != ALIGN_LEFT) { + switch (align) { + case ALIGN_CENTER: + container.getStyle().setProperty("textAlign", "center"); + break; + case ALIGN_RIGHT: + default: + container.getStyle().setProperty("textAlign", "right"); + break; + } + } + + if (description != null && !description.equals("")) { + TooltipInfo info = new TooltipInfo(description); + client.registerTooltip(VScrollTable.this, td, info); + } else { + // Remove possibly previously set tooltip + client.registerTooltip(VScrollTable.this, td, null); + } + + td.appendChild(container); + getElement().appendChild(td); + } + + public void addCell(UIDL rowUidl, Widget w, char align, + String style, boolean sorted) { + final TableCellElement td = DOM.createTD().cast(); + initCellWithWidget(w, align, style, sorted, td); + } + + protected void initCellWithWidget(Widget w, char align, + String style, boolean sorted, final TableCellElement td) { + final Element container = DOM.createDiv(); + String className = CLASSNAME + "-cell-content"; + if (style != null && !style.equals("")) { + className += " " + CLASSNAME + "-cell-content-" + style; + } + if (sorted) { + className += " " + CLASSNAME + "-cell-content-sorted"; + } + td.setClassName(className); + container.setClassName(CLASSNAME + "-cell-wrapper"); + // TODO most components work with this, but not all (e.g. + // Select) + // Old comment: make widget cells respect align. + // text-align:center for IE, margin: auto for others + if (align != ALIGN_LEFT) { + switch (align) { + case ALIGN_CENTER: + container.getStyle().setProperty("textAlign", "center"); + break; + case ALIGN_RIGHT: + default: + container.getStyle().setProperty("textAlign", "right"); + break; + } + } + td.appendChild(container); + getElement().appendChild(td); + // ensure widget not attached to another element (possible tBody + // change) + w.removeFromParent(); + container.appendChild(w.getElement()); + adopt(w); + childWidgets.add(w); + } + + @Override + public Iterator iterator() { + return childWidgets.iterator(); + } + + @Override + public boolean remove(Widget w) { + if (childWidgets.contains(w)) { + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), + w.getElement()); + childWidgets.remove(w); + return true; + } else { + return false; + } + } + + /** + * If there are registered click listeners, sends a click event and + * returns true. Otherwise, does nothing and returns false. + * + * @param event + * @param targetTdOrTr + * @param immediate + * Whether the event is sent immediately + * @return Whether a click event was sent + */ + private boolean handleClickEvent(Event event, Element targetTdOrTr, + boolean immediate) { + if (!client.hasEventListeners(VScrollTable.this, + ITEM_CLICK_EVENT_ID)) { + // Don't send an event if nobody is listening + return false; + } + + // This row was clicked + client.updateVariable(paintableId, "clickedKey", "" + rowKey, + false); + + if (getElement() == targetTdOrTr.getParentElement()) { + // A specific column was clicked + int childIndex = DOM.getChildIndex(getElement(), + targetTdOrTr); + String colKey = null; + colKey = tHead.getHeaderCell(childIndex).getColKey(); + client.updateVariable(paintableId, "clickedColKey", colKey, + false); + } + + MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(event); + + client.updateVariable(paintableId, "clickEvent", + details.toString(), immediate); + + return true; + } + + private void handleTooltips(final Event event, Element target) { + if (target.hasTagName("TD")) { + // Table cell (td) + Element container = target.getFirstChildElement().cast(); + Element widget = container.getFirstChildElement().cast(); + + boolean containsWidget = false; + for (Widget w : childWidgets) { + if (widget == w.getElement()) { + containsWidget = true; + break; + } + } + + if (!containsWidget) { + // Only text nodes has tooltips + if (ConnectorMap.get(client).getWidgetTooltipInfo( + VScrollTable.this, target) != null) { + // Cell has description, use it + client.handleTooltipEvent(event, VScrollTable.this, + target); + } else { + // Cell might have row description, use row + // description + client.handleTooltipEvent(event, VScrollTable.this, + target.getParentElement()); + } + } + + } else { + // Table row (tr) + client.handleTooltipEvent(event, VScrollTable.this, target); + } + } + + /** + * 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) { + 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); + } + 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) { + event.preventDefault(); + event.stopPropagation(); + if (!BrowserInfo.get().isAndroid()) { + Util.simulateClickFromTouchEvent(touchStart, + this); + } + 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, + ITEM_CLICK_EVENT_ID))) { + /* + * Prevent browser context menu only if there are + * action handlers or item click listeners + * registered + */ + event.stopPropagation(); + event.preventDefault(); + } + return; + } + + boolean targetCellOrRowFound = targetTdOrTr != null; + if (targetCellOrRowFound) { + handleTooltips(event, targetTdOrTr); + } + + switch (type) { + case Event.ONDBLCLICK: + if (targetCellOrRowFound) { + handleClickEvent(event, targetTdOrTr, true); + } + break; + case Event.ONMOUSEUP: + if (targetCellOrRowFound) { + /* + * Queue here, send at the same time as the + * corresponding value change event - see #7127 + */ + boolean clickEventSent = handleClickEvent(event, + targetTdOrTr, false); + + if (event.getButton() == Event.BUTTON_LEFT + && isSelectable()) { + + // Ctrl+Shift click + if ((event.getCtrlKey() || event.getMetaKey()) + && event.getShiftKey() + && isMultiSelectModeDefault()) { + toggleShiftSelection(false); + setRowFocus(this); + + // Ctrl click + } else if ((event.getCtrlKey() || event + .getMetaKey()) + && isMultiSelectModeDefault()) { + boolean wasSelected = isSelected(); + toggleSelection(); + setRowFocus(this); + /* + * next possible range select must start on + * this row + */ + selectionRangeStart = this; + if (wasSelected) { + removeRowFromUnsentSelectionRanges(this); + } + + } else if ((event.getCtrlKey() || event + .getMetaKey()) && isSingleSelectMode()) { + // Ctrl (or meta) click (Single selection) + if (!isSelected() + || (isSelected() && nullSelectionAllowed)) { + + if (!isSelected()) { + deselectAll(); + } + + toggleSelection(); + setRowFocus(this); + } + + } else if (event.getShiftKey() + && isMultiSelectModeDefault()) { + // Shift click + toggleShiftSelection(true); + + } else { + // click + boolean currentlyJustThisRowSelected = selectedRowKeys + .size() == 1 + && selectedRowKeys + .contains(getKey()); + + if (!currentlyJustThisRowSelected) { + if (isSingleSelectMode() + || isMultiSelectModeDefault()) { + /* + * For default multi select mode + * (ctrl/shift) and for single + * select mode we need to clear the + * previous selection before + * selecting a new one when the user + * clicks on a row. Only in + * multiselect/simple mode the old + * selection should remain after a + * normal click. + */ + deselectAll(); + } + toggleSelection(); + } else if ((isSingleSelectMode() || isMultiSelectModeSimple()) + && nullSelectionAllowed) { + toggleSelection(); + }/* + * else NOP to avoid excessive server + * visits (selection is removed with + * CTRL/META click) + */ + + selectionRangeStart = this; + setRowFocus(this); + } + + // Remove IE text selection hack + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()) + .setPropertyJSO("onselectstart", + null); + } + // Queue value change + sendSelectedRows(false); + } + /* + * Send queued click and value change events if any + * If a click event is sent, send value change with + * it regardless of the immediate flag, see #7127 + */ + if (immediate || clickEventSent) { + client.sendPendingVariableChanges(); + } + } + break; + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + if (touchStart != null) { + /* + * Touch has not been handled as neither context or + * drag start, handle it as a click. + */ + Util.simulateClickFromTouchEvent(touchStart, this); + touchStart = null; + } + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + } + break; + case Event.ONTOUCHMOVE: + if (isSignificantMove(event)) { + /* + * TODO figure out scroll delegate don't eat events + * if row is selected. Null check for active + * delegate is as a workaround. + */ + if (dragmode != 0 + && touchStart != null + && (TouchScrollDelegate + .getActiveScrollDelegate() == null)) { + startRowDrag(touchStart, type, targetTdOrTr); + } + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + } + /* + * Avoid clicks and drags by clearing touch start + * flag. + */ + touchStart = null; + } + + break; + case Event.ONTOUCHSTART: + touchStart = event; + Touch touch = event.getChangedTouches().get(0); + // save position to fields, touches in events are same + // isntance during the operation. + touchStartX = touch.getClientX(); + touchStartY = touch.getClientY(); + /* + * Prevent simulated mouse events. + */ + touchStart.preventDefault(); + if (dragmode != 0 || actionKeys != null) { + new Timer() { + @Override + public void run() { + TouchScrollDelegate activeScrollDelegate = TouchScrollDelegate + .getActiveScrollDelegate(); + /* + * If there's a scroll delegate, check if + * we're actually scrolling and handle it. + * If no delegate, do nothing here and let + * the row handle potential drag'n'drop or + * context menu. + */ + if (activeScrollDelegate != null) { + if (activeScrollDelegate.isMoved()) { + /* + * Prevent the row from handling + * touch move/end events (the + * delegate handles those) and from + * doing drag'n'drop or opening a + * context menu. + */ + touchStart = null; + } else { + /* + * Scrolling hasn't started, so + * cancel delegate and let the row + * handle potential drag'n'drop or + * context menu. + */ + activeScrollDelegate + .stopScrolling(); + } + } + } + }.schedule(TOUCHSCROLL_TIMEOUT); + + if (contextTouchTimeout == null + && actionKeys != null) { + contextTouchTimeout = new Timer() { + @Override + public void run() { + if (touchStart != null) { + showContextMenu(touchStart); + touchStart = null; + } + } + }; + } + if (contextTouchTimeout != null) { + contextTouchTimeout.cancel(); + contextTouchTimeout + .schedule(TOUCH_CONTEXT_MENU_TIMEOUT); + } + } + break; + case Event.ONMOUSEDOWN: + if (targetCellOrRowFound) { + setRowFocus(this); + ensureFocus(); + if (dragmode != 0 + && (event.getButton() == NativeEvent.BUTTON_LEFT)) { + startRowDrag(event, type, targetTdOrTr); + + } else if (event.getCtrlKey() + || event.getShiftKey() + || event.getMetaKey() + && isMultiSelectModeDefault()) { + + // Prevent default text selection in Firefox + event.preventDefault(); + + // Prevent default text selection in IE + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()) + .setPropertyJSO( + "onselectstart", + getPreventTextSelectionIEHack()); + } + + event.stopPropagation(); + } + } + break; + case Event.ONMOUSEOUT: + break; + default: + break; + } + } + super.onBrowserEvent(event); + } + + private boolean isSignificantMove(Event event) { + if (touchStart == null) { + // no touch start + return false; + } + /* + * TODO calculate based on real distance instead of separate + * axis checks + */ + Touch touch = event.getChangedTouches().get(0); + if (Math.abs(touch.getClientX() - touchStartX) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) { + return true; + } + if (Math.abs(touch.getClientY() - touchStartY) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) { + return true; + } + return false; + } + + protected void startRowDrag(Event event, final int type, + Element targetTdOrTr) { + VTransferable transferable = new VTransferable(); + transferable.setDragSource(ConnectorMap.get(client) + .getConnector(VScrollTable.this)); + transferable.setData("itemId", "" + rowKey); + NodeList cells = rowElement.getCells(); + for (int i = 0; i < cells.getLength(); i++) { + if (cells.getItem(i).isOrHasChild(targetTdOrTr)) { + HeaderCell headerCell = tHead.getHeaderCell(i); + transferable.setData("propertyId", headerCell.cid); + break; + } + } + + VDragEvent ev = VDragAndDropManager.get().startDrag( + transferable, event, true); + if (dragmode == DRAGMODE_MULTIROW && isMultiSelectModeAny() + && selectedRowKeys.contains("" + rowKey)) { + ev.createDragImage( + (Element) scrollBody.tBodyElement.cast(), true); + Element dragImage = ev.getDragImage(); + int i = 0; + for (Iterator iterator = scrollBody.iterator(); iterator + .hasNext();) { + VScrollTableRow next = (VScrollTableRow) iterator + .next(); + Element child = (Element) dragImage.getChild(i++); + if (!selectedRowKeys.contains("" + next.rowKey)) { + child.getStyle().setVisibility(Visibility.HIDDEN); + } + } + } else { + ev.createDragImage(getElement(), true); + } + if (type == Event.ONMOUSEDOWN) { + event.preventDefault(); + } + event.stopPropagation(); + } + + /** + * Finds the TD that the event interacts with. Returns null if the + * target of the event should not be handled. If the event target is + * the row directly this method returns the TR element instead of + * the TD. + * + * @param event + * @return TD or TR element that the event targets (the actual event + * target is this element or a child of it) + */ + private Element getEventTargetTdOrTr(Event event) { + final Element eventTarget = event.getEventTarget().cast(); + Widget widget = Util.findWidget(eventTarget, null); + final Element thisTrElement = getElement(); + + if (widget != this) { + /* + * This is a workaround to make Labels, read only TextFields + * and Embedded in a Table clickable (see #2688). It is + * really not a fix as it does not work with a custom read + * only components (not extending VLabel/VEmbedded). + */ + while (widget != null && widget.getParent() != this) { + widget = widget.getParent(); + } + + if (!(widget instanceof VLabel) + && !(widget instanceof VEmbedded) + && !(widget instanceof VTextField && ((VTextField) widget) + .isReadOnly())) { + return null; + } + } + if (eventTarget == thisTrElement) { + // This was a click on the TR element + return thisTrElement; + } + + // Iterate upwards until we find the TR element + Element element = eventTarget; + while (element != null + && element.getParentElement().cast() != thisTrElement) { + element = element.getParentElement().cast(); + } + return element; + } + + public void showContextMenu(Event event) { + if (enabled && actionKeys != null) { + // Show context menu if there are registered action handlers + int left = Util.getTouchOrMouseClientX(event); + int top = Util.getTouchOrMouseClientY(event); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + contextMenu = new ContextMenuDetails(getKey(), left, top); + client.getContextMenu().showAt(this, left, top); + } + } + + /** + * Has the row been selected? + * + * @return Returns true if selected, else false + */ + public boolean isSelected() { + return selected; + } + + /** + * Toggle the selection of the row + */ + public void toggleSelection() { + selected = !selected; + selectionChanged = true; + if (selected) { + selectedRowKeys.add(String.valueOf(rowKey)); + addStyleName("v-selected"); + } else { + removeStyleName("v-selected"); + selectedRowKeys.remove(String.valueOf(rowKey)); + } + } + + /** + * Is called when a user clicks an item when holding SHIFT key down. + * This will select a new range from the last focused row + * + * @param deselectPrevious + * Should the previous selected range be deselected + */ + private void toggleShiftSelection(boolean deselectPrevious) { + + /* + * Ensures that we are in multiselect mode and that we have a + * previous selection which was not a deselection + */ + if (isSingleSelectMode()) { + // No previous selection found + deselectAll(); + toggleSelection(); + return; + } + + // Set the selectable range + VScrollTableRow endRow = this; + VScrollTableRow startRow = selectionRangeStart; + if (startRow == null) { + startRow = focusedRow; + // If start row is null then we have a multipage selection + // from + // above + if (startRow == null) { + startRow = (VScrollTableRow) scrollBody.iterator() + .next(); + setRowFocus(endRow); + } + } + // Deselect previous items if so desired + if (deselectPrevious) { + deselectAll(); + } + + // we'll ensure GUI state from top down even though selection + // was the opposite way + if (!startRow.isBefore(endRow)) { + VScrollTableRow tmp = startRow; + startRow = endRow; + endRow = tmp; + } + SelectionRange range = new SelectionRange(startRow, endRow); + + for (Widget w : scrollBody) { + VScrollTableRow row = (VScrollTableRow) w; + if (range.inRange(row)) { + if (!row.isSelected()) { + row.toggleSelection(); + } + selectedRowKeys.add(row.getKey()); + } + } + + // Add range + if (startRow != endRow) { + selectedRowRanges.add(range); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.IActionOwner#getActions () + */ + @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 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).getWidth()); + calcAndSetSpanWidthOnCell(1); + } else { + calcAndSetSpanWidthOnCell(0); + } + } + }); + } + + @Override + protected boolean isRenderHtmlInCells() { + return htmlContentAllowed; + } + + @Override + protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col, + int visibleColumnIndex) { + htmlContentAllowed = uidl.getBooleanAttribute("gen_html"); + spanColumns = uidl.getBooleanAttribute("gen_span"); + + final Iterator cells = uidl.getChildIterator(); + if (spanColumns) { + int colCount = uidl.getChildCount(); + if (cells.hasNext()) { + final Object cell = cells.next(); + if (cell instanceof String) { + addSpannedCell(uidl, cell.toString(), aligns[0], + "", htmlContentAllowed, false, null, + colCount); + } else { + addSpannedCell(uidl, (Widget) cell, aligns[0], "", + false, colCount); + } + } + } else { + super.addCellsFromUIDL(uidl, aligns, col, + visibleColumnIndex); + } + } + + private void addSpannedCell(UIDL rowUidl, Widget w, char align, + String style, boolean sorted, int colCount) { + TableCellElement td = DOM.createTD().cast(); + td.setColSpan(colCount); + initCellWithWidget(w, align, style, sorted, td); + } + + private void addSpannedCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean sorted, + String description, int colCount) { + // String only content is optimized by not using Label widget + final TableCellElement td = DOM.createTD().cast(); + td.setColSpan(colCount); + initCellWithText(text, align, style, textIsHTML, sorted, + description, td); + } + + @Override + protected void setCellWidth(int cellIx, int width) { + if (isSpanColumns()) { + if (showRowHeaders) { + if (cellIx == 0) { + super.setCellWidth(0, width); + } else { + // We need to recalculate the spanning TDs width for + // every cellIx in order to support column resizing. + calcAndSetSpanWidthOnCell(1); + } + } else { + // Same as above. + calcAndSetSpanWidthOnCell(0); + } + } else { + super.setCellWidth(cellIx, width); + } + } + + private void calcAndSetSpanWidthOnCell(final int cellIx) { + int spanWidth = 0; + for (int ix = (showRowHeaders ? 1 : 0); ix < tHead + .getVisibleCellCount(); ix++) { + spanWidth += tHead.getHeaderCell(ix).getOffsetWidth(); + } + Util.setWidthExcludingPaddingAndBorder((Element) getElement() + .getChild(cellIx), spanWidth, 13, false); + } + } + + /** + * Ensure the component has a focus. + * + * TODO the current implementation simply always calls focus for the + * component. In case the Table at some point implements focus/blur + * listeners, this method needs to be evolved to conditionally call + * focus only if not currently focused. + */ + protected void ensureFocus() { + if (!hasFocus) { + scrollBodyPanel.setFocus(true); + } + + } + + } + + /** + * Deselects all items + */ + public void deselectAll() { + for (Widget w : scrollBody) { + VScrollTableRow row = (VScrollTableRow) w; + if (row.isSelected()) { + row.toggleSelection(); + } + } + // still ensure all selects are removed from (not necessary rendered) + selectedRowKeys.clear(); + selectedRowRanges.clear(); + // also notify server that it clears all previous selections (the client + // side does not know about the invisible ones) + instructServerToForgetPreviousSelections(); + } + + /** + * Used in multiselect mode when the client side knows that all selections + * are in the next request. + */ + private void instructServerToForgetPreviousSelections() { + client.updateVariable(paintableId, "clearSelections", true, false); + } + + /** + * Determines the pagelength when the table height is fixed. + */ + public void updatePageLength() { + // Only update if visible and enabled + if (!isVisible() || !enabled) { + return; + } + + if (scrollBody == null) { + return; + } + + if (isDynamicHeight()) { + return; + } + + int rowHeight = (int) Math.round(scrollBody.getRowHeight()); + int bodyH = scrollBodyPanel.getOffsetHeight(); + int rowsAtOnce = bodyH / rowHeight; + boolean anotherPartlyVisible = ((bodyH % rowHeight) != 0); + if (anotherPartlyVisible) { + rowsAtOnce++; + } + if (pageLength != rowsAtOnce) { + pageLength = rowsAtOnce; + client.updateVariable(paintableId, "pagelength", pageLength, false); + + if (!rendering) { + int currentlyVisible = scrollBody.lastRendered + - scrollBody.firstRendered; + if (currentlyVisible < pageLength + && currentlyVisible < totalRows) { + // shake scrollpanel to fill empty space + scrollBodyPanel.setScrollPosition(scrollTop + 1); + scrollBodyPanel.setScrollPosition(scrollTop - 1); + } + + sizeNeedsInit = true; + } + } + + } + + void updateWidth() { + if (!isVisible()) { + /* + * Do not update size when the table is hidden as all column widths + * will be set to zero and they won't be recalculated when the table + * is set visible again (until the size changes again) + */ + return; + } + + if (!isDynamicWidth()) { + int innerPixels = getOffsetWidth() - getBorderWidth(); + if (innerPixels < 0) { + innerPixels = 0; + } + setContentWidth(innerPixels); + + // readjust undefined width columns + triggerLazyColumnAdjustment(false); + + } else { + + sizeNeedsInit = true; + + // readjust undefined width columns + triggerLazyColumnAdjustment(false); + } + + /* + * setting width may affect wheter the component has scrollbars -> needs + * scrolling or not + */ + setProperTabIndex(); + } + + private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300; + + private final Timer lazyAdjustColumnWidths = new Timer() { + /** + * Check for column widths, and available width, to see if we can fix + * column widths "optimally". Doing this lazily to avoid expensive + * calculation when resizing is not yet finished. + */ + @Override + public void run() { + if (scrollBody == null) { + // Try again later if we get here before scrollBody has been + // initalized + triggerLazyColumnAdjustment(false); + return; + } + + Iterator headCells = tHead.iterator(); + int usedMinimumWidth = 0; + int totalExplicitColumnsWidths = 0; + float expandRatioDivider = 0; + int colIndex = 0; + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + if (hCell.isDefinedWidth()) { + totalExplicitColumnsWidths += hCell.getWidth(); + usedMinimumWidth += hCell.getWidth(); + } else { + usedMinimumWidth += hCell.getNaturalColumnWidth(colIndex); + expandRatioDivider += hCell.getExpandRatio(); + } + colIndex++; + } + + int availW = scrollBody.getAvailableWidth(); + // Hey IE, are you really sure about this? + availW = scrollBody.getAvailableWidth(); + int visibleCellCount = tHead.getVisibleCellCount(); + availW -= scrollBody.getCellExtraWidth() * visibleCellCount; + if (willHaveScrollbars()) { + availW -= Util.getNativeScrollbarSize(); + } + + int extraSpace = availW - usedMinimumWidth; + if (extraSpace < 0) { + extraSpace = 0; + } + + int totalUndefinedNaturalWidths = usedMinimumWidth + - totalExplicitColumnsWidths; + + // we have some space that can be divided optimally + HeaderCell hCell; + colIndex = 0; + headCells = tHead.iterator(); + int checksum = 0; + while (headCells.hasNext()) { + hCell = (HeaderCell) headCells.next(); + if (!hCell.isDefinedWidth()) { + int w = hCell.getNaturalColumnWidth(colIndex); + int newSpace; + if (expandRatioDivider > 0) { + // divide excess space by expand ratios + newSpace = Math.round((w + extraSpace + * hCell.getExpandRatio() / expandRatioDivider)); + } else { + if (totalUndefinedNaturalWidths != 0) { + // divide relatively to natural column widths + newSpace = Math.round(w + (float) extraSpace + * (float) w / totalUndefinedNaturalWidths); + } else { + newSpace = w; + } + } + checksum += newSpace; + setColWidth(colIndex, newSpace, false); + } else { + checksum += hCell.getWidth(); + } + colIndex++; + } + + if (extraSpace > 0 && checksum != availW) { + /* + * There might be in some cases a rounding error of 1px when + * extra space is divided so if there is one then we give the + * first undefined column 1 more pixel + */ + headCells = tHead.iterator(); + colIndex = 0; + while (headCells.hasNext()) { + HeaderCell hc = (HeaderCell) headCells.next(); + if (!hc.isDefinedWidth()) { + setColWidth(colIndex, + hc.getWidth() + availW - checksum, false); + break; + } + colIndex++; + } + } + + if (isDynamicHeight() && totalRows == pageLength) { + // fix body height (may vary if lazy loading is offhorizontal + // scrollbar appears/disappears) + int bodyHeight = scrollBody.getRequiredHeight(); + boolean needsSpaceForHorizontalScrollbar = (availW < usedMinimumWidth); + if (needsSpaceForHorizontalScrollbar) { + bodyHeight += Util.getNativeScrollbarSize(); + } + int heightBefore = getOffsetHeight(); + scrollBodyPanel.setHeight(bodyHeight + "px"); + if (heightBefore != getOffsetHeight()) { + Util.notifyParentOfSizeChange(VScrollTable.this, false); + } + } + scrollBody.reLayoutComponents(); + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + }); + + forceRealignColumnHeaders(); + } + + }; + + private void forceRealignColumnHeaders() { + if (BrowserInfo.get().isIE()) { + /* + * IE does not fire onscroll event if scroll position is reverted to + * 0 due to the content element size growth. Ensure headers are in + * sync with content manually. Safe to use null event as we don't + * actually use the event object in listener. + */ + onScroll(null); + } + } + + /** + * helper to set pixel size of head and body part + * + * @param pixels + */ + private void setContentWidth(int pixels) { + tHead.setWidth(pixels + "px"); + scrollBodyPanel.setWidth(pixels + "px"); + tFoot.setWidth(pixels + "px"); + } + + private int borderWidth = -1; + + /** + * @return border left + border right + */ + private int getBorderWidth() { + if (borderWidth < 0) { + borderWidth = Util.measureHorizontalPaddingAndBorder( + scrollBodyPanel.getElement(), 2); + if (borderWidth < 0) { + borderWidth = 0; + } + } + return borderWidth; + } + + /** + * Ensures scrollable area is properly sized. This method is used when fixed + * size is used. + */ + private int containerHeight; + + private void setContainerHeight() { + if (!isDynamicHeight()) { + containerHeight = getOffsetHeight(); + containerHeight -= showColHeaders ? tHead.getOffsetHeight() : 0; + containerHeight -= tFoot.getOffsetHeight(); + containerHeight -= getContentAreaBorderHeight(); + if (containerHeight < 0) { + containerHeight = 0; + } + scrollBodyPanel.setHeight(containerHeight + "px"); + } + } + + private int contentAreaBorderHeight = -1; + private int scrollLeft; + private int scrollTop; + VScrollTableDropHandler dropHandler; + private boolean navKeyDown; + boolean multiselectPending; + + /** + * @return border top + border bottom of the scrollable area of table + */ + private int getContentAreaBorderHeight() { + if (contentAreaBorderHeight < 0) { + + DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow", + "hidden"); + int oh = scrollBodyPanel.getOffsetHeight(); + int ch = scrollBodyPanel.getElement() + .getPropertyInt("clientHeight"); + contentAreaBorderHeight = oh - ch; + DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow", + "auto"); + } + return contentAreaBorderHeight; + } + + @Override + public void setHeight(String height) { + if (height.length() == 0 + && getElement().getStyle().getHeight().length() != 0) { + /* + * Changing from defined to undefined size -> should do a size init + * to take page length into account again + */ + sizeNeedsInit = true; + } + super.setHeight(height); + } + + void updateHeight() { + setContainerHeight(); + + updatePageLength(); + + if (!rendering) { + // Webkit may sometimes get an odd rendering bug (white space + // between header and body), see bug #3875. Running + // overflow hack here to shake body element a bit. + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + + /* + * setting height may affect wheter the component has scrollbars -> + * needs scrolling or not + */ + setProperTabIndex(); + + } + + /* + * Overridden due Table might not survive of visibility change (scroll pos + * lost). Example ITabPanel just set contained components invisible and back + * when changing tabs. + */ + @Override + public void setVisible(boolean visible) { + if (isVisible() != visible) { + super.setVisible(visible); + if (initializedAndAttached) { + if (visible) { + Scheduler.get().scheduleDeferred(new Command() { + @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")) { + s = "\"icon\"" + s; + } + return s; + } + + /** + * This method has logic which rows needs to be requested from server when + * user scrolls + */ + @Override + public void onScroll(ScrollEvent event) { + scrollLeft = scrollBodyPanel.getElement().getScrollLeft(); + scrollTop = scrollBodyPanel.getScrollPosition(); + /* + * #6970 - IE sometimes fires scroll events for a detached table. + * + * FIXME initializedAndAttached should probably be renamed - its name + * doesn't seem to reflect its semantics. onDetach() doesn't set it to + * false, and changing that might break something else, so we need to + * check isAttached() separately. + */ + if (!initializedAndAttached || !isAttached()) { + return; + } + if (!enabled) { + scrollBodyPanel + .setScrollPosition(measureRowHeightOffset(firstRowInViewPort)); + return; + } + + rowRequestHandler.cancel(); + + if (BrowserInfo.get().isSafari() && event != null && scrollTop == 0) { + // due to the webkitoverflowworkaround, top may sometimes report 0 + // for webkit, although it really is not. Expecting to have the + // correct + // value available soon. + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + onScroll(null); + } + }); + return; + } + + // fix headers horizontal scrolling + tHead.setHorizontalScrollPosition(scrollLeft); + + // fix footers horizontal scrolling + tFoot.setHorizontalScrollPosition(scrollLeft); + + firstRowInViewPort = calcFirstRowInViewPort(); + if (firstRowInViewPort > totalRows - pageLength) { + firstRowInViewPort = totalRows - pageLength; + } + + int postLimit = (int) (firstRowInViewPort + (pageLength - 1) + pageLength + * cache_react_rate); + if (postLimit > totalRows - 1) { + postLimit = totalRows - 1; + } + int preLimit = (int) (firstRowInViewPort - pageLength + * cache_react_rate); + if (preLimit < 0) { + preLimit = 0; + } + final int lastRendered = scrollBody.getLastRendered(); + final int firstRendered = scrollBody.getFirstRendered(); + + if (postLimit <= lastRendered && preLimit >= firstRendered) { + // we're within no-react area, no need to request more rows + // remember which firstvisible we requested, in case the server has + // a differing opinion + lastRequestedFirstvisible = firstRowInViewPort; + client.updateVariable(paintableId, "firstvisible", + firstRowInViewPort, false); + return; + } + + if (firstRowInViewPort - pageLength * cache_rate > lastRendered + || firstRowInViewPort + pageLength + pageLength * cache_rate < firstRendered) { + // need a totally new set of rows + rowRequestHandler + .setReqFirstRow((firstRowInViewPort - (int) (pageLength * cache_rate))); + int last = firstRowInViewPort + (int) (cache_rate * pageLength) + + pageLength - 1; + if (last >= totalRows) { + last = totalRows - 1; + } + rowRequestHandler.setReqRows(last + - rowRequestHandler.getReqFirstRow() + 1); + rowRequestHandler.deferRowFetch(); + return; + } + if (preLimit < firstRendered) { + // need some rows to the beginning of the rendered area + rowRequestHandler + .setReqFirstRow((int) (firstRowInViewPort - pageLength + * cache_rate)); + rowRequestHandler.setReqRows(firstRendered + - rowRequestHandler.getReqFirstRow()); + rowRequestHandler.deferRowFetch(); + + return; + } + if (postLimit > lastRendered) { + // need some rows to the end of the rendered area + rowRequestHandler.setReqFirstRow(lastRendered + 1); + rowRequestHandler.setReqRows((int) ((firstRowInViewPort + + pageLength + pageLength * cache_rate) - lastRendered)); + rowRequestHandler.deferRowFetch(); + } + } + + protected int calcFirstRowInViewPort() { + return (int) Math.ceil(scrollTop / scrollBody.getRowHeight()); + } + + @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; + } + + // @Override + // public int hashCode() { + // return overkey; + // } + } + + public class VScrollTableDropHandler extends VAbstractDropHandler { + + private static final String ROWSTYLEBASE = "v-table-row-drag-"; + private TableDDDetails dropDetails; + private TableDDDetails lastEmphasized; + + @Override + public void dragEnter(VDragEvent drag) { + updateDropDetails(drag); + super.dragEnter(drag); + } + + private void updateDropDetails(VDragEvent drag) { + dropDetails = new TableDDDetails(); + Element elementOver = drag.getElementOver(); + + VScrollTableRow row = Util.findWidget(elementOver, getRowClass()); + if (row != null) { + dropDetails.overkey = row.rowKey; + Element tr = row.getElement(); + Element element = elementOver; + while (element != null && element.getParentElement() != tr) { + element = (Element) element.getParentElement(); + } + int childIndex = DOM.getChildIndex(tr, element); + dropDetails.colkey = tHead.getHeaderCell(childIndex) + .getColKey(); + dropDetails.dropLocation = DDUtil.getVerticalDropLocation( + row.getElement(), drag.getCurrentGwtEvent(), 0.2); + } + + drag.getDropDetails().put("itemIdOver", dropDetails.overkey + ""); + drag.getDropDetails().put( + "detail", + dropDetails.dropLocation != null ? dropDetails.dropLocation + .toString() : null); + + } + + private Class getRowClass() { + // get the row type this way to make dd work in derived + // implementations + return scrollBody.iterator().next().getClass(); + } + + @Override + public void dragOver(VDragEvent drag) { + TableDDDetails oldDetails = dropDetails; + updateDropDetails(drag); + if (!oldDetails.equals(dropDetails)) { + deEmphasis(); + final TableDDDetails newDetails = dropDetails; + VAcceptCallback cb = new VAcceptCallback() { + @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(), CLASSNAME + "-drag", false); + if (lastEmphasized == null) { + return; + } + for (Widget w : scrollBody.renderedRows) { + VScrollTableRow row = (VScrollTableRow) w; + if (lastEmphasized != null + && row.rowKey == lastEmphasized.overkey) { + String stylename = ROWSTYLEBASE + + lastEmphasized.dropLocation.toString() + .toLowerCase(); + VScrollTableRow.setStyleName(row.getElement(), stylename, + false); + lastEmphasized = null; + return; + } + } + } + + /** + * TODO needs different drop modes ?? (on cells, on rows), now only + * supports rows + */ + private void emphasis(TableDDDetails details) { + deEmphasis(); + UIObject.setStyleName(getElement(), CLASSNAME + "-drag", true); + // iterate old and new emphasized row + for (Widget w : scrollBody.renderedRows) { + VScrollTableRow row = (VScrollTableRow) w; + if (details != null && details.overkey == row.rowKey) { + String stylename = ROWSTYLEBASE + + details.dropLocation.toString().toLowerCase(); + VScrollTableRow.setStyleName(row.getElement(), stylename, + true); + lastEmphasized = details; + return; + } + } + } + + @Override + protected void dragAccepted(VDragEvent drag) { + emphasis(dropDetails); + } + + @Override + public ComponentConnector getConnector() { + return ConnectorMap.get(client).getConnector(VScrollTable.this); + } + + @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(CLASSNAME_SELECTION_FOCUS); + } + + if (row != null) { + + // Apply focus style to new selection + row.addStyleName(CLASSNAME_SELECTION_FOCUS); + + /* + * Trying to set focus on already focused row + */ + if (row == focusedRow) { + return false; + } + + // Set new focused row + focusedRow = row; + + ensureRowIsVisible(row); + + return true; + } + + return false; + } + + /** + * Ensures that the row is visible + * + * @param row + * The row to ensure is visible + */ + private void ensureRowIsVisible(VScrollTableRow row) { + if (BrowserInfo.get().isTouchDevice()) { + // Skip due to android devices that have broken scrolltop will may + // get odd scrolling here. + return; + } + Util.scrollIntoViewVertically(row.getElement()); + } + + /** + * Handles the keyboard events handled by the table + * + * @param event + * The keyboard event received + * @return true iff the navigation event was handled + */ + protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + if (keycode == KeyCodes.KEY_TAB || keycode == KeyCodes.KEY_SHIFT) { + // Do not handle tab key + return false; + } + + // Down navigation + if (!isSelectable() && keycode == getNavigationDownKey()) { + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() + scrollingVelocity); + return true; + } else if (keycode == getNavigationDownKey()) { + if (isMultiSelectModeAny() && moveFocusDown()) { + selectFocusedRow(ctrl, shift); + + } else if (isSingleSelectMode() && !shift && moveFocusDown()) { + selectFocusedRow(ctrl, shift); + } + return true; + } + + // Up navigation + if (!isSelectable() && keycode == getNavigationUpKey()) { + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() - scrollingVelocity); + return true; + } else if (keycode == getNavigationUpKey()) { + if (isMultiSelectModeAny() && moveFocusUp()) { + selectFocusedRow(ctrl, shift); + } else if (isSingleSelectMode() && !shift && moveFocusUp()) { + selectFocusedRow(ctrl, shift); + } + return true; + } + + if (keycode == getNavigationLeftKey()) { + // Left navigation + scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel + .getHorizontalScrollPosition() - scrollingVelocity); + return true; + + } else if (keycode == getNavigationRightKey()) { + // Right navigation + scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel + .getHorizontalScrollPosition() + scrollingVelocity); + return true; + } + + // Select navigation + if (isSelectable() && keycode == getNavigationSelectKey()) { + if (isSingleSelectMode()) { + boolean wasSelected = focusedRow.isSelected(); + deselectAll(); + if (!wasSelected || !nullSelectionAllowed) { + focusedRow.toggleSelection(); + } + } else { + focusedRow.toggleSelection(); + removeRowFromUnsentSelectionRanges(focusedRow); + } + + sendSelectedRows(); + return true; + } + + // Page Down navigation + if (keycode == getNavigationPageDownKey()) { + if (isSelectable()) { + /* + * If selectable we plagiate MSW behaviour: first scroll to the + * end of current view. If at the end, scroll down one page + * length and keep the selected row in the bottom part of + * visible area. + */ + if (!isFocusAtTheEndOfTable()) { + VScrollTableRow lastVisibleRowInViewPort = scrollBody + .getRowByRowIndex(firstRowInViewPort + + getFullyVisibleRowCount() - 1); + if (lastVisibleRowInViewPort != null + && lastVisibleRowInViewPort != focusedRow) { + // focused row is not at the end of the table, move + // focus and select the last visible row + setRowFocus(lastVisibleRowInViewPort); + selectFocusedRow(ctrl, shift); + sendSelectedRows(); + } else { + int indexOfToBeFocused = focusedRow.getIndex() + + getFullyVisibleRowCount(); + if (indexOfToBeFocused >= totalRows) { + indexOfToBeFocused = totalRows - 1; + } + VScrollTableRow toBeFocusedRow = scrollBody + .getRowByRowIndex(indexOfToBeFocused); + + if (toBeFocusedRow != null) { + /* + * if the next focused row is rendered + */ + setRowFocus(toBeFocusedRow); + selectFocusedRow(ctrl, shift); + // TODO needs scrollintoview ? + sendSelectedRows(); + } else { + // scroll down by pixels and return, to wait for + // new rows, then select the last item in the + // viewport + selectLastItemInNextRender = true; + multiselectPending = shift; + scrollByPagelenght(1); + } + } + } + } else { + /* No selections, go page down by scrolling */ + scrollByPagelenght(1); + } + return true; + } + + // Page Up navigation + if (keycode == getNavigationPageUpKey()) { + if (isSelectable()) { + /* + * If selectable we plagiate MSW behaviour: first scroll to the + * end of current view. If at the end, scroll down one page + * length and keep the selected row in the bottom part of + * visible area. + */ + if (!isFocusAtTheBeginningOfTable()) { + VScrollTableRow firstVisibleRowInViewPort = scrollBody + .getRowByRowIndex(firstRowInViewPort); + if (firstVisibleRowInViewPort != null + && firstVisibleRowInViewPort != focusedRow) { + // focus is not at the beginning of the table, move + // focus and select the first visible row + setRowFocus(firstVisibleRowInViewPort); + selectFocusedRow(ctrl, shift); + sendSelectedRows(); + } else { + int indexOfToBeFocused = focusedRow.getIndex() + - getFullyVisibleRowCount(); + if (indexOfToBeFocused < 0) { + indexOfToBeFocused = 0; + } + VScrollTableRow toBeFocusedRow = scrollBody + .getRowByRowIndex(indexOfToBeFocused); + + if (toBeFocusedRow != null) { // if the next focused row + // is rendered + setRowFocus(toBeFocusedRow); + selectFocusedRow(ctrl, shift); + // TODO needs scrollintoview ? + sendSelectedRows(); + } else { + // unless waiting for the next rowset already + // scroll down by pixels and return, to wait for + // new rows, then select the last item in the + // viewport + selectFirstItemInNextRender = true; + multiselectPending = shift; + scrollByPagelenght(-1); + } + } + } + } else { + /* No selections, go page up by scrolling */ + scrollByPagelenght(-1); + } + + return true; + } + + // Goto start navigation + if (keycode == getNavigationStartKey()) { + scrollBodyPanel.setScrollPosition(0); + if (isSelectable()) { + if (focusedRow != null && focusedRow.getIndex() == 0) { + return false; + } else { + VScrollTableRow rowByRowIndex = (VScrollTableRow) scrollBody + .iterator().next(); + if (rowByRowIndex.getIndex() == 0) { + setRowFocus(rowByRowIndex); + selectFocusedRow(ctrl, shift); + sendSelectedRows(); + } else { + // first row of table will come in next row fetch + if (ctrl) { + focusFirstItemInNextRender = true; + } else { + selectFirstItemInNextRender = true; + multiselectPending = shift; + } + } + } + } + return true; + } + + // Goto end navigation + if (keycode == getNavigationEndKey()) { + scrollBodyPanel.setScrollPosition(scrollBody.getOffsetHeight()); + if (isSelectable()) { + final int lastRendered = scrollBody.getLastRendered(); + if (lastRendered + 1 == totalRows) { + VScrollTableRow rowByRowIndex = scrollBody + .getRowByRowIndex(lastRendered); + if (focusedRow != rowByRowIndex) { + setRowFocus(rowByRowIndex); + selectFocusedRow(ctrl, shift); + sendSelectedRows(); + } + } else { + if (ctrl) { + focusLastItemInNextRender = true; + } else { + selectLastItemInNextRender = true; + multiselectPending = shift; + } + } + } + return true; + } + + return false; + } + + private boolean isFocusAtTheBeginningOfTable() { + return focusedRow.getIndex() == 0; + } + + private boolean isFocusAtTheEndOfTable() { + return focusedRow.getIndex() + 1 >= totalRows; + } + + private int getFullyVisibleRowCount() { + return (int) (scrollBodyPanel.getOffsetHeight() / scrollBody + .getRowHeight()); + } + + private void scrollByPagelenght(int i) { + int pixels = i * scrollBodyPanel.getOffsetHeight(); + int newPixels = scrollBodyPanel.getScrollPosition() + pixels; + if (newPixels < 0) { + newPixels = 0; + } // else if too high, NOP (all know browsers accept illegally big + // values here) + scrollBodyPanel.setScrollPosition(newPixels); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + @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) { + hasFocus = false; + navKeyDown = false; + + if (BrowserInfo.get().isIE()) { + // IE sometimes moves focus to a clicked table cell... + Element focusedElement = Util.getIEFocusedElement(); + if (Util.getConnectorForElement(client, getParent(), focusedElement) == this) { + // ..in that case, steal the focus back to the focus handler + // but not if focus is in a child component instead (#7965) + focus(); + return; + } + } + + if (isFocusable()) { + // Unfocus any row + setRowFocus(null); + } + } + + /** + * Removes a key from a range if the key is found in a selected range + * + * @param key + * The key to remove + */ + private void removeRowFromUnsentSelectionRanges(VScrollTableRow row) { + Collection newRanges = null; + for (Iterator iterator = selectedRowRanges.iterator(); iterator + .hasNext();) { + SelectionRange range = iterator.next(); + if (range.inRange(row)) { + // Split the range if given row is in range + Collection splitranges = range.split(row); + if (newRanges == null) { + newRanges = new ArrayList(); + } + newRanges.addAll(splitranges); + iterator.remove(); + } + } + if (newRanges != null) { + selectedRowRanges.addAll(newRanges); + } + } + + /** + * Can the Table be focused? + * + * @return True if the table can be focused, else false + */ + public boolean isFocusable() { + if (scrollBody != null && enabled) { + return !(!hasHorizontalScrollbar() && !hasVerticalScrollbar() && !isSelectable()); + } + return false; + } + + private boolean hasHorizontalScrollbar() { + return scrollBody.getOffsetWidth() > scrollBodyPanel.getOffsetWidth(); + } + + private boolean hasVerticalScrollbar() { + return scrollBody.getOffsetHeight() > scrollBodyPanel.getOffsetHeight(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Focusable#focus() + */ + @Override + public void focus() { + if (isFocusable()) { + scrollBodyPanel.focus(); + } + } + + /** + * Sets the proper tabIndex for scrollBodyPanel (the focusable elemen in the + * component). + * + * If the component has no explicit tabIndex a zero is given (default + * tabbing order based on dom hierarchy) or -1 if the component does not + * need to gain focus. The component needs no focus if it has no scrollabars + * (not scrollable) and not selectable. Note that in the future shortcut + * actions may need focus. + * + */ + void setProperTabIndex() { + int storedScrollTop = 0; + int storedScrollLeft = 0; + + if (BrowserInfo.get().getOperaVersion() >= 11) { + // Workaround for Opera scroll bug when changing tabIndex (#6222) + storedScrollTop = scrollBodyPanel.getScrollPosition(); + storedScrollLeft = scrollBodyPanel.getHorizontalScrollPosition(); + } + + if (tabIndex == 0 && !isFocusable()) { + scrollBodyPanel.setTabIndex(-1); + } else { + scrollBodyPanel.setTabIndex(tabIndex); + } + + if (BrowserInfo.get().getOperaVersion() >= 11) { + // Workaround for Opera scroll bug when changing tabIndex (#6222) + scrollBodyPanel.setScrollPosition(storedScrollTop); + scrollBodyPanel.setHorizontalScrollPosition(storedScrollLeft); + } + } + + public void startScrollingVelocityTimer() { + if (scrollingVelocityTimer == null) { + scrollingVelocityTimer = new Timer() { + @Override + public void run() { + scrollingVelocity++; + } + }; + scrollingVelocityTimer.scheduleRepeating(100); + } + } + + public void cancelScrollingVelocityTimer() { + if (scrollingVelocityTimer != null) { + // Remove velocityTimer if it exists and the Table is disabled + scrollingVelocityTimer.cancel(); + scrollingVelocityTimer = null; + scrollingVelocity = 10; + } + } + + /** + * + * @param keyCode + * @return true if the given keyCode is used by the table for navigation + */ + private boolean isNavigationKey(int keyCode) { + return keyCode == getNavigationUpKey() + || keyCode == getNavigationLeftKey() + || keyCode == getNavigationRightKey() + || keyCode == getNavigationDownKey() + || keyCode == getNavigationPageUpKey() + || keyCode == getNavigationPageDownKey() + || keyCode == getNavigationEndKey() + || keyCode == getNavigationStartKey(); + } + + public void lazyRevertFocusToRow(final VScrollTableRow currentlyFocusedRow) { + Scheduler.get().scheduleFinally(new ScheduledCommand() { + @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; + } +}