/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.connectors.treegrid;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.Event;
import com.vaadin.client.annotations.OnStateChange;
import com.vaadin.client.connectors.grid.GridConnector;
import com.vaadin.client.data.AbstractRemoteDataSource;
import com.vaadin.client.data.DataChangeHandler;
import com.vaadin.client.data.DataSource;
import com.vaadin.client.renderers.HierarchyRenderer;
import com.vaadin.client.widget.grid.EventCellReference;
import com.vaadin.client.widget.grid.GridEventHandler;
import com.vaadin.client.widget.treegrid.TreeGrid;
import com.vaadin.client.widgets.Grid;
import com.vaadin.shared.Range;
import com.vaadin.shared.data.DataCommunicatorConstants;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.treegrid.FocusParentRpc;
import com.vaadin.shared.ui.treegrid.FocusRpc;
import com.vaadin.shared.ui.treegrid.NodeCollapseRpc;
import com.vaadin.shared.ui.treegrid.TreeGridClientRpc;
import com.vaadin.shared.ui.treegrid.TreeGridCommunicationConstants;
import com.vaadin.shared.ui.treegrid.TreeGridState;
import elemental.json.JsonObject;
/**
* A connector class for the TreeGrid component.
*
* @author Vaadin Ltd
* @since 8.1
*/
@Connect(com.vaadin.ui.TreeGrid.class)
public class TreeGridConnector extends GridConnector {
private static enum AwaitingRowsState {
NONE, COLLAPSE, EXPAND
}
public TreeGridConnector() {
registerRpc(FocusRpc.class, (rowIndex, cellIndex) -> {
getWidget().focusCell(rowIndex, cellIndex);
});
}
private String hierarchyColumnId;
private HierarchyRenderer hierarchyRenderer;
private Set<String> rowKeysPendingExpand = new HashSet<>();
private AwaitingRowsState awaitingRowsState = AwaitingRowsState.NONE;
@Override
public TreeGrid getWidget() {
return (TreeGrid) super.getWidget();
}
@Override
public TreeGridState getState() {
return (TreeGridState) super.getState();
}
/**
* This method has been scheduled finally to avoid possible race conditions
* between state change handling for the Grid and its columns. The renderer
* of the column is set in a state change handler, and might not be
* available when this method is executed.
* <p>
* TODO: This might need some clean up if we decide to allow setting a new
* renderer for hierarchy columns.
*/
@OnStateChange("hierarchyColumnId")
void updateHierarchyColumn() {
Scheduler.get().scheduleFinally(() -> {
// Id of old hierarchy column
String oldHierarchyColumnId = hierarchyColumnId;
// Id of new hierarchy column. Choose first when nothing explicitly
// set
String newHierarchyColumnId = getState().hierarchyColumnId;
if (newHierarchyColumnId == null
&& !getState().columnOrder.isEmpty()) {
newHierarchyColumnId = getState().columnOrder.get(0);
}
// Columns
Grid.Column<?, ?> newColumn = getColumn(newHierarchyColumnId);
Grid.Column<?, ?> oldColumn = getColumn(oldHierarchyColumnId);
if (newColumn == null && oldColumn == null) {
// No hierarchy column defined
return;
}
// Unwrap renderer of old column
if (oldColumn != null
&& oldColumn.getRenderer() instanceof HierarchyRenderer) {
oldColumn.setRenderer(
((HierarchyRenderer) oldColumn.getRenderer())
.getInnerRenderer());
}
// Wrap renderer of new column
if (newColumn != null) {
HierarchyRenderer wrapperRenderer = getHierarchyRenderer();
wrapperRenderer.setInnerRenderer(newColumn.getRenderer());
newColumn.setRenderer(wrapperRenderer);
// Set frozen columns again after setting hierarchy column as
// setRenderer() replaces DOM elements
getWidget().setFrozenColumnCount(getState().frozenColumnCount);
hierarchyColumnId = newHierarchyColumnId;
} else {
Logger.getLogger(TreeGridConnector.class.getName()).warning(
"Couldn't find column: " + newHierarchyColumnId);
}
});
}
private HierarchyRenderer getHierarchyRenderer() {
if (hierarchyRenderer == null) {
hierarchyRenderer = new HierarchyRenderer(this::setCollapsed,
getState().primaryStyleName);
}
return hierarchyRenderer;
}
@Override
protected void init() {
super.init();
// Swap Grid's CellFocusEventHandler to this custom one
// The handler is identical to the original one except for the child
// widget check
replaceCellFocusEventHandler(getWidget(), new CellFocusEventHandler());
getWidget().addBrowserEventHandler(5, new NavigationEventHandler());
registerRpc(TreeGridClientRpc.class, new TreeGridClientRpc() {
@Override
public void setExpanded(List<String> keys) {
rowKeysPendingExpand.addAll(keys);
checkExpand();
}
@Override
public void setCollapsed(List<String> keys) {
rowKeysPendingExpand.removeAll(keys);
}
@Override
public void clearPendingExpands() {
rowKeysPendingExpand.clear();
}
});
}
@Override
public void setDataSource(DataSource<JsonObject> dataSource) {
super.setDataSource(dataSource);
dataSource.addDataChangeHandler(new DataChangeHandler() {
@Override
public void dataUpdated(int firstRowIndex, int numberOfRows) {
checkExpand(firstRowIndex, numberOfRows);
}
@Override
public void dataRemoved(int firstRowIndex, int numberOfRows) {
if (awaitingRowsState == AwaitingRowsState.COLLAPSE) {
awaitingRowsState = AwaitingRowsState.NONE;
}
checkExpand();
}
@Override
public void dataAdded(int firstRowIndex, int numberOfRows) {
if (awaitingRowsState == AwaitingRowsState.EXPAND) {
awaitingRowsState = AwaitingRowsState.NONE;
}
checkExpand();
}
@Override
public void dataAvailable(int firstRowIndex, int numberOfRows) {
// NO-OP
}
@Override
public void resetDataAndSize(int estimatedNewDataSize) {
awaitingRowsState = AwaitingRowsState.NONE;
}
});
}
@OnStateChange("primaryStyleName")
private void updateHierarchyRendererStyleName() {
getHierarchyRenderer().setStyleNames(getState().primaryStyleName);
}
private native void replaceCellFocusEventHandler(Grid<?> grid,
GridEventHandler<?> eventHandler)
/*-{
var browserEventHandlers = grid.@com.vaadin.client.widgets.Grid::browserEventHandlers;
// FocusEventHandler is initially 5th in the list of browser event handlers
browserEventHandlers.@java.util.List::set(*)(5, eventHandler);
}-*/;
private native EventCellReference<?> getEventCell(Grid<?> grid)
/*-{
return grid.@com.vaadin.client.widgets.Grid::eventCell;
}-*/;
private boolean isHierarchyColumn(EventCellReference<JsonObject> cell) {
return cell.getColumn().getRenderer() instanceof HierarchyRenderer;
}
/**
* Delegates to {@link #setCollapsed(int, boolean, boolean)}, with
* {@code userOriginated} as {@code true}.
*
* @see #setCollapsed(int, boolean, boolean)
*/
private void setCollapsed(int rowIndex, boolean collapsed) {
setCollapsed(rowIndex, collapsed, true);
}
/**
* Set the collapse state for the row in the given index.
* <p>
* Calling this method will have no effect if a response has not yet been
* received for a previous call to this method.
*
* @param rowIndex
* index of the row to set the state for
* @param collapsed
* {@code true} to collapse the row, {@code false} to expand the
* row
* @param userOriginated
* whether this method was originated from a user interaction
*/
private void setCollapsed(int rowIndex, boolean collapsed,
boolean userOriginated) {
if (isAwaitingRowChange()) {
return;
}
if (collapsed) {
awaitingRowsState = AwaitingRowsState.COLLAPSE;
} else {
awaitingRowsState = AwaitingRowsState.EXPAND;
}
String rowKey = getRowKey(getDataSource().getRow(rowIndex));
getRpcProxy(NodeCollapseRpc.class).setNodeCollapsed(rowKey, rowIndex,
collapsed, userOriginated);
}
/**
* Class to replace
* {@link com.vaadin.client.widgets.Grid.CellFocusEventHandler}. The only
* difference is that it handles events originated from widgets in hierarchy
* cells.
*/
private class CellFocusEventHandler
implements GridEventHandler<JsonObject> {
@Override
public void onEvent(Grid.GridEvent<JsonObject> event) {
Element target = Element.as(event.getDomEvent().getEventTarget());
boolean elementInChildWidget = getWidget()
.isElementInChildWidget(target);
// Ignore if event was handled by keyboard navigation handler
if (event.isHandled() && !elementInChildWidget) {
return;
}
// Ignore target in child widget but handle hierarchy widget
if (elementInChildWidget
&& !HierarchyRenderer.isElementInHierarchyWidget(target)) {
return;
}
Collection<String> navigation = getNavigationEvents(getWidget());
if (navigation.contains(event.getDomEvent().getType())) {
handleNavigationEvent(getWidget(), event);
}
}
private native Collection<String> getNavigationEvents(Grid<?> grid)
/*-{
return grid.@com.vaadin.client.widgets.Grid::cellFocusHandler
.@com.vaadin.client.widgets.Grid.CellFocusHandler::getNavigationEvents()();
}-*/;
private native void handleNavigationEvent(Grid<?> grid,
Grid.GridEvent<JsonObject> event)
/*-{
grid.@com.vaadin.client.widgets.Grid::cellFocusHandler
.@com.vaadin.client.widgets.Grid.CellFocusHandler::handleNavigationEvent(*)(
event.@com.vaadin.client.widgets.Grid.GridEvent::getDomEvent()(),
event.@com.vaadin.client.widgets.Grid.GridEvent::getCell()())
}-*/;
}
private class NavigationEventHandler
implements GridEventHandler<JsonObject> {
@Override
public void onEvent(Grid.GridEvent<JsonObject> event) {
if (event.isHandled()) {
return;
}
Event domEvent = event.getDomEvent();
if (!domEvent.getType().equals(BrowserEvents.KEYDOWN)) {
return;
}
// Navigate within hierarchy with ARROW KEYs
if (domEvent.getKeyCode() == KeyCodes.KEY_LEFT
|| domEvent.getKeyCode() == KeyCodes.KEY_RIGHT) {
event.setHandled(true);
EventCellReference<JsonObject> cell = event.getCell();
// Hierarchy metadata
JsonObject rowData = cell.getRow();
if (rowData == null) {
// Row data is lost from the cache, i.e. the row is at least outside the visual area,
// let's scroll the row into the view
getWidget().scrollToRow(cell.getRowIndex());
} else if (rowData.hasKey(
TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION)) {
JsonObject rowDescription = rowData.getObject(
TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION);
boolean leaf = rowDescription.getBoolean(
TreeGridCommunicationConstants.ROW_LEAF);
boolean collapsed = isCollapsed(rowData);
switch (domEvent.getKeyCode()) {
case KeyCodes.KEY_RIGHT:
if (collapsed && !leaf) {
setCollapsed(cell.getRowIndex(), false);
}
break;
case KeyCodes.KEY_LEFT:
if (collapsed || leaf) {
// navigate up
int columnIndex = cell.getColumnIndex();
getRpcProxy(FocusParentRpc.class).focusParent(
cell.getRowIndex(), columnIndex);
} else if (isCollapseAllowed(rowDescription)) {
setCollapsed(cell.getRowIndex(), true);
}
break;
}
}
}
}
}
private boolean isAwaitingRowChange() {
return awaitingRowsState != AwaitingRowsState.NONE;
}
private void checkExpand() {
Range cache = ((AbstractRemoteDataSource) getDataSource())
.getCachedRange();
checkExpand(cache.getStart(), cache.length());
}
private void checkExpand(int firstRowIndex, int numberOfRows) {
if (rowKeysPendingExpand.isEmpty() || isAwaitingRowChange()) {
// will not perform the check if an expand or collapse action is
// already pending or there are no rows pending expand
return;
}
for (int rowIndex = firstRowIndex; rowIndex < firstRowIndex
+ numberOfRows; rowIndex++) {
String rowKey = getDataSource().getRow(rowIndex)
.getString(DataCommunicatorConstants.KEY);
if (rowKeysPendingExpand.remove(rowKey)) {
setCollapsed(rowIndex, false, false);
return;
}
}
}
private static boolean isCollapsed(JsonObject rowData) {
assert rowData
.hasKey(TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION) : "missing hierarchy data for row "
+ rowData.asString();
return rowData
.getObject(
TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION)
.getBoolean(TreeGridCommunicationConstants.ROW_COLLAPSED);
}
/**
* Checks if the item can be collapsed
*
* @param row the item row
* @return {@code true} if the item is allowed to be collapsed, {@code false} otherwise.
*/
public static boolean isCollapseAllowed(JsonObject row) {
return row.getBoolean(
TreeGridCommunicationConstants.ROW_COLLAPSE_ALLOWED);
}
}