/* * Copyright 2015 Red Hat, Inc. and/or its affiliates. * * 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 org.uberfire.ext.wires.core.grids.client.widget.grid.renderers.grids.impl; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.uberfire.commons.validation.PortablePreconditions; import org.uberfire.ext.wires.core.grids.client.model.Bounds; import org.uberfire.ext.wires.core.grids.client.model.GridColumn; import org.uberfire.ext.wires.core.grids.client.model.GridData; import org.uberfire.ext.wires.core.grids.client.model.GridRow; import org.uberfire.ext.wires.core.grids.client.widget.grid.GridWidget; import org.uberfire.ext.wires.core.grids.client.widget.grid.renderers.grids.GridRenderer; import org.uberfire.ext.wires.core.grids.client.widget.layer.GridLayer; import org.uberfire.ext.wires.core.grids.client.widget.layer.impl.DefaultGridLayer; /** * Helper for rendering a grid. */ public class BaseGridRendererHelper { private final GridWidget view; public BaseGridRendererHelper(final GridWidget view) { this.view = PortablePreconditions.checkNotNull("view", view); } /** * Get the x-offset for a given Column in the model relative to zero. * @param column The GridColumn. * @return */ public double getColumnOffset(final GridColumn<?> column) { final GridData model = view.getModel(); final int columnIndex = model.getColumns().indexOf(column); if (columnIndex == -1) { return 0; } return getColumnOffset(columnIndex); } /** * Get the x-offset for a given Column index in the model relative to zero. * @param columnIndex The index of the GridColumn. * @return */ public double getColumnOffset(final int columnIndex) { double columnOffset = 0; final GridData model = view.getModel(); final List<GridColumn<?>> columns = model.getColumns(); for (int i = 0; i < columnIndex; i++) { final GridColumn column = columns.get(i); if (column.isVisible()) { columnOffset = columnOffset + column.getWidth(); } } return columnOffset; } /** * Get the x-offset for a given Column index in a list of Columns relative to zero. * @param columns * @param columnIndex * @return */ public double getColumnOffset(final List<GridColumn<?>> columns, final int columnIndex) { double columnOffset = 0; for (int idx = 0; idx < columnIndex; idx++) { final GridColumn<?> column = columns.get(idx); if (column.isVisible()) { columnOffset = columnOffset + column.getWidth(); } } return columnOffset; } /** * Get the y-offset for a given Row. * @param row The GridRow. * @return */ public double getRowOffset(final GridRow row) { final GridData model = view.getModel(); final int rowIndex = model.getRows().indexOf(row); return getRowOffset(rowIndex); } /** * Get the y-offset for a given Row index. * @param rowIndex The index of the GridRow. * @return */ public double getRowOffset(final int rowIndex) { double rowOffset = 0; final GridData model = view.getModel(); for (int i = 0; i < rowIndex; i++) { final GridRow row = model.getRow(i); rowOffset = rowOffset + row.getHeight(); } return rowOffset; } /** * Get the width of a set of columns, ignoring hidden columns. * @param columns The columns. * @return */ public double getWidth(final List<GridColumn<?>> columns) { double width = 0; for (GridColumn<?> column : columns) { if (column.isVisible()) { width = width + column.getWidth(); } } return width; } /** * Get rendering information about which columns are floating, which are visible. This method never returns null. * It returns a RenderingInformation object representing the columns that are visible and/or floating. * @return A RenderingInformation object or null if the GridWidget is not even partially visible. */ public RenderingInformation getRenderingInformation() { final GridData model = view.getModel(); final Bounds bounds = getVisibleBounds(); final List<GridColumn<?>> allColumns = new ArrayList<GridColumn<?>>(); final List<GridColumn<?>> bodyColumns = new ArrayList<GridColumn<?>>(); final List<GridColumn<?>> floatingColumns = new ArrayList<GridColumn<?>>(); final double vpX = bounds.getX(); final double vpY = bounds.getY(); final double vpWidth = bounds.getWidth(); final double vpHeight = bounds.getHeight(); final double headerOffsetY = getHeaderOffsetY(); final GridRenderer renderer = view.getRenderer(); //Simple bounds check if (view.getX() > vpX + vpWidth) { return null; } else if (view.getX() + view.getWidth() < vpX) { return null; } else if (view.getY() > vpY + vpHeight) { return null; } else if (view.getY() + view.getHeight() < vpY) { return null; } //Identify type of header boolean isFixedHeader = false; boolean isFloatingHeader = false; if (view.isSelected()) { if (view.getY() < vpY) { //GridWidget is selected and clipped at the top if (view.getY() + view.getHeight() > vpY + renderer.getHeaderHeight()) { //GridWidget is taller than the Header; add floating header isFloatingHeader = true; } else { //GridWidget is shorter than the Header; add fixed header isFixedHeader = true; } } else if (view.getY() <= vpY + vpHeight) { //GridWidget is selected and not clipped at the top; add fixed header isFixedHeader = true; } } else if (view.getY() + renderer.getHeaderHeight() > vpY && view.getY() < vpY + vpHeight) { //GridWidget is not selected; add fixed header isFixedHeader = true; } //Identify rows to render GridRow row; int minVisibleRowIndex = 0; if (model.getRowCount() > 0) { double clipTop = vpY - view.getY() - (isFloatingHeader ? 0.0 : renderer.getHeaderHeight()); while ((row = model.getRow(minVisibleRowIndex)).getHeight() < clipTop && minVisibleRowIndex < model.getRowCount() - 1) { clipTop = clipTop - row.getHeight(); minVisibleRowIndex++; } } int maxVisibleRowIndex = minVisibleRowIndex; if (model.getRowCount() > 0) { double clipBottom = vpY - view.getY() - renderer.getHeaderHeight() + vpHeight - getRowOffset(minVisibleRowIndex); while ((row = model.getRow(maxVisibleRowIndex)).getHeight() < clipBottom && maxVisibleRowIndex < model.getRowCount() - 1) { clipBottom = clipBottom - row.getHeight(); maxVisibleRowIndex++; } } //Identify columns to render double x = 0; for (GridColumn<?> column : model.getColumns()) { allColumns.add(column); final double floatingColumnsWidth = getWidth(floatingColumns); if (view.getX() + x + column.getWidth() >= vpX + floatingColumnsWidth) { if (view.getX() + x < vpX + vpWidth) { bodyColumns.add(column); } } if (view.isSelected()) { if (column.isFloatable()) { if (view.getX() + x < vpX + floatingColumnsWidth) { allColumns.remove(column); bodyColumns.remove(column); floatingColumns.add(column); } } } if (column.isVisible()) { x = x + column.getWidth(); } } //If the floating columns obscure the body columns remove the float and just show the body columns if (view.getX() + x - vpX < getWidth(floatingColumns)) { allColumns.clear(); bodyColumns.clear(); floatingColumns.clear(); allColumns.addAll(model.getColumns()); x = 0; for (GridColumn<?> column : model.getColumns()) { if (view.getX() + x + column.getWidth() >= vpX) { if (view.getX() + x < vpX + vpWidth) { bodyColumns.add(column); } } if (column.isVisible()) { x = x + column.getWidth(); } } } //Construct details of Floating and Body blocks final double bodyOffsetY = getRowOffset(minVisibleRowIndex) + renderer.getHeaderHeight(); final double offsetX = (bodyColumns.size() > 0 ? getColumnOffset(bodyColumns.get(0)) : 0); final double floatingOffsetX = getFloatingColumnOffset(); final RenderingBlockInformation bodyBlockInformation = new RenderingBlockInformation(bodyColumns, offsetX, headerOffsetY, bodyOffsetY, getWidth(bodyColumns)); final RenderingBlockInformation floatingBlockInformation = new RenderingBlockInformation(floatingColumns, floatingOffsetX, headerOffsetY, bodyOffsetY, getWidth(floatingColumns)); // Construct "row offsets". The row offsets are based from zero; for each row to be rendered. // The minVisibleRowIndex corresponds to index zero and maxVisibleRowIndex corresponds to visibleRowOffsets.size() - 1. // This is useful to calculate the Y co-ordinate of each Row's top. It is calculated once and passed to // each column as an optimisation to prevent each column from recalculating the same values. final List<Double> visibleRowOffsets = new ArrayList<Double>(); if (model.getRowCount() > 0) { double visibleRowOffset = getRowOffset(minVisibleRowIndex); for (int rowIndex = minVisibleRowIndex; rowIndex <= maxVisibleRowIndex; rowIndex++) { visibleRowOffsets.add(visibleRowOffset); visibleRowOffset = visibleRowOffset + model.getRow(rowIndex).getHeight(); } } final int headerRowCount = model.getHeaderRowCount(); final double headerHeight = renderer.getHeaderHeight(); final double headerRowHeight = renderer.getHeaderRowHeight(); final double headerRowsHeight = headerRowHeight * headerRowCount; final double headerRowsYOffset = headerHeight - headerRowsHeight; //Finally return all rendering information return new RenderingInformation(bounds, allColumns, bodyBlockInformation, floatingBlockInformation, minVisibleRowIndex, maxVisibleRowIndex, visibleRowOffsets, isFixedHeader, isFloatingHeader, headerRowsHeight, headerRowCount, headerRowsYOffset); } /** * Get information about a column corresponding to a grid-relative x-coordinate. This method never returns null. * It returns a ColumnInformation object representing the column corresponding to the grid-relative x-coordinate; * or an empty ColumnInformation object if no corresponding column was found. * @param cx An x-coordinate relative to the GridWidget. * @return A non-null ColumnInformation object. */ public ColumnInformation getColumnInformation(final double cx) { //Gather information on columns final RenderingInformation renderingInformation = getRenderingInformation(); if (renderingInformation == null) { return new ColumnInformation(); } final GridData model = view.getModel(); final List<GridColumn<?>> columns = model.getColumns(); final RenderingBlockInformation bodyBlockInformation = renderingInformation.getBodyBlockInformation(); final RenderingBlockInformation floatingBlockInformation = renderingInformation.getFloatingBlockInformation(); final List<GridColumn<?>> bodyColumns = bodyBlockInformation.getColumns(); final List<GridColumn<?>> floatingColumns = floatingBlockInformation.getColumns(); final double floatingX = floatingBlockInformation.getX(); final double floatingWidth = floatingBlockInformation.getWidth(); //Check floating columns double offsetX = floatingX; GridColumn<?> column = null; for (GridColumn<?> gridColumn : floatingColumns) { if (gridColumn.isVisible()) { final double columnWidth = gridColumn.getWidth(); if (cx > offsetX && cx < offsetX + columnWidth) { column = gridColumn; break; } offsetX = offsetX + columnWidth; } } if (column != null) { return new ColumnInformation(column, columns.indexOf(column), offsetX); } //Check all other columns offsetX = bodyBlockInformation.getX(); for (GridColumn<?> gridColumn : bodyColumns) { if (gridColumn.isVisible()) { final double columnWidth = gridColumn.getWidth(); if (offsetX + columnWidth > floatingX + floatingWidth) { if (cx > offsetX && cx < offsetX + columnWidth) { column = gridColumn; break; } } offsetX = offsetX + columnWidth; } } if (column == null) { return new ColumnInformation(); } return new ColumnInformation(column, columns.indexOf(column), offsetX); } /** * Get the visible bounds (canvas coordinate system) of the given GridWidget. * @return */ private Bounds getVisibleBounds() { final GridLayer gridLayer = ((DefaultGridLayer) view.getLayer()); final Bounds bounds = gridLayer.getVisibleBounds(); return bounds; } /** * Find the x-offset relative to the GridWidget origin where Floating columns are positioned. * @return */ private double getFloatingColumnOffset() { final Bounds bounds = getVisibleBounds(); return bounds.getX() - view.getX(); } /** * Find the y-offset relative to the GridWidget origin where Floating Header is positioned. * @return */ private double getHeaderOffsetY() { final double vpY = getVisibleBounds().getY(); if (view.isSelected()) { if (view.getY() < vpY && view.getY() + view.getHeight() > vpY + view.getRenderer().getHeaderHeight()) { return vpY - view.getY(); } } return 0.0; } /** * A container for Column Information. */ public static class ColumnInformation { private GridColumn<?> column; private int uiColumnIndex = -1; private double offsetX = -1; ColumnInformation() { } public ColumnInformation(final GridColumn<?> column, final int uiColumnIndex, final double offsetX) { this.column = column; this.uiColumnIndex = uiColumnIndex; this.offsetX = offsetX; } /** * The GridWidget's column corresponding to the grid-relative x-coordinate, or null if none was found. * @return */ public GridColumn<?> getColumn() { return column; } /** * The index of the GridWidget's column. This is equivalent to columns.indexOf(column). * @return */ public int getUiColumnIndex() { return uiColumnIndex; } /** * The x-offset of the Column's left-hand edge relative to the GridWidget. i.e. column 0 has an x-offset of 0. * Floating columns canvas position is set dynamically depending on the GridWidget's position and the canvas's * Viewport. Therefore the x-offset of the first floating column is not zero but subject to the Viewport. * @return */ public double getOffsetX() { return offsetX; } } /** * A container for Rendering Information. */ public static class RenderingInformation { private final Bounds bounds; private final List<GridColumn<?>> allColumns; private final RenderingBlockInformation bodyBlockInformation; private final RenderingBlockInformation floatingBlockInformation; private final int minVisibleRowIndex; private final int maxVisibleRowIndex; private final List<Double> visibleRowOffsets; private final boolean isFixedHeader; private final boolean isFloatingHeader; private final double headerRowsHeight; private final double headerRowCount; private final double headerRowsYOffset; public RenderingInformation(final Bounds bounds, final List<GridColumn<?>> allColumns, final RenderingBlockInformation bodyBlockInformation, final RenderingBlockInformation floatingBlockInformation, final int minVisibleRowIndex, final int maxVisibleRowIndex, final List<Double> visibleRowOffsets, final boolean isFixedHeader, final boolean isFloatingHeader, final double headerRowsHeight, final double headerRowCount, final double headerRowsYOffset) { this.bounds = bounds; this.allColumns = allColumns; this.bodyBlockInformation = bodyBlockInformation; this.floatingBlockInformation = floatingBlockInformation; this.minVisibleRowIndex = minVisibleRowIndex; this.maxVisibleRowIndex = maxVisibleRowIndex; this.visibleRowOffsets = visibleRowOffsets; this.isFixedHeader = isFixedHeader; this.isFloatingHeader = isFloatingHeader; this.headerRowsHeight = headerRowsHeight; this.headerRowCount = headerRowCount; this.headerRowsYOffset = headerRowsYOffset; } public Bounds getBounds() { return bounds; } public List<GridColumn<?>> getAllColumns() { return allColumns; } public RenderingBlockInformation getBodyBlockInformation() { return bodyBlockInformation; } public RenderingBlockInformation getFloatingBlockInformation() { return floatingBlockInformation; } public int getMinVisibleRowIndex() { return minVisibleRowIndex; } public int getMaxVisibleRowIndex() { return maxVisibleRowIndex; } public List<Double> getVisibleRowOffsets() { return Collections.unmodifiableList(visibleRowOffsets); } public boolean isFixedHeader() { return isFixedHeader; } public boolean isFloatingHeader() { return isFloatingHeader; } public double getHeaderRowsHeight() { return headerRowsHeight; } public double getHeaderRowCount() { return headerRowCount; } public double getHeaderRowsYOffset() { return headerRowsYOffset; } } /** * A container for Rendering Block Information. */ public static class RenderingBlockInformation { private final List<GridColumn<?>> columns; private final double x; private final double headerY; private final double bodyY; private final double width; public RenderingBlockInformation(final List<GridColumn<?>> columns, final double x, final double headerY, final double bodyY, final double width) { this.columns = columns; this.x = x; this.headerY = headerY; this.bodyY = bodyY; this.width = width; } public List<GridColumn<?>> getColumns() { return Collections.unmodifiableList(columns); } public double getX() { return x; } public double getHeaderY() { return headerY; } public double getBodyY() { return bodyY; } public double getWidth() { return width; } } }