/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.v7.client.ui; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import com.google.gwt.animation.client.Animation; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.SpanElement; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.dom.client.TableCellElement; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ComputedStyle; import com.vaadin.client.UIDL; import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.Icon; import com.vaadin.v7.client.ui.VTreeTable.VTreeTableScrollBody.VTreeTableRow; public class VTreeTable extends VScrollTable { /** For internal use only. May be removed or replaced in the future. */ public static class PendingNavigationEvent { public final int keycode; public final boolean ctrl; public final boolean shift; public PendingNavigationEvent(int keycode, boolean ctrl, boolean shift) { this.keycode = keycode; this.ctrl = ctrl; this.shift = shift; } @Override public String toString() { String string = "Keyboard event: " + keycode; if (ctrl) { string += " + ctrl"; } if (shift) { string += " + shift"; } return string; } } /** For internal use only. May be removed or replaced in the future. */ public boolean collapseRequest; private boolean selectionPending; /** For internal use only. May be removed or replaced in the future. */ public int colIndexOfHierarchy; /** For internal use only. May be removed or replaced in the future. */ public String collapsedRowKey; /** For internal use only. May be removed or replaced in the future. */ public VTreeTableScrollBody scrollBody; /** For internal use only. May be removed or replaced in the future. */ public boolean animationsEnabled; /** For internal use only. May be removed or replaced in the future. */ public LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTable.PendingNavigationEvent>(); /** For internal use only. May be removed or replaced in the future. */ public boolean focusParentResponsePending; @Override protected VScrollTableBody createScrollBody() { scrollBody = new VTreeTableScrollBody(); return scrollBody; } /* * Overridden to allow animation of expands and collapses of nodes. */ @Override public void addAndRemoveRows(UIDL partialRowAdditions) { if (partialRowAdditions == null) { return; } if (animationsEnabled) { if (partialRowAdditions.hasAttribute("hide")) { scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished( partialRowAdditions.getIntAttribute("firstprowix"), partialRowAdditions.getIntAttribute("numprows")); } else { scrollBody.insertRowsAnimated(partialRowAdditions, partialRowAdditions.getIntAttribute("firstprowix"), partialRowAdditions.getIntAttribute("numprows")); discardRowsOutsideCacheWindow(); } } else { super.addAndRemoveRows(partialRowAdditions); } } @Override protected int getHierarchyColumnIndex() { return colIndexOfHierarchy + (showRowHeaders ? 1 : 0); } public class VTreeTableScrollBody extends VScrollTable.VScrollTableBody { private int indentWidth = -1; private int maxIndent = 0; protected VTreeTableScrollBody() { super(); } @Override protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) { if (uidl.hasAttribute("gen_html")) { // This is a generated row. return new VTreeTableGeneratedRow(uidl, aligns2); } return new VTreeTableRow(uidl, aligns2); } public class VTreeTableRow extends VScrollTable.VScrollTableBody.VScrollTableRow { private boolean isTreeCellAdded = false; private SpanElement treeSpacer; private boolean open; private int depth; private boolean canHaveChildren; protected Widget widgetInHierarchyColumn; public VTreeTableRow(UIDL uidl, char[] aligns2) { super(uidl, aligns2); // this fix causes #15118 and doesn't work for treetable anyway applyZeroWidthFix = false; } @Override public void addCell(UIDL rowUidl, String text, char align, String style, boolean textIsHTML, boolean isSorted, String description) { super.addCell(rowUidl, text, align, style, textIsHTML, isSorted, description); addTreeSpacer(rowUidl); } protected boolean addTreeSpacer(UIDL rowUidl) { if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) { Element container = (Element) getElement().getLastChild() .getFirstChild(); if (rowUidl.hasAttribute("icon")) { Icon icon = client .getIcon(rowUidl.getStringAttribute("icon")); icon.setAlternateText("icon"); container.insertFirst(icon.getElement()); } String classname = "v-treetable-treespacer"; if (rowUidl.getBooleanAttribute("ca")) { canHaveChildren = true; open = rowUidl.getBooleanAttribute("open"); classname += open ? " v-treetable-node-open" : " v-treetable-node-closed"; } treeSpacer = Document.get().createSpanElement(); treeSpacer.setClassName(classname); container.insertFirst(treeSpacer); depth = rowUidl.hasAttribute("depth") ? rowUidl.getIntAttribute("depth") : 0; setIndent(); isTreeCellAdded = true; return true; } return false; } private boolean cellShowsTreeHierarchy(int curColIndex) { if (isTreeCellAdded) { return false; } return curColIndex == getHierarchyColumnIndex(); } @Override public void onBrowserEvent(Event event) { if (event.getEventTarget().cast() == treeSpacer && treeSpacer.getClassName().contains("node")) { if (event.getTypeInt() == Event.ONMOUSEUP) { sendToggleCollapsedUpdate(getKey()); } return; } super.onBrowserEvent(event); } @Override public void addCell(UIDL rowUidl, Widget w, char align, String style, boolean isSorted, String description) { super.addCell(rowUidl, w, align, style, isSorted, description); if (addTreeSpacer(rowUidl)) { widgetInHierarchyColumn = w; } } private void setIndent() { if (getIndentWidth() > 0) { treeSpacer.getParentElement().getStyle() .setPaddingLeft(getIndent(), Unit.PX); treeSpacer.getStyle().setWidth(getIndent(), Unit.PX); int colWidth = getColWidth(getHierarchyColumnIndex()); if (colWidth > 0 && getIndent() > colWidth) { VTreeTable.this.setColWidth(getHierarchyColumnIndex(), getIndent(), false); } } } @Override protected void onAttach() { super.onAttach(); if (getIndentWidth() < 0) { detectIndent(this); // If we detect indent here then the size of the hierarchy // column is still wrong as it has been set when the indent // was not known. int w = getCellWidthFromDom(getHierarchyColumnIndex()); if (w >= 0) { setColWidth(getHierarchyColumnIndex(), w); } } } private int getCellWidthFromDom(int cellIndex) { final Element cell = DOM.getChild(getElement(), cellIndex); String w = cell.getStyle().getProperty("width"); if (w == null || "".equals(w) || !w.endsWith("px")) { return -1; } else { return Integer.parseInt(w.substring(0, w.length() - 2)); } } private int getHierarchyAndIconWidth() { int consumedSpace = treeSpacer.getOffsetWidth(); if (treeSpacer.getParentElement().getChildCount() > 2) { // icon next to tree spacer consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer .getNextSibling()).getOffsetWidth(); } return consumedSpace; } @Override protected void setCellWidth(int cellIx, int width) { if (cellIx == getHierarchyColumnIndex()) { // take indentation padding into account if this is the // hierarchy column int indent = getIndent(); if (indent != -1) { width = Math.max(width - indent, 0); } } super.setCellWidth(cellIx, width); } private int getIndent() { return (depth + 1) * getIndentWidth(); } } protected class VTreeTableGeneratedRow extends VTreeTableRow { private boolean spanColumns; private boolean htmlContentAllowed; public VTreeTableGeneratedRow(UIDL uidl, char[] aligns) { super(uidl, aligns); addStyleName("v-table-generated-row"); } public boolean isSpanColumns() { return spanColumns; } @Override protected void initCellWidths() { if (spanColumns) { setSpannedColumnWidthAfterDOMFullyInited(); } else { super.initCellWidths(); } } private void setSpannedColumnWidthAfterDOMFullyInited() { // Defer setting width on spanned columns to make sure that // they are added to the DOM before trying to calculate // widths. Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { if (showRowHeaders) { setCellWidth(0, tHead.getHeaderCell(0) .getWidthWithIndent()); calcAndSetSpanWidthOnCell(1); } else { calcAndSetSpanWidthOnCell(0); } } }); } @Override protected boolean isRenderHtmlInCells() { return htmlContentAllowed; } @Override protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col, int visibleColumnIndex) { htmlContentAllowed = uidl.getBooleanAttribute("gen_html"); spanColumns = uidl.getBooleanAttribute("gen_span"); final Iterator<?> cells = uidl.getChildIterator(); if (spanColumns) { int colCount = uidl.getChildCount(); if (cells.hasNext()) { final Object cell = cells.next(); if (cell instanceof String) { addSpannedCell(uidl, cell.toString(), aligns[0], "", htmlContentAllowed, false, null, colCount); } else { addSpannedCell(uidl, (Widget) cell, aligns[0], "", false, colCount); } } } else { super.addCellsFromUIDL(uidl, aligns, col, visibleColumnIndex); } } private void addSpannedCell(UIDL rowUidl, Widget w, char align, String style, boolean sorted, int colCount) { TableCellElement td = DOM.createTD().cast(); td.setColSpan(colCount); initCellWithWidget(w, align, style, sorted, td); td.getStyle().setHeight(getRowHeight(), Unit.PX); if (addTreeSpacer(rowUidl)) { widgetInHierarchyColumn = w; } } private void addSpannedCell(UIDL rowUidl, String text, char align, String style, boolean textIsHTML, boolean sorted, String description, int colCount) { // String only content is optimized by not using Label widget final TableCellElement td = DOM.createTD().cast(); td.setColSpan(colCount); initCellWithText(text, align, style, textIsHTML, sorted, description, td); td.getStyle().setHeight(getRowHeight(), Unit.PX); addTreeSpacer(rowUidl); } @Override protected void setCellWidth(int cellIx, int width) { if (isSpanColumns()) { if (showRowHeaders) { if (cellIx == 0) { super.setCellWidth(0, width); } else { // We need to recalculate the spanning TDs width for // every cellIx in order to support column resizing. calcAndSetSpanWidthOnCell(1); } } else { // Same as above. calcAndSetSpanWidthOnCell(0); } } else { super.setCellWidth(cellIx, width); } } private void calcAndSetSpanWidthOnCell(final int cellIx) { int spanWidth = 0; for (int ix = (showRowHeaders ? 1 : 0); ix < tHead .getVisibleCellCount(); ix++) { spanWidth += tHead.getHeaderCell(ix).getOffsetWidth(); } WidgetUtil.setWidthExcludingPaddingAndBorder( (Element) getElement().getChild(cellIx), spanWidth, 13, false); } } private int getIndentWidth() { return indentWidth; } @Override protected int getMaxIndent() { return maxIndent; } @Override protected void calculateMaxIndent() { int maxIndent = 0; Iterator<Widget> iterator = iterator(); while (iterator.hasNext()) { VTreeTableRow next = (VTreeTableRow) iterator.next(); maxIndent = Math.max(maxIndent, next.getIndent()); } this.maxIndent = maxIndent; } private void detectIndent(VTreeTableRow vTreeTableRow) { indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth(); if (indentWidth == 0) { indentWidth = -1; return; } Iterator<Widget> iterator = iterator(); while (iterator.hasNext()) { VTreeTableRow next = (VTreeTableRow) iterator.next(); next.setIndent(); } calculateMaxIndent(); } protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished( final int firstIndex, final int rows) { List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>(); for (int ix = firstIndex; ix < firstIndex + rows; ix++) { VScrollTableRow row = getRowByRowIndex(ix); if (row != null) { rowsToDelete.add(row); } } if (!rowsToDelete.isEmpty()) { // #8810 Only animate if there's something to animate RowCollapseAnimation anim = new RowCollapseAnimation( rowsToDelete) { @Override protected void onComplete() { super.onComplete(); // Actually unlink the rows and update the cache after // the // animation is done. unlinkAndReindexRows(firstIndex, rows); discardRowsOutsideCacheWindow(); ensureCacheFilled(); } }; anim.run(150); } } protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData, int firstIndex, int rows) { List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData, firstIndex, rows); if (!insertedRows.isEmpty()) { // Only animate if there's something to animate (#8810) RowExpandAnimation anim = new RowExpandAnimation(insertedRows); anim.run(150); } scrollBody.calculateMaxIndent(); return insertedRows; } /** * Prepares the table for animation by copying the background colors of * all TR elements to their respective TD elements if the TD element is * transparent. This is needed, since if TDs have transparent * backgrounds, the rows sliding behind them are visible. */ private class AnimationPreparator { private final int lastItemIx; public AnimationPreparator(int lastItemIx) { this.lastItemIx = lastItemIx; } public void prepareTableForAnimation() { int ix = lastItemIx; VScrollTableRow row = null; while ((row = getRowByRowIndex(ix)) != null) { copyTRBackgroundsToTDs(row); --ix; } } private void copyTRBackgroundsToTDs(VScrollTableRow row) { Element tr = row.getElement(); ComputedStyle cs = new ComputedStyle(tr); String backgroundAttachment = cs .getProperty("backgroundAttachment"); String backgroundClip = cs.getProperty("backgroundClip"); String backgroundColor = cs.getProperty("backgroundColor"); String backgroundImage = cs.getProperty("backgroundImage"); String backgroundOrigin = cs.getProperty("backgroundOrigin"); for (int ix = 0; ix < tr.getChildCount(); ix++) { Element td = tr.getChild(ix).cast(); if (!elementHasBackground(td)) { td.getStyle().setProperty("backgroundAttachment", backgroundAttachment); td.getStyle().setProperty("backgroundClip", backgroundClip); td.getStyle().setProperty("backgroundColor", backgroundColor); td.getStyle().setProperty("backgroundImage", backgroundImage); td.getStyle().setProperty("backgroundOrigin", backgroundOrigin); } } } private boolean elementHasBackground(Element element) { ComputedStyle cs = new ComputedStyle(element); String clr = cs.getProperty("backgroundColor"); String img = cs.getProperty("backgroundImage"); return !("rgba(0, 0, 0, 0)".equals(clr.trim()) || "transparent".equals(clr.trim()) || img == null); } public void restoreTableAfterAnimation() { int ix = lastItemIx; VScrollTableRow row = null; while ((row = getRowByRowIndex(ix)) != null) { restoreStyleForTDsInRow(row); --ix; } } private void restoreStyleForTDsInRow(VScrollTableRow row) { Element tr = row.getElement(); for (int ix = 0; ix < tr.getChildCount(); ix++) { Element td = tr.getChild(ix).cast(); td.getStyle().clearProperty("backgroundAttachment"); td.getStyle().clearProperty("backgroundClip"); td.getStyle().clearProperty("backgroundColor"); td.getStyle().clearProperty("backgroundImage"); td.getStyle().clearProperty("backgroundOrigin"); } } } /** * Animates row expansion using the GWT animation framework. * * The idea is as follows: * * 1. Insert all rows normally * * 2. Insert a newly created DIV containing a new TABLE element below * the DIV containing the actual scroll table body. * * 3. Clone the rows that were inserted in step 1 and attach the clones * to the new TABLE element created in step 2. * * 4. The new DIV from step 2 is absolutely positioned so that the last * inserted row is just behind the row that was expanded. * * 5. Hide the contents of the originally inserted rows by setting the * DIV.v-table-cell-wrapper to display:none;. * * 6. Set the height of the originally inserted rows to 0. * * 7. The animation loop slides the DIV from step 2 downwards, while at * the same pace growing the height of each of the inserted rows from 0 * to full height. The first inserted row grows from 0 to full and after * this the second row grows from 0 to full, etc until all rows are full * height. * * 8. Remove the DIV from step 2 * * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements. * * 10. DONE */ private class RowExpandAnimation extends Animation { private final List<VScrollTableRow> rows; private Element cloneDiv; private Element cloneTable; private AnimationPreparator preparator; /** * @param rows * List of rows to animate. Must not be empty. */ public RowExpandAnimation(List<VScrollTableRow> rows) { this.rows = rows; buildAndInsertAnimatingDiv(); preparator = new AnimationPreparator( rows.get(0).getIndex() - 1); preparator.prepareTableForAnimation(); for (VScrollTableRow row : rows) { cloneAndAppendRow(row); row.addStyleName("v-table-row-animating"); setCellWrapperDivsToDisplayNone(row); row.setHeight(getInitialHeight()); } } protected String getInitialHeight() { return "0px"; } private void cloneAndAppendRow(VScrollTableRow row) { Element clonedTR = null; clonedTR = row.getElement().cloneNode(true).cast(); clonedTR.getStyle().setVisibility(Visibility.VISIBLE); cloneTable.appendChild(clonedTR); } protected double getBaseOffset() { return rows.get(0).getAbsoluteTop() - rows.get(0).getParent().getAbsoluteTop() - rows.size() * getRowHeight(); } private void buildAndInsertAnimatingDiv() { cloneDiv = DOM.createDiv(); cloneDiv.addClassName("v-treetable-animation-clone-wrapper"); cloneTable = DOM.createTable(); cloneTable.addClassName("v-treetable-animation-clone"); cloneDiv.appendChild(cloneTable); insertAnimatingDiv(); } private void insertAnimatingDiv() { Element tableBody = getElement(); Element tableBodyParent = tableBody.getParentElement(); tableBodyParent.insertAfter(cloneDiv, tableBody); } @Override protected void onUpdate(double progress) { animateDiv(progress); animateRowHeights(progress); } private void animateDiv(double progress) { double offset = calculateDivOffset(progress, getRowHeight()); cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX); } private void animateRowHeights(double progress) { double rh = getRowHeight(); double vlh = calculateHeightOfAllVisibleLines(progress, rh); int ix = 0; while (ix < rows.size()) { double height = vlh < rh ? vlh : rh; rows.get(ix).setHeight(height + "px"); vlh -= height; ix++; } } protected double calculateHeightOfAllVisibleLines(double progress, double rh) { return rows.size() * rh * progress; } protected double calculateDivOffset(double progress, double rh) { return progress * rows.size() * rh; } @Override protected void onComplete() { preparator.restoreTableAfterAnimation(); for (VScrollTableRow row : rows) { resetCellWrapperDivsDisplayProperty(row); row.removeStyleName("v-table-row-animating"); } Element tableBodyParent = getElement().getParentElement(); tableBodyParent.removeChild(cloneDiv); } private void setCellWrapperDivsToDisplayNone(VScrollTableRow row) { Element tr = row.getElement(); for (int ix = 0; ix < tr.getChildCount(); ix++) { getWrapperDiv(tr, ix).getStyle().setDisplay(Display.NONE); } } private Element getWrapperDiv(Element tr, int tdIx) { Element td = tr.getChild(tdIx).cast(); return td.getChild(0).cast(); } private void resetCellWrapperDivsDisplayProperty( VScrollTableRow row) { Element tr = row.getElement(); for (int ix = 0; ix < tr.getChildCount(); ix++) { getWrapperDiv(tr, ix).getStyle().clearProperty("display"); } } } /** * This is the inverse of the RowExpandAnimation and is implemented by * extending it and overriding the calculation of offsets and heights. */ private class RowCollapseAnimation extends RowExpandAnimation { private final List<VScrollTableRow> rows; /** * @param rows * List of rows to animate. Must not be empty. */ public RowCollapseAnimation(List<VScrollTableRow> rows) { super(rows); this.rows = rows; } @Override protected String getInitialHeight() { return getRowHeight() + "px"; } @Override protected double getBaseOffset() { return getRowHeight(); } @Override protected double calculateHeightOfAllVisibleLines(double progress, double rh) { return rows.size() * rh * (1 - progress); } @Override protected double calculateDivOffset(double progress, double rh) { return -super.calculateDivOffset(progress, rh); } } } /** * Icons rendered into first actual column in TreeTable, not to row header * cell */ @Override protected String buildCaptionHtmlSnippet(UIDL uidl) { if (uidl.getTag().equals("column")) { return super.buildCaptionHtmlSnippet(uidl); } else { String s = uidl.getStringAttribute("caption"); return s; } } /** For internal use only. May be removed or replaced in the future. */ @Override public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { if (collapseRequest || focusParentResponsePending) { // Enqueue the event if there might be pending content changes from // the server if (pendingNavigationEvents.size() < 10) { // Only keep 10 keyboard events in the queue PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent( keycode, ctrl, shift); pendingNavigationEvents.add(pendingNavigationEvent); } return true; } VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow(); if (focusedRow != null) { if (focusedRow.canHaveChildren && ((keycode == KeyCodes.KEY_RIGHT && !focusedRow.open) || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) { if (!ctrl) { client.updateVariable(paintableId, "selectCollapsed", true, false); } sendSelectedRows(false); sendToggleCollapsedUpdate(focusedRow.getKey()); return true; } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) { // already expanded, move selection down if next is on a deeper // level (is-a-child) VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow .getParent(); Iterator<Widget> iterator = body.iterator(); VTreeTableRow next = null; while (iterator.hasNext()) { next = (VTreeTableRow) iterator.next(); if (next == focusedRow) { next = (VTreeTableRow) iterator.next(); break; } } if (next != null) { if (next.depth > focusedRow.depth) { selectionPending = true; return super.handleNavigation(getNavigationDownKey(), ctrl, shift); } } else { // Note, a minor change here for a bit false behavior if // cache rows is disabled + last visible row + no childs for // the node selectionPending = true; return super.handleNavigation(getNavigationDownKey(), ctrl, shift); } } else if (keycode == KeyCodes.KEY_LEFT) { // already collapsed move selection up to parent node // do on the server side as the parent is not necessary // rendered on the client, could check if parent is visible if // a performance issue arises client.updateVariable(paintableId, "focusParent", focusedRow.getKey(), true); // Set flag that we should enqueue navigation events until we // get a response to this request focusParentResponsePending = true; return true; } } return super.handleNavigation(keycode, ctrl, shift); } private void sendToggleCollapsedUpdate(String rowKey) { collapsedRowKey = rowKey; collapseRequest = true; client.updateVariable(paintableId, "toggleCollapsed", rowKey, true); } @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); if (event.getTypeInt() == Event.ONKEYUP && selectionPending) { sendSelectedRows(); } } @Override protected void sendSelectedRows(boolean immediately) { super.sendSelectedRows(immediately); selectionPending = false; } @Override protected void reOrderColumn(String columnKey, int newIndex) { super.reOrderColumn(columnKey, newIndex); // current impl not intelligent enough to survive without visiting the // server to redraw content client.sendPendingVariableChanges(); } @Override public void setStyleName(String style) { super.setStyleName(style + " v-treetable"); } @Override public void updateTotalRows(UIDL uidl) { // Make sure that initializedAndAttached & al are not reset when the // totalrows are updated on expand/collapse requests. int newTotalRows = uidl.getIntAttribute("totalrows"); setTotalRows(newTotalRows); } }