io.github.palexdev.mfxcore.collections.ObservableGrid Maven / Gradle / Ivy
Show all versions of materialfx-all Show documentation
/*
* Copyright (C) 2022 Parisi Alessandro - [email protected]
* This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
*
* MaterialFX 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.
*
* MaterialFX 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 MaterialFX. If not, see .
*/
package io.github.palexdev.mfxcore.collections;
import io.github.palexdev.mfxcore.enums.GridChangeType;
import io.github.palexdev.mfxcore.utils.GridUtils;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
/**
* Extension of {@link Grid} to provide observables capabilities.
* Note though that since JavaFX's implementation of listeners/observables and such is utter garbage because of private
* APIs, complex class structures and others unhappy design decisions... The observability of this data structure is
* implemented through an {@link ObjectProperty} that always contains the last {@link Change} occurred.
*
* Instead of implementing {@link Observable}, this implements {@link ObservableValue} to also get {@link ChangeListener}
* capabilities. Note though that the relevant value carried by the listener is the {@code newValue} as once a {@link Change}
* has been processed it should be disposed with {@link Change#endChange()} (this will reset the property to a useless state)
*/
public class ObservableGrid extends Grid implements ObservableValue> {
//================================================================================
// Properties
//================================================================================
private final ObjectProperty> change = new SimpleObjectProperty<>() {
@Override
protected void fireValueChangedEvent() {
if (get() == Change.EMPTY) return;
super.fireValueChangedEvent();
}
};
//================================================================================
// Constructors
//================================================================================
public ObservableGrid() {
}
public ObservableGrid(int nRows, int nColumns) {
super(nRows, nColumns);
}
public ObservableGrid(List data, int nRows, int nColumns) {
super(data, nRows, nColumns);
}
//================================================================================
// Methods
//================================================================================
protected void registerChange(Change change) {
this.change.set(change);
}
//================================================================================
// Static Methods
//================================================================================
/**
* Given a list of items and the number of columns the grid will have,
* generates a new {@code Grid}.
* Every {@code columnsNum} the list is "sliced" in rows.
*
* @param list the data to generate the grid
* @param columnsNum the number of columns for the grid, also used to "slice" the given list in rows
*/
public static ObservableGrid fromList(List list, int columnsNum) {
if (list.isEmpty()) {
if (columnsNum == 0) return new ObservableGrid<>();
throw new IllegalArgumentException("List is empty, but cols is " + columnsNum);
}
if (list.size() % columnsNum != 0)
throw new IllegalArgumentException("List size must be a multiple of " + columnsNum);
return new ObservableGrid<>(list, list.size() / columnsNum, columnsNum);
}
/**
* Given an array of items and the number of columns the grid will have,
* generates a new {@code Grid}.
* Every {@code columnsNum} the array is "sliced" in rows.
*
* @param arr the data to generate the grid
* @param columnsNum the number of columns for the grid, also used to "slice" the given array in rows
*/
public static ObservableGrid fromArray(T[] arr, int columnsNum) {
return fromList(List.of(arr), columnsNum);
}
/**
* Given a 2D array of items generates a new {@code Grid}.
*
* @param matrix the data to generate the grid
*/
public static ObservableGrid fromMatrix(T[][] matrix) {
int rowsNum = matrix.length;
int columnsNum = matrix[0].length;
List tmp = new ArrayList<>();
for (T[] row : matrix) {
tmp.addAll(Arrays.asList(row));
}
return new ObservableGrid<>(tmp, rowsNum, columnsNum);
}
//================================================================================
// Overridden Methods
//================================================================================
@Override
public ObservableGrid init() {
List tmp = new ArrayList<>(getData());
super.init();
registerChange(
Change.of(this, GridChangeType.INIT)
.setStart(0)
.setEnd(totalSize())
.setStep(1)
.removed(tmp)
);
return this;
}
@Override
public ObservableGrid init(int rows, int columns) {
List tmp = new ArrayList<>(getData());
super.init(rows, columns);
registerChange(
Change.of(this, GridChangeType.INIT)
.setStart(0)
.setEnd(totalSize())
.setStep(1)
.removed(tmp)
);
return this;
}
@Override
public ObservableGrid init(int rows, int columns, T val) {
List tmp = new ArrayList<>(getData());
super.init(rows, columns, val);
registerChange(
Change.of(this, GridChangeType.INIT)
.setStart(0)
.setEnd(totalSize())
.setStep(1)
.removed(tmp)
.added(val)
);
return this;
}
@Override
public ObservableGrid init(int rows, int columns, BiFunction valFunction) {
if (rows == 0 || columns == 0) {
throw new IllegalStateException("Both rows num and columns num must be greater than 0 but they are "
+ rows + ", " + columns);
}
List tmp = new ArrayList<>(getData());
clear();
this.rowsNum = rows;
this.columnsNum = columns;
List added = new ArrayList<>();
for (int i = 0; i < totalSize(); i++) {
Coordinates rc = GridUtils.indToSub(columnsNum, i);
T val = valFunction.apply(rc.getRow(), rc.getColumn());
tmp.add(val);
data.add(val);
}
registerChange(
Change.of(this, GridChangeType.INIT)
.setStart(0)
.setEnd(totalSize())
.setStep(1)
.removed(tmp)
.added(added)
);
return this;
}
@Override
public void setElement(int index, T val) {
T rem = getElement(index);
super.setElement(index, val);
registerChange(
Change.of(this, GridChangeType.REPLACE_ELEMENT)
.setCoordinates(GridUtils.indToSub(getColumnsNum(), index))
.setStart(index)
.setEnd(index)
.setStep(1)
.removed(rem)
.added(val)
);
}
@Override
public void setDiagonal(List diag) {
List rem = getDiagonal();
super.setDiagonal(diag);
registerChange(
Change.of(this, GridChangeType.REPLACE_DIAGONAL)
.setStart(0)
.setEnd(totalSize())
.setStep(columnsNum + 1)
.removed(rem)
.added(diag)
);
}
@Override
public void addRow(int index, List row) {
super.addRow(index, row);
registerChange(
Change.of(this, GridChangeType.ADD_ROW)
.setCoordinates(index, -1)
.setStart(index * columnsNum)
.setEnd(index * columnsNum + row.size())
.setStep(1)
.added(row)
);
}
@Override
public void addColumn(int index, List column) {
super.addColumn(index, column);
registerChange(
Change.of(this, GridChangeType.ADD_COLUMN)
.setCoordinates(-1, index)
.setStart(index)
.setEnd(((rowsNum - 1) * columnsNum + index) + 1)
.setStep(columnsNum)
.added(column)
);
}
@Override
public void setRow(int index, List row) {
if (row.isEmpty())
throw new IllegalArgumentException("Row to set cannot be empty");
if (row.size() > columnsNum)
throw new IllegalArgumentException("Row size does not match, expecting " + columnsNum + ", but was " + row.size());
if (isEmpty() && index == 0) {
addRow(row);
return;
}
if (index < 0 || index > rowsNum)
throw new IndexOutOfBoundsException(index);
List tmp = getRow(index);
int start = index * columnsNum;
int j = 0;
for (int i = start; i < start + columnsNum; i++) {
data.set(i, row.get(j));
j++;
}
registerChange(
Change.of(this, GridChangeType.REPLACE_ROW)
.setCoordinates(index, -1)
.setStart(start)
.setEnd(start + columnsNum)
.setStep(1)
.removed(tmp)
.added(row)
);
}
@Override
public void setColumn(int index, List column) {
if (column.isEmpty())
throw new IllegalArgumentException("Column to set cannot be empty");
if (column.size() > rowsNum)
throw new IllegalArgumentException("Column size does not match, expecting " + rowsNum + ", but was " + column.size());
if (isEmpty() && index == 0) {
addColumn(column);
return;
}
if (index < 0 || index > columnsNum)
throw new IndexOutOfBoundsException(index);
List tmp = getColumn(index);
int end = (rowsNum - 1) * columnsNum + index;
int j = 0;
for (int i = index; i <= end; i += columnsNum) {
data.set(i, column.get(j));
j++;
}
registerChange(
Change.of(this, GridChangeType.REPLACE_COLUMN)
.setCoordinates(-1, index)
.setStart(index)
.setEnd(end + 1)
.setStep(columnsNum)
.removed(tmp)
.added(column)
);
}
@Override
public List removeRow(int index) {
List rem = super.removeRow(index);
registerChange(
Change.of(this, GridChangeType.REMOVE_ROW)
.setCoordinates(index, -1)
.setStart(index * columnsNum)
.setEnd(index * columnsNum + columnsNum)
.setStep(1)
.removed(rem)
);
return rem;
}
@Override
public List removeColumn(int index) {
List rem = super.removeColumn(index);
int columnsNum = this.columnsNum + 1;
registerChange(
Change.of(this, GridChangeType.REMOVE_COLUMN)
.setCoordinates(-1, index)
.setStart(index)
.setEnd(((rowsNum - 1) * columnsNum + index) + 1)
.setStep(columnsNum)
.removed(rem)
);
return rem;
}
@Override
public Grid transpose() {
Grid grid = super.transpose();
registerChange(
Change.of(this, GridChangeType.TRANSPOSE)
.setStart(0)
.setEnd(totalSize())
.setStep(1)
);
return grid;
}
@Override
public void clear() {
List tmp = new ArrayList<>(getData());
super.clear();
registerChange(
Change.of(this, GridChangeType.CLEAR)
.setStart(0)
.setEnd(totalSize())
.setStep(1)
.removed(tmp)
);
}
@Override
public void addListener(InvalidationListener listener) {
change.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
change.removeListener(listener);
}
@Override
public void addListener(ChangeListener super Change> listener) {
change.addListener(listener);
}
@Override
public void removeListener(ChangeListener super Change> listener) {
change.removeListener(listener);
}
/**
* @return the last {@link Change} occurred or {@link Change#EMPTY} if no change occurred
* or the last change was disposed with {@link Change#endChange()}
*/
@Override
public Change getValue() {
return change.get();
}
/**
* Delegate for {@link #getValue()}
*/
public Change getChange() {
return getValue();
}
//================================================================================
// Internal Classes
//================================================================================
/**
* Bean used to represent any type of change occurring in a {@link ObservableGrid} data structure.
*
* The {@code Change} brings a series of useful information like:
* - The grid's data after the change
*
- The change's type, see {@link GridChangeType}
*
- A list containing the added items
*
- A list containing the removed items
*
- The coordinates at which the change occurred. Note though that this information is not always available,
* see {@link #getCoordinates()}
*
- The linear index at which the change started (inclusive)
*
- The linear index at which the change ended (exclusive)
*
- The "step" which is an integer useful to iterate over the change's [start, end] range.
* When a change occurs on a row typically is 1, when it occurs on a column typically it is the number of
* columns of the grid before the change.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public static class Change {
public static final Change EMPTY = new Change();
private ObservableGrid grid;
private List data;
private final Pair size;
private final GridChangeType type;
private final List added = new ArrayList<>();
private final List removed = new ArrayList<>();
private Coordinates coordinates;
private int start;
private int end;
private int step;
private Change() {
this.data = List.of();
this.size = null;
this.type = null;
}
public Change(ObservableGrid grid, GridChangeType type) {
this.grid = grid;
this.data = Collections.unmodifiableList(grid.getData());
this.size = grid.size();
this.type = type;
}
public static Change of(ObservableGrid grid, GridChangeType type) {
return new ObservableGrid.Change<>(grid, type);
}
/**
* @return the {@link ObservableGrid} instance this change refers to
*/
protected ObservableGrid getGrid() {
return grid;
}
/**
* @return the grid's data as an unmodifiable {@link List}
*/
public List getData() {
return data;
}
/**
* @return the size of the grid as a {@link Pair} object. The key is the number of rows and
* the value is the number of columns
*/
public Pair getSize() {
return size;
}
/**
* @return the type of operation that lead to this change, see {@link GridChangeType}
*/
public GridChangeType getType() {
return type;
}
/**
* @return a {@link List} containing all the added elements. For "replacement" operations
* this will contain all the new items
*/
public List getAdded() {
return added;
}
/**
* @return a {@link List} containing all the removed elements. For "replacement" operations
* this will contain all the elements that have been replaced
*/
public List getRemoved() {
return removed;
}
@SafeVarargs
private Change added(T... data) {
Collections.addAll(added, data);
return this;
}
private Change added(List data) {
added.addAll(data);
return this;
}
@SafeVarargs
private Change removed(T... data) {
Collections.addAll(removed, data);
return this;
}
private Change removed(List data) {
removed.addAll(data);
return this;
}
/**
* @return a {@link Coordinates} object that specifies the row and column at which the change occurred.
* This information is available only for the following changes:
* - Set element
*
- Add row (column will be -1)
*
- Add column (row will be -1)
*
- Set row (column will be -1)
*
- Set column (row will be -1)
*
- Remove row (column will be -1)
*
- Remove column (row will be -1)
*/
public Coordinates getCoordinates() {
return coordinates;
}
private Change setCoordinates(Coordinates coordinates) {
this.coordinates = coordinates;
return this;
}
private Change setCoordinates(int row, int column) {
this.coordinates = Coordinates.of(row, column);
return this;
}
/**
* @return the linear index at which the change started
*/
public int getStart() {
return start;
}
private Change setStart(int start) {
this.start = start;
return this;
}
/**
* @return the linear index at which the change ended (exclusive)
*/
public int getEnd() {
return end;
}
private Change setEnd(int end) {
this.end = end;
return this;
}
/**
* @return an integer useful to iterate over the change's [start, end] range.
* When a change occurs on a row typically is 1, when it occurs on a column typically it is the number of
* columns of the grid before the change
*/
public int step() {
return step;
}
private Change setStep(int step) {
this.step = step;
return this;
}
/**
* Disposes this change and resets the grid's property responsible for holding new changes.
*/
public void endChange() {
grid.registerChange(EMPTY);
grid = null;
data = null;
added.clear();
removed.clear();
}
}
}