add ignored files to the repo
diff --git a/org.eclipse.osbp.fork.vaadin.addon.filteringtable/src/com/vaadin/client/ui/VCustomScrollTable.java b/org.eclipse.osbp.fork.vaadin.addon.filteringtable/src/com/vaadin/client/ui/VCustomScrollTable.java
new file mode 100644
index 0000000..7212104
--- /dev/null
+++ b/org.eclipse.osbp.fork.vaadin.addon.filteringtable/src/com/vaadin/client/ui/VCustomScrollTable.java
@@ -0,0 +1,9629 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.ui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.TextAlign;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableRowElement;
+import com.google.gwt.dom.client.TableSectionElement;
+import com.google.gwt.dom.client.Touch;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.dom.client.ScrollEvent;
+import com.google.gwt.event.dom.client.ScrollHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.regexp.shared.MatchResult;
+import com.google.gwt.regexp.shared.RegExp;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorMap;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.Focusable;
+import com.vaadin.client.MouseEventDetailsBuilder;
+import com.vaadin.client.StyleConstants;
+import com.vaadin.client.TooltipInfo;
+import com.vaadin.client.UIDL;
+import com.vaadin.client.Util;
+import com.vaadin.client.VConsole;
+import com.vaadin.client.VTooltip;
+import com.vaadin.client.ui.VCustomScrollTable.VScrollTableBody.VScrollTableRow;
+import com.vaadin.client.ui.dd.DDUtil;
+import com.vaadin.client.ui.dd.VAbstractDropHandler;
+import com.vaadin.client.ui.dd.VAcceptCallback;
+import com.vaadin.client.ui.dd.VDragAndDropManager;
+import com.vaadin.client.ui.dd.VDragEvent;
+import com.vaadin.client.ui.dd.VHasDropHandler;
+import com.vaadin.client.ui.dd.VTransferable;
+import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.shared.ui.table.TableConstants;
+
+// TODO: Auto-generated Javadoc
+/**
+ * VCustomScrollTable
+ *
+ * VCustomScrollTable 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 VCustomScrollTable 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
+ */
+@SuppressWarnings("deprecation")
+public class VCustomScrollTable extends FlowPanel implements HasWidgets,
+ ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable,
+ ActionOwner, SubPartAware, DeferredWorker {
+
+ /** Simple interface for parts of the table capable of owning a context
+ * menu.
+ *
+ * @author Vaadin Ltd
+ * @since 7.2
+ */
+ private interface ContextMenuOwner {
+
+ /** Show context menu.
+ *
+ * @param event
+ * the event
+ */
+ public void showContextMenu(Event event);
+ }
+
+ /** Handles showing context menu on "long press" from a touch screen.
+ *
+ * @author Vaadin Ltd
+ * @since 7.2
+ */
+ private class TouchContextProvider {
+
+ /** The Constant TOUCH_CONTEXT_MENU_TIMEOUT. */
+ private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500;
+
+ /** The context touch timeout. */
+ private Timer contextTouchTimeout;
+
+ /** The touch start. */
+ private Event touchStart;
+
+ /** The touch start y. */
+ private int touchStartY;
+
+ /** The touch start x. */
+ private int touchStartX;
+
+ /** The target. */
+ private ContextMenuOwner target;
+
+ /**
+ * Initializes a handler for a certain context menu owner.
+ *
+ * @param target
+ * the owner of the context menu
+ */
+ public TouchContextProvider(ContextMenuOwner target) {
+ this.target = target;
+ }
+
+ /**
+ * Cancels the current context touch timeout.
+ */
+ public void cancel() {
+ if (contextTouchTimeout != null) {
+ contextTouchTimeout.cancel();
+ contextTouchTimeout = null;
+ }
+ touchStart = null;
+ }
+
+ /**
+ * A function to handle touch context events in a table.
+ *
+ * @param event
+ * browser event to handle
+ */
+ public void handleTouchEvent(final Event event) {
+ int type = event.getTypeInt();
+
+ switch (type) {
+ case Event.ONCONTEXTMENU:
+ target.showContextMenu(event);
+ break;
+ case Event.ONTOUCHSTART:
+ // save position to fields, touches in events are same
+ // instance during the operation.
+ touchStart = event;
+
+ Touch touch = event.getChangedTouches().get(0);
+ touchStartX = touch.getClientX();
+ touchStartY = touch.getClientY();
+
+ if (contextTouchTimeout == null) {
+ contextTouchTimeout = new Timer() {
+
+ @Override
+ public void run() {
+ if (touchStart != null) {
+ // Open the context menu if finger
+ // is held in place long enough.
+ target.showContextMenu(touchStart);
+ event.preventDefault();
+ touchStart = null;
+ }
+ }
+ };
+ }
+ contextTouchTimeout.schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
+ break;
+ case Event.ONTOUCHCANCEL:
+ case Event.ONTOUCHEND:
+ cancel();
+ break;
+ case Event.ONTOUCHMOVE:
+ if (isSignificantMove(event)) {
+ // Moved finger before the context menu timer
+ // expired, so let the browser handle the event.
+ cancel();
+ }
+ }
+ }
+
+ /**
+ * Calculates how many pixels away the user's finger has traveled. This
+ * reduces the chance of small non-intentional movements from canceling
+ * the long press detection.
+ *
+ * @param event
+ * the Event for which to check the move distance
+ * @return true if this is considered an intentional move by the user
+ */
+ protected boolean isSignificantMove(Event event) {
+ if (touchStart == null) {
+ // no touch start
+ return false;
+ }
+
+ // Calculate the distance between touch start and the current touch
+ // position
+ Touch touch = event.getChangedTouches().get(0);
+ int deltaX = touch.getClientX() - touchStartX;
+ int deltaY = touch.getClientY() - touchStartY;
+ int delta = deltaX * deltaX + deltaY * deltaY;
+
+ // Compare to the square of the significant move threshold to remove
+ // the need for a square root
+ if (delta > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD
+ * TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /** The Constant STYLENAME. */
+ public static final String STYLENAME = "v-table";
+
+ /** The Enum SelectMode.
+ */
+ public enum SelectMode {
+
+ /** The none. */
+ NONE(0),
+ /** The single. */
+ SINGLE(1),
+ /** The multi. */
+ MULTI(2);
+
+ /** The id. */
+ private int id;
+
+ /** Instantiates a new select mode.
+ *
+ * @param id
+ * the id
+ */
+ private SelectMode(int id) {
+ this.id = id;
+ }
+
+ /** Gets the id.
+ *
+ * @return the id
+ */
+ public int getId() {
+ return id;
+ }
+ }
+
+ /** The Constant ROW_HEADER_COLUMN_KEY. */
+ private static final String ROW_HEADER_COLUMN_KEY = "0";
+
+ /** The Constant CACHE_RATE_DEFAULT. */
+ 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;
+
+ /** The Constant ALIGN_CENTER. */
+ public static final char ALIGN_CENTER = 'c';
+
+ /** The Constant ALIGN_LEFT. */
+ public static final char ALIGN_LEFT = 'b';
+
+ /** The Constant ALIGN_RIGHT. */
+ public static final char ALIGN_RIGHT = 'e';
+
+ /** The Constant CHARCODE_SPACE. */
+ private static final int CHARCODE_SPACE = 32;
+
+ /** The first row in view port. */
+ private int firstRowInViewPort = 0;
+
+ /** The page length. */
+ private int pageLength = 15;
+
+ /** The last requested firstvisible. */
+ private int lastRequestedFirstvisible = 0; // to detect "serverside scroll"
+
+ /** The firstvisible on last page. */
+ private int firstvisibleOnLastPage = -1; // To detect if the first visible
+ // is on the last page
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean showRowHeaders = false;
+
+ /** The column order. */
+ private String[] columnOrder;
+
+ /** The client. */
+ protected ApplicationConnection client;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String paintableId;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean immediate;
+
+ /** The updated req rows. */
+ private boolean updatedReqRows = true;
+
+ /** The null selection allowed. */
+ private boolean nullSelectionAllowed = true;
+
+ /** The select mode. */
+ private SelectMode selectMode = SelectMode.NONE;
+
+ /** The selected row keys. */
+ public final HashSet<String> selectedRowKeys = new HashSet<String>();
+
+ /** The un syncedselections before row fetch. */
+ /*
+ * When scrolling and selecting at the same time, the selections are not in
+ * sync with the server while retrieving new rows (until key is released).
+ */
+ private HashSet<Object> unSyncedselectionsBeforeRowFetch;
+
+ /*
+ * These are used when jumping between pages when pressing Home and End
+ */
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean selectLastItemInNextRender = false;
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean selectFirstItemInNextRender = false;
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean focusFirstItemInNextRender = false;
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean focusLastItemInNextRender = false;
+
+ /**
+ * The currently focused row.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public VScrollTableRow focusedRow;
+
+ /**
+ * Helper to store selection range start in when using the keyboard
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public VScrollTableRow selectionRangeStart;
+
+ /**
+ * Flag for notifying when the selection has changed and should be sent to
+ * the server
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public boolean selectionChanged = false;
+
+ /** The scrolling velocity. */
+ /*
+ * The speed (in pixels) which the scrolling scrolls vertically/horizontally
+ */
+ private int scrollingVelocity = 10;
+
+ /** The scrolling velocity timer. */
+ private Timer scrollingVelocityTimer = null;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String[] bodyActionKeys;
+
+ /** The enable debug. */
+ private boolean enableDebug = false;
+
+ /** The Constant hasNativeTouchScrolling. */
+ private static final boolean hasNativeTouchScrolling = BrowserInfo.get()
+ .isTouchDevice()
+ && !BrowserInfo.get().requiresTouchScrollDelegate();
+
+ /** The noncollapsible columns. */
+ private Set<String> noncollapsibleColumns;
+
+ /**
+ * The last known row height used to preserve the height of a table with
+ * custom row heights and a fixed page length after removing the last row
+ * from the table.
+ *
+ * A new VScrollTableBody instance is created every time the number of rows
+ * changes causing {@link VScrollTableBody#rowHeight} to be discarded and
+ * the height recalculated by {@link VScrollTableBody#getRowHeight(boolean)}
+ * to avoid some rounding problems, e.g. round(2 * 19.8) / 2 = 20 but
+ * round(3 * 19.8) / 3 = 19.66.
+ */
+ private double lastKnownRowHeight = Double.NaN;
+
+ /**
+ * Remember scroll position when getting detached to properly scroll back to
+ * the location that there is data for if getting attached again.
+ */
+ private int detachedScrollPosition = 0;
+
+ /** Represents a select range of rows.
+ */
+ private class SelectionRange {
+
+ /** The start row. */
+ private VScrollTableRow startRow;
+
+ /** The length. */
+ private final int length;
+
+ /** Constuctor.
+ *
+ * @param row1
+ * the row1
+ * @param row2
+ * the row2
+ */
+ 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;
+ }
+
+ /** Instantiates a new selection range.
+ *
+ * @param row
+ * the row
+ * @param length
+ * the length
+ */
+ 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;
+ }
+
+ /** In range.
+ *
+ * @param row
+ * the row
+ * @return true, if successful
+ */
+ private boolean inRange(VScrollTableRow row) {
+ return row.getIndex() >= startRow.getIndex()
+ && row.getIndex() < startRow.getIndex() + length;
+ }
+
+ /** Split.
+ *
+ * @param row
+ * the row
+ * @return the collection
+ */
+ public Collection<SelectionRange> split(VScrollTableRow row) {
+ assert row.isAttached();
+ ArrayList<SelectionRange> ranges = new ArrayList<SelectionRange>(2);
+
+ int endOfFirstRange = row.getIndex() - 1;
+ if (endOfFirstRange >= startRow.getIndex()) {
+ // create range of first part unless its length is < 1
+ ranges.add(new SelectionRange(startRow, endOfFirstRange
+ - startRow.getIndex() + 1));
+ }
+ int startOfSecondRange = row.getIndex() + 1;
+ if (getEndIndex() >= startOfSecondRange) {
+ // create range of second part unless its length is < 1
+ VScrollTableRow startOfRange = scrollBody
+ .getRowByRowIndex(startOfSecondRange);
+ if (startOfRange != null) {
+ ranges.add(new SelectionRange(startOfRange, getEndIndex()
+ - startOfSecondRange + 1));
+ }
+ }
+ return ranges;
+ }
+
+ /** Gets the end index.
+ *
+ * @return the end index
+ */
+ private int getEndIndex() {
+ return startRow.getIndex() + length - 1;
+ }
+
+ }
+
+ /** The selected row ranges. */
+ private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>();
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean initializedAndAttached = false;
+
+ /**
+ * Flag to indicate if a column width recalculation is needed due update.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public boolean headerChangedDuringUpdate = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public final TableHead tHead = new TableHead();
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public final TableFooter tFoot = new TableFooter();
+
+ /** Handles context menu for table body. */
+ private ContextMenuOwner contextMenuOwner = new ContextMenuOwner() {
+
+ @Override
+ public void showContextMenu(Event event) {
+ int left = Util.getTouchOrMouseClientX(event);
+ int top = Util.getTouchOrMouseClientY(event);
+ boolean menuShown = handleBodyContextMenu(left, top);
+ if (menuShown) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+ };
+
+ /** Handles touch events to display a context menu for table body. */
+ private TouchContextProvider touchContextProvider = new TouchContextProvider(
+ contextMenuOwner);
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * Overwrites onBrowserEvent function on FocusableScrollPanel to give event
+ * access to touchContextProvider. Has to be public to give TableConnector
+ * access to the scrollBodyPanel field.
+ *
+ * @author Vaadin Ltd
+ * @since 7.2
+ */
+ public class FocusableScrollContextPanel extends FocusableScrollPanel {
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user.client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ touchContextProvider.handleTouchEvent(event);
+ };
+
+ /** Instantiates a new focusable scroll context panel.
+ *
+ * @param useFakeFocusElement
+ * the use fake focus element
+ */
+ public FocusableScrollContextPanel(boolean useFakeFocusElement) {
+ super(useFakeFocusElement);
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public final FocusableScrollContextPanel scrollBodyPanel = new FocusableScrollContextPanel(
+ true);
+
+ /** The nav key press handler. */
+ 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();
+ }
+ }
+
+ };
+
+ /** The nav key up handler. */
+ 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;
+ }
+ }
+ };
+
+ /** The nav key down handler. */
+ private KeyDownHandler navKeyDownHandler = new KeyDownHandler() {
+
+ @Override
+ public void onKeyDown(KeyDownEvent keyDownEvent) {
+ NativeEvent event = keyDownEvent.getNativeEvent();
+ // This is not used for Firefox
+ if (BrowserInfo.get().isGecko()) {
+ return;
+ }
+
+ if (!enabled) {
+ // Cancel default keyboard events on a disabled Table
+ // (prevents scrolling)
+ event.preventDefault();
+ } else if (hasFocus) {
+ if (handleNavigation(event.getKeyCode(), event.getCtrlKey()
+ || event.getMetaKey(), event.getShiftKey())) {
+ navKeyDown = true;
+ event.preventDefault();
+ }
+
+ startScrollingVelocityTimer();
+ }
+ }
+ };
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int totalRows;
+
+ /** The collapsed columns. */
+ private Set<String> collapsedColumns;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public final RowRequestHandler rowRequestHandler;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public VScrollTableBody scrollBody;
+
+ /** The firstvisible. */
+ private int firstvisible = 0;
+
+ /** The sort ascending. */
+ private boolean sortAscending;
+
+ /** The sort column. */
+ private String sortColumn;
+
+ /** The old sort column. */
+ private String oldSortColumn;
+
+ /** The column reordering. */
+ private boolean columnReordering;
+
+ /**
+ * This map contains captions and icon urls for actions like: * "33_c" ->
+ * "Edit" * "33_i" -> "http://dom.com/edit.png"
+ */
+ private final HashMap<Object, String> actionMap = new HashMap<Object, String>();
+
+ /** The visible col order. */
+ private String[] visibleColOrder;
+
+ /** The initial content received. */
+ private boolean initialContentReceived = false;
+
+ /** The scroll position element. */
+ private Element scrollPositionElement;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean enabled;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean showColHeaders;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean showColFooters;
+
+ /** flag to indicate that table body has changed. */
+ private boolean isNewBody = true;
+
+ /**
+ * Read from the "recalcWidths" -attribute. When it is true, the table will
+ * recalculate the widths for columns - desirable in some cases. For #1983,
+ * marked experimental. See also variable <code>refreshContentWidths</code>
+ * in method {@link TableHead#updateCellsFromUIDL(UIDL)}.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public boolean recalcWidths = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean rendering = false;
+
+ /** The has focus. */
+ private boolean hasFocus = false;
+
+ /** The dragmode. */
+ private int dragmode;
+
+ /** The multiselectmode. */
+ private int multiselectmode;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int tabIndex;
+
+ /** The touch scroll delegate. */
+ private TouchScrollDelegate touchScrollDelegate;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int lastRenderedHeight;
+
+ /**
+ * Values (serverCacheFirst+serverCacheLast) sent by server that tells which
+ * rows (indexes) are in the server side cache (page buffer). -1 means
+ * unknown. The server side cache row MUST MATCH the client side cache rows.
+ *
+ * If the client side cache contains additional rows with e.g. buttons, it
+ * will cause out of sync when such a button is pressed.
+ *
+ * If the server side cache contains additional rows with e.g. buttons,
+ * scrolling in the client will cause empty buttons to be rendered
+ * (cached=true request for non-existing components)
+ *
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public int serverCacheFirst = -1;
+
+ /** The server cache last. */
+ public int serverCacheLast = -1;
+
+ /**
+ * In several cases TreeTable depends on the scrollBody.lastRendered being
+ * 'out of sync' while the update is being done. In those cases the sanity
+ * check must be performed afterwards.
+ */
+ public boolean postponeSanityCheckForLastRendered;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean sizeNeedsInit = true;
+
+ /**
+ * Used to recall the position of an open context menu if we need to close
+ * and reopen it during a row update.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public class ContextMenuDetails implements CloseHandler<PopupPanel> {
+
+ /** The row key. */
+ public String rowKey;
+
+ /** The left. */
+ public int left;
+
+ /** The top. */
+ public int top;
+
+ /** The close registration. */
+ HandlerRegistration closeRegistration;
+
+ /** Instantiates a new context menu details.
+ *
+ * @param menu
+ * the menu
+ * @param rowKey
+ * the row key
+ * @param left
+ * the left
+ * @param top
+ * the top
+ */
+ public ContextMenuDetails(VContextMenu menu, String rowKey, int left,
+ int top) {
+ this.rowKey = rowKey;
+ this.left = left;
+ this.top = top;
+ closeRegistration = menu.addCloseHandler(this);
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google.gwt.event.logical.shared.CloseEvent)
+ */
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ contextMenu = null;
+ closeRegistration.removeHandler();
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public ContextMenuDetails contextMenu = null;
+
+ /** The had scroll bars. */
+ private boolean hadScrollBars = false;
+
+ /** The add close handler. */
+ private HandlerRegistration addCloseHandler;
+
+ /** Changes to manage mouseDown and mouseUp. */
+ /**
+ * The element where the last mouse down event was registered.
+ */
+ private Element lastMouseDownTarget;
+
+ /**
+ * Set to true by {@link #mouseUpPreviewHandler} if it gets a mouseup at the
+ * same element as {@link #lastMouseDownTarget}.
+ */
+ private boolean mouseUpPreviewMatched = false;
+
+ /** The mouse up event preview registration. */
+ private HandlerRegistration mouseUpEventPreviewRegistration;
+
+ /**
+ * Previews events after a mousedown to detect where the following mouseup
+ * hits.
+ */
+ private final NativePreviewHandler mouseUpPreviewHandler = new NativePreviewHandler() {
+
+ @Override
+ public void onPreviewNativeEvent(NativePreviewEvent event) {
+ if (event.getTypeInt() == Event.ONMOUSEUP) {
+ mouseUpEventPreviewRegistration.removeHandler();
+
+ // Event's reported target not always correct if event
+ // capture is in use
+ Element elementUnderMouse = Util.getElementUnderMouse(event
+ .getNativeEvent());
+ if (lastMouseDownTarget != null
+ && lastMouseDownTarget.isOrHasChild(elementUnderMouse)) {
+ mouseUpPreviewMatched = true;
+ } else {
+ getLogger().log(
+ Level.FINEST,
+ "Ignoring mouseup from " + elementUnderMouse
+ + " when mousedown was on "
+ + lastMouseDownTarget);
+ }
+ }
+ }
+ };
+
+ /** Instantiates a new v custom scroll table.
+ */
+ public VCustomScrollTable() {
+ setMultiSelectMode(MULTISELECT_MODE_DEFAULT);
+
+ scrollBodyPanel.addFocusHandler(this);
+ scrollBodyPanel.addBlurHandler(this);
+
+ scrollBodyPanel.addScrollHandler(this);
+
+ /*
+ * Firefox auto-repeat works correctly only if we use a key press
+ * handler, other browsers handle it correctly when using a key down
+ * handler
+ */
+ if (BrowserInfo.get().isGecko()) {
+ scrollBodyPanel.addKeyPressHandler(navKeyPressHandler);
+ } else {
+ scrollBodyPanel.addKeyDownHandler(navKeyDownHandler);
+ }
+ scrollBodyPanel.addKeyUpHandler(navKeyUpHandler);
+
+ scrollBodyPanel.sinkEvents(Event.TOUCHEVENTS | Event.ONCONTEXTMENU);
+
+ setStyleName(STYLENAME);
+
+ add(tHead);
+ add(scrollBodyPanel);
+ add(tFoot);
+
+ rowRequestHandler = new RowRequestHandler();
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.UIObject#setStyleName(java.lang.String)
+ */
+ @Override
+ public void setStyleName(String style) {
+ updateStyleNames(style, false);
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.UIObject#setStylePrimaryName(java.lang.String)
+ */
+ @Override
+ public void setStylePrimaryName(String style) {
+ updateStyleNames(style, true);
+ }
+
+ /** Update style names.
+ *
+ * @param newStyle
+ * the new style
+ * @param isPrimary
+ * the is primary
+ */
+ private void updateStyleNames(String newStyle, boolean isPrimary) {
+ scrollBodyPanel
+ .removeStyleName(getStylePrimaryName() + "-body-wrapper");
+ scrollBodyPanel.removeStyleName(getStylePrimaryName() + "-body");
+
+ if (scrollBody != null) {
+ scrollBody.removeStyleName(getStylePrimaryName()
+ + "-body-noselection");
+ }
+
+ if (isPrimary) {
+ super.setStylePrimaryName(newStyle);
+ } else {
+ super.setStyleName(newStyle);
+ }
+
+ scrollBodyPanel.addStyleName(getStylePrimaryName() + "-body-wrapper");
+ scrollBodyPanel.addStyleName(getStylePrimaryName() + "-body");
+
+ tHead.updateStyleNames(getStylePrimaryName());
+ tFoot.updateStyleNames(getStylePrimaryName());
+
+ if (scrollBody != null) {
+ scrollBody.updateStyleNames(getStylePrimaryName());
+ }
+ }
+
+ /** Inits the.
+ *
+ * @param client
+ * the client
+ */
+ public void init(ApplicationConnection client) {
+ this.client = client;
+ // Add a handler to clear saved context menu details when the menu
+ // closes. See #8526.
+ addCloseHandler = client.getContextMenu().addCloseHandler(
+ new CloseHandler<PopupPanel>() {
+
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ contextMenu = null;
+ }
+ });
+ }
+
+ /**
+ * Handles a context menu event on table body.
+ *
+ * @param left
+ * left position of the context menu
+ * @param top
+ * top position of the context menu
+ * @return true if a context menu was shown, otherwise false
+ */
+ private boolean handleBodyContextMenu(int left, int top) {
+ if (enabled && bodyActionKeys != null) {
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Fires a column resize event which sends the resize information to the
+ * server.
+ *
+ * @param columnId
+ * The columnId of the column which was resized
+ * @param originalWidth
+ * The width in pixels of the column before the resize event
+ * @param newWidth
+ * The width in pixels of the column after the resize event
+ */
+ private void fireColumnResizeEvent(String columnId, int originalWidth,
+ int newWidth) {
+ client.updateVariable(paintableId, "columnResizeEventColumn", columnId,
+ false);
+ client.updateVariable(paintableId, "columnResizeEventPrev",
+ originalWidth, false);
+ client.updateVariable(paintableId, "columnResizeEventCurr", newWidth,
+ immediate);
+
+ }
+
+ /**
+ * Non-immediate variable update of column widths for a collection of
+ * columns.
+ *
+ * @param columns
+ * the columns to trigger the events for.
+ */
+ private void sendColumnWidthUpdates(Collection<HeaderCell> columns) {
+ String[] newSizes = new String[columns.size()];
+ int ix = 0;
+ for (HeaderCell cell : columns) {
+ newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth();
+ }
+ client.updateVariable(paintableId, "columnWidthUpdates", newSizes,
+ false);
+ }
+
+ /** Moves the focus one step down.
+ *
+ * @return Returns true if succeeded
+ */
+ private boolean moveFocusDown() {
+ return moveFocusDown(0);
+ }
+
+ /** Moves the focus down by 1+offset rows.
+ *
+ * @param offset
+ * the offset
+ * @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.
+ *
+ * @param offset
+ * the offset
+ * @return Returns true if succeeded, else false if the selection could not
+ * be move upwards
+ */
+ private boolean moveFocusUp(int offset) {
+ if (isSelectable()) {
+ if (focusedRow == null && scrollBody.iterator().hasNext()) {
+ // FIXME logic is exactly the same as in moveFocusDown, should
+ // be the opposite??
+ return setRowFocus((VScrollTableRow) scrollBody.iterator()
+ .next());
+ } else {
+ VScrollTableRow prev = getPreviousRow(focusedRow, offset);
+ if (prev != null) {
+ return setRowFocus(prev);
+ } else {
+ VConsole.log("no previous available");
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /** Selects a row where the current selection head is.
+ *
+ * @param ctrlSelect
+ * Is the selection a ctrl+selection
+ * @param shiftSelect
+ * Is the selection a shift+selection
+ * @return Returns truw
+ */
+ private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) {
+ if (focusedRow != null) {
+ // Arrows moves the selection and clears previous selections
+ if (isSelectable() && !ctrlSelect && !shiftSelect) {
+ deselectAll();
+ focusedRow.toggleSelection();
+ selectionRangeStart = focusedRow;
+ } else if (isSelectable() && ctrlSelect && !shiftSelect) {
+ // Ctrl+arrows moves selection head
+ selectionRangeStart = focusedRow;
+ // No selection, only selection head is moved
+ } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) {
+ // Shift+arrows selection selects a range
+ focusedRow.toggleShiftSelection(shiftSelect);
+ }
+ }
+ }
+
+ /**
+ * Sends the selection to the server if changed since the last update/visit.
+ */
+ protected void sendSelectedRows() {
+ sendSelectedRows(immediate);
+ }
+
+ /**
+ * Sends the selection to the server if it has been changed since the last
+ * update/visit.
+ *
+ * @param immediately
+ * set to true to immediately send the rows
+ */
+ protected void sendSelectedRows(boolean immediately) {
+ // Don't send anything if selection has not changed
+ if (!selectionChanged) {
+ return;
+ }
+
+ // Reset selection changed flag
+ selectionChanged = false;
+
+ // Note: changing the immediateness of this might require changes to
+ // "clickEvent" immediateness also.
+ if (isMultiSelectModeDefault()) {
+ // Convert ranges to a set of strings
+ Set<String> ranges = new HashSet<String>();
+ for (SelectionRange range : selectedRowRanges) {
+ ranges.add(range.toString());
+ }
+
+ // Send the selected row ranges
+ client.updateVariable(paintableId, "selectedRanges",
+ ranges.toArray(new String[selectedRowRanges.size()]), false);
+ selectedRowRanges.clear();
+
+ // clean selectedRowKeys so that they don't contain excess values
+ for (Iterator<String> iterator = selectedRowKeys.iterator(); iterator
+ .hasNext();) {
+ String key = iterator.next();
+ VScrollTableRow renderedRowByKey = getRenderedRowByKey(key);
+ if (renderedRowByKey != null) {
+ for (SelectionRange range : selectedRowRanges) {
+ if (range.inRange(renderedRowByKey)) {
+ iterator.remove();
+ }
+ }
+ } else {
+ // orphaned selected key, must be in a range, ignore
+ iterator.remove();
+ }
+
+ }
+ }
+
+ // Send the selected rows
+ client.updateVariable(paintableId, "selected",
+ selectedRowKeys.toArray(new String[selectedRowKeys.size()]),
+ immediately);
+
+ }
+
+ /**
+ * Get the key that moves the selection head upwards. By default it is the
+ * up arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationUpKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /**
+ * Get the key that moves the selection head downwards. By default it is the
+ * down arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationDownKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Get the key that scrolls to the left in the table. By default it is the
+ * left arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationLeftKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Get the key that scroll to the right on the table. By default it is the
+ * right arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationRightKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /** Get the key that selects an item in the table. By default it is the
+ * space bar key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return the navigation select key
+ */
+ 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 the navigation page up key
+ */
+ 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 the navigation page down key
+ */
+ 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 the navigation start key
+ */
+ 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 the navigation end key
+ */
+ protected int getNavigationEndKey() {
+ return KeyCodes.KEY_END;
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the uidl
+ * @param rowData
+ * the row data
+ */
+ public void initializeRows(UIDL uidl, UIDL rowData) {
+ if (scrollBody != null) {
+ scrollBody.removeFromParent();
+ }
+
+ // Without this call the scroll position is messed up in IE even after
+ // the lazy scroller has set the scroll position to the first visible
+ // item
+ scrollBodyPanel.getScrollPosition();
+
+ 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();
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the uidl
+ */
+ public void updateColumnProperties(UIDL uidl) {
+ updateColumnOrder(uidl);
+
+ updateCollapsedColumns(uidl);
+
+ UIDL vc = uidl.getChildByTagName("visiblecolumns");
+ if (vc != null) {
+ tHead.updateCellsFromUIDL(vc);
+ tFoot.updateCellsFromUIDL(vc);
+ }
+
+ updateHeader(uidl.getStringArrayAttribute("vcolorder"));
+ updateFooter(uidl.getStringArrayAttribute("vcolorder"));
+ if (uidl.hasVariable("noncollapsiblecolumns")) {
+ noncollapsibleColumns = uidl
+ .getStringArrayVariableAsSet("noncollapsiblecolumns");
+ }
+ }
+
+ /** Update collapsed columns.
+ *
+ * @param uidl
+ * the uidl
+ */
+ private void updateCollapsedColumns(UIDL uidl) {
+ if (uidl.hasVariable("collapsedcolumns")) {
+ tHead.setColumnCollapsingAllowed(true);
+ collapsedColumns = uidl
+ .getStringArrayVariableAsSet("collapsedcolumns");
+ } else {
+ tHead.setColumnCollapsingAllowed(false);
+ }
+ }
+
+ /** Update column order.
+ *
+ * @param uidl
+ * the uidl
+ */
+ private void updateColumnOrder(UIDL uidl) {
+ if (uidl.hasVariable("columnorder")) {
+ columnReordering = true;
+ columnOrder = uidl.getStringArrayVariable("columnorder");
+ } else {
+ columnReordering = false;
+ columnOrder = null;
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the uidl
+ * @return true, if successful
+ */
+ public boolean selectSelectedRows(UIDL uidl) {
+ boolean keyboardSelectionOverRowFetchInProgress = false;
+
+ if (uidl.hasVariable("selected")) {
+ final Set<String> selectedKeys = uidl
+ .getStringArrayVariableAsSet("selected");
+ removeUnselectedRowKeys(selectedKeys);
+
+ if (scrollBody != null) {
+ Iterator<Widget> iterator = scrollBody.iterator();
+ while (iterator.hasNext()) {
+ /*
+ * Make the focus reflect to the server side state unless we
+ * are currently selecting multiple rows with keyboard.
+ */
+ VScrollTableRow row = (VScrollTableRow) iterator.next();
+ boolean selected = selectedKeys.contains(row.getKey());
+ if (!selected
+ && unSyncedselectionsBeforeRowFetch != null
+ && unSyncedselectionsBeforeRowFetch.contains(row
+ .getKey())) {
+ selected = true;
+ keyboardSelectionOverRowFetchInProgress = true;
+ }
+ if (selected && selectedKeys.size() == 1) {
+ /*
+ * If a single item is selected, move focus to the
+ * selected row. (#10522)
+ */
+ setRowFocus(row);
+ }
+
+ if (selected != row.isSelected()) {
+ row.toggleSelection();
+
+ if (!isSingleSelectMode() && !selected) {
+ // Update selection range in case a row is
+ // unselected from the middle of a range - #8076
+ removeRowFromUnsentSelectionRanges(row);
+ }
+ }
+ }
+
+ }
+ }
+ unSyncedselectionsBeforeRowFetch = null;
+ return keyboardSelectionOverRowFetchInProgress;
+ }
+
+ /** Removes the unselected row keys.
+ *
+ * @param selectedKeys
+ * the selected keys
+ */
+ private void removeUnselectedRowKeys(final Set<String> selectedKeys) {
+ List<String> unselectedKeys = new ArrayList<String>(0);
+ for (String key : selectedRowKeys) {
+ if (!selectedKeys.contains(key)) {
+ unselectedKeys.add(key);
+ }
+ }
+ selectedRowKeys.removeAll(unselectedKeys);
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the uidl
+ */
+ public void updateSortingProperties(UIDL uidl) {
+ oldSortColumn = sortColumn;
+ if (uidl.hasVariable("sortascending")) {
+ sortAscending = uidl.getBooleanVariable("sortascending");
+ sortColumn = uidl.getStringVariable("sortcolumn");
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void resizeSortedColumnForSortIndicator() {
+ // Force recalculation of the captionContainer element inside the header
+ // cell to accomodate for the size of the sort arrow.
+ HeaderCell sortedHeader = tHead.getHeaderCell(sortColumn);
+ if (sortedHeader != null) {
+ // Mark header as sorted now. Any earlier marking would lead to
+ // columns with wrong sizes
+ sortedHeader.setSorted(true);
+ tHead.resizeCaptionContainer(sortedHeader);
+ }
+ // Also recalculate the width of the captionContainer element in the
+ // previously sorted header, since this now has more room.
+ HeaderCell oldSortedHeader = tHead.getHeaderCell(oldSortColumn);
+ if (oldSortedHeader != null) {
+ tHead.resizeCaptionContainer(oldSortedHeader);
+ }
+ }
+
+ /** The lazy scroller is active. */
+ private boolean lazyScrollerIsActive;
+
+ /** Disable lazy scroller.
+ */
+ private void disableLazyScroller() {
+ lazyScrollerIsActive = false;
+ scrollBodyPanel.getElement().getStyle().clearOverflowX();
+ scrollBodyPanel.getElement().getStyle().clearOverflowY();
+ }
+
+ /** Enable lazy scroller.
+ */
+ private void enableLazyScroller() {
+ Scheduler.get().scheduleDeferred(lazyScroller);
+ lazyScrollerIsActive = true;
+ // prevent scrolling to jump in IE11
+ scrollBodyPanel.getElement().getStyle().setOverflowX(Overflow.HIDDEN);
+ scrollBodyPanel.getElement().getStyle().setOverflowY(Overflow.HIDDEN);
+ }
+
+ /** Checks if is lazy scroller active.
+ *
+ * @return true, if is lazy scroller active
+ */
+ private boolean isLazyScrollerActive() {
+ return lazyScrollerIsActive;
+ }
+
+ /** The lazy scroller. */
+ private ScheduledCommand lazyScroller = new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (firstvisible >= 0) {
+ firstRowInViewPort = firstvisible;
+ if (firstvisibleOnLastPage > -1) {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstvisibleOnLastPage));
+ } else {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstvisible));
+ }
+ }
+ disableLazyScroller();
+ }
+ };
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the uidl
+ */
+ public void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) {
+ firstvisible = uidl.hasVariable("firstvisible") ? uidl
+ .getIntVariable("firstvisible") : 0;
+ firstvisibleOnLastPage = uidl.hasVariable("firstvisibleonlastpage") ? uidl
+ .getIntVariable("firstvisibleonlastpage") : -1;
+ if (firstvisible != lastRequestedFirstvisible && scrollBody != null) {
+
+ // Update lastRequestedFirstvisible right away here
+ // (don't rely on update in the timer which could be cancelled).
+ lastRequestedFirstvisible = firstRowInViewPort;
+
+ // Only scroll if the first visible changes from the server side.
+ // Else we might unintentionally scroll even when the scroll
+ // position has not changed.
+ enableLazyScroller();
+ }
+ }
+
+ /** Measure row height offset.
+ *
+ * @param rowIx
+ * the row ix
+ * @return the int
+ */
+ protected int measureRowHeightOffset(int rowIx) {
+ return (int) (rowIx * scrollBody.getRowHeight());
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the uidl
+ */
+ public 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;
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the uidl
+ * @param state
+ * the state
+ * @param readOnly
+ * the read only
+ */
+ public void updateSelectionProperties(UIDL uidl,
+ AbstractComponentState state, boolean readOnly) {
+ setMultiSelectMode(uidl.hasAttribute("multiselectmode") ? uidl
+ .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT);
+
+ nullSelectionAllowed = uidl.hasAttribute("nsa") ? uidl
+ .getBooleanAttribute("nsa") : true;
+
+ if (uidl.hasAttribute("selectmode")) {
+ if (readOnly) {
+ selectMode = SelectMode.NONE;
+ } else if (uidl.getStringAttribute("selectmode").equals("multi")) {
+ selectMode = SelectMode.MULTI;
+ } else if (uidl.getStringAttribute("selectmode").equals("single")) {
+ selectMode = SelectMode.SINGLE;
+ } else {
+ selectMode = SelectMode.NONE;
+ }
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the uidl
+ */
+ public void updateDragMode(UIDL uidl) {
+ dragmode = uidl.hasAttribute("dragmode") ? uidl
+ .getIntAttribute("dragmode") : 0;
+ if (BrowserInfo.get().isIE()) {
+ if (dragmode > 0) {
+ getElement().setPropertyJSO("onselectstart",
+ getPreventTextSelectionIEHack());
+ } else {
+ getElement().setPropertyJSO("onselectstart", null);
+ }
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the uidl
+ */
+ public void updateTotalRows(UIDL uidl) {
+ int newTotalRows = uidl.getIntAttribute("totalrows");
+ if (newTotalRows != getTotalRows()) {
+ if (scrollBody != null) {
+ if (getTotalRows() == 0) {
+ tHead.clear();
+ tFoot.clear();
+ }
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ setTotalRows(newTotalRows);
+ }
+ }
+
+ /** Sets the for internal use only.
+ *
+ * @param newTotalRows
+ * the new for internal use only
+ */
+ protected void setTotalRows(int newTotalRows) {
+ totalRows = newTotalRows;
+ }
+
+ /** Gets the for internal use only.
+ *
+ * @return the for internal use only
+ */
+ public int getTotalRows() {
+ return totalRows;
+ }
+
+ /**
+ * Returns the extra space that is given to the header column when column
+ * width is determined by header text.
+ *
+ * @return extra space in pixels
+ */
+ private int getHeaderPadding() {
+ return scrollBody.getCellExtraWidth();
+ }
+
+ /**
+ * This method exists for the needs of {@link VTreeTable} only. Not part of
+ * the official API, <b>extend at your own risk</b>. May be removed or
+ * replaced in the future.
+ *
+ * @return index of TreeTable's hierarchy column, or -1 if not applicable
+ */
+ protected int getHierarchyColumnIndex() {
+ return -1;
+ }
+
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public void updateMaxIndent() {
+ int oldIndent = scrollBody.getMaxIndent();
+ scrollBody.calculateMaxIndent();
+ if (oldIndent != scrollBody.getMaxIndent()) {
+ // indent updated, headers might need adjusting
+ triggerLazyColumnAdjustment(true);
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void focusRowFromBody() {
+ if (selectedRowKeys.size() == 1) {
+ // try to focus a row currently selected and in viewport
+ String selectedRowKey = selectedRowKeys.iterator().next();
+ if (selectedRowKey != null) {
+ VScrollTableRow renderedRow = getRenderedRowByKey(selectedRowKey);
+ if (renderedRow == null || !renderedRow.isInViewPort()) {
+ setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
+ } else {
+ setRowFocus(renderedRow);
+ }
+ }
+ } else {
+ // multiselect mode
+ setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
+ }
+ }
+
+ /** Creates the scroll body.
+ *
+ * @return the v scroll table body
+ */
+ protected VScrollTableBody createScrollBody() {
+ return new VScrollTableBody();
+ }
+
+ /**
+ * Selects the last row visible in the table
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param focusOnly
+ * Should the focus only be moved to the last row
+ */
+ public void selectLastRenderedRowInViewPort(boolean focusOnly) {
+ int index = firstRowInViewPort + getFullyVisibleRowCount();
+ VScrollTableRow lastRowInViewport = scrollBody.getRowByRowIndex(index);
+ if (lastRowInViewport == null) {
+ // this should not happen in normal situations (white space at the
+ // end of viewport). Select the last rendered as a fallback.
+ lastRowInViewport = scrollBody.getRowByRowIndex(scrollBody
+ .getLastRendered());
+ if (lastRowInViewport == null) {
+ return; // empty table
+ }
+ }
+ setRowFocus(lastRowInViewport);
+ if (!focusOnly) {
+ selectFocusedRow(false, multiselectPending);
+ sendSelectedRows();
+ }
+ }
+
+ /**
+ * Selects the first row visible in the table
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param focusOnly
+ * Should the focus only be moved to the first row
+ */
+ public void selectFirstRenderedRowInViewPort(boolean focusOnly) {
+ int index = firstRowInViewPort;
+ VScrollTableRow firstInViewport = scrollBody.getRowByRowIndex(index);
+ if (firstInViewport == null) {
+ // this should not happen in normal situations
+ return;
+ }
+ setRowFocus(firstInViewport);
+ if (!focusOnly) {
+ selectFocusedRow(false, multiselectPending);
+ sendSelectedRows();
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * the new cache rate from uidl
+ */
+ public void setCacheRateFromUIDL(UIDL uidl) {
+ setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr")
+ : CACHE_RATE_DEFAULT);
+ }
+
+ /** Sets the cache rate.
+ *
+ * @param d
+ * the new cache rate
+ */
+ private void setCacheRate(double d) {
+ if (cache_rate != d) {
+ cache_rate = d;
+ cache_react_rate = 0.75 * d;
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param mainUidl
+ * the main uidl
+ */
+ public void updateActionMap(UIDL mainUidl) {
+ UIDL actionsUidl = mainUidl.getChildByTagName("actions");
+ if (actionsUidl == null) {
+ return;
+ }
+
+ final Iterator<?> it = actionsUidl.getChildIterator();
+ while (it.hasNext()) {
+ final UIDL action = (UIDL) it.next();
+ final String key = action.getStringAttribute("key");
+ final String caption = action.getStringAttribute("caption");
+ actionMap.put(key + "_c", caption);
+ if (action.hasAttribute("icon")) {
+ // TODO need some uri handling ??
+ actionMap.put(key + "_i", client.translateVaadinUri(action
+ .getStringAttribute("icon")));
+ } else {
+ actionMap.remove(key + "_i");
+ }
+ }
+
+ }
+
+ /** Gets the action caption.
+ *
+ * @param actionKey
+ * the action key
+ * @return the action caption
+ */
+ public String getActionCaption(String actionKey) {
+ return actionMap.get(actionKey + "_c");
+ }
+
+ /** Gets the action icon.
+ *
+ * @param actionKey
+ * the action key
+ * @return the action icon
+ */
+ public String getActionIcon(String actionKey) {
+ return actionMap.get(actionKey + "_i");
+ }
+
+ /** Update header.
+ *
+ * @param strings
+ * the strings
+ */
+ private void updateHeader(String[] strings) {
+ if (strings == null) {
+ return;
+ }
+
+ int visibleCols = strings.length;
+ int colIndex = 0;
+ if (showRowHeaders) {
+ tHead.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
+ visibleCols++;
+ visibleColOrder = new String[visibleCols];
+ visibleColOrder[colIndex] = ROW_HEADER_COLUMN_KEY;
+ colIndex++;
+ } else {
+ visibleColOrder = new String[visibleCols];
+ tHead.removeCell(ROW_HEADER_COLUMN_KEY);
+ }
+
+ int i;
+ for (i = 0; i < strings.length; i++) {
+ final String cid = strings[i];
+ visibleColOrder[colIndex] = cid;
+ tHead.enableColumn(cid, colIndex);
+ colIndex++;
+ }
+
+ tHead.setVisible(showColHeaders);
+ setContainerHeight();
+
+ }
+
+ /** Updates footers.
+ * <p>
+ * Update headers whould be called before this method is called!
+ * </p>
+ *
+ * @param strings
+ * the strings
+ */
+ private void updateFooter(String[] strings) {
+ if (strings == null) {
+ return;
+ }
+
+ // Add dummy column if row headers are present
+ int colIndex = 0;
+ if (showRowHeaders) {
+ tFoot.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
+ colIndex++;
+ } else {
+ tFoot.removeCell(ROW_HEADER_COLUMN_KEY);
+ }
+
+ int i;
+ for (i = 0; i < strings.length; i++) {
+ final String cid = strings[i];
+ tFoot.enableColumn(cid, colIndex);
+ colIndex++;
+ }
+
+ tFoot.setVisible(showColFooters);
+ }
+
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param uidl
+ * which contains row data
+ * @param firstRow
+ * first row in data set
+ * @param reqRows
+ * amount of rows in data set
+ */
+ public void updateBody(UIDL uidl, int firstRow, int reqRows) {
+ int oldIndent = scrollBody.getMaxIndent();
+ if (uidl == null || reqRows < 1) {
+ // container is empty, remove possibly existing rows
+ if (firstRow <= 0) {
+ postponeSanityCheckForLastRendered = true;
+ while (scrollBody.getLastRendered() > scrollBody
+ .getFirstRendered()) {
+ scrollBody.unlinkRow(false);
+ }
+ postponeSanityCheckForLastRendered = false;
+ scrollBody.unlinkRow(false);
+ }
+ return;
+ }
+
+ scrollBody.renderRows(uidl, firstRow, reqRows);
+
+ discardRowsOutsideCacheWindow();
+ scrollBody.calculateMaxIndent();
+ if (oldIndent != scrollBody.getMaxIndent()) {
+ // indent updated, headers might need adjusting
+ headerChangedDuringUpdate = true;
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @param partialRowUpdates
+ * the partial row updates
+ */
+ public void updateRowsInBody(UIDL partialRowUpdates) {
+ if (partialRowUpdates == null) {
+ return;
+ }
+ int firstRowIx = partialRowUpdates.getIntAttribute("firsturowix");
+ int count = partialRowUpdates.getIntAttribute("numurows");
+ scrollBody.unlinkRows(firstRowIx, count);
+ scrollBody.insertRows(partialRowUpdates, firstRowIx, count);
+ }
+
+ /**
+ * Updates the internal cache by unlinking rows that fall outside of the
+ * caching window.
+ */
+ protected void discardRowsOutsideCacheWindow() {
+ int firstRowToKeep = (int) (firstRowInViewPort - pageLength
+ * cache_rate);
+ int lastRowToKeep = (int) (firstRowInViewPort + pageLength + pageLength
+ * cache_rate);
+ // sanity checks:
+ if (firstRowToKeep < 0) {
+ firstRowToKeep = 0;
+ }
+ if (lastRowToKeep > totalRows) {
+ lastRowToKeep = totalRows - 1;
+ }
+ debug("Client side calculated cache rows to keep: " + firstRowToKeep
+ + "-" + lastRowToKeep);
+
+ if (serverCacheFirst != -1) {
+ firstRowToKeep = serverCacheFirst;
+ lastRowToKeep = serverCacheLast;
+ debug("Server cache rows that override: " + serverCacheFirst + "-"
+ + serverCacheLast);
+ if (firstRowToKeep < scrollBody.getFirstRendered()
+ || lastRowToKeep > scrollBody.getLastRendered()) {
+ debug("*** Server wants us to keep " + serverCacheFirst + "-"
+ + serverCacheLast + " but we only have rows "
+ + scrollBody.getFirstRendered() + "-"
+ + scrollBody.getLastRendered() + " rendered!");
+ }
+ }
+ discardRowsOutsideOf(firstRowToKeep, lastRowToKeep);
+
+ scrollBody.fixSpacers();
+
+ scrollBody.restoreRowVisibility();
+ }
+
+ /** Discard rows outside of.
+ *
+ * @param optimalFirstRow
+ * the optimal first row
+ * @param optimalLastRow
+ * the optimal last row
+ */
+ private void discardRowsOutsideOf(int optimalFirstRow, int optimalLastRow) {
+ /*
+ * firstDiscarded and lastDiscarded are only calculated for debug
+ * purposes
+ */
+ int firstDiscarded = -1, lastDiscarded = -1;
+ boolean cont = true;
+ while (cont && scrollBody.getLastRendered() > optimalFirstRow
+ && scrollBody.getFirstRendered() < optimalFirstRow) {
+ if (firstDiscarded == -1) {
+ firstDiscarded = scrollBody.getFirstRendered();
+ }
+
+ // removing row from start
+ cont = scrollBody.unlinkRow(true);
+ }
+ if (firstDiscarded != -1) {
+ lastDiscarded = scrollBody.getFirstRendered() - 1;
+ debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
+ }
+ firstDiscarded = lastDiscarded = -1;
+
+ cont = true;
+ while (cont && scrollBody.getLastRendered() > optimalLastRow) {
+ if (lastDiscarded == -1) {
+ lastDiscarded = scrollBody.getLastRendered();
+ }
+
+ // removing row from the end
+ cont = scrollBody.unlinkRow(false);
+ }
+ if (lastDiscarded != -1) {
+ firstDiscarded = scrollBody.getLastRendered() + 1;
+ debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
+ }
+
+ debug("Now in cache: " + scrollBody.getFirstRendered() + "-"
+ + scrollBody.getLastRendered());
+ }
+
+ /**
+ * Inserts rows in the table body or removes them from the table body based
+ * on the commands in the UIDL.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param partialRowAdditions
+ * the UIDL containing row updates.
+ */
+ public void addAndRemoveRows(UIDL partialRowAdditions) {
+ if (partialRowAdditions == null) {
+ return;
+ }
+ if (partialRowAdditions.hasAttribute("hide")) {
+ scrollBody.unlinkAndReindexRows(
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ scrollBody.ensureCacheFilled();
+ } else {
+ if (partialRowAdditions.hasAttribute("delbelow")) {
+ scrollBody.insertRowsDeleteBelow(partialRowAdditions,
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ } else {
+ scrollBody.insertAndReindexRows(partialRowAdditions,
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ }
+ }
+
+ discardRowsOutsideCacheWindow();
+ }
+
+ /** Gives correct column index for given column key ("cid" in UIDL).
+ *
+ * @param colKey
+ * the col key
+ * @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;
+ }
+
+ /** Checks if is multi select mode simple.
+ *
+ * @return true, if is multi select mode simple
+ */
+ private boolean isMultiSelectModeSimple() {
+ return selectMode == SelectMode.MULTI
+ && multiselectmode == MULTISELECT_MODE_SIMPLE;
+ }
+
+ /** Checks if is single select mode.
+ *
+ * @return true, if is single select mode
+ */
+ private boolean isSingleSelectMode() {
+ return selectMode == SelectMode.SINGLE;
+ }
+
+ /** Checks if is multi select mode any.
+ *
+ * @return true, if is multi select mode any
+ */
+ private boolean isMultiSelectModeAny() {
+ return selectMode == SelectMode.MULTI;
+ }
+
+ /** Checks if is multi select mode default.
+ *
+ * @return true, if is multi select mode default
+ */
+ private boolean isMultiSelectModeDefault() {
+ return selectMode == SelectMode.MULTI
+ && multiselectmode == MULTISELECT_MODE_DEFAULT;
+ }
+
+ /** Sets the multi select mode.
+ *
+ * @param multiselectmode
+ * the new multi select mode
+ */
+ private void setMultiSelectMode(int multiselectmode) {
+ if (BrowserInfo.get().isTouchDevice()) {
+ // Always use the simple mode for touch devices that do not have
+ // shift/ctrl keys
+ this.multiselectmode = MULTISELECT_MODE_SIMPLE;
+ } else {
+ this.multiselectmode = multiselectmode;
+ }
+
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @return true, if is selectable
+ */
+ public boolean isSelectable() {
+ return selectMode.getId() > SelectMode.NONE.getId();
+ }
+
+ /** Checks if is collapsed column.
+ *
+ * @param colKey
+ * the col key
+ * @return true, if is collapsed column
+ */
+ private boolean isCollapsedColumn(String colKey) {
+ if (collapsedColumns == null) {
+ return false;
+ }
+ if (collapsedColumns.contains(colKey)) {
+ return true;
+ }
+ return false;
+ }
+
+ /** Gets the col key by index.
+ *
+ * @param index
+ * the index
+ * @return the col key by index
+ */
+ private String getColKeyByIndex(int index) {
+ return tHead.getHeaderCell(index).getColKey();
+ }
+
+ /**
+ * Note: not part of the official API, extend at your own risk. May be
+ * removed or replaced in the future.
+ *
+ * Sets the indicated column's width for headers and scrollBody alike.
+ *
+ * @param colIndex
+ * index of the modified column
+ * @param w
+ * new width (may be subject to modifications if doesn't meet
+ * minimum requirements)
+ * @param isDefinedWidth
+ * disables expand ratio if set true
+ */
+ protected void setColWidth(int colIndex, int w, boolean isDefinedWidth) {
+ final HeaderCell hcell = tHead.getHeaderCell(colIndex);
+
+ // Make sure that the column grows to accommodate the sort indicator if
+ // necessary.
+ // get min width with no indent or padding
+ int minWidth = hcell.getMinWidth(false, false);
+ if (w < minWidth) {
+ w = minWidth;
+ }
+
+ // Set header column width WITHOUT INDENT
+ hcell.setWidth(w, isDefinedWidth);
+
+ // Set footer column width likewise
+ FooterCell fcell = tFoot.getFooterCell(colIndex);
+ fcell.setWidth(w, isDefinedWidth);
+
+ // Ensure indicators have been taken into account
+ tHead.resizeCaptionContainer(hcell);
+
+ // Make sure that the body column grows to accommodate the indent if
+ // necessary.
+ // get min width with indent, no padding
+ minWidth = hcell.getMinWidth(true, false);
+ if (w < minWidth) {
+ w = minWidth;
+ }
+
+ // Set body column width
+ scrollBody.setColWidth(colIndex, w);
+ }
+
+ /** Gets the col width.
+ *
+ * @param colKey
+ * the col key
+ * @return the col width
+ */
+ private int getColWidth(String colKey) {
+ return tHead.getHeaderCell(colKey).getWidthWithIndent();
+ }
+
+ /** Get a rendered row by its key.
+ *
+ * @param key
+ * The key to search with
+ * @return the rendered row by key
+ */
+ public VScrollTableRow getRenderedRowByKey(String key) {
+ if (scrollBody != null) {
+ final Iterator<Widget> it = scrollBody.iterator();
+ VScrollTableRow r = null;
+ while (it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ if (r.getKey().equals(key)) {
+ return r;
+ }
+ }
+ }
+ return null;
+ }
+
+ /** Returns the next row to the given row.
+ *
+ * @param row
+ * The row to calculate from
+ * @param offset
+ * the offset
+ * @return The next row or null if no row exists
+ */
+ private VScrollTableRow getNextRow(VScrollTableRow row, int offset) {
+ final Iterator<Widget> it = scrollBody.iterator();
+ VScrollTableRow r = null;
+ while (it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ if (r == row) {
+ r = null;
+ while (offset >= 0 && it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ offset--;
+ }
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the previous row from the given row.
+ *
+ * @param row
+ * The row to calculate from
+ * @param offset
+ * the offset
+ * @return The previous row or null if no row exists
+ */
+ private VScrollTableRow getPreviousRow(VScrollTableRow row, int offset) {
+ final Iterator<Widget> it = scrollBody.iterator();
+ final Iterator<Widget> offsetIt = scrollBody.iterator();
+ VScrollTableRow r = null;
+ VScrollTableRow prev = null;
+ while (it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ if (offset < 0) {
+ prev = (VScrollTableRow) offsetIt.next();
+ }
+ if (r == row) {
+ return prev;
+ }
+ offset--;
+ }
+
+ return null;
+ }
+
+ /** Re order column.
+ *
+ * @param columnKey
+ * the column key
+ * @param newIndex
+ * the new index
+ */
+ protected void reOrderColumn(String columnKey, int newIndex) {
+
+ final int oldIndex = getColIndexByKey(columnKey);
+
+ // Change header order
+ tHead.moveCell(oldIndex, newIndex);
+
+ // Change body order
+ scrollBody.moveCol(oldIndex, newIndex);
+
+ // Change footer order
+ tFoot.moveCell(oldIndex, newIndex);
+
+ /*
+ * Build new columnOrder and update it to server Note that columnOrder
+ * also contains collapsed columns so we cannot directly build it from
+ * cells vector Loop the old columnOrder and append in order to new
+ * array unless on moved columnKey. On new index also put the moved key
+ * i == index on columnOrder, j == index on newOrder
+ */
+ final String oldKeyOnNewIndex = visibleColOrder[newIndex];
+ if (showRowHeaders) {
+ newIndex--; // columnOrder don't have rowHeader
+ }
+ // add back hidden rows,
+ for (int i = 0; i < columnOrder.length; i++) {
+ if (columnOrder[i].equals(oldKeyOnNewIndex)) {
+ break; // break loop at target
+ }
+ if (isCollapsedColumn(columnOrder[i])) {
+ newIndex++;
+ }
+ }
+ // finally we can build the new columnOrder for server
+ final String[] newOrder = new String[columnOrder.length];
+ for (int i = 0, j = 0; j < newOrder.length; i++) {
+ if (j == newIndex) {
+ newOrder[j] = columnKey;
+ j++;
+ }
+ if (i == columnOrder.length) {
+ break;
+ }
+ if (columnOrder[i].equals(columnKey)) {
+ continue;
+ }
+ newOrder[j] = columnOrder[i];
+ j++;
+ }
+ columnOrder = newOrder;
+ // also update visibleColumnOrder
+ int i = showRowHeaders ? 1 : 0;
+ for (int j = 0; j < newOrder.length; j++) {
+ final String cid = newOrder[j];
+ if (!isCollapsedColumn(cid)) {
+ visibleColOrder[i++] = cid;
+ }
+ }
+ client.updateVariable(paintableId, "columnorder", columnOrder, false);
+ if (client.hasEventListeners(this,
+ TableConstants.COLUMN_REORDER_EVENT_ID)) {
+ client.sendPendingVariableChanges();
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Widget#onDetach()
+ */
+ @Override
+ protected void onDetach() {
+ detachedScrollPosition = scrollBodyPanel.getScrollPosition();
+ rowRequestHandler.cancel();
+ super.onDetach();
+ // ensure that scrollPosElement will be detached
+ if (scrollPositionElement != null) {
+ final Element parent = DOM.getParent(scrollPositionElement);
+ if (parent != null) {
+ DOM.removeChild(parent, scrollPositionElement);
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Widget#onAttach()
+ */
+ @Override
+ public void onAttach() {
+ super.onAttach();
+ scrollBodyPanel.setScrollPosition(detachedScrollPosition);
+ }
+
+ /**
+ * Run only once when component is attached and received its initial
+ * content. This function:
+ *
+ * * Syncs headers and bodys "natural widths and saves the values.
+ *
+ * * Sets proper width and height
+ *
+ * * Makes deferred request to get some cache rows
+ *
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public void sizeInit() {
+ sizeNeedsInit = false;
+
+ scrollBody.setContainerHeight();
+
+ /*
+ * We will use browsers table rendering algorithm to find proper column
+ * widths. If content and header take less space than available, we will
+ * divide extra space relatively to each column which has not width set.
+ *
+ * Overflow pixels are added to last column.
+ */
+
+ Iterator<Widget> headCells = tHead.iterator();
+ Iterator<Widget> footCells = tFoot.iterator();
+ int i = 0;
+ int totalExplicitColumnsWidths = 0;
+ int total = 0;
+ float expandRatioDivider = 0;
+
+ final int[] widths = new int[tHead.visibleCells.size()];
+
+ tHead.enableBrowserIntelligence();
+ tFoot.enableBrowserIntelligence();
+
+ int hierarchyColumnIndent = scrollBody != null ? scrollBody
+ .getMaxIndent() : 0;
+ HeaderCell hierarchyHeaderWithExpandRatio = null;
+
+ // first loop: collect natural widths
+ while (headCells.hasNext()) {
+ final HeaderCell hCell = (HeaderCell) headCells.next();
+ final FooterCell fCell = (FooterCell) footCells.next();
+ boolean needsIndent = hierarchyColumnIndent > 0
+ && hCell.isHierarchyColumn();
+ int w = hCell.getWidth();
+ if (hCell.isDefinedWidth()) {
+ // server has defined column width explicitly
+ if (needsIndent && w < hierarchyColumnIndent) {
+ // hierarchy indent overrides explicitly set width
+ w = hierarchyColumnIndent;
+ }
+ totalExplicitColumnsWidths += w;
+ } else {
+ if (hCell.getExpandRatio() > 0) {
+ expandRatioDivider += hCell.getExpandRatio();
+ w = 0;
+ if (needsIndent && w < hierarchyColumnIndent) {
+ hierarchyHeaderWithExpandRatio = hCell;
+ // don't add to widths here, because will be included in
+ // the expand ratio space if there's enough of it
+ }
+ } else {
+ // get and store greater of header width and column width,
+ // and store it as a minimum natural column width (these
+ // already contain the indent if any)
+ int headerWidth = hCell.getNaturalColumnWidth(i);
+ int footerWidth = fCell.getNaturalColumnWidth(i);
+ w = headerWidth > footerWidth ? headerWidth : footerWidth;
+ }
+ hCell.setNaturalMinimumColumnWidth(w);
+ fCell.setNaturalMinimumColumnWidth(w);
+ }
+ widths[i] = w;
+ total += w;
+ i++;
+ }
+ if (hierarchyHeaderWithExpandRatio != null) {
+ total += hierarchyColumnIndent;
+ }
+
+ tHead.disableBrowserIntelligence();
+ tFoot.disableBrowserIntelligence();
+
+ boolean willHaveScrollbarz = willHaveScrollbars();
+
+ // fix "natural" width if width not set
+ if (isDynamicWidth()) {
+ int w = total;
+ w += scrollBody.getCellExtraWidth() * visibleColOrder.length;
+ if (willHaveScrollbarz) {
+ w += 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
+
+ if (availW > total) {
+ // natural size is smaller than available space
+ int extraSpace = availW - total;
+ if (hierarchyHeaderWithExpandRatio != null) {
+ /*
+ * add the indent's space back to ensure each column gets an
+ * even share according to the expand ratios (note: if the
+ * allocated space isn't enough for the hierarchy column it
+ * shall be treated like a defined width column and the indent
+ * space gets removed from the extra space again)
+ */
+ extraSpace += hierarchyColumnIndent;
+ }
+ final int totalWidthR = total - totalExplicitColumnsWidths;
+ int checksum = 0;
+
+ if (extraSpace == 1) {
+ // We cannot divide one single pixel so we give it the first
+ // undefined column
+ // no need to worry about indent here
+ headCells = tHead.iterator();
+ i = 0;
+ checksum = availW;
+ while (headCells.hasNext()) {
+ HeaderCell hc = (HeaderCell) headCells.next();
+ if (!hc.isDefinedWidth()) {
+ widths[i]++;
+ break;
+ }
+ i++;
+ }
+
+ } else if (expandRatioDivider > 0) {
+ boolean setIndentToHierarchyHeader = false;
+ if (hierarchyHeaderWithExpandRatio != null) {
+ // ensure first that the hierarchyColumn gets at least the
+ // space allocated for indent
+ final int newSpace = Math
+ .round((extraSpace * (hierarchyHeaderWithExpandRatio
+ .getExpandRatio() / expandRatioDivider)));
+ if (newSpace < hierarchyColumnIndent) {
+ // not enough space for indent, remove indent from the
+ // extraSpace again and handle hierarchy column's header
+ // separately
+ setIndentToHierarchyHeader = true;
+ extraSpace -= hierarchyColumnIndent;
+ }
+ }
+
+ // visible columns have some active expand ratios, excess
+ // space is divided according to them
+ headCells = tHead.iterator();
+ i = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hCell = (HeaderCell) headCells.next();
+ if (hCell.getExpandRatio() > 0) {
+ int w = widths[i];
+ if (setIndentToHierarchyHeader
+ && hierarchyHeaderWithExpandRatio.equals(hCell)) {
+ // hierarchy column's header is no longer part of
+ // the expansion divide and only gets indent
+ w += hierarchyColumnIndent;
+ } else {
+ final int newSpace = Math
+ .round((extraSpace * (hCell
+ .getExpandRatio() / expandRatioDivider)));
+ w += newSpace;
+ }
+ widths[i] = w;
+ }
+ checksum += widths[i];
+ i++;
+ }
+ } else if (totalWidthR > 0) {
+ // no expand ratios defined, we will share extra space
+ // relatively to "natural widths" among those without
+ // explicit width
+ // no need to worry about indent here, it's already included
+ headCells = tHead.iterator();
+ i = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hCell = (HeaderCell) headCells.next();
+ if (!hCell.isDefinedWidth()) {
+ int w = widths[i];
+ final int newSpace = Math.round((float) extraSpace
+ * (float) w / totalWidthR);
+ w += newSpace;
+ widths[i] = w;
+ }
+ checksum += widths[i];
+ i++;
+ }
+ }
+
+ if (extraSpace > 0 && checksum != availW) {
+ /*
+ * There might be in some cases a rounding error of 1px when
+ * extra space is divided so if there is one then we give the
+ * first undefined column 1 more pixel
+ */
+ headCells = tHead.iterator();
+ i = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hc = (HeaderCell) headCells.next();
+ if (!hc.isDefinedWidth()) {
+ widths[i] += availW - checksum;
+ break;
+ }
+ i++;
+ }
+ }
+
+ } else {
+ // body's size will be more than available and scrollbar will appear
+ }
+
+ // last loop: set possibly modified values or reset if new tBody
+ i = 0;
+ headCells = tHead.iterator();
+ while (headCells.hasNext()) {
+ final HeaderCell hCell = (HeaderCell) headCells.next();
+ if (isNewBody || hCell.getWidth() == -1) {
+ final int w = widths[i];
+ setColWidth(i, w, false);
+ }
+ i++;
+ }
+
+ initializedAndAttached = true;
+
+ updatePageLength();
+
+ /*
+ * Fix "natural" height if height is not set. This must be after width
+ * fixing so the components' widths have been adjusted.
+ */
+ if (isDynamicHeight()) {
+ /*
+ * We must force an update of the row height as this point as it
+ * might have been (incorrectly) calculated earlier
+ */
+
+ /*
+ * TreeTable updates stuff in a funky order, so we must set the
+ * height as zero here before doing the real update to make it
+ * realize that there is no content,
+ */
+ if (pageLength == totalRows && pageLength == 0) {
+ scrollBody.setHeight("0px");
+ }
+
+ int bodyHeight;
+ if (pageLength == totalRows) {
+ /*
+ * A hack to support variable height rows when paging is off.
+ * Generally this is not supported by scrolltable. We want to
+ * show all rows so the bodyHeight should be equal to the table
+ * height.
+ */
+ // int bodyHeight = scrollBody.getOffsetHeight();
+ bodyHeight = scrollBody.getRequiredHeight();
+ } else {
+ bodyHeight = (int) Math.round(scrollBody.getRowHeight(true)
+ * pageLength);
+ }
+ boolean needsSpaceForHorizontalSrollbar = (total > availW);
+ if (needsSpaceForHorizontalSrollbar) {
+ bodyHeight += Util.getNativeScrollbarSize();
+ }
+ scrollBodyPanel.setHeight(bodyHeight + "px");
+ Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ }
+
+ isNewBody = false;
+
+ if (firstvisible > 0) {
+ enableLazyScroller();
+ }
+
+ if (enabled) {
+ // Do we need cache rows
+ if (scrollBody.getLastRendered() + 1 < firstRowInViewPort
+ + pageLength + (int) cache_react_rate * pageLength) {
+ if (totalRows - 1 > scrollBody.getLastRendered()) {
+ // fetch cache rows
+ int firstInNewSet = scrollBody.getLastRendered() + 1;
+ int lastInNewSet = (int) (firstRowInViewPort + pageLength + cache_rate
+ * pageLength);
+ if (lastInNewSet > totalRows - 1) {
+ lastInNewSet = totalRows - 1;
+ }
+ rowRequestHandler.triggerRowFetch(firstInNewSet,
+ lastInNewSet - firstInNewSet + 1, 1);
+ }
+ }
+ }
+
+ /*
+ * Ensures the column alignments are correct at initial loading. <br>
+ * (child components widths are correct)
+ */
+ Util.runWebkitOverflowAutoFixDeferred(scrollBodyPanel.getElement());
+
+ hadScrollBars = willHaveScrollbarz;
+ }
+
+ /**
+ * Note: this method is not part of official API although declared as
+ * protected. Extend at your own risk.
+ *
+ * @return true if content area will have scrollbars visible.
+ */
+ protected boolean willHaveScrollbars() {
+ if (isDynamicHeight()) {
+ if (pageLength < totalRows) {
+ return true;
+ }
+ } else {
+ int fakeheight = (int) Math.round(scrollBody.getRowHeight()
+ * totalRows);
+ int availableHeight = scrollBodyPanel.getElement().getPropertyInt(
+ "clientHeight");
+ if (fakeheight > availableHeight) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Announce scroll position.
+ */
+ private void announceScrollPosition() {
+ if (scrollPositionElement == null) {
+ scrollPositionElement = DOM.createDiv();
+ scrollPositionElement.setClassName(getStylePrimaryName()
+ + "-scrollposition");
+ scrollPositionElement.getStyle().setPosition(Position.ABSOLUTE);
+ scrollPositionElement.getStyle().setDisplay(Display.NONE);
+ getElement().appendChild(scrollPositionElement);
+ }
+
+ Style style = scrollPositionElement.getStyle();
+ style.setMarginLeft(getElement().getOffsetWidth() / 2 - 80, Unit.PX);
+ style.setMarginTop(-scrollBodyPanel.getOffsetHeight(), Unit.PX);
+
+ // indexes go from 1-totalRows, as rowheaders in index-mode indicate
+ int last = (firstRowInViewPort + pageLength);
+ if (last > totalRows) {
+ last = totalRows;
+ }
+ scrollPositionElement.setInnerHTML("<span>" + (firstRowInViewPort + 1)
+ + " – " + (last) + "..." + "</span>");
+ style.setDisplay(Display.BLOCK);
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void hideScrollPositionAnnotation() {
+ if (scrollPositionElement != null) {
+ scrollPositionElement.getStyle().setDisplay(Display.NONE);
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future.
+ *
+ * @return true, if is scroll position visible
+ */
+ public boolean isScrollPositionVisible() {
+ return scrollPositionElement != null
+ && !scrollPositionElement.getStyle().getDisplay()
+ .equals(Display.NONE.toString());
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public class RowRequestHandler extends Timer {
+
+ /** The req first row. */
+ private int reqFirstRow = 0;
+
+ /** The req rows. */
+ private int reqRows = 0;
+
+ /** The is request handler running. */
+ private boolean isRequestHandlerRunning = false;
+
+ /** Trigger row fetch.
+ *
+ * @param first
+ * the first
+ * @param rows
+ * the rows
+ */
+ public void triggerRowFetch(int first, int rows) {
+ setReqFirstRow(first);
+ setReqRows(rows);
+ deferRowFetch();
+ }
+
+ /** Trigger row fetch.
+ *
+ * @param first
+ * the first
+ * @param rows
+ * the rows
+ * @param delay
+ * the delay
+ */
+ public void triggerRowFetch(int first, int rows, int delay) {
+ setReqFirstRow(first);
+ setReqRows(rows);
+ deferRowFetch(delay);
+ }
+
+ /** Defer row fetch.
+ */
+ public void deferRowFetch() {
+ deferRowFetch(250);
+ }
+
+ /** Checks if is request handler running.
+ *
+ * @return true, if is request handler running
+ */
+ public boolean isRequestHandlerRunning() {
+ return isRequestHandlerRunning;
+ }
+
+ /** Defer row fetch.
+ *
+ * @param msec
+ * the msec
+ */
+ public void deferRowFetch(int msec) {
+ isRequestHandlerRunning = true;
+ if (reqRows > 0 && reqFirstRow < totalRows) {
+ schedule(msec);
+
+ // tell scroll position to user if currently "visible" rows are
+ // not rendered
+ if (totalRows > pageLength
+ && ((firstRowInViewPort + pageLength > scrollBody
+ .getLastRendered()) || (firstRowInViewPort < scrollBody
+ .getFirstRendered()))) {
+ announceScrollPosition();
+ } else {
+ hideScrollPositionAnnotation();
+ }
+ }
+ }
+
+ /** Gets the req first row.
+ *
+ * @return the req first row
+ */
+ public int getReqFirstRow() {
+ return reqFirstRow;
+ }
+
+ /** Sets the req first row.
+ *
+ * @param reqFirstRow
+ * the new req first row
+ */
+ public void setReqFirstRow(int reqFirstRow) {
+ if (reqFirstRow < 0) {
+ this.reqFirstRow = 0;
+ } else if (reqFirstRow >= totalRows) {
+ this.reqFirstRow = totalRows - 1;
+ } else {
+ this.reqFirstRow = reqFirstRow;
+ }
+ }
+
+ /** Sets the req rows.
+ *
+ * @param reqRows
+ * the new req rows
+ */
+ public void setReqRows(int reqRows) {
+ if (reqRows < 0) {
+ this.reqRows = 0;
+ } else if (reqFirstRow + reqRows > totalRows) {
+ this.reqRows = totalRows - reqFirstRow;
+ } else {
+ this.reqRows = reqRows;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.Timer#run()
+ */
+ @Override
+ public void run() {
+ if (client.getMessageSender().hasActiveRequest() || navKeyDown) {
+ // if client connection is busy, don't bother loading it more
+ VConsole.log("Postponed rowfetch");
+ schedule(250);
+ } else if (!updatedReqRows && allRenderedRowsAreNew()) {
+
+ /*
+ * If all rows are new, there might have been a server-side call
+ * to Table.setCurrentPageFirstItemIndex(int) In this case,
+ * scrolling event takes way too late, and all the rows from
+ * previous viewport to this one were requested.
+ *
+ * This should prevent requesting unneeded rows by updating
+ * reqFirstRow and reqRows before needing them. See (#14135)
+ */
+
+ setReqFirstRow((firstRowInViewPort - (int) (pageLength * cache_rate)));
+ int last = firstRowInViewPort + (int) (cache_rate * pageLength)
+ + pageLength - 1;
+ if (last >= totalRows) {
+ last = totalRows - 1;
+ }
+ setReqRows(last - getReqFirstRow() + 1);
+ updatedReqRows = true;
+ schedule(250);
+ } else {
+
+ int firstRendered = scrollBody.getFirstRendered();
+ int lastRendered = scrollBody.getLastRendered();
+ if (lastRendered > totalRows) {
+ lastRendered = totalRows - 1;
+ }
+ boolean rendered = firstRendered >= 0 && lastRendered >= 0;
+
+ int firstToBeRendered = firstRendered;
+
+ if (reqFirstRow < firstToBeRendered) {
+ firstToBeRendered = reqFirstRow;
+ } else if (firstRowInViewPort - (int) (cache_rate * pageLength) > firstToBeRendered) {
+ firstToBeRendered = firstRowInViewPort
+ - (int) (cache_rate * pageLength);
+ if (firstToBeRendered < 0) {
+ firstToBeRendered = 0;
+ }
+ } else if (rendered && firstRendered + 1 < reqFirstRow
+ && lastRendered + 1 < reqFirstRow) {
+ // requested rows must fall within the requested rendering
+ // area
+ firstToBeRendered = reqFirstRow;
+ }
+ if (firstToBeRendered + reqRows < firstRendered) {
+ // must increase the required row count accordingly,
+ // otherwise may leave a gap and the rows beyond will get
+ // removed
+ setReqRows(firstRendered - firstToBeRendered);
+ }
+
+ int lastToBeRendered = lastRendered;
+ int lastReqRow = reqFirstRow + reqRows - 1;
+
+ if (lastReqRow > lastToBeRendered) {
+ lastToBeRendered = lastReqRow;
+ } else if (firstRowInViewPort + pageLength + pageLength
+ * cache_rate < lastToBeRendered) {
+ lastToBeRendered = (firstRowInViewPort + pageLength + (int) (pageLength * cache_rate));
+ if (lastToBeRendered >= totalRows) {
+ lastToBeRendered = totalRows - 1;
+ }
+ // due Safari 3.1 bug (see #2607), verify reqrows, original
+ // problem unknown, but this should catch the issue
+ if (lastReqRow > lastToBeRendered) {
+ setReqRows(lastToBeRendered - reqFirstRow);
+ }
+ } else if (rendered && lastRendered - 1 > lastReqRow
+ && firstRendered - 1 > lastReqRow) {
+ // requested rows must fall within the requested rendering
+ // area
+ lastToBeRendered = lastReqRow;
+ }
+
+ if (lastToBeRendered > totalRows) {
+ lastToBeRendered = totalRows - 1;
+ }
+ if (reqFirstRow < firstToBeRendered
+ || (reqFirstRow > firstToBeRendered && (reqFirstRow < firstRendered || reqFirstRow > lastRendered + 1))) {
+ setReqFirstRow(firstToBeRendered);
+ }
+ if (lastRendered < lastToBeRendered
+ && lastRendered + reqRows < lastToBeRendered) {
+ // must increase the required row count accordingly,
+ // otherwise may leave a gap and the rows after will get
+ // removed
+ setReqRows(lastToBeRendered - lastRendered);
+ } else if (lastToBeRendered >= firstRendered
+ && reqFirstRow + reqRows < firstRendered) {
+ setReqRows(lastToBeRendered - lastRendered);
+ }
+
+ client.updateVariable(paintableId, "firstToBeRendered",
+ firstToBeRendered, false);
+ client.updateVariable(paintableId, "lastToBeRendered",
+ lastToBeRendered, false);
+
+ // don't request server to update page first index in case it
+ // has not been changed
+ if (firstRowInViewPort != firstvisible) {
+ // remember which firstvisible we requested, in case the
+ // server has a differing opinion
+ lastRequestedFirstvisible = firstRowInViewPort;
+ client.updateVariable(paintableId, "firstvisible",
+ firstRowInViewPort, false);
+ }
+ client.updateVariable(paintableId, "reqfirstrow", reqFirstRow,
+ false);
+ client.updateVariable(paintableId, "reqrows", reqRows, true);
+
+ if (selectionChanged) {
+ unSyncedselectionsBeforeRowFetch = new HashSet<Object>(
+ selectedRowKeys);
+ }
+ isRequestHandlerRunning = false;
+ }
+ }
+
+ /**
+ * Sends request to refresh content at this position.
+ */
+ public void refreshContent() {
+ isRequestHandlerRunning = true;
+ int first = (int) (firstRowInViewPort - pageLength * cache_rate);
+ int reqRows = (int) (2 * pageLength * cache_rate + pageLength);
+ if (first < 0) {
+ reqRows = reqRows + first;
+ first = 0;
+ }
+ setReqFirstRow(first);
+ setReqRows(reqRows);
+ run();
+ }
+ }
+
+ /** The Class HeaderCell.
+ */
+ public class HeaderCell extends Widget {
+
+ /** The td. */
+ Element td = DOM.createTD();
+
+ /** The caption container. */
+ Element captionContainer = DOM.createDiv();
+
+ /** The sort indicator. */
+ Element sortIndicator = DOM.createDiv();
+
+ /** The col resize widget. */
+ Element colResizeWidget = DOM.createDiv();
+
+ /** The floating copy of header cell. */
+ Element floatingCopyOfHeaderCell;
+
+ /** The sortable. */
+ private boolean sortable = false;
+
+ /** The cid. */
+ private final String cid;
+
+ /** The dragging. */
+ private boolean dragging;
+
+ /** The drag start x. */
+ private int dragStartX;
+
+ /** The col index. */
+ private int colIndex;
+
+ /** The original width. */
+ private int originalWidth;
+
+ /** The is resizing. */
+ private boolean isResizing;
+
+ /** The header x. */
+ private int headerX;
+
+ /** The moved. */
+ private boolean moved;
+
+ /** The closest slot. */
+ private int closestSlot;
+
+ /** The width. */
+ private int width = -1;
+
+ /** The natural width. */
+ private int naturalWidth = -1;
+
+ /** The align. */
+ private char align = ALIGN_LEFT;
+
+ /** The defined width. */
+ boolean definedWidth = false;
+
+ /** The expand ratio. */
+ private float expandRatio = 0;
+
+ /** The sorted. */
+ private boolean sorted;
+
+ /** Sets the sortable.
+ *
+ * @param b
+ * the new sortable
+ */
+ 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
+ *
+ * @param rightSpacing
+ * the right spacing
+ */
+ 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();
+ }
+ }
+
+ /** Sets the natural minimum column width.
+ *
+ * @param w
+ * the new natural minimum column width
+ */
+ public void setNaturalMinimumColumnWidth(int w) {
+ naturalWidth = w;
+ }
+
+ /** Instantiates a new header cell.
+ *
+ * @param colId
+ * the col id
+ * @param headerText
+ * the header text
+ */
+ public HeaderCell(String colId, String headerText) {
+ cid = colId;
+
+ setText(headerText);
+
+ td.appendChild(colResizeWidget);
+
+ // ensure no clipping initially (problem on column additions)
+ captionContainer.getStyle().setOverflow(Overflow.VISIBLE);
+
+ td.appendChild(sortIndicator);
+ td.appendChild(captionContainer);
+
+ DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK
+ | Event.ONCONTEXTMENU | Event.TOUCHEVENTS);
+
+ setElement(td);
+
+ setAlign(ALIGN_LEFT);
+ }
+
+ /** Update style names.
+ *
+ * @param primaryStyleName
+ * the primary style name
+ */
+ protected void updateStyleNames(String primaryStyleName) {
+ colResizeWidget.setClassName(primaryStyleName + "-resizer");
+ sortIndicator.setClassName(primaryStyleName + "-sort-indicator");
+ captionContainer.setClassName(primaryStyleName
+ + "-caption-container");
+ if (sorted) {
+ if (sortAscending) {
+ setStyleName(primaryStyleName + "-header-cell-asc");
+ } else {
+ setStyleName(primaryStyleName + "-header-cell-desc");
+ }
+ } else {
+ setStyleName(primaryStyleName + "-header-cell");
+ }
+
+ final String ALIGN_PREFIX = primaryStyleName
+ + "-caption-container-align-";
+
+ switch (align) {
+ case ALIGN_CENTER:
+ captionContainer.addClassName(ALIGN_PREFIX + "center");
+ break;
+ case ALIGN_RIGHT:
+ captionContainer.addClassName(ALIGN_PREFIX + "right");
+ break;
+ default:
+ captionContainer.addClassName(ALIGN_PREFIX + "left");
+ break;
+ }
+
+ }
+
+ /** Disable auto width calculation.
+ */
+ public void disableAutoWidthCalculation() {
+ definedWidth = true;
+ expandRatio = 0;
+ }
+
+ /**
+ * Sets width to the header cell. This width should not include any
+ * possible indent modifications that are present in
+ * {@link VScrollTableBody#getMaxIndent()}.
+ *
+ * @param w
+ * required width of the cell sans indentations
+ * @param ensureDefinedWidth
+ * disables expand ratio if required
+ */
+ public void setWidth(int w, boolean ensureDefinedWidth) {
+ if (ensureDefinedWidth) {
+ definedWidth = true;
+ // on column resize expand ratio becomes zero
+ expandRatio = 0;
+ }
+ if (width == -1) {
+ // go to default mode, clip content if necessary
+ captionContainer.getStyle().clearOverflow();
+ }
+ width = w;
+ if (w == -1) {
+ captionContainer.getStyle().clearWidth();
+ setWidth("");
+ } else {
+ tHead.resizeCaptionContainer(this);
+
+ /*
+ * if we already have tBody, set the header width properly, if
+ * not defer it. IE will fail with complex float in table header
+ * unless TD width is not explicitly set.
+ */
+ if (scrollBody != null) {
+ int maxIndent = scrollBody.getMaxIndent();
+ if (w < maxIndent && isHierarchyColumn()) {
+ w = maxIndent;
+ }
+ int tdWidth = w + scrollBody.getCellExtraWidth();
+ setWidth(tdWidth + "px");
+ } else {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ int maxIndent = scrollBody.getMaxIndent();
+ int tdWidth = width;
+ if (tdWidth < maxIndent && isHierarchyColumn()) {
+ tdWidth = maxIndent;
+ }
+ tdWidth += scrollBody.getCellExtraWidth();
+ setWidth(tdWidth + "px");
+ }
+ });
+ }
+ }
+ }
+
+ /** Sets the undefined width.
+ */
+ public void setUndefinedWidth() {
+ definedWidth = false;
+ if (!isResizing) {
+ 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;
+ }
+
+ /**
+ * This method exists for the needs of {@link VTreeTable} only.
+ *
+ * Returns the pixels width of the header cell. This includes the
+ * indent, if applicable.
+ *
+ * @return The width in pixels
+ */
+ public int getWidthWithIndent() {
+ if (scrollBody != null && isHierarchyColumn()) {
+ int maxIndent = scrollBody.getMaxIndent();
+ if (maxIndent > width) {
+ return maxIndent;
+ }
+ }
+ return width;
+ }
+
+ /**
+ * Returns the pixels width of the header cell.
+ *
+ * @return The width in pixels
+ */
+ public int getWidth() {
+ return width;
+ }
+
+ /**
+ * This method exists for the needs of {@link VTreeTable} only.
+ *
+ * @return <code>true</code> if this is hierarcyColumn's header cell,
+ * <code>false</code> otherwise
+ */
+ private boolean isHierarchyColumn() {
+ int hierarchyColumnIndex = getHierarchyColumnIndex();
+ return hierarchyColumnIndex >= 0
+ && tHead.visibleCells.indexOf(this) == hierarchyColumnIndex;
+ }
+
+ /** Sets the text.
+ *
+ * @param headerText
+ * the new text
+ */
+ public void setText(String headerText) {
+ DOM.setInnerHTML(captionContainer, headerText);
+ }
+
+ /** Gets the col key.
+ *
+ * @return the col key
+ */
+ public String getColKey() {
+ return cid;
+ }
+
+ /** Sets the sorted.
+ *
+ * @param sorted
+ * the new sorted
+ */
+ private void setSorted(boolean sorted) {
+ this.sorted = sorted;
+ updateStyleNames(VCustomScrollTable.this.getStylePrimaryName());
+ }
+
+ /** Handle column reordering.
+ *
+ * @param event
+ * the event
+ */
+
+ @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(
+ VCustomScrollTable.this,
+ TableConstants.HEADER_CLICK_EVENT_ID)) {
+ // Prevent showing the browser's context menu only when
+ // there is a header click listener.
+ stopPropagation = false;
+ }
+ if (stopPropagation) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+ }
+ }
+
+ /** Creates the floating copy.
+ */
+ private void createFloatingCopy() {
+ floatingCopyOfHeaderCell = DOM.createDiv();
+ DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td));
+ floatingCopyOfHeaderCell = DOM
+ .getChild(floatingCopyOfHeaderCell, 2);
+ // #12714 the shown "ghost element" should be inside
+ // v-overlay-container, and it should contain the same styles as the
+ // table to enable theming (except v-table & v-widget).
+ String stylePrimaryName = VCustomScrollTable.this
+ .getStylePrimaryName();
+ StringBuilder sb = new StringBuilder();
+ for (String s : VCustomScrollTable.this.getStyleName().split(" ")) {
+ if (!s.equals(StyleConstants.UI_WIDGET)) {
+ sb.append(s);
+ if (s.equals(stylePrimaryName)) {
+ sb.append("-header-drag ");
+ } else {
+ sb.append(" ");
+ }
+ }
+ }
+ floatingCopyOfHeaderCell.setClassName(sb.toString().trim());
+ // otherwise might wrap or be cut if narrow column
+ floatingCopyOfHeaderCell.getStyle().setProperty("width", "auto");
+ updateFloatingCopysPosition(DOM.getAbsoluteLeft(td),
+ DOM.getAbsoluteTop(td));
+ DOM.appendChild(VOverlay.getOverlayContainer(client),
+ floatingCopyOfHeaderCell);
+ }
+
+ /** Update floating copys position.
+ *
+ * @param x
+ * the x
+ * @param y
+ * the y
+ */
+ private void updateFloatingCopysPosition(int x, int y) {
+ x -= DOM.getElementPropertyInt(floatingCopyOfHeaderCell,
+ "offsetWidth") / 2;
+ floatingCopyOfHeaderCell.getStyle().setLeft(x, Unit.PX);
+ if (y > 0) {
+ floatingCopyOfHeaderCell.getStyle().setTop(y + 7, Unit.PX);
+ }
+ }
+
+ /** Hide floating copy.
+ */
+ private void hideFloatingCopy() {
+ floatingCopyOfHeaderCell.removeFromParent();
+ floatingCopyOfHeaderCell = null;
+ }
+
+ /** Fires a header click event after the user has clicked a column
+ * header cell.
+ *
+ * @param event
+ * The click event
+ */
+ private void fireHeaderClickedEvent(Event event) {
+ if (client.hasEventListeners(VCustomScrollTable.this,
+ TableConstants.HEADER_CLICK_EVENT_ID)) {
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event);
+ client.updateVariable(paintableId, "headerClickEvent",
+ details.toString(), false);
+ client.updateVariable(paintableId, "headerClickCID", cid, true);
+ }
+ }
+
+ /** Handle caption event.
+ *
+ * @param event
+ * the event
+ */
+ 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);
+ // getColWidth only returns the internal width
+ // without padding, not the offset width of the
+ // whole td (#10890)
+ slotX += getColWidth(colKey)
+ + scrollBody.getCellExtraWidth();
+ }
+ final int dist = Math.abs(x - slotX);
+ if (closestDistance == -1 || dist < closestDistance) {
+ closestDistance = dist;
+ closestSlot = i;
+ }
+ }
+ tHead.focusSlot(closestSlot);
+
+ updateFloatingCopysPosition(clientX, -1);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ /** On resize event.
+ *
+ * @param event
+ * the event
+ */
+ 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 = getWidthWithIndent();
+ 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;
+ // get min width with indent, no padding
+ int minWidth = getMinWidth(true, false);
+ if (newWidth < minWidth) {
+ // already includes indent if any
+ newWidth = minWidth;
+ }
+ setColWidth(colIndex, newWidth, true);
+ triggerLazyColumnAdjustment(false);
+ forceRealignColumnHeaders();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ /** Returns the smallest possible cell width in pixels.
+ *
+ * @param includeIndent
+ * - width should include hierarchy column indent if
+ * applicable (VTreeTable only)
+ * @param includeCellExtraWidth
+ * - width should include paddings etc.
+ * @return the min width
+ */
+ private int getMinWidth(boolean includeIndent,
+ boolean includeCellExtraWidth) {
+ int minWidth = sortIndicator.getOffsetWidth();
+ if (scrollBody != null) {
+ // check the need for indent before adding paddings etc.
+ if (includeIndent && isHierarchyColumn()) {
+ int maxIndent = scrollBody.getMaxIndent();
+ if (minWidth < maxIndent) {
+ minWidth = maxIndent;
+ }
+ }
+ if (includeCellExtraWidth) {
+ minWidth += scrollBody.getCellExtraWidth();
+ }
+ }
+ return minWidth;
+ }
+
+ /** Gets the min width.
+ *
+ * @return the min width
+ */
+ public int getMinWidth() {
+ // get min width with padding, no indent
+ return getMinWidth(false, true);
+ }
+
+ /** Gets the caption.
+ *
+ * @return the caption
+ */
+ public String getCaption() {
+ return DOM.getInnerText(captionContainer);
+ }
+
+ /** Checks if is enabled.
+ *
+ * @return true, if is enabled
+ */
+ public boolean isEnabled() {
+ return getParent() != null;
+ }
+
+ /** Sets the align.
+ *
+ * @param c
+ * the new align
+ */
+ public void setAlign(char c) {
+ align = c;
+ updateStyleNames(VCustomScrollTable.this.getStylePrimaryName());
+ }
+
+ /** Gets the align.
+ *
+ * @return the align
+ */
+ 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 the natural column width
+ */
+ public int getNaturalColumnWidth(int columnIndex) {
+ final int iw = columnIndex == getHierarchyColumnIndex() ? scrollBody
+ .getMaxIndent() : 0;
+ if (isDefinedWidth()) {
+ if (iw > width) {
+ return iw;
+ }
+ return width;
+ } else {
+ if (naturalWidth < 0) {
+ // This is recently revealed column. Try to detect a proper
+ // value (greater of header and data columns)
+
+ int hw = captionContainer.getOffsetWidth()
+ + getHeaderPadding();
+ if (BrowserInfo.get().isGecko()) {
+ hw += sortIndicator.getOffsetWidth();
+ }
+ if (columnIndex < 0) {
+ columnIndex = 0;
+ for (Iterator<Widget> it = tHead.iterator(); it
+ .hasNext(); columnIndex++) {
+ if (it.next() == this) {
+ break;
+ }
+ }
+ }
+ final int cw = scrollBody.getColWidth(columnIndex);
+ naturalWidth = (hw > cw ? hw : cw);
+ }
+ if (iw > naturalWidth) {
+ // indent is temporary value, naturalWidth shouldn't be
+ // updated
+ return iw;
+ } else {
+ return naturalWidth;
+ }
+ }
+ }
+
+ /** Sets the expand ratio.
+ *
+ * @param floatAttribute
+ * the new expand ratio
+ */
+ public void setExpandRatio(float floatAttribute) {
+ if (floatAttribute != expandRatio) {
+ triggerLazyColumnAdjustment(false);
+ }
+ expandRatio = floatAttribute;
+ }
+
+ /** Gets the expand ratio.
+ *
+ * @return the expand ratio
+ */
+ public float getExpandRatio() {
+ return expandRatio;
+ }
+
+ /** Checks if is sorted.
+ *
+ * @return true, if is sorted
+ */
+ 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 {
+
+ /** Instantiates a new row headers header cell.
+ */
+ RowHeadersHeaderCell() {
+ super(ROW_HEADER_COLUMN_KEY, "");
+ updateStyleNames(VCustomScrollTable.this.getStylePrimaryName());
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.VCustomScrollTable.HeaderCell#updateStyleNames(java.lang.String)
+ */
+ @Override
+ protected void updateStyleNames(String primaryStyleName) {
+ super.updateStyleNames(primaryStyleName);
+ setStyleName(primaryStyleName + "-header-cell-rowheader");
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.VCustomScrollTable.HeaderCell#handleCaptionEvent(com.google.gwt.user.client.Event)
+ */
+ @Override
+ protected void handleCaptionEvent(Event event) {
+ // NOP: RowHeaders cannot be reordered
+ // TODO It'd be nice to reset sorting here
+ }
+ }
+
+ /** The Class TableHead.
+ */
+ public class TableHead extends Panel implements ActionOwner {
+
+ /** The Constant WRAPPER_WIDTH. */
+ private static final int WRAPPER_WIDTH = 900000;
+
+ /** The visible cells. */
+ ArrayList<Widget> visibleCells = new ArrayList<Widget>();
+
+ /** The available cells. */
+ HashMap<String, HeaderCell> availableCells = new HashMap<String, HeaderCell>();
+
+ /** The div. */
+ Element div = DOM.createDiv();
+
+ /** The h table wrapper. */
+ Element hTableWrapper = DOM.createDiv();
+
+ /** The h table container. */
+ Element hTableContainer = DOM.createDiv();
+
+ /** The table. */
+ Element table = DOM.createTable();
+
+ /** The header table body. */
+ Element headerTableBody = DOM.createTBody();
+
+ /** The tr. */
+ Element tr = DOM.createTR();
+
+ /** The column selector. */
+ private final Element columnSelector = DOM.createDiv();
+
+ /** The focused slot. */
+ private int focusedSlot = -1;
+
+ /** Instantiates a new table head.
+ */
+ public TableHead() {
+ if (BrowserInfo.get().isIE()) {
+ table.setPropertyInt("cellSpacing", 0);
+ }
+
+ hTableWrapper.getStyle().setOverflow(Overflow.HIDDEN);
+ columnSelector.getStyle().setDisplay(Display.NONE);
+
+ DOM.appendChild(table, headerTableBody);
+ DOM.appendChild(headerTableBody, tr);
+ DOM.appendChild(hTableContainer, table);
+ DOM.appendChild(hTableWrapper, hTableContainer);
+ DOM.appendChild(div, hTableWrapper);
+ DOM.appendChild(div, columnSelector);
+ setElement(div);
+
+ DOM.sinkEvents(columnSelector, Event.ONCLICK);
+
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersHeaderCell());
+ }
+
+ /** Update style names.
+ *
+ * @param primaryStyleName
+ * the primary style name
+ */
+ protected void updateStyleNames(String primaryStyleName) {
+ hTableWrapper.setClassName(primaryStyleName + "-header");
+ columnSelector.setClassName(primaryStyleName + "-column-selector");
+ setStyleName(primaryStyleName + "-header-wrap");
+ for (HeaderCell c : availableCells.values()) {
+ c.updateStyleNames(primaryStyleName);
+ }
+ }
+
+ /** Resize caption container.
+ *
+ * @param cell
+ * the cell
+ */
+ public void resizeCaptionContainer(HeaderCell cell) {
+ HeaderCell lastcell = getHeaderCell(visibleCells.size() - 1);
+ int columnSelectorOffset = columnSelector.getOffsetWidth();
+
+ if (cell == lastcell && columnSelectorOffset > 0
+ && !hasVerticalScrollbar()) {
+
+ // Measure column widths
+ int columnTotalWidth = 0;
+ for (Widget w : visibleCells) {
+ int cellExtraWidth = w.getOffsetWidth();
+ if (scrollBody != null
+ && visibleCells.indexOf(w) == getHierarchyColumnIndex()
+ && cellExtraWidth < scrollBody.getMaxIndent()) {
+ // indent must be taken into consideration even if it
+ // hasn't been applied yet
+ columnTotalWidth += scrollBody.getMaxIndent();
+ } else {
+ columnTotalWidth += cellExtraWidth;
+ }
+ }
+
+ int divOffset = div.getOffsetWidth();
+ if (columnTotalWidth >= divOffset - columnSelectorOffset) {
+ /*
+ * Ensure column caption is visible when placed under the
+ * column selector widget by shifting and resizing the
+ * caption.
+ */
+ int offset = 0;
+ int diff = divOffset - columnTotalWidth;
+ if (diff < columnSelectorOffset && diff > 0) {
+ /*
+ * If the difference is less than the column selectors
+ * width then just offset by the difference
+ */
+ offset = columnSelectorOffset - diff;
+ } else {
+ // Else offset by the whole column selector
+ offset = columnSelectorOffset;
+ }
+ lastcell.resizeCaptionContainer(offset);
+ } else {
+ cell.resizeCaptionContainer(0);
+ }
+ } else {
+ cell.resizeCaptionContainer(0);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Panel#clear()
+ */
+ @Override
+ public void clear() {
+ for (String cid : availableCells.keySet()) {
+ removeCell(cid);
+ }
+ availableCells.clear();
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersHeaderCell());
+ }
+
+ /** Update cells from uidl.
+ *
+ * @param uidl
+ * the uidl
+ */
+ public void updateCellsFromUIDL(UIDL uidl) {
+ Iterator<?> it = uidl.getChildIterator();
+ HashSet<String> updated = new HashSet<String>();
+ boolean refreshContentWidths = initializedAndAttached
+ && hadScrollBars != willHaveScrollbars();
+ while (it.hasNext()) {
+ final UIDL col = (UIDL) it.next();
+ final String cid = col.getStringAttribute("cid");
+ updated.add(cid);
+
+ String caption = buildCaptionHtmlSnippet(col);
+ HeaderCell c = getHeaderCell(cid);
+ if (c == null) {
+ c = new HeaderCell(cid, caption);
+ availableCells.put(cid, c);
+ if (initializedAndAttached) {
+ // we will need a column width recalculation
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ } else {
+ c.setText(caption);
+ }
+
+ if (col.hasAttribute("sortable")) {
+ c.setSortable(true);
+ 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") && !c.isResizing) {
+ // Make sure to accomodate for the sort indicator if
+ // necessary.
+ int width = col.getIntAttribute("width");
+ int widthWithoutAddedIndent = width;
+
+ // get min width with indent, no padding
+ int minWidth = c.getMinWidth(true, false);
+ if (width < minWidth) {
+ width = minWidth;
+ }
+ if (scrollBody != null && width != c.getWidthWithIndent()) {
+ // Do a more thorough update if a column is resized from
+ // the server *after* the header has been properly
+ // initialized
+ final int colIx = getColIndexByKey(c.cid);
+ final int newWidth = width;
+ Scheduler.get().scheduleDeferred(
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ setColWidth(colIx, newWidth, true);
+ }
+ });
+ refreshContentWidths = true;
+ } else {
+ // get min width with no indent or padding
+ minWidth = c.getMinWidth(false, false);
+ if (widthWithoutAddedIndent < minWidth) {
+ widthWithoutAddedIndent = minWidth;
+ }
+ // save min width without indent
+ c.setWidth(widthWithoutAddedIndent, true);
+ }
+ } else if (col.hasAttribute("er")) {
+ c.setExpandRatio(col.getFloatAttribute("er"));
+
+ } else if (recalcWidths) {
+ c.setUndefinedWidth();
+
+ } else {
+ boolean hadExpandRatio = c.getExpandRatio() > 0;
+ boolean hadDefinedWidth = c.isDefinedWidth();
+ if (hadExpandRatio || hadDefinedWidth) {
+ // Someone has removed a expand width or the defined
+ // width on the server side (setting it to -1), make the
+ // column undefined again and measure columns again.
+ c.setUndefinedWidth();
+ c.setExpandRatio(0);
+ refreshContentWidths = true;
+ }
+ }
+
+ if (col.hasAttribute("collapsed")) {
+ // ensure header is properly removed from parent (case when
+ // collapsing happens via servers side api)
+ if (c.isAttached()) {
+ c.removeFromParent();
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ if (refreshContentWidths) {
+ // Recalculate the column sizings if any column has changed
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ triggerLazyColumnAdjustment(true);
+ }
+ });
+ }
+
+ // check for orphaned header cells
+ for (Iterator<String> cit = availableCells.keySet().iterator(); cit
+ .hasNext();) {
+ String cid = cit.next();
+ if (!updated.contains(cid)) {
+ removeCell(cid);
+ cit.remove();
+ // we will need a column width recalculation, since columns
+ // with expand ratios should expand to fill the void.
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ }
+ }
+
+ /** Enable column.
+ *
+ * @param cid
+ * the cid
+ * @param index
+ * the index
+ */
+ 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;
+ }
+ }
+ }
+
+ /** Gets the visible cell count.
+ *
+ * @return the visible cell count
+ */
+ public int getVisibleCellCount() {
+ return visibleCells.size();
+ }
+
+ /** Sets the horizontal scroll position.
+ *
+ * @param scrollLeft
+ * the new horizontal scroll position
+ */
+ public void setHorizontalScrollPosition(int scrollLeft) {
+ hTableWrapper.setScrollLeft(scrollLeft);
+ }
+
+ /** Sets the column collapsing allowed.
+ *
+ * @param cc
+ * the new column collapsing allowed
+ */
+ public void setColumnCollapsingAllowed(boolean cc) {
+ if (cc) {
+ columnSelector.getStyle().setDisplay(Display.BLOCK);
+ } else {
+ columnSelector.getStyle().setDisplay(Display.NONE);
+ }
+ }
+
+ /** Disable browser intelligence.
+ */
+ public void disableBrowserIntelligence() {
+ hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX);
+ }
+
+ /** Enable browser intelligence.
+ */
+ public void enableBrowserIntelligence() {
+ hTableContainer.getStyle().clearWidth();
+ }
+
+ /** Sets the header cell.
+ *
+ * @param index
+ * the index
+ * @param cell
+ * the cell
+ */
+ 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");
+ }
+ }
+
+ /** Gets the header cell.
+ *
+ * @param index
+ * the index
+ * @return the header cell
+ */
+ 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);
+ }
+
+ /** Move cell.
+ *
+ * @param oldIndex
+ * the old index
+ * @param newIndex
+ * the new index
+ */
+ 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);
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.HasWidgets#iterator()
+ */
+ @Override
+ public Iterator<Widget> iterator() {
+ return visibleCells.iterator();
+ }
+
+ /* (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;
+ }
+
+ /** Removes the cell.
+ *
+ * @param colKey
+ * the col key
+ */
+ public void removeCell(String colKey) {
+ final HeaderCell c = getHeaderCell(colKey);
+ remove(c);
+ }
+
+ /** Focus slot.
+ *
+ * @param index
+ * the index
+ */
+ private void focusSlot(int index) {
+ removeSlotFocus();
+ if (index > 0) {
+ Element child = tr.getChild(index - 1).getFirstChild().cast();
+ child.setClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-resizer");
+ child.addClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-focus-slot-right");
+ } else {
+ Element child = tr.getChild(index).getFirstChild().cast();
+ child.setClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-resizer");
+ child.addClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-focus-slot-left");
+ }
+ focusedSlot = index;
+ }
+
+ /** Removes the slot focus.
+ */
+ private void removeSlotFocus() {
+ if (focusedSlot < 0) {
+ return;
+ }
+ if (focusedSlot == 0) {
+ Element child = tr.getChild(focusedSlot).getFirstChild().cast();
+ child.setClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-resizer");
+ } else if (focusedSlot > 0) {
+ Element child = tr.getChild(focusedSlot - 1).getFirstChild()
+ .cast();
+ child.setClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-resizer");
+ }
+ focusedSlot = -1;
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user.client.Event)
+ */
+ @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);
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Widget#onDetach()
+ */
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ if (client != null) {
+ client.getContextMenu().ensureHidden(this);
+ }
+ }
+
+ /** The Class VisibleColumnAction.
+ */
+ class VisibleColumnAction extends Action {
+
+ /** The col key. */
+ String colKey;
+
+ /** The collapsed. */
+ private boolean collapsed;
+
+ /** The noncollapsible. */
+ private boolean noncollapsible = false;
+
+ /** The currently focused row. */
+ private VScrollTableRow currentlyFocusedRow;
+
+ /** Instantiates a new visible column action.
+ *
+ * @param colKey
+ * the col key
+ */
+ public VisibleColumnAction(String colKey) {
+ super(VCustomScrollTable.TableHead.this);
+ this.colKey = colKey;
+ caption = tHead.getHeaderCell(colKey).getCaption();
+ currentlyFocusedRow = focusedRow;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.Action#execute()
+ */
+ @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);
+ }
+
+ /** Sets the collapsed.
+ *
+ * @param b
+ * the new collapsed
+ */
+ public void setCollapsed(boolean b) {
+ collapsed = b;
+ }
+
+ /** Sets the noncollapsible.
+ *
+ * @param b
+ * the new noncollapsible
+ */
+ public void setNoncollapsible(boolean b) {
+ noncollapsible = b;
+ }
+
+ /** Override default method to distinguish on/off columns.
+ *
+ * @return the html
+ */
+
+ @Override
+ public String getHTML() {
+ final StringBuffer buf = new StringBuffer();
+ buf.append("<span class=\"");
+ if (collapsed) {
+ buf.append("v-off");
+ } else {
+ buf.append("v-on");
+ }
+ if (noncollapsible) {
+ buf.append(" v-disabled");
+ }
+ buf.append("\">");
+
+ buf.append(super.getHTML());
+ buf.append("</span>");
+
+ return buf.toString();
+ }
+
+ }
+
+ /*
+ * Returns columns as Action array for column select popup
+ */
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.ActionOwner#getActions()
+ */
+ @Override
+ public Action[] getActions() {
+ Object[] cols;
+ if (columnReordering && columnOrder != null) {
+ cols = columnOrder;
+ } else {
+ // if columnReordering is disabled, we need different way to get
+ // all available columns
+ cols = visibleColOrder;
+ cols = new Object[visibleColOrder.length
+ + collapsedColumns.size()];
+ int i;
+ for (i = 0; i < visibleColOrder.length; i++) {
+ cols[i] = visibleColOrder[i];
+ }
+ for (final Iterator<String> it = collapsedColumns.iterator(); it
+ .hasNext();) {
+ cols[i++] = it.next();
+ }
+ }
+ 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;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.ActionOwner#getClient()
+ */
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.ActionOwner#getPaintableId()
+ */
+ @Override
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ /** Returns column alignments for visible columns.
+ *
+ * @return the column alignments
+ */
+ public char[] getColumnAlignments() {
+ final Iterator<Widget> it = visibleCells.iterator();
+ final char[] aligns = new char[visibleCells.size()];
+ int colIndex = 0;
+ while (it.hasNext()) {
+ aligns[colIndex++] = ((HeaderCell) it.next()).getAlign();
+ }
+ return aligns;
+ }
+
+ /** Disables the automatic calculation of all column widths by
+ * forcing the widths to be "defined" thus turning off expand ratios and
+ * such.
+ *
+ * @param source
+ * the source
+ */
+ public void disableAutoColumnWidthCalculation(HeaderCell source) {
+ for (HeaderCell cell : availableCells.values()) {
+ cell.disableAutoWidthCalculation();
+ }
+ // fire column resize events for all columns but the source of the
+ // resize action, since an event will fire separately for this.
+ ArrayList<HeaderCell> columns = new ArrayList<HeaderCell>(
+ availableCells.values());
+ columns.remove(source);
+ sendColumnWidthUpdates(columns);
+ forceRealignColumnHeaders();
+ }
+ }
+
+ /** A cell in the footer.
+ */
+ public class FooterCell extends Widget {
+
+ /** The td. */
+ private final Element td = DOM.createTD();
+
+ /** The caption container. */
+ private final Element captionContainer = DOM.createDiv();
+
+ /** The align. */
+ private char align = ALIGN_LEFT;
+
+ /** The width. */
+ private int width = -1;
+
+ /** The expand ratio. */
+ private float expandRatio = 0;
+
+ /** The cid. */
+ private final String cid;
+
+ /** The defined width. */
+ boolean definedWidth = false;
+
+ /** The natural width. */
+ private int naturalWidth = -1;
+
+ /** Instantiates a new footer cell.
+ *
+ * @param colId
+ * the col id
+ * @param headerText
+ * the header text
+ */
+ public FooterCell(String colId, String headerText) {
+ cid = colId;
+
+ setText(headerText);
+
+ // ensure no clipping initially (problem on column additions)
+ captionContainer.getStyle().setOverflow(Overflow.VISIBLE);
+
+ DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS);
+
+ DOM.appendChild(td, captionContainer);
+
+ DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK
+ | Event.ONCONTEXTMENU);
+
+ setElement(td);
+
+ updateStyleNames(VCustomScrollTable.this.getStylePrimaryName());
+ }
+
+ /** Update style names.
+ *
+ * @param primaryStyleName
+ * the primary style name
+ */
+ protected void updateStyleNames(String primaryStyleName) {
+ captionContainer.setClassName(primaryStyleName
+ + "-footer-container");
+ }
+
+ /** Sets the text of the footer.
+ *
+ * @param footerText
+ * The text in the footer
+ */
+ public void setText(String footerText) {
+ if (footerText == null || footerText.equals("")) {
+ footerText = " ";
+ }
+
+ DOM.setInnerHTML(captionContainer, footerText);
+ }
+
+ /** Set alignment of the text in the cell.
+ *
+ * @param c
+ * The alignment which can be ALIGN_CENTER, ALIGN_LEFT,
+ * ALIGN_RIGHT
+ */
+ public void setAlign(char c) {
+ if (align != c) {
+ switch (c) {
+ case ALIGN_CENTER:
+ captionContainer.getStyle().setTextAlign(TextAlign.CENTER);
+ break;
+ case ALIGN_RIGHT:
+ captionContainer.getStyle().setTextAlign(TextAlign.RIGHT);
+ break;
+ default:
+ captionContainer.getStyle().setTextAlign(TextAlign.LEFT);
+ break;
+ }
+ }
+ align = c;
+ }
+
+ /** Get the alignment of the text int the cell.
+ *
+ * @return Returns either ALIGN_CENTER, ALIGN_LEFT or ALIGN_RIGHT
+ */
+ public char getAlign() {
+ return align;
+ }
+
+ /**
+ * Sets the width of the cell. This width should not include any
+ * possible indent modifications that are present in
+ * {@link VScrollTableBody#getMaxIndent()}.
+ *
+ * @param w
+ * The width of the cell
+ * @param ensureDefinedWidth
+ * Ensures that the given width is not recalculated
+ */
+ public void setWidth(int w, boolean ensureDefinedWidth) {
+
+ if (ensureDefinedWidth) {
+ definedWidth = true;
+ // on column resize expand ratio becomes zero
+ expandRatio = 0;
+ }
+ if (width == w) {
+ return;
+ }
+ if (width == -1) {
+ // go to default mode, clip content if necessary
+ captionContainer.getStyle().clearOverflow();
+ }
+ width = w;
+ if (w == -1) {
+ captionContainer.getStyle().clearWidth();
+ setWidth("");
+ } else {
+ /*
+ * Reduce width with one pixel for the right border since the
+ * footers does not have any spacers between them.
+ */
+ final int borderWidths = 1;
+
+ // Set the container width (check for negative value)
+ captionContainer.getStyle().setPropertyPx("width",
+ Math.max(w - borderWidths, 0));
+
+ /*
+ * if we already have tBody, set the header width properly, if
+ * not defer it. IE will fail with complex float in table header
+ * unless TD width is not explicitly set.
+ */
+ if (scrollBody != null) {
+ int maxIndent = scrollBody.getMaxIndent();
+ if (w < maxIndent
+ && tFoot.visibleCells.indexOf(this) == getHierarchyColumnIndex()) {
+ // ensure there's room for the indent
+ w = maxIndent;
+ }
+ int tdWidth = w + scrollBody.getCellExtraWidth()
+ - borderWidths;
+ setWidth(Math.max(tdWidth, 0) + "px");
+ } else {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ int tdWidth = width;
+ int maxIndent = scrollBody.getMaxIndent();
+ if (tdWidth < maxIndent
+ && tFoot.visibleCells.indexOf(this) == getHierarchyColumnIndex()) {
+ // ensure there's room for the indent
+ tdWidth = maxIndent;
+ }
+ tdWidth += scrollBody.getCellExtraWidth()
+ - borderWidths;
+ setWidth(Math.max(tdWidth, 0) + "px");
+ }
+ });
+ }
+ }
+ }
+
+ /** Sets the width to undefined.
+ */
+ public void setUndefinedWidth() {
+ definedWidth = false;
+ setWidth(-1, false);
+ }
+
+ /**
+ * Detects if width is fixed by developer on server side or resized to
+ * current width by user.
+ *
+ * @return true if defined, false if "natural" width
+ */
+ public boolean isDefinedWidth() {
+ return definedWidth && width >= 0;
+ }
+
+ /**
+ * Returns the pixels width of the footer cell.
+ *
+ * @return The width in pixels
+ */
+ public int getWidth() {
+ return width;
+ }
+
+ /** Sets the expand ratio of the cell.
+ *
+ * @param floatAttribute
+ * The expand ratio
+ */
+ public void setExpandRatio(float floatAttribute) {
+ expandRatio = floatAttribute;
+ }
+
+ /** Returns the expand 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.
+ *
+ * @param event
+ * the event
+ */
+
+ @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(VCustomScrollTable.this,
+ TableConstants.FOOTER_CLICK_EVENT_ID)) {
+ // Show browser context menu if a footer click listener is
+ // not present
+ stopPropagation = false;
+ }
+ if (stopPropagation) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+ }
+
+ /** Handles a event on the captions.
+ *
+ * @param event
+ * The event to handle
+ */
+ protected void handleCaptionEvent(Event event) {
+ if (event.getTypeInt() == Event.ONMOUSEUP
+ || event.getTypeInt() == Event.ONDBLCLICK) {
+ fireFooterClickedEvent(event);
+ }
+ }
+
+ /** Fires a footer click event after the user has clicked a column
+ * footer cell.
+ *
+ * @param event
+ * The click event
+ */
+ private void fireFooterClickedEvent(Event event) {
+ if (client.hasEventListeners(VCustomScrollTable.this,
+ TableConstants.FOOTER_CLICK_EVENT_ID)) {
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event);
+ client.updateVariable(paintableId, "footerClickEvent",
+ details.toString(), false);
+ client.updateVariable(paintableId, "footerClickCID", cid, true);
+ }
+ }
+
+ /** Returns the column key of the column.
+ *
+ * @return The column key
+ */
+ public String getColKey() {
+ return cid;
+ }
+
+ /** 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 the natural column width
+ */
+ public int getNaturalColumnWidth(int columnIndex) {
+ final int iw = columnIndex == getHierarchyColumnIndex() ? scrollBody
+ .getMaxIndent() : 0;
+ if (isDefinedWidth()) {
+ if (iw > width) {
+ return iw;
+ }
+ 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() + getHeaderPadding();
+ if (columnIndex < 0) {
+ columnIndex = 0;
+ for (Iterator<Widget> it = tHead.iterator(); it
+ .hasNext(); columnIndex++) {
+ if (it.next() == this) {
+ break;
+ }
+ }
+ }
+ final int cw = scrollBody.getColWidth(columnIndex);
+ naturalWidth = (hw > cw ? hw : cw);
+ }
+ if (iw > naturalWidth) {
+ return iw;
+ } else {
+ return naturalWidth;
+ }
+ }
+ }
+
+ /** Sets the natural minimum column width.
+ *
+ * @param w
+ * the new natural minimum column width
+ */
+ 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 {
+
+ /** Instantiates a new row headers footer cell.
+ */
+ RowHeadersFooterCell() {
+ super(ROW_HEADER_COLUMN_KEY, "");
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.VCustomScrollTable.FooterCell#handleCaptionEvent(com.google.gwt.user.client.Event)
+ */
+ @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 {
+
+ /** The Constant WRAPPER_WIDTH. */
+ private static final int WRAPPER_WIDTH = 900000;
+
+ /** The visible cells. */
+ ArrayList<Widget> visibleCells = new ArrayList<Widget>();
+
+ /** The available cells. */
+ HashMap<String, FooterCell> availableCells = new HashMap<String, FooterCell>();
+
+ /** The div. */
+ Element div = DOM.createDiv();
+
+ /** The h table wrapper. */
+ Element hTableWrapper = DOM.createDiv();
+
+ /** The h table container. */
+ Element hTableContainer = DOM.createDiv();
+
+ /** The table. */
+ Element table = DOM.createTable();
+
+ /** The header table body. */
+ Element headerTableBody = DOM.createTBody();
+
+ /** The tr. */
+ Element tr = DOM.createTR();
+
+ /** Instantiates a new table footer.
+ */
+ public TableFooter() {
+
+ hTableWrapper.getStyle().setOverflow(Overflow.HIDDEN);
+
+ DOM.appendChild(table, headerTableBody);
+ DOM.appendChild(headerTableBody, tr);
+ DOM.appendChild(hTableContainer, table);
+ DOM.appendChild(hTableWrapper, hTableContainer);
+ DOM.appendChild(div, hTableWrapper);
+ setElement(div);
+
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersFooterCell());
+
+ updateStyleNames(VCustomScrollTable.this.getStylePrimaryName());
+ }
+
+ /** Update style names.
+ *
+ * @param primaryStyleName
+ * the primary style name
+ */
+ protected void updateStyleNames(String primaryStyleName) {
+ hTableWrapper.setClassName(primaryStyleName + "-footer");
+ setStyleName(primaryStyleName + "-footer-wrap");
+ for (FooterCell c : availableCells.values()) {
+ c.updateStyleNames(primaryStyleName);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Panel#clear()
+ */
+ @Override
+ public void clear() {
+ for (String cid : availableCells.keySet()) {
+ removeCell(cid);
+ }
+ availableCells.clear();
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersFooterCell());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client
+ * .ui.Widget)
+ */
+
+ @Override
+ public boolean remove(Widget w) {
+ if (visibleCells.contains(w)) {
+ visibleCells.remove(w);
+ orphan(w);
+ DOM.removeChild(DOM.getParent(w.getElement()), w.getElement());
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.HasWidgets#iterator()
+ */
+
+ @Override
+ public Iterator<Widget> iterator() {
+ return visibleCells.iterator();
+ }
+
+ /** Gets a footer cell which represents the given columnId.
+ *
+ * @param cid
+ * The columnId
+ * @return The cell
+ */
+ public FooterCell getFooterCell(String cid) {
+ return availableCells.get(cid);
+ }
+
+ /** Gets a footer cell by using a column index.
+ *
+ * @param index
+ * The index of the column
+ * @return The Cell
+ */
+ public FooterCell getFooterCell(int index) {
+ if (index < visibleCells.size()) {
+ return (FooterCell) visibleCells.get(index);
+ } else {
+ return null;
+ }
+ }
+
+ /** Updates the cells contents when updateUIDL request is received.
+ *
+ * @param uidl
+ * The UIDL
+ */
+ public void updateCellsFromUIDL(UIDL uidl) {
+ Iterator<?> columnIterator = uidl.getChildIterator();
+ HashSet<String> updated = new HashSet<String>();
+ while (columnIterator.hasNext()) {
+ final UIDL col = (UIDL) columnIterator.next();
+ final String cid = col.getStringAttribute("cid");
+ updated.add(cid);
+
+ String caption = col.hasAttribute("fcaption") ? col
+ .getStringAttribute("fcaption") : "";
+ FooterCell c = getFooterCell(cid);
+ if (c == null) {
+ c = new FooterCell(cid, caption);
+ availableCells.put(cid, c);
+ if (initializedAndAttached) {
+ // we will need a column width recalculation
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ } else {
+ c.setText(caption);
+ }
+
+ if (col.hasAttribute("align")) {
+ c.setAlign(col.getStringAttribute("align").charAt(0));
+ } else {
+ c.setAlign(ALIGN_LEFT);
+
+ }
+ if (col.hasAttribute("width")) {
+ if (scrollBody == null || isNewBody) {
+ // Already updated by setColWidth called from
+ // TableHeads.updateCellsFromUIDL in case of a server
+ // side resize
+ final int width = col.getIntAttribute("width");
+ c.setWidth(width, true);
+ }
+ } else if (recalcWidths) {
+ c.setUndefinedWidth();
+ }
+ if (col.hasAttribute("er")) {
+ c.setExpandRatio(col.getFloatAttribute("er"));
+ }
+ if (col.hasAttribute("collapsed")) {
+ // ensure header is properly removed from parent (case when
+ // collapsing happens via servers side api)
+ if (c.isAttached()) {
+ c.removeFromParent();
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ // check for orphaned header cells
+ for (Iterator<String> cit = availableCells.keySet().iterator(); cit
+ .hasNext();) {
+ String cid = cit.next();
+ if (!updated.contains(cid)) {
+ removeCell(cid);
+ cit.remove();
+ }
+ }
+ }
+
+ /** Set a footer cell for a specified column index.
+ *
+ * @param index
+ * The index
+ * @param cell
+ * The footer cell
+ */
+ public void setFooterCell(int index, FooterCell cell) {
+ if (cell.isEnabled()) {
+ // we're moving the cell
+ DOM.removeChild(tr, cell.getElement());
+ orphan(cell);
+ visibleCells.remove(cell);
+ }
+ if (index < visibleCells.size()) {
+ // insert to right slot
+ DOM.insertChild(tr, cell.getElement(), index);
+ adopt(cell);
+ visibleCells.add(index, cell);
+ } else if (index == visibleCells.size()) {
+ // simply append
+ DOM.appendChild(tr, cell.getElement());
+ adopt(cell);
+ visibleCells.add(cell);
+ } else {
+ throw new RuntimeException(
+ "Header cells must be appended in order");
+ }
+ }
+
+ /** Remove a cell by using the columnId.
+ *
+ * @param colKey
+ * The columnId to remove
+ */
+ public void removeCell(String colKey) {
+ final FooterCell c = getFooterCell(colKey);
+ remove(c);
+ }
+
+ /** Enable a column (Sets the footer cell).
+ *
+ * @param cid
+ * The columnId
+ * @param index
+ * The index of the column
+ */
+ public void enableColumn(String cid, int index) {
+ final FooterCell c = getFooterCell(cid);
+ if (!c.isEnabled() || getFooterCell(index) != c) {
+ setFooterCell(index, c);
+ if (initializedAndAttached) {
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ /** Disable browser measurement of the table width.
+ */
+ public void disableBrowserIntelligence() {
+ hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX);
+ }
+
+ /** Enable browser measurement of the table width.
+ */
+ public void enableBrowserIntelligence() {
+ hTableContainer.getStyle().clearWidth();
+ }
+
+ /**
+ * Set the horizontal position in the cell in the footer. This is done
+ * when a horizontal scrollbar is present.
+ *
+ * @param scrollLeft
+ * The value of the leftScroll
+ */
+ public void setHorizontalScrollPosition(int scrollLeft) {
+ hTableWrapper.setScrollLeft(scrollLeft);
+ }
+
+ /** Swap cells when the column are dragged.
+ *
+ * @param oldIndex
+ * The old index of the cell
+ * @param newIndex
+ * The new index of the cell
+ */
+ public void moveCell(int oldIndex, int newIndex) {
+ final FooterCell hCell = getFooterCell(oldIndex);
+ final Element cell = hCell.getElement();
+
+ visibleCells.remove(oldIndex);
+ DOM.removeChild(tr, cell);
+
+ DOM.insertChild(tr, cell, newIndex);
+ visibleCells.add(newIndex, hCell);
+ }
+ }
+
+ /**
+ * This Panel can only contain VScrollTableRow type of widgets. This
+ * "simulates" very large table, keeping spacers which take room of
+ * unrendered rows.
+ *
+ */
+ public class VScrollTableBody extends Panel {
+
+ /** The Constant DEFAULT_ROW_HEIGHT. */
+ public static final int DEFAULT_ROW_HEIGHT = 24;
+
+ /** The row height. */
+ private double rowHeight = -1;
+
+ /** The rendered rows. */
+ private final LinkedList<Widget> renderedRows = new LinkedList<Widget>();
+
+ /**
+ * Due some optimizations row height measuring is deferred and initial
+ * set of rows is rendered detached. Flag set on when table body has
+ * been attached in dom and rowheight has been measured.
+ */
+ private boolean tBodyMeasurementsDone = false;
+
+ /** The pre spacer. */
+ Element preSpacer = DOM.createDiv();
+
+ /** The post spacer. */
+ Element postSpacer = DOM.createDiv();
+
+ /** The container. */
+ Element container = DOM.createDiv();
+
+ /** The t body element. */
+ TableSectionElement tBodyElement = Document.get().createTBodyElement();
+
+ /** The table. */
+ Element table = DOM.createTable();
+
+ /** The first rendered. */
+ private int firstRendered;
+
+ /** The last rendered. */
+ private int lastRendered;
+
+ /** The aligns. */
+ private char[] aligns;
+
+ /** Instantiates a new v scroll table body.
+ */
+ protected VScrollTableBody() {
+ constructDOM();
+ setElement(container);
+ }
+
+ /** Sets the last rendered.
+ *
+ * @param lastRendered
+ * the new last rendered
+ */
+ public void setLastRendered(int lastRendered) {
+ if (totalRows >= 0 && lastRendered > totalRows) {
+ VConsole.log("setLastRendered: " + this.lastRendered + " -> "
+ + lastRendered);
+ this.lastRendered = totalRows - 1;
+ } else {
+ this.lastRendered = lastRendered;
+ }
+ }
+
+ /** Gets the last rendered.
+ *
+ * @return the last rendered
+ */
+ public int getLastRendered() {
+ return lastRendered;
+ }
+
+ /** Gets the first rendered.
+ *
+ * @return the first rendered
+ */
+ public int getFirstRendered() {
+ return firstRendered;
+ }
+
+ /** Gets the row by row index.
+ *
+ * @param indexInTable
+ * the index in table
+ * @return the row by row index
+ */
+ public VScrollTableRow getRowByRowIndex(int indexInTable) {
+ int internalIndex = indexInTable - firstRendered;
+ if (internalIndex >= 0 && internalIndex < renderedRows.size()) {
+ return (VScrollTableRow) renderedRows.get(internalIndex);
+ } else {
+ return null;
+ }
+ }
+
+ /** Gets the required height.
+ *
+ * @return the height of scrollable body, subpixels ceiled.
+ */
+ public int getRequiredHeight() {
+ return preSpacer.getOffsetHeight() + postSpacer.getOffsetHeight()
+ + Util.getRequiredHeight(table);
+ }
+
+ /** Construct dom.
+ */
+ private void constructDOM() {
+ if (BrowserInfo.get().isIE()) {
+ table.setPropertyInt("cellSpacing", 0);
+ }
+
+ table.appendChild(tBodyElement);
+ DOM.appendChild(container, preSpacer);
+ DOM.appendChild(container, table);
+ DOM.appendChild(container, postSpacer);
+ if (BrowserInfo.get().requiresTouchScrollDelegate()) {
+ NodeList<Node> childNodes = container.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Element item = (Element) childNodes.getItem(i);
+ item.getStyle().setProperty("webkitTransform",
+ "translate3d(0,0,0)");
+ }
+ }
+ updateStyleNames(VCustomScrollTable.this.getStylePrimaryName());
+ }
+
+ /** Update style names.
+ *
+ * @param primaryStyleName
+ * the primary style name
+ */
+ protected void updateStyleNames(String primaryStyleName) {
+ table.setClassName(primaryStyleName + "-table");
+ preSpacer.setClassName(primaryStyleName + "-row-spacer");
+ postSpacer.setClassName(primaryStyleName + "-row-spacer");
+ for (Widget w : renderedRows) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ row.updateStyleNames(primaryStyleName);
+ }
+ }
+
+ /** Gets the available width.
+ *
+ * @return the available width
+ */
+ public int getAvailableWidth() {
+ int availW = scrollBodyPanel.getOffsetWidth() - getBorderWidth();
+ return availW;
+ }
+
+ /** Render initial rows.
+ *
+ * @param rowData
+ * the row data
+ * @param firstIndex
+ * the first index
+ * @param rows
+ * the rows
+ */
+ public void renderInitialRows(UIDL rowData, int firstIndex, int rows) {
+ firstRendered = firstIndex;
+ setLastRendered(firstIndex + rows - 1);
+ final Iterator<?> it = rowData.getChildIterator();
+ aligns = tHead.getColumnAlignments();
+ while (it.hasNext()) {
+ final VScrollTableRow row = createRow((UIDL) it.next(), aligns);
+ addRow(row);
+ }
+ if (isAttached()) {
+ fixSpacers();
+ }
+ }
+
+ /** Render rows.
+ *
+ * @param rowData
+ * the row data
+ * @param firstIndex
+ * the first index
+ * @param rows
+ * the rows
+ */
+ public void renderRows(UIDL rowData, int firstIndex, int rows) {
+ // FIXME REVIEW
+ aligns = tHead.getColumnAlignments();
+ final Iterator<?> it = rowData.getChildIterator();
+ if (firstIndex == lastRendered + 1) {
+ while (it.hasNext()) {
+ final VScrollTableRow row = prepareRow((UIDL) it.next());
+ addRow(row);
+ setLastRendered(lastRendered + 1);
+ }
+ fixSpacers();
+ } else if (firstIndex + rows == firstRendered) {
+ final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
+ int i = rows;
+ while (it.hasNext()) {
+ i--;
+ rowArray[i] = prepareRow((UIDL) it.next());
+ }
+ for (i = 0; i < rows; i++) {
+ addRowBeforeFirstRendered(rowArray[i]);
+ firstRendered--;
+ }
+ } else {
+ // completely new set of rows
+
+ // there can't be sanity checks for last rendered within this
+ // while loop regardless of what has been set previously, so
+ // change it temporarily to true and then return the original
+ // value
+ boolean temp = postponeSanityCheckForLastRendered;
+ postponeSanityCheckForLastRendered = true;
+ while (lastRendered + 1 > firstRendered) {
+ unlinkRow(false);
+ }
+ postponeSanityCheckForLastRendered = temp;
+ VScrollTableRow row = prepareRow((UIDL) it.next());
+ firstRendered = firstIndex;
+ setLastRendered(firstIndex - 1);
+ addRow(row);
+ setLastRendered(lastRendered + 1);
+ setContainerHeight();
+ fixSpacers();
+ while (it.hasNext()) {
+ addRow(prepareRow((UIDL) it.next()));
+ setLastRendered(lastRendered + 1);
+ }
+ fixSpacers();
+ }
+
+ // this may be a new set of rows due content change,
+ // ensure we have proper cache rows
+ ensureCacheFilled();
+ }
+
+ /**
+ * Ensure we have the correct set of rows on client side, e.g. if the
+ * content on the server side has changed, or the client scroll position
+ * has changed since the last request.
+ */
+ protected void ensureCacheFilled() {
+ int reactFirstRow = (int) (firstRowInViewPort - pageLength
+ * cache_react_rate);
+ int reactLastRow = (int) (firstRowInViewPort + pageLength + pageLength
+ * cache_react_rate);
+ if (reactFirstRow < 0) {
+ reactFirstRow = 0;
+ }
+ if (reactLastRow >= totalRows) {
+ reactLastRow = totalRows - 1;
+ }
+ if (lastRendered < reactFirstRow || firstRendered > reactLastRow) {
+ /*
+ * #8040 - scroll position is completely changed since the
+ * latest request, so request a new set of rows.
+ *
+ * TODO: We should probably check whether the fetched rows match
+ * the current scroll position right when they arrive, so as to
+ * not waste time rendering a set of rows that will never be
+ * visible...
+ */
+ rowRequestHandler.triggerRowFetch(reactFirstRow, reactLastRow
+ - reactFirstRow + 1, 1);
+ } else if (lastRendered < reactLastRow) {
+ // get some cache rows below visible area
+ rowRequestHandler.triggerRowFetch(lastRendered + 1,
+ reactLastRow - lastRendered, 1);
+ } else if (firstRendered > reactFirstRow) {
+ /*
+ * Branch for fetching cache above visible area.
+ *
+ * If cache needed for both before and after visible area, this
+ * will be rendered after-cache is received and rendered. So in
+ * some rare situations the table may make two cache visits to
+ * server.
+ */
+ rowRequestHandler.triggerRowFetch(reactFirstRow, firstRendered
+ - reactFirstRow, 1);
+ }
+ }
+
+ /** Inserts rows as provided in the rowData starting at firstIndex.
+ *
+ * @param rowData
+ * the row data
+ * @param firstIndex
+ * the first index
+ * @param rows
+ * the number of rows
+ * @return a list of the rows added.
+ */
+ protected List<VScrollTableRow> insertRows(UIDL rowData,
+ int firstIndex, int rows) {
+ aligns = tHead.getColumnAlignments();
+ final Iterator<?> it = rowData.getChildIterator();
+ List<VScrollTableRow> insertedRows = new ArrayList<VScrollTableRow>();
+
+ if (firstIndex == lastRendered + 1) {
+ while (it.hasNext()) {
+ final VScrollTableRow row = prepareRow((UIDL) it.next());
+ addRow(row);
+ insertedRows.add(row);
+ if (postponeSanityCheckForLastRendered) {
+ lastRendered++;
+ } else {
+ setLastRendered(lastRendered + 1);
+ }
+ }
+ fixSpacers();
+ } else if (firstIndex + rows == firstRendered) {
+ final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
+ int i = rows;
+ while (it.hasNext()) {
+ i--;
+ rowArray[i] = prepareRow((UIDL) it.next());
+ }
+ for (i = 0; i < rows; i++) {
+ addRowBeforeFirstRendered(rowArray[i]);
+ insertedRows.add(rowArray[i]);
+ firstRendered--;
+ }
+ } else {
+ // insert in the middle
+ int ix = firstIndex;
+ while (it.hasNext()) {
+ VScrollTableRow row = prepareRow((UIDL) it.next());
+ insertRowAt(row, ix);
+ insertedRows.add(row);
+ if (postponeSanityCheckForLastRendered) {
+ lastRendered++;
+ } else {
+ setLastRendered(lastRendered + 1);
+ }
+ ix++;
+ }
+ fixSpacers();
+ }
+ return insertedRows;
+ }
+
+ /** Insert and reindex rows.
+ *
+ * @param rowData
+ * the row data
+ * @param firstIndex
+ * the first index
+ * @param rows
+ * the rows
+ * @return the list
+ */
+ protected List<VScrollTableRow> insertAndReindexRows(UIDL rowData,
+ int firstIndex, int rows) {
+ List<VScrollTableRow> inserted = insertRows(rowData, firstIndex,
+ rows);
+ int actualIxOfFirstRowAfterInserted = firstIndex + rows
+ - firstRendered;
+ for (int ix = actualIxOfFirstRowAfterInserted; ix < renderedRows
+ .size(); ix++) {
+ VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix);
+ r.setIndex(r.getIndex() + rows);
+ }
+ setContainerHeight();
+ return inserted;
+ }
+
+ /** Insert rows delete below.
+ *
+ * @param rowData
+ * the row data
+ * @param firstIndex
+ * the first index
+ * @param rows
+ * the rows
+ */
+ 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
+ * the uidl
+ * @return the v scroll table row
+ */
+ private VScrollTableRow prepareRow(UIDL uidl) {
+ final VScrollTableRow row = createRow(uidl, aligns);
+ row.initCellWidths();
+ return row;
+ }
+
+ /** Creates the row.
+ *
+ * @param uidl
+ * the uidl
+ * @param aligns2
+ * the aligns2
+ * @return the v scroll table 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);
+ }
+
+ /** Adds the row before first rendered.
+ *
+ * @param row
+ * the row
+ */
+ 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);
+ }
+
+ /** Adds the row.
+ *
+ * @param row
+ * the row
+ */
+ private void addRow(VScrollTableRow row) {
+ row.setIndex(firstRendered + renderedRows.size());
+ if (row.isSelected()) {
+ row.addStyleName("v-selected");
+ }
+ tBodyElement.appendChild(row.getElement());
+ // Add to renderedRows before adopt so iterator() will return also
+ // this row if called in an attach handler (#9264)
+ renderedRows.add(row);
+ adopt(row);
+ }
+
+ /** Insert row at.
+ *
+ * @param row
+ * the row
+ * @param index
+ * the index
+ */
+ 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);
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.HasWidgets#iterator()
+ */
+ @Override
+ public Iterator<Widget> iterator() {
+ return renderedRows.iterator();
+ }
+
+ /** Unlink row.
+ *
+ * @param fromBeginning
+ * the from beginning
+ * @return false if couldn't remove row
+ */
+ protected boolean unlinkRow(boolean fromBeginning) {
+ if (lastRendered - firstRendered < 0) {
+ return false;
+ }
+ int actualIx;
+ if (fromBeginning) {
+ actualIx = 0;
+ firstRendered++;
+ } else {
+ actualIx = renderedRows.size() - 1;
+ if (postponeSanityCheckForLastRendered) {
+ --lastRendered;
+ } else {
+ setLastRendered(lastRendered - 1);
+ }
+ }
+ if (actualIx >= 0) {
+ unlinkRowAtActualIndex(actualIx);
+ fixSpacers();
+ return true;
+ }
+ return false;
+ }
+
+ /** Unlink rows.
+ *
+ * @param firstIndex
+ * the first index
+ * @param count
+ * the count
+ */
+ protected void unlinkRows(int firstIndex, int count) {
+ if (count < 1) {
+ return;
+ }
+ if (firstRendered > firstIndex
+ && firstRendered < firstIndex + count) {
+ count = count - (firstRendered - firstIndex);
+ firstIndex = firstRendered;
+ }
+ int lastIndex = firstIndex + count - 1;
+ if (lastRendered < lastIndex) {
+ lastIndex = lastRendered;
+ }
+ for (int ix = lastIndex; ix >= firstIndex; ix--) {
+ unlinkRowAtActualIndex(actualIndex(ix));
+ if (postponeSanityCheckForLastRendered) {
+ // partialUpdate handles sanity check later
+ lastRendered--;
+ } else {
+ setLastRendered(lastRendered - 1);
+ }
+ }
+ fixSpacers();
+ }
+
+ /** Unlink and reindex rows.
+ *
+ * @param firstIndex
+ * the first index
+ * @param count
+ * the count
+ */
+ 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();
+ }
+
+ /** Unlink all rows starting at.
+ *
+ * @param index
+ * the index
+ */
+ protected void unlinkAllRowsStartingAt(int index) {
+ if (firstRendered > index) {
+ index = firstRendered;
+ }
+ for (int ix = renderedRows.size() - 1; ix >= index; ix--) {
+ unlinkRowAtActualIndex(actualIndex(ix));
+ setLastRendered(lastRendered - 1);
+ }
+ fixSpacers();
+ }
+
+ /** Actual index.
+ *
+ * @param index
+ * the index
+ * @return the int
+ */
+ private int actualIndex(int index) {
+ return index - firstRendered;
+ }
+
+ /** Unlink row at actual index.
+ *
+ * @param index
+ * the index
+ */
+ private void unlinkRowAtActualIndex(int index) {
+ final VScrollTableRow toBeRemoved = (VScrollTableRow) renderedRows
+ .get(index);
+ tBodyElement.removeChild(toBeRemoved.getElement());
+ orphan(toBeRemoved);
+ renderedRows.remove(index);
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client.ui.Widget)
+ */
+ @Override
+ public boolean remove(Widget w) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** Fix container blocks height according to totalRows to avoid
+ * "bouncing" when scrolling.
+ */
+ private void setContainerHeight() {
+ fixSpacers();
+ container.getStyle().setHeight(measureRowHeightOffset(totalRows),
+ Unit.PX);
+ }
+
+ /** Fix spacers.
+ */
+ private void fixSpacers() {
+ int prepx = measureRowHeightOffset(firstRendered);
+ if (prepx < 0) {
+ prepx = 0;
+ }
+ preSpacer.getStyle().setPropertyPx("height", prepx);
+ int postpx;
+ if (pageLength == 0 && totalRows == pageLength) {
+ /*
+ * TreeTable depends on having lastRendered out of sync in some
+ * situations, which makes this method miss the special
+ * situation in which one row worth of post spacer to be added
+ * if there are no rows in the table. #9203
+ */
+ postpx = measureRowHeightOffset(1);
+ } else {
+ postpx = measureRowHeightOffset(totalRows - 1)
+ - measureRowHeightOffset(lastRendered);
+ }
+
+ if (postpx < 0) {
+ postpx = 0;
+ }
+ postSpacer.getStyle().setPropertyPx("height", postpx);
+ }
+
+ /** Gets the row height.
+ *
+ * @return the row height
+ */
+ public double getRowHeight() {
+ return getRowHeight(false);
+ }
+
+ /** Gets the row height.
+ *
+ * @param forceUpdate
+ * the force update
+ * @return the row height
+ */
+ public double getRowHeight(boolean forceUpdate) {
+ if (tBodyMeasurementsDone && !forceUpdate) {
+ return rowHeight;
+ } else {
+ if (tBodyElement.getRows().getLength() > 0) {
+ int tableHeight = getTableHeight();
+ int rowCount = tBodyElement.getRows().getLength();
+ rowHeight = tableHeight / (double) rowCount;
+ } else {
+ // Special cases if we can't just measure the current rows
+ if (!Double.isNaN(lastKnownRowHeight)) {
+ // Use previous value if available
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * IE needs to reflow the table element at this
+ * point to work correctly (e.g.
+ * com.vaadin.tests.components.table.
+ * ContainerSizeChange) - the other code paths
+ * already trigger reflows, but here it must be done
+ * explicitly.
+ */
+ getTableHeight();
+ }
+ rowHeight = lastKnownRowHeight;
+ } else if (isAttached()) {
+ // measure row height by adding a dummy row
+ VScrollTableRow scrollTableRow = new VScrollTableRow();
+ tBodyElement.appendChild(scrollTableRow.getElement());
+ getRowHeight(forceUpdate);
+ tBodyElement.removeChild(scrollTableRow.getElement());
+ } else {
+ // TODO investigate if this can never happen anymore
+ return DEFAULT_ROW_HEIGHT;
+ }
+ }
+ lastKnownRowHeight = rowHeight;
+ tBodyMeasurementsDone = true;
+ return rowHeight;
+ }
+ }
+
+ /** Gets the table height.
+ *
+ * @return the table height
+ */
+ public int getTableHeight() {
+ return table.getOffsetHeight();
+ }
+
+ /** Returns the width available for column content.
+ *
+ * @param columnIndex
+ * the column index
+ * @return the col width
+ */
+ 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
+ * the col index
+ * @param w
+ * the w
+ */
+ public void setColWidth(int colIndex, int w) {
+ for (Widget row : renderedRows) {
+ ((VScrollTableRow) row).setCellWidth(colIndex, w);
+ }
+ }
+
+ /** The cell extra width. */
+ private int cellExtraWidth = -1;
+
+ /** Method to return the space used for cell paddings + border.
+ *
+ * @return the cell extra width
+ */
+ private int getCellExtraWidth() {
+ if (cellExtraWidth < 0) {
+ detectExtrawidth();
+ }
+ return cellExtraWidth;
+ }
+
+ /** This method exists for the needs of {@link VTreeTable} only. May
+ * be removed or replaced in the future.<br> <br> Returns the maximum
+ * indent of the hierarcyColumn, if applicable.
+ *
+ * @return maximum indent in pixels
+ * @see #getHierarchyColumnIndex()
+ */
+ protected int getMaxIndent() {
+ return 0;
+ }
+
+ /**
+ * This method exists for the needs of {@link VTreeTable} only. May be
+ * removed or replaced in the future.<br> <br> Calculates the maximum
+ * indent of the hierarcyColumn, if applicable.
+ */
+ protected void calculateMaxIndent() {
+ // NOP
+ }
+
+ /** Detect extrawidth.
+ */
+ private void detectExtrawidth() {
+ NodeList<TableRowElement> rows = tBodyElement.getRows();
+ if (rows.getLength() == 0) {
+ /* need to temporary add empty row and detect */
+ VScrollTableRow scrollTableRow = new VScrollTableRow();
+ scrollTableRow.updateStyleNames(VCustomScrollTable.this
+ .getStylePrimaryName());
+ tBodyElement.appendChild(scrollTableRow.getElement());
+ detectExtrawidth();
+ tBodyElement.removeChild(scrollTableRow.getElement());
+ } else {
+ boolean noCells = false;
+ TableRowElement item = rows.getItem(0);
+ TableCellElement firstTD = item.getCells().getItem(0);
+ if (firstTD == null) {
+ // content is currently empty, we need to add a fake cell
+ // for measuring
+ noCells = true;
+ VScrollTableRow next = (VScrollTableRow) iterator().next();
+ boolean sorted = tHead.getHeaderCell(0) != null ? tHead
+ .getHeaderCell(0).isSorted() : false;
+ next.addCell(null, "", ALIGN_LEFT, "", true, sorted);
+ firstTD = item.getCells().getItem(0);
+ }
+ com.google.gwt.dom.client.Element wrapper = firstTD
+ .getFirstChildElement();
+ cellExtraWidth = firstTD.getOffsetWidth()
+ - wrapper.getOffsetWidth();
+ if (noCells) {
+ firstTD.getParentElement().removeChild(firstTD);
+ }
+ }
+ }
+
+ /** Move col.
+ *
+ * @param oldIndex
+ * the old index
+ * @param newIndex
+ * the new index
+ */
+ 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", "");
+ }
+ }
+
+ /** Index of.
+ *
+ * @param row
+ * the row
+ * @return the int
+ */
+ public int indexOf(Widget row) {
+ int relIx = -1;
+ for (int ix = 0; ix < renderedRows.size(); ix++) {
+ if (renderedRows.get(ix) == row) {
+ relIx = ix;
+ break;
+ }
+ }
+ if (relIx >= 0) {
+ return firstRendered + relIx;
+ }
+ return -1;
+ }
+
+ /** The Class VScrollTableRow.
+ */
+ public class VScrollTableRow extends Panel implements ActionOwner,
+ ContextMenuOwner {
+
+ /** The Constant TOUCHSCROLL_TIMEOUT. */
+ private static final int TOUCHSCROLL_TIMEOUT = 100;
+
+ /** The Constant DRAGMODE_MULTIROW. */
+ private static final int DRAGMODE_MULTIROW = 2;
+
+ /** The child widgets. */
+ protected ArrayList<Widget> childWidgets = new ArrayList<Widget>();
+
+ /** The selected. */
+ private boolean selected = false;
+
+ /** The row key. */
+ protected final int rowKey;
+
+ /** The action keys. */
+ private String[] actionKeys = null;
+
+ /** The row element. */
+ private final TableRowElement rowElement;
+
+ /** The index. */
+ private int index;
+
+ /** The touch start. */
+ private Event touchStart;
+
+ /** The Constant TOUCH_CONTEXT_MENU_TIMEOUT. */
+ private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500;
+
+ /** The context touch timeout. */
+ private Timer contextTouchTimeout;
+
+ /** The drag touch timeout. */
+ private Timer dragTouchTimeout;
+
+ /** The touch start y. */
+ private int touchStartY;
+
+ /** The touch start x. */
+ private int touchStartX;
+
+ /** The touch context provider. */
+ private TouchContextProvider touchContextProvider = new TouchContextProvider(
+ this);
+
+ /** The tooltip info. */
+ private TooltipInfo tooltipInfo = null;
+
+ /** The cell tool tips. */
+ private Map<TableCellElement, TooltipInfo> cellToolTips = new HashMap<TableCellElement, TooltipInfo>();
+
+ /** The is dragging. */
+ private boolean isDragging = false;
+
+ /** The row style. */
+ private String rowStyle = null;
+
+ /** Instantiates a new v scroll table row.
+ *
+ * @param rowKey
+ * the row key
+ */
+ 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);
+ }
+
+ /** Instantiates a new v scroll table row.
+ *
+ * @param uidl
+ * the uidl
+ * @param aligns
+ * the aligns
+ */
+ public VScrollTableRow(UIDL uidl, char[] aligns) {
+ this(uidl.getIntAttribute("key"));
+
+ /*
+ * Rendering the rows as hidden improves Firefox and Safari
+ * performance drastically.
+ */
+ getElement().getStyle().setProperty("visibility", "hidden");
+
+ rowStyle = uidl.getStringAttribute("rowstyle");
+ updateStyleNames(VCustomScrollTable.this.getStylePrimaryName());
+
+ String rowDescription = uidl.getStringAttribute("rowdescr");
+ if (rowDescription != null && !rowDescription.equals("")) {
+ tooltipInfo = new TooltipInfo(rowDescription, null, this);
+ } else {
+ tooltipInfo = null;
+ }
+
+ tHead.getColumnAlignments();
+ int col = 0;
+ int visibleColumnIndex = -1;
+
+ // row header
+ if (showRowHeaders) {
+ boolean sorted = tHead.getHeaderCell(col).isSorted();
+ addCell(uidl, buildCaptionHtmlSnippet(uidl), aligns[col++],
+ "rowheader", true, sorted);
+ visibleColumnIndex++;
+ }
+
+ if (uidl.hasAttribute("al")) {
+ actionKeys = uidl.getStringArrayAttribute("al");
+ }
+
+ addCellsFromUIDL(uidl, aligns, col, visibleColumnIndex);
+
+ if (uidl.hasAttribute("selected") && !isSelected()) {
+ toggleSelection();
+ }
+ }
+
+ /** Update style names.
+ *
+ * @param primaryStyleName
+ * the primary style name
+ */
+ protected void updateStyleNames(String primaryStyleName) {
+
+ if (getStylePrimaryName().contains("odd")) {
+ setStyleName(primaryStyleName + "-row-odd");
+ } else {
+ setStyleName(primaryStyleName + "-row");
+ }
+
+ if (rowStyle != null) {
+ addStyleName(primaryStyleName + "-row-" + rowStyle);
+ }
+
+ for (int i = 0; i < rowElement.getChildCount(); i++) {
+ TableCellElement cell = (TableCellElement) rowElement
+ .getChild(i);
+ updateCellStyleNames(cell, primaryStyleName);
+ }
+ }
+
+ /** Gets the tooltip info.
+ *
+ * @return the tooltip info
+ */
+ public TooltipInfo getTooltipInfo() {
+ return tooltipInfo;
+ }
+
+ /**
+ * Add a dummy row, used for measurements if Table is empty.
+ */
+ public VScrollTableRow() {
+ this(0);
+ addCell(null, "_", 'b', "", true, false);
+ }
+
+ /** Inits the cell widths.
+ */
+ protected void initCellWidths() {
+ final int cells = tHead.getVisibleCellCount();
+ for (int i = 0; i < cells; i++) {
+ int w = VCustomScrollTable.this
+ .getColWidth(getColKeyByIndex(i));
+ if (w < 0) {
+ w = 0;
+ }
+ setCellWidth(i, w);
+ }
+ }
+
+ /** Sets the cell width.
+ *
+ * @param cellIx
+ * the cell ix
+ * @param width
+ * the width
+ */
+ protected void setCellWidth(int cellIx, int width) {
+ final Element cell = DOM.getChild(getElement(), cellIx);
+ Style wrapperStyle = cell.getFirstChildElement().getStyle();
+ int wrapperWidth = width;
+ if (BrowserInfo.get().isWebkit()
+ || BrowserInfo.get().isOpera10()) {
+ /*
+ * Some versions of Webkit and Opera ignore the width
+ * definition of zero width table cells. Instead, use 1px
+ * and compensate with a negative margin.
+ */
+ if (width == 0) {
+ wrapperWidth = 1;
+ wrapperStyle.setMarginRight(-1, Unit.PX);
+ } else {
+ wrapperStyle.clearMarginRight();
+ }
+ }
+ wrapperStyle.setPropertyPx("width", wrapperWidth);
+ cell.getStyle().setPropertyPx("width", width);
+ }
+
+ /** Adds the cells from uidl.
+ *
+ * @param uidl
+ * the uidl
+ * @param aligns
+ * the aligns
+ * @param col
+ * the col
+ * @param visibleColumnIndex
+ * the visible column index
+ */
+ protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
+ int visibleColumnIndex) {
+ final Iterator<?> cells = uidl.getChildIterator();
+ while (cells.hasNext()) {
+ final Object cell = cells.next();
+ visibleColumnIndex++;
+
+ String columnId = visibleColOrder[visibleColumnIndex];
+
+ String style = "";
+ if (uidl.hasAttribute("style-" + columnId)) {
+ style = uidl.getStringAttribute("style-" + columnId);
+ }
+
+ String description = null;
+ if (uidl.hasAttribute("descr-" + columnId)) {
+ description = uidl.getStringAttribute("descr-"
+ + columnId);
+ }
+
+ boolean sorted = tHead.getHeaderCell(col).isSorted();
+ if (cell instanceof String) {
+ addCell(uidl, cell.toString(), aligns[col++], style,
+ isRenderHtmlInCells(), sorted, description);
+ } else {
+ final ComponentConnector cellContent = client
+ .getPaintable((UIDL) cell);
+
+ addCell(uidl, cellContent.getWidget(), aligns[col++],
+ style, sorted, description);
+ }
+ }
+ }
+
+ /**
+ * Overriding this and returning true causes all text cells to be
+ * rendered as HTML.
+ *
+ * @return always returns false in the default implementation
+ */
+ protected boolean isRenderHtmlInCells() {
+ return false;
+ }
+
+ /** Detects whether row is visible in tables viewport.
+ *
+ * @return true, if is in view port
+ */
+ public boolean isInViewPort() {
+ int absoluteTop = getAbsoluteTop();
+ int absoluteBottom = absoluteTop + getOffsetHeight();
+ int viewPortTop = scrollBodyPanel.getAbsoluteTop();
+ int viewPortBottom = viewPortTop
+ + scrollBodyPanel.getOffsetHeight();
+ return absoluteBottom > viewPortTop
+ && absoluteTop < viewPortBottom;
+ }
+
+ /** Makes a check based on indexes whether the row is before the
+ * compared row.
+ *
+ * @param row1
+ * the 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
+ * the new index
+ */
+ private void setIndex(int indexInWholeTable) {
+ index = indexInWholeTable;
+ boolean isOdd = indexInWholeTable % 2 == 0;
+ // Inverted logic to be backwards compatible with earlier 6.4.
+ // It is very strange because rows 1,3,5 are considered "even"
+ // and 2,4,6 "odd".
+ //
+ // First remove any old styles so that both styles aren't
+ // applied when indexes are updated.
+ String primaryStyleName = getStylePrimaryName();
+ if (primaryStyleName != null && !primaryStyleName.equals("")) {
+ removeStyleName(getStylePrimaryName());
+ }
+ if (!isOdd) {
+ addStyleName(VCustomScrollTable.this.getStylePrimaryName()
+ + "-row-odd");
+ } else {
+ addStyleName(VCustomScrollTable.this.getStylePrimaryName()
+ + "-row");
+ }
+ }
+
+ /** Gets the index.
+ *
+ * @return the index
+ */
+ public int getIndex() {
+ return index;
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Widget#onDetach()
+ */
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ client.getContextMenu().ensureHidden(this);
+ }
+
+ /** Gets the key.
+ *
+ * @return the key
+ */
+ public String getKey() {
+ return String.valueOf(rowKey);
+ }
+
+ /** Adds the cell.
+ *
+ * @param rowUidl
+ * the row uidl
+ * @param text
+ * the text
+ * @param align
+ * the align
+ * @param style
+ * the style
+ * @param textIsHTML
+ * the text is html
+ * @param sorted
+ * the sorted
+ */
+ public void addCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean sorted) {
+ addCell(rowUidl, text, align, style, textIsHTML, sorted, null);
+ }
+
+ /** Adds the cell.
+ *
+ * @param rowUidl
+ * the row uidl
+ * @param text
+ * the text
+ * @param align
+ * the align
+ * @param style
+ * the style
+ * @param textIsHTML
+ * the text is html
+ * @param sorted
+ * the sorted
+ * @param description
+ * the description
+ */
+ 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);
+ }
+
+ /** Inits the cell with text.
+ *
+ * @param text
+ * the text
+ * @param align
+ * the align
+ * @param style
+ * the style
+ * @param textIsHTML
+ * the text is html
+ * @param sorted
+ * the sorted
+ * @param description
+ * the description
+ * @param td
+ * the td
+ */
+ protected void initCellWithText(String text, char align,
+ String style, boolean textIsHTML, boolean sorted,
+ String description, final TableCellElement td) {
+ final Element container = DOM.createDiv();
+ container.setClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-cell-wrapper");
+
+ td.setClassName(VCustomScrollTable.this.getStylePrimaryName()
+ + "-cell-content");
+
+ if (style != null && !style.equals("")) {
+ td.addClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-cell-content-" + style);
+ }
+
+ if (sorted) {
+ td.addClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-cell-content-sorted");
+ }
+
+ if (textIsHTML) {
+ container.setInnerHTML(text);
+ } else {
+ container.setInnerText(text);
+ }
+ setAlign(align, container);
+ setTooltip(td, description);
+
+ td.appendChild(container);
+ getElement().appendChild(td);
+ }
+
+ /** Update cell style names.
+ *
+ * @param td
+ * the td
+ * @param primaryStyleName
+ * the primary style name
+ */
+ protected void updateCellStyleNames(TableCellElement td,
+ String primaryStyleName) {
+ Element container = td.getFirstChild().cast();
+ container.setClassName(primaryStyleName + "-cell-wrapper");
+
+ /*
+ * Replace old primary style name with new one
+ */
+ String className = td.getClassName();
+ String oldPrimaryName = className.split("-cell-content")[0];
+ td.setClassName(className.replaceAll(oldPrimaryName,
+ primaryStyleName));
+ }
+
+ /** Adds the cell.
+ *
+ * @param rowUidl
+ * the row uidl
+ * @param w
+ * the w
+ * @param align
+ * the align
+ * @param style
+ * the style
+ * @param sorted
+ * the sorted
+ * @param description
+ * the description
+ */
+ public void addCell(UIDL rowUidl, Widget w, char align,
+ String style, boolean sorted, String description) {
+ final TableCellElement td = DOM.createTD().cast();
+ initCellWithWidget(w, align, style, sorted, td);
+ setTooltip(td, description);
+ }
+
+ /** Sets the tooltip.
+ *
+ * @param td
+ * the td
+ * @param description
+ * the description
+ */
+ private void setTooltip(TableCellElement td, String description) {
+ if (description != null && !description.equals("")) {
+ TooltipInfo info = new TooltipInfo(description, null, this);
+ cellToolTips.put(td, info);
+ } else {
+ cellToolTips.remove(td);
+ }
+
+ }
+
+ /** Sets the align.
+ *
+ * @param align
+ * the align
+ * @param container
+ * the container
+ */
+ private void setAlign(char align, final Element container) {
+ switch (align) {
+ case ALIGN_CENTER:
+ container.getStyle().setProperty("textAlign", "center");
+ break;
+ case ALIGN_LEFT:
+ container.getStyle().setProperty("textAlign", "left");
+ break;
+ case ALIGN_RIGHT:
+ default:
+ container.getStyle().setProperty("textAlign", "right");
+ break;
+ }
+ }
+
+ /** Inits the cell with widget.
+ *
+ * @param w
+ * the w
+ * @param align
+ * the align
+ * @param style
+ * the style
+ * @param sorted
+ * the sorted
+ * @param td
+ * the td
+ */
+ protected void initCellWithWidget(Widget w, char align,
+ String style, boolean sorted, final TableCellElement td) {
+ final Element container = DOM.createDiv();
+ String className = VCustomScrollTable.this
+ .getStylePrimaryName() + "-cell-content";
+ if (style != null && !style.equals("")) {
+ className += " "
+ + VCustomScrollTable.this.getStylePrimaryName()
+ + "-cell-content-" + style;
+ }
+ if (sorted) {
+ className += " "
+ + VCustomScrollTable.this.getStylePrimaryName()
+ + "-cell-content-sorted";
+ }
+ td.setClassName(className);
+ container.setClassName(VCustomScrollTable.this
+ .getStylePrimaryName() + "-cell-wrapper");
+ setAlign(align, container);
+ td.appendChild(container);
+ getElement().appendChild(td);
+ // ensure widget not attached to another element (possible tBody
+ // change)
+ w.removeFromParent();
+ container.appendChild(w.getElement());
+ adopt(w);
+ childWidgets.add(w);
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.HasWidgets#iterator()
+ */
+ @Override
+ public Iterator<Widget> iterator() {
+ return childWidgets.iterator();
+ }
+
+ /* (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 (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
+ * the event
+ * @param targetTdOrTr
+ * the target td or tr
+ * @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(VCustomScrollTable.this,
+ TableConstants.ITEM_CLICK_EVENT_ID)) {
+ // Don't send an event if nobody is listening
+ return false;
+ }
+
+ // This row was clicked
+ client.updateVariable(paintableId, "clickedKey", "" + rowKey,
+ false);
+
+ if (getElement() == targetTdOrTr.getParentElement()) {
+ // A specific column was clicked
+ int childIndex = DOM.getChildIndex(getElement(),
+ targetTdOrTr);
+ String colKey = null;
+ colKey = tHead.getHeaderCell(childIndex).getColKey();
+ client.updateVariable(paintableId, "clickedColKey", colKey,
+ false);
+ }
+
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event);
+
+ client.updateVariable(paintableId, "clickEvent",
+ details.toString(), immediate);
+
+ return true;
+ }
+
+ /** Gets the tooltip.
+ *
+ * @param target
+ * the target
+ * @return the tooltip
+ */
+ public TooltipInfo getTooltip(
+ com.google.gwt.dom.client.Element target) {
+
+ TooltipInfo info = null;
+ final Element targetTdOrTr = getTdOrTr(target);
+ if (targetTdOrTr != null
+ && "td".equals(targetTdOrTr.getTagName().toLowerCase())) {
+ TableCellElement td = (TableCellElement) targetTdOrTr
+ .cast();
+ info = cellToolTips.get(td);
+ }
+
+ if (info == null) {
+ info = tooltipInfo;
+ }
+
+ return info;
+ }
+
+ /** Gets the td or tr.
+ *
+ * @param target
+ * the target
+ * @return the td or tr
+ */
+ private Element getTdOrTr(Element target) {
+ Element thisTrElement = getElement();
+ if (target == thisTrElement) {
+ // This was a on the TR element
+ return target;
+ }
+
+ // Iterate upwards until we find the TR element
+ Element element = target;
+ while (element != null
+ && element.getParentElement() != thisTrElement) {
+ element = element.getParentElement();
+ }
+ return element;
+ }
+
+ /** Special handler for touch devices that support native
+ * scrolling.
+ *
+ * @param event
+ * the event
+ * @return Whether the event was handled by this method.
+ */
+ private boolean handleTouchEvent(final Event event) {
+
+ boolean touchEventHandled = false;
+
+ if (enabled && hasNativeTouchScrolling) {
+ touchContextProvider.handleTouchEvent(event);
+
+ final Element targetTdOrTr = getEventTargetTdOrTr(event);
+ final int type = event.getTypeInt();
+
+ switch (type) {
+ case Event.ONTOUCHSTART:
+ touchEventHandled = true;
+ touchStart = event;
+ isDragging = false;
+ Touch touch = event.getChangedTouches().get(0);
+ // save position to fields, touches in events are same
+ // instance during the operation.
+ touchStartX = touch.getClientX();
+ touchStartY = touch.getClientY();
+
+ if (dragmode != 0) {
+ if (dragTouchTimeout == null) {
+ dragTouchTimeout = new Timer() {
+
+ @Override
+ public void run() {
+ if (touchStart != null) {
+ // Start a drag if a finger is held
+ // in place long enough, then moved
+ isDragging = true;
+ }
+ }
+ };
+ }
+ dragTouchTimeout.schedule(TOUCHSCROLL_TIMEOUT);
+ }
+
+ if (actionKeys != null) {
+ if (contextTouchTimeout == null) {
+ contextTouchTimeout = new Timer() {
+
+ @Override
+ public void run() {
+ if (touchStart != null) {
+ // Open the context menu if finger
+ // is held in place long enough.
+ showContextMenu(touchStart);
+ event.preventDefault();
+ touchStart = null;
+ }
+ }
+ };
+ }
+ contextTouchTimeout
+ .schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
+ }
+ break;
+ case Event.ONTOUCHMOVE:
+ touchEventHandled = true;
+ if (isSignificantMove(event)) {
+ if (contextTouchTimeout != null) {
+ // Moved finger before the context menu timer
+ // expired, so let the browser handle this as a
+ // scroll.
+ contextTouchTimeout.cancel();
+ contextTouchTimeout = null;
+ }
+ if (!isDragging && dragTouchTimeout != null) {
+ // Moved finger before the drag timer expired,
+ // so let the browser handle this as a scroll.
+ dragTouchTimeout.cancel();
+ dragTouchTimeout = null;
+ }
+
+ if (dragmode != 0 && touchStart != null
+ && isDragging) {
+ event.preventDefault();
+ event.stopPropagation();
+ startRowDrag(touchStart, type, targetTdOrTr);
+ }
+ touchStart = null;
+ }
+ break;
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ touchEventHandled = true;
+ if (contextTouchTimeout != null) {
+ contextTouchTimeout.cancel();
+ }
+ if (dragTouchTimeout != null) {
+ dragTouchTimeout.cancel();
+ }
+ if (touchStart != null) {
+ if (!BrowserInfo.get().isAndroid()) {
+ event.preventDefault();
+ event.stopPropagation();
+ Util.simulateClickFromTouchEvent(touchStart,
+ this);
+ }
+ touchStart = null;
+ }
+ isDragging = false;
+ break;
+ }
+ }
+ return touchEventHandled;
+ }
+
+ /*
+ * React on click that occur on content cells only
+ */
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user.client.Event)
+ */
+ @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(
+ VCustomScrollTable.this,
+ TableConstants.ITEM_CLICK_EVENT_ID))) {
+ /*
+ * Prevent browser context menu only if there are
+ * action handlers or item click listeners
+ * registered
+ */
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ return;
+ }
+
+ boolean targetCellOrRowFound = targetTdOrTr != null;
+
+ switch (type) {
+ case Event.ONDBLCLICK:
+ if (targetCellOrRowFound) {
+ handleClickEvent(event, targetTdOrTr, true);
+ }
+ break;
+ case Event.ONMOUSEUP:
+ /*
+ * Only fire a click if the mouseup hits the same
+ * element as the corresponding mousedown. This is first
+ * checked in the event preview but we can't fire the
+ * event there as the event might get canceled before it
+ * gets here.
+ */
+ if (mouseUpPreviewMatched
+ && lastMouseDownTarget != null
+ && lastMouseDownTarget == getElementTdOrTr(Util
+ .getElementUnderMouse(event))) {
+ // "Click" with left, right or middle button
+
+ if (targetCellOrRowFound) {
+ /*
+ * Queue here, send at the same time as the
+ * corresponding value change event - see #7127
+ */
+ boolean clickEventSent = handleClickEvent(
+ event, targetTdOrTr, false);
+
+ if (event.getButton() == Event.BUTTON_LEFT
+ && isSelectable()) {
+
+ // Ctrl+Shift click
+ if ((event.getCtrlKey() || event
+ .getMetaKey())
+ && event.getShiftKey()
+ && isMultiSelectModeDefault()) {
+ toggleShiftSelection(false);
+ setRowFocus(this);
+
+ // Ctrl click
+ } else if ((event.getCtrlKey() || event
+ .getMetaKey())
+ && isMultiSelectModeDefault()) {
+ boolean wasSelected = isSelected();
+ toggleSelection();
+ setRowFocus(this);
+ /*
+ * next possible range select must start
+ * on this row
+ */
+ selectionRangeStart = this;
+ if (wasSelected) {
+ removeRowFromUnsentSelectionRanges(this);
+ }
+
+ } else if ((event.getCtrlKey() || event
+ .getMetaKey())
+ && isSingleSelectMode()) {
+ // Ctrl (or meta) click (Single
+ // selection)
+ if (!isSelected()
+ || (isSelected() && nullSelectionAllowed)) {
+
+ if (!isSelected()) {
+ deselectAll();
+ }
+
+ toggleSelection();
+ setRowFocus(this);
+ }
+
+ } else if (event.getShiftKey()
+ && isMultiSelectModeDefault()) {
+ // Shift click
+ toggleShiftSelection(true);
+
+ } else {
+ // click
+ boolean currentlyJustThisRowSelected = selectedRowKeys
+ .size() == 1
+ && selectedRowKeys
+ .contains(getKey());
+
+ if (!currentlyJustThisRowSelected) {
+ if (isSingleSelectMode()
+ || isMultiSelectModeDefault()) {
+ /*
+ * For default multi select mode
+ * (ctrl/shift) and for single
+ * select mode we need to clear
+ * the previous selection before
+ * selecting a new one when the
+ * user clicks on a row. Only in
+ * multiselect/simple mode the
+ * old selection should remain
+ * after a normal click.
+ */
+ deselectAll();
+ }
+ toggleSelection();
+ } else if ((isSingleSelectMode() || isMultiSelectModeSimple())
+ && nullSelectionAllowed) {
+ toggleSelection();
+ }/*
+ * else NOP to avoid excessive server
+ * visits (selection is removed with
+ * CTRL/META click)
+ */
+
+ selectionRangeStart = this;
+ setRowFocus(this);
+ }
+
+ // Remove IE text selection hack
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget()
+ .cast()).setPropertyJSO(
+ "onselectstart", null);
+ }
+ // Queue value change
+ sendSelectedRows(false);
+ }
+ /*
+ * Send queued click and value change events if
+ * any If a click event is sent, send value
+ * change with it regardless of the immediate
+ * flag, see #7127
+ */
+ if (immediate || clickEventSent) {
+ client.sendPendingVariableChanges();
+ }
+ }
+ }
+ mouseUpPreviewMatched = false;
+ lastMouseDownTarget = null;
+ break;
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ if (touchStart != null) {
+ /*
+ * Touch has not been handled as neither context or
+ * drag start, handle it as a click.
+ */
+ Util.simulateClickFromTouchEvent(touchStart, this);
+ touchStart = null;
+ }
+ touchContextProvider.cancel();
+ break;
+ case Event.ONTOUCHMOVE:
+ if (isSignificantMove(event)) {
+ /*
+ * TODO figure out scroll delegate don't eat events
+ * if row is selected. Null check for active
+ * delegate is as a workaround.
+ */
+ if (dragmode != 0
+ && touchStart != null
+ && (TouchScrollDelegate
+ .getActiveScrollDelegate() == null)) {
+ startRowDrag(touchStart, type, targetTdOrTr);
+ }
+ touchContextProvider.cancel();
+ /*
+ * Avoid clicks and drags by clearing touch start
+ * flag.
+ */
+ touchStart = null;
+ }
+
+ break;
+ case Event.ONTOUCHSTART:
+ touchStart = event;
+ Touch touch = event.getChangedTouches().get(0);
+ // save position to fields, touches in events are same
+ // 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:
+ /*
+ * When getting a mousedown event, we must detect where
+ * the corresponding mouseup event if it's on a
+ * different part of the page.
+ */
+ lastMouseDownTarget = getElementTdOrTr(Util
+ .getElementUnderMouse(event));
+ mouseUpPreviewMatched = false;
+ mouseUpEventPreviewRegistration = Event
+ .addNativePreviewHandler(mouseUpPreviewHandler);
+
+ if (targetCellOrRowFound) {
+ setRowFocus(this);
+ ensureFocus();
+ if (dragmode != 0
+ && (event.getButton() == NativeEvent.BUTTON_LEFT)) {
+ startRowDrag(event, type, targetTdOrTr);
+
+ } else if (event.getCtrlKey()
+ || event.getShiftKey()
+ || event.getMetaKey()
+ && isMultiSelectModeDefault()) {
+
+ // Prevent default text selection in Firefox
+ event.preventDefault();
+
+ // Prevent default text selection in IE
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast())
+ .setPropertyJSO(
+ "onselectstart",
+ getPreventTextSelectionIEHack());
+ }
+
+ event.stopPropagation();
+ }
+ }
+ break;
+ case Event.ONMOUSEOUT:
+ break;
+ default:
+ break;
+ }
+ }
+ super.onBrowserEvent(event);
+ }
+
+ /** Checks if is significant move.
+ *
+ * @param event
+ * the event
+ * @return true, if is significant move
+ */
+ private boolean isSignificantMove(Event event) {
+ if (touchStart == null) {
+ // no touch start
+ return false;
+ }
+ /*
+ * TODO calculate based on real distance instead of separate
+ * axis checks
+ */
+ Touch touch = event.getChangedTouches().get(0);
+ if (Math.abs(touch.getClientX() - touchStartX) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
+ return true;
+ }
+ if (Math.abs(touch.getClientY() - touchStartY) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
+ return true;
+ }
+ return false;
+ }
+
+ /** Checks if the row represented by the row key has been
+ * selected.
+ *
+ * @param rowKey
+ * the row key
+ * @return true, if successful
+ */
+ private boolean rowKeyIsSelected(int rowKey) {
+ // Check single selections
+ if (selectedRowKeys.contains("" + rowKey)) {
+ return true;
+ }
+
+ // Check range selections
+ for (SelectionRange r : selectedRowRanges) {
+ if (r.inRange(getRenderedRowByKey("" + rowKey))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Start row drag.
+ *
+ * @param event
+ * the event
+ * @param type
+ * the type
+ * @param targetTdOrTr
+ * the target td or tr
+ */
+ protected void startRowDrag(Event event, final int type,
+ Element targetTdOrTr) {
+ VTransferable transferable = new VTransferable();
+ transferable.setDragSource(ConnectorMap.get(client)
+ .getConnector(VCustomScrollTable.this));
+ transferable.setData("itemId", "" + rowKey);
+ NodeList<TableCellElement> cells = rowElement.getCells();
+ for (int i = 0; i < cells.getLength(); i++) {
+ if (cells.getItem(i).isOrHasChild(targetTdOrTr)) {
+ HeaderCell headerCell = tHead.getHeaderCell(i);
+ transferable.setData("propertyId", headerCell.cid);
+ break;
+ }
+ }
+
+ VDragEvent ev = VDragAndDropManager.get().startDrag(
+ transferable, event, true);
+ if (dragmode == DRAGMODE_MULTIROW && isMultiSelectModeAny()
+ && rowKeyIsSelected(rowKey)) {
+
+ // Create a drag image of ALL rows
+ ev.createDragImage(scrollBody.tBodyElement, true);
+
+ // Hide rows which are not selected
+ Element dragImage = ev.getDragImage();
+ int i = 0;
+ for (Iterator<Widget> iterator = scrollBody.iterator(); iterator
+ .hasNext();) {
+ VScrollTableRow next = (VScrollTableRow) iterator
+ .next();
+
+ Element child = (Element) dragImage.getChild(i++);
+
+ if (!rowKeyIsSelected(next.rowKey)) {
+ child.getStyle().setVisibility(Visibility.HIDDEN);
+ }
+ }
+ } else {
+ ev.createDragImage(getElement(), true);
+ }
+ if (type == Event.ONMOUSEDOWN) {
+ event.preventDefault();
+ }
+ event.stopPropagation();
+ }
+
+ /** Finds the TD that the event interacts with. Returns null if
+ * the target of the event should not be handled. If the event
+ * target is the row directly this method returns the TR element
+ * instead of the TD.
+ *
+ * @param event
+ * the event
+ * @return TD or TR element that the event targets (the actual event
+ * target is this element or a child of it)
+ */
+ private Element getEventTargetTdOrTr(Event event) {
+ final Element eventTarget = event.getEventTarget().cast();
+ return getElementTdOrTr(eventTarget);
+ }
+
+ /** Gets the element td or tr.
+ *
+ * @param element
+ * the element
+ * @return the element td or tr
+ */
+ private Element getElementTdOrTr(Element element) {
+
+ Widget widget = Util.findWidget(element, null);
+
+ if (widget != this) {
+ /*
+ * This is a workaround to make Labels, read only TextFields
+ * and Embedded in a Table clickable (see #2688). It is
+ * really not a fix as it does not work with a custom read
+ * only components (not extending VLabel/VEmbedded).
+ */
+ while (widget != null && widget.getParent() != this) {
+ widget = widget.getParent();
+ }
+
+ if (!(widget instanceof VLabel)
+ && !(widget instanceof VEmbedded)
+ && !(widget instanceof VTextField && ((VTextField) widget)
+ .isReadOnly())) {
+ return null;
+ }
+ }
+ return getTdOrTr(element);
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.VCustomScrollTable.ContextMenuOwner#showContextMenu(com.google.gwt.user.client.Event)
+ */
+ @Override
+ public void showContextMenu(Event event) {
+ if (enabled && actionKeys != null) {
+ // Show context menu if there are registered action handlers
+ int left = Util.getTouchOrMouseClientX(event)
+ + Window.getScrollLeft();
+ int top = Util.getTouchOrMouseClientY(event)
+ + Window.getScrollTop();
+ showContextMenu(left, top);
+ }
+ }
+
+ /** Show context menu.
+ *
+ * @param left
+ * the left
+ * @param top
+ * the top
+ */
+ public void showContextMenu(int left, int top) {
+ VContextMenu menu = client.getContextMenu();
+ contextMenu = new ContextMenuDetails(menu, getKey(), left, top);
+ menu.showAt(this, left, top);
+ }
+
+ /** Has the row been selected?.
+ *
+ * @return Returns true if selected, else false
+ */
+ public boolean isSelected() {
+ return selected;
+ }
+
+ /** Toggle the selection of the row.
+ */
+ public void toggleSelection() {
+ selected = !selected;
+ selectionChanged = true;
+ if (selected) {
+ selectedRowKeys.add(String.valueOf(rowKey));
+ addStyleName("v-selected");
+ } else {
+ removeStyleName("v-selected");
+ selectedRowKeys.remove(String.valueOf(rowKey));
+ }
+ }
+
+ /**
+ * Is called when a user clicks an item when holding SHIFT key down.
+ * This will select a new range from the last focused row
+ *
+ * @param deselectPrevious
+ * Should the previous selected range be deselected
+ */
+ private void toggleShiftSelection(boolean deselectPrevious) {
+
+ /*
+ * Ensures that we are in multiselect mode and that we have a
+ * previous selection which was not a deselection
+ */
+ if (isSingleSelectMode()) {
+ // No previous selection found
+ deselectAll();
+ toggleSelection();
+ return;
+ }
+
+ // Set the selectable range
+ VScrollTableRow endRow = this;
+ VScrollTableRow startRow = selectionRangeStart;
+ if (startRow == null) {
+ startRow = focusedRow;
+ selectionRangeStart = focusedRow;
+ // If start row is null then we have a multipage selection
+ // from
+ // above
+ if (startRow == null) {
+ startRow = (VScrollTableRow) scrollBody.iterator()
+ .next();
+ setRowFocus(endRow);
+ }
+ } else if (!startRow.isSelected()) {
+ // The start row is no longer selected (probably removed)
+ // and so we select from above
+ startRow = (VScrollTableRow) scrollBody.iterator().next();
+ setRowFocus(endRow);
+ }
+
+ // Deselect previous items if so desired
+ if (deselectPrevious) {
+ deselectAll();
+ }
+
+ // we'll ensure GUI state from top down even though selection
+ // was the opposite way
+ if (!startRow.isBefore(endRow)) {
+ VScrollTableRow tmp = startRow;
+ startRow = endRow;
+ endRow = tmp;
+ }
+ SelectionRange range = new SelectionRange(startRow, endRow);
+
+ for (Widget w : scrollBody) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (range.inRange(row)) {
+ if (!row.isSelected()) {
+ row.toggleSelection();
+ }
+ selectedRowKeys.add(row.getKey());
+ }
+ }
+
+ // Add range
+ if (startRow != endRow) {
+ selectedRowRanges.add(range);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.ui.IActionOwner#getActions ()
+ */
+
+ @Override
+ public Action[] getActions() {
+ if (actionKeys == null) {
+ return new Action[] {};
+ }
+ final Action[] actions = new Action[actionKeys.length];
+ for (int i = 0; i < actions.length; i++) {
+ final String actionKey = actionKeys[i];
+ final TreeAction a = new TreeAction(this,
+ String.valueOf(rowKey), actionKey) {
+
+ @Override
+ public void execute() {
+ super.execute();
+ lazyRevertFocusToRow(VScrollTableRow.this);
+ }
+ };
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.ActionOwner#getClient()
+ */
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.ActionOwner#getPaintableId()
+ */
+ @Override
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ /** Gets the col index of.
+ *
+ * @param child
+ * the child
+ * @return the col index of
+ */
+ private int getColIndexOf(Widget child) {
+ com.google.gwt.dom.client.Element widgetCell = child
+ .getElement().getParentElement().getParentElement();
+ NodeList<TableCellElement> cells = rowElement.getCells();
+ for (int i = 0; i < cells.getLength(); i++) {
+ if (cells.getItem(i) == widgetCell) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /** Gets the widget for paintable.
+ *
+ * @return the widget for paintable
+ */
+ public Widget getWidgetForPaintable() {
+ return this;
+ }
+ }
+
+ /** The Class VScrollTableGeneratedRow.
+ */
+ protected class VScrollTableGeneratedRow extends VScrollTableRow {
+
+ /** The span columns. */
+ private boolean spanColumns;
+
+ /** The html content allowed. */
+ private boolean htmlContentAllowed;
+
+ /** Instantiates a new v scroll table generated row.
+ *
+ * @param uidl
+ * the uidl
+ * @param aligns
+ * the aligns
+ */
+ public VScrollTableGeneratedRow(UIDL uidl, char[] aligns) {
+ super(uidl, aligns);
+ addStyleName("v-table-generated-row");
+ }
+
+ /** Checks if is span columns.
+ *
+ * @return true, if is span columns
+ */
+ public boolean isSpanColumns() {
+ return spanColumns;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.VCustomScrollTable.VScrollTableBody.VScrollTableRow#initCellWidths()
+ */
+ @Override
+ protected void initCellWidths() {
+ if (spanColumns) {
+ setSpannedColumnWidthAfterDOMFullyInited();
+ } else {
+ super.initCellWidths();
+ }
+ }
+
+ /** Sets the spanned column width after dom fully inited.
+ */
+ private void setSpannedColumnWidthAfterDOMFullyInited() {
+ // Defer setting width on spanned columns to make sure that
+ // they are added to the DOM before trying to calculate
+ // widths.
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (showRowHeaders) {
+ setCellWidth(0, tHead.getHeaderCell(0)
+ .getWidthWithIndent());
+ calcAndSetSpanWidthOnCell(1);
+ } else {
+ calcAndSetSpanWidthOnCell(0);
+ }
+ }
+ });
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.VCustomScrollTable.VScrollTableBody.VScrollTableRow#isRenderHtmlInCells()
+ */
+ @Override
+ protected boolean isRenderHtmlInCells() {
+ return htmlContentAllowed;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.VCustomScrollTable.VScrollTableBody.VScrollTableRow#addCellsFromUIDL(com.vaadin.client.UIDL, char[], int, int)
+ */
+ @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);
+ }
+ }
+
+ /** Adds the spanned cell.
+ *
+ * @param rowUidl
+ * the row uidl
+ * @param w
+ * the w
+ * @param align
+ * the align
+ * @param style
+ * the style
+ * @param sorted
+ * the sorted
+ * @param colCount
+ * the col count
+ */
+ 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);
+ }
+
+ /** Adds the spanned cell.
+ *
+ * @param rowUidl
+ * the row uidl
+ * @param text
+ * the text
+ * @param align
+ * the align
+ * @param style
+ * the style
+ * @param textIsHTML
+ * the text is html
+ * @param sorted
+ * the sorted
+ * @param description
+ * the description
+ * @param colCount
+ * the col count
+ */
+ 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);
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.VCustomScrollTable.VScrollTableBody.VScrollTableRow#setCellWidth(int, int)
+ */
+ @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);
+ }
+ }
+
+ /** Calc and set span width on cell.
+ *
+ * @param cellIx
+ * the cell ix
+ */
+ 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.getLastRendered()
+ - scrollBody.getFirstRendered();
+ if (currentlyVisible < pageLength
+ && currentlyVisible < totalRows) {
+ // shake scrollpanel to fill empty space
+ scrollBodyPanel.setScrollPosition(scrollTop + 1);
+ scrollBodyPanel.setScrollPosition(scrollTop - 1);
+ }
+
+ sizeNeedsInit = true;
+ }
+ }
+
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void updateWidth() {
+ if (!isVisible()) {
+ /*
+ * Do not update size when the table is hidden as all column widths
+ * will be set to zero and they won't be recalculated when the table
+ * is set visible again (until the size changes again)
+ */
+ return;
+ }
+
+ if (!isDynamicWidth()) {
+ int innerPixels = getOffsetWidth() - getBorderWidth();
+ if (innerPixels < 0) {
+ innerPixels = 0;
+ }
+ setContentWidth(innerPixels);
+
+ // readjust undefined width columns
+ triggerLazyColumnAdjustment(false);
+
+ } else {
+
+ sizeNeedsInit = true;
+
+ // readjust undefined width columns
+ triggerLazyColumnAdjustment(false);
+ }
+
+ /*
+ * setting width may affect wheter the component has scrollbars -> needs
+ * scrolling or not
+ */
+ setProperTabIndex();
+ }
+
+ /** The Constant LAZY_COLUMN_ADJUST_TIMEOUT. */
+ private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300;
+
+ /** The lazy adjust column widths. */
+ private final Timer lazyAdjustColumnWidths = new Timer() {
+ /**
+ * Check for column widths, and available width, to see if we can fix
+ * column widths "optimally". Doing this lazily to avoid expensive
+ * calculation when resizing is not yet finished.
+ */
+
+ @Override
+ public void run() {
+ if (scrollBody == null) {
+ // Try again later if we get here before scrollBody has been
+ // initalized
+ triggerLazyColumnAdjustment(false);
+ return;
+ }
+
+ Iterator<Widget> headCells = tHead.iterator();
+ int usedMinimumWidth = 0;
+ int totalExplicitColumnsWidths = 0;
+ float expandRatioDivider = 0;
+ int colIndex = 0;
+
+ int hierarchyColumnIndent = scrollBody.getMaxIndent();
+ int hierarchyColumnIndex = getHierarchyColumnIndex();
+ HeaderCell hierarchyHeaderInNeedOfFurtherHandling = null;
+
+ while (headCells.hasNext()) {
+ final HeaderCell hCell = (HeaderCell) headCells.next();
+ boolean hasIndent = hierarchyColumnIndent > 0
+ && hCell.isHierarchyColumn();
+ if (hCell.isDefinedWidth()) {
+ // get width without indent to find out whether adjustments
+ // are needed (requires special handling further ahead)
+ int w = hCell.getWidth();
+ if (hasIndent && w < hierarchyColumnIndent) {
+ // enforce indent if necessary
+ w = hierarchyColumnIndent;
+ hierarchyHeaderInNeedOfFurtherHandling = hCell;
+ }
+ totalExplicitColumnsWidths += w;
+ usedMinimumWidth += w;
+ } else {
+ // natural width already includes indent if any
+ int naturalColumnWidth = hCell
+ .getNaturalColumnWidth(colIndex);
+ usedMinimumWidth += naturalColumnWidth;
+ expandRatioDivider += hCell.getExpandRatio();
+ if (hasIndent) {
+ hierarchyHeaderInNeedOfFurtherHandling = hCell;
+ }
+ }
+ colIndex++;
+ }
+
+ int availW = scrollBody.getAvailableWidth();
+ // Hey IE, are you really sure about this?
+ availW = scrollBody.getAvailableWidth();
+ int visibleCellCount = tHead.getVisibleCellCount();
+ int totalExtraWidth = scrollBody.getCellExtraWidth()
+ * visibleCellCount;
+ if (willHaveScrollbars()) {
+ totalExtraWidth += Util.getNativeScrollbarSize();
+ }
+ availW -= totalExtraWidth;
+ int forceScrollBodyWidth = -1;
+
+ int extraSpace = availW - usedMinimumWidth;
+ if (extraSpace < 0) {
+ if (getTotalRows() == 0) {
+ /*
+ * Too wide header combined with no rows in the table.
+ *
+ * No horizontal scrollbars would be displayed because
+ * there's no rows that grows too wide causing the
+ * scrollBody container div to overflow. Must explicitely
+ * force a width to a scrollbar. (see #9187)
+ */
+ forceScrollBodyWidth = usedMinimumWidth + totalExtraWidth;
+ }
+ extraSpace = 0;
+ }
+
+ if (forceScrollBodyWidth > 0) {
+ scrollBody.container.getStyle().setWidth(forceScrollBodyWidth,
+ Unit.PX);
+ } else {
+ // Clear width that might have been set to force horizontal
+ // scrolling if there are no rows
+ scrollBody.container.getStyle().clearWidth();
+ }
+
+ int totalUndefinedNaturalWidths = usedMinimumWidth
+ - totalExplicitColumnsWidths;
+
+ if (hierarchyHeaderInNeedOfFurtherHandling != null
+ && !hierarchyHeaderInNeedOfFurtherHandling.isDefinedWidth()) {
+ // ensure the cell gets enough space for the indent
+ int w = hierarchyHeaderInNeedOfFurtherHandling
+ .getNaturalColumnWidth(hierarchyColumnIndex);
+ int newSpace = Math.round(w + (float) extraSpace * (float) w
+ / totalUndefinedNaturalWidths);
+ if (newSpace >= hierarchyColumnIndent) {
+ // no special handling required
+ hierarchyHeaderInNeedOfFurtherHandling = null;
+ } else {
+ // treat as a defined width column of indent's width
+ totalExplicitColumnsWidths += hierarchyColumnIndent;
+ usedMinimumWidth -= w - hierarchyColumnIndent;
+ totalUndefinedNaturalWidths = usedMinimumWidth
+ - totalExplicitColumnsWidths;
+ expandRatioDivider += hierarchyHeaderInNeedOfFurtherHandling
+ .getExpandRatio();
+ extraSpace = Math.max(availW - usedMinimumWidth, 0);
+ }
+ }
+
+ // we have some space that can be divided optimally
+ HeaderCell hCell;
+ colIndex = 0;
+ headCells = tHead.iterator();
+ int checksum = 0;
+ while (headCells.hasNext()) {
+ hCell = (HeaderCell) headCells.next();
+ if (hCell.isResizing) {
+ continue;
+ }
+ if (!hCell.isDefinedWidth()) {
+ int w = hCell.getNaturalColumnWidth(colIndex);
+ int newSpace;
+ if (expandRatioDivider > 0) {
+ // divide excess space by expand ratios
+ newSpace = Math.round((w + extraSpace
+ * hCell.getExpandRatio() / expandRatioDivider));
+ } else {
+ if (hierarchyHeaderInNeedOfFurtherHandling == hCell) {
+ // still exists, so needs exactly the indent's width
+ newSpace = hierarchyColumnIndent;
+ } else if (totalUndefinedNaturalWidths != 0) {
+ // divide relatively to natural column widths
+ newSpace = Math.round(w + (float) extraSpace
+ * (float) w / totalUndefinedNaturalWidths);
+ } else {
+ newSpace = w;
+ }
+ }
+ checksum += newSpace;
+ setColWidth(colIndex, newSpace, false);
+
+ } else {
+ if (hierarchyHeaderInNeedOfFurtherHandling == hCell) {
+ // defined with enforced into indent width
+ checksum += hierarchyColumnIndent;
+ setColWidth(colIndex, hierarchyColumnIndent, false);
+ } else {
+ int cellWidth = hCell.getWidthWithIndent();
+ checksum += cellWidth;
+ if (hCell.isHierarchyColumn()) {
+ // update in case the indent has changed
+ // (not detectable earlier)
+ setColWidth(colIndex, cellWidth, true);
+ }
+ }
+ }
+ colIndex++;
+ }
+
+ if (extraSpace > 0 && checksum != availW) {
+ /*
+ * There might be in some cases a rounding error of 1px when
+ * extra space is divided so if there is one then we give the
+ * first undefined column 1 more pixel
+ */
+ headCells = tHead.iterator();
+ colIndex = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hc = (HeaderCell) headCells.next();
+ if (!hc.isResizing && !hc.isDefinedWidth()) {
+ setColWidth(colIndex, hc.getWidthWithIndent() + availW
+ - checksum, false);
+ break;
+ }
+ colIndex++;
+ }
+ }
+
+ if (isDynamicHeight() && totalRows == pageLength) {
+ // fix body height (may vary if lazy loading is offhorizontal
+ // scrollbar appears/disappears)
+ int bodyHeight = scrollBody.getRequiredHeight();
+ boolean needsSpaceForHorizontalScrollbar = (availW < usedMinimumWidth);
+ if (needsSpaceForHorizontalScrollbar) {
+ bodyHeight += Util.getNativeScrollbarSize();
+ }
+ int heightBefore = getOffsetHeight();
+ scrollBodyPanel.setHeight(bodyHeight + "px");
+
+ if (heightBefore != getOffsetHeight()) {
+ Util.notifyParentOfSizeChange(VCustomScrollTable.this,
+ rendering);
+ }
+ }
+
+ Util.runWebkitOverflowAutoFixDeferred(scrollBodyPanel.getElement());
+
+ forceRealignColumnHeaders();
+ }
+
+ };
+
+ /** Force realign column headers.
+ */
+ 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
+ * the new content width
+ */
+ protected void setContentWidth(int pixels) {
+ tHead.setWidth(pixels + "px");
+ scrollBodyPanel.setWidth(pixels + "px");
+ tFoot.setWidth(pixels + "px");
+ }
+
+ /** The border width. */
+ private int borderWidth = -1;
+
+ /** Gets the border width.
+ *
+ * @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.
+ */
+ protected int containerHeight;
+
+ /** Sets the container height.
+ */
+ protected void setContainerHeight() {
+ if (!isDynamicHeight()) {
+
+ /*
+ * Android 2.3 cannot measure the height of the inline-block
+ * properly, and will return the wrong offset height. So for android
+ * 2.3 we set the element to a block element while measuring and
+ * then restore it which yields the correct result. #11331
+ */
+ if (BrowserInfo.get().isAndroid23()) {
+ getElement().getStyle().setDisplay(Display.BLOCK);
+ }
+
+ containerHeight = getOffsetHeight();
+ containerHeight -= showColHeaders ? tHead.getOffsetHeight() : 0;
+ containerHeight -= tFoot.getOffsetHeight();
+ containerHeight -= getContentAreaBorderHeight();
+ if (containerHeight < 0) {
+ containerHeight = 0;
+ }
+
+ scrollBodyPanel.setHeight(containerHeight + "px");
+
+ if (BrowserInfo.get().isAndroid23()) {
+ getElement().getStyle().clearDisplay();
+ }
+ }
+ }
+
+ /** The content area border height. */
+ private int contentAreaBorderHeight = -1;
+
+ /** The scroll left. */
+ protected int scrollLeft;
+
+ /** The scroll top. */
+ private int scrollTop;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public VScrollTableDropHandler dropHandler;
+
+ /** The nav key down. */
+ private boolean navKeyDown;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean multiselectPending;
+
+ /** Gets the content area border height.
+ *
+ * @return border top + border bottom of the scrollable area of table
+ */
+ protected int getContentAreaBorderHeight() {
+ if (contentAreaBorderHeight < 0) {
+
+ scrollBodyPanel.getElement().getStyle()
+ .setOverflow(Overflow.HIDDEN);
+ int oh = scrollBodyPanel.getOffsetHeight();
+ int ch = scrollBodyPanel.getElement()
+ .getPropertyInt("clientHeight");
+ contentAreaBorderHeight = oh - ch;
+ scrollBodyPanel.getElement().getStyle().setOverflow(Overflow.AUTO);
+ }
+ return contentAreaBorderHeight;
+ }
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.UIObject#setHeight(java.lang.String)
+ */
+ @Override
+ public void setHeight(String height) {
+ if (height.length() == 0
+ && getElement().getStyle().getHeight().length() != 0) {
+ /*
+ * Changing from defined to undefined size -> should do a size init
+ * to take page length into account again
+ */
+ sizeNeedsInit = true;
+ }
+ super.setHeight(height);
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void updateHeight() {
+ setContainerHeight();
+
+ if (initializedAndAttached) {
+ updatePageLength();
+ }
+ 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.
+ // We must run the fix as a deferred command to prevent it from
+ // overwriting the scroll position with an outdated value, see
+ // #7607.
+ Util.runWebkitOverflowAutoFixDeferred(scrollBodyPanel.getElement());
+ }
+
+ triggerLazyColumnAdjustment(false);
+
+ /*
+ * setting height may affect wheter the component has scrollbars ->
+ * needs scrolling or not
+ */
+ setProperTabIndex();
+
+ }
+
+ /*
+ * Overridden due Table might not survive of visibility change (scroll pos
+ * lost). Example ITabPanel just set contained components invisible and back
+ * when changing tabs.
+ */
+
+ /* (non-Javadoc)
+ * @see com.google.gwt.user.client.ui.UIObject#setVisible(boolean)
+ */
+ @Override
+ public void setVisible(boolean visible) {
+ if (isVisible() != visible) {
+ super.setVisible(visible);
+ if (initializedAndAttached) {
+ if (visible) {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /** Helper function to build html snippet for column or row headers.
+ *
+ * @param uidl
+ * possibly with values caption and icon
+ * @return html snippet containing possibly an icon + caption text
+ */
+ protected String buildCaptionHtmlSnippet(UIDL uidl) {
+ String s = uidl.hasAttribute("caption") ? uidl
+ .getStringAttribute("caption") : "";
+ if (uidl.hasAttribute("icon")) {
+ Icon icon = client.getIcon(uidl.getStringAttribute("icon"));
+ icon.setAlternateText("icon");
+ s = icon.getElement().getString() + s;
+ }
+ return s;
+ }
+
+ /** This method has logic which rows needs to be requested from server
+ * when user scrolls.
+ *
+ * @param event
+ * the event
+ */
+
+ @Override
+ public void onScroll(ScrollEvent event) {
+ // Do not handle scroll events while there is scroll initiated from
+ // server side which is not yet executed (#11454)
+ if (isLazyScrollerActive()) {
+ return;
+ }
+
+ scrollLeft = scrollBodyPanel.getElement().getScrollLeft();
+ scrollTop = scrollBodyPanel.getScrollPosition();
+ /*
+ * #6970 - IE sometimes fires scroll events for a detached table.
+ *
+ * FIXME initializedAndAttached should probably be renamed - its name
+ * doesn't seem to reflect its semantics. onDetach() doesn't set it to
+ * false, and changing that might break something else, so we need to
+ * check isAttached() separately.
+ */
+ if (!initializedAndAttached || !isAttached()) {
+ return;
+ }
+ if (!enabled) {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
+ return;
+ }
+
+ rowRequestHandler.cancel();
+
+ if (BrowserInfo.get().isSafari() && event != null && scrollTop == 0) {
+ // due to the webkitoverflowworkaround, top may sometimes report 0
+ // for webkit, although it really is not. Expecting to have the
+ // correct
+ // value available soon.
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ onScroll(null);
+ }
+ });
+ return;
+ }
+
+ // fix headers horizontal scrolling
+ tHead.setHorizontalScrollPosition(scrollLeft);
+
+ // fix footers horizontal scrolling
+ tFoot.setHorizontalScrollPosition(scrollLeft);
+
+ if (totalRows == 0) {
+ // No rows, no need to fetch new rows
+ return;
+ }
+
+ firstRowInViewPort = calcFirstRowInViewPort();
+ int maxFirstRow = totalRows - pageLength;
+ if (firstRowInViewPort > maxFirstRow && maxFirstRow >= 0) {
+ firstRowInViewPort = maxFirstRow;
+ }
+
+ int postLimit = (int) (firstRowInViewPort + (pageLength - 1) + pageLength
+ * cache_react_rate);
+ if (postLimit > totalRows - 1) {
+ postLimit = totalRows - 1;
+ }
+ int preLimit = (int) (firstRowInViewPort - pageLength
+ * cache_react_rate);
+ if (preLimit < 0) {
+ preLimit = 0;
+ }
+ final int lastRendered = scrollBody.getLastRendered();
+ final int firstRendered = scrollBody.getFirstRendered();
+
+ if (postLimit <= lastRendered && preLimit >= firstRendered) {
+ // we're within no-react area, no need to request more rows
+ // remember which firstvisible we requested, in case the server has
+ // a differing opinion
+ lastRequestedFirstvisible = firstRowInViewPort;
+ client.updateVariable(paintableId, "firstvisible",
+ firstRowInViewPort, false);
+ return;
+ }
+
+ if (allRenderedRowsAreNew()) {
+ // need a totally new set of rows
+ rowRequestHandler
+ .setReqFirstRow((firstRowInViewPort - (int) (pageLength * cache_rate)));
+ int last = firstRowInViewPort + (int) (cache_rate * pageLength)
+ + pageLength - 1;
+ if (last >= totalRows) {
+ last = totalRows - 1;
+ }
+ rowRequestHandler.setReqRows(last
+ - rowRequestHandler.getReqFirstRow() + 1);
+ updatedReqRows = false;
+ rowRequestHandler.deferRowFetch();
+ return;
+ }
+ if (preLimit < firstRendered) {
+ // need some rows to the beginning of the rendered area
+ rowRequestHandler
+ .setReqFirstRow((int) (firstRowInViewPort - pageLength
+ * cache_rate));
+ rowRequestHandler.setReqRows(firstRendered
+ - rowRequestHandler.getReqFirstRow());
+ rowRequestHandler.deferRowFetch();
+
+ return;
+ }
+ if (postLimit > lastRendered) {
+ // need some rows to the end of the rendered area
+ int reqRows = (int) ((firstRowInViewPort + pageLength + pageLength
+ * cache_rate) - lastRendered);
+ rowRequestHandler.triggerRowFetch(lastRendered + 1, reqRows);
+ }
+ }
+
+ /** All rendered rows are new.
+ *
+ * @return true, if successful
+ */
+ private boolean allRenderedRowsAreNew() {
+ int firstRowInViewPort = calcFirstRowInViewPort();
+ int firstRendered = scrollBody.getFirstRendered();
+ int lastRendered = scrollBody.getLastRendered();
+ return (firstRowInViewPort - pageLength * cache_rate > lastRendered || firstRowInViewPort
+ + pageLength + pageLength * cache_rate < firstRendered);
+ }
+
+ /** Calc first row in view port.
+ *
+ * @return the int
+ */
+ protected int calcFirstRowInViewPort() {
+ return (int) Math.ceil(scrollTop / scrollBody.getRowHeight());
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.dd.VHasDropHandler#getDropHandler()
+ */
+ @Override
+ public VScrollTableDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ /** The Class TableDDDetails.
+ */
+ private static class TableDDDetails {
+
+ /** The overkey. */
+ int overkey = -1;
+
+ /** The drop location. */
+ VerticalDropLocation dropLocation;
+
+ /** The colkey. */
+ String colkey;
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof TableDDDetails) {
+ TableDDDetails other = (TableDDDetails) obj;
+ return dropLocation == other.dropLocation
+ && overkey == other.overkey
+ && ((colkey != null && colkey.equals(other.colkey)) || (colkey == null && other.colkey == null));
+ }
+ return false;
+ }
+
+ //
+ // public int hashCode() {
+ // return overkey;
+ // }
+ }
+
+ /** The Class VScrollTableDropHandler.
+ */
+ public class VScrollTableDropHandler extends VAbstractDropHandler {
+
+ /** The Constant ROWSTYLEBASE. */
+ private static final String ROWSTYLEBASE = "v-table-row-drag-";
+
+ /** The drop details. */
+ private TableDDDetails dropDetails;
+
+ /** The last emphasized. */
+ private TableDDDetails lastEmphasized;
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.dd.VAbstractDropHandler#dragEnter(com.vaadin.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ updateDropDetails(drag);
+ super.dragEnter(drag);
+ }
+
+ /** Update drop details.
+ *
+ * @param drag
+ * the drag
+ */
+ private void updateDropDetails(VDragEvent drag) {
+ dropDetails = new TableDDDetails();
+ Element elementOver = drag.getElementOver();
+
+ Class<? extends Widget> clazz = getRowClass();
+ VScrollTableRow row = null;
+ if (clazz != null) {
+ row = Util.findWidget(elementOver, clazz);
+ }
+ if (row != null) {
+ dropDetails.overkey = row.rowKey;
+ Element tr = row.getElement();
+ Element element = elementOver;
+ while (element != null && element.getParentElement() != tr) {
+ element = element.getParentElement();
+ }
+ int childIndex = DOM.getChildIndex(tr, element);
+ dropDetails.colkey = tHead.getHeaderCell(childIndex)
+ .getColKey();
+ dropDetails.dropLocation = DDUtil.getVerticalDropLocation(
+ row.getElement(), drag.getCurrentGwtEvent(), 0.2);
+ }
+
+ drag.getDropDetails().put("itemIdOver", dropDetails.overkey + "");
+ drag.getDropDetails().put(
+ "detail",
+ dropDetails.dropLocation != null ? dropDetails.dropLocation
+ .toString() : null);
+
+ }
+
+ /** Gets the row class.
+ *
+ * @return the row class
+ */
+ private Class<? extends Widget> getRowClass() {
+ // get the row type this way to make dd work in derived
+ // implementations
+ Iterator<Widget> iterator = scrollBody.iterator();
+ if (iterator.hasNext()) {
+ return iterator.next().getClass();
+ } else {
+ return null;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.dd.VAbstractDropHandler#dragOver(com.vaadin.client.ui.dd.VDragEvent)
+ */
+ @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);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.dd.VAbstractDropHandler#dragLeave(com.vaadin.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ deEmphasis();
+ super.dragLeave(drag);
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.dd.VAbstractDropHandler#drop(com.vaadin.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public boolean drop(VDragEvent drag) {
+ deEmphasis();
+ return super.drop(drag);
+ }
+
+ /** De emphasis.
+ */
+ private void deEmphasis() {
+ UIObject.setStyleName(getElement(),
+ getStylePrimaryName() + "-drag", false);
+ if (lastEmphasized == null) {
+ return;
+ }
+ for (Widget w : scrollBody.renderedRows) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (lastEmphasized != null
+ && row.rowKey == lastEmphasized.overkey) {
+ String stylename = ROWSTYLEBASE
+ + lastEmphasized.dropLocation.toString()
+ .toLowerCase();
+ VScrollTableRow.setStyleName(row.getElement(), stylename,
+ false);
+ lastEmphasized = null;
+ return;
+ }
+ }
+ }
+
+ /** TODO needs different drop modes ?? (on cells, on rows), now only
+ * supports rows.
+ *
+ * @param details
+ * the details
+ */
+ private void emphasis(TableDDDetails details) {
+ deEmphasis();
+ UIObject.setStyleName(getElement(),
+ getStylePrimaryName() + "-drag", true);
+ // iterate old and new emphasized row
+ for (Widget w : scrollBody.renderedRows) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (details != null && details.overkey == row.rowKey) {
+ String stylename = ROWSTYLEBASE
+ + details.dropLocation.toString().toLowerCase();
+ VScrollTableRow.setStyleName(row.getElement(), stylename,
+ true);
+ lastEmphasized = details;
+ return;
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.dd.VAbstractDropHandler#dragAccepted(com.vaadin.client.ui.dd.VDragEvent)
+ */
+ @Override
+ protected void dragAccepted(VDragEvent drag) {
+ emphasis(dropDetails);
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.dd.VAbstractDropHandler#getConnector()
+ */
+ @Override
+ public ComponentConnector getConnector() {
+ return ConnectorMap.get(client).getConnector(
+ VCustomScrollTable.this);
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.dd.VDropHandler#getApplicationConnection()
+ */
+ @Override
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ }
+
+ /** Gets the currently focused row.
+ *
+ * @return the currently focused row
+ */
+ protected VScrollTableRow getFocusedRow() {
+ return focusedRow;
+ }
+
+ /** Moves the selection head to a specific row.
+ *
+ * @param row
+ * The row to where the selection head should move
+ * @return Returns true if focus was moved successfully, else false
+ */
+ public boolean setRowFocus(VScrollTableRow row) {
+
+ if (!isSelectable()) {
+ return false;
+ }
+
+ // Remove previous selection
+ if (focusedRow != null && focusedRow != row) {
+ focusedRow.removeStyleName(getStylePrimaryName() + "-focus");
+ }
+
+ if (row != null) {
+ // Apply focus style to new selection
+ row.addStyleName(getStylePrimaryName() + "-focus");
+
+ /*
+ * Trying to set focus on already focused row
+ */
+ if (row == focusedRow) {
+ return false;
+ }
+
+ // Set new focused row
+ focusedRow = row;
+
+ ensureRowIsVisible(row);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Ensures that the row is visible.
+ *
+ * @param row
+ * The row to ensure is visible
+ */
+ private void ensureRowIsVisible(VScrollTableRow row) {
+ if (BrowserInfo.get().isTouchDevice()) {
+ // Skip due to android devices that have broken scrolltop will may
+ // get odd scrolling here.
+ return;
+ }
+ /*
+ * FIXME The next line doesn't always do what expected, because if the
+ * row is not in the DOM it won't scroll to it.
+ */
+ Util.scrollIntoViewVertically(row.getElement());
+ }
+
+ /** Handles the keyboard events handled by the table.
+ *
+ * @param keycode
+ * the keycode
+ * @param ctrl
+ * the ctrl
+ * @param shift
+ * the shift
+ * @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;
+ }
+
+ /** Checks if is focus at the beginning of table.
+ *
+ * @return true, if is focus at the beginning of table
+ */
+ private boolean isFocusAtTheBeginningOfTable() {
+ return focusedRow.getIndex() == 0;
+ }
+
+ /** Checks if is focus at the end of table.
+ *
+ * @return true, if is focus at the end of table
+ */
+ private boolean isFocusAtTheEndOfTable() {
+ return focusedRow.getIndex() + 1 >= totalRows;
+ }
+
+ /** Gets the fully visible row count.
+ *
+ * @return the fully visible row count
+ */
+ private int getFullyVisibleRowCount() {
+ return (int) (scrollBodyPanel.getOffsetHeight() / scrollBody
+ .getRowHeight());
+ }
+
+ /** Scroll by pagelenght.
+ *
+ * @param i
+ * the i
+ */
+ 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... (#7965)
+ * ...and sometimes it sends blur events even though the focus
+ * handler is still active. (#10464)
+ */
+ Element focusedElement = Util.getIEFocusedElement();
+ if (Util.getConnectorForElement(client, getParent(), focusedElement) == this
+ && focusedElement != null
+ && focusedElement != scrollBodyPanel.getFocusElement()) {
+ /*
+ * Steal focus back to the focus handler if it was moved to some
+ * other part of the table. Avoid stealing focus in other cases.
+ */
+ focus();
+ return;
+ }
+ }
+
+ if (isFocusable()) {
+ // Unfocus any row
+ setRowFocus(null);
+ }
+ }
+
+ /** Removes a key from a range if the key is found in a selected range.
+ *
+ * @param row
+ * the row
+ */
+ private void removeRowFromUnsentSelectionRanges(VScrollTableRow row) {
+ Collection<SelectionRange> newRanges = null;
+ for (Iterator<SelectionRange> iterator = selectedRowRanges.iterator(); iterator
+ .hasNext();) {
+ SelectionRange range = iterator.next();
+ if (range.inRange(row)) {
+ // Split the range if given row is in range
+ Collection<SelectionRange> splitranges = range.split(row);
+ if (newRanges == null) {
+ newRanges = new ArrayList<SelectionRange>();
+ }
+ newRanges.addAll(splitranges);
+ iterator.remove();
+ }
+ }
+ if (newRanges != null) {
+ selectedRowRanges.addAll(newRanges);
+ }
+ }
+
+ /** Can the Table be focused?.
+ *
+ * @return True if the table can be focused, else false
+ */
+ public boolean isFocusable() {
+ if (scrollBody != null && enabled) {
+ return !(!hasHorizontalScrollbar() && !hasVerticalScrollbar() && !isSelectable());
+ }
+ return false;
+ }
+
+ /** Checks for horizontal scrollbar.
+ *
+ * @return true, if successful
+ */
+ private boolean hasHorizontalScrollbar() {
+ return scrollBody.getOffsetWidth() > scrollBodyPanel.getOffsetWidth();
+ }
+
+ /** Checks for vertical scrollbar.
+ *
+ * @return true, if successful
+ */
+ private boolean hasVerticalScrollbar() {
+ return scrollBody.getOffsetHeight() > scrollBodyPanel.getOffsetHeight();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.Focusable#focus()
+ */
+
+ @Override
+ public void focus() {
+ if (isFocusable()) {
+ scrollBodyPanel.focus();
+ }
+ }
+
+ /**
+ * Sets the proper tabIndex for scrollBodyPanel (the focusable elemen in the
+ * component).
+ * <p>
+ * If the component has no explicit tabIndex a zero is given (default
+ * tabbing order based on dom hierarchy) or -1 if the component does not
+ * need to gain focus. The component needs no focus if it has no scrollabars
+ * (not scrollable) and not selectable. Note that in the future shortcut
+ * actions may need focus.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public void setProperTabIndex() {
+ int storedScrollTop = 0;
+ int storedScrollLeft = 0;
+
+ if (BrowserInfo.get().getOperaVersion() >= 11) {
+ // Workaround for Opera scroll bug when changing tabIndex (#6222)
+ storedScrollTop = scrollBodyPanel.getScrollPosition();
+ storedScrollLeft = scrollBodyPanel.getHorizontalScrollPosition();
+ }
+
+ if (tabIndex == 0 && !isFocusable()) {
+ scrollBodyPanel.setTabIndex(-1);
+ } else {
+ scrollBodyPanel.setTabIndex(tabIndex);
+ }
+
+ if (BrowserInfo.get().getOperaVersion() >= 11) {
+ // Workaround for Opera scroll bug when changing tabIndex (#6222)
+ scrollBodyPanel.setScrollPosition(storedScrollTop);
+ scrollBodyPanel.setHorizontalScrollPosition(storedScrollLeft);
+ }
+ }
+
+ /** Start scrolling velocity timer.
+ */
+ public void startScrollingVelocityTimer() {
+ if (scrollingVelocityTimer == null) {
+ scrollingVelocityTimer = new Timer() {
+
+ @Override
+ public void run() {
+ scrollingVelocity++;
+ }
+ };
+ scrollingVelocityTimer.scheduleRepeating(100);
+ }
+ }
+
+ /** Cancel scrolling velocity timer.
+ */
+ public void cancelScrollingVelocityTimer() {
+ if (scrollingVelocityTimer != null) {
+ // Remove velocityTimer if it exists and the Table is disabled
+ scrollingVelocityTimer.cancel();
+ scrollingVelocityTimer = null;
+ scrollingVelocity = 10;
+ }
+ }
+
+ /** Checks if is navigation key.
+ *
+ * @param keyCode
+ * the key code
+ * @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();
+ }
+
+ /** Lazy revert focus to row.
+ *
+ * @param currentlyFocusedRow
+ * the currently focused row
+ */
+ 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();
+ }
+ });
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.ActionOwner#getActions()
+ */
+ @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;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.ActionOwner#getClient()
+ */
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.ActionOwner#getPaintableId()
+ */
+ @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; };
+ }-*/;
+
+ /** Trigger lazy column adjustment.
+ *
+ * @param now
+ * the now
+ */
+ public void triggerLazyColumnAdjustment(boolean now) {
+ lazyAdjustColumnWidths.cancel();
+ if (now) {
+ lazyAdjustColumnWidths.run();
+ } else {
+ lazyAdjustColumnWidths.schedule(LAZY_COLUMN_ADJUST_TIMEOUT);
+ }
+ }
+
+ /** Checks if is dynamic width.
+ *
+ * @return true, if is dynamic width
+ */
+ private boolean isDynamicWidth() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedWidth();
+ }
+
+ /** Checks if is dynamic height.
+ *
+ * @return true, if is dynamic height
+ */
+ protected 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();
+ }
+
+ /** Debug.
+ *
+ * @param msg
+ * the msg
+ */
+ private void debug(String msg) {
+ if (enableDebug) {
+ VConsole.error(msg);
+ }
+ }
+
+ /** Gets the widget for paintable.
+ *
+ * @return the widget for paintable
+ */
+ public Widget getWidgetForPaintable() {
+ return this;
+ }
+
+ /** The Constant SUBPART_HEADER. */
+ private static final String SUBPART_HEADER = "header";
+
+ /** The Constant SUBPART_FOOTER. */
+ private static final String SUBPART_FOOTER = "footer";
+
+ /** The Constant SUBPART_ROW. */
+ private static final String SUBPART_ROW = "row";
+
+ /** The Constant SUBPART_COL. */
+ private static final String SUBPART_COL = "col";
+
+ /** Matches header[ix] - used for extracting the index of the targeted
+ * header cell.
+ */
+ private static final RegExp SUBPART_HEADER_REGEXP = RegExp
+ .compile(SUBPART_HEADER + "\\[(\\d+)\\]");
+
+ /** Matches footer[ix] - used for extracting the index of the targeted
+ * footer cell.
+ */
+ private static final RegExp SUBPART_FOOTER_REGEXP = RegExp
+ .compile(SUBPART_FOOTER + "\\[(\\d+)\\]");
+
+ /** Matches row[ix] - used for extracting the index of the targeted row. */
+ private static final RegExp SUBPART_ROW_REGEXP = RegExp.compile(SUBPART_ROW
+ + "\\[(\\d+)]");
+
+ /** Matches col[ix] - used for extracting the index of the targeted
+ * column.
+ */
+ private static final RegExp SUBPART_ROW_COL_REGEXP = RegExp
+ .compile(SUBPART_ROW + "\\[(\\d+)\\]/" + SUBPART_COL
+ + "\\[(\\d+)\\]");
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.SubPartAware#getSubPartElement(java.lang.String)
+ */
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
+ if (SUBPART_ROW_COL_REGEXP.test(subPart)) {
+ MatchResult result = SUBPART_ROW_COL_REGEXP.exec(subPart);
+ int rowIx = Integer.valueOf(result.getGroup(1));
+ int colIx = Integer.valueOf(result.getGroup(2));
+ VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx);
+ if (row != null) {
+ Element rowElement = row.getElement();
+ if (colIx < rowElement.getChildCount()) {
+ return rowElement.getChild(colIx).getFirstChild().cast();
+ }
+ }
+
+ } else if (SUBPART_ROW_REGEXP.test(subPart)) {
+ MatchResult result = SUBPART_ROW_REGEXP.exec(subPart);
+ int rowIx = Integer.valueOf(result.getGroup(1));
+ VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx);
+ if (row != null) {
+ return row.getElement();
+ }
+
+ } else if (SUBPART_HEADER_REGEXP.test(subPart)) {
+ MatchResult result = SUBPART_HEADER_REGEXP.exec(subPart);
+ int headerIx = Integer.valueOf(result.getGroup(1));
+ HeaderCell headerCell = tHead.getHeaderCell(headerIx);
+ if (headerCell != null) {
+ return headerCell.getElement();
+ }
+
+ } else if (SUBPART_FOOTER_REGEXP.test(subPart)) {
+ MatchResult result = SUBPART_FOOTER_REGEXP.exec(subPart);
+ int footerIx = Integer.valueOf(result.getGroup(1));
+ FooterCell footerCell = tFoot.getFooterCell(footerIx);
+ if (footerCell != null) {
+ return footerCell.getElement();
+ }
+ }
+ // Nothing found.
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.ui.SubPartAware#getSubPartName(com.google.gwt.user.client.Element)
+ */
+ @Override
+ public String getSubPartName(com.google.gwt.user.client.Element subElement) {
+ Widget widget = Util.findWidget(subElement, null);
+ if (widget instanceof HeaderCell) {
+ return SUBPART_HEADER + "[" + tHead.visibleCells.indexOf(widget)
+ + "]";
+ } else if (widget instanceof FooterCell) {
+ return SUBPART_FOOTER + "[" + tFoot.visibleCells.indexOf(widget)
+ + "]";
+ } else if (widget instanceof VScrollTableRow) {
+ // a cell in a row
+ VScrollTableRow row = (VScrollTableRow) widget;
+ int rowIx = scrollBody.indexOf(row);
+ if (rowIx >= 0) {
+ int colIx = -1;
+ for (int ix = 0; ix < row.getElement().getChildCount(); ix++) {
+ if (row.getElement().getChild(ix).isOrHasChild(subElement)) {
+ colIx = ix;
+ break;
+ }
+ }
+ if (colIx >= 0) {
+ return SUBPART_ROW + "[" + rowIx + "]/" + SUBPART_COL + "["
+ + colIx + "]";
+ }
+ return SUBPART_ROW + "[" + rowIx + "]";
+ }
+ }
+ // Nothing found.
+ return null;
+ }
+
+ /** On unregister.
+ *
+ * @since 7.2.6
+ */
+ public void onUnregister() {
+ if (addCloseHandler != null) {
+ addCloseHandler.removeHandler();
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.client.DeferredWorker#isWorkPending()
+ */
+ /*
+ * Return true if component need to perform some work and false otherwise.
+ */
+ @Override
+ public boolean isWorkPending() {
+ return lazyAdjustColumnWidths.isRunning();
+ }
+
+ /** Gets the logger.
+ *
+ * @return the logger
+ */
+ private static Logger getLogger() {
+ return Logger.getLogger(VCustomScrollTable.class.getName());
+ }
+}
diff --git a/org.eclipse.osbp.fork.vaadin.addon.filteringtable/src/com/vaadin/ui/CustomTable.java b/org.eclipse.osbp.fork.vaadin.addon.filteringtable/src/com/vaadin/ui/CustomTable.java
new file mode 100644
index 0000000..5456948
--- /dev/null
+++ b/org.eclipse.osbp.fork.vaadin.addon.filteringtable/src/com/vaadin/ui/CustomTable.java
@@ -0,0 +1,6638 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.ContainerOrderedWrapper;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.ConverterUtil;
+import com.vaadin.event.Action;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.event.DataBoundTransferable;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.ItemClickEvent.ItemClickNotifier;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DragSource;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.LegacyCommunicationManager;
+import com.vaadin.server.LegacyPaint;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.server.Resource;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.MultiSelectMode;
+import com.vaadin.shared.ui.table.TableConstants;
+
+// TODO: Auto-generated Javadoc
+/**
+ * <p>
+ * <code>CustomTable</code> is used for representing data or components in a
+ * pageable and selectable CustomTable.
+ *
+ *
+ * <p>
+ * Scalability of the CustomTable is largely dictated by the container. A table
+ * does not have a limit for the number of items and is just as fast with
+ * hundreds of thousands of items as with just a few. The current GWT
+ * implementation with scrolling however limits the number of rows to around
+ * 500000, depending on the browser and the pixel height of rows.
+ *
+ *
+ * <p>
+ * Components in a CustomTable will not have their caption nor icon rendered.
+ *
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+@SuppressWarnings({ "deprecation" })
+public class CustomTable extends AbstractSelect implements Action.Container,
+ Container.Ordered, Container.Sortable, ItemClickNotifier, DragSource,
+ DropTarget, HasComponents {
+
+ /** The logger. */
+ private transient Logger logger = null;
+
+ /**
+ * Modes that CustomTable support as drag sourse.
+ */
+ public enum TableDragMode {
+ /**
+ * CustomTable does not start drag and drop events. HTM5 style events
+ * started by browser may still happen.
+ */
+ NONE,
+ /**
+ * CustomTable starts drag with a one row only.
+ */
+ ROW,
+ /**
+ * CustomTable drags selected rows, if drag starts on a selected rows.
+ * Else it starts like in ROW mode. Note, that in Transferable there
+ * will still be only the row on which the drag started, other dragged
+ * rows need to be checked from the source CustomTable.
+ */
+ MULTIROW
+ }
+
+ /** The Constant CELL_KEY. */
+ protected static final int CELL_KEY = 0;
+
+ /** The Constant CELL_HEADER. */
+ protected static final int CELL_HEADER = 1;
+
+ /** The Constant CELL_ICON. */
+ protected static final int CELL_ICON = 2;
+
+ /** The Constant CELL_ITEMID. */
+ protected static final int CELL_ITEMID = 3;
+
+ /** The Constant CELL_GENERATED_ROW. */
+ protected static final int CELL_GENERATED_ROW = 4;
+
+ /** The Constant CELL_FIRSTCOL. */
+ protected static final int CELL_FIRSTCOL = 5;
+
+ /** The Enum Align.
+ */
+ public enum Align {
+ /**
+ * Left column alignment. <b>This is the default behaviour. </b>
+ */
+ LEFT("b"),
+
+ /**
+ * Center column alignment.
+ */
+ CENTER("c"),
+
+ /**
+ * Right column alignment.
+ */
+ RIGHT("e");
+
+ /** The alignment. */
+ private String alignment;
+
+ /** Instantiates a new align.
+ *
+ * @param alignment
+ * the alignment
+ */
+ private Align(String alignment) {
+ this.alignment = alignment;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Enum#toString()
+ */
+ @Override
+ public String toString() {
+ return alignment;
+ }
+
+ /** Convert string to align.
+ *
+ * @param string
+ * the string
+ * @return the align
+ */
+ public Align convertStringToAlign(String string) {
+ if (string == null) {
+ return null;
+ }
+ if (string.equals("b")) {
+ return Align.LEFT;
+ } else if (string.equals("c")) {
+ return Align.CENTER;
+ } else if (string.equals("e")) {
+ return Align.RIGHT;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /** The Constant ALIGN_LEFT.
+ *
+ * @deprecated As of 7.0, use {@link Align#LEFT} instead
+ */
+ @Deprecated
+ public static final Align ALIGN_LEFT = Align.LEFT;
+
+ /** The Constant ALIGN_CENTER.
+ *
+ * @deprecated As of 7.0, use {@link Align#CENTER} instead
+ */
+ @Deprecated
+ public static final Align ALIGN_CENTER = Align.CENTER;
+
+ /** The Constant ALIGN_RIGHT.
+ *
+ * @deprecated As of 7.0, use {@link Align#RIGHT} instead
+ */
+ @Deprecated
+ public static final Align ALIGN_RIGHT = Align.RIGHT;
+
+ /** The Enum ColumnHeaderMode.
+ */
+ public enum ColumnHeaderMode {
+ /**
+ * Column headers are hidden.
+ */
+ HIDDEN,
+ /**
+ * Property ID:s are used as column headers.
+ */
+ ID,
+ /**
+ * Column headers are explicitly specified with
+ * {@link #setColumnHeaders(String[])}.
+ */
+ EXPLICIT,
+ /**
+ * Column headers are explicitly specified with
+ * {@link #setColumnHeaders(String[])}. If a header is not specified for
+ * a given property, its property id is used instead.
+ * <p>
+ * <b>This is the default behavior. </b>
+ */
+ EXPLICIT_DEFAULTS_ID
+ }
+
+ /** The Constant COLUMN_HEADER_MODE_HIDDEN.
+ *
+ * @deprecated As of 7.0, use {@link ColumnHeaderMode#HIDDEN} instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_HIDDEN = ColumnHeaderMode.HIDDEN;
+
+ /** The Constant COLUMN_HEADER_MODE_ID.
+ *
+ * @deprecated As of 7.0, use {@link ColumnHeaderMode#ID} instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_ID = ColumnHeaderMode.ID;
+
+ /** The Constant COLUMN_HEADER_MODE_EXPLICIT.
+ *
+ * @deprecated As of 7.0, use {@link ColumnHeaderMode#EXPLICIT} instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT = ColumnHeaderMode.EXPLICIT;
+
+ /** The Constant COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID.
+ *
+ * @deprecated As of 7.0, use {@link ColumnHeaderMode#EXPLICIT_DEFAULTS_ID}
+ * instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /** The Enum RowHeaderMode.
+ */
+ public enum RowHeaderMode {
+ /**
+ * Row caption mode: The row headers are hidden. <b>This is the default
+ * mode. </b>
+ */
+ HIDDEN(null),
+ /**
+ * Row caption mode: Items Id-objects toString is used as row caption.
+ */
+ ID(ItemCaptionMode.ID),
+ /**
+ * Row caption mode: Item-objects toString is used as row caption.
+ */
+ ITEM(ItemCaptionMode.ITEM),
+ /**
+ * Row caption mode: Index of the item is used as item caption. The
+ * index mode can only be used with the containers implementing the
+ * {@link com.vaadin.data.Container.Indexed} interface.
+ */
+ INDEX(ItemCaptionMode.INDEX),
+ /**
+ * Row caption mode: Item captions are explicitly specified, but if the
+ * caption is missing, the item id objects <code>toString()</code> is
+ * used instead.
+ */
+ EXPLICIT_DEFAULTS_ID(ItemCaptionMode.EXPLICIT_DEFAULTS_ID),
+ /**
+ * Row caption mode: Item captions are explicitly specified.
+ */
+ EXPLICIT(ItemCaptionMode.EXPLICIT),
+ /**
+ * Row caption mode: Only icons are shown, the captions are hidden.
+ */
+ ICON_ONLY(ItemCaptionMode.ICON_ONLY),
+ /**
+ * Row caption mode: Item captions are read from property specified with
+ * {@link #setItemCaptionPropertyId(Object)}.
+ */
+ PROPERTY(ItemCaptionMode.PROPERTY);
+
+ /** The mode. */
+ ItemCaptionMode mode;
+
+ /** Instantiates a new row header mode.
+ *
+ * @param mode
+ * the mode
+ */
+ private RowHeaderMode(ItemCaptionMode mode) {
+ this.mode = mode;
+ }
+
+ /** Gets the item caption mode.
+ *
+ * @return the item caption mode
+ */
+ public ItemCaptionMode getItemCaptionMode() {
+ return mode;
+ }
+ }
+
+ /** The Constant ROW_HEADER_MODE_HIDDEN.
+ *
+ * @deprecated As of 7.0, use {@link RowHeaderMode#HIDDEN} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_HIDDEN = RowHeaderMode.HIDDEN;
+
+ /** The Constant ROW_HEADER_MODE_ID.
+ *
+ * @deprecated As of 7.0, use {@link RowHeaderMode#ID} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_ID = RowHeaderMode.ID;
+
+ /** The Constant ROW_HEADER_MODE_ITEM.
+ *
+ * @deprecated As of 7.0, use {@link RowHeaderMode#ITEM} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_ITEM = RowHeaderMode.ITEM;
+
+ /** The Constant ROW_HEADER_MODE_INDEX.
+ *
+ * @deprecated As of 7.0, use {@link RowHeaderMode#INDEX} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_INDEX = RowHeaderMode.INDEX;
+
+ /** The Constant ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID.
+ *
+ * @deprecated As of 7.0, use {@link RowHeaderMode#EXPLICIT_DEFAULTS_ID}
+ * instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID = RowHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /** The Constant ROW_HEADER_MODE_EXPLICIT.
+ *
+ * @deprecated As of 7.0, use {@link RowHeaderMode#EXPLICIT} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT = RowHeaderMode.EXPLICIT;
+
+ /** The Constant ROW_HEADER_MODE_ICON_ONLY.
+ *
+ * @deprecated As of 7.0, use {@link RowHeaderMode#ICON_ONLY} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_ICON_ONLY = RowHeaderMode.ICON_ONLY;
+
+ /** The Constant ROW_HEADER_MODE_PROPERTY.
+ *
+ * @deprecated As of 7.0, use {@link RowHeaderMode#PROPERTY} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_PROPERTY = RowHeaderMode.PROPERTY;
+
+ /**
+ * The default rate that table caches rows for smooth scrolling.
+ */
+ private static final double CACHE_RATE_DEFAULT = 2;
+
+ /** The Constant ROW_HEADER_COLUMN_KEY. */
+ private static final String ROW_HEADER_COLUMN_KEY = "0";
+
+ /** The Constant ROW_HEADER_FAKE_PROPERTY_ID. */
+ private static final Object ROW_HEADER_FAKE_PROPERTY_ID = new UniqueSerializable() {
+ };
+
+ /* Private table extensions to Select */
+
+ /**
+ * True if column collapsing is allowed.
+ */
+ private boolean columnCollapsingAllowed = false;
+
+ /**
+ * True if reordering of columns is allowed on the client side.
+ */
+ private boolean columnReorderingAllowed = false;
+
+ /**
+ * Keymapper for column ids.
+ */
+ protected final KeyMapper<Object> columnIdMap = new KeyMapper<Object>();
+
+ /**
+ * Holds visible column propertyIds - in order.
+ */
+ private LinkedList<Object> visibleColumns = new LinkedList<Object>();
+
+ /**
+ * Holds noncollapsible columns.
+ */
+ private HashSet<Object> noncollapsibleColumns = new HashSet<Object>();
+
+ /**
+ * Holds propertyIds of currently collapsed columns.
+ */
+ private final HashSet<Object> collapsedColumns = new HashSet<Object>();
+
+ /**
+ * Holds headers for visible columns (by propertyId).
+ */
+ private final HashMap<Object, String> columnHeaders = new HashMap<Object, String>();
+
+ /**
+ * Holds footers for visible columns (by propertyId).
+ */
+ private final HashMap<Object, String> columnFooters = new HashMap<Object, String>();
+
+ /**
+ * Holds icons for visible columns (by propertyId).
+ */
+ private final HashMap<Object, Resource> columnIcons = new HashMap<Object, Resource>();
+
+ /**
+ * Holds alignments for visible columns (by propertyId).
+ */
+ private HashMap<Object, Align> columnAlignments = new HashMap<Object, Align>();
+
+ /**
+ * Holds column widths in pixels for visible columns (by propertyId).
+ */
+ private final HashMap<Object, Integer> columnWidths = new HashMap<Object, Integer>();
+
+ /**
+ * Holds column expand rations for visible columns (by propertyId).
+ */
+ private final HashMap<Object, Float> columnExpandRatios = new HashMap<Object, Float>();
+
+ /** Holds column generators. */
+ private final HashMap<Object, ColumnGenerator> columnGenerators = new LinkedHashMap<Object, ColumnGenerator>();
+
+ /**
+ * Holds value of property pageLength. 0 disables paging.
+ */
+ private int pageLength = 15;
+
+ /**
+ * Id the first item on the current page.
+ */
+ private Object currentPageFirstItemId = null;
+
+ /**
+ * Index of the first item on the current page.
+ */
+ private int currentPageFirstItemIndex = 0;
+
+ /**
+ * Index of the "first" item on the last page if a user has used
+ * setCurrentPageFirstItemIndex to scroll down. -1 if not set.
+ */
+ private int currentPageFirstItemIndexOnLastPage = -1;
+
+ /**
+ * Holds value of property selectable.
+ */
+ private boolean selectable = false;
+
+ /**
+ * Holds value of property columnHeaderMode.
+ */
+ private ColumnHeaderMode columnHeaderMode = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * Holds value of property rowHeaderMode.
+ */
+ private RowHeaderMode rowHeaderMode = RowHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /** Should the CustomTable footer be visible?. */
+ private boolean columnFootersVisible = false;
+
+ /**
+ * Page contents buffer used in buffered mode.
+ */
+ private Object[][] pageBuffer = null;
+
+ /**
+ * Set of properties listened - the list is kept to release the listeners
+ * later.
+ */
+ private HashSet<Property<?>> listenedProperties = null;
+
+ /**
+ * Set of visible components - the is used for needsRepaint calculation.
+ */
+ protected HashSet<Component> visibleComponents = null;
+
+ /**
+ * List of action handlers.
+ */
+ private LinkedList<Handler> actionHandlers = null;
+
+ /**
+ * Action mapper.
+ */
+ private KeyMapper<Action> actionMapper = null;
+
+ /**
+ * CustomTable cell editor factory.
+ */
+ private TableFieldFactory fieldFactory = DefaultFieldFactory.get();
+
+ /**
+ * Is table editable.
+ */
+ private boolean editable = false;
+
+ /**
+ * Current sorting direction.
+ */
+ private boolean sortAscending = true;
+
+ /**
+ * Currently table is sorted on this propertyId.
+ */
+ private Object sortContainerPropertyId = null;
+
+ /**
+ * Is table sorting by the user enabled.
+ */
+ private boolean sortEnabled = true;
+
+ /**
+ * Number of rows explicitly requested by the client to be painted on next
+ * paint. This is -1 if no request by the client is made. Painting the
+ * component will automatically reset this to -1.
+ */
+ private int reqRowsToPaint = -1;
+
+ /**
+ * Index of the first rows explicitly requested by the client to be painted.
+ * This is -1 if no request by the client is made. Painting the component
+ * will automatically reset this to -1.
+ */
+ private int reqFirstRowToPaint = -1;
+
+ /** The first to be rendered in client. */
+ private int firstToBeRenderedInClient = -1;
+
+ /** The last to be rendered in client. */
+ private int lastToBeRenderedInClient = -1;
+
+ /** The is content refreshes enabled. */
+ private boolean isContentRefreshesEnabled = true;
+
+ /** The page buffer first index. */
+ private int pageBufferFirstIndex;
+
+ /** The container change to be rendered. */
+ private boolean containerChangeToBeRendered = false;
+
+ /** CustomTable cell specific style generator. */
+ private CellStyleGenerator cellStyleGenerator = null;
+
+ /** CustomTable cell specific tooltip generator. */
+ private ItemDescriptionGenerator itemDescriptionGenerator;
+
+ /** The always recalculate column widths. */
+ /*
+ * EXPERIMENTAL feature: will tell the client to re-calculate column widths
+ * if set to true. Currently no setter: extend to enable.
+ */
+ protected boolean alwaysRecalculateColumnWidths = false;
+
+ /** The cache rate. */
+ private double cacheRate = CACHE_RATE_DEFAULT;
+
+ /** The drag mode. */
+ private TableDragMode dragMode = TableDragMode.NONE;
+
+ /** The drop handler. */
+ private DropHandler dropHandler;
+
+ /** The multi select mode. */
+ private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT;
+
+ /** The row cache invalidated. */
+ private boolean rowCacheInvalidated;
+
+ /** The row generator. */
+ private RowGenerator rowGenerator = null;
+
+ /** The associated properties. */
+ private final Map<Field<?>, Property<?>> associatedProperties = new HashMap<Field<?>, Property<?>>();
+
+ /** The painted. */
+ private boolean painted = false;
+
+ /** The property value converters. */
+ private HashMap<Object, Converter<String, Object>> propertyValueConverters = new HashMap<Object, Converter<String, Object>>();
+
+ /**
+ * Set to true if the client-side should be informed that the key mapper has
+ * been reset so it can avoid sending back references to keys that are no
+ * longer present.
+ */
+ private boolean keyMapperReset;
+
+ /** The exceptions during cache population. */
+ private List<Throwable> exceptionsDuringCachePopulation = new ArrayList<Throwable>();
+
+ /** The is being painted. */
+ private boolean isBeingPainted;
+
+ /* CustomTable constructors */
+
+ /**
+ * Creates a new empty table.
+ */
+ public CustomTable() {
+ setRowHeaderMode(ROW_HEADER_MODE_HIDDEN);
+ }
+
+ /** Creates a new empty table with caption.
+ *
+ * @param caption
+ * the caption
+ */
+ public CustomTable(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /** Creates a new table with caption and connect it to a Container.
+ *
+ * @param caption
+ * the caption
+ * @param dataSource
+ * the data source
+ */
+ public CustomTable(String caption, Container dataSource) {
+ this();
+ setCaption(caption);
+ setContainerDataSource(dataSource);
+ }
+
+ /* CustomTable functionality */
+
+ /**
+ * Gets the array of visible column id:s, including generated columns.
+ *
+ * <p>
+ * The columns are show in the order of their appearance in this array.
+ *
+ *
+ * @return an array of currently visible propertyIds and generated column
+ * ids.
+ */
+ public Object[] getVisibleColumns() {
+ if (visibleColumns == null) {
+ return null;
+ }
+ return visibleColumns.toArray();
+ }
+
+ /**
+ * Sets the array of visible column property id:s.
+ *
+ * <p>
+ * The columns are show in the order of their appearance in this array.
+ *
+ *
+ * @param visibleColumns
+ * the Array of shown property id:s.
+ */
+ public void setVisibleColumns(Object... visibleColumns) {
+
+ // Visible columns must exist
+ if (visibleColumns == null) {
+ throw new NullPointerException(
+ "Can not set visible columns to null value");
+ }
+
+ final LinkedList<Object> newVC = new LinkedList<Object>();
+
+ // Checks that the new visible columns contains no nulls, properties
+ // exist and that there are no duplicates before adding them to newVC.
+ final Collection<?> properties = getContainerPropertyIds();
+ for (int i = 0; i < visibleColumns.length; i++) {
+ if (visibleColumns[i] == null) {
+ throw new NullPointerException("Ids must be non-nulls");
+ } else if (!properties.contains(visibleColumns[i])
+ && !columnGenerators.containsKey(visibleColumns[i])) {
+ throw new IllegalArgumentException(
+ "Ids must exist in the Container or as a generated column, missing id: "
+ + visibleColumns[i]);
+ } else if (newVC.contains(visibleColumns[i])) {
+ throw new IllegalArgumentException(
+ "Ids must be unique, duplicate id: "
+ + visibleColumns[i]);
+ } else {
+ newVC.add(visibleColumns[i]);
+ }
+ }
+
+ // Removes alignments, icons and headers from hidden columns
+ if (this.visibleColumns != null) {
+ boolean disabledHere = disableContentRefreshing();
+ try {
+ for (final Iterator<Object> i = this.visibleColumns.iterator(); i
+ .hasNext();) {
+ final Object col = i.next();
+ if (!newVC.contains(col)) {
+ setColumnHeader(col, null);
+ setColumnAlignment(col, (Align) null);
+ setColumnIcon(col, null);
+ }
+ }
+ } finally {
+ if (disabledHere) {
+ enableContentRefreshing(false);
+ }
+ }
+ }
+
+ this.visibleColumns = newVC;
+
+ // Assures visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Gets the headers of the columns.
+ *
+ * <p>
+ * The headers match the property id:s given my the set visible column
+ * headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the
+ * headers. In the defaults mode any nulls in the headers array are replaced
+ * with id.toString().
+ *
+ *
+ * @return the Array of column headers.
+ */
+ public String[] getColumnHeaders() {
+ if (columnHeaders == null) {
+ return null;
+ }
+ final String[] headers = new String[visibleColumns.size()];
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); i++) {
+ headers[i] = getColumnHeader(it.next());
+ }
+ return headers;
+ }
+
+ /**
+ * Sets the headers of the columns.
+ *
+ * <p>
+ * The headers match the property id:s given my the set visible column
+ * headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the
+ * headers. In the defaults mode any nulls in the headers array are replaced
+ * with id.toString() outputs when rendering.
+ *
+ *
+ * @param columnHeaders
+ * the Array of column headers that match the
+ * {@link #getVisibleColumns()} method.
+ */
+ public void setColumnHeaders(String... columnHeaders) {
+
+ if (columnHeaders.length != visibleColumns.size()) {
+ throw new IllegalArgumentException(
+ "The length of the headers array must match the number of visible columns");
+ }
+
+ this.columnHeaders.clear();
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext() && i < columnHeaders.length; i++) {
+ this.columnHeaders.put(it.next(), columnHeaders[i]);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Gets the icons of the columns.
+ *
+ * <p>
+ * The icons in headers match the property id:s given my the set visible
+ * column headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers
+ * with icons.
+ *
+ *
+ * @return the Array of icons that match the {@link #getVisibleColumns()}.
+ */
+ public Resource[] getColumnIcons() {
+ if (columnIcons == null) {
+ return null;
+ }
+ final Resource[] icons = new Resource[visibleColumns.size()];
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); i++) {
+ icons[i] = columnIcons.get(it.next());
+ }
+
+ return icons;
+ }
+
+ /**
+ * Sets the icons of the columns.
+ *
+ * <p>
+ * The icons in headers match the property id:s given my the set visible
+ * column headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers
+ * with icons.
+ *
+ *
+ * @param columnIcons
+ * the Array of icons that match the {@link #getVisibleColumns()}
+ * .
+ */
+ public void setColumnIcons(Resource... columnIcons) {
+
+ if (columnIcons.length != visibleColumns.size()) {
+ throw new IllegalArgumentException(
+ "The length of the icons array must match the number of visible columns");
+ }
+
+ this.columnIcons.clear();
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext() && i < columnIcons.length; i++) {
+ this.columnIcons.put(it.next(), columnIcons[i]);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Gets the array of column alignments.
+ *
+ * <p>
+ * The items in the array must match the properties identified by
+ * {@link #getVisibleColumns()}. The possible values for the alignments
+ * include:
+ * <ul>
+ * <li>{@link Align#LEFT}: Left alignment</li>
+ * <li>{@link Align#CENTER}: Centered</li>
+ * <li>{@link Align#RIGHT}: Right alignment</li>
+ * </ul>
+ * The alignments default to {@link Align#LEFT}: any null values are
+ * rendered as align lefts.
+ *
+ *
+ * @return the Column alignments array.
+ */
+ public Align[] getColumnAlignments() {
+ if (columnAlignments == null) {
+ return null;
+ }
+ final Align[] alignments = new Align[visibleColumns.size()];
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); i++) {
+ alignments[i] = getColumnAlignment(it.next());
+ }
+
+ return alignments;
+ }
+
+ /**
+ * Sets the column alignments.
+ *
+ * <p>
+ * The amount of items in the array must match the amount of properties
+ * identified by {@link #getVisibleColumns()}. The possible values for the
+ * alignments include:
+ * <ul>
+ * <li>{@link Align#LEFT}: Left alignment</li>
+ * <li>{@link Align#CENTER}: Centered</li>
+ * <li>{@link Align#RIGHT}: Right alignment</li>
+ * </ul>
+ * The alignments default to {@link Align#LEFT}
+ *
+ *
+ * @param columnAlignments
+ * the Column alignments array.
+ */
+ public void setColumnAlignments(Align... columnAlignments) {
+
+ if (columnAlignments.length != visibleColumns.size()) {
+ throw new IllegalArgumentException(
+ "The length of the alignments array must match the number of visible columns");
+ }
+
+ // Resets the alignments
+ final HashMap<Object, Align> newCA = new HashMap<Object, Align>();
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext() && i < columnAlignments.length; i++) {
+ newCA.put(it.next(), columnAlignments[i]);
+ }
+ this.columnAlignments = newCA;
+
+ // Assures the visual refresh. No need to reset the page buffer before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+
+ /**
+ * Sets columns width (in pixels). Theme may not necessary respect very
+ * small or very big values. Setting width to -1 (default) means that theme
+ * will make decision of width.
+ *
+ * <p>
+ * Column can either have a fixed width or expand ratio. The latter one set
+ * is used. See @link {@link #setColumnExpandRatio(Object, float)}.
+ *
+ * @param propertyId
+ * colunmns property id
+ * @param width
+ * width to be reserved for colunmns content
+ * @since 4.0.3
+ */
+ public void setColumnWidth(Object propertyId, int width) {
+ if (propertyId == null) {
+ // Since propertyId is null, this is the row header. Use the magic
+ // id to store the width of the row header.
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+
+ // Setting column width should remove any expand ratios as well
+ columnExpandRatios.remove(propertyId);
+
+ if (width < 0) {
+ columnWidths.remove(propertyId);
+ } else {
+ columnWidths.put(propertyId, width);
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Sets the column expand ratio for given column.
+ * <p>
+ * Expand ratios can be defined to customize the way how excess space is
+ * divided among columns. CustomTable can have excess space if it has its
+ * width defined and there is horizontally more space than columns consume
+ * naturally. Excess space is the space that is not used by columns with
+ * explicit width (see {@link #setColumnWidth(Object, int)}) or with natural
+ * width (no width nor expand ratio).
+ *
+ * <p>
+ * By default (without expand ratios) the excess space is divided
+ * proportionally to columns natural widths.
+ *
+ * <p>
+ * Only expand ratios of visible columns are used in final calculations.
+ *
+ * <p>
+ * Column can either have a fixed width or expand ratio. The latter one set
+ * is used.
+ *
+ * <p>
+ * A column with expand ratio is considered to be minimum width by default
+ * (if no excess space exists). The minimum width is defined by terminal
+ * implementation.
+ *
+ * <p>
+ * If terminal implementation supports re-sizable columns the column becomes
+ * fixed width column if users resizes the column.
+ *
+ * @param propertyId
+ * columns property id
+ * @param expandRatio
+ * the expandRatio used to divide excess space for this column
+ */
+ public void setColumnExpandRatio(Object propertyId, float expandRatio) {
+ if (propertyId == null) {
+ // Since propertyId is null, this is the row header. Use the magic
+ // id to store the width of the row header.
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+
+ // Setting the column expand ratio should remove and defined column
+ // width
+ columnWidths.remove(propertyId);
+
+ if (expandRatio < 0) {
+ columnExpandRatios.remove(propertyId);
+ } else {
+ columnExpandRatios.put(propertyId, expandRatio);
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Gets the column expand ratio for a columnd. See
+ * {@link #setColumnExpandRatio(Object, float)}
+ *
+ * @param propertyId
+ * columns property id
+ * @return the expandRatio used to divide excess space for this column
+ */
+ public float getColumnExpandRatio(Object propertyId) {
+ final Float width = columnExpandRatios.get(propertyId);
+ if (width == null) {
+ return -1;
+ }
+ return width.floatValue();
+ }
+
+ /** Gets the pixel width of column.
+ *
+ * @param propertyId
+ * the property id
+ * @return width of column or -1 when value not set
+ */
+ public int getColumnWidth(Object propertyId) {
+ if (propertyId == null) {
+ // Since propertyId is null, this is the row header. Use the magic
+ // id to retrieve the width of the row header.
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+ final Integer width = columnWidths.get(propertyId);
+ if (width == null) {
+ return -1;
+ }
+ return width.intValue();
+ }
+
+ /**
+ * Gets the page length.
+ *
+ * <p>
+ * Setting page length 0 disables paging.
+ *
+ *
+ * @return the Length of one page.
+ */
+ public int getPageLength() {
+ return pageLength;
+ }
+
+ /**
+ * Sets the page length.
+ *
+ * <p>
+ * Setting page length 0 disables paging. The page length defaults to 15.
+ *
+ *
+ * <p>
+ * If CustomTable has width set ({@link #setColumnWidth(Object, int)} ) the client
+ * side may update the page length automatically the correct value.
+ *
+ *
+ * @param pageLength
+ * the length of one page.
+ */
+ public void setPageLength(int pageLength) {
+ if (pageLength >= 0 && this.pageLength != pageLength) {
+ this.pageLength = pageLength;
+ // Assures the visual refresh
+ refreshRowCache();
+ }
+ }
+
+ /**
+ * This method adjusts a possible caching mechanism of table implementation.
+ *
+ * <p>
+ * CustomTable component may fetch and render some rows outside visible
+ * area. With complex tables (for example containing layouts and
+ * components), the client side may become unresponsive. Setting the value
+ * lower, UI will become more responsive. With higher values scrolling in
+ * client will hit server less frequently.
+ *
+ * <p>
+ * The amount of cached rows will be cacheRate multiplied with pageLength (
+ * {@link #setPageLength(int)} both below and above visible area..
+ *
+ * @param cacheRate
+ * a value over 0 (fastest rendering time). Higher value will
+ * cache more rows on server (smoother scrolling). Default value
+ * is 2.
+ */
+ public void setCacheRate(double cacheRate) {
+ if (cacheRate < 0) {
+ throw new IllegalArgumentException(
+ "cacheRate cannot be less than zero");
+ }
+ if (this.cacheRate != cacheRate) {
+ this.cacheRate = cacheRate;
+ markAsDirty();
+ }
+ }
+
+ /** Gets the cache rate.
+ *
+ * @return the current cache rate value
+ * @see #setCacheRate(double)
+ */
+ public double getCacheRate() {
+ return cacheRate;
+ }
+
+ /**
+ * Getter for property currentPageFirstItem.
+ *
+ * @return the Value of property currentPageFirstItem.
+ */
+ public Object getCurrentPageFirstItemId() {
+
+ // Priorise index over id if indexes are supported
+ if (items instanceof Container.Indexed) {
+ final int index = getCurrentPageFirstItemIndex();
+ Object id = null;
+ if (index >= 0 && index < size()) {
+ id = getIdByIndex(index);
+ }
+ if (id != null && !id.equals(currentPageFirstItemId)) {
+ currentPageFirstItemId = id;
+ }
+ }
+
+ // If there is no item id at all, use the first one
+ if (currentPageFirstItemId == null) {
+ currentPageFirstItemId = firstItemId();
+ }
+
+ return currentPageFirstItemId;
+ }
+
+ /**
+ * Returns the item ID for the item represented by the index given. Assumes
+ * that the current container implements {@link Container.Indexed}.
+ *
+ * See {@link Container.Indexed#getIdByIndex(int)} for more information
+ * about the exceptions that can be thrown.
+ *
+ * @param index
+ * the index for which the item ID should be fetched
+ * @return the item ID for the given index
+ *
+ * @throws ClassCastException
+ * if container does not implement {@link Container.Indexed}
+ * @throws IndexOutOfBoundsException
+ * thrown by {@link Container.Indexed#getIdByIndex(int)} if the
+ * index is invalid
+ */
+ protected Object getIdByIndex(int index) {
+ return ((Container.Indexed) items).getIdByIndex(index);
+ }
+
+ /**
+ * Setter for property currentPageFirstItemId.
+ *
+ * @param currentPageFirstItemId
+ * the New value of property currentPageFirstItemId.
+ */
+ public void setCurrentPageFirstItemId(Object currentPageFirstItemId) {
+
+ // Gets the corresponding index
+ int index = -1;
+ if (items instanceof Container.Indexed) {
+ index = indexOfId(currentPageFirstItemId);
+ } else {
+ // If the table item container does not have index, we have to
+ // calculates the index by hand
+ Object id = firstItemId();
+ while (id != null && !id.equals(currentPageFirstItemId)) {
+ index++;
+ id = nextItemId(id);
+ }
+ if (id == null) {
+ index = -1;
+ }
+ }
+
+ // If the search for item index was successful
+ if (index >= 0) {
+ /*
+ * The table is not capable of displaying an item in the container
+ * as the first if there are not enough items following the selected
+ * item so the whole table (pagelength) is filled.
+ */
+ int maxIndex = size() - pageLength;
+ if (maxIndex < 0) {
+ maxIndex = 0;
+ }
+
+ if (index > maxIndex) {
+ // Note that we pass index, not maxIndex, letting
+ // setCurrentPageFirstItemIndex handle the situation.
+ setCurrentPageFirstItemIndex(index);
+ return;
+ }
+
+ this.currentPageFirstItemId = currentPageFirstItemId;
+ currentPageFirstItemIndex = index;
+ }
+
+ // Assures the visual refresh
+ refreshRowCache();
+
+ }
+
+ /** Index of id.
+ *
+ * @param itemId
+ * the item id
+ * @return the int
+ */
+ protected int indexOfId(Object itemId) {
+ return ((Container.Indexed) items).indexOfId(itemId);
+ }
+
+ /**
+ * Gets the icon Resource for the specified column.
+ *
+ * @param propertyId
+ * the propertyId indentifying the column.
+ * @return the icon for the specified column; null if the column has no icon
+ * set, or if the column is not visible.
+ */
+ public Resource getColumnIcon(Object propertyId) {
+ return columnIcons.get(propertyId);
+ }
+
+ /**
+ * Sets the icon Resource for the specified column.
+ * <p>
+ * Throws IllegalArgumentException if the specified column is not visible.
+ *
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @param icon
+ * the icon Resource to set.
+ */
+ public void setColumnIcon(Object propertyId, Resource icon) {
+
+ if (icon == null) {
+ columnIcons.remove(propertyId);
+ } else {
+ columnIcons.put(propertyId, icon);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Gets the header for the specified column.
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @return the header for the specified column if it has one.
+ */
+ public String getColumnHeader(Object propertyId) {
+ if (getColumnHeaderMode() == ColumnHeaderMode.HIDDEN) {
+ return null;
+ }
+
+ String header = columnHeaders.get(propertyId);
+ if ((header == null && getColumnHeaderMode() == ColumnHeaderMode.EXPLICIT_DEFAULTS_ID)
+ || getColumnHeaderMode() == ColumnHeaderMode.ID) {
+ header = propertyId.toString();
+ }
+
+ return header;
+ }
+
+ /** Sets the column header for the specified column;.
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @param header
+ * the header to set.
+ */
+ public void setColumnHeader(Object propertyId, String header) {
+
+ if (header == null) {
+ columnHeaders.remove(propertyId);
+ } else {
+ columnHeaders.put(propertyId, header);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Gets the specified column's alignment.
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @return the specified column's alignment if it as one; {@link Align#LEFT}
+ * otherwise.
+ */
+ public Align getColumnAlignment(Object propertyId) {
+ final Align a = columnAlignments.get(propertyId);
+ return a == null ? Align.LEFT : a;
+ }
+
+ /**
+ * Sets the specified column's alignment.
+ *
+ * <p>
+ * Throws IllegalArgumentException if the alignment is not one of the
+ * following: {@link Align#LEFT}, {@link Align#CENTER} or
+ * {@link Align#RIGHT}
+ *
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @param alignment
+ * the desired alignment.
+ */
+ public void setColumnAlignment(Object propertyId, Align alignment) {
+ if (alignment == null || alignment == Align.LEFT) {
+ columnAlignments.remove(propertyId);
+ } else {
+ columnAlignments.put(propertyId, alignment);
+ }
+
+ // Assures the visual refresh. No need to reset the page buffer before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+
+ /**
+ * Checks if the specified column is collapsed.
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @return true if the column is collapsed; false otherwise;
+ */
+ public boolean isColumnCollapsed(Object propertyId) {
+ return collapsedColumns != null
+ && collapsedColumns.contains(propertyId);
+ }
+
+ /**
+ * Sets whether the specified column is collapsed or not.
+ *
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @param collapsed
+ * the desired collapsedness.
+ * @throws IllegalStateException
+ * if column collapsing is not allowed
+ */
+ public void setColumnCollapsed(Object propertyId, boolean collapsed)
+ throws IllegalStateException {
+ if (!isColumnCollapsingAllowed()) {
+ throw new IllegalStateException("Column collapsing not allowed!");
+ }
+ if (collapsed && noncollapsibleColumns.contains(propertyId)) {
+ throw new IllegalStateException("The column is noncollapsible!");
+ }
+
+ if (collapsed) {
+ collapsedColumns.add(propertyId);
+ } else {
+ collapsedColumns.remove(propertyId);
+ }
+
+ // Assures the visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Checks if column collapsing is allowed.
+ *
+ * @return true if columns can be collapsed; false otherwise.
+ */
+ public boolean isColumnCollapsingAllowed() {
+ return columnCollapsingAllowed;
+ }
+
+ /**
+ * Sets whether column collapsing is allowed or not.
+ *
+ * @param collapsingAllowed
+ * specifies whether column collapsing is allowed.
+ */
+ public void setColumnCollapsingAllowed(boolean collapsingAllowed) {
+ columnCollapsingAllowed = collapsingAllowed;
+
+ if (!collapsingAllowed) {
+ collapsedColumns.clear();
+ }
+
+ // Assures the visual refresh. No need to reset the page buffer before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+
+ /**
+ * Sets whether the given column is collapsible. Note that collapsible
+ * columns can only be actually collapsed (via UI or with
+ * {@link #setColumnCollapsed(Object, boolean) setColumnCollapsed()}) if
+ * {@link #isColumnCollapsingAllowed()} is true. By default all columns are
+ * collapsible.
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @param collapsible
+ * true if the column should be collapsible, false otherwise.
+ */
+ public void setColumnCollapsible(Object propertyId, boolean collapsible) {
+ if (collapsible) {
+ noncollapsibleColumns.remove(propertyId);
+ } else {
+ noncollapsibleColumns.add(propertyId);
+ collapsedColumns.remove(propertyId);
+ }
+ refreshRowCache();
+ }
+
+ /** Checks if the given column is collapsible. Note that even if this
+ * method returns <code>true</code>, the column can only be actually
+ * collapsed (via UI or with {@link #setColumnCollapsed(Object, boolean)
+ * setColumnCollapsed()}) if {@link #isColumnCollapsingAllowed()} is also
+ * true.
+ *
+ * @param propertyId
+ * the property id
+ * @return true if the column can be collapsed; false otherwise.
+ */
+ public boolean isColumnCollapsible(Object propertyId) {
+ return !noncollapsibleColumns.contains(propertyId);
+ }
+
+ /**
+ * Checks if column reordering is allowed.
+ *
+ * @return true if columns can be reordered; false otherwise.
+ */
+ public boolean isColumnReorderingAllowed() {
+ return columnReorderingAllowed;
+ }
+
+ /**
+ * Sets whether column reordering is allowed or not.
+ *
+ * @param columnReorderingAllowed
+ * specifies whether column reordering is allowed.
+ */
+ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
+ if (columnReorderingAllowed != this.columnReorderingAllowed) {
+ this.columnReorderingAllowed = columnReorderingAllowed;
+ markAsDirty();
+ }
+ }
+
+ /** Sets the column order.
+ *
+ * @param columnOrder
+ * the new column order
+ */
+ /*
+ * Arranges visible columns according to given columnOrder. Silently ignores
+ * colimnId:s that are not visible columns, and keeps the internal order of
+ * visible columns left out of the ordering (trailing). Silently does
+ * nothing if columnReordering is not allowed.
+ */
+ private void setColumnOrder(Object[] columnOrder) {
+ if (columnOrder == null || !isColumnReorderingAllowed()) {
+ return;
+ }
+ final LinkedList<Object> newOrder = new LinkedList<Object>();
+ for (int i = 0; i < columnOrder.length; i++) {
+ if (columnOrder[i] != null
+ && visibleColumns.contains(columnOrder[i])) {
+ visibleColumns.remove(columnOrder[i]);
+ newOrder.add(columnOrder[i]);
+ }
+ }
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext();) {
+ final Object columnId = it.next();
+ if (!newOrder.contains(columnId)) {
+ newOrder.add(columnId);
+ }
+ }
+ visibleColumns = newOrder;
+
+ // Assure visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Getter for property currentPageFirstItem.
+ *
+ * @return the Value of property currentPageFirstItem.
+ */
+ public int getCurrentPageFirstItemIndex() {
+ return currentPageFirstItemIndex;
+ }
+
+ /** Sets the current page first item index.
+ *
+ * @param newIndex
+ * the new index
+ * @param needsPageBufferReset
+ * the needs page buffer reset
+ */
+ void setCurrentPageFirstItemIndex(int newIndex, boolean needsPageBufferReset) {
+
+ if (newIndex < 0) {
+ newIndex = 0;
+ }
+
+ /*
+ * minimize Container.size() calls which may be expensive. For example
+ * it may cause sql query.
+ */
+ final int size = size();
+
+ /*
+ * The table is not capable of displaying an item in the container as
+ * the first if there are not enough items following the selected item
+ * so the whole table (pagelength) is filled.
+ */
+ int maxIndex = size - pageLength;
+ if (maxIndex < 0) {
+ maxIndex = 0;
+ }
+
+ /*
+ * If the new index is on the last page we set the index to be the first
+ * item on that last page and make a note of the real index for the
+ * client side to be able to move the scroll position to the correct
+ * position.
+ */
+ int indexOnLastPage = -1;
+ if (newIndex > maxIndex) {
+ indexOnLastPage = newIndex;
+ newIndex = maxIndex;
+ }
+
+ // Refresh first item id
+ if (items instanceof Container.Indexed) {
+ try {
+ currentPageFirstItemId = getIdByIndex(newIndex);
+ } catch (final IndexOutOfBoundsException e) {
+ currentPageFirstItemId = null;
+ }
+ currentPageFirstItemIndex = newIndex;
+
+ if (needsPageBufferReset) {
+ /*
+ * The flag currentPageFirstItemIndexOnLastPage denotes a user
+ * set scrolling position on the last page via
+ * setCurrentPageFirstItemIndex() and shouldn't be changed by
+ * the table component internally changing the firstvisible item
+ * on lazy row fetching. Doing so would make the scrolling
+ * position not be updated correctly when the lazy rows are
+ * finally rendered.
+ */
+
+ boolean isLastRowPossiblyPartiallyVisible = true;
+ if (indexOnLastPage != -1) {
+ /*
+ * If the requested row was greater than maxIndex, the last
+ * row should be fully visible (See
+ * TestCurrentPageFirstItem).
+ */
+ isLastRowPossiblyPartiallyVisible = false;
+ }
+
+ int extraRows = isLastRowPossiblyPartiallyVisible ? 0 : 1;
+ currentPageFirstItemIndexOnLastPage = currentPageFirstItemIndex
+ + extraRows;
+ } else {
+ currentPageFirstItemIndexOnLastPage = -1;
+ }
+
+ } else {
+
+ // For containers not supporting indexes, we must iterate the
+ // container forwards / backwards
+ // next available item forward or backward
+
+ currentPageFirstItemId = firstItemId();
+
+ // Go forwards in the middle of the list (respect borders)
+ while (currentPageFirstItemIndex < newIndex
+ && !isLastId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex++;
+ currentPageFirstItemId = nextItemId(currentPageFirstItemId);
+ }
+
+ // If we did hit the border
+ if (isLastId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex = size - 1;
+ }
+
+ // Go backwards in the middle of the list (respect borders)
+ while (currentPageFirstItemIndex > newIndex
+ && !isFirstId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex--;
+ currentPageFirstItemId = prevItemId(currentPageFirstItemId);
+ }
+
+ // If we did hit the border
+ if (isFirstId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex = 0;
+ }
+
+ // Go forwards once more
+ while (currentPageFirstItemIndex < newIndex
+ && !isLastId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex++;
+ currentPageFirstItemId = nextItemId(currentPageFirstItemId);
+ }
+
+ // If for some reason we do hit border again, override
+ // the user index request
+ if (isLastId(currentPageFirstItemId)) {
+ newIndex = currentPageFirstItemIndex = size - 1;
+ }
+ }
+ if (needsPageBufferReset) {
+ // Assures the visual refresh
+ refreshRowCache();
+ }
+ }
+
+ /**
+ * Setter for property currentPageFirstItem.
+ *
+ * @param newIndex
+ * the New value of property currentPageFirstItem.
+ */
+ public void setCurrentPageFirstItemIndex(int newIndex) {
+ setCurrentPageFirstItemIndex(newIndex, true);
+ }
+
+ /**
+ * Getter for property selectable.
+ *
+ * <p>
+ * The table is not selectable by default.
+ *
+ *
+ * @return the Value of property selectable.
+ */
+ public boolean isSelectable() {
+ return selectable;
+ }
+
+ /**
+ * Setter for property selectable.
+ *
+ * <p>
+ * The table is not selectable by default.
+ *
+ *
+ * @param selectable
+ * the New value of property selectable.
+ */
+ public void setSelectable(boolean selectable) {
+ if (this.selectable != selectable) {
+ this.selectable = selectable;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Getter for property columnHeaderMode.
+ *
+ * @return the Value of property columnHeaderMode.
+ */
+ public ColumnHeaderMode getColumnHeaderMode() {
+ return columnHeaderMode;
+ }
+
+ /**
+ * Setter for property columnHeaderMode.
+ *
+ * @param columnHeaderMode
+ * the New value of property columnHeaderMode.
+ */
+ public void setColumnHeaderMode(ColumnHeaderMode columnHeaderMode) {
+ if (columnHeaderMode == null) {
+ throw new IllegalArgumentException(
+ "Column header mode can not be null");
+ }
+ if (columnHeaderMode != this.columnHeaderMode) {
+ this.columnHeaderMode = columnHeaderMode;
+ markAsDirty();
+ }
+
+ }
+
+ /**
+ * Refreshes the rows in the internal cache. Only if
+ * {@link #resetPageBuffer()} is called before this then all values are
+ * guaranteed to be recreated.
+ */
+ protected void refreshRenderedCells() {
+ if (!isAttached()) {
+ return;
+ }
+
+ if (!isContentRefreshesEnabled) {
+ return;
+ }
+
+ // Collects the basic facts about the table page
+ final int pagelen = getPageLength();
+ int rows, totalRows;
+ rows = totalRows = size();
+ int firstIndex = Math
+ .min(getCurrentPageFirstItemIndex(), totalRows - 1);
+ if (rows > 0 && firstIndex >= 0) {
+ rows -= firstIndex;
+ }
+ if (pagelen > 0 && pagelen < rows) {
+ rows = pagelen;
+ }
+
+ // If "to be painted next" variables are set, use them
+ if (lastToBeRenderedInClient - firstToBeRenderedInClient > 0) {
+ rows = lastToBeRenderedInClient - firstToBeRenderedInClient + 1;
+ }
+ if (firstToBeRenderedInClient >= 0) {
+ if (firstToBeRenderedInClient < totalRows) {
+ firstIndex = firstToBeRenderedInClient;
+ } else {
+ firstIndex = totalRows - 1;
+ }
+ } else {
+ // initial load
+
+ // #8805 send one extra row in the beginning in case a partial
+ // row is shown on the UI
+ if (firstIndex > 0) {
+ firstIndex = firstIndex - 1;
+ rows = rows + 1;
+ }
+ firstToBeRenderedInClient = firstIndex;
+ }
+ if (totalRows > 0) {
+ if (rows + firstIndex > totalRows) {
+ rows = totalRows - firstIndex;
+ }
+ } else {
+ rows = 0;
+ }
+
+ // Saves the results to internal buffer
+ pageBuffer = getVisibleCellsNoCache(firstIndex, rows, true);
+
+ if (rows > 0) {
+ pageBufferFirstIndex = firstIndex;
+ }
+ if (getPageLength() != 0) {
+ removeUnnecessaryRows();
+ }
+
+ setRowCacheInvalidated(true);
+ markAsDirty();
+ maybeThrowCacheUpdateExceptions();
+
+ }
+
+ /** Maybe throw cache update exceptions.
+ */
+ private void maybeThrowCacheUpdateExceptions() {
+ if (!exceptionsDuringCachePopulation.isEmpty()) {
+ Throwable[] causes = new Throwable[exceptionsDuringCachePopulation
+ .size()];
+ exceptionsDuringCachePopulation.toArray(causes);
+
+ exceptionsDuringCachePopulation.clear();
+ throw new CacheUpdateException(this,
+ "Error during CustomTable cache update.", causes);
+ }
+
+ }
+
+ /**
+ * Exception thrown when one or more exceptions occurred during updating of
+ * the CustomTable cache.
+ * <p>
+ * Contains all exceptions which occurred during the cache update. The first
+ * occurred exception is set as the cause of this exception. All occurred
+ * exceptions can be accessed using {@link #getCauses()}.
+ *
+ *
+ */
+ public static class CacheUpdateException extends RuntimeException {
+
+ /** The causes. */
+ private Throwable[] causes;
+
+ /** The table. */
+ private CustomTable table;
+
+ /** Instantiates a new cache update exception.
+ *
+ * @param table
+ * the table
+ * @param message
+ * the message
+ * @param causes
+ * the causes
+ */
+ public CacheUpdateException(CustomTable table, String message,
+ Throwable[] causes) {
+ super(maybeSupplementMessage(message, causes.length), causes[0]);
+ this.table = table;
+ this.causes = causes;
+ }
+
+ /** Maybe supplement message.
+ *
+ * @param message
+ * the message
+ * @param causeCount
+ * the cause count
+ * @return the string
+ */
+ private static String maybeSupplementMessage(String message,
+ int causeCount) {
+ if (causeCount > 1) {
+ return message + " Additional causes not shown.";
+ } else {
+ return message;
+ }
+ }
+
+ /** Returns the cause(s) for this exception.
+ *
+ * @return the exception(s) which caused this exception
+ */
+ public Throwable[] getCauses() {
+ return causes;
+ }
+
+ /** Gets the table.
+ *
+ * @return the table
+ */
+ public CustomTable getTable() {
+ return table;
+ }
+
+ }
+
+ /**
+ * Removes rows that fall outside the required cache.
+ */
+ private void removeUnnecessaryRows() {
+ int minPageBufferIndex = getMinPageBufferIndex();
+ int maxPageBufferIndex = getMaxPageBufferIndex();
+
+ int maxBufferSize = maxPageBufferIndex - minPageBufferIndex + 1;
+
+ /*
+ * Number of rows that were previously cached. This is not necessarily
+ * the same as pageLength if we do not have enough rows in the
+ * container.
+ */
+ int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length;
+
+ if (currentlyCachedRowCount <= maxBufferSize) {
+ // removal unnecessary
+ return;
+ }
+
+ /* Figure out which rows to get rid of. */
+ int firstCacheRowToRemoveInPageBuffer = -1;
+ if (minPageBufferIndex > pageBufferFirstIndex) {
+ firstCacheRowToRemoveInPageBuffer = pageBufferFirstIndex;
+ } else if (maxPageBufferIndex < pageBufferFirstIndex
+ + currentlyCachedRowCount) {
+ firstCacheRowToRemoveInPageBuffer = maxPageBufferIndex + 1;
+ }
+
+ if (firstCacheRowToRemoveInPageBuffer - pageBufferFirstIndex < currentlyCachedRowCount) {
+ /*
+ * Unregister all components that fall beyond the cache limits after
+ * inserting the new rows.
+ */
+ unregisterComponentsAndPropertiesInRows(
+ firstCacheRowToRemoveInPageBuffer, currentlyCachedRowCount
+ - firstCacheRowToRemoveInPageBuffer);
+ }
+ }
+
+ /**
+ * Requests that the CustomTable should be repainted as soon as possible.
+ *
+ * Note that a {@code CustomTable} does not necessarily repaint its contents
+ * when this method has been called. See {@link #refreshRowCache()} for
+ * forcing an update of the contents.
+ *
+ * @deprecated As of 7.0, use {@link #markAsDirty()} instead
+ */
+
+ @Deprecated
+ @Override
+ public void requestRepaint() {
+ markAsDirty();
+ }
+
+ /**
+ * Requests that the CustomTable should be repainted as soon as possible.
+ *
+ * Note that a {@code CustomTable} does not necessarily repaint its contents
+ * when this method has been called. See {@link #refreshRowCache()} for
+ * forcing an update of the contents.
+ */
+
+ @Override
+ public void markAsDirty() {
+ // Overridden only for javadoc
+ super.markAsDirty();
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.server.AbstractClientConnector#markAsDirtyRecursive()
+ */
+ @Override
+ public void markAsDirtyRecursive() {
+ super.markAsDirtyRecursive();
+
+ // Avoid sending a partial repaint (#8714)
+ refreshRowCache();
+ }
+
+ /** Removes the rows from cache and fill bottom.
+ *
+ * @param firstIndex
+ * the first index
+ * @param rows
+ * the rows
+ */
+ private void removeRowsFromCacheAndFillBottom(int firstIndex, int rows) {
+ int totalCachedRows = pageBuffer[CELL_ITEMID].length;
+ int totalRows = size();
+ int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex;
+
+ /*
+ * firstIndexInPageBuffer is the first row to be removed. "rows" rows
+ * after that should be removed. If the page buffer does not contain
+ * that many rows, we only remove the rows that actually are in the page
+ * buffer.
+ */
+ if (firstIndexInPageBuffer + rows > totalCachedRows) {
+ rows = totalCachedRows - firstIndexInPageBuffer;
+ }
+
+ /*
+ * Unregister components that will no longer be in the page buffer to
+ * make sure that no components leak.
+ */
+ unregisterComponentsAndPropertiesInRows(firstIndex, rows);
+
+ /*
+ * The number of rows that should be in the cache after this operation
+ * is done (pageBuffer currently contains the expanded items).
+ */
+ int newCachedRowCount = totalCachedRows;
+ if (newCachedRowCount + pageBufferFirstIndex > totalRows) {
+ newCachedRowCount = totalRows - pageBufferFirstIndex;
+ }
+
+ /*
+ * The index at which we should render the first row that does not come
+ * from the previous page buffer.
+ */
+ int firstAppendedRowInPageBuffer = totalCachedRows - rows;
+ int firstAppendedRow = firstAppendedRowInPageBuffer
+ + pageBufferFirstIndex;
+
+ /*
+ * Calculate the maximum number of new rows that we can add to the page
+ * buffer. Less than the rows we removed if the container does not
+ * contain that many items afterwards.
+ */
+ int maxRowsToRender = (totalRows - firstAppendedRow);
+ int rowsToAdd = rows;
+ if (rowsToAdd > maxRowsToRender) {
+ rowsToAdd = maxRowsToRender;
+ }
+
+ Object[][] cells = null;
+ if (rowsToAdd > 0) {
+ cells = getVisibleCellsNoCache(firstAppendedRow, rowsToAdd, false);
+ }
+ /*
+ * Create the new cache buffer by copying the first rows from the old
+ * buffer, moving the following rows upwards and appending more rows if
+ * applicable.
+ */
+ Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount];
+
+ for (int i = 0; i < pageBuffer.length; i++) {
+ for (int row = 0; row < firstIndexInPageBuffer; row++) {
+ // Copy the first rows
+ newPageBuffer[i][row] = pageBuffer[i][row];
+ }
+ for (int row = firstIndexInPageBuffer; row < firstAppendedRowInPageBuffer; row++) {
+ // Move the rows that were after the expanded rows
+ newPageBuffer[i][row] = pageBuffer[i][row + rows];
+ }
+ for (int row = firstAppendedRowInPageBuffer; row < newCachedRowCount; row++) {
+ // Add the newly rendered rows. Only used if rowsToAdd > 0
+ // (cells != null)
+ newPageBuffer[i][row] = cells[i][row
+ - firstAppendedRowInPageBuffer];
+ }
+ }
+ pageBuffer = newPageBuffer;
+ }
+
+ /** Gets the visible cells update cache rows.
+ *
+ * @param firstIndex
+ * the first index
+ * @param rows
+ * the rows
+ * @return the visible cells update cache rows
+ */
+ private Object[][] getVisibleCellsUpdateCacheRows(int firstIndex, int rows) {
+ Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false);
+ int cacheIx = firstIndex - pageBufferFirstIndex;
+ // update the new rows in the cache.
+ int totalCachedRows = pageBuffer[CELL_ITEMID].length;
+ int end = Math.min(cacheIx + rows, totalCachedRows);
+ for (int ix = cacheIx; ix < end; ix++) {
+ for (int i = 0; i < pageBuffer.length; i++) {
+ pageBuffer[i][ix] = cells[i][ix - cacheIx];
+ }
+ }
+ return cells;
+ }
+
+ /** Gets the visible cells insert into cache.
+ *
+ * @param firstIndex
+ * The position where new rows should be inserted
+ * @param rows
+ * The maximum number of rows that should be inserted at position
+ * firstIndex. Less rows will be inserted if the page buffer is
+ * too small.
+ * @return the visible cells insert into cache
+ */
+ private Object[][] getVisibleCellsInsertIntoCache(int firstIndex, int rows) {
+ getLogger()
+ .log(Level.FINEST,
+ "Insert {0} rows at index {1} to existing page buffer requested",
+ new Object[] { rows, firstIndex });
+
+ int minPageBufferIndex = getMinPageBufferIndex();
+ int maxPageBufferIndex = getMaxPageBufferIndex();
+
+ int maxBufferSize = maxPageBufferIndex - minPageBufferIndex + 1;
+
+ if (getPageLength() == 0) {
+ // If pageLength == 0 then all rows should be rendered
+ maxBufferSize = pageBuffer[CELL_ITEMID].length + rows;
+ }
+ /*
+ * Number of rows that were previously cached. This is not necessarily
+ * the same as maxBufferSize.
+ */
+ int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length;
+
+ /* If rows > size available in page buffer */
+ if (firstIndex + rows - 1 > maxPageBufferIndex) {
+ rows = maxPageBufferIndex - firstIndex + 1;
+ }
+
+ /*
+ * "rows" rows will be inserted at firstIndex. Find out how many old
+ * rows fall outside the new buffer so we can unregister components in
+ * the cache.
+ */
+
+ /*
+ * if there are rows before the new pageBuffer limits they must be
+ * removed
+ */
+ int lastCacheRowToRemove = minPageBufferIndex - 1;
+ int rowsFromBeginning = lastCacheRowToRemove - pageBufferFirstIndex + 1;
+ if (lastCacheRowToRemove >= pageBufferFirstIndex) {
+ unregisterComponentsAndPropertiesInRows(pageBufferFirstIndex,
+ rowsFromBeginning);
+ } else {
+ rowsFromBeginning = 0;
+ }
+
+ /*
+ * the rows that fall outside of the new pageBuffer limits after the new
+ * rows are inserted must also be removed
+ */
+ int firstCacheRowToRemove = firstIndex;
+ /*
+ * IF there is space remaining in the buffer after the rows have been
+ * inserted, we can keep more rows.
+ */
+ int numberOfOldRowsAfterInsertedRows = Math.min(pageBufferFirstIndex
+ + currentlyCachedRowCount + rows, maxPageBufferIndex + 1)
+ - (firstIndex + rows - 1);
+ if (numberOfOldRowsAfterInsertedRows > 0) {
+ firstCacheRowToRemove += numberOfOldRowsAfterInsertedRows;
+ }
+ int rowsFromAfter = currentlyCachedRowCount
+ - (firstCacheRowToRemove - pageBufferFirstIndex);
+
+ if (rowsFromAfter > 0) {
+ /*
+ * Unregister all components that fall beyond the cache limits after
+ * inserting the new rows.
+ */
+ unregisterComponentsAndPropertiesInRows(firstCacheRowToRemove,
+ rowsFromAfter);
+ }
+
+ // Calculate the new cache size
+ int newCachedRowCount = maxBufferSize;
+ if (pageBufferFirstIndex + currentlyCachedRowCount + rows - 1 < maxPageBufferIndex) {
+ // there aren't enough rows to fill the whole potential -> use what
+ // there is
+ newCachedRowCount -= maxPageBufferIndex
+ - (pageBufferFirstIndex + currentlyCachedRowCount + rows - 1);
+ } else if (minPageBufferIndex < pageBufferFirstIndex) {
+ newCachedRowCount -= pageBufferFirstIndex - minPageBufferIndex;
+ }
+ /* calculate the internal location of the new rows within the new cache */
+ int firstIndexInNewPageBuffer = firstIndex - pageBufferFirstIndex
+ - rowsFromBeginning;
+
+ /* Paint the new rows into a separate buffer */
+ Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false);
+
+ /*
+ * Create the new cache buffer and fill it with the data from the old
+ * buffer as well as the inserted rows.
+ */
+ Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount];
+
+ for (int i = 0; i < pageBuffer.length; i++) {
+ for (int row = 0; row < firstIndexInNewPageBuffer; row++) {
+ // Copy the first rows
+ newPageBuffer[i][row] = pageBuffer[i][rowsFromBeginning + row];
+ }
+ for (int row = firstIndexInNewPageBuffer; row < firstIndexInNewPageBuffer
+ + rows; row++) {
+ // Copy the newly created rows
+ newPageBuffer[i][row] = cells[i][row
+ - firstIndexInNewPageBuffer];
+ }
+ for (int row = firstIndexInNewPageBuffer + rows; row < newCachedRowCount; row++) {
+ // Move the old rows down below the newly inserted rows
+ newPageBuffer[i][row] = pageBuffer[i][rowsFromBeginning + row
+ - rows];
+ }
+ }
+ pageBuffer = newPageBuffer;
+ pageBufferFirstIndex = Math.max(pageBufferFirstIndex
+ + rowsFromBeginning, minPageBufferIndex);
+ if (getLogger().isLoggable(Level.FINEST)) {
+ getLogger().log(
+ Level.FINEST,
+ "Page Buffer now contains {0} rows ({1}-{2})",
+ new Object[] {
+ pageBuffer[CELL_ITEMID].length,
+ pageBufferFirstIndex,
+ (pageBufferFirstIndex
+ + pageBuffer[CELL_ITEMID].length - 1) });
+ }
+ return cells;
+ }
+
+ /** Gets the max page buffer index.
+ *
+ * @return the max page buffer index
+ */
+ private int getMaxPageBufferIndex() {
+ int total = size();
+ if (getPageLength() == 0) {
+ // everything is shown at once, no caching
+ return total - 1;
+ }
+ // Page buffer must not become larger than pageLength*cacheRate after
+ // the current page
+ int maxPageBufferIndex = getCurrentPageFirstItemIndex()
+ + (int) (getPageLength() * (1 + getCacheRate()));
+ if (shouldHideNullSelectionItem()) {
+ --total;
+ }
+ if (maxPageBufferIndex >= total) {
+ maxPageBufferIndex = total - 1;
+ }
+ return maxPageBufferIndex;
+ }
+
+ /** Gets the min page buffer index.
+ *
+ * @return the min page buffer index
+ */
+ private int getMinPageBufferIndex() {
+ if (getPageLength() == 0) {
+ // everything is shown at once, no caching
+ return 0;
+ }
+ // Page buffer must not become larger than pageLength*cacheRate before
+ // the current page
+ int minPageBufferIndex = getCurrentPageFirstItemIndex()
+ - (int) (getPageLength() * getCacheRate());
+ if (minPageBufferIndex < 0) {
+ minPageBufferIndex = 0;
+ }
+ return minPageBufferIndex;
+ }
+
+ /** Render rows with index "firstIndex" to "firstIndex+rows-1" to a new
+ * buffer.
+ *
+ * Reuses values from the current page buffer if the rows are found there.
+ *
+ * @param firstIndex
+ * the first index
+ * @param rows
+ * the rows
+ * @param replaceListeners
+ * the replace listeners
+ * @return the visible cells no cache
+ */
+ private Object[][] getVisibleCellsNoCache(int firstIndex, int rows,
+ boolean replaceListeners) {
+ if (getLogger().isLoggable(Level.FINEST)) {
+ getLogger().log(Level.FINEST,
+ "Render visible cells for rows {0}-{1}",
+ new Object[] { firstIndex, (firstIndex + rows - 1) });
+ }
+ final Object[] colids = getVisibleColumns();
+ final int cols = colids.length;
+
+ HashSet<Property<?>> oldListenedProperties = listenedProperties;
+ HashSet<Component> oldVisibleComponents = visibleComponents;
+
+ if (replaceListeners) {
+ // initialize the listener collections, this should only be done if
+ // the entire cache is refreshed (through refreshRenderedCells)
+ listenedProperties = new HashSet<Property<?>>();
+ visibleComponents = new HashSet<Component>();
+ }
+
+ Object[][] cells = new Object[cols + CELL_FIRSTCOL][rows];
+ if (rows == 0) {
+ unregisterPropertiesAndComponents(oldListenedProperties,
+ oldVisibleComponents);
+ return cells;
+ }
+
+ final RowHeaderMode headmode = getRowHeaderMode();
+ final boolean[] iscomponent = new boolean[cols];
+ for (int i = 0; i < cols; i++) {
+ iscomponent[i] = columnGenerators.containsKey(colids[i])
+ || Component.class.isAssignableFrom(getType(colids[i]));
+ }
+ int firstIndexNotInCache;
+ if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) {
+ firstIndexNotInCache = pageBufferFirstIndex
+ + pageBuffer[CELL_ITEMID].length;
+ } else {
+ firstIndexNotInCache = -1;
+ }
+
+ // Creates the page contents
+ int filledRows = 0;
+ if (items instanceof Container.Indexed) {
+ // more efficient implementation for containers supporting access by
+ // index
+
+ List<?> itemIds = getItemIds(firstIndex, rows);
+ for (int i = 0; i < rows && i < itemIds.size(); i++) {
+ Object id = itemIds.get(i);
+ // Start by parsing the values, id should already be set
+ parseItemIdToCells(cells, id, i, firstIndex, headmode, cols,
+ colids, firstIndexNotInCache, iscomponent,
+ oldListenedProperties);
+
+ filledRows++;
+ }
+ } else {
+ // slow back-up implementation for cases where the container does
+ // not support access by index
+
+ // Gets the first item id
+ Object id = firstItemId();
+ for (int i = 0; i < firstIndex; i++) {
+ id = nextItemId(id);
+ }
+ for (int i = 0; i < rows && id != null; i++) {
+ // Start by parsing the values, id should already be set
+ parseItemIdToCells(cells, id, i, firstIndex, headmode, cols,
+ colids, firstIndexNotInCache, iscomponent,
+ oldListenedProperties);
+
+ // Gets the next item id for non indexed container
+ id = nextItemId(id);
+
+ filledRows++;
+ }
+ }
+
+ // Assures that all the rows of the cell-buffer are valid
+ if (filledRows != cells[0].length) {
+ final Object[][] temp = new Object[cells.length][filledRows];
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < filledRows; j++) {
+ temp[i][j] = cells[i][j];
+ }
+ }
+ cells = temp;
+ }
+
+ unregisterPropertiesAndComponents(oldListenedProperties,
+ oldVisibleComponents);
+
+ return cells;
+ }
+
+ /** Gets the item ids.
+ *
+ * @param firstIndex
+ * the first index
+ * @param rows
+ * the rows
+ * @return the item ids
+ */
+ protected List<Object> getItemIds(int firstIndex, int rows) {
+ return (List<Object>) ((Container.Indexed) items).getItemIds(
+ firstIndex, rows);
+ }
+
+ /** Update a cache array for a row, register any relevant listeners etc.
+ *
+ * This is an internal method extracted from
+ * {@link #getVisibleCellsNoCache(int, int, boolean)} and should be removed
+ * when the CustomTable is rewritten.
+ *
+ * @param cells
+ * the cells
+ * @param id
+ * the id
+ * @param i
+ * the i
+ * @param firstIndex
+ * the first index
+ * @param headmode
+ * the headmode
+ * @param cols
+ * the cols
+ * @param colids
+ * the colids
+ * @param firstIndexNotInCache
+ * the first index not in cache
+ * @param iscomponent
+ * the iscomponent
+ * @param oldListenedProperties
+ * the old listened properties
+ */
+ private void parseItemIdToCells(Object[][] cells, Object id, int i,
+ int firstIndex, RowHeaderMode headmode, int cols, Object[] colids,
+ int firstIndexNotInCache, boolean[] iscomponent,
+ HashSet<Property<?>> oldListenedProperties) {
+
+ cells[CELL_ITEMID][i] = id;
+ cells[CELL_KEY][i] = itemIdMapper.key(id);
+ if (headmode != ROW_HEADER_MODE_HIDDEN) {
+ switch (headmode) {
+ case INDEX:
+ cells[CELL_HEADER][i] = String.valueOf(i + firstIndex + 1);
+ break;
+ default:
+ try {
+ cells[CELL_HEADER][i] = getItemCaption(id);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ cells[CELL_HEADER][i] = "";
+ }
+ }
+ try {
+ cells[CELL_ICON][i] = getItemIcon(id);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ cells[CELL_ICON][i] = null;
+ }
+ }
+
+ GeneratedRow generatedRow = rowGenerator != null ? rowGenerator
+ .generateRow(this, id) : null;
+ cells[CELL_GENERATED_ROW][i] = generatedRow;
+
+ for (int j = 0; j < cols; j++) {
+ if (isColumnCollapsed(colids[j])) {
+ continue;
+ }
+ Property<?> p = null;
+ Object value = "";
+ boolean isGeneratedRow = generatedRow != null;
+ boolean isGeneratedColumn = columnGenerators.containsKey(colids[j]);
+ boolean isGenerated = isGeneratedRow || isGeneratedColumn;
+
+ if (!isGenerated) {
+ try {
+ p = getContainerProperty(id, colids[j]);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ }
+
+ if (isGeneratedRow) {
+ if (generatedRow.isSpanColumns() && j > 0) {
+ value = null;
+ } else if (generatedRow.isSpanColumns() && j == 0
+ && generatedRow.getValue() instanceof Component) {
+ value = generatedRow.getValue();
+ } else if (generatedRow.getText().length > j) {
+ value = generatedRow.getText()[j];
+ }
+ } else {
+ // check if current pageBuffer already has row
+ int index = firstIndex + i;
+ if (p != null || isGenerated) {
+ int indexInOldBuffer = index - pageBufferFirstIndex;
+ if (index < firstIndexNotInCache
+ && index >= pageBufferFirstIndex
+ && pageBuffer[CELL_GENERATED_ROW][indexInOldBuffer] == null
+ && id.equals(pageBuffer[CELL_ITEMID][indexInOldBuffer])) {
+ // we already have data in our cache,
+ // recycle it instead of fetching it via
+ // getValue/getPropertyValue
+ value = pageBuffer[CELL_FIRSTCOL + j][indexInOldBuffer];
+ if (!isGeneratedColumn && iscomponent[j]
+ || !(value instanceof Component)) {
+ listenProperty(p, oldListenedProperties);
+ }
+ } else {
+ if (isGeneratedColumn) {
+ ColumnGenerator cg = columnGenerators
+ .get(colids[j]);
+ try {
+ value = cg.generateCell(this, id, colids[j]);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ if (value != null && !(value instanceof Component)
+ && !(value instanceof String)) {
+ // Avoid errors if a generator returns
+ // something
+ // other than a Component or a String
+ value = value.toString();
+ }
+ } else if (iscomponent[j]) {
+ try {
+ value = p.getValue();
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ listenProperty(p, oldListenedProperties);
+ } else if (p != null) {
+ try {
+ value = getPropertyValue(id, colids[j], p);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ /*
+ * If returned value is Component (via fieldfactory
+ * or overridden getPropertyValue) we expect it to
+ * listen property value changes. Otherwise if
+ * property emits value change events, table will
+ * start to listen them and refresh content when
+ * needed.
+ */
+ if (!(value instanceof Component)) {
+ listenProperty(p, oldListenedProperties);
+ }
+ } else {
+ try {
+ value = getPropertyValue(id, colids[j], null);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ }
+ }
+ }
+ }
+
+ if (value instanceof Component) {
+ registerComponent((Component) value);
+ }
+ cells[CELL_FIRSTCOL + j][i] = value;
+ }
+ }
+
+ /** Register component.
+ *
+ * @param component
+ * the component
+ */
+ protected void registerComponent(Component component) {
+ getLogger().log(
+ Level.FINEST,
+ "Registered {0}: {1}",
+ new Object[] { component.getClass().getSimpleName(),
+ component.getCaption() });
+ if (!equals(component.getParent())) {
+ component.setParent(this);
+ }
+ visibleComponents.add(component);
+ }
+
+ /** Listen property.
+ *
+ * @param p
+ * the p
+ * @param oldListenedProperties
+ * the old listened properties
+ */
+ private void listenProperty(Property<?> p,
+ HashSet<Property<?>> oldListenedProperties) {
+ if (p instanceof Property.ValueChangeNotifier) {
+ if (oldListenedProperties == null
+ || !oldListenedProperties.contains(p)) {
+ ((Property.ValueChangeNotifier) p).addListener(this);
+ }
+ /*
+ * register listened properties, so we can do proper cleanup to free
+ * memory. Essential if table has loads of data and it is used for a
+ * long time.
+ */
+ listenedProperties.add(p);
+
+ }
+ }
+
+ /** Unregister components and properties in rows.
+ *
+ * @param firstIx
+ * Index of the first row to process. Global index, not relative
+ * to page buffer.
+ * @param count
+ * the count
+ */
+ private void unregisterComponentsAndPropertiesInRows(int firstIx, int count) {
+ if (getLogger().isLoggable(Level.FINEST)) {
+ getLogger().log(Level.FINEST,
+ "Unregistering components in rows {0}-{1}",
+ new Object[] { firstIx, (firstIx + count - 1) });
+ }
+ Object[] colids = getVisibleColumns();
+ if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) {
+ int bufSize = pageBuffer[CELL_ITEMID].length;
+ int ix = firstIx - pageBufferFirstIndex;
+ ix = ix < 0 ? 0 : ix;
+ if (ix < bufSize) {
+ count = count > bufSize - ix ? bufSize - ix : count;
+ for (int i = 0; i < count; i++) {
+ for (int c = 0; c < colids.length; c++) {
+ Object cellVal = pageBuffer[CELL_FIRSTCOL + c][i + ix];
+ if (cellVal instanceof Component
+ && visibleComponents.contains(cellVal)) {
+ visibleComponents.remove(cellVal);
+ unregisterComponent((Component) cellVal);
+ } else {
+ Property<?> p = getContainerProperty(
+ pageBuffer[CELL_ITEMID][i + ix], colids[c]);
+ if (p instanceof ValueChangeNotifier
+ && listenedProperties.contains(p)) {
+ listenedProperties.remove(p);
+ ((ValueChangeNotifier) p).removeListener(this);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method to remove listeners and maintain correct component
+ * hierarchy. Detaches properties and components if those are no more
+ * rendered in client.
+ *
+ * @param oldListenedProperties
+ * set of properties that where listened in last render
+ * @param oldVisibleComponents
+ * set of components that where attached in last render
+ */
+ private void unregisterPropertiesAndComponents(
+ HashSet<Property<?>> oldListenedProperties,
+ HashSet<Component> oldVisibleComponents) {
+ if (oldVisibleComponents != null) {
+ for (final Iterator<Component> i = oldVisibleComponents.iterator(); i
+ .hasNext();) {
+ Component c = i.next();
+ if (!visibleComponents.contains(c)) {
+ unregisterComponent(c);
+ }
+ }
+ }
+
+ if (oldListenedProperties != null) {
+ for (final Iterator<Property<?>> i = oldListenedProperties
+ .iterator(); i.hasNext();) {
+ Property.ValueChangeNotifier o = (ValueChangeNotifier) i.next();
+ if (!listenedProperties.contains(o)) {
+ o.removeListener(this);
+ }
+ }
+ }
+ }
+
+ /** This method cleans up a Component that has been generated when
+ * CustomTable is in editable mode. The component needs to be detached from
+ * its parent and if it is a field, it needs to be detached from its
+ * property data source in order to allow garbage collection to take care of
+ * removing the unused component from memory.
+ *
+ * Override this method and getPropertyValue(Object, Object, Property) with
+ * custom logic if you need to deal with buffered fields.
+ *
+ * @param component
+ * the component
+ * @see #getPropertyValue(Object, Object, Property)
+ */
+ protected void unregisterComponent(Component component) {
+ getLogger().log(
+ Level.FINEST,
+ "Unregistered {0}: {1}",
+ new Object[] { component.getClass().getSimpleName(),
+ component.getCaption() });
+ component.setParent(null);
+ /*
+ * Also remove property data sources to unregister listeners keeping the
+ * fields in memory.
+ */
+ if (component instanceof Field) {
+ Field<?> field = (Field<?>) component;
+ Property<?> associatedProperty = associatedProperties
+ .remove(component);
+ if (associatedProperty != null
+ && field.getPropertyDataSource() == associatedProperty) {
+ // Remove the property data source only if it's the one we
+ // added in getPropertyValue
+ field.setPropertyDataSource(null);
+ }
+ }
+ }
+
+ /**
+ * Sets the row header mode.
+ * <p>
+ * The mode can be one of the following ones:
+ * <ul>
+ * <li>{@link #ROW_HEADER_MODE_HIDDEN}: The row captions are hidden.</li>
+ * <li>{@link #ROW_HEADER_MODE_ID}: Items Id-objects <code>toString()</code>
+ * is used as row caption.
+ * <li>{@link #ROW_HEADER_MODE_ITEM}: Item-objects <code>toString()</code>
+ * is used as row caption.
+ * <li>{@link #ROW_HEADER_MODE_PROPERTY}: Property set with
+ * {@link #setItemCaptionPropertyId(Object)} is used as row header.
+ * <li>{@link #ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID}: Items Id-objects
+ * <code>toString()</code> is used as row header. If caption is explicitly
+ * specified, it overrides the id-caption.
+ * <li>{@link #ROW_HEADER_MODE_EXPLICIT}: The row headers must be explicitly
+ * specified.</li>
+ * <li>{@link #ROW_HEADER_MODE_INDEX}: The index of the item is used as row
+ * caption. The index mode can only be used with the containers implementing
+ * <code>Container.Indexed</code> interface.</li>
+ * </ul>
+ * The default value is {@link #ROW_HEADER_MODE_HIDDEN}
+ *
+ *
+ * @param mode
+ * the One of the modes listed above.
+ */
+ public void setRowHeaderMode(RowHeaderMode mode) {
+ if (mode != null) {
+ rowHeaderMode = mode;
+ if (mode != RowHeaderMode.HIDDEN) {
+ setItemCaptionMode(mode.getItemCaptionMode());
+ }
+ // Assures the visual refresh. No need to reset the page buffer
+ // before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+ }
+
+ /**
+ * Gets the row header mode.
+ *
+ * @return the Row header mode.
+ * @see #setRowHeaderMode
+ */
+ public RowHeaderMode getRowHeaderMode() {
+ return rowHeaderMode;
+ }
+
+ /** Adds the new row to table and fill the visible cells (except
+ * generated columns) with given values.
+ *
+ * @param cells
+ * the Object array that is used for filling the visible cells
+ * new row. The types must be settable to visible column property
+ * types.
+ * @param itemId
+ * the Id the new row. If null, a new id is automatically
+ * assigned. If given, the table cant already have a item with
+ * given id.
+ * @return Returns item id for the new row. Returns null if operation fails.
+ * @throws UnsupportedOperationException
+ * the unsupported operation exception
+ */
+ public Object addItem(Object[] cells, Object itemId)
+ throws UnsupportedOperationException {
+
+ // remove generated columns from the list of columns being assigned
+ final LinkedList<Object> availableCols = new LinkedList<Object>();
+ for (Iterator<Object> it = visibleColumns.iterator(); it.hasNext();) {
+ Object id = it.next();
+ if (!columnGenerators.containsKey(id)) {
+ availableCols.add(id);
+ }
+ }
+ // Checks that a correct number of cells are given
+ if (cells.length != availableCols.size()) {
+ return null;
+ }
+
+ // Creates new item
+ Item item;
+ if (itemId == null) {
+ itemId = items.addItem();
+ if (itemId == null) {
+ return null;
+ }
+ item = items.getItem(itemId);
+ } else {
+ item = items.addItem(itemId);
+ }
+ if (item == null) {
+ return null;
+ }
+
+ // Fills the item properties
+ for (int i = 0; i < availableCols.size(); i++) {
+ item.getItemProperty(availableCols.get(i)).setValue(cells[i]);
+ }
+
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+
+ return itemId;
+ }
+
+ /**
+ * Discards and recreates the internal row cache. Call this if you make
+ * changes that affect the rows but the information about the changes are
+ * not automatically propagated to the CustomTable.
+ * <p>
+ * Do not call this e.g. if you have updated the data model through a
+ * Property. These types of changes are automatically propagated to the
+ * CustomTable.
+ * <p>
+ * A typical case when this is needed is if you update a generator (e.g.
+ * CellStyleGenerator) and want to ensure that the rows are redrawn with new
+ * styles.
+ * <p>
+ * <i>Note that calling this method is not cheap so avoid calling it
+ * unnecessarily.</i>
+ *
+ * @since 6.7.2
+ */
+ public void refreshRowCache() {
+ resetPageBuffer();
+ refreshRenderedCells();
+ }
+
+ /**
+ * Sets the Container that serves as the data source of the viewer. As a
+ * side-effect the table's selection value is set to null as the old
+ * selection might not exist in new Container.<br>
+ * <br>
+ * All rows and columns are generated as visible using this method. If the
+ * new container contains properties that are not meant to be shown you
+ * should use
+ * {@link CustomTable#setContainerDataSource(Container, Collection)}
+ * instead, especially if the table is editable.
+ * <p>
+ * Keeps propertyValueConverters if the corresponding id exists in the new
+ * data source and is of a compatible type.
+ *
+ *
+ * @param newDataSource
+ * the new data source.
+ */
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ if (newDataSource == null) {
+ newDataSource = new IndexedContainer();
+ }
+ Collection<Object> generated;
+ if (columnGenerators != null) {
+ generated = columnGenerators.keySet();
+ } else {
+ generated = Collections.emptyList();
+ }
+ List<Object> visibleIds = new ArrayList<Object>();
+ if (generated.isEmpty()) {
+ visibleIds.addAll(newDataSource.getContainerPropertyIds());
+ } else {
+ for (Object id : newDataSource.getContainerPropertyIds()) {
+ // don't add duplicates
+ if (!generated.contains(id)) {
+ visibleIds.add(id);
+ }
+ }
+ // generated columns to the end
+ visibleIds.addAll(generated);
+ }
+ setContainerDataSource(newDataSource, visibleIds);
+ }
+
+ /** Sets the container data source and the columns that will be visible.
+ * Columns are shown in the collection's iteration order.
+ * <p>
+ * Keeps propertyValueConverters if the corresponding id exists in the new
+ * data source and is of a compatible type.
+ *
+ * @param newDataSource
+ * the new data source.
+ * @param visibleIds
+ * IDs of the visible columns
+ * @see CustomTable#setContainerDataSource(Container)
+ * @see CustomTable#setVisibleColumns(Object[])
+ * @see CustomTable#setConverter(Object, Converter)
+ */
+ public void setContainerDataSource(Container newDataSource,
+ Collection<?> visibleIds) {
+
+ disableContentRefreshing();
+
+ if (newDataSource == null) {
+ newDataSource = new IndexedContainer();
+ }
+ if (visibleIds == null) {
+ visibleIds = new ArrayList<Object>();
+ }
+
+ // Retain propertyValueConverters if their corresponding ids are
+ // properties of the new
+ // data source and are of a compatible type
+ if (propertyValueConverters != null) {
+ Collection<?> newPropertyIds = newDataSource
+ .getContainerPropertyIds();
+ LinkedList<Object> retainableValueConverters = new LinkedList<Object>();
+ for (Object propertyId : newPropertyIds) {
+ Converter<String, ?> converter = getConverter(propertyId);
+ if (converter != null) {
+ if (typeIsCompatible(converter.getModelType(),
+ newDataSource.getType(propertyId))) {
+ retainableValueConverters.add(propertyId);
+ }
+ }
+ }
+ propertyValueConverters.keySet().retainAll(
+ retainableValueConverters);
+ }
+
+ // Assures that the data source is ordered by making unordered
+ // containers ordered by wrapping them
+ if (newDataSource instanceof Container.Ordered) {
+ super.setContainerDataSource(newDataSource);
+ } else {
+ super.setContainerDataSource(new ContainerOrderedWrapper(
+ newDataSource));
+ }
+
+ // Resets page position
+ currentPageFirstItemId = null;
+ currentPageFirstItemIndex = 0;
+
+ // Resets column properties
+ if (collapsedColumns != null) {
+ collapsedColumns.clear();
+ }
+
+ // don't add the same id twice
+ Collection<Object> col = new LinkedList<Object>();
+ for (Iterator<?> it = visibleIds.iterator(); it.hasNext();) {
+ Object id = it.next();
+ if (!col.contains(id)) {
+ col.add(id);
+ }
+ }
+
+ setVisibleColumns(col.toArray());
+
+ // Assure visual refresh
+ resetPageBuffer();
+
+ enableContentRefreshing(true);
+ }
+
+ /** Checks if class b can be safely assigned to class a.
+ *
+ * @param a
+ * the a
+ * @param b
+ * the b
+ * @return true, if successful
+ */
+ private boolean typeIsCompatible(Class<?> a, Class<?> b) {
+ // TODO Implement this check properly
+ // Basically we need to do a a.isAssignableFrom(b)
+ // with special considerations for primitive types.
+ return true;
+ }
+
+ /** Gets items ids from a range of key values.
+ *
+ * @param itemId
+ * the item id
+ * @param length
+ * the length
+ * @return the item ids in range
+ */
+ private LinkedHashSet<Object> getItemIdsInRange(Object itemId,
+ final int length) {
+ LinkedHashSet<Object> ids = new LinkedHashSet<Object>();
+ for (int i = 0; i < length; i++) {
+ assert itemId != null; // should not be null unless client-server
+ // are out of sync
+ ids.add(itemId);
+ itemId = nextItemId(itemId);
+ }
+ return ids;
+ }
+
+ /** Handles selection if selection is a multiselection.
+ *
+ * @param variables
+ * The variables
+ */
+ private void handleSelectedItems(Map<String, Object> variables) {
+ final String[] ka = (String[]) variables.get("selected");
+ final String[] ranges = (String[]) variables.get("selectedRanges");
+
+ Set<Object> renderedButNotSelectedItemIds = getCurrentlyRenderedItemIds();
+
+ @SuppressWarnings("unchecked")
+ HashSet<Object> newValue = new LinkedHashSet<Object>(
+ (Collection<Object>) getValue());
+
+ if (variables.containsKey("clearSelections")) {
+ // the client side has instructed to swipe all previous selections
+ newValue.clear();
+ }
+
+ /*
+ * Then add (possibly some of them back) rows that are currently
+ * selected on the client side (the ones that the client side is aware
+ * of).
+ */
+ for (int i = 0; i < ka.length; i++) {
+ // key to id
+ final Object id = itemIdMapper.get(ka[i]);
+ if (!isNullSelectionAllowed()
+ && (id == null || id == getNullSelectionItemId())) {
+ // skip empty selection if nullselection is not allowed
+ markAsDirty();
+ } else if (id != null && containsId(id)) {
+ newValue.add(id);
+ renderedButNotSelectedItemIds.remove(id);
+ }
+ }
+
+ /* Add range items aka shift clicked multiselection areas */
+ if (ranges != null) {
+ for (String range : ranges) {
+ String[] split = range.split("-");
+ Object startItemId = itemIdMapper.get(split[0]);
+ int length = Integer.valueOf(split[1]);
+ LinkedHashSet<Object> itemIdsInRange = getItemIdsInRange(
+ startItemId, length);
+ newValue.addAll(itemIdsInRange);
+ renderedButNotSelectedItemIds.removeAll(itemIdsInRange);
+ }
+ }
+ /*
+ * finally clear all currently rendered rows (the ones that the client
+ * side counterpart is aware of) that the client didn't send as selected
+ */
+ newValue.removeAll(renderedButNotSelectedItemIds);
+
+ if (!isNullSelectionAllowed() && newValue.isEmpty()) {
+ // empty selection not allowed, keep old value
+ markAsDirty();
+ return;
+ }
+
+ setValue(newValue, true);
+
+ }
+
+ /** Gets the currently rendered item ids.
+ *
+ * @return the currently rendered item ids
+ */
+ private Set<Object> getCurrentlyRenderedItemIds() {
+ HashSet<Object> ids = new HashSet<Object>();
+ if (pageBuffer != null) {
+ for (int i = 0; i < pageBuffer[CELL_ITEMID].length; i++) {
+ ids.add(pageBuffer[CELL_ITEMID][i]);
+ }
+ }
+ return ids;
+ }
+
+ /* Component basics */
+
+ /** Invoked when the value of a variable has changed.
+ *
+ * @param source
+ * the source
+ * @param variables
+ * the variables
+ * @see com.vaadin.ui.Select#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ boolean clientNeedsContentRefresh = false;
+
+ handleClickEvent(variables);
+
+ handleColumnResizeEvent(variables);
+
+ handleColumnWidthUpdates(variables);
+
+ disableContentRefreshing();
+
+ if (!isSelectable() && variables.containsKey("selected")) {
+ // Not-selectable is a special case, AbstractSelect does not support
+ // TODO could be optimized.
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ /*
+ * The AbstractSelect cannot handle the multiselection properly, instead
+ * we handle it ourself
+ */
+ else if (isSelectable() && isMultiSelect()
+ && variables.containsKey("selected")
+ && multiSelectMode == MultiSelectMode.DEFAULT) {
+ handleSelectedItems(variables);
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ super.changeVariables(source, variables);
+
+ // Client might update the pagelength if CustomTable height is fixed
+ if (variables.containsKey("pagelength")) {
+ // Sets pageLength directly to avoid repaint that setter causes
+ pageLength = (Integer) variables.get("pagelength");
+ }
+
+ // Page start index
+ if (variables.containsKey("firstvisible")) {
+ final Integer value = (Integer) variables.get("firstvisible");
+ if (value != null) {
+ setCurrentPageFirstItemIndex(value.intValue(), false);
+ }
+ }
+
+ // Sets requested firstrow and rows for the next paint
+ if (variables.containsKey("reqfirstrow")
+ || variables.containsKey("reqrows")) {
+
+ try {
+ firstToBeRenderedInClient = ((Integer) variables
+ .get("firstToBeRendered")).intValue();
+ lastToBeRenderedInClient = ((Integer) variables
+ .get("lastToBeRendered")).intValue();
+ } catch (Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.FINER,
+ "Could not parse the first and/or last rows.", e);
+ }
+
+ // respect suggested rows only if table is not otherwise updated
+ // (row caches emptied by other event)
+ if (!containerChangeToBeRendered) {
+ Integer value = (Integer) variables.get("reqfirstrow");
+ if (value != null) {
+ reqFirstRowToPaint = value.intValue();
+ }
+
+ value = (Integer) variables.get("reqrows");
+ if (value != null) {
+ reqRowsToPaint = value.intValue();
+ int size = size();
+ // sanity check
+
+ if (reqFirstRowToPaint >= size) {
+ reqFirstRowToPaint = size;
+ }
+
+ if (reqFirstRowToPaint + reqRowsToPaint > size) {
+ reqRowsToPaint = size - reqFirstRowToPaint;
+ }
+ }
+ }
+ if (getLogger().isLoggable(Level.FINEST)) {
+ getLogger().log(
+ Level.FINEST,
+ "Client wants rows {0}-{1}",
+ new Object[] { reqFirstRowToPaint,
+ (reqFirstRowToPaint + reqRowsToPaint - 1) });
+ }
+ clientNeedsContentRefresh = true;
+ }
+
+ if (isSortEnabled()) {
+ // Sorting
+ boolean doSort = false;
+ if (variables.containsKey("sortcolumn")) {
+ final String colId = (String) variables.get("sortcolumn");
+ if (colId != null && !"".equals(colId) && !"null".equals(colId)) {
+ final Object id = columnIdMap.get(colId);
+ setSortContainerPropertyId(id, false);
+ doSort = true;
+ }
+ }
+ if (variables.containsKey("sortascending")) {
+ final boolean state = ((Boolean) variables.get("sortascending"))
+ .booleanValue();
+ if (state != sortAscending) {
+ setSortAscending(state, false);
+ doSort = true;
+ }
+ }
+ if (doSort) {
+ this.sort();
+ resetPageBuffer();
+ }
+ }
+
+ // Dynamic column hide/show and order
+ // Update visible columns
+ if (isColumnCollapsingAllowed()) {
+ if (variables.containsKey("collapsedcolumns")) {
+ try {
+ final Object[] ids = (Object[]) variables
+ .get("collapsedcolumns");
+ Set<Object> idSet = new HashSet<Object>();
+ for (Object id : ids) {
+ idSet.add(columnIdMap.get(id.toString()));
+ }
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext();) {
+ Object propertyId = it.next();
+ if (isColumnCollapsed(propertyId)) {
+ if (!idSet.contains(propertyId)) {
+ setColumnCollapsed(propertyId, false);
+ }
+ } else if (idSet.contains(propertyId)) {
+ setColumnCollapsed(propertyId, true);
+ }
+ }
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.FINER,
+ "Could not determine column collapsing state", e);
+ }
+ clientNeedsContentRefresh = true;
+ }
+ }
+ if (isColumnReorderingAllowed()) {
+ if (variables.containsKey("columnorder")) {
+ try {
+ final Object[] ids = (Object[]) variables
+ .get("columnorder");
+ // need a real Object[], ids can be a String[]
+ final Object[] idsTemp = new Object[ids.length];
+ for (int i = 0; i < ids.length; i++) {
+ idsTemp[i] = columnIdMap.get(ids[i].toString());
+ }
+ setColumnOrder(idsTemp);
+ if (hasListeners(ColumnReorderEvent.class)) {
+ fireEvent(new ColumnReorderEvent(this));
+ }
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.FINER,
+ "Could not determine column reordering state", e);
+ }
+ clientNeedsContentRefresh = true;
+ }
+ }
+
+ enableContentRefreshing(clientNeedsContentRefresh);
+
+ // Actions
+ if (variables.containsKey("action")) {
+ final StringTokenizer st = new StringTokenizer(
+ (String) variables.get("action"), ",");
+ if (st.countTokens() == 2) {
+ final Object itemId = itemIdMapper.get(st.nextToken());
+ final Action action = actionMapper.get(st.nextToken());
+
+ if (action != null && (itemId == null || containsId(itemId))
+ && actionHandlers != null) {
+ for (Handler ah : actionHandlers) {
+ ah.handleAction(action, this, itemId);
+ }
+ }
+ }
+ }
+
+ }
+
+ /** Handles click event.
+ *
+ * @param variables
+ * the variables
+ */
+ private void handleClickEvent(Map<String, Object> variables) {
+
+ // Item click event
+ if (variables.containsKey("clickEvent")) {
+ String key = (String) variables.get("clickedKey");
+ Object itemId = itemIdMapper.get(key);
+ Object propertyId = null;
+ String colkey = (String) variables.get("clickedColKey");
+ // click is not necessary on a property
+ if (colkey != null) {
+ propertyId = columnIdMap.get(colkey);
+ }
+ MouseEventDetails evt = MouseEventDetails
+ .deSerialize((String) variables.get("clickEvent"));
+ Item item = getItem(itemId);
+ if (item != null) {
+ fireEvent(new ItemClickEvent(this, item, itemId, propertyId,
+ evt));
+ }
+ }
+
+ // Header click event
+ else if (variables.containsKey("headerClickEvent")) {
+
+ MouseEventDetails details = MouseEventDetails
+ .deSerialize((String) variables.get("headerClickEvent"));
+
+ Object cid = variables.get("headerClickCID");
+ Object propertyId = null;
+ if (cid != null) {
+ propertyId = columnIdMap.get(cid.toString());
+ }
+ fireEvent(new HeaderClickEvent(this, propertyId, details));
+ }
+
+ // Footer click event
+ else if (variables.containsKey("footerClickEvent")) {
+ MouseEventDetails details = MouseEventDetails
+ .deSerialize((String) variables.get("footerClickEvent"));
+
+ Object cid = variables.get("footerClickCID");
+ Object propertyId = null;
+ if (cid != null) {
+ propertyId = columnIdMap.get(cid.toString());
+ }
+ fireEvent(new FooterClickEvent(this, propertyId, details));
+ }
+ }
+
+ /** Handles the column resize event sent by the client.
+ *
+ * @param variables
+ * the variables
+ */
+ private void handleColumnResizeEvent(Map<String, Object> variables) {
+ if (variables.containsKey("columnResizeEventColumn")) {
+ Object cid = variables.get("columnResizeEventColumn");
+ Object propertyId = null;
+ if (cid != null) {
+ propertyId = columnIdMap.get(cid.toString());
+
+ Object prev = variables.get("columnResizeEventPrev");
+ int previousWidth = -1;
+ if (prev != null) {
+ previousWidth = Integer.valueOf(prev.toString());
+ }
+
+ Object curr = variables.get("columnResizeEventCurr");
+ int currentWidth = -1;
+ if (curr != null) {
+ currentWidth = Integer.valueOf(curr.toString());
+ }
+
+ fireColumnResizeEvent(propertyId, previousWidth, currentWidth);
+ }
+ }
+ }
+
+ /** Fire column resize event.
+ *
+ * @param propertyId
+ * the property id
+ * @param previousWidth
+ * the previous width
+ * @param currentWidth
+ * the current width
+ */
+ private void fireColumnResizeEvent(Object propertyId, int previousWidth,
+ int currentWidth) {
+ /*
+ * Update the sizes on the server side. If a column previously had a
+ * expand ratio and the user resized the column then the expand ratio
+ * will be turned into a static pixel size.
+ */
+ setColumnWidth(propertyId, currentWidth);
+
+ fireEvent(new ColumnResizeEvent(this, propertyId, previousWidth,
+ currentWidth));
+ }
+
+ /** Handle column width updates.
+ *
+ * @param variables
+ * the variables
+ */
+ private void handleColumnWidthUpdates(Map<String, Object> variables) {
+ if (variables.containsKey("columnWidthUpdates")) {
+ String[] events = (String[]) variables.get("columnWidthUpdates");
+ for (String str : events) {
+ String[] eventDetails = str.split(":");
+ Object propertyId = columnIdMap.get(eventDetails[0]);
+ if (propertyId == null) {
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+ int width = Integer.valueOf(eventDetails[1]);
+ setColumnWidth(propertyId, width);
+ }
+ }
+ }
+
+ /**
+ * Go to mode where content updates are not done. This is due we want to
+ * bypass expensive content for some reason (like when we know we may have
+ * other content changes on their way).
+ *
+ * @return true if content refresh flag was enabled prior this call
+ */
+ protected boolean disableContentRefreshing() {
+ boolean wasDisabled = isContentRefreshesEnabled;
+ isContentRefreshesEnabled = false;
+ return wasDisabled;
+ }
+
+ /**
+ * Go to mode where content content refreshing has effect.
+ *
+ * @param refreshContent
+ * true if content refresh needs to be done
+ */
+ protected void enableContentRefreshing(boolean refreshContent) {
+ isContentRefreshesEnabled = true;
+ if (refreshContent) {
+ refreshRenderedCells();
+ // Ensure that client gets a response
+ markAsDirty();
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.ui.AbstractField#beforeClientResponse(boolean)
+ */
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ super.beforeClientResponse(initial);
+
+ // Ensure pageBuffer is filled before sending the response to avoid
+ // calls to markAsDirty during paint
+ getVisibleCells();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractSelect#paintContent(com.vaadin.
+ * terminal.PaintTarget)
+ */
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ isBeingPainted = true;
+ try {
+ doPaintContent(target);
+ } finally {
+ isBeingPainted = false;
+ }
+ }
+
+ /** Do paint content.
+ *
+ * @param target
+ * the target
+ * @throws PaintException
+ * the paint exception
+ */
+ private void doPaintContent(PaintTarget target) throws PaintException {
+ /*
+ * Body actions - Actions which has the target null and can be invoked
+ * by right clicking on the table body.
+ */
+ final Set<Action> actionSet = findAndPaintBodyActions(target);
+
+ final Object[][] cells = getVisibleCells();
+ int rows = findNumRowsToPaint(target, cells);
+
+ int total = size();
+ if (shouldHideNullSelectionItem()) {
+ total--;
+ rows--;
+ }
+
+ // CustomTable attributes
+ paintTableAttributes(target, rows, total);
+
+ paintVisibleColumnOrder(target);
+
+ // Rows
+ if (isPartialRowUpdate() && painted && !target.isFullRepaint()) {
+ paintPartialRowUpdate(target, actionSet);
+ } else if (target.isFullRepaint() || isRowCacheInvalidated()) {
+ paintRows(target, cells, actionSet);
+ setRowCacheInvalidated(false);
+ }
+
+ /*
+ * Send the page buffer indexes to ensure that the client side stays in
+ * sync. Otherwise we _might_ have the situation where the client side
+ * discards too few or too many rows, causing out of sync issues.
+ */
+ int pageBufferLastIndex = pageBufferFirstIndex
+ + pageBuffer[CELL_ITEMID].length - 1;
+ target.addAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_FIRST,
+ pageBufferFirstIndex);
+ target.addAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_LAST,
+ pageBufferLastIndex);
+
+ paintSorting(target);
+
+ resetVariablesAndPageBuffer(target);
+
+ // Actions
+ paintActions(target, actionSet);
+
+ paintColumnOrder(target);
+
+ // Available columns
+ paintAvailableColumns(target);
+
+ paintVisibleColumns(target);
+
+ if (keyMapperReset) {
+ keyMapperReset = false;
+ target.addAttribute(TableConstants.ATTRIBUTE_KEY_MAPPER_RESET, true);
+ }
+
+ if (dropHandler != null) {
+ dropHandler.getAcceptCriterion().paint(target);
+ }
+
+ painted = true;
+ }
+
+ /** Sets the row cache invalidated.
+ *
+ * @param invalidated
+ * the new row cache invalidated
+ */
+ private void setRowCacheInvalidated(boolean invalidated) {
+ rowCacheInvalidated = invalidated;
+ }
+
+ /** Checks if is row cache invalidated.
+ *
+ * @return true, if is row cache invalidated
+ */
+ protected boolean isRowCacheInvalidated() {
+ return rowCacheInvalidated;
+ }
+
+ /** Paint partial row update.
+ *
+ * @param target
+ * the target
+ * @param actionSet
+ * the action set
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintPartialRowUpdate(PaintTarget target, Set<Action> actionSet)
+ throws PaintException {
+ paintPartialRowUpdates(target, actionSet);
+ paintPartialRowAdditions(target, actionSet);
+ }
+
+ /** Paint partial row updates.
+ *
+ * @param target
+ * the target
+ * @param actionSet
+ * the action set
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintPartialRowUpdates(PaintTarget target,
+ Set<Action> actionSet) throws PaintException {
+ final boolean[] iscomponent = findCellsWithComponents();
+
+ int firstIx = getFirstUpdatedItemIndex();
+ int count = getUpdatedRowCount();
+
+ target.startTag("urows");
+ target.addAttribute("firsturowix", firstIx);
+ target.addAttribute("numurows", count);
+
+ // Partial row updates bypass the normal caching mechanism.
+ Object[][] cells = getVisibleCellsUpdateCacheRows(firstIx, count);
+ for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) {
+ final Object itemId = cells[CELL_ITEMID][indexInRowbuffer];
+
+ if (shouldHideNullSelectionItem()) {
+ // Remove null selection item if null selection is not allowed
+ continue;
+ }
+
+ paintRow(target, cells, isEditable(), actionSet, iscomponent,
+ indexInRowbuffer, itemId);
+ }
+ target.endTag("urows");
+ maybeThrowCacheUpdateExceptions();
+ }
+
+ /** Paint partial row additions.
+ *
+ * @param target
+ * the target
+ * @param actionSet
+ * the action set
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintPartialRowAdditions(PaintTarget target,
+ Set<Action> actionSet) throws PaintException {
+ final boolean[] iscomponent = findCellsWithComponents();
+
+ int firstIx = getFirstAddedItemIndex();
+ int count = getAddedRowCount();
+
+ target.startTag("prows");
+
+ if (!shouldHideAddedRows()) {
+ getLogger().log(Level.FINEST,
+ "Paint rows for add. Index: {0}, count: {1}.",
+ new Object[] { firstIx, count });
+
+ // Partial row additions bypass the normal caching mechanism.
+ Object[][] cells = getVisibleCellsInsertIntoCache(firstIx, count);
+ if (cells[0].length < count) {
+ // delete the rows below, since they will fall beyond the cache
+ // page.
+ target.addAttribute("delbelow", true);
+ count = cells[0].length;
+ }
+
+ for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) {
+ final Object itemId = cells[CELL_ITEMID][indexInRowbuffer];
+ if (shouldHideNullSelectionItem()) {
+ // Remove null selection item if null selection is not
+ // allowed
+ continue;
+ }
+
+ paintRow(target, cells, isEditable(), actionSet, iscomponent,
+ indexInRowbuffer, itemId);
+ }
+ } else {
+ getLogger().log(Level.FINEST,
+ "Paint rows for remove. Index: {0}, count: {1}.",
+ new Object[] { firstIx, count });
+ removeRowsFromCacheAndFillBottom(firstIx, count);
+ target.addAttribute("hide", true);
+ }
+
+ target.addAttribute("firstprowix", firstIx);
+ target.addAttribute("numprows", count);
+ target.endTag("prows");
+ maybeThrowCacheUpdateExceptions();
+ }
+
+ /**
+ * Subclass and override this to enable partial row updates and additions,
+ * which bypass the normal caching mechanism. This is useful for e.g.
+ * TreeTable.
+ *
+ * @return true if this update is a partial row update, false if not. For
+ * plain CustomTable it is always false.
+ */
+ protected boolean isPartialRowUpdate() {
+ return false;
+ }
+
+ /**
+ * Subclass and override this to enable partial row additions, bypassing the
+ * normal caching mechanism. This is useful for e.g. TreeTable, where
+ * expanding a node should only fetch and add the items inside of that node.
+ *
+ * @return The index of the first added item. For plain CustomTable it is
+ * always 0.
+ */
+ protected int getFirstAddedItemIndex() {
+ return 0;
+ }
+
+ /**
+ * Subclass and override this to enable partial row additions, bypassing the
+ * normal caching mechanism. This is useful for e.g. TreeTable, where
+ * expanding a node should only fetch and add the items inside of that node.
+ *
+ * @return the number of rows to be added, starting at the index returned by
+ * {@link #getFirstAddedItemIndex()}. For plain CustomTable it is
+ * always 0.
+ */
+ protected int getAddedRowCount() {
+ return 0;
+ }
+
+ /**
+ * Subclass and override this to enable removing of rows, bypassing the
+ * normal caching and lazy loading mechanism. This is useful for e.g.
+ * TreeTable, when you need to hide certain rows as a node is collapsed.
+ *
+ * This should return true if the rows pointed to by
+ * {@link #getFirstAddedItemIndex()} and {@link #getAddedRowCount()} should
+ * be hidden instead of added.
+ *
+ * @return whether the rows to add (see {@link #getFirstAddedItemIndex()}
+ * and {@link #getAddedRowCount()}) should be added or hidden. For
+ * plain CustomTable it is always false.
+ */
+ protected boolean shouldHideAddedRows() {
+ return false;
+ }
+
+ /**
+ * Subclass and override this to enable partial row updates, bypassing the
+ * normal caching and lazy loading mechanism. This is useful for updating
+ * the state of certain rows, e.g. in the TreeTable the collapsed state of a
+ * single node is updated using this mechanism.
+ *
+ * @return the index of the first item to be updated. For plain CustomTable
+ * it is always 0.
+ */
+ protected int getFirstUpdatedItemIndex() {
+ return 0;
+ }
+
+ /**
+ * Subclass and override this to enable partial row updates, bypassing the
+ * normal caching and lazy loading mechanism. This is useful for updating
+ * the state of certain rows, e.g. in the TreeTable the collapsed state of a
+ * single node is updated using this mechanism.
+ *
+ * @return the number of rows to update, starting at the index returned by
+ * {@link #getFirstUpdatedItemIndex()}. For plain table it is always
+ * 0.
+ */
+ protected int getUpdatedRowCount() {
+ return 0;
+ }
+
+ /** Paint table attributes.
+ *
+ * @param target
+ * the target
+ * @param rows
+ * the rows
+ * @param total
+ * the total
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintTableAttributes(PaintTarget target, int rows, int total)
+ throws PaintException {
+ paintTabIndex(target);
+ paintDragMode(target);
+ paintSelectMode(target);
+
+ if (cacheRate != CACHE_RATE_DEFAULT) {
+ target.addAttribute("cr", cacheRate);
+ }
+
+ target.addAttribute("cols", getVisibleColumns().length);
+ target.addAttribute("rows", rows);
+
+ target.addAttribute("firstrow",
+ (reqFirstRowToPaint >= 0 ? reqFirstRowToPaint
+ : firstToBeRenderedInClient));
+ target.addAttribute("totalrows", total);
+ if (getPageLength() != 0) {
+ target.addAttribute("pagelength", getPageLength());
+ }
+ if (areColumnHeadersEnabled()) {
+ target.addAttribute("colheaders", true);
+ }
+ if (rowHeadersAreEnabled()) {
+ target.addAttribute("rowheaders", true);
+ }
+
+ target.addAttribute("colfooters", columnFootersVisible);
+
+ // The cursors are only shown on pageable table
+ if (getCurrentPageFirstItemIndex() != 0 || getPageLength() > 0) {
+ target.addVariable(this, "firstvisible",
+ getCurrentPageFirstItemIndex());
+ target.addVariable(this, "firstvisibleonlastpage",
+ currentPageFirstItemIndexOnLastPage);
+ }
+ }
+
+ /** Resets and paints "to be painted next" variables. Also reset
+ * pageBuffer
+ *
+ * @param target
+ * the target
+ * @throws PaintException
+ * the paint exception
+ */
+ private void resetVariablesAndPageBuffer(PaintTarget target)
+ throws PaintException {
+ reqFirstRowToPaint = -1;
+ reqRowsToPaint = -1;
+ containerChangeToBeRendered = false;
+ target.addVariable(this, "reqrows", reqRowsToPaint);
+ target.addVariable(this, "reqfirstrow", reqFirstRowToPaint);
+ }
+
+ /** Are column headers enabled.
+ *
+ * @return true, if successful
+ */
+ private boolean areColumnHeadersEnabled() {
+ return getColumnHeaderMode() != ColumnHeaderMode.HIDDEN;
+ }
+
+ /** Paint visible columns.
+ *
+ * @param target
+ * the target
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintVisibleColumns(PaintTarget target) throws PaintException {
+ target.startTag("visiblecolumns");
+ if (rowHeadersAreEnabled()) {
+ target.startTag("column");
+ target.addAttribute("cid", ROW_HEADER_COLUMN_KEY);
+ paintColumnWidth(target, ROW_HEADER_FAKE_PROPERTY_ID);
+ paintColumnExpandRatio(target, ROW_HEADER_FAKE_PROPERTY_ID);
+ target.endTag("column");
+ }
+ final Collection<?> sortables = getSortableContainerPropertyIds();
+ for (Object colId : visibleColumns) {
+ if (colId != null) {
+ target.startTag("column");
+ target.addAttribute("cid", columnIdMap.key(colId));
+ final String head = getColumnHeader(colId);
+ target.addAttribute("caption", (head != null ? head : ""));
+ final String foot = getColumnFooter(colId);
+ target.addAttribute("fcaption", (foot != null ? foot : ""));
+ if (isColumnCollapsed(colId)) {
+ target.addAttribute("collapsed", true);
+ }
+ if (areColumnHeadersEnabled()) {
+ if (getColumnIcon(colId) != null) {
+ target.addAttribute("icon", getColumnIcon(colId));
+ }
+ if (sortables.contains(colId)) {
+ target.addAttribute("sortable", true);
+ }
+ }
+ if (!Align.LEFT.equals(getColumnAlignment(colId))) {
+ target.addAttribute("align", getColumnAlignment(colId)
+ .toString());
+ }
+ paintColumnWidth(target, colId);
+ paintColumnExpandRatio(target, colId);
+ target.endTag("column");
+ }
+ }
+ target.endTag("visiblecolumns");
+ }
+
+ /** Paint available columns.
+ *
+ * @param target
+ * the target
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintAvailableColumns(PaintTarget target)
+ throws PaintException {
+ if (columnCollapsingAllowed) {
+ final HashSet<Object> collapsedCols = new HashSet<Object>();
+ for (Object colId : visibleColumns) {
+ if (isColumnCollapsed(colId)) {
+ collapsedCols.add(colId);
+ }
+ }
+ final String[] collapsedKeys = new String[collapsedCols.size()];
+ int nextColumn = 0;
+ for (Object colId : visibleColumns) {
+ if (isColumnCollapsed(colId)) {
+ collapsedKeys[nextColumn++] = columnIdMap.key(colId);
+ }
+ }
+ target.addVariable(this, "collapsedcolumns", collapsedKeys);
+
+ final String[] noncollapsibleKeys = new String[noncollapsibleColumns
+ .size()];
+ nextColumn = 0;
+ for (Object colId : noncollapsibleColumns) {
+ noncollapsibleKeys[nextColumn++] = columnIdMap.key(colId);
+ }
+ target.addVariable(this, "noncollapsiblecolumns",
+ noncollapsibleKeys);
+ }
+
+ }
+
+ /** Paint actions.
+ *
+ * @param target
+ * the target
+ * @param actionSet
+ * the action set
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintActions(PaintTarget target, final Set<Action> actionSet)
+ throws PaintException {
+ if (!actionSet.isEmpty()) {
+ target.addVariable(this, "action", "");
+ target.startTag("actions");
+ for (Action a : actionSet) {
+ target.startTag("action");
+ if (a.getCaption() != null) {
+ target.addAttribute("caption", a.getCaption());
+ }
+ if (a.getIcon() != null) {
+ target.addAttribute("icon", a.getIcon());
+ }
+ target.addAttribute("key", actionMapper.key(a));
+ target.endTag("action");
+ }
+ target.endTag("actions");
+ }
+ }
+
+ /** Paint column order.
+ *
+ * @param target
+ * the target
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintColumnOrder(PaintTarget target) throws PaintException {
+ if (columnReorderingAllowed) {
+ final String[] colorder = new String[visibleColumns.size()];
+ int i = 0;
+ for (Object colId : visibleColumns) {
+ colorder[i++] = columnIdMap.key(colId);
+ }
+ target.addVariable(this, "columnorder", colorder);
+ }
+ }
+
+ /** Paint sorting.
+ *
+ * @param target
+ * the target
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintSorting(PaintTarget target) throws PaintException {
+ // Sorting
+ if (getContainerDataSource() instanceof Container.Sortable) {
+ target.addVariable(this, "sortcolumn",
+ columnIdMap.key(sortContainerPropertyId));
+ target.addVariable(this, "sortascending", sortAscending);
+ }
+ }
+
+ /** Paint rows.
+ *
+ * @param target
+ * the target
+ * @param cells
+ * the cells
+ * @param actionSet
+ * the action set
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintRows(PaintTarget target, final Object[][] cells,
+ final Set<Action> actionSet) throws PaintException {
+ final boolean[] iscomponent = findCellsWithComponents();
+
+ target.startTag("rows");
+ // cells array contains all that are supposed to be visible on client,
+ // but we'll start from the one requested by client
+ int start = 0;
+ if (reqFirstRowToPaint != -1 && firstToBeRenderedInClient != -1) {
+ start = reqFirstRowToPaint - firstToBeRenderedInClient;
+ }
+ int end = cells[0].length;
+ if (reqRowsToPaint != -1) {
+ end = start + reqRowsToPaint;
+ }
+ // sanity check
+ if (lastToBeRenderedInClient != -1 && lastToBeRenderedInClient < end) {
+ end = lastToBeRenderedInClient + 1;
+ }
+ if (start > cells[CELL_ITEMID].length || start < 0) {
+ start = 0;
+ }
+ if (end > cells[CELL_ITEMID].length) {
+ end = cells[CELL_ITEMID].length;
+ }
+
+ for (int indexInRowbuffer = start; indexInRowbuffer < end; indexInRowbuffer++) {
+ final Object itemId = cells[CELL_ITEMID][indexInRowbuffer];
+
+ if (shouldHideNullSelectionItem()) {
+ // Remove null selection item if null selection is not allowed
+ continue;
+ }
+
+ paintRow(target, cells, isEditable(), actionSet, iscomponent,
+ indexInRowbuffer, itemId);
+ }
+ target.endTag("rows");
+ }
+
+ /** Find cells with components.
+ *
+ * @return the boolean[]
+ */
+ private boolean[] findCellsWithComponents() {
+ final boolean[] isComponent = new boolean[visibleColumns.size()];
+ int ix = 0;
+ for (Object columnId : visibleColumns) {
+ if (columnGenerators.containsKey(columnId)) {
+ isComponent[ix++] = true;
+ } else {
+ final Class<?> colType = getType(columnId);
+ isComponent[ix++] = colType != null
+ && Component.class.isAssignableFrom(colType);
+ }
+ }
+ return isComponent;
+ }
+
+ /** Paint visible column order.
+ *
+ * @param target
+ * the target
+ */
+ private void paintVisibleColumnOrder(PaintTarget target) {
+ // Visible column order
+ final ArrayList<String> visibleColOrder = new ArrayList<String>();
+ for (Object columnId : visibleColumns) {
+ if (!isColumnCollapsed(columnId)) {
+ visibleColOrder.add(columnIdMap.key(columnId));
+ }
+ }
+ target.addAttribute("vcolorder", visibleColOrder.toArray());
+ }
+
+ /** Find and paint body actions.
+ *
+ * @param target
+ * the target
+ * @return the sets the
+ */
+ private Set<Action> findAndPaintBodyActions(PaintTarget target) {
+ Set<Action> actionSet = new LinkedHashSet<Action>();
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ for (Handler ah : actionHandlers) {
+ // Getting actions for the null item, which in this case means
+ // the body item
+ final Action[] actions = ah.getActions(null, this);
+ if (actions != null) {
+ for (Action action : actions) {
+ actionSet.add(action);
+ keys.add(actionMapper.key(action));
+ }
+ }
+ }
+ target.addAttribute("alb", keys.toArray());
+ }
+ return actionSet;
+ }
+
+ /** Should hide null selection item.
+ *
+ * @return true, if successful
+ */
+ private boolean shouldHideNullSelectionItem() {
+ return !isNullSelectionAllowed() && getNullSelectionItemId() != null
+ && containsId(getNullSelectionItemId());
+ }
+
+ /** Find num rows to paint.
+ *
+ * @param target
+ * the target
+ * @param cells
+ * the cells
+ * @return the int
+ * @throws PaintException
+ * the paint exception
+ */
+ private int findNumRowsToPaint(PaintTarget target, final Object[][] cells)
+ throws PaintException {
+ int rows;
+ if (reqRowsToPaint >= 0) {
+ rows = reqRowsToPaint;
+ } else {
+ rows = cells[0].length;
+ if (alwaysRecalculateColumnWidths) {
+ // TODO experimental feature for now: tell the client to
+ // recalculate column widths.
+ // We'll only do this for paints that do not originate from
+ // table scroll/cache requests (i.e when reqRowsToPaint<0)
+ target.addAttribute("recalcWidths", true);
+ }
+ }
+ return rows;
+ }
+
+ /** Paint select mode.
+ *
+ * @param target
+ * the target
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintSelectMode(PaintTarget target) throws PaintException {
+ if (multiSelectMode != MultiSelectMode.DEFAULT) {
+ target.addAttribute("multiselectmode", multiSelectMode.ordinal());
+ }
+ if (isSelectable()) {
+ target.addAttribute("selectmode", (isMultiSelect() ? "multi"
+ : "single"));
+ } else {
+ target.addAttribute("selectmode", "none");
+ }
+ if (!isNullSelectionAllowed()) {
+ target.addAttribute("nsa", false);
+ }
+
+ // selection support
+ // The select variable is only enabled if selectable
+ if (isSelectable()) {
+ target.addVariable(this, "selected", findSelectedKeys());
+ }
+ }
+
+ /** Find selected keys.
+ *
+ * @return the string[]
+ */
+ private String[] findSelectedKeys() {
+ LinkedList<String> selectedKeys = new LinkedList<String>();
+ if (isMultiSelect()) {
+ HashSet<?> sel = new HashSet<Object>((Set<?>) getValue());
+ Collection<?> vids = getVisibleItemIds();
+ for (Iterator<?> it = vids.iterator(); it.hasNext();) {
+ Object id = it.next();
+ if (sel.contains(id)) {
+ selectedKeys.add(itemIdMapper.key(id));
+ }
+ }
+ } else {
+ Object value = getValue();
+ if (value == null) {
+ value = getNullSelectionItemId();
+ }
+ if (value != null) {
+ selectedKeys.add(itemIdMapper.key(value));
+ }
+ }
+ return selectedKeys.toArray(new String[selectedKeys.size()]);
+ }
+
+ /** Paint drag mode.
+ *
+ * @param target
+ * the target
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintDragMode(PaintTarget target) throws PaintException {
+ if (dragMode != TableDragMode.NONE) {
+ target.addAttribute("dragmode", dragMode.ordinal());
+ }
+ }
+
+ /** Paint tab index.
+ *
+ * @param target
+ * the target
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintTabIndex(PaintTarget target) throws PaintException {
+ // The tab ordering number
+ if (getTabIndex() > 0) {
+ target.addAttribute("tabindex", getTabIndex());
+ }
+ }
+
+ /** Paint column width.
+ *
+ * @param target
+ * the target
+ * @param columnId
+ * the column id
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintColumnWidth(PaintTarget target, final Object columnId)
+ throws PaintException {
+ if (columnWidths.containsKey(columnId)) {
+ target.addAttribute("width", getColumnWidth(columnId));
+ }
+ }
+
+ /** Paint column expand ratio.
+ *
+ * @param target
+ * the target
+ * @param columnId
+ * the column id
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintColumnExpandRatio(PaintTarget target,
+ final Object columnId) throws PaintException {
+ if (columnExpandRatios.containsKey(columnId)) {
+ target.addAttribute("er", getColumnExpandRatio(columnId));
+ }
+ }
+
+ /** Row headers are enabled.
+ *
+ * @return true, if successful
+ */
+ private boolean rowHeadersAreEnabled() {
+ return getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN;
+ }
+
+ /** Paint row.
+ *
+ * @param target
+ * the target
+ * @param cells
+ * the cells
+ * @param iseditable
+ * the iseditable
+ * @param actionSet
+ * the action set
+ * @param iscomponent
+ * the iscomponent
+ * @param indexInRowbuffer
+ * the index in rowbuffer
+ * @param itemId
+ * the item id
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintRow(PaintTarget target, final Object[][] cells,
+ final boolean iseditable, final Set<Action> actionSet,
+ final boolean[] iscomponent, int indexInRowbuffer,
+ final Object itemId) throws PaintException {
+ target.startTag("tr");
+
+ paintRowAttributes(target, cells, actionSet, indexInRowbuffer, itemId);
+
+ // cells
+ int currentColumn = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); currentColumn++) {
+ final Object columnId = it.next();
+ if (columnId == null || isColumnCollapsed(columnId)) {
+ continue;
+ }
+ /*
+ * For each cell, if a cellStyleGenerator is specified, get the
+ * specific style for the cell. If there is any, add it to the
+ * target.
+ */
+ if (cellStyleGenerator != null) {
+ String cellStyle = cellStyleGenerator.getStyle(this, itemId,
+ columnId);
+ if (cellStyle != null && !cellStyle.equals("")) {
+ target.addAttribute("style-" + columnIdMap.key(columnId),
+ cellStyle);
+ }
+ }
+
+ if ((iscomponent[currentColumn] || iseditable || cells[CELL_GENERATED_ROW][indexInRowbuffer] != null)
+ && Component.class.isInstance(cells[CELL_FIRSTCOL
+ + currentColumn][indexInRowbuffer])) {
+ final Component c = (Component) cells[CELL_FIRSTCOL
+ + currentColumn][indexInRowbuffer];
+ if (c == null
+ || !LegacyCommunicationManager
+ .isComponentVisibleToClient(c)) {
+ target.addText("");
+ } else {
+ LegacyPaint.paint(c, target);
+ }
+ } else {
+ target.addText((String) cells[CELL_FIRSTCOL + currentColumn][indexInRowbuffer]);
+ }
+ paintCellTooltips(target, itemId, columnId);
+ }
+
+ target.endTag("tr");
+ }
+
+ /** Paint cell tooltips.
+ *
+ * @param target
+ * the target
+ * @param itemId
+ * the item id
+ * @param columnId
+ * the column id
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintCellTooltips(PaintTarget target, Object itemId,
+ Object columnId) throws PaintException {
+ if (itemDescriptionGenerator != null) {
+ String itemDescription = itemDescriptionGenerator
+ .generateDescription(this, itemId, columnId);
+ if (itemDescription != null && !itemDescription.equals("")) {
+ target.addAttribute("descr-" + columnIdMap.key(columnId),
+ itemDescription);
+ }
+ }
+ }
+
+ /** Paint row tooltips.
+ *
+ * @param target
+ * the target
+ * @param itemId
+ * the item id
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintRowTooltips(PaintTarget target, Object itemId)
+ throws PaintException {
+ if (itemDescriptionGenerator != null) {
+ String rowDescription = itemDescriptionGenerator
+ .generateDescription(this, itemId, null);
+ if (rowDescription != null && !rowDescription.equals("")) {
+ target.addAttribute("rowdescr", rowDescription);
+ }
+ }
+ }
+
+ /** Paint row attributes.
+ *
+ * @param target
+ * the target
+ * @param cells
+ * the cells
+ * @param actionSet
+ * the action set
+ * @param indexInRowbuffer
+ * the index in rowbuffer
+ * @param itemId
+ * the item id
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintRowAttributes(PaintTarget target, final Object[][] cells,
+ final Set<Action> actionSet, int indexInRowbuffer,
+ final Object itemId) throws PaintException {
+ // tr attributes
+
+ paintRowIcon(target, cells, indexInRowbuffer);
+ paintRowHeader(target, cells, indexInRowbuffer);
+ paintGeneratedRowInfo(target, cells, indexInRowbuffer);
+ target.addAttribute("key",
+ Integer.parseInt(cells[CELL_KEY][indexInRowbuffer].toString()));
+
+ if (isSelected(itemId)) {
+ target.addAttribute("selected", true);
+ }
+
+ // Actions
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ for (Handler ah : actionHandlers) {
+ final Action[] aa = ah.getActions(itemId, this);
+ if (aa != null) {
+ for (int ai = 0; ai < aa.length; ai++) {
+ final String key = actionMapper.key(aa[ai]);
+ actionSet.add(aa[ai]);
+ keys.add(key);
+ }
+ }
+ }
+ target.addAttribute("al", keys.toArray());
+ }
+
+ /*
+ * For each row, if a cellStyleGenerator is specified, get the specific
+ * style for the cell, using null as propertyId. If there is any, add it
+ * to the target.
+ */
+ if (cellStyleGenerator != null) {
+ String rowStyle = cellStyleGenerator.getStyle(this, itemId, null);
+ if (rowStyle != null && !rowStyle.equals("")) {
+ target.addAttribute("rowstyle", rowStyle);
+ }
+ }
+
+ paintRowTooltips(target, itemId);
+
+ paintRowAttributes(target, itemId);
+ }
+
+ /** Paint generated row info.
+ *
+ * @param target
+ * the target
+ * @param cells
+ * the cells
+ * @param indexInRowBuffer
+ * the index in row buffer
+ * @throws PaintException
+ * the paint exception
+ */
+ private void paintGeneratedRowInfo(PaintTarget target, Object[][] cells,
+ int indexInRowBuffer) throws PaintException {
+ GeneratedRow generatedRow = (GeneratedRow) cells[CELL_GENERATED_ROW][indexInRowBuffer];
+ if (generatedRow != null) {
+ target.addAttribute("gen_html", generatedRow.isHtmlContentAllowed());
+ target.addAttribute("gen_span", generatedRow.isSpanColumns());
+ target.addAttribute("gen_widget",
+ generatedRow.getValue() instanceof Component);
+ }
+ }
+
+ /** Paint row header.
+ *
+ * @param target
+ * the target
+ * @param cells
+ * the cells
+ * @param indexInRowbuffer
+ * the index in rowbuffer
+ * @throws PaintException
+ * the paint exception
+ */
+ protected void paintRowHeader(PaintTarget target, Object[][] cells,
+ int indexInRowbuffer) throws PaintException {
+ if (rowHeadersAreEnabled()) {
+ if (cells[CELL_HEADER][indexInRowbuffer] != null) {
+ target.addAttribute("caption",
+ (String) cells[CELL_HEADER][indexInRowbuffer]);
+ }
+ }
+
+ }
+
+ /** Paint row icon.
+ *
+ * @param target
+ * the target
+ * @param cells
+ * the cells
+ * @param indexInRowbuffer
+ * the index in rowbuffer
+ * @throws PaintException
+ * the paint exception
+ */
+ protected void paintRowIcon(PaintTarget target, final Object[][] cells,
+ int indexInRowbuffer) throws PaintException {
+ if (rowHeadersAreEnabled()
+ && cells[CELL_ICON][indexInRowbuffer] != null) {
+ target.addAttribute("icon",
+ (Resource) cells[CELL_ICON][indexInRowbuffer]);
+ }
+ }
+
+ /** A method where extended CustomTable implementations may add their
+ * custom attributes for rows.
+ *
+ * @param target
+ * the target
+ * @param itemId
+ * the item id
+ * @throws PaintException
+ * the paint exception
+ */
+ protected void paintRowAttributes(PaintTarget target, Object itemId)
+ throws PaintException {
+
+ }
+
+ /**
+ * Gets the cached visible table contents.
+ *
+ * @return the cached visible table contents.
+ */
+ private Object[][] getVisibleCells() {
+ if (pageBuffer == null) {
+ refreshRenderedCells();
+ }
+ return pageBuffer;
+ }
+
+ /**
+ * Gets the value of property.
+ *
+ * By default if the table is editable the fieldFactory is used to create
+ * editors for table cells. Otherwise formatPropertyValue is used to format
+ * the value representation.
+ *
+ * @param rowId
+ * the Id of the row (same as item Id).
+ * @param colId
+ * the Id of the column.
+ * @param property
+ * the Property to be presented.
+ * @return Object Either formatted value or Component for field.
+ * @see #setTableFieldFactory(TableFieldFactory)
+ */
+ protected Object getPropertyValue(Object rowId, Object colId,
+ Property property) {
+ if (isEditable() && fieldFactory != null) {
+ final Field<?> f = fieldFactory.createField(
+ getContainerDataSource(), rowId, colId, this);
+ if (f != null) {
+ // Remember that we have made this association so we can remove
+ // it when the component is removed
+ associatedProperties.put(f, property);
+ bindPropertyToField(rowId, colId, property, f);
+ return f;
+ }
+ }
+
+ return formatPropertyValue(rowId, colId, property);
+ }
+
+ /** Binds an item property to a field generated by TableFieldFactory. The
+ * default behavior is to bind property straight to Field. If
+ * Property.Viewer type property (e.g. PropertyFormatter) is already set for
+ * field, the property is bound to that Property.Viewer.
+ *
+ * @param rowId
+ * the row id
+ * @param colId
+ * the col id
+ * @param property
+ * the property
+ * @param field
+ * the field
+ * @since 6.7.3
+ */
+ protected void bindPropertyToField(Object rowId, Object colId,
+ Property property, Field field) {
+ // check if field has a property that is Viewer set. In that case we
+ // expect developer has e.g. PropertyFormatter that he wishes to use and
+ // assign the property to the Viewer instead.
+ boolean hasFilterProperty = field.getPropertyDataSource() != null
+ && (field.getPropertyDataSource() instanceof Property.Viewer);
+ if (hasFilterProperty) {
+ ((Property.Viewer) field.getPropertyDataSource())
+ .setPropertyDataSource(property);
+ } else {
+ field.setPropertyDataSource(property);
+ }
+ }
+
+ /**
+ * Formats table cell property values. By default the property.toString()
+ * and return a empty string for null properties.
+ *
+ * @param rowId
+ * the Id of the row (same as item Id).
+ * @param colId
+ * the Id of the column.
+ * @param property
+ * the Property to be formatted.
+ * @return the String representation of property and its value.
+ * @since 3.1
+ */
+ protected String formatPropertyValue(Object rowId, Object colId,
+ Property<?> property) {
+ if (property == null) {
+ return "";
+ }
+ Converter<String, Object> converter = null;
+
+ if (hasConverter(colId)) {
+ converter = getConverter(colId);
+ } else {
+ converter = (Converter) ConverterUtil.getConverter(String.class,
+ property.getType(), getSession());
+ }
+ Object value = property.getValue();
+ if (converter != null) {
+ return converter.convertToPresentation(value, String.class,
+ getLocale());
+ }
+ return (null != value) ? value.toString() : "";
+ }
+
+ /* Action container */
+
+ /** Registers a new action handler for this container.
+ *
+ * @param actionHandler
+ * the action handler
+ * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler)
+ */
+
+ @Override
+ public void addActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandler != null) {
+
+ if (actionHandlers == null) {
+ actionHandlers = new LinkedList<Handler>();
+ actionMapper = new KeyMapper<Action>();
+ }
+
+ if (!actionHandlers.contains(actionHandler)) {
+ actionHandlers.add(actionHandler);
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the action
+ // handlers.
+ refreshRenderedCells();
+ }
+
+ }
+ }
+
+ /** Removes a previously registered action handler for the contents of
+ * this container.
+ *
+ * @param actionHandler
+ * the action handler
+ * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler)
+ */
+
+ @Override
+ public void removeActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandlers != null && actionHandlers.contains(actionHandler)) {
+
+ actionHandlers.remove(actionHandler);
+
+ if (actionHandlers.isEmpty()) {
+ actionHandlers = null;
+ actionMapper = null;
+ }
+
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the action
+ // handlers.
+ refreshRenderedCells();
+ }
+ }
+
+ /** Removes all action handlers.
+ */
+ public void removeAllActionHandlers() {
+ actionHandlers = null;
+ actionMapper = null;
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the action
+ // handlers.
+ refreshRenderedCells();
+ }
+
+ /* Property value change listening support */
+
+ /** Notifies this listener that the Property's value has changed.
+ *
+ * Also listens changes in rendered items to refresh content area.
+ *
+ * @param event
+ * the event
+ * @see com.vaadin.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent)
+ */
+
+ @Override
+ public void valueChange(Property.ValueChangeEvent event) {
+ if (equals(event.getProperty())
+ || event.getProperty() == getPropertyDataSource()) {
+ super.valueChange(event);
+ } else {
+ refreshRowCache();
+ containerChangeToBeRendered = true;
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Clears the current page buffer. Call this before
+ * {@link #refreshRenderedCells()} to ensure that all content is updated
+ * from the properties.
+ */
+ protected void resetPageBuffer() {
+ firstToBeRenderedInClient = -1;
+ lastToBeRenderedInClient = -1;
+ reqFirstRowToPaint = -1;
+ reqRowsToPaint = -1;
+ pageBuffer = null;
+ }
+
+ /**
+ * Notifies the component that it is connected to an application.
+ *
+ * @see com.vaadin.ui.Component#attach()
+ */
+
+ @Override
+ public void attach() {
+ super.attach();
+
+ refreshRenderedCells();
+ }
+
+ /** Notifies the component that it is detached from the application.
+ *
+ * @see com.vaadin.ui.Component#detach()
+ */
+
+ @Override
+ public void detach() {
+ super.detach();
+ }
+
+ /** Removes all Items from the Container.
+ *
+ * @return true, if successful
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+
+ @Override
+ public boolean removeAllItems() {
+ currentPageFirstItemId = null;
+ currentPageFirstItemIndex = 0;
+ return super.removeAllItems();
+ }
+
+ /** Removes the Item identified by <code>ItemId</code> from the
+ * Container.
+ *
+ * @param itemId
+ * the item id
+ * @return true, if successful
+ * @see com.vaadin.data.Container#removeItem(Object)
+ */
+
+ @Override
+ public boolean removeItem(Object itemId) {
+ final Object nextItemId = nextItemId(itemId);
+ final boolean ret = super.removeItem(itemId);
+ if (ret && (itemId != null) && (itemId.equals(currentPageFirstItemId))) {
+ currentPageFirstItemId = nextItemId;
+ }
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return ret;
+ }
+
+ /** Removes a Property specified by the given Property ID from the
+ * Container.
+ *
+ * @param propertyId
+ * the property id
+ * @return true, if successful
+ * @throws UnsupportedOperationException
+ * the unsupported operation exception
+ * @see com.vaadin.data.Container#removeContainerProperty(Object)
+ */
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+
+ // If a visible property is removed, remove the corresponding column
+ visibleColumns.remove(propertyId);
+ columnAlignments.remove(propertyId);
+ columnIcons.remove(propertyId);
+ columnHeaders.remove(propertyId);
+ columnFooters.remove(propertyId);
+ // If a propertyValueConverter was defined for the property, remove it.
+ propertyValueConverters.remove(propertyId);
+
+ return super.removeContainerProperty(propertyId);
+ }
+
+ /** Adds a new property to the table and show it as a visible column.
+ *
+ * @param propertyId
+ * the Id of the proprty.
+ * @param type
+ * the class of the property.
+ * @param defaultValue
+ * the default value given for all existing items.
+ * @return true, if successful
+ * @throws UnsupportedOperationException
+ * the unsupported operation exception
+ * @see com.vaadin.data.Container#addContainerProperty(Object, Class,
+ * Object)
+ */
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+
+ boolean visibleColAdded = false;
+ if (!visibleColumns.contains(propertyId)) {
+ visibleColumns.add(propertyId);
+ visibleColAdded = true;
+ }
+
+ if (!super.addContainerProperty(propertyId, type, defaultValue)) {
+ if (visibleColAdded) {
+ visibleColumns.remove(propertyId);
+ }
+ return false;
+ }
+ if (!(items instanceof Container.PropertySetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return true;
+ }
+
+ /** Adds a new property to the table and show it as a visible column.
+ *
+ * @param propertyId
+ * the Id of the proprty
+ * @param type
+ * the class of the property
+ * @param defaultValue
+ * the default value given for all existing items
+ * @param columnHeader
+ * the Explicit header of the column. If explicit header is not
+ * needed, this should be set null.
+ * @param columnIcon
+ * the Icon of the column. If icon is not needed, this should be
+ * set null.
+ * @param columnAlignment
+ * the Alignment of the column. Null implies align left.
+ * @return true, if successful
+ * @throws UnsupportedOperationException
+ * if the operation is not supported.
+ * @see com.vaadin.data.Container#addContainerProperty(Object, Class,
+ * Object)
+ */
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue, String columnHeader, Resource columnIcon,
+ Align columnAlignment) throws UnsupportedOperationException {
+ if (!this.addContainerProperty(propertyId, type, defaultValue)) {
+ return false;
+ }
+ setColumnAlignment(propertyId, columnAlignment);
+ setColumnHeader(propertyId, columnHeader);
+ setColumnIcon(propertyId, columnIcon);
+ return true;
+ }
+
+ /**
+ * Adds a generated column to the CustomTable.
+ * <p>
+ * A generated column is a column that exists only in the CustomTable, not
+ * as a property in the underlying Container. It shows up just as a regular
+ * column.
+ *
+ * <p>
+ * A generated column will override a property with the same id, so that the
+ * generated column is shown instead of the column representing the
+ * property. Note that getContainerProperty() will still get the real
+ * property.
+ *
+ * <p>
+ * CustomTable will not listen to value change events from properties
+ * overridden by generated columns. If the content of your generated column
+ * depends on properties that are not directly visible in the table, attach
+ * value change listener to update the content on all depended properties.
+ * Otherwise your UI might not get updated as expected.
+ *
+ * <p>
+ * Also note that getVisibleColumns() will return the generated columns,
+ * while getContainerPropertyIds() will not.
+ *
+ *
+ * @param id
+ * the id of the column to be added
+ * @param generatedColumn
+ * the {@link ColumnGenerator} to use for this column
+ */
+ public void addGeneratedColumn(Object id, ColumnGenerator generatedColumn) {
+ if (generatedColumn == null) {
+ throw new IllegalArgumentException(
+ "Can not add null as a GeneratedColumn");
+ }
+ if (columnGenerators.containsKey(id)) {
+ throw new IllegalArgumentException(
+ "Can not add the same GeneratedColumn twice, id:" + id);
+ } else {
+ columnGenerators.put(id, generatedColumn);
+ /*
+ * add to visible column list unless already there (overriding
+ * column from DS)
+ */
+ if (!visibleColumns.contains(id)) {
+ visibleColumns.add(id);
+ }
+ refreshRowCache();
+ }
+ }
+
+ /** Returns the ColumnGenerator used to generate the given column.
+ *
+ * @param columnId
+ * The id of the generated column
+ * @return The ColumnGenerator used for the given columnId or null.
+ * @throws IllegalArgumentException
+ * the illegal argument exception
+ */
+ public ColumnGenerator getColumnGenerator(Object columnId)
+ throws IllegalArgumentException {
+ return columnGenerators.get(columnId);
+ }
+
+ /**
+ * Removes a generated column previously added with addGeneratedColumn.
+ *
+ * @param columnId
+ * id of the generated column to remove
+ * @return true if the column could be removed (existed in the CustomTable)
+ */
+ public boolean removeGeneratedColumn(Object columnId) {
+ if (columnGenerators.containsKey(columnId)) {
+ columnGenerators.remove(columnId);
+ // remove column from visibleColumns list unless it exists in
+ // container (generator previously overrode this column)
+ if (!items.getContainerPropertyIds().contains(columnId)) {
+ visibleColumns.remove(columnId);
+ }
+ refreshRowCache();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /** Returns item identifiers of the items which are currently rendered on
+ * the client.
+ * <p>
+ * Note, that some due to historical reasons the name of the method is bit
+ * misleading. Some items may be partly or totally out of the viewport of
+ * the table's scrollable area. Actually detecting rows which can be
+ * actually seen by the end user may be problematic due to the client server
+ * architecture. Using {@link #getCurrentPageFirstItemId()} combined with
+ * {@link #getPageLength()} may produce good enough estimates in some
+ * situations.
+ *
+ * @return the visible item ids
+ * @see com.vaadin.ui.Select#getVisibleItemIds()
+ */
+
+ @Override
+ public Collection<?> getVisibleItemIds() {
+
+ final LinkedList<Object> visible = new LinkedList<Object>();
+
+ final Object[][] cells = getVisibleCells();
+ // may be null if the table has not been rendered yet (e.g. not attached
+ // to a layout)
+ if (null != cells) {
+ for (int i = 0; i < cells[CELL_ITEMID].length; i++) {
+ visible.add(cells[CELL_ITEMID][i]);
+ }
+ }
+
+ return visible;
+ }
+
+ /** Container datasource item set change. CustomTable must flush its
+ * buffers on change.
+ *
+ * @param event
+ * the event
+ * @see com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent)
+ */
+
+ @Override
+ public void containerItemSetChange(Container.ItemSetChangeEvent event) {
+ if (isBeingPainted) {
+ return;
+ }
+
+ super.containerItemSetChange(event);
+
+ // super method clears the key map, must inform client about this to
+ // avoid getting invalid keys back (#8584)
+ keyMapperReset = true;
+
+ // ensure that page still has first item in page, ignore buffer refresh
+ // (forced in this method)
+ setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false);
+ refreshRowCache();
+ }
+
+ /** Container datasource property set change. CustomTable must flush its
+ * buffers on change.
+ *
+ * @param event
+ * the event
+ * @see com.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent)
+ */
+
+ @Override
+ public void containerPropertySetChange(
+ Container.PropertySetChangeEvent event) {
+ if (isBeingPainted) {
+ return;
+ }
+
+ disableContentRefreshing();
+ super.containerPropertySetChange(event);
+
+ // sanitetize visibleColumns. note that we are not adding previously
+ // non-existing properties as columns
+ Collection<?> containerPropertyIds = getContainerDataSource()
+ .getContainerPropertyIds();
+
+ LinkedList<Object> newVisibleColumns = new LinkedList<Object>(
+ visibleColumns);
+ for (Iterator<Object> iterator = newVisibleColumns.iterator(); iterator
+ .hasNext();) {
+ Object id = iterator.next();
+ if (!(containerPropertyIds.contains(id) || columnGenerators
+ .containsKey(id))) {
+ iterator.remove();
+ }
+ }
+ setVisibleColumns(newVisibleColumns.toArray());
+ // same for collapsed columns
+ for (Iterator<Object> iterator = collapsedColumns.iterator(); iterator
+ .hasNext();) {
+ Object id = iterator.next();
+ if (!(containerPropertyIds.contains(id) || columnGenerators
+ .containsKey(id))) {
+ iterator.remove();
+ }
+ }
+
+ resetPageBuffer();
+ enableContentRefreshing(true);
+ }
+
+ /** Adding new items is not supported.
+ *
+ * @param allowNewOptions
+ * the new new items allowed
+ * @throws UnsupportedOperationException
+ * if set to true.
+ * @see com.vaadin.ui.Select#setNewItemsAllowed(boolean)
+ */
+
+ @Override
+ public void setNewItemsAllowed(boolean allowNewOptions)
+ throws UnsupportedOperationException {
+ if (allowNewOptions) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /** Gets the ID of the Item following the Item that corresponds to
+ * itemId.
+ *
+ * @param itemId
+ * the item id
+ * @return the object
+ * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return ((Container.Ordered) items).nextItemId(itemId);
+ }
+
+ /** Gets the ID of the Item preceding the Item that corresponds to the
+ * itemId.
+ *
+ * @param itemId
+ * the item id
+ * @return the object
+ * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return ((Container.Ordered) items).prevItemId(itemId);
+ }
+
+ /** Gets the ID of the first Item in the Container.
+ *
+ * @return the object
+ * @see com.vaadin.data.Container.Ordered#firstItemId()
+ */
+
+ @Override
+ public Object firstItemId() {
+ return ((Container.Ordered) items).firstItemId();
+ }
+
+ /** Gets the ID of the last Item in the Container.
+ *
+ * @return the object
+ * @see com.vaadin.data.Container.Ordered#lastItemId()
+ */
+
+ @Override
+ public Object lastItemId() {
+ return ((Container.Ordered) items).lastItemId();
+ }
+
+ /** Tests if the Item corresponding to the given Item ID is the first
+ * Item in the Container.
+ *
+ * @param itemId
+ * the item id
+ * @return true, if is first id
+ * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isFirstId(Object itemId) {
+ return ((Container.Ordered) items).isFirstId(itemId);
+ }
+
+ /** Tests if the Item corresponding to the given Item ID is the last Item
+ * in the Container.
+ *
+ * @param itemId
+ * the item id
+ * @return true, if is last id
+ * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ return ((Container.Ordered) items).isLastId(itemId);
+ }
+
+ /** Adds new item after the given item.
+ *
+ * @param previousItemId
+ * the previous item id
+ * @return the object
+ * @throws UnsupportedOperationException
+ * the unsupported operation exception
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+ */
+
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ Object itemId = ((Container.Ordered) items)
+ .addItemAfter(previousItemId);
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return itemId;
+ }
+
+ /** Adds new item after the given item.
+ *
+ * @param previousItemId
+ * the previous item id
+ * @param newItemId
+ * the new item id
+ * @return the item
+ * @throws UnsupportedOperationException
+ * the unsupported operation exception
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
+ * java.lang.Object)
+ */
+
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ Item item = ((Container.Ordered) items).addItemAfter(previousItemId,
+ newItemId);
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return item;
+ }
+
+ /**
+ * Sets the TableFieldFactory that is used to create editor for table cells.
+ *
+ * The TableFieldFactory is only used if the CustomTable is editable. By
+ * default the DefaultFieldFactory is used.
+ *
+ * @param fieldFactory
+ * the field factory to set.
+ * @see #isEditable
+ * @see DefaultFieldFactory
+ */
+ public void setTableFieldFactory(TableFieldFactory fieldFactory) {
+ this.fieldFactory = fieldFactory;
+
+ // Assure visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Gets the TableFieldFactory that is used to create editor for table cells.
+ *
+ * The FieldFactory is only used if the CustomTable is editable.
+ *
+ * @return TableFieldFactory used to create the Field instances.
+ * @see #isEditable
+ */
+ public TableFieldFactory getTableFieldFactory() {
+ return fieldFactory;
+ }
+
+ /**
+ * Is table editable.
+ *
+ * If table is editable a editor of type Field is created for each table
+ * cell. The assigned FieldFactory is used to create the instances.
+ *
+ * To provide custom editors for table cells create a class implementins the
+ * FieldFactory interface, and assign it to table, and set the editable
+ * property to true.
+ *
+ * @return true if table is editable, false oterwise.
+ * @see Field
+ * @see fieldFactory
+ *
+ */
+ public boolean isEditable() {
+ return editable;
+ }
+
+ /**
+ * Sets the editable property.
+ *
+ * If table is editable a editor of type Field is created for each table
+ * cell. The assigned FieldFactory is used to create the instances.
+ *
+ * To provide custom editors for table cells create a class implementins the
+ * FieldFactory interface, and assign it to table, and set the editable
+ * property to true.
+ *
+ * @param editable
+ * true if table should be editable by user.
+ * @see Field
+ * @see fieldFactory
+ *
+ */
+ public void setEditable(boolean editable) {
+ this.editable = editable;
+
+ // Assure visual refresh
+ refreshRowCache();
+ }
+
+ /** Sorts the table.
+ *
+ * @param propertyId
+ * the property id
+ * @param ascending
+ * the ascending
+ * @throws UnsupportedOperationException
+ * if the container data source does not implement
+ * Container.Sortable
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ */
+
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending)
+ throws UnsupportedOperationException {
+ final Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable) {
+ final int pageIndex = getCurrentPageFirstItemIndex();
+ boolean refreshingPreviouslyEnabled = disableContentRefreshing();
+ ((Container.Sortable) c).sort(propertyId, ascending);
+ setCurrentPageFirstItemIndex(pageIndex);
+ if (refreshingPreviouslyEnabled) {
+ enableContentRefreshing(true);
+ }
+ if (propertyId.length > 0 && ascending.length > 0) {
+ // The first propertyId is the primary sorting criterion,
+ // therefore the sort indicator should be there
+ sortAscending = ascending[0];
+ sortContainerPropertyId = propertyId[0];
+ } else {
+ sortAscending = true;
+ sortContainerPropertyId = null;
+ }
+ } else if (c != null) {
+ throw new UnsupportedOperationException(
+ "Underlying Data does not allow sorting");
+ }
+ }
+
+ /**
+ * Sorts the table by currently selected sorting column.
+ *
+ * @throws UnsupportedOperationException
+ * if the container data source does not implement
+ * Container.Sortable
+ */
+ public void sort() {
+ if (getSortContainerPropertyId() == null) {
+ return;
+ }
+ sort(new Object[] { sortContainerPropertyId },
+ new boolean[] { sortAscending });
+ }
+
+ /** Gets the container property IDs, which can be used to sort the item.
+ * <p>
+ * Note that the {@link #isSortEnabled()} state affects what this method
+ * returns. Disabling sorting causes this method to always return an empty
+ * collection.
+ *
+ * @return the sortable container property ids
+ * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds()
+ */
+
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ final Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable && isSortEnabled()) {
+ return ((Container.Sortable) c).getSortableContainerPropertyIds();
+ } else {
+ return Collections.EMPTY_LIST;
+ }
+ }
+
+ /**
+ * Gets the currently sorted column property ID.
+ *
+ * @return the Container property id of the currently sorted column.
+ */
+ public Object getSortContainerPropertyId() {
+ return sortContainerPropertyId;
+ }
+
+ /**
+ * Sets the currently sorted column property id.
+ *
+ * @param propertyId
+ * the Container property id of the currently sorted column.
+ */
+ public void setSortContainerPropertyId(Object propertyId) {
+ setSortContainerPropertyId(propertyId, true);
+ }
+
+ /** Internal method to set currently sorted column property id. With
+ * doSort flag actual sorting may be bypassed.
+ *
+ * @param propertyId
+ * the property id
+ * @param doSort
+ * the do sort
+ */
+ private void setSortContainerPropertyId(Object propertyId, boolean doSort) {
+ if ((sortContainerPropertyId != null && !sortContainerPropertyId
+ .equals(propertyId))
+ || (sortContainerPropertyId == null && propertyId != null)) {
+ sortContainerPropertyId = propertyId;
+
+ if (doSort) {
+ sort();
+ // Assures the visual refresh. This should not be necessary as
+ // sort() calls refreshRowCache
+ refreshRenderedCells();
+ }
+ }
+ }
+
+ /**
+ * Is the table currently sorted in ascending order.
+ *
+ * @return <code>true</code> if ascending, <code>false</code> if descending.
+ */
+ public boolean isSortAscending() {
+ return sortAscending;
+ }
+
+ /**
+ * Sets the table in ascending order.
+ *
+ * @param ascending
+ * <code>true</code> if ascending, <code>false</code> if
+ * descending.
+ */
+ public void setSortAscending(boolean ascending) {
+ setSortAscending(ascending, true);
+ }
+
+ /** Internal method to set sort ascending. With doSort flag actual sort
+ * can be bypassed.
+ *
+ * @param ascending
+ * the ascending
+ * @param doSort
+ * the do sort
+ */
+ private void setSortAscending(boolean ascending, boolean doSort) {
+ if (sortAscending != ascending) {
+ sortAscending = ascending;
+ if (doSort) {
+ sort();
+ // Assures the visual refresh. This should not be necessary as
+ // sort() calls refreshRowCache
+ refreshRenderedCells();
+ }
+ }
+ }
+
+ /**
+ * Is sorting disabled altogether.
+ *
+ * True iff no sortable columns are given even in the case where data source
+ * would support this.
+ *
+ * @return True iff sorting is disabled.
+ * @deprecated As of 7.0, use {@link #isSortEnabled()} instead
+ */
+ @Deprecated
+ public boolean isSortDisabled() {
+ return !isSortEnabled();
+ }
+
+ /**
+ * Checks if sorting is enabled.
+ *
+ * @return true if sorting by the user is allowed, false otherwise
+ */
+ public boolean isSortEnabled() {
+ return sortEnabled;
+ }
+
+ /**
+ * Disables the sorting by the user altogether.
+ *
+ * @param sortDisabled
+ * True iff sorting is disabled.
+ * @deprecated As of 7.0, use {@link #setSortEnabled(boolean)} instead
+ */
+ @Deprecated
+ public void setSortDisabled(boolean sortDisabled) {
+ setSortEnabled(!sortDisabled);
+ }
+
+ /**
+ * Enables or disables sorting.
+ * <p>
+ * Setting this to false disallows sorting by the user. It is still possible
+ * to call {@link #sort()}.
+ *
+ *
+ * @param sortEnabled
+ * true to allow the user to sort the table, false to disallow it
+ */
+ public void setSortEnabled(boolean sortEnabled) {
+ if (this.sortEnabled != sortEnabled) {
+ this.sortEnabled = sortEnabled;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Used to create "generated columns"; columns that exist only in the
+ * CustomTable, not in the underlying Container. Implement this interface
+ * and pass it to CustomTable.addGeneratedColumn along with an id for the
+ * column to be generated.
+ *
+ */
+ public interface ColumnGenerator extends Serializable {
+
+ /**
+ * Called by CustomTable when a cell in a generated column needs to be
+ * generated.
+ *
+ * @param source
+ * the source CustomTable
+ * @param itemId
+ * the itemId (aka rowId) for the of the cell to be generated
+ * @param columnId
+ * the id for the generated column (as specified in
+ * addGeneratedColumn)
+ * @return A {@link Component} that should be rendered in the cell or a
+ * {@link String} that should be displayed in the cell. Other
+ * return values are not supported.
+ */
+ public abstract Object generateCell(CustomTable source, Object itemId,
+ Object columnId);
+ }
+
+ /**
+ * Set cell style generator for CustomTable.
+ *
+ * @param cellStyleGenerator
+ * New cell style generator or null to remove generator.
+ */
+ public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) {
+ this.cellStyleGenerator = cellStyleGenerator;
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the style generators
+ refreshRenderedCells();
+
+ }
+
+ /** Get the current cell style generator.
+ *
+ * @return the customTable cell specific style generator
+ */
+ public CellStyleGenerator getCellStyleGenerator() {
+ return cellStyleGenerator;
+ }
+
+ /**
+ * Allow to define specific style on cells (and rows) contents. Implements
+ * this interface and pass it to CustomTable.setCellStyleGenerator. Row
+ * styles are generated when porpertyId is null. The CSS class name that
+ * will be added to the cell content is
+ * <tt>v-table-cell-content-[style name]</tt>, and the row style will be
+ * <tt>v-table-row-[style name]</tt>.
+ */
+ public interface CellStyleGenerator extends Serializable {
+
+ /**
+ * Called by CustomTable when a cell (and row) is painted.
+ *
+ * @param source
+ * the source CustomTable
+ * @param itemId
+ * The itemId of the painted cell
+ * @param propertyId
+ * The propertyId of the cell, null when getting row style
+ * @return The style name to add to this cell or row. (the CSS class
+ * name will be v-table-cell-content-[style name], or
+ * v-table-row-[style name] for rows)
+ */
+ public abstract String getStyle(CustomTable source, Object itemId,
+ Object propertyId);
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.event.ItemClickEvent.ItemClickNotifier#addItemClickListener(com.vaadin.event.ItemClickEvent.ItemClickListener)
+ */
+ @Override
+ public void addItemClickListener(ItemClickListener listener) {
+ addListener(TableConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener, ItemClickEvent.ITEM_CLICK_METHOD);
+ }
+
+ /** Adds the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #addItemClickListener(ItemClickListener)}
+ */
+ @Override
+ @Deprecated
+ public void addListener(ItemClickListener listener) {
+ addItemClickListener(listener);
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.event.ItemClickEvent.ItemClickNotifier#removeItemClickListener(com.vaadin.event.ItemClickEvent.ItemClickListener)
+ */
+ @Override
+ public void removeItemClickListener(ItemClickListener listener) {
+ removeListener(TableConstants.ITEM_CLICK_EVENT_ID,
+ ItemClickEvent.class, listener);
+ }
+
+ /** Removes the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeItemClickListener(ItemClickListener)}
+ */
+ @Override
+ @Deprecated
+ public void removeListener(ItemClickListener listener) {
+ removeItemClickListener(listener);
+ }
+
+ // Identical to AbstractCompoenentContainer.setEnabled();
+
+ /* (non-Javadoc)
+ * @see com.vaadin.ui.AbstractComponent#setEnabled(boolean)
+ */
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (getParent() != null && !getParent().isEnabled()) {
+ // some ancestor still disabled, don't update children
+ return;
+ } else {
+ markAsDirtyRecursive();
+ }
+ }
+
+ /** Sets the drag start mode of the CustomTable. Drag start mode controls
+ * how CustomTable behaves as a drag source.
+ *
+ * @param newDragMode
+ * the new drag mode
+ */
+ public void setDragMode(TableDragMode newDragMode) {
+ dragMode = newDragMode;
+ markAsDirty();
+ }
+
+ /** Gets the drag mode.
+ *
+ * @return the current start mode of the CustomTable. Drag start mode
+ * controls how CustomTable behaves as a drag source.
+ */
+ public TableDragMode getDragMode() {
+ return dragMode;
+ }
+
+ /**
+ * Concrete implementation of {@link DataBoundTransferable} for data
+ * transferred from a table.
+ *
+ * @see DataBoundTransferable
+ *
+ * @since 6.3
+ */
+ public class TableTransferable extends DataBoundTransferable {
+
+ /** Instantiates a new table transferable.
+ *
+ * @param rawVariables
+ * the raw variables
+ */
+ protected TableTransferable(Map<String, Object> rawVariables) {
+ super(CustomTable.this, rawVariables);
+ Object object = rawVariables.get("itemId");
+ if (object != null) {
+ setData("itemId", itemIdMapper.get((String) object));
+ }
+ object = rawVariables.get("propertyId");
+ if (object != null) {
+ setData("propertyId", columnIdMap.get((String) object));
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.event.DataBoundTransferable#getItemId()
+ */
+ @Override
+ public Object getItemId() {
+ return getData("itemId");
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.event.DataBoundTransferable#getPropertyId()
+ */
+ @Override
+ public Object getPropertyId() {
+ return getData("propertyId");
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.event.TransferableImpl#getSourceComponent()
+ */
+ @Override
+ public CustomTable getSourceComponent() {
+ return (CustomTable) super.getSourceComponent();
+ }
+
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.event.dd.DragSource#getTransferable(java.util.Map)
+ */
+ @Override
+ public TableTransferable getTransferable(Map<String, Object> rawVariables) {
+ TableTransferable transferable = new TableTransferable(rawVariables);
+ return transferable;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.event.dd.DropTarget#getDropHandler()
+ */
+ @Override
+ public DropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ /** Sets the drop handler.
+ *
+ * @param dropHandler
+ * the new drop handler
+ */
+ public void setDropHandler(DropHandler dropHandler) {
+ this.dropHandler = dropHandler;
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.event.dd.DropTarget#translateDropTargetDetails(java.util.Map)
+ */
+ @Override
+ public AbstractSelectTargetDetails translateDropTargetDetails(
+ Map<String, Object> clientVariables) {
+ return new AbstractSelectTargetDetails(clientVariables);
+ }
+
+ /**
+ * Sets the behavior of how the multi-select mode should behave when the
+ * table is both selectable and in multi-select mode.
+ * <p>
+ * Note, that on some clients the mode may not be respected. E.g. on touch
+ * based devices CTRL/SHIFT base selection method is invalid, so touch based
+ * browsers always use the {@link MultiSelectMode#SIMPLE}.
+ *
+ * @param mode
+ * The select mode of the table
+ */
+ public void setMultiSelectMode(MultiSelectMode mode) {
+ multiSelectMode = mode;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the select mode in which multi-select is used.
+ *
+ * @return The multi select mode
+ */
+ public MultiSelectMode getMultiSelectMode() {
+ return multiSelectMode;
+ }
+
+ /**
+ * Lazy loading accept criterion for CustomTable. Accepted target rows are
+ * loaded from server once per drag and drop operation. Developer must
+ * override one method that decides on which rows the currently dragged data
+ * can be dropped.
+ *
+ * <p>
+ * Initially pretty much no data is sent to client. On first required
+ * criterion check (per drag request) the client side data structure is
+ * initialized from server and no subsequent requests requests are needed
+ * during that drag and drop operation.
+ */
+ public static abstract class TableDropCriterion extends ServerSideCriterion {
+
+ /** The table. */
+ private CustomTable table;
+
+ /** The allowed item ids. */
+ private Set<Object> allowedItemIds;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptcriteria.ServerSideCriterion#getIdentifier
+ * ()
+ */
+
+ @Override
+ protected String getIdentifier() {
+ return TableDropCriterion.class.getCanonicalName();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#accepts(com.vaadin
+ * .event.dd.DragAndDropEvent)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean accept(DragAndDropEvent dragEvent) {
+ AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent
+ .getTargetDetails();
+ table = (CustomTable) dragEvent.getTargetDetails().getTarget();
+ Collection<?> visibleItemIds = table.getVisibleItemIds();
+ allowedItemIds = getAllowedItemIds(dragEvent, table,
+ (Collection<Object>) visibleItemIds);
+
+ return allowedItemIds.contains(dropTargetData.getItemIdOver());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#paintResponse(
+ * com.vaadin.server.PaintTarget)
+ */
+
+ @Override
+ public void paintResponse(PaintTarget target) throws PaintException {
+ /*
+ * send allowed nodes to client so subsequent requests can be
+ * avoided
+ */
+ Object[] array = allowedItemIds.toArray();
+ for (int i = 0; i < array.length; i++) {
+ String key = table.itemIdMapper.key(array[i]);
+ array[i] = key;
+ }
+ target.addAttribute("allowedIds", array);
+ }
+
+ /** Gets the allowed item ids.
+ *
+ * @param dragEvent
+ * the drag event
+ * @param table
+ * the table for which the allowed item identifiers are
+ * defined
+ * @param visibleItemIds
+ * the list of currently rendered item identifiers, accepted
+ * item id's need to be detected only for these visible items
+ * @return the set of identifiers for items on which the dragEvent will
+ * be accepted
+ */
+ protected abstract Set<Object> getAllowedItemIds(
+ DragAndDropEvent dragEvent, CustomTable table,
+ Collection<Object> visibleItemIds);
+
+ }
+
+ /**
+ * Click event fired when clicking on the CustomTable headers. The event
+ * includes a reference the the CustomTable the event originated from, the
+ * property id of the column which header was pressed and details about the
+ * mouse event itself.
+ */
+ public static class HeaderClickEvent extends ClickEvent {
+
+ /** The Constant HEADER_CLICK_METHOD. */
+ public static final Method HEADER_CLICK_METHOD;
+
+ static {
+ try {
+ // Set the header click method
+ HEADER_CLICK_METHOD = HeaderClickListener.class
+ .getDeclaredMethod("headerClick",
+ new Class[] { HeaderClickEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ /** The column property id. */
+ // The property id of the column which header was pressed
+ private final Object columnPropertyId;
+
+ /** Instantiates a new header click event.
+ *
+ * @param source
+ * the source
+ * @param propertyId
+ * the property id
+ * @param details
+ * the details
+ */
+ public HeaderClickEvent(Component source, Object propertyId,
+ MouseEventDetails details) {
+ super(source, details);
+ columnPropertyId = propertyId;
+ }
+
+ /** Gets the property id of the column which header was pressed.
+ *
+ * @return The column propety id
+ */
+ public Object getPropertyId() {
+ return columnPropertyId;
+ }
+ }
+
+ /**
+ * Click event fired when clicking on the CustomTable footers. The event
+ * includes a reference the the CustomTable the event originated from, the
+ * property id of the column which header was pressed and details about the
+ * mouse event itself.
+ */
+ public static class FooterClickEvent extends ClickEvent {
+
+ /** The Constant FOOTER_CLICK_METHOD. */
+ public static final Method FOOTER_CLICK_METHOD;
+
+ static {
+ try {
+ // Set the header click method
+ FOOTER_CLICK_METHOD = FooterClickListener.class
+ .getDeclaredMethod("footerClick",
+ new Class[] { FooterClickEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ /** The column property id. */
+ // The property id of the column which header was pressed
+ private final Object columnPropertyId;
+
+ /** Constructor.
+ *
+ * @param source
+ * The source of the component
+ * @param propertyId
+ * The propertyId of the column
+ * @param details
+ * The mouse details of the click
+ */
+ public FooterClickEvent(Component source, Object propertyId,
+ MouseEventDetails details) {
+ super(source, details);
+ columnPropertyId = propertyId;
+ }
+
+ /** Gets the property id of the column which header was pressed.
+ *
+ * @return The column propety id
+ */
+ public Object getPropertyId() {
+ return columnPropertyId;
+ }
+ }
+
+ /** Interface for the listener for column header mouse click events. The
+ * headerClick method is called when the user presses a header column cell.
+ *
+ * @see HeaderClickEvent
+ */
+ public interface HeaderClickListener extends Serializable {
+
+ /** Called when a user clicks a header column cell.
+ *
+ * @param event
+ * The event which contains information about the column and
+ * the mouse click event
+ */
+ public void headerClick(HeaderClickEvent event);
+ }
+
+ /** Interface for the listener for column footer mouse click events. The
+ * footerClick method is called when the user presses a footer column cell.
+ *
+ * @see FooterClickEvent
+ */
+ public interface FooterClickListener extends Serializable {
+
+ /** Called when a user clicks a footer column cell.
+ *
+ * @param event
+ * The event which contains information about the column and
+ * the mouse click event
+ */
+ public void footerClick(FooterClickEvent event);
+ }
+
+ /**
+ * Adds a header click listener which handles the click events when the user
+ * clicks on a column header cell in the CustomTable.
+ * <p>
+ * The listener will receive events which contain information about which
+ * column was clicked and some details about the mouse event.
+ *
+ *
+ * @param listener
+ * The handler which should handle the header click events.
+ */
+ public void addHeaderClickListener(HeaderClickListener listener) {
+ addListener(TableConstants.HEADER_CLICK_EVENT_ID,
+ HeaderClickEvent.class, listener,
+ HeaderClickEvent.HEADER_CLICK_METHOD);
+ }
+
+ /** Adds the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #addHeaderClickListener(HeaderClickListener)}
+ */
+ @Deprecated
+ public void addListener(HeaderClickListener listener) {
+ addHeaderClickListener(listener);
+ }
+
+ /** Removes a header click listener.
+ *
+ * @param listener
+ * The listener to remove.
+ */
+ public void removeHeaderClickListener(HeaderClickListener listener) {
+ removeListener(TableConstants.HEADER_CLICK_EVENT_ID,
+ HeaderClickEvent.class, listener);
+ }
+
+ /** Removes the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeHeaderClickListener(HeaderClickListener)}
+ */
+ @Deprecated
+ public void removeListener(HeaderClickListener listener) {
+ removeHeaderClickListener(listener);
+ }
+
+ /**
+ * Adds a footer click listener which handles the click events when the user
+ * clicks on a column footer cell in the CustomTable.
+ * <p>
+ * The listener will receive events which contain information about which
+ * column was clicked and some details about the mouse event.
+ *
+ *
+ * @param listener
+ * The handler which should handle the footer click events.
+ */
+ public void addFooterClickListener(FooterClickListener listener) {
+ addListener(TableConstants.FOOTER_CLICK_EVENT_ID,
+ FooterClickEvent.class, listener,
+ FooterClickEvent.FOOTER_CLICK_METHOD);
+ }
+
+ /** Adds the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #addFooterClickListener(FooterClickListener)}
+ */
+ @Deprecated
+ public void addListener(FooterClickListener listener) {
+ addFooterClickListener(listener);
+ }
+
+ /** Removes a footer click listener.
+ *
+ * @param listener
+ * The listener to remove.
+ */
+ public void removeFooterClickListener(FooterClickListener listener) {
+ removeListener(TableConstants.FOOTER_CLICK_EVENT_ID,
+ FooterClickEvent.class, listener);
+ }
+
+ /** Removes the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeFooterClickListener(FooterClickListener)}
+ */
+ @Deprecated
+ public void removeListener(FooterClickListener listener) {
+ removeFooterClickListener(listener);
+ }
+
+ /** Gets the footer caption beneath the rows.
+ *
+ * @param propertyId
+ * The propertyId of the column *
+ * @return The caption of the footer or NULL if not set
+ */
+ public String getColumnFooter(Object propertyId) {
+ return columnFooters.get(propertyId);
+ }
+
+ /**
+ * Sets the column footer caption. The column footer caption is the text
+ * displayed beneath the column if footers have been set visible.
+ *
+ * @param propertyId
+ * The properyId of the column
+ *
+ * @param footer
+ * The caption of the footer
+ */
+ public void setColumnFooter(Object propertyId, String footer) {
+ if (footer == null) {
+ columnFooters.remove(propertyId);
+ } else {
+ columnFooters.put(propertyId, footer);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Sets the footer visible in the bottom of the table.
+ * <p>
+ * The footer can be used to add column related data like sums to the bottom
+ * of the CustomTable using setColumnFooter(Object propertyId, String
+ * footer).
+ *
+ *
+ * @param visible
+ * Should the footer be visible
+ */
+ public void setFooterVisible(boolean visible) {
+ if (visible != columnFootersVisible) {
+ columnFootersVisible = visible;
+ markAsDirty();
+ }
+ }
+
+ /** Is the footer currently visible?.
+ *
+ * @return Returns true if visible else false
+ */
+ public boolean isFooterVisible() {
+ return columnFootersVisible;
+ }
+
+ /**
+ * This event is fired when a column is resized. The event contains the
+ * columns property id which was fired, the previous width of the column and
+ * the width of the column after the resize.
+ */
+ public static class ColumnResizeEvent extends Component.Event {
+
+ /** The Constant COLUMN_RESIZE_METHOD. */
+ public static final Method COLUMN_RESIZE_METHOD;
+
+ static {
+ try {
+ COLUMN_RESIZE_METHOD = ColumnResizeListener.class
+ .getDeclaredMethod("columnResize",
+ new Class[] { ColumnResizeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ /** The previous width. */
+ private final int previousWidth;
+
+ /** The current width. */
+ private final int currentWidth;
+
+ /** The column property id. */
+ private final Object columnPropertyId;
+
+ /** Constructor.
+ *
+ * @param source
+ * The source of the event
+ * @param propertyId
+ * The columns property id
+ * @param previous
+ * The width in pixels of the column before the resize event
+ * @param current
+ * The width in pixels of the column after the resize event
+ */
+ public ColumnResizeEvent(Component source, Object propertyId,
+ int previous, int current) {
+ super(source);
+ previousWidth = previous;
+ currentWidth = current;
+ columnPropertyId = propertyId;
+ }
+
+ /**
+ * Get the column property id of the column that was resized.
+ *
+ * @return The column property id
+ */
+ public Object getPropertyId() {
+ return columnPropertyId;
+ }
+
+ /** Get the width in pixels of the column before the resize event.
+ *
+ * @return Width in pixels
+ */
+ public int getPreviousWidth() {
+ return previousWidth;
+ }
+
+ /** Get the width in pixels of the column after the resize event.
+ *
+ * @return Width in pixels
+ */
+ public int getCurrentWidth() {
+ return currentWidth;
+ }
+ }
+
+ /** Interface for listening to column resize events.
+ *
+ * @see ColumnResizeEvent
+ */
+ public interface ColumnResizeListener extends Serializable {
+
+ /** This method is triggered when the column has been resized.
+ *
+ * @param event
+ * The event which contains the column property id, the
+ * previous width of the column and the current width of the
+ * column
+ */
+ public void columnResize(ColumnResizeEvent event);
+ }
+
+ /**
+ * Adds a column resize listener to the CustomTable. A column resize
+ * listener is called when a user resizes a columns width.
+ *
+ * @param listener
+ * The listener to attach to the CustomTable
+ */
+ public void addColumnResizeListener(ColumnResizeListener listener) {
+ addListener(TableConstants.COLUMN_RESIZE_EVENT_ID,
+ ColumnResizeEvent.class, listener,
+ ColumnResizeEvent.COLUMN_RESIZE_METHOD);
+ }
+
+ /** Adds the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #addColumnResizeListener(ColumnResizeListener)}
+ */
+ @Deprecated
+ public void addListener(ColumnResizeListener listener) {
+ addColumnResizeListener(listener);
+ }
+
+ /**
+ * Removes a column resize listener from the CustomTable.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeColumnResizeListener(ColumnResizeListener listener) {
+ removeListener(TableConstants.COLUMN_RESIZE_EVENT_ID,
+ ColumnResizeEvent.class, listener);
+ }
+
+ /** Removes the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeColumnResizeListener(ColumnResizeListener)}
+ */
+ @Deprecated
+ public void removeListener(ColumnResizeListener listener) {
+ removeColumnResizeListener(listener);
+ }
+
+ /**
+ * This event is fired when a columns are reordered by the end user user.
+ */
+ public static class ColumnReorderEvent extends Component.Event {
+
+ /** The Constant METHOD. */
+ public static final Method METHOD;
+
+ static {
+ try {
+ METHOD = ColumnReorderListener.class.getDeclaredMethod(
+ "columnReorder",
+ new Class[] { ColumnReorderEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ /** Constructor.
+ *
+ * @param source
+ * The source of the event
+ */
+ public ColumnReorderEvent(Component source) {
+ super(source);
+ }
+
+ }
+
+ /** Interface for listening to column reorder events.
+ *
+ * @see ColumnReorderEvent
+ */
+ public interface ColumnReorderListener extends Serializable {
+
+ /** This method is triggered when the column has been reordered.
+ *
+ * @param event
+ * the event
+ */
+ public void columnReorder(ColumnReorderEvent event);
+ }
+
+ /**
+ * Adds a column reorder listener to the CustomTable. A column reorder
+ * listener is called when a user reorders columns.
+ *
+ * @param listener
+ * The listener to attach to the CustomTable
+ */
+ public void addColumnReorderListener(ColumnReorderListener listener) {
+ addListener(TableConstants.COLUMN_REORDER_EVENT_ID,
+ ColumnReorderEvent.class, listener, ColumnReorderEvent.METHOD);
+ }
+
+ /** Adds the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #addColumnReorderListener(ColumnReorderListener)}
+ */
+ @Deprecated
+ public void addListener(ColumnReorderListener listener) {
+ addColumnReorderListener(listener);
+ }
+
+ /**
+ * Removes a column reorder listener from the CustomTable.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeColumnReorderListener(ColumnReorderListener listener) {
+ removeListener(TableConstants.COLUMN_REORDER_EVENT_ID,
+ ColumnReorderEvent.class, listener);
+ }
+
+ /** Removes the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeColumnReorderListener(ColumnReorderListener)}
+ */
+ @Deprecated
+ public void removeListener(ColumnReorderListener listener) {
+ removeColumnReorderListener(listener);
+ }
+
+ /** Set the item description generator which generates tooltips for cells
+ * and rows in the CustomTable.
+ *
+ * @param generator
+ * The generator to use or null to disable
+ */
+ public void setItemDescriptionGenerator(ItemDescriptionGenerator generator) {
+ if (generator != itemDescriptionGenerator) {
+ itemDescriptionGenerator = generator;
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the descriptions
+ refreshRenderedCells();
+ }
+ }
+
+ /** Get the item description generator which generates tooltips for cells
+ * and rows in the CustomTable.
+ *
+ * @return the customTable cell specific tooltip generator
+ */
+ public ItemDescriptionGenerator getItemDescriptionGenerator() {
+ return itemDescriptionGenerator;
+ }
+
+ /**
+ * Row generators can be used to replace certain items in a table with a
+ * generated string. The generator is called each time the table is
+ * rendered, which means that new strings can be generated each time.
+ *
+ * Row generators can be used for e.g. summary rows or grouping of items.
+ */
+ public interface RowGenerator extends Serializable {
+ /**
+ * Called for every row that is painted in the CustomTable. Returning a
+ * GeneratedRow object will cause the row to be painted based on the
+ * contents of the GeneratedRow. A generated row is by default styled
+ * similarly to a header or footer row.
+ * <p>
+ * The GeneratedRow data object contains the text that should be
+ * rendered in the row. The itemId in the container thus works only as a
+ * placeholder.
+ * <p>
+ * If GeneratedRow.setSpanColumns(true) is used, there will be one
+ * String spanning all columns (use setText("Spanning text")). Otherwise
+ * you can define one String per visible column.
+ * <p>
+ * If GeneratedRow.setRenderAsHtml(true) is used, the strings can
+ * contain HTML markup, otherwise all strings will be rendered as text
+ * (the default).
+ * <p>
+ * A "v-table-generated-row" CSS class is added to all generated rows.
+ * For custom styling of a generated row you can combine a RowGenerator
+ * with a CellStyleGenerator.
+ * <p>
+ *
+ * @param table
+ * The CustomTable that is being painted
+ * @param itemId
+ * The itemId for the row
+ * @return A GeneratedRow describing how the row should be painted or
+ * null to paint the row with the contents from the container
+ */
+ public GeneratedRow generateRow(CustomTable table, Object itemId);
+ }
+
+ /** The Class GeneratedRow.
+ */
+ public static class GeneratedRow implements Serializable {
+
+ /** The html content allowed. */
+ private boolean htmlContentAllowed = false;
+
+ /** The span columns. */
+ private boolean spanColumns = false;
+
+ /** The text. */
+ private String[] text = null;
+
+ /** Creates a new generated row. If only one string is passed in,
+ * columns are automatically spanned.
+ *
+ * @param text
+ * the text
+ */
+ public GeneratedRow(String... text) {
+ setHtmlContentAllowed(false);
+ setSpanColumns(text == null || text.length == 1);
+ setText(text);
+ }
+
+ /** Pass one String if spanColumns is used, one String for each
+ * visible column otherwise.
+ *
+ * @param text
+ * the new text
+ */
+ public void setText(String... text) {
+ if (text == null || (text.length == 1 && text[0] == null)) {
+ text = new String[] { "" };
+ }
+ this.text = text;
+ }
+
+ /** Gets the text.
+ *
+ * @return the text
+ */
+ protected String[] getText() {
+ return text;
+ }
+
+ /** Gets the value.
+ *
+ * @return the value
+ */
+ protected Object getValue() {
+ return getText();
+ }
+
+ /** Checks if is html content allowed.
+ *
+ * @return true, if is html content allowed
+ */
+ protected boolean isHtmlContentAllowed() {
+ return htmlContentAllowed;
+ }
+
+ /** If set to true, all strings passed to {@link #setText(String...)}
+ * will be rendered as HTML.
+ *
+ * @param htmlContentAllowed
+ * the new html content allowed
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ this.htmlContentAllowed = htmlContentAllowed;
+ }
+
+ /** Checks if is span columns.
+ *
+ * @return true, if is span columns
+ */
+ protected boolean isSpanColumns() {
+ return spanColumns;
+ }
+
+ /** If set to true, only one string will be rendered, spanning the
+ * entire row.
+ *
+ * @param spanColumns
+ * the new span columns
+ */
+ public void setSpanColumns(boolean spanColumns) {
+ this.spanColumns = spanColumns;
+ }
+ }
+
+ /**
+ * Assigns a row generator to the table. The row generator will be able to
+ * replace rows in the table when it is rendered.
+ *
+ * @param generator
+ * the new row generator
+ */
+ public void setRowGenerator(RowGenerator generator) {
+ rowGenerator = generator;
+ refreshRowCache();
+ }
+
+ /** Gets the row generator.
+ *
+ * @return the current row generator
+ */
+ public RowGenerator getRowGenerator() {
+ return rowGenerator;
+ }
+
+ /**
+ * Sets a converter for a property id.
+ * <p>
+ * The converter is used to format the the data for the given property id
+ * before displaying it in the table.
+ *
+ *
+ * @param propertyId
+ * The propertyId to format using the converter
+ * @param converter
+ * The converter to use for the property id
+ */
+ public void setConverter(Object propertyId, Converter<String, ?> converter) {
+ if (!getContainerPropertyIds().contains(propertyId)) {
+ throw new IllegalArgumentException("PropertyId " + propertyId
+ + " must be in the container");
+ }
+
+ if (!typeIsCompatible(converter.getModelType(), getType(propertyId))) {
+ throw new IllegalArgumentException("Property type ("
+ + getType(propertyId)
+ + ") must match converter source type ("
+ + converter.getModelType() + ")");
+ }
+ propertyValueConverters.put(propertyId,
+ (Converter<String, Object>) converter);
+ refreshRowCache();
+ }
+
+ /**
+ * Checks if there is a converter set explicitly for the given property id.
+ *
+ * @param propertyId
+ * The propertyId to check
+ * @return true if a converter has been set for the property id, false
+ * otherwise
+ */
+ protected boolean hasConverter(Object propertyId) {
+ return propertyValueConverters.containsKey(propertyId);
+ }
+
+ /**
+ * Returns the converter used to format the given propertyId.
+ *
+ * @param propertyId
+ * The propertyId to check
+ * @return The converter used to format the propertyId or null if no
+ * converter has been set
+ */
+ public Converter<String, Object> getConverter(Object propertyId) {
+ return propertyValueConverters.get(propertyId);
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.ui.AbstractComponent#setVisible(boolean)
+ */
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible) {
+ // We need to ensure that the rows are sent to the client when the
+ // CustomTable is made visible if it has been rendered as invisible.
+ setRowCacheInvalidated(true);
+ }
+ super.setVisible(visible);
+ }
+
+ /* (non-Javadoc)
+ * @see com.vaadin.ui.HasComponents#iterator()
+ */
+ @Override
+ public Iterator<Component> iterator() {
+ if (visibleComponents == null) {
+ Collection<Component> empty = Collections.emptyList();
+ return empty.iterator();
+ }
+ return visibleComponents.iterator();
+ }
+
+ /** Gets the component iterator.
+ *
+ * @return the component iterator
+ * @deprecated As of 7.0, use {@link #iterator()} instead.
+ */
+ @Deprecated
+ public Iterator<Component> getComponentIterator() {
+ return iterator();
+ }
+
+ /** Gets the logger.
+ *
+ * @return the logger
+ */
+ private final Logger getLogger() {
+ if (logger == null) {
+ logger = Logger.getLogger(CustomTable.class.getName());
+ }
+ return logger;
+ }
+}
diff --git a/org.eclipse.osbp.fork.vaadin.addon.filteringtable/src/com/vaadin/ui/CustomTreeTable.java b/org.eclipse.osbp.fork.vaadin.addon.filteringtable/src/com/vaadin/ui/CustomTreeTable.java
new file mode 100644
index 0000000..3fcf910
--- /dev/null
+++ b/org.eclipse.osbp.fork.vaadin.addon.filteringtable/src/com/vaadin/ui/CustomTreeTable.java
@@ -0,0 +1,894 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Collapsible;
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Hierarchical;
+import com.vaadin.data.util.ContainerHierarchicalWrapper;
+import com.vaadin.data.util.HierarchicalContainer;
+import com.vaadin.data.util.HierarchicalContainerOrderedWrapper;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.server.Resource;
+import com.vaadin.shared.ui.treetable.TreeTableConstants;
+import com.vaadin.ui.Tree.CollapseEvent;
+import com.vaadin.ui.Tree.CollapseListener;
+import com.vaadin.ui.Tree.ExpandEvent;
+import com.vaadin.ui.Tree.ExpandListener;
+
+/**
+ * TreeTable extends the {@link Table} component so that it can also visualize a
+ * hierarchy of its Items in a similar manner that {@link Tree} does. The tree
+ * hierarchy is always displayed in the first actual column of the TreeTable.
+ * <p>
+ * The TreeTable supports the usual {@link Table} features like lazy loading, so
+ * it should be no problem to display lots of items at once. Only required rows
+ * and some cache rows are sent to the client.
+ * <p>
+ * TreeTable supports standard {@link Hierarchical} container interfaces, but
+ * also a more fine tuned version - {@link Collapsible}. A container
+ * implementing the {@link Collapsible} interface stores the collapsed/expanded
+ * state internally and can this way scale better on the server side than with
+ * standard Hierarchical implementations. Developer must however note that
+ * {@link Collapsible} containers can not be shared among several users as they
+ * share UI state in the container.
+ */
+@SuppressWarnings({ "serial", "deprecation" })
+public class CustomTreeTable extends CustomTable implements Hierarchical {
+
+ private interface ContainerStrategy extends Serializable {
+ public int size();
+
+ public boolean isNodeOpen(Object itemId);
+
+ public int getDepth(Object itemId);
+
+ public void toggleChildVisibility(Object itemId);
+
+ public Object getIdByIndex(int index);
+
+ public int indexOfId(Object id);
+
+ public Object nextItemId(Object itemId);
+
+ public Object lastItemId();
+
+ public Object prevItemId(Object itemId);
+
+ public boolean isLastId(Object itemId);
+
+ public Collection<?> getItemIds();
+
+ public void containerItemSetChange(ItemSetChangeEvent event);
+ }
+
+ private abstract class AbstractStrategy implements ContainerStrategy {
+
+ /**
+ * Consider adding getDepth to {@link Collapsible}, might help
+ * scalability with some container implementations.
+ */
+
+ @Override
+ public int getDepth(Object itemId) {
+ int depth = 0;
+ Hierarchical hierarchicalContainer = getContainerDataSource();
+ while (!hierarchicalContainer.isRoot(itemId)) {
+ depth++;
+ itemId = hierarchicalContainer.getParent(itemId);
+ }
+ return depth;
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ }
+
+ }
+
+ /**
+ * This strategy is used if current container implements {@link Collapsible}
+ * .
+ *
+ * open-collapsed logic diverted to container, otherwise use default
+ * implementations.
+ */
+ private class CollapsibleStrategy extends AbstractStrategy {
+
+ private Collapsible c() {
+ return (Collapsible) getContainerDataSource();
+ }
+
+ @Override
+ public void toggleChildVisibility(Object itemId) {
+ c().setCollapsed(itemId, !c().isCollapsed(itemId));
+ }
+
+ @Override
+ public boolean isNodeOpen(Object itemId) {
+ return !c().isCollapsed(itemId);
+ }
+
+ @Override
+ public int size() {
+ return CustomTreeTable.super.size();
+ }
+
+ @Override
+ public Object getIdByIndex(int index) {
+ return CustomTreeTable.super.getIdByIndex(index);
+ }
+
+ @Override
+ public int indexOfId(Object id) {
+ return CustomTreeTable.super.indexOfId(id);
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ // using the default impl
+ return CustomTreeTable.super.isLastId(itemId);
+ }
+
+ @Override
+ public Object lastItemId() {
+ // using the default impl
+ return CustomTreeTable.super.lastItemId();
+ }
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return CustomTreeTable.super.nextItemId(itemId);
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return CustomTreeTable.super.prevItemId(itemId);
+ }
+
+ @Override
+ public Collection<?> getItemIds() {
+ return CustomTreeTable.super.getItemIds();
+ }
+
+ }
+
+ /**
+ * Strategy for Hierarchical but not Collapsible container like
+ * {@link HierarchicalContainer}.
+ *
+ * Store collapsed/open states internally, fool Table to use preorder when
+ * accessing items from container via Ordered/Indexed methods.
+ */
+ private class HierarchicalStrategy extends AbstractStrategy {
+
+ private final HashSet<Object> openItems = new HashSet<Object>();
+
+ @Override
+ public boolean isNodeOpen(Object itemId) {
+ return openItems.contains(itemId);
+ }
+
+ @Override
+ public int size() {
+ return getPreOrder().size();
+ }
+
+ @Override
+ public Collection<Object> getItemIds() {
+ return Collections.unmodifiableCollection(getPreOrder());
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+
+ return itemId.equals(lastItemId());
+ }
+
+ @Override
+ public Object lastItemId() {
+ if (getPreOrder().size() > 0) {
+ return getPreOrder().get(getPreOrder().size() - 1);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ int indexOf = getPreOrder().indexOf(itemId);
+ if (indexOf == -1) {
+ return null;
+ }
+ indexOf++;
+ if (indexOf == getPreOrder().size()) {
+ return null;
+ } else {
+ return getPreOrder().get(indexOf);
+ }
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ int indexOf = getPreOrder().indexOf(itemId);
+ indexOf--;
+ if (indexOf < 0) {
+ return null;
+ } else {
+ return getPreOrder().get(indexOf);
+ }
+ }
+
+ @Override
+ public void toggleChildVisibility(Object itemId) {
+ boolean removed = openItems.remove(itemId);
+ if (!removed) {
+ openItems.add(itemId);
+ getLogger().log(Level.FINEST, "Item {0} is now expanded",
+ itemId);
+ } else {
+ getLogger().log(Level.FINEST, "Item {0} is now collapsed",
+ itemId);
+ }
+ clearPreorderCache();
+ }
+
+ private void clearPreorderCache() {
+ preOrder = null; // clear preorder cache
+ }
+
+ List<Object> preOrder;
+
+ /**
+ * Preorder of ids currently visible
+ *
+ * @return
+ */
+ private List<Object> getPreOrder() {
+ if (preOrder == null) {
+ preOrder = new ArrayList<Object>();
+ Collection<?> rootItemIds = getContainerDataSource()
+ .rootItemIds();
+ for (Object id : rootItemIds) {
+ preOrder.add(id);
+ addVisibleChildTree(id);
+ }
+ }
+ return preOrder;
+ }
+
+ private void addVisibleChildTree(Object id) {
+ if (isNodeOpen(id)) {
+ Collection<?> children = getContainerDataSource().getChildren(
+ id);
+ if (children != null) {
+ for (Object childId : children) {
+ preOrder.add(childId);
+ addVisibleChildTree(childId);
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public int indexOfId(Object id) {
+ return getPreOrder().indexOf(id);
+ }
+
+ @Override
+ public Object getIdByIndex(int index) {
+ return getPreOrder().get(index);
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ // preorder becomes invalid on sort, item additions etc.
+ clearPreorderCache();
+ super.containerItemSetChange(event);
+ }
+
+ }
+
+ /**
+ * Creates an empty TreeTable with a default container.
+ */
+ public CustomTreeTable() {
+ super(null, new HierarchicalContainer());
+ }
+
+ /**
+ * Creates an empty TreeTable with a default container.
+ *
+ * @param caption
+ * the caption for the TreeTable
+ */
+ public CustomTreeTable(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a TreeTable instance with given captions and data source.
+ *
+ * @param caption
+ * the caption for the component
+ * @param dataSource
+ * the dataSource that is used to list items in the component
+ */
+ public CustomTreeTable(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ private ContainerStrategy cStrategy;
+ private Object focusedRowId = null;
+ private Object hierarchyColumnId;
+
+ /**
+ * The item id that was expanded or collapsed during this request. Reset at
+ * the end of paint and only used for determining if a partial or full paint
+ * should be done.
+ *
+ * Can safely be reset to null whenever a change occurs that would prevent a
+ * partial update from rendering the correct result, e.g. rows added or
+ * removed during an expand operation.
+ */
+ private Object toggledItemId;
+ private boolean animationsEnabled;
+ private boolean clearFocusedRowPending;
+
+ /**
+ * If the container does not send item set change events, always do a full
+ * repaint instead of a partial update when expanding/collapsing nodes.
+ */
+ private boolean containerSupportsPartialUpdates;
+
+ private ContainerStrategy getContainerStrategy() {
+ if (cStrategy == null) {
+ if (getContainerDataSource() instanceof Collapsible) {
+ cStrategy = new CollapsibleStrategy();
+ } else {
+ cStrategy = new HierarchicalStrategy();
+ }
+ }
+ return cStrategy;
+ }
+
+ @Override
+ protected void paintRowAttributes(PaintTarget target, Object itemId)
+ throws PaintException {
+ super.paintRowAttributes(target, itemId);
+ target.addAttribute("depth", getContainerStrategy().getDepth(itemId));
+ if (getContainerDataSource().areChildrenAllowed(itemId)) {
+ target.addAttribute("ca", true);
+ target.addAttribute("open",
+ getContainerStrategy().isNodeOpen(itemId));
+ }
+ }
+
+ @Override
+ protected void paintRowIcon(PaintTarget target, Object[][] cells,
+ int indexInRowbuffer) throws PaintException {
+ // always paint if present (in parent only if row headers visible)
+ if (getRowHeaderMode() == ROW_HEADER_MODE_HIDDEN) {
+ Resource itemIcon = getItemIcon(cells[CELL_ITEMID][indexInRowbuffer]);
+ if (itemIcon != null) {
+ target.addAttribute("icon", itemIcon);
+ }
+ } else if (cells[CELL_ICON][indexInRowbuffer] != null) {
+ target.addAttribute("icon",
+ (Resource) cells[CELL_ICON][indexInRowbuffer]);
+ }
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ super.changeVariables(source, variables);
+
+ if (variables.containsKey("toggleCollapsed")) {
+ String object = (String) variables.get("toggleCollapsed");
+ Object itemId = itemIdMapper.get(object);
+ toggledItemId = itemId;
+ toggleChildVisibility(itemId, false);
+ if (variables.containsKey("selectCollapsed")) {
+ // ensure collapsed is selected unless opened with selection
+ // head
+ if (isSelectable()) {
+ select(itemId);
+ }
+ }
+ } else if (variables.containsKey("focusParent")) {
+ String key = (String) variables.get("focusParent");
+ Object refId = itemIdMapper.get(key);
+ Object itemId = getParent(refId);
+ focusParent(itemId);
+ }
+ }
+
+ private void focusParent(Object itemId) {
+ boolean inView = false;
+ Object inPageId = getCurrentPageFirstItemId();
+ for (int i = 0; inPageId != null && i < getPageLength(); i++) {
+ if (inPageId.equals(itemId)) {
+ inView = true;
+ break;
+ }
+ inPageId = nextItemId(inPageId);
+ i++;
+ }
+ if (!inView) {
+ setCurrentPageFirstItemId(itemId);
+ }
+ // Select the row if it is selectable.
+ if (isSelectable()) {
+ if (isMultiSelect()) {
+ setValue(Collections.singleton(itemId));
+ } else {
+ setValue(itemId);
+ }
+ }
+ setFocusedRow(itemId);
+ }
+
+ private void setFocusedRow(Object itemId) {
+ focusedRowId = itemId;
+ if (focusedRowId == null) {
+ // Must still inform the client that the focusParent request has
+ // been processed
+ clearFocusedRowPending = true;
+ }
+ markAsDirty();
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (focusedRowId != null) {
+ target.addAttribute("focusedRow", itemIdMapper.key(focusedRowId));
+ focusedRowId = null;
+ } else if (clearFocusedRowPending) {
+ // Must still inform the client that the focusParent request has
+ // been processed
+ target.addAttribute("clearFocusPending", true);
+ clearFocusedRowPending = false;
+ }
+ target.addAttribute("animate", animationsEnabled);
+ if (hierarchyColumnId != null) {
+ Object[] visibleColumns2 = getVisibleColumns();
+ for (int i = 0; i < visibleColumns2.length; i++) {
+ Object object = visibleColumns2[i];
+ if (hierarchyColumnId.equals(object)) {
+ target.addAttribute(
+ TreeTableConstants.ATTRIBUTE_HIERARCHY_COLUMN_INDEX,
+ i);
+ break;
+ }
+ }
+ }
+ super.paintContent(target);
+ toggledItemId = null;
+ }
+
+ /*
+ * Override methods for partial row updates and additions when expanding /
+ * collapsing nodes.
+ */
+
+ @Override
+ protected boolean isPartialRowUpdate() {
+ return toggledItemId != null && containerSupportsPartialUpdates
+ && !isRowCacheInvalidated();
+ }
+
+ @Override
+ protected int getFirstAddedItemIndex() {
+ return indexOfId(toggledItemId) + 1;
+ }
+
+ @Override
+ protected int getAddedRowCount() {
+ return countSubNodesRecursively(getContainerDataSource(), toggledItemId);
+ }
+
+ private int countSubNodesRecursively(Hierarchical hc, Object itemId) {
+ int count = 0;
+ // we need the number of children for toggledItemId no matter if its
+ // collapsed or expanded. Other items' children are only counted if the
+ // item is expanded.
+ if (getContainerStrategy().isNodeOpen(itemId)
+ || itemId == toggledItemId) {
+ Collection<?> children = hc.getChildren(itemId);
+ if (children != null) {
+ count += children != null ? children.size() : 0;
+ for (Object id : children) {
+ count += countSubNodesRecursively(hc, id);
+ }
+ }
+ }
+ return count;
+ }
+
+ @Override
+ protected int getFirstUpdatedItemIndex() {
+ return indexOfId(toggledItemId);
+ }
+
+ @Override
+ protected int getUpdatedRowCount() {
+ return 1;
+ }
+
+ @Override
+ protected boolean shouldHideAddedRows() {
+ return !getContainerStrategy().isNodeOpen(toggledItemId);
+ }
+
+ private void toggleChildVisibility(Object itemId, boolean forceFullRefresh) {
+ getContainerStrategy().toggleChildVisibility(itemId);
+ // ensure that page still has first item in page, DON'T clear the
+ // caches.
+ setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false);
+
+ if (isCollapsed(itemId)) {
+ fireCollapseEvent(itemId);
+ } else {
+ fireExpandEvent(itemId);
+ }
+
+ if (containerSupportsPartialUpdates && !forceFullRefresh) {
+ markAsDirty();
+ } else {
+ // For containers that do not send item set change events, always do
+ // full repaint instead of partial row update.
+ refreshRowCache();
+ }
+ }
+
+ @Override
+ public int size() {
+ return getContainerStrategy().size();
+ }
+
+ @Override
+ public Hierarchical getContainerDataSource() {
+ return (Hierarchical) super.getContainerDataSource();
+ }
+
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ cStrategy = null;
+
+ // FIXME: This disables partial updates until TreeTable is fixed so it
+ // does not change component hierarchy during paint
+ containerSupportsPartialUpdates = (newDataSource instanceof ItemSetChangeNotifier) && false;
+
+ if (newDataSource != null && !(newDataSource instanceof Hierarchical)) {
+ newDataSource = new ContainerHierarchicalWrapper(newDataSource);
+ }
+
+ if (newDataSource != null && !(newDataSource instanceof Ordered)) {
+ newDataSource = new HierarchicalContainerOrderedWrapper(
+ (Hierarchical) newDataSource);
+ }
+
+ super.setContainerDataSource(newDataSource);
+ }
+
+ @Override
+ public void containerItemSetChange(
+ com.vaadin.data.Container.ItemSetChangeEvent event) {
+ // Can't do partial repaints if items are added or removed during the
+ // expand/collapse request
+ toggledItemId = null;
+ getContainerStrategy().containerItemSetChange(event);
+ super.containerItemSetChange(event);
+ }
+
+ @Override
+ protected Object getIdByIndex(int index) {
+ return getContainerStrategy().getIdByIndex(index);
+ }
+
+ @Override
+ protected int indexOfId(Object itemId) {
+ return getContainerStrategy().indexOfId(itemId);
+ }
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return getContainerStrategy().nextItemId(itemId);
+ }
+
+ @Override
+ public Object lastItemId() {
+ return getContainerStrategy().lastItemId();
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return getContainerStrategy().prevItemId(itemId);
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ return getContainerStrategy().isLastId(itemId);
+ }
+
+ @Override
+ public Collection<?> getItemIds() {
+ return getContainerStrategy().getItemIds();
+ }
+
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ return getContainerDataSource().areChildrenAllowed(itemId);
+ }
+
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+ return getContainerDataSource().getChildren(itemId);
+ }
+
+ @Override
+ public Object getParent(Object itemId) {
+ return getContainerDataSource().getParent(itemId);
+ }
+
+ @Override
+ public boolean hasChildren(Object itemId) {
+ return getContainerDataSource().hasChildren(itemId);
+ }
+
+ @Override
+ public boolean isRoot(Object itemId) {
+ return getContainerDataSource().isRoot(itemId);
+ }
+
+ @Override
+ public Collection<?> rootItemIds() {
+ return getContainerDataSource().rootItemIds();
+ }
+
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed)
+ throws UnsupportedOperationException {
+ return getContainerDataSource().setChildrenAllowed(itemId,
+ areChildrenAllowed);
+ }
+
+ @Override
+ public boolean setParent(Object itemId, Object newParentId)
+ throws UnsupportedOperationException {
+ return getContainerDataSource().setParent(itemId, newParentId);
+ }
+
+ /**
+ * Sets the Item specified by given identifier as collapsed or expanded. If
+ * the Item is collapsed, its children are not displayed to the user.
+ *
+ * @param itemId
+ * the identifier of the Item
+ * @param collapsed
+ * true if the Item should be collapsed, false if expanded
+ */
+ public void setCollapsed(Object itemId, boolean collapsed) {
+ if (isCollapsed(itemId) != collapsed) {
+ if (null == toggledItemId && !isRowCacheInvalidated()
+ && getVisibleItemIds().contains(itemId)) {
+ // optimization: partial refresh if only one item is
+ // collapsed/expanded
+ toggledItemId = itemId;
+ toggleChildVisibility(itemId, false);
+ } else {
+ // make sure a full refresh takes place - otherwise neither
+ // partial nor full repaint of table content is performed
+ toggledItemId = null;
+ toggleChildVisibility(itemId, true);
+ }
+ }
+ }
+
+ /**
+ * Checks if Item with given identifier is collapsed in the UI.
+ *
+ * <p>
+ *
+ * @param itemId
+ * the identifier of the checked Item
+ * @return true if the Item with given id is collapsed
+ * @see Collapsible#isCollapsed(Object)
+ */
+ public boolean isCollapsed(Object itemId) {
+ return !getContainerStrategy().isNodeOpen(itemId);
+ }
+
+ /** Explicitly sets the column in which the TreeTable visualizes the
+ * hierarchy. If hierarchyColumnId is not set, the hierarchy is visualized
+ * in the first visible column.
+ *
+ * @param hierarchyColumnId
+ * the new hierarchy column
+ */
+ public void setHierarchyColumn(Object hierarchyColumnId) {
+ this.hierarchyColumnId = hierarchyColumnId;
+ }
+
+ /**
+ * @return the identifier of column into which the hierarchy will be
+ * visualized or null if the column is not explicitly defined.
+ */
+ public Object getHierarchyColumnId() {
+ return hierarchyColumnId;
+ }
+
+ /**
+ * Adds an expand listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addExpandListener(ExpandListener listener) {
+ addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD);
+ }
+
+ /** Adds the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #addExpandListener(ExpandListener)}
+ */
+ @Deprecated
+ public void addListener(ExpandListener listener) {
+ addExpandListener(listener);
+ }
+
+ /**
+ * Removes an expand listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeExpandListener(ExpandListener listener) {
+ removeListener(ExpandEvent.class, listener,
+ ExpandListener.EXPAND_METHOD);
+ }
+
+ /** Removes the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeExpandListener(ExpandListener)}
+ */
+ @Deprecated
+ public void removeListener(ExpandListener listener) {
+ removeExpandListener(listener);
+ }
+
+ /**
+ * Emits an expand event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireExpandEvent(Object itemId) {
+ fireEvent(new ExpandEvent(this, itemId));
+ }
+
+ /**
+ * Adds a collapse listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addCollapseListener(CollapseListener listener) {
+ addListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /** Adds the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #addCollapseListener(CollapseListener)}
+ */
+ @Deprecated
+ public void addListener(CollapseListener listener) {
+ addCollapseListener(listener);
+ }
+
+ /**
+ * Removes a collapse listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeCollapseListener(CollapseListener listener) {
+ removeListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /** Removes the listener.
+ *
+ * @param listener
+ * the listener
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeCollapseListener(CollapseListener)}
+ */
+ @Deprecated
+ public void removeListener(CollapseListener listener) {
+ removeCollapseListener(listener);
+ }
+
+ /**
+ * Emits a collapse event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireCollapseEvent(Object itemId) {
+ fireEvent(new CollapseEvent(this, itemId));
+ }
+
+ /**
+ * @return true if animations are enabled
+ */
+ public boolean isAnimationsEnabled() {
+ return animationsEnabled;
+ }
+
+ /**
+ * Animations can be enabled by passing true to this method. Currently
+ * expanding rows slide in from the top and collapsing rows slide out the
+ * same way. NOTE! not supported in Internet Explorer 6 or 7.
+ *
+ * @param animationsEnabled
+ * true or false whether to enable animations or not.
+ */
+ public void setAnimationsEnabled(boolean animationsEnabled) {
+ this.animationsEnabled = animationsEnabled;
+ markAsDirty();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(TreeTable.class.getName());
+ }
+
+ @Override
+ protected List<Object> getItemIds(int firstIndex, int rows) {
+ List<Object> itemIds = new ArrayList<Object>();
+ for (int i = firstIndex; i < firstIndex + rows; i++) {
+ itemIds.add(getIdByIndex(i));
+ }
+ return itemIds;
+ }
+}