Skip to content

Commit

Permalink
✨ Implement "manual" update mechanism for all VFXContainers
Browse files Browse the repository at this point in the history
📝 Addressed some TODOs
📝 Add some more TODOs (:sigh: will this ever end)

Signed-off-by: palexdev <alessandro.parisi406@gmail.com>
  • Loading branch information
palexdev committed May 21, 2024
1 parent 402248b commit c40837a
Show file tree
Hide file tree
Showing 17 changed files with 523 additions and 132 deletions.
4 changes: 3 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
- Optimize cells to update only if and invalidation occurred (?)
- Rename special EMPTY state to INVALID
- Common interface for managers (?)
- Look better for clone states for VFXTable
- Look better for clone states for VFXTable
- Reorganize tests classes/packages
- Review cells!
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.github.palexdev.mfxcore.base.properties.styleable.StyleableObjectProperty;
import io.github.palexdev.virtualizedfx.enums.BufferSize;
import io.github.palexdev.virtualizedfx.events.VFXContainerEvent;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
Expand All @@ -11,6 +12,54 @@
*/
public interface VFXContainer<T> {

/**
* This method should be used by implementations to "manually" update the container.
* <p>
* This can be useful when working with models that do not use JavaFX properties.
* <p>
* Note: the {@code indexes} var-arg parameter can be used to specify which cells need to be updated. An empty
* array should update all of them.
* <p>
* More details: some cells may use an update mechanism which relies on property invalidation. Follow this example
* to better understand what I mean:
* <pre>
* {@code
* // Let's say I have a User class with 'firstName' and 'lastName' fields (we also have both getters and setters)
* // Now, let's assume I have a UserCell class used by the VFXContainer to display User objects (in a label for example)
* // This is a part of its implementation...
* public class UserCell extends Label implements Cell<User> {
* private final ObjectProperty<User> item = new SimpleObjectProperty<>() {
* @Overridden
* protected void invalidated() {
* update();
* }
* };
*
* protected void update() {
* // This will update the cell's text based on the current item
* }
* }
*
* // Remember, the 'invalidated()' method is called only when the reference changes, because internally it does not
* // check for equality but for identity
*
* // Now let's say I want to change a User's 'lastName' field like this...
* container.getItems().get(i).setLastName("NewLastName");
*
* // Question: how can we tell the cell to force the update?
* // There are two possible ways...
* // 1) For the invalidation to occur, we first set the item property to 'null', and then back to the old value
* // 2) We use an event-based mechanism to tell cells to force update themselves. This solution requires cells to
* // subscribe to such events to support "manual" updates
*
* // Solution 2 is more flexible, see VFXContainerEvent class
* }
* </pre>
*
* @see VFXContainerEvent
*/
void update(int... indexes);

default ObservableList<T> getItems() {
return itemsProperty().get();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.github.palexdev.virtualizedfx.events;

import io.github.palexdev.virtualizedfx.base.VFXContainer;
import io.github.palexdev.virtualizedfx.cells.Cell;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.event.EventType;
import javafx.scene.Node;

/**
* Custom event implementation to be used with {@link VFXContainer}s. Event-based systems are very useful for loose coupling,
* because it allows components to communicate with each other without needing to know the specifics of their implementations.
* <p>
* This mechanism should be used only when there are no other reasonable ways as the performance implications of this system
* are not very clear.
*/
public class VFXContainerEvent extends Event {
//================================================================================
// Event Types
//================================================================================

/**
* This event type can be used to tell cells to forcefully update. Especially useful when the model's data does not
* use JavaFX's properties.
*
* @see VFXContainer#update(int...)
*/
public static final EventType<VFXContainerEvent> UPDATE = new EventType<>("UPDATE");

//================================================================================
// Constructors
//================================================================================
public VFXContainerEvent(Object source, EventTarget target, EventType<? extends Event> eventType) {
super(source, target, eventType);
}

//================================================================================
// Static Methods
//================================================================================
public static <T> void update(Cell<T> cell) {
if (cell == null) return; // Avoid null targets
Node node = cell.toNode();
fireEvent(node, new VFXContainerEvent(null, node, UPDATE));
}
}
17 changes: 17 additions & 0 deletions src/main/java/io/github/palexdev/virtualizedfx/grid/VFXGrid.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.github.palexdev.virtualizedfx.base.VFXStyleable;
import io.github.palexdev.virtualizedfx.cells.Cell;
import io.github.palexdev.virtualizedfx.enums.BufferSize;
import io.github.palexdev.virtualizedfx.events.VFXContainerEvent;
import io.github.palexdev.virtualizedfx.list.VFXListHelper;
import io.github.palexdev.virtualizedfx.list.VFXListState;
import io.github.palexdev.virtualizedfx.properties.VFXGridStateProperty;
Expand Down Expand Up @@ -250,6 +251,22 @@ public void requestViewportLayout() {
//================================================================================
// Overridden Methods
//================================================================================
@Override
public void update(int... indexes) {
VFXGridState<T, C> state = getState();
if (state.isEmpty()) return;
if (indexes.length == 0) {
state.getCellsByIndex().values().forEach(VFXContainerEvent::update);
return;
}

for (int index : indexes) {
C c = state.getCellsByIndex().get(index);
if (c == null) continue;
VFXContainerEvent.update(c);
}
}

@Override
public List<String> defaultStyleClasses() {
return List.of("vfx-grid");
Expand Down
19 changes: 18 additions & 1 deletion src/main/java/io/github/palexdev/virtualizedfx/list/VFXList.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.github.palexdev.virtualizedfx.base.VFXStyleable;
import io.github.palexdev.virtualizedfx.cells.Cell;
import io.github.palexdev.virtualizedfx.enums.BufferSize;
import io.github.palexdev.virtualizedfx.events.VFXContainerEvent;
import io.github.palexdev.virtualizedfx.list.VFXListHelper.HorizontalHelper;
import io.github.palexdev.virtualizedfx.list.VFXListHelper.VerticalHelper;
import io.github.palexdev.virtualizedfx.properties.VFXListStateProperty;
Expand Down Expand Up @@ -51,7 +52,7 @@
* that influence the way items are shown (how many, start, end, changes in the list, etc.) will produce a specific state.
* This is an important concept as some of the features I'm going to mention below are due to the combination of default
* skin + default behavior. You are allowed to change/customize the skin and the behavior as you please. BUT, beware, VFX
* * components are no joke, they are complex, make sure to read the documentation before!
* * components are no joke, they are complex, make sure to read the documentation before!
* <p> - The items list is managed automatically (permutations, insertions, removals, updates). Compared to previous
* algorithms, the {@link VFXListManager} adopts a much simpler strategy while still trying to keep the cell updates count
* as low as possible to improve performance. See {@link VFXListManager#onItemsChanged()}.
Expand Down Expand Up @@ -216,6 +217,22 @@ public void requestViewportLayout() {
//================================================================================
// Overridden Methods
//================================================================================
@Override
public void update(int... indexes) {
VFXListState<T, C> state = getState();
if (state.isEmpty()) return;
if (indexes.length == 0) {
state.getCellsByIndex().values().forEach(VFXContainerEvent::update);
return;
}

for (int index : indexes) {
C c = state.getCellsByIndex().get(index);
if (c == null) continue;
VFXContainerEvent.update(c);
}
}

@Override
public List<String> defaultStyleClasses() {
return List.of("vfx-list");
Expand Down
33 changes: 27 additions & 6 deletions src/main/java/io/github/palexdev/virtualizedfx/table/VFXTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.github.palexdev.virtualizedfx.cells.TableCell;
import io.github.palexdev.virtualizedfx.enums.BufferSize;
import io.github.palexdev.virtualizedfx.enums.ColumnsLayoutMode;
import io.github.palexdev.virtualizedfx.events.VFXContainerEvent;
import io.github.palexdev.virtualizedfx.grid.VFXGrid;
import io.github.palexdev.virtualizedfx.list.VFXList;
import io.github.palexdev.virtualizedfx.properties.VFXTableStateProperty;
Expand Down Expand Up @@ -158,7 +159,7 @@
* Their behavior depends on the set {@link ColumnsLayoutMode}.
* In {@link ColumnsLayoutMode#VARIABLE} mode, columns will be resized to make their header and all their "children" cells fit the content.
* In {@link ColumnsLayoutMode#FIXED} mode, since columns can't have different size, the algorithm chooses the greatest
* needed width among all the columns and then sets the {@link #columnsSizeProperty()}. // TODO implement
* needed width among all the columns and then sets the {@link #columnsSizeProperty()}.
* Of course, the width computation is done on the currently shown items, meaning that if you scroll and there are now
* items that are even bigger than the current set width, then you'll have to autosize again.
* <p> - Columns' indexes. Since columns are stored in a list, there is not a fast way to retrieve
Expand All @@ -172,7 +173,6 @@
* list, that may involve having duplicates in it, it's recommended to use a temporary new list and then use 'setAll'.
* {@link VFXTableColumn} offers a utility method to swap to columns, so please use {@link VFXTableColumn#swapColumns(VFXTable, int, int)}
* or {@link VFXTableColumn#swapColumns(ObservableList, int, int)} instead of {@link Collections#swap(List, int, int)}.
* <p> - // TODO implement manual update for all containers
*
* @param <T> the type of items in the table
*/
Expand Down Expand Up @@ -338,10 +338,6 @@ public int indexOf(VFXTableColumn<T, ?> column) {
return column.getIndex();
}

public void update() {
// TODO implement
}

/**
* Setter for the {@link #stateProperty()}.
*/
Expand Down Expand Up @@ -393,6 +389,31 @@ protected void requestViewportLayout(VFXTableColumn<T, ?> column) {
//================================================================================
// Overridden Methods
//================================================================================

/**
* {@inheritDoc}
* <p></p>
* Note that this may be a costly operation due to nested loops. Since cells are inside rows we must first iterate
* over the rows, then iterate on each of their cells and fire an update event on each of them.
*/
@Override
public void update(int... indexes) {
VFXTableState<T> state = getState();
if (state.isEmpty()) return;
if (indexes.length == 0) {
state.getRowsByIndex().values().forEach(r ->
r.getCellsByIndex().values().forEach(VFXContainerEvent::update)
);
return;
}

for (int index : indexes) {
VFXTableRow<T> row = state.getRowsByIndex().get(index);
if (row == null) continue;
row.getCellsByIndex().values().forEach(VFXContainerEvent::update);
}
}

@Override
protected SkinBase<?, ?> buildSkin() {
return new VFXTableSkin<>(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ public HPos getIconAlignment() {
/**
* Specifies the side on which the icon will be placed.
* <p></p>
* {@link HPos#CENTER} is ignored by the default skin, the icon will be placed to the right. // TODO change this
* By setting the alignment to {@link HPos#CENTER} the default skin, {@link VFXDefaultTableColumnSkin}, will hide the
* text and show only the icon at the center.
* <p></p>
* This is settable via CSS with the "-vfx-icon-alignment" property.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/app/Playground.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package app;

import assets.User;
import interactive.table.TableTestUtils.Table;
import interactive.table.TableTestUtils.User;
import io.github.palexdev.mfxcore.base.TriConsumer;
import io.github.palexdev.mfxcore.builders.InsetsBuilder;
import io.github.palexdev.mfxcore.builders.bindings.StringBindingBuilder;
Expand Down

0 comments on commit c40837a

Please sign in to comment.