All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.palexdev.virtualizedfx.table.VFXTableHelper Maven / Gradle / Ivy

There is a newer version: 21.6.4
Show newest version
/*
 * Copyright (C) 2024 Parisi Alessandro - [email protected]
 * This file is part of VirtualizedFX (https://github.com/palexdev/VirtualizedFX)
 *
 * VirtualizedFX is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 3 of the License,
 * or (at your option) any later version.
 *
 * VirtualizedFX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with VirtualizedFX. If not, see .
 */

package io.github.palexdev.virtualizedfx.table;

import io.github.palexdev.mfxcore.base.beans.Position;
import io.github.palexdev.mfxcore.base.beans.Size;
import io.github.palexdev.mfxcore.base.beans.range.DoubleRange;
import io.github.palexdev.mfxcore.base.beans.range.IntegerRange;
import io.github.palexdev.mfxcore.base.beans.range.NumberRange;
import io.github.palexdev.mfxcore.base.properties.PositionProperty;
import io.github.palexdev.mfxcore.base.properties.range.IntegerRangeProperty;
import io.github.palexdev.mfxcore.builders.bindings.DoubleBindingBuilder;
import io.github.palexdev.mfxcore.builders.bindings.ObjectBindingBuilder;
import io.github.palexdev.mfxcore.observables.OnInvalidated;
import io.github.palexdev.mfxcore.observables.When;
import io.github.palexdev.mfxcore.utils.NumberUtils;
import io.github.palexdev.virtualizedfx.cells.base.VFXCell;
import io.github.palexdev.virtualizedfx.cells.base.VFXTableCell;
import io.github.palexdev.virtualizedfx.enums.ColumnsLayoutMode;
import io.github.palexdev.virtualizedfx.utils.Utils;
import io.github.palexdev.virtualizedfx.utils.VFXCellsCache;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.scene.Node;

import java.util.Objects;
import java.util.Optional;
import java.util.stream.IntStream;

/**
 * This interface is a utility API for {@link VFXTable}, computations may change depending on the
 * {@link VFXTable#columnsLayoutModeProperty()}. For this reason, there are two concrete implementations:
 * {@link FixedTableHelper} and {@link VariableTableHelper}.
 */
public interface VFXTableHelper {

	/**
	 * @return the index of the first visible column
	 */
	int firstColumn();

	/**
	 * @return the index of the last visible column
	 */
	int lastColumn();

	/**
	 * @return the number of columns visible in the viewport. Not necessarily the same as {@link #totalColumns()}
	 */
	int visibleColumns();

	/**
	 * @return the total number of columns in the viewport which doesn't include only the number of visible columns but also
	 * the number of buffer columns
	 * @see VFXTable#columnsBufferSizeProperty()
	 */
	int totalColumns();

	/**
	 * Specifies the range of columns that should be present in the viewport. This also takes into account buffer columns,
	 * see {@link #visibleColumns()} and {@link #totalColumns()}.
	 */
	ReadOnlyObjectProperty> columnsRangeProperty();

	/**
	 * @return the range of columns that should be present in the viewport. This also takes into account buffer columns,
	 * see {@link #visibleColumns()} and {@link #totalColumns()}
	 */
	default IntegerRange columnsRange() {
		return (IntegerRange) columnsRangeProperty().get();
	}

	/**
	 * @return the index of the first visible row
	 */
	int firstRow();

	/**
	 * @return the index of the last visible row
	 */
	int lastRow();

	/**
	 * @return the number of rows visible in the viewport. Not necessarily the same as {@link #totalRows()}
	 */
	int visibleRows();

	/**
	 * @return the total number of rows in the viewport which doesn't include only the number of visible rows but also
	 * the number of buffer rows
	 */
	int totalRows();

	/**
	 * Specifies the range of rows that should be present in the viewport. This also takes into account buffer rows,
	 * see {@link #visibleRows()} and {@link #totalRows()}.
	 */
	ReadOnlyObjectProperty> rowsRangeProperty();

	/**
	 * @return the range of rows that should be present in the viewport. This also takes into account buffer rows,
	 * see {@link #visibleRows()} and {@link #totalRows()}.
	 */
	default IntegerRange rowsRange() {
		return (IntegerRange) rowsRangeProperty().get();
	}

	/**
	 * Specifies the total number of pixels on the x-axis.
	 */
	ReadOnlyDoubleProperty virtualMaxXProperty();

	/**
	 * @return the total number of pixels on the x-axis.
	 */
	default double getVirtualMaxX() {
		return virtualMaxXProperty().get();
	}

	/**
	 * Specifies the total number of pixels on the y-axis.
	 */
	ReadOnlyDoubleProperty virtualMaxYProperty();

	/**
	 * @return the total number of pixels on the y-axis.
	 */
	default double getVirtualMaxY() {
		return virtualMaxYProperty().get();
	}

	/**
	 * @return the maximum possible vertical position.
	 */
	default double getMaxVScroll() {
		return maxVScrollProperty().get();
	}

	/**
	 * Specifies the maximum possible vertical position.
	 */
	ReadOnlyDoubleProperty maxVScrollProperty();

	/**
	 * @return the maximum possible horizontal position.
	 */
	default double getMaxHScroll() {
		return maxHScrollProperty().get();
	}

	/**
	 * Specifies the maximum possible horizontal position.
	 */
	ReadOnlyDoubleProperty maxHScrollProperty();

	/**
	 * Cells are actually contained in a separate pane called 'viewport'. The scroll is applied on this pane.
	 * 

* This property specifies the translation of the viewport, the value depends on the implementation. */ ReadOnlyObjectProperty viewportPositionProperty(); /** * @return the position the viewport should be at in the container */ default Position getViewportPosition() { return viewportPositionProperty().get(); } /** * @return the width for the given column */ double getColumnWidth(VFXTableColumn column); /** * @return the x position for the given column and its layout index in the viewport */ double getColumnPos(int layoutIdx, VFXTableColumn column); /** * @return whether the given column is currently visible in the viewport */ boolean isInViewport(VFXTableColumn column); /** * Lays out the given column. * The layout index is necessary to identify the position of a column among the others (comes before/after). * * @return whether the column was sized and positioned successfully * @see VFXTableSkin#layoutColumns() */ boolean layoutColumn(int layoutIdx, VFXTableColumn column); /** * Lays out the given row. * The layout index is necessary to identify the position of a row among the others (comes above/below). *

* Positions the row at {@code X: 0} and {@code Y: index * rowsHeight}. *

* Sizes the row to be {@code W: virtualMaxX} and {@code H: rowsHeight}. * * @see VFXTableSkin#layoutRows() */ default void layoutRow(int layoutIdx, VFXTableRow row) { double w = getVirtualMaxX(); double h = getTable().getRowsHeight(); double y = layoutIdx * h; row.beforeLayout(); row.resizeRelocate(0, y, w, h); row.afterLayout(); } /** * Lays out the given cell. * The layout index is necessary to identify the position of a cell among the others (comes before/after). * * @see VFXTableRow#layoutCells() */ boolean layoutCell(int layoutIdx, VFXTableCell cell); /** * Determines and sets the ideal width for the given column, where 'ideal' means that * the column's header as well as all the related cells' content will be fully visible. *

* Note: for obvious reasons, the computation is done on the currently visible items! */ void autosizeColumn(VFXTableColumn column); /** * Depends on the implementation! */ void autosizeColumns(); /** * @return the {@link VFXTable} instance the helper is related to */ VFXTable getTable(); /** * Depends on the implementation! */ int visibleCells(); /** * @return the total number of cells in the viewport which doesn't include only the number of visible cells but also * the number of buffer cells */ default int totalCells() { int nColumns = columnsRange().diff() + 1; int nRows = rowsRange().diff() + 1; return nColumns * nRows; } /** * Forces the {@link VFXTable#vPosProperty()} and {@link VFXTable#hPosProperty()} to be invalidated. * This is simply done by calling the respective setters with their current respective values. Those two properties * will automatically call {@link #getMaxVScroll()} and {@link #getMaxHScroll()} to ensure the values are correct. * Automatically invoked by the {@link VFXTableManager} when needed. */ default void invalidatePos() { VFXTable table = getTable(); table.setVPos(table.getVPos()); table.setHPos(table.getHPos()); } /** * Converts the given index to an item (shortcut for {@code getTable().getItems().get(index)}). */ default T indexToItem(int index) { return getTable().getItems().get(index); } /** * Converts the given index to a row. Uses {@link #itemToRow(Object)}. */ default VFXTableRow indexToRow(int index) { T item = indexToItem(index); return itemToRow(item); } /** * Converts the given item to a row. The result is either on of the rows cached in {@link VFXCellsCache} that * is updated with the given item, or a totally new one created by the {@link VFXTable#rowFactoryProperty()}. */ default VFXTableRow itemToRow(T item) { VFXCellsCache> cache = getTable().getCache(); Optional> opt = cache.tryTake(); opt.ifPresent(c -> c.updateItem(item)); return opt.orElseGet(() -> getTable().getRowFactory().apply(item)); } /** * @return the viewport's height by taking into account the table's header height, which is given by * {@link VFXTable#columnsSizeProperty()} */ default double getViewportHeight() { VFXTable table = getTable(); return Math.max(0, table.getHeight() - table.getColumnsSize().getHeight()); } /** * Checks whether the given column is the last in {@link VFXTable#getColumns()}. * Basically a shortcut for {@code table.getColumn().getLast() == column)} */ default boolean isLastColumn(VFXTableColumn column) { ObservableList>> columns = getTable().getColumns(); if (columns.isEmpty()) return false; return columns.getLast() == column; } /** * Scrolls in the viewport, in the given direction (orientation) by the given number of pixels. */ default void scrollBy(Orientation orientation, double pixels) { VFXTable table = getTable(); if (orientation == Orientation.HORIZONTAL) { table.setHPos(table.getHPos() + pixels); } else { table.setVPos(table.getVPos() + pixels); } } /** * Scrolls in the viewport, in the given direction (orientation) to the given pixel value. */ default void scrollToPixel(Orientation orientation, double pixel) { VFXTable table = getTable(); if (orientation == Orientation.HORIZONTAL) { table.setHPos(pixel); } else { table.setVPos(pixel); } } /** * Scrolls in the viewport, depending on the given direction (orientation) to: *

- the item at the given index if it's {@link Orientation#VERTICAL} *

- the column at the given index if it's {@link Orientation#HORIZONTAL} */ void scrollToIndex(Orientation orientation, int index); /** * Automatically called by {@link VFXTable} when a helper is not needed anymore (changed). * If the helper uses listeners/bindings that may lead to memory leaks, this is the right place to remove them. */ default void dispose() {} /** * Abstract implementation of {@link VFXTableHelper}, contains common members for the two concrete implementations * {@link FixedTableHelper} and {@link VariableTableHelper}, such as: *

- the range of columns to display as a {@link IntegerRangeProperty} *

- the range of rows to display as a {@link IntegerRangeProperty} *

- the virtual max x as a {@link ReadOnlyDoubleWrapper} *

- the virtual max y as a {@link ReadOnlyDoubleWrapper} *

- the viewport's position, {@link #viewportPositionProperty()} as a {@link PositionProperty} * * @param */ abstract class AbstractHelper implements VFXTableHelper { protected VFXTable table; protected final IntegerRangeProperty columnsRange = new IntegerRangeProperty(); protected final IntegerRangeProperty rowsRange = new IntegerRangeProperty(); protected final ReadOnlyDoubleWrapper virtualMaxX = new ReadOnlyDoubleWrapper(); protected final ReadOnlyDoubleWrapper virtualMaxY = new ReadOnlyDoubleWrapper(); protected final ReadOnlyDoubleWrapper maxHScroll = new ReadOnlyDoubleWrapper(); protected final ReadOnlyDoubleWrapper maxVScroll = new ReadOnlyDoubleWrapper(); protected final PositionProperty viewportPosition = new PositionProperty(); public AbstractHelper(VFXTable table) { this.table = table; initBindings(); } /** * Bindings and listeners should be initialized here, automatically called after the table instance is set. */ protected void initBindings() { maxHScroll.bind(DoubleBindingBuilder.build() .setMapper(() -> Math.max(0, getVirtualMaxX() - table.getWidth())) .addSources(virtualMaxX, table.widthProperty()) .get() ); maxVScroll.bind(DoubleBindingBuilder.build() .setMapper(() -> Math.max(0, getVirtualMaxY() - getViewportHeight())) .addSources(virtualMaxY, table.heightProperty(), table.columnsSizeProperty()) .get() ); } /** * {@inheritDoc} *

* Given by {@code columnsRange().getMax()} */ @Override public int lastColumn() { return columnsRange().getMax(); } @Override public ReadOnlyObjectProperty> columnsRangeProperty() { return columnsRange; } /** * {@inheritDoc} *

* Given by {@code Math.floor(vPos / rowsHeight)}, clamped between 0 and {@link VFXTable#size()} - 1. */ @Override public int firstRow() { return NumberUtils.clamp( (int) Math.floor(table.getVPos() / table.getRowsHeight()), 0, table.size() - 1 ); } /** * {@inheritDoc} *

* Given by {@code rowsRange().getMax()} */ @Override public int lastRow() { return rowsRange().getMax(); } /** * {@inheritDoc} *

* Given by {@code Math.ceil(viewportHeight / rowsHeight)}. 0 if the rows height is also 0. */ @Override public int visibleRows() { double height = table.getRowsHeight(); return height > 0 ? (int) Math.ceil(getViewportHeight() / height) : 0; } /** * {@inheritDoc} *

* Given by {@code visibleRows + rowsBuffer * 2}, can't exceed {@link VFXTable#size()} and it's 0 if the number * of visible rows is also 0. */ @Override public int totalRows() { int visible = visibleRows(); return visible == 0 ? 0 : Math.min(visible + table.getRowsBufferSize().val() * 2, table.size()); } @Override public ReadOnlyObjectProperty> rowsRangeProperty() { return rowsRange; } @Override public ReadOnlyDoubleProperty virtualMaxXProperty() { return virtualMaxX; } @Override public ReadOnlyDoubleProperty virtualMaxYProperty() { return virtualMaxY; } @Override public ReadOnlyDoubleProperty maxVScrollProperty() { return maxVScroll.getReadOnlyProperty(); } @Override public ReadOnlyDoubleProperty maxHScrollProperty() { return maxHScroll.getReadOnlyProperty(); } @Override public ReadOnlyObjectProperty viewportPositionProperty() { return viewportPosition; } @Override public VFXTable getTable() { return table; } /** * {@inheritDoc} *

* Sets the table reference to {@code null}. */ @Override public void dispose() { table = null; } } /** * Concrete implementation of {@link AbstractHelper} for {@link ColumnsLayoutMode#FIXED}. * Here the range of rows and columns to display, as well as the viewport position, * the virtual max x and y properties are defined as follows: *

- the columns range is given by the {@link #firstColumn()} element minus the buffer size {@link VFXTable#columnsBufferSizeProperty()}, * (cannot be negative) and the sum between this start index and the total number of needed columns given by {@link #totalColumns()}. * It may happen that the number of indexes given by the range {@code end - start + 1} is * lesser than the total number of columns we need. In such cases, the range start is corrected to be * {@code end - needed + 1}. A typical situation for this is when the table's horizontal position reaches the max scroll. * If the table's width is 0 or the number of needed columns is 0, then the range will be {@link Utils#INVALID_RANGE}. * The computation has the following dependencies: the columns' list, the table width, the horizontal position, * the columns buffer size and the columns' size. *

- the rows range is given by the {@link #firstRow()} element minus the buffer size {@link VFXTable#rowsBufferSizeProperty()}, * (cannot be negative) and the sum between this start index and the total number of needed rows given by {@link #totalRows()}. * It may happen that the number of indexes given by the range {@code end - start + 1} is * lesser than the number of rows we need. In such cases, the range start is corrected to be * {@code end - needed + 1}. A typical situation for this is when the table's vertical position reaches the max scroll. * If the viewport's height is 0 or the number of needed rows is 0, then the range will be {@link Utils#INVALID_RANGE}. * The computation has the following dependencies: the table's height, the column's size (because it also specifies the * header height, which influences the viewport's height), the vertical position, the rows buffer size, the rows' height * and the items' list size. *

- the viewport's position, a computation that is at the core of virtual scrolling. The viewport, which contains * the columns and the cells (even though the table's viewport is a bit more complex), is not supposed to scroll by insane * numbers of pixels both for performance reasons and because it is not necessary. * For both the horizontal and vertical positions, we use the same technique, just using the appropriate values according * to the axis we are working on. * First we get the range of rows/columns to display, then their respective sizes (rows' height, columns' width). * We compute the ranges to the first visible row/column, which are given by {@code IntegerRange.of(range.getMin(), first())}, * in other words we limit the 'complete' ranges to the start buffer including the first row/column after the buffer. * The number of indexes in the newfound ranges (given by {@link IntegerRange#diff()}) is multiplied by the respective * sizes, this way we are finding the number of pixels to the first visible row/column, {@code pixelsToFirst}. * At this point, we are missing only one last piece of information: how much of the first row/column do we actually see? * We call this amount {@code visibleAmountFirst} and it's given by {@code pos % size}. * Finally, the viewport's position is given by this formula {@code -(pixelsToFirst + visibleAmountFirst)} * (for both hPos and vPos of course). * If a range is equal to {@link Utils#INVALID_RANGE}, the respective position will be 0! * While it's true that the calculations are more complex and 'needy', it's important to note that this approach * allows avoiding 'hacks' to correctly lay out the cells in the viewport. No need for special offsets at the top * or bottom anymore. * The viewport's position computation has the following dependencies: the vertical and horizontal positions, * the rows' height and the columns' size. *

- the virtual max x and y properties, which give the total number of pixels on the x-axis and y-axis. Virtual * means that it's not the actual size of the container, rather the size it would have if it was not virtualized. * The two values are given by the number of rows/columns multiplied by the respective size (rows' height, columns' width). * Notes: 1) the virtualMaxX is the maximum between the aforementioned computation and the table's width (because the last * column must always take all the available space). 2) the virtualMaxY is going to be 0 if there are no columns in the table. * The computations have the following dependencies: the table's width, the number of columns and items, the columns' size, * the rows' height. */ @SuppressWarnings("JavadocReference") // I don't know why since the method is public class FixedTableHelper extends AbstractHelper { private boolean forceLayout = false; public FixedTableHelper(VFXTable table) { super(table); } @Override protected void initBindings() { super.initBindings(); columnsRange.bind(ObjectBindingBuilder.build() .setMapper(() -> { if (table.getWidth() <= 0) return Utils.INVALID_RANGE; int needed = totalColumns(); if (needed == 0) return Utils.INVALID_RANGE; int start = Math.max(0, firstColumn() - table.getColumnsBufferSize().val()); int end = Math.min(table.getColumns().size() - 1, start + needed - 1); if (end - start + 1 < needed) start = Math.max(0, end - needed + 1); return IntegerRange.of(start, end); }) .addSources(table.getColumns()) .addSources(table.widthProperty()) .addSources(table.hPosProperty()) .addSources(table.columnsBufferSizeProperty()) .addSources(table.columnsSizeProperty()) .get() ); rowsRange.bind(ObjectBindingBuilder.build() .setMapper(() -> { if (getViewportHeight() <= 0) return Utils.INVALID_RANGE; int needed = totalRows(); if (needed == 0) return Utils.INVALID_RANGE; int start = Math.max(0, firstRow() - table.getRowsBufferSize().val()); int end = Math.min(table.size() - 1, start + needed - 1); if (end - start + 1 < needed) start = Math.max(0, end - needed + 1); return IntegerRange.of(start, end); }) .addSources(table.sizeProperty()) .addSources(table.heightProperty(), table.columnsSizeProperty()) .addSources(table.vPosProperty()) .addSources(table.rowsBufferSizeProperty()) .addSources(table.rowsHeightProperty()) .get() ); virtualMaxX.bind(DoubleBindingBuilder.build() .setMapper(() -> Math.max(table.getWidth(), table.getColumns().size() * table.getColumnsSize().getWidth())) .addSources(table.widthProperty(), table.getColumns(), table.columnsSizeProperty()) .get() ); virtualMaxY.bind(DoubleBindingBuilder.build() .setMapper(() -> table.getColumns().isEmpty() ? 0.0 : table.size() * table.getRowsHeight()) .addSources(table.getColumns(), table.columnsSizeProperty()) .addSources(table.sizeProperty(), table.rowsHeightProperty()) .get() ); viewportPosition.bind(ObjectBindingBuilder.build() .setMapper(() -> { double x = 0; double y = 0; IntegerRange rowsRange = rowsRange(); IntegerRange columnsRange = columnsRange(); if (!Utils.INVALID_RANGE.equals(rowsRange)) { double cHeight = table.getRowsHeight(); IntegerRange rRangeToFirstVisible = IntegerRange.of(rowsRange.getMin(), firstRow()); double rPixelsToFirst = rRangeToFirstVisible.diff() * cHeight; double rVisibleAmount = table.getVPos() % cHeight; y = -(rPixelsToFirst + rVisibleAmount); } if (!Utils.INVALID_RANGE.equals(columnsRange)) { double cWidth = table.getColumnsSize().getWidth(); IntegerRange cRangeToFirstVisible = IntegerRange.of(columnsRange.getMin(), firstColumn()); double cPixelsToFirst = cRangeToFirstVisible.diff() * cWidth; double cVisibleAmount = table.getHPos() % cWidth; x = -(cPixelsToFirst + cVisibleAmount); } return Position.of(x, y); }) .addSources(table.layoutBoundsProperty()) .addSources(table.vPosProperty(), table.hPosProperty()) .addSources(table.rowsHeightProperty(), table.columnsSizeProperty()) .get() ); } /** * {@inheritDoc} *

* Given by {@code Math.floor(hPos / columnsWidth)}, clamped between 0 and the number of columns - 1. */ @Override public int firstColumn() { return NumberUtils.clamp( (int) Math.floor(table.getHPos() / table.getColumnsSize().getWidth()), 0, table.getColumns().size() - 1 ); } /** * {@inheritDoc} *

* Given by {@code Math.ceil(tableWidth / columnsWidth)}. 0 if the columns' width is also 0. */ @Override public int visibleColumns() { double width = table.getColumnsSize().getWidth(); return width > 0 ? (int) Math.ceil(table.getWidth() / width) : 0; } /** * {@inheritDoc} *

* Given by {@link #visibleColumns()} plus double the value of {@link VFXTable#columnsBufferSizeProperty()}, cannot * exceed the number of columns in the table. */ @Override public int totalColumns() { int visible = visibleColumns(); return visible == 0 ? 0 : Math.min(visible + table.getColumnsBufferSize().val() * 2, table.getColumns().size()); } /** * {@inheritDoc} *

* For the {@link ColumnsLayoutMode#FIXED} mode, all columns will have the same width specified by * {@link VFXTable#columnsSizeProperty()}, except for the last one that needs to take all the available * space (if any left). The last column's width is given by * {@code Math.max(fixedWidth, tableWidth - ((nColumns - 1) * fixedWidth))}. */ @Override public double getColumnWidth(VFXTableColumn column) { VFXTable table = getTable(); double width = table.getColumnsSize().getWidth(); if (!isLastColumn(column)) return width; return Math.max(width, table.getWidth() - ((table.getColumns().size() - 1) * width)); } /** * {@inheritDoc} *

* Given by {@code columnsWidth * layoutIndex}. */ @Override public double getColumnPos(int layoutIdx, VFXTableColumn column) { return table.getColumnsSize().getWidth() * layoutIdx; } /** * {@inheritDoc} *

* Positions the column at {@code X: getColumnPos(index, column)} and {@code Y: 0}. *

* Sizes the column to {@code W: getColumnWidth(column)} and {@code H: columnsHeight} * * @return always true */ @Override public boolean layoutColumn(int layoutIdx, VFXTableColumn column) { Size size = getTable().getColumnsSize(); double x = getColumnPos(layoutIdx, column); double w = getColumnWidth(column); double h = size.getHeight(); column.resizeRelocate(x, 0, w, h); return true; } /** * {@inheritDoc} *

* The width and x position values are the exact same used by the {@link #lastColumn()} method. * So both {@link #getColumnPos(int, VFXTableColumn)} and {@link #getColumnWidth(VFXTableColumn)} are used * to find the cell's x and w respectively. However, before doing so, we must convert the given layout index to * the respective column index and then extract the column (since the aforementioned methods need the column as a parameter). * The conversion is done by this simple formula: {@code columnsRange.getMin() + layoutIdx}. *

* The y position will be 0 and the height will be {@code rowsHeight}. * * @return always true */ @Override public boolean layoutCell(int layoutIdx, VFXTableCell cell) { VFXTable table = getTable(); IntegerRange columnsRange = columnsRange(); int colIndex = columnsRange.getMin() + layoutIdx; VFXTableColumn> column = table.getColumns().get(colIndex); Node node = cell.toNode(); double x = getColumnPos(layoutIdx, column); double w = getColumnWidth(column); double h = table.getRowsHeight(); cell.beforeLayout(); node.resizeRelocate(x, 0, w, h); cell.afterLayout(); return true; } /** * This method is a no-op as the operation is not possible in {@link ColumnsLayoutMode#FIXED}. */ @Override public void autosizeColumn(VFXTableColumn column) {/*NO-OP*/} /** * In {@link ColumnsLayoutMode#FIXED} this can be still used by setting the {@link VFXTable#columnsSizeProperty()} * rather than the width of each column. *

* If the current state is {@link VFXTableState#INVALID} then exits immediately. *

* If the last column in range ({@link #columnsRange()}), has its skin still {@code null}, then we assume that * every other column is in the same situation. In such case, we need to 'delay' the operation and wait for the * skin to be created, so that we can compute the columns' width. *

* The first pass is to get the widest column by iterating over them, computing the width with * {@link VFXTableColumn#computePrefWidth(double)} and keeping the maximum value found. *

* If the state is empty (no rows), the computation ends and the {@link VFXTable#columnsSizeProperty()} is set to: * {@code Math.max(fixedW, foundMax + extra)}, where 'fixedW' is the current width specified by the * property itself. *

* The second pass is to get the widest cell among the ones in the viewport by using * {@link VFXTableRow#getWidthOf(VFXTableColumn, boolean)}. The {@code forceLayout} flag is {@code true} if * this operation was 'delayed' before for the aforementioned reasons. *

* Finally, the {@link VFXTable#columnsSizeProperty()} is set to: * {@code Math.max(Math.max(fixedW, maxColumnsW + extra), maxCellsW + extra)}, where 'fixedW' is the current width * specified by the property itself. * * @see VFXTable#extraAutosizeWidthProperty() */ @Override public void autosizeColumns() { VFXTableState state = table.getState(); if (state == VFXTableState.INVALID) return; ObservableList>> columns = table.getColumns(); // It may happen that the columns still have a null skin // In such case, we must delay the autosize while also ensuring that layout infos are available // by forcing the computation of CSS. // // Check this by getting the last column in range // If the first's skin is still null, then most probably every other column is in the same situation IntegerRange columnsRange = columnsRange(); VFXTableColumn> column = columns.get(columnsRange.getMax()); if (column.getSkin() == null) { When.onInvalidated(column.skinProperty()) .condition(Objects::nonNull) .then(v -> { forceLayout = true; table.applyCss(); autosizeColumns(); }) .oneShot() .listen(); return; } double extra = table.getExtraAutosizeWidth(); double fixedW = table.getColumnsSize().getWidth(); double maxColumnsW = columns.stream() .mapToDouble(c -> c.computePrefWidth(-1)) .max() .orElse(-1); if (state.isEmpty()) { table.setColumnsWidth(Math.max(fixedW, maxColumnsW + extra)); return; } double maxCellsW = columns.stream() .mapToDouble(c -> state.getRowsByIndex().values() .stream() .mapToDouble(r -> r.getWidthOf(c, forceLayout)) .max() .orElse(-1.0) ) .max() .orElse(-1.0); table.setColumnsWidth(Math.max(Math.max(fixedW, maxColumnsW + extra), maxCellsW + extra)); forceLayout = false; } /** * @return the theoretical number of cells present in the viewport. It's given by {@code visibleRows * visibleColumns}, * which means that it does not take into account {@code null} cells or anything else */ @Override public int visibleCells() { int nColumns = visibleColumns(); int nRows = visibleRows(); return nColumns * nRows; } /** * {@inheritDoc} *

* To check whether the given column is visible this uses its index and the current state's columns range to * call {@link IntegerRange#inRangeOf(int, IntegerRange)}. *

* The index is retrieved with {@link VFXTable#indexOf(VFXTableColumn)}. */ @Override public boolean isInViewport(VFXTableColumn column) { if (column.getTable() == null || column.getScene() == null || column.getParent() == null) return false; VFXTableState state = table.getState(); if (state == VFXTableState.INVALID) return false; int index = table.indexOf(column); return IntegerRange.inRangeOf(index, state.getColumnsRange()); } @Override public void scrollToIndex(Orientation orientation, int index) { if (orientation == Orientation.HORIZONTAL) { table.setHPos(table.getColumnsSize().getWidth() * index); } else { table.setVPos(table.getRowsHeight() * index); } } } /** * Concrete implementation of {@link AbstractHelper} for {@link ColumnsLayoutMode#VARIABLE}. * Here the range of rows and columns to display, as well as the viewport position, * the virtual max x and y properties are defined as follows: *

- the columns range is always given by the number of columns in the table - 1. If there are no column, then it * will be {@link Utils#INVALID_RANGE}. * The computation depends only on the columns' list. *

- the rows range is given by the {@link #firstRow()} element minus the buffer size {@link VFXTable#rowsBufferSizeProperty()}, * (cannot be negative) and the sum between this start index and the total number of needed rows given by {@link #totalRows()}. * It may happen that the number of indexes given by the range {@code end - start + 1} is * lesser than the number of rows we need. In such cases, the range start is corrected to be * {@code end - needed + 1}. A typical situation for this is when the table's vertical position reaches the max scroll. * If the viewport's height is 0 or the number of needed rows is 0, then the range will be {@link Utils#INVALID_RANGE}. * The computation has the following dependencies: the table's height, the column's size (because it also specifies the * header height, which influences the viewport's height), the vertical position, the rows buffer size, the rows' height * and the items' list size. *

- the viewport's position, a computation that is at the core of virtual scrolling. The viewport, which contains * the columns and the cells (even though the table's viewport is a bit more complex), is not supposed to scroll by insane * numbers of pixels both for performance reasons and because it is not necessary. * For the vertical positions, first we get the range of rows to display and the rows' height. * We compute the range to the first visible row, which is given by {@code IntegerRange.of(range.getMin(), first())}, * in other words we limit the 'complete' range to the start buffer including the first row after the buffer. * The number of indexes in the newfound range (given by {@link IntegerRange#diff()}) is multiplied by the rows' height, * this way we are finding the number of pixels to the first visible row, {@code pixelsToFirst}. * At this point, we are missing only one last piece of information: how much of the first row do we actually see? * We call this amount {@code visibleAmountFirst} and it's given by {@code vPos % size}. * Finally, the viewport's vertical position is given by this formula {@code -(pixelsToFirst + visibleAmountFirst)}. * Since this layout mode disables virtualization along the x-axis, the horizontal position is simply given by * {@code -hPos}. * If a range is equal to {@link Utils#INVALID_RANGE}, the respective position will be 0! * While it's true that the calculations are more complex and 'needy', it's important to note that this approach * allows avoiding 'hacks' to correctly lay out the cells in the viewport. No need for special offsets at the top * or bottom anymore. * The viewport's position computation has the following dependencies: the vertical and horizontal positions, * the rows' height and the columns' size. *

- the virtual max y property, which gives the total number of pixels on the y-axis. * Virtual means that it's not the actual size of the container, rather the size it would have if it was not virtualized. * The value is given by the number of rows multiplied by the rows' height. The computation depends on the columns' list, * the columns' size (because the viewport height also depends on the height specified by the columns' size property), * the table's size (number of items), and the rows' height. *

- the virtual max x property, which gives the total number of pixels on the x-axis. Since virtualization is * disabled in this axis, the value is simply the sum of all the table's columns (to be precise, the vaòue is given by * the cache-binding, see below). *

* Performance Optimizations * Disabling virtualization on a complex 2D structure like {@link VFXTable} is indeed dangerous. While it's safe to * assume that the number of columns is pretty much never going to be a big enough number to cause performance issues, * it's also true that: 1) we can't be sure on how many columns are actually too many; 2) since it is a 2D structure * having {@code n} more columns in the viewport does not mean that we will have {@code n} more nodes in the scene graph. * Rather, we can affirm that at best we will have {@code n} more nodes, but keep in mind that for each column * there are going to be a number of cells equal to the number of rows. *

* I believe it's worth to optimize the helper as much as possible to mitigate the issue. So, for this reason, this * helper makes use of special cache {@link ColumnsLayoutCache} which aims to improve layout operations by avoiding * re-computations when they are not needed. For example, if we compute the width and the position of column, * then we don't need to re-compute it again when laying out corresponding cells, that would be a waste! *

* The cache can compute columns' widths, their x positions and even check whether they are visible in the viewport. * I won't go into many details here on how the cache exactly works, read its docs to know more about it, just know that * after the first computation, values will be memorized. Further requests will be as fast as a simple 'getter' method. * The cache is also responsible for automatically invalidate the cached values when certain conditions change. *

* For {@link ColumnsLayoutCache} to work properly, this helper defines a series of methods which are actually * responsible for the computations. I decided to keep such methods here rather than defining them in the cache mainly * for two reasons: 1) I strongly believe such operations are the helper's responsibility; 2) By doing so we generalize * the cache class, making it flexible to use, and suitable for more use-cases. These methods are: * {@link #computeColumnWidth(VFXTableColumn, boolean)}, {@link #computeColumnPos(int, double)}, {@link #computeVisibility(VFXTableColumn)}. */ @SuppressWarnings("JavadocReference") // I don't know why since the method is public class VariableTableHelper extends AbstractHelper { private ColumnsLayoutCache layoutCache; private boolean forceLayout = false; private boolean forceAll = false; public VariableTableHelper(VFXTable table) { super(table); } /** * This is used by the {@link ColumnsLayoutCache} to compute the width of the given column. * The value is given by {@code Math.max(minW, prefW)}, where: *

- {@code minW} is given by {@link VFXTable#columnsSizeProperty()} *

- {@code prefW} is given by {@link VFXTableColumn#prefWidth(double)} *

* If there's only one column in the table, then the returned value is the maximum between the above formula and * the table's width. *

* If the column is the last one in the list, then the final value is given by * {@code Math.max(Math.max(minW, prefW), tableW - partialW)}, where {@code partialW} is given by * {@link ColumnsLayoutCache#getPartialWidth()}. */ protected double computeColumnWidth(VFXTableColumn column, boolean isLast) { double minW = table.getColumnsSize().getWidth(); double prefW = Math.max(column.prefWidth(-1), minW); if (table.getColumns().size() == 1) return Math.max(prefW, table.getWidth()); if (!isLast) return column.snapSizeX(prefW); double partialW = layoutCache.getPartialWidth(); return column.snapSizeX(Math.max(prefW, table.getWidth() - partialW)); } /** * This is used by the {@link ColumnsLayoutCache} to compute the x position of a given column (by its index) * and the last know position. For example, let's suppose I want to lay out the column at index 1, and the column * at index 0 (the previous one) is 100px wider. The {@code prevPos} parameter passed to this method will be 100px. * This makes the computation for index 1 easy, as it is simply is the sum {@code prevPos + col1Width}. */ protected double computeColumnPos(int index, double prevPos) { VFXTableColumn> column = table.getColumns().get(index); return column.snapPositionX(prevPos + layoutCache.getColumnWidth(column)); } /** * This is used by the {@link ColumnsLayoutCache} to compute the visibility of a given column (and all its * related cells ofc). There are a lot of requirements for this check but the concept is quite simple. *

* Before getting all the dependencies, we ensure that the column's table instance is not {@code null}, that its index * is not negative, that the column is in the scene graph ({@code null} check on the column's Scene and Parent). * If any of these conditions fail {@code false} is returned. *

* The idea is to use something similar to {@link Bounds#intersects(Bounds)} but only for the width and x position. * First we compute the viewport bounds which are given by {@code [hPos, hPos + tableWidth]}, then we get the * column's position and width by using {@link ColumnsLayoutCache#getColumnPos(int)} and {@link ColumnsLayoutCache#getColumnWidth(VFXTableColumn)}. *

* The result is given by this formula: {@code (columnX + columnWidth >= vBounds.getMin()) && (columnX <= vBounds.getMax())} */ protected boolean computeVisibility(VFXTableColumn column) { VFXTable table = column.getTable(); int index = column.getIndex(); if (table == null || index < 0 || column.getScene() == null || column.getParent() == null ) return false; try { double tableW = table.getWidth(); double hPos = table.getHPos(); DoubleRange viewBounds = DoubleRange.of(hPos, hPos + tableW); double columnX = layoutCache.getColumnPos(index); double columnW = layoutCache.getColumnWidth(column); return (columnX + columnW >= viewBounds.getMin()) && (columnX <= viewBounds.getMax()); } catch (Exception ex) { return false; } } @Override protected void initBindings() { super.initBindings(); // Initialize layout cache layoutCache = new ColumnsLayoutCache<>(table) .setWidthFunction(this::computeColumnWidth) .setPositionFunction(this::computeColumnPos) .setVisibilityFunction(this::computeVisibility) .init(); // Initialize bindings columnsRange.bind(ObjectBindingBuilder.build() .setMapper(() -> { ObservableList>> columns = table.getColumns(); if (columns.isEmpty()) return Utils.INVALID_RANGE; return IntegerRange.of(0, columns.size() - 1); }) .addSources(table.getColumns()) .get() ); rowsRange.bind(ObjectBindingBuilder.build() .setMapper(() -> { if (getViewportHeight() <= 0) return Utils.INVALID_RANGE; int needed = totalRows(); if (needed == 0) return Utils.INVALID_RANGE; int start = Math.max(0, firstRow() - table.getRowsBufferSize().val()); int end = Math.min(table.size() - 1, start + needed - 1); if (end - start + 1 < needed) start = Math.max(0, end - needed + 1); return IntegerRange.of(start, end); }) .addSources(table.heightProperty(), table.columnsSizeProperty()) .addSources(table.vPosProperty()) .addSources(table.rowsBufferSizeProperty()) .addSources(table.sizeProperty(), table.rowsHeightProperty()) .get() ); virtualMaxX.bind(layoutCache); virtualMaxY.bind(DoubleBindingBuilder.build() .setMapper(() -> table.getColumns().isEmpty() ? 0.0 : table.size() * table.getRowsHeight()) .addSources(table.getColumns(), table.columnsSizeProperty()) .addSources(table.sizeProperty(), table.rowsHeightProperty()) .get() ); viewportPosition.bind(ObjectBindingBuilder.build() .setMapper(() -> { double x = 0; double y = 0; IntegerRange rowsRange = rowsRange(); IntegerRange columnsRange = columnsRange(); if (!Utils.INVALID_RANGE.equals(rowsRange)) { double cHeight = table.getRowsHeight(); IntegerRange rRangeToFirstVisible = IntegerRange.of(rowsRange.getMin(), firstRow()); double rPixelsToFirst = rRangeToFirstVisible.diff() * cHeight; double rVisibleAmount = table.getVPos() % cHeight; y = -(rPixelsToFirst + rVisibleAmount); } if (!Utils.INVALID_RANGE.equals(columnsRange)) { x = -table.getHPos(); } return Position.of(x, y); }) .addSources(table.layoutBoundsProperty()) .addSources(table.vPosProperty(), table.hPosProperty()) .addSources(table.rowsHeightProperty(), table.columnsSizeProperty()) .get() ); } /** * Always 0. */ @Override public int firstColumn() { return 0; } /** * Always the size of {@link VFXTable#getColumns()}. */ @Override public int visibleColumns() { return table.getColumns().size(); } /** * Always the size of {@link VFXTable#getColumns()}. */ @Override public int totalColumns() { return table.getColumns().size(); } /** * Delegates to {@link ColumnsLayoutCache#getColumnWidth(VFXTableColumn)}. */ @Override public double getColumnWidth(VFXTableColumn column) { return layoutCache.getColumnWidth(column); } /** * Delegates to {@link ColumnsLayoutCache#getColumnPos(int)}. */ @Override public double getColumnPos(int layoutIdx, VFXTableColumn column) { return layoutCache.getColumnPos(layoutIdx); } /** * Delegates to {@link ColumnsLayoutCache#isInViewport(VFXTableColumn)}. */ @Override public boolean isInViewport(VFXTableColumn column) { return layoutCache.isInViewport(column); } /** * {@inheritDoc} *

* In {@link ColumnsLayoutMode#VARIABLE} the {@code layoutIndex} is always the same as the column's index. *

* Positions the column at {@code X: getColumnPos(index, column)} and {@code Y: 0}. *

* Sizes the column to {@code W: getColumnWidth(column)} and {@code H: columnsHeight} *

* Additionally, this method makes use of the 'inViewport' functionality to hide and not lay out columns when they * are not visible in the viewport. The layout operation is also avoided in case the column is visible and both * its x positions and width are already good to go. * * @return {@code false} if the column was hidden or no layout operation was run, {@code true} otherwise */ @Override public boolean layoutColumn(int layoutIdx, VFXTableColumn column) { if (!isInViewport(column)) { column.setVisible(false); return false; } Size size = getTable().getColumnsSize(); double x = getColumnPos(layoutIdx, column); double w = getColumnWidth(column); double h = size.getHeight(); if (column.isVisible() && column.getLayoutX() == x && column.getWidth() == w) return false; column.resizeRelocate(x, 0, w, h); column.setVisible(true); return true; } /** * {@inheritDoc} *

* In {@link ColumnsLayoutMode#VARIABLE} the {@code layoutIndex} is always the same as the column's index. *

* The layout logic is the exact same as described here {@link #layoutColumn(int, VFXTableColumn)}. *

* Here's where the {@link ColumnsLayoutCache} shines. Since cells are laid out the exact same way as * the corresponding column, the width, positions and visibility computations are already done when invoking * {@link #layoutColumn(int, VFXTableColumn)}. Obviously, to do so, we first need to get the cell's corresponding * column from {@link VFXTable#getColumns()} by the given index. *

* Note: the pre/post layout hooks defined by {@link VFXCell} are called even if the cell will only be set to * hidden. This allows implementations to perform actions depending on the visibility state without relying on * listeners. */ @Override public boolean layoutCell(int layoutIdx, VFXTableCell cell) { ObservableList>> columns = table.getColumns(); VFXTableColumn> column = columns.get(layoutIdx); Node node = cell.toNode(); cell.beforeLayout(); if (!isInViewport(column)) { node.setVisible(false); cell.afterLayout(); return false; } double w = getColumnWidth(column); double h = getTable().getRowsHeight(); double x = getColumnPos(layoutIdx, column); if (node.isVisible() && node.getLayoutX() == x && node.getLayoutBounds().getWidth() == w) return false; node.resizeRelocate(x, 0, w, h); node.setVisible(true); cell.afterLayout(); return true; } /** * If the current state is {@link VFXTableState#INVALID} then exits immediately. *

* If the given column's skin is still {@code null}, then we must 'delay' the operation and wait for the skin to * be created, so that we can compute the column's width. *

* The first pass is to get the column's ideal width which is given by {@code Math.max(minW, prefW) + extra} where: *

- {@code minW} is specified by {@link VFXTable#columnsSizeProperty()} *

- {@code prefW} is obtained by calling {@link VFXTableColumn#computePrefWidth(double)} *

- {@code extra} is an extra number of pixels added to the final value specified by {@link VFXTable#extraAutosizeWidthProperty()} *

* If the state is empty (no rows), the computation ends and the column's width is set to the value found by the * above formula. *

* The second pass is to get the widest cell among the ones in the viewport by using * {@link VFXTableRow#getWidthOf(VFXTableColumn, boolean)}. The {@code forceLayout} flag is {@code true} if * this operation was 'delayed' before for the aforementioned reasons. *

* Finally, the column's width is set to: {@code Math.max(Math.max(minW, prefW), maxCellsWidth) + extra}. *

* Note: the columns are resized using the method {@link VFXTableColumn#resize(double)}. * * @see VFXTableColumn * @see VFXTable#extraAutosizeWidthProperty() */ @Override public void autosizeColumn(VFXTableColumn column) { VFXTableState state = table.getState(); if (state == VFXTableState.INVALID) return; // It may happen that the column still has a null skin // In such cases, we must delay the autosize while also ensuring that layout infos are available // by forcing the computation of CSS. if (column.getSkin() == null) { new OnInvalidated<>(column.skinProperty()) { static boolean forced = false; { condition(Objects::nonNull); then(v -> { if (!forced) { forced = true; forceLayout = true; table.applyCss(); } autosizeColumn(column); }); oneShot(); listen(); } }; return; } double extra = table.getExtraAutosizeWidth(); double minW = table.getColumnsSize().getWidth(); double prefW = column.computePrefWidth(-1); if (state.isEmpty()) { column.resize(Math.max(minW, prefW) + extra); return; } double maxCellsW = state.getRowsByIndex().values().stream() .mapToDouble(r -> r.getWidthOf(column, forceLayout)) .max() .orElse(-1.0); column.resize(Math.max(Math.max(minW, prefW), maxCellsW) + extra); if (!forceAll) forceLayout = false; } /** * This simply calls {@link #autosizeColumn(VFXTableColumn)} on all the table's columns. */ @Override public void autosizeColumns() { VFXTableState state = table.getState(); if (state == VFXTableState.INVALID) return; forceAll = true; table.getColumns().forEach(this::autosizeColumn); forceLayout = false; } /** * @return the number of cells for which the corresponding column is visible in the viewport * @see #isInViewport(VFXTableColumn) */ @Override public int visibleCells() { VFXTableState state = table.getState(); if (state.isEmpty()) return 0; IntegerRange cRange = state.getColumnsRange(); int nRows = state.getRowsRange().diff() + 1; int nCellsPerRow = (int) IntStream.rangeClosed(cRange.getMin(), cRange.getMax()) .mapToObj(table.getColumns()::get) .filter(this::isInViewport) .count(); return nRows * nCellsPerRow; } @Override public void scrollToIndex(Orientation orientation, int index) { if (orientation == Orientation.HORIZONTAL) { try { VFXTableColumn> column = table.getColumns().get(index); table.setHPos(getColumnPos(table.indexOf(column), column)); } catch (Exception ignored) {} } else { table.setVPos(table.getRowsHeight() * index); } } /** * {@inheritDoc} *

* Overridden here to also dispose the {@link ColumnsLayoutCache}. */ @Override public void dispose() { layoutCache.dispose(); layoutCache = null; super.dispose(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy