You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

GridConnector.java 41KB

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