Java tutorial
/* * #%L * GwtMaterial * %% * Copyright (C) 2015 - 2016 GwtMaterialDesign * %% * 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. * #L% */ package gwt.material.design.client.data; import com.google.gwt.cell.client.Cell.Context; import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.event.logical.shared.AttachEvent; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.Widget; import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.Range; import gwt.material.design.client.base.MaterialWidget; import gwt.material.design.client.base.constants.TableCssName; import gwt.material.design.client.data.component.CategoryComponent; import gwt.material.design.client.data.component.CategoryComponent.OrphanCategoryComponent; import gwt.material.design.client.data.component.Component; import gwt.material.design.client.data.component.ComponentFactory; import gwt.material.design.client.data.component.Components; import gwt.material.design.client.data.component.RowComponent; import gwt.material.design.client.data.events.CategoryClosedEvent; import gwt.material.design.client.data.events.CategoryOpenedEvent; import gwt.material.design.client.data.events.ColumnSortEvent; import gwt.material.design.client.data.events.ComponentsRenderedEvent; import gwt.material.design.client.data.events.DestroyEvent; import gwt.material.design.client.data.events.InsertColumnEvent; import gwt.material.design.client.data.events.RangeChangeEvent; import gwt.material.design.client.data.events.RemoveColumnEvent; import gwt.material.design.client.data.events.RenderedEvent; import gwt.material.design.client.data.events.RowCollapsedEvent; import gwt.material.design.client.data.events.RowCollapsingEvent; import gwt.material.design.client.data.events.RowContextMenuEvent; import gwt.material.design.client.data.events.RowDoubleClickEvent; import gwt.material.design.client.data.events.RowExpandingEvent; import gwt.material.design.client.data.events.RowExpandedEvent; import gwt.material.design.client.data.events.RowLongPressEvent; import gwt.material.design.client.data.events.RowSelectEvent; import gwt.material.design.client.data.events.RowShortPressEvent; import gwt.material.design.client.data.events.SelectAllEvent; import gwt.material.design.client.data.events.SetupEvent; import gwt.material.design.client.data.factory.CategoryComponentFactory; import gwt.material.design.client.data.factory.RowComponentFactory; import gwt.material.design.client.jquery.JQueryExtension; import gwt.material.design.client.js.Js; import gwt.material.design.client.js.JsTableElement; import gwt.material.design.client.js.JsTableSubHeaders; import gwt.material.design.client.js.StickyTableOptions; import gwt.material.design.client.ui.MaterialCheckBox; import gwt.material.design.client.ui.MaterialProgress; import gwt.material.design.client.ui.Selectors; import gwt.material.design.client.ui.table.*; import gwt.material.design.client.ui.table.cell.Column; import gwt.material.design.jquery.client.api.Event; import gwt.material.design.jquery.client.api.JQueryElement; import gwt.material.design.jquery.client.api.MouseEvent; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import static gwt.material.design.jquery.client.api.JQuery.$; import static gwt.material.design.jquery.client.api.JQuery.window; /** * Abstract DataView handles the creation, preparation and UI logic for * the table rows and subheaders (if enabled). All of the basic table * rendering is handled. * * @param <T> * @author Ben Dol */ public abstract class AbstractDataView<T> implements DataView<T> { private static final Logger logger = Logger.getLogger(AbstractDataView.class.getName()); // Main protected final String id; protected DataDisplay<T> display; protected DataSource<T> dataSource; protected Renderer<T> renderer; protected SortContext<T> sortContext; protected Column<T, ?> autoSortColumn; protected RowComponentFactory<T> rowFactory; protected ComponentFactory<? extends CategoryComponent, String> categoryFactory; protected ProvidesKey<T> keyProvider; //protected List<ComponentFactory<?, T>> componentFactories; protected JsTableSubHeaders subheaderLib; protected int categoryHeight = 0; protected String height; protected boolean rendering; protected boolean redraw; protected boolean redrawCategories; private boolean pendingRenderEvent; // DOM protected Table table; protected MaterialWidget thead; protected MaterialWidget tbody; protected MaterialProgress progressWidget; protected TableRow headerRow; protected JQueryElement container; protected JsTableElement $table; protected JQueryElement maskElement; protected JQueryElement tableBody; protected JQueryElement topPanel; // Configurations protected Range range = new Range(0, 0); protected int totalRows = 20; protected int longPressDuration = 500; private int lastSelected; private boolean setup; private boolean loadMask; private boolean shiftDown; private boolean useRowExpansion; private boolean useStickyHeader; private boolean useLoadOverlay; private boolean useCategories; private SelectionType selectionType = SelectionType.NONE; // Components protected final Components<RowComponent<T>> rows = new Components<>(); protected final Components<RowComponent<T>> pendingRows = new Components<>(); protected final Components<CategoryComponent> categories = new Components<>(); // Rendering protected final List<Column<T, ?>> columns = new ArrayList<>(); protected final List<TableHeader> headers = new ArrayList<>(); protected HandlerRegistration attachHandler; public static final String ORPHAN_PATTERN = "<@orphans@>"; private static final String expansionHtml = "<tr class='expansion'>" + "<td class='expansion' colspan='100%'>" + "<div>" + "<section class='overlay'>" + "<div class='progress' style='height:4px;top:-1px;'>" + "<div class='indeterminate'></div>" + "</div>" + "</section>" + "<div class='content'><br/><br/><br/></div>" + "</div>" + "</td></tr>"; public static final String maskHtml = "<div class='mask'>" + //"<!--i style='left:50%;top:20%;z-index:9999;position:absolute;color:white' class='fa fa-3x fa-spinner fa-spin'></i-->" + "</div>"; public static final String transitionEvents = "transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd"; public AbstractDataView() { this("DataView"); } public AbstractDataView(String id) { this(id, null); } public AbstractDataView(ProvidesKey<T> keyProvider) { this("DataView", keyProvider); } public AbstractDataView(String id, ProvidesKey<T> keyProvider) { this.id = id; this.keyProvider = keyProvider; this.categoryFactory = new CategoryComponentFactory(); this.rowFactory = new RowComponentFactory<>(); //this.componentFactories = new ArrayList<>(); setRenderer(new BaseRenderer<>()); onConstructed(); } /** * Called after the data view is constructed. * Note that this is not when the data view is attached, * see {@link #setup(TableScaffolding)}. */ protected void onConstructed() { // Do nothing by default } @Override public void render(Components<Component<?>> components) { // Clear the current row components // This does not clear the rows DOM elements this.rows.clearComponents(); // Render the new components for (Component<?> component : components) { renderComponent(component); } redraw = false; prepareRows(); // Reset category indexes and row counts if (isUseCategories()) { for (CategoryComponent category : categories) { category.setCurrentIndex(-1); category.setRowCount(0); } } if (!components.isEmpty()) { // Remove the last attach handler if (attachHandler != null) { attachHandler.removeHandler(); } // When the last component has been rendered we // will set the rendering flag to false. // This can be improved later. Component<?> component = components.get(components.size() - 1); Widget componentWidget = component.getWidget(); AttachEvent.Handler handler = event -> { if (attachHandler != null) { attachHandler.removeHandler(); } // Recheck the row height to ensure // the calculated row height is accurate. getCalculatedRowHeight(); // Fixes an issue with heights updating too early. // Also ensure the cell widths are updated. subheaderLib.recalculate(true); // Fixes an issue with heights updating too early. subheaderLib.updateHeights(); rendering = false; if (attachHandler != null) { attachHandler.removeHandler(); } ComponentsRenderedEvent.fire(this); if (pendingRenderEvent) { RenderedEvent.fire(this); pendingRenderEvent = false; } }; if (componentWidget == null || componentWidget.isAttached()) { handler.onAttachOrDetach(null); } else { attachHandler = componentWidget.addAttachHandler(handler); } } else { rendering = false; } } /** * Compile list of {@link RowComponent}'s and invoke a render. * Controls the building of category components and custom components. * Which then {@link #render(Components)} is invoked to perform DOM render. * * Rows which are already rendered are reprocessed based on their equals method. * If the data is equal to the row in the same index it use the existing row. * * @param rows list of rows to be rendered against the existing rows. */ protected boolean renderRows(Components<RowComponent<T>> rows) { // Make sure we are setup, if we aren't then store the rows // the rows will be attached upon setup. if (!setup) { pendingRows.clear(); pendingRows.addAll(rows); return false; // early exit, not setup yet. } rendering = true; Range visibleRange = getVisibleRange(); // Check if we need to redraw categories if (redrawCategories) { redrawCategories = false; // When we perform a category redraw we have // to clear the row elements also. this.rows.clearWidgets(); if (isUseCategories()) { List<CategoryComponent> openCategories = getOpenCategories(); categories.clearWidgets(); for (CategoryComponent category : categories) { // Re-render the category component renderComponent(category); if (openCategories.contains(category) && category.isRendered()) { subheaderLib.open(category.getWidget().$this()); } } } else { categories.clearWidgets(); } } // The linear component list. // This component list will be the rendering // blueprint for the current view, so the sequence // is compiled according to the component generation. Components<Component<?>> components = new Components<>(visibleRange.getLength()); int index = 0; for (RowComponent<T> row : rows) { if (components.isFull()) { break; // Component stack is full, break } if (isUseCategories()) { CategoryComponent category = row.getCategory(); if (category == null) { category = buildCategoryComponent(row); categories.add(category); } if (category.isRendered()) { category.getWidget().setVisible(true); } } // Do we have an existing row to use if (index < this.rows.size()) { RowComponent<T> existingRow = this.rows.get(index); if (existingRow != null) { // Replace the rows element with the // existing indexes element. row.setWidget(existingRow.getWidget()); row.setRedraw(true); // Rebuild the rows custom components //existingRow.destroyChildren(); //buildCustomComponents(existingRow); } } row.setIndex(index++); components.add(row); } // Render the component stack render(components); return true; } @SuppressWarnings("unchecked") protected void renderComponent(Component<?> component) { if (component != null) { TableRow row; int index = -1; if (component instanceof RowComponent) { RowComponent<T> rowComponent = (RowComponent<T>) component; // Check if the row has a category // Categories have been rendered before the rows CategoryComponent category = null; if (isUseCategories()) { category = rowComponent.getCategory(); // Ensure the category exists and is rendered if (category != null && !category.isRendered()) { renderComponent(category); } } T data = rowComponent.getData(); // Draw the table row row = renderer.drawRow(this, rowComponent, getValueKey(data), columns, redraw); if (row != null) { if (category != null) { if (categories.size() > 1) { int categoryIndex = 0/*category.getCurrentIndex()*/; //if(categoryIndex == -1) { categoryIndex = tbody.getWidgetIndex(category.getWidget()); //category.setCurrentIndex(categoryIndex); //} int categoryCount = category.getRowCount() + 1; category.setRowCount(categoryCount); // Calculate the rows index index = (categoryIndex + categoryCount) - 1; } // Check the display of the row TableSubHeader subHeader = category.getWidget(); if (subHeader != null && subHeader.isOpen()) { row.getElement().getStyle().clearDisplay(); } } else { // Not using categories row.getElement().getStyle().clearDisplay(); } rows.add(rowComponent); } } else if (component instanceof CategoryComponent) { CategoryComponent categoryComponent = (CategoryComponent) component; row = bindCategoryEvents(renderer.drawCategory(categoryComponent)); if (categoryComponent.isOpenByDefault()) { row.addAttachHandler(event -> openCategory(categoryComponent), true); } } else { row = renderer.drawCustom(component); } if (row != null) { if (row.getParent() == null) { if (index < 0) { tbody.add(row); } else { tbody.insert(row, index + 1); } } else { // TODO: calculate row element repositioning. // This will only apply if its index is different // to the new index generated based on the category. } } else { logger.warning("Attempted to add a null TableRow to tbody, the row was ignored."); } // Render the components children /*for(Component<?> child : component.getChildren()) { renderComponent(child); }*/ } } protected void renderColumns() { for (Column<T, ?> column : columns) { renderColumn(column); } } public void renderColumn(Column<T, ?> column) { int index = columns.indexOf(column) + getColumnOffset(); TableHeader th = renderer.drawColumnHeader(column, column.getName(), index); if (th != null) { if (column.isSortable()) { th.$this().on("click", e -> { sort(rows, th, column, index); return true; }); th.addStyleName(TableCssName.SORTABLE); } addHeader(index, th); } for (RowComponent<T> row : rows) { Context context = new Context(row.getIndex(), index, getValueKey(row.getData())); renderer.drawColumn(row.getWidget(), context, row.getData(), column, index, true); } refreshStickyHeaders(); } @Override public void refresh() { // Recheck the row height to ensure // the calculated row height is accurate. getCalculatedRowHeight(); if (redraw && setup) { // Render the rows renderRows(rows); } } @Override public void setRenderer(Renderer<T> renderer) { if (this.renderer != null) { // Copy existing render properties. renderer.copy(this.renderer); } this.renderer = renderer; } public Renderer<T> getRenderer() { return renderer; } @Override public void setDataSource(DataSource<T> dataSource) { if (dataSource instanceof HasDataView) { ((HasDataView<T>) dataSource).setDataView(this); } this.dataSource = dataSource; } @Override public DataSource<T> getDataSource() { return dataSource; } @Override public JsTableSubHeaders getSubheaderLib() { return subheaderLib; } @Override public void setup(TableScaffolding scaffolding) throws Exception { try { container = $(getContainer()); table = scaffolding.getTable(); tableBody = $(scaffolding.getTableBody()); topPanel = $(scaffolding.getTopPanel()); tbody = table.getBody(); thead = table.getHead(); $table = table.getJsElement(); headerRow = new TableRow(); thead.add(headerRow); // Create progress widget progressWidget = new MaterialProgress(); progressWidget.setTop(0); progressWidget.setGwtDisplay(Display.NONE); TableRow progressRow = new TableRow(); progressRow.addStyleName(TableCssName.STICKYEXCLUDE); progressRow.setHeight("3px"); TableData progressTd = new TableData(); progressTd.getElement().setAttribute("colspan", "999"); progressTd.setPadding(0); progressTd.setHeight("0px"); progressTd.add(progressWidget); progressRow.add(progressTd); thead.add(progressRow); if (useRowExpansion) { // Add the expand header TableHeader expandHeader = new TableHeader(); expandHeader.setStyleName(TableCssName.COLEX); addHeader(0, expandHeader); } if (!selectionType.equals(SelectionType.NONE)) { setupHeaderSelectionBox(); if (selectionType.equals(SelectionType.MULTIPLE)) { setupShiftDetection(); } } // Setup the sticky header bar if (useStickyHeader) { setupStickyHeader(); } // Setup the subheaders for categories setupSubHeaders(); // Setup the resize event handlers tableBody.on("resize." + id, e -> { refresh(); return true; }); // We will check the window resize just in case // it has updated the view size of the data view. $(window()).on("resize." + id, e -> { // In the cases where the table is not currently attached. if (getContainer().isAttached()) { refresh(); } return true; }); setup = true; onSetup(scaffolding); SetupEvent.fire(this, scaffolding); } catch (Exception ex) { logger.log(Level.SEVERE, "Problem setting up the DataView.", ex); throw ex; } } protected void onSetup(TableScaffolding scaffolding) { // We are setup, lets check the render tasks if (height != null) { setHeight(height); } setSelectionType(selectionType); renderColumns(); for (CategoryComponent category : categories) { if (!category.isRendered()) { renderCategory(category); } } if (!pendingRows.isEmpty()) { Components<RowComponent<T>> sortedRows = null; if (maybeApplyAutoSortColumn()) { // We have an auto sort column, sort the pending rows. Column<T, ?> column = sortContext.getSortColumn(); sortedRows = sort(pendingRows, sortContext.getTableHeader(), column, columns.indexOf(column) + getColumnOffset(), false); } if (sortedRows == null) { renderRows(pendingRows); pendingRows.clearComponents(); } else { renderRows(sortedRows); } } } @Override public void destroy() { rows.clear(); categories.clear(); columns.clear(); headers.clear(); headerRow.clear(); container.off("." + id); tableBody.off("." + id); $(window()).off("." + id); $table.stickyTableHeaders("destroy"); subheaderLib.unload(); setRedraw(true); rendering = false; setup = false; DestroyEvent.fire(this); } /** * Prepare all row specific functionality. */ protected void prepareRows() { JQueryElement rows = $table.find("tr.data-row"); rows.off("." + id); // Select row click bind // This will also update the check status of check all input. rows.on("tap." + id + " click." + id, (e, o) -> { Element row = $(e.getCurrentTarget()).asElement(); int rowIndex = getRowIndexByElement(row); if (selectionType.equals(SelectionType.MULTIPLE) && shiftDown) { if (lastSelected < rowIndex) { // Increment for (int i = lastSelected; i <= rowIndex; i++) { if (i < getVisibleItemCount()) { RowComponent<T> rowComponent = this.rows.get(i); if (rowComponent != null && rowComponent.isRendered()) { selectRow(rowComponent.getWidget().getElement(), true); } } } } else { // Decrement for (int i = lastSelected - 1; i >= rowIndex - 1; i--) { if (i >= 0) { RowComponent<T> rowComponent = this.rows.get(i); if (rowComponent != null && rowComponent.isRendered()) { selectRow(rowComponent.getWidget().getElement(), true); } } } } } else { toggleRowSelect(e, row); } return true; }); rows.on("contextmenu." + id, (e, o) -> { Element row = $(e.getCurrentTarget()).asElement(); // Fire row select event RowContextMenuEvent.fire(this, (MouseEvent) e, getModelByRowElement(row), row); return false; }); rows.on("dblclick." + id, (e, o) -> { Element row = $(e.getCurrentTarget()).asElement(); // Fire row select event RowDoubleClickEvent.fire(this, e, getModelByRowElement(row), row); return false; }); JQueryExtension.$(rows).longpress(e -> { Element row = $(e.getCurrentTarget()).asElement(); // Fire row select event RowLongPressEvent.fire(this, e, getModelByRowElement(row), row); return true; }, e -> { Element row = $(e.getCurrentTarget()).asElement(); // Fire row select event RowShortPressEvent.fire(this, e, getModelByRowElement(row), row); return true; }, longPressDuration); JQueryElement expands = $table.find("i#expand"); expands.off("." + id); if (useRowExpansion) { // Expand current row extra information expands.on("tap." + id + " click." + id, e -> { final boolean[] recalculated = { false }; JQueryElement tr = $(e.getCurrentTarget()).parent().parent(); if (!tr.hasClass("disabled") && !tr.is("[disabled]")) { JQueryElement[] expansion = new JQueryElement[] { tr.next().find("td.expansion div") }; if (expansion[0].length() < 1) { expansion[0] = $(expansionHtml).insertAfter(tr); expansion[0] = expansion[0].find("td.expansion div"); } final boolean expanding = !expansion[0].hasClass("expanded"); final JQueryElement row = tr.next(); final T model = getModelByRowElement(tr.asElement()); RowExpansion<T> rowExpansion = new RowExpansion<>(model, row); expansion[0].one(transitionEvents, (e1, param1) -> { if (!recalculated[0]) { // Recalculate subheaders subheaderLib.recalculate(true); recalculated[0] = true; // Apply overlay JQueryElement overlay = row.find("section.overlay"); overlay.height(row.outerHeight(false)); if (expanding) { // Fire table expanded event RowExpandedEvent.fire(this, rowExpansion); } else { // Fire table collapsed event RowCollapsedEvent.fire(this, rowExpansion); } } return true; }); if (expanding) { // Fire table expand event RowExpandingEvent.fire(this, rowExpansion); } else { RowCollapsingEvent.fire(this, rowExpansion); } Scheduler.get().scheduleDeferred(() -> { expansion[0].toggleClass("expanded"); }); } e.stopPropagation(); return true; }); } subheaderLib.detect(); subheaderLib.recalculate(true); } protected void setupStickyHeader() { if ($table != null && display != null) { $table.stickyTableHeaders(StickyTableOptions.create($(".table-body", getContainer()))); } } protected void setupSubHeaders() { if ($table != null && display != null) { subheaderLib = JsTableSubHeaders.newInstance($(".table-body", getContainer()), "tr.subheader"); final JQueryElement header = $table.find("thead"); $(subheaderLib).off("before-recalculate"); $(subheaderLib).on("before-recalculate", e -> { boolean updateMargin = header.is(":visible") && isUseStickyHeader(); subheaderLib.setMarginTop(updateMargin ? header.outerHeight() : 0); return true; }); // Load the subheaders after binding. subheaderLib.load(); } } protected boolean isWithinView(int start, int length) { return isWithinView(start, length, true); } protected boolean isWithinView(int start, int length, boolean canOverflow) { int end = start + length; int rangeStart = range.getStart(); int rangeEnd = rangeStart + range.getLength(); return ((canOverflow ? (end > rangeStart && start < rangeEnd) : (start >= rangeStart && end <= rangeEnd))); } @Override public int getRowCount() { return rows.size(); } @Override public Range getVisibleRange() { return range; } @Override public void setVisibleRange(int start, int length) { setVisibleRange(new Range(start, length)); } @Override public void setVisibleRange(Range range) { setVisibleRange(range, true); } protected void setVisibleRange(Range range, boolean forceRangeChangeEvent) { final int start = range.getStart(); final int length = range.getLength(); if (start < 0) { throw new IllegalArgumentException("Range start cannot be less than 0"); } if (length < 0) { throw new IllegalArgumentException("Range length cannot be less than 0"); } // Update the page start. final int pageStart = this.range.getStart(); final int pageSize = this.range.getLength(); final boolean pageStartChanged = (pageStart != start); if (pageStartChanged) { // Update the range start this.range = new Range(start, this.range.getLength()); } // Update the page size final boolean pageSizeChanged = (pageSize != length); if (pageSizeChanged) { this.range = new Range(this.range.getStart(), length); } // Clear the rows rows.clear(); // Update the pager and data source if the range changed if (pageStartChanged || pageSizeChanged || forceRangeChangeEvent) { RangeChangeEvent.fire(this, getVisibleRange()); } } @Override public boolean isHeaderVisible(int colIndex) { return colIndex < headers.size() && (headers.get(colIndex).$this().is(":visible") || headers.get(colIndex).isVisible()); } @Override public void addColumn(Column<T, ?> column) { addColumn(column, ""); } @Override public void addColumn(Column<T, ?> column, String header) { insertColumn(columns.size(), column, header); } @Override public void insertColumn(int beforeIndex, Column<T, ?> column, String header) { // Allow insert at the end. if (beforeIndex != getColumnCount()) { checkColumnBounds(beforeIndex); } String name = column.getName(); if (name == null || name.isEmpty()) { // Set the columns name column.setName(header); } if (columns.size() < beforeIndex) { columns.add(column); } else { columns.add(beforeIndex, column); } if (setup) { renderColumn(column); } InsertColumnEvent.fire(this, beforeIndex, column, header); } protected void updateSortContext(TableHeader th, Column<T, ?> column) { updateSortContext(th, column, null); } protected void updateSortContext(TableHeader th, Column<T, ?> column, SortDir dir) { if (sortContext == null) { sortContext = new SortContext<>(column, th); } else { Column<T, ?> sortColumn = sortContext.getSortColumn(); if (sortColumn != column) { sortContext.setSortColumn(column); sortContext.setTableHeader(th); } else if (dir == null && sortContext.isSorted()) { sortContext.reverse(); } } if (dir != null) { sortContext.setSortDir(dir); } } @Override public void sort(int columnIndex) { sort(columnIndex, null); } @Override public void sort(int columnIndex, SortDir dir) { sort(columns.get(columnIndex), dir); } @Override public void sort(Column<T, ?> column) { sort(column, null); } @Override public void sort(Column<T, ?> column, SortDir dir) { if (column != null) { int index = columns.indexOf(column) + getColumnOffset(); TableHeader th = headers.get(index); sort(rows, th, column, index, dir); } else { throw new RuntimeException("Cannot sort on a null column."); } } protected Components<RowComponent<T>> sort(Components<RowComponent<T>> rows, TableHeader th, Column<T, ?> column, int index) { return sort(rows, th, column, index, dataSource == null || !dataSource.useRemoteSort()); } protected Components<RowComponent<T>> sort(Components<RowComponent<T>> rows, TableHeader th, Column<T, ?> column, int index, SortDir dir) { return sort(rows, th, column, index, dir, dataSource == null || !dataSource.useRemoteSort()); } protected Components<RowComponent<T>> sort(Components<RowComponent<T>> rows, TableHeader th, Column<T, ?> column, int index, boolean renderRows) { return sort(rows, th, column, index, null, renderRows); } protected Components<RowComponent<T>> sort(Components<RowComponent<T>> rows, TableHeader th, Column<T, ?> column, int index, SortDir dir, boolean renderRows) { SortContext<T> oldSortContext = new SortContext<>(this.sortContext); updateSortContext(th, column, dir); Components<RowComponent<T>> clonedRows = new Components<>(rows, RowComponent::new); if (doSort(sortContext, clonedRows)) { th.addStyleName(TableCssName.SELECTED); // Draw and apply the sort icon. renderer.drawSortIcon(th, sortContext); // No longer a fresh sort sortContext.setSorted(true); if (renderRows) { // Render the new sort order. renderRows(clonedRows); } ColumnSortEvent.fire(this, sortContext, index); } else { // revert the sort context sortContext = oldSortContext; } return clonedRows; } /** * Perform a sort on the a set of {@link RowComponent}'s. * Sorting will check for each components category and sort per category if found. * @return true if the data was sorted, false if no sorting was performed. */ protected boolean doSort(SortContext<T> sortContext, Components<RowComponent<T>> rows) { if (dataSource != null && dataSource.useRemoteSort()) { // The sorting should be handled by an external // data source rather than re-ordered by the // client comparator. return true; } Comparator<? super RowComponent<T>> comparator = sortContext != null ? sortContext.getSortColumn().getSortComparator() : null; if (isUseCategories()) { // Split row data into categories Map<String, List<RowComponent<T>>> splitMap = new HashMap<>(); List<RowComponent<T>> orphanRows = new ArrayList<>(); for (RowComponent<T> row : rows) { if (row != null) { String category = row.getCategoryName(); if (category != null) { List<RowComponent<T>> data = splitMap.computeIfAbsent(category, k -> new ArrayList<>()); data.add(row); } else { orphanRows.add(row); } } } if (!orphanRows.isEmpty()) { splitMap.put(ORPHAN_PATTERN, orphanRows); } rows.clearComponents(); for (Map.Entry<String, List<RowComponent<T>>> entry : splitMap.entrySet()) { List<RowComponent<T>> list = entry.getValue(); if (comparator != null) { list.sort(new DataSort<>(comparator, sortContext.getSortDir())); } rows.addAll(list); } } else { if (comparator != null) { rows.sort(new DataSort<>(comparator, sortContext.getSortDir())); } else if (sortContext != null) { rows.sort(new DataSort<>(new Comparator<RowComponent<T>>() { @Override public int compare(RowComponent<T> o1, RowComponent<T> o2) { return o1.getData().toString().compareToIgnoreCase(o2.getData().toString()); } }, sortContext.getSortDir())); } else { return false; } } return true; } @Override public void removeColumn(int colIndex) { removeColumn(colIndex, true); } public void removeColumn(int colIndex, boolean hardRemove) { int index = colIndex + getColumnOffset(); headerRow.remove(index); for (RowComponent<T> row : rows) { row.getWidget().remove(index); } reindexColumns(); refreshStickyHeaders(); if (hardRemove) { columns.remove(colIndex); RemoveColumnEvent.fire(this, colIndex); } } @Override public void removeColumns() { if (!columns.isEmpty()) { int size = columns.size() - 1; for (int i = 0; i < size; i++) { removeColumn(i, false); } columns.clear(); for (int i = 0; i < size; i++) { RemoveColumnEvent.fire(this, i); } } } @Override public List<Column<T, ?>> getColumns() { return columns; } @Override public int getColumnOffset() { return selectionType.equals(SelectionType.NONE) ? 0 : 1; } /** * Check that the specified column is within bounds. * * @param col the column index * @throws IndexOutOfBoundsException if the column is out of bounds */ private void checkColumnBounds(int col) { if (col < 0 || col >= getColumnCount()) { throw new IndexOutOfBoundsException("Column index is out of bounds: " + col); } } /** * Get the number of columns in the table. * * @return the column count */ public int getColumnCount() { return columns.size(); } @Override public SelectionType getSelectionType() { return selectionType; } @Override public void setSelectionType(SelectionType selectionType) { boolean hadSelection = !this.selectionType.equals(SelectionType.NONE); this.selectionType = selectionType; // Add the selection header if (setup) { if (!selectionType.equals(SelectionType.NONE) && !hadSelection) { setupHeaderSelectionBox(); if (selectionType.equals(SelectionType.MULTIPLE)) { setupShiftDetection(); } // Rebuild the columns for (RowComponent<T> row : rows) { row.getWidget().insert(renderer.drawSelectionCell(), 0); } reindexColumns(); } else if (selectionType.equals(SelectionType.NONE) && hadSelection) { removeHeader(0); $("td#col0", getContainer()).remove(); reindexColumns(); } } } protected void setupHeaderSelectionBox() { // Setup select all checkbox TableHeader th = new TableHeader(); th.setId("col0"); th.setStyleName(TableCssName.SELECTION); if (selectionType.equals(SelectionType.MULTIPLE)) { new MaterialCheckBox(th.getElement()); // Select all row click bind // This will also update the check status of check all input. JQueryElement selectAll = $(th).find("label"); selectAll.off("." + id); selectAll.on("tap." + id + " click." + id, (e) -> { JQueryElement input = $("input", th); boolean marked = Js.isTrue(input.prop("checked")) || Js.isTrue(input.prop("indeterminate")); selectAllRows(!marked || hasDeselectedRows(true)); return false; }); } addHeader(0, th); } protected void setupShiftDetection() { tableBody.attr("tabindex", "0"); tableBody.off("keydown"); tableBody.keydown(e -> { shiftDown = e.isShiftKey(); return true; }); tableBody.off("keyup"); tableBody.keyup(e -> { shiftDown = e.isShiftKey(); return true; }); } protected void reindexColumns() { int colMod = getColumnOffset(); for (RowComponent<T> row : rows) { TableRow tableRow = row.getWidget(); for (int i = colMod; i < tableRow.getWidgetCount(); i++) { TableData td = tableRow.getColumn(i); if (!td.getStyleName().contains("colex")) { td.setId("col" + i); } } } for (int i = colMod; i < headerRow.getWidgetCount(); i++) { TableData td = headerRow.getColumn(i); if (!td.getStyleName().contains("colex")) { td.setId("col" + i); } } } @Override public boolean isSetup() { return setup; } @Override public boolean isRendering() { return rendering; } @Override public void setUseStickyHeader(boolean stickyHeader) { if (this.useStickyHeader && !stickyHeader) { // Destroy existing sticky header function $table.stickyTableHeaders("destroy"); } else if (stickyHeader) { // Initialize sticky header setupStickyHeader(); } this.useStickyHeader = stickyHeader; } @Override public boolean isUseStickyHeader() { return useStickyHeader; } /** * TODO: This method can be optimized. */ private void refreshStickyHeaders() { if ($table != null) { // Destroy existing sticky header function $table.stickyTableHeaders("destroy"); if (isUseStickyHeader()) { // Initialize sticky header setupStickyHeader(); } } } @Override public void selectAllRows(boolean select) { selectAllRows(select, true); } @Override public void selectAllRows(boolean select, boolean fireEvent) { List<Element> rows = new ArrayList<>(); // Select all rows $table.find("tr.data-row").each((i, e) -> { JQueryElement row = $(e); if (row.is(":visible") && !row.hasClass("disabled") && !row.is("[disabled]")) { JQueryElement input = $("td#col0 input", row); input.prop("checked", select); boolean isSelected = row.hasClass("selected"); row.removeClass("selected"); if (select) { row.addClass("selected"); // Only add to row selection if // not selected previously. if (!isSelected) { rows.add(row.asElement()); } } else if (isSelected) { rows.add(row.asElement()); } } }); // Update check all input updateCheckAllInputState(); if (fireEvent) { // Fire select all event SelectAllEvent.fire(this, getModelsByRowElements(rows), rows, select); } } /** * Select a row by given element. * * @param event sourced even (can be null) * @param row element of the row selection */ public void toggleRowSelect(Event event, Element row) { toggleRowSelect(event, row, true); } /** * Select a row by given element. * * @param event sourced even (can be null) * @param row element of the row selection * @param fireEvent fire the row select event. */ public void toggleRowSelect(Event event, Element row, boolean fireEvent) { JQueryElement $row = $(row); if (!$row.hasClass("disabled") && !$row.is("[disabled]")) { boolean selected = Js.isTrue($row.hasClass("selected")); if (selected) { $("td#col0 input", row).prop("checked", false); $row.removeClass("selected"); } else { // deselect all rows when using single selection if (!selectionType.equals(SelectionType.MULTIPLE)) { selectAllRows(false, true); } $("td#col0 input", row).prop("checked", true); $row.addClass("selected"); lastSelected = getRowIndexByElement(row); } // Update check all input updateCheckAllInputState(); if (fireEvent) { // Fire row select event RowSelectEvent.fire(this, event, getModelByRowElement(row), row, !selected); } } } @Override public void selectRow(Element row, boolean fireEvent) { JQueryElement $row = $(row); if (!$row.hasClass("disabled") && !$row.is("[disabled]")) { if (!Js.isTrue($row.hasClass("selected"))) { // deselect all rows when using single selection if (selectionType.equals(SelectionType.SINGLE)) { selectAllRows(false, true); } $("td#col0 input", row).prop("checked", true); $row.addClass("selected"); lastSelected = getRowIndexByElement(row); } // Update check all input updateCheckAllInputState(); if (fireEvent) { // Fire row select event RowSelectEvent.fire(this, null, getModelByRowElement(row), row, true); } } } @Override public void deselectRow(Element row, boolean fireEvent) { JQueryElement $row = $(row); if (!$row.hasClass("disabled") && !$row.is("[disabled]")) { if (Js.isTrue($row.hasClass("selected"))) { $("td#col0 input", row).prop("checked", false); $row.removeClass("selected"); } // Update check all input updateCheckAllInputState(); if (fireEvent) { // Fire row select event RowSelectEvent.fire(this, null, getModelByRowElement(row), row, false); } } } @Override public boolean hasDeselectedRows(boolean visibleOnly) { return $table.find(Selectors.rowInputNotCheckedSelector + (visibleOnly ? ":visible" : "")).length() > 0; } @Override public boolean hasSelectedRows(boolean visibleOnly) { return $table.find(Selectors.rowInputCheckedSelector + (visibleOnly ? ":visible" : "")).length() > 0; } @Override public List<T> getSelectedRowModels(boolean visibleOnly) { final List<T> models = new ArrayList<>(); $table.find(Selectors.rowInputCheckedSelector + (visibleOnly ? ":visible" : "")).each((i, e) -> { T model = getModelByRowElement($(e).parent().parent().asElement()); if (model != null) { models.add(model); } }); return models; } @Override public void setRowData(int start, List<? extends T> values) { int length = values.size(); int end = start + length; // Make sure we have a valid range if (range.getStart() < 0 || range.getLength() < 1) { setVisibleRange(0, length); } // Current range start and end int rangeStart = range.getStart(); int rangeEnd = rangeStart + range.getLength(); // Calculated boundary scope int boundedStart = Math.max(start, rangeStart); int boundedEnd = Math.min(end, rangeEnd); if (start != rangeStart && boundedStart >= boundedEnd) { // The data is out of range for the current range. // Intentionally allow empty lists that start on the range start. return; } // Merge the existing data with the new data Components<RowComponent<T>> rows = new Components<>(this.rows); for (int i = boundedStart; i < boundedEnd; i++) { RowComponent<T> newRow = buildRowComponent(values.get(i - boundedStart)); if (i < rows.size()) { // Within the existing data set rows.set(i, newRow); } else { // Expanding the data set rows.add(newRow); } } // Ensure sort order is applied for new rows doSort(sortContext, rows); pendingRenderEvent = true; if (maybeApplyAutoSortColumn()) { // We have an auto sort column, sort the new rows. Column<T, ?> column = sortContext.getSortColumn(); rows = sort(rows, sortContext.getTableHeader(), column, columns.indexOf(column) + getColumnOffset(), false); } // Render the new rows normally renderRows(rows); } /** * Check and apply the auto sort column {@link Column#setAutoSort(boolean)} * if no sort has been invoked. * @return true if the auto sort column is assigned. */ protected boolean maybeApplyAutoSortColumn() { // Check if we already have a sort column if (sortContext == null || sortContext.getSortColumn() == null) { Column<T, ?> autoSortColumn = getAutoSortColumn(); if (autoSortColumn != null) { if (setup) { int index = columns.indexOf(autoSortColumn) + getColumnOffset(); updateSortContext(headers.get(index), autoSortColumn); return true; } } } return false; } /** * Get the auto sorting column, or null if no column is auto sorting. */ protected Column<T, ?> getAutoSortColumn() { if (autoSortColumn == null) { for (Column<T, ?> column : columns) { if (column.isAutoSort()) { autoSortColumn = column; return autoSortColumn; } } } return autoSortColumn; } public ComponentFactory<? extends CategoryComponent, String> getCategoryFactory() { return categoryFactory; } protected RowComponent<T> buildRowComponent(T data) { if (data != null) { assert rowFactory != null : "The dataview's row factory cannot be null"; return /*buildCustomComponents(*/rowFactory.generate(this, data)/*)*/; } return null; } protected CategoryComponent buildCategoryComponent(RowComponent<T> row) { return row != null ? buildCategoryComponent(row.getCategoryName()) : null; } protected CategoryComponent buildCategoryComponent(String categoryName) { if (categoryName != null) { // Generate the category if not exists if (categoryFactory != null) { CategoryComponent category = getCategory(categoryName); if (category == null) { return categoryFactory.generate(this, categoryName); } else { return category; } } } return null; } /*protected RowComponent<T> buildCustomComponents(RowComponent<T> row) { if(row != null) { // custom components for (ComponentFactory<?, T> factory : componentFactories) { T data = row.getData(); if(data != null) { Component<?> component = factory.generate(data); if (component != null) { row.add(component); } else { logger.fine("DataView component factory: " + factory.toString() + " returned a null component."); } } } } return row; }*/ /** * Get the key for the specified value. If a keyProvider is not specified or the value is null, * the value is returned. If the key provider is specified, it is used to get the key from * the value. * * @param value the value * @return the key */ public Object getValueKey(T value) { ProvidesKey<T> keyProvider = getKeyProvider(); return (keyProvider == null || value == null) ? value : keyProvider.getKey(value); } @Override public ProvidesKey<T> getKeyProvider() { return keyProvider; } @Override public int getVisibleItemCount() { return rows.size(); } @Override public int getRowHeight() { return renderer.getExpectedRowHeight(); } @Override public void setRowHeight(int rowHeight) { renderer.setExpectedRowHeight(rowHeight); } protected int getCalculatedRowHeight() { if (!rows.isEmpty()) { renderer.calculateRowHeight(rows.get(0)); } return renderer.getCalculatedRowHeight(); } @Override public void addCategory(String category) { if (category != null) { addCategory(buildCategoryComponent(category)); } } @Override public void addCategory(final CategoryComponent category) { if (category != null && !hasCategory(category.getName())) { categories.add(category); if (setup && isUseCategories()) { renderCategory(category); } } } protected void renderCategory(CategoryComponent category) { if (category != null) { // Render the category component renderComponent(category); if (subheaderLib != null) { subheaderLib.detect(); subheaderLib.recalculate(true); } } } @Override public boolean hasCategory(String categoryName) { if (categoryName != null) { for (CategoryComponent category : categories) { if (category.getName().equals(categoryName)) { return true; } } } return getOrphansCategory() != null; } @Override public void disableCategory(String categoryName) { CategoryComponent category = getCategory(categoryName); if (category != null && category.isRendered()) { subheaderLib.close(category.getWidget().$this()); category.getWidget().setEnabled(false); } } @Override public void enableCategory(String categoryName) { CategoryComponent category = getCategory(categoryName); if (category != null && category.isRendered()) { category.getWidget().setEnabled(true); } } @Override public List<CategoryComponent> getCategories() { return Collections.unmodifiableList(categories); } @Override public List<CategoryComponent> getOpenCategories() { List<CategoryComponent> openCategories = null; if (isUseCategories()) { openCategories = new ArrayList<>(); for (CategoryComponent category : categories) { TableSubHeader element = category.getWidget(); if (element != null && element.isOpen()) { openCategories.add(category); } } } return openCategories; } @Override public boolean isCategoryEmpty(CategoryComponent category) { for (RowComponent<T> row : rows) { if (row.getCategoryName().equals(category.getName())) { return false; } } return true; } @Override public SortContext<T> getSortContext() { return sortContext; } @Override public boolean isRedraw() { return redraw; } @Override public void setRedraw(boolean redraw) { setRedrawCategories(redraw); this.redraw = redraw; } @Override public void updateRow(final T model) { RowComponent<T> row = getRowByModel(model); if (row != null) { row.setRedraw(true); row.setData(model); renderComponent(row); } } @Override public RowComponent<T> getRow(T model) { for (RowComponent<T> row : rows) { if (row.getData().equals(model)) { return row; } } return null; } @Override public RowComponent<T> getRow(int index) { for (RowComponent<T> row : rows) { if (row.isRendered() && row.getIndex() == index) { return row; } } return null; } @Override public RowComponent<T> getRowByModel(T model) { for (final RowComponent<T> row : rows) { if (row.getData().equals(model)) { return row; } } return null; } protected int getRowIndexByElement(Element rowElement) { for (RowComponent<T> row : rows) { if (row.isRendered() && row.getWidget().getElement().equals(rowElement)) { return row.getIndex(); } } return -1; } protected Element getRowElementByModel(T model) { for (RowComponent<T> row : rows) { if (row.getData().equals(model)) { return row.getWidget().getElement(); } } return null; } protected T getModelByRowElement(Element rowElement) { for (RowComponent<T> row : rows) { if (row.isRendered() && row.getWidget().getElement().equals(rowElement)) { return row.getData(); } } return null; } protected List<T> getModelsByRowElements(List<Element> rowElements) { List<T> models = new ArrayList<>(); for (Element element : rowElements) { models.add(getModelByRowElement(element)); } return models; } protected List<RowComponent<T>> getRowsByCategory(Components<RowComponent<T>> rows, CategoryComponent category) { List<RowComponent<T>> byCategory = new ArrayList<>(); for (RowComponent<T> row : rows) { if (row.getCategoryName().equals(category.getName())) { byCategory.add(row); } } return byCategory; } protected List<CategoryComponent> getHiddenCategories() { List<CategoryComponent> hidden = new ArrayList<>(); for (CategoryComponent category : categories) { TableSubHeader element = category.getWidget(); if (element != null && !element.isVisible()) { hidden.add(category); } } return hidden; } protected List<CategoryComponent> getVisibleCategories() { List<CategoryComponent> visible = new ArrayList<>(); for (CategoryComponent category : categories) { TableSubHeader element = category.getWidget(); if (element != null && element.isVisible()) { visible.add(category); } } return visible; } protected List<CategoryComponent> getPassedCategories() { List<CategoryComponent> passed = new ArrayList<>(); int scrollTop = tableBody.scrollTop(); for (CategoryComponent category : categories) { if (isCategoryEmpty(category) && scrollTop > (getRowHeight() + thead.$this().height())) { passed.add(category); } else { // Hit the current category return passed; } } // No categories are populated. return new ArrayList<>(); } @Override public void setRowFactory(RowComponentFactory<T> rowFactory) { this.rowFactory = rowFactory; } @Override public RowComponentFactory<T> getRowFactory() { return rowFactory; } @Override public void setCategoryFactory(ComponentFactory<? extends CategoryComponent, String> categoryFactory) { this.categoryFactory = categoryFactory; } @Override public void setLoadMask(boolean loadMask) { if (!isSetup()) { // The widget isn't ready yet return; } if (!this.loadMask && loadMask) { if (isUseLoadOverlay()) { if (maskElement == null) { maskElement = $(maskHtml); } $table.prepend(maskElement); } } else if (!loadMask && maskElement != null) { maskElement.detach(); } getProgressWidget().setVisible(loadMask); this.loadMask = loadMask; } @Override public boolean isLoadMask() { return loadMask; } @Override public MaterialProgress getProgressWidget() { return progressWidget; } @Override public int getTotalRows() { return totalRows; } @Override public void setTotalRows(int totalRows) { this.totalRows = totalRows; } @Override public boolean isUseCategories() { return useCategories; } @Override public void setUseCategories(boolean useCategories) { if (this.useCategories && !useCategories) { //subheaderLib.unload(); categories.clearWidgets(); setRedrawCategories(true); } this.useCategories = useCategories; } @Override public boolean isUseLoadOverlay() { return useLoadOverlay; } @Override public void setUseLoadOverlay(boolean useLoadOverlay) { this.useLoadOverlay = useLoadOverlay; } @Override public boolean isUseRowExpansion() { return useRowExpansion; } @Override public void setUseRowExpansion(boolean useRowExpansion) { this.useRowExpansion = useRowExpansion; } @Override public int getLongPressDuration() { return longPressDuration; } @Override public void setLongPressDuration(int longPressDuration) { this.longPressDuration = longPressDuration; } @Override public void loaded(int startIndex, List<T> data) { setRowData(startIndex, data); setLoadMask(false); } @Override public Widget getContainer() { return display.asWidget(); } @Override public String getId() { return id; } @Override public void setDisplay(DataDisplay<T> display) { assert display != null : "Display cannot be null"; this.display = display; } @Override public final void fireEvent(GwtEvent<?> event) { getContainer().fireEvent(event); } protected TableSubHeader bindCategoryEvents(TableSubHeader category) { if (category != null) { // Attach the category events category.$this().off("opened"); category.$this().on("opened", (e, categoryElem) -> { CategoryOpenedEvent.fire(this, category.getName()); return true; }); category.$this().off("closed"); category.$this().on("closed", (e, categoryElem) -> { CategoryClosedEvent.fire(this, category.getName()); return true; }); } return category; } public void updateCheckAllInputState() { updateCheckAllInputState(null); } protected void updateCheckAllInputState(JQueryElement input) { if (Js.isUndefinedOrNull(input)) { input = $table.find("th#col0 input"); } input.prop("indeterminate", false); input.prop("checked", false); if ($("tr.data-row:visible", getContainer()).length() > 0) { boolean fullSelection = !hasDeselectedRows(false); if (!fullSelection && hasSelectedRows(true)) { input.prop("indeterminate", true); } else if (fullSelection) { input.prop("checked", true); } } } public List<RowComponent<T>> getRows() { return Collections.unmodifiableList(rows); } protected List<T> getData() { return RowComponent.extractData(rows); } /** * Get a stored data category by name. */ @Override public CategoryComponent getCategory(String name) { if (name != null) { for (CategoryComponent category : categories) { if (category.getName().equals(name)) { return category; } } } else { return getOrphansCategory(); } return null; } /** * Get the {@link OrphanCategoryComponent} for orphan rows. */ protected OrphanCategoryComponent getOrphansCategory() { for (CategoryComponent category : categories) { if (category instanceof OrphanCategoryComponent) { return (OrphanCategoryComponent) category; } } return null; } public int getCategoryHeight() { if (isUseCategories() && categoryHeight == 0) { try { CategoryComponent categoryComponent = categories.get(0); if (categoryComponent != null && categoryComponent.isRendered()) { categoryHeight = categoryComponent.getWidget().getOffsetHeight(); } } catch (IndexOutOfBoundsException ex) { logger.log(Level.FINE, "Couldn't get the first category.", ex); } } return categoryHeight; } @Override public void openCategory(String categoryName) { openCategory(getCategory(categoryName)); } @Override public void openCategory(CategoryComponent category) { if (category != null && category.isRendered()) { subheaderLib.open(category.getWidget().$this()); } } @Override public void closeCategory(String categoryName) { closeCategory(getCategory(categoryName)); } @Override public void closeCategory(CategoryComponent category) { if (category != null && category.isRendered()) { subheaderLib.close(category.getWidget().$this()); } } /** * Get a stored data categories subheader by name. */ protected TableSubHeader getTableSubHeader(String name) { CategoryComponent category = getCategory(name); return category != null ? category.getWidget() : null; } /** * Get a stored data categories subheader by jquery element. */ protected TableSubHeader getTableSubHeader(JQueryElement elem) { for (CategoryComponent category : categories) { TableSubHeader subheader = category.getWidget(); if (subheader != null && $(subheader).is(elem)) { return subheader; } } return null; } protected void clearExpansions() { $("tr.expansion", getContainer()).remove(); } @Override public void clearRows(boolean clearData) { if (clearData) { rows.clear(); } else { rows.clearWidgets(); } } @Override public void clearCategories() { for (CategoryComponent category : categories) { TableSubHeader subheader = category.getWidget(); if (subheader != null && subheader.isAttached()) { subheader.removeFromParent(); } } categories.clear(); } @Override public void clearRowsAndCategories(boolean clearData) { clearRows(clearData); clearCategories(); } @Override public List<TableHeader> getHeaders() { return Collections.unmodifiableList(headers); } protected void addHeader(int index, TableHeader header) { if (headers.size() < 1) { headers.add(header); } else { headers.add(index, header); } headerRow.insert(header, index); } protected void removeHeader(int index) { if (index < headers.size()) { headers.remove(index); headerRow.remove(index); } refreshStickyHeaders(); } protected int getCategoryRowCount(String category) { int count = 0; for (RowComponent<T> row : rows) { String rowCategory = row.getCategoryName(); if (rowCategory != null) { if (rowCategory.equals(category)) { count++; } } else if (category == null) { // no category count++; } } return count; } protected void setRedrawCategories(boolean redrawCategories) { this.redrawCategories = redrawCategories; } @Override public void setHeight(String height) { this.height = height; // Avoid setting the height prematurely. if (setup) { tableBody.height(height); } } @Override public String getHeight() { return height; } public boolean isShiftDown() { return shiftDown; } }