![JAR search and dependency download from the Maven repository](/logo.png)
io.github.palexdev.virtualizedfx.table.VFXTableRow Maven / Gradle / Ivy
/*
* 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.range.IntegerRange;
import io.github.palexdev.virtualizedfx.base.VFXStyleable;
import io.github.palexdev.virtualizedfx.cells.base.VFXCell;
import io.github.palexdev.virtualizedfx.cells.base.VFXTableCell;
import io.github.palexdev.virtualizedfx.table.defaults.VFXDefaultTableRow;
import io.github.palexdev.virtualizedfx.utils.IndexBiMap.RowsStateMap;
import io.github.palexdev.virtualizedfx.utils.Utils;
import io.github.palexdev.virtualizedfx.utils.VFXCellsCache;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import java.util.Collections;
import java.util.List;
import java.util.SequencedMap;
import java.util.function.Function;
/**
* Base class that defines common properties and behaviors for all rows to be used with {@link VFXTable}.
* The default style class is set to '.vfx-row'.
*
* This class has two peculiarities:
*
1) Extends {@link Region} because each row is actually a wrapping container for the table's cells
*
2) Implements {@link VFXCell} because most of the API is the same! Let's see the benefits:
* First and foremost this allows us to use the same cache class {@link VFXCellsCache} for the rows too
* which is super convenient. As for the {@link #updateIndex(int)} and {@link #updateItem(Object)} methods,
* well the row is no different from any other cell really. Each row wraps a cell for each table's column.
* All its cells 'operate' on the same item, they just process the object differently (generally speaking).
* So, a row which displays an item 'T' at index 17 in the list will have its index property set to 17, its item property
* as well as all of its cells' item property will be set to 'T'.
* As you can imagine, when we scroll in the viewport, make changes to the list (to cite only a few of the many changes types)
* we want to update the cells by either or both index and item. The table does so through rows
* (see {@link #updateColumns(IntegerRange, boolean)} for example).
*
* There are two more properties which we need to discuss:
* 1) Besides the index and item properties, each row also has the current range of columns the table needs to display,
* {@link #getColumnsRange()}. This piece of information is crucial for the row to build (and thus wrap) the correct type
* of cells. For example, let's say for a hypothetical User class I can see the 'First Name' column but not the 'Last Name'
* one. We want the row to ask the 'First Name' column to give us a cell for this field
* (it could be created or de-cached see {@link #getCell(int, VFXTableColumn, boolean)}).
* Additionally, we want to make sure that every other cell produced by columns that are not visible anymore to be removed
* from the children list (such cells can be disposed or cached, see {@link #saveCell(VFXTableColumn, VFXTableCell)}).
* Every time something relevant to the rows changes in the table, each row computes its new state and if cells change
* they call {@link #onCellsChanged()} to update the children list.
*
2) What do we mean by row's state? As probably already mentioned here {@link VFXTable}, this container is a bit special
* because we have two kinds of states. The global state which is a separate class {@link VFXTableState}, but then each row
* has its own "mini-state". The global state specifies which and how many items/rows should be present in the viewport,
* but it misses one crucial detail which is covered by the rows' state, which and how many cells to show. Exactly as it
* would be for a traditional state object, the cells keep a map of all their cells. When changes happen, we can easily
* compute which cells to keep using, which ones need to be disposed and whether new ones are needed.
* The type of map used is {@link RowsStateMap}.
*
* Aside from those core details, rows also have much more going on: they can copy the state of other rows
* (for optimization reasons, see {@link #copyState(VFXTableRow)}), the layout is completely 'manual' and will
* not respond to the traditional JavaFX 'triggers' but only to {@link VFXTable#needsViewportLayoutProperty()}.
*
* Note: because some of the base methods are actually quite complex to implement it's not recommended to use this
* as a base class for extension but rather {@link VFXDefaultTableRow}. Either way, always take a look at how original
* algorithms work before customizing!
*/
public abstract class VFXTableRow extends Region implements VFXCell, VFXStyleable {
//================================================================================
// Properties
//================================================================================
private final ReadOnlyObjectWrapper> table = new ReadOnlyObjectWrapper<>();
private final ReadOnlyIntegerWrapper index = new ReadOnlyIntegerWrapper(-1);
private final ReadOnlyObjectWrapper item = new ReadOnlyObjectWrapper<>();
protected IntegerRange columnsRange = Utils.INVALID_RANGE;
protected RowsStateMap> cells;
//================================================================================
// Constructors
//================================================================================
public VFXTableRow(T item) {
cells = new RowsStateMap<>();
updateItem(item);
initialize();
}
//================================================================================
// Abstract Methods
//===============================================================================
/**
* Implementations of this method should mainly react to two types of change in the table:
* 1) the visible columns changes, or more in general the state's columns range changes
*
2) changes in the columns' list that may not necessarily change the range. In particular, this type of change
* is distinguished by the 'change' parameter set to true. In this case, the row's state should always be computed as
* there is no information on how the list changed, a 'better safe than sorry' approach.
*/
protected abstract void updateColumns(IntegerRange columnsRange, boolean changed);
/**
* This method mainly exists to react to cell factory changes. When a column changes its factory, there is no need to
* re-compute each rows' state, rather we can just replace the cells for that column with new ones.
* Implementations should have a proper algorithm for the sake of performance.
*/
protected abstract boolean replaceCells(VFXTableColumn> column);
/**
* This method should be responsible for computing the ideal width of a cell given the corresponding column so that
* its content can be fully shown. This is important for the table's autosize feature to work properly!
*
* @param forceLayout it may happen that this garbage that is JavaFX to still have an incomplete scene graph, meaning
* for example that skins are not still available for some controls/cells, which means that we could
* fail in computing any size. This may happen even if the table's layout has been already computed
* at least once. Internal checks try to identify such occasions and may pass 'true' to this method.
* What implementations could do to get a correct value is to force the cells to compute their
* layout (as well as all of their children) by invoking {@link Node#applyCss()}.
* Note, however, that this is going to be a costly operation at init, but it's pretty much
* the only way.
*/
protected abstract double getWidthOf(VFXTableColumn column, boolean forceLayout);
//================================================================================
// Methods
//================================================================================
private void initialize() {
getStyleClass().setAll(defaultStyleClasses());
}
/**
* Sets this row's state to be exactly the same as the one given as parameter. This is mainly useful when the table
* changes its {@link VFXTable#rowFactoryProperty()} because while it's true that the old rows are to be disposed
* abd removed, the new ones would still have the same state of the old ones. In such occasions, it's a great
* optimization to just copy the state of the old corresponsig row rather than re-computing it from zero.
*
* To further detail what happens when this is called:
*
- the index is updated to be the same as the 'other'
*
- the columns range is copied over
*
- the cells' map is copied over and the instance in the 'other' row is set to {@link RowsStateMap#EMPTY}
*
- calls {@link VFXTableCell#updateRow(VFXTableRow)} on all the cells from the 'other' row
*
- finally calls {@link #onCellsChanged()}
*
* Last but not least, note that such operation is likely going to need a layout request, but it's not the rows'
* responsibility to do so.
*/
@SuppressWarnings("unchecked")
protected void copyState(VFXTableRow other) {
updateIndex(other.getIndex());
this.columnsRange = other.columnsRange;
this.cells = other.cells;
other.cells = RowsStateMap.EMPTY;
cells.getByIndex().values().forEach(c -> c.updateRow(this));
onCellsChanged();
// A layout request is also needed!
}
/**
* Clears the row's state without disposing it. This will cause all cells to be cached by {@link #saveAllCells()},
* the index set to -1, the item set to {@code null}, the columns range set to {@link Utils#INVALID_RANGE} and the children
* list to be cleared.
*/
protected void clear() {
saveAllCells();
setIndex(-1);
setItem(null);
columnsRange = Utils.INVALID_RANGE;
getChildren().clear();
}
/**
* This is crucial to call when the row's cells change. All cells are 'collected' as nodes by {@link #getCellsAsNodes()}
* and added to the row's children list.
*/
protected void onCellsChanged() {
getChildren().setAll(getCellsAsNodes());
}
/**
* This method is responsible for creating cells given the "parent" column (from which takes the cell factory),
* and its index. Before creating a new cell using the factory, this attempts to retrieve one from the column's
* cells' cache, and only if there are no cached cells, a new one is built. The cache usage is optional and can be
* avoided by passing false as the {@code useCache} parameter.
*
* In any case, the cell will be fully updated: {@link VFXTableCell#updateRow(VFXTableRow)}, {@link VFXTableCell#updateColumn(VFXTableColumn)},
* {@link VFXTableCell#updateItem(Object)} (if de-cached), and {@link VFXTableCell#updateIndex(int)}.
*/
protected VFXTableCell getCell(int index, VFXTableColumn> column, boolean useCache) {
T item = getItem();
VFXTableCell cell;
if (useCache && column.cacheSize() > 0) { // Try cache first
cell = column.cache().take();
cell.updateItem(item);
} else { // Create new otherwise
Function> cellFactory = column.getCellFactory();
if (cellFactory == null) return null; // Take into account null generators
cell = cellFactory.apply(item);
}
cell.updateRow(this);
cell.updateColumn(column);
cell.updateIndex(index);
return cell;
}
/**
* Asks the given column to save the given cell in its cache. Beware that this operation won't remove the cell
* from the state map and the children list; therefore, you must do it before calling this
*
* By convention, when this is called, the cell's row and column properties are reset to {@code null}.
* This is to clearly indicate that the cell is not in the viewport anymore.
*/
protected void saveCell(VFXTableColumn> column, VFXTableCell cell) {
column.cache().cache(cell);
cell.updateRow(null);
cell.updateColumn(null);
// A cell (or row) that is not in the viewport anymore should state it clearly
// This is the convention, therefore, set both to 'null'
}
/**
* Caches all the row's cells by iterating over the state map and calling {@link #saveCell(VFXTableColumn, VFXTableCell)}.
* The difference here is that the state map is also cleared at the end.
*
* Beware that this will not call {@link #onCellsChanged()}, therefore, if needed, you will have to do it afterward.
*/
@SuppressWarnings("unchecked")
protected boolean saveAllCells() {
if (cells.isEmpty()) return false;
cells.getByKey().forEach((c, idxs) -> {
for (Integer idx : idxs) {
saveCell((VFXTableColumn>) c, cells.get(idx));
}
});
cells.clear();
return true;
}
/**
* This core method is responsible for sizing and positioning the cells in the row.
* This is done by iterating over the columns range, getting every cell and, if not {@code null}, delegating the
* operation to {@link VFXTableHelper#layoutCell(int, VFXTableCell)}.
*
* This only defines the algorithm and is not automatically called by the row. Rather, it's the default table skin
* to call this on each row upon a layout request received from the {@link VFXTable#needsViewportLayoutProperty()}.
*
* Note that this implementation allows having columns that produce {@code null} cells.
*/
protected void layoutCells() {
// It's crucial to process the layout this way.
// Some columns may not be present in the map as the cell factory could be null or produce null cells.
// So, we have to skip such cases, but we still need to increment the 'i' counter to get the correct absolute position
VFXTable table = getTable();
if (table == null || !table.isNeedsViewportLayout()) return;
VFXTableHelper helper = table.getHelper();
int i = 0;
for (Integer idx : columnsRange) {
VFXTableCell cell = cells.get(idx);
if (cell != null) helper.layoutCell(i, cell);
i++;
}
}
//================================================================================
// Overridden Methods
//================================================================================
@Override
public Region toNode() {
return this;
}
@Override
public void updateIndex(int index) {
setIndex(index);
}
@Override
public void updateItem(T item) {
setItem(item);
}
@Override
public List defaultStyleClasses() {
return List.of("vfx-row");
}
/**
* Overridden to be a no-op. We manage the layout manually like real giga-chads.
*/
@Override
protected void layoutChildren() {}
/**
* Automatically called by the table's system when the row is not needed anymore. Most of the operations are performed
* by {@link #clear()}. In addition, the table's instance is set to {@code null}.
*/
@Override
public void dispose() {
clear();
setTable(null);
}
//================================================================================
// Getters/Setters
//================================================================================
public VFXTable getTable() {
return table.get();
}
/**
* Specifies the table's instance this row belongs to.
*/
public ReadOnlyObjectProperty> tableProperty() {
return table.getReadOnlyProperty();
}
protected void setTable(VFXTable table) {
this.table.set(table);
}
public int getIndex() {
return index.get();
}
/**
* Specifies the index of the item displayed by the row and its cells.
*/
public ReadOnlyIntegerProperty indexProperty() {
return index.getReadOnlyProperty();
}
protected void setIndex(int index) {
this.index.set(index);
}
public T getItem() {
return item.get();
}
/**
* Specifies the object displayed by the row and its cells.
*/
public ReadOnlyObjectProperty itemProperty() {
return item.getReadOnlyProperty();
}
protected void setItem(T item) {
this.item.set(item);
}
/**
* The range of columns visible in the viewport. This should always be the same as the current {@link VFXTableState},
* and it's used to make the row always have the correct cells displayed (in accord to the visualized columns).
*/
public IntegerRange getColumnsRange() {
return columnsRange;
}
/**
* @return the row's cells as an unmodifiable {@link SequencedMap}, mapped by the row's {@link #indexProperty()}.
*/
public SequencedMap> getCellsUnmodifiable() {
return Collections.unmodifiableSequencedMap(cells.getByIndex());
}
/**
* @return the row's state map, which contains the cells both mapped by the row's index or the cell's "parent" column.
*/
protected RowsStateMap> getCells() {
return cells;
}
/**
* @return the row's cells as a {@link SequencedMap}, mapped by the row's {@link #indexProperty()}.
*/
protected SequencedMap> getCellsByIndex() {
return cells.getByIndex();
}
/**
* Converts and collects all the cells from the row's state map to JavaFX nodes by using {@link VFXCell#toNode()}.
*/
public List getCellsAsNodes() {
return getCellsByIndex().values().stream()
.map(VFXCell::toNode)
.toList();
}
}