![JAR search and dependency download from the Maven repository](/logo.png)
io.github.palexdev.virtualizedfx.grid.GridManager Maven / Gradle / Ivy
/*
* 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.range.IntegerRange;
import io.github.palexdev.mfxcore.base.beans.range.NumberRange;
import io.github.palexdev.mfxcore.base.properties.range.IntegerRangeProperty;
import io.github.palexdev.mfxcore.collections.ObservableGrid.Change;
import io.github.palexdev.mfxcore.enums.GridChangeType;
import io.github.palexdev.virtualizedfx.beans.GridStateProperty;
import io.github.palexdev.virtualizedfx.cell.GridCell;
/**
* The {@code FlowManager} is responsible for managing the grid's viewport, track its current {@link GridState}
* and trigger states transitions.
*
* It stores the state of the viewport at any given time with three properties:
*
- the main one is the {@link #stateProperty()} which holds the {@link GridState} object representing the current state
* of the viewport
*
- the last rows range property, which holds the last range of displayed rows as a {@link IntegerRange}
*
- the last columns range property which holds the last range of displayed columns as a {@link IntegerRange}
*
* As mentioned above this is also responsible for handling states transitions when: initializations occur (cells supply
* and removals when the viewport size changes); vertical and horizontal scrolling; changes occurred in the data structure;
* clear and reset the viewport when needed.
*
* @param the type of items
* @param the type of cell used
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public class GridManager> {
//================================================================================
// Properties
//================================================================================
private final VirtualGrid grid;
private final GridStateProperty state = new GridStateProperty<>(GridState.EMPTY);
private final IntegerRangeProperty lastRowsRange = new IntegerRangeProperty();
private final IntegerRangeProperty lastColumnsRange = new IntegerRangeProperty();
//================================================================================
// Constructors
//================================================================================
GridManager(VirtualGrid grid) {
this.grid = grid;
}
//================================================================================
// Methods
//================================================================================
/**
* This is responsible for filling the viewport with the right amount of rows/columns/cells.
* So, it is a bit more than just an initialization method since this is also called for example when the viewport
* size changes and cells may need to be added or removed.
*
* The first step is to gather a series of useful information such as:
* - the expected range of rows, {@link GridHelper#rowsRange()}
*
- the expected range of columns, {@link GridHelper#columnsRange()}
*
- the old/current state, {@link #stateProperty()}
*
* We also ensure that the estimated size of the viewport is correct by calling {@link GridHelper#computeEstimatedSize()}.
*
* The second step is to distinguish between two cases:
* 1) The old/current state is {@link GridState#EMPTY}
*
2) The old/current state is a valid state
*
* In the first case it means that the viewport is empty. For each row in the expected rows range we add a row
* to the new state with {@link GridState#addRow(int)}. Since the viewport is empty we also need to call
* {@link GridState#cellsChanged()}. Last but not least we update all the properties of the manager, and then
* request the viewport layout with {@link VirtualGrid#requestViewportLayout()}.
*
* In the second case we call {@link GridState#init(IntegerRange, IntegerRange)} on the old/current state so that
* a new state, which reuses when possible the current already present rows, can be computed. At this point a special
* check is needed, the aforementioned method could also return the old/current state for whatever reason, in such case
* there's no need to proceed and the method exits immediately. Otherwise, same as above, update all the properties,
* call {@link GridState#cellsChanged()} and then {@link VirtualGrid#requestViewportLayout()}.
*
* @return in addition to the various computations made in this method, it also returns a boolean value to indicate
* whether computations lead to a layout request or not, {@link VirtualGrid#requestViewportLayout()}
*/
public boolean init() {
if (grid.getCellFactory() == null || itemsEmpty()) return false;
// Pre-computation
GridHelper helper = grid.getGridHelper();
IntegerRange rowsRange = helper.rowsRange();
IntegerRange columnsRange = helper.columnsRange();
helper.computeEstimatedSize();
// Check old state
GridState oldState = getState();
GridState newState = new GridState<>(grid, rowsRange, columnsRange);
if (oldState == GridState.EMPTY) {
for (Integer row : rowsRange) {
newState.addRow(row);
}
newState.cellsChanged();
setState(newState);
setLastRowsRange(rowsRange);
setLastColumnsRange(columnsRange);
grid.requestViewportLayout();
return true;
}
// Transition from old state to new state
newState = oldState.init(rowsRange, columnsRange);
if (newState == oldState) return false;
newState.cellsChanged();
setState(newState);
setLastRowsRange(rowsRange);
setLastColumnsRange(columnsRange);
grid.requestViewportLayout();
return true;
}
/**
* This is responsible for handling vertical scrolling.
*
* If the current state is empty exits immediately.
*
* Before transitioning to a new state and eventually requesting a layout computation to the grid,
* we must check some parameters.
*
* First we compute the rows range and if that is not equal to the range of the current state then
* we call {@link GridState#vScroll(IntegerRange)} to create a new state which will contain
* all the needed rows for the new range
*
* The layout instead uses a different range and is compared against the last rows range, if they are not
* equal then {@link VirtualGrid#requestViewportLayout()} is invoked.
*
* At the end the last rows range property is updated.
*/
public void onVScroll() {
GridState state = getState();
if (state == GridState.EMPTY || state.isEmpty() || grid.isEmpty()) return;
GridHelper helper = grid.getGridHelper();
int rows = helper.maxRows();
// State Computation
int sFirstRow = helper.firstRow();
int sLastRow = helper.lastRow();
int sTrueFirstRow = Math.max(sLastRow - rows + 1, 0);
IntegerRange sRange = IntegerRange.of(sTrueFirstRow, sLastRow);
if (!sRange.equals(state.getRowsRange())) {
setState(state.vScroll(sRange));
}
// Layout Computation
IntegerRange lRange = IntegerRange.of(sFirstRow, sLastRow);
if (!lRange.equals(getLastRowsRange())) {
grid.requestViewportLayout();
}
setLastRowsRange(lRange);
}
/**
* This is responsible for handling horizontal scrolling.
*
* If the current state is empty exits immediately.
*
* Before transitioning to a new state and eventually requesting a layout computation to the grid,
* we must check some parameters.
*
* First we compute the columns range and if that is not equal to the range of the current state then
* we call {@link GridState#hScroll(IntegerRange)} to create a new state which will contain
* all the needed columns for the new range
*
* The layout instead uses a different range and is compared against the last columns range, if they are not
* equal then {@link VirtualGrid#requestViewportLayout()} is invoked.
*
* At the end the last columns range property is updated.
*/
public void onHScroll() {
GridState state = getState();
if (state == GridState.EMPTY || state.isEmpty()) return;
GridHelper helper = grid.getGridHelper();
int columns = helper.maxColumns();
// State Computation
int sFirstColumn = helper.firstColumn();
int sLastColumn = helper.lastColumn();
int sTrueFirstColumn = Math.max(sLastColumn - columns + 1, 0);
IntegerRange sRange = IntegerRange.of(sTrueFirstColumn, sLastColumn);
if (!sRange.equals(state.getColumnsRange())) {
setState(state.hScroll(sRange));
}
// Layout Computation
IntegerRange lRange = IntegerRange.of(sFirstColumn, sLastColumn);
if (!lRange.equals(getLastColumnsRange())) {
grid.requestViewportLayout();
}
setLastColumnsRange(lRange);
}
/**
* This is responsible for handling changes occurring in the grid's items data structure.
*
* Before transitioning to a new state, there are three special cases that need to be covered:
* - if there are no items in the data structure (was cleared), we invoke {@link #clear()}
*
- if the current state is the {@link GridState#EMPTY} state than we must call {@link #init()}
*
- if the change is of type {@link GridChangeType#TRANSPOSE} then we {@link #reset()} the viewport
*
* In any other case we can call {@link GridState#change(Change)} on the current state
* to produce a new state that reflects the changes occurred in the data structure.
* At the end a layout request is sent to the grid, {@link VirtualGrid#requestViewportLayout()} and
* both the last rows and columns ranges are updated.
*/
public void onChange(Change change) {
GridState state = getState();
if (itemsEmpty()) {
clear();
change.endChange();
return;
}
if (state == GridState.EMPTY) {
init();
change.endChange();
return;
}
if (change.getType() == GridChangeType.TRANSPOSE) {
reset();
change.endChange();
return;
}
state = state.change(change);
setState(state);
grid.requestViewportLayout();
setLastRowsRange(state.getRowsRange());
setLastColumnsRange(state.getColumnsRange());
}
/**
* Responsible for clearing the viewport and resetting the manager' state.
*/
public void clear() {
getState().clear();
setState(GridState.EMPTY);
setLastRowsRange(IntegerRange.of(-1));
setLastColumnsRange(IntegerRange.of(-1));
GridHelper helper = grid.getGridHelper();
helper.computeEstimatedSize();
helper.invalidatedPos();
}
/**
* Responsible for resetting the viewport. First it calls {@link #clear()}
* then {@link #init()}.
*/
public void reset() {
clear();
init();
}
/**
* @return whether the data structure is empty
*/
protected boolean itemsEmpty() {
return grid.isEmpty();
}
//================================================================================
// Getters/Setters
//================================================================================
public GridState getState() {
return state.get();
}
/**
* Keeps the {@link GridState} object which represents the current state of the viewport.
*/
public GridStateProperty stateProperty() {
return state;
}
protected void setState(GridState state) {
this.state.set(state);
}
public NumberRange getLastRowsRange() {
return lastRowsRange.get();
}
/**
* Specifies the last range of rows.
*/
public IntegerRangeProperty lastRowsRangeProperty() {
return lastRowsRange;
}
public void setLastRowsRange(NumberRange lastRowsRange) {
this.lastRowsRange.set(lastRowsRange);
}
public NumberRange getLastColumnsRange() {
return lastColumnsRange.get();
}
/**
* Specifies the last range of columns.
*/
public IntegerRangeProperty lastColumnsRangeProperty() {
return lastColumnsRange;
}
public void setLastColumnsRange(NumberRange lastColumnsRange) {
this.lastColumnsRange.set(lastColumnsRange);
}
}