org.rstudio.core.client.widget.FastSelectTable.java Source code

Java tutorial

Introduction

Here is the source code for org.rstudio.core.client.widget.FastSelectTable.java

Source

/*
 * FastSelectTable.java
 *
 * Copyright (C) 2009-12 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.core.client.widget;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.*;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Cursor;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.Widget;
import org.rstudio.core.client.Rectangle;
import org.rstudio.core.client.command.KeyboardShortcut;
import org.rstudio.core.client.dom.DomUtils;
import org.rstudio.core.client.dom.NativeWindow;
import org.rstudio.core.client.widget.events.SelectionChangedEvent;
import org.rstudio.core.client.widget.events.SelectionChangedHandler;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class FastSelectTable<TItemInput, TItemOutput, TItemOutput2> extends Widget
        implements HasAllMouseHandlers, HasClickHandlers, HasAllKeyHandlers {
    public interface ItemCodec<T, TItemOutput, TItemOutput2> {
        TableRowElement getRowForItem(T entry);

        void onRowsChanged(TableSectionElement tbody);

        TItemOutput getOutputForRow(TableRowElement row);

        TItemOutput2 getOutputForRow2(TableRowElement row);

        boolean isValueRow(TableRowElement row);

        boolean hasNonValueRows();

        Integer logicalOffsetToPhysicalOffset(TableElement table, int offset);

        Integer physicalOffsetToLogicalOffset(TableElement table, int offset);

        int getLogicalRowCount(TableElement table);
    }

    public FastSelectTable(ItemCodec<TItemInput, TItemOutput, TItemOutput2> codec, String selectedClassName,
            boolean focusable, boolean allowMultiSelect) {
        codec_ = codec;
        selectedClassName_ = selectedClassName;
        focusable_ = focusable;
        allowMultiSelect_ = allowMultiSelect;

        table_ = Document.get().createTableElement();
        if (focusable_)
            table_.setTabIndex(0);
        table_.setCellPadding(0);
        table_.setCellSpacing(0);
        table_.setBorder(0);
        table_.getStyle().setCursor(Cursor.DEFAULT);
        setElement(table_);

        addMouseDownHandler(new MouseDownHandler() {
            public void onMouseDown(MouseDownEvent event) {
                if (event.getNativeButton() != NativeEvent.BUTTON_LEFT)
                    return;

                event.preventDefault();

                NativeWindow.get().focus();
                DomUtils.setActive(getElement());

                Element cell = getEventTargetCell((Event) event.getNativeEvent());
                if (cell == null)
                    return;
                TableRowElement row = (TableRowElement) cell.getParentElement();
                if (codec_.isValueRow(row))
                    handleRowClick(event, row);
            }
        });
        addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                event.preventDefault();
            }
        });

        addKeyDownHandler(new KeyDownHandler() {
            public void onKeyDown(KeyDownEvent event) {
                handleKeyDown(event);
            }
        });
    }

    public void setCellPadding(int padding) {
        table_.setCellPadding(padding);
    }

    public void setCellSpacing(int spacing) {
        table_.setCellSpacing(spacing);
    }

    public void setOwningScrollPanel(ScrollPanel scrollPanel) {
        scrollPanel_ = scrollPanel;
    }

    private void handleRowClick(MouseDownEvent event, TableRowElement row) {
        int modifiers = KeyboardShortcut.getModifierValue(event.getNativeEvent());
        modifiers &= ~KeyboardShortcut.ALT; // ALT has no effect

        if (!allowMultiSelect_)
            modifiers = KeyboardShortcut.NONE;

        // We'll treat Ctrl and Meta as equivalent--and normalize to Ctrl.
        if (KeyboardShortcut.META == (modifiers & KeyboardShortcut.META))
            modifiers |= KeyboardShortcut.CTRL;
        modifiers &= ~KeyboardShortcut.META;

        if (modifiers == KeyboardShortcut.NONE) {
            // Select only the target row
            clearSelection();
            setSelected(row, true);
        } else if (modifiers == KeyboardShortcut.CTRL) {
            // Toggle the target row
            setSelected(row, !isSelected(row));
        } else {
            // SHIFT or CTRL+SHIFT

            int target = row.getRowIndex();
            Integer min = null;
            Integer max = null;
            for (TableRowElement selectedRow : selectedRows_) {
                if (min == null)
                    min = selectedRow.getRowIndex();
                max = selectedRow.getRowIndex();
            }

            int offset; // selection offset
            int length; // selection length

            if (min == null) {
                // Nothing is selected
                offset = target;
                length = 1;
            } else if (target < min) {
                // Select target..max
                offset = target;
                length = max - target + 1;
            } else if (target > max) {
                offset = min;
                length = target - min + 1;
            } else {
                // target is in between min and max
                if (modifiers == (KeyboardShortcut.CTRL | KeyboardShortcut.SHIFT)) {
                    offset = min;
                    length = target - min + 1;
                } else {
                    offset = target;
                    length = 1;
                }
            }

            clearSelection();
            if (length > 0) {
                setSelectedPhysical(offset, length, true);
            }
        }
    }

    private void handleKeyDown(KeyDownEvent event) {
        int modifiers = KeyboardShortcut.getModifierValue(event.getNativeEvent());
        switch (event.getNativeKeyCode()) {
        case KeyCodes.KEY_UP:
        case KeyCodes.KEY_DOWN:
            break;
        default:
            return;
        }

        if (!allowMultiSelect_)
            modifiers = KeyboardShortcut.NONE;

        event.preventDefault();
        event.stopPropagation();

        switch (modifiers) {
        case 0:
        case KeyboardShortcut.SHIFT:
            break;
        default:
            return;
        }

        sortSelectedRows();
        int min = table_.getRows().getLength();
        int max = -1;
        if (selectedRows_.size() > 0) {
            min = selectedRows_.get(0).getRowIndex();
            max = selectedRows_.get(selectedRows_.size() - 1).getRowIndex();
        }

        switch (event.getNativeKeyCode()) {
        case KeyCodes.KEY_UP: {
            Integer row = findNextValueRow(min, true);
            if (row != null) {
                if (modifiers != KeyboardShortcut.SHIFT)
                    clearSelection();
                setSelectedPhysical(row, 1, true);
                ensureRowVisible(row);
            }
            break;
        }
        case KeyCodes.KEY_DOWN: {
            Integer row = findNextValueRow(max, false);
            if (row != null) {
                if (modifiers != KeyboardShortcut.SHIFT)
                    clearSelection();
                setSelectedPhysical(row, 1, true);
                ensureRowVisible(row);
            }
            break;
        }
        }
    }

    private void ensureRowVisible(final int row) {
        if (scrollPanel_ != null)
            DomUtils.ensureVisibleVert(scrollPanel_.getElement(), getRow(row), 0);
    }

    private Integer findNextValueRow(int physicalRowIndex, boolean up) {
        int limit = up ? -1 : table_.getRows().getLength();
        int increment = up ? -1 : 1;
        for (int i = physicalRowIndex + increment; i != limit; i += increment) {
            if (codec_.isValueRow(getRow(i)))
                return i;
        }
        return null;
    }

    public void clearSelection() {
        while (selectedRows_.size() > 0)
            setSelected(selectedRows_.get(0), false);
    }

    public void addItems(Iterable<TItemInput> items, boolean top) {
        TableSectionElement tbody = Document.get().createTBodyElement();
        for (TItemInput item : items)
            tbody.appendChild(codec_.getRowForItem(item));
        if (top)
            addToTop(tbody);
        else
            getElement().appendChild(tbody);

        codec_.onRowsChanged(tbody);
    }

    protected void addToTop(TableSectionElement tbody) {
        getElement().insertFirst(tbody);
    }

    public void clear() {
        table_.setInnerText("");
        selectedRows_.clear();
    }

    public void focus() {
        if (focusable_)
            table_.focus();
    }

    public int getRowCount() {
        return codec_.getLogicalRowCount(table_);
    }

    public void removeTopRows(int rowCount) {
        if (rowCount <= 0)
            return;

        NodeList<TableSectionElement> tBodies = table_.getTBodies();
        for (int i = 0; i < tBodies.getLength(); i++) {
            rowCount = removeTopRows(tBodies.getItem(i), rowCount);
            if (rowCount == 0)
                return;
        }
    }

    private int removeTopRows(TableSectionElement tbody, int rowCount) {
        while (rowCount > 0 && tbody.getRows().getLength() >= 0) {
            TableRowElement topRow = tbody.getRows().getItem(0);
            if (codec_.isValueRow(topRow))
                rowCount--;
            selectedRows_.remove(topRow);
            topRow.removeFromParent();
        }

        if (tbody.getRows().getLength() > 0)
            codec_.onRowsChanged(tbody);
        else
            tbody.removeFromParent();

        return rowCount;
    }

    public ArrayList<Integer> getSelectedRowIndexes() {
        sortSelectedRows();

        ArrayList<Integer> results = new ArrayList<Integer>();
        for (TableRowElement row : selectedRows_)
            results.add(codec_.physicalOffsetToLogicalOffset(table_, row.getRowIndex()));
        return results;
    }

    private boolean isSelected(TableRowElement tr) {
        return tr.getClassName().contains(selectedClassName_);
    }

    @Deprecated
    public void setSelected(int row, boolean selected) {
        setSelected(getRow(row), selected);
    }

    public void setSelected(int offset, int length, boolean selected) {
        if (codec_.hasNonValueRows()) {
            // If the codec might have stuck in some non-value rows, we need
            // to translate the given offset/length to the actual row
            // offset/length, which may be different (greater).

            Integer start = codec_.logicalOffsetToPhysicalOffset(table_, offset);
            Integer end = codec_.logicalOffsetToPhysicalOffset(table_, offset + length);

            if (start == null || end == null) {
                return;
            }

            offset = start;
            length = end - start;
        }

        setSelectedPhysical(offset, length, selected);
    }

    private void setSelectedPhysical(int offset, int length, boolean selected) {
        for (int i = 0; i < length; i++)
            setSelected(getRow(offset + i), selected);
    }

    public void setSelected(TableRowElement row, boolean selected) {
        try {
            if (row.getParentElement().getParentElement() != table_)
                return;
        } catch (NullPointerException npe) {
            return;
        }

        boolean isCurrentlySelected = isSelected(row);
        if (isCurrentlySelected == selected)
            return;

        if (selected && !codec_.isValueRow(row))
            return;

        setStyleName(row, selectedClassName_, selected);
        if (selected)
            selectedRows_.add(row);
        else
            selectedRows_.remove(row);

        if (selected && !allowMultiSelect_) {
            Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                public void execute() {
                    fireEvent(new SelectionChangedEvent());
                }
            });
        }
    }

    public ArrayList<TItemOutput> getSelectedValues() {
        sortSelectedRows();

        ArrayList<TItemOutput> results = new ArrayList<TItemOutput>();
        for (TableRowElement row : selectedRows_)
            results.add(codec_.getOutputForRow(row));
        return results;
    }

    private void sortSelectedRows() {
        Collections.sort(selectedRows_, new Comparator<TableRowElement>() {
            public int compare(TableRowElement r1, TableRowElement r2) {
                return r1.getRowIndex() - r2.getRowIndex();
            }
        });
    }

    public ArrayList<TItemOutput2> getSelectedValues2() {
        sortSelectedRows();

        ArrayList<TItemOutput2> results = new ArrayList<TItemOutput2>();
        for (TableRowElement row : selectedRows_)
            results.add(codec_.getOutputForRow2(row));
        return results;
    }

    public boolean moveSelectionUp() {
        if (selectedRows_.size() == 0)
            return false;

        sortSelectedRows();
        int top = selectedRows_.get(0).getRowIndex();

        NodeList<TableRowElement> rows = table_.getRows();
        TableRowElement rowToSelect = null;
        while (--top >= 0) {
            TableRowElement row = rows.getItem(top);
            if (codec_.isValueRow(row)) {
                rowToSelect = row;
                break;
            }
        }
        if (rowToSelect == null)
            return false;

        clearSelection();
        setSelected(rowToSelect, true);
        return true;
    }

    public boolean moveSelectionDown() {
        if (selectedRows_.size() == 0)
            return false;

        sortSelectedRows();
        int bottom = selectedRows_.get(selectedRows_.size() - 1).getRowIndex();

        NodeList<TableRowElement> rows = table_.getRows();
        TableRowElement rowToSelect = null;
        while (++bottom < rows.getLength()) {
            TableRowElement row = rows.getItem(bottom);
            if (codec_.isValueRow(row)) {
                rowToSelect = row;
                break;
            }
        }
        if (rowToSelect == null)
            return false;

        clearSelection();
        setSelected(rowToSelect, true);
        return true;
    }

    private TableRowElement getRow(int row) {
        return (TableRowElement) table_.getRows().getItem(row).cast();
    }

    public TableRowElement getTopRow() {
        if (table_.getRows().getLength() > 0)
            return getRow(0);
        else
            return null;
    }

    public ArrayList<TableRowElement> getSelectedRows() {
        return new ArrayList<TableRowElement>(selectedRows_);
    }

    public Rectangle getSelectionRect() {
        if (selectedRows_.size() == 0)
            return null;

        sortSelectedRows();

        TableRowElement first = selectedRows_.get(0);
        TableRowElement last = selectedRows_.get(selectedRows_.size() - 1);
        int top = first.getOffsetTop();
        int bottom = last.getOffsetTop() + last.getOffsetHeight();
        int left = first.getOffsetLeft();
        int width = first.getOffsetWidth();
        return new Rectangle(left, top, width, bottom - top);
    }

    protected Element getEventTargetCell(Event event) {
        Element td = DOM.eventGetTarget(event);
        for (; td != null; td = DOM.getParent(td)) {
            // If it's a TD, it might be the one we're looking for.
            if (td.getPropertyString("tagName").equalsIgnoreCase("td")) {
                // Make sure it's directly a part of this table before returning
                // it.

                Element tr = td.getParentElement();
                Element body = tr.getParentElement();
                Element table = body.getParentElement();
                if (table == getElement()) {
                    return td;
                }
            }
            // If we run into this table's body, we're out of options.
            if (td == getElement()) {
                return null;
            }
        }
        return null;
    }

    public HandlerRegistration addMouseUpHandler(MouseUpHandler handler) {
        return addDomHandler(handler, MouseUpEvent.getType());
    }

    public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) {
        return addDomHandler(handler, MouseOutEvent.getType());
    }

    public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) {
        return addDomHandler(handler, MouseOverEvent.getType());
    }

    public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) {
        return addDomHandler(handler, MouseWheelEvent.getType());
    }

    public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
        return addDomHandler(handler, MouseDownEvent.getType());
    }

    public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
        return addDomHandler(handler, MouseMoveEvent.getType());
    }

    public HandlerRegistration addClickHandler(ClickHandler handler) {
        return addDomHandler(handler, ClickEvent.getType());
    }

    public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
        return addDomHandler(handler, KeyUpEvent.getType());
    }

    public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
        return addDomHandler(handler, KeyDownEvent.getType());
    }

    public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
        return addDomHandler(handler, KeyPressEvent.getType());
    }

    public HandlerRegistration addSelectionChangedHandler(SelectionChangedHandler handler) {
        assert !allowMultiSelect_ : "Selection changed event will only fire " + "if multiselect is disabled";
        return addHandler(handler, SelectionChangedEvent.TYPE);
    }

    private final ArrayList<TableRowElement> selectedRows_ = new ArrayList<TableRowElement>();
    private final ItemCodec<TItemInput, TItemOutput, TItemOutput2> codec_;
    private final TableElement table_;
    private final String selectedClassName_;
    private final boolean allowMultiSelect_;
    private ScrollPanel scrollPanel_;
    private final boolean focusable_;
}