TableManager to represent the state of the viewport at a given time.
The idea is to have an immutable state so that each state is a different object, with some exceptional cases when the state doesn't need to be re-computed, so the old object is returned.
This offers information such as: - The range of rows contained in the state, getRowsRange()
- The range of columns contained by each TableRow in the state, getColumnsRange()
- The cells in the viewport. These are stored in TableRows, each of these has a cell for each column.
TableRows are kept in a map: rowIndex -> tableRow
- The expected number of rows, getTargetSize(). Note that this is computed by TableHelper.maxRows(),
so the result may be greater than the number of items available in the data structure
- The type of event that lead the old state to transition to the new one, see UpdateType
- A flag to check if new rows were created or some were deleted, haveRowsChanged().
This is used by VirtualTableSkin since we want to update the rContainer's children only when the rows change.
- A flag specifically for the PaginatedVirtualTable to indicate whether the state also contains some
rows that are hidden in the viewport, anyHidden()
empty in two different ways:
1) The table has no items
2) The table has no items and no columns
2a) The table has no columns
For this reason there are two different methods to check the state of the viewport in such cases:
isEmpty() and isEmptyAll().
-
Constructor Summary
ConstructorsConstructorDescriptionTableState(VirtualTable<T> table, io.github.palexdev.mfxcore.base.beans.range.IntegerRange rowsRange, io.github.palexdev.mfxcore.base.beans.range.IntegerRange columnsRange) -
Method Summary
Modifier and TypeMethodDescriptionprotected voidaddRow(int index) Creates a newTableRowusingVirtualTable.rowFactoryProperty(), then adds it to the state at the given index.protected voidAdds an already builtTableRowto the state at the given index.protected voidAdds all the rows contained in the given map to the state.booleanprotected TableState<T>This is responsible for transitioning this state to a new one given a series of changes occurred in the items list.protected voidclear()Shortcut to dispose all rows present in this state's cells map and then clear it.protected TableState<T>columnChangedFactory(TableColumn<T, ? extends TableCell<T>> column) Given a certain column, this method will find its index withVirtualTable.getColumnIndex(TableColumn), and, if the index is in the currentgetColumnsRange(), will tell all the rows in the state to update the cell at the given index withTableRow.updateColumnFactory(int).booleanChecks whether there are enough columns in this state to fill the viewport horizontally.protected intcomputeTargetSize(int expectedSize) protected intGiven an ordered list of indexes and the index to find, returns the index at which resides.Converts the columns to a list orRegionsby iterating over thegetColumnsRange()and usingVirtualTable.getColumn(int).io.github.palexdev.mfxcore.base.beans.range.IntegerRangegetRows()io.github.palexdev.mfxcore.base.beans.range.IntegerRangegetTable()intgetType()booleanprotected TableState<T>hScroll(io.github.palexdev.mfxcore.base.beans.range.IntegerRange columnsRange) This is responsible for transitioning to a new state when the viewport scrolls horizontally.protected TableState<T>init(io.github.palexdev.mfxcore.base.beans.range.IntegerRange rowsRange, io.github.palexdev.mfxcore.base.beans.range.IntegerRange columnsRange) Responsible for filling the viewport the needed amount of rows/cells.booleanisEmpty()booleanprotected TableState<T>processChange(io.github.palexdev.mfxcore.utils.fx.ListChangeHelper.Change change) This is responsible for processing a singleListChangeHelper.Changebean and produce a new state according to the change'sListChangeHelper.ChangeType.protected voidSets the rowsChanged flag to true, causinghaveRowsChanged()to return true, which will tell the rContainer to update its children.booleanChecks ifsize()is greater or equal to the expectedgetTargetSize(), in other words if there are enough rows to fill the viewport vertically.intsize()intprotected TableState<T>vScroll(io.github.palexdev.mfxcore.base.beans.range.IntegerRange rowsRange) This is responsible for transitioning to a new state when the viewport scrolls vertically.
-
Constructor Details
-
TableState
public TableState(VirtualTable<T> table, io.github.palexdev.mfxcore.base.beans.range.IntegerRange rowsRange, io.github.palexdev.mfxcore.base.beans.range.IntegerRange columnsRange)
-
-
Method Details
-
init
protected TableState<T> init(io.github.palexdev.mfxcore.base.beans.range.IntegerRange rowsRange, io.github.palexdev.mfxcore.base.beans.range.IntegerRange columnsRange) Responsible for filling the viewport the needed amount of rows/cells. So this may supply or remove cells according to the viewport size.If the given ranges for rows and columns are the same as the ones of the state then the old state is returned.
This is used by
TableManager.init().- Returns:
- a new
TableStatewhich is the result of transitioning from this state to a new one given the new ranges for rows and columns
-
vScroll
This is responsible for transitioning to a new state when the viewport scrolls vertically.Used by
TableManager.onVScroll(). -
hScroll
protected TableState<T> hScroll(io.github.palexdev.mfxcore.base.beans.range.IntegerRange columnsRange) This is responsible for transitioning to a new state when the viewport scrolls horizontally.Used by
TableManager.onHScroll(). -
change
protected TableState<T> change(List<io.github.palexdev.mfxcore.utils.fx.ListChangeHelper.Change> changes) This is responsible for transitioning this state to a new one given a series of changes occurred in the items list.Note that this is made to work with
ListChangeHelperandListChangeHelper.Change.Why? Tl;dr: For performance reasons.
-
processChange
protected TableState<T> processChange(io.github.palexdev.mfxcore.utils.fx.ListChangeHelper.Change change) This is responsible for processing a singleListChangeHelper.Changebean and produce a new state according to the change'sListChangeHelper.ChangeType. In the following lines I'm going to document each type. PERMUTATION The permutation case while not being the most complicated can still be considered a bit heavy to compute since cells keep their index but all their items must be updated, so the performance is totally dependent on the cell'sCell.updateItem(Object)implementation. REPLACE The algorithm for replacements is quite complex because of the shitty JavaFX apis. A replacement is also considered a removal immediately followed by an addition and this leads to some serious issues. For example there is no easy way to distinguish between a simple replace and a "setAll()" (which also clears the list).That being said the first thing to check is whether the change occurred before the last displayed rows, otherwise we simply ignore it and return the old state.
Among the current displayed cells, those who have not been changed are simply moved to the new state. The rest of the rows are updated (if there are still rows to be reused) or created.
At the end we ensure that the old state is empty by clearing and disposing the remaining rows.
ADDITION The computation in case of added items to the list is complex and a bit heavy on performance. There are several things to consider, and it was hard to find a generic algorithm that would correctly compute the new state in all possible situations, included exceptional cases and forPaginatedVirtualTabletoo. The simplest of these cases is when changes occur after the displayed range of rows and there are already enough rows to fill the viewport, the old state is returned. In all the other cases the computation for the new state can begin.The first step is to get a series of information such as:
- the index of the first row to display,
TableHelper.firstRow()- the index of the last row to display,
TableHelper.lastRow()At this point the computation begins. The algorithm makes a distinction between the rows: some are valid and can be moved to the new state; some others are partially valid meaning that they can be reused as the item is the same but the index has changed; the remaining ones are invalid, meaning that they need to be updated both for the item and the index
A Set keeps track of the available rows, the ones that will be reused/updated.
- Valid rows are moved to the new state and removed from the Set
- Partially valid rows are first removed from the current state, then after their new index has been computed as the
oldIndex + change.size() (number of added items), they are updated withTableRow.updateIndex(int)and finally copied to the new state- Invalid rows are the ones whose index is not included in the new "rowsRange". We have two cases here:
1) The row removed from the current state is not null, so we can reuse it, it is updated with
TableRow.updateFull(int), then moved to the new state2) The row removed from the current state is null, a new row is created and added to the new state with
REMOVAL The computation in case of removed items from the list is complex and a bit heavy on performance. There are several things to consider, and it was hard to find a generic algorithm that would correctly compute the new state in all possible situations, included exceptional cases. The simplest of these cases is when changes occur after the displayed range of rows, the old state will be returned. In all the other cases the computation for the new state can begin.addRow(int)The first step is to separate those cells that only require a partial update (only index) from the others. First we get a Set of indexes from the state's range using
IntegerRange.expandRangeToSet(IntegerRange), then we remove from the Set all the indexes at which the removal occurred. Before looping on these indexes we also convert theListChangeHelper.Change.getIndexes()to a sorted List.In the loop we extract the row at index "i" and to update its index we must first compute the shift. We do this by using binary search,
Collections.binarySearch(List, Object). The new index will be:int newIndex = index - findShift(list, index), seefindShift(List, int).If the new index is below the range min the update is skipped and the loop goes to the next index, otherwise, the index is updated and the row moved to the new state.
The next step is to update those cells which need a full update (both item and index). First we compute their indexes by expanding the new state's range to a Set and then removing from it all the rows that have already been added to the new state (which means they have been updated already, also keep in mind that we always operate on indexes so newState.rows.keySet()).
Now a
Dequeis built on the remaining cells in the old state (cells.keySet() again) and the update can begin. We loop over the previous built Set of indexes:1) We get one of the indexes from the deque as
Deque.removeFirst()3) We remove the row at that index from the old state
4) We update the row with
TableRow.updateFull(int)using the loop "i"5) The row is added to the new state
6) We ensure that no rows are left in the old state
At the end we change the new state's type toUpdateType.CHANGEand check if the number of rows has changed (in such caserowsChanged()is called) and then return the new state. -
columnChangedFactory
Given a certain column, this method will find its index withVirtualTable.getColumnIndex(TableColumn), and, if the index is in the currentgetColumnsRange(), will tell all the rows in the state to update the cell at the given index withTableRow.updateColumnFactory(int).Two side notes:
1) Since the factory changed for the given column, all cells cached for that column in
TableCacheare invalid, soTableCache.clear(TableColumn)must also be called2) This will produce a new state object
- Parameters:
column- the column for which the cell factory changed
-
findShift
Given an ordered list of indexes and the index to find, returns the index at which resides. If the index is not present, returns the index at which it would be located.- See Also:
-
addRow
protected void addRow(int index) Creates a newTableRowusingVirtualTable.rowFactoryProperty(), then adds it to the state at the given index. -
addRow
Adds an already builtTableRowto the state at the given index. -
addRows
Adds all the rows contained in the given map to the state. -
getKeysDequeue
- Returns:
- converts the cells keySet to a
Deque.
-
computeTargetSize
protected int computeTargetSize(int expectedSize) - Returns:
- the expected number of items of the state
-
clear
protected void clear()Shortcut to dispose all rows present in this state's cells map and then clear it.- See Also:
-
rowsFilled
public boolean rowsFilled()Checks ifsize()is greater or equal to the expectedgetTargetSize(), in other words if there are enough rows to fill the viewport vertically. -
columnsFilled
public boolean columnsFilled()Checks whether there are enough columns in this state to fill the viewport horizontally. -
size
public int size()- Returns:
- the number of rows in this state
-
totalSize
public int totalSize()- Returns:
- the total number of cells in this state, as the sum of all the cells of each individual
TableRow
-
isEmpty
public boolean isEmpty()- Returns:
- whether the state is empty, no rows in it. Note that this will return true also when
the state is completely empty, see
isEmptyAll()
-
isEmptyAll
public boolean isEmptyAll()- Returns:
- whether the state is empty (no rows) AND the columns range is (-1, -1) (no columns).
Note that this is not an OR evaluation, as the
TableManagerwon't create a state which has rows/items if the table has no columns. Cells cannot be created and rendered if there isn't a column that instructs the viewport how to build them
-
anyHidden
public boolean anyHidden()- Returns:
- whether there are hidden rows in the viewport
-
getTable
- Returns:
- the
VirtualTableinstance this state is referring to
-
getRows
- Returns:
- the rows map
-
getRowsUnmodifiable
- Returns:
- the rows as an unmodifiable map
-
getColumnsAsNodes
Converts the columns to a list orRegionsby iterating over thegetColumnsRange()and usingVirtualTable.getColumn(int). -
getRowsRange
public io.github.palexdev.mfxcore.base.beans.range.IntegerRange getRowsRange()- Returns:
- the range of rows in the state
-
getColumnsRange
public io.github.palexdev.mfxcore.base.beans.range.IntegerRange getColumnsRange()- Returns:
- the range of columns of each
TableRowin the state
-
getTargetSize
public int getTargetSize()- Returns:
- the expected number of rows
-
getType
- Returns:
- the type of change/event that caused an old state to transition to this new one
-
haveRowsChanged
public boolean haveRowsChanged()- Returns:
- whether a change caused the number of rows of the new state to change compared to the old state
-
rowsChanged
protected void rowsChanged()Sets the rowsChanged flag to true, causinghaveRowsChanged()to return true, which will tell the rContainer to update its children.- See Also:
-