| /* |
| * 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<>(); |
| |
| /** 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<>(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<>(); |
| |
| /** 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<>(); |
| |
| /** 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()); |
| } |
| 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()); |
| } |
| VScrollTableRow prev = getPreviousRow(focusedRow, offset); |
| if (prev != null) { |
| return setRowFocus(prev); |
| } |
| 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<>(); |
| 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<>(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<>( |
| 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; |
| } |
| 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; |
| } |
| 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<>(); |
| |
| /** The available cells. */ |
| HashMap<String, HeaderCell> availableCells = new HashMap<>(); |
| |
| /** 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<>(); |
| 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); |
| } |
| 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<>( |
| 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; |
| } |
| 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; |
| } |
| 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<>(); |
| |
| /** The available cells. */ |
| HashMap<String, FooterCell> availableCells = new HashMap<>(); |
| |
| /** 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); |
| } |
| 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<>(); |
| 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<>(); |
| |
| /** |
| * 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); |
| } |
| 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<>(); |
| |
| 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; |
| } |
| 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 && |
| !renderedRows.isEmpty()) { |
| 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; |
| } |
| |
| /** 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<>(); |
| |
| /** 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<>(); |
| |
| /** 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; |
| } |
| 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 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(); |
| } |
| 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; |
| } |
| 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<>(); |
| } |
| 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()); |
| } |
| } |