PageRenderTime 58ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/client/src/com/vaadin/client/connectors/GridConnector.java

https://gitlab.com/jforge/vaadin
Java | 1428 lines | 1007 code | 212 blank | 209 comment | 151 complexity | 667aee920cbc73867c87e9fc0efec3a1 MD5 | raw file
Possible License(s): Apache-2.0, MPL-2.0-no-copyleft-exception, 0BSD, EPL-1.0, LGPL-2.0, LGPL-2.1, BSD-3-Clause, JSON

Large files files are truncated, but you can click here to view the full file

  1. /*
  2. * Copyright 2000-2014 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.connectors;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.Collection;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.HashSet;
  23. import java.util.Iterator;
  24. import java.util.LinkedHashSet;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Set;
  28. import java.util.logging.Logger;
  29. import com.google.gwt.core.client.Scheduler;
  30. import com.google.gwt.core.client.Scheduler.ScheduledCommand;
  31. import com.google.gwt.dom.client.NativeEvent;
  32. import com.google.gwt.user.client.Timer;
  33. import com.google.gwt.user.client.ui.Widget;
  34. import com.vaadin.client.ComponentConnector;
  35. import com.vaadin.client.ConnectorHierarchyChangeEvent;
  36. import com.vaadin.client.DeferredWorker;
  37. import com.vaadin.client.MouseEventDetailsBuilder;
  38. import com.vaadin.client.annotations.OnStateChange;
  39. import com.vaadin.client.communication.StateChangeEvent;
  40. import com.vaadin.client.connectors.RpcDataSourceConnector.DetailsListener;
  41. import com.vaadin.client.connectors.RpcDataSourceConnector.RpcDataSource;
  42. import com.vaadin.client.data.DataSource.RowHandle;
  43. import com.vaadin.client.renderers.Renderer;
  44. import com.vaadin.client.ui.AbstractFieldConnector;
  45. import com.vaadin.client.ui.AbstractHasComponentsConnector;
  46. import com.vaadin.client.ui.SimpleManagedLayout;
  47. import com.vaadin.client.widget.grid.CellReference;
  48. import com.vaadin.client.widget.grid.CellStyleGenerator;
  49. import com.vaadin.client.widget.grid.DetailsGenerator;
  50. import com.vaadin.client.widget.grid.EditorHandler;
  51. import com.vaadin.client.widget.grid.RowReference;
  52. import com.vaadin.client.widget.grid.RowStyleGenerator;
  53. import com.vaadin.client.widget.grid.events.BodyClickHandler;
  54. import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler;
  55. import com.vaadin.client.widget.grid.events.ColumnReorderEvent;
  56. import com.vaadin.client.widget.grid.events.ColumnReorderHandler;
  57. import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent;
  58. import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler;
  59. import com.vaadin.client.widget.grid.events.GridClickEvent;
  60. import com.vaadin.client.widget.grid.events.GridDoubleClickEvent;
  61. import com.vaadin.client.widget.grid.events.SelectAllEvent;
  62. import com.vaadin.client.widget.grid.events.SelectAllHandler;
  63. import com.vaadin.client.widget.grid.selection.AbstractRowHandleSelectionModel;
  64. import com.vaadin.client.widget.grid.selection.SelectionEvent;
  65. import com.vaadin.client.widget.grid.selection.SelectionHandler;
  66. import com.vaadin.client.widget.grid.selection.SelectionModel;
  67. import com.vaadin.client.widget.grid.selection.SelectionModelMulti;
  68. import com.vaadin.client.widget.grid.selection.SelectionModelNone;
  69. import com.vaadin.client.widget.grid.selection.SelectionModelSingle;
  70. import com.vaadin.client.widget.grid.sort.SortEvent;
  71. import com.vaadin.client.widget.grid.sort.SortHandler;
  72. import com.vaadin.client.widget.grid.sort.SortOrder;
  73. import com.vaadin.client.widgets.Grid;
  74. import com.vaadin.client.widgets.Grid.Column;
  75. import com.vaadin.client.widgets.Grid.FooterCell;
  76. import com.vaadin.client.widgets.Grid.FooterRow;
  77. import com.vaadin.client.widgets.Grid.HeaderCell;
  78. import com.vaadin.client.widgets.Grid.HeaderRow;
  79. import com.vaadin.shared.Connector;
  80. import com.vaadin.shared.data.sort.SortDirection;
  81. import com.vaadin.shared.ui.Connect;
  82. import com.vaadin.shared.ui.grid.DetailsConnectorChange;
  83. import com.vaadin.shared.ui.grid.EditorClientRpc;
  84. import com.vaadin.shared.ui.grid.EditorServerRpc;
  85. import com.vaadin.shared.ui.grid.GridClientRpc;
  86. import com.vaadin.shared.ui.grid.GridColumnState;
  87. import com.vaadin.shared.ui.grid.GridConstants;
  88. import com.vaadin.shared.ui.grid.GridServerRpc;
  89. import com.vaadin.shared.ui.grid.GridState;
  90. import com.vaadin.shared.ui.grid.GridState.SharedSelectionMode;
  91. import com.vaadin.shared.ui.grid.GridStaticSectionState;
  92. import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
  93. import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
  94. import com.vaadin.shared.ui.grid.ScrollDestination;
  95. import elemental.json.JsonObject;
  96. import elemental.json.JsonValue;
  97. /**
  98. * Connects the client side {@link Grid} widget with the server side
  99. * {@link com.vaadin.ui.components.grid.Grid} component.
  100. * <p>
  101. * The Grid is typed to JSONObject. The structure of the JSONObject is described
  102. * at {@link com.vaadin.shared.data.DataProviderRpc#setRowData(int, List)
  103. * DataProviderRpc.setRowData(int, List)}.
  104. *
  105. * @since 7.4
  106. * @author Vaadin Ltd
  107. */
  108. @Connect(com.vaadin.ui.Grid.class)
  109. public class GridConnector extends AbstractHasComponentsConnector implements
  110. SimpleManagedLayout, DeferredWorker {
  111. private static final class CustomCellStyleGenerator implements
  112. CellStyleGenerator<JsonObject> {
  113. @Override
  114. public String getStyle(CellReference<JsonObject> cellReference) {
  115. JsonObject row = cellReference.getRow();
  116. if (!row.hasKey(GridState.JSONKEY_CELLSTYLES)) {
  117. return null;
  118. }
  119. Column<?, JsonObject> column = cellReference.getColumn();
  120. if (!(column instanceof CustomGridColumn)) {
  121. // Selection checkbox column
  122. return null;
  123. }
  124. CustomGridColumn c = (CustomGridColumn) column;
  125. JsonObject cellStylesObject = row
  126. .getObject(GridState.JSONKEY_CELLSTYLES);
  127. assert cellStylesObject != null;
  128. if (cellStylesObject.hasKey(c.id)) {
  129. return cellStylesObject.getString(c.id);
  130. } else {
  131. return null;
  132. }
  133. }
  134. }
  135. private static final class CustomRowStyleGenerator implements
  136. RowStyleGenerator<JsonObject> {
  137. @Override
  138. public String getStyle(RowReference<JsonObject> rowReference) {
  139. JsonObject row = rowReference.getRow();
  140. if (row.hasKey(GridState.JSONKEY_ROWSTYLE)) {
  141. return row.getString(GridState.JSONKEY_ROWSTYLE);
  142. } else {
  143. return null;
  144. }
  145. }
  146. }
  147. /**
  148. * Custom implementation of the custom grid column using a JSONObject to
  149. * represent the cell value and String as a column type.
  150. */
  151. private class CustomGridColumn extends Grid.Column<Object, JsonObject> {
  152. private final String id;
  153. private AbstractRendererConnector<Object> rendererConnector;
  154. private AbstractFieldConnector editorConnector;
  155. public CustomGridColumn(String id,
  156. AbstractRendererConnector<Object> rendererConnector) {
  157. super(rendererConnector.getRenderer());
  158. this.rendererConnector = rendererConnector;
  159. this.id = id;
  160. }
  161. /**
  162. * Sets a new renderer for this column object
  163. *
  164. * @param rendererConnector
  165. * a renderer connector object
  166. */
  167. public void setRenderer(
  168. AbstractRendererConnector<Object> rendererConnector) {
  169. setRenderer(rendererConnector.getRenderer());
  170. this.rendererConnector = rendererConnector;
  171. }
  172. @Override
  173. public Object getValue(final JsonObject obj) {
  174. final JsonObject rowData = obj.getObject(GridState.JSONKEY_DATA);
  175. if (rowData.hasKey(id)) {
  176. final JsonValue columnValue = rowData.get(id);
  177. return rendererConnector.decode(columnValue);
  178. }
  179. return null;
  180. }
  181. private AbstractFieldConnector getEditorConnector() {
  182. return editorConnector;
  183. }
  184. private void setEditorConnector(AbstractFieldConnector editorConnector) {
  185. this.editorConnector = editorConnector;
  186. }
  187. }
  188. /*
  189. * An editor handler using Vaadin RPC to manage the editor state.
  190. */
  191. private class CustomEditorHandler implements EditorHandler<JsonObject> {
  192. private EditorServerRpc rpc = getRpcProxy(EditorServerRpc.class);
  193. private EditorRequest<JsonObject> currentRequest = null;
  194. private boolean serverInitiated = false;
  195. public CustomEditorHandler() {
  196. registerRpc(EditorClientRpc.class, new EditorClientRpc() {
  197. @Override
  198. public void bind(final int rowIndex) {
  199. // call this finally to avoid issues with editing on init
  200. Scheduler.get().scheduleFinally(new ScheduledCommand() {
  201. @Override
  202. public void execute() {
  203. GridConnector.this.getWidget().editRow(rowIndex);
  204. }
  205. });
  206. }
  207. @Override
  208. public void cancel(int rowIndex) {
  209. serverInitiated = true;
  210. GridConnector.this.getWidget().cancelEditor();
  211. }
  212. @Override
  213. public void confirmBind(final boolean bindSucceeded) {
  214. endRequest(bindSucceeded, null, null);
  215. }
  216. @Override
  217. public void confirmSave(boolean saveSucceeded,
  218. String errorMessage, List<String> errorColumnsIds) {
  219. endRequest(saveSucceeded, errorMessage, errorColumnsIds);
  220. }
  221. });
  222. }
  223. @Override
  224. public void bind(EditorRequest<JsonObject> request) {
  225. startRequest(request);
  226. rpc.bind(request.getRowIndex());
  227. }
  228. @Override
  229. public void save(EditorRequest<JsonObject> request) {
  230. startRequest(request);
  231. rpc.save(request.getRowIndex());
  232. }
  233. @Override
  234. public void cancel(EditorRequest<JsonObject> request) {
  235. if (!handleServerInitiated(request)) {
  236. // No startRequest as we don't get (or need)
  237. // a confirmation from the server
  238. rpc.cancel(request.getRowIndex());
  239. }
  240. }
  241. @Override
  242. public Widget getWidget(Grid.Column<?, JsonObject> column) {
  243. assert column != null;
  244. if (column instanceof CustomGridColumn) {
  245. AbstractFieldConnector c = ((CustomGridColumn) column)
  246. .getEditorConnector();
  247. return c != null ? c.getWidget() : null;
  248. } else {
  249. throw new IllegalStateException("Unexpected column type: "
  250. + column.getClass().getName());
  251. }
  252. }
  253. /**
  254. * Used to handle the case where the editor calls us because it was
  255. * invoked by the server via RPC and not by the client. In that case,
  256. * the request can be simply synchronously completed.
  257. *
  258. * @param request
  259. * the request object
  260. * @return true if the request was originally triggered by the server,
  261. * false otherwise
  262. */
  263. private boolean handleServerInitiated(EditorRequest<?> request) {
  264. assert request != null : "Cannot handle null request";
  265. assert currentRequest == null : "Earlier request not yet finished";
  266. if (serverInitiated) {
  267. serverInitiated = false;
  268. request.success();
  269. return true;
  270. } else {
  271. return false;
  272. }
  273. }
  274. private void startRequest(EditorRequest<JsonObject> request) {
  275. assert currentRequest == null : "Earlier request not yet finished";
  276. currentRequest = request;
  277. }
  278. private void endRequest(boolean succeeded, String errorMessage,
  279. List<String> errorColumnsIds) {
  280. assert currentRequest != null : "Current request was null";
  281. /*
  282. * Clear current request first to ensure the state is valid if
  283. * another request is made in the callback.
  284. */
  285. EditorRequest<JsonObject> request = currentRequest;
  286. currentRequest = null;
  287. if (succeeded) {
  288. request.success();
  289. } else {
  290. Collection<Column<?, JsonObject>> errorColumns;
  291. if (errorColumnsIds != null) {
  292. errorColumns = new ArrayList<Grid.Column<?, JsonObject>>();
  293. for (String colId : errorColumnsIds) {
  294. errorColumns.add(columnIdToColumn.get(colId));
  295. }
  296. } else {
  297. errorColumns = null;
  298. }
  299. request.failure(errorMessage, errorColumns);
  300. }
  301. }
  302. }
  303. private class ItemClickHandler implements BodyClickHandler,
  304. BodyDoubleClickHandler {
  305. @Override
  306. public void onClick(GridClickEvent event) {
  307. if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
  308. fireItemClick(event.getTargetCell(), event.getNativeEvent());
  309. }
  310. }
  311. @Override
  312. public void onDoubleClick(GridDoubleClickEvent event) {
  313. if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
  314. fireItemClick(event.getTargetCell(), event.getNativeEvent());
  315. }
  316. }
  317. private void fireItemClick(CellReference<?> cell, NativeEvent mouseEvent) {
  318. String rowKey = getRowKey((JsonObject) cell.getRow());
  319. String columnId = getColumnId(cell.getColumn());
  320. getRpcProxy(GridServerRpc.class)
  321. .itemClick(
  322. rowKey,
  323. columnId,
  324. MouseEventDetailsBuilder
  325. .buildMouseEventDetails(mouseEvent));
  326. }
  327. }
  328. private ColumnReorderHandler<JsonObject> columnReorderHandler = new ColumnReorderHandler<JsonObject>() {
  329. @Override
  330. public void onColumnReorder(ColumnReorderEvent<JsonObject> event) {
  331. if (!columnsUpdatedFromState) {
  332. List<Column<?, JsonObject>> columns = getWidget().getColumns();
  333. final List<String> newColumnOrder = new ArrayList<String>();
  334. for (Column<?, JsonObject> column : columns) {
  335. if (column instanceof CustomGridColumn) {
  336. newColumnOrder.add(((CustomGridColumn) column).id);
  337. } // the other case would be the multi selection column
  338. }
  339. getRpcProxy(GridServerRpc.class).columnsReordered(
  340. newColumnOrder, columnOrder);
  341. columnOrder = newColumnOrder;
  342. getState().columnOrder = newColumnOrder;
  343. }
  344. }
  345. };
  346. private ColumnVisibilityChangeHandler<JsonObject> columnVisibilityChangeHandler = new ColumnVisibilityChangeHandler<JsonObject>() {
  347. @Override
  348. public void onVisibilityChange(
  349. ColumnVisibilityChangeEvent<JsonObject> event) {
  350. if (!columnsUpdatedFromState) {
  351. Column<?, JsonObject> column = event.getColumn();
  352. if (column instanceof CustomGridColumn) {
  353. getRpcProxy(GridServerRpc.class).columnVisibilityChanged(
  354. ((CustomGridColumn) column).id, column.isHidden(),
  355. event.isUserOriginated());
  356. for (GridColumnState state : getState().columns) {
  357. if (state.id.equals(((CustomGridColumn) column).id)) {
  358. state.hidden = event.isHidden();
  359. break;
  360. }
  361. }
  362. } else {
  363. getLogger().warning(
  364. "Visibility changed for a unknown column type in Grid: "
  365. + column.toString() + ", type "
  366. + column.getClass());
  367. }
  368. }
  369. }
  370. };
  371. private static class CustomDetailsGenerator implements DetailsGenerator {
  372. private final Map<Integer, ComponentConnector> indexToDetailsMap = new HashMap<Integer, ComponentConnector>();
  373. @Override
  374. @SuppressWarnings("boxing")
  375. public Widget getDetails(int rowIndex) {
  376. ComponentConnector componentConnector = indexToDetailsMap
  377. .get(rowIndex);
  378. if (componentConnector != null) {
  379. return componentConnector.getWidget();
  380. } else {
  381. return null;
  382. }
  383. }
  384. public void setDetailsConnectorChanges(
  385. Set<DetailsConnectorChange> changes) {
  386. /*
  387. * To avoid overwriting connectors while moving them about, we'll
  388. * take all the affected connectors, first all remove those that are
  389. * removed or moved, then we add back those that are moved or added.
  390. */
  391. /* Remove moved/removed connectors from bookkeeping */
  392. for (DetailsConnectorChange change : changes) {
  393. Integer oldIndex = change.getOldIndex();
  394. Connector removedConnector = indexToDetailsMap.remove(oldIndex);
  395. Connector connector = change.getConnector();
  396. assert removedConnector == null || connector == null
  397. || removedConnector.equals(connector) : "Index "
  398. + oldIndex + " points to " + removedConnector
  399. + " while " + connector + " was expected";
  400. }
  401. /* Add moved/added connectors to bookkeeping */
  402. for (DetailsConnectorChange change : changes) {
  403. Integer newIndex = change.getNewIndex();
  404. ComponentConnector connector = (ComponentConnector) change
  405. .getConnector();
  406. if (connector != null) {
  407. assert newIndex != null : "An existing connector has a missing new index.";
  408. ComponentConnector prevConnector = indexToDetailsMap.put(
  409. newIndex, connector);
  410. assert prevConnector == null : "Connector collision at index "
  411. + newIndex
  412. + " between old "
  413. + prevConnector
  414. + " and new " + connector;
  415. }
  416. }
  417. }
  418. }
  419. @SuppressWarnings("boxing")
  420. private static class DetailsConnectorFetcher implements DeferredWorker {
  421. private static final int FETCH_TIMEOUT_MS = 5000;
  422. public interface Listener {
  423. void fetchHasBeenScheduled(int id);
  424. void fetchHasReturned(int id);
  425. }
  426. /** A flag making sure that we don't call scheduleFinally many times. */
  427. private boolean fetcherHasBeenCalled = false;
  428. /** A rolling counter for unique values. */
  429. private int detailsFetchCounter = 0;
  430. /** A collection that tracks the amount of requests currently underway. */
  431. private Set<Integer> pendingFetches = new HashSet<Integer>(5);
  432. private final ScheduledCommand lazyDetailsFetcher = new ScheduledCommand() {
  433. @Override
  434. public void execute() {
  435. int currentFetchId = detailsFetchCounter++;
  436. pendingFetches.add(currentFetchId);
  437. rpc.sendDetailsComponents(currentFetchId);
  438. fetcherHasBeenCalled = false;
  439. if (listener != null) {
  440. listener.fetchHasBeenScheduled(currentFetchId);
  441. }
  442. assert assertRequestDoesNotTimeout(currentFetchId);
  443. }
  444. };
  445. private DetailsConnectorFetcher.Listener listener = null;
  446. private final GridServerRpc rpc;
  447. public DetailsConnectorFetcher(GridServerRpc rpc) {
  448. assert rpc != null : "RPC was null";
  449. this.rpc = rpc;
  450. }
  451. public void schedule() {
  452. if (!fetcherHasBeenCalled) {
  453. Scheduler.get().scheduleFinally(lazyDetailsFetcher);
  454. fetcherHasBeenCalled = true;
  455. }
  456. }
  457. public void responseReceived(int fetchId) {
  458. if (fetchId < 0) {
  459. /* Ignore negative fetchIds (they're pushed, not fetched) */
  460. return;
  461. }
  462. boolean success = pendingFetches.remove(fetchId);
  463. assert success : "Received a response with an unidentified fetch id";
  464. if (listener != null) {
  465. listener.fetchHasReturned(fetchId);
  466. }
  467. }
  468. @Override
  469. public boolean isWorkPending() {
  470. return fetcherHasBeenCalled || !pendingFetches.isEmpty();
  471. }
  472. private boolean assertRequestDoesNotTimeout(final int fetchId) {
  473. /*
  474. * This method will not be compiled without asserts enabled. This
  475. * only makes sure that any request does not time out.
  476. *
  477. * TODO Should this be an explicit check? Is it worth the overhead?
  478. */
  479. new Timer() {
  480. @Override
  481. public void run() {
  482. assert !pendingFetches.contains(fetchId) : "Fetch id "
  483. + fetchId + " timed out.";
  484. }
  485. }.schedule(FETCH_TIMEOUT_MS);
  486. return true;
  487. }
  488. public void setListener(DetailsConnectorFetcher.Listener listener) {
  489. // if more are needed, feel free to convert this into a collection.
  490. this.listener = listener;
  491. }
  492. }
  493. /**
  494. * The functionality that makes sure that the scroll position is still kept
  495. * up-to-date even if more details are being fetched lazily.
  496. */
  497. private class LazyDetailsScrollAdjuster implements DeferredWorker {
  498. private static final int SCROLL_TO_END_ID = -2;
  499. private static final int NO_SCROLL_SCHEDULED = -1;
  500. private class ScrollStopChecker implements DeferredWorker {
  501. private final ScheduledCommand checkCommand = new ScheduledCommand() {
  502. @Override
  503. public void execute() {
  504. isScheduled = false;
  505. if (queuedFetches.isEmpty()) {
  506. currentRow = NO_SCROLL_SCHEDULED;
  507. destination = null;
  508. }
  509. }
  510. };
  511. private boolean isScheduled = false;
  512. public void schedule() {
  513. if (isScheduled) {
  514. return;
  515. }
  516. Scheduler.get().scheduleDeferred(checkCommand);
  517. isScheduled = true;
  518. }
  519. @Override
  520. public boolean isWorkPending() {
  521. return isScheduled;
  522. }
  523. }
  524. private DetailsConnectorFetcher.Listener fetcherListener = new DetailsConnectorFetcher.Listener() {
  525. @Override
  526. @SuppressWarnings("boxing")
  527. public void fetchHasBeenScheduled(int id) {
  528. if (currentRow != NO_SCROLL_SCHEDULED) {
  529. queuedFetches.add(id);
  530. }
  531. }
  532. @Override
  533. @SuppressWarnings("boxing")
  534. public void fetchHasReturned(int id) {
  535. if (currentRow == NO_SCROLL_SCHEDULED
  536. || queuedFetches.isEmpty()) {
  537. return;
  538. }
  539. queuedFetches.remove(id);
  540. if (currentRow == SCROLL_TO_END_ID) {
  541. getWidget().scrollToEnd();
  542. } else {
  543. getWidget().scrollToRow(currentRow, destination);
  544. }
  545. /*
  546. * Schedule a deferred call whether we should stop adjusting for
  547. * scrolling.
  548. *
  549. * This is done deferredly just because we can't be absolutely
  550. * certain whether this most recent scrolling won't cascade into
  551. * further lazy details loading (perhaps deferredly).
  552. */
  553. scrollStopChecker.schedule();
  554. }
  555. };
  556. private int currentRow = NO_SCROLL_SCHEDULED;
  557. private final Set<Integer> queuedFetches = new HashSet<Integer>();
  558. private final ScrollStopChecker scrollStopChecker = new ScrollStopChecker();
  559. private ScrollDestination destination;
  560. public LazyDetailsScrollAdjuster() {
  561. detailsConnectorFetcher.setListener(fetcherListener);
  562. }
  563. public void adjustForEnd() {
  564. currentRow = SCROLL_TO_END_ID;
  565. }
  566. public void adjustFor(int row, ScrollDestination destination) {
  567. currentRow = row;
  568. this.destination = destination;
  569. }
  570. @Override
  571. public boolean isWorkPending() {
  572. return currentRow != NO_SCROLL_SCHEDULED
  573. || !queuedFetches.isEmpty()
  574. || scrollStopChecker.isWorkPending();
  575. }
  576. }
  577. /**
  578. * Maps a generated column id to a grid column instance
  579. */
  580. private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>();
  581. private AbstractRowHandleSelectionModel<JsonObject> selectionModel;
  582. private Set<String> selectedKeys = new LinkedHashSet<String>();
  583. private List<String> columnOrder = new ArrayList<String>();
  584. /**
  585. * {@link #selectionUpdatedFromState} is set to true when
  586. * {@link #updateSelectionFromState()} makes changes to selection. This flag
  587. * tells the {@code internalSelectionChangeHandler} to not send same data
  588. * straight back to server. Said listener sets it back to false when
  589. * handling that event.
  590. */
  591. private boolean selectionUpdatedFromState;
  592. /**
  593. * {@link #columnsUpdatedFromState} is set to true when
  594. * {@link #updateColumnOrderFromState(List)} is updating the column order
  595. * for the widget. This flag tells the {@link #columnReorderHandler} to not
  596. * send same data straight back to server. After updates, listener sets the
  597. * value back to false.
  598. */
  599. private boolean columnsUpdatedFromState;
  600. private RpcDataSource dataSource;
  601. private SelectionHandler<JsonObject> internalSelectionChangeHandler = new SelectionHandler<JsonObject>() {
  602. @Override
  603. public void onSelect(SelectionEvent<JsonObject> event) {
  604. if (event.isBatchedSelection()) {
  605. return;
  606. }
  607. if (!selectionUpdatedFromState) {
  608. for (JsonObject row : event.getRemoved()) {
  609. selectedKeys.remove(dataSource.getRowKey(row));
  610. }
  611. for (JsonObject row : event.getAdded()) {
  612. selectedKeys.add(dataSource.getRowKey(row));
  613. }
  614. getRpcProxy(GridServerRpc.class).select(
  615. new ArrayList<String>(selectedKeys));
  616. } else {
  617. selectionUpdatedFromState = false;
  618. }
  619. }
  620. };
  621. private ItemClickHandler itemClickHandler = new ItemClickHandler();
  622. private String lastKnownTheme = null;
  623. private final CustomDetailsGenerator customDetailsGenerator = new CustomDetailsGenerator();
  624. private final DetailsConnectorFetcher detailsConnectorFetcher = new DetailsConnectorFetcher(
  625. getRpcProxy(GridServerRpc.class));
  626. private final DetailsListener detailsListener = new DetailsListener() {
  627. @Override
  628. public void reapplyDetailsVisibility(int rowIndex, JsonObject row) {
  629. if (hasDetailsOpen(row)) {
  630. getWidget().setDetailsVisible(rowIndex, true);
  631. detailsConnectorFetcher.schedule();
  632. } else {
  633. getWidget().setDetailsVisible(rowIndex, false);
  634. }
  635. }
  636. private boolean hasDetailsOpen(JsonObject row) {
  637. return row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
  638. && row.getBoolean(GridState.JSONKEY_DETAILS_VISIBLE);
  639. }
  640. @Override
  641. public void closeDetails(int rowIndex) {
  642. getWidget().setDetailsVisible(rowIndex, false);
  643. }
  644. };
  645. private final LazyDetailsScrollAdjuster lazyDetailsScrollAdjuster = new LazyDetailsScrollAdjuster();
  646. @Override
  647. @SuppressWarnings("unchecked")
  648. public Grid<JsonObject> getWidget() {
  649. return (Grid<JsonObject>) super.getWidget();
  650. }
  651. @Override
  652. public GridState getState() {
  653. return (GridState) super.getState();
  654. }
  655. @Override
  656. protected void init() {
  657. super.init();
  658. // All scroll RPC calls are executed finally to avoid issues on init
  659. registerRpc(GridClientRpc.class, new GridClientRpc() {
  660. @Override
  661. public void scrollToStart() {
  662. /*
  663. * no need for lazyDetailsScrollAdjuster, because the start is
  664. * always 0, won't change a bit.
  665. */
  666. Scheduler.get().scheduleFinally(new ScheduledCommand() {
  667. @Override
  668. public void execute() {
  669. getWidget().scrollToStart();
  670. }
  671. });
  672. }
  673. @Override
  674. public void scrollToEnd() {
  675. lazyDetailsScrollAdjuster.adjustForEnd();
  676. Scheduler.get().scheduleFinally(new ScheduledCommand() {
  677. @Override
  678. public void execute() {
  679. getWidget().scrollToEnd();
  680. }
  681. });
  682. }
  683. @Override
  684. public void scrollToRow(final int row,
  685. final ScrollDestination destination) {
  686. lazyDetailsScrollAdjuster.adjustFor(row, destination);
  687. Scheduler.get().scheduleFinally(new ScheduledCommand() {
  688. @Override
  689. public void execute() {
  690. getWidget().scrollToRow(row, destination);
  691. }
  692. });
  693. }
  694. @Override
  695. public void recalculateColumnWidths() {
  696. getWidget().recalculateColumnWidths();
  697. }
  698. @Override
  699. @SuppressWarnings("boxing")
  700. public void setDetailsConnectorChanges(
  701. Set<DetailsConnectorChange> connectorChanges, int fetchId) {
  702. customDetailsGenerator
  703. .setDetailsConnectorChanges(connectorChanges);
  704. List<DetailsConnectorChange> removedFirst = new ArrayList<DetailsConnectorChange>(
  705. connectorChanges);
  706. Collections.sort(removedFirst,
  707. DetailsConnectorChange.REMOVED_FIRST_COMPARATOR);
  708. // refresh moved/added details rows
  709. for (DetailsConnectorChange change : removedFirst) {
  710. Integer oldIndex = change.getOldIndex();
  711. Integer newIndex = change.getNewIndex();
  712. assert oldIndex == null || oldIndex >= 0 : "Got an "
  713. + "invalid old index: " + oldIndex
  714. + " (connector: " + change.getConnector() + ")";
  715. assert newIndex == null || newIndex >= 0 : "Got an "
  716. + "invalid new index: " + newIndex
  717. + " (connector: " + change.getConnector() + ")";
  718. if (oldIndex != null) {
  719. /* Close the old/removed index */
  720. getWidget().setDetailsVisible(oldIndex, false);
  721. if (change.isShouldStillBeVisible()) {
  722. getWidget().setDetailsVisible(oldIndex, true);
  723. }
  724. }
  725. if (newIndex != null) {
  726. /*
  727. * Since the component was lazy loaded, we need to
  728. * refresh the details by toggling it.
  729. */
  730. getWidget().setDetailsVisible(newIndex, false);
  731. getWidget().setDetailsVisible(newIndex, true);
  732. }
  733. }
  734. detailsConnectorFetcher.responseReceived(fetchId);
  735. }
  736. });
  737. getWidget().addSelectionHandler(internalSelectionChangeHandler);
  738. /* Item click events */
  739. getWidget().addBodyClickHandler(itemClickHandler);
  740. getWidget().addBodyDoubleClickHandler(itemClickHandler);
  741. getWidget().addSortHandler(new SortHandler<JsonObject>() {
  742. @Override
  743. public void sort(SortEvent<JsonObject> event) {
  744. List<SortOrder> order = event.getOrder();
  745. String[] columnIds = new String[order.size()];
  746. SortDirection[] directions = new SortDirection[order.size()];
  747. for (int i = 0; i < order.size(); i++) {
  748. SortOrder sortOrder = order.get(i);
  749. CustomGridColumn column = (CustomGridColumn) sortOrder
  750. .getColumn();
  751. columnIds[i] = column.id;
  752. directions[i] = sortOrder.getDirection();
  753. }
  754. if (!Arrays.equals(columnIds, getState().sortColumns)
  755. || !Arrays.equals(directions, getState().sortDirs)) {
  756. // Report back to server if changed
  757. getRpcProxy(GridServerRpc.class).sort(columnIds,
  758. directions, event.isUserOriginated());
  759. }
  760. }
  761. });
  762. getWidget().addSelectAllHandler(new SelectAllHandler<JsonObject>() {
  763. @Override
  764. public void onSelectAll(SelectAllEvent<JsonObject> event) {
  765. getRpcProxy(GridServerRpc.class).selectAll();
  766. }
  767. });
  768. getWidget().setEditorHandler(new CustomEditorHandler());
  769. getWidget().addColumnReorderHandler(columnReorderHandler);
  770. getWidget().addColumnVisibilityChangeHandler(
  771. columnVisibilityChangeHandler);
  772. getWidget().setDetailsGenerator(customDetailsGenerator);
  773. getLayoutManager().registerDependency(this, getWidget().getElement());
  774. layout();
  775. }
  776. @Override
  777. public void onStateChanged(final StateChangeEvent stateChangeEvent) {
  778. super.onStateChanged(stateChangeEvent);
  779. // Column updates
  780. if (stateChangeEvent.hasPropertyChanged("columns")) {
  781. // Remove old columns
  782. purgeRemovedColumns();
  783. // Add new columns
  784. for (GridColumnState state : getState().columns) {
  785. if (!columnIdToColumn.containsKey(state.id)) {
  786. addColumnFromStateChangeEvent(state);
  787. }
  788. updateColumnFromStateChangeEvent(state);
  789. }
  790. }
  791. if (stateChangeEvent.hasPropertyChanged("columnOrder")) {
  792. if (orderNeedsUpdate(getState().columnOrder)) {
  793. updateColumnOrderFromState(getState().columnOrder);
  794. }
  795. }
  796. // Header and footer
  797. if (stateChangeEvent.hasPropertyChanged("header")) {
  798. updateHeaderFromState(getState().header);
  799. }
  800. if (stateChangeEvent.hasPropertyChanged("footer")) {
  801. updateFooterFromState(getState().footer);
  802. }
  803. // Selection
  804. if (stateChangeEvent.hasPropertyChanged("selectionMode")) {
  805. onSelectionModeChange();
  806. updateSelectDeselectAllowed();
  807. } else if (stateChangeEvent
  808. .hasPropertyChanged("singleSelectDeselectAllowed")) {
  809. updateSelectDeselectAllowed();
  810. }
  811. if (stateChangeEvent.hasPropertyChanged("selectedKeys")) {
  812. updateSelectionFromState();
  813. }
  814. // Sorting
  815. if (stateChangeEvent.hasPropertyChanged("sortColumns")
  816. || stateChangeEvent.hasPropertyChanged("sortDirs")) {
  817. onSortStateChange();
  818. }
  819. // Editor
  820. if (stateChangeEvent.hasPropertyChanged("editorEnabled")) {
  821. getWidget().setEditorEnabled(getState().editorEnabled);
  822. }
  823. // Frozen columns
  824. if (stateChangeEvent.hasPropertyChanged("frozenColumnCount")) {
  825. getWidget().setFrozenColumnCount(getState().frozenColumnCount);
  826. }
  827. // Theme features
  828. String activeTheme = getConnection().getUIConnector().getActiveTheme();
  829. if (lastKnownTheme == null) {
  830. lastKnownTheme = activeTheme;
  831. } else if (!lastKnownTheme.equals(activeTheme)) {
  832. getWidget().resetSizesFromDom();
  833. lastKnownTheme = activeTheme;
  834. }
  835. }
  836. private void updateSelectDeselectAllowed() {
  837. SelectionModel<JsonObject> model = getWidget().getSelectionModel();
  838. if (model instanceof SelectionModel.Single<?>) {
  839. ((SelectionModel.Single<?>) model)
  840. .setDeselectAllowed(getState().singleSelectDeselectAllowed);
  841. }
  842. }
  843. private void updateColumnOrderFromState(List<String> stateColumnOrder) {
  844. CustomGridColumn[] columns = new CustomGridColumn[stateColumnOrder
  845. .size()];
  846. int i = 0;
  847. for (String id : stateColumnOrder) {
  848. columns[i] = columnIdToColumn.get(id);
  849. i++;
  850. }
  851. columnsUpdatedFromState = true;
  852. getWidget().setColumnOrder(columns);
  853. columnsUpdatedFromState = false;
  854. columnOrder = stateColumnOrder;
  855. }
  856. private boolean orderNeedsUpdate(List<String> stateColumnOrder) {
  857. if (stateColumnOrder.size() == columnOrder.size()) {
  858. for (int i = 0; i < columnOrder.size(); ++i) {
  859. if (!stateColumnOrder.get(i).equals(columnOrder.get(i))) {
  860. return true;
  861. }
  862. }
  863. return false;
  864. }
  865. return true;
  866. }
  867. private void updateHeaderFromState(GridStaticSectionState state) {
  868. getWidget().setHeaderVisible(state.visible);
  869. while (getWidget().getHeaderRowCount() > 0) {
  870. getWidget().removeHeaderRow(0);
  871. }
  872. for (RowState rowState : state.rows) {
  873. HeaderRow row = getWidget().appendHeaderRow();
  874. for (CellState cellState : rowState.cells) {
  875. CustomGridColumn column = columnIdToColumn
  876. .get(cellState.columnId);
  877. updateHeaderCellFromState(row.getCell(column), cellState);
  878. }
  879. for (Set<String> group : rowState.cellGroups.keySet()) {
  880. Grid.Column<?, ?>[] columns = new Grid.Column<?, ?>[group
  881. .size()];
  882. CellState cellState = rowState.cellGroups.get(group);
  883. int i = 0;
  884. for (String columnId : group) {
  885. columns[i] = columnIdToColumn.get(columnId);
  886. i++;
  887. }
  888. // Set state to be the same as first in group.
  889. updateHeaderCellFromState(row.join(columns), cellState);
  890. }
  891. if (rowState.defaultRow) {
  892. getWidget().setDefaultHeaderRow(row);
  893. }
  894. row.setStyleName(rowState.styleName);
  895. }
  896. }
  897. private void updateHeaderCellFromState(HeaderCell cell, CellState cellState) {
  898. switch (cellState.type) {
  899. case TEXT:
  900. cell.setText(cellState.text);
  901. break;
  902. case HTML:
  903. cell.setHtml(cellState.html);
  904. break;
  905. case WIDGET:
  906. ComponentConnector connector = (ComponentConnector) cellState.connector;
  907. cell.setWidget(connector.getWidget());
  908. break;
  909. default:
  910. throw new IllegalStateException("unexpected cell type: "
  911. + cellState.type);
  912. }
  913. cell.setStyleName(cellState.styleName);
  914. }
  915. private void updateFooterFromState(GridStaticSectionState state) {
  916. getWidget().setFooterVisible(state.visible);
  917. while (getWidget().getFooterRowCount() > 0) {
  918. getWidget().removeFooterRow(0);
  919. }
  920. for (RowState rowState : state.rows) {
  921. FooterRow row = getWidget().appendFooterRow();
  922. for (CellState cellState : rowState.cells) {
  923. CustomGridColumn column = columnIdToColumn
  924. .get(cellState.columnId);
  925. updateFooterCellFromState(row.getCell(column), cellState);
  926. }
  927. for (Set<String> group : rowState.cellGroups.keySet()) {
  928. Grid.Column<?, ?>[] columns = new Grid.Column<?, ?>[group
  929. .size()];
  930. CellState cellState = rowState.cellGroups.get(group);
  931. int i = 0;
  932. for (String columnId : group) {
  933. columns[i] = columnIdToColumn.get(columnId);
  934. i++;
  935. }
  936. // Set state to be the same as first in group.
  937. updateFooterCellFromState(row.join(columns), cellState);
  938. }
  939. row.setStyleName(rowState.styleName);
  940. }
  941. }
  942. private void updateFooterCellFromState(FooterCell cell, CellState cellState) {
  943. switch (cellState.type) {
  944. case TEXT:
  945. cell.setText(cellState.text);
  946. break;
  947. case HTML:
  948. cell.setHtml(cellState.html);
  949. break;
  950. case WIDGET:
  951. ComponentConnector connector = (ComponentConnector) cellState.connector;
  952. cell.setWidget(connector.getWidget());
  953. break;
  954. default:
  955. throw new IllegalStateException("unexpected cell type: "
  956. + cellState.type);
  957. }
  958. cell.setStyleName(cellState.styleName);
  959. }
  960. /**
  961. * Updates a column from a state change event.
  962. *
  963. * @param columnIndex
  964. * The index of the column to update
  965. */
  966. private void updateColumnFromStateChangeEvent(GridColumnState columnState) {
  967. CustomGridColumn column = columnIdToColumn.get(columnState.id);
  968. columnsUpdatedFromState = true;
  969. updateColumnFromState(column, columnState);
  970. columnsUpdatedFromState = false;
  971. }
  972. /**
  973. * Adds a new column to the grid widget from a state change event
  974. *
  975. * @param columnIndex
  976. * The index of the column, according to how it
  977. */
  978. private void addColumnFromStateChangeEvent(GridColumnState state) {
  979. @SuppressWarnings("unchecked")
  980. CustomGridColumn column = new CustomGridColumn(state.id,
  981. ((AbstractRendererConnector<Object>) state.rendererConnector));
  982. columnIdToColumn.put(state.id, column);
  983. /*
  984. * Add column to grid. Reordering is handled as a separate problem.
  985. */
  986. getWidget().addColumn(column);
  987. columnOrder.add(state.id);
  988. }
  989. /**
  990. * If we have a selection column renderer, we need to offset the index by
  991. * one when referring to the column index in the widget.
  992. */
  993. private int getWidgetColumnIndex(final int columnIndex) {
  994. Renderer<Boolean> selectionColumnRenderer = getWidget()
  995. .getSelectionModel().getSelectionColumnRenderer();
  996. int widgetColumnIndex = columnIndex;
  997. if (selectionColumnRenderer != null) {
  998. widgetColumnIndex++;
  999. }
  1000. return widgetColumnIndex;
  1001. }
  1002. /**
  1003. * Updates the column values from a state
  1004. *
  1005. * @param column
  1006. * The column to update
  1007. * @param state
  1008. * The state to get the data from
  1009. */
  1010. @SuppressWarnings("unchecked")
  1011. private static void updateColumnFromState(CustomGridColumn column,
  1012. GridColumnState state) {
  1013. column.setWidth(state.width);
  1014. column.setMinimumWidth(state.minWidth);
  1015. column.setMaximumWidth(state.maxWidth);
  1016. column.setExpandRatio(state.expandRatio);
  1017. assert state.rendererConnector instanceof AbstractRendererConnector : "GridColumnState.rendererConnector is invalid (not subclass of AbstractRendererConnector)";
  1018. column.setRenderer((AbstractRendererConnector<Object>) state.rendererConnector);
  1019. column.setSortable(state.sortable);
  1020. column.setHidden(state.hidden);
  1021. column.setHidable(state.hidable);
  1022. column.setHidingToggleCaption(state.hidingToggleCaption);
  1023. column.setEditable(state.editable);
  1024. column.setEditorConnector((AbstractFieldConnector) state.editorConnector);
  1025. }
  1026. /**
  1027. * Removes any orphan columns that has been removed from the state from the
  1028. * grid
  1029. */
  1030. private void purgeRemovedColumns() {
  1031. // Get columns still registered in the state
  1032. Set<String> columnsInState = new HashSet<String>();
  1033. for (GridColumnState columnState : getState().columns) {
  1034. columnsInState.add(columnState.id);
  1035. }
  1036. // Remove column no longer in state
  1037. Iterator<String> columnIdIterator = columnIdToColumn.keySet()
  1038. .iterator();
  1039. while (columnIdIterator.hasNext()) {
  1040. String id = columnIdIterator.next();
  1041. if (!columnsInState.contains(id)) {
  1042. CustomGridColumn column = columnIdToColumn.get(id);
  1043. columnIdIterator.remove();
  1044. getWidget().removeColumn(column);
  1045. columnOrder.remove(id);
  1046. }
  1047. }
  1048. }
  1049. public void setDataSource(RpcDataSource dataSource) {
  1050. this.dataSource = dataSource;
  1051. getWidget().setDataSource(this.dataSource);
  1052. }
  1053. private void onSelectionModeChange() {
  1054. SharedSelectionMode mode = getState().selectionMode;
  1055. if (mode == null) {
  1056. getLogger().fine("ignored mode change");
  1057. return;
  1058. }
  1059. AbstractRowHandleSelectionModel<JsonObject> model = createSelectionModel(mode);
  1060. if (selectionModel == null
  1061. || !model.getClass().equals(selectionModel.getClass())) {
  1062. selectionModel = model;
  1063. getWidget().setSelectionModel(model);
  1064. selectedKeys.clear();
  1065. }
  1066. }
  1067. @OnStateChange("hasCellStyleGenerator")
  1068. private void onCellStyleGeneratorChange() {
  1069. if (getState().hasCellStyleGenerator) {
  1070. getWidget().setCellStyleGenerator(new CustomCellStyleGenerator());
  1071. } else {
  1072. getWidget().setCellStyleGenerator(null);
  1073. }
  1074. }
  1075. @OnStateChange("hasRowStyleGenerator")
  1076. private void onRowStyleGeneratorChange() {
  1077. if (getState().hasRowStyleGenerator) {
  1078. getWidget().setRowStyleGenerator(new CustomRowStyleGenerator());
  1079. } else {
  1080. getWidget().setRowStyleGenerator(null);
  1081. }
  1082. }
  1083. private void updateSelectionFromState() {
  1084. boolean changed = false;
  1085. List<String> stateKeys = getState().selectedKeys;
  1086. // find new deselections
  1087. for (String key : selectedKeys) {
  1088. if (!stateKeys.contains(key)) {
  1089. changed = true;
  1090. deselectByHandle(dataSource.getHandleByKey(key));
  1091. }
  1092. }
  1093. // find new selections
  1094. for (String key : stateKeys) {
  1095. if (!selectedKeys.contains(key)) {
  1096. changed = true;
  1097. selectByHandle(dataSource.getHandleByKey(key));
  1098. }
  1099. }
  1100. /*
  1101. * A defensive copy in case the collection in the state is mutated
  1102. * instead of re-assigned.
  1103. */
  1104. selectedKeys = new LinkedHashSet<String>(stateKeys);
  1105. /*
  1106. * We need to fire this event so that Grid is able to re-render the
  1107. * selection changes (if applicable).
  1108. */
  1109. if (changed) {
  1110. // At least for now there's no way to send the selected and/or
  1111. // deselected row data. Some data is only stored as keys
  1112. selectionUpdatedFromState = true;
  1113. getWidget().fireEvent(
  1114. new SelectionEvent<JsonObject>(getWidget(),
  1115. (List<JsonObject>) null, null, false));
  1116. }
  1117. }
  1118. private void onSortStateChange() {
  1119. List<SortOrder> sortOrder = new ArrayList<SortOrder>();
  1120. String[] sortColumns = getState().sortColumns;
  1121. SortDirection[] sortDirs = getState().sortDirs;
  1122. for (int i = 0; i < sortColumns.length; i++) {
  1123. sortOrder.add(new SortOrder(columnIdToColumn.get(sortColumns[i]),
  1124. sortDirs[i]));
  1125. }
  1126. getWidget().setSortOrder(sortOrder);
  1127. }
  1128. private Logger getLogger() {
  1129. return Logger.getLogger(getClass().getName());
  1130. }
  1131. @SuppressWarnings("static-method")
  1132. private AbstractRowHandleSelectionModel<JsonObject> createSelectionModel(
  1133. SharedSelectionMode mode) {
  1134. switch (mode) {
  1135. case SINGLE:
  1136. return new SelectionModelSingle<JsonObject>();
  1137. case MULTI:
  1138. return new SelectionModelMulti<JsonObject>();
  1139. case NONE:
  1140. return new SelectionModelNone<JsonObject>();
  1141. default:
  1142. throw new IllegalStateException("unexpected mode value: " + mode);

Large files files are truncated, but you can click here to view the full file