All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.terminal.gwt.client.ui.VScrollTable Maven / Gradle / Ivy

Go to download

Vaadin is a web application framework for Rich Internet Applications (RIA). Vaadin enables easy development and maintenance of fast and secure rich web applications with a stunning look and feel and a wide browser support. It features a server-side architecture with the majority of the logic running on the server. Ajax technology is used at the browser-side to ensure a rich and interactive user experience.

There is a newer version: 24.4.6
Show newest version
/*
 * Copyright 2011 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.terminal.gwt.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.Set;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.dom.client.Touch;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ContextMenuEvent;
import com.google.gwt.event.dom.client.ContextMenuHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.dom.client.ScrollEvent;
import com.google.gwt.event.dom.client.ScrollHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.terminal.gwt.client.ApplicationConnection;
import com.vaadin.terminal.gwt.client.BrowserInfo;
import com.vaadin.terminal.gwt.client.Container;
import com.vaadin.terminal.gwt.client.Focusable;
import com.vaadin.terminal.gwt.client.MouseEventDetails;
import com.vaadin.terminal.gwt.client.Paintable;
import com.vaadin.terminal.gwt.client.RenderSpace;
import com.vaadin.terminal.gwt.client.TooltipInfo;
import com.vaadin.terminal.gwt.client.UIDL;
import com.vaadin.terminal.gwt.client.Util;
import com.vaadin.terminal.gwt.client.VConsole;
import com.vaadin.terminal.gwt.client.VTooltip;
import com.vaadin.terminal.gwt.client.ui.VScrollTable.VScrollTableBody.VScrollTableRow;
import com.vaadin.terminal.gwt.client.ui.dd.DDUtil;
import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler;
import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback;
import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent;
import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation;

/**
 * VScrollTable
 * 
 * VScrollTable is a FlowPanel having two widgets in it: * TableHead component *
 * ScrollPanel
 * 
 * TableHead contains table's header and widgets + logic for resizing,
 * reordering and hiding columns.
 * 
 * ScrollPanel contains VScrollTableBody object which handles content. To save
 * some bandwidth and to improve clients responsiveness with loads of data, in
 * VScrollTableBody all rows are not necessary rendered. There are "spacers" in
 * VScrollTableBody to use the exact same space as non-rendered rows would use.
 * This way we can use seamlessly traditional scrollbars and scrolling to fetch
 * more rows instead of "paging".
 * 
 * In VScrollTable we listen to scroll events. On horizontal scrolling we also
 * update TableHeads scroll position which has its scrollbars hidden. On
 * vertical scroll events we will check if we are reaching the end of area where
 * we have rows rendered and
 * 
 * TODO implement unregistering for child components in Cells
 */
public class VScrollTable extends FlowPanel implements Table, ScrollHandler,
        VHasDropHandler, FocusHandler, BlurHandler, Focusable, ActionOwner {

    public static final String ATTRIBUTE_PAGEBUFFER_FIRST = "pb-ft";
    public static final String ATTRIBUTE_PAGEBUFFER_LAST = "pb-l";

    /**
     * Tell the client that old keys are no longer valid because the server has
     * cleared its key map.
     */
    public static final String ATTRIBUTE_KEY_MAPPER_RESET = "clearKeyMap";

    private static final String ROW_HEADER_COLUMN_KEY = "0";

    public static final String CLASSNAME = "v-table";
    public static final String CLASSNAME_SELECTION_FOCUS = CLASSNAME + "-focus";

    public static final String ITEM_CLICK_EVENT_ID = "itemClick";
    public static final String HEADER_CLICK_EVENT_ID = "handleHeaderClick";
    public static final String FOOTER_CLICK_EVENT_ID = "handleFooterClick";
    public static final String COLUMN_RESIZE_EVENT_ID = "columnResize";
    public static final String COLUMN_REORDER_EVENT_ID = "columnReorder";

    private static final double CACHE_RATE_DEFAULT = 2;

    /**
     * The default multi select mode where simple left clicks only selects one
     * item, CTRL+left click selects multiple items and SHIFT-left click selects
     * a range of items.
     */
    private static final int MULTISELECT_MODE_DEFAULT = 0;

    /**
     * The simple multiselect mode is what the table used to have before
     * ctrl/shift selections were added. That is that when this is set clicking
     * on an item selects/deselects the item and no ctrl/shift selections are
     * available.
     */
    private static final int MULTISELECT_MODE_SIMPLE = 1;

    /**
     * multiple of pagelength which component will cache when requesting more
     * rows
     */
    private double cache_rate = CACHE_RATE_DEFAULT;
    /**
     * fraction of pageLenght which can be scrolled without making new request
     */
    private double cache_react_rate = 0.75 * cache_rate;

    public static final char ALIGN_CENTER = 'c';
    public static final char ALIGN_LEFT = 'b';
    public static final char ALIGN_RIGHT = 'e';
    private static final int CHARCODE_SPACE = 32;
    private int firstRowInViewPort = 0;
    private int pageLength = 15;
    private int lastRequestedFirstvisible = 0; // to detect "serverside scroll"

    protected boolean showRowHeaders = false;

    private String[] columnOrder;

    protected ApplicationConnection client;
    protected String paintableId;

    private boolean immediate;
    private boolean nullSelectionAllowed = true;

    private int selectMode = Table.SELECT_MODE_NONE;

    private final HashSet selectedRowKeys = new HashSet();

    /*
     * When scrolling and selecting at the same time, the selections are not in
     * sync with the server while retrieving new rows (until key is released).
     */
    private HashSet unSyncedselectionsBeforeRowFetch;

    /*
     * These are used when jumping between pages when pressing Home and End
     */
    private boolean selectLastItemInNextRender = false;
    private boolean selectFirstItemInNextRender = false;
    private boolean focusFirstItemInNextRender = false;
    private boolean focusLastItemInNextRender = false;

    /*
     * The currently focused row
     */
    private VScrollTableRow focusedRow;

    /*
     * Helper to store selection range start in when using the keyboard
     */
    private VScrollTableRow selectionRangeStart;

    /*
     * Flag for notifying when the selection has changed and should be sent to
     * the server
     */
    private boolean selectionChanged = false;

    /*
     * The speed (in pixels) which the scrolling scrolls vertically/horizontally
     */
    private int scrollingVelocity = 10;

    private Timer scrollingVelocityTimer = null;

    private String[] bodyActionKeys;

    private boolean enableDebug = false;

    private static final boolean hasNativeTouchScrolling = BrowserInfo.get()
            .isTouchDevice()
            && !BrowserInfo.get().requiresTouchScrollDelegate();

    private Set 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;

    /**
     * Represents a select range of rows
     */
    private class SelectionRange {
        private VScrollTableRow startRow;
        private final int length;

        /**
         * Constuctor.
         */
        public SelectionRange(VScrollTableRow row1, VScrollTableRow row2) {
            VScrollTableRow endRow;
            if (row2.isBefore(row1)) {
                startRow = row2;
                endRow = row1;
            } else {
                startRow = row1;
                endRow = row2;
            }
            length = endRow.getIndex() - startRow.getIndex() + 1;
        }

        public SelectionRange(VScrollTableRow row, int length) {
            startRow = row;
            this.length = length;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return startRow.getKey() + "-" + length;
        }

        private boolean inRange(VScrollTableRow row) {
            return row.getIndex() >= startRow.getIndex()
                    && row.getIndex() < startRow.getIndex() + length;
        }

        public Collection split(VScrollTableRow row) {
            assert row.isAttached();
            ArrayList ranges = new ArrayList(2);

            int endOfFirstRange = row.getIndex() - 1;
            if (!(endOfFirstRange - startRow.getIndex() < 0)) {
                // create range of first part unless its length is < 1
                ranges.add(new SelectionRange(startRow, endOfFirstRange
                        - startRow.getIndex() + 1));
            }
            int startOfSecondRange = row.getIndex() + 1;
            if (!(getEndIndex() - startOfSecondRange < 0)) {
                // create range of second part unless its length is < 1
                VScrollTableRow startOfRange = scrollBody
                        .getRowByRowIndex(startOfSecondRange);
                if (startOfRange != null) {
                    ranges.add(new SelectionRange(startOfRange, getEndIndex()
                            - startOfSecondRange + 1));
                }
            }
            return ranges;
        }

        private int getEndIndex() {
            return startRow.getIndex() + length - 1;
        }

    };

    private final HashSet selectedRowRanges = new HashSet();

    private boolean initializedAndAttached = false;

    /**
     * Flag to indicate if a column width recalculation is needed due update.
     */
    private boolean headerChangedDuringUpdate = false;

    protected final TableHead tHead = new TableHead();

    private final TableFooter tFoot = new TableFooter();

    private final FocusableScrollPanel scrollBodyPanel = new FocusableScrollPanel(
            true);

    private KeyPressHandler navKeyPressHandler = new KeyPressHandler() {
        public void onKeyPress(KeyPressEvent keyPressEvent) {
            // This is used for Firefox only, since Firefox auto-repeat
            // works correctly only if we use a key press handler, other
            // browsers handle it correctly when using a key down handler
            if (!BrowserInfo.get().isGecko()) {
                return;
            }

            NativeEvent event = keyPressEvent.getNativeEvent();
            if (!enabled) {
                // Cancel default keyboard events on a disabled Table
                // (prevents scrolling)
                event.preventDefault();
            } else if (hasFocus) {
                // Key code in Firefox/onKeyPress is present only for
                // special keys, otherwise 0 is returned
                int keyCode = event.getKeyCode();
                if (keyCode == 0 && event.getCharCode() == ' ') {
                    // Provide a keyCode for space to be compatible with
                    // FireFox keypress event
                    keyCode = CHARCODE_SPACE;
                }

                if (handleNavigation(keyCode,
                        event.getCtrlKey() || event.getMetaKey(),
                        event.getShiftKey())) {
                    event.preventDefault();
                }

                startScrollingVelocityTimer();
            }
        }

    };

    private KeyUpHandler navKeyUpHandler = new KeyUpHandler() {

        public void onKeyUp(KeyUpEvent keyUpEvent) {
            NativeEvent event = keyUpEvent.getNativeEvent();
            int keyCode = event.getKeyCode();

            if (!isFocusable()) {
                cancelScrollingVelocityTimer();
            } else if (isNavigationKey(keyCode)) {
                if (keyCode == getNavigationDownKey()
                        || keyCode == getNavigationUpKey()) {
                    /*
                     * in multiselect mode the server may still have value from
                     * previous page. Clear it unless doing multiselection or
                     * just moving focus.
                     */
                    if (!event.getShiftKey() && !event.getCtrlKey()) {
                        instructServerToForgetPreviousSelections();
                    }
                    sendSelectedRows();
                }
                cancelScrollingVelocityTimer();
                navKeyDown = false;
            }
        }
    };

    private KeyDownHandler navKeyDownHandler = new KeyDownHandler() {

        public void onKeyDown(KeyDownEvent keyDownEvent) {
            NativeEvent event = keyDownEvent.getNativeEvent();
            // This is not used for Firefox
            if (BrowserInfo.get().isGecko()) {
                return;
            }

            if (!enabled) {
                // Cancel default keyboard events on a disabled Table
                // (prevents scrolling)
                event.preventDefault();
            } else if (hasFocus) {
                if (handleNavigation(event.getKeyCode(), event.getCtrlKey()
                        || event.getMetaKey(), event.getShiftKey())) {
                    navKeyDown = true;
                    event.preventDefault();
                }

                startScrollingVelocityTimer();
            }
        }
    };
    private int totalRows;

    private Set collapsedColumns;

    private final RowRequestHandler rowRequestHandler;
    private VScrollTableBody scrollBody;
    private int firstvisible = 0;
    private boolean sortAscending;
    private String sortColumn;
    private String oldSortColumn;
    private boolean columnReordering;

    /**
     * This map contains captions and icon urls for actions like: * "33_c" ->
     * "Edit" * "33_i" -> "http://dom.com/edit.png"
     */
    private final HashMap actionMap = new HashMap();
    private String[] visibleColOrder;
    private boolean initialContentReceived = false;
    private Element scrollPositionElement;
    private boolean enabled;
    private boolean showColHeaders;
    private 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 refreshContentWidths
     * in method {@link TableHead#updateCellsFromUIDL(UIDL)}.
     */
    boolean recalcWidths = false;

    private final ArrayList lazyUnregistryBag = new ArrayList();
    private String height;
    private String width = "";
    private boolean rendering = false;
    private boolean hasFocus = false;
    private int dragmode;

    private int multiselectmode;
    private int tabIndex;
    private TouchScrollDelegate touchScrollDelegate;

    private 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)
     */
    private int serverCacheFirst = -1;
    private 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.
     */
    private boolean postponeSanityCheckForLastRendered;

    /**
     * Used to recall the position of an open context menu if we need to close
     * and reopen it during a row update.
     */
    private class ContextMenuDetails {
        String rowKey;
        int left;
        int top;

        ContextMenuDetails(String rowKey, int left, int top) {
            this.rowKey = rowKey;
            this.left = left;
            this.top = top;
        }
    }

    ContextMenuDetails contextMenu;
    private boolean hadScrollBars = false;

    public VScrollTable() {
        setMultiSelectMode(MULTISELECT_MODE_DEFAULT);

        scrollBodyPanel.addStyleName(CLASSNAME + "-body-wrapper");
        scrollBodyPanel.addFocusHandler(this);
        scrollBodyPanel.addBlurHandler(this);

        scrollBodyPanel.addScrollHandler(this);
        scrollBodyPanel.addStyleName(CLASSNAME + "-body");

        /*
         * Firefox auto-repeat works correctly only if we use a key press
         * handler, other browsers handle it correctly when using a key down
         * handler
         */
        if (BrowserInfo.get().isGecko()) {
            scrollBodyPanel.addKeyPressHandler(navKeyPressHandler);
        } else {
            scrollBodyPanel.addKeyDownHandler(navKeyDownHandler);
        }
        scrollBodyPanel.addKeyUpHandler(navKeyUpHandler);

        scrollBodyPanel.sinkEvents(Event.TOUCHEVENTS);

        scrollBodyPanel.sinkEvents(Event.ONCONTEXTMENU);
        scrollBodyPanel.addDomHandler(new ContextMenuHandler() {
            public void onContextMenu(ContextMenuEvent event) {
                handleBodyContextMenu(event);
            }
        }, ContextMenuEvent.getType());

        setStyleName(CLASSNAME);

        add(tHead);
        add(scrollBodyPanel);
        add(tFoot);

        rowRequestHandler = new RowRequestHandler();
    }

    private void handleBodyContextMenu(ContextMenuEvent event) {
        if (enabled && bodyActionKeys != null) {
            int left = Util.getTouchOrMouseClientX(event.getNativeEvent());
            int top = Util.getTouchOrMouseClientY(event.getNativeEvent());
            top += Window.getScrollTop();
            left += Window.getScrollLeft();
            client.getContextMenu().showAt(this, left, top);

            // Only prevent browser context menu if there are action handlers
            // registered
            event.stopPropagation();
            event.preventDefault();
        }
    }

    /**
     * Fires a column resize event which sends the resize information to the
     * server.
     * 
     * @param columnId
     *            The columnId of the column which was resized
     * @param originalWidth
     *            The width in pixels of the column before the resize event
     * @param newWidth
     *            The width in pixels of the column after the resize event
     */
    private void fireColumnResizeEvent(String columnId, int originalWidth,
            int newWidth) {
        client.updateVariable(paintableId, "columnResizeEventColumn", columnId,
                false);
        client.updateVariable(paintableId, "columnResizeEventPrev",
                originalWidth, false);
        client.updateVariable(paintableId, "columnResizeEventCurr", newWidth,
                immediate);

    }

    /**
     * Non-immediate variable update of column widths for a collection of
     * columns.
     * 
     * @param columns
     *            the columns to trigger the events for.
     */
    private void sendColumnWidthUpdates(Collection columns) {
        String[] newSizes = new String[columns.size()];
        int ix = 0;
        for (HeaderCell cell : columns) {
            newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth();
        }
        client.updateVariable(paintableId, "columnWidthUpdates", newSizes,
                false);
    }

    /**
     * Moves the focus one step down
     * 
     * @return Returns true if succeeded
     */
    private boolean moveFocusDown() {
        return moveFocusDown(0);
    }

    /**
     * Moves the focus down by 1+offset rows
     * 
     * @return Returns true if succeeded, else false if the selection could not
     *         be move downwards
     */
    private boolean moveFocusDown(int offset) {
        if (isSelectable()) {
            if (focusedRow == null && scrollBody.iterator().hasNext()) {
                // FIXME should focus first visible from top, not first rendered
                // ??
                return setRowFocus((VScrollTableRow) scrollBody.iterator()
                        .next());
            } else {
                VScrollTableRow next = getNextRow(focusedRow, offset);
                if (next != null) {
                    return setRowFocus(next);
                }
            }
        }

        return false;
    }

    /**
     * Moves the selection one step up
     * 
     * @return Returns true if succeeded
     */
    private boolean moveFocusUp() {
        return moveFocusUp(0);
    }

    /**
     * Moves the focus row upwards
     * 
     * @return Returns true if succeeded, else false if the selection could not
     *         be move upwards
     * 
     */
    private boolean moveFocusUp(int offset) {
        if (isSelectable()) {
            if (focusedRow == null && scrollBody.iterator().hasNext()) {
                // FIXME logic is exactly the same as in moveFocusDown, should
                // be the opposite??
                return setRowFocus((VScrollTableRow) scrollBody.iterator()
                        .next());
            } else {
                VScrollTableRow prev = getPreviousRow(focusedRow, offset);
                if (prev != null) {
                    return setRowFocus(prev);
                } else {
                    VConsole.log("no previous available");
                }
            }
        }

        return false;
    }

    /**
     * Selects a row where the current selection head is
     * 
     * @param ctrlSelect
     *            Is the selection a ctrl+selection
     * @param shiftSelect
     *            Is the selection a shift+selection
     * @return Returns truw
     */
    private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) {
        if (focusedRow != null) {
            // Arrows moves the selection and clears previous selections
            if (isSelectable() && !ctrlSelect && !shiftSelect) {
                deselectAll();
                focusedRow.toggleSelection();
                selectionRangeStart = focusedRow;
            } else if (isSelectable() && ctrlSelect && !shiftSelect) {
                // Ctrl+arrows moves selection head
                selectionRangeStart = focusedRow;
                // No selection, only selection head is moved
            } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) {
                // Shift+arrows selection selects a range
                focusedRow.toggleShiftSelection(shiftSelect);
            }
        }
    }

    /**
     * Sends the selection to the server if changed since the last update/visit.
     */
    protected void sendSelectedRows() {
        sendSelectedRows(immediate);
    }

    /**
     * Sends the selection to the server if it has been changed since the last
     * update/visit.
     * 
     * @param immediately
     *            set to true to immediately send the rows
     */
    protected void sendSelectedRows(boolean immediately) {
        // Don't send anything if selection has not changed
        if (!selectionChanged) {
            return;
        }

        // Reset selection changed flag
        selectionChanged = false;

        // Note: changing the immediateness of this might require changes to
        // "clickEvent" immediateness also.
        if (isMultiSelectModeDefault()) {
            // Convert ranges to a set of strings
            Set ranges = new HashSet();
            for (SelectionRange range : selectedRowRanges) {
                ranges.add(range.toString());
            }

            // Send the selected row ranges
            client.updateVariable(paintableId, "selectedRanges",
                    ranges.toArray(new String[selectedRowRanges.size()]), false);

            // clean selectedRowKeys so that they don't contain excess values
            for (Iterator iterator = selectedRowKeys.iterator(); iterator
                    .hasNext();) {
                String key = iterator.next();
                VScrollTableRow renderedRowByKey = getRenderedRowByKey(key);
                if (renderedRowByKey != null) {
                    for (SelectionRange range : selectedRowRanges) {
                        if (range.inRange(renderedRowByKey)) {
                            iterator.remove();
                        }
                    }
                } else {
                    // orphaned selected key, must be in a range, ignore
                    iterator.remove();
                }

            }
        }

        // Send the selected rows
        client.updateVariable(paintableId, "selected",
                selectedRowKeys.toArray(new String[selectedRowKeys.size()]),
                immediately);

    }

    /**
     * Get the key that moves the selection head upwards. By default it is the
     * up arrow key but by overriding this you can change the key to whatever
     * you want.
     * 
     * @return The keycode of the key
     */
    protected int getNavigationUpKey() {
        return KeyCodes.KEY_UP;
    }

    /**
     * Get the key that moves the selection head downwards. By default it is the
     * down arrow key but by overriding this you can change the key to whatever
     * you want.
     * 
     * @return The keycode of the key
     */
    protected int getNavigationDownKey() {
        return KeyCodes.KEY_DOWN;
    }

    /**
     * Get the key that scrolls to the left in the table. By default it is the
     * left arrow key but by overriding this you can change the key to whatever
     * you want.
     * 
     * @return The keycode of the key
     */
    protected int getNavigationLeftKey() {
        return KeyCodes.KEY_LEFT;
    }

    /**
     * Get the key that scroll to the right on the table. By default it is the
     * right arrow key but by overriding this you can change the key to whatever
     * you want.
     * 
     * @return The keycode of the key
     */
    protected int getNavigationRightKey() {
        return KeyCodes.KEY_RIGHT;
    }

    /**
     * Get the key that selects an item in the table. By default it is the space
     * bar key but by overriding this you can change the key to whatever you
     * want.
     * 
     * @return
     */
    protected int getNavigationSelectKey() {
        return CHARCODE_SPACE;
    }

    /**
     * Get the key the moves the selection one page up in the table. By default
     * this is the Page Up key but by overriding this you can change the key to
     * whatever you want.
     * 
     * @return
     */
    protected int getNavigationPageUpKey() {
        return KeyCodes.KEY_PAGEUP;
    }

    /**
     * Get the key the moves the selection one page down in the table. By
     * default this is the Page Down key but by overriding this you can change
     * the key to whatever you want.
     * 
     * @return
     */
    protected int getNavigationPageDownKey() {
        return KeyCodes.KEY_PAGEDOWN;
    }

    /**
     * Get the key the moves the selection to the beginning of the table. By
     * default this is the Home key but by overriding this you can change the
     * key to whatever you want.
     * 
     * @return
     */
    protected int getNavigationStartKey() {
        return KeyCodes.KEY_HOME;
    }

    /**
     * Get the key the moves the selection to the end of the table. By default
     * this is the End key but by overriding this you can change the key to
     * whatever you want.
     * 
     * @return
     */
    protected int getNavigationEndKey() {
        return KeyCodes.KEY_END;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal
     * .gwt.client.UIDL, com.vaadin.terminal.gwt.client.ApplicationConnection)
     */
    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
        rendering = true;

        // On the first rendering, add a handler to clear saved context menu
        // details when the menu closes. See #8526.
        if (this.client == null) {
            client.getContextMenu().addCloseHandler(
                    new CloseHandler() {
                        public void onClose(CloseEvent event) {
                            contextMenu = null;
                        }
                    });
        }
        // If a row has an open context menu, it will be closed as the row is
        // detached. Retain a reference here so we can restore the menu if
        // required.
        ContextMenuDetails savedContextMenu = contextMenu;

        if (uidl.hasAttribute(ATTRIBUTE_PAGEBUFFER_FIRST)) {
            serverCacheFirst = uidl.getIntAttribute(ATTRIBUTE_PAGEBUFFER_FIRST);
            serverCacheLast = uidl.getIntAttribute(ATTRIBUTE_PAGEBUFFER_LAST);
        } else {
            serverCacheFirst = -1;
            serverCacheLast = -1;
        }
        /*
         * We need to do this before updateComponent since updateComponent calls
         * this.setHeight() which will calculate a new body height depending on
         * the space available.
         */
        if (uidl.hasAttribute("colfooters")) {
            showColFooters = uidl.getBooleanAttribute("colfooters");
        }

        tFoot.setVisible(showColFooters);

        if (client.updateComponent(this, uidl, true)) {
            rendering = false;
            return;
        }

        enabled = !uidl.hasAttribute("disabled");

        if (BrowserInfo.get().isIE8() && !enabled) {
            /*
             * The disabled shim will not cover the table body if it is relative
             * in IE8. See #7324
             */
            scrollBodyPanel.getElement().getStyle()
                    .setPosition(Position.STATIC);
        } else if (BrowserInfo.get().isIE8()) {
            scrollBodyPanel.getElement().getStyle()
                    .setPosition(Position.RELATIVE);
        }

        this.client = client;
        paintableId = uidl.getStringAttribute("id");
        immediate = uidl.getBooleanAttribute("immediate");

        int previousTotalRows = totalRows;
        updateTotalRows(uidl);
        boolean totalRowsChanged = (totalRows != previousTotalRows);

        updateDragMode(uidl);

        updateSelectionProperties(uidl);

        if (uidl.hasAttribute("alb")) {
            bodyActionKeys = uidl.getStringArrayAttribute("alb");
        } else {
            // Need to clear the actions if the action handlers have been
            // removed
            bodyActionKeys = null;
        }

        setCacheRateFromUIDL(uidl);

        recalcWidths = uidl.hasAttribute("recalcWidths");
        if (recalcWidths) {
            tHead.clear();
            tFoot.clear();
        }

        updatePageLength(uidl);

        updateFirstVisibleAndScrollIfNeeded(uidl);

        showRowHeaders = uidl.getBooleanAttribute("rowheaders");
        showColHeaders = uidl.getBooleanAttribute("colheaders");

        updateSortingProperties(uidl);

        boolean keyboardSelectionOverRowFetchInProgress = selectSelectedRows(uidl);

        updateActionMap(uidl);

        updateColumnProperties(uidl);

        UIDL ac = uidl.getChildByTagName("-ac");
        if (ac == null) {
            if (dropHandler != null) {
                // remove dropHandler if not present anymore
                dropHandler = null;
            }
        } else {
            if (dropHandler == null) {
                dropHandler = new VScrollTableDropHandler();
            }
            dropHandler.updateAcceptRules(ac);
        }

        UIDL partialRowAdditions = uidl.getChildByTagName("prows");
        UIDL partialRowUpdates = uidl.getChildByTagName("urows");
        if (partialRowUpdates != null || partialRowAdditions != null) {
            postponeSanityCheckForLastRendered = true;
            // we may have pending cache row fetch, cancel it. See #2136
            rowRequestHandler.cancel();

            updateRowsInBody(partialRowUpdates);
            addAndRemoveRows(partialRowAdditions);

            // sanity check (in case the value has slipped beyond the total
            // amount of rows)
            scrollBody.setLastRendered(scrollBody.getLastRendered());
        } else {
            postponeSanityCheckForLastRendered = false;
            UIDL rowData = uidl.getChildByTagName("rows");
            if (rowData != null) {
                // we may have pending cache row fetch, cancel it. See #2136
                rowRequestHandler.cancel();

                if (!recalcWidths && initializedAndAttached) {
                    updateBody(rowData, uidl.getIntAttribute("firstrow"),
                            uidl.getIntAttribute("rows"));
                    if (headerChangedDuringUpdate) {
                        triggerLazyColumnAdjustment(true);
                    } else if (!isScrollPositionVisible()
                            || totalRowsChanged
                            || lastRenderedHeight != scrollBody
                                    .getOffsetHeight()) {
                        // webkits may still bug with their disturbing scrollbar
                        // bug, see #3457
                        // Run overflow fix for the scrollable area
                        // #6698 - If there's a scroll going on, don't abort it
                        // by changing overflows as the length of the contents
                        // *shouldn't* have changed (unless the number of rows
                        // or the height of the widget has also changed)
                        Scheduler.get().scheduleDeferred(new Command() {
                            public void execute() {
                                Util.runWebkitOverflowAutoFix(scrollBodyPanel
                                        .getElement());
                            }
                        });
                    }
                } else {
                    initializeRows(uidl, rowData);
                }
            }
        }

        // If a row had an open context menu before the update, and after the
        // update there's a row with the same key as that row, restore the
        // context menu. See #8526.
        if (enabled && savedContextMenu != null) {
            for (Widget w : scrollBody.renderedRows) {
                VScrollTableRow row = (VScrollTableRow) w;
                if (row.isVisible()
                        && row.getKey().equals(savedContextMenu.rowKey)) {
                    contextMenu = savedContextMenu;
                    client.getContextMenu().showAt(row, savedContextMenu.left,
                            savedContextMenu.top);
                }
            }
        }

        if (!isSelectable()) {
            scrollBody.addStyleName(CLASSNAME + "-body-noselection");
        } else {
            scrollBody.removeStyleName(CLASSNAME + "-body-noselection");
        }

        hideScrollPositionAnnotation();
        purgeUnregistryBag();

        // selection is no in sync with server, avoid excessive server visits by
        // clearing to flag used during the normal operation
        if (!keyboardSelectionOverRowFetchInProgress) {
            selectionChanged = false;
        }

        /*
         * This is called when the Home or page up button has been pressed in
         * selectable mode and the next selected row was not yet rendered in the
         * client
         */
        if (selectFirstItemInNextRender || focusFirstItemInNextRender) {
            selectFirstRenderedRowInViewPort(focusFirstItemInNextRender);
            selectFirstItemInNextRender = focusFirstItemInNextRender = false;
        }

        /*
         * This is called when the page down or end button has been pressed in
         * selectable mode and the next selected row was not yet rendered in the
         * client
         */
        if (selectLastItemInNextRender || focusLastItemInNextRender) {
            selectLastRenderedRowInViewPort(focusLastItemInNextRender);
            selectLastItemInNextRender = focusLastItemInNextRender = false;
        }
        multiselectPending = false;

        if (focusedRow != null) {
            if (!focusedRow.isAttached() && !rowRequestHandler.isRunning()) {
                // focused row has been orphaned, can't focus
                focusRowFromBody();
            }
        }

        /*
         * If the server has (re)initialized the rows, our selectionRangeStart
         * row will point to an index that the server knows nothing about,
         * causing problems if doing multi selection with shift. The field will
         * be cleared a little later when the row focus has been restored.
         * (#8584)
         */
        if (uidl.hasAttribute(ATTRIBUTE_KEY_MAPPER_RESET)
                && uidl.getBooleanAttribute(ATTRIBUTE_KEY_MAPPER_RESET)
                && selectionRangeStart != null) {
            assert !selectionRangeStart.isAttached();
            selectionRangeStart = focusedRow;
        }

        tabIndex = uidl.hasAttribute("tabindex") ? uidl
                .getIntAttribute("tabindex") : 0;
        setProperTabIndex();

        resizeSortedColumnForSortIndicator();

        // Remember this to detect situations where overflow hack might be
        // needed during scrolling
        lastRenderedHeight = scrollBody.getOffsetHeight();

        rendering = false;
        headerChangedDuringUpdate = false;
    }

    private void initializeRows(UIDL uidl, UIDL rowData) {
        if (scrollBody != null) {
            scrollBody.removeFromParent();
            lazyUnregistryBag.add(scrollBody);
        }
        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;
        if (isAttached()) {
            sizeInit();
        }
        scrollBody.restoreRowVisibility();
    }

    private void updateColumnProperties(UIDL uidl) {
        updateColumnOrder(uidl);

        updateCollapsedColumns(uidl);

        UIDL vc = uidl.getChildByTagName("visiblecolumns");
        if (vc != null) {
            tHead.updateCellsFromUIDL(vc);
            tFoot.updateCellsFromUIDL(vc);
        }

        updateHeader(uidl.getStringArrayAttribute("vcolorder"));
        updateFooter(uidl.getStringArrayAttribute("vcolorder"));
        if (uidl.hasVariable("noncollapsiblecolumns")) {
            noncollapsibleColumns = uidl
                    .getStringArrayVariableAsSet("noncollapsiblecolumns");
        }
    }

    private void updateCollapsedColumns(UIDL uidl) {
        if (uidl.hasVariable("collapsedcolumns")) {
            tHead.setColumnCollapsingAllowed(true);
            collapsedColumns = uidl
                    .getStringArrayVariableAsSet("collapsedcolumns");
        } else {
            tHead.setColumnCollapsingAllowed(false);
        }
    }

    private void updateColumnOrder(UIDL uidl) {
        if (uidl.hasVariable("columnorder")) {
            columnReordering = true;
            columnOrder = uidl.getStringArrayVariable("columnorder");
        } else {
            columnReordering = false;
            columnOrder = null;
        }
    }

    private boolean selectSelectedRows(UIDL uidl) {
        boolean keyboardSelectionOverRowFetchInProgress = false;

        if (uidl.hasVariable("selected")) {
            final Set selectedKeys = uidl
                    .getStringArrayVariableAsSet("selected");
            final Set changedSelectedKeys = uidl
                    .getStringArrayVariableAsSet("selectionchanged");
            if (scrollBody != null) {
                Iterator iterator = scrollBody.iterator();
                while (iterator.hasNext()) {
                    /*
                     * Make the focus reflect to the server side state unless we
                     * are currently selecting multiple rows with keyboard.
                     */
                    VScrollTableRow row = (VScrollTableRow) iterator.next();
                    boolean selected = selectedKeys.contains(row.getKey());
                    boolean changedOnServerSide = changedSelectedKeys
                            .contains(row.getKey());
                    if (!selected
                            && unSyncedselectionsBeforeRowFetch != null
                            && unSyncedselectionsBeforeRowFetch.contains(row
                                    .getKey())) {
                        selected = true;
                        keyboardSelectionOverRowFetchInProgress = true;
                    } else if (selected != row.isSelected()
                            && !changedOnServerSide) {
                        // ensure the previous selection doesn't override a
                        // selection made on client side on an update that
                        // arrives before server side has been updated (see
                        // #6684)
                        selected = !selected;
                    }
                    if (selected != row.isSelected()) {
                        row.toggleSelection();
                        if (!isSingleSelectMode() && !selected) {
                            // Update selection range in case a row is
                            // unselected from the middle of a range - #8076
                            removeRowFromUnsentSelectionRanges(row);
                        }
                    }
                }
            }
        }
        unSyncedselectionsBeforeRowFetch = null;
        return keyboardSelectionOverRowFetchInProgress;
    }

    private void updateSortingProperties(UIDL uidl) {
        oldSortColumn = sortColumn;
        if (uidl.hasVariable("sortascending")) {
            sortAscending = uidl.getBooleanVariable("sortascending");
            sortColumn = uidl.getStringVariable("sortcolumn");
        }
    }

    private void resizeSortedColumnForSortIndicator() {
        // Force recalculation of the captionContainer element inside the header
        // cell to accomodate for the size of the sort arrow.
        HeaderCell sortedHeader = tHead.getHeaderCell(sortColumn);
        if (sortedHeader != null) {
            tHead.resizeCaptionContainer(sortedHeader);
        }
        // Also recalculate the width of the captionContainer element in the
        // previously sorted header, since this now has more room.
        HeaderCell oldSortedHeader = tHead.getHeaderCell(oldSortColumn);
        if (oldSortedHeader != null) {
            tHead.resizeCaptionContainer(oldSortedHeader);
        }
    }

    private void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) {
        firstvisible = uidl.hasVariable("firstvisible") ? uidl
                .getIntVariable("firstvisible") : 0;
        if (firstvisible != lastRequestedFirstvisible && scrollBody != null) {
            // received 'surprising' firstvisible from server: scroll there
            firstRowInViewPort = firstvisible;
            scrollBodyPanel
                    .setScrollPosition(measureRowHeightOffset(firstvisible));
        }
    }

    protected int measureRowHeightOffset(int rowIx) {
        return (int) (rowIx * scrollBody.getRowHeight());
    }

    private 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
            sizeInit();
        }
    }

    private void updateSelectionProperties(UIDL uidl) {
        setMultiSelectMode(uidl.hasAttribute("multiselectmode") ? uidl
                .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT);

        nullSelectionAllowed = uidl.hasAttribute("nsa") ? uidl
                .getBooleanAttribute("nsa") : true;

        if (uidl.hasAttribute("selectmode")) {
            if (uidl.getBooleanAttribute("readonly")) {
                selectMode = Table.SELECT_MODE_NONE;
            } else if (uidl.getStringAttribute("selectmode").equals("multi")) {
                selectMode = Table.SELECT_MODE_MULTI;
            } else if (uidl.getStringAttribute("selectmode").equals("single")) {
                selectMode = Table.SELECT_MODE_SINGLE;
            } else {
                selectMode = Table.SELECT_MODE_NONE;
            }
        }
    }

    private void updateDragMode(UIDL uidl) {
        dragmode = uidl.hasAttribute("dragmode") ? uidl
                .getIntAttribute("dragmode") : 0;
        if (BrowserInfo.get().isIE()) {
            if (dragmode > 0) {
                getElement().setPropertyJSO("onselectstart",
                        getPreventTextSelectionIEHack());
            } else {
                getElement().setPropertyJSO("onselectstart", null);
            }
        }
    }

    protected void updateTotalRows(UIDL uidl) {
        int newTotalRows = uidl.getIntAttribute("totalrows");
        if (newTotalRows != getTotalRows()) {
            if (scrollBody != null) {
                if (getTotalRows() == 0) {
                    tHead.clear();
                    tFoot.clear();
                }
                initializedAndAttached = false;
                initialContentReceived = false;
                isNewBody = true;
            }
            setTotalRows(newTotalRows);
        }
    }

    protected void setTotalRows(int newTotalRows) {
        totalRows = newTotalRows;
    }

    protected int getTotalRows() {
        return totalRows;
    }

    private void focusRowFromBody() {
        if (selectedRowKeys.size() == 1) {
            // try to focus a row currently selected and in viewport
            String selectedRowKey = selectedRowKeys.iterator().next();
            if (selectedRowKey != null) {
                VScrollTableRow renderedRow = getRenderedRowByKey(selectedRowKey);
                if (renderedRow == null || !renderedRow.isInViewPort()) {
                    setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
                } else {
                    setRowFocus(renderedRow);
                }
            }
        } else {
            // multiselect mode
            setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
        }
    }

    protected VScrollTableBody createScrollBody() {
        return new VScrollTableBody();
    }

    /**
     * Selects the last row visible in the table
     * 
     * @param focusOnly
     *            Should the focus only be moved to the last row
     */
    private void selectLastRenderedRowInViewPort(boolean focusOnly) {
        int index = firstRowInViewPort + getFullyVisibleRowCount();
        VScrollTableRow lastRowInViewport = scrollBody.getRowByRowIndex(index);
        if (lastRowInViewport == null) {
            // this should not happen in normal situations (white space at the
            // end of viewport). Select the last rendered as a fallback.
            lastRowInViewport = scrollBody.getRowByRowIndex(scrollBody
                    .getLastRendered());
            if (lastRowInViewport == null) {
                return; // empty table
            }
        }
        setRowFocus(lastRowInViewport);
        if (!focusOnly) {
            selectFocusedRow(false, multiselectPending);
            sendSelectedRows();
        }
    }

    /**
     * Selects the first row visible in the table
     * 
     * @param focusOnly
     *            Should the focus only be moved to the first row
     */
    private 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();
        }
    }

    private void setCacheRateFromUIDL(UIDL uidl) {
        setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr")
                : CACHE_RATE_DEFAULT);
    }

    private void setCacheRate(double d) {
        if (cache_rate != d) {
            cache_rate = d;
            cache_react_rate = 0.75 * d;
        }
    }

    /**
     * Unregisters Paintables in "trashed" HasWidgets (IScrollTableBodys or
     * IScrollTableRows). This is done lazily as Table must survive from
     * "subtreecaching" logic.
     */
    private void purgeUnregistryBag() {
        for (Iterator iterator = lazyUnregistryBag.iterator(); iterator
                .hasNext();) {
            client.unregisterChildPaintables(iterator.next());
        }
        lazyUnregistryBag.clear();
    }

    private void updateActionMap(UIDL mainUidl) {
        UIDL actionsUidl = mainUidl.getChildByTagName("actions");
        if (actionsUidl == null) {
            return;
        }

        final Iterator it = actionsUidl.getChildIterator();
        while (it.hasNext()) {
            final UIDL action = (UIDL) it.next();
            final String key = action.getStringAttribute("key");
            final String caption = action.getStringAttribute("caption");
            actionMap.put(key + "_c", caption);
            if (action.hasAttribute("icon")) {
                // TODO need some uri handling ??
                actionMap.put(key + "_i", client.translateVaadinUri(action
                        .getStringAttribute("icon")));
            } else {
                actionMap.remove(key + "_i");
            }
        }

    }

    public String getActionCaption(String actionKey) {
        return actionMap.get(actionKey + "_c");
    }

    public String getActionIcon(String actionKey) {
        return actionMap.get(actionKey + "_i");
    }

    private void updateHeader(String[] strings) {
        if (strings == null) {
            return;
        }

        int visibleCols = strings.length;
        int colIndex = 0;
        if (showRowHeaders) {
            tHead.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
            visibleCols++;
            visibleColOrder = new String[visibleCols];
            visibleColOrder[colIndex] = ROW_HEADER_COLUMN_KEY;
            colIndex++;
        } else {
            visibleColOrder = new String[visibleCols];
            tHead.removeCell(ROW_HEADER_COLUMN_KEY);
        }

        int i;
        for (i = 0; i < strings.length; i++) {
            final String cid = strings[i];
            visibleColOrder[colIndex] = cid;
            tHead.enableColumn(cid, colIndex);
            colIndex++;
        }

        tHead.setVisible(showColHeaders);
        setContainerHeight();

    }

    /**
     * Updates footers.
     * 

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

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