From f2de1481ac1a67f9830e12da989fa6abd4dd8001 Mon Sep 17 00:00:00 2001 From: a2geek Date: Tue, 6 Oct 2026 13:50:20 -0500 Subject: [PATCH] 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. --- .../org/applecommander/javafx/FileView.java | 244 +++++++++++------- .../storage/DiskFileStoreAdapter.java | 2 +- .../filestore/DisplayColumn.java | 36 ++- 3 files changed, 172 insertions(+), 110 deletions(-) diff --git a/app/gui-javafx/src/main/java/org/applecommander/javafx/FileView.java b/app/gui-javafx/src/main/java/org/applecommander/javafx/FileView.java index 60e3eecc..d90dbf4d 100644 --- a/app/gui-javafx/src/main/java/org/applecommander/javafx/FileView.java +++ b/app/gui-javafx/src/main/java/org/applecommander/javafx/FileView.java @@ -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 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 fileTable; private final ObservableList directoryPath = FXCollections.observableArrayList(); private final ObjectProperty 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) 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 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) 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 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 displayColumns = directory.getFileStore().getDisplayColumns(); + boolean editable = false; + List> tableColumns = new ArrayList<>(); for (final DisplayColumn displayColumn : displayColumns) { - TableColumn 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 column = switch (displayColumn.dataType()) { + case STRING -> { + TableColumn 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 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 focusModel = fileTable.getFocusModel(); @@ -296,9 +347,10 @@ public class FileView extends BorderPane { ObservableList rowList = FXCollections.observableArrayList(rows); SortedList 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); diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskFileStoreAdapter.java b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskFileStoreAdapter.java index 9fb6a5ed..dd2ea85d 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskFileStoreAdapter.java +++ b/lib/ac-api/src/main/java/com/webcodepro/applecommander/storage/DiskFileStoreAdapter.java @@ -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); } } diff --git a/lib/ac-api/src/main/java/org/applecommander/filestore/DisplayColumn.java b/lib/ac-api/src/main/java/org/applecommander/filestore/DisplayColumn.java index 7c12be98..79e3bd0c 100644 --- a/lib/ac-api/src/main/java/org/applecommander/filestore/DisplayColumn.java +++ b/lib/ac-api/src/main/java/org/applecommander/filestore/DisplayColumn.java @@ -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 getValueFn, BiConsumer 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 Builder builder(Class 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 add(String name, Alignment alignment, + private Builder add(String name, Alignment alignment, DataType dataType, Function getValueFn, BiConsumer 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 add(String name, Alignment alignment, Function getValueFn, + private Builder add(String name, Alignment alignment, DataType dataType, Function 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 addIntField(String name, Function valueFn, Mode ...modes) { return addIntField(name, valueFn, "%d", modes); } public Builder addIntField(String name, Function 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 addStringField(String name, Function getValueFn, BiConsumer 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 addStringField(String name, Function 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 addStringField(String name, Alignment alignment, Function 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 addPercentField(String name, Function numeratorFn, Function 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 addFileTimeField(String name, Function 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 addLongField(String name, Function valueFn, Mode ...modes) { return addLongField(name, valueFn, "%d", modes); } public Builder addLongField(String name, Function 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 addDateField(String name, Function 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 convert(FileEntry fileEntry, Function valueFn) {