mirror of
https://github.com/AppleCommander/AppleCommander.git
synced 2026-10-24 08:02:19 +00:00
Adding a data type classification to the display columns and applying renderers to the file view. There has been a lingering sort issue where prior disks somehow come into play when a column is sorted. Resolved it by entirely tossing the old TableView away and replacing with the new one.
This commit is contained in:
1 parent
1d8b132c9a
commit
f2de1481ac
3 files changed
+172
-110
No files matched your search
@@ -19,10 +19,7 @@
|
||||
*/
|
||||
package org.applecommander.javafx;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -59,10 +56,18 @@ import static org.applecommander.javafx.FxUtils.*;
|
||||
public class FileView extends BorderPane {
|
||||
private final FileStoreWindow fileStoreWindow;
|
||||
private final HBox breadcrumbBar;
|
||||
private final TableView<FileEntry> fileTable;
|
||||
private ToggleButton nativeToolButton;
|
||||
private ToggleButton detailToolButton;
|
||||
private ToggleButton deletedFilesToggleButton;
|
||||
private final ToggleButton nativeToolButton;
|
||||
private final ToggleButton detailToolButton;
|
||||
private final ToggleButton deletedFilesToggleButton;
|
||||
|
||||
// Note that the TableView has a lingering data issue that I couldn't resolve.
|
||||
// Therefore, it gets tossed and recreated as needed. The primary issue is that
|
||||
// it hangs on to the old items and tries to access values with the new item
|
||||
// converters. So if GameDOS was loaded and then a Zip was loaded into the table,
|
||||
// a sort caused an exception because something tried to render a GameDOS file
|
||||
// entry with the Zip renderer. But only when a sort was applied. Clearing out
|
||||
// every property didn't seem to help. Thus this extreme solution. Please fix!
|
||||
private TableView<FileEntry> fileTable;
|
||||
|
||||
private final ObservableList<Directory> directoryPath = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Directory> selectedDirectory = new SimpleObjectProperty<>();
|
||||
@@ -88,6 +93,81 @@ public class FileView extends BorderPane {
|
||||
breadcrumbBar.setAlignment(Pos.CENTER_LEFT);
|
||||
setTop(breadcrumbBar);
|
||||
|
||||
createNewFileTable();
|
||||
|
||||
nativeToolButton.visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES));
|
||||
nativeToolButton.managedProperty().bind(nativeToolButton.visibleProperty());
|
||||
detailToolButton.visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES));
|
||||
detailToolButton.managedProperty().bind(detailToolButton.visibleProperty());
|
||||
deletedFilesToggleButton.visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES));
|
||||
deletedFilesToggleButton.managedProperty().bind(deletedFilesToggleButton.visibleProperty());
|
||||
deletedFilesToggleButton.disableProperty().bind(supportsFileDeletion.not());
|
||||
|
||||
breadcrumbBar.visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES).and(supportsDirectories));
|
||||
breadcrumbBar.managedProperty().bind(breadcrumbBar.visibleProperty());
|
||||
|
||||
visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES));
|
||||
|
||||
fileStoreWindow.fileStoreSelection().selectedItemProperty().addListener((_, _, newValue) -> {
|
||||
if (newValue != null) {
|
||||
directoryPath.clear();
|
||||
directoryPath.add(newValue.getRootDirectory());
|
||||
listingMode.setValue(DisplayColumn.Mode.NATIVE);
|
||||
supportsDirectories.setValue(newValue.can(Capability.SUPPORTS_DIRECTORIES));
|
||||
supportsFileDeletion.setValue(newValue.can(Capability.DELETE_FILES));
|
||||
}
|
||||
});
|
||||
|
||||
fileStoreWindow.viewModeProperty().addListener((_, _, _) -> populateDiskRows());
|
||||
|
||||
listingMode.addListener((_, _, newValue) -> {
|
||||
nativeToolButton.setSelected(newValue == DisplayColumn.Mode.NATIVE);
|
||||
detailToolButton.setSelected(newValue == DisplayColumn.Mode.DETAIL);
|
||||
fileTable.getColumns().forEach(column -> {
|
||||
if (column.getUserData() instanceof DisplayColumn displayColumn) {
|
||||
column.visibleProperty().setValue(displayColumn.supports(newValue));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
directoryPath.addListener((ListChangeListener<? super Directory>) change -> {
|
||||
breadcrumbBar.getChildren().clear();
|
||||
if (fileStoreWindow.fileStoreSelection().getSelectedItem() == null) {
|
||||
return;
|
||||
}
|
||||
String pathSeparator = fileStoreWindow.fileStoreSelection().getSelectedItem().getPathSeparator();
|
||||
|
||||
Label pathLabel = new Label("Path:");
|
||||
breadcrumbBar.getChildren().add(pathLabel);
|
||||
|
||||
for (int i = 0; i < directoryPath.size(); i++) {
|
||||
Directory dir = directoryPath.get(i);
|
||||
// FIXME this is because ProdosFormatDisk uses "/DISK.NAME/"
|
||||
Button crumb = new Button(dir.getName().replace("/", ""));
|
||||
final int index = i;
|
||||
crumb.setOnAction(event -> {
|
||||
List<Directory> newPath = new ArrayList<>(directoryPath.subList(0, index + 1));
|
||||
directoryPath.clear();
|
||||
directoryPath.addAll(newPath);
|
||||
populateDiskRows();
|
||||
});
|
||||
Label separator = new Label(pathSeparator);
|
||||
breadcrumbBar.getChildren().add(separator);
|
||||
breadcrumbBar.getChildren().add(crumb);
|
||||
}
|
||||
selectedDirectory.set(directoryPath.isEmpty() ? null : directoryPath.getLast());
|
||||
});
|
||||
|
||||
selectedDirectory.addListener((_, _, newValue) -> {
|
||||
populateDiskRows();
|
||||
});
|
||||
}
|
||||
|
||||
public void createNewFileTable() {
|
||||
if (fileTable != null) {
|
||||
fileTable.visibleProperty().unbind();
|
||||
}
|
||||
|
||||
fileTable = new TableView<>();
|
||||
VBox placeholder = new VBox(new Label("This image has no files."));
|
||||
placeholder.setAlignment(Pos.CENTER);
|
||||
@@ -146,73 +226,7 @@ public class FileView extends BorderPane {
|
||||
event.consume();
|
||||
});
|
||||
|
||||
nativeToolButton.visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES));
|
||||
nativeToolButton.managedProperty().bind(nativeToolButton.visibleProperty());
|
||||
detailToolButton.visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES));
|
||||
detailToolButton.managedProperty().bind(detailToolButton.visibleProperty());
|
||||
deletedFilesToggleButton.visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES));
|
||||
deletedFilesToggleButton.managedProperty().bind(deletedFilesToggleButton.visibleProperty());
|
||||
deletedFilesToggleButton.disableProperty().bind(supportsFileDeletion.not());
|
||||
|
||||
breadcrumbBar.visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES).and(supportsDirectories));
|
||||
breadcrumbBar.managedProperty().bind(breadcrumbBar.visibleProperty());
|
||||
|
||||
visibleProperty().bind(fileStoreWindow.viewModeProperty().isEqualTo(ViewMode.FILES));
|
||||
managedProperty().bind(fileTable.visibleProperty());
|
||||
|
||||
fileStoreWindow.fileStoreSelection().selectedItemProperty().addListener((_, _, newValue) -> {
|
||||
if (newValue != null) {
|
||||
directoryPath.clear();
|
||||
directoryPath.add(newValue.getRootDirectory());
|
||||
listingMode.setValue(DisplayColumn.Mode.NATIVE);
|
||||
supportsDirectories.setValue(newValue.can(Capability.SUPPORTS_DIRECTORIES));
|
||||
supportsFileDeletion.setValue(newValue.can(Capability.DELETE_FILES));
|
||||
}
|
||||
});
|
||||
|
||||
fileStoreWindow.viewModeProperty().addListener((_, _, _) -> populateDiskRows());
|
||||
|
||||
listingMode.addListener((_, _, newValue) -> {
|
||||
nativeToolButton.setSelected(newValue == DisplayColumn.Mode.NATIVE);
|
||||
detailToolButton.setSelected(newValue == DisplayColumn.Mode.DETAIL);
|
||||
fileTable.getColumns().forEach(column -> {
|
||||
if (column.getUserData() instanceof DisplayColumn displayColumn) {
|
||||
column.visibleProperty().setValue(displayColumn.supports(newValue));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
directoryPath.addListener((ListChangeListener<? super Directory>) change -> {
|
||||
breadcrumbBar.getChildren().clear();
|
||||
if (fileStoreWindow.fileStoreSelection().getSelectedItem() == null) {
|
||||
return;
|
||||
}
|
||||
String pathSeparator = fileStoreWindow.fileStoreSelection().getSelectedItem().getPathSeparator();
|
||||
|
||||
Label pathLabel = new Label("Path:");
|
||||
breadcrumbBar.getChildren().add(pathLabel);
|
||||
|
||||
for (int i = 0; i < directoryPath.size(); i++) {
|
||||
Directory dir = directoryPath.get(i);
|
||||
// FIXME this is because ProdosFormatDisk uses "/DISK.NAME/"
|
||||
Button crumb = new Button(dir.getName().replace("/", ""));
|
||||
final int index = i;
|
||||
crumb.setOnAction(event -> {
|
||||
List<Directory> newPath = new ArrayList<>(directoryPath.subList(0, index + 1));
|
||||
directoryPath.clear();
|
||||
directoryPath.addAll(newPath);
|
||||
populateDiskRows();
|
||||
});
|
||||
Label separator = new Label(pathSeparator);
|
||||
breadcrumbBar.getChildren().add(separator);
|
||||
breadcrumbBar.getChildren().add(crumb);
|
||||
}
|
||||
selectedDirectory.set(directoryPath.isEmpty() ? null : directoryPath.getLast());
|
||||
});
|
||||
|
||||
selectedDirectory.addListener((_, _, newValue) -> {
|
||||
populateDiskRows();
|
||||
});
|
||||
}
|
||||
|
||||
public void bindScene(Scene scene) {
|
||||
@@ -228,11 +242,11 @@ public class FileView extends BorderPane {
|
||||
selectedDirectory.set(null);
|
||||
listingMode.setValue(DisplayColumn.Mode.NATIVE);
|
||||
deletedFilesToggleButton.setSelected(false);
|
||||
createNewFileTable();
|
||||
fileTable.setItems(FXCollections.emptyObservableList());
|
||||
fileTable.getColumns().clear();
|
||||
}
|
||||
|
||||
|
||||
private void selectNativeView() {
|
||||
listingMode.set(DisplayColumn.Mode.NATIVE);
|
||||
}
|
||||
@@ -242,33 +256,70 @@ public class FileView extends BorderPane {
|
||||
}
|
||||
|
||||
private void populateDiskRows() {
|
||||
fileTable.getColumns().clear();
|
||||
if (selectedDirectory.isNull().get()) {
|
||||
// No directories, leave a cleared list. Likely in transition.
|
||||
return;
|
||||
}
|
||||
|
||||
createNewFileTable();
|
||||
Directory directory = selectedDirectory.get();
|
||||
List<DisplayColumn> displayColumns = directory.getFileStore().getDisplayColumns();
|
||||
|
||||
boolean editable = false;
|
||||
List<TableColumn<FileEntry,?>> tableColumns = new ArrayList<>();
|
||||
for (final DisplayColumn displayColumn : displayColumns) {
|
||||
TableColumn<FileEntry,String> column = new TableColumn<>(displayColumn.headerText());
|
||||
column.setUserData(displayColumn);
|
||||
if (displayColumn.editInline()) {
|
||||
column.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
column.setEditable(true);
|
||||
column.setCellValueFactory(cell -> {
|
||||
SimpleStringProperty property = new SimpleStringProperty(displayColumn.formatAsText(cell.getValue()));
|
||||
property.addListener((_, _, newValue) -> {
|
||||
displayColumn.setValueFn().accept(cell.getValue(), newValue);
|
||||
fileStoreWindow.hasChangedProperty().set(true);
|
||||
TableColumn<FileEntry,?> column = switch (displayColumn.dataType()) {
|
||||
case STRING -> {
|
||||
TableColumn<FileEntry,String> stringColumn = new TableColumn<>(displayColumn.headerText());
|
||||
if (displayColumn.editInline()) {
|
||||
stringColumn.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
stringColumn.setEditable(true);
|
||||
stringColumn.setCellValueFactory(cell -> {
|
||||
SimpleStringProperty property = new SimpleStringProperty(displayColumn.formatAsText(cell.getValue()));
|
||||
property.addListener((_, _, newValue) -> {
|
||||
displayColumn.setValueFn().accept(cell.getValue(), newValue);
|
||||
fileStoreWindow.hasChangedProperty().set(true);
|
||||
});
|
||||
return property;
|
||||
});
|
||||
editable = true;
|
||||
} else {
|
||||
stringColumn.setCellValueFactory(cell ->
|
||||
new SimpleStringProperty(displayColumn.formatAsText(cell.getValue())));
|
||||
}
|
||||
yield stringColumn;
|
||||
}
|
||||
case INTEGER,LONG,DOUBLE -> {
|
||||
TableColumn<FileEntry,Number> numberColumn = new TableColumn<>(displayColumn.headerText());
|
||||
numberColumn.setCellFactory(c -> new TableCell<>() {
|
||||
@Override
|
||||
protected void updateItem(Number item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(String.format(displayColumn.fmt(), item));
|
||||
}
|
||||
}
|
||||
});
|
||||
return property;
|
||||
});
|
||||
} else {
|
||||
column.setCellValueFactory(cell ->
|
||||
new SimpleStringProperty(displayColumn.formatAsText(cell.getValue())));
|
||||
}
|
||||
switch (displayColumn.dataType()) {
|
||||
case INTEGER:
|
||||
numberColumn.setCellValueFactory(cell ->
|
||||
new SimpleIntegerProperty((Integer)displayColumn.getValueFn().apply(cell.getValue())));
|
||||
break;
|
||||
case LONG:
|
||||
numberColumn.setCellValueFactory(cell ->
|
||||
new SimpleLongProperty((Long)displayColumn.getValueFn().apply(cell.getValue())));
|
||||
break;
|
||||
case DOUBLE:
|
||||
numberColumn.setCellValueFactory(cell ->
|
||||
new SimpleDoubleProperty((Double)displayColumn.getValueFn().apply(cell.getValue())));
|
||||
break;
|
||||
}
|
||||
yield numberColumn;
|
||||
}
|
||||
};
|
||||
column.setUserData(displayColumn);
|
||||
column.visibleProperty().setValue(displayColumn.supports(listingMode.get()));
|
||||
if (displayColumn.alignment() == DisplayColumn.Alignment.RIGHT) {
|
||||
column.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
@@ -277,9 +328,9 @@ public class FileView extends BorderPane {
|
||||
} else {
|
||||
column.setStyle("-fx-alignment: CENTER-LEFT;");
|
||||
}
|
||||
fileTable.getColumns().add(column);
|
||||
tableColumns.add(column);
|
||||
}
|
||||
fileTable.setEditable(true);
|
||||
fileTable.setEditable(editable);
|
||||
fileTable.setOnKeyPressed(event -> {
|
||||
if (event.getCode() == KeyCode.F2) {
|
||||
TableView.TableViewFocusModel<FileEntry> focusModel = fileTable.getFocusModel();
|
||||
@@ -296,9 +347,10 @@ public class FileView extends BorderPane {
|
||||
|
||||
ObservableList<FileEntry> rowList = FXCollections.observableArrayList(rows);
|
||||
SortedList<FileEntry> sortedRows = new SortedList<>(rowList);
|
||||
sortedRows.comparatorProperty().bind(fileTable.comparatorProperty());
|
||||
fileTable.setItems(sortedRows);
|
||||
fileTable.getSortOrder().clear();
|
||||
fileTable.setItems(sortedRows);
|
||||
fileTable.getColumns().addAll(tableColumns);
|
||||
sortedRows.comparatorProperty().bind(fileTable.comparatorProperty());
|
||||
// If selection is bound, the ToolButton crashes and burns, so need to manage it manually.
|
||||
nativeToolButton.setSelected(listingMode.get() == DisplayColumn.Mode.NATIVE);
|
||||
detailToolButton.setSelected(listingMode.get() == DisplayColumn.Mode.DETAIL);
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ public class DiskFileStoreAdapter implements FileStore {
|
||||
throw new RuntimeException("Unexpected file entry type: " + entry.getClass().getName());
|
||||
};
|
||||
DisplayColumn displayColumn = new DisplayColumn(header.getTitle(),
|
||||
alignment, mappingFn::apply, null, false, "%s", mode);
|
||||
alignment, DisplayColumn.DataType.STRING, mappingFn::apply, null, false, "%s", mode);
|
||||
columns.add(displayColumn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import java.util.function.Function;
|
||||
/// A DisplayColumn supports formatted output from various FileStores in a
|
||||
/// dynamic fashion. The intent is to provide a flexible display interface
|
||||
/// that does not rely on hard-coded implementations.
|
||||
public record DisplayColumn(String headerText, Alignment alignment,
|
||||
public record DisplayColumn(String headerText, Alignment alignment, DataType dataType,
|
||||
Function<FileEntry,Object> getValueFn, BiConsumer<FileEntry,Object> setValueFn,
|
||||
boolean editInline, String fmt, Mode... modes) {
|
||||
|
||||
@@ -61,6 +61,16 @@ public record DisplayColumn(String headerText, Alignment alignment,
|
||||
RIGHT
|
||||
}
|
||||
|
||||
/// Indicates the data type to help with user interfaces.
|
||||
/// (Handling this via typing got too twisty to follow, and it was abandoned.)
|
||||
/// Note that Date and FileTime are currently converted to Strings.
|
||||
public enum DataType {
|
||||
STRING,
|
||||
INTEGER,
|
||||
LONG,
|
||||
DOUBLE
|
||||
}
|
||||
|
||||
public static <T extends FileEntry> Builder<T> builder(Class<T> clazz) {
|
||||
return new Builder<>(clazz);
|
||||
}
|
||||
@@ -78,52 +88,52 @@ public record DisplayColumn(String headerText, Alignment alignment,
|
||||
}
|
||||
|
||||
/// A helper method that allows a short-cut of 0 modes meaning ALL modes.
|
||||
private Builder<T> add(String name, Alignment alignment,
|
||||
private Builder<T> add(String name, Alignment alignment, DataType dataType,
|
||||
Function<FileEntry,Object> getValueFn, BiConsumer<FileEntry,Object> setValueFn,
|
||||
boolean editInline, String fmt, Mode... modes) {
|
||||
if (modes.length == 0) {
|
||||
modes = new Mode[] { Mode.NATIVE, Mode.DETAIL };
|
||||
}
|
||||
columns.add(new DisplayColumn(name, alignment, getValueFn, setValueFn, editInline, fmt, modes));
|
||||
columns.add(new DisplayColumn(name, alignment, dataType, getValueFn, setValueFn, editInline, fmt, modes));
|
||||
return this;
|
||||
}
|
||||
private Builder<T> add(String name, Alignment alignment, Function<FileEntry,Object> getValueFn,
|
||||
private Builder<T> add(String name, Alignment alignment, DataType dataType, Function<FileEntry,Object> getValueFn,
|
||||
String fmt, Mode... modes) {
|
||||
return add(name, alignment, getValueFn, null, false, fmt, modes);
|
||||
return add(name, alignment, dataType, getValueFn, null, false, fmt, modes);
|
||||
}
|
||||
|
||||
public Builder<T> addIntField(String name, Function<T,Integer> valueFn, Mode ...modes) {
|
||||
return addIntField(name, valueFn, "%d", modes);
|
||||
}
|
||||
public Builder<T> addIntField(String name, Function<T,Integer> valueFn, String fmt, Mode ...modes) {
|
||||
return add(name, Alignment.RIGHT, entry -> convert(entry, valueFn), fmt, modes);
|
||||
return add(name, Alignment.RIGHT, DataType.INTEGER, entry -> convert(entry, valueFn), fmt, modes);
|
||||
}
|
||||
public Builder<T> addStringField(String name, Function<T,String> getValueFn, BiConsumer<T,String> setValueFn,
|
||||
boolean editInline, Mode ...modes) {
|
||||
return add(name, Alignment.LEFT, entry -> convert(entry, getValueFn),
|
||||
return add(name, Alignment.LEFT, DataType.STRING, entry -> convert(entry, getValueFn),
|
||||
(entry,value) -> convert(entry, setValueFn, String.class, value),
|
||||
editInline, "%s", modes);
|
||||
}
|
||||
public Builder<T> addStringField(String name, Function<T,String> valueFn, Mode ...modes) {
|
||||
return add(name, Alignment.LEFT, entry -> convert(entry, valueFn), "%s", modes);
|
||||
return add(name, Alignment.LEFT, DataType.STRING, entry -> convert(entry, valueFn), "%s", modes);
|
||||
}
|
||||
public Builder<T> addStringField(String name, Alignment alignment, Function<T,String> valueFn, Mode ...modes) {
|
||||
return add(name, alignment, entry -> convert(entry, valueFn), "%s", modes);
|
||||
return add(name, alignment, DataType.STRING, entry -> convert(entry, valueFn), "%s", modes);
|
||||
}
|
||||
public Builder<T> addPercentField(String name, Function<T,Number> numeratorFn, Function<T,Number> denominatorFn, String fmt, Mode ...modes) {
|
||||
return add(name, Alignment.RIGHT, entry -> percentage(entry, numeratorFn, denominatorFn), fmt, modes);
|
||||
return add(name, Alignment.RIGHT, DataType.DOUBLE, entry -> percentage(entry, numeratorFn, denominatorFn), fmt, modes);
|
||||
}
|
||||
public Builder<T> addFileTimeField(String name, Function<T,FileTime> valueFn, Mode ...modes) {
|
||||
return add(name, Alignment.CENTER, entry -> formatFileTime(entry, valueFn), "%s", modes);
|
||||
return add(name, Alignment.CENTER, DataType.STRING, entry -> formatFileTime(entry, valueFn), "%s", modes);
|
||||
}
|
||||
public Builder<T> addLongField(String name, Function<T,Long> valueFn, Mode ...modes) {
|
||||
return addLongField(name, valueFn, "%d", modes);
|
||||
}
|
||||
public Builder<T> addLongField(String name, Function<T,Long> valueFn, String fmt, Mode ...modes) {
|
||||
return add(name, Alignment.RIGHT, entry -> convert(entry, valueFn), fmt, modes);
|
||||
return add(name, Alignment.RIGHT, DataType.LONG, entry -> convert(entry, valueFn), fmt, modes);
|
||||
}
|
||||
public Builder<T> addDateField(String name, Function<T,Date> valueFn, Mode ...modes) {
|
||||
return add(name, Alignment.CENTER, entry -> formatDate(entry, valueFn), "%s", modes);
|
||||
return add(name, Alignment.CENTER, DataType.STRING, entry -> formatDate(entry, valueFn), "%s", modes);
|
||||
}
|
||||
|
||||
private <S> S convert(FileEntry fileEntry, Function<T,S> valueFn) {
|
||||
|
||||
Reference in new issue
Block a user