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:
a2geek committed 2026-10-06 13:50:20 -05:00
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);
@@ -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) {