Class TableState<T>

java.lang.Object
io.github.palexdev.virtualizedfx.table.TableState<T>

public class TableState<T> extends Object
Class used by the 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()

The state can be 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 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 TableState which is the result of transitioning from this state to a new one given the new ranges for rows and columns
    • vScroll

      protected 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.

      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 ListChangeHelper and ListChangeHelper.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 single ListChangeHelper.Change bean and produce a new state according to the change's ListChangeHelper.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's Cell.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 for PaginatedVirtualTable too.

      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 with TableRow.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 state

      2) The row removed from the current state is null, a new row is created and added to the new state with addRow(int)

      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.

      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 the ListChangeHelper.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), see findShift(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 Deque is 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 to UpdateType.CHANGE and check if the number of rows has changed (in such case rowsChanged() is called) and then return the new state.
    • columnChangedFactory

      protected TableState<T> columnChangedFactory(TableColumn<T,? extends TableCell<T>> column)
      Given a certain column, this method will find its index with VirtualTable.getColumnIndex(TableColumn), and, if the index is in the current getColumnsRange(), will tell all the rows in the state to update the cell at the given index with TableRow.updateColumnFactory(int).

      Two side notes:

      1) Since the factory changed for the given column, all cells cached for that column in TableCache are invalid, so TableCache.clear(TableColumn) must also be called

      2) This will produce a new state object

      Parameters:
      column - the column for which the cell factory changed
    • findShift

      protected int findShift(List<Integer> indexes, int index)
      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 new TableRow using VirtualTable.rowFactoryProperty(), then adds it to the state at the given index.
    • addRow

      protected void addRow(int index, TableRow<T> row)
      Adds an already built TableRow to the state at the given index.
    • addRows

      protected void addRows(Map<Integer,TableRow<T>> rows)
      Adds all the rows contained in the given map to the state.
    • getKeysDequeue

      protected Deque<Integer> 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 if size() is greater or equal to the expected getTargetSize(), 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 TableManager won'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

      public VirtualTable<T> getTable()
      Returns:
      the VirtualTable instance this state is referring to
    • getRows

      protected Map<Integer,TableRow<T>> getRows()
      Returns:
      the rows map
    • getRowsUnmodifiable

      public Map<Integer,TableRow<T>> getRowsUnmodifiable()
      Returns:
      the rows as an unmodifiable map
    • getColumnsAsNodes

      public List<Region> getColumnsAsNodes()
      Converts the columns to a list or Regions by iterating over the getColumnsRange() and using VirtualTable.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 TableRow in the state
    • getTargetSize

      public int getTargetSize()
      Returns:
      the expected number of rows
    • getType

      public UpdateType 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, causing haveRowsChanged() to return true, which will tell the rContainer to update its children.
      See Also: