All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.palexdev.virtualizedfx.table.VFXTableColumn Maven / Gradle / Ivy

/*
 * Copyright (C) 2024 Parisi Alessandro - [email protected]
 * 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.table;

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

import io.github.palexdev.mfxcore.base.properties.styleable.StyleableBooleanProperty;
import io.github.palexdev.mfxcore.base.properties.styleable.StyleableIntegerProperty;
import io.github.palexdev.mfxcore.controls.Labeled;
import io.github.palexdev.mfxcore.utils.fx.StyleUtils;
import io.github.palexdev.virtualizedfx.base.VFXContainer;
import io.github.palexdev.virtualizedfx.base.VFXStyleable;
import io.github.palexdev.virtualizedfx.base.WithCellFactory;
import io.github.palexdev.virtualizedfx.cells.VFXSimpleTableCell;
import io.github.palexdev.virtualizedfx.cells.base.VFXTableCell;
import io.github.palexdev.virtualizedfx.enums.ColumnsLayoutMode;
import io.github.palexdev.virtualizedfx.properties.CellFactory;
import io.github.palexdev.virtualizedfx.table.defaults.VFXTableColumnBehavior;
import io.github.palexdev.virtualizedfx.utils.VFXCellsCache;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleablePropertyFactory;
import javafx.scene.Node;

/**
 * Base class that defines common properties and behaviors for all columns to be used with {@link VFXTable}.
 * Extends {@link Labeled} for simplicity, and uses behaviors of type {@link VFXTableColumnBehavior}.
 * The default style class is set to '.vfx-column'.
 * 

* This class has three basic properties: *

1) The {@link #tableProperty()}. Every column should specify the table's instance they belong to. * By default, as handled by {@link VFXTableManager#onColumnsChanged(ListChangeListener.Change)}, the system will manage * this property automatically, by setting or re-setting (to {@code null}) the instance as columns are added/removed to/from the * table. This is an improvement over the previous implementation which required the user to pass the table's instance to * the constructors. *

2) The {@link #indexProperty()}. I believe it may be useful for every column to specify their position in * {@link VFXTable#getColumns()}. However, there is not a fast way to know the index of a column from its instance, * {@link List#indexOf(Object)} is way too slow for a virtualized container. For this reason, the property is automatically * updated by the {@link VFXTableSkin} when a layout is performed, see {@link VFXTableSkin#updateColumnIndex(VFXTableColumn, int)}. *

3) Every column should specify a function to build cells for some data from the model of type {@code T}. These functions * do not have to produce different cell types necessarily. Rather, since every column is probably going to refer to a specific * piece of sub-data for a class of type {@code T}, it should tell the built cells how to map to that piece of sub-data. * In fact, if you check the default cell implementations (e.g. {@link VFXSimpleTableCell}) you can see that they need * an 'extractor' and 'converter' functions to work as intended. *

* Last but not least, every column has a {@link VFXCellsCache} instance which will hold cells that are not used anymore, * but could be in the future. Creating nodes is a very costly operation. * We can "dampen" this cost by caching and just update them when needed again. You can increment the capacity or disable * it by setting the {@link #cellsCacheCapacityProperty()}. *

* Width Handling *

* This new implementation handles the width in a special way. First and foremost, the width of each column is decided * by the {@link VFXTableHelper} implementation, since the base layout methods are defined there. The criteria are various. * There is the {@link VFXTable#columnsSizeProperty()} which depending on the {@link ColumnsLayoutMode} specifies the * fixed or minimum width every column must have. So, even in the latter case, we do not use {@link Node#minWidth(double)} * to determine the minimum width. If you want a column to have a specific width (in variable mode ofc!!) then you must * use this method to resize it: {@link #resize(double)}. This new implementation uses the {@link #prefWidthProperty()} * to store the user's preferred width for the column. Not only that, this 'slot'/variable/property is used by autosize methods * too {@link VFXTable#autosizeColumn(VFXTableColumn)}, and should be used by gestures too (see {@link VFXTableColumnBehavior}). *

* The default behavior implementation allows you to resize the column at runtime with the mouse cursor. The feature can * be enabled/disabled through the {@link #gestureResizableProperty()}. * * @param the type of data in the table * @param the type of cells this column will produce */ public abstract class VFXTableColumn> extends Labeled> implements WithCellFactory, VFXStyleable { //================================================================================ // Properties //================================================================================ private final VFXCellsCache cache; private final ReadOnlyObjectWrapper> table = new ReadOnlyObjectWrapper<>(); private final ReadOnlyIntegerWrapper index = new ReadOnlyIntegerWrapper(-1); private final CellFactory cellFactory = new CellFactory<>(null) { @Override public VFXContainer getOwner() { return table.get(); } @Override protected void onInvalidated(Function newFactory) { onCellFactoryChanged(newFactory); } }; //================================================================================ // Constructors //================================================================================ public VFXTableColumn() { cache = createCache(); initialize(); } public VFXTableColumn(String text) { super(text); cache = createCache(); initialize(); } public VFXTableColumn(String text, Node graphic) { super(text, graphic); cache = createCache(); initialize(); } //================================================================================ // Static Methods //================================================================================ /** * This convenience method swaps the columns at the given indexes in the given list. *

* As probably already explained in {@link VFXTable}, since columns are concrete nodes, there's the potential risk * of adding duplicates in the list which would lead to a JavaFX exception. This is the exact reason why * {@link Collections#swap(List, int, int)} cannot be used. What this method does internally, is to copy the columns * to a temporary array, swap the columns in that, and finally use {@link ObservableList#setAll(Object[])}. */ @SuppressWarnings({"rawtypes", "unchecked"}) public static void swapColumns(ObservableList>> columns, int i, int j) { VFXTableColumn[] arr = columns.toArray(VFXTableColumn[]::new); VFXTableColumn tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; columns.setAll(arr); } /** * Delegates to {@link #swapColumns(ObservableList, int, int)} by using {@link VFXTable#getColumns()}. */ public static void swapColumns(VFXTable table, int i, int j) { swapColumns(table.getColumns(), i, j); } //================================================================================ // Methods //================================================================================ private void initialize() { setCellFactory(defaultCellFactory()); getStyleClass().setAll(defaultStyleClasses()); setDefaultBehaviorProvider(); } /** * Sets the column's pref width to the given value and invokes a layout request by calling * {@link VFXTableManager#onColumnWidthChanged(VFXTableColumn)}. */ public void resize(double width) { setPrefWidth(width); VFXTable table = getTable(); if (table == null) return; table.getBehavior().onColumnWidthChanged(this); } /** * Responsible for creating the cells' cache instance used by this column. * * @see VFXCellsCache * @see #cellsCacheCapacityProperty() */ protected VFXCellsCache createCache() { return new VFXCellsCache<>(cellFactory, getCellsCacheCapacity()); } /** * @return the default function used to build cells. Uses {@link VFXSimpleTableCell}. */ @SuppressWarnings("unchecked") protected Function defaultCellFactory() { return t -> (C) new VFXSimpleTableCell<>(t, t1 -> (t1 != null) ? Function.identity() : null); } /** * This core method is responsible for telling the table to update its state when a column changes its cell factory. * Since every column has its own cell factory property, it would be too inconvenient to handle such case on the table * side. Rather, this column implementation is responsible for communicating it to the table's manager by calling * {@link VFXTableManager#onCellFactoryChanged(VFXTableColumn)}. (automatically called by the property!) *

* This will also cause the cells' cache to clear and dispose all cached cells built by the previous factory. */ @SuppressWarnings("unchecked") protected void onCellFactoryChanged(Function newFactory) { VFXTable table = getTable(); if (table == null) return; VFXTableManager manager = table.getBehavior(); manager.onCellFactoryChanged((VFXTableColumn>) this); cache.clear(); } //================================================================================ // Overridden Methods //================================================================================ // Public to allow bypassing the pref width cache which is used by the drag resizer // Useful to autosize columns /** * {@inheritDoc} *

* Overridden to make it public. Since we use the {@link #prefWidthProperty()} to store the width we'd like the * column to have, and for how JavaFX works, invoking {@link #prefWidth(double)} would return a cached value which * may not be what we want. In fact, this is useful for example when we have to autosize the column as this method * will ignore such cached value and return a computed value depending solely on its content. */ @Override public double computePrefWidth(double height) { return super.computePrefWidth(height); } @Override public Supplier> defaultBehaviorProvider() { return () -> new VFXTableColumnBehavior<>(this); } @Override public List defaultStyleClasses() { return List.of("vfx-column"); } //================================================================================ // Styleable Properties //================================================================================ private final StyleableIntegerProperty cellsCacheCapacity = new StyleableIntegerProperty( StyleableProperties.CELLS_CACHE_CAPACITY, this, "cellsCacheCapacity", 10 ) { @Override protected void invalidated() { cache.setCapacity(get()); } }; private final StyleableBooleanProperty gestureResizable = new StyleableBooleanProperty( StyleableProperties.GESTURE_RESIZABLE, this, "gestureResizable", true ); public int getCellsCacheCapacity() { return cellsCacheCapacity.get(); } /** * Specifies the maximum number of cells the cache can contain at any time. Excess will not be added to the queue and * disposed immediately. *

* Can be set in CSS via the property: '-vfx-cells-cache-capacity'. */ public StyleableIntegerProperty cellsCacheCapacityProperty() { return cellsCacheCapacity; } public void setCellsCacheCapacity(int cellsCacheCapacity) { this.cellsCacheCapacity.set(cellsCacheCapacity); } public boolean isGestureResizable() { return gestureResizable.get(); } /** * The default behavior {@link VFXTableColumnBehavior} allows to resize the column at runtime via gestures. * This property is used to enable or disable such feature. *

* This is also settable via CSS with the "-vfx-resizable" property. */ public StyleableBooleanProperty gestureResizableProperty() { return gestureResizable; } public void setGestureResizable(boolean gestureResizable) { this.gestureResizable.set(gestureResizable); } //================================================================================ // CssMetaData //================================================================================ private static class StyleableProperties { private static final StyleablePropertyFactory> FACTORY = new StyleablePropertyFactory<>(Labeled.getClassCssMetaData()); private static final List> cssMetaDataList; private static final CssMetaData, Number> CELLS_CACHE_CAPACITY = FACTORY.createSizeCssMetaData( "-vfx-cells-cache-capacity", VFXTableColumn::cellsCacheCapacityProperty, 10 ); private static final CssMetaData, Boolean> GESTURE_RESIZABLE = FACTORY.createBooleanCssMetaData( "-vfx-resizable", VFXTableColumn::gestureResizableProperty, true ); static { cssMetaDataList = StyleUtils.cssMetaDataList( Labeled.getClassCssMetaData(), CELLS_CACHE_CAPACITY, GESTURE_RESIZABLE ); } } public static List> getClassCssMetaData() { return StyleableProperties.cssMetaDataList; } @Override public List> getControlCssMetaData() { return getClassCssMetaData(); } //================================================================================ // Getters/Setters //================================================================================ /** * @return the cells' cache instance used by this column */ protected VFXCellsCache cache() { return cache; } /** * Delegate for {@link VFXCellsCache#populate()}. */ public void populateCache() { cache.populate(); } /** * Delegate for {@link VFXCellsCache#size()}. */ public int cacheSize() { return cache.size(); } public VFXTable getTable() { return table.get(); } /** * Specifies the table's instance this column belongs to. * The value will be {@code null} if the column is not yet in the scene graph or if the column is not part of any table. */ public ReadOnlyObjectProperty> tableProperty() { return table.getReadOnlyProperty(); } protected void setTable(VFXTable table) { this.table.set(table); } public int getIndex() { return index.get(); } /** * Specifies the index of the columns in the list {@link VFXTable#getColumns()}. * The value will be -1 if the property has not been updated yet or if the column is not in a table. *

* This method will be reliable 99% of the time, however, just to be sure I suggest you to use * {@link VFXTable#indexOf(VFXTableColumn)} instead. */ public ReadOnlyIntegerProperty indexProperty() { return index.getReadOnlyProperty(); } protected void setIndex(int index) { this.index.set(index); } /** * Specifies the function used to build the cells. * See also {@link #defaultCellFactory()}. */ @Override public CellFactory getCellFactory() { return cellFactory; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy