VFXTable
. Although, to be precise, and as the name also suggests,
this can be considered more like a 'manager' than a behavior. Behaviors typically respond to user input, and then update
the component's state. This behavior contains core methods to respond to various properties change in VFXTable
.
All computations here will generate a new VFXTableState
, if possible, and update the table and the layout
(indirectly, call to VFXTable.requestViewportLayout()
. Beware, some changes may end up generating 'clone' states,
because apparently nothing changed, see VFXTableState
, VFXTableState.clone()
, VFXTableState.isClone()
.
By default, manages the following changes:
- geometry changes (width/height changes), onGeometryChanged(GeometryChangeType)
- columns' width changes, onColumnWidthChanged(VFXTableColumn)
- columns list changes, onColumnsChanged(ListChangeListener.Change)
- items change, onItemsChanged()
- position changes, onPositionChanged(Orientation)
- row factory changes, onRowFactoryChanged()
- cell factory changes in columns, onCellFactoryChanged(VFXTableColumn)
- row height changes, onRowHeightChanged()
- columns size changes, onColumnsSizeChanged()
(specified by VFXTable.columnsSizeProperty()
)
- layout mode changes onColumnsLayoutModeChanged()
VFXTableHelper.invalidatePos()
is called.
However, invalidating the positions, also means that the onPositionChanged(Orientation)
method could be potentially
triggered, thus generating an unwanted 'middle' state. For this reason a special flag invalidatingPos
is set
to true
before the invalidation, so that the other method will exit immediately. It's reset back to false
after the computation or if any of the checks before the actual computation fails.-
Field Summary
FieldsModifier and TypeFieldDescriptionprotected boolean
protected boolean
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprotected VFXTableState
<T> This method is responsible for properly compute an invalidVFXTableState
depending on certain conditions.protected boolean
Avoids code duplication.protected VFXTableState
<T> Avoids code duplication.protected void
moveReuseCreateAlgorithm
(io.github.palexdev.mfxcore.base.beans.range.IntegerRange rowsRange, io.github.palexdev.mfxcore.base.beans.range.IntegerRange columnsRange, VFXTableState<T> newState) Avoids code duplication.protected void
onCellFactoryChanged
(VFXTableColumn<T, VFXTableCell<T>> column) This method should be called byVFXTableColumn
s when their cell factory changes.protected void
onColumnsChanged
(javafx.collections.ListChangeListener.Change<? extends VFXTableColumn<T, ?>> change) This is responsible for handling changes inVFXTable.getColumns()
as well as initializing theVFXTableColumn.tableProperty()
of each column.protected void
This is responsible for updating the table's state when theVFXTable.columnsLayoutModeProperty()
changes.protected void
This method is responsible for computing a new state when theVFXTable.columnsSizeProperty()
changes.protected void
onColumnWidthChanged
(VFXTableColumn<T, ?> column) Used inColumnsLayoutMode.VARIABLE
mode to callVFXTable.requestViewportLayout(VFXTableColumn)
.protected void
This core method is responsible for ensuring that the viewport always has the right number of columns, rows and cells.protected void
Before describing the operations performed by this method, it's important for the reader to understand the difference between the two changes caught by this method.protected void
onPositionChanged
(javafx.geometry.Orientation axis) This core method is responsible for updating the table's state when the vertical and horizontal positions change.protected void
This method is responsible for updating the table's state when theVFXTable.rowFactoryProperty()
changes.protected void
This method is responsible for computing a new state when theVFXTable.rowsHeightProperty()
changes.protected boolean
rangeCheck
(io.github.palexdev.mfxcore.base.beans.range.IntegerRange range, boolean update, boolean dispose) Avoids code duplication.protected void
remainingAlgorithm
(io.github.palexdev.mfxcore.base.beans.range.ExcludingIntegerRange eRange, VFXTableState<T> newState) Avoids code duplication.protected boolean
Avoids code duplication.Methods inherited from class io.github.palexdev.mfxcore.behavior.BehaviorBase
dispose, getActions, getNode, init, keyPressed, keyPressed, keyReleased, keyReleased, keyTyped, keyTyped, mouseClicked, mouseClicked, mouseDragged, mouseDragged, mouseEntered, mouseEntered, mouseExited, mouseExited, mouseMoved, mouseMoved, mousePressed, mousePressed, mouseReleased, mouseReleased, register, scroll, scroll, touchMoved, touchMoved, touchPressed, touchPressed, touchReleased, touchReleased, touchStationary, touchStationary
-
Field Details
-
invalidatingPos
protected boolean invalidatingPos -
wasGeometryChange
protected boolean wasGeometryChange
-
-
Constructor Details
-
VFXTableManager
-
-
Method Details
-
onGeometryChanged
This core method is responsible for ensuring that the viewport always has the right number of columns, rows and cells. This is called every time the table's geometry changes (width/height depending on the orientation), which means that this is also responsible for initialization (when width/height becomes > 0.0).After preliminary checks done by
Note that to compute a valid new state, it is important to also validate the table's positions by invokingtableFactorySizeCheck()
andrangeCheck(IntegerRange, boolean, boolean)
, (on the columns range) the computation for the new state is delegated to themoveReuseCreateAlgorithm(IntegerRange, IntegerRange, VFXTableState)
.VFXTableHelper.invalidatePos()
. Note that this is also responsible for the last column to always fill the table, when the table's width changes andVFXTableState.isLayoutNeeded()
isfalse
, this will invokeVFXTable.requestViewportLayout(VFXTableColumn)
with the last column as parameter. -
onColumnWidthChanged
Used inColumnsLayoutMode.VARIABLE
mode to callVFXTable.requestViewportLayout(VFXTableColumn)
. Essentially, this should trigger a partial layout computation.- See Also:
-
onColumnsChanged
protected void onColumnsChanged(javafx.collections.ListChangeListener.Change<? extends VFXTableColumn<T, ?>> change) This is responsible for handling changes inVFXTable.getColumns()
as well as initializing theVFXTableColumn.tableProperty()
of each column. Anull
parameter indicates that we want to just initialize the columns, and no change occurred. Removed columns will have both the table instance and index properties reset tonull
and -1 respectively. Before starting the new state computation, we must make sure that the viewport position is valid by callingVFXTableHelper.invalidatePos()
. Then we get both the columns and rows ranges by usingVFXTableHelper.columnsRange()
andVFXTableHelper.rowsRange()
. If the column range is invalid, then we set the state toVFXTableState.INVALID
, dispose the old one and exit immediately, all of this is done byrangeCheck(IntegerRange, boolean, boolean)
.At this point, we can compute the new state. If the rows range is valid, we iterate over it and for each row we call
VFXTableRow.updateColumns(IntegerRange, boolean)
withtrue
as parameter, thus we ensure that all the cells have the right cells. Finally, callsVFXTable.update(VFXTableState)
to set the new state and trigger the layout computation. -
onItemsChanged
protected void onItemsChanged()Before describing the operations performed by this method, it's important for the reader to understand the difference between the two changes caught by this method.VFXTable
makes use of aListProperty
to store the items to display. The property is essentially the equivalent of thisObjectProperty<ObservableList>
. Now, if you are familiar with JavaFX, you probably know that there are two possible changes to listen to: one is changes toObjectProperty
(if theObservableList
instance changes), and the other are changes in theObservableList
instance itself. As you may guess, managing both these changes with a simpleObjectProperty
is quite cumbersome, because you need two listeners: one that that catches changes in the list, and another to catch changes to the property. In particular, the latter has the task to add the first listener to the newObservableList
instance.And here is where
ListProperty
comes in handy. By adding anInvalidationListener
to this special property we are able to intercept both the type of changes always, even if theObservableList
instance changes, everything is handled automatically.Needless to say, we use a
This core method is responsible for updating the table's state when any of the two aforementioned changes happen.Property
to store the items to allow the usage of bindings!These kind of updates are the most tricky and expensive. In particular, additions and removals can occur at any position in the list, which means that calculating the new state solely on the indexes is a no-go. It is indeed possible by, in theory, isolating the indexes at which the changes occurred, separating the rows that need only an index update from the ones that actually need a full update. However, such an approach requires a lot of code, is error-prone, and a bit heavy on performance. The new approach implemented here requires changes to the state class as well,
VFXTableState
.The computation for the new state is similar to the
moveReuseCreateAlgorithm(IntegerRange, IntegerRange, VFXTableState)
, but the first step, which tries to identify the common cells, is quite different. You see, as I said before, additions and removals can occur at any place in the list. Picture it with this example:In list before: 0 1 2 3 4 5 Add at index 2 these items: 99, 98 In list after: 0 1 99 98 2 3 4 5 Now let's suppose the range of displayed items is the same: [0, 5] (6 items) (I'm going now to write items with the index too, like this Index:Item) Items before: [0:0, 1:1, 2:2, 3:3, 4:4, 5:5] Items after: [0:0, 1:1, 2:99, 3:98, 4:2, 5:3] See? Items 2 and 3 are still there but in a different position (index) Since we assume item updates are more expensive than index updates, we must ensure to take those two rows and update them just by index
VFXTableState.removeRow(Object)
. First, we retrieve the item from the list that is now at index i (this index comes from the loop on the range), then we try to remove the row for this item from the old state. If the row is found, we update it by index and add it to the new state. Note that the index is also excluded from the range.Now that 'common' rows have been properly updated, the remaining items are processed by the
remainingAlgorithm(ExcludingIntegerRange, VFXTableState)
.1) This is one of those methods that to produce a valid new state needs to validate the table's positions, so it calls
VFXTableHelper.invalidatePos()
2) To make sure the layout is always correct, at the end we always invoke
VFXTable.requestViewportLayout()
. You can guess why from the above example, items 2 and 3 are still in the viewport, but at different indexes, which also means at different layout positions. There is no easy way to detect this, so better safe than sorry, always update the layout. -
onPositionChanged
protected void onPositionChanged(javafx.geometry.Orientation axis) This core method is responsible for updating the table's state when the vertical and horizontal positions change. Since the table doesn't use any throttling technique to limit the number of events/changes, and since scrolling can happen very fast, performance here is crucial.Immediately exits if: the special flag
Before further discussing the internal mechanisms of this method, notice that this accepts a parameter of typeinvalidatingPos
is true or the current state isVFXTableState.INVALID
. Many other computations here need to validate the positions by callingVFXTableHelper.invalidatePos()
, to ensure that the resulting state is valid. However, invalidating the positions may trigger this method, causing two or more state computations to run at the 'same time'; this behavior must be avoided, and that flag exists specifically for this reason.Orientation
. The reason is simple. The table is virtualized on both the x-axis and y-axis, but changes are 'atomic', meaning that only one can be processed at a time. For the scroll it's the same. If you imagine scroll on a timeline, each event occurs after the other. Even if you scroll in both directions at the same time, there's a difference between what you perceive and what happens under the hood. For this reason, to also avoid code duplication, that parameter tells this method on which axis the scroll happened.The state computation changes depending on the
Orientation
parameter.Horizontal
Requests the layout computation through
VFXTable.requestViewportLayout()
and exits immediately if the layout mode is set toColumnsLayoutMode.VARIABLE
. In such mode, the columns range will never change, but the layout is still required.Does nothing and exits if the new columns range is the same as the old one.
The new state is computed by simply copying all the rows from the old state and just calling
VerticalVFXTableRow.updateColumns(IntegerRange, boolean)
on each of them.Does nothing and exits if the new rows range is the same as the old one.
The computation is delegated to the
moveReuseCreateAlgorithm(IntegerRange, IntegerRange, VFXTableState)
algorithm. -
onRowFactoryChanged
protected void onRowFactoryChanged()This method is responsible for updating the table's state when theVFXTable.rowFactoryProperty()
changes. Before proceeding, it checks whether a new state can be generated by usingtableFactorySizeCheck()
, if not the rows' cache given byVFXTable.getCache()
is cleared.If the old state is valid and not empty, then we can optimize the algorithm by copying each of the old rows' state to the corresponding new ones. This is possible because ranges cannot change by simply switching the factory. For each index in the rows range, a new row is created and its state is set to the one at the same index in the old state by using
VFXTableRow.copyState(VFXTableRow)
.Otherwise, if the old state has no rows from which copy the state, then it calls both
VFXTableRow.updateIndex(int)
andVFXTableRow.updateColumns(IntegerRange, boolean)
.Finally, the old state is disposed,
VFXTableState.dispose()
, the rows' cache is cleared (old rows cannot be used since the factory changed), and the table updated with the new state. -
onCellFactoryChanged
This method should be called byVFXTableColumn
s when their cell factory changes.For each row in the current state, this calls
VFXTableRow.replaceCells(VFXTableColumn)
which makes the operation as efficient as possible.This is one of those methods that do not really change the table's state, rather it updates the rows' state, see
VFXTableState
. If the replacement was done, then the table's state is set to a clone of the current one. -
onRowHeightChanged
protected void onRowHeightChanged()This method is responsible for computing a new state when theVFXTable.rowsHeightProperty()
changes. We could say that this is essentially equal to changing the cells' height.After preliminary checks done by
Note that to compute a valid new state, it is important to also validate the table's positions by invokingtableFactorySizeCheck()
, the computation for the new state is delegated to theintersectionAlgorithm()
.VFXTableHelper.invalidatePos()
. Also, it will request the layout computation,VFXTable.requestViewportLayout()
, even if the cells didn't change for obvious reasons. -
onColumnsSizeChanged
protected void onColumnsSizeChanged()This method is responsible for computing a new state when theVFXTable.columnsSizeProperty()
changes. Since the property specifies both the width and height of the columns, both columns and rows ranges can vary.First, it checks if the new columns range is valid by using
rangeCheck(IntegerRange, boolean, boolean)
. Then it checks if the rows range changed, and if that's the case, delegates the update toonGeometryChanged(GeometryChangeType)
.Otherwise, we can reuse the rows from the old state, and only if the columns range has changed, we also update them by calling
VFXTableRow.updateColumns(IntegerRange, boolean)
on each of them.In any case, this will trigger a layout computation, since columns and rows may need to be resized/repositioned.
This is one of those methods that to produce a valid new state needs to validate the table's positions, so it callsVFXTableHelper.invalidatePos()
-
onColumnsLayoutModeChanged
protected void onColumnsLayoutModeChanged()This is responsible for updating the table's state when theVFXTable.columnsLayoutModeProperty()
changes.The algorithm is almost the same for both cases: [FIXED -> VARIABLE], [VARIABLE -> FIXED]. The only thing that may change is the columns range, so, on each of the rows from the old state this calls
VFXTableRow.updateColumns(IntegerRange, boolean)
. The new state is almost a copy of the old one except for the columns range.There are three extra steps when it's switching from VARIABLE to FIXED mode:
1) Before updating the rows, we need to validate the horizontal position by using
VFXTableHelper.invalidatePos()
. Also, since in VARIABLE mode columns and cells may be hidden to enhance performance, this also resets all columns' visibility.2) When updating the rows we also reset every cell's visibility.
3) At the end of the computation, if
VFXTableState.isLayoutNeeded()
returns false, we still trigger a layout computation withVFXTable.requestViewportLayout()
. -
moveReuseCreateAlgorithm
protected void moveReuseCreateAlgorithm(io.github.palexdev.mfxcore.base.beans.range.IntegerRange rowsRange, io.github.palexdev.mfxcore.base.beans.range.IntegerRange columnsRange, VFXTableState<T> newState) Avoids code duplication. Typically used when, while iterating on the rows and columns ranges, it's enough to move the rows from the current state to the new state. For indexes which are not found in the current state, a new row is either taken from the old state, taken from cache or created by the row factory.(The last operations are delegated to the
remainingAlgorithm(ExcludingIntegerRange, VFXTableState)
).Note that the columns range parameter is only needed to ensure each row is displaying the correct cells by invoking
VFXTableRow.updateColumns(IntegerRange, boolean)
. We don't know when this is needed and when not, we simply do it always and delegate to the method the check (if the given range is not equal to the old one then update).- See Also:
-
intersectionAlgorithm
Avoids code duplication. Typically used in situations where the previous rows range and the new one are likely to be very close, but most importantly, that do not involve any change in the items' list. In such cases, the computation for the new state is divided in two parts:0) Prerequisites: the new rows range [min, max], the excluding range (a helper class to keep track of common rows), the current state, and the intersection between the current rows range and the new rows range
1) The intersection allows us to distinguish between rows that can be moved as they are, without any update, from the current state to the new one. For this, it's enough to check that the intersection range is valid, and then a for loop. Common indexes are also excluded from the range!
2) The remaining indexes are items that are new. Which means that if there are still rows in the current state, they need to be updated (both index, item, and maybe columns range too). Otherwise, new ones are created by the row factory.
- See
Utils.intersection(io.github.palexdev.mfxcore.base.beans.range.IntegerRange, io.github.palexdev.mfxcore.base.beans.range.IntegerRange)
: used to find the intersection between two ranges- See
rangeCheck(IntegerRange, boolean, boolean)
: used to validate the intersection range, both parameters are false!- See
remainingAlgorithm(ExcludingIntegerRange, VFXTableState)
: the second part of the algorithm is delegated to this method- See Also:
-
remainingAlgorithm
protected void remainingAlgorithm(io.github.palexdev.mfxcore.base.beans.range.ExcludingIntegerRange eRange, VFXTableState<T> newState) Avoids code duplication. Typically used to process indexes not found in the current state.For any index in the given collection, a row is needed. Also, it needs to be updated by index, item and maybe columns range too. This row can come from three sources:
1) from the current state if it's not empty yet. Since the rows are stored in a
SequencedMap
, one is removed by callingIndexBiMap.StateMapBase.pollFirst()
.2) from the
VFXCellsCache
if not empty (hereVFXTable.getCache()
)3) created by the row factory
- See
After a row is retrieved from any of the three sources, this callsVFXTableHelper.indexToRow(int)
: this handles the second and third cases. If a row can be taken from the cache, automatically updates its item then returns it. Otherwise, invokes theVFXTable.rowFactoryProperty()
to create a new oneVFXTableRow.updateColumns(IntegerRange, boolean)
to ensure it is displaying the correct cells. -
tableFactorySizeCheck
protected boolean tableFactorySizeCheck()Avoids code duplication. This method checks for six things:1) If the columns' list is empty
2) If the items' list is empty
3) If the row factory is
null
4) If the rows' height is lesser or equal to 0
5) If the table's width is lesser or equal to 0
6) If the table's height is lesser or equal to 0
If any of those checks is true: the table's state is set to
computeInvalidState()
, the current state is disposed, the 'invalidatingPos' flag is reset, finally returns false.Otherwise, does nothing and returns true.
- See
VFXTable.rowFactoryProperty()
- See
VFXTable.rowsHeightProperty()
- See
disposeCurrent()
: for the current state disposal- Returns:
- whether all the aforementioned checks have passed
-
rangeCheck
protected boolean rangeCheck(io.github.palexdev.mfxcore.base.beans.range.IntegerRange range, boolean update, boolean dispose) Avoids code duplication. Used to check whether the given range is valid, not equal toUtils.INVALID_RANGE
.When invalid, returns false, but first runs the following operations: disposes the current state (only if the 'dispose' parameter is true), sets the table's state to
VFXTableState.INVALID
(only if the 'update' parameter is true), resets the 'invalidatingPos' flag. Otherwise, does nothing and returns true.Last but not least, this is a note for the future on why the method is structured like this. It's crucial for the disposal operation to happen before the table's state is set to
VFXTableState.INVALID
, otherwise the disposal method will fail, since it will then retrieve the empty state instead of the correct one.- See
disposeCurrent()
: for the current state disposal- Parameters:
range
- the range to checkupdate
- whether to set the table's state to 'empty' if the range is not validdispose
- whether to dispose the current/old state if the range is not valid- Returns:
- whether the range is valid or not
-
disposeCurrent
protected boolean disposeCurrent()Avoids code duplication. Responsible for disposing the current state if it is not empty.- See
VFXTableState.dispose()
- Returns:
- whether the disposal was done or not
-
computeInvalidState
This method is responsible for properly compute an invalidVFXTableState
depending on certain conditions. You see, the table is a special component also because it can technically work even if there are no items in it. TheVFXTableState.INVALID
state is to be used only if there are no columns in the table, or in general ifVFXTableHelper.columnsRange()
returnsUtils.INVALID_RANGE
.Otherwise, this will return a new state with
Utils.INVALID_RANGE
as the rows range, andVFXTableHelper.columnsRange()
as the columns range.Note that this new state will have its
VFXTableState.haveRowsChanged()
andVFXTableState.haveColumnsChanged()
flags set depending on the old state.
-