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 45KB

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