PageRenderTime 64ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 2ms

/src/com/vaadin/client/ui/VCustomScrollTable.java

https://gitlab.com/jforge/FilteringTable
Java | 8224 lines | 5411 code | 933 blank | 1880 comment | 1400 complexity | f807ea2292c5164ecd39a40005efbcbf MD5 | raw file

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

  1. /*
  2. * Copyright 2000-2014 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.ui;
  17. import java.util.ArrayList;
  18. import java.util.Collection;
  19. import java.util.HashMap;
  20. import java.util.HashSet;
  21. import java.util.Iterator;
  22. import java.util.LinkedList;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Set;
  26. import java.util.logging.Level;
  27. import java.util.logging.Logger;
  28. import com.google.gwt.core.client.JavaScriptObject;
  29. import com.google.gwt.core.client.Scheduler;
  30. import com.google.gwt.core.client.Scheduler.ScheduledCommand;
  31. import com.google.gwt.dom.client.Document;
  32. import com.google.gwt.dom.client.Element;
  33. import com.google.gwt.dom.client.NativeEvent;
  34. import com.google.gwt.dom.client.Node;
  35. import com.google.gwt.dom.client.NodeList;
  36. import com.google.gwt.dom.client.Style;
  37. import com.google.gwt.dom.client.Style.Display;
  38. import com.google.gwt.dom.client.Style.Overflow;
  39. import com.google.gwt.dom.client.Style.Position;
  40. import com.google.gwt.dom.client.Style.TextAlign;
  41. import com.google.gwt.dom.client.Style.Unit;
  42. import com.google.gwt.dom.client.Style.Visibility;
  43. import com.google.gwt.dom.client.TableCellElement;
  44. import com.google.gwt.dom.client.TableRowElement;
  45. import com.google.gwt.dom.client.TableSectionElement;
  46. import com.google.gwt.dom.client.Touch;
  47. import com.google.gwt.event.dom.client.BlurEvent;
  48. import com.google.gwt.event.dom.client.BlurHandler;
  49. import com.google.gwt.event.dom.client.FocusEvent;
  50. import com.google.gwt.event.dom.client.FocusHandler;
  51. import com.google.gwt.event.dom.client.KeyCodes;
  52. import com.google.gwt.event.dom.client.KeyDownEvent;
  53. import com.google.gwt.event.dom.client.KeyDownHandler;
  54. import com.google.gwt.event.dom.client.KeyPressEvent;
  55. import com.google.gwt.event.dom.client.KeyPressHandler;
  56. import com.google.gwt.event.dom.client.KeyUpEvent;
  57. import com.google.gwt.event.dom.client.KeyUpHandler;
  58. import com.google.gwt.event.dom.client.ScrollEvent;
  59. import com.google.gwt.event.dom.client.ScrollHandler;
  60. import com.google.gwt.event.logical.shared.CloseEvent;
  61. import com.google.gwt.event.logical.shared.CloseHandler;
  62. import com.google.gwt.event.shared.HandlerRegistration;
  63. import com.google.gwt.regexp.shared.MatchResult;
  64. import com.google.gwt.regexp.shared.RegExp;
  65. import com.google.gwt.user.client.Command;
  66. import com.google.gwt.user.client.DOM;
  67. import com.google.gwt.user.client.Event;
  68. import com.google.gwt.user.client.Event.NativePreviewEvent;
  69. import com.google.gwt.user.client.Event.NativePreviewHandler;
  70. import com.google.gwt.user.client.Timer;
  71. import com.google.gwt.user.client.Window;
  72. import com.google.gwt.user.client.ui.FlowPanel;
  73. import com.google.gwt.user.client.ui.HasWidgets;
  74. import com.google.gwt.user.client.ui.Panel;
  75. import com.google.gwt.user.client.ui.PopupPanel;
  76. import com.google.gwt.user.client.ui.UIObject;
  77. import com.google.gwt.user.client.ui.Widget;
  78. import com.vaadin.client.ApplicationConnection;
  79. import com.vaadin.client.BrowserInfo;
  80. import com.vaadin.client.ComponentConnector;
  81. import com.vaadin.client.ConnectorMap;
  82. import com.vaadin.client.DeferredWorker;
  83. import com.vaadin.client.Focusable;
  84. import com.vaadin.client.MouseEventDetailsBuilder;
  85. import com.vaadin.client.StyleConstants;
  86. import com.vaadin.client.TooltipInfo;
  87. import com.vaadin.client.UIDL;
  88. import com.vaadin.client.Util;
  89. import com.vaadin.client.VConsole;
  90. import com.vaadin.client.VTooltip;
  91. import com.vaadin.client.ui.VCustomScrollTable.VScrollTableBody.VScrollTableRow;
  92. import com.vaadin.client.ui.dd.DDUtil;
  93. import com.vaadin.client.ui.dd.VAbstractDropHandler;
  94. import com.vaadin.client.ui.dd.VAcceptCallback;
  95. import com.vaadin.client.ui.dd.VDragAndDropManager;
  96. import com.vaadin.client.ui.dd.VDragEvent;
  97. import com.vaadin.client.ui.dd.VHasDropHandler;
  98. import com.vaadin.client.ui.dd.VTransferable;
  99. import com.vaadin.shared.AbstractComponentState;
  100. import com.vaadin.shared.MouseEventDetails;
  101. import com.vaadin.shared.ui.dd.VerticalDropLocation;
  102. import com.vaadin.shared.ui.table.TableConstants;
  103. /**
  104. * VCustomScrollTable
  105. *
  106. * VCustomScrollTable is a FlowPanel having two widgets in it: * TableHead
  107. * component * ScrollPanel
  108. *
  109. * TableHead contains table's header and widgets + logic for resizing,
  110. * reordering and hiding columns.
  111. *
  112. * ScrollPanel contains VScrollTableBody object which handles content. To save
  113. * some bandwidth and to improve clients responsiveness with loads of data, in
  114. * VScrollTableBody all rows are not necessary rendered. There are "spacers" in
  115. * VScrollTableBody to use the exact same space as non-rendered rows would use.
  116. * This way we can use seamlessly traditional scrollbars and scrolling to fetch
  117. * more rows instead of "paging".
  118. *
  119. * In VCustomScrollTable we listen to scroll events. On horizontal scrolling we
  120. * also update TableHeads scroll position which has its scrollbars hidden. On
  121. * vertical scroll events we will check if we are reaching the end of area where
  122. * we have rows rendered and
  123. *
  124. * TODO implement unregistering for child components in Cells
  125. */
  126. @SuppressWarnings("deprecation")
  127. public class VCustomScrollTable extends FlowPanel implements HasWidgets,
  128. ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable,
  129. ActionOwner, SubPartAware, DeferredWorker {
  130. /**
  131. * Simple interface for parts of the table capable of owning a context menu.
  132. *
  133. * @since 7.2
  134. * @author Vaadin Ltd
  135. */
  136. private interface ContextMenuOwner {
  137. public void showContextMenu(Event event);
  138. }
  139. /**
  140. * Handles showing context menu on "long press" from a touch screen.
  141. *
  142. * @since 7.2
  143. * @author Vaadin Ltd
  144. */
  145. private class TouchContextProvider {
  146. private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500;
  147. private Timer contextTouchTimeout;
  148. private Event touchStart;
  149. private int touchStartY;
  150. private int touchStartX;
  151. private ContextMenuOwner target;
  152. /**
  153. * Initializes a handler for a certain context menu owner.
  154. *
  155. * @param target
  156. * the owner of the context menu
  157. */
  158. public TouchContextProvider(ContextMenuOwner target) {
  159. this.target = target;
  160. }
  161. /**
  162. * Cancels the current context touch timeout.
  163. */
  164. public void cancel() {
  165. if (contextTouchTimeout != null) {
  166. contextTouchTimeout.cancel();
  167. contextTouchTimeout = null;
  168. }
  169. touchStart = null;
  170. }
  171. /**
  172. * A function to handle touch context events in a table.
  173. *
  174. * @param event
  175. * browser event to handle
  176. */
  177. public void handleTouchEvent(final Event event) {
  178. int type = event.getTypeInt();
  179. switch (type) {
  180. case Event.ONCONTEXTMENU:
  181. target.showContextMenu(event);
  182. break;
  183. case Event.ONTOUCHSTART:
  184. // save position to fields, touches in events are same
  185. // instance during the operation.
  186. touchStart = event;
  187. Touch touch = event.getChangedTouches().get(0);
  188. touchStartX = touch.getClientX();
  189. touchStartY = touch.getClientY();
  190. if (contextTouchTimeout == null) {
  191. contextTouchTimeout = new Timer() {
  192. @Override
  193. public void run() {
  194. if (touchStart != null) {
  195. // Open the context menu if finger
  196. // is held in place long enough.
  197. target.showContextMenu(touchStart);
  198. event.preventDefault();
  199. touchStart = null;
  200. }
  201. }
  202. };
  203. }
  204. contextTouchTimeout.schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
  205. break;
  206. case Event.ONTOUCHCANCEL:
  207. case Event.ONTOUCHEND:
  208. cancel();
  209. break;
  210. case Event.ONTOUCHMOVE:
  211. if (isSignificantMove(event)) {
  212. // Moved finger before the context menu timer
  213. // expired, so let the browser handle the event.
  214. cancel();
  215. }
  216. }
  217. }
  218. /**
  219. * Calculates how many pixels away the user's finger has traveled. This
  220. * reduces the chance of small non-intentional movements from canceling
  221. * the long press detection.
  222. *
  223. * @param event
  224. * the Event for which to check the move distance
  225. * @return true if this is considered an intentional move by the user
  226. */
  227. protected boolean isSignificantMove(Event event) {
  228. if (touchStart == null) {
  229. // no touch start
  230. return false;
  231. }
  232. // Calculate the distance between touch start and the current touch
  233. // position
  234. Touch touch = event.getChangedTouches().get(0);
  235. int deltaX = touch.getClientX() - touchStartX;
  236. int deltaY = touch.getClientY() - touchStartY;
  237. int delta = deltaX * deltaX + deltaY * deltaY;
  238. // Compare to the square of the significant move threshold to remove
  239. // the need for a square root
  240. if (delta > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD
  241. * TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
  242. return true;
  243. }
  244. return false;
  245. }
  246. }
  247. public static final String STYLENAME = "v-table";
  248. public enum SelectMode {
  249. NONE(0), SINGLE(1), MULTI(2);
  250. private int id;
  251. private SelectMode(int id) {
  252. this.id = id;
  253. }
  254. public int getId() {
  255. return id;
  256. }
  257. }
  258. private static final String ROW_HEADER_COLUMN_KEY = "0";
  259. private static final double CACHE_RATE_DEFAULT = 2;
  260. /**
  261. * The default multi select mode where simple left clicks only selects one
  262. * item, CTRL+left click selects multiple items and SHIFT-left click selects
  263. * a range of items.
  264. */
  265. private static final int MULTISELECT_MODE_DEFAULT = 0;
  266. /**
  267. * The simple multiselect mode is what the table used to have before
  268. * ctrl/shift selections were added. That is that when this is set clicking
  269. * on an item selects/deselects the item and no ctrl/shift selections are
  270. * available.
  271. */
  272. private static final int MULTISELECT_MODE_SIMPLE = 1;
  273. /**
  274. * multiple of pagelength which component will cache when requesting more
  275. * rows
  276. */
  277. private double cache_rate = CACHE_RATE_DEFAULT;
  278. /**
  279. * fraction of pageLenght which can be scrolled without making new request
  280. */
  281. private double cache_react_rate = 0.75 * cache_rate;
  282. public static final char ALIGN_CENTER = 'c';
  283. public static final char ALIGN_LEFT = 'b';
  284. public static final char ALIGN_RIGHT = 'e';
  285. private static final int CHARCODE_SPACE = 32;
  286. private int firstRowInViewPort = 0;
  287. private int pageLength = 15;
  288. private int lastRequestedFirstvisible = 0; // to detect "serverside scroll"
  289. private int firstvisibleOnLastPage = -1; // To detect if the first visible
  290. // is on the last page
  291. /** For internal use only. May be removed or replaced in the future. */
  292. public boolean showRowHeaders = false;
  293. private String[] columnOrder;
  294. protected ApplicationConnection client;
  295. /** For internal use only. May be removed or replaced in the future. */
  296. public String paintableId;
  297. /** For internal use only. May be removed or replaced in the future. */
  298. public boolean immediate;
  299. private boolean updatedReqRows = true;
  300. private boolean nullSelectionAllowed = true;
  301. private SelectMode selectMode = SelectMode.NONE;
  302. public final HashSet<String> selectedRowKeys = new HashSet<String>();
  303. /*
  304. * When scrolling and selecting at the same time, the selections are not in
  305. * sync with the server while retrieving new rows (until key is released).
  306. */
  307. private HashSet<Object> unSyncedselectionsBeforeRowFetch;
  308. /*
  309. * These are used when jumping between pages when pressing Home and End
  310. */
  311. /** For internal use only. May be removed or replaced in the future. */
  312. public boolean selectLastItemInNextRender = false;
  313. /** For internal use only. May be removed or replaced in the future. */
  314. public boolean selectFirstItemInNextRender = false;
  315. /** For internal use only. May be removed or replaced in the future. */
  316. public boolean focusFirstItemInNextRender = false;
  317. /** For internal use only. May be removed or replaced in the future. */
  318. public boolean focusLastItemInNextRender = false;
  319. /**
  320. * The currently focused row.
  321. * <p>
  322. * For internal use only. May be removed or replaced in the future.
  323. */
  324. public VScrollTableRow focusedRow;
  325. /**
  326. * Helper to store selection range start in when using the keyboard
  327. * <p>
  328. * For internal use only. May be removed or replaced in the future.
  329. */
  330. public VScrollTableRow selectionRangeStart;
  331. /**
  332. * Flag for notifying when the selection has changed and should be sent to
  333. * the server
  334. * <p>
  335. * For internal use only. May be removed or replaced in the future.
  336. */
  337. public boolean selectionChanged = false;
  338. /*
  339. * The speed (in pixels) which the scrolling scrolls vertically/horizontally
  340. */
  341. private int scrollingVelocity = 10;
  342. private Timer scrollingVelocityTimer = null;
  343. /** For internal use only. May be removed or replaced in the future. */
  344. public String[] bodyActionKeys;
  345. private boolean enableDebug = false;
  346. private static final boolean hasNativeTouchScrolling = BrowserInfo.get()
  347. .isTouchDevice()
  348. && !BrowserInfo.get().requiresTouchScrollDelegate();
  349. private Set<String> noncollapsibleColumns;
  350. /**
  351. * The last known row height used to preserve the height of a table with
  352. * custom row heights and a fixed page length after removing the last row
  353. * from the table.
  354. *
  355. * A new VScrollTableBody instance is created every time the number of rows
  356. * changes causing {@link VScrollTableBody#rowHeight} to be discarded and
  357. * the height recalculated by {@link VScrollTableBody#getRowHeight(boolean)}
  358. * to avoid some rounding problems, e.g. round(2 * 19.8) / 2 = 20 but
  359. * round(3 * 19.8) / 3 = 19.66.
  360. */
  361. private double lastKnownRowHeight = Double.NaN;
  362. /**
  363. * Remember scroll position when getting detached to properly scroll back to
  364. * the location that there is data for if getting attached again.
  365. */
  366. private int detachedScrollPosition = 0;
  367. /**
  368. * Represents a select range of rows
  369. */
  370. private class SelectionRange {
  371. private VScrollTableRow startRow;
  372. private final int length;
  373. /**
  374. * Constuctor.
  375. */
  376. public SelectionRange(VScrollTableRow row1, VScrollTableRow row2) {
  377. VScrollTableRow endRow;
  378. if (row2.isBefore(row1)) {
  379. startRow = row2;
  380. endRow = row1;
  381. } else {
  382. startRow = row1;
  383. endRow = row2;
  384. }
  385. length = endRow.getIndex() - startRow.getIndex() + 1;
  386. }
  387. public SelectionRange(VScrollTableRow row, int length) {
  388. startRow = row;
  389. this.length = length;
  390. }
  391. /*
  392. * (non-Javadoc)
  393. *
  394. * @see java.lang.Object#toString()
  395. */
  396. @Override
  397. public String toString() {
  398. return startRow.getKey() + "-" + length;
  399. }
  400. private boolean inRange(VScrollTableRow row) {
  401. return row.getIndex() >= startRow.getIndex()
  402. && row.getIndex() < startRow.getIndex() + length;
  403. }
  404. public Collection<SelectionRange> split(VScrollTableRow row) {
  405. assert row.isAttached();
  406. ArrayList<SelectionRange> ranges = new ArrayList<SelectionRange>(2);
  407. int endOfFirstRange = row.getIndex() - 1;
  408. if (endOfFirstRange >= startRow.getIndex()) {
  409. // create range of first part unless its length is < 1
  410. ranges.add(new SelectionRange(startRow, endOfFirstRange
  411. - startRow.getIndex() + 1));
  412. }
  413. int startOfSecondRange = row.getIndex() + 1;
  414. if (getEndIndex() >= startOfSecondRange) {
  415. // create range of second part unless its length is < 1
  416. VScrollTableRow startOfRange = scrollBody
  417. .getRowByRowIndex(startOfSecondRange);
  418. if (startOfRange != null) {
  419. ranges.add(new SelectionRange(startOfRange, getEndIndex()
  420. - startOfSecondRange + 1));
  421. }
  422. }
  423. return ranges;
  424. }
  425. private int getEndIndex() {
  426. return startRow.getIndex() + length - 1;
  427. }
  428. }
  429. private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>();
  430. /** For internal use only. May be removed or replaced in the future. */
  431. public boolean initializedAndAttached = false;
  432. /**
  433. * Flag to indicate if a column width recalculation is needed due update.
  434. * <p>
  435. * For internal use only. May be removed or replaced in the future.
  436. */
  437. public boolean headerChangedDuringUpdate = false;
  438. /** For internal use only. May be removed or replaced in the future. */
  439. public final TableHead tHead = new TableHead();
  440. /** For internal use only. May be removed or replaced in the future. */
  441. public final TableFooter tFoot = new TableFooter();
  442. /** Handles context menu for table body */
  443. private ContextMenuOwner contextMenuOwner = new ContextMenuOwner() {
  444. @Override
  445. public void showContextMenu(Event event) {
  446. int left = Util.getTouchOrMouseClientX(event);
  447. int top = Util.getTouchOrMouseClientY(event);
  448. boolean menuShown = handleBodyContextMenu(left, top);
  449. if (menuShown) {
  450. event.stopPropagation();
  451. event.preventDefault();
  452. }
  453. }
  454. };
  455. /** Handles touch events to display a context menu for table body */
  456. private TouchContextProvider touchContextProvider = new TouchContextProvider(
  457. contextMenuOwner);
  458. /**
  459. * For internal use only. May be removed or replaced in the future.
  460. *
  461. * Overwrites onBrowserEvent function on FocusableScrollPanel to give event
  462. * access to touchContextProvider. Has to be public to give TableConnector
  463. * access to the scrollBodyPanel field.
  464. *
  465. * @since 7.2
  466. * @author Vaadin Ltd
  467. */
  468. public class FocusableScrollContextPanel extends FocusableScrollPanel {
  469. @Override
  470. public void onBrowserEvent(Event event) {
  471. super.onBrowserEvent(event);
  472. touchContextProvider.handleTouchEvent(event);
  473. };
  474. public FocusableScrollContextPanel(boolean useFakeFocusElement) {
  475. super(useFakeFocusElement);
  476. }
  477. }
  478. /** For internal use only. May be removed or replaced in the future. */
  479. public final FocusableScrollContextPanel scrollBodyPanel = new FocusableScrollContextPanel(
  480. true);
  481. private KeyPressHandler navKeyPressHandler = new KeyPressHandler() {
  482. @Override
  483. public void onKeyPress(KeyPressEvent keyPressEvent) {
  484. // This is used for Firefox only, since Firefox auto-repeat
  485. // works correctly only if we use a key press handler, other
  486. // browsers handle it correctly when using a key down handler
  487. if (!BrowserInfo.get().isGecko()) {
  488. return;
  489. }
  490. NativeEvent event = keyPressEvent.getNativeEvent();
  491. if (!enabled) {
  492. // Cancel default keyboard events on a disabled Table
  493. // (prevents scrolling)
  494. event.preventDefault();
  495. } else if (hasFocus) {
  496. // Key code in Firefox/onKeyPress is present only for
  497. // special keys, otherwise 0 is returned
  498. int keyCode = event.getKeyCode();
  499. if (keyCode == 0 && event.getCharCode() == ' ') {
  500. // Provide a keyCode for space to be compatible with
  501. // FireFox keypress event
  502. keyCode = CHARCODE_SPACE;
  503. }
  504. if (handleNavigation(keyCode,
  505. event.getCtrlKey() || event.getMetaKey(),
  506. event.getShiftKey())) {
  507. event.preventDefault();
  508. }
  509. startScrollingVelocityTimer();
  510. }
  511. }
  512. };
  513. private KeyUpHandler navKeyUpHandler = new KeyUpHandler() {
  514. @Override
  515. public void onKeyUp(KeyUpEvent keyUpEvent) {
  516. NativeEvent event = keyUpEvent.getNativeEvent();
  517. int keyCode = event.getKeyCode();
  518. if (!isFocusable()) {
  519. cancelScrollingVelocityTimer();
  520. } else if (isNavigationKey(keyCode)) {
  521. if (keyCode == getNavigationDownKey()
  522. || keyCode == getNavigationUpKey()) {
  523. /*
  524. * in multiselect mode the server may still have value from
  525. * previous page. Clear it unless doing multiselection or
  526. * just moving focus.
  527. */
  528. if (!event.getShiftKey() && !event.getCtrlKey()) {
  529. instructServerToForgetPreviousSelections();
  530. }
  531. sendSelectedRows();
  532. }
  533. cancelScrollingVelocityTimer();
  534. navKeyDown = false;
  535. }
  536. }
  537. };
  538. private KeyDownHandler navKeyDownHandler = new KeyDownHandler() {
  539. @Override
  540. public void onKeyDown(KeyDownEvent keyDownEvent) {
  541. NativeEvent event = keyDownEvent.getNativeEvent();
  542. // This is not used for Firefox
  543. if (BrowserInfo.get().isGecko()) {
  544. return;
  545. }
  546. if (!enabled) {
  547. // Cancel default keyboard events on a disabled Table
  548. // (prevents scrolling)
  549. event.preventDefault();
  550. } else if (hasFocus) {
  551. if (handleNavigation(event.getKeyCode(), event.getCtrlKey()
  552. || event.getMetaKey(), event.getShiftKey())) {
  553. navKeyDown = true;
  554. event.preventDefault();
  555. }
  556. startScrollingVelocityTimer();
  557. }
  558. }
  559. };
  560. /** For internal use only. May be removed or replaced in the future. */
  561. public int totalRows;
  562. private Set<String> collapsedColumns;
  563. /** For internal use only. May be removed or replaced in the future. */
  564. public final RowRequestHandler rowRequestHandler;
  565. /** For internal use only. May be removed or replaced in the future. */
  566. public VScrollTableBody scrollBody;
  567. private int firstvisible = 0;
  568. private boolean sortAscending;
  569. private String sortColumn;
  570. private String oldSortColumn;
  571. private boolean columnReordering;
  572. /**
  573. * This map contains captions and icon urls for actions like: * "33_c" ->
  574. * "Edit" * "33_i" -> "http://dom.com/edit.png"
  575. */
  576. private final HashMap<Object, String> actionMap = new HashMap<Object, String>();
  577. private String[] visibleColOrder;
  578. private boolean initialContentReceived = false;
  579. private Element scrollPositionElement;
  580. /** For internal use only. May be removed or replaced in the future. */
  581. public boolean enabled;
  582. /** For internal use only. May be removed or replaced in the future. */
  583. public boolean showColHeaders;
  584. /** For internal use only. May be removed or replaced in the future. */
  585. public boolean showColFooters;
  586. /** flag to indicate that table body has changed */
  587. private boolean isNewBody = true;
  588. /**
  589. * Read from the "recalcWidths" -attribute. When it is true, the table will
  590. * recalculate the widths for columns - desirable in some cases. For #1983,
  591. * marked experimental. See also variable <code>refreshContentWidths</code>
  592. * in method {@link TableHead#updateCellsFromUIDL(UIDL)}.
  593. * <p>
  594. * For internal use only. May be removed or replaced in the future.
  595. */
  596. public boolean recalcWidths = false;
  597. /** For internal use only. May be removed or replaced in the future. */
  598. public boolean rendering = false;
  599. private boolean hasFocus = false;
  600. private int dragmode;
  601. private int multiselectmode;
  602. /** For internal use only. May be removed or replaced in the future. */
  603. public int tabIndex;
  604. private TouchScrollDelegate touchScrollDelegate;
  605. /** For internal use only. May be removed or replaced in the future. */
  606. public int lastRenderedHeight;
  607. /**
  608. * Values (serverCacheFirst+serverCacheLast) sent by server that tells which
  609. * rows (indexes) are in the server side cache (page buffer). -1 means
  610. * unknown. The server side cache row MUST MATCH the client side cache rows.
  611. *
  612. * If the client side cache contains additional rows with e.g. buttons, it
  613. * will cause out of sync when such a button is pressed.
  614. *
  615. * If the server side cache contains additional rows with e.g. buttons,
  616. * scrolling in the client will cause empty buttons to be rendered
  617. * (cached=true request for non-existing components)
  618. *
  619. * For internal use only. May be removed or replaced in the future.
  620. */
  621. public int serverCacheFirst = -1;
  622. public int serverCacheLast = -1;
  623. /**
  624. * In several cases TreeTable depends on the scrollBody.lastRendered being
  625. * 'out of sync' while the update is being done. In those cases the sanity
  626. * check must be performed afterwards.
  627. */
  628. public boolean postponeSanityCheckForLastRendered;
  629. /** For internal use only. May be removed or replaced in the future. */
  630. public boolean sizeNeedsInit = true;
  631. /**
  632. * Used to recall the position of an open context menu if we need to close
  633. * and reopen it during a row update.
  634. * <p>
  635. * For internal use only. May be removed or replaced in the future.
  636. */
  637. public class ContextMenuDetails implements CloseHandler<PopupPanel> {
  638. public String rowKey;
  639. public int left;
  640. public int top;
  641. HandlerRegistration closeRegistration;
  642. public ContextMenuDetails(VContextMenu menu, String rowKey, int left,
  643. int top) {
  644. this.rowKey = rowKey;
  645. this.left = left;
  646. this.top = top;
  647. closeRegistration = menu.addCloseHandler(this);
  648. }
  649. @Override
  650. public void onClose(CloseEvent<PopupPanel> event) {
  651. contextMenu = null;
  652. closeRegistration.removeHandler();
  653. }
  654. }
  655. /** For internal use only. May be removed or replaced in the future. */
  656. public ContextMenuDetails contextMenu = null;
  657. private boolean hadScrollBars = false;
  658. private HandlerRegistration addCloseHandler;
  659. /**
  660. * Changes to manage mouseDown and mouseUp
  661. */
  662. /**
  663. * The element where the last mouse down event was registered.
  664. */
  665. private Element lastMouseDownTarget;
  666. /**
  667. * Set to true by {@link #mouseUpPreviewHandler} if it gets a mouseup at the
  668. * same element as {@link #lastMouseDownTarget}.
  669. */
  670. private boolean mouseUpPreviewMatched = false;
  671. private HandlerRegistration mouseUpEventPreviewRegistration;
  672. /**
  673. * Previews events after a mousedown to detect where the following mouseup
  674. * hits.
  675. */
  676. private final NativePreviewHandler mouseUpPreviewHandler = new NativePreviewHandler() {
  677. @Override
  678. public void onPreviewNativeEvent(NativePreviewEvent event) {
  679. if (event.getTypeInt() == Event.ONMOUSEUP) {
  680. mouseUpEventPreviewRegistration.removeHandler();
  681. // Event's reported target not always correct if event
  682. // capture is in use
  683. Element elementUnderMouse = Util.getElementUnderMouse(event
  684. .getNativeEvent());
  685. if (lastMouseDownTarget != null
  686. && lastMouseDownTarget.isOrHasChild(elementUnderMouse)) {
  687. mouseUpPreviewMatched = true;
  688. } else {
  689. getLogger().log(
  690. Level.FINEST,
  691. "Ignoring mouseup from " + elementUnderMouse
  692. + " when mousedown was on "
  693. + lastMouseDownTarget);
  694. }
  695. }
  696. }
  697. };
  698. public VCustomScrollTable() {
  699. setMultiSelectMode(MULTISELECT_MODE_DEFAULT);
  700. scrollBodyPanel.addFocusHandler(this);
  701. scrollBodyPanel.addBlurHandler(this);
  702. scrollBodyPanel.addScrollHandler(this);
  703. /*
  704. * Firefox auto-repeat works correctly only if we use a key press
  705. * handler, other browsers handle it correctly when using a key down
  706. * handler
  707. */
  708. if (BrowserInfo.get().isGecko()) {
  709. scrollBodyPanel.addKeyPressHandler(navKeyPressHandler);
  710. } else {
  711. scrollBodyPanel.addKeyDownHandler(navKeyDownHandler);
  712. }
  713. scrollBodyPanel.addKeyUpHandler(navKeyUpHandler);
  714. scrollBodyPanel.sinkEvents(Event.TOUCHEVENTS | Event.ONCONTEXTMENU);
  715. setStyleName(STYLENAME);
  716. add(tHead);
  717. add(scrollBodyPanel);
  718. add(tFoot);
  719. rowRequestHandler = new RowRequestHandler();
  720. }
  721. @Override
  722. public void setStyleName(String style) {
  723. updateStyleNames(style, false);
  724. }
  725. @Override
  726. public void setStylePrimaryName(String style) {
  727. updateStyleNames(style, true);
  728. }
  729. private void updateStyleNames(String newStyle, boolean isPrimary) {
  730. scrollBodyPanel
  731. .removeStyleName(getStylePrimaryName() + "-body-wrapper");
  732. scrollBodyPanel.removeStyleName(getStylePrimaryName() + "-body");
  733. if (scrollBody != null) {
  734. scrollBody.removeStyleName(getStylePrimaryName()
  735. + "-body-noselection");
  736. }
  737. if (isPrimary) {
  738. super.setStylePrimaryName(newStyle);
  739. } else {
  740. super.setStyleName(newStyle);
  741. }
  742. scrollBodyPanel.addStyleName(getStylePrimaryName() + "-body-wrapper");
  743. scrollBodyPanel.addStyleName(getStylePrimaryName() + "-body");
  744. tHead.updateStyleNames(getStylePrimaryName());
  745. tFoot.updateStyleNames(getStylePrimaryName());
  746. if (scrollBody != null) {
  747. scrollBody.updateStyleNames(getStylePrimaryName());
  748. }
  749. }
  750. public void init(ApplicationConnection client) {
  751. this.client = client;
  752. // Add a handler to clear saved context menu details when the menu
  753. // closes. See #8526.
  754. addCloseHandler = client.getContextMenu().addCloseHandler(
  755. new CloseHandler<PopupPanel>() {
  756. @Override
  757. public void onClose(CloseEvent<PopupPanel> event) {
  758. contextMenu = null;
  759. }
  760. });
  761. }
  762. /**
  763. * Handles a context menu event on table body.
  764. *
  765. * @param left
  766. * left position of the context menu
  767. * @param top
  768. * top position of the context menu
  769. * @return true if a context menu was shown, otherwise false
  770. */
  771. private boolean handleBodyContextMenu(int left, int top) {
  772. if (enabled && bodyActionKeys != null) {
  773. top += Window.getScrollTop();
  774. left += Window.getScrollLeft();
  775. client.getContextMenu().showAt(this, left, top);
  776. return true;
  777. }
  778. return false;
  779. }
  780. /**
  781. * Fires a column resize event which sends the resize information to the
  782. * server.
  783. *
  784. * @param columnId
  785. * The columnId of the column which was resized
  786. * @param originalWidth
  787. * The width in pixels of the column before the resize event
  788. * @param newWidth
  789. * The width in pixels of the column after the resize event
  790. */
  791. private void fireColumnResizeEvent(String columnId, int originalWidth,
  792. int newWidth) {
  793. client.updateVariable(paintableId, "columnResizeEventColumn", columnId,
  794. false);
  795. client.updateVariable(paintableId, "columnResizeEventPrev",
  796. originalWidth, false);
  797. client.updateVariable(paintableId, "columnResizeEventCurr", newWidth,
  798. immediate);
  799. }
  800. /**
  801. * Non-immediate variable update of column widths for a collection of
  802. * columns.
  803. *
  804. * @param columns
  805. * the columns to trigger the events for.
  806. */
  807. private void sendColumnWidthUpdates(Collection<HeaderCell> columns) {
  808. String[] newSizes = new String[columns.size()];
  809. int ix = 0;
  810. for (HeaderCell cell : columns) {
  811. newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth();
  812. }
  813. client.updateVariable(paintableId, "columnWidthUpdates", newSizes,
  814. false);
  815. }
  816. /**
  817. * Moves the focus one step down
  818. *
  819. * @return Returns true if succeeded
  820. */
  821. private boolean moveFocusDown() {
  822. return moveFocusDown(0);
  823. }
  824. /**
  825. * Moves the focus down by 1+offset rows
  826. *
  827. * @return Returns true if succeeded, else false if the selection could not
  828. * be move downwards
  829. */
  830. private boolean moveFocusDown(int offset) {
  831. if (isSelectable()) {
  832. if (focusedRow == null && scrollBody.iterator().hasNext()) {
  833. // FIXME should focus first visible from top, not first rendered
  834. // ??
  835. return setRowFocus((VScrollTableRow) scrollBody.iterator()
  836. .next());
  837. } else {
  838. VScrollTableRow next = getNextRow(focusedRow, offset);
  839. if (next != null) {
  840. return setRowFocus(next);
  841. }
  842. }
  843. }
  844. return false;
  845. }
  846. /**
  847. * Moves the selection one step up
  848. *
  849. * @return Returns true if succeeded
  850. */
  851. private boolean moveFocusUp() {
  852. return moveFocusUp(0);
  853. }
  854. /**
  855. * Moves the focus row upwards
  856. *
  857. * @return Returns true if succeeded, else false if the selection could not
  858. * be move upwards
  859. *
  860. */
  861. private boolean moveFocusUp(int offset) {
  862. if (isSelectable()) {
  863. if (focusedRow == null && scrollBody.iterator().hasNext()) {
  864. // FIXME logic is exactly the same as in moveFocusDown, should
  865. // be the opposite??
  866. return setRowFocus((VScrollTableRow) scrollBody.iterator()
  867. .next());
  868. } else {
  869. VScrollTableRow prev = getPreviousRow(focusedRow, offset);
  870. if (prev != null) {
  871. return setRowFocus(prev);
  872. } else {
  873. VConsole.log("no previous available");
  874. }
  875. }
  876. }
  877. return false;
  878. }
  879. /**
  880. * Selects a row where the current selection head is
  881. *
  882. * @param ctrlSelect
  883. * Is the selection a ctrl+selection
  884. * @param shiftSelect
  885. * Is the selection a shift+selection
  886. * @return Returns truw
  887. */
  888. private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) {
  889. if (focusedRow != null) {
  890. // Arrows moves the selection and clears previous selections
  891. if (isSelectable() && !ctrlSelect && !shiftSelect) {
  892. deselectAll();
  893. focusedRow.toggleSelection();
  894. selectionRangeStart = focusedRow;
  895. } else if (isSelectable() && ctrlSelect && !shiftSelect) {
  896. // Ctrl+arrows moves selection head
  897. selectionRangeStart = focusedRow;
  898. // No selection, only selection head is moved
  899. } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) {
  900. // Shift+arrows selection selects a range
  901. focusedRow.toggleShiftSelection(shiftSelect);
  902. }
  903. }
  904. }
  905. /**
  906. * Sends the selection to the server if changed since the last update/visit.
  907. */
  908. protected void sendSelectedRows() {
  909. sendSelectedRows(immediate);
  910. }
  911. /**
  912. * Sends the selection to the server if it has been changed since the last
  913. * update/visit.
  914. *
  915. * @param immediately
  916. * set to true to immediately send the rows
  917. */
  918. protected void sendSelectedRows(boolean immediately) {
  919. // Don't send anything if selection has not changed
  920. if (!selectionChanged) {
  921. return;
  922. }
  923. // Reset selection changed flag
  924. selectionChanged = false;
  925. // Note: changing the immediateness of this might require changes to
  926. // "clickEvent" immediateness also.
  927. if (isMultiSelectModeDefault()) {
  928. // Convert ranges to a set of strings
  929. Set<String> ranges = new HashSet<String>();
  930. for (SelectionRange range : selectedRowRanges) {
  931. ranges.add(range.toString());
  932. }
  933. // Send the selected row ranges
  934. client.updateVariable(paintableId, "selectedRanges",
  935. ranges.toArray(new String[selectedRowRanges.size()]), false);
  936. selectedRowRanges.clear();
  937. // clean selectedRowKeys so that they don't contain excess values
  938. for (Iterator<String> iterator = selectedRowKeys.iterator(); iterator
  939. .hasNext();) {
  940. String key = iterator.next();
  941. VScrollTableRow renderedRowByKey = getRenderedRowByKey(key);
  942. if (renderedRowByKey != null) {
  943. for (SelectionRange range : selectedRowRanges) {
  944. if (range.inRange(renderedRowByKey)) {
  945. iterator.remove();
  946. }
  947. }
  948. } else {
  949. // orphaned selected key, must be in a range, ignore
  950. iterator.remove();
  951. }
  952. }
  953. }
  954. // Send the selected rows
  955. client.updateVariable(paintableId, "selected",
  956. selectedRowKeys.toArray(new String[selectedRowKeys.size()]),
  957. immediately);
  958. }
  959. /**
  960. * Get the key that moves the selection head upwards. By default it is the
  961. * up arrow key but by overriding this you can change the key to whatever
  962. * you want.
  963. *
  964. * @return The keycode of the key
  965. */
  966. protected int getNavigationUpKey() {
  967. return KeyCodes.KEY_UP;
  968. }
  969. /**
  970. * Get the key that moves the selection head downwards. By default it is the
  971. * down arrow key but by overriding this you can change the key to whatever
  972. * you want.
  973. *
  974. * @return The keycode of the key
  975. */
  976. protected int getNavigationDownKey() {
  977. return KeyCodes.KEY_DOWN;
  978. }
  979. /**
  980. * Get the key that scrolls to the left in the table. By default it is the
  981. * left arrow key but by overriding this you can change the key to whatever
  982. * you want.
  983. *
  984. * @return The keycode of the key
  985. */
  986. protected int getNavigationLeftKey() {
  987. return KeyCodes.KEY_LEFT;
  988. }
  989. /**
  990. * Get the key that scroll to the right on the table. By default it is the
  991. * right arrow key but by overriding this you can change the key to whatever
  992. * you want.
  993. *
  994. * @return The keycode of the key
  995. */
  996. protected int getNavigationRightKey() {
  997. return KeyCodes.KEY_RIGHT;
  998. }
  999. /**
  1000. * Get the key that selects an item in the table. By default it is the space
  1001. * bar key but by overriding this you can change the key to whatever you
  1002. * want.
  1003. *
  1004. * @return
  1005. */
  1006. protected int getNavigationSelectKey() {
  1007. return CHARCODE_SPACE;
  1008. }
  1009. /**
  1010. * Get the key the moves the selection one page up in the table. By default
  1011. * this is the Page Up key but by overriding this you can change the key to
  1012. * whatever you want.
  1013. *
  1014. * @return
  1015. */
  1016. protected int getNavigationPageUpKey() {
  1017. return KeyCodes.KEY_PAGEUP;
  1018. }
  1019. /**
  1020. * Get the key the moves the selection one page down in the table. By
  1021. * default this is the Page Down key but by overriding this you can change
  1022. * the key to whatever you want.
  1023. *
  1024. * @return
  1025. */
  1026. protected int getNavigationPageDownKey() {
  1027. return KeyCodes.KEY_PAGEDOWN;
  1028. }
  1029. /**
  1030. * Get the key the moves the selection to the beginning of the table. By
  1031. * default this is the Home key but by overriding this you can change the
  1032. * key to whatever you want.
  1033. *
  1034. * @return
  1035. */
  1036. protected int getNavigationStartKey() {
  1037. return KeyCodes.KEY_HOME;
  1038. }
  1039. /**
  1040. * Get the key the moves the selection to the end of the table. By default
  1041. * this is the End key but by overriding this you can change the key to
  1042. * whatever you want.
  1043. *
  1044. * @return
  1045. */
  1046. protected int getNavigationEndKey() {
  1047. return KeyCodes.KEY_END;
  1048. }
  1049. /** For internal use only. May be removed or replaced in the future. */
  1050. public void initializeRows(UIDL uidl, UIDL rowData) {
  1051. if (scrollBody != null) {
  1052. scrollBody.removeFromParent();
  1053. }
  1054. // Without this call the scroll position is messed up in IE even after
  1055. // the lazy scroller has set the scroll position to the first visible
  1056. // item
  1057. scrollBodyPanel.getScrollPosition();
  1058. scrollBody = createScrollBody();
  1059. scrollBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"),
  1060. uidl.getIntAttribute("rows"));
  1061. scrollBodyPanel.add(scrollBody);
  1062. // New body starts scrolled to the left, make sure the header and footer
  1063. // are also scrolled to the left
  1064. tHead.setHorizontalScrollPosition(0);
  1065. tFoot.setHorizontalScrollPosition(0);
  1066. initialContentReceived = true;
  1067. sizeNeedsInit = true;
  1068. scrollBody.restoreRowVisibility();
  1069. }
  1070. /** For internal use only. May be removed or replaced in the future. */
  1071. public void updateColumnProperties(UIDL uidl) {
  1072. updateColumnOrder(uidl);
  1073. updateCollapsedColumns(uidl);
  1074. UIDL vc = uidl.getChildByTagName("visiblecolumns");
  1075. if (vc != null) {
  1076. tHead.updateCellsFromUIDL(vc);
  1077. tFoot.updateCellsFromUIDL(vc);
  1078. }
  1079. updateHeader(uidl.getStringArrayAttribute("vcolorder"));
  1080. updateFooter(uidl.getStringArrayAttribute("vcolorder"));
  1081. if (uidl.hasVariable("noncollapsiblecolumns")) {
  1082. noncollapsibleColumns = uidl
  1083. .getStringArrayVariableAsSet("noncollapsiblecolumns");
  1084. }
  1085. }
  1086. private void updateCollapsedColumns(UIDL uidl) {
  1087. if (uidl.hasVariable("collapsedcolumns")) {
  1088. tHead.setColumnCollapsingAllowed(true);
  1089. collapsedColumns = uidl
  1090. .getStringArrayVariableAsSet("collapsedcolumns");
  1091. } else {
  1092. tHead.setColumnCollapsingAllowed(false);
  1093. }
  1094. }
  1095. private void updateColumnOrder(UIDL uidl) {
  1096. if (uidl.hasVariable("columnorder")) {
  1097. columnReordering = true;
  1098. columnOrder = uidl.getStringArrayVariable("columnorder");
  1099. } else {
  1100. columnReordering = false;
  1101. columnOrder = null;
  1102. }
  1103. }
  1104. /** For internal use only. May be removed or replaced in the future. */
  1105. public boolean selectSelectedRows(UIDL uidl) {
  1106. boolean keyboardSelectionOverRowFetchInProgress = false;
  1107. if (uidl.hasVariable("selected")) {
  1108. final Set<String> selectedKeys = uidl
  1109. .getStringArrayVariableAsSet("selected");
  1110. removeUnselectedRowKeys(selectedKeys);
  1111. if (scrollBody != null) {
  1112. Iterator<Widget> iterator = scrollBody.iterator();
  1113. while (iterator.hasNext()) {
  1114. /*
  1115. * Make the focus reflect to the server side state unless we
  1116. * are currently selecting multiple rows with keyboard.
  1117. */
  1118. VScrollTableRow row = (VScrollTableRow) iterator.next();
  1119. boolean selected = selectedKeys.contains(row.getKey());
  1120. if (!selected
  1121. && unSyncedselectionsBeforeRowFetch != null
  1122. && unSyncedselectionsBeforeRowFetch.contains(row
  1123. .getKey())) {
  1124. selected = true;
  1125. keyboardSelectionOverRowFetchInProgress = true;
  1126. }
  1127. if (selected && selectedKeys.size() == 1) {
  1128. /*
  1129. * If a single item is selected, move focus to the
  1130. * selected row. (#10522)
  1131. */
  1132. setRowFocus(row);
  1133. }
  1134. if (selected != row.isSelected()) {
  1135. row.toggleSelection();
  1136. if (!isSingleSelectMode() && !selected) {
  1137. // Update selection range in case a row is
  1138. // unselected from the middle of a range - #8076
  1139. removeRowFromUnsentSelectionRanges(row);
  1140. }
  1141. }
  1142. }
  1143. }
  1144. }
  1145. unSyncedselectionsBeforeRowFetch = null;
  1146. return keyboardSelectionOverRowFetchInProgress;
  1147. }
  1148. private void removeUnselectedRowKeys(final Set<String> selectedKeys) {
  1149. List<String> unselectedKeys = new ArrayList<String>(0);
  1150. for (String key : selectedRowKeys) {
  1151. if (!selectedKeys.contains(key)) {
  1152. unselectedKeys.add(key);
  1153. }
  1154. }
  1155. selectedRowKeys.removeAll(unselectedKeys);
  1156. }
  1157. /** For internal use only. May be removed or replaced in the future. */
  1158. public void updateSortingProperties(UIDL uidl) {
  1159. oldSortColumn = sortColumn;
  1160. if (uidl.hasVariable("sortascending")) {
  1161. sortAscending = uidl.getBooleanVariable("sortascending");
  1162. sortColumn = uidl.getStringVariable("sortcolumn");
  1163. }
  1164. }
  1165. /** For internal use only. May be removed or replaced in the future. */
  1166. public void resizeSortedColumnForSortIndicator() {
  1167. // Force recalculation of the captionContainer element inside the header
  1168. // cell to accomodate for the size of the sort arrow.
  1169. HeaderCell sortedHeader = tHead.getHeaderCell(sortColumn);
  1170. if (sortedHeader != null) {
  1171. // Mark header as sorted now. Any earlier marking would lead to
  1172. // columns with wrong sizes
  1173. sortedHeader.setSorted(true);
  1174. tHead.resizeCaptionContainer(sortedHeader);
  1175. }
  1176. // Also recalculate the width of the captionContainer element in the
  1177. // previously sorted header, since this now has more room.
  1178. HeaderCell oldSortedHeader = tHead.getHeaderCell(oldSortColumn);
  1179. if (oldSortedHeader != null) {
  1180. tHead.resizeCaptionContainer(oldSortedHeader);
  1181. }
  1182. }
  1183. private boolean lazyScrollerIsActive;
  1184. private void disableLazyScroller() {
  1185. lazyScrollerIsActive = false;
  1186. scrollBodyPanel.getElement().getStyle().clearOverflowX();
  1187. scrollBodyPanel.getElement().getStyle().clearOverflowY();
  1188. }
  1189. private void enableLazyScroller() {
  1190. Scheduler.get().scheduleDeferred(lazyScroller);
  1191. lazyScrollerIsActive = true;
  1192. // prevent scrolling to jump in IE11
  1193. scrollBodyPanel.getElement().getStyle().setOverflowX(Overflow.HIDDEN);
  1194. scrollBodyPanel.getElement().getStyle().setOverflowY(Overflow.HIDDEN);
  1195. }
  1196. private boolean isLazyScrollerActive() {
  1197. return lazyScrollerIsActive;
  1198. }
  1199. private ScheduledCommand lazyScroller = new ScheduledCommand() {
  1200. @Override
  1201. public void execute() {
  1202. if (firstvisible >= 0) {
  1203. firstRowInViewPort = firstvisible;
  1204. if (firstvisibleOnLastPage > -1) {
  1205. scrollBodyPanel
  1206. .setScrollPosition(measureRowHeightOffset(firstvisibleOnLastPage));
  1207. } else {
  1208. scrollBodyPanel
  1209. .setScrollPosition(measureRowHeightOffset(firstvisible));
  1210. }

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