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.

DetailsManagerConnector.java 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. /*
  2. * Copyright 2000-2018 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.grid;
  17. import java.util.HashMap;
  18. import java.util.Map;
  19. import java.util.TreeMap;
  20. import com.google.gwt.core.client.Scheduler;
  21. import com.google.gwt.core.client.Scheduler.ScheduledCommand;
  22. import com.google.gwt.dom.client.Element;
  23. import com.google.gwt.event.shared.HandlerRegistration;
  24. import com.google.gwt.user.client.ui.Widget;
  25. import com.vaadin.client.ComponentConnector;
  26. import com.vaadin.client.ConnectorMap;
  27. import com.vaadin.client.LayoutManager;
  28. import com.vaadin.client.ServerConnector;
  29. import com.vaadin.client.WidgetUtil;
  30. import com.vaadin.client.connectors.data.DataCommunicatorConnector;
  31. import com.vaadin.client.data.DataChangeHandler;
  32. import com.vaadin.client.extensions.AbstractExtensionConnector;
  33. import com.vaadin.client.ui.layout.ElementResizeListener;
  34. import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent;
  35. import com.vaadin.client.widget.escalator.events.SpacerIndexChangedHandler;
  36. import com.vaadin.client.widget.grid.HeightAwareDetailsGenerator;
  37. import com.vaadin.client.widgets.Grid;
  38. import com.vaadin.shared.Range;
  39. import com.vaadin.shared.Registration;
  40. import com.vaadin.shared.data.DataCommunicatorClientRpc;
  41. import com.vaadin.shared.ui.Connect;
  42. import com.vaadin.shared.ui.grid.DetailsManagerState;
  43. import com.vaadin.shared.ui.grid.GridState;
  44. import com.vaadin.ui.Grid.DetailsManager;
  45. import elemental.json.JsonObject;
  46. /**
  47. * Connector class for {@link DetailsManager} of the Grid component.
  48. *
  49. * @author Vaadin Ltd
  50. * @since 8.0
  51. */
  52. @Connect(DetailsManager.class)
  53. public class DetailsManagerConnector extends AbstractExtensionConnector {
  54. /* Map for tracking which details are open on which row */
  55. private TreeMap<Integer, String> indexToDetailConnectorId = new TreeMap<>();
  56. /* For listening data changes that originate from DataSource. */
  57. private Registration dataChangeRegistration;
  58. /* For listening spacer index changes that originate from Escalator. */
  59. private HandlerRegistration spacerIndexChangedHandlerRegistration;
  60. /* For listening when Escalator's visual range is changed. */
  61. private HandlerRegistration rowVisibilityChangeHandlerRegistration;
  62. private final Map<Element, ScheduledCommand> elementToResizeCommand = new HashMap<Element, Scheduler.ScheduledCommand>();
  63. private final ElementResizeListener detailsRowResizeListener = event -> {
  64. if (elementToResizeCommand.containsKey(event.getElement())) {
  65. Scheduler.get().scheduleFinally(
  66. elementToResizeCommand.get(event.getElement()));
  67. }
  68. };
  69. /* for delayed alert if Grid needs to run or cancel pending operations */
  70. private boolean delayedDetailsAddedOrUpdatedAlertTriggered = false;
  71. private boolean delayedDetailsAddedOrUpdated = false;
  72. /* for delayed re-positioning of Escalator contents to prevent gaps */
  73. /* -1 is a possible spacer index in Escalator so can't be used as default */
  74. private boolean delayedRepositioningTriggered = false;
  75. private Integer delayedRepositioningStart = null;
  76. private Integer delayedRepositioningEnd = null;
  77. /* calculated when the first details row is opened */
  78. private Double spacerCellBorderHeights = null;
  79. private Range availableRowRange = Range.emptyRange();
  80. private Range latestVisibleRowRange = Range.emptyRange();
  81. /**
  82. * DataChangeHandler for updating the visibility of detail widgets.
  83. */
  84. private final class DetailsChangeHandler implements DataChangeHandler {
  85. @Override
  86. public void resetDataAndSize(int estimatedNewDataSize) {
  87. // No need to do anything, dataUpdated and dataAvailable take care
  88. // of cleanup.
  89. }
  90. @Override
  91. public void dataUpdated(int firstRowIndex, int numberOfRows) {
  92. if (!getState().hasDetailsGenerator) {
  93. markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
  94. return;
  95. }
  96. Range updatedRange = Range.withLength(firstRowIndex, numberOfRows);
  97. // NOTE: this relies on Escalator getting updated first
  98. Range newVisibleRowRange = getWidget().getEscalator()
  99. .getVisibleRowRange();
  100. if (updatedRange.partitionWith(availableRowRange)[1]
  101. .length() != updatedRange.length()
  102. || availableRowRange.partitionWith(newVisibleRowRange)[1]
  103. .length() != newVisibleRowRange.length()) {
  104. // full visible range not available yet or full refresh coming
  105. // up anyway, leave updating to dataAvailable
  106. if (numberOfRows == 1
  107. && latestVisibleRowRange.contains(firstRowIndex)) {
  108. // A single details row has been opened or closed within
  109. // visual range, trigger scrollTo after dataAvailable has
  110. // done its thing. Do not attempt to scroll to details rows
  111. // that are opened outside of the visual range.
  112. Scheduler.get().scheduleFinally(() -> {
  113. getParent().singleDetailsOpened(firstRowIndex);
  114. // we don't know yet whether there are details or not,
  115. // mark them added or updated just in case, so that
  116. // the potential scrolling attempt gets triggered after
  117. // another layout phase is finished
  118. markDetailsAddedOrUpdatedForDelayedAlertToGrid(true);
  119. });
  120. }
  121. return;
  122. }
  123. // only trigger scrolling attempt if the single updated row is
  124. // within existing visual range
  125. boolean scrollToFirst = numberOfRows == 1
  126. && latestVisibleRowRange.contains(firstRowIndex);
  127. if (!newVisibleRowRange.equals(latestVisibleRowRange)) {
  128. // update visible range
  129. latestVisibleRowRange = newVisibleRowRange;
  130. // do full refresh
  131. detachOldAndRefreshCurrentDetails();
  132. } else {
  133. // refresh only the updated range
  134. refreshDetailsVisibilityWithRange(updatedRange);
  135. // the update may have affected details row contents and size,
  136. // recalculation and triggering of any pending navigation
  137. // confirmations etc. is needed
  138. triggerDelayedRepositioning(firstRowIndex, numberOfRows);
  139. }
  140. if (scrollToFirst) {
  141. // scroll to opened row (if it got closed instead, nothing
  142. // happens)
  143. getParent().singleDetailsOpened(firstRowIndex);
  144. markDetailsAddedOrUpdatedForDelayedAlertToGrid(true);
  145. }
  146. }
  147. @Override
  148. public void dataRemoved(int firstRowIndex, int numberOfRows) {
  149. if (!getState().hasDetailsGenerator) {
  150. markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
  151. return;
  152. }
  153. Range removing = Range.withLength(firstRowIndex, numberOfRows);
  154. // update the handled range to only contain rows that fall before
  155. // the removed range
  156. latestVisibleRowRange = Range
  157. .between(latestVisibleRowRange.getStart(),
  158. Math.max(latestVisibleRowRange.getStart(),
  159. Math.min(firstRowIndex,
  160. latestVisibleRowRange.getEnd())));
  161. // reduce the available range accordingly
  162. Range[] partitions = availableRowRange.partitionWith(removing);
  163. Range removedAbove = partitions[0];
  164. Range removedAvailable = partitions[1];
  165. availableRowRange = Range.withLength(
  166. Math.max(0,
  167. availableRowRange.getStart()
  168. - removedAbove.length()),
  169. Math.max(0, availableRowRange.length()
  170. - removedAvailable.length()));
  171. for (int i = 0; i < numberOfRows; ++i) {
  172. int rowIndex = firstRowIndex + i;
  173. if (indexToDetailConnectorId.containsKey(rowIndex)) {
  174. String id = indexToDetailConnectorId.get(rowIndex);
  175. ComponentConnector connector = (ComponentConnector) ConnectorMap
  176. .get(getConnection()).getConnector(id);
  177. if (connector != null) {
  178. Element element = connector.getWidget().getElement();
  179. elementToResizeCommand.remove(element);
  180. getLayoutManager().removeElementResizeListener(element,
  181. detailsRowResizeListener);
  182. }
  183. indexToDetailConnectorId.remove(rowIndex);
  184. }
  185. }
  186. // Grid and Escalator take care of their own cleanup at removal, no
  187. // need to clear details from those. Because this removal happens
  188. // instantly any pending scroll to row or such should not need
  189. // another attempt and unless something else causes such need the
  190. // pending operations should be cleared out.
  191. markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
  192. }
  193. @Override
  194. public void dataAvailable(int firstRowIndex, int numberOfRows) {
  195. if (!getState().hasDetailsGenerator) {
  196. markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
  197. return;
  198. }
  199. // update available range
  200. availableRowRange = Range.withLength(firstRowIndex, numberOfRows);
  201. // NOTE: this relies on Escalator getting updated first
  202. Range newVisibleRowRange = getWidget().getEscalator()
  203. .getVisibleRowRange();
  204. // only process the section that is actually available
  205. newVisibleRowRange = availableRowRange
  206. .partitionWith(newVisibleRowRange)[1];
  207. if (newVisibleRowRange.equals(latestVisibleRowRange)) {
  208. // no need to update
  209. return;
  210. }
  211. // check whether the visible range has simply got shortened
  212. // (e.g. by changing the default row height)
  213. boolean subsectionOfOld = latestVisibleRowRange
  214. .partitionWith(newVisibleRowRange)[1]
  215. .length() == newVisibleRowRange.length();
  216. // update visible range
  217. latestVisibleRowRange = newVisibleRowRange;
  218. if (subsectionOfOld) {
  219. // only detach extra rows
  220. detachExcludingRange(latestVisibleRowRange);
  221. } else {
  222. // there are completely new visible rows, full refresh
  223. detachOldAndRefreshCurrentDetails();
  224. }
  225. }
  226. @Override
  227. public void dataAdded(int firstRowIndex, int numberOfRows) {
  228. refreshDetailsVisibilityWithRange(
  229. Range.withLength(firstRowIndex, numberOfRows));
  230. }
  231. }
  232. /**
  233. * Height aware details generator for client-side Grid.
  234. */
  235. @SuppressWarnings("deprecation")
  236. private class CustomDetailsGenerator
  237. implements HeightAwareDetailsGenerator {
  238. @Override
  239. public Widget getDetails(int rowIndex) {
  240. String id = getDetailsComponentConnectorId(rowIndex);
  241. if (id == null) {
  242. detachIfNeeded(rowIndex, id);
  243. return null;
  244. }
  245. String oldId = indexToDetailConnectorId.get(rowIndex);
  246. if (oldId != null && !oldId.equals(id)) {
  247. // remove outdated connector
  248. ComponentConnector connector = (ComponentConnector) ConnectorMap
  249. .get(getConnection()).getConnector(oldId);
  250. if (connector != null) {
  251. Element element = connector.getWidget().getElement();
  252. elementToResizeCommand.remove(element);
  253. getLayoutManager().removeElementResizeListener(element,
  254. detailsRowResizeListener);
  255. }
  256. }
  257. indexToDetailConnectorId.put(rowIndex, id);
  258. getWidget().setDetailsVisible(rowIndex, true);
  259. Widget widget = getConnector(id).getWidget();
  260. getLayoutManager().addElementResizeListener(widget.getElement(),
  261. detailsRowResizeListener);
  262. elementToResizeCommand.put(widget.getElement(),
  263. createResizeCommand(rowIndex, widget.getElement()));
  264. return widget;
  265. }
  266. private ScheduledCommand createResizeCommand(final int rowIndex,
  267. final Element element) {
  268. return () -> {
  269. // It should not be possible to get here without calculating
  270. // the spacerCellBorderHeights or without having the details
  271. // row open, nor for this command to be triggered while
  272. // layout is running, but it's safer to check anyway.
  273. if (spacerCellBorderHeights != null
  274. && !getLayoutManager().isLayoutRunning()
  275. && getDetailsComponentConnectorId(rowIndex) != null) {
  276. // Measure and set details height if element is visible
  277. if (WidgetUtil.isDisplayed(element)) {
  278. double height = getLayoutManager().getOuterHeightDouble(
  279. element) + spacerCellBorderHeights;
  280. getWidget().setDetailsHeight(rowIndex, height);
  281. }
  282. }
  283. };
  284. }
  285. @Override
  286. public double getDetailsHeight(int rowIndex) {
  287. // Case of null is handled in the getDetails method and this method
  288. // will not called if it returns null.
  289. String id = getDetailsComponentConnectorId(rowIndex);
  290. ComponentConnector componentConnector = getConnector(id);
  291. getLayoutManager().setNeedsMeasureRecursively(componentConnector);
  292. if (!getLayoutManager().isLayoutRunning()
  293. && !getConnection().getMessageHandler().isUpdatingState()) {
  294. getLayoutManager().layoutNow();
  295. }
  296. Element element = componentConnector.getWidget().getElement();
  297. if (spacerCellBorderHeights == null) {
  298. // If theme is changed, new details generator is created from
  299. // scratch, so this value doesn't need to be updated elsewhere.
  300. spacerCellBorderHeights = WidgetUtil
  301. .getBorderTopAndBottomThickness(
  302. element.getParentElement());
  303. }
  304. return getLayoutManager().getOuterHeightDouble(element);
  305. }
  306. private ComponentConnector getConnector(String id) {
  307. return (ComponentConnector) ConnectorMap.get(getConnection())
  308. .getConnector(id);
  309. }
  310. }
  311. @Override
  312. protected void extend(ServerConnector target) {
  313. getWidget().setDetailsGenerator(new CustomDetailsGenerator());
  314. spacerIndexChangedHandlerRegistration = getWidget()
  315. .addSpacerIndexChangedHandler(new SpacerIndexChangedHandler() {
  316. @Override
  317. public void onSpacerIndexChanged(
  318. SpacerIndexChangedEvent event) {
  319. // Move spacer from old index to new index. Escalator is
  320. // responsible for making sure the new index doesn't
  321. // already contain a spacer.
  322. String connectorId = indexToDetailConnectorId
  323. .remove(event.getOldIndex());
  324. indexToDetailConnectorId.put(event.getNewIndex(),
  325. connectorId);
  326. }
  327. });
  328. dataChangeRegistration = getWidget().getDataSource()
  329. .addDataChangeHandler(new DetailsChangeHandler());
  330. rowVisibilityChangeHandlerRegistration = getWidget()
  331. .addRowVisibilityChangeHandler(event -> {
  332. if (getConnection().getMessageHandler().isUpdatingState()) {
  333. // don't update in the middle of state changes,
  334. // leave to dataAvailable
  335. return;
  336. }
  337. Range newVisibleRowRange = event.getVisibleRowRange();
  338. if (newVisibleRowRange.equals(latestVisibleRowRange)) {
  339. // no need to update
  340. return;
  341. }
  342. Range availableAndVisible = availableRowRange
  343. .partitionWith(newVisibleRowRange)[1];
  344. if (availableAndVisible.isEmpty()) {
  345. // nothing to update yet, leave to dataAvailable
  346. return;
  347. }
  348. if (!availableAndVisible.equals(latestVisibleRowRange)) {
  349. // check whether the visible range has simply got
  350. // shortened
  351. // (e.g. by changing the default row height)
  352. boolean subsectionOfOld = latestVisibleRowRange
  353. .partitionWith(newVisibleRowRange)[1]
  354. .length() == newVisibleRowRange
  355. .length();
  356. // update visible range
  357. latestVisibleRowRange = availableAndVisible;
  358. if (subsectionOfOld) {
  359. // only detach extra rows
  360. detachExcludingRange(latestVisibleRowRange);
  361. } else {
  362. // there are completely new visible rows, full
  363. // refresh
  364. detachOldAndRefreshCurrentDetails();
  365. }
  366. } else {
  367. // refresh only the visible range, nothing to detach
  368. refreshDetailsVisibilityWithRange(availableAndVisible);
  369. // the update may have affected details row contents and
  370. // size, recalculation is needed
  371. triggerDelayedRepositioning(
  372. availableAndVisible.getStart(),
  373. availableAndVisible.length());
  374. }
  375. });
  376. }
  377. /**
  378. * Triggers repositioning of the the contents from the first affected row
  379. * downwards if any of the rows fall within the visual range. If any other
  380. * delayed repositioning has been triggered within this round trip the
  381. * affected range is expanded as needed. The processing is delayed to make
  382. * sure all updates have time to get in, otherwise the repositioning will be
  383. * calculated separately for each details row addition or removal from the
  384. * server side (see
  385. * {@link DataCommunicatorClientRpc#updateData(elemental.json.JsonArray)}
  386. * implementation within {@link DataCommunicatorConnector}).
  387. *
  388. * @param firstRowIndex
  389. * the index of the first changed row
  390. * @param numberOfRows
  391. * the number of changed rows
  392. */
  393. private void triggerDelayedRepositioning(int firstRowIndex,
  394. int numberOfRows) {
  395. if (delayedRepositioningStart == null
  396. || delayedRepositioningStart > firstRowIndex) {
  397. delayedRepositioningStart = firstRowIndex;
  398. }
  399. if (delayedRepositioningEnd == null
  400. || delayedRepositioningEnd < firstRowIndex + numberOfRows) {
  401. delayedRepositioningEnd = firstRowIndex + numberOfRows;
  402. }
  403. if (!delayedRepositioningTriggered) {
  404. delayedRepositioningTriggered = true;
  405. Scheduler.get().scheduleFinally(() -> {
  406. // refresh the positions of all affected rows and those
  407. // below them, unless all affected rows are outside of the
  408. // visual range
  409. if (getWidget().getEscalator().getVisibleRowRange()
  410. .intersects(Range.between(delayedRepositioningStart,
  411. delayedRepositioningEnd))) {
  412. getWidget().getEscalator().getBody().updateRowPositions(
  413. delayedRepositioningStart,
  414. getWidget().getEscalator().getBody().getRowCount()
  415. - delayedRepositioningStart);
  416. }
  417. delayedRepositioningTriggered = false;
  418. delayedRepositioningStart = null;
  419. delayedRepositioningEnd = null;
  420. });
  421. }
  422. }
  423. /**
  424. * Makes sure that after the layout phase has finished Grid will be informed
  425. * whether any details rows were added or updated. This delay is needed to
  426. * allow the details row(s) to get their final size, and it's possible that
  427. * more than one operation that might affect that size or details row
  428. * existence will be performed (and consequently this method called) before
  429. * the check can actually be made.
  430. * <p>
  431. * If this method is called with value {@code true} at least once within the
  432. * delay phase Grid will be told to run any pending position-sensitive
  433. * operations it might have in store.
  434. * <p>
  435. * If this method is only called with value {@code false} within the delay
  436. * period Grid will be told to cancel the pending operations.
  437. * <p>
  438. * If this method isn't called at all, Grid won't be instructed to either
  439. * trigger the pending operations or cancel them and hence they remain in a
  440. * pending state.
  441. *
  442. * @param newOrUpdatedDetails
  443. * {@code true} if the calling operation added or updated
  444. * details, {@code false} otherwise
  445. */
  446. private void markDetailsAddedOrUpdatedForDelayedAlertToGrid(
  447. boolean newOrUpdatedDetails) {
  448. if (newOrUpdatedDetails) {
  449. delayedDetailsAddedOrUpdated = true;
  450. }
  451. if (!delayedDetailsAddedOrUpdatedAlertTriggered) {
  452. delayedDetailsAddedOrUpdatedAlertTriggered = true;
  453. Scheduler.get().scheduleFinally(() -> {
  454. getParent().detailsRefreshed(delayedDetailsAddedOrUpdated);
  455. delayedDetailsAddedOrUpdatedAlertTriggered = false;
  456. delayedDetailsAddedOrUpdated = false;
  457. });
  458. }
  459. }
  460. private void detachIfNeeded(int rowIndex, String id) {
  461. if (indexToDetailConnectorId.containsKey(rowIndex)) {
  462. if (indexToDetailConnectorId.get(rowIndex).equals(id)) {
  463. return;
  464. }
  465. if (id == null) {
  466. // Details have been hidden, listeners attached to the old
  467. // component need to be removed
  468. id = indexToDetailConnectorId.get(rowIndex);
  469. }
  470. // New or removed Details component, hide old one
  471. ComponentConnector connector = (ComponentConnector) ConnectorMap
  472. .get(getConnection()).getConnector(id);
  473. if (connector != null) {
  474. Element element = connector.getWidget().getElement();
  475. elementToResizeCommand.remove(element);
  476. getLayoutManager().removeElementResizeListener(element,
  477. detailsRowResizeListener);
  478. }
  479. getWidget().setDetailsVisible(rowIndex, false);
  480. indexToDetailConnectorId.remove(rowIndex);
  481. }
  482. }
  483. @Override
  484. public void onUnregister() {
  485. super.onUnregister();
  486. dataChangeRegistration.remove();
  487. dataChangeRegistration = null;
  488. spacerIndexChangedHandlerRegistration.removeHandler();
  489. rowVisibilityChangeHandlerRegistration.removeHandler();
  490. indexToDetailConnectorId.clear();
  491. }
  492. @Override
  493. public GridConnector getParent() {
  494. return (GridConnector) super.getParent();
  495. }
  496. @Override
  497. public DetailsManagerState getState() {
  498. return (DetailsManagerState) super.getState();
  499. }
  500. private Grid<JsonObject> getWidget() {
  501. return getParent().getWidget();
  502. }
  503. /**
  504. * Returns the connector id for a details component.
  505. *
  506. * @param rowIndex
  507. * the row index of details component
  508. * @return connector id; {@code null} if row or id is not found
  509. */
  510. private String getDetailsComponentConnectorId(int rowIndex) {
  511. JsonObject row = getWidget().getDataSource().getRow(rowIndex);
  512. if (row == null || !row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
  513. || row.getString(GridState.JSONKEY_DETAILS_VISIBLE).isEmpty()) {
  514. return null;
  515. }
  516. return row.getString(GridState.JSONKEY_DETAILS_VISIBLE);
  517. }
  518. private LayoutManager getLayoutManager() {
  519. return LayoutManager.get(getConnection());
  520. }
  521. /**
  522. * Refreshes the existence of details components within the given range, and
  523. * gives a delayed notice to Grid if any got added or updated.
  524. */
  525. private void refreshDetailsVisibilityWithRange(Range rangeToRefresh) {
  526. if (!getState().hasDetailsGenerator) {
  527. markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
  528. return;
  529. }
  530. boolean newOrUpdatedDetails = false;
  531. // Don't update the latestVisibleRowRange class variable here, the
  532. // calling method should take care of that if relevant.
  533. Range currentVisibleRowRange = getWidget().getEscalator()
  534. .getVisibleRowRange();
  535. Range[] partitions = currentVisibleRowRange
  536. .partitionWith(rangeToRefresh);
  537. // only inspect the range where visible and refreshed rows overlap
  538. Range intersectingRange = partitions[1];
  539. for (int i = intersectingRange.getStart(); i < intersectingRange
  540. .getEnd(); ++i) {
  541. String id = getDetailsComponentConnectorId(i);
  542. detachIfNeeded(i, id);
  543. if (id == null) {
  544. continue;
  545. }
  546. indexToDetailConnectorId.put(i, id);
  547. getWidget().setDetailsVisible(i, true);
  548. newOrUpdatedDetails = true;
  549. }
  550. markDetailsAddedOrUpdatedForDelayedAlertToGrid(newOrUpdatedDetails);
  551. }
  552. private void detachOldAndRefreshCurrentDetails() {
  553. Range[] partitions = availableRowRange
  554. .partitionWith(latestVisibleRowRange);
  555. Range availableAndVisible = partitions[1];
  556. detachExcludingRange(availableAndVisible);
  557. boolean newOrUpdatedDetails = refreshRange(availableAndVisible);
  558. markDetailsAddedOrUpdatedForDelayedAlertToGrid(newOrUpdatedDetails);
  559. }
  560. private void detachExcludingRange(Range keep) {
  561. // remove all spacers that are no longer in range
  562. for (Integer existingIndex : indexToDetailConnectorId.keySet()) {
  563. if (!keep.contains(existingIndex)) {
  564. detachDetails(existingIndex);
  565. }
  566. }
  567. }
  568. private boolean refreshRange(Range rangeToRefresh) {
  569. // make sure all spacers that are currently in range are up to date
  570. boolean newOrUpdatedDetails = false;
  571. for (int i = rangeToRefresh.getStart(); i < rangeToRefresh
  572. .getEnd(); ++i) {
  573. int rowIndex = i;
  574. if (refreshDetails(rowIndex)) {
  575. newOrUpdatedDetails = true;
  576. }
  577. }
  578. return newOrUpdatedDetails;
  579. }
  580. private void detachDetails(int rowIndex) {
  581. String id = indexToDetailConnectorId.remove(rowIndex);
  582. if (id != null) {
  583. ComponentConnector connector = (ComponentConnector) ConnectorMap
  584. .get(getConnection()).getConnector(id);
  585. if (connector != null) {
  586. Element element = connector.getWidget().getElement();
  587. elementToResizeCommand.remove(element);
  588. getLayoutManager().removeElementResizeListener(element,
  589. detailsRowResizeListener);
  590. }
  591. }
  592. getWidget().setDetailsVisible(rowIndex, false);
  593. }
  594. private boolean refreshDetails(int rowIndex) {
  595. String id = getDetailsComponentConnectorId(rowIndex);
  596. String oldId = indexToDetailConnectorId.get(rowIndex);
  597. if ((oldId == null && id == null)
  598. || (oldId != null && oldId.equals(id))) {
  599. // nothing to update, move along
  600. return false;
  601. }
  602. boolean newOrUpdatedDetails = false;
  603. if (oldId != null) {
  604. // Details have been hidden or updated, listeners attached
  605. // to the old component need to be removed
  606. ComponentConnector connector = (ComponentConnector) ConnectorMap
  607. .get(getConnection()).getConnector(oldId);
  608. if (connector != null) {
  609. Element element = connector.getWidget().getElement();
  610. elementToResizeCommand.remove(element);
  611. getLayoutManager().removeElementResizeListener(element,
  612. detailsRowResizeListener);
  613. }
  614. if (id == null) {
  615. // hidden, clear reference
  616. getWidget().setDetailsVisible(rowIndex, false);
  617. indexToDetailConnectorId.remove(rowIndex);
  618. } else {
  619. // updated, replace reference
  620. indexToDetailConnectorId.put(rowIndex, id);
  621. newOrUpdatedDetails = true;
  622. }
  623. } else {
  624. // new Details content, listeners will get attached to the connector
  625. // when Escalator requests for the Details through
  626. // CustomDetailsGenerator#getDetails(int)
  627. indexToDetailConnectorId.put(rowIndex, id);
  628. newOrUpdatedDetails = true;
  629. getWidget().setDetailsVisible(rowIndex, true);
  630. }
  631. return newOrUpdatedDetails;
  632. }
  633. }