io.github.palexdev.virtualizedfx.grid.GridRow Maven / Gradle / Ivy
Show all versions of virtualizedfx Show documentation
/*
* Copyright (C) 2022 Parisi Alessandro
* 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.grid;
import io.github.palexdev.mfxcore.base.beans.Size;
import io.github.palexdev.mfxcore.base.beans.range.IntegerRange;
import io.github.palexdev.mfxcore.collections.ObservableGrid;
import io.github.palexdev.mfxcore.enums.GridChangeType;
import io.github.palexdev.mfxcore.utils.GridUtils;
import io.github.palexdev.virtualizedfx.cell.GridCell;
import javafx.scene.Node;
import java.util.*;
import java.util.stream.Collectors;
/**
* This is a helper class to support {@link GridState}.
*
* The major issue in managing {@link VirtualGrid} is that we are dealing with a 2D data structure, and a type of cell
* {@link GridCell}, that works on both linear indexes and coordinated. All of this makes the algorithms a lot more complex
* and easy to screw up.
*
* {@code GridRow}'s goal is to simplify this mess by having one specific task: managing the columns for one specific row
* of items. So, in the end with {@code GridRow} we have to manage a 1D structure which is the map containing the cells
* representing the columns of a single row.
*
* {@code GridRow}s contain information such as:
* - the row's index
*
- the range of columns for the row
*
- the actual cells that will be displayed in the viewport, kept in a map as: columnIndex -> Cell
*
- a map containing the horizontal positions of the cells, so that in some cases these can be reused avoiding
* a part of the {@link #layoutCells(double, boolean)} computation
*
- the vertical position at which the row (every cell) will be positioned
*/
public class GridRow> {
//================================================================================
// Properties
//================================================================================
private final VirtualGrid grid;
private int index;
private IntegerRange columns;
private final Map cells = new TreeMap<>();
private final Set positions = new TreeSet<>();
private double position;
private boolean reusablePositions = false;
private boolean visible = true;
//================================================================================
// Constructors
//================================================================================
public GridRow(VirtualGrid grid, int index, IntegerRange columns) {
this.grid = grid;
this.index = index;
this.columns = columns;
}
public static > GridRow of(VirtualGrid virtualGrid, int index, IntegerRange columns) {
return new GridRow<>(virtualGrid, index, columns);
}
//================================================================================
// Methods
//================================================================================
// Init
/**
* Initializes {@code GridRow} by creating the needed amount of cells given by the {@link #getColumns()} range.
*
* If the row has already been initialized before all the cells are disposed, cleared and re-created.
*/
public GridRow init() {
if (index < 0 || IntegerRange.of(-1).equals(columns)) return this;
clear();
for (Integer column : columns) {
int linear = toLinear(index, column);
T item = grid.getItems().getElement(linear);
C cell = grid.getCellFactory().apply(item);
cell.updateIndex(linear);
cell.updateCoordinates(index, column);
cells.put(column, cell);
}
return this;
}
/**
* This is responsible for supplying of removing cells according to the new given columns range.
*
* This is used by {@link GridManager#init()}. If the given range is equal to the current one no operation
* is done.
*/
protected void onInit(IntegerRange columns) {
if (this.columns.equals(columns)) return;
Map tmp = new HashMap<>();
Set range = IntegerRange.expandRangeToSet(columns);
int targetSize = columns.diff() + 1;
for (Integer column : columns) {
if (cells.containsKey(column)) {
tmp.put(column, cells.remove(column));
range.remove(column);
}
}
Deque reusable = new ArrayDeque<>(cells.keySet());
Deque remaining = new ArrayDeque<>(range);
while (tmp.size() != targetSize) {
int rIndex = remaining.removeFirst();
Integer oIndex = reusable.poll();
int linear = toLinear(index, rIndex);
T item = grid.getItems().getElement(linear);
C cell;
if (oIndex != null) {
cell = cells.remove(rIndex);
cell.updateItem(item);
} else {
cell = grid.getCellFactory().apply(item);
}
cell.updateIndex(linear);
cell.updateCoordinates(index, rIndex);
tmp.put(rIndex, cell);
}
clear();
cells.putAll(tmp);
this.columns = columns;
}
// Update & Scroll
/**
* This is responsible for updating the cells when the row's index must change to the given one.
* All cells are fully updated (both index and item).
*/
protected void updateIndex(int index) {
if (this.index == index) return;
for (Map.Entry e : cells.entrySet()) {
int column = e.getKey();
int linear = toLinear(index, column);
C cell = e.getValue();
T item = grid.getItems().getElement(linear);
cell.updateItem(item);
cell.updateIndex(linear);
cell.updateCoordinates(index, column);
}
this.index = index;
reusablePositions = true;
}
/**
* This is responsible for updating the {@code GridRow} when the viewport scrolls horizontally.
*/
protected void onScroll(IntegerRange columns) {
Map tmp = new HashMap<>();
Set range = IntegerRange.expandRangeToSet(columns);
int targetSize = columns.diff() + 1;
for (Integer column : columns) {
if (cells.containsKey(column)) {
int linear = toLinear(index, column);
C cell = cells.remove(column);
cell.updateIndex(linear);
cell.updateCoordinates(index, column);
tmp.put(column, cell);
range.remove(column);
}
}
Deque reusable = new ArrayDeque<>(cells.keySet());
Deque remaining = new ArrayDeque<>(range);
while (tmp.size() != targetSize) {
int rIndex = remaining.removeFirst();
int oIndex = reusable.removeFirst();
int linear = toLinear(index, rIndex);
T item = grid.getItems().getElement(linear);
C cell = cells.remove(oIndex);
cell.updateItem(item);
cell.updateIndex(linear);
cell.updateCoordinates(index, rIndex);
tmp.put(rIndex, cell);
}
clear();
cells.putAll(tmp);
this.columns = columns;
}
// Changes
/**
* This is responsible for updating the {@code GridRow} when the row has been replaced.
* All the cells keep their indexes but their item must be updated.
*/
protected void onReplace() {
cells.forEach((i, c) -> {
int linear = toLinear(index, i);
T item = grid.getItems().getElement(linear);
c.updateItem(item);
});
reusablePositions = true;
}
/**
* This is responsible for updating only the cell at the given column index (if present in the map)
* with the given item.
*/
protected void onReplace(int column, T item) {
Optional.ofNullable(cells.get(column))
.ifPresent(cell -> cell.updateItem(item));
reusablePositions = true;
}
/**
* Calls {@link #onReplace(int, Object)}, since it is a diagonal replacement
* the column index is the same as the row's index.
*/
protected void onDiagReplace(T item) {
onReplace(index, item);
}
/**
* Calls {@link #partialUpdate(int)}.
*/
protected void onRowAdd(int newIndex) {
partialUpdate(newIndex);
}
/**
* Calls {@link #partialUpdate(int)}.
*/
protected void onRowRemove(int newIndex) {
partialUpdate(newIndex);
}
/**
* This is responsible for correctly updating the {@code GridRow} when a {@link GridChangeType#ADD_COLUMN} change
* occurs in the grid's data structure.
*
* The algorithm is very similar to the one used for {@link GridChangeType#ADD_ROW},
* described here {@link GridState#change(ObservableGrid.Change)}.
*/
protected void onColumnAdd(int column, IntegerRange columns) {
Map processed = new HashMap<>();
Set available = new HashSet<>(cells.keySet());
int targetSize = columns.diff() + 1;
// Before change index
for (int i = columns.getMin(); i < column; i++) {
C cell = cells.remove(i);
if (index != 0) {
int linear = toLinear(index, i);
cell.updateIndex(linear);
}
processed.put(i, cell);
available.remove(i);
}
// After change index
Iterator> it = cells.entrySet().iterator();
while (it.hasNext()) {
Map.Entry next = it.next();
int cIndex = next.getKey();
int newIndex = cIndex + 1;
if (!IntegerRange.inRangeOf(newIndex, columns) || (newIndex >= columns.getMin() && newIndex < column)) {
continue;
}
int lIndex = (index == 0) ? newIndex : toLinear(index, newIndex);
C cell = next.getValue();
cell.updateIndex(lIndex);
cell.updateCoordinates(index, newIndex);
processed.put(newIndex, cell);
available.remove(cIndex);
it.remove();
}
// Remaining
if (processed.size() != targetSize) {
Integer oIndex = new ArrayDeque<>(available).poll();
int lIndex = (index == 0) ? column : toLinear(index, column);
T item = grid.getItems().getElement(lIndex);
C cell;
if (oIndex != null) {
cell = cells.remove(oIndex);
cell.updateItem(item);
} else {
cell = grid.getCellFactory().apply(item);
}
cell.updateIndex(lIndex);
cell.updateCoordinates(index, column);
processed.put(column, cell);
}
clear();
cells.putAll(processed);
this.columns = columns;
}
/**
* This is responsible for correctly updating the {@code GridRow} when a {@link GridChangeType#REMOVE_COLUMN} change
* occurs in the grid's data structure.
*
* The algorithm is very similar to the one used for {@link GridChangeType#REMOVE_ROW},
* described here {@link GridState#change(ObservableGrid.Change)}.
*/
protected void onColumnRemove(int column, IntegerRange columns) {
Map processed = new HashMap<>();
Set available = new HashSet<>(cells.keySet());
Set rangeSet = IntegerRange.expandRangeToSet(columns);
// Valid
int start = Math.max(this.columns.getMin(), columns.getMin());
int end = Math.min(columns.getMax(), column);
for (int i = start; i < end; i++) {
C cell = cells.remove(i);
if (index != 0) {
int linear = toLinear(index, i);
cell.updateIndex(linear);
}
processed.put(i, cell);
available.remove(i);
rangeSet.remove(i);
}
// After change index
Iterator> it = cells.entrySet().iterator();
while (it.hasNext()) {
Map.Entry next = it.next();
int cIndex = next.getKey();
int newIndex = cIndex - 1;
if (!IntegerRange.inRangeOf(newIndex, columns) || (newIndex >= start && newIndex < end)) {
continue;
}
int lIndex = (index == 0) ? newIndex : toLinear(index, newIndex);
C cell = next.getValue();
cell.updateIndex(lIndex);
cell.updateCoordinates(index, newIndex);
processed.put(newIndex, cell);
available.remove(cIndex);
rangeSet.remove(newIndex);
it.remove();
}
// Remaining
if (!rangeSet.isEmpty()) {
Integer oIndex = new ArrayDeque<>(available).poll();
int nIndex = new ArrayDeque<>(rangeSet).removeFirst();
int lIndex = (index == 0) ? nIndex : toLinear(index, nIndex);
T item = grid.getItems().getElement(lIndex);
C cell;
if (oIndex != null) {
cell = cells.remove(oIndex);
cell.updateItem(item);
} else {
cell = grid.getCellFactory().apply(item);
}
cell.updateIndex(lIndex);
cell.updateCoordinates(index, nIndex);
processed.put(nIndex, cell);
}
clear();
cells.putAll(processed);
this.columns = columns;
}
/**
* This is responsible for partially updating all the cells in the row. Only the indexes of the cell
* will be updated as the item is expected to be valid.
*/
private void partialUpdate(int newIndex) {
cells.forEach((i, c) -> {
int linear = toLinear(newIndex, i);
c.updateIndex(linear);
c.updateCoordinates(newIndex, i);
});
this.index = newIndex;
reusablePositions = true;
}
// Layout
/**
* This core method is called by {@link GridState#layoutRows()} and it's responsible for laying out all the cells in
* the {@code GridRow}. Two information are required, the vertical position of the row in the viewport and whether
* we are in an exceptional case as described here {@link GridState#layoutRows()}.
*
* The horizontal positions for each column/cell are computed or reused if possible.
*
* Then from the last cell and position, cells are laid out using {@link GridHelper#layout(Node, double, double)}.
*/
public void layoutCells(double position, boolean adjustColumns) {
if (cells.isEmpty()) return;
GridHelper helper = grid.getGridHelper();
Size size = grid.getCellSize();
double right = columns.diff() * size.getWidth();
if (adjustColumns) right -= size.getWidth();
// Compute positions
if (!canReusePositions() || (positions.size() != columns.diff() + 1 || adjustColumns)) {
positions.clear();
for (int i = 0; i < size(); i++) {
positions.add(right);
right -= size.getWidth();
}
}
// Layout
ListIterator cIt = new ArrayList<>(cells.values()).listIterator(size());
ListIterator pIt = new ArrayList<>(positions).listIterator(size());
while (cIt.hasPrevious()) {
C cell = cIt.previous();
Double pos = pIt.previous();
Node node = cell.getNode();
cell.beforeLayout();
helper.layout(node, pos, position);
cell.afterLayout();
}
this.position = position;
this.reusablePositions = false;
}
// Misc
/**
* Calls {@link C#dispose()} on all the cells in the map, then clears the map, leading to
* an empty {@code GridRow}.
*/
protected void clear() {
cells.values().forEach(C::dispose);
cells.clear();
}
/**
* @return the number of cells/columns in the {@code GridRow}
*/
public int size() {
return cells.size();
}
/**
* Converts the given coordinates to a linear index using {@link GridUtils#subToInd(int, int, int)}.
*/
public int toLinear(int row, int column) {
return GridUtils.subToInd(grid.getColumnsNum(), row, column);
}
//================================================================================
// Getters/Setters
//================================================================================
/**
* @return the {@link VirtualGrid} instance associated to this row
*/
public VirtualGrid getVirtualGrid() {
return grid;
}
/**
* @return this row's index
*/
public int getIndex() {
return index;
}
/**
* @return the range of columns represented by this row
*/
public IntegerRange getColumns() {
return columns;
}
/**
* @return the cells map
*/
protected Map getCells() {
return cells;
}
/**
* @return the cells map as an unmodifiable map
*/
public Map getCellsUnmodifiable() {
return Collections.unmodifiableMap(cells);
}
/**
* Converts the cells map to a new map with a different key -> value mapping.
*
* Instead of using the column index as the key, this map converts each index to a linear index
* using {@link #toLinear(int, int)}.
*/
public Map getLinearCells() {
return cells.entrySet().stream()
.collect(Collectors.toMap(
e -> toLinear(index, e.getKey()),
Map.Entry::getValue
));
}
/**
* @return the cells' positions set
*/
protected Set getPositions() {
return positions;
}
/**
* @return the cells' positions set as an unmodifiable set
*/
public Set getPositionsUnmodifiable() {
return Collections.unmodifiableSet(positions);
}
/**
* @return the vertical position at which the row should be positioned in the viewport
*/
public double getPosition() {
return position;
}
/**
* @return whether the already computed positions for the cells can be reused by {@link #layoutCells(double, boolean)}
*/
public boolean canReusePositions() {
return reusablePositions;
}
protected void setReusablePositions(boolean reusablePositions) {
this.reusablePositions = reusablePositions;
}
/**
* @return whether the cells of this row are visible in the viewport
*/
public boolean isVisible() {
return visible;
}
/**
* Responsible for showing/hiding the cells of this row. Cells are visible by default but various
* implementations of the Grid may decide to hide rows under certain conditions.
*/
protected void setVisible(boolean visible) {
cells.values().forEach(c -> c.getNode().setVisible(visible));
this.visible = visible;
}
//================================================================================
// Ignore
//================================================================================
/*
// This is the old onColumnsAdd() algorithm implementation before refactoring to a common
// solution for both index = 0 and index > 0
protected void onColumnAdd(int column, IntegerRange range) {
Map processed = new HashMap<>();
Set available = new HashSet<>(cells.keySet());
int targetSize = columns.diff() + 1;
Iterator> it;
if (index == 0) {
// Before change index
for (int i = columns.getMin(); i < column; i++) {
processed.put(i, cells.remove(i));
available.remove(i);
}
// After change index
it = cells.entrySet().iterator();
while (it.hasNext()) {
Map.Entry next = it.next();
int cIndex = next.getKey();
int newIndex = cIndex + 1;
if (!IntegerRange.inRangeOf(newIndex, columns) || (newIndex >= columns.getMin() && newIndex < column)) {
continue;
}
C cell = next.getValue();
cell.updateIndex(newIndex); // For row 0, linear and coordinate are the same
cell.updateCoordinates(index, newIndex);
processed.put(newIndex, cell);
available.remove(cIndex);
it.remove();
}
// Remaining
if (processed.size() != targetSize) {
Integer oIndex = new ArrayDeque<>(available).poll();
C cell;
T item = grid.getItems().getElement(column); // For row 0, linear and coordinate are the same
if (oIndex != null) {
cell = cells.remove(oIndex);
cell.updateItem(item);
} else {
cell = grid.getCellFactory().apply(item);
}
cell.updateIndex(column);
cell.updateCoordinates(index, column);
processed.put(column, cell);
}
clear();
cells.putAll(processed);
this.columns = columns;
return;
}
// Before change index
for (int i = columns.getMin(); i < column; i++) {
int linear = toLinear(index, i);
C cell = cells.remove(i);
cell.updateIndex(linear);
processed.put(i, cell);
available.remove(i);
}
// After change index
it = cells.entrySet().iterator();
while (it.hasNext()) {
Map.Entry next = it.next();
int cIndex = next.getKey();
int newIndex = cIndex + 1;
if (!IntegerRange.inRangeOf(newIndex, columns) || (newIndex >= columns.getMin() && newIndex < column)) {
continue;
}
C cell = next.getValue();
cell.updateIndex(toLinear(index, newIndex));
cell.updateCoordinates(index, newIndex);
processed.put(newIndex, cell);
available.remove(cIndex);
it.remove();
}
// Remaining
if (processed.size() != targetSize) {
Integer oIndex = new ArrayDeque<>(available).poll();
int linear = toLinear(index, column);
T item = grid.getItems().getElement(linear);
C cell;
if (oIndex != null) {
cell = cells.remove(oIndex);
cell.updateItem(item);
} else {
cell = grid.getCellFactory().apply(item);
}
cell.updateIndex(linear);
cell.updateCoordinates(index, column);
processed.put(column, cell);
}
clear();
cells.putAll(processed);
this.columns = columns;
}
*/
}